- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings. - Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations. - Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration. - Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options. - Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations. - Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client. - Updated project file to include necessary package references for the new Issuer Directory Client library.
		
			
				
	
	
	
		
			24 KiB
		
	
	
	
	
	
	
	
			
		
		
	
	component_architecture_attestor.md — Stella Ops Attestor (2025Q4)
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 Rekor v2, retrieves/validates inclusion proofs, caches results, and exposes verification APIs. It accepts DSSE only from the Signer over mTLS, enforces chain‑of‑trust to Stella Ops roots, and returns
{uuid, index, proof, logURL}to calling services (Scanner.WebService for SBOMs; backend for final reports; Excititor exports when configured).
0) Mission & boundaries
Mission. Turn a signed DSSE envelope from the Signer into a transparency‑logged, verifiable fact with a durable, replayable proof (Merkle inclusion + (optional) checkpoint anchoring). Provide fast verification for downstream consumers and a stable retrieval interface for UI/CLI.
Boundaries.
- Attestor does not sign; it must not accept unsigned or third‑party‑signed bundles.
 - Attestor does not decide PASS/FAIL; it logs attestations for SBOMs, reports, and export artifacts.
 - Rekor v2 backends may be local (self‑hosted) or remote; Attestor handles both with retries, backoff, and idempotency.
 
1) Topology & dependencies
Process shape: single stateless service stellaops/attestor behind mTLS.
Dependencies:
- Signer (caller) — authenticated via mTLS and Authority OpToks.
 - Rekor v2 — tile‑backed transparency log endpoint(s).
 - MinIO (S3) — optional archive store for DSSE envelopes & verification bundles.
 - MongoDB — local cache of 
{uuid, index, proof, artifactSha256, bundleSha256}; job state; audit. - Redis — dedupe/idempotency keys and short‑lived rate‑limit buckets.
 - Licensing Service (optional) — “endorse” call for cross‑log publishing when customer opts‑in.
 
Trust boundary: Only the Signer is allowed to call submission endpoints; enforced by mTLS peer cert allowlist + aud=attestor OpTok.
Roles, identities & scopes
- Subjects — immutable digests for artifacts (container images, SBOMs, reports) referenced in DSSE envelopes.
 - Issuers — authenticated builders/scanners/policy engines signing evidence; tracked with mode (
keyless,kms,hsm,fido2) and tenant scope. - Consumers — Scanner, Export Center, CLI, Console, Policy Engine that verify proofs using Attestor APIs.
 - Authority scopes — 
attestor.write,attestor.verify,attestor.read, and administrative scopes for key management; all calls mTLS/DPoP-bound. 
Supported predicate types
StellaOps.BuildProvenance@1StellaOps.SBOMAttestation@1StellaOps.ScanResults@1StellaOps.PolicyEvaluation@1StellaOps.VEXAttestation@1StellaOps.RiskProfileEvidence@1
Each predicate embeds subject digests, issuer metadata, policy context, materials, and optional transparency hints. Unsupported predicates return 422 predicate_unsupported.
Golden fixtures: Deterministic JSON statements for each predicate live in
src/Attestor/StellaOps.Attestor.Types/samples. They are kept stable by theStellaOps.Attestor.Types.Testsproject so downstream docs and contracts can rely on them without drifting.
Envelope & signature model
- DSSE envelopes canonicalised (stable JSON ordering) prior to hashing.
 - Signature modes: keyless (Fulcio cert chain), keyful (KMS/HSM), hardware (FIDO2/WebAuthn). Multiple signatures allowed.
 - Rekor entry stores bundle hash, certificate chain, and optional witness endorsements.
 - Archive CAS retains original envelope plus metadata for offline verification.
 - Envelope serializer emits compact (canonical, minified) and expanded (annotated, indented) JSON variants off the same canonical byte stream so hashing stays deterministic while humans get context.
 - Payload handling supports optional compression (
gzip,brotli) with compression metadata recorded in the expanded view and digesting always performed over the uncompressed bytes. - Expanded envelopes surface detached payload references (URI, digest, media type, size) so large artifacts can live in CAS/object storage while the canonical payload remains embedded for verification.
 - Payload previews auto-render JSON or UTF-8 text in the expanded output to simplify triage in air-gapped and offline review flows.
 
