- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
18 KiB
component_architecture_signer.md — Stella Ops Signer (2025Q4)
Supports deliverables from Epic 10 – Export Center and Epic 19 – Attestor Console.
Scope. Implementation‑ready architecture for the Signer: the only service allowed to produce Stella Ops‑verified signatures over SBOMs and reports. It enforces entitlement (PoE), release integrity (scanner provenance), sender‑constrained auth (DPoP/mTLS), and emits in‑toto/DSSE bundles suitable for Rekor v2 logging by the Attestor. Includes APIs, data flow, storage, quotas, security, and test matrices.
0) Mission & boundaries
Mission. Convert authenticated signing requests from trusted Stella Ops services into verifiable DSSE bundles while enforcing license policy and supply‑chain integrity.
Boundaries.
- Signer does not push to Rekor — it returns DSSE to the caller; Attestor logs to Rekor v2.
- Signer does not compute PASS/FAIL — it signs SBOMs/reports produced by Scanner/WebService after backend evaluation.
- Signer is stateless for hot path — long‑term storage is limited to audit events; all secrets/keys live in KMS/HSM or are ephemeral (keyless).
1) Responsibilities (contract)
-
Authenticate caller with OpTok (Authority OIDC, DPoP or mTLS‑bound).
-
Authorize scopes (
signer.sign) + audience (aud=signer) + tenant/installation. -
Validate entitlement via PoE (Proof‑of‑Entitlement) against Cloud Licensing
/license/introspect. -
Verify release integrity of the scanner image digest presented in the request: must be cosign‑signed by Stella Ops release key, discoverable via OCI Referrers API.
-
Enforce plan & quotas (concurrency/QPS/artifact size/rate caps).
-
Mint signing identity:
- Keyless (default): get a short‑lived X.509 cert from Fulcio using the Signer’s OIDC identity and sign the DSSE.
- Keyful (optional): sign with an HSM/KMS key.
-
Return DSSE bundle (subject digests + predicate + cert chain or KMS key id).
-
Audit every decision; expose metrics.
2) External dependencies
- Authority (on‑prem OIDC): validates OpToks (JWKS/introspection) and DPoP/mTLS.
- Licensing Service (cloud):
/license/introspectto verify PoE (active, claims, expiry, revocation). - Fulcio (Sigstore) or KMS/HSM: to obtain certs or perform signatures.
- OCI Registry (Referrers API): to verify scanner image release signature.
- Attestor: downstream service that writes DSSE bundles to Rekor v2.
- Config/state stores: Redis (caches, rate buckets), Mongo/Postgres (audit log).
3) API surface (mTLS; DPoP supported)
Base path: /api/v1/signer. All endpoints require:
-
Access token (JWT) from Authority with
aud=signer,scope=signer.sign. -
Sender constraint: DPoP proof per request or mTLS client cert.
-
PoE presented as either:
- Client TLS cert (if PoE is mTLS‑style) chained to Licensing CA, or
- PoE JWT (DPoP/mTLS‑bound) in
X-PoEheader or request body.
3.1 POST /sign/dsse
Request (JSON):
{
"subject": [
{ "name": "s3://stellaops/images/sha256:.../inventory.cdx.pb",
"digest": { "sha256": "..." } }
],
"predicateType": "https://stella-ops.org/attestations/sbom/1",
"predicate": {
"image_digest": "sha256:...",
"stellaops_version": "2.3.1 (2027.04)",
"license_id": "LIC-9F2A...",
"customer_id": "CUST-ACME",
"plan": "pro",
"policy_digest": "sha256:...", // optional for final reports
"views": ["inventory", "usage"],
"created": "2025-10-17T12:34:56Z"
},
"scannerImageDigest": "sha256:sc-web-or-worker-digest",
"poe": {
"format": "jwt", // or "mtls"
"value": "eyJhbGciOi..." // PoE JWT when not using mTLS PoE
},
"options": {
"signingMode": "keyless", // "keyless" | "kms"
"expirySeconds": 600, // cert lifetime hint (keyless)
"returnBundle": "dsse+cert" // dsse (default) | dsse+cert
}
}
Response 200:
{
"bundle": {
"dsse": { "payloadType": "application/vnd.in-toto+json", "payload": "<base64>", "signatures": [ ... ] },
"certificateChain": [ "-----BEGIN CERTIFICATE-----...", "... root ..." ],
"mode": "keyless",
"signingIdentity": { "issuer": "https://fulcio.internal", "san": "urn:stellaops:signer", "certExpiry": "2025-10-17T12:44:56Z" }
},
"policy": { "plan": "pro", "maxArtifactBytes": 104857600, "qpsRemaining": 97 },
"auditId": "a7c9e3f2-1b7a-4e87-8c3a-90d7d2c3ad12"
}
Errors (RFC 7807):
401 invalid_token(JWT/DPoP/mTLS failure)403 entitlement_denied(PoE invalid/revoked/expired; release year mismatch)403 release_untrusted(scanner image not Stella‑signed)429 plan_throttled(license plan caps)413 artifact_too_large(size cap)400 invalid_request(schema/predicate/type invalid)500 signing_unavailable(Fulcio/KMS outage)
3.2 GET /verify/referrers?imageDigest=<sha256>
Checks whether the image at digest is signed by Stella Ops release key.
Response:
{ "trusted": true, "signatures": [ { "type": "cosign", "digest": "sha256:...", "signedBy": "StellaOps Release 2027 Q2" } ] }
Note: This endpoint is also used internally by Signer before issuing signatures.
KMS drivers (keyful mode)
Signer now ships five deterministic KMS adapters alongside the default keyless flow:
services.AddFileKms(...)– stores encrypted ECDSA material on disk for air-gapped or lab installs.services.AddAwsKms(options => { options.Region = "us-east-1"; /* optional: options.Endpoint, UseFipsEndpoint */ });– delegates signing to AWS KMS, caches metadata/public keys offline, and never exports the private scalar. Rotation/revocation still run through AWS tooling (this library intentionally throws for those APIs so we do not paper over operator approvals).services.AddGcpKms(options => { options.Endpoint = "kms.googleapis.com"; });– integrates with Google Cloud KMS asymmetric keys, auto-resolves the primary key version when callers omit a version, and verifies signatures locally with exported PEM material.services.AddPkcs11Kms(options => { options.LibraryPath = "/opt/hsm/libpkcs11.so"; options.PrivateKeyLabel = "stella-attestor"; });– loads a PKCS#11 module, opens read-only sessions, signs digests via HSM mechanisms, and never hoists the private scalar into process memory.services.AddFido2Kms(options => { options.CredentialId = "<base64url>"; options.PublicKeyPem = "-----BEGIN PUBLIC KEY-----..."; options.AuthenticatorFactory = sp => new WebAuthnAuthenticator(); });– routes signing to a WebAuthn/FIDO2 authenticator for dual-control or air-gap scenarios. The authenticator must supply the CTAP/WebAuthn plumbing; the library handles digesting, key material caching, and verification.
Cloud & hardware-backed drivers share a few invariants:
- Hash payloads server-side (SHA-256) before invoking provider APIs – signatures remain reproducible and digest inputs are observable in structured audit logs.
- Cache metadata for the configurable window (default 5 min) and subject-public-key-info blobs for 10 min; tune these per sovereignty policy when running in sealed/offline environments.
- Only expose public coordinates (
Qx,Qy) to the host ―KmsKeyMaterial.Dis blank for non-exportable keys so downstream code cannot accidentally persist secrets.
Security review checkpoint: rotate/destroy remains an administrative action in the provider. Document those runbooks per tenant, and gate AWS/GCP traffic in sealed-mode via the existing egress allowlist. PKCS#11 loads native code, so keep library paths on the allowlist and validate HSM policies separately. FIDO2 authenticators expect an operator in the loop; plan for session timeouts and explicit audit fields when enabling interactive signing.
4) Validation pipeline (hot path)
sequenceDiagram
autonumber
participant Client as Scanner.WebService
participant Auth as Authority (OIDC)
participant Sign as Signer
participant Lic as Licensing Service (cloud)
participant Reg as OCI Registry (Referrers)
participant Ful as Fulcio/KMS
Client->>Sign: POST /sign/dsse (OpTok + DPoP/mTLS, PoE, request)
Note over Sign: 1) Validate OpTok, audience, scope, DPoP/mTLS binding
Sign->>Lic: /license/introspect(PoE)
Lic-->>Sign: { active, claims: {license_id, plan, valid_release_year, max_version}, exp }
Note over Sign: 2) Enforce plan/version window and revocation
Sign->>Reg: Verify scannerImageDigest signed (Referrers + cosign)
Reg-->>Sign: OK with signer identity
Note over Sign: 3) Enforce release integrity
Note over Sign: 4) Enforce quotas (QPS/concurrency/size)
Sign->>Ful: Mint cert (keyless) or sign via KMS
Ful-->>Sign: Cert or signature
Sign-->>Client: DSSE bundle (+cert chain), policy counters, auditId
DPoP nonce dance (when enabled for high‑value ops):
- If DPoP proof lacks a valid nonce, Signer replies
401withWWW-Authenticate: DPoP error="use_dpop_nonce", dpop_nonce="<nonce>". - Client retries with new proof including the nonce; Signer validates nonce and
jtiuniqueness (Redis TTL cache).
5) Entitlement enforcement (PoE)
-
Accepted forms:
- mTLS PoE: client presents a PoE client cert at TLS handshake; Signer validates chain to Licensing CA (CA bundle configured) and calls
/license/introspectwith cert thumbprint + serial. - JWT PoE:
X-PoEbearer token (DPoP/mTLS‑bound) is validated (sig +cnf) locally (Licensing JWKS) and then introspected for status and claims.
- mTLS PoE: client presents a PoE client cert at TLS handshake; Signer validates chain to Licensing CA (CA bundle configured) and calls
-
Claims required:
license_id,plan(free|pro|enterprise|gov),valid_release_year,max_version,exp.- Optional:
tenant_id,customer_id,entitlements[].
-
Enforcements:
- Reject if revoked, expired, plan mismatch or release outside window (
stellaops_versionin predicate exceedsmax_versionor release date beyondvalid_release_year). - Apply plan throttles (QPS/concurrency/artifact bytes) via token‑bucket in Redis keyed by
license_id.
- Reject if revoked, expired, plan mismatch or release outside window (
6) Release integrity (scanner provenance)
-
Input:
scannerImageDigestrepresenting the actual Scanner component that produced the artifact. -
Check:
- Use OCI Referrers API to enumerate signatures of that digest.
- Verify cosign signatures against the configured Stella Ops Release keyring (keyless Fulcio roots or keyful public keys).
- Optionally require Rekor inclusion for those signatures.
-
Policy:
- If not signed by an authorized Stella Ops Release identity → deny.
- If signed but release year > PoE
valid_release_year→ deny.
-
Cache: LRU of digest → verification result (TTL 10–30 min) to avoid registry thrash.
7) Signing modes
7.1 Keyless (default; Sigstore Fulcio)
- Signer authenticates to Fulcio using its on‑prem OIDC identity (client credentials) and requests a short‑lived cert (5–10 min).
- Generates ephemeral keypair, gets cert for the public key, signs DSSE with the private key.
- DSSE bundle includes certificate chain; verifiers validate to Fulcio root.
7.2 Keyful (optional; KMS/HSM)
- Signer uses a configured KMS key (AWS KMS, GCP KMS, Azure Key Vault, Vault Transit, or HSM).
- DSSE bundle includes key metadata (kid, cert chain if x509).
- Recommended for FIPS/sovereign environments.
8) Predicates & schema
Supported predicate types (extensible):
https://stella-ops.org/attestations/sbom/1(SBOM emissions)https://stella-ops.org/attestations/report/1(final PASS/FAIL reports)https://stella-ops.org/attestations/vex-export/1(Excititor exports; optional)
Validation:
- JSON‑Schema per predicate type; canonical property order.
subject[*].digestmust includesha256.predicate.stellaops_versionmust parse and match policy windows.
9) Quotas & throttling
Per license_id (from PoE):
- QPS (token bucket), concurrency (semaphore), artifact bytes (sliding window).
- On exceed →
429 plan_throttledwithRetry-After. - Free/community plan may also receive randomized delay to disincentivize farmed signing.
10) Storage & caches
-
Redis:
- DPoP nonce &
jtireplay cache (TTL ≤ 10 min). - PoE introspection cache (short TTL, e.g., 60–120 s).
- Release‑verify cache (
scannerImageDigest→ { trusted, ts }).
- DPoP nonce &
-
Audit store (Mongo or Postgres):
signer.audit_events
{ _id, ts, tenantId, installationId, licenseId, customerId,
plan, actor{sub,cnf}, request{predicateType, subjectSha256[], imageDigest},
poe{type, thumbprint|jwtKid, exp, introspectSnapshot},
release{digest, signerId, policy},
mode: "keyless"|"kms",
result: "success"|"deny:<reason>"|"error:<reason>",
bundleSha256? }
- Config: Stella Ops release signing keyring, Fulcio roots, Licensing CA bundle.
11) Security & privacy
- mTLS on all Signer endpoints.
- No bearer fallbacks — DPoP/mTLS enforced for
aud=signer. - PoE is never persisted beyond audit snapshots (minimized fields).
- Secrets: no long‑lived private keys on disk (keyless) or handled via KMS APIs.
- Input hardening: schema‑validate predicates; cap payload sizes; zstd/gzip decompression bombs guarded.
- Logging: redact PoE JWTs, access tokens, DPoP proofs; log only hashes and identifiers.
12) Metrics & observability
signer.requests_total{result}signer.latency_seconds{stage=auth|introspect|release_verify|sign}signer.poe_failures_total{reason}signer.release_verify_failures_total{reason}signer.plan_throttle_total{license_id}signer.bundle_bytes_totalsigner.keyless_certs_issued_total/signer.kms_sign_total- OTEL traces across stages; correlation id (
auditId) returned to client.
13) Configuration (YAML)
signer:
listen: "https://0.0.0.0:8443"
authority:
issuer: "https://authority.internal"
jwksUrl: "https://authority.internal/jwks"
require: "dpop" # "dpop" | "mtls"
poe:
mode: "both" # "jwt" | "mtls" | "both"
licensing:
introspectUrl: "https://www.stella-ops.org/api/v1/license/introspect"
caBundle: "/etc/ssl/licensing-ca.pem"
cacheTtlSeconds: 90
release:
referrers:
allowRekorVerified: true
keyrings:
- type: "cosign-keyless"
fulcioRoots: ["/etc/fulcio/root.pem"]
identities:
- san: "mailto:release@stella-ops.org"
- san: "https://sigstore.dev/oidc/stellaops"
signing:
mode: "keyless" # "keyless" | "kms"
fulcio:
issuer: "https://fulcio.internal"
oidcClientId: "signer"
oidcClientSecretRef: "env:FULCIO_CLIENT_SECRET"
certTtlSeconds: 600
kms:
provider: "aws-kms"
keyId: "arn:aws:kms:...:key/..."
quotas:
default:
qps: 100
concurrency: 20
maxArtifactBytes: 104857600
free:
qps: 5
concurrency: 1
maxArtifactBytes: 1048576
14) Testing matrix
- Auth & DPoP: bad
aud, wrongjkt, replayedjti, missing nonce, mTLS mismatch. - PoE: expired, revoked, plan mismatch, release year gate, max_version gate.
- Release verify: unsigned digest, wrong signer, Rekor‑absent (when required), referrers unreachable.
- Signing: Fulcio outage; KMS timeouts; bundle correctness (verifier harness).
- Quotas: burst above QPS, artifact over size, concurrency overflow.
- Schema: invalid predicate types/required fields.
- Determinism: same request → identical DSSE (aside from cert validity period).
- Perf: P95 end‑to‑end under 120 ms with caches warm (excluding network to Fulcio).
15) Failure modes & responses
| Failure | HTTP | Problem type | Notes |
|---|---|---|---|
| Invalid OpTok / DPoP | 401 | invalid_token |
WWW-Authenticate with DPoP nonce if needed |
| PoE invalid/revoked | 403 | entitlement_denied |
Include license_id (hashed) and reason |
| Scanner image untrusted | 403 | release_untrusted |
Include digest and required identity |
| Plan throttle | 429 | plan_throttled |
Include limits and Retry-After |
| Artifact too large | 413 | artifact_too_large |
Include cap |
| Fulcio/KMS down | 503 | signing_unavailable |
Retry‑After with jitter |
16) Deployment & HA
- Run ≥ 2 replicas; front with L7 LB; sticky not required.
- Redis for replay/quota caches (HA).
- Audit sink (Mongo/Postgres) in primary region; asynchronous write with local fallback buffer.
- Fulcio/KMS clients configured with retries/backoff; circuit breakers.
17) Implementation notes
- .NET 10 minimal API + Kestrel mTLS; custom DPoP middleware; JWT/JWKS cache.
- Cosign verification via sigstore libraries; Referrers queries over registry API with retries.
- DSSE via in‑toto libs; canonical JSON writer for predicates.
- Backpressure paths: refuse at auth/quota stages before any expensive network calls.
18) Examples (wire)
Request (free plan; expect throttle if burst):
POST /api/v1/signer/sign/dsse HTTP/1.1
Authorization: DPoP <JWT>
DPoP: <proof>
Content-Type: application/json
{ ...body as above... }
Error (release untrusted):
{
"type": "https://stella-ops.org/problems/release_untrusted",
"title": "Scanner image not signed by StellaOps",
"status": 403,
"detail": "sha256:abcd... not in trusted keyring",
"instance": "urn:audit:a7c9e3f2-..."
}
19) Roadmap
- Key Transparency: optional publication of Signer’s own certs to a KT log.
- Attested Build: SLSA‑style provenance for Signer container itself, checked at startup.
- FIPS mode: enforce
ES256+ KMS/HSM only; disallow Ed25519. - Dual attestation: optional immediate push to Attestor (sync mode) with timeout budget, returning Rekor UUID inline.