Skip to main content

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_generation
  • sign

Encoding rules

  • Arbitrary byte arrays use RFC 4648 standard base64 with padding.
  • user_id is 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_pubkey or ecdsa_signature fields. 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

FieldTypeRequiredDescription
algstringyesML-DSA parameter set. Currently ML-DSA-44.
user_idstringnoOpaque 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_id is used as the DynamoDB partition key, the KMS EncryptionContext.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 a signing-failed envelope.

Response

{
"user_id": "customer-wallet-123",
"mldsa_pubkey": "<base64>",
"birth_attestation": "<base64 COSE_Sign1>",
"enclave_version": "0.1.0"
}
FieldTypeDescription
user_idstringAlways echoed. Verbatim if the custodian supplied one, otherwise the generated UUIDv4. Persist this value, it is the only key-record identifier.
mldsa_pubkeybase64The ML-DSA public key.
birth_attestationbase64COSE_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_versionstringBuild-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

FieldTypeRequiredDescription
algstringyesMust match the algorithm bound at keygen. Currently ML-DSA-44.
user_idstringyesOpaque identifier returned by key_generation. Non-empty, up to 256 bytes.
messagebase64yesDecoded as raw bytes with no UTF-8 normalization and no JSON reserialization. Maximum decoded length is 64 bytes.
request_attestationboolnoReserved. 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_id and 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:

ClassTriggerNotes
malformed-requestBad JSON, missing/oversized fields, unknown request_type, wrong alg.Returned before any DynamoDB or enclave interaction.
signing-failedAuthorization 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-errorUnexpected 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 8443 only to the Nitro host security group.
  • Nitro host security-group ingress permits TCP 8443 only 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 8443 with rustls server-side mTLS.
  • Each request is one length-prefixed JSON frame: u32be(len) ‖ json.
  • Request envelope is GatewayToHostRequest; response is GatewayToHostResponse.
  • Both key_generation and sign are routed over this transport and relayed to the local enclave over vsock.
  • Frames carry the gateway-resolved opaque user_id string. 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) ‖ json frames.
  • The first frame from the host-agent is an EnclaveRequest.
  • The enclave may then send EnclaveToHostFrame::KmsGenerateDataKey or EnclaveToHostFrame::KmsDecryptDataKey containing an enclave-derived EncryptionContext.user_id (the exact opaque string from the request) and an NSM Recipient attestation document.
  • The host-agent performs the AWS KMS call with the Nitro host role and returns only CiphertextForRecipient plus 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.