use bytes::Bytes;
use ed25519_dalek::{Signature as EdSignature, Signer as _, SigningKey, VerifyingKey};
use serde::{Deserialize, Serialize};
use crate::codec::to_canonical_bytes;
use crate::error::{Error, SignError};
use crate::objects::{Commit, Operation, Signature};
pub const ALGO_ED25519: &str = "ed25519";
pub struct Signer {
inner: SigningKey,
}
impl Signer {
#[must_use]
pub fn from_seed_bytes(seed: [u8; 32]) -> Self {
Self {
inner: SigningKey::from_bytes(&seed),
}
}
#[must_use]
pub fn public_key_bytes(&self) -> [u8; 32] {
*self.inner.verifying_key().as_bytes()
}
pub fn sign_commit(&self, commit: &mut Commit) -> Result<(), Error> {
let bytes = canonical_bytes_for_commit(commit)?;
let sig = self.inner.sign(&bytes);
commit.signature = Some(signature_from_parts(
self.inner.verifying_key().as_bytes(),
&sig.to_bytes(),
));
Ok(())
}
pub fn sign_operation(&self, op: &mut Operation) -> Result<(), Error> {
let bytes = canonical_bytes_for_operation(op)?;
let sig = self.inner.sign(&bytes);
op.signature = Some(signature_from_parts(
self.inner.verifying_key().as_bytes(),
&sig.to_bytes(),
));
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Revocation {
pub public_key: Bytes,
pub revoked_at: u64,
pub reason: String,
}
#[derive(Debug, Default)]
pub struct Verifier {
revocations: Vec<Revocation>,
}
impl Verifier {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_revocations(revocations: Vec<Revocation>) -> Self {
Self { revocations }
}
pub fn verify_commit(&self, commit: &Commit) -> Result<(), SignError> {
let sig = commit.signature.as_ref().ok_or(SignError::NoSignature)?;
let (vk, ed_sig) = extract_verifier_inputs(sig)?;
let bytes =
canonical_bytes_for_commit(commit).map_err(|e| SignError::Encoding(e.to_string()))?;
vk.verify_strict(&bytes, &ed_sig)
.map_err(|_| SignError::InvalidSignature)?;
self.check_revocation(sig.public_key.as_ref(), commit.time)
}
pub fn verify_operation(&self, op: &Operation) -> Result<(), SignError> {
let sig = op.signature.as_ref().ok_or(SignError::NoSignature)?;
let (vk, ed_sig) = extract_verifier_inputs(sig)?;
let bytes =
canonical_bytes_for_operation(op).map_err(|e| SignError::Encoding(e.to_string()))?;
vk.verify_strict(&bytes, &ed_sig)
.map_err(|_| SignError::InvalidSignature)?;
self.check_revocation(sig.public_key.as_ref(), op.time)
}
fn check_revocation(&self, public_key: &[u8], time: u64) -> Result<(), SignError> {
for rev in &self.revocations {
if rev.public_key.as_ref() == public_key && time > rev.revoked_at {
return Err(SignError::RevokedKey {
revoked_at: rev.revoked_at,
time,
});
}
}
Ok(())
}
}
fn signature_from_parts(public_key: &[u8; 32], sig: &[u8; 64]) -> Signature {
Signature {
algo: ALGO_ED25519.into(),
public_key: Bytes::copy_from_slice(public_key),
sig: Bytes::copy_from_slice(sig),
}
}
fn extract_verifier_inputs(sig: &Signature) -> Result<(VerifyingKey, EdSignature), SignError> {
if sig.algo != ALGO_ED25519 {
return Err(SignError::WrongAlgorithm {
got: sig.algo.clone(),
});
}
let pk_arr: [u8; 32] = sig
.public_key
.as_ref()
.try_into()
.map_err(|_| SignError::MalformedKey)?;
let vk = VerifyingKey::from_bytes(&pk_arr).map_err(|_| SignError::MalformedKey)?;
let sig_arr: [u8; 64] = sig
.sig
.as_ref()
.try_into()
.map_err(|_| SignError::MalformedSignature)?;
let ed_sig = EdSignature::from_bytes(&sig_arr);
Ok((vk, ed_sig))
}
fn canonical_bytes_for_commit(commit: &Commit) -> Result<Vec<u8>, Error> {
let mut c = commit.clone();
c.signature = None;
Ok(to_canonical_bytes(&c)?.to_vec())
}
fn canonical_bytes_for_operation(op: &Operation) -> Result<Vec<u8>, Error> {
let mut o = op.clone();
o.signature = None;
Ok(to_canonical_bytes(&o)?.to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::id::{CODEC_RAW, ChangeId, Cid, Multihash};
fn raw(n: u32) -> Cid {
Cid::new(CODEC_RAW, Multihash::sha2_256(&n.to_be_bytes()))
}
fn sample_commit(time: u64) -> Commit {
Commit::new(
ChangeId::from_bytes_raw([1u8; 16]),
raw(1),
raw(2),
raw(3),
"alice@example.org",
time,
"init",
)
}
fn sample_operation(time: u64) -> Operation {
Operation::new(raw(1), "alice@example.org", time, "commit: init")
}
#[test]
fn sign_then_verify_commit() {
let signer = Signer::from_seed_bytes([0x42u8; 32]);
let mut c = sample_commit(1_000);
signer.sign_commit(&mut c).unwrap();
Verifier::new().verify_commit(&c).unwrap();
}
#[test]
fn sign_then_verify_operation() {
let signer = Signer::from_seed_bytes([0x21u8; 32]);
let mut op = sample_operation(1_000);
signer.sign_operation(&mut op).unwrap();
Verifier::new().verify_operation(&op).unwrap();
}
#[test]
fn verify_with_no_signature_errors() {
let c = sample_commit(1_000);
let err = Verifier::new().verify_commit(&c).unwrap_err();
assert!(matches!(err, SignError::NoSignature));
}
#[test]
fn tampered_commit_fails_verify() {
let signer = Signer::from_seed_bytes([0x42u8; 32]);
let mut c = sample_commit(1_000);
signer.sign_commit(&mut c).unwrap();
c.message = "I am a thief".into();
let err = Verifier::new().verify_commit(&c).unwrap_err();
assert!(matches!(err, SignError::InvalidSignature));
}
#[test]
fn wrong_algorithm_rejected() {
let signer = Signer::from_seed_bytes([0x1u8; 32]);
let mut c = sample_commit(1_000);
signer.sign_commit(&mut c).unwrap();
let sig = c.signature.as_mut().unwrap();
sig.algo = "rsa".into();
let err = Verifier::new().verify_commit(&c).unwrap_err();
assert!(matches!(err, SignError::WrongAlgorithm { .. }));
}
#[test]
fn malformed_key_length_rejected() {
let signer = Signer::from_seed_bytes([0x1u8; 32]);
let mut c = sample_commit(1_000);
signer.sign_commit(&mut c).unwrap();
let sig = c.signature.as_mut().unwrap();
sig.public_key = Bytes::from(vec![0u8; 16]); let err = Verifier::new().verify_commit(&c).unwrap_err();
assert!(matches!(err, SignError::MalformedKey));
}
#[test]
fn revocation_after_commit_time_still_valid() {
let signer = Signer::from_seed_bytes([0x42u8; 32]);
let mut c = sample_commit(1_000);
signer.sign_commit(&mut c).unwrap();
let verifier = Verifier::with_revocations(vec![Revocation {
public_key: Bytes::copy_from_slice(&signer.public_key_bytes()),
revoked_at: 2_000, reason: "rotated".into(),
}]);
verifier.verify_commit(&c).unwrap();
}
#[test]
fn revocation_before_commit_time_rejects() {
let signer = Signer::from_seed_bytes([0x42u8; 32]);
let mut c = sample_commit(1_000);
signer.sign_commit(&mut c).unwrap();
let verifier = Verifier::with_revocations(vec![Revocation {
public_key: Bytes::copy_from_slice(&signer.public_key_bytes()),
revoked_at: 500, reason: "compromised".into(),
}]);
let err = verifier.verify_commit(&c).unwrap_err();
match err {
SignError::RevokedKey { revoked_at, time } => {
assert_eq!(revoked_at, 500);
assert_eq!(time, 1_000);
}
e => panic!("wrong variant: {e:?}"),
}
}
#[test]
fn revocation_equals_commit_time_still_valid() {
let signer = Signer::from_seed_bytes([0x42u8; 32]);
let mut c = sample_commit(1_000);
signer.sign_commit(&mut c).unwrap();
let verifier = Verifier::with_revocations(vec![Revocation {
public_key: Bytes::copy_from_slice(&signer.public_key_bytes()),
revoked_at: 1_000, reason: "rotated".into(),
}]);
verifier.verify_commit(&c).unwrap();
}
#[test]
fn re_signing_is_idempotent() {
let signer = Signer::from_seed_bytes([0x42u8; 32]);
let mut c1 = sample_commit(1_000);
signer.sign_commit(&mut c1).unwrap();
let mut c2 = sample_commit(1_000);
signer.sign_commit(&mut c2).unwrap();
Verifier::new().verify_commit(&c1).unwrap();
Verifier::new().verify_commit(&c2).unwrap();
assert_eq!(c1.signature, c2.signature);
}
}