API Reference
This page documents the external request contract (custodian client to Gateway) and the internal Gateway to host-agent to enclave frames.
Trust posture
PQ Signer is a mirror-mode ML-DSA co-signer. The Gateway authenticates its upstream custodian channel outside this JSON contract, and the request body carries an opaque user_id, not ECDSA authorization material.
The Gateway is intended to be reachable only from custodian-controlled clients or services that have already:
- authenticated the caller,
- authorized the signing operation,
- validated the message and policy.
user_id is an opaque key-selection value, not an authorization credential.
Endpoint and envelope
All external requests use a single endpoint:
POST /v1/pq-signer
Content-Type: application/json
Envelope:
{
"request_type": "key_generation",
"payload": {}
}
request_type is one of:
key_generationsign
Encoding rules
- Arbitrary byte arrays use RFC 4648 standard base64 with padding.
user_idis an opaque UTF-8 string chosen by the custodian or generated by the Gateway. After Gateway resolution it must be non-empty and no more than 256 bytes.- The API does not accept
ecdsa_pubkeyorecdsa_signaturefields. ECDSA authorization and same-message policy checks are upstream custodian responsibilities.
key_generation
Generates a new ML-DSA keypair inside the enclave, persists the wrapped private key, and returns the public key plus a birth attestation.
Request
Custodian supplies user_id:
{
"request_type": "key_generation",
"payload": {
"alg": "ML-DSA-44",
"user_id": "customer-wallet-123"
}
}
Let the Gateway mint one (omit the field or pass an empty string):
{
"request_type": "key_generation",
"payload": {
"alg": "ML-DSA-44"
}
}
Field reference
| Field | Type | Required | Description |
|---|---|---|---|
alg | string | yes | ML-DSA parameter set. Currently ML-DSA-44. |
user_id | string | no | Opaque identifier. If omitted or empty, the Gateway generates a UUIDv4 from a system CSPRNG. If supplied, validated as an opaque bounded string and forwarded unchanged. |
Behavior
- The effective
user_idis used as the DynamoDB partition key, the KMSEncryptionContext.user_id, the AES-GCM AAD input, and the birth-attestation binding input. - The custodian platform is trusted as the initial key-creation authority; there is no separate proof-of-possession challenge inside PQ Signer.
- The Gateway writes the row with a conditional
PutItem(attribute_not_exists(user_id)). A collision causes the keygen to fail with asigning-failedenvelope.
Response
{
"user_id": "customer-wallet-123",
"mldsa_pubkey": "<base64>",
"birth_attestation": "<base64 COSE_Sign1>",
"enclave_version": "0.1.0"
}
| Field | Type | Description |
|---|---|---|
user_id | string | Always echoed. Verbatim if the custodian supplied one, otherwise the generated UUIDv4. Persist this value, it is the only key-record identifier. |
mldsa_pubkey | base64 | The ML-DSA public key. |
birth_attestation | base64 | COSE_Sign1 document signed by the AWS Nitro Attestation PKI. Its user_data commits sha256(mldsa_pubkey) ‖ sha256(wrapped_dk) ‖ user_id ‖ kms_key_arn ‖ alg ‖ timestamp. The authoritative (PCR0, PCR1, PCR2, PCR8) for the keypair live inside this payload, not in a separate field, so birth attestations remain verifiable across enclave upgrades. |
enclave_version | string | Build-time release label compiled into the EIF. A fast pre-check for the custodian, with the COSE_Sign1 payload as the ground truth. |
sign
Produces an ML-DSA signature over the supplied message using the keypair identified by user_id.
Request
{
"request_type": "sign",
"payload": {
"alg": "ML-DSA-44",
"user_id": "customer-wallet-123",
"message": "<base64, decoded payload up to 64 bytes>",
"request_attestation": false
}
}
Field reference
| Field | Type | Required | Description |
|---|---|---|---|
alg | string | yes | Must match the algorithm bound at keygen. Currently ML-DSA-44. |
user_id | string | yes | Opaque identifier returned by key_generation. Non-empty, up to 256 bytes. |
message | base64 | yes | Decoded as raw bytes with no UTF-8 normalization and no JSON reserialization. Maximum decoded length is 64 bytes. |
request_attestation | bool | no | Reserved. Omitted or false uses the normal sign path. true is reserved for a future per-signature attestation feature and is not currently honored. |
Behavior
- The Gateway looks up the key record by
user_idand forwards the value to the enclave for KMS decrypt and AEAD opening. - PQ Signer performs no ECDSA verification, no ecrecover, and no internal modified-message rejection. Relying parties and custodians must verify externally that any ECDSA authorization and the ML-DSA signature cover the same message.
- ML-DSA operates over the full message, not a hash, so the complete payload reaches the enclave.
Response
{
"mldsa_signature": "<base64>",
"mldsa_public_key": "<base64>"
}
When request_attestation becomes active, the response will additionally contain:
{
"mldsa_signature": "<base64>",
"mldsa_public_key": "<base64>",
"attestation": "<base64 COSE_Sign1 with user_data = sha256(mldsa_signature)>"
}
Error contract
The public surface intentionally collapses errors into three deterministic shapes so the API does not become a behavioral oracle:
| Class | Trigger | Notes |
|---|---|---|
malformed-request | Bad JSON, missing/oversized fields, unknown request_type, wrong alg. | Returned before any DynamoDB or enclave interaction. |
signing-failed | Authorization or key-access failure: missing key record, KMS attestation mismatch, KMS encryption-context mismatch, AES-GCM AAD failure, conditional-write conflict on keygen. | All KMS and enclave-side failures collapse here. The wire response does not distinguish which check failed. |
internal-error | Unexpected internal failure: transport error, KMS service error, host-agent unavailable. | Treat as transient. |
Internal metrics, logs, and live AWS evidence (CloudTrail, Gateway logs, host-agent logs) still distinguish the enforcement layer for operators.
Internal: Gateway to host-agent
The Gateway and Nitro host-agent communicate over a private, security-group-restricted mTLS link. This contract is internal to a customer deployment; it is not part of the external API and is documented here so operators can reason about the full path.
Network boundary
- Gateway security-group egress permits TCP
8443only to the Nitro host security group. - Nitro host security-group ingress permits TCP
8443only from the Gateway security group. - The Nitro host has no public ingress.
- The Gateway has DynamoDB permissions and no wrapping-CMK access.
- The Nitro host has KMS attestation-path permissions and no DynamoDB access.
mTLS
- A single internal CA issues the host-agent server certificate and the Gateway client certificate.
- The host-agent rejects clients without a valid CA-issued client certificate. The Gateway pins the same CA.
Wire protocol
- The host-agent listens on TCP
8443with rustls server-side mTLS. - Each request is one length-prefixed JSON frame:
u32be(len) ‖ json. - Request envelope is
GatewayToHostRequest; response isGatewayToHostResponse. - Both
key_generationandsignare routed over this transport and relayed to the local enclave over vsock. - Frames carry the gateway-resolved opaque
user_idstring. The host-agent treats it as relay data; it does not authorize or derive identity from it.
Host-agent to enclave (vsock)
- Also
u32be(len) ‖ jsonframes. - The first frame from the host-agent is an
EnclaveRequest. - The enclave may then send
EnclaveToHostFrame::KmsGenerateDataKeyorEnclaveToHostFrame::KmsDecryptDataKeycontaining an enclave-derivedEncryptionContext.user_id(the exact opaque string from the request) and an NSMRecipientattestation document. - The host-agent performs the AWS KMS call with the Nitro host role and returns only
CiphertextForRecipientplus the CMK-wrapped data key. The plaintext data key is never accepted on this relay; the enclave unwraps it itself.
Commands
pq-signer-gateway serve-http --bind 0.0.0.0:8080 --host <nitro-host> --table <ddb> --ca <ca.pem> --client-cert <cert.pem> --client-key <key.pem> --kms-key-arn <arn>pq-signer-host-agent serve-mtls --cid <enclave-cid> --port 5005 --listen-port 8443 --client-ca <ca.pem> --server-cert <cert.pem> --server-key <key.pem>
End-to-end sequence (sign)
╭──────────╮ ╭─────────╮ ╭───────────╮ ╭─────────╮ ╭─────╮
│ Custodian│ │ Gateway │ │ Host-agent│ │ Enclave │ │ KMS │
│ client │ │ EC2 │ │ EC2 │ │ (EIF) │ │ │
╰────┬─────╯ ╰────┬────╯ ╰─────┬─────╯ ╰────┬────╯ ╰──┬──╯
│ POST /v1/pq-signer │ │ │ │
│ (sign payload) │ │ │ │
├───────────────────▶│ │ │ │
│ │ DynamoDB GetItem │ │ │
│ │ (wrapped_dk, │ │ │
│ │ ct_mldsa_priv) │ │ │
│ │ mTLS frame (sign) │ │ │
│ ├───────────────────▶│ vsock frame │ │
│ │ ├───────────────────▶│ │
│ │ │ │ kms:Decrypt │
│ │ │ │ (Recipient, │
│ │ │ │ EncCtx user_id)│
│ │ │◀───────────────────┤ │
│ │ │ KmsDecryptDataKey │ │
│ │ ├──────────────────────────────────────▶│
│ │ │ CiphertextForRecipient + wrapped_dk │
│ │ │◀─────────────────────────────────────┤
│ │ │ vsock relay │ │
│ │ ├───────────────────▶│ │
│ │ │ │ unwrap DK, │
│ │ │ │ AES-GCM decrypt,│
│ │ │ │ ML-DSA-sign, │
│ │ │ │ zeroize │
│ │ │ vsock response │ │
│ │ │◀───────────────────┤ │
│ │ mTLS response │ │ │
│ │◀───────────────────┤ │ │
│ HTTP response │ │ │ │
│ (mldsa_signature, │ │ │ │
│ mldsa_public_key) │ │ │ │
│◀───────────────────┤ │ │ │
Verifying a birth attestation
Use the vendor-provided pq-attest tool to verify a keygen response against the custodian's accepted release manifest and the persisted key-record metadata. See the playbook for the exact command and expected output.
pq-attest verify \
--birth-attestation <birth.b64> \
--manifest <measurement-manifest.json> \
--metadata <key-record.json>
Output includes verified_pcrs, user_data_sha256, and nitro_root_der_sha256. Any PCR mismatch, certificate-chain failure, or metadata mismatch causes pq-attest to exit non-zero.