Verification pipeline overview
- Fetch envelope (from request, cache, or storage) and validate DSSE structure.
 - Verify signature(s) against configured trust roots; evaluate issuer policy.
 - Retrieve or acquire inclusion proof from Rekor (primary + optional mirror).
 - Validate Merkle proof against checkpoint; optionally verify witness endorsement.
 - Return cached verification bundle including policy verdict and timestamps.
 
UI & CLI touchpoints
- Console: Evidence browser, verification report, chain-of-custody graph, issuer/key management, attestation workbench, bulk verification views.
 - CLI: 
stella attest sign|verify|list|fetch|keywith offline verification and export bundle support. - SDKs expose sign/verify primitives for build pipelines.
 
Performance & observability targets
- Throughput goal: ≥1 000 envelopes/minute per worker with cached verification.
 - Metrics: 
attestor_submission_total,attestor_verify_seconds,attestor_rekor_latency_seconds,attestor_cache_hit_ratio. - Logs include 
tenant,issuer,subjectDigest,rekorUuid,proofStatus; traces cover submission → Rekor → cache → response path. 
2) Data model (Mongo)
Database: attestor
Collections & schemas
- 
entries{ _id: "<rekor-uuid>", artifact: { sha256: "<sha256>", kind: "sbom|report|vex-export", imageDigest?, subjectUri? }, bundleSha256: "<sha256>", // canonicalized DSSE index: <int>, // log index/sequence if provided by backend proof: { // inclusion proof checkpoint: { origin, size, rootHash, timestamp }, inclusion: { leafHash, path[] } // Merkle path (tiles) }, log: { url, logId? }, createdAt, status: "included|pending|failed", signerIdentity: { mode: "keyless|kms", issuer, san?, kid? } } - 
dedupe{ key: "bundle:<sha256>", rekorUuid, createdAt, ttlAt } // idempotency key - 
audit{ _id, ts, caller: { cn, mTLSThumbprint, sub, aud }, // from mTLS + OpTok action: "submit|verify|fetch", artifactSha256, bundleSha256, rekorUuid?, index?, result, latencyMs, backend } 
Indexes:
entriesonartifact.sha256,bundleSha256,createdAt, and{status:1, createdAt:-1}.dedupe.keyunique (TTL 24–48h).audit.tsfor time‑range queries.
3) Input contract (from Signer)
Attestor accepts only DSSE envelopes that satisfy all of:
- mTLS peer certificate maps to 
signerservice (CA‑pinned). - Authority OpTok with 
aud=attestor,scope=attestor.write, DPoP or mTLS bound. - DSSE envelope is signed by the Signer’s key (or includes a Fulcio‑issued cert chain) and chains to configured roots (Fulcio/KMS).
 - Predicate type is one of Stella Ops types (sbom/report/vex‑export) with valid schema.
 subject[*].digest.sha256is present and canonicalized.
Wire shape (JSON):
{
  "bundle": { "dsse": { "payloadType": "application/vnd.in-toto+json", "payload": "<b64>", "signatures": [ ... ] },
              "certificateChain": [ "-----BEGIN CERTIFICATE-----..." ],
              "mode": "keyless" },
  "meta": {
    "artifact": { "sha256": "<subject sha256>", "kind": "sbom|report|vex-export", "imageDigest": "sha256:..." },
    "bundleSha256": "<sha256 of canonical dsse>",
    "logPreference": "primary",               // "primary" | "mirror" | "both"
    "archive": true                           // whether Attestor should archive bundle to S3
  }
}
4) APIs
4.1 Signing
POST /api/v1/attestations:sign (mTLS + OpTok required)
- 
Purpose: Deterministically wrap Stella Ops payloads in DSSE envelopes before Rekor submission. Reuses the submission rate limiter and honours caller tenancy/audience scopes.
 - 
Body:
{ "keyId": "signing-key-id", "payloadType": "application/vnd.in-toto+json", "payload": "<base64 payload>", "mode": "keyless|keyful|kms", "certificateChain": ["-----BEGIN CERTIFICATE-----..."], "artifact": { "sha256": "<subject sha256>", "kind": "sbom|report|vex-export", "imageDigest": "sha256:...", "subjectUri": "oci://..." }, "logPreference": "primary|mirror|both", "archive": true } - 
Behaviour:
- Resolve the signing key from 
attestor.signing.keys[](includes algorithm, provider, and optional KMS version). - Compute DSSE pre‑authentication encoding, sign with the resolved provider (default EC, BouncyCastle Ed25519, or File‑KMS ES256), and add static + request certificate chains.
 - Canonicalise the resulting bundle, derive 
