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:
parent
6e62b1ad1a
commit
35d711fa55
@ -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 |
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user