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-attached stellaops:tenant claim. The X-Tenant-Id header 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 403 tenant_claim_mismatch. When neither is present, HTTP 401 tenant_claim_missing. See SPRINT_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.

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:

RuntimeImage / containerRole
Attestorstellaops/attestorstellaops-attestorThe stateless service described by this dossier (submission, proof retrieval, verification, verdicts). Behind mTLS.
Attestor TileProxystellaops/attestor-tileproxystellaops-attestor-tileproxyA 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:

Trust boundary: Only the Signer is allowed to call submission endpoints; enforced by mTLS peer cert allowlist + aud=attestor OpTok.


Roles, identities & scopes

Supported predicate types

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.*@1 names above are the headline submission predicates with JSON Schemas + golden samples under src/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 by 001_v1_attestor_baseline.sql:450 — folded in from the pre-1.0 002_add_predicate_type_registry.sql, which is archived and not embedded) covering ~20+ additional URIs across the stella-core and stella-proof categories — 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. The StellaOps.Attestor.Types schemas 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, with category / isActive / offset / limit filters) and GET /api/v1/attestor/predicates/{uri} (URL-encoded URI) return registry entries; both require attestor:read (PredicateRegistryEndpoints).

Golden fixtures: Deterministic JSON statements for the headline predicates live in src/Attestor/StellaOps.Attestor.Types/samples. They are kept stable by the StellaOps.Attestor.Types.Tests project 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 StatementBuilder exposes eight typed methods and has no production registration/caller, while its IProofChainSigner remains deliberately uncomposed without a production key store. Production DSSE construction/signing is owned by Signer, which selects Statement v1 for SLSA v1 and stella.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 StatementBuilder remains 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. SlsaSchemaValidator now 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, IAIArtifactReplayer has 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 separate StellaOps.AirGap.Bundle library; that library is itself unhosted, so it is not evidence for a live Attestor replay/proof-spine pipeline.

Envelope & signature model

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

  1. Fetch envelope (from request, cache, or storage) and validate DSSE structure.
  2. Verify signature(s) against configured trust roots; evaluate issuer policy.
  3. Retrieve or acquire inclusion proof from Rekor (primary + optional mirror).
  4. Validate Merkle proof against checkpoint; optionally verify witness endorsement.
  5. Return cached verification bundle including policy verdict and timestamps.

Local DSSE trust-root verifier (2026-04-28)

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)

Unsupported Trust Endpoint Fail-Closed Contract (2026-04-28)

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:

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:

  1. Parse checkpoint body (origin, tree size, root hash)
  2. Verify checkpoint signature against Rekor public key
  3. Compute leaf hash from canonicalized entry
  4. Walk Merkle path from leaf to root using RFC 6962 interior node hashing
  5. Compare computed root with checkpoint root hash (constant-time)

Offline Mode:

Offline checkpoint requirement (2026-04-28):

Metrics:

UI & CLI touchpoints

Performance & observability targets


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

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 under Migrations/_archived/pre_1.0/ and are not embedded — cite the baseline, not those names.

Indexes (live):

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 TypeFormatSourceExample
ArtifactIDsha256:<64-hex>Container manifest or binary hashsha256:a1b2c3d4e5f6...
SBOMEntryID<sbomDigest>:<purl>[@<version>]SBOM hash + component PURLsha256:91f2ab3c:pkg:npm/lodash@4.17.21
EvidenceIDsha256:<hash>Canonical evidence JSONsha256:e7f8a9b0c1d2...
ReasoningIDsha256:<hash>Canonical reasoning JSONsha256:f0e1d2c3b4a5...
VEXVerdictIDsha256:<hash>Canonical VEX verdict JSONsha256:d4c5b6a7e8f9...
ProofBundleIDsha256:<merkle_root>Merkle root of bundle componentssha256:1a2b3c4d5e6f...
GraphRevisionIDgrv_sha256:<hash>Merkle root of graph stategrv_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:

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:

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

PredicateType URIPurposeSigner Role
Evidenceevidence.stella/v1Raw evidence from scanner/ingestor (findings, reachability data)Scanner/Ingestor key
Reasoningreasoning.stella/v1Policy evaluation trace with inputs and intermediate findingsPolicy/Authority key
VEX Verdictcdx-vex.stella/v1VEX verdict with status, justification, and provenanceVEXer/Vendor key
Proof Spineproofspine.stella/v1Merkle-aggregated proof spine linking evidence to verdictAuthority key
Verdict Receiptverdict.stella/v1Final surfaced decision receipt with policy rule referenceAuthority key
SBOM Linkagehttps://stella-ops.org/predicates/sbom-linkage/v1SBOM-to-component linkage metadataGenerator key
Signed Exceptionhttps://stellaops.io/attestation/v1/signed-exceptionDSSE-signed budget exception with recheck policyAuthority key

Evidence Statement (evidence.stella/v1)

Captures raw evidence collected from scanners or vulnerability feeds.

FieldTypeDescription
sourcestringScanner or feed name that produced this evidence
sourceVersionstringVersion of the source tool
collectionTimeDateTimeOffsetUTC timestamp when evidence was collected
sbomEntryIdstringReference to the SBOM entry this evidence relates to
vulnerabilityIdstring?CVE or vulnerability identifier if applicable
rawFindingobjectPointer to or inline representation of raw finding data
evidenceIdstringContent-addressed ID (sha256:<hash>)

Reasoning Statement (reasoning.stella/v1)

Captures policy evaluation traces linking evidence to decisions.

FieldTypeDescription
sbomEntryIdstringSBOM entry this reasoning applies to
evidenceIdsstring[]Evidence IDs considered in this reasoning
policyVersionstringVersion of the policy used for evaluation
inputsobjectInputs to the reasoning process (evaluation time, thresholds, lattice rules)
intermediateFindingsobject?Intermediate findings from the evaluation
reasoningIdstringContent-addressed ID (sha256:<hash>)

