Skip to content

Signing Service

This is the authoritative bounded-context reference for the Signing Service that ships under internal/signing and the cmd/plexsphere-signer binary. The reference is split into four focused pages plus this entry point.

The Signing Service is the sharpest privilege edge in a plexsphere deployment: the only process in the system that ever touches Ed25519 private key material. Every other subsystem reaches signatures through the two-RPC gRPC surface and holds only public halves. The "what the Signing Service is not" list (in Operations) pins the shape of that boundary verbatim against ../../../README.md#signing-service.

Pages

  • Deployment scoping and rotation — the (saas | selfhosted-single | selfhosted-multi) profile matrix, the three-state rotation lifecycle (active, rotating, retired), and the rotation overlap window invariant.
  • KeyProvider contract and adapters — the outbound port every back-end implements (software, PKCS #11 HSM, cloud KMS, test double) plus the production HSM wiring rules.
  • Security invariants — the no-private-half invariant in the core binary, the mTLS peer-identity gate, and the one-entry-per-RPC audit contract.
  • gRPC surface and operations — the two RPCs and their stable error strings, the NetworkPolicy topology, the operator debugging workflow, and the explicit "what the Signing Service is not" boundary.

See also

Ubiquitous language

The terms below travel together across the Go code, the audit log, the .proto contract, and operator-facing tooling. Names are preserved verbatim in error messages and structured log attributes.

TermDefinitionCode anchor
SigningKeyThe aggregate that owns a single signing-key row: (scope, key_id, state, valid_from, valid_until, public_bytes, provider_handle_id). Carries only the public half plus the opaque provider_handle_id; the private bytes never enter this process address space.internal/signing/types.go
ScopeThe compound identifier (ScopeKind, ID) where ScopeKind is platform or domain. The wire form is platform or domain:<uuid>; NewScope enforces ID == uuid.Nil iff Kind == platform.Scope, NewScope, ParseScope in internal/signing/types.go
KeyIDAn opaque URL-safe identifier of a single SigningKey row (^[A-Za-z0-9._~-]+$, ≤ 128 bytes). The zero value is always invalid.NewKeyID in internal/signing/types.go
StateThe lifecycle phase of a SigningKey: active, rotating, or retired. Transitions are funnelled exclusively through internal/signing/rotation.State constants in internal/signing/types.go; transition fns in internal/signing/rotation/state.go
RotationTransitionThe aggregate binding an outgoing OldKey and incoming NewKey to a Window. Cross-scope transitions are rejected at construction time.internal/signing/rotation/overlap.go
WindowThe (OpenedAt, ClosesAt) pair during which both halves of a rotation are advertised. Strict OpenedAt < ClosesAt.NewWindow in internal/signing/rotation/overlap.go
KeyProviderThe outbound port for the signing back-end (software, PKCS #11 HSM, cloud KMS, test double). The only code in the system that touches private key material.internal/signing/ports.go
SignRequest / SignResponseThe Sign RPC shape: canonical_bytes, scope, key_id in; raw 64-byte Ed25519 signature plus echoed key_id out.api/proto/signing/v1/signing.proto
PublicKeyRequest / PublicKeyResponseThe PublicKey RPC shape: scope, key_id in; 32-byte public_key, echoed key_id, and lifecycle state out.api/proto/signing/v1/signing.proto
canonical bytesThe exact pre-serialised byte sequence passed to Sign. The signer NEVER re-canonicalises or transforms the payload; the caller owns deterministic encoding.internal/signing/envelope/canonical.go
SPIFFE peer identityThe spiffe:// URI SAN on the caller's mTLS client certificate, extracted from peer.FromContext before any business logic runs. Drives admission through the --allow-spiffe-id allow-list.internal/signing/peeridentity/interceptor.go
audit grant / denyOne audit.Entry per admission decision: granted when the allow-list admits, insufficient_relation when the allow-list refuses (or when the domain refuses with ErrClientIdentityDenied), caveat_violation for lifecycle refusals (ErrKeyRetired, ErrRotationInProgress, ErrProviderUnavailable).internal/signing/audit_middleware.go, internal/signing/peeridentity/interceptor.go