Files
git.stella-ops.org/docs/modules/attestor/architecture.md
master f98cea3bcf Add Authority Advisory AI and API Lifecycle Configuration
- 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.
2025-11-02 13:50:25 +02:00

24 KiB
Raw Blame History

component_architecture_attestor.md — StellaOps Attestor (2025Q4)

Derived from Epic19 Attestor Console with provenance hooks aligned to the Export Center bundle workflows scoped in Epic10.

Scope. Implementationready 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 chainoftrust to StellaOps 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 transparencylogged, 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 thirdpartysigned bundles.
  • Attestor does not decide PASS/FAIL; it logs attestations for SBOMs, reports, and export artifacts.
  • Rekor v2 backends may be local (selfhosted) 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 — tilebacked 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 shortlived ratelimit buckets.
  • Licensing Service (optional) — “endorse” call for crosslog publishing when customer optsin.

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 scopesattestor.write, attestor.verify, attestor.read, and administrative scopes for key management; all calls mTLS/DPoP-bound.

Supported predicate types

  • StellaOps.BuildProvenance@1
  • StellaOps.SBOMAttestation@1
  • StellaOps.ScanResults@1
  • StellaOps.PolicyEvaluation@1
  • StellaOps.VEXAttestation@1
  • StellaOps.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 the StellaOps.Attestor.Types.Tests project 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

  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.

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|key with offline verification and export bundle support.
  • SDKs expose sign/verify primitives for build pipelines.

Performance & observability targets

  • Throughput goal: ≥1000 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:

  • entries on artifact.sha256, bundleSha256, createdAt, and {status:1, createdAt:-1}.
  • dedupe.key unique (TTL 2448h).
  • audit.ts for timerange queries.

3) Input contract (from Signer)

Attestor accepts only DSSE envelopes that satisfy all of:

  1. mTLS peer certificate maps to signer service (CApinned).
  2. Authority OpTok with aud=attestor, scope=attestor.write, DPoP or mTLS bound.
  3. DSSE envelope is signed by the Signers key (or includes a Fulcioissued cert chain) and chains to configured roots (Fulcio/KMS).
  4. Predicate type is one of StellaOps types (sbom/report/vexexport) 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

4.1 Signing

POST /api/v1/attestations:sign (mTLS + OpTok required)

  • Purpose: Deterministically wrap StellaOps 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 preauthentication encoding, sign with the resolved provider (default EC, BouncyCastle Ed25519, or FileKMS 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} and attestor.sign_latency_seconds{algorithm,provider} metrics and append an audit row (action=sign).
  • 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; check dedupe. If present, return existing rekorUuid.
    • 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, return status=pending and retry asynchronously.
    • Persist entries record; archive DSSE to S3 if archive=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 existing uuid), 502 rekor_unavailable, 504 proof_timeout.

4.3 Proof retrieval

GET /api/v1/rekor/entries/{uuid}

  • Returns entries row (refreshes proof from Rekor if stale/missing).
  • Accepts ?refresh=true to force backend query.

4.4 Verification (thirdparty or internal)

POST /api/v1/rekor/verify

  • Body (one of):

    • { "uuid": "…" }
    • { "bundle": { …DSSE… } }
    • { "artifactSha256": "…" } (looks up most recent entry)
  • Checks:

    1. Bundle signature → cert chain to Fulcio/KMS roots configured.
    2. Inclusion proof → recompute leaf hash; verify Merkle path against checkpoint root.
    3. Optionally verify checkpoint against local trust anchors (if Rekor signs checkpoints).
    4. Confirm subject.digest matches callerprovided hash (when given).
    5. 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 jobs
  • attestor.bulk_job_duration_seconds{status} job runtime
  • attestor.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 pending and schedule a proof fetcher job (Mongo job doc + backoff).
  • Mirrors/dual logs:

    • When logPreference="both", submit to primary and mirror; store both UUIDs (primary canonical).
    • Optional cloud endorsement: POST to the StellaOps cloud /attest/endorse with {uuid, artifactSha256}; store returned endorsement id.

6) Security model

  • mTLS required for submission from Signer (CApinned).

  • Authority token with aud=attestor and 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:signer or pinned OIDC issuer).
    • Predicate predicateType must be on allowlist (sbom/report/vex-export).
    • subject.digest.sha256 values must be present and wellformed (hex).
  • No public submission path. Never accept bundles from untrusted clients.

  • Client certificate allowlists: optional security.mtls.allowedSubjects / allowedThumbprints tighten peer identity checks beyond CA pinning.

  • Rate limits: token-bucket per caller derived from quotas.perCaller (QPS/burst) returns 429 + Retry-After when exceeded.

  • Scope enforcement: API separates attestor.write, attestor.verify, and attestor.read policies; 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 2MiB), 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 rekorUuid and 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.txt with verification steps & hashes.

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_total
  • attestor.errors_total{type}

SLO guardrails:

  • attestor.verify_latency_seconds P95 ≤2s per policy.
  • attestor.verify_total{result="failed"}1% of attestor.verify_total over 30min rolling windows.

Correlation:

  • HTTP callers may supply X-Correlation-Id; Attestor will echo the header and push CorrelationId into 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 audit rows (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.preferredProviders defines 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 uses mode: kms; the password should be injected via secret store or environment.
  • For keyful providers, supply inline material or materialPath plus materialFormat (pem (default), base64, or hex). KMS keys ignore these fields and require kmsVersionId.
  • certificateChain entries are appended to returned bundles so offline verifiers do not need to dereference external stores.

10) Endtoend 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 ≤ 300ms (warm log; local Rekor).
    • Verify P95 ≤ 30ms from cache; ≤ 120ms with live proof fetch.
    • 1k submissions/minute per replica sustained.
  • Hot caches: dedupe (bundle hash → uuid), recent entries by artifact sha256.


13) Testing matrix

  • Happy path: valid DSSE, inclusion within timeout.
  • Idempotency: resubmit same bundleSha256 → same uuid.
  • Security: reject nonSigner mTLS, wrong aud, DPoP replay, untrusted cert chain, forbidden predicateType.
  • Rekor variants: promisethenproof, proof delayed, mirror dualsubmit, 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; HttpClient with 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/nonretryable with granular mapping.
  • Safety: size caps on bundles; decompress bombs guarded; strict UTF8.
  • CLI integration: stellaops verify attestation <uuid|bundle|artifact> calls /rekor/verify.

15) Optional features

  • Duallog write (primary + mirror) and crosslog proof packaging.
  • Cloud endorsement: send {uuid, artifactSha256} to StellaOps cloud; store returned endorsement id for marketing/chainofcustody.
  • Checkpoint pinning: periodically pin latest Rekor checkpoints to an external audit store for independent monitoring.