VEX Verdict Statement (cdx-vex.stella/v1)

Captures VEX status determinations with provenance.

FieldTypeDescription
sbomEntryIdstringSBOM entry this verdict applies to
vulnerabilityIdstringCVE, GHSA, or other vulnerability identifier
statusstringVEX status: not_affected, affected, fixed, under_investigation
justificationstringJustification for the VEX status
policyVersionstringVersion of the policy used
reasoningIdstringReference to the reasoning that led to this verdict
vexVerdictIdstringContent-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.

FieldTypeDescription
sbomEntryIdstringSBOM entry this proof spine covers
evidenceIdsstring[]Sorted list of evidence IDs included in this proof bundle
reasoningIdstringReasoning ID linking evidence to verdict
vexVerdictIdstringVEX verdict ID for this entry
policyVersionstringVersion of the policy used
proofBundleIdstringContent-addressed ID (sha256:<merkle_root>)

Verdict Receipt Statement (verdict.stella/v1)

Final surfaced decision receipt with full provenance.

FieldTypeDescription
graphRevisionIdstringGraph revision ID this verdict was computed from
findingKeyobjectFinding key (sbomEntryId + vulnerabilityId)
ruleobjectPolicy rule that produced this verdict
decisionobjectDecision made by the rule
inputsobjectInputs used to compute this verdict
outputsobjectOutputs/references from this verdict
createdAtDateTimeOffsetUTC timestamp when verdict was created

SBOM Linkage Statement (sbom-linkage/v1)

SBOM-to-component linkage metadata.

FieldTypeDescription
sbomobjectSBOM descriptor (id, format, specVersion, mediaType, sha256, location)
generatorobjectGenerator tool descriptor
generatedAtDateTimeOffsetUTC timestamp when linkage was generated
incompleteSubjectsobject[]?Subjects that could not be fully resolved
tagsobject?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.

FieldTypeDescription
schemaVersionstringSchema version (current: “1.0”)
exceptionobjectThe wrapped BudgetExceptionEntry
exceptionContentIdstringContent-addressed ID (sha256:<hash>) for deduplication
signedAtDateTimeOffsetUTC timestamp when the exception was signed
recheckPolicyobjectRecheck policy configuration
environmentsstring[]?Environments this exception applies to (dev, staging, prod)
coveredViolationIdsstring[]?IDs of violations this exception covers
approvalPolicyDigeststring?Digest of the policy bundle that approved this exception
renewsExceptionIdstring?Previous exception ID for renewal chains
statusstringStatus: Active, PendingRecheck, Expired, Revoked, PendingApproval
Recheck Policy Schema
FieldTypeDescription
recheckIntervalDaysintInterval in days between rechecks (default: 30)
autoRecheckEnabledboolWhether automatic recheck scheduling is enabled
maxRenewalCountint?Maximum renewals before escalated approval required
renewalCountintCurrent renewal count
nextRecheckAtDateTimeOffset?Next scheduled recheck timestamp
lastRecheckAtDateTimeOffset?Last completed recheck timestamp
requiresReapprovalOnExpiryboolWhether re-approval is required after expiry
approvalRolesstring[]?Roles required for approval
Exception Signing API

The exception signing service provides endpoints for signing, verifying, and renewing exceptions:

