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", 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