From 84d40356827c62d402d0c94cc4886c806d82108a Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Wed, 9 Sep 2026 00:22:00 -0700 Subject: [PATCH] Pin refusal of variable-width content-ref carriages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keyhive is generic over its content-reference type, and bincode encodes `[u8; 32]` bare but `Vec` length-prefixed. A peer instantiating Keyhive over a variable-width reference therefore emits a different wire format under the same Keyhive version, and `kh0` — Keyhive's default `[u8; 32]` hash — cannot read it. Minted carriages always have an empty `after_content`, which encodes identically under either type, so no existing test could observe the mismatch. Freeze a browser-captured carriage whose root delegation carries a 10-byte reference and pin the outcome: `UndecodableEvent` at entry 0, nobody authorized, nothing on path. Undecodable vouches nothing and is never misread into an acceptance. Document the `[u8; 32]` commitment on `ENVELOPE_TAG` and add a fixture README with the capture's provenance. --- onomancy_keyhive/src/carriage.rs | 11 ++- onomancy_keyhive/tests/fixtures/README.md | 18 ++++ .../variable_width_content_ref_carriage.hex | 8 ++ .../tests/variable_width_content_ref.rs | 88 +++++++++++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 onomancy_keyhive/tests/fixtures/README.md create mode 100644 onomancy_keyhive/tests/fixtures/variable_width_content_ref_carriage.hex create mode 100644 onomancy_keyhive/tests/variable_width_content_ref.rs diff --git a/onomancy_keyhive/src/carriage.rs b/onomancy_keyhive/src/carriage.rs index 9358b3d..f26ea0e 100644 --- a/onomancy_keyhive/src/carriage.rs +++ b/onomancy_keyhive/src/carriage.rs @@ -17,7 +17,16 @@ use keyhive_core::event::static_event::StaticEvent; use onomancy_core::delegation_chain::{DelegationChain, SignedDelegationBytes}; -/// The envelope version tag for Keyhive 0.5 bincode encoding. +/// The envelope version tag for Keyhive 0.5 bincode encoding with +/// `[u8; 32]` content references — Keyhive's default `ContentRef`, a +/// content hash. +/// +/// Keyhive is generic over its content-reference type, and bincode +/// encodes `[u8; 32]` bare but `Vec` length-prefixed, so a peer +/// instantiating Keyhive over a variable-width reference emits a +/// different wire format under the same Keyhive version. Such entries +/// are undecodable here: `kh0` names Keyhive's default, and every peer +/// whose proofs must verify here instantiates `[u8; 32]`. /// /// Keyhive is pre-alpha; when its event encoding changes, this tag /// bumps (`kh1`, …) and old entries fail loudly instead of misparsing. diff --git a/onomancy_keyhive/tests/fixtures/README.md b/onomancy_keyhive/tests/fixtures/README.md new file mode 100644 index 0000000..0216a58 --- /dev/null +++ b/onomancy_keyhive/tests/fixtures/README.md @@ -0,0 +1,18 @@ +# Carriage Fixtures + +Frozen authority carriages exercised by the integration tests in `tests/`. One `kh0` entry per line, hex-encoded. These are real-world captures, not generated: do not regenerate, recapture deliberately if ever needed. + +## Catalog + +| File | Test | Expectation | +|------|------|-------------| +| `variable_width_content_ref_carriage.hex` | `tests/variable_width_content_ref.rs` | invalid — `UndecodableEvent { index: 0 }`; authorizes nobody | + +## `variable_width_content_ref_carriage.hex` + +Captured 2026-09-08 from the `onomancy-demo` browser app (keyhive_wasm 0.1.0-alpha.8, automerge-repo-keyhive 0.5.0-alpha.6) while minting a certificate for `brooklynzelenka.com`. Eight entries: + +- entry 0 — `Delegated`: root-issued, `Admin`, delegate = the certificate's signer, issuer = the document. Signature verifies. Its `after_content` carries a length-prefixed 10-byte reference: the peer instantiated Keyhive over `Vec` content references, where `kh0` is Keyhive's default `[u8; 32]`. +- entries 1–7 — `PrekeysExpanded` by the signer. No content references, so they decode identically under either instantiation. + +The two wire formats differ only when `after_content` is non-empty, which minted carriages never are, so no generated fixture can detect an instantiation mismatch. This capture is the only thing that turns red if the crate is ever switched to variable-width references. Replace with a positive fixture once the browser stack emits `[u8; 32]` references. diff --git a/onomancy_keyhive/tests/fixtures/variable_width_content_ref_carriage.hex b/onomancy_keyhive/tests/fixtures/variable_width_content_ref_carriage.hex new file mode 100644 index 0000000..224dd32 --- /dev/null +++ b/onomancy_keyhive/tests/fixtures/variable_width_content_ref_carriage.hex @@ -0,0 +1,8 @@ +6b683003000000030000000020000000000000001423beeb0d6e8780f34ad031441269712c52345470bece360e256d642d66fff30000000000000000010000000000000020000000000000004d9a7593eaacded04a9773d34564a3a4cc280bab3e57eed908018250ba14243e01000000000000000a000000000000006623e915b1a66588610520000000000000004d9a7593eaacded04a9773d34564a3a4cc280bab3e57eed908018250ba14243ee135ee1e22086050bbd8ba867b14e18cac8f5cfe765906618d1f6ee0bfe693aab475ae5aec113388c3eac36486f4823e5fa71795234b44b929e680f41d101505 +6b6830000000006c49845e20a152c4771edf5f90ec027583fad1fb231b36805b8bccbae8acc75a20000000000000001423beeb0d6e8780f34ad031441269712c52345470bece360e256d642d66fff38641c0afac58c4fe0ed26a521b3749feb116b6a07b3924a9fa89bf09a5884682d0b2e0fc2e772134fe136a7dfa9b461a9e9af69b551b9e8dbdb9771e4f1b110a +6b683000000000a0b282a271f554c3dfc00aec0bd58a25549dfd12ed1c350f53be1d0e0799d53320000000000000001423beeb0d6e8780f34ad031441269712c52345470bece360e256d642d66fff38b821ff4c561cb11320f6be47ac80379697adc522e39e8154879786ceac98c2429a1fbd4c9e790ff0ebf3d969253c83410ae049f124482a2a7e818bc6f0de805 +6b683000000000f3b3d80a7b20736d4f8ee0b79856cd56bdae02d981427d37336794f4a7df550720000000000000001423beeb0d6e8780f34ad031441269712c52345470bece360e256d642d66fff31c74303db4118fc620e7d4e698cecfb6c5cefdb5c505d7c5de4cfbcb0b6de3ed927edcdb9d05606c51ec50d0bc9f4cc7b00800cb9f8822462570685a2a286a0e +6b683000000000d18e594f8815b21a2dee703870de1c22206e4021125a3d4ae2566fdc6986483620000000000000001423beeb0d6e8780f34ad031441269712c52345470bece360e256d642d66fff33c86931f700c08e38510d4f5f1340debdc2f58212b118c35273dcc05de17dc24e4268e48d3dc2435384efd7c8ef04124d57427f0c8e6c8a1568fed681ac0a502 +6b683000000000655d6db5ea9afdc9f6ea42b45f48381b77221599752502a4011ef58d6a83c24120000000000000001423beeb0d6e8780f34ad031441269712c52345470bece360e256d642d66fff3d1ce7ba0f110fc1de91c2b3837a5f096ec9dae8e00f7270fa9e2a6562bdf5af9bab0792a7c983623d133abf8287841372a309aef266525936f032c4b48a1ad0d +6b6830000000006c30b5f2eb94699c7572da9a3db228b7401dac040ac76cacd0a641a7cec2760220000000000000001423beeb0d6e8780f34ad031441269712c52345470bece360e256d642d66fff36f7960b6f39ca345972e46f6c71059838d564d3d8ffcc422bc54cffbe9ca089ce2ea2d8cc911c3eb5818be7ee623712c742440d55cd079297c2e78305b6b2d0f +6b683000000000cdc0f9c49a68b5830a20d9c76192f7b81f142d33127dd7a5283d4151d4605e4f20000000000000001423beeb0d6e8780f34ad031441269712c52345470bece360e256d642d66fff34e93a0950bb91dbbdc0aa62852907f940862b49da01d15ae3d604940b5172095d98038776d40c1da68c680355c1766c4b1e97f9575be803afce15ae7d5b43e08 diff --git a/onomancy_keyhive/tests/variable_width_content_ref.rs b/onomancy_keyhive/tests/variable_width_content_ref.rs new file mode 100644 index 0000000..49c09be --- /dev/null +++ b/onomancy_keyhive/tests/variable_width_content_ref.rs @@ -0,0 +1,88 @@ +//! A carriage from a peer that instantiated Keyhive over a +//! variable-width content reference (`Vec`) instead of the `[u8; 32]` +//! hash `kh0` names. Its one delegation is otherwise sound — root-issued, +//! Admin, to the signer — but `after_content` carries a length-prefixed +//! 10-byte reference where a bare 32-byte hash belongs, so the entry +//! cannot be read as a `kh0` event. +//! +//! The wire formats differ only when `after_content` is non-empty, which +//! minted carriages never are. This fixture pins the refusal: a proof in +//! the wrong encoding is undecodable, and undecodable vouches nothing — +//! it is never misread into an acceptance. + +use core::str; + +use ed25519_dalek::VerifyingKey; +use onomancy_core::{ + anchor::doc::DocAnchor, + delegation_chain::{DelegationChain, SignedDelegationBytes}, +}; +use onomancy_dnssec::txt::generation_key::GenerationKey; +use onomancy_keyhive::{ + authority::KeyhiveAuthority, + carriage::{Carriage, ParseCarriageError}, +}; +use onomancy_protocol::verifier::state::authority_verifier::AuthorityVerifier; +use testresult::TestResult; + +const CARRIAGE: &str = include_str!("fixtures/variable_width_content_ref_carriage.hex"); +const DOCUMENT: &str = "4d9a7593eaacded04a9773d34564a3a4cc280bab3e57eed908018250ba14243e"; +const SIGNER: &str = "1423beeb0d6e8780f34ad031441269712c52345470bece360e256d642d66fff3"; + +fn unhex(hex: &str) -> TestResult> { + let digits = hex.as_bytes(); + if !digits.len().is_multiple_of(2) { + return Err("odd-length hex".into()); + } + + digits + .chunks_exact(2) + .map(|pair| Ok(u8::from_str_radix(str::from_utf8(pair)?, 16)?)) + .collect() +} + +fn key(hex: &str) -> TestResult { + let bytes: [u8; 32] = unhex(hex)? + .try_into() + .map_err(|_| "key fixture is not 32 bytes")?; + Ok(VerifyingKey::from_bytes(&bytes)?) +} + +fn carriage() -> TestResult { + let entries = CARRIAGE + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| unhex(line.trim()).map(SignedDelegationBytes::from)) + .collect::>>()?; + Ok(DelegationChain::from(entries)) +} + +/// The delegation is entry 0; the prekey introductions after it carry +/// no content references and would decode under either instantiation. +#[test] +fn the_delegation_is_undecodable_and_named() -> TestResult { + assert!(matches!( + Carriage::parse(&carriage()?), + Err(ParseCarriageError::UndecodableEvent { index: 0, .. }) + )); + Ok(()) +} + +#[test] +fn an_undecodable_carriage_authorizes_nobody() -> TestResult { + let root = DocAnchor::from(key(DOCUMENT)?); + + assert!( + !KeyhiveAuthority.authorizes(&root, &key(SIGNER)?, &carriage()?), + "the signer really is the delegate, and it still proves nothing" + ); + Ok(()) +} + +#[test] +fn an_undecodable_carriage_puts_nothing_on_the_path() -> TestResult { + let generation = GenerationKey::from(key(SIGNER)?); + + assert!(!KeyhiveAuthority.on_path(&carriage()?, &generation)); + Ok(()) +}