bundleSha256, and mirror the request meta shape used by/api/v1/rekor/entries. - Emit 
attestor.sign_total{result,algorithm,provider}andattestor.sign_latency_seconds{algorithm,provider}metrics and append an audit row (action=sign). 
 - Resolve the signing key from 
 - 
Response 200:
{ "bundle": { "dsse": { "payloadType": "...", "payload": "...", "signatures": [{ "keyid": "signing-key-id", "sig": "..." }] }, "certificateChain": ["..."], "mode": "kms" }, "meta": { "artifact": { "sha256": "...", "kind": "sbom" }, "bundleSha256": "...", "logPreference": "primary", "archive": true }, "key": { "keyId": "signing-key-id", "algorithm": "ES256", "mode": "kms", "provider": "kms", "signedAt": "2025-11-01T12:34:56Z" } } - 
Errors:
400 key_not_found,400 payload_missing|payload_invalid_base64|artifact_sha_missing,400 mode_not_allowed,403 client_certificate_required,401 invalid_token,500 signing_failed. 
4.2 Submission
POST /api/v1/rekor/entries  (mTLS + OpTok required)
- 
Body: as above.
 - 
Behavior:
- Verify caller (mTLS + OpTok).
 - Validate DSSE bundle (signature, cert chain to Fulcio/KMS; DSSE structure; payloadType allowed).
 - Idempotency: compute 
bundleSha256; checkdedupe. If present, return existingrekorUuid. - Submit canonicalized bundle to Rekor v2 (primary or mirror according to 
logPreference). - Retrieve inclusion proof (blocking until inclusion or up to 
proofTimeoutMs); if backend returns promise only, returnstatus=pendingand retry asynchronously. - Persist 
entriesrecord; archive DSSE to S3 ifarchive=true. 
 - 
Response 200:
{ "uuid": "…", "index": 123456, "proof": { "checkpoint": { "origin": "rekor@site", "size": 987654, "rootHash": "…", "timestamp": "…" }, "inclusion": { "leafHash": "…", "path": ["…","…"] } }, "logURL": "https://rekor…/api/v2/log/…/entries/…", "status": "included" } - 
Errors:
401 invalid_token,403 not_signer|chain_untrusted,409 duplicate_bundle(with existinguuid),502 rekor_unavailable,504 proof_timeout. 
4.3 Proof retrieval
GET /api/v1/rekor/entries/{uuid}
- Returns 
entriesrow (refreshes proof from Rekor if stale/missing). - Accepts 
?refresh=trueto force backend query. 
4.4 Verification (third‑party or internal)
POST /api/v1/rekor/verify
- 
Body (one of):
{ "uuid": "…" }{ "bundle": { …DSSE… } }{ "artifactSha256": "…" }(looks up most recent entry)
 - 
Checks:
- Bundle signature → cert chain to Fulcio/KMS roots configured.
 - Inclusion proof → recompute leaf hash; verify Merkle path against checkpoint root.
 - Optionally verify checkpoint against local trust anchors (if Rekor signs checkpoints).
 - Confirm subject.digest matches caller‑provided hash (when given).
 - Fetch transparency witness statement when enabled; cache results and downgrade status to WARN when endorsements are missing or mismatched.
 
 - 
Response:
{ "ok": true, "uuid": "…", "index": 123, "logURL": "…", "checkedAt": "…" } 
4.5 Bulk verification
POST /api/v1/rekor/verify:bulk enqueues a verification job containing up to quotas.bulk.maxItemsPerJob items. Each item mirrors the single verification payload (uuid | artifactSha256 | subject+envelopeId, optional policyVersion/refreshProof). The handler persists a MongoDB job document (bulk_jobs collection) and returns 202 Accepted with a job descriptor and polling URL.
GET /api/v1/rekor/verify:bulk/{jobId} returns progress and per-item results (subject/uuid, status, issues, cached verification report if available). Jobs are tenant- and subject-scoped; only the initiating principal can read their progress.
Worker path: BulkVerificationWorker claims queued jobs (status=queued → running), executes items sequentially through the cached verification service, updates progress counters, and records metrics:
attestor.bulk_jobs_total{status}– completed/failed jobsattestor.bulk_job_duration_seconds{status}– job runtimeattestor.bulk_items_total{status}– per-item outcomes (succeeded,verification_failed,exception)
The worker honours bulkVerification.itemDelayMilliseconds for throttling and reschedules persistence conflicts with optimistic version checks. Results hydrate the verification cache; failed items record the error reason without aborting the overall job.
5) Rekor v2 driver (backend)
- 
Canonicalization: DSSE envelopes are normalized (stable JSON ordering, no insignificant whitespace) before hashing and submission.
 - 
