419 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			419 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # component_architecture_signer.md — **Stella Ops Signer** (2025Q4)
 | ||
| 
 | ||
| > **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)
 | ||
| 
 | ||
| 1. **Authenticate** caller with **OpTok** (Authority OIDC, DPoP or mTLS‑bound).
 | ||
| 2. **Authorize** scopes (`signer.sign`) + audience (`aud=signer`) + tenant/installation.
 | ||
| 3. **Validate entitlement** via **PoE** (Proof‑of‑Entitlement) against Cloud Licensing `/license/introspect`.
 | ||
| 4. **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**.
 | ||
| 5. **Enforce plan & quotas** (concurrency/QPS/artifact size/rate caps).
 | ||
| 6. **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.
 | ||
| 7. **Return DSSE bundle** (subject digests + predicate + cert chain or KMS key id).
 | ||
| 8. **Audit** every decision; expose metrics.
 | ||
| 
 | ||
| ---
 | ||
| 
 | ||
| ## 2) External dependencies
 | ||
| 
 | ||
| * **Authority** (on‑prem OIDC): validates OpToks (JWKS/introspection) and DPoP/mTLS.
 | ||
| * **Licensing Service (cloud)**: `/license/introspect` to 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-PoE` header or request body.
 | ||
| 
 | ||
| ### 3.1 `POST /sign/dsse`
 | ||
| 
 | ||
| Request (JSON):
 | ||
| 
 | ||
| ```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:
 | ||
| 
 | ||
| ```json
 | ||
| {
 | ||
|   "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:
 | ||
| 
 | ||
| ```json
 | ||
| { "trusted": true, "signatures": [ { "type": "cosign", "digest": "sha256:...", "signedBy": "StellaOps Release 2027 Q2" } ] }
 | ||
| ```
 | ||
| 
 | ||
| > **Note:** This endpoint is also used internally by Signer before issuing signatures.
 | ||
| 
 | ||
| ---
 | ||
| 
 | ||
| ## 4) Validation pipeline (hot path)
 | ||
| 
 | ||
| ```mermaid
 | ||
| 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 `401` with `WWW-Authenticate: DPoP error="use_dpop_nonce", dpop_nonce="<nonce>"`.
 | ||
| * Client retries with new proof including the nonce; Signer validates nonce and `jti` uniqueness (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/introspect` with cert thumbprint + serial.
 | ||
|   * **JWT PoE**: `X-PoE` bearer token (DPoP/mTLS‑bound) is validated (sig + `cnf`) locally (Licensing JWKS) and then **introspected** for status and claims.
 | ||
| 
 | ||
| * **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_version` in predicate exceeds `max_version` or release date beyond `valid_release_year`).
 | ||
|   * Apply plan **throttles** (QPS/concurrency/artifact bytes) via token‑bucket in Redis keyed by `license_id`.
 | ||
| 
 | ||
| ---
 | ||
| 
 | ||
| ## 6) Release integrity (scanner provenance)
 | ||
| 
 | ||
| * **Input**: `scannerImageDigest` representing the actual Scanner component that produced the artifact.
 | ||
| 
 | ||
| * **Check**:
 | ||
| 
 | ||
|   1. Use **OCI Referrers API** to enumerate signatures of that digest.
 | ||
|   2. Verify **cosign** signatures against the configured **Stella Ops Release** keyring (keyless Fulcio roots *or* keyful public keys).
 | ||
|   3. 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[*].digest` must include `sha256`.
 | ||
| * `predicate.stellaops_version` must 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_throttled` with `Retry-After`.
 | ||
| * Free/community plan may also receive **randomized delay** to disincentivize farmed signing.
 | ||
| 
 | ||
| ---
 | ||
| 
 | ||
| ## 10) Storage & caches
 | ||
| 
 | ||
| * **Redis**:
 | ||
| 
 | ||
|   * DPoP nonce & `jti` replay cache (TTL ≤ 10 min).
 | ||
|   * PoE introspection cache (short TTL, e.g., 60–120 s).
 | ||
|   * Release‑verify cache (`scannerImageDigest` → { trusted, ts }).
 | ||
| 
 | ||
| * **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_total`
 | ||
| * `signer.keyless_certs_issued_total` / `signer.kms_sign_total`
 | ||
| * OTEL traces across stages; correlation id (`auditId`) returned to client.
 | ||
| 
 | ||
| ---
 | ||
| 
 | ||
| ## 13) Configuration (YAML)
 | ||
| 
 | ||
| ```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`, wrong `jkt`, replayed `jti`, 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):**
 | ||
| 
 | ||
| ```http
 | ||
| POST /api/v1/signer/sign/dsse HTTP/1.1
 | ||
| Authorization: DPoP <JWT>
 | ||
| DPoP: <proof>
 | ||
| Content-Type: application/json
 | ||
| 
 | ||
| ```
 | ||
| 
 | ||
| **Error (release untrusted):**
 | ||
| 
 | ||
| ```json
 | ||
| {
 | ||
|   "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.
 | ||
| 
 |