Stella Ops Signer

Cryptographic signing service for the Stella Ops release control plane: DSSE envelope signing/verification, dual-control key ceremonies, and trust-anchor key rotation, with fail-closed regional crypto profiles (FIPS/eIDAS/GOST/SM/PQ).

Audience: operators wiring the trust domain, and developers integrating signing into evidence-production workflows.

Consolidated under Attestor (Sprint 204, 2026-03-04). The Signer source now lives at src/Attestor/StellaOps.Signer/, and the authoritative design lives in the Attestor architecture dossier. Archived standalone docs are in docs-archive/modules/signer/. Runtime identity, the HTTP surface, and scopes are unchanged and are documented below.

Runtime Identity

Build and custody seams

Signer custody policy and orchestration remain service-owned in StellaOps.Signer.Core, StellaOps.Signer.Infrastructure, StellaOps.Signer.KeyManagement, and StellaOps.Signer.Keyless. Consumers must not compile those projects. They may consume the BCL-only StellaOps.Signer.Contracts project and the neutral mechanism libraries:

The historical namespaces are intentionally preserved. Compatibility type forwarders remain in StellaOps.Signer.Core and StellaOps.Signer.Infrastructure for already-built consumers, while fresh CLI and ExportCenter builds resolve the neutral assemblies directly. The CLI’s offline DORA path composes the published ports locally; it does not reuse the service’s SignerPipeline or introduce an online dependency.

HTTP API surface

All endpoints are minimal-API routes registered in StellaOps.Signer.WebService. Authorization policies are declared in Program.cs via AddStellaOpsScopePolicy and map to the canonical scopes in StellaOps.Auth.Abstractions/StellaOpsScopes.cs. Mutating endpoints emit audit events under AuditModules.Attestor.

Signing & verification (SignerEndpoints.cs)

Method & routePolicy → scopeNotes
POST /api/v1/signer/sign/dsseSigner.Sign → signer:signBody SignDsseRequestDto; optional Idempotency-Key; response SignDsseResponseDto (bundle, policy, auditId). Declares 200/400/403/409/425/429/500. The bundle carries providerName, algorithmId, and optional cryptoProfile. Audited as SignDsse with request-body capture disabled so PoE and signing payload bytes do not enter Timeline.
POST /api/v1/signer/verify/dsseSigner.Verify → signer:readA full Signer bundle is verified over DSSE PAE through ICryptoProviderRegistry using its provider/algorithm/key metadata. {dsse} or a bare envelope remains supported only for the explicit Development/Testing HMAC signer, which also signs/verifies PAE. Response VerifyDsseResponseDto (verified, keyId, reason). Audited (VerifyDsse).
GET /api/v1/signer/verify/referrers?digest=<digest>Signer.Verify → signer:readResponse VerifyReferrersResponseDto (trusted, trustedSigner).

The sign/dsse body always carries subject[] (name, digest) and poe (format = jwt|mtls, value). It then selects exactly one payload shape:

Mixed or incomplete shapes are rejected, as are requests that supply both producer and scannerImageDigest. producer.digest must be sha256:<64 hex> and match Signer’s allowlist for that producer kind. The legacy scanner digest is converted to producer kind scanner; it is not a generic bypass. Optional options contains signingMode = kms|keyless, expirySeconds, returnBundle, cadenced, and cadesProfile = baselineB|baselineT|baselineLT|baselineLTA. JWT PoE additionally requires a DPoP request header; mTLS PoE requires a client certificate. A caller must present an explicit tenant claim, subject, exact audience signer, and scope signer:sign before either fresh execution or completed replay.

With Idempotency-Key, the durable key is tenant + authenticated subject + key, and the operation digest covers every parsed request semantic plus PoE format and its non-secret entitlement identity while excluding the credential value. The same operation replays the stored status/content-type/body bytes and sets Idempotency-Replayed; a different operation returns 409, an active lease returns 425 with Retry-After, and a completion-store failure returns 500 instead of a false 200. Stale-lease takeover is fenced and clock-injected, but the signed-before-completion crash boundary can sign/audit more than once. The replay ledger is P13 source-of-truth and the completed row is the only exact response/audit replay copy; no production pruner is invented here.

The checked-in OpenAPI now declares the request body, optional idempotency header, and response statuses. The first-party OpenAPI generator still emits no security scheme for minimal-API .RequireAuthorization() metadata, so generated api-reference.md says Auth: Not declared; that is a documentation-generator limitation, not anonymous access. The source-enforced Bearer/tenant/scope and PoE/DPoP contracts above remain authoritative. A completed replay reruns the caller guard and current entitlement check. The direct endpoint’s current DPoP check is structural only. The staged OfflineKit path therefore uses the gateway’s route-scoped DPoP policy for /api/v1/signer: it requires a token with matching cnf.jkt, validates signature/htm/htu/iat/jti, and fails closed without a durable replay store. The gateway does not currently validate the optional ath claim. Direct Signer access must remain unreachable or untrusted until equivalent cryptographic sender validation exists there.

/verify/dsse is Signer-key verification, not arbitrary third-party Sigstore trust evaluation. Production callers should submit the complete bundle returned by /sign/dsse; the verifier decodes standard base64 or base64url, reconstructs DSSEv1 PAE from payloadType and payload bytes, and asks the named registered provider to verify the matching key id. Attestor remains responsible for broader certificate-chain, identity-policy, and transparency-log verification.

