first commit
This commit is contained in:
138
docs/security-stack/security/slsa/README.md
Normal file
138
docs/security-stack/security/slsa/README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# @suaveplan/slsa - SLSA L1-L2 supply-chain security: provenance, SBOM, DSSE attestations, Sigstore Rekor transparency-log client, policy evaluation, OTel audit events
|
||||
|
||||
## Summary
|
||||
|
||||
SLSA L1-L2 supply-chain security for Suaveplan. Generates and
|
||||
verifies in-toto / SLSA v1.0 provenance, CycloneDX 1.5 SBOMs, DSSE
|
||||
attestation envelopes, and RFC 6962 Sigstore Rekor transparency-log
|
||||
proofs. Every capability emits structured OTel span events and raises
|
||||
typed `SlsaError` subclasses with stable error codes.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [slsa-error](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [slsa-otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [canonical-json](./src/provenance/canonical-json.md) — Produces a byte-stable, lexicographically-ordered JSON encoding for provenance statements, SBOM documents, and DSSE pay…
|
||||
- [digest](./src/provenance/digest.md) — Single canonical entry point for SHA-256 digest computation inside the SLSA package. Deliberately routes through `@suav…
|
||||
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [inclusion-proof](./src/rekor/inclusion-proof.md) — Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor. Given a leaf (the canonical body a Rekor ent…
|
||||
- [rekor-client](./src/rekor/rekor-client.md) — Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan cares about: `hashedrekord` v0.0.1 (when logging…
|
||||
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [schemas](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [parse](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [checks](./src/verification/checks.md) — Individual, stateless verification rules that consume a provenance statement plus policy inputs and return a structured…
|
||||
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-error](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [canonical-json](./src/provenance/canonical-json.md) — Produces a byte-stable, lexicographically-ordered JSON encoding for provenance statements, SBOM documents, and DSSE pay…
|
||||
- [digest](./src/provenance/digest.md) — Single canonical entry point for SHA-256 digest computation inside the SLSA package. Deliberately routes through `@suav…
|
||||
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [inclusion-proof](./src/rekor/inclusion-proof.md) — Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor. Given a leaf (the canonical body a Rekor ent…
|
||||
- [rekor-client](./src/rekor/rekor-client.md) — Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan cares about: `hashedrekord` v0.0.1 (when logging…
|
||||
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [checks](./src/verification/checks.md) — Individual, stateless verification rules that consume a provenance statement plus policy inputs and return a structured…
|
||||
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-error](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [canonical-json](./src/provenance/canonical-json.md) — Produces a byte-stable, lexicographically-ordered JSON encoding for provenance statements, SBOM documents, and DSSE pay…
|
||||
- [digest](./src/provenance/digest.md) — Single canonical entry point for SHA-256 digest computation inside the SLSA package. Deliberately routes through `@suav…
|
||||
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [inclusion-proof](./src/rekor/inclusion-proof.md) — Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor. Given a leaf (the canonical body a Rekor ent…
|
||||
- [rekor-client](./src/rekor/rekor-client.md) — Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan cares about: `hashedrekord` v0.0.1 (when logging…
|
||||
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [checks](./src/verification/checks.md) — Individual, stateless verification rules that consume a provenance statement plus policy inputs and return a structured…
|
||||
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-error](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [canonical-json](./src/provenance/canonical-json.md) — Produces a byte-stable, lexicographically-ordered JSON encoding for provenance statements, SBOM documents, and DSSE pay…
|
||||
- [digest](./src/provenance/digest.md) — Single canonical entry point for SHA-256 digest computation inside the SLSA package. Deliberately routes through `@suav…
|
||||
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [inclusion-proof](./src/rekor/inclusion-proof.md) — Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor. Given a leaf (the canonical body a Rekor ent…
|
||||
- [rekor-client](./src/rekor/rekor-client.md) — Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan cares about: `hashedrekord` v0.0.1 (when logging…
|
||||
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [checks](./src/verification/checks.md) — Individual, stateless verification rules that consume a provenance statement plus policy inputs and return a structured…
|
||||
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [slsa-error](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [slsa-otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_ - [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_ - [canonical-json](./src/provenance/canonical-json.md) — Produces a byte-stable, lexicographically-ordered JSON encoding for provenance statements, SBOM documents, and DSSE pay…
|
||||
- [digest](./src/provenance/digest.md) — Single canonical entry point for SHA-256 digest computation inside the SLSA package. Deliberately routes through `@suav…
|
||||
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [inclusion-proof](./src/rekor/inclusion-proof.md) — Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor. Given a leaf (the canonical body a Rekor ent…
|
||||
- [rekor-client](./src/rekor/rekor-client.md) — Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan cares about: `hashedrekord` v0.0.1 (when logging…
|
||||
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_ - [parse](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_ - [checks](./src/verification/checks.md) — Individual, stateless verification rules that consume a provenance statement plus policy inputs and return a structured…
|
||||
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/slsa
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import {
|
||||
assessSlsaLevel,
|
||||
generateProvenance,
|
||||
generateSbom,
|
||||
RekorClient,
|
||||
verifyProvenance,
|
||||
wrapInDsse,
|
||||
} from "@suaveplan/slsa";
|
||||
|
||||
const statement = generateProvenance({
|
||||
packageName: "@suaveplan/utils",
|
||||
packageVersion: "1.2.3",
|
||||
artifactDigest: "abc123...",
|
||||
sourceCommit: "deadbeef...",
|
||||
sourceRepo: "https://gitlab.fq.io/suaveplan/project",
|
||||
sourceRef: "refs/heads/main",
|
||||
builderId: "https://gitlab.fq.io/suaveplan/project/-/runners/1",
|
||||
buildInvocationId: "ci-12345",
|
||||
buildStartedOn: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const result = verifyProvenance(statement, {
|
||||
trustedBuilders: ["https://gitlab.fq.io/suaveplan/project/-/runners/*"],
|
||||
expectedSourceRepo: "https://gitlab.fq.io/suaveplan/project",
|
||||
});
|
||||
|
||||
const sbom = generateSbom({
|
||||
packageName: "@suaveplan/utils",
|
||||
packageVersion: "1.2.3",
|
||||
dependencies: { lodash: "4.17.21" },
|
||||
});
|
||||
|
||||
const envelope = wrapInDsse(statement, signatures);
|
||||
const rekor = new RekorClient();
|
||||
const entry = await rekor.uploadIntoto(envelope);
|
||||
|
||||
console.log(assessSlsaLevel({ ...environment, hasRekorInclusion: true }).level);
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`errors/slsa-error`](./src/errors/slsa-error.md) — typed error hierarchy
|
||||
- stable `SLSA_*` code registry.
|
||||
- [`otel/slsa-otel`](./src/otel/slsa-otel.md) — OTel audit-event adapter.
|
||||
- [`schemas/parse`](./src/schemas/parse.md) — Zod-validated JSON parsers.
|
||||
- [`provenance/canonical-json`](./src/provenance/canonical-json.md) —
|
||||
deterministic JSON encoder.
|
||||
- [`provenance/digest`](./src/provenance/digest.md) — SHA-256 helper over
|
||||
`@suaveplan/crypto-hash`.
|
||||
- [`provenance/provenance`](./src/provenance/provenance.md) — in-toto /
|
||||
SLSA v1.0 statement generator.
|
||||
- [`verification/checks`](./src/verification/checks.md) — individual
|
||||
verification rules.
|
||||
- [`verification/verification`](./src/verification/verification.md) —
|
||||
policy-driven verifier.
|
||||
- [`sbom/sbom`](./src/sbom/sbom.md) — CycloneDX 1.5 SBOM generator.
|
||||
- [`assessment/assessment`](./src/assessment/assessment.md) — SLSA L0-L3
|
||||
environment assessor.
|
||||
- [`attestation/dsse/dsse`](./src/attestation/dsse/dsse.md) — DSSE PAE,
|
||||
envelope wrapper, ed25519 verification.
|
||||
- [`rekor/inclusion-proof`](./src/rekor/inclusion-proof.md) — RFC 6962
|
||||
Merkle inclusion proof verification.
|
||||
- [`rekor/rekor-client`](./src/rekor/rekor-client.md) — Sigstore Rekor
|
||||
REST client.
|
||||
- [`policy/policy`](./src/policy/policy.md) — composite policy evaluator.
|
||||
|
||||
## Status
|
||||
|
||||
- Tests: 170 passing (13 files).
|
||||
- Coverage: 99.2% statements, 100% functions, 99.44% lines, 95.15% branches.
|
||||
- Typecheck, lint, and build all green.
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# assessment
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Evaluates the build environment against SLSA Build track levels L0-L3 and
|
||||
returns a structured `SlsaAssessment`. Intended for CI dashboards, publish
|
||||
gates, and compliance audits so teams can see in plain language which
|
||||
controls are in place and which are missing. The assessor is pure: it
|
||||
takes signals via `AssessmentEnvironment` and has no side effects, so it
|
||||
can be fed synthetic input in tests.
|
||||
|
||||
## Features
|
||||
|
||||
- L0-L3 progression matching the SLSA Build specification.
|
||||
- Rekor inclusion is an optional L3 gate — when `hasRekorInclusion` is
|
||||
`undefined` it is ignored; when `false` it blocks L3 even if all other
|
||||
controls are met.
|
||||
- Parallel `met` / `unmet` string lists for dashboard rendering.
|
||||
- `detectEnvironment()` reads `process.env` to auto-populate the signals
|
||||
from the SLSA convention variables (`SLSA_*`, `CI`, `GITLAB_CI`,
|
||||
`GITHUB_ACTIONS`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { assessSlsaLevel } from "@suaveplan/slsa";
|
||||
|
||||
const result = assessSlsaLevel({
|
||||
isCI: true,
|
||||
hasProvenance: true,
|
||||
hasIsolatedBuild: false,
|
||||
hasHardenedProvenance: false,
|
||||
ciOnlyPublish: true,
|
||||
hasSbom: true,
|
||||
hasDigestVerification: true,
|
||||
hasVersionControl: true,
|
||||
});
|
||||
|
||||
console.log(result.level); // "L2"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { assessSlsaLevel, detectEnvironment } from "@suaveplan/slsa";
|
||||
|
||||
const env = { ...detectEnvironment(), hasRekorInclusion: await verifyLogs() };
|
||||
const result = assessSlsaLevel(env);
|
||||
|
||||
if (result.level !== "L3") {
|
||||
log.warn({ missing: result.unmet }, "publish blocked below L3");
|
||||
process.exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `assessSlsaLevel(env: AssessmentEnvironment): SlsaAssessment` — returns
|
||||
`{ level, met, unmet }`.
|
||||
- `detectEnvironment(): AssessmentEnvironment` — reads `SLSA_*` + CI env
|
||||
vars. `hasVersionControl` is hard-wired to `true` because the project
|
||||
is Git-backed.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`hasRekorInclusion` has three states: `undefined` (ignored), `true` (L3
|
||||
eligible), `false` (L3 blocked). This lets callers opt in to the Rekor
|
||||
gate without forcing it on every existing pipeline. The level-determination
|
||||
logic cascades — L3 implies L2 implies L1 — so adding additional L3
|
||||
constraints only requires appending to the L3 conjunction.
|
||||
@@ -0,0 +1,87 @@
|
||||
# dsse
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the Dead Simple Signing Envelope (DSSE) binding used by
|
||||
in-toto/SLSA attestations. Provides the Pre-Authentication Encoding (PAE)
|
||||
function that callers sign, an envelope constructor that wraps a provenance
|
||||
statement with supplied signatures, and a verification entry point that
|
||||
checks every signature against a set of trusted ed25519 keys. The module
|
||||
deliberately does not sign — Suaveplan signing keys live only inside the
|
||||
publish pipeline, never in consumer bundles — but it provides everything
|
||||
needed for attestation producers and verifiers to interoperate.
|
||||
|
||||
## Features
|
||||
|
||||
- RFC-aligned `DSSEv1 <typeLen> <type> <payloadLen> <payload>` PAE.
|
||||
- `wrapInDsse` binds a canonicalised `ProvenanceStatement` to a
|
||||
base64-encoded payload plus caller-supplied signatures.
|
||||
- `verifyDsseEnvelope` iterates every signature and reports which trusted
|
||||
key (if any) accepted it, returning a structured `DsseVerificationResult`.
|
||||
- `keyid`-scoped key matching so the same envelope can carry signatures
|
||||
from multiple key material with disjoint trust roots.
|
||||
- Structured `AttestationInvalidError` on empty signature lists or empty
|
||||
key lists.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encodeDssePae, wrapInDsse } from "@suaveplan/slsa";
|
||||
|
||||
const pae = encodeDssePae(
|
||||
"application/vnd.in-toto+json",
|
||||
new TextEncoder().encode(JSON.stringify(statement))
|
||||
);
|
||||
const signature = ed25519.sign(secretKey, pae);
|
||||
|
||||
const envelope = wrapInDsse(statement, [
|
||||
{ sig: toBase64(signature), keyid: "release-key" },
|
||||
]);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
verifyDsseEnvelope,
|
||||
type TrustedEd25519Key,
|
||||
} from "@suaveplan/slsa";
|
||||
|
||||
const trusted: TrustedEd25519Key[] = [
|
||||
{ publicKey: releaseKey, keyid: "release-key" },
|
||||
{ publicKey: rotationKey, keyid: "release-key-next" },
|
||||
];
|
||||
|
||||
const result = verifyDsseEnvelope(envelope, trusted);
|
||||
if (!result.valid) {
|
||||
const bad = result.records.filter((r) => !r.valid);
|
||||
log.warn({ bad }, "DSSE envelope failed verification");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DSSE_PAYLOAD_TYPE` — canonical in-toto payload type.
|
||||
- `encodeDssePae(payloadType, payload): Uint8Array` — exact byte sequence
|
||||
that must be signed.
|
||||
- `wrapInDsse(statement, signatures): DsseEnvelope` — canonicalises the
|
||||
statement, base64-encodes the payload, attaches the signatures. Throws
|
||||
`AttestationInvalidError` on empty `signatures`.
|
||||
- `verifyDsseEnvelope(envelope, keys): DsseVerificationResult` — verifies
|
||||
every signature. Throws `AttestationInvalidError` when `keys` is empty.
|
||||
Returns per-signature `DsseVerificationRecord`s with `keyid`, `keyIndex`,
|
||||
`signatureIndex`, and a boolean `valid`.
|
||||
- `TrustedEd25519Key { publicKey, keyid? }` — public-key bundle.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Signing is intentionally absent from the public API — the DSSE spec
|
||||
separates the PAE construction (standardised here) from the signing
|
||||
algorithm (ed25519 only for now). Consumers that want ECDSA / RSA signing
|
||||
can use `@suaveplan/crypto` directly and pass the resulting `sig` into
|
||||
`wrapInDsse`. The PAE uses the exact space-delimited format from the DSSE
|
||||
spec (`DSSEv1 <len> <type> <len> <body>`) — do not reformat whitespace, as
|
||||
even a stray trailing space changes signed bytes and breaks verifiers.
|
||||
81
docs/security-stack/security/slsa/src/errors/slsa-error.md
Normal file
81
docs/security-stack/security/slsa/src/errors/slsa-error.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# slsa-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides the structured error substrate for the `@suaveplan/slsa` package. Every
|
||||
failure raised by provenance generation, verification, SBOM emission, DSSE
|
||||
wrapping, Rekor transparency-log operations, and policy evaluation flows
|
||||
through a `SlsaError` subclass carrying a stable, namespaced error code drawn
|
||||
from `SLSA_CODES`. This makes it safe to pattern-match on `err.code` in
|
||||
consumer code and to redact / serialise errors with the rest of the Suaveplan
|
||||
observability pipeline.
|
||||
|
||||
## Features
|
||||
|
||||
- Namespaced, frozen registry built via `@suaveplan/error`'s `defineCodes`.
|
||||
- One domain-specific subclass per code (`InvalidProvenanceError`,
|
||||
`VerificationFailedError`, `RekorUploadError`, `RekorInclusionError`,
|
||||
`PolicyViolationError`, `AttestationInvalidError`,
|
||||
`EnvironmentUnsupportedError`, `InvalidSbomError`).
|
||||
- `instanceof SlsaError` narrows to the full SLSA code union.
|
||||
- `cause`, `context`, `traceId` fields inherited from `SuaveplanError`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { InvalidProvenanceError } from "@suaveplan/slsa";
|
||||
|
||||
throw new InvalidProvenanceError("error.slsa.invalid_provenance", {
|
||||
context: { missingField: "builderId" },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { RekorInclusionError, SLSA_CODES, SlsaError } from "@suaveplan/slsa";
|
||||
|
||||
try {
|
||||
await client.verifyInclusionProof(entry);
|
||||
} catch (cause) {
|
||||
throw new RekorInclusionError("error.slsa.rekor.inclusion_failed", {
|
||||
cause: cause as Error,
|
||||
context: { uuid: entry.uuid, logIndex: entry.logIndex },
|
||||
traceId: currentSpan?.spanContext().traceId,
|
||||
});
|
||||
}
|
||||
|
||||
// Downstream handler
|
||||
if (err instanceof SlsaError && err.code === SLSA_CODES.REKOR_INCLUSION_FAILED) {
|
||||
metrics.rekorInclusionFailures.add(1);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `SLSA_CODES` — frozen registry of stable error-code strings.
|
||||
- `SlsaCode` — union type of all SLSA error codes.
|
||||
- `SlsaError` — base class; extends `SuaveplanError<SlsaCode>`.
|
||||
- `InvalidProvenanceError(messageKey, options?)` — schema / shape failure.
|
||||
- `InvalidSbomError(messageKey, options?)` — SBOM schema failure.
|
||||
- `VerificationFailedError(messageKey, options?)` — provenance verification rejected.
|
||||
- `PolicyViolationError(messageKey, options?)` — policy evaluator rejection.
|
||||
- `RekorUploadError(messageKey, options?)` — transparency-log HTTP / protocol failure.
|
||||
- `RekorInclusionError(messageKey, options?)` — merkle proof failed verification.
|
||||
- `AttestationInvalidError(messageKey, options?)` — DSSE envelope / signature invalid.
|
||||
- `EnvironmentUnsupportedError(messageKey, options?)` — runtime lacks capability.
|
||||
|
||||
All constructors accept `SuaveplanErrorOptions` (`{ cause?, context?, traceId?, clock? }`).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Subclasses are thin wrappers that bind a specific `SLSA_CODES` value, so
|
||||
`new InvalidProvenanceError(...)` and `new SlsaError(SLSA_CODES.INVALID_PROVENANCE, ...)`
|
||||
are behaviourally identical. Message keys are strings — resolution to a human
|
||||
message happens through `@suaveplan/error`'s i18n resolver so catalogs can be
|
||||
localised without touching throw sites. The class name is preserved in
|
||||
`err.name` via the `SuaveplanError` base constructor, which makes stack-trace
|
||||
capture correct even under V8's `captureStackTrace`.
|
||||
74
docs/security-stack/security/slsa/src/otel/slsa-otel.md
Normal file
74
docs/security-stack/security/slsa/src/otel/slsa-otel.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# slsa-otel
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
OpenTelemetry adapter for SLSA audit events. Every major capability in the
|
||||
package — provenance generation, verification, Rekor upload, inclusion-proof
|
||||
verification, policy evaluation — emits a structured span event through this
|
||||
adapter. By default the adapter attaches events to the currently-active span,
|
||||
so it composes naturally with the calling service's trace context without
|
||||
SLSA needing to manage its own span lifecycle.
|
||||
|
||||
## Features
|
||||
|
||||
- Five well-known event names: `slsa.provenance.generated`,
|
||||
`slsa.provenance.verified`, `slsa.rekor.upload`,
|
||||
`slsa.rekor.inclusion_verified`, `slsa.policy.violation`.
|
||||
- Typed attribute bag (`SlsaEventAttributes`) under the `slsa.*` namespace.
|
||||
- Zero-config no-op default when no exporter is registered.
|
||||
- Explicit `tracer` override for background / detached contexts.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createSlsaOtelAdapter, generateProvenance } from "@suaveplan/slsa";
|
||||
|
||||
const otel = createSlsaOtelAdapter();
|
||||
const stmt = generateProvenance({
|
||||
/* ...input... */,
|
||||
otel,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { trace } from "@opentelemetry/api";
|
||||
import { createSlsaOtelAdapter, RekorClient } from "@suaveplan/slsa";
|
||||
|
||||
const tracer = trace.getTracer("publish-pipeline");
|
||||
const otel = createSlsaOtelAdapter(tracer);
|
||||
|
||||
const client = new RekorClient({ otel });
|
||||
await tracer.startActiveSpan("publish", async (span) => {
|
||||
try {
|
||||
const entry = await client.uploadHashedrekord(rekord);
|
||||
await client.verifyInclusionProof(entry);
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `SlsaEventAttributes` — named attribute bag; every field is optional.
|
||||
- `SlsaOtelAdapter` — interface with five `on*` methods, all returning `void`.
|
||||
- `createSlsaOtelAdapter(tracer?)` — returns a live adapter. With no
|
||||
`tracer`, events attach to the active span; otherwise spans are started
|
||||
inline and ended.
|
||||
- `noopSlsaOtelAdapter` — default adapter that discards events.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Span events are preferred over metrics because SLSA events are inherently
|
||||
correlated: "generated", "verified", and "logged to Rekor" usually happen
|
||||
inside a single CI job's trace. Counters would lose that correlation.
|
||||
Attributes are typed as a closed shape so accidental typos surface as
|
||||
compile-time errors, but the runtime cast to `Record<string, never>` is used
|
||||
because `@opentelemetry/api`'s `AttributeValue` type is wider than our event
|
||||
schema and TypeScript's invariance on index signatures would otherwise
|
||||
require an unsafe upcast at every call site.
|
||||
88
docs/security-stack/security/slsa/src/policy/policy.md
Normal file
88
docs/security-stack/security/slsa/src/policy/policy.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# policy
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`evaluatePolicy` is the composition point that layers every other
|
||||
verification capability into a single decision: given a provenance
|
||||
statement, its optional DSSE envelope, its optional Rekor entry, and a
|
||||
`VerificationPolicy`, return one `VerificationResult` with every relevant
|
||||
rule reported. Where `verifyProvenance` only runs structural rules and
|
||||
doesn't know about signatures or transparency logs, `evaluatePolicy` ties
|
||||
the full chain of trust together and is what release-gate scripts should
|
||||
call.
|
||||
|
||||
## Features
|
||||
|
||||
- Runs the baseline `verifyProvenance` suite first, then appends:
|
||||
- DSSE signature verification against `policy.trustedPublicKeys`.
|
||||
- Rekor inclusion-proof presence (when `policy.requireRekorInclusion`).
|
||||
- Minimum-level assertion against an explicit `pipelineLevel`.
|
||||
- OTel `slsa.policy.violation` event emitted on any failure, with
|
||||
violation count and required level attached.
|
||||
- Returns structured `VerificationResult` — never throws on policy failure.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { evaluatePolicy } from "@suaveplan/slsa";
|
||||
|
||||
const result = evaluatePolicy(
|
||||
statement,
|
||||
envelope,
|
||||
rekorEntry,
|
||||
{
|
||||
trustedBuilders: ["https://gitlab.fq.io/suaveplan/project/-/runners/*"],
|
||||
trustedPublicKeys: [releaseKey],
|
||||
requireRekorInclusion: true,
|
||||
minSlsaLevel: "L2",
|
||||
},
|
||||
{ pipelineLevel: "L3" }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createSlsaOtelAdapter,
|
||||
evaluatePolicy,
|
||||
RekorClient,
|
||||
} from "@suaveplan/slsa";
|
||||
|
||||
const otel = createSlsaOtelAdapter();
|
||||
const client = new RekorClient({ otel });
|
||||
const rekorEntry = await client.uploadIntoto(envelope);
|
||||
const result = evaluatePolicy(statement, envelope, rekorEntry, policy, {
|
||||
otel,
|
||||
clock: () => Date.now(),
|
||||
pipelineLevel: "L2",
|
||||
});
|
||||
|
||||
if (!result.valid) {
|
||||
for (const c of result.checks.filter((c) => !c.passed)) {
|
||||
log.warn({ rule: c.name, message: c.message }, "policy violation");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `evaluatePolicy(statement, envelope | null, rekorEntry | null, policy,
|
||||
opts?)` — returns `VerificationResult`. `opts.pipelineLevel` must be
|
||||
supplied when `policy.minSlsaLevel` is set; otherwise the check fails
|
||||
with a self-describing message.
|
||||
- `EvaluatePolicyOptions { otel?, clock?, pipelineLevel? }` — all optional.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The extra checks are appended, never interleaved, so existing consumers
|
||||
that match on `checks[i].name` stay stable. `checkDsseSignatures` builds
|
||||
`TrustedEd25519Key` records without `keyid` by default — if keyids are
|
||||
required for scoping, call `verifyDsseEnvelope` directly and feed the
|
||||
derived checks in. `minSlsaLevel` uses an ordinal comparison
|
||||
(`L0 < L1 < L2 < L3`) rather than string equality so `"L3"` satisfies
|
||||
`minSlsaLevel: "L2"` without additional policy tweaks.
|
||||
@@ -0,0 +1,59 @@
|
||||
# canonical-json
|
||||
|
||||
## Purpose
|
||||
|
||||
Produces a byte-stable, lexicographically-ordered JSON encoding for
|
||||
provenance statements, SBOM documents, and DSSE payloads. Deterministic
|
||||
encoding is required whenever a document will be hashed, signed, or
|
||||
compared across producers — two semantically equivalent documents must
|
||||
always serialise to the same bytes, otherwise digest and signature
|
||||
verification become order-dependent.
|
||||
|
||||
## Features
|
||||
|
||||
- Recursive key sorting (parent, then children).
|
||||
- `undefined` properties are omitted (matching `JSON.stringify` semantics).
|
||||
- Arrays are preserved in source order (ordering carries semantic meaning for
|
||||
verification checks, Merkle proofs, etc.).
|
||||
- Pure, zero-dependency, allocation-bounded.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { canonicalize } from "@suaveplan/slsa";
|
||||
|
||||
canonicalize({ b: 2, a: 1 }); // '{"a":1,"b":2}'
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { canonicalize } from "@suaveplan/slsa";
|
||||
import { sha256 } from "@suaveplan/crypto-hash";
|
||||
import { toHex } from "@suaveplan/crypto-utils";
|
||||
|
||||
const digest = toHex(
|
||||
sha256(new TextEncoder().encode(canonicalize(provenanceStatement)))
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `canonicalize(value: unknown): string` — returns a deterministic JSON
|
||||
string. Supports plain objects, arrays, strings, numbers, booleans, and
|
||||
`null`. Functions, symbols, `undefined` values, and class instances are
|
||||
treated as plain records (own-enumerable keys only).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The implementation deliberately avoids JCS (RFC 8785) full compliance — SLSA
|
||||
in-toto statements use sort-keys-only canonicalisation, which this matches.
|
||||
We do not normalise numeric representations (e.g. `1e2` vs `100`), because
|
||||
SLSA predicates do not contain non-integer numerics outside timestamps
|
||||
(which are strings). `undefined` is treated as "omit this field" so that
|
||||
calling code can opt a subject out with `field: undefined` without forcing
|
||||
all consumers to delete the key.
|
||||
|
||||
### Why Canonical JSON
|
||||
|
||||
In-toto / DSSE signing requires the bytes signed to be bit-exact so verifiers reproduce the hash. Standard `JSON.stringify` is not canonical — object key order, number-formatting, and string-escape choices all vary. `canonicalJson` follows RFC 8785 JCS: sorted keys, shortest-form numbers, minimal string escapes. Output is always UTF-8 and deterministic across Node / Bun / browsers.
|
||||
58
docs/security-stack/security/slsa/src/provenance/digest.md
Normal file
58
docs/security-stack/security/slsa/src/provenance/digest.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# digest
|
||||
|
||||
## Purpose
|
||||
|
||||
Single canonical entry point for SHA-256 digest computation inside the SLSA
|
||||
package. Deliberately routes through `@suaveplan/crypto-hash` instead of
|
||||
`node:crypto` so the primitive selector layer chooses the correct backend
|
||||
(Noble / WebCrypto / native) at build time, and so the package stays
|
||||
runtime-agnostic for non-Node environments.
|
||||
|
||||
## Features
|
||||
|
||||
- Accepts `Uint8Array` or UTF-8 strings.
|
||||
- Returns lowercase hex via `@suaveplan/crypto-utils`'s `toHex`.
|
||||
- Uses the package-wide `sha256` selector — no direct noble/webcrypto import.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { computeDigest } from "@suaveplan/slsa";
|
||||
|
||||
const d = computeDigest("hello");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { computeDigest } from "@suaveplan/slsa";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
const artifact = await fs.readFile("./dist/@suaveplan-utils-1.2.3.tgz");
|
||||
const digest = computeDigest(new Uint8Array(artifact));
|
||||
// feed into generateProvenance({ artifactDigest: digest, ... })
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `computeDigest(content: Uint8Array | string): string` — returns a 64-char
|
||||
lowercase hex SHA-256. Strings are encoded as UTF-8 before hashing.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The string branch uses `TextEncoder` (available in all modern Node / Bun /
|
||||
browsers) rather than `Buffer.from(str)` so the helper remains universal.
|
||||
SHA-512 is intentionally not exposed here — callers that need it should
|
||||
compose `sha512` from `@suaveplan/crypto-hash` with `toHex` directly, which
|
||||
keeps the SLSA surface tight and the digest format homogeneous.
|
||||
|
||||
### Hash Family
|
||||
|
||||
`digestProvenance` computes the SHA-256 of the canonical-JSON encoding of an in-toto statement and returns it as a hex string. SHA-256 is the only algorithm currently surfaced because it is the only one the Sigstore transparency log (Rekor) accepts for hashedrekord v0.0.2 entries. The helper composes `canonicalJson` and `sha256` from `@suaveplan/crypto-hash` so there is no custom hashing logic local to this module.
|
||||
|
||||
## Further Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { digestProvenance } from "@suaveplan/slsa/provenance/digest";
|
||||
const hex = await digestProvenance({ _type: "https://in-toto.io/Statement/v1" } as any);
|
||||
```
|
||||
@@ -0,0 +1,89 @@
|
||||
# provenance
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Generates SLSA v1.0 provenance statements wrapped in in-toto v1 envelopes.
|
||||
Each statement records the artifact digest, builder identity, source
|
||||
repository, git ref, and build timing — the tamper-evident audit trail that
|
||||
lets downstream consumers verify an artifact was built by a trusted pipeline
|
||||
from an expected source.
|
||||
|
||||
## Features
|
||||
|
||||
- Canonical in-toto / SLSA v1.0 envelope shape.
|
||||
- Deterministic serialisation through `canonicalize` (sorted keys).
|
||||
- SHA-256 digest of the canonical encoding for signing / transparency logs.
|
||||
- Builder version auto-detection (Bun in CI; `unknown` elsewhere).
|
||||
- Structured `InvalidProvenanceError` for any malformed input.
|
||||
- OTel `slsa.provenance.generated` span event with builder + source attributes.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generateProvenance, hashProvenance } from "@suaveplan/slsa";
|
||||
|
||||
const statement = generateProvenance({
|
||||
packageName: "@suaveplan/utils",
|
||||
packageVersion: "1.2.3",
|
||||
artifactDigest: "abc123...",
|
||||
sourceCommit: "deadbeef...",
|
||||
sourceRepo: "https://gitlab.fq.io/suaveplan/project",
|
||||
sourceRef: "refs/heads/main",
|
||||
builderId: "https://gitlab.fq.io/suaveplan/project/-/runners/1",
|
||||
buildInvocationId: "ci-12345",
|
||||
buildStartedOn: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const digest = hashProvenance(statement);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createSlsaOtelAdapter,
|
||||
generateProvenance,
|
||||
serializeProvenance,
|
||||
} from "@suaveplan/slsa";
|
||||
|
||||
const otel = createSlsaOtelAdapter();
|
||||
const statement = generateProvenance({
|
||||
/* ...fields... */,
|
||||
resolvedDependencies: [
|
||||
{
|
||||
uri: "pkg:npm/lodash@4.17.21",
|
||||
digest: { sha256: "lodashHashHex" },
|
||||
name: "lodash",
|
||||
},
|
||||
],
|
||||
otel,
|
||||
});
|
||||
|
||||
await fs.writeFile("provenance.json", serializeProvenance(statement));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `generateProvenance(input: ProvenanceInput): ProvenanceStatement` —
|
||||
validates all required fields, returns the in-toto envelope. Throws
|
||||
`InvalidProvenanceError` when any field is empty, non-string, or when
|
||||
`buildStartedOn` / `buildFinishedOn` are not parseable ISO 8601.
|
||||
- `serializeProvenance(statement): string` — canonical (sorted-keys) JSON.
|
||||
- `hashProvenance(statement): string` — SHA-256 hex of the canonical
|
||||
serialisation, used as the subject of Rekor hashedrekord entries.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`buildType` is pinned to `https://suaveplan.app/slsa/build-type/bun-project/v1`
|
||||
— bumping this identifier is a semver break, since downstream verifiers may
|
||||
pattern-match on it. `resolvedDependencies` is emitted at both
|
||||
`buildDefinition.resolvedDependencies` and
|
||||
`buildDefinition.internalParameters.resolvedDependencies` to match the two
|
||||
common shapes found in GitHub's SLSA generator and the SLSA reference CLI.
|
||||
Builder version detection uses `globalThis.Bun` rather than a top-level
|
||||
import so the module is importable from non-Bun runtimes without a cold
|
||||
failure. Hashing uses the same canonicalisation function as DSSE payload
|
||||
encoding, so "the thing we signed" always matches "the thing we hashed".
|
||||
@@ -0,0 +1,79 @@
|
||||
# inclusion-proof
|
||||
|
||||
## Purpose
|
||||
|
||||
Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor.
|
||||
Given a leaf (the canonical body a Rekor entry was logged as), a sibling
|
||||
audit path, the leaf's log index, the tree size at inclusion time, and the
|
||||
signed root hash, recomputes the root and confirms it matches. This is the
|
||||
cryptographic half of Rekor trust — once the root matches, the transparency
|
||||
log has published the entry and cannot later lie about having done so.
|
||||
|
||||
## Features
|
||||
|
||||
- RFC 6962 hashing with explicit domain separators (`0x00` for leaves,
|
||||
`0x01` for interior nodes).
|
||||
- Correct handling of last-node-at-level carrying (odd tree sizes).
|
||||
- Structured `RekorInclusionError` when `logIndex` is out of range.
|
||||
- Exposes `rfc6962LeafHash` for batch / cache use-cases.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyRfc6962InclusionProof } from "@suaveplan/slsa";
|
||||
|
||||
const { valid, computedRootHex } = verifyRfc6962InclusionProof({
|
||||
leafBytes: new TextEncoder().encode(entry.body),
|
||||
hashes: entry.verification.inclusionProof.hashes,
|
||||
logIndex: entry.logIndex,
|
||||
treeSize: entry.verification.inclusionProof.treeSize,
|
||||
expectedRootHex: entry.verification.inclusionProof.rootHash,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
RekorClient,
|
||||
rfc6962LeafHash,
|
||||
verifyRfc6962InclusionProof,
|
||||
} from "@suaveplan/slsa";
|
||||
|
||||
const client = new RekorClient();
|
||||
const entry = await client.uploadHashedrekord(rekord);
|
||||
const leafBytes = new TextEncoder().encode(entry.body);
|
||||
|
||||
const leafHex = rfc6962LeafHash(leafBytes);
|
||||
log.info({ leafHex, logIndex: entry.logIndex }, "rekor entry hashed");
|
||||
|
||||
const proof = entry.verification?.inclusionProof;
|
||||
if (proof) {
|
||||
const { valid } = verifyRfc6962InclusionProof({
|
||||
leafBytes,
|
||||
hashes: proof.hashes,
|
||||
logIndex: entry.logIndex,
|
||||
treeSize: proof.treeSize,
|
||||
expectedRootHex: proof.rootHash,
|
||||
});
|
||||
if (!valid) throw new Error("inclusion failed");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyRfc6962InclusionProof(args): { valid, computedRootHex }` — returns
|
||||
whether the computed root matches. Throws `RekorInclusionError` when
|
||||
`logIndex >= treeSize` or `logIndex < 0`.
|
||||
- `rfc6962LeafHash(body: Uint8Array): string` — lowercase hex SHA-256 with
|
||||
the `0x00` leaf prefix.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The carry-up branch (odd-count level) handles trees where the last node at a
|
||||
given level has no sibling — rather than duplicating, RFC 6962 propagates
|
||||
the node up unchanged. Rekor's server-side proof generator will either
|
||||
produce a shorter path for such leaves or include a duplicate; this
|
||||
implementation tolerates the former. String comparison is
|
||||
case-insensitive on the expected root to match Sigstore's mixed-case hex
|
||||
output.
|
||||
90
docs/security-stack/security/slsa/src/rekor/rekor-client.md
Normal file
90
docs/security-stack/security/slsa/src/rekor/rekor-client.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# rekor-client
|
||||
|
||||
## Purpose
|
||||
|
||||
Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan
|
||||
cares about: `hashedrekord` v0.0.1 (when logging a raw artifact digest
|
||||
with an ed25519 signature) and `intoto` v0.0.2 (when logging a DSSE-
|
||||
wrapped provenance statement). Also provides entry retrieval by uuid or
|
||||
logIndex and an inclusion-proof verifier that reuses the RFC 6962 audit
|
||||
path helper in the sibling module. The client is designed around an
|
||||
injectable `fetch`, so unit tests can run fully offline and non-Node
|
||||
runtimes can plug in their own client.
|
||||
|
||||
## Features
|
||||
|
||||
- `hashedrekord` and `intoto` upload.
|
||||
- `getEntryByUuid` / `getEntryByLogIndex` retrieval.
|
||||
- `verifyInclusionProof` returning `{ valid, reason? }` rather than
|
||||
throwing — easier to branch on in policy pipelines.
|
||||
- AbortSignal support on every HTTP call.
|
||||
- Structured `RekorUploadError` on non-2xx responses, with the status,
|
||||
statusText, and response body attached as context.
|
||||
- Response shape normalisation: Rekor returns `{ [uuid]: entry }`; the
|
||||
client flattens that to the Zod-validated `RekorEntry` shape.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RekorClient } from "@suaveplan/slsa";
|
||||
|
||||
const client = new RekorClient();
|
||||
const entry = await client.uploadHashedrekord({
|
||||
signature: {
|
||||
content: base64Signature,
|
||||
publicKey: { content: base64PublicKey },
|
||||
},
|
||||
data: { hash: { algorithm: "sha256", value: digestHex } },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createSlsaOtelAdapter,
|
||||
RekorClient,
|
||||
verifyDsseEnvelope,
|
||||
wrapInDsse,
|
||||
} from "@suaveplan/slsa";
|
||||
|
||||
const otel = createSlsaOtelAdapter();
|
||||
const client = new RekorClient({
|
||||
baseUrl: "https://rekor.example.com",
|
||||
fetch: myFetch,
|
||||
otel,
|
||||
});
|
||||
|
||||
const envelope = wrapInDsse(provenanceStatement, signatures);
|
||||
const entry = await client.uploadIntoto(envelope, { signal: abort.signal });
|
||||
const { valid, reason } = client.verifyInclusionProof(entry);
|
||||
if (!valid) throw new Error(`rekor inclusion failed: ${reason ?? "?"}`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new RekorClient(options?)` — fields: `baseUrl` (default
|
||||
`https://rekor.sigstore.dev`), `fetch` (default `globalThis.fetch`),
|
||||
`clock`, `otel`.
|
||||
- `uploadHashedrekord(body, { signal }?)` — POST; returns `RekorEntry`.
|
||||
- `uploadIntoto(envelope, { signal }?)` — POST; returns `RekorEntry`.
|
||||
Computes the payload SHA-256 for the intoto v0.0.2 `content.hash` field
|
||||
automatically.
|
||||
- `getEntryByUuid(uuid, { signal }?)` — GET.
|
||||
- `getEntryByLogIndex(logIndex, { signal }?)` — GET.
|
||||
- `verifyInclusionProof(entry)` — reuses `verifyRfc6962InclusionProof`.
|
||||
Emits the `slsa.rekor.inclusion_verified` OTel event.
|
||||
- `RekorFetch` — minimal type describing the fetch shape the client uses.
|
||||
- `HashedrekordBody` — strongly-typed spec body.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Response parsing validates with a Zod schema rather than trusting the
|
||||
remote server. A malformed response therefore surfaces as a
|
||||
`RekorUploadError` (not an uncaught `TypeError`) which is far easier to
|
||||
alert on. The `fetch` type is intentionally narrow — we only use `method`,
|
||||
`headers`, `body`, `signal`, and the `.ok / .json() / .text()` shape of
|
||||
the response — so stubbing in tests requires no DOM types. HTTP errors
|
||||
capture the response body as `context.body` when it's still readable, but
|
||||
silently discard if the body was already consumed by the browser fetch
|
||||
implementation.
|
||||
83
docs/security-stack/security/slsa/src/sbom/sbom.md
Normal file
83
docs/security-stack/security/slsa/src/sbom/sbom.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# sbom
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Generates CycloneDX 1.5 compatible Software Bill of Materials documents for
|
||||
packages in Suaveplan. An SBOM lists every component a package
|
||||
depends on, keyed by `purl`, enabling downstream vulnerability scanners,
|
||||
license audits, and provenance correlation. Alongside generation, this
|
||||
module exports canonical serialisation, SHA-256 digesting, and `purl`
|
||||
construction so other packages (notably the publish pipeline) can
|
||||
reproduce the same output bytes.
|
||||
|
||||
## Features
|
||||
|
||||
- CycloneDX 1.5 schema adherence, validated by the sibling Zod schema.
|
||||
- Scoped-package `purl` construction (`pkg:npm/%40scope/name@ver`).
|
||||
- Workspace/catalog placeholder resolution (`workspace:*` → `*`,
|
||||
`catalog:key` → `catalog-key`).
|
||||
- Deterministic sort-key serialisation via `canonicalize`.
|
||||
- Injectable `clock` and `serial` factories for deterministic tests.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generateSbom } from "@suaveplan/slsa";
|
||||
|
||||
const sbom = generateSbom({
|
||||
packageName: "@suaveplan/utils",
|
||||
packageVersion: "1.2.3",
|
||||
dependencies: { lodash: "4.17.21" },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { generateSbom, hashSbom, serializeSbom } from "@suaveplan/slsa";
|
||||
|
||||
const sbom = generateSbom(
|
||||
{
|
||||
packageName: "@suaveplan/app",
|
||||
packageVersion: "2.0.0",
|
||||
packageLicense: "MIT",
|
||||
componentType: "application",
|
||||
dependencies: {
|
||||
"@suaveplan/crypto": "workspace:*",
|
||||
vitest: "catalog:testing",
|
||||
},
|
||||
},
|
||||
{
|
||||
clock: () => 0,
|
||||
serial: () => "urn:uuid:deadbeef-dead-beef-dead-beefdeadbeef",
|
||||
}
|
||||
);
|
||||
|
||||
console.log(hashSbom(sbom)); // stable across runs
|
||||
await fs.writeFile("sbom.json", serializeSbom(sbom));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `generateSbom(input, opts?): Sbom` — constructs the SBOM. Throws
|
||||
`InvalidSbomError` when `packageName`, `packageVersion`, or `dependencies`
|
||||
is missing.
|
||||
- `GenerateSbomOptions { clock?, serial? }` — deterministic seam.
|
||||
- `serializeSbom(sbom): string` — canonical JSON encoding.
|
||||
- `hashSbom(sbom): string` — SHA-256 hex digest of the canonical encoding.
|
||||
- `buildPurl(name, version): string` — constructs a `purl` for either a
|
||||
scoped or unscoped npm package.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`crypto.randomUUID` is the default serial source; the fallback construction
|
||||
using `Math.random` is retained only to keep the module universally
|
||||
importable — runtimes that land in the fallback should be considered a
|
||||
configuration bug and should inject `serial` explicitly. `resolveVersion`
|
||||
maps catalog references to a stable `catalog-<key>` form rather than the
|
||||
resolved pin because catalog resolution happens at publish time, not at
|
||||
SBOM generation time; the downstream publisher can post-process the
|
||||
document if stricter pins are required.
|
||||
67
docs/security-stack/security/slsa/src/schemas/parse.md
Normal file
67
docs/security-stack/security/slsa/src/schemas/parse.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# parse
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Runtime-safe entry points that turn untrusted JSON or objects into strongly
|
||||
typed, schema-validated SLSA artifacts. Every other module in the package
|
||||
treats its inputs as already valid — these two helpers (`parseProvenance`,
|
||||
`parseSbom`) are the boundary between "unvalidated wire input" and the rest of
|
||||
the codebase. Both throw structured `SlsaError` subclasses with a
|
||||
`context.issues` array of path-formatted Zod messages so callers can surface
|
||||
precise validation feedback without parsing Zod internals.
|
||||
|
||||
## Features
|
||||
|
||||
- Accepts either a JSON string or a pre-parsed object.
|
||||
- Validates against the canonical Zod schemas for in-toto/SLSA provenance and
|
||||
CycloneDX 1.5 SBOMs.
|
||||
- Structured error output: `err.code === "SLSA.INVALID_PROVENANCE"` /
|
||||
`"SLSA.INVALID_SBOM"`.
|
||||
- Captures the JSON parse error as `err.cause` when input is a malformed
|
||||
string.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseProvenance } from "@suaveplan/slsa";
|
||||
|
||||
const statement = parseProvenance(wireJson);
|
||||
console.log(statement.predicate.runDetails.builder.id);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseSbom, SlsaError, SLSA_CODES } from "@suaveplan/slsa";
|
||||
|
||||
try {
|
||||
const sbom = parseSbom(await fs.readFile("sbom.json", "utf8"));
|
||||
audit.record({ serial: sbom.serialNumber, components: sbom.components.length });
|
||||
} catch (err) {
|
||||
if (err instanceof SlsaError && err.code === SLSA_CODES.INVALID_SBOM) {
|
||||
log.warn({ issues: err.context?.issues }, "SBOM rejected");
|
||||
return Response.json({ error: "invalid sbom" }, { status: 400 });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `parseProvenance(input: string | unknown): ProvenanceStatement` — validates
|
||||
against `ProvenanceStatementSchema`. Throws `InvalidProvenanceError` on any
|
||||
failure (malformed JSON, wrong `_type`, missing digests, etc.).
|
||||
- `parseSbom(input: string | unknown): Sbom` — validates against `SbomSchema`.
|
||||
Throws `InvalidSbomError`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
JSON parsing and schema validation are kept distinct so that a malformed JSON
|
||||
string surfaces a separate error (with the `SyntaxError` attached as `cause`)
|
||||
from a structurally valid but schema-invalid document. Zod's `safeParse` is
|
||||
used — we never throw a `ZodError` out of the public API. Issues are flattened
|
||||
to `path: message` strings because consumers often log `context.issues`
|
||||
directly and a JSON-stringified Zod issue tree is unfriendly in log pipelines.
|
||||
76
docs/security-stack/security/slsa/src/verification/checks.md
Normal file
76
docs/security-stack/security/slsa/src/verification/checks.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# checks
|
||||
|
||||
## Purpose
|
||||
|
||||
Individual, stateless verification rules that consume a provenance statement
|
||||
plus policy inputs and return a structured `VerificationCheck` (name,
|
||||
pass/fail, message). Each rule is isolated so new checks compose without
|
||||
touching the verifier orchestrator, and so the test surface for each rule
|
||||
stays small. The verifier module imports these and decides which to run
|
||||
based on the policy shape.
|
||||
|
||||
## Features
|
||||
|
||||
- Rules: statement type, predicate type, subject present, subject digest,
|
||||
builder trusted (with `allowedBuilders` intersection), source-repo match
|
||||
(exact or regex), source-repo prefix, build timestamps, finishedOn
|
||||
required, build duration bound, build age bound.
|
||||
- All rules return a stable `name` key used by the verifier to de-duplicate.
|
||||
- Strings in messages deliberately echo the offending values so logs are
|
||||
self-contained for post-mortem.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { checkStatementType } from "@suaveplan/slsa/verification/verification";
|
||||
|
||||
const result = checkStatementType(statement);
|
||||
if (!result.passed) console.warn(result.message);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
checkBuilderTrusted,
|
||||
checkBuildAge,
|
||||
checkSourceRepoPrefix,
|
||||
} from "@suaveplan/slsa/verification/verification";
|
||||
|
||||
const clock = () => Date.now();
|
||||
const checks = [
|
||||
checkBuilderTrusted(stmt, {
|
||||
trustedBuilders: ["https://gitlab.fq.io/.../runners/*"],
|
||||
allowedBuilders: ["https://gitlab.fq.io/.../runners/1"],
|
||||
}),
|
||||
checkSourceRepoPrefix(stmt, "https://gitlab.fq.io/suaveplan/"),
|
||||
checkBuildAge(stmt, 7 * 24 * 60 * 60 * 1000, clock()),
|
||||
];
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Each of the following returns a `VerificationCheck`:
|
||||
|
||||
- `checkStatementType(s)` — `_type === IN_TOTO_STATEMENT_TYPE`.
|
||||
- `checkPredicateType(s)` — `predicateType === SLSA_PREDICATE_TYPE`.
|
||||
- `checkSubjectPresent(s)` — `subject` is a non-empty array.
|
||||
- `checkSubjectDigest(s)` — every subject has a sha256 or sha512 digest.
|
||||
- `checkBuilderTrusted(s, policy)` — builder id matches `trustedBuilders`
|
||||
and (if set) `allowedBuilders`.
|
||||
- `checkBuildTimestamps(s)` — `startedOn` is a valid ISO 8601 string.
|
||||
- `checkSourceRepo(s, expected)` — repo equals or regex-matches `expected`.
|
||||
- `checkSourceRepoPrefix(s, prefix)` — repo starts with `prefix`.
|
||||
- `checkFinishedOn(s)` — `finishedOn` is a valid ISO 8601 string.
|
||||
- `checkBuildDuration(s, maxMs)` — `finishedOn - startedOn <= maxMs`.
|
||||
- `checkBuildAge(s, maxAgeMs, now)` — `now - startedOn <= maxAgeMs`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`StatementLike` is deliberately a `Partial` so tests can construct tampered
|
||||
objects (`{ ...statement, _type: "wrong" }`) without casting through `any`.
|
||||
Pattern matching on builders supports exact strings and `*`-terminated
|
||||
prefixes — glob parsing is intentionally rudimentary because builder URIs
|
||||
are canonicalised by the issuer and do not need regex-level expressivity.
|
||||
Time-based checks take `now` as a parameter rather than calling `Date.now()`
|
||||
internally so tests can inject a fake clock.
|
||||
@@ -0,0 +1,74 @@
|
||||
# verification
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Top-level provenance verifier. Given a `ProvenanceStatement` and a
|
||||
`VerificationPolicy`, runs the fixed baseline of rules (statement type,
|
||||
predicate type, subject presence / digests, builder trust, timestamps) plus
|
||||
any optional rules that the policy enables (source-repo match, source-repo
|
||||
prefix, finishedOn required, max build duration, max build age). Returns a
|
||||
structured `VerificationResult` without throwing — callers branch on
|
||||
`result.valid` and surface `result.checks` for audit logs.
|
||||
|
||||
## Features
|
||||
|
||||
- Never throws on policy failure; always returns a result.
|
||||
- Injectable clock (`opts.clock`) so age-bound checks are deterministic.
|
||||
- OTel `slsa.provenance.verified` span event on every call, with the result
|
||||
outcome and builder id attached.
|
||||
- Skips optional rules cleanly when the corresponding policy field is unset
|
||||
— no "N/A" checks in the output.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyProvenance } from "@suaveplan/slsa";
|
||||
|
||||
const result = verifyProvenance(statement, {
|
||||
trustedBuilders: ["https://gitlab.fq.io/suaveplan/project/-/runners/*"],
|
||||
expectedSourceRepo: "https://gitlab.fq.io/suaveplan/project",
|
||||
});
|
||||
if (!result.valid) {
|
||||
console.error(result.summary, result.checks.filter((c) => !c.passed));
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createSlsaOtelAdapter, verifyProvenance } from "@suaveplan/slsa";
|
||||
|
||||
const otel = createSlsaOtelAdapter();
|
||||
const now = () => fakeClock.nowMs();
|
||||
|
||||
const result = verifyProvenance(
|
||||
statement,
|
||||
{
|
||||
trustedBuilders: ["https://gitlab.fq.io/suaveplan/project/-/runners/*"],
|
||||
expectedSourceRepoPrefix: "https://gitlab.fq.io/suaveplan/",
|
||||
requireFinishedOn: true,
|
||||
maxBuildDurationMs: 30 * 60 * 1000,
|
||||
maxAgeMs: 7 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{ clock: now, otel }
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyProvenance(statement, policy, opts?): VerificationResult` —
|
||||
evaluates all applicable rules. `opts.clock` defaults to `Date.now`;
|
||||
`opts.otel` defaults to a no-op adapter.
|
||||
- `VerifyOptions { clock?, otel? }` — both optional.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Rule ordering is stable across releases because consumers may depend on
|
||||
check names for filtering. Adding a new rule always appends to the check
|
||||
list, never inserts. `maxBuildDurationMs: 0` and `maxAgeMs: 0` are treated
|
||||
as "disabled" — matches the behaviour inherited from the thin legacy
|
||||
implementation, and avoids an accidental zero-tolerance configuration when
|
||||
a caller forgets to set a value.
|
||||
Reference in New Issue
Block a user