Transport: HTTP/2 with retries (exponential backoff, jitter), budgeted timeouts.
 - 
Idempotency: if backend returns “already exists,” map to existing
uuid. - 
Proof acquisition:
- In synchronous mode, poll the log for inclusion up to 
proofTimeoutMs. - In asynchronous mode, return 
pendingand schedule a proof fetcher job (Mongo job doc + backoff). 
 - In synchronous mode, poll the log for inclusion up to 
 - 
Mirrors/dual logs:
- When 
logPreference="both", submit to primary and mirror; store both UUIDs (primary canonical). - Optional cloud endorsement: POST to the Stella Ops cloud 
/attest/endorsewith{uuid, artifactSha256}; store returned endorsement id. 
 - When 
 
6) Security model
- 
mTLS required for submission from Signer (CA‑pinned).
 - 
Authority token with
aud=attestorand DPoP/mTLS binding must be presented; Attestor verifies both. - 
Bundle acceptance policy:
- DSSE signature must chain to the configured Fulcio (keyless) or KMS/HSM roots.
 - SAN (Subject Alternative Name) must match Signer identity policy (e.g., 
urn:stellaops:signeror pinned OIDC issuer). - Predicate 
predicateTypemust be on allowlist (sbom/report/vex-export). subject.digest.sha256values must be present and well‑formed (hex).
 - 
No public submission path. Never accept bundles from untrusted clients.
 - 
Client certificate allowlists: optional
security.mtls.allowedSubjects/allowedThumbprintstighten peer identity checks beyond CA pinning. - 
Rate limits: token-bucket per caller derived from
quotas.perCaller(QPS/burst) returns429+Retry-Afterwhen exceeded. - 
Scope enforcement: API separates
attestor.write,attestor.verify, andattestor.readpolicies; verification/list endpoints accept read or verify scopes while submission endpoints remain write-only. - 
Request hygiene: JSON content-type is mandatory (415 returned otherwise); DSSE payloads are capped (default 2 MiB), certificate chains limited to six entries, and signatures to six per envelope to mitigate parsing abuse.
 - 
Redaction: Attestor never logs secret material; DSSE payloads should be public by design (SBOMs/reports). If customers require redaction, enforce policy at Signer (predicate minimization) before Attestor.
 
7) Storage & archival
- 
Entries in Mongo provide a local ledger keyed by
rekorUuidand artifact sha256 for quick reverse lookups. - 
S3 archival (if enabled):
s3://stellaops/attest/ dsse/<bundleSha256>.json proof/<rekorUuid>.json bundle/<artifactSha256>.zip # optional verification bundle - 
Verification bundles (zip):
- DSSE (
*.dsse.json), proof (*.proof.json),chain.pem(certs),README.txtwith verification steps & hashes. 
 - DSSE (
 
8) Observability & audit
Metrics (Prometheus):
attestor.sign_total{result,algorithm,provider}attestor.sign_latency_seconds{algorithm,provider}attestor.submit_total{result,backend}attestor.submit_latency_seconds{backend}attestor.proof_fetch_total{subject,issuer,policy,result,attestor.log.backend}attestor.verify_total{subject,issuer,policy,result}attestor.verify_latency_seconds{subject,issuer,policy,result}attestor.dedupe_hits_totalattestor.errors_total{type}
SLO guardrails:
attestor.verify_latency_secondsP95 ≤ 2 s per policy.attestor.verify_total{result="failed"}≤ 1 % ofattestor.verify_totalover 30 min rolling windows.
Correlation:
- HTTP callers may supply 
X-Correlation-Id; Attestor will echo the header and pushCorrelationIdinto the log scope for cross-service tracing. 
Tracing:
- Spans: 
attestor.sign,validate,rekor.submit,rekor.poll,persist,archive,attestor.verify,attestor.verify.refresh_proof. 
Audit:
- Immutable 
auditrows (ts, caller, action, hashes, uuid, index, backend, result, latency). 
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"
  mongo:
    uri: "mongodb://mongo/attestor"
  s3:
    enabled: true
    endpoint: "http://minio:9000"
    bucket: "stellaops"
    prefix: "attest/"
    objectLock: "governance"
  redis:
    url: "redis://redis:6379/2"
  quotas:
    perCaller:
      qps: 50
      burst: 100
