@sdxc/scitt
SCITT Signed Statements, Receipts and a SCRAPI client for registering with a Transparency Service
- Installs with
- @sdxc/backoff@sdxc/cbor@sdxc/crypto@sdxc/result
- Source
- packages/scitt
SCITT Signed Statements, Receipts and a SCRAPI client for registering with a Transparency Service.
Installation
npm add @sdxc/scittEvery function answers a Result from @sdxc/result, which installs alongside this package. COSE signing comes from @sdxc/cbor's cose entry.
Usage
SCITT makes statements about software (an SBOM, a scan result, a release attestation) transparent: an issuer signs a statement, a Transparency Service appends it to a Merkle tree log and returns a Receipt, and anyone holding the service's key can check the statement is in the log without asking the service.
Sign a statement
import { JWK } from "@sdxc/jwt";
import { signStatement } from "@sdxc/scitt";
import { unwrap } from "@sdxc/result";
let key = await JWK.importKeyPair(await JWK.generateKeyPair("ES256"));
let statement = unwrap(
await signStatement({
issuer: "https://vendor.example",
subject: "pkg:npm/@vendor/cli@1.2.3",
contentType: "application/spdx+json",
payload: sbomBytes,
key, // any { id, alg, private }; an @sdxc/jwt key pair fits
}),
);The protected header carries the algorithm, the kid, the content type and the CWT claims with iss and sub. Pass detached: true to sign a payload that stays out of the message, such as the digest of a large artifact, and issuedAt to add an iat claim.
Register it and get a Receipt
import { attachReceipts, TransparencyService } from "@sdxc/scitt";
let service = new TransparencyService("https://transparency.example", {
headers: { authorization: `Bearer ${token}` },
});
let receipt = unwrap(await service.submit(statement));
let transparent = unwrap(attachReceipts(statement, [receipt]));submit posts to /entries and, when the service answers 202, polls the Location until the Receipt is ready. It waits the Retry-After the service sends, and otherwise backs off exponentially with jitter, giving up as pending after maxAttempts (default 10).
Verify a Transparent Statement
import { verifyTransparentStatement } from "@sdxc/scitt";
let keys = unwrap(await service.keys()); // /.well-known/scitt-keys
let result = await verifyTransparentStatement(transparent, {
keys,
issuer: "https://transparency.example",
issuerKey, // optional: also check the issuer's own signature
});Each attached Receipt whose key is in keys must prove the statement's inclusion, and at least one must. A Receipt verifies when its inclusion proof leads from the statement's leaf hash to a Merkle root and the service's signature covers that root.
API
Statements
signStatement(options): a tagged COSE_Sign1 Signed Statement.options:issuer,subject,payload,key, and optionallycontentType,detached,issuedAt,claims(more CWT claims) andheaders(more protected header parameters).readStatement(bytes): theSignedStatement(issuer,subject,contentType,kid,algorithm,claims,payload,receipts,message) without checking its signature. Failsmissing-claimwithoutissandsub, andmalformedwithout akidor X.509 reference.verifyStatement(statement, key, { payload?, algorithms? }): the statement once the issuer's signature verifies.attachReceipts(statement, receipts): the Transparent Statement, with Receipts appended to the unprotected header (label 394).registeredEntry(statement): the statement with its unprotected header emptied, the bytes a Transparency Service logs and a Receipt's leaf hash is taken over.
Receipts
readReceipt(bytes): theReceipt(issuer,subject,kid,algorithm,vds,inclusionProofs,message). Only the RFC9162_SHA256 tree is understood; any other failsunsupported-vds.verifyReceipt(receipt, { entry, keys, issuer?, algorithms? }): theVerifiedReceiptwith itsrootandproof.keysis oneCryptoKeyor a COSE key set, searched by the receipt'skidand then by RFC 9679 thumbprint.verifyTransparentStatement(bytes, { keys, issuer?, issuerKey?, payload? }): the statement and the receipts that verified.leafHash(entry)/rootFromInclusionProof(leaf, proof): the RFC 9162 hashing underneath.
TransparencyService
new TransparencyService(url, { headers?, maxAttempts?, backoff? }), where backoff is an @sdxc/backoff schedule.
register(statement):{ location, receipt, retryAfter };receiptisnulluntil registration finishes.receipt(entry):{ status: "registered", receipt }or{ status: "running", retryAfter }for alocationURL or an entry ID.waitForReceipt(entry, { retryAfter?, signal? }): polls until the Receipt is ready.submit(statement):registerandwaitForReceiptin one call.keys()/key(kid): the service's COSE key set, or one key bykid.
Errors
SCITTError:codeis a COSE failure (malformed,invalid-signature,unsupported-algorithm,unsupported-key,unknown-critical) ormissing-claim,unsupported-vds,invalid-proof,unknown-key,untrusted-issuer.ServiceError:codeiserror-response(withstatus, and the Concise Problem Detailstitleanddetail),invalid-response,network-errororpending;retryAfterholds the service'sRetry-Afterin milliseconds.
Label
The registered numbers: Label.RECEIPTS (394), Label.VDS (395), Label.VDP (396), Label.ISS, Label.SUB, Label.IAT, Label.RFC9162_SHA256, Label.INCLUSION_PROOFS.
Specifications
RFC 9943: SCITT architecture, Signed Statements and Transparent Statements.
RFC 9942: COSE Receipts and the RFC9162_SHA256 inclusion proof.
draft-ietf-scitt-scrapi-11: the SCITT Reference API.