import { secp256k1 } from "@suaveplan/crypto-curve"; import { describe, expect, it } from "@suaveplan/testing/runner"; import fc from "fast-check"; import { packPayload, unpackPayload } from "../src/codec/payload/payload.js"; import { LICENSE_SCHEMA } from "../src/constants/constants.js"; import { generateLicense } from "../src/generate/generate.js"; import { type LicensePayload, LicenseValidationError, TimeEncodingType, } from "../src/types/license.js"; import { validateLicense } from "../src/validate/validate.js"; const payloadArb = fc.record({ version: fc.integer({ min: 0, max: LICENSE_SCHEMA.version }), signingKeyId: fc.integer({ min: 0, max: LICENSE_SCHEMA.signingKeyId }), productId: fc.integer({ min: 0, max: LICENSE_SCHEMA.productId }), packageTier: fc.integer({ min: 0, max: LICENSE_SCHEMA.packageTier }), flags: fc.integer({ min: 0, max: LICENSE_SCHEMA.flags }), timeEncodingType: fc .integer({ min: 0, max: LICENSE_SCHEMA.timeEncodingType }) .map((v) => v as TimeEncodingType), timeValue: fc.integer({ min: 0, max: LICENSE_SCHEMA.timeValue }), featureMask: fc.integer({ min: 0, max: LICENSE_SCHEMA.featureMask }), nonce: fc.integer({ min: 0, max: LICENSE_SCHEMA.nonce }), }); function fresh() { const s = secp256k1.randomPrivateKey(); const p = secp256k1.getPublicKey(s); return { signingKey: { s, p }, publicKey: { p } }; } describe("property tests", () => { it("unpackPayload inverts packPayload", { timeout: 30000 }, () => { fc.assert( fc.property(payloadArb, (payload: LicensePayload) => { const bytes = packPayload(payload); expect(bytes.length).toBe(9); const decoded = unpackPayload(bytes); expect(decoded).toEqual(payload); }), { numRuns: 200 } ); }); it("1-byte mutation surfaces as INVALID_SIGNATURE or MALFORMED", { timeout: 60000, }, async () => { const { signingKey, publicKey } = fresh(); // Sample once at the boundary; we mutate the generated key's // characters for the actual property under test. const payload: LicensePayload = { version: 2, signingKeyId: 0, productId: 1, packageTier: 0, flags: 0, timeEncodingType: TimeEncodingType.NONE, timeValue: 0, featureMask: 0, nonce: 0x111, }; const r = await generateLicense(payload, { signingKey, useFullSignature: false, }); const validPubKeys = new Map([[0, publicKey]]); const keyLen = r.key.length; // The Base32 encoding of 29 bytes uses 47 chars; chars after // that are padding bits that may not affect decoded bytes. // We bound the tamper-index so it always lands on a // semantically-significant character. await fc.assert( fc.asyncProperty( fc.integer({ min: 0, max: keyLen - 2 }), fc.integer({ min: 0, max: 31 }), async (idx, alphabetIdx) => { const chars = r.key.split(""); if (chars[idx] === "-") return true; // skip dashes const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; const newChar = ALPHABET[alphabetIdx] as string; if (chars[idx] === newChar) return true; // no-op mutation chars[idx] = newChar; const tampered = chars.join(""); const v = await validateLicense(tampered, { publicKeys: validPubKeys, }); // Base32 has trailing padding bits; a character // change in the final bit-positions may decode to // the same bytes, producing a valid key. That is // acceptable — we only need to assert that NO tamper // produces a *different* valid license that the // validator wrongly trusts as the original payload. if (v.success) { return ( v.details.nonce === payload.nonce && v.details.productId === payload.productId ); } return [ LicenseValidationError.INVALID_SIGNATURE, LicenseValidationError.MALFORMED, LicenseValidationError.INVALID_ENCODING, LicenseValidationError.UNKNOWN_SIGNING_KEY, LicenseValidationError.UNSUPPORTED_VERSION, LicenseValidationError.EXPIRED, ].includes(v.error); } ), { numRuns: 200 } ); }); it("expired trial always fails validation", { timeout: 60000, }, async () => { const { signingKey, publicKey } = fresh(); await fc.assert( fc.asyncProperty( fc.integer({ min: 1, max: 365 }), async (durationDays) => { const payload: LicensePayload = { version: 2, signingKeyId: 0, productId: 1, packageTier: 0, flags: 1, // TRIAL timeEncodingType: TimeEncodingType.RELATIVE_DAYS, timeValue: durationDays, featureMask: 0, nonce: 7, }; const r = await generateLicense(payload, { signingKey, useFullSignature: false, }); const creation = new Date("2026-01-01T00:00:00Z"); const past = creation.getTime() + (durationDays + 1) * 86_400_000; const v = await validateLicense(r.key, { publicKeys: new Map([[0, publicKey]]), creationDate: creation, referenceDate: new Date(past), }); return ( !v.success && v.error === LicenseValidationError.EXPIRED ); } ), { numRuns: 50 } ); }); });