From 0c8f1de3fdbe1ae9067b0f7e037d1379a479aff1 Mon Sep 17 00:00:00 2001 From: Aere Network Date: Mon, 19 Oct 2026 05:19:21 +0300 Subject: [PATCH] identity: the SD-JWT form of an AERE hybrid identity is judged on its ML-DSA-65 half by default, the Ed25519 half only on explicit request, and every verdict says postQuantum (rule sdjwt.hybrid-half); pq-pki: the signature algorithm a certificate or a revocation list declares must be its issuer key's (ALG_KEY, CRL_ALG_KEY); agents: an equivocation proof requires the agent's ML-DSA-65 key; pq-gateway: the status names the path to the backend and hybrid-only refuses a plain-HTTP backend; conformance 241 rules, 631 vectors --- CITATIONS-UNRESOLVED.md | 2 +- README.md | 14 +- agents/README.md | 4 +- agents/agent-ledger.mjs | 8 +- agents/control-negativ-aprobare.mjs | 3 + agents/proba-agent-ledger.mjs | 27 +++ conformance/README.md | 39 +++- conformance/REGULI.json | 7 + conformance/genereaza-vectori.mjs | 40 ++-- conformance/implementare-aere.mjs | 7 +- conformance/proba-conformance.mjs | 2 +- conformance/reguli.mjs | 2 + conformance/vectori/INDEX.json | 218 +++++++++++------- ...ere-hybrid-keys-classical.hybrid-half.json | 66 ++++++ .../aere-hybrid-keys-classical.valid.json | 60 +++++ .../aere-hybrid-keys.hybrid-half-named.json | 68 ++++++ .../aere-hybrid-keys.hybrid-half.json | 67 ++++++ .../aere-hybrid-keys.valid.json | 59 +++++ .../aere-issued.alg-none.json | 1 + .../aere-issued.claim-conflict.json | 1 + .../sdjwt-presentation/aere-issued.crit.json | 1 + .../aere-issued.digest-twice.json | 1 + .../aere-issued.disclosure-form-not-json.json | 1 + .../aere-issued.disclosure-form-sd.json | 1 + .../aere-issued.disclosure-form.json | 1 + .../aere-issued.disclosure-name-proto.json | 1 + .../aere-issued.disclosure-name.json | 1 + .../aere-issued.key-confusion.json | 1 + .../aere-issued.sd-alg-nested.json | 1 + .../aere-issued.sd-alg.json | 1 + .../aere-issued.time-no-exp.json | 1 + .../aere-issued.time-years-later.json | 1 + .../sdjwt-presentation/aere-issued.typ.json | 1 + .../sdjwt-presentation/aere-issued.valid.json | 1 + .../aere-ml-dsa-65.valid.json | 1 + .../sdjwt-presentation/aere.kb-claims.json | 1 + .../sdjwt-presentation/aere.kb-cnf.json | 1 + .../aere.kb-freshness-exp.json | 1 + .../sdjwt-presentation/aere.signature-kb.json | 1 + .../sdjwt-presentation/aere.time-expired.json | 1 + .../sdjwt-presentation/aere.time-not-yet.json | 1 + .../sdjwt-presentation/aere.typ-kb.json | 1 + .../sdjwt-presentation/aere.valid.json | 3 +- .../rfc9901-issued.disclosure-twice.json | 1 + .../rfc9901-issued.form-empty.json | 1 + .../rfc9901-issued.unreferenced.json | 1 + .../rfc9901-issued.valid.json | 1 + .../rfc9901.form-clock.json | 1 + .../sdjwt-presentation/rfc9901.form.json | 1 + .../sdjwt-presentation/rfc9901.issuer.json | 1 + .../rfc9901.kb-audience.json | 1 + .../rfc9901.kb-context.json | 1 + .../rfc9901.kb-freshness.json | 1 + .../sdjwt-presentation/rfc9901.kb-nonce.json | 1 + .../rfc9901.kb-required.json | 1 + .../rfc9901.kb-sd-hash.json | 1 + .../sdjwt-presentation/rfc9901.key.json | 1 + .../sdjwt-presentation/rfc9901.signature.json | 1 + .../sdjwt-presentation/rfc9901.typ.json | 1 + .../sdjwt-presentation/rfc9901.valid.json | 1 + .../sd-jwt-core-es256.valid.json | 1 + .../sd-jwt-core-ml-dsa-65.valid.json | 1 + .../sd-jwt-core-typed.valid.json | 1 + .../sdjwt-presentation/sd-jwt-core.alg.json | 1 + .../sd-jwt-core.digest-twice.json | 1 + .../sd-jwt-core.kb-audience.json | 1 + .../sd-jwt-core.kb-sd-hash.json | 1 + .../sd-jwt-core.sd-alg.json | 1 + .../sd-jwt-core.signature-kb.json | 1 + .../sdjwt-presentation/sd-jwt-core.typ.json | 1 + .../sd-jwt-core.unreferenced.json | 1 + .../sdjwt-presentation/sd-jwt-core.valid.json | 1 + .../aere-no-status.valid.json | 1 + .../aere-pid-8-bits.valid.json | 1 + .../aere-pid-ml-dsa-65.valid.json | 1 + .../aere-pid-status-waived.partial.json | 1 + .../aere-pid.disclosed-protected.json | 1 + .../aere-pid.form-idx.json | 1 + .../aere-pid.form-integrity.json | 1 + .../aere-pid.form-status.json | 1 + .../aere-pid.status-index.json | 1 + .../aere-pid.status-list-expired.json | 1 + .../aere-pid.status-list-no-iat.json | 1 + .../aere-pid.status-list-other-uri.json | 1 + .../aere-pid.status-list-stranger.json | 1 + .../aere-pid.status-list-typ.json | 1 + .../aere-pid.status-revoked.json | 1 + .../aere-pid.status-suspended.json | 1 + .../aere-pid.status-unjudged.json | 1 + .../sdjwt-vc-presentation/aere-pid.valid.json | 1 + .../aere-pid.vct-expected.json | 1 + .../aere-pid.vct-mismatch.json | 1 + .../sdjwt-vc-presentation/aere-pid.vct.json | 1 + .../sd-jwt-vc-8-bits.valid.json | 1 + .../sd-jwt-vc-es256.valid.json | 1 + .../sd-jwt-vc-ml-dsa-65.valid.json | 1 + .../sd-jwt-vc-status-waived.partial.json | 1 + .../sd-jwt-vc.status-list-expired.json | 1 + .../sd-jwt-vc.status-list-other-uri.json | 1 + .../sd-jwt-vc.status-revoked.json | 1 + .../sd-jwt-vc.status-suspended.json | 1 + .../sd-jwt-vc.status-unjudged.json | 1 + .../sd-jwt-vc.valid.json | 1 + .../sd-jwt-vc.vct-mismatch.json | 1 + identity/README.md | 63 +++-- identity/control-negativ-sdjwt-vc.mjs | 7 +- identity/control-negativ-sdjwt.mjs | 17 +- identity/identity-cli.mjs | 47 ++-- identity/interop/interop-sdjwt-vc.mjs | 11 +- identity/interop/interop-sdjwt.mjs | 7 +- identity/proba-sdjwt-vc.mjs | 34 ++- identity/proba-sdjwt.mjs | 145 ++++++++++-- identity/sdjwt-vc.mjs | 40 ++-- identity/sdjwt.mjs | 118 +++++++--- pq-gateway/README.md | 17 +- pq-gateway/pq-gateway.mjs | 46 +++- .../proba-pq-gateway-control-negativ.sh | 16 +- pq-gateway/proba-pq-gateway.mjs | 52 ++++- pq-pki/README.md | 5 +- pq-pki/pki.mjs | 15 +- pq-pki/test/control-negativ.mjs | 5 + pq-pki/test/proba.mjs | 41 ++++ 122 files changed, 1205 insertions(+), 262 deletions(-) create mode 100644 conformance/vectori/sdjwt-presentation/aere-hybrid-keys-classical.hybrid-half.json create mode 100644 conformance/vectori/sdjwt-presentation/aere-hybrid-keys-classical.valid.json create mode 100644 conformance/vectori/sdjwt-presentation/aere-hybrid-keys.hybrid-half-named.json create mode 100644 conformance/vectori/sdjwt-presentation/aere-hybrid-keys.hybrid-half.json create mode 100644 conformance/vectori/sdjwt-presentation/aere-hybrid-keys.valid.json diff --git a/CITATIONS-UNRESOLVED.md b/CITATIONS-UNRESOLVED.md index f7094a7..1ff1453 100644 --- a/CITATIONS-UNRESOLVED.md +++ b/CITATIONS-UNRESOLVED.md @@ -1,7 +1,7 @@ # Citations in this repository that you cannot open This file is generated by the publication stager of this repository (scripts/legaturi-declaratie.cjs in the Aere working -tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-18, of every path cited in this +tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-19, of every path cited in this repository that does not resolve to a published file. A citation is a promise that a claim is checkable. Where the target is not published, or names a file the reader creates diff --git a/README.md b/README.md index 068f5a3..c950030 100644 --- a/README.md +++ b/README.md @@ -22,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), 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 | +| [`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), 241 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), 631 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 241 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) | @@ -37,23 +37,23 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j | component | tests | negative control | |---|---|---| -| pq-gateway | 33/33 (`node proba-pq-gateway.mjs`) | 33/33 (`bash proba-pq-gateway-control-negativ.sh`) | +| pq-gateway | 35/35 on 2026-10-19 (`node proba-pq-gateway.mjs`; the status names the path to the service behind the gateway: its mode, minimum TLS version and groups, and `AERE_PQGW_UPSTREAM_MODE=hybrid-only` refuses a plain-HTTP backend) | 37/37 (`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`) | +| pq-pki | 28/28 on 2026-10-19 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5; since 2026-10-19 the signature algorithm a certificate or a revocation list declares must be its issuer key's | 26/26 (`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= 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 | 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 | 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`) and Travel Rule 20/20 (`node proba-travel-rule.mjs`) with the same verifier (without it each reports one test unmeasured and exits 2; 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 `) | 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 | +| 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`) and Travel Rule 20/20 (`node proba-travel-rule.mjs`) with the same verifier (without it each reports one test unmeasured and exits 2; 2026-10-18: a missing originator id or nonce is a refusal, found when the verifier was enrolled in the conformance suite), SD-JWT 31/31 (`node proba-sdjwt.mjs`, the RFC 9901 vectors and fixtures derived from @sd-jwt/core 0.23.0 included; since 2026-10-19 the keys of an AERE identity are judged on their ML-DSA-65 half by default, the Ed25519 half only on request, and every verdict says `postQuantum`), SD-JWT VC 11/11 (`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, before the 2026-10-19 change; the scripts now ask for the Ed25519 half explicitly and have not been re-run since | 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 39/39 (`node control-negativ-sdjwt.mjs`); SD-JWT VC 20/20 (`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 59/59 on 2026-10-19 (since then an equivocation proof must be made with the agent's ML-DSA-65 key), 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 `) | 28/28 (`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 | 11/11 from a fresh clone of this repository on 2026-10-18 (`AERE_VERIFY_PROOF= node proba-conformance.mjs --online`; the twelfth row, the comparison with the published AIP-23 kind vectors, needs their generator, which is not part of this copy, and is reported as information); 12/12 in the development repository, 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 | +| conformance | 11/11 from a fresh clone of this repository on 2026-10-18 (`AERE_VERIFY_PROOF= node proba-conformance.mjs --online`; the twelfth row, the comparison with the published AIP-23 kind vectors, needs their generator, which is not part of this copy, and is reported as information); 12/12 in the development repository, 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 631/631, an independent Python implementation passes the 65 it claims and the other 566 are reported not covered, 101,947 malformed inputs and 183 malformed envelope files each get a verdict; remeasured 2026-10-19 02:13Z after the rule that judges the post-quantum half of an AERE identity by default); without `--online` the nine chain vectors are reported unmeasured and the exit code is 2 | 323/323 on 2026-10-19: 241/241 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 | @@ -64,4 +64,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ... ## Licence -MIT, see [LICENSE](LICENSE). Files: 962 (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). +MIT, see [LICENSE](LICENSE). Files: 967 (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 643, device-identity 11, exposure-report 18, readiness 4). diff --git a/agents/README.md b/agents/README.md index d25a7f9..3b09682 100644 --- a/agents/README.md +++ b/agents/README.md @@ -112,13 +112,13 @@ Measured on 2026-09-29 (Node.js 24.14.1, Windows): | test | result | |---|---| | `node proba-agent-policy.mjs` | 27/27 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF=`); without it 25 run, 2 are reported as skipped and the exit code is 2 | -| `node proba-agent-ledger.mjs` | 54/54 (measured 2026-10-05): a ledger written and signed with a classical key refused by name, by the verifier and by the writer; limits over the ledger, identity, tampering, a re-signed false decision, a looser policy under the real hash, backdating (refused when written; caught with anchors or `notBefore`), a branch, equivocation proofs, resuming | +| `node proba-agent-ledger.mjs` | 59/59 (measured 2026-10-19; since then an equivocation proof must be made with the agent's ML-DSA-65 key, AIP-25 finding C2): a ledger written and signed with a classical key refused by name, by the verifier and by the writer; limits over the ledger, identity, tampering, a re-signed false decision, a looser policy under the real hash, backdating (refused when written; caught with anchors or `notBefore`), a branch, equivocation proofs, resuming | | `node proba-agent-aprobare.mjs` | 39/39: threshold, unnamed or repeated approvers, classical keys, another action or other tool arguments, reused nonces (also after a restart), expiry, approvals across branches, revocation, an omitted revocation | | `node proba-agent-cli.mjs` | 23/23 on Windows, through files and processes only; on Linux and macOS one more test checks the 0600 mode of the key (not measured here) | | `node proba-agent-ancora.mjs` | 26/26 without a network (the AIP-23 verifier's answer in its own form): the head, the time read from the finality level (pending, failed, a mismatched or unknown form: no anchor), an envelope that claims another time, backdating caught with the anchor and not without it, another branch, the refusals before any transaction (a modified head, chain 2800 without confirmation, no contract at the notary) | | `AERE_TESTNET_KEY_FILE= node proba-agent-ancora-testnet.mjs` | 7/7 on the public testnet 28001, measured 2026-09-30: a real ledger's head notarized, read back under a post-quantum certified anchor by the published AIP-23 verifier within 20 s, the certified time equal to the contract's first-seen time, the honest continuation valid with the anchor, a backdated continuation valid without it and caught with it, another branch refused; evidence in `dovezi-ancora-28001/` | | `node control-negativ-ancora.mjs` | 11/11: each anchoring guard removed in a copy turns its named test red | -| `node control-negativ-aprobare.mjs` | 26/26: each guard is removed in a copy, one at a time, and the named test must fail for that reason; a test that stops before its summary counts as a failure of the control | +| `node control-negativ-aprobare.mjs` | 28/28 (2026-10-19): each guard is removed in a copy, one at a time, and the named test must fail for that reason; a test that stops before its summary counts as a failure of the control | The data format is version 2 (2026-09-29). Code comments, test names and most internal names are in Romanian; messages, data and the exported interface are in English. No third party has reviewed this component. diff --git a/agents/agent-ledger.mjs b/agents/agent-ledger.mjs index 49463d3..dd1778c 100644 --- a/agents/agent-ledger.mjs +++ b/agents/agent-ledger.mjs @@ -307,6 +307,10 @@ export function verifyLedger(ledger, { policy: politicaData, policyHash: hashFix // DOVADA DE ECHIVOCARE: doua intrari semnate de aceeasi cheie de agent, cu aceeasi sesiune si acelasi seq, si continut diferit. Cum // sesiunea e una pe agent si politica, asta inseamna ca agentul a scris doua istorii diferite ale aceluiasi registru (o aprobare // folosita de doua ori, o cheltuiala dubla). Dovada se verifica fara incredere in cine o aduce: numai cheia publica si doua semnaturi. +// 2026-10-19 (AIP-25 constatarea C2, D-536): si aici cheia trebuie sa fie ML-DSA-65, ca la verifyLedger (regula `cheie`). Pana atunci +// findEquivocation si verifyEquivocation verificau cu orice cheie arata dovada, deci o "echivocare" semnata cu o cheie Ed25519 (agentId +// derivat din ea) iesea ok, desi registrul aceleiasi chei e refuzat: o dovada impotriva a ceva ce nu e o identitate de agent. +const cheieDeAgent = (pub) => !!pub && pub.asymmetricKeyType === ALG; function intrareSemnata(e, pub) { if (!e || !e.body || !Number.isInteger(e.seq) || e.body.seq !== e.seq) return false; let ok = false; @@ -320,6 +324,7 @@ function intrareSemnata(e, pub) { export function findEquivocation(a, b) { if (!a || !b || a.agentId !== b.agentId || a.session !== b.session) return null; let pub; try { pub = crypto.createPublicKey(a.publicKeyPem); } catch { return null; } + if (!cheieDeAgent(pub)) return null; // AIP-25 C2: numai cheia unei identitati de agent (ML-DSA-65) face o dovada if (agentIdFromKey(pub) !== a.agentId) return null; const n = Math.min((a.entries || []).length, (b.entries || []).length); for (let i = 0; i < n; i++) { @@ -330,10 +335,11 @@ export function findEquivocation(a, b) { } return null; } -/** @returns {{ok:boolean, error?:string}} */ +/** @returns {{ok:boolean, error?:string, rule?:string}} (`rule: 'cheie'` cand cheia aratata nu e o cheie de agent ML-DSA-65, AIP-25 C2) */ export function verifyEquivocation(p) { if (!p || p.kind !== 'aere-agent-equivocation' || p.v !== 1) return { ok: false, error: 'not an aere-agent-equivocation v1' }; let pub; try { pub = crypto.createPublicKey(p.publicKeyPem); } catch { return { ok: false, error: 'the public key cannot be read' }; } + if (!cheieDeAgent(pub)) return { ok: false, rule: 'cheie', error: `the agent key is ${pub.asymmetricKeyType}, not ML-DSA-65: an agent identity is an ML-DSA-65 key, so this proves nothing against an agent` }; if (agentIdFromKey(pub) !== p.agentId) return { ok: false, error: 'agentId is not derived from the public key' }; for (const e of [p.a, p.b]) { if (!e || !e.body || e.seq !== p.seq || e.body.agentId !== p.agentId || e.body.session !== p.session) return { ok: false, error: 'the two entries are not at the same position of the same ledger' }; diff --git a/agents/control-negativ-aprobare.mjs b/agents/control-negativ-aprobare.mjs index 4a39578..aeba723 100644 --- a/agents/control-negativ-aprobare.mjs +++ b/agents/control-negativ-aprobare.mjs @@ -34,6 +34,9 @@ const PLANTARI = [ ['verificatorul nu mai cere sesiunea registrului in intrare', 'agent-ledger.mjs', ' && b.session === ledger.session /*PAZA:corp*/)) return', ' /*PAZA:corp*/)) return', P.reg], ['sesiunea registrului nu mai trebuie sa fie cea derivata', 'agent-ledger.mjs', 'if (!(ledger.session === sessionId(ledger.agentId, policyHash) /*PAZA:sesiune*/)) return', 'if (false) return', P.reg], ['dovada de echivocare fara semnaturile agentului', 'agent-ledger.mjs', "if (!intrareSemnata(e, pub)) return { ok: false, error: 'an entry is not signed by the agent' };", '', P.reg], + // 2026-10-19 (AIP-25 C2, D-536): dovada de echivocare cere cheia unei identitati de agent, ML-DSA-65, ca verifyLedger + ['AIP-25 C2: verifyEquivocation primeste o cheie care nu e ML-DSA-65', 'agent-ledger.mjs', "if (!cheieDeAgent(pub)) return { ok: false, rule: 'cheie',", "if (false) return { ok: false, rule: 'cheie',", P.reg], + ['AIP-25 C2: findEquivocation face o dovada din cheia oricui', 'agent-ledger.mjs', 'if (!cheieDeAgent(pub)) return null;', 'if (false) return null;', P.reg], ['un registru atins se poate relua', 'agent-ledger.mjs', 'if (!v.ok) throw new Error(`agent-ledger: the ledger to resume does not verify', 'if (false) throw new Error(`agent-ledger: the ledger to resume does not verify', P.reg], ['reluarea uita nonce-urile folosite', 'agent-ledger.mjs', 'for (const e of entries) for (const n of nonceuri(e.body.approvals)) folosite.add(n);', '', P.apr], ['reluarea uita revocarea', 'agent-ledger.mjs', 'JSON.parse(JSON.stringify(ledger.entries)), v.revocations.revokedAt);', 'JSON.parse(JSON.stringify(ledger.entries)), null);', P.apr], diff --git a/agents/proba-agent-ledger.mjs b/agents/proba-agent-ledger.mjs index 767088b..0f06892 100644 --- a/agents/proba-agent-ledger.mjs +++ b/agents/proba-agent-ledger.mjs @@ -228,6 +228,33 @@ cer(!vt4.ok && /false identity/.test(vt4.error), `11. CONTROL: cheia publica inl cer(bun.ok === true && bun.rule === undefined, '20. martor: acelasi fel de registru, cu o cheie ML-DSA-65, verifica'); } +// 21. AIP-25 C2 (D-536, 2026-10-19): dovada de echivocare cere aceeasi cheie ca registrul, ML-DSA-65. Pana atunci findEquivocation si +// verifyEquivocation verificau cu orice cheie arata dovada, deci o "echivocare" a unei chei Ed25519 (agentId derivat din ea) iesea ok, +// desi verifyLedger (proba 20) refuza registrul aceleiasi chei: o dovada impotriva unei identitati care nu e o identitate de agent. +{ + const sha = (s) => crypto.createHash('sha256').update(Buffer.from(s, 'utf8')).digest('hex'); + // doua intrari la seq 0, cu continut diferit, semnate cu cheia `priv`, pentru agentul derivat din `pub` + const echivocare = (pub, priv, semnaB = priv) => { + const agentId = agentIdFromKey(pub); const pc = definePolicy({ agentId, tools: ['retrieval', 'search'] }); const session = sessionId(agentId, pc.policyHash); + const intrare = (tool, cheie) => { const body = { v: 2, agentId, policyHash: pc.policyHash, session, at: 6000, action: { kind: 'tool', tool, at: 6000 }, decision: { allowed: true, reason: 'tool allowed' }, provenance: null, seq: 0 }; + const prev = '0'.repeat(64); const signature = crypto.sign(null, Buffer.from(`0|${prev}|${canonical(body)}`, 'utf8'), cheie).toString('base64'); return { seq: 0, prev, body, signature, hash: sha(`0|${prev}|${canonical(body)}|${signature}`) }; }; + const a = intrare('retrieval', priv), b = intrare('search', semnaB); + const exp = (e) => ({ version: 2, agentId, publicKeyPem: pub.export({ type: 'spki', format: 'pem' }), policyHash: pc.policyHash, session, entries: [e] }); + return { dovada: { v: 1, kind: 'aere-agent-equivocation', agentId, session, publicKeyPem: pub.export({ type: 'spki', format: 'pem' }), seq: 0, a, b }, A: exp(a), B: exp(b) }; + }; + const ed = crypto.generateKeyPairSync('ed25519'); const E = echivocare(ed.publicKey, ed.privateKey); + const v = verifyEquivocation(E.dovada); + cer(!v.ok && v.rule === 'cheie' && /not ML-DSA-65/.test(String(v.error)), `21. AIP-25 C2: o echivocare semnata cu o cheie Ed25519 e refuzata pe nume (${v.ok ? 'ok' : v.rule + ': ' + String(v.error).slice(0, 60)})`); + cer(findEquivocation(E.A, E.B) === null, '21. AIP-25 C2: findEquivocation nu face o dovada din doua exporturi ale unei chei Ed25519'); + const m44 = crypto.generateKeyPairSync('ml-dsa-44'); const v44 = verifyEquivocation(echivocare(m44.publicKey, m44.privateKey).dovada); + cer(!v44.ok && v44.rule === 'cheie', `21. AIP-25 C2 ATAC: si o cheie post-cuantica de alt fel (ML-DSA-44) e refuzata: identitatea agentului e ML-DSA-65 (${v44.ok ? 'ok' : v44.error})`); + // chei de feluri diferite in cele doua intrari: cheia aratata e ML-DSA-65, a doua intrare e semnata cu o cheie Ed25519 -> nu e semnata de agent + const m65 = crypto.generateKeyPairSync('ml-dsa-65'); const Mx = echivocare(m65.publicKey, m65.privateKey, ed.privateKey); + const vx = verifyEquivocation(Mx.dovada); cer(!vx.ok && /not signed by the agent/.test(String(vx.error)), `21. AIP-25 C2 ATAC: o intrare semnata cu alt fel de cheie decat cea aratata -> refuzata (${vx.ok ? 'ok' : vx.error})`); + const M = echivocare(m65.publicKey, m65.privateKey); + cer(verifyEquivocation(M.dovada).ok === true && !!findEquivocation(M.A, M.B), '21. martor: aceeasi echivocare, cu o cheie ML-DSA-65, e o dovada (verifyEquivocation ok, findEquivocation o gaseste)'); +} + for (const l of linii) console.log(l); console.log(`agent-ledger: ${ok}/${ok + rau} cum trebuia`); process.exitCode = rau ? 1 : 0; diff --git a/conformance/README.md b/conformance/README.md index 1ddccfd..9700161 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -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`): 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 +`REGULI.json` is derived from the code (`node deriva-reguli.mjs --verifica`): 241 rules of 15 verifiers, 21 artifact kinds (since +2026-10-19: 241 with `sdjwt.hybrid-half`, AIP-25 finding C1; 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`, 50 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 | @@ -60,8 +60,8 @@ verifiers earlier that day; 181 rules of 11 verifiers, 15 kinds before). By leve | `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` | 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` | +| `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) | 24: `sdjwt.form`, `.alg`, `.typ`, `.crit`, `.key`, `.hybrid-half` (since 2026-10-19), `.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 24 of `sdjwt`: `sdjwtvc.vct-expected`, `.vct`, `.vct-mismatch`, `.disclosed-protected`, `.form`, `.status-list`, `.status-index`, `.status-revoked`, `.status-unjudged` | | `travel-rule-binding`, `travel-rule-message`, `travel-rule-receipt` (the Travel Rule between VASPs: the beneficiary's KEM key binding with its VASP presentation, judged by the originator with its id, its nonce, its clock, the registries it trusts and their status lists; the sealed IVMS101 message, judged by the beneficiary with its id, its own KEM key pair, which decrypts, its clock, its registries and lists and its replay store; the signed receipt, judged by the originator with the message it sent and the beneficiary it accepted) | `identity/travel-rule.mjs` (enrolled 2026-10-18) | 23: `travelrule.binding-form`, `.binding-id`, `.binding-signature`, `.binding-window`, `.vasp-presentation`, `.vasp-status`, `.vasp-identity`, `.vasp-claim`, `.message-form`, `.message-id`, `.message-signature`, `.message-recipient`, `.message-kem-keys`, `.message-freshness`, `.replay-store`, `.replay`, `.kem-alg`, `.kem-open`, `.ciphertext`, `.receipt-form`, `.receipt-from`, `.receipt-signature`, `.receipt-message` | | `compliance-presentation` (a presentation of a credential judged against the verifier's compliance policy: the issuers it believes and the claims it requires, with the verifier's audience, nonce and clock, all required, and the status lists and revocations it holds) | `identity/conformitate.mjs` (enrolled 2026-10-18) | 3: `compliance.presentation`, `.status`, `.rule` | @@ -116,9 +116,9 @@ issuer and for its status at once. ## The vectors -`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`: +`vectori/` holds 631 vectors (622 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 +10, audit-consistency 4, agent-ledger 18, identity-presentation 69, device-bundle 194, sdjwt-presentation 59, sdjwt-vc-presentation 32, travel-rule-binding 19, travel-rule-message 26, travel-rule-receipt 9, compliance-presentation 12. By expected verdict: 89 `VALID`, 70 `PARTIAL`, 472 `INVALID`. None is typed by hand (`node genereaza-vectori.mjs --verifica` regenerates them and compares byte for byte): - 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 @@ -194,7 +194,7 @@ not JSON at all: a file that is not an envelope is refused at the form level, by the vectors show (`{ log, head?, signer? }` for an audit log, `{ ledger, policy }` for an agent's ledger, `{ bundle }` for a trust chain, `{ presentation, now, audience?, nonce?, trustedIssuers?, statusLists?, revocations?, maxAgeS? }` for a presentation of a credential, `{ bundle, now, nonce?, audience?, trustedManufacturers?, statusLists?, firmwarePolicies?, statusRecords?, -softwareAttestation?, anchors?, maxAgeS? }` for a device bundle, `{ sdJwt, issuerKey, issuerAlg?, now, audience?, nonce?, +softwareAttestation?, anchors?, maxAgeS? }` for a device bundle, `{ sdJwt, issuerKey, issuerAlg?, classicalHalf?, now, audience?, nonce?, expectedIssuer?, expectedTyp?, requireKeyBinding?, requireExp?, maxAgeS? }` for an SD-JWT presentation and the same plus `{ expectedVct, statusListJwt?, statusListKey?, statusListAlg?, requireStatus? }` for an SD-JWT VC, `{ binding, presentation, registries, originatorId, nonce, now, statusLists?, maxAgeS? }` for a Travel Rule binding (`originatorId` and `nonce` are the @@ -225,6 +225,29 @@ not implementations of this suite's contract). ## How the suite itself is checked +Measured on 2026-10-19 (UTC), after the SD-JWT verifier gained the rule `hybrid-half` (AIP-25 finding C1, D-535: with the keys of an +AERE hybrid identity the ML-DSA-65 half is judged by default and the Ed25519 half only when the verifier asks for it, `classicalHalf`; +until then the Ed25519 half was the default, so a token signed with it alone verified against the whole hybrid identity). Five vectors +were derived from the existing fresh source `sdjwt` (no fresh source was remade; the issuer's two public JWKs give its AERE public keys, +since the SPKI of a key is canonical): two VALID (the ML-DSA-65 credential against the hybrid keys; the Ed25519 credential with +`classicalHalf`) and three INVALID by `sdjwt.hybrid-half` (the Ed25519 credential with nothing asked; `issuerAlg` EdDSA named without +asking; `classicalHalf` removed). The other SD-JWT and SD-JWT VC vectors changed only in `exercises`, which now lists +`sdjwt.hybrid-half`, and one title; their tokens judged with a plain Ed25519 JWK stay VALID (there is no post-quantum half to lose), +and the verdict of the verifier says `postQuantum: false` for them. On Node.js 24.14.1, offline, with the served reference verifier: +`node proba-conformance.mjs` gives 12 rows green, 0 red, 1 not measured (the 9 vectors that read testnet 28001). 241 rules of 15 +verifiers, 631 vectors (`LA ZI`), every rule has a vector (241 of 241), every INVALID vector (472) is its base with one operation +(306) or one declared producer change (166), the published verifiers pass 622 of 622 offline vectors, the independent Python +implementation passes the 65 it claims and reports the other 566 not covered, 101,947 malformed inputs each got a verdict. +`node proba-conformance.mjs --control-negativ --doar sdjwt` (offline) gives 39 rows green, 0 red, 2 not measured (the rules +and device halves left out by `--doar`): 33 of 33 SD-JWT and SD-JWT VC rules proven load-bearing, `sdjwt.hybrid-half` among them +(planted out, exactly its 3 vectors fail). Not run after the change: `--online`, and `--control-negativ` over the rules of the other verifiers. +Run online later the same night (01:54-02:13 UTC), with the reference verifier as served at aere.network/tools (fetched and installed +in an empty folder) and testnet 28001 read: `node proba-conformance.mjs --online` gives 12 rows green, 0 red; the published verifiers +pass 631 of 631 vectors, the independent Python implementation passes the 65 it claims and reports the other 566 not covered, 101,947 +malformed inputs and 183 malformed envelope files each got a verdict. `node proba-conformance.mjs --control-negativ --online` gives 323 +rows green, 0 red: 241 of 241 rules proven load-bearing through their vectors; the other 82 rows are the conditions of the device +verifier and the controls of the instruments. + 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 diff --git a/conformance/REGULI.json b/conformance/REGULI.json index 0a92b1c..cef004f 100644 --- a/conformance/REGULI.json +++ b/conformance/REGULI.json @@ -1445,6 +1445,13 @@ "sites": 3, "says": "the key given is a public EC, OKP or AKP JWK that can be read, and of the algorithm the JWS declares (no algorithm confusion)" }, + { + "id": "sdjwt.hybrid-half", + "source": "hybrid-half", + "level": "authenticity", + "sites": 2, + "says": "with the keys of an AERE hybrid identity (Ed25519 and ML-DSA-65), the ML-DSA-65 half is judged by default; the Ed25519 half alone is judged only when the verifier asks for it (classicalHalf), and the verdict then says it is not post-quantum" + }, { "id": "sdjwt.signature", "source": "signature", diff --git a/conformance/genereaza-vectori.mjs b/conformance/genereaza-vectori.mjs index 77d443d..0650de9 100644 --- a/conformance/genereaza-vectori.mjs +++ b/conformance/genereaza-vectori.mjs @@ -416,8 +416,9 @@ const PROASPETE_IN_PLUS = { const iss = ID.generateKeys(), hol = ID.generateKeys(), strain = ID.generateKeys(); const AUD = 'https://verifier.example', NONCE = 'n-' + crypto.randomBytes(8).toString('hex'), ISS = 'https://issuer.example'; const CL = { given_name: 'Ada', family_name: 'Example', age_over_18: true, nationalities: ['RO', 'DE'], address: { locality: 'Cluj', country: 'RO' } }; - const emite = (o = {}) => SJ.issueSdJwt({ issuer: iss, holder: hol, claims: CL, disclosable: ['given_name', 'family_name', 'age_over_18'], arrayDisclosable: ['nationalities'], iss: ISS, iat: NOW, exp: NOW + 365 * 86400, decoys: 2, ...o }); - const arata = (sd, reveal, o = {}) => SJ.presentSdJwt({ sdJwt: sd, reveal, holder: hol, audience: AUD, nonce: NONCE, iat: NOW, ...o }); + // 2026-10-19 (AIP-25 C1): the recorded source used the Ed25519 half of these AERE keys by default; it is asked for explicitly now + const emite = (o = {}) => SJ.issueSdJwt({ alg: 'EdDSA', holderAlg: 'EdDSA', classicalHalf: true, issuer: iss, holder: hol, claims: CL, disclosable: ['given_name', 'family_name', 'age_over_18'], arrayDisclosable: ['nationalities'], iss: ISS, iat: NOW, exp: NOW + 365 * 86400, decoys: 2, ...o }); + const arata = (sd, reveal, o = {}) => SJ.presentSdJwt({ classicalHalf: true, sdJwt: sd, reveal, holder: hol, audience: AUD, nonce: NONCE, iat: NOW, ...o }); const b64u = (x) => Buffer.from(x).toString('base64url'); const dec = (s) => JSON.parse(Buffer.from(s, 'base64url').toString('utf8')); const jwtDin = (antet, corp, cheie = iss.privat.ed25519) => { const cap = b64u(JSON.stringify(antet)) + '.' + b64u(JSON.stringify(corp)); return cap + '.' + b64u(crypto.sign(null, Buffer.from(cap), cheie)); }; const sha = (d) => crypto.createHash('sha256').update(d).digest('base64url'); @@ -427,11 +428,11 @@ const PROASPETE_IN_PLUS = { const bazaDirect = direct.split('~').slice(0, -1).join('~') + '~'; const kbPeste = (baza, corpKb = {}, cheie = hol.privat.ed25519, antet = { alg: 'EdDSA', typ: 'kb+jwt' }) => baza + jwtDin(antet, { iat: NOW, aud: AUD, nonce: NONCE, sd_hash: sha(baza), ...corpKb }, cheie); // a hand-made issuer-signed JWT (EdDSA, typ aere+sd-jwt, cnf = the holder key), no key binding - const jeton = (extra, antet = { alg: 'EdDSA', typ: SJ.TYP }) => jwtDin(antet, { iss: ISS, iat: NOW, exp: NOW + 365 * 86400, _sd_alg: 'sha-256', cnf: { jwk: SJ.publicJwk(hol) }, ...extra }) + '~'; + const jeton = (extra, antet = { alg: 'EdDSA', typ: SJ.TYP }) => jwtDin(antet, { iss: ISS, iat: NOW, exp: NOW + 365 * 86400, _sd_alg: 'sha-256', cnf: { jwk: SJ.publicJwk(hol, 'EdDSA', { classicalHalf: true }) }, ...extra }) + '~'; const dz = (c) => { const d = b64u(JSON.stringify(c)); return { d, dg: sha(d) }; }; const d1 = dz(['salt-1', 'a', 1]), dSd = dz(['salt-2', '_sd', ['x']]), dRole = dz(['salt-3', 'role', 'admin']), dUnu = dz(['only-a-salt']); return { now: iso(NOW + 60), later: iso(NOW + 3660), muchLater: iso(NOW + 400 * 86400), audience: AUD, nonce: NONCE, issuer: ISS, - issuerJwk: SJ.publicJwk(iss), issuerJwkMlDsa: SJ.publicJwk(iss, 'ML-DSA-65'), holderJwk: SJ.publicJwk(hol), + issuerJwk: SJ.publicJwk(iss, 'EdDSA', { classicalHalf: true }), issuerJwkMlDsa: SJ.publicJwk(iss, 'ML-DSA-65'), holderJwk: SJ.publicJwk(hol, 'EdDSA', { classicalHalf: true }), issued: E.sdJwt, direct, directMlDsa: arata(M.sdJwt, ['age_over_18'], { holderAlg: 'ML-DSA-65' }), forged: { kbStranger: arata(E.sdJwt, ['age_over_18'], { holder: strain }), @@ -464,26 +465,27 @@ const PROASPETE_IN_PLUS = { const iss = ID.generateKeys(), hol = ID.generateKeys(), strain = ID.generateKeys(); const AUD = 'https://verifier.example', NONCE = 'n-' + crypto.randomBytes(8).toString('hex'), ISS = 'https://issuer.example', VCT = 'urn:eudi:pid:1', URI = 'https://issuer.example/status/pid/1', IDX = 5; const CL = { given_name: 'Ada', family_name: 'Example', birthdate: '1990-01-01', age_over_18: true, nationalities: ['RO', 'DE'], address: { locality: 'Cluj', country: 'RO' }, issuing_authority: 'Example Authority', issuing_country: 'RO' }; - const emite = (o = {}) => SV.issueSdJwtVc({ issuer: iss, holder: hol, vct: VCT, claims: CL, disclosable: ['given_name', 'family_name', 'birthdate', 'age_over_18', 'address'], arrayDisclosable: ['nationalities'], iss: ISS, iat: NOW, nbf: NOW, exp: NOW + 365 * 86400, status: { status_list: { idx: IDX, uri: URI } }, decoys: 2, ...o }); - const arata = (sd, reveal, o = {}) => SV.presentSdJwtVc({ sdJwt: sd, reveal, holder: hol, audience: AUD, nonce: NONCE, iat: NOW, ...o }); - const lista = (statuses, bits = 1, o = {}) => SV.issueStatusListJwt({ issuer: iss, uri: URI, statuses, bits, iat: NOW, exp: NOW + 3600, ...o }); + // 2026-10-19 (AIP-25 C1): as in the source sdjwt, the Ed25519 half of these AERE keys is asked for explicitly where the recorded source used it + const emite = (o = {}) => SV.issueSdJwtVc({ alg: 'EdDSA', holderAlg: 'EdDSA', classicalHalf: true, issuer: iss, holder: hol, vct: VCT, claims: CL, disclosable: ['given_name', 'family_name', 'birthdate', 'age_over_18', 'address'], arrayDisclosable: ['nationalities'], iss: ISS, iat: NOW, nbf: NOW, exp: NOW + 365 * 86400, status: { status_list: { idx: IDX, uri: URI } }, decoys: 2, ...o }); + const arata = (sd, reveal, o = {}) => SV.presentSdJwtVc({ classicalHalf: true, sdJwt: sd, reveal, holder: hol, audience: AUD, nonce: NONCE, iat: NOW, ...o }); + const lista = (statuses, bits = 1, o = {}) => SV.issueStatusListJwt({ alg: 'EdDSA', classicalHalf: true, issuer: iss, uri: URI, statuses, bits, iat: NOW, exp: NOW + 3600, ...o }); const b64u = (x) => Buffer.from(x).toString('base64url'); const jwtDin = (antet, corp, cheie = iss.privat.ed25519) => { const cap = b64u(JSON.stringify(antet)) + '.' + b64u(JSON.stringify(corp)); return cap + '.' + b64u(crypto.sign(null, Buffer.from(cap), cheie)); }; const sha = (d) => crypto.createHash('sha256').update(d).digest('base64url'); const T0 = new Array(16).fill(0), REV = T0.map((_, i) => (i === IDX ? 1 : 0)), SUS = T0.map((_, i) => (i === IDX ? 2 : 0)); - const E = emite(), M = emite({ alg: 'ML-DSA-65', holderAlg: 'ML-DSA-65' }), faraStare = SV.issueSdJwtVc({ issuer: iss, holder: hol, vct: VCT, claims: CL, disclosable: ['birthdate'], iss: ISS, iat: NOW, exp: NOW + 365 * 86400 }); + const E = emite(), M = emite({ alg: 'ML-DSA-65', holderAlg: 'ML-DSA-65' }), faraStare = SV.issueSdJwtVc({ alg: 'EdDSA', holderAlg: 'EdDSA', classicalHalf: true, issuer: iss, holder: hol, vct: VCT, claims: CL, disclosable: ['birthdate'], iss: ISS, iat: NOW, exp: NOW + 365 * 86400 }); const reveal = ['birthdate', 'age_over_18', { element: 'RO', in: 'nationalities' }]; const corpL = JSON.parse(Buffer.from(lista(T0).split('.')[1], 'base64url').toString('utf8')); - const jeton = (extra, dezv = []) => jwtDin({ alg: 'EdDSA', typ: 'dc+sd-jwt' }, { vct: VCT, iss: ISS, iat: NOW, exp: NOW + 365 * 86400, _sd_alg: 'sha-256', cnf: { jwk: SJ.publicJwk(hol) }, ...extra }) + '~' + dezv.map((d) => d + '~').join(''); + const jeton = (extra, dezv = []) => jwtDin({ alg: 'EdDSA', typ: 'dc+sd-jwt' }, { vct: VCT, iss: ISS, iat: NOW, exp: NOW + 365 * 86400, _sd_alg: 'sha-256', cnf: { jwk: SJ.publicJwk(hol, 'EdDSA', { classicalHalf: true }) }, ...extra }) + '~' + dezv.map((d) => d + '~').join(''); const dStatus = b64u(JSON.stringify(['salt-s', 'status', { status_list: { idx: 1, uri: URI } }])); - return { now: iso(NOW + 60), audience: AUD, nonce: NONCE, issuer: ISS, vct: VCT, uri: URI, idx: IDX, issuerJwk: SJ.publicJwk(iss), issuerJwkMlDsa: SJ.publicJwk(iss, 'ML-DSA-65'), + return { now: iso(NOW + 60), audience: AUD, nonce: NONCE, issuer: ISS, vct: VCT, uri: URI, idx: IDX, issuerJwk: SJ.publicJwk(iss, 'EdDSA', { classicalHalf: true }), issuerJwkMlDsa: SJ.publicJwk(iss, 'ML-DSA-65'), direct: arata(E.sdJwt, reveal), directMlDsa: arata(M.sdJwt, reveal, { holderAlg: 'ML-DSA-65' }), noStatus: arata(faraStare.sdJwt, ['birthdate']), lists: { clean: lista(T0), revoked: lista(REV), suspended: lista(SUS, 2), eightBits: lista(T0, 8), otherUri: lista(T0, 1, { uri: URI + '-other' }), expired: lista(T0, 1, { exp: NOW + 30 }), - stranger: SV.issueStatusListJwt({ issuer: strain, uri: URI, statuses: T0, iat: NOW }), short: SV.issueStatusListJwt({ issuer: iss, uri: URI, statuses: [0, 0, 0, 0], bits: 8, iat: NOW }), + stranger: SV.issueStatusListJwt({ alg: 'EdDSA', classicalHalf: true, issuer: strain, uri: URI, statuses: T0, iat: NOW }), short: SV.issueStatusListJwt({ alg: 'EdDSA', classicalHalf: true, issuer: iss, uri: URI, statuses: [0, 0, 0, 0], bits: 8, iat: NOW }), typJwt: jwtDin({ alg: 'EdDSA', typ: 'JWT' }, corpL), noIat: jwtDin({ alg: 'EdDSA', typ: 'statuslist+jwt' }, (({ iat, ...r }) => r)(corpL)), mlDsa: SV.issueStatusListJwt({ issuer: iss, alg: 'ML-DSA-65', uri: URI, statuses: T0, iat: NOW, exp: NOW + 3600 }) }, forged: { - noVct: arata(SJ.issueSdJwt({ issuer: iss, holder: hol, claims: { given_name: 'Ada' }, iss: ISS, iat: NOW, exp: NOW + 365 * 86400, typ: 'dc+sd-jwt' }).sdJwt, []), + noVct: arata(SJ.issueSdJwt({ alg: 'EdDSA', holderAlg: 'EdDSA', classicalHalf: true, issuer: iss, holder: hol, claims: { given_name: 'Ada' }, iss: ISS, iat: NOW, exp: NOW + 365 * 86400, typ: 'dc+sd-jwt' }).sdJwt, []), statusDisclosed: jeton({ _sd: [sha(dStatus)] }, [dStatus]), statusIdxNegative: jeton({ status: { status_list: { idx: -1, uri: URI } } }), statusNotObject: jeton({ status: 'revoked' }), @@ -1230,7 +1232,7 @@ function construieste() { const srcA = 'producers (sdjwt.mjs: issueSdJwt, presentSdJwt), three hybrid key pairs made and discarded at generation'; const bo = 'sdjwt-presentation/aere.valid', bom = 'sdjwt-presentation/aere-ml-dsa-65.valid', bi = 'sdjwt-presentation/aere-issued.valid'; const intrareA = (sdJwt, o = {}) => ({ sdJwt, issuerKey: X.issuerJwk, issuerAlg: 'EdDSA', audience: X.audience, nonce: X.nonce, now: X.now, expectedIssuer: X.issuer, ...o }); - valid(bo, A, 'an SD-JWT issued with AERE keys (EdDSA): two of three disclosable claims and one list element shown, two decoy digests, a key binding JWT for this verifier', srcA, intrareA(X.direct), { exercises: SK, levels: AP }); + valid(bo, A, 'an SD-JWT issued with AERE keys (EdDSA), judged with the Ed25519 half given as a plain JWK (classical interoperation: the verdict is not post-quantum): two of three disclosable claims and one list element shown, two decoy digests, a key binding JWT for this verifier', srcA, intrareA(X.direct), { exercises: SK, levels: AP }); valid(bom, A, 'the same credential, issuer and holder keys ML-DSA-65 (RFC 9964, JWK type AKP)', srcA, intrareA(X.directMlDsa, { issuerKey: X.issuerJwkMlDsa, issuerAlg: 'ML-DSA-65' }), { exercises: SK, levels: AP }); valid(bi, A, 'the same SD-JWT as issued (every disclosure, no key binding), judged by a verifier that does not require key binding', srcA, { sdJwt: X.issued, issuerKey: X.issuerJwk, issuerAlg: 'EdDSA', now: X.now, requireKeyBinding: false }, { exercises: SK, levels: AP }); const F = X.forged; @@ -1256,6 +1258,18 @@ function construieste() { prod('sdjwt-presentation/aere-issued.disclosure-form-not-json', 'a disclosure that is base64url of text that is not JSON', bi, F.disclosureNotJson, 'sdjwt.disclosure-form', 'the issuer signed the digest of a disclosure whose bytes are not JSON'); prod('sdjwt-presentation/aere-issued.disclosure-form-sd', '_sd is a string, not a list of digests', bi, F.sdNotList, 'sdjwt.disclosure-form', 'the issuer signed a payload whose _sd is a string'); inv('sdjwt-presentation/aere-issued.time-years-later', 'the verifier clock is years later: the credential has expired', bi, { op: 'set', path: ['now'], value: X.muchLater }, 'sdjwt.time'); + // 2026-10-19 (AIP-25 C1, D-535): the same issuer keys given the way an AERE identity publishes them ({alg, ed25519, mldsa65}, the output + // of `identity-cli.mjs pub`), rebuilt from the two public JWKs of the fresh source (the SPKI of a key is canonical, so this is the object of + // that identity). Against hybrid keys the ML-DSA-65 half is judged; the Ed25519 half only when the verifier asks for it (classicalHalf) + const spki = (jwk) => crypto.createPublicKey({ key: jwk, format: 'jwk' }).export({ type: 'spki', format: 'der' }).toString('base64'); + const aerePub = { alg: 'ed25519+ml-dsa-65', ed25519: spki(X.issuerJwk), mldsa65: spki(X.issuerJwkMlDsa) }; + const bh = 'sdjwt-presentation/aere-hybrid-keys.valid', bhc = 'sdjwt-presentation/aere-hybrid-keys-classical.valid'; + const intrareH = (sdJwt, o = {}) => ({ sdJwt, issuerKey: aerePub, audience: X.audience, nonce: X.nonce, now: X.now, expectedIssuer: X.issuer, ...o }); + valid(bh, A, 'the credential signed with the ML-DSA-65 half (issuer and holder), judged against the issuer\'s AERE hybrid keys ({alg, ed25519, mldsa65}) with no algorithm named: the ML-DSA-65 half is the one judged', srcA, intrareH(X.directMlDsa), { exercises: SK, levels: AP }); + inv('sdjwt-presentation/aere-hybrid-keys.hybrid-half', 'a credential signed with the Ed25519 half of the same AERE identity alone, against its hybrid keys, by a verifier that asks for nothing (what a holder of the classical key alone can make)', bh, { op: 'set', path: ['sdJwt'], value: X.direct }, 'sdjwt.hybrid-half'); + inv('sdjwt-presentation/aere-hybrid-keys.hybrid-half-named', 'the verifier names EdDSA for the AERE hybrid keys without asking for the classical half (classicalHalf)', bh, { op: 'add', path: ['issuerAlg'], value: 'EdDSA' }, 'sdjwt.hybrid-half'); + valid(bhc, A, 'the credential signed with the Ed25519 half alone, against the AERE hybrid keys, by a verifier that asks for the classical half (classicalHalf): VALID, and the verdict says it is not post-quantum', srcA, intrareH(X.direct, { classicalHalf: true }), { exercises: SK, levels: AP }); + inv('sdjwt-presentation/aere-hybrid-keys-classical.hybrid-half', 'the same, the verifier no longer asking for the classical half', bhc, { op: 'remove', path: ['classicalHalf'] }, 'sdjwt.hybrid-half'); // @sd-jwt/core (openwallet-foundation-labs/identity-common-ts), from the interop fixtures: its tokens, with its public test keys const srcIo = `@sd-jwt/core ${IO.biblioteci['@sd-jwt/core']} with jose ${IO.biblioteci.jose} (interop/fixturi-interop-sdjwt.json of the identity component, generated ${IO.generat.slice(0, 10)}; its test keys made and discarded at generation)`; const laIo = new Date(IO.ceasVerificare * 1000).toISOString(); diff --git a/conformance/implementare-aere.mjs b/conformance/implementare-aere.mjs index 8890f42..49e0e8e 100644 --- a/conformance/implementare-aere.mjs +++ b/conformance/implementare-aere.mjs @@ -182,7 +182,9 @@ async function judeca(artifact, input, online) { const vc = artifact === 'sdjwt-vc-presentation'; const M = await incarca(vc ? 'sdjwtvc' : 'sdjwt'); const RB = VERIFICATOARE.sdjwt.reguli, RV = VERIFICATOARE['sdjwt-vc'].reguli; if (typeof input.now !== 'string') throw new Error('an SD-JWT 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 ['issuerKey', 'issuerAlg', 'audience', 'nonce', 'expectedIssuer', 'requireKeyBinding', 'requireExp', 'maxAgeS', 'expectedTyp', 'expectedVct', 'statusListJwt', 'statusListKey', 'statusListAlg', 'requireStatus']) if (input[k] !== undefined) o[k] = input[k]; + // 2026-10-19 (AIP-25 C1): classicalHalf, which the verifier brings (the Ed25519 half of AERE keys judged on request), is passed too: a + // field this adapter does not name never reaches the verifier + const o = { now: input.now }; for (const k of ['issuerKey', 'issuerAlg', 'classicalHalf', 'audience', 'nonce', 'expectedIssuer', 'requireKeyBinding', 'requireExp', 'maxAgeS', 'expectedTyp', 'expectedVct', 'statusListJwt', 'statusListKey', 'statusListAlg', 'requireStatus']) if (input[k] !== undefined) o[k] = input[k]; const r = vc ? M.verifySdJwtVc(input.sdJwt, o) : M.verifySdJwt(input.sdJwt, o); if (r.valid !== (r.rule == null)) throw new Error('the SD-JWT verifier refused without naming a rule'); if (!r.valid) { @@ -190,7 +192,8 @@ async function judeca(artifact, input, online) { pica(acc, m[1], m[0], r.reason); nemasurat(acc, m[1] === 'integrity' ? 'authenticity' : 'integrity', 'not-reached', 'the verifier stopped at its first refusal'); } else { trece(acc, 'integrity', `no rule of the SD-JWT${vc ? ' VC' : ''} verifier refused: ${r.disclosed.length} disclosure(s) bound to the issuer-signed JWT${vc ? ', vct ' + r.vct : ''}`); - trece(acc, 'authenticity', `the issuer-signed JWT verifies (${r.alg}) with the issuer key the verifier gave` + (r.keyBinding ? `; the key binding JWT (${r.keyBinding.alg}) verifies with cnf.jwk for this audience and nonce` : '; no key binding (the verifier did not require one)')); + trece(acc, 'authenticity', `the issuer-signed JWT verifies (${r.alg}) with the issuer key the verifier gave` + (r.keyBinding ? `; the key binding JWT (${r.keyBinding.alg}) verifies with cnf.jwk for this audience and nonce` : '; no key binding (the verifier did not require one)') + + (r.postQuantum === true ? '; every signature judged is post-quantum (ML-DSA-65)' : r.postQuantum === false ? '; NOT post-quantum: a signature judged is classical' : '')); if (vc && r.status && r.status.present && !r.status.judged) nemasurat(acc, 'integrity', 'claim-not-checked', `the credential names a status (${r.status.mechanism}${r.status.uri ? ' at ' + r.status.uri : ''}) and no list was judged: the verifier waived it`); } for (const l of ['anchoring', 'finality']) nemasurat(acc, l, 'not-judged', 'an SD-JWT is anchored by notarizing an AIP-23 envelope of it, judged as the artifact `envelope`'); diff --git a/conformance/proba-conformance.mjs b/conformance/proba-conformance.mjs index 3cd3fb3..f15e50c 100644 --- a/conformance/proba-conformance.mjs +++ b/conformance/proba-conformance.mjs @@ -190,7 +190,7 @@ const ramase = () => [...Object.values(MODULE).map((m) => path.dirname(fileURLTo async function malformate() { const R = await incarca('risc'), C = await incarca('chitanta'), P = await incarca('piata'), B = await incarca('baza'), SC = await incarca('sidecar'), AR = await incarca('arbore'), L = await incarca('ledger'), ID = await incarca('identity'), DV = await incarca('device'), SJ = await incarca('sdjwt'), SV = await incarca('sdjwtvc'), TRm = await incarca('travelrule'), CF = await incarca('compliance'); const clona = (x) => JSON.parse(JSON.stringify(x)); // the SD-JWT verifiers (2026-10-18), called the way the adapter calls them; a VC whose status was waived and not judged is PARTIAL, not accepted - const optSd = (i) => { const o = { now: i.now }; for (const k of ['issuerKey', 'issuerAlg', 'audience', 'nonce', 'expectedIssuer', 'requireKeyBinding', 'requireExp', 'maxAgeS', 'expectedTyp', 'expectedVct', 'statusListJwt', 'statusListKey', 'statusListAlg', 'requireStatus']) if (i[k] !== undefined) o[k] = i[k]; return o; }; + const optSd = (i) => { const o = { now: i.now }; for (const k of ['issuerKey', 'issuerAlg', 'classicalHalf', 'audience', 'nonce', 'expectedIssuer', 'requireKeyBinding', 'requireExp', 'maxAgeS', 'expectedTyp', 'expectedVct', 'statusListJwt', 'statusListKey', 'statusListAlg', 'requireStatus']) if (i[k] !== undefined) o[k] = i[k]; return o; }; const J = { 'payment-risk': (i) => R.judeca(i.policy, i.inputs).decision === 'ALLOW', 'payment-receipt': (i) => C.verificaChitanta(i.receipt, { policy: i.policy, inputs: i.inputs, tx: i.tx, receipt: i.txReceipt }).verdict === 'VALID', 'market-timeline': (i) => P.judeca(i).verdict === 'VALID', 'dataset-record': (i) => B.verificaInregistrare(i.record, i.commitment).ok, 'dataset-answer': (i) => B.verificaRaspuns(i.answer, i.commitment).verdict === 'VALID', 'dataset-consistency': (i) => B.verificaConsistentaVersiuni(i.proof, i.older, i.newer).ok, // the verifiers enrolled after the first four (2026-10-05), called the way the adapter calls them; ai-trust is not here: each case would run the reference verifier several times diff --git a/conformance/reguli.mjs b/conformance/reguli.mjs index d301cff..186ded7 100644 --- a/conformance/reguli.mjs +++ b/conformance/reguli.mjs @@ -260,6 +260,8 @@ export const VERIFICATOARE = { typ: ['sdjwt.typ', I, 'the issuer-signed JWT carries the explicit type the verifier expects (any ...+sd-jwt by default; RFC 9901 Section 9.11) and the key binding JWT kb+jwt'], crit: ['sdjwt.crit', I, 'no critical header parameter (crit) is present: none is understood'], key: ['sdjwt.key', AU, 'the key given is a public EC, OKP or AKP JWK that can be read, and of the algorithm the JWS declares (no algorithm confusion)'], + // 2026-10-19 (AIP-25 C1, D-535; the rule cryptopolicy.hybrid-no-single-half of AIP-25 section 5) + 'hybrid-half': ['sdjwt.hybrid-half', AU, 'with the keys of an AERE hybrid identity (Ed25519 and ML-DSA-65), the ML-DSA-65 half is judged by default; the Ed25519 half alone is judged only when the verifier asks for it (classicalHalf), and the verdict then says it is not post-quantum'], signature: ['sdjwt.signature', AU, 'the JWS signature verifies with that key: the issuer-signed JWT with the issuer key the verifier chose, the key binding JWT with cnf.jwk'], issuer: ['sdjwt.issuer', AU, 'iss is the issuer the verifier expects, when it names one'], 'sd-alg': ['sdjwt.sd-alg', I, '_sd_alg is sha-256 (or absent), and appears only at the top level of the payload (RFC 9901 Section 4.1.1)'], diff --git a/conformance/vectori/INDEX.json b/conformance/vectori/INDEX.json index 84ec81d..efb1db2 100644 --- a/conformance/vectori/INDEX.json +++ b/conformance/vectori/INDEX.json @@ -1,7 +1,7 @@ { "v": 1, "kind": "aere-conformance-vector-index", - "count": 626, + "count": 631, "artifacts": { "envelope": 67, "payment-risk": 11, @@ -14,7 +14,7 @@ "agent-ledger": 18, "identity-presentation": 69, "device-bundle": 194, - "sdjwt-presentation": 54, + "sdjwt-presentation": 59, "sdjwt-vc-presentation": 32, "travel-rule-binding": 19, "travel-rule-message": 26, @@ -25,7 +25,7 @@ "dataset-consistency": 3, "dataset-sample": 2 }, - "offline": 617, + "offline": 622, "online": 9, "rules": { "agents.anchor": [ @@ -800,6 +800,11 @@ "sdjwt-presentation/rfc9901.form", "sdjwt-presentation/rfc9901-issued.form-empty" ], + "sdjwt.hybrid-half": [ + "sdjwt-presentation/aere-hybrid-keys.hybrid-half", + "sdjwt-presentation/aere-hybrid-keys.hybrid-half-named", + "sdjwt-presentation/aere-hybrid-keys-classical.hybrid-half" + ], "sdjwt.issuer": [ "sdjwt-presentation/rfc9901.issuer" ], @@ -4192,602 +4197,637 @@ "artifact": "sdjwt-presentation", "verdict": "VALID", "requires": [], - "sha256": "0xb17ee5bbb8da8bae3d03143e6160d4d64acf63b55a02c25a16fd4094ed8a9ff8" + "sha256": "0x6297025adb1f6f1632064fac794ec9accfdf172d12928ce62e38eea4cf928c01" }, { "id": "sdjwt-presentation/rfc9901-issued.valid", "artifact": "sdjwt-presentation", "verdict": "VALID", "requires": [], - "sha256": "0x04c37a5643c2709acdd97554beecd6f236422d43e65d359c3a9285e98ffbda35" + "sha256": "0xcdfd0d815893e7c8f4513bc6e8587b2e69e02d469e464b57e3c0187cd6698897" }, { "id": "sdjwt-presentation/rfc9901.kb-audience", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x0ffcf487db005bcbace1d7d0961737b5642d4eabc6ccec2fdf383974657cf50b" + "sha256": "0x839d3f231fa0af51e74bef7458032b6b9ea091fcb29a83feac3ed1bd38f524d6" }, { "id": "sdjwt-presentation/rfc9901.kb-nonce", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xdb638acdc2a716dcdb39bb604193563bb635829be33e71335c591c7e579149f0" + "sha256": "0x97807b3af18b30a6d565885a76fc83619ba21eb1b1035dea545e5e4ea0fc015d" }, { "id": "sdjwt-presentation/rfc9901.kb-freshness", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x87d7a9a0b9024a3650ac95e626ed9b6431d8b681e035f4737e7e8769e273eb56" + "sha256": "0xa53df394b1f5fcb38015965544f4901eb023c572a7d2354d3d5d4c384024cd7e" }, { "id": "sdjwt-presentation/rfc9901.kb-context", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xe4d777dc0cf33955839d38d08fb6964d443045b41855f124f0b4b07ec6ae8441" + "sha256": "0xe636e6f0bcfcffdccd97e6a57989592d184d38b7738f8575b6d283b51f45b6f2" }, { "id": "sdjwt-presentation/rfc9901.issuer", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x79002f5b01ff4319a8ade3c075ab529e4a8f21b0901836495a9b664dd9ea498e" + "sha256": "0xa513d3d4adf019411418c1648dc50f51e5fde7c9cb7ceaf1221cae4be94d2756" }, { "id": "sdjwt-presentation/rfc9901.key", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xd4f715d484688437ceaed4de485dfc60d23474474c99cb3d5c5ed08e9c114ad5" + "sha256": "0xfe068cf4af8b01b6948d89e84591f845a08abb484cb9c73cdc7adec8ccb79c4f" }, { "id": "sdjwt-presentation/rfc9901.signature", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xbed1f3e911242d854ed61119b0ea7ed3d3ba7b22c66f0093fc411cd07b337ba4" + "sha256": "0xc314cafb0808cca3109e501d8d8a22a21e8cbcf0d61425c17c208d9ee8d915a7" }, { "id": "sdjwt-presentation/rfc9901.typ", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x610b0511d94253f4ed741599f8bdc8560de2ce824a6b265b02af851ef2ad2468" + "sha256": "0xc0379e76fd186ebd32a6a3b7c69b93332eca79e2dd222880fdc31415cf998e43" }, { "id": "sdjwt-presentation/rfc9901.form-clock", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x0548482f4dd888b221529a681153f98468c05eeceb71124a062ed6cd3fecd4ff" + "sha256": "0x92ff28c6cf96b0efd277217d7d4e63e86c0fdbffce8f5e0968965d4527f93903" }, { "id": "sdjwt-presentation/rfc9901.form", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x75c50a18e68f039ee30268349b4f8d596243630283c7aac37e52bfe86a4f4cd9" + "sha256": "0x8cd892946b5019abbe3c9ec60d43d46a218a2cadb187d75beb3465d3a04835a4" }, { "id": "sdjwt-presentation/rfc9901.kb-required", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xf7e658e06a4915c8c3c2316e71d94aa39f07f0a8143f711b37cd10c3d215064d" + "sha256": "0x1dd20021e03c45a4e7f698dd294898e83e57b40fd6688f957b01103064f5817e" }, { "id": "sdjwt-presentation/rfc9901.kb-sd-hash", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xfbd3aa0ad889053fc367fe07d0ba9c3cb317f9699b5471bdb3945a3b72380ea4" + "sha256": "0x9c3689026ea8eb2d71780a874c817c252b3fc40161d691b4bb298f9c72f1d7c1" }, { "id": "sdjwt-presentation/rfc9901-issued.disclosure-twice", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x0e7b8c7215d4a411169962bb2427ad362a5d861508c1c694e7a5efa5a71d42d5" + "sha256": "0x20a06347b8b0854c88de27c9a11c437d3af628fd4beb5891ad1f6c95b95531d0" }, { "id": "sdjwt-presentation/rfc9901-issued.form-empty", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x4ed4bd8bfcbd330fed266458bedf5924dcd06683918052546ec20b92b15d1f7b" + "sha256": "0x1361876045b34a515150fb7d09d02bf7b7de1a76d4b5d7aed30057e2cc605e9b" }, { "id": "sdjwt-presentation/rfc9901-issued.unreferenced", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x5c948c3cd0334abfacdd0b45c46165d211218516041d21bc966db66c64666283" + "sha256": "0x6221423fedfc063d389d56fc3ee14b75fb1ac49a9ff2cc50b6d032589bf990fd" }, { "id": "sdjwt-presentation/aere.valid", "artifact": "sdjwt-presentation", "verdict": "VALID", "requires": [], - "sha256": "0xa4bf25e6c7be85907e2faa208a3cfbc24b4afee8a9bdc42afa7e4a03402c4244" + "sha256": "0x8a837b2ab7d047c497582dc1574b7b2b4f2e8e2aec081db254f81cfd1826e8be" }, { "id": "sdjwt-presentation/aere-ml-dsa-65.valid", "artifact": "sdjwt-presentation", "verdict": "VALID", "requires": [], - "sha256": "0x566239bb9b499e3d9880278862d039190862ddc739b81ad047a5bc3cd4c2bd84" + "sha256": "0x9f952f16393b4cd223d746d20149d301ee1a6d5e3ef33207ff52fa520f4427a2" }, { "id": "sdjwt-presentation/aere-issued.valid", "artifact": "sdjwt-presentation", "verdict": "VALID", "requires": [], - "sha256": "0x060d56983b8b5866320966db55e0e62bacc03bd50d87f3fabb69909599b6758e" + "sha256": "0x08c2291c7769025d4249793fd10f991e92c4a76660493ff387b56673b2a9d328" }, { "id": "sdjwt-presentation/aere.signature-kb", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x6c4a37b0ee1f278f680076721f8dd9ca13ae438b50d9c5845e260433bcaa1cdf" + "sha256": "0x534d097a76d8029fc6cb07ab6cbbc3fcf7c3a5da789f5ed78ab702e887e9b0fa" }, { "id": "sdjwt-presentation/aere.kb-claims", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xcf83d7bc18ada8ff77c1912c1c3678afbd93a96b790b8af4eb715c0077f41c0e" + "sha256": "0x381b7b32b17981515e8805d6a52366ce1a2fc446eeae0030041886f615108a94" }, { "id": "sdjwt-presentation/aere.typ-kb", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x5d19972b3f596cc41986eacb9ce8e9fe15eefe64d77578d271c9ec323bb80054" + "sha256": "0x8ddf79377615079359055ad9b7823ee40a1542d8fe6b8224edc2fa52b4d24d08" }, { "id": "sdjwt-presentation/aere.kb-freshness-exp", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x113fad84b78b77a54a36b694e446bd97501b53088c53dd0b6370b25e1d1c5d18" + "sha256": "0x02680ab12addc099282d46d0a8c54f331d215e53812b6e0cbb9a6f15b90cf1f8" }, { "id": "sdjwt-presentation/aere.kb-cnf", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xf9d13dac0ffa00600d47be96bbd5c04e7c59eebd0b247ee9638c7246a321b6c0" + "sha256": "0xbda639e6da6277b9d987b46e62413aa5a6f3a859a5455d7f953eab41eb151370" }, { "id": "sdjwt-presentation/aere.time-expired", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x69e121ba34eb04588a2600eb1b8ad64ce72e2f9f995994e2ddad4bd863ce08b3" + "sha256": "0xa7134d24f0f39a869b21a04a60ed0ad7fca1edda55d36e664c6b09b16977e844" }, { "id": "sdjwt-presentation/aere.time-not-yet", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x062fcd99cb0ff9fe38a67b91d3aaf1219f57401b0aa24a0c78be9257d759e9ac" + "sha256": "0xbc2e8502b281345d2f8dc151171027bed2a7ba96e49f18685e33c9857511f755" }, { "id": "sdjwt-presentation/aere-issued.digest-twice", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x44887dec6503ca912cea3a6ddafa4ed5e01333ab826f5259414dc684402e45f0" + "sha256": "0x916256016730e58fba938df6e1c4b9262481b9acf7dc659a4647999bf71982d8" }, { "id": "sdjwt-presentation/aere-issued.sd-alg-nested", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x0ea82f93edb3b19517207915b79574a6da7f1133a257927ef4186cb5d658d3a2" + "sha256": "0xb246f41e5f1d09e7131224b18ac428858044f733261f33aab35b7225e59b9a4e" }, { "id": "sdjwt-presentation/aere-issued.sd-alg", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xa2d1384753a66a5dba6f2687500055c6bf37fc2ffdfa338beba4fbc7206e4301" + "sha256": "0x2b8c994c69a9b98c01df329381e2920ad5dbcbf78ec88401e70a827ce0a18276" }, { "id": "sdjwt-presentation/aere-issued.disclosure-name", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x132e6e91a14bc3d0d6677269ec6c2a398c3d8e1adb194e09fab8bff5c34c21fe" + "sha256": "0x651b65d7a61d6925d1cdc58b376a486b02aa1e8ad11c75a9d93a8a8faaf5daeb" }, { "id": "sdjwt-presentation/aere-issued.disclosure-name-proto", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x5338feb82fe56b769e75c5fb64c3f94efe179401e60b855ca1c87919eacebbd4" + "sha256": "0x45c8b252f83566fa9fd57c501274f906f265d3a53ef15020fbeea152f3527ea8" }, { "id": "sdjwt-presentation/aere-issued.claim-conflict", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xed21f15610061eb4f8e1888283913fb96785e8bf2cd9e7afc8d92a98e2cbc47d" + "sha256": "0xda6db391792d9e1e2c94d5bb3afdf928bf6e89c42ccbedf5e801362d1fb092ff" }, { "id": "sdjwt-presentation/aere-issued.crit", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xa2ce76f79c13e9faec8f0a172bab8af7d41d0be77d2eb7bda7966c27c1a712db" + "sha256": "0xc8580536436a88e827ce1503ef9036a6b424496f65a63b239a8550fff0e48dd4" }, { "id": "sdjwt-presentation/aere-issued.typ", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x66d813e2099009cb947872aff7afba7d53ae5d57d80c4ff30fb44b9d18b3c873" + "sha256": "0xe482ba29d5af3514a58c1e5aa49dc79dce143b37fc6486bf04bc96fae7072a05" }, { "id": "sdjwt-presentation/aere-issued.key-confusion", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x170a83409c1388467a3e3a287f21d965aea111738df4bf608ed4c40cdbcf52ac" + "sha256": "0x4f9a01375efe24c351c65f2604ee35a586bb2e0ad3cadade2b0d47528c6606d6" }, { "id": "sdjwt-presentation/aere-issued.alg-none", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xdd89f1d66bcb56a2f3dbe8153b8c87801f818be52d616702a4d40edcb5102277" + "sha256": "0x88c9e458a457b7b5864880935c3f4ff62d142542af831d139aad8fe13eb0b9da" }, { "id": "sdjwt-presentation/aere-issued.time-no-exp", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xe673e99b16402da40f4fdfef1dc32c77aba7903972bbb277be76ea802366b5e3" + "sha256": "0x5dbea70948a712429000e76a07c552e65815694c2e21feaea02dc53760e9ec65" }, { "id": "sdjwt-presentation/aere-issued.disclosure-form", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x9c63eed2d02e6f8207267937799233ff643de6796b3755d44b8c52a60e9739f8" + "sha256": "0xb3f4ffa98533f5f6ed6b9326d877fe94d687602f0eef1716eccbded481f3737a" }, { "id": "sdjwt-presentation/aere-issued.disclosure-form-not-json", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xb669d3f8a57b0d926beaf415578e809a3b3e7f90440818a60bc2ae6da92a0373" + "sha256": "0xd39fe089f202df98897ee0e83b9711ef8fc864780ff56ffd206b5dec9c8e6dc8" }, { "id": "sdjwt-presentation/aere-issued.disclosure-form-sd", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x6df675c2e2ddda38eca7dcc925af1532418b90318dbdf88c6ac10bbdb9e7e9a1" + "sha256": "0x91706b53e6a0203a94556f33832d3cf105c4176ac8cc69736e69ca2c3422c065" }, { "id": "sdjwt-presentation/aere-issued.time-years-later", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xf660f7dea5fe9bedb4495bb7a0df617385b73ef8ad41797d3a6b5ed77303a926" + "sha256": "0x0612c01535b5105dcb806676919a678febb4f217c25cf599598afe82c8aed574" + }, + { + "id": "sdjwt-presentation/aere-hybrid-keys.valid", + "artifact": "sdjwt-presentation", + "verdict": "VALID", + "requires": [], + "sha256": "0xfd5df2ce6851c740367dfdd4784a4b87deb9e32d9ac527bc3fbef7d4888086c1" + }, + { + "id": "sdjwt-presentation/aere-hybrid-keys.hybrid-half", + "artifact": "sdjwt-presentation", + "verdict": "INVALID", + "requires": [], + "sha256": "0xa77ec219167ca7b8084ae329e516f72cb2b527f262643d7365d89bd31e41e43a" + }, + { + "id": "sdjwt-presentation/aere-hybrid-keys.hybrid-half-named", + "artifact": "sdjwt-presentation", + "verdict": "INVALID", + "requires": [], + "sha256": "0xac0e492b0245d4ede7dd83aed50b76a0fbf86d4e1ab57b3d56893fca5a9cc933" + }, + { + "id": "sdjwt-presentation/aere-hybrid-keys-classical.valid", + "artifact": "sdjwt-presentation", + "verdict": "VALID", + "requires": [], + "sha256": "0x7c0630a55e437188440f5c0bcff6b518a822eee133e616de1066b3d49fca6c7b" + }, + { + "id": "sdjwt-presentation/aere-hybrid-keys-classical.hybrid-half", + "artifact": "sdjwt-presentation", + "verdict": "INVALID", + "requires": [], + "sha256": "0x01fc1c751ed53a96a3a31ccab1ff1a94d6f0f5db65dd22551fc986248566aec9" }, { "id": "sdjwt-presentation/sd-jwt-core.valid", "artifact": "sdjwt-presentation", "verdict": "VALID", "requires": [], - "sha256": "0x71d62658266c8f31fa6aeff83fb5565c407d2b981ab78b6a06e90f580791da59" + "sha256": "0x2e0df797423670574d475084c8cdf83a120cfeaab463be1f22f1fef44ec7b337" }, { "id": "sdjwt-presentation/sd-jwt-core-es256.valid", "artifact": "sdjwt-presentation", "verdict": "VALID", "requires": [], - "sha256": "0x1a63524779967f5fbd00b39ffed1af4dcee3cbf87e92be2be21020a15142c2ac" + "sha256": "0x6462feb9e415e520d6ebec9c105a0a3902ff8773cca1d66c512d75303b92b184" }, { "id": "sdjwt-presentation/sd-jwt-core-ml-dsa-65.valid", "artifact": "sdjwt-presentation", "verdict": "VALID", "requires": [], - "sha256": "0x482b805ad218e84510a35632c63045f310f2b78050d95d0207361ab6bf8b0abc" + "sha256": "0x1a8ff789a4aa9e615090342463bedf88eff748811a0893508829a005785b78a4" }, { "id": "sdjwt-presentation/sd-jwt-core-typed.valid", "artifact": "sdjwt-presentation", "verdict": "VALID", "requires": [], - "sha256": "0xa1d9246df259269051d7a2a7ac3153f7254bb5cc6ff8f37e3c2c2f0368ebbf4f" + "sha256": "0xb5ce2729ad0c98f657fd25bbce6121030d40016d2d64f2f2689983cc39c05e1e" }, { "id": "sdjwt-presentation/sd-jwt-core.typ", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xfb5ba1d694fb5f7bf163f49a1e4d02916ad8cea38a0f677347f69b3fe82478e1" + "sha256": "0x3991ca458d7eea09813c0aef5c9312943f9e86f87df4bc8817c3fe19fd159494" }, { "id": "sdjwt-presentation/sd-jwt-core.unreferenced", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x456d9abc237881556e309710c823998f2e84b53d3a3a4d3b3878454ef80a32e9" + "sha256": "0xbd7648096d92389853766fa3e475ced4885546d6ec29f698de4553bf338c053e" }, { "id": "sdjwt-presentation/sd-jwt-core.kb-audience", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x6cfa3d696a540aaa0ea0b9a721ee4716ee7687bb95413b1119e85f682e0e55dc" + "sha256": "0xd22917dd9e8513c688fd9adb1e46893f9601aaad08d158ae7351445ae34b30cb" }, { "id": "sdjwt-presentation/sd-jwt-core.alg", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x8cd27350a3cd59185d43122746f416477403e75afa100bddca2ed8b064be14db" + "sha256": "0xd733a98c57faf812a14b8c0e27daae13d6abb1b172358e240f02072fc2216f72" }, { "id": "sdjwt-presentation/sd-jwt-core.digest-twice", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x02a188377b49d09c05ad3b1983bbf66063c6be05aadeb7024348efb90c4afed8" + "sha256": "0x85718dac77c316e78d5d6a9dad9cfe08a38f573aebb188a9b6966eec2a414035" }, { "id": "sdjwt-presentation/sd-jwt-core.signature-kb", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x424f7a30fe9735ea5908430cbef3514f1bff7b77ce16ab3e4455fa31e3b1a0d5" + "sha256": "0x9057ca0586b5341940ffd6d61dd87c33b410003d9ccc3366c255d646699f11ed" }, { "id": "sdjwt-presentation/sd-jwt-core.kb-sd-hash", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xfc9fbaf5afb2e166b24747502dd4a07cb8adf72b937546a1e7f2f3cf5daf5f6c" + "sha256": "0xa52f7def3a096fe40500d395e16bab1f813e38857358e8bc31b5cafa07baff03" }, { "id": "sdjwt-presentation/sd-jwt-core.sd-alg", "artifact": "sdjwt-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x72a06db812e62ebf82790e5b6ebe2c17d5db41f171fa6d2c09fd66bbe5519fc8" + "sha256": "0xa152555d488deb7e383ea8a14dd71dfbbab23911fd07338427a2770c1fd42a65" }, { "id": "sdjwt-vc-presentation/aere-pid.valid", "artifact": "sdjwt-vc-presentation", "verdict": "VALID", "requires": [], - "sha256": "0x4037fa40a084b4c2f1a7af6802b2571a93bc8042aae8b38e4d6793464e1bab8b" + "sha256": "0x331053c263e790ebafd6f84c90bd56cf9d124f8337073ef93a842ca325bb41a4" }, { "id": "sdjwt-vc-presentation/aere-pid-ml-dsa-65.valid", "artifact": "sdjwt-vc-presentation", "verdict": "VALID", "requires": [], - "sha256": "0xd9f9a5f9f6ae3565fa24512fb3fc8cf2555c6126bc0712b72959acd2a1aec56b" + "sha256": "0xbc810c7187cc588a04d7ff0f31f34910c680f1912f18be78ebd2f98f579e3109" }, { "id": "sdjwt-vc-presentation/aere-no-status.valid", "artifact": "sdjwt-vc-presentation", "verdict": "VALID", "requires": [], - "sha256": "0xa875911438b0d95388b243f86e76406cd5ae2fa8ba20626f04037a6024816890" + "sha256": "0x48d1aefe93ea144d8b75287679e76fb82117b394402dba016ddcaf72d8791c5e" }, { "id": "sdjwt-vc-presentation/aere-pid-status-waived.partial", "artifact": "sdjwt-vc-presentation", "verdict": "PARTIAL", "requires": [], - "sha256": "0xb5a2b2ec0165ca31cee3ee3eeed8768f1ba2263738c3efd2f99bdceabf04ab55" + "sha256": "0xcddfd5016f2be573ce9fa7657334d8e155d9c6619c64f58ea4bd2dbc2a2732dd" }, { "id": "sdjwt-vc-presentation/aere-pid-8-bits.valid", "artifact": "sdjwt-vc-presentation", "verdict": "VALID", "requires": [], - "sha256": "0x83b6a8c4a65390d5b6debaed5042222620f0a82aff53473792e136925c28195a" + "sha256": "0x9404c128cc2adc14d9ec30bffb8c46c3ff339034e59a67b0baf886b15f567b31" }, { "id": "sdjwt-vc-presentation/aere-pid.vct-expected", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x45771debbe260bbbc6e2a7b5fbe551317d4660948581925a158edae9689b08f7" + "sha256": "0xc1f7584db6222f5b154f33409c8befbedd1dda9645e1a397587a75f26af66fbe" }, { "id": "sdjwt-vc-presentation/aere-pid.vct-mismatch", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x2f1e4cd09d7184e0a9ec39466de9053100ff48636ee9a33dab389c50abc9af62" + "sha256": "0x97d643dac6be2cffa3e11f372c326db82220e1b584b04d67344d2c7a4a1ecbe4" }, { "id": "sdjwt-vc-presentation/aere-pid.status-revoked", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xd9a95cc74c4b566d23e0513c25ea2d177ac3e7e426534995d36748fc50ff7552" + "sha256": "0xea8e1d16512572caa537ae6cca28d24b5d79f7fb88d3bec4bf84057af7729ffe" }, { "id": "sdjwt-vc-presentation/aere-pid.status-suspended", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xa846cc09df82afbe62a374ccf6329cf7d4b2cbfe9ca012696738179d76dc20fe" + "sha256": "0x8d8b6b8be1f0d1e4908afb080abef19f703e6e4c4784edcbd96db7b5491d584a" }, { "id": "sdjwt-vc-presentation/aere-pid.status-list-other-uri", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xf772ddd4a3807705110eb41028ecd3ab28626de41199710b085215afbbbddd44" + "sha256": "0x915554c96d7b797648806384e5fe750d667ec925ef5950f395575cf549523301" }, { "id": "sdjwt-vc-presentation/aere-pid.status-list-expired", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x75081bea94d1a739b44ae005d6cc81c46e2a960dd3c4c4ebefd2f8d22419c55b" + "sha256": "0x3f44acb3f3efb77e8054c6147cb8e67d694192468990edf99f7faa61c7308bd5" }, { "id": "sdjwt-vc-presentation/aere-pid.status-list-stranger", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xa079f4cd56609a180571173a91b1e9fc47b84b94916d68a060a26bcbc80e4537" + "sha256": "0xe7fe2f98ef00ea3dc6086d238cf52825e9f650da7cccf2dcff468c024c66414b" }, { "id": "sdjwt-vc-presentation/aere-pid.status-list-typ", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x2ad1585933c5901cec2bdae407374f104e95a279e9ab6a66feb339cba355d2b4" + "sha256": "0xacd562584c9d09256a19c48c9467e3c1b4a3645f8b4a7c50846be8334e3ba2f6" }, { "id": "sdjwt-vc-presentation/aere-pid.status-list-no-iat", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x72a4a8c5085e0c222722413d1e42e71d07a89df6c8fa68067d49fcbcc81a2bd5" + "sha256": "0xc731a9fd905295629cee49e25441b2dec03a385fc4c2be28555267df82ec48f4" }, { "id": "sdjwt-vc-presentation/aere-pid.status-index", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x22fde994e6e073f8e28f7dffdad024917abce8ae3522de2fe5b7c5e7bc40abf2" + "sha256": "0x1fcd735451afd9dc6df4b3d3f6e1639c6faa13048e874dfc46d592345dfb1984" }, { "id": "sdjwt-vc-presentation/aere-pid.status-unjudged", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x3ad94f9eda82f09dab7505fabba4ed0809428c5d8ba86e0fc2f73402cac076a9" + "sha256": "0xd0f8332c496b0d9c36a71a3084d5eb95123803bd18e4525d115a96925b250d51" }, { "id": "sdjwt-vc-presentation/aere-pid.vct", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x114f09bf63c5cbe717905385662ab75a3b9fa61e07e779b1168192389d558884" + "sha256": "0xb7bff5186c86b17a56b2fe271a65e5a8fa2926d2196b238d4e5ef0401f8648e9" }, { "id": "sdjwt-vc-presentation/aere-pid.disclosed-protected", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xc8a6be1abaa3dae8f31cf26dad961c812502b2d376988f58ef35cd089892caad" + "sha256": "0xbb404bdeff4c014566f8b685a7737dee0fd72ae0993eff915eab7be376a55cd9" }, { "id": "sdjwt-vc-presentation/aere-pid.form-idx", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x83f5bc27803169753d4ac30ef44d82f66c73ed6e6842650168696cb6397e8459" + "sha256": "0xe3ee88251f3f7d98b789265b717f2d83e9925598d78fd08d2e4e9c97b462da43" }, { "id": "sdjwt-vc-presentation/aere-pid.form-status", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x44c5dddb20d400c6091e9deec56b9ebd69a69c6e7f325907e21720603111fddc" + "sha256": "0xc6f2e11798e8feb64549f364fe109de2432e9c523406e5752a256ba7654eabb3" }, { "id": "sdjwt-vc-presentation/aere-pid.form-integrity", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xe6a38bfdfb1ebbd0fea2e1d60ffd6832f3370f927a2bfb3fad03cdd590823f31" + "sha256": "0x018e3f4dddcfe4ba1019751a6c4be4578752656a1102be894cc3ea6881ea1a7d" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc.valid", "artifact": "sdjwt-vc-presentation", "verdict": "VALID", "requires": [], - "sha256": "0x1404a0597db6f869adaf5ff8abedf8670cb63ba48819083908d3572679e72411" + "sha256": "0x1d2d6e32f2641ca6aff06a401e896aee538bbec7dae3e3cb95216758074d30bb" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc-es256.valid", "artifact": "sdjwt-vc-presentation", "verdict": "VALID", "requires": [], - "sha256": "0xf7b34bf9da366783955d233ad50a17ff7582a04a99532b1cb16d489912dcd372" + "sha256": "0xe7f83fb6c2c8b4bc449d575c14c7670e41a4f7327d01ac378218edcb4a49d9b0" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc-ml-dsa-65.valid", "artifact": "sdjwt-vc-presentation", "verdict": "VALID", "requires": [], - "sha256": "0xdea179ad9cd7ca65e786922c680828a38f0e694ae6c0f263d4fde195982c528f" + "sha256": "0x3e8d5cb4c97a4ee31324cb954e3f1d17429029deeed12698c25015dedd23e2f9" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc-8-bits.valid", "artifact": "sdjwt-vc-presentation", "verdict": "VALID", "requires": [], - "sha256": "0xb8197a79e6faddf9b35fd95754d17dc8aa2d8174fe8d8a3be3239c767bd395ce" + "sha256": "0x01de3dc59b46ea0d28472ef3686782da6f54e5a4264e3159abee983f330ebf34" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc-status-waived.partial", "artifact": "sdjwt-vc-presentation", "verdict": "PARTIAL", "requires": [], - "sha256": "0xa7311135ea7ca068e7bc3f5e95c361f9462bec2b3abf52c30df8475cf9441a43" + "sha256": "0x7564a116e6b9ee22d998f34e682499526bf89a528b3fb44aabe1ff9cdd3221ad" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc.status-revoked", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0xeb4ac475c2cd1debe08d4dfc29359e6e9f30419b7cd3c4a01e1a227a14996ef8" + "sha256": "0x7bc0cd827b1568420751b5770cee4754cba50202f63d777b6234822dec4d067f" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc.status-suspended", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x1aa605fd51ecb74c7bf6508f10921112938d672483a942398bc2f3f15a49d423" + "sha256": "0xd8f48086eead0fe0625f5925fed796a116917cc5b75dccbfb3a619fa5d1de09d" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc.status-list-other-uri", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x4020a11a308284d3570b98e72c5476b28ccd24bde24afeb00a2366966404154f" + "sha256": "0x3da1f851d3ed5cdf4f38f924fc0c73461daabbe451d2a9a00c5f203538101d9e" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc.status-list-expired", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x25bbd41833ee8d02cb7d046f1c502a16f826316d8ca246271c37e310635a6b0d" + "sha256": "0x6c5cb2872427359e010e2e34c9a785b82d0fdbe87eeb449ebcadfeb56bac95f8" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc.vct-mismatch", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x6d245525ebed4580c4d4044712df02acf084f8c39671516b5328eb73904159ee" + "sha256": "0x956901d80bd4469059ce91439821342b2722de43623778d266aa91b5891c7289" }, { "id": "sdjwt-vc-presentation/sd-jwt-vc.status-unjudged", "artifact": "sdjwt-vc-presentation", "verdict": "INVALID", "requires": [], - "sha256": "0x10fed0db565c3f655e2ba85c08651d31fa42b0f7628b30e3295039aa8f9ac6b1" + "sha256": "0xf85981bdc6739c5c56bc67af87e8d95338041e56159aea4ee54d18d2109574c6" }, { "id": "travel-rule-binding/beneficiary.valid", diff --git a/conformance/vectori/sdjwt-presentation/aere-hybrid-keys-classical.hybrid-half.json b/conformance/vectori/sdjwt-presentation/aere-hybrid-keys-classical.hybrid-half.json new file mode 100644 index 0000000..935a0f1 --- /dev/null +++ b/conformance/vectori/sdjwt-presentation/aere-hybrid-keys-classical.hybrid-half.json @@ -0,0 +1,66 @@ +{ + "v": 1, + "kind": "aere-conformance-vector", + "id": "sdjwt-presentation/aere-hybrid-keys-classical.hybrid-half", + "artifact": "sdjwt-presentation", + "title": "the same, the verifier no longer asking for the classical half", + "source": "derived from sdjwt-presentation/aere-hybrid-keys-classical.valid", + "requires": [], + "exercises": [ + "sdjwt.alg", + "sdjwt.claim-conflict", + "sdjwt.crit", + "sdjwt.digest-twice", + "sdjwt.disclosure-form", + "sdjwt.disclosure-name", + "sdjwt.disclosure-twice", + "sdjwt.form", + "sdjwt.hybrid-half", + "sdjwt.issuer", + "sdjwt.kb-audience", + "sdjwt.kb-claims", + "sdjwt.kb-cnf", + "sdjwt.kb-context", + "sdjwt.kb-freshness", + "sdjwt.kb-nonce", + "sdjwt.kb-required", + "sdjwt.kb-sd-hash", + "sdjwt.key", + "sdjwt.sd-alg", + "sdjwt.signature", + "sdjwt.time", + "sdjwt.typ", + "sdjwt.unreferenced" + ], + "base": "sdjwt-presentation/aere-hybrid-keys-classical.valid", + "mutation": { + "op": "remove", + "path": [ + "classicalHalf" + ] + }, + "input": { + "sdJwt": "eyJhbGciOiJFZERTQSIsInR5cCI6ImFlcmUrc2Qtand0In0.eyJuYXRpb25hbGl0aWVzIjpbeyIuLi4iOiJvR1ZLcV9iYUlSbkdjX0x6S2c3Rjd4R0M2OTdOLXJSWjdNbGJMdk9jMzlvIn0seyIuLi4iOiJYU1BvSVhtdkRMMHY4a3BTZ1NGZ3ZuTWZtcy0zTllLdmtYM0cxd2RCNmg4In1dLCJhZGRyZXNzIjp7ImxvY2FsaXR5IjoiQ2x1aiIsImNvdW50cnkiOiJSTyJ9LCJfc2QiOlsiMGl3bk5fZjRkSGhfcUI0SlhyX201OGdoWFNYRmk3LTk0SDR4QnpOdXBMSSIsIjM1QnNjekpxVEdFNEdWY0ZZa0tEMEY3R0wweUlUdlp5MVpSU0x2Y2tpdTAiLCIzWm5SQk9fMGFZbUZCQUl6Tl9QZmdNYVlacUVYdWNibkg1Y2E5RFNNTFdBIiwiYVptbDlYNUU5cDhxaVJvcExVeGtTeUpsXzgtemEzQVRnRWFEanhBQ2o0OCIsImpLSlJ6WS00NmR5cWFHMnliQkk4OUxuT0ZuQ0xRSl9Ddk5rWnlnOFVxQzAiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiaWF0IjoxNzkyMzAyNTUyLCJleHAiOjE4MjM4Mzg1NTIsImNuZiI6eyJqd2siOnsiY3J2IjoiRWQyNTUxOSIsIngiOiJwZGd3WVJzOEtTdl9ESUJvaEFhaWx4MEoxS01xVGkyd2cwVGY3TmJaOFY0Iiwia3R5IjoiT0tQIn19fQ.h75vV6jAkI1n0st_TUrGseDJKEk0RScfX5neSm_CSN2GoVjL2Ml2oKmB0szWDCjjlAcq1Tn1B39KNwEE1dWzBw~WyJSYTAyRkdCZDJmMW5HRlJPUFNUdURnIiwiYWdlX292ZXJfMTgiLHRydWVd~WyJZUVdma1BBSW1UTUpTU09vNmQ1WVJBIiwiUk8iXQ~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE3OTIzMDI1NTIsImF1ZCI6Imh0dHBzOi8vdmVyaWZpZXIuZXhhbXBsZSIsIm5vbmNlIjoibi04NzlmYzQxN2UzMzFiYzgwIiwic2RfaGFzaCI6IlBWY1dzVDZ1SVFhUHBRUUM1QUQyUzQ3Q3Z5QllpRTBxQ1RVZUtpd0ZfRlUifQ.i8LKIVBV7X67veBsk-vyyCHouKBFEXP5uLl927e3m3-_JE1hOHuyv0KX2gioAO81kI1FY1TV6xHOiQVF-UyBBg", + "issuerKey": { + "alg": "ed25519+ml-dsa-65", + "ed25519": "MCowBQYDK2VwAyEAhca9rLOG6cMHfk2weCWFz7wily7iDR/Qvhhs6tTQGjA=", + "mldsa65": "MIIHsjALBglghkgBZQMEAxIDggehAOk5+trXXL8kl3d/8xVbMH33e7FIvN4r0xAKE+iOsC2LUz7ZPSz7CDh9kh+6cekIKGQNLBuedGbAAygXRx2nG4X1kiuUUkQREX72SIkYUYvag2s3Qk8eLd2MbH5IrkWNyiJRGOSs6tBzJBsnBnOY9tiJYwNsXOwopFLjvf29SzjX0gAep4COZiLNpnx4TcSBGhIxuywWbGeAG+XOkL/UgjUN8SbRobhtXgifgmKddAxlaYXpmSyyQE/jpC+3yFprzIbyDGSsRzDAlOrxRYt7VyHzFTOqGHMNqCZlnE9B6tFxT/Fnd6gyG5JSdHohYYyhC2VMxAoGAhspeRPtYHeD5YTX/aa189p6XBsKF79R8MH9fLloMwY3JkrA8BZeJ8hdHko3Z/HU7DoGA95JMn7s1KxegZ9HHOBRy5vT4wk8od12jHHHiVIy5ohBTkQAd6oT1WPKRdOQF+Wf+SGkyQXwpLhfBkZhlkIgfp/inyep8arKvqqAnC98H2fkzfsSqh4p3bdhvtxkcQy/9P9mPDlcIS/zKbr95Audq8QmZmV5iVOlN7Qsp58WJ7MU1/qaqAG36bjT9giuz+CPOZYzhkt4bABzb6u28nK9hDUK/wyK57DtKfPwL12aOD8QZf6PkK07QLG+ojWzxn1DwSskUwQ5ffW+HooiyvrYoDJwA+aBanRWFuZKWAth4aIaBsq8kIH/26nJmYAcBkVIcNqjsEwKsW8JfJgD+xxsV4r+S92RH7kX/kv695+F1p9xNAFdiJK7XisWPrTfjdA478UG3cyRfZHyexjCA6yyiVbRR6XDibNK2Tdy1t0Qv2ibf9ytXY14snCkIX1OWKzwr6a0GHDEWxCl0UHFFhtdDbrYySQvgmyUzWL0FQKpZy18tyWQzrohGhi17D/tgzky9dFGIePdD1Ew8DQ0ShXXDe5F/M1Onc0D88D680AiZWrpfe+RbdkgIHp2M+Gzhx1cdahXAOvB5UwzdXkwjo1IK6H2S8qfpgk2SiijKYW/3vrtC8ieaEkRs/VdmpYYwd23AxP6vX10+2owV+QXHWuseO9uyaTeUiDWry4+0sNffiEwtue2Fe7l8bc4NRDt/9IHF7AyHckQirHz4wg1qSS+RnNvkyVyu/bEzn4EEnlKfE1trv6n8hWxhUg0vk09uNSIeKhhhfywBd2FRrd13zlY2vNW33iXVZkWPTbF7lIt7VfIFaXgLeg8UZyVS0Bt97hvdnyBGcrsGk5OBYgsvlTm2qbudukalIjfzauiKKb7tYJv67NiG4R1r3dxQJj1DtfB9N3a2DlUcq36ZZj76LWdwMjtvuPVkwKmVi72wsNmnu7jt8iaSF/7h8jgXFRvUGTav5GeLlcY+yV6yO36K3prrgU6X0rHw+9kHlFB8x5M3pDirh53CAPXv+p7HP8c5dciYp7yraT21HQKu8goyYySunHSl0z/sRH0eVJV+fr6v9uRvoC/SA6Xp+phtSQW8UPAZNbhD1nLpW2EiG7LbzX6Ps236+SRENpa167yxG8WxArvFzq6G0vxMzm9hY4A1tGRINy5xKXeXRlrxyIU/zARWtkflco5yH1u5gZf5YHT+E/LsNv8j9/2z2nZB2UbM6IenNuvlWU/rQjN3+EXC+ccrvNt8+MDDnzuNzlNDPQmeNq0/6+lHUAzNB9Tcw3tsCr5dsWAAcq8a8JNr09j2vcEYR4X6u5p4cLZIYP8N9fTsq1/+fQqvgVaCZTDodzlgyCNalCnJ2TgyAdy8SE/XPdJcvWgPRR2y3VMrXi/o9q/oPJQRbxDYZwCFbSM94T8Y+XwCM8l4ubTKWS4ITu2DNtQIvXvQH+AU6SJypKPhGCzwGbeSCoS20qrwIu0LfwCBz3xEkpVioq2/9Y/zBr7yzvZpe6pdqw0hzx9mvZl7xEP4roD7quo8Ktl7iGiPgL6lN2rTPQgIKb2A8Zp/azLh89QHd6PlJmboAseNU5YiGZQ0JPUBKWz2MfuhYlx9BsUvKTOO4Jfi5kKQ4Ujs5lLMWTdTAcrax/5+BC6Dadv1b2N6Las/0Hoq7oIQNscbd4GBKXqyHAv2nRNxrolEdspjglpAt6MGaYvZ3onZ1uscWDKA20al/uLMtR3jBs5IMcR+H27a/ExRFazDaKt6+JrYA8OKGhpNia2DMCjRga/zvwmc4jECh0r1A6R9wWyhzdmYIsmOO0Y8gxoxtK30c2NUW3BQ45aucum0qENXQuGuwMeKqm9dWIw55RCwbld9wxnFK25RMa80oeU5kvSWKfQ3nrUAPJYDMqo8SkKcNlSslY6EdOP32H8bU4yHH0nHMwchv3czgaQrACyZmx8K7Qpe9Jx8oC5rwOf9B5e3gfZG690ck99vQP1/x5MPmkjyVLUtpvuaLZtzyviv6V6BpLxsGKKCB0M6YOymczvl6gozBAnWnIup2qyuOuMKkMRJOGiTFAyH37Dk/a8QFmupZe0FN9/7vi3vC8diL3zYKYUx/6DlWScTB9mpvf9L/hj00qVebCQu7MFEjb9qy16N0ofpz+S41L3I2AsqTEtIPpRdxO6CwMHqobXmiPrCmmrxys4AUg6X8BoGQtJK39SdgUS291C9cF4EAm7LQWw" + }, + "audience": "https://verifier.example", + "nonce": "n-879fc417e331bc80", + "now": "2026-10-18T05:50:12.000Z", + "expectedIssuer": "https://issuer.example" + }, + "expected": { + "verdict": "INVALID", + "failed": [ + "sdjwt.hybrid-half" + ], + "levels": { + "integrity": "NOT_MEASURED", + "authenticity": "FAILED", + "anchoring": "NOT_MEASURED", + "finality": "NOT_MEASURED" + } + } +} diff --git a/conformance/vectori/sdjwt-presentation/aere-hybrid-keys-classical.valid.json b/conformance/vectori/sdjwt-presentation/aere-hybrid-keys-classical.valid.json new file mode 100644 index 0000000..5a00547 --- /dev/null +++ b/conformance/vectori/sdjwt-presentation/aere-hybrid-keys-classical.valid.json @@ -0,0 +1,60 @@ +{ + "v": 1, + "kind": "aere-conformance-vector", + "id": "sdjwt-presentation/aere-hybrid-keys-classical.valid", + "artifact": "sdjwt-presentation", + "title": "the credential signed with the Ed25519 half alone, against the AERE hybrid keys, by a verifier that asks for the classical half (classicalHalf): VALID, and the verdict says it is not post-quantum", + "source": "producers (sdjwt.mjs: issueSdJwt, presentSdJwt), three hybrid key pairs made and discarded at generation", + "requires": [], + "exercises": [ + "sdjwt.alg", + "sdjwt.claim-conflict", + "sdjwt.crit", + "sdjwt.digest-twice", + "sdjwt.disclosure-form", + "sdjwt.disclosure-name", + "sdjwt.disclosure-twice", + "sdjwt.form", + "sdjwt.hybrid-half", + "sdjwt.issuer", + "sdjwt.kb-audience", + "sdjwt.kb-claims", + "sdjwt.kb-cnf", + "sdjwt.kb-context", + "sdjwt.kb-freshness", + "sdjwt.kb-nonce", + "sdjwt.kb-required", + "sdjwt.kb-sd-hash", + "sdjwt.key", + "sdjwt.sd-alg", + "sdjwt.signature", + "sdjwt.time", + "sdjwt.typ", + "sdjwt.unreferenced" + ], + "base": null, + "mutation": null, + "input": { + "sdJwt": "eyJhbGciOiJFZERTQSIsInR5cCI6ImFlcmUrc2Qtand0In0.eyJuYXRpb25hbGl0aWVzIjpbeyIuLi4iOiJvR1ZLcV9iYUlSbkdjX0x6S2c3Rjd4R0M2OTdOLXJSWjdNbGJMdk9jMzlvIn0seyIuLi4iOiJYU1BvSVhtdkRMMHY4a3BTZ1NGZ3ZuTWZtcy0zTllLdmtYM0cxd2RCNmg4In1dLCJhZGRyZXNzIjp7ImxvY2FsaXR5IjoiQ2x1aiIsImNvdW50cnkiOiJSTyJ9LCJfc2QiOlsiMGl3bk5fZjRkSGhfcUI0SlhyX201OGdoWFNYRmk3LTk0SDR4QnpOdXBMSSIsIjM1QnNjekpxVEdFNEdWY0ZZa0tEMEY3R0wweUlUdlp5MVpSU0x2Y2tpdTAiLCIzWm5SQk9fMGFZbUZCQUl6Tl9QZmdNYVlacUVYdWNibkg1Y2E5RFNNTFdBIiwiYVptbDlYNUU5cDhxaVJvcExVeGtTeUpsXzgtemEzQVRnRWFEanhBQ2o0OCIsImpLSlJ6WS00NmR5cWFHMnliQkk4OUxuT0ZuQ0xRSl9Ddk5rWnlnOFVxQzAiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiaWF0IjoxNzkyMzAyNTUyLCJleHAiOjE4MjM4Mzg1NTIsImNuZiI6eyJqd2siOnsiY3J2IjoiRWQyNTUxOSIsIngiOiJwZGd3WVJzOEtTdl9ESUJvaEFhaWx4MEoxS01xVGkyd2cwVGY3TmJaOFY0Iiwia3R5IjoiT0tQIn19fQ.h75vV6jAkI1n0st_TUrGseDJKEk0RScfX5neSm_CSN2GoVjL2Ml2oKmB0szWDCjjlAcq1Tn1B39KNwEE1dWzBw~WyJSYTAyRkdCZDJmMW5HRlJPUFNUdURnIiwiYWdlX292ZXJfMTgiLHRydWVd~WyJZUVdma1BBSW1UTUpTU09vNmQ1WVJBIiwiUk8iXQ~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE3OTIzMDI1NTIsImF1ZCI6Imh0dHBzOi8vdmVyaWZpZXIuZXhhbXBsZSIsIm5vbmNlIjoibi04NzlmYzQxN2UzMzFiYzgwIiwic2RfaGFzaCI6IlBWY1dzVDZ1SVFhUHBRUUM1QUQyUzQ3Q3Z5QllpRTBxQ1RVZUtpd0ZfRlUifQ.i8LKIVBV7X67veBsk-vyyCHouKBFEXP5uLl927e3m3-_JE1hOHuyv0KX2gioAO81kI1FY1TV6xHOiQVF-UyBBg", + "issuerKey": { + "alg": "ed25519+ml-dsa-65", + "ed25519": "MCowBQYDK2VwAyEAhca9rLOG6cMHfk2weCWFz7wily7iDR/Qvhhs6tTQGjA=", + "mldsa65": "MIIHsjALBglghkgBZQMEAxIDggehAOk5+trXXL8kl3d/8xVbMH33e7FIvN4r0xAKE+iOsC2LUz7ZPSz7CDh9kh+6cekIKGQNLBuedGbAAygXRx2nG4X1kiuUUkQREX72SIkYUYvag2s3Qk8eLd2MbH5IrkWNyiJRGOSs6tBzJBsnBnOY9tiJYwNsXOwopFLjvf29SzjX0gAep4COZiLNpnx4TcSBGhIxuywWbGeAG+XOkL/UgjUN8SbRobhtXgifgmKddAxlaYXpmSyyQE/jpC+3yFprzIbyDGSsRzDAlOrxRYt7VyHzFTOqGHMNqCZlnE9B6tFxT/Fnd6gyG5JSdHohYYyhC2VMxAoGAhspeRPtYHeD5YTX/aa189p6XBsKF79R8MH9fLloMwY3JkrA8BZeJ8hdHko3Z/HU7DoGA95JMn7s1KxegZ9HHOBRy5vT4wk8od12jHHHiVIy5ohBTkQAd6oT1WPKRdOQF+Wf+SGkyQXwpLhfBkZhlkIgfp/inyep8arKvqqAnC98H2fkzfsSqh4p3bdhvtxkcQy/9P9mPDlcIS/zKbr95Audq8QmZmV5iVOlN7Qsp58WJ7MU1/qaqAG36bjT9giuz+CPOZYzhkt4bABzb6u28nK9hDUK/wyK57DtKfPwL12aOD8QZf6PkK07QLG+ojWzxn1DwSskUwQ5ffW+HooiyvrYoDJwA+aBanRWFuZKWAth4aIaBsq8kIH/26nJmYAcBkVIcNqjsEwKsW8JfJgD+xxsV4r+S92RH7kX/kv695+F1p9xNAFdiJK7XisWPrTfjdA478UG3cyRfZHyexjCA6yyiVbRR6XDibNK2Tdy1t0Qv2ibf9ytXY14snCkIX1OWKzwr6a0GHDEWxCl0UHFFhtdDbrYySQvgmyUzWL0FQKpZy18tyWQzrohGhi17D/tgzky9dFGIePdD1Ew8DQ0ShXXDe5F/M1Onc0D88D680AiZWrpfe+RbdkgIHp2M+Gzhx1cdahXAOvB5UwzdXkwjo1IK6H2S8qfpgk2SiijKYW/3vrtC8ieaEkRs/VdmpYYwd23AxP6vX10+2owV+QXHWuseO9uyaTeUiDWry4+0sNffiEwtue2Fe7l8bc4NRDt/9IHF7AyHckQirHz4wg1qSS+RnNvkyVyu/bEzn4EEnlKfE1trv6n8hWxhUg0vk09uNSIeKhhhfywBd2FRrd13zlY2vNW33iXVZkWPTbF7lIt7VfIFaXgLeg8UZyVS0Bt97hvdnyBGcrsGk5OBYgsvlTm2qbudukalIjfzauiKKb7tYJv67NiG4R1r3dxQJj1DtfB9N3a2DlUcq36ZZj76LWdwMjtvuPVkwKmVi72wsNmnu7jt8iaSF/7h8jgXFRvUGTav5GeLlcY+yV6yO36K3prrgU6X0rHw+9kHlFB8x5M3pDirh53CAPXv+p7HP8c5dciYp7yraT21HQKu8goyYySunHSl0z/sRH0eVJV+fr6v9uRvoC/SA6Xp+phtSQW8UPAZNbhD1nLpW2EiG7LbzX6Ps236+SRENpa167yxG8WxArvFzq6G0vxMzm9hY4A1tGRINy5xKXeXRlrxyIU/zARWtkflco5yH1u5gZf5YHT+E/LsNv8j9/2z2nZB2UbM6IenNuvlWU/rQjN3+EXC+ccrvNt8+MDDnzuNzlNDPQmeNq0/6+lHUAzNB9Tcw3tsCr5dsWAAcq8a8JNr09j2vcEYR4X6u5p4cLZIYP8N9fTsq1/+fQqvgVaCZTDodzlgyCNalCnJ2TgyAdy8SE/XPdJcvWgPRR2y3VMrXi/o9q/oPJQRbxDYZwCFbSM94T8Y+XwCM8l4ubTKWS4ITu2DNtQIvXvQH+AU6SJypKPhGCzwGbeSCoS20qrwIu0LfwCBz3xEkpVioq2/9Y/zBr7yzvZpe6pdqw0hzx9mvZl7xEP4roD7quo8Ktl7iGiPgL6lN2rTPQgIKb2A8Zp/azLh89QHd6PlJmboAseNU5YiGZQ0JPUBKWz2MfuhYlx9BsUvKTOO4Jfi5kKQ4Ujs5lLMWTdTAcrax/5+BC6Dadv1b2N6Las/0Hoq7oIQNscbd4GBKXqyHAv2nRNxrolEdspjglpAt6MGaYvZ3onZ1uscWDKA20al/uLMtR3jBs5IMcR+H27a/ExRFazDaKt6+JrYA8OKGhpNia2DMCjRga/zvwmc4jECh0r1A6R9wWyhzdmYIsmOO0Y8gxoxtK30c2NUW3BQ45aucum0qENXQuGuwMeKqm9dWIw55RCwbld9wxnFK25RMa80oeU5kvSWKfQ3nrUAPJYDMqo8SkKcNlSslY6EdOP32H8bU4yHH0nHMwchv3czgaQrACyZmx8K7Qpe9Jx8oC5rwOf9B5e3gfZG690ck99vQP1/x5MPmkjyVLUtpvuaLZtzyviv6V6BpLxsGKKCB0M6YOymczvl6gozBAnWnIup2qyuOuMKkMRJOGiTFAyH37Dk/a8QFmupZe0FN9/7vi3vC8diL3zYKYUx/6DlWScTB9mpvf9L/hj00qVebCQu7MFEjb9qy16N0ofpz+S41L3I2AsqTEtIPpRdxO6CwMHqobXmiPrCmmrxys4AUg6X8BoGQtJK39SdgUS291C9cF4EAm7LQWw" + }, + "audience": "https://verifier.example", + "nonce": "n-879fc417e331bc80", + "now": "2026-10-18T05:50:12.000Z", + "expectedIssuer": "https://issuer.example", + "classicalHalf": true + }, + "expected": { + "verdict": "VALID", + "failed": [], + "levels": { + "integrity": "PASSED", + "authenticity": "PASSED", + "anchoring": "NOT_MEASURED", + "finality": "NOT_MEASURED" + } + } +} diff --git a/conformance/vectori/sdjwt-presentation/aere-hybrid-keys.hybrid-half-named.json b/conformance/vectori/sdjwt-presentation/aere-hybrid-keys.hybrid-half-named.json new file mode 100644 index 0000000..28a2922 --- /dev/null +++ b/conformance/vectori/sdjwt-presentation/aere-hybrid-keys.hybrid-half-named.json @@ -0,0 +1,68 @@ +{ + "v": 1, + "kind": "aere-conformance-vector", + "id": "sdjwt-presentation/aere-hybrid-keys.hybrid-half-named", + "artifact": "sdjwt-presentation", + "title": "the verifier names EdDSA for the AERE hybrid keys without asking for the classical half (classicalHalf)", + "source": "derived from sdjwt-presentation/aere-hybrid-keys.valid", + "requires": [], + "exercises": [ + "sdjwt.alg", + "sdjwt.claim-conflict", + "sdjwt.crit", + "sdjwt.digest-twice", + "sdjwt.disclosure-form", + "sdjwt.disclosure-name", + "sdjwt.disclosure-twice", + "sdjwt.form", + "sdjwt.hybrid-half", + "sdjwt.issuer", + "sdjwt.kb-audience", + "sdjwt.kb-claims", + "sdjwt.kb-cnf", + "sdjwt.kb-context", + "sdjwt.kb-freshness", + "sdjwt.kb-nonce", + "sdjwt.kb-required", + "sdjwt.kb-sd-hash", + "sdjwt.key", + "sdjwt.sd-alg", + "sdjwt.signature", + "sdjwt.time", + "sdjwt.typ", + "sdjwt.unreferenced" + ], + "base": "sdjwt-presentation/aere-hybrid-keys.valid", + "mutation": { + "op": "add", + "path": [ + "issuerAlg" + ], + "value": "EdDSA" + }, + "input": { + "sdJwt": "eyJhbGciOiJNTC1EU0EtNjUiLCJ0eXAiOiJhZXJlK3NkLWp3dCJ9.eyJuYXRpb25hbGl0aWVzIjpbeyIuLi4iOiIxRlgwR3hrbVlvT2xBSXpiYXhaYUhwQnhLSlI3RmtFWTVKNHBqc2hrMG1zIn0seyIuLi4iOiJSSXVOb2ZvT21scjlsbnFsWmJneGNIdmxKZUp4S0JwaHRZbXRwSWp6NENRIn1dLCJhZGRyZXNzIjp7ImxvY2FsaXR5IjoiQ2x1aiIsImNvdW50cnkiOiJSTyJ9LCJfc2QiOlsiNTJ3RnVzclB1V0lfLXRfUUdfWmdxTDhWUkg0Njk2MUw0OWtpMGMwRDVPOCIsIk4xNlJZd2lnanc4dW0tMWxza1NfTloxVlpUWFNYaWRaUy1oQ2pYZ1lvVUUiLCJRUGluaVdOOEFHT0QwNDltOHU2TzI5bmtiVWg3YkJhakNkckxzQTFTSUdRIiwiYVZjSTRJdUZweEgwOGNPdFlqQTRYZldJSlJsTGFvMmhsU3h2U2tVbG1YOCIsImJic3FsZWRTUnQ3ZTFPeXpoVmZLdXF3SDZhZ2kybTN1b2h0VG1zMGV4ZGsiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiaWF0IjoxNzkyMzAyNTUyLCJleHAiOjE4MjM4Mzg1NTIsImNuZiI6eyJqd2siOnsia3R5IjoiQUtQIiwiYWxnIjoiTUwtRFNBLTY1IiwicHViIjoiekxNT3Y5eEJrRzE5ZTVacnNqLVhVbXJhVk9wNkMyVS1WYi0zQURQWU05NUduRXBLdVVnLXBNN2tOM0tiWi1ZLUlJQ0pZa05IbFA2ZThRUGtpNzJkaGtGUTdUbnBXdnJoQXJ2d2dkZ1JibWtxSFNDcEo3OTc3ZU1kQjNfMmhYcmhLN0J3a0g0elZGdXk5eFJDN0RyZUlHeGNIZHdEaDFfYTFQRWJXVUlWcklhNXZvZURRSVpSMF9HQWlkRlEwN042Y1lQZ1BzallJVERpVHF3N0x1Tnk2MVlObFF6R2F5OFlYRV9xU2ZYdVhpWkxFU29KNS1ZaG5BWjRqMHRlQ1V6ZkJmcmE3ZnZ5VEhNMGFMN29Bd1M5RmIwcVExbVlLMV82NGJlQjhLTEJfWGFoUV9ZbkpNUXg2ZVZQWGJURVljSTNNNHVqRHIyVXV2dzRTLXptdEZ3N0dRLTZGdzl6Vk80YWJzRTVvb2laT3FvbURoSjFfdldSU1BUX096bDlfMVRhaVIwUzBUQ09ZcVg3YU80bi1sVXFpcko2ZzFTMFJYRUlUTkE2SWJXSEpGbjVueTJUZ09LWVE5WE90ZGFSekVSSi03cHZacS1uLVhHNWE4RlhrNklZWnY2cWduaEw2M3hkcksxUmRFT2d5cXJMcEdvNDdXYlhGb2pFaDQza25jUHEwbVM0bC1YMmdienZFYmhpMzhRRVk3VzRaZ2hueWJBT0QxNVFfeFJSa1IxajVJMVlrNXRGb2gxZ0tyZ1pTckdWRWJnZG52TWQ5dFUtZmZuNVhRZkdja0VqeUJrXzlJVS1wRzRON1QzNmozTUw4SmFEaVRYMHpjRVlCRGxuWEV6UlUwZ3BDdnp3UG5kQjFCcGdGNl9zRENaUDFOQVQ0MGdmZEFKc3VOaXRSb2l2T2tCbEhpYmVDbjY1MEI0WEZSYmdmaFJpTVRzVzF2WnM4OE5yLWh0dHc1SnJwMGdyQXo2T2owczFwa1BPUG5ZNGM3X3VXQ3dVaGFDVTF5b0lEYXQ2X2JRNUk4ZmJjOXFBQjUtYU03S3ktWlU5bkoyU09XS3ZvbHhiV2dwU2NpSFRUcjlCWFpROVlrQktGSGVvRGV4RDF6US1NQURtd2JnSC1lWmhTcXBXYVBNaXNBdUZ6VGU2bDNNdDduTklVSTVuUVJKSnkwTlM1WlBsWUxrX3hMdGZaNEdDWHJhZnRhaFhyRFpjNWFodDdKNURwQlV0VzBBaHl4TExvajdiU1RMNmhvelc0NnVRZEIyYkw0V2VsTjNBX0F2LXBtNmlJRVBndGhxbXo5cUlXdnBXTHNneEZ5SlZYV0Z3R1JCVUstaDM0Zmo1aHR0dW1FcUZZelNfNzBVeTljdWwxSy1qZkNXb1VTSDhFQl9nNXJMVk00Rks0QVpWNDRkN3FzSnk0VmZwUWk4WTJQaUJJVHVTc09UcUhRQkw3ampwVTNWak9NSVNRUi1XRFlzQWpORzJnYVpfVWtNVk9zaDhUQ1RHa2FYNXUyX1hXTzJLSDF4eXhESFJ2VWtsUy1sRWhOVHg1RzV5T3BUaVJnaWg2ajZ0dlVkbEdEM2VOYXBDLXpmUEVuRGhUdUplWFBqUE5PV1RZWXFEZkxtUXB1TEZfLXhXekZIMXQzamRHSFVpT2NocE5ab2VfaDZSRVFoTWx1dUx4cGJkLXhmRi1GTnQ4Z3FUeWwzMDhWaUY4aWR2SzlUWjR1ZHVOenQ3WTdtMTN5cklxRDRFVlV4bUlobFZCQzNCYWZqOW1GZ0NBckFSOU1oOG5GdTBFUGtUS1RoQ2pQbmF2Q0hJeWNJZ1JZaXhzSUdMcHRRZjEtM0w2cHpTenRQTU9sb2dYTmRKeDB4WHZuclVnRldDNUZIS3V1MFI5MlFtQ3FvTXA0bGJ0ZU50TURKbmQ3cDdMS1ZZUlNkODdDRUZBQmJIZXltamxFS1ZhdHBiMXQ1dWFIU3BxcG5tQlJWWlpCUklMa0xnRXAwVExBOHhzcEhnMWxvNjJ6UmRWdmRDYUkyMlJLWVNYcGR3bHNBdS10Mm43b3VqdzRMWmFEN0wzT0tLY254VjEtNVpULWtlb1NjOFhVMEpUbmhIaVdHYmJLbjEzME5fVkZMYUlSalhyQ2lremVXRlI1V2JFVDQ2cVZpcVI5b3FBb3lpekxXbXFoaGxORVFTZXNlcXdLUXA1NExjNTVRWnM3c3djYmstLTJWTHZZOE5WVmxCaFFKS1lVUHJVeWI0dE0ydzdqa3R1WUhHMGFPN0VMSlRnMTZoU3Bua3hZS0g0cERsUW5vQ2phTXMyTXgwYjNoQnNSOGh2QXN0UXRsZEtma1NOVEw0RmVFODB4eEdtckJrS2s5OEY0SHRteHFQemJxbnBPRlp5cWo0Z3BhZkpHd1N0TVFpa1ZLb0RHR1NFc1M1QTBJZDdBdEdGZGdGWjh6OFYySTZxOFVtckN3SzF4Z1dXSkRkTlNpVFlwT2pHdURNRksxUUtMVU1PM3luWDZDSldBOEthaG9JZjdSNVNXQmxtOV9VSUNPeHhJOGlNTmtDMWdiTXpSbDdfWU9ZV0c2d2d5YlVyN3hoeE1NUF9Sd2l0NEZyd0hPXzlYM0NEUTZacUpTVmd3dFZmaHBpeWlZSlJiWFEtNE9DSGZNRmJQalpYNWhURUZoTmp4VTNWMEVCWG5IbmxfRFNuLUd6bXZOcHNTZFJvMHZUVGRPb1RyTUVHcE5jc0s4SGwxaVpuMlJVLUFCV3lBVG5FRXg0bC1pZEVtZV9rVDE0T29CTTV2eTh6Sm9IVHNHcmpHeFktWGtJa0hudEpIWFJGVklWMEJ4ekVaaHhDd3c5MmNMTDRicEJZM0JSejlaVi10NnZKcmhBVG1BQ3lYY28wQ3ktb1d5czFhTW9jS2I0MWJvLWJpSlRFanRRdFhYaW9jRDJuZjlzdzR3NDhGd2FQZVppNTYxQmtYdDB0MEZfbVdZaWx3YmUyRDRaS0RfaHNCeTBsVFUzaDREUUNtOVdxUDdEbkRyaHBKbm1OMTNWZEZWUVZMV1BoOV9aQTdiNEthZ1I0SzdVS3FHUFI0X2ZOUFE3NUo3QW9WZlZKY2RMZElIUU5ocEJJNG9Ba0pVU0RSdTZmUXVMbFJJaERFM2N6MVV5X0xYZlQzT3lUY2NkcFdheVVDdXNqeUZ0M3JkcGFrNlQwV2t4dFNNcHdMQ29GWGNfcnp4QlVYTDhiRDR2ZG9sTng4Um5YcWVIS2RvYTVPR3pYSDNGTWwtQy1EYk5ISTlUaUt5b21rVnUzQm9wbVVBenpzcFRwUzF4RmhwVjEwVmVwem9xZUpjUzhhTUI2LWNyY1lEQm5KMmxpSkxyY2hCT1hsZ3ZLalBKZGc4WjRpVjBjZDQ5ZDN4YTd2WGVIT1Via3AzS3lheWtWUUxhSDA5ZGxrOVZuMlFweFhiaGRJb1RQWlZiSnJCWW4ydnNLRXloU1o3VnJKYmhwZ2cifX19.rzRJtm9JwwdtV_pyYoLNMFh-LeT826WsdO_OSU3s5Hhqu9iZQ_q5g7d74vPNB62EtcAV1CrcpUYorEeJnYyatGz_rcqTHwybNysflBJCBlVK0xBhrdC8LhLwLhhFLrIDit1VP-kZbF2PQXi4pVwRQNRVdVDoSx2FDf6EVlKc6TzpyRJhkrXrxtThAmi4kv8_-58V5L2BFTqrwGzyaaZaXU_vWqHyQzM_1p0Nzw0KrKHIZQsvjQ_md-_TEgK6e0_cUg7h43AOygbek3GfMn9X3gzVA4WVuWa3BcUtlAGZreBDLCwqmaGwWNeiE--P2bHJN-L5Gwg5SFYpIw0VN0nEKXvGuQQH6NPZYQwcDLaclkJNw3C2BtS1FE3XJfrQFMg8Q_XDqfB7VvKib2AYK8_5Ss5Iz81WlpSwvRyVBdTe6F0zf1pJTfnmzGfKHACKyL4Y09t16EmBrek2Zbc6_y-EP4QD3CJV-5GtDpVn7o6t14YEffSZL-dQ3gixShl91wIf4WSnytCiuZm-dqQD2eVkOP613m18UKQ4a36euW_I3lPm_72y5LqUg5Zcx10DvnsFBcBIhuOGZOKUvfFkINwMUEfNQzYME_okAfBtB1NMy3u8z--H9-a36Kgq7zt0Cy4JD-S2E0YyNTMx_BgnsuyLbklKVzRMfXHhy68Y10gRHPAfUPkxIAdaIjF3olgQrQH16tgg-6morpe-g0iEtug4XXFZjwljIiT86UJiF2haJnA4ORb16VOzlOUXEV3g7ZXsNRFm61F0HDsn34Ii3n-guRJBEgprqp91R7XS8i_qOXfAJ6_0ycijSOEBl7WK8uLhfPQY698UtJhvpLtmtzP8oLxpR0tfeV0i7T-VQOA-d1g-De6XF0p53e2DHdEWDZvy6uqaARC52VR-uNsVljeVeuI6SX3LIUQ6U4wYUTy2pBjL4N0MyhrrYG9PA3lihC9yKZyUrRO_AhsMs6bLgmDgUaCI718y70GyoWIGk6TviCGUImCsLBT4FncJpwHHJCAr92J5-nIjEWJLk_UW2PcZOtQCFvTFnXbtab1eZLAAxYHDHtVNix1HOLS6QzzUPolYpntE2y4LTHyMicUjMQOApBkmlfgld4QaZPlqpB76TQPQTQswE5Lzc1KM6h5R_hlINhChWilGd995xf9KYvPMofKT3yuSBU8kZLBNBOXW_T2WWPrOjEL4rGZbZw68sG0YEvN3AoISvGMeTyO0uoBI9Ptvo1EDWz8TJ1wTzRUFUqaKTvAFLphIVuLxJFW8FW4QejLE63DEqJEm8kaZBT2Lp6i0PdITvDG13hEAzfvchhLHq1NsSXc8CgP8KeBDeaTLCOHs1UfHIcFsGabTMkMKpooW7_2hjzqamMhT4RSDjswoaY6DGEZfJS7StAnQA0GOcM0vONMg3nJDqL_PPtDDhccYiDVoxPeEloonb-jnFsSarokclQPbs6HifZP6fsfH9UXOxKz9BUlzSBl6eDb8b5B3x6jFe2sFCWCuZ5a-DFbxVpy-i3jeua4rznyi91hWDGgaquV5zXOzxpF3ZaszqRqmBmkpRhoRCLRF4MAFIl5YMNhgJdfbouwhnW7FDICJC3miJXCQ2lebXvhGZMkWR2JYWXE9-j3EKv-6vJO4XbxFsfuVruHoA4vC_XRaSeQClnHZBgWxvVeVwLRYfXjB1OeRrhb4xaNN_6MDLJvHlnfrgbfaJAAH8VhKIPo5w4EVc_UkRZ5WUDzrEzngU3HnPLQKsSRvYPVSYPvxdSvYUPAHpx3BK57S_AcGIjc7S0ILNUEbJXi49XVTMb411iEDk7_lQ2J-A1XdUofeT13yAVjXbY9vGTh-yh3VY-FYJyHn39ckm10kIlNpQYLC1xrTJZtOLys8bOW7tJkQmMT1uXJofi0vc0CrNsUOV_RPQfn892DK0l4riaVYZrE8cAl8Ac8ThtKev3cVHwNA_C_Sxol612ZPw1GrNbCRriRG2ZNob0C10L8qce8g6Yi4dKm8E2__iR1JqnJ6wsdS3u2QbevjxvPsYsxNv77GmyXsbL9M15jqCXDJlYYwuFXnhtvfAeMDjoK7vCJna4OxMX9rL5AwmKH6-z-s0tKImzhZFrjkWskIzABmXbY2RztStW_t9E8lgV4qSlaOO0X9rtyx9ExJqHUYcM9Db-_7Z9vl7LCn3TkplgP_hIc6aX5LrGbrD3ek-yPPbjAq42xDCYbKK1jaJLeW9Udbt2cMDTBPkUzT7jlMo8EMjKFCiQp5ynjp-m0ZZkmpUpHOoy2OY48xMlordk_BY6HuUzECzoLKiYjDqwui2Re8Ab0PwJegM2myMUV3CmAIxdo1kb8aYKo6L4scIA6y75GEwxtBWjTA3LkYB0l5Ab-uNDi4Xkg5p9U6QN5Kq9_hCb3hxMRYiltm1wfb5SjfT6ZA_nLsGF2N05C-YOfqmppHaBDWLxiXIjCbC9GADedC-TH9Q9sQwWQ8x6wcQWENOkLtT6Cdade9dA7P1rI32CzOpMvMADHm83DgM1SliT4ZoVNO2R2xYHunoRrRcfvqjZKO4khv3bfZzR4YIAEYdWdPV2cn3Gszkqz8bUA8CvQwawwibXmBFvcbkZ-2ylFAd_ZpW8NRgroxnkMLRT5h9KYw0RirTF7eypOLNheyD6Julb9lo8u2cQ2PcaUeqFcXsas7tjU-c3yuDgcVuo3JHXzyXcHef0zMi_vA4nZDbfLWg9oqSnivRuJ4sSVIlcb6S_ZEjVH_DaGdINHom2DtjkEpC2_GvnFK09bpNQfGfWz9Wggv8VGuiEJy3bxQH0jNpR9A1FpkWKoXJbKHP9LEIM5gogA138Y6u9dQ2tg8CyXmOjYEu58tTxIcDucTtPLAGbMEjegtyLuxAoCg3vz-HIyJvBhuL3HaDkxJmafvyZz3Ahv3JlzrMJel8TrzofxUWgs4IjXmoETSbN2Y8YhFmNuClXMDre0FZ4pcB7JW4ys2cWtsIIwcyroG_xd5XOGG_VLDG1R4YCFsZbxgjs82olNEI-qGA9n_00LVch8pXTqPHP8Pw6nNiBgek8qjDEd9ON7S3q7DkYqHjOSbPsFLJWz_Vejuf9GPNZQCqHYUafZSPc1krtETreauN4OAyCUJ-LW2WVsbqK4_D_1CL--atJhxqQQrK48jl-RPPTC_0bPM0ph3KEZ1bWW3dh9xRx763TVw4It-oYrF9WU8BSn43HDtcSGtE8QLHaFoXxLIfbf3RC7V-dfTFr14InB31rCc2ybmZPR-TgEcC3mNajmJVIS51geUNuBRuw-dLtQSAcFzqrcu6J7BjSRF0viSG13-6ObAMM5ugFGukGje-pQgMv5mHK1Z0p1RKl7GD6gjGXTvZl4fGpjAqJuzfr8kOQi1nv01Hv84NnmpR4D6EpO6PGk28jnRF908xwxJa7FzA-_6CyrqYKuGX2e4uF0RHFdlHYgGUVfZThh5kTBwlF6uors9jlmuiH6gaoJUhcHuQHoFMf1ZeWc-ZZO3HczJb2N44gWhMWxo38SjSjBf9LoaDeOp7mvKW9huj6YHaNLiz5rwXdTTwqswqdU_9qBq4zZGJAjmW0S_g4pLPLQdhrpbp8YFBEOh1OeizyHLwgWoQEvf8ZFWeBT_brmvat6pRbZ1ZTuYiL9fWJhwTOY3T55RE7JEBBdtOf8wHzUHaNyI-BZPVbBRu5BPGeErpINkcHopWGfmUMHdyDO9u5risNdGiqxl0IjrKYghMKDIa-gW1mQtFCQWGLA992pkIapZ3Squp6K9lH2Hoyy1ODiJ-t-gFCA44QfkXgWQyIRv2BZap-0BlR0TcGM9W8z8giO725Pj8fmmPnX0rKP1KIdi_UYFH0UwG5XKDugePTtOxE6RxpLL2oTkxrNZP7FsCStMu03jmt9c8LQm2mAb5Bu0NAV-mvJAa26uGNrpVs-aieO7Eq-YtqXnBa5PLuvUaR4dNQ-wnmpkAmtqbt4ALEaut7A5Kb-soZEOOx0i5suMEHJVH1rcYTHtSp7awNsUbu8az1-ZV4qMSx87QHy7sHCeZHNtgPT4Ar6AbMwzFaKUkrLVL4JsE0ulFY61mED2MhrmxD5AgyXOUp7hQUI7olhhFCeth3eleOeo8C5zEqVTaJ5k7dTiv4Zuq6y_68wgIbF8vgobfFK18iqXC3UMh3P3FbA1y2WivO4GUj4KfhDWWlPkoJ5Zo5wN9L-_OMRAUVZbOGwSXRFMW7ONlyPFPBJJj69oeiRF1xpUPqkjNqH0Q81NInHishyIYy95h2LIVg_VxHWJq0ttE4e_UY3aDPGRec90Fg0ue9YF9-LrRCPsKBCVEFIjO4NIh07nFigU-cE0hMTnAwgdJVJ55Pb92-hizjdpgMXyLDZ4ut7zAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA0PERYc~WyJFRUFTNGFoVXhYRmxkX1JraWttQzB3IiwiYWdlX292ZXJfMTgiLHRydWVd~eyJhbGciOiJNTC1EU0EtNjUiLCJ0eXAiOiJrYitqd3QifQ.eyJpYXQiOjE3OTIzMDI1NTIsImF1ZCI6Imh0dHBzOi8vdmVyaWZpZXIuZXhhbXBsZSIsIm5vbmNlIjoibi04NzlmYzQxN2UzMzFiYzgwIiwic2RfaGFzaCI6IkIxYnh1LW5uNXRia0FuV2U0cF8zYkNEWEF1VGpsZDBXVldaQWZ2NWhlLUkifQ.A1HrZsE1X-F7fTZGauV-cVRjQt2fMo2CUKV2Iv95uwYP8SEp2dLsiiO0BItZaT0Fkan-J9O6K5JmXbHJlCr3rX-1Hhh4HyMCl_aKDGpgQGLqkI1BitaIxJ0NaM6OQtzBMLRiN-2Rb7M0wVLKazVGIeLxUvEsBCyAh_biD_NXvj6SJ8ka5Q3Kx2op_5jTlrVPUBgKPN0kBYSQuRbVwQsMvnHysocxYipzjNP7RKFgHnTqnn8MdoUC1Uy6eahfOJGq2f_68LhKTR0YNQT7ege7XJnQusZ20ad9X6WKKYVo2eIDm9BJGmn9dT62z4m10hZCm-oobtSO3ZGyl_n-eIw1NFYVTGjG-aXJVzDA51dU24yLkYY4E3XZ--Ha5sBkCNw4adc4nay-4SqeTYbLcUR2uCja5e8ZWV17Z2muZkaiH4scWPPrn8ZC0Qgx4hNPpmTxrnuYrgW7l4JqQGZas70lFS5ytrqTemSq8oh8vEtCa1GYIKETM-e2ioRe6SbySB2uokesThIhrRMz1IEqhHu02gl8SQEDh9KFazpVSLFzT80eZ06OD9DuL5gcwk-6ATLfDMT4LfRgRAx9gQbKxosJM7-kQh_rZQ8ooIQXX9jioQdl4vuaA_sRq-W_wFSlwfWVD9YGaFmvxLqpfbVdZiZo9A_2ENpy0DNAf6uqInJc5eX3vPwvl46AxVNK7t56mrn-MyQz0nQt4d4Ut9y1lJt80BsOLPcQBsZf9wvlXcZ4E0--Ry4f2sm6p5xHXOVvIeDGOvkYXzx3QfPWlHNo5RuXkeUwO8OzUaGBLTOzY1aUAU4PYyiZwkHlh2T_gVdhh5i1egmlSYL7rimmI_gFXLadYxY1KclKY9l2IKEhfFLOlfPcRSkOQmcYU2tocQuKG9nU1lZBOfPP4shc5kMb8B173SprmWbmNXrmZdWZZwp176TX-DZj60XJjIpzHQS6Cey49UyKifWlBV7g7G19KnGihZnud_bxMKKnfb0mhfqZZJ3UJWzDeCOcBXNhftQ1AUf1DKNBE5MxYl_3z4f7LEQTUlWNK-nSxOfgpmwZpFDv8pWQYBBRwmXVb6IlM23g-kQnd5vZohn2W9RYCl8m5cHNLC8RFClrZbBYvb24oICOOLYBtdCo-3QPpZypG6hZroDjrztd5pVMzf8Xu8ANx64U9_LpY4M6SC59pVO_keSk7ZnAjnbGvXYFsp-9IrfLWi-VL7S_WIgnr-Em3nI1u_iPpksRtY_-AsDNBVis2U6D7Skju0Cf3wPN-625PGNKAVeZXjqByRljuMRv3tUmfxNSX2AkBTf9Ua4jYNWUst-KLnJdR2MTpqsbVivscgHAYXE3jCcTVco67G3qrNW8VrvDdYXVK-6Mq_LAnls8beXbsCg1hh8jqHunjp1k9FQd2h7tQoj0Ai-FhN_vtOJaeQAnUgEN9VXsiOQWsKaeH5f3IV4SmAXbhIQYimMQSHH8hOTNsUJYkorJ9XYoEl7VPqwpyp7MElAyIbQKePAYzanpAEErKfHr1ADXtx3iuVuowg9dPSXXyZj4_Pe0Em0fLKvv8rDTYE8mRCHBfwbuQSA8DVjJSlH5PubIV5r9SK7HVJ_APnHAtRs6FsZgvyL-SagP_SYrZwtNgiXTpFTYtdmy1M5G3Y1GEkk0ZaorTPBtpj5HFgPg-FkTeBYDlMnD5sqm3pns9N9q_22NGAPxt_cMhezny4eoNXPzokJy73Y03PWCtetcmkdDT-Zv06GcQwA4FlHtS3mkVBFueaeG_dBAYsZmfWslQmFyxHH3bN1TZOshNsurkXU05pgCDzpoCMZxfz8rkkjbCnljNO-iwOxHz2EBbjRWOjNPUp2WjRa03MMAN71AhlMso78kBPJzoxgPSMvAlq92njXoByD5ODhHd0W8TBS7LbEreBk1wfouA1OAfgFUY3AlSmgcQN_fYjju4cnq7gn1SLqFC2Shnf_zgBC0Bu55GPVk_lf5hilER2kpQjOvs3A6v-uXebKLkQJLLG_28a9CS6S3ra9i9DPr-f9b8iVTGrA4NmLLvfFySx4uGzXGmTQ_yritOx0yri6dJvAnqm0Xce50Zhx7cocJ2ij5sbSpo4YuoLZBQbFE1-ykbIUjK2CVkdkVTYC2cQR4I121XB02kt6z303erUc89eqIxb0ouQYvU_53XElm7X4Eo1UGgEfp1urLgWv_-3_qiUKzoGB4vN0xrnmlg6TK7hRWMecjWGz3b3Pn6KiSIawGeGNDd5RtkeFzkv9Td5svPaERZtGpskGc6RCeIBGz5GQBtSkWeJc1gkE9w5EyhlNCewDn3YyF3ZgYIvbfmX8sL6NiyDS0O-MmlSdMwuKvxrHchA0jYUimEvoWPDFFFFqDfswHODs9Ae06HRIH23su1dQuiD7kR0Yc-U4a-jUS6f4qIbDuMOMnrsZ6uWlr8u8IREMrVcu9qbmx4vdSB6DTgsAlCeV5alnc5WJZqP6Qy3e0wir7AMDcYrMp5AGyhTLdNIASQdUrkPrzpdmsTehd3JEl-VN94pFAWOKz0JhmNgbQhgD6nsFCbsxmb5S_Lik2meb9CgZwZlIt_r6WIzFEFaLlobUFKo3eh2kblhsxFZWyeUi6glWmhICNq_yJf4jgg9hzAFASPdSolc8F77VO2QzCqSSQOmkpf0w2NrwKVFeiKZf83tk4nQyYu1jj4IkNjgPbibd92wCjiFiu7_J5Igy7cOMDpjy0kjujc2jkCyMm_mGfDyjwfn4SbR5u8hS3rkSnR6nCAzh0j118lQ4hFEcwDH2MBfecLsUjEuYj_gh73t2GVK3PxSMdVRmSUy2dfd0Vbqb41xuIOqnr0k11dJP9qg812GaQzdWgAA_bxVRacJ0xO8OPvc2MoralPuw8fWXnH6J8mbXEqV-n5wfQCZz8EYrCZimtFbg29G2tmFfdJLsIYOvVMPlbWdzoRH1elqnH_OGJN07SMzBKWts3divUoxI65dUEy_gh4v9ZVBfiLreTNeYvVTn_yjACuXCkmWGG6mkf7AosBV4004c4e6Jwd9uAELLVzlILZ5ZOzECTjrOd1ZT11oUIW7vJYKuCgqegXDEpIB6abmyHbYsNE6rGU9ZEbixQ2d-x8V-_ac-MBGmn1I6zJcPXkCUZW9sdqlmdbad4AYuJ4f0ts0KFmwimpOgCeN2RP4aR6AMHmVXqlB-pP7-OF8_Wohe9F3WfLszYE3z5IW8tMqt-zCu17GoY3orqSJSXSytOa5fz-hCATslXiq7taFOPX4nR5iwFd6bKie-QZ24skWHJSjWH9P5uIK2fxaf43HZJMoMIvPSXnFHXBogtRNm3GMs4xQA2bOoDxQTHCE7TwAXbeUqS-KlhWtH8jwD_7ODQ3D8Mn3vNkdNDdCmoHxjFzdrK-lN67S__yvwYDi-LTM3K06LcApkHwPCJ52r51bnyQDgaUxzrsQTmI9l3Kky3TIXFu6gBAmFJ7fvc5D7s4-IXlY_kNi0tXFgI7B7dyG3eieEGmKBPvPuCu9q33bzM1EjWxeBViukXwjuiDEdSlGSN_vQaLD2MKbTyCD7GRbyQ0wITH_DqYpmkjcKp_UDT0BUgvvUGZunwg7zeTEDh6gzVWKj56JViUwdQM-KR99EtaIPJ2NFKqqyyxfVuWg6efxLzphSONvwE-bpVOVw1BNYgPFuejvSRtl3QmkwIZRsGP8Fvn1JCsvdbjqvG9kVJK4sAOTrDDII8zFq7aXDcpwEm54fQp1tlsP96G_xNqCjnCFfDz-ln-6ET7BfQFaBmcI06OkUxFWfC2KVLxhzN3n926oMDhdUhXxYTc-LZg8ZvBo3Hbuz2PE_7h2Pa4BjX3CeTSINStbFRrwY_wFDy3TYouROYDUbYToB1pSQipPRmirIunlaah6cqNOVBT7Ga9weG2XeakoAF3R8xEGwCToVZaRPXaYq172q6NAhrqAic461aJxlZMJZwf15LNr3NilCQ2lGfLpswyw1vp9WwxyidoQRrK9ZeMH7PGLBVOxAj_5KAN1xqqYp1Xofj8dCfjFjbifYi28w-VAQ9ylIljiwpCnpWRBQWMgMUmL3f6cAOQkZCKlO7g9FkDBchf_a9wsPa4CwTlwD9suLwQrxoBEBB4S_4gDU7cZbi8vIjiBpHDXpmlDEniVrKgcYKqpU74VfgGm7J2hVAoRHydjzlFhTkYkjJ9NKtLdQkU0XZmj_l1yI4WdlO92akBWQKaGKa4W5CS2XwIO4tFxYL7nkIeXQt9_F7yRgLsWN_vW2wuI2DVN6VkrbU2d8XPJ9bfcIOd50mUS0O1S9ftP_q_kbJJq1CtUFDXx2i2R8k9DpbL71f01vPzac8SFVXcSYvSJaio8zi9v4CJCpYWYet-wMHNTpLpbq-BkBrvtBrcH6YAAAAAAAAAAAAAAAAAAAABQ8XHyQo", + "issuerKey": { + "alg": "ed25519+ml-dsa-65", + "ed25519": "MCowBQYDK2VwAyEAhca9rLOG6cMHfk2weCWFz7wily7iDR/Qvhhs6tTQGjA=", + "mldsa65": "MIIHsjALBglghkgBZQMEAxIDggehAOk5+trXXL8kl3d/8xVbMH33e7FIvN4r0xAKE+iOsC2LUz7ZPSz7CDh9kh+6cekIKGQNLBuedGbAAygXRx2nG4X1kiuUUkQREX72SIkYUYvag2s3Qk8eLd2MbH5IrkWNyiJRGOSs6tBzJBsnBnOY9tiJYwNsXOwopFLjvf29SzjX0gAep4COZiLNpnx4TcSBGhIxuywWbGeAG+XOkL/UgjUN8SbRobhtXgifgmKddAxlaYXpmSyyQE/jpC+3yFprzIbyDGSsRzDAlOrxRYt7VyHzFTOqGHMNqCZlnE9B6tFxT/Fnd6gyG5JSdHohYYyhC2VMxAoGAhspeRPtYHeD5YTX/aa189p6XBsKF79R8MH9fLloMwY3JkrA8BZeJ8hdHko3Z/HU7DoGA95JMn7s1KxegZ9HHOBRy5vT4wk8od12jHHHiVIy5ohBTkQAd6oT1WPKRdOQF+Wf+SGkyQXwpLhfBkZhlkIgfp/inyep8arKvqqAnC98H2fkzfsSqh4p3bdhvtxkcQy/9P9mPDlcIS/zKbr95Audq8QmZmV5iVOlN7Qsp58WJ7MU1/qaqAG36bjT9giuz+CPOZYzhkt4bABzb6u28nK9hDUK/wyK57DtKfPwL12aOD8QZf6PkK07QLG+ojWzxn1DwSskUwQ5ffW+HooiyvrYoDJwA+aBanRWFuZKWAth4aIaBsq8kIH/26nJmYAcBkVIcNqjsEwKsW8JfJgD+xxsV4r+S92RH7kX/kv695+F1p9xNAFdiJK7XisWPrTfjdA478UG3cyRfZHyexjCA6yyiVbRR6XDibNK2Tdy1t0Qv2ibf9ytXY14snCkIX1OWKzwr6a0GHDEWxCl0UHFFhtdDbrYySQvgmyUzWL0FQKpZy18tyWQzrohGhi17D/tgzky9dFGIePdD1Ew8DQ0ShXXDe5F/M1Onc0D88D680AiZWrpfe+RbdkgIHp2M+Gzhx1cdahXAOvB5UwzdXkwjo1IK6H2S8qfpgk2SiijKYW/3vrtC8ieaEkRs/VdmpYYwd23AxP6vX10+2owV+QXHWuseO9uyaTeUiDWry4+0sNffiEwtue2Fe7l8bc4NRDt/9IHF7AyHckQirHz4wg1qSS+RnNvkyVyu/bEzn4EEnlKfE1trv6n8hWxhUg0vk09uNSIeKhhhfywBd2FRrd13zlY2vNW33iXVZkWPTbF7lIt7VfIFaXgLeg8UZyVS0Bt97hvdnyBGcrsGk5OBYgsvlTm2qbudukalIjfzauiKKb7tYJv67NiG4R1r3dxQJj1DtfB9N3a2DlUcq36ZZj76LWdwMjtvuPVkwKmVi72wsNmnu7jt8iaSF/7h8jgXFRvUGTav5GeLlcY+yV6yO36K3prrgU6X0rHw+9kHlFB8x5M3pDirh53CAPXv+p7HP8c5dciYp7yraT21HQKu8goyYySunHSl0z/sRH0eVJV+fr6v9uRvoC/SA6Xp+phtSQW8UPAZNbhD1nLpW2EiG7LbzX6Ps236+SRENpa167yxG8WxArvFzq6G0vxMzm9hY4A1tGRINy5xKXeXRlrxyIU/zARWtkflco5yH1u5gZf5YHT+E/LsNv8j9/2z2nZB2UbM6IenNuvlWU/rQjN3+EXC+ccrvNt8+MDDnzuNzlNDPQmeNq0/6+lHUAzNB9Tcw3tsCr5dsWAAcq8a8JNr09j2vcEYR4X6u5p4cLZIYP8N9fTsq1/+fQqvgVaCZTDodzlgyCNalCnJ2TgyAdy8SE/XPdJcvWgPRR2y3VMrXi/o9q/oPJQRbxDYZwCFbSM94T8Y+XwCM8l4ubTKWS4ITu2DNtQIvXvQH+AU6SJypKPhGCzwGbeSCoS20qrwIu0LfwCBz3xEkpVioq2/9Y/zBr7yzvZpe6pdqw0hzx9mvZl7xEP4roD7quo8Ktl7iGiPgL6lN2rTPQgIKb2A8Zp/azLh89QHd6PlJmboAseNU5YiGZQ0JPUBKWz2MfuhYlx9BsUvKTOO4Jfi5kKQ4Ujs5lLMWTdTAcrax/5+BC6Dadv1b2N6Las/0Hoq7oIQNscbd4GBKXqyHAv2nRNxrolEdspjglpAt6MGaYvZ3onZ1uscWDKA20al/uLMtR3jBs5IMcR+H27a/ExRFazDaKt6+JrYA8OKGhpNia2DMCjRga/zvwmc4jECh0r1A6R9wWyhzdmYIsmOO0Y8gxoxtK30c2NUW3BQ45aucum0qENXQuGuwMeKqm9dWIw55RCwbld9wxnFK25RMa80oeU5kvSWKfQ3nrUAPJYDMqo8SkKcNlSslY6EdOP32H8bU4yHH0nHMwchv3czgaQrACyZmx8K7Qpe9Jx8oC5rwOf9B5e3gfZG690ck99vQP1/x5MPmkjyVLUtpvuaLZtzyviv6V6BpLxsGKKCB0M6YOymczvl6gozBAnWnIup2qyuOuMKkMRJOGiTFAyH37Dk/a8QFmupZe0FN9/7vi3vC8diL3zYKYUx/6DlWScTB9mpvf9L/hj00qVebCQu7MFEjb9qy16N0ofpz+S41L3I2AsqTEtIPpRdxO6CwMHqobXmiPrCmmrxys4AUg6X8BoGQtJK39SdgUS291C9cF4EAm7LQWw" + }, + "audience": "https://verifier.example", + "nonce": "n-879fc417e331bc80", + "now": "2026-10-18T05:50:12.000Z", + "expectedIssuer": "https://issuer.example", + "issuerAlg": "EdDSA" + }, + "expected": { + "verdict": "INVALID", + "failed": [ + "sdjwt.hybrid-half" + ], + "levels": { + "integrity": "NOT_MEASURED", + "authenticity": "FAILED", + "anchoring": "NOT_MEASURED", + "finality": "NOT_MEASURED" + } + } +} diff --git a/conformance/vectori/sdjwt-presentation/aere-hybrid-keys.hybrid-half.json b/conformance/vectori/sdjwt-presentation/aere-hybrid-keys.hybrid-half.json new file mode 100644 index 0000000..5561e9b --- /dev/null +++ b/conformance/vectori/sdjwt-presentation/aere-hybrid-keys.hybrid-half.json @@ -0,0 +1,67 @@ +{ + "v": 1, + "kind": "aere-conformance-vector", + "id": "sdjwt-presentation/aere-hybrid-keys.hybrid-half", + "artifact": "sdjwt-presentation", + "title": "a credential signed with the Ed25519 half of the same AERE identity alone, against its hybrid keys, by a verifier that asks for nothing (what a holder of the classical key alone can make)", + "source": "derived from sdjwt-presentation/aere-hybrid-keys.valid", + "requires": [], + "exercises": [ + "sdjwt.alg", + "sdjwt.claim-conflict", + "sdjwt.crit", + "sdjwt.digest-twice", + "sdjwt.disclosure-form", + "sdjwt.disclosure-name", + "sdjwt.disclosure-twice", + "sdjwt.form", + "sdjwt.hybrid-half", + "sdjwt.issuer", + "sdjwt.kb-audience", + "sdjwt.kb-claims", + "sdjwt.kb-cnf", + "sdjwt.kb-context", + "sdjwt.kb-freshness", + "sdjwt.kb-nonce", + "sdjwt.kb-required", + "sdjwt.kb-sd-hash", + "sdjwt.key", + "sdjwt.sd-alg", + "sdjwt.signature", + "sdjwt.time", + "sdjwt.typ", + "sdjwt.unreferenced" + ], + "base": "sdjwt-presentation/aere-hybrid-keys.valid", + "mutation": { + "op": "set", + "path": [ + "sdJwt" + ], + "value": "eyJhbGciOiJFZERTQSIsInR5cCI6ImFlcmUrc2Qtand0In0.eyJuYXRpb25hbGl0aWVzIjpbeyIuLi4iOiJvR1ZLcV9iYUlSbkdjX0x6S2c3Rjd4R0M2OTdOLXJSWjdNbGJMdk9jMzlvIn0seyIuLi4iOiJYU1BvSVhtdkRMMHY4a3BTZ1NGZ3ZuTWZtcy0zTllLdmtYM0cxd2RCNmg4In1dLCJhZGRyZXNzIjp7ImxvY2FsaXR5IjoiQ2x1aiIsImNvdW50cnkiOiJSTyJ9LCJfc2QiOlsiMGl3bk5fZjRkSGhfcUI0SlhyX201OGdoWFNYRmk3LTk0SDR4QnpOdXBMSSIsIjM1QnNjekpxVEdFNEdWY0ZZa0tEMEY3R0wweUlUdlp5MVpSU0x2Y2tpdTAiLCIzWm5SQk9fMGFZbUZCQUl6Tl9QZmdNYVlacUVYdWNibkg1Y2E5RFNNTFdBIiwiYVptbDlYNUU5cDhxaVJvcExVeGtTeUpsXzgtemEzQVRnRWFEanhBQ2o0OCIsImpLSlJ6WS00NmR5cWFHMnliQkk4OUxuT0ZuQ0xRSl9Ddk5rWnlnOFVxQzAiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiaWF0IjoxNzkyMzAyNTUyLCJleHAiOjE4MjM4Mzg1NTIsImNuZiI6eyJqd2siOnsiY3J2IjoiRWQyNTUxOSIsIngiOiJwZGd3WVJzOEtTdl9ESUJvaEFhaWx4MEoxS01xVGkyd2cwVGY3TmJaOFY0Iiwia3R5IjoiT0tQIn19fQ.h75vV6jAkI1n0st_TUrGseDJKEk0RScfX5neSm_CSN2GoVjL2Ml2oKmB0szWDCjjlAcq1Tn1B39KNwEE1dWzBw~WyJSYTAyRkdCZDJmMW5HRlJPUFNUdURnIiwiYWdlX292ZXJfMTgiLHRydWVd~WyJZUVdma1BBSW1UTUpTU09vNmQ1WVJBIiwiUk8iXQ~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE3OTIzMDI1NTIsImF1ZCI6Imh0dHBzOi8vdmVyaWZpZXIuZXhhbXBsZSIsIm5vbmNlIjoibi04NzlmYzQxN2UzMzFiYzgwIiwic2RfaGFzaCI6IlBWY1dzVDZ1SVFhUHBRUUM1QUQyUzQ3Q3Z5QllpRTBxQ1RVZUtpd0ZfRlUifQ.i8LKIVBV7X67veBsk-vyyCHouKBFEXP5uLl927e3m3-_JE1hOHuyv0KX2gioAO81kI1FY1TV6xHOiQVF-UyBBg" + }, + "input": { + "sdJwt": "eyJhbGciOiJFZERTQSIsInR5cCI6ImFlcmUrc2Qtand0In0.eyJuYXRpb25hbGl0aWVzIjpbeyIuLi4iOiJvR1ZLcV9iYUlSbkdjX0x6S2c3Rjd4R0M2OTdOLXJSWjdNbGJMdk9jMzlvIn0seyIuLi4iOiJYU1BvSVhtdkRMMHY4a3BTZ1NGZ3ZuTWZtcy0zTllLdmtYM0cxd2RCNmg4In1dLCJhZGRyZXNzIjp7ImxvY2FsaXR5IjoiQ2x1aiIsImNvdW50cnkiOiJSTyJ9LCJfc2QiOlsiMGl3bk5fZjRkSGhfcUI0SlhyX201OGdoWFNYRmk3LTk0SDR4QnpOdXBMSSIsIjM1QnNjekpxVEdFNEdWY0ZZa0tEMEY3R0wweUlUdlp5MVpSU0x2Y2tpdTAiLCIzWm5SQk9fMGFZbUZCQUl6Tl9QZmdNYVlacUVYdWNibkg1Y2E5RFNNTFdBIiwiYVptbDlYNUU5cDhxaVJvcExVeGtTeUpsXzgtemEzQVRnRWFEanhBQ2o0OCIsImpLSlJ6WS00NmR5cWFHMnliQkk4OUxuT0ZuQ0xRSl9Ddk5rWnlnOFVxQzAiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiaWF0IjoxNzkyMzAyNTUyLCJleHAiOjE4MjM4Mzg1NTIsImNuZiI6eyJqd2siOnsiY3J2IjoiRWQyNTUxOSIsIngiOiJwZGd3WVJzOEtTdl9ESUJvaEFhaWx4MEoxS01xVGkyd2cwVGY3TmJaOFY0Iiwia3R5IjoiT0tQIn19fQ.h75vV6jAkI1n0st_TUrGseDJKEk0RScfX5neSm_CSN2GoVjL2Ml2oKmB0szWDCjjlAcq1Tn1B39KNwEE1dWzBw~WyJSYTAyRkdCZDJmMW5HRlJPUFNUdURnIiwiYWdlX292ZXJfMTgiLHRydWVd~WyJZUVdma1BBSW1UTUpTU09vNmQ1WVJBIiwiUk8iXQ~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE3OTIzMDI1NTIsImF1ZCI6Imh0dHBzOi8vdmVyaWZpZXIuZXhhbXBsZSIsIm5vbmNlIjoibi04NzlmYzQxN2UzMzFiYzgwIiwic2RfaGFzaCI6IlBWY1dzVDZ1SVFhUHBRUUM1QUQyUzQ3Q3Z5QllpRTBxQ1RVZUtpd0ZfRlUifQ.i8LKIVBV7X67veBsk-vyyCHouKBFEXP5uLl927e3m3-_JE1hOHuyv0KX2gioAO81kI1FY1TV6xHOiQVF-UyBBg", + "issuerKey": { + "alg": "ed25519+ml-dsa-65", + "ed25519": "MCowBQYDK2VwAyEAhca9rLOG6cMHfk2weCWFz7wily7iDR/Qvhhs6tTQGjA=", + "mldsa65": "MIIHsjALBglghkgBZQMEAxIDggehAOk5+trXXL8kl3d/8xVbMH33e7FIvN4r0xAKE+iOsC2LUz7ZPSz7CDh9kh+6cekIKGQNLBuedGbAAygXRx2nG4X1kiuUUkQREX72SIkYUYvag2s3Qk8eLd2MbH5IrkWNyiJRGOSs6tBzJBsnBnOY9tiJYwNsXOwopFLjvf29SzjX0gAep4COZiLNpnx4TcSBGhIxuywWbGeAG+XOkL/UgjUN8SbRobhtXgifgmKddAxlaYXpmSyyQE/jpC+3yFprzIbyDGSsRzDAlOrxRYt7VyHzFTOqGHMNqCZlnE9B6tFxT/Fnd6gyG5JSdHohYYyhC2VMxAoGAhspeRPtYHeD5YTX/aa189p6XBsKF79R8MH9fLloMwY3JkrA8BZeJ8hdHko3Z/HU7DoGA95JMn7s1KxegZ9HHOBRy5vT4wk8od12jHHHiVIy5ohBTkQAd6oT1WPKRdOQF+Wf+SGkyQXwpLhfBkZhlkIgfp/inyep8arKvqqAnC98H2fkzfsSqh4p3bdhvtxkcQy/9P9mPDlcIS/zKbr95Audq8QmZmV5iVOlN7Qsp58WJ7MU1/qaqAG36bjT9giuz+CPOZYzhkt4bABzb6u28nK9hDUK/wyK57DtKfPwL12aOD8QZf6PkK07QLG+ojWzxn1DwSskUwQ5ffW+HooiyvrYoDJwA+aBanRWFuZKWAth4aIaBsq8kIH/26nJmYAcBkVIcNqjsEwKsW8JfJgD+xxsV4r+S92RH7kX/kv695+F1p9xNAFdiJK7XisWPrTfjdA478UG3cyRfZHyexjCA6yyiVbRR6XDibNK2Tdy1t0Qv2ibf9ytXY14snCkIX1OWKzwr6a0GHDEWxCl0UHFFhtdDbrYySQvgmyUzWL0FQKpZy18tyWQzrohGhi17D/tgzky9dFGIePdD1Ew8DQ0ShXXDe5F/M1Onc0D88D680AiZWrpfe+RbdkgIHp2M+Gzhx1cdahXAOvB5UwzdXkwjo1IK6H2S8qfpgk2SiijKYW/3vrtC8ieaEkRs/VdmpYYwd23AxP6vX10+2owV+QXHWuseO9uyaTeUiDWry4+0sNffiEwtue2Fe7l8bc4NRDt/9IHF7AyHckQirHz4wg1qSS+RnNvkyVyu/bEzn4EEnlKfE1trv6n8hWxhUg0vk09uNSIeKhhhfywBd2FRrd13zlY2vNW33iXVZkWPTbF7lIt7VfIFaXgLeg8UZyVS0Bt97hvdnyBGcrsGk5OBYgsvlTm2qbudukalIjfzauiKKb7tYJv67NiG4R1r3dxQJj1DtfB9N3a2DlUcq36ZZj76LWdwMjtvuPVkwKmVi72wsNmnu7jt8iaSF/7h8jgXFRvUGTav5GeLlcY+yV6yO36K3prrgU6X0rHw+9kHlFB8x5M3pDirh53CAPXv+p7HP8c5dciYp7yraT21HQKu8goyYySunHSl0z/sRH0eVJV+fr6v9uRvoC/SA6Xp+phtSQW8UPAZNbhD1nLpW2EiG7LbzX6Ps236+SRENpa167yxG8WxArvFzq6G0vxMzm9hY4A1tGRINy5xKXeXRlrxyIU/zARWtkflco5yH1u5gZf5YHT+E/LsNv8j9/2z2nZB2UbM6IenNuvlWU/rQjN3+EXC+ccrvNt8+MDDnzuNzlNDPQmeNq0/6+lHUAzNB9Tcw3tsCr5dsWAAcq8a8JNr09j2vcEYR4X6u5p4cLZIYP8N9fTsq1/+fQqvgVaCZTDodzlgyCNalCnJ2TgyAdy8SE/XPdJcvWgPRR2y3VMrXi/o9q/oPJQRbxDYZwCFbSM94T8Y+XwCM8l4ubTKWS4ITu2DNtQIvXvQH+AU6SJypKPhGCzwGbeSCoS20qrwIu0LfwCBz3xEkpVioq2/9Y/zBr7yzvZpe6pdqw0hzx9mvZl7xEP4roD7quo8Ktl7iGiPgL6lN2rTPQgIKb2A8Zp/azLh89QHd6PlJmboAseNU5YiGZQ0JPUBKWz2MfuhYlx9BsUvKTOO4Jfi5kKQ4Ujs5lLMWTdTAcrax/5+BC6Dadv1b2N6Las/0Hoq7oIQNscbd4GBKXqyHAv2nRNxrolEdspjglpAt6MGaYvZ3onZ1uscWDKA20al/uLMtR3jBs5IMcR+H27a/ExRFazDaKt6+JrYA8OKGhpNia2DMCjRga/zvwmc4jECh0r1A6R9wWyhzdmYIsmOO0Y8gxoxtK30c2NUW3BQ45aucum0qENXQuGuwMeKqm9dWIw55RCwbld9wxnFK25RMa80oeU5kvSWKfQ3nrUAPJYDMqo8SkKcNlSslY6EdOP32H8bU4yHH0nHMwchv3czgaQrACyZmx8K7Qpe9Jx8oC5rwOf9B5e3gfZG690ck99vQP1/x5MPmkjyVLUtpvuaLZtzyviv6V6BpLxsGKKCB0M6YOymczvl6gozBAnWnIup2qyuOuMKkMRJOGiTFAyH37Dk/a8QFmupZe0FN9/7vi3vC8diL3zYKYUx/6DlWScTB9mpvf9L/hj00qVebCQu7MFEjb9qy16N0ofpz+S41L3I2AsqTEtIPpRdxO6CwMHqobXmiPrCmmrxys4AUg6X8BoGQtJK39SdgUS291C9cF4EAm7LQWw" + }, + "audience": "https://verifier.example", + "nonce": "n-879fc417e331bc80", + "now": "2026-10-18T05:50:12.000Z", + "expectedIssuer": "https://issuer.example" + }, + "expected": { + "verdict": "INVALID", + "failed": [ + "sdjwt.hybrid-half" + ], + "levels": { + "integrity": "NOT_MEASURED", + "authenticity": "FAILED", + "anchoring": "NOT_MEASURED", + "finality": "NOT_MEASURED" + } + } +} diff --git a/conformance/vectori/sdjwt-presentation/aere-hybrid-keys.valid.json b/conformance/vectori/sdjwt-presentation/aere-hybrid-keys.valid.json new file mode 100644 index 0000000..e30ae8b --- /dev/null +++ b/conformance/vectori/sdjwt-presentation/aere-hybrid-keys.valid.json @@ -0,0 +1,59 @@ +{ + "v": 1, + "kind": "aere-conformance-vector", + "id": "sdjwt-presentation/aere-hybrid-keys.valid", + "artifact": "sdjwt-presentation", + "title": "the credential signed with the ML-DSA-65 half (issuer and holder), judged against the issuer's AERE hybrid keys ({alg, ed25519, mldsa65}) with no algorithm named: the ML-DSA-65 half is the one judged", + "source": "producers (sdjwt.mjs: issueSdJwt, presentSdJwt), three hybrid key pairs made and discarded at generation", + "requires": [], + "exercises": [ + "sdjwt.alg", + "sdjwt.claim-conflict", + "sdjwt.crit", + "sdjwt.digest-twice", + "sdjwt.disclosure-form", + "sdjwt.disclosure-name", + "sdjwt.disclosure-twice", + "sdjwt.form", + "sdjwt.hybrid-half", + "sdjwt.issuer", + "sdjwt.kb-audience", + "sdjwt.kb-claims", + "sdjwt.kb-cnf", + "sdjwt.kb-context", + "sdjwt.kb-freshness", + "sdjwt.kb-nonce", + "sdjwt.kb-required", + "sdjwt.kb-sd-hash", + "sdjwt.key", + "sdjwt.sd-alg", + "sdjwt.signature", + "sdjwt.time", + "sdjwt.typ", + "sdjwt.unreferenced" + ], + "base": null, + "mutation": null, + "input": { + "sdJwt": "eyJhbGciOiJNTC1EU0EtNjUiLCJ0eXAiOiJhZXJlK3NkLWp3dCJ9.eyJuYXRpb25hbGl0aWVzIjpbeyIuLi4iOiIxRlgwR3hrbVlvT2xBSXpiYXhaYUhwQnhLSlI3RmtFWTVKNHBqc2hrMG1zIn0seyIuLi4iOiJSSXVOb2ZvT21scjlsbnFsWmJneGNIdmxKZUp4S0JwaHRZbXRwSWp6NENRIn1dLCJhZGRyZXNzIjp7ImxvY2FsaXR5IjoiQ2x1aiIsImNvdW50cnkiOiJSTyJ9LCJfc2QiOlsiNTJ3RnVzclB1V0lfLXRfUUdfWmdxTDhWUkg0Njk2MUw0OWtpMGMwRDVPOCIsIk4xNlJZd2lnanc4dW0tMWxza1NfTloxVlpUWFNYaWRaUy1oQ2pYZ1lvVUUiLCJRUGluaVdOOEFHT0QwNDltOHU2TzI5bmtiVWg3YkJhakNkckxzQTFTSUdRIiwiYVZjSTRJdUZweEgwOGNPdFlqQTRYZldJSlJsTGFvMmhsU3h2U2tVbG1YOCIsImJic3FsZWRTUnQ3ZTFPeXpoVmZLdXF3SDZhZ2kybTN1b2h0VG1zMGV4ZGsiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiaWF0IjoxNzkyMzAyNTUyLCJleHAiOjE4MjM4Mzg1NTIsImNuZiI6eyJqd2siOnsia3R5IjoiQUtQIiwiYWxnIjoiTUwtRFNBLTY1IiwicHViIjoiekxNT3Y5eEJrRzE5ZTVacnNqLVhVbXJhVk9wNkMyVS1WYi0zQURQWU05NUduRXBLdVVnLXBNN2tOM0tiWi1ZLUlJQ0pZa05IbFA2ZThRUGtpNzJkaGtGUTdUbnBXdnJoQXJ2d2dkZ1JibWtxSFNDcEo3OTc3ZU1kQjNfMmhYcmhLN0J3a0g0elZGdXk5eFJDN0RyZUlHeGNIZHdEaDFfYTFQRWJXVUlWcklhNXZvZURRSVpSMF9HQWlkRlEwN042Y1lQZ1BzallJVERpVHF3N0x1Tnk2MVlObFF6R2F5OFlYRV9xU2ZYdVhpWkxFU29KNS1ZaG5BWjRqMHRlQ1V6ZkJmcmE3ZnZ5VEhNMGFMN29Bd1M5RmIwcVExbVlLMV82NGJlQjhLTEJfWGFoUV9ZbkpNUXg2ZVZQWGJURVljSTNNNHVqRHIyVXV2dzRTLXptdEZ3N0dRLTZGdzl6Vk80YWJzRTVvb2laT3FvbURoSjFfdldSU1BUX096bDlfMVRhaVIwUzBUQ09ZcVg3YU80bi1sVXFpcko2ZzFTMFJYRUlUTkE2SWJXSEpGbjVueTJUZ09LWVE5WE90ZGFSekVSSi03cHZacS1uLVhHNWE4RlhrNklZWnY2cWduaEw2M3hkcksxUmRFT2d5cXJMcEdvNDdXYlhGb2pFaDQza25jUHEwbVM0bC1YMmdienZFYmhpMzhRRVk3VzRaZ2hueWJBT0QxNVFfeFJSa1IxajVJMVlrNXRGb2gxZ0tyZ1pTckdWRWJnZG52TWQ5dFUtZmZuNVhRZkdja0VqeUJrXzlJVS1wRzRON1QzNmozTUw4SmFEaVRYMHpjRVlCRGxuWEV6UlUwZ3BDdnp3UG5kQjFCcGdGNl9zRENaUDFOQVQ0MGdmZEFKc3VOaXRSb2l2T2tCbEhpYmVDbjY1MEI0WEZSYmdmaFJpTVRzVzF2WnM4OE5yLWh0dHc1SnJwMGdyQXo2T2owczFwa1BPUG5ZNGM3X3VXQ3dVaGFDVTF5b0lEYXQ2X2JRNUk4ZmJjOXFBQjUtYU03S3ktWlU5bkoyU09XS3ZvbHhiV2dwU2NpSFRUcjlCWFpROVlrQktGSGVvRGV4RDF6US1NQURtd2JnSC1lWmhTcXBXYVBNaXNBdUZ6VGU2bDNNdDduTklVSTVuUVJKSnkwTlM1WlBsWUxrX3hMdGZaNEdDWHJhZnRhaFhyRFpjNWFodDdKNURwQlV0VzBBaHl4TExvajdiU1RMNmhvelc0NnVRZEIyYkw0V2VsTjNBX0F2LXBtNmlJRVBndGhxbXo5cUlXdnBXTHNneEZ5SlZYV0Z3R1JCVUstaDM0Zmo1aHR0dW1FcUZZelNfNzBVeTljdWwxSy1qZkNXb1VTSDhFQl9nNXJMVk00Rks0QVpWNDRkN3FzSnk0VmZwUWk4WTJQaUJJVHVTc09UcUhRQkw3ampwVTNWak9NSVNRUi1XRFlzQWpORzJnYVpfVWtNVk9zaDhUQ1RHa2FYNXUyX1hXTzJLSDF4eXhESFJ2VWtsUy1sRWhOVHg1RzV5T3BUaVJnaWg2ajZ0dlVkbEdEM2VOYXBDLXpmUEVuRGhUdUplWFBqUE5PV1RZWXFEZkxtUXB1TEZfLXhXekZIMXQzamRHSFVpT2NocE5ab2VfaDZSRVFoTWx1dUx4cGJkLXhmRi1GTnQ4Z3FUeWwzMDhWaUY4aWR2SzlUWjR1ZHVOenQ3WTdtMTN5cklxRDRFVlV4bUlobFZCQzNCYWZqOW1GZ0NBckFSOU1oOG5GdTBFUGtUS1RoQ2pQbmF2Q0hJeWNJZ1JZaXhzSUdMcHRRZjEtM0w2cHpTenRQTU9sb2dYTmRKeDB4WHZuclVnRldDNUZIS3V1MFI5MlFtQ3FvTXA0bGJ0ZU50TURKbmQ3cDdMS1ZZUlNkODdDRUZBQmJIZXltamxFS1ZhdHBiMXQ1dWFIU3BxcG5tQlJWWlpCUklMa0xnRXAwVExBOHhzcEhnMWxvNjJ6UmRWdmRDYUkyMlJLWVNYcGR3bHNBdS10Mm43b3VqdzRMWmFEN0wzT0tLY254VjEtNVpULWtlb1NjOFhVMEpUbmhIaVdHYmJLbjEzME5fVkZMYUlSalhyQ2lremVXRlI1V2JFVDQ2cVZpcVI5b3FBb3lpekxXbXFoaGxORVFTZXNlcXdLUXA1NExjNTVRWnM3c3djYmstLTJWTHZZOE5WVmxCaFFKS1lVUHJVeWI0dE0ydzdqa3R1WUhHMGFPN0VMSlRnMTZoU3Bua3hZS0g0cERsUW5vQ2phTXMyTXgwYjNoQnNSOGh2QXN0UXRsZEtma1NOVEw0RmVFODB4eEdtckJrS2s5OEY0SHRteHFQemJxbnBPRlp5cWo0Z3BhZkpHd1N0TVFpa1ZLb0RHR1NFc1M1QTBJZDdBdEdGZGdGWjh6OFYySTZxOFVtckN3SzF4Z1dXSkRkTlNpVFlwT2pHdURNRksxUUtMVU1PM3luWDZDSldBOEthaG9JZjdSNVNXQmxtOV9VSUNPeHhJOGlNTmtDMWdiTXpSbDdfWU9ZV0c2d2d5YlVyN3hoeE1NUF9Sd2l0NEZyd0hPXzlYM0NEUTZacUpTVmd3dFZmaHBpeWlZSlJiWFEtNE9DSGZNRmJQalpYNWhURUZoTmp4VTNWMEVCWG5IbmxfRFNuLUd6bXZOcHNTZFJvMHZUVGRPb1RyTUVHcE5jc0s4SGwxaVpuMlJVLUFCV3lBVG5FRXg0bC1pZEVtZV9rVDE0T29CTTV2eTh6Sm9IVHNHcmpHeFktWGtJa0hudEpIWFJGVklWMEJ4ekVaaHhDd3c5MmNMTDRicEJZM0JSejlaVi10NnZKcmhBVG1BQ3lYY28wQ3ktb1d5czFhTW9jS2I0MWJvLWJpSlRFanRRdFhYaW9jRDJuZjlzdzR3NDhGd2FQZVppNTYxQmtYdDB0MEZfbVdZaWx3YmUyRDRaS0RfaHNCeTBsVFUzaDREUUNtOVdxUDdEbkRyaHBKbm1OMTNWZEZWUVZMV1BoOV9aQTdiNEthZ1I0SzdVS3FHUFI0X2ZOUFE3NUo3QW9WZlZKY2RMZElIUU5ocEJJNG9Ba0pVU0RSdTZmUXVMbFJJaERFM2N6MVV5X0xYZlQzT3lUY2NkcFdheVVDdXNqeUZ0M3JkcGFrNlQwV2t4dFNNcHdMQ29GWGNfcnp4QlVYTDhiRDR2ZG9sTng4Um5YcWVIS2RvYTVPR3pYSDNGTWwtQy1EYk5ISTlUaUt5b21rVnUzQm9wbVVBenpzcFRwUzF4RmhwVjEwVmVwem9xZUpjUzhhTUI2LWNyY1lEQm5KMmxpSkxyY2hCT1hsZ3ZLalBKZGc4WjRpVjBjZDQ5ZDN4YTd2WGVIT1Via3AzS3lheWtWUUxhSDA5ZGxrOVZuMlFweFhiaGRJb1RQWlZiSnJCWW4ydnNLRXloU1o3VnJKYmhwZ2cifX19.rzRJtm9JwwdtV_pyYoLNMFh-LeT826WsdO_OSU3s5Hhqu9iZQ_q5g7d74vPNB62EtcAV1CrcpUYorEeJnYyatGz_rcqTHwybNysflBJCBlVK0xBhrdC8LhLwLhhFLrIDit1VP-kZbF2PQXi4pVwRQNRVdVDoSx2FDf6EVlKc6TzpyRJhkrXrxtThAmi4kv8_-58V5L2BFTqrwGzyaaZaXU_vWqHyQzM_1p0Nzw0KrKHIZQsvjQ_md-_TEgK6e0_cUg7h43AOygbek3GfMn9X3gzVA4WVuWa3BcUtlAGZreBDLCwqmaGwWNeiE--P2bHJN-L5Gwg5SFYpIw0VN0nEKXvGuQQH6NPZYQwcDLaclkJNw3C2BtS1FE3XJfrQFMg8Q_XDqfB7VvKib2AYK8_5Ss5Iz81WlpSwvRyVBdTe6F0zf1pJTfnmzGfKHACKyL4Y09t16EmBrek2Zbc6_y-EP4QD3CJV-5GtDpVn7o6t14YEffSZL-dQ3gixShl91wIf4WSnytCiuZm-dqQD2eVkOP613m18UKQ4a36euW_I3lPm_72y5LqUg5Zcx10DvnsFBcBIhuOGZOKUvfFkINwMUEfNQzYME_okAfBtB1NMy3u8z--H9-a36Kgq7zt0Cy4JD-S2E0YyNTMx_BgnsuyLbklKVzRMfXHhy68Y10gRHPAfUPkxIAdaIjF3olgQrQH16tgg-6morpe-g0iEtug4XXFZjwljIiT86UJiF2haJnA4ORb16VOzlOUXEV3g7ZXsNRFm61F0HDsn34Ii3n-guRJBEgprqp91R7XS8i_qOXfAJ6_0ycijSOEBl7WK8uLhfPQY698UtJhvpLtmtzP8oLxpR0tfeV0i7T-VQOA-d1g-De6XF0p53e2DHdEWDZvy6uqaARC52VR-uNsVljeVeuI6SX3LIUQ6U4wYUTy2pBjL4N0MyhrrYG9PA3lihC9yKZyUrRO_AhsMs6bLgmDgUaCI718y70GyoWIGk6TviCGUImCsLBT4FncJpwHHJCAr92J5-nIjEWJLk_UW2PcZOtQCFvTFnXbtab1eZLAAxYHDHtVNix1HOLS6QzzUPolYpntE2y4LTHyMicUjMQOApBkmlfgld4QaZPlqpB76TQPQTQswE5Lzc1KM6h5R_hlINhChWilGd995xf9KYvPMofKT3yuSBU8kZLBNBOXW_T2WWPrOjEL4rGZbZw68sG0YEvN3AoISvGMeTyO0uoBI9Ptvo1EDWz8TJ1wTzRUFUqaKTvAFLphIVuLxJFW8FW4QejLE63DEqJEm8kaZBT2Lp6i0PdITvDG13hEAzfvchhLHq1NsSXc8CgP8KeBDeaTLCOHs1UfHIcFsGabTMkMKpooW7_2hjzqamMhT4RSDjswoaY6DGEZfJS7StAnQA0GOcM0vONMg3nJDqL_PPtDDhccYiDVoxPeEloonb-jnFsSarokclQPbs6HifZP6fsfH9UXOxKz9BUlzSBl6eDb8b5B3x6jFe2sFCWCuZ5a-DFbxVpy-i3jeua4rznyi91hWDGgaquV5zXOzxpF3ZaszqRqmBmkpRhoRCLRF4MAFIl5YMNhgJdfbouwhnW7FDICJC3miJXCQ2lebXvhGZMkWR2JYWXE9-j3EKv-6vJO4XbxFsfuVruHoA4vC_XRaSeQClnHZBgWxvVeVwLRYfXjB1OeRrhb4xaNN_6MDLJvHlnfrgbfaJAAH8VhKIPo5w4EVc_UkRZ5WUDzrEzngU3HnPLQKsSRvYPVSYPvxdSvYUPAHpx3BK57S_AcGIjc7S0ILNUEbJXi49XVTMb411iEDk7_lQ2J-A1XdUofeT13yAVjXbY9vGTh-yh3VY-FYJyHn39ckm10kIlNpQYLC1xrTJZtOLys8bOW7tJkQmMT1uXJofi0vc0CrNsUOV_RPQfn892DK0l4riaVYZrE8cAl8Ac8ThtKev3cVHwNA_C_Sxol612ZPw1GrNbCRriRG2ZNob0C10L8qce8g6Yi4dKm8E2__iR1JqnJ6wsdS3u2QbevjxvPsYsxNv77GmyXsbL9M15jqCXDJlYYwuFXnhtvfAeMDjoK7vCJna4OxMX9rL5AwmKH6-z-s0tKImzhZFrjkWskIzABmXbY2RztStW_t9E8lgV4qSlaOO0X9rtyx9ExJqHUYcM9Db-_7Z9vl7LCn3TkplgP_hIc6aX5LrGbrD3ek-yPPbjAq42xDCYbKK1jaJLeW9Udbt2cMDTBPkUzT7jlMo8EMjKFCiQp5ynjp-m0ZZkmpUpHOoy2OY48xMlordk_BY6HuUzECzoLKiYjDqwui2Re8Ab0PwJegM2myMUV3CmAIxdo1kb8aYKo6L4scIA6y75GEwxtBWjTA3LkYB0l5Ab-uNDi4Xkg5p9U6QN5Kq9_hCb3hxMRYiltm1wfb5SjfT6ZA_nLsGF2N05C-YOfqmppHaBDWLxiXIjCbC9GADedC-TH9Q9sQwWQ8x6wcQWENOkLtT6Cdade9dA7P1rI32CzOpMvMADHm83DgM1SliT4ZoVNO2R2xYHunoRrRcfvqjZKO4khv3bfZzR4YIAEYdWdPV2cn3Gszkqz8bUA8CvQwawwibXmBFvcbkZ-2ylFAd_ZpW8NRgroxnkMLRT5h9KYw0RirTF7eypOLNheyD6Julb9lo8u2cQ2PcaUeqFcXsas7tjU-c3yuDgcVuo3JHXzyXcHef0zMi_vA4nZDbfLWg9oqSnivRuJ4sSVIlcb6S_ZEjVH_DaGdINHom2DtjkEpC2_GvnFK09bpNQfGfWz9Wggv8VGuiEJy3bxQH0jNpR9A1FpkWKoXJbKHP9LEIM5gogA138Y6u9dQ2tg8CyXmOjYEu58tTxIcDucTtPLAGbMEjegtyLuxAoCg3vz-HIyJvBhuL3HaDkxJmafvyZz3Ahv3JlzrMJel8TrzofxUWgs4IjXmoETSbN2Y8YhFmNuClXMDre0FZ4pcB7JW4ys2cWtsIIwcyroG_xd5XOGG_VLDG1R4YCFsZbxgjs82olNEI-qGA9n_00LVch8pXTqPHP8Pw6nNiBgek8qjDEd9ON7S3q7DkYqHjOSbPsFLJWz_Vejuf9GPNZQCqHYUafZSPc1krtETreauN4OAyCUJ-LW2WVsbqK4_D_1CL--atJhxqQQrK48jl-RPPTC_0bPM0ph3KEZ1bWW3dh9xRx763TVw4It-oYrF9WU8BSn43HDtcSGtE8QLHaFoXxLIfbf3RC7V-dfTFr14InB31rCc2ybmZPR-TgEcC3mNajmJVIS51geUNuBRuw-dLtQSAcFzqrcu6J7BjSRF0viSG13-6ObAMM5ugFGukGje-pQgMv5mHK1Z0p1RKl7GD6gjGXTvZl4fGpjAqJuzfr8kOQi1nv01Hv84NnmpR4D6EpO6PGk28jnRF908xwxJa7FzA-_6CyrqYKuGX2e4uF0RHFdlHYgGUVfZThh5kTBwlF6uors9jlmuiH6gaoJUhcHuQHoFMf1ZeWc-ZZO3HczJb2N44gWhMWxo38SjSjBf9LoaDeOp7mvKW9huj6YHaNLiz5rwXdTTwqswqdU_9qBq4zZGJAjmW0S_g4pLPLQdhrpbp8YFBEOh1OeizyHLwgWoQEvf8ZFWeBT_brmvat6pRbZ1ZTuYiL9fWJhwTOY3T55RE7JEBBdtOf8wHzUHaNyI-BZPVbBRu5BPGeErpINkcHopWGfmUMHdyDO9u5risNdGiqxl0IjrKYghMKDIa-gW1mQtFCQWGLA992pkIapZ3Squp6K9lH2Hoyy1ODiJ-t-gFCA44QfkXgWQyIRv2BZap-0BlR0TcGM9W8z8giO725Pj8fmmPnX0rKP1KIdi_UYFH0UwG5XKDugePTtOxE6RxpLL2oTkxrNZP7FsCStMu03jmt9c8LQm2mAb5Bu0NAV-mvJAa26uGNrpVs-aieO7Eq-YtqXnBa5PLuvUaR4dNQ-wnmpkAmtqbt4ALEaut7A5Kb-soZEOOx0i5suMEHJVH1rcYTHtSp7awNsUbu8az1-ZV4qMSx87QHy7sHCeZHNtgPT4Ar6AbMwzFaKUkrLVL4JsE0ulFY61mED2MhrmxD5AgyXOUp7hQUI7olhhFCeth3eleOeo8C5zEqVTaJ5k7dTiv4Zuq6y_68wgIbF8vgobfFK18iqXC3UMh3P3FbA1y2WivO4GUj4KfhDWWlPkoJ5Zo5wN9L-_OMRAUVZbOGwSXRFMW7ONlyPFPBJJj69oeiRF1xpUPqkjNqH0Q81NInHishyIYy95h2LIVg_VxHWJq0ttE4e_UY3aDPGRec90Fg0ue9YF9-LrRCPsKBCVEFIjO4NIh07nFigU-cE0hMTnAwgdJVJ55Pb92-hizjdpgMXyLDZ4ut7zAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA0PERYc~WyJFRUFTNGFoVXhYRmxkX1JraWttQzB3IiwiYWdlX292ZXJfMTgiLHRydWVd~eyJhbGciOiJNTC1EU0EtNjUiLCJ0eXAiOiJrYitqd3QifQ.eyJpYXQiOjE3OTIzMDI1NTIsImF1ZCI6Imh0dHBzOi8vdmVyaWZpZXIuZXhhbXBsZSIsIm5vbmNlIjoibi04NzlmYzQxN2UzMzFiYzgwIiwic2RfaGFzaCI6IkIxYnh1LW5uNXRia0FuV2U0cF8zYkNEWEF1VGpsZDBXVldaQWZ2NWhlLUkifQ.A1HrZsE1X-F7fTZGauV-cVRjQt2fMo2CUKV2Iv95uwYP8SEp2dLsiiO0BItZaT0Fkan-J9O6K5JmXbHJlCr3rX-1Hhh4HyMCl_aKDGpgQGLqkI1BitaIxJ0NaM6OQtzBMLRiN-2Rb7M0wVLKazVGIeLxUvEsBCyAh_biD_NXvj6SJ8ka5Q3Kx2op_5jTlrVPUBgKPN0kBYSQuRbVwQsMvnHysocxYipzjNP7RKFgHnTqnn8MdoUC1Uy6eahfOJGq2f_68LhKTR0YNQT7ege7XJnQusZ20ad9X6WKKYVo2eIDm9BJGmn9dT62z4m10hZCm-oobtSO3ZGyl_n-eIw1NFYVTGjG-aXJVzDA51dU24yLkYY4E3XZ--Ha5sBkCNw4adc4nay-4SqeTYbLcUR2uCja5e8ZWV17Z2muZkaiH4scWPPrn8ZC0Qgx4hNPpmTxrnuYrgW7l4JqQGZas70lFS5ytrqTemSq8oh8vEtCa1GYIKETM-e2ioRe6SbySB2uokesThIhrRMz1IEqhHu02gl8SQEDh9KFazpVSLFzT80eZ06OD9DuL5gcwk-6ATLfDMT4LfRgRAx9gQbKxosJM7-kQh_rZQ8ooIQXX9jioQdl4vuaA_sRq-W_wFSlwfWVD9YGaFmvxLqpfbVdZiZo9A_2ENpy0DNAf6uqInJc5eX3vPwvl46AxVNK7t56mrn-MyQz0nQt4d4Ut9y1lJt80BsOLPcQBsZf9wvlXcZ4E0--Ry4f2sm6p5xHXOVvIeDGOvkYXzx3QfPWlHNo5RuXkeUwO8OzUaGBLTOzY1aUAU4PYyiZwkHlh2T_gVdhh5i1egmlSYL7rimmI_gFXLadYxY1KclKY9l2IKEhfFLOlfPcRSkOQmcYU2tocQuKG9nU1lZBOfPP4shc5kMb8B173SprmWbmNXrmZdWZZwp176TX-DZj60XJjIpzHQS6Cey49UyKifWlBV7g7G19KnGihZnud_bxMKKnfb0mhfqZZJ3UJWzDeCOcBXNhftQ1AUf1DKNBE5MxYl_3z4f7LEQTUlWNK-nSxOfgpmwZpFDv8pWQYBBRwmXVb6IlM23g-kQnd5vZohn2W9RYCl8m5cHNLC8RFClrZbBYvb24oICOOLYBtdCo-3QPpZypG6hZroDjrztd5pVMzf8Xu8ANx64U9_LpY4M6SC59pVO_keSk7ZnAjnbGvXYFsp-9IrfLWi-VL7S_WIgnr-Em3nI1u_iPpksRtY_-AsDNBVis2U6D7Skju0Cf3wPN-625PGNKAVeZXjqByRljuMRv3tUmfxNSX2AkBTf9Ua4jYNWUst-KLnJdR2MTpqsbVivscgHAYXE3jCcTVco67G3qrNW8VrvDdYXVK-6Mq_LAnls8beXbsCg1hh8jqHunjp1k9FQd2h7tQoj0Ai-FhN_vtOJaeQAnUgEN9VXsiOQWsKaeH5f3IV4SmAXbhIQYimMQSHH8hOTNsUJYkorJ9XYoEl7VPqwpyp7MElAyIbQKePAYzanpAEErKfHr1ADXtx3iuVuowg9dPSXXyZj4_Pe0Em0fLKvv8rDTYE8mRCHBfwbuQSA8DVjJSlH5PubIV5r9SK7HVJ_APnHAtRs6FsZgvyL-SagP_SYrZwtNgiXTpFTYtdmy1M5G3Y1GEkk0ZaorTPBtpj5HFgPg-FkTeBYDlMnD5sqm3pns9N9q_22NGAPxt_cMhezny4eoNXPzokJy73Y03PWCtetcmkdDT-Zv06GcQwA4FlHtS3mkVBFueaeG_dBAYsZmfWslQmFyxHH3bN1TZOshNsurkXU05pgCDzpoCMZxfz8rkkjbCnljNO-iwOxHz2EBbjRWOjNPUp2WjRa03MMAN71AhlMso78kBPJzoxgPSMvAlq92njXoByD5ODhHd0W8TBS7LbEreBk1wfouA1OAfgFUY3AlSmgcQN_fYjju4cnq7gn1SLqFC2Shnf_zgBC0Bu55GPVk_lf5hilER2kpQjOvs3A6v-uXebKLkQJLLG_28a9CS6S3ra9i9DPr-f9b8iVTGrA4NmLLvfFySx4uGzXGmTQ_yritOx0yri6dJvAnqm0Xce50Zhx7cocJ2ij5sbSpo4YuoLZBQbFE1-ykbIUjK2CVkdkVTYC2cQR4I121XB02kt6z303erUc89eqIxb0ouQYvU_53XElm7X4Eo1UGgEfp1urLgWv_-3_qiUKzoGB4vN0xrnmlg6TK7hRWMecjWGz3b3Pn6KiSIawGeGNDd5RtkeFzkv9Td5svPaERZtGpskGc6RCeIBGz5GQBtSkWeJc1gkE9w5EyhlNCewDn3YyF3ZgYIvbfmX8sL6NiyDS0O-MmlSdMwuKvxrHchA0jYUimEvoWPDFFFFqDfswHODs9Ae06HRIH23su1dQuiD7kR0Yc-U4a-jUS6f4qIbDuMOMnrsZ6uWlr8u8IREMrVcu9qbmx4vdSB6DTgsAlCeV5alnc5WJZqP6Qy3e0wir7AMDcYrMp5AGyhTLdNIASQdUrkPrzpdmsTehd3JEl-VN94pFAWOKz0JhmNgbQhgD6nsFCbsxmb5S_Lik2meb9CgZwZlIt_r6WIzFEFaLlobUFKo3eh2kblhsxFZWyeUi6glWmhICNq_yJf4jgg9hzAFASPdSolc8F77VO2QzCqSSQOmkpf0w2NrwKVFeiKZf83tk4nQyYu1jj4IkNjgPbibd92wCjiFiu7_J5Igy7cOMDpjy0kjujc2jkCyMm_mGfDyjwfn4SbR5u8hS3rkSnR6nCAzh0j118lQ4hFEcwDH2MBfecLsUjEuYj_gh73t2GVK3PxSMdVRmSUy2dfd0Vbqb41xuIOqnr0k11dJP9qg812GaQzdWgAA_bxVRacJ0xO8OPvc2MoralPuw8fWXnH6J8mbXEqV-n5wfQCZz8EYrCZimtFbg29G2tmFfdJLsIYOvVMPlbWdzoRH1elqnH_OGJN07SMzBKWts3divUoxI65dUEy_gh4v9ZVBfiLreTNeYvVTn_yjACuXCkmWGG6mkf7AosBV4004c4e6Jwd9uAELLVzlILZ5ZOzECTjrOd1ZT11oUIW7vJYKuCgqegXDEpIB6abmyHbYsNE6rGU9ZEbixQ2d-x8V-_ac-MBGmn1I6zJcPXkCUZW9sdqlmdbad4AYuJ4f0ts0KFmwimpOgCeN2RP4aR6AMHmVXqlB-pP7-OF8_Wohe9F3WfLszYE3z5IW8tMqt-zCu17GoY3orqSJSXSytOa5fz-hCATslXiq7taFOPX4nR5iwFd6bKie-QZ24skWHJSjWH9P5uIK2fxaf43HZJMoMIvPSXnFHXBogtRNm3GMs4xQA2bOoDxQTHCE7TwAXbeUqS-KlhWtH8jwD_7ODQ3D8Mn3vNkdNDdCmoHxjFzdrK-lN67S__yvwYDi-LTM3K06LcApkHwPCJ52r51bnyQDgaUxzrsQTmI9l3Kky3TIXFu6gBAmFJ7fvc5D7s4-IXlY_kNi0tXFgI7B7dyG3eieEGmKBPvPuCu9q33bzM1EjWxeBViukXwjuiDEdSlGSN_vQaLD2MKbTyCD7GRbyQ0wITH_DqYpmkjcKp_UDT0BUgvvUGZunwg7zeTEDh6gzVWKj56JViUwdQM-KR99EtaIPJ2NFKqqyyxfVuWg6efxLzphSONvwE-bpVOVw1BNYgPFuejvSRtl3QmkwIZRsGP8Fvn1JCsvdbjqvG9kVJK4sAOTrDDII8zFq7aXDcpwEm54fQp1tlsP96G_xNqCjnCFfDz-ln-6ET7BfQFaBmcI06OkUxFWfC2KVLxhzN3n926oMDhdUhXxYTc-LZg8ZvBo3Hbuz2PE_7h2Pa4BjX3CeTSINStbFRrwY_wFDy3TYouROYDUbYToB1pSQipPRmirIunlaah6cqNOVBT7Ga9weG2XeakoAF3R8xEGwCToVZaRPXaYq172q6NAhrqAic461aJxlZMJZwf15LNr3NilCQ2lGfLpswyw1vp9WwxyidoQRrK9ZeMH7PGLBVOxAj_5KAN1xqqYp1Xofj8dCfjFjbifYi28w-VAQ9ylIljiwpCnpWRBQWMgMUmL3f6cAOQkZCKlO7g9FkDBchf_a9wsPa4CwTlwD9suLwQrxoBEBB4S_4gDU7cZbi8vIjiBpHDXpmlDEniVrKgcYKqpU74VfgGm7J2hVAoRHydjzlFhTkYkjJ9NKtLdQkU0XZmj_l1yI4WdlO92akBWQKaGKa4W5CS2XwIO4tFxYL7nkIeXQt9_F7yRgLsWN_vW2wuI2DVN6VkrbU2d8XPJ9bfcIOd50mUS0O1S9ftP_q_kbJJq1CtUFDXx2i2R8k9DpbL71f01vPzac8SFVXcSYvSJaio8zi9v4CJCpYWYet-wMHNTpLpbq-BkBrvtBrcH6YAAAAAAAAAAAAAAAAAAAABQ8XHyQo", + "issuerKey": { + "alg": "ed25519+ml-dsa-65", + "ed25519": "MCowBQYDK2VwAyEAhca9rLOG6cMHfk2weCWFz7wily7iDR/Qvhhs6tTQGjA=", + "mldsa65": "MIIHsjALBglghkgBZQMEAxIDggehAOk5+trXXL8kl3d/8xVbMH33e7FIvN4r0xAKE+iOsC2LUz7ZPSz7CDh9kh+6cekIKGQNLBuedGbAAygXRx2nG4X1kiuUUkQREX72SIkYUYvag2s3Qk8eLd2MbH5IrkWNyiJRGOSs6tBzJBsnBnOY9tiJYwNsXOwopFLjvf29SzjX0gAep4COZiLNpnx4TcSBGhIxuywWbGeAG+XOkL/UgjUN8SbRobhtXgifgmKddAxlaYXpmSyyQE/jpC+3yFprzIbyDGSsRzDAlOrxRYt7VyHzFTOqGHMNqCZlnE9B6tFxT/Fnd6gyG5JSdHohYYyhC2VMxAoGAhspeRPtYHeD5YTX/aa189p6XBsKF79R8MH9fLloMwY3JkrA8BZeJ8hdHko3Z/HU7DoGA95JMn7s1KxegZ9HHOBRy5vT4wk8od12jHHHiVIy5ohBTkQAd6oT1WPKRdOQF+Wf+SGkyQXwpLhfBkZhlkIgfp/inyep8arKvqqAnC98H2fkzfsSqh4p3bdhvtxkcQy/9P9mPDlcIS/zKbr95Audq8QmZmV5iVOlN7Qsp58WJ7MU1/qaqAG36bjT9giuz+CPOZYzhkt4bABzb6u28nK9hDUK/wyK57DtKfPwL12aOD8QZf6PkK07QLG+ojWzxn1DwSskUwQ5ffW+HooiyvrYoDJwA+aBanRWFuZKWAth4aIaBsq8kIH/26nJmYAcBkVIcNqjsEwKsW8JfJgD+xxsV4r+S92RH7kX/kv695+F1p9xNAFdiJK7XisWPrTfjdA478UG3cyRfZHyexjCA6yyiVbRR6XDibNK2Tdy1t0Qv2ibf9ytXY14snCkIX1OWKzwr6a0GHDEWxCl0UHFFhtdDbrYySQvgmyUzWL0FQKpZy18tyWQzrohGhi17D/tgzky9dFGIePdD1Ew8DQ0ShXXDe5F/M1Onc0D88D680AiZWrpfe+RbdkgIHp2M+Gzhx1cdahXAOvB5UwzdXkwjo1IK6H2S8qfpgk2SiijKYW/3vrtC8ieaEkRs/VdmpYYwd23AxP6vX10+2owV+QXHWuseO9uyaTeUiDWry4+0sNffiEwtue2Fe7l8bc4NRDt/9IHF7AyHckQirHz4wg1qSS+RnNvkyVyu/bEzn4EEnlKfE1trv6n8hWxhUg0vk09uNSIeKhhhfywBd2FRrd13zlY2vNW33iXVZkWPTbF7lIt7VfIFaXgLeg8UZyVS0Bt97hvdnyBGcrsGk5OBYgsvlTm2qbudukalIjfzauiKKb7tYJv67NiG4R1r3dxQJj1DtfB9N3a2DlUcq36ZZj76LWdwMjtvuPVkwKmVi72wsNmnu7jt8iaSF/7h8jgXFRvUGTav5GeLlcY+yV6yO36K3prrgU6X0rHw+9kHlFB8x5M3pDirh53CAPXv+p7HP8c5dciYp7yraT21HQKu8goyYySunHSl0z/sRH0eVJV+fr6v9uRvoC/SA6Xp+phtSQW8UPAZNbhD1nLpW2EiG7LbzX6Ps236+SRENpa167yxG8WxArvFzq6G0vxMzm9hY4A1tGRINy5xKXeXRlrxyIU/zARWtkflco5yH1u5gZf5YHT+E/LsNv8j9/2z2nZB2UbM6IenNuvlWU/rQjN3+EXC+ccrvNt8+MDDnzuNzlNDPQmeNq0/6+lHUAzNB9Tcw3tsCr5dsWAAcq8a8JNr09j2vcEYR4X6u5p4cLZIYP8N9fTsq1/+fQqvgVaCZTDodzlgyCNalCnJ2TgyAdy8SE/XPdJcvWgPRR2y3VMrXi/o9q/oPJQRbxDYZwCFbSM94T8Y+XwCM8l4ubTKWS4ITu2DNtQIvXvQH+AU6SJypKPhGCzwGbeSCoS20qrwIu0LfwCBz3xEkpVioq2/9Y/zBr7yzvZpe6pdqw0hzx9mvZl7xEP4roD7quo8Ktl7iGiPgL6lN2rTPQgIKb2A8Zp/azLh89QHd6PlJmboAseNU5YiGZQ0JPUBKWz2MfuhYlx9BsUvKTOO4Jfi5kKQ4Ujs5lLMWTdTAcrax/5+BC6Dadv1b2N6Las/0Hoq7oIQNscbd4GBKXqyHAv2nRNxrolEdspjglpAt6MGaYvZ3onZ1uscWDKA20al/uLMtR3jBs5IMcR+H27a/ExRFazDaKt6+JrYA8OKGhpNia2DMCjRga/zvwmc4jECh0r1A6R9wWyhzdmYIsmOO0Y8gxoxtK30c2NUW3BQ45aucum0qENXQuGuwMeKqm9dWIw55RCwbld9wxnFK25RMa80oeU5kvSWKfQ3nrUAPJYDMqo8SkKcNlSslY6EdOP32H8bU4yHH0nHMwchv3czgaQrACyZmx8K7Qpe9Jx8oC5rwOf9B5e3gfZG690ck99vQP1/x5MPmkjyVLUtpvuaLZtzyviv6V6BpLxsGKKCB0M6YOymczvl6gozBAnWnIup2qyuOuMKkMRJOGiTFAyH37Dk/a8QFmupZe0FN9/7vi3vC8diL3zYKYUx/6DlWScTB9mpvf9L/hj00qVebCQu7MFEjb9qy16N0ofpz+S41L3I2AsqTEtIPpRdxO6CwMHqobXmiPrCmmrxys4AUg6X8BoGQtJK39SdgUS291C9cF4EAm7LQWw" + }, + "audience": "https://verifier.example", + "nonce": "n-879fc417e331bc80", + "now": "2026-10-18T05:50:12.000Z", + "expectedIssuer": "https://issuer.example" + }, + "expected": { + "verdict": "VALID", + "failed": [], + "levels": { + "integrity": "PASSED", + "authenticity": "PASSED", + "anchoring": "NOT_MEASURED", + "finality": "NOT_MEASURED" + } + } +} diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.alg-none.json b/conformance/vectori/sdjwt-presentation/aere-issued.alg-none.json index c309683..7e1718d 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.alg-none.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.alg-none.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.claim-conflict.json b/conformance/vectori/sdjwt-presentation/aere-issued.claim-conflict.json index b83808e..d42b34b 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.claim-conflict.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.claim-conflict.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.crit.json b/conformance/vectori/sdjwt-presentation/aere-issued.crit.json index c230573..6c0806f 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.crit.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.crit.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.digest-twice.json b/conformance/vectori/sdjwt-presentation/aere-issued.digest-twice.json index 2dee2df..4cba21d 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.digest-twice.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.digest-twice.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form-not-json.json b/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form-not-json.json index 0846b0a..5ea2d25 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form-not-json.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form-not-json.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form-sd.json b/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form-sd.json index 121ccdd..fa59be7 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form-sd.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form-sd.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form.json b/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form.json index 6b15656..a34250e 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-form.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-name-proto.json b/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-name-proto.json index 46cbea4..12cd39d 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-name-proto.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-name-proto.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-name.json b/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-name.json index 2839210..e1db497 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-name.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.disclosure-name.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.key-confusion.json b/conformance/vectori/sdjwt-presentation/aere-issued.key-confusion.json index ee167e4..b4ed7ea 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.key-confusion.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.key-confusion.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.sd-alg-nested.json b/conformance/vectori/sdjwt-presentation/aere-issued.sd-alg-nested.json index f98f9be..e7cef38 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.sd-alg-nested.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.sd-alg-nested.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.sd-alg.json b/conformance/vectori/sdjwt-presentation/aere-issued.sd-alg.json index b9d2d7c..874c792 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.sd-alg.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.sd-alg.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.time-no-exp.json b/conformance/vectori/sdjwt-presentation/aere-issued.time-no-exp.json index 38eb350..d64f8d0 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.time-no-exp.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.time-no-exp.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.time-years-later.json b/conformance/vectori/sdjwt-presentation/aere-issued.time-years-later.json index 2fc9033..9dea60e 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.time-years-later.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.time-years-later.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.typ.json b/conformance/vectori/sdjwt-presentation/aere-issued.typ.json index e6c4252..221d8df 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.typ.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.typ.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-issued.valid.json b/conformance/vectori/sdjwt-presentation/aere-issued.valid.json index 37d783b..15c357f 100644 --- a/conformance/vectori/sdjwt-presentation/aere-issued.valid.json +++ b/conformance/vectori/sdjwt-presentation/aere-issued.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere-ml-dsa-65.valid.json b/conformance/vectori/sdjwt-presentation/aere-ml-dsa-65.valid.json index 856241c..8ce3972 100644 --- a/conformance/vectori/sdjwt-presentation/aere-ml-dsa-65.valid.json +++ b/conformance/vectori/sdjwt-presentation/aere-ml-dsa-65.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere.kb-claims.json b/conformance/vectori/sdjwt-presentation/aere.kb-claims.json index 982aefe..a4b69a6 100644 --- a/conformance/vectori/sdjwt-presentation/aere.kb-claims.json +++ b/conformance/vectori/sdjwt-presentation/aere.kb-claims.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere.kb-cnf.json b/conformance/vectori/sdjwt-presentation/aere.kb-cnf.json index 7803bcc..ccfb380 100644 --- a/conformance/vectori/sdjwt-presentation/aere.kb-cnf.json +++ b/conformance/vectori/sdjwt-presentation/aere.kb-cnf.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere.kb-freshness-exp.json b/conformance/vectori/sdjwt-presentation/aere.kb-freshness-exp.json index 46de945..a63c225 100644 --- a/conformance/vectori/sdjwt-presentation/aere.kb-freshness-exp.json +++ b/conformance/vectori/sdjwt-presentation/aere.kb-freshness-exp.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere.signature-kb.json b/conformance/vectori/sdjwt-presentation/aere.signature-kb.json index 5bc8847..305e03d 100644 --- a/conformance/vectori/sdjwt-presentation/aere.signature-kb.json +++ b/conformance/vectori/sdjwt-presentation/aere.signature-kb.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere.time-expired.json b/conformance/vectori/sdjwt-presentation/aere.time-expired.json index 049617e..d119429 100644 --- a/conformance/vectori/sdjwt-presentation/aere.time-expired.json +++ b/conformance/vectori/sdjwt-presentation/aere.time-expired.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere.time-not-yet.json b/conformance/vectori/sdjwt-presentation/aere.time-not-yet.json index 3fc703f..7433f70 100644 --- a/conformance/vectori/sdjwt-presentation/aere.time-not-yet.json +++ b/conformance/vectori/sdjwt-presentation/aere.time-not-yet.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere.typ-kb.json b/conformance/vectori/sdjwt-presentation/aere.typ-kb.json index fb7580d..e238a20 100644 --- a/conformance/vectori/sdjwt-presentation/aere.typ-kb.json +++ b/conformance/vectori/sdjwt-presentation/aere.typ-kb.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/aere.valid.json b/conformance/vectori/sdjwt-presentation/aere.valid.json index 390ce53..399ffa7 100644 --- a/conformance/vectori/sdjwt-presentation/aere.valid.json +++ b/conformance/vectori/sdjwt-presentation/aere.valid.json @@ -3,7 +3,7 @@ "kind": "aere-conformance-vector", "id": "sdjwt-presentation/aere.valid", "artifact": "sdjwt-presentation", - "title": "an SD-JWT issued with AERE keys (EdDSA): two of three disclosable claims and one list element shown, two decoy digests, a key binding JWT for this verifier", + "title": "an SD-JWT issued with AERE keys (EdDSA), judged with the Ed25519 half given as a plain JWK (classical interoperation: the verdict is not post-quantum): two of three disclosable claims and one list element shown, two decoy digests, a key binding JWT for this verifier", "source": "producers (sdjwt.mjs: issueSdJwt, presentSdJwt), three hybrid key pairs made and discarded at generation", "requires": [], "exercises": [ @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901-issued.disclosure-twice.json b/conformance/vectori/sdjwt-presentation/rfc9901-issued.disclosure-twice.json index 785a8e8..9fe3819 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901-issued.disclosure-twice.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901-issued.disclosure-twice.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901-issued.form-empty.json b/conformance/vectori/sdjwt-presentation/rfc9901-issued.form-empty.json index 4ad35db..a54bab0 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901-issued.form-empty.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901-issued.form-empty.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901-issued.unreferenced.json b/conformance/vectori/sdjwt-presentation/rfc9901-issued.unreferenced.json index 45487d5..fa912a3 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901-issued.unreferenced.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901-issued.unreferenced.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901-issued.valid.json b/conformance/vectori/sdjwt-presentation/rfc9901-issued.valid.json index 4eae794..ba6064a 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901-issued.valid.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901-issued.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.form-clock.json b/conformance/vectori/sdjwt-presentation/rfc9901.form-clock.json index 1c17756..1e89961 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.form-clock.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.form-clock.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.form.json b/conformance/vectori/sdjwt-presentation/rfc9901.form.json index f6d8734..e5622c3 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.form.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.form.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.issuer.json b/conformance/vectori/sdjwt-presentation/rfc9901.issuer.json index 9938d4f..a7fe7a9 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.issuer.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.issuer.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.kb-audience.json b/conformance/vectori/sdjwt-presentation/rfc9901.kb-audience.json index 569bd06..c4caeff 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.kb-audience.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.kb-audience.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.kb-context.json b/conformance/vectori/sdjwt-presentation/rfc9901.kb-context.json index 248ffac..9be49da 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.kb-context.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.kb-context.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.kb-freshness.json b/conformance/vectori/sdjwt-presentation/rfc9901.kb-freshness.json index 7b6d1c5..e60c5ff 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.kb-freshness.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.kb-freshness.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.kb-nonce.json b/conformance/vectori/sdjwt-presentation/rfc9901.kb-nonce.json index 632e7bf..77317b6 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.kb-nonce.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.kb-nonce.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.kb-required.json b/conformance/vectori/sdjwt-presentation/rfc9901.kb-required.json index 882d0ea..6d44104 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.kb-required.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.kb-required.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.kb-sd-hash.json b/conformance/vectori/sdjwt-presentation/rfc9901.kb-sd-hash.json index 296e152..d4e3fc2 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.kb-sd-hash.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.kb-sd-hash.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.key.json b/conformance/vectori/sdjwt-presentation/rfc9901.key.json index 5e9e198..39796b7 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.key.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.key.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.signature.json b/conformance/vectori/sdjwt-presentation/rfc9901.signature.json index a00205c..6277165 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.signature.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.signature.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.typ.json b/conformance/vectori/sdjwt-presentation/rfc9901.typ.json index 0410151..8e41964 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.typ.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.typ.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/rfc9901.valid.json b/conformance/vectori/sdjwt-presentation/rfc9901.valid.json index 7e01197..c91904d 100644 --- a/conformance/vectori/sdjwt-presentation/rfc9901.valid.json +++ b/conformance/vectori/sdjwt-presentation/rfc9901.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core-es256.valid.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core-es256.valid.json index 25f9775..9308edd 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core-es256.valid.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core-es256.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core-ml-dsa-65.valid.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core-ml-dsa-65.valid.json index 2d65d4c..e9c7d52 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core-ml-dsa-65.valid.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core-ml-dsa-65.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core-typed.valid.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core-typed.valid.json index a348727..a191033 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core-typed.valid.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core-typed.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core.alg.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core.alg.json index 548b60a..6772c31 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core.alg.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core.alg.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core.digest-twice.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core.digest-twice.json index 58b65bd..ec97508 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core.digest-twice.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core.digest-twice.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core.kb-audience.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core.kb-audience.json index e71f567..c77baf6 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core.kb-audience.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core.kb-audience.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core.kb-sd-hash.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core.kb-sd-hash.json index 26bfeef..eb0aba2 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core.kb-sd-hash.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core.kb-sd-hash.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core.sd-alg.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core.sd-alg.json index 5c48ed8..0b1d312 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core.sd-alg.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core.sd-alg.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core.signature-kb.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core.signature-kb.json index 63e1740..d81d644 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core.signature-kb.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core.signature-kb.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core.typ.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core.typ.json index 79d3fce..041e2e6 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core.typ.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core.typ.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core.unreferenced.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core.unreferenced.json index 57f539a..70f4dfb 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core.unreferenced.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core.unreferenced.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-presentation/sd-jwt-core.valid.json b/conformance/vectori/sdjwt-presentation/sd-jwt-core.valid.json index 8aa0f13..be951ad 100644 --- a/conformance/vectori/sdjwt-presentation/sd-jwt-core.valid.json +++ b/conformance/vectori/sdjwt-presentation/sd-jwt-core.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-no-status.valid.json b/conformance/vectori/sdjwt-vc-presentation/aere-no-status.valid.json index f8492dd..fd9a116 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-no-status.valid.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-no-status.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid-8-bits.valid.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid-8-bits.valid.json index 1740fbf..1059212 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid-8-bits.valid.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid-8-bits.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid-ml-dsa-65.valid.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid-ml-dsa-65.valid.json index f3075e4..b4fc5db 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid-ml-dsa-65.valid.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid-ml-dsa-65.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid-status-waived.partial.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid-status-waived.partial.json index 816ab19..180418e 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid-status-waived.partial.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid-status-waived.partial.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.disclosed-protected.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.disclosed-protected.json index 3035d58..b43db24 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.disclosed-protected.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.disclosed-protected.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-idx.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-idx.json index 56e4f40..ec503cd 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-idx.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-idx.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-integrity.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-integrity.json index 8b9eb21..46e0715 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-integrity.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-integrity.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-status.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-status.json index 2cf932f..9220bac 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-status.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.form-status.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-index.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-index.json index 0980c16..7e49f33 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-index.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-index.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-expired.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-expired.json index d68f5a1..a8bfc44 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-expired.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-expired.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-no-iat.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-no-iat.json index ba3e162..efc8b75 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-no-iat.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-no-iat.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-other-uri.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-other-uri.json index 3aae446..5f1b760 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-other-uri.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-other-uri.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-stranger.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-stranger.json index c85b73c..9995e56 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-stranger.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-stranger.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-typ.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-typ.json index b491f82..373ce70 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-typ.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-list-typ.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-revoked.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-revoked.json index c4d704d..ed41696 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-revoked.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-revoked.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-suspended.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-suspended.json index 3ee8ba0..128bb3f 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-suspended.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-suspended.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-unjudged.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-unjudged.json index cfe134f..2092a51 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-unjudged.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.status-unjudged.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.valid.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.valid.json index ac643d7..2ffbb51 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.valid.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct-expected.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct-expected.json index 8b3a5dc..978a165 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct-expected.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct-expected.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct-mismatch.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct-mismatch.json index 2ad84e1..67e3b22 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct-mismatch.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct-mismatch.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct.json b/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct.json index 821feed..d469a7b 100644 --- a/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct.json +++ b/conformance/vectori/sdjwt-vc-presentation/aere-pid.vct.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-8-bits.valid.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-8-bits.valid.json index 37c956e..b797147 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-8-bits.valid.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-8-bits.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-es256.valid.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-es256.valid.json index 0b2ef86..9a08f92 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-es256.valid.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-es256.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-ml-dsa-65.valid.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-ml-dsa-65.valid.json index 0e3e40a..7a9ad47 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-ml-dsa-65.valid.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-ml-dsa-65.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-status-waived.partial.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-status-waived.partial.json index 6c4fd16..02e81c0 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-status-waived.partial.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc-status-waived.partial.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-list-expired.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-list-expired.json index 6b568ea..239823b 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-list-expired.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-list-expired.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-list-other-uri.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-list-other-uri.json index d32c9f3..da81f7f 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-list-other-uri.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-list-other-uri.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-revoked.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-revoked.json index be5618c..7c720df 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-revoked.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-revoked.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-suspended.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-suspended.json index a782b02..c5f9491 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-suspended.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-suspended.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-unjudged.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-unjudged.json index 62e0bd2..e5460ea 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-unjudged.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.status-unjudged.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.valid.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.valid.json index 73a7cd7..5d2067a 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.valid.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.valid.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.vct-mismatch.json b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.vct-mismatch.json index 0ec20d8..b677060 100644 --- a/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.vct-mismatch.json +++ b/conformance/vectori/sdjwt-vc-presentation/sd-jwt-vc.vct-mismatch.json @@ -15,6 +15,7 @@ "sdjwt.disclosure-name", "sdjwt.disclosure-twice", "sdjwt.form", + "sdjwt.hybrid-half", "sdjwt.issuer", "sdjwt.kb-audience", "sdjwt.kb-claims", diff --git a/identity/README.md b/identity/README.md index 70a9003..52c2d9d 100644 --- a/identity/README.md +++ b/identity/README.md @@ -168,17 +168,40 @@ import { issueSdJwt, presentSdJwt, verifySdJwt } from './sdjwt.mjs'; // issuer: an issuer-signed JWT (digests of the disclosable claims in _sd, list elements as {"...": digest}, _sd_alg sha-256, the holder's // key in cnf.jwk) followed by the disclosures, joined by ~ const { sdJwt } = issueSdJwt({ issuer, holder: holderPublicJwkOrKeys, claims, disclosable: ['given_name', 'age_over_18'], - arrayDisclosable: ['nationalities'], iss: 'https://issuer.example', exp, alg: 'EdDSA' }); // or alg: 'ML-DSA-65' + arrayDisclosable: ['nationalities'], iss: 'https://issuer.example', exp }); // AERE keys: ML-DSA-65 (alg: 'EdDSA' needs classicalHalf: true) // holder: keep only the chosen disclosures and add a Key Binding JWT (typ kb+jwt, aud, nonce, iat, sd_hash) signed with the holder key // a name reveals a top-level claim; { element, in } an element of the top-level list named (only that list) const shown = presentSdJwt({ sdJwt, reveal: ['age_over_18', { element: 'RO', in: 'nationalities' }], holder, audience, nonce }); // verifier: the steps of RFC 9901 sections 7.1 and 7.3, with the issuer key the verifier chose (never one taken from the token); // its audience and nonce are required, exp is required, the token must be explicitly typed ...+sd-jwt (expectedTyp to change it) -const r = verifySdJwt(shown, { issuerKey, audience, nonce, expectedIssuer: 'https://issuer.example' }); // { valid, reason, payload, disclosed, keyBinding } +const r = verifySdJwt(shown, { issuerKey, audience, nonce, expectedIssuer: 'https://issuer.example' }); +// { valid, rule, reason, payload, disclosed, alg, postQuantum, keyBinding } ``` From the command line (the issuer key a public JWK, or the output of `pub`): `node identity-cli.mjs verify-sdjwt --sd-jwt token.txt --issuer-key issuer.pub.json ---issuer-alg EdDSA --audience A --nonce N [--expected-issuer I] [--at T] [--json]`; Aere Cloud runs it behind `POST /v1/identity/sd-jwt/verify`. +[--issuer-alg A] [--classical-half] --audience A --nonce N [--expected-issuer I] [--at T] [--json]`; Aere Cloud runs it behind +`POST /v1/identity/sd-jwt/verify` (fields `issuerAlg` and `classicalHalf`; the response carries `postQuantum`). + +**The post-quantum half of an AERE identity (since 2026-10-19; AIP-25, finding C1).** The keys of an AERE identity are an Ed25519 key +AND an ML-DSA-65 key, and its own credentials need both signatures. An SD-JWT carries one JWS signature. Until 2026-10-19 the issuer, +the holder's `cnf.jwk` and the verifier used the Ed25519 half by default, so a token signed with the issuer's Ed25519 key alone, which +someone who broke only that classical key could make, verified against the issuer's whole hybrid identity, on the command line and +on the public route alike. Since then, with AERE keys (the key pair, or the public keys `{alg, ed25519, mldsa65}`): + +- issuing, the holder key bound in `cnf.jwk`, the Key Binding JWT and the Token Status List (below) use the ML-DSA-65 half by default; +- the verifier requires the ML-DSA-65 half by default; a token signed with the Ed25519 half is refused by the rule `hybrid-half`, and + naming `issuerAlg: 'EdDSA'` alone is refused the same way; +- the Ed25519 half is used, and judged, only when the caller asks for it: `classicalHalf: true` (`--classical-half` on the command line, + `classicalHalf` on the route), for classical interoperation with a library that does not know RFC 9964; the verifier then takes the + half the token is signed with, EdDSA or ML-DSA-65; +- every verdict says `postQuantum`: true only when every signature it judged (the issuer's and the Key Binding JWT, and for an SD-JWT VC + the status list) is ML-DSA-65; false on every refusal. + +A plain JWK (a key of another issuer, ES256 or EdDSA; the tokens of `@sd-jwt/core` and of European wallets) is judged as before, since +there is no post-quantum half to lose, and the verdict says `postQuantum: false` for it too. Someone who turns AERE keys into a plain +Ed25519 JWK (`publicJwk(keys, 'EdDSA', { classicalHalf: true })`) has asked for the classical half, and the verdict says so. Tokens +issued with EdDSA by AERE identities before this date stop verifying against their hybrid keys unless the verifier asks for the +classical half. Checked against the standard's own vectors (`fixturi-rfc9901.json`, extracted from RFC 9901 Section 5 and Appendix A.5; IETF code components, Revised BSD License): the digest of each of the ten example disclosures; the example SD-JWT, signed ES256 by someone @@ -191,7 +214,8 @@ type AKP (`alg` `ML-DSA-65`, parameters `pub` and `priv`), the names and key for `draft-ietf-cose-dilithium`, which this file quoted as not yet a standard). Measured 2026-10-18 on Node.js 24.14.1: jose 6.2.12 signs and verifies ML-DSA-65 JWS through Node's WebCrypto (which prints an experimental-feature warning), its signatures and ours verify each other, and `@sd-jwt/core` 0.23.0 accepts an ML-DSA-65 SD-JWT and KB-JWT through its injected verifier; a library that does not -know RFC 9964 rejects such a token, so for it issue with EdDSA. A hybrid (two signatures in one token) would need the general JWS JSON +know RFC 9964 rejects such a token, so for it issue with EdDSA (with AERE keys, since 2026-10-19, `alg: 'EdDSA', classicalHalf: true`, +and the verdict says the token is not post-quantum). A hybrid (two signatures in one token) would need the general JWS JSON serialization and is not done here. The algorithm must match the key the verifier gives (no `none`, no algorithm confusion); a disclosure not bound to a digest, a digest seen twice, a disclosure naming `_sd`, `...`, `_sd_alg` or `__proto__`, or one that overwrites a claim, or a claim named `__proto__` anywhere, is refused; `_sd_alg` below the top level of the payload is refused (RFC @@ -223,7 +247,10 @@ rest to policy): kept, measured as our refusal of their `sha-384` token. Nested claim; the RFC's MUST NOT decided it, repaired here. The salt of their default helper, `generateSalt(16)` of `@sd-jwt/crypto-nodejs`, is 16 hexadecimal characters, 64 bits, under the 128 bits Section 9.3 recommends; ours is 16 random bytes; theirs, and the generator is injectable. Disclosures are ordered by the presentation frame there and by issuance order here; the RFC orders them not at all. -An AKP JWK without `alg` is refused by Node's `crypto` and by jose alike (RFC 9964 Section 3 requires it). +An AKP JWK without `alg` is refused by Node's `crypto` and by jose alike (RFC 9964 Section 3 requires it). These rows were measured on +2026-10-18, when the Ed25519 half of AERE keys was the default; since 2026-10-19 the interop scripts ask for it explicitly +(`classicalHalf: true`), and they have not been run again since (the foreign libraries are not installed here): not measured after +the change. Their recorded fixtures carry plain JWKs and are judged offline as before. ## SD-JWT VC (`sdjwt-vc.mjs`) @@ -238,9 +265,14 @@ const { sdJwt } = issueSdJwtVc({ issuer, holder, vct: 'urn:eudi:pid:1', iss, cla const shown = presentSdJwtVc({ sdJwt, reveal: ['birthdate'], holder, audience, nonce }); const list = issueStatusListJwt({ issuer, uri, statuses: [0, 0, 0, 0, 0, 1], bits: 1, exp }); // typ statuslist+jwt, sub = uri, iat, status_list { bits, lst } const r = verifySdJwtVc(shown, { issuerKey, audience, nonce, expectedVct: 'urn:eudi:pid:1', statusListJwt: list }); -// r: { valid, reason, payload, disclosed, keyBinding, vct, status: { present, judged, uri, idx, value, name } } +// r: { valid, reason, payload, disclosed, keyBinding, postQuantum, vct, status: { present, judged, uri, idx, value, name } } ``` +Since 2026-10-19 (AIP-25 C1) the Status List Token of an AERE issuer is signed and judged with the ML-DSA-65 half by default, as the +credential is: a list signed with the Ed25519 half alone, which could "unrevoke" a credential for someone who broke only that key, is +refused (`status-list`, naming the Ed25519 half) unless the verifier asks for the classical half (`classicalHalf`), and `postQuantum` +of a VC verdict is true only when the list judged is ML-DSA-65 too. + What the verifier requires beyond RFC 9901: the header type `dc+sd-jwt` (Section 2.2.1); a `vct` in the payload equal to one the verifier names (`expectedVct` is required: a verifier that does not say which credential type it expects would accept any); none of `iss`, `nbf`, `exp`, `cnf`, `vct`, `vct#integrity`, `aka_vcts`, `status` selectively disclosed (Section 2.2.2.3; the issuer refuses to @@ -254,7 +286,7 @@ not judged); one that names no status is judged without a list. Nothing is fetch JWT VC Issuer Metadata, `x5c`) or taken from the token. From the command line: `node identity-cli.mjs verify-sdjwt-vc --sd-jwt token.txt --issuer-key issuer.pub.json --audience A --nonce N ---expected-vct urn:eudi:pid:1 [--status-list list.txt [--status-list-key k.json]] [--status-not-required] [--expected-issuer I] +--expected-vct urn:eudi:pid:1 [--status-list list.txt [--status-list-key k.json]] [--status-not-required] [--classical-half] [--expected-issuer I] [--at T] [--json]`; 0 VALID, 1 INVALID, 2 a usage error. **Interoperability, measured 2026-10-18** (`interop/interop-sdjwt-vc.mjs`; `@sd-jwt/sd-jwt-vc` 0.23.0, `@owf/token-status-list` 0.5.0, @@ -309,13 +341,14 @@ no row failed, also when rows were not judged; a caller that needs "nothing left 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`, +SD-JWT verifier and the SD-JWT VC profile are enrolled there too: `verifySdJwt` names the rule of its refusal (`rule`, one of the 24 of +`REGULI` in `sdjwt.mjs`: `form`, `alg`, `typ`, `crit`, `key`, `hybrid-half` (since 2026-10-19), `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`), and `verifySdJwtVc` one of those or one of its own 9 (`vct-expected`, `vct`, `vct-mismatch`, `disclosed-protected`, `form`, `status-list`, `status-index`, `status-revoked`, `status-unjudged`); both stop at their -first refusal. The suite holds 54 vectors of the kind `sdjwt-presentation` (the RFC 9901 example, the tokens of `@sd-jwt/core` from the -interop fixtures, and tokens issued with AERE keys) and 32 of `sdjwt-vc-presentation` (ours with their status lists, and the credentials +first refusal. The suite holds 59 vectors of the kind `sdjwt-presentation` (the RFC 9901 example, the tokens of `@sd-jwt/core` from the +interop fixtures, and tokens issued with AERE keys; since 2026-10-19 five judged against the AERE hybrid keys, three of them refused by +`hybrid-half`) and 32 of `sdjwt-vc-presentation` (ours with their status lists, and the credentials and lists of `@sd-jwt/sd-jwt-vc` and `@owf/token-status-list`); a VC whose status was waived and not judged is `PARTIAL` there, never `VALID`. @@ -362,10 +395,10 @@ node proba-conformitate.mjs # 18: compliance policies judged on real pr node control-negativ-conformitate.mjs # on a copy, each of 17 guards removed -> its own named test turns red node proba-travel-rule.mjs # 20: 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 20 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 -node control-negativ-sdjwt.mjs # on a copy, each of 31 guards removed -> its own named test turns red -node proba-sdjwt-vc.mjs # 10: the SD-JWT VC profile, the status list, the fixtures of @sd-jwt/sd-jwt-vc and @owf/token-status-list, the command line -node control-negativ-sdjwt-vc.mjs # on a copy, each of 16 guards removed -> its own named test turns red +node proba-sdjwt.mjs # 31: the RFC 9901 vectors, SD-JWTs issued with AERE keys (ML-DSA-65 by default, EdDSA on request), each attack, the post-quantum half (AIP-25 C1), the fixtures of @sd-jwt/core +node control-negativ-sdjwt.mjs # on a copy, each of 39 guards removed -> its own named test turns red +node proba-sdjwt-vc.mjs # 11: the SD-JWT VC profile, the status list (and its post-quantum half), the fixtures of @sd-jwt/sd-jwt-vc and @owf/token-status-list, the command line +node control-negativ-sdjwt-vc.mjs # on a copy, each of 20 guards removed -> its own named test turns red node interop/interop-sdjwt.mjs --lab # the measurement itself, with the foreign libraries installed in , outside this repository node interop/interop-sdjwt-vc.mjs --lab # (npm: @sd-jwt/core, @sd-jwt/sd-jwt-vc, @sd-jwt/crypto-nodejs, jose); --scrie rewrites the fixtures ``` diff --git a/identity/control-negativ-sdjwt-vc.mjs b/identity/control-negativ-sdjwt-vc.mjs index bbf0f19..66f3473 100644 --- a/identity/control-negativ-sdjwt-vc.mjs +++ b/identity/control-negativ-sdjwt-vc.mjs @@ -24,10 +24,15 @@ const PLANTARI = [ ['impachetarea bitilor citita de la bitul cel mai semnificativ (ordinea gresita)', '(octeti[Math.floor(idx * bits / 8)] >> ((idx * bits) % 8)) & ((1 << bits) - 1)', '(octeti[Math.floor(idx * bits / 8)] >> (8 - bits - ((idx * bits) % 8))) & ((1 << bits) - 1)', 'STARE: revocat'], ['sub-ul listei nu se mai compara cu uri-ul din jeton', 'if (!(corp.sub === uri /*PAZA:status-list*/)) return nu(', 'if (false) return nu(', 'STARE: lista pentru alt uri'], ['lista expirata primita', 'if (!(corp.exp === undefined || (Number.isFinite(corp.exp) && acum < corp.exp) /*PAZA:status-list*/)) return nu(', 'if (false) return nu(', 'STARE: lista pentru alt uri'], - ['typ-ul statuslist+jwt al listei nu se mai cere', "verifyJws(jwt, publicJwk(key, alg), { typ: TYP_STATUS_LIST })", "verifyJws(jwt, publicJwk(key, alg), { typ: null })", 'STARE: lista pentru alt uri'], + ['typ-ul statuslist+jwt al listei nu se mai cere', "headerAlg: headerAlgOf(jwt) }), { typ: TYP_STATUS_LIST })", "headerAlg: headerAlgOf(jwt) }), { typ: null })", 'STARE: lista pentru alt uri'], ['iat-ul listei nu se mai cere', "if (!(Number.isFinite(corp.iat) /*PAZA:status-list*/)) return nu('iat is required", "if (false) return nu('iat is required", 'STARE: lista pentru alt uri'], ['indexul dincolo de lista citit ca 0', 'if (!(v !== null /*PAZA:status-index*/)) return refuz(', 'if (false) return refuz(', 'STARE: lista pentru alt uri'], // fixturile straine: cu impachetarea gresita, listele @owf/token-status-list pe 1 bit cu indexul 5 = 1 ar fi citite altfel + // 2026-10-19 (AIP-25 C1, D-535): lista de stare a unui emitent AERE pe jumatatea ML-DSA-65; paznicii noi si forma de dinainte + ['AIP-25 C1: lista de stare emisa implicit cu jumatatea Ed25519 (forma de dinainte)', 'const algL = alg ?? defaultAlg(issuer);', "const algL = alg ?? 'EdDSA'; classicalHalf = true;", 'AIP-25 C1: lista de stare'], + ['AIP-25 C1: lista de stare judecata implicit pe jumatatea Ed25519 (forma de dinainte)', 'judgedJwk(key, alg, { classicalHalf, headerAlg: headerAlgOf(jwt) })', "judgedJwk(key, alg ?? 'EdDSA', { classicalHalf: true, headerAlg: headerAlgOf(jwt) })", 'AIP-25 C1: lista de stare'], + ['AIP-25 C1: verdictul VC post-cuantic si cu o lista semnata Ed25519', "pqLista = l.alg === 'ML-DSA-65';", 'pqLista = true;', 'AIP-25 C1: lista de stare'], + ['AIP-25 C1: classicalHalf netransmis listei de stare', 'classicalHalf: o.classicalHalf, uri: ref.uri', 'classicalHalf: false, uri: ref.uri', 'AIP-25 C1: lista de stare'], ['fixturile straine: lista LOR revocata citita cu ordinea gresita a bitilor', 'const statusAt = (idx) => (Number.isInteger(idx) && idx >= 0 && idx < entries ?', 'const statusAt = (idx) => (false && Number.isInteger(idx) && idx >= 0 && idx < entries ?', 'INTEROP'], ]; function copie() { diff --git a/identity/control-negativ-sdjwt.mjs b/identity/control-negativ-sdjwt.mjs index 4d7207a..eacded7 100644 --- a/identity/control-negativ-sdjwt.mjs +++ b/identity/control-negativ-sdjwt.mjs @@ -13,7 +13,7 @@ import { fileURLToPath } from 'node:url'; const AICI = path.dirname(fileURLToPath(import.meta.url)); const S = 'sdjwt.mjs'; const PLANTARI = [ - // [nume, tipar, inlocuire, proba (inceputul numelui ei)] + // [nume, tipar, inlocuire, proba (inceputul numelui ei), fisierul plantat (implicit sdjwt.mjs; din 2026-10-19 si linia de comanda)] ['alg none primit', "if (!(ALGS.includes(antet.alg) /*PAZA:alg*/)) throw", "if (antet.alg === 'nimic') throw", 'ATAC: alg none'], ['algoritmul nu mai trebuie sa fie al cheii (confuzia)', 'if (!(potriveste(antet.alg, key) /*PAZA:key*/)) throw', 'if (false) throw', 'ATAC: alg none'], ['o dezvaluire nelegata de niciun digest primita', 'if (!(folosite.size === dupaDigest.size /*PAZA:unreferenced*/)) return', 'if (false) return', 'ATAC: o dezvaluire cu valoarea schimbata'], @@ -47,7 +47,16 @@ const PLANTARI = [ ['emiterea nu mai cere un typ explicit de SD-JWT', "if (!tipBun(typ, undefined)) throw new Error('sd-jwt: typ must be", "if (false) throw new Error('sd-jwt: typ must be", 'INTEROP: _sd_alg imbricat'], ['cheia publica Node nu mai e primita', "(k.type === 'public' ? k : crypto.createPublicKey(k))", 'crypto.createPublicKey(k)', 'REVIZUIRE: cheia emitentului'], ['o afirmatie __proto__ primita (prototipul schimbat)', "if (!(!Object.hasOwn(x, '__proto__') /*PAZA:disclosure-name*/)) throw", 'if (false) throw', 'ATAC: o afirmatie numita __proto__'], - ['elementele de lista nedezvaluite pastrate', 'if (!c) continue; // nedezvaluit: elementul se scoate', "if (!c) { out.push(e); continue; }", 'AERE -> SD-JWT (EdDSA'], + ['elementele de lista nedezvaluite pastrate', 'if (!c) continue; // nedezvaluit: elementul se scoate', "if (!c) { out.push(e); continue; }", 'AERE -> SD-JWT (implicit'], + // 2026-10-19 (AIP-25 C1, D-535): jumatatea clasica a unei identitati hibride; fiecare paznic nou si forma de dinainte a reparatiei + ['AIP-25 C1: jetonul doar-Ed25519 contra cheilor AERE nu mai e numit (cade pe key)', "if (!(!(a === 'ML-DSA-65' && CLASIC.includes(headerAlg)) /*PAZA:hybrid-half*/)) throw", 'if (false) throw', 'AIP-25 C1: un SD-JWT semnat NUMAI'], + ['AIP-25 C1: jumatatea Ed25519 a cheilor AERE data fara classicalHalf', 'if (!(classicalHalf === true /*PAZA:hybrid-half*/)) throw', 'if (false) throw', 'AIP-25 C1: jumatatea clasica se judeca NUMAI'], + ['AIP-25 C1: forma de dinainte: cheile AERE judecate implicit pe jumatatea Ed25519, fara cerere', 'if (!esteAere(k)) return publicJwk(k, alg);', "if (true) return publicJwk(k, alg ?? 'EdDSA', { classicalHalf: true });", 'AIP-25 C1: un SD-JWT semnat NUMAI'], + ['AIP-25 C1: postQuantum spus adevarat pentru orice semnatura a emitentului', "const pqEmitent = antet.alg === 'ML-DSA-65';", 'const pqEmitent = true;', 'AIP-25 C1: un JWK simplu ramane'], + ['AIP-25 C1: Key Binding-ul clasic nu mai coboara postQuantum', "postQuantum: rez.postQuantum && k.antet.alg === 'ML-DSA-65',", 'postQuantum: rez.postQuantum,', 'AIP-25 C1: un JWK simplu ramane'], + ['AIP-25 C1: emiterea implicita cu chei AERE pe Ed25519 (forma de dinainte)', 'const algE = alg ?? defaultAlg(issuer), algH = holderAlg ?? defaultAlg(holder);', "const algE = alg ?? 'EdDSA', algH = holderAlg ?? 'EdDSA'; classicalHalf = true;", 'AIP-25 C1: emiterea implicita'], + ['AIP-25 C1: linia de comanda ignora --classical-half', "const classicalHalf = are('--classical-half');", 'const classicalHalf = false;', 'linia de comanda AIP-25 C1', 'identity-cli.mjs'], + ['AIP-25 C1: linia de comanda cere implicit EdDSA (forma de dinainte)', "issuerAlg: algDat ?? (aere(cheie) ? undefined : 'EdDSA'),", "issuerAlg: algDat ?? 'EdDSA',", 'linia de comanda AIP-25 C1', 'identity-cli.mjs'], ]; function copie() { const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-sdjwt-ctl-')); @@ -64,10 +73,10 @@ function ruleaza(t) { c.on('close', (cod) => { clearTimeout(ceas); resolve({ cod, rulat: /aere-sd-jwt: \d+\/\d+/.test(out), rosii: out.split('\n').filter((l) => l.startsWith(' RAU ')) }); }); }); } -async function planteaza([nume, din, inl, tinta]) { +async function planteaza([nume, din, inl, tinta, fis = S]) { const t = copie(); try { - const f = path.join(t, S); const src = fs.readFileSync(f, 'utf8'); + const f = path.join(t, fis); const src = fs.readFileSync(f, 'utf8'); if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: tiparul apare de ${src.split(din).length - 1} ori`]; fs.writeFileSync(f, src.replace(din, inl)); const r = await ruleaza(t); diff --git a/identity/identity-cli.mjs b/identity/identity-cli.mjs index 370cfdc..9779f71 100644 --- a/identity/identity-cli.mjs +++ b/identity/identity-cli.mjs @@ -15,10 +15,11 @@ // comply --presentation p.json --policy policy.json --audience A --nonce N [--status-list l.json ...] [--revocation r.json ...] // [--record record.json] [--pseudonym-key-file k] a compliance policy judged; the record carries no personal data // [--json [--with-record]] [--at T] the whole result as one JSON object (with the record inside) -// verify-sdjwt --sd-jwt f.txt --issuer-key k.json [--issuer-alg EdDSA|ML-DSA-65|ES256] --audience A --nonce N +// verify-sdjwt --sd-jwt f.txt --issuer-key k.json [--issuer-alg EdDSA|ML-DSA-65|ES256] [--classical-half] --audience A --nonce N // [--expected-issuer I] [--expected-typ T] [--max-age S] [--at T] [--json] an SD-JWT+KB (IETF RFC 9901), sdjwt.mjs; -// the issuer key is a public JWK or the public keys of an AERE identity (the output of pub) -// verify-sdjwt-vc --sd-jwt f.txt --issuer-key k.json [--issuer-alg A] --audience A --nonce N --expected-vct V [--expected-vct V2 ...] +// the issuer key is a public JWK or the public keys of an AERE identity (the output of pub). With AERE keys the ML-DSA-65 +// half is judged; the Ed25519 half only with --classical-half (2026-10-19, AIP-25 C1), and then the verdict says postQuantum false +// verify-sdjwt-vc --sd-jwt f.txt --issuer-key k.json [--issuer-alg A] [--classical-half] --audience A --nonce N --expected-vct V [--expected-vct V2 ...] // [--status-list f.txt [--status-list-key k.json] [--status-list-alg A]] [--status-not-required] [--expected-issuer I] // [--max-age S] [--at T] [--json] an SD-JWT VC (draft-ietf-oauth-sd-jwt-vc), sdjwt-vc.mjs; the status list is the Token // Status List JWT the verifier fetched from the uri the credential names (without it a credential with `status` is INVALID) @@ -139,32 +140,42 @@ async function main() { const { verifySdJwt, publicJwk } = await import('./sdjwt.mjs'); const vc = cmd === 'verify-sdjwt-vc'; let jeton; try { jeton = fs.readFileSync(cere('--sd-jwt'), 'utf8').trim(); } catch (e) { throw new Folosire(`cannot read --sd-jwt: ${e.message}`); } + // 2026-10-19 (AIP-25 C1, D-535): cu cheile publice ale unei identitati AERE (iesirea lui `pub`), fara --issuer-alg se judeca jumatatea + // ML-DSA-65; jumatatea Ed25519 numai cu --classical-half (si verdictul spune postQuantum false). Pana atunci implicitul era EdDSA, deci + // jumatatea clasica singura. Pentru un JWK simplu, --issuer-alg ramane cum era (implicit EdDSA, verificat contra cheii). + const classicalHalf = are('--classical-half'); + const aere = (k) => !!k && typeof k === 'object' && k.alg === I.ALG; // revizuirea din 30 sept: cheia emitentului e intrarea VERIFICATORULUI; una care nu se citeste, e privata sau nu e a algoritmului cerut e o - // greseala a lui (cod 2, cu motivul), nu un credential INVALID (cheile stricate ieseau INVALID cu mesajul Node) + // greseala a lui (cod 2, cu motivul), nu un credential INVALID (cheile stricate ieseau INVALID cu mesajul Node). Pentru chei AERE se + // citesc jumatatile pe care verificarea le poate folosi; cererea jumatatii Ed25519 fara --classical-half nu e o greseala de intrare, ci + // un refuz al verificatorului (INVALID, regula hybrid-half), ca jetonul doar-Ed25519 const cheiePublicaBuna = (optiune, cheie, alg) => { - if (!['EdDSA', 'Ed25519', 'ES256', 'ML-DSA-65'].includes(alg)) throw new Folosire(`${optiune}-alg is EdDSA, ES256 or ML-DSA-65`); - try { - const jwk = publicJwk(cheie, alg); - if ('d' in jwk || 'priv' in jwk) throw new Error('a private JWK was given where a public key belongs'); - const k = crypto.createPublicKey({ key: jwk, format: 'jwk' }); - const buna = alg === 'ES256' ? k.asymmetricKeyType === 'ec' && k.asymmetricKeyDetails.namedCurve === 'prime256v1' : alg === 'ML-DSA-65' ? k.asymmetricKeyType === 'ml-dsa-65' : k.asymmetricKeyType === 'ed25519'; - if (!buna) throw new Error(`the key is a ${k.asymmetricKeyType} key, not one for ${alg}`); - } catch (e) { if (e instanceof Folosire) throw e; throw new Folosire(`${optiune}: ` + String(e.message || e).replace(/^sd-jwt: /, '').slice(0, 160)); } + if (alg != null && !['EdDSA', 'Ed25519', 'ES256', 'ML-DSA-65'].includes(alg)) throw new Folosire(`${optiune}-alg is EdDSA, ES256 or ML-DSA-65`); + const deCitit = !aere(cheie) ? [alg ?? 'EdDSA'] : alg != null ? (['EdDSA', 'Ed25519'].includes(alg) && !classicalHalf ? [] : [alg]) : (classicalHalf ? ['ML-DSA-65', 'EdDSA'] : ['ML-DSA-65']); + for (const a of deCitit) { + try { + const jwk = publicJwk(cheie, a, { classicalHalf: true }); + if ('d' in jwk || 'priv' in jwk) throw new Error('a private JWK was given where a public key belongs'); + const k = crypto.createPublicKey({ key: jwk, format: 'jwk' }); + const buna = a === 'ES256' ? k.asymmetricKeyType === 'ec' && k.asymmetricKeyDetails.namedCurve === 'prime256v1' : a === 'ML-DSA-65' ? k.asymmetricKeyType === 'ml-dsa-65' : k.asymmetricKeyType === 'ed25519'; + if (!buna) throw new Error(`the key is a ${k.asymmetricKeyType} key, not one for ${a}`); + } catch (e) { if (e instanceof Folosire) throw e; throw new Folosire(`${optiune}: ` + String(e.message || e).replace(/^sd-jwt: /, '').slice(0, 160)); } + } }; const cheie = citeste(cere('--issuer-key')); - const alg = get('--issuer-alg') ?? 'EdDSA'; - cheiePublicaBuna('--issuer-key', cheie, alg); + const algDat = get('--issuer-alg'); + cheiePublicaBuna('--issuer-key', cheie, algDat); const maxAgeS = Number(get('--max-age') ?? 300); if (!Number.isInteger(maxAgeS) || maxAgeS < 1 || maxAgeS > 3600) throw new Folosire('--max-age is a number of seconds, 1..3600'); const tip = get('--expected-typ'); - const comune = { issuerKey: cheie, issuerAlg: alg, audience: cere('--audience'), nonce: cere('--nonce'), expectedIssuer: get('--expected-issuer'), maxAgeS, now: momentul(), ...(tip != null ? { expectedTyp: tip } : {}) }; + const comune = { issuerKey: cheie, issuerAlg: algDat ?? (aere(cheie) ? undefined : 'EdDSA'), classicalHalf, audience: cere('--audience'), nonce: cere('--nonce'), expectedIssuer: get('--expected-issuer'), maxAgeS, now: momentul(), ...(tip != null ? { expectedTyp: tip } : {}) }; let r; if (!vc) r = verifySdJwt(jeton, comune); else { const { verifySdJwtVc } = await import('./sdjwt-vc.mjs'); const vcts = toate('--expected-vct'); if (!vcts.length) throw new Folosire('verify-sdjwt-vc needs --expected-vct (the credential type the verifier expects; may be repeated)'); let statusListJwt = null; if (get('--status-list')) { try { statusListJwt = fs.readFileSync(get('--status-list'), 'utf8').trim(); } catch (e) { throw new Folosire(`cannot read --status-list: ${e.message}`); } } - const statusListKey = get('--status-list-key') ? citeste(get('--status-list-key')) : null, statusListAlg = get('--status-list-alg') ?? alg; + const statusListKey = get('--status-list-key') ? citeste(get('--status-list-key')) : null, statusListAlg = get('--status-list-alg') ?? algDat ?? undefined; if (statusListKey) cheiePublicaBuna('--status-list-key', statusListKey, statusListAlg); r = verifySdJwtVc(jeton, { ...comune, expectedVct: vcts, statusListJwt, statusListKey, statusListAlg, requireStatus: !are('--status-not-required') }); } @@ -173,7 +184,9 @@ async function main() { else if (!r.valid) console.log('INVALID: ' + r.reason); else { const stare = !vc ? '' : '; status ' + (r.status.judged ? `${r.status.value} (${r.status.name}) in ${r.status.uri} at ${r.status.idx}` : r.status.present ? `not judged (${r.status.uri ?? r.status.mechanism})` : 'none'); - console.log(`VALID: issued by ${r.issuer}${r.issuerChecked ? '' : ' (not checked against an expected issuer)'}${vc ? `; vct ${r.vct}` : ''}; disclosed: ${r.disclosed.join(', ') || 'nothing'}${stare}`); + // AIP-25 C1: textul spune si daca fiecare semnatura judecata e post-cuantica (in --json: postQuantum) + const pq = r.postQuantum ? '; post-quantum: every signature judged is ML-DSA-65' : `; NOT post-quantum: the issuer signed ${r.alg}${r.keyBinding ? ', the key binding ' + r.keyBinding.alg : ''}${vc && r.status && r.status.judged ? ' (or the status list is not ML-DSA-65)' : ''}`; + console.log(`VALID: issued by ${r.issuer}${r.issuerChecked ? '' : ' (not checked against an expected issuer)'}${vc ? `; vct ${r.vct}` : ''}; disclosed: ${r.disclosed.join(', ') || 'nothing'}${stare}${pq}`); } return r.valid ? 0 : 1; } diff --git a/identity/interop/interop-sdjwt-vc.mjs b/identity/interop/interop-sdjwt-vc.mjs index e352a92..a3239b1 100644 --- a/identity/interop/interop-sdjwt-vc.mjs +++ b/identity/interop/interop-sdjwt-vc.mjs @@ -60,14 +60,15 @@ async function main() { // ---------------------------------------------------------------- partea NOASTRA function cheiNoi(alg) { if (alg === 'ES256') { const k = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); return { priv: k.privateKey, pub: k.publicKey, jwk: J.publicJwk(k.publicKey), alg }; } - const k = I.generateKeys(); return { priv: k, pub: k.public, jwk: J.publicJwk(k, alg), alg }; + const k = I.generateKeys(); return { priv: k, pub: k.public, jwk: J.publicJwk(k, alg, { classicalHalf: true }), alg }; } const CL = { given_name: 'Ana', family_name: 'Pop', birthdate: '1990-01-01', age_over_18: true, nationalities: ['RO', 'DE'], address: { locality: 'Cluj', country: 'RO' }, issuing_authority: 'Example Authority', issuing_country: 'RO' }; // status: null = credential FARA status (un `undefined` ar lua valoarea implicita a parametrului) - const noiEmite = ({ iss, hol, alg, status = { status_list: { idx: IDX, uri: URI } }, extra = {} }) => V.issueSdJwtVc({ issuer: iss.priv, holder: hol.pub, vct: VCT, claims: CL, disclosable: ['given_name', 'family_name', 'birthdate', 'age_over_18', 'address'], arrayDisclosable: ['nationalities'], iss: 'https://issuer.aere.example', iat: CEAS, nbf: CEAS, exp: CEAS + 365 * 86400, alg, holderAlg: hol.alg, decoys: 2, ...(status !== null ? { status } : {}), ...extra }); - const noiPrezinta = (sd, reveal, hol, extra = {}) => V.presentSdJwtVc({ sdJwt: sd, reveal, holder: hol.priv, holderAlg: hol.alg, audience: AUD, nonce: NONCE, iat: CEAS, ...extra }); + // 2026-10-19 (AIP-25 C1): jumatatea Ed25519 a cheilor AERE se cere explicit (classicalHalf); masuratoarea EdDSA din 18 oct o folosea implicit + const noiEmite = ({ iss, hol, alg, status = { status_list: { idx: IDX, uri: URI } }, extra = {} }) => V.issueSdJwtVc({ classicalHalf: true, issuer: iss.priv, holder: hol.pub, vct: VCT, claims: CL, disclosable: ['given_name', 'family_name', 'birthdate', 'age_over_18', 'address'], arrayDisclosable: ['nationalities'], iss: 'https://issuer.aere.example', iat: CEAS, nbf: CEAS, exp: CEAS + 365 * 86400, alg, holderAlg: hol.alg, decoys: 2, ...(status !== null ? { status } : {}), ...extra }); + const noiPrezinta = (sd, reveal, hol, extra = {}) => V.presentSdJwtVc({ classicalHalf: true, sdJwt: sd, reveal, holder: hol.priv, holderAlg: hol.alg, audience: AUD, nonce: NONCE, iat: CEAS, ...extra }); const noiVerifica = (sd, { issuerKey, alg, statusListJwt = null, optiuni = {} }) => V.verifySdJwtVc(sd, { issuerKey, issuerAlg: alg, audience: AUD, nonce: NONCE, now: LA, expectedVct: VCT, ...(statusListJwt != null ? { statusListJwt } : {}), ...optiuni }); - const listaNoi = (iss, statuses, bits = 1, extra = {}) => V.issueStatusListJwt({ issuer: iss.priv, alg: iss.alg, uri: URI, statuses, bits, iat: CEAS, exp: CEAS + 3600, ...extra }); + const listaNoi = (iss, statuses, bits = 1, extra = {}) => V.issueStatusListJwt({ classicalHalf: true, issuer: iss.priv, alg: iss.alg, uri: URI, statuses, bits, iat: CEAS, exp: CEAS + 3600, ...extra }); const toateValide = new Array(N).fill(0); const revocat = toateValide.map((_, i) => (i === IDX ? 1 : 0)); const suspendat = toateValide.map((_, i) => (i === IDX ? 2 : 0)); @@ -170,7 +171,7 @@ async function main() { const citit = V.readStatusListJwt(lAmestec, { key: iss.jwk, alg: 'EdDSA', uri: URI, now: LA }); const citite = citit.ok ? amestec.map((_, i) => citit.statusAt(i)) : null; rand({ sens: 'lor->noi', caz: 'IMPACHETARE: lista LOR pe 2 biti cu 12 stari amestecate, citite de noi index cu index', noi: { valid: citit.ok && egal(citite, amestec), reason: citit.ok ? 'citit ' + JSON.stringify(citite) : citit.reason }, lor: { valid: true }, asteptat: { noi: 'VALID', lor: 'VALID' }, nota: 'citit: ' + JSON.stringify(citite) + ' = scris: ' + JSON.stringify(amestec) }); - const lNoiAmestec = V.issueStatusListJwt({ issuer: cheiNoi('EdDSA').priv, uri: URI, statuses: amestec, bits: 2, iat: CEAS }); + const lNoiAmestec = V.issueStatusListJwt({ issuer: cheiNoi('EdDSA').priv, alg: 'EdDSA', classicalHalf: true, uri: URI, statuses: amestec, bits: 2, iat: CEAS }); const lorCitit = amestec.map((_, i) => tsl.getListFromStatusListJWT(lNoiAmestec).getStatus(i)); rand({ sens: 'noi->lor', caz: 'IMPACHETARE: lista NOASTRA pe 2 biti cu 12 stari amestecate, citite de ei index cu index', noi: { valid: true }, lor: { valid: egal(lorCitit, amestec), reason: 'citit ' + JSON.stringify(lorCitit) }, asteptat: { noi: 'VALID', lor: 'VALID' }, nota: 'citit: ' + JSON.stringify(lorCitit) + ' = scris: ' + JSON.stringify(amestec) }); await neg('NEGATIV: lista LOR pentru ALT uri (sub != uri-ul din jeton)', { lista: await listaLor(iss, toateValide, 1, { uri: URI + '-alta' }), motivNoi: /sub .* is not the uri/, motivLor: /sub|uri|Status List/i }); diff --git a/identity/interop/interop-sdjwt.mjs b/identity/interop/interop-sdjwt.mjs index 5a55d6c..4317428 100644 --- a/identity/interop/interop-sdjwt.mjs +++ b/identity/interop/interop-sdjwt.mjs @@ -76,10 +76,11 @@ async function main() { const CL = { given_name: 'Ana', family_name: 'Pop', age_over_18: true, nationalities: ['RO', 'DE'], address: { street_address: 'Str. Lunga 1', locality: 'Cluj', country: 'RO' } }; function cheiNoi(alg) { if (alg === 'ES256') { const k = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); return { priv: k.privateKey, pub: k.publicKey, jwk: J.publicJwk(k.publicKey), alg }; } - const k = I.generateKeys(); return { priv: k, pub: k.public, jwk: J.publicJwk(k, alg), alg }; + const k = I.generateKeys(); return { priv: k, pub: k.public, jwk: J.publicJwk(k, alg, { classicalHalf: true }), alg }; } - const noiEmite = ({ iss, hol, alg, extra = {} }) => J.issueSdJwt({ issuer: iss.priv, holder: hol.pub, claims: CL, disclosable: ['given_name', 'family_name', 'age_over_18'], arrayDisclosable: ['nationalities'], iss: 'https://issuer.aere.example', iat: CEAS, exp: CEAS + 365 * 86400, alg, holderAlg: hol.alg, decoys: 2, ...extra }); - const noiPrezinta = (sd, reveal, hol, extra = {}) => J.presentSdJwt({ sdJwt: sd, reveal, holder: hol.priv, holderAlg: hol.alg, audience: AUD, nonce: NONCE, iat: CEAS, ...extra }); + // 2026-10-19 (AIP-25 C1): jumatatea Ed25519 a cheilor AERE se cere explicit (classicalHalf); masuratoarea EdDSA din 18 oct o folosea implicit + const noiEmite = ({ iss, hol, alg, extra = {} }) => J.issueSdJwt({ classicalHalf: true, issuer: iss.priv, holder: hol.pub, claims: CL, disclosable: ['given_name', 'family_name', 'age_over_18'], arrayDisclosable: ['nationalities'], iss: 'https://issuer.aere.example', iat: CEAS, exp: CEAS + 365 * 86400, alg, holderAlg: hol.alg, decoys: 2, ...extra }); + const noiPrezinta = (sd, reveal, hol, extra = {}) => J.presentSdJwt({ classicalHalf: true, sdJwt: sd, reveal, holder: hol.priv, holderAlg: hol.alg, audience: AUD, nonce: NONCE, iat: CEAS, ...extra }); const jwtDin = (antet, corp, k) => { const cap = b64u(JSON.stringify(antet)) + '.' + b64u(JSON.stringify(corp)); const date = Buffer.from(cap, 'ascii'); return cap + '.' + b64u(antet.alg === 'ES256' ? crypto.sign('sha256', date, { key: k, dsaEncoding: 'ieee-p1363' }) : crypto.sign(null, date, k)); }; const sha = (d) => crypto.createHash('sha256').update(d).digest('base64url'); diff --git a/identity/proba-sdjwt-vc.mjs b/identity/proba-sdjwt-vc.mjs index efe8d84..af83e23 100644 --- a/identity/proba-sdjwt-vc.mjs +++ b/identity/proba-sdjwt-vc.mjs @@ -31,17 +31,33 @@ const arata = (sd, reveal, o = {}) => V.presentSdJwtVc({ sdJwt: sd, reveal, hold const judeca = (p, o = {}) => V.verifySdJwtVc(p, { issuerKey: iss, audience: AUD, nonce: NONCE, now: LA, expectedVct: VCT, ...o }); const lista = (statuses, bits = 1, o = {}) => V.issueStatusListJwt({ issuer: iss, uri: URI, statuses, bits, iat: NOW, exp: NOW + 3600, ...o }); const TOATE0 = new Array(16).fill(0), REVOCAT = TOATE0.map((_, i) => (i === IDX ? 1 : 0)), SUSPENDAT = TOATE0.map((_, i) => (i === IDX ? 2 : 0)); +// 2026-10-19 (AIP-25 C1): cu chei AERE, jetonul si lista de stare se semneaza si se judeca implicit cu jumatatea ML-DSA-65; cele facute de +// mana mai jos sunt semnate cu ea (cu jumatatea Ed25519 singura ar cadea pe hybrid-half inaintea regulii masurate) +const PQ = 'ML-DSA-65', EM = iss.privat.mldsa65; const jwtDin = (antet, corp, cheie) => { const cap = b64u(JSON.stringify(antet)) + '.' + b64u(JSON.stringify(corp)); return cap + '.' + b64u(crypto.sign(null, Buffer.from(cap), cheie)); }; const E = emite(), P = arata(E.sdJwt, ['birthdate', 'age_over_18', { element: 'RO', in: 'nationalities' }]); test('PROFIL: emitere SD-JWT VC (typ dc+sd-jwt, vct, iss, cnf, nbf/exp, status) si verificare cu lista de stare (toate 0): VALID, starea judecata 0, numai ce s-a aratat', () => { - const h = JSON.parse(Buffer.from(E.sdJwt.split('.')[0], 'base64url').toString('utf8')); cere(h.typ === 'dc+sd-jwt' && h.alg === 'EdDSA', JSON.stringify(h)); + const h = JSON.parse(Buffer.from(E.sdJwt.split('.')[0], 'base64url').toString('utf8')); cere(h.typ === 'dc+sd-jwt' && h.alg === PQ, JSON.stringify(h)); const r = judeca(P, { statusListJwt: lista(TOATE0) }); cere(r.valid, r.reason); cere(r.vct === VCT && r.payload.vct === VCT && r.payload.iss === 'https://issuer.example' && r.payload.cnf && r.payload.nbf === NOW && r.payload.exp === NOW + 365 * 86400, JSON.stringify(Object.keys(r.payload))); cere(r.payload.birthdate === '1990-01-01' && r.payload.age_over_18 === true && !('given_name' in r.payload) && !('address' in r.payload) && egal(r.payload.nationalities, ['RO']) && r.payload.issuing_country === 'RO', JSON.stringify(r.payload)); cere(egal(r.status, { present: true, judged: true, mechanism: 'status_list', uri: URI, idx: IDX, value: 0, name: 'VALID' }), JSON.stringify(r.status)); cere(r.keyBinding && r.keyBinding.aud === AUD && r.keyBinding.nonce === NONCE, JSON.stringify(r.keyBinding)); + // AIP-25 C1: credentialul, Key Binding-ul si lista de stare implicite ale unor chei AERE sunt toate ML-DSA-65 + cere(r.postQuantum === true && r.alg === PQ && r.keyBinding.alg === PQ, 'postQuantum=' + r.postQuantum + ' ' + r.alg + '/' + r.keyBinding.alg); +}); +test('AIP-25 C1: lista de stare a unui emitent AERE e semnata implicit ML-DSA-65; una semnata NUMAI cu jumatatea Ed25519 (cine a spart doar cheia clasica "dezrevoca" credentialul) -> refuzata implicit (Ed25519 half); cu classicalHalf -> primita, dar verdictul VC e postQuantum false; un credential doar-Ed25519 -> hybrid-half', () => { + const lp = lista(TOATE0); cere(JSON.parse(Buffer.from(lp.split('.')[0], 'base64url').toString('utf8')).alg === PQ, 'lista implicita nu e ML-DSA-65'); + const corp = JSON.parse(Buffer.from(lp.split('.')[1], 'base64url').toString('utf8')); + const clasica = jwtDin({ alg: 'EdDSA', typ: 'statuslist+jwt' }, corp, iss.privat.ed25519); + const a = judeca(P, { statusListJwt: clasica }); cere(!a.valid && a.rule === 'status-list' && /Ed25519 half/.test(a.reason) && a.postQuantum === false, 'lista doar-Ed25519, implicit: ' + (a.valid ? 'VALID' : a.rule + ': ' + a.reason)); + const b = judeca(P, { statusListJwt: clasica, classicalHalf: true }); cere(b.valid && b.status.judged && b.alg === PQ && b.postQuantum === false, 'lista doar-Ed25519 cu classicalHalf: ' + (b.valid ? 'pq=' + b.postQuantum : b.reason)); + cere(/classicalHalf/.test((() => { try { V.issueStatusListJwt({ issuer: iss, alg: 'EdDSA', uri: URI, statuses: TOATE0, iat: NOW }); return ''; } catch (e) { return e.message; } })()), 'lista EdDSA emisa fara classicalHalf'); + const corpC = JSON.parse(Buffer.from(E.sdJwt.split('~')[0].split('.')[1], 'base64url').toString('utf8')); + const c1 = jwtDin({ alg: 'EdDSA', typ: 'dc+sd-jwt' }, corpC, iss.privat.ed25519) + '~' + E.sdJwt.split('~').slice(1).join('~'); + const c = judeca(arata(c1, ['birthdate']), { statusListJwt: lp }); cere(!c.valid && c.rule === 'hybrid-half', 'credentialul doar-Ed25519: ' + (c.valid ? 'VALID' : c.rule)); }); test('PROFIL vct: verificatorul fara expectedVct -> refuzat; alt vct asteptat -> refuzat; lista de vct-uri acceptate -> primit; jeton fara vct -> refuzat (s.2.2.2.3 REQUIRED)', () => { cere(refuz(V.verifySdJwtVc(P, { issuerKey: iss, audience: AUD, nonce: NONCE, now: LA, statusListJwt: lista(TOATE0) }), /must name the vct/), 'fara expectedVct a trecut'); @@ -66,10 +82,10 @@ test('PROFIL s.2.2.2.3: emitentul refuza vct/status/iss/exp/cnf dezvaluibile, vc cere(incearca({ disclosable: ['sub'] }) === null, 'sub dezvaluibil refuzat (s.2.2.2.3 il lasa)'); // verificatorul: o dezvaluire numita status / vct, facuta de mana si semnata de emitent const fa = (nume, val) => { const d = b64u(JSON.stringify(['s1', nume, val])); const dg = crypto.createHash('sha256').update(d).digest('base64url'); - return jwtDin({ alg: 'EdDSA', typ: 'dc+sd-jwt' }, { vct: VCT, iss: 'x', exp: NOW + 100, _sd_alg: 'sha-256', _sd: [dg] }, iss.privat.ed25519) + '~' + d + '~'; }; + return jwtDin({ alg: PQ, typ: 'dc+sd-jwt' }, { vct: VCT, iss: 'x', exp: NOW + 100, _sd_alg: 'sha-256', _sd: [dg] }, EM) + '~' + d + '~'; }; cere(refuz(judeca(fa('status', { status_list: { idx: 1, uri: URI } }), { requireKeyBinding: false }), /status was selectively disclosed/), 'status dezvaluit a trecut'); const dv = b64u(JSON.stringify(['s1', 'nationalities', ['RO']])), dgv = crypto.createHash('sha256').update(dv).digest('base64url'); - const bun = jwtDin({ alg: 'EdDSA', typ: 'dc+sd-jwt' }, { vct: VCT, iss: 'x', exp: NOW + 100, _sd_alg: 'sha-256', _sd: [dgv] }, iss.privat.ed25519) + '~' + dv + '~'; + const bun = jwtDin({ alg: PQ, typ: 'dc+sd-jwt' }, { vct: VCT, iss: 'x', exp: NOW + 100, _sd_alg: 'sha-256', _sd: [dgv] }, EM) + '~' + dv + '~'; cere(judeca(bun, { requireKeyBinding: false }).valid, 'martorul (o dezvaluire obisnuita) refuzat'); }); test('STARE: lista lipsa -> refuzat implicit (requireStatus, cu uri-ul de adus in motiv); requireStatus:false -> VALID cu starea prezenta si nejudecata; credential fara status -> VALID fara lista', () => { @@ -77,7 +93,7 @@ test('STARE: lista lipsa -> refuzat implicit (requireStatus, cu uri-ul de adus i const r2 = judeca(P, { requireStatus: false }); cere(r2.valid && egal(r2.status, { present: true, judged: false, mechanism: 'status_list', uri: URI, idx: IDX }), r2.reason + ' ' + JSON.stringify(r2.status)); const F = emite({ status: undefined }); const r3 = judeca(arata(F.sdJwt, ['birthdate'])); cere(r3.valid && egal(r3.status, { present: false, judged: false }), r3.reason + ' ' + JSON.stringify(r3.status)); // un mecanism de stare pe care nu il stim: prezent, nejudecat - const alt = jwtDin({ alg: 'EdDSA', typ: 'dc+sd-jwt' }, { vct: VCT, iss: 'x', exp: NOW + 100, status: { identifier_list: { id: '1', uri: URI } } }, iss.privat.ed25519) + '~'; + const alt = jwtDin({ alg: PQ, typ: 'dc+sd-jwt' }, { vct: VCT, iss: 'x', exp: NOW + 100, status: { identifier_list: { id: '1', uri: URI } } }, EM) + '~'; const r4 = judeca(alt, { requireKeyBinding: false }); cere(refuz(r4, /was not judged/) && r4.status.mechanism === 'identifier_list', r4.reason); cere(judeca(alt, { requireKeyBinding: false, requireStatus: false }).valid, 'mecanismul necunoscut cu requireStatus:false refuzat'); }); @@ -102,11 +118,11 @@ test('STARE: lista pentru alt uri (sub), expirata, semnata de alta cheie, cu typ cere(refuz(judeca(P, { statusListJwt: V.issueStatusListJwt({ issuer: strain, uri: URI, statuses: TOATE0, iat: NOW }) }), /status list: the signature does not verify/), 'alta cheie a trecut'); cere(judeca(P, { statusListJwt: V.issueStatusListJwt({ issuer: strain, uri: URI, statuses: TOATE0, iat: NOW }), statusListKey: strain }).valid, 'cheia listei data separat refuzata'); const corp = JSON.parse(Buffer.from(lista(TOATE0).split('.')[1], 'base64url').toString('utf8')); - cere(refuz(judeca(P, { statusListJwt: jwtDin({ alg: 'EdDSA', typ: 'JWT' }, corp, iss.privat.ed25519) }), /status list: typ/), 'typ JWT a trecut'); + cere(refuz(judeca(P, { statusListJwt: jwtDin({ alg: PQ, typ: 'JWT' }, corp, EM) }), /status list: typ/), 'typ JWT a trecut'); const { iat: _i, ...faraIat } = corp; - cere(refuz(judeca(P, { statusListJwt: jwtDin({ alg: 'EdDSA', typ: 'statuslist+jwt' }, faraIat, iss.privat.ed25519) }), /status list: iat is required/), 'fara iat a trecut'); - cere(refuz(judeca(P, { statusListJwt: jwtDin({ alg: 'EdDSA', typ: 'statuslist+jwt' }, { ...corp, status_list: { ...corp.status_list, bits: 3 } }, iss.privat.ed25519) }), /status list: status_list must be/), 'bits 3 a trecut'); - cere(refuz(judeca(P, { statusListJwt: jwtDin({ alg: 'EdDSA', typ: 'statuslist+jwt' }, { ...corp, iat: NOW + 600 }, iss.privat.ed25519) }), /status list: issued in the future/), 'iat in viitor a trecut'); + cere(refuz(judeca(P, { statusListJwt: jwtDin({ alg: PQ, typ: 'statuslist+jwt' }, faraIat, EM) }), /status list: iat is required/), 'fara iat a trecut'); + cere(refuz(judeca(P, { statusListJwt: jwtDin({ alg: PQ, typ: 'statuslist+jwt' }, { ...corp, status_list: { ...corp.status_list, bits: 3 } }, EM) }), /status list: status_list must be/), 'bits 3 a trecut'); + cere(refuz(judeca(P, { statusListJwt: jwtDin({ alg: PQ, typ: 'statuslist+jwt' }, { ...corp, iat: NOW + 600 }, EM) }), /status list: issued in the future/), 'iat in viitor a trecut'); const scurta = V.issueStatusListJwt({ issuer: iss, uri: URI, statuses: [0, 0, 0, 0], bits: 8, iat: NOW }); cere(refuz(judeca(P, { statusListJwt: scurta }), /index 5 is beyond the list \(4 entries\)/), 'index dincolo a trecut'); let m = null; try { V.issueStatusListJwt({ issuer: iss, uri: URI, statuses: [0, 2], bits: 1, iat: NOW }); } catch (e) { m = e.message; } cere(/below 2\^1/.test(m || ''), 'starea 2 pe 1 bit emisa'); @@ -116,7 +132,7 @@ test('POST-CUANTIC: credential ML-DSA-65 cu lista de stare semnata ML-DSA-65 -> const p = arata(M.sdJwt, ['birthdate'], { holderAlg: 'ML-DSA-65' }); const l = V.issueStatusListJwt({ issuer: iss, alg: 'ML-DSA-65', uri: URI, statuses: TOATE0, iat: NOW }); const r = judeca(p, { issuerAlg: 'ML-DSA-65', statusListJwt: l }); cere(r.valid && r.alg === 'ML-DSA-65' && r.status.judged, r.reason); - cere(refuz(judeca(p, { issuerAlg: 'ML-DSA-65', statusListJwt: l, statusListAlg: 'EdDSA' }), /status list: .*does not match|status list: signed with/), 'lista ML-DSA-65 judecata cu Ed25519 a trecut'); + cere(refuz(judeca(p, { issuerAlg: 'ML-DSA-65', statusListJwt: l, statusListAlg: 'EdDSA' }), /status list: .*does not match|status list: signed with|status list: .*Ed25519 half/), 'lista ML-DSA-65 judecata cu Ed25519 a trecut'); }); const FX = path.join(AICI, 'interop', 'fixturi-interop-sdjwt-vc.json'); const F = fs.existsSync(FX) ? JSON.parse(fs.readFileSync(FX, 'utf8')) : null; diff --git a/identity/proba-sdjwt.mjs b/identity/proba-sdjwt.mjs index 2ff0b7a..45f3de1 100644 --- a/identity/proba-sdjwt.mjs +++ b/identity/proba-sdjwt.mjs @@ -57,32 +57,38 @@ test('RFC 9901: o dezvaluire scoasa din prezentare dupa semnarea KB (sd_hash) sa }); // ---------------------------------------------------------------- jetoane emise cu chei AERE +// 2026-10-19 (AIP-25 C1, D-535): cu chei AERE (Ed25519 SI ML-DSA-65), emiterea, cnf-ul detinatorului si verificarea folosesc IMPLICIT jumatatea +// post-cuantica, ML-DSA-65; jumatatea Ed25519 numai la cerere (classicalHalf), cu verdictul postQuantum false. Probele de mai jos o masoara +// pe calea implicita; jetoanele facute de mana sunt semnate cu jumatatea ML-DSA-65 (cu cea Ed25519 singura ar cadea pe hybrid-half inaintea +// regulii pe care o masoara proba). const NOW = Math.floor(Date.parse('2026-09-30T08:00:00Z') / 1000), LA = new Date(NOW * 1000); const iss = I.generateKeys(), hol = I.generateKeys(), strain = I.generateKeys(); +const PQ = 'ML-DSA-65', EM = iss.privat.mldsa65, HM = hol.privat.mldsa65; const CL = { given_name: 'Ana', family_name: 'Pop', age_over_18: true, nationalities: ['RO', 'DE'], employer: 'Example Ltd' }; const emite = (o = {}) => J.issueSdJwt({ issuer: iss, holder: hol, claims: CL, disclosable: ['given_name', 'family_name', 'age_over_18'], arrayDisclosable: ['nationalities'], iss: 'https://issuer.aere.example', iat: NOW, exp: NOW + 86400, decoys: 2, ...o }); const AUD = 'https://verifier.example', NONCE = 'n-' + crypto.randomBytes(4).toString('hex'); const arata = (sd, reveal, o = {}) => J.presentSdJwt({ sdJwt: sd, reveal, holder: hol, audience: AUD, nonce: NONCE, iat: NOW, ...o }); const judeca = (p, o = {}) => J.verifySdJwt(p, { issuerKey: iss, audience: AUD, nonce: NONCE, now: LA, ...o }); const E = emite(); -test('AERE -> SD-JWT (EdDSA, cheia Ed25519 a emitentului AERE): detinatorul arata age_over_18 si RO, verificatorul vede numai atat plus afirmatiile in clar', () => { +test('AERE -> SD-JWT (implicit ML-DSA-65, jumatatea post-cuantica a emitentului AERE): detinatorul arata age_over_18 si RO, verificatorul vede numai atat plus afirmatiile in clar, postQuantum true', () => { const r = judeca(arata(E.sdJwt, ['age_over_18', { element: 'RO', in: 'nationalities' }])); cere(r.valid, r.reason); - cere(r.payload.age_over_18 === true && !('given_name' in r.payload) && egal(r.payload.nationalities, ['RO']) && r.payload.employer === 'Example Ltd' && r.alg === 'EdDSA', JSON.stringify(r.payload)); + cere(r.payload.age_over_18 === true && !('given_name' in r.payload) && egal(r.payload.nationalities, ['RO']) && r.payload.employer === 'Example Ltd' && r.alg === PQ && r.postQuantum === true, JSON.stringify(r.payload) + ' alg=' + r.alg + ' pq=' + r.postQuantum); }); -test('AERE -> SD-JWT in forma standardului: typ explicit, _sd sortat (cu momeli), _sd_alg sha-256, cnf.jwk = cheia Ed25519 a detinatorului, dezvaluiri [sare, nume, valoare]', () => { +test('AERE -> SD-JWT in forma standardului: typ explicit, _sd sortat (cu momeli), _sd_alg sha-256, cnf.jwk = cheia ML-DSA-65 (AKP, RFC 9964) a detinatorului, dezvaluiri [sare, nume, valoare]', () => { const [h, p] = E.sdJwt.split('~')[0].split('.').slice(0, 2).map((x) => JSON.parse(Buffer.from(x, 'base64url').toString('utf8'))); - cere(h.alg === 'EdDSA' && h.typ === J.TYP, JSON.stringify(h)); + cere(h.alg === PQ && h.typ === J.TYP, JSON.stringify(h)); cere(Array.isArray(p._sd) && p._sd.length === 5 && egal([...p._sd].sort(), p._sd) && p._sd_alg === 'sha-256', JSON.stringify(p._sd)); - cere(p.cnf.jwk.kty === 'OKP' && p.cnf.jwk.crv === 'Ed25519' && p.cnf.jwk.x === J.publicJwk(hol).x && !('d' in p.cnf.jwk), JSON.stringify(p.cnf)); + cere(p.cnf.jwk.kty === 'AKP' && p.cnf.jwk.alg === PQ && p.cnf.jwk.pub === J.publicJwk(hol).pub && !('priv' in p.cnf.jwk), JSON.stringify(p.cnf).slice(0, 120)); const c = JSON.parse(Buffer.from(E.disclosures[0].disclosure, 'base64url').toString('utf8')); cere(c.length === 3 && typeof c[0] === 'string' && c[0].length >= 22 && c[1] === 'given_name', JSON.stringify(c)); }); -test('AERE -> SD-JWT post-cuantic (ML-DSA-65, cheia AKP): verifica cu cheia ML-DSA-65 a emitentului, nu cu cea Ed25519; detinatorul poate semna KB tot cu ML-DSA-65', () => { - const M = emite({ alg: 'ML-DSA-65', holderAlg: 'ML-DSA-65' }); - const p = arata(M.sdJwt, ['age_over_18'], { holderAlg: 'ML-DSA-65' }); - const r = judeca(p, { issuerAlg: 'ML-DSA-65' }); cere(r.valid && r.alg === 'ML-DSA-65' && r.keyBinding.alg === 'ML-DSA-65', r.reason); - cere(refuz(judeca(p, { issuerAlg: 'EdDSA' }), /does not match/), 'cheia Ed25519 a primit un jeton ML-DSA-65'); +test('AERE -> SD-JWT post-cuantic numit explicit (alg ML-DSA-65): verifica cu jumatatea ML-DSA-65 a emitentului; issuerAlg EdDSA fara classicalHalf -> hybrid-half; cu classicalHalf, cheia Ed25519 nu primeste un jeton ML-DSA-65', () => { + const M = emite({ alg: PQ, holderAlg: PQ }); + const p = arata(M.sdJwt, ['age_over_18'], { holderAlg: PQ }); + const r = judeca(p, { issuerAlg: PQ }); cere(r.valid && r.alg === PQ && r.keyBinding.alg === PQ && r.postQuantum === true, r.reason); + const e = judeca(p, { issuerAlg: 'EdDSA' }); cere(!e.valid && e.rule === 'hybrid-half', 'issuerAlg EdDSA fara classicalHalf: ' + (e.valid ? 'VALID' : e.rule + ' ' + e.reason)); + cere(refuz(judeca(p, { issuerAlg: 'EdDSA', classicalHalf: true }), /does not match/), 'cheia Ed25519 a primit un jeton ML-DSA-65'); }); // ---------------------------------------------------------------- atacuri @@ -105,14 +111,14 @@ test('ATAC: o dezvaluire cu valoarea schimbata (alt digest, nelegata) -> refuzat }); test('ATAC: un emitent care pune in _sd o dezvaluire cu numele "_sd", una care acopera o afirmatie in clar, sau acelasi digest de doua ori -> refuzat', () => { const faci = (dezvaluiri, extra = {}) => { const ds = dezvaluiri.map((x) => b64u(JSON.stringify(x))); const sd = ds.map((d) => crypto.createHash('sha256').update(d).digest('base64url')); - return jwtDin({ alg: 'EdDSA', typ: J.TYP }, { _sd: extra.dublu ? [...sd, sd[0]] : sd, _sd_alg: 'sha-256', iss: 'x', ...(extra.clar || {}) }, iss.privat.ed25519) + '~' + ds.map((d) => d + '~').join(''); }; + return jwtDin({ alg: PQ, typ: J.TYP }, { _sd: extra.dublu ? [...sd, sd[0]] : sd, _sd_alg: 'sha-256', iss: 'x', ...(extra.clar || {}) }, EM) + '~' + ds.map((d) => d + '~').join(''); }; cere(refuz(J.verifySdJwt(faci([['s1', '_sd', ['x']]]), { issuerKey: iss, requireKeyBinding: false, now: LA }), /may not name _sd/), '_sd a trecut'); cere(refuz(J.verifySdJwt(faci([['s1', 'role', 'admin']], { clar: { role: 'user' } }), { issuerKey: iss, requireKeyBinding: false, now: LA }), /already exists/), 'coliziunea a trecut'); cere(refuz(J.verifySdJwt(faci([['s1', 'a', 1]], { dublu: true }), { issuerKey: iss, requireKeyBinding: false, now: LA }), /more than once/), 'digestul dublu a trecut'); cere(refuz(J.verifySdJwt(faci([['s1', '__proto__', { admin: true }]]), { issuerKey: iss, requireKeyBinding: false, now: LA }), /may not name __proto__/), '__proto__ a trecut'); }); test('ATAC: o afirmatie numita __proto__ in clar sau intr-o valoare dezvaluita (prototipul payload-ului schimbat) -> refuzata', () => { - const cap = (corp) => jwtDin({ alg: 'EdDSA', typ: J.TYP }, corp, iss.privat.ed25519); + const cap = (corp) => jwtDin({ alg: PQ, typ: J.TYP }, corp, EM); const clar = JSON.parse('{"iss":"x","_sd_alg":"sha-256","__proto__":{"admin":true}}'); cere(refuz(J.verifySdJwt(cap(clar) + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /__proto__/), 'in clar a trecut'); const d = b64u('["s1","profil",' + '{"__proto__":{"admin":true}}' + ']'); @@ -124,9 +130,9 @@ test('ATAC: KB semnat de alta cheie decat cnf.jwk, KB cu typ gresit, KB fara non cere(refuz(judeca(alt), /key binding: the signature does not verify/), 'cheia straina a trecut'); const baza = arata(E.sdJwt, ['age_over_18']).split('~').slice(0, -1).join('~') + '~'; const sh = crypto.createHash('sha256').update(Buffer.from(baza, 'ascii')).digest('base64url'); - const tip = jwtDin({ alg: 'EdDSA', typ: 'JWT' }, { iat: NOW, aud: AUD, nonce: NONCE, sd_hash: sh }, hol.privat.ed25519); + const tip = jwtDin({ alg: PQ, typ: 'JWT' }, { iat: NOW, aud: AUD, nonce: NONCE, sd_hash: sh }, HM); cere(refuz(judeca(baza + tip), /typ/), 'typ gresit a trecut'); - const faraNonce = jwtDin({ alg: 'EdDSA', typ: 'kb+jwt' }, { iat: NOW, aud: AUD, sd_hash: sh }, hol.privat.ed25519); + const faraNonce = jwtDin({ alg: PQ, typ: 'kb+jwt' }, { iat: NOW, aud: AUD, sd_hash: sh }, HM); cere(refuz(judeca(baza + faraNonce), /required/), 'fara nonce a trecut'); }); test('timpul: exp trecut -> refuzat (fara toleranta); nbf peste 30 s -> admis (toleranta de ceas), peste 90 s -> refuzat', () => { @@ -154,24 +160,27 @@ test('REVIZUIRE: prezentarea arata elementul NUMAI din lista numita (acelasi RO let m = null; try { arata(D.sdJwt, [{ element: 'IT', in: 'nationalities' }]); } catch (e) { m = e.message; } cere(/nothing to reveal/.test(m || ''), 'alegerea fara corespondent a tacut'); }); test('REVIZUIRE: cheia emitentului data ca KeyObject public sau ca identitate AERE publica (keys.public) e primita; detinatorul dat ca cheie publica', () => { - const pubKO = crypto.createPublicKey(iss.privat.ed25519); + const pubKO = crypto.createPublicKey(EM); const D = J.issueSdJwt({ issuer: iss, holder: crypto.createPublicKey(hol.privat.ed25519), claims: { age_over_18: true }, disclosable: ['age_over_18'], iss: 'x', iat: NOW }); const p = arata(D.sdJwt, ['age_over_18']); cere(J.verifySdJwt(p, { issuerKey: pubKO, audience: AUD, nonce: NONCE, now: LA }).valid, 'KeyObject public refuzat'); - cere(J.verifySdJwt(p, { issuerKey: iss.public, audience: AUD, nonce: NONCE, now: LA }).valid, 'identitatea AERE publica refuzata'); + const r = J.verifySdJwt(p, { issuerKey: iss.public, audience: AUD, nonce: NONCE, now: LA }); + cere(r.valid, 'identitatea AERE publica refuzata'); + // detinatorul legat cu o cheie Ed25519 (KeyObject dat la emitere): Key Binding-ul e clasic, deci verdictul nu e post-cuantic (AIP-25 C1) + cere(r.keyBinding && r.keyBinding.alg === 'EdDSA' && r.postQuantum === false, 'KB Ed25519: ' + JSON.stringify(r.keyBinding) + ' pq=' + r.postQuantum); }); test('REVIZUIRE: exp cerut implicit (un exp dezvaluibil si retinut de detinator nu mai lasa jetonul fara expirare); requireExp:false il lasa', () => { const dExp = b64u(JSON.stringify(['s', 'exp', NOW - 10])); const corp = { _sd: [crypto.createHash('sha256').update(dExp).digest('base64url')], _sd_alg: 'sha-256', iss: 'x' }; - const jwt = jwtDin({ alg: 'EdDSA', typ: J.TYP }, corp, iss.privat.ed25519); + const jwt = jwtDin({ alg: PQ, typ: J.TYP }, corp, EM); cere(refuz(J.verifySdJwt(jwt + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /exp is required/), 'fara exp a trecut'); cere(refuz(J.verifySdJwt(jwt + '~' + dExp + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /expired/), 'exp dezvaluit si trecut a trecut'); cere(J.verifySdJwt(jwt + '~', { issuerKey: iss, requireKeyBinding: false, requireExp: false, now: LA }).valid, 'requireExp:false a refuzat'); }); test('REVIZUIRE: tipul jetonului: un KB-JWT sau un JWT cu typ JWT dat drept jeton de emitent -> refuzat implicit; expectedIssuer altul -> refuzat', () => { const corp = { iss: 'https://issuer.aere.example', exp: NOW + 100, _sd_alg: 'sha-256' }; - cere(refuz(J.verifySdJwt(jwtDin({ alg: 'EdDSA', typ: 'kb+jwt' }, corp, iss.privat.ed25519) + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /typ/), 'kb+jwt drept emitent a trecut'); - const simplu = jwtDin({ alg: 'EdDSA', typ: 'JWT' }, corp, iss.privat.ed25519) + '~'; + cere(refuz(J.verifySdJwt(jwtDin({ alg: PQ, typ: 'kb+jwt' }, corp, EM) + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /typ/), 'kb+jwt drept emitent a trecut'); + const simplu = jwtDin({ alg: PQ, typ: 'JWT' }, corp, EM) + '~'; cere(refuz(J.verifySdJwt(simplu, { issuerKey: iss, requireKeyBinding: false, now: LA }), /typ/), 'typ JWT a trecut implicit'); cere(J.verifySdJwt(simplu, { issuerKey: iss, requireKeyBinding: false, expectedTyp: null, now: LA }).valid, 'expectedTyp:null a refuzat un JWT simplu'); const bun = J.verifySdJwt(arata(E.sdJwt, ['age_over_18']), { issuerKey: iss, audience: AUD, nonce: NONCE, now: LA, expectedIssuer: 'https://issuer.aere.example' }); @@ -180,22 +189,24 @@ test('REVIZUIRE: tipul jetonului: un KB-JWT sau un JWT cu typ JWT dat drept jeto }); test('REVIZUIRE: crit (orice forma), base64url necanonic al semnaturii, KB expirat sau inca nevalabil (nbf) -> refuzate', () => { const corp = { iss: 'x', exp: NOW + 100, _sd_alg: 'sha-256', meta: { note: 1 } }; - for (const crit of [[], 'b64', ['b64']]) cere(refuz(J.verifySdJwt(jwtDin({ alg: 'EdDSA', typ: J.TYP, crit, b64: false }, corp, iss.privat.ed25519) + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /crit/), 'crit ' + JSON.stringify(crit)); + for (const crit of [[], 'b64', ['b64']]) cere(refuz(J.verifySdJwt(jwtDin({ alg: PQ, typ: J.TYP, crit, b64: false }, corp, EM) + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /crit/), 'crit ' + JSON.stringify(crit)); + // base64url necanonic: o semnatura Ed25519 (64 de octeti) are doi biti nefolositi in ultimul caracter; una ML-DSA-65 (3309 = 3 x 1103 + // octeti) nu are niciunul, deci proba ramane pe jumatatea clasica, ceruta explicit (AIP-25 C1) const j = jwtDin({ alg: 'EdDSA', typ: J.TYP }, corp, iss.privat.ed25519); - const r = J.verifySdJwt(j + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }); + const r = J.verifySdJwt(j + '~', { issuerKey: iss, classicalHalf: true, requireKeyBinding: false, now: LA }); cere(r.valid && r.payload.meta.note === 1 && !('_sd_alg' in r.payload), 'martorul: ' + JSON.stringify(r.payload)); const ALF = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; const u = j[j.length - 1], alt = ALF[ALF.indexOf(u) ^ 1]; const nec = j.slice(0, -1) + alt; cere(Buffer.from(nec.split('.')[2], 'base64url').equals(Buffer.from(j.split('.')[2], 'base64url')), 'proba: scrierea alternativa trebuie sa dea aceiasi octeti'); - cere(refuz(J.verifySdJwt(nec + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /canonical/), 'semnatura necanonica a trecut'); + cere(refuz(J.verifySdJwt(nec + '~', { issuerKey: iss, classicalHalf: true, requireKeyBinding: false, now: LA }), /canonical/), 'semnatura necanonica a trecut'); const baza = arata(E.sdJwt, ['age_over_18']).split('~').slice(0, -1).join('~') + '~'; const sh = crypto.createHash('sha256').update(Buffer.from(baza, 'ascii')).digest('base64url'); - const kbExp = jwtDin({ alg: 'EdDSA', typ: 'kb+jwt' }, { iat: NOW, aud: AUD, nonce: NONCE, sd_hash: sh, exp: NOW - 100 }, hol.privat.ed25519); + const kbExp = jwtDin({ alg: PQ, typ: 'kb+jwt' }, { iat: NOW, aud: AUD, nonce: NONCE, sd_hash: sh, exp: NOW - 100 }, HM); cere(refuz(judeca(baza + kbExp), /key binding: expired/), 'KB expirat a trecut'); // 2026-10-01: nbf-ul KB (a doua jumatate a reparatiei "exp/nbf ale KB" din B-32) nu avea proba: paznicul scos lasa proba 23/23. // Martorul aceluiasi KB facut de mana, cu nbf in toleranta de ceas, e VALID: refuzul de mai jos e al nbf-ului, nu al constructiei. - const kbNbf = (nbf) => jwtDin({ alg: 'EdDSA', typ: 'kb+jwt' }, { iat: NOW, aud: AUD, nonce: NONCE, sd_hash: sh, nbf }, hol.privat.ed25519); + const kbNbf = (nbf) => jwtDin({ alg: PQ, typ: 'kb+jwt' }, { iat: NOW, aud: AUD, nonce: NONCE, sd_hash: sh, nbf }, HM); const rNbfBun = judeca(baza + kbNbf(NOW + 30)); cere(rNbfBun.valid, 'martorul: KB cu nbf in toleranta (30 s) trebuie sa fie VALID: ' + rNbfBun.reason); cere(refuz(judeca(baza + kbNbf(NOW + 600)), /key binding: not valid before/), 'KB inca nevalabil (nbf peste 10 minute) a trecut'); @@ -205,7 +216,7 @@ test('REVIZUIRE: crit (orice forma), base64url necanonic al semnaturii, KB expir // 2026-10-18, din interoperarea cu @sd-jwt/core 0.23.0 (care refuza _sd_alg imbricat): RFC 9901 s.4.1.1 "It MUST NOT be used in any object // nested within the payload"; pana atunci il pastram ca afirmatie. Si RFC 7515 s.4.1.9: typ-ul "application/x+sd-jwt" e acelasi cu "x+sd-jwt". test('INTEROP: _sd_alg imbricat (in clar sau intr-o valoare dezvaluita) -> refuzat (RFC 9901 s.4.1.1); o dezvaluire numita _sd_alg -> refuzata; typ cu prefixul application/ -> acelasi tip (RFC 7515 s.4.1.9)', () => { - const cap = (corp, typ = J.TYP) => jwtDin({ alg: 'EdDSA', typ }, corp, iss.privat.ed25519); + const cap = (corp, typ = J.TYP) => jwtDin({ alg: PQ, typ }, corp, EM); const o = { issuerKey: iss, requireKeyBinding: false, now: LA }; cere(refuz(J.verifySdJwt(cap({ iss: 'x', exp: NOW + 100, _sd_alg: 'sha-256', meta: { _sd_alg: 'x', note: 1 } }) + '~', o), /_sd_alg below the top level/), 'imbricat in clar a trecut'); const dIm = b64u(JSON.stringify(['s1', 'meta', { _sd_alg: 'sha-256', note: 1 }])), dgIm = crypto.createHash('sha256').update(dIm).digest('base64url'); @@ -234,6 +245,8 @@ test(`INTEROP (fixturi @sd-jwt/core ${F2 ? F2.biblioteci['@sd-jwt/core'] : '?'} cere(r.valid, c.nume + ': ' + r.reason); cere(egal(r.payload, c.asteptat.payload) && egal([...r.disclosed].sort(), c.asteptat.disclosed), c.nume + ': rezultatul difera de cel inregistrat: ' + JSON.stringify(r.payload).slice(0, 200)); cere(r.payload.address && r.payload.address.locality === 'Cluj' && !('street_address' in r.payload.address) && egal(r.payload.nationalities, ['RO']) && !('given_name' in r.payload), c.nume + ': dezvaluirile imbricate/de lista nu sunt cele asteptate'); + // AIP-25 C1: cheia lor e un JWK simplu (nicio jumatate de pierdut); verdictul spune daca fiecare semnatura judecata e post-cuantica + cere(r.postQuantum === (r.alg === PQ && (!r.keyBinding || r.keyBinding.alg === PQ)), c.nume + ': postQuantum=' + r.postQuantum + ' pentru ' + r.alg + '/' + (r.keyBinding && r.keyBinding.alg)); valide++; } else { cere(refuz(r, new RegExp(c.asteptat.motiv)), c.nume + ': asteptat refuz /' + c.asteptat.motiv + '/, primit ' + (r.valid ? 'VALID' : r.reason)); refuzate++; } } @@ -244,6 +257,73 @@ test(`INTEROP (fixturi @sd-jwt/core ${F2 ? F2.biblioteci['@sd-jwt/core'] : '?'} cere(faraTyp && refuz(J.verifySdJwt(faraTyp.prezentare, { issuerKey: faraTyp.issuerJwk, issuerAlg: faraTyp.alg, audience: F2.audience, nonce: F2.nonce, now: la }), /typ/), 'jetonul lor fara typ a trecut de tipul implicit'); }); +// ---------------------------------------------------------------- AIP-25 C1 (D-535, 2026-10-19): jumatatea clasica a unei identitati hibride +// Cheile unei identitati AERE sunt Ed25519 SI ML-DSA-65 (identity.mjs cere amandoua semnaturile). Pana pe 19 oct verificatorul SD-JWT alegea +// IMPLICIT jumatatea Ed25519, deci cine spargea NUMAI cheia clasica a emitentului facea credentiale primite contra identitatii lui intregi. +const corpE = corpDe(E.sdJwt); +const dezvE = E.sdJwt.split('~').slice(1).join('~'); // dezvaluirile lui E, fiecare urmata de ~ +// ce poate face cine are NUMAI cheia Ed25519 a emitentului: aceleasi afirmatii, semnate cu ea +const doarClasic = (alg = 'EdDSA', corp = corpE, cheie = iss.privat.ed25519) => jwtDin({ alg, typ: J.TYP }, corp, cheie) + '~' + dezvE; +const nuArunca = (f) => { try { f(); return null; } catch (e) { return String(e.message || e); } }; +test('AIP-25 C1: un SD-JWT semnat NUMAI cu jumatatea Ed25519 a emitentului AERE (ce face cine a spart doar cheia clasica), judecat implicit contra cheilor AERE -> REFUZAT pe regula hybrid-half, postQuantum false; acelasi credential cinstit -> VALID, postQuantum true', () => { + const atac = judeca(arata(doarClasic(), ['age_over_18'])); + cere(!atac.valid && atac.rule === 'hybrid-half' && atac.postQuantum === false, 'jetonul doar-Ed25519, cheile AERE private: ' + (atac.valid ? 'VALID' : atac.rule + ': ' + atac.reason)); + const pub = judeca(arata(doarClasic(), ['age_over_18']), { issuerKey: iss.public }); + cere(!pub.valid && pub.rule === 'hybrid-half' && /Ed25519 half/.test(pub.reason), 'contra identitatii AERE PUBLICE (keys.public): ' + (pub.valid ? 'VALID' : pub.rule)); + const plin = judeca(arata(doarClasic('Ed25519'), ['age_over_18'])); + cere(!plin.valid && plin.rule === 'hybrid-half', 'antetul Ed25519 (numele complet din RFC 9864): ' + (plin.valid ? 'VALID' : plin.rule)); + const bun = judeca(arata(E.sdJwt, ['age_over_18'])); + cere(bun.valid && bun.alg === PQ && bun.postQuantum === true, 'credentialul cinstit: ' + (bun.valid ? bun.alg + ' pq=' + bun.postQuantum : bun.reason)); +}); +test('AIP-25 C1: jumatatea clasica se judeca NUMAI la cerere: issuerAlg EdDSA fara classicalHalf -> hybrid-half; classicalHalf:true -> VALID cu postQuantum false (verdictul o spune); classicalHalf primeste si jetonul ML-DSA-65, cu postQuantum true; classicalHalf + issuerAlg ML-DSA-65 pe jetonul clasic -> hybrid-half', () => { + const p = arata(doarClasic(), ['age_over_18']); + const a = judeca(p, { issuerAlg: 'EdDSA' }); cere(!a.valid && a.rule === 'hybrid-half', 'issuerAlg EdDSA fara classicalHalf: ' + (a.valid ? 'VALID' : a.rule)); + const b = judeca(p, { classicalHalf: true }); cere(b.valid && b.alg === 'EdDSA' && b.postQuantum === false, 'classicalHalf: ' + (b.valid ? 'pq=' + b.postQuantum : b.reason)); + const c = judeca(p, { classicalHalf: true, issuerAlg: 'EdDSA' }); cere(c.valid && c.postQuantum === false, 'classicalHalf + issuerAlg EdDSA: ' + (c.valid ? 'pq=' + c.postQuantum : c.reason)); + const d = judeca(arata(E.sdJwt, ['age_over_18']), { classicalHalf: true }); cere(d.valid && d.alg === PQ && d.postQuantum === true, 'classicalHalf pe jetonul post-cuantic: ' + (d.valid ? d.alg + ' pq=' + d.postQuantum : d.reason)); + const e = judeca(p, { classicalHalf: true, issuerAlg: PQ }); cere(!e.valid && e.rule === 'hybrid-half', 'classicalHalf + issuerAlg ML-DSA-65 pe jetonul clasic: ' + (e.valid ? 'VALID' : e.rule)); +}); +test('AIP-25 C1: emiterea implicita cu chei AERE e post-cuantica (antet ML-DSA-65, cnf.jwk AKP ML-DSA-65, KB ML-DSA-65); EdDSA la emitere sau in cnf cere classicalHalf; publicJwk(chei AERE) = jumatatea ML-DSA-65, cea Ed25519 numai cu { classicalHalf: true }, ES256 deloc', () => { + const h = JSON.parse(Buffer.from(E.sdJwt.split('.')[0], 'base64url').toString('utf8')); + cere(h.alg === PQ && corpE.cnf.jwk.kty === 'AKP' && corpE.cnf.jwk.alg === PQ, 'emiterea implicita: ' + JSON.stringify(h) + ' cnf ' + (corpE.cnf.jwk.kty + '/' + corpE.cnf.jwk.alg)); + const kb = arata(E.sdJwt, ['age_over_18']).split('~').pop(); const hk = JSON.parse(Buffer.from(kb.split('.')[0], 'base64url').toString('utf8')); + cere(hk.alg === PQ, 'Key Binding-ul implicit: ' + JSON.stringify(hk)); + cere(/classicalHalf/.test(nuArunca(() => emite({ alg: 'EdDSA' })) || ''), 'emiterea EdDSA fara classicalHalf a tacut'); + cere(/classicalHalf/.test(nuArunca(() => emite({ holderAlg: 'EdDSA' })) || ''), 'cnf Ed25519 fara classicalHalf a tacut'); + const C = emite({ alg: 'EdDSA', holderAlg: 'EdDSA', classicalHalf: true }); + const rc = judeca(arata(C.sdJwt, ['age_over_18']), { classicalHalf: true }); + cere(rc.valid && rc.alg === 'EdDSA' && rc.keyBinding.alg === 'EdDSA' && rc.postQuantum === false, 'emiterea clasica ceruta: ' + (rc.valid ? 'pq=' + rc.postQuantum : rc.reason)); + cere(J.publicJwk(iss).kty === 'AKP' && J.publicJwk(iss.public).kty === 'AKP' && J.publicJwk(iss.public, PQ).pub === J.publicJwk(iss).pub, 'publicJwk implicit'); + cere(/classicalHalf/.test(nuArunca(() => J.publicJwk(iss, 'EdDSA')) || '') && /classicalHalf/.test(nuArunca(() => J.publicJwk(iss.public, 'Ed25519')) || ''), 'publicJwk EdDSA/Ed25519 fara classicalHalf a tacut'); + cere(J.publicJwk(iss, 'EdDSA', { classicalHalf: true }).crv === 'Ed25519', 'publicJwk EdDSA cu classicalHalf'); + cere(/no ES256 key/.test(nuArunca(() => J.publicJwk(iss, 'ES256')) || ''), 'publicJwk ES256 pe chei AERE a dat o cheie'); +}); +test('AIP-25 C1: un JWK simplu ramane cum e (nicio jumatate de pierdut), iar verdictul spune postQuantum: Ed25519 OKP -> VALID postQuantum false; AKP ML-DSA-65 -> VALID postQuantum true; ES256 al standardului -> postQuantum false; un Key Binding clasic peste un emitent ML-DSA-65 -> postQuantum false', () => { + const okp = judeca(arata(doarClasic(), ['age_over_18']), { issuerKey: J.publicJwk(iss, 'EdDSA', { classicalHalf: true }) }); + cere(okp.valid && okp.alg === 'EdDSA' && okp.postQuantum === false, 'OKP simplu: ' + (okp.valid ? 'pq=' + okp.postQuantum : okp.reason)); + const akp = judeca(arata(E.sdJwt, ['age_over_18']), { issuerKey: J.publicJwk(iss) }); + cere(akp.valid && akp.postQuantum === true, 'AKP simplu: ' + (akp.valid ? 'pq=' + akp.postQuantum : akp.reason)); + const rfc = J.verifySdJwt(F.presentation, { issuerKey: F.issuerJwk, audience: KB.aud, nonce: KB.nonce, now: LA_KB }); + cere(rfc.valid && rfc.alg === 'ES256' && rfc.postQuantum === false, 'ES256 al standardului: ' + (rfc.valid ? 'pq=' + rfc.postQuantum : rfc.reason)); + const K = emite({ holderAlg: 'EdDSA', classicalHalf: true }); const rk = judeca(arata(K.sdJwt, ['age_over_18'])); + cere(rk.valid && rk.alg === PQ && rk.keyBinding.alg === 'EdDSA' && rk.postQuantum === false, 'KB clasic peste emitent ML-DSA-65: ' + (rk.valid ? rk.alg + '/' + rk.keyBinding.alg + ' pq=' + rk.postQuantum : rk.reason)); + const refuzat = judeca(arata(doarClasic(), ['age_over_18']), { audience: 'https://alt.example' }); + cere(!refuzat.valid && refuzat.postQuantum === false, 'un refuz nu spune postQuantum true: ' + refuzat.postQuantum); +}); +test('AIP-25 C1 ATAC: alg none sau ES256 contra cheilor AERE (si cu classicalHalf) -> refuzate pe alg / key; chei AERE amestecate (Ed25519 al strainului langa ML-DSA-65 al emitentului) nu primesc implicit jetonul doar-Ed25519 al strainului; chei AERE fara jumatatea ML-DSA-65 -> key, nu o cadere si nu jumatatea Ed25519', () => { + const none = b64u(JSON.stringify({ alg: 'none', typ: J.TYP })) + '.' + b64u(JSON.stringify(corpE)) + '.~' + dezvE; + for (const ch of [false, true]) { + const n = J.verifySdJwt(none, { issuerKey: iss, requireKeyBinding: false, classicalHalf: ch, now: LA }); cere(!n.valid && n.rule === 'alg', 'none, classicalHalf ' + ch + ': ' + (n.valid ? 'VALID' : n.rule)); + const es = J.verifySdJwt(doarClasic('ES256'), { issuerKey: iss, requireKeyBinding: false, classicalHalf: ch, now: LA }); cere(!es.valid && es.rule === 'key', 'ES256 cu cheia Ed25519, classicalHalf ' + ch + ': ' + (es.valid ? 'VALID' : es.rule)); + } + const amestec = { alg: iss.public.alg, ed25519: strain.public.ed25519, mldsa65: iss.public.mldsa65 }; + const s = J.verifySdJwt(doarClasic('EdDSA', corpE, strain.privat.ed25519), { issuerKey: amestec, requireKeyBinding: false, now: LA }); + cere(!s.valid && s.rule === 'hybrid-half', 'chei amestecate, implicit: ' + (s.valid ? 'VALID' : s.rule)); + const faraMl = { alg: iss.public.alg, ed25519: iss.public.ed25519 }; + const fm = J.verifySdJwt(arata(E.sdJwt, ['age_over_18']), { issuerKey: faraMl, audience: AUD, nonce: NONCE, now: LA }); + cere(!fm.valid && fm.rule === 'key', 'chei AERE fara ML-DSA-65: ' + (fm.valid ? 'VALID (pe jumatatea Ed25519)' : fm.rule + ': ' + fm.reason)); +}); + test('linia de comanda: verify-sdjwt pe prezentarea RFC 9901 s.5.2 -> 0 VALID la momentul ei (--at), 1 INVALID acum (KB vechi); o cheie privata -> 2', () => { const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-sdjwt-cli-')); try { @@ -255,6 +335,21 @@ test('linia de comanda: verify-sdjwt pe prezentarea RFC 9901 s.5.2 -> 0 VALID la const c = run(); cere(c.status === 2 && /private JWK/.test(c.stderr), 'cheia privata: ' + c.status + ' ' + c.stderr); } finally { fs.rmSync(T, { recursive: true, force: true }); } }); +test('linia de comanda AIP-25 C1: verify-sdjwt cu cheile AERE publice ale emitentului: jetonul doar-Ed25519 -> 1 INVALID (hybrid-half), si cu --issuer-alg EdDSA fara --classical-half; cu --classical-half -> 0 VALID, postQuantum false in --json si "NOT post-quantum" in text; jetonul implicit -> 0, postQuantum true', () => { + const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-sdjwt-cli-c1-')); + try { + fs.writeFileSync(path.join(T, 'clasic.txt'), arata(doarClasic(), ['age_over_18'])); fs.writeFileSync(path.join(T, 'pq.txt'), arata(E.sdJwt, ['age_over_18'])); + fs.writeFileSync(path.join(T, 'k.json'), JSON.stringify(iss.public)); + const run = (f, ...a) => spawnSync(process.execPath, [path.join(AICI, 'identity-cli.mjs'), 'verify-sdjwt', '--sd-jwt', f, '--issuer-key', 'k.json', '--audience', AUD, '--nonce', NONCE, '--at', LA.toISOString(), ...a], { cwd: T, encoding: 'utf8' }); + const j = (r) => { try { return JSON.parse(r.stdout); } catch { return {}; } }; + const a = run('clasic.txt', '--json'); cere(a.status === 1 && j(a).rule === 'hybrid-half' && j(a).postQuantum === false, 'doar-Ed25519 implicit: ' + a.status + ' ' + (a.stdout + a.stderr).slice(0, 200)); + const b = run('clasic.txt', '--issuer-alg', 'EdDSA', '--json'); cere(b.status === 1 && j(b).rule === 'hybrid-half', '--issuer-alg EdDSA fara --classical-half: ' + b.status + ' ' + (b.stdout + b.stderr).slice(0, 200)); + const c = run('clasic.txt', '--classical-half', '--json'); cere(c.status === 0 && j(c).valid === true && j(c).postQuantum === false, '--classical-half: ' + c.status + ' ' + (c.stdout + c.stderr).slice(0, 200)); + const d = run('clasic.txt', '--classical-half'); cere(d.status === 0 && /NOT post-quantum/.test(d.stdout), 'textul cu --classical-half: ' + d.stdout.slice(0, 200)); + const e = run('pq.txt', '--json'); cere(e.status === 0 && j(e).postQuantum === true && j(e).alg === PQ, 'jetonul implicit: ' + e.status + ' ' + (e.stdout + e.stderr).slice(0, 200)); + const g = run('pq.txt'); cere(g.status === 0 && /post-quantum/.test(g.stdout) && !/NOT post-quantum/.test(g.stdout), 'textul pe jetonul implicit: ' + g.stdout.slice(0, 200)); + } finally { fs.rmSync(T, { recursive: true, force: true }); } +}); console.log(`\naere-sd-jwt: ${treceri}/${treceri + esecuri.length} cum trebuia`); process.exitCode = esecuri.length ? 1 : 0; diff --git a/identity/sdjwt-vc.mjs b/identity/sdjwt-vc.mjs index b2f1acf..9a9a80d 100644 --- a/identity/sdjwt-vc.mjs +++ b/identity/sdjwt-vc.mjs @@ -18,8 +18,12 @@ // CE NU FACE: nu aduce nimic de pe internet (nici metadatele de tip ale lui `vct`, s.5, nici lista de stare: verificatorul le aduce si le da, // ca `--status-list` la credentialul AERE); nu rezolva cheia emitentului din `iss` (s.2.5: JWT VC Issuer Metadata sau x5c): cheia e intrarea // verificatorului, niciodata din jeton; nu e mdoc (ISO/IEC 18013-5, CBOR/COSE): formatul de proximitate al ARF e alt format, nefacut aici. +// +// AIP-25 C1 (2026-10-19, D-535): lista de stare a unui emitent AERE se semneaza si se judeca implicit cu jumatatea ML-DSA-65 a cheilor lui, +// ca jetonul (sdjwt.mjs); jumatatea Ed25519 numai cu classicalHalf: true. Altfel cine spargea numai cheia clasica putea "dezrevoca" un +// credential cu o lista semnata de ea. `postQuantum` al verdictului VC e true numai daca si lista judecata e ML-DSA-65. import zlib from 'node:zlib'; -import { issueSdJwt, presentSdJwt, verifySdJwt, publicJwk, verifyJws, signJws, privateKeyOf, ALGS } from './sdjwt.mjs'; +import { issueSdJwt, presentSdJwt, verifySdJwt, verifyJws, signJws, privateKeyOf, judgedJwk, headerAlgOf, defaultAlg, ALGS } from './sdjwt.mjs'; export const TYP_VC = 'dc+sd-jwt'; export const TYP_STATUS_LIST = 'statuslist+jwt'; @@ -66,8 +70,9 @@ export function issueSdJwtVc({ vct, vctIntegrity, status, claims = {}, disclosab * fiecare sub 2^bits; bits din {1, 2, 4, 8}. Impachetarea (s.4.1): indexul i sta la octetul floor(i*bits/8), de la bitul (i*bits) mod 8, adica * de la bitul cel mai putin semnificativ catre cel mai semnificativ. lst = base64url(ZLIB(DEFLATE(octeti))). */ -export function issueStatusListJwt({ issuer, alg = 'EdDSA', uri, statuses, bits = 1, iat = Math.floor(Date.now() / 1000), exp, ttl, aggregationUri }) { - if (!ALGS.includes(alg)) throw new Error('status-list: unknown alg'); +export function issueStatusListJwt({ issuer, alg, classicalHalf = false, uri, statuses, bits = 1, iat = Math.floor(Date.now() / 1000), exp, ttl, aggregationUri }) { + const algL = alg ?? defaultAlg(issuer); // AIP-25 C1: ML-DSA-65 pentru chei AERE + if (!ALGS.includes(algL)) throw new Error('status-list: unknown alg'); if (typeof uri !== 'string' || !uri) throw new Error('status-list: uri (the sub of the token) is required'); if (![1, 2, 4, 8].includes(bits)) throw new Error('status-list: bits is 1, 2, 4 or 8'); if (!Array.isArray(statuses) || !statuses.length || !statuses.every((s) => Number.isInteger(s) && s >= 0 && s < 2 ** bits)) throw new Error(`status-list: statuses is a non-empty list of integers below 2^${bits}`); @@ -75,20 +80,22 @@ export function issueStatusListJwt({ issuer, alg = 'EdDSA', uri, statuses, bits statuses.forEach((s, i) => { octeti[Math.floor(i * bits / 8)] |= s << ((i * bits) % 8); }); const lst = b64u(zlib.deflateSync(octeti, { level: 9 })); const corp = { sub: uri, iat, ...(exp != null ? { exp } : {}), ...(ttl != null ? { ttl } : {}), status_list: { bits, lst, ...(aggregationUri ? { aggregation_uri: aggregationUri } : {}) } }; - return signJws({ alg, typ: TYP_STATUS_LIST }, corp, privateKeyOf(issuer, alg)); + return signJws({ alg: algL, typ: TYP_STATUS_LIST }, corp, privateKeyOf(issuer, algL, classicalHalf)); } /** * Citeste si verifica un Status List Token JWT cu cheia DATA (a emitentului listei; de obicei emitentul credentialului), pentru URI-ul cerut. - * Intoarce { ok, reason, entries, bits, statusAt(idx) -> 0..2^bits-1 sau null (dincolo de lista), payload }. Toate verificarile de aici + * Cu chei AERE se judeca implicit jumatatea ML-DSA-65; jumatatea Ed25519 numai cu classicalHalf (AIP-25 C1). + * Intoarce { ok, reason, entries, bits, statusAt(idx) -> 0..2^bits-1 sau null (dincolo de lista), payload, alg }. Toate verificarile de aici * sunt regula `status-list` a profilului. */ -export function readStatusListJwt(jwt, { key, alg = 'EdDSA', uri, now = new Date(), clockSkewS = 60 } = {}) { - const nu = (reason) => ({ ok: false, reason, entries: 0, bits: 0, statusAt: () => null, payload: null }); +export function readStatusListJwt(jwt, { key, alg, classicalHalf = false, uri, now = new Date(), clockSkewS = 60 } = {}) { + const nu = (reason) => ({ ok: false, reason, entries: 0, bits: 0, statusAt: () => null, payload: null, alg: null }); const motiv = (e) => String((e && e.message) || e).replace(/^sd-jwt: /, ''); - // semnatura, typ-ul si potrivirea alg/cheie le judeca verifyJws (o lista semnata ML-DSA-65 judecata cu cheia Ed25519 cade acolo: "does not match") - let corp = {}; - try { ({ corp } = verifyJws(jwt, publicJwk(key, alg), { typ: TYP_STATUS_LIST })); } catch (e) { if (!(false /*PAZA:status-list*/)) return nu(motiv(e)); } + // semnatura, typ-ul si potrivirea alg/cheie le judeca verifyJws (o lista semnata ML-DSA-65 judecata cu cheia Ed25519 cade acolo: "does not match"); + // jumatatea cheilor AERE o alege judgedJwk (o lista semnata numai cu jumatatea Ed25519 cade acolo, "Ed25519 half", fara classicalHalf) + let corp = {}, antet = {}; + try { ({ antet, corp } = verifyJws(jwt, judgedJwk(key, alg, { classicalHalf, headerAlg: headerAlgOf(jwt) }), { typ: TYP_STATUS_LIST })); } catch (e) { if (!(false /*PAZA:status-list*/)) return nu(motiv(e)); } if (!(typeof uri === 'string' && !!uri /*PAZA:status-list*/)) return nu('the verifier must give the uri the list was fetched for'); if (!(corp.sub === uri /*PAZA:status-list*/)) return nu(`sub ${JSON.stringify(corp.sub)} is not the uri referenced (${uri})`); const acum = Math.floor(new Date(now).getTime() / 1000); @@ -104,7 +111,7 @@ export function readStatusListJwt(jwt, { key, alg = 'EdDSA', uri, now = new Date } catch (e) { if (!(false /*PAZA:status-list*/)) return nu('lst cannot be inflated: ' + String(e.message || e).slice(0, 80)); } const bits = obiect(sl) && [1, 2, 4, 8].includes(sl.bits) ? sl.bits : 1, entries = Math.floor(octeti.length * 8 / bits); const statusAt = (idx) => (Number.isInteger(idx) && idx >= 0 && idx < entries ? (octeti[Math.floor(idx * bits / 8)] >> ((idx * bits) % 8)) & ((1 << bits) - 1) : null); - return { ok: true, reason: '', entries, bits, statusAt, payload: corp }; + return { ok: true, reason: '', entries, bits, statusAt, payload: corp, alg: antet.alg }; } /** @@ -116,7 +123,7 @@ export function readStatusListJwt(jwt, { key, alg = 'EdDSA', uri, now = new Date * regula care a refuzat (a profilului sau a lui sdjwt.mjs), null cand e valid. */ export function verifySdJwtVc(token, { expectedVct, statusListJwt = null, statusListKey = null, statusListAlg = null, requireStatus = true, expectedTyp = TYP_VC, ...o } = {}) { - const refuz = (rule, reason, extra = {}) => ({ valid: false, rule, reason, payload: null, disclosed: [], keyBinding: null, vct: null, status: null, ...extra }); + const refuz = (rule, reason, extra = {}) => ({ valid: false, rule, reason, payload: null, disclosed: [], keyBinding: null, postQuantum: false, vct: null, status: null, ...extra }); const vcts = Array.isArray(expectedVct) ? expectedVct : expectedVct == null ? null : [expectedVct]; if (!(Array.isArray(vcts) && vcts.length > 0 && vcts.every((v) => typeof v === 'string' && !!v) /*PAZA:vct-expected*/)) return refuz('vct-expected', 'the verifier must name the vct it expects (expectedVct)'); const tipuri = Array.isArray(vcts) ? vcts : []; @@ -128,7 +135,7 @@ export function verifySdJwtVc(token, { expectedVct, statusListJwt = null, status const dez = r.disclosed.filter((n) => NEDEZVALUIBILE.includes(n)); if (!(dez.length === 0 /*PAZA:disclosed-protected*/)) return refuz('disclosed-protected', `${dez[0]} was selectively disclosed; an SD-JWT VC carries it in the clear (s.2.2.2.3)`); if (!(p['vct#integrity'] === undefined || typeof p['vct#integrity'] === 'string' /*PAZA:form*/)) return refuz('form', 'vct#integrity is not a string'); - let status = { present: false, judged: false }; + let status = { present: false, judged: false }, pqLista = true; if (p.status !== undefined) { if (!(obiect(p.status) /*PAZA:form*/)) return refuz('form', 'status is not an object'); const ref = obiect(p.status) ? p.status.status_list : undefined; @@ -138,7 +145,9 @@ export function verifySdJwtVc(token, { expectedVct, statusListJwt = null, status if (!(e === null /*PAZA:form*/)) return refuz('form', 'status.status_list: ' + e); status.uri = ref.uri; status.idx = ref.idx; if (statusListJwt != null) { - const l = readStatusListJwt(statusListJwt, { key: statusListKey ?? o.issuerKey, alg: statusListAlg ?? o.issuerAlg ?? 'EdDSA', uri: ref.uri, now: o.now, clockSkewS: o.clockSkewS }); + // AIP-25 C1: fara statusListAlg, o lista a unei chei AERE se judeca pe jumatatea ML-DSA-65 (Ed25519 numai cu classicalHalf) + const l = readStatusListJwt(statusListJwt, { key: statusListKey ?? o.issuerKey, alg: statusListAlg ?? o.issuerAlg, classicalHalf: o.classicalHalf, uri: ref.uri, now: o.now, clockSkewS: o.clockSkewS }); + pqLista = l.alg === 'ML-DSA-65'; if (!(l.ok /*PAZA:status-list*/)) return refuz('status-list', 'status list: ' + l.reason, { vct: p.vct, status }); const v = l.statusAt(ref.idx); if (!(v !== null /*PAZA:status-index*/)) return refuz('status-index', `status list: index ${ref.idx} is beyond the list (${l.entries} entries)`, { vct: p.vct, status }); @@ -148,5 +157,6 @@ export function verifySdJwtVc(token, { expectedVct, statusListJwt = null, status } if (!(status.judged || !requireStatus /*PAZA:status-unjudged*/)) return refuz('status-unjudged', `the credential names a status (${status.mechanism}) that was not judged: give statusListJwt fetched from ${status.uri ?? 'the issuer'}, or set requireStatus:false`, { vct: p.vct, status }); } - return { ...r, vct: p.vct, status }; + // AIP-25 C1: post-cuantic numai daca si lista de stare judecata (cand a fost judecata) e semnata ML-DSA-65 + return { ...r, postQuantum: r.postQuantum && pqLista, vct: p.vct, status }; } diff --git a/identity/sdjwt.mjs b/identity/sdjwt.mjs index d3f59bc..0273dfe 100644 --- a/identity/sdjwt.mjs +++ b/identity/sdjwt.mjs @@ -11,9 +11,20 @@ // nonce-ul lui CERUTE cand exista (sau se cere) Key Binding, cu `exp` cerut, cu tipul explicit al jetonului verificat // // ALGORITMII (JWS): ES256 (P-256, cel din exemplele RFC 9901), EdDSA / Ed25519 (RFC 8037) si ML-DSA-65 (post-cuantic; numele si forma -// cheii JWK "AKP" din RFC 9964, mai 2026). Un emitent AERE semneaza cu cheia lui Ed25519 (algoritm cunoscut tuturor bibliotecilor JOSE) -// sau cu cheia ML-DSA-65 (masurat 2026-10-18: jose 6.2.12 pe Node 24 o semneaza si o verifica); o semnatura hibrida in acelasi jeton ar -// cere serializarea JSON generala a JWS si nu e facuta aici. +// cheii JWK "AKP" din RFC 9964, mai 2026). O semnatura hibrida in acelasi jeton ar cere serializarea JSON generala a JWS si nu e facuta aici. +// +// JUMATATEA POST-CUANTICA (2026-10-19, AIP-25 constatarea C1, D-535): cheile unei identitati AERE sunt Ed25519 SI ML-DSA-65 (identity.mjs +// cere amandoua semnaturile). Un JWS poarta UNA. Pana pe 19 oct emiterea, cnf-ul detinatorului si verificarea alegeau implicit jumatatea +// Ed25519, deci cine spargea NUMAI cheia clasica a emitentului facea credentiale primite contra identitatii lui intregi (masurat de +// aips/aip25-inventar/masoara-constatari.mjs). De atunci, cu chei AERE (perechea privata, sau cheile publice {alg, ed25519, mldsa65}): +// - emiterea (alg), cnf-ul detinatorului (holderAlg), Key Binding-ul si lista de stare (sdjwt-vc.mjs) folosesc implicit ML-DSA-65; +// - verificarea cere implicit jumatatea ML-DSA-65; un jeton semnat cu jumatatea Ed25519 e refuzat pe regula `hybrid-half`; +// - jumatatea Ed25519 se foloseste si se judeca NUMAI la cererea explicita a apelantului (`classicalHalf: true`, pentru interoperarea +// clasica), si atunci verificatorul primeste jumatatea pe care o numeste jetonul (EdDSA sau ML-DSA-65); +// - verdictul spune `postQuantum`: true numai cand FIECARE semnatura judecata (a emitentului si, daca exista, Key Binding-ul) e ML-DSA-65. +// Un JWK simplu (o cheie straina, ES256 sau EdDSA, @sd-jwt/core, portofelele europene) ramane cum era: nu exista o jumatate post-cuantica +// de pierdut, iar verdictul spune la fel postQuantum false. Cine isi transforma singur cheile AERE intr-un JWK Ed25519 simplu +// (publicJwk(chei, 'EdDSA', { classicalHalf: true })) a cerut jumatatea clasica, si verdictul o spune. // // Revizuirea adversariala din 2026-09-30 (inainte de publicare) a gasit si s-au reparat: emitentul semna structura SD-JWT venita in valorile // afirmatiilor (`_sd`, `{"...": d}`), deci detinatorul putea dezvalui afirmatii pe care emitentul nu le-a vazut; prezentarea arata un element @@ -34,8 +45,10 @@ export const ALGS = ['ES256', 'EdDSA', 'Ed25519', 'ML-DSA-65']; export const TYP = 'aere+sd-jwt'; const AERE_ALG = 'ed25519+ml-dsa-65'; /** regulile pe care le poate numi un refuz al lui verifySdJwt (ordinea e cea a verificarii; `form` si pentru ceasul verificatorului) */ -export const REGULI = ['form', 'alg', 'typ', 'crit', 'key', 'signature', 'issuer', 'sd-alg', 'disclosure-form', 'disclosure-twice', 'digest-twice', +export const REGULI = ['form', 'alg', 'typ', 'crit', 'key', 'hybrid-half', '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']; +// numele JOSE ale semnaturii Ed25519: EdDSA (RFC 8037) si Ed25519 (numele complet, RFC 9864) +const CLASIC = ['EdDSA', 'Ed25519']; class Refuz extends Error { constructor(rule, message) { super(message); this.rule = rule; } } const nu = (rule, msg) => { if (!REGULI.includes(rule)) throw new Error('sd-jwt: undeclared rule ' + rule); return new Refuz(rule, 'sd-jwt: ' + msg); }; // base64url CANONIC: decodat si recodat trebuie sa dea acelasi text (altfel aceeasi semnatura are mai multe scrieri, si un depozit de @@ -51,18 +64,54 @@ const sha256b64u = (s) => crypto.createHash('sha256').update(Buffer.from(s, 'asc const obiect = (x) => x !== null && typeof x === 'object' && !Array.isArray(x); // ---------------------------------------------------------------- chei JWK si semnaturi JWS +/** Chei AERE: perechea (identity.mjs, cu `privat`) sau cheile publice ale unei identitati ({alg: 'ed25519+ml-dsa-65', ed25519, mldsa65}). */ +const esteAere = (k) => !!k && !(k instanceof crypto.KeyObject) && (obiect(k.privat) || (obiect(k) && k.alg === AERE_ALG)); +/** Algoritmul implicit al unei chei la EMITERE: ML-DSA-65 pentru chei AERE (AIP-25 C1), EdDSA altfel (ca pana acum). */ +export const defaultAlg = (k) => (esteAere(k) ? 'ML-DSA-65' : 'EdDSA'); +/** + * Jumatatea unei chei AERE ceruta de `alg` (implicit ML-DSA-65). Jumatatea Ed25519 NUMAI cu `classicalHalf` (AIP-25 C1): altfel refuz + * `hybrid-half`. Un algoritm pe care o identitate AERE nu il are (ES256) e refuz `key`, nu cheia Ed25519 data in tacere (pana pe 19 oct). + */ +function jumatateaAere(alg, classicalHalf) { + const a = alg == null ? 'ML-DSA-65' : alg; + if (a === 'ML-DSA-65') return 'mldsa65'; + if (!CLASIC.includes(a)) throw nu('key', `an AERE hybrid identity has an Ed25519 and an ML-DSA-65 key, no ${a} key`); + if (!(classicalHalf === true /*PAZA:hybrid-half*/)) throw nu('hybrid-half', `${a} names the Ed25519 half of an AERE hybrid identity (Ed25519 + ML-DSA-65); that half alone is not post-quantum and is used only when the caller asks for it (classicalHalf: true); the post-quantum half is ML-DSA-65, the default`); + return 'ed25519'; +} /** * Cheia PUBLICA ca JWK, din: o pereche de chei AERE (identity.mjs; `privat`), cheile publice ale unei identitati AERE ({alg, ed25519, - * mldsa65}), un KeyObject public sau privat, sau un JWK public. `alg` alege cheia AERE: 'EdDSA' (Ed25519) sau 'ML-DSA-65'. + * mldsa65}), un KeyObject public sau privat, sau un JWK public. Pentru chei AERE, `alg` alege jumatatea: implicit 'ML-DSA-65'; 'EdDSA' / + * 'Ed25519' (jumatatea clasica) numai cu { classicalHalf: true } (AIP-25 C1). Pentru celelalte chei `alg` nu conteaza (cheia e cea data). */ -export function publicJwk(k, alg = 'EdDSA') { - const ml = alg === 'ML-DSA-65'; - if (k && k.privat) k = ml ? k.privat.mldsa65 : k.privat.ed25519; - else if (obiect(k) && k.alg === AERE_ALG && typeof k.ed25519 === 'string') k = crypto.createPublicKey({ key: Buffer.from(ml ? k.mldsa65 : k.ed25519, 'base64'), format: 'der', type: 'spki' }); +export function publicJwk(k, alg, { classicalHalf = false } = {}) { + if (esteAere(k)) { + const j = jumatateaAere(alg, classicalHalf); + let x = null; + try { x = k.privat ? k.privat[j] : crypto.createPublicKey({ key: Buffer.from(k[j], 'base64'), format: 'der', type: 'spki' }); } catch { x = null; } + if (!(x instanceof crypto.KeyObject)) throw nu('key', `the ${j === 'mldsa65' ? 'ML-DSA-65' : 'Ed25519'} half of the AERE identity keys cannot be read`); + k = x; + } if (k instanceof crypto.KeyObject) return (k.type === 'public' ? k : crypto.createPublicKey(k)).export({ format: 'jwk' }); if (obiect(k) && k.kty) return k; throw nu('key', 'not a key (a JWK, a Node key object, or AERE identity keys)'); } +/** + * Cheia cu care se judeca un JWS semnat de cheia `k` a emitentului (sau a listei de stare), data de verificator (AIP-25 C1, 2026-10-19). + * O cheie care nu e AERE: ea insasi, ca pana acum. Chei AERE: jumatatea `alg` daca verificatorul o numeste; altfel ML-DSA-65, sau, numai + * cu `classicalHalf`, jumatatea pe care o numeste antetul (EdDSA / Ed25519; verdictul spune atunci postQuantum false). Un jeton semnat cu + * jumatatea Ed25519, judecat de un verificator care cere jumatatea ML-DSA-65, e refuzat pe NUMELE lui (`hybrid-half`), nu ca "alta cheie". + */ +export function judgedJwk(k, alg, { classicalHalf = false, headerAlg } = {}) { + if (!esteAere(k)) return publicJwk(k, alg); + const a = alg != null ? alg : classicalHalf === true && CLASIC.includes(headerAlg) ? headerAlg : 'ML-DSA-65'; + if (!(!(a === 'ML-DSA-65' && CLASIC.includes(headerAlg)) /*PAZA:hybrid-half*/)) throw nu('hybrid-half', `the JWS is signed ${headerAlg} with the Ed25519 half of an AERE hybrid identity alone (Ed25519 + ML-DSA-65): that half is not post-quantum, so it is judged only when the verifier asks for it (classicalHalf: true); by default the ML-DSA-65 half is required`); + return publicJwk(k, a, { classicalHalf }); +} +/** Algoritmul din antetul unui JWS compact, citit fara sa fie crezut (numai ca sa se numeasca un refuz); undefined daca nu se citeste. */ +export function headerAlgOf(jws) { + try { const h = jsonB64u(String(jws).split('.')[0], 'the JWS header'); return obiect(h) && typeof h.alg === 'string' ? h.alg : undefined; } catch { return undefined; } +} function cheiePublica(jwk) { if (!(obiect(jwk) && ['EC', 'OKP', 'AKP'].includes(jwk.kty) /*PAZA:key*/)) throw nu('key', 'the key is not an EC, OKP or AKP JWK'); if (!(!('d' in jwk) && !('priv' in jwk) /*PAZA:key*/)) throw nu('key', 'a private JWK was given where a public key belongs'); @@ -109,8 +158,14 @@ function semneazaJws(antet, corp, priv) { const sig = antet.alg === 'ES256' ? crypto.sign('sha256', date, { key: priv, dsaEncoding: 'ieee-p1363' }) : crypto.sign(null, date, priv); return cap + '.' + b64u(sig); } -const cheiePrivata = (k, alg) => (k && k.privat ? (alg === 'ML-DSA-65' ? k.privat.mldsa65 : k.privat.ed25519) : k); -// pentru profilurile de deasupra (sdjwt-vc.mjs: Status List Token), ca semnatura JWS sa se faca intr-un singur loc +// cheia privata care semneaza: pentru chei AERE jumatatea numita de alg (Ed25519 numai cu classicalHalf, AIP-25 C1); altfel cheia data +function cheiePrivata(k, alg, classicalHalf = false) { + if (!(k && k.privat)) return k; + const j = jumatateaAere(alg, classicalHalf); + if (!k.privat[j]) throw new Error(`sd-jwt: these AERE keys have no private ${j === 'mldsa65' ? 'ML-DSA-65' : 'Ed25519'} half`); + return k.privat[j]; +} +// pentru profilurile de deasupra (sdjwt-vc.mjs: Status List Token), ca semnatura JWS si alegerea jumatatii AERE sa se faca intr-un singur loc export { verificaJws as verifyJws, semneazaJws as signJws, cheiePrivata as privateKeyOf }; // structura SD-JWT intr-o valoare: o cheie `_sd`/`_sd_alg` intr-un obiect sau un element {"...": x} intr-o lista, oriunde in adancime function areStructuraSd(v) { @@ -130,12 +185,14 @@ function areStructuraSd(v) { * issuer: chei AERE sau KeyObject privat. holder: cheia PUBLICA a detinatorului (sau cheile lui AERE). disclosable: numele afirmatiilor de * sus dezvaluibile; arrayDisclosable: listele de sus ale caror ELEMENTE sunt dezvaluibile una cate una. exp implicit: un an de la iat * (null: fara exp, pe care verifySdJwt il refuza implicit). typ: tipul explicit al jetonului (implicit TYP; un profil ca SD-JWT VC isi - * pune al lui, "dc+sd-jwt"; RFC 9901 s.9.11). Intoarce { sdJwt, disclosures:[{name, value, disclosure}] }. + * pune al lui, "dc+sd-jwt"; RFC 9901 s.9.11). alg / holderAlg: implicit ML-DSA-65 pentru chei AERE, EdDSA pentru celelalte; jumatatea + * Ed25519 a unor chei AERE (emitent sau detinator) numai cu classicalHalf: true (AIP-25 C1). Intoarce { sdJwt, disclosures:[{name, value, disclosure}] }. */ -export function issueSdJwt({ issuer, claims, disclosable = [], arrayDisclosable = [], holder, iss, iat = Math.floor(Date.now() / 1000), exp, nbf, alg = 'EdDSA', holderAlg = 'EdDSA', decoys = 0, typ = TYP }) { +export function issueSdJwt({ issuer, claims, disclosable = [], arrayDisclosable = [], holder, iss, iat = Math.floor(Date.now() / 1000), exp, nbf, alg, holderAlg, classicalHalf = false, decoys = 0, typ = TYP }) { if (!obiect(claims)) throw new Error('sd-jwt: claims must be an object'); if (typeof iss !== 'string' || !iss) throw new Error('sd-jwt: iss is required'); - if (!ALGS.includes(alg) || !ALGS.includes(holderAlg)) throw new Error('sd-jwt: unknown alg'); + const algE = alg ?? defaultAlg(issuer), algH = holderAlg ?? defaultAlg(holder); + if (!ALGS.includes(algE) || !ALGS.includes(algH)) throw new Error('sd-jwt: unknown alg'); if (!tipBun(typ, undefined)) throw new Error('sd-jwt: typ must be an explicit SD-JWT type ("...+sd-jwt", RFC 9901 s.9.11)'); // RFC 9901 s.4.1 (regula 7 a emitentului): emitentul nu semneaza digesturi pe care nu le-a facut el; o valoare care poarta deja `_sd`, // `_sd_alg` sau un element {"...": d} ar lasa detinatorul sa dezvaluie afirmatii nevazute de emitent @@ -155,8 +212,8 @@ export function issueSdJwt({ issuer, claims, disclosable = [], arrayDisclosable corp.iss = iss; corp.iat = iat; const e = exp === undefined ? iat + 365 * 86400 : exp; if (e != null) corp.exp = e; if (nbf != null) corp.nbf = nbf; - corp.cnf = { jwk: publicJwk(holder, holderAlg) }; - const jwt = semneazaJws({ alg, typ }, corp, cheiePrivata(issuer, alg)); + corp.cnf = { jwk: publicJwk(holder, algH, { classicalHalf }) }; + const jwt = semneazaJws({ alg: algE, typ }, corp, cheiePrivata(issuer, algE, classicalHalf)); return { sdJwt: jwt + '~' + dez.map((x) => x.disclosure + '~').join(''), disclosures: dez }; } @@ -164,12 +221,17 @@ export function issueSdJwt({ issuer, claims, disclosable = [], arrayDisclosable * Detinatorul pastreaza numai dezvaluirile alese si leaga prezentarea de verificator. reveal: un sir = afirmatia de SUS cu acel nume; * { element, in } = elementul cu acea valoare din lista de SUS `in` (numai din ea). O alegere care nu gaseste nimic e o eroare, nu o tacere. */ -export function presentSdJwt({ sdJwt, reveal = [], holder, holderAlg = 'EdDSA', audience, nonce, iat = Math.floor(Date.now() / 1000) }) { +export function presentSdJwt({ sdJwt, reveal = [], holder, holderAlg, classicalHalf = false, audience, nonce, iat = Math.floor(Date.now() / 1000) }) { if (typeof audience !== 'string' || !audience || typeof nonce !== 'string' || !nonce) throw new Error('sd-jwt: a presentation needs the verifier\'s audience and nonce'); const parti = String(sdJwt).split('~'); if (parti[parti.length - 1] !== '') throw new Error('sd-jwt: this SD-JWT already ends with a key binding JWT'); const jwt = parti[0], dez = parti.slice(1, -1); const corp = jsonB64u(jwt.split('.')[1] || '', 'the issuer-signed JWT payload'); + // AIP-25 C1 (2026-10-19): un detinator AERE semneaza Key Binding-ul cu jumatatea pe care emitentul a legat-o in cnf.jwk (ML-DSA-65 implicit; + // o cheie OKP in cnf inseamna ca emitentul a cerut jumatatea Ed25519, iar verdictul spune postQuantum false); un holderAlg dat castiga + const cnfKty = obiect(corp.cnf) && obiect(corp.cnf.jwk) ? corp.cnf.jwk.kty : null; + const urmeazaCnf = holderAlg == null && esteAere(holder) && cnfKty === 'OKP'; + const algH = holderAlg ?? (urmeazaCnf ? 'EdDSA' : defaultAlg(holder)); // unde sta fiecare digest in JWT-ul emitentului: proprietate de sus, sau element al unei liste de sus (cu numele listei) const loc = new Map(); for (const dg of Array.isArray(corp._sd) ? corp._sd : []) loc.set(dg, { tip: 'prop' }); @@ -189,7 +251,7 @@ export function presentSdJwt({ sdJwt, reveal = [], holder, holderAlg = 'EdDSA', const lipsa = reveal.filter((_, i) => !gasite.has(i)); if (lipsa.length) throw new Error('sd-jwt: nothing to reveal for ' + JSON.stringify(lipsa) + ' (a name reveals a top-level claim; {element, in} an element of the top-level list named)'); const baza = jwt + '~' + alese.map((d) => d + '~').join(''); - const kb = semneazaJws({ alg: holderAlg, typ: 'kb+jwt' }, { iat, aud: audience, nonce, sd_hash: sha256b64u(baza) }, cheiePrivata(holder, holderAlg)); + const kb = semneazaJws({ alg: algH, typ: 'kb+jwt' }, { iat, aud: audience, nonce, sd_hash: sha256b64u(baza) }, cheiePrivata(holder, algH, classicalHalf || urmeazaCnf)); return baza + kb; } @@ -200,11 +262,13 @@ export function presentSdJwt({ sdJwt, reveal = [], holder, holderAlg = 'EdDSA', * now?:Date, maxAgeS?:number, clockSkewS?:number, expectedTyp?:string|null}} o * issuerKey: cheia EMITENTULUI data de verificator (JWK, KeyObject, cheile AERE ale emitentului); niciodata din jeton. audience si nonce * sunt ale verificatorului si se CER cand exista (sau se cere) Key Binding. expectedTyp: implicit orice tip explicit "...+sd-jwt". - * Intoarce { valid, rule, reason, payload, disclosed:[nume], issuer, issuerChecked, keyBinding:{aud, nonce, iat, alg}|null }; `rule` e - * regula (din REGULI) care a refuzat, null cand e valid. + * Cu chei AERE (AIP-25 C1): implicit se cere jumatatea ML-DSA-65 (issuerAlg o poate numi); jumatatea Ed25519 numai cu classicalHalf: true. + * Intoarce { valid, rule, reason, payload, disclosed:[nume], issuer, issuerChecked, alg, postQuantum, keyBinding:{aud, nonce, iat, alg}|null }; + * `rule` e regula (din REGULI) care a refuzat, null cand e valid; `postQuantum` e true numai cand fiecare semnatura judecata (a emitentului + * si Key Binding-ul, daca exista) e ML-DSA-65, si false pe orice refuz. */ -export function verifySdJwt(sdJwt, { issuerKey, issuerAlg = 'EdDSA', expectedIssuer = null, audience = null, nonce = null, requireKeyBinding = true, requireExp = true, now = new Date(), maxAgeS = 300, clockSkewS = 60, expectedTyp } = {}) { - const refuz = (rule, reason) => ({ valid: false, rule, reason, payload: null, disclosed: [], keyBinding: null }); +export function verifySdJwt(sdJwt, { issuerKey, issuerAlg, classicalHalf = false, expectedIssuer = null, audience = null, nonce = null, requireKeyBinding = true, requireExp = true, now = new Date(), maxAgeS = 300, clockSkewS = 60, expectedTyp } = {}) { + const refuz = (rule, reason) => ({ valid: false, rule, reason, payload: null, disclosed: [], keyBinding: null, postQuantum: false }); const motiv = (e) => String((e && e.message) || e).replace(/^sd-jwt: /, ''); const regula = (e) => (e instanceof Refuz ? e.rule : 'form'); try { @@ -218,8 +282,8 @@ export function verifySdJwt(sdJwt, { issuerKey, issuerAlg = 'EdDSA', expectedIss // RFC 9901 s.7.3 pasul 5: publicul si nonce-ul verificatorului se compara; fara ele, o prezentare facuta pentru altul ar trece const context = typeof audience === 'string' && !!audience && typeof nonce === 'string' && !!nonce; if (!(!(requireKeyBinding || kb !== '') || context /*PAZA:kb-context*/)) return refuz('kb-context', 'the verifier must give its audience and nonce to judge a key binding (RFC 9901 s.7.3)'); - // 2. JWT-ul emitentului - const { antet, corp } = verificaJws(jwt, publicJwk(issuerKey, issuerAlg), { typ: expectedTyp }); + // 2. JWT-ul emitentului, cu jumatatea ceruta a unor chei AERE (AIP-25 C1: implicit ML-DSA-65; Ed25519 numai cu classicalHalf) + const { antet, corp } = verificaJws(jwt, judgedJwk(issuerKey, issuerAlg, { classicalHalf, headerAlg: headerAlgOf(jwt) }), { typ: expectedTyp }); if (!(expectedIssuer == null || corp.iss === expectedIssuer /*PAZA:issuer*/)) return refuz('issuer', `issued by ${JSON.stringify(corp.iss)}, not ${expectedIssuer}`); const alg = corp._sd_alg === undefined ? 'sha-256' : corp._sd_alg; if (!(alg === 'sha-256' /*PAZA:sd-alg*/)) return refuz('sd-alg', `_sd_alg ${JSON.stringify(corp._sd_alg)} is not understood (only sha-256)`); @@ -288,7 +352,9 @@ export function verifySdJwt(sdJwt, { issuerKey, issuerAlg = 'EdDSA', expectedIss if (!(payload.exp === undefined || acum < payload.exp /*PAZA:time*/)) return refuz('time', `expired at ${payload.exp}`); if (!(payload.nbf === undefined || payload.nbf - clockSkewS <= acum /*PAZA:time*/)) return refuz('time', `not valid before ${payload.nbf}`); if (!(payload.iat === undefined || payload.iat - clockSkewS <= acum /*PAZA:time*/)) return refuz('time', `issued in the future (${payload.iat})`); - const rez = { valid: true, rule: null, reason: '', payload, disclosed, issuer: corp.iss, issuerChecked: expectedIssuer != null, alg: antet.alg }; + // AIP-25 C1: verdictul spune daca semnatura emitentului judecata e post-cuantica (Key Binding-ul o poate cobori, mai jos) + const pqEmitent = antet.alg === 'ML-DSA-65'; + const rez = { valid: true, rule: null, reason: '', payload, disclosed, issuer: corp.iss, issuerChecked: expectedIssuer != null, alg: antet.alg, postQuantum: pqEmitent }; // s.7.3: Key Binding JWT if (kb === '') { if (!(!requireKeyBinding /*PAZA:kb-required*/)) return refuz('kb-required', 'key binding is required and the SD-JWT has none (it ends with ~)'); @@ -307,6 +373,6 @@ export function verifySdJwt(sdJwt, { issuerKey, issuerAlg = 'EdDSA', expectedIss if (!(K.nbf === undefined || (Number.isFinite(K.nbf) && K.nbf - clockSkewS <= acum) /*PAZA:kb-freshness*/)) return refuz('kb-freshness', `key binding: not valid before ${K.nbf}`); const baza = sdJwt.slice(0, sdJwt.length - kb.length); if (!(sha256b64u(baza) === K.sd_hash /*PAZA:kb-sd-hash*/)) return refuz('kb-sd-hash', 'key binding: sd_hash is not the hash of what was presented'); - return { ...rez, keyBinding: { aud: K.aud, nonce: K.nonce, iat: K.iat, alg: k.antet.alg } }; + return { ...rez, postQuantum: rez.postQuantum && k.antet.alg === 'ML-DSA-65', keyBinding: { aud: K.aud, nonce: K.nonce, iat: K.iat, alg: k.antet.alg } }; } catch (e) { return refuz(regula(e), motiv(e)); } } diff --git a/pq-gateway/README.md b/pq-gateway/README.md index 1915369..36ff106 100644 --- a/pq-gateway/README.md +++ b/pq-gateway/README.md @@ -24,8 +24,11 @@ It does **not**: `classical`. Clients must support ML-DSA signatures (OpenSSL 3.5 does; many browsers do not yet). - **Protect the hop from the gateway to your upstream** when `UPSTREAM` is `http://`. That hop is plain HTTP. Keep it on a trusted network (same host, same private network, loopback) or use an `https://` upstream. When the upstream is - `https://`, the gateway also prefers X25519MLKEM768 on that hop, but it makes no claim about which group was used - there. + `https://`, the gateway prefers X25519MLKEM768 on that hop and accepts TLS 1.2 (`AERE_PQGW_UPSTREAM_MODE=hybrid-preferred`, + the default), or offers only X25519MLKEM768 over TLS 1.3 (`hybrid-only`: an upstream without it gets `502`). Since + 2026-10-19 (AIP-25, finding C8) the status endpoint reports that hop: its mode, minimum version, the groups offered, + whether a hybrid exchange is guaranteed, and the handshakes counted by protocol and group; until then it reported only + the scheme. - **Speak HTTP/2 or HTTP/3.** The gateway advertises only `http/1.1` over ALPN. A client that offers `h2` and `http/1.1` gets `http/1.1`. - **Report the negotiated group per connection in `hybrid-preferred` mode.** The Node.js TLS server API does not expose @@ -101,6 +104,7 @@ Environment variables take precedence over the optional JSON file named by `AERE | `AERE_PQGW_KEY` | `key` | required | PEM private key; must match the leaf certificate or the gateway refuses to start | | `AERE_PQGW_UPSTREAM` | `upstream` | required | `http://host:port[/base]` or `https://host:port[/base]`; no credentials, query or fragment | | `AERE_PQGW_UPSTREAM_CA` | `upstreamCa` | system CAs | PEM CA bundle for an `https://` upstream with a private CA | +| `AERE_PQGW_UPSTREAM_MODE` | `upstreamMode` | `hybrid-preferred` | the hop to an `https://` upstream: `hybrid-preferred` (X25519MLKEM768 first, then X25519 and P-256; TLS 1.2 accepted) or `hybrid-only` (X25519MLKEM768 over TLS 1.3 only; refused with an `http://` upstream). Since 2026-10-19 | | `AERE_PQGW_LISTEN` | `listen` | `:8443` | `host:port`, `[ipv6]:port`, or `:port` for all interfaces | | `AERE_PQGW_CONNECT_TIMEOUT_MS` | `connectTimeoutMs` | `5000` | time to connect to the upstream (TCP, plus TLS for https) | | `AERE_PQGW_REQUEST_TIMEOUT_MS` | `requestTimeoutMs` | `60000` | time for the upstream to start its response after the request was fully sent | @@ -146,7 +150,14 @@ nothing secret: no private key and no file paths. It returns: - `negotiatedGroupPerConnection`: states that the negotiated group is not reported per connection, and why - `certificate`: SHA-256 fingerprint, subject and expiry of the served certificate; `authentication` (`post-quantum`, `mixed`, `classical`) and `chain`, each served certificate with its public key and signature algorithm -- `upstream`: only the scheme and whether that hop is encrypted (not the address) +- `upstream` (never the address): `scheme` and `encrypted`; for `https` also, since 2026-10-19, `mode`, `minTlsVersion`, + `groupsOffered`, `groupPreference`, `guarantee` (`hybridKeyExchangeOnEveryConnection` true only with `UPSTREAM_MODE` + `hybrid-only`, with its `basis`), `handshakes` (each new TLS connection to the upstream counted as ` `, + for example `TLSv1.3 X25519MLKEM768` or `TLSv1.3 X25519`), `postQuantumHandshakes`, `classicalHandshakes` and + `handshakeGroupBasis`: here the gateway is the client, so it reads its own handshakes, but Node names X25519 and P-256 and + names no hybrid group, so a TLS 1.3 handshake whose group it does not name is counted as X25519MLKEM768, the only other + group offered (the tests check this against the group read from the wire). For `http` the guarantee says the hop is not + encrypted at all. - `runtime`: Node.js and OpenSSL versions - `counters`: `connectionsAccepted`, `handshakesRefused` by reason (`no shared group`, `unsupported protocol`, `no shared cipher`, `no shared signature algorithm` (a client that offers no ML-DSA signature algorithm diff --git a/pq-gateway/pq-gateway.mjs b/pq-gateway/pq-gateway.mjs index cd90d8f..45fe454 100644 --- a/pq-gateway/pq-gateway.mjs +++ b/pq-gateway/pq-gateway.mjs @@ -18,6 +18,13 @@ // client), deci gateway-ul nu spune per conexiune ce grup s-a folosit. In hybrid-only nu are nevoie (e singurul grup); // in hybrid-preferred punctul de stare spune explicit ca nu poate spune, si nu numara conexiuni "hibride". // +// DRUMUL CATRE SERVICIUL DIN SPATE (2026-10-19, AIP-25 constatarea C8): aici gateway-ul e CLIENT, deci isi poate citi +// strangerile de mana. Pana atunci drumul prefera hibridul cu TLS 1.2 minim (sau era HTTP simplu) si starea nu spunea nimic +// despre el. Acum: (1) starea spune, sub `upstream`, modul, versiunea minima, grupurile oferite, garantia (sau lipsa ei) si +// strangerile de mana numarate pe protocol si grup; (2) AERE_PQGW_UPSTREAM_MODE=hybrid-only ofera serviciului NUMAI +// X25519MLKEM768 peste TLS 1.3 (un serviciu fara el primeste 502), si refuza sa porneasca cu un UPSTREAM http. Implicitul ramane +// hybrid-preferred, comportamentul de dinainte. +// // Configuratie: variabile AERE_PQGW_* (lista in CHEI, mai jos, si in README), optional un fisier JSON dat prin // AERE_PQGW_CONFIG; mediul are prioritate fata de fisier. O cheie necunoscuta in fisier e o eroare, nu o tacere. // @@ -57,6 +64,7 @@ const CHEI = { AERE_PQGW_KEY: 'key', AERE_PQGW_UPSTREAM: 'upstream', AERE_PQGW_UPSTREAM_CA: 'upstreamCa', + AERE_PQGW_UPSTREAM_MODE: 'upstreamMode', AERE_PQGW_MODE: 'mode', AERE_PQGW_CONNECT_TIMEOUT_MS: 'connectTimeoutMs', AERE_PQGW_REQUEST_TIMEOUT_MS: 'requestTimeoutMs', @@ -124,12 +132,17 @@ export function citesteConfig(env = process.env) { if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('UPSTREAM must start with http:// or https://'); if (u.username || u.password) throw new Error('UPSTREAM must not contain credentials'); if (u.search || u.hash) throw new Error('UPSTREAM must not contain a query string or fragment'); + // AIP-25 C8: modul drumului catre upstream; hybrid-only nu are inteles pe HTTP simplu (acolo nu exista niciun schimb de chei) + const upstreamMode = v('AERE_PQGW_UPSTREAM_MODE', 'hybrid-preferred'); + if (!Object.prototype.hasOwnProperty.call(GRUPURI, upstreamMode)) throw new Error('UPSTREAM_MODE must be hybrid-only or hybrid-preferred'); + if (upstreamMode === 'hybrid-only' && u.protocol !== 'https:') throw new Error('UPSTREAM_MODE hybrid-only requires an https:// UPSTREAM (plain http has no key exchange at all)'); const { host, port } = citesteAdresa(v('AERE_PQGW_LISTEN', ':8443')); const maxHeaderSize = v('AERE_PQGW_MAX_HEADER_SIZE', null); return { host, port, cert, key, mode, upstream: u.href, upstreamCa: v('AERE_PQGW_UPSTREAM_CA', null), + upstreamMode, connectTimeoutMs: intreg('CONNECT_TIMEOUT_MS', v('AERE_PQGW_CONNECT_TIMEOUT_MS', 5000), 100, 600000), requestTimeoutMs: intreg('REQUEST_TIMEOUT_MS', v('AERE_PQGW_REQUEST_TIMEOUT_MS', 60000), 100, 3600000), handshakeTimeoutMs: intreg('HANDSHAKE_TIMEOUT_MS', v('AERE_PQGW_HANDSHAKE_TIMEOUT_MS', 10000), 100, 600000), @@ -250,10 +263,24 @@ export function pornesteGateway(cfg, { jurnal = scrieJurnal } = {}) { if (susHttps && cfg.upstreamCa) { try { caSus = fs.readFileSync(cfg.upstreamCa); } catch (e) { throw new Error(`cannot read UPSTREAM_CA (${e.code || 'error'})`); } } - // Si drumul catre upstream, cand e https, prefera hibridul; nu se afirma nimic despre el in stare. + // Drumul catre upstream, cand e https (AIP-25 C8): hybrid-preferred (implicit, ca pana acum) ofera intai hibridul si primeste + // TLS 1.2; hybrid-only ofera numai X25519MLKEM768 peste TLS 1.3. Ce s-a negociat se numara (numaraStrangereSus) si se spune in stare. + const modSus = cfg.upstreamMode || 'hybrid-preferred'; + const minSus = modSus === 'hybrid-only' ? 'TLSv1.3' : 'TLSv1.2'; const agent = susHttps - ? new https.Agent({ keepAlive: true, minVersion: 'TLSv1.2', ecdhCurve: GRUPURI['hybrid-preferred'], ...(caSus ? { ca: caSus } : {}) }) + ? new https.Agent({ keepAlive: true, minVersion: minSus, ecdhCurve: GRUPURI[modSus], ...(caSus ? { ca: caSus } : {}) }) : new http.Agent({ keepAlive: true }); + const strangeriSus = new Map(); + // Node numeste pe client X25519 si P-256 (getEphemeralKeyInfo) si NU numeste grupul hibrid (masurat 2026-09-25: {}), deci o + // strangere TLS 1.3 fara nume e X25519MLKEM768, singurul alt grup oferit; in TLS 1.2 nu exista hibrid. + function numaraStrangereSus(s) { + let protocol = null, eki = null; + try { protocol = s.getProtocol(); eki = s.getEphemeralKeyInfo(); } catch { /* socket deja inchis */ } + const grup = eki && eki.name ? (eki.name === 'prime256v1' ? 'P-256' : eki.name) : protocol === 'TLSv1.3' ? 'X25519MLKEM768' : 'unnamed'; + const k = `${protocol || 'unknown'} ${grup}`; + strangeriSus.set(k, (strangeriSus.get(k) || 0) + 1); + } + const numarSus = (pq) => [...strangeriSus].filter(([k]) => /MLKEM/.test(k) === pq).reduce((a, [, n]) => a + n, 0); const caUpgrade = caSus ? { ca: caSus } : {}; const c = { @@ -288,7 +315,17 @@ export function pornesteGateway(cfg, { jurnal = scrieJurnal } = {}) { negotiatedGroupPerConnection: 'not reported: the Node.js TLS server API does not expose the negotiated group (getEphemeralKeyInfo is client-side only), so this gateway makes no per-connection claim', certificate: { sha256: x509.fingerprint256, subject: x509.subject, notAfter: x509.validTo, authentication: lantCert.autentificare, chain: lantCert.lant }, - upstream: { scheme: susHttps ? 'https' : 'http', encrypted: susHttps }, + // AIP-25 C8: drumul catre serviciul din spate, din ce a masurat gateway-ul (strangerile numarate), nu din ce spera + upstream: susHttps + ? { + scheme: 'https', encrypted: true, mode: modSus, minTlsVersion: minSus, groupsOffered: grupuriOferite(modSus), groupPreference: GRUPURI[modSus], + guarantee: modSus === 'hybrid-only' + ? { hybridKeyExchangeOnEveryConnection: true, basis: 'structural: X25519MLKEM768 over TLS 1.3 is the only key exchange offered to the service, so a connection that does not use it fails (502)' } + : { hybridKeyExchangeOnEveryConnection: false, basis: 'X25519MLKEM768 is offered first; a service without it gets X25519 or P-256, and TLS 1.2 is accepted: the handshakes below say what happened' }, + handshakes: Object.fromEntries(strangeriSus), postQuantumHandshakes: numarSus(true), classicalHandshakes: numarSus(false), + handshakeGroupBasis: 'read on each new TLS connection to the service: Node names X25519 and P-256 (getEphemeralKeyInfo) and names no hybrid group, so a TLS 1.3 handshake whose group it does not name is X25519MLKEM768, the only other group offered', + } + : { scheme: 'http', encrypted: false, mode: null, guarantee: { hybridKeyExchangeOnEveryConnection: false, basis: 'plain HTTP: the leg from the gateway to the service is not encrypted at all; keep it on the same host or a private network, or use https with UPSTREAM_MODE hybrid-only' } }, runtime: { node: process.version, openssl: process.versions.openssl }, startedAt: pornitLa, counters: { @@ -356,7 +393,7 @@ export function pornesteGateway(cfg, { jurnal = scrieJurnal } = {}) { method: req.method, path: req.url === '*' ? '*' : prefix + req.url, headers: anteturi, setHost: false, agent: peUpgrade ? false : agent, - ...(peUpgrade && susHttps ? { minVersion: 'TLSv1.2', ecdhCurve: GRUPURI['hybrid-preferred'], ...caUpgrade } : {}), + ...(peUpgrade && susHttps ? { minVersion: minSus, ecdhCurve: GRUPURI[modSus], ...caUpgrade } : {}), }; } @@ -369,6 +406,7 @@ export function pornesteGateway(cfg, { jurnal = scrieJurnal } = {}) { const expirat = (tip) => Object.assign(new Error(`upstream ${tip} timeout`), { aereTimeout: tip }); upReq.on('socket', (s) => { if (!s.connecting) return; + if (susHttps) s.once('secureConnect', () => numaraStrangereSus(s)); // AIP-25 C8: fiecare conexiune noua, o data tConectare = setTimeout(() => upReq.destroy(expirat('connect')), cfg.connectTimeoutMs); s.once(susHttps ? 'secureConnect' : 'connect', () => { clearTimeout(tConectare); tConectare = null; }); }); diff --git a/pq-gateway/proba-pq-gateway-control-negativ.sh b/pq-gateway/proba-pq-gateway-control-negativ.sh index 0ee9189..b8e1057 100644 --- a/pq-gateway/proba-pq-gateway-control-negativ.sh +++ b/pq-gateway/proba-pq-gateway-control-negativ.sh @@ -100,8 +100,9 @@ control "upstream cazut: niciun raspuns (agatare)" "(i)" \ "raspundeJson(req, res, cod, corpEroare(cod, e), true);" "if (Boolean(process.env.AERE_NU)) raspundeJson(req, res, cod, corpEroare(cod, e), true);" control "timeout raportat ca 502" "(i2)" \ "const cod = e && e.aereTimeout ? 504 : 502;" "const cod = 502;" +# 2026-10-19: ancora cu cele doua spatii din fata (forma de pe un rand propriu), unica dupa ce starea drumului catre upstream (C8) a primit aceeasi cheie control "hybrid-preferred pretinde hibrid garantat" "(j)" \ - "hybridKeyExchangeOnEveryConnection: false," "hybridKeyExchangeOnEveryConnection: true," + " hybridKeyExchangeOnEveryConnection: false," " hybridKeyExchangeOnEveryConnection: true," control "hybrid-preferred ca lista plata (fara tuplu, fara HelloRetryRequest)" "(j2)" \ "'hybrid-preferred': 'X25519MLKEM768/X25519:P-256'," "'hybrid-preferred': 'X25519MLKEM768:X25519:P-256'," control "corpul bufferat intreg inainte de trimitere" "(k)" \ @@ -113,9 +114,18 @@ control "upgrade refuzat de upstream trimis fara Connection: close" "(d2)" \ control "upgrade catre upstream cazut: niciun raspuns pe socket" "(i3)" \ "if (!socket.destroyed) scrieBrut(socket, cod, corpEroare(cod, e));" "if (Boolean(process.env.AERE_NU)) scrieBrut(socket, cod, corpEroare(cod, e));" control "drumul catre upstream https fara preferinta hibrida" "(q)" \ - "ecdhCurve: GRUPURI['hybrid-preferred'], ...(caSus ?" "ecdhCurve: 'X25519', ...(caSus ?" + "ecdhCurve: GRUPURI[modSus], ...(caSus ?" "ecdhCurve: 'X25519', ...(caSus ?" control "upgrade catre upstream https fara CA-ul configurat" "(d3)" \ - "ecdhCurve: GRUPURI['hybrid-preferred'], ...caUpgrade }" "ecdhCurve: GRUPURI['hybrid-preferred'] }" + "ecdhCurve: GRUPURI[modSus], ...caUpgrade }" "ecdhCurve: GRUPURI[modSus] }" +# 2026-10-19 (AIP-25 C8): drumul catre serviciul din spate spus in stare, si modul hybrid-only al lui +control "C8: strangerile catre upstream nenumarate (starea tace din nou)" "(q)" \ + "if (susHttps) s.once('secureConnect', () => numaraStrangereSus(s));" "if (Boolean(process.env.AERE_NU)) s.once('secureConnect', () => numaraStrangereSus(s));" +control "C8: o strangere clasica numarata drept post-cuantica" "(q3)" \ + "filter(([k]) => /MLKEM/.test(k) === pq)" "filter(([k]) => /MLKEM|X25519/.test(k) === pq)" +control "C8: UPSTREAM_MODE hybrid-only fara efect (oferta ramane hybrid-preferred)" "(q4)" \ + "const modSus = cfg.upstreamMode || 'hybrid-preferred';" "const modSus = 'hybrid-preferred';" +control "C8: UPSTREAM_MODE hybrid-only primit cu un upstream http" "(p)" \ + "if (upstreamMode === 'hybrid-only' && u.protocol !== 'https:') throw" "if (Boolean(process.env.AERE_NU)) throw" control "plecarea clientului numarata ca 502 (cerere)" "(s)" \ "if (res.headersSent || res.destroyed) { res.destroy(); return; }" "" control "plecarea clientului numarata ca 502 (upgrade)" "(s)" \ diff --git a/pq-gateway/proba-pq-gateway.mjs b/pq-gateway/proba-pq-gateway.mjs index 28b48f5..05a8d15 100644 --- a/pq-gateway/proba-pq-gateway.mjs +++ b/pq-gateway/proba-pq-gateway.mjs @@ -318,8 +318,9 @@ function laUpgrade(req, sock, head) { if (head && head.length) sock.write(head); sock.on('data', (d) => sock.write(d)); } -function pornesteUpstreamHttps() { - const srv = https.createServer({ key: CHEIE_PEM, cert: CERT_PEM }, ecou); +// extra (2026-10-19, AIP-25 C8): un upstream care stie numai grupuri clasice (ecdhCurve) sau numai TLS 1.2 (maxVersion) +function pornesteUpstreamHttps(extra = {}) { + const srv = https.createServer({ key: CHEIE_PEM, cert: CERT_PEM, ...extra }, ecou); srv.on('upgrade', laUpgrade); deInchis.push(srv); return new Promise((r) => srv.listen(0, GAZDA, () => r(srv.address().port))); @@ -720,8 +721,48 @@ await test('(q) upstream https cu AERE_PQGW_UPSTREAM_CA: cererea trece, iar drum cere(final && final.grup === HIBRID, `drumul catre upstream: ${descrieSH(f.sh)}`); const s = await stare(GS.port); cere(s.upstream && s.upstream.scheme === 'https' && s.upstream.encrypted === true, `starea: ${JSON.stringify(s.upstream)}`); - cere(JSON.stringify(Object.keys(s.upstream)) === '["scheme","encrypted"]', `starea spune despre upstream mai mult decat schema: ${JSON.stringify(s.upstream)}`); - return `drum catre upstream: ${descrieSH(f.sh)}`; + // 2026-10-19 (AIP-25 C8): starea spune si drumul catre upstream, din ce a MASURAT gateway-ul (strangerile de mana numarate), nu din ce + // spera: implicit hybrid-preferred, TLS 1.2 minim, fara garantie; strangerea de aici, citita de pe fir drept hibrid, e numarata hibrid + const u = s.upstream; + cere(u.mode === 'hybrid-preferred' && u.minTlsVersion === 'TLSv1.2' && u.guarantee && u.guarantee.hybridKeyExchangeOnEveryConnection === false, `starea drumului catre upstream: ${JSON.stringify(u).slice(0, 300)}`); + cere(u.handshakes && u.handshakes['TLSv1.3 X25519MLKEM768'] >= 1 && u.postQuantumHandshakes >= 1 && u.classicalHandshakes === 0, `strangerile catre upstream numarate: ${JSON.stringify(u.handshakes)} pq ${u.postQuantumHandshakes} clasice ${u.classicalHandshakes}`); + return `drum catre upstream: ${descrieSH(f.sh)}; starea: ${JSON.stringify(u.handshakes)}`; +}); +await test('(q3) AIP-25 C8: drumul catre upstream in starea gateway-ului: http -> necriptat, fara garantie; hybrid-preferred catre un upstream numai X25519 -> 200 si strangerea CLASICA numarata (starea nu mai tace)', async () => { + const sh = await stare(GH.port); + cere(sh.upstream && sh.upstream.scheme === 'http' && sh.upstream.encrypted === false && sh.upstream.guarantee && sh.upstream.guarantee.hybridKeyExchangeOnEveryConnection === false && /plain HTTP/.test(sh.upstream.guarantee.basis), `starea unui upstream http: ${JSON.stringify(sh.upstream)}`); + const portClasic = await pornesteUpstreamHttps({ ecdhCurve: 'X25519' }); + const g = await pornesteGw('gateway catre upstream numai X25519', { AERE_PQGW_LISTEN: `${GAZDA}:0`, AERE_PQGW_CERT: CALE_CERT, AERE_PQGW_KEY: CALE_CHEIE, AERE_PQGW_MODE: 'hybrid-only', AERE_PQGW_UPSTREAM: `https://${GAZDA}:${portClasic}`, AERE_PQGW_UPSTREAM_CA: CALE_CERT }); + const r = await cerere({ port: g.port, cale: '/q3' }); + cere(r.status === 200 && json(r).url === '/q3', `status ${r.status}`); + const s = await stare(g.port); const u = s.upstream; + cere(u.handshakes && u.handshakes['TLSv1.3 X25519'] >= 1 && u.classicalHandshakes >= 1 && u.postQuantumHandshakes === 0, `strangerea clasica nu e numarata: ${JSON.stringify(u.handshakes)} pq ${u.postQuantumHandshakes} clasice ${u.classicalHandshakes}`); + return `upstream http: ${sh.upstream.guarantee.basis.slice(0, 60)}...; upstream numai X25519: ${JSON.stringify(u.handshakes)}`; +}); +await test('(q4) AIP-25 C8: UPSTREAM_MODE hybrid-only: catre un upstream cu hibrid -> 200, pe fir X25519MLKEM768, starea cu garantie; catre unul numai X25519 sau numai TLS 1.2 -> 502, nicio strangere clasica numarata', async () => { + const baza = { AERE_PQGW_LISTEN: `${GAZDA}:0`, AERE_PQGW_CERT: CALE_CERT, AERE_PQGW_KEY: CALE_CHEIE, AERE_PQGW_MODE: 'hybrid-only', AERE_PQGW_UPSTREAM_MODE: 'hybrid-only', AERE_PQGW_UPSTREAM_CA: CALE_CERT }; + const [portClasic, port12] = await Promise.all([pornesteUpstreamHttps({ ecdhCurve: 'X25519' }), pornesteUpstreamHttps({ maxVersion: 'TLSv1.2' })]); + const [gh, gc, g12] = await Promise.all([ + pornesteGw('gateway UPSTREAM_MODE hybrid-only catre upstream cu hibrid', { ...baza, AERE_PQGW_UPSTREAM: `https://${GAZDA}:${TS.port}` }), + pornesteGw('gateway UPSTREAM_MODE hybrid-only catre upstream numai X25519', { ...baza, AERE_PQGW_UPSTREAM: `https://${GAZDA}:${portClasic}` }), + pornesteGw('gateway UPSTREAM_MODE hybrid-only catre upstream numai TLS 1.2', { ...baza, AERE_PQGW_UPSTREAM: `https://${GAZDA}:${port12}` }), + ]); + const inainte = TS.conexiuni.length; + const r = await cerere({ port: gh.port, cale: '/q4' }); + cere(r.status === 200 && json(r).url === '/q4', `catre upstream cu hibrid: status ${r.status}`); + const f = citesteFir(TS.conexiuni[inainte] || { c2s: [], s2c: [] }); + cere(f.ch.length >= 1 && f.ch[0].grupuri.join() === String(HIBRID) && !f.ch[0].versiuni.includes(0x0303), `gateway-ul a oferit upstream-ului ${f.ch[0] ? f.ch[0].grupuri.map(numeGrup) + ' / ' + f.ch[0].versiuni.map((x) => x.toString(16)) : '-'}`); + const sh = await stare(gh.port); + cere(sh.upstream.mode === 'hybrid-only' && sh.upstream.minTlsVersion === 'TLSv1.3' && sh.upstream.guarantee.hybridKeyExchangeOnEveryConnection === true && sh.upstream.postQuantumHandshakes >= 1, `starea: ${JSON.stringify(sh.upstream).slice(0, 300)}`); + const note = []; + for (const [g, nume] of [[gc, 'numai X25519'], [g12, 'numai TLS 1.2']]) { + const rr = await cerere({ port: g.port, cale: '/q4-refuz' }); + cere(rr.status === 502 && json(rr).error === 'bad_gateway', `catre upstream ${nume}: status ${rr.status}`); + const s = await stare(g.port); + cere(s.upstream.classicalHandshakes === 0 && s.counters.responses502 >= 1, `catre upstream ${nume}: ${JSON.stringify(s.upstream.handshakes)} 502=${s.counters.responses502}`); + const rand = g.jurnal.filter((j) => j.event === 'upstream_error').pop(); note.push(`${nume}: 502 (${rand ? rand.code : '?'})`); + } + return `catre upstream cu hibrid: ${descrieSH(f.sh)}; ${note.join('; ')}`; }); await test('(q2) upstream https cu certificat pe care gateway-ul nu il poate verifica (fara UPSTREAM_CA): 502, nicio incredere oarba', async () => { @@ -833,6 +874,9 @@ await test('(p) configuratie gresita: refuza sa porneasca, cu motivul numit, si ['MODE necunoscut', { ...baza, AERE_PQGW_MODE: 'classic' }, 2, /MODE must be/], ['cheie in fisier gresita', { ...baza, AERE_PQGW_MODE: 'hybrid-only', AERE_PQGW_CONFIG: fisRau }, 2, /unknown key in config file: upstrem/], ['cheia nu se potriveste cu certificatul', { ...baza, AERE_PQGW_MODE: 'hybrid-only', AERE_PQGW_KEY: caleAlta }, 1, /KEY does not match/], + // 2026-10-19 (AIP-25 C8): modul drumului catre upstream + ['UPSTREAM_MODE necunoscut', { ...baza, AERE_PQGW_MODE: 'hybrid-only', AERE_PQGW_UPSTREAM_MODE: 'classic' }, 2, /UPSTREAM_MODE must be/], + ['UPSTREAM_MODE hybrid-only cu un upstream http', { ...baza, AERE_PQGW_MODE: 'hybrid-only', AERE_PQGW_UPSTREAM_MODE: 'hybrid-only' }, 2, /UPSTREAM_MODE hybrid-only requires an https/], ]; const note = []; for (const [nume, env, codAsteptat, motiv] of cazuri) { diff --git a/pq-pki/README.md b/pq-pki/README.md index e73c37c..8bacaf1 100644 --- a/pq-pki/README.md +++ b/pq-pki/README.md @@ -48,12 +48,13 @@ A Node TLS server loads the key in process, without ever writing it in clear: `t - **Revocation**, for each issuer in the chain: among that issuer's lists that verify under its key and are not dated in the future, **the one with the newest `thisUpdate` is used** (ties broken by the larger `crlNumber`); the order of the input does not matter. That list must be current (`CRL_STALE` after `nextUpdate`), and a listed serial is `REVOKED`. `requireCrl` makes a missing list `CRL_MISSING`. A list whose signature does not verify is `CRL_SIGNATURE`. - **CRL extensions**: `authorityKeyIdentifier` and `crlNumber` are understood; `issuingDistributionPoint` and `deltaCRLIndicator` are refused however they are marked, and so is any other critical extension of the list or of an entry (`certificateIssuer` of an indirect list, for instance): verdict `CRL_EXT`, naming the extension. Non-critical unknown extensions (a `reasonCode` on an entry, for instance) are ignored. - **End entity**: not a CA, `digitalSignature`, the extended key usage of the purpose, and the host name or IPv4 address in the subject alternative name. +- **The declared algorithm is the issuer key's** (since 2026-10-19; AIP-25, finding C3): the signature algorithm a certificate declares must be the algorithm of its issuer's public key, and a trusted root's must be that of its own key (verdict `ALG_KEY`); a revocation list that verifies under its issuer's key but declares another algorithm is `CRL_ALG_KEY` (RFC 5280 Sections 4.1.2.3 and 5.1.2.2; RFC 9881). Until then a certificate declaring `id-ml-dsa-87`, signed with an ML-DSA-44 key, passed: the signature was verified with whatever key the issuer has, so a policy that reads the declared algorithm was misled. OpenSSL 3.5 refuses the same cases (`unable to get local issuer certificate`, `CRL signature failure`), measured in `test/proba.mjs`. -Every refusal carries a code: `SIGNATURE`, `UNTRUSTED`, `ROOT`, `EXPIRED`, `NOT_YET_VALID`, `NOT_CA`, `KEY_USAGE`, `PATH_LEN`, `EKU`, `SPKI`, `REVOKED`, `CRL_MISSING`, `CRL_SIGNATURE`, `CRL_STALE`, `CRL_EXT`, `LEAF_IS_CA`, `HOSTNAME`, `CRITICAL_EXT`, `ALG`, `ALG_MISMATCH`, `ALG_PARAMS`, `VERSION`, `EXT_DUP`, `DER`. The DER parser is strict: no indefinite lengths, no non-minimal lengths, no trailing bytes, no non-canonical integers or object identifiers; object identifiers with a multi-byte first subidentifier (`2.999` is `06 02 88 37`) encode and decode as OpenSSL does. +Every refusal carries a code: `SIGNATURE`, `UNTRUSTED`, `ROOT`, `EXPIRED`, `NOT_YET_VALID`, `NOT_CA`, `KEY_USAGE`, `PATH_LEN`, `EKU`, `SPKI`, `REVOKED`, `CRL_MISSING`, `CRL_SIGNATURE`, `CRL_STALE`, `CRL_EXT`, `CRL_ALG_KEY`, `ALG_KEY`, `LEAF_IS_CA`, `HOSTNAME`, `CRITICAL_EXT`, `ALG`, `ALG_MISMATCH`, `ALG_PARAMS`, `VERSION`, `EXT_DUP`, `DER`. The DER parser is strict: no indefinite lengths, no non-minimal lengths, no trailing bytes, no non-canonical integers or object identifiers; object identifiers with a multi-byte first subidentifier (`2.999` is `06 02 88 37`) encode and decode as OpenSSL does. ## How it is proven -`node test/proba.mjs` (27 probes, about 25 s) compares every verdict with a foreign implementation, the OpenSSL 3.5 command line in `-x509_strict` mode: the valid chain, revocation, the newest of several lists (both orders), lists made by `openssl ca -gencrl` (plain, with a revoked entry and a reason code, with a critical issuing distribution point, with an unknown critical extension), a delta list (OpenSSL refuses it only with `-extended_crl`), a changed byte, another root, expiry and not-yet-valid, a path-length violation, a non-CA issuer (a chain made by OpenSSL), host and IP names, purpose on the leaf and on authorities (`unsuitable certificate purpose` at depth 1), a renewed root (both anchors in one `CAfile`), malformed public keys (`decode error`), an unknown critical extension (made by OpenSSL), object identifier bytes (`asn1parse -genstr`). It also reads and verifies certificates made by OpenSSL, runs the real TLS handshake, measures the cost of a passphrase guess, exercises the passphrase policy, and drives the command line end to end. `node test/control-negativ.mjs` (about 3 minutes, four plantings in parallel) breaks each check in a copy of the code (22 plantings, at least one per finding of the review) and requires the named probe to turn red, the original to stay byte-identical, and the untouched suite to stay green; the number of probes is taken from the untouched run, not written down. +`node test/proba.mjs` (28 probes, about 25 s) compares every verdict with a foreign implementation, the OpenSSL 3.5 command line in `-x509_strict` mode: the valid chain, revocation, the newest of several lists (both orders), lists made by `openssl ca -gencrl` (plain, with a revoked entry and a reason code, with a critical issuing distribution point, with an unknown critical extension), a delta list (OpenSSL refuses it only with `-extended_crl`), a changed byte, another root, expiry and not-yet-valid, a path-length violation, a non-CA issuer (a chain made by OpenSSL), host and IP names, purpose on the leaf and on authorities (`unsuitable certificate purpose` at depth 1), a renewed root (both anchors in one `CAfile`), malformed public keys (`decode error`), an unknown critical extension (made by OpenSSL), object identifier bytes (`asn1parse -genstr`). It also reads and verifies certificates made by OpenSSL, runs the real TLS handshake, measures the cost of a passphrase guess, exercises the passphrase policy, and drives the command line end to end. `node test/control-negativ.mjs` (about 3 minutes, four plantings in parallel) breaks each check in a copy of the code (26 plantings, at least one per finding of the review and four for C3) and requires the named probe to turn red, the original to stay byte-identical, and the untouched suite to stay green; the number of probes is taken from the untouched run, not written down. ## What it does not do, and where it differs from OpenSSL diff --git a/pq-pki/pki.mjs b/pq-pki/pki.mjs index c70f0a5..b0c146c 100644 --- a/pq-pki/pki.mjs +++ b/pq-pki/pki.mjs @@ -8,6 +8,12 @@ // PBKDF2 slab (A1), prima lista de revocare in loc de cea mai noua (A2), extensiile listei ignorate (A3), exceptie pe SPKI malformat si // frunza cu cheie de 10 octeti admisa (A5), EKU pe autoritati neaplicat (A6), prima ancora cu acelasi nume la reinnoirea radacinii (A7), // OID cu primul subidentificator pe mai multi octeti (A10, in der.mjs). +// 2026-10-19 (AIP-25 constatarea C3, D-537): algoritmul de semnatura DECLARAT de un certificat sau de o lista de revocare trebuie sa fie chiar +// algoritmul cheii emitentului (RFC 5280, campul signature din TBSCertificate si din TBSCertList: "the algorithm used by the CA to sign"; RFC 9881: ML-DSA-44, -65 si -87 sunt +// trei algoritmi). Pana atunci semnatura se verifica cu cheia emitentului oricare ar fi algoritmul declarat, deci o frunza care declara +// id-ml-dsa-87, semnata de o radacina ml-dsa-44, trecea (masurat de aips/aip25-inventar/masoara-constatari.mjs), iar o politica ce citeste +// algoritmul declarat era mintita. O nepotrivire e un refuz numit: ALG_KEY (certificat, si radacina fata de propria cheie), CRL_ALG_KEY (lista). +// OpenSSL 3.5 refuza aceleasi cazuri ("unable to get local issuer certificate", "CRL signature failure"; masurat in test/proba.mjs). import crypto from 'node:crypto'; import * as D from './der.mjs'; @@ -441,6 +447,8 @@ function verifyChainInner({ leaf, intermediates = [], roots = [], at = new Date( function judeca(path) { const top = path[path.length - 1]; + // AIP-25 C3: o radacina auto-semnata declara algoritmul propriei chei + if (!(top.alg === top.pkAlg)) return fail('ALG_KEY', `the trusted root "${cnOf(top)}" declares its signature as ${top.alg}, and its own key is ${top.pkAlg} (RFC 9881: the signature algorithm is the issuer key's)`); if (!top.subjectRaw.equals(top.issuerRaw) || !verifySig(top.tbs, top.signature, top.spki, cnOf(top))) return fail('ROOT', 'the trusted root is not a valid self-signed certificate'); // 2. fiecare legatura for (let k = 0; k < path.length; k++) { @@ -456,6 +464,8 @@ function verifyChainInner({ leaf, intermediates = [], roots = [], at = new Date( if (p.pathLen !== undefined && casBelow > p.pathLen) return fail('PATH_LEN', `"${cnOf(p)}" allows ${p.pathLen} CA(s) below it, the chain has ${casBelow}`); // [A6] EKU pe o autoritate restrange tot ce e sub ea (OpenSSL, Chrome, Mozilla fac la fel); anyExtendedKeyUsage NU scuteste, ca la OpenSSL. if (purpose && p.eku && !p.eku.has(OID[purpose])) return fail('EKU', `the issuer "${cnOf(p)}" is not valid for ${purpose} (its extended key usage does not allow it)`); + // AIP-25 C3: algoritmul declarat de c e cel al cheii lui p (altfel o politica ce citeste algoritmul declarat e mintita) + if (!(c.alg === p.pkAlg)) return fail('ALG_KEY', `"${cnOf(c)}" declares its signature as ${c.alg}, and the key of its issuer "${cnOf(p)}" is ${p.pkAlg} (RFC 5280, the signature field of TBSCertificate; RFC 9881: the signature algorithm is the issuer key's)`); if (!verifySig(c.tbs, c.signature, p.spki, cnOf(c))) return fail('SIGNATURE', `the signature on "${cnOf(c)}" does not verify under "${cnOf(p)}"`); const rv = revocare(c, p); if (rv) return rv; @@ -485,6 +495,9 @@ function verifyChainInner({ leaf, intermediates = [], roots = [], at = new Date( if (!verificate.length) return fail('CRL_SIGNATURE', `the revocation list of "${cnOf(p)}" does not verify`); const rea = verificate.find((l) => l.refusedExt); if (rea) return fail('CRL_EXT', `the revocation list of "${cnOf(p)}" carries ${rea.refusedExt}`); + // AIP-25 C3: o lista care verifica sub cheia emitentului dar declara alt algoritm decat al ei e un verdict, ca CRL_EXT, nu e sarita + const altAlg = verificate.find((l) => !(l.alg === p.pkAlg)); + if (altAlg) return fail('CRL_ALG_KEY', `the revocation list of "${cnOf(p)}" declares its signature as ${altAlg.alg}, and the key of "${cnOf(p)}" is ${p.pkAlg} (RFC 5280, the signature field of TBSCertList; RFC 9881)`); const curente = verificate.filter((l) => at >= l.thisUpdate); if (!curente.length) return fail('CRL_STALE', `the revocation list of "${cnOf(p)}" is dated in the future`); curente.sort((a, b) => (b.thisUpdate - a.thisUpdate) || cmpBig(b.crlNumber, a.crlNumber)); @@ -498,7 +511,7 @@ function verifyChainInner({ leaf, intermediates = [], roots = [], at = new Date( /** Semnatura unui certificat auto-semnat, verificata cu propria cheie (interoperabilitatea inversa: certificate facute de altii). */ export function selfSignatureValid(der) { const c = parseCert(der); - return c.subjectRaw.equals(c.issuerRaw) && verifySig(c.tbs, c.signature, c.spki, cnOf(c)); + return c.alg === c.pkAlg && c.subjectRaw.equals(c.issuerRaw) && verifySig(c.tbs, c.signature, c.spki, cnOf(c)); // AIP-25 C3: algoritmul declarat e al cheii } export function cnOf(c) { diff --git a/pq-pki/test/control-negativ.mjs b/pq-pki/test/control-negativ.mjs index f7d4239..52e31e1 100644 --- a/pq-pki/test/control-negativ.mjs +++ b/pq-pki/test/control-negativ.mjs @@ -45,6 +45,11 @@ const PLANTARI = [ ['[A7] numai primul intermediar candidat', 'pki.mjs', 'certs.inter.filter((c) => c !== cur && !stiva.includes(c) && emite(c, cur)) : [];', 'certs.inter.filter((c) => c !== cur && !stiva.includes(c) && emite(c, cur)).slice(0, 1) : [];', 'reinnoirea radacinii'], ['[A10] primul subidentificator OID scris pe un singur octet', 'der.mjs', ' const out = base128(p[0] * 40n + p[1]);', ' const out = [Number((p[0] * 40n + p[1]) & 0x7fn)];', 'OID'], ['[A10] arcul OID dedus fara regula de 80', 'der.mjs', ' const arc = first < 80n ? first / 40n : 2n;', ' const arc = first / 40n;', 'OID'], + // 2026-10-19 (AIP-25 C3, D-537): algoritmul declarat e al cheii emitentului; fiecare paznic nou scos pe rand + ['[AIP-25 C3] algoritmul declarat de un certificat nu mai e legat de cheia emitentului', 'pki.mjs', "if (!(c.alg === p.pkAlg)) return fail('ALG_KEY'", "if (!(c.alg === p.pkAlg) && Boolean(Number('0'))) return fail('ALG_KEY'", 'algoritmul declarat e al cheii emitentului'], + ['[AIP-25 C3] radacina declara alt algoritm decat propria cheie', 'pki.mjs', "if (!(top.alg === top.pkAlg)) return fail('ALG_KEY'", "if (!(top.alg === top.pkAlg) && Boolean(Number('0'))) return fail('ALG_KEY'", 'algoritmul declarat e al cheii emitentului'], + ['[AIP-25 C3] lista de revocare declara alt algoritm decat cheia emitentului', 'pki.mjs', 'const altAlg = verificate.find((l) => !(l.alg === p.pkAlg));', "const altAlg = verificate.find((l) => !(l.alg === p.pkAlg) && Boolean(Number('0')));", 'algoritmul declarat e al cheii emitentului'], + ['[AIP-25 C3] selfSignatureValid nu mai cere algoritmul cheii', 'pki.mjs', 'return c.alg === c.pkAlg && c.subjectRaw.equals(c.issuerRaw)', 'return c.subjectRaw.equals(c.issuerRaw)', 'algoritmul declarat e al cheii emitentului'], ]; // 1. martorul: originalul trebuie sa fie verde, si din el se ia numarul de probe diff --git a/pq-pki/test/proba.mjs b/pq-pki/test/proba.mjs index f646ce9..1ca6899 100644 --- a/pq-pki/test/proba.mjs +++ b/pq-pki/test/proba.mjs @@ -268,6 +268,47 @@ await test('forma: algoritm exterior diferit de cel interior (ALG_MISMATCH), par e = null; try { P.parseCert(Buffer.concat([leaf, Buffer.from([0])])); } catch (x) { e = x; } has(e && e.message, 'bytes after the structure', 'coada'); const nc = Buffer.from([0x30, 0x81, 0x03, 0x02, 0x01, 0x05]); e = null; try { P.parseCert(nc); } catch (x) { e = x; } has(e && e.message, 'non-canonical', 'lungime lunga pentru una scurta'); }); +// AIP-25 C3 (D-537, 2026-10-19): algoritmul de semnatura pe care il declara un certificat (si o lista de revocare) e cel al CHEII emitentului +// (RFC 5280 s.4.1.2.3 si 5.1.2.2: algoritmul cu care a semnat CA-ul; RFC 9881: ML-DSA-44/65/87 sunt trei algoritmi). Pana atunci semnatura se +// verifica cu cheia emitentului oricare ar fi algoritmul declarat, deci o politica ce citeste algoritmul declarat era mintita. Cazurile sunt +// facute aici: TBS-ul cu OID-ul de semnatura schimbat, RESEMNAT cu cheia adevarata a emitentului (semnatura e buna, numai declaratia minte). +const OIDS = { 'ml-dsa-44': '0609608648016503040311', 'ml-dsa-65': '0609608648016503040312', 'ml-dsa-87': '0609608648016503040313' }; +function declara(der, din, cu, cheie) { + const [tbsEl] = Dm.children(Dm.parse(der)); const tbs = Buffer.from(tbsEl.raw); + const a = Buffer.from(OIDS[din], 'hex'), p = tbs.indexOf(a); if (p < 0) throw new Error('fixtura: OID-ul ' + din + ' nu e in TBS'); + const t2 = Buffer.from(tbs); Buffer.from(OIDS[cu], 'hex').copy(t2, p); + return Dm.seq(t2, Dm.seq(Dm.oid(P.ALG[cu])), Dm.bits(crypto.sign(null, t2, cheie))); +} +await test('algoritmul declarat e al cheii emitentului [AIP-25 C3]: o frunza care declara ml-dsa-87, semnata de o radacina ml-dsa-44: ALG_KEY la noi, "unable to get local issuer" la OpenSSL; o radacina care declara alt algoritm decat cheia ei: ALG_KEY, si la OpenSSL nu e auto-semnata; o lista de revocare care declara alt algoritm: CRL_ALG_KEY la noi, "CRL signature failure" la OpenSSL; martorul aceleiasi forme, cu algoritmul adevarat: OK la amandoi', () => { + const k44 = P.generateKey('ml-dsa-44'), k65 = P.generateKey('ml-dsa-65'); + const r44 = P.issue({ issuer: null, signingKey: k44.privateKey, subject: { cn: 'C3 Root 44' }, publicKey: k44.publicKey, ca: true, pathLen: 0, days: 2 }); + const fr = P.issue({ issuer: r44, signingKey: k44.privateKey, subject: { cn: 'localhost' }, publicKey: k65.publicKey, days: 1, dns: ['localhost'], eku: ['serverAuth'] }); + const fr87 = declara(fr, 'ml-dsa-44', 'ml-dsa-87', k44.privateKey); + eq(P.parseCert(fr87).alg, 'ml-dsa-87', 'fixtura: frunza declara ml-dsa-87'); + const martor = declara(fr, 'ml-dsa-44', 'ml-dsa-44', k44.privateKey); // aceeasi constructie, algoritmul adevarat: trebuie sa treaca + eq(P.verifyChain({ leaf: martor, roots: [r44], host: 'localhost' }).ok, true, 'martorul resemnat cu algoritmul adevarat'); + const v = P.verifyChain({ leaf: fr87, roots: [r44], host: 'localhost' }); eq(v.code, 'ALG_KEY', 'noi, frunza ' + v.reason); has(v.reason, 'ml-dsa-87', 'motivul numeste algoritmul declarat'); has(v.reason, 'ml-dsa-44', 'motivul numeste cheia emitentului'); + const fR = w('c3-r44.pem', r44), fM = w('c3-martor.pem', martor), fF = w('c3-fr87.pem', fr87); + eq(ossl(['verify', '-x509_strict', '-purpose', 'sslserver', '-CAfile', fR, fM]).code, 0, 'openssl martorul'); + const of = ossl(['verify', '-x509_strict', '-purpose', 'sslserver', '-CAfile', fR, fF]); eq(of.code === 0, false, 'openssl frunza ' + of.out); has(of.out, 'unable to get local issuer', 'openssl frunza'); + // radacina auto-semnata cu cheia ml-dsa-44 care declara ml-dsa-87 + const r87 = declara(r44, 'ml-dsa-44', 'ml-dsa-87', k44.privateKey); + const vr = P.verifyChain({ leaf: fr, roots: [r87], host: 'localhost' }); eq(vr.code, 'ALG_KEY', 'noi, radacina ' + vr.reason); + eq(P.selfSignatureValid(r87), false, 'selfSignatureValid pe radacina care minte'); eq(P.selfSignatureValid(r44), true, 'selfSignatureValid pe radacina buna'); + has(ossl(['verify', '-x509_strict', '-CAfile', w('c3-r87.pem', r87), f('c3-r87.pem')]).out, 'unable to get local issuer', 'openssl radacina'); + // lista de revocare a radacinii, semnata cu cheia ml-dsa-44, care declara ml-dsa-87; martorul: aceeasi lista cu algoritmul adevarat + const l = P.crl({ issuer: r44, signingKey: k44.privateKey, revoked: [], days: 7, number: 1 }); + const l87 = declara(l, 'ml-dsa-44', 'ml-dsa-87', k44.privateKey), lm = declara(l, 'ml-dsa-44', 'ml-dsa-44', k44.privateKey); + eq(P.verifyChain({ leaf: fr, roots: [r44], host: 'localhost', crls: [lm], requireCrl: true }).ok, true, 'martorul listei'); + const vl = P.verifyChain({ leaf: fr, roots: [r44], host: 'localhost', crls: [l87], requireCrl: true }); eq(vl.code, 'CRL_ALG_KEY', 'noi, lista ' + vl.reason); + const ol = ossl(['verify', '-x509_strict', '-purpose', 'sslserver', '-crl_check', '-CAfile', fR, '-CRLfile', w('c3-l87.pem', l87, 'X509 CRL'), w('c3-fr.pem', fr)]); + eq(ol.code === 0, false, 'openssl lista ' + ol.out); has(ol.out, 'CRL signature failure', 'openssl lista'); + // o cheie ml-dsa-44 (1312 octeti) declarata ml-dsa-87 in SPKI e refuzata la citire (lungimea FIPS 204), inainte de orice semnatura + const sp = Buffer.from(P.parseCert(fr).spki); const s87 = Buffer.from(sp); const po = s87.indexOf(Buffer.from(OIDS['ml-dsa-65'], 'hex')); Buffer.from(OIDS['ml-dsa-87'], 'hex').copy(s87, po); + const tbsS = Dm.seq(...Dm.children(Dm.parse(P.parseCert(fr).tbs)).map((x) => (x.raw.equals(sp) ? s87 : x.raw))); + let e = null; try { P.parseCert(Dm.seq(tbsS, Dm.seq(Dm.oid(P.ALG['ml-dsa-44'])), Dm.bits(crypto.sign(null, tbsS, k44.privateKey)))); } catch (x) { e = x; } eq(e && e.code, 'SPKI', 'cheia 65 declarata 87 in SPKI'); + return 'OpenSSL: frunza "' + (of.out.split('\n').find((x) => /error \d+/.test(x)) || '').trim() + '"; lista "' + (ol.out.split('\n').find((x) => /error \d+/.test(x)) || '').trim() + '"'; +}); // [A5] SPKI const cuSpkiRau = (der) => { const c = P.parseCert(der); const [algEl] = Dm.children(Dm.parse(c.spki)); const s = Dm.seq(algEl.raw, Dm.bits(Buffer.alloc(10, 7))); return { c, tbs: Dm.seq(...Dm.children(Dm.parse(c.tbs)).map((k) => (k.raw.equals(c.spki) ? s : k.raw))) }; }; await test('cheie publica malformata [A5]: SPKI de 10 octeti sub id-ml-dsa pe frunza, pe intermediar si pe radacina: verdict SPKI (cu lungimea) la noi, fara exceptie; "decode error" la OpenSSL; o cheie de 1312 octeti sub id-ml-dsa-65 e tot SPKI', () => {