Stella Ops Signer Architecture
Verified against commit
942a18727c(2026-09-26, DOC-2 of SPRINT_20260926_001). Re-verify by reading the evidence paths listed in the Evidence section.
Signer is the key-custody and DSSE-signing service of the trust domain. It is a live, separate deployable (compose key signer) with its own database. Its source lives under src/Attestor/StellaOps.Signer/ (WebService, Core, Infrastructure, Contracts, Tests), with key rotation and trust-anchor logic in src/Attestor/__Libraries/StellaOps.Signer.KeyManagement/, keyless support in src/Attestor/__Libraries/StellaOps.Signer.Keyless/, and the database in src/Attestor/__Libraries/StellaOps.Signer.Persistence/. The src/Attestor/ placement is a source root only: the attestor deployable was retired on 2026-09-05 and its attestation capability runs in the Evidence family (../evidence/architecture.md).
Related: Signer README (HTTP contract detail), no-merge decision, cryptography.
Deployment
| Aspect | Value | Source |
|---|---|---|
| Compose key / container | signer / ${STELLAOPS_ESTATE_PREFIX:-stellaops}-signer | devops/compose/docker-compose.stella-services.yml, devops/release/bundle/docker-compose.yml |
| Image | stellaops/signer (one image; regional overlays mount crypto packs) | same; devops/compose/docker-compose.compliance-{eu,russia,china}.yml |
| Listen / alias | http://+:8441, signer.stella-ops.local | same |
| Router registration | service name signer, consumer group signer | Program.cs (AddRouterMicroservice), bundle env Router__Messaging__ConsumerGroup |
| Depends on | authority, postgres, valkey (bundle) | devops/release/bundle/docker-compose.yml |
| Database | stellaops_signer, schema signer, variable STELLAOPS_POSTGRES_SIGNER_CONNECTION (compose refuses to render without it) | SignerPersistenceExtensions.cs, bundle env |
| Opt-in overlays | docker-compose.signer.yml (own-database cutover recipe), docker-compose.offlinekit-signer.yml (OfflineKit package-signing staging) | devops/compose/ |
Runtime at a glance (source-grounded)
Verified against src/Attestor/StellaOps.Signer/ on this checkout.
- Docker image / base path:
stellaops/signer; HTTP surface is rooted at/api/v1/signer/(SignerEndpoints.MapSignerEndpoints). - Authentication: Authority resource-server auth in live hosts (
AddStellaOpsResourceServerAuthentication); a test bearer/scope handler is injected only in theTestingenvironment by the test harness (Program.cs). - Database: Signer’s own PostgreSQL database
stellaops_signer, schemasigner, auto-migrated byStellaOps.Signer.Persistence. Its consolidated001_signer_consolidated_baseline.sqlreplaces the legacy KeyManagement startup chain;002_dsse_request_replays.sqlis the forward idempotency store. Live tables:signer.key_history,signer.key_audit_log,signer.trust_anchors,signer.ceremonies,signer.ceremony_approvals,signer.ceremony_audit_log,signer.operator_decision_envelopes, andsigner.dsse_request_replays. A liveSTELLAOPS_POSTGRES_SIGNER_CONNECTION(orPostgres:Signer:ConnectionString) is required outside the test host harness; EF InMemory key-management storage is rejected. (019 SGN-6, 2026-08-18: the formerConnectionStrings:KeyManagementkey is retired and no longer accepted.) - Audit retention window is applied, not only declared (retention-convergence RFC-14, 2026-09):
signer.key_audit_logandsigner.ceremony_audit_logare P13append-auditwith a seven-year window declared bysigner.prune_key_audit_log(interval)andsigner.prune_ceremony_audit_log(interval). Until RFC-14 nothing called them.SignerAuditRetentionService, registered only where Signer’s own connection is configured, now calls both with their declared default once a day after a five-minute start delay.Signer:AuditRetention:Enabled,SweepIntervalandStartDelaycontrol the schedule; there is no window setting, because the value lives in the baseline. The Doctor checkdoctor.signer.retention-window.p13names the sweep and fails when a row outlives the window, which is what a disabled or failing sweep looks like. - Ceremony-quorum trust evidence is durable (SPRINT_20260712_003 EVI-2): on quorum,
CeremonyQuorumAttestorassembles the multi-signedoperator-decision@v1DSSE envelope and persists it throughPostgresOperatorDecisionEnvelopeStoreintosigner.operator_decision_envelopes(idempotent on(tenant_id, envelope_digest)).InMemoryOperatorDecisionEnvelopeStoreis now registered only when no Signer-owned connection is configured (the test host) andSignerRuntimeGuardsrejects it outside Development/Testing. See operator-decision predicate — envelope persistence.
HTTP endpoints
Routes and policies as registered in Program.cs / the *Endpoints.cs files.
| Method & path | Policy (scope) | Source |
|---|---|---|
GET / | none (readiness banner) | Program.cs |
GET /healthz, GET /readyz | anonymous | Program.cs (SIGNER_HEALTH-01) |
GET /internal/plugins/status | anonymous | Program.cs — crypto-provider pack readiness (provider names, pack status) |
POST /internal/plugins/probe | anonymous | Program.cs — crypto-provider pack probe |
build-info endpoint (MapBuildInfoEndpoint) | anonymous | Program.cs — version/build banner |
GET /doctor/signer/checks | Signer.DoctorHealth → ops.health | Program.cs (MapServiceDoctorEndpoints("signer")), checks in Doctor/SignerDoctorChecks.cs |
GET /openapi/* (MapOpenApi) | anonymous | Program.cs — OpenAPI document |
POST /api/v1/signer/sign/dsse | Signer.Sign → signer:sign | SignerEndpoints |
POST /api/v1/signer/verify/dsse | Signer.Verify → signer:read | SignerEndpoints |
GET /api/v1/signer/verify/referrers?digest= | Signer.Verify → signer:read | SignerEndpoints |
POST /api/v1/anchors/{anchorId}/keys | Signer.KeyManagement → signer:rotate | KeyRotationEndpoints |
POST /api/v1/anchors/{anchorId}/keys/{keyId}/revoke | Signer.KeyManagement → signer:rotate | KeyRotationEndpoints |
GET /api/v1/anchors/{anchorId}/keys/{keyId}/validity | Signer.KeyManagement → signer:rotate | KeyRotationEndpoints |
GET /api/v1/anchors/{anchorId}/keys/history | Signer.KeyManagement → signer:rotate | KeyRotationEndpoints |
GET /api/v1/anchors/{anchorId}/keys/warnings | Signer.KeyManagement → signer:rotate | KeyRotationEndpoints |
POST /api/v1/ceremonies | Signer.CeremonyCreate → signer:sign | CeremonyEndpoints |
GET /api/v1/ceremonies | Signer.CeremonyRead → signer:read | CeremonyEndpoints |
GET /api/v1/ceremonies/{ceremonyId} | Signer.CeremonyRead → signer:read | CeremonyEndpoints |
POST /api/v1/ceremonies/{ceremonyId}/approve | Signer.CeremonyApprove → signer:sign | CeremonyEndpoints |
POST /api/v1/ceremonies/{ceremonyId}/execute | Signer.CeremonyExecute → signer:admin | CeremonyEndpoints |
DELETE /api/v1/ceremonies/{ceremonyId} | Signer.CeremonyCancel → signer:admin | CeremonyEndpoints |
Notes:
- The ceremony and anchor endpoints declare the canonical
SignerPolicies.*constants (Signer.CeremonyRead,Signer.KeyManagement, …) registered inProgram.cs; the pre-consolidation raw string aliases (ceremony:read,KeyManagement, …) were removed 2026-08-25 (P8-14). POST /verify/dsseverifies a complete Signer-produced bundle over DSSE PAE. The bundle carries the signer-selected provider and algorithm;IDsseVerifierresolves that registered provider and key id throughICryptoProviderRegistry. Raw-envelope input is retained only for the explicit Development/Testing HMAC signer, and that path is PAE-bound as well. This is Signer-key verification, not arbitrary Sigstore/certificate-chain/transparency-policy evaluation; the broader trust decision belongs to the attestation layer in the Evidence family.POST /sign/dsseaccepts an optionalIdempotency-Key. Signer binds it to tenant + authenticated subject + canonical operation, persists exact response bytes, and returns409for a different operation or425while another lease owns the same operation. Replays revalidate explicit tenant, subject, exact audiencesigner,signer:sign, and current PoE entitlement. A stale takeover may re-sign across the signed-before-completion crash boundary; the contract is not an exactly-once audit claim.- Generated OpenAPI declares the body, optional idempotency header, and
200/400/403/409/425/429/500. Its current generator does not project the minimal-API authorization policy into a security scheme; generated “Auth: Not declared” text does not override source-enforced authorization or DPoP/PoE. - In-toto statement version is selected from the normalized predicate type on every default signing backend. SLSA provenance v1 and
stella.ops/*predicates usehttps://in-toto.io/Statement/v1; legacy SLSA v0.2 and the currently mapped CycloneDX/SPDX/OpenVEX families retain Statement v0.1. KMS, keyless, and the Development/Testing HMAC harness must not produce different statement contracts for the same predicate solely because the signer backend changed. GET /verify/referrersresolves release-integrity trust for a scanner image digest viaIReleaseIntegrityVerifierand returns{ trusted, releaseSigner }.
Scopes
The Signer scope constants live in StellaOps.Auth.Abstractions/StellaOpsScopes.cs:
signer:read(SignerRead) — read Signer configuration/key metadata and perform DSSE verify / ceremony read.signer:sign(SignerSign) — create signatures, create/approve ceremonies.signer:rotate(SignerRotate) — key-rotation and trust-anchor management.signer:admin(SignerAdmin) — execute/cancel ceremonies; administrative control.
Configuration keys
Bound in Program.cs:
Authority:ResourceServer(resource-server auth; audiencesigner).STELLAOPS_POSTGRES_SIGNER_CONNECTION(orPostgres:Signer:ConnectionString) (PostgreSQL, databasestellaops_signer; required in live hosts. The legacyConnectionStrings:KeyManagementkey was retired by 019 SGN-6 and is no longer accepted).Signer:Entitlements,Signer:ReleaseVerification,Signer:Crypto,Signer:Dsse.Signer:Dsse:CadesBaselineBPfxPath— optional offline-only CAdES-B companion (PKCS#12/PFX). It does not provide qualified eIDAS/QSCD evidence and does not satisfy the T/LT/LTA profiles, which remain fail-closed (cadenced.cades.provider_pack_required).Router(Stella Router microservice registration;serviceName: "signer").
DSSE signing notes
- The DSSE sign endpoint runs
ISignerPipeline.SignAsync, which validates the proof-of-entitlement (PoE), sender binding (DPoP for JWT PoE, mTLS client cert for mTLS PoE), predicate, release integrity, quota, and provider resolution before emitting the DSSE envelope and an audit id. - The in-toto request predicate type must match the exact
PredicateTypesallowlist and the predicate payload must be a JSON object. Unsupported types and null/array/scalar payloads fail aspredicate_type_unsupportedorpredicate_invalidbefore entitlement, release-integrity, quota, provider, crypto, or audit side effects. - Signing mode is
kms(default) orkeyless; primary signatures are algorithm-pinned by the active crypto profile before provider lookup. - The CRA “cadenced” DSSE wrapper is opt-in via
options.cadenced=trueor areturnBundletoken containingcadenced, with CAdES profilesBaselineB/BaselineT/BaselineLT/BaselineLTA. - In local-runtime environments (
Development/Testing) the pipeline is replaced with a deterministicHmacDsseSignerharness; live hosts reject it. - Audit capture is body-disabled on the sign route. PoE values and signing payloads are not copied into Timeline audit, and rejected entitlement logging emits no token-derived length, name, or fingerprint.
- The direct endpoint currently performs structural, not cryptographic, DPoP validation. A live consumer must traverse a validating gateway or wait for a direct-endpoint
htu/htm/iat/jti/ath/JWK-signature closure.
Evidence
| Claim | Source |
|---|---|
| Host, policies, health, plugin, build-info and Doctor routes | src/Attestor/StellaOps.Signer/StellaOps.Signer.WebService/Program.cs |
| Policy names | src/Attestor/StellaOps.Signer/StellaOps.Signer.WebService/Security/SignerPolicies.cs |
| Signing and verification routes | src/Attestor/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/SignerEndpoints.cs |
| Ceremony routes | src/Attestor/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/CeremonyEndpoints.cs |
| Key-rotation routes | src/Attestor/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/KeyRotationEndpoints.cs |
Scopes signer:read, signer:sign, signer:rotate, signer:admin, ops.health | src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs |
| Tables and startup migrations | src/Attestor/__Libraries/StellaOps.Signer.Persistence/Migrations/001_signer_consolidated_baseline.sql, 002_dsse_request_replays.sql; Extensions/SignerPersistenceExtensions.cs |
| Frozen KeyManagement predecessor migrations | src/__Obsoleted/Attestor/__Libraries/StellaOps.Signer.KeyManagement/Migrations/ |
Audit retention keys Signer:AuditRetention:* | src/Attestor/StellaOps.Signer/StellaOps.Signer.WebService/Retention/ |
Doctor check ids doctor.signer.* | src/Attestor/StellaOps.Signer/StellaOps.Signer.WebService/Doctor/SignerDoctorChecks.cs |
| Profile algorithm pins, CAdES-B companion, cadenced wrapper | src/__Libraries/StellaOps.Cryptography.Dsse.Signing/CryptoDsseSigner.cs, DsseSignerOptions.cs, CadencedEnvelopeBuilder.cs |
| Deployment wiring | devops/compose/docker-compose.stella-services.yml, devops/release/bundle/docker-compose.yml, devops/compose/docker-compose.compliance-eu.yml, devops/compose/docker-compose.signer.yml, devops/compose/docker-compose.offlinekit-signer.yml |
