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
signerindevops/compose/docker-compose.stella-services.ymland in the release bundledevops/release/bundle/docker-compose.yml. Its source lives undersrc/Attestor/StellaOps.Signer/(a source-root placement only; theattestordeployable 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 indocs-archive/modules/signer/.
Runtime Identity
- Image and container:
stellaops/signer(dev estatestellaops/signer:dev; release bundleregistry.stella-ops.org/stellaops/signer:${STELLA_VERSION}), listening onhttp://+:8441, network aliassigner.stella-ops.local, Router microservice namesigner(consumer groupsigner). There is one image for every region: the compliance overlays (devops/compose/docker-compose.compliance-{eu,russia,china}.yml) mount regional crypto provider packs and anappsettings.crypto.<region>.yamlinto the samesignerservice. - API base paths:
/api/v1/signer/,/api/v1/ceremonies/, and/api/v1/anchors/(mapped byEndpoints/SignerEndpoints.cs,Endpoints/CeremonyEndpoints.csandEndpoints/KeyRotationEndpoints.cs, registered fromProgram.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. 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 & 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). GET /doctor/signer/checks— the shared service Doctor surface (MapServiceDoctorEndpoints("signer")), gated by policySigner.DoctorHealth→ scopeops.health. Check ids aredoctor.signer.*(Doctor/SignerDoctorChecks.cs).- 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 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.
Related documentation
- Signer architecture — runtime contract, configuration and evidence.
- Evidence architecture — the attestation service that consumes Signer bundles.
- Attestor reference — predicate, bundle and verification formats.
- Cryptography — crypto providers and regional profiles.
- Canonical scope catalog:
src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs.
