Files
git.stella-ops.org/docs/modules/attestor/architecture.md
master 8bbfe4d2d2 feat(rate-limiting): Implement core rate limiting functionality with configuration, decision-making, metrics, middleware, and service registration
- Add RateLimitConfig for configuration management with YAML binding support.
- Introduce RateLimitDecision to encapsulate the result of rate limit checks.
- Implement RateLimitMetrics for OpenTelemetry metrics tracking.
- Create RateLimitMiddleware for enforcing rate limits on incoming requests.
- Develop RateLimitService to orchestrate instance and environment rate limit checks.
- Add RateLimitServiceCollectionExtensions for dependency injection registration.
2025-12-17 18:02:37 +02:00

35 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.
  • PostgreSQL — 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.

Rekor Inclusion Proof Verification (SPRINT_3000_0001_0001)

The Attestor implements RFC 6962-compliant Merkle inclusion proof verification for Rekor transparency log entries:

Components:

  • MerkleProofVerifier — Verifies Merkle audit paths per RFC 6962 Section 2.1.1
  • CheckpointSignatureVerifier — Parses and verifies Rekor checkpoint signatures (ECDSA/Ed25519)
  • RekorVerificationOptions — Configuration for public keys, offline mode, and checkpoint caching

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:

  • Bundled checkpoints can be used in air-gapped environments
  • EnableOfflineMode and OfflineCheckpointBundlePath configuration options
  • AllowOfflineWithoutSignature for fully disconnected scenarios (reduced security)

Metrics:

  • attestor.rekor_inclusion_verify_total — Verification attempts by result
  • attestor.rekor_checkpoint_verify_total — Checkpoint signature verifications
  • attestor.rekor_offline_verify_total — Offline mode verifications
  • attestor.rekor_checkpoint_cache_hits/misses — Checkpoint cache performance

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 (PostgreSQL)

Database: attestor

Tables & schemas

  • entries table

    CREATE TABLE attestor.entries (
      id UUID PRIMARY KEY,                                -- rekor-uuid
      artifact_sha256 TEXT NOT NULL,
      artifact_kind TEXT NOT NULL,                        -- sbom|report|vex-export
      artifact_image_digest TEXT,
      artifact_subject_uri TEXT,
      bundle_sha256 TEXT NOT NULL,                        -- canonicalized DSSE
      log_index INTEGER,                                  -- log index/sequence if provided by backend
      proof_checkpoint JSONB,                             -- { origin, size, rootHash, timestamp }
      proof_inclusion JSONB,                              -- { leafHash, path[] } Merkle path (tiles)
      log_url TEXT,
      log_id TEXT,
      created_at TIMESTAMPTZ DEFAULT NOW(),
      status TEXT NOT NULL,                               -- included|pending|failed
      signer_identity JSONB                               -- { mode, issuer, san?, kid? }
    );
    
  • dedupe table

    CREATE TABLE attestor.dedupe (
      key TEXT PRIMARY KEY,                               -- bundle:<sha256> idempotency key
      rekor_uuid UUID NOT NULL,
      created_at TIMESTAMPTZ DEFAULT NOW(),
      ttl_at TIMESTAMPTZ NOT NULL                         -- for scheduled cleanup
    );
    
  • audit table

    CREATE TABLE attestor.audit (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      ts TIMESTAMPTZ DEFAULT NOW(),
      caller_cn TEXT,
      caller_mtls_thumbprint TEXT,
      caller_sub TEXT,
      caller_aud TEXT,
      action TEXT NOT NULL,                               -- submit|verify|fetch
      artifact_sha256 TEXT,
      bundle_sha256 TEXT,
      rekor_uuid UUID,
      log_index INTEGER,
      result TEXT NOT NULL,
      latency_ms INTEGER,
      backend TEXT
    );
    

Indexes:

  • entries: indexes on artifact_sha256, bundle_sha256, created_at, and composite (status, created_at DESC).
  • dedupe: unique index on key; scheduled job cleans rows where ttl_at < NOW() (2448h retention).
  • audit: index on ts for timerange queries.

2.1) Content-Addressed Identifier Formats

The ProofChain library (StellaOps.Attestor.ProofChain) defines canonical content-addressed identifiers for all proof chain components. These IDs ensure determinism, tamper-evidence, and reproducibility.

Identifier Types

ID Type Format Source Example
ArtifactID sha256:<64-hex> Container manifest or binary hash sha256:a1b2c3d4e5f6...
SBOMEntryID <sbomDigest>:<purl>[@<version>] SBOM hash + component PURL sha256:91f2ab3c:pkg:npm/lodash@4.17.21
EvidenceID sha256:<hash> Canonical evidence JSON sha256:e7f8a9b0c1d2...
ReasoningID sha256:<hash> Canonical reasoning JSON sha256:f0e1d2c3b4a5...
VEXVerdictID sha256:<hash> Canonical VEX verdict JSON sha256:d4c5b6a7e8f9...
ProofBundleID sha256:<merkle_root> Merkle root of bundle components sha256:1a2b3c4d5e6f...
GraphRevisionID grv_sha256:<hash> Merkle root of graph state grv_sha256:9f8e7d6c5b4a...

Canonicalization (RFC 8785)

All JSON-based IDs use RFC 8785 (JCS) canonicalization:

  • UTF-8 encoding
  • Lexicographically sorted keys
  • No whitespace (minified)
  • No volatile fields (timestamps, random values excluded)

Implementation: StellaOps.Attestor.ProofChain.Json.Rfc8785JsonCanonicalizer

