use super::action::{DagAction, DagActionHash};
use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
pub struct DagSigningKey {
inner: SigningKey,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DagVerifyingKey {
inner: VerifyingKey,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyResult {
pub valid: bool,
pub public_key: String,
pub action_hash: String,
pub detail: String,
}
impl DagSigningKey {
pub fn generate() -> Self {
let mut rng = rand::rng();
let mut seed = [0u8; 32];
rand::RngCore::fill_bytes(&mut rng, &mut seed);
Self {
inner: SigningKey::from_bytes(&seed),
}
}
pub fn from_seed(seed: &[u8; 32]) -> Self {
Self {
inner: SigningKey::from_bytes(seed),
}
}
pub fn seed(&self) -> [u8; 32] {
self.inner.to_bytes()
}
pub fn verifying_key(&self) -> DagVerifyingKey {
DagVerifyingKey {
inner: self.inner.verifying_key(),
}
}
pub fn public_key_bytes(&self) -> [u8; 32] {
self.inner.verifying_key().to_bytes()
}
pub fn public_key_hex(&self) -> String {
self.public_key_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect()
}
pub fn sign(&self, action: &mut DagAction) {
let hash = action.compute_hash();
let sig = self.inner.sign(&hash.0);
action.signature = Some(sig.to_bytes().to_vec());
}
pub fn sign_hash(&self, hash: &DagActionHash) -> Vec<u8> {
self.inner.sign(&hash.0).to_bytes().to_vec()
}
}
impl DagVerifyingKey {
pub fn from_bytes(bytes: &[u8; 32]) -> crate::Result<Self> {
let inner = VerifyingKey::from_bytes(bytes)
.map_err(|e| crate::Error::Config(format!("Invalid Ed25519 public key: {}", e)))?;
Ok(Self { inner })
}
pub fn from_hex(hex: &str) -> crate::Result<Self> {
if hex.len() != 64 {
return Err(crate::Error::Config(
"Public key hex must be 64 characters".into(),
));
}
let mut bytes = [0u8; 32];
for i in 0..32 {
bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)
.map_err(|_| crate::Error::Config("Invalid hex in public key".into()))?;
}
Self::from_bytes(&bytes)
}
pub fn as_bytes(&self) -> [u8; 32] {
self.inner.to_bytes()
}
pub fn to_hex(&self) -> String {
self.as_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect()
}
pub fn verify(&self, action: &DagAction) -> crate::Result<bool> {
let sig_bytes = action
.signature
.as_ref()
.ok_or_else(|| crate::Error::Config("Action has no signature".into()))?;
if sig_bytes.len() != 64 {
return Ok(false);
}
let mut sig_arr = [0u8; 64];
sig_arr.copy_from_slice(sig_bytes);
let signature = ed25519_dalek::Signature::from_bytes(&sig_arr);
let hash = action.compute_hash();
Ok(self.inner.verify(&hash.0, &signature).is_ok())
}
}
pub fn verify_action(action: &DagAction, public_key: &[u8; 32]) -> crate::Result<VerifyResult> {
let vk = DagVerifyingKey::from_bytes(public_key)?;
let hash = action.compute_hash();
let (valid, detail) = match &action.signature {
None => (false, "Action has no signature".into()),
Some(sig) if sig.len() != 64 => (false, format!("Invalid signature length: {}", sig.len())),
Some(_) => match vk.verify(action) {
Ok(true) => (true, "Signature valid".into()),
Ok(false) => (false, "Signature verification failed".into()),
Err(e) => (false, format!("Verification error: {}", e)),
},
};
Ok(VerifyResult {
valid,
public_key: vk.to_hex(),
action_hash: hash.to_hex(),
detail,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dag::{DagPayload, TripleInsertPayload};
use crate::NodeId;
use chrono::Utc;
fn make_unsigned_action(seq: u64) -> DagAction {
DagAction {
parents: vec![],
author: NodeId::named("node:1"),
seq,
timestamp: Utc::now(),
payload: DagPayload::TripleInsert {
triples: vec![TripleInsertPayload {
subject: "alice".into(),
predicate: "knows".into(),
object: serde_json::json!("bob"),
provenance: None,
}],
},
signature: None,
}
}
#[test]
fn test_key_generation() {
let key = DagSigningKey::generate();
let pk = key.public_key_bytes();
assert_eq!(pk.len(), 32);
assert_eq!(key.public_key_hex().len(), 64);
}
#[test]
fn test_deterministic_key() {
let seed = [42u8; 32];
let k1 = DagSigningKey::from_seed(&seed);
let k2 = DagSigningKey::from_seed(&seed);
assert_eq!(k1.public_key_bytes(), k2.public_key_bytes());
}
#[test]
fn test_sign_and_verify() {
let key = DagSigningKey::generate();
let vk = key.verifying_key();
let mut action = make_unsigned_action(1);
assert!(action.signature.is_none());
key.sign(&mut action);
assert!(action.signature.is_some());
assert_eq!(action.signature.as_ref().unwrap().len(), 64);
assert!(vk.verify(&action).unwrap());
}
#[test]
fn test_verify_rejects_tampered_action() {
let key = DagSigningKey::generate();
let vk = key.verifying_key();
let mut action = make_unsigned_action(1);
key.sign(&mut action);
action.seq = 999;
assert!(!vk.verify(&action).unwrap());
}
#[test]
fn test_verify_rejects_wrong_key() {
let key1 = DagSigningKey::generate();
let key2 = DagSigningKey::generate();
let mut action = make_unsigned_action(1);
key1.sign(&mut action);
let vk2 = key2.verifying_key();
assert!(!vk2.verify(&action).unwrap());
}
#[test]
fn test_verify_unsigned_action_returns_error() {
let key = DagSigningKey::generate();
let vk = key.verifying_key();
let action = make_unsigned_action(1);
assert!(vk.verify(&action).is_err());
}
#[test]
fn test_verify_action_convenience() {
let key = DagSigningKey::generate();
let pk = key.public_key_bytes();
let mut action = make_unsigned_action(1);
key.sign(&mut action);
let result = verify_action(&action, &pk).unwrap();
assert!(result.valid);
assert_eq!(result.detail, "Signature valid");
}
#[test]
fn test_verify_action_no_signature() {
let key = DagSigningKey::generate();
let pk = key.public_key_bytes();
let action = make_unsigned_action(1);
let result = verify_action(&action, &pk).unwrap();
assert!(!result.valid);
assert_eq!(result.detail, "Action has no signature");
}
#[test]
fn test_signature_excluded_from_hash() {
let key = DagSigningKey::generate();
let mut action = make_unsigned_action(1);
let hash_before = action.compute_hash();
key.sign(&mut action);
let hash_after = action.compute_hash();
assert_eq!(hash_before, hash_after);
}
#[test]
fn test_verifying_key_hex_roundtrip() {
let key = DagSigningKey::generate();
let vk = key.verifying_key();
let hex = vk.to_hex();
let restored = DagVerifyingKey::from_hex(&hex).unwrap();
assert_eq!(vk, restored);
}
#[test]
fn test_sign_hash_matches_sign() {
let key = DagSigningKey::generate();
let vk = key.verifying_key();
let mut action = make_unsigned_action(1);
let hash = action.compute_hash();
let sig_bytes = key.sign_hash(&hash);
action.signature = Some(sig_bytes);
assert!(vk.verify(&action).unwrap());
}
#[test]
fn test_verifying_key_from_bytes_invalid() {
let bad_bytes = [0u8; 32]; let _ = DagVerifyingKey::from_bytes(&bad_bytes);
}
}