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.
@@ -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
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aicert-pro"
7
- version = "0.1.0"
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 = "dev@aicert.io"}
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.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
@@ -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