EndpointMethodDescription
/internal/api/v1/exceptions/signPOSTSign an exception and wrap in DSSE envelope
/internal/api/v1/exceptions/verifyPOSTVerify a signed exception envelope
/internal/api/v1/exceptions/recheck-statusPOSTCheck if exception requires recheck
/internal/api/v1/exceptions/renewPOSTRenew 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"), and RequireClientCertificate is opt-in (AttestorWebServiceComposition.cs:411). The enforced boundary in the reference deployment is the attestor:write policy, which is scope (attest:create) OR trusted-network service bypass with no RequireAuthenticatedUser (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:

  1. mTLS peer certificate maps to signer service (CA‑pinned) — when mTLS is configured; see caveat above.
  2. Authority OpTok with aud=attestor, scope=attestor.write, DPoP or mTLS bound.
  3. DSSE envelope is signed by the Signer’s key (or includes a Fulcio‑issued cert chain) and chains to configured roots (Fulcio/KMS).
  4. Predicate type is one of Stella Ops types (sbom/report/vex‑export) with valid schema.
  5. subject[*].digest.sha256 is 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 return 501 Not Implemented with code=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 / controllerRoutesStatusNotes
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/*ImplementedCore trust surface. /api/v1/attestations is a GET list only (no bare POST — the :sign/:export/:import/:export-build action routes are the writes). POST …/rekor/entries/proof-backfill (attestor:write, rate-limiter attestor-verifications, SER-5b) backfills stored inclusion proofs.
POST /api/v1/rekor/verify:bulk, GET /api/v1/rekor/verify:bulk/{jobId}bulk verifyImplemented, durableNon-testing runtime uses PostgresBulkVerificationJobStore plus BulkVerificationWorker; submit returns 202 Accepted. A custom/testing host with an unsupported store may still return 501.
WatchlistEndpoints (minimal API)…/api/v1/watchlist*ImplementedSee §18.
PredicateRegistryEndpoints (minimal API)predicate registry readsImplemented
ProofChainControllerGET /api/v1/proofs/{subjectDigest}, …/{subjectDigest}/chain, …/id/{proofId}, …/id/{proofId}/verifyImplemented, durableNon-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 hostThe controller has traversal/visualization logic, but no durable tenant-aware IChainQueryService composition. See §4.6.
BundlesController…/api/v1/bundles* (create/get/list/verify/get-attestation)Prototype — excluded from hostThe 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 hostDSSE-signed budget exceptions; see “Signed Exception” §2.1. The controller and IExceptionSigningService are deliberately not registered because IProofChainSigner needs an IProofChainKeyStore with 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-gatedLive 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 503 verdict_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 503 verdict_lookup_unavailable; only an EvidenceLocker 404 yields 404 verdict_not_found.
ProofsControllerPOST /proofs/{entry}/spine, GET /proofs/{entry}/{receipt,spine,vex}501 stubAlways returns 501 feature_not_implemented (even when ProofsEnabled=true). Forward spec only.
AnchorsControllerGET/POST/PATCH/DELETE /anchors*, …/revoke-key501 stubAlways 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 stubAlways returns 501. Distinct from the implemented POST /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 StellaOpsScopes constants attest:create (write) and attest: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:writeattest:create or configured service-to-service bypass network.
  • attestor:verifyattest:read or attest:create or bypass (verify is a read-only inclusion check, so it collapses to attest:read; write is a superset).
  • attestor:readattest:read or attest:create or bypass.

The legacy attestor.write/attestor.verify/attestor.read dot-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 granted attest: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.

4.2 Submission

POST /api/v1/rekor/entries (mTLS + OpTok required) — requires attestor:write; rate-limited via attestor-submissions; audited (AuditActions.Attestor.SubmitRekorEntry).

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.

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).

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:

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)

MethodPathScopeDescription
GET/api/v1/proofs/{subjectDigest}attestor:readTenant-scoped proof list for a strict SHA-256 subject digest.
GET/api/v1/proofs/{subjectDigest}/chain?maxDepth=attestor:readDeterministic tenant-scoped graph; depth clamps to 1-10, empty returns typed RFC 7807 404.
GET/api/v1/proofs/id/{proofId}attestor:readTenant-scoped durable entry detail.
GET/api/v1/proofs/id/{proofId}/verifyattestor:verifyTenant-scoped proof verification; tenant identity is carried through repository and verification cache lookups.

ChainController prototype (/api/v1/chains; excluded from the host)

MethodPathDescription
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)


6) Security model


7) Storage & archival


8) Observability & audit

Metrics (Prometheus):

SLO guardrails:

Correlation:

Tracing:

Audit:


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:


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

ConditionReturnDetails
mTLS/OpTok invalid401 invalid_tokenInclude WWW-Authenticate DPoP challenge when applicable
Bundle not signed by trusted identity403 chain_untrustedDSSE accepted only from Signer identities
Duplicate bundle409 duplicate_bundleReturn existing uuid (idempotent)
Rekor unreachable/timeout502 rekor_unavailableRetry with backoff; surface Retry-After
Inclusion proof timeout202 acceptedstatus=pending, background job continues to fetch proof
Archive unavailable/failure200 with archive warningEntry recorded; archive write records an error metric; no archive success is fabricated
Verification mismatch400 verify_failedInclude reason: chainleafHashrootMismatch

12) Performance & scale


13) Testing matrix


14) Implementation notes


15) Optional features


16) Observability (stub)


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 TypeConstantDescription
rekor.entry.loggedRekorEventTypes.EntryLoggedBundle successfully logged with inclusion proof
rekor.entry.queuedRekorEventTypes.EntryQueuedBundle queued for logging (async mode)
rekor.entry.inclusion_verifiedRekorEventTypes.InclusionVerifiedInclusion proof independently verified
rekor.entry.failedRekorEventTypes.EntryFailedLogging 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:

  1. Events are not emitted when Rekor is unreachable
  2. Bundles are queued locally for later submission
  3. Verification uses bundled checkpoints
  4. 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:

LevelContentsUse Case
AAttestation bundles onlyOnline verification still available
BEvidence + verification material (Fulcio roots, Rekor keys)Standard air-gap transfer
CFull state: policies, trust anchors, org keysFully disconnected deployment

Key Types:

Integrity:

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

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

ModeBehaviorExample PatternMatches
exactCase-insensitive equalityalice@example.comAlice@example.com
prefixStarts-with matchhttps://accounts.google.com/Any Google OIDC issuer
globGlob pattern (*, ?)*@example.comalice@example.com, bob@example.com
regexFull regex (with timeout)repo:org/(frontend|backend):.*repo:org/frontend:ref:main

Scope Hierarchy

ScopeVisibilityWho Can Create
tenantOwning tenant onlyOperators with trust:write
globalAll tenantsPlatform admins with trust:admin
systemAll 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

MethodPathDescription
POST/api/v1/watchlistCreate watchlist entry
GET/api/v1/watchlistList 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}/testTest pattern against sample identity
GET/api/v1/watchlist/alertsList 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

MetricDescription
attestor.watchlist.entries_scanned_totalEntries processed by monitor
attestor.watchlist.matches_total{severity}Pattern matches by severity
attestor.watchlist.alerts_emitted_totalAlerts sent to notification system
attestor.watchlist.alerts_suppressed_totalAlerts deduplicated
attestor.watchlist.scan_latency_secondsPer-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:


⚠ 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 the StellaOps.Attestor.ProofChain library but are not capabilities the running stellaops-attestor service 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 with new, exercise them — and the stateful ones are backed by an in-process ConcurrentDictionary that 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 Scanner StellaOps.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 (AddProofChainServices was pruned in CLO-1). The host currently registers IJsonCanonicalizer, the durable IContentAddressedStore/IObjectStorageProvider infrastructure, and ICryptoProfileResolver; IExceptionSigningService is 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

DimensionCodeRangeDescription
ProbabilityP[0,1]Likelihood of exploitability or relevance
ExposureE[0,1]Attack surface exposure (internal → internet-facing)
UncertaintyU[0,1]Confidence deficit (fully understood → unknown)
ConsequenceC[0,1]Impact severity (negligible → catastrophic)
Signal FreshnessS[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

BandThresholdAction
Hot≥ 0.70Immediate triage required
Warm≥ 0.40Scheduled review
Cold< 0.40Archive / low priority

Thresholds are configurable via TriageBandThresholds.

Key Types

OTel Metrics

MetricDescription
triage.scored.totalTotal unknowns scored
triage.band.hot.totalUnknowns classified as Hot
triage.band.warm.totalUnknowns classified as Warm
triage.band.cold.totalUnknowns 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

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

MetricDescription
findings.get.totalFindings retrieved by ID
findings.query.totalFinding queries executed
findings.upsert.totalFindings upserted
findings.resolve.totalProof resolution requests
findings.proofs.totalProof 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:

Library: StellaOps.Attestor.ProofChain Namespace: StellaOps.Attestor.ProofChain.FingerprintStore

Models

TypePurpose
BinaryFingerprintRecordStored fingerprint: ID, format, architecture, file SHA-256, Build-ID, section hashes, package PURL, golden-set flag, trust score, evidence digests, timestamps.
FingerprintRegistrationInput for RegisterAsync: format, architecture, file hash, section hashes, optional PURL/Build-ID/evidence.
FingerprintLookupResultMatch result: found flag, matched record, golden match, section similarity (0.0–1.0), matched/differing section lists.
TrustScoreBreakdownDecomposed score: golden bonus, Build-ID score, section coverage, evidence score, provenance score.
GoldenSetNamed golden set with count and timestamps.
FingerprintQueryFilters: format, architecture, PURL prefix, golden flag, golden set name, min trust score, limit/offset.

Service Interface (IBinaryFingerprintStore)

MethodDescription
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

FactorWeightRaw value
Golden-set membership0.301.0 if golden, 0.0 otherwise
Build-ID present0.201.0 if Build-ID exists, 0.0 otherwise
Section coverage0.25Ratio of key sections (.text, .rodata, .data, .bss) present
Evidence count0.15min(count/5, 1.0)
Package provenance0.101.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

MetricTypeDescription
fingerprint.store.registeredCounterFingerprints registered
fingerprint.store.lookupsCounterStore lookups performed
fingerprint.store.golden_addedCounterFingerprints added to golden sets
fingerprint.store.deletedCounterFingerprints deleted

Test Coverage

36 tests in StellaOps.Attestor.ProofChain.Tests/FingerprintStore/BinaryFingerprintStoreTests.cs:


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. S3Compatible and Gcs configuration 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

TypeDescription
SbomSoftware Bill of Materials
VexVEX (Vulnerability Exploitability Exchange) document
AttestationDSSE-signed attestation envelope
ProofBundleProof chain bundle
EvidencePackEvidence pack manifest
BinaryFingerprintBinary fingerprint record
OtherGeneric/other artifact

Models

TypePurpose
CasArtifactStored artifact metadata: digest, type, media type, size, tags, related digests, timestamps, dedup flag.
CasPutRequestInput: raw content bytes, artifact type, media type, optional tags and related digests.
CasPutResultOutput: stored artifact + dedup flag.
CasGetResultRetrieved artifact with content bytes.
CasQueryFilters: artifact type, media type, tag key/value, limit/offset.
CasStatisticsStore metrics: total artifacts, bytes, dedup count, type breakdown.

Service Interface (IContentAddressedStore)

MethodDescription
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:

  1. The existing artifact metadata is returned (no duplicate storage).
  2. CasPutResult.Deduplicated is set to true.
  3. 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

MetricTypeDescription
cas.putsCounterCAS put operations
cas.deduplicationsCounterDeduplicated puts
cas.getsCounterCAS get operations
cas.deletesCounterCAS 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:


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)

ProfileAlgorithm IDStandard
Ed25519ED25519RFC 8032
EcdsaP256ES256NIST FIPS 186-4
EcdsaP384ES384NIST FIPS 186-4
RsaPssPS256PKCS#1 v2.1
Gost2012_256GOST-R34.10-2012-256Russian Federation
Gost2012_512GOST-R34.10-2012-512Russian Federation
Sm2SM2Chinese GB/T 32918
Dilithium3DILITHIUM3NIST FIPS 204 (ML-DSA)
Falcon512FALCON512NIST PQC Round 3
EidasRsaSha256eIDAS-RSA-SHA256EU eIDAS + CAdES
EidasEcdsaSha256eIDAS-ECDSA-SHA256EU 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

RegionDefault AlgorithmRequirements
InternationalEd25519None
EuEidaseIDAS-RSA-SHA256Qualified timestamp (Article 42), CAdES-T minimum
UsFipsECDSA-P256HSM-backed keys
RuGostGOST-2012-256GOST algorithms only
CnSmSM2SM national standards only
PostQuantumDilithium3PQC finalist algorithms only

Service Interface (ICryptoProfileResolver)

MethodDescription
ResolveAsync(keyProfile)Resolve key profile using active region.
ResolveAsync(keyProfile, region)Resolve key profile with explicit region override.
ActiveRegionGet 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)

  1. SigningKeyProfile (role: Evidence/Reasoning/etc.) arrives at ICryptoProfileResolver
  2. Active CryptoSovereignRegion determines the CryptoSovereignPolicy
  3. Policy’s DefaultAlgorithm produces a CryptoProfileBinding
  4. Binding carries: algorithm ID, region, CAdES level, HSM/timestamp requirements
  5. 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:

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

LevelDescription
CadesBBasic Electronic Signature
CadesTWith Timestamp (Article 42 minimum)
CadesLTWith Long-Term validation data
CadesLTAWith 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

MetricTypeDescription
crypto_sovereign.resolvesCounterProfile resolution operations (tagged by region)
crypto_sovereign.timestamp_validationsCounterQualified 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:


FixChain Attestation Verification Contract

FixChain verification is fail closed:


DSSE Envelope Size Management (Guardrails, Chunking, Gateway Awareness)

Prototype library subsystem — not wired into the live service. IDsseEnvelopeSizeGuard has 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

ModeTriggerBehavior
FullEnvelopeSize ≤ soft limitEnvelope submitted to Rekor as-is
HashOnlySoft limit < size ≤ hard limit, hash-only enabledOnly SHA-256 payload digest submitted
ChunkedSoft limit < size ≤ hard limit, chunking enabledEnvelope split into chunks with manifest
RejectedSize > hard limit, or no fallback availableSubmission blocked

Size Policy (DsseEnvelopeSizePolicy)

PropertyDefaultDescription
SoftLimitBytes102,400 (100 KB)Threshold for hash-only/chunked fallback
HardLimitBytes1,048,576 (1 MB)Absolute rejection threshold
ChunkSizeBytes65,536 (64 KB)Maximum size per chunk
EnableHashOnlyFallbacktrueAllow hash-only submission for oversized envelopes
EnableChunkingfalseAllow chunked submission (takes priority over hash-only)
HashAlgorithm“SHA-256”Hash algorithm for digest computation

Service Interface (IDsseEnvelopeSizeGuard)

MethodDescription
ValidateAsync(DsseEnvelope)Validate a typed DSSE envelope against size policy
ValidateAsync(ReadOnlyMemory<byte>)Validate raw serialized envelope bytes
PolicyGet the active size policy

Chunk Manifest

When chunking is enabled and an envelope exceeds the soft limit, the guard produces an EnvelopeChunkManifest containing:

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

MetricTypeDescription
envelope_size.validationsCounterTotal envelope size validations
envelope_size.hash_only_fallbacksCounterHash-only fallback activations
envelope_size.chunkedCounterChunked submission activations
envelope_size.rejectionsCounterEnvelope rejections

Test Coverage

21 direct prototype tests in StellaOps.Attestor.ProofChain.Tests/Rekor/DsseEnvelopeSizeGuardTests.cs:

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:

FieldTypeDescription
SchemaVersionstringAlways “1.0.0”
GraphDigeststringDeterministic SHA-256 digest of sorted graph content
GraphCasUristring?Optional CAS URI for externalized graph storage
ScanIdstringIdentifier of the originating scan
ArtifactRefstringPackage URL or image reference of the scanned artifact
NodesImmutableArray<ReachMapNode>All nodes in the reachability graph
EdgesImmutableArray<ReachMapEdge>All edges (call relationships)
FindingsImmutableArray<ReachMapFinding>Vulnerability findings with reachability status
AggregatedWitnessIdsImmutableArray<string>Deduplicated witness IDs from findings + explicit additions
AnalysisReachMapAnalysisAnalyzer metadata (tool, version, confidence, completeness)
SummaryReachMapSummaryComputed statistics (counts of nodes, edges, entry points, sinks)

ReachMapNode

FieldTypeDescription
NodeIdstringUnique identifier for the node
QualifiedNamestringFully qualified name (e.g., class.method)
ModulestringModule or assembly containing the node
IsEntryPointboolWhether this node is a graph entry point
IsSinkboolWhether this node is a vulnerability sink
ReachabilityStatestringOne of the 8-state lattice values

ReachMapEdge

FieldTypeDescription
SourceNodeIdstringOrigin node of the call edge
TargetNodeIdstringDestination node of the call edge
CallTypestringEdge type (direct, virtual, reflection, etc.)
ConfidencedoubleEdge confidence score (0.0–1.0), default 1.0

ReachMapFinding

FieldTypeDescription
VulnIdstringVulnerability identifier
CveIdstring?Optional CVE identifier
Purlstring?Optional package URL
IsReachableboolWhether the vulnerability is reachable
ConfidenceScoredoubleReachability confidence (0.0–1.0)
SinkNodeIdsImmutableArray<string>Nodes where the vulnerability manifests
ReachableEntryPointIdsImmutableArray<string>Entry points that can reach sinks
WitnessIdstring?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:

  1. Nodes are sorted by NodeId, each contributing NodeId|QualifiedName|ReachabilityState
  2. Edges are sorted by SourceNodeId then TargetNodeId, each contributing Source→Target|CallType
  3. Findings are sorted by VulnId, each contributing VulnId|IsReachable|ConfidenceScore
  4. 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:

All witness IDs are deduplicated in the final predicate.

Schema Validation

The reach-map predicate type is registered in PredicateSchemaValidator:

Statement Integration

ReachMapStatement extends InTotoStatement with:

Source Files

Test Coverage (25 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. IEvidenceCoverageScorer has 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:

DimensionDefault WeightDescription
Reachability0.25Call graph analysis, micro-witnesses, reach-maps
BinaryAnalysis0.20Binary fingerprints, build-id verification, section hashes
SbomCompleteness0.25Component inventory, dependency resolution completeness
VexCoverage0.20Vulnerability status decisions (affected/not_affected/fixed)
Provenance0.10Build provenance, source attestation, supply chain evidence

Scoring Algorithm

  1. For each dimension, the scorer trims identifiers, removes blanks, and deduplicates them ordinally so evidence padding cannot inflate coverage
  2. Each identifier is checked against an evidence resolver (Func<string, bool>) — the same pattern used by AIAuthorityClassifier
  3. Dimension score = (resolvable count) / (total count), producing a 0.0–1.0 value
  4. Overall score = weighted average across all dimensions (normalized by total weight)
  5. Missing dimensions receive a score of 0.0

Coverage Levels (Badge Rendering)

LevelThresholdMeaning
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:

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

CounterDescription
coverage.evaluationsTotal coverage evaluations performed
coverage.gating.passEvaluations that met AI gating threshold
coverage.gating.failEvaluations that failed AI gating threshold

Source Files

Prototype Test Coverage (22 targeted tests)


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

FormatUse CaseOutput
MermaidBrowser-side rendering via Mermaid.jsgraph TD markup with class definitions
DotStatic/server-side rendering via Graphvizdigraph markup with color/shape attributes
JsonCustom frontend rendering (D3.js, Cytoscape.js)Structured {nodes, edges} JSON

Visualization Models

VisualizationNode

FieldTypeDescription
IdstringUnique node identifier
LabelstringFormatted display label (type + truncated digest)
TypestringNode type string for icon/color selection
ContentDigeststring?Full content digest for provenance verification
IsRootboolWhether this is the subgraph root
DepthintBFS depth from root (for layout layering)
MetadataImmutableDictionary?Optional key-value pairs for tooltips

VisualizationEdge

FieldTypeDescription
SourcestringSource node ID
TargetstringTarget node ID
LabelstringHuman-readable edge type label
TypestringEdge 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 TypeMermaid ShapeDOT 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

Test Coverage (25 retained targeted tests)


Field-Level Ownership Map for Receipts and Bundles

Prototype library subsystem — not wired into the live service. IFieldOwnershipValidator has 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

ModuleResponsibility
CoreFundamental identifiers, timestamps, versions, tool digests
SigningKey identifiers and signature-related fields
RekorTransparency log indices and inclusion proofs
VerificationTrust anchors, verification results, check details
SbomVexSBOM/VEX document references
ProvenanceProvenance and build attestation fields
PolicyPolicy evaluation results
ExternalFields populated by external integrations

Ownership Map Structure

The FieldOwnershipMap record contains:

Each FieldOwnershipEntry declares:

Default Receipt Ownership Map (14 entries)

Field PathOwnerRequired
proofBundleIdCoreYes
verifiedAtCoreYes
verifierVersionCoreYes
anchorIdVerificationYes
resultVerificationYes
checksVerificationYes
checks[].checkVerificationYes
checks[].statusVerificationYes
checks[].keyIdSigningNo
checks[].logIndexRekorNo
checks[].expectedVerificationNo
checks[].actualVerificationNo
checks[].detailsVerificationNo
toolDigestsCoreNo

Validation

ValidateReceiptOwnershipAsync checks a VerificationReceipt against the ownership map:

  1. Iterates top-level fields, recording population status
  2. Expands per-check fields for each VerificationCheck entry
  3. Counts missing required fields
  4. Returns FieldOwnershipValidationResult with computed properties:
    • IsValid — true when MissingRequiredCount == 0
    • TotalFields — total field population records
    • PopulatedCount — fields that have values
    • ValidCount — fields with valid ownership

DI Registration

IFieldOwnershipValidator -> FieldOwnershipValidator (TryAddSingleton)

Source Files

Test Coverage (24 tests)


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:

  1. SBOM Ingest — wraps CAS PutAsync with SBOM-specific metadata (media type, tags, artifact type) and returns a typed SbomEntryId
  2. Attestation Verify — stores attestation in CAS, performs verification checks, and caches results by content hash in a ConcurrentDictionary
  3. Idempotency Key Support — optional client-provided keys that map to content digests, enabling safe retries even when content bytes differ

Idempotency Guarantees

ScenarioBehavior
Same content, no keyCAS deduplicates by SHA-256 hash, returns Deduplicated = true
Same content, same keyReturns cached result via key lookup
Different content, same keyReturns original result mapped to the key
Same content, different keyBoth keys map to the same digest

Verification Checks

The baseline attestation verification performs three deterministic checks:

CheckDescription
content_presentContent is non-empty
digest_formatValid SHA-256 digest format (71 chars)
json_structureContent 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

TypeDescription
SbomIngestRequestContent, MediaType, Tags, optional IdempotencyKey
SbomIngestResultDigest, Deduplicated, Artifact, SbomEntryId
AttestationVerifyRequestContent, MediaType, optional IdempotencyKey
AttestationVerifyResultDigest, CacheHit, Verified, Summary, Checks, VerifiedAt
AttestationCheckResultCheck, Passed, Details
IdempotencyKeyEntryKey, 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

CounterDescription
idempotent.sbom.ingestsTotal SBOM ingest operations
idempotent.sbom.deduplicationsSBOM submissions that were deduplicated
idempotent.attest.verificationsTotal attestation verifications (non-cached)
idempotent.attest.cache_hitsAttestation verifications served from cache
idempotent.key.hitsIdempotency key lookups that found existing entries

Source Files

Test Coverage (30 tests)


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

FrameworkControlsDescription
NIS25EU Network and Information Security Directive 2
DORA5EU Digital Operational Resilience Act
ISO-270016ISO/IEC 27001 Information Security Management
EU CRA4EU Cyber Resilience Act

Evidence Artifact Types

TypeDescription
SbomSoftware Bill of Materials
VexStatementVulnerability Exploitability Exchange statement
SignedAttestationSigned attestation envelope
TransparencyLogEntryRekor transparency log entry
VerificationReceiptProof of verification
ProofBundleBundled evidence pack
ReachabilityAnalysisBinary fingerprint or reachability analysis
PolicyEvaluationPolicy evaluation result
ProvenanceAttestationBuild origin proof
IncidentReportIncident response documentation

Control Registry (20 controls)

NIS2 Controls

IDCategorySatisfied By
NIS2-Art21.2dSupply Chain SecuritySBOM, VEX, Provenance
NIS2-Art21.2eSupply Chain SecurityVEX, Reachability
NIS2-Art21.2aRisk ManagementPolicy, Attestation
NIS2-Art21.2gRisk ManagementReceipt, ProofBundle
NIS2-Art23Incident ManagementIncident, Transparency

DORA Controls

IDCategorySatisfied By
DORA-Art6.1ICT Risk ManagementPolicy, Attestation
DORA-Art9.1ICT Risk ManagementAttestation, Receipt, ProofBundle
DORA-Art17Incident ClassificationIncident, VEX
DORA-Art28Third-Party RiskSBOM, Provenance, Reachability
DORA-Art11ICT Risk Management (optional)ProofBundle, Transparency

ISO-27001 Controls

IDCategorySatisfied By
A.8.28Application SecuritySBOM, Reachability, Provenance
A.8.9Configuration ManagementPolicy, Attestation
A.8.8Vulnerability ManagementVEX, Reachability, SBOM
A.5.23Cloud Security (optional)Provenance, ProofBundle
A.5.37Operations SecurityReceipt, Transparency
A.5.21Supply Chain SecuritySBOM, VEX, Provenance

EU CRA Controls

IDCategorySatisfied By
CRA-AnnexI.2.1Product SecuritySBOM
CRA-AnnexI.2.5Vulnerability ManagementVEX, Reachability
CRA-Art11Vulnerability ManagementVEX, Incident, Transparency
CRA-AnnexI.1.2Product SecurityPolicy, Attestation, Receipt

Report Structure

ComplianceReport computed properties:

DI Registration

IComplianceReportGenerator -> ComplianceReportGenerator (TryAddSingleton factory)

OTel Metrics

Meter: StellaOps.Attestor.ProofChain.Compliance

CounterDescription
compliance.reports.generatedTotal compliance reports generated
compliance.controls.evaluatedTotal individual controls evaluated

Source Files

Test Coverage (26 tests)


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. ILinkCaptureService has 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

RecordPurpose
CapturedMaterialInput artifact (URI + digest map)
CapturedProductOutput artifact (URI + digest map)
CapturedEnvironmentExecution context (hostname, OS, variables)
LinkCaptureRequestCapture request with step, functionary, command, materials, products, env, byproducts, pipeline/step IDs
LinkCaptureResultResult with content-addressed digest, dedup flag, stored record
CapturedLinkRecordStored link with all fields + CapturedAt timestamp
LinkCaptureQueryQuery filter: step name, functionary, pipeline ID, limit

Deduplication

Content-addressed deduplication uses canonical hashing:

Service Interface

ILinkCaptureService:

DI Registration

ILinkCaptureService -> LinkCaptureService (TryAddSingleton factory)

OTel Metrics

Meter: StellaOps.Attestor.ProofChain.LinkCapture

CounterDescription
link.capturesTotal new link attestations captured
link.deduplicationsTotal deduplicated captures
link.queriesTotal query operations

Source Files

Test Coverage (30 tests)


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

RecordPurpose
RotationStatusEnum: Pending, Verified, ReSigned, Completed, Failed, Skipped
RotationCadenceEnum: Monthly, Quarterly, OnDemand
KeyTransitionOld/new key IDs, algorithm, effective date, grace period
BundleRotationRequestRotation cycle request with transition, bundle digests, cadence, tenant
BundleRotationEntryPer-bundle result (original/new digest, status, error)
BundleRotationResultFull cycle result with computed SuccessCount/FailureCount/SkippedCount
TransitionAttestationAudit record: attestation ID, rotation ID, result digest, counts
RotationScheduleEntrySchedule config: cadence, next/last rotation, current key, enabled
RotationHistoryQueryQuery filter: tenant, key ID, status, limit

Re-Signing Workflow

  1. Validate request (rotation ID, key IDs, bundle digests)
  2. Verify old key and new key exist in IProofChainKeyStore
  3. For each bundle: verify with old key → compute re-signed digest → record entry
  4. Determine overall status from individual entries
  5. Create TransitionAttestation with result digest for integrity verification
  6. Store in rotation history

Service Interface

IBundleRotationService:

DI Registration

IBundleRotationService -> BundleRotationService (TryAddSingleton factory, requires IProofChainKeyStore)

OTel Metrics

Meter: StellaOps.Attestor.ProofChain.Signing.Rotation

CounterDescription
rotation.cycles.startedTotal rotation cycles initiated
rotation.cycles.completedTotal rotation cycles completed
rotation.bundles.resignedTotal bundles successfully re-signed
rotation.bundles.skippedTotal bundles skipped
rotation.bundles.failedTotal bundles that failed rotation

Source Files

Test Coverage (35 tests)


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

TypePurpose
SuppressionCategoryEnum: VexOverride, AlertDedup, PolicyRule, OperatorAck, SeverityFilter, ComponentExclusion, FalsePositive
FindingSeverityEnum: None, Low, Medium, High, Critical
NoiseLedgerEntryImmutable record with digest, finding, category, severity, component, justification, suppressor, timestamps, expiry, evidence
RecordSuppressionRequestRequest to log a suppression
RecordSuppressionResultResult with digest, dedup flag, entry
NoiseLedgerQueryQuery filter: finding, category, severity, component, suppressor, tenant, active-only, limit
SuppressionStatisticsAggregated 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:

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

CounterDescription
noise.suppressions.recordedNew suppression entries
noise.suppressions.deduplicatedDeduplicated entries
noise.queries.executedQuery operations
noise.statistics.computedStatistics computations

Source Files

Test Coverage (37 tests)


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. SchemaIsolationService only 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:

SchemaPostgreSQL NameTables
ProofChainproofchainsbom_entries, dsse_envelopes, spines, trust_anchors, rekor_entries
Attestorattestorrekor_submission_queue, submission_state, entries, local_transparency_entries, external_transparency_targets, external_transparency_mirror_receipts, transparency_exchange_batches
Verdictverdictverdict_ledger, verdict_policies
Watchlistwatchlistwatched_identities, identity_alerts, alert_dedup
Auditauditnoise_ledger, hash_audit_log, suppression_stats

RLS Policy Coverage

Tenant isolation policies are defined for schemas that contain tenant-scoped 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:

TableHistory TableRetention
verdict.verdict_ledgerverdict.verdict_ledger_history7 years
watchlist.watched_identitieswatchlist.watched_identities_history1 year
audit.noise_ledgeraudit.noise_ledger_history7 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:

DI Registration

PersistenceServiceCollectionExtensions.AddAttestorPersistence() registers ISchemaIsolationService as a singleton with TimeProvider and IMeterFactory.

OTel Metrics

Meter: StellaOps.Attestor.Persistence.SchemaIsolation

CounterDescription
schema.provisioning.operationsSchema provisioning SQL generations
schema.rls.operationsRLS policy SQL generations
schema.temporal.operationsTemporal table SQL generations

Source Files

Prototype Test Coverage (54 tests verified 2026-07-18)


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:

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:

PropertyDescription
ProviderFileSystem, S3Compatible, or Gcs
RootPathRoot directory (FileSystem only)
BucketNameS3/GCS bucket name
EndpointUrlCustom endpoint (MinIO, localstack)
RegionAWS/GCS region
PrefixKey prefix for namespace isolation
EnforceWriteOnceWORM mode (prevents deletes and overwrites)

FileSystem Provider

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

CounterDescription
objectstorage.fs.putsFilesystem put operations
objectstorage.fs.getsFilesystem get operations
objectstorage.fs.deletesFilesystem delete operations

Meter: StellaOps.Attestor.ProofChain.Cas.ObjectStorage

CounterDescription
cas.objectstorage.putsCAS put via object storage
cas.objectstorage.deduplicationsDeduplicated puts
cas.objectstorage.getsCAS get via object storage
cas.objectstorage.deletesCAS delete via object storage

Source Files

Test Coverage (42 tests)

ObjectStorageContentAddressedStore (27 tests):

FileSystemObjectStorageProvider (13 tests):

ObjectStorageModels (5 tests):


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:

  1. Score Replay — Re-executes deterministic scoring from captured inputs (policy weights, coverage data, severity), computing a replayed score and determinism hash
  2. Score Comparison — Compares two replay results, quantifying divergence and identifying specific differences (score, hash, status)
  3. DSSE Attestation — Produces JSON-encoded attestation payloads ready for DSSE signing with application/vnd.stella.score+json payload type

Deterministic Scoring

Models

TypeDescription
ScoreReplayRequestReplay request with verdict ID, original score, scoring inputs
ScoreReplayResultResult with replay digest, status, replayed/original scores, divergence, determinism hash
ScoreReplayStatusMatched, Diverged, FailedMissingInputs, FailedError
ScoreComparisonRequestRequest to compare two replays by digest
ScoreComparisonResultComparison with divergence, determinism flag, difference details
ScoreReplayAttestationDSSE-ready attestation with JSON payload and signing key slot
ScoreReplayQueryQuery 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

CounterDescription
score.replays.executedTotal replay executions
score.replays.matchedReplays matching original score
score.replays.divergedReplays diverging from original
score.comparisons.executedComparison operations
score.attestations.createdAttestation productions

Source Files

Prototype Test Coverage (36 retained targeted tests)


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

  1. FormatReceipt — Converts VerificationReceiptReceiptSidebarDetail: maps ProofBundleId.Digest → string, TrustAnchorId.Value → string, iterates checks to build ReceiptCheckDetail list, derives overall ReceiptVerificationStatus from pass/fail counts, sets DsseVerified and RekorInclusionVerified by scanning check names for DSSE/Rekor keywords
  2. GetDetailAsync — Looks up registered receipt by bundle ID, returns ReceiptSidebarDetail with optional check and tool digest exclusion
  3. GetContextAsync — Returns VexReceiptSidebarContext combining 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

ConditionStatus
No checks presentUnverified
All checks passVerified
Some pass, some failPartiallyVerified
All checks failFailed

Models

TypeDescription
ReceiptVerificationStatusVerified, PartiallyVerified, Unverified, Failed
ReceiptCheckDetailSingle check formatted for sidebar (Name, Passed, KeyId?, LogIndex?, Detail?)
ReceiptSidebarDetailFull receipt DTO with computed TotalChecks/PassedChecks/FailedChecks, DsseVerified, RekorInclusionVerified
VexReceiptSidebarContextReceipt + Decision + Justification + EvidenceRefs + finding metadata
ReceiptSidebarRequestQuery 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

CounterDescription
sidebar.detail.totalSidebar detail requests
sidebar.context.totalSidebar context requests
sidebar.format.totalReceipts formatted for sidebar

Source Files

Test Coverage (29 retained targeted tests)

Advisory Commitments (2026-02-26 Batch)


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:

  1. Attestor – transparency log submission, inclusion proof verification, evidence caching
  2. Signer – DSSE envelope creation, cryptographic signing (keyless/keyful/HSM), entitlement enforcement
  3. 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 CategoryOwner ServiceStorageSensitivity
Attestation evidence (proofchain, inclusion proofs, Rekor entries)Attestorattestor PostgreSQL schemaHigh – tamper-evident, integrity-critical
Provenance evidence (SLSA predicates, build attestations, Merkle trees)Provenance (library)Consumed by Attestor/EvidenceLockerHigh – deterministic, reproducible
Signer metadata (audit events, signing ceremony state, rate limits)Signersigner PostgreSQL schemaHigh – operational security
Signer key material (KMS/HSM refs, Fulcio certs, trust anchors, rotation state)Signer (KeyManagement)signer PostgreSQL schemaCritical – cryptographic trust root

PostgreSQL Schema Ownership

Each trust-domain service retains its own DbContext and dedicated PostgreSQL schema:

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:

Implications:

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