use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use irontide_core::{Id20, sha1};
use crate::error::{Error, Result};
pub const MAX_VALUE_SIZE: usize = 1000;
pub const MAX_SALT_SIZE: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImmutableItem {
pub value: Vec<u8>,
pub target: Id20,
}
impl ImmutableItem {
pub fn new(value: Vec<u8>) -> Result<Self> {
if value.len() > MAX_VALUE_SIZE {
return Err(Error::Bep44ValueTooLarge {
size: value.len(),
max: MAX_VALUE_SIZE,
});
}
let target = sha1(&value);
Ok(Self { value, target })
}
#[must_use]
pub fn verify(&self) -> bool {
sha1(&self.value) == self.target
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MutableItem {
pub value: Vec<u8>,
pub public_key: [u8; 32],
pub signature: [u8; 64],
pub seq: i64,
pub salt: Vec<u8>,
pub target: Id20,
}
impl MutableItem {
pub fn create(keypair: &SigningKey, value: Vec<u8>, seq: i64, salt: Vec<u8>) -> Result<Self> {
if value.len() > MAX_VALUE_SIZE {
return Err(Error::Bep44ValueTooLarge {
size: value.len(),
max: MAX_VALUE_SIZE,
});
}
if salt.len() > MAX_SALT_SIZE {
return Err(Error::Bep44SaltTooLarge {
size: salt.len(),
max: MAX_SALT_SIZE,
});
}
let public_key = keypair.verifying_key().to_bytes();
let target = compute_mutable_target(&public_key, &salt);
let sign_buf = build_signing_buffer(&salt, seq, &value);
let signature = keypair.sign(&sign_buf);
Ok(Self {
value,
public_key,
signature: signature.to_bytes(),
seq,
salt,
target,
})
}
#[must_use]
pub fn verify(&self) -> bool {
let Ok(verifying_key) = VerifyingKey::from_bytes(&self.public_key) else {
return false;
};
let signature = Signature::from_bytes(&self.signature);
let sign_buf = build_signing_buffer(&self.salt, self.seq, &self.value);
verifying_key.verify(&sign_buf, &signature).is_ok()
}
#[must_use]
pub fn verify_target(&self) -> bool {
compute_mutable_target(&self.public_key, &self.salt) == self.target
}
}
#[must_use]
pub fn compute_mutable_target(public_key: &[u8; 32], salt: &[u8]) -> Id20 {
let mut buf = Vec::with_capacity(32 + salt.len());
buf.extend_from_slice(public_key);
buf.extend_from_slice(salt);
sha1(&buf)
}
#[must_use]
pub fn build_signing_buffer(salt: &[u8], seq: i64, value: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(64 + value.len() + salt.len());
if !salt.is_empty() {
buf.extend_from_slice(b"4:salt");
buf.extend_from_slice(salt.len().to_string().as_bytes());
buf.push(b':');
buf.extend_from_slice(salt);
}
buf.extend_from_slice(b"3:seqi");
buf.extend_from_slice(seq.to_string().as_bytes());
buf.push(b'e');
buf.extend_from_slice(b"1:v");
buf.extend_from_slice(value.len().to_string().as_bytes());
buf.push(b':');
buf.extend_from_slice(value);
buf
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn immutable_item_hash_is_sha1_of_value() {
let value = b"12:Hello World!".to_vec(); let item = ImmutableItem::new(value.clone()).unwrap();
assert_eq!(item.target, sha1(&value));
assert!(item.verify());
}
#[test]
fn immutable_item_rejects_oversized_value() {
let value = vec![0u8; MAX_VALUE_SIZE + 1];
let err = ImmutableItem::new(value).unwrap_err();
assert!(err.to_string().contains("too large"));
}
#[test]
fn immutable_item_verify_detects_corruption() {
let mut item = ImmutableItem::new(b"5:hello".to_vec()).unwrap();
item.value[0] = 0xFF; assert!(!item.verify());
}
#[test]
fn mutable_item_sign_and_verify() {
let keypair = SigningKey::from_bytes(&[1u8; 32]);
let value = b"12:Hello World!".to_vec();
let item = MutableItem::create(&keypair, value, 1, Vec::new()).unwrap();
assert!(item.verify());
assert!(item.verify_target());
}
#[test]
fn mutable_item_with_salt() {
let keypair = SigningKey::from_bytes(&[2u8; 32]);
let value = b"4:test".to_vec();
let salt = b"my-salt".to_vec();
let item = MutableItem::create(&keypair, value, 42, salt.clone()).unwrap();
assert!(item.verify());
assert!(item.verify_target());
assert_eq!(item.salt, salt);
}
#[test]
fn mutable_item_rejects_oversized_value() {
let keypair = SigningKey::from_bytes(&[3u8; 32]);
let value = vec![0u8; MAX_VALUE_SIZE + 1];
let err = MutableItem::create(&keypair, value, 1, Vec::new()).unwrap_err();
assert!(err.to_string().contains("too large"));
}
#[test]
fn mutable_item_rejects_oversized_salt() {
let keypair = SigningKey::from_bytes(&[4u8; 32]);
let value = b"4:test".to_vec();
let salt = vec![0u8; MAX_SALT_SIZE + 1];
let err = MutableItem::create(&keypair, value, 1, salt).unwrap_err();
assert!(err.to_string().contains("salt"));
}
#[test]
fn mutable_item_verify_fails_on_wrong_key() {
let keypair = SigningKey::from_bytes(&[5u8; 32]);
let value = b"4:test".to_vec();
let mut item = MutableItem::create(&keypair, value, 1, Vec::new()).unwrap();
item.public_key = SigningKey::from_bytes(&[6u8; 32])
.verifying_key()
.to_bytes();
assert!(!item.verify());
}
#[test]
fn mutable_item_verify_fails_on_tampered_value() {
let keypair = SigningKey::from_bytes(&[7u8; 32]);
let value = b"4:test".to_vec();
let mut item = MutableItem::create(&keypair, value, 1, Vec::new()).unwrap();
item.value = b"5:tampr".to_vec();
assert!(!item.verify());
}
#[test]
fn mutable_target_is_sha1_of_pubkey_plus_salt() {
let pubkey = [0xABu8; 32];
let salt = b"hello";
let target = compute_mutable_target(&pubkey, salt);
let mut expected_buf = Vec::new();
expected_buf.extend_from_slice(&pubkey);
expected_buf.extend_from_slice(salt);
assert_eq!(target, sha1(&expected_buf));
}
#[test]
fn mutable_target_no_salt() {
let pubkey = [0xCDu8; 32];
let target = compute_mutable_target(&pubkey, &[]);
assert_eq!(target, sha1(&pubkey));
}
#[test]
fn signing_buffer_no_salt() {
let buf = build_signing_buffer(&[], 1, b"12:Hello World!");
assert_eq!(buf, b"3:seqi1e1:v15:12:Hello World!");
}
#[test]
fn signing_buffer_with_salt() {
let buf = build_signing_buffer(b"foobar", 4, b"12:Hello World!");
assert_eq!(buf, b"4:salt6:foobar3:seqi4e1:v15:12:Hello World!");
}
#[test]
fn signing_buffer_negative_seq() {
let buf = build_signing_buffer(&[], -1, b"1:x");
assert_eq!(buf, b"3:seqi-1e1:v3:1:x");
}
#[test]
fn sequence_number_monotonicity() {
let keypair = SigningKey::from_bytes(&[8u8; 32]);
let item1 = MutableItem::create(&keypair, b"4:aaaa".to_vec(), 1, Vec::new()).unwrap();
let item2 = MutableItem::create(&keypair, b"4:bbbb".to_vec(), 2, Vec::new()).unwrap();
assert!(item2.seq > item1.seq);
assert!(item1.verify());
assert!(item2.verify());
}
const BEP44_PUBLIC_KEY: [u8; 32] = [
0x77, 0xff, 0x84, 0x90, 0x5a, 0x91, 0x93, 0x63, 0x67, 0xc0, 0x13, 0x60, 0x80, 0x31, 0x04,
0xf9, 0x24, 0x32, 0xfc, 0xd9, 0x04, 0xa4, 0x35, 0x11, 0x87, 0x6d, 0xf5, 0xcd, 0xf3, 0xe7,
0xe5, 0x48,
];
const BEP44_EXPANDED_KEY: [u8; 64] = [
0xe0, 0x6d, 0x31, 0x83, 0xd1, 0x41, 0x59, 0x22, 0x84, 0x33, 0xed, 0x59, 0x92, 0x21, 0xb8,
0x0b, 0xd0, 0xa5, 0xce, 0x83, 0x52, 0xe4, 0xbd, 0xf0, 0x26, 0x2f, 0x76, 0x78, 0x6e, 0xf1,
0xc7, 0x4d, 0xb7, 0xe7, 0xa9, 0xfe, 0xa2, 0xc0, 0xeb, 0x26, 0x9d, 0x61, 0xe3, 0xb3, 0x8e,
0x45, 0x0a, 0x22, 0xe7, 0x54, 0x94, 0x1a, 0xc7, 0x84, 0x79, 0xd6, 0xc5, 0x4e, 0x1f, 0xaf,
0x60, 0x37, 0x88, 0x1d,
];
const BEP44_VALUE: &[u8] = b"12:Hello World!";
fn bep44_spec_sign(message: &[u8]) -> [u8; 64] {
use ed25519_dalek::hazmat::{ExpandedSecretKey, raw_sign};
use sha2::Sha512;
let esk = ExpandedSecretKey::from_bytes(&BEP44_EXPANDED_KEY);
let vk = VerifyingKey::from_bytes(&BEP44_PUBLIC_KEY).unwrap();
let sig = raw_sign::<Sha512>(&esk, message, &vk);
sig.to_bytes()
}
#[test]
fn bep44_mutable_no_salt_spec_vector() {
let expected_target = Id20::from_hex("4a533d47ec9c7d95b1ad75f576cffc641853b750").unwrap();
let value = BEP44_VALUE.to_vec();
let seq: i64 = 1;
let salt = Vec::new();
let target = compute_mutable_target(&BEP44_PUBLIC_KEY, &salt);
assert_eq!(target, expected_target, "mutable target (no salt) mismatch");
let sign_buf = build_signing_buffer(&salt, seq, &value);
let sig_bytes = bep44_spec_sign(&sign_buf);
let item = MutableItem {
value,
public_key: BEP44_PUBLIC_KEY,
signature: sig_bytes,
seq,
salt,
target,
};
assert!(item.verify(), "signature verification failed");
assert!(item.verify_target(), "target verification failed");
}
#[test]
fn bep44_mutable_salted_spec_vector() {
let expected_target = Id20::from_hex("411eba73b6f087ca51a3795d9c8c938d365e32c1").unwrap();
let value = BEP44_VALUE.to_vec();
let seq: i64 = 1;
let salt = b"foobar".to_vec();
let target = compute_mutable_target(&BEP44_PUBLIC_KEY, &salt);
assert_eq!(target, expected_target, "mutable target (salted) mismatch");
let sign_buf = build_signing_buffer(&salt, seq, &value);
let sig_bytes = bep44_spec_sign(&sign_buf);
let item = MutableItem {
value,
public_key: BEP44_PUBLIC_KEY,
signature: sig_bytes,
seq,
salt,
target,
};
assert!(item.verify(), "signature verification failed");
assert!(item.verify_target(), "target verification failed");
}
#[test]
fn bep44_immutable_spec_vector() {
let expected_target = Id20::from_hex("e5f96f6f38320f0f33959cb4d3d656452117aadb").unwrap();
let item = ImmutableItem::new(BEP44_VALUE.to_vec()).unwrap();
assert_eq!(item.target, expected_target, "immutable target mismatch");
assert!(item.verify(), "immutable item verification failed");
}
#[test]
fn bep44_signing_buffer_construction() {
let buf = build_signing_buffer(&[], 1, BEP44_VALUE);
let expected = b"3:seqi1e1:v15:12:Hello World!";
assert_eq!(buf, expected, "signing buffer mismatch for vector 1");
let buf_salted = build_signing_buffer(b"foobar", 1, BEP44_VALUE);
let expected_salted = b"4:salt6:foobar3:seqi1e1:v15:12:Hello World!";
assert_eq!(
buf_salted, expected_salted,
"signing buffer mismatch for vector 2"
);
}
}