use std::convert::{TryFrom, TryInto};
use blake2::Blake2b;
use chrono::{DateTime, Utc};
use digest::consts::U64;
use prost::Message;
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use tari_crypto::hashing::DomainSeparatedHasher;
use tari_utilities::{ByteArray, ByteArrayError};
use super::hashing::{CommsCorePeerManagerDomain, IDENTITY_SIGNATURE, comms_core_peer_manager_domain};
use crate::{
message::MessageExt,
multiaddr::Multiaddr,
peer_manager::{PeerFeatures, PeerManagerError},
proto,
types::{CommsPublicKey, CommsSecretKey, CompressedSignature, Signature},
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IdentitySignature {
version: u8,
signature: CompressedSignature,
updated_at: DateTime<Utc>,
}
impl IdentitySignature {
pub const LATEST_VERSION: u8 = 0;
pub fn new(version: u8, signature: CompressedSignature, updated_at: DateTime<Utc>) -> Self {
Self {
version,
signature,
updated_at,
}
}
pub(crate) fn sign_new<'a, I: IntoIterator<Item = &'a Multiaddr>>(
secret_key: &CommsSecretKey,
features: PeerFeatures,
addresses: I,
updated_at: DateTime<Utc>,
) -> Self {
let public_key = CommsPublicKey::from_secret_key(secret_key);
let (secret_nonce, public_nonce) = CommsPublicKey::random_keypair(&mut OsRng);
let challenge = Self::construct_challenge(
&public_key,
&public_nonce,
Self::LATEST_VERSION,
features,
addresses,
updated_at,
)
.finalize();
let signature = CompressedSignature::new_from_schnorr(
Signature::sign_raw_uniform(secret_key, secret_nonce, challenge.as_ref())
.expect("unreachable panic: challenge hash digest is the correct length"),
);
Self {
version: Self::LATEST_VERSION,
signature,
updated_at,
}
}
pub fn signature(&self) -> &CompressedSignature {
&self.signature
}
pub fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
pub fn version(&self) -> u8 {
self.version
}
pub fn is_valid<'a, I: IntoIterator<Item = &'a Multiaddr>>(
&self,
public_key: &CommsPublicKey,
features: PeerFeatures,
addresses: I,
) -> Result<bool, ByteArrayError> {
if self.updated_at.timestamp() < 0 {
return Ok(false);
}
if self.updated_at > Utc::now() + chrono::Duration::days(1) {
return Ok(false);
}
let challenge = Self::construct_challenge(
public_key,
self.signature.get_compressed_public_nonce(),
self.version,
features,
addresses,
self.updated_at,
)
.finalize();
let ristretto_key = public_key.to_public_key()?;
let ristretto_signature = self.signature.to_schnorr_signature()?;
Ok(ristretto_signature.verify_raw_uniform(&ristretto_key, challenge.as_ref()))
}
fn construct_challenge<'a, I: IntoIterator<Item = &'a Multiaddr>>(
public_key: &CommsPublicKey,
public_nonce: &CommsPublicKey,
version: u8,
features: PeerFeatures,
addresses: I,
updated_at: DateTime<Utc>,
) -> DomainSeparatedHasher<Blake2b<U64>, CommsCorePeerManagerDomain> {
let addresses = addresses.into_iter().collect::<Vec<_>>();
let challenge = comms_core_peer_manager_domain::<Blake2b<U64>>(IDENTITY_SIGNATURE)
.chain(public_key.as_bytes())
.chain(public_nonce.as_bytes())
.chain(version.to_le_bytes())
.chain(u64::try_from(updated_at.timestamp()).unwrap().to_le_bytes())
.chain(features.bits().to_le_bytes());
addresses
.into_iter()
.fold(challenge, |challenge, addr| challenge.chain(addr))
}
pub fn to_bytes(&self) -> Vec<u8> {
proto::identity::IdentitySignature::from(self).to_encoded_bytes()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, PeerManagerError> {
let sig = proto::identity::IdentitySignature::decode(bytes)
.map_err(|_| PeerManagerError::InvalidIdentitySignature)?
.try_into()?;
Ok(sig)
}
}
impl TryFrom<proto::identity::IdentitySignature> for IdentitySignature {
type Error = PeerManagerError;
fn try_from(value: proto::identity::IdentitySignature) -> Result<Self, Self::Error> {
let version = u8::try_from(value.version).map_err(|_| PeerManagerError::InvalidIdentitySignature)?;
let public_nonce = CommsPublicKey::from_canonical_bytes(&value.public_nonce)
.map_err(|_| PeerManagerError::InvalidIdentitySignature)?;
let signature = CommsSecretKey::from_canonical_bytes(&value.signature)
.map_err(|_| PeerManagerError::InvalidIdentitySignature)?;
let updated_at =
DateTime::<Utc>::from_timestamp(value.updated_at, 0).ok_or(PeerManagerError::InvalidIdentitySignature)?;
Ok(Self {
version,
signature: CompressedSignature::new(public_nonce, signature),
updated_at,
})
}
}
impl From<&IdentitySignature> for proto::identity::IdentitySignature {
fn from(identity_sig: &IdentitySignature) -> Self {
proto::identity::IdentitySignature {
version: u32::from(identity_sig.version),
signature: identity_sig.signature.get_signature().to_vec(),
public_nonce: identity_sig.signature.get_compressed_public_nonce().to_vec(),
updated_at: identity_sig.updated_at.timestamp(),
}
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use tari_crypto::keys::SecretKey;
use super::*;
mod is_valid_for_peer {
use super::*;
#[test]
fn it_returns_true_for_valid_signature() {
let secret = CommsSecretKey::random(&mut OsRng);
let public_key = CommsPublicKey::from_secret_key(&secret);
let address = Multiaddr::from_str("/ip4/127.0.0.1/tcp/1234").unwrap();
let updated_at = Utc::now();
let identity =
IdentitySignature::sign_new(&secret, PeerFeatures::COMMUNICATION_NODE, [&address], updated_at);
assert!(
identity
.is_valid(&public_key, PeerFeatures::COMMUNICATION_NODE, [&address])
.unwrap(),
"Signature is not valid"
);
}
#[test]
fn it_returns_false_for_tampered_address() {
let secret = CommsSecretKey::random(&mut OsRng);
let public_key = CommsPublicKey::from_secret_key(&secret);
let address = Multiaddr::from_str("/ip4/127.0.0.1/tcp/1234").unwrap();
let updated_at = Utc::now();
let identity =
IdentitySignature::sign_new(&secret, PeerFeatures::COMMUNICATION_NODE, [&address], updated_at);
let tampered = Multiaddr::from_str("/ip4/127.0.0.1/tcp/4321").unwrap();
assert!(
!identity
.is_valid(&public_key, PeerFeatures::COMMUNICATION_NODE, [&tampered])
.unwrap(),
"Signature is not valid"
);
}
#[test]
fn it_returns_false_for_tampered_features() {
let secret = CommsSecretKey::random(&mut OsRng);
let public_key = CommsPublicKey::from_secret_key(&secret);
let address = Multiaddr::from_str("/ip4/127.0.0.1/tcp/1234").unwrap();
let updated_at = Utc::now();
let identity =
IdentitySignature::sign_new(&secret, PeerFeatures::COMMUNICATION_NODE, [&address], updated_at);
let tampered = PeerFeatures::COMMUNICATION_CLIENT;
assert!(
!identity.is_valid(&public_key, tampered, [&address]).unwrap(),
"Signature is not valid"
);
}
}
}