Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down
2 changes: 1 addition & 1 deletion entangled-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
29 changes: 29 additions & 0 deletions entangled-core/src/tor/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions entangled-core/tests/tor/address_decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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
Expand Down
Loading