← Back to blog
Launch·11 min read

Attested Signing

Every PostQ hybrid signature can now carry proof of the enclave that produced it — cryptographically bound to the payload hash and the signature hash. No more “trust the API.”

A month ago we shipped the PostQ Hybrid Signer: three SDK calls to mint a managed key and produce composite ML-DSA + Ed25519 signatures over your payloads. The math is sound and the surface is small — but it has the same weakness every managed signing service has. When you call POST /v1/sign and a signature comes back, you’re trusting that PostQ’s API actually ran the signing code we said it ran, on the binary we said it ran, with the key custody we said we’d enforce.

That trust is reasonable. It is not verifiable. And “verifiable” is the entire point of cryptography.

Today we’re shipping Attested Signing: every PostQ hybrid signature can now ride alongside a remote-attestation document produced inside the enclavethat signed it. The attestation document carries the hash of the enclave image, a monotonic counter, a fresh nonce, the timestamp — and critically, the hash of the payload that was signed and the hash of the signature that was returned. You can pin the policy on your end, and re-verify the whole bundle from @postq/sdk, the postq CLI, or the dashboard.

Why

Hybrid signatures answer “is this payload intact?” They don’t answer “was this signature produced by the binary I expected, on hardware I trust, using a key that never left its enclave?” That second question is the one a security reviewer actually asks when they’re reading your release pipeline. Without a verifiable answer, every managed signing service collapses to the same trust model as a long-lived API token.

Remote attestation is the standard answer. A TEE (AWS Nitro Enclave, Azure Confidential VM, GCP Confidential Space) produces a signed document over “here’s the image hash of what I’m running, here are my PCR values, here’s a nonce you gave me.” The document is signed by a root key whose chain terminates at the cloud vendor. You verify the chain, you check that the image hash is on your allow-list, and now you know what code produced the output.

The catch is that the standard pattern attests the enclave, not the operation. Nothing in a vanilla attestation doc says “and the thing I just signed for you is X.” A malicious operator could attest a clean enclave, then route your sign request to a different one. PostQ Attested Signing closes that gap by signing the attestation claims and the operation bindings in the same atomic step inside the enclave.

What we shipped

The release has five moving pieces:

  1. postq-enclave: a small Go binary that holds the hybrid signing keys, exposes a local HTTP interface to the API, and produces a JWS-shaped attestation document on every sign. The Phase 1 backend (mock) signs the doc with a local Ed25519 root; the same code path will accept real Nitro, Azure MAA, and GCP Confidential Space attestations as those backends land.
  2. Attestation policies in the API: a per-org resource at /v1/attestation-policies describing which vendor, which image hashes, which root public key, and optionally a maximum doc age the org will accept. Policies are bound to keys at create time; enclave-kind keys without a bound policy are rejected.
  3. Sign-path enforcement: every POST /v1/signagainst an enclave-kind key now verifies the returned attestation doc under the key’s bound policy before the API persists the signature. The response includes an attestationblock with the doc, the vendor, and the verifier’s verdict. Successes are logged to the ledger as signature.attested; failures land as attestation.violation and (if the policy has enforce: true) the sign call fails with a 422.
  4. Client surface: verifyAttestationDoc() in @postq/sdk, postq attest verify in the Go CLI, and a policies page in the dashboard. All three speak the same JWS-shaped wire format and produce the same verdict.
  5. Cross-tool smoke test: a CI-ready script in apps/api/scripts/smoke-cli-attest.sh that boots the enclave, mints a policy, signs a payload, verifies the doc with the CLI, and exercises the wrong-image rejection path (exit 2). Green end-to-end today.

The wire format

Attestation documents are JWS-shaped: base64url(header) . base64url(payload) . base64url(sig). That gives us three things for free: opaque to operators (the doc is just bytes on the response), standard tooling to parse, and a natural extension point as the real cloud TEEs come online (Nitro COSE, Azure JWT, GCP OIDC all look JWS-ish from a parsing standpoint).

The payload claims look like this:

