Repository navigation
feat: verify offline licenses locally (verifyOfflineLicense) - #3
Merged
Merged
Conversation
Add verifyOfflineLicense(license, options), also exposed as KeyMint.verifyOfflineLicense. It checks a signed offline license (.lic file or bare token) with node:crypto only, following the shared Keymint offline-verifier spec: parse, EdDSA-only, trusted kid, Ed25519 signature, type, productId, machineCode, claim shape, expiry. productId is required because one signing key covers every product in an org. Failures throw OfflineLicenseError with a reason code; invalid options throw TypeError/RangeError. Error messages never contain the license key. Tests run the 34 shared test vectors plus argument-error cases. The live test verifies the real /key/sign output when KEYMINT_TEST_PUBLIC_KEY is set, wired from a repository variable in CI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
verifyOfflineLicense(license, options): OfflineLicenseexported from the package entry (synchronous), plusKeyMint.verifyOfflineLicenseas a static alias next toverifyWebhookSignature.OfflineVerifyOptions { publicKeys, productId, machineCode, now?: Date, clockSkewSeconds? },OfflineLicense { licenseKey, productId, machineCode, keyId, issuedAt: Date | null, expiresAt: Date },OfflineLicenseErrorCode.OfflineLicenseError extends Errorwithcode:malformed,unsupported_algorithm,unknown_key,bad_signature,wrong_type,wrong_product,wrong_machine,expired. Messages never include the license key.productId/machineCode,clockSkewSecondsoutside 0..3600, invalidnow) throwTypeError/RangeError, notOfflineLicenseError.node:crypto(createPublicKey,verify(null, …),createHash). No new dependencies. Base64url is converted by hand rather than with the'base64url'Buffer encoding, becausepackage.jsondoesn't declare anenginesfloor.Spec
Follows the shared Keymint offline-verifier spec: the nine checks run in order and the first failure sets the code. This is a step-for-step port of the server repo's reference implementation.
productIdis required. One signing key covers every product in an org, so without the product check a license for one product would unlock another (security review finding L-04).tests/fixtures/offline-license-fixtures.jsonis the shared vector file, copied unchanged. All 34 cases pass.Tests
tests/offline.test.cjs: one test per fixture (times compared as Unix seconds, or the expectedOfflineLicenseError.code). It also covers argument errors (generated RSA key, garbage PEM, Ed25519 private-key PEM, empty product or machine, skew -1/3601/NaN, invalidnow), inclusive skew bounds, a check that errors don't contain the key, and the static alias. Added tonpm test.tests/live.test.cjs: whenKEYMINT_TEST_PUBLIC_KEYis set, it verifies the real/key/signfile with the test product and machine code and checks that another product ID giveswrong_product. It skips with a log line when the variable is unset. CI passesKEYMINT_TEST_PUBLIC_KEY: ${{ vars.KEYMINT_TEST_PUBLIC_KEY }}to the live job; the repository variable still needs to be set.Commands run (Node 22.23.1)
npm run lint: passnpx tsc --noEmit: passnpm run build: pass.dist/index.d.tsexports the function, the error class and the types, andnpm pack --dry-runincludesdist/offline.{js,d.ts}npm test: 47/47 pass (5 contract tests + 42 offline tests)node --checkpasses on it.