use crate::base::hash::{hex_decode, hex_encode};
use crate::record::author::Signature;
use crate::record::record::Record;
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
pub type PublicKeyBytes = [u8; 32];
pub struct Ed25519Signer {
signing_key: SigningKey,
}
impl Ed25519Signer {
pub fn from_secret_bytes(secret: &[u8; 32]) -> Self {
Self {
signing_key: SigningKey::from_bytes(secret),
}
}
pub fn public_key(&self) -> PublicKeyBytes {
self.signing_key.verifying_key().to_bytes()
}
pub fn public_key_hex(&self) -> String {
hex_encode(&self.public_key())
}
pub fn sign(&self, record: &Record) -> Result<Signature, serde_json::Error> {
let canonical = record.signing_bytes()?;
let sig = self.signing_key.sign(&canonical);
Ok(Signature {
key_id: self.public_key_hex(),
sig: sig.to_bytes().to_vec(),
})
}
}
pub fn signature_verifies(record: &Record) -> bool {
verified_key(record).is_some()
}
pub fn verified_key(record: &Record) -> Option<PublicKeyBytes> {
let sig = record.author.signature.as_ref()?;
let key_bytes = hex_decode(&sig.key_id)?;
let verifying_key = VerifyingKey::from_bytes(&key_bytes).ok()?;
let sig_bytes: [u8; 64] = sig.sig.as_slice().try_into().ok()?;
let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes);
let canonical = record.signing_bytes().ok()?;
verifying_key
.verify_strict(&canonical, &signature)
.ok()
.map(|_| key_bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::schema::{schema_id, SCHEMA_REQUEST};
use crate::record::author::Author;
use crate::record::evidence::Evidence;
use crate::record::kind::{AuthorType, Kind};
fn test_record() -> Record {
Record {
id: [0u8; 32],
space: [1u8; 32],
thread: [2u8; 32],
time: 1,
author: Author {
id: "human".into(),
type_: AuthorType::User,
signature: None,
},
kind: Kind::Request,
schema: schema_id(SCHEMA_REQUEST),
data: b"{}".to_vec(),
refs: vec![],
evidence: Evidence::Reported,
}
.with_computed_id()
.unwrap()
}
#[test]
fn test_sign_and_verify_roundtrip() {
let signer = Ed25519Signer::from_secret_bytes(&[7u8; 32]);
let mut record = test_record();
let unsigned_id = record.id;
record.author.signature = Some(signer.sign(&record).unwrap());
record = record.with_computed_id().unwrap();
assert_ne!(record.id, unsigned_id);
assert_eq!(record.compute_id().unwrap(), record.id);
assert!(signature_verifies(&record));
assert_eq!(verified_key(&record), Some(signer.public_key()));
}
#[test]
fn test_tampered_content_fails() {
let signer = Ed25519Signer::from_secret_bytes(&[7u8; 32]);
let mut record = test_record();
record.author.signature = Some(signer.sign(&record).unwrap());
record.data = b"{\"altered\":true}".to_vec();
assert!(!signature_verifies(&record));
}
#[test]
fn test_cross_domain_signatures_fail() {
let signer = Ed25519Signer::from_secret_bytes(&[7u8; 32]);
let mut record = test_record();
let current: serde_json::Value =
serde_json::from_slice(&record.signing_bytes().unwrap()).unwrap();
let legacy = crate::base::canonical::canonical_json(¤t["record"]).unwrap();
let legacy_sig = signer.signing_key.sign(&legacy);
record.author.signature = Some(Signature {
key_id: signer.public_key_hex(),
sig: legacy_sig.to_bytes().to_vec(),
});
assert!(!signature_verifies(&record));
let mut future = current;
future["domain"] = serde_json::Value::String("bellbook.record-signature.v0.3".into());
let future_bytes = crate::base::canonical::canonical_json(&future).unwrap();
let future_sig = signer.signing_key.sign(&future_bytes);
record.author.signature = Some(Signature {
key_id: signer.public_key_hex(),
sig: future_sig.to_bytes().to_vec(),
});
assert!(!signature_verifies(&record));
}
#[test]
fn test_malformed_signature_fails() {
let signer = Ed25519Signer::from_secret_bytes(&[7u8; 32]);
let mut record = test_record();
let mut sig = signer.sign(&record).unwrap();
sig.sig[0] ^= 0xff;
record.author.signature = Some(sig);
assert!(!signature_verifies(&record));
let mut sig2 = signer.sign(&record).unwrap();
sig2.key_id = "not-hex".into();
record.author.signature = Some(sig2);
assert!(!signature_verifies(&record));
record.author.signature = None;
assert!(!signature_verifies(&record));
}
}