Dual-control ceremonies (CeremonyEndpoints.cs)

Route group /api/v1/ceremonies (group-level policy Signer.CeremonyRead → signer:read).

Method & routePolicy → scopeNotes
POST /api/v1/ceremonies/Signer.CeremonyCreate → signer:signBody CreateCeremonyRequestDto; 201 Created with CeremonyResponseDto. Audited.
GET /api/v1/ceremonies/(inherits Signer.CeremonyRead → signer:read)Filters: state, operationType, initiatedBy, tenantId, limit, offset. Returns CeremonyListResponseDto.
GET /api/v1/ceremonies/{ceremonyId:guid}(inherits Signer.CeremonyRead)CeremonyResponseDto or 404.
POST /api/v1/ceremonies/{ceremonyId:guid}/approveSigner.CeremonyApprove → signer:signBody ApproveCeremonyRequestDto (reason, base64 signature, signingKeyId). Audited.
POST /api/v1/ceremonies/{ceremonyId:guid}/executeSigner.CeremonyExecute → signer:adminAudited.
DELETE /api/v1/ceremonies/{ceremonyId:guid}?reason=Signer.CeremonyCancel → signer:admin204 No Content. Audited.

CeremonyOperationType: KeyGeneration, KeyRotation, KeyRevocation, KeyExport, KeyImport, KeyRecovery. CeremonyState: Pending, PartiallyApproved, Approved, Executed, Expired, Cancelled.

Policy naming note: the endpoints reference the SignerPolicies.* constants (Signer.CeremonyRead, etc.) registered in Program.cs; the pre-consolidation raw aliases (ceremony:read/ceremony:create/…) were removed 2026-08-25 (P8-14).

Key rotation & trust anchors (KeyRotationEndpoints.cs)

Route group /api/v1/anchors (group-level policy Signer.KeyManagement → signer:rotate).

Method & routeNotes
POST /api/v1/anchors/{anchorId:guid}/keysAdd a signing key; 201 Created with AddKeyResponseDto. Audited (AddKey).
POST /api/v1/anchors/{anchorId:guid}/keys/{keyId}/revokeRevoke a key; RevokeKeyResponseDto. Audited (RevokeKey).
GET /api/v1/anchors/{anchorId:guid}/keys/{keyId}/validity?signedAt=Temporal key-validity check; KeyValidityResponseDto.
GET /api/v1/anchors/{anchorId:guid}/keys/historyKeyHistoryResponseDto.
GET /api/v1/anchors/{anchorId:guid}/keys/warningsRotationWarningsResponseDto.

Operational endpoints (Program.cs)

Scopes

The Signer-specific canonical scopes (from StellaOpsScopes.cs) are:

Observability

The Signer service participates in the shared StellaOps host tracing/auditing pipeline (AddAuditEmission, Router/host instrumentation). A planned set of Signer-specific span attributes (signer.key_id, signer.algorithm, signer.signature_id, signer.subject_count, signer.predicate_type, signer.signing_mode, signer.signing_ms) is documented as the target semantic convention in StellaOps.Signer.Tests/Observability/SignerOTelTraceTests.cs.

Not yet implemented. No production code in StellaOps.Signer.WebService / StellaOps.Signer.Infrastructure emits these custom signer.* span tags today — the OTel test only asserts the documented attribute list is non-empty and tolerates their absence (“may be internal”). Treat the attribute names above as a forward convention, not a live signal, until span emission is wired.

Runtime Binding Contract

Live Signer hosts are fail-closed against local-only runtime services. Outside the test host harness, startup requires:

The optional local CAdES-B companion provider is enabled only by explicit Signer:Dsse:CadesBaselineBPfxPath configuration and is suitable for sealed offline harnesses that need verifiable CMS bytes. It does not upgrade Signer to qualified eIDAS/QSCD evidence and does not satisfy T/LT/LTA evidence profiles.

Signer does not bake regional crypto provider packs into the service image. Base compose mounts only the shared Router/Messaging bundle roots needed for service registration. Crypto provider packs remain host-owned for default and offline.verification; FIPS/eIDAS/GOST/SM/HSM/simulator/cloud entries require explicit profile overlays and signed provider material before they become runtime providers.

Primary DSSE signatures are algorithm-pinned by the Signer active crypto profile before provider lookup. Built-in pins are fips -> ES256, eidas -> ES256, gost -> GOST12-256, sm -> SM2, and pq -> DILITHIUM3; deployments may supply explicit ProfileAlgorithmPins for operator-controlled provider packs. Profiles that require SM or PQ still obey their existing soft-provider gates and otherwise fail closed.

The WebService project no longer references EF InMemory or test bearer handlers. Test-only auth, EF InMemory key-management storage, and local ceremony/PoE/quota/audit doubles live in StellaOps.Signer.Tests and are injected by SignerApplicationFactory. Development hosts must provide PostgreSQL key-management storage; the local HMAC signer remains a Development/Testing-only deterministic signing harness.

Why Signer lives under Attestor

Signer, Attestor, and Provenance form the trust domain — the set of services responsible for cryptographic evidence production, transparency logging, and verification. Consolidating source ownership under src/Attestor/ makes trust-boundary responsibilities explicit while preserving runtime isolation between Signer state and Attestor evidence state.

See the Trust Domain Model ADR for the no-merge rationale.