Attestor architecture
Stella Ops Attestor — derived from Epic 19 (Attestor Console), with provenance hooks aligned to the Export Center bundle workflows scoped in Epic 10.
Scope. Implementation-ready architecture for the Attestor: the service that submits DSSE envelopes to a Stella-owned local transparency log by default, optionally mirrors to Rekor-compatible external logs when configured, retrieves/validates inclusion proofs, caches results, and exposes verification APIs. It accepts DSSE only from trusted Stella services over the configured service-auth boundary, enforces chain-of-trust to Stella Ops roots, and returns
{uuid, index, proof, logURL}to calling services (Scanner.WebService for SBOMs; Findings Ledger for signed VEX overrides; backend for final reports; Excititor exports when configured).
Tenant identity (envelope-bound, 2026-04-30). The verdict surface (
VerdictController) resolves tenant identity exclusively from the envelope-attachedstellaops:tenantclaim. TheX-Tenant-Idheader is stripped at ingress and is never a tenancy source. When the header is also present and disagrees with the claim, the request is rejected with HTTP 403tenant_claim_mismatch. When neither is present, HTTP 401tenant_claim_missing. SeeSPRINT_20260430_007_MultiTenant_identity_sweep(TENANT-ATTESTOR-01).
0) Mission & boundaries
Mission. Turn a signed DSSE envelope from the Signer into a transparency‑logged, verifiable fact with a durable, replayable proof (Merkle inclusion + (optional) checkpoint anchoring). Provide fast verification for downstream consumers and a stable retrieval interface for UI/CLI.
Boundaries.
- Attestor does not sign; it must not accept unsigned or third‑party‑signed bundles.
- Attestor does not decide PASS/FAIL; it logs attestations for SBOMs, reports, and export artifacts.
- The local Stella transparency log is the air-gap primary. Rekor v2-compatible backends may be local (self-hosted) or remote for optional mirroring; Attestor handles configured external publication with retries, backoff, and idempotency.
Release-trace identity boundary. The canonical subject and evidence-spine contract is shared with Release Orchestrator and EvidenceLocker. An Attestor receipt uuid identifies a logged record; the attested artifact remains the in-toto subject name plus digest map. Listing by GET /api/v1/attestations?subject=... locates receipts for a digest/PURL, but only verification of signature, trust, and subject binding earns a record-level Proven state.
Air-gap transparency default (2026-05-24)
Attestor’s default primary transparency endpoint is stellaops://attestor/local-transparency. Submissions to that URL are appended to attestor.local_transparency_entries in PostgreSQL and receive a deterministic UUID, monotonic local log index, RFC 6962-style leaf hash, Merkle proof, and checkpoint root. The log origin defaults to stellaops.local-transparency.v1.
This local log is the authoritative receipt source for sealed deployments. Public Rekor, private Rekor/Tessera, or another external log is never required for local signing, anchoring, replay, or verification. External publication is a separate operator-controlled sync path for connected windows; failed external sync must not erase or downgrade the local receipt.
External mirroring has two operator modes. Connected-window sync submits local entries directly while an approved network path is open. Manual exchange uses the shared air-gap schedule/import-file pattern: the sealed site emits a signed schedule file, a connected relay produces a signed import file with mirror receipts, and the sealed site validates and imports those receipts. Both file types are signed with the Authority/Platform-preserved installation certificate using the active regional crypto profile; Attestor must not mint a separate exchange certificate. The operator runbook is docs/runbooks/attestor-transparency-mirror-exchange.md.
Durable mirror state is additive to the local log. Attestor stores configured targets in attestor.external_transparency_targets, per-target mirror receipts in attestor.external_transparency_mirror_receipts, and connected/manual exchange audit records in attestor.transparency_exchange_batches. ITransparencyMirrorRepository may read local entries to find pending work, but external receipt retries and imports never mutate attestor.local_transparency_entries.
IExternalTransparencyMirrorClient is the adapter boundary for external targets. The Rekor-compatible implementation rebuilds the DSSE submission from a local entry, submits it through the configured Rekor client, and calls inclusion verification with the local bundle digest before returning a mirrored receipt. Failed validation returns retry/error metadata only; it does not create an external UUID/proof-bearing receipt.
ITransparencySyncRunner owns connected-window execution. It enumerates enabled external targets for the authenticated tenant, applies operator bounds (targetIds/targetNames, max entries, max runtime, dry-run, and stop-on-hard-error), mirrors pending local entries through the adapter, upserts receipts idempotently, and records one connected_sync audit batch per target. POST /api/v1/transparency/external-sync:run exposes the same runner for manual operator windows. TransparencySyncWorker uses it only when explicitly enabled; default compose keeps external sync and the worker disabled.
ITransparencyExchangeFileService owns the manual schedule/import-file flow. It creates deterministic schedule packages, validates them at a connected relay, creates deterministic import packages with external mirror receipts, and applies returned receipts only when the local UUID, local index, and bundle digest still match the sealed site’s local transparency row. The web API exposes POST /api/v1/transparency/exchange/schedules, POST /api/v1/transparency/exchange/relay-imports, POST /api/v1/transparency/exchange/imports:validate, and POST /api/v1/transparency/exchange/imports:apply. IInstallationCertificateResolver and ITransparencyExchangeSigner are Authority/Platform and regional-crypto extension points; default infrastructure registrations fail closed until a deployment provides those concrete implementations.
Attestor attestor:* policies accept either a bearer token carrying the matching scope or a request from a configured service-to-service bypass network. That bypass is intentionally limited to trusted in-network Stella services so Findings Ledger can anchor signed VEX overrides without requiring internet access or minted public-Rekor credentials.
1) Topology & dependencies
Process shape: the module runs two containers in the shipped stack:
| Runtime | Image / container | Role |
|---|---|---|
| Attestor | stellaops/attestor → stellaops-attestor | The stateless service described by this dossier (submission, proof retrieval, verification, verdicts). Behind mTLS. |
| Attestor TileProxy | stellaops/attestor-tileproxy → stellaops-attestor-tileproxy | A read-only transparency-log tile cache. Source: src/Attestor/StellaOps.Attestor.TileProxy/; compose: devops/compose/docker-compose.stella-services.yml. Design: tile-proxy-design.md. |
TileProxy is offline by default (2026-07-12, EVI-3). It ships with no upstream and the sync job disabled: it serves only the tiles already in its local content-addressed cache and performs zero egress. A cache miss returns 503 upstream_not_configured; readiness reports mode: offline-cache-only instead of probing upstream. Pointing it at a transparency log (self-hosted or public) is an explicit operator act — ATTESTOR_TILEPROXY_UPSTREAM_URL / …_SYNC_ENABLED, or the opt-in overlay devops/compose/docker-compose.tile-proxy.yml. Enabling sync without an upstream fails startup by design; there is no hardcoded public fallback. This matches the module’s air-gap default above: the local transparency log is the primary, and any external log is operator-configured.
Dependencies:
- Signer (caller) — authenticated via mTLS and Authority OpToks.
- Local transparency log (default,
stellaops://attestor/local-transparency) — see §0. Rekor v2 tile‑backed endpoints are an optional, operator-configured mirror target. - RustFS (S3-compatible) — optional archive store for DSSE envelopes & verification bundles.
- PostgreSQL — local cache of
{uuid, index, proof, artifactSha256, bundleSha256}; job state; audit. - Valkey — dedupe/idempotency keys and short‑lived rate‑limit buckets.
- Licensing Service (optional) — “endorse” call for cross‑log publishing when customer opts‑in. ⚠ Not implemented (verified HEAD 2026-07-12): no endorsement client, option block, or endpoint exists in
src/Attestor(grep -ri "endorsementclient|/attest/endorse"→ no hits). This is an aspirational item, and a call to a “Stella Ops cloud” endpoint would sit against the on-prem/ sovereign-first posture — it is not a live dependency of the shipped service.
Trust boundary: Only the Signer is allowed to call submission endpoints; enforced by mTLS peer cert allowlist + aud=attestor OpTok.
Roles, identities & scopes
- Subjects — immutable digests for artifacts (container images, SBOMs, reports) referenced in DSSE envelopes.
- Issuers — authenticated builders/scanners/policy engines signing evidence; tracked with mode (
keyless,kms,hsm,fido2) and tenant scope. - Consumers — Scanner, Export Center, CLI, Console, Policy Engine that verify proofs using Attestor APIs.
- Authority scopes — canonical
attest:create(write) andattest:read(read + verify), and administrative scopes for key management; all calls mTLS/DPoP-bound. (ASP.NET policy names stayattestor:{write,verify,read}— see §4 scope/policy naming.)
Supported predicate types
StellaOps.BuildProvenance@1StellaOps.SBOMAttestation@1StellaOps.ScanResults@1StellaOps.PolicyEvaluation@1StellaOps.VEXAttestation@1StellaOps.RiskProfileEvidence@1StellaOps.SignedException@1
Each predicate embeds subject digests, issuer metadata, policy context, materials, and optional transparency hints. Unsupported predicates return 422 predicate_unsupported.
Source reality (2026-05-29). The seven
StellaOps.*@1names above are the headline submission predicates with JSON Schemas + golden samples undersrc/Attestor/StellaOps.Attestor.Types/. They are not the full set of predicate types the platform recognizes. There is a live, queryable predicate type registry (proofchain.predicate_type_registry, created and seeded by001_v1_attestor_baseline.sql:450— folded in from the pre-1.0002_add_predicate_type_registry.sql, which is archived and not embedded) covering ~20+ additional URIs across thestella-coreandstella-proofcategories — e.g.…/predicates/sbom-linkage/v1,…/vex-verdict/v1,…/evidence/v1,…/reasoning/v1,…/proof-spine/v1,…/policy-decision/v1,…/fix-chain/v1,path-witness/v1,binary-micro-witness@v1,trust-verdict@v1, etc. These ProofChain/proof-spine predicate types are documented in detail in §2.1 and the per-predicate sections later in this doc. TheStellaOps.Attestor.Typesschemas directory additionally carries schema-only types not in the §1 list (custom-evidence,path-witness/binary-micro-witness,fix-chain,smart-diff,uncertainty,uncertainty-budget,verification-policy).Discovery endpoint (implemented).
GET /api/v1/attestor/predicates(list, withcategory/isActive/offset/limitfilters) andGET /api/v1/attestor/predicates/{uri}(URL-encoded URI) return registry entries; both requireattestor:read(PredicateRegistryEndpoints).
Golden fixtures: Deterministic JSON statements for the headline predicates live in
src/Attestor/StellaOps.Attestor.Types/samples. They are kept stable by theStellaOps.Attestor.Types.Testsproject so downstream docs and contracts can rely on them without drifting.
Ownership boundary (2026-07-18). The Types schemas, registry entries, and ProofChain statement classes are separate bounded vocabularies; they do not form one production “multi-predicate builder”. ProofChain
StatementBuilderexposes eight typed methods and has no production registration/caller, while itsIProofChainSignerremains deliberately uncomposed without a production key store. Production DSSE construction/signing is owned by Signer, which selects Statement v1 for SLSA v1 andstella.ops/*predicates and keeps the mapped legacy families on v0.1. Predicate registry discovery does not imply that every schema or ProofChain prototype is buildable and signable through one hosted pipeline.
Statement/provenance aggregate boundary (2026-07-18). The historical “in-toto statement and provenance system” feature combined several discrete owners that are not composed into one end-to-end production pipeline. ProofChain’s
StatementBuilderremains an unregistered library builder; StandardPredicates supplies parsing and schema-validation primitives; SPDX3 supplies partial mapping/signing primitives; and Signer owns the hosted DSSE signing boundary.SlsaSchemaValidatornow fails closed with stable validation errors for non-object roots and malformed nested structures, but it has no production registration or caller. None of these bounded capabilities implies automatic link capture, one aggregate provenance workflow, or a hosted build-map-sign-verify chain.
Knowledge-snapshot aggregate boundary (2026-07-18). The historical Attestor feature combined replay DTOs, ProofChain Merkle/proof-spine models, and the separate GraphRoot library into one claimed snapshot-sealing system. No production host composes those pieces,
IAIArtifactReplayerhas no implementation, and ProofChain’s Merkle builder/proof-spine assembly is not registered by the Attestor host. Snapshot archive writing, reading, and import now belong to the separateStellaOps.AirGap.Bundlelibrary; that library is itself unhosted, so it is not evidence for a live Attestor replay/proof-spine pipeline.
Envelope & signature model
- DSSE envelopes canonicalised (stable JSON ordering) prior to hashing.
- The hosted submission/signing contract accepts keyless, keyful, and KMS modes.
AttestorVerificationEngineverifies keyless signatures from their certificate chain, legacy KMS HMAC signatures from configured secrets, and keyful signatures through the same operator-ownedSigning.Keysregistry used byAttestorSigningService. Hardware/FIDO2 verification is not a composed Attestor mode at HEAD. - A request may override the configured key mode for the resulting bundle. The successful signing audit records that effective
bundle.Mode, not the registry entry’s default mode, so the immutable audit metadata and returned DSSE result cannot disagree. Failure records have no completed bundle and retain the configured-entry fallback when one is available. - Each
signatures[*]object carrieskeyid,sig, andalg(added by SPRINT_20260503_010). Thealgfield uses RFC 7518 short names where they apply (HS256,HS384,HS512,ES256,RS256,EdDSA, …) and Stella-specific identifiers for non-RFC MACs (HSGost3411,HSSm3). Keyful verification resolves the configured logical key id and requires provider, provider-key, and algorithm identity to remain aligned before verifying DSSE PAE bytes. Legacy KMS MAC verification supports HS256/384/512; regional MAC identifiers currently fail closed as unsupported. Envelopes produced before the field existed deserialize withalg = "HS256"; unknown values produce a structuredsignature_alg_unsupported:<alg>issue instead of silently dispatching into HS256. - The ProofChain library verifier treats malformed/null signature-array entries as unusable before deterministic key ordering; an envelope with no usable signature returns an invalid result rather than throwing during verification.
- Rekor entry stores bundle hash, certificate chain, and optional witness endorsements.
- Archive CAS retains original envelope plus metadata for offline verification.
- Envelope serializer emits compact (canonical, minified) and expanded (annotated, indented) JSON variants off the same canonical byte stream so hashing stays deterministic while humans get context.
- Payload handling supports optional compression (
gzip,brotli) with compression metadata recorded in the expanded view and digesting always performed over the uncompressed bytes. - Expanded envelopes surface detached payload references (URI, digest, media type, size) so large artifacts can live in CAS/object storage while the canonical payload remains embedded for verification.
- Payload previews auto-render JSON or UTF-8 text in the expanded output to simplify triage in air-gapped and offline review flows.
Cadenced DSSE wrapper for CRA dossiers (2026-04-30)
Signer supports an opt-in cadenced wrapper for CRA technical-file and conformity-dossier exports. Callers set options.cadenced=true or request a returnBundle token containing cadenced; the default remains the existing single DSSE envelope.
Cadenced mode first creates the normal inner DSSE envelope, then signs an outer DSSE envelope whose payload type is:
application/vnd.stellaops.cadenced-dsse+json
The outer payload is canonical JSON with schema version stellaops.cadenced-dsse-envelope.v1, profile cra-dossier-envelope, the canonical inner DSSE envelope, innerEnvelopeSha256, and a cadesCompanion object. Offline verification must verify the outer DSSE signature first, then validate the inner envelope digest before processing the wrapped dossier index.
CAdES companion output is fail-closed unless Signer has configured certificate private-key material for the requested profile. The default local provider can emit a detached CMS/CAdES-B-compatible companion for BaselineB when Signer:Dsse:CadesBaselineBPfxPath points at an operator-provided PKCS#12/PFX file containing private-key material. The companion signs the canonical outer cadenced payload bytes, returns the CMS bytes and SHA-256 digest in bundle.cadenced.cadesCompanion, and includes the PEM signing certificate so offline harnesses can verify the detached CMS without network access.
This local BaselineB path is not production QES/QTSP evidence and does not claim qualified certificate, QSCD, trusted-list, timestamp, revocation, or archive evidence. Requests for BaselineT, BaselineLT, or BaselineLTA continue to record status=blocked and errorCode=cadenced.cades.provider_pack_required. DSSE verification is not weakened by missing CAdES material. Production CAdES companion bytes require a Signer-facing provider pack backed by configured qualified certificate material or the eIDAS QTSP/QSCD bridge, and for T/LT/LTA can embed and locally validate RFC 3161 timestamp, OCSP/CRL, archive timestamp, signing/TSA certificate-chain, and sealed TSL/LOTL fixtures. This remains tracked in docs/implplan/SPRINT_20260427_018_Cryptography_eidas_production_evidence.md.
Product update manifest signing (2026-04-30)
Signer owns the Stella product update manifest signing slice for CRA Annex I update-channel integrity. The contract is docs/contracts/product-update-manifest-v1.md and the predicate type is stella.ops/product-update-manifest@v1.
The Signer Core model normalizes release manifest inputs before signing: image entries are sorted by name, platform, and digest; fixed advisory summaries are sorted by source and id; SHA-256 image digests are lowercased; and the manifest body is canonicalized with the shared canonical JSON helper. The generated SigningRequest uses the existing SignerPipeline, so proof of entitlement, release-integrity, quota, audit, and configured crypto-provider checks remain on the normal signing path.
The signed in-toto statement binds the canonical manifest hash as subject stella-release-manifest:<release>:<version> and adds one subject per image digest. The local ProductUpdateManifestOfflineVerifier verifies DSSE PAE signatures against bundled PEM trust roots, recomputes the manifest hash from the signed predicate, and fails closed when the manifest subject or any image subject no longer matches the predicate. The CLI exposes this verifier through stella verify release-manifest <file> --trust-root <path> and the Offline Update Kit carries the signed DSSE envelope under manifest/ plus release trust roots under manifest/trust-roots/.
The release workflow consumes the Signer-produced DSSE envelope as the authoritative manifest. It verifies the envelope offline twice with the CLI and compares the JSON evidence before publication so the verification evidence is deterministic for identical release inputs. Registry referrer publication uses the same DSSE envelope and is gated behind an explicit workflow input because live registry attachment depends on release-runner credentials and oras.
Product CSAF direct payload signing (2026-04-30)
Signer also owns the production DSSE signing path for Stella product CSAF advisory payloads emitted by Notify. Product CSAF uses registry id product-csaf-advisory-v1, payload type application/vnd.stellaops.product.csaf-advisory.v1+json, schema pin 2.0, and signer profile stella-manufacturer-release.
Notify passes the canonical CSAF JSON bytes through SigningRequest.DirectPayload, so the DSSE envelope payload remains the direct CSAF document rather than an in-toto statement. SignerPipeline validates the registry id, media type, schema pin, canonical hash, and canonical JSON bytes before applying proof-of-entitlement, scanner release-integrity verification, quota, Signer audit, and crypto-provider resolution.
The primary DSSE signing algorithm is pinned from Signer’s active crypto profile before provider lookup: fips and eidas use ES256, gost uses GOST12-256, sm uses SM2, and pq uses DILITHIUM3 unless an explicit ProfileAlgorithmPins deployment override is configured. SM and PQ profiles still fail closed unless their existing soft-provider gates or production provider packs are present.
Signer verification consumes the complete bundle emitted by its signing endpoint. SigningMetadata.ProviderName and AlgorithmId are carried into the HTTP bundle, and CryptoDsseVerifier reconstructs DSSE PAE, accepts standard base64/base64url encoding, and resolves the recorded key through ICryptoProviderRegistry with CryptoCapability.Verification. The local HMAC signer remains Development/Testing-only and now signs/verifies the same PAE rather than the raw payload. This bounded Signer self-verification does not replace Attestor’s certificate-chain, identity-policy, or Rekor verification.
Verification pipeline overview
- Fetch envelope (from request, cache, or storage) and validate DSSE structure.
- Verify signature(s) against configured trust roots; evaluate issuer policy.
- Retrieve or acquire inclusion proof from Rekor (primary + optional mirror).
- Validate Merkle proof against checkpoint; optionally verify witness endorsement.
- Return cached verification bundle including policy verdict and timestamps.
Local DSSE trust-root verifier (2026-04-28)
StellaOps.Attestationowns the shared local DSSE verifier contract for consumers that need offline trust-root checks before full Attestor/Rekor verification is available.IDsseTrustRootVerifier/LocalTrustRootDsseVerifierload explicit PEM public keys plus configured file or directory trust roots. Directories are searched recursively for*.pem, matching offline bundle layouts such askeys/*.pem.- The adapter ignores private keys and unsupported PEM material, fails closed with
trust_roots_requiredwhen no local roots are loaded, and delegates actual DSSE PAE signature verification to the existingIDsseVerifier. - This adapter verifies keyful DSSE signatures only. It does not implement JWS verification, Fulcio chain validation, or Rekor inclusion proof verification; those remain explicit follow-up requirements.
StellaOps.Attestor.Bundleexposes strict offline verification switches for Sigstore-style bundles.RequireTrustedRootrequires certificate material to validate against explicitly configured local roots instead of accepting an embedded certificate by validity window alone;RequireInclusionProoffails closed when Rekor transparency material is absent instead of reporting the proof as skipped. These switches are intended for evidence-pack gates that must prove local trust roots and bundled transparency evidence are present before a reviewer accepts a pack.- Inclusion-proof verification follows the RFC 6962 audit-path algorithm and rejects short, extra, or non-SHA-256 path elements before comparing the computed root in constant time. This Sigstore bundle builder/serializer/verifier is a library boundary at HEAD: it has no production host registration or non-test consumer. The separate monthly
IAttestationBundlerservice has no productionIBundleAggregator/IBundleStoreimplementation or host registration, soBundlesControlleris deliberately excluded from MVC discovery instead of advertising a route that fails activation.
Release evidence-pack verification boundary (2026-07-17)
StellaOps.Attestor.EvidencePack builds and serializes portable directory, tar.gz, and zip packs whose manifest can reference release artifacts, SBOMs, provenance, attestations, Rekor proofs, compliance-control evidence, checksums, and an optional replay log. ReleaseEvidencePackOfflineVerifier validates the exported directory against manifest hashes, declared file sizes, typed attestation/Rekor/compliance references, and replay-log binding. Integration coverage uses the production copying serializer and mutates the copied artifact; missing copied bytes cannot silently pass the tamper case.
The evidence-pack manifest does not currently define policy-bundle, VEX-specific, or knowledge-snapshot fields. Callers may carry such files only as generic artifacts or attestations, which is not a typed contract. S3 archival is a separate Attestor Core storage seam and no live HTTP/CLI evidence-pack export or S3 round trip is established by the library verification.
The separate StellaOps.Attestor.Bundling library aggregates monthly attestations, stores/verifies them through IBundleStore, exports store-provided bytes through OfflineKitBundleProvider, and applies retention actions through supplied store/archive/notification interfaces. Its Merkle leaves are the server-canonical bytes of each complete BundledAttestation, so changing the DSSE payload, artifact digest, signing identity, or inclusion proof invalidates the root; the optional organization signature transitively binds those contents through that root. These production classes are behaviorally tested, including actual offline file output. At HEAD there is no production implementation or registration of Bundling’s IBundleAggregator or IBundleStore, and no host registration of IAttestationBundler, IOfflineKitBundleProvider, or IRetentionPolicyEnforcer; BundlesController is therefore excluded from the host. The test-local scheduler workflow reimplements creation and retention helpers and is not product scheduling evidence. This library is also distinct from Attestor Core’s IAttestorArchiveStore/S3 and IAttestorBundleService contracts.
The durable PostgreSQL Rekor submission queue and hosted retry worker are a separate bounded path. The current submission service does not enqueue failed submissions, its queue tests remain experimental-symbol gated, RekorSyncBackgroundService synchronizes checkpoints/tiles rather than publishing bundles, and no orchestrator connects monthly bundles to Rekor batch publication. Do not present those adjacent components as an implemented bundling-to-Rekor pipeline.
Backport-proof library boundary (2026-07-17)
StellaOps.Attestor.ProofChain owns the multi-tier BackportProofGenerator: Tier 1 consumes authoritative distro advisories, Tier 2 changelogs, Tier 3 patch headers/signatures, and Tier 4 caller-scored binary fingerprints. Evidence aggregation applies the documented strength hierarchy and bounded corroboration boost. Vulnerable, Unknown, and NotAffected are explicit proof outcomes. Generated proofs normalize their derived identity fields, calculate one canonical SHA-256 digest, expose it as both ProofId=sha256:<digest> and ProofHash=<digest>, and verify both fields. Combined evidence is ordered by its canonical value before hashing, and every generator samples its injected clock once for generated timestamps and snapshot identity. This prevents caller order or an advancing clock from changing the logical proof identity.
This is a library-only ProofBlob hashing contract. ProofChainSigner signs typed in-toto statements, not ProofBlob directly; no BLAKE3 implementation, ProofBlob-to-DSSE adapter, production proof-chain key store, host DI composition, API, or live signing path is implied by these primitives.
Proof-audit and verdict-ledger boundaries (2026-07-18)
AuditHashLogger creates a structured, process-local record containing hashes of caller-supplied raw and canonical bytes. It does not persist records, carry tenant/actor custody, append to a chain, or verify a stored ProofBlob. ProofHashing separately gives the in-memory ProofBlob model a canonical SHA-256 identity and tamper check. The deprecated AuditLogEntity/proof-chain audit repository methods were removed; endpoint-operation audit is owned by the shared .Audited() filter and Timeline’s timeline.unified_audit_events sink.
The Attestor persistence baseline does contain the separate verdict_ledger table and PostgresVerdictLedgerRepository. VerdictLedgerService hashes verdict content and links entries by the previous verdict hash; chain verification recomputes each entry’s content hash as well as checking links. However, the baseline’s REVOKE UPDATE, DELETE statements remain comments, and no production registration of the WebService ledger service/repository was found. The adjacent VerdictRekorPublisher defines a different empty ledger interface and is likewise unregistered. There is no proof_blobs table, proof_hash uniqueness constraint, persisted AuditHashLogger record, or composed ProofBlob-to-ledger-to-Rekor-to-Timeline pipeline. These bounded primitives must not be presented as one proof-transparency feature.
Delta and change-trace attestation boundary (2026-07-17)
Attestor owns typed delta/change-trace statement models and two caller-driven signing adapters. Core DeltaAttestationService requires a complete signed DSSE result, verifies that the returned payload type/bytes match its request, and identifies the result with SHA-256 over the canonical signed envelope. ProofChain ChangeTraceAttestationService maps a caller-supplied Scanner trace, applies hysteresis filtering, and delegates to IProofChainSigner.
Attestor does not own the full delta system. Scanner owns live comparison/change-trace computation; VexLens owns VEX-delta persistence; ExportCenter owns bundle packaging. Neither Attestor signing service is registered in the production host, and VexLens/ExportCenter treat the dependencies as optional. Do not infer a hosted end-to-end delta API, persistence pipeline, frontend contract, or automatic cross-module signing path from the bounded models/adapters.
Concelier’s separate BackportProofService is an orchestration library over distro, changelog, patch-header, and patch-signature repository contracts. Its no-evidence and combined-evidence paths propagate the injected TimeProvider, so fixed inputs and a fixed clock produce stable timestamps, snapshot IDs, proof IDs, and hashes. At verified HEAD the service has no production host registration or non-test consumer, and its binary-path resolver returns null; direct Tier-4 generation works, but Tier-4 orchestration through this service is unreachable. No live route or worker is claimed.
BinaryDiffV1 predicate and local DSSE boundary (2026-07-17)
StellaOps.Attestor.StandardPredicates.BinaryDiff owns the stellaops.binarydiff.v1 models, fluent builder, RFC 8785 serializer, JSON schema, and local DSSE signer/verifier. Subjects, finding paths, section deltas, analyzed section names, and map keys are normalized for deterministic serialization. Verification checks payload type, a matching key ID and DSSE signature, schema validity, predicate type, deterministic sequence ordering, and fixed-time equality between the signed payload and production canonical serialization. Payload tampering fails signature verification; schema-valid but non-canonical signed JSON is rejected. Fixed predicate/key inputs produce byte-identical Ed25519 payload, signature, and envelope bytes. ECDSA remains supported for signing/verification, but repeated ECDSA signature bytes are not a deterministic-output promise.
The DI extension treats the serializer, signer, and verifier as stateless singletons, but the fluent builder is transient because it owns mutable subjects/findings/inputs/metadata. The previous singleton lifetime allowed independent resolves to share construction state. The production stella scan diff path registers these services, computes an ELF section-hash diff through CLI-owned BinaryDiffService, and optionally emits local payload/DSSE files. It does not submit to Rekor or attach the envelope automatically; external-registry/operator-key execution and PE/Mach-O analysis are not established by the library verification. ProofChain’s BinaryFingerprintEvidenceGenerator/VexProofIntegrator use separate models; BinaryDiffV1-to-VEX auto-linking is owned instead by Concelier’s VexEvidenceLinker and is not evidence of this DSSE path.
Binary-fingerprint evidence and micro-witness boundary (2026-07-17)
BinaryFingerprintEvidenceGenerator consumes BinaryFingerprintEvidencePredicate (binary identity, layer digest, optional scan context, and vulnerability matches) and emits a hash-identified ProofBlob. Each proof contains canonical whole-predicate evidence so the binary identity/layer remain attestable even when there are no vulnerability matches, plus one evidence item per match. Fixed, vulnerable, not-affected, and unknown match sets map to explicit proof types with weighted confidence. One injected time sample controls proof creation, snapshot ID, and all evidence timestamps; proof identity/integrity uses the shared canonical ProofHashing contract.
The BinaryMicroWitnessPredicate/BinaryMicroWitnessStatement family is a separate compact, caller-populated function-evidence/in-toto vocabulary. The fingerprint generator does not create or sign that statement. At verified HEAD no production DI registration or non-test consumer of BinaryFingerprintEvidenceGenerator was found, and no automatic reachability producer connects the generator output to micro-witness evidence, SBOM references, or DSSE. These are verified library contracts, not a live reachability pipeline.
VexProofIntegrator is a separate bounded adapter from a generic ProofBlob to a VexVerdictStatement; it does not consume BinaryMicroWitnessPredicate or establish the absent function-level reachability pipeline. For proof-aware outputs, the adapter verifies that ProofId and ProofHash still match the canonical proof content before emission, then carries proof_ref, proof_method, proof_confidence, and evidence_summary in the actual in-toto predicate alongside the verdict; vexVerdictId commits to the same metadata. A mutated or unhashed proof fails closed. Callers that construct a plain VexVerdictPayload may omit those fields for backward compatibility. Returning proof metadata only as a detached companion object is not an authenticated proof-carrying VEX contract.
The Attestor VexOverridePredicateBuilder and VexOverridePredicateParser are likewise bounded predicate primitives, not the platform’s native VEX ingestion/decision owner. They are not registered in the production standard-predicate registry, and VexProofIntegrator has no production caller. Malformed required override fields now fail closed as structured validation errors instead of escaping as JSON value-kind exceptions. Native ingestion, durable statements/conflicts, and resolution remain owned by Excititor/Concelier, VexHub, and VexLens respectively.
Fulcio short-lived certificate + DSSE/JWS envelope verifier (2026-05-02)
FulcioCertificateVerifier(inStellaOps.Attestor.Core.Verification) verifies a Fulcio leaf certificate against per-deployment trust roots (IFulcioTrustRootProvider) and configured CT log keys (ICtLogKeyProvider). Roots, intermediates, allowed OIDC issuers, allowed SANs, and CT log keys are all per-deployment configuration — the verifier never hard-codes public Sigstore material, so air-gap deployments must explicitly load their own trust roots and CT log keys.- The verifier returns a structured
FulcioVerificationResult { Verdict, FailureReason, Identity }with verdictsValid | MissingTrustRoot | ChainBuildFailed | LeafExpired | SctMissing | SctSignatureInvalid | IdentityRejected | UnknownIssuer. It fails closed when any required trust material is missing. - SCT verification accepts either a detached SCT or extracts the embedded SCT extension (OID
1.3.6.1.4.1.11129.2.4.2). It parses the RFC 6962 wire format (single SCT orSctList), reconstructs the digitally-signed pre-cert blob usingExtractTbsWithoutSctExtension, and matches the SCT’sLogID(SHA-256 of SPKI) against the configured CT log key set before verifying the signature with ECDSA P-256 SHA-256 (or RSA fallback). DsseJwsEnvelopeVerifierwraps the Fulcio verifier and provides DSSE pre-authentication-encoding (PAE) signature verification for envelopes signed either by a supplied Fulcio leaf or by a configured static key (IStaticDsseKeyProvider, keyed by DSSEkeyid). Result verdicts:Valid | Malformed | NoVerificationMaterial | CertificateRejected | SignatureInvalid | AlgorithmNotImplemented.AlgorithmNotImplementedcarries thefeature_not_implementedparity wording so unsupported algorithms surface the same code as the WebService HTTP fail-closed path.VerificationReplayLogBuilderconsumes the verifier outcome and records averify_fulcio_certificatestep with the Fulcio verdict, expected vs. actual subject identity, the matching CT log id (asKeyId), and the resolved signature algorithm. The step is omitted entirely when noFulcioVerificationVerdictis supplied (purely keyful DSSE flow).
Unsupported Trust Endpoint Fail-Closed Contract (2026-04-28)
- Anchor, proof-spine, and direct verification HTTP routes stay routable even when their implementation flags are disabled, but they return
501 Not Implementedwithcode=feature_not_implemented. They must not disappear as404, and they must not return receipt/check fields until real verification has run. - TrustVerdict OCI attachment is disabled by default. Setting
TrustVerdictOci:Enabled=truefails options validation because attach/fetch/list/detach are not complete; direct calls return failure/null/empty/false and never emit OCI digests or attachment records. - FixChain Rekor publication is opt-in. When publication is requested, the service requires a configured Rekor client and fails the create operation if submission fails, rather than returning an attestation that looks transparently logged.
- FixChain analyzer identity is explicit.
AnalyzerName,AnalyzerVersion, andAnalyzerSourceDigestmust be configured before a statement is built;AnalyzerSourceDigestmust besha256:<64 hex>. The builder fails closed instead of synthesizingsha256:unknownor default analyzer versions. - Offline Rekor break-glass records the bypass metadata but does not mark an invalid inclusion proof as valid. Downstream promotion policy may choose to handle break-glass as an operational exception, not as proof verification success.
OCI DSSE attachment boundary (2026-07-18)
OrasAttestationAttacher is a bounded OCI Distribution 1.1 adapter for attaching, listing, fetching, and removing DSSE referrers. A fetched DSSE layer is accepted for deserialization only when SHA-256 over the returned bytes matches the layer descriptor digest; substituted or corrupt registry content fails closed with a digest-mismatch error.
The CLI composes this adapter with OciAttestationRegistryClient. Registry references default to HTTPS; an operator must include an explicit http:// scheme to use an insecure local lab registry, and that transport choice is carried separately from the registry authority. attest attach --sign may create the first ES256 signature on unsigned DSSE input or append one to an existing envelope. attest oci-verify --key accepts PEM or DER SPKI public-key material and still requires the trust key id (the PEM filename stem for a direct --key) to match the DSSE signature key id. Run-004 proved the real attach/list/fetch/strict-signature-verify/remove path against local Zot and removed the exact QA referrer afterward.
This adapter does not establish an evidence-first umbrella pipeline. Scanner’s SmartDiff delta builder/publisher has no production composition, Attestor’s SBOM publisher stores raw canonical SBOM bytes rather than a DSSE envelope, and the TrustVerdict OCI adapter remains disabled and unimplemented. Current DSSE production ownership therefore remains split across domain-specific Signer, Scanner, Verdict, Attestor, and CLI paths.
Per-layer attestation boundary (2026-07-18)
Attestor Core’s LayerAttestationService is an unregistered in-memory prototype. Its bundled InMemoryLayerAttestationSigner derives identifiers and remembers them for same-process lookup; it does not create or verify DSSE envelopes. No production host registers ILayerAttestationService, ILayerAttestationSigner, or ILayerAttestationStore, and the CLI’s historical /api/v1/attestor/layers/* targets have no backend route. Generic ProofChain signing, disabled Attestor bundling, and the OCI adapter above are independent primitives, not a composed per-layer Attestor workflow.
Current per-layer fragment DSSE emission belongs to Scanner Worker: SurfaceManifestStageExecutor and HmacDsseEnvelopeSigner are production-registered and emit DSSE artifacts for composition recipes and available layer fragments. That flow does not revive the Attestor prototype’s batch API, parent-link store, generic bundle, or per-layer OCI attachment claims. Attestor’s bounded prototype now propagates caller cancellation instead of converting it into an ordinary failed attestation result.
Transparency status health folding (2026-07-18)
TransparencyStatusProvider reports checkpoint freshness and refreshed backend availability as one overall status. For a configured online primary, an Unhealthy backend result overrides checkpoint age and produces overall Unhealthy; a Slow primary degrades an otherwise Healthy fresh checkpoint. Offline mode and stale/critical checkpoint age rules remain authoritative. The returned backend list is primary-first and ordinally stable so API/observability consumers receive deterministic status output.
Rekor Inclusion Proof Verification (SPRINT_3000_0001_0001)
The Attestor implements RFC 6962-compliant Merkle inclusion proof verification for Rekor transparency log entries:
Components:
MerkleProofVerifier— Verifies Merkle audit paths per RFC 6962 Section 2.1.1CheckpointSignatureVerifier— Parses and verifies Rekor checkpoint signatures (ECDSA/Ed25519)RekorLocalTransparencyBundleVerifier- Loads configured local/offline Rekor proof bundles and composes receipt, checkpoint, full tile, and sparse proof verification with fail-closed diagnosticsRekorTileRootConsistencyVerifier- Verifies strict local full tile snapshots against signed checkpoints and receipt inclusion pathsRekorSparseTileProofVerifier- Verifies bounded sparse local proof nodes for large logs without requiring a complete local tile snapshotRekorVerificationOptions— Configuration for public keys, offline mode, and checkpoint caching
Conformance boundary (2026-07-17): StellaOps.Attestor.Conformance.Tests now calls the production MerkleProofVerifier, CheckpointSignatureVerifier, and RekorOfflineReceiptVerifier against frozen RFC 6962 values and real platform-crypto signatures, including proof and signed-body tamper rejection. The former mock-only WAN/proxy/offline comparison suite and fabricated fixtures were removed. This proves the local/offline cryptographic composition only; external Rekor traffic, TileProxy transport parity, and byte-for-byte parity with an external reference implementation remain separate network-enabled proof work.
Online checkpoint trust boundary (2026-07-17): each Attestor:Rekor:Primary|Mirror backend accepts either PublicKeyBase64 or PublicKeyPath (PEM or DER SPKI). Direct and service-map resolution load that key into RekorBackend.PublicKey. When a key is configured, HttpRekorClient rejects missing or invalid signed checkpoint notes instead of returning Merkle-only success. The persisted-entry verification engine also requires checkpoint signatures by default (Attestor:Verification:RequireCheckpointSignature=true) and binds the verified signed-note origin, tree size, and root to the persisted checkpoint. Missing/invalid key material, missing/invalid signatures, or signed/persisted field mismatch fails the transparency section. The wider PostgreSQL checkpoint synchronization, divergence detection, and Notify alert stack remains uncomposed and must not be described as live.
Local transparency path boundary (2026-07-17): LocalTransparencyRekorClient stores RFC 6962 leaf hashes in PostgreSQL and emits audit nodes as L:<base64> / R:<base64>, where the prefix states which side of the running hash owns the sibling. Its verifier parses those nodes fail-closed, requires SHA-256-sized sibling hashes, and folds them in the declared order. The oriented representation is intentional: odd-sized trees promote siblingless nodes without adding a path element, so verification must not consume one proof node per tree level. A PostgreSQL-backed three-leaf forcing function covers the first leaf, the promoted third leaf, and wrong-digest rejection. This is the supported local runtime; it does not compose the historical EnhancedRekorProofBuilder ProofChain aggregate.
Core audit-path boundary (2026-07-18): MerkleProofVerifier advances its proof cursor only when the RFC 6962 traversal has an actual left or right sibling. A siblingless trailing node in an odd-sized tree is promoted without consuming proof material; a three-leaf regression covers the promoted final leaf. This verifies a supplied audit path only. It does not implement or compose the historical IProofSpineAssembler, ProofChain signing pipeline, or aggregate Merkle proof system.
Checkpoint/tile-sync boundary (2026-07-18): Core’s RekorSyncBackgroundService is a separate external-log synchronization library worker and is not registered by the production Attestor host. When explicitly composed, it snapshots the previous stored checkpoint before persisting the new checkpoint, then calculates and fetches missing tiles from that prior tree size; loading “previous” after storage would compare the new checkpoint to itself and skip the entire incremental range. Focused and full Core tests cover the repaired ordering. This library correctness does not establish the still-uncomposed PostgreSQL checkpoint synchronization, divergence/Notify path, or hosted TileProxy workflow, and it must not be presented as part of the supported local append/proof runtime above.
Periodic Rekor re-verification boundary (2026-07-18): Core contains RekorVerificationJob, its options, verification service, metrics, and health status primitives, but the production Attestor host registers none of the job, IRekorVerificationStatusProvider, or RekorVerificationHealthCheck. The adjacent Infrastructure tests construct the job with in-memory collaborators; they do not start the BackgroundService, advance its cron schedule, publish health state, or exercise persisted entries. The separate checkpoint divergence detector/alert publisher is not called by this job, and Doctor’s timestamping plugin checks TST-to-Rekor time correlation rather than periodic verification status. Online-only configuration no longer requires an offline checkpoint key; that requirement now applies only when offline verification is enabled. Sampling still uses process-randomized string.GetHashCode() despite being described as deterministic, so cross-process sample stability remains an explicit gap. These library primitives must not be described as one hosted periodic verification pipeline until registration and an executed schedule/health/divergence forcing function exist.
Verification Flow:
- Parse checkpoint body (origin, tree size, root hash)
- Verify checkpoint signature against Rekor public key
- Compute leaf hash from canonicalized entry
- Walk Merkle path from leaf to root using RFC 6962 interior node hashing
- Compare computed root with checkpoint root hash (constant-time)
Offline Mode:
- Bundled checkpoints can be used in air-gapped environments
EnableOfflineModeandOfflineCheckpointBundlePathconfiguration optionsAllowOfflineWithoutSignaturefor fully disconnected scenarios (reduced security)- Configured local transparency bundle roots can be verified with
RekorLocalTransparencyBundleVerifier. The verifier resolves only local files under the configured root, requires a pinned Rekor public key and checkpoint file, bindsrekor-receipt.jsonto that checkpoint before inclusion verification, and optionally verifiesrekor-tile-snapshot.jsonandrekor-sparse-proof.jsonwhen present or required.
Offline checkpoint requirement (2026-04-28):
- Periodic/offline Rekor verification is fail-closed unless the stored inclusion proof is bound to a checkpoint.
- The verifier recomputes the RFC 6962 Merkle root from the entry body digest and path, verifies the signed checkpoint note with the configured local transparency-log public key, and requires checkpoint tree size and root hash to match the proof.
- Missing checkpoint material, missing log public key, signature failure, tree-size mismatch, or root mismatch returns an invalid result. Merkle proof shape alone is not transparency verification.
AllowOfflineWithoutCheckpointSignature=trueis an explicit reduced-security escape hatch; it parses the checkpoint but does not create cryptographic checkpoint-signature evidence.- VexLens compact JWS verification now has two offline proof paths: local Fulcio
x5cchain validation against configured roots, and local JWKS/kid verification against bundled public keys. Structure-only JWS/Fulcio metadata must not raise issuer trust. - Scanner uses the Attestor Core offline Rekor receipt verifier through a Scanner-owned adapter for signed SBOM archives and offline-kit imports. The adapter binds receipts to DSSE envelope bytes, verifies the signed local Rekor checkpoint against a bundled public key, and verifies the Merkle inclusion path. Scanner signed-SBOM archives also validate keyless Fulcio chains to local roots before packaging, can enforce local revocation bundles against the verified Fulcio signing leaf when requested, and enforce local tile/root consistency when
rekor-tile-snapshot.jsonmaterial is supplied or explicitly required. - The cross-module proof-shape fixture in
src/VexLens/__Tests/StellaOps.VexLens.Tests/Verification/CrossModuleTransparencyFixtureTests.csverifies Scanner-shaped DSSE/Fulcio/Rekor material and VexLens compact JWS/JWKS/revocation material through their own local verifier paths, then asserts each path rejects the other proof shape. - Local tile/root consistency is now an Attestor Core opt-in contract, not part of receipt success by default.
RekorTileRootConsistencyVerifierrequiresstellaops.rekor.tile-snapshot.v1material with complete level0..roothashes, recomputes each upper tile hash, matches the derived root to the signed checkpoint, and requires the snapshot-derived inclusion path to match the receipt. Missing/malformed tile material, UUID/log-index-only material, or checkpoint-shaped text without tile hashes fails closed and must not be reported as tile consistency success. - Sparse local proof consistency is an Attestor Core opt-in contract for large logs.
RekorSparseTileProofVerifierrequiresstellaops.rekor.sparse-tile-proof.v1material containingorigin,treeSize,rootHash,logIndex,leafHash, and orderedproofNodeswith{level,index,hash}coordinates from the receipt leaf to the checkpoint root. The verifier first validates the receipt inclusion proof and signed checkpoint, then requires the sparse proof to match the checkpoint origin/tree/root, the receipt log index, the payload-derived RFC 6962 leaf hash, the expected audit-path coordinates, and the receipthashesbyte for byte. Metadata-only sparse material, missing nodes, extra nodes, malformed hashes, coordinate mismatches, root mismatch, or receipt/proof divergence fails closed. - Configured local bundle verification is now an Attestor Core contract for offline proof material.
RekorLocalTransparencyBundleVerifieraccepts a local bundle root plus optional explicit local paths, rejects URI or path-escape inputs, resolvesrekor-receipt.json,checkpoint.sig, a single pinned key underkeys/tlog-root, and optional tile/sparse proof files, and returns diagnostics for each stage. Missing bundle roots, missing or ambiguous keys, missing pinned checkpoints, tampered checkpoint signatures, missing required tile/sparse material, malformed files, or failed consistency checks return invalid results. - Scanner’s signed-SBOM handoff now treats
includeRekorProofas archive packaging control only. Any submitted Rekor receipt/checkpoint/key/log-index field, full tile snapshot field, sparse proof field, or tile/sparse consistency flag still forces the local Attestor-backed verifier before Scanner stores the material. - Still separate gaps: live/online Rekor tile fetching/consistency proofs and a persisted arbitrary tile-source lookup contract beyond configured local bundle roots.
Metrics:
attestor.rekor_inclusion_verify_total— Verification attempts by resultattestor.rekor_checkpoint_verify_total— Checkpoint signature verificationsattestor.rekor_offline_verify_total— Offline mode verificationsattestor.rekor_checkpoint_cache_hits/misses— Checkpoint cache performanceattestor.scanner_signed_sbom_handoff_total/attestor.scanner_signed_sbom_handoff_latency_seconds- Scanner signed-SBOM handoff attempts and latency by result
UI & CLI touchpoints
- Console: Evidence browser, verification report, chain-of-custody graph, issuer/key management, attestation workbench, bulk verification views.
- CLI:
stella attest sign|verify|list|fetch|keywith offline verification and export bundle support. - SDKs expose sign/verify primitives for build pipelines.
Performance & observability targets
- Throughput goal: ≥1 000 envelopes/minute per worker with cached verification.
- Metrics:
attestor_submission_total,attestor_verify_seconds,attestor_rekor_latency_seconds,attestor_cache_hit_ratio. - Logs include
tenant,issuer,subjectDigest,rekorUuid,proofStatus; traces cover submission → Rekor → cache → response path.
2) Data model (PostgreSQL)
Schemas: attestor (runtime entries, submission queue, watchlist) and proofchain (trust anchors, SBOM entries, DSSE envelopes, spines, Rekor entries). Both are created and auto-migrated on startup from embedded SQL under src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Migrations/. The StellaOps.Attestor.Persistence.SchemaIsolationService additionally generates (does not execute) schema/RLS/temporal SQL for operator review — see “PostgreSQL Persistence Layer” later in this doc.
Tables & schemas
entriestableCREATE TABLE attestor.entries ( rekor_uuid TEXT PRIMARY KEY, bundle_sha256 TEXT NOT NULL UNIQUE, artifact_sha256 TEXT NOT NULL, artifact_kind TEXT NOT NULL, -- sbom|report|vex-export|policy-eval artifact_image_digest TEXT, artifact_subject_uri TEXT, log_index BIGINT, log_backend TEXT NOT NULL, log_url TEXT NOT NULL, log_id TEXT, log_integrated_time BIGINT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), status TEXT NOT NULL, -- included|pending|failed signer_mode TEXT NOT NULL, signer_issuer TEXT, signer_subject_alternative_name TEXT, signer_key_id TEXT, proof JSONB, -- canonical Rekor checkpoint + inclusion proof witness JSONB, mirror JSONB );Source reality (migration citations refreshed 2026-07-12). This is the live
attestor.entriesschema, created by001_v1_attestor_baseline.sql:564(folded in from the pre-1.0002_attestor_runtime_entries_and_watchlist.sql, archived and not embedded), and it matches the table above. The livebundle_sha256uniqueness is enforced by constraintuq_attestor_entries_bundle_sha.dedupe— Redis/Valkey, NOT a PostgreSQL table. Idempotency/dedupe state is held in Redis/Valkey (bundle:<sha256> → rekorUuid), configured viaattestor:redis:url. There is noattestor.dedupeSQL table in the migrations; the previous DDL was a design sketch. See §7 (Storage) for the live behaviour: non-local runtime fails startup when Redis/Valkey is unconfigured; Development/Testing may use an in-memory fallback.audit log—proofchain.audit_logis created and then DROPPED inside the same baseline.001_v1_attestor_baseline.sqlcreates it at:244(folded in from the pre-1.0001_initial_schema.sql) and drops it at:776(DROP TABLE IF EXISTS proofchain.audit_log CASCADE, folded in from the pre-1.0004_drop_deprecated_audit_log.sql) — so a converged database has noproofchain.audit_log. Attestor runtime auditing flows through the shared audit-emission pipeline (StellaOps.Audit.Emission,.Audited(AuditModules.Attestor, …)on the write endpoints), not a module-local audit table. Treat any reference toproofchain.audit_logas historical.watchlisttables (live —001_v1_attestor_baseline.sql:630/:699)CREATE TABLE attestor.identity_watchlist (...); CREATE TABLE attestor.identity_alert_dedup (...);proofchain.*tables (live):trust_anchors,sbom_entries,dsse_envelopes,spines, andrekor_entriesare created by001_v1_attestor_baseline.sql; the Attestor-owned query projectiongraph_nodesandgraph_edgesis created by003_tenant_proof_graph.sql. The projection keys every node, edge, and foreign-key relationship by authenticatedtenant_id. Saving an Attestor entry transactionally projects the Artifact, in-toto statement, transparency entry, signing key, and their deterministic relationships.InMemoryProofGraphServiceremains test/development-only.attestor.rekor_submission_queue(live legacy-compatibility table —001_v1_attestor_baseline.sql:264): retained so the hosted compatibility worker can drain rows created by older releases. Current submission code has no enqueue caller; new external publication uses local transparency entries plus durable per-target mirror receipts andITransparencySyncRunner.
Migration inventory (verified on disk 2026-07-20). The embedded set is
001_v1_attestor_baseline.sql(the collapsed pre-1.0 baseline) +002_bulk_verification_jobs.sql(attestor.bulk_verification_jobs, backing the durable bulk-verify surface in §4.5) +003_tenant_proof_graph.sql(tenant-partitioned Attestor entries and durable proof-graph projection). The pre-1.0 per-feature files (001_initial_schema.sql,002_attestor_runtime_entries_and_watchlist.sql,002_add_predicate_type_registry.sql,004_drop_deprecated_audit_log.sql, …) live underMigrations/_archived/pre_1.0/and are not embedded — cite the baseline, not those names.
Indexes (live):
attestor.entries: tenant-aware(tenant_id, artifact_sha256, created_at, rekor_uuid)and(tenant_id, created_at DESC, rekor_uuid)indexes, plus(artifact_sha256, created_at DESC),artifact_image_digest(partial),artifact_subject_uri(partial),(created_at DESC, rekor_uuid),signer_subject_alternative_name(partial),signer_issuer(partial); uniquebundle_sha256.proofchain.*: per-table indexes on digests, signer key id, log index/id/uuid (migration 001), plus tenant/digest node lookup and tenant/source/target edge traversal indexes (migration 003).
The GET /api/v1/attestations type filter addresses attestor.entries.artifact_kind (submission meta.artifact.kind), not the in-toto predicate URI embedded in the DSSE payload. Consumers that need scan-result receipts therefore query type=scan-result while retaining stellaops.io/predicates/scan-result@v1 as the signed predicate contract.
2.1) Content-Addressed Identifier Formats
The ProofChain library (StellaOps.Attestor.ProofChain) defines canonical content-addressed identifiers for all proof chain components. These IDs ensure determinism, tamper-evidence, and reproducibility.
Identifier Types
| ID Type | Format | Source | Example |
|---|---|---|---|
| ArtifactID | sha256:<64-hex> | Container manifest or binary hash | sha256:a1b2c3d4e5f6... |
| SBOMEntryID | <sbomDigest>:<purl>[@<version>] | SBOM hash + component PURL | sha256:91f2ab3c:pkg:npm/lodash@4.17.21 |
| EvidenceID | sha256:<hash> | Canonical evidence JSON | sha256:e7f8a9b0c1d2... |
| ReasoningID | sha256:<hash> | Canonical reasoning JSON | sha256:f0e1d2c3b4a5... |
| VEXVerdictID | sha256:<hash> | Canonical VEX verdict JSON | sha256:d4c5b6a7e8f9... |
| ProofBundleID | sha256:<merkle_root> | Merkle root of bundle components | sha256:1a2b3c4d5e6f... |
| GraphRevisionID | grv_sha256:<hash> | Merkle root of graph state | grv_sha256:9f8e7d6c5b4a... |
SBOMEntryID is deliberately a compound ProofChain identifier: the digest covers the RFC 8785 canonical complete SBOM, while the PURL/version suffix selects a top-level component. It is not an isolated component hash and does not replace caller-provided CycloneDX bom-ref or SPDX identifiers. CycloneDxSubjectExtractor exposes this bounded library behavior, but it has no production DI registration or hosted SBOM mutation path; ComponentRefExtractor remains pass-through extraction only. PURL versions are normalized before qualifiers/subpaths, and conflicting embedded/explicit versions fail closed.
StandardPredicates SBOM format boundary (2026-07-17)
StellaOps.Attestor.StandardPredicates retains deterministic CycloneDX 1.7 and compact SPDX 3.0.1 JSON-LD writers plus shallow predicate parsers. The parsers accept only audited versions (CycloneDX 1.4-1.7; SPDX 2.3 and 3.0.1), require structural validity before extraction, and the SPDX parser accepts the compact @graph shape emitted by its sibling writer. PredicateTypeRouter never extracts an SBOM after an invalid parse.
The writers own format-specific collection ordering, then delegate compact object-key ordering and hashing to SbomCanonicalizer/shared CanonJson. The SPDX license-expression parser follows the SPDX grammar precedence WITH > AND > OR, while explicit parentheses override that order; this prevents a writer from changing the meaning of mixed license expressions. This is the deterministic Stella server profile, not a full RFC 8785/JCS interoperability claim; it applies NFC normalization and preserves the platform serializer’s number and escaping behavior. There is no cross-format transformation implementation in this boundary.
These primitives are not a claim of complete CycloneDX/SPDX profile compliance. The local schemas are structural subsets, the writers have no Attestor-host production consumer/DI composition, and the DSSE SPDX signer remains library-only. Rich inbound SBOM parsing is owned by Concelier ParsedSbomParser; typed SPDX 3 parsing/validation is owned by shared StellaOps.Spdx3. A future full-compliance claim requires complete offline schemas/contexts through the license/supply-chain gate plus representative all-profile validation and production forcing functions.
Canonicalization (RFC 8785)
All JSON-based IDs use RFC 8785 (JCS) canonicalization:
- UTF-8 encoding
- Lexicographically sorted keys
- No whitespace (minified)
- No volatile fields (timestamps, random values excluded)
- Duplicate property names are rejected after optional NFC normalization, including collisions with the injected
_canonVersionmarker; ambiguous JSON is never assigned a canonical hash.
Implementation: StellaOps.Attestor.ProofChain.Json.Rfc8785JsonCanonicalizer
ProofChainSigner canonicalizes the complete typed statement before DSSE PAE and signature creation. Reproducibility therefore treats caller-supplied timestamps and every predicate field, plus the selected key/profile, as inputs. A fixed Ed25519 key produces deterministic signatures; this contract does not claim byte-identical signatures for randomized algorithms.
Merkle Tree Construction
ProofBundleID and GraphRevisionID use deterministic binary Merkle trees:
- SHA-256 hash function
- Lexicographically sorted leaf inputs
- Standard binary tree construction (pair-wise hashing)
- Odd leaves promoted to next level
Implementation: StellaOps.Attestor.ProofChain.Merkle.DeterministicMerkleTreeBuilder
ID Generation Interface
// Core interface for ID generation
public interface IContentAddressedIdGenerator
{
EvidenceId GenerateEvidenceId(EvidencePredicate predicate);
ReasoningId GenerateReasoningId(ReasoningPredicate predicate);
VexVerdictId GenerateVexVerdictId(VexPredicate predicate);
ProofBundleId GenerateProofBundleId(SbomEntryId sbom, EvidenceId[] evidence,
ReasoningId reasoning, VexVerdictId verdict);
GraphRevisionId GenerateGraphRevisionId(GraphState state);
}
Predicate Types
The ProofChain library defines DSSE predicates for proof chain attestations. All predicates follow the in-toto Statement/v1 format.
Predicate Type Registry
| Predicate | Type URI | Purpose | Signer Role |
|---|---|---|---|
| Evidence | evidence.stella/v1 | Raw evidence from scanner/ingestor (findings, reachability data) | Scanner/Ingestor key |
| Reasoning | reasoning.stella/v1 | Policy evaluation trace with inputs and intermediate findings | Policy/Authority key |
| VEX Verdict | cdx-vex.stella/v1 | VEX verdict with status, justification, and provenance | VEXer/Vendor key |
| Proof Spine | proofspine.stella/v1 | Merkle-aggregated proof spine linking evidence to verdict | Authority key |
| Verdict Receipt | verdict.stella/v1 | Final surfaced decision receipt with policy rule reference | Authority key |
| SBOM Linkage | https://stella-ops.org/predicates/sbom-linkage/v1 | SBOM-to-component linkage metadata | Generator key |
| Signed Exception | https://stellaops.io/attestation/v1/signed-exception | DSSE-signed budget exception with recheck policy | Authority key |
Evidence Statement (evidence.stella/v1)
Captures raw evidence collected from scanners or vulnerability feeds.
| Field | Type | Description |
|---|---|---|
source | string | Scanner or feed name that produced this evidence |
sourceVersion | string | Version of the source tool |
collectionTime | DateTimeOffset | UTC timestamp when evidence was collected |
sbomEntryId | string | Reference to the SBOM entry this evidence relates to |
vulnerabilityId | string? | CVE or vulnerability identifier if applicable |
rawFinding | object | Pointer to or inline representation of raw finding data |
evidenceId | string | Content-addressed ID (sha256:<hash>) |
Reasoning Statement (reasoning.stella/v1)
Captures policy evaluation traces linking evidence to decisions.
| Field | Type | Description |
|---|---|---|
sbomEntryId | string | SBOM entry this reasoning applies to |
evidenceIds | string[] | Evidence IDs considered in this reasoning |
policyVersion | string | Version of the policy used for evaluation |
inputs | object | Inputs to the reasoning process (evaluation time, thresholds, lattice rules) |
intermediateFindings | object? | Intermediate findings from the evaluation |
reasoningId | string | Content-addressed ID (sha256:<hash>) |
VEX Verdict Statement (cdx-vex.stella/v1)
Captures VEX status determinations with provenance.
| Field | Type | Description |
|---|---|---|
sbomEntryId | string | SBOM entry this verdict applies to |
vulnerabilityId | string | CVE, GHSA, or other vulnerability identifier |
status | string | VEX status: not_affected, affected, fixed, under_investigation |
justification | string | Justification for the VEX status |
policyVersion | string | Version of the policy used |
reasoningId | string | Reference to the reasoning that led to this verdict |
vexVerdictId | string | Content-addressed ID (sha256:<hash>) |
Proof Spine Statement (proofspine.stella/v1)
Schema prototype for a Merkle-aggregated proof bundle linking chain components. As of 2026-07-17, IProofSpineAssembler, IProofChainPipeline, and IReceiptGenerator have no production implementations, and the named assembly test uses test-local orchestration around the deterministic Merkle builder. Do not treat the model below as a hosted Attestor event-to-Rekor workflow. Scanner owns a narrower policy/human-decision DSSE lane; production proof-spine writes and the historical cross-module event-spine aggregate remain unimplemented.
The typed ProofChain verdict-receipt aggregate is likewise uncomposed. IReceiptGenerator is interface-only, IVerificationPipeline is unregistered, VerdictReceiptStatement has no production caller, and IProofChainSigner still lacks a production key store. The hosted POST /internal/api/v1/attestations/verdict path is a separate successor that canonicalizes a raw verdict predicate, signs/verifies it through the production Attestor signing registry, and persists it in EvidenceLocker before success. It does not establish typed VerificationReceipt generation or retrieval.
The standalone verification pipeline does honor an explicit SkipTrustAnchorVerification request while continuing its other supplied steps. When trust-anchor verification is enabled, revoked-key membership takes precedence over the allow list and fails closed. These bounded library behaviors do not change the composition boundary: there is still no production path that joins the in-memory proof graph, content IDs, Merkle construction, signing, Rekor submission, and receipt custody into one proof-carrying security-decision workflow.
| Field | Type | Description |
|---|---|---|
sbomEntryId | string | SBOM entry this proof spine covers |
evidenceIds | string[] | Sorted list of evidence IDs included in this proof bundle |
reasoningId | string | Reasoning ID linking evidence to verdict |
vexVerdictId | string | VEX verdict ID for this entry |
policyVersion | string | Version of the policy used |
proofBundleId | string | Content-addressed ID (sha256:<merkle_root>) |
Verdict Receipt Statement (verdict.stella/v1)
Final surfaced decision receipt with full provenance.
| Field | Type | Description |
|---|---|---|
graphRevisionId | string | Graph revision ID this verdict was computed from |
findingKey | object | Finding key (sbomEntryId + vulnerabilityId) |
rule | object | Policy rule that produced this verdict |
decision | object | Decision made by the rule |
inputs | object | Inputs used to compute this verdict |
outputs | object | Outputs/references from this verdict |
createdAt | DateTimeOffset | UTC timestamp when verdict was created |
SBOM Linkage Statement (sbom-linkage/v1)
SBOM-to-component linkage metadata.
| Field | Type | Description |
|---|---|---|
sbom | object | SBOM descriptor (id, format, specVersion, mediaType, sha256, location) |
generator | object | Generator tool descriptor |
generatedAt | DateTimeOffset | UTC timestamp when linkage was generated |
incompleteSubjects | object[]? | Subjects that could not be fully resolved |
tags | object? | Arbitrary tags for classification or filtering |
Reference: src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/
Signed Exception Statement (signed-exception/v1)
DSSE-signed exception objects with recheck policy for independent verification and automated re-approval workflows.
| Field | Type | Description |
|---|---|---|
schemaVersion | string | Schema version (current: “1.0”) |
exception | object | The wrapped BudgetExceptionEntry |
exceptionContentId | string | Content-addressed ID (sha256:<hash>) for deduplication |
signedAt | DateTimeOffset | UTC timestamp when the exception was signed |
recheckPolicy | object | Recheck policy configuration |
environments | string[]? | Environments this exception applies to (dev, staging, prod) |
coveredViolationIds | string[]? | IDs of violations this exception covers |
approvalPolicyDigest | string? | Digest of the policy bundle that approved this exception |
renewsExceptionId | string? | Previous exception ID for renewal chains |
status | string | Status: Active, PendingRecheck, Expired, Revoked, PendingApproval |
Recheck Policy Schema
| Field | Type | Description |
|---|---|---|
recheckIntervalDays | int | Interval in days between rechecks (default: 30) |
autoRecheckEnabled | bool | Whether automatic recheck scheduling is enabled |
maxRenewalCount | int? | Maximum renewals before escalated approval required |
renewalCount | int | Current renewal count |
nextRecheckAt | DateTimeOffset? | Next scheduled recheck timestamp |
lastRecheckAt | DateTimeOffset? | Last completed recheck timestamp |
requiresReapprovalOnExpiry | bool | Whether re-approval is required after expiry |
approvalRoles | string[]? | Roles required for approval |
Exception Signing API
The exception signing service provides endpoints for signing, verifying, and renewing exceptions:
| Endpoint | Method | Description |
|---|---|---|
/internal/api/v1/exceptions/sign | POST | Sign an exception and wrap in DSSE envelope |
/internal/api/v1/exceptions/verify | POST | Verify a signed exception envelope |
/internal/api/v1/exceptions/recheck-status | POST | Check if exception requires recheck |
/internal/api/v1/exceptions/renew | POST | Renew an expired/expiring exception |
Reference: src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/DsseSignedExceptionPayload.cs
3) Input contract (from Signer)
Reference-deployment caveat (verified
src/HEAD, 2026-07-12). The list below is the hardened trust contract. mTLS is config-conditional, not mandatory in the shipped stack: the default compose runs the Attestor over plain HTTP behind the router (ASPNETCORE_URLS: "http://+:8442"), andRequireClientCertificateis opt-in (AttestorWebServiceComposition.cs:411). The enforced boundary in the reference deployment is theattestor:writepolicy, which is scope (attest:create) OR trusted-network service bypass with noRequireAuthenticatedUser(AttestorWebServiceComposition.cs:344-348). See §0 for the bypass disclosure. Read item 1 below as “mTLS when configured”, not an absolute.
Attestor accepts (when fully hardened) only DSSE envelopes that satisfy all of:
- mTLS peer certificate maps to
signerservice (CA‑pinned) — when mTLS is configured; see caveat above. - Authority OpTok with
aud=attestor,scope=attestor.write, DPoP or mTLS bound. - DSSE envelope is signed by the Signer’s key (or includes a Fulcio‑issued cert chain) and chains to configured roots (Fulcio/KMS).
- Predicate type is one of Stella Ops types (sbom/report/vex‑export) with valid schema.
subject[*].digest.sha256is present and canonicalized.
Wire shape (JSON):
{
"bundle": { "dsse": { "payloadType": "application/vnd.in-toto+json", "payload": "<b64>", "signatures": [ ... ] },
"certificateChain": [ "-----BEGIN CERTIFICATE-----..." ],
"mode": "keyless" },
"meta": {
"artifact": { "sha256": "<subject sha256>", "kind": "sbom|report|vex-export", "imageDigest": "sha256:..." },
"bundleSha256": "<sha256 of canonical dsse>",
"logPreference": "primary", // "primary" | "mirror" | "both"
"archive": true // whether Attestor should archive bundle to S3
}
}
4) APIs
Endpoint reality map (2026-05-29, verified against
StellaOps.Attestor.WebService). The Attestor WebService is a hybrid host: the core submission/sign/verify/transparency/list/export surface is minimal-API (AttestorWebServiceEndpoints.cs,WatchlistEndpoints.cs,PredicateRegistryEndpoints.cs), while a set of MVC controllers add proof-chain, chain, bundle, verdict, and exception surfaces. Some controllers are real; others are feature-flagged 501 stubs that stay routable (never 404) and return501 Not Implementedwithcode=feature_not_implemented. The table below is the source of truth; the per-endpoint subsections that follow describe the implemented minimal-API surface in detail.
Surface / controller Routes Status Notes AttestorWebServiceEndpoints(minimal API)POST /api/v1/attestations:sign,POST /api/v1/rekor/entries,POST /api/v1/rekor/entries/proof-backfill,GET /api/v1/rekor/entries/{uuid},GET /api/v1/attestations/{uuid},POST /api/v1/rekor/verify,GET /api/v1/attestations,POST /api/v1/attestations:export/:import/:export-build,POST /api/v1/attestor/links,…/transparency/*Implemented Core trust surface. /api/v1/attestationsis aGETlist only (no barePOST— the:sign/:export/:import/:export-buildaction routes are the writes).POST …/rekor/entries/proof-backfill(attestor:write, rate-limiterattestor-verifications, SER-5b) backfills stored inclusion proofs.POST /api/v1/rekor/verify:bulk,GET /api/v1/rekor/verify:bulk/{jobId}bulk verify Implemented, durable Non-testing runtime uses PostgresBulkVerificationJobStoreplusBulkVerificationWorker; submit returns202 Accepted. A custom/testing host with an unsupported store may still return 501.WatchlistEndpoints(minimal API)…/api/v1/watchlist*Implemented See §18. PredicateRegistryEndpoints(minimal API)predicate registry reads Implemented — ProofChainControllerGET /api/v1/proofs/{subjectDigest},…/{subjectDigest}/chain,…/id/{proofId},…/id/{proofId}/verifyImplemented, durable Non-testing composition uses PostgresProofGraphService; entry persistence transactionally updates the Attestor-owned tenant projection. Every route requires the authenticated tenant claim. Subject digests are strict normalized SHA-256 values, graph output is ordinally stable, and an empty chain returns typed RFC 7807 404.ChainControllerGET /api/v1/chains/{attestationId}(+/upstream,/downstream,/graph),GET /api/v1/chains/artifact/{artifactDigest}Prototype — excluded from host The controller has traversal/visualization logic, but no durable tenant-aware IChainQueryServicecomposition. See §4.6.BundlesController…/api/v1/bundles*(create/get/list/verify/get-attestation)Prototype — excluded from host The bounded bundling library is tested, but no production aggregator/store/DI composition exists; host exclusion prevents an opaque activation 500. ExceptionControllerPOST /internal/api/v1/exceptions/sign,…/verify,…/recheck-status,…/renewPrototype — excluded from host DSSE-signed budget exceptions; see “Signed Exception” §2.1. The controller and IExceptionSigningServiceare deliberately not registered becauseIProofChainSignerneeds anIProofChainKeyStorewith no production implementation. Wiring a proof-chain key store is a trust-path decision; the host fails closed instead of advertising routes that return opaque 500s.VerdictControllerPOST /internal/api/v1/attestations/verdict,GET /api/v1/verdicts/{verdictId}Implemented, flag-gated Live when VerdictsEnabled=true(default); when disabled the controller is removed and routes 501. Create returns 201 only after EvidenceLocker durably accepts the tenant-bound verdict (persist-then-ack); downstream rejection/unavailability returns 503verdict_storage_failed. Lookup is tenant-scoped and served exclusively from EvidenceLocker — the controller holds no in-process verdict state (2026-07-12, EVI-1). A lookup that cannot reach EvidenceLocker returns 503verdict_lookup_unavailable; only an EvidenceLocker 404 yields 404verdict_not_found.ProofsControllerPOST /proofs/{entry}/spine,GET /proofs/{entry}/{receipt,spine,vex}501 stub Always returns 501 feature_not_implemented(even whenProofsEnabled=true). Forward spec only.AnchorsControllerGET/POST/PATCH/DELETE /anchors*,…/revoke-key501 stub Always returns 501. Trust-anchor management CRUD is not implemented (anchors are read indirectly via the proof-chain surface). Forward spec only.VerifyControllerPOST /verify/{proofBundleId},GET /verify/envelope/{hash},GET /verify/rekor/{hash}501 stub Always returns 501. Distinct from the implementedPOST /api/v1/rekor/verify. Forward spec only.Scope / policy naming (verified — reconciled to the canonical catalog, SPRINT_20260704_011). The scope claim values the Attestor checks are the canonical
StellaOpsScopesconstantsattest:create(write) andattest:read(read + verify). The ASP.NET authorization policy names referenced by endpoints keep the colon form (attestor:write,attestor:verify,attestor:read) as internal labels; each maps to the canonical claim plus a service bypass:
attestor:write←attest:createor configured service-to-service bypass network.attestor:verify←attest:readorattest:createor bypass (verify is a read-only inclusion check, so it collapses toattest:read; write is a superset).attestor:read←attest:readorattest:createor bypass.The legacy
attestor.write/attestor.verify/attestor.readdot-claims existed in no client grant (so the birth-certificate endpoint was unreachable by any real caller) and no longer appear anywhere; do not reintroduce them. The CLI (stellaops-cli/stellaops-cli-automation) is grantedattest:create attest:read. Rate-limiter policies attached per endpoint:attestor-submissions,attestor-verifications,attestor-reads,attestor-bulk.
4.1 Signing
POST /api/v1/attestations:sign (mTLS + OpTok required) — requires attestor:write (scope attest:create or service bypass); rate-limited via attestor-submissions.
Purpose: Deterministically wrap Stella Ops payloads in DSSE envelopes before Rekor submission. Reuses the submission rate limiter and honours caller tenancy/audience scopes.
Body:
{ "keyId": "signing-key-id", "payloadType": "application/vnd.in-toto+json", "payload": "<base64 payload>", "mode": "keyless|keyful|kms", "certificateChain": ["-----BEGIN CERTIFICATE-----..."], "artifact": { "sha256": "<subject sha256>", "kind": "sbom|report|vex-export", "imageDigest": "sha256:...", "subjectUri": "oci://..." }, "logPreference": "primary|mirror|both", "archive": true }Behaviour:
- Resolve the signing key from
attestor.signing.keys[](includes algorithm, provider, and optional KMS version). - Compute DSSE pre‑authentication encoding, sign with the resolved provider (default EC, BouncyCastle Ed25519, or File‑KMS ES256), and add static + request certificate chains.
- Canonicalise the resulting bundle, derive
bundleSha256, and mirror the request meta shape used by/api/v1/rekor/entries. - Emit
attestor.sign_total{result,algorithm,provider}andattestor.sign_latency_seconds{algorithm,provider}metrics and append an audit row (action=sign).
- Resolve the signing key from
Response 200:
{ "bundle": { "dsse": { "payloadType": "...", "payload": "...", "signatures": [{ "keyid": "signing-key-id", "sig": "..." }] }, "certificateChain": ["..."], "mode": "kms" }, "meta": { "artifact": { "sha256": "...", "kind": "sbom" }, "bundleSha256": "...", "logPreference": "primary", "archive": true }, "key": { "keyId": "signing-key-id", "algorithm": "ES256", "mode": "kms", "provider": "kms", "signedAt": "2025-11-01T12:34:56Z" } }Errors:
400 key_not_found,400 payload_missing|payload_invalid_base64|artifact_sha_missing,400 mode_not_allowed,403 client_certificate_required,401 invalid_token,500 signing_failed.
4.2 Submission
POST /api/v1/rekor/entries (mTLS + OpTok required) — requires attestor:write; rate-limited via attestor-submissions; audited (AuditActions.Attestor.SubmitRekorEntry).
Body: as above.
Behavior:
- Verify caller (mTLS + OpTok).
- Validate DSSE bundle (signature, cert chain to Fulcio/KMS; DSSE structure; payloadType allowed).
- Idempotency: compute
bundleSha256; check Redis/Valkey-backed dedupe state. If present, return existingrekorUuid. Development and Testing may use the in-memory fallback, but non-local runtime requiresattestor:redis:urlat startup. - Rekor pre-check: call Rekor index lookup (
/api/v2/index/retrievewith v1 fallback) by bundle hash before submit; if a UUID is found, fetch and reuse existing entry metadata instead of creating a duplicate. - Submit canonicalized bundle to Rekor v2 (primary or mirror according to
logPreference). - Retrieve inclusion proof (blocking until inclusion or up to
proofTimeoutMs); if backend returns promise only, returnstatus=pendingand retry asynchronously. - Persist
entriesrecord; archive DSSE to S3 ifarchive=true.entries.signer_key_idis the deterministic, non-empty DSSE signaturekeyid(ordinal-first when an envelope has multiple signatures), falling back to the caller client id only for legacy envelopes without a key id. The caller client id identifies the authenticated transport; downstream cryptographic trust matches the persisted DSSE key id to the producer’s published JWKS. If archive storage is disabled, write attempts report unsupported configuration instead of fabricating a successful archive. - Optional Scanner handoff: when
meta.scannerSignedSbomHandoffis present, Attestor forwards the submitted DSSE envelope bytes, real signing certificate material fromsigningCertPemorcertificateChain[0], and any explicitly supplied Rekor/tile/revocation material to ScannerPOST /api/v1/scans/{scanId}/exports/signed-sbom-materialafter the local entry is stored. Attestor does not create signatures, certificates, Fulcio roots, tile snapshots, revocation bundles, or placeholder proof material. Scanner remains the validation and persistence authority and rejects handoff material whose DSSE payload does not exactly match the current SBOM export.
Response 200:
{ "uuid": "…", "index": 123456, "proof": { "checkpoint": { "origin": "rekor@site", "size": 987654, "rootHash": "…", "timestamp": "…" }, "inclusion": { "leafHash": "…", "path": ["…","…"] } }, "logURL": "https://rekor…/api/v2/log/…/entries/…", "status": "included" }Errors:
401 invalid_token,403 not_signer|chain_untrusted,409 duplicate_bundle(with existinguuid),502 rekor_unavailable,504 proof_timeout.
4.3 Proof retrieval
GET /api/v1/rekor/entries/{uuid} (and the alias GET /api/v1/attestations/{uuid}) — requires attestor:read; rate-limited via attestor-reads.
- Returns the
attestor.entriesrow (refreshes proof from Rekor if stale/missing), plus the local transparency receipt and any external mirror receipts for the entry. - Accepts
?refresh=trueto force backend query.
4.4 Verification (third‑party or internal)
POST /api/v1/rekor/verify — requires attestor:verify (scope attest:read, or attest:create, or bypass); rate-limited via attestor-verifications; audited (AuditActions.Attestor.VerifyDsse).
Body (one of):
{ "uuid": "…" }{ "bundle": { …DSSE… } }{ "artifactSha256": "…" }(looks up most recent entry)
Checks:
- Bundle signature → cert chain to Fulcio/KMS roots configured.
- Inclusion proof → recompute leaf hash; verify Merkle path against checkpoint root.
- Optionally verify checkpoint against local trust anchors (if Rekor signs checkpoints).
- Confirm subject.digest matches caller‑provided hash (when given).
- Fetch transparency witness statement when enabled; cache results and downgrade status to WARN when endorsements are missing or mismatched.
Response:
{ "ok": true, "uuid": "…", "index": 123, "logURL": "…", "checkedAt": "…" }
4.5 Bulk verification
The non-testing runtime uses PostgresBulkVerificationJobStore and BulkVerificationWorker. POST /api/v1/rekor/verify:bulk enqueues up to quotas.bulk.maxItemsPerJob items and returns 202 Accepted with a job descriptor plus polling URL. GET /api/v1/rekor/verify:bulk/{jobId} returns progress and per-item results. Jobs remain tenant- and subject-scoped; only the initiating principal can read their progress. The Postgres worker claims queued jobs without double-processing and persists progress/results across restarts. A custom or testing host that deliberately registers an unsupported store may still return 501; that is not the shipped non-testing default.
Worker path: BulkVerificationWorker claims queued jobs (status=queued → running), executes items sequentially through the cached verification service, updates progress counters, and records metrics:
attestor.bulk_jobs_total{status}– completed/failed jobsattestor.bulk_job_duration_seconds{status}– job runtimeattestor.bulk_items_total{status}– per-item outcomes (succeeded,verification_failed,exception)
The worker honours bulkVerification.itemDelayMilliseconds for throttling and reschedules persistence conflicts with optimistic version checks. Results hydrate the verification cache; failed items record the error reason without aborting the overall job.
4.6 Durable proof-chain query and excluded legacy chain surface
ProofChainController is active. Non-testing composition binds IProofGraphService to PostgresProofGraphService, while Testing can explicitly replace it with the in-memory test helper. The durable projection is owned by Attestor as a read model of durable Attestor entries; it does not replace or mutate the Graph module’s canonical estate graph. ChainController remains excluded because its separate IChainQueryService contract has no durable production composition. The older bare /proofs/* shapes remain explicit 501 feature_not_implemented compatibility diagnostics.
ProofChainController (/api/v1/proofs; active)
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /api/v1/proofs/{subjectDigest} | attestor:read | Tenant-scoped proof list for a strict SHA-256 subject digest. |
GET | /api/v1/proofs/{subjectDigest}/chain?maxDepth= | attestor:read | Deterministic tenant-scoped graph; depth clamps to 1-10, empty returns typed RFC 7807 404. |
GET | /api/v1/proofs/id/{proofId} | attestor:read | Tenant-scoped durable entry detail. |
GET | /api/v1/proofs/id/{proofId}/verify | attestor:verify | Tenant-scoped proof verification; tenant identity is carried through repository and verification cache lookups. |
ChainController prototype (/api/v1/chains; excluded from the host)
| Method | Path | Description |
|---|---|---|
GET | /api/v1/chains/{attestationId}?maxDepth= | Prototype full-chain shape; not routed in production. |
GET | /api/v1/chains/{attestationId}/upstream?maxDepth= | Prototype dependency traversal; not routed in production. |
GET | /api/v1/chains/{attestationId}/downstream?maxDepth= | Prototype dependents traversal; not routed in production. |
GET | /api/v1/chains/{attestationId}/graph?format=&maxDepth= | Prototype Mermaid/DOT/JSON shape; not routed in production. |
GET | /api/v1/chains/artifact/{artifactDigest}?chain=&maxDepth= | Prototype artifact-chain shape; not routed in production. |
The proof controller clamps maxDepth to 1–10 (default 5). 003_tenant_proof_graph.sql upgrades attestor.entries identity/deduplication to tenant-composite keys and creates tenant-composite graph tables, foreign keys, and traversal indexes. PostgresAttestorEntryRepository.SaveAsync commits the entry and its projection in one transaction. Reads never accept a tenant header as authority: the stellaops:tenant/tenant_id/tenant authenticated claim supplies the partition, and missing tenant context fails closed. Node/edge collections use ordinal IDs; digest normalization and stable persisted timestamps make unchanged responses restart-stable.
The library-level InMemoryProofGraphService remains a test/development helper and is not the durable graph behind production routes. The separate GraphRoot library can sort inputs, compute a SHA-256 Merkle root, and sign a graph-root statement when a key resolver is explicitly supplied, but the Attestor host does not compose the two libraries. GraphRoot registration therefore requires the resolver-bearing AddGraphRootAttestation(keyResolver) overload; the parameterless overload rejects registration immediately instead of installing an attestor that DI cannot construct. The broader canonical estate-graph identity remains owned by src/Graph/StellaOps.Graph.Indexer/Schema/GraphIdentity.cs, which derives tenant- and kind-scoped SHA-256/Base32 identifiers from canonical identity tuples.
The cross-service Chain/Custody experience (archived program SPRINT_20260709_001, EF-1) remains the canonical operator view joining Release, Approval, Deployment, Attestor, and Estate reads. The active Attestor graph is deliberately narrower: it is an Attestor-owned subject-evidence projection and does not claim ownership of the broader estate topology.
5) Rekor v2 driver (backend)
Canonicalization: DSSE envelopes are normalized (stable JSON ordering, no insignificant whitespace) before hashing and submission.
Transport: HTTP/2 with retries (exponential backoff, jitter), budgeted timeouts.
Idempotency: if backend returns “already exists,” map to existing
uuid.Proof acquisition:
- In synchronous mode, poll the log for inclusion up to
proofTimeoutMs. - In asynchronous mode, return
pendingand schedule a proof fetcher job (PostgreSQL job record + backoff).
- In synchronous mode, poll the log for inclusion up to
Mirrors/dual logs:
- When
logPreference="both", submit to primary and mirror; store both UUIDs (primary canonical). - Optional cloud endorsement: POST to the Stella Ops cloud
/attest/endorsewith{uuid, artifactSha256}; store returned endorsement id. ⚠ Not implemented — see §1 dependencies note; no such client or endpoint exists insrc/.
- When
6) Security model
mTLS for submission from Signer (CA‑pinned) — config-conditional, not on by default: the reference compose stack runs plain HTTP behind the router and enforces the
attestor:writepolicy (scopeattest:createOR trusted-network service bypass) instead. EnableAttestor:Security:Mtls:RequireClientCertificateto make the peer-cert check mandatory. (See §3 caveat.)Authority token with
aud=attestorand DPoP/mTLS binding must be presented; Attestor verifies both.Bundle acceptance policy:
- DSSE signature must chain to the configured Fulcio (keyless) or KMS/HSM roots.
- SAN (Subject Alternative Name) must match Signer identity policy (e.g.,
urn:stellaops:signeror pinned OIDC issuer). - Predicate
predicateTypemust be on allowlist (sbom/report/vex-export). subject.digest.sha256values must be present and well‑formed (hex).
No public submission path. Never accept bundles from untrusted clients.
Client certificate allowlists: optional
security.mtls.allowedSubjects/allowedThumbprintstighten peer identity checks beyond CA pinning.Rate limits: token-bucket per caller derived from
quotas.perCaller(QPS/burst) returns429+Retry-Afterwhen exceeded.Scope enforcement: API separates
attestor:write,attestor:verify, andattestor:readpolicy names. Their canonical scope claims areattest:createandattest:read, with create satisfying read/verify as described in §4. Verification/list endpoints accept read while submission endpoints require create. Verdict creation (POST /internal/api/v1/attestations/verdict) uses the write policy; verdict lookup (GET /api/v1/verdicts/{verdictId}) uses the read policy.Verdict canonical-byte boundary: before signing or storage,
VerdictControllercanonicalizes the predicate once with the host-registered ProofChain canonicalizer. Those exact bytes determineVerdictId, the DSSE payload, and Evidence Locker’spredicate_digest; semantically equivalent object-key/whitespace forms therefore converge. Malformed or post-NFC-duplicate JSON returns400 invalid_predicate_jsonwithout invoking signing or storage. This is a server-authoritative deterministic profile, not a full RFC 8785/JCS interoperability claim.Verdict durability boundary: after authenticating and tenant-binding a request, Attestor signs a two-minute internal identity envelope containing only that tenant and the operation scope (
evidence:createfor store,evidence:readfor lookup). EvidenceLocker verifies the envelope and re-enforces tenant/scope. Attestor returns201only after the store response succeeds; missing identity/signing configuration, non-success responses, timeouts, and transport errors fail closed as503 verdict_storage_failed. No verdict is retained in process memory (EVI-1, 2026-07-12): the durable EvidenceLocker record is the single source of truth forGET /api/v1/verdicts/{verdictId}, so a verdict can never be reported as attested by a replica that merely created it. Lookups that cannot consult EvidenceLocker fail closed as503 verdict_lookup_unavailablerather than asserting a404they cannot substantiate. EvidenceLocker echoes the canonical tenant UUID it resolved from the delegated tenant claim (not the caller’s tenant slug); the lookup accepts that resolved form and refuses any other disagreeing tenant value.Request hygiene: JSON content-type is mandatory (415 returned otherwise); DSSE payloads are capped (default 2 MiB), certificate chains limited to six entries, and signatures to six per envelope to mitigate parsing abuse.
Redaction: Attestor never logs secret material; DSSE payloads should be public by design (SBOMs/reports). If customers require redaction, enforce policy at Signer (predicate minimization) before Attestor.
7) Storage & archival
Entries in PostgreSQL provide a local ledger keyed by
rekorUuidand artifact sha256 for quick reverse lookups.Dedupe/idempotency in non-local runtime is Redis/Valkey-backed and configured by
attestor:redis:url. Missing Redis/Valkey configuration fails startup outside Development and Testing so bundle dedupe is not process-local.S3 archival (if enabled):
s3://stellaops/attest/ dsse/<bundleSha256>.json proof/<rekorUuid>.json bundle/<artifactSha256>.zip # optional verification bundleArchive storage is optional, but explicit archive writes require a configured S3-compatible endpoint and bucket. When archive storage is disabled, reads return no bundle and writes emit unsupported-configuration errors rather than silently dropping content.
Verification bundles (zip):
- DSSE (
*.dsse.json), proof (*.proof.json),chain.pem(certs),README.txtwith verification steps & hashes.
- DSSE (
8) Observability & audit
Metrics (Prometheus):
attestor.sign_total{result,algorithm,provider}attestor.sign_latency_seconds{algorithm,provider}attestor.submit_total{result,backend}attestor.submit_latency_seconds{backend}attestor.proof_fetch_total{subject,issuer,policy,result,attestor.log.backend}attestor.verify_total{subject,issuer,policy,result}attestor.verify_latency_seconds{subject,issuer,policy,result}attestor.dedupe_hits_totalattestor.errors_total{type}
SLO guardrails:
attestor.verify_latency_secondsP95 ≤ 2 s per policy.attestor.verify_total{result="failed"}≤ 1 % ofattestor.verify_totalover 30 min rolling windows.
Correlation:
- HTTP callers may supply
X-Correlation-Id; Attestor will echo the header and pushCorrelationIdinto the log scope for cross-service tracing. - A validation rejection from
POST /api/v1/rekor/entriesremains an HTTP 400ProblemDetailsresponse and also emits an operator-visible Warning with structuredValidationCode, exact safeReason,CorrelationId, and resolvedTenantId(when available). The event never includes the DSSE envelope, signature, authorization token, or request headers.
Tracing:
- Spans:
attestor.sign,validate,rekor.submit,rekor.poll,persist,archive,attestor.verify,attestor.verify.refresh_proof.
Audit:
- Write/verify endpoints carry the shared endpoint-level
.Audited(AuditModules.Attestor, …)filter (AuditActions.Attestor.SubmitRekorEntry,VerifyDsse,CreateBulkVerification, etc.), which emits immutable audit events into the Timeline unified audit sink (timeline.unified_audit_events). There is no module-localattestor.audit/proofchain.audit_logtable — the latter is created and dropped inside001_v1_attestor_baseline.sql(:244/:776; the drop is folded in from the archived004_drop_deprecated_audit_log.sql).
9) Configuration (YAML)
attestor:
listen: "https://0.0.0.0:8444"
security:
mtls:
caBundle: /etc/ssl/signer-ca.pem
requireClientCert: true
authority:
issuer: "https://authority.internal"
jwksUrl: "https://authority.internal/jwks"
requireSenderConstraint: "dpop" # or "mtls"
signerIdentity:
mode: ["keyless","kms"]
fulcioRoots: ["/etc/fulcio/root.pem"]
allowedSANs: ["urn:stellaops:signer"]
kmsKeys: ["kms://cluster-kms/stellaops-signer"]
submissionLimits:
maxPayloadBytes: 2097152
maxCertificateChainEntries: 6
maxSignatures: 6
signing:
preferredProviders: ["kms","bouncycastle.ed25519","default"]
kms:
enabled: true
rootPath: "/var/lib/stellaops/kms"
password: "${ATTESTOR_KMS_PASSWORD}"
keys:
- keyId: "kms-primary"
algorithm: ES256
mode: kms
provider: "kms"
providerKeyId: "kms-primary"
kmsVersionId: "v1"
- keyId: "ed25519-offline"
algorithm: Ed25519
mode: keyful
provider: "bouncycastle.ed25519"
materialFormat: base64
materialPath: "/etc/stellaops/keys/ed25519.key"
certificateChain:
- "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----"
rekor:
primary:
url: "https://rekor-v2.internal"
proofTimeoutMs: 15000
pollIntervalMs: 250
maxAttempts: 60
mirror:
enabled: false
url: "https://rekor-v2.mirror"
scannerSignedSbomHandoff:
enabled: false
baseUrl: "https://scanner.internal"
bearerToken: "${ATTESTOR_SCANNER_HANDOFF_TOKEN}"
rekorPublicKeyPem: "${ATTESTOR_REKOR_PUBLIC_KEY_PEM}"
requestTimeoutMs: 15000
postgres:
connectionString: "Host=postgres;Port=5432;Database=attestor;Username=stellaops;Password=secret"
s3:
enabled: true
endpoint: "http://rustfs:8080"
bucket: "stellaops"
prefix: "attest/"
objectLock: "governance"
valkey:
url: "valkey://valkey:6379/2"
quotas:
perCaller:
qps: 50
burst: 100
Notes:
signing.preferredProvidersdefines the resolution order when multiple providers support the requested algorithm. Omit to fall back to registration order.- File-backed KMS (
signing.kms) is required when at least one key usesmode: kms; the password should be injected via secret store or environment. - For keyful providers, supply inline
materialormaterialPathplusmaterialFormat(pem(default),base64, orhex). KMS keys ignore these fields and requirekmsVersionId. certificateChainentries are appended to returned bundles so offline verifiers do not need to dereference external stores.
10) End‑to‑end sequences
A) Submit & include (happy path)
sequenceDiagram
autonumber
participant SW as Scanner.WebService
participant SG as Signer
participant AT as Attestor
participant RK as Rekor v2
SW->>SG: POST /sign/dsse (OpTok+PoE)
SG-->>SW: DSSE bundle (+certs)
SW->>AT: POST /rekor/entries (mTLS + OpTok)
AT->>AT: Validate DSSE (chain to Fulcio/KMS; signer identity)
AT->>RK: submit(bundle)
RK-->>AT: {uuid, index?}
AT->>RK: poll inclusion until proof or timeout
RK-->>AT: inclusion proof (checkpoint + path)
AT-->>SW: {uuid, index, proof, logURL}
B) Verify by artifact digest (CLI)
sequenceDiagram
autonumber
participant CLI as stellaops verify
participant SW as Scanner.WebService
participant AT as Attestor
CLI->>SW: GET /catalog/artifacts/{id}
SW-->>CLI: {artifactSha256, rekor: {uuid}}
CLI->>AT: POST /rekor/verify { uuid }
AT-->>CLI: { ok: true, index, logURL }
11) Failure modes & responses
| Condition | Return | Details | ||
|---|---|---|---|---|
| mTLS/OpTok invalid | 401 invalid_token | Include WWW-Authenticate DPoP challenge when applicable | ||
| Bundle not signed by trusted identity | 403 chain_untrusted | DSSE accepted only from Signer identities | ||
| Duplicate bundle | 409 duplicate_bundle | Return existing uuid (idempotent) | ||
| Rekor unreachable/timeout | 502 rekor_unavailable | Retry with backoff; surface Retry-After | ||
| Inclusion proof timeout | 202 accepted | status=pending, background job continues to fetch proof | ||
| Archive unavailable/failure | 200 with archive warning | Entry recorded; archive write records an error metric; no archive success is fabricated | ||
| Verification mismatch | 400 verify_failed | Include reason: chain | leafHash | rootMismatch |
12) Performance & scale
Stateless; scale horizontally. The verdict surface holds no replica-local state (the former static in-process
VerdictCachewas removed in EVI-1, 2026-07-12 — it could answer200on the creating replica and404on its peers, and could outlive the evidence it described). Every verdict lookup reads the durable EvidenceLocker record, so any replica answers identically.Targets:
- Submit+proof P95 ≤ 300 ms (warm log; local Rekor).
- Verify P95 ≤ 30 ms from cache; ≤ 120 ms with live proof fetch.
- 1k submissions/minute per replica sustained.
Hot caches: Redis/Valkey
dedupe(bundle hash → uuid), recententriesby artifact sha256.
13) Testing matrix
- Happy path: valid DSSE, inclusion within timeout.
- Idempotency: resubmit same
bundleSha256→ sameuuid. - Security: reject non‑Signer mTLS, wrong
aud, DPoP replay, untrusted cert chain, forbidden predicateType. - Rekor variants: promise‑then‑proof, proof delayed, mirror dual‑submit, mirror failure.
- Verification: corrupt leaf path, wrong root, tampered bundle.
- Throughput: soak test with 10k submissions; latency SLOs, zero drops.
14) Implementation notes
- Language: .NET 10 minimal API;
HttpClientwith sockets handler tuned for HTTP/2. - JSON: canonical writer for DSSE payload hashing.
- Crypto: use BouncyCastle/System.Security.Cryptography; PEM parsing for cert chains.
- Rekor client: pluggable driver; treat backend errors as retryable/non‑retryable with granular mapping.
- Safety: size caps on bundles; decompress bombs guarded; strict UTF‑8.
- CLI integration:
stellaops verify attestation <uuid|bundle|artifact>calls/rekor/verify.
15) Optional features
- Dual‑log write (primary + mirror) and cross‑log proof packaging.
- Cloud endorsement (not implemented — aspirational; see §1): send
{uuid, artifactSha256}to Stella Ops cloud; store returned endorsement id for marketing/chain‑of‑custody. No client/endpoint exists insrc/, and this would run against the on-prem/sovereign-first posture. - Checkpoint pinning: periodically pin latest Rekor checkpoints to an external audit store for independent monitoring.
16) Observability (stub)
- Runbook + dashboard placeholder for offline import:
operations/observability.md,operations/dashboards/attestor-observability.json. - Metrics to surface: signing latency p95/p99, verification failure rate, transparency log submission lag, key rotation age, queue backlog, attestation bundle size histogram.
- Health endpoints (live, mapped in
AttestorWebServiceComposition):/health/live(liveness) and/health/ready(readiness). There is no/statusendpoint and no/api/attestations/verifyprobe mapped today; the verification probe remains a forward placeholder pending a demo bundle (see runbook). - Alert hints: signing latency > 1s p99, verification failure spikes, tlog submission lag >10s, key rotation age over policy threshold, backlog above configured threshold.
17) Rekor Entry Events
Sprint: SPRINT_20260112_007_ATTESTOR_rekor_entry_events
Attestor emits deterministic events when DSSE bundles are logged to Rekor and inclusion proofs become available. These events drive policy reanalysis.
Event Types
| Event Type | Constant | Description |
|---|---|---|
rekor.entry.logged | RekorEventTypes.EntryLogged | Bundle successfully logged with inclusion proof |
rekor.entry.queued | RekorEventTypes.EntryQueued | Bundle queued for logging (async mode) |
rekor.entry.inclusion_verified | RekorEventTypes.InclusionVerified | Inclusion proof independently verified |
rekor.entry.failed | RekorEventTypes.EntryFailed | Logging or verification failed |
RekorEntryEvent Schema
{
"eventId": "rekor-evt-sha256:...",
"eventType": "rekor.entry.logged",
"tenant": "default",
"bundleDigest": "sha256:abc123...",
"artifactDigest": "sha256:def456...",
"predicateType": "StellaOps.ScanResults@1",
"rekorEntry": {
"uuid": "24296fb24b8ad77a...",
"logIndex": 123456789,
"logUrl": "https://rekor.sigstore.dev",
"integratedTime": "2026-01-15T10:30:02Z"
},
"reanalysisHints": {
"cveIds": ["CVE-2026-1234"],
"productKeys": ["pkg:npm/lodash@4.17.21"],
"mayAffectDecision": true,
"reanalysisScope": "immediate"
},
"occurredAtUtc": "2026-01-15T10:30:05Z"
}
Offline Mode Behavior
When operating in offline/air-gapped mode:
- Events are not emitted when Rekor is unreachable
- Bundles are queued locally for later submission
- Verification uses bundled checkpoints
- Events are generated when connectivity is restored
Snapshot Export/Import for Air-Gap Transfer
Sprint: SPRINT_20260208_021_Attestor_snapshot_export_import_for_air_gap
The Offline library provides snapshot export and import for transferring attestation state to air-gapped systems via portable archives.
Snapshot Levels:
| Level | Contents | Use Case |
|---|---|---|
| A | Attestation bundles only | Online verification still available |
| B | Evidence + verification material (Fulcio roots, Rekor keys) | Standard air-gap transfer |
| C | Full state: policies, trust anchors, org keys | Fully disconnected deployment |
Key Types:
SnapshotManifest— Content-addressed manifest with SHA-256 digests per entrySnapshotManifestEntry— Individual artifact withRelativePath,Digest,SizeBytes,CategoryISnapshotExporter— Produces portable JSON archives at the requested levelISnapshotImporter— Validates archive integrity and ingests entries into local storesSnapshotExportRequest/Result,SnapshotImportRequest/Result— Request/response models
Integrity:
- Each entry carries a SHA-256 digest; the manifest digest is computed from sorted
path:digestpairs plus the creation timestamp. - Import verifies all entry digests before ingestion (configurable via
VerifyIntegrity). - Existing entries can be skipped during import (
SkipExisting).
DI Registration:
services.AddAttestorOffline(); // registers ISnapshotExporter, ISnapshotImporter
18) Identity Watchlist & Monitoring
Sprint: SPRINT_0129_001_ATTESTOR_identity_watchlist_alerting
The Attestor provides proactive monitoring for signing identities appearing in transparency logs. Organizations can define watchlists to receive alerts when specific identities sign artifacts.
Purpose
- Credential compromise detection: Alert when your signing identity appears unexpectedly
- Third-party monitoring: Watch for specific vendors or dependencies signing artifacts
- Compliance auditing: Track all signing activity for specific issuers
Watchlist Entry Model
{
"id": "uuid",
"tenantId": "tenant-123",
"scope": "tenant", // tenant | global | system
"displayName": "GitHub Actions Signer",
"description": "Watch for GitHub Actions OIDC tokens",
// Identity fields (at least one required)
"issuer": "https://token.actions.githubusercontent.com",
"subjectAlternativeName": "repo:org/repo:*", // glob pattern
"keyId": null,
"matchMode": "glob", // exact | prefix | glob | regex
// Alert configuration
"severity": "warning", // info | warning | critical
"enabled": true,
"channelOverrides": ["slack-security"],
"suppressDuplicatesMinutes": 60,
"tags": ["github", "ci-cd"],
"createdAt": "2026-01-29T10:00:00Z",
"createdBy": "admin@example.com"
}
Matching Modes
| Mode | Behavior | Example Pattern | Matches |
|---|---|---|---|
exact | Case-insensitive equality | alice@example.com | Alice@example.com |
prefix | Starts-with match | https://accounts.google.com/ | Any Google OIDC issuer |
glob | Glob pattern (*, ?) | *@example.com | alice@example.com, bob@example.com |
regex | Full regex (with timeout) | repo:org/(frontend|backend):.* | repo:org/frontend:ref:main |
Scope Hierarchy
| Scope | Visibility | Who Can Create |
|---|---|---|
tenant | Owning tenant only | Operators with trust:write |
global | All tenants | Platform admins with trust:admin |
system | All tenants (read-only) | System bootstrap |
Authorization for the live watchlist surface follows the canonical trust scope family (trust:read, trust:write, trust:admin). The service still accepts legacy watchlist:* aliases for backward compatibility, but new clients and UI sessions should rely on the trust scopes.
Non-testing runtime persists watchlist state in PostgreSQL (attestor.identity_watchlist, attestor.identity_alert_dedup) and reserves in-memory watchlist storage for test harnesses only. If watchlist monitoring is enabled without a durable IAttestorEntrySource or IIdentityAlertPublisher, the runtime fails closed through disabled adapters instead of silently dropping entries or alerts.
Event Flow
New AttestorEntry persisted
→ SignerIdentityDescriptor extracted
→ IIdentityMatcher.MatchAsync()
→ For each match:
→ Check dedup window (default 60 min)
→ Emit attestor.identity.matched event
→ Route via Notifier rules → Slack/Email/Webhook
Event Schema (IdentityAlertEvent)
{
"eventId": "uuid",
"eventKind": "attestor.identity.matched",
"tenantId": "tenant-123",
"watchlistEntryId": "uuid",
"watchlistEntryName": "GitHub Actions Signer",
"matchedIdentity": {
"issuer": "https://token.actions.githubusercontent.com",
"subjectAlternativeName": "repo:org/repo:ref:refs/heads/main",
"keyId": null
},
"rekorEntry": {
"uuid": "24296fb24b8ad77a...",
"logIndex": 123456789,
"artifactSha256": "sha256:abc123...",
"integratedTimeUtc": "2026-01-29T10:30:00Z"
},
"severity": "warning",
"occurredAtUtc": "2026-01-29T10:30:05Z",
"suppressedCount": 0
}
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/v1/watchlist | Create watchlist entry |
GET | /api/v1/watchlist | List entries (tenant + optional global) |
GET | /api/v1/watchlist/{id} | Get single entry |
PUT | /api/v1/watchlist/{id} | Update entry |
DELETE | /api/v1/watchlist/{id} | Delete entry |
POST | /api/v1/watchlist/{id}/test | Test pattern against sample identity |
GET | /api/v1/watchlist/alerts | List recent alerts (paginated) |
CLI Commands
# Add a watchlist entry
stella watchlist add --issuer "https://token.actions.githubusercontent.com" \
--san "repo:org/*" --match-mode glob --severity warning
# List entries
stella watchlist list --include-global
# Test a pattern
stella watchlist test <id> --issuer "https://..." --san "repo:org/repo:ref:main"
# View recent alerts
stella watchlist alerts --since 24h --severity warning
Metrics
| Metric | Description |
|---|---|
attestor.watchlist.entries_scanned_total | Entries processed by monitor |
attestor.watchlist.matches_total{severity} | Pattern matches by severity |
attestor.watchlist.alerts_emitted_total | Alerts sent to notification system |
attestor.watchlist.alerts_suppressed_total | Alerts deduplicated |
attestor.watchlist.scan_latency_seconds | Per-entry scan duration |
The names above are logical keys. The filesystem provider percent-encodes individual key segments on disk so digest separators such as : are portable to Windows. Prefix is a physical namespace and is not returned as part of logical list keys. Rooted/traversal keys are rejected before filesystem access.
Configuration
attestor:
watchlist:
enabled: true
monitorMode: "changefeed" # changefeed | polling
pollingIntervalSeconds: 5 # only for polling mode
maxEventsPerSecond: 100 # rate limit
defaultDedupWindowMinutes: 60
regexTimeoutMs: 100 # safety limit
maxWatchlistEntriesPerTenant: 1000
Offline Mode
In air-gapped environments:
- Polling mode used instead of Postgres NOTIFY
- Alerts queued locally if notification channels unavailable
- Alerts delivered when connectivity restored
⚠ Status of certain ProofChain library subsystems (prototype — not wired into the live service)
Verified against
src/at HEAD, 2026-07-12 (SPRINT_20260712_009 CLO-1). Several sections that follow document subsystems that live in theStellaOps.Attestor.ProofChainlibrary but are not capabilities the runningstellaops-attestorservice exposes — each such section carries its own “Prototype library subsystem — not wired into the live service” caveat header. (Interleaved sections without that caveat — e.g. the Content-Addressed Store, Crypto-Sovereign design, the PostgreSQL persistence layer / schema isolation, and the Trust Domain Model — describe live service concerns and are unaffected.) As of HEAD each prototype subsystem below has zero consumers repo-wide — no HTTP endpoint, no worker, no other module calls them; only their own unit tests, which construct them directly withnew, exercise them — and the stateful ones are backed by an in-processConcurrentDictionarythat evaporates on restart:
- Unknowns five-dimensional triage scorer (
IUnknownsTriageScorer)- VEX findings API (
IVexFindingsService)- Binary fingerprint store (
IBinaryFingerprintStore)- Evidence subgraph visualization (
ISubgraphVisualizationService)- Idempotent SBOM/attestation ingest (
IIdempotentIngestService)- Regulatory compliance report generator (
IComplianceReportGenerator)- Monthly bundle rotation / re-signing (
IBundleRotationService)- In-toto link attestation capture (
ILinkCaptureService)- Noise ledger of suppressions (
INoiseLedgerService)- Score replay & verification (ProofChain
IScoreReplayService— distinct from the live ScannerStellaOps.Scanner.WebService.Services.IScoreReplayService, which is unrelated)- VEX receipt sidebar (
IReceiptSidebarService)- Field-ownership validator (
IFieldOwnershipValidator), evidence-coverage scorer (IEvidenceCoverageScorer), DSSE envelope size guard (IDsseEnvelopeSizeGuard)These are no longer registered in the Attestor host’s DI container (
AddProofChainServiceswas pruned in CLO-1). The host currently registersIJsonCanonicalizer, the durableIContentAddressedStore/IObjectStorageProviderinfrastructure, andICryptoProfileResolver;IExceptionSigningServiceis also deliberately absent because its trust-path dependencies have no production implementation. The types remain in the library as a tested prototype; a host that wants one must register it explicitly together with a durable store. Treat every “the service does X” / “the API exposes Y” sentence below as “the prototype library type can do X in-memory” — not as a live, durable, reachable Attestor capability. Do not integrate against these without first giving them a persistence backend.
Unknowns Five-Dimensional Triage Scoring (P/E/U/C/S)
Prototype library subsystem — not wired into the live service. See the status banner above.
Sprint: SPRINT_20260208_022_Attestor_unknowns_five_dimensional_triage_scoring
Overview
The triage scorer extends the existing IUnknownsAggregator pipeline with a five-dimensional scoring model for unknowns, enabling prioritized triage and temperature-band classification.
Scoring Dimensions
| Dimension | Code | Range | Description |
|---|---|---|---|
| Probability | P | [0,1] | Likelihood of exploitability or relevance |
| Exposure | E | [0,1] | Attack surface exposure (internal → internet-facing) |
| Uncertainty | U | [0,1] | Confidence deficit (fully understood → unknown) |
| Consequence | C | [0,1] | Impact severity (negligible → catastrophic) |
| Signal Freshness | S | [0,1] | Recency of intelligence (stale → just reported) |
Composite Score
Composite = Σ(dimension × weight) / Σ(weights), clamped to [0, 1].
Default weights: P=0.30, E=0.25, U=0.20, C=0.15, S=0.10 (configurable via TriageDimensionWeights).
Temperature Bands
| Band | Threshold | Action |
|---|---|---|
| Hot | ≥ 0.70 | Immediate triage required |
| Warm | ≥ 0.40 | Scheduled review |
| Cold | < 0.40 | Archive / low priority |
Thresholds are configurable via TriageBandThresholds.
Key Types
IUnknownsTriageScorer— Interface:Score(),ComputeComposite(),Classify()UnknownsTriageScorer— Implementation with OTel countersTriageScore— Five-dimensional score vectorTriageDimensionWeights— Configurable weights with staticDefaultTriageBandThresholds— Configurable Hot/Warm thresholds with staticDefaultTriageScoredItem— Scored unknown with composite score and bandTriageScoringRequest/Result— Batch scoring request/response
OTel Metrics
| Metric | Description |
|---|---|
triage.scored.total | Total unknowns scored |
triage.band.hot.total | Unknowns classified as Hot |
triage.band.warm.total | Unknowns classified as Warm |
triage.band.cold.total | Unknowns classified as Cold |
DI Registration
services.AddAttestorProofChain(); // registers IUnknownsTriageScorer
VEX Findings API with Proof Artifacts
Prototype library subsystem — not wired into the live service. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
Sprint: SPRINT_20260208_023_Attestor_vex_findings_api_with_proof_artifacts
Overview
The VEX Findings API provides a query and resolution service for VEX findings (CVE + component combinations) with their associated proof artifacts. Each finding carries DSSE signatures, Rekor receipts, Merkle proofs, and policy decision attestations that prove how the VEX status was determined.
Key Types
VexFinding— A finding withFindingId,VulnerabilityId,ComponentPurl,Status,Justification,ProofArtifacts,DeterminedAtProofArtifact— Proof material:Kind(DsseSignature/RekorReceipt/MerkleProof/ PolicyDecision/VexDelta/ReachabilityWitness),Digest,Payload,ProducedAtVexFindingStatus— NotAffected | Affected | Fixed | UnderInvestigationIVexFindingsService—GetByIdAsync,QueryAsync,ResolveProofsAsync,UpsertAsyncVexFindingQuery— Filters: VulnerabilityId, ComponentPurlPrefix, Status, TenantId, Limit, Offset
Proof Resolution
ResolveProofsAsync() merges new proof artifacts into a finding, deduplicating by digest. This allows incremental proof collection as new evidence is produced.
Finding IDs
Finding IDs are deterministic: SHA-256(vulnId:componentPurl) prefixed with finding:. This ensures the same CVE + component always maps to the same ID.
OTel Metrics
| Metric | Description |
|---|---|
findings.get.total | Findings retrieved by ID |
findings.query.total | Finding queries executed |
findings.upsert.total | Findings upserted |
findings.resolve.total | Proof resolution requests |
findings.proofs.total | Proof artifacts resolved |
DI Registration
// No live registration. IVexFindingsService remains a directly constructed prototype.
Binary Fingerprint Store & Trust Scoring
Prototype library subsystem — not wired into the live service. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
Overview
The Binary Fingerprint Store is a content-addressed repository for caller-supplied section-level binary hashes (for example ELF .text/.rodata or PE sections) with golden-set management and trust scoring. It enables:
- Content-addressed lookup: Fingerprints identified by
fp:sha256:…computed from(format, architecture, sectionHashes). - Section-level matching: Find closest match by comparing individual section hashes with a similarity score.
- Golden-set management: Define named sets of known-good fingerprints for baseline comparison.
- Trust scoring: Multi-factor score (0.0–0.99) based on golden membership, Build-ID, section coverage, evidence, and package provenance.
Library: StellaOps.Attestor.ProofChain Namespace: StellaOps.Attestor.ProofChain.FingerprintStore
Models
| Type | Purpose |
|---|---|
BinaryFingerprintRecord | Stored fingerprint: ID, format, architecture, file SHA-256, Build-ID, section hashes, package PURL, golden-set flag, trust score, evidence digests, timestamps. |
FingerprintRegistration | Input for RegisterAsync: format, architecture, file hash, section hashes, optional PURL/Build-ID/evidence. |
FingerprintLookupResult | Match result: found flag, matched record, golden match, section similarity (0.0–1.0), matched/differing section lists. |
TrustScoreBreakdown | Decomposed score: golden bonus, Build-ID score, section coverage, evidence score, provenance score. |
GoldenSet | Named golden set with count and timestamps. |
FingerprintQuery | Filters: format, architecture, PURL prefix, golden flag, golden set name, min trust score, limit/offset. |
Service Interface (IBinaryFingerprintStore)
| Method | Description |
|---|---|
RegisterAsync(registration) | Register fingerprint (idempotent by content-addressed ID). |
GetByIdAsync(fingerprintId) | Look up by content-addressed ID. |
GetByFileSha256Async(fileSha256) | Look up by whole-file hash. |
FindBySectionHashesAsync(sectionHashes, minSimilarity) | Best-match search by section hashes. |
ComputeTrustScoreAsync(fingerprintId) | Detailed trust-score breakdown. |
ListAsync(query) | Filtered + paginated listing. |
AddToGoldenSetAsync(fingerprintId, goldenSetName) | Mark fingerprint as golden (recalculates trust score). |
RemoveFromGoldenSetAsync(fingerprintId) | Remove golden flag. |
CreateGoldenSetAsync(name, description) | Create a named golden set. |
ListGoldenSetsAsync() | List all golden sets. |
GetGoldenSetMembersAsync(goldenSetName) | List members of a golden set. |
DeleteAsync(fingerprintId) | Remove fingerprint from store. |
Trust Score Computation
| Factor | Weight | Raw value |
|---|---|---|
| Golden-set membership | 0.30 | 1.0 if golden, 0.0 otherwise |
| Build-ID present | 0.20 | 1.0 if Build-ID exists, 0.0 otherwise |
| Section coverage | 0.25 | Ratio of key sections (.text, .rodata, .data, .bss) present |
| Evidence count | 0.15 | min(count/5, 1.0) |
| Package provenance | 0.10 | 1.0 if PURL present, 0.0 otherwise |
Final score is capped at 0.99.
DI Registration
AddProofChainServices() deliberately does not register IBinaryFingerprintStore. The in-memory implementation has no durable backing and no production consumer; a future owner must supply explicit durable registration and startup migration before live composition.
Observability (OTel Metrics)
Meter: StellaOps.Attestor.ProofChain.FingerprintStore
| Metric | Type | Description |
|---|---|---|
fingerprint.store.registered | Counter | Fingerprints registered |
fingerprint.store.lookups | Counter | Store lookups performed |
fingerprint.store.golden_added | Counter | Fingerprints added to golden sets |
fingerprint.store.deleted | Counter | Fingerprints deleted |
Test Coverage
36 tests in StellaOps.Attestor.ProofChain.Tests/FingerprintStore/BinaryFingerprintStoreTests.cs:
- Registration (new, idempotent, different sections → different IDs, validation)
- Lookup (by ID, by file SHA-256, not-found cases)
- Section-hash matching (exact, partial, below threshold, empty)
- Trust scoring (with/without Build-ID/PURL, minimal, golden bonus, cap at 0.99, determinism)
- Golden-set management (create, idempotent add, move, remove, delete bookkeeping, list members, list sets)
- List/query with filters (format, min trust score)
- Delete (existing, non-existent)
- Content-addressed ID determinism
Content-Addressed Store (CAS) for SBOM/VEX/Attestation Artifacts
Partial runtime infrastructure (verified 2026-07-17). The filesystem-backed CAS is registered and its storage is now mounted durably in the reference Compose stack. No production SBOM, VEX, or attestation producer calls it: the only business consumer type,
IdempotentIngestService, remains an unregistered prototype, and there is no VEX consumer.S3CompatibleandGcsconfiguration values fail explicitly because their providers are not implemented here. This is not a live unified artifact API, retention/GC service, or replacement for domain storage and EvidenceLocker custody.
Overview
The CAS provides a unified content-addressed storage service for all artifact types (SBOM, VEX, attestation, proof bundles, evidence packs, binary fingerprints). All blobs are keyed by SHA-256 digest of their raw content. Puts are idempotent: storing the same content twice returns the existing record with a dedup flag.
Library: StellaOps.Attestor.ProofChain Namespace: StellaOps.Attestor.ProofChain.Cas
Artifact Types
| Type | Description |
|---|---|
Sbom | Software Bill of Materials |
Vex | VEX (Vulnerability Exploitability Exchange) document |
Attestation | DSSE-signed attestation envelope |
ProofBundle | Proof chain bundle |
EvidencePack | Evidence pack manifest |
BinaryFingerprint | Binary fingerprint record |
Other | Generic/other artifact |
Models
| Type | Purpose |
|---|---|
CasArtifact | Stored artifact metadata: digest, type, media type, size, tags, related digests, timestamps, dedup flag. |
CasPutRequest | Input: raw content bytes, artifact type, media type, optional tags and related digests. |
CasPutResult | Output: stored artifact + dedup flag. |
CasGetResult | Retrieved artifact with content bytes. |
CasQuery | Filters: artifact type, media type, tag key/value, limit/offset. |
CasStatistics | Store metrics: total artifacts, bytes, dedup count, type breakdown. |
Service Interface (IContentAddressedStore)
| Method | Description |
|---|---|
PutAsync(request) | Store artifact (idempotent by SHA-256 digest). Returns dedup flag. |
GetAsync(digest) | Retrieve artifact + content by digest. |
ExistsAsync(digest) | Check existence by digest. |
DeleteAsync(digest) | Remove artifact. |
ListAsync(query) | Filtered + paginated listing. |
GetStatisticsAsync() | Total artifacts, bytes, dedup savings, type breakdown. |
Deduplication
When PutAsync receives content whose SHA-256 digest already exists in the store:
- The existing artifact metadata is returned (no duplicate storage).
CasPutResult.Deduplicatedis set totrue.- An OTel counter is incremented for audit.
DI Registration
AddProofChainServices() registers IContentAddressedStore -> ObjectStorageContentAddressedStore (singleton, via TryAddSingleton). Attestor WebService supplies ObjectStorageConfig from attestor:proofChain:cas and requires rootPath outside Testing when the filesystem provider is selected. InMemoryContentAddressedStore is retained for explicit unit tests only and must not be registered by production hosts. The reference Compose stack mounts attestor-proofchain-cas at /app/data/proofchain-cas so the configured filesystem root survives service recreation. Registration makes the storage infrastructure resolvable; it does not make it producer-reachable.
Observability (OTel Metrics)
Meter: StellaOps.Attestor.ProofChain.Cas
| Metric | Type | Description |
|---|---|---|
cas.puts | Counter | CAS put operations |
cas.deduplications | Counter | Deduplicated puts |
cas.gets | Counter | CAS get operations |
cas.deletes | Counter | CAS delete operations |
Test Coverage
StellaOps.Attestor.ProofChain.Tests/Cas/ObjectStorageTests.cs covers durable object-storage CAS behavior and DI registration. Legacy in-memory CAS tests remain as unit coverage for the test helper:
- Put (new, dedup, different content, validation, tags, related digests)
- Get (existing, non-existent)
- Exists (stored, not stored)
- Delete (existing, non-existent)
- List with filters (artifact type, media type, tags, pagination)
- Statistics (counts, bytes, dedup tracking)
- Digest determinism
Crypto-Sovereign Design (eIDAS/FIPS/GOST/SM/PQC)
Overview
The ProofChain library models role-based SigningKeyProfile values (Evidence, Reasoning, VexVerdict, Authority, Generator, Exception) and regional crypto policies. This vocabulary does not by itself enable every family in a deployed Attestor. The canonical hosted signing path is AttestorSigningService -> AttestorSigningKeyRegistry -> ICryptoProviderRegistry; newer HostProviders productization composes GOST and SM only. eIDAS-qualified, FIPS-HSM, and PQ host routes plus cross-profile negotiation remain incomplete.
Library: StellaOps.Attestor.ProofChain Namespace: StellaOps.Attestor.ProofChain.Signing
Algorithm-profile vocabulary (not runtime availability)
| Profile | Algorithm ID | Standard |
|---|---|---|
Ed25519 | ED25519 | RFC 8032 |
EcdsaP256 | ES256 | NIST FIPS 186-4 |
EcdsaP384 | ES384 | NIST FIPS 186-4 |
RsaPss | PS256 | PKCS#1 v2.1 |
Gost2012_256 | GOST-R34.10-2012-256 | Russian Federation |
Gost2012_512 | GOST-R34.10-2012-512 | Russian Federation |
Sm2 | SM2 | Chinese GB/T 32918 |
Dilithium3 | DILITHIUM3 | NIST FIPS 204 (ML-DSA) |
Falcon512 | FALCON512 | NIST PQC Round 3 |
EidasRsaSha256 | eIDAS-RSA-SHA256 | EU eIDAS + CAdES |
EidasEcdsaSha256 | eIDAS-ECDSA-SHA256 | EU eIDAS + CAdES |
Runtime enforcement is stricter than the policy vocabulary. A profile may appear in the model before a production provider is available, but Attestor must not treat it as usable unless ICryptoProviderRegistry resolves a configured provider with real key/trust material. A directly constructed ProofChainSigner resolves the regional policy and fails closed on key/algorithm mismatch, but the shipped host has no production IProofChainKeyStore and therefore does not compose IProofChainSigner. The current software GOST provider does not provide production GOST-R34.10-2012-512 custody, and generated/local FIPS, GOST, SM, PQ, or HSM simulation keys are local-harness only.
Attestor signing key registration treats provider names as explicit trust-root routes. Unknown configured providers fail closed through the shared crypto registry, and production-like runtimes reject sim.crypto.remote plus software-only cn.sm.soft before a key can become active. Production regional evidence keys must route to real adapters such as cn.sm.remote.http, GOST/CryptoPro/PKCS#11, eIDAS QSCD/TSP, HSM, or KMS-backed providers with operator-configured key material.
The pluginized crypto split keeps Attestor/Signer base startup on host-owned default and offline.verification providers. Regional pack IDs may exist in the catalog without host registration. pq.soft, eIDAS, and FIPS are not Attestor regional routes today; GOST/SM are the productized HostProviders families and still require certified/operator material in production-like environments. Source or catalog presence is not live acceptance.
The local EnvelopeSignatureService remains an Ed25519/ECDSA helper. Regional algorithms such as SM2 and GOST must use provider-backed signing paths; if they reach the local envelope helper, signing and verification fail with UnsupportedAlgorithm rather than emitting or accepting misleading envelopes.
Sovereign Regions
| Region | Default Algorithm | Requirements |
|---|---|---|
International | Ed25519 | None |
EuEidas | eIDAS-RSA-SHA256 | Qualified timestamp (Article 42), CAdES-T minimum |
UsFips | ECDSA-P256 | HSM-backed keys |
RuGost | GOST-2012-256 | GOST algorithms only |
CnSm | SM2 | SM national standards only |
PostQuantum | Dilithium3 | PQC finalist algorithms only |
Service Interface (ICryptoProfileResolver)
| Method | Description |
|---|---|
ResolveAsync(keyProfile) | Resolve key profile using active region. |
ResolveAsync(keyProfile, region) | Resolve key profile with explicit region override. |
ActiveRegion | Get the configured sovereign region. |
GetPolicy(region) | Get the sovereign policy for a region. |
ValidateQualifiedTimestampAsync(...) | eIDAS Article 42 timestamp validation. |
ProofChain model resolution flow (library-only)
SigningKeyProfile(role: Evidence/Reasoning/etc.) arrives atICryptoProfileResolver- Active
CryptoSovereignRegiondetermines theCryptoSovereignPolicy - Policy’s
DefaultAlgorithmproduces aCryptoProfileBinding - Binding carries: algorithm ID, region, CAdES level, HSM/timestamp requirements
- A direct caller may use the binding with its own key store; the production Attestor host does not compose this ProofChain signer/key-store path
eIDAS Article 42 Qualified Timestamp Validation
ValidateQualifiedTimestampAsync performs only the local pre-checks that are safe inside ProofChain:
- Non-eIDAS regions return
IsQualified = falseimmediately - Empty tokens or signed data are rejected
- ASN.1 SEQUENCE tag (0x30) is treated as a structural pre-check, not as qualified evidence
- Structural-only, simulator, or placeholder tokens return
IsQualified = false - A qualified result requires vendor-qualified sealed RFC 3161 evidence, TSA certificate-chain validation, and EU TSL/LOTL fixture validation in the eIDAS provider path
General Attestor RFC-3161 timestamp verification is handled by StellaOps.Attestor.Timestamping. It parses CMS SignedData timestamp tokens, validates the embedded TSTInfo message imprint against the attestation envelope digest, checks the CMS signature, and validates the TSA signing certificate against explicit offline trust anchors. Missing trust anchors, malformed tokens, bad imprints, invalid signatures, and expired TSA certificates fail closed with explicit verification statuses.
TimeCorrelationValidator is the anti-backdating-specific library check: the TST may be after Rekor only within ClockSkewTolerance, correlation is valid at the exact MaximumGap, and one tick beyond either boundary fails with TstAfterRekor or GapExceeded. TimestampPolicyEvaluator uses an injected TimeProvider for TSA certificate-freshness decisions. At HEAD these timestamp service, correlation, policy, and multi-provider types have no Attestor host registration or non-test consumer; AttestationTimestampService uses a separate symmetric Rekor-skew check and does not compose TimeCorrelationValidator.
CAdES Levels
| Level | Description |
|---|---|
CadesB | Basic Electronic Signature |
CadesT | With Timestamp (Article 42 minimum) |
CadesLT | With Long-Term validation data |
CadesLTA | With Long-Term Archival validation data |
DI Registration
AddProofChainServices() registers ICryptoProfileResolver → DefaultCryptoProfileResolver with the International fallback. It deliberately does not register IProofChainSigner because no production IProofChainKeyStore exists. Hosted regional signing uses the separate Attestor signing registry and HostProviders path; do not infer a production bridge from the resolver registration.
Observability (OTel Metrics)
Meter: StellaOps.Attestor.ProofChain.CryptoSovereign
| Metric | Type | Description |
|---|---|---|
crypto_sovereign.resolves | Counter | Profile resolution operations (tagged by region) |
crypto_sovereign.timestamp_validations | Counter | Qualified timestamp validations |
Test Coverage
Fresh run-004 executes the resolver plus direct ProofChain signer 49/49, canonical Attestor signing/SM2 gates 17/17, and regional registration/GOST-SM DSSE/PQ provider behavior 33/33. These prove bounded local behavior, not the incomplete five-family production aggregate. The resolver coverage includes:
- Region-based resolution (International/eIDAS/FIPS/GOST/SM/PQC default algorithms)
- Explicit region override
- All key profiles resolve for all regions
- Active region property
- Policy access and validation (all regions, eIDAS timestamp requirement, FIPS HSM requirement)
- Algorithm ID mapping (all 11 profiles)
- Qualified timestamp validation (non-eIDAS, empty token, empty data, invalid ASN.1, structural-only rejection)
- Cancellation handling
- Determinism (same inputs → identical bindings)
- Policy consistency (default in allowed list, non-empty allowed lists)
FixChain Attestation Verification Contract
FixChain verification is fail closed:
- Unsigned envelopes are invalid.
- Envelopes with signatures require configured trusted public key material; signature presence alone is never accepted as proof.
- DSSE verification uses the shared PAE verifier and reports deterministic failure reasons for missing keys, malformed signatures, or signature mismatch.
RequireRekorProof=trueremains invalid until a real Rekor/offline proof verifier is wired for FixChain envelopes.
DSSE Envelope Size Management (Guardrails, Chunking, Gateway Awareness)
Prototype library subsystem — not wired into the live service.
IDsseEnvelopeSizeGuardhas no host DI registration or consumer at HEAD. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
Overview
Pre-submission size guard for DSSE envelopes submitted to Rekor transparency logs. Validates envelope size against a configurable policy and determines the submission mode: full envelope (under soft limit), hash-only fallback, chunked with manifest, or rejected.
Library: StellaOps.Attestor.ProofChain Namespace: StellaOps.Attestor.ProofChain.Rekor
Submission Modes
| Mode | Trigger | Behavior |
|---|---|---|
FullEnvelope | Size ≤ soft limit | Envelope submitted to Rekor as-is |
HashOnly | Soft limit < size ≤ hard limit, hash-only enabled | Only SHA-256 payload digest submitted |
Chunked | Soft limit < size ≤ hard limit, chunking enabled | Envelope split into chunks with manifest |
Rejected | Size > hard limit, or no fallback available | Submission blocked |
Size Policy (DsseEnvelopeSizePolicy)
| Property | Default | Description |
|---|---|---|
SoftLimitBytes | 102,400 (100 KB) | Threshold for hash-only/chunked fallback |
HardLimitBytes | 1,048,576 (1 MB) | Absolute rejection threshold |
ChunkSizeBytes | 65,536 (64 KB) | Maximum size per chunk |
EnableHashOnlyFallback | true | Allow hash-only submission for oversized envelopes |
EnableChunking | false | Allow chunked submission (takes priority over hash-only) |
HashAlgorithm | “SHA-256” | Hash algorithm for digest computation |
Service Interface (IDsseEnvelopeSizeGuard)
| Method | Description |
|---|---|
ValidateAsync(DsseEnvelope) | Validate a typed DSSE envelope against size policy |
ValidateAsync(ReadOnlyMemory<byte>) | Validate raw serialized envelope bytes |
Policy | Get the active size policy |
Chunk Manifest
When chunking is enabled and an envelope exceeds the soft limit, the guard produces an EnvelopeChunkManifest containing:
TotalSizeBytes: original envelope sizeChunkCount: number of chunksOriginalDigest: SHA-256 digest of the complete original envelopeChunks: ordered array ofChunkDescriptor(index, size, digest, offset)
Each chunk is content-addressed by its SHA-256 digest for integrity verification.
DI Registration
AddProofChainServices() does not register IDsseEnvelopeSizeGuard. CLO-1 removed that registration after a repo-wide search found no production consumer; the type remains a directly tested prototype only. The live submission path instead uses AttestorSubmissionValidator, which enforces decoded-payload, signature-count, and certificate-chain-count boundaries. Its production registration binds those constraints from operator-owned Attestor:Security:SubmissionLimits.
The Gateway retains its global request-size boundary. There is no Attestor-route-specific size coordination, and the prototype’s hash-only/chunked results are not persisted, reconstructed, or submitted to Rekor by any production component.
Observability (OTel Metrics)
Meter: StellaOps.Attestor.ProofChain.EnvelopeSize
| Metric | Type | Description |
|---|---|---|
envelope_size.validations | Counter | Total envelope size validations |
envelope_size.hash_only_fallbacks | Counter | Hash-only fallback activations |
envelope_size.chunked | Counter | Chunked submission activations |
envelope_size.rejections | Counter | Envelope rejections |
Test Coverage
21 direct prototype tests in StellaOps.Attestor.ProofChain.Tests/Rekor/DsseEnvelopeSizeGuardTests.cs:
- Full envelope (small, exact soft limit)
- Hash-only fallback (activation, digest determinism)
- Chunked mode (activation, correct chunk count, priority over hash-only)
- Hard limit rejection
- Both fallbacks disabled rejection
- Raw bytes validation (under limit, empty rejection)
- Policy validation (negative soft, hard < soft, zero chunk size, defaults)
- Cancellation handling
- Digest determinism (same/different input)
- Chunk manifest determinism
- Size tracking
The live bounded path is additionally covered by three production-DI configured-limit regressions, four validator-hardening cases, and the 57-test Attestor Infrastructure suite (run-004, 2026-07-17).
DSSE-Wrapped Reach-Maps
Purpose
Reach-maps are standalone in-toto attestation artifacts that capture the full reachability graph for a scanned artifact. Unlike micro-witnesses (which capture individual vulnerability reachability paths), a reach-map aggregates the entire graph — all nodes, edges, findings, and analysis metadata — into a single DSSE-wrapped statement that can be stored, transmitted, and verified independently.
Predicate Type
URI: reach-map.stella/v1
The reach-map predicate follows Pattern B (predicate model in Predicates/, statement delegates PredicateType).
Data Model
ReachMapPredicate
Top-level predicate record containing:
| Field | Type | Description |
|---|---|---|
SchemaVersion | string | Always “1.0.0” |
GraphDigest | string | Deterministic SHA-256 digest of sorted graph content |
GraphCasUri | string? | Optional CAS URI for externalized graph storage |
ScanId | string | Identifier of the originating scan |
ArtifactRef | string | Package URL or image reference of the scanned artifact |
Nodes | ImmutableArray<ReachMapNode> | All nodes in the reachability graph |
Edges | ImmutableArray<ReachMapEdge> | All edges (call relationships) |
Findings | ImmutableArray<ReachMapFinding> | Vulnerability findings with reachability status |
AggregatedWitnessIds | ImmutableArray<string> | Deduplicated witness IDs from findings + explicit additions |
Analysis | ReachMapAnalysis | Analyzer metadata (tool, version, confidence, completeness) |
Summary | ReachMapSummary | Computed statistics (counts of nodes, edges, entry points, sinks) |
ReachMapNode
| Field | Type | Description |
|---|---|---|
NodeId | string | Unique identifier for the node |
QualifiedName | string | Fully qualified name (e.g., class.method) |
Module | string | Module or assembly containing the node |
IsEntryPoint | bool | Whether this node is a graph entry point |
IsSink | bool | Whether this node is a vulnerability sink |
ReachabilityState | string | One of the 8-state lattice values |
ReachMapEdge
| Field | Type | Description |
|---|---|---|
SourceNodeId | string | Origin node of the call edge |
TargetNodeId | string | Destination node of the call edge |
CallType | string | Edge type (direct, virtual, reflection, etc.) |
Confidence | double | Edge confidence score (0.0–1.0), default 1.0 |
ReachMapFinding
| Field | Type | Description |
|---|---|---|
VulnId | string | Vulnerability identifier |
CveId | string? | Optional CVE identifier |
Purl | string? | Optional package URL |
IsReachable | bool | Whether the vulnerability is reachable |
ConfidenceScore | double | Reachability confidence (0.0–1.0) |
SinkNodeIds | ImmutableArray<string> | Nodes where the vulnerability manifests |
ReachableEntryPointIds | ImmutableArray<string> | Entry points that can reach sinks |
WitnessId | string? | Optional micro-witness identifier |
ReachMapBuilder (Fluent API)
ReachMapBuilder provides a fluent interface for constructing reach-map predicates:
var predicate = new ReachMapBuilder()
.WithScanId("scan-001")
.WithArtifactRef("pkg:docker/myapp@sha256:abc123")
.WithAnalyzer("stella-reach", "2.0.0", 0.95, "full")
.WithGeneratedAt(DateTimeOffset.UtcNow)
.AddNodes(nodes)
.AddEdges(edges)
.AddFindings(findings)
.Build();
Deterministic Graph Digest
The builder computes a deterministic SHA-256 digest over the graph content:
- Nodes are sorted by
NodeId, each contributingNodeId|QualifiedName|ReachabilityState - Edges are sorted by
SourceNodeIdthenTargetNodeId, each contributingSource→Target|CallType - Findings are sorted by
VulnId, each contributingVulnId|IsReachable|ConfidenceScore - All contributions are concatenated with newlines and hashed
This ensures identical graphs always produce the same digest regardless of insertion order.
Witness Aggregation
Witness IDs are collected from two sources:
WitnessIdfields on individualReachMapFindingrecords- Explicit
AddWitnessId()calls on the builder
All witness IDs are deduplicated in the final predicate.
Schema Validation
The reach-map predicate type is registered in PredicateSchemaValidator:
HasSchema("reach-map.stella/v1")→trueValidateByPredicateTyperoutes toValidateReachMapPredicate- Required JSON properties:
graph_digest,scan_id,artifact_ref,nodes,edges,analysis,summary
Statement Integration
ReachMapStatement extends InTotoStatement with:
PredicateType→"reach-map.stella/v1"(fromReachMapPredicate.PredicateTypeUri)Type→"https://in-toto.io/Statement/v1"(inherited)
Source Files
- Predicate:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/ReachMapPredicate.cs - Statement:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/ReachMapStatement.cs - Builder:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Rekor/ReachMapBuilder.cs - Validator:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Json/PredicateSchemaValidator.DeltaValidators.cs
Test Coverage (25 tests)
- Build validation (missing ScanId, ArtifactRef, Analyzer)
- Minimal build, full build with summary statistics
- Graph digest determinism (same input, different order, different content)
- Witness aggregation (from findings, explicit, deduplication)
- Bulk add operations (AddNodes, AddEdges, AddFindings)
- CAS URI inclusion
- Statement integration (predicate type, statement type)
- Null argument protection (5 tests)
Attestable reachability witness and subgraph contracts
The Attestor ProofChain library defines two reachability evidence shapes. ReachabilityWitnessStatement wraps a per-finding ReachabilityWitnessPayload containing a call path, entrypoint, sink, gates, evidence metadata and observation time under predicate type stellaops.dev/predicates/reachability-witness@v1. ReachabilitySubgraphStatement wraps a minimal graph digest, finding keys and analyzer metadata under reachability-subgraph.stella/v1, which is registered with PredicateSchemaValidator.
ProofChainSigner serializes the typed in-toto Statement/v1, canonicalizes it with Rfc8785JsonCanonicalizer, binds it to DSSE payload type application/vnd.in-toto+json, and signs/verifies through the configured proof-chain key store and envelope signature service. Statement-level schema validation extracts the envelope’s predicate member before applying predicate-specific validation; validating the entire envelope as a predicate would incorrectly reject valid statements. The validator recognizes both reachability predicate types and applies the repository’s bounded required-field checks to each; previously the witness statement was rejected solely because its predicate type was absent from the registry.
Runtime boundary: these exact contracts are tested Attestor library types with no production consumer or Attestor host registration found at HEAD. Scanner owns separate reachability witness/subgraph publisher models and predicate URIs; its optional AddReachabilityWitnessAttestation registration also has no production call site. PathWitnessPredicateTypes and Attestor Core IProofEmitter belong to separate path-witness and Proof-of-Exposure flows.
The per-finding witness model can carry call-path nodes, entrypoint/sink details, optional gate descriptions, an IsReachable result, and analysis method/tool/hash metadata. It does not define the claimed analysis-assumptions field or enforce coherence between a negative result and path/gate content. The generic monthly AttestationBundler can Merkle-bind caller-aggregated DSSE entries, but it has no production aggregator/store registration, is explicitly removed from controller discovery, and does not provide a reachability-specific SBOM/VEX export-import verifier. Those pieces are not evidence of a self-contained, cross-environment proof-carrying reachability workflow.
AI explanation attestation contracts
The ProofChain library models AI explanations as AIExplanationPredicate, an AIArtifactBasePredicate specialization that binds the model identifier, prompt-template version, decoding parameters, ordered input hashes, authority claim, timestamps and output hash to explanation content, citations, confidence, subject, context and one of seven stable string-valued AIExplanationType members. AIArtifactReplayManifest carries the model, decoding parameters, prompt template, ordered inputs and expected output hash needed by an explicitly composed replayer.
AIExplanationStatement serializes the predicate in an in-toto Statement/v1 envelope with predicate type ai-explanation.stella/v1. AIArtifactMediaTypes maps that predicate type bidirectionally to application/vnd.stellaops.ai.explanation+json and recognizes the separate replay-manifest media type. These are library contracts; their presence does not imply that the live Attestor host publishes, discovers, replays or automatically verifies them.
AI remediation plan attestation contracts
AIRemediationPlanPredicate extends the common AI artifact metadata with a vulnerability and affected component, ordered typed steps, expected risk delta, before/after risk assessment, verification status, PR readiness, optional fix commit and evidence references. AIRemediationPlanStatement wraps it as in-toto Statement/v1 predicate ai-remediation.stella/v1; AIArtifactMediaTypes maps that predicate bidirectionally to application/vnd.stellaops.ai.remediation+json. An AIArtifactReplayManifest can bind the same model, decoding parameters, ordered inputs and expected output hash for an explicitly composed replayer.
Remediation authority classification honors the shared RequireResolvableEvidence threshold. When enabled, only references accepted by the configured resolver contribute to evidence backing; when disabled, the classifier deliberately does not consult the resolver or downgrade the plan for unresolvable references. Explicit AIArtifactVerificationStep execution still reclassifies claimed authority with its configured resolver.
Policy Studio Copilot attestation boundary (2026-07-18)
ProofChain defines AIPolicyDraftPredicate, AIPolicyDraftStatement, policy-rule and test-result DTOs, media-type mapping, and bounded policy-draft authority classification. Classification now treats validation as coherent only when syntax, semantics, and the overall result all pass and no failed-test identifiers are reported; contradictory summary state is forced to Suggestion and cannot auto-process.
These contracts are not a hosted Policy Studio attestation workflow. AdvisoryAI owns natural-language parsing and rule generation, while its validate and compile endpoints explicitly return HTTP 501 until durable generated-rule storage exists. The historical PolicyStudioIntegrationTests exercise test-local parser, generator, and synthesizer stubs rather than the production implementations. ExportCenter contains an optional policy-draft OCI publisher, but it has no production DI registration or consumer, and AdvisoryAI does not create the ProofChain policy predicate/statement. Generic ProofChainSigner behavior does not bridge that ownership gap. Do not claim generated tests, executed validation, signed snapshots, or Policy Studio-to-Attestor composition until one production owner maps and durably proves the complete flow.
AI authority classification
AIAuthorityClassifier is an Attestor ProofChain trust-boundary component. It deterministically reclassifies AI explanation, remediation-plan, VEX-draft, and policy-draft predicates as Suggestion, EvidenceBacked, or AuthorityThreshold. AIArtifactVerificationStep invokes the classifier while verifying an AI predicate and compares the claimed authority with the independently computed authority.
Explanation citations count as verified only when the predicate marks them verified and, when RequireResolvableEvidence=true, the configured Attestor evidence resolver can resolve their evidence IDs. Resolver failure therefore lowers both verified-citation rate and explanation quality; an unresolved high-confidence explanation cannot reach AuthorityThreshold merely by claiming that its citations were verified.
Runtime boundary: the classifier and explicit AIArtifactVerificationStep are real library components, but VerificationPipeline.CreateDefault does not currently include the AI step and no Attestor host DI registration was found for it. This feature proves classifier and explicit-step behavior; it does not claim that every live Attestor verification request automatically runs AI authority classification.
Evidence Coverage Score for AI Gating
Prototype library subsystem — not wired into the live service.
IEvidenceCoverageScorerhas no host DI registration or consumer at HEAD. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
Purpose
The Evidence Coverage Scorer prototype provides a deterministic, multi-dimensional assessment of how thoroughly an artifact’s evidence base covers the key verification axes. It does not currently gate any production AI auto-processing decision; that requires a production evidence resolver and an explicit promotion-policy consumer.
Evidence Dimensions
The scorer evaluates five independent dimensions:
| Dimension | Default Weight | Description |
|---|---|---|
| Reachability | 0.25 | Call graph analysis, micro-witnesses, reach-maps |
| BinaryAnalysis | 0.20 | Binary fingerprints, build-id verification, section hashes |
| SbomCompleteness | 0.25 | Component inventory, dependency resolution completeness |
| VexCoverage | 0.20 | Vulnerability status decisions (affected/not_affected/fixed) |
| Provenance | 0.10 | Build provenance, source attestation, supply chain evidence |
Scoring Algorithm
- For each dimension, the scorer trims identifiers, removes blanks, and deduplicates them ordinally so evidence padding cannot inflate coverage
- Each identifier is checked against an evidence resolver (
Func<string, bool>) — the same pattern used byAIAuthorityClassifier - Dimension score = (resolvable count) / (total count), producing a 0.0–1.0 value
- Overall score = weighted average across all dimensions (normalized by total weight)
- Missing dimensions receive a score of 0.0
Coverage Levels (Badge Rendering)
| Level | Threshold | Meaning |
|---|---|---|
| Green | >= 80% (configurable) | Full prototype coverage; not production auto-processing authorization |
| Yellow | >= 50% (configurable) | Partial coverage, manual review recommended |
| Red | < 50% | Insufficient evidence, gating blocks promotion |
AI Gating Policy
The EvidenceCoveragePolicy record controls:
- Per-dimension weights (must be non-negative)
- AI gating threshold (default 0.80) — minimum overall score for auto-processing
- Green/yellow badge thresholds
MeetsAiGatingThreshold is a prototype result only. No production caller binds it to AIAuthorityClassifier.CanAutoProcess or a verdict-promotion path.
DI Registration
IEvidenceCoverageScorer is deliberately not registered by ProofChainServiceCollectionExtensions and has zero production consumers. There is no infrastructure-owned persistence resolver at HEAD.
OTel Metrics
Meter: StellaOps.Attestor.ProofChain.EvidenceCoverage
| Counter | Description |
|---|---|
coverage.evaluations | Total coverage evaluations performed |
coverage.gating.pass | Evaluations that met AI gating threshold |
coverage.gating.fail | Evaluations that failed AI gating threshold |
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/AI/EvidenceCoverageModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/AI/IEvidenceCoverageScorer.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/AI/EvidenceCoverageScorer.cs - DI:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/ProofChainServiceCollectionExtensions.cs
Prototype Test Coverage (22 targeted tests)
- Full coverage (all dimensions resolvable, Green level)
- No evidence (empty inputs, Red, zero score)
- Partial coverage (weighted score calculation)
- Per-dimension breakdown (counts, reasons)
- Missing dimensions (zero score)
- Gating threshold (at threshold, below threshold)
- Custom thresholds (coverage level boundaries)
- Policy validation (negative weight, invalid threshold, green < yellow)
- Null argument protection (policy, resolver, meter factory, subject ref, inputs, result)
- Cancellation handling
- Determinism (same inputs produce same results)
- Default policy values
- Reason text verification
Evidence Subgraph UI Visualization
Prototype library subsystem — not wired into the live service. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
Purpose
The Subgraph Visualization Service renders proof graph subgraphs into multiple visualization formats suitable for interactive frontend rendering. It bridges the existing IProofGraphService.GetArtifactSubgraphAsync() BFS traversal with UI-ready output in Mermaid, Graphviz DOT, and structured JSON formats.
Render Formats
| Format | Use Case | Output |
|---|---|---|
| Mermaid | Browser-side rendering via Mermaid.js | graph TD markup with class definitions |
| Dot | Static/server-side rendering via Graphviz | digraph markup with color/shape attributes |
| Json | Custom frontend rendering (D3.js, Cytoscape.js) | Structured {nodes, edges} JSON |
Visualization Models
VisualizationNode
| Field | Type | Description |
|---|---|---|
Id | string | Unique node identifier |
Label | string | Formatted display label (type + truncated digest) |
Type | string | Node type string for icon/color selection |
ContentDigest | string? | Full content digest for provenance verification |
IsRoot | bool | Whether this is the subgraph root |
Depth | int | BFS depth from root (for layout layering) |
Metadata | ImmutableDictionary? | Optional key-value pairs for tooltips |
VisualizationEdge
| Field | Type | Description |
|---|---|---|
Source | string | Source node ID |
Target | string | Target node ID |
Label | string | Human-readable edge type label |
Type | string | Edge type string for styling |
Depth Computation
The service computes BFS depth from the root node bidirectionally through all edges, enabling hierarchical layout rendering. Unreachable nodes receive the maximum depth value.
Node Type Styling
| Node Type | Mermaid Shape | DOT Color |
|---|---|---|
| Artifact / Subject | [box] | #4CAF50 (green) |
| SbomDocument | ([stadium]) | #2196F3 (blue) |
| InTotoStatement / DsseEnvelope | [[subroutine]] | #FF9800 (orange) |
| VexStatement | ([stadium]) | #9C27B0 (purple) |
| RekorEntry | [(cylinder)] | #795548 (brown) |
| SigningKey / TrustAnchor | ((circle)) | #607D8B (blue-grey) |
DI Registration
ISubgraphVisualizationService is deliberately not registered by ProofChainServiceCollectionExtensions. It has no production caller; its Mermaid/DOT/JSON renderer is a directly constructed formatting prototype rather than the Findings/Web evidence-subgraph API contract.
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Graph/SubgraphVisualizationModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Graph/ISubgraphVisualizationService.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Graph/SubgraphVisualizationService.cs
Test Coverage (25 retained targeted tests)
- Empty subgraph rendering
- Single node with root detection and depth
- Multi-node depth computation (root=0, child=1, grandchild=2)
- Mermaid format (graph directive, node/edge content, class definitions)
- DOT format (digraph directive, node colors)
- JSON format (valid JSON output)
- Edge type labels (5 inline data tests)
- Node type preservation (4 inline data tests)
- Content digest truncation in labels
- Cancellation handling
- Null argument protection
- Determinism (same input produces same output)
- All three formats produce non-empty content (3 inline data tests)
Field-Level Ownership Map for Receipts and Bundles
Prototype library subsystem — not wired into the live service.
IFieldOwnershipValidatorhas no host DI registration or consumer at HEAD. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
Purpose
The Field-Level Ownership Map provides a machine-readable and human-readable document that maps each field in VerificationReceipt and VerificationCheck to the responsible module. This enables automated validation that fields are populated by their designated owner module, supporting audit trails and cross-module accountability.
Owner Modules
| Module | Responsibility |
|---|---|
| Core | Fundamental identifiers, timestamps, versions, tool digests |
| Signing | Key identifiers and signature-related fields |
| Rekor | Transparency log indices and inclusion proofs |
| Verification | Trust anchors, verification results, check details |
| SbomVex | SBOM/VEX document references |
| Provenance | Provenance and build attestation fields |
| Policy | Policy evaluation results |
| External | Fields populated by external integrations |
Ownership Map Structure
The FieldOwnershipMap record contains:
DocumentType— the document being mapped (e.g., “VerificationReceipt”)SchemaVersion— version of the ownership schema (default “1.0”)Entries— immutable list ofFieldOwnershipEntryrecords
Each FieldOwnershipEntry declares:
FieldPath— dot-path or array-path (e.g.,proofBundleId,checks[].keyId)Owner— theOwnerModuleresponsible for populating the fieldIsRequired— whether the field must be populated for validityDescription— human-readable purpose of the field
Default Receipt Ownership Map (14 entries)
| Field Path | Owner | Required |
|---|---|---|
proofBundleId | Core | Yes |
verifiedAt | Core | Yes |
verifierVersion | Core | Yes |
anchorId | Verification | Yes |
result | Verification | Yes |
checks | Verification | Yes |
checks[].check | Verification | Yes |
checks[].status | Verification | Yes |
checks[].keyId | Signing | No |
checks[].logIndex | Rekor | No |
checks[].expected | Verification | No |
checks[].actual | Verification | No |
checks[].details | Verification | No |
toolDigests | Core | No |
Validation
ValidateReceiptOwnershipAsync checks a VerificationReceipt against the ownership map:
- Iterates top-level fields, recording population status
- Expands per-check fields for each
VerificationCheckentry - Counts missing required fields
- Returns
FieldOwnershipValidationResultwith computed properties:IsValid— true whenMissingRequiredCount == 0TotalFields— total field population recordsPopulatedCount— fields that have valuesValidCount— fields with valid ownership
DI Registration
IFieldOwnershipValidator -> FieldOwnershipValidator (TryAddSingleton)
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/FieldOwnershipModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/IFieldOwnershipValidator.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/FieldOwnershipValidator.cs
Test Coverage (24 tests)
- Ownership map structure (document type, entry count, top-level fields, check fields)
- Owner assignment theories (7 top-level + 4 check-level field-to-owner mappings)
- Description completeness (all entries have descriptions)
- Full receipt validation (valid, all populated, correct counts)
- Minimal receipt validation (valid, optional fields not populated)
- Empty checks validation (missing required → invalid)
- Multi-check field expansion (fields per check entry)
- Ownership validity (all fields valid in static map)
- ValidatedAt propagation
- Null receipt protection
- Cancellation token handling
- Determinism (same inputs produce same results)
- Static map required/optional field markers
- Computed property correctness
Idempotent SBOM/Attestation APIs
Prototype library subsystem — not wired into the live service. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
Purpose
The Idempotent Ingest Service provides content-hash-based deduplication for SBOM ingest and attestation verification operations. Duplicate submissions return the original result without creating duplicate records, ensuring safe retries and deterministic outcomes.
Architecture
The service builds on the existing IContentAddressedStore (CAS), which already provides SHA-256-based deduplication at the storage layer. The idempotent service adds:
- SBOM Ingest — wraps CAS
PutAsyncwith SBOM-specific metadata (media type, tags, artifact type) and returns a typedSbomEntryId - Attestation Verify — stores attestation in CAS, performs verification checks, and caches results by content hash in a
ConcurrentDictionary - Idempotency Key Support — optional client-provided keys that map to content digests, enabling safe retries even when content bytes differ
Idempotency Guarantees
| Scenario | Behavior |
|---|---|
| Same content, no key | CAS deduplicates by SHA-256 hash, returns Deduplicated = true |
| Same content, same key | Returns cached result via key lookup |
| Different content, same key | Returns original result mapped to the key |
| Same content, different key | Both keys map to the same digest |
Verification Checks
The baseline attestation verification performs three deterministic checks:
| Check | Description |
|---|---|
content_present | Content is non-empty |
digest_format | Valid SHA-256 digest format (71 chars) |
json_structure | Content parses as a valid JSON object |
These checks are prototype input-shape checks only; they do not perform DSSE signature, trust-policy, subject-binding, or transparency verification. The composed IAttestorVerificationService owns those production checks.
Models
| Type | Description |
|---|---|
SbomIngestRequest | Content, MediaType, Tags, optional IdempotencyKey |
SbomIngestResult | Digest, Deduplicated, Artifact, SbomEntryId |
AttestationVerifyRequest | Content, MediaType, optional IdempotencyKey |
AttestationVerifyResult | Digest, CacheHit, Verified, Summary, Checks, VerifiedAt |
AttestationCheckResult | Check, Passed, Details |
IdempotencyKeyEntry | Key, Digest, CreatedAt, OperationType |
DI status
IIdempotentIngestService is not registered by the Attestor host. A prototype consumer must opt in explicitly and supply IContentAddressedStore, optional TimeProvider, and IMeterFactory; production callers should use the durable Rekor submission and verification surfaces instead.
OTel Metrics
Meter: StellaOps.Attestor.ProofChain.Idempotency
| Counter | Description |
|---|---|
idempotent.sbom.ingests | Total SBOM ingest operations |
idempotent.sbom.deduplications | SBOM submissions that were deduplicated |
idempotent.attest.verifications | Total attestation verifications (non-cached) |
idempotent.attest.cache_hits | Attestation verifications served from cache |
idempotent.key.hits | Idempotency key lookups that found existing entries |
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Idempotency/IdempotentIngestModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Idempotency/IIdempotentIngestService.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Idempotency/IdempotentIngestService.cs
Test Coverage (30 tests)
- SBOM ingest: first submission, duplicate dedup, different content, tags, idempotency key retry, empty content, empty media type, null request, cancellation, artifact type
- Attestation verify: first submission, duplicate cache hit, JSON structure pass/fail, content check, digest check, idempotency key, null request, empty content, cancellation, determinism, summary text
- Idempotency key lookup: unknown key, after ingest, after verify, null key
- Constructor validation: null store, null meter factory, null time provider
Regulatory Compliance Report Generator (NIS2/DORA/ISO-27001/EU CRA)
Prototype library subsystem — not wired into the live service. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
Purpose
The Compliance Report Generator provides a static registry of regulatory controls and maps evidence artifacts to regulatory requirements. It generates compliance reports that identify which controls are satisfied by available evidence and which have gaps, enabling auditable regulatory alignment for release decisions.
Supported Frameworks
| Framework | Controls | Description |
|---|---|---|
| NIS2 | 5 | EU Network and Information Security Directive 2 |
| DORA | 5 | EU Digital Operational Resilience Act |
| ISO-27001 | 6 | ISO/IEC 27001 Information Security Management |
| EU CRA | 4 | EU Cyber Resilience Act |
Evidence Artifact Types
| Type | Description |
|---|---|
Sbom | Software Bill of Materials |
VexStatement | Vulnerability Exploitability Exchange statement |
SignedAttestation | Signed attestation envelope |
TransparencyLogEntry | Rekor transparency log entry |
VerificationReceipt | Proof of verification |
ProofBundle | Bundled evidence pack |
ReachabilityAnalysis | Binary fingerprint or reachability analysis |
PolicyEvaluation | Policy evaluation result |
ProvenanceAttestation | Build origin proof |
IncidentReport | Incident response documentation |
Control Registry (20 controls)
NIS2 Controls
| ID | Category | Satisfied By |
|---|---|---|
| NIS2-Art21.2d | Supply Chain Security | SBOM, VEX, Provenance |
| NIS2-Art21.2e | Supply Chain Security | VEX, Reachability |
| NIS2-Art21.2a | Risk Management | Policy, Attestation |
| NIS2-Art21.2g | Risk Management | Receipt, ProofBundle |
| NIS2-Art23 | Incident Management | Incident, Transparency |
DORA Controls
| ID | Category | Satisfied By |
|---|---|---|
| DORA-Art6.1 | ICT Risk Management | Policy, Attestation |
| DORA-Art9.1 | ICT Risk Management | Attestation, Receipt, ProofBundle |
| DORA-Art17 | Incident Classification | Incident, VEX |
| DORA-Art28 | Third-Party Risk | SBOM, Provenance, Reachability |
| DORA-Art11 | ICT Risk Management (optional) | ProofBundle, Transparency |
ISO-27001 Controls
| ID | Category | Satisfied By |
|---|---|---|
| A.8.28 | Application Security | SBOM, Reachability, Provenance |
| A.8.9 | Configuration Management | Policy, Attestation |
| A.8.8 | Vulnerability Management | VEX, Reachability, SBOM |
| A.5.23 | Cloud Security (optional) | Provenance, ProofBundle |
| A.5.37 | Operations Security | Receipt, Transparency |
| A.5.21 | Supply Chain Security | SBOM, VEX, Provenance |
EU CRA Controls
| ID | Category | Satisfied By |
|---|---|---|
| CRA-AnnexI.2.1 | Product Security | SBOM |
| CRA-AnnexI.2.5 | Vulnerability Management | VEX, Reachability |
| CRA-Art11 | Vulnerability Management | VEX, Incident, Transparency |
| CRA-AnnexI.1.2 | Product Security | Policy, Attestation, Receipt |
Report Structure
ComplianceReport computed properties:
CompliancePercentage— ratio of satisfied to total controlsMandatoryGapCount— mandatory controls not satisfiedMeetsMinimumCompliance— true when all mandatory controls satisfied
DI Registration
IComplianceReportGenerator -> ComplianceReportGenerator (TryAddSingleton factory)
OTel Metrics
Meter: StellaOps.Attestor.ProofChain.Compliance
| Counter | Description |
|---|---|
compliance.reports.generated | Total compliance reports generated |
compliance.controls.evaluated | Total individual controls evaluated |
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Compliance/RegulatoryComplianceModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Compliance/IComplianceReportGenerator.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Compliance/ComplianceReportGenerator.cs
Test Coverage (26 tests)
- Supported frameworks (count and membership)
- Control counts per framework (4 theories)
- Control ID presence per framework (4 theories)
- Framework assignment and required field validation
- Full evidence → 100% compliance (4 theories)
- No evidence → 0% compliance (4 theories)
- Partial evidence → partial compliance
- Subject ref and framework recording
- Generated timestamp
- Artifact ref tracing
- Gap descriptions (present for unsatisfied, absent for satisfied)
- Null subject/evidence protection
- Cancellation token
- Determinism
- Constructor validation
- Mandatory vs optional controls
- NIS2 control categories (5 theories)
In-toto Link Attestation Capture (Sprint 015)
The historical aggregate is terminally superseded. The ProofChain ILinkCaptureService below remains an unregistered process-memory prototype. It is distinct from the bounded live POST /api/v1/attestor/links route, which builds a Link/v1 statement from caller-supplied materials/products and delegates signing to IInTotoLinkSigningService. The live route is not an in-toto-run command wrapper, CI capture integration, durable link repository, query API, or complete layout chain. Its signing boundary rejects missing products/subjects, missing artifact identifiers/digests, and absent or invalid signer-produced DSSE signatures; it never substitutes a placeholder signature.
Prototype library subsystem — not wired into the live service.
ILinkCaptureServicehas no host DI registration or consumer at HEAD; captured links live in an in-process dictionary that does not survive restart. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
The LinkCapture subsystem provides in-toto link attestation capture for supply chain step recording. It captures materials (inputs) and products (outputs) with content-addressed deduplication, enabling CI pipeline step evidence collection.
Domain Model
| Record | Purpose |
|---|---|
CapturedMaterial | Input artifact (URI + digest map) |
CapturedProduct | Output artifact (URI + digest map) |
CapturedEnvironment | Execution context (hostname, OS, variables) |
LinkCaptureRequest | Capture request with step, functionary, command, materials, products, env, byproducts, pipeline/step IDs |
LinkCaptureResult | Result with content-addressed digest, dedup flag, stored record |
CapturedLinkRecord | Stored link with all fields + CapturedAt timestamp |
LinkCaptureQuery | Query filter: step name, functionary, pipeline ID, limit |
Deduplication
Content-addressed deduplication uses canonical hashing:
- Canonical form: step name + functionary + command + sorted materials + sorted products
- Environment and byproducts are excluded from the digest to ensure deterministic deduplication across different execution contexts
- SHA-256 digest with
sha256:prefix - Materials and products sorted by URI (ordinal) before hashing
Service Interface
ILinkCaptureService:
CaptureAsync(LinkCaptureRequest)→LinkCaptureResult— idempotent captureGetByDigestAsync(string digest)→CapturedLinkRecord?— lookup by content digestQueryAsync(LinkCaptureQuery)→ImmutableArray<CapturedLinkRecord>— filtered query (case-insensitive, ordered by descending timestamp)
DI Registration
ILinkCaptureService -> LinkCaptureService (TryAddSingleton factory)
OTel Metrics
Meter: StellaOps.Attestor.ProofChain.LinkCapture
| Counter | Description |
|---|---|
link.captures | Total new link attestations captured |
link.deduplications | Total deduplicated captures |
link.queries | Total query operations |
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/LinkCapture/LinkCaptureModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/LinkCapture/ILinkCaptureService.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/LinkCapture/LinkCaptureService.cs
Test Coverage (30 tests)
- Basic capture with digest, step, functionary verification
- Timestamp from TimeProvider
- Materials and products recording
- Environment and byproducts recording
- Pipeline/step ID recording
- Deduplication (same request returns deduplicated=true)
- Different step/functionary/materials produce different digests
- Deterministic digest (material order invariance)
- Environment excluded from digest
- Null/empty validation (request, step, functionary)
- Cancellation token handling
- GetByDigest (found, not found, null, cancelled)
- Query by step name, functionary, pipeline ID
- Case-insensitive query filtering
- Empty store query
- No-filter returns all
- Limit enforcement
- Descending timestamp ordering
- Constructor validation
Monthly Bundle Rotation and Re-Signing (Sprint 016)
Prototype library subsystem — not wired into the live service. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
The BundleRotation subsystem provides scheduled key rotation for DSSE-signed bundles. It verifies bundles with the old key, re-signs them with a new key, and records a transition attestation for audit trail.
Domain Model
| Record | Purpose |
|---|---|
RotationStatus | Enum: Pending, Verified, ReSigned, Completed, Failed, Skipped |
RotationCadence | Enum: Monthly, Quarterly, OnDemand |
KeyTransition | Old/new key IDs, algorithm, effective date, grace period |
BundleRotationRequest | Rotation cycle request with transition, bundle digests, cadence, tenant |
BundleRotationEntry | Per-bundle result (original/new digest, status, error) |
BundleRotationResult | Full cycle result with computed SuccessCount/FailureCount/SkippedCount |
TransitionAttestation | Audit record: attestation ID, rotation ID, result digest, counts |
RotationScheduleEntry | Schedule config: cadence, next/last rotation, current key, enabled |
RotationHistoryQuery | Query filter: tenant, key ID, status, limit |
Re-Signing Workflow
- Validate request (rotation ID, key IDs, bundle digests)
- Verify old key and new key exist in
IProofChainKeyStore - For each bundle: verify with old key → compute re-signed digest → record entry
- Determine overall status from individual entries
- Create
TransitionAttestationwith result digest for integrity verification - Store in rotation history
Service Interface
IBundleRotationService:
RotateAsync(BundleRotationRequest)→BundleRotationResult— execute rotation cycleGetTransitionAttestationAsync(string rotationId)→TransitionAttestation?— get audit attestationQueryHistoryAsync(RotationHistoryQuery)→ImmutableArray<BundleRotationResult>— query historyComputeNextRotationDate(RotationCadence, DateTimeOffset?)→DateTimeOffset— schedule computation
DI Registration
IBundleRotationService -> BundleRotationService (TryAddSingleton factory, requires IProofChainKeyStore)
OTel Metrics
Meter: StellaOps.Attestor.ProofChain.Signing.Rotation
| Counter | Description |
|---|---|
rotation.cycles.started | Total rotation cycles initiated |
rotation.cycles.completed | Total rotation cycles completed |
rotation.bundles.resigned | Total bundles successfully re-signed |
rotation.bundles.skipped | Total bundles skipped |
rotation.bundles.failed | Total bundles that failed rotation |
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Signing/BundleRotationModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Signing/IBundleRotationService.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Signing/BundleRotationService.cs
Test Coverage (35 tests)
- Basic rotation (completed result, success count, new digests, transition, timestamps)
- Key validation (old key missing, new key missing → all fail)
- Empty bundle digest → entry fails
- Argument validation (null request, empty rotation ID, empty bundles, empty key IDs, cancellation)
- Transition attestation (created after rotation, has result digest, records transition, not found for unknown, null/cancel)
- Query history (empty, after rotation, filter by key ID, filter by status, limit, null/cancel)
- Schedule computation (monthly +1 month, quarterly +3 months, on-demand immediate, null last uses current time)
- Determinism (same inputs → same re-signed digests)
- Constructor validation (null key store, null meter factory, null time provider OK)
Noise Ledger — Audit Log of Suppressions (Sprint 017)
Prototype library subsystem — not wired into the live service. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”). The “audit log” is an in-process dictionary that does not survive restart — do not rely on it as durable suppression evidence.
The NoiseLedger prototype provides an in-process, queryable collection of caller-supplied suppression decisions. The production attestation pipeline does not call it; no endpoint, worker, or durable repository composes it. It therefore does not currently record VEX overrides, alert deduplications, policy suppressions, acknowledgments, or false-positive decisions from live workflows.
Domain Model
| Type | Purpose |
|---|---|
SuppressionCategory | Enum: VexOverride, AlertDedup, PolicyRule, OperatorAck, SeverityFilter, ComponentExclusion, FalsePositive |
FindingSeverity | Enum: None, Low, Medium, High, Critical |
NoiseLedgerEntry | Immutable record with digest, finding, category, severity, component, justification, suppressor, timestamps, expiry, evidence |
RecordSuppressionRequest | Request to log a suppression |
RecordSuppressionResult | Result with digest, dedup flag, entry |
NoiseLedgerQuery | Query filter: finding, category, severity, component, suppressor, tenant, active-only, limit |
SuppressionStatistics | Aggregated counts by category, severity, active/expired |
Deduplication
Content-addressed using SHA-256 of canonical form: tenantId + findingId + category + severity + componentRef + suppressedBy + justification. Tenant identity is normalized before hashing so identical decisions can deduplicate inside one tenant without crossing tenant boundaries.
Service Interface
INoiseLedgerService:
RecordAsync(RecordSuppressionRequest)→RecordSuppressionResult— idempotent recordGetByDigestAsync(string)→NoiseLedgerEntry?— lookup by digestQueryAsync(NoiseLedgerQuery)→ImmutableArray<NoiseLedgerEntry>— filtered queryGetStatisticsAsync(string? tenantId)→SuppressionStatistics— aggregated stats
DI Registration
AddProofChainServices deliberately does not register INoiseLedgerService. Tests construct NoiseLedgerService directly; a future production host must provide an explicit registration together with a durable tenant-scoped repository and a real ingestion/API owner.
OTel Metrics
Meter: StellaOps.Attestor.ProofChain.Audit.NoiseLedger
| Counter | Description |
|---|---|
noise.suppressions.recorded | New suppression entries |
noise.suppressions.deduplicated | Deduplicated entries |
noise.queries.executed | Query operations |
noise.statistics.computed | Statistics computations |
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Audit/NoiseLedgerModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Audit/INoiseLedgerService.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Audit/NoiseLedgerService.cs
Test Coverage (37 tests)
- Basic recording (digest, timestamp, all fields, evidence, correlation)
- Deduplication (same request, different finding/category, cross-tenant isolation)
- Validation (null, empty findingId/componentRef/justification/suppressedBy, cancellation)
- GetByDigest (found, not found, null)
- Query by findingId, category, severity, componentRef, active-only
- No-filter returns all, limit enforcement
- Statistics: empty, by category, by severity, active/expired tracking
- IsExpired model method (expired, no expiration)
- Constructor validation
- Determinism (same inputs → same digest)
PostgreSQL Persistence Layer — Schema Isolation, RLS, Temporal Tables
Sprint: SPRINT_20260208_018_Attestor_postgresql_persistence_layer
Purpose
Manages per-module PostgreSQL schema isolation, Row-Level Security (RLS) policy scaffolding, and temporal table configuration for Attestor persistence modules. Generates SQL statements for schema provisioning, tenant isolation, and history tracking without modifying existing ProofChainDbContext or entity classes.
Production boundary (verified 2026-07-18). The Attestor host does apply its embedded migrations at startup, and the durable Rekor and bulk-verification queues use PostgreSQL
FOR UPDATE SKIP LOCKED. However, no active migration enables or forces RLS or creates an RLS policy.SchemaIsolationServiceonly returns SQL strings; it has no production executor or tenant-session setter, and its static registry is not the migration authority. Treat the registry below as prototype metadata, not as an applied database contract.
Schema Registry
Five schema assignments covering all Attestor persistence modules:
| Schema | PostgreSQL Name | Tables |
|---|---|---|
| ProofChain | proofchain | sbom_entries, dsse_envelopes, spines, trust_anchors, rekor_entries |
| Attestor | attestor | rekor_submission_queue, submission_state, entries, local_transparency_entries, external_transparency_targets, external_transparency_mirror_receipts, transparency_exchange_batches |
| Verdict | verdict | verdict_ledger, verdict_policies |
| Watchlist | watchlist | watched_identities, identity_alerts, alert_dedup |
| Audit | audit | noise_ledger, hash_audit_log, suppression_stats |
RLS Policy Coverage
Tenant isolation policies are defined for schemas that contain tenant-scoped data:
- Verdict: verdict_ledger, verdict_policies
- Watchlist: watched_identities, identity_alerts
- Attestor: rekor_submission_queue, external_transparency_targets, external_transparency_mirror_receipts, transparency_exchange_batches
- Audit: noise_ledger
- ProofChain: No RLS (shared read-only reference data)
Generated policies use a tenant_id column with the current_setting('app.tenant_id')::uuid expression. These policies are not present in the active migration stream.
Temporal Table Configuration
Three tables configured for system-versioned history tracking:
| Table | History Table | Retention |
|---|---|---|
| verdict.verdict_ledger | verdict.verdict_ledger_history | 7 years |
| watchlist.watched_identities | watchlist.watched_identities_history | 1 year |
| audit.noise_ledger | audit.noise_ledger_history | 7 years |
Generated temporal-table SQL uses PostgreSQL trigger-based versioning with valid_from/valid_to period columns. It is not executed by the production host.
SQL Generation (Not Execution)
The service generates SQL statements for operators to review and execute:
- Provisioning:
CREATE SCHEMA IF NOT EXISTS,GRANT USAGE, default privileges, documentation comments - RLS:
ENABLE ROW LEVEL SECURITY,FORCE ROW LEVEL SECURITY,CREATE POLICYwith tenant isolation - Temporal: Period column addition, history table creation, trigger functions, trigger attachment
DI Registration
PersistenceServiceCollectionExtensions.AddAttestorPersistence() registers ISchemaIsolationService as a singleton with TimeProvider and IMeterFactory.
OTel Metrics
Meter: StellaOps.Attestor.Persistence.SchemaIsolation
| Counter | Description |
|---|---|
schema.provisioning.operations | Schema provisioning SQL generations |
schema.rls.operations | RLS policy SQL generations |
schema.temporal.operations | Temporal table SQL generations |
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.Persistence/SchemaIsolationModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.Persistence/ISchemaIsolationService.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.Persistence/SchemaIsolationService.cs - DI:
src/Attestor/__Libraries/StellaOps.Attestor.Persistence/PersistenceServiceCollectionExtensions.cs
Prototype Test Coverage (54 tests verified 2026-07-18)
- GetAssignment per schema (5 schemas, correct names, table counts)
- Invalid schema throws ArgumentException
- GetAllAssignments returns all five, all have tables
- Provisioning SQL: CREATE SCHEMA, GRANT, default privileges, comment, timestamp, statement count
- RLS policies per schema (Verdict has policies, ProofChain empty, all have tenant_id, UsingExpression)
- RLS SQL: ENABLE/FORCE/CREATE POLICY, permissive mode, empty for ProofChain, multiple for Watchlist
- Temporal tables: count, retention values per table, history table names
- Temporal SQL: period columns, history table, trigger function, trigger, retention comment, statement count
- GetSummary: complete data, ProvisionedCount, RlsEnabledCount, timestamp
- Constructor validation (null TimeProvider fallback, null MeterFactory throws)
- Cross-schema consistency (RLS references valid schemas, temporal references valid schemas)
- Determinism (provisioning, RLS, temporal SQL produce identical output)
S3/MinIO/GCS Object Storage for Tiles
Sprint: SPRINT_20260208_019_Attestor_s3_minio_gcs_object_storage_for_tiles
Purpose
Provides a pluggable object storage abstraction for the Content-Addressed Store (CAS), enabling durable blob storage via S3-compatible backends (AWS S3, MinIO, Wasabi), Google Cloud Storage, or local filesystem. The existing InMemoryContentAddressedStore is complemented by ObjectStorageContentAddressedStore which delegates to an IObjectStorageProvider for persistence.
Architecture
IContentAddressedStore (existing interface)
├── InMemoryContentAddressedStore (existing, for tests)
└── ObjectStorageContentAddressedStore (new, durable)
└── delegates to IObjectStorageProvider
├── FileSystemObjectStorageProvider (offline/air-gap)
├── S3-compatible (AWS/MinIO/Wasabi) — future
└── GCS — future
Provider Interface
IObjectStorageProvider defines five low-level operations:
PutAsync— Store a blob by key, idempotent with write-once supportGetAsync— Retrieve blob content and metadata by keyExistsAsync— Check blob existenceDeleteAsync— Remove a blob (blocked in WORM mode)ListAsync— List blobs with prefix filtering and pagination
Storage Layout
Content blobs: blobs/sha256:<hex> — raw content Metadata sidecars: meta/sha256:<hex>.json — JSON with artifact type, tags, timestamps
Configuration
ObjectStorageConfig selects the backend and connection details:
| Property | Description |
|---|---|
Provider | FileSystem, S3Compatible, or Gcs |
RootPath | Root directory (FileSystem only) |
BucketName | S3/GCS bucket name |
EndpointUrl | Custom endpoint (MinIO, localstack) |
Region | AWS/GCS region |
Prefix | Key prefix for namespace isolation |
EnforceWriteOnce | WORM mode (prevents deletes and overwrites) |
FileSystem Provider
- Atomic writes via temp file + rename
- Metadata stored as
.metasidecar files - WORM enforcement: skips overwrite, blocks delete
- Offset-based pagination for listing
DI Registration
IObjectStorageProvider -> FileSystemObjectStorageProvider is registered via TryAddSingleton in ProofChainServiceCollectionExtensions. IContentAddressedStore resolves to ObjectStorageContentAddressedStore, not the in-memory store. Non-filesystem provider values fail clearly until a corresponding runtime provider is registered or implemented.
OTel Metrics
Meter: StellaOps.Attestor.ProofChain.Cas.FileSystem
| Counter | Description |
|---|---|
objectstorage.fs.puts | Filesystem put operations |
objectstorage.fs.gets | Filesystem get operations |
objectstorage.fs.deletes | Filesystem delete operations |
Meter: StellaOps.Attestor.ProofChain.Cas.ObjectStorage
| Counter | Description |
|---|---|
cas.objectstorage.puts | CAS put via object storage |
cas.objectstorage.deduplications | Deduplicated puts |
cas.objectstorage.gets | CAS get via object storage |
cas.objectstorage.deletes | CAS delete via object storage |
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/ObjectStorageModels.cs - Provider interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/IObjectStorageProvider.cs - Filesystem provider:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/FileSystemObjectStorageProvider.cs - CAS bridge:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/ObjectStorageContentAddressedStore.cs
Test Coverage (42 tests)
ObjectStorageContentAddressedStore (27 tests):
- Put: store, dedup, null/empty-media-type throws, tags, related digests, timestamp
- Get: retrieves, missing returns null, null/empty throws
- Exists: true for stored, false for missing
- Delete: removes, false for missing
- List: returns all, filters by type, respects limit
- Statistics: accurate counts, dedup tracking
- Constructor validation (null provider/meterFactory, null timeProvider fallback)
- Determinism: same content → same digest
FileSystemObjectStorageProvider (13 tests):
- Put: store and retrieve, write-once enforcement
- Exists: true/false
- Delete: removes, false for missing, blocked in WORM mode
- List: returns stored, empty directory
- Metadata preservation
- Constructor validation (null config, empty root, null meterFactory)
ObjectStorageModels (5 tests):
- Default values for config, put request, get result, list query
- Provider kind enum count
- Determinism (provisioning, RLS, temporal SQL produce identical output)
Score Replay and Verification
Prototype library subsystem — not wired into the live service. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”). This is the ProofChain
IScoreReplayService, distinct from the live Scanner score-replay surface.
Sprint: SPRINT_20260208_020_Attestor_score_replay_and_verification
Purpose
Enables deterministic replay of verdict scores by re-executing scoring computations with captured inputs, comparing original and replayed scores to quantify divergence, and producing DSSE-ready attestations with payload type application/vnd.stella.score+json.
Architecture
The score replay service sits alongside the existing AI artifact replay infrastructure in ProofChain/Replay/ and provides:
- Score Replay — Re-executes deterministic scoring from captured inputs (policy weights, coverage data, severity), computing a replayed score and determinism hash
- Score Comparison — Compares two replay results, quantifying divergence and identifying specific differences (score, hash, status)
- DSSE Attestation — Produces JSON-encoded attestation payloads ready for DSSE signing with
application/vnd.stella.score+jsonpayload type
Deterministic Scoring
- Inputs sorted by key (ordinal) for canonical ordering
- Weighted average of numeric values, normalized to [0, 1]
- Weight inputs identified by key containing “weight”
- Non-numeric inputs silently ignored
- Determinism hash computed from canonical key=value\n format
Models
| Type | Description |
|---|---|
ScoreReplayRequest | Replay request with verdict ID, original score, scoring inputs |
ScoreReplayResult | Result with replay digest, status, replayed/original scores, divergence, determinism hash |
ScoreReplayStatus | Matched, Diverged, FailedMissingInputs, FailedError |
ScoreComparisonRequest | Request to compare two replays by digest |
ScoreComparisonResult | Comparison with divergence, determinism flag, difference details |
ScoreReplayAttestation | DSSE-ready attestation with JSON payload and signing key slot |
ScoreReplayQuery | Query with verdict ID, tenant, status, limit filters |
DI Registration
IScoreReplayService is deliberately not registered by ProofChainServiceCollectionExtensions; it has zero production consumers and keeps replay/history state only in process memory. The live score-replay API is the unrelated Scanner-owned service described in docs/modules/scanner/architecture.md.
OTel Metrics
Meter: StellaOps.Attestor.ProofChain.Replay.Score
| Counter | Description |
|---|---|
score.replays.executed | Total replay executions |
score.replays.matched | Replays matching original score |
score.replays.diverged | Replays diverging from original |
score.comparisons.executed | Comparison operations |
score.attestations.created | Attestation productions |
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Replay/ScoreReplayModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Replay/IScoreReplayService.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Replay/ScoreReplayService.cs
Prototype Test Coverage (36 retained targeted tests)
- ReplayAsync: produces digest, matched/diverged status, duration, determinism hash match/mismatch, null original hash, empty inputs, validation (null request, empty verdictId, cancellation)
- CompareAsync: identical results deterministic, divergent reports differences, null validation
- CreateAttestationAsync: payload type, valid JSON, null signing key, null validation
- GetByDigestAsync: stored result, missing returns null, null throws
- QueryAsync: no filter, verdict ID filter, status filter, limit enforcement, null throws
- ComputeScore: empty inputs, non-numeric ignored, deterministic, clamped [0,1]
- ComputeDeterminismHash: same inputs same hash, different inputs different hash
- Constructor validation (null meterFactory throws, null timeProvider fallback)
VEX Receipt Sidebar
Prototype library subsystem — not wired into the live service. See the status banner above (before “Unknowns Five-Dimensional Triage Scoring”).
Converts VerificationReceipt domain objects into sidebar-ready DTOs for the UI, providing a formatted view of DSSE signature verification, Rekor inclusion proofs, and per-check results.
Architecture
- FormatReceipt — Converts
VerificationReceipt→ReceiptSidebarDetail: mapsProofBundleId.Digest→ string,TrustAnchorId.Value→ string, iterates checks to buildReceiptCheckDetaillist, derives overallReceiptVerificationStatusfrom pass/fail counts, setsDsseVerifiedandRekorInclusionVerifiedby scanning check names for DSSE/Rekor keywords - GetDetailAsync — Looks up registered receipt by bundle ID, returns
ReceiptSidebarDetailwith optional check and tool digest exclusion - GetContextAsync — Returns
VexReceiptSidebarContextcombining receipt detail with VEX decision, justification, evidence refs, and finding metadata; falls back to receipt-only context when no explicit context is registered
Verification Status Derivation
| Condition | Status |
|---|---|
| No checks present | Unverified |
| All checks pass | Verified |
| Some pass, some fail | PartiallyVerified |
| All checks fail | Failed |
Models
| Type | Description |
|---|---|
ReceiptVerificationStatus | Verified, PartiallyVerified, Unverified, Failed |
ReceiptCheckDetail | Single check formatted for sidebar (Name, Passed, KeyId?, LogIndex?, Detail?) |
ReceiptSidebarDetail | Full receipt DTO with computed TotalChecks/PassedChecks/FailedChecks, DsseVerified, RekorInclusionVerified |
VexReceiptSidebarContext | Receipt + Decision + Justification + EvidenceRefs + finding metadata |
ReceiptSidebarRequest | Query by BundleId with IncludeChecks/IncludeToolDigests flags |
DI Registration
IReceiptSidebarService is deliberately not registered by ProofChainServiceCollectionExtensions. It has no production caller and its in-memory receipt index is a tested prototype, not a restart-safe provenance API. Any future host composition must add an explicit durable store and a tenant-aware API/export contract rather than advertising the process-local implementation.
OTel Metrics
Meter: StellaOps.Attestor.ProofChain.Receipts.Sidebar
| Counter | Description |
|---|---|
sidebar.detail.total | Sidebar detail requests |
sidebar.context.total | Sidebar context requests |
sidebar.format.total | Receipts formatted for sidebar |
Source Files
- Models:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/ReceiptSidebarModels.cs - Interface:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/IReceiptSidebarService.cs - Implementation:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/ReceiptSidebarService.cs
Test Coverage (29 retained targeted tests)
- ReceiptVerificationStatus: 4 enum values
- ReceiptCheckDetail: property roundtrips, optional defaults
- ReceiptSidebarDetail: computed check counts, empty checks
- VexReceiptSidebarContext: defaults, full roundtrip
- ReceiptSidebarRequest: defaults
- FormatReceipt: bundle/anchor/version mapping, all-pass/mixed/all-fail/no-checks status, DSSE verified/not-verified, Rekor verified/absent, check detail mapping, expected/actual formatting, tool digests mapping, null tool digests, null throws
- GetDetailAsync: unknown returns null, registered returns detail, exclude checks, exclude tool digests, null throws
- GetContextAsync: unknown returns null, registered context, fallback receipt-only, null/empty/whitespace throws
- DeriveVerificationStatus: single pass, single fail
- Register: null throws
- RegisterContext: null/empty/whitespace bundleId throws
Advisory Commitments (2026-02-26 Batch)
SPRINT_20260226_225_Attestor_signature_trust_and_verdict_api_hardeninggoverns:- DSSE signature verifier trust behavior (including deterministic failure reasons).
- authority roster validation for verdict creation.
- authenticated tenant context enforcement over header-only spoofable inputs.
- deterministic verdict retrieval APIs for hash-based lookup.
Rekor/tile verification commitments from
Deterministic tile verification with Rekor v2are coordinated with Symbols sprintSPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity.
Trust Domain Model (Sprint 204 – 2026-03-04)
Overview
As of Sprint 204, the Attestor module directory (src/Attestor/) is the trust domain owner for three runtime services and their supporting libraries:
- Attestor – transparency log submission, inclusion proof verification, evidence caching
- Signer – DSSE envelope creation, cryptographic signing (keyless/keyful/HSM), entitlement enforcement
- Provenance – SLSA/DSSE attestation generation, Merkle tree construction, verification tooling
Source consolidation places all trust-domain code under a single directory for ownership clarity, while preserving runtime service identities and security boundaries.
Trust Data Classification
| Data Category | Owner Service | Storage | Sensitivity |
|---|---|---|---|
| Attestation evidence (proofchain, inclusion proofs, Rekor entries) | Attestor | attestor PostgreSQL schema | High – tamper-evident, integrity-critical |
| Provenance evidence (SLSA predicates, build attestations, Merkle trees) | Provenance (library) | Consumed by Attestor/EvidenceLocker | High – deterministic, reproducible |
| Signer metadata (audit events, signing ceremony state, rate limits) | Signer | signer PostgreSQL schema | High – operational security |
| Signer key material (KMS/HSM refs, Fulcio certs, trust anchors, rotation state) | Signer (KeyManagement) | signer PostgreSQL schema | Critical – cryptographic trust root |
PostgreSQL Schema Ownership
Each trust-domain service retains its own DbContext and dedicated PostgreSQL schema:
attestorschema – Owned by the Attestor service. Live runtime tables (per the embedded migrations) areentries,verdict_ledger,rekor_submission_queue,identity_watchlist,identity_alert_dedup,local_transparency_entries,external_transparency_targets,external_transparency_mirror_receipts, andtransparency_exchange_batches;proofchain.graph_nodesandproofchain.graph_edgesare the tenant-partitioned Attestor query projection. There is noattestor.dedupetable — bundle dedupe is Redis/Valkey-backed (see §2/§7) — and there is noattestor.audittable — proof-chain operation auditing flows through the shared endpoint-level.Audited()filter into the Timeline unified audit sink (proofchain.audit_logis created at001_v1_attestor_baseline.sql:244and dropped at:776in the same baseline). Live service composition wiresIAttestorEntryRepositorythroughAddAttestorRuntimePersistence(...), which resolves toPostgresAttestorEntryRepository; the same save transaction updates the proof projection. In-memory entry and graph repositories are test/dev-only explicit registrations and are not provided by the production composition. The embedded verdict table is a separate release-verdict store; its repository/service is not registered by that runtime-persistence extension.signerschema – Owned by the Signer trust-domain runtime. Contains ceremony state/audit plus key-management tables (trust_anchors,key_history,key_audit_log) that the live Signer host auto-migrates on startup.- KeyManagement library boundary – Key rotation, trust anchor management, and HSM/KMS binding logic remain isolated in the
StellaOps.Signer.KeyManagementlibrary, but the canonical runtime tables now live inside thesignerschema rather than a separatekey_managementschema.
There is no cross-service schema merge. Attestor evidence state remains isolated from Signer runtime state, and the Signer host now requires a real KeyManagement connection string outside Development and Testing. Live Signer startup validates its runtime bindings and rejects EF InMemory key-management storage, in-memory ceremony persistence/audit, stub bearer auth, in-memory PoE/quota/audit sinks, and local HMAC DSSE signing outside local harness environments. The live composition root resolves PostgreSQL ceremony services, Authority resource-server auth, configured PoE introspection, stateless quota checks, structured audit emission, and crypto-backed DSSE signing.
Security Boundary: No-Merge Decision (ADR)
Decision: Signer key-material isolation from attestation evidence is a deliberate security boundary. The schemas will NOT be merged into a unified DbContext.
Rationale:
- A merged DbContext would require a single connection string with access to both key material (signing keys, HSM/KMS bindings, trust anchors) and evidence stores (proofchain entries, Rekor logs).
- This widens the blast radius of any credential compromise: an attacker gaining the Attestor database credential would also gain access to key rotation state and trust anchor configurations.
- Schema isolation is a defense-in-depth measure. Each service authenticates to PostgreSQL independently, with schema-level
GRANTrestrictions. - The Signer’s KeyManagement database contains material that, if compromised, could allow forging of signatures. This material must be isolated from the higher-volume, lower-privilege evidence store.
Implications:
- No shared EF Core DbContext across trust services.
- Each service manages its own migrations independently (
src/Attestor/__Libraries/StellaOps.Attestor.Persistence/for Attestor;src/Attestor/__Libraries/StellaOps.Signer.KeyManagement/for Signer key management). - Cross-service queries (e.g., “find the signing identity for a given attestation entry”) use API calls, not database joins.
Source Layout (post-Sprint 204)
src/Attestor/
StellaOps.Attestation/ # DSSE envelope model library
StellaOps.Attestation.Tests/
StellaOps.Attestor/ # Attestor service (Core, Infrastructure, WebService, Tests)
StellaOps.Attestor.Envelope/ # Envelope serialization
StellaOps.Attestor.TileProxy/ # Rekor tile proxy
StellaOps.Attestor.Types/ # Shared predicate types
StellaOps.Attestor.Verify/ # Verification pipeline
StellaOps.Signer/ # Signer service (Core, Infrastructure, WebService, Tests)
StellaOps.Provenance.Attestation/ # Provenance attestation library
StellaOps.Provenance.Attestation.Tool/ # Forensic verification CLI tool
__Libraries/
StellaOps.Attestor.*/ # Attestor domain libraries
StellaOps.Signer.KeyManagement/ # Key rotation and trust anchor management
StellaOps.Signer.Keyless/ # Keyless (Fulcio/Sigstore) signing support
__Tests/
StellaOps.Attestor.*/ # Attestor test projects
StellaOps.Provenance.Attestation.Tests/ # Provenance test project
What Did NOT Change
- Namespaces – All
StellaOps.Signer.*andStellaOps.Provenance.*namespaces are preserved. - Runtime service identities – Docker image names (
stellaops/signer), container names, network aliases, and API base paths (/api/v1/signer/) are unchanged. - Database schemas – No schema changes, no migrations, no data movement.
- API contracts – All endpoints including
/api/v1/signer/sign/dsseremain stable.
