first commit
This commit is contained in:
178
docs/security-stack/security/license/README.md
Normal file
178
docs/security-stack/security/license/README.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# @suaveplan/license - Compact, offline-verifiable, secp256k1-signed software-license-key format: 72-bit bitpacked payload (version + signing-key id + product id + tier + flags + time encoding + time value + feature mask + nonce) plus full or truncated (20-byte) signature, Crockford-Base32 encoded with signature-derived obfuscation. suaveplan-enterprise additions: SuaveplanError-typed errors, OTel counters/histograms/spans, Zod boundary validation, injectable Clock/Rng, AbortSignal propagation, pluggable RevocationStore. Universal.
|
||||
|
||||
Compact, offline-verifiable, secp256k1-signed software-license-key
|
||||
format. Bit-for-bit wire-format compatible with upstream
|
||||
`@suaveplan/license`. runs unchanged in Bun, Node, and the browser.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [base32](./src/codec/base32/base32.md) — Crockford-style Base32 codec used to serialise the packed payload and signature into a printable license-key string. Th…
|
||||
- [base32](./src/codec/base32/base32.md) — Crockford-style Base32 codec used to serialise the packed payload and signature into a printable license-key string. Th…
|
||||
- [bitpack](./src/codec/bitpack/bitpack.md) — Bit-precise pack/unpack primitives used to serialise the 72-bit license payload into 9 bytes. Plain `DataView` / typed-…
|
||||
- [bitpack](./src/codec/bitpack/bitpack.md) — Bit-precise pack/unpack primitives used to serialise the 72-bit license payload into 9 bytes. Plain `DataView` / typed-…
|
||||
- [obfuscation](./src/codec/obfuscation/obfuscation.md) — Per-key character obfuscation pass run after Base32 encoding. The first 15 non-dash characters of the encoded key (whic…
|
||||
- [obfuscation](./src/codec/obfuscation/obfuscation.md) — Per-key character obfuscation pass run after Base32 encoding. The first 15 non-dash characters of the encoded key (whic…
|
||||
- [payload](./src/codec/payload/payload.md) — `packPayload` and `unpackPayload` are the 72-bit payload codec on top of the bitpack primitives. They emit exactly 9 by…
|
||||
- [payload](./src/codec/payload/payload.md) — `packPayload` and `unpackPayload` are the 72-bit payload codec on top of the bitpack primitives. They emit exactly 9 by…
|
||||
- [constants](./src/constants/constants.md) — Wire-format constants for the license package. Every value here is part of the binary contract: the per-field b…
|
||||
- [constants](./src/constants/constants.md) — Wire-format constants for the license package. Every value here is part of the binary contract: the per-field b…
|
||||
- [flags](./src/flags/flags.md) — Bitmask helpers for the 5-bit `flags` field (`LicenseFlags`) and the 19-bit `featureMask` field. The flags field stores…
|
||||
- [flags](./src/flags/flags.md) — Bitmask helpers for the 5-bit `flags` field (`LicenseFlags`) and the 19-bit `featureMask` field. The flags field stores…
|
||||
- [generate](./src/generate/generate.md) — Top-level license-key generation, payload construction, and parse-only inspection. This is the place that brings togeth…
|
||||
- [generate](./src/generate/generate.md) — Top-level license-key generation, payload construction, and parse-only inspection. This is the place that brings togeth…
|
||||
- [manager](./src/manager/manager.md) — High-level `LicenseManager` class that bundles a public-key registry, an optional signing key, default `signingKeyId` /…
|
||||
- [manager](./src/manager/manager.md) — High-level `LicenseManager` class that bundles a public-key registry, an optional signing key, default `signingKeyId` /…
|
||||
- [otel](./src/otel/instruments.md) — OpenTelemetry instrumentation for the license package. Registers a single `@opentelemetry/api` meter (`@suaveplan/licen…
|
||||
- [instruments](./src/otel/instruments.md) — OpenTelemetry instrumentation for the license package. Registers a single `@opentelemetry/api` meter (`@suaveplan/licen…
|
||||
- [revocation-store](./src/revocation/revocation-store/revocation-store.md) — Pluggable revocation blocklist for the license package. The `RevocationStore` interface defines three methods (`add`, `…
|
||||
- [revocation-store](./src/revocation/revocation-store/revocation-store.md) — Pluggable revocation blocklist for the license package. The `RevocationStore` interface defines three methods (`add`, `…
|
||||
- [schemas](./src/schemas/schemas.md) — Zod schemas for every public input boundary in the license package. `generateLicense`, `validateLicense`, `createPayloa…
|
||||
- [schemas](./src/schemas/schemas.md) — Zod schemas for every public input boundary in the license package. `generateLicense`, `validateLicense`, `createPayloa…
|
||||
- [time](./src/time/time.md) — Time-encoding helpers for the four `TimeEncodingType` variants. The 72-bit payload encodes time in 18 bits with an addi…
|
||||
- [time](./src/time/time.md) — Time-encoding helpers for the four `TimeEncodingType` variants. The 72-bit payload encodes time in 18 bits with an addi…
|
||||
- [types](./src/types/license.md) — Public type surface for the license package, ported verbatim from the upstream `index.types.ts` at the wire-format leve…
|
||||
- [license](./src/types/license.md) — Public type surface for the license package, ported verbatim from the upstream `index.types.ts` at the wire-format leve…
|
||||
- [validate](./src/validate/validate.md) — License-key validation: parse, verify signature, check version, check expiration, surface every failure as a typed enum…
|
||||
- [validate](./src/validate/validate.md) — License-key validation: parse, verify signature, check version, check expiration, surface every failure as a typed enum…
|
||||
- [error-codes](./src/error-codes.md) — This module owns the stable error-code registry for `@suaveplan/license`. It exposes `LICENSE_CODES` (a frozen `defineC…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Highlights
|
||||
|
||||
- **72-bit bitpacked payload** (version + signing-key id + product id
|
||||
- tier + flags + time encoding + time value + feature mask + nonce)
|
||||
encoded as 9 bytes.
|
||||
- **Truncated 20-byte signature by default** keeps keys ~50-60
|
||||
characters; opt into 64-byte full signatures with
|
||||
`useFullSignature: true`.
|
||||
- **Crockford-style Base32 alphabet** (no `I`/`L`/`O`/`U` — paste-
|
||||
friendly).
|
||||
- **Signature-derived obfuscation pass** rotates the first 15 payload
|
||||
characters by per-license offsets drawn from the signature.
|
||||
- **Four time-encoding strategies**: `NONE`, `ABSOLUTE_DAYS`,
|
||||
`RELATIVE_DAYS`, `ABSOLUTE_TIMESTAMP`.
|
||||
- **19-bit feature mask** + 5-bit flag bitfield (TRIAL, UPGRADE,
|
||||
MACHINE_LOCKED, SUBSCRIPTION, EDUCATIONAL).
|
||||
- **Enterprise extras**: typed `LicenseError` registry, OTel
|
||||
counters/histograms, Zod boundary validation, injectable `Clock` /
|
||||
`Rng`, `AbortSignal` propagation, pluggable `RevocationStore`.
|
||||
|
||||
## Quick-start
|
||||
|
||||
```ts
|
||||
import { secp256k1 } from "@suaveplan/crypto-curve";
|
||||
import { LicenseManager, PackageTier } from "@suaveplan/license";
|
||||
|
||||
// 1. Generate a signing key (typically done once, off-line)
|
||||
const secret = secp256k1.randomPrivateKey();
|
||||
const publicKey = { p: secp256k1.getPublicKey(secret) };
|
||||
|
||||
// 2. Bind the manager to that key
|
||||
const m = new LicenseManager({
|
||||
signingKey: { s: secret, p: publicKey.p },
|
||||
publicKeys: { 0: publicKey },
|
||||
});
|
||||
|
||||
// 3. Mint a license key
|
||||
const { key } = await m.generate({
|
||||
productId: 5,
|
||||
tier: PackageTier.PRO,
|
||||
expirationDays: 365,
|
||||
});
|
||||
|
||||
// 4. Hand the key to the user, validate it on incoming requests
|
||||
const v = await m.validate(key);
|
||||
if (v.success) {
|
||||
console.log(`Tier ${v.details.packageTier}, expires ${v.details.expirationDate}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
Every feature module is a tree-shakeable subpath export:
|
||||
|
||||
| Module | Subpath | Docs |
|
||||
|---|---|---|
|
||||
| Error codes | `./error-codes/error-codes` | [src/error-codes.md](src/error-codes.md) |
|
||||
| Constants | `./constants/constants` | [src/constants/constants.md](src/constants/constants.md) |
|
||||
| Types + enums | `./types/license` | [src/types/license.md](src/types/license.md) |
|
||||
| Base32 codec | `./codec/base32/base32` | [src/codec/base32/base32.md](src/codec/base32/base32.md) |
|
||||
| Bitpack | `./codec/bitpack/bitpack` | [src/codec/bitpack/bitpack.md](src/codec/bitpack/bitpack.md) |
|
||||
| Payload codec | `./codec/payload/payload` | [src/codec/payload/payload.md](src/codec/payload/payload.md) |
|
||||
| Obfuscation | `./codec/obfuscation/obfuscation` | [src/codec/obfuscation/obfuscation.md](src/codec/obfuscation/obfuscation.md) |
|
||||
| Time helpers | `./time/time` | [src/time/time.md](src/time/time.md) |
|
||||
| Flag helpers | `./flags/flags` | [src/flags/flags.md](src/flags/flags.md) |
|
||||
| Zod schemas | `./schemas/schemas` | [src/schemas/schemas.md](src/schemas/schemas.md) |
|
||||
| OTel instruments | `./otel/instruments` | [src/otel/instruments.md](src/otel/instruments.md) |
|
||||
| Generate | `./generate/generate` | [src/generate/generate.md](src/generate/generate.md) |
|
||||
| Validate | `./validate/validate` | [src/validate/validate.md](src/validate/validate.md) |
|
||||
| Manager | `./manager/manager` | [src/manager/manager.md](src/manager/manager.md) |
|
||||
| Revocation | `./revocation/revocation-store/revocation-store` | [src/revocation/revocation-store/revocation-store.md](src/revocation/revocation-store/revocation-store.md) |
|
||||
|
||||
The package root (`.`) re-exports every public symbol from each module
|
||||
for ergonomic single-import usage.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Captured with `mitata` on Intel(R) Xeon(R) CPU E5-2696 v4 @ 2.20GHz,
|
||||
Bun 1.3.14 (Linux x64). Run yourself with
|
||||
`bun src/__benchmarks__/license.bench.ts`.
|
||||
|
||||
| Benchmark | Mean | p75 | p99 |
|
||||
|---|---|---|---|
|
||||
| `base32Encode` (73 bytes) | 38.09 µs | 42.82 µs | 134.18 µs |
|
||||
| `base32Decode` (73 bytes) | 65.06 µs | 70.70 µs | 189.76 µs |
|
||||
| `packPayload` | 733.58 ns | 744.46 ns | 1.37 µs |
|
||||
| `unpackPayload` | 1.16 µs | 1.18 µs | 1.75 µs |
|
||||
| `generateLicense` (truncated) | 67.64 µs | 73.84 µs | 242.36 µs |
|
||||
| `validateLicense` (truncated) | 40.52 µs | 42.73 µs | 116.04 µs |
|
||||
|
||||
A full generate + validate round-trip completes in ~108 µs on this
|
||||
hardware, comfortably below the 1 ms p99 budget targeted by the
|
||||
upstream enterprise audit.
|
||||
|
||||
## Revocation
|
||||
|
||||
Revocation is an opt-in pluggable layer. Wire any store implementing
|
||||
the `RevocationStore` interface (sync or async); the package ships a
|
||||
synchronous in-memory store with optional LRU eviction.
|
||||
|
||||
```ts
|
||||
import {
|
||||
createMemoryRevocationStore,
|
||||
fingerprintLicense,
|
||||
LicenseManager,
|
||||
} from "@suaveplan/license";
|
||||
|
||||
const store = createMemoryRevocationStore({ maxEntries: 100_000 });
|
||||
const m = new LicenseManager({ signingKey, publicKeys, revocationStore: store });
|
||||
|
||||
// Block a compromised key
|
||||
await m.revoke(fingerprintLicense(compromisedKey));
|
||||
|
||||
// Subsequent validates of that key return { error: 'REVOKED' }
|
||||
```
|
||||
|
||||
## Wire format
|
||||
|
||||
The on-the-wire layout is unchanged from upstream `@suaveplan/license`.
|
||||
Each 9-byte payload packs nine fields MSB-first:
|
||||
|
||||
```text
|
||||
| v(3) | kid(5) | pid(6) | tier(4) | flg(5) | te(2) | timeValue(18) | featureMask(19) | nonce(10) |
|
||||
total = 72 bits
|
||||
```
|
||||
|
||||
A complete key encodes `payload || signature` (29 bytes with truncated
|
||||
signature, 73 bytes with full signature) as Base32, dashes inserted
|
||||
every 5 chars for legibility, then runs the signature-derived
|
||||
obfuscation pass over the first 15 payload chars. The
|
||||
seed-from-key-region trick (chars 16-30 carry the obfuscation seed)
|
||||
breaks the circular dependency between "decode needs the seed" and
|
||||
"the seed comes from a decoded signature".
|
||||
|
||||
## License
|
||||
|
||||
MIT.
|
||||
|
||||
Reference in New Issue
Block a user