Skip to content

The canonical envelope names its algorithm: schema_version 3, SHA-384/512 by option, SHA-256 the default - #42

Merged
HackTuah merged 10 commits into
mainfrom
slice/021-hash-agile-envelope
Sep 17, 2026
Merged

HackTuah merged 10 commits into
mainfrom
slice/021-hash-agile-envelope

Conversation

@HackTuah

@HackTuah HackTuah commented Sep 16, 2026

Copy link
Copy Markdown
Member

The canonical envelope names the digest it is hashed with, in its bytes — so a verifier reads the algorithm from what it holds, not from a page — and SHA-384 and SHA-512 become a host's option beside the SHA-256 default.

What the page claimed before. docs/connectome-canonical.md's rule 9 said "the hash is SHA-256 over exactly these bytes" — the algorithm was prose beside the bytes, the bytes named a schema_version and nothing about a digest, and the code spelled :sha256 at two sites. Moving to SHA-384 would have been a format break announced in a release note.

Now. A fourth fixed top-level member, "algorithm", right after "schema_version": "sha256", "sha384" or "sha512", and the hash is that digest over exactly those bytes — the member is under the hash like every other, so two envelopes of one graph naming different digests are different bytes with different hashes and neither is a rewrite of the other. The diff record gains the same member (its keys sort, so it comes first). schema_version is 3 on the graph and on the diff record; Graph.new/1 refuses 2 as it refused 1 — bytes are produced and hashed here, never re-imported. algorithm: on Canonical.encode/2, hash/2, hash_hex/2, hash_value/2, on Diff.encode/2, hash/2, hash_hex/2, and in the wire surface's host options; SHA-256 when absent, indefinitely; a fourth name refused by ArgumentError before a byte is written. The digest is computed at one site under lib/, the algorithm a variable bound from the option — the key-holding census moves from "two sites, both :sha256" to "one site, no literal". The wire's tools/call result keys the hex by the algorithm's name: sha256 as before under the default.

A verifier holding 0.4.0 or 0.5.0 bytes (schema_version 1 or 2) hashes them unchanged with SHA-256 and compares — those bytes name no algorithm and at those versions the digest is SHA-256 by the page's rule; published hashes stay verifiable forever. The page's Versions section says it in full — read the version; below 3, SHA-256; at 3, the named digest; anything else, refuse — and 0.5.0's goldens are kept in the tree and verified that way by a test. The worked example is given under all three digests, with the SHA-384 bytes shown, so a blind reader reproduces a SHA-384 hash from the page alone.

Two pages: docs/crypto-posture.md (one primitive at one site; three digests named in the bytes; no key, no signature, the seam for a signing package; why the algorithm is under the hash) and docs/fips.md (what a FIPS-mode host needs, in OTP 28's own words — a crypto built with --enable-fips against a validated OpenSSL FIPS provider; the fips_mode configuration parameter in the environment before the crypto module is first loaded, since OTP reads it when the NIF loads; enable_fips_mode/1 deprecated in OTP 28; and OTP's own sentence that a FIPS mode requested but not available fails to load the crypto module, so a release requiring crypto does not boot — and that this package enables none of it and cannot, by the same census; measured on the development runtime, where :crypto.info_fips/0 answers :not_supported, which OTP says names the build). A review lane reproduced every hash on both pages with coreutils alone, and the 0.5.0 goldens as the Versions section says.

Found on the way, by writing the FIPS page's sentence about it: the .app did not require crypto. An HTTP host had it only through plug and bandit, both optional; a stdio-only release built from the .app would have had no :crypto.hash/2 at all. Fixed (extra_applications), pinned by a test that reads the built .app.

Red first — the vocabulary census went red on the @type alone, before any test of the bytes. Twelve mutants over the envelope, zero survivors; three survived a first cut, each a pin that passed for the wrong reason: the diff's non-raising hash/2 hashed with the default over bytes naming SHA-384 while every pin went through hash!/2 (one body now, both forms pinned); the refusal pin proved the string "sha256" was refused, not that strings were (the domain is now generated from algorithms/0 — every other spelling of the three names refused); and the wire surface's diff path had no pin under a non-default algorithm, so a hash under the key sha384 beside bytes naming sha256 — the thing the member exists to prevent — passed 647 tests (pinned; the surface now reads the option once and threads it, so the key and the member cannot come from two reads). A review lane wrote a verifier from the page alone, in Python, and reproduced every hash on the page and every golden. Gate on the head: thirteen pass. 11 properties, 649 tests, 0 failures — 626 on main plus twenty-three. Latency stays out of the signed envelope, by decision: a measurement of one machine on one day, not a property of the graph; the unsigned sidecar carries it as before.