Merkle Tree Construction

ProofBundleID and GraphRevisionID use deterministic binary Merkle trees:

  • SHA-256 hash function
  • Lexicographically sorted leaf inputs
  • Standard binary tree construction (pair-wise hashing)
  • Odd leaves promoted to next level

Implementation: StellaOps.Attestor.ProofChain.Merkle.DeterministicMerkleTreeBuilder

ID Generation Interface

// Core interface for ID generation
public interface IContentAddressedIdGenerator
{
    EvidenceId GenerateEvidenceId(EvidencePredicate predicate);
    ReasoningId GenerateReasoningId(ReasoningPredicate predicate);
    VexVerdictId GenerateVexVerdictId(VexPredicate predicate);
    ProofBundleId GenerateProofBundleId(SbomEntryId sbom, EvidenceId[] evidence, 
        ReasoningId reasoning, VexVerdictId verdict);
    GraphRevisionId GenerateGraphRevisionId(GraphState state);
}

Predicate Types

The ProofChain library defines DSSE predicates for proof chain attestations. All predicates follow the in-toto Statement/v1 format.

Predicate Type Registry

Predicate Type URI Purpose Signer Role
Evidence evidence.stella/v1 Raw evidence from scanner/ingestor (findings, reachability data) Scanner/Ingestor key
Reasoning reasoning.stella/v1 Policy evaluation trace with inputs and intermediate findings Policy/Authority key
VEX Verdict cdx-vex.stella/v1 VEX verdict with status, justification, and provenance VEXer/Vendor key
Proof Spine proofspine.stella/v1 Merkle-aggregated proof spine linking evidence to verdict Authority key
Verdict Receipt verdict.stella/v1 Final surfaced decision receipt with policy rule reference Authority key
SBOM Linkage https://stella-ops.org/predicates/sbom-linkage/v1 SBOM-to-component linkage metadata Generator key

Evidence Statement (evidence.stella/v1)

Captures raw evidence collected from scanners or vulnerability feeds.

Field Type Description
source string Scanner or feed name that produced this evidence
sourceVersion string Version of the source tool
collectionTime DateTimeOffset UTC timestamp when evidence was collected
sbomEntryId string Reference to the SBOM entry this evidence relates to
vulnerabilityId string? CVE or vulnerability identifier if applicable
rawFinding object Pointer to or inline representation of raw finding data
evidenceId string Content-addressed ID (sha256:<hash>)

Reasoning Statement (reasoning.stella/v1)

Captures policy evaluation traces linking evidence to decisions.

Field Type Description
sbomEntryId string SBOM entry this reasoning applies to
evidenceIds string[] Evidence IDs considered in this reasoning
policyVersion string Version of the policy used for evaluation
inputs object Inputs to the reasoning process (evaluation time, thresholds, lattice rules)
intermediateFindings object? Intermediate findings from the evaluation
reasoningId string Content-addressed ID (sha256:<hash>)

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

Captures VEX status determinations with provenance.

Field Type Description
sbomEntryId string SBOM entry this verdict applies to
vulnerabilityId string CVE, GHSA, or other vulnerability identifier
status string VEX status: not_affected, affected, fixed, under_investigation
justification string Justification for the VEX status
policyVersion string Version of the policy used
reasoningId string Reference to the reasoning that led to this verdict
vexVerdictId string Content-addressed ID (sha256:<hash>)

Proof Spine Statement (proofspine.stella/v1)

Merkle-aggregated proof bundle linking all chain components.

Field Type Description
sbomEntryId string SBOM entry this proof spine covers
evidenceIds string[] Sorted list of evidence IDs included in this proof bundle
reasoningId string Reasoning ID linking evidence to verdict
vexVerdictId string VEX verdict ID for this entry
policyVersion string Version of the policy used
proofBundleId string Content-addressed ID (sha256:<merkle_root>)

Verdict Receipt Statement (verdict.stella/v1)

Final surfaced decision receipt with full provenance.

Field Type Description
graphRevisionId string Graph revision ID this verdict was computed from
findingKey object Finding key (sbomEntryId + vulnerabilityId)
rule object Policy rule that produced this verdict
decision object Decision made by the rule
inputs object Inputs used to compute this verdict
outputs object Outputs/references from this verdict
createdAt DateTimeOffset UTC timestamp when verdict was created

SBOM Linkage Statement (sbom-linkage/v1)

SBOM-to-component linkage metadata.

Field Type Description
sbom object SBOM descriptor (id, format, specVersion, mediaType, sha256, location)
generator object Generator tool descriptor
generatedAt DateTimeOffset UTC timestamp when linkage was generated
incompleteSubjects object[]? Subjects that could not be fully resolved
tags object? Arbitrary tags for classification or filtering

Reference: src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/


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 PostgreSQL job record (bulk_jobs table) 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 PostgreSQL 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"
  postgres:
    connectionString: "Host=postgres;Port=5432;Database=attestor;Username=stellaops;Password=secret"
  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.

16) Observability (stub)

  • Runbook + dashboard placeholder for offline import: operations/observability.md, operations/dashboards/attestor-observability.json.
  • Metrics to surface: signing latency p95/p99, verification failure rate, transparency log submission lag, key rotation age, queue backlog, attestation bundle size histogram.
  • Health endpoints: /health/liveness, /health/readiness, /status; verification probe /api/attestations/verify once demo bundle is available (see runbook).
  • Alert hints: signing latency > 1s p99, verification failure spikes, tlog submission lag >10s, key rotation age over policy threshold, backlog above configured threshold.