From eb24902d38b7e68554a01512cd71754e937677e5 Mon Sep 17 00:00:00 2001 From: samjanny Date: Wed, 3 Jun 2026 11:59:58 +0200 Subject: [PATCH 1/2] tor: add OnionAddress::from_origin_pubkey (pubkey -> address), crate 0.9.0 The crate could decode and strictly verify a Tor v3 onion address but had no public way to derive an address from an origin public key. A publisher building a manifest origin needs that direction. Add OnionAddress::from_origin_pubkey, the exact inverse of verify_strict: it computes the SHA3-256 checksum, encodes pubkey||checksum||version as lowercase base32 per rend-spec-v3, and appends .onion. Tests cover the encode/verify_strict round trip and the canonical corpus origin fixture (Gp8y4JM7... -> dkptfyeth...onion), confirming the output matches an independent encoder byte for byte. Additive public API; bump the crate 0.8.0 -> 0.9.0. Spec revision is unchanged (rc.48). --- README.md | 2 +- entangled-core/Cargo.toml | 2 +- entangled-core/src/tor/address.rs | 29 ++++++++++++++++++++++ entangled-core/tests/tor/address_decode.rs | 28 +++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c6581e..2324c54 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A site built with Entangled is not a web application. It is a set of signed JSON - [`entangled-core`](./entangled-core): the protocol core library. -Current crate version: `0.8.0`. +Current crate version: `0.9.0`. Implemented in `entangled-core`: diff --git a/entangled-core/Cargo.toml b/entangled-core/Cargo.toml index f1d0702..2b2bcdf 100644 --- a/entangled-core/Cargo.toml +++ b/entangled-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "entangled-core" -version = "0.8.0" +version = "0.9.0" edition = "2021" rust-version = "1.88" license = "MIT OR Apache-2.0" diff --git a/entangled-core/src/tor/address.rs b/entangled-core/src/tor/address.rs index 3b4c7bc..2ce3ab8 100644 --- a/entangled-core/src/tor/address.rs +++ b/entangled-core/src/tor/address.rs @@ -110,6 +110,35 @@ impl OnionAddress { }) } + /// Derive the canonical Tor v3 onion address from an origin public key. + /// + /// This is the inverse of [`OnionAddress::verify_strict`]: it computes the + /// `SHA3-256(".onion checksum" || pubkey || version)[:2]` checksum, encodes + /// `pubkey || checksum || version` as lowercase base32 per `rend-spec-v3.txt`, + /// and appends `.onion`. The result round-trips: `verify_strict` on the + /// returned address yields back `pubkey`, an unchanged checksum, and version + /// `0x03`. A publisher building a manifest origin derives the address this + /// way from its origin key. + pub fn from_origin_pubkey(pubkey: &OriginPubkey) -> Self { + let mut hasher = Sha3_256::new(); + hasher.update(CHECKSUM_PREFIX); + hasher.update(pubkey.as_bytes()); + hasher.update([TOR_V3_VERSION]); + let digest = hasher.finalize(); + + let mut body = [0u8; 35]; + body[..32].copy_from_slice(pubkey.as_bytes()); + body[32] = digest[0]; + body[33] = digest[1]; + body[34] = TOR_V3_VERSION; + + let address = BASE32.encode(&body).to_ascii_lowercase() + ".onion"; + // The encoding is canonical by construction (lowercase base32, 56-char + // body, .onion suffix), so the syntactic TryFrom cannot fail; the + // expect documents that invariant rather than guarding a real path. + OnionAddress::try_from(address).expect("derived onion address is canonical") + } + /// Strict verification per ยง05: decode, then check `version == 0x03` and /// recompute the SHA3-256 checksum, comparing byte-exact against the /// embedded prefix. Returns a [`DecodedOnionAddress`] whose `pubkey` is diff --git a/entangled-core/tests/tor/address_decode.rs b/entangled-core/tests/tor/address_decode.rs index ca73d44..efe1df0 100644 --- a/entangled-core/tests/tor/address_decode.rs +++ b/entangled-core/tests/tor/address_decode.rs @@ -3,6 +3,7 @@ use data_encoding::BASE32; use entangled_core::crypto::PublisherSigningKey; use entangled_core::tor::TorError; +use entangled_core::types::keys::OriginPubkey; use entangled_core::types::manifest::OnionAddress; use sha3::{Digest, Sha3_256}; @@ -129,6 +130,33 @@ fn rejects_wrong_length() { assert!(format!("{err}").contains("characters")); } +/// `from_origin_pubkey` is the inverse of `verify_strict`: encoding a pubkey +/// and decoding the result yields back the same pubkey, a valid checksum, and +/// version 0x03. +#[test] +fn from_origin_pubkey_round_trip() { + let pubkey = OriginPubkey::from_bytes(pubkey_from_seed(0x5A)); + let addr = OnionAddress::from_origin_pubkey(&pubkey); + let decoded = addr.verify_strict().expect("derived address must verify"); + assert_eq!(decoded.pubkey, pubkey); + assert_eq!(decoded.version, 0x03); +} + +/// `from_origin_pubkey` matches the canonical address an independent encoder +/// (the corpus generator) produces for the same origin key. The corpus origin +/// fixture key `Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo` derives to +/// `dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion`. +#[test] +fn from_origin_pubkey_corpus_fixture() { + let pubkey = OriginPubkey::try_from("Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo") + .expect("valid base64url pubkey"); + let addr = OnionAddress::from_origin_pubkey(&pubkey); + assert_eq!( + addr.as_str(), + "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + ); +} + /// Public test vector: DuckDuckGo's onion service v3 address. We can't verify /// "the right pubkey" without their key material, but the checksum and version /// byte must verify under our decoder for the implementation to be From 38e95c83994479fc05b74b16795b09731324a5fa Mon Sep 17 00:00:00 2001 From: samjanny Date: Wed, 3 Jun 2026 12:03:02 +0200 Subject: [PATCH 2/2] tor: update Cargo.lock for the 0.9.0 version bump CI builds with --locked, which rejects a Cargo.lock that disagrees with Cargo.toml. The 0.8.0 -> 0.9.0 bump updated the manifest but not the lock, so every --locked job failed. Sync the lock. --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index af054df..d32e6ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,7 +300,7 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "entangled-core" -version = "0.8.0" +version = "0.9.0" dependencies = [ "criterion", "data-encoding",