Repository navigation
feat: verify offline licenses locally (OfflineLicenseVerifier) - #5
Merged
Merged
Conversation
Add OfflineLicenseVerifier.Verify/TryVerify, which checks a .lic file (or bare token) from SignKey or the dashboard against trusted Ed25519 public key PEMs with no network call. ProductId and MachineCode are required and compared exactly, so a license for one product cannot unlock another product signed by the same org key (L-04). Follows the shared Keymint offline-verifier spec: nine ordered checks and eight reason codes, exposed as OfflineLicenseErrorCode plus the shared snake_case string. Tests run all 34 shared fixtures, argument errors, and verify the real signed file in the live suite when KEYMINT_TEST_PUBLIC_KEY is set. New dependency: BouncyCastle.Cryptography 2.7.0 for Ed25519.
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
OfflineLicenseVerifierverifies an offline license (the.licfile fromSignKeyor the dashboard, or the bare token) locally against your public key PEMs, with no network call.Verify(license, options)returnsOfflineLicense { LicenseKey, ProductId, MachineCode, KeyId, IssuedAt?, ExpiresAt }, or throwsOfflineLicenseExceptionwithCode(OfflineLicenseErrorCode) andCodeString(the shared snake_case code, e.g.wrong_product; alsoCode.ToSpecString()).TryVerify(license, options, out result, out error)returns false instead of throwing for an invalid license.ComputeKeyId(pem)gives the kid Keymint uses for a public key.ProductId/MachineCode) throwArgumentException;ClockSkewoutside 0..1 h throwsArgumentOutOfRangeException. Exception messages never contain the license key.Spec
Implements the shared Keymint offline-verifier spec (the same one the Go, Node and Python SDKs follow): parse,
algexactlyEdDSAwith nocrit,kidmust match a trusted key's computed kid, 64-byte Ed25519 signature over the segments as received, thentype,productId,machineCode(exact comparisons),exp/subshape, andnow < exp + skew. The first failing check decides the code. No JWT library: the three segments are hand-parsed (segments checked against[A-Za-z0-9_-]before decoding).ProductIdis required because one signing key covers every product in an org and the token has noaud; a verifier that skipped it would let a license for one product unlock another (security review finding L-04).Passes all 34 shared fixtures (
Tests/fixtures/offline-license-fixtures.json, copied unchanged).New dependency
BouncyCastle.Cryptography2.7.0 (MIT, managed, about 6 MB) for Ed25519, which .NET 8/9 don't provide. Chosen over NSec, which ships native libsodium per platform. Apps still referencing the oldPortable.BouncyCastleand using its types directly may see ambiguous-type compile errors; noted in the CHANGELOG.Other changes
.lic, bundling the public key, handling each code, clock rollback viaNow = max(system time, last trusted time)).SignKey, verifies the real file with the test product ID and machine code whenKEYMINT_TEST_PUBLIC_KEYis set; skipped otherwise. CI passesKEYMINT_TEST_PUBLIC_KEY: ${{ vars.KEYMINT_TEST_PUBLIC_KEY }}to the live job; the repository variable (the test org's PEM from Developer → Public Keys) still needs to be set..gitignoreignores*.json, so it now un-ignoresTests/fixtures/*.json.Testing
[Theory]over all 34 cases, comparing returned fields (times as Unix seconds) or the snake_case error code, through bothVerifyandTryVerify.Built and tested in this PR's GitHub Actions run (the library builds for net8.0 and net9.0 with 0 warnings; the contract suite passes 63/63 on net8.0, including all 34 fixtures) and locally in the
mcr.microsoft.com/dotnet/sdk:9.0container (0 warnings; 63/63 on net9.0). The live job runs only on push to main / manual dispatch, so the live offline check runs after merge onceKEYMINT_TEST_PUBLIC_KEYis set.