Notes:
signing.preferredProvidersdefines the resolution order when multiple providers support the requested algorithm. Omit to fall back to registration order.- File-backed KMS (
signing.kms) is required when at least one key usesmode: kms; the password should be injected via secret store or environment. - For keyful providers, supply inline 
materialormaterialPathplusmaterialFormat(pem(default),base64, orhex). KMS keys ignore these fields and requirekmsVersionId. certificateChainentries are appended to returned bundles so offline verifiers do not need to dereference external stores.
10) End‑to‑end sequences
A) Submit & include (happy path)
sequenceDiagram
  autonumber
  participant SW as Scanner.WebService
  participant SG as Signer
  participant AT as Attestor
  participant RK as Rekor v2
  SW->>SG: POST /sign/dsse (OpTok+PoE)
  SG-->>SW: DSSE bundle (+certs)
  SW->>AT: POST /rekor/entries (mTLS + OpTok)
  AT->>AT: Validate DSSE (chain to Fulcio/KMS; signer identity)
  AT->>RK: submit(bundle)
  RK-->>AT: {uuid, index?}
  AT->>RK: poll inclusion until proof or timeout
  RK-->>AT: inclusion proof (checkpoint + path)
  AT-->>SW: {uuid, index, proof, logURL}
B) Verify by artifact digest (CLI)
sequenceDiagram
  autonumber
  participant CLI as stellaops verify
  participant SW as Scanner.WebService
  participant AT as Attestor
  CLI->>SW: GET /catalog/artifacts/{id}
  SW-->>CLI: {artifactSha256, rekor: {uuid}}
  CLI->>AT: POST /rekor/verify { uuid }
  AT-->>CLI: { ok: true, index, logURL }
11) Failure modes & responses
| Condition | Return | Details | ||
|---|---|---|---|---|
| mTLS/OpTok invalid | 401 invalid_token | 
Include WWW-Authenticate DPoP challenge when applicable | 
||
| Bundle not signed by trusted identity | 403 chain_untrusted | 
DSSE accepted only from Signer identities | ||
| Duplicate bundle | 409 duplicate_bundle | 
Return existing uuid (idempotent) | 
||
| Rekor unreachable/timeout | 502 rekor_unavailable | 
Retry with backoff; surface Retry-After | 
||
| Inclusion proof timeout | 202 accepted | 
status=pending, background job continues to fetch proof | 
||
| Archive failure | 207 multi-status | 
Entry recorded; archive will retry asynchronously | ||
| Verification mismatch | 400 verify_failed | 
Include reason: chain | leafHash | rootMismatch | 
12) Performance & scale
- 
Stateless; scale horizontally.
 - 
Targets:
- Submit+proof P95 ≤ 300 ms (warm log; local Rekor).
 - Verify P95 ≤ 30 ms from cache; ≤ 120 ms with live proof fetch.
 - 1k submissions/minute per replica sustained.
 
 - 
Hot caches:
dedupe(bundle hash → uuid), recententriesby artifact sha256. 
13) Testing matrix
- Happy path: valid DSSE, inclusion within timeout.
 - Idempotency: resubmit same 
bundleSha256→ sameuuid. - Security: reject non‑Signer mTLS, wrong 
aud, DPoP replay, untrusted cert chain, forbidden predicateType. - Rekor variants: promise‑then‑proof, proof delayed, mirror dual‑submit, mirror failure.
 - Verification: corrupt leaf path, wrong root, tampered bundle.
 - Throughput: soak test with 10k submissions; latency SLOs, zero drops.
 
14) Implementation notes
- Language: .NET 10 minimal API; 
HttpClientwith sockets handler tuned for HTTP/2. - JSON: canonical writer for DSSE payload hashing.
 - Crypto: use BouncyCastle/System.Security.Cryptography; PEM parsing for cert chains.
 - Rekor client: pluggable driver; treat backend errors as retryable/non‑retryable with granular mapping.
 - Safety: size caps on bundles; decompress bombs guarded; strict UTF‑8.
 - CLI integration: 
stellaops verify attestation <uuid|bundle|artifact>calls/rekor/verify. 
15) Optional features
- Dual‑log write (primary + mirror) and cross‑log proof packaging.
 - Cloud endorsement: send 
{uuid, artifactSha256}to Stella Ops cloud; store returned endorsement id for marketing/chain‑of‑custody. - Checkpoint pinning: periodically pin latest Rekor checkpoints to an external audit store for independent monitoring.