verify-layer: signed heads. keygen makes the operator's ML-DSA-65 head key; attest-head --sign-key signs the head statement in the form the AIP-23 reference verifier checks at its signature level (outside the statement, so the statementHash and its notarization are unchanged); verify-log --attested --signer requires the expected key, and without it says who signed and that no expected signer was compared. A signature says who vouches for the head, not when and not that the history is true. Tests 44/44 with the reference verifier (39 run and 5 skipped without it); negative control 9/9 here.

This commit is contained in:
Aere Network 2026-09-29 22:58:20 +03:00
parent 6e62b1ad1a
commit 35d711fa55
5 changed files with 122 additions and 24 deletions

View File

@ -10,7 +10,7 @@ command of the verification layer, which needs `ethers`.
| [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 |
| [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL |
| [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Go, PEM blocks, dependency manifests): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow |
| [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and the head of the chain can be notarized on Aere Network for post-quantum finality; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth) |
| [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and the head of the chain can be signed by the operator (ML-DSA-65) and notarized on Aere Network for post-quantum finality; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth) |
| [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content |
| [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass |
| [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again |
@ -30,7 +30,7 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
| pq-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`test/control-negativ-hsm-incredere.mjs`) |
| pq-pki | 27/27 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5 | 22/22 (`node test/control-negativ.mjs`) |
| crypto-inventory | 37/37 (`node test/proba.mjs`); cost on hostile input 8/8 linear (`node test/proba-timp.mjs`) | 18/18 (`node test/control-negativ.mjs`); cost 3/3 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 | 34/34 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere-node> node proba-sidecar.mjs`); without it 30 run, 4 are reported as skipped and the exit code is 2 | 6/6 in this repository (`node control-negativ-sidecar.mjs`; its seventh case compares with the version from the development history and is skipped here) |
| verify-layer | 44/44 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere-node> node proba-sidecar.mjs`); without it 39 run, 5 are reported as skipped and the exit code is 2 | 9/9 in this repository (`node control-negativ-sidecar.mjs`; its tenth case compares with the version from the development history and is skipped here) |
| 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 | remediation 7/7, compliance report 3/3 in this repository |

View File

@ -22,8 +22,14 @@ Node.js 24, no dependencies; `notarize-head` alone needs `ethers` (`npm install`
proof of `firstSeen[statementHash]`). `verify-log --attested <head.json>` requires that today's log **continues** that head:
the same first `count` entries, the entry `count - 1` hashing to the attested head. A rewritten or shortened history fails.
So: publish heads often (every deployment, or on a timer), and judge a log against the latest head you hold from outside the
host, never on its own.
- **A head can be signed (2026-09-29).** `keygen` makes an ML-DSA-65 key for the operator; `attest-head --sign-key` signs the
head statement in the form the AIP-23 reference verifier checks at its `signature` level; `verify-log --attested head.json
--signer head.pub.pem` then requires that the head was signed by that key. A signature says **who** vouches for the head, not
**when** (the notarization says when) and not that the history is true: whoever holds the key can sign the head of a rewritten
history, and without `--signer` a head signed by anyone passes, which the output says.
So: publish heads often (every deployment, or on a timer), sign them with the operator's key, and judge a log against the latest
head you hold from outside the host, with the signer you expect, never on its own.
## Commands
@ -31,8 +37,9 @@ host, never on its own.
node sidecar.mjs record --kind deployment --name <name> --version <v> --content-file <file> [--host h] [--log p] [--at T]
node sidecar.mjs record --kind proof --proof-file <envelope.json> # any AIP-23 envelope, after checking its form and hash
node sidecar.mjs scan --source docker|kubernetes --input <export.json> [--host h] [--log p]
node sidecar.mjs verify-log --log p [--attested head.json] # 0 intact (and continues the head), 1 broken or not continued
node sidecar.mjs attest-head --log p [--host h] [--out head.json] # the head of the chain as an aere-audit-head envelope
node sidecar.mjs verify-log --log p [--attested head.json [--signer head.pub.pem]] # 0 intact (and continues the head), 1 broken or not continued
node sidecar.mjs attest-head --log p [--host h] [--out head.json] [--sign-key head.key.pem] # the head as an aere-audit-head envelope
node sidecar.mjs keygen --out <dir> # the operator's ML-DSA-65 head key (head.key.pem 0600, head.pub.pem)
node sidecar.mjs notarize-head --head head.json --rpc <url> --key-file <file> [--notary 0x..] [--out notarized.json]
node sidecar.mjs bundle --log p [--out bundle.json] # {host, count, head, chainOk, entries[]} for a console
@ -80,26 +87,28 @@ post-quantum` under a certified anchor of the testnet, and `VALID`.
## Tests
node proba-sidecar.mjs # 34 checks
node proba-sidecar.mjs # 44 checks
node control-negativ-sidecar.mjs # puts each guard back to its absent form and requires the named check to fail
`proba-sidecar.mjs` records runtime, deployment and envelope entries; checks that a modified entry breaks the chain at its
seq, a changed hash is caught, and nothing is appended to a broken chain; that a chain **rebuilt from genesis** passes
`verify-log` alone (the stated limit) but fails against the attested head, as do a shortened log and a modified attestation,
while a log that only grew passes; that two writers appending 150 entries each at the same time leave an intact chain of 300;
while a log that only grew passes; that a signed head passes the verifier's signature level and `--signer`, while a head
signed by another key, an unsigned head with `--signer`, and a damaged signature are refused; that two writers appending 150 entries each at the same time leave an intact chain of 300;
that a line that is not JSON is reported as broken at its seq; that the Docker and Kubernetes adapters record only running
containers and no secret value; and that every envelope is `VALID` for the AIP-23 reference verifier. The verifier is looked
for at `AERE_VERIFY_PROOF` (for example `verify-proof.mjs` from the `aere-node` repository, after `npm install` there); without
it, the four checks that need it are reported as skipped and the exit code is 2, not 0.
it, the five checks that need it are reported as skipped and the exit code is 2, not 0.
`control-negativ-sidecar.mjs` measured 7 of 7 on 2026-09-29: the version before the review (taken from the development
history, skipped where that history is absent) and six plantings, each disabling one guard (the writer lock, the head
comparison, the length check, the attestation hash check, the handling of an unreadable line, the digest check of `--attested`).
`control-negativ-sidecar.mjs` measured 10 of 10 on 2026-09-29: the version before the review (taken from the development
history, skipped where that history is absent) and nine plantings, each disabling one guard (the writer lock, the head
comparison, the length check, the attestation hash check, the handling of an unreadable line, the digest check of `--attested`,
the head signature check, the comparison with `--signer`, the refusal of an unsigned head when `--signer` is given).
`proba-notarizare-testnet.mjs --key-file <testnet key>` repeats the on-chain run above (9 checks, it needs a funded testnet key).
## What it is not (yet)
It does not report to a live console over the network, and it has no per-cloud adapters that read deployments from the AWS,
Azure or GCP APIs with credentials: the runtime adapter above, without credentials, is the first one. It does not sign entries
with a host key, so it cannot tell two hosts apart by itself; the host name in an entry is also a declaration. No third party
Azure or GCP APIs with credentials: the runtime adapter above, without credentials, is the first one. It signs heads, not
each entry, and the host name in an entry is still a declaration. No third party
has reviewed it.

View File

@ -8,6 +8,9 @@
// P4 statementHash-ul capului atestat nu se mai reface -> R CONTROL (cap manipulat) rosu
// P5 un rand care nu e JSON arunca din nou (cadere) -> N rosu
// P6 --attested nu se mai cere digest -> N CONTROL rosu
// P7 semnatura capului nu se mai verifica (punctul 34, 2026-09-29) -> S CONTROL (semnatura stricata) rosu
// P8 semnatarul capului nu se mai compara cu --signer -> S CONTROL (alta cheie) rosu
// P9 un cap nesemnat trece cand --signer il cere semnat -> S CONTROL (nesemnat) rosu
// Copiile stau LANGA original (importul lui proof-kinds e relativ) si se sterg; originalul trebuie sa ramana octet cu octet.
// node control-negativ-sidecar.mjs -> 0 toate prinse, 1 una scapata, 2 STRICAT
import crypto from 'node:crypto';
@ -31,6 +34,9 @@ const T = {
necitibil: 'N: rand care nu e JSON',
digest: 'N CONTROL: --attested care nu e digest',
concurent: 'C: doi scriitori concurenti',
semnStricata: 'S CONTROL: o semnatura de cap stricata',
altaCheie: 'S CONTROL: capul semnat de alta cheie',
nesemnat: 'S CONTROL: un cap nesemnat',
};
function caz(id, text, tinte) {
@ -65,6 +71,9 @@ const P = [
['P4', "if (!eDigest(cap.statementHash) || sha256(Buffer.from(JSON.stringify(st), 'utf8')) !== cap.statementHash.toLowerCase()) return", `if (!eDigest(cap.statementHash) || (sha256(Buffer.from(JSON.stringify(st), 'utf8')) !== cap.statementHash.toLowerCase() && ${NU})) return`, [T.capMan]],
['P5', "try { return JSON.parse(l); } catch { return { necitibil: true }; }", `try { return JSON.parse(l); } catch (e) { if (${NU}) return { necitibil: true }; throw e; }`, [T.necitibil]],
['P6', 'if (!eDigest(attested)) {', `if (!eDigest(attested) && ${NU}) {`, [T.digest]],
['P7', "if (!ok) return { ok: false, semnat: true, keyId: idCheie(pub), motiv: 'the head signature does not verify over the statement' };", `if (!ok && ${NU}) return { ok: false, semnat: true, keyId: idCheie(pub), motiv: 'the head signature does not verify over the statement' };`, [T.semnStricata]],
['P8', 'if (idCheie(asteptat) !== idCheie(pub)) return', `if (idCheie(asteptat) !== idCheie(pub) && ${NU}) return`, [T.altaCheie]],
['P9', 'if (!s) return semnatarPem ? { ok: false,', `if (!s) return (semnatarPem && ${NU}) ? { ok: false,`, [T.nesemnat]],
];
for (const [id, a, b, tinte] of P) {
let t = planta(nou, a, b);

View File

@ -6,6 +6,8 @@
// atestat (verify-log --attested); la fel un jurnal TAIAT si un cap atestat manipulat; un jurnal care continua capul trece
// C doi scriitori concurenti nu rup lantul (lacatul)
// N un rand care nu e JSON iese RUPT la seq-ul lui, nu cadere; --attested care nu e digest e refuzat
// S capul SEMNAT ML-DSA-65 (punctul 34): semnatura verificata de verificatorul AIP-23 de referinta; --signer cere cheia
// operatorului; un cap rescris si re-semnat de alta cheie, unul nesemnat cerut semnat, o semnatura stricata: refuzate
// node proba-sidecar.mjs -> 0 toate cum trebuia, 1 cel putin una rea, 2 cel putin una SARITA (verificatorul AIP-23 lipseste)
// Mediu: AERE_SIDECAR_MODUL (copia masurata, pentru controlul negativ), AERE_VERIFY_PROOF (verificatorul AIP-23; implicit cel din
// depozitul de dezvoltare, tools/aere-proof-protocol/verify.mjs, sau verify-proof.mjs din aere-node/tools intr-o copie publica).
@ -95,6 +97,36 @@ try {
const rM = side(['verify-log', '--log', log, '--attested', capMf]);
cer('R CONTROL: cap atestat manipulat (count schimbat, statementHash vechi) -> refuzat (cod 1)', rM.cod === 1 && /modified attestation/.test(rM.out));
// ---- S: capul semnat (2026-09-29, punctul 34) ------------------------------------------------------------------------------
// citirile de mai jos nu arunca: un fisier nescris (o versiune fara capete semnate) inroseste verificarea lui, nu opreste proba
// (masurat 2026-09-29: pe versiunea veche proba se oprea aici si verificarile N de dupa nu mai rulau)
const jr = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return {}; } };
const kOp = path.join(T, 'op'), kAt = path.join(T, 'atacator');
const rk = side(['keygen', '--out', kOp]); side(['keygen', '--out', kAt]);
cer('S: keygen scrie cheia capetelor (ML-DSA-65) si tipareste numai amprenta', rk.cod === 0 && /key 0x[0-9a-f]{64}/.test(rk.out) && !/PRIVATE KEY/.test(rk.out) && fs.existsSync(path.join(kOp, 'head.key.pem')));
cer('S CONTROL: keygen nu suprascrie o cheie existenta (cod 2)', side(['keygen', '--out', kOp]).cod === 2);
const hs = path.join(T, 'head-semnat.json');
cer('S: attest-head --sign-key -> cap semnat, acelasi statementHash ca cel nesemnat (semnatura e in afara declaratiei)', side(['attest-head', '--log', log, '--out', hs, '--host', 'h1', '--at', '2026-09-26T09:04:00Z', '--sign-key', path.join(kOp, 'head.key.pem')]).cod === 0 && jr(hs).statementHash === jr(hout).statementHash && (jr(hs).signature || {}).scheme === 'ml-dsa-65');
if (areVerificator) {
const vs = (() => { try { return execFileSync(process.execPath, [VERIFY, hs], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); } catch (e) { return String(e.stdout || ''); } })();
cer('S: semnatura capului e PASSED la nivelul signature al verificatorului AIP-23', /PASSED\s*\]\s*signature/.test(vs));
} else sari('S: semnatura capului e PASSED la nivelul signature al verificatorului AIP-23');
const rs = side(['verify-log', '--log', log, '--attested', hs, '--signer', path.join(kOp, 'head.pub.pem')]);
cer('S: jurnalul continua capul semnat de operator, cu --signer = cheia lui (cod 0)', rs.cod === 0 && /the expected signer/.test(rs.out));
cer('S: fara --signer, verify-log spune cine a semnat si ca nu s-a comparat cu o cheie asteptata', /not checked against an expected signer/.test(side(['verify-log', '--log', log, '--attested', hs]).out));
// atacatorul reface lantul de la geneza (ca la R), isi face capul si il semneaza cu cheia LUI
const hAt = path.join(T, 'head-atacator.json');
side(['attest-head', '--log', logRef, '--out', hAt, '--host', 'h1', '--at', '2026-09-26T09:04:00Z', '--sign-key', path.join(kAt, 'head.key.pem')]);
cer('S: fara --signer, lantul refacut si capul lui semnat de atacator trec (limita: o semnatura spune cine, nu care)', side(['verify-log', '--log', logRef, '--attested', hAt]).cod === 0);
const rAt = side(['verify-log', '--log', logRef, '--attested', hAt, '--signer', path.join(kOp, 'head.pub.pem')]);
cer('S CONTROL: capul semnat de alta cheie, cu --signer = operatorul -> refuzat (cod 1)', rAt.cod === 1 && /another key/.test(rAt.out));
const rNe = side(['verify-log', '--log', log, '--attested', hout, '--signer', path.join(kOp, 'head.pub.pem')]);
cer('S CONTROL: un cap nesemnat, cu --signer -> refuzat (cod 1)', rNe.cod === 1 && /not signed/.test(rNe.out));
const cs = jr(hs); if (cs.signature) { const sg = Buffer.from(cs.signature.signature, 'base64'); sg[100] ^= 1; cs.signature.signature = sg.toString('base64'); }
const hsr = path.join(T, 'head-semnat-rau.json'); fs.writeFileSync(hsr, JSON.stringify(cs));
const rSr = side(['verify-log', '--log', log, '--attested', hsr]);
cer('S CONTROL: o semnatura de cap stricata -> refuzat chiar fara --signer (cod 1)', rSr.cod === 1 && /does not verify/.test(rSr.out));
// ---- N: intrari nevalide -------------------------------------------------------------------------------------------------
const logN = path.join(T, 'necitibil.log'); fs.writeFileSync(logN, JSON.stringify(intrari[0]) + '\n{nu e json\n');
const rN = side(['verify-log', '--log', logN]);

View File

@ -18,8 +18,9 @@
// sidecar record --kind proof --proof-file f.json (leaga orice plic AIP-23 gata facut, dupa ce ii verifica forma si integritatea)
// sidecar scan --source docker|kubernetes --input export.json (fiecare container care RULEAZA; NUMAI numele variabilelor de mediu,
// linia de comanda ca hash; exportul il face operatorul)
// sidecar verify-log --log p [--attested cap.json] -> 0 intreg (si continua capul atestat), 1 rupt sau necontinuat
// sidecar attest-head --log p [--out f] -> capul lantului ca plic AIP-23 aere-audit-head
// sidecar verify-log --log p [--attested cap.json [--signer head.pub.pem]] -> 0 intreg (si continua capul atestat), 1 rupt sau necontinuat
// sidecar attest-head --log p [--out f] [--sign-key head.key.pem] -> capul lantului ca plic AIP-23 aere-audit-head, semnat ML-DSA-65
// sidecar keygen --out <dosar> -> cheia ML-DSA-65 cu care operatorul semneaza capetele (head.key.pem 0600, head.pub.pem)
// sidecar notarize-head --head cap.json --rpc URL --key-file f [--notary 0x..] [--out f]
// sidecar bundle --log p [--out f] -> buraf {host, count, head, entries[]} pentru consola planului de control
// Mesajele catre utilizator sunt in engleza. Numai Node 24 (ethers numai pentru notarize-head).
@ -69,17 +70,47 @@ export function verificaJurnal(intrari) {
* Cere: plicul intreg (statementHash se reface), lantul intreg, cel putin `count` intrari, si hash-ul intrarii count-1 == head.
* Returneaza {ok, motiv, count, extra} - extra = intrarile scrise dupa cap.
*/
export function verificaFataDeCap(intrari, cap) {
export function verificaFataDeCap(intrari, cap, { semnatar = null } = {}) {
const st = cap && cap.statement;
if (!cap || cap.kind !== 'aere-audit-head-attestation' || !st || st.kind !== 'aere-audit-head') return { ok: false, motiv: 'the file is not an aere-audit-head attestation' };
if (!eDigest(cap.statementHash) || sha256(Buffer.from(JSON.stringify(st), 'utf8')) !== cap.statementHash.toLowerCase()) return { ok: false, motiv: 'the attestation statementHash does not match its statement (modified attestation)' };
const sg = verificaSemnaturaCap(cap, semnatar);
if (!sg.ok) return { ok: false, motiv: sg.motiv };
if (!Number.isInteger(st.count) || st.count < 0 || !eDigest(st.head)) return { ok: false, motiv: 'the attestation has no valid count and head' };
const v = verificaJurnal(intrari);
if (!v.ok) return { ok: false, motiv: `the log is broken at seq ${v.rupt}: ${v.motiv}` };
if (intrari.length < st.count) return { ok: false, motiv: `the log has ${intrari.length} entries and the attested head covers ${st.count}: entries were removed` };
const h = st.count === 0 ? GENEZA : intrari[st.count - 1].hash;
if (h !== st.head.toLowerCase()) return { ok: false, motiv: `entry ${st.count - 1} is not the attested head: the history before the attested point was rewritten` };
return { ok: true, count: st.count, extra: intrari.length - st.count };
return { ok: true, count: st.count, extra: intrari.length - st.count, semnatDe: sg.semnat ? sg.keyId : null };
}
// ---- capul SEMNAT (2026-09-29, roadmap punctul 34, "jurnale semnate") ---------------------------------------------------------
// Capul poate purta semnatura ML-DSA-65 a operatorului, in forma pe care o verifica verificatorul AIP-23 de referinta (nivelul
// `signature`): { scheme: 'ml-dsa-65', publicKey: SPKI DER base64, signature: base64 peste textul canonic al declaratiei }. Semnatura
// spune CINE garanteaza capul, nu CAND (asta o da notarizarea) si nu ca istoria e adevarata. Cine tine cheia poate semna si un cap
// al unei istorii rescrise: semnatura leaga capul de operator, notarizarea il leaga de un moment; `--signer` cere cheia asteptata.
const idCheie = (pub) => sha256(pub.export({ type: 'spki', format: 'der' }));
export function semneazaCap(plic, cheiePem) {
const priv = crypto.createPrivateKey(cheiePem);
if (priv.asymmetricKeyType !== 'ml-dsa-65') throw new Error('the head signing key must be ML-DSA-65 (sidecar keygen)');
const pub = crypto.createPublicKey(priv);
const sig = crypto.sign(null, Buffer.from(JSON.stringify(plic.statement), 'utf8'), priv);
return { ...plic, signature: { scheme: 'ml-dsa-65', publicKey: pub.export({ type: 'spki', format: 'der' }).toString('base64'), signature: sig.toString('base64') } };
}
/** @returns {{ok:boolean, semnat:boolean, keyId?:string, motiv?:string}} semnatarPem: cheia publica asteptata (optional) */
export function verificaSemnaturaCap(plic, semnatarPem = null) {
const s = plic && plic.signature;
if (!s) return semnatarPem ? { ok: false, semnat: false, motiv: 'the head is not signed, and a signer was required' } : { ok: true, semnat: false };
if (String(s.scheme).toLowerCase() !== 'ml-dsa-65' || !s.publicKey || !s.signature) return { ok: false, semnat: true, motiv: 'the head signature is not an ml-dsa-65 signature' };
let pub; try { pub = crypto.createPublicKey({ key: Buffer.from(String(s.publicKey), 'base64'), format: 'der', type: 'spki' }); } catch { return { ok: false, semnat: true, motiv: 'the head signature public key cannot be read' }; }
let ok = false; try { ok = crypto.verify(null, Buffer.from(JSON.stringify(plic.statement), 'utf8'), pub, Buffer.from(String(s.signature), 'base64')); } catch { ok = false; }
if (!ok) return { ok: false, semnat: true, keyId: idCheie(pub), motiv: 'the head signature does not verify over the statement' };
if (semnatarPem) {
let asteptat; try { asteptat = crypto.createPublicKey(semnatarPem); } catch { return { ok: false, semnat: true, motiv: 'the expected signer key cannot be read' }; }
if (idCheie(asteptat) !== idCheie(pub)) return { ok: false, semnat: true, keyId: idCheie(pub), motiv: `the head is signed by another key (${idCheie(pub).slice(0, 18)}), not the expected signer` };
}
return { ok: true, semnat: true, keyId: idCheie(pub) };
}
// ---- adaptorul de runtime: exportul pe care operatorul il face el insusi, fara acreditari de cloud ------------------------------
@ -252,12 +283,15 @@ async function main() {
}
case 'verify-log': {
const intrari = citesteJurnal(log);
const af = get('--attested');
const af = get('--attested'); const sf = get('--signer');
if (sf && !af) { console.log('--signer needs --attested <head>: the signature is on the attested head'); return 2; }
if (af) {
let cap;
let cap, semnatar = null;
try { cap = JSON.parse(fs.readFileSync(af, 'utf8')); } catch (e) { console.log(`cannot read the attestation ${af}: ${e.message}`); return 2; }
const r = verificaFataDeCap(intrari, cap);
if (r.ok) { console.log(`log CONTINUES the attested head: its first ${r.count} entries are the attested ones, ${r.extra} written after`); return 0; }
if (sf) { try { semnatar = fs.readFileSync(sf, 'utf8'); } catch (e) { console.log(`cannot read the signer key ${sf}: ${e.message}`); return 2; } }
const r = verificaFataDeCap(intrari, cap, { semnatar });
const cine = r.semnatDe ? `the head is signed by key ${r.semnatDe.slice(0, 18)}${semnatar ? ', the expected signer' : ' (not checked against an expected signer: --signer)'}` : 'the head is not signed';
if (r.ok) { console.log(`log CONTINUES the attested head: its first ${r.count} entries are the attested ones, ${r.extra} written after; ${cine}`); return 0; }
console.log(`log does NOT continue the attested head: ${r.motiv}`); return 1;
}
const v = verificaJurnal(intrari);
@ -271,11 +305,25 @@ async function main() {
const v = verificaJurnal(intrari);
if (!v.ok) { console.log(`refusing to attest a broken chain (seq ${v.rupt})`); return 1; }
const statement = { v: 1, kind: 'aere-audit-head', host, count: v.count, head: v.head, createdAt: at };
const plic = { v: 1, kind: 'aere-audit-head-attestation', statement, statementHash: sha256(Buffer.from(JSON.stringify(statement), 'utf8')) };
let plic = { v: 1, kind: 'aere-audit-head-attestation', statement, statementHash: sha256(Buffer.from(JSON.stringify(statement), 'utf8')) };
const kf = get('--sign-key');
if (kf) { try { plic = semneazaCap(plic, fs.readFileSync(kf, 'utf8')); } catch (e) { console.log(`cannot sign the head: ${String(e.message).replace(/[0-9a-fA-F]{32,}/g, '<hex>').slice(0, 160)}`); return 2; } }
const out = get('--out'); const s = JSON.stringify(plic, null, 1);
if (out) { fs.writeFileSync(out, s); console.log('written', out, plic.statementHash); } else console.log(s);
return 0;
}
case 'keygen': {
// cheia ML-DSA-65 a operatorului pentru capete; cheia privata ramane in fisierul ei (0600), nu se tipareste nimic din ea
const out = get('--out'); if (!out) { console.error('keygen --out <dir>'); return 2; }
fs.mkdirSync(out, { recursive: true });
const kf = path.join(out, 'head.key.pem');
if (fs.existsSync(kf)) { console.error(`${kf} exists; refusing to overwrite a key`); return 2; }
const { publicKey, privateKey } = crypto.generateKeyPairSync('ml-dsa-65');
fs.writeFileSync(kf, privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });
fs.writeFileSync(path.join(out, 'head.pub.pem'), publicKey.export({ type: 'spki', format: 'pem' }), { mode: 0o644 });
console.log(`head signing key written to ${out} (head.key.pem, head.pub.pem): key ${idCheie(publicKey)}`);
return 0;
}
case 'notarize-head': {
// capul jurnalului pe AereNotary, ca verificatorul AIP-23 sa ii dea finalitate post-cuantica (ancora certificata -> radacina de
// stare -> dovada Merkle a lui firstSeen[statementHash]). Garzi: lantul se CITESTE de pe RPC (eth_chainId), nu se crede din
@ -334,7 +382,7 @@ async function main() {
}
default:
console.log('AERE Verification Layer (sidecar). Commands: record | scan | verify-log | attest-head | notarize-head | bundle');
console.log(' sidecar record --kind runtime --artifact f --attested 0x.. sidecar verify-log --log p [--attested cap.json] sidecar attest-head --log p --out f');
console.log(' sidecar record --kind runtime --artifact f --attested 0x.. sidecar verify-log --log p [--attested cap.json [--signer head.pub.pem]] sidecar attest-head --log p --out f [--sign-key head.key.pem] sidecar keygen --out dir');
return cmd ? 1 : 0;
}
}