843ff0e5d7
Glue files not covered by subproject repos: top-level docs, logo, keysat-design-system, and crosscheck tests. Subproject folders are gitignored (each has its own Gitea remote).
208 lines
6.4 KiB
Python
208 lines
6.4 KiB
Python
"""Produce LIC1-...-... keys the service would accept — used to verify the
|
|
SDK parsers round-trip correctly.
|
|
|
|
Emits two fixtures now: v1 (legacy fixed-74 payload) and v2 (variable-length
|
|
payload with expires_at + entitlements). SDKs must accept both.
|
|
|
|
Byte layout (matches licensing-service/src/crypto/mod.rs exactly):
|
|
|
|
v1 (74 bytes, fixed):
|
|
[0] version = 1
|
|
[1] flags
|
|
[2..18] product_id (UUID BE, 16 bytes)
|
|
[18..34] license_id (UUID BE, 16 bytes)
|
|
[34..42] issued_at (u64 BE, unix seconds)
|
|
[42..74] fingerprint_hash (SHA-256, zero if unbound)
|
|
|
|
v2 (83 bytes + variable entitlements):
|
|
[0] version = 2
|
|
[1] flags
|
|
[2..18] product_id
|
|
[18..34] license_id
|
|
[34..42] issued_at (u64 BE)
|
|
[42..50] expires_at (u64 BE, 0 = perpetual)
|
|
[50..82] fingerprint_hash (SHA-256, zero if unbound)
|
|
[82] num_entitlements (u8)
|
|
[83..] for each: [u8 len][len bytes of UTF-8 slug]
|
|
|
|
Signature: 64 bytes over the raw payload bytes.
|
|
Encoding: LIC1-<base32(payload)>-<base32(signature)>
|
|
RFC 4648 base32, uppercase, no padding.
|
|
"""
|
|
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
import uuid
|
|
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
from cryptography.hazmat.primitives.serialization import (
|
|
Encoding, PrivateFormat, PublicFormat, NoEncryption,
|
|
)
|
|
|
|
KEY_VERSION_V1 = 1
|
|
KEY_VERSION_V2 = 2
|
|
|
|
FLAG_FINGERPRINT_BOUND = 0b0000_0001
|
|
FLAG_TRIAL = 0b0000_0010
|
|
|
|
|
|
def b32nopad(b: bytes) -> str:
|
|
return base64.b32encode(b).decode("ascii").rstrip("=")
|
|
|
|
|
|
def make_payload_v1(
|
|
flags: int,
|
|
product_id: bytes, license_id: bytes,
|
|
issued_at: int, fp_hash: bytes,
|
|
) -> bytes:
|
|
assert len(product_id) == 16
|
|
assert len(license_id) == 16
|
|
assert len(fp_hash) == 32
|
|
payload = (
|
|
bytes([KEY_VERSION_V1, flags])
|
|
+ product_id
|
|
+ license_id
|
|
+ issued_at.to_bytes(8, "big")
|
|
+ fp_hash
|
|
)
|
|
assert len(payload) == 74, f"v1 payload is {len(payload)} bytes, expected 74"
|
|
return payload
|
|
|
|
|
|
def make_payload_v2(
|
|
flags: int,
|
|
product_id: bytes, license_id: bytes,
|
|
issued_at: int, expires_at: int,
|
|
fp_hash: bytes, entitlements: list[str],
|
|
) -> bytes:
|
|
assert len(product_id) == 16
|
|
assert len(license_id) == 16
|
|
assert len(fp_hash) == 32
|
|
assert 0 <= len(entitlements) <= 255
|
|
tail = bytearray()
|
|
for slug in entitlements:
|
|
encoded = slug.encode("utf-8")
|
|
assert len(encoded) <= 255, f"entitlement '{slug}' too long"
|
|
tail.append(len(encoded))
|
|
tail.extend(encoded)
|
|
head = (
|
|
bytes([KEY_VERSION_V2, flags])
|
|
+ product_id
|
|
+ license_id
|
|
+ issued_at.to_bytes(8, "big")
|
|
+ expires_at.to_bytes(8, "big")
|
|
+ fp_hash
|
|
+ bytes([len(entitlements)])
|
|
)
|
|
assert len(head) == 83
|
|
return head + bytes(tail)
|
|
|
|
|
|
def sign_and_encode(sk: Ed25519PrivateKey, payload: bytes) -> str:
|
|
sig = sk.sign(payload)
|
|
assert len(sig) == 64
|
|
return f"LIC1-{b32nopad(payload)}-{b32nopad(sig)}"
|
|
|
|
|
|
def main():
|
|
# Deterministic test vector — fixed seeds so the output is stable.
|
|
sk = Ed25519PrivateKey.from_private_bytes(bytes(range(32)))
|
|
pk = sk.public_key()
|
|
pub_pem = pk.public_bytes(
|
|
Encoding.PEM, PublicFormat.SubjectPublicKeyInfo
|
|
).decode()
|
|
|
|
product_id = uuid.UUID("6f46a4f6-0d2f-4a28-b6aa-3e8fbf6f28f0").bytes
|
|
license_id_v1 = uuid.UUID("11111111-2222-3333-4444-555555555555").bytes
|
|
license_id_v2 = uuid.UUID("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee").bytes
|
|
issued_at = 1_700_000_000
|
|
expires_at_v2 = 1_900_000_000 # ~2030
|
|
fingerprint_raw = "test-machine-fingerprint"
|
|
fp_hash = hashlib.sha256(fingerprint_raw.encode()).digest()
|
|
entitlements_v2 = ["pro", "multi-device"]
|
|
|
|
# v1: legacy fingerprint-bound key.
|
|
v1_payload = make_payload_v1(
|
|
FLAG_FINGERPRINT_BOUND,
|
|
product_id, license_id_v1, issued_at, fp_hash,
|
|
)
|
|
v1_key = sign_and_encode(sk, v1_payload)
|
|
|
|
# v2: trial + fingerprint-bound, with entitlements and expiry.
|
|
v2_flags = FLAG_FINGERPRINT_BOUND | FLAG_TRIAL
|
|
v2_payload = make_payload_v2(
|
|
v2_flags,
|
|
product_id, license_id_v2, issued_at, expires_at_v2,
|
|
fp_hash, entitlements_v2,
|
|
)
|
|
v2_key = sign_and_encode(sk, v2_payload)
|
|
|
|
# v2 perpetual, unbound, no entitlements — the "happy path" for a normal
|
|
# permanent license purchase.
|
|
v2_plain_payload = make_payload_v2(
|
|
0,
|
|
product_id, license_id_v2, issued_at, 0,
|
|
bytes(32), [],
|
|
)
|
|
v2_plain_key = sign_and_encode(sk, v2_plain_payload)
|
|
|
|
out = {
|
|
"publicKeyPem": pub_pem,
|
|
"v1": {
|
|
"licenseKey": v1_key,
|
|
"expected": {
|
|
"version": 1,
|
|
"productUuid": "6f46a4f6-0d2f-4a28-b6aa-3e8fbf6f28f0",
|
|
"licenseUuid": "11111111-2222-3333-4444-555555555555",
|
|
"issuedAt": issued_at,
|
|
"expiresAt": 0,
|
|
"flags": FLAG_FINGERPRINT_BOUND,
|
|
"isFingerprintBound": True,
|
|
"isTrial": False,
|
|
"entitlements": [],
|
|
"fingerprintRaw": fingerprint_raw,
|
|
"fingerprintHashHex": fp_hash.hex(),
|
|
},
|
|
},
|
|
"v2": {
|
|
"licenseKey": v2_key,
|
|
"expected": {
|
|
"version": 2,
|
|
"productUuid": "6f46a4f6-0d2f-4a28-b6aa-3e8fbf6f28f0",
|
|
"licenseUuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
"issuedAt": issued_at,
|
|
"expiresAt": expires_at_v2,
|
|
"flags": v2_flags,
|
|
"isFingerprintBound": True,
|
|
"isTrial": True,
|
|
"entitlements": entitlements_v2,
|
|
"fingerprintRaw": fingerprint_raw,
|
|
"fingerprintHashHex": fp_hash.hex(),
|
|
},
|
|
},
|
|
"v2_perpetual_unbound": {
|
|
"licenseKey": v2_plain_key,
|
|
"expected": {
|
|
"version": 2,
|
|
"productUuid": "6f46a4f6-0d2f-4a28-b6aa-3e8fbf6f28f0",
|
|
"licenseUuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
"issuedAt": issued_at,
|
|
"expiresAt": 0,
|
|
"flags": 0,
|
|
"isFingerprintBound": False,
|
|
"isTrial": False,
|
|
"entitlements": [],
|
|
"fingerprintRaw": None,
|
|
"fingerprintHashHex": "00" * 32,
|
|
},
|
|
},
|
|
}
|
|
json.dump(out, sys.stdout, indent=2)
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|