device-identity: third adversarial review and its repairs (with the release left out of a bundle, the firmware the device attests is still judged against the manufacturer's policies; a verifier clock and every time read strictly as RFC 3339 UTC, days that do not exist refused; C1, DEL and direction marks escaped on the terminal; a key file is never overwritten); probe 372/372, negative control 220/220. conformance: the halves of the device verifier mapped again (263/263 under the negative control) and the present-tense counts brought to the measurement of 2026-10-14 (467 vectors, 467/467 with the AIP-23 verifier as served)
This commit is contained in:
parent
ed4d3d79d8
commit
86779c8334
@ -51,8 +51,8 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
|
||||
| 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 on 2026-10-05 with the AIP-23 verifier, 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 422/422, an independent Python implementation passes the 62 it claims and the other 360 are reported not covered, 70,358 malformed inputs and 183 malformed envelope files each get a verdict); without `--online` the nine chain vectors are reported unmeasured and the exit code is 2 | 181/181 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 | 175/175 with the AIP-23 verifier, measured 2026-10-05 (`AERE_VERIFY_PROOF=... node proba-device.mjs`: twenty-two bundles that must be VALID; thirty-three with something not brought, not read or not readable, each PARTIAL for its named causes; one hundred and six attack rows, each refused for exactly its named rules, among them a clone with another key, a rolled-back firmware, 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; 804 damaged bundles each given a verdict; the command line through files only; the evidence recorded on testnet 28001 judged again offline); without the verifier 174 run, one is reported as skipped and the exit code is 2; 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) | 139/139 (`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; 91 more plantings, most of them one condition of a compound check taken out alone, each with its rows) |
|
||||
| conformance | 12/12 on 2026-10-14 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 467/467, an independent Python implementation passes the 62 it claims and the other 405 are reported not covered, 96,757 malformed inputs and 183 malformed envelope files each get a verdict); without `--online` the nine chain vectors are reported unmeasured and the exit code is 2 | 263/263 on 2026-10-14: 181/181 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 |
|
||||
|
||||
Code comments, most function and variable names (also many exported between the files of a component), test names and control
|
||||
|
||||
@ -94,9 +94,9 @@ not rule ids. Their own probes and negative controls stay what checks them.
|
||||
|
||||
## The vectors
|
||||
|
||||
`vectori/` holds 422 vectors (413 offline, 9 that read testnet 28001; 16 MB, since every device and identity vector carries its ML-DSA-65 keys and signatures), listed with their digests in `vectori/INDEX.json`:
|
||||
`vectori/` holds 467 vectors (458 offline, 9 that read testnet 28001; 19 MB, since every device and identity vector carries its ML-DSA-65 keys and signatures), listed with their digests in `vectori/INDEX.json`:
|
||||
envelope 64, 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 65, device-bundle 149. By expected verdict: 59 `VALID`, 47 `PARTIAL`, 316 `INVALID`. None is typed by hand
|
||||
10, audit-consistency 4, agent-ledger 18, identity-presentation 65, device-bundle 194. By expected verdict: 64 `VALID`, 66 `PARTIAL`, 337 `INVALID`. None is typed by hand
|
||||
(`node genereaza-vectori.mjs --verifica` regenerates them and compares byte for byte):
|
||||
|
||||
- the 16 AIP-23 kinds come from their producers on fixed inputs (the same envelopes as the published AIP-23 kind vectors); the audit
|
||||
@ -121,10 +121,10 @@ envelope 64, payment-risk 11, payment-receipt 10, market-timeline 26, dataset 22
|
||||
from the chain (two read calls).
|
||||
|
||||
An `INVALID` vector names its `base` and its `mutation`: **one operation** (set, remove or add at one JSON path) applied to the input
|
||||
of a vector that is not invalid, and it must be refused for exactly the rules in `expected.failed`, not merely refused: 209 of the
|
||||
316 are made this way. Where the rule sits under a hash or a signature, one place is not reachable (changing an amount under the
|
||||
of a vector that is not invalid, and it must be refused for exactly the rules in `expected.failed`, not merely refused: 221 of the
|
||||
337 are made this way. Where the rule sits under a hash or a signature, one place is not reachable (changing an amount under the
|
||||
instruction hash trips the binding first; a false decision in a ledger must still be signed by the agent; a disclosure the issuer never signed must still be signed for by
|
||||
the holder; an authorization by a previous owner must still be signed by that owner): the other 107 are made at
|
||||
the holder; an authorization by a previous owner must still be signed by that owner): the other 116 are made at
|
||||
the producer, the commitments above the change recomputed with the published formulas, and every path that moved is declared
|
||||
(`mutation.changed`). Eleven vectors expect more than one rule: rules that guard the same field cannot be separated (a looser policy shown
|
||||
under a receipt also makes the risk verdict not reproduce; a Proof of AI envelope that is changed is also no longer the one the
|
||||
@ -136,9 +136,9 @@ key breaks both the id derivation and the match with the manufacturer's credenti
|
||||
|
||||
```
|
||||
AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools, with verify-anchor.mjs, mpt.mjs, falcon512.mjs, registry-v3.mjs and package.json beside it and npm install done>
|
||||
node conformance.mjs --impl "node implementare-aere.mjs" # the published verifiers behind the one verdict form: 413 offline vectors
|
||||
node conformance.mjs --impl "node implementare-aere.mjs" # the published verifiers behind the one verdict form: 458 offline vectors
|
||||
node conformance.mjs --impl "node implementare-aere.mjs" --online # and the 9 that read testnet 28001 (nothing is sent)
|
||||
node conformance.mjs --impl "python3 implementare-integritate.py" # an independent implementation: 62 vectors, 360 reported NOT COVERED
|
||||
node conformance.mjs --impl "python3 implementare-integritate.py" # an independent implementation: 62 vectors, 405 reported NOT COVERED
|
||||
node conformance.mjs --impl "<your command>" # yours
|
||||
node implementare-aere.mjs <artifact.json> [--online] # one artifact { artifact, input }: its verdict
|
||||
```
|
||||
@ -161,12 +161,32 @@ refusal. Without `--online` an implementation must not open a connection.
|
||||
`implementare-integritate.py` is the second implementation: Python, standard library only, written from the rules and not from the
|
||||
JavaScript. It claims 6 of the 181 rules (the form and the statement hash of an envelope; the form, the commitment, the salted field
|
||||
commitments and the RFC 9162 inclusion of a dataset record) and passes all 62 vectors it claims. It cannot check an ML-DSA signature
|
||||
or read a chain, says so in its capabilities, and the runner reports the rest as not covered: 360 vectors, among them every vector of
|
||||
or read a chain, says so in its capabilities, and the runner reports the rest as not covered: 405 vectors, among them every vector of
|
||||
the trust chain, the audit log, the agents' ledger, the identity presentations and the device bundles. For those six verifiers the
|
||||
suite has one implementation, ours.
|
||||
|
||||
## How the suite itself is checked
|
||||
|
||||
Measured on 2026-10-14 (UTC), after the third review of the device verifier (Node.js 24.14.1; the AIP-23 reference verifier fetched
|
||||
from aere.network/tools with its four sibling files and `npm install` done in an empty folder, as a stranger would; testnet 28001
|
||||
read, nothing sent):
|
||||
|
||||
- `node proba-conformance.mjs --online`: 12 rows green, 0 red (in this published copy the comparison of the 16 kind envelopes with
|
||||
the published AIP-23 kind vectors is printed as information; the twelfth row is green where their generator is). The rules and the
|
||||
vectors are what the code derives now: 181 rules of 11 verifiers, 467 vectors. Every rule has a vector that expects it (181 of 181);
|
||||
every artifact kind has a valid vector (15); every invalid vector is its base with one operation (221) or one declared producer
|
||||
change (116); the published verifiers pass 467/467; the independent implementation passes all 62 it claims and the other 405 are
|
||||
reported not covered; 96,757 malformed inputs given to the module verifiers each got a verdict and none threw (one vector,
|
||||
`device-bundle/chain-of-64.partial`, is larger than 300,000 characters and is not damaged this way); 183 malformed envelope files
|
||||
given to the reference verifier each got a verdict.
|
||||
- `node proba-conformance.mjs --control-negativ --online`: 263 rows green, 0 red: 181 of 181 rules, 76 of 76 conditions of the device
|
||||
verifier (under them 96 distinct device vectors fail, each exactly under the conditions written for it), six controls of the
|
||||
instruments (an implementation that calls everything valid fails 426 of the 458 offline vectors, every one that is not plainly valid).
|
||||
|
||||
The finer counts in the paragraphs below (how many plantings of the device verifier no vector reaches, how many device vectors the
|
||||
purpose separation breaks at once, and the `--pe-loc` measurement) are those of the measurement of 2026-10-05; they were not counted
|
||||
again on 2026-10-14.
|
||||
|
||||
Measured on 2026-10-05 (Node.js 24.14.1, Python 3.14.4, testnet 28001 read):
|
||||
|
||||
- `node proba-conformance.mjs --online`: every row green, 11 in this copy (a twelfth, the comparison of the 16 kind envelopes with
|
||||
@ -247,7 +267,7 @@ On 2026-10-05, while the identity verifier was being enrolled, one more, fixed t
|
||||
## What it is not, and what is not measured
|
||||
|
||||
- **Not a certification, not an audit, not a mark.** Passing says that an implementation agrees with the published verifiers on these
|
||||
422 inputs. It does not say either of them is correct, and no third party has reviewed the verifiers, the vectors or this suite.
|
||||
467 inputs. It does not say either of them is correct, and no third party has reviewed the verifiers, the vectors or this suite.
|
||||
- **The vectors and the verifiers have the same authors.** The expected verdicts were written by the people who wrote the verifiers.
|
||||
The second implementation is independent in code and language, not in authorship, and it covers 6 rules of 181; the other
|
||||
175 rules, among them every rule of the trust chain, the audit log, the agents' ledger, the identity verifier and the device
|
||||
@ -289,7 +309,7 @@ On 2026-10-05, while the identity verifier was being enrolled, one more, fixed t
|
||||
survive a sale: after a transfer it is named and not applied (the manufacturer's is final). A retirement by an owner that holds
|
||||
across a sale needs a witness of when it and the transfer were signed, which is not built; every time in a transfer, an
|
||||
authorization and a status record of these vectors is the statement of the key holder that signed it.
|
||||
- **What a damaged artifact can still pass.** Of the 70,358 malformed inputs, 3,889 were still accepted, at 192 distinct paths (81 of them in device bundles: a status record that decided nothing, such as a suspension already lifted, a stranger's record, a record not in effect yet or one of a previous owner; a recall list or a firmware policy that could not be read and stood beside a clean one, which damaged is no longer signed by the manufacturer and counts like one that was not handed; the envelope and its notarization claim, which are optional; and the signature of a Proof of Software attestation, which the device verifier does not read): parts a
|
||||
- **What a damaged artifact can still pass.** Of the 96,757 malformed inputs (measured 2026-10-14), 4,280 were still accepted, at 187 distinct paths (76 of them in device bundles; the kinds that follow were listed on 2026-10-05, when the count was 81 and the device verifier had not had its third review: a status record that decided nothing, such as a suspension already lifted, a stranger's record, a record not in effect yet or one of a previous owner; a recall list or a firmware policy that could not be read and stood beside a clean one, which damaged is no longer signed by the manufacturer and counts like one that was not handed; the envelope and its notarization claim, which are optional; and the signature of a Proof of Software attestation, which the device verifier does not read): parts a
|
||||
verifier treats as optional (the transaction facts of a receipt, the envelope of a timeline, the head of an audit log), a policy
|
||||
field removed that falls back to its default, a log or a ledger without its last entry (still a valid shorter one), a revocation that was not in effect (damaged, it is
|
||||
ignored as before), and fields no rule reads
|
||||
|
||||
@ -74,9 +74,9 @@ const JUMATATI_DEVICE = {
|
||||
'identities are compared by their keys alone (the id half of the comparison taken out)': ['manufacture-only.manufacture-holder-other-id'],
|
||||
'identities are compared by their id alone (the keys half of the comparison taken out)': ['manufacture-only.manufacture-holder-other-keys'],
|
||||
'a manufacture credential that has not begun is accepted': ['manufacture-only.manufacture-window-not-yet'],
|
||||
'an expired manufacture credential is accepted': ['manufacture-only.manufacture-window'],
|
||||
'an expired manufacture credential is accepted': ['manufacture-only.manufacture-window', 'manufacture-only.manufacture-window-expired-30s'],
|
||||
'a recall list of another manufacturer is believed': ['recall-list-of-another-manufacturer.partial'],
|
||||
'an expired recall list is read as current': ['recall-list-expired.partial', 'recall-list-unreadable-beside-a-clean-one.partial'],
|
||||
'an expired recall list is read as current': ['recall-list-expired.partial', 'recall-list-unreadable-beside-a-clean-one.partial', 'recall-list-expired-30s.partial'],
|
||||
'a recall list whose validity is not a time is dropped in silence': ['recall-list-unreadable-beside-a-clean-one.partial'],
|
||||
'a hardware root in the manufacture credential passes in silence': ['manufacture-hardware-root.partial'],
|
||||
'the id of who signed a release is not derived from its keys': ['release-only.firmware-signer-id'],
|
||||
@ -86,13 +86,13 @@ const JUMATATI_DEVICE = {
|
||||
'a firmware policy of a stranger is believed': ['sold-once.firmware-signer'],
|
||||
'a firmware policy of another model is believed': ['policy-of-another-model.partial'],
|
||||
'a firmware policy of a version this verifier does not read is dropped in silence': ['policy-of-another-version-beside-a-clean-one.partial'],
|
||||
'an expired firmware policy is read as current': ['policy-expired.partial', 'policy-validity-unreadable-beside-a-clean-one.partial'],
|
||||
'an expired firmware policy is read as current': ['policy-expired.partial', 'policy-validity-unreadable-beside-a-clean-one.partial', 'policy-expired-30s.partial'],
|
||||
'a firmware policy that has not begun is read as current': ['policy-not-yet.partial'],
|
||||
'a retracted entry that is not a digest is read as nothing retracted': ['policy-unreadable-beside-a-clean-one.partial', 'policy-unreadable.partial'],
|
||||
'a floor that is not a whole number is read as a floor': ['policy-floor-unreadable-beside-a-clean-one.partial'],
|
||||
'a floor that is not a whole number is read as a floor': ['policy-floor-unreadable-beside-a-clean-one.partial', 'policy-floor-negative.partial'],
|
||||
'a policy whose validity is not a time is dropped in silence': ['policy-validity-unreadable-beside-a-clean-one.partial'],
|
||||
'a policy that cannot be read passes in silence': ['policy-floor-unreadable-beside-a-clean-one.partial', 'policy-of-another-version-beside-a-clean-one.partial', 'policy-unreadable-beside-a-clean-one.partial', 'policy-unreadable.partial', 'policy-validity-unreadable-beside-a-clean-one.partial'],
|
||||
'the Proof of Software attestation is not required to list the image': ['with-software.firmware-software'],
|
||||
'a policy that cannot be read passes in silence': ['policy-floor-unreadable-beside-a-clean-one.partial', 'policy-of-another-version-beside-a-clean-one.partial', 'policy-unreadable-beside-a-clean-one.partial', 'policy-unreadable.partial', 'policy-validity-unreadable-beside-a-clean-one.partial', 'policy-floor-negative.partial'],
|
||||
'the Proof of Software attestation is not required to list the image': ['with-software.firmware-software', 'with-software.firmware-software-not-a-proof-of-software'],
|
||||
'the Proof of Software attestation is not required to be the one named': ['with-software.firmware-software-not-the-one-named'],
|
||||
'the id of who GIVES a transfer is not derived from its keys': ['chain-unrooted.transfer-ids-giver'],
|
||||
'the id of who RECEIVES a transfer is not derived from its keys': ['chain-only.transfer-ids'],
|
||||
@ -107,27 +107,27 @@ const JUMATATI_DEVICE = {
|
||||
'an authorization that ends before the time of the attestation is accepted': ['sold-once.authorization-window-ends-before-attestation'],
|
||||
'the authorization is not judged at the time of the attestation at all': ['sold-once.authorization-window-after-attestation', 'sold-once.authorization-window-ends-before-attestation'],
|
||||
'the beginning of an authorization is not judged at all': ['sold-once.authorization-window-after-attestation', 'sold-once.authorization-window-begins-after-tolerance', 'sold-once.authorization-window-not-yet'],
|
||||
"the manufacturer's retirement, and the owner's, are lifted like a suspension": ['bought-back.status-retired-first-ownership', 'resold.status-retired-by-manufacturer', 'retired-then-active.status-retired', 'sold-once.status-retired', 'sold-once.status-retired-by-owner', 'sold-once.status-retired-by-owner-no-head', 'sold-once.status-retired-date-not-a-time', 'sold-once.status-retired-dated-just-ahead'],
|
||||
"the manufacturer's records stop counting once the device is sold": ['resold.status-retired-by-manufacturer', 'retired-then-active.status-retired', 'sold-once.status-retired', 'sold-once.status-retired-date-not-a-time', 'sold-once.status-retired-dated-just-ahead', 'status-record-of-another-version.partial', 'suspension-lifted.status-suspended-by-manufacturer'],
|
||||
'anyone can write a status record': ['retired-by-a-stranger.valid', 'status-of-a-stranger.partial'],
|
||||
"the manufacturer's retirement, and the owner's, are lifted like a suspension": ['bought-back.status-retired-first-ownership', 'resold.status-retired-by-manufacturer', 'retired-then-active.status-retired', 'sold-once.status-retired', 'sold-once.status-retired-by-owner', 'sold-once.status-retired-by-owner-no-head', 'sold-once.status-retired-date-not-a-time', 'sold-once.status-retired-dated-just-ahead', 'sold-once.status-retired-device-id-upper-case'],
|
||||
"the manufacturer's records stop counting once the device is sold": ['resold.status-retired-by-manufacturer', 'retired-then-active.status-retired', 'sold-once.status-retired', 'sold-once.status-retired-date-not-a-time', 'sold-once.status-retired-dated-just-ahead', 'status-record-of-another-version.partial', 'suspension-lifted.status-suspended-by-manufacturer', 'sold-once.status-retired-device-id-upper-case'],
|
||||
'anyone can write a status record': ['retired-by-a-stranger.valid', 'status-of-a-stranger.partial', 'status-of-an-untrusted-manufacturer.valid', 'status-of-another-trusted-manufacturer.partial'],
|
||||
'a record of a PREVIOUS owner binds the owner of now': ['resold-after-retirement.valid', 'resold-after-suspension.valid', 'resold-seller-retires-after.valid'],
|
||||
"the owner's retirement and suspension count only under the current ownership head": ['bought-back.status-retired-first-ownership', 'bought-back.status-suspended-first-ownership', 'sold-once.status-retired-by-owner-no-head'],
|
||||
'an "active" record of the owner is believed under any ownership head': ['bought-back-active-of-first-ownership.partial'],
|
||||
'a record of who may give it that cannot be read is ignored (the registry is read as clean)': ['status-record-of-another-version.partial'],
|
||||
'a record of another version, or with a state the verifier does not know, is read as one of this version': ['status-record-of-another-version.partial'],
|
||||
'a record of who may give it that cannot be read is ignored (the registry is read as clean)': ['status-record-of-another-version.partial', 'suspended-record-number-as-text.partial'],
|
||||
'a record of another version, or with a state the verifier does not know, is read as one of this version': ['status-record-of-another-version.partial', 'active-record-number-not-whole.partial', 'suspended-record-number-as-text.partial'],
|
||||
'a record of another device is applied to this one': ['retirement-of-another-device.valid'],
|
||||
'DOUBLE: a status record under an id its keys do not derive is applied (the derivation in the status loop AND the id half of the comparison, both out)': ['manufacture-only.manufacture-holder-other-id', 'retired-under-an-id-not-derived.valid'],
|
||||
"a suspension is lifted by another signer's later record": ['suspension-lifted.status-suspended-by-manufacturer'],
|
||||
'a suspension expires with its record': ['sold-once.status-suspended-record-expired'],
|
||||
'an expired "active" record is believed': ['active-record-expired.partial'],
|
||||
'of two records with one number the laxer holds': ['sold-once.status-suspended-same-number'],
|
||||
'an expired "active" record is believed': ['active-record-expired.partial', 'active-record-expired-30s.partial'],
|
||||
'of two records with one number the laxer holds': ['sold-once.status-suspended-same-number', 'sold-once.status-suspended-same-number-listed-first'],
|
||||
'a record dated in the future is applied': ['retirement-not-yet-in-effect.valid'],
|
||||
"a record dated just ahead of the verifier's clock is given no tolerance": ['sold-once.status-retired-dated-just-ahead'],
|
||||
'a retirement whose date cannot be read is dropped': ['sold-once.status-retired-date-not-a-time'],
|
||||
'the clone check compares the device id alone (the key half taken out)': ['sold-once.attestation-device-claimed-id'],
|
||||
'an attestation dated in the future is accepted up to the allowed age': ['sold-once.freshness-ahead'],
|
||||
'an attestation dated in the future is accepted, however far': ['sold-once.freshness-ahead'],
|
||||
'an old attestation is accepted, however old': ['sold-once.freshness'],
|
||||
'an old attestation is accepted, however old': ['sold-once.freshness', 'sold-once.freshness-330s'],
|
||||
'an attestation dated ahead is given no tolerance': ['attested-just-ahead.valid', 'sold-once.authorization-window-begins-after-tolerance', 'sold-once.authorization-window-ends-before-attestation'],
|
||||
'the sequence the device attests is not compared with the release': ['sold-once.attestation-firmware-sequence'],
|
||||
'the digest the device attests is not compared with the release': ['sold-once.attestation-firmware-digest'],
|
||||
@ -138,7 +138,7 @@ const JUMATATI_DEVICE = {
|
||||
'a claim of notarization passes for a reading': ['notarization-not-read.partial', 'reading-of-another-envelope.partial'],
|
||||
'a reading of another envelope is taken for this one': ['reading-of-another-envelope.partial'],
|
||||
'a reading whose time is in milliseconds is taken for one in seconds': ['reading-in-milliseconds.partial'],
|
||||
'a reading that cannot be read passes in silence': ['reading-in-milliseconds.partial'],
|
||||
'a reading that cannot be read passes in silence': ['reading-in-milliseconds.partial', 'reading-time-as-text.partial'],
|
||||
'a bundle of another version is read': ['sold-once.form-version'],
|
||||
'transfers that are not a list are read': ['sold-once.form-transfers-not-a-list'],
|
||||
};
|
||||
|
||||
@ -45,6 +45,31 @@ is `{ link, name, pass, detail, rule }`; a check that could not be made is `{ pa
|
||||
shape than the one it reads or beyond the number of things it judges: each keeps the verdict from VALID.
|
||||
- **VALID**: every link was brought and judged, and what could revoke it was read.
|
||||
|
||||
**PARTIAL is not a weaker VALID.** Whoever hands the bundle chooses which links it brings. A link left out turns every refusal
|
||||
that link would have drawn into its `-missing` cause: a device the manufacturer retired, an authorization a stranger signed, an
|
||||
attestation to an old nonce are each INVALID, and each is PARTIAL with the refused link left out (row W08 of the probe). It is
|
||||
never VALID that way, and what can still be judged without the link is judged: with the release left out, the firmware the device
|
||||
attests is judged against the policies all the same (rows W01 to W07). Admit on `verdict === 'VALID'`, or on a PARTIAL whose causes
|
||||
are exactly the ones you chose not to bring. The field `valid` is `true` when no rule refused, PARTIAL included: it is not a
|
||||
verdict.
|
||||
|
||||
Every cause, by the name `unjudged` gives it:
|
||||
|
||||
| cause | what was not brought or not read | verdict |
|
||||
|---|---|---|
|
||||
| `manufacture-missing`, `firmware-missing`, `authorization-missing`, `attestation-missing` | that link of the bundle | PARTIAL |
|
||||
| `ownership-unrooted` | transfers shown without the manufacture credential they start from | PARTIAL |
|
||||
| `manufacturer-trust` | the manufacturers the verifier trusts | PARTIAL |
|
||||
| `manufacture-status-unread` | the recall list the credential names (none current, one that cannot be read, or something handed in its place) | PARTIAL |
|
||||
| `firmware-policy-unread` | a current firmware policy that can be read | PARTIAL |
|
||||
| `software-unread` | the Proof of Software attestation the release names | PARTIAL |
|
||||
| `status-unread` | a status record that can be applied, or a record of the manufacturer or of the current owner that cannot be read | PARTIAL |
|
||||
| `status-other-manufacturer` | a record of another manufacturer the verifier trusts (not applied, and not judged) | PARTIAL |
|
||||
| `nonce`, `audience` | the verifier's own nonce, its own audience | PARTIAL |
|
||||
| `hardware-root` | a hardware root carried by the credential or the attestation (never judged here) | PARTIAL |
|
||||
| `witness-unread` | a reading of the notarization the envelope claims, or a reading that cannot be read | PARTIAL |
|
||||
| `manufacture-status-none`, `status-ignored`, `status-prior-ownership`, `status-future` | see below | named, VALID stands |
|
||||
|
||||
Four causes are **named and do not change the verdict** (the export `NOTATE`):
|
||||
|
||||
- `manufacture-status-none`: the manufacture credential names no recall list.
|
||||
@ -66,8 +91,12 @@ tolerance ahead** of the verifier's clock and **no more than `maxAgeS` (300 s) b
|
||||
tolerance is refused, also when it is well inside the allowed age.
|
||||
|
||||
A time is an RFC 3339 UTC text (`2026-10-05T12:00:00Z`, with up to three decimals of a second). A time without a zone, with a
|
||||
zone offset, or one that is not a date is not a time. In a link of the bundle, the bundle cannot be read (`unreadable`). In a
|
||||
firmware policy or a recall list handed to the verifier, that policy or list cannot be read (PARTIAL). In a status record, see
|
||||
zone offset, or one that is not a date is not a time, and a day that does not exist is not a date: a date parser moves
|
||||
`2026-02-30` to March 2 and `T24:00:00` to the next day, and this verifier refuses both. In a time a link of the bundle carries and
|
||||
the verifier reads (the validity of the manufacture credential and when it was issued, the date of a transfer, the window of an
|
||||
authorization, the date of the attestation), the bundle cannot be read (`unreadable`). The `issuedAt` of a release and of an
|
||||
authorization are carried and not read: nothing is decided on them. In a firmware policy or a recall list handed to the verifier,
|
||||
that policy or list cannot be read (PARTIAL). In a status record, see
|
||||
[Status](#status-whose-record-counts-and-for-how-long): a retirement or a suspension is applied all the same, an `active` is
|
||||
not believed.
|
||||
|
||||
@ -85,7 +114,9 @@ What the verifier brings, and what happens without it:
|
||||
|
||||
A configuration of the verifier that cannot be read (a clock that is not a time, a tolerance outside 0 to 600 seconds, a trusted
|
||||
manufacturer that is neither an id nor a set of public keys, a list that is not a list) is not an invalid bundle: `verifyDevice`
|
||||
throws an error that begins with `verifyDevice:`.
|
||||
throws an error that begins with `verifyDevice:`. The clock `now` is a `Date`, a number of milliseconds since 1970, or an RFC 3339
|
||||
UTC text, and nothing else: a text without a zone (which a date parser reads as local time), `"1"` (which it reads as the year
|
||||
2001), a date alone, `null` or a list are not a clock.
|
||||
|
||||
### What the verifier is handed: its shape, and how much of it is judged
|
||||
|
||||
@ -111,7 +142,11 @@ of sight in silence. Hand the verifier the records of the device it judges, not
|
||||
when a clean policy stands beside it. What the readable policies refuse is still refused. An expired policy says nothing about
|
||||
now. The same holds for a recall list of the manufacturer whose validity is not a time (`manufacture-status-unread`). A policy
|
||||
or a list counts as the manufacturer's only when it names the manufacturer as its issuer **and** its signature verifies with
|
||||
the manufacturer's keys.
|
||||
the manufacturer's keys. **With the release left out** (`firmware-missing`), the firmware the device attests, its digest and its
|
||||
sequence, is judged against the same policies of the manufacturer of the credential for its model: a retracted digest or a
|
||||
sequence below the floor is refused all the same (`firmware-retracted`, `firmware-rollback`), and no current policy is said
|
||||
(`firmware-policy-unread`). Leaving the release out keeps the verdict from VALID; it does not hide what the device itself says
|
||||
it runs.
|
||||
- **Ownership.** A transfer counts only when signed by the owner at that point of the chain (`transfer-from`), and each transfer
|
||||
is dated after the one before it. An authorization counts only when granted by the current owner of the chain shown, under its
|
||||
ownership head (`authorization-owner`, `authorization-ownership`), and when it was in force both on the verifier's clock and
|
||||
@ -122,12 +157,15 @@ of sight in silence. Hand the verifier the records of the device it judges, not
|
||||
dated ahead of it (`nonce`, `audience`, `freshness`). With the verifier's reading of the notarized envelope, an attestation
|
||||
dated after the moment its envelope was already on the chain is refused (`witness-time`); with several readings the earliest
|
||||
is the bound. A reading is a time in whole seconds: one in milliseconds, or written as text, is not used, and the verdict is
|
||||
PARTIAL (`witness-unread`).
|
||||
PARTIAL (`witness-unread`). The witness is offered by whoever hands the bundle: a bundle without an envelope is judged without
|
||||
one (on the verifier's clock and nonce), and a reading handed for an envelope the bundle does not carry is not used.
|
||||
- **Keys and signatures.** The device key must be a canonical ML-DSA-65 public key and the device id must be derived from it, in
|
||||
the manufacture credential and in the attestation, and the two must be the same key (`device-key`, `device-id`,
|
||||
`attestation-key`, `attestation-device`). An Ed25519 key in a field named ML-DSA-65 is refused. The hybrid signatures need
|
||||
both halves, and each signature must declare its scheme. Every signature, like the device key, is accepted in **one text
|
||||
only**, its canonical base64: the same bytes in another base64 text are refused.
|
||||
both halves, and each signature must declare its scheme. Every signature of a link, of a status record and of a firmware
|
||||
policy, like the device key, is accepted in **one text only**, its canonical base64: the same bytes in another base64 text are
|
||||
refused. A recall list is an identity status list, judged by the identity verifier's signature check, which does not hold its
|
||||
signature to one text; a list is named by no hash, so nothing depends on that text.
|
||||
- **The form of a link.** A link of the bundle is exactly `{ statement, signature }`, and a signature object exactly its members
|
||||
(`alg`, `ed25519`, `mldsa65`; for the device `alg`, `value`). One member more, beside the statement or inside the signature,
|
||||
is refused by the signature rule of that link (`manufacture-signature`, `firmware-signature`, `transfer-signature`,
|
||||
@ -192,7 +230,7 @@ The 47 rules are the export `REGULI` of `device.mjs`; each check carries a marke
|
||||
## Use
|
||||
|
||||
```sh
|
||||
node device-cli.mjs new-identity --out maker.keys.json # the manufacturer (key files are written 0600)
|
||||
node device-cli.mjs new-identity --out maker.keys.json # the manufacturer (a key file is created 0600, never written over)
|
||||
node device-cli.mjs new-identity --out owner.keys.json
|
||||
node device-cli.mjs new-device --out device.key.json # the device key K
|
||||
|
||||
@ -234,10 +272,13 @@ is, and the verifier names it (PARTIAL).
|
||||
|
||||
**What is printed.** Everything `verify` prints that came from a file (a model, an id, a date) passes through one function,
|
||||
`visible()`, exported by `device.mjs`: the C0 and C1 control characters, DEL, the Unicode line and paragraph separators, the
|
||||
bidirectional controls and the byte order mark are printed in a visible form (`\x1b`, `
`), and a backslash is doubled so
|
||||
that the form cannot be forged. A terminal executes nothing a bundle wrote, and a bundle cannot draw a second verdict line.
|
||||
`--json` prints JSON, whose strings escape control characters; its rows carry the text exactly as it came, so a program that
|
||||
shows them to a person passes them through `visible()` itself.
|
||||
bidirectional controls (U+061C, U+200E, U+200F, U+202A to U+202E, U+2066 to U+2069: the characters Unicode gives the property
|
||||
Bidi_Control) and the byte order mark are printed in a visible form (`\x1b`, `\u2028`), and a backslash is doubled so that the
|
||||
form cannot be forged. A terminal executes nothing a bundle wrote, and a bundle cannot draw a second verdict line. `--json`
|
||||
prints JSON through `visibleJson()`, also exported: JSON escapes C0, and every other character `visible()` replaces is written
|
||||
as a `\u` escape, so the bytes printed hold no control character but the line feeds of the layout, and the text parses to
|
||||
exactly the same value. Its rows carry the text exactly as it came, so a program that shows them to a person passes them
|
||||
through `visible()` itself.
|
||||
|
||||
`status` writes the record of whoever signs it: with the manufacturer's keys, a record of the manufacturer (no ownership head);
|
||||
with any other keys, a record under the ownership head of the chain given (`--manufacture` and every `--transfer`, in order).
|
||||
@ -329,7 +370,7 @@ returned only when no rule refused. The rows carry text as the bundle wrote it:
|
||||
processor.
|
||||
- The chain as witness on Aere Network mainnet (2800): measured only on testnet 28001, where the finality is the testnet's.
|
||||
- The chain as witness with the verifier as it is now, end to end: the run on testnet 28001 was made on 2026-10-05 before the
|
||||
repairs of that day and of the next (see below). What was recorded then (the bundles, what the verifier brought, its two
|
||||
repairs of that day, of the next and of 2026-10-14 (see below). What was recorded then (the bundles, what the verifier brought, its two
|
||||
readings) is judged again offline by the verifier of now in the probe (row D08) and gives the six verdicts recorded; nothing
|
||||
was notarized or read on the chain again.
|
||||
- A witness of time for transfers, authorizations and status records, and a witness of the ownership head: none is built.
|
||||
@ -340,7 +381,7 @@ returned only when no rule refused. The rows carry text as the bundle wrote it:
|
||||
- The conditions of the **producers** and of the key files (the functions that write links), one at a time. The search described
|
||||
below covers the verifier and everything it calls. What a producer refuses to write is a convenience to its caller, not a
|
||||
guarantee to a verifier, which must assume links written by other tools; rows D01 and D02 of the probe measure the refusals
|
||||
this page names. @@INAFARA@@ conditions lie there.
|
||||
this page names. The search counts 164 mutants there and runs none of them.
|
||||
- The conditions of `device-cli.mjs`, one at a time: the command line is covered by the plantings written by hand (the exit
|
||||
codes, the list files, what is printed), not by the search.
|
||||
- The kinds of change the search does not derive: a constant changed, an arithmetic operator changed, a statement removed, two
|
||||
@ -350,11 +391,11 @@ returned only when no rule refused. The rows carry text as the bundle wrote it:
|
||||
|
||||
| what | command | measured |
|
||||
|---|---|---|
|
||||
| the probe | `AERE_VERIFY_PROOF=<verify-proof.mjs> node proba-device.mjs` | @@PROBA@@ |
|
||||
| the negative control | `node control-negativ-device.mjs` | @@CONTROL@@ |
|
||||
| the search for conditions without a row | `AERE_ACORN=<directory of the acorn parser> node mutanti-device.mjs` | @@MUTANTI@@ |
|
||||
| the probe | `AERE_VERIFY_PROOF=<verify-proof.mjs> node proba-device.mjs` | 372/372 on 2026-10-14 (Node 24.14.1, Windows): 54 bundles that must be VALID; 77 PARTIAL, each for exactly its named causes; 204 attack rows, each refused for exactly its named rules; 37 other rows (the command line through files and processes, the envelope under the reference verifier, the evidence recorded on testnet 28001 judged again offline, the rows of the third adversarial review); 804 damaged bundles each given a verdict |
|
||||
| the negative control | `node control-negativ-device.mjs` | 220/220 on 2026-10-14: the untouched copy green; each of the 47 rules taken out, the rows that expect it and only they go red; 172 more plantings, each with exactly its rows |
|
||||
| the search for conditions without a row | `AERE_ACORN=<directory of the acorn parser> node mutanti-device.mjs` | 892 mutants of the verifier on 2026-10-14 (acorn 8.16.0): 890 killed (36 of them because the probe could no longer build its world), 2 survive, both declared in the tool with the reason no row can see them (two assertions of the verifier about itself); 164 more lie in the producers and the key files and are not run. The run before row W14 found one condition without a row (the release judged only when it is left out), and W14 was written for it |
|
||||
| the chain as witness | `AERE_TESTNET_KEY_FILE=<funded testnet key> AERE_VERIFY_PROOF=<verify-proof.mjs> node proba-device-ancora-testnet.mjs` | 10/10 on testnet 28001 on 2026-10-05, with the verifier as it was before the repairs of that day: two envelopes notarized (two transactions), both read back by the published reference verifier under a post-quantum certified anchor (after 17 s and 16 s); the honest bundle VALID with the reading and PARTIAL without it; an attestation the device dated twenty minutes ahead, judged on a clock set to the time it claims, passes without the chain and is refused with the reading (`witness-time`); the envelope of another attestation and the reading of another envelope do not witness. Evidence in `dovezi-ancora-28001/`. Not run again after the repairs: the recorded bundles and readings give the same six verdicts under the verifier of now (row D08 of the probe, offline) |
|
||||
| the conformance suite | `node ../conformance/proba-conformance.mjs` | @@SUITA@@ |
|
||||
| the conformance suite | `node ../conformance/proba-conformance.mjs` | 12/12 on 2026-10-14 with the AIP-23 verifier, Python 3 and testnet 28001 read (`--online`; nothing is sent): 181 rules of 11 verifiers, the published verifiers pass 467 of 467 vectors, 194 of them device bundles |
|
||||
|
||||
An attack row of the probe requires the verdict INVALID **and the exact set of rules that refused**: refused for another reason is
|
||||
a red row. A row that must be PARTIAL requires the exact set of causes. Three things check that the rows can go red.
|
||||
|
||||
@ -47,13 +47,19 @@ const toate = (f) => rest.flatMap((a, i) => (a === f && rest[i + 1] != null ? [r
|
||||
const cere = (f) => { const v = get(f); if (v == null) throw new Folosire(`${cmd}: ${f} is required`); return v; };
|
||||
const numar = (f, imp = null) => { const v = get(f); if (v == null) { if (imp === null) throw new Folosire(`${cmd}: ${f} is required`); return imp; } const n = Number(v); if (!Number.isInteger(n) || n < 0) throw new Folosire(`${cmd}: ${f} is a whole number`); return n; };
|
||||
const citeste = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { throw new Folosire(`cannot read ${f}: ${e.message}`); } };
|
||||
const scrie = (o, f, { secret = false } = {}) => { if (!f) throw new Folosire(`${cmd}: --out is required`); fs.writeFileSync(f, JSON.stringify(o, null, 1) + '\n', secret ? { mode: 0o600 } : {}); };
|
||||
// A key file is CREATED, never written over: the mode 0600 is given only to a file that is created (an existing file keeps its own), and
|
||||
// the key a file already holds would be lost, with nothing to rotate or recover it.
|
||||
const scrie = (o, f, { secret = false } = {}) => {
|
||||
if (!f) throw new Folosire(`${cmd}: --out is required`);
|
||||
try { fs.writeFileSync(f, JSON.stringify(o, null, 1) + '\n', secret ? { mode: 0o600, flag: 'wx' } : {}); }
|
||||
catch (e) { if (secret && e && e.code === 'EEXIST') throw new Folosire(`${cmd}: ${f} exists; a key file is never written over (the key it holds would be lost)`); throw e; }
|
||||
};
|
||||
const identitate = (f) => ID.importKeys(citeste(f));
|
||||
/** what a file given to verify holds: one object, a list of them, or a registry { records: [...] }; flattened, every element is handed over */
|
||||
const netezeste = (x, adancime = 0) => (adancime > 8 ? [x] : Array.isArray(x) ? x.flatMap((y) => netezeste(y, adancime + 1)) : x && typeof x === 'object' && Array.isArray(x.records) && x.statement === undefined ? netezeste(x.records, adancime + 1) : [x]);
|
||||
const elemente = (f) => netezeste(citeste(f));
|
||||
/** the public keys of an identity: a keys file, a { id, public } file, or the public keys object itself */
|
||||
const publice = (f) => { const j = citeste(f); const p = j && j.public ? j.public : j; ID.publicKeyObjects(p); return p; };
|
||||
const publice = (f) => { const j = citeste(f); const p = j && j.public ? j.public : j; try { ID.publicKeyObjects(p); } catch (e) { throw new Folosire(`${f} holds no public keys: ${e.message}`); } return p; };
|
||||
const inOre = (h) => new Date(Date.now() + h * 3600000).toISOString();
|
||||
const lant = () => ({ m: citeste(cere('--manufacture')), t: toate('--transfer').map(citeste) });
|
||||
|
||||
@ -102,7 +108,7 @@ async function main() {
|
||||
if (get('--software')) o.softwareAttestation = citeste(get('--software'));
|
||||
const pachet = citeste(cere('--bundle')); let r;
|
||||
try { r = D.verifyDevice(pachet, o); } catch (e) { if (/^verifyDevice: /.test(String(e && e.message))) throw new Folosire(e.message); throw e; } // the verifier's own configuration cannot be read: no verdict
|
||||
if (rest.includes('--json')) console.log(JSON.stringify(r, null, 1));
|
||||
if (rest.includes('--json')) console.log(D.visibleJson(r, 1));
|
||||
else { for (const x of r.rows) spune(`${x.pass === true ? 'ok ' : x.pass === false ? 'FAIL' : '-- '} ${x.name}${x.pass === true || !x.detail ? '' : ': ' + x.detail}`);
|
||||
console.log('');
|
||||
spune(r.verdict + (r.rules.length ? ` (refused by: ${r.rules.join(', ')})` : '') + (r.verdict === 'PARTIAL' ? ` (not judged: ${r.unjudged.filter((c) => D.PARTIALE.includes(c)).join(', ')})` : ''));
|
||||
|
||||
@ -66,7 +66,13 @@ const hashOf = (o) => hex0x(sha(Buffer.from(canonical(o), 'utf8')));
|
||||
const ob = (x) => !!x && typeof x === 'object' && !Array.isArray(x);
|
||||
const sir = (x) => typeof x === 'string' && x.length > 0 && x.length <= 256;
|
||||
const iso = (d) => new Date(d).toISOString();
|
||||
function data(s, ce) { if (typeof s !== 'string' || !DATA.test(s) || Number.isNaN(Date.parse(s))) throw new Error(`aere-device: ${ce} is not an RFC 3339 UTC time`); return Date.parse(s); }
|
||||
/**
|
||||
* The day a text of the form of DATA names exists. A date parser MOVES a day that does not exist (2026-02-30 becomes March 2, T24:00:00
|
||||
* the next day) and reads nothing from what it cannot (month 13, second 60): in every case the day of the instant read is not the day
|
||||
* written (an hour or a month cannot overflow without moving the day), and a text read as nothing has no day at all.
|
||||
*/
|
||||
const exista = (s) => new Date(Date.parse(s)).getUTCDate() === Number(String(s).slice(8, 10));
|
||||
function data(s, ce) { if (typeof s !== 'string' || !DATA.test(s) || !exista(s)) throw new Error(`aere-device: ${ce} is not an RFC 3339 UTC time`); return Date.parse(s); }
|
||||
// `a` is an identity that a form check, or the signature check of a record, already required to be an object; `b` may be absent (a
|
||||
// chain that keeps no owner)
|
||||
const aceeasi = (a, b) => ob(b) && a.id === b.id && canonical(a.keys) === canonical(b.keys);
|
||||
@ -88,17 +94,28 @@ export const linkHash = (l) => hashOf(l);
|
||||
* (\xNN, \uNNNN), and a backslash is doubled so that the form cannot be forged. What is printed stays on its line, and a terminal
|
||||
* executes nothing a bundle wrote. The rows of verifyDevice carry the text as it came: print them through this.
|
||||
*/
|
||||
// the code points replaced: C0, DEL and C1, the two marks of direction, the line and paragraph separators with the bidirectional
|
||||
// embeddings and overrides after them, the bidirectional isolates, the byte order mark
|
||||
const DE_ARATAT = [[0x00, 0x1f], [0x7f, 0x9f], [0x200e, 0x200f], [0x2028, 0x202e], [0x2066, 0x2069], [0xfeff, 0xfeff]];
|
||||
// the code points replaced: C0, DEL and C1, the arabic letter mark and the two marks of direction (with the embeddings, overrides and
|
||||
// isolates below, the characters Unicode gives the property Bidi_Control), the line and paragraph separators, the byte order mark
|
||||
const DE_ARATAT = [[0x00, 0x1f], [0x7f, 0x9f], [0x061c, 0x061c], [0x200e, 0x200f], [0x2028, 0x202e], [0x2066, 0x2069], [0xfeff, 0xfeff]];
|
||||
const deAratat = (n) => DE_ARATAT.some(([de, la]) => n >= de && n <= la);
|
||||
export function visible(s) {
|
||||
let out = '';
|
||||
for (const c of String(s)) { const n = c.codePointAt(0);
|
||||
if (n === 0x5c) out += '\\\\';
|
||||
else if (DE_ARATAT.some(([de, la]) => n >= de && n <= la)) { const h = n.toString(16); out += h.length <= 2 ? '\\x' + h.padStart(2, '0') : '\\u' + h.padStart(4, '0'); }
|
||||
else if (deAratat(n)) { const h = n.toString(16); out += h.length <= 2 ? '\\x' + h.padStart(2, '0') : '\\u' + h.padStart(4, '0'); }
|
||||
else out += c; }
|
||||
return out;
|
||||
}
|
||||
/**
|
||||
* A value as JSON text in which every code point visible() replaces is written as a \u escape. JSON.stringify escapes only C0: DEL, C1,
|
||||
* the marks of direction and the line separators would reach the terminal as they came. The text parses to exactly the same value.
|
||||
* (What it replaces from DEL up can stand only inside a string; the C0 left in the text are the line feeds of its layout.)
|
||||
*/
|
||||
export function visibleJson(v, spatiu = 1) {
|
||||
let out = '';
|
||||
for (const c of JSON.stringify(v, null, spatiu)) { const n = c.codePointAt(0); out += n >= 0x7f && deAratat(n) ? '\\u' + n.toString(16).padStart(4, '0') : c; }
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the device key K (ML-DSA-65 alone)
|
||||
function cheiaDispozitivului(b64) {
|
||||
@ -301,7 +318,10 @@ export function verifyDevice(b, { now = new Date(), nonce = null, audience = nul
|
||||
const ok = (rule, link, name, pass, detail = '') => { if (!REGULI.includes(rule)) throw new Nedeclarata('aere-device: undeclared rule ' + rule); rows.push({ link, name, pass: !!pass, detail: pass ? '' : detail, rule }); return !!pass; };
|
||||
const nejudecat = (cauza, link, name, detail) => { if (!NEJUDECATE.includes(cauza)) throw new Nedeclarata('aere-device: undeclared cause ' + cauza); rows.push({ link, name, pass: null, detail, unjudged: cauza }); };
|
||||
// the verifier's own configuration is validated first (a mistake of the verifier is not an invalid bundle)
|
||||
const acum = new Date(now).getTime(); if (Number.isNaN(acum)) throw new Error('verifyDevice: now is not a time');
|
||||
// the verifier's clock: a Date, a number of milliseconds since 1970, or an RFC 3339 UTC text, and nothing else. A date parser reads a
|
||||
// text without a zone as local time and '1' as the year 2001: a clock read so would judge on a time nobody chose
|
||||
const acum = now instanceof Date ? now.getTime() : typeof now === 'number' ? now : typeof now === 'string' && DATA.test(now) && exista(now) ? Date.parse(now) : NaN;
|
||||
if (!Number.isFinite(acum)) throw new Error('verifyDevice: now is not a time (a Date, milliseconds since 1970, or an RFC 3339 UTC text)');
|
||||
if (!Number.isInteger(clockSkewS) || clockSkewS < 0 || clockSkewS > 600) throw new Error('verifyDevice: clockSkewS is 0..600 seconds');
|
||||
if (!Number.isInteger(maxAgeS) || maxAgeS < 1) throw new Error('verifyDevice: maxAgeS is a positive number of seconds');
|
||||
if (trustedManufacturers != null && !Array.isArray(trustedManufacturers)) throw new Error('verifyDevice: trustedManufacturers is a list of ids or public keys');
|
||||
@ -374,6 +394,31 @@ export function verifyDevice(b, { now = new Date(), nonce = null, audience = nul
|
||||
}
|
||||
|
||||
// ---- 2. firmware: which bytes, which version, signed by whom, still allowed
|
||||
// The policies signed by the SAME signer for this model. Of those, the ones in force on the verifier's clock that can be READ
|
||||
// (the version this verifier reads, a floor that is a whole number, a list of digests) are judged; with several, the strictest
|
||||
// holds (a retraction is not lifted). One that is signed, not out of its validity, and cannot be read is NOT read as a clean
|
||||
// policy: what it retracts cannot be seen, and the verdict says so (PARTIAL), whatever the other policies say. What is judged is
|
||||
// the release shown or, when the bundle brings none, the firmware the device attests (`fw`: model, signer, digest, sequence).
|
||||
const judecaPolitici = (fw, t) => {
|
||||
const ale = FP.filter((p) => { try { const q = p.statement; return q.model === fw.model && aceeasi(q.issuer, fw.signer) && verifica('firmware-policy', q, p.signature, fw.signer.keys); } catch { return false; } });
|
||||
const curente = [], necitite = [];
|
||||
for (const p of ale) { const q = p.statement; let inVigoare = null;
|
||||
if (q.v !== VERSION) { necitite.push('it is of a version this verifier does not read'); continue; }
|
||||
try { inVigoare = data(q.validFrom, 'policy validFrom') - sk <= acum && acum <= data(q.validUntil, 'policy validUntil'); } catch { /* a validity that is not a time: below */ }
|
||||
if (inVigoare === false) continue; // expired, or not yet in force: it says nothing about now
|
||||
if (inVigoare === null) necitite.push('its validity is not a time');
|
||||
else if (!(Number.isInteger(q.minSequence) && q.minSequence >= 0)) necitite.push('its floor is not a whole number from 0 up');
|
||||
else if (!(Array.isArray(q.retracted) && q.retracted.every((x) => typeof x === 'string' && DIGEST_SCRIS.test(x)))) necitite.push('its retracted list has an entry that is not a digest (0x and 64 hex digits)');
|
||||
else curente.push(p); }
|
||||
if (necitite.length) nejudecat('firmware-policy-unread', 'firmware', 'firmware: a policy that cannot be read', `${necitite.length} of the firmware policies signed by the manufacturer for this model cannot be read (${[...new Set(necitite)].join('; ')}): what such a policy retracts and the floor it sets cannot be seen, so it is not read as a policy that allows ${t.ce}`);
|
||||
if (!curente.length) { if (!necitite.length) nejudecat('firmware-policy-unread', 'firmware', 'firmware: still allowed', ale.length ? `the firmware policies given are not valid at ${iso(acum)}: fetch a current one` : 'no firmware policy of the manufacturer for this model was handed to the verifier; a retracted or outdated firmware cannot be seen'); }
|
||||
else {
|
||||
const scris = (x) => String(x).toLowerCase(); // a digest written in upper case by the manufacturer's own tooling is the same digest
|
||||
const retras = curente.some((p) => p.statement.retracted.some((x) => scris(x) === fw.digest)), prag = Math.max(...curente.map((p) => p.statement.minSequence));
|
||||
ok('firmware-retracted', 'firmware', t.neretras, !retras /*PAZA:firmware-retracted*/, t.retras);
|
||||
ok('firmware-rollback', 'firmware', t.prag(prag), fw.sequence >= prag /*PAZA:firmware-rollback*/, t.subPrag(prag));
|
||||
}
|
||||
};
|
||||
const F = b.firmware; let R = null;
|
||||
if (F == null) nejudecat('firmware-missing', 'firmware', 'firmware: release', 'the bundle brings no firmware release: the digest the device attests names nothing a manufacturer signed');
|
||||
else {
|
||||
@ -385,28 +430,9 @@ export function verifyDevice(b, { now = new Date(), nonce = null, audience = nul
|
||||
ok('firmware-signer', 'firmware', 'firmware: released by the manufacturer of the device', idDin(R.signer.keys) === R.signer.id && (S === null || aceeasi(R.signer, S.issuer)) /*PAZA:firmware-signer*/, `released by ${R.signer.id}` + (S ? `, the device was made by ${S.issuer.id}` : ', an id not derived from its keys'));
|
||||
ok('firmware-signature', 'firmware', 'firmware: signed by who released it (Ed25519 and ML-DSA-65, both)', numai(F, LEGATURA) && verifica('firmware', R, F.signature, R.signer.keys) /*PAZA:firmware-signature*/, 'the hybrid signature does not verify with the signer\'s keys');
|
||||
if (S) ok('firmware-model', 'firmware', `firmware: built for model ${S.claims.model}`, R.model === S.claims.model /*PAZA:firmware-model*/, `the release is for model ${R.model}`);
|
||||
// The policies signed by the SAME signer for this model. Of those, the ones in force on the verifier's clock that can be READ
|
||||
// (the version this verifier reads, a floor that is a whole number, a list of digests) are judged; with several, the strictest
|
||||
// holds (a retraction is not lifted). One that is signed, not out of its validity, and cannot be read is NOT read as a clean
|
||||
// policy: what it retracts cannot be seen, and the verdict says so (PARTIAL), whatever the other policies say.
|
||||
const ale = FP.filter((p) => { try { const q = p.statement; return q.model === R.model && aceeasi(q.issuer, R.signer) && verifica('firmware-policy', q, p.signature, R.signer.keys); } catch { return false; } });
|
||||
const curente = [], necitite = [];
|
||||
for (const p of ale) { const q = p.statement; let inVigoare = null;
|
||||
if (q.v !== VERSION) { necitite.push('it is of a version this verifier does not read'); continue; }
|
||||
try { inVigoare = data(q.validFrom, 'policy validFrom') - sk <= acum && acum <= data(q.validUntil, 'policy validUntil'); } catch { /* a validity that is not a time: below */ }
|
||||
if (inVigoare === false) continue; // expired, or not yet in force: it says nothing about now
|
||||
if (inVigoare === null) necitite.push('its validity is not a time');
|
||||
else if (!(Number.isInteger(q.minSequence) && q.minSequence >= 0)) necitite.push('its floor is not a whole number from 0 up');
|
||||
else if (!(Array.isArray(q.retracted) && q.retracted.every((x) => typeof x === 'string' && DIGEST_SCRIS.test(x)))) necitite.push('its retracted list has an entry that is not a digest (0x and 64 hex digits)');
|
||||
else curente.push(p); }
|
||||
if (necitite.length) nejudecat('firmware-policy-unread', 'firmware', 'firmware: a policy that cannot be read', `${necitite.length} of the firmware policies signed by the manufacturer for this model cannot be read (${[...new Set(necitite)].join('; ')}): what such a policy retracts and the floor it sets cannot be seen, so it is not read as a policy that allows this release`);
|
||||
if (!curente.length) { if (!necitite.length) nejudecat('firmware-policy-unread', 'firmware', 'firmware: still allowed', ale.length ? `the firmware policies given are not valid at ${iso(acum)}: fetch a current one` : 'no firmware policy of the manufacturer for this model was handed to the verifier; a retracted or outdated firmware cannot be seen'); }
|
||||
else {
|
||||
const scris = (x) => String(x).toLowerCase(); // a digest written in upper case by the manufacturer's own tooling is the same digest
|
||||
const retras = curente.some((p) => p.statement.retracted.some((x) => scris(x) === R.digest)), prag = Math.max(...curente.map((p) => p.statement.minSequence));
|
||||
ok('firmware-retracted', 'firmware', 'firmware: not retracted', !retras /*PAZA:firmware-retracted*/, `the manufacturer retracted the firmware ${R.digest}`);
|
||||
ok('firmware-rollback', 'firmware', `firmware: sequence ${R.sequence} is at or above the floor ${prag}`, R.sequence >= prag /*PAZA:firmware-rollback*/, `version ${R.version} (sequence ${R.sequence}) is older than the lowest sequence the manufacturer still allows (${prag})`);
|
||||
}
|
||||
judecaPolitici({ model: R.model, signer: R.signer, digest: R.digest, sequence: R.sequence }, {
|
||||
ce: 'this release', neretras: 'firmware: not retracted', retras: `the manufacturer retracted the firmware ${R.digest}`,
|
||||
prag: (p) => `firmware: sequence ${R.sequence} is at or above the floor ${p}`, subPrag: (p) => `version ${R.version} (sequence ${R.sequence}) is older than the lowest sequence the manufacturer still allows (${p})` });
|
||||
if (R.softwareAttestationHash !== undefined) {
|
||||
if (softwareAttestation == null) nejudecat('software-unread', 'firmware', 'firmware: Proof of Software', 'the release names a Proof of Software attestation that was not handed to the verifier');
|
||||
else ok('firmware-software', 'firmware', 'firmware: the Proof of Software attestation named covers this image', softwareAttestation.statementHash === R.softwareAttestationHash && softwareCovers(softwareAttestation, R.digest) /*PAZA:firmware-software*/, 'the attestation given is not the one the release names, or does not list an artifact with the firmware digest');
|
||||
@ -465,6 +491,12 @@ export function verifyDevice(b, { now = new Date(), nonce = null, audience = nul
|
||||
if (q.hardwareRoot != null) nejudecat('hardware-root', 'attestation', 'attestation: hardware root', 'the attestation carries a hardware root; this verifier does not judge one, so nothing here says the key lives in hardware');
|
||||
}
|
||||
}
|
||||
// ---- 2 (again), when the bundle brings no release: the firmware the DEVICE says it runs, judged against the policies of the
|
||||
// manufacturer of the credential for its model. Leaving the release out keeps the verdict from VALID (firmware-missing); it does not
|
||||
// hide a digest the manufacturer retracted, nor a sequence below its floor, that the device itself attests.
|
||||
if (F == null && S !== null && Q !== null) judecaPolitici({ model: S.claims.model, signer: S.issuer, digest: Q.firmware.digest, sequence: Q.firmware.sequence }, {
|
||||
ce: 'the firmware the device attests', neretras: 'firmware: the firmware the device attests is not retracted (no release was brought)', retras: `the device says it runs ${Q.firmware.digest}, which the manufacturer retracted`,
|
||||
prag: (p) => `firmware: the sequence the device attests (${Q.firmware.sequence}) is at or above the floor ${p} (no release was brought)`, subPrag: (p) => `the device says it runs sequence ${Q.firmware.sequence}, older than the lowest sequence the manufacturer still allows (${p})` });
|
||||
|
||||
// the device the authorization and the registry are about: the one the manufacturer made; with no credential, the one that attests
|
||||
const dev = idDispozitiv !== null ? idDispozitiv : Q !== null ? Q.deviceId : null;
|
||||
|
||||
@ -13,7 +13,7 @@ export const M = 'device.mjs', C = 'device-cli.mjs';
|
||||
// the rows a planting must turn red can be derived from the witness run: by the rule they expect, or by the verdict they require
|
||||
const cuRegula = (r) => (rows) => rows.filter((x) => x.expects.includes(r)).map((x) => x.id);
|
||||
// every row that requires a PARTIAL verdict: the rows of that kind, and the rows of other kinds that require one among other things
|
||||
const partiale = (rows) => [...rows.filter((x) => x.kind === 'partial').map((x) => x.id), 'D05', 'D08', 'D09', 'S24'];
|
||||
const partiale = (rows) => [...rows.filter((x) => x.kind === 'partial').map((x) => x.id), 'D05', 'D08', 'D09', 'S24', 'W08'];
|
||||
// every attack row that reaches the end of the verifier (a bundle refused for its form, or one that cannot be read, has no subject to return)
|
||||
const cuSubiect = (rows) => rows.filter((x) => x.kind === 'attack' && !x.expects.includes('form') && !x.expects.includes('unreadable')).map((x) => x.id);
|
||||
// the conditions of the windows, as they stand in the verifier
|
||||
@ -21,9 +21,9 @@ const FER = 'nb - sk <= acum && acum <= na && nb - sk <= tq && tq <= na /*PAZA:a
|
||||
const PRO = 'tq <= acum + sk && acum - tq <= maxAgeS * 1000 /*PAZA:freshness*/';
|
||||
const MW = 'vf - sk <= acum && acum <= vu /*PAZA:manufacture-window*/';
|
||||
const ACE = 'a.id === b.id && canonical(a.keys) === canonical(b.keys)';
|
||||
const DATA_ = "if (typeof s !== 'string' || !DATA.test(s) || Number.isNaN(Date.parse(s)))";
|
||||
const DATA_ = "if (typeof s !== 'string' || !DATA.test(s) || !exista(s))";
|
||||
const LISTA_A_LUI = "aceeasi(l.statement.issuer, S.issuer) && verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys)";
|
||||
const POLITICA_A_LUI = "&& aceeasi(q.issuer, R.signer) && verifica('firmware-policy', q, p.signature, R.signer.keys)";
|
||||
const POLITICA_A_LUI = "&& aceeasi(q.issuer, fw.signer) && verifica('firmware-policy', q, p.signature, fw.signer.keys)";
|
||||
const CITIBIL = 'const citibil = s.v === VERSION && STATES.includes(s.state) && Number.isInteger(s.seq);';
|
||||
const VEDERE = 'if (!v || s.seq > v.seq) vederi.set(s.by.id,';
|
||||
const ROL = "S !== null && aceeasi(s.by, S.issuer) ? 'manufacturer' : aceeasi(s.by, owner) ? 'owner' : fosti.some((x) => aceeasi(s.by, x)) ? 'previous' : null;";
|
||||
@ -54,9 +54,12 @@ export const PLANTARI = [
|
||||
['a member more beside the statement and the signature of an attestation is accepted', M, 'numai(X, LEGATURA) && numai(X.signature, SEMN_DISPOZITIV)', 'numai(X.signature, SEMN_DISPOZITIV)', ['M07']],
|
||||
['a member more in the attestation\'s signature object is accepted', M, 'numai(X.signature, SEMN_DISPOZITIV) && X.signature.alg', 'X.signature.alg', ['M08']],
|
||||
// the form of a time: three conditions
|
||||
['a time in another form than RFC 3339 UTC is read (whatever a date parser understands)', M, DATA_, "if (typeof s !== 'string' || Number.isNaN(Date.parse(s)))", ['H21', 'H22']],
|
||||
['a time that has the form of a time and is not a date is read', M, DATA_, "if (typeof s !== 'string' || !DATA.test(s))", ['T23']],
|
||||
['a time that is not a text is read', M, DATA_, 'if (!DATA.test(s) || Number.isNaN(Date.parse(s)))', ['T22']],
|
||||
['a time in another form than RFC 3339 UTC is read (whatever a date parser understands)', M, DATA_, "if (typeof s !== 'string' || Number.isNaN(Date.parse(s)))", ['H21', 'H22', 'W09', 'W10']],
|
||||
['a time that has the form of a time and is not a date is read', M, DATA_, "if (typeof s !== 'string' || !DATA.test(s))", ['T23', 'W09', 'W10']],
|
||||
['a time that is not a text is read', M, DATA_, 'if (!DATA.test(s) || !exista(s))', ['T22']],
|
||||
// 2026-10-14 (third review): the check before was "the parser reads something" (Number.isNaN), and a date parser MOVES a day that does not exist
|
||||
['a day that does not exist is read where the parser moves it (only what it cannot read at all is refused)', M, DATA_, "if (typeof s !== 'string' || !DATA.test(s) || Number.isNaN(Date.parse(s)))", ['W09', 'W10']],
|
||||
['the verifier\'s clock is read by a date parser (a text without a zone is local time, "1" is the year 2001)', M, "typeof now === 'string' && DATA.test(now) && exista(now) ? Date.parse(now) : NaN", "typeof now === 'string' ? new Date(now).getTime() : NaN", ['W12', 'D11']],
|
||||
// ---- the manufacture credential
|
||||
['a manufacture credential that has not begun is accepted', M, MW, 'acum <= vu /*PAZA:manufacture-window*/', ['F01']],
|
||||
['an expired manufacture credential is accepted', M, MW, 'vf - sk <= acum /*PAZA:manufacture-window*/', ['C05', 'H13']],
|
||||
@ -76,11 +79,13 @@ export const PLANTARI = [
|
||||
['the id of who signed a release is not derived from its keys', M, 'idDin(R.signer.keys) === R.signer.id && (S === null', '(S === null', ['F26']],
|
||||
['with two firmware policies the loosest floor holds', M, 'prag = Math.max(...curente', 'prag = Math.min(...curente', ['C22']],
|
||||
['a retraction is lifted by a policy that does not list it', M, 'const retras = curente.some((p) => p.statement.retracted.some(', 'const retras = curente.every((p) => p.statement.retracted.some(', ['C24', 'D09']],
|
||||
['a retraction written in upper case is not seen', M, 'const scris = (x) => String(x).toLowerCase();', 'const scris = (x) => String(x);', ['C24b']],
|
||||
['a firmware policy of a stranger is believed', M, POLITICA_A_LUI, "&& verifica('firmware-policy', q, p.signature, q.issuer.keys)", ['B08']],
|
||||
['a firmware policy is believed on the issuer it names (its signature is not checked)', M, POLITICA_A_LUI, '&& aceeasi(q.issuer, R.signer)', ['S19']],
|
||||
['a firmware policy signed with the manufacturer\'s keys is believed whatever issuer it names', M, POLITICA_A_LUI, "&& verifica('firmware-policy', q, p.signature, R.signer.keys)", ['S20']],
|
||||
['a firmware policy of another model is believed', M, 'return q.model === R.model && aceeasi(q.issuer, R.signer)', 'return aceeasi(q.issuer, R.signer)', ['F12']],
|
||||
['a retraction written in upper case is not seen', M, 'const scris = (x) => String(x).toLowerCase();', 'const scris = (x) => String(x);', ['C24b', 'W04']],
|
||||
['a firmware policy of a stranger is believed', M, POLITICA_A_LUI, "&& verifica('firmware-policy', q, p.signature, q.issuer.keys)", ['B08', 'W06']],
|
||||
['a firmware policy is believed on the issuer it names (its signature is not checked)', M, POLITICA_A_LUI, '&& aceeasi(q.issuer, fw.signer)', ['S19']],
|
||||
['a firmware policy signed with the manufacturer\'s keys is believed whatever issuer it names', M, POLITICA_A_LUI, "&& verifica('firmware-policy', q, p.signature, fw.signer.keys)", ['S20']],
|
||||
['a firmware policy of another model is believed', M, 'return q.model === fw.model && aceeasi(q.issuer, fw.signer)', 'return aceeasi(q.issuer, fw.signer)', ['F12', 'W05']],
|
||||
// 2026-10-14 (third review): with the release left out, the firmware the device attests was not judged against the policies at all
|
||||
['with the release left out, the firmware the device attests is not judged against the policies', M, 'if (F == null && S !== null && Q !== null) judecaPolitici(', 'if (false) judecaPolitici(', ['W01', 'W02', 'W03', 'W04', 'W07']],
|
||||
['a firmware policy of a version this verifier does not read is dropped in silence', M, "if (q.v !== VERSION) { necitite.push('it is of a version this verifier does not read'); continue; }", 'if (q.v !== VERSION) continue;', ['F19c']],
|
||||
// the end of a policy's validity is no longer read: an expired policy counts, and a validity that is not a time is never looked at
|
||||
['an expired firmware policy is read as current', M, "inVigoare = data(q.validFrom, 'policy validFrom') - sk <= acum && acum <= data(q.validUntil, 'policy validUntil');", "inVigoare = data(q.validFrom, 'policy validFrom') - sk <= acum;", ['F13', 'F14', 'F19b', 'H18']],
|
||||
@ -119,7 +124,7 @@ export const PLANTARI = [
|
||||
['the ending of an authorization is given the tolerance at the time of the attestation', M, FER, 'nb - sk <= acum && acum <= na && nb - sk <= tq && tq <= na + sk /*PAZA:authorization-window*/', ['F05']],
|
||||
// ---- status: whose record counts
|
||||
['the manufacturer\'s retirement, and the owner\'s, are lifted like a suspension', M, "if (s.state === 'retired') { if (retras === null", "if (false) { if (retras === null", cuRegula('status-retired')],
|
||||
['the manufacturer\'s records stop counting once the device is sold', M, "S !== null && aceeasi(s.by, S.issuer) ? 'manufacturer'", "S !== null && aceeasi(s.by, S.issuer) && aceeasi(s.by, owner) ? 'manufacturer'", ['A08', 'A08b', 'C51', 'C52', 'C53', 'C53b', 'C55', 'D05', 'G09', 'G11', 'G12', 'H28', 'J01', 'J04', 'K07', 'S01', 'S03', 'S06', 'S09', 'T16']],
|
||||
['the manufacturer\'s records stop counting once the device is sold', M, "S !== null && aceeasi(s.by, S.issuer) ? 'manufacturer'", "S !== null && aceeasi(s.by, S.issuer) && aceeasi(s.by, owner) ? 'manufacturer'", ['A08', 'A08b', 'C51', 'C52', 'C53', 'C53b', 'C55', 'D05', 'G09', 'G11', 'G12', 'H28', 'J01', 'J04', 'K07', 'S01', 'S03', 'S06', 'S09', 'T16', 'W08']],
|
||||
['anyone can write a status record', M, "fosti.some((x) => aceeasi(s.by, x)) ? 'previous' : null;", "fosti.some((x) => aceeasi(s.by, x)) ? 'previous' : 'owner';", ['A06', 'J05', 'K01', 'K02', 'K03', 'K04', 'K05', 'K06', 'S25']],
|
||||
['a record of a PREVIOUS owner binds the owner of now', M, "aceeasi(s.by, x)) ? 'previous' : null;", "aceeasi(s.by, x)) ? 'owner' : null;", ['A07', 'D07', 'G06', 'G07', 'G08', 'G14', 'I10', 'J06']],
|
||||
['an owner that owned the device before is taken for a previous owner (the order of the roles turned round)', M, ROL, "S !== null && aceeasi(s.by, S.issuer) ? 'manufacturer' : fosti.some((x) => aceeasi(s.by, x)) ? 'previous' : aceeasi(s.by, owner) ? 'owner' : null;", ['A11', 'G03', 'G04', 'G05']],
|
||||
@ -207,8 +212,10 @@ export const PLANTARI = [
|
||||
['the private half of a device key is enumerable', M, "Object.defineProperty(d, 'privat', { value: priv, enumerable: false });", "Object.defineProperty(d, 'privat', { value: priv, enumerable: true });", ['D01']],
|
||||
// ---- text made visible
|
||||
['visible(): a backslash is not doubled (a visible form can be forged)', M, 'if (n === 0x5c) out +=', 'if (false) out +=', ['V01']],
|
||||
['visible(): the C1 control characters are sent as they are', M, '[0x7f, 0x9f]', '[0x7f, 0x7f]', ['D10', 'V01']],
|
||||
['visible(): the line and paragraph separators are sent as they are', M, '[0x2028, 0x202e]', '[0x202a, 0x202e]', ['D10', 'V01']],
|
||||
['visible(): the C1 control characters are sent as they are', M, '[0x7f, 0x9f]', '[0x7f, 0x7f]', ['D10', 'V01', 'W13', 'D10b']],
|
||||
['visible(): the line and paragraph separators are sent as they are', M, '[0x2028, 0x202e]', '[0x202a, 0x202e]', ['D10', 'V01', 'W13', 'D10b']],
|
||||
// 2026-10-14 (third review): U+061C ARABIC LETTER MARK has the Unicode property Bidi_Control and was printed as it came
|
||||
['visible(): the arabic letter mark is sent as it is', M, '[0x061c, 0x061c], ', '', ['D10', 'V01', 'W13', 'D10b']],
|
||||
// ---- the command line
|
||||
['the command line exits 0 whatever the verdict', C, "return r.verdict === 'VALID' ? 0 : r.verdict === 'INVALID' ? 1 : 2;", 'return 0;', ['D05', 'D07', 'D09', 'D10']],
|
||||
['the command line does not say what is named beside the verdict', C, 'if (notate.length) spune(', 'if (false) spune(', ['D07']],
|
||||
@ -216,7 +223,11 @@ export const PLANTARI = [
|
||||
['the command line hands a file that holds a list as ONE thing (it is not read element by element)', C, LISTE_CLI, "o.statusLists = toate('--status-list').map(citeste); o.firmwarePolicies = toate('--policy').map(citeste); o.statusRecords = toate('--status').map(citeste); o.anchors = toate('--reading').map(citeste);", ['D09']],
|
||||
['the command line does not open a registry object { records: [...] }', C, " : x && typeof x === 'object' && Array.isArray(x.records) && x.statement === undefined ? netezeste(x.records, adancime + 1) : [x]);", ' : [x]);', ['D09']],
|
||||
['the command line prints text from a bundle as it came (control characters reach the terminal)', C, 'const spune = (text) => { console.log(D.visible(text)); };', 'const spune = (text) => { console.log(text); };', ['D10']],
|
||||
['the command line passes its JSON through visible() (it is no longer the bundle\'s text)', C, "if (rest.includes('--json')) console.log(JSON.stringify(r, null, 1));", "if (rest.includes('--json')) spune(JSON.stringify(r, null, 1));", ['D05', 'D07', 'D09', 'D10b']],
|
||||
['an input that cannot be read exits with the code of a PARTIAL verdict', C, 'const COD_FOLOSIRE = 3, COD_CADERE = 4;', 'const COD_FOLOSIRE = 2, COD_CADERE = 4;', ['D11']],
|
||||
['the command line passes its JSON through visible() (it is no longer the bundle\'s text)', C, "if (rest.includes('--json')) console.log(D.visibleJson(r, 1));", "if (rest.includes('--json')) spune(D.visibleJson(r, 1));", ['D05', 'D07', 'D09', 'D10b']],
|
||||
// 2026-10-14 (third review): JSON.stringify escapes C0 only; DEL, C1, the marks of direction and the line separators reached the terminal
|
||||
['the command line prints its JSON as JSON.stringify writes it', C, "if (rest.includes('--json')) console.log(D.visibleJson(r, 1));", "if (rest.includes('--json')) console.log(JSON.stringify(r, null, 1));", ['D10b']],
|
||||
['a key file is written over a file that exists (the key it held is lost, and the file keeps its own mode)', C, "secret ? { mode: 0o600, flag: 'wx' } : {}", 'secret ? { mode: 0o600 } : {}', ['D12']],
|
||||
['a trusted manufacturer file that holds no keys exits as an unexpected failure', C, 'catch (e) { throw new Folosire(`${f} holds no public keys', 'catch (e) { throw new Error(`${f} holds no public keys', ['D11']],
|
||||
['an input that cannot be read exits with the code of a PARTIAL verdict', C, 'const COD_FOLOSIRE = 3, COD_CADERE = 4;', 'const COD_FOLOSIRE = 2, COD_CADERE = 4;', ['D11', 'D12']],
|
||||
['a clock that is not a time exits as an unexpected failure, not as an input that cannot be read', C, 'if (/^verifyDevice: /.test(String(e && e.message))) throw new Folosire(e.message);', 'if (false) throw new Folosire(e.message);', ['D11']],
|
||||
];
|
||||
|
||||
@ -407,10 +407,10 @@ spune('M. a member more in a link or in its signature is another text of it, and
|
||||
|
||||
// ================================================================ V. text from a bundle, made visible
|
||||
{ const C = (...n) => String.fromCodePoint(...n);
|
||||
const ascunse = [0x00, 0x09, 0x0a, 0x0d, 0x1b, 0x1f, 0x7f, 0x80, 0x9b, 0x9f, 0x200e, 0x200f, 0x2028, 0x2029, 0x202a, 0x202e, 0x2066, 0x2069, 0xfeff], lasate = [0x20, 0x7e, 0xa0, 0xe9, 0x200d, 0x2027, 0x202f, 0x2065, 0x206a, 0xfefe, 0x1f600];
|
||||
const ascunse = [0x00, 0x09, 0x0a, 0x0d, 0x1b, 0x1f, 0x7f, 0x80, 0x9b, 0x9f, 0x061c, 0x200e, 0x200f, 0x2028, 0x2029, 0x202a, 0x202e, 0x2066, 0x2069, 0xfeff], lasate = [0x20, 0x7e, 0xa0, 0xe9, 0x061b, 0x061d, 0x200d, 0x2027, 0x202f, 0x2065, 0x206a, 0xfefe, 0x1f600];
|
||||
const totAscuns = ascunse.every((n) => { const v = D.visible('a' + C(n) + 'b'); return v === 'a' + (n < 256 ? '\\x' + n.toString(16).padStart(2, '0') : '\\u' + n.toString(16).padStart(4, '0')) + 'b'; });
|
||||
const totLasat = lasate.every((n) => D.visible('a' + C(n) + 'b') === 'a' + C(n) + 'b');
|
||||
cer('V01', `visible(): each of ${ascunse.length} control characters (the edges of C0, DEL, C1, the marks of direction, the line and paragraph separators, the bidirectional controls, the byte order mark) becomes its visible form, each of ${lasate.length} neighbours of those ranges is left as it is, and a backslash is doubled so that the form cannot be forged`,
|
||||
cer('V01', `visible(): each of ${ascunse.length} control characters (the edges of C0, DEL, C1, the arabic letter mark and the marks of direction, the line and paragraph separators, the bidirectional controls, the byte order mark) becomes its visible form, each of ${lasate.length} neighbours of those ranges is left as it is, and a backslash is doubled so that the form cannot be forged`,
|
||||
totAscuns && totLasat && D.visible('a\\x1b') === 'a\\\\x1b' && D.visible('plain text, 100%') === 'plain text, 100%' && D.visible(7) === '7'); }
|
||||
|
||||
// ================================================================ R. the form of each link, one field at a time (after a search for conditions without a row, 2026-10-06)
|
||||
@ -573,6 +573,44 @@ if (!REPEDE) { let aruncate = 0, cazuri = 0; const b0 = B({ anchor: PLIC }); con
|
||||
{ let a = 0; for (const o of [{ trustedManufacturers: 'x' }, { statusRecords: null }, { clockSkewS: 9999 }, { now: 'not a time' }]) { try { D.verifyDevice(B(), O(o)); } catch { a++; } }
|
||||
cer('C76', 'a configuration of the VERIFIER that cannot be read throws (it is the verifier\'s mistake, not an invalid bundle)', a === 4); }
|
||||
|
||||
// ================================================================ W. what a third adversarial review found (2026-10-14)
|
||||
spune('W. the release left out, a link left out, a day that does not exist, the verifier\'s clock, the JSON that is printed');
|
||||
{ const fara = (o = {}) => B({ firmware: null, ...o }); // the bundle with its release left out by whoever hands it
|
||||
const retrage = (d) => politica({ retracted: [d] });
|
||||
atac('W01', 'THE RELEASE LEFT OUT: the device attests the digest the manufacturer retracted, and the bundle is handed without the release. Refused all the same (the device itself says what it runs), and the release is still named as not brought',
|
||||
fara(), O({ firmwarePolicies: [retrage(fw2.statement.digest)] }), ['firmware-retracted'], (r) => r.unjudged.includes('firmware-missing') && r.rows.some((x) => x.rule === 'firmware-retracted' && x.pass === false && x.detail.includes('the device says it runs')));
|
||||
atac('W02', 'the release left out: the device attests a sequence below the floor of the current policy', fara({ attestation: atest({ firmware: fw1 }) }), O({ firmwarePolicies: [politica({ minSequence: 2 })] }), ['firmware-rollback'], (r) => r.unjudged.includes('firmware-missing'));
|
||||
partial('W03', 'the release left out: the device attests a sequence AT the floor (the edge is allowed, and it is judged): PARTIAL for the release alone', fara(), O({ firmwarePolicies: [politica({ minSequence: 2 })] }), ['firmware-missing'],
|
||||
(r) => r.rows.some((x) => x.rule === 'firmware-rollback' && x.pass === true) && r.rows.some((x) => x.rule === 'firmware-retracted' && x.pass === true));
|
||||
atac('W04', 'the release left out: the retraction is written in upper case by the manufacturer\'s tooling', fara(), O({ firmwarePolicies: [reSemn('firmware-policy', retrage(fw2.statement.digest), mfr, (st) => { st.retracted = ['0x' + st.retracted[0].slice(2).toUpperCase()]; })] }), ['firmware-retracted']);
|
||||
partial('W05', 'the release left out: a policy of ANOTHER model of the same manufacturer retracts the attested digest: it is not this model\'s policy', fara(), O({ firmwarePolicies: [politica(), politica({ model: 'AX-300', minSequence: 9, retracted: [fw2.statement.digest] })] }), ['firmware-missing']);
|
||||
partial('W06', 'the release left out: a STRANGER\'s policy for this model retracts the attested digest: it is not the manufacturer\'s policy', fara(),
|
||||
O({ firmwarePolicies: [politica(), D.firmwarePolicy({ manufacturer: strain, model: 'AX-200', minSequence: 9, retracted: [fw2.statement.digest], validFrom: at(-3600), validUntil: at(ZI), now: cand(-3600) })] }), ['firmware-missing']);
|
||||
partial('W07', 'the release left out and no firmware policy handed: what the device attests cannot be judged either, and that is said', fara(), O({ firmwarePolicies: [] }), ['firmware-missing', 'firmware-policy-unread']);
|
||||
// found by the search for conditions without a row (mutanti-device.mjs) the same day: nothing told "only when the release is left out" from "always"
|
||||
atac('W14', 'WITH the release in the bundle, it is the release the policy judges: the release shown is allowed and the device attests ANOTHER firmware, one the policy retracts; refused for not running the release shown (attestation-firmware), and for that alone',
|
||||
B({ attestation: atest({ firmware: fw1 }) }), O({ firmwarePolicies: [retrage(fw1.statement.digest)] }), ['attestation-firmware']);
|
||||
// whoever hands the bundle chooses which links it brings: a link left out never turns an INVALID into a VALID
|
||||
const casuri = [['manufacture', B(), O({ statusRecords: [activ, stare(mfr, 'retired', 1, null)] }), 'manufacture-missing'], ['firmware', B(), O({ firmwarePolicies: [retrage(fw2.statement.digest)] }), 'firmware-missing'],
|
||||
['authorization', B({ authorization: aut(strain, cap1) }), O(), 'authorization-missing'], ['attestation', B({ attestation: atest({ nonce: 'old' }) }), O(), 'attestation-missing']];
|
||||
const rez = casuri.map(([l, b, o, c]) => { const cu = judeca(b, o), farl = judeca({ ...b, [l]: undefined }, o); return { l, cu: cu.verdict, fara: farl.verdict, numit: farl.unjudged.includes(c), valid: farl.valid }; });
|
||||
// the four bundles are INVALID only while the four rules hold: the row expects them (a rule taken out leaves a case with nothing to show)
|
||||
const parti = rez.filter((x) => x.fara === 'PARTIAL');
|
||||
cer('W08', 'A LINK LEFT OUT NEVER MAKES AN INVALID BUNDLE VALID: the manufacturer\'s retirement, a retracted release, a stranger\'s authorization and an old nonce are each INVALID; with the refused link left out each is at best PARTIAL, with the link named as not brought (and `valid`, "no rule refused", is then true: it is not a verdict)',
|
||||
rez.every((x) => x.cu === 'INVALID' && x.fara !== 'VALID' && x.numit) && parti.length > 0 && parti.every((x) => x.valid === true), JSON.stringify(rez), ['status-retired', 'firmware-retracted', 'authorization-owner', 'nonce']);
|
||||
atac('W09', 'an authorization signed by the owner valid until 2099-02-30, a day that does not exist (a date parser moves it to March 2): not a time', B({ authorization: reSemn('authorization', A1, op, (st) => { st.notAfter = '2099-02-30T00:00:00Z'; }) }), O(), ['unreadable']);
|
||||
atac('W10', 'an authorization whose end is written T24:00:00 (a date parser reads it as the next day): not an RFC 3339 time', B({ authorization: reSemn('authorization', A1, op, (st) => { st.notAfter = at(ZI).slice(0, 10) + 'T24:00:00Z'; }) }), O(), ['unreadable']);
|
||||
valid('W11', 'the last day of February of a leap year, the last day of a month of 31 days and the last instant of a day are times', B({ authorization: reSemn('authorization', A1, op, (st) => { st.notAfter = '2028-02-29T23:59:59.999Z'; }) }),
|
||||
O({ statusRecords: [stare(op, 'active', 1, cap1, { validUntil: '2036-12-31T23:59:59Z' })] }));
|
||||
const bune = [at(65), cand(65), cand(65).getTime()].map((n) => judeca(B(), O({ now: n })).verdict);
|
||||
const rele = [at(65).slice(0, 10) + 'T12:00:00.000', '1', at(65).slice(0, 10), '2026-02-30T00:00:00Z', null, true, new Date(NaN), Infinity, [at(65)]].filter((n) => { try { judeca(B(), O({ now: n })); return true; } catch (e) { return !/^verifyDevice: now is not a time/.test(e.message); } });
|
||||
cer('W12', 'the verifier\'s clock is a Date, a number of milliseconds or an RFC 3339 UTC text and nothing else: the three forms of one instant give one verdict (VALID); a text without a zone (local time to a date parser), "1" (the year 2001 to it), a date alone, a day that does not exist, null, true, an invalid Date, Infinity and a list each throw `verifyDevice:`, never a verdict on a clock nobody chose',
|
||||
la(bune) === 'VALID,VALID,VALID' && !rele.length, `${bune}; taken as a clock: ${rele.map((x) => JSON.stringify(x))}`);
|
||||
const v = { a: 'x\u009b2J\u007f\u2028\u2029\u202e\u061c\u200e\u2066\ufeffy', b: 'back\\slash ESC\u001b é 😀', c: [1, null, true] }; let t = '', p = null; try { t = D.visibleJson(v, 1); p = JSON.parse(t); } catch { /* not JSON, or no such function: a red row, not a broken probe */ }
|
||||
const crud = [0x9b, 0x7f, 0x2028, 0x2029, 0x202e, 0x061c, 0x200e, 0x2066, 0xfeff, 0x1b].filter((n) => t.includes(String.fromCodePoint(n)));
|
||||
cer('W13', 'visibleJson(), what verify --json prints: DEL, C1, the bidirectional controls, the line and paragraph separators and the byte order mark are written as \\u escapes (JSON.stringify escapes only C0); the text parses to exactly the same value, keeps its line layout, holds none of them raw, and leaves letters and emoji as they are',
|
||||
p !== null && JSON.stringify(p) === JSON.stringify(v) && !crud.length && t.includes('\n') && t.includes('é 😀'), `parses: ${p !== null}; raw: ${crud.map((n) => n.toString(16))}`); }
|
||||
|
||||
// ================================================================ D. keys, producers, the envelope under the reference verifier, the command line
|
||||
spune('D. keys, producers, the envelope, the command line');
|
||||
{ const f = D.exportDeviceKey(dev), k2 = D.importDeviceKey(clona(f)); let refuz = 0; try { D.importDeviceKey({ ...clona(f), publicKey: clonaDev.publicKey, deviceId: clonaDev.deviceId }); } catch { refuz++; }
|
||||
@ -655,22 +693,37 @@ if (!REPEDE) { // the command line, through files and processes only
|
||||
scrJ('rau.json', D.bundle({ manufacture: credZ, firmware: fwZ, authorization: D.authorize({ owner: atacator, deviceId: dz.deviceId, ownership: capZ, actions: ['door.unlock'], notBefore: iso2(-3600000), notAfter: iso2(3600000), now: new Date(acum - 3600000) }),
|
||||
attestation: D.attest({ device: dz, nonce: 'n-1', audience: 'https://gateway.example', firmware: fwZ, ownership: capZ, action: 'door.unlock', now: new Date(acum - 1000) }) }));
|
||||
// and a status record nobody signed whose signer id carries new lines, a false verdict line, a C1 control and a line separator
|
||||
scrJ('rau-stare.json', { statement: { v: 1, kind: 'aere-device-status', deviceId: activF.statement.deviceId, state: 'retired', seq: 1, ownership: null, by: { id: 'x)' + NL + NL + 'INVALID (refused by: status-retired)' + NL + E + '[8m' + String.fromCharCode(0x9b) + '2J' + String.fromCharCode(0x2028) + 'VALID' }, at: iso2(-600000), validUntil: iso2(86400000) }, signature: {} });
|
||||
const SEMNATAR = 'x)' + NL + NL + 'INVALID (refused by: status-retired)' + NL + E + '[8m' + String.fromCharCode(0x9b) + '2J' + String.fromCharCode(0x2028) + 'VALID' + String.fromCharCode(0x061c, 0x202e, 0x7f);
|
||||
scrJ('rau-stare.json', { statement: { v: 1, kind: 'aere-device-status', deviceId: activF.statement.deviceId, state: 'retired', seq: 1, ownership: null, by: { id: SEMNATAR }, at: iso2(-600000), validUntil: iso2(86400000) }, signature: {} });
|
||||
const brut = (...a) => spawnSync(process.execPath, [path.join(AICI, 'device-cli.mjs'), ...a], { cwd: T }); // no encoding: stdout is a Buffer
|
||||
const r1 = brut('verify', '--bundle', 'rau.json', '--nonce', 'n-1', '--audience', 'https://gateway.example', '--trust', mid), r2 = brut(...comun.filter((x) => x !== '--json'), '--nonce', 'n-123', '--status', 'rau-stare.json');
|
||||
const curat = (buf) => { for (let i = 0; i < buf.length; i++) { const o = buf[i]; if ((o < 0x20 && o !== 0x0a) || o === 0x7f) return false; if (o === 0xc2 && buf[i + 1] >= 0x80 && buf[i + 1] <= 0x9f) return false; if (o === 0xe2 && buf[i + 1] === 0x80 && (buf[i + 2] === 0xa8 || buf[i + 2] === 0xa9)) return false; } return true; };
|
||||
// no control byte but LF, no DEL, no C1 (C2 80..9F), no U+061C (D8 9C), no mark of direction (E2 80 8E/8F), no line or paragraph separator nor
|
||||
// embedding or override (E2 80 A8..AE), no isolate (E2 81 A6..A9), no byte order mark (EF BB BF)
|
||||
const curat = (buf) => { for (let i = 0; i < buf.length; i++) { const o = buf[i], u = buf[i + 1], w = buf[i + 2]; if ((o < 0x20 && o !== 0x0a) || o === 0x7f) return false; if (o === 0xc2 && u >= 0x80 && u <= 0x9f) return false; if (o === 0xd8 && u === 0x9c) return false;
|
||||
if (o === 0xe2 && u === 0x80 && (w === 0x8e || w === 0x8f || (w >= 0xa8 && w <= 0xae))) return false; if (o === 0xe2 && u === 0x81 && w >= 0xa6 && w <= 0xa9) return false; if (o === 0xef && u === 0xbb && w === 0xbf) return false; } return true; };
|
||||
const verdicte = (buf) => buf.toString('utf8').split(NL).filter((l) => /^(VALID|INVALID|PARTIAL)( |$)/.test(l)), t1b = r1.stdout.toString('utf8'), t2b = r2.stdout.toString('utf8');
|
||||
cer('D10', 'device-cli.mjs: text a bundle or a registry file wrote is SHOWN, never sent to the terminal. Read as bytes, the output of verify holds no control byte but the line feeds the command itself writes (no ESC, no C1, no line separator), exactly one verdict line, and the hostile text in its visible form: for a manufacture model that clears the screen and writes "VALID" (the verdict stays INVALID, exit 1) and for an unsigned status record whose signer id writes a line "INVALID (...)" (the verdict stays VALID, exit 0)',
|
||||
r1.status === 1 && r2.status === 0 && curat(r1.stdout) && curat(r2.stdout) && la(verdicte(r1.stdout)) === 'INVALID (refused by: manufacturer-trusted)' && la(verdicte(r2.stdout)) === 'VALID' && t1b.includes('AX-200\\x1b[2J\\x1b[Hok') && t1b.includes('\\x0a\\x0aVALID\\x0a\\x1b[8m') && t2b.includes('(x)\\x0a\\x0aINVALID (refused by: status-retired)\\x0a\\x1b[8m\\x9b2J\\u2028VALID)'),
|
||||
r1.status === 1 && r2.status === 0 && curat(r1.stdout) && curat(r2.stdout) && la(verdicte(r1.stdout)) === 'INVALID (refused by: manufacturer-trusted)' && la(verdicte(r2.stdout)) === 'VALID' && t1b.includes('AX-200\\x1b[2J\\x1b[Hok') && t1b.includes('\\x0a\\x0aVALID\\x0a\\x1b[8m') && t2b.includes('(x)\\x0a\\x0aINVALID (refused by: status-retired)\\x0a\\x1b[8m\\x9b2J\\u2028VALID\\u061c\\u202e\\x7f)'),
|
||||
`exits ${r1.status} ${r2.status}; clean ${curat(r1.stdout)} ${curat(r2.stdout)}; verdict lines [${verdicte(r1.stdout)}] [${verdicte(r2.stdout)}]`, ['manufacturer-trusted']);
|
||||
const rj = spawnSync(process.execPath, [path.join(AICI, 'device-cli.mjs'), 'verify', '--bundle', 'rau.json', '--nonce', 'n-1', '--audience', 'https://gateway.example', '--trust', mid, '--json'], { cwd: T }); let pj = null; try { pj = JSON.parse(rj.stdout.toString('utf8')); } catch { /* not JSON */ }
|
||||
cer('D10b', 'and --json stays JSON: it parses, its strings carry the model exactly as the bundle wrote it (escaped by JSON), and no ESC byte is in the output', pj !== null && pj.verdict === 'INVALID' && JSON.stringify(pj).includes('[2J') && pj.rows.some((x) => x.name.includes(MODEL)) && !rj.stdout.includes(27), '', ['manufacturer-trusted']); }
|
||||
const rj2 = brut(...comun, '--nonce', 'n-123', '--status', 'rau-stare.json'); let pj2 = null; try { pj2 = JSON.parse(rj2.stdout.toString('utf8')); } catch { /* not JSON */ }
|
||||
cer('D10b', 'and --json stays JSON: it parses, its strings carry the model and the signer id exactly as they were written, and its BYTES hold no control byte but its line feeds: no ESC, and (since 2026-10-14) no DEL, C1, mark of direction, override or line separator either, which JSON.stringify writes as they came',
|
||||
pj !== null && pj.verdict === 'INVALID' && JSON.stringify(pj).includes('[2J') && pj.rows.some((x) => x.name.includes(MODEL)) && !rj.stdout.includes(27) && curat(rj.stdout)
|
||||
&& pj2 !== null && pj2.verdict === 'VALID' && pj2.rows.some((x) => x.unjudged === 'status-ignored' && x.detail.includes(SEMNATAR)) && curat(rj2.stdout), `clean ${curat(rj.stdout)} ${curat(rj2.stdout)}`, ['manufacturer-trusted']); }
|
||||
// ---- an input that cannot be read has an exit code of its own: 0, 1 and 2 are verdicts and nothing else
|
||||
{ scrJ('stricat.json', '{ not json'); const faraVerdict = (r) => !/^(VALID|INVALID|PARTIAL)/m.test(r.stdout) && !/"verdict"/.test(r.stdout);
|
||||
const lipsa = cli(...comun, '--nonce', 'n-123', '--status', 'lipsa.json'), stricat = cli(...comun, '--nonce', 'n-123', '--status', 'stricat.json'), ceas = cli('verify', '--bundle', 'bundle.json', '--now', 'not-a-time', '--status', 'status.json'), fara = cli('verify'), alta = cli('no-such-command');
|
||||
// since 2026-10-14: a clock written without a zone (local time to a date parser) and a trusted manufacturer given as a JSON file that holds no keys
|
||||
const ceasLocal = cli(...comun, '--nonce', 'n-123', '--now', new Date().toISOString().slice(0, 10) + 'T12:00:00'), incredereRea = cli(...comun, '--nonce', 'n-123', '--trust', 'status.json');
|
||||
const prod = cli('status', '--by', 'owner.keys.json', '--manufacture', 'manufacture.json', '--transfer', 't1.json', '--state', 'destroyed', '--seq', '1', '--hours', '24', '--out', 'x.json');
|
||||
cer('D11', 'device-cli.mjs: 0, 1 and 2 are verdicts only. A status file that is missing, one that is not JSON, a clock that is not a time, a missing --bundle and an unknown command each exit 3 with no verdict printed; a command other than verify that fails (a state that does not exist) exits 1 and writes no file',
|
||||
[lipsa, stricat, ceas, fara, alta].every((r) => r.status === 3 && faraVerdict(r) && /device-cli: /.test(r.stderr)) && prod.status === 1 && !fs.existsSync(path.join(T, 'x.json')), `exits ${[lipsa, stricat, ceas, fara, alta, prod].map((r) => r.status)}`); }
|
||||
cer('D11', 'device-cli.mjs: 0, 1 and 2 are verdicts only. A status file that is missing, one that is not JSON, a clock that is not a time, a clock written without a zone, a trusted manufacturer file that holds no keys, a missing --bundle and an unknown command each exit 3 with no verdict printed; a command other than verify that fails (a state that does not exist) exits 1 and writes no file',
|
||||
[lipsa, stricat, ceas, ceasLocal, incredereRea, fara, alta].every((r) => r.status === 3 && faraVerdict(r) && /device-cli: /.test(r.stderr)) && prod.status === 1 && !fs.existsSync(path.join(T, 'x.json')), `exits ${[lipsa, stricat, ceas, ceasLocal, incredereRea, fara, alta, prod].map((r) => r.status)}`); }
|
||||
// ---- a key file is created, never written over (since 2026-10-14): the key it holds would be lost, and a file that exists keeps its own mode
|
||||
{ const inainte = ['mfr.keys.json', 'dev.key.json'].map((f) => fs.readFileSync(path.join(T, f)));
|
||||
const peste = [cli('new-identity', '--out', 'mfr.keys.json'), cli('new-device', '--out', 'dev.key.json')], nou = cli('new-identity', '--out', 'nou.keys.json');
|
||||
const dupa = ['mfr.keys.json', 'dev.key.json'].map((f) => fs.readFileSync(path.join(T, f)));
|
||||
cer('D12', 'device-cli.mjs: new-identity and new-device refuse to write a key over a file that exists (exit 3, the bytes of the file unchanged, so the key it held is not lost); into a new file they write one (exit 0)',
|
||||
peste.every((r) => r.status === 3 && /never written over/.test(r.stderr)) && inainte.every((b, i) => b.equals(dupa[i])) && nou.status === 0 && /"private"/.test(fs.readFileSync(path.join(T, 'nou.keys.json'), 'utf8')), `exits ${peste.map((r) => r.status)} ${nou.status}; unchanged ${inainte.map((b, i) => b.equals(dupa[i]))}`); }
|
||||
} finally { fs.rmSync(T, { recursive: true, force: true }); } }
|
||||
|
||||
{ // the evidence recorded on testnet 28001 (proba-device-ancora-testnet.mjs: two envelopes notarized there, each read back by the published
|
||||
|
||||
Loading…
Reference in New Issue
Block a user