identity: verify-sdjwt on the command line (an SD-JWT+KB checked with the issuer key you give; Aere Cloud runs it behind POST /v1/identity/sd-jwt/verify)
The issuer key (a public JWK, or the public keys of an AERE identity) is checked first: one that cannot be read, is private, or is not a key for the algorithm asked exits 2 with the reason, instead of reporting the token INVALID. --json prints compact JSON (indented output grew with the square of the claims' nesting depth). --at judges at a given time, as for verify. Test on the RFC 9901 example presentation: valid at its own time, invalid now, a private key refused. Tests: SD-JWT 23/23, negative control 28/28; identity 44/44, negative control 49/49.
This commit is contained in:
parent
4ffec8d7e3
commit
5f1e60b8d2
@ -15,7 +15,7 @@ notarization command of the verification layer, and the agents' x402 wallet (EIP
|
||||
| [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass |
|
||||
| [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again |
|
||||
| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, either holding the payment key for the owner or co-signing from a 2-of-2 contract wallet that neither the agent nor the owner can spend alone; both run on the public testnet with their evidence; and the chain as the witness of a ledger: a notarized head, read through the AIP-23 verifier under a post-quantum certified anchor, bounds entry times from below (a backdated entry is caught) |
|
||||
| [`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 |
|
||||
| [`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 (`identity-cli.mjs verify-sdjwt`, behind Aere Cloud's POST /v1/identity/sd-jwt/verify) |
|
||||
|
||||
Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them.
|
||||
|
||||
@ -35,7 +35,7 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
|
||||
| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six 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 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), 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 | 44/44 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 19/19 (`node proba-travel-rule.mjs`), SD-JWT 22/22 (`node proba-sdjwt.mjs`, the RFC 9901 vectors included), measured 2026-09-30 | 49/49 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 19/19 (`node control-negativ-travel-rule.mjs`); SD-JWT 28/28 (`node control-negativ-sdjwt.mjs`) |
|
||||
| identity | 44/44 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 19/19 (`node proba-travel-rule.mjs`), SD-JWT 23/23 (`node proba-sdjwt.mjs`, the RFC 9901 vectors included), measured 2026-09-30 | 49/49 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 19/19 (`node control-negativ-travel-rule.mjs`); SD-JWT 28/28 (`node control-negativ-sdjwt.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 51/51, 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 (`x402/proba-x402-testnet.mjs`, `x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository |
|
||||
|
||||
Code comments, most function and variable names (also many exported between the files of a component), test names and control
|
||||
|
||||
@ -163,6 +163,9 @@ const shown = presentSdJwt({ sdJwt, reveal: ['age_over_18', { element: 'RO', in:
|
||||
const r = verifySdJwt(shown, { issuerKey, audience, nonce, expectedIssuer: 'https://issuer.example' }); // { valid, reason, payload, disclosed, 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`.
|
||||
|
||||
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
|
||||
else with the key in A.5, verifies and rebuilds every input claim; the example presentation with its Key Binding JWT verifies and
|
||||
@ -195,7 +198,7 @@ node proba-conformitate.mjs # 15: compliance policies judged on real pr
|
||||
node control-negativ-conformitate.mjs # on a copy, each of 14 guards removed -> its own named test turns red
|
||||
node proba-travel-rule.mjs # 19: two VASPs with registry credentials, the whole exchange, and each attack of the review
|
||||
node control-negativ-travel-rule.mjs # on a copy, each of 19 guards removed -> its own named test turns red
|
||||
node proba-sdjwt.mjs # 22: the RFC 9901 vectors, SD-JWTs issued with AERE keys (EdDSA and ML-DSA-65), each attack
|
||||
node proba-sdjwt.mjs # 23: the RFC 9901 vectors, SD-JWTs issued with AERE keys (EdDSA and ML-DSA-65), each attack
|
||||
node control-negativ-sdjwt.mjs # on a copy, each of 28 guards removed -> its own named test turns red
|
||||
```
|
||||
|
||||
|
||||
@ -63,7 +63,8 @@ const PLANTARI = [
|
||||
['o lista stricata acuza credentialul', L, "verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys); } catch { return false; } });", "verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys); } catch (e) { throw e; } });", 'intrari stricate date verificatorului'],
|
||||
['o revocare stricata acuza prezentarea', L, "try { canonical(R); } catch { nejudecat(`${et}: a revocation`, 'ignored: not readable'); continue; }", '', 'intrari stricate date verificatorului'],
|
||||
['linia de comanda suprascrie o cheie', C, 'if (privat && fs.existsSync(f)) throw new Folosire(', 'if (false) throw new Folosire(', 'linia de comanda'],
|
||||
['verify din linia de comanda iese 0 si pe INVALID', C, 'return r.valid ? 0 : 1;', 'return 0;', 'linia de comanda'],
|
||||
// 2026-09-30: tiparul poarta randul de dinainte, fiindca verify-sdjwt are si el `return r.valid ? 0 : 1;`
|
||||
['verify din linia de comanda iese 0 si pe INVALID', C, "JSON.stringify(r.claims));\n }\n return r.valid ? 0 : 1;", "JSON.stringify(r.claims));\n }\n return 0;", 'linia de comanda'],
|
||||
];
|
||||
const FISIERE = [L, C, 'proba-identity.mjs'];
|
||||
function copie() {
|
||||
|
||||
@ -44,7 +44,7 @@ const PLANTARI = [
|
||||
];
|
||||
function copie() {
|
||||
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-sdjwt-ctl-'));
|
||||
for (const f of [S, 'identity.mjs', 'proba-sdjwt.mjs', 'fixturi-rfc9901.json']) fs.copyFileSync(path.join(AICI, f), path.join(t, f));
|
||||
for (const f of [S, 'identity.mjs', 'identity-cli.mjs', 'proba-sdjwt.mjs', 'fixturi-rfc9901.json']) fs.copyFileSync(path.join(AICI, f), path.join(t, f));
|
||||
return t;
|
||||
}
|
||||
function ruleaza(t) {
|
||||
|
||||
@ -15,9 +15,13 @@
|
||||
// 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
|
||||
// [--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)
|
||||
// Coduri de iesire: 0 VALID / COMPLIANT, 1 INVALID / NOT COMPLIANT, 2 intrare respinsa (motivul pe stderr, `error: ...`).
|
||||
// O valoare de --claim se citeste ca JSON daca e JSON (true, 42, {"a":1}), altfel ca text.
|
||||
import fs from 'node:fs';
|
||||
import crypto from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as I from './identity.mjs';
|
||||
@ -124,7 +128,33 @@ async function main() {
|
||||
else console.log(r.compliant ? `COMPLIANT with ${r.policyId} (${r.policyHash})` : `NOT COMPLIANT with ${r.policyId}:\n ` + r.reasons.join('\n '));
|
||||
return r.compliant ? 0 : 1;
|
||||
}
|
||||
throw new Folosire('usage: identity-cli.mjs keygen|pub|id|issue|status-list|delegate|revoke|present|verify|comply ... (see README.md)');
|
||||
if (cmd === 'verify-sdjwt') {
|
||||
// 2026-09-30: SD-JWT (IETF RFC 9901) cu sdjwt.mjs; jetonul dintr-un fisier text (serializarea compacta, fara spatii)
|
||||
const { verifySdJwt, publicJwk } = await import('./sdjwt.mjs');
|
||||
let jeton; try { jeton = fs.readFileSync(cere('--sd-jwt'), 'utf8').trim(); } catch (e) { throw new Folosire(`cannot read --sd-jwt: ${e.message}`); }
|
||||
const cheie = citeste(cere('--issuer-key'));
|
||||
const alg = get('--issuer-alg') ?? 'EdDSA';
|
||||
if (!['EdDSA', 'Ed25519', 'ES256', 'ML-DSA-65'].includes(alg)) throw new Folosire('--issuer-alg is EdDSA, ES256 or ML-DSA-65');
|
||||
// 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)
|
||||
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 cere2 = alg === 'ES256' ? k.asymmetricKeyType === 'ec' && k.asymmetricKeyDetails.namedCurve === 'prime256v1' : alg === 'ML-DSA-65' ? k.asymmetricKeyType === 'ml-dsa-65' : k.asymmetricKeyType === 'ed25519';
|
||||
if (!cere2) throw new Error(`the key is a ${k.asymmetricKeyType} key, not one for ${alg}`);
|
||||
} catch (e) { throw new Folosire('--issuer-key: ' + String(e.message || e).replace(/^sd-jwt: /, '').slice(0, 160)); }
|
||||
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 r = verifySdJwt(jeton, { issuerKey: cheie, issuerAlg: alg, audience: cere('--audience'), nonce: cere('--nonce'), expectedIssuer: get('--expected-issuer'),
|
||||
maxAgeS, now: momentul(), ...(tip != null ? { expectedTyp: tip } : {}) });
|
||||
// JSON compact: cu indentare, iesirea creste cu patratul adancimii afirmatiilor (un jeton de 7 KB imbricat de 2.500 de ori dadea 6 MB)
|
||||
if (are('--json')) console.log(JSON.stringify(r));
|
||||
else console.log(r.valid ? `VALID: issued by ${r.issuer}${r.issuerChecked ? '' : ' (not checked against an expected issuer)'}; disclosed: ${r.disclosed.join(', ') || 'nothing'}` : 'INVALID: ' + r.reason);
|
||||
return r.valid ? 0 : 1;
|
||||
}
|
||||
throw new Folosire('usage: identity-cli.mjs keygen|pub|id|issue|status-list|delegate|revoke|present|verify|comply|verify-sdjwt ... (see README.md)');
|
||||
}
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().then((c) => { process.exitCode = c; }, (e) => { console.error('error: ' + (e.message || e)); process.exitCode = e instanceof Folosire ? 2 : 1; });
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import os from 'node:os';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as I from './identity.mjs';
|
||||
import * as J from './sdjwt.mjs';
|
||||
@ -193,5 +195,17 @@ test('REVIZUIRE: crit (orice forma), base64url necanonic al semnaturii, KB expir
|
||||
cere(refuz(judeca(baza + kbExp), /key binding: expired/), 'KB expirat a trecut');
|
||||
});
|
||||
|
||||
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 {
|
||||
fs.writeFileSync(path.join(T, 'p.txt'), F.presentation); fs.writeFileSync(path.join(T, 'k.json'), JSON.stringify(F.issuerJwk));
|
||||
const run = (...a) => spawnSync(process.execPath, [path.join(AICI, 'identity-cli.mjs'), 'verify-sdjwt', '--sd-jwt', 'p.txt', '--issuer-key', 'k.json', '--issuer-alg', 'ES256', '--audience', KB.aud, '--nonce', KB.nonce, ...a], { cwd: T, encoding: 'utf8' });
|
||||
const a = run('--at', new Date(KB.iat * 1000).toISOString(), '--json'); cere(a.status === 0 && JSON.parse(a.stdout).valid === true, 'la iat: ' + a.status + ' ' + a.stderr);
|
||||
const b = run(); cere(b.status === 1 && /outside 300 s/.test(b.stdout), 'acum: ' + b.status + ' ' + b.stdout);
|
||||
fs.writeFileSync(path.join(T, 'k.json'), JSON.stringify(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey.export({ format: 'jwk' })));
|
||||
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 }); }
|
||||
});
|
||||
|
||||
console.log(`\naere-sd-jwt: ${treceri}/${treceri + esecuri.length} cum trebuia`);
|
||||
process.exitCode = esecuri.length ? 1 : 0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user