{
  "vendor": "mock",
  "imageHash": "sha256:8f2e...",
  "counter": 17,
  "nonce": "k4c2yE...",
  "iat": 1748438400,

  "payloadSha256": "sha256:6c1f...",
  "sigSha256":     "sha256:a93d...",

  "rootPublicKeyB64": "MCowB..."
}

The two bindings — payloadSha256 and sigSha256— are what turn an enclave attestation into an operationattestation. The verifier re-hashes the payload you pass in and the signature the API returned, and refuses the doc if either binding doesn’t match. A malicious operator who diverts your sign request to a different enclave can’t produce a doc that re-binds to your exact bytes.

Using it

Pin a policy out of band — the image hash and root public key are things you should know from your build pipeline and enclave provisioning, not things you should fetch live from the server you’re trying to verify. Then bind a key to it, sign as usual, and re-verify the doc on the client.

import { PostQ, verifyAttestationDoc } from "@postq/sdk";

const pq = new PostQ({ apiKey: process.env.POSTQ_API_KEY! });

const policy = await pq.attestationPolicies.create({
  name: "release-signing-prod",
  vendor: "mock",
  matchRules: {
    allowedImageHashes: [process.env.ENCLAVE_IMAGE_HASH!],
    rootPublicKeyB64:   process.env.ENCLAVE_ROOT_PUBKEY_B64!,
  },
  maxDocAgeSeconds: 300,
  enforce: true,
});

const key = await pq.hybridKeys.create({
  name: "release-signing",
  algorithm: "mldsa65+ed25519",
  pqProvider: "enclave-mock",
  attestationPolicyId: policy.id,
});

const sig = await pq.sign({ keyId: key.id, payload: artifact });

// Zero trust in the API's own verdict — re-check locally.
const verdict = await verifyAttestationDoc({
  docB64: sig.attestation!.docB64,
  vendor: sig.attestation!.vendor,
  payload: artifact,
  signatureB64: sig.signatureB64,
  policy,
});
if (!verdict.ok) {
  throw new Error("attestation rejected: " + verdict.reason);
}

From the CLI — useful in CI gates where you want a hard exit code rather than a thrown exception:

postq sign --key release-signing --in ./build/artifact.tar > sig.json

postq attest verify \
  --policy ./policy.json \
  --sign-result ./sig.json \
  --payload ./build/artifact.tar
# exit 0 = verified, exit 2 = policy violation, exit 1 = error

The dashboard has the same view at Settings → Attestation Policies: create/edit/delete policies, browse signatures grouped by policy, and see the live ledger stream of signature.attested and attestation.violation events.

What we’re not claiming

The Phase 1 backend is the mock enclave: a real binary, real Ed25519 root, real JWS-shaped docs, real bind-to-payload-and-signature claims. It is nothardware attestation. The honest pitch is “today this proves which binary signed your payload, assuming you trust our process for provisioning the enclave; tomorrow this proves it against AWS, Azure, or GCP’s root of trust.” The verifier interface, wire format, policy schema, ledger events, and client surface are all production today; the cloud-vendor verifiers slot in behind the same vendor field.

The four reserved vendors:

  • aws-nitro-enclave— PCR0/PCR1/PCR2 allow-list, AWS Nitro root CA chain
  • azure-confidential-vm— Microsoft Azure Attestation (MAA) JWT validation, TPM-reported PCRs
  • gcp-confidential-space— Confidential Space OIDC token, image digest pinning
  • mock— what ships today

We chose to ship the operation-binding semantics, the policy model, and the full re-verifiable client surface first — not because the hardware backends don’t matter, but because the hard part of attested signing isn’t the TEE, it’s the glue. Get the glue right and swapping the root of trust is mechanical.

Try it

Three commands, end to end:

npm i @postq/sdk
brew install PostQDev/tap/postq

# Then read:
#   https://postq.dev/product/attested-signing
#   https://postq.dev/docs#attested-signing

Hybrid signing answered “is this payload safe against a quantum adversary?” Attested Signing answers “and was it signed in a place I trust?” That’s the second of the two questions every signing pipeline has to answer. Now both of them have a one-line, machine-checkable answer.