Signer Boundary

The signer is the only component allowed to perform private-key operations: certificate PDF signatures, blockchain transaction signatures, defensive certificate HMACs, and decryption of agent proof envelopes. Everything else in the platform consumes the signer as a service.

Current State: In-Process Test Signer

Local and CI stacks use the in-process test signer. It is not a custody boundary.

AttributeValue
ModeSIGNER_MODE=test
Packageinternal/adapters/signer/testsigner
SeedTEST_SIGNER_SEED (shared by API and proof worker so HMAC verification is stable)
Capabilitiesproof-envelope:v1 decryption, deterministic HMACs, RSA-backed PAdES signing, placeholder blockchain transaction signing
TrustNone — no remote custody, no durable key store, no approval workflow, no hardware backing

Both SIGNER_MODE=test and AGENT_TRUST_MODE=dev are refused at startup when ENVIRONMENT, APP_ENV, or OBSERVABILITY_ENVIRONMENT is set to prod or production. Do not run production load against SIGNER_MODE=test. A dedicated signer service is the production boundary.

Remote Signer (cmd/signer)

SIGNER_MODE=remote points the backend at the remote Signer, a separate Go binary (backend/cmd/signer, image wipe-signer, chart component signer). It is the only workload that holds private keys, and it receives none of the backend environment: no database, queue or object-store credentials.

Routes

EndpointOperationBehaviour
GET /healthznoneUnauthenticated liveness, for the kubelet.
GET /v1/keys/{key_id}keysKey id, version (first 16 hex of the SHA-256 of the public key), algorithm, public key (SPKI DER), certificate chain.
POST /v1/sign/digestsignalgo: "sha256": a 32-byte SHA-256 digest, signed as given (never hashed again). ECDSA returns a DER ECDSA-Sig-Value, RSA a PKCS#1 v1.5 signature. algo: "ed25519": the message itself, pure Ed25519 (Hedera HCS transactions and anchor messages).
POST /v1/hmac, POST /v1/hmac/verifyhmacHMAC-SHA256, encoded hmac:v1:<hex> like the test signer.
POST /v1/decryptdecryptOpens an exawipe-cms-1 AuthEnvelopedData addressed to the key’s certificate (envelope.KeyDecrypter).
POST /v1/sign/pades, POST /v1/verify/padessign_pades, verify_padesPAdES with the Signer’s PAdES key: dev (self-signed) or eIDAS (key certificate chain + mandatory TSA).
POST /v1/sign/transaction—Not offered (501): HCS transactions are signed through sign/digest with algo: "ed25519".

JSON byte fields are standard base64. A refused call answers 401 (unknown caller), 403 (operation not granted, or unknown key), 400 (malformed input), 422 (an envelope or PDF the key cannot process).

Keys

One Kubernetes Secret (signer.keysSecret), one entry per file; the key id is the file name without its suffix:

FileHolds
<id>.keyPEM private key: PKCS#8, SEC 1 EC PRIVATE KEY or PKCS#1 RSA PRIVATE KEY (RSA at least 2048 bits).
<id>.crtOptional certificate chain of <id>.key, leaf first. Required for decrypt and for eIDAS PAdES.
<id>.hmacRaw HMAC-SHA256 secret, at least 32 bytes.

The Signer refuses to start on a malformed file, an unknown suffix, or a certificate that is not for its key.

Key id (backend default)TypeUsed by
pades-signing-keyRSA or ECDSA (+ .crt in eIDAS mode)Certificate PDFs
hedera-operator-keyEd25519HCS anchoring (BLOCKCHAIN_HEDERA_*_OPERATOR_KEY_ID)
agent-decryption-keyRSA ≥ 2048 + .crtProof envelope decryption (PROOF_AGENT_DECRYPTION_KEY_ID)
consumption-receipt-signing-keyECDSA or RSAConsumption receipts
wipe-hmacHMAC secret (.hmac)Defensive certificate HMACs
agents-caECDSA P-256The agents CA: the ISO issuer signs ISO certificates through sign/digest

Callers

signer.policy.clients names every caller and what it may do on which key; nothing is granted by default. A caller is recognised by the Common Name of a client certificate verified against signer.tls.clientCAKey, or by the SHA-256 (hex) of its bearer token — the token itself is never stored. Give the ISO issuer its own client, allowed sign and keys on agents-ca only.

Every call writes one signer.audit log line: caller, operation, key id, key version, SHA-256 of the request body, status.

With networkPolicy.enabled, only the API, the ingest gateway and the workers may open a connection to the Signer.

Operator Checks

SymptomFirst check
WipeSignerFailures by operationdecrypt, hmac, hmac_verify, pades_sign, or receipt_sign — see Incident response.
Proofs stuck in SIGNINGSigner reachability, SIGNER_MODE, mTLS material, key IDs, TSA URL.
PAdES verification failures on /verifyTSA reachability and signer certificate chain (PAdES trust anchors).
INVALID_HMAC on public verificationSigner key version drift between issue-time and verify-time.

Production Readiness Inputs

The signer is externally blocked until the signer team provides the following. Track these against adr/0001-remaining-backend-gaps.md item 2:

InputOwner
Remote signer protocol semanticsSigner team
mTLS material (client cert, CA)Infra
Key IDs and rotation metadataSigner team
Transaction signing behaviorSigner team
HMAC, decrypt, PAdES, TSA behaviorSigner team

The remote Signer now exists (see above); what remains is the production material itself: the keys, the eIDAS certificate chain, the TSA and the mTLS certificates of each caller.

Backend Integration Notes

When the production signer lands, the backend integration is already scaffolded:

  1. Domain services stay on SecretsProvider, KeySignerProvider, and SignatureProvider ports.
  2. The remote signer client (adapters/signer/remote) ships beside testsigner and also exposes a crypto.Signer backed by sign/digest, which the Hedera HCS adapter uses in remote mode.
  3. PAdES public certificate retrieval moves from local self-signed into signer metadata.
  4. Signer contract tests pin HMAC, digest signing, transaction signing, and proof-envelope decryption to fixed test vectors.
  5. Non-test modes fail at startup unless the remote signer URL, auth, and trust roots are configured.

Operators do not need to change anything in the application database when the signer mode flips; the change is configuration only.