Skip to main content

Private Signer Service

JEP-738 removes AWS KMS authority from the long-running, browser-reachable qtg.main process. Core registers RemoteSignerProxy; a separate private FastAPI process owns exactly one AwsKmsEvmSigner and the AWS credential chain. This implementation is a no-live foundation. Its presence does not authorize deployment, migration, KMS access, signing, broadcast, or fund movement.

Process boundary

The signer service has no published host port in infra/docker-compose.stargate-kms-mainnet.yml. Only the private Compose network can reach port 8120. The overlay mounts the AWS profile directory on qtg-signer, never qtg-mainnet. Lighter secure withdraw remains unregistered under this remote plane. JEP-821 owns remote KMS backend attestation for that lane; JEP-738 does not weaken its concrete KMS gate.

HTTP surface

  • POST /sign accepts the current required SignRequest and returns SignResult. Supported formats are movement evm_tx/eip712_digest, strict-SIWE eip191_message, and typed-data hyperliquid_approve_agent_eip712_json (the latter requires a matching HyperliquidApproveAgentIntent); strict-SIWE requires a matching MessageSigningIntent and canonical base64: original bytes.
  • GET /health returns exactly ok, signer_address, chain_families, and protocol_version.
  • OpenAPI, Swagger, and ReDoc are disabled.
  • Both routes require the same signer-specific nonce-HMAC authentication.
The v1 canonical string is newline-delimited:
Headers are X-QTG-Signer-Version, X-QTG-Signer-Key-Id, X-QTG-Signer-Timestamp, X-QTG-Signer-Nonce, and X-QTG-Signer-Signature. The default freshness window is 300 seconds. For eip191_message, AwsKmsEvmSigner independently decodes the canonical base64 payload, recomputes SHA-256 and the EIP-191 version 0x45 decimal-byte-length digest, and compares the message intent before KMS I/O. Opaque digest-only requests, malformed encoding, intent mismatch, and unsupported local-private-key fallback fail closed. After MAC verification, the server commits (key_id, nonce) to public.signer_request_nonces before routing or KMS I/O. The uniqueness constraint is the multi-process replay arbiter. Missing schema, unavailable PostgreSQL, duplicate nonce, malformed request, and oversized body all fail closed before signer I/O.

Core configuration

All signer-plane values are required together:
MG_REMOTE_SIGNER_AUTH_TOKEN is retired. Any non-empty value fails bootstrap. Before every /sign, the proxy authenticates /health and requires its address to match MG_REMOTE_SIGNER_EXPECTED_ADDRESS. qtg.main rejects non-empty MG_LOCAL_SIGNER_* values and non-empty MG_EXTRA_SIGNERS_JSON. CEX-only deployments may leave every remote signer value empty.

Signer-service configuration

Optional signer-only inputs are MG_SIGNER_AWS_ENDPOINT_URL and MG_SIGNER_KMS_TIMEOUT_SECONDS. Unknown MG_SIGNER_* variables fail startup. Nonce retention must be at least twice the timestamp tolerance so a future-skewed accepted request cannot become replayable while its timestamp remains valid. Startup proves all of the following before the service accepts requests:
  1. configuration is complete and bounded;
  2. public.signer_request_nonces is queryable;
  3. KMS key metadata/state is valid;
  4. signer address and EVM chain-family health are valid.
Errors and health never include payloads, SIWE statements/URI paths/resources/nonces, HMAC values, signatures, KMS IDs/ARNs, AWS account/profile/region, provider bodies, or provider exception text. The signer service does not own browser identity, wallet policy, job approval, or retry decisions; Core commits that evidence before invoking /sign.

Migration and database privilege boundary

Revision 0012_signer_request_nonces is additive and global. It creates only the persistent replay table and timestamp index. A downgrade refuses to erase non-empty nonce evidence. The repository supplies the schema, not production grants. The deployed signer DB role should be limited to SELECT, INSERT, and retention DELETE on public.signer_request_nonces plus the owned sequence. Applying the migration or grants is an operator action outside this document.

Safe verification

Render the topology without starting it:
Verify that qtg-mainnet has no AWS mount or AWS/KMS inputs, qtg-signer has no ports entry, and only the signer has the read-only AWS profile mount. Do not run up, call KMS, or send a sign request merely to validate packaging.

HMAC rotation

The current service accepts one HMAC key only; there is no undocumented dual-key overlap. Coordinate a bounded maintenance window:
  1. stop Core signing dispatch;
  2. generate a new 32-byte-or-longer secret and a new key ID;
  3. update both Core and signer configuration;
  4. restart the signer and confirm startup readiness;
  5. restart Core and confirm authenticated signer health;
  6. remove the old secret from the secret manager and record the rotation.
A failed readiness check leaves signing unavailable. It never justifies falling back to Bearer auth or an in-process signer.

Explicit one-off tools

Reviewed operator CLIs may still construct AwsKmsEvmSigner directly with MG_LOCAL_SIGNER_*. Those commands retain their existing acknowledgements and live-operation approval boundaries. They do not run inside qtg.main and do not claim JEP-738 dispatcher durability by themselves.