use ed25519_dalek::{Signer, Verifier};
use sha2::{Digest, Sha256};
use crate::merkle::{ConsistencyProof, TreeHead};
use super::{Accepted, KeyName, NoteSignature, Origin, Witness, WitnessError, WitnessStore};
const ALG_ED25519: u8 = 0x01;
const ALG_COSIGNATURE: u8 = 0x04;
const SIG_LEN: usize = 64;
const TIME_LEN: usize = 8;
fn key_hash(name: &KeyName, algorithm: u8, public: &[u8; 32]) -> [u8; 4] {
let mut hasher = Sha256::new();
hasher.update(name.as_str().as_bytes());
hasher.update(b"\n");
hasher.update([algorithm]);
hasher.update(public);
let digest = hasher.finalize();
let mut out = [0u8; 4];
if let Some(prefix) = digest.get(..4) {
out.copy_from_slice(prefix);
}
out
}
fn cosignature_message(body: &str, at: u64) -> String {
format!("cosignature/v1\ntime {at}\n{body}")
}
#[derive(Debug)]
pub struct SigningKey {
name: KeyName,
inner: ed25519_dalek::SigningKey,
}
impl SigningKey {
#[must_use]
pub fn from_seed(name: KeyName, seed: [u8; 32]) -> Self {
Self {
name,
inner: ed25519_dalek::SigningKey::from_bytes(&seed),
}
}
#[must_use]
pub fn name(&self) -> &KeyName {
&self.name
}
#[must_use]
pub fn verifying_key(&self) -> VerifyingKey {
VerifyingKey {
name: self.name.clone(),
inner: self.inner.verifying_key(),
}
}
#[must_use]
pub fn sign(&self, body: &str) -> NoteSignature {
let public = self.inner.verifying_key().to_bytes();
NoteSignature {
name: self.name.clone(),
key_hash: key_hash(&self.name, ALG_ED25519, &public),
value: self.inner.sign(body.as_bytes()).to_bytes().to_vec(),
}
}
#[must_use]
pub fn cosign_body(&self, body: &str, at: u64) -> NoteSignature {
let public = self.inner.verifying_key().to_bytes();
let message = cosignature_message(body, at);
let mut value = Vec::with_capacity(TIME_LEN.saturating_add(SIG_LEN));
value.extend_from_slice(&at.to_be_bytes());
value.extend_from_slice(&self.inner.sign(message.as_bytes()).to_bytes());
NoteSignature {
name: self.name.clone(),
key_hash: key_hash(&self.name, ALG_COSIGNATURE, &public),
value,
}
}
}
#[derive(Debug, Clone)]
pub struct VerifyingKey {
name: KeyName,
inner: ed25519_dalek::VerifyingKey,
}
impl VerifyingKey {
pub fn from_bytes(name: KeyName, bytes: [u8; 32]) -> Result<Self, BadKey> {
Ok(Self {
name,
inner: ed25519_dalek::VerifyingKey::from_bytes(&bytes).map_err(|_| BadKey)?,
})
}
#[must_use]
pub fn name(&self) -> &KeyName {
&self.name
}
#[must_use]
pub fn to_bytes(&self) -> [u8; 32] {
self.inner.to_bytes()
}
#[must_use]
pub fn note_key_hash(&self) -> [u8; 4] {
key_hash(&self.name, ALG_ED25519, &self.inner.to_bytes())
}
#[must_use]
pub fn cosignature_key_hash(&self) -> [u8; 4] {
key_hash(&self.name, ALG_COSIGNATURE, &self.inner.to_bytes())
}
#[must_use]
pub fn verify(&self, body: &str, signature: &NoteSignature) -> bool {
if signature.name != self.name || signature.key_hash != self.note_key_hash() {
return false;
}
let Some(bytes) = signature
.value
.get(..SIG_LEN)
.filter(|_| signature.value.len() == SIG_LEN)
else {
return false;
};
let Ok(array) = <[u8; SIG_LEN]>::try_from(bytes) else {
return false;
};
self.inner
.verify(
body.as_bytes(),
&ed25519_dalek::Signature::from_bytes(&array),
)
.is_ok()
}
#[must_use]
pub fn verify_cosignature(&self, body: &str, signature: &NoteSignature) -> Option<u64> {
if signature.name != self.name || signature.key_hash != self.cosignature_key_hash() {
return None;
}
if signature.value.len() != TIME_LEN.saturating_add(SIG_LEN) {
return None;
}
let at = u64::from_be_bytes(
signature
.value
.get(..TIME_LEN)
.and_then(|s| <[u8; TIME_LEN]>::try_from(s).ok())?,
);
let array = signature
.value
.get(TIME_LEN..)
.and_then(|s| <[u8; SIG_LEN]>::try_from(s).ok())?;
let message = cosignature_message(body, at);
self.inner
.verify(
message.as_bytes(),
&ed25519_dalek::Signature::from_bytes(&array),
)
.ok()
.map(|()| at)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("not a valid Ed25519 public key")]
pub struct BadKey;
#[derive(Debug)]
pub struct Cosigner<S> {
witness: Witness<S>,
key: SigningKey,
}
impl<S: WitnessStore> Cosigner<S> {
#[must_use]
pub fn new(witness: Witness<S>, key: SigningKey) -> Self {
Self { witness, key }
}
#[must_use]
pub fn witness(&self) -> &Witness<S> {
&self.witness
}
pub fn witness_mut(&mut self) -> &mut Witness<S> {
&mut self.witness
}
#[must_use]
pub fn verifying_key(&self) -> VerifyingKey {
self.key.verifying_key()
}
pub fn cosign(
&mut self,
origin: &Origin,
head: TreeHead,
proof: Option<&ConsistencyProof>,
at: u64,
) -> Result<NoteSignature, WitnessError> {
let accepted: Accepted = self.witness.offer(origin, head, proof)?;
let body = super::SignedTreeHead::new(origin.clone(), accepted.head).body();
Ok(self.key.cosign_body(&body, at))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::Hash;
use crate::merkle::MerkleLog;
use crate::witness::{MemoryWitnessStore, SignedTreeHead};
fn origin() -> Origin {
Origin::new("example.com/ledgers/acme").expect("valid")
}
fn name(s: &str) -> KeyName {
KeyName::new(s).expect("valid")
}
fn log_of(n: u64) -> MerkleLog {
let mut log = MerkleLog::new();
for i in 0..n {
let mut bytes = [0u8; 32];
bytes
.get_mut(..8)
.expect("32 > 8")
.copy_from_slice(&i.to_le_bytes());
log.append(Hash::from_bytes(bytes));
}
log
}
fn cosigner_at(log: &MerkleLog, size: u64) -> Cosigner<MemoryWitnessStore> {
let mut witness = Witness::new(MemoryWitnessStore::new());
witness
.trust(origin(), log.head_at(size).expect("in range"))
.expect("first time");
Cosigner::new(
witness,
SigningKey::from_seed(name("witness.example"), [7; 32]),
)
}
#[test]
fn a_cosignature_verifies_and_carries_a_checked_timestamp() {
let log = log_of(8);
let mut cosigner = cosigner_at(&log, 3);
let public = cosigner.verifying_key();
let proof = log.consistency_proof_between(3, 8).expect("in range");
let signature = cosigner
.cosign(&origin(), log.head(), Some(&proof), 1_800_000_000)
.expect("extends");
let body = SignedTreeHead::new(origin(), log.head()).body();
assert_eq!(
public.verify_cosignature(&body, &signature),
Some(1_800_000_000)
);
}
#[test]
fn a_refused_head_produces_no_signature_at_all() {
let log = log_of(8);
let mut cosigner = cosigner_at(&log, 8);
let mut forked = log_of(7);
forked.append(Hash::from_bytes([0xff; 32]));
assert!(matches!(
cosigner.cosign(&origin(), forked.head(), None, 1_800_000_000),
Err(WitnessError::Fork { size: 8, .. })
));
}
#[test]
fn a_cosignature_over_a_different_head_does_not_verify() {
let log = log_of(8);
let mut cosigner = cosigner_at(&log, 3);
let public = cosigner.verifying_key();
let proof = log.consistency_proof_between(3, 8).expect("in range");
let signature = cosigner
.cosign(&origin(), log.head(), Some(&proof), 1_800_000_000)
.expect("extends");
let other = SignedTreeHead::new(origin(), log.head_at(3).expect("in range")).body();
assert_eq!(public.verify_cosignature(&other, &signature), None);
}
#[test]
fn a_tampered_timestamp_fails_the_signature_rather_than_being_returned() {
let log = log_of(8);
let mut cosigner = cosigner_at(&log, 3);
let public = cosigner.verifying_key();
let proof = log.consistency_proof_between(3, 8).expect("in range");
let mut signature = cosigner
.cosign(&origin(), log.head(), Some(&proof), 1_800_000_000)
.expect("extends");
let body = SignedTreeHead::new(origin(), log.head()).body();
signature.value.splice(..8, 1_831_536_000u64.to_be_bytes());
assert_eq!(
public.verify_cosignature(&body, &signature),
None,
"the timestamp is inside the signed message, not beside it"
);
}
#[test]
fn a_note_signature_cannot_be_presented_as_a_cosignature() {
let key = SigningKey::from_seed(name("operator.example"), [3; 32]);
let public = key.verifying_key();
let body = SignedTreeHead::new(origin(), log_of(8).head()).body();
let note_sig = key.sign(&body);
assert!(public.verify(&body, ¬e_sig));
assert_eq!(
public.verify_cosignature(&body, ¬e_sig),
None,
"the algorithm byte is inside the key hash, so the selector differs"
);
}
#[test]
fn another_key_of_the_same_name_does_not_verify() {
let body = SignedTreeHead::new(origin(), log_of(8).head()).body();
let mine = SigningKey::from_seed(name("witness.example"), [1; 32]);
let theirs = SigningKey::from_seed(name("witness.example"), [2; 32]);
assert!(!mine.verifying_key().verify(&body, &theirs.sign(&body)));
}
#[test]
fn a_signed_note_round_trips_through_the_wire_format() {
let log = log_of(8);
let key = SigningKey::from_seed(name("operator.example"), [9; 32]);
let public = key.verifying_key();
let mut sth = SignedTreeHead::new(origin(), log.head());
let body = sth.body();
sth.add_signature(key.sign(&body));
let text = sth.to_note();
let parsed = SignedTreeHead::parse(&text).expect("parses");
let signature = parsed.signatures.first().expect("one signature");
assert!(
public.verify(&parsed.body(), signature),
"a signature has to survive the text it travels in"
);
assert!(parsed.has_signature_from(public.name(), public.note_key_hash()));
}
#[test]
fn a_key_hash_is_the_documented_construction() {
let key = SigningKey::from_seed(name("witness.example"), [7; 32]);
let public = key.verifying_key();
let mut hasher = Sha256::new();
hasher.update(b"witness.example\n");
hasher.update([ALG_COSIGNATURE]);
hasher.update(public.to_bytes());
let digest = hasher.finalize();
assert_eq!(
public.cosignature_key_hash().as_slice(),
digest.get(..4).expect("32 > 4")
);
}
}