Key protection levels

On this page attestation means remote attestation in the hardware sense, a signed measurement of the machine that holds the key, produced by that machine. That is a different thing from Asqav's Control Attestation, which records which enforcement controls ran on a sign, and from a Governance Attestation, which is an evidence pack about an agent's trust posture. Those two are Asqav features. Remote attestation is not.

Three different protections get sold under overlapping words, and the difference only shows up when something goes wrong. The capability levels below separate them. Every claim here names the file it came from, read at commit a8d0e7d0.

Capability level Where the private key lives Private key during a signature Offered
1. Envelope encryption Asqav's database, wrapped under a master key held in the application environment plaintext inside the Asqav process Yes. The default on every plan
2. Nonexportable signing the customer's AWS KMS or GCP KMS never inside the Asqav process Yes. Enterprise, opt in per agent
3. Remote attestation not applicable, it is a statement about a machine rather than about a key not applicable No

Level 1. Envelope encryption

Every agent gets this unless you ask for something else.

When an agent is created without use_cloud_kms, Asqav generates the ML-DSA keypair inside its own process and stores the secret key encrypted with AES-256-GCM (src/asqav_cloud/api/routes/agents.py:529-547). The stored blob is marker(4) + key_id(1) + nonce(12) + ciphertext + tag(16) (src/asqav_cloud/api/crypto.py:211-216). The wrapping key is a single 32-byte value read from the environment as ASQAV_WEBHOOK_ENCRYPTION_KEY (src/asqav_cloud/api/config.py:294-296).

What it protects against

Someone who obtains the database and nothing else. The secret_key column holds ciphertext, and the key that opens it is not in the database.

A ciphertext copied out of one agent's row and pasted into another. The agent id is bound into the GCM authentication tag as associated data, so the copy fails to decrypt (agents.py:546, crypto.py:219-232).

What it does not protect against

To produce a signature, Asqav decrypts the key back into its own memory as plain bytes (agents.py:281-294, calling decrypt_secret_key at crypto.py:257). The master key sits in the environment of that same process. Anyone who can read that process or its environment while it runs can recover the agent's private key.

Level 1 is not nonexportable signing

A key wrapped under a key encryption key can be unwrapped, because unwrapping is the point of wrapping it. Envelope encryption is sometimes described as a key that never leaves. It does leave. It comes back into a process every time it is used.

What it records

Every local decrypt writes a DECRYPT row to the key access log before the signature is produced (agents.py:345-356). The signing paths pass reraise_db_errors=True, so a failed audit write surfaces as an error instead of being dropped (agents.py:354, agents.py:2653-2665). Rows are hash-chained, each carrying the previous row's hash, so a removed row is detectable (crypto.py:411-434). Key use is auditable at this level. It is not prevented.

Rotation

A prior master key can be held as ASQAV_WEBHOOK_ENCRYPTION_KEY_PREVIOUS so rows written before a rotation still open (config.py:298-300, crypto.py:88-99). Each ciphertext names its own key with a one-byte id derived from the key material (crypto.py:61-63), and a blob carrying an unknown id is refused rather than guessed at (crypto.py:226-229).

Level 2. Nonexportable signing

Enterprise, opted into per agent by sending use_cloud_kms=true at creation (agents.py:494). Without the cloud_kms tier feature the request is refused with 403 (agents.py:496-500, src/asqav_cloud/api/tier_config.py:47). With no KMS configured on the server it is refused with 503 (agents.py:503-507).

On this path Asqav never holds a private key at all. It asks the KMS to create one and keeps a JSON reference holding the provider, the key id, the key version and the resource name (agents.py:514-520). The local secret_key_encrypted variable stays None (agents.py:491) and that None is what gets written to the secret_key column (agents.py:554).

Why the key cannot be exported

