> ## Documentation Index
> Fetch the complete documentation index at: https://jephalabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Private Signer Service

> JEP-738 Core-to-KMS process boundary, nonce-HMAC contract, and configuration

# 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

| Process        | Holds                                                                                    | Must not hold                                                         |
| -------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `qtg.main`     | signer URL, signer HMAC credential, pinned signer address                                | AWS profile/config/credentials, KMS key ID, local/extra signer config |
| private signer | one KMS key ID, AWS credential chain, signer HMAC credential, table-limited nonce DB URL | Core/BFF/browser routes, movement policy, key selector from request   |

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:

```text theme={null}
1
METHOD
PATH
RAW_QUERY
SHA256(EXACT_BODY_BYTES)
UNIX_TIMESTAMP
NONCE
```

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:

```dotenv theme={null}
MG_REMOTE_SIGNER_KEY=qtg-kms-primary
MG_REMOTE_SIGNER_BASE_URL=http://qtg-signer:8120
MG_REMOTE_SIGNER_HMAC_KEY_ID=core-primary
MG_REMOTE_SIGNER_HMAC_SECRET=<at-least-32-byte-secret>
MG_REMOTE_SIGNER_EXPECTED_ADDRESS=0x<nonzero-40-hex-address>
MG_REMOTE_SIGNER_TIMEOUT_SECONDS=30
```

`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

```dotenv theme={null}
MG_ENV_FILE=/dev/null
MG_SIGNER_SIGNER_KEY=qtg-kms-primary
MG_SIGNER_KMS_KEY_ID=alias/qtg/primary
MG_SIGNER_AWS_REGION=ap-northeast-2
MG_SIGNER_AWS_PROFILE=qtg-signer
MG_SIGNER_HMAC_KEY_ID=core-primary
MG_SIGNER_HMAC_SECRET=<same-at-least-32-byte-secret>
MG_SIGNER_DATABASE_URL=postgresql+asyncpg://<table-limited-role>:<secret>@postgres:5432/qtg_v3
MG_SIGNER_TIMESTAMP_TOLERANCE_SECONDS=300
MG_SIGNER_NONCE_RETENTION_SECONDS=900
MG_SIGNER_MAX_BODY_BYTES=262144
```

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:

```bash theme={null}
docker compose \
  -f infra/docker-compose.yml \
  -f infra/docker-compose.stargate-kms-mainnet.yml \
  config --format json
```

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.
