115 lines
9.1 KiB
Markdown
115 lines
9.1 KiB
Markdown
# AERE Verification Layer (sidecar)
|
|
|
|
A sidecar that runs next to a deployment, on any cloud or on premises, and keeps an **audit log** of it. Every entry is an
|
|
AERE Proof Protocol envelope (AIP-23), built by the same `proof-kinds` builder and checked by the same AIP-23 reference
|
|
verifier as every other AERE proof, and every entry covers the hash of the one before it (an append-only hash chain,
|
|
`hash = sha256(seq | prev | envelope)`, genesis `0x00..00`). The head of the chain can be notarized on Aere Network, which
|
|
gives the history up to that head post-quantum finality that anyone can check.
|
|
|
|
Node.js 24, no dependencies; `notarize-head` alone needs `ethers` (`npm install` in this directory).
|
|
|
|
## What it proves, and what it does not
|
|
|
|
- **The content of the entries is what the host says.** The sidecar runs on the host and reads the export the operator makes.
|
|
An entry proves that the host *declared* a container was running, with this image and this configuration, at the time it
|
|
declared (`--at` is also a declaration). It does not prove that the declaration is true.
|
|
- **The chain has no key.** Whoever can write the log file can rebuild the whole chain from genesis with any content, and
|
|
`verify-log` on that file alone reports it intact. A partial edit is caught (the chain breaks at the edited entry and
|
|
`verify-log` names it), a full rewrite is not.
|
|
- **What is checkable without trusting the host is the history before a published head.** `attest-head` turns the current
|
|
head into an envelope; `notarize-head` records its hash on the AereNotary contract; the AIP-23 reference verifier then gives
|
|
it post-quantum finality (the most recent certified anchor, the parent header bound by hash, its state root, and a Merkle
|
|
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.
|
|
|
|
- **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
|
|
|
|
node sidecar.mjs record --kind runtime --artifact <file> --attested 0x<sha256> [--host h] [--log p] [--at T]
|
|
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 [--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
|
|
|
|
The default log is `aere-audit.log` in the current directory. Writers take a lock file beside the log (`<log>.lock`, holding
|
|
the writer's process id), so two writers on the same host do not break the chain; a lock left by a process that no longer
|
|
exists is removed after 10 seconds. A log that is already broken is never appended to.
|
|
|
|
`record --kind runtime` records that a file on the host (the binary that runs) has the digest you expected: `matches` says
|
|
whether it does. `record --kind deployment` records the digest of a deployed artifact, never its content.
|
|
|
|
## The runtime adapter
|
|
|
|
`scan` reads an export the operator makes on the host, so the sidecar needs no access to the Docker daemon, the cluster, or
|
|
any cloud credential:
|
|
|
|
docker inspect $(docker ps -q) > export.json # then: scan --source docker --input export.json
|
|
kubectl get pods -A -o json > export.json # then: scan --source kubernetes --input export.json
|
|
|
|
For every container that is **running**, it writes a canonical descriptor (image and image id, name, namespace and pod on
|
|
Kubernetes, start time, restart count, the **hash** of the command line, the **names** of the environment variables, the
|
|
destinations of the mounts) and records the descriptor's digest as a deployment entry. No environment value, no argument in
|
|
clear and no host path of a mount is stored. The descriptors themselves are written beside the log
|
|
(`<log>.descriptori.jsonl`), so an auditor can re-hash each one and compare it with the chain. An export with no running
|
|
container, or one that is not in the expected shape, is refused rather than recorded as "zero deployments".
|
|
|
|
A hash does not hide a guessable secret: if a password is passed on a command line, anyone who knows the rest of the command
|
|
can test guesses against `commandSha256`. Pass secrets through files or the environment, not arguments.
|
|
|
|
## Notarization
|
|
|
|
`notarize-head` reads the chain id from the RPC endpoint (it does not trust an argument), refuses an attestation whose
|
|
statement does not match its hash before sending anything, and sends nothing if the head is already notarized. On chain 2800
|
|
(Aere Network mainnet) the notarization is a real transaction paid by the key's account, so it is sent only with
|
|
`AERE_CONFIRM_MAINNET=yes`. The key is read from a file (`d=<hex>` or `PRIVATE_KEY=0x<hex>`) and never printed; error
|
|
messages are cut before any long hex string.
|
|
|
|
`dovezi-notarizare-testnet/` holds a real run on the public testnet (chain 28001): a log of three test deployments and its
|
|
head, notarized in block 3,699,939 on 2026-09-27. Anyone can check both halves:
|
|
|
|
node sidecar.mjs verify-log --log dovezi-notarizare-testnet/audit.log --attested dovezi-notarizare-testnet/cap-notarizat-28001.json
|
|
node verify-proof.mjs dovezi-notarizare-testnet/cap-notarizat-28001.json # the AIP-23 reference verifier (aere-node/tools)
|
|
|
|
The first says the log is the one that was notarized; the second, measured on 2026-09-29, reports `finality: PASSED
|
|
post-quantum` under a certified anchor of the testnet, and `VALID`.
|
|
|
|
## Tests
|
|
|
|
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 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 five checks that need it are reported as skipped and the exit code is 2, not 0.
|
|
|
|
`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 signs heads, not
|
|
each entry, and the host name in an entry is still a declaration. No third party
|
|
has reviewed it.
|