How to tell whether you are affected: a consumer that verifies by hashing the bytes it holds is not. A consumer that parses by position or by a fixed member list meets the member in three artefacts, each differently: the graph envelope at the second position, the diff record at the first (its keys sort), the sidecar not at all — but the sidecar's schema_version moves to 3 with the graph's. A host that passes nothing gets sha256 everywhere it did.

…ourth fixed member after the version, sha256 by default, sha384/sha512 by option, anything else refused at the option by name), schema_version 3 on the graph and on the diff record, the wire's hex keyed by the algorithm's name, and bytes at 2 verified the way the migration note will say (the version first, then SHA-256); the vocabulary census is the first red -- a @type over the three digests with no definition rows yet; the v2 goldens kept beside the new ones: 14 failures

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…ed member after the version -- sha256 by default and indefinitely, sha384/sha512 by option on encode/hash/hash_hex/hash_value, the diff's encode/hash/hash_hex and the wire surface's host options, anything else refused at the option by name before a byte is written; schema_version 3 on the graph and the diff record, 2 refused as 1 was; the digest computed at one site with the algorithm a variable, the key-holding census moved from two sites to one; the wire's hex keyed by the algorithm's name; the canonical page's rules 1 and 9, its worked example under all three digests with the sha384 bytes shown, and a Versions section that says what a verifier holding 1 or 2 does (SHA-256, unchanged bytes, forever); the diff page; the Algorithm family in the vocabulary; docs/crypto-posture.md and docs/fips.md; and a defect the FIPS page found on the way -- the .app did not require crypto, so a stdio-only release had no :crypto.hash/2 -- fixed and pinned by reading the built .app: 647 tests

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…e default over bytes naming sha384 survived while the bang had a body of its own and every pin went through it; the non-raising forms pinned; Mha2 re-cut at a caller of the digest site, since the census reads that line as text and a text kill proves nothing

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…, the default flipped, a fourth name admitted, bytes and hash encoded under different algorithms, the graph's version left at 2, the diff record without the member, the wire keyed sha256 whatever the choice, the diff hashing with the default -- nine, zero survivors; one survived a first cut (the diff's non-raising hash/2, which no pin reached while hash!/2 had its own body) and one was re-cut past a text kill

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…gorithms/0 -- every other spelling of the three names refused (a plant admitting the string sha384 survived a list that refused sha256 alone), given twice refused by that name, a non-keyword refused by name, a value naming a digest the option disagrees with refused; to_json/2 takes the option; the surface's diff path pinned under sha384 (a plant encoding the record under the default while keying the hex sha384 survived every test); the test's page-derived verifier reads the fixed-order prefix and refuses what the page calls malformed: 2 failures

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…the three atoms (every other spelling refused by name; given twice refused as such; a non-keyword refused by name, as Graph.new/1 does); hash_value/2 refuses a value naming a digest the option disagrees with -- the one public path where bytes could name a digest they were not hashed with; to_json/2 takes the option and the surface's byte-for-byte claim is stated under the same algorithm; the surface reads the algorithm once and threads it, so the key and the member cannot come from two reads; the page says the nodes and edges members do not depend on the algorithm, which is what lets the sha384/sha512 goldens be derived; rule 9 names all five functions; the graph's version comment says why 3; the test's page-derived verifier reads the fixed-order prefix and refuses what the page calls malformed

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…ring sha384 admitted; the surface's diff path under the default while the hex is keyed by the option) plus the value-names-another-digest guard removed; Mha8 re-anchored to the surface's one read; Mha11 re-cut past a compiler kill -- twelve, zero survivors

Signed-off-by: Ayla Croft <aylacroft@proton.me>
… recipe named schema_version 2, which Graph.new/1 now refuses -- it names the version by the function and a pin holds it there; docs/fips.md restated in OTP 28's own words -- enable_fips_mode/1 is deprecated ('use config parameter fips_mode'), the parameter is read when the crypto NIF loads so it must be in the environment before the module is, crypto:start/0 does not work for FIPS mode, and :not_supported names the build rather than the provider; the how-to-tell names the member's position in all three artefacts (graph second, diff first, sidecar none) and the sidecar's version move, which it had called 'as before'; '0.6.0' replaced by 'the release after 0.5.0' where a number was not the agent's to write; the livebook's one SHA-256 sentence

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…he mutated file)

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…s carry schema_version 2 or later, 3 today; docs/fips.md carries OTP's sentence the page had omitted -- a FIPS mode requested but not available fails to load the crypto module, so a release requiring crypto does not boot, the failure a FIPS host wants and one that comes before any check -- and marks its one inference from on_load as its own; the no-environment claim cites the reach census's pin on Application

Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah merged commit fddc85b into main Sep 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant