use k256::ecdsa::SigningKey;
pub const MAGIC: &[u8; 6] = b"lhsdp2";
const DIGEST_TAG: &[u8] = b"localharness/v0/sdpseal";
const ADDR_LEN: usize = 20;
const SIG_LEN: usize = 65;
const HEADER_LEN: usize = MAGIC.len() + ADDR_LEN + SIG_LEN;
fn envelope_digest(sender: &[u8; 20], recipient: &[u8; 20], sealed: &[u8]) -> [u8; 32] {
use sha3::{Digest, Keccak256};
let mut h = Keccak256::new();
h.update(DIGEST_TAG);
h.update(sender);
h.update(recipient);
h.update(sealed);
let d = h.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&d);
out
}
pub fn seal_envelope(sender_key: &SigningKey, recipient: &[u8; 20], sealed: &[u8]) -> Vec<u8> {
let sender = crate::wallet::address(sender_key);
let digest = envelope_digest(&sender, recipient, sealed);
let sig = crate::wallet::sign_hash(sender_key, &digest);
let mut out = Vec::with_capacity(HEADER_LEN + sealed.len());
out.extend_from_slice(MAGIC);
out.extend_from_slice(&sender);
out.extend_from_slice(&sig);
out.extend_from_slice(sealed);
out
}
pub fn open_envelope(
blob: &[u8],
expected_sender: &[u8; 20],
recipient: &[u8; 20],
) -> Option<Vec<u8>> {
if blob.len() < HEADER_LEN || &blob[..MAGIC.len()] != MAGIC {
return None;
}
let sender: [u8; 20] = blob[MAGIC.len()..MAGIC.len() + ADDR_LEN].try_into().ok()?;
if &sender != expected_sender {
return None;
}
let sig: [u8; 65] = blob[MAGIC.len() + ADDR_LEN..HEADER_LEN].try_into().ok()?;
let sealed = &blob[HEADER_LEN..];
let digest = envelope_digest(&sender, recipient, sealed);
let recovered = crate::wallet::recover_address(&sig, &digest).ok()?;
(recovered == sender).then(|| sealed.to_vec())
}
pub fn address_of_sec1_pubkey(pubkey_sec1: &[u8]) -> Option<[u8; 20]> {
use k256::PublicKey;
use k256::elliptic_curve::sec1::ToEncodedPoint;
use sha3::{Digest, Keccak256};
let pk = PublicKey::from_sec1_bytes(pubkey_sec1).ok()?;
let uncompressed = pk.to_encoded_point(false); let bytes = uncompressed.as_bytes();
if bytes.len() != 65 {
return None;
}
let digest = Keccak256::digest(&bytes[1..]); let mut addr = [0u8; 20];
addr.copy_from_slice(&digest[12..]);
Some(addr)
}
pub fn roster_entry_valid(
peer: &[u8; 20],
ts: u64,
pubkey_sec1: &[u8],
now: u64,
ttl_secs: u64,
) -> bool {
if pubkey_sec1.is_empty() {
return false; }
if now.saturating_sub(ts) > ttl_secs {
return false;
}
address_of_sec1_pubkey(pubkey_sec1).is_some_and(|derived| &derived == peer)
}
#[cfg(test)]
mod tests {
use super::*;
fn keypair() -> (SigningKey, [u8; 20]) {
let w = crate::wallet::generate();
let addr = crate::wallet::address(&w.signer);
(w.signer.clone(), addr)
}
#[test]
fn seal_open_round_trip_binary_payload() {
let (sender_key, sender) = keypair();
let (_, recipient) = keypair();
let sealed: Vec<u8> = vec![0x02, 0x00, b'\n', 0xFF, b'\n', 0x00, 0xAB];
let blob = seal_envelope(&sender_key, &recipient, &sealed);
assert_eq!(&blob[..6], MAGIC);
assert_eq!(&blob[6..26], &sender);
let opened = open_envelope(&blob, &sender, &recipient).expect("round trip");
assert_eq!(opened, sealed);
}
#[test]
fn tampering_any_byte_rejects() {
let (sender_key, sender) = keypair();
let (_, recipient) = keypair();
let sealed = b"sealed-sdp-bytes".to_vec();
let blob = seal_envelope(&sender_key, &recipient, &sealed);
for i in 0..blob.len() {
let mut t = blob.clone();
t[i] ^= 0x01;
assert!(
open_envelope(&t, &sender, &recipient).is_none(),
"tampered byte {i} must be rejected"
);
}
}
#[test]
fn forged_sender_claim_rejects() {
let (attacker_key, _) = keypair();
let (_, legit_sender) = keypair();
let (_, recipient) = keypair();
let sealed = b"attacker sdp".to_vec();
let digest = envelope_digest(&legit_sender, &recipient, &sealed);
let sig = crate::wallet::sign_hash(&attacker_key, &digest);
let mut blob = Vec::new();
blob.extend_from_slice(MAGIC);
blob.extend_from_slice(&legit_sender);
blob.extend_from_slice(&sig);
blob.extend_from_slice(&sealed);
assert!(open_envelope(&blob, &legit_sender, &recipient).is_none());
}
#[test]
fn wrong_expected_sender_rejects() {
let (a_key, a) = keypair();
let (_, b) = keypair();
let (_, recipient) = keypair();
let blob = seal_envelope(&a_key, &recipient, b"sdp");
assert!(open_envelope(&blob, &b, &recipient).is_none());
assert!(open_envelope(&blob, &a, &recipient).is_some());
}
#[test]
fn cross_inbox_replay_rejects() {
let (sender_key, sender) = keypair();
let (_, r1) = keypair();
let (_, r2) = keypair();
let blob = seal_envelope(&sender_key, &r1, b"sdp");
assert!(open_envelope(&blob, &sender, &r2).is_none());
assert!(open_envelope(&blob, &sender, &r1).is_some());
}
#[test]
fn legacy_and_garbage_blobs_reject() {
let (_, sender) = keypair();
let (_, recipient) = keypair();
let legacy = b"0x1111111111111111111111111111111111111111\n\x02sealed".to_vec();
assert!(open_envelope(&legacy, &sender, &recipient).is_none());
assert!(open_envelope(b"", &sender, &recipient).is_none());
assert!(open_envelope(b"lhsdp2", &sender, &recipient).is_none()); assert!(open_envelope(&[0xAAu8; 200], &sender, &recipient).is_none());
let (k, s) = keypair();
let mut blob = seal_envelope(&k, &recipient, b"x");
blob[0] ^= 0xFF;
assert!(open_envelope(&blob, &s, &recipient).is_none());
}
#[test]
fn empty_sealed_payload_still_authenticates() {
let (sender_key, sender) = keypair();
let (_, recipient) = keypair();
let blob = seal_envelope(&sender_key, &recipient, b"");
assert_eq!(blob.len(), HEADER_LEN);
assert_eq!(open_envelope(&blob, &sender, &recipient).unwrap(), b"");
}
#[test]
fn envelope_digest_is_domain_separated_and_order_sensitive() {
let (_, a) = keypair();
let (_, b) = keypair();
assert_ne!(envelope_digest(&a, &b, b"x"), envelope_digest(&b, &a, b"x"));
assert_ne!(envelope_digest(&a, &b, b"x"), envelope_digest(&a, &b, b"y"));
use sha3::{Digest, Keccak256};
let mut pre = Vec::new();
pre.extend_from_slice(b"localharness/v0/sdpseal");
pre.extend_from_slice(&a);
pre.extend_from_slice(&b);
pre.extend_from_slice(b"x");
let expect: [u8; 32] = Keccak256::digest(&pre).into();
assert_eq!(envelope_digest(&a, &b, b"x"), expect);
}
#[test]
fn address_of_sec1_pubkey_matches_wallet_address() {
let (key, addr) = keypair();
let compressed = crate::wallet::pubkey_compressed(&key);
assert_eq!(address_of_sec1_pubkey(&compressed), Some(addr));
let uncompressed = k256::ecdsa::VerifyingKey::from(&key)
.to_encoded_point(false)
.as_bytes()
.to_vec();
assert_eq!(address_of_sec1_pubkey(&uncompressed), Some(addr));
assert_eq!(address_of_sec1_pubkey(&[0u8; 33]), None);
assert_eq!(address_of_sec1_pubkey(b""), None);
}
#[test]
fn roster_entry_validation_gates() {
let (key, addr) = keypair();
let pubkey = crate::wallet::pubkey_compressed(&key);
let now = 1_000_000u64;
let ttl = 600u64;
assert!(roster_entry_valid(&addr, now, &pubkey, now, ttl));
assert!(roster_entry_valid(&addr, now - ttl, &pubkey, now, ttl)); assert!(roster_entry_valid(&addr, now + 30, &pubkey, now, ttl)); assert!(!roster_entry_valid(&addr, now - ttl - 1, &pubkey, now, ttl));
assert!(!roster_entry_valid(&addr, now, &[], now, ttl));
let (_, other) = keypair();
assert!(!roster_entry_valid(&other, now, &pubkey, now, ttl));
assert!(!roster_entry_valid(&addr, now, &[0u8; 33], now, ttl));
}
}