pq-secrets: a zero-trust secrets service on top of pq-kms (each request authorized by an Aere Identity presentation signed for that exact request, per-secret policy whose non-admin grants can only narrow, the same refusal for every unauthorized caller, every operation a signed AIP-23 secret-access envelope); identity: a presentation optionally signed for one exact request; proof-kinds: secret-access with requestHash; conformance 240 rules, 626 vectors

This commit is contained in:
Aere Network 2026-10-19 00:30:58 +03:00
parent 8c7d72c1a7
commit ad16b1f69b
97 changed files with 4258 additions and 133 deletions

View File

@ -12,7 +12,7 @@ wrote it.
Paths that DO resolve are written relative to a side-by-side checkout of the Aere repositories, so they begin with a
repository name, for example `aere-contracts/contracts/pqc/AereCryptoRegistry.sol`.
Unresolvable distinct paths in this repository: **16**.
Unresolvable distinct paths in this repository: **19**.
- `ContainerApps_ListBySubscription.json` cited in: aere-quantum/verify-layer/fixturi-cloud/README.md
- `VirtualMachine_List_MaximumSet_Gen.json` cited in: aere-quantum/verify-layer/fixturi-cloud/README.md
@ -21,12 +21,15 @@ Unresolvable distinct paths in this repository: **16**.
- `btc/antete.mjs` cited in: aere-quantum/exposure-report/README.md
- `data/audit.log` cited in: aere-quantum/pq-kms/README.md
- `data/root-check.json` cited in: aere-quantum/pq-kms/README.md
- `data/service.json` cited in: aere-quantum/pq-secrets/README.md
- `data/service.lock` cited in: aere-quantum/pq-secrets/README.md
- `fixturi-cloud/deriva.mjs` cited in: aere-quantum/README.md
- `libs.versions.toml` cited in: aere-quantum/crypto-inventory/README.md
- `plic-aip23.json` cited in: aere-quantum/exposure-report/README.md
- `proba-client-real.mjs` cited in: aere-quantum/readiness/README.md
- `raport.json` cited in: aere-quantum/exposure-report/README.md
- `raport.md` cited in: aere-quantum/exposure-report/README.md
- `service.json` cited in: aere-quantum/pq-secrets/README.md
- `src/azure-cli/azure/cli/command_modules/vm/custom.py` cited in: aere-quantum/verify-layer/fixturi-cloud/README.md
- `verify.mjs` cited in: aere-quantum/exposure-report/README.md
- `wallet.json` cited in: aere-quantum/agents/x402/README.md

View File

@ -9,10 +9,11 @@ notarization command of the verification layer and for the agents' x402 wallet (
|---|---|
| [`pq-gateway/`](pq-gateway/) | a TLS 1.3 terminating proxy in front of any HTTP service, with the hybrid key exchange X25519MLKEM768: `hybrid-only` refuses a classical client at the handshake, `hybrid-preferred` keeps it working; optional client authentication with ML-DSA certificates |
| [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 |
| [`pq-secrets/`](pq-secrets/) | a secrets service on top of pq-kms/: named secrets with versions and metadata, each version under its own data key wrapped in the KMS hybrid envelope; every request is authorized by a presented Aere Identity credential signed for that exact request (audience, the operation, the secret and the whole body under the holder's signature; a nonce the service issued and accepts once; a time limit; the holder's key or a delegate's; delegation and revocation judged offline) and by a per-secret policy (grants made by non-admins can only narrow, never widen), never by the network, a bearer token or the host; every operation, allowed or refused, is a `secret-access` AIP-23 envelope in the verification layer's audit log; it says what it is not (not zero trust towards its own host, no HSM of its own, rollback not detected, single process, no rate limit) |
| [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL |
| [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Kotlin, Swift, Go, Rust, PEM blocks, dependency manifests including Cargo.toml, Package.swift, Podfile and Gradle): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow |
| [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and from 2026-10-18 the same for what the operator's own cloud CLI exports (AWS ECS, EC2, Lambda; Azure VMs, Container Instances, Container Apps, Web Apps; GCP Compute Engine, Cloud Run), read from the export file and never from an account, and the head of the chain can be signed by the operator (ML-DSA-65) and notarized on Aere Network under a post-quantum validator certificate; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth); the same log as a Merkle tree (RFC 9162): signed tree heads, and inclusion and consistency proofs of a few hashes that anyone checks without the log |
| [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content |
| [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fifteen proof kinds (`secret-access` since 2026-10-18), one envelope format, digests instead of raw content |
| [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass |
| [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again |
| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, either holding the payment key for the owner or co-signing from a 2-of-2 contract wallet that neither the agent nor the owner can spend alone; both run on the public testnet with their evidence; and the chain as the witness of a ledger: a notarized head, read through the AIP-23 verifier under a post-quantum certified anchor, bounds entry times from below (a backdated entry is caught) |
@ -21,7 +22,7 @@ notarization command of the verification layer and for the agents' x402 wallet (
| [`market-settlement/`](market-settlement/) | the dispute and settlement rail of a verifiable infrastructure market, measured end to end on testnet 28001: an order with its reward in escrow and a provider bond, the result committed on chain with its execution envelope notarized under a post-quantum certified anchor, a challenge window in which anyone disputes with a recomputed result and a bond, an arbiter that can only route the escrow (with a permissionless 14-day timeout), deadline reclaims, and an AIP-23 settlement receipt; a pure judge over the job's timeline that checks deadlines, the window, who resolved, the payout, the replay and the proof |
| [`verifiable-data/`](verifiable-data/) | a dataset whose every answer is checkable against a short commitment without the data: RFC 9162 commitments per version (append-only, consistency proofs), equality and range queries proven correct AND complete (contiguous run of a sorted index plus its two neighbours), hash-based selective disclosure of fields, availability sampling against the serving endpoint, and the version's AIP-23 envelope notarized and read back under a post-quantum certified anchor; not zero knowledge, and says so |
| [`ai-trust/`](ai-trust/) | the trust chain of an AI result: the agent's identity (agents/), the model artifact's attestation, the host's runtime record (verify-layer/), the policy and signed ledger, the Proof of AI envelope and the chain as witness, joined in one bundle whose verifier checks only the links between them |
| [`conformance/`](conformance/) | a conformance suite for the AIP-23 proof kinds: one verdict form over the verifiers published here (four levels: integrity, authenticity, anchoring, finality; each passed, failed or not measured, with the named rule), 239 rules of fifteen verifiers read from their code (the presentation verifier of identity/ and the device verifier of device-identity/ among them since 2026-10-05; the SD-JWT, SD-JWT VC, Travel Rule and compliance-policy verifiers since 2026-10-18, so nothing of identity/ is left out), 619 vectors derived from the producers and from evidence recorded on testnet 28001 (each invalid one is one operation away from one that is not, or one change at its producer, and must be refused for exactly its named rules), and a runner that scores any implementation given as a command, in any language; a second implementation in Python covers 6 of the 239 rules. Not a certification and not an audit |
| [`conformance/`](conformance/) | a conformance suite for the AIP-23 proof kinds: one verdict form over the verifiers published here (four levels: integrity, authenticity, anchoring, finality; each passed, failed or not measured, with the named rule), 240 rules of fifteen verifiers read from their code (the presentation verifier of identity/ and the device verifier of device-identity/ among them since 2026-10-05; the SD-JWT, SD-JWT VC, Travel Rule and compliance-policy verifiers since 2026-10-18, so nothing of identity/ is left out), 626 vectors derived from the producers and from evidence recorded on testnet 28001 (each invalid one is one operation away from one that is not, or one change at its producer, and must be refused for exactly its named rules), and a runner that scores any implementation given as a command, in any language; a second implementation in Python covers 6 of the 240 rules. Not a certification and not an audit |
| [`identity/`](identity/) | post-quantum credentials: an issuer signs claims (hybrid Ed25519 + ML-DSA-65, both required), the holder shows only the claims it picks (selective disclosure in the manner of SD-JWT, RFC 9901, in a format of its own), bound to the verifier's audience and nonce; delegation to a phone, an agent or a ten-minute session key that can only narrow; revocation and an issuer status list in the manner of W3C Bitstring Status List; all checked offline from the files; and compliance without surveillance: a verifier's policy (issuers, required claims) judged on a presentation, recorded as an AIP-23 envelope that carries no personal data; and the Travel Rule between VASPs, the IVMS101 data sealed for the receiving VASP with a hybrid X25519 + ML-KEM-768 key encapsulation, signed, bound to the transfer and acknowledged; and the same credentials as standard SD-JWT (IETF RFC 9901): issued with the issuer's Ed25519 key (EdDSA) or its ML-DSA-65 key, presented with a key binding JWT, and verified against the RFC's own example vectors and, since 2026-10-18, interoperable in both directions with a foreign library (@sd-jwt/core 0.23.0 with jose 6.2.12, ML-DSA-65 included; `interop/`), plus the SD-JWT VC profile (draft-ietf-oauth-sd-jwt-vc-19, `dc+sd-jwt`, `vct`, a Token Status List judged offline) (`identity-cli.mjs verify-sdjwt`, behind Aere Cloud's POST /v1/identity/sd-jwt/verify, and `verify-sdjwt-vc`) |
| [`device-identity/`](device-identity/) | what a machine is, as a chain of signed statements checked offline: the manufacturer's credential for the device key (the manufacturer and the owners sign hybrid, Ed25519 + ML-DSA-65, both required; the device key is ML-DSA-65), a firmware release with a floor and retractions (a rolled-back or retracted firmware is refused), ownership by transfers each signed by the owner before, the current owner's authorization, a status registry in which the manufacturer's retirement is final and an owner's lasts as long as its ownership (after a sale the records of the previous owner are named and not applied: a seller could date one before the sale after it; a retirement by an owner that survives a sale needs a witness of time that is not built), and the device's fresh attestation to the verifier's nonce, with an AIP-23 envelope that can be notarized and read back under a post-quantum certified anchor; one verifier makes every check it can reach, names every rule that refused and says VALID, PARTIAL (what it was not handed, or could not read) or INVALID. The device key is a software key: no TPM, secure element or TEE is used or checked, and the README says what that leaves open |
| [`exposure-report/`](exposure-report/) | classifies Bitcoin and Ethereum addresses by whether their public key is already readable from chain data, in three declared modes (offline, by address or script type only; through your own Electrum server, Esplora index, Bitcoin Core or Ethereum JSON-RPC, one declared target; through a third-party provider only with written consent, recorded in the report), with exactly two labels ("exposed (on-chain evidence)", "no on-chain evidence of exposure") and "unmeasured" when the reading did not happen, the evidence named for each address, and a dated report (JSON, Markdown, an AIP-23 `compliance` envelope that binds the JSON). It records what was readable on the date of the report: it does not protect, move or recover any coins, it does not change what a spend reveals, and it says nothing about the future. The one component with npm dependencies (`@noble/hashes`, `@noble/curves` 1.x: `npm install --ignore-scripts` in its directory) |
@ -38,20 +39,21 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
|---|---|---|
| pq-gateway | 33/33 (`node proba-pq-gateway.mjs`) | 33/33 (`bash proba-pq-gateway-control-negativ.sh`) |
| pq-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`pq-kms/test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`pq-kms/test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`pq-kms/test/control-negativ-hsm-incredere.mjs`) |
| pq-secrets | 43/43 on 2026-10-18 (`node test/proba-secrete.mjs`: the whole flow with keys born at run time, each refusal by name, a presentation signed for one request refused on any other, grant attenuation, the same refusal for every unauthorized caller, concurrent writes, rotation, revoked versions, the value searched for as a string in the log and on disk, the log intact after 100 operations, every envelope VALID for the AIP-23 reference verifier when `AERE_VERIFY_PROOF` names it, otherwise skipped by name); an adversarial review ran more than sixty attacks before publication, and the three that still succeed are limits written in the README | 43/43 on 2026-10-18 (`node test/control-negativ-secrete.mjs`: each guard planted out in a copy, the named checks must fail; `--autoproba` 5/5) |
| pq-pki | 27/27 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5 | 22/22 (`node test/control-negativ.mjs`) |
| crypto-inventory | 68/68 (`node test/proba.mjs`, measured 2026-10-18 with the Rust, Swift and Kotlin detectors, glob imports, libcrux, BoringSSL, the libsignal Java/Node/Swift/Android APIs, CryptoKit, CommonCrypto, Security.framework, Android Keystore, Tink and OkHttp included); cost on hostile input 13/13 linear (`node test/proba-timp.mjs`) | 35/35 (`node test/control-negativ.mjs`); cost 5/5 in this repository (`node test/control-negativ-timp.mjs`; its fourth case compares with version 0.1.0 from the development history and is skipped here) |
| verify-layer | 45/45 with the AIP-23 reference verifier, measured from a fresh clone on 2026-10-18 (`AERE_VERIFY_PROOF=<verify-proof.mjs> node proba-sidecar.mjs`, where verify-proof.mjs is the one served at aere.network/tools with verify-anchor.mjs, mpt.mjs, falcon512.mjs, registry-v3.mjs and package.json beside it and `npm install` done there; verify-proof.mjs alone gives 41/45, because the four finality checks need the anchor verifier); without it 40 run, 5 are reported as skipped and the exit code is 2; the cloud adapters 60/60 with the same verifier on 2026-10-18 (`node proba-adaptoare-cloud.mjs`: nine fixtures derived from the vendors' documented CLI and REST examples by `fixturi-cloud/deriva.mjs`; without the verifier 59 run and one is skipped; a real account is unmeasured and the README says so); the Merkle tree 20/20 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 37/37 from a fresh clone of this repository on 2026-10-18 (`node control-negativ-sidecar.mjs`: the ten original plantings plus the cloud adapter rules; the case that compares with the development history is skipped here, and is the 38th in the development tree); the tree 14/14 (`node control-negativ-arbore.mjs`) |
| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test |
| proof-kinds | 28/28 on 2026-10-18 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`: fifteen kinds, the allowed values of the `secret-access` fields included) | seven negative controls inside the test |
| readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) |
| control-plane | planner 44/44 (2026-10-18: a key agreement or a client-side TLS configured by the application itself is not routed to the TLS gateway; a self-signed trust anchor is blocked, not "automatic through PKI"; test code is LOW with its own scope), command line 10/10, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers (all five server profiles measured end to end on 2026-10-18: nginx, Apache, HAProxy, Node, Go), compliance report 30/30 (with the AIP-23 verifier; test-code findings are counted apart and are not "exposed today"), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) |
| identity | 46/46 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 20/20 (`node proba-travel-rule.mjs`; 2026-10-18: a missing originator id or nonce is a refusal, found when the verifier was enrolled in the conformance suite), SD-JWT 25/25 (`node proba-sdjwt.mjs`, the RFC 9901 vectors and fixtures derived from @sd-jwt/core 0.23.0 included), SD-JWT VC 10/10 (`node proba-sdjwt-vc.mjs`); interoperability with @sd-jwt/core and jose in both directions 26/26 and 28/28 (`interop/`, with those libraries installed in a folder you name; otherwise reported unmeasured), measured 2026-10-18 | 53/53 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 20/20 (`node control-negativ-travel-rule.mjs`); SD-JWT 31/31 (`node control-negativ-sdjwt.mjs`); SD-JWT VC 16/16 (`node control-negativ-sdjwt-vc.mjs`) |
| identity | 52/52 (`node proba-identity.mjs`, a presentation optionally signed for one exact request (`request`) included, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 18/18 (`node proba-conformitate.mjs`), Travel Rule 20/20 (`node proba-travel-rule.mjs`; 2026-10-18: a missing originator id or nonce is a refusal, found when the verifier was enrolled in the conformance suite), SD-JWT 25/25 (`node proba-sdjwt.mjs`, the RFC 9901 vectors and fixtures derived from @sd-jwt/core 0.23.0 included), SD-JWT VC 10/10 (`node proba-sdjwt-vc.mjs`); interoperability with @sd-jwt/core and jose in both directions 26/26 and 28/28 (`interop/`, with those libraries installed in a folder you name; otherwise reported unmeasured), measured 2026-10-18 | 57/57 (`node control-negativ-identity.mjs`); compliance 17/17 (`node control-negativ-conformitate.mjs`); Travel Rule 20/20 (`node control-negativ-travel-rule.mjs`); SD-JWT 31/31 (`node control-negativ-sdjwt.mjs`); SD-JWT VC 16/16 (`node control-negativ-sdjwt-vc.mjs`) |
| agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 54/54 on 2026-10-05, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`agents/x402/proba-x402-testnet.mjs`, `agents/x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository |
| proof-of-ai | 6/6 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-proof-of-ai.mjs`); without it the probe reports NEMASURAT and exits 2 | one negative control inside the test (an envelope touched after its hash: INVALID) |
| payments | 23/23 without a chain (`node proba-risc.mjs`: the verdicts under a policy, the reproducible verdict hash, a receipt verified, nine forged ones refused by name, a malformed payment refused by name); 9/9 on testnet 28001 on 2026-10-02 (`AERE_TESTNET_KEY_FILE=... AERE_ETHERS_DIR=... node proba-plata-28001.mjs`: a payment allowed, paid, its receipt notarized and read back with finality PASSED, the public reader VALID; a payment above the limit refused with nothing sent; the fourth in the window refused; a forged receipt and a loosened policy INVALID) | 10/10 (`node proba-risc.mjs --control-negativ`: form, asset, counterparty, rate, per-payment limit, window, linkage, USD arithmetic, verdict reproduction, chain facts, each planted out on a copy and each forged fixture then accepted) |
| market-settlement | 20/20 on testnet 28001 on 2026-10-02 (`AERE_TESTNET_KEY_FILE=... AERE_ETHERS_DIR=... node proba-piata-28001.mjs`: five scenarios on AereComputeMarketV3 deployed from the committed artifact, every refusal by its named custom error, the provider's notarized envelope read back under a post-quantum certified anchor); the judge on the recorded timelines 26/26 without a chain (`node proba-judecata.mjs`: seven honest readings VALID, eighteen forged or malformed ones INVALID for their named reason, the settlement receipt) | 10/10 (`node proba-judecata.mjs --control-negativ`: form, completeness of the timeline, posting, deadline, window, arbiter, payout, result bytes, replay, proof, each planted out on a copy and each forged fixture then accepted) |
| verifiable-data | 23/23 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-verifiable-data.mjs`: honest answers VALID, ten forged or malformed ones INVALID for their named reason, availability sampling on a local server; the envelope row is NEMASURAT without the verifier, exit 2); the two chain rows of `--viu` measured on testnet 28001 on 2026-10-02 (a version notarized with a test key and read back under a post-quantum certified anchor; evidence in `dovezi/`) | 5/5 (`node proba-verifiable-data.mjs --control-negativ`: field commitment, index order, completeness, consistency, sampling, each planted out on a copy and each forged fixture then accepted) |
| ai-trust | 16/16 with the AIP-23 verifier, measured 2026-10-05 (`AERE_VERIFY_PROOF=... node proba-lant-de-incredere.mjs`: the whole chain on a committed fixture, then thirteen bundles broken on purpose, each refused for its named reason, among them an agent with a classical key and an audit head signed with one); without the verifier it exits 2 | 11/11 (`node proba-lant-de-incredere.mjs --control-negativ`) |
| conformance | 12/12 on 2026-10-18 with the AIP-23 verifier as served (fetched and installed in an empty folder), Python 3 and testnet 28001 read (`AERE_VERIFY_PROOF=... node proba-conformance.mjs --online`: the rules and the vectors are what the code derives, every rule has a vector, every invalid vector is one operation from its base or one declared change at its producer, the published verifiers pass 619/619, an independent Python implementation passes the 62 it claims and the other 557 are reported not covered, 100,825 malformed inputs and 183 malformed envelope files each get a verdict); without `--online` the nine chain vectors are reported unmeasured and the exit code is 2 | 321/321 on 2026-10-18: 239/239 rules (`node proba-conformance.mjs --control-negativ --online`: each rule planted out of its verifier in a copy; the vectors that expect it, and only they, must fail), then 76/76 conditions of the device verifier's compound checks, each planted out alone (exactly the vectors written for it must fail), then six controls of the instruments |
| conformance | 12/12 on 2026-10-18 with the AIP-23 verifier as served (fetched and installed in an empty folder), Python 3 and testnet 28001 read (`AERE_VERIFY_PROOF=... node proba-conformance.mjs --online`: the rules and the vectors are what the code derives, every rule has a vector, every invalid vector is one operation from its base or one declared change at its producer, the published verifiers pass 626/626, an independent Python implementation passes the 65 it claims and the other 561 are reported not covered, 101,833 malformed inputs and 183 malformed envelope files each get a verdict; remeasured 2026-10-18 21:25Z after the request-signed presentation); without `--online` the nine chain vectors are reported unmeasured and the exit code is 2 | 322/322 on 2026-10-18: 240/240 rules (`node proba-conformance.mjs --control-negativ --online`: each rule planted out of its verifier in a copy; the vectors that expect it, and only they, must fail), then 76/76 conditions of the device verifier's compound checks, each planted out alone (exactly the vectors written for it must fail), then six controls of the instruments |
| device-identity | 372/372 with the AIP-23 verifier, measured 2026-10-14 (`AERE_VERIFY_PROOF=... node proba-device.mjs`: fifty-four bundles that must be VALID; seventy-seven with something not brought, not read or not readable, each PARTIAL for its named causes; two hundred and four attack rows, each refused for exactly its named rules, among them a clone with another key, a rolled-back or retracted firmware (also when the release is left out of the bundle), a previous owner that still signs, an owner's retirement written under no ownership head, a replayed attestation, an attestation dated ahead of the verifier's clock, a manufacturer the verifier does not trust, a retirement, a classical key under the ML-DSA-65 name, a signature in another base64 text, a day that does not exist; 804 damaged bundles each given a verdict; the command line through files only; the evidence recorded on testnet 28001 judged again offline); the chain as witness 10/10 on testnet 28001 on 2026-10-05, before the repairs of that day (`proba-device-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier; not run again since: the bundles and readings it recorded give the same verdicts under the verifier of now) | 220/220 (`node control-negativ-device.mjs`: the untouched copy green; each of the 47 rules taken out of a copy, the rows that expect it, and only they, go red; 172 more plantings, most of them one condition of a compound check taken out alone, each with its rows) |
| exposure-report | 164 passed and 1 `NEMASURAT` of 165 rows in the AERE development tree on 2026-10-14 (`node proba-expunere.mjs`: codec checks with real public vectors, mode 1 and mode 2 through four mock targets that serve recorded responses, the network guard's proof that in modes 1 and 2 nothing leaves the machine, the language gate, the AIP-23 `compliance` envelopes judged by the reference verifier; the `NEMASURAT` row is mode 3 against a real provider, which would send addresses to a third party); in this copy, after `npm install --ignore-scripts` in its directory: 153 passed, 0 red, 0 broken, 12 `NEMASURAT` with the reason (the rows compared with the AIP-23 verifier, the anchor verifier's RLP encoder and MPT walker and the shield's header pins, none of which is in this repository: with `AERE_PROOF_PROTOCOL_VERIFY`, `AERE_VERIFY_PROOF` and `AERE_EXPUNERE_PINI` naming them the rows run, 162 passed and 3 `NEMASURAT`; the scan of the directory for names from the AERE host inventory; the git-ignore row outside a git tree); the exit code is 0 unless a row is red or broken | inside the test, 2026-10-14: each rule has its reverse planting in a copy of the tool (a foreign provider planted on each network path is caught, a label planted in the language gate is refused, each classification rule taken out turns its row red, a `@noble/curves` 2.x planted first is passed over and alone is named as missing), and the recorded fixtures are judged against their declared date |
@ -62,4 +64,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ...
## Licence
MIT, see [LICENSE](LICENSE). Files: 948 (pq-gateway 9, pq-kms 10, pq-pki 6, crypto-inventory 73, verify-layer 56, proof-kinds 3, control-plane 27, agents 39, identity 24, proof-of-ai 3, ai-trust 6, verifiable-data 7, market-settlement 9, payments 8, conformance 631, device-identity 11, exposure-report 18, readiness 4).
MIT, see [LICENSE](LICENSE). Files: 961 (pq-gateway 9, pq-kms 10, pq-secrets 6, pq-pki 6, crypto-inventory 73, verify-layer 56, proof-kinds 3, control-plane 27, agents 39, identity 24, proof-of-ai 3, ai-trust 6, verifiable-data 7, market-settlement 9, payments 8, conformance 638, device-identity 11, exposure-report 18, readiness 4).

View File

@ -42,9 +42,9 @@ verdict (`vectori/payment-receipt/company-p1-28001.valid.json`).
## What is enrolled
`REGULI.json` is derived from the code (`node deriva-reguli.mjs --verifica`): 239 rules of 15 verifiers, 21 artifact kinds (since
2026-10-18, when the Travel Rule and the compliance verifiers were enrolled; 213 rules of 13 verifiers and 17 kinds after the SD-JWT
verifiers earlier that day; 181 rules of 11 verifiers, 15 kinds before). By level: 187 rules are reported at `integrity`, 49 at
`REGULI.json` is derived from the code (`node deriva-reguli.mjs --verifica`): 240 rules of 15 verifiers, 21 artifact kinds (since
2026-10-18: 239 when the Travel Rule and the compliance verifiers were enrolled, 240 with `identity.request` later that day; 213 rules of 13 verifiers and 17 kinds after the SD-JWT
verifiers earlier that day; 181 rules of 11 verifiers, 15 kinds before). By level: 188 rules are reported at `integrity`, 49 at
`authenticity`, 3 at `anchoring` (one of them, `envelope.finality`, is reported at `anchoring` or at `finality` by the place that refused).
| artifact kind | judged by | rules |
@ -58,7 +58,7 @@ verifiers earlier that day; 181 rules of 11 verifiers, 15 kinds before). By leve
| `audit-log` (a hash-chained log, alone or against an attested head, signed or not) | `verify-layer/sidecar.mjs` | 13: `auditlog.entry`, `.seq`, `.chain`, `.hash`, `.head-form`, `.head-hash`, `.head-unsigned`, `.head-scheme`, `.head-key`, `.head-signature`, `.head-signer`, `.head-short`, `.head-rewritten` |
| `audit-inclusion`, `audit-consistency` (RFC 9162 proofs over that log) | `verify-layer/jurnal-arbore.mjs` | 12: `audittree.proof-form`, `.head-form`, `.head-hash`, `.inclusion-index`, `.inclusion-entry`, `.inclusion-envelope`, `.inclusion-leaf`, `.inclusion-path`, `.inclusion-head`, `.consistency-start`, `.consistency-end`, `.consistency` |
| `agent-ledger` (an agent's signed ledger under its policy) | `agents/agent-ledger.mjs` | 16: `agents.ledger-form`, `.policy`, `.other-policy`, `.session`, `.key`, `.identity`, `.anchor`, `.seq`, `.chain`, `.body`, `.signature`, `.hash`, `.time`, `.revocation`, `.approvals`, `.decision` |
| `identity-presentation` (a credential shown by its holder or by the last delegate of a chain, judged with what the verifier brings: its audience, its nonce, its clock, the issuers it trusts, the status lists and the revocations it was handed) | `identity/identity.mjs` | 37: `identity.form`, `.issuer-id`, `.holder-id`, `.credential-signature`, `.issuer-trusted`, `.credential-window`, `.status-list`, `.credential-revoked`, `.sd-distinct`, `.disclosure-readable`, `.disclosure-signed`, `.disclosure-once`, `.disclosure-plain`, `.claim-name`, `.binding-credential`, `.binding-disclosures`, `.binding-delegations`, `.presenter`, `.presentation-signature`, `.audience`, `.nonce`, `.freshness`, `.chain-length`, `.delegation-form`, `.delegation-ids`, `.delegation-from`, `.delegation-parent`, `.delegation-signature`, `.delegation-scope`, `.delegation-window`, `.delegation-depth`, `.delegation-narrows`, `.delegation-credential`, `.delegation-claims`, `.delegation-audience`, `.delegation-revoked`, `.unreadable` |
| `identity-presentation` (a credential shown by its holder or by the last delegate of a chain, judged with what the verifier brings: its audience, its nonce, its clock, the issuers it trusts, the status lists and the revocations it was handed) | `identity/identity.mjs` | 38: `identity.form`, `.issuer-id`, `.holder-id`, `.credential-signature`, `.issuer-trusted`, `.credential-window`, `.status-list`, `.credential-revoked`, `.sd-distinct`, `.disclosure-readable`, `.disclosure-signed`, `.disclosure-once`, `.disclosure-plain`, `.claim-name`, `.binding-credential`, `.binding-disclosures`, `.binding-delegations`, `.presenter`, `.presentation-signature`, `.audience`, `.nonce`, `.freshness`, `.chain-length`, `.delegation-form`, `.delegation-ids`, `.delegation-from`, `.delegation-parent`, `.delegation-signature`, `.delegation-scope`, `.delegation-window`, `.delegation-depth`, `.delegation-narrows`, `.delegation-credential`, `.delegation-claims`, `.delegation-audience`, `.delegation-revoked`, `.unreadable`, `.request` (since 2026-10-18: the presentation commits to the request the verifier received, OpenID4VP `transaction_data`) |
| `device-bundle` (the chain of a device: the manufacturer's credential for the device key, a firmware release, the ownership transfers, the owner's authorization, the device's attestation and its AIP-23 envelope, judged with what the verifier brings: its clock, nonce and audience, the manufacturers it trusts, the recall lists, firmware policies and status records it read, its own reading of the notarized envelope) | `device-identity/device.mjs` | 47: `device.form`, `.manufacture-form`, `.manufacturer-id`, `.manufacture-holder`, `.manufacture-signature`, `.manufacturer-trusted`, `.manufacture-window`, `.manufacture-status-list`, `.manufacture-revoked`, `.device-key`, `.device-id`, `.firmware-form`, `.firmware-signer`, `.firmware-signature`, `.firmware-model`, `.firmware-retracted`, `.firmware-rollback`, `.firmware-software`, `.chain-length`, `.transfer-form`, `.transfer-device`, `.transfer-ids`, `.transfer-from`, `.transfer-prev`, `.transfer-signature`, `.transfer-time`, `.authorization-form`, `.authorization-device`, `.authorization-owner`, `.authorization-ownership`, `.authorization-signature`, `.authorization-window`, `.authorization-action`, `.status-retired`, `.status-suspended`, `.attestation-form`, `.attestation-key`, `.attestation-device`, `.attestation-signature`, `.audience`, `.nonce`, `.freshness`, `.attestation-firmware`, `.attestation-ownership`, `.witness-envelope`, `.witness-time`, `.unreadable` |
| `sdjwt-presentation` (an SD-JWT of IETF RFC 9901, as issued or with its Key Binding JWT, judged with what the verifier brings: the issuer key it chose, a public JWK, never one taken from the token; its clock; its audience and nonce; the issuer and the explicit type it expects; whether it requires key binding and `exp`) | `identity/sdjwt.mjs` (enrolled 2026-10-18) | 23: `sdjwt.form`, `.alg`, `.typ`, `.crit`, `.key`, `.signature`, `.issuer`, `.sd-alg`, `.disclosure-form`, `.disclosure-twice`, `.digest-twice`, `.disclosure-name`, `.claim-conflict`, `.unreferenced`, `.time`, `.kb-context`, `.kb-required`, `.kb-cnf`, `.kb-claims`, `.kb-audience`, `.kb-nonce`, `.kb-freshness`, `.kb-sd-hash` |
| `sdjwt-vc-presentation` (an SD-JWT VC of `draft-ietf-oauth-sd-jwt-vc`, judged as the SD-JWT above plus the profile: the credential type the verifier expects, and the Token Status List it fetched for the `status` the credential names) | `identity/sdjwt-vc.mjs` (enrolled 2026-10-18) | 9 of its own, on top of the 23 of `sdjwt`: `sdjwtvc.vct-expected`, `.vct`, `.vct-mismatch`, `.disclosed-protected`, `.form`, `.status-list`, `.status-index`, `.status-revoked`, `.status-unjudged` |
@ -116,12 +116,12 @@ issuer and for its status at once.
## The vectors
`vectori/` holds 619 vectors (610 offline, 9 that read testnet 28001; 22 MB, since every device, identity and Travel Rule vector carries its ML-DSA-65 keys and signatures), listed with their digests in `vectori/INDEX.json`:
envelope 64, payment-risk 11, payment-receipt 10, market-timeline 26, dataset 22, ai-trust-bundle 24, audit-log 19, audit-inclusion
10, audit-consistency 4, agent-ledger 18, identity-presentation 65, device-bundle 194, sdjwt-presentation 54, sdjwt-vc-presentation 32, travel-rule-binding 19, travel-rule-message 26, travel-rule-receipt 9, compliance-presentation 12. By expected verdict: 85 `VALID`, 69 `PARTIAL`, 465 `INVALID`. None is typed by hand
`vectori/` holds 626 vectors (617 offline, 9 that read testnet 28001; 22 MB, since every device, identity and Travel Rule vector carries its ML-DSA-65 keys and signatures), listed with their digests in `vectori/INDEX.json`:
envelope 67, payment-risk 11, payment-receipt 10, market-timeline 26, dataset 22, ai-trust-bundle 24, audit-log 19, audit-inclusion
10, audit-consistency 4, agent-ledger 18, identity-presentation 69, device-bundle 194, sdjwt-presentation 54, sdjwt-vc-presentation 32, travel-rule-binding 19, travel-rule-message 26, travel-rule-receipt 9, compliance-presentation 12. By expected verdict: 87 `VALID`, 70 `PARTIAL`, 469 `INVALID`. None is typed by hand
(`node genereaza-vectori.mjs --verifica` regenerates them and compares byte for byte):
- the 16 AIP-23 kinds come from their producers on fixed inputs (the same envelopes as the published AIP-23 kind vectors); the audit
- the 17 AIP-23 kinds come from their producers on fixed inputs (the same envelopes as the published AIP-23 kind vectors; `secret-access`, the audit entry of the secrets service, since 2026-10-18); the audit
log, its heads and its inclusion and consistency proofs come from the verify-layer producers on fixed inputs;
- the receipt, the five market timelines, the notarized dataset commitment and one trust chain of an AI result are the evidence
recorded on testnet 28001 on 2026-10-02 beside those components;
@ -162,12 +162,12 @@ envelope 64, payment-risk 11, payment-receipt 10, market-timeline 26, dataset 22
libraries' real output on 2026-10-18, with their public test keys.
An `INVALID` vector names its `base` and its `mutation`: **one operation** (set, remove or add at one JSON path) applied to the input
of a vector that is not invalid, and it must be refused for exactly the rules in `expected.failed`, not merely refused: 300 of the
465 are made this way. Where the rule sits under a hash or a signature, one place is not reachable (changing an amount under the
of a vector that is not invalid, and it must be refused for exactly the rules in `expected.failed`, not merely refused: 303 of the
469 are made this way. Where the rule sits under a hash or a signature, one place is not reachable (changing an amount under the
instruction hash trips the binding first; a false decision in a ledger must still be signed by the agent; a disclosure the issuer never signed must still be signed for by
the holder; an authorization by a previous owner must still be signed by that owner; a digest repeated in an SD-JWT must still be
signed by its issuer, a key binding JWT for another audience by its holder; a Travel Rule header addressed to another VASP, or with a
cut ML-KEM ciphertext, must still be signed by the originator): the other 165 are made at
cut ML-KEM ciphertext, must still be signed by the originator; a presentation without a request must still be signed by its holder): the other 166 are made at
the producer, the commitments above the change recomputed with the published formulas, and every path that moved is declared
(`mutation.changed`). Twenty-four vectors expect more than one rule: rules that guard the same field cannot be separated (a looser policy shown
under a receipt also makes the risk verdict not reproduce; a Proof of AI envelope that is changed is also no longer the one the
@ -182,7 +182,7 @@ also establishes no claim, so its policy's rules are not met either).
```
AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools, with verify-anchor.mjs, mpt.mjs, falcon512.mjs, registry-v3.mjs and package.json beside it and npm install done>
node conformance.mjs --impl "node implementare-aere.mjs" # the published verifiers behind the one verdict form: 610 offline vectors
node conformance.mjs --impl "node implementare-aere.mjs" # the published verifiers behind the one verdict form: 617 offline vectors
node conformance.mjs --impl "node implementare-aere.mjs" --online # and the 9 that read testnet 28001 (nothing is sent)
node conformance.mjs --impl "python3 implementare-integritate.py" # an independent implementation: 62 vectors, 557 reported NOT COVERED
node conformance.mjs --impl "<your command>" # yours
@ -215,9 +215,9 @@ exact set of failed rules and the state of each of the four levels are the expec
refusal. Without `--online` an implementation must not open a connection.
`implementare-integritate.py` is the second implementation: Python, standard library only, written from the rules and not from the
JavaScript. It claims 6 of the 239 rules (the form and the statement hash of an envelope; the form, the commitment, the salted field
commitments and the RFC 9162 inclusion of a dataset record) and passes all 62 vectors it claims. It cannot check an ML-DSA signature
or read a chain, says so in its capabilities, and the runner reports the rest as not covered: 557 vectors, among them every vector of
JavaScript. It claims 6 of the 240 rules (the form and the statement hash of an envelope; the form, the commitment, the salted field
commitments and the RFC 9162 inclusion of a dataset record) and passes all 65 vectors it claims (measured 2026-10-18). It cannot check an ML-DSA signature
or read a chain, says so in its capabilities, and the runner reports the rest as not covered: 561 vectors, among them every vector of
the trust chain, the audit log, the agents' ledger, the identity presentations, the device bundles, the SD-JWT presentations, the
Travel Rule artifacts and the compliance presentations. For those ten verifiers the suite has one implementation, ours (for SD-JWT,
the identity component measured our verifier against `@sd-jwt/core` and `@sd-jwt/sd-jwt-vc` outside this suite; those libraries are
@ -225,6 +225,31 @@ not implementations of this suite's contract).
## How the suite itself is checked
Measured on 2026-10-18 (UTC), in the evening, after the identity verifier gained the rule `request` (a presentation committed to the
request the verifier received; the adversarial review of the secrets service) and its four vectors were derived from a new fresh
source (`identityRequest`; every other fresh source kept, so no other vector's input moved; the 65 earlier identity vectors changed
only in `exercises`, which now lists `identity.request`), on Node.js 24.14.1, offline, with the served reference verifier and with the
one of the development repository: `node proba-conformance.mjs` gives 12 rows green, 0 red, 1 not measured (the 9 vectors that read
testnet 28001). 240 rules of 15 verifiers, 626 vectors (`LA ZI`), every rule has a vector (240 of 240), every INVALID vector (469) is
its base with one operation (303) or one declared producer change (166), the published verifiers pass 617 of 617 offline vectors,
the independent Python implementation passes the 65 it claims and reports the other 561 not covered, 101,833 malformed inputs each got
a verdict. `node proba-conformance.mjs --control-negativ` (offline) gives 319 rows green, 0 red, 3 not measured (the three rules whose
vectors need the testnet): 237 of 240 rules proven load-bearing, `identity.request` among them (planted out, exactly its 2 vectors
fail). Before the change, the same commands on the untouched tree: 12 green, 0 red, 1 not measured; 318 green, 0 red, 3 not
measured (236 of 239). Not run after the change: `--online`.
Measured again on 2026-10-18 (UTC), later the same day, after the AIP-23 kind `secret-access` (the audit entry of the secrets service,
`pq-secrets/`) was added to proof-kinds and its three envelope vectors derived (Node.js 24.14.1, the reference verifier of the
development repository, offline): `node proba-conformance.mjs` gives 10 rows green, 2 red, 1 not measured. The vectors on disk are what
the generator derives (622, `LA ZI`); the 17 kind envelopes are the published kind vectors; every rule has a vector (239 of 239); every
artifact kind has a valid vector (21); every INVALID vector (467) is its base with one operation (302) or one declared producer change
(165); the published verifiers pass 613 of 613 offline vectors; the independent Python implementation passes all 65 it claims and the
other 557 are reported not covered; 100,825 malformed inputs and 183 malformed envelope files each got a verdict. The 9 vectors that
read testnet 28001 were not run (`--online`). The two red rows are the derivation of the `aip23-envelope` rules from the verifier's code
(its finality sites, "appears on 0 FAILED lines of the verifier") and `REGULI.json`, and they were red before this change, on the
untouched tree, in the same run form (measured first): the reference verifier was refactored earlier that day (`judge()` exported,
commit b2cf499c6) and the rule derivation has not followed it yet; the kind added here does not touch them.
Measured on 2026-10-18 (UTC), after the Travel Rule verifier and the compliance verifier were enrolled, the last two of the identity
component (Node.js 24.14.1; the reference verifier of the development repository; testnet 28001 read, nothing sent; the run is the one
on the final code, after the three fixes below):
@ -368,10 +393,10 @@ On 2026-10-05, while the identity verifier was being enrolled, one more, fixed t
## What it is not, and what is not measured
- **Not a certification, not an audit, not a mark.** Passing says that an implementation agrees with the published verifiers on these
619 inputs. It does not say either of them is correct, and no third party has reviewed the verifiers, the vectors or this suite.
626 inputs. It does not say either of them is correct, and no third party has reviewed the verifiers, the vectors or this suite.
- **The vectors and the verifiers have the same authors.** The expected verdicts were written by the people who wrote the verifiers.
The second implementation is independent in code and language, not in authorship, and it covers 6 rules of 239; the other
233 rules, among them every rule of the trust chain, the audit log, the agents' ledger, the identity verifier, the device
The second implementation is independent in code and language, not in authorship, and it covers 6 rules of 240; the other
234 rules, among them every rule of the trust chain, the audit log, the agents' ledger, the identity verifier, the device
verifier, the SD-JWT verifiers, the Travel Rule verifier and the compliance verifier, are judged here by one implementation.
- **Finality is the chain's.** The level says that the post-quantum certificate of the covering anchor verified on the chain where the
artifact was notarized. Every notarized vector here is on chain 28001, a testnet; nothing in this suite ran on mainnet.

View File

@ -1057,6 +1057,13 @@
"level": "integrity",
"sites": 1,
"says": "every field a check reads can be read (a time is an RFC 3339 UTC time, a hash input is JSON)"
},
{
"id": "identity.request",
"source": "request",
"level": "integrity",
"sites": 1,
"says": "the presentation commits to the request the verifier received: when the verifier gives the digest of its request, the signed binding carries exactly that digest (a binding without one is refused)"
}
],
"marked": "by a marker on every check"

View File

@ -10,7 +10,8 @@
// and its classical impostor (Ed25519), a random envelope nobody notarized, a small dataset
// (random salts), the presentations of one identity credential with their forged siblings
// (six hybrid key pairs), the SD-JWT and SD-JWT VC tokens, one Travel Rule exchange (with the
// beneficiary's KEM key pair, test material declared at the scene) and one compliance exchange,
// beneficiary's KEM key pair, test material declared at the scene), one compliance exchange,
// the presentations of one credential with and without a request (2026-10-18),
// and the transaction of the recorded receipt READ from testnet 28001 (two
// eth_ calls; nothing is sent). With --tot everything is made anew; with --refa <name>
// ONE source is made anew (its vectors change, the others stay byte for byte)
@ -567,11 +568,26 @@ const PROASPETE_IN_PLUS = {
valuesOutsidePolicy: prez(emite({ claims: { ...CL, age_over_18: false, jurisdiction: 'KP', kyc_level: 1, kyc_provider: 'unknown-kyc' }, status: { list: LID, index: 4 } })),
otherIssuer: prez(emite({ issuer: alt })), revoked: prez(emite({ status: { list: LID, index: 9 } })) } };
},
// 2026-10-18 (B8, cloud-gateway/pq-secrets): presentations that commit to the REQUEST the verifier received (OpenID4VP 1.0
// transaction_data; the transaction_data_hashes of the SD-JWT VC Key Binding JWT). One holder, two hybrid key pairs made and discarded
// here; the request is the digest of a get on a secret in the canonical form of the secrets service (requestHash), the other request
// the digest of a revoke of a grant on the same secret; the same holder presents with the request and without it
identityRequest: async () => {
const T0 = Math.floor(Date.now() / 1000) * 1000 - 3600000, zi = 86400; const at = (s) => new Date(T0 + s * 1000).toISOString(), la = (s) => new Date(T0 + s * 1000);
const [iss, hol] = Array.from({ length: 2 }, () => ID.generateKeys());
const AUD = 'https://secrets.example', NONCE = 'n-' + crypto.randomBytes(8).toString('hex'), LID = 'https://issuer.example/status/2';
const cerere = (operation, body) => sha256(Buffer.from(ID.canonical({ v: 1, kind: 'aere-secrets-request', service: AUD, operation, secret: 'db', body }), 'utf8'));
const { credential, disclosures } = ID.issueCredential({ issuer: iss, holder: hol.public, type: 'ServiceAccess', claims: { role: 'deployer', tenant: 'example' }, disclosable: ['role', 'tenant'], validFrom: at(-zi), validUntil: at(365 * zi), status: { list: LID, index: 1 }, now: la(-zi) });
const lista = ID.createStatusList({ issuer: iss, id: LID, validFrom: at(-3600), validUntil: at(zi), now: la(-3600) });
const prez = (o = {}) => ID.present({ credential, disclosures, reveal: ['role', 'tenant'], presenter: hol, audience: AUD, nonce: NONCE, now: la(0), ...o });
const REQUEST = cerere('get', {});
return { audience: AUD, nonce: NONCE, now: at(30), trusted: [iss.id], list: lista, request: REQUEST, otherRequest: cerere('revoke', { grantId: 'deployers' }), withRequest: prez({ request: REQUEST }), withoutRequest: prez() };
},
};
// ---------- fixed inputs of the AIP-23 kinds (the inputs of the published AIP-23 kind vectors) ----------
function intrariKinds() {
const AT = '2026-09-26T00:00:00Z'; const H = (s) => sha256(Buffer.from(s, 'utf8'));
const AT = '2026-09-26T00:00:00Z'; const H = (s) => sha256(Buffer.from(s, 'utf8')); const DB = 'db-password';
return { data: { name: 'report.csv', content: 'a,b,c', createdAt: AT }, execution: { program: 'aere-node', input: 'in', output: 'out', exitCode: 0, createdAt: AT },
identity: { subjectId: 'agent-1', publicKey: 'PK', method: 'ml-dsa-65', createdAt: AT }, compliance: { subject: 'org-x', policy: 'pq-ready', result: 'pass', evidence: 'EV', createdAt: AT },
runtime: { host: 'h1', artifactContent: 'BIN', attested: H('BIN'), matches: true, createdAt: AT }, location: { subject: 'srv-eu', region: 'eu-central', evidence: 'EV', createdAt: AT },
@ -580,7 +596,9 @@ function intrariKinds() {
block: { blockHash: '0x' + 'ab'.repeat(32), stateRoot: '0x' + 'cd'.repeat(32), anchorHeight: 20255488, certificateDigest: '0x' + 'ef'.repeat(32), createdAt: AT },
authorization: { grantor: 'org-x', grantee: 'agent-7', scope: 'payments:send<=100', policy: 'POLICY', expiresAt: '2026-12-31T00:00:00Z', createdAt: AT },
settlement: { chainId: 2800, txHash: '0x' + '12'.repeat(32), blockHash: '0x' + '34'.repeat(32), from: '0xa', to: '0xb', amount: '100', asset: 'AERE', instruction: 'INSTR', createdAt: AT },
provenance: { subject: 'ARTIFACT', parents: ['SRC-1', 'SRC-2'], process: 'BUILD', actor: 'build-bot', createdAt: AT } };
provenance: { subject: 'ARTIFACT', parents: ['SRC-1', 'SRC-2'], process: 'BUILD', actor: 'build-bot', createdAt: AT },
// 2026-10-18 (B8, cloud-gateway/pq-secrets): the same fixed input as the published kind vector (aere-proof-protocol/genereaza-vectori-kinds.mjs)
'secret-access': { service: 'https://secrets.example', secret: DB, version: 2, operation: 'get', verdict: 'allowed', rules: [], grant: 'deployers', holder: 'aere-id:' + '0'.repeat(40), presenter: 'aere-id:' + '1'.repeat(40), presentation: 'BINDING', policy: 'POLICY', createdAt: AT } };
}
/** Every AIP-23 kind, from its producer, on the fixed inputs. */
export function plicuriKinds() {
@ -906,6 +924,18 @@ function construieste() {
pl('delegation-credential', 'the link is for another credential', F.linkOtherCredential, ['identity.delegation-credential'], 'the holder gave a link scoped to another credential id');
pl('delegation-claims', 'the phone shows a claim outside the scope of its link', F.linkOtherClaims, ['identity.delegation-claims'], 'the holder gave a link for one claim; the phone disclosed two');
pl('delegation-audience', 'the link is for another verifier', F.linkOtherAudience, ['identity.delegation-audience'], 'the holder gave a link scoped to another audience'); }
// (I2) 2026-10-18 (B8): the presentation commits to the request the verifier received. A verifier that gives the digest of its request
// requires exactly that digest in the signed binding; one that gives none leaves it unjudged (PARTIAL), like the audience and the nonce
{ const IK = Object.keys(HARTA).filter((r) => r.startsWith('identity.')); const X = P.identityRequest; const A = 'identity-presentation';
const intrare = (prez, o = {}) => ({ presentation: prez, audience: X.audience, nonce: X.nonce, now: X.now, trustedIssuers: X.trusted, statusLists: [X.list], request: X.request, ...o });
const src = 'producers (identity: issueCredential, createStatusList, present with a request), two hybrid key pairs made and discarded at generation';
const AP = { authenticity: 'PASSED' }; const b = 'identity-presentation/request.valid';
valid(b, A, 'a holder presents for one request (the digest of a get on a secret); the verifier gives the digest of the request it received, and the signed binding carries it', src, intrare(X.withRequest), { exercises: IK, levels: AP });
const fara = { op: 'remove', path: ['request'] };
valid('identity-presentation/request.no-request.partial', A, 'the verifier gives no request: what the presentation was signed for is not judged, so it is not VALID', src, aplica(intrare(X.withRequest), fara), { exercises: IK, verdict: 'PARTIAL', levels: { ...AP, integrity: 'NOT_MEASURED' }, base: b, mutation: fara });
invalid('identity-presentation/request.other-request', 'the presentation was signed for a get and is handed over for a revoke of a grant (another request digest)', b, { op: 'set', path: ['request'], value: X.otherRequest }, ['identity.request']);
invalid('identity-presentation/request.presentation-without-request', 'the same holder presented without a request (the form made before the field existed), to a verifier that requires one', b, null, ['identity.request'],
{ producer: { note: 'the holder signed a binding that carries no request', input: intrare(X.withoutRequest), changed: [['presentation']] } }); }
// (J) a device bundle (device identity): the manufacturer's credential for the device key, a firmware release, the ownership chain,
// the owner's authorization, the device's fresh attestation, judged with what the verifier brings (clock, nonce, audience, trusted
// manufacturers, the recall list, the firmware policy, the status records, its reading of the notarized envelope)

View File

@ -117,7 +117,7 @@ async function judeca(artifact, input, online) {
// status lists and the revocations it was handed. The verifier makes every check it can reach and names every rule that refused.
const M = await incarca('identity'); const R = VERIFICATOARE.identity.reguli;
if (typeof input.now !== 'string') throw new Error('an identity presentation is judged on the verifier\'s clock: input.now (an RFC 3339 time) is required');
const o = { now: input.now }; for (const k of ['audience', 'nonce', 'trustedIssuers', 'statusLists', 'revocations', 'maxAgeS']) if (input[k] !== undefined) o[k] = input[k];
const o = { now: input.now }; for (const k of ['audience', 'nonce', 'trustedIssuers', 'statusLists', 'revocations', 'maxAgeS', 'request']) if (input[k] !== undefined) o[k] = input[k];
const r = M.verifyPresentation(input.presentation, o);
if (r.valid !== (r.rules.length === 0)) throw new Error('the identity verifier refused without naming a rule');
for (const s of r.rules) { if (!R[s]) throw new Error('the identity verifier named a rule this suite does not map: ' + s); pica(acc, R[s][1], R[s][0], r.rows.filter((x) => x.pass === false && x.rule === s).map((x) => x.name + ': ' + x.detail).join('; ')); }
@ -232,7 +232,7 @@ async function judeca(artifact, input, online) {
const M = await incarca('compliance'); const R = VERIFICATOARE.compliance.reguli;
if (typeof input.now !== 'string') throw new Error('a compliance judgement is made on the verifier\'s clock: input.now (an RFC 3339 time) is required');
if (typeof input.audience !== 'string' || typeof input.nonce !== 'string') throw new Error('a compliance judgement needs the verifier\'s audience and nonce (input.audience, input.nonce)');
const o = { audience: input.audience, nonce: input.nonce, now: input.now }; for (const k of ['statusLists', 'revocations']) if (input[k] !== undefined) o[k] = input[k];
const o = { audience: input.audience, nonce: input.nonce, now: input.now }; for (const k of ['statusLists', 'revocations', 'request']) if (input[k] !== undefined) o[k] = input[k];
const r = M.checkCompliance(input.presentation, input.policy, o);
if (r.compliant !== (r.rules.length === 0)) throw new Error('the compliance verifier refused without naming a rule');
for (const s of r.rules) { if (!R[s]) throw new Error('the compliance verifier named a rule this suite does not map: ' + s); pica(acc, R[s][1], R[s][0], r.rows.filter((x) => !x.pass && x.rule === s).map((x) => x.detail).join('; ')); }
@ -240,7 +240,7 @@ async function judeca(artifact, input, online) {
if (!r.rules.includes('presentation')) trece(acc, 'authenticity', 'the presentation is valid for this verifier: judged by the identity verifier with the policy\'s trusted issuers, this audience and nonce (the credential and the presentation signed Ed25519 and ML-DSA-65, both)');
// what the identity verifier inside left unjudged: a status the policy waived and nobody read is a claim not checked (PARTIAL)
const NEJ = NEJUDECATE_COMPLIANCE;
for (const c of r.unjudged) { if (!(c in NEJ)) throw new Error('the compliance verifier left unjudged for a cause this suite does not map: ' + c); if (NEJ[c]) nemasurat(acc, NEJ[c][0], NEJ[c][1], 'the credential names a status list and the verifier read none; the policy waived the status (requireStatus false)'); }
for (const c of r.unjudged) { if (!(c in NEJ)) throw new Error('the compliance verifier left unjudged for a cause this suite does not map: ' + c); if (NEJ[c]) nemasurat(acc, NEJ[c][0], NEJ[c][1], c === 'request' ? 'the presentation commits to a request and the verifier gave none to compare it with' : 'the credential names a status list and the verifier read none; the policy waived the status (requireStatus false)'); }
for (const l of ['anchoring', 'finality']) nemasurat(acc, l, 'not-judged', 'a compliance judgement is anchored by notarizing its record (an AIP-23 compliance envelope without personal data), judged as the artifact `envelope`');
} else if (artifact.startsWith('dataset-')) {
const M = await incarca('baza'); let r;

View File

@ -196,7 +196,9 @@ export const VERIFICATOARE = {
'delegation-claims': ['identity.delegation-claims', I, 'the scope of every link covers the claims disclosed'],
'delegation-audience': ['identity.delegation-audience', I, 'the scope of every link covers the audience'],
'delegation-revoked': ['identity.delegation-revoked', I, 'no revocation handed to the verifier, signed by the giver of a link or by the holder, has taken effect'],
unreadable: ['identity.unreadable', I, 'every field a check reads can be read (a time is an RFC 3339 UTC time, a hash input is JSON)'] } },
unreadable: ['identity.unreadable', I, 'every field a check reads can be read (a time is an RFC 3339 UTC time, a hash input is JSON)'],
// 2026-10-18 (B8): the presentation commits to the request (OpenID4VP transaction_data, SD-JWT transaction_data_hashes)
request: ['identity.request', I, 'the presentation commits to the request the verifier received: when the verifier gives the digest of its request, the signed binding carries exactly that digest (a binding without one is refused)'] } },
// a device bundle (the manufacturer's credential for a device key, a firmware release, the ownership chain, the owner's
// authorization, the device's fresh attestation and its AIP-23 envelope), judged with what the verifier brings: its clock, its nonce
// and audience, the manufacturers it trusts, the recall lists, the firmware policies and the status records it read, its own
@ -348,7 +350,10 @@ export const FINALITY_SITES = [
* revocation handed (a verifier applies the revocations it is given; VALID does not say that none exists).
*/
export const NEJUDECATE_IDENTITY = { 'issuer-trust': ['authenticity', 'not-supplied'], audience: ['integrity', 'not-supplied'], nonce: ['integrity', 'not-supplied'], 'status-unread': ['integrity', 'claim-not-checked'],
'status-none': null, 'revocation-ignored': null, 'revocations-none': null };
'status-none': null, 'revocation-ignored': null, 'revocations-none': null,
// 2026-10-18: the binding commits to a request and the verifier gave none: what it was signed for is not judged (PARTIAL), like the
// audience and the nonce. A binding with no request, judged by a verifier that gives none, has no request row at all.
request: ['integrity', 'not-supplied'] };
/**
* What the device verifier leaves UNJUDGED, by the cause it names -> [level, cause of the suite], or null when nothing the artifact
* claims is left unmeasured. A link that is not in the bundle, something of the verifier that was not brought (its nonce, its
@ -372,7 +377,7 @@ export const NEJUDECATE_DEVICE = { 'manufacture-missing': ['integrity', 'not-sup
* always supplied, so only the status and revocation causes can occur), mapped like NEJUDECATE_IDENTITY. A status list the credential
* names and nobody read is a claim not checked (PARTIAL) only when the policy waived the status; when it requires it, the verifier refuses.
*/
export const NEJUDECATE_COMPLIANCE = { 'status-unread': ['integrity', 'claim-not-checked'], 'status-none': null, 'revocation-ignored': null, 'revocations-none': null };
export const NEJUDECATE_COMPLIANCE = { 'status-unread': ['integrity', 'claim-not-checked'], 'status-none': null, 'revocation-ignored': null, 'revocations-none': null, request: ['integrity', 'not-supplied'] };
/** Verifiers published beside these that the suite does NOT enrol yet, with the pattern that counts their refusal sites. Empty since
* 2026-10-18: the Travel Rule and the compliance verifiers, the last two of the identity component, are enrolled above. */
export const NEINROLATE = [];

View File

@ -16315,5 +16315,190 @@
}
}
}
},
"identityRequest": {
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"now": "2026-10-18T18:46:46.000Z",
"trusted": [
"aere-id:562f57b77cd7d679b0f532de4ce161d77b284342"
],
"list": {
"statement": {
"v": 1,
"kind": "aere-status-list",
"id": "https://issuer.example/status/2",
"purpose": "revocation",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "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"
}
},
"size": 131072,
"encodedList": "H4sIAAAAAAACCu3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA",
"validFrom": "2026-10-18T17:46:16.000Z",
"validUntil": "2026-10-19T18:46:16.000Z",
"issuedAt": "2026-10-18T17:46:16.000Z"
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "VAWqnDiDcHbVGEJ3/JKzxf2iQ2WbisfZYiarENimk9AYr/p15+5K3FVO54zEvue3Mjd889+mkv1ooKFYfH2VCA==",
"mldsa65": "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"
}
},
"request": "0xcd5dbf15da49494dc8d166e14349393e3c3134b9a6b8bd19adb70d9e6dc7ee22",
"otherRequest": "0x154f8031f2b55e0115a71bdc8cf195104b9f28e34eade3202dfd8428fbc24f5c",
"withRequest": {
"v": 1,
"kind": "aere-presentation",
"credential": {
"statement": {
"v": 1,
"kind": "aere-credential",
"id": "urn:uuid:db69707d-6bb1-45fb-ad9c-c4cb75d37342",
"type": "ServiceAccess",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "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"
}
},
"holder": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "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"
}
},
"claims": {},
"sd": [
"YXErnXMmQQ-dBoKBooNn4JradJhgLNdTDafgUj84liA",
"z-g-gnjeUMBtn9Gs0ejG221J4xA4fVdzliaecIsrkaI"
],
"sdAlg": "sha-256",
"issuedAt": "2026-10-17T18:46:16.000Z",
"validFrom": "2026-10-17T18:46:16.000Z",
"validUntil": "2027-10-18T18:46:16.000Z",
"status": {
"list": "https://issuer.example/status/2",
"index": 1
}
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "Dgg0VtBp87EHd/WylhCG+7aqqAzfFlR7X6f0w8jV8Wzpx9VqyHkbs8tEoi12hPWSB7SePJYfJzLowAwpXZUzDA==",
"mldsa65": "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"
}
},
"disclosures": [
"WyJYTWUyODAxME53VkJiV1hacGZsRi13Iiwicm9sZSIsImRlcGxveWVyIl0",
"WyJqbUt5ODlYcExOV055NVN5aGhSUllBIiwidGVuYW50IiwiZXhhbXBsZSJd"
],
"delegations": [],
"binding": {
"v": 1,
"kind": "aere-presentation-binding",
"credentialHash": "0x8e2066db4ffb2972654bd2b25b5672927e75c74a403ae94052acb4588da563ed",
"disclosuresHash": "0xd6ba5f9f2b1b0509827a6c9382a75ecc7808061b7227e655c7cb38bc549d860e",
"delegations": [],
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"createdAt": "2026-10-18T18:46:16.000Z",
"presenter": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "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"
}
},
"request": "0xcd5dbf15da49494dc8d166e14349393e3c3134b9a6b8bd19adb70d9e6dc7ee22"
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "xmeYRK2lSE+aqPAyyCFicgTLScVBiZeSEzZ3ZOYFBXLohQdVF1wV8r29p4oNRNcaVnU9i9pSUUQlx/92EsbpAg==",
"mldsa65": "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"
}
},
"withoutRequest": {
"v": 1,
"kind": "aere-presentation",
"credential": {
"statement": {
"v": 1,
"kind": "aere-credential",
"id": "urn:uuid:db69707d-6bb1-45fb-ad9c-c4cb75d37342",
"type": "ServiceAccess",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "MIIHsjALBglghkgBZQMEAxIDggehAOthG2oBBbhW4WDQZRu8jJ6I43lSZSb851H/Hm3OMnN3nYhB8vDSFAJhdG9KyzlGLccWNMKg9EPhrvgpksBG5B+40uImjCNchV47U6/seeXNfj3ja7lmf9Ob9hrLIHbqoNH2F1fnLssWt4HHQhCe6IkBTElZ9ZjH2GCRi7ZJuojarEWW/VMwSx65lKQ6cqiM+ZekCU9zTYepIZ+vBJ1UTGmZ0MLs0UtTGjmdMoIFYeW2p6OyfCcpC34csdIJQKnmjNoSeG4KvKQfw6M/f8+idYb0mAdfi1apNMTV8DBaefxuP4pyJ7lGIPJnrrHM0UyH3q2TVFHcZXfXvgdGFZwGOpMLXlK60zQVAf7JnWdK3uZaWDf1LcQusI2tTzomO4hmApSatQgCGwg9TxyXTX1B28A+Fh4WIFmt0OjqiHdlYp/uHyp9PDM1994CHmn7JzxUcjY0vv1afNXzECeEVNLXmVoHem3j+oRIOUMM2SA2vSLLn2z5t4l2pEl+LcVMT67pGrogxCU9yTpTM8SRA8OX2wZKlBtrc1dSY6ddMjFew2MhScUZtwkp/4V09Eb7m0Nxt20qQWAIHa8QDtWQ/ZL8tfxx4DmjGqeG3YadaExhWPdw9fIjm+Nv5LhliQ6oB34l4fEp+YJ5ZLcK6baozr+QujUQlMHruueLJL9Zi/5CoxmBR2boBo8w+V/RyQbnZGW/YWa4IuHpRZaj/w4mJZKUZgV+V6cJOqsLwxLukIits5ZmqbVRUfbrRVKezxWPwC2y3w4EifEZZHZ+Wj2zTiuy+oVERo10n48oEArp0ZRBy70xURU9MGYEwSnIJ3zltB+yh2BNFGoRErZS6X7YWCWAduD1/dosf3adS6Ijp5TZiK/xiLbBTcJLClI0hUjyAs5F4n3vri3eZcDzT/E12NX/FGEgEB7K2ObbLx4TBfME7aLNSmCVRkKtDG9Rjsr1Bm3NnPIVJNevlGstRZO3aTR8F1GGxMl7iHjvjKhQPjwB23T8ac0Kdaq17BGL6nNvkLXvzGDL8ovYKA6EYitLCbwSG+cNNsa9sQ7u9l5/oraCiWf9BX+To43bym0Is/SHLWBnWFalN9UODQoFEYCAbz62MKKUz/4rLBre7wSeNFzKE1IrX3XQu3Ep1jDCq6R9wJDt2z01bn4l1LjeeKJY+aYs7/qns1JNQR48IVNQeyOkYZF0Njy+MY12HRF4qxzvMZhBFrscuDY1htEZfGkAWdX/MPud4klYT/RB7b6vsx6KKGDBd3H2yHu5JanEiRU0t0ZQ6kwfX1kwM3Jx43p/UfukHjbUFlkZ24oZGUQ9/92veDJNA5bYR7DIq7TSJ3Qtu2NK5sDpLKyDmmze7KV1PSKeFm2Lv3HctFS6UglTijHn5m28yqM0T0yR2tqcxpEAfJpW6Qx8ulGL23yCaDyEuX4Cl3e3KaxJ0nNgMfGc+BvZE8c2+O+9bDQinUrTP3C+nsOXx09OMY48hvjTyM4a2ezIZ6gup0sGlOPwdERT0bJwRfOBeeptGIDOo94uXTp7HQ3/+xcNuAVf2nOUI3M6tpNxqtfNnT3lm91I124Nt8iNA6kdRpujOxvS0Tyt+0WAGhHNokxfCI+3ZQTiunBc+byZ0hw7AVtoa3+jj0fN33k7zEyHtbnZzfgYdW88gsvuLhYJZ1ODo/DtSr4u4jogYesbH0h3TGkNTPKXgiJFs1qr5UzCVLaINm3yIDIbu0dB99GKMCWsTR/z2PmXRmRGRDEOTiaHeP+nRcccOdso19prb69mROxO7OF9FHE8o1FGSywRYadnyZhxUV9HfPM2uzbPsUx/NxSYhF7JZ7F7X6rKpc7+LeTR/hR2Co7dWHFG8pgpi/4Avf7+hZ+I7HPM3Ws1hTFYt6X/Ln9uzGjD1QHtBoeYDmTooWvQFNBaHXA81x8vEa3Ph5Lu6JZnvAG5qFmyUWglLYRFmDtANRZ0iAM7gjFwSJdXlBaw1guumCtYgxJ0Q8e1sfluNuDEvIcHVtHdsfxnvI7hXOPyck6AYGMwBCn0lULEX3z8Mhk+6K9PH0htAFmr3EGPHDIZB3+7aX6hGCrzaiOZOam46z7rM/so+wmU1TmQg/7mW8qk5UZgDlKnrNWjTFM69Fuioaa4J4tpUxhCx4JnQky2KHvZEc+4ekLKeLgywtDoDieuiT6zxgX6fwkkZSSVjOEMLQPb9EwZ3JgULQ8NL6xiescDsyVMhJ/VxFBr8xvZJDemrVi5WE02NcHmUlH18Ol/aEHGm8O0x0CNWCyoRJfEe2NTGP1KJfOazIgxKGZmh+3KSvTGEmSr5mguXzDTlDpGrP2ssDmZdNhNFEJ7/HqmoduAwONQ/UY4t0UAitcXTW77DnqjQQbWx4zLXPr7jGQKASgjYleafg/zdOv/115zQ02HXKvHwoHyBQIpyZYS7BqzyRr0yVW+6Uf2UQbehWOxXVNuHB/5PEPvT0FkshR0hDQmtQOi0jAGgbsWMTFKj0E0b7X1ntruzjLum5Btq1q92ZGtQKyqTEvpAmfZA4HmqE52CN4Ny7jvsHZdUQNL1atpEkQ0743JCbqH/oXUui+8AXfaaDhbT7yxUbxvSAu8cExMdQTO4JuF"
}
},
"holder": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "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"
}
},
"claims": {},
"sd": [
"YXErnXMmQQ-dBoKBooNn4JradJhgLNdTDafgUj84liA",
"z-g-gnjeUMBtn9Gs0ejG221J4xA4fVdzliaecIsrkaI"
],
"sdAlg": "sha-256",
"issuedAt": "2026-10-17T18:46:16.000Z",
"validFrom": "2026-10-17T18:46:16.000Z",
"validUntil": "2027-10-18T18:46:16.000Z",
"status": {
"list": "https://issuer.example/status/2",
"index": 1
}
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "Dgg0VtBp87EHd/WylhCG+7aqqAzfFlR7X6f0w8jV8Wzpx9VqyHkbs8tEoi12hPWSB7SePJYfJzLowAwpXZUzDA==",
"mldsa65": "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"
}
},
"disclosures": [
"WyJYTWUyODAxME53VkJiV1hacGZsRi13Iiwicm9sZSIsImRlcGxveWVyIl0",
"WyJqbUt5ODlYcExOV055NVN5aGhSUllBIiwidGVuYW50IiwiZXhhbXBsZSJd"
],
"delegations": [],
"binding": {
"v": 1,
"kind": "aere-presentation-binding",
"credentialHash": "0x8e2066db4ffb2972654bd2b25b5672927e75c74a403ae94052acb4588da563ed",
"disclosuresHash": "0xd6ba5f9f2b1b0509827a6c9382a75ecc7808061b7227e655c7cb38bc549d860e",
"delegations": [],
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"createdAt": "2026-10-18T18:46:16.000Z",
"presenter": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "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"
}
}
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "cp0EmFGGE2FeTW3LpUtk6mJyx0nVAuceyqA2XycapQjO0rVbGutkEllLPaM1+58+OC7VH/ruOM6i6TpAqKIuCw==",
"mldsa65": "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"
}
}
}
}

View File

@ -1,9 +1,9 @@
{
"v": 1,
"kind": "aere-conformance-vector-index",
"count": 619,
"count": 626,
"artifacts": {
"envelope": 64,
"envelope": 67,
"payment-risk": 11,
"payment-receipt": 10,
"market-timeline": 26,
@ -12,7 +12,7 @@
"audit-inclusion": 10,
"audit-consistency": 4,
"agent-ledger": 18,
"identity-presentation": 65,
"identity-presentation": 69,
"device-bundle": 194,
"sdjwt-presentation": 54,
"sdjwt-vc-presentation": 32,
@ -25,7 +25,7 @@
"dataset-consistency": 3,
"dataset-sample": 2
},
"offline": 610,
"offline": 617,
"online": 9,
"rules": {
"agents.anchor": [
@ -515,6 +515,7 @@
"envelope/authorization.form",
"envelope/settlement.form",
"envelope/provenance.form",
"envelope/secret-access.form",
"envelope/ai.form",
"envelope/agent-decision.form",
"envelope/data.form-null",
@ -540,6 +541,7 @@
"envelope/authorization.integrity",
"envelope/settlement.integrity",
"envelope/provenance.integrity",
"envelope/secret-access.integrity",
"envelope/ai.integrity",
"envelope/agent-decision.integrity",
"market-timeline/s1.proof-other-result"
@ -673,6 +675,10 @@
"identity-presentation/direct.presenter",
"identity-presentation/delegated.chain-cut"
],
"identity.request": [
"identity-presentation/request.other-request",
"identity-presentation/request.presentation-without-request"
],
"identity.sd-distinct": [
"identity-presentation/direct.sd-distinct"
],
@ -1293,6 +1299,27 @@
"requires": [],
"sha256": "0x126385bf75da4c67060bcf60827be429240efc0f7178ab47100c3d9cb7297a78"
},
{
"id": "envelope/secret-access.valid",
"artifact": "envelope",
"verdict": "VALID",
"requires": [],
"sha256": "0xddac3a094788b0e3544049d1410d1cec1730b56b736c7433f93028baf5e6e7f3"
},
{
"id": "envelope/secret-access.form",
"artifact": "envelope",
"verdict": "INVALID",
"requires": [],
"sha256": "0x7d1642d0493672f0b58d31756115606c21e1b4331a808e3a35b6ecab327364b5"
},
{
"id": "envelope/secret-access.integrity",
"artifact": "envelope",
"verdict": "INVALID",
"requires": [],
"sha256": "0x8c1f6f75345a65444348e01f9a95932cf62e0168b5e55fdb13934d306ad3556a"
},
{
"id": "envelope/ai.valid",
"artifact": "envelope",
@ -2324,455 +2351,483 @@
"artifact": "identity-presentation",
"verdict": "VALID",
"requires": [],
"sha256": "0x2cd843f0ac5a18485e606cc6c16278588c3a43b927d7f82c2779f454076bafd1"
"sha256": "0x5bda7a39decf2fb7977755a1d95e5886358cbf43c5b17e8bf2d2bfdb64d6cb63"
},
{
"id": "identity-presentation/delegated.valid",
"artifact": "identity-presentation",
"verdict": "VALID",
"requires": [],
"sha256": "0x69f157152efa834126703216e74db21df431f55929ee174e1b96aa10c26479ba"
"sha256": "0x80a749192d74e1bcbde52fb07f935ffc2d4d87cc823a66e91db76be86dbb36e4"
},
{
"id": "identity-presentation/direct.no-audience.partial",
"artifact": "identity-presentation",
"verdict": "PARTIAL",
"requires": [],
"sha256": "0xf6a4f806012e7241310031a47c89c5991531d68da82a4e03693adb62fcfed862"
"sha256": "0x59967adcc5774dcf6404ad9b478aa4ae159427bb08d6dfe518d746970d2400aa"
},
{
"id": "identity-presentation/direct.no-nonce.partial",
"artifact": "identity-presentation",
"verdict": "PARTIAL",
"requires": [],
"sha256": "0x3ff031f2d016edd5a97cb41699d7bfa535c2a11a412ce4d06a6c5acee02eab80"
"sha256": "0xf97eefc540711aa77266834a65ca4088b55a7503e060d4e8faf100bec5a64a80"
},
{
"id": "identity-presentation/direct.no-trusted-issuers.partial",
"artifact": "identity-presentation",
"verdict": "PARTIAL",
"requires": [],
"sha256": "0xf29f1d3c13ac9f893d72bd81b50caf55022cc4b2d7b60d95f1414365ea4626e7"
"sha256": "0x7785a2c7300e9664af643ad35df64a2e8d6bc4b6efcdb00ff979174854641306"
},
{
"id": "identity-presentation/direct.no-status-list.partial",
"artifact": "identity-presentation",
"verdict": "PARTIAL",
"requires": [],
"sha256": "0x4300a19338893abed2b1e9623bb795107a2ac9acfb43c58bb549d5725220833c"
"sha256": "0x9682272f06332dba3a948b6893732d5ac77a37ea1bec439eafaee60ac58a1c70"
},
{
"id": "identity-presentation/direct.status-list-other-issuer.partial",
"artifact": "identity-presentation",
"verdict": "PARTIAL",
"requires": [],
"sha256": "0x2627d5a91f32ab3128e158a2c19b44010c58ae1061209192befe5ad92b0fc2fa"
"sha256": "0xe6705b867ebf0501818e4cd6e63c52ea17cc71c8a1de34b6ebd078a9ba9410e7"
},
{
"id": "identity-presentation/direct.status-list-expired.partial",
"artifact": "identity-presentation",
"verdict": "PARTIAL",
"requires": [],
"sha256": "0xf2fa7a516954590bd8225783262be59fa9a7783473ad95b736dfe478672f2b4c"
"sha256": "0x3a5ca098291a89f6eb74573510c089ac468ab6b9a39bd18f092d762a046ec235"
},
{
"id": "identity-presentation/delegated.revocation-by-stranger.valid",
"artifact": "identity-presentation",
"verdict": "VALID",
"requires": [],
"sha256": "0x0cc30308af78fe2ca098a06be7fa298ab3797d15e9a45ab136ccaa3f6c9f57b6"
"sha256": "0x174e387f5d8564335ea9fe19b99d8dfc2d3fee851d4b0d2e354f594e4f5680e2"
},
{
"id": "identity-presentation/delegated.revocation-in-the-future.valid",
"artifact": "identity-presentation",
"verdict": "VALID",
"requires": [],
"sha256": "0x1094885bd85aaaf57989d15a4a76946283f4c9e5e510523953ef2ab72f48a5d4"
"sha256": "0x4f2ea7bd2e56059154a7b11d14a608dabe4ce66fdc7ddc1ab8dc724f1e22a09a"
},
{
"id": "identity-presentation/direct.form",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x6db970fc0e701dd82e77a092f215952c1fe66ba63fcbe8a40c20718088771db3"
"sha256": "0x90284fea67b4efce0d0b4f205c1da0689fc91452409410c03b398aa72da749b8"
},
{
"id": "identity-presentation/direct.form-null",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x2fbfdf1c7b6df911453731c32257e038d2f137d3c5c8cda0891bdf37f8b75b58"
"sha256": "0x2218abb9a5e141ea822aaedcaba5b4d98b0465ab105963afe92403d7736955a6"
},
{
"id": "identity-presentation/direct.form-credential",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xfcee75ae1f03392e53b6ca85a0001c118128c1c70539025b8dfd3ee02758b4a6"
"sha256": "0x61f140ed0cd4db054518a3cb1203a3b792218ddf53d1c332a9095b5e6db49146"
},
{
"id": "identity-presentation/direct.form-binding",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xdd5e4eaa39c9f3bf2572a88b95b99ab330c8a1ec41d4b5c4fb2d78f600bf5c27"
"sha256": "0xd20dc53d3a66fd7e91438ae48f6da9f13edfe98abd12250ec40c7628cba1290a"
},
{
"id": "identity-presentation/direct.issuer-trusted",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x413806a53ff65f4ac24b5377429258fd32ddedd0b7c390bfe3280d0c13bc46a9"
"sha256": "0xc3bb5f08fc6de014590d5357225d0812827cc538ce5c3251206dfc80dd6197da"
},
{
"id": "identity-presentation/direct.credential-revoked",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xb365e9a08e4bcdb1fad9c21aab3b0c9327ee1a6a8b5c86ca048e1048263ccd30"
"sha256": "0x4628059c9f6a984ccaf568cd7dd340762664b70c77bb8f77efd8e3859b1ffbcd"
},
{
"id": "identity-presentation/direct.credential-revoked-second-list",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xdaf536b463d57407fd8d940cb01941ffa0dfd5a78fd6f0fe78a4174ba969dd8a"
"sha256": "0x404df59cc17b4a1fce92b520b1cf8a5d060f66a080e7e067693493a754857090"
},
{
"id": "identity-presentation/direct.status-list",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x7d1870e20c27412ff712f28d9b06a4d8d9c4edc55711d0e0766ea6351bcfd34a"
"sha256": "0x73fb8a2c22b8c670b8044fc90109b58db759345b657478cc91fd04fdab060e8c"
},
{
"id": "identity-presentation/direct.status-list-index",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xc0b9af28f8bf6bcc9927e9aeb77415b1a6cf64609452944724040febe954f432"
"sha256": "0x8637e64b21fb478c0cbbb7bc26ad2a8eb54711c58ee9f57ac88b91110ffdb430"
},
{
"id": "identity-presentation/direct.binding-disclosures-removed",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x4c89bb4dabb9fab8abb44577f4dce13b7b44745edb3dac66a42e0467748ec37e"
"sha256": "0x068371112abf4a3a98cf30be59ba9dabc079dfe4f125a026e52c55085221f7b3"
},
{
"id": "identity-presentation/direct.binding-disclosures-added",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x16168c0fe0cde15b4db0d9c09c673867e5a30fce61c3b0076ac62875c55aec26"
"sha256": "0xa42c8b5cbb9015040990b7c20ef7b10c46c9fd08dc2a1f2609c64ad3e953d8c9"
},
{
"id": "identity-presentation/direct.presentation-signature",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x391ba6f98d09a91ac92ba2025900d534e34236acda69d8d31f29768c216e0083"
"sha256": "0x8c38a9237f39605c3a53239f5c63238ece8761344ba6beed2086a16caa1859c9"
},
{
"id": "identity-presentation/direct.presentation-signature-ed25519",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x05500d4610b6da075b34cd54cfa4cf566c6c6ab0fbb701e3ca56d09a74615136"
"sha256": "0xf1a271e201aba7930d9fc9d43d9a0f0e193b98d7d305462a028d11f62d074a1b"
},
{
"id": "identity-presentation/direct.presentation-signature-no-ml-dsa",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xf9bb52b57c83e1477919b70f0f57a56524fdc313a7200967e41f58e2175e72ba"
"sha256": "0x86f8552c368bd98c58b8df00543fb5421c6a0d2957b5ec8dae00c6c981290cd3"
},
{
"id": "identity-presentation/direct.presentation-signature-alg",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x49b54e807b0563aa5f8750c84a28e4eda146dd145e6efeccfeab8f989c128dae"
"sha256": "0x2cceb0b92ffe3afc839351a7690842422061049cc198e332d8479bd63ee5e70a"
},
{
"id": "identity-presentation/direct.audience",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xd460a9059f5884c75eae011538d76bb47392fa03c56aa30da5b19c3eeebd359b"
"sha256": "0x30023d18af77d4baf83341403be1363ff445bba8e7d5c899bfd86a0ebdde456b"
},
{
"id": "identity-presentation/direct.nonce",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xa36492e495e920e5ddccc15b7b097d8427013896ba039e567edb2d702636bd81"
"sha256": "0x2b8711028b9ef53576530a99fff0adb6f09471d7ba7abaaa6d840c1f1ae7de77"
},
{
"id": "identity-presentation/direct.freshness",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xd6f69e447ecb0c964d54660f0cd3f55709b0a0996cddd3bc9e90bb84ddecccc5"
"sha256": "0xe0cfa1311e1b66dbf6d0d86813359ce88a62e658398a37a756bd3cd095ac48b4"
},
{
"id": "identity-presentation/direct.clock-years-later",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xd0a0eb8a67bcad06abd156ef182b6c3c608e1d2f3b76883efc56fd15cbda6a34"
"sha256": "0x16c9ca931c108c20d3fb1336d06a3791894530adb2cbe0d513cc9e2ed274e6db"
},
{
"id": "identity-presentation/direct.issuer-id-impostor",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xc3f4eb80e51f6383c41be9ef1982622e53ad16b1bff06b13e06c7c711b39b259"
"sha256": "0xb938bef9630b6066653663d2397a9adb3abc1cc1f3f225fdb94560f367612aaa"
},
{
"id": "identity-presentation/direct.issuer-classical-key",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xf191d6941d3fd41737c7732e945d72e0523d9a45dcdcd6f43f51dcbf37429cf3"
"sha256": "0x2c8a27f66512febcd5cb149a65cb331209d7320c03db27514d2a553971f9980d"
},
{
"id": "identity-presentation/direct.holder-id",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xdd4caad757b0bd3f0030d60fa77c6b1fb714f286b159985637cd2f3f93ce1e45"
"sha256": "0x224cee9d6c3095d5d3afe6cad49b8a6c3f0679b98e083c5f7921e970485fd033"
},
{
"id": "identity-presentation/direct.credential-signature",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xd9182e35ce04b40f47ed2ffbeda58941996b70bdf6fae1994a0012280f474afe"
"sha256": "0x91bd57b524db3eed51aa383a12647dab6cee390d3ef1c37d01b2ce25b6d6674e"
},
{
"id": "identity-presentation/direct.credential-signature-ed25519",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xdcd93cf06e46b5ecc07117c6bb436d10618bcef9ca16bbc61b41f17dbe9a6246"
"sha256": "0x8042966979e4c450e3ef3a72e31dde3a26b004b756a606a236d09e2af4c7a9a7"
},
{
"id": "identity-presentation/direct.credential-signature-claim-edited",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xfac17f6b19d37ed30536183fe1a44510be217d411db02a0432b198ccde48ccf5"
"sha256": "0xa00ac93e32587c0e5f2a49bb74fe176d323b3bb3a3994754c687c213ec281cb7"
},
{
"id": "identity-presentation/direct.credential-window-expired",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xbc3b1851677841c6c20c92dba19e4a828a71ddbaef8b6473c2eca9fef3022ee2"
"sha256": "0xc4f262216f654d1bf2348a8a03e58305679378db3f6c1d1924f42c3442cb5055"
},
{
"id": "identity-presentation/direct.credential-window-not-yet",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xd47462221a1fed170975c56e1d188a8dda809d554cdccdbcbdded9bf4d82b357"
"sha256": "0x11cd5af738e2937d202c2d50cfe7d78da46ff1b46414563c1ccf0ad08990a37c"
},
{
"id": "identity-presentation/direct.sd-distinct",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xf2cbffbc8b9ba8beef0344e91f43619f2faccf650f28473a01fe1fa87f956001"
"sha256": "0x68d2f91bc544f2294ce60489a458feef1bed1ca65e68dcc7dafc56eefdc68ff0"
},
{
"id": "identity-presentation/direct.disclosure-readable",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x95620de4af04dcfed3ca517171853741fc0f9612723a75aa365d6e058b29b38b"
"sha256": "0x3ef3cb7730cf0cbb8e2dc68b19a3e750946fc1b84ab8cbfc82040e8aab3470ea"
},
{
"id": "identity-presentation/direct.disclosure-signed",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x32095f4750634a06d20a7f7d24052d43e3279cba65d0edfbf9fb9079931e5cab"
"sha256": "0xb65a67c3dcba5d92c60baedae037ad3401a8e1e644b1773ffaede1766e5eb04d"
},
{
"id": "identity-presentation/direct.disclosure-once",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x8b33ae2df90f7753fc7bf6f2fdab97652d55faca5e301abacd4c5b7dcc530f72"
"sha256": "0x1bde3f3eb5a410396ac88a4f95e4c78121dbd2b45913c075f84a89366a57747a"
},
{
"id": "identity-presentation/direct.disclosure-plain",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x59a94a95d8380db071d0fb0d1ca5dd08bfd4492055051f5952c1c4b6ddc09a8f"
"sha256": "0x94cc8848bc68d8e19b500ed38bdc5c98ae28c4e62efe8eed95f5b610b101b4b3"
},
{
"id": "identity-presentation/direct.claim-name",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x898ebaa48f976857b948eb8e78c5a23a860dc152e56a9b76bd2dcaab252d29e7"
"sha256": "0xc50a2afde09c5dcd76d83188309c2b45c39abe0b7b4529007d437d807d91c5c0"
},
{
"id": "identity-presentation/direct.binding-credential",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x388ad1d6f32d8432dc6f33ce54696ee50009f7500c8cec227ef51d9b3fccc3c4"
"sha256": "0xa614cbcd0c46e95eefbfe6ceabd1c8e3cdd3f98ec94d890a4b977da69a7a24b5"
},
{
"id": "identity-presentation/direct.presenter",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x8f5fbe53736fe9b41511a68d5a0c66ea8cd993be6c3545681f1432f25845d5f8"
"sha256": "0x8fc1c10a2c45e355f6a505356b1f643d7ca30a9d24cbbf10ebe74e2f838ba10a"
},
{
"id": "identity-presentation/direct.unreadable",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xcbf5ddb07fed4b0f965eada47b98c724fa7e4351770334bbb6233d1c28ca45b6"
"sha256": "0x189c1fc38184b2baa10bb58b77f9207f32725d4edca7ba4c9687c46c1bcb87bf"
},
{
"id": "identity-presentation/delegated.delegation-revoked",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x89c52f384796c4441818b0c5a0b4ed0e0498882771a87cb43c08c55407d91b60"
"sha256": "0x49f60f505f6c7b2ddf95ff8be36a81c594d52024834dd7e1647ae8a063443b4e"
},
{
"id": "identity-presentation/delegated.delegation-revoked-by-giver",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x5bd8a19abcd7ac65395b59a689bc9da7bdfd08edeee16af02e88df32764bf4cc"
"sha256": "0x20ad4492d83b3520455494241bfc73c734bc8544d80aa54189dc54dd48aed1aa"
},
{
"id": "identity-presentation/delegated.chain-cut",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x080530f20b328172215992597feded8e108048a58397bc8a670cd25e840a773b"
"sha256": "0xedb36684b8735ef8450c6d31019bf5331f223a959bec1c4cb863e0362a70eeb8"
},
{
"id": "identity-presentation/delegated.binding-delegations",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x5fe866c0a5b4640b46799aed8431f8eb1c05a4c4cedc4f59b5e52e184272d74d"
"sha256": "0x9afee83f79cbfdf1b4619b78bb30374af76214892126a8fbcf1d43d2f7cb872e"
},
{
"id": "identity-presentation/delegated.chain-length",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x16e79221ea462ddbc126d206be22a9917b0fa8407ddc23f247bbdaa070197928"
"sha256": "0x9c6ec6c46529c1e8aa6d73a922c4cb00be4897f5e195d60d12b80bcf7b88033e"
},
{
"id": "identity-presentation/delegated.delegation-form",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x98f161844bc16c16b03b90736aa87eaaf66018b1be88cbd8bb803cd7e6240d51"
"sha256": "0xebb32fee31fdf759eb35fce0502199c3808bef61b9e12c6794df34aff46c03b5"
},
{
"id": "identity-presentation/delegated.delegation-ids",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x5f77221f87875f46310578f3322b4c5ff70bd798ce658f97bfba513782c8b090"
"sha256": "0x3c28556210936f230388ede3302706a09f2f5c0ebae845d9a8e0a0f198c2dcce"
},
{
"id": "identity-presentation/delegated.delegation-from",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xa4cf18b5efceb038a681c68c60ee26fe7cd1b8e6b4c9c01dda31977630b31c84"
"sha256": "0xdc6e07e1709ed4e5bccc07a268b629f5ce010fc7f87697926191da06964d7bf9"
},
{
"id": "identity-presentation/delegated.delegation-parent",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xb89a5b8d3b4fc4c97af63a7786cca1d1e015f7e718f22f946b0ee8d8d983c643"
"sha256": "0xfa308e816f27e16fa9c94622b78de9f755dabf908c668bc202e2d4490aca7de2"
},
{
"id": "identity-presentation/delegated.delegation-signature",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x7773b50cffa03823195da0467c79d28304aa65bb3960590b35286cd0b8342a5e"
"sha256": "0x03a12f5558142107858821bad65063b0633e2aed5ab22ad1490dba2fbb703e25"
},
{
"id": "identity-presentation/delegated.delegation-scope-field",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x4c4f28d9b020136af57bce74c34286fcc7dff51155a7036739967c00ad7bea0e"
"sha256": "0xf97bbc8a244be334f215f55f0d654a03c436f6ee91a727e3ccd1cfab440d2086"
},
{
"id": "identity-presentation/delegated.delegation-scope-unsorted",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xe9212d24c67684e22fbd2f747ebcc19c4816edf272ab54802659a14bad30508c"
"sha256": "0x0e76821c16f47109fa83f414297ec192404971dc20d760166c15bcb874e39203"
},
{
"id": "identity-presentation/delegated.delegation-window",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xaea4497bfe401612741bd9f4d6c3b6eaa2cc5ba7b54cda1ab31344913ad09087"
"sha256": "0xf872b21e860b0f14920b35fed2e134140a52afdb757ea892622d44710fbf8b29"
},
{
"id": "identity-presentation/delegated.delegation-depth-zero",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x4a3ddc53858f8ce134028070a2765dff1e299299c9db5db6f6fcfaaae8311b7a"
"sha256": "0xfd791d04a75180809693c8b3f1681478e1c603159fa704807e89bf99976ae2bb"
},
{
"id": "identity-presentation/delegated.delegation-depth-negative",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x5a53e8bc5d283632e4488775c5363314663d80305885d2c5b8be28088dd81c47"
"sha256": "0xe9b1d39c5cedb5f6cad6050a959f5ee608c8a52ef6dda81bd9d34cad46a5771e"
},
{
"id": "identity-presentation/delegated.delegation-narrows",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x0152cf57b7c5f694b90ae038f03244ab91762ce60a6db96272235095b00de618"
"sha256": "0xa1baf02e96d7a6c4d0a893641729a15d44162164115a6129cd7dfe9ebb9f3621"
},
{
"id": "identity-presentation/delegated.delegation-credential",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x6cf0ddd08c7cdb6f74a53b96cf7e51ff63e44cff7da933a40f55a9deb011f657"
"sha256": "0x4508ae807c520af23d6565df5dbce60f9f93190c9a9903ad67001e3b407c27c1"
},
{
"id": "identity-presentation/delegated.delegation-claims",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x6854d5ad358838087a96de9eb7cd6ba7b579a639fc95253d3f7333bc44539fae"
"sha256": "0xac16a177d27ca84f67f7225c1cce4b2037c99e55f60ad6b1637c3a81779a9738"
},
{
"id": "identity-presentation/delegated.delegation-audience",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x9e9c83d66a52897c503531e8f742c7d24a9d6437898cbec6997861039d4a497c"
"sha256": "0x9cc216c94373b5b6dc5824bc0370cf1af5a4978b062ffad94e5452fe2f708281"
},
{
"id": "identity-presentation/request.valid",
"artifact": "identity-presentation",
"verdict": "VALID",
"requires": [],
"sha256": "0x30a9ca22d34f41be3ccc1e7543fba36ca23126c23399248a072bd077850f6645"
},
{
"id": "identity-presentation/request.no-request.partial",
"artifact": "identity-presentation",
"verdict": "PARTIAL",
"requires": [],
"sha256": "0x595b3b2c0415d981824bc8550ba6a02707fa84890ad804ad3046f2c6632a57e0"
},
{
"id": "identity-presentation/request.other-request",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0xecd9d2e2f2b7b51242b377a437ec56e5b169a69d67547a7bcd41959012e69304"
},
{
"id": "identity-presentation/request.presentation-without-request",
"artifact": "identity-presentation",
"verdict": "INVALID",
"requires": [],
"sha256": "0x038897cfa683b4f6185525da4442af7ea3f12d74c0baeebc53f0cb9897e5363c"
},
{
"id": "device-bundle/sold-once.valid",

View File

@ -0,0 +1,57 @@
{
"v": 1,
"kind": "aere-conformance-vector",
"id": "envelope/secret-access.form",
"artifact": "envelope",
"title": "secret-access: the statementHash is 63 hex digits",
"source": "derived from envelope/secret-access.valid",
"requires": [],
"exercises": [
"envelope.form",
"envelope.integrity"
],
"base": "envelope/secret-access.valid",
"mutation": {
"op": "set",
"path": [
"envelope",
"statementHash"
],
"value": "0xcb48955f43aa3108fea9deecaeb14240287e253b4ab97fb27a07fd7fa36c9be"
},
"input": {
"envelope": {
"v": 1,
"kind": "aere-proof-of-secret-access-attestation",
"statement": {
"v": 1,
"kind": "aere-proof-of-secret-access",
"presentationHash": "0x6e36fd65925c49ec4f04faa69ea1c9c656f8f182706b59788b5fd98f6d9a9f9c",
"policyHash": "0xf062025b73d9324b189fcec2b5c88338aa6fa9fb7877700681b2049b57e6aedd",
"service": "https://secrets.example",
"secret": "db-password",
"version": 2,
"operation": "get",
"verdict": "allowed",
"rules": [],
"grant": "deployers",
"holder": "aere-id:0000000000000000000000000000000000000000",
"presenter": "aere-id:1111111111111111111111111111111111111111",
"createdAt": "2026-09-26T00:00:00Z"
},
"statementHash": "0xcb48955f43aa3108fea9deecaeb14240287e253b4ab97fb27a07fd7fa36c9be"
}
},
"expected": {
"verdict": "INVALID",
"failed": [
"envelope.form"
],
"levels": {
"integrity": "FAILED",
"authenticity": "NOT_MEASURED",
"anchoring": "NOT_MEASURED",
"finality": "NOT_MEASURED"
}
}
}

View File

@ -0,0 +1,58 @@
{
"v": 1,
"kind": "aere-conformance-vector",
"id": "envelope/secret-access.integrity",
"artifact": "envelope",
"title": "secret-access: statement.presentationHash changed after hashing",
"source": "derived from envelope/secret-access.valid",
"requires": [],
"exercises": [
"envelope.form",
"envelope.integrity"
],
"base": "envelope/secret-access.valid",
"mutation": {
"op": "set",
"path": [
"envelope",
"statement",
"presentationHash"
],
"value": "0x6e36fd65925c49ec4f04faa69ea1c9c656f8f182706b59788b5fd98f6d9a9f9c0"
},
"input": {
"envelope": {
"v": 1,
"kind": "aere-proof-of-secret-access-attestation",
"statement": {
"v": 1,
"kind": "aere-proof-of-secret-access",
"presentationHash": "0x6e36fd65925c49ec4f04faa69ea1c9c656f8f182706b59788b5fd98f6d9a9f9c0",
"policyHash": "0xf062025b73d9324b189fcec2b5c88338aa6fa9fb7877700681b2049b57e6aedd",
"service": "https://secrets.example",
"secret": "db-password",
"version": 2,
"operation": "get",
"verdict": "allowed",
"rules": [],
"grant": "deployers",
"holder": "aere-id:0000000000000000000000000000000000000000",
"presenter": "aere-id:1111111111111111111111111111111111111111",
"createdAt": "2026-09-26T00:00:00Z"
},
"statementHash": "0xcb48955f43aa3108fea9deecaeb14240287e253b4ab97fb27a07fd7fa36c9bee"
}
},
"expected": {
"verdict": "INVALID",
"failed": [
"envelope.integrity"
],
"levels": {
"integrity": "FAILED",
"authenticity": "NOT_MEASURED",
"anchoring": "NOT_MEASURED",
"finality": "NOT_MEASURED"
}
}
}

View File

@ -0,0 +1,48 @@
{
"v": 1,
"kind": "aere-conformance-vector",
"id": "envelope/secret-access.valid",
"artifact": "envelope",
"title": "the secret-access envelope from its producer",
"source": "producer on fixed inputs",
"requires": [],
"exercises": [
"envelope.form",
"envelope.integrity"
],
"base": null,
"mutation": null,
"input": {
"envelope": {
"v": 1,
"kind": "aere-proof-of-secret-access-attestation",
"statement": {
"v": 1,
"kind": "aere-proof-of-secret-access",
"presentationHash": "0x6e36fd65925c49ec4f04faa69ea1c9c656f8f182706b59788b5fd98f6d9a9f9c",
"policyHash": "0xf062025b73d9324b189fcec2b5c88338aa6fa9fb7877700681b2049b57e6aedd",
"service": "https://secrets.example",
"secret": "db-password",
"version": 2,
"operation": "get",
"verdict": "allowed",
"rules": [],
"grant": "deployers",
"holder": "aere-id:0000000000000000000000000000000000000000",
"presenter": "aere-id:1111111111111111111111111111111111111111",
"createdAt": "2026-09-26T00:00:00Z"
},
"statementHash": "0xcb48955f43aa3108fea9deecaeb14240287e253b4ab97fb27a07fd7fa36c9bee"
}
},
"expected": {
"verdict": "VALID",
"failed": [],
"levels": {
"integrity": "PASSED",
"authenticity": "NOT_MEASURED",
"anchoring": "NOT_MEASURED",
"finality": "NOT_MEASURED"
}
}
}

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -41,6 +41,7 @@
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"

View File

@ -0,0 +1,177 @@
{
"v": 1,
"kind": "aere-conformance-vector",
"id": "identity-presentation/request.no-request.partial",
"artifact": "identity-presentation",
"title": "the verifier gives no request: what the presentation was signed for is not judged, so it is not VALID",
"source": "producers (identity: issueCredential, createStatusList, present with a request), two hybrid key pairs made and discarded at generation",
"requires": [],
"exercises": [
"identity.audience",
"identity.binding-credential",
"identity.binding-delegations",
"identity.binding-disclosures",
"identity.chain-length",
"identity.claim-name",
"identity.credential-revoked",
"identity.credential-signature",
"identity.credential-window",
"identity.delegation-audience",
"identity.delegation-claims",
"identity.delegation-credential",
"identity.delegation-depth",
"identity.delegation-form",
"identity.delegation-from",
"identity.delegation-ids",
"identity.delegation-narrows",
"identity.delegation-parent",
"identity.delegation-revoked",
"identity.delegation-scope",
"identity.delegation-signature",
"identity.delegation-window",
"identity.disclosure-once",
"identity.disclosure-plain",
"identity.disclosure-readable",
"identity.disclosure-signed",
"identity.form",
"identity.freshness",
"identity.holder-id",
"identity.issuer-id",
"identity.issuer-trusted",
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"
],
"base": "identity-presentation/request.valid",
"mutation": {
"op": "remove",
"path": [
"request"
]
},
"input": {
"presentation": {
"v": 1,
"kind": "aere-presentation",
"credential": {
"statement": {
"v": 1,
"kind": "aere-credential",
"id": "urn:uuid:db69707d-6bb1-45fb-ad9c-c4cb75d37342",
"type": "ServiceAccess",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "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"
}
},
"holder": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "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"
}
},
"claims": {},
"sd": [
"YXErnXMmQQ-dBoKBooNn4JradJhgLNdTDafgUj84liA",
"z-g-gnjeUMBtn9Gs0ejG221J4xA4fVdzliaecIsrkaI"
],
"sdAlg": "sha-256",
"issuedAt": "2026-10-17T18:46:16.000Z",
"validFrom": "2026-10-17T18:46:16.000Z",
"validUntil": "2027-10-18T18:46:16.000Z",
"status": {
"list": "https://issuer.example/status/2",
"index": 1
}
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "Dgg0VtBp87EHd/WylhCG+7aqqAzfFlR7X6f0w8jV8Wzpx9VqyHkbs8tEoi12hPWSB7SePJYfJzLowAwpXZUzDA==",
"mldsa65": "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"
}
},
"disclosures": [
"WyJYTWUyODAxME53VkJiV1hacGZsRi13Iiwicm9sZSIsImRlcGxveWVyIl0",
"WyJqbUt5ODlYcExOV055NVN5aGhSUllBIiwidGVuYW50IiwiZXhhbXBsZSJd"
],
"delegations": [],
"binding": {
"v": 1,
"kind": "aere-presentation-binding",
"credentialHash": "0x8e2066db4ffb2972654bd2b25b5672927e75c74a403ae94052acb4588da563ed",
"disclosuresHash": "0xd6ba5f9f2b1b0509827a6c9382a75ecc7808061b7227e655c7cb38bc549d860e",
"delegations": [],
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"createdAt": "2026-10-18T18:46:16.000Z",
"presenter": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "MIIHsjALBglghkgBZQMEAxIDggehAKPi1zCRbHAXzBZu4IBG3t5vXCYRQdrFxq8CyRVqy8Ye5arrPhFPizJgXlE26i2hNy/h/PVa1f/1P11xUoMXIrI4JE1Z1oTBMOZt7iCVPnd+ZhmxXMj+GqD7fMWfw31tTz7Cz/RaueXtriLClpPVHOOIKxSD324JTFBcykiC65KZmsubPAGEFaECkKzncjqeioHXTA7tYNPX83fYd8HuMxrFaE87tKsQOBWt6Z8zSBipJV15n34ItE40XEpMbfCmf5j1A1n7M3sJzrILNyvjxSmS63wsJSsrWDRtLFWTDS6vN8dmdZPY0WfDwrMv0NQCnBrq3gVcuLZp4xdW8Qp0/K2HGOimo6vZ4sdYFgobg+J+bfjUvhDp6DyhYJWJN+fSkKzv0BxfACnsPpBtSUUFmBp/WTVhmXw4sDQY/wmcPZksYSiRMQGw8ByG8NGK2ln0Q/tdTcC1pWm8RYq3cx8sP5MRsnYFEuQqwPwPgc9V5NdwaddRGZdYYTYun8+eQMdYpJ9kPkOsLz2pqx9eTgGZLRN6QKBYzZwDBcer2q8SYxRxluQC7ABSSx8Ba/Dq/vygnCfB+pVSGrMqFDlw1jvex/eSrdOUEG9aIgIGCwuPbQO6ChiePHfPnr4SKhlEQZcEm8dLM/vI/Z9PU42f61jAq5Ek4MSeFU2goeAhOxBWjmllo1Wk0vIlBDGFfkeyTAtBarSdvT5EpM6rVzGh+rCB0kg2M4tWac9dI0Cbo1xnp85udY7VA7y+k7UxXjwJQVUezi1XnKL/wf6q8wj0TCFUItrzrFOGvJPJrptnIPxjlRzrfIFuo5nBBg2qMiwvnVnQ7EQJ9Zooof/zdJ1v7Rn50SWESWX+TVOh4o9F9USM4JPJKhMTNAiBLI9e+dd6wZTlYPm7UaaCzMKgFpkM3N7qVhK4JTqGVMbUtyQK5jL+WDVytWj9bJwgLsBQZIQdIZWQiqOcSEmcBUByVBrRQA/JTdG7Y2oHdR1huxXDk7hNVwyN4GQj5ClBlUtJPdgGaB3odYGXoI08hORWaqfpcBD1Ie69xBQods3FwrM5ZEXwkNJpPhWx4BmwWd77u2wivg+yh5gaAjkqPmFuquhsadxJ38EsZos2vtQLeYwEDgCMIL5ZhoukxE++j6ozm1/3IrxQP1XfFsEJqhVyQYfOJty2g5A8Ukv+ium9iwxiEYdE09Y2UQWQ3KXrftYnd+ISaq0rfwse/waptcsdlaRYorOjIvjDRuy2mtW0cD5YKSG2aQ+vWYqD8tnNmQeWWhujdB76T294AyCipUUZhDvgCEAo5JK1uqdvESTlcEIsug2wsMNzd4hGPiTzmiUdzRDKYBP7dFmGhnf/XdNfRJ7VOlQg1NYwLDg3ZimmAlcAuPUkyS6Itn/GnaWpDZsURmy/Xw6duemM+zN+ZEpnz70DKplMir/AW+Sxf4/+MQDnw7Fx6PGn+lSFCGOhH3BjywwKagmjFvZ+AS8Hvvo/ioxnokOCgKf0wDHwYpOug7I4N+ntr2HAgw0MNZfN2ndRoI4mq/j9BjC92xacgPjA3PSH3Zg4Tgp7Cct7ogEjjww18EA6ZJ6F5pdzA9BuXeOz6u1P3u4TwhMXeYflnLMKwSJHPEu0D6Niz7imYeCx0hdW36NMn6pc4aA7VUiiiD7QQNe9My9wECwOPeSE2Mizlkvd7ROHqigRRhfctVsHB2pLILIjlz6qWEN7931kN5/13aROJBhUowczxdkSRsqB5/t6bPA9tV3xb5M1Oe5yjwz4YcF/9uU0UAm6GSyt3I89YdPVdLM+Fc2Vj/GLgmKmCSwh5oiNEKApLdBdkv7zvPGjvW9jLPPgc9Bqz4Frc+CreAG0epW5RB4HFIXwFYRIgrnnS3L9w3P2tuqkaJ24U/SauJC0+/1YF26L0l/tIVM1+A0UGYRhMd7nckqjIVIshqr39ckaCmGljXbaZpbTkns6svpEe8AX8FP5O79sytl0lj43PYGbIgQDijngYSI3JJDZ++ALBdfJ9WnbttyM2PuKoH+fAKI/8vGLjY8lhriG+m6VDz7SOaUhQXtHV5spVGi1xtSpEa3H8afCcfxpnN4k4o2ywvEsCXWF+qRpjrCdu38PlSvXheo/5ZIl83t4LhgRRi9dykqG6Vj2ma7uQJ2y6meHXAJMHxm8483yoUT1R0fasmWlAOtwnxBh7fMxBIIKvqxqshTXmRlwWqycDXu9hHjnCx7pBewa0BRwhLrGcVfn8ZtPl4wPXl6oLQ3W5tLYrRQ1PX4Gsu5momHF0nzNDn9bXsQ0m6+bhzPA5xZY/a06aDqQOnLAX3I3HJ0lSfL5yFlFrIDdqSwLgRmRnwqh1gxedqbXNxeKttOni+SkKh/tysam7DXcQmqeECgQyhaeLbPPJ8Z5+zGuUwnTBI6kj3tipNEESoH4DXD7mNScZ8AnjzqmZO+mpKvVQmHUrZZ4/qVf3C3CXfvBnkDLuFPebGXyQFSITb2+SV9qlAy8ulIhhxHeFsuEFyfH4QKi/Mrtk+mYwcMCtrt0ivxXzDMyp2xCYvhALaD4ruAJj3DR3JTUzG40a0Yv7T5+zDd/pvnjqSmhZxR3CKzLQ4ptb8yNW9pgSfe7"
}
},
"request": "0xcd5dbf15da49494dc8d166e14349393e3c3134b9a6b8bd19adb70d9e6dc7ee22"
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "xmeYRK2lSE+aqPAyyCFicgTLScVBiZeSEzZ3ZOYFBXLohQdVF1wV8r29p4oNRNcaVnU9i9pSUUQlx/92EsbpAg==",
"mldsa65": "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"
}
},
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"now": "2026-10-18T18:46:46.000Z",
"trustedIssuers": [
"aere-id:562f57b77cd7d679b0f532de4ce161d77b284342"
],
"statusLists": [
{
"statement": {
"v": 1,
"kind": "aere-status-list",
"id": "https://issuer.example/status/2",
"purpose": "revocation",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "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"
}
},
"size": 131072,
"encodedList": "H4sIAAAAAAACCu3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA",
"validFrom": "2026-10-18T17:46:16.000Z",
"validUntil": "2026-10-19T18:46:16.000Z",
"issuedAt": "2026-10-18T17:46:16.000Z"
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "VAWqnDiDcHbVGEJ3/JKzxf2iQ2WbisfZYiarENimk9AYr/p15+5K3FVO54zEvue3Mjd889+mkv1ooKFYfH2VCA==",
"mldsa65": "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"
}
}
]
},
"expected": {
"verdict": "PARTIAL",
"failed": [],
"levels": {
"integrity": "NOT_MEASURED",
"authenticity": "PASSED",
"anchoring": "NOT_MEASURED",
"finality": "NOT_MEASURED"
}
}
}

View File

@ -0,0 +1,181 @@
{
"v": 1,
"kind": "aere-conformance-vector",
"id": "identity-presentation/request.other-request",
"artifact": "identity-presentation",
"title": "the presentation was signed for a get and is handed over for a revoke of a grant (another request digest)",
"source": "derived from identity-presentation/request.valid",
"requires": [],
"exercises": [
"identity.audience",
"identity.binding-credential",
"identity.binding-delegations",
"identity.binding-disclosures",
"identity.chain-length",
"identity.claim-name",
"identity.credential-revoked",
"identity.credential-signature",
"identity.credential-window",
"identity.delegation-audience",
"identity.delegation-claims",
"identity.delegation-credential",
"identity.delegation-depth",
"identity.delegation-form",
"identity.delegation-from",
"identity.delegation-ids",
"identity.delegation-narrows",
"identity.delegation-parent",
"identity.delegation-revoked",
"identity.delegation-scope",
"identity.delegation-signature",
"identity.delegation-window",
"identity.disclosure-once",
"identity.disclosure-plain",
"identity.disclosure-readable",
"identity.disclosure-signed",
"identity.form",
"identity.freshness",
"identity.holder-id",
"identity.issuer-id",
"identity.issuer-trusted",
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"
],
"base": "identity-presentation/request.valid",
"mutation": {
"op": "set",
"path": [
"request"
],
"value": "0x154f8031f2b55e0115a71bdc8cf195104b9f28e34eade3202dfd8428fbc24f5c"
},
"input": {
"presentation": {
"v": 1,
"kind": "aere-presentation",
"credential": {
"statement": {
"v": 1,
"kind": "aere-credential",
"id": "urn:uuid:db69707d-6bb1-45fb-ad9c-c4cb75d37342",
"type": "ServiceAccess",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "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"
}
},
"holder": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "MIIHsjALBglghkgBZQMEAxIDggehAKPi1zCRbHAXzBZu4IBG3t5vXCYRQdrFxq8CyRVqy8Ye5arrPhFPizJgXlE26i2hNy/h/PVa1f/1P11xUoMXIrI4JE1Z1oTBMOZt7iCVPnd+ZhmxXMj+GqD7fMWfw31tTz7Cz/RaueXtriLClpPVHOOIKxSD324JTFBcykiC65KZmsubPAGEFaECkKzncjqeioHXTA7tYNPX83fYd8HuMxrFaE87tKsQOBWt6Z8zSBipJV15n34ItE40XEpMbfCmf5j1A1n7M3sJzrILNyvjxSmS63wsJSsrWDRtLFWTDS6vN8dmdZPY0WfDwrMv0NQCnBrq3gVcuLZp4xdW8Qp0/K2HGOimo6vZ4sdYFgobg+J+bfjUvhDp6DyhYJWJN+fSkKzv0BxfACnsPpBtSUUFmBp/WTVhmXw4sDQY/wmcPZksYSiRMQGw8ByG8NGK2ln0Q/tdTcC1pWm8RYq3cx8sP5MRsnYFEuQqwPwPgc9V5NdwaddRGZdYYTYun8+eQMdYpJ9kPkOsLz2pqx9eTgGZLRN6QKBYzZwDBcer2q8SYxRxluQC7ABSSx8Ba/Dq/vygnCfB+pVSGrMqFDlw1jvex/eSrdOUEG9aIgIGCwuPbQO6ChiePHfPnr4SKhlEQZcEm8dLM/vI/Z9PU42f61jAq5Ek4MSeFU2goeAhOxBWjmllo1Wk0vIlBDGFfkeyTAtBarSdvT5EpM6rVzGh+rCB0kg2M4tWac9dI0Cbo1xnp85udY7VA7y+k7UxXjwJQVUezi1XnKL/wf6q8wj0TCFUItrzrFOGvJPJrptnIPxjlRzrfIFuo5nBBg2qMiwvnVnQ7EQJ9Zooof/zdJ1v7Rn50SWESWX+TVOh4o9F9USM4JPJKhMTNAiBLI9e+dd6wZTlYPm7UaaCzMKgFpkM3N7qVhK4JTqGVMbUtyQK5jL+WDVytWj9bJwgLsBQZIQdIZWQiqOcSEmcBUByVBrRQA/JTdG7Y2oHdR1huxXDk7hNVwyN4GQj5ClBlUtJPdgGaB3odYGXoI08hORWaqfpcBD1Ie69xBQods3FwrM5ZEXwkNJpPhWx4BmwWd77u2wivg+yh5gaAjkqPmFuquhsadxJ38EsZos2vtQLeYwEDgCMIL5ZhoukxE++j6ozm1/3IrxQP1XfFsEJqhVyQYfOJty2g5A8Ukv+ium9iwxiEYdE09Y2UQWQ3KXrftYnd+ISaq0rfwse/waptcsdlaRYorOjIvjDRuy2mtW0cD5YKSG2aQ+vWYqD8tnNmQeWWhujdB76T294AyCipUUZhDvgCEAo5JK1uqdvESTlcEIsug2wsMNzd4hGPiTzmiUdzRDKYBP7dFmGhnf/XdNfRJ7VOlQg1NYwLDg3ZimmAlcAuPUkyS6Itn/GnaWpDZsURmy/Xw6duemM+zN+ZEpnz70DKplMir/AW+Sxf4/+MQDnw7Fx6PGn+lSFCGOhH3BjywwKagmjFvZ+AS8Hvvo/ioxnokOCgKf0wDHwYpOug7I4N+ntr2HAgw0MNZfN2ndRoI4mq/j9BjC92xacgPjA3PSH3Zg4Tgp7Cct7ogEjjww18EA6ZJ6F5pdzA9BuXeOz6u1P3u4TwhMXeYflnLMKwSJHPEu0D6Niz7imYeCx0hdW36NMn6pc4aA7VUiiiD7QQNe9My9wECwOPeSE2Mizlkvd7ROHqigRRhfctVsHB2pLILIjlz6qWEN7931kN5/13aROJBhUowczxdkSRsqB5/t6bPA9tV3xb5M1Oe5yjwz4YcF/9uU0UAm6GSyt3I89YdPVdLM+Fc2Vj/GLgmKmCSwh5oiNEKApLdBdkv7zvPGjvW9jLPPgc9Bqz4Frc+CreAG0epW5RB4HFIXwFYRIgrnnS3L9w3P2tuqkaJ24U/SauJC0+/1YF26L0l/tIVM1+A0UGYRhMd7nckqjIVIshqr39ckaCmGljXbaZpbTkns6svpEe8AX8FP5O79sytl0lj43PYGbIgQDijngYSI3JJDZ++ALBdfJ9WnbttyM2PuKoH+fAKI/8vGLjY8lhriG+m6VDz7SOaUhQXtHV5spVGi1xtSpEa3H8afCcfxpnN4k4o2ywvEsCXWF+qRpjrCdu38PlSvXheo/5ZIl83t4LhgRRi9dykqG6Vj2ma7uQJ2y6meHXAJMHxm8483yoUT1R0fasmWlAOtwnxBh7fMxBIIKvqxqshTXmRlwWqycDXu9hHjnCx7pBewa0BRwhLrGcVfn8ZtPl4wPXl6oLQ3W5tLYrRQ1PX4Gsu5momHF0nzNDn9bXsQ0m6+bhzPA5xZY/a06aDqQOnLAX3I3HJ0lSfL5yFlFrIDdqSwLgRmRnwqh1gxedqbXNxeKttOni+SkKh/tysam7DXcQmqeECgQyhaeLbPPJ8Z5+zGuUwnTBI6kj3tipNEESoH4DXD7mNScZ8AnjzqmZO+mpKvVQmHUrZZ4/qVf3C3CXfvBnkDLuFPebGXyQFSITb2+SV9qlAy8ulIhhxHeFsuEFyfH4QKi/Mrtk+mYwcMCtrt0ivxXzDMyp2xCYvhALaD4ruAJj3DR3JTUzG40a0Yv7T5+zDd/pvnjqSmhZxR3CKzLQ4ptb8yNW9pgSfe7"
}
},
"claims": {},
"sd": [
"YXErnXMmQQ-dBoKBooNn4JradJhgLNdTDafgUj84liA",
"z-g-gnjeUMBtn9Gs0ejG221J4xA4fVdzliaecIsrkaI"
],
"sdAlg": "sha-256",
"issuedAt": "2026-10-17T18:46:16.000Z",
"validFrom": "2026-10-17T18:46:16.000Z",
"validUntil": "2027-10-18T18:46:16.000Z",
"status": {
"list": "https://issuer.example/status/2",
"index": 1
}
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "Dgg0VtBp87EHd/WylhCG+7aqqAzfFlR7X6f0w8jV8Wzpx9VqyHkbs8tEoi12hPWSB7SePJYfJzLowAwpXZUzDA==",
"mldsa65": "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"
}
},
"disclosures": [
"WyJYTWUyODAxME53VkJiV1hacGZsRi13Iiwicm9sZSIsImRlcGxveWVyIl0",
"WyJqbUt5ODlYcExOV055NVN5aGhSUllBIiwidGVuYW50IiwiZXhhbXBsZSJd"
],
"delegations": [],
"binding": {
"v": 1,
"kind": "aere-presentation-binding",
"credentialHash": "0x8e2066db4ffb2972654bd2b25b5672927e75c74a403ae94052acb4588da563ed",
"disclosuresHash": "0xd6ba5f9f2b1b0509827a6c9382a75ecc7808061b7227e655c7cb38bc549d860e",
"delegations": [],
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"createdAt": "2026-10-18T18:46:16.000Z",
"presenter": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "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"
}
},
"request": "0xcd5dbf15da49494dc8d166e14349393e3c3134b9a6b8bd19adb70d9e6dc7ee22"
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "xmeYRK2lSE+aqPAyyCFicgTLScVBiZeSEzZ3ZOYFBXLohQdVF1wV8r29p4oNRNcaVnU9i9pSUUQlx/92EsbpAg==",
"mldsa65": "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"
}
},
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"now": "2026-10-18T18:46:46.000Z",
"trustedIssuers": [
"aere-id:562f57b77cd7d679b0f532de4ce161d77b284342"
],
"statusLists": [
{
"statement": {
"v": 1,
"kind": "aere-status-list",
"id": "https://issuer.example/status/2",
"purpose": "revocation",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "MIIHsjALBglghkgBZQMEAxIDggehAOthG2oBBbhW4WDQZRu8jJ6I43lSZSb851H/Hm3OMnN3nYhB8vDSFAJhdG9KyzlGLccWNMKg9EPhrvgpksBG5B+40uImjCNchV47U6/seeXNfj3ja7lmf9Ob9hrLIHbqoNH2F1fnLssWt4HHQhCe6IkBTElZ9ZjH2GCRi7ZJuojarEWW/VMwSx65lKQ6cqiM+ZekCU9zTYepIZ+vBJ1UTGmZ0MLs0UtTGjmdMoIFYeW2p6OyfCcpC34csdIJQKnmjNoSeG4KvKQfw6M/f8+idYb0mAdfi1apNMTV8DBaefxuP4pyJ7lGIPJnrrHM0UyH3q2TVFHcZXfXvgdGFZwGOpMLXlK60zQVAf7JnWdK3uZaWDf1LcQusI2tTzomO4hmApSatQgCGwg9TxyXTX1B28A+Fh4WIFmt0OjqiHdlYp/uHyp9PDM1994CHmn7JzxUcjY0vv1afNXzECeEVNLXmVoHem3j+oRIOUMM2SA2vSLLn2z5t4l2pEl+LcVMT67pGrogxCU9yTpTM8SRA8OX2wZKlBtrc1dSY6ddMjFew2MhScUZtwkp/4V09Eb7m0Nxt20qQWAIHa8QDtWQ/ZL8tfxx4DmjGqeG3YadaExhWPdw9fIjm+Nv5LhliQ6oB34l4fEp+YJ5ZLcK6baozr+QujUQlMHruueLJL9Zi/5CoxmBR2boBo8w+V/RyQbnZGW/YWa4IuHpRZaj/w4mJZKUZgV+V6cJOqsLwxLukIits5ZmqbVRUfbrRVKezxWPwC2y3w4EifEZZHZ+Wj2zTiuy+oVERo10n48oEArp0ZRBy70xURU9MGYEwSnIJ3zltB+yh2BNFGoRErZS6X7YWCWAduD1/dosf3adS6Ijp5TZiK/xiLbBTcJLClI0hUjyAs5F4n3vri3eZcDzT/E12NX/FGEgEB7K2ObbLx4TBfME7aLNSmCVRkKtDG9Rjsr1Bm3NnPIVJNevlGstRZO3aTR8F1GGxMl7iHjvjKhQPjwB23T8ac0Kdaq17BGL6nNvkLXvzGDL8ovYKA6EYitLCbwSG+cNNsa9sQ7u9l5/oraCiWf9BX+To43bym0Is/SHLWBnWFalN9UODQoFEYCAbz62MKKUz/4rLBre7wSeNFzKE1IrX3XQu3Ep1jDCq6R9wJDt2z01bn4l1LjeeKJY+aYs7/qns1JNQR48IVNQeyOkYZF0Njy+MY12HRF4qxzvMZhBFrscuDY1htEZfGkAWdX/MPud4klYT/RB7b6vsx6KKGDBd3H2yHu5JanEiRU0t0ZQ6kwfX1kwM3Jx43p/UfukHjbUFlkZ24oZGUQ9/92veDJNA5bYR7DIq7TSJ3Qtu2NK5sDpLKyDmmze7KV1PSKeFm2Lv3HctFS6UglTijHn5m28yqM0T0yR2tqcxpEAfJpW6Qx8ulGL23yCaDyEuX4Cl3e3KaxJ0nNgMfGc+BvZE8c2+O+9bDQinUrTP3C+nsOXx09OMY48hvjTyM4a2ezIZ6gup0sGlOPwdERT0bJwRfOBeeptGIDOo94uXTp7HQ3/+xcNuAVf2nOUI3M6tpNxqtfNnT3lm91I124Nt8iNA6kdRpujOxvS0Tyt+0WAGhHNokxfCI+3ZQTiunBc+byZ0hw7AVtoa3+jj0fN33k7zEyHtbnZzfgYdW88gsvuLhYJZ1ODo/DtSr4u4jogYesbH0h3TGkNTPKXgiJFs1qr5UzCVLaINm3yIDIbu0dB99GKMCWsTR/z2PmXRmRGRDEOTiaHeP+nRcccOdso19prb69mROxO7OF9FHE8o1FGSywRYadnyZhxUV9HfPM2uzbPsUx/NxSYhF7JZ7F7X6rKpc7+LeTR/hR2Co7dWHFG8pgpi/4Avf7+hZ+I7HPM3Ws1hTFYt6X/Ln9uzGjD1QHtBoeYDmTooWvQFNBaHXA81x8vEa3Ph5Lu6JZnvAG5qFmyUWglLYRFmDtANRZ0iAM7gjFwSJdXlBaw1guumCtYgxJ0Q8e1sfluNuDEvIcHVtHdsfxnvI7hXOPyck6AYGMwBCn0lULEX3z8Mhk+6K9PH0htAFmr3EGPHDIZB3+7aX6hGCrzaiOZOam46z7rM/so+wmU1TmQg/7mW8qk5UZgDlKnrNWjTFM69Fuioaa4J4tpUxhCx4JnQky2KHvZEc+4ekLKeLgywtDoDieuiT6zxgX6fwkkZSSVjOEMLQPb9EwZ3JgULQ8NL6xiescDsyVMhJ/VxFBr8xvZJDemrVi5WE02NcHmUlH18Ol/aEHGm8O0x0CNWCyoRJfEe2NTGP1KJfOazIgxKGZmh+3KSvTGEmSr5mguXzDTlDpGrP2ssDmZdNhNFEJ7/HqmoduAwONQ/UY4t0UAitcXTW77DnqjQQbWx4zLXPr7jGQKASgjYleafg/zdOv/115zQ02HXKvHwoHyBQIpyZYS7BqzyRr0yVW+6Uf2UQbehWOxXVNuHB/5PEPvT0FkshR0hDQmtQOi0jAGgbsWMTFKj0E0b7X1ntruzjLum5Btq1q92ZGtQKyqTEvpAmfZA4HmqE52CN4Ny7jvsHZdUQNL1atpEkQ0743JCbqH/oXUui+8AXfaaDhbT7yxUbxvSAu8cExMdQTO4JuF"
}
},
"size": 131072,
"encodedList": "H4sIAAAAAAACCu3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA",
"validFrom": "2026-10-18T17:46:16.000Z",
"validUntil": "2026-10-19T18:46:16.000Z",
"issuedAt": "2026-10-18T17:46:16.000Z"
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "VAWqnDiDcHbVGEJ3/JKzxf2iQ2WbisfZYiarENimk9AYr/p15+5K3FVO54zEvue3Mjd889+mkv1ooKFYfH2VCA==",
"mldsa65": "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"
}
}
],
"request": "0x154f8031f2b55e0115a71bdc8cf195104b9f28e34eade3202dfd8428fbc24f5c"
},
"expected": {
"verdict": "INVALID",
"failed": [
"identity.request"
],
"levels": {
"integrity": "FAILED",
"authenticity": "PASSED",
"anchoring": "NOT_MEASURED",
"finality": "NOT_MEASURED"
}
}
}

View File

@ -0,0 +1,181 @@
{
"v": 1,
"kind": "aere-conformance-vector",
"id": "identity-presentation/request.presentation-without-request",
"artifact": "identity-presentation",
"title": "the same holder presented without a request (the form made before the field existed), to a verifier that requires one",
"source": "derived from identity-presentation/request.valid",
"requires": [],
"exercises": [
"identity.audience",
"identity.binding-credential",
"identity.binding-delegations",
"identity.binding-disclosures",
"identity.chain-length",
"identity.claim-name",
"identity.credential-revoked",
"identity.credential-signature",
"identity.credential-window",
"identity.delegation-audience",
"identity.delegation-claims",
"identity.delegation-credential",
"identity.delegation-depth",
"identity.delegation-form",
"identity.delegation-from",
"identity.delegation-ids",
"identity.delegation-narrows",
"identity.delegation-parent",
"identity.delegation-revoked",
"identity.delegation-scope",
"identity.delegation-signature",
"identity.delegation-window",
"identity.disclosure-once",
"identity.disclosure-plain",
"identity.disclosure-readable",
"identity.disclosure-signed",
"identity.form",
"identity.freshness",
"identity.holder-id",
"identity.issuer-id",
"identity.issuer-trusted",
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"
],
"base": "identity-presentation/request.valid",
"mutation": {
"producer": "the holder signed a binding that carries no request",
"changed": [
[
"presentation"
]
]
},
"input": {
"presentation": {
"v": 1,
"kind": "aere-presentation",
"credential": {
"statement": {
"v": 1,
"kind": "aere-credential",
"id": "urn:uuid:db69707d-6bb1-45fb-ad9c-c4cb75d37342",
"type": "ServiceAccess",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "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"
}
},
"holder": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "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"
}
},
"claims": {},
"sd": [
"YXErnXMmQQ-dBoKBooNn4JradJhgLNdTDafgUj84liA",
"z-g-gnjeUMBtn9Gs0ejG221J4xA4fVdzliaecIsrkaI"
],
"sdAlg": "sha-256",
"issuedAt": "2026-10-17T18:46:16.000Z",
"validFrom": "2026-10-17T18:46:16.000Z",
"validUntil": "2027-10-18T18:46:16.000Z",
"status": {
"list": "https://issuer.example/status/2",
"index": 1
}
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "Dgg0VtBp87EHd/WylhCG+7aqqAzfFlR7X6f0w8jV8Wzpx9VqyHkbs8tEoi12hPWSB7SePJYfJzLowAwpXZUzDA==",
"mldsa65": "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"
}
},
"disclosures": [
"WyJYTWUyODAxME53VkJiV1hacGZsRi13Iiwicm9sZSIsImRlcGxveWVyIl0",
"WyJqbUt5ODlYcExOV055NVN5aGhSUllBIiwidGVuYW50IiwiZXhhbXBsZSJd"
],
"delegations": [],
"binding": {
"v": 1,
"kind": "aere-presentation-binding",
"credentialHash": "0x8e2066db4ffb2972654bd2b25b5672927e75c74a403ae94052acb4588da563ed",
"disclosuresHash": "0xd6ba5f9f2b1b0509827a6c9382a75ecc7808061b7227e655c7cb38bc549d860e",
"delegations": [],
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"createdAt": "2026-10-18T18:46:16.000Z",
"presenter": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "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"
}
}
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "cp0EmFGGE2FeTW3LpUtk6mJyx0nVAuceyqA2XycapQjO0rVbGutkEllLPaM1+58+OC7VH/ruOM6i6TpAqKIuCw==",
"mldsa65": "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"
}
},
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"now": "2026-10-18T18:46:46.000Z",
"trustedIssuers": [
"aere-id:562f57b77cd7d679b0f532de4ce161d77b284342"
],
"statusLists": [
{
"statement": {
"v": 1,
"kind": "aere-status-list",
"id": "https://issuer.example/status/2",
"purpose": "revocation",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "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"
}
},
"size": 131072,
"encodedList": "H4sIAAAAAAACCu3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA",
"validFrom": "2026-10-18T17:46:16.000Z",
"validUntil": "2026-10-19T18:46:16.000Z",
"issuedAt": "2026-10-18T17:46:16.000Z"
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "VAWqnDiDcHbVGEJ3/JKzxf2iQ2WbisfZYiarENimk9AYr/p15+5K3FVO54zEvue3Mjd889+mkv1ooKFYfH2VCA==",
"mldsa65": "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"
}
}
],
"request": "0xcd5dbf15da49494dc8d166e14349393e3c3134b9a6b8bd19adb70d9e6dc7ee22"
},
"expected": {
"verdict": "INVALID",
"failed": [
"identity.request"
],
"levels": {
"integrity": "FAILED",
"authenticity": "PASSED",
"anchoring": "NOT_MEASURED",
"finality": "NOT_MEASURED"
}
}
}

View File

@ -0,0 +1,173 @@
{
"v": 1,
"kind": "aere-conformance-vector",
"id": "identity-presentation/request.valid",
"artifact": "identity-presentation",
"title": "a holder presents for one request (the digest of a get on a secret); the verifier gives the digest of the request it received, and the signed binding carries it",
"source": "producers (identity: issueCredential, createStatusList, present with a request), two hybrid key pairs made and discarded at generation",
"requires": [],
"exercises": [
"identity.audience",
"identity.binding-credential",
"identity.binding-delegations",
"identity.binding-disclosures",
"identity.chain-length",
"identity.claim-name",
"identity.credential-revoked",
"identity.credential-signature",
"identity.credential-window",
"identity.delegation-audience",
"identity.delegation-claims",
"identity.delegation-credential",
"identity.delegation-depth",
"identity.delegation-form",
"identity.delegation-from",
"identity.delegation-ids",
"identity.delegation-narrows",
"identity.delegation-parent",
"identity.delegation-revoked",
"identity.delegation-scope",
"identity.delegation-signature",
"identity.delegation-window",
"identity.disclosure-once",
"identity.disclosure-plain",
"identity.disclosure-readable",
"identity.disclosure-signed",
"identity.form",
"identity.freshness",
"identity.holder-id",
"identity.issuer-id",
"identity.issuer-trusted",
"identity.nonce",
"identity.presentation-signature",
"identity.presenter",
"identity.request",
"identity.sd-distinct",
"identity.status-list",
"identity.unreadable"
],
"base": null,
"mutation": null,
"input": {
"presentation": {
"v": 1,
"kind": "aere-presentation",
"credential": {
"statement": {
"v": 1,
"kind": "aere-credential",
"id": "urn:uuid:db69707d-6bb1-45fb-ad9c-c4cb75d37342",
"type": "ServiceAccess",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "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"
}
},
"holder": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "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"
}
},
"claims": {},
"sd": [
"YXErnXMmQQ-dBoKBooNn4JradJhgLNdTDafgUj84liA",
"z-g-gnjeUMBtn9Gs0ejG221J4xA4fVdzliaecIsrkaI"
],
"sdAlg": "sha-256",
"issuedAt": "2026-10-17T18:46:16.000Z",
"validFrom": "2026-10-17T18:46:16.000Z",
"validUntil": "2027-10-18T18:46:16.000Z",
"status": {
"list": "https://issuer.example/status/2",
"index": 1
}
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "Dgg0VtBp87EHd/WylhCG+7aqqAzfFlR7X6f0w8jV8Wzpx9VqyHkbs8tEoi12hPWSB7SePJYfJzLowAwpXZUzDA==",
"mldsa65": "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"
}
},
"disclosures": [
"WyJYTWUyODAxME53VkJiV1hacGZsRi13Iiwicm9sZSIsImRlcGxveWVyIl0",
"WyJqbUt5ODlYcExOV055NVN5aGhSUllBIiwidGVuYW50IiwiZXhhbXBsZSJd"
],
"delegations": [],
"binding": {
"v": 1,
"kind": "aere-presentation-binding",
"credentialHash": "0x8e2066db4ffb2972654bd2b25b5672927e75c74a403ae94052acb4588da563ed",
"disclosuresHash": "0xd6ba5f9f2b1b0509827a6c9382a75ecc7808061b7227e655c7cb38bc549d860e",
"delegations": [],
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"createdAt": "2026-10-18T18:46:16.000Z",
"presenter": {
"id": "aere-id:8b0ca41af3e8aad581cd06cd11fe7e5cf02976f4",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEA82zAAUVxYCGBZVOJX8FeFRnD+eF0ItSOa9fHFSWx5zc=",
"mldsa65": "MIIHsjALBglghkgBZQMEAxIDggehAKPi1zCRbHAXzBZu4IBG3t5vXCYRQdrFxq8CyRVqy8Ye5arrPhFPizJgXlE26i2hNy/h/PVa1f/1P11xUoMXIrI4JE1Z1oTBMOZt7iCVPnd+ZhmxXMj+GqD7fMWfw31tTz7Cz/RaueXtriLClpPVHOOIKxSD324JTFBcykiC65KZmsubPAGEFaECkKzncjqeioHXTA7tYNPX83fYd8HuMxrFaE87tKsQOBWt6Z8zSBipJV15n34ItE40XEpMbfCmf5j1A1n7M3sJzrILNyvjxSmS63wsJSsrWDRtLFWTDS6vN8dmdZPY0WfDwrMv0NQCnBrq3gVcuLZp4xdW8Qp0/K2HGOimo6vZ4sdYFgobg+J+bfjUvhDp6DyhYJWJN+fSkKzv0BxfACnsPpBtSUUFmBp/WTVhmXw4sDQY/wmcPZksYSiRMQGw8ByG8NGK2ln0Q/tdTcC1pWm8RYq3cx8sP5MRsnYFEuQqwPwPgc9V5NdwaddRGZdYYTYun8+eQMdYpJ9kPkOsLz2pqx9eTgGZLRN6QKBYzZwDBcer2q8SYxRxluQC7ABSSx8Ba/Dq/vygnCfB+pVSGrMqFDlw1jvex/eSrdOUEG9aIgIGCwuPbQO6ChiePHfPnr4SKhlEQZcEm8dLM/vI/Z9PU42f61jAq5Ek4MSeFU2goeAhOxBWjmllo1Wk0vIlBDGFfkeyTAtBarSdvT5EpM6rVzGh+rCB0kg2M4tWac9dI0Cbo1xnp85udY7VA7y+k7UxXjwJQVUezi1XnKL/wf6q8wj0TCFUItrzrFOGvJPJrptnIPxjlRzrfIFuo5nBBg2qMiwvnVnQ7EQJ9Zooof/zdJ1v7Rn50SWESWX+TVOh4o9F9USM4JPJKhMTNAiBLI9e+dd6wZTlYPm7UaaCzMKgFpkM3N7qVhK4JTqGVMbUtyQK5jL+WDVytWj9bJwgLsBQZIQdIZWQiqOcSEmcBUByVBrRQA/JTdG7Y2oHdR1huxXDk7hNVwyN4GQj5ClBlUtJPdgGaB3odYGXoI08hORWaqfpcBD1Ie69xBQods3FwrM5ZEXwkNJpPhWx4BmwWd77u2wivg+yh5gaAjkqPmFuquhsadxJ38EsZos2vtQLeYwEDgCMIL5ZhoukxE++j6ozm1/3IrxQP1XfFsEJqhVyQYfOJty2g5A8Ukv+ium9iwxiEYdE09Y2UQWQ3KXrftYnd+ISaq0rfwse/waptcsdlaRYorOjIvjDRuy2mtW0cD5YKSG2aQ+vWYqD8tnNmQeWWhujdB76T294AyCipUUZhDvgCEAo5JK1uqdvESTlcEIsug2wsMNzd4hGPiTzmiUdzRDKYBP7dFmGhnf/XdNfRJ7VOlQg1NYwLDg3ZimmAlcAuPUkyS6Itn/GnaWpDZsURmy/Xw6duemM+zN+ZEpnz70DKplMir/AW+Sxf4/+MQDnw7Fx6PGn+lSFCGOhH3BjywwKagmjFvZ+AS8Hvvo/ioxnokOCgKf0wDHwYpOug7I4N+ntr2HAgw0MNZfN2ndRoI4mq/j9BjC92xacgPjA3PSH3Zg4Tgp7Cct7ogEjjww18EA6ZJ6F5pdzA9BuXeOz6u1P3u4TwhMXeYflnLMKwSJHPEu0D6Niz7imYeCx0hdW36NMn6pc4aA7VUiiiD7QQNe9My9wECwOPeSE2Mizlkvd7ROHqigRRhfctVsHB2pLILIjlz6qWEN7931kN5/13aROJBhUowczxdkSRsqB5/t6bPA9tV3xb5M1Oe5yjwz4YcF/9uU0UAm6GSyt3I89YdPVdLM+Fc2Vj/GLgmKmCSwh5oiNEKApLdBdkv7zvPGjvW9jLPPgc9Bqz4Frc+CreAG0epW5RB4HFIXwFYRIgrnnS3L9w3P2tuqkaJ24U/SauJC0+/1YF26L0l/tIVM1+A0UGYRhMd7nckqjIVIshqr39ckaCmGljXbaZpbTkns6svpEe8AX8FP5O79sytl0lj43PYGbIgQDijngYSI3JJDZ++ALBdfJ9WnbttyM2PuKoH+fAKI/8vGLjY8lhriG+m6VDz7SOaUhQXtHV5spVGi1xtSpEa3H8afCcfxpnN4k4o2ywvEsCXWF+qRpjrCdu38PlSvXheo/5ZIl83t4LhgRRi9dykqG6Vj2ma7uQJ2y6meHXAJMHxm8483yoUT1R0fasmWlAOtwnxBh7fMxBIIKvqxqshTXmRlwWqycDXu9hHjnCx7pBewa0BRwhLrGcVfn8ZtPl4wPXl6oLQ3W5tLYrRQ1PX4Gsu5momHF0nzNDn9bXsQ0m6+bhzPA5xZY/a06aDqQOnLAX3I3HJ0lSfL5yFlFrIDdqSwLgRmRnwqh1gxedqbXNxeKttOni+SkKh/tysam7DXcQmqeECgQyhaeLbPPJ8Z5+zGuUwnTBI6kj3tipNEESoH4DXD7mNScZ8AnjzqmZO+mpKvVQmHUrZZ4/qVf3C3CXfvBnkDLuFPebGXyQFSITb2+SV9qlAy8ulIhhxHeFsuEFyfH4QKi/Mrtk+mYwcMCtrt0ivxXzDMyp2xCYvhALaD4ruAJj3DR3JTUzG40a0Yv7T5+zDd/pvnjqSmhZxR3CKzLQ4ptb8yNW9pgSfe7"
}
},
"request": "0xcd5dbf15da49494dc8d166e14349393e3c3134b9a6b8bd19adb70d9e6dc7ee22"
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "xmeYRK2lSE+aqPAyyCFicgTLScVBiZeSEzZ3ZOYFBXLohQdVF1wV8r29p4oNRNcaVnU9i9pSUUQlx/92EsbpAg==",
"mldsa65": "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"
}
},
"audience": "https://secrets.example",
"nonce": "n-acb484c787a3aefa",
"now": "2026-10-18T18:46:46.000Z",
"trustedIssuers": [
"aere-id:562f57b77cd7d679b0f532de4ce161d77b284342"
],
"statusLists": [
{
"statement": {
"v": 1,
"kind": "aere-status-list",
"id": "https://issuer.example/status/2",
"purpose": "revocation",
"issuer": {
"id": "aere-id:562f57b77cd7d679b0f532de4ce161d77b284342",
"keys": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "MCowBQYDK2VwAyEAwwJoxzRKtH4YSKNbh2KjV0V7r8qiz4b2GBaaO7ZBnVQ=",
"mldsa65": "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"
}
},
"size": 131072,
"encodedList": "H4sIAAAAAAACCu3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA",
"validFrom": "2026-10-18T17:46:16.000Z",
"validUntil": "2026-10-19T18:46:16.000Z",
"issuedAt": "2026-10-18T17:46:16.000Z"
},
"signature": {
"alg": "ed25519+ml-dsa-65",
"ed25519": "VAWqnDiDcHbVGEJ3/JKzxf2iQ2WbisfZYiarENimk9AYr/p15+5K3FVO54zEvue3Mjd889+mkv1ooKFYfH2VCA==",
"mldsa65": "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"
}
}
],
"request": "0xcd5dbf15da49494dc8d166e14349393e3c3134b9a6b8bd19adb70d9e6dc7ee22"
},
"expected": {
"verdict": "VALID",
"failed": [],
"levels": {
"integrity": "PASSED",
"authenticity": "PASSED",
"anchoring": "NOT_MEASURED",
"finality": "NOT_MEASURED"
}
}
}

View File

@ -49,6 +49,19 @@ delegation chain, the verifier's audience and nonce, and the time. Without the v
for someone else would be accepted, so `verify` reports those checks as not judged instead of passing them. A nonce is the
verifier's to make fresh and to accept once: a verifier that reuses one accepts a replay within the presentation's age limit.
**A presentation can also be bound to one request** (since 2026-10-18): `present({ ..., request })` puts `request`, the digest of
what the holder asks the verifier to do (`0x` + 64 lowercase hex, computed by the verifier's own rule from the request), into the
binding before it is signed, and `verifyPresentation(p, { ..., request })` then requires exactly that digest (rule `request`: another
digest, or none at all, is a refusal). Without it the signature covers who the presentation is for, but not what it is used for: a
presentation made for "read secret db" was handed over as "revoke a grant on db" (the adversarial review of the secrets service).
A verifier that gives no request leaves a binding that carries one **not judged** (cause `request`), like the audience and the nonce;
a binding without one, judged by a verifier that gives none, has no request row at all, so every presentation made before the field
existed verifies exactly as before. The model is `transaction_data` in OpenID for Verifiable Presentations 1.0 (Final, 9 July 2025):
section 5.1 (the request parameter), section 8.4 (the data signed with the same key as the proof of possession) and appendix B.3.3
(the `transaction_data_hashes` of the SD-JWT VC Key Binding JWT, SHA-256 by default). `checkCompliance` passes `request` through, and
takes a `cache` (a `Map` made by the caller for one presentation) so that a service judging the same presentation against many
policies verifies its signatures once per distinct pair of trusted issuers and maximum age, with the same result as without it.
Inputs the verifier fetched and cannot read (a malformed status list or revocation) are ignored and reported, never counted against
the credential; a malformed trusted issuer key is the verifier's own error and stops the verification.
@ -287,15 +300,15 @@ the table says, row by row, what is measured here.
Since 2026-10-05 every refusal of `verifyPresentation` names its rule. A row is `{ name, pass, detail, rule }`; a row that was not
judged is `{ name, pass: null, detail, unjudged }` with its cause. The result carries `rules`, the sorted ids of the rules that
refused, and `unjudged`, the sorted causes of what was not judged. The module exports the lists: `REGULI` (37 rules), `NEJUDECATE`
(7 causes) and `OPRIRI` (the four rules after whose refusal other checks were not made: `form`, `chain-length`, `delegation-form`,
refused, and `unjudged`, the sorted causes of what was not judged. The module exports the lists: `REGULI` (38 rules since 2026-10-18, with `request`), `NEJUDECATE`
(8 causes) and `OPRIRI` (the four rules after whose refusal other checks were not made: `form`, `chain-length`, `delegation-form`,
`unreadable`). A time field that cannot be read (a credential's validity, a delegation's or a presentation's bounds) and a malformed
earlier link are reported as `unreadable`, which is one of the stopping rules: the checks after it are not made. The verifier does
not stop at its first refusal: it makes every check it can reach. `valid` is unchanged: true when
no row failed, also when rows were not judged; a caller that needs "nothing left unjudged" reads `unjudged`.
With these ids the presentation verifier is enrolled in the conformance suite beside this folder (`conformance/`: 37 rules,
65 vectors of the artifact kind `identity-presentation`, each rule taken out of a copy of the verifier in turn). Since 2026-10-18 the
With these ids the presentation verifier is enrolled in the conformance suite beside this folder (`conformance/`: 38 rules,
69 vectors of the artifact kind `identity-presentation`, four of them for `request` since 2026-10-18, each rule taken out of a copy of the verifier in turn). Since 2026-10-18 the
SD-JWT verifier and the SD-JWT VC profile are enrolled there too: `verifySdJwt` names the rule of its refusal (`rule`, one of the 23 of
`REGULI` in `sdjwt.mjs`: `form`, `alg`, `typ`, `crit`, `key`, `signature`, `issuer`, `sd-alg`, `disclosure-form`, `disclosure-twice`,
`digest-twice`, `disclosure-name`, `claim-conflict`, `unreferenced`, `time`, `kb-context`, `kb-required`, `kb-cnf`, `kb-claims`,
@ -343,10 +356,10 @@ line is reported as not judged. The private key files are written with mode 0600
## Tests
```
node proba-identity.mjs # 46: the paths above, each attack of the adversarial review as its own test, the named rules
node control-negativ-identity.mjs # on a copy, each of 53 guards removed -> its own named test turns red
node proba-conformitate.mjs # 15: compliance policies judged on real presentations, the record without personal data, the command line
node control-negativ-conformitate.mjs # on a copy, each of 14 guards removed -> its own named test turns red
node proba-identity.mjs # 52: the paths above, each attack of the adversarial review as its own test, the named rules
node control-negativ-identity.mjs # on a copy, each of 57 guards removed -> its own named test turns red
node proba-conformitate.mjs # 18: compliance policies judged on real presentations, the record without personal data, the command line
node control-negativ-conformitate.mjs # on a copy, each of 17 guards removed -> its own named test turns red
node proba-travel-rule.mjs # 19: two VASPs with registry credentials, the whole exchange, and each attack of the review
node control-negativ-travel-rule.mjs # on a copy, each of 19 guards removed -> its own named test turns red
node proba-sdjwt.mjs # 25: the RFC 9901 vectors, SD-JWTs issued with AERE keys (EdDSA and ML-DSA-65), each attack, the fixtures of @sd-jwt/core

View File

@ -64,16 +64,40 @@ function regula(r, claims) {
return false;
}
// 2026-10-18 (B8, revizuirea adversariala a lui pq-secrets): un serviciu care judeca ACEEASI prezentare dupa mai multe politici (un
// grant fiecare) refacea verificarea hibrida intreaga pentru fiecare: 200 de grant-uri, 0,5 s de CPU pentru un apelant fara niciun drept.
// Cu `cache` (un Map facut de apelant pentru O prezentare), verificarea se face o data pentru fiecare pereche distincta (emitentii
// politicii, varsta maxima), singurele lucruri prin care difera de la o politica la alta; contextul (prezentarea, publicul, nonce-ul,
// ceasul, listele de stare, revocarile, cererea) e legat de cache la prima folosire si o folosire cu alt context e o greseala a
// apelantului (arunca). Iesirea lui checkCompliance e aceeasi, cu cache sau fara.
const CONTEXT = Symbol('context');
function verifica(p, o, cache) {
if (cache == null) return verifyPresentation(p, o);
if (!(cache instanceof Map)) throw new Error('compliance: cache is a Map made by the caller for one presentation');
const t = new Date(o.now).getTime(); const ctx = cache.get(CONTEXT);
// listele se compara dupa identitate (aceeasi lista data de apelant), doua liste goale sunt aceeasi (implicitul `[]` e alt obiect la
// fiecare apel)
const aceeasi = (x, y) => x === y || (Array.isArray(x) && Array.isArray(y) && x.length === 0 && y.length === 0);
if (ctx === undefined) cache.set(CONTEXT, { p, audience: o.audience, nonce: o.nonce, t, statusLists: o.statusLists, revocations: o.revocations, request: o.request });
else if (!(ctx.p === p && ctx.audience === o.audience && ctx.nonce === o.nonce && ctx.t === t && aceeasi(ctx.statusLists, o.statusLists) && aceeasi(ctx.revocations, o.revocations) && ctx.request === o.request)) {
throw new Error('compliance: a cache serves one presentation judged in one context (the same presentation, audience, nonce, clock, status lists, revocations and request)');
}
const k = canonical([o.trustedIssuers, o.maxAgeS]);
if (!cache.has(k)) cache.set(k, verifyPresentation(p, o));
return cache.get(k);
}
/**
* Judeca o prezentare dupa o politica. Cere: prezentarea VALIDA, legata de publicul si nonce-ul verificatorului, emitentul intre cei
* ai politicii, starea credentialului JUDECATA (nu doar "nerevocat pentru ca nu stiu") cand politica o cere, si fiecare regula.
* Intoarce { compliant, reasons, policyHash, presentation } fara afirmatiile detinatorului (le are verificatorul in `presentation.claims`
* numai daca le cere explicit, cu `keepClaims`).
* numai daca le cere explicit, cu `keepClaims`). `request` (optional): digestul cererii primite, dat verificatorului de identitate
* (regula lui `request`); `cache` (optional): vezi mai sus.
*/
export function checkCompliance(p, politica, { audience, nonce, now = new Date(), statusLists = [], revocations = [], keepClaims = false } = {}) {
export function checkCompliance(p, politica, { audience, nonce, now = new Date(), statusLists = [], revocations = [], keepClaims = false, request = null, cache = null } = {}) {
const { policy, policyHash } = definePolicy(politica);
if (typeof audience !== 'string' || !audience || typeof nonce !== 'string' || !nonce) throw new Error('compliance: the verifier\'s audience and nonce are required (without them a presentation can be replayed)');
const v = verifyPresentation(p, { audience, nonce, now, trustedIssuers: policy.trustedIssuers, statusLists, revocations, maxAgeS: policy.maxAgeS });
const v = verifica(p, { audience, nonce, now, trustedIssuers: policy.trustedIssuers, statusLists, revocations, maxAgeS: policy.maxAgeS, request }, cache);
const rows = []; const ok = (rule, pass, detail) => { if (!REGULI.includes(rule)) throw new Error('compliance: undeclared rule ' + rule); rows.push({ rule, pass: !!pass, detail: pass ? '' : detail }); };
ok('presentation', v.valid /*PAZA:presentation*/, 'the presentation is not valid: ' + v.rows.filter((r) => r.pass === false).map((r) => r.name + (r.detail ? ' (' + r.detail + ')' : '')).join('; '));
const status = v.rows.find((r) => /^credential: (not revoked|status)/.test(r.name));
@ -87,8 +111,8 @@ export function checkCompliance(p, politica, { audience, nonce, now = new Date()
// oricine, schimba obiectul fara sa schimbe nimic semnat, deci digestul vechi nu numea unic prezentarea. Semnatura nu intra: ML-DSA
// semneaza aleator, deci aceeasi legatura poate avea oricate semnaturi valide. Fara legatura (prezentare stricata), obiectul intreg.
const legatura = p && typeof p === 'object' && p.binding && typeof p.binding === 'object' && !Array.isArray(p.binding) ? p.binding : null;
return { compliant: !motive.length, reasons: motive, rules: [...new Set(rows.filter((r) => !r.pass).map((r) => r.rule))].sort(), rows, unjudged: v.unjudged, policyHash, policyId: policy.id, holder, notJudged: v.notJudged,
presentationHash: sha(canonical(legatura || p || null)), ...(keepClaims && !motive.length ? { claims } : {}) };
return { compliant: !motive.length, reasons: motive, rules: [...new Set(rows.filter((r) => !r.pass).map((r) => r.rule))].sort(), rows, unjudged: [...v.unjudged], policyHash, policyId: policy.id, holder, notJudged: v.notJudged,
presentationHash: sha(canonical(legatura || p || null)), ...(keepClaims && !motive.length ? { claims: { ...claims } } : {}) };
}
/**

View File

@ -25,7 +25,11 @@ const PLANTARI = [
['regulile nu mai sunt in forma normala', '}).sort((a, b) => (canonical(a) < canonical(b) ? -1 : 1));', '});', 'politica: forma normala'],
['campuri necunoscute primite in politica', 'if (necunoscute.length) throw', 'if (false) throw', 'CONTROL: politica refuza'],
['id-ul detinatorului scris in inregistrare', 'subject: pseudonim,', 'subject: rez.holder,', 'inregistrarea (plic AIP-23 compliance) NU poarta date personale'],
['afirmatiile intoarse si pentru o judecata neconforma', '...(keepClaims && !motive.length ? { claims } : {})', '...(keepClaims ? { claims } : {})', 'rezultatul nu poarta afirmatiile'],
['afirmatiile intoarse si pentru o judecata neconforma', '...(keepClaims && !motive.length ? { claims: { ...claims } } : {})', '...(keepClaims ? { claims: { ...claims } } : {})', 'rezultatul nu poarta afirmatiile'],
// 2026-10-18 (B8): cererea si cache-ul verificarii
['cererea nu mai ajunge la verificatorul de identitate', 'maxAgeS: policy.maxAgeS, request }, cache);', 'maxAgeS: policy.maxAgeS }, cache);', 'cererea: checkCompliance da cererea'],
['cache-ul nu mai cere acelasi context (verdictul altei prezentari imprumutat)', "else if (!(ctx.p === p && ctx.audience === o.audience", "else if (false && !(ctx.p === p && ctx.audience === o.audience", 'CONTROL: un cache folosit pentru alta prezentare'],
['cache-ul nu mai deosebeste emitentii si varsta politicii', 'const k = canonical([o.trustedIssuers, o.maxAgeS]);', "const k = 'una';", 'cache: aceeasi prezentare dupa patru politici'],
['cheia verificatorului ignorata la pseudonim', '(pseudonymKey ? crypto.createHmac(', '(false ? crypto.createHmac(', 'pseudonimul'],
];
const FISIERE = ['identity.mjs', 'identity-cli.mjs', C, 'proba-conformitate.mjs'];

View File

@ -26,6 +26,11 @@ const PLANTARI = [
['alta codare base64url a aceleiasi dezvaluiri primita', L, "if (b.toString('base64url') !== enc) throw", 'if (false) throw', 'ATAC: o dezvaluire in alta codare base64url'],
['publicul verificatorului nu se mai compara', L, 'B.audience === audience /*PAZA:audience*/,', 'true,', 'ATAC: reluata la alt verificator'],
['nonce-ul verificatorului nu se mai compara', L, 'B.nonce === nonce /*PAZA:nonce*/,', 'true,', 'ATAC: reluata la alt verificator'],
// 2026-10-18 (B8): cererea
['cererea nu se mai compara (o prezentare pentru get depusa pentru revoke)', L, 'B.request === request /*PAZA:request*/,', 'true,', 'ATAC: o prezentare semnata pentru alta cerere'],
['cererea lipsa din legatura acceptata de un verificator care o cere', L, 'B.request === request /*PAZA:request*/,', '(B.request === undefined || B.request === request),', 'ATAC: o prezentare FARA cerere'],
['present nu mai pune cererea in legatura', L, "...(request != null ? { request } : {}) };", '};', 'cererea: present({ request })'],
['o cerere semnata si nejudecata nu mai e spusa', L, "else if (Object.hasOwn(B, 'request')) nejudecat(", 'else if (false) nejudecat(', 'cererea nejudecata'],
['prospetimea prezentarii nu se mai cere', L, 'Math.abs(acum - t) <= maxAgeS * 1000 /*PAZA:freshness*/,', 'true,', 'ATAC: prezentare veche'],
['oricine poate prezenta credentialul detinatorului', L, 'asteptat && B.presenter.id === asteptat.id && canonical(B.presenter.keys) === canonical(asteptat.keys) /*PAZA:presenter*/,', 'true,', 'ATAC: un strain prezinta'],
['prezentatorul nu mai trebuie sa fie ultimul delegat', L, 'asteptat && B.presenter.id === asteptat.id && canonical(B.presenter.keys) === canonical(asteptat.keys) /*PAZA:presenter*/,', 'true,', 'ATAC: detinatorul prezinta singur'],

View File

@ -259,20 +259,30 @@ export function statusBit(list, index) {
}
// ---------------------------------------------------------------- prezentarea
// CEREREA (2026-10-18, B8, revizuirea adversariala a lui pq-secrets): fara ea, semnatura prezentatorului acopera verificatorul, nonce-ul
// si momentul, dar NU ce i se cere verificatorului; o prezentare facuta pentru "citeste db" a fost depusa ca "revoca grantul" pe db.
// `request` e digestul cererii pe care detinatorul o face (0x + 64 hex, calculat de verificator intr-un singur loc, din cerere), pus in
// legatura INAINTE de semnare; verificatorul care il da cere ca legatura sa poarte exact acel digest. Modelul, citit 2026-10-18: OpenID
// for Verifiable Presentations 1.0 (Final, 9 iulie 2025), sectiunea 5.1 (parametrul `transaction_data` al cererii), 8.4 (datele
// tranzactiei semnate cu aceeasi cheie ca dovada de posesie a credentialului) si anexa B.3.3 (in Key Binding JWT-ul unui SD-JWT VC:
// `transaction_data_hashes`, sha-256 implicit, peste sirul primit, fara decodare).
const DIGEST = /^0x[0-9a-f]{64}$/;
/**
* Prezinta un credential: arata numai afirmatiile din `reveal` si leaga totul de publicul si nonce-ul verificatorului.
* `presenter` e detinatorul, sau, cu `delegations` (lantul de la detinator la el), delegatul.
* `presenter` e detinatorul, sau, cu `delegations` (lantul de la detinator la el), delegatul. `request` (optional): digestul cererii
* catre verificator (0x + 64 hex), semnat in legatura; fara el legatura e exact cea de pana acum.
*/
export function present({ credential, disclosures = [], reveal = [], presenter, delegations = [], audience, nonce, now = new Date() }) {
export function present({ credential, disclosures = [], reveal = [], presenter, delegations = [], audience, nonce, now = new Date(), request = null }) {
if (!presenter || !presenter.privat) throw new Error('aere-identity: presenting needs the presenter\'s private keys');
if (typeof audience !== 'string' || !audience || typeof nonce !== 'string' || !nonce) throw new Error('aere-identity: a presentation needs the verifier\'s audience and nonce');
if (request != null && (typeof request !== 'string' || !DIGEST.test(request))) throw new Error('aere-identity: request is the digest of the request (0x + 64 lowercase hex)');
const dupa = new Map(disclosures.map((e) => [decodeDisclosure(e).name, e]));
const alese = [];
// o afirmatie in clar se vede oricum: numele ei in `reveal` nu cere nimic
const inClar = (credential && credential.statement && credential.statement.claims) || {};
for (const n of [...new Set(reveal)].sort()) { if (Object.hasOwn(inClar, n)) continue; if (!dupa.has(n)) throw new Error('aere-identity: no disclosure for ' + n); alese.push(dupa.get(n)); }
const binding = { v: VERSION, kind: 'aere-presentation-binding', credentialHash: credentialHash(credential), disclosuresHash: hashOf(alese),
delegations: delegations.map(delegationHash), audience, nonce, createdAt: iso(now), presenter: { id: presenter.id, keys: presenter.public } };
delegations: delegations.map(delegationHash), audience, nonce, createdAt: iso(now), presenter: { id: presenter.id, keys: presenter.public }, ...(request != null ? { request } : {}) };
return { v: VERSION, kind: 'aere-presentation', credential, disclosures: alese, delegations, binding, signature: signText('presentation', canonical(binding), presenter) };
}
@ -293,11 +303,15 @@ export const CLOCK_SKEW_S = 60;
export const REGULI = ['form', 'issuer-id', 'holder-id', 'credential-signature', 'issuer-trusted', 'credential-window', 'status-list', 'credential-revoked', 'sd-distinct',
'disclosure-readable', 'disclosure-signed', 'disclosure-once', 'disclosure-plain', 'claim-name', 'binding-credential', 'binding-disclosures', 'binding-delegations', 'presenter',
'presentation-signature', 'audience', 'nonce', 'freshness', 'chain-length', 'delegation-form', 'delegation-ids', 'delegation-from', 'delegation-parent', 'delegation-signature',
'delegation-scope', 'delegation-window', 'delegation-depth', 'delegation-narrows', 'delegation-credential', 'delegation-claims', 'delegation-audience', 'delegation-revoked', 'unreadable'];
export const NEJUDECATE = ['issuer-trust', 'status-none', 'status-unread', 'audience', 'nonce', 'revocation-ignored', 'revocations-none'];
'delegation-scope', 'delegation-window', 'delegation-depth', 'delegation-narrows', 'delegation-credential', 'delegation-claims', 'delegation-audience', 'delegation-revoked', 'unreadable',
// 2026-10-18: the presentation commits to the request the verifier received (OpenID4VP transaction_data, SD-JWT transaction_data_hashes)
'request'];
// `request` as a cause: the binding commits to a request and the verifier gave none to compare. A binding with no request, judged by a
// verifier that gives none, has NO request row at all (the rows of every presentation made before the field existed are unchanged).
export const NEJUDECATE = ['issuer-trust', 'status-none', 'status-unread', 'audience', 'nonce', 'revocation-ignored', 'revocations-none', 'request'];
export const OPRIRI = ['form', 'chain-length', 'delegation-form', 'unreadable'];
class Nedeclarata extends Error {}
export function verifyPresentation(p, { audience = null, nonce = null, now = new Date(), trustedIssuers = null, statusLists = [], revocations = [], maxAgeS = 300, clockSkewS = CLOCK_SKEW_S } = {}) {
export function verifyPresentation(p, { audience = null, nonce = null, now = new Date(), trustedIssuers = null, statusLists = [], revocations = [], maxAgeS = 300, clockSkewS = CLOCK_SKEW_S, request = null } = {}) {
const rows = [];
const ok = (rule, name, pass, detail = '') => { if (!REGULI.includes(rule)) throw new Nedeclarata('aere-identity: undeclared rule ' + rule); rows.push({ name, pass: !!pass, detail: pass ? '' : detail, rule }); return !!pass; };
const nejudecat = (cauza, name, detail) => { if (!NEJUDECATE.includes(cauza)) throw new Nedeclarata('aere-identity: undeclared cause ' + cauza); rows.push({ name, pass: null, detail, unjudged: cauza }); };
@ -313,6 +327,7 @@ export function verifyPresentation(p, { audience = null, nonce = null, now = new
if (trustedIssuers != null && !Array.isArray(trustedIssuers)) throw new Error('verifyPresentation: trustedIssuers is a list of ids or public keys');
const idsIncredere = trustedIssuers == null ? null : trustedIssuers.map((x) => (typeof x === 'string' && ID.test(x) ? x : idOf(x)));
if (!Array.isArray(statusLists) || !Array.isArray(revocations)) throw new Error('verifyPresentation: statusLists and revocations are lists');
if (request != null && (typeof request !== 'string' || !DIGEST.test(request))) throw new Error('verifyPresentation: request is the digest of the request the verifier received (0x + 64 lowercase hex)');
try {
if (!((!!p && p.kind === 'aere-presentation' && p.v === VERSION && !!p.credential && !!p.binding && Array.isArray(p.disclosures) && Array.isArray(p.delegations)) /*PAZA:form*/)) {
ok('form', 'presentation: well formed', false, 'not an aere-presentation'); return gata();
@ -382,6 +397,10 @@ export function verifyPresentation(p, { audience = null, nonce = null, now = new
else ok('audience', `presentation: made for ${audience}`, B.audience === audience /*PAZA:audience*/, `made for ${B.audience}`);
if (nonce == null) nejudecat('nonce', 'presentation: nonce', 'not judged: without the verifier\'s nonce an old presentation can be replayed');
else ok('nonce', 'presentation: carries the verifier\'s nonce', B.nonce === nonce /*PAZA:nonce*/, 'another nonce');
// the request: given by the verifier, the binding must carry exactly that digest (absent is a refusal); not given, a binding that
// carries one leaves it unjudged, and a binding without one has no row (as before the field existed)
if (request != null) ok('request', 'presentation: commits to the request the verifier received', B.request === request /*PAZA:request*/, Object.hasOwn(B, 'request') ? 'signed for another request' : 'the presentation commits to no request');
else if (Object.hasOwn(B, 'request')) nejudecat('request', 'presentation: request', 'not judged: the presentation commits to a request and the verifier gave none to compare it with');
const t = data(B.createdAt, 'createdAt');
ok('freshness', `presentation: made within ${maxAgeS} s of the verifier's clock`, Math.abs(acum - t) <= maxAgeS * 1000 /*PAZA:freshness*/, `made at ${B.createdAt}`);
// 7. lantul de delegare

View File

@ -105,6 +105,33 @@ test('plicul verifica la verificatorul AIP-23 de referinta; unul cu rezultatul r
} finally { fs.rmSync(T, { recursive: true, force: true }); }
});
// ---------------------------------------------------------------- 2026-10-18 (B8): cererea si verificarea o data pentru mai multe politici
const CER = '0x' + crypto.createHash('sha256').update('cererea').digest('hex');
const prezC = () => I.present({ credential, disclosures, reveal: ['age_over_18', 'jurisdiction', 'kyc_level'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW, request: CER });
test('cererea: checkCompliance da cererea verificatorului de identitate: aceeasi cerere conform; alta cerere neconform (presentation)', () => {
const p = prezC();
const a = judeca(p, POL, { request: CER }), b = judeca(p, POL, { request: '0x' + '0'.repeat(64) });
cere(a.compliant && !b.compliant && b.rules.includes('presentation') && /commits to the request/.test(b.reasons.join(' ')), JSON.stringify([a.reasons, b.reasons]));
});
test('cache: aceeasi prezentare dupa patru politici (doua perechi de emitenti si varsta) da EXACT aceleasi rezultate cu cache si fara, cu trei verificari in loc de patru (doua politici au aceeasi pereche)', () => {
const p = prezC(); const liste = [LISTA]; const o = { audience: AUD, nonce: NONCE, now: NOW, statusLists: liste, request: CER };
const pol = [POL, { ...POL, id: 'p2', require: [{ claim: 'kyc_level', atLeast: 3 }] }, { ...POL, id: 'p3', maxAgeS: 60 }, { ...POL, id: 'p4', trustedIssuers: [alt.id] }];
// numaratoarea verificarilor: fiecare verifyPresentation citeste o data semnatura prezentarii
let citiri = 0; const numarat = new Proxy(p, { get: (t, k) => { if (k === 'signature') citiri++; return t[k]; } });
const fara = pol.map((x) => checkCompliance(numarat, x, o)); const nFara = citiri; citiri = 0;
const cache = new Map(); const cu = pol.map((x) => checkCompliance(numarat, x, { ...o, cache })); const nCu = citiri;
cere(JSON.stringify(fara) === JSON.stringify(cu), 'rezultatele difera cu cache');
cere(nFara === 4 && nCu === 3, `verificari: fara cache ${nFara}, cu cache ${nCu} (asteptat 4 si 3: POL si p2 au aceeasi pereche)`);
cere(fara[0].compliant && !fara[1].compliant && fara[2].compliant && !fara[3].compliant, JSON.stringify(fara.map((x) => x.compliant)));
});
test('CONTROL: un cache folosit pentru alta prezentare sau alt context arunca (o greseala a apelantului, nu un verdict de imprumut)', () => {
const cache = new Map(); const o = { audience: AUD, nonce: NONCE, now: NOW, statusLists: [LISTA] };
checkCompliance(prezC(), POL, { ...o, cache });
cere(/a cache serves one presentation/.test(arunca(() => checkCompliance(prezC(), POL, { ...o, cache })) || ''), 'alta prezentare a primit verdictul din cache');
const p = prezC(); const c2 = new Map(); checkCompliance(p, POL, { ...o, cache: c2 });
cere(/a cache serves one presentation/.test(arunca(() => checkCompliance(p, POL, { ...o, cache: c2, nonce: 'alt' })) || ''), 'alt nonce a primit verdictul din cache');
cere(/a cache serves one presentation/.test(arunca(() => checkCompliance(p, POL, { ...o, cache: c2, request: CER })) || ''), 'alta cerere a primit verdictul din cache');
});
test('linia de comanda: comply -> COMPLIANT (0) si inregistrarea scrisa fara date personale; o prezentare care nu arata destul -> NOT COMPLIANT (1)', () => {
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-conf-cli-')); const CLI = path.join(AICI, 'identity-cli.mjs');
const run = (...a) => { const r = spawnSync(process.execPath, [CLI, ...a], { cwd: T, encoding: 'utf8' }); return { cod: r.status, out: (r.stdout || '') + (r.stderr || '') }; };

View File

@ -101,6 +101,37 @@ test('ATAC: reluata la alt verificator (alt public) -> INVALID; cu alt nonce ->
const r1 = I.verifyPresentation(prez(), { ...toate, audience: 'https://other.example' }); cere(picaPe(r1, /made for/), fals(r1) || 'trecut');
const r2 = I.verifyPresentation(prez(), { ...toate, nonce: 'alt' }); cere(picaPe(r2, /nonce/), fals(r2) || 'trecut');
});
// ---------------------------------------------------------------- cererea (2026-10-18, B8): ce i se cere verificatorului, semnat in legatura
const CER = '0x' + crypto.createHash('sha256').update('cererea: get db').digest('hex'), ALTA = '0x' + crypto.createHash('sha256').update('cererea: revoke db').digest('hex');
test('cererea: present({ request }) o pune in legatura semnata; verificatorul care o da cere exact acel digest: VALID, randul request trecut', () => {
const p = prez({ request: CER });
cere(p.binding.request === CER, 'legatura nu poarta cererea: ' + JSON.stringify(Object.keys(p.binding)));
const r = I.verifyPresentation(p, { ...toate, request: CER });
cere(r.valid && r.notJudged === 0 && rand(r, /commits to the request/).pass === true, fals(r) || JSON.stringify(r.rows.filter((x) => x.pass !== true)));
});
test('ATAC: o prezentare semnata pentru alta cerere (get db) depusa pentru alta (revoke db) -> INVALID, regula request', () => {
const r = I.verifyPresentation(prez({ request: CER }), { ...toate, request: ALTA });
cere(!r.valid && r.rules.join(',') === 'request' && /another request/.test(rand(r, /commits to the request/).detail), fals(r) || 'trecut');
});
test('ATAC: o prezentare FARA cerere la un verificator care o cere -> INVALID, regula request ("commits to no request")', () => {
const r = I.verifyPresentation(prez(), { ...toate, request: CER });
cere(!r.valid && r.rules.join(',') === 'request' && /no request/.test(rand(r, /commits to the request/).detail), fals(r) || 'trecut');
});
test('ATAC: cererea rescrisa in legatura dupa semnare -> INVALID (semnatura prezentatorului); cu cererea noua data verificatorului, tot INVALID', () => {
const p = prez({ request: CER }); p.binding.request = ALTA;
const r = I.verifyPresentation(p, { ...toate, request: ALTA });
cere(!r.valid && r.rules.includes('presentation-signature') && !r.rules.includes('request'), fals(r) || 'trecut');
});
test('cererea nejudecata: legatura poarta o cerere, verificatorul nu da niciuna -> VALID cu randul request NEJUDECAT (cauza request); o legatura fara cerere la un verificator fara cerere nu are rand request', () => {
const r = I.verifyPresentation(prez({ request: CER }), toate);
cere(r.valid && r.notJudged === 1 && r.unjudged.join(',') === 'request' && rand(r, /presentation: request/).pass === null, JSON.stringify(r.rows.filter((x) => x.pass !== true)));
const v = I.verifyPresentation(prez(), toate);
cere(v.valid && v.notJudged === 0 && !v.rows.some((x) => /request/.test(x.name)), JSON.stringify(v.rows.filter((x) => /request/.test(x.name))));
});
test('CONTROL: o cerere care nu e un digest e refuzata de present si de verificator (o greseala a celui care o da, nu o prezentare invalida)', () => {
cere(/request is the digest/.test(arunca(() => prez({ request: 'get db' })) || ''), 'present a primit o cerere care nu e digest');
cere(/request is the digest/.test(arunca(() => I.verifyPresentation(prez(), { ...toate, request: CER.toUpperCase().replace('0X', '0x') })) || ''), 'verificatorul a primit un digest cu majuscule');
});
test('ATAC: prezentare veche (301 s) sau din viitor (301 s) -> INVALID', () => {
const r1 = I.verifyPresentation(prez(), { ...toate, now: new Date(NOW.getTime() + 301000) }); cere(picaPe(r1, /within 300 s/), fals(r1) || 'veche trecuta');
const r2 = I.verifyPresentation(prez(), { ...toate, now: new Date(NOW.getTime() - 301000) }); cere(picaPe(r2, /within 300 s/), fals(r2) || 'viitoare trecuta');

4
pq-secrets/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# dosarul implicit de date (service.json, secretele sigilate, jurnalul sidecar-ului, KMS-ul in proces) si dosarul de incredere al
# instalarii (listele de stare si revocarile aduse de operator); nu intra niciodata in depozit
data/
trust/

357
pq-secrets/README.md Normal file
View File

@ -0,0 +1,357 @@
# 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:<rule>*/` 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=<the name presentations are made for, e.g. https://secrets.example>
export AERE_SECRETS_ADMIN_POLICY=<path of a JSON compliance policy: id, trustedIssuers, require>
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/<name>.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` | `<data>/aere-audit.log` | the sidecar log |
| `AERE_KMS_ROOT_KEY` / `AERE_KMS_DATA_DIR` | none / `<data>/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<n>:...`, AAD
`aeresecrets/v1/mac-key\0<audience>`). 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/<name>.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<name>\0<version>`) and
`sealed` (`base64(iv(12) || AES-256-GCM ciphertext || tag(16))` of the value under that data key, AAD
`aeresecrets/v1/value\0<name>\0<version>`). 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=<verify-proof.mjs> 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.

828
pq-secrets/secrete.mjs Normal file
View File

@ -0,0 +1,828 @@
// secrete.mjs - Aere PQ Secrets: un serviciu de secrete (nume, versiuni, metadate) peste Aere PQ KMS, in care FIECARE acces e autorizat
// de o identitate AERE PREZENTATA (zero trust fata de retea si fata de apelant: nicio incredere in adresa, in token sau in gazda
// apelantului, numai in dovada prezentata) si inregistrat ca plic AIP-23 in jurnalul sidecar-ului (AERE Verification Layer).
// Roadmap pista B, punctul B8 (linia Shield, partea DA a hotararii 4 a fondatorului), 2026-10-18. Numai Node 24, fara dependinte.
//
// CE FACE:
// - fiecare versiune a unui secret e cifrata cu o cheie de date proprie (AES-256-GCM), iar cheia de date e invelita de KMS in plicul
// lui hibrid (X25519 + ML-KEM-768); pe disc stau numai forma sigilata si plicul KMS; fisierul secretului poarta un HMAC sub o cheie
// a serviciului, ea insasi invelita de KMS (un grant adaugat pe disc cu mana e refuzat);
// - fiecare cerere poarta o PREZENTARE a unei credentiale AERE (tools/aere-identity), facuta pentru ACEST serviciu (audienta = numele
// serviciului), cu un NONCE emis de serviciu si consumat la prima vedere, intr-un termen (prospetimea verificatorului de identitate),
// semnata de cheia detinatorului sau a unui delegat al lui, si legata de CEREREA insasi (`request`, digestul operatiei, al secretului
// si al corpului, requestHash mai jos): o prezentare facuta pentru "get db" nu deschide "revoke" pe db, nici "get" pe alt secret;
// verificarea e cea de productie, verifyPresentation, iar politica fiecarui grant e judecata de checkCompliance;
// - un grant spune ce operatii permite (put, get, metadata, rotate, revoke-version, grant, revoke), daca si versiunile VECHI se pot
// citi (`versions: 'any'`), caror detinatori (`holders`) si daca si delegatilor lor (`delegates`); politica de ADMIN permite tot; un
// grant facut de un ne-admin e o ATENUARE a grantului prin care a intrat (regula grant-scope);
// - fiecare refuz isi numeste regula (REGULI, cu /*PAZA:<regula>*/ pe conditia buna); un refuz de AUTORIZARE e acelasi raspuns, octet cu
// octet, pentru un secret care nu exista, unul fara grant potrivit si unul cu grant si regula picata; detaliul sta numai in jurnal;
// - fiecare operatie, reusita sau refuzata, e o intrare `secret-access` (tools/proof-kinds) in jurnalul sidecar-ului; pentru o operatie
// care MUTA, randul de audit se scrie si se fsync-uieste INAINTE de schimbare, iar o schimbare care nu se poate pune pe disc dupa el
// primeste al doilea rand, `failed`: la o cadere ramane o intrare fara schimbare, niciodata o schimbare fara intrare.
//
// CE NU FACE (spus si in README): nu apara impotriva gazdei pe care ruleaza (procesul tine in memorie cheile de date cat le foloseste,
// si, cu KMS-ul in proces, chiar cheia radacina a KMS-ului); fara HSM radacina KMS-ului e un fisier sigilat de un secret din mediu; nu
// detecteaza o intoarcere a dosarului de date la o stare veche valid-MAC-ata; limiteaza numai nonce-urile in zbor pe adresa; un singur
// proces pe un dosar de date (lacatul service.lock o cere); nu atesta runtime-ul serviciului insusi (asta e Proof of runtime).
//
// Mesajele de eroare sunt texte fixe plus nume de secrete, numere de versiune si id-uri de grant: niciodata o valoare, un nonce, material
// de cheie, sau o intrare a cererii pe care nu a verificat-o nimeni.
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import http from 'node:http';
import https from 'node:https';
import { fileURLToPath } from 'node:url';
import { KmsError, canonicalJson } from '../pq-kms/kms.mjs';
// Componentele surori, in forma publicata (alaturi: ../identity, ../proof-kinds, ../verify-layer) sau in depozitul de dezvoltare
// (tools/aere-identity, tools/proof-kinds, tools/aere-verify-layer). Un rand cu candidati, cum face control-plane/raport-conformitate.mjs.
const U_ID = [new URL('../identity/identity.mjs', import.meta.url), new URL('../../tools/aere-identity/identity.mjs', import.meta.url)].find((u) => fs.existsSync(fileURLToPath(u)));
const U_CF = [new URL('../identity/conformitate.mjs', import.meta.url), new URL('../../tools/aere-identity/conformitate.mjs', import.meta.url)].find((u) => fs.existsSync(fileURLToPath(u)));
const U_PK = [new URL('../proof-kinds/proof-kinds.mjs', import.meta.url), new URL('../../tools/proof-kinds/proof-kinds.mjs', import.meta.url)].find((u) => fs.existsSync(fileURLToPath(u)));
const U_SC = [new URL('../verify-layer/sidecar.mjs', import.meta.url), new URL('../../tools/aere-verify-layer/sidecar.mjs', import.meta.url)].find((u) => fs.existsSync(fileURLToPath(u)));
for (const [u, ce] of [[U_ID, 'identity/identity.mjs'], [U_CF, 'identity/conformitate.mjs'], [U_PK, 'proof-kinds/proof-kinds.mjs'], [U_SC, 'verify-layer/sidecar.mjs']]) {
if (!u) throw new Error(`pq-secrets: ${ce} was not found beside this component (as published) nor in the development tree`);
}
const ID = await import(U_ID.href);
const CF = await import(U_CF.href);
const PK = await import(U_PK.href);
const SC = await import(U_SC.href);
/** modulele surori rezolvate aici, ca probele sa nu le rezolve a doua oara (doua rezolvari diverg); `cai` = fisierele lor */
export const MODULE = Object.freeze({ identity: ID, conformitate: CF, proofKinds: PK, sidecar: SC,
cai: Object.freeze({ identity: fileURLToPath(U_ID), conformitate: fileURLToPath(U_CF), proofKinds: fileURLToPath(U_PK), sidecar: fileURLToPath(U_SC) }) });
export const VERSION = 1;
export const KIND = 'secret-access';
export const FORMAT_OBIECT = 'aeresecrets-secret/1'; // formatul fisierului unui secret
export const FORMAT_SERVICE = 'aeresecrets-service/1';
/** operatiile pe care le poate numi un grant (list nu e pe secret: vede numele secretelor cu un grant al detinatorului) */
export const OPERATII_GRANT = Object.freeze(['put', 'get', 'metadata', 'rotate', 'revoke-version', 'grant', 'revoke']);
export const OPERATII = Object.freeze([...OPERATII_GRANT, 'list']);
/** operatiile pe care numai adminul le poate da mai departe (o re-delegare a administrarii) */
const OPERATII_ADMIN = Object.freeze(['grant', 'revoke']);
/** regulile pe care le poate numi un refuz al serviciului; orice alta e o greseala a codului, nu un refuz */
export const REGULI = Object.freeze(['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']);
export const MAX_VALUE = 65536; // octeti (Azure Key Vault: 25 KB; AWS Secrets Manager: 64 KB)
export const MAX_METADATA = 4096; // octeti de JSON canonic
export const MAX_GRANTS = 32; // grant-uri pe un secret: fiecare costa o judecata de politica pentru orice apelant
export const MAX_JSON_DEPTH = 32; // adancimea corpului unei cereri, prezentarea inclusa (una reala are 8)
export const MAX_JSON_NODES = 20000; // valorile corpului unei cereri (o prezentare reala are ~150, 32 de grant-uri ~1000)
export const MAX_AUDIT_ROW = 16384; // octetii unui plic de audit; campurile lui sunt marginite, plafonul e paznicul de la capat
export const NONCE_TTL_S = 120;
export const NONCE_MAX = 10000;
export const NONCE_PER_ORIGIN = 32; // nonce-uri in zbor pe o adresa de origine (serverul HTTP o da); peste: NONCE_ORIGIN_FULL (429)
const NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
// numele de dispozitiv ale Windows: un fisier con.json, nul.json... nu e un fisier obisnuit pe toate versiunile de Windows
const NUME_REZERVATE = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/;
const GRANT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/;
const META_KEY_RE = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/;
const CHEI_REZERVATE = new Set(['__proto__', 'constructor', 'prototype']);
const AERE_ID_RE = /^aere-id:[0-9a-f]{40}$/;
const L = Object.freeze({ dek: 'aeresecrets/v1/dek', value: 'aeresecrets/v1/value', mac: 'aeresecrets/v1/mac-key', request: 'aere-secrets-request' });
/** raspunsul unui refuz de autorizare cu identitate valida: acelasi text pentru orice secret, existent sau nu, cu grant-uri sau fara */
export const MESAJ_NEAUTORIZAT = 'no grant admits this presentation for this operation on this secret (a secret that does not exist is refused the same way)';
export class SecretsError extends Error {
constructor(code, message, extra) {
super(message);
this.name = 'SecretsError';
this.code = code;
if (extra) Object.assign(this, extra);
}
}
const refuz = (rule, code, message, extra = {}) => {
if (!REGULI.includes(rule)) throw new Error('pq-secrets: undeclared rule ' + rule);
return new SecretsError(code, message, { rule, ...extra });
};
// --------------------------------------------------------------------------------------------- primitive
const utf8 = (s) => Buffer.from(s, 'utf8');
const sha256hex = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex');
const hmac = (key, b) => crypto.createHmac('sha256', key).update(b).digest();
const ctEq = (a, b) => a.length === b.length && crypto.timingSafeEqual(a, b);
const iso = (d) => new Date(d).toISOString();
/** o versiune ceruta, numai daca e un intreg: un mesaj nu reflecta o intrare pe care nu a verificat-o nimeni */
const versiuneAfisata = (v) => (Number.isSafeInteger(v) ? String(v) : '(not an integer)');
/** un id pretins de o prezentare neverificata: numai in forma aere-id (marime fixa), altfel nimic */
const idPretins = (x) => (typeof x === 'string' && AERE_ID_RE.test(x) ? x : null);
function writeFileAtomic(p, text) {
const tmp = `${p}.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
const fd = fs.openSync(tmp, 'w', 0o600);
try { fs.writeSync(fd, text); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
try { fs.renameSync(tmp, p); } catch (e) { try { fs.rmSync(tmp, { force: true }); } catch { /* temporarul ramane; nu poarta nicio valoare in clar */ } throw e; }
}
function checkName(name) {
if (typeof name !== 'string' || !NAME_RE.test(name)) throw refuz('secret-name', 'INVALID_SECRET_NAME', 'secret name must match ^[a-z0-9][a-z0-9_-]{0,63}$');
if (NUME_REZERVATE.test(name)) throw refuz('secret-name', 'INVALID_SECRET_NAME', 'secret name is a device name reserved by Windows (con, prn, aux, nul, com0-9, lpt0-9)');
return name;
}
function checkAudience(a) {
if (typeof a !== 'string' || a.length === 0 || a.length > 256 || !/^[\x21-\x7e]+$/.test(a)) {
throw new SecretsError('AUDIENCE_INVALID', 'the audience must be 1..256 printable ASCII characters without spaces (the name under which presentations are made for this service)');
}
return a;
}
/**
* Marimea unui corp de cerere, fara recursie (o lista imbricata de 30.000 de ori nu arunca stiva aici): null daca e in limite, altfel
* ce limita a depasit. Judecata INAINTEA oricarei canonizari; octetii unei valori (Buffer) sunt o singura valoare.
*/
function preaComplex(v, maxAdancime = MAX_JSON_DEPTH, maxNoduri = MAX_JSON_NODES) {
const stiva = [[v, 1]]; let noduri = 1;
while (stiva.length) {
const [x, d] = stiva.pop();
if (d > maxAdancime) return 'depth';
if (x === null || typeof x !== 'object' || Buffer.isBuffer(x) || x instanceof Uint8Array) continue;
for (const k of Object.keys(x)) { if (++noduri > maxNoduri) return 'nodes'; stiva.push([x[k], d + 1]); }
}
return null;
}
/** metadatele unei versiuni: un obiect mic de perechi nume -> sir/numar/boolean; refuzat altfel */
function checkMetadata(m) {
if (m === undefined || m === null) return {};
if (!m || typeof m !== 'object' || Array.isArray(m)) throw refuz('value-form', 'INVALID_METADATA', 'metadata must be a JSON object of name -> string | number | boolean');
const chei = Object.keys(m);
if (chei.length > 16) throw refuz('value-form', 'INVALID_METADATA', 'metadata has at most 16 entries');
const out = {};
for (const k of chei.sort()) {
const v = m[k];
if (!META_KEY_RE.test(k)) throw refuz('value-form', 'INVALID_METADATA', 'a metadata name must match ^[A-Za-z_][A-Za-z0-9_.-]{0,63}$');
// __proto__ ar disparea in tacere la out[k] = v (ar seta prototipul), constructor si prototype sunt numele lui: refuzate pe nume
if (CHEI_REZERVATE.has(k)) throw refuz('value-form', 'INVALID_METADATA', `metadata name "${k}" is reserved (__proto__, constructor, prototype)`);
if (!((typeof v === 'string' && v.length <= 1024) || (typeof v === 'number' && Number.isFinite(v)) || typeof v === 'boolean')) {
throw refuz('value-form', 'INVALID_METADATA', `metadata "${k}" must be a string of at most 1024 characters, a finite number or a boolean`);
}
out[k] = v;
}
if (Buffer.byteLength(canonicalJson(out)) > MAX_METADATA) throw refuz('value-form', 'INVALID_METADATA', `metadata is larger than ${MAX_METADATA} bytes`);
return out;
}
function toValue(v) {
let b;
if (Buffer.isBuffer(v) || v instanceof Uint8Array) b = Buffer.from(v);
else if (typeof v === 'string') b = utf8(v);
else throw refuz('value-form', 'INVALID_VALUE', 'value must be a Buffer, Uint8Array or string');
if (b.length === 0) throw refuz('value-form', 'INVALID_VALUE', 'value must not be empty');
if (b.length > MAX_VALUE) throw refuz('value-form', 'VALUE_TOO_LARGE', `value is larger than ${MAX_VALUE} bytes`);
return b;
}
/**
* Digestul unei CERERI catre acest serviciu, intr-un singur loc: clientul il pune in prezentare (present({ request })) INAINTE de
* semnare, serviciul il calculeaza din cererea primita si cere ca prezentarea sa poarte exact acel digest (regula `request` a
* verificatorului de identitate). Modelul: `transaction_data` din OpenID4VP 1.0 si `transaction_data_hashes` din Key Binding JWT.
*
* sha256( JSON canonic al { v: 1, kind: 'aere-secrets-request', service, operation, secret, body } )
*
* `service` = audienta serviciului; `operation` = una din OPERATII; `secret` = numele (null la list); `body` = TOATE campurile corpului
* cererii in afara de `presentation` si de cele `undefined` (version, value, metadata, grants, grant, grantId, bytes, encoding, si orice
* alt camp: unul adaugat de cine nu a semnat schimba digestul). `value` (la put) intra ca base64-ul octetilor: din biblioteca, un Buffer
* sau un sir (octetii lui UTF-8, `valueEncoding: 'bytes'`, implicit); din HTTP, sirul base64 asa cum pleaca (`valueEncoding: 'base64'`).
* JSON-ul canonic e al identitatii (chei sortate, fara spatii; arunca pe o valoare pe care JSON nu o poate spune).
*/
export function requestHash({ audience, operation, secret = null, body = {}, valueEncoding = 'bytes' } = {}) {
if (typeof audience !== 'string' || !audience) throw new SecretsError('REQUEST_HASH_INPUT', 'requestHash needs the audience of the service');
if (!OPERATII.includes(operation)) throw new SecretsError('REQUEST_HASH_INPUT', 'requestHash needs an operation, one of: ' + OPERATII.join(', '));
if (secret !== null && typeof secret !== 'string') throw new SecretsError('REQUEST_HASH_INPUT', 'requestHash: secret is a name, or null for list');
if (!body || typeof body !== 'object' || Array.isArray(body)) throw new SecretsError('REQUEST_HASH_INPUT', 'requestHash: body is the object of the request');
if (valueEncoding !== 'bytes' && valueEncoding !== 'base64') throw new SecretsError('REQUEST_HASH_INPUT', 'requestHash: valueEncoding is bytes or base64');
const corp = Object.create(null); // fara prototip: o cheie __proto__ a cererii ramane o cheie, nu un prototip
for (const k of Object.keys(body)) {
if (k === 'presentation' || body[k] === undefined) continue;
let v = body[k];
if (k === 'value' && operation === 'put' && valueEncoding === 'bytes') {
if (Buffer.isBuffer(v) || v instanceof Uint8Array) v = Buffer.from(v).toString('base64');
else if (typeof v === 'string') v = utf8(v).toString('base64');
}
corp[k] = v;
}
return sha256hex(utf8(ID.canonical({ v: 1, kind: L.request, service: audience, operation, secret, body: corp })));
}
/** Un grant, in forma normala: { id, operations, versions, holders?, delegates?, policy } ; politica trece prin definePolicy. */
export function normalizeGrant(g) {
if (!g || typeof g !== 'object' || Array.isArray(g)) throw refuz('grant-form', 'INVALID_GRANT', 'a grant is an object { id, operations, policy, versions?, holders?, delegates? }');
const extra = Object.keys(g).filter((k) => !['id', 'operations', 'policy', 'versions', 'holders', 'delegates'].includes(k));
if (extra.length) throw refuz('grant-form', 'INVALID_GRANT', 'unknown grant field: ' + extra.join(', '));
if (typeof g.id !== 'string' || !GRANT_ID_RE.test(g.id)) throw refuz('grant-form', 'INVALID_GRANT', 'grant id must match ^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$');
// `admin` numeste politica de admin in jurnal (grant: "admin"): un grant al unui secret nu poate purta acelasi nume
if (!(g.id !== 'admin' /*PAZA:grant-form*/)) throw refuz('grant-form', 'INVALID_GRANT', 'grant id "admin" is reserved for the admin policy');
if (!Array.isArray(g.operations) || !g.operations.length || g.operations.some((o) => !OPERATII_GRANT.includes(o))) {
throw refuz('grant-form', 'INVALID_GRANT', 'grant operations is a non-empty list from: ' + OPERATII_GRANT.join(', '));
}
const versions = g.versions === undefined ? 'current' : g.versions;
if (versions !== 'current' && versions !== 'any') throw refuz('grant-form', 'INVALID_GRANT', 'grant versions is "current" (default) or "any"');
let holders;
if (g.holders !== undefined) {
if (!Array.isArray(g.holders) || !g.holders.length || g.holders.some((h) => typeof h !== 'string' || !AERE_ID_RE.test(h))) {
throw refuz('grant-form', 'INVALID_GRANT', 'grant holders is a non-empty list of aere-id:<40 hex> identities');
}
holders = [...new Set(g.holders)].sort();
}
if (g.delegates !== undefined && typeof g.delegates !== 'boolean') throw refuz('grant-form', 'INVALID_GRANT', 'grant delegates is true (default: a delegate of an admitted holder is admitted) or false (the holder alone)');
let policy;
try { policy = CF.definePolicy(g.policy).policy; } catch (e) { throw refuz('grant-form', 'INVALID_GRANT', 'grant policy: ' + String(e && e.message || e)); }
// Un grant nu poate scuti starea credentialului: o credentiala pe care emitentul nu o poate revoca nu deschide un secret. Politica de
// conformitate accepta requireStatus: false pentru alte verificatoare; aici e refuzat PE NUME, la scriere (unicul loc, ca o plantare
// sa aiba un singur paznic de scos).
if (!(g.policy.requireStatus !== false /*PAZA:grant-form*/)) throw refuz('grant-form', 'INVALID_GRANT', 'a grant cannot waive the credential status (requireStatus: false): the status must be judged');
return { id: g.id, operations: [...new Set(g.operations)].sort(), versions, ...(holders ? { holders } : {}), ...(g.delegates === false ? { delegates: false } : {}), policy };
}
/**
* Un grant facut de un ne-admin trebuie sa fie o ATENUARE a grantului `o` prin care a intrat cererea `grant`: operatii incluse in ale
* lui, fara grant si revoke (numai adminul da administrarea mai departe); versiuni incluse (current nu da any); emitentii crezuti
* inclusi; fiecare regula a politicii lui prezenta, neschimbata (deci afirmatii cel putin la fel de stricte), varsta maxima cel mult a
* lui; detinatorii inclusi in ai lui, cand el ii numeste; delegatii opriti, cand el ii opreste. O comparatie care nu se poate face
* sigur (o regula scrisa altfel, chiar daca mai stricta) e un refuz: regula grant-scope.
*/
function atenuare(g, o) {
const nu = (ce) => { throw refuz('grant-scope', 'GRANT_NOT_ATTENUATED', `a grant made by a holder who is not the admin must be narrower than grant "${o.id}" that admitted the request: ${ce}`); };
if (!(g.operations.every((x) => o.operations.includes(x) && !OPERATII_ADMIN.includes(x)) /*PAZA:grant-scope*/)) nu('its operations must be among those of that grant, without grant and revoke');
if (!(o.versions === 'any' || g.versions === 'current' /*PAZA:grant-scope*/)) nu('it may not read older versions (versions: any) when that grant reads only the current one');
if (!(g.policy.trustedIssuers.every((x) => o.policy.trustedIssuers.includes(x)) /*PAZA:grant-scope*/)) nu('it may trust only issuers that grant trusts');
const ale = new Set(g.policy.require.map((r) => ID.canonical(r)));
if (!(o.policy.require.every((r) => ale.has(ID.canonical(r))) && g.policy.maxAgeS <= o.policy.maxAgeS && g.policy.requireStatus === true /*PAZA:grant-scope*/)) {
nu('its policy must keep every rule of that grant unchanged, a maximum age no longer, and the status required');
}
if (!(o.holders === undefined || (Array.isArray(g.holders) && g.holders.every((h) => o.holders.includes(h))) /*PAZA:grant-scope*/)) nu('it must name holders among those that grant names');
if (!(o.delegates !== false || g.delegates === false /*PAZA:grant-scope*/)) nu('it must refuse delegates (delegates: false) as that grant does');
}
// --------------------------------------------------------------------------------------------- clientul HTTP al KMS-ului
/**
* Un client pentru serverul Aere PQ KMS (pq-kms/server.mjs), cu exact metodele pe care le foloseste serviciul de secrete, in formele
* bibliotecii KMS: createKey, getKey, datakey (plaintext base64), decrypt (plaintext Buffer). Refuzurile KMS devin SecretsError cu codul
* `KMS_<cod>` si regula `kms`. Tokenul nu se tipareste si nu apare in erori.
*/
export function kmsClientHttp({ url, token, timeoutMs = 10000 } = {}) {
let base;
try { base = new URL(url); } catch { throw new SecretsError('KMS_URL_INVALID', 'AERE_KMS_URL is not a URL'); }
if (base.protocol !== 'http:' && base.protocol !== 'https:') throw new SecretsError('KMS_URL_INVALID', 'AERE_KMS_URL must be http(s)');
if (typeof token !== 'string' || token.length < 32) throw new SecretsError('KMS_TOKEN_INVALID', 'AERE_KMS_TOKEN must be at least 32 characters');
const mod = base.protocol === 'https:' ? https : http;
const call = (method, p, body) => new Promise((resolve, reject) => {
const data = body === undefined ? null : Buffer.from(JSON.stringify(body));
const headers = { authorization: `Bearer ${token}`, accept: 'application/json' };
if (data) { headers['content-type'] = 'application/json'; headers['content-length'] = data.length; }
const req = mod.request(new URL(p, base), { method, headers, timeout: timeoutMs }, (res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
let json = null;
try { json = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { json = null; }
resolve({ status: res.statusCode, json });
});
});
req.on('timeout', () => req.destroy(new Error('timeout')));
req.on('error', (e) => reject(new SecretsError('KMS_UNREACHABLE', 'the KMS did not answer (' + (e && e.code || 'error') + ')', { rule: 'kms' })));
if (data) req.write(data);
req.end();
});
const ok = (r) => {
if (r.status === 200 && r.json) return r.json;
const cod = r.json && typeof r.json.error === 'string' ? r.json.error : 'HTTP_' + r.status;
throw new SecretsError('KMS_' + cod, 'the KMS refused: ' + cod, { rule: 'kms' });
};
const b64 = (aad) => (aad === undefined || aad === null ? undefined : Buffer.from(aad).toString('base64'));
return {
kind: 'http',
async createKey(name, { type }) { return ok(await call('POST', `/v1/keys/${encodeURIComponent(name)}`, { type })); },
async getKey(name) { return ok(await call('GET', `/v1/keys/${encodeURIComponent(name)}`)); },
async datakey(name, { aad, bits = 256, includePlaintext = true } = {}) {
return ok(await call('POST', `/v1/datakey/${encodeURIComponent(name)}`, { aad: b64(aad), bits, include_plaintext: includePlaintext }));
},
async decrypt(name, ciphertext, aad) {
const j = ok(await call('POST', `/v1/decrypt/${encodeURIComponent(name)}`, { ciphertext, aad: b64(aad) }));
return { plaintext: Buffer.from(String(j.plaintext), 'base64'), version: j.version };
},
};
}
// --------------------------------------------------------------------------------------------- serviciul
export function openSecrets(opts) { return Secrets.open(opts); }
/** traieste procesul cu acest PID pe gazda aceasta? (EPERM: exista, al altui utilizator) */
function traieste(pid) { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } }
const V_IDENTITATE = Symbol('identitatea, verificata o data pe cerere');
export class Secrets {
#dir; #secretsDir; #jurnal; #trust; #kms; #kmsKey; #audience; #admin; #macKey; #nonces = new Map(); #peOrigine = new Map(); #noncePerOrigin = NONCE_PER_ORIGIN;
#acum; #nonceTtlMs; #maxAgeS; #closed = false; #lacate = new Map(); #lacatDosar = null; #lacatPreluat = null;
/**
* @param {object} o
* @param {string} o.dataDir dosarul de date (service.json, secrets/<name>.json, jurnalul implicit, lacatul service.lock)
* @param {object} o.kms o instanta Kms (pq-kms/kms.mjs, in proces) sau kmsClientHttp(...)
* @param {string} o.audience numele sub care se fac prezentarile pentru acest serviciu (ex. https://secrets.example)
* @param {object} o.adminPolicy o politica de conformitate (id, trustedIssuers, require) care permite ORICE operatie
* @param {string} [o.trustDir] dosarul cu status-lists/*.json si revocations/*.json (citite la fiecare cerere)
* @param {string} [o.auditLog] jurnalul sidecar-ului (implicit <dataDir>/aere-audit.log)
* @param {string} [o.kmsKey] numele cheii KMS de tip encrypt (implicit "secrets"; creata daca lipseste)
* @param {function} [o.now] ceasul serviciului (pentru probe)
* @param {string} [o.staleLock] 'refuse' (implicit) sau 'take': ce face cu un service.lock al unui proces care nu mai ruleaza
* @param {number} [o.noncePerOrigin] nonce-uri in zbor pe o adresa de origine (implicit 32)
*/
static async open({ dataDir, kms, audience, adminPolicy, trustDir = null, auditLog = null, kmsKey = 'secrets', now = () => new Date(), nonceTtlS = NONCE_TTL_S, maxAgeS = 300, staleLock = 'refuse', noncePerOrigin = NONCE_PER_ORIGIN } = {}) {
const s = new Secrets();
if (typeof dataDir !== 'string' || dataDir === '') throw new SecretsError('DATA_DIR_MISSING', 'dataDir is required');
if (!kms || typeof kms.datakey !== 'function' || typeof kms.decrypt !== 'function') throw new SecretsError('KMS_MISSING', 'a KMS (openKms(...) or kmsClientHttp(...)) is required');
s.#audience = checkAudience(audience);
if (!NAME_RE.test(String(kmsKey))) throw new SecretsError('KMS_KEY_NAME_INVALID', 'kmsKey must match ^[a-z0-9][a-z0-9_-]{0,63}$');
if (typeof now !== 'function') throw new SecretsError('CLOCK_INVALID', 'now must be a function returning a Date');
if (!Number.isInteger(nonceTtlS) || nonceTtlS < 5 || nonceTtlS > 3600) throw new SecretsError('NONCE_TTL_INVALID', 'nonceTtlS is 5..3600 seconds');
if (!Number.isInteger(maxAgeS) || maxAgeS < 1 || maxAgeS > 3600) throw new SecretsError('MAX_AGE_INVALID', 'maxAgeS is 1..3600 seconds');
if (staleLock !== 'refuse' && staleLock !== 'take') throw new SecretsError('STALE_LOCK_INVALID', 'staleLock is "refuse" (default) or "take"');
if (!Number.isInteger(noncePerOrigin) || noncePerOrigin < 1 || noncePerOrigin > NONCE_MAX) throw new SecretsError('NONCE_PER_ORIGIN_INVALID', `noncePerOrigin is 1..${NONCE_MAX}`);
let admin;
try { admin = CF.definePolicy(adminPolicy).policy; } catch (e) { throw new SecretsError('ADMIN_POLICY_INVALID', 'adminPolicy: ' + String(e && e.message || e)); }
if (adminPolicy.requireStatus === false) throw new SecretsError('ADMIN_POLICY_INVALID', 'adminPolicy cannot waive the credential status (requireStatus: false)');
s.#admin = admin; s.#kms = kms; s.#kmsKey = String(kmsKey); s.#acum = now; s.#nonceTtlMs = nonceTtlS * 1000; s.#maxAgeS = maxAgeS; s.#noncePerOrigin = noncePerOrigin;
s.#dir = path.resolve(dataDir); s.#secretsDir = path.join(s.#dir, 'secrets');
s.#jurnal = auditLog ? path.resolve(auditLog) : path.join(s.#dir, 'aere-audit.log');
s.#trust = trustDir ? path.resolve(trustDir) : null;
fs.mkdirSync(s.#secretsDir, { recursive: true, mode: 0o700 });
s.#iaLacatulDosarului(staleLock);
try { await s.#deschideServiciu(); } catch (e) { s.#elibereazaLacatulDosarului(); throw e; }
return s;
}
get audience() { return this.#audience; }
get dataDir() { return this.#dir; }
get auditLog() { return this.#jurnal; }
/** daca deschiderea a preluat lacatul unui proces oprit (staleLock: 'take'): { pid, since }; altfel null */
get tookStaleLock() { return this.#lacatPreluat; }
close() {
if (this.#closed) return;
this.#closed = true; if (this.#macKey) this.#macKey.fill(0); this.#nonces.clear(); this.#peOrigine.clear();
this.#elibereazaLacatulDosarului();
}
// ------------------------------------------------------------------- lacatul dosarului: un singur serviciu pe un dosar de date
// Doua instante pe acelasi dosar (doua procese, sau doua deschideri in acelasi proces) isi pierd una alteia actualizarile: fiecare
// are mutexul ei. Lacatul e un fisier creat EXCLUSIV, cu PID-ul si gazda; o a doua deschidere refuza pe nume. Un lacat al unui proces
// care nu mai ruleaza pe gazda asta se recunoaste dupa PID si se SPUNE (DATA_DIR_LOCK_STALE), nu se fura in tacere; se preia numai cu
// staleLock: 'take', si atunci tookStaleLock il numeste.
#iaLacatulDosarului(staleLock) {
const p = path.join(this.#dir, 'service.lock');
for (let incercare = 0; incercare < 2; incercare++) {
let fd;
try { fd = fs.openSync(p, 'wx', 0o600); } catch (e) {
if (e.code !== 'EEXIST') throw new SecretsError('DATA_DIR_LOCK_FAILED', 'the lock of the data directory (service.lock) could not be created (' + (e.code || 'error') + ')');
let rec = null; try { rec = JSON.parse(fs.readFileSync(p, 'utf8')); } catch { rec = null; }
const pid = rec && Number.isSafeInteger(rec.pid) && rec.pid > 0 ? rec.pid : null;
const gazda = rec && typeof rec.host === 'string' ? rec.host : null;
if (!(pid !== null && gazda === os.hostname() && !traieste(pid))) {
throw new SecretsError('DATA_DIR_LOCKED', `the data directory is held by process ${pid ?? '?'} on host ${gazda === os.hostname() ? 'this host' : 'another host or an unreadable lock'} (service.lock): two instances on one data directory lose each other's updates; refusing to open`);
}
if (staleLock !== 'take') throw new SecretsError('DATA_DIR_LOCK_STALE', `service.lock names process ${pid}, which is not running on this host: an instance stopped without closing. Check that no instance runs on this directory, then remove service.lock or open with staleLock: 'take' (AERE_SECRETS_STALE_LOCK=take)`);
this.#lacatPreluat = { pid, since: typeof rec.since === 'string' ? rec.since : null };
fs.rmSync(p, { force: true });
continue;
}
try { fs.writeSync(fd, JSON.stringify({ pid: process.pid, host: os.hostname(), since: iso(Date.now()) }) + '\n'); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
this.#lacatDosar = p;
return;
}
throw new SecretsError('DATA_DIR_LOCKED', 'another instance took the lock of the data directory while this one was taking over a stale lock; refusing to open');
}
#elibereazaLacatulDosarului() {
const p = this.#lacatDosar; this.#lacatDosar = null;
if (!p) return;
try { const rec = JSON.parse(fs.readFileSync(p, 'utf8')); if (rec && rec.pid === process.pid && rec.host === os.hostname()) fs.rmSync(p, { force: true }); } catch { /* nu mai e al nostru, sau a disparut */ }
}
// ------------------------------------------------------------------- mutexul unui secret: incarca -> judeca -> muta -> audit -> salveaza
// Cu un KMS la distanta intre citirea fisierului si scrierea lui sta un await real; fara mutex, doua put-uri citeau aceeasi stare si al
// doilea il stergea pe primul (8 put-uri simultane intorceau toate versiunea 2). Mutexul tine tot ciclul unui secret, nu doar scrierea.
async #cuLacat(cheie, fn) {
const inainte = this.#lacate.get(cheie) || Promise.resolve();
let elibereaza; const randulMeu = new Promise((r) => { elibereaza = r; });
const coada = inainte.then(() => randulMeu);
this.#lacate.set(cheie, coada);
await inainte;
try { return await fn(); } finally { elibereaza(); if (this.#lacate.get(cheie) === coada) this.#lacate.delete(cheie); }
}
// ------------------------------------------------------------------- fisierul de serviciu: cheia KMS si cheia de MAC a fisierelor
async #kmsCall(f) {
try { return await f(); } catch (e) {
if (e instanceof SecretsError) throw e;
if (e instanceof KmsError) throw new SecretsError('KMS_' + e.code, 'the KMS refused: ' + e.code, { rule: 'kms' });
throw e;
}
}
async #deschideServiciu() {
const p = path.join(this.#dir, 'service.json');
// cheia KMS de tip encrypt: creata daca lipseste; una existenta de alt tip e refuzata la prima folosire (WRONG_KEY_TYPE)
let descr = null;
try { descr = await this.#kmsCall(() => this.#kms.getKey(this.#kmsKey)); } catch (e) { if (e.code !== 'KMS_KEY_NOT_FOUND') throw e; }
if (!descr) descr = await this.#kmsCall(() => this.#kms.createKey(this.#kmsKey, { type: 'encrypt' }));
if (descr.type !== 'encrypt') throw new SecretsError('KMS_WRONG_KEY_TYPE', `KMS key "${this.#kmsKey}" is a ${descr.type} key; the secrets service needs an encrypt key`);
if (fs.existsSync(p)) {
let rec;
try { rec = JSON.parse(fs.readFileSync(p, 'utf8')); } catch { throw new SecretsError('SERVICE_FILE_UNREADABLE', 'service.json is not valid JSON; refusing to start'); }
if (!rec || rec.format !== FORMAT_SERVICE || typeof rec.mac_key_wrapped !== 'string') throw new SecretsError('SERVICE_FILE_UNREADABLE', 'service.json is not an aeresecrets-service/1 file; refusing to start');
if (rec.audience !== this.#audience) throw new SecretsError('AUDIENCE_MISMATCH', 'this data directory was created for another audience; refusing to start');
if (rec.kms_key !== this.#kmsKey) throw new SecretsError('KMS_KEY_MISMATCH', 'this data directory was created with another KMS key name; refusing to start');
const { plaintext } = await this.#kmsCall(() => this.#kms.decrypt(this.#kmsKey, rec.mac_key_wrapped, utf8(L.mac + '\0' + this.#audience)));
if (plaintext.length !== 32) throw new SecretsError('SERVICE_FILE_UNREADABLE', 'the service MAC key has an unexpected size; refusing to start');
this.#macKey = plaintext;
return;
}
const dk = await this.#kmsCall(() => this.#kms.datakey(this.#kmsKey, { aad: utf8(L.mac + '\0' + this.#audience), bits: 256, includePlaintext: true }));
this.#macKey = Buffer.from(dk.plaintext, 'base64');
writeFileAtomic(p, JSON.stringify({ format: FORMAT_SERVICE, audience: this.#audience, kms_key: this.#kmsKey, kms_version: dk.version, created: iso(this.#acum()), mac_key_wrapped: dk.ciphertext }, null, 2) + '\n');
}
// ------------------------------------------------------------------- fisierele secretelor
#cale(name) { return path.join(this.#secretsDir, `${name}.json`); }
#mac(obj) { return hmac(this.#macKey, utf8(canonicalJson(obj))).toString('hex'); }
#incarca(name) {
const p = this.#cale(name);
if (!fs.existsSync(p)) return null;
let rec;
try { rec = JSON.parse(fs.readFileSync(p, 'utf8')); } catch { throw refuz('storage', 'SECRET_FILE_TAMPERED', `the file of secret "${name}" is not valid JSON`); }
const { mac, ...rest } = rec || {};
if (!(typeof mac === 'string' && ctEq(utf8(mac), utf8(this.#mac(rest))))) throw refuz('storage', 'SECRET_FILE_TAMPERED', `the file of secret "${name}" failed its integrity check`);
if (rest.name !== name || rest.format !== FORMAT_OBIECT) throw refuz('storage', 'SECRET_FILE_TAMPERED', `the file of secret "${name}" belongs to another secret or format`);
return rest;
}
#salveaza(sec) {
sec.updated = iso(this.#acum());
writeFileAtomic(this.#cale(sec.name), JSON.stringify({ ...sec, mac: this.#mac(sec) }, null, 2) + '\n');
}
#nume() { return fs.readdirSync(this.#secretsDir).filter((f) => f.endsWith('.json')).map((f) => f.slice(0, -5)).filter((n) => NAME_RE.test(n) && !NUME_REZERVATE.test(n)).sort(); }
// ------------------------------------------------------------------- cifrarea unei versiuni: cheie de date per versiune, invelita de KMS
async #versiuneNoua(sec, valoare, metadata, holder) {
const ver = sec.current_version + 1;
const aadDek = utf8(`${L.dek}\0${sec.name}\0${ver}`);
const dk = await this.#kmsCall(() => this.#kms.datakey(this.#kmsKey, { aad: aadDek, bits: 256, includePlaintext: true }));
const dek = Buffer.from(String(dk.plaintext), 'base64');
if (dek.length !== 32) throw new SecretsError('KMS_DATAKEY_INVALID', 'the KMS returned a data key of an unexpected size', { rule: 'kms' });
const iv = crypto.randomBytes(12);
const c = crypto.createCipheriv('aes-256-gcm', dek, iv);
c.setAAD(utf8(`${L.value}\0${sec.name}\0${ver}`));
const ct = Buffer.concat([c.update(valoare), c.final()]);
const sealed = Buffer.concat([iv, ct, c.getAuthTag()]).toString('base64');
dek.fill(0); valoare.fill(0);
sec.versions[String(ver)] = { created: iso(this.#acum()), state: 'enabled', revoked_at: null, metadata, created_by: holder, kms_version: dk.version, dek_wrapped: dk.ciphertext, sealed };
sec.current_version = ver;
return ver;
}
async #deschide(sec, ver) {
const v = sec.versions[String(ver)];
const { plaintext: dek } = await this.#kmsCall(() => this.#kms.decrypt(this.#kmsKey, v.dek_wrapped, utf8(`${L.dek}\0${sec.name}\0${ver}`)));
try {
const b = Buffer.from(v.sealed, 'base64');
const d = crypto.createDecipheriv('aes-256-gcm', dek, b.subarray(0, 12));
d.setAAD(utf8(`${L.value}\0${sec.name}\0${ver}`));
d.setAuthTag(b.subarray(b.length - 16));
return Buffer.concat([d.update(b.subarray(12, b.length - 16)), d.final()]);
} catch {
throw refuz('storage', 'SECRET_FILE_TAMPERED', `version ${ver} of secret "${sec.name}" could not be opened (modified file)`);
} finally { dek.fill(0); }
}
// ------------------------------------------------------------------- nonce: emis de serviciu, o singura folosire, cu termen
#uitaNonce(n) {
const rec = this.#nonces.get(n); this.#nonces.delete(n);
if (rec && rec.origin !== null) { const set = this.#peOrigine.get(rec.origin); if (set) { set.delete(n); if (!set.size) this.#peOrigine.delete(rec.origin); } }
return rec;
}
/**
* Un nonce proaspat pentru o prezentare catre acest serviciu. `origin` (optional, serverul HTTP da adresa de la care vine cererea):
* cel mult noncePerOrigin nonce-uri in zbor pe o origine (NONCE_ORIGIN_FULL), ca o singura adresa sa nu umple magazia pentru toti.
*/
nonce({ origin = null } = {}) {
if (this.#closed) throw new SecretsError('SERVICE_CLOSED', 'this service instance was closed');
if (origin !== null && (typeof origin !== 'string' || origin.length > 128)) throw new SecretsError('NONCE_ORIGIN_INVALID', 'origin is a string of at most 128 characters (the address of the caller)');
const acum = this.#acum().getTime();
if (this.#nonces.size >= NONCE_MAX / 2) for (const [n, rec] of this.#nonces) if (rec.exp <= acum) this.#uitaNonce(n);
if (origin !== null) {
const set = this.#peOrigine.get(origin);
if (set && set.size >= this.#noncePerOrigin) for (const n of [...set]) { const rec = this.#nonces.get(n); if (!rec || rec.exp <= acum) this.#uitaNonce(n); }
const inZbor = this.#peOrigine.has(origin) ? this.#peOrigine.get(origin).size : 0;
if (!(inZbor < this.#noncePerOrigin /*paznicul nonce-urilor pe origine*/)) throw new SecretsError('NONCE_ORIGIN_FULL', `this address has ${inZbor} nonces outstanding, the most one address may have; present one or let it expire (${this.#nonceTtlMs / 1000} s)`);
}
if (this.#nonces.size >= NONCE_MAX) throw new SecretsError('NONCE_STORE_FULL', `too many outstanding nonces (${NONCE_MAX}); try again shortly`);
const n = crypto.randomBytes(32).toString('base64url');
const exp = acum + this.#nonceTtlMs;
this.#nonces.set(n, { exp, origin });
if (origin !== null) { if (!this.#peOrigine.has(origin)) this.#peOrigine.set(origin, new Set()); this.#peOrigine.get(origin).add(n); }
return { nonce: n, audience: this.#audience, expiresAt: iso(exp) };
}
// Intoarce null daca nonce-ul e al serviciului, neexpirat si nefolosit; altfel motivul. Nonce-ul e CONSUMAT la prima vedere, oricare
// ar fi verdictul de dupa (o a doua cerere cu aceeasi prezentare e refuzata chiar daca prima a fost refuzata).
#judecaNonce(n, acum) {
const rec = this.#nonces.has(n) ? this.#nonces.get(n) : null;
const exp = rec ? rec.exp : 0;
this.#uitaNonce(n);
if (!(exp > acum /*PAZA:nonce*/)) return exp === 0 ? 'not issued by this service, or already used' : 'expired';
return null;
}
// ------------------------------------------------------------------- dosarul de incredere: listele de stare si revocarile
#incredere() {
const out = { statusLists: [], revocations: [], ignored: 0 };
if (!this.#trust) return out;
for (const [sub, cheie] of [['status-lists', 'statusLists'], ['revocations', 'revocations']]) {
const d = path.join(this.#trust, sub);
if (!fs.existsSync(d)) continue;
for (const f of fs.readdirSync(d).filter((x) => x.endsWith('.json')).sort()) {
try { out[cheie].push(JSON.parse(fs.readFileSync(path.join(d, f), 'utf8'))); } catch { out.ignored++; }
}
}
return out;
}
// ------------------------------------------------------------------- autorizarea: identitatea (o data), apoi politica fiecarui grant
/** contextul judecatii unei cereri: acelasi obiect pentru toate grant-urile (si toate secretele, la list), ca verificarea sa se faca o data */
#context(p, acum, rh) {
const incredere = this.#incredere();
return { audience: this.#audience, nonce: p.binding.nonce, now: acum, statusLists: incredere.statusLists, revocations: incredere.revocations, request: rh };
}
#judeca(op, secret, p, ctx, cache, { oricare = false } = {}) {
// 1. prezentarea, pe drumul de productie al identitatii, cu digestul cererii: fiecare refuz isi numeste regula (audience, nonce,
// request, freshness, presenter, credential-revoked, delegation-revoked, ...); emitentii se judeca pe grant, mai jos
let v = cache.get(V_IDENTITATE);
if (!v) { v = ID.verifyPresentation(p, { ...ctx, trustedIssuers: null, maxAgeS: this.#maxAgeS }); cache.set(V_IDENTITATE, v); }
if (!v.valid) return { grant: null, identity: v, judged: [] };
// 2. grantul: politica de admin permite tot; grantul unui secret permite operatiile lui; politica fiecaruia (emitenti, stare, afirmatii)
// e judecata de checkCompliance, cu verificarea hibrida facuta o data pentru fiecare pereche (emitenti, varsta maxima): `cache`
const candidati = [{ id: 'admin', operations: null, versions: 'any', policy: this.#admin }, ...(secret ? secret.grants : [])];
const judged = [];
for (const g of candidati) {
if (!(oricare || g.operations === null || g.operations.includes(op) /*PAZA:authorization*/)) continue;
let r;
try { r = CF.checkCompliance(p, g.policy, { ...ctx, cache }); } catch (e) { judged.push({ grant: g.id, compliant: false, rules: ['policy-unreadable'] }); continue; }
const strain = Array.isArray(g.holders) && !g.holders.includes(r.holder);
const delegat = g.delegates === false && !!v.subject && v.subject.delegations > 0;
const reguli = [...r.rules, ...(strain ? ['holder'] : []), ...(delegat ? ['delegates'] : [])];
judged.push({ grant: g.id, compliant: r.compliant && !strain && !delegat, rules: reguli });
if (!(r.compliant && !strain && !delegat /*PAZA:authorization*/)) continue;
return { grant: g, result: r, identity: v, judged };
}
return { grant: null, identity: v, judged };
}
// Cine a prezentat, in randul de audit: un id se scrie ca `holder` / `presenter` numai daca semnaturile prezentarii au fost VERIFICATE
// (id-urile derivate din chei, credentialul semnat, prezentarea semnata de cine trebuia, lantul de delegare semnat); altfel raman
// cele pretinse, in claimedHolder / claimedPresenter, si numai in forma aere-id.
#cineAPrezentat(a, v, p) {
const SEMNATURI = ['issuer-id', 'holder-id', 'credential-signature', 'binding-credential', 'binding-delegations', 'presenter', 'presentation-signature', 'delegation-form', 'delegation-ids', 'delegation-from', 'delegation-parent', 'delegation-signature'];
const randuri = (rule) => v.rows.filter((x) => x.rule === rule);
const tin = SEMNATURI.every((rule) => randuri(rule).every((x) => x.pass === true)) && ['holder-id', 'credential-signature', 'presenter', 'presentation-signature'].every((rule) => randuri(rule).some((x) => x.pass === true));
if (!(tin /*identitatile verificate*/)) return;
a.holder = idPretins(p.credential.statement.holder.id); a.presenter = idPretins(p.binding.presenter.id);
a.claimedHolder = null; a.claimedPresenter = null;
}
#refuzaAutorizarea(a, j, refuza) {
if (!j.identity.valid) {
// regulile identitatii nu spun nimic despre secret (identitatea se judeca inaintea oricarui grant): raman in raspuns
refuza(refuz('authorization', 'UNAUTHORIZED', 'the presentation is not valid for this service (identity rules: ' + j.identity.rules.join(', ') + ')', { identityRules: j.identity.rules }), j.identity.rules);
}
// identitate valida, niciun grant: ACELASI raspuns pentru orice secret; grant-urile judecate si regulile lor merg numai in jurnal
a.judged = j.judged.map((x) => ({ grant: x.grant, rules: [...new Set(x.rules)].sort() }));
refuza(refuz('authorization', 'UNAUTHORIZED', MESAJ_NEAUTORIZAT), [...new Set(j.judged.flatMap((x) => x.rules))].sort());
}
// ------------------------------------------------------------------- jurnalul: un plic secret-access pe operatie
#plic(a, acum) {
let prezentare = 'no-presentation';
// o legatura care nu se poate canoniza (o valoare pe care JSON nu o poate spune, data din biblioteca) nu arunca afara din refuz
if (a.binding) { try { prezentare = ID.canonical(a.binding); } catch { prezentare = 'unreadable-presentation'; } }
return PK.buildProof(KIND, {
service: this.#audience, secret: a.secret, version: a.version, operation: a.op, verdict: a.verdict, rules: a.rules, grant: a.grant,
holder: a.holder, presenter: a.presenter,
...(a.claimedHolder ? { claimedHolder: a.claimedHolder } : {}), ...(a.claimedPresenter ? { claimedPresenter: a.claimedPresenter } : {}),
...(a.judged && a.judged.length ? { judged: a.judged } : {}),
...(a.requestHash ? { requestHash: a.requestHash } : {}),
presentation: prezentare,
...(a.policyHash ? { policyHash: a.policyHash } : {}),
createdAt: iso(acum),
});
}
#scrieAudit(a, acum) {
let plic;
try { plic = this.#plic(a, acum); } catch (e) {
throw new SecretsError('AUDIT_WRITE_FAILED', 'the audit entry could not be built; the result of the operation is withheld', { rule: 'internal' });
}
if (!(Buffer.byteLength(JSON.stringify(plic)) <= MAX_AUDIT_ROW)) throw new SecretsError('AUDIT_WRITE_FAILED', `the audit entry is larger than ${MAX_AUDIT_ROW} bytes; the result of the operation is withheld`, { rule: 'internal' });
try {
const r = SC.adaugaPlicuri(this.#jurnal, [plic])[0];
// randul trebuie sa fie pe disc inainte de mutatia pe care o anunta (sidecar-ul adauga fara fsync)
const fd = fs.openSync(this.#jurnal, 'r+');
try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
return r;
} catch (e) {
throw new SecretsError('AUDIT_WRITE_FAILED', 'the audit log could not be written; the result of the operation is withheld', { rule: 'storage' });
}
}
/** Capul jurnalului: { ok, count, head } (verifica tot lantul; liniar in marimea jurnalului). */
auditHead() {
const intrari = fs.existsSync(this.#jurnal) ? fs.readFileSync(this.#jurnal, 'utf8').split('\n').filter((l) => l.trim()).map((l) => { try { return JSON.parse(l); } catch { return { necitibil: true }; } }) : [];
const v = SC.verificaJurnal(intrari);
return { ok: v.ok, count: v.count, head: v.head, broken: v.ok ? null : { seq: v.rupt, reason: v.motiv } };
}
// ------------------------------------------------------------------- inceputul oricarei operatii: marimea, prezentarea, cererea, nonce-ul
// Intoarce { a, p, refuza }: `a` e randul de audit in lucru, `refuza(e, reguli)` scrie randul de refuz si arunca e.
#inceput(op, name, body, acum) {
const a = { op, secret: op === 'list' ? '*' : name, version: null, verdict: 'refused', rules: [], grant: null, holder: null, presenter: null,
claimedHolder: null, claimedPresenter: null, binding: null, policyHash: null, requestHash: null, judged: null };
const refuza = (e, reguli = []) => { a.rules = [...new Set([e.rule, ...reguli])]; this.#scrieAudit(a, acum); throw e; };
// marimea, INAINTEA oricarei canonizari (o lista imbricata de 30.000 de ori arunca stiva in JSON-ul canonic): refuzat pe nume, auditat
const prea = preaComplex(body);
if (!(prea === null /*PAZA:request-form*/)) refuza(refuz('request-form', 'REQUEST_TOO_COMPLEX', `the request is deeper than ${MAX_JSON_DEPTH} levels or carries more than ${MAX_JSON_NODES} values`));
const p = body.presentation;
if (!(!!p && typeof p === 'object' && !!p.binding && typeof p.binding === 'object' && typeof p.binding.nonce === 'string' /*PAZA:presentation-form*/)) {
refuza(refuz('presentation-form', 'PRESENTATION_MISSING', 'the request carries no presentation of an AERE credential (presentation.binding.nonce is required)'));
}
a.binding = p.binding;
// pe un rand refuzat inainte de verificare, id-urile sunt cele PRETINSE, si numai in forma aere-id (un sir oarecare nu intra in jurnal)
try { a.claimedHolder = idPretins(p.credential.statement.holder.id); } catch { a.claimedHolder = null; }
try { a.claimedPresenter = idPretins(p.binding.presenter.id); } catch { a.claimedPresenter = null; }
try { a.requestHash = requestHash({ audience: this.#audience, operation: op, secret: op === 'list' ? null : name, body }); } catch {
refuza(refuz('request-form', 'INVALID_REQUEST', 'the request cannot be read as JSON (it carries a value JSON cannot say)'));
}
const motivNonce = this.#judecaNonce(p.binding.nonce, acum.getTime());
if (motivNonce) refuza(refuz('nonce', 'NONCE_INVALID', 'the presentation nonce is ' + motivNonce + ' (ask POST /v1/nonce for a fresh one and present it once)'));
return { a, p, refuza };
}
// ------------------------------------------------------------------- o operatie: forma, nonce, autorizare, lucrul, randul de audit
// `fn({ secret, grant, result, acum })` intoarce { value, version?, salveaza? } si poate arunca SecretsError (refuz numit); `salveaza`
// e secretul de pus pe disc. Ordinea pentru o mutatie: randul de audit (scris si fsync-at), APOI fisierul; o scriere a fisierului care
// cade dupa rand primeste un al doilea rand, `failed`, cu acelasi requestHash.
async #opereaza(op, name, body, { cereSecret = true } = {}, fn) {
if (this.#closed) throw new SecretsError('SERVICE_CLOSED', 'this service instance was closed');
// forma cererii si numele nu sunt operatii: refuzate fara intrare in jurnal si fara sa consume nonce-ul (spus in README)
if (!(!!body && typeof body === 'object' && !Array.isArray(body) /*PAZA:request-form*/)) throw refuz('request-form', 'INVALID_REQUEST', 'the request must be an object { presentation, ... }');
checkName(name);
return this.#cuLacat(name, async () => {
const acum = this.#acum();
const { a, p, refuza } = this.#inceput(op, name, body, acum);
// un fisier stricat (MAC-ul nu tine) se spune numai unui apelant autorizat de politica de admin, si se auditeaza; pana atunci
// secretul e tratat ca inexistent (candidatii raman numai politica de admin)
let secret = null, defect = null;
try { secret = this.#incarca(name); } catch (e) { defect = e; }
const j = this.#judeca(op, secret, p, this.#context(p, acum, a.requestHash), new Map());
this.#cineAPrezentat(a, j.identity, p);
if (!j.grant) this.#refuzaAutorizarea(a, j, refuza);
a.grant = j.grant.id; a.policyHash = j.result.policyHash; a.holder = j.result.holder;
if (defect) refuza(defect);
// existenta secretului se spune numai unui apelant autorizat (un strain nu afla daca un nume exista)
if (cereSecret && !secret) refuza(refuz('secret-exists', 'SECRET_NOT_FOUND', `secret "${name}" does not exist`));
let r;
try {
r = await fn({ secret, grant: j.grant, result: j.result, identity: j.identity, acum });
} catch (e0) {
let e = e0;
if (e instanceof KmsError) e = new SecretsError('KMS_' + e.code, 'the KMS refused: ' + e.code, { rule: 'kms' });
else if (!(e instanceof SecretsError)) { e = new SecretsError('INTERNAL', 'internal error', { rule: 'internal' }); Object.defineProperty(e, 'cause', { value: e0, enumerable: false }); }
if (!e.rule) e.rule = 'internal';
refuza(e);
}
a.verdict = 'allowed'; a.version = r.version ?? null;
this.#scrieAudit(a, acum); // AUDIT_WRITE_FAILED aici: nimic nu s-a schimbat pe disc
if (r.salveaza) {
try { this.#salveaza(r.salveaza); } catch (es) {
try { this.#scrieAudit({ ...a, verdict: 'failed', rules: ['storage'] }, acum); } catch { /* ramane randul `allowed` singur: o intrare fara schimbare */ }
throw refuz('storage', 'STORAGE_WRITE_FAILED', `the change to secret "${name}" could not be stored (${es && es.code || 'error'}); its entry is followed by a failed entry`);
}
}
return r.value;
});
}
// ------------------------------------------------------------------- operatiile
/** Pune o versiune noua (sau creeaza secretul, cu grants, daca nu exista). Intoarce { name, version, createdAt }. */
put(name, body) {
return this.#opereaza('put', name, body, { cereSecret: false }, async ({ secret, result, acum }) => {
const valoare = toValue(body.value);
const metadata = checkMetadata(body.metadata);
let sec = secret;
if (!sec) {
const grants = body.grants === undefined ? [] : body.grants;
if (!Array.isArray(grants)) throw refuz('grant-form', 'INVALID_GRANT', 'grants must be a list of grants');
if (!(grants.length <= MAX_GRANTS /*PAZA:grant-limit*/)) throw refuz('grant-limit', 'TOO_MANY_GRANTS', `a secret has at most ${MAX_GRANTS} grants`);
const norm = grants.map(normalizeGrant);
if (new Set(norm.map((g) => g.id)).size !== norm.length) throw refuz('grant-exists', 'GRANT_EXISTS', 'two grants carry the same id');
sec = { format: FORMAT_OBIECT, name, created: iso(acum), updated: iso(acum), kms_key: this.#kmsKey, current_version: 0, grants: norm.map((g) => ({ ...g, createdBy: result.holder })), versions: {} };
} else if (body.grants !== undefined) {
throw refuz('request-form', 'INVALID_REQUEST', 'grants are set when a secret is created; use grant / revoke on an existing secret');
}
const ver = await this.#versiuneNoua(sec, valoare, metadata, result.holder);
return { value: { name, version: ver, current: ver, createdAt: sec.versions[String(ver)].created }, version: ver, salveaza: sec };
});
}
/** Citeste o versiune (implicit cea curenta). Intoarce { name, version, current, value: Buffer, metadata, createdAt }. */
get(name, body) {
return this.#opereaza('get', name, body, {}, async ({ secret, grant }) => {
const ceruta = body.version === undefined || body.version === null ? secret.current_version : body.version;
if (!(Number.isSafeInteger(ceruta) && Object.hasOwn(secret.versions, String(ceruta)) /*PAZA:version-exists*/)) throw refuz('version-exists', 'VERSION_NOT_FOUND', `secret "${name}" has no version ${versiuneAfisata(ceruta)}`);
if (!(ceruta === secret.current_version || grant.versions === 'any' /*PAZA:version-scope*/)) throw refuz('version-scope', 'VERSION_NOT_IN_SCOPE', `grant "${grant.id}" reads only the current version of "${name}" (versions: any is required for version ${ceruta})`);
const v = secret.versions[String(ceruta)];
if (!(v.state !== 'revoked' /*PAZA:version-revoked*/)) throw refuz('version-revoked', 'VERSION_REVOKED', `version ${ceruta} of secret "${name}" was revoked at ${v.revoked_at}`);
const value = await this.#deschide(secret, ceruta);
return { value: { name, version: ceruta, current: secret.current_version, value, metadata: { ...v.metadata }, createdAt: v.created }, version: ceruta };
});
}
/** Metadatele secretului: versiunile cu starea lor si grants (lista de control intreaga), fara nicio valoare si fara material sigilat. */
metadata(name, body) {
return this.#opereaza('metadata', name, body, {}, async ({ secret }) => {
const versions = {};
for (const [k, v] of Object.entries(secret.versions)) versions[k] = { created: v.created, state: v.state, revokedAt: v.revoked_at, metadata: { ...v.metadata }, createdBy: v.created_by, kmsVersion: v.kms_version };
return { value: { name, created: secret.created, updated: secret.updated, current: secret.current_version, kmsKey: secret.kms_key, versions, grants: secret.grants.map((g) => JSON.parse(JSON.stringify(g))) }, version: secret.current_version };
});
}
/** O versiune noua cu o valoare NASCUTA aici (bytes 16..64 aleatori, base64url sau hex); valoarea nu se intoarce: se citeste cu get. */
rotate(name, body) {
return this.#opereaza('rotate', name, body, {}, async ({ secret, result }) => {
const bytes = body.bytes === undefined ? 32 : body.bytes;
const encoding = body.encoding === undefined ? 'base64url' : body.encoding;
if (!Number.isInteger(bytes) || bytes < 16 || bytes > 64) throw refuz('request-form', 'INVALID_REQUEST', 'bytes is 16..64');
if (encoding !== 'base64url' && encoding !== 'hex') throw refuz('request-form', 'INVALID_REQUEST', 'encoding is base64url or hex');
const metadata = checkMetadata(body.metadata);
const valoare = utf8(crypto.randomBytes(bytes).toString(encoding));
const ver = await this.#versiuneNoua(secret, valoare, { ...metadata, rotated: true }, result.holder);
return { value: { name, version: ver, current: ver, createdAt: secret.versions[String(ver)].created }, version: ver, salveaza: secret };
});
}
/** Revoca o versiune: nu se mai citeste niciodata prin acest serviciu (ireversibil); textul cifrat si cheia de date dispar din fisier. */
revokeVersion(name, body) {
return this.#opereaza('revoke-version', name, body, {}, async ({ secret, acum }) => {
const ver = body.version;
if (!(Number.isSafeInteger(ver) && Object.hasOwn(secret.versions, String(ver)) /*PAZA:version-exists*/)) throw refuz('version-exists', 'VERSION_NOT_FOUND', `secret "${name}" has no version ${versiuneAfisata(ver)}`);
const v = secret.versions[String(ver)];
if (v.state === 'revoked') throw refuz('version-revoked', 'VERSION_REVOKED', `version ${ver} of secret "${name}" was already revoked at ${v.revoked_at}`);
v.state = 'revoked'; v.revoked_at = iso(acum);
v.sealed = null; v.dek_wrapped = null; // nici textul cifrat, nici cheia de date invelita nu mai stau pe disc: nimic de deschis
return { value: { name, version: ver, state: 'revoked', revokedAt: v.revoked_at }, version: ver, salveaza: secret };
});
}
/** Adauga un grant (id unic, cel mult MAX_GRANTS pe secret; facut de un ne-admin, o atenuare a grantului lui). */
grant(name, body) {
return this.#opereaza('grant', name, body, {}, async ({ secret, grant: origine, result }) => {
const g = normalizeGrant(body.grant);
if (!(!secret.grants.some((x) => x.id === g.id) /*PAZA:grant-exists*/)) throw refuz('grant-exists', 'GRANT_EXISTS', `secret "${name}" already has a grant "${g.id}"`);
if (!(secret.grants.length < MAX_GRANTS /*PAZA:grant-limit*/)) throw refuz('grant-limit', 'TOO_MANY_GRANTS', `secret "${name}" has ${MAX_GRANTS} grants, the most a secret may have`);
if (origine.id !== 'admin') atenuare(g, origine);
secret.grants.push({ ...g, createdBy: result.holder });
return { value: { name, grant: g.id, grants: secret.grants.map((x) => x.id) }, version: secret.current_version, salveaza: secret };
});
}
/** Retrage un grant dupa id (un ne-admin: numai un grant facut chiar de el). */
revoke(name, body) {
return this.#opereaza('revoke', name, body, {}, async ({ secret, grant: origine, result }) => {
const id = body.grantId;
if (typeof id !== 'string' || !GRANT_ID_RE.test(id)) throw refuz('request-form', 'INVALID_REQUEST', 'grantId is required');
const tinta = secret.grants.find((x) => x.id === id);
if (!(!!tinta /*PAZA:grant-exists*/)) throw refuz('grant-exists', 'GRANT_NOT_FOUND', `secret "${name}" has no grant "${id}"`);
if (!(origine.id === 'admin' || tinta.createdBy === result.holder /*PAZA:grant-scope*/)) throw refuz('grant-scope', 'GRANT_NOT_OWNED', `grant "${id}" may be revoked by the admin, or by the holder who made it`);
secret.grants = secret.grants.filter((x) => x.id !== id);
return { value: { name, revoked: id, grants: secret.grants.map((x) => x.id) }, version: secret.current_version, salveaza: secret };
});
}
/** Numele secretelor pe care apelantul le poate atinge: toate pentru admin, altfel cele cu un grant care il admite (orice operatie). */
list(body) {
if (this.#closed) throw new SecretsError('SERVICE_CLOSED', 'this service instance was closed');
if (!(!!body && typeof body === 'object' && !Array.isArray(body) /*PAZA:request-form*/)) throw refuz('request-form', 'INVALID_REQUEST', 'the request must be an object { presentation }');
const acum = this.#acum();
const { a, p, refuza } = this.#inceput('list', null, body, acum);
// identitatea o data (si politica fiecarei perechi de emitenti o data, pentru toate secretele); admin vede tot; altfel fiecare
// secret cu un grant (orice operatie) care il admite
const ctx = this.#context(p, acum, a.requestHash); const cache = new Map();
const j = this.#judeca('list', null, p, ctx, cache, { oricare: true });
this.#cineAPrezentat(a, j.identity, p);
if (!j.identity.valid) this.#refuzaAutorizarea(a, j, refuza);
const nume = this.#nume();
let secrets;
if (j.grant) { secrets = nume; a.grant = 'admin'; a.policyHash = j.result.policyHash; a.holder = j.result.holder; } else {
secrets = [];
for (const n of nume) {
let sec; try { sec = this.#incarca(n); } catch { continue; } // un fisier stricat nu apare in lista nimanui; adminul il afla la metadata
const r = this.#judeca('list', sec, p, ctx, cache, { oricare: true });
if (r.grant && r.grant.id !== 'admin') { secrets.push(n); a.holder = r.result.holder; }
}
if (!secrets.length) refuza(refuz('authorization', 'UNAUTHORIZED', 'no secret has a grant that admits this presentation'));
a.grant = 'grants';
}
a.verdict = 'allowed';
this.#scrieAudit(a, acum);
return { secrets };
}
}

226
pq-secrets/server.mjs Normal file
View File

@ -0,0 +1,226 @@
// server.mjs - serverul HTTP al Aere PQ Secrets (node:http, fara dependinte). Toate operatiile sunt POST cu JSON, fiindca fiecare
// cerere poarta o PREZENTARE de credential AERE (zeci de KB cu semnaturile hibride), care nu incape intr-un antet.
//
// Mediu:
// AERE_SECRETS_AUDIENCE obligatoriu: numele sub care se fac prezentarile pentru acest serviciu (ex. https://secrets.example)
// AERE_SECRETS_ADMIN_POLICY obligatoriu: fisier JSON cu politica de conformitate a adminului (id, trustedIssuers, require)
// AERE_SECRETS_DATA_DIR implicit ./data langa acest fisier (service.json, secrets/, aere-audit.log)
// AERE_SECRETS_TRUST_DIR implicit ./trust langa acest fisier (status-lists/*.json, revocations/*.json, citite la fiecare cerere)
// AERE_SECRETS_AUDIT_LOG implicit <data>/aere-audit.log (jurnalul sidecar-ului)
// KMS-ul, exact unul din doua:
// AERE_KMS_ROOT_KEY [+ AERE_KMS_DATA_DIR, implicit <data>/kms] KMS-ul in acest proces (pq-kms/kms.mjs)
// AERE_KMS_URL + AERE_KMS_TOKEN serverul pq-kms, prin HTTP
// AERE_SECRETS_KMS_KEY implicit secrets (cheia KMS de tip encrypt; creata daca lipseste)
// AERE_SECRETS_HOST implicit 127.0.0.1
// AERE_SECRETS_PORT implicit 8430 (0 alege unul liber)
// AERE_SECRETS_MAX_BODY implicit 262144 (octeti), 65536..16777216
// AERE_SECRETS_NONCE_TTL implicit 120 (secunde)
// AERE_SECRETS_NONCE_PER_ORIGIN implicit 32: nonce-uri in zbor pe o adresa de origine (peste: 429 NONCE_ORIGIN_FULL)
// AERE_SECRETS_STALE_LOCK implicit refuse; take = preia service.lock-ul unui proces care nu mai ruleaza pe gazda (si o spune)
//
// Nicio valoare din mediu, niciun secret, niciun nonce si nicio cheie nu se tipareste. Raspunsurile de eroare sunt { error, rule, message, ... }.
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { openKms, KmsError } from '../pq-kms/kms.mjs';
import { openSecrets, kmsClientHttp, SecretsError, OPERATII_GRANT, NONCE_MAX } from './secrete.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
// codul HTTP se alege dupa REGULA refuzului (un cod dupa fiecare cod de eroare ar fi o a doua lista, care diverge)
const STATUS_REGULA = {
'request-form': 400, 'secret-name': 400, 'value-form': 400, 'grant-form': 400,
'presentation-form': 401, nonce: 401,
authorization: 403, 'version-scope': 403, 'grant-scope': 403,
'grant-limit': 409,
'secret-exists': 404, 'version-exists': 404,
'version-revoked': 410,
kms: 502, storage: 500, internal: 500,
};
const STATUS_COD = { GRANT_EXISTS: 409, GRANT_NOT_FOUND: 404, AUDIT_WRITE_FAILED: 500, SERVICE_CLOSED: 503, NONCE_STORE_FULL: 503, NONCE_ORIGIN_FULL: 429, SECRET_FILE_TAMPERED: 500 };
class HttpError extends Error {
constructor(status, code, message) { super(message); this.status = status; this.code = code; }
}
export function configFromEnv(env) {
const s = (k, d = '') => String(env[k] ?? '').trim() || d;
const audience = s('AERE_SECRETS_AUDIENCE');
if (audience === '') throw new SecretsError('AUDIENCE_MISSING', 'AERE_SECRETS_AUDIENCE is not set; refusing to start (presentations must name the service they are made for)');
const adminFile = s('AERE_SECRETS_ADMIN_POLICY');
if (adminFile === '') throw new SecretsError('ADMIN_POLICY_MISSING', 'AERE_SECRETS_ADMIN_POLICY is not set; refusing to start (a service with no admin policy cannot create a secret)');
let adminPolicy;
try { adminPolicy = JSON.parse(fs.readFileSync(adminFile, 'utf8')); } catch { throw new SecretsError('ADMIN_POLICY_UNREADABLE', 'AERE_SECRETS_ADMIN_POLICY does not name a readable JSON file'); }
const dataDir = s('AERE_SECRETS_DATA_DIR', path.join(HERE, 'data'));
const trustDir = s('AERE_SECRETS_TRUST_DIR', path.join(HERE, 'trust'));
const auditLog = s('AERE_SECRETS_AUDIT_LOG', path.join(dataDir, 'aere-audit.log'));
const inProces = s('AERE_KMS_ROOT_KEY') !== '', laDistanta = s('AERE_KMS_URL') !== '';
if (inProces && laDistanta) throw new SecretsError('KMS_AMBIGUOUS', 'set AERE_KMS_ROOT_KEY (KMS in this process) or AERE_KMS_URL (remote KMS), not both');
if (!inProces && !laDistanta) throw new SecretsError('KMS_MISSING', 'set AERE_KMS_ROOT_KEY (KMS in this process) or AERE_KMS_URL + AERE_KMS_TOKEN (remote KMS); refusing to start');
const kms = inProces ? { mode: 'in-process', rootKey: env.AERE_KMS_ROOT_KEY, dataDir: s('AERE_KMS_DATA_DIR', path.join(dataDir, 'kms')) } : { mode: 'http', url: s('AERE_KMS_URL'), token: s('AERE_KMS_TOKEN') };
const kmsKey = s('AERE_SECRETS_KMS_KEY', 'secrets');
const host = s('AERE_SECRETS_HOST', '127.0.0.1');
const port = Number(s('AERE_SECRETS_PORT', '8430'));
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new SecretsError('INVALID_PORT', 'AERE_SECRETS_PORT must be an integer between 0 and 65535');
const maxBody = Number(s('AERE_SECRETS_MAX_BODY', '262144'));
if (!Number.isInteger(maxBody) || maxBody < 65536 || maxBody > 16 * 1024 * 1024) throw new SecretsError('INVALID_MAX_BODY', 'AERE_SECRETS_MAX_BODY must be an integer between 65536 and 16777216');
const nonceTtlS = Number(s('AERE_SECRETS_NONCE_TTL', '120'));
if (!Number.isInteger(nonceTtlS) || nonceTtlS < 5 || nonceTtlS > 3600) throw new SecretsError('INVALID_NONCE_TTL', 'AERE_SECRETS_NONCE_TTL must be an integer between 5 and 3600');
const noncePerOrigin = Number(s('AERE_SECRETS_NONCE_PER_ORIGIN', '32'));
if (!Number.isInteger(noncePerOrigin) || noncePerOrigin < 1 || noncePerOrigin > NONCE_MAX) throw new SecretsError('INVALID_NONCE_PER_ORIGIN', `AERE_SECRETS_NONCE_PER_ORIGIN must be an integer between 1 and ${NONCE_MAX}`);
const staleLock = s('AERE_SECRETS_STALE_LOCK', 'refuse');
if (staleLock !== 'refuse' && staleLock !== 'take') throw new SecretsError('INVALID_STALE_LOCK', 'AERE_SECRETS_STALE_LOCK is refuse (default) or take');
return { audience, adminPolicy, dataDir, trustDir, auditLog, kms, kmsKey, host, port, maxBody, nonceTtlS, noncePerOrigin, staleLock };
}
/** Deschide KMS-ul si serviciul din configuratie (folosit de main si de probe). */
export async function openFromConfig(cfg) {
const kms = cfg.kms.mode === 'in-process' ? openKms({ dataDir: cfg.kms.dataDir, rootKey: cfg.kms.rootKey }) : kmsClientHttp({ url: cfg.kms.url, token: cfg.kms.token });
const secrets = await openSecrets({ dataDir: cfg.dataDir, kms, audience: cfg.audience, adminPolicy: cfg.adminPolicy, trustDir: cfg.trustDir, auditLog: cfg.auditLog, kmsKey: cfg.kmsKey, nonceTtlS: cfg.nonceTtlS,
...(cfg.noncePerOrigin ? { noncePerOrigin: cfg.noncePerOrigin } : {}), ...(cfg.staleLock ? { staleLock: cfg.staleLock } : {}) });
return { kms, secrets };
}
function discard(req, cap) {
let n = 0;
req.on('data', (c) => { n += c.length; if (n > cap) req.destroy(); });
req.on('error', () => {});
req.resume();
}
function readBody(req, maxBody) {
return new Promise((resolve, reject) => {
const declared = req.headers['content-length'];
if (declared !== undefined) {
const n = Number(declared);
if (!Number.isSafeInteger(n) || n < 0) return reject(new HttpError(400, 'INVALID_CONTENT_LENGTH', 'invalid Content-Length'));
if (n > maxBody) return reject(new HttpError(413, 'BODY_TOO_LARGE', `request body is larger than ${maxBody} bytes`));
}
const chunks = []; let size = 0; let done = false;
req.on('data', (c) => {
if (done) return;
size += c.length;
if (size > maxBody) { done = true; chunks.length = 0; reject(new HttpError(413, 'BODY_TOO_LARGE', `request body is larger than ${maxBody} bytes`)); return; }
chunks.push(c);
});
req.on('end', () => { if (!done) { done = true; resolve(Buffer.concat(chunks)); } });
req.on('error', () => { if (!done) { done = true; reject(new HttpError(400, 'BAD_REQUEST', 'the request body could not be read')); } });
});
}
function send(res, status, obj, close = false) {
const body = JSON.stringify(obj);
const headers = { 'content-type': 'application/json; charset=utf-8', 'content-length': Buffer.byteLength(body), 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' };
if (close) headers.connection = 'close';
res.writeHead(status, headers);
res.end(body);
}
function b64Field(obj, field) {
const v = obj[field];
if (v === undefined || v === null) throw new HttpError(400, 'MISSING_FIELD', `field "${field}" is required`);
if (typeof v !== 'string' || !/^[A-Za-z0-9+/]*={0,2}$/.test(v) || v.length % 4 !== 0) throw new HttpError(400, 'INVALID_BASE64', `field "${field}" must be standard base64`);
const b = Buffer.from(v, 'base64');
if (b.toString('base64') !== v) throw new HttpError(400, 'INVALID_BASE64', `field "${field}" must be canonical base64`);
return b;
}
// un refuz poarta { error, rule, message } si, numai cand prezentarea insasi nu e valida, identityRules (regulile identitatii nu spun
// nimic despre secret); grant-urile judecate si regulile lor stau in jurnal, nu in raspuns
const refuzJson = (e) => {
const out = { error: e.code, rule: e.rule ?? null, message: e.message };
if (Array.isArray(e.identityRules) && e.identityRules.length) out.identityRules = e.identityRules;
return out;
};
export function createServer({ secrets, maxBody = 262144 }) {
const drainCap = maxBody * 4 + 1024 * 1024;
async function handle(req, res) {
let url;
try { url = new URL(req.url, 'http://localhost'); } catch { discard(req, drainCap); return send(res, 400, { error: 'BAD_REQUEST', message: 'invalid request URL' }, true); }
const method = req.method;
const parts = url.pathname.split('/').filter((p) => p !== '');
if (method === 'GET' && url.pathname === '/v1/health') { discard(req, drainCap); return send(res, 200, { ok: true, audience: secrets.audience }); }
if (method === 'GET' && url.pathname === '/v1/audit/head') { discard(req, drainCap); return send(res, 200, secrets.auditHead()); }
if (method !== 'POST') { discard(req, drainCap); throw new HttpError(405, 'METHOD_NOT_ALLOWED', 'this service takes POST requests with a JSON body (every request carries a presentation)'); }
let body;
{
const ctype = String(req.headers['content-type'] ?? '');
let raw;
try { raw = await readBody(req, maxBody); } catch (e) { discard(req, drainCap); throw e; }
if (raw.length > 0 && ctype !== '' && !/^application\/json\b/i.test(ctype)) throw new HttpError(415, 'UNSUPPORTED_MEDIA_TYPE', 'request body must be application/json');
if (raw.length === 0) body = {};
else { try { body = JSON.parse(raw.toString('utf8')); } catch { throw new HttpError(400, 'INVALID_JSON', 'request body is not valid JSON'); } }
if (!body || typeof body !== 'object' || Array.isArray(body)) throw new HttpError(400, 'INVALID_JSON', 'request body must be a JSON object');
}
if (parts[0] !== 'v1') throw new HttpError(404, 'NOT_FOUND', 'no such route');
// nonce-urile in zbor se numara pe adresa de la care vine cererea (in spatele unui proxy, adresa proxy-ului: spus in README)
if (parts.length === 2 && parts[1] === 'nonce') return send(res, 200, secrets.nonce({ origin: String(req.socket.remoteAddress || 'unknown') }));
if (parts.length === 2 && parts[1] === 'list') return send(res, 200, await secrets.list(body));
if (parts.length === 4 && parts[1] === 'secrets') {
const name = decodeURIComponent(parts[2]); const op = parts[3];
// corpul INTREG ajunge la biblioteca: digestul cererii (requestHash) acopera fiecare camp, deci un camp adaugat de cine nu a semnat
// prezentarea schimba digestul si cererea e refuzata (regula request a identitatii)
switch (op) {
case 'put': {
const value = b64Field(body, 'value');
const r = await secrets.put(name, { ...body, value });
value.fill(0);
return send(res, 200, r);
}
case 'get': {
const r = await secrets.get(name, body);
const out = { name: r.name, version: r.version, current: r.current, value: r.value.toString('base64'), metadata: r.metadata, createdAt: r.createdAt };
r.value.fill(0);
return send(res, 200, out);
}
case 'metadata': return send(res, 200, await secrets.metadata(name, body));
case 'rotate': return send(res, 200, await secrets.rotate(name, body));
case 'revoke-version': return send(res, 200, await secrets.revokeVersion(name, body));
case 'grant': return send(res, 200, await secrets.grant(name, body));
case 'revoke': return send(res, 200, await secrets.revoke(name, body));
default: throw new HttpError(404, 'NOT_FOUND', 'no such operation; one of: ' + OPERATII_GRANT.join(', '));
}
}
throw new HttpError(404, 'NOT_FOUND', 'no such route');
}
const server = http.createServer((req, res) => {
handle(req, res).catch((e) => {
if (res.headersSent) { res.destroy(); return; }
if (e instanceof HttpError) return send(res, e.status, { error: e.code, message: e.message }, e.status === 413);
if (e instanceof SecretsError) return send(res, STATUS_COD[e.code] ?? STATUS_REGULA[e.rule] ?? 400, refuzJson(e));
if (e instanceof KmsError) return send(res, 502, { error: 'KMS_' + e.code, rule: 'kms', message: 'the KMS refused: ' + e.code });
if (e instanceof URIError) return send(res, 400, { error: 'BAD_REQUEST', message: 'invalid percent-encoding in path' });
return send(res, 500, { error: 'INTERNAL', rule: 'internal', message: 'internal error' });
});
});
server.headersTimeout = 10_000;
server.requestTimeout = 30_000;
return server;
}
async function main() {
let cfg, deschis;
try {
cfg = configFromEnv(process.env);
deschis = await openFromConfig(cfg);
} catch (e) {
const code = e instanceof SecretsError || e instanceof KmsError ? e.code : 'INTERNAL';
const msg = e instanceof SecretsError || e instanceof KmsError ? e.message : 'internal error';
process.stderr.write(`aere-pq-secrets: refusing to start: ${code}: ${msg}\n`);
process.exitCode = 1;
return;
}
const server = createServer({ secrets: deschis.secrets, maxBody: cfg.maxBody });
server.on('error', (e) => { process.stderr.write(`aere-pq-secrets: server error: ${e.code ?? 'ERROR'}\n`); process.exitCode = 1; });
if (deschis.secrets.tookStaleLock) process.stderr.write(`aere-pq-secrets: took over the stale lock of process ${deschis.secrets.tookStaleLock.pid} (AERE_SECRETS_STALE_LOCK=take)\n`);
server.listen(cfg.port, cfg.host, () => {
const a = server.address();
process.stdout.write(`aere-pq-secrets: listening on ${a.address}:${a.port} for audience ${cfg.audience} (KMS ${cfg.kms.mode})\n`);
});
const stop = () => { server.close(() => { deschis.secrets.close(); if (deschis.kms && typeof deschis.kms.close === 'function') deschis.kms.close(); }); };
process.on('SIGINT', stop);
process.on('SIGTERM', stop);
}
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
main();
}

View File

@ -0,0 +1,292 @@
// test/control-negativ-secrete.mjs - controlul negativ al probei Aere PQ Secrets.
//
// O proba care nu poate iesi rosie nu masoara nimic. Aici se planteaza, intr-o COPIE a codului (serviciul, KMS-ul, identitatea,
// conformitatea, proof-kinds si sidecar-ul, in aceeasi asezare relativa), cate un defect real, si se cere ca proba sa iasa ROSIE pe exact
// probele numite, pentru MOTIVUL numit (un fragment din mesajul afirmatiei care a cazut), nu doar "ceva a picat".
//
// Regulile casei, platite in alte parti:
// - verdictul se citeste din JSON-ul probei (AERE_SECRETS_PROBA_JSON), nu din codul de iesire; o proba care nu a ajuns la capat sau a
// rulat alt numar de probe e STRICAT, nu ROSU (trei stari: VERDE, ROSU, STRICAT);
// - copia se reface pentru FIECARE plantare si se compara sha256 cu originalul inainte de a planta;
// - ancora unei plantari trebuie sa apara EXACT o data; o plantare care nu se poate pune e un ESEC al controlului, nu o trecere;
// - plantarile raman cod valid, ca rosul sa vina din proba, nu din incarcarea modulului; la sfarsit originalul e identic octet cu octet.
// 2026-10-18 (revizuirea adversariala B8): plantarile paznicilor noi (cererea, atenuarea, raspunsul generic, id-urile verificate,
// mutexul, lacatul dosarului, marimea cererii, numele si cheile rezervate, plafoanele, nonce-urile pe origine, ordinea la mutatie,
// delegatii, vocabularul felului) si patru lucratori in paralel, fiecare cu copia lui (o plantare = o rulare intreaga a probei).
//
// Iesire: 0 numai daca proba e VERDE pe copia neatinsa, fiecare plantare e ROSIE pe probele numite si originalul e neatins; altfel 1.
// --autoproba: controlul controlului, cu momeli care TREBUIE sa primeasca fiecare verdictul ei de esec.
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const RADACINA = path.resolve(HERE, '..', '..', '..'); // aerenew/
const S = 'cloud-gateway/pq-secrets/secrete.mjs', SV = 'cloud-gateway/pq-secrets/server.mjs', PR = 'cloud-gateway/pq-secrets/test/proba-secrete.mjs';
const IDN = 'tools/aere-identity/identity.mjs', CFM = 'tools/aere-identity/conformitate.mjs', PKS = 'tools/proof-kinds/proof-kinds.mjs';
const FISIERE = [S, SV, PR, 'cloud-gateway/pq-kms/kms.mjs', 'cloud-gateway/pq-kms/server.mjs', 'cloud-gateway/pq-kms/hsm-radacina.mjs', IDN, CFM, PKS, 'tools/aere-verify-layer/sidecar.mjs'];
const DEV_VERIFY = path.join(RADACINA, 'tools', 'aere-proof-protocol', 'verify.mjs');
const VERIFY = process.env.AERE_VERIFY_PROOF ? path.resolve(process.env.AERE_VERIFY_PROOF) : (fs.existsSync(DEV_VERIFY) ? DEV_VERIFY : '');
const NICIODATA = "process.env.AERE_SECRETS_PLANTA_NICIODATA === 'da'";
const LUCRATORI = 4;
// Fiecare plantare: fisierul, ancora (exact o aparitie), inlocuirea, si probele care TREBUIE sa iasa rosii: [inceputul numelui probei
// (unic), un fragment din motivul pe care trebuie sa cada].
const PLANTARI = [
{ id: 'P01', defect: 'nonce-ul nu se mai consuma la prima vedere (replay acceptat)', fisier: S,
ancora: ' const exp = rec ? rec.exp : 0;\n this.#uitaNonce(n);', inlocuire: ' const exp = rec ? rec.exp : 0;',
rosii: [['refuz: nonce refolosit', 'a doua oara aceeasi prezentare: trebuia refuzat cu NONCE_INVALID'], ['server: flux complet', 'replay 401']] },
{ id: 'P02', defect: 'nonce-ul nu se mai judeca (neemis si expirat acceptate)', fisier: S,
ancora: 'if (!(exp > acum /*PAZA:nonce*/))', inlocuire: 'if (!(exp > acum || true))',
rosii: [['refuz: nonce refolosit', 'trebuia refuzat cu NONCE_INVALID'], ['refuz: nonce expirat', 'la 121 s: trebuia refuzat']] },
{ id: 'P03', defect: 'audienta serviciului nu se mai cere (se accepta cea din prezentare)', fisier: S,
ancora: 'return { audience: this.#audience, nonce: p.binding.nonce,', inlocuire: 'return { audience: p.binding.audience, nonce: p.binding.nonce,',
// 2026-10-18: cu regula request, o prezentare facuta pentru ALT serviciu e refuzata si pe cerere (digestul ei numeste serviciul):
// plantarea se vede ca refuzul care nu mai e numit audience
rosii: [['refuz: prezentare facuta pentru alt serviciu', 'alt public: regula identitatii asteptata audience'], ['server: flux complet', 'regula audience in raspuns']] },
{ id: 'P04', defect: 'revocarile delegarilor din dosarul de incredere nu se mai citesc', fisier: S,
ancora: 'revocations: incredere.revocations, request: rh };', inlocuire: 'revocations: [], request: rh };',
rosii: [['delegare: telefonul', 'delegare revocata: trebuia refuzat']] },
{ id: 'P05', defect: 'un grant poate scuti starea credentialului (requireStatus: false acceptat)', fisier: S,
ancora: "if (!(g.policy.requireStatus !== false /*PAZA:grant-form*/)) throw", inlocuire: 'if (false) throw',
rosii: [['refuz: credential fara lista de stare nu deschide nimic', 'grant care scuteste starea: trebuia refuzat']] },
{ id: 'P06', defect: 'operatiile grantului nu se mai verifica (un grant get permite put)', fisier: S,
ancora: 'if (!(oricare || g.operations === null || g.operations.includes(op) /*PAZA:authorization*/)) continue;', inlocuire: 'if (false) continue;',
rosii: [['refuz: operatia nu e in grant', 'put: trebuia refuzat']] },
{ id: 'P07', defect: 'verdictul politicii grantului e ignorat (afirmatiile nu mai conteaza)', fisier: S,
ancora: 'if (!(r.compliant && !strain && !delegat /*PAZA:authorization*/)) continue;', inlocuire: 'if (false) continue;',
rosii: [['refuz: afirmatiile nu indeplinesc politica', 'cititorul: trebuia refuzat'], ['list: admin vede toate secretele', 'deployer: asteptat "db"']] },
{ id: 'P08', defect: 'o versiune revocata nu mai e refuzata pe nume', fisier: S,
ancora: "if (!(v.state !== 'revoked' /*PAZA:version-revoked*/)) throw", inlocuire: 'if (false) throw',
rosii: [['revoke-version: versiunea 1 revocata', 'auditorul la versiunea revocata']] },
{ id: 'P09', defect: 'versiunile vechi se citesc fara versions: any', fisier: S,
ancora: "if (!(ceruta === secret.current_version || grant.versions === 'any' /*PAZA:version-scope*/)) throw", inlocuire: 'if (false) throw',
rosii: [['rotatie: rotate face versiunea 2', 'deployer la versiunea 1: trebuia refuzat'], ['server: flux complet', 'versiunea 1 fara any 403']] },
{ id: 'P10', defect: 'serverul tipareste valoarea citita (un print de depanare uitat)', fisier: SV,
ancora: " const out = { name: r.name, version: r.version, current: r.current, value: r.value.toString('base64'), metadata: r.metadata, createdAt: r.createdAt };",
inlocuire: " const out = { name: r.name, version: r.version, current: r.current, value: r.value.toString('base64'), metadata: r.metadata, createdAt: r.createdAt }; process.stdout.write('debug get ' + out.value + '\\n');",
rosii: [['server: pornit din linia de comanda', 'valoarea nu e in stdout/stderr']] },
{ id: 'P11', defect: 'valoarea scrisa in clar in fisierul secretului, langa forma sigilata', fisier: S,
ancora: ' dek.fill(0); valoare.fill(0);', inlocuire: " dek.fill(0); sec.plain_debug = valoare.toString('base64'); valoare.fill(0);",
rosii: [['stocare: valoarea nu apare pe disc', 'valoarea nu e nicaieri pe disc'], ['server: pornit din linia de comanda', 'valoarea nu e pe discul procesului']] },
{ id: 'P12', defect: 'fisierul se scrie INAINTEA randului de audit (ordinea veche: o cadere lasa o schimbare fara intrare)', fisier: S,
ancora: ' this.#scrieAudit(a, acum); // AUDIT_WRITE_FAILED aici: nimic nu s-a schimbat pe disc\n if (r.salveaza) {',
inlocuire: ` if (r.salveaza) this.#salveaza(r.salveaza);\n this.#scrieAudit(a, acum);\n if (r.salveaza && (${NICIODATA})) {`,
rosii: [['jurnal: o mutatie al carei rand de audit', 'rotirea nu a ramas pe disc'], ['ordinea la mutatie', 'a aruncat altceva decat SecretsError']] },
{ id: 'P13', defect: 'o prezentare semnata de alta cheie decat a detinatorului e acceptata (verificatorul de identitate, regula presenter)', fisier: IDN,
ancora: 'asteptat && B.presenter.id === asteptat.id && canonical(B.presenter.keys) === canonical(asteptat.keys) /*PAZA:presenter*/,', inlocuire: 'true,',
rosii: [['refuz: un strain prezinta credentialul', 'strainul cu credentialul altuia: trebuia refuzat']] },
{ id: 'P14', defect: 'termenul prezentarii nu se mai cere (verificatorul de identitate, regula freshness)', fisier: IDN,
ancora: 'Math.abs(acum - t) <= maxAgeS * 1000 /*PAZA:freshness*/,', inlocuire: 'true,',
rosii: [['refuz: prezentare veche', 'prezentare veche: trebuia refuzat']] },
{ id: 'P15', defect: 'starea credentialului nu mai e ceruta de politica (verificatorul de conformitate, regula status)', fisier: CFM,
ancora: '!policy.requireStatus || !!(status && status.pass === true) /*PAZA:status*/,', inlocuire: 'true,',
rosii: [['refuz: credential fara lista de stare nu deschide nimic', 'fara lista de stare: trebuia refuzat'], ['refuz: credential revocat in lista', 'fara lista: starea nejudecata: trebuia refuzat']] },
{ id: 'P16', defect: 'refuzurile nu mai sunt scrise in jurnal', fisier: S,
ancora: 'const refuza = (e, reguli = []) => { a.rules = [...new Set([e.rule, ...reguli])]; this.#scrieAudit(a, acum); throw e; };',
inlocuire: 'const refuza = (e, reguli = []) => { a.rules = [...new Set([e.rule, ...reguli])]; throw e; };',
rosii: [['refuz: cerere fara prezentare', 'doua intrari de refuz: asteptat'], ['jurnal: dupa 100 de operatii', 'capul se schimba la operatia']] },
{ id: 'P17', defect: 'hash-ul prezentarii din jurnal nu mai e al legaturii semnate', fisier: S,
ancora: "try { prezentare = ID.canonical(a.binding); }", inlocuire: "try { prezentare = 'no-presentation'; }",
rosii: [['jurnal: fiecare operatie', 'hash-ul legaturii semnate: asteptat']] },
// ---------------------------------------------------------------------------------- 2026-10-18: paznicii revizuirii adversariale
{ id: 'P18', defect: 'cererea nu se mai compara (verificatorul de identitate, regula request): o prezentare pentru get deschide revoke', fisier: IDN,
ancora: 'B.request === request /*PAZA:request*/,', inlocuire: 'true,',
rosii: [['cererea (substitutie)', 'revoke cu prezentarea adminului pentru get: trebuia refuzat'], ['cererea prin HTTP', 'revoke-version cu corpul de get']] },
{ id: 'P19', defect: 'serviciul nu mai da digestul cererii verificatorului (cererea ramane nejudecata)', fisier: S,
ancora: 'revocations: incredere.revocations, request: rh };', inlocuire: 'revocations: incredere.revocations, request: null };',
rosii: [['cererea (substitutie)', 'revoke cu prezentarea adminului pentru get: trebuia refuzat']] },
{ id: 'P20', defect: 'digestul cererii nu mai acopera corpul (numai operatia si secretul)', fisier: S,
ancora: ' for (const k of Object.keys(body)) {\n if (k === \'presentation\' || body[k] === undefined) continue;', inlocuire: ' for (const k of []) {\n if (k === \'presentation\' || body[k] === undefined) continue;',
rosii: [['cererea (substitutie)', 'get db cu version adaugat: trebuia refuzat'], ['cererea prin HTTP', 'put cu valoarea schimbata']] },
{ id: 'P21', defect: 'un grant facut de un ne-admin nu mai e atenuat (auto-acordarea din revizuire)', fisier: S,
ancora: "if (origine.id !== 'admin') atenuare(g, origine);", inlocuire: 'if (false) atenuare(g, origine);',
rosii: [['atenuare:', 'put, pe care grantul lui nu il are: trebuia refuzat']] },
{ id: 'P22', defect: 'un ne-admin poate da mai departe grant si revoke (re-delegarea administrarii)', fisier: S,
ancora: 'o.operations.includes(x) && !OPERATII_ADMIN.includes(x)', inlocuire: 'o.operations.includes(x)',
rosii: [['atenuare:', 'grant mai departe: trebuia refuzat']] },
{ id: 'P23', defect: 'un ne-admin retrage si grant-urile altora', fisier: S,
ancora: "origine.id === 'admin' || tinta.createdBy === result.holder /*PAZA:grant-scope*/", inlocuire: 'true',
rosii: [['atenuare:', 'un grant al adminului: trebuia refuzat']] },
{ id: 'P24', defect: 'refuzul de autorizare numeste grant-urile in mesaj (oracolul din revizuire)', fisier: S,
ancora: "refuza(refuz('authorization', 'UNAUTHORIZED', MESAJ_NEAUTORIZAT), ", inlocuire: "refuza(refuz('authorization', 'UNAUTHORIZED', MESAJ_NEAUTORIZAT + ' [' + j.judged.map((x) => x.grant).join(', ') + ']'), ",
rosii: [['raspunsul generic', 'db (3 grant-uri) si api-key (0): asteptat']] },
{ id: 'P25', defect: 'refuzul de autorizare poarta grant-urile judecate in eroare (si deci in HTTP, daca serverul le-ar trimite)', fisier: S,
ancora: "refuza(refuz('authorization', 'UNAUTHORIZED', MESAJ_NEAUTORIZAT), ", inlocuire: "refuza(refuz('authorization', 'UNAUTHORIZED', MESAJ_NEAUTORIZAT, { grants: j.judged }), ",
rosii: [['raspunsul generic', 'db (3 grant-uri) si api-key (0): asteptat']] },
{ id: 'P26', defect: 'id-urile pretinse intra in jurnal oricum ar arata (100.000 de caractere)', fisier: S,
ancora: "const idPretins = (x) => (typeof x === 'string' && AERE_ID_RE.test(x) ? x : null);", inlocuire: "const idPretins = (x) => (typeof x === 'string' ? x : null);",
rosii: [['jurnalul unui refuz', 'anonimul: cod asteptat NONCE_INVALID, primit AUDIT_WRITE_FAILED']] },
{ id: 'P27', defect: 'id-urile pretinse scrise ca holder/presenter verificate', fisier: S,
ancora: 'if (!(tin /*identitatile verificate*/)) return;', inlocuire: 'if (!(true /*identitatile verificate*/)) return;',
rosii: [['jurnalul unui refuz', 'semnatura picata: holder nescris']] },
{ id: 'P28', defect: 'mutexul secretului nu mai asteapta (pierderi de actualizari cu KMS-ul la distanta)', fisier: S,
ancora: ' this.#lacate.set(cheie, coada);\n await inainte;', inlocuire: ' this.#lacate.set(cheie, coada);',
rosii: [['concurenta cu KMS-ul la distanta', 'versiunile intoarse: asteptat']] },
{ id: 'P29', defect: 'lacatul dosarului nu se mai ia (doua instante pe acelasi dosar)', fisier: S,
ancora: ' s.#iaLacatulDosarului(staleLock);\n', inlocuire: '',
rosii: [['lacatul dosarului', 'a doua deschidere in acelasi proces: asteptat']] },
{ id: 'P30', defect: 'lacatul unui proces mort e furat in tacere', fisier: S,
ancora: "if (staleLock !== 'take') throw new SecretsError('DATA_DIR_LOCK_STALE',", inlocuire: "if (false) throw new SecretsError('DATA_DIR_LOCK_STALE',",
rosii: [['lacatul dosarului', 'lacatul unui proces mort: asteptat']] },
{ id: 'P31', defect: 'marimea cererii nu se mai judeca (JSON adanc: RangeError si refuz fara rand)', fisier: S,
ancora: 'if (!(prea === null /*PAZA:request-form*/)) refuza(', inlocuire: 'if (false) refuza(',
rosii: [['JSON adanc', 'binding adanc: cod asteptat REQUEST_TOO_COMPLEX']] },
{ id: 'P32', defect: 'numele de dispozitiv ale Windows primite', fisier: S,
ancora: 'if (NUME_REZERVATE.test(name)) throw', inlocuire: 'if (false) throw',
rosii: [['nume si chei rezervate', 'con: trebuia refuzat']] },
{ id: 'P33', defect: 'cheile __proto__, constructor, prototype primite in metadate', fisier: S,
ancora: 'if (CHEI_REZERVATE.has(k)) throw', inlocuire: 'if (false) throw',
rosii: [['nume si chei rezervate', '__proto__: trebuia refuzat']] },
{ id: 'P34', defect: 'versiunea ceruta reflectata in mesaj', fisier: S,
ancora: "const versiuneAfisata = (v) => (Number.isSafeInteger(v) ? String(v) : '(not an integer)');", inlocuire: 'const versiuneAfisata = (v) => String(v);',
rosii: [['mesajele nu reflecta', 'mesajul trebuia sa contina "(not an integer)"']] },
{ id: 'P35', defect: 'un grant poate purta id-ul admin', fisier: S,
ancora: "if (!(g.id !== 'admin' /*PAZA:grant-form*/)) throw", inlocuire: 'if (false) throw',
rosii: [['mesajele nu reflecta', 'grant admin: trebuia refuzat']] },
{ id: 'P36', defect: 'fara plafon de grant-uri la creare', fisier: S,
ancora: 'if (!(grants.length <= MAX_GRANTS /*PAZA:grant-limit*/)) throw', inlocuire: 'if (false) throw',
rosii: [['plafonul grant-urilor', '33 la creare: trebuia refuzat']] },
{ id: 'P37', defect: 'fara plafon de grant-uri la grant', fisier: S,
ancora: 'if (!(secret.grants.length < MAX_GRANTS /*PAZA:grant-limit*/)) throw', inlocuire: 'if (false) throw',
rosii: [['plafonul grant-urilor', 'al 33-lea: trebuia refuzat']] },
{ id: 'P38', defect: 'prezentarea se verifica din nou pentru fiecare grant (fara cache)', fisier: S,
ancora: 'r = CF.checkCompliance(p, g.policy, { ...ctx, cache });', inlocuire: 'r = CF.checkCompliance(p, g.policy, { ...ctx });',
rosii: [['plafonul grant-urilor', 'citirile semnaturii']] },
{ id: 'P39', defect: 'fara plafon de nonce-uri pe origine', fisier: S,
ancora: 'if (!(inZbor < this.#noncePerOrigin /*paznicul nonce-urilor pe origine*/)) throw', inlocuire: 'if (false) throw',
rosii: [['nonce-uri pe origine', 'al 33-lea: asteptat']] },
{ id: 'P40', defect: 'serverul nu mai da adresa la emiterea nonce-ului', fisier: SV,
ancora: "secrets.nonce({ origin: String(req.socket.remoteAddress || 'unknown') })", inlocuire: 'secrets.nonce()',
rosii: [['nonce-uri pe origine', '32 de 200: asteptat']] },
{ id: 'P41', defect: 'o scriere cazuta dupa rand nu mai primeste randul failed', fisier: S,
ancora: "try { this.#scrieAudit({ ...a, verdict: 'failed', rules: ['storage'] }, acum); } catch", inlocuire: 'try { /* fara randul failed */ } catch',
rosii: [['ordinea la mutatie', 'randul inainte, apoi failed']] },
{ id: 'P42', defect: 'delegates: false nu mai opreste delegatii', fisier: S,
ancora: 'const delegat = g.delegates === false && !!v.subject && v.subject.delegations > 0;', inlocuire: 'const delegat = false;',
rosii: [['delegates: false', 'telefonul: trebuia refuzat']] },
{ id: 'P43', defect: 'vocabularul felului secret-access nu se mai judeca (verdict banana construit)', fisier: PKS,
ancora: "if (statement[camp] !== undefined && !bun(statement[camp])) throw", inlocuire: 'if (false) throw',
rosii: [['vocabularul plicului secret-access', 'verdict="banana": construit']] },
];
const MOMELI = [
{ id: 'M1', defect: 'ancora care nu exista in cod', fisier: S, ancora: 'ACEASTA ANCORA NU EXISTA IN COD', inlocuire: 'x', rosii: [['refuz: nonce refolosit', 'x']], asteptat: 'ESEC CONTROL' },
{ id: 'M2', defect: 'plantare fara efect (un comentariu)', fisier: S, ancora: "export const KIND = 'secret-access';", inlocuire: "export const KIND = 'secret-access'; // momeala fara efect", rosii: [['refuz: nonce refolosit', 'x']], asteptat: 'VERDE (NEPRINS)' },
{ id: 'M3', defect: 'defect real, dar motivul cerut nu e cel pe care cade proba', fisier: S, ancora: 'revocations: incredere.revocations, request: rh };', inlocuire: 'revocations: [], request: rh };', rosii: [['delegare: telefonul', 'un motiv care nu apare niciodata']], asteptat: 'ROSU GRESIT' },
{ id: 'M4', defect: 'proba numita nu exista', fisier: S, ancora: 'revocations: incredere.revocations, request: rh };', inlocuire: 'revocations: [], request: rh };', rosii: [['o proba care nu exista', 'x']], asteptat: 'ESEC CONTROL' },
{ id: 'M5', defect: 'plantare care rupe incarcarea modulului', fisier: S, ancora: 'revocations: incredere.revocations, request: rh };', inlocuire: 'revocations: incredere.revocations ,, request: rh };', rosii: [['delegare: telefonul', 'delegare revocata']], asteptat: 'STRICAT' },
];
const sha = (p) => crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
const amprente = () => Object.fromEntries(FISIERE.map((f) => [f, sha(path.join(RADACINA, f))]));
function pregatesteCopie(dest) {
fs.rmSync(dest, { recursive: true, force: true });
for (const f of FISIERE) { const d = path.join(dest, f); fs.mkdirSync(path.dirname(d), { recursive: true }); fs.copyFileSync(path.join(RADACINA, f), d); }
for (const f of FISIERE) if (sha(path.join(dest, f)) !== sha(path.join(RADACINA, f))) throw new Error(`copia lui ${f} difera de original inainte de plantare`);
}
function planteaza(dest, p) {
const fp = path.join(dest, p.fisier);
const text = fs.readFileSync(fp, 'utf8');
const bucati = text.split(p.ancora);
if (bucati.length !== 2) return { ok: false, motiv: `ancora apare de ${bucati.length - 1} ori in ${p.fisier} (trebuie exact o data)` };
fs.writeFileSync(fp, bucati.join(p.inlocuire));
if (sha(fp) === sha(path.join(RADACINA, p.fisier))) return { ok: false, motiv: 'fisierul plantat e identic cu originalul' };
return { ok: true };
}
const envCurat = () => Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('AERE_KMS_') && !k.startsWith('AERE_SECRETS_')));
function ruleaza(dest) {
const out = path.join(dest, 'rezultat-proba.json');
fs.rmSync(out, { force: true });
const t0 = Date.now();
const env = { ...envCurat(), AERE_SECRETS_PROBA_JSON: out };
if (VERIFY) env.AERE_VERIFY_PROOF = VERIFY; else delete env.AERE_VERIFY_PROOF;
return new Promise((gata) => {
const c = spawn(process.execPath, [path.join(dest, PR)], { cwd: path.join(dest, 'cloud-gateway', 'pq-secrets'), env, stdio: ['ignore', 'ignore', 'pipe'] });
let stderr = ''; c.stderr.on('data', (d) => { stderr = (stderr + d).slice(-4000); });
const ceas = setTimeout(() => c.kill(), 600000);
c.on('close', (status, signal) => {
clearTimeout(ceas);
let json = null;
try { json = JSON.parse(fs.readFileSync(out, 'utf8')); } catch { json = null; }
gata({ json, status, signal, ms: Date.now() - t0, stderr: stderr.slice(-600) });
});
});
}
// o proba se numeste prin INCEPUTUL numelui ei, care trebuie sa fie unic
function gaseste(nume, prefix) { const c = nume.filter((n) => n.startsWith(prefix)); return c.length === 1 ? c[0] : null; }
async function judecaPlantarea(dest, p, nume, baza) {
pregatesteCopie(dest);
const numite = p.rosii.map(([pre]) => [pre, gaseste(nume, pre)]);
const lipsa = numite.filter(([, n]) => !n).map(([pre]) => pre);
if (lipsa.length) return { p, verdict: 'ESEC CONTROL', detaliu: `proba numita nu exista (sau prefixul nu e unic): ${lipsa.join(' | ')}`, esec: true };
const pl = planteaza(dest, p);
if (!pl.ok) return { p, verdict: 'ESEC CONTROL', detaliu: `plantarea nu s-a putut pune: ${pl.motiv}`, esec: true };
const r = await ruleaza(dest);
if (!r.json || r.json.completed !== true || r.json.total !== baza.json.total) return { p, verdict: 'STRICAT', detaliu: `proba nu a rulat intreaga (cod ${r.status}, probe ${r.json ? r.json.total : '-'}/${baza.json.total}); ${r.stderr.split('\n').slice(-3).join(' ')}`, esec: true };
const dupaNume = new Map(r.json.results.map((x) => [x.nume, x]));
const probleme = []; let rosiiNumite = 0;
for (let i = 0; i < p.rosii.length; i++) {
const [, n] = numite[i]; const motiv = p.rosii[i][1]; const x = dupaNume.get(n);
if (x.ok) probleme.push(`VERDE: ${n.slice(0, 70)}`);
else if (!String(x.motiv).includes(motiv)) probleme.push(`ROSU DIN ALT MOTIV: ${n.slice(0, 60)} -> "${String(x.motiv).slice(0, 160)}" (cerut: "${motiv}")`);
else rosiiNumite++;
}
const inPlus = r.json.failed.filter((n) => !numite.some(([, m]) => m === n));
if (probleme.length === 0) return { p, verdict: 'ROSU', detaliu: `${rosiiNumite}/${p.rosii.length} numite rosii pe motivul cerut; alte ${inPlus.length} rosii in plus`, inPlus, ms: r.ms };
return { p, verdict: r.json.failed.length === 0 ? 'VERDE (NEPRINS)' : 'ROSU GRESIT', detaliu: probleme.join('\n '), inPlus, ms: r.ms, esec: true };
}
async function main() {
const autoproba = process.argv.includes('--autoproba');
const PL = autoproba ? MOMELI : PLANTARI;
// --ancore: numai verificarea ancorelor pe original (fiecare exact o data), fara nicio rulare
if (process.argv.includes('--ancore')) { let rele = 0; for (const p of PL) { const n = fs.readFileSync(path.join(RADACINA, p.fisier), 'utf8').split(p.ancora).length - 1; if (n !== 1) rele++; console.log(`${n === 1 ? 'OK ' : 'RAU '} ${p.id} ancora de ${n} ori in ${p.fisier}`); } return rele ? 1 : 0; }
if (new Set(PL.map((p) => p.id)).size !== PL.length) throw new Error('doua plantari cu acelasi id');
if (autoproba) console.log('AUTOPROBA: momelile de mai jos TREBUIE sa primeasca verdictul lor de esec\n');
if (!VERIFY) console.log('verificatorul AIP-23 lipseste (AERE_VERIFY_PROOF): proba plicurilor va fi SARITA in toate rularile\n');
const inainte = amprente();
const lucru = fs.mkdtempSync(path.join(os.tmpdir(), 'aeresecrets-control-'));
const linii = new Array(PL.length);
let esecuri = 0;
let baza;
try {
const d0 = path.join(lucru, 'baza');
pregatesteCopie(d0);
baza = await ruleaza(d0);
if (!baza.json || baza.json.completed !== true) { console.log(`LINIA DE BAZA STRICATA: proba nu a ajuns la capat (cod ${baza.status}, semnal ${baza.signal})\n${baza.stderr}`); return 1; }
if (baza.json.failed.length !== 0) { console.log(`LINIA DE BAZA ROSIE: ${baza.json.failed.length} probe pica pe codul neatins:\n ${baza.json.failed.join('\n ')}`); return 1; }
const nume = baza.json.results.map((r) => r.nume);
console.log(`linia de baza: VERDE, ${baza.json.passed}/${baza.json.total} probe (${baza.json.skipped.length} sarite), ${baza.ms} ms (node ${baza.json.node}, openssl ${baza.json.openssl}); ${LUCRATORI} lucratori\n`);
let urm = 0;
await Promise.all(Array.from({ length: LUCRATORI }, async (_, k) => {
const dest = path.join(lucru, 'copie-' + k);
while (urm < PL.length) { const i = urm++; linii[i] = await judecaPlantarea(dest, PL[i], nume, baza); }
}));
} finally { fs.rmSync(lucru, { recursive: true, force: true }); }
for (const l of linii) {
if (l.esec) esecuri++;
console.log(`${l.verdict.padEnd(15)} ${l.p.id} ${l.p.defect}${l.ms ? ` (${l.ms} ms)` : ''}\n ${l.detaliu}`);
if (l.inPlus && l.inPlus.length) console.log(` in plus: ${l.inPlus.map((n) => n.slice(0, 60)).join(' | ')}`);
}
const dupa = amprente();
const schimbate = FISIERE.filter((f) => inainte[f] !== dupa[f]);
if (schimbate.length) { esecuri++; console.log(`\nORIGINALUL S-A SCHIMBAT: ${schimbate.join(', ')}`); } else console.log(`\noriginal neatins (sha256): ${FISIERE.map((f) => `${path.basename(f)}=${dupa[f].slice(0, 10)}`).join(' ')}`);
if (autoproba) {
let potrivite = 0;
for (const l of linii) { const ok = l.verdict === l.p.asteptat; if (ok) potrivite++; console.log(`${ok ? 'CORECT ' : 'GRESIT '} ${l.p.id}: controlul a spus "${l.verdict}", trebuia "${l.p.asteptat}"`); }
const originalOk = schimbate.length === 0;
console.log(`\nautoproba controlului: ${potrivite}/${MOMELI.length} momeli cu verdictul cerut; original ${originalOk ? 'neatins' : 'SCHIMBAT'}`);
return potrivite === MOMELI.length && linii.length === MOMELI.length && originalOk ? 0 : 1;
}
const rosii = linii.filter((l) => l.verdict === 'ROSU').length;
console.log(`\ncontrol negativ: ${rosii}/${PLANTARI.length} plantari prinse pe probele numite; ${esecuri} esecuri ale controlului`);
return esecuri === 0 && rosii === PLANTARI.length ? 0 : 1;
}
main().then((c) => { process.exitCode = c; }, (e) => { console.log(`controlul negativ a cazut: ${(e && e.stack) || e}`); process.exitCode = 2; });

View File

@ -0,0 +1,974 @@
// test/proba-secrete.mjs - proba Aere PQ Secrets, fara framework.
//
// Fluxul intreg cu chei NASCUTE la rulare (un emitent, un admin, un deployer, un cititor, un rotator, un auditor, un telefon delegat, un
// strain, un detinator revocat, unul fara lista de stare): put/get autorizate, fiecare refuz PE NUME (regula serviciului si regulile
// verificatorului de identitate), rotatia cu versiunile vechi citibile numai cu politica, revocarea de versiune, grant/revoke, list,
// valoarea cautata ca SIR (si ca base64) pe disc, in jurnalul sidecar-ului, in jurnalul KMS si in iesirea serverului (cu control pozitiv al
// scanerului), jurnalul intact dupa 100 de operatii, rollback-ul mutatiei al carei rand de audit nu se poate scrie, plicurile VALID pentru
// verificatorul AIP-23 de referinta (AERE_VERIFY_PROOF; fara el, SARIT pe nume si cod 2), serverul HTTP in proces, din linia de comanda si cu
// un KMS la distanta.
//
// Iesire: tabel pe stdout; cod 0 daca toate trec, 1 daca una pica, 2 daca una e SARITA (fara verificator) si niciuna nu pica.
// Cu AERE_SECRETS_PROBA_JSON=<fisier> scrie rezultatul complet ca JSON (il citeste test/control-negativ-secrete.mjs; completed: true
// inseamna ca s-a ajuns la capat).
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import http from 'node:http';
import { spawn, spawnSync, execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { openKms } from '../../pq-kms/kms.mjs';
import { createServer as createKmsServer } from '../../pq-kms/server.mjs';
import { openSecrets as deschideSecrete, kmsClientHttp, SecretsError, MODULE, REGULI, KIND, MAX_VALUE, OPERATII, requestHash, MESAJ_NEAUTORIZAT, MAX_GRANTS, NONCE_PER_ORIGIN, MAX_JSON_DEPTH } from '../secrete.mjs';
import { createServer, configFromEnv, openFromConfig } from '../server.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const SERVER_PATH = path.join(HERE, '..', 'server.mjs');
const { identity: ID, sidecar: SC, proofKinds: PK } = MODULE;
const SIDECAR_PATH = MODULE.cai.sidecar;
const VERIFY = process.env.AERE_VERIFY_PROOF ? path.resolve(process.env.AERE_VERIFY_PROOF) : path.resolve(HERE, '..', '..', '..', 'tools', 'aere-proof-protocol', 'verify.mjs');
const areVerificator = fs.existsSync(VERIFY);
// ---------------------------------------------------------------------------------- harness
const probe = [];
function proba(nume, fn) { if (probe.some((p) => p.nume === nume)) throw new Error('proba dublata: ' + nume); probe.push({ nume, fn }); }
class Esec extends Error {}
class Sarit extends Error {}
const adevarat = (c, ce) => { if (!c) throw new Esec(ce); };
const egal = (a, b, ce) => { if (a !== b) throw new Esec(`${ce}: asteptat ${JSON.stringify(b)}, primit ${JSON.stringify(a)}`); };
/** cere un refuz SecretsError cu codul dat; optional regula serviciului, o regula a identitatii, o regula de conformitate, un fragment de mesaj */
async function refuza(fn, cod, ce, { rule, identityRule, complianceRule, mesaj } = {}) {
let e;
try { await fn(); } catch (x) { e = x; }
if (!e) throw new Esec(`${ce}: trebuia refuzat cu ${cod}, a trecut`);
if (!(e instanceof SecretsError)) throw new Esec(`${ce}: a aruncat altceva decat SecretsError: ${e && e.stack}`);
if (e.code !== cod) throw new Esec(`${ce}: cod asteptat ${cod}, primit ${e.code} (${e.message})`);
if (rule !== undefined && e.rule !== rule) throw new Esec(`${ce}: regula asteptata ${rule}, primita ${e.rule}`);
if (identityRule !== undefined && !(e.identityRules || []).includes(identityRule)) throw new Esec(`${ce}: regula identitatii asteptata ${identityRule}, primite [${(e.identityRules || []).join(', ')}] (${e.message})`);
if (complianceRule !== undefined && !(e.complianceRules || []).includes(complianceRule)) throw new Esec(`${ce}: regula de conformitate asteptata ${complianceRule}, primite [${(e.complianceRules || []).join(', ')}] (${e.message})`);
if (mesaj !== undefined && !String(e.message).includes(mesaj)) throw new Esec(`${ce}: mesajul trebuia sa contina "${mesaj}": ${e.message}`);
return e;
}
const tmpDirs = [];
const tmpDir = (tag = 'd') => { const d = fs.mkdtempSync(path.join(os.tmpdir(), `aeresecrets-proba-${tag}-`)); tmpDirs.push(d); return d; };
const rootHex = () => crypto.randomBytes(32).toString('hex');
const iso = (d) => new Date(d).toISOString();
const ms = (s) => s * 1000;
// ---------------------------------------------------------------------------------- identitatile de proba (nascute la fiecare rulare)
const AUD = 'https://secrets.example';
const ALT_AUD = 'https://other-service.example';
const LID = 'urn:example:secrets-status:1';
const T0 = Date.now();
const issuer = ID.generateKeys(), altIssuer = ID.generateKeys();
const K = { admin: ID.generateKeys(), deployer: ID.generateKeys(), reader: ID.generateKeys(), reader2: ID.generateKeys(), rotator: ID.generateKeys(), auditor: ID.generateKeys(), granter: ID.generateKeys(), phone: ID.generateKeys(), stranger: ID.generateKeys(), revoked: ID.generateKeys(), nostatus: ID.generateKeys() };
function emite(holder, claims, index, { iss = issuer } = {}) {
return ID.issueCredential({ issuer: iss, holder: holder.public, type: 'ServiceAccess', claims, disclosable: Object.keys(claims), validFrom: iso(T0 - ms(3600)), validUntil: iso(T0 + ms(86400 * 365)), status: index === null ? null : { list: LID, index }, now: new Date(T0 - ms(3600)) });
}
const C = {
admin: emite(K.admin, { role: 'secrets-admin', tenant: 'acme' }, 0),
deployer: emite(K.deployer, { role: 'deployer', tenant: 'acme' }, 1),
reader: emite(K.reader, { role: 'reader', tenant: 'acme' }, 2),
reader2: emite(K.reader2, { role: 'reader', tenant: 'acme' }, 5),
rotator: emite(K.rotator, { role: 'rotator', tenant: 'acme' }, 3),
auditor: emite(K.auditor, { role: 'auditor', tenant: 'acme' }, 4),
granter: emite(K.granter, { role: 'granter', tenant: 'acme' }, 6),
revoked: emite(K.revoked, { role: 'deployer', tenant: 'acme' }, 9),
nostatus: emite(K.nostatus, { role: 'deployer', tenant: 'acme' }, null),
// strainul isi emite singur un credential de "secrets-admin" cu cheile LUI de emitent
stranger: emite(K.stranger, { role: 'secrets-admin', tenant: 'acme' }, 0, { iss: altIssuer }),
};
const LISTA = ID.createStatusList({ issuer, id: LID, revoked: [9], validFrom: iso(T0 - ms(3600)), validUntil: iso(T0 + ms(86400)), now: new Date(T0 - ms(3600)) });
const CRED_IDS = [C.deployer.credential.statement.id]; // id-urile credentialelor pe care le acopera delegarea (scope.credentials)
const DELEGARE = ID.delegate({ from: K.deployer, to: K.phone.public, scope: { credentials: CRED_IDS, claims: '*', audiences: [AUD] }, notBefore: iso(T0 - ms(3600)), notAfter: iso(T0 + ms(86400)), maxDepth: 0, now: new Date(T0 - ms(3600)) });
const DELEGARE_INGUSTA = ID.delegate({ from: K.deployer, to: K.phone.public, scope: { credentials: CRED_IDS, claims: ['role'], audiences: [AUD] }, notBefore: iso(T0 - ms(3600)), notAfter: iso(T0 + ms(86400)), maxDepth: 0, now: new Date(T0 - ms(3600)) });
const REVOCARE = ID.revokeDelegation({ by: K.deployer, delegation: DELEGARE, at: iso(T0 - ms(60)) });
const POL = (id, role) => ({ id, trustedIssuers: [issuer.id], require: [{ claim: 'role', equals: role }] });
const ADMIN_POLICY = { id: 'secrets-admin-v1', trustedIssuers: [issuer.id], require: [{ claim: 'role', equals: 'secrets-admin' }, { claim: 'tenant', equals: 'acme' }] };
const GRANTS = () => [
{ id: 'deployers', operations: ['get', 'metadata'], policy: { id: 'deployers-v1', trustedIssuers: [issuer.id], require: [{ claim: 'role', equals: 'deployer' }, { claim: 'tenant', equals: 'acme' }] } },
{ id: 'rotators', operations: ['rotate', 'revoke-version'], policy: POL('rotators-v1', 'rotator') },
{ id: 'auditors', operations: ['get', 'metadata'], versions: 'any', policy: POL('auditors-v1', 'auditor') },
];
const VALOARE = 'S3cr3t-Db-P4ssw0rd-' + crypto.randomBytes(12).toString('hex');
// CLIENTUL DE PROBA, 2026-10-18 (B8): o prezentare se semneaza pentru CEREREA pe care o trimite (digestul ei, requestHash, in legatura).
// prez() intoarce prezentarea fara cerere, cu ce trebuie ca sa fie semnata; serviciul de proba (infasoara) si cererea HTTP (cerere) o
// semneaza, la PRIMA trimitere, pentru exact operatia, secretul si corpul trimise, ca un client cinstit. O prezentare atinsa dupa prez()
// (un atac) pleaca asa cum e; una data cu `pentru: [operatie, secret, corp]` e semnata pe loc pentru acea cerere, si oriunde altundeva e
// depusa e o SUBSTITUTIE (proba ei). A doua trimitere a aceleiasi prezentari e o reluare: pleaca neschimbata.
const INTENTIE = Symbol('cererea pentru care se semneaza la trimitere');
function semneaza(p, op, name, body, valueEncoding = 'bytes') {
const m = p && typeof p === 'object' ? p[INTENTIE] : null;
if (!m || m.folosita) return;
m.folosita = true;
let neatinsa = false; try { neatinsa = ID.canonical(p.binding) === m.semnat; } catch { neatinsa = false; }
if (!neatinsa) return;
let rh; try { rh = requestHash({ audience: m.args.audience, operation: op, secret: name, body, valueEncoding }); } catch { return; }
const q = ID.present({ ...m.args, request: rh });
p.binding = q.binding; p.signature = q.signature;
}
const METODE = { put: 'put', get: 'get', metadata: 'metadata', rotate: 'rotate', revokeVersion: 'revoke-version', grant: 'grant', revoke: 'revoke' };
function infasoara(s) {
for (const [m, op] of Object.entries(METODE)) { const f = s[m].bind(s); s[m] = (name, body) => { semneaza(body && body.presentation, op, name, body); return f(name, body); }; }
const l = s.list.bind(s); s.list = (body) => { semneaza(body && body.presentation, 'list', null, body); return l(body); };
return s;
}
const openSecrets = async (o) => infasoara(await deschideSecrete(o));
async function nouServiciu({ now, cuLista = true, auditLog, trustDir, kms: kmsDat = null } = {}) {
const dir = tmpDir('svc');
const kms = kmsDat || openKms({ dataDir: path.join(dir, 'kms'), rootKey: rootHex() });
const trust = trustDir || path.join(dir, 'trust');
fs.mkdirSync(path.join(trust, 'status-lists'), { recursive: true });
fs.mkdirSync(path.join(trust, 'revocations'), { recursive: true });
if (cuLista) fs.writeFileSync(path.join(trust, 'status-lists', 'lista.json'), JSON.stringify(LISTA));
const s = await openSecrets({ dataDir: path.join(dir, 'data'), kms, audience: AUD, adminPolicy: ADMIN_POLICY, trustDir: trust, auditLog, now });
return { s, dir, kms, trust, data: path.join(dir, 'data') };
}
/** o prezentare pentru serviciu, cu un nonce proaspat al lui (sau unul dat); semnata la trimitere pentru cererea trimisa (vezi sus) */
function prez(s, keys, cred, { reveal = null, nonce = null, audience = AUD, now = new Date(), delegations = [], pentru = null } = {}) {
const r = reveal || Object.keys(Object.fromEntries(cred.disclosures.map((d) => [ID.decodeDisclosure(d).name, 1])));
const args = { credential: cred.credential, disclosures: cred.disclosures, reveal: r, presenter: keys, delegations, audience, nonce: nonce || s.nonce().nonce, now };
if (pentru) { const [op, name, body = {}, enc = 'bytes'] = pentru; return ID.present({ ...args, request: requestHash({ audience, operation: op, secret: name, body, valueEncoding: enc }) }); }
const p = ID.present(args);
Object.defineProperty(p, INTENTIE, { value: { args, semnat: ID.canonical(p.binding), folosita: false }, enumerable: false });
return p;
}
/** ultima declaratie din jurnal */
const ultima = (s) => { const j = jurnal(s.auditLog); return j[j.length - 1].proof.statement; };
/** un serviciu populat: secretul db (valoarea VALOARE, grants: deployers/rotators/auditors) si api-key (fara grants), ambele puse de admin */
async function populat(opts) {
const x = await nouServiciu(opts);
await x.s.put('db', { presentation: prez(x.s, K.admin, C.admin), value: VALOARE, metadata: { owner: 'platform', kind: 'password' }, grants: GRANTS() });
await x.s.put('api-key', { presentation: prez(x.s, K.admin, C.admin), value: 'ak-' + crypto.randomBytes(8).toString('hex') });
return x;
}
/** cauta un sir (si forma lui base64) in toate fisierele de sub un dosar; intoarce fisierele in care apare */
function cauta(dosar, sir) {
const forme = [sir, Buffer.from(sir, 'utf8').toString('base64'), Buffer.from(sir, 'utf8').toString('base64url'), Buffer.from(sir, 'utf8').toString('hex')];
const out = [];
const merge = (d) => { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) merge(p); else if (forme.some((f) => fs.readFileSync(p, 'latin1').includes(f))) out.push(p); } };
if (fs.existsSync(dosar)) merge(dosar);
return out;
}
const jurnal = (p) => fs.readFileSync(p, 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
function cerere(port, method, p, { headers = {}, body } = {}) {
// clientul de proba semneaza prezentarea pentru ruta si corpul pe care le trimite (valoarea, in HTTP, e base64-ul asa cum pleaca)
if (body && typeof body === 'object' && !Buffer.isBuffer(body) && body.presentation) {
const m = /^\/v1\/secrets\/([^/]+)\/([^/?]+)$/.exec(p);
try { if (m) semneaza(body.presentation, m[2], decodeURIComponent(m[1]), body, 'base64'); else if (p === '/v1/list') semneaza(body.presentation, 'list', null, body, 'base64'); } catch { /* o ruta pe care clientul nu o stie: pleaca nesemnata */ }
}
return new Promise((resolve, reject) => {
const data = body === undefined ? null : Buffer.isBuffer(body) ? body : Buffer.from(typeof body === 'string' ? body : JSON.stringify(body));
const h = { ...headers };
if (data) { h['content-length'] = data.length; if (h['content-type'] === undefined) h['content-type'] = 'application/json'; }
const req = http.request({ host: '127.0.0.1', port, method, path: p, headers: h, agent: false }, (res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => { const text = Buffer.concat(chunks).toString('utf8'); let json = null; try { json = JSON.parse(text); } catch { json = null; } resolve({ status: res.statusCode, json, text }); });
});
req.on('error', reject);
if (data) req.write(data);
req.end();
});
}
const envCurat = () => Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('AERE_KMS_') && !k.startsWith('AERE_SECRETS_')));
// ================================================================================== PROBELE
proba('pornire: creeaza cheia KMS (encrypt) si service.json; redeschiderea citeste aceeasi cheie de MAC; refuzuri numite: fara audienta, admin fara trustedIssuers, admin cu requireStatus false, alta audienta pe acelasi dosar', async () => {
const { s, dir, kms, trust } = await nouServiciu();
const k = kms.getKey('secrets');
egal(k.type, 'encrypt', 'tipul cheii KMS');
adevarat(fs.existsSync(path.join(dir, 'data', 'service.json')), 'service.json scris');
const sj = JSON.parse(fs.readFileSync(path.join(dir, 'data', 'service.json'), 'utf8'));
adevarat(typeof sj.mac_key_wrapped === 'string' && sj.mac_key_wrapped.startsWith('aerekms:v1:'), 'cheia de MAC e invelita de KMS');
await s.put('x', { presentation: prez(s, K.admin, C.admin), value: 'v' });
s.close();
const s2 = await openSecrets({ dataDir: path.join(dir, 'data'), kms, audience: AUD, adminPolicy: ADMIN_POLICY, trustDir: trust });
const r = await s2.get('x', { presentation: prez(s2, K.admin, C.admin) });
egal(r.value.toString(), 'v', 'dupa redeschidere fisierul trece MAC-ul si se citeste');
s2.close();
let e;
try { await openSecrets({ dataDir: tmpDir(), kms, audience: '', adminPolicy: ADMIN_POLICY }); } catch (x) { e = x; }
egal(e && e.code, 'AUDIENCE_INVALID', 'fara audienta');
e = null; try { await openSecrets({ dataDir: tmpDir(), kms, audience: AUD, adminPolicy: { id: 'a', trustedIssuers: [], require: [{ claim: 'role', equals: 'x' }] } }); } catch (x) { e = x; }
egal(e && e.code, 'ADMIN_POLICY_INVALID', 'admin fara trustedIssuers');
e = null; try { await openSecrets({ dataDir: tmpDir(), kms, audience: AUD, adminPolicy: { ...ADMIN_POLICY, requireStatus: false } }); } catch (x) { e = x; }
egal(e && e.code, 'ADMIN_POLICY_INVALID', 'admin cu requireStatus false');
e = null; try { await openSecrets({ dataDir: path.join(dir, 'data'), kms, audience: ALT_AUD, adminPolicy: ADMIN_POLICY }); } catch (x) { e = x; }
egal(e && e.code, 'AUDIENCE_MISMATCH', 'alta audienta pe acelasi dosar');
e = null; try { await openSecrets({ dataDir: tmpDir(), audience: AUD, adminPolicy: ADMIN_POLICY }); } catch (x) { e = x; }
egal(e && e.code, 'KMS_MISSING', 'fara KMS');
kms.close();
});
proba('flux: admin pune secretul cu grants, deployer-ul il citeste: valoarea e exact cea pusa, versiunea 1, metadatele; metadata nu contine valoarea; intrarea de audit a put-ului poarta grantul admin', async () => {
const { s, kms } = await populat();
const r = await s.get('db', { presentation: prez(s, K.deployer, C.deployer) });
egal(r.value.toString('utf8'), VALOARE, 'valoarea citita');
egal(r.version, 1, 'versiunea'); egal(r.current, 1, 'curenta');
egal(r.metadata.owner, 'platform', 'metadata');
const m = await s.metadata('db', { presentation: prez(s, K.deployer, C.deployer) });
egal(m.current, 1, 'metadata: curenta'); egal(m.versions['1'].state, 'enabled', 'metadata: starea');
egal(m.grants.map((g) => g.id).join(','), 'deployers,rotators,auditors', 'metadata: grants');
adevarat(!JSON.stringify(m).includes(VALOARE) && !JSON.stringify(m).includes('sealed') && !JSON.stringify(m).includes('dek_wrapped'), 'metadata nu contine valoarea, nici materialul sigilat');
const j = jurnal(s.auditLog);
egal(j[0].proof.statement.operation, 'put', 'prima intrare: put'); egal(j[0].proof.statement.grant, 'admin', 'grantul admin'); egal(j[0].proof.statement.verdict, 'allowed', 'verdict');
egal(j[0].proof.statement.holder, K.admin.id, 'detinatorul verificat');
s.close(); kms.close();
});
proba('refuz: cerere fara prezentare -> PRESENTATION_MISSING (presentation-form), auditat; cerere care nu e obiect -> INVALID_REQUEST si nume invalid -> INVALID_SECRET_NAME, fara intrare in jurnal', async () => {
const { s, kms } = await populat();
const n0 = jurnal(s.auditLog).length;
await refuza(() => s.get('db', {}), 'PRESENTATION_MISSING', 'fara prezentare', { rule: 'presentation-form' });
await refuza(() => s.get('db', { presentation: 'nu' }), 'PRESENTATION_MISSING', 'prezentare care nu e obiect', { rule: 'presentation-form' });
egal(jurnal(s.auditLog).length, n0 + 2, 'doua intrari de refuz');
const j = jurnal(s.auditLog); const st = j[j.length - 1].proof.statement;
egal(st.verdict, 'refused', 'verdictul'); adevarat(st.rules.includes('presentation-form'), 'regula numita in jurnal: ' + st.rules.join(','));
await refuza(() => s.get('db', null), 'INVALID_REQUEST', 'cerere nula', { rule: 'request-form' });
await refuza(() => s.get('DB NAME', { presentation: prez(s, K.admin, C.admin) }), 'INVALID_SECRET_NAME', 'nume invalid', { rule: 'secret-name' });
await refuza(() => s.get('../x', { presentation: prez(s, K.admin, C.admin) }), 'INVALID_SECRET_NAME', 'nume cu cale', { rule: 'secret-name' });
egal(jurnal(s.auditLog).length, n0 + 2, 'forma cererii si numele nu sunt operatii: fara intrare');
s.close(); kms.close();
});
proba('refuz: nonce refolosit (aceeasi prezentare a doua oara) -> NONCE_INVALID (nonce), si dupa o prima cerere REFUZATA; nonce neemis de serviciu -> NONCE_INVALID "not issued"', async () => {
const { s, kms } = await populat();
const p = prez(s, K.deployer, C.deployer);
egal((await s.get('db', { presentation: p })).version, 1, 'prima citire');
await refuza(() => s.get('db', { presentation: p }), 'NONCE_INVALID', 'a doua oara aceeasi prezentare', { rule: 'nonce', mesaj: 'not issued by this service, or already used' });
const q = prez(s, K.reader, C.reader);
await refuza(() => s.get('db', { presentation: q }), 'UNAUTHORIZED', 'cititorul e refuzat de politica');
await refuza(() => s.get('db', { presentation: q }), 'NONCE_INVALID', 'nonce-ul s-a consumat si la refuz', { rule: 'nonce' });
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer, { nonce: crypto.randomBytes(32).toString('base64url') }) }), 'NONCE_INVALID', 'nonce strain', { rule: 'nonce', mesaj: 'not issued' });
s.close(); kms.close();
});
proba('refuz: nonce expirat (ceasul serviciului trecut de TTL) -> NONCE_INVALID "expired"; inainte de TTL acelasi nonce merge', async () => {
let ceas = Date.now();
const { s, kms } = await populat({ now: () => new Date(ceas) });
const n1 = s.nonce(); ceas += ms(119);
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer, { nonce: n1.nonce, now: new Date(ceas) }) })).version, 1, 'la 119 s nonce-ul e bun');
const n2 = s.nonce(); ceas += ms(121);
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer, { nonce: n2.nonce, now: new Date(ceas) }) }), 'NONCE_INVALID', 'la 121 s', { rule: 'nonce', mesaj: 'expired' });
s.close(); kms.close();
});
proba('refuz: prezentare facuta pentru alt serviciu -> UNAUTHORIZED cu regula identitatii audience', async () => {
const { s, kms } = await populat();
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer, { audience: ALT_AUD }) }), 'UNAUTHORIZED', 'alt public', { rule: 'authorization', identityRule: 'audience' });
s.close(); kms.close();
});
proba('refuz: prezentare veche (facuta acum 10 minute, cu nonce proaspat) -> UNAUTHORIZED cu regula identitatii freshness', async () => {
const { s, kms } = await populat();
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer, { now: new Date(Date.now() - ms(600)) }) }), 'UNAUTHORIZED', 'prezentare veche', { rule: 'authorization', identityRule: 'freshness' });
s.close(); kms.close();
});
proba('refuz: un strain prezinta credentialul deployer-ului cu cheia lui -> UNAUTHORIZED cu regula identitatii presenter', async () => {
const { s, kms } = await populat();
await refuza(() => s.get('db', { presentation: prez(s, K.stranger, C.deployer) }), 'UNAUTHORIZED', 'strainul cu credentialul altuia', { rule: 'authorization', identityRule: 'presenter' });
s.close(); kms.close();
});
proba('refuz: afirmatiile nu indeplinesc politica: cititorul nu citeste secretul deployer-ilor (authorization, grantul deployers refuza pe rule); credentialul auto-emis al strainului e refuzat (presentation: emitent necrezut)', async () => {
const { s, kms } = await populat();
const e = await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'cititorul', { rule: 'authorization', mesaj: MESAJ_NEAUTORIZAT });
const st = ultima(s);
adevarat(st.rules.includes('rule') && st.judged.some((g) => g.grant === 'deployers' && g.rules.includes('rule')), 'grantul deployers a refuzat pe rule, in jurnal: ' + JSON.stringify(st.judged));
adevarat(e.identityRules === undefined && e.grants === undefined, 'identitatea e valida, politica refuza: raspunsul nu poarta detaliul');
await refuza(() => s.get('db', { presentation: prez(s, K.stranger, C.stranger) }), 'UNAUTHORIZED', 'strainul cu credential auto-emis', { rule: 'authorization' });
adevarat(ultima(s).rules.includes('presentation'), 'strainul: emitent necrezut (presentation), in jurnal: ' + ultima(s).rules.join(','));
await refuza(() => s.put('nou', { presentation: prez(s, K.stranger, C.stranger), value: 'x' }), 'UNAUTHORIZED', 'strainul nu creeaza secrete', { rule: 'authorization' });
s.close(); kms.close();
});
proba('refuz: operatia nu e in grant: deployer-ul (get, metadata) nu poate put, rotate, revoke-version, grant, revoke (authorization)', async () => {
const { s, kms } = await populat();
const d = () => prez(s, K.deployer, C.deployer);
await refuza(() => s.put('db', { presentation: d(), value: 'alta' }), 'UNAUTHORIZED', 'put', { rule: 'authorization' });
await refuza(() => s.rotate('db', { presentation: d() }), 'UNAUTHORIZED', 'rotate', { rule: 'authorization' });
await refuza(() => s.revokeVersion('db', { presentation: d(), version: 1 }), 'UNAUTHORIZED', 'revoke-version', { rule: 'authorization' });
await refuza(() => s.grant('db', { presentation: d(), grant: { id: 'me', operations: ['put'], policy: POL('me', 'deployer') } }), 'UNAUTHORIZED', 'grant', { rule: 'authorization' });
await refuza(() => s.revoke('db', { presentation: d(), grantId: 'rotators' }), 'UNAUTHORIZED', 'revoke', { rule: 'authorization' });
egal((await s.get('db', { presentation: d() })).value.toString(), VALOARE, 'get ramane permis');
s.close(); kms.close();
});
proba('refuz: credential revocat in lista de stare -> UNAUTHORIZED cu regula identitatii credential-revoked; fara lista in dosarul de incredere starea e nejudecata si tot refuzat (status), si pentru admin', async () => {
const { s, kms, trust } = await populat();
await refuza(() => s.get('db', { presentation: prez(s, K.revoked, C.revoked) }), 'UNAUTHORIZED', 'credential revocat', { rule: 'authorization', identityRule: 'credential-revoked' });
// lista dispare din dosarul de incredere: starea nu mai poate fi judecata si NIMENI nu mai intra, nici adminul (nu "nerevocat fiindca nu stim")
fs.rmSync(path.join(trust, 'status-lists', 'lista.json'));
await refuza(() => s.get('db', { presentation: prez(s, K.revoked, C.revoked) }), 'UNAUTHORIZED', 'fara lista: starea nejudecata', { rule: 'authorization' });
adevarat(ultima(s).rules.includes('status'), 'fara lista: starea nejudecata: regula status in jurnal: ' + ultima(s).rules.join(','));
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer) }), 'UNAUTHORIZED', 'fara lista nici deployer-ul bun nu intra', { rule: 'authorization' });
adevarat(ultima(s).rules.includes('status'), 'deployer-ul: status in jurnal');
await refuza(() => s.put('alt', { presentation: prez(s, K.admin, C.admin), value: 'x' }), 'UNAUTHORIZED', 'fara lista nici adminul', { rule: 'authorization' });
adevarat(ultima(s).rules.includes('status'), 'adminul: status in jurnal');
s.close(); kms.close();
});
proba('refuz: credential fara lista de stare nu deschide nimic (status nejudecat); un grant cu requireStatus false e refuzat la scriere (INVALID_GRANT, grant-form, "waive")', async () => {
const { s, kms } = await populat();
await refuza(() => s.get('db', { presentation: prez(s, K.nostatus, C.nostatus) }), 'UNAUTHORIZED', 'fara lista de stare', { rule: 'authorization' });
adevarat(ultima(s).rules.includes('status'), 'fara lista de stare: trebuia refuzat pe status, in jurnal: ' + ultima(s).rules.join(','));
const g = { id: 'lax', operations: ['get'], policy: { ...POL('lax-v1', 'deployer'), requireStatus: false } };
await refuza(() => s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: g }), 'INVALID_GRANT', 'grant care scuteste starea', { rule: 'grant-form', mesaj: 'waive' });
await refuza(() => s.put('alt', { presentation: prez(s, K.admin, C.admin), value: 'v', grants: [g] }), 'INVALID_GRANT', 'la creare la fel', { rule: 'grant-form', mesaj: 'waive' });
adevarat(!fs.existsSync(path.join(s.dataDir, 'secrets', 'alt.json')), 'secretul refuzat nu a ramas pe disc');
s.close(); kms.close();
});
proba('delegare: telefonul deployer-ului citeste in scopul delegarii; o revocare pusa in dosarul de incredere il opreste (delegation-revoked); o delegare care nu acopera afirmatiile aratate e refuzata (delegation-claims)', async () => {
const { s, kms, trust } = await populat();
const r = await s.get('db', { presentation: prez(s, K.phone, C.deployer, { delegations: [DELEGARE] }) });
egal(r.value.toString(), VALOARE, 'telefonul citeste');
const j = jurnal(s.auditLog); const st = j[j.length - 1].proof.statement;
egal(st.holder, K.deployer.id, 'detinatorul din jurnal'); egal(st.presenter, K.phone.id, 'prezentatorul din jurnal');
await refuza(() => s.get('db', { presentation: prez(s, K.phone, C.deployer, { delegations: [DELEGARE_INGUSTA] }) }), 'UNAUTHORIZED', 'afirmatii in afara scopului', { rule: 'authorization', identityRule: 'delegation-claims' });
fs.writeFileSync(path.join(trust, 'revocations', 'telefon.json'), JSON.stringify(REVOCARE));
await refuza(() => s.get('db', { presentation: prez(s, K.phone, C.deployer, { delegations: [DELEGARE] }) }), 'UNAUTHORIZED', 'delegare revocata', { rule: 'authorization', identityRule: 'delegation-revoked' });
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer) })).version, 1, 'detinatorul insusi citeste in continuare');
s.close(); kms.close();
});
proba('rotatie: rotate face versiunea 2 cu o valoare noua; get fara versiune da 2; versiunea 1 cere versions any: deployer-ul (current) e refuzat (VERSION_NOT_IN_SCOPE), auditorul citeste 1 si 2; versiunea 7 nu exista (VERSION_NOT_FOUND)', async () => {
const { s, kms } = await populat();
const rot = await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator), bytes: 24, encoding: 'hex' });
egal(rot.version, 2, 'versiunea noua'); adevarat(!('value' in rot), 'rotate nu intoarce valoarea');
const r2 = await s.get('db', { presentation: prez(s, K.deployer, C.deployer) });
egal(r2.version, 2, 'curenta e 2'); egal(r2.current, 2, 'current');
adevarat(/^[0-9a-f]{48}$/.test(r2.value.toString()) && r2.value.toString() !== VALOARE, 'valoare noua, 24 de octeti hex');
egal(r2.metadata.rotated, true, 'metadata rotated');
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer), version: 1 }), 'VERSION_NOT_IN_SCOPE', 'deployer la versiunea 1', { rule: 'version-scope' });
egal((await s.get('db', { presentation: prez(s, K.auditor, C.auditor), version: 1 })).value.toString(), VALOARE, 'auditorul citeste versiunea 1 (valoarea veche)');
egal((await s.get('db', { presentation: prez(s, K.auditor, C.auditor), version: 2 })).value.toString(), r2.value.toString(), 'auditorul citeste versiunea 2');
await refuza(() => s.get('db', { presentation: prez(s, K.auditor, C.auditor), version: 7 }), 'VERSION_NOT_FOUND', 'versiunea 7', { rule: 'version-exists' });
await refuza(() => s.rotate('db', { presentation: prez(s, K.rotator, C.rotator), bytes: 8 }), 'INVALID_REQUEST', 'bytes sub 16', { rule: 'request-form' });
s.close(); kms.close();
});
proba('revoke-version: versiunea 1 revocata nu se mai citeste nici cu versions any (VERSION_REVOKED), textul cifrat si cheia invelita dispar de pe disc; a doua revocare refuzata; curenta revocata -> get fara versiune refuzat pana la un put nou', async () => {
const { s, kms } = await populat();
await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) });
const rv = await s.revokeVersion('db', { presentation: prez(s, K.rotator, C.rotator), version: 1 });
egal(rv.state, 'revoked', 'starea');
await refuza(() => s.get('db', { presentation: prez(s, K.auditor, C.auditor), version: 1 }), 'VERSION_REVOKED', 'auditorul la versiunea revocata', { rule: 'version-revoked' });
const f = JSON.parse(fs.readFileSync(path.join(s.dataDir, 'secrets', 'db.json'), 'utf8'));
egal(f.versions['1'].sealed, null, 'sealed sters'); egal(f.versions['1'].dek_wrapped, null, 'cheia invelita stearsa'); adevarat(typeof f.versions['2'].sealed === 'string', 'versiunea 2 ramane');
await refuza(() => s.revokeVersion('db', { presentation: prez(s, K.rotator, C.rotator), version: 1 }), 'VERSION_REVOKED', 'a doua revocare', { rule: 'version-revoked' });
await refuza(() => s.revokeVersion('db', { presentation: prez(s, K.rotator, C.rotator), version: 3 }), 'VERSION_NOT_FOUND', 'versiune inexistenta', { rule: 'version-exists' });
await s.revokeVersion('db', { presentation: prez(s, K.rotator, C.rotator), version: 2 });
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer) }), 'VERSION_REVOKED', 'curenta revocata', { rule: 'version-revoked' });
const p3 = await s.put('db', { presentation: prez(s, K.admin, C.admin), value: 'a-treia' });
egal(p3.version, 3, 'put dupa revocari');
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer) })).value.toString(), 'a-treia', 'citeste versiunea 3');
const m = await s.metadata('db', { presentation: prez(s, K.deployer, C.deployer) });
egal([m.versions['1'].state, m.versions['2'].state, m.versions['3'].state].join(','), 'revoked,revoked,enabled', 'starile in metadata');
s.close(); kms.close();
});
proba('grant/revoke: admin da cititorilor un grant get, cititorul citeste, admin il retrage, cititorul e refuzat; grant dublat (GRANT_EXISTS), revoke pe id inexistent (GRANT_NOT_FOUND), grant malformat (INVALID_GRANT); grant cu holders admite numai detinatorul numit', async () => {
const { s, kms } = await populat();
const a = () => prez(s, K.admin, C.admin);
await s.grant('db', { presentation: a(), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } });
egal((await s.get('db', { presentation: prez(s, K.reader, C.reader) })).value.toString(), VALOARE, 'cititorul citeste dupa grant');
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } }), 'GRANT_EXISTS', 'grant dublat', { rule: 'grant-exists' });
const rv = await s.revoke('db', { presentation: a(), grantId: 'readers' });
adevarat(!rv.grants.includes('readers'), 'grantul retras');
await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'cititorul dupa retragere', { rule: 'authorization' });
await refuza(() => s.revoke('db', { presentation: a(), grantId: 'readers' }), 'GRANT_NOT_FOUND', 'retragere a doua oara', { rule: 'grant-exists' });
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'x', operations: ['fly'], policy: POL('x', 'reader') } }), 'INVALID_GRANT', 'operatie necunoscuta', { rule: 'grant-form' });
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'x', operations: ['get'], policy: { id: 'x', trustedIssuers: [], require: [{ claim: 'role', equals: 'reader' }] } } }), 'INVALID_GRANT', 'politica fara emitenti', { rule: 'grant-form' });
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'x', operations: ['get'], policy: POL('x', 'reader'), holders: ['cineva'] } }), 'INVALID_GRANT', 'holders invalid', { rule: 'grant-form' });
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'x', operations: ['get'], policy: POL('x', 'reader'), versions: 'all' } }), 'INVALID_GRANT', 'versions invalid', { rule: 'grant-form' });
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'x', operations: ['get'], policy: POL('x', 'reader'), extra: 1 } }), 'INVALID_GRANT', 'camp necunoscut', { rule: 'grant-form' });
await s.grant('db', { presentation: a(), grant: { id: 'one-reader', operations: ['get'], holders: [K.reader.id], policy: POL('one-reader-v1', 'reader') } });
egal((await s.get('db', { presentation: prez(s, K.reader, C.reader) })).version, 1, 'detinatorul numit citeste');
await refuza(() => s.get('db', { presentation: prez(s, K.reader2, C.reader2) }), 'UNAUTHORIZED', 'alt cititor, cu aceleasi afirmatii, nu e numit', { rule: 'authorization' });
adevarat(ultima(s).judged.some((g) => g.grant === 'one-reader' && g.rules.includes('holder')), 'grantul one-reader a refuzat pe holder (jurnal): ' + JSON.stringify(ultima(s).judged));
s.close(); kms.close();
});
proba('list: admin vede toate secretele; deployer-ul vede numai secretele cu un grant care il admite; strainul e refuzat; o cerere fara prezentare e refuzata', async () => {
const { s, kms } = await populat();
egal((await s.list({ presentation: prez(s, K.admin, C.admin) })).secrets.join(','), 'api-key,db', 'admin');
egal((await s.list({ presentation: prez(s, K.deployer, C.deployer) })).secrets.join(','), 'db', 'deployer');
await refuza(() => s.list({ presentation: prez(s, K.stranger, C.stranger) }), 'UNAUTHORIZED', 'strainul', { rule: 'authorization' });
await refuza(() => s.list({}), 'PRESENTATION_MISSING', 'fara prezentare', { rule: 'presentation-form' });
const j = jurnal(s.auditLog); const st = j[j.length - 1].proof.statement;
egal(st.operation, 'list', 'auditat'); egal(st.secret, '*', 'secretul * la list');
s.close(); kms.close();
});
proba('secret inexistent: admin primeste SECRET_NOT_FOUND (secret-exists); deployer-ul primeste UNAUTHORIZED, fara sa afle daca exista', async () => {
const { s, kms } = await populat();
await refuza(() => s.get('nimic', { presentation: prez(s, K.admin, C.admin) }), 'SECRET_NOT_FOUND', 'admin', { rule: 'secret-exists' });
const e1 = await refuza(() => s.get('nimic', { presentation: prez(s, K.deployer, C.deployer) }), 'UNAUTHORIZED', 'deployer pe secret inexistent', { rule: 'authorization' });
const e2 = await refuza(() => s.get('api-key', { presentation: prez(s, K.deployer, C.deployer) }), 'UNAUTHORIZED', 'deployer pe secret existent fara grant', { rule: 'authorization' });
egal(e1.message.replace(/nimic/g, 'N'), e2.message.replace(/api-key/g, 'N'), 'acelasi raspuns, exista sau nu');
s.close(); kms.close();
});
proba('valoare: goala sau peste 64 KB refuzata (value-form); metadate nevalide refuzate; put pe secret existent cu grants refuzat (request-form)', async () => {
const { s, kms } = await populat();
const a = () => prez(s, K.admin, C.admin);
await refuza(() => s.put('v', { presentation: a(), value: '' }), 'INVALID_VALUE', 'goala', { rule: 'value-form' });
await refuza(() => s.put('v', { presentation: a(), value: Buffer.alloc(MAX_VALUE + 1, 1) }), 'VALUE_TOO_LARGE', 'peste limita', { rule: 'value-form' });
await refuza(() => s.put('v', { presentation: a(), value: 7 }), 'INVALID_VALUE', 'numar', { rule: 'value-form' });
await refuza(() => s.put('v', { presentation: a(), value: 'x', metadata: { 'bad name': 1 } }), 'INVALID_METADATA', 'nume de metadata', { rule: 'value-form' });
await refuza(() => s.put('v', { presentation: a(), value: 'x', metadata: { a: { b: 1 } } }), 'INVALID_METADATA', 'valoare de metadata', { rule: 'value-form' });
await refuza(() => s.put('db', { presentation: a(), value: 'x', grants: [] }), 'INVALID_REQUEST', 'grants pe secret existent', { rule: 'request-form' });
adevarat(!fs.existsSync(path.join(s.dataDir, 'secrets', 'v.json')), 'niciun fisier dupa refuzuri');
egal((await s.put('v', { presentation: a(), value: Buffer.alloc(MAX_VALUE, 2) })).version, 1, 'exact la limita trece');
s.close(); kms.close();
});
proba('stocare: valoarea nu apare pe disc (nici base64/hex), cu controlul pozitiv al scanerului; cheia de date e invelita de KMS; un grant adaugat in fisier cu mana e refuzat (SECRET_FILE_TAMPERED) numai adminului, si auditat; jurnalul KMS nu contine valoarea', async () => {
const { s, kms, dir } = await populat();
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer) })).value.toString(), VALOARE, 'o citire, ca sa existe si decrypt la KMS');
egal(cauta(dir, VALOARE).length, 0, 'valoarea nu e nicaieri pe disc: ' + cauta(dir, VALOARE).join(', '));
const momeala = path.join(dir, 'momeala.txt'); fs.writeFileSync(momeala, 'x' + Buffer.from(VALOARE).toString('base64') + 'y');
egal(cauta(dir, VALOARE).length, 1, 'controlul pozitiv: scanerul gaseste momeala');
fs.rmSync(momeala);
const f = JSON.parse(fs.readFileSync(path.join(dir, 'data', 'secrets', 'db.json'), 'utf8'));
adevarat(f.versions['1'].dek_wrapped.startsWith('aerekms:v1:'), 'cheia de date e un plic KMS');
adevarat(typeof f.mac === 'string' && f.mac.length === 64, 'fisierul poarta MAC');
const kmsLog = fs.readFileSync(path.join(dir, 'kms', 'audit.log'), 'utf8');
adevarat(/"op":"datakey"/.test(kmsLog) && /"op":"decrypt"/.test(kmsLog), 'jurnalul KMS are datakey si decrypt');
// manipulare: grantul deployers primeste put, fara MAC nou
f.grants[0].operations.push('put');
fs.writeFileSync(path.join(dir, 'data', 'secrets', 'db.json'), JSON.stringify(f));
const n0 = jurnal(s.auditLog).length;
await refuza(() => s.put('db', { presentation: prez(s, K.deployer, C.deployer), value: 'x' }), 'UNAUTHORIZED', 'deployer-ul nu afla ca fisierul e stricat', { rule: 'authorization' });
await refuza(() => s.metadata('db', { presentation: prez(s, K.admin, C.admin) }), 'SECRET_FILE_TAMPERED', 'adminul afla', { rule: 'storage' });
const j = jurnal(s.auditLog); egal(j.length, n0 + 2, 'ambele auditate'); adevarat(j[j.length - 1].proof.statement.rules.includes('storage'), 'regula storage in jurnal');
s.close(); kms.close();
});
proba('jurnal: fiecare operatie (reusita sau refuzata) e o intrare secret-access cu serviciul, secretul, versiunea, operatia, verdictul, regulile, grantul, hash-ul legaturii prezentarii; valoarea nu apare in jurnal, cu control pozitiv; lantul INTACT (verify-log cod 0)', async () => {
const { s, kms } = await populat();
const p = prez(s, K.deployer, C.deployer);
await s.get('db', { presentation: p });
const j = jurnal(s.auditLog);
egal(j.length, 3, 'put, put, get');
const st = j[2].proof.statement;
egal(j[2].proof.kind, `aere-proof-of-${KIND}-attestation`, 'felul plicului'); egal(st.kind, `aere-proof-of-${KIND}`, 'felul declaratiei');
egal(st.service, AUD, 'serviciul'); egal(st.secret, 'db', 'secretul'); egal(st.version, 1, 'versiunea'); egal(st.operation, 'get', 'operatia'); egal(st.verdict, 'allowed', 'verdictul');
egal(st.grant, 'deployers', 'grantul'); egal(st.holder, K.deployer.id, 'detinatorul'); egal(st.presenter, K.deployer.id, 'prezentatorul');
egal(st.presentationHash, '0x' + crypto.createHash('sha256').update(ID.canonical(p.binding), 'utf8').digest('hex'), 'hash-ul legaturii semnate');
adevarat(/^0x[0-9a-f]{64}$/.test(st.policyHash), 'hash-ul politicii grantului');
egal(j[2].proof.statementHash, '0x' + crypto.createHash('sha256').update(JSON.stringify(st), 'utf8').digest('hex'), 'statementHash-ul plicului');
egal(j[2].seq, 2, 'seq'); egal(j[2].prev, j[1].hash, 'prev leaga intrarea de dinainte');
await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'refuz de auditat');
const r = jurnal(s.auditLog)[3].proof.statement;
egal(r.verdict, 'refused', 'refuzul'); adevarat(r.rules.includes('authorization') && r.rules.includes('rule'), 'regulile refuzului: ' + r.rules.join(','));
egal(r.holder, K.reader.id, 'detinatorul pretins'); egal(r.grant, null, 'fara grant');
const text = fs.readFileSync(s.auditLog, 'utf8');
adevarat(!text.includes(VALOARE) && !text.includes(Buffer.from(VALOARE).toString('base64')), 'valoarea nu e in jurnal');
adevarat(('x' + Buffer.from(VALOARE).toString('base64')).includes(Buffer.from(VALOARE).toString('base64')), 'controlul pozitiv al cautarii');
const v = SC.verificaJurnal(jurnal(s.auditLog)); adevarat(v.ok, 'lantul intact'); egal(v.count, 4, 'patru intrari');
const cli = execFileSync(process.execPath, [SIDECAR_PATH, 'verify-log', '--log', s.auditLog], { encoding: 'utf8' });
adevarat(cli.includes('log INTACT: 4 entries'), 'verify-log al sidecar-ului: ' + cli.trim());
const cap = s.auditHead(); egal(cap.count, 4, 'capul'); egal(cap.head, jurnal(s.auditLog)[3].hash, 'head-ul e hash-ul ultimei intrari');
s.close(); kms.close();
});
proba('jurnal: dupa 100 de operatii amestecate lantul e intact, are exact numarul de intrari si capul se schimba la fiecare', async () => {
const { s, kms } = await populat();
const n0 = jurnal(s.auditLog).length; const capete = new Set([s.auditHead().head]);
let reusite = 0, refuzuri = 0;
for (let i = 0; i < 100; i++) {
const fel = i % 5;
try {
if (fel === 0) await s.put('db', { presentation: prez(s, K.admin, C.admin), value: 'v' + i });
else if (fel === 1) await s.get('db', { presentation: prez(s, K.deployer, C.deployer) });
else if (fel === 2) await s.get('db', { presentation: prez(s, K.reader, C.reader) });
else if (fel === 3) await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) });
else await s.metadata('db', { presentation: prez(s, K.auditor, C.auditor) });
reusite++;
} catch (e) { if (!(e instanceof SecretsError) || e.code !== 'UNAUTHORIZED') throw e; refuzuri++; }
const h = s.auditHead().head; adevarat(!capete.has(h), 'capul se schimba la operatia ' + i); capete.add(h);
}
egal(reusite, 80, 'reusite'); egal(refuzuri, 20, 'refuzuri');
const j = jurnal(s.auditLog); egal(j.length, n0 + 100, 'exact 100 de intrari noi');
const v = SC.verificaJurnal(j); adevarat(v.ok && v.count === n0 + 100, 'lantul intact dupa 100');
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer) })).version, 41, 'versiunea curenta dupa 20 put + 20 rotate');
s.close(); kms.close();
});
proba('jurnal: o mutatie al carei rand de audit nu se poate scrie NU ramane pe disc (put nou, rotate, grant), iar lantul ramane valid', async () => {
const { s, kms } = await populat();
const log = s.auditLog; const copie = log + '.copie'; fs.copyFileSync(log, copie);
const f = path.join(s.dataDir, 'secrets', 'db.json'); const inainte = fs.readFileSync(f);
fs.rmSync(log); fs.mkdirSync(log); // jurnalul devine un dosar: nimic nu se mai poate adauga
await refuza(() => s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) }), 'AUDIT_WRITE_FAILED', 'rotate fara rand de audit', { rule: 'storage' });
adevarat(fs.readFileSync(f).equals(inainte), 'rotirea nu a ramas pe disc');
await refuza(() => s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'g', operations: ['get'], policy: POL('g', 'reader') } }), 'AUDIT_WRITE_FAILED', 'grant fara rand de audit', { rule: 'storage' });
adevarat(fs.readFileSync(f).equals(inainte), 'grantul nu a ramas pe disc');
await refuza(() => s.put('nou', { presentation: prez(s, K.admin, C.admin), value: 'x' }), 'AUDIT_WRITE_FAILED', 'put nou fara rand de audit', { rule: 'storage' });
adevarat(!fs.existsSync(path.join(s.dataDir, 'secrets', 'nou.json')), 'secretul nou nu a ramas pe disc');
fs.rmSync(log, { recursive: true }); fs.copyFileSync(copie, log);
const v = SC.verificaJurnal(jurnal(log)); adevarat(v.ok, 'lantul de dinainte e valid');
egal((await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) })).version, 2, 'dupa ce jurnalul e la loc, rotirea merge si e versiunea 2');
s.close(); kms.close();
});
proba('plicuri: intrarile de put, get, metadata, rotate, revoke-version, grant, revoke, list si un refuz sunt VALID pentru verificatorul AIP-23 de referinta (SARIT fara verificator)', async () => {
if (!areVerificator) throw new Sarit(`verificatorul AIP-23 nu e la ${VERIFY}; dati AERE_VERIFY_PROOF=<verify-proof.mjs>`);
const { s, kms } = await populat();
await s.get('db', { presentation: prez(s, K.deployer, C.deployer) });
await s.metadata('db', { presentation: prez(s, K.deployer, C.deployer) });
await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) });
await s.revokeVersion('db', { presentation: prez(s, K.rotator, C.rotator), version: 1 });
await s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } });
await s.revoke('db', { presentation: prez(s, K.admin, C.admin), grantId: 'readers' });
await s.list({ presentation: prez(s, K.admin, C.admin) });
await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'refuz');
const j = jurnal(s.auditLog); const ops = j.map((e) => e.proof.statement.operation + ':' + e.proof.statement.verdict);
egal(ops.join(' '), 'put:allowed put:allowed get:allowed metadata:allowed rotate:allowed revoke-version:allowed grant:allowed revoke:allowed list:allowed get:refused', 'operatiile din jurnal');
const d = tmpDir('plic');
for (let i = 0; i < j.length; i++) {
const f = path.join(d, `${i}.json`); fs.writeFileSync(f, JSON.stringify(j[i].proof, null, 1));
let verdict = '?';
try { verdict = JSON.parse(execFileSync(process.execPath, [VERIFY, f, '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })).verdict; } catch (e) { try { verdict = JSON.parse(e.stdout || '').verdict; } catch { verdict = '?'; } }
egal(verdict, 'VALID', `plicul ${i} (${ops[i]})`);
}
// control negativ: un plic atins dupa hash e INVALID
const rau = JSON.parse(JSON.stringify(j[2].proof)); rau.statement.verdict = 'refused';
const fr = path.join(d, 'rau.json'); fs.writeFileSync(fr, JSON.stringify(rau));
let vr = '?'; try { vr = JSON.parse(execFileSync(process.execPath, [VERIFY, fr, '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })).verdict; } catch (e) { try { vr = JSON.parse(e.stdout || '').verdict; } catch { vr = '?'; } }
egal(vr, 'INVALID', 'plic atins dupa hash');
s.close(); kms.close();
});
proba('server: flux complet prin HTTP (health, nonce, put, get, metadata, rotate, revoke-version, grant, revoke, list, audit head); refuzurile poarta error, rule si identityRules; raspunsurile nu contin valoarea decat la get autorizat; GET pe o operatie -> 405; corp prea mare -> 413', async () => {
const { s, kms } = await nouServiciu();
const server = createServer({ secrets: s, maxBody: 65536 });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
const nonce = async () => (await cerere(port, 'POST', '/v1/nonce')).json.nonce;
const P = async (keys, cred, o = {}) => prez(s, keys, cred, { ...o, nonce: await nonce() });
const toate = [];
const post = async (p, body) => { const r = await cerere(port, 'POST', p, { body }); toate.push([p, r.text]); return r; };
try {
const h = await cerere(port, 'GET', '/v1/health'); egal(h.status, 200, 'health'); egal(h.json.audience, AUD, 'audienta in health');
const n = await cerere(port, 'POST', '/v1/nonce'); egal(n.status, 200, 'nonce'); adevarat(typeof n.json.nonce === 'string' && n.json.audience === AUD && typeof n.json.expiresAt === 'string', 'forma nonce-ului');
const b64 = Buffer.from(VALOARE).toString('base64');
const put = await post('/v1/secrets/db/put', { presentation: await P(K.admin, C.admin), value: b64, metadata: { owner: 'platform' }, grants: GRANTS() });
egal(put.status, 200, 'put: ' + put.text); egal(put.json.version, 1, 'put versiunea');
const get = await post('/v1/secrets/db/get', { presentation: await P(K.deployer, C.deployer) });
egal(get.status, 200, 'get: ' + get.text); egal(get.json.value, b64, 'valoarea base64'); egal(get.json.metadata.owner, 'platform', 'metadata');
const bad = await post('/v1/secrets/db/get', { presentation: await P(K.reader, C.reader) });
egal(bad.status, 403, 'cititorul 403'); egal(bad.json.error, 'UNAUTHORIZED', 'error'); egal(bad.json.rule, 'authorization', 'rule'); egal(Object.keys(bad.json).join(','), 'error,rule,message', 'raspunsul generic: numai error, rule, message'); egal(bad.json.message, MESAJ_NEAUTORIZAT, 'mesajul generic');
const alt = await post('/v1/secrets/db/get', { presentation: await P(K.deployer, C.deployer, { audience: ALT_AUD }) });
egal(alt.status, 403, 'alt public 403'); adevarat(alt.json.identityRules.includes('audience'), 'regula audience in raspuns');
const fara = await post('/v1/secrets/db/get', {}); egal(fara.status, 401, 'fara prezentare 401'); egal(fara.json.rule, 'presentation-form', 'regula');
const pr = await P(K.deployer, C.deployer); await post('/v1/secrets/db/get', { presentation: pr });
const replay = await post('/v1/secrets/db/get', { presentation: pr }); egal(replay.status, 401, 'replay 401'); egal(replay.json.rule, 'nonce', 'regula nonce');
const md = await post('/v1/secrets/db/metadata', { presentation: await P(K.deployer, C.deployer) }); egal(md.status, 200, 'metadata'); egal(md.json.current, 1, 'metadata current');
const rot = await post('/v1/secrets/db/rotate', { presentation: await P(K.rotator, C.rotator), bytes: 16 }); egal(rot.status, 200, 'rotate: ' + rot.text); egal(rot.json.version, 2, 'rotate versiunea');
const sc = await post('/v1/secrets/db/get', { presentation: await P(K.deployer, C.deployer), version: 1 }); egal(sc.status, 403, 'versiunea 1 fara any 403'); egal(sc.json.rule, 'version-scope', 'regula');
const rv = await post('/v1/secrets/db/revoke-version', { presentation: await P(K.rotator, C.rotator), version: 1 }); egal(rv.status, 200, 'revoke-version: ' + rv.text);
const gone = await post('/v1/secrets/db/get', { presentation: await P(K.auditor, C.auditor), version: 1 }); egal(gone.status, 410, 'versiunea revocata 410'); egal(gone.json.rule, 'version-revoked', 'regula');
const gr = await post('/v1/secrets/db/grant', { presentation: await P(K.admin, C.admin), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } }); egal(gr.status, 200, 'grant: ' + gr.text);
const ok2 = await post('/v1/secrets/db/get', { presentation: await P(K.reader, C.reader) }); egal(ok2.status, 200, 'cititorul dupa grant');
const dup = await post('/v1/secrets/db/grant', { presentation: await P(K.admin, C.admin), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } }); egal(dup.status, 409, 'grant dublat 409');
const rk = await post('/v1/secrets/db/revoke', { presentation: await P(K.admin, C.admin), grantId: 'readers' }); egal(rk.status, 200, 'revoke: ' + rk.text);
const nf = await post('/v1/secrets/db/revoke', { presentation: await P(K.admin, C.admin), grantId: 'readers' }); egal(nf.status, 404, 'revoke inexistent 404');
const ls = await post('/v1/list', { presentation: await P(K.admin, C.admin) }); egal(ls.status, 200, 'list'); egal(ls.json.secrets.join(','), 'db', 'lista');
const nx = await post('/v1/secrets/nimic/get', { presentation: await P(K.admin, C.admin) }); egal(nx.status, 404, 'inexistent 404'); egal(nx.json.rule, 'secret-exists', 'regula');
const head = await cerere(port, 'GET', '/v1/audit/head'); egal(head.status, 200, 'audit head'); adevarat(head.json.ok && head.json.count >= 14, 'capul: ' + head.text);
egal((await cerere(port, 'GET', '/v1/secrets/db/get')).status, 405, 'GET pe o operatie');
egal((await cerere(port, 'POST', '/v1/secrets/db/fly', { body: {} })).status, 404, 'operatie necunoscuta');
egal((await cerere(port, 'POST', '/v1/secrets/db/put', { body: '{' })).status, 400, 'JSON stricat');
egal((await cerere(port, 'POST', '/v1/secrets/db/put', { body: Buffer.alloc(70000, 0x20) })).status, 413, 'corp prea mare');
egal((await cerere(port, 'POST', '/v1/secrets/db/put', { body: { presentation: await P(K.admin, C.admin), value: 'nu-e-base64!' } })).status, 400, 'valoare care nu e base64');
for (const [p, text] of toate) if (!(p === '/v1/secrets/db/get' && text.includes('"value"') && text.includes(b64) && JSON.parse(text).version === 1)) adevarat(!text.includes(VALOARE) && !text.includes(b64), 'valoarea intr-un raspuns care nu e get autorizat: ' + p + ' ' + text.slice(0, 120));
adevarat(toate.some(([p, text]) => p === '/v1/secrets/db/get' && text.includes(b64)), 'controlul pozitiv: get-ul autorizat o contine');
} finally { await new Promise((r) => server.close(r)); s.close(); kms.close(); }
});
proba('server: pornit din linia de comanda (KMS in proces): nonce, put, get prin HTTP; stdout/stderr nu contin valoarea; refuza fara AERE_SECRETS_AUDIENCE, fara politica de admin, cu ambele KMS-uri, si nu tipareste cheia radacina', async () => {
const dir = tmpDir('cli');
fs.mkdirSync(path.join(dir, 'trust', 'status-lists'), { recursive: true });
fs.writeFileSync(path.join(dir, 'trust', 'status-lists', 'l.json'), JSON.stringify(LISTA));
fs.writeFileSync(path.join(dir, 'admin.json'), JSON.stringify(ADMIN_POLICY));
const root = rootHex();
const env = { ...envCurat(), AERE_SECRETS_AUDIENCE: AUD, AERE_SECRETS_ADMIN_POLICY: path.join(dir, 'admin.json'), AERE_SECRETS_DATA_DIR: path.join(dir, 'data'), AERE_SECRETS_TRUST_DIR: path.join(dir, 'trust'), AERE_KMS_ROOT_KEY: root, AERE_SECRETS_PORT: '0' };
const porneste = (e) => new Promise((resolve) => {
const c = spawn(process.execPath, [SERVER_PATH], { env: e, stdio: ['ignore', 'pipe', 'pipe'] });
let out = '', err = '', gata = false;
const fin = (port) => { if (!gata) { gata = true; resolve({ c, port, out: () => out, err: () => err }); } };
c.stdout.on('data', (d) => { out += d; const m = /listening on 127\.0\.0\.1:(\d+)/.exec(out); if (m) fin(Number(m[1])); });
c.stderr.on('data', (d) => { err += d; });
c.on('close', () => fin(null)); // close, nu exit: la exit stderr-ul poate fi inca pe drum
setTimeout(() => fin(null), 20000);
});
const srv = await porneste(env);
try {
adevarat(srv.port, 'serverul a pornit: ' + srv.err());
// cheia secp... radacina: nu apare in iesire; audienta da
adevarat(!srv.out().includes(root) && !srv.err().includes(root), 'cheia radacina nu e tiparita');
const nonce = async () => (await cerere(srv.port, 'POST', '/v1/nonce')).json.nonce;
const svcFals = { nonce: () => ({ nonce: 'x' }) };
const b64 = Buffer.from(VALOARE).toString('base64');
const put = await cerere(srv.port, 'POST', '/v1/secrets/db/put', { body: { presentation: prez(svcFals, K.admin, C.admin, { nonce: await nonce() }), value: b64, grants: GRANTS() } });
egal(put.status, 200, 'put prin procesul pornit: ' + put.text);
const get = await cerere(srv.port, 'POST', '/v1/secrets/db/get', { body: { presentation: prez(svcFals, K.deployer, C.deployer, { nonce: await nonce() }) } });
egal(get.status, 200, 'get: ' + get.text); egal(get.json.value, b64, 'valoarea');
const rau = await cerere(srv.port, 'POST', '/v1/secrets/db/get', { body: { presentation: prez(svcFals, K.reader, C.reader, { nonce: await nonce() }) } });
egal(rau.status, 403, 'refuz prin procesul pornit');
await new Promise((r) => setTimeout(r, 200));
adevarat(!srv.out().includes(VALOARE) && !srv.out().includes(b64) && !srv.err().includes(VALOARE) && !srv.err().includes(b64), 'valoarea nu e in stdout/stderr');
adevarat(cauta(dir, VALOARE).length === 0, 'valoarea nu e pe discul procesului');
} finally { srv.c.kill(); await new Promise((r) => srv.c.on('exit', r)); }
const refuzaPornirea = async (e, cod) => { const r = await porneste(e); if (r.port) { r.c.kill(); throw new Esec('a pornit desi trebuia refuzat: ' + cod); } adevarat(r.err().includes(cod), `refuz asteptat ${cod}: ${r.err().trim()}`); adevarat(!r.err().includes(root), 'cheia radacina nu e in refuz'); };
await refuzaPornirea({ ...env, AERE_SECRETS_AUDIENCE: '' }, 'AUDIENCE_MISSING');
await refuzaPornirea({ ...env, AERE_SECRETS_ADMIN_POLICY: '' }, 'ADMIN_POLICY_MISSING');
await refuzaPornirea({ ...env, AERE_KMS_URL: 'http://127.0.0.1:1', AERE_KMS_TOKEN: 'x'.repeat(32) }, 'KMS_AMBIGUOUS');
await refuzaPornirea({ ...env, AERE_KMS_ROOT_KEY: '' }, 'KMS_MISSING');
const cfg = configFromEnv(env); egal(cfg.kms.mode, 'in-process', 'configFromEnv: KMS in proces'); egal(cfg.nonceTtlS, 120, 'TTL implicit');
});
proba('server: KMS la distanta (serverul pq-kms pornit in proces, kmsClientHttp): put/get merg, jurnalul KMS are datakey si decrypt fara valori, token gresit -> KMS_INVALID_TOKEN (kms), KMS oprit -> KMS_UNREACHABLE', async () => {
const kdir = tmpDir('kmsd');
const kms = openKms({ dataDir: kdir, rootKey: rootHex() });
const token = crypto.randomBytes(24).toString('base64url') + 'x'.repeat(8);
const ks = createKmsServer({ kms, token, maxBody: 65536 });
await new Promise((r) => ks.listen(0, '127.0.0.1', r));
const url = `http://127.0.0.1:${ks.address().port}`;
const dir = tmpDir('rem');
fs.mkdirSync(path.join(dir, 'trust', 'status-lists'), { recursive: true }); fs.writeFileSync(path.join(dir, 'trust', 'status-lists', 'l.json'), JSON.stringify(LISTA));
try {
const client = kmsClientHttp({ url, token });
const s = await openSecrets({ dataDir: path.join(dir, 'data'), kms: client, audience: AUD, adminPolicy: ADMIN_POLICY, trustDir: path.join(dir, 'trust') });
egal(kms.getKey('secrets').type, 'encrypt', 'cheia creata prin HTTP in KMS');
await s.put('db', { presentation: prez(s, K.admin, C.admin), value: VALOARE, grants: GRANTS() });
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer) })).value.toString(), VALOARE, 'get prin KMS la distanta');
const log = fs.readFileSync(path.join(kdir, 'audit.log'), 'utf8');
adevarat(/"op":"datakey","key":"secrets"/.test(log) && /"op":"decrypt","key":"secrets"/.test(log), 'jurnalul KMS');
adevarat(!log.includes(VALOARE) && cauta(kdir, VALOARE).length === 0, 'valoarea nu e la KMS');
s.close();
let e; try { await openSecrets({ dataDir: path.join(dir, 'data'), kms: kmsClientHttp({ url, token: 'y'.repeat(40) }), audience: AUD, adminPolicy: ADMIN_POLICY }); } catch (x) { e = x; }
egal(e && e.code, 'KMS_INVALID_TOKEN', 'token gresit'); egal(e && e.rule, 'kms', 'regula kms');
let e2; try { kmsClientHttp({ url, token: 'scurt' }); } catch (x) { e2 = x; } egal(e2 && e2.code, 'KMS_TOKEN_INVALID', 'token scurt refuzat la construire');
} finally { await new Promise((r) => ks.close(r)); }
let e3; try { await openSecrets({ dataDir: path.join(dir, 'data'), kms: kmsClientHttp({ url, token }), audience: AUD, adminPolicy: ADMIN_POLICY }); } catch (x) { e3 = x; }
egal(e3 && e3.code, 'KMS_UNREACHABLE', 'KMS oprit');
kms.close();
});
// ================================================================================== revizuirea adversariala din 18 oct (B8): cele 14 puncte
proba('cererea (substitutie): o prezentare semnata pentru "get db" nu deschide revoke pe db, get pe db2, nici get db cu un camp adaugat; una fara cerere e refuzata; regula identitatii request; randul poarta digestul cererii primite', async () => {
const { s, kms } = await populat();
await s.put('db2', { presentation: prez(s, K.admin, C.admin), value: 'valoarea-lui-db2', grants: GRANTS() });
const pentruGet = (k, c) => prez(s, k, c, { pentru: ['get', 'db', {}] });
await refuza(() => s.revoke('db', { presentation: pentruGet(K.admin, C.admin), grantId: 'deployers' }), 'UNAUTHORIZED', 'revoke cu prezentarea adminului pentru get', { rule: 'authorization', identityRule: 'request' });
egal(ultima(s).requestHash, requestHash({ audience: AUD, operation: 'revoke', secret: 'db', body: { grantId: 'deployers' } }), 'randul de refuz poarta digestul cererii PRIMITE');
await refuza(() => s.get('db2', { presentation: pentruGet(K.deployer, C.deployer) }), 'UNAUTHORIZED', 'get pe db2 cu prezentarea pentru db', { rule: 'authorization', identityRule: 'request' });
await refuza(() => s.get('db', { presentation: pentruGet(K.auditor, C.auditor), version: 1 }), 'UNAUTHORIZED', 'get db cu version adaugat', { rule: 'authorization', identityRule: 'request' });
const fara = ID.present({ credential: C.deployer.credential, disclosures: C.deployer.disclosures, reveal: ['role', 'tenant'], presenter: K.deployer, audience: AUD, nonce: s.nonce().nonce });
await refuza(() => s.get('db', { presentation: fara }), 'UNAUTHORIZED', 'o prezentare fara cerere (forma de dinainte)', { rule: 'authorization', identityRule: 'request', mesaj: 'identity rules: request' });
const bun = await s.get('db', { presentation: pentruGet(K.deployer, C.deployer) });
egal(bun.value.toString(), VALOARE, 'prezentarea semnata pentru get db deschide get db');
egal(ultima(s).requestHash, requestHash({ audience: AUD, operation: 'get', secret: 'db', body: {} }), 'randul permis poarta digestul cererii');
egal((await s.metadata('db', { presentation: prez(s, K.admin, C.admin) })).grants.map((g) => g.id).join(','), 'deployers,rotators,auditors', 'grantul deployers nu a fost scos');
s.close(); kms.close();
});
proba('cererea prin HTTP: corpul unui get capturat, depus pe revoke-version cu version 1 adaugat -> 403 (request); un put cu valoarea schimbata in drum -> 403; versiunea 1 ramane', async () => {
const { s, kms } = await populat();
const server = createServer({ secrets: s, maxBody: 65536 });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
const nonce = async () => (await cerere(port, 'POST', '/v1/nonce')).json.nonce;
try {
const corp = { presentation: prez(null, K.rotator, C.rotator, { nonce: await nonce(), pentru: ['get', 'db', {}, 'base64'] }) };
const r = await cerere(port, 'POST', '/v1/secrets/db/revoke-version', { body: { ...corp, version: 1 } });
egal(r.status, 403, 'revoke-version cu corpul de get: ' + r.text); adevarat(r.json.identityRules.includes('request'), 'regula request in raspuns: ' + r.text);
const a = Buffer.from('valoarea-adminului').toString('base64'), b = Buffer.from('valoarea-atacatorului').toString('base64');
const pPut = prez(null, K.admin, C.admin, { nonce: await nonce(), pentru: ['put', 'db', { value: a }, 'base64'] });
const r2 = await cerere(port, 'POST', '/v1/secrets/db/put', { body: { presentation: pPut, value: b } });
egal(r2.status, 403, 'put cu valoarea schimbata: ' + r2.text); adevarat(r2.json.identityRules.includes('request'), 'regula request');
const m = await s.metadata('db', { presentation: prez(s, K.admin, C.admin) });
egal(m.versions['1'].state, 'enabled', 'versiunea 1 nu a fost revocata'); egal(m.current, 1, 'nicio versiune noua');
const ok = await cerere(port, 'POST', '/v1/secrets/db/put', { body: { presentation: prez(null, K.admin, C.admin, { nonce: await nonce(), pentru: ['put', 'db', { value: a }, 'base64'] }), value: a } });
egal(ok.status, 200, 'acelasi put, cu valoarea semnata: ' + ok.text); egal(ok.json.version, 2, 'versiunea 2');
} finally { await new Promise((r) => server.close(r)); s.close(); kms.close(); }
});
proba('atenuare: un ne-admin cu grant face numai grant-uri mai inguste (operatii fara grant/revoke, versiuni, emitenti, regulile politicii, varsta, holders, delegates), si retrage numai ce a facut el (grant-scope)', async () => {
const { s, kms } = await populat();
const a = () => prez(s, K.admin, C.admin), gr = () => prez(s, K.granter, C.granter);
const PG = { id: 'granters-v1', trustedIssuers: [issuer.id], require: [{ claim: 'role', equals: 'granter' }] };
await s.grant('db', { presentation: a(), grant: { id: 'granters', operations: ['get', 'grant', 'metadata', 'revoke'], policy: PG } });
const ingust = (o = {}) => ({ id: 'g-' + crypto.randomBytes(3).toString('hex'), operations: ['get'], holders: [K.granter.id], policy: { ...PG, id: 'ingust', require: [...PG.require, { claim: 'tenant', equals: 'acme' }] }, ...o });
const nu = async (g, ce, mesaj) => refuza(() => s.grant('db', { presentation: gr(), grant: g }), 'GRANT_NOT_ATTENUATED', ce, { rule: 'grant-scope', mesaj });
await nu(ingust({ operations: ['put'] }), 'put, pe care grantul lui nu il are', 'operations');
await nu(ingust({ operations: ['get', 'grant'] }), 'grant mai departe', 'without grant and revoke');
await nu(ingust({ operations: ['revoke'] }), 'revoke mai departe', 'without grant and revoke');
await nu(ingust({ versions: 'any' }), 'versions any dintr-un grant current', 'older versions');
await nu(ingust({ policy: { ...PG, id: 'alt', trustedIssuers: [issuer.id, altIssuer.id] } }), 'un emitent in plus', 'issuers');
await nu(ingust({ policy: { id: 'lax', trustedIssuers: [issuer.id], require: [{ claim: 'tenant', equals: 'acme' }] } }), 'regula role=granter scoasa', 'every rule');
await nu(ingust({ policy: { ...PG, id: 'vechi', maxAgeS: 600 } }), 'o varsta maxima mai mare', 'maximum age');
// auto-acordarea din revizuire (9a) si usa din spate cu emitent strain (9b): refuzate
await nu({ id: 'eu-tot', operations: ['put', 'get', 'metadata', 'rotate', 'revoke-version', 'grant', 'revoke'], versions: 'any', policy: POL('eu-tot', 'granter') }, '9a: toate operatiile', 'operations');
await nu({ id: 'backdoor', operations: ['get'], policy: { id: 'bd', trustedIssuers: [altIssuer.id], require: [{ claim: 'role', present: true }] } }, '9b: emitent strain', 'issuers');
const bun = ingust({ id: 'al-meu' });
egal((await s.grant('db', { presentation: gr(), grant: bun })).grant, 'al-meu', 'un grant mai ingust trece');
const m = await s.metadata('db', { presentation: a() }); const g = m.grants.find((x) => x.id === 'al-meu');
egal(g.createdBy, K.granter.id, 'grantul poarta cine l-a facut'); egal(m.grants.find((x) => x.id === 'deployers').createdBy, K.admin.id, 'grantul de la creare: adminul');
await refuza(() => s.revoke('db', { presentation: gr(), grantId: 'deployers' }), 'GRANT_NOT_OWNED', 'un grant al adminului', { rule: 'grant-scope' });
egal((await s.revoke('db', { presentation: gr(), grantId: 'al-meu' })).revoked, 'al-meu', 'grantul lui il retrage');
// un grant numai cu `grant` nu mai poate da nimic: operatiile lui fara grant si revoke sunt o multime goala
await s.grant('db', { presentation: a(), grant: { id: 'doar-grant', operations: ['grant'], policy: { ...PG, id: 'dg', require: [{ claim: 'role', equals: 'granter' }, { claim: 'tenant', equals: 'acme' }] } } });
// holders si delegates: pe un secret al carui grant de origine ii numeste
await s.put('h', { presentation: a(), value: 'v', grants: [{ id: 'gh', operations: ['get', 'grant'], holders: [K.granter.id], delegates: false, policy: PG }] });
const pe = (g) => s.grant('h', { presentation: gr(), grant: g });
await refuza(() => pe({ id: 'x1', operations: ['get'], policy: PG }), 'GRANT_NOT_ATTENUATED', 'fara holders cand originea ii numeste', { rule: 'grant-scope', mesaj: 'holders' });
await refuza(() => pe({ id: 'x2', operations: ['get'], holders: [K.granter.id], policy: PG }), 'GRANT_NOT_ATTENUATED', 'cu delegati cand originea ii opreste', { rule: 'grant-scope', mesaj: 'delegates' });
egal((await pe({ id: 'x3', operations: ['get'], holders: [K.granter.id], delegates: false, policy: PG })).grant, 'x3', 'holders inclusi si delegatii opriti: trece');
// adminul nu e atenuat: da si grant/revoke
egal((await s.grant('db', { presentation: a(), grant: { id: 'admin-da', operations: ['grant', 'revoke', 'get'], versions: 'any', policy: POL('ad', 'reader') } })).grant, 'admin-da', 'adminul da grant si revoke');
adevarat(jurnal(s.auditLog).filter((e) => e.proof.statement.rules.includes('grant-scope')).length >= 11, 'refuzurile grant-scope sunt in jurnal');
s.close(); kms.close();
});
proba('raspunsul generic: un refuz de autorizare e acelasi, octet cu octet, pentru un secret cu 3 grant-uri, unul cu 0 si unul inexistent (biblioteca si HTTP); detaliul (grant-urile judecate, regulile) e numai in jurnal', async () => {
const { s, kms } = await populat();
// tot ce poarta eroarea: mesajul (propriu, neenumerabil) si fiecare camp enumerabil, cu valoarea lui
const forma = (e) => JSON.stringify({ message: e.message, ...Object.fromEntries(Object.entries(e).sort(([x], [y]) => (x < y ? -1 : 1))) });
const e = []; const judecate = [];
for (const n of ['db', 'api-key', 'nu-exista']) { e.push(await refuza(() => s.get(n, { presentation: prez(s, K.stranger, C.stranger) }), 'UNAUTHORIZED', 'strainul pe ' + n, { rule: 'authorization', mesaj: MESAJ_NEAUTORIZAT })); judecate.push(ultima(s).judged.map((x) => x.grant).join(',')); }
egal(forma(e[1]), forma(e[0]), 'db (3 grant-uri) si api-key (0)'); egal(forma(e[2]), forma(e[0]), 'db si un nume inexistent');
const rot = await refuza(() => s.rotate('db', { presentation: prez(s, K.deployer, C.deployer) }), 'UNAUTHORIZED', 'deployer-ul rotate (grant pe get, nu pe rotate)', { rule: 'authorization' });
const cit = await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'cititorul (grant potrivit pe get, regula picata)', { rule: 'authorization' });
egal(forma(rot), forma(e[0]), 'rotate'); egal(forma(cit), forma(e[0]), 'regula picata');
egal(judecate.join(' | '), 'admin,deployers,auditors | admin | admin', 'jurnalul are grant-urile judecate pentru fiecare');
const server = createServer({ secrets: s, maxBody: 65536 });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
try {
const P = async (k, c) => prez(null, k, c, { nonce: (await cerere(port, 'POST', '/v1/nonce')).json.nonce });
const r = [];
for (const n of ['db', 'api-key', 'nu-exista']) r.push(await cerere(port, 'POST', `/v1/secrets/${n}/get`, { body: { presentation: await P(K.stranger, C.stranger) } }));
r.push(await cerere(port, 'POST', '/v1/secrets/db/get', { body: { presentation: await P(K.reader, C.reader) } }));
for (const x of r) { egal(x.status, 403, 'HTTP 403'); egal(x.text, r[0].text, 'acelasi corp, octet cu octet'); }
adevarat(!r[0].text.includes('deployers') && !r[0].text.includes('grants'), 'raspunsul nu numeste grant-uri: ' + r[0].text);
} finally { await new Promise((q) => server.close(q)); s.close(); kms.close(); }
});
proba('jurnalul unui refuz: un id se scrie ca holder/presenter numai VERIFICAT; cele pretinse ca claimedHolder/claimedPresenter numai in forma aere-id; un anonim cu 100.000 de caractere scrie un rand sub 2 KB', async () => {
const { s, kms } = await populat();
const fals = { v: 1, kind: 'aere-presentation', credential: { statement: { holder: { id: 'A'.repeat(100000) } } }, disclosures: [], delegations: [], binding: { nonce: 'nu-conteaza', presenter: { id: K.admin.id } } };
await refuza(() => s.get('db', { presentation: fals }), 'NONCE_INVALID', 'anonimul', { rule: 'nonce' });
const st = ultima(s); const linie = fs.readFileSync(s.auditLog, 'utf8').split('\n').filter(Boolean).pop();
egal(st.holder, null, 'detinatorul neverificat nu e scris ca holder'); egal(st.presenter, null, 'adminul pretins nu e scris ca presenter');
egal(st.claimedHolder, undefined, 'un id pretins de alta forma nu intra'); egal(st.claimedPresenter, K.admin.id, 'id-ul pretins, numit pretins');
adevarat(Buffer.byteLength(linie) < 2048, 'randul are ' + Buffer.byteLength(linie) + ' octeti');
await refuza(() => s.get('db', { presentation: prez(s, K.stranger, C.deployer) }), 'UNAUTHORIZED', 'strainul cu credentialul deployer-ului', { rule: 'authorization', identityRule: 'presenter' });
const s2 = ultima(s);
egal(s2.holder, null, 'semnatura picata: holder nescris'); egal(s2.claimedHolder, K.deployer.id, 'detinatorul pretins'); egal(s2.claimedPresenter, K.stranger.id, 'prezentatorul pretins');
await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'cititorul, refuzat de politica', { rule: 'authorization' });
const s3 = ultima(s);
egal(s3.holder, K.reader.id, 'identitatea verificata: holder'); egal(s3.presenter, K.reader.id, 'presenter'); egal(s3.claimedHolder, undefined, 'fara pretinse');
s.close(); kms.close();
});
proba('concurenta cu KMS-ul la distanta: 8 put-uri simultane -> versiunile 2..9, distincte si citibile; grant si rotate simultane -> amandoua pe disc', async () => {
const kdir = tmpDir('kmsc'); const kmsR = openKms({ dataDir: kdir, rootKey: rootHex() });
const token = crypto.randomBytes(24).toString('base64url') + 'x'.repeat(8);
const ks = createKmsServer({ kms: kmsR, token, maxBody: 65536 });
await new Promise((r) => ks.listen(0, '127.0.0.1', r));
try {
const { s } = await nouServiciu({ kms: kmsClientHttp({ url: `http://127.0.0.1:${ks.address().port}`, token }) });
await s.put('db', { presentation: prez(s, K.admin, C.admin), value: VALOARE, grants: GRANTS() });
const rs = await Promise.all(Array.from({ length: 8 }, (_, i) => s.put('db', { presentation: prez(s, K.admin, C.admin), value: 'conc-' + i })));
egal(rs.map((r) => r.version).sort((x, y) => x - y).join(','), '2,3,4,5,6,7,8,9', 'versiunile intoarse');
const valori = new Set();
for (let v = 2; v <= 9; v++) valori.add((await s.get('db', { presentation: prez(s, K.admin, C.admin), version: v })).value.toString());
egal([...valori].sort().join(','), Array.from({ length: 8 }, (_, i) => 'conc-' + i).join(','), 'toate cele 8 valori citibile, fiecare in versiunea ei');
const [g, r] = await Promise.all([s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } }), s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) })]);
const m = await s.metadata('db', { presentation: prez(s, K.admin, C.admin) });
egal(r.version, 10, 'rotirea'); egal(m.current, 10, 'curenta pe disc'); adevarat(m.grants.some((x) => x.id === 'readers') && g.grants.includes('readers'), 'grantul a supravietuit rotirii');
s.close();
} finally { await new Promise((r) => ks.close(r)); kmsR.close(); }
});
proba('lacatul dosarului: a doua deschidere a aceluiasi dosar e refuzata (DATA_DIR_LOCKED), din acelasi proces si din altul; dupa close se deschide; lacatul unui proces mort -> DATA_DIR_LOCK_STALE, preluat numai cu staleLock take', async () => {
const { s, dir, kms, trust } = await nouServiciu();
const o = { dataDir: path.join(dir, 'data'), kms, audience: AUD, adminPolicy: ADMIN_POLICY, trustDir: trust };
const lacat = path.join(dir, 'data', 'service.lock');
let e; try { await deschideSecrete(o); } catch (x) { e = x; }
egal(e && e.code, 'DATA_DIR_LOCKED', 'a doua deschidere in acelasi proces');
// alt proces, cu alt KMS: lacatul se judeca inaintea KMS-ului
const copil = path.join(tmpDir('copil'), 'deschide.mjs');
fs.writeFileSync(copil, `import { openSecrets } from ${JSON.stringify(new URL('../secrete.mjs', import.meta.url).href)};\nimport { openKms } from ${JSON.stringify(new URL('../../pq-kms/kms.mjs', import.meta.url).href)};\n`
+ `try { await openSecrets({ dataDir: process.argv[2], kms: openKms({ dataDir: process.argv[3], rootKey: '11'.repeat(32) }), audience: ${JSON.stringify(AUD)}, adminPolicy: ${JSON.stringify(ADMIN_POLICY)} }); console.log('DESCHIS'); } catch (e) { console.log(e.code); }\n`);
const alt = spawnSync(process.execPath, [copil, path.join(dir, 'data'), tmpDir('kmscopil')], { encoding: 'utf8' });
egal(alt.stdout.trim(), 'DATA_DIR_LOCKED', 'un alt proces: ' + alt.stderr);
s.close(); adevarat(!fs.existsSync(lacat), 'close scoate lacatul');
const s2 = await deschideSecrete(o); s2.close();
const mort = spawnSync(process.execPath, ['-e', 'process.stdout.write(String(process.pid))'], { encoding: 'utf8' }); const pid = Number(mort.stdout);
adevarat(Number.isSafeInteger(pid) && pid > 0, 'pid-ul unui proces terminat');
fs.writeFileSync(lacat, JSON.stringify({ pid, host: os.hostname(), since: iso(Date.now() - 60000) }));
e = null; try { await deschideSecrete(o); } catch (x) { e = x; }
egal(e && e.code, 'DATA_DIR_LOCK_STALE', 'lacatul unui proces mort'); adevarat(e.message.includes(String(pid)), 'il numeste: ' + e.message);
const s3 = await deschideSecrete({ ...o, staleLock: 'take' });
egal(s3.tookStaleLock && s3.tookStaleLock.pid, pid, 'preluat si spus'); egal(JSON.parse(fs.readFileSync(lacat, 'utf8')).pid, process.pid, 'lacatul e acum al acestui proces');
s3.close(); kms.close();
});
proba('JSON adanc: o legatura cu 30.000 de liste imbricate (biblioteca si HTTP) si un corp cu 25.000 de valori -> REQUEST_TOO_COMPLEX (request-form), auditat; un camp imbricat de 20 de niveluri trece', async () => {
const { s, kms } = await populat();
let adanc = []; for (let i = 0; i < 30000; i++) adanc = [adanc];
const n0 = jurnal(s.auditLog).length;
await refuza(() => s.get('db', { presentation: { v: 1, kind: 'aere-presentation', credential: {}, disclosures: [], delegations: [], binding: { nonce: s.nonce().nonce, adanc } } }), 'REQUEST_TOO_COMPLEX', 'binding adanc', { rule: 'request-form' });
egal(jurnal(s.auditLog).length, n0 + 1, 'refuzul e in jurnal'); egal(ultima(s).rules.join(','), 'request-form', 'regula numita'); egal(ultima(s).verdict, 'refused', 'refused');
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer), extra: Array.from({ length: 25000 }, () => 0) }), 'REQUEST_TOO_COMPLEX', '25.000 de valori', { rule: 'request-form' });
let douazeci = 'x'; for (let i = 0; i < 20; i++) douazeci = { a: douazeci };
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer), extra: douazeci })).value.toString(), VALOARE, 'un camp de 20 de niveluri, semnat in cerere, trece');
const server = createServer({ secrets: s, maxBody: 262144 });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
try {
const n1 = jurnal(s.auditLog).length; const n = (await cerere(port, 'POST', '/v1/nonce')).json.nonce;
const corp = `{"presentation":{"v":1,"kind":"aere-presentation","credential":{},"disclosures":[],"delegations":[],"binding":{"nonce":"${n}","adanc":${'['.repeat(30000) + ']'.repeat(30000)}}}}`;
const r = await cerere(port, 'POST', '/v1/secrets/db/get', { body: corp });
egal(r.status, 400, 'HTTP 400: ' + r.text.slice(0, 120)); egal(r.json.error, 'REQUEST_TOO_COMPLEX', 'error'); egal(r.json.rule, 'request-form', 'rule');
egal(jurnal(s.auditLog).length, n1 + 1, 'si prin HTTP refuzul e in jurnal'); egal((await cerere(port, 'GET', '/v1/health')).status, 200, 'serverul viu');
} finally { await new Promise((q) => server.close(q)); s.close(); kms.close(); }
egal(MAX_JSON_DEPTH, 32, 'limita de adancime');
});
proba('nume si chei rezervate: con, nul, aux, prn, com0-9, lpt0-9 refuzate ca nume de secret (secret-name); __proto__, constructor, prototype refuzate ca nume de metadate (value-form)', async () => {
const { s, kms } = await populat();
const a = () => prez(s, K.admin, C.admin);
for (const n of ['con', 'nul', 'aux', 'prn', 'com1', 'com0', 'lpt9']) await refuza(() => s.put(n, { presentation: a(), value: 'v' }), 'INVALID_SECRET_NAME', n, { rule: 'secret-name', mesaj: 'reserved by Windows' });
egal((await s.put('console', { presentation: a(), value: 'v' })).version, 1, 'console nu e rezervat');
adevarat(!['con', 'nul', 'aux'].some((n) => fs.existsSync(path.join(s.dataDir, 'secrets', n + '.json'))), 'niciun fisier rezervat');
for (const k of ['__proto__', 'constructor', 'prototype']) await refuza(() => s.put('meta', { presentation: a(), value: 'v', metadata: JSON.parse(`{"${k}":"x","a":1}`) }), 'INVALID_METADATA', k, { rule: 'value-form', mesaj: 'reserved' });
adevarat(!fs.existsSync(path.join(s.dataDir, 'secrets', 'meta.json')), 'niciun secret cu metadate rezervate');
s.close(); kms.close();
});
proba('mesajele nu reflecta intrarea neverificata: o versiune de 5000 de caractere -> VERSION_NOT_FOUND cu "(not an integer)", mesaj scurt; grant-ul cu id-ul admin e refuzat (rezervat politicii de admin)', async () => {
const { s, kms } = await populat();
const e = await refuza(() => s.get('db', { presentation: prez(s, K.auditor, C.auditor), version: 'X'.repeat(5000) }), 'VERSION_NOT_FOUND', 'versiunea uriasa', { rule: 'version-exists', mesaj: '(not an integer)' });
adevarat(e.message.length < 120 && !e.message.includes('XXXX'), 'mesajul are ' + e.message.length + ' caractere');
await refuza(() => s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'admin', operations: ['get'], policy: POL('fals-admin', 'reader') } }), 'INVALID_GRANT', 'grant admin', { rule: 'grant-form', mesaj: 'reserved' });
await refuza(() => s.put('x', { presentation: prez(s, K.admin, C.admin), value: 'v', grants: [{ id: 'admin', operations: ['get'], policy: POL('fa', 'reader') }] }), 'INVALID_GRANT', 'grant admin la creare', { rule: 'grant-form' });
s.close(); kms.close();
});
proba('plafonul grant-urilor: 33 la creare -> TOO_MANY_GRANTS (grant-limit), 32 trec si al 33-lea grant e refuzat; un get prin al 32-lea grant verifica prezentarea de doua ori (identitatea, o pereche emitent-varsta), nu de 34', async () => {
const { s, kms } = await populat();
const a = () => prez(s, K.admin, C.admin);
const multe = (n) => Array.from({ length: n }, (_, i) => ({ id: 'g' + i, operations: ['get'], policy: POL('p' + i, 'rol-' + i) }));
await refuza(() => s.put('greu', { presentation: a(), value: 'v', grants: multe(MAX_GRANTS + 1) }), 'TOO_MANY_GRANTS', '33 la creare', { rule: 'grant-limit' });
await s.put('greu', { presentation: a(), value: 'v', grants: [...multe(MAX_GRANTS - 1), { id: 'ultimul', operations: ['get'], policy: POL('ultimul', 'deployer') }] });
await refuza(() => s.grant('greu', { presentation: a(), grant: { id: 'al-33', operations: ['get'], policy: POL('x', 'reader') } }), 'TOO_MANY_GRANTS', 'al 33-lea', { rule: 'grant-limit' });
// numaratoarea verificarilor: fiecare verifyPresentation citeste o data semnatura prezentarii; marimea corpului o mai citeste o data
let citiri = 0; const p = new Proxy(prez(s, K.deployer, C.deployer), { get: (tinta, k) => { if (k === 'signature') citiri++; return tinta[k]; } });
egal((await s.get('greu', { presentation: p })).value.toString(), 'v', 'deployer-ul citeste prin ultimul grant');
egal(citiri, 3, 'citirile semnaturii (1 marimea corpului + 1 identitatea + 1 perechea emitent-varsta a celor 33 de candidati)');
s.close(); kms.close();
});
proba('nonce-uri pe origine: 32 in zbor de la o adresa, al 33-lea NONCE_ORIGIN_FULL (HTTP 429); dupa ce unul e prezentat, inca unul; alta adresa nu e atinsa', async () => {
const { s, kms } = await populat();
const n = []; for (let i = 0; i < NONCE_PER_ORIGIN; i++) n.push(s.nonce({ origin: '10.0.0.1' }).nonce);
let e; try { s.nonce({ origin: '10.0.0.1' }); } catch (x) { e = x; } egal(e && e.code, 'NONCE_ORIGIN_FULL', 'al 33-lea');
adevarat(s.nonce({ origin: '10.0.0.2' }).nonce && s.nonce().nonce, 'alta adresa si fara adresa: nonce');
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer, { nonce: n[0] }) })).version, 1, 'unul prezentat');
adevarat(s.nonce({ origin: '10.0.0.1' }).nonce, 'dupa prezentare, inca unul');
const server = createServer({ secrets: s, maxBody: 65536 });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
try {
const st = []; for (let i = 0; i < NONCE_PER_ORIGIN + 1; i++) st.push((await cerere(port, 'POST', '/v1/nonce')).status);
egal(st.filter((x) => x === 200).length, NONCE_PER_ORIGIN, '32 de 200'); egal(st[NONCE_PER_ORIGIN], 429, 'al 33-lea: 429');
const r = await cerere(port, 'POST', '/v1/nonce'); egal(r.json.error, 'NONCE_ORIGIN_FULL', 'refuzul numit');
} finally { await new Promise((q) => server.close(q)); s.close(); kms.close(); }
});
proba('ordinea la mutatie: randul de audit e scris INAINTEA fisierului; o scriere a fisierului care cade dupa rand -> STORAGE_WRITE_FAILED, un rand allowed urmat de unul failed cu acelasi requestHash, fisierul neschimbat', async () => {
const dirK = tmpDir('kmss'); const baza = openKms({ dataDir: dirK, rootKey: rootHex() }); let sabotaj = null;
const kmsS = { getKey: (...x) => baza.getKey(...x), createKey: (...x) => baza.createKey(...x), decrypt: (...x) => baza.decrypt(...x),
datakey: (...x) => { const r = baza.datakey(...x); if (sabotaj) { const f = sabotaj; sabotaj = null; f(); } return r; } };
const { s, data } = await nouServiciu({ kms: kmsS });
try {
await s.put('db', { presentation: prez(s, K.admin, C.admin), value: VALOARE, grants: GRANTS() });
const f = path.join(data, 'secrets', 'db.json'); const inainte = fs.readFileSync(f); const n0 = jurnal(s.auditLog).length;
sabotaj = () => { fs.rmSync(f); fs.mkdirSync(f); }; // fisierul devine un dosar: redenumirea nu mai poate ajunge pe el
await refuza(() => s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) }), 'STORAGE_WRITE_FAILED', 'rotate cu fisierul blocat', { rule: 'storage' });
const j = jurnal(s.auditLog).slice(n0).map((e) => e.proof.statement);
egal(j.map((x) => x.operation + ':' + x.verdict).join(' '), 'rotate:allowed rotate:failed', 'randul inainte, apoi failed');
egal(j[1].requestHash, j[0].requestHash, 'acelasi requestHash'); egal(j[1].rules.join(','), 'storage', 'regula storage');
fs.rmSync(f, { recursive: true }); fs.writeFileSync(f, inainte);
adevarat(!fs.readdirSync(path.join(data, 'secrets')).some((x) => x.includes('.tmp-')), 'niciun temporar ramas');
egal((await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) })).version, 2, 'rotirea cazuta nu a ramas: urmatoarea e tot versiunea 2');
} finally { s.close(); baza.close(); }
});
proba('delegates: false: un grant care refuza delegatii admite detinatorul, nu si telefonul lui (regula delegates in jurnal)', async () => {
const { s, kms } = await populat();
await s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'numai-el', operations: ['rotate'], holders: [K.deployer.id], delegates: false, policy: POL('ne', 'deployer') } });
await refuza(() => s.rotate('db', { presentation: prez(s, K.phone, C.deployer, { delegations: [DELEGARE] }) }), 'UNAUTHORIZED', 'telefonul', { rule: 'authorization' });
adevarat(ultima(s).judged.some((g) => g.grant === 'numai-el' && g.rules.includes('delegates')), 'delegates in jurnal: ' + JSON.stringify(ultima(s).judged));
egal((await s.rotate('db', { presentation: prez(s, K.deployer, C.deployer) })).version, 2, 'detinatorul insusi roteste');
await refuza(() => s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'x', operations: ['get'], delegates: 'no', policy: POL('x', 'reader') } }), 'INVALID_GRANT', 'delegates care nu e boolean', { rule: 'grant-form' });
s.close(); kms.close();
});
proba('vocabularul plicului secret-access: buildProof refuza verdict banana, operatie fly, rules sir, version -5, holder oricine, judged fara forma; operatiile serviciului sunt exact vocabularul felului', async () => {
egal(OPERATII.join(','), PK.SECRET_ACCESS.operations.join(','), 'OPERATII == vocabularul felului');
const baza = { service: AUD, secret: 'db', version: 2, operation: 'get', verdict: 'allowed', rules: [], grant: 'deployers', holder: K.deployer.id, presenter: K.deployer.id, presentation: 'B', createdAt: iso(T0) };
egal(PK.buildProof(KIND, baza).statement.verdict, 'allowed', 'forma buna se construieste');
for (const [camp, v] of [['verdict', 'banana'], ['operation', 'fly'], ['rules', 'nu-e-lista'], ['version', -5], ['holder', 'oricine'], ['grant', 42], ['presenter', { x: 1 }], ['createdAt', 'ieri'], ['judged', [{ grant: 'g' }]], ['claimedHolder', 'aere-id:XYZ']]) {
let m = null; try { PK.buildProof(KIND, { ...baza, [camp]: v }); } catch (e) { m = e.message; }
adevarat(m && m.includes(`field "${camp}"`) && m.includes('vocabulary'), `${camp}=${JSON.stringify(v)}: ${m || 'construit'}`);
}
let m = null; try { PK.buildProof(KIND, { ...baza, requestHash: 'nu-e-hash' }); } catch (e) { m = e.message; }
adevarat(m && m.includes('requestHash'), 'requestHash care nu e un hash: ' + m);
});
proba('reguli: fiecare regula declarata are cel putin un /*PAZA:<regula>*/ in cod, si fiecare PAZA numeste o regula declarata', () => {
const src = fs.readFileSync(path.join(HERE, '..', 'secrete.mjs'), 'utf8');
const marcate = [...src.matchAll(/\/\*PAZA:([a-z-]+)\*\//g)].map((m) => m[1]);
const nedeclarate = marcate.filter((r) => !REGULI.includes(r));
egal(nedeclarate.join(','), '', 'PAZA cu regula nedeclarata');
const faraPaza = REGULI.filter((r) => !marcate.includes(r) && !['kms', 'storage', 'internal', 'secret-name', 'value-form', 'secret-exists'].includes(r));
egal(faraPaza.join(','), '', 'reguli declarate fara PAZA (in afara celor cu un singur loc de refuz, numite prin refuz())');
for (const r of REGULI) adevarat(src.includes(`refuz('${r}'`) || src.includes(`rule: '${r}'`) || marcate.includes(r), 'regula fara niciun refuz: ' + r);
});
// ================================================================================== rulare
async function main() {
const t0 = Date.now();
const rez = [];
for (const p of probe) {
const t = Date.now();
try { await p.fn(); rez.push({ nume: p.nume, ok: true, ms: Date.now() - t }); } catch (e) {
if (e instanceof Sarit) rez.push({ nume: p.nume, ok: true, sarit: true, ms: Date.now() - t, motiv: e.message });
else rez.push({ nume: p.nume, ok: false, ms: Date.now() - t, motiv: String((e && e.stack) || e).slice(0, 700), tip: e instanceof Esec ? 'afirmatie' : 'exceptie' });
}
}
for (const d of tmpDirs) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* dosar temporar; ramane in %TEMP% daca Windows il tine ocupat */ } }
const picate = rez.filter((r) => !r.ok); const sarite = rez.filter((r) => r.sarit);
for (const r of rez) process.stdout.write(`${r.sarit ? 'SARIT' : r.ok ? 'TRECE' : 'PICA '} ${r.nume}${r.ok && !r.sarit ? '' : `\n -> ${r.motiv}`}\n`);
process.stdout.write(`\nAere PQ Secrets: ${rez.length - picate.length - sarite.length}/${rez.length} cum trebuia, ${picate.length} picate, ${sarite.length} sarite, ${Date.now() - t0} ms\n`);
if (process.env.AERE_SECRETS_PROBA_JSON) {
fs.writeFileSync(process.env.AERE_SECRETS_PROBA_JSON, JSON.stringify({ completed: true, total: rez.length, passed: rez.length - picate.length, failed: picate.map((r) => r.nume), skipped: sarite.map((r) => r.nume), results: rez, node: process.versions.node, openssl: process.versions.openssl }, null, 2));
}
process.exitCode = picate.length ? 1 : sarite.length ? 2 : 0;
}
main().catch((e) => { process.stderr.write(`harnessul probei a cazut: ${(e && e.stack) || e}\n`); process.exitCode = 2; });

View File

@ -1,9 +1,10 @@
# proof-kinds: one builder for every AIP-23 proof kind
A generic builder of AERE Proof Protocol envelopes (AIP-23) for the standard proof kinds: `data`, `execution`, `identity`,
`compliance`, `runtime`, `location`, `device`, `payment`, `ownership`, `time`, `block`, `authorization`, `settlement` and
`provenance`. Every kind produces the same envelope (`statement` plus `statementHash`, the SHA-256 of the canonical statement
text), so the same AIP-23 reference verifier checks all of them with no kind-specific verification code.
`compliance`, `runtime`, `location`, `device`, `payment`, `ownership`, `time`, `block`, `authorization`, `settlement`,
`provenance` and, since 2026-10-18, `secret-access`. Every kind produces the same envelope (`statement` plus `statementHash`, the
SHA-256 of the canonical statement text), so the same AIP-23 reference verifier checks all of them with no kind-specific
verification code.
```
import { buildProof, KINDS } from './proof-kinds.mjs';
@ -15,7 +16,24 @@ A kind is a schema: its required fields, some of them digests, plus `createdAt`.
an envelope carries digests, never the raw content. A missing required field stops the build with an error instead of producing
an incomplete envelope. Notarization and the post-quantum certificate over it come from AIP-23, not from this builder.
Test: `node proba-proof-kinds.mjs` (24 checks: every kind built and found `VALID` by the reference verifier, six negative
controls such as content changed after hashing or a required field missing, no raw content in any envelope, bytes hashed as
bytes). It needs the AIP-23 reference verifier: `AERE_VERIFY_PROOF=<path to verify-proof.mjs>` (as published at
`secret-access` is the audit entry of the secrets service (`pq-secrets/`): the service (its audience), the secret name, the version,
the operation, the verdict (`allowed`, `refused`, or since 2026-10-18 `failed`: allowed and entered, then the change could not be
stored) with the rules that refused, the grant that allowed, the holder and the presenter (key-derived `aere-id` identities,
verified), the digest of the presentation's signed binding (`presentationHash`) and of the grant's policy (`policyHash`). It never
carries the secret value, nor a digest of it: a digest of a guessable value does not hide it. Since 2026-10-18 (the adversarial
review of the secrets service) it may also carry `requestHash` (the digest of the request received, already a 32-byte hash),
`claimedHolder` / `claimedPresenter` (ids a presentation claimed and nobody verified) and `judged` (on a refusal, each grant judged
with the rules it refused on); envelopes without these fields are byte for byte what they were.
**A kind may have a vocabulary** (`valori` in its schema: per field, a predicate and what it says), judged by `buildProof` on every
field present; `secret-access` is the only kind with one: `verdict` is `allowed`, `refused` or `failed`, `operation` one of the
operations of the secrets service (`SECRET_ACCESS.operations`, which the service requires to be its own list), `version` an integer of
at least 1 or `null`, `rules` a list of rule names, ids `aere-id:` + 40 hex or `null`, `createdAt` an RFC 3339 time. An envelope with
the verdict `banana` is no longer built. The reference verifier does not judge vocabularies (it judges the form and the hash of any
kind), so an envelope written by hand with such values stays `VALID` there: the vocabulary is the builder's promise, not the
verifier's. `KINDS.json` of the published copy names the values of each field that has them.
Test: `node proba-proof-kinds.mjs` (28 checks: every kind built and found `VALID` by the reference verifier, seven negative
controls such as content changed after hashing, a required field missing or a `secret-access` value outside its vocabulary, no raw
content in any envelope, bytes hashed as bytes, the new `secret-access` fields, the vocabulary held by that kind alone). It needs the AIP-23 reference verifier: `AERE_VERIFY_PROOF=<path to verify-proof.mjs>` (as published at
aere.network/tools, with `verify-anchor.mjs`, `mpt.mjs`, `falcon512.mjs`, `registry-v3.mjs` and `package.json` beside it and `npm install` done there); without it the test reports that it did not measure and exits with 2. Node.js 24.

View File

@ -5,7 +5,7 @@
import fs from 'node:fs'; import crypto from 'node:crypto'; import os from 'node:os'; import path from 'node:path';
import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url';
import { buildProof, KINDS } from './proof-kinds.mjs';
import { buildProof, KINDS, SCHEME } from './proof-kinds.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const VERIFY = process.env.AERE_VERIFY_PROOF ? path.resolve(process.env.AERE_VERIFY_PROOF) : path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs');
@ -16,6 +16,7 @@ function verdict(plicPath) { try { return JSON.parse(execFileSync(process.execPa
// intrari de proba pentru fiecare fel (continut brut, ca sa verific ca nu se scurge)
const BRUT = 'CONTINUT-BRUT-BRUT-42';
const DB = 'db-password'; // numele unui secret din intrarea fixa a felului secret-access (un NUME, nu o valoare)
const INTRARI = {
data: { name: 'raport.csv', content: BRUT, createdAt: '2026-09-26T09:00:00Z' },
execution: { program: 'aere-node', input: BRUT, output: BRUT + 'o', exitCode: 0, createdAt: '2026-09-26T09:00:00Z' },
@ -31,10 +32,11 @@ const INTRARI = {
authorization: { grantor: 'org-x', grantee: 'agent-7', scope: 'payments:send<=100', policy: BRUT, expiresAt: '2026-12-31T00:00:00Z', createdAt: '2026-09-28T09:00:00Z' },
settlement: { chainId: 2800, txHash: '0x' + '12'.repeat(32), blockHash: '0x' + '34'.repeat(32), from: '0xa', to: '0xb', amount: '100', asset: 'AERE', instruction: BRUT, createdAt: '2026-09-28T09:00:00Z' },
provenance: { subject: BRUT, parents: [BRUT + '1', BRUT + '2'], process: BRUT + 'p', actor: 'build-bot', createdAt: '2026-09-28T09:00:00Z' },
'secret-access': { service: 'https://secrets.example', secret: DB, version: 2, operation: 'get', verdict: 'allowed', rules: [], grant: 'deployers', holder: 'aere-id:' + '0'.repeat(40), presenter: 'aere-id:' + '1'.repeat(40), presentation: BRUT, policy: BRUT + 'pol', createdAt: '2026-10-18T09:00:00Z' },
};
try {
cer('schema acopera cele 14 feluri (10 + bloc + autorizare, decontare, provenienta)', KINDS.length >= 14 && ['authorization', 'settlement', 'provenance'].every((k) => KINDS.includes(k)));
cer('schema acopera cele 15 feluri (10 + bloc + autorizare, decontare, provenienta + acces la secret)', KINDS.length >= 15 && ['authorization', 'settlement', 'provenance', 'secret-access'].every((k) => KINDS.includes(k)));
for (const k of KINDS) {
// fiecare fel se judeca separat: un fel care nu se mai poate construi e un RAU numit, nu o exceptie care opreste proba (2026-09-28)
let plic; try { plic = buildProof(k, INTRARI[k]); } catch (e) { cer(`${k}: constructia a cazut (${e.message})`, false); continue; }
@ -69,7 +71,17 @@ try {
const ps = buildProof('data', { name: 'a', content: 'abc', createdAt: '2026-09-26T09:00:00Z' });
cer('Buffer/Uint8Array: digestul = sha256 al octetilor (reproductibil cu sha256sum)', pb.statement.sha256 === asteptat && pu.statement.sha256 === asteptat);
cer('sirurile raman ca inainte (vectorii publicati neschimbati)', ps.statement.sha256 === '0x' + crypto.createHash('sha256').update('abc', 'utf8').digest('hex'));
// 2026-10-18 (revizuirea adversariala B8): VOCABULARUL felului secret-access. Un plic cu verdictul "banana" era VALID la verificator.
const SA = INTRARI['secret-access'];
const rele8b = [['verdict', 'banana'], ['operation', 'fly'], ['rules', 'nu-e-lista'], ['version', -5], ['holder', 'oricine'], ['grant', 42], ['presenter', { x: 1 }], ['createdAt', 'ieri'], ['secret', '../x'], ['judged', [{ grant: 'g' }]], ['claimedPresenter', 'aere-id:XY']]
.filter(([c, v]) => !arunca(() => buildProof('secret-access', { ...SA, [c]: v })));
cer('CONTROL: acces la secret cu valori in afara vocabularului (verdict banana, operatie fly, rules sir, version -5, holder oricine, ...) -> eroare la constructie' + (rele8b.length ? ': construite ' + rele8b.map(([c]) => c).join(', ') : ''), rele8b.length === 0);
const nou = buildProof('secret-access', { ...SA, verdict: 'failed', rules: ['storage'], requestHash: '0x' + 'ab'.repeat(32), claimedPresenter: 'aere-id:' + '2'.repeat(40), judged: [{ grant: 'admin', rules: ['rule'] }] });
const vechi = buildProof('secret-access', SA);
cer('acces la secret: requestHash, claimedPresenter, judged si verdictul failed se construiesc; fara ele declaratia are exact campurile de dinainte, in ordinea lor',
nou.statement.requestHash === '0x' + 'ab'.repeat(32) && nou.statement.verdict === 'failed' && Object.keys(vechi.statement).join(',') === 'v,kind,presentationHash,policyHash,service,secret,version,operation,verdict,rules,grant,holder,presenter,createdAt');
cer('vocabularul e numai al felului secret-access (celelalte feluri se construiesc exact ca ieri)', KINDS.filter((k) => SCHEME[k].valori).join(',') === 'secret-access');
} finally { fs.rmSync(T, { recursive: true, force: true }); }
const total = KINDS.length + 10;
console.log(`\nProof of everything: ${total - rele}/${total} cum trebuia (${KINDS.length} feluri + 6 controale negative + acoperire + Buffer + parinti)`);
const total = KINDS.length + 13;
console.log(`\nProof of everything: ${total - rele}/${total} cum trebuia (${KINDS.length} feluri + 7 controale negative + acoperire + Buffer + parinti + campurile noi + vocabularul)`);
process.exit(rele ? 1 : 0);

View File

@ -20,6 +20,16 @@ const dg = (v) => (v == null ? null : (isDigest(v) ? v.toLowerCase()
: (Buffer.isBuffer(v) || v instanceof Uint8Array) ? sha256(Buffer.from(v))
: sha256(Buffer.from(typeof v === 'string' ? v : JSON.stringify(v), 'utf8'))));
// VOCABULARUL unui fel (2026-10-18, B8): `valori: { camp: [predicat, ce spune] }`, judecat de buildProof pe fiecare camp prezent in
// declaratie (un camp lipsa il judeca `req`). Un mecanism mic si general; felurile fara `valori` nu se schimba cu nimic.
// Operatiile si verdictele unui acces la un secret, intr-un singur loc (cloud-gateway/pq-secrets cere, intr-o proba, ca OPERATII lui sa
// fie exact aceasta lista).
export const SECRET_ACCESS = Object.freeze({ operations: Object.freeze(['put', 'get', 'metadata', 'rotate', 'revoke-version', 'grant', 'revoke', 'list']), verdicts: Object.freeze(['allowed', 'refused', 'failed']) });
const NUME_IN_SERVICIU = /^[a-z0-9][a-z0-9_-]{0,63}$/;
const eRegula = (r) => typeof r === 'string' && /^[a-z][a-z0-9-]{0,63}$/.test(r);
const eGrant = (g) => typeof g === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/.test(g);
const eIdSauNull = (v) => v === null || (typeof v === 'string' && /^aere-id:[0-9a-f]{40}$/.test(v));
// schema fiecarui fel: campuri de tip 'digest' (se hashuiesc), 'plain' (raman ca atare), 'bool', 'list'. Toate obligatorii daca in `req`.
export const SCHEME = {
'data': { digest: { sha256: 'content' }, plain: { name: 'subject' }, req: ['sha256', 'name'] },
@ -45,6 +55,32 @@ export const SCHEME = {
'settlement': { digest: { instructionHash: 'instruction' }, hash: ['txHash', 'blockHash'], plain: { chainId: 'chainId', from: 'from', to: 'to', amount: 'amount', asset: 'asset' }, req: ['chainId', 'txHash', 'blockHash', 'amount', 'asset'] },
// provenance: din ce a iesit un artefact (parents), prin ce proces, cine l-a produs
'provenance': { digest: { subjectHash: 'subject', processHash: 'process' }, list: { parents: 'parentsContent' }, plain: { actor: 'actor' }, req: ['subjectHash', 'parents'] },
// 2026-10-18 (roadmap pista B, B8, linia Shield): un acces la un secret (cloud-gateway/pq-secrets): ce serviciu (audienta), ce secret, ce
// versiune, ce operatie, verdictul (allowed | refused) cu regulile care au refuzat, grantul care a permis, detinatorul si prezentatorul
// (id-uri aere-id, derivate din chei), digestul PREZENTARII (al legaturii semnate: ea numeste prin hash credentialul, dezvaluirile, lantul
// de delegare, publicul, nonce-ul si momentul) si al politicii grantului. NICIODATA valoarea secretului, nici un digest al ei (un digest
// al unei valori ghicibile nu o ascunde).
// 2026-10-18 (revizuirea adversariala B8): `requestHash` (digestul cererii pentru care detinatorul a semnat prezentarea, deja un hash),
// `claimedHolder` / `claimedPresenter` (id-urile PRETINSE de o prezentare a carei semnatura nu a fost verificata; `holder` si `presenter`
// poarta numai id-uri verificate), `judged` (pe un refuz: fiecare grant judecat, cu regulile pe care a refuzat) si VOCABULARUL valorilor
// (`valori`): un plic secret-access cu verdictul "banana" sau operatia "fly" nu se mai poate construi. Campurile noi lipsesc din plicurile
// de dinainte, deci acelea raman octet cu octet aceleasi.
'secret-access': { digest: { presentationHash: 'presentation', policyHash: 'policy' }, hash: ['requestHash'], plain: { service: 'service', secret: 'secret', version: 'version', operation: 'operation', verdict: 'verdict', rules: 'rules', grant: 'grant', holder: 'holder', presenter: 'presenter', claimedHolder: 'claimedHolder', claimedPresenter: 'claimedPresenter', judged: 'judged' }, req: ['service', 'operation', 'verdict', 'presentationHash'],
valori: {
service: [(v) => typeof v === 'string' && v.length >= 1 && v.length <= 256 && /^[\x21-\x7e]+$/.test(v), '1 to 256 printable ASCII characters without spaces (the audience of the service)'],
secret: [(v) => v === null || v === '*' || (typeof v === 'string' && NUME_IN_SERVICIU.test(v)), 'a secret name (^[a-z0-9][a-z0-9_-]{0,63}$), "*" for list, or null'],
version: [(v) => v === null || (Number.isSafeInteger(v) && v >= 1), 'an integer >= 1, or null'],
operation: [(v) => SECRET_ACCESS.operations.includes(v), SECRET_ACCESS.operations.join(' | ')],
verdict: [(v) => SECRET_ACCESS.verdicts.includes(v), SECRET_ACCESS.verdicts.join(' | ') + ' (failed: allowed and entered, then the change could not be stored)'],
rules: [(v) => Array.isArray(v) && v.length <= 128 && v.every(eRegula), 'a list of rule names (^[a-z][a-z0-9-]{0,63}$), at most 128'],
grant: [(v) => v === null || eGrant(v), 'a grant id (^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$), or null'],
holder: [eIdSauNull, 'an aere-id (aere-id: + 40 hex) verified from the presentation, or null'],
presenter: [eIdSauNull, 'an aere-id verified from the presentation, or null'],
claimedHolder: [eIdSauNull, 'an aere-id the presentation claims and nobody verified, or null'],
claimedPresenter: [eIdSauNull, 'an aere-id the presentation claims and nobody verified, or null'],
judged: [(v) => Array.isArray(v) && v.length <= 64 && v.every((x) => x && typeof x === 'object' && !Array.isArray(x) && Object.keys(x).sort().join(',') === 'grant,rules' && eGrant(x.grant) && Array.isArray(x.rules) && x.rules.length <= 64 && x.rules.every(eRegula)), 'a list of { grant, rules }, at most 64'],
createdAt: [(v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/.test(v) && !Number.isNaN(Date.parse(v)), 'an RFC 3339 time'],
} },
};
export const KINDS = Object.keys(SCHEME);
@ -81,6 +117,9 @@ export function buildProof(kind, p = {}) {
for (const r of s.req) {
if (statement[r] === undefined || statement[r] === null) throw new Error('proof-kinds: ' + kind + ' needs the field "' + r + '"');
}
for (const [camp, [bun, spune]] of Object.entries(s.valori || {})) {
if (statement[camp] !== undefined && !bun(statement[camp])) throw new Error('proof-kinds: ' + kind + ' field "' + camp + '" has a value outside its vocabulary (' + spune + ')');
}
const text = JSON.stringify(statement); // canonic AIP-23
return { v: 1, kind: 'aere-proof-of-' + kind + '-attestation', statement, statementHash: sha256(Buffer.from(text, 'utf8')) };
}