# Aere PQ Secrets A secrets service on top of [Aere PQ KMS](../pq-kms/): named secrets with versions and metadata, each version encrypted under its own data key, the data key wrapped by the KMS in its hybrid envelope (X25519 + ML-KEM-768). **Every request is authorized by an AERE identity the caller presents**, never by the network it comes from, a bearer token or the host it runs on: a presentation of an [Aere Identity](../identity/) credential, made for this service (its audience), with a nonce the service issued and accepts once, within a time limit, signed by the holder's key or by a delegate of the holder, **and signed for this one request**: the digest of the operation, the secret and the whole body is inside the signed binding, so a presentation made to read `db` opens nothing else. **Every operation, allowed or refused, is an AIP-23 envelope** (`secret-access`, built by [proof-kinds](../proof-kinds/)) appended to the audit log of the [verification layer](../verify-layer/) sidecar, whose head can be signed and notarized on Aere Network. Node.js 24 only, `node:crypto` (OpenSSL 3.5), `node:http`, `node:fs`. No dependencies. The KMS can run in the same process (`AERE_KMS_ROOT_KEY`) or be the `pq-kms` server reached over HTTP (`AERE_KMS_URL` + `AERE_KMS_TOKEN`). ## What it is NOT - **Not zero trust towards its own host.** Zero trust here is towards the network and the caller: no address, token or TLS client certificate opens a secret, only a verified presentation. The process holds each data key in memory while it uses it, the service MAC key for the whole run, and, with the KMS in process, the KMS root key. Whoever controls the host, the process memory or the environment has the secrets. Nothing here attests the service's own runtime; that is Proof of runtime (`runtime` envelopes of the verification layer), to be bound to this service separately. - **An audit envelope alone proves nothing.** An entry carries no signature of the service or of the presenter: a `secret-access` envelope made up by hand is `VALID` for the reference verifier (which judges form and hash, not that the access happened), and `buildProof` refuses only values outside the kind's vocabulary. The weight is in the log: its head, signed and notarized outside the host (`attest-head`, `notarize-head` of the sidecar), fixes everything before it. **Between two attested heads, whoever holds the host can append entries or remove the last ones** (a `get` that happened can vanish, an access that did not can appear) and the chain still verifies; only a head published outside the host catches a rewrite of what it covers. - **`GET /v1/audit/head` is public**: anyone learns the number of operations so far (refusals included) and the current head. It verifies the whole log on each call (linear in its size). - **`metadata` shows the whole access list**: a holder with the `metadata` operation sees every grant of the secret, with its trusted issuers, its claim rules, its `holders` and who made it. - **Without an HSM the root is a sealed file.** The KMS root comes from the environment or from a file sealed by a PKCS#11 token (see the KMS README); the secrets service adds no key ceremony of its own. Its own file-integrity key is a KMS data key, wrapped on disk and opened through the KMS at start. - **Not audited.** No third party has reviewed this code, the identity verifier it relies on, or the KMS under it. - **One process per data directory, and the only rate limit is on nonces.** Opening a data directory takes `service.lock` (created exclusively, with the PID and the host); a second instance, in this process or another, is refused (`DATA_DIR_LOCKED`). A lock left by a process that no longer runs on this host is reported by its PID (`DATA_DIR_LOCK_STALE`) and taken over only when asked (`staleLock: 'take'`, `AERE_SECRETS_STALE_LOCK=take`), never silently; the check of the PID cannot tell a reused PID from the old one, and two instances taking over the same stale lock at the same moment are not excluded. Nonces live in process memory (120 s, at most 10,000 outstanding, at most 32 outstanding per address of origin as the HTTP server sees it, `429 NONCE_ORIGIN_FULL` above). **What remains:** an attacker with many addresses still fills the 10,000; behind a proxy every caller has the proxy's address, so the per-address cap becomes a cap for everyone (raise `AERE_SECRETS_NONCE_PER_ORIGIN` or limit at the proxy); and refused requests also write audit entries, so a flood fills the log. - **Rollback is not detected.** A data directory, or one secret file, put back to an older state that still carries a valid MAC is accepted (the same limit as the KMS). An audit log rebuilt from genesis is detected only against a head published outside the host (`verify-log --attested`, see the verification layer). - **A revoked version is gone through this service, not from the world.** `revoke-version` deletes the sealed value and the wrapped data key from the file; a copy of the file taken before, together with the KMS, still opens it. Rotating the KMS key does not re-wrap stored data keys (no `rewrap` here yet); raising the KMS key's `min_decryption_version` makes older versions unreadable. - **The status lists and revocations are what the operator puts in `trust/`.** The service reads them at every request and hands them to the identity verifier; it fetches nothing. A list that is missing or expired leaves the status unjudged, and an unjudged status is a refusal, for everyone, the admin included. - **Time is still an oracle.** A refusal says the same bytes whether the secret exists or not (below), but the time it takes is not made constant: measured on 2026-10-18, a stranger asking for a secret with three grants took a median of 9.0 ms and for a name that does not exist 9.1 ms (15 tries each; one hybrid verification per distinct pair of trusted issuers and maximum age among the grants, so secrets whose grants trust different issuers take measurably longer). ## How a request is judged 1. **Form.** The body is an object and the name matches `^[a-z0-9][a-z0-9_-]{0,63}$` and is not a Windows device name (`con`, `prn`, `aux`, `nul`, `com0`-`com9`, `lpt0`-`lpt9`). Otherwise `request-form` or `secret-name`. **These two refusals are not audited and do not consume the nonce** (they are not operations: the same presentation can be sent again to a valid name). 2. **Size.** The whole body, the presentation included, is at most 32 levels deep and 20,000 values, measured without recursion **before** anything is put in canonical form (a list nested 30,000 times would otherwise overflow the stack). Otherwise `request-form` (`REQUEST_TOO_COMPLEX`), audited. 3. **Presentation form.** The body carries a `presentation` with `binding.nonce`. Otherwise `presentation-form`, audited. 4. **The request digest** (`requestHash`, below) is computed from the request received; a body that JSON cannot carry (a value given through the library) is `request-form`, audited. 5. **Nonce.** The nonce must be one this service issued (`POST /v1/nonce`), not expired (120 s) and never seen before. It is consumed at first sight, whatever the verdict after it: a second request with the same presentation is refused even when the first one was refused. Otherwise `nonce`. 6. **Identity.** `verifyPresentation` of the identity component, on its production path, with the service's audience, the nonce, **the request digest**, the service clock, the status lists and revocations of `trust/`, no trusted issuers yet (they belong to each grant) and a freshness of 300 s. Every refusal names the identity rule: `request` (signed for another request, or for none), `audience` (made for another service), `freshness` (older than the limit), `presenter` and `presentation-signature` (signed by a key that is not the holder's or the last delegate's), `credential-revoked`, `delegation-revoked`, `delegation-claims`, `delegation-window`, and the rest of its 38. Otherwise `authorization`, with `identityRules` naming them (they say nothing about the secret: the identity is judged before any grant). 7. **Grant.** The candidates are the admin policy (every operation, every version) and the secret's grants that list the operation. Each candidate's policy is judged by `checkCompliance` of the identity component, the hybrid signatures verified once per distinct pair (trusted issuers, maximum age): the issuer must be one the policy names, the credential status must be **judged** (a grant cannot waive it: `requireStatus: false` is refused by name at write time), and every claim rule must hold on the claims shown. A grant with `holders` admits only those holders **and their delegates**; with `delegates: false`, the holder alone. The first compliant grant authorizes. **None is one answer for every secret**: `UNAUTHORIZED`, rule `authorization`, the fixed message "no grant admits this presentation for this operation on this secret (a secret that does not exist is refused the same way)", the same bytes whether the secret does not exist, has no grant that lists the operation, or has one whose rule failed. Which grants were judged and the rules each refused on go to the audit entry only (`judged`). 8. **The operation.** Only now the service says whether the secret exists (`secret-exists`, to a caller a grant admits: for a name that does not exist, the admin only), whether the version exists (`version-exists`), whether the grant reads old versions (`version-scope`: a grant reads the current version unless it says `versions: "any"`), whether the version was revoked (`version-revoked`), and so on. A message names a version only when it is an integer. 9. **The audit entry, then the change, then the result.** For an operation that changes the secret, the `secret-access` envelope is appended to the sidecar log and the log is fsync-ed **before** the secret file is written; if the entry cannot be written, nothing is changed and the result is withheld (`AUDIT_WRITE_FAILED`); if the file cannot be written after the entry, a second entry with the verdict `failed` and the same `requestHash` follows and the request is refused (`STORAGE_WRITE_FAILED`). **So a crash leaves at most an entry without its change, never a change without its entry** (the crash itself is not simulated by the tests; the order and the failed write are). For a read, the entry is written before the value leaves the process. The whole cycle of one secret (load, judge, change, entry, write) runs under a mutex of that secret, so concurrent requests on one secret are serialized even when the KMS is remote and answers in between. Every refusal names one of the service's rules, `REGULI` in `secrete.mjs` (`request-form`, `secret-name`, `presentation-form`, `nonce`, `authorization`, `secret-exists`, `value-form`, `version-exists`, `version-revoked`, `version-scope`, `grant-form`, `grant-exists`, `grant-scope`, `grant-limit`, `kms`, `storage`, `internal`); each check carries a `/*PAZA:*/` marker on its condition, in the form the conformance suite reads. ## The request a presentation is signed for ``` requestHash = sha256( canonical JSON of { v: 1, kind: "aere-secrets-request", service, operation, secret, body } ) ``` `service` is the audience; `operation` one of `put`, `get`, `metadata`, `rotate`, `revoke-version`, `grant`, `revoke`, `list`; `secret` the name (`null` for `list`); `body` **every field of the request body except `presentation`** and those that are `undefined` (`version`, `value`, `metadata`, `grants`, `grant`, `grantId`, `bytes`, `encoding`, and any other: a field added by whoever did not sign changes the digest). `value` (for `put`) enters as the base64 of its bytes: through the library a Buffer, or a string taken as its UTF-8 bytes (`valueEncoding: 'bytes'`, the default); over HTTP the base64 string as sent (`valueEncoding: 'base64'`). The canonical JSON is the identity component's (keys sorted, no spaces). `requestHash` is exported by `secrete.mjs` and is the one place both the client and the service compute it: the client passes it to `present({ ..., request })` before signing, the service computes it from what it received and gives it to the identity verifier, whose rule `request` requires the binding to carry exactly that digest (absent or other: refused). A presentation made before the field existed carries none and is refused here. The model, read on 2026-10-18: OpenID for Verifiable Presentations 1.0 (Final, 9 July 2025), `transaction_data` (section 5.1, the request parameter; section 8.4, the transaction data signed with the same key as the proof of possession of the credential) and appendix B.3.3, where the SD-JWT VC Key Binding JWT carries `transaction_data_hashes` (SHA-256 by default, over the string received). Here the verifier does not send the data to sign; it states the rule by which the holder computes it, and the digest of the request itself is what is signed. ## Grants and policies A secret's policy is a list of grants, at most 32 (`grant-limit`, at creation and at `grant`). A grant is: ```json { "id": "deployers", "operations": ["get", "metadata"], "versions": "current", "policy": { "id": "deployers-v1", "trustedIssuers": ["aere-id:..."], "require": [{ "claim": "role", "equals": "deployer" }, { "claim": "tenant", "equals": "acme" }] } } ``` `operations` is a non-empty subset of `put`, `get`, `metadata`, `rotate`, `revoke-version`, `grant`, `revoke`; `versions` is `current` (default) or `any`; `holders` (optional) is a list of `aere-id:` identities, which admits them **and any delegate they present through**; `delegates: false` (optional) admits the holder alone; `policy` is a compliance policy of the identity component (`trustedIssuers`, `require` with `equals`, `in`, `notIn`, `atLeast`, `present`; `maxAgeS` up to 3600), normalized and hashed by `definePolicy`. The id `admin` is reserved (it names the admin policy in the log). Each stored grant carries `createdBy`, the verified holder who made it. The **admin policy** (`AERE_SECRETS_ADMIN_POLICY`) is such a policy on its own: whoever satisfies it may do anything, including creating secrets and granting. **A grant made by a holder who is not the admin is an attenuation** of the grant through which the `grant` request was admitted (rule `grant-scope`, `GRANT_NOT_ATTENUATED`): its operations are among that grant's, **without `grant` and `revoke`** (only the admin hands administration on, so a grant with `grant` alone gives nothing); it reads old versions only if that grant does; it trusts only issuers that grant trusts; its policy keeps **every rule of that grant unchanged** (it may add rules), a maximum age no longer, and the status required; when that grant names holders it names holders among them; when that grant refuses delegates it refuses them too. A comparison that cannot be made safely (a rule written differently, even if stricter) is a refusal. A holder who is not the admin revokes only grants it made itself (`GRANT_NOT_OWNED`, `grant-scope`). `list` is not a per-secret operation: it returns every name for the admin and, for anyone else, the names of the secrets with a grant that admits the presentation (for any operation); when none does, it is refused with one fixed message. ## The audit entry ```json { "v": 1, "kind": "aere-proof-of-secret-access", "presentationHash": "0x...", "policyHash": "0x...", "requestHash": "0x...", "service": "https://secrets.example", "secret": "db", "version": 2, "operation": "get", "verdict": "allowed", "rules": [], "grant": "deployers", "holder": "aere-id:...", "presenter": "aere-id:...", "createdAt": "2026-10-18T12:00:00.000Z" } ``` `presentationHash` is the SHA-256 of the canonical signed binding of the presentation (it names, by hash, the credential, the disclosures shown, the delegation chain, the audience, the nonce, the time and the request; the same rule as the compliance record of the identity component). `requestHash` is the digest of the request **received** (on a substitution it differs from the request the binding names). `verdict` is `allowed`, `refused` or `failed` (allowed and entered, then the change could not be written). On a refused entry, `rules` holds the service rule followed by the identity and compliance rules that refused, and `judged` lists each candidate grant with the rules it refused on. **`holder` and `presenter` are written only when the presentation's signatures were verified** (the ids derived from the keys, the credential signed, the presentation signed by the holder or the last delegate, the chain signed); otherwise they are `null`, and what the presentation claims is written as `claimedHolder` / `claimedPresenter`, only in the form `aere-id:` + 40 hex, or not at all. Every field copied from a request is bounded (a name, an id of fixed length, a digest), and an entry is at most 16 KB; an entry of an anonymous refusal measured 831 bytes. The proof-kinds builder refuses any value outside the kind's vocabulary. The value never enters the log, nor a digest of it (a digest of a guessable value does not hide it). `verify-log`, `attest-head`, `notarize-head` and the Merkle-tree commands of the sidecar work on this log unchanged, and each entry is `VALID` for the AIP-23 reference verifier. ## Running ```sh export AERE_SECRETS_AUDIENCE= export AERE_SECRETS_ADMIN_POLICY= export AERE_KMS_ROOT_KEY=<64 hex characters> # KMS in this process (or AERE_KMS_URL + AERE_KMS_TOKEN) node server.mjs ``` | variable | default | meaning | |---|---|---| | `AERE_SECRETS_AUDIENCE` | none (required) | the audience; 1..256 printable characters without spaces | | `AERE_SECRETS_ADMIN_POLICY` | none (required) | a JSON compliance policy; a service with no admin policy cannot create a secret | | `AERE_SECRETS_DATA_DIR` | `./data` next to `server.mjs` | `service.json`, `service.lock`, `secrets/.json`, the audit log, the in-process KMS data | | `AERE_SECRETS_TRUST_DIR` | `./trust` next to `server.mjs` | `status-lists/*.json` and `revocations/*.json`, read at every request | | `AERE_SECRETS_AUDIT_LOG` | `/aere-audit.log` | the sidecar log | | `AERE_KMS_ROOT_KEY` / `AERE_KMS_DATA_DIR` | none / `/kms` | the KMS in this process (exactly one of this and `AERE_KMS_URL`) | | `AERE_KMS_URL` / `AERE_KMS_TOKEN` | none | the `pq-kms` server over HTTP, with its bearer token | | `AERE_SECRETS_KMS_KEY` | `secrets` | the KMS `encrypt` key that wraps the data keys; created if missing | | `AERE_SECRETS_HOST`, `AERE_SECRETS_PORT` | `127.0.0.1`, `8430` | listen address and port (`0` picks a free one) | | `AERE_SECRETS_MAX_BODY` | `262144` | request body limit, 65536 to 16777216 (a presentation with hybrid signatures is tens of KB) | | `AERE_SECRETS_NONCE_TTL` | `120` | nonce lifetime in seconds, 5 to 3600 | | `AERE_SECRETS_NONCE_PER_ORIGIN` | `32` | nonces outstanding per address of origin, 1 to 10000 (`429 NONCE_ORIGIN_FULL` above) | | `AERE_SECRETS_STALE_LOCK` | `refuse` | `take`: take over the `service.lock` of a process that no longer runs on this host (said on stderr) | No value from the environment, no secret value, no nonce and no key is ever printed. The server speaks plain HTTP on the loopback address by default; put a TLS-terminating proxy (the `pq-gateway`, for instance) in front of it before exposing it. ## HTTP API Every operation is a `POST` with a JSON body that carries the `presentation`, since a presentation does not fit in a header. The whole body reaches the service and enters the request digest. Errors are `{ error, rule, message }`, plus `identityRules` only when the presentation itself is not valid for this service. | method and path | body | response | |---|---|---| | `GET /v1/health` | | `{ ok, audience }` | | `POST /v1/nonce` | | `{ nonce, audience, expiresAt }` | | `POST /v1/secrets/:name/put` | `{ presentation, value (base64), metadata?, grants? }` | `{ name, version, current, createdAt }`; `grants` only when the secret is created | | `POST /v1/secrets/:name/get` | `{ presentation, version? }` | `{ name, version, current, value (base64), metadata, createdAt }` | | `POST /v1/secrets/:name/metadata` | `{ presentation }` | versions with their state, grants (the whole access list); no value, no sealed material | | `POST /v1/secrets/:name/rotate` | `{ presentation, bytes?: 16..64, encoding?: base64url\|hex, metadata? }` | `{ name, version, current, createdAt }` (a value born here; read it with `get`) | | `POST /v1/secrets/:name/revoke-version` | `{ presentation, version }` | `{ name, version, state: "revoked", revokedAt }` (irreversible) | | `POST /v1/secrets/:name/grant` | `{ presentation, grant }` | `{ name, grant, grants }` | | `POST /v1/secrets/:name/revoke` | `{ presentation, grantId }` | `{ name, revoked, grants }` | | `POST /v1/list` | `{ presentation }` | `{ secrets: [...] }`, the names | | `GET /v1/audit/head` | | `{ ok, count, head }` (public; verifies the whole log: linear in its size) | Secret names match `^[a-z0-9][a-z0-9_-]{0,63}$` and are not Windows device names; a value is 1 to 65,536 bytes; metadata is at most 16 entries of name to string, number or boolean, and `__proto__`, `constructor`, `prototype` are refused as names. The HTTP status follows the rule: 400 for `request-form`, `secret-name`, `value-form`, `grant-form`; 401 for `presentation-form` and `nonce`; 403 for `authorization`, `version-scope` and `grant-scope`; 404 for `secret-exists`, `version-exists` and a grant to revoke that does not exist; 409 for a duplicate grant and for `grant-limit`; 410 for `version-revoked`; 429 for `NONCE_ORIGIN_FULL`; 502 for `kms`; 500 for `storage`, `internal`, `AUDIT_WRITE_FAILED` and `STORAGE_WRITE_FAILED`. ## Library API ```js import { openSecrets, kmsClientHttp, requestHash } from './secrete.mjs'; import { openKms } from '../pq-kms/kms.mjs'; import { present } from '../identity/identity.mjs'; const kms = openKms({ dataDir: './data/kms', rootKey: process.env.AERE_KMS_ROOT_KEY }); // or kmsClientHttp({ url, token }) const s = await openSecrets({ dataDir: './data', kms, audience: 'https://secrets.example', adminPolicy, trustDir: './trust' }); const { nonce } = s.nonce(); // hand it to the caller; over HTTP the server passes { origin } const body = { version: 2 }; // what the caller asks for const request = requestHash({ audience: s.audience, operation: 'get', secret: 'db', body }); const presentation = present({ credential, disclosures, reveal, presenter, audience: s.audience, nonce, request }); const r = await s.get('db', { ...body, presentation }); // { name, version, current, value: Buffer, metadata, createdAt } await s.put('db', { presentation, value: 'hunter2', metadata: { owner: 'platform' }, grants: [deployers] }); // each with its own presentation await s.rotate('db', { presentation, bytes: 32 }); await s.revokeVersion('db', { presentation, version: 1 }); await s.grant('db', { presentation, grant }); await s.revoke('db', { presentation, grantId: 'readers' }); await s.metadata('db', { presentation }); await s.list({ presentation }); s.auditHead(); s.close(); // close releases service.lock ``` Refusals throw `SecretsError` with `code` and `rule`, and `identityRules` when the presentation is not valid for the service; an authorization refusal after a valid identity carries nothing else (the detail is in the audit entry). `openSecrets` also takes `staleLock` (`refuse` or `take`; `tookStaleLock` names the PID taken over) and `noncePerOrigin`. `MODULE` exports the sibling modules the service resolved (identity, compliance, proof-kinds, sidecar), so a test judges with the same code the service runs. ## Storage - `data/service.json`: the audience, the KMS key name, and the service MAC key wrapped by the KMS (`aerekms:v:...`, AAD `aeresecrets/v1/mac-key\0`). Opening the directory with another audience or key name is refused. - `data/service.lock`: `{ pid, host, since }` of the instance that holds the directory, removed by `close()`. - `data/secrets/.json`: the grants (each with `createdBy`), `current_version`, and per version `created`, `state`, `metadata`, `created_by`, `kms_version`, `dek_wrapped` (the data key in the KMS envelope, AAD `aeresecrets/v1/dek\0\0`) and `sealed` (`base64(iv(12) || AES-256-GCM ciphertext || tag(16))` of the value under that data key, AAD `aeresecrets/v1/value\0\0`). The whole file carries `mac = HMAC-SHA-256(service MAC key, canonical JSON)`; a grant edited on disk is refused (`SECRET_FILE_TAMPERED`), reported only to a caller the admin policy admits, and audited. Files are written atomically (temp file, fsync, rename; the temp file removed when the rename fails). - The KMS audit log records every `datakey` and `decrypt` (key name, version, verdict), never a value; the sidecar log records every operation of this service. ## Where the shape comes from Read on 2026-10-18 (public documentation): HashiCorp Vault KV version 2 (a secret is versioned; soft delete, undelete and destroy; metadata with `custom_metadata`, `max_versions`, check-and-set), Vault policies (path-based, deny by default, capabilities per operation) and Vault audit devices (every request and response logged, sensitive strings written as keyed hashes, a request refused when no audit device can write); AWS Secrets Manager (versions identified by staging labels `AWSCURRENT`, `AWSPREVIOUS`, `AWSPENDING`, the value encrypted under a KMS key); Google Secret Manager (a secret holds metadata and versions, IAM per secret, CMEK); Azure Key Vault secrets (versions in the identifier, `enabled` / `nbf` / `exp` attributes, soft delete and purge protection, per-principal permissions); OpenID for Verifiable Presentations 1.0 (`transaction_data`, above). What is kept here: named secrets with numbered versions and metadata, a current version, irreversible revocation of a version, a per-secret policy that names operations, envelope encryption through a KMS, and an audit entry per request, refusals included, written before the change and before the result and never carrying a value. What is different: the principal is not a token, a role or an IAM identity bound to the cloud, but a post-quantum credential presented for this service and this request, bound to a fresh nonce, with delegation and revocation judged offline from files; and the audit log is a chain of AIP-23 envelopes anyone can verify with the reference verifier and anchor on Aere Network. What is not here: leases and dynamic secrets, check-and-set, rotation hooks that reach into a database, replication, soft delete with restore. ## Tests ```sh node test/proba-secrete.mjs # the test suite (AERE_VERIFY_PROOF= for the envelope test) node test/control-negativ-secrete.mjs # plants defects in a copy; each must turn named tests red (four workers) node test/control-negativ-secrete.mjs --autoproba # decoy plants; the control itself must report each as a failure node test/control-negativ-secrete.mjs --ancore # only checks that every planting's anchor appears exactly once ``` `test/proba-secrete.mjs` makes every key at run time (an issuer, an admin, a deployer, a reader, a rotator, an auditor, a granter, a phone the deployer delegates to, a stranger who issues himself an admin credential, a revoked holder, a holder whose credential names no status list). Its client signs each presentation for the request it sends (the digest above), as an honest client does; a presentation an attack touches after making it leaves untouched, and one made `pentru` (for) a stated request and sent elsewhere is a substitution. It measures: the authorized flow; each refusal by name (no presentation; a nonce reused, never issued, or expired; a presentation for another service; one made ten minutes ago; one signed by a stranger's key; claims outside the policy; an operation outside the grant; a revoked credential; a credential whose status cannot be judged, the admin's included once the list is removed; a delegate in and out of its scope, and after a revocation is put in `trust/`); rotation with the old version readable only under `versions: "any"`; revocation of a version, the sealed material gone from disk; grant and revoke, duplicates, malformed grants, `holders`; `list`; the value searched as a string, base64, base64url and hex in every file under the data directory, in the KMS audit log, in the sidecar log and in the server's stdout and stderr, with a positive control of the scanner; a grant edited on disk refused; every operation an audit entry with the fields above and the signed binding's hash; the chain intact after 100 mixed operations; the envelopes `VALID` for the AIP-23 reference verifier (and one touched after hashing `INVALID`); the HTTP server in process, started from the command line and with the `pq-kms` server as a remote KMS; that every declared rule has its marker. And, since the adversarial review of 2026-10-18: a presentation signed for `get db` refused for `revoke` on db, for `get` on db2, for `get db` with a field added, and over HTTP on `revoke-version` and on a `put` whose value changed on the way (identity rule `request`); a presentation with no request refused; attenuation (operations, `grant`/`revoke` not handed on, versions, issuers, policy rules, maximum age, `holders`, `delegates`), revoking only one's own grants, the two self-grant attacks of the review refused; the authorization refusal the same bytes for a secret with three grants, with none and for a name that does not exist, through the library and HTTP, with the detail in the log; verified and claimed ids in the log, an anonymous entry under 2 KB; 8 concurrent `put`s with the remote KMS giving versions 2 to 9, all readable, and a `grant` concurrent with a `rotate` both kept; the directory lock (same process, another process, a stale lock refused and taken over only when asked); a binding nested 30,000 times and a body of 25,000 values refused and audited, through the library and HTTP; Windows device names and reserved metadata names; a 5,000-character version not reflected; the id `admin` reserved; the cap of 32 grants, and a `get` through the 32nd grant reading the presentation's signature three times (the size check, the identity, one pair of issuers and age for the 33 candidates); 32 nonces per address then `429`; the entry written before the file and a `failed` entry after a write that fails; `delegates: false`; the vocabulary of the envelope kind. `test/control-negativ-secrete.mjs` copies the service, the KMS, the identity verifier, the compliance checker, proof-kinds and the sidecar into temporary trees with the same layout, plants one defect at a time and requires the named tests to fail for the named reason, reading the suite's JSON output, not its exit code, with three states (red as required, green although planted, broken); the originals must be byte-identical afterwards. The 43 plantings: the 17 of the first version (the nonce not consumed or not judged; the service's audience replaced by the presentation's; delegation revocations not read; a grant allowed to waive the status; grant operations not checked; the policy verdict ignored; a revoked version not refused; old versions read without `versions: "any"`; the server printing the value; the value stored in clear; the file written before the audit entry; in the identity verifier the presenter and the freshness checks removed; in the compliance checker the status not required; refusals not logged; the presentation hash not the signed binding's) and 26 for the review: the request not compared by the identity verifier, not given by the service, its digest not covering the body; attenuation off, `grant`/`revoke` handed on, other holders' grants revoked; grant names in the refusal message or in the error; claimed ids unfiltered, claimed ids written as verified; the mutex not awaited; no directory lock; a stale lock taken silently; no size check; Windows names and reserved metadata keys accepted; the version reflected; the id `admin` allowed; no cap at creation, no cap at `grant`; no verification cache; no per-address nonce cap, the server not passing the address; no `failed` entry; `delegates: false` ignored; the kind's vocabulary not judged. Measured on 2026-10-18 (UTC), Windows 11, Node.js 24.14.1, OpenSSL 3.5.5, after the repairs of the adversarial review: 43/43 tests pass (about 7 s), with the reference verifier of the development repository and with the served one; the negative control catches 43/43 plantings on the named tests for the named reasons, the originals byte-identical afterwards; its self-test gives 5/5 decoys their failure verdict. The nine attack scripts of the review, run again with their harness made an honest client (it signs each presentation for the request it sends; the attacks that state an intent sign for it), leave 3 of the 17 attacks that succeeded before still succeeding, all three declared above (an envelope made up by hand is `VALID`; an entry appended or the last ones removed between two attested heads); the same adapted harness run on the code before the repairs reproduces all 17, plus the two it measures that the original did not (the order of the entry and the write, and the per-address nonce cap). Before the repairs: 28/28 tests, 17/17 plantings, 5/5 decoys. Not measured: Linux or macOS (file modes `0o600`/`0o700` are not applied on Windows), throughput and latency under load, two processes racing for the same stale lock, a crash between the audit entry and the write (the order is measured, the crash is not), side channels other than the timing above, a physical HSM under the KMS, a real deployment behind a TLS proxy.