use alloc::string::String;
use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
use pamoja_core::{Error, Result};
use crate::Signature;
#[derive(Clone)]
pub struct DeviceIdentity {
signing: SigningKey,
}
impl DeviceIdentity {
pub fn from_seed(seed: &[u8; 32]) -> Self {
Self {
signing: SigningKey::from_bytes(seed),
}
}
pub fn public(&self) -> PublicIdentity {
PublicIdentity {
verifying: self.signing.verifying_key(),
}
}
pub fn sign(&self, payload: &[u8]) -> Signature {
Signature(self.signing.sign(payload))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PublicIdentity {
verifying: VerifyingKey,
}
impl PublicIdentity {
pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self> {
VerifyingKey::from_bytes(bytes)
.map(|verifying| Self { verifying })
.map_err(|_| Error::Auth("invalid public identity".into()))
}
pub fn to_bytes(&self) -> [u8; 32] {
self.verifying.to_bytes()
}
pub fn fingerprint(&self) -> String {
let bytes = self.verifying.to_bytes();
let mut hex = String::with_capacity(16);
for &byte in &bytes[..8] {
hex.push(nibble(byte >> 4));
hex.push(nibble(byte & 0x0f));
}
hex
}
pub fn verify(&self, payload: &[u8], signature: &Signature) -> Result<()> {
self.verifying
.verify(payload, &signature.0)
.map_err(|_| Error::Auth("signature verification failed".into()))
}
}
fn nibble(value: u8) -> char {
char::from_digit(u32::from(value), 16).unwrap_or('0')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_signature_verifies_against_its_signer() {
let device = DeviceIdentity::from_seed(&[1u8; 32]);
let signature = device.sign(b"reading");
assert!(device.public().verify(b"reading", &signature).is_ok());
}
#[test]
fn a_tampered_payload_fails_verification() {
let device = DeviceIdentity::from_seed(&[2u8; 32]);
let signature = device.sign(b"4.8C");
let result = device.public().verify(b"9.9C", &signature);
assert!(matches!(result, Err(Error::Auth(_))));
}
#[test]
fn another_device_cannot_verify_the_signature() {
let device = DeviceIdentity::from_seed(&[3u8; 32]);
let other = DeviceIdentity::from_seed(&[4u8; 32]);
let signature = device.sign(b"reading");
assert!(other.public().verify(b"reading", &signature).is_err());
}
#[test]
fn a_public_identity_round_trips_through_bytes() {
let public = DeviceIdentity::from_seed(&[5u8; 32]).public();
let restored = PublicIdentity::from_bytes(&public.to_bytes()).expect("valid key");
assert_eq!(public, restored);
}
#[test]
fn a_signature_round_trips_through_bytes() {
let device = DeviceIdentity::from_seed(&[6u8; 32]);
let signature = device.sign(b"reading");
let restored = Signature::from_bytes(&signature.to_bytes());
assert!(device.public().verify(b"reading", &restored).is_ok());
}
#[test]
fn the_fingerprint_is_sixteen_hex_characters() {
let public = DeviceIdentity::from_seed(&[7u8; 32]).public();
let fingerprint = public.fingerprint();
assert_eq!(fingerprint.len(), 16);
assert!(fingerprint.chars().all(|c| c.is_ascii_hexdigit()));
}
}