Stella Ops Signer

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 of architecture.md.

Service wiring (image, database and role, compose profile, bundle presence) is generated in the service inventory; this dossier does not restate it.

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.

Status (2026-09-26). Signer is a live, separate key-custody deployable: compose key signer in devops/compose/docker-compose.stella-services.yml and in the release bundle devops/release/bundle/docker-compose.yml. Its source lives under src/Attestor/StellaOps.Signer/ (a source-root placement only; the attestor deployable itself was retired on 2026-09-05 and its capability now runs in the Evidence family). This directory is the Signer dossier; architecture.md carries the runtime contract and evidence. Older standalone Signer docs are archived in docs-archive/modules/signer/.

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. Broader certificate-chain, identity-policy, and transparency-log verification belongs to the attestation layer in the Evidence family.

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 the source lives under src/Attestor/

Signer, the attestation libraries and Provenance form the trust domain. Their source was grouped under src/Attestor/ in Sprint 204 (2026-03-04). The grouping is a source-root placement: Signer keeps its own deployable, database (stellaops_signer) and role, and attestation evidence lives in the Evidence database. The two stores are deliberately not merged; see the no-merge decision.