aicert-pro 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/PKG-INFO +2 -2
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/pyproject.toml +2 -2
- aicert_pro-0.1.1/src/aicert_pro/license.py +201 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/src/aicert_pro.egg-info/PKG-INFO +2 -2
- aicert_pro-0.1.0/src/aicert_pro/license.py +0 -144
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/README.md +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/setup.cfg +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/src/aicert_pro/__init__.py +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/src/aicert_pro/baseline.py +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/src/aicert_pro/cli.py +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/src/aicert_pro.egg-info/SOURCES.txt +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/src/aicert_pro.egg-info/dependency_links.txt +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/src/aicert_pro.egg-info/entry_points.txt +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/src/aicert_pro.egg-info/requires.txt +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/src/aicert_pro.egg-info/top_level.txt +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/tests/test_baseline.py +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/tests/test_cli.py +0 -0
- {aicert_pro-0.1.0 → aicert_pro-0.1.1}/tests/test_license.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aicert-pro
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Pro baseline regression enforcement for aicert CLI
|
|
5
|
-
Author-email: aicert <
|
|
5
|
+
Author-email: aicert <mfifth@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "aicert-pro"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "Pro baseline regression enforcement for aicert CLI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = {text = "MIT"}
|
|
12
12
|
authors = [
|
|
13
|
-
{name = "aicert", email = "
|
|
13
|
+
{name = "aicert", email = "mfifth@gmail.com"}
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
16
|
"typer>=0.9.0",
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""License verification for aicert-pro using Ed25519 signatures."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
11
|
+
from cryptography.hazmat.primitives import serialization
|
|
12
|
+
from cryptography.exceptions import InvalidSignature
|
|
13
|
+
|
|
14
|
+
# Embedded Ed25519 public key for license verification
|
|
15
|
+
LICENSE_PUBLIC_KEY = ed25519.Ed25519PublicKey.from_public_bytes(
|
|
16
|
+
base64.b64decode(
|
|
17
|
+
"PgOgheIjMd7RMHXb4vZ48sWjp2YY0aQGe2de3H2XJc4="
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
LICENSE_ENV_VAR = "AICERT_PRO_LICENSE"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LicenseError(Exception):
|
|
25
|
+
"""Base exception for license errors."""
|
|
26
|
+
|
|
27
|
+
exit_code = 5
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LicenseMissingError(LicenseError):
|
|
31
|
+
"""Raised when license is not found."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
super().__init__("License not found. Set AICERT_PRO_LICENSE environment variable.")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LicenseInvalidError(LicenseError):
|
|
38
|
+
"""Raised when license signature is invalid."""
|
|
39
|
+
|
|
40
|
+
def __init__(self) -> None:
|
|
41
|
+
super().__init__("License signature is invalid.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LicenseExpiredError(LicenseError):
|
|
45
|
+
"""Raised when license has expired."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, expired_at: str) -> None:
|
|
48
|
+
super().__init__(f"License expired at {expired_at}.")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class LicenseData:
|
|
53
|
+
"""License data parsed from base64-encoded JSON."""
|
|
54
|
+
|
|
55
|
+
license_id: str
|
|
56
|
+
owner: str
|
|
57
|
+
issued_at: str
|
|
58
|
+
expires_at: Optional[str] = None
|
|
59
|
+
features: Optional[list[str]] = None
|
|
60
|
+
signature: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_expired(self) -> bool:
|
|
64
|
+
"""Check if license is expired."""
|
|
65
|
+
if self.expires_at is None:
|
|
66
|
+
return False
|
|
67
|
+
try:
|
|
68
|
+
# Handle Unix timestamp (integer) or ISO format
|
|
69
|
+
if self.expires_at.isdigit():
|
|
70
|
+
from datetime import datetime
|
|
71
|
+
expires = datetime.fromtimestamp(int(self.expires_at), tz=timezone.utc)
|
|
72
|
+
return datetime.now(timezone.utc) > expires
|
|
73
|
+
else:
|
|
74
|
+
expires = datetime.fromisoformat(self.expires_at.replace("Z", "+00:00"))
|
|
75
|
+
return datetime.now(timezone.utc) > expires
|
|
76
|
+
except ValueError:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def verify_license(license_data: LicenseData, skip_signature: bool = False) -> bool:
|
|
81
|
+
"""Verify Ed25519 signature on license data.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
license_data: Parsed license data including signature.
|
|
85
|
+
skip_signature: If True, skip signature verification (for testing).
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
True if signature is valid.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
LicenseInvalidError: If signature verification fails.
|
|
92
|
+
LicenseExpiredError: If license has expired.
|
|
93
|
+
"""
|
|
94
|
+
if license_data.is_expired:
|
|
95
|
+
raise LicenseExpiredError(license_data.expires_at)
|
|
96
|
+
|
|
97
|
+
if skip_signature:
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
if license_data.signature is None:
|
|
101
|
+
raise LicenseInvalidError()
|
|
102
|
+
|
|
103
|
+
# Reconstruct the same payload JSON that was signed (must match issue_license.py)
|
|
104
|
+
payload_dict = {
|
|
105
|
+
"sub": license_data.license_id,
|
|
106
|
+
"plan": license_data.features[0] if license_data.features else "",
|
|
107
|
+
"issued_at": int(license_data.issued_at),
|
|
108
|
+
"expires_at": int(license_data.expires_at) if license_data.expires_at else None,
|
|
109
|
+
"version": 1,
|
|
110
|
+
}
|
|
111
|
+
# Remove None values
|
|
112
|
+
payload_dict = {k: v for k, v in payload_dict.items() if v is not None}
|
|
113
|
+
|
|
114
|
+
# Create canonical JSON bytes (same as canonical_json_bytes in issue_license.py)
|
|
115
|
+
import json
|
|
116
|
+
payload_bytes = json.dumps(payload_dict, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Handle base64url encoding (convert -_ to +/ and add padding)
|
|
120
|
+
signature = license_data.signature
|
|
121
|
+
signature = signature.replace("-", "+").replace("_", "/")
|
|
122
|
+
padding_needed = 4 - (len(signature) % 4)
|
|
123
|
+
if padding_needed != 4:
|
|
124
|
+
signature += "=" * padding_needed
|
|
125
|
+
signature_bytes = base64.b64decode(signature)
|
|
126
|
+
|
|
127
|
+
LICENSE_PUBLIC_KEY.verify(signature_bytes, payload_bytes)
|
|
128
|
+
except (InvalidSignature, ValueError) as e:
|
|
129
|
+
raise LicenseInvalidError()
|
|
130
|
+
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def read_license_from_env() -> str:
|
|
135
|
+
"""Read license string from environment variable.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Base64-encoded license string.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
LicenseMissingError: If environment variable is not set.
|
|
142
|
+
"""
|
|
143
|
+
license_str = os.environ.get(LICENSE_ENV_VAR)
|
|
144
|
+
if not license_str:
|
|
145
|
+
raise LicenseMissingError()
|
|
146
|
+
return license_str
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def require_pro() -> LicenseData:
|
|
150
|
+
"""Require and verify a valid pro license.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Parsed and verified license data.
|
|
154
|
+
|
|
155
|
+
Exits:
|
|
156
|
+
Exit code 5 if license is missing or invalid.
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
skip_signature = os.environ.get("AICERT_PRO_SKIP_SIGNATURE", "").lower() in ("1", "true", "yes")
|
|
160
|
+
|
|
161
|
+
license_str = read_license_from_env()
|
|
162
|
+
|
|
163
|
+
# Strip license prefix if present (e.g., "AICERT-1.")
|
|
164
|
+
if "." in license_str:
|
|
165
|
+
parts = license_str.split(".", 1)[1]
|
|
166
|
+
else:
|
|
167
|
+
parts = license_str
|
|
168
|
+
|
|
169
|
+
# Split into JSON payload and signature
|
|
170
|
+
if "." in parts:
|
|
171
|
+
json_b64, signature_b64 = parts.split(".", 1)
|
|
172
|
+
else:
|
|
173
|
+
json_b64 = parts
|
|
174
|
+
signature_b64 = None
|
|
175
|
+
|
|
176
|
+
# Handle base64url encoding in JSON (convert -_ to +/)
|
|
177
|
+
json_b64 = json_b64.replace("-", "+").replace("_", "/")
|
|
178
|
+
|
|
179
|
+
# Add base64 padding if necessary
|
|
180
|
+
padding_needed = 4 - (len(json_b64) % 4)
|
|
181
|
+
if padding_needed != 4:
|
|
182
|
+
json_b64 += "=" * padding_needed
|
|
183
|
+
|
|
184
|
+
import json
|
|
185
|
+
data = json.loads(base64.b64decode(json_b64))
|
|
186
|
+
|
|
187
|
+
# Map JSON fields to LicenseData (handle different field names)
|
|
188
|
+
license_data = LicenseData(
|
|
189
|
+
license_id=data.get("sub", data.get("license_id", "")),
|
|
190
|
+
owner=data.get("sub", data.get("owner", "")),
|
|
191
|
+
issued_at=str(data.get("issued_at", "")),
|
|
192
|
+
expires_at=str(data.get("expires_at", "")) if data.get("expires_at") else None,
|
|
193
|
+
features=[data.get("plan", "")],
|
|
194
|
+
signature=signature_b64,
|
|
195
|
+
)
|
|
196
|
+
verify_license(license_data, skip_signature=skip_signature)
|
|
197
|
+
return license_data
|
|
198
|
+
except LicenseError:
|
|
199
|
+
raise
|
|
200
|
+
except Exception:
|
|
201
|
+
raise LicenseInvalidError()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aicert-pro
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Pro baseline regression enforcement for aicert CLI
|
|
5
|
-
Author-email: aicert <
|
|
5
|
+
Author-email: aicert <mfifth@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
"""License verification for aicert-pro using Ed25519 signatures."""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from datetime import datetime, timezone
|
|
8
|
-
from typing import Optional
|
|
9
|
-
|
|
10
|
-
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
11
|
-
from cryptography.hazmat.primitives import serialization
|
|
12
|
-
from cryptography.exceptions import InvalidSignature
|
|
13
|
-
|
|
14
|
-
# Embedded Ed25519 public key for license verification (placeholder)
|
|
15
|
-
# Replace with actual public key for production
|
|
16
|
-
LICENSE_PUBLIC_KEY = ed25519.Ed25519PublicKey.from_public_bytes(
|
|
17
|
-
base64.b64decode(
|
|
18
|
-
"ewWxgkhpOJpLGcQKky3gMuEKaBlvlEZjy9MJ6YYCpuY="
|
|
19
|
-
)
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
LICENSE_ENV_VAR = "AICERT_PRO_LICENSE"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class LicenseError(Exception):
|
|
26
|
-
"""Base exception for license errors."""
|
|
27
|
-
|
|
28
|
-
exit_code = 5
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class LicenseMissingError(LicenseError):
|
|
32
|
-
"""Raised when license is not found."""
|
|
33
|
-
|
|
34
|
-
def __init__(self) -> None:
|
|
35
|
-
super().__init__("License not found. Set AICERT_PRO_LICENSE environment variable.")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class LicenseInvalidError(LicenseError):
|
|
39
|
-
"""Raised when license signature is invalid."""
|
|
40
|
-
|
|
41
|
-
def __init__(self) -> None:
|
|
42
|
-
super().__init__("License signature is invalid.")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class LicenseExpiredError(LicenseError):
|
|
46
|
-
"""Raised when license has expired."""
|
|
47
|
-
|
|
48
|
-
def __init__(self, expired_at: str) -> None:
|
|
49
|
-
super().__init__(f"License expired at {expired_at}.")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass
|
|
53
|
-
class LicenseData:
|
|
54
|
-
"""License data parsed from base64-encoded JSON."""
|
|
55
|
-
|
|
56
|
-
license_id: str
|
|
57
|
-
owner: str
|
|
58
|
-
issued_at: str
|
|
59
|
-
expires_at: Optional[str] = None
|
|
60
|
-
features: Optional[list[str]] = None
|
|
61
|
-
signature: Optional[str] = None
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def is_expired(self) -> bool:
|
|
65
|
-
"""Check if license is expired."""
|
|
66
|
-
if self.expires_at is None:
|
|
67
|
-
return False
|
|
68
|
-
try:
|
|
69
|
-
expires = datetime.fromisoformat(self.expires_at.replace("Z", "+00:00"))
|
|
70
|
-
return datetime.now(timezone.utc) > expires
|
|
71
|
-
except ValueError:
|
|
72
|
-
return False
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def verify_license(license_data: LicenseData) -> bool:
|
|
76
|
-
"""Verify Ed25519 signature on license data.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
license_data: Parsed license data including signature.
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
True if signature is valid.
|
|
83
|
-
|
|
84
|
-
Raises:
|
|
85
|
-
LicenseInvalidError: If signature verification fails.
|
|
86
|
-
LicenseExpiredError: If license has expired.
|
|
87
|
-
"""
|
|
88
|
-
if license_data.is_expired:
|
|
89
|
-
raise LicenseExpiredError(license_data.expires_at)
|
|
90
|
-
|
|
91
|
-
if license_data.signature is None:
|
|
92
|
-
raise LicenseInvalidError()
|
|
93
|
-
|
|
94
|
-
# Create signable payload (license data without signature)
|
|
95
|
-
payload = f"{license_data.license_id}:{license_data.owner}:{license_data.issued_at}"
|
|
96
|
-
if license_data.expires_at:
|
|
97
|
-
payload += f":{license_data.expires_at}"
|
|
98
|
-
if license_data.features:
|
|
99
|
-
payload += f":{','.join(license_data.features)}"
|
|
100
|
-
|
|
101
|
-
try:
|
|
102
|
-
signature_bytes = base64.b64decode(license_data.signature)
|
|
103
|
-
LICENSE_PUBLIC_KEY.verify(signature_bytes, payload.encode("utf-8"))
|
|
104
|
-
except (InvalidSignature, ValueError) as e:
|
|
105
|
-
raise LicenseInvalidError()
|
|
106
|
-
|
|
107
|
-
return True
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def read_license_from_env() -> str:
|
|
111
|
-
"""Read license string from environment variable.
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
Base64-encoded license string.
|
|
115
|
-
|
|
116
|
-
Raises:
|
|
117
|
-
LicenseMissingError: If environment variable is not set.
|
|
118
|
-
"""
|
|
119
|
-
license_str = os.environ.get(LICENSE_ENV_VAR)
|
|
120
|
-
if not license_str:
|
|
121
|
-
raise LicenseMissingError()
|
|
122
|
-
return license_str
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def require_pro() -> LicenseData:
|
|
126
|
-
"""Require and verify a valid pro license.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
Parsed and verified license data.
|
|
130
|
-
|
|
131
|
-
Exits:
|
|
132
|
-
Exit code 5 if license is missing or invalid.
|
|
133
|
-
"""
|
|
134
|
-
try:
|
|
135
|
-
license_str = read_license_from_env()
|
|
136
|
-
import json
|
|
137
|
-
data = json.loads(base64.b64decode(license_str))
|
|
138
|
-
license_data = LicenseData(**data)
|
|
139
|
-
verify_license(license_data)
|
|
140
|
-
return license_data
|
|
141
|
-
except LicenseError:
|
|
142
|
-
raise
|
|
143
|
-
except Exception:
|
|
144
|
-
raise LicenseInvalidError()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|