signing-primitive
Per-tenant evidence signing, a detached Ed25519 signature over a canonical, chain-anchored manifest body, with an optional RFC-3161 trusted-timestamp countersignature and a fail-closed verify path.
@caisson/signing-primitive signs an evidence pack with a key that belongs to the tenant,
not to Caisson. It produces a detached Ed25519 signature over the canonicalized manifest
body concatenated with the audit chain's tip hash. The signature is detached, so the signed
body stays byte-stable and independently verifiable, and the per-tenant identity is
deliberately distinct from the Caisson license-issuer key, a buyer proves the provenance of
their own evidence with their own identity.
Install
bun add @caisson/signing-primitiveWhat it does
- Per-tenant detached signing:
signEvidencePacksignscanonicalize(manifest) ∥ chainAnchor.tipHashwith anEd25519Signerscoped to the tenant. The signature is detached and never injected into the body, so the manifest stays byte-identical after signing. - Optional trusted timestamp: supply a
TimestampAuthorityand the signature is countersigned with an RFC-3161 token, attesting the instant the signature existed. - Fail-closed verify:
verifyEvidenceSignaturereuses one shared Ed25519 primitive and returnsfalseon an unknown algorithm, malformed hex, or wrong-length key or signature. A forgery never passes as valid, and a malformed signing result throws rather than emitting a bad signature. - Timing-safe compares:
signaturesEqualandtimestampCountersignsSignaturecompare hex signatures and RFC-3161 message imprints with a constant-time equality, so neither leaks how many leading bytes matched.
The guarantee
The signing key is the tenant's, held per tenant and never the license-issuer key. The signature covers the manifest and the WORM audit-chain tip together, so a signature is bound to the exact evidence state it was produced over, re-anchoring the chain or editing the manifest invalidates it:
import {
Ed25519Signer,
signEvidencePack,
verifyEvidenceSignature,
} from "@caisson/signing-primitive";
// A per-tenant signer — the 32-byte seed is the tenant's signing key, never Caisson's.
const signer = new Ed25519Signer(keyId, tenantSigningKey);
// Sign the pack: detached signature over canonicalize(manifest) ∥ chainAnchor.tipHash.
const signature = await signEvidencePack(signer, manifest);
// Verify fails closed — any tampering or forgery returns false, never throws through.
const ok = await verifyEvidenceSignature(manifest, signature);
if (!ok) {
throw new Error("evidence signature does not verify");
}manifest only needs to structurally satisfy SignableManifest: a chainAnchor.tipHash
field. The evidence-pack manifest produced by @caisson/compliance-core matches it; this
package never imports that generator, so the signing surface stands alone.
The timestamp authority port
The RFC-3161 countersignature is reached through a TimestampAuthority port. It attests
that a signature existed at a point in time, layered on top of the Ed25519 signature, never
replacing it. A live authority implements countersign(signature) by POSTing a DER
TimeStampReq (message imprint sha256(signature)) over fetchWithTimeout and parsing
the response, the port is stable, so it slots in without touching call sites.
import {
signEvidencePack,
type TimestampAuthority,
} from "@caisson/signing-primitive";
const tsa: TimestampAuthority = myRfc3161Authority;
// A countersigned signature carries an RFC-3161 timestamp token alongside the Ed25519 bytes.
const signature = await signEvidencePack(signer, manifest, {
timestampAuthority: tsa,
});For tests, StubTimestampAuthority is a network-free double that reproduces the same
message imprint a live TSA would attest, against an injected clock:
import { StubTimestampAuthority } from "@caisson/signing-primitive";
const tsa = new StubTimestampAuthority({ now: new Date("2026-01-01") });API reference
Ed25519Signer(keyId, secretKey): the baseSigner.secretKeymust be a 32-byte seed; construction throws on an emptykeyIdor a wrong-length key.publicKey()andsign(payload)are async; the seed is a private field, never enumerable or logged.Signer: the signing-identity port (keyId,algorithm,publicKey(),sign()). A buyer-supplied KMS asymmetric-sign implementation is a drop-in of this same interface: the secret key never leaves the HSM.signEvidencePack(signer, manifest, options?): returns anEvidenceSignature(algorithm,keyId,publicKey,signature, optionaltimestamp). Throws on a malformed signing result rather than emitting a bad signature.verifyEvidenceSignature(manifest, signature):Promise<boolean>, fails closed.evidenceSignablePayload(manifest): the exactUint8Arraythat gets signed:canonicalize(manifest)concatenated withmanifest.chainAnchor.tipHash. Exposed so a caller can hash or log the signed payload without re-deriving it.TimestampAuthority/TimestampToken/StubTimestampAuthority: the RFC-3161 countersign port, its token shape, and the CI-safe test double.signaturesEqual(a, b): timing-safe hex-signature compare.timestampCountersignsSignature(token, signature): recomputessha256(signature)and timing-safe compares it againsttoken.messageImprint, confirming a timestamp token actually countersigns this signature rather than a different one.
Configuration
There is no env-based configuration, every input is a constructor or call argument, not a read environment variable:
Ed25519Signer(keyId, secretKey):secretKeyis a 32-byte seed you provision per tenant and pass in directly; the module never reads or derives a key from the environment. Construction throws on an emptykeyIdor a wrong-length key, so a misconfigured signer fails at construction, not at first sign.SignEvidencePackOptions.timestampAuthority: optional; omit it andsignEvidencePackreturns a signature with notimestampfield. Supply anyTimestampAuthorityimplementation (a live RFC-3161 client orStubTimestampAuthorityfor tests) to add the countersignature.StubTimestampAuthority({ authority?, now? }): both fields default ("urn:caisson:test-tsa", epoch); only meant for tests, never wired to a live TSA.
Composition
@caisson/signing-primitive depends only on @caisson/kernel (canonicalize,
safeEqualFixed), dependencies are down-only, so it never imports the evidence generator
or an edition. The Compliance and Provenance bundles compose it alongside
@caisson/compliance-core and @caisson/audit-worm to sign the evidence packs those
packages produce.
Sold standalone at $199 or bundled, check current bundle composition and pricing on the marketplace.
Provenance
Cryptographic provenance, detached signing over a canonical manifest, an append-only WORM audit chain, and per-tenant field encryption. Proof, not a promise.
audit-worm
S3/GCS/R2 Object-Lock WORM storage plus an append-only SHA-256 audit chain and a derived-current locked-version table, evidence that cannot be altered before retention expires, and tampering that is provable.