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 indocs-archive/modules/signer/. Runtime identity, the HTTP surface, and scopes are unchanged and are documented below.
Runtime Identity
- Docker image:
stellaops/signer:dev(verified indevops/compose/docker-compose.stella-services.yml; regional variantssigner:eu/signer:russia/signer:chinaexist for compliance compose files). - API base paths:
/api/v1/signer/,/api/v1/ceremonies/, and/api/v1/anchors/(all mapped inStellaOps.Signer.WebService/Program.cs). - Direct payload signing: internal callers can use
SigningRequest.DirectPayload(SigningRequest.CreateDirectPayload) and HTTP callers can useSignDsseRequestDto.directPayloadfor registry-backed canonical JSON artifacts. The Signer-owned registry includes EU payloads such asproduct-csaf-advisory-v1and the OfflineKitofflinekit-manifest-v1payload.SignerPipelinevalidates the payload type, media type, schema pin, canonical JSON hash, entitlement, typed producer digest, quota, provider resolution, and audit before emitting the DSSE envelope. - CRA dossier wrapper: the DSSE signing endpoint accepts opt-in
options.cadenced=trueor areturnBundletoken containingcadenced. The response returns an outer DSSE payload typeapplication/vnd.stellaops.cadenced-dsse+json(schemastellaops.cadenced-dsse-envelope.v1, profilecra-dossier-envelope) plusbundle.cadencedmetadata.BaselineBcan emit a detached local CMS/CAdES-B-compatible companion whenSigner:Dsse:CadesBaselineBPfxPathis configured with a PKCS#12/PFX certificate containing private-key material; this is offline harness coverage only, not production QES/QTSP evidence.BaselineT,BaselineLT, andBaselineLTAremain fail-closed withcadenced.cades.provider_pack_requireduntil the production CAdES provider pack is available. (Grounded inCadencedEnvelopeBuilder.cs.) - Database: Signer’s own
stellaops_signer, schemasigner, auto-migrated byStellaOps.Signer.Persistence. The consolidated001_*owns the seven key/ceremony/operator-decision tables; forward002_dsse_request_replays.sqlowns the durable HTTP idempotency table. The older KeyManagement migration chain is compatibility history, not a second startup authority. Signer-web is the sole startup migration authority; the former centralSignerMigrationModulePluginwas retired at SGN-7, and the generic CLI migration-module inventory itself was deleted on 2026-09-14 (SPRINT_20260722_021 PLT-4). Signer’s doctor no longer probes the former plugin-directory environment variables orplugins/migrations(removed 2026-09-14, SPRINT_20260914_005 PEF-4);doctor.signer.migration-authority.coherencemeasures only the ledger’sapplied_by. - Authentication: Authority resource-server auth (
AddStellaOpsResourceServerAuthentication) in live hosts; test bearer auth is injected only by the test project harness (Testingenvironment).
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:
StellaOps.Cryptography.Dsse.Signingowns local DSSE signing/verification, the EU payload registry and offline trust-root verifier, and product-update manifest construction/verification.StellaOps.Cryptography.Sigstoreowns Fulcio/Rekor transport and ephemeral Sigstore signing/verification; long-lived enrollment, wrapping, rotation, entitlement, quota, and ceremony policy remain in Signer.
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 & route | Policy → scope | Notes |
|---|---|---|
POST /api/v1/signer/sign/dsse | Signer.Sign → signer:sign | Body 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/dsse | Signer.Verify → signer:read | A 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:read | Response 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:
- predicate shape:
predicateType,predicate(raw JSON), and exactly one producer identity — typedproducer, or the legacyscannerImageDigestfallback; or - direct shape:
directPayload(payloadTypeId,payloadType,schemaPinId,payloadBase64,canonicalPayloadSha256) and a typedproducer(kind,digest).
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 & route | Policy → scope | Notes |
|---|---|---|
POST /api/v1/ceremonies/ | Signer.CeremonyCreate → signer:sign | Body 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}/approve | Signer.CeremonyApprove → signer:sign | Body ApproveCeremonyRequestDto (reason, base64 signature, signingKeyId). Audited. |
POST /api/v1/ceremonies/{ceremonyId:guid}/execute | Signer.CeremonyExecute → signer:admin | Audited. |
DELETE /api/v1/ceremonies/{ceremonyId:guid}?reason= | Signer.CeremonyCancel → signer:admin | 204 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 inProgram.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 & route | Notes |
|---|---|
POST /api/v1/anchors/{anchorId:guid}/keys | Add a signing key; 201 Created with AddKeyResponseDto. Audited (AddKey). |
POST /api/v1/anchors/{anchorId:guid}/keys/{keyId}/revoke | Revoke 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/history | KeyHistoryResponseDto. |
GET /api/v1/anchors/{anchorId:guid}/keys/warnings | RotationWarningsResponseDto. |
Operational endpoints (Program.cs)
GET /healthz— liveness, anonymous,Predicate=false(always200while the process is responsive).GET /readyz— readiness, anonymous, runs all registered health checks.GET /— anonymous readiness string"StellaOps Signer service ready.".GET /internal/plugins/status- anonymous canonical plugin status report for Signer’s crypto provider packs. Basedefaultandoffline.verificationproviders are host-owned and required; regional packs are optional unless explicitly selected by a regional compose overlay.POST /internal/plugins/probe- anonymous deterministic dry-run probe using the sharedPluginProbeReportcontract. The base probe requiresdefaultandoffline.verificationto respond and leaves unmounted regional packs visible as non-blockingnotMounted.- Build-info endpoint mapped via
BuildInfoEndpointExtensions.MapBuildInfoEndpoint(operator verify aggregator). - OpenAPI document mapped anonymously (
MapOpenApi().AllowAnonymous(); default route/openapi/{documentName}.json).
Scopes
The Signer-specific canonical scopes (from StellaOpsScopes.cs) are:
signer:read— read-only access to Signer configuration and key metadata (DSSE verify, referrers, ceremony read).signer:sign— create signatures (DSSE sign, ceremony create/approve).signer:rotate— rotate Signer keys (the/api/v1/anchorskey-management group).signer:admin— administrative control (ceremony execute/cancel).
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.Infrastructureemits these customsigner.*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:
STELLAOPS_POSTGRES_SIGNER_CONNECTION(orPostgres:Signer:ConnectionString) bound to PostgreSQL; EF InMemory key-management storage is rejected. (Updated 2026-08-18, 019 SGN-6: Signer resolves its OWN databasestellaops_signer. The formerConnectionStrings:KeyManagementkey was retired — it is no longer accepted, and a host configured only with it now fails closed rather than reaching the shared database.)- Authority resource-server authentication; the stub bearer handler is rejected.
- PostgreSQL ceremony repository and audit sink; in-memory ceremony services are rejected.
- Configured PoE introspection, stateless quota enforcement, structured audit emission, and crypto-backed DSSE signing; in-memory PoE/quota/audit sinks and local HMAC signing are rejected.
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.
Related documentation
- Attestor architecture dossier — authoritative trust-domain design.
- Attestor module overview — evidence production and verification surface.
- Canonical scope catalog:
StellaOps.Auth.Abstractions/StellaOpsScopes.cs.
