use std::fmt;
use std::future::Future;
use std::pin::Pin;
use canton_core::{Error, Result};
use ring::signature::{Ed25519KeyPair, KeyPair as _};
use crate::{KeyFormat, KeySpec, PublicKey, Signature, SignatureFormat, Signer, SigningAlgorithm};
const SEED_LEN: usize = 32;
pub struct Ed25519Key {
pair: Ed25519KeyPair,
}
impl fmt::Debug for Ed25519Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Ed25519Key").finish_non_exhaustive()
}
}
impl Ed25519Key {
pub fn generate() -> Result<Self> {
let rng = ring::rand::SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).map_err(|_| {
Error::InvalidRequest("the platform random source is unavailable".to_string())
})?;
Self::from_pkcs8(pkcs8.as_ref())
}
pub fn from_seed(seed: &[u8]) -> Result<Self> {
if seed.len() != SEED_LEN {
return Err(Error::InvalidRequest(format!(
"an Ed25519 seed is {SEED_LEN} bytes, got {}",
seed.len()
)));
}
Ed25519KeyPair::from_seed_unchecked(seed)
.map(|pair| Self { pair })
.map_err(|_| Error::InvalidRequest("not a valid Ed25519 seed".to_string()))
}
pub fn from_pkcs8(document: &[u8]) -> Result<Self> {
Ed25519KeyPair::from_pkcs8(document)
.map(|pair| Self { pair })
.map_err(|_| Error::InvalidRequest("not a PKCS#8 Ed25519 key pair".to_string()))
}
pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
let path = path.as_ref();
let bytes = std::fs::read(path).map_err(|e| {
Error::InvalidRequest(format!("cannot read the key at {}: {e}", path.display()))
})?;
if bytes.len() == SEED_LEN {
Self::from_seed(&bytes)
} else {
Self::from_pkcs8(&bytes)
}
}
#[must_use]
pub fn public_key(&self) -> PublicKey {
PublicKey::new(
KeyFormat::Raw,
self.pair.public_key().as_ref().to_vec(),
KeySpec::EcCurve25519,
)
}
#[must_use]
pub fn into_signer(self, fingerprint: impl Into<String>) -> Ed25519Signer {
Ed25519Signer {
key: self,
fingerprint: fingerprint.into(),
}
}
#[must_use]
pub fn sign_raw(&self, hash: &[u8]) -> Vec<u8> {
self.pair.sign(hash).as_ref().to_vec()
}
}
pub struct Ed25519Signer {
key: Ed25519Key,
fingerprint: String,
}
impl fmt::Debug for Ed25519Signer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Ed25519Signer")
.field("fingerprint", &self.fingerprint)
.finish_non_exhaustive()
}
}
impl Ed25519Signer {
#[must_use]
pub fn key(&self) -> &Ed25519Key {
&self.key
}
}
impl Signer for Ed25519Signer {
fn public_key(&self) -> PublicKey {
self.key.public_key()
}
fn fingerprint(&self) -> &str {
&self.fingerprint
}
fn sign(&self, hash: &[u8]) -> Pin<Box<dyn Future<Output = Result<Signature>> + Send + '_>> {
let signature = Signature::new(
SignatureFormat::Concat,
self.key.sign_raw(hash),
self.fingerprint.clone(),
SigningAlgorithm::Ed25519,
);
Box::pin(std::future::ready(signature))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
const SEED: [u8; SEED_LEN] = [9; SEED_LEN];
struct Cleanup(std::path::PathBuf);
impl Drop for Cleanup {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).ok();
}
}
#[test]
fn a_seed_gives_the_same_key_every_time() {
let a = Ed25519Key::from_seed(&SEED).expect("valid seed");
let b = Ed25519Key::from_seed(&SEED).expect("valid seed");
assert_eq!(a.public_key(), b.public_key());
assert_eq!(a.sign_raw(b"hash"), b.sign_raw(b"hash"));
}
#[test]
fn a_generated_key_is_not_the_same_as_another() {
let a = Ed25519Key::generate().expect("a random source");
let b = Ed25519Key::generate().expect("a random source");
assert_ne!(a.public_key(), b.public_key());
}
#[test]
fn the_signature_verifies_against_the_public_key_that_is_registered() {
let key = Ed25519Key::from_seed(&SEED).expect("valid seed");
let public = key.public_key();
let signature = key.sign_raw(b"a prepared transaction hash");
ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, public.data())
.verify(b"a prepared transaction hash", &signature)
.expect("the signature must verify against the registered key");
}
#[test]
fn a_signature_over_another_hash_does_not_verify() {
let key = Ed25519Key::from_seed(&SEED).expect("valid seed");
let signature = key.sign_raw(b"one hash");
assert!(
ring::signature::UnparsedPublicKey::new(
&ring::signature::ED25519,
key.public_key().data()
)
.verify(b"another hash", &signature)
.is_err()
);
}
#[test]
fn the_public_key_is_raw_and_thirty_two_bytes() {
let key = Ed25519Key::from_seed(&SEED).expect("valid seed");
let public = key.public_key();
assert_eq!(public.format(), KeyFormat::Raw);
assert_eq!(public.spec(), KeySpec::EcCurve25519);
assert_eq!(public.data().len(), SEED_LEN);
}
#[tokio::test]
async fn a_ledger_signature_declares_concat_and_ed25519() {
let signer = Ed25519Key::from_seed(&SEED)
.expect("valid seed")
.into_signer("1220deadbeef");
let signature = signer.sign(b"hash").await.expect("in-memory signing");
assert_eq!(signature.format(), SignatureFormat::Concat);
assert_eq!(signature.algorithm(), SigningAlgorithm::Ed25519);
assert_eq!(signature.bytes().len(), 64);
assert_eq!(signature.signed_by(), "1220deadbeef");
}
#[tokio::test]
async fn the_hash_is_signed_as_given_and_not_hashed_again() {
let key = Ed25519Key::from_seed(&SEED).expect("valid seed");
let expected = key.sign_raw(b"the participant's hash");
let signer = key.into_signer("1220deadbeef");
let signature = signer.sign(b"the participant's hash").await.expect("sign");
assert_eq!(signature.bytes(), expected.as_slice());
}
#[test]
fn a_seed_of_the_wrong_length_is_refused() {
let err = Ed25519Key::from_seed(&[1, 2, 3]).expect_err("too short");
assert!(
err.to_string().contains("32 bytes"),
"the error should say what was expected: {err}"
);
}
#[test]
fn a_file_holding_either_form_is_read() {
let dir = std::env::temp_dir().join(format!(
"canton-signer-key-forms-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&dir).expect("temp dir");
let _cleanup = Cleanup(dir.clone());
let seed_path = dir.join("seed.bin");
std::fs::write(&seed_path, SEED).expect("write");
let from_seed = Ed25519Key::from_file(&seed_path).expect("a raw seed is read");
assert_eq!(
from_seed.public_key(),
Ed25519Key::from_seed(&SEED).expect("seed").public_key()
);
let rng = ring::rand::SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).expect("generate");
let pkcs8_path = dir.join("key.pk8");
std::fs::write(&pkcs8_path, pkcs8.as_ref()).expect("write");
let from_pkcs8 = Ed25519Key::from_file(&pkcs8_path).expect("a PKCS#8 document is read");
assert_eq!(
from_pkcs8.public_key(),
Ed25519Key::from_pkcs8(pkcs8.as_ref())
.expect("pkcs8")
.public_key()
);
}
#[test]
fn debug_does_not_print_the_key() {
let key = Ed25519Key::from_seed(&SEED).expect("valid seed");
let hex = |bytes: &[u8]| -> String {
use std::fmt::Write as _;
bytes.iter().fold(String::new(), |mut out, b| {
let _ = write!(out, "{b:02x}");
out
})
};
let seed_hex = hex(&SEED);
let public_hex = hex(key.public_key().data());
let rendered = format!("{key:?}");
assert!(!rendered.contains(&seed_hex), "{rendered}");
assert!(!rendered.contains(&public_hex), "{rendered}");
let signer = key.into_signer("1220deadbeef");
let rendered = format!("{signer:?}");
assert!(!rendered.contains(&seed_hex), "{rendered}");
assert!(
rendered.contains("1220deadbeef"),
"the fingerprint is public and identifies the signer: {rendered}"
);
}
#[test]
fn debug_names_the_type_and_hides_everything_else() {
let key = Ed25519Key::from_seed(&[7; 32]).expect("a valid seed");
assert_eq!(format!("{key:?}"), "Ed25519Key { .. }");
}
#[test]
fn the_signer_reports_the_fingerprint_it_was_given() {
let signer = Ed25519Key::from_seed(&[7; 32])
.expect("a valid seed")
.into_signer("1220cafe");
assert_eq!(signer.fingerprint(), "1220cafe");
}
}