Files
git.stella-ops.org/docs/ARCHITECTURE_ATTESTOR.md
master 5fd4032c7c
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Add channel test providers for Email, Slack, Teams, and Webhook
- Implemented EmailChannelTestProvider to generate email preview payloads.
- Implemented SlackChannelTestProvider to create Slack message previews.
- Implemented TeamsChannelTestProvider for generating Teams Adaptive Card previews.
- Implemented WebhookChannelTestProvider to create webhook payloads.
- Added INotifyChannelTestProvider interface for channel-specific preview generation.
- Created ChannelTestPreviewContracts for request and response models.
- Developed NotifyChannelTestService to handle test send requests and generate previews.
- Added rate limit policies for test sends and delivery history.
- Implemented unit tests for service registration and binding.
- Updated project files to include necessary dependencies and configurations.
2025-10-19 23:29:34 +03:00

15 KiB
Raw Blame History

component_architecture_attestor.md — StellaOps Attestor (2025Q4)

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.


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 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.2 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.3 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).
  • Response:

    { "ok": true, "uuid": "…", "index": 123, "logURL": "…", "checkedAt": "…" }
    

4.4 Batch submission (optional)

POST /api/v1/rekor/batch accepts an array of submission objects; processes with peritem results.


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.

  • 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.submit_total{result,backend}
  • attestor.submit_latency_seconds{backend}
  • attestor.proof_fetch_total{result}
  • attestor.verify_total{result}
  • attestor.dedupe_hits_total
  • attestor.errors_total{type}

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: validate, rekor.submit, rekor.poll, persist, archive, verify.

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"]
  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

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.