The entire interface Asqav has to a KMS is four methods: create_signing_key, sign, get_public_key and destroy_key (src/asqav_cloud/core/kms.py:117-138). None of them returns private key material. There is no call the server could make to get the key out, no decrypt and no data-key export.

AWS keys are created with KeySpec="ML_DSA_65" and KeyUsage="SIGN_VERIFY" (kms.py:175-177), and signing calls the KMS Sign API with SigningAlgorithm="ML_DSA_SHAKE_256" (kms.py:212-217). AWS documents that the KMS keys you create are protected by FIPS 140-3 Security Level 3 validated hardware security modules (AWS KMS developer guide). GCP keys are created with purpose ASYMMETRIC_SIGN, algorithm PQ_SIGN_ML_DSA_65 and protection level HSM (kms.py:310-316). The GCP client notes that this algorithm needs that provider's post-quantum preview access (kms.py:297-301), so check with GCP before planning around it.

No fallback to a local key

_invoke_signer branches on whether the agent carries a KMS reference (agents.py:334). When the KMS call fails the request ends as HTTP 500 (agents.py:336-342). It does not retry against a local key, because for a level 2 agent there is no local key to retry against. The countersign path uses the same exclusive branch (agents.py:2653).

The branch is closed from the other side too. _reconstruct_keypair raises HTTP 400 when secret_key is null (agents.py:281-287), and _sign_with_kms raises when the reference is missing (agents.py:303-304). Neither half can quietly stand in for the other.

kms_key_ref is assigned in exactly two places in the whole route module, both of them inside agent creation (agents.py:514, agents.py:555). Nothing later in the lifecycle attaches a KMS reference to an agent that already holds a local key.

What it does not protect against

Someone who controls the Asqav process while it holds working KMS credentials cannot take the key, but can ask the KMS to sign bytes of their choosing. The message is assembled server-side and handed to the KMS client unchanged (agents.py:297-309). Nonexportable means the key cannot be copied. It does not mean the key cannot be used.

It also says nothing about the machine that made the request. That is level 3, and it is not on offer.

Delegated child agents are level 1

A delegation creates a child agent with a freshly generated local keypair, encrypted at rest under the same master key as any other level 1 agent (agents.py:6449-6464). The child is given no kms_key_ref, so the parent's KMS configuration does not carry over. Delegating from a KMS-backed agent puts the child's key in Asqav's database.

The scope of these claims

Everything above describes what the code does at agent creation and at signing time, read at commit a8d0e7d0. It is not a report on how any particular production agent is configured. To check a specific agent, read the uses_cloud_kms field that the API derives from that agent's stored key reference (agents.py:269) rather than taking this page's word for it.

Level 3. Remote attestation

Asqav does not offer it.

No receipt field carries an attestation result. The SDKs say so in their list of what a receipt does not prove: "Not that the environment was intact. No field here carries a remote attestation result." (asqav-sdk/python/README.md:163, asqav-sdk/typescript/README.md:200).

Neither repository claims SGX, TPM, TEE or Nitro Enclave support anywhere. Measured rather than asserted: at commit a8d0e7d0 a sweep over 1823 source and documentation files across both Asqav repositories matched those four terms in zero files, while the same sweep on the same instrument matched KMS in 74 files and 502 places, which is the control proving the sweep fires. The only place either repository uses those four words now is this page.

What that costs a reader: a receipt tells you which key signed, and at level 2 it tells you that key cannot be copied out of the HSM. Nothing in it tells you the caller was the program you expected or that the host was healthy at the time.

Customer-side enclaves are a separate question with a real answer. If the key and the payload have to stay inside your own boundary, the signer can run as a container you deploy. See Self-hosted Signer and Out-of-process signing.

Choosing a level

An auditor asking where the signing key is stored is answered by level 1.

A customer asking that Asqav be unable to produce the key is answered by level 2, and is not answered by level 1.

Anyone asking for proof that the signing host was not tampered with is answered by neither, and no Asqav document should suggest otherwise.

Related