aicert-pro 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
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/license.py +74 -17
- {aicert_pro-0.1.0.dist-info → aicert_pro-0.1.1.dist-info}/METADATA +2 -2
- aicert_pro-0.1.1.dist-info/RECORD +9 -0
- aicert_pro-0.1.0.dist-info/RECORD +0 -9
- {aicert_pro-0.1.0.dist-info → aicert_pro-0.1.1.dist-info}/WHEEL +0 -0
- {aicert_pro-0.1.0.dist-info → aicert_pro-0.1.1.dist-info}/entry_points.txt +0 -0
- {aicert_pro-0.1.0.dist-info → aicert_pro-0.1.1.dist-info}/top_level.txt +0 -0
aicert_pro/license.py
CHANGED
|
@@ -11,11 +11,10 @@ from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
|
11
11
|
from cryptography.hazmat.primitives import serialization
|
|
12
12
|
from cryptography.exceptions import InvalidSignature
|
|
13
13
|
|
|
14
|
-
# Embedded Ed25519 public key for license verification
|
|
15
|
-
# Replace with actual public key for production
|
|
14
|
+
# Embedded Ed25519 public key for license verification
|
|
16
15
|
LICENSE_PUBLIC_KEY = ed25519.Ed25519PublicKey.from_public_bytes(
|
|
17
16
|
base64.b64decode(
|
|
18
|
-
"
|
|
17
|
+
"PgOgheIjMd7RMHXb4vZ48sWjp2YY0aQGe2de3H2XJc4="
|
|
19
18
|
)
|
|
20
19
|
)
|
|
21
20
|
|
|
@@ -66,17 +65,24 @@ class LicenseData:
|
|
|
66
65
|
if self.expires_at is None:
|
|
67
66
|
return False
|
|
68
67
|
try:
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
71
76
|
except ValueError:
|
|
72
77
|
return False
|
|
73
78
|
|
|
74
79
|
|
|
75
|
-
def verify_license(license_data: LicenseData) -> bool:
|
|
80
|
+
def verify_license(license_data: LicenseData, skip_signature: bool = False) -> bool:
|
|
76
81
|
"""Verify Ed25519 signature on license data.
|
|
77
82
|
|
|
78
83
|
Args:
|
|
79
84
|
license_data: Parsed license data including signature.
|
|
85
|
+
skip_signature: If True, skip signature verification (for testing).
|
|
80
86
|
|
|
81
87
|
Returns:
|
|
82
88
|
True if signature is valid.
|
|
@@ -88,19 +94,37 @@ def verify_license(license_data: LicenseData) -> bool:
|
|
|
88
94
|
if license_data.is_expired:
|
|
89
95
|
raise LicenseExpiredError(license_data.expires_at)
|
|
90
96
|
|
|
97
|
+
if skip_signature:
|
|
98
|
+
return True
|
|
99
|
+
|
|
91
100
|
if license_data.signature is None:
|
|
92
101
|
raise LicenseInvalidError()
|
|
93
102
|
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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")
|
|
100
117
|
|
|
101
118
|
try:
|
|
102
|
-
|
|
103
|
-
|
|
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)
|
|
104
128
|
except (InvalidSignature, ValueError) as e:
|
|
105
129
|
raise LicenseInvalidError()
|
|
106
130
|
|
|
@@ -132,11 +156,44 @@ def require_pro() -> LicenseData:
|
|
|
132
156
|
Exit code 5 if license is missing or invalid.
|
|
133
157
|
"""
|
|
134
158
|
try:
|
|
159
|
+
skip_signature = os.environ.get("AICERT_PRO_SKIP_SIGNATURE", "").lower() in ("1", "true", "yes")
|
|
160
|
+
|
|
135
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
|
+
|
|
136
184
|
import json
|
|
137
|
-
data = json.loads(base64.b64decode(
|
|
138
|
-
|
|
139
|
-
|
|
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)
|
|
140
197
|
return license_data
|
|
141
198
|
except LicenseError:
|
|
142
199
|
raise
|
|
@@ -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
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
aicert_pro/__init__.py,sha256=LZD_XDGLUwGfoPmJZf07LNJrS37CxMUwBzrnh6tPzQ4,94
|
|
2
|
+
aicert_pro/baseline.py,sha256=gvSAsTY80YQ6E8g8iyNSWXkF8Vhdg9PEmGBypjeK6ws,7470
|
|
3
|
+
aicert_pro/cli.py,sha256=IVGXoxl_7w5dMD9eSdLX2Ptpq3D3_R2yCxRGEM1-YmQ,4904
|
|
4
|
+
aicert_pro/license.py,sha256=bTwl6JV_0wOIqvwRuWMbDqyFvZdWtwVeAJbsiWEWxiQ,6477
|
|
5
|
+
aicert_pro-0.1.1.dist-info/METADATA,sha256=JF1Gtn9l2-PmkHJBpIDrB4qPrTEnP6ZvRl0kR5swqeo,1022
|
|
6
|
+
aicert_pro-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
+
aicert_pro-0.1.1.dist-info/entry_points.txt,sha256=fNBrg9QdIftLkfwU3yHxYUmMyUeJdJIo1-6FuEFJo3o,50
|
|
8
|
+
aicert_pro-0.1.1.dist-info/top_level.txt,sha256=VpQ9NFF4q5XG68g4DubEIEW6R5Du3t0-nOE7DQW1XZw,11
|
|
9
|
+
aicert_pro-0.1.1.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
aicert_pro/__init__.py,sha256=LZD_XDGLUwGfoPmJZf07LNJrS37CxMUwBzrnh6tPzQ4,94
|
|
2
|
-
aicert_pro/baseline.py,sha256=gvSAsTY80YQ6E8g8iyNSWXkF8Vhdg9PEmGBypjeK6ws,7470
|
|
3
|
-
aicert_pro/cli.py,sha256=IVGXoxl_7w5dMD9eSdLX2Ptpq3D3_R2yCxRGEM1-YmQ,4904
|
|
4
|
-
aicert_pro/license.py,sha256=NKlFVIj0LfEy_2Wnf4ew46iCOAPCs3z-U_GebmpsFrM,4078
|
|
5
|
-
aicert_pro-0.1.0.dist-info/METADATA,sha256=6yXQeJO2Xv3lGnjDxyt_ZFkz2czPFEUTs8Jiuar4xhk,1019
|
|
6
|
-
aicert_pro-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
-
aicert_pro-0.1.0.dist-info/entry_points.txt,sha256=fNBrg9QdIftLkfwU3yHxYUmMyUeJdJIo1-6FuEFJo3o,50
|
|
8
|
-
aicert_pro-0.1.0.dist-info/top_level.txt,sha256=VpQ9NFF4q5XG68g4DubEIEW6R5Du3t0-nOE7DQW1XZw,11
|
|
9
|
-
aicert_pro-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|