use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, time::SystemTime};
use crate::Id;
const MAX_TIMESTAMP_TOLERANCE: u64 = 45 * 1000 * 1000;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SignedAnnounce {
pub(crate) key: [u8; 32],
pub(crate) timestamp: u64,
#[serde(with = "serde_bytes")]
pub(crate) signature: [u8; 64],
}
#[allow(dead_code)]
impl SignedAnnounce {
pub fn new(signer: &SigningKey, info_hash: &Id) -> Self {
let timestamp = system_time();
Self::new_with_timestamp(signer, info_hash, timestamp)
}
pub(crate) fn new_with_timestamp(signer: &SigningKey, info_hash: &Id, timestamp: u64) -> Self {
let signable = encode_signable(info_hash, timestamp);
let signature = signer.sign(&signable);
Self {
key: signer.verifying_key().as_bytes().to_owned(),
timestamp,
signature: signature.to_bytes(),
}
}
pub(crate) fn from_dht_request(
info_hash: &Id,
key: &[u8],
timestamp: u64,
signature: &[u8],
) -> Result<Self, SignedAnnounceError> {
Self::from_dht_message(info_hash, key, timestamp, signature, true)
}
pub(crate) fn from_dht_response(
info_hash: &Id,
key: &[u8],
timestamp: u64,
signature: &[u8],
) -> Result<Self, SignedAnnounceError> {
Self::from_dht_message(info_hash, key, timestamp, signature, false)
}
fn from_dht_message(
info_hash: &Id,
key: &[u8],
timestamp: u64,
signature: &[u8],
validate_timestamp: bool,
) -> Result<Self, SignedAnnounceError> {
let key = VerifyingKey::try_from(key).map_err(|_| SignedAnnounceError::PublicKey)?;
let signature =
Signature::from_slice(signature).map_err(|_| SignedAnnounceError::Signature)?;
key.verify(&encode_signable(info_hash, timestamp), &signature)
.map_err(|_| SignedAnnounceError::Signature)?;
let now = system_time();
if validate_timestamp && now.abs_diff(timestamp) > MAX_TIMESTAMP_TOLERANCE {
return Err(SignedAnnounceError::Timestamp);
}
Ok(Self {
key: key.to_bytes(),
timestamp,
signature: signature.to_bytes(),
})
}
pub fn key(&self) -> &[u8; 32] {
&self.key
}
pub fn timestamp(&self) -> u64 {
self.timestamp
}
pub fn signature(&self) -> &[u8; 64] {
&self.signature
}
}
fn system_time() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("time drift")
.as_micros() as u64
}
pub fn encode_signable(info_hash: &Id, timestamp: u64) -> Box<[u8]> {
let mut signable = vec![];
signable.extend(info_hash.as_bytes());
signable.extend(timestamp.to_be_bytes());
signable.into()
}
#[n0_error::stack_error(derive, std_sources)]
pub enum SignedAnnounceError {
#[error("Invalid signed announce signature")]
Signature,
#[error("Invalid signed announce public key")]
PublicKey,
#[error("Invalid signed announce timestamp (too far in the future or the past)")]
Timestamp,
}
#[cfg(test)]
mod tests {
use rand::Rng;
use super::*;
#[test]
fn more_than_time_tolerance() {
let mut secret_key = [0; 32];
rand::rng().fill_bytes(&mut secret_key);
let signer = SigningKey::from_bytes(&secret_key);
let info_hash = Id::random();
let now = system_time();
let announce =
SignedAnnounce::new_with_timestamp(&signer, &info_hash, now + 50 * 1000 * 1000);
let result = SignedAnnounce::from_dht_request(
&info_hash,
announce.key(),
announce.timestamp,
&announce.signature,
);
assert!(matches!(result, Err(SignedAnnounceError::Timestamp)));
let now = system_time();
let announce =
SignedAnnounce::new_with_timestamp(&signer, &info_hash, now - 50 * 1000 * 1000);
let result = SignedAnnounce::from_dht_request(
&info_hash,
announce.key(),
announce.timestamp,
&announce.signature,
);
assert!(matches!(result, Err(SignedAnnounceError::Timestamp)));
}
#[test]
fn invalid_signature() {
let mut secret_key = [0; 32];
rand::rng().fill_bytes(&mut secret_key);
let signer = SigningKey::from_bytes(&secret_key);
let info_hash = Id::random();
let announce = SignedAnnounce::new(&signer, &info_hash);
SignedAnnounce::from_dht_request(
&info_hash,
announce.key(),
announce.timestamp,
&announce.signature,
)
.unwrap();
let result = SignedAnnounce::from_dht_request(
&info_hash,
announce.key(),
announce.timestamp,
&[0; 64],
);
assert!(matches!(result, Err(SignedAnnounceError::Signature)));
let result = SignedAnnounce::from_dht_request(
&info_hash,
&[0; 30],
announce.timestamp,
&announce.signature,
);
assert!(matches!(result, Err(SignedAnnounceError::PublicKey)));
}
}