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 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 (placeholder)
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
- "ewWxgkhpOJpLGcQKky3gMuEKaBlvlEZjy9MJ6YYCpuY="
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
- expires = datetime.fromisoformat(self.expires_at.replace("Z", "+00:00"))
70
- return datetime.now(timezone.utc) > expires
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
- # 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)}"
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
- signature_bytes = base64.b64decode(license_data.signature)
103
- LICENSE_PUBLIC_KEY.verify(signature_bytes, payload.encode("utf-8"))
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(license_str))
138
- license_data = LicenseData(**data)
139
- verify_license(license_data)
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.0
3
+ Version: 0.1.1
4
4
  Summary: Pro baseline regression enforcement for aicert CLI
5
- Author-email: aicert <dev@aicert.io>
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,,