use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use wm_core::Galaxy;
use wm_core::kdf::{RECORD_ATTESTATION_INFO, hkdf32};
use crate::memory::MemoryId;
pub const ATTESTATION_DOMAIN: &str = "wm-record-attestation/v1";
pub const ATTESTATIONS_DB: &str = "attestations";
pub const ATTESTATION_KEY_ENV: &str = "WM_MESH_KEY";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RecordAttestation {
pub domain: String,
pub galaxy: String,
pub memory_id: String,
pub record_hash: String,
pub agent_id: String,
pub timestamp: u64,
pub public_key_hex: String,
pub signature_hex: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttestationReport {
pub attested: bool,
pub signature_valid: bool,
pub matches_head: bool,
pub memory_present: bool,
pub breaks: Vec<String>,
}
#[must_use]
pub fn attestation_payload(
galaxy: &str,
memory_id: &str,
record_hash: &str,
agent_id: &str,
timestamp: u64,
) -> String {
format!("{ATTESTATION_DOMAIN}|{galaxy}|{memory_id}|{record_hash}|{agent_id}|{timestamp}")
}
#[must_use]
pub fn attestation_key(galaxy: Galaxy, id: MemoryId) -> Vec<u8> {
format!("att:{}:{}", galaxy.db_name(), id).into_bytes()
}
#[must_use]
pub fn attestation_prefix() -> Vec<u8> {
b"att:".to_vec()
}
#[must_use]
pub fn sha256_hex(s: &str) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let digest = Sha256::digest(s.as_bytes());
digest.iter().fold(String::with_capacity(64), |mut out, b| {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
out
})
}
fn decode_key_hex(hex: &str) -> Option<[u8; 32]> {
if hex.len() != 64 {
return None;
}
let bytes = hex.as_bytes();
let mut out = [0u8; 32];
for (i, chunk) in bytes.chunks_exact(2).enumerate() {
let hi = hex_val(chunk[0])?;
let lo = hex_val(chunk[1])?;
out[i] = (hi << 4) | lo;
}
Some(out)
}
const fn hex_val(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[must_use]
pub fn sign_attestation(payload: &str, secret_hex: &str) -> Option<(String, String)> {
let secret = decode_key_hex(secret_hex.trim())?;
let signing = SigningKey::from_bytes(&secret);
let sig = signing.sign(payload.as_bytes());
Some((
hex_of(&signing.verifying_key().to_bytes()),
hex_of(&sig.to_bytes()),
))
}
#[must_use]
pub fn derive_attestation_key_hex(root_hex: &str) -> Option<String> {
let root = decode_key_hex(root_hex.trim())?;
Some(hex_of(&hkdf32(&root, RECORD_ATTESTATION_INFO)))
}
#[must_use]
pub fn sign_attestation_from_root(payload: &str, root_hex: &str) -> Option<(String, String)> {
let key = derive_attestation_key_hex(root_hex)?;
sign_attestation(payload, &key)
}
fn hex_of(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
out
}
#[must_use]
pub fn verify_attestation(att: &RecordAttestation) -> bool {
if att.domain != ATTESTATION_DOMAIN {
return false;
}
let payload = attestation_payload(
&att.galaxy,
&att.memory_id,
&att.record_hash,
&att.agent_id,
att.timestamp,
);
let (Some(pk_bytes), Some(sig_bytes)) = (
decode_pubkey(&att.public_key_hex),
decode_sig(&att.signature_hex),
) else {
return false;
};
let Ok(pk) = VerifyingKey::from_bytes(&pk_bytes) else {
return false;
};
let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
pk.verify(payload.as_bytes(), &sig).is_ok()
}
fn decode_pubkey(hex: &str) -> Option<[u8; 32]> {
decode_key_hex(hex)
}
fn decode_sig(hex: &str) -> Option<[u8; 64]> {
if hex.len() != 128 {
return None;
}
let bytes = hex.as_bytes();
let mut out = [0u8; 64];
for (i, chunk) in bytes.chunks_exact(2).enumerate() {
out[i] = (hex_val(chunk[0])? << 4) | hex_val(chunk[1])?;
}
Some(out)
}
#[must_use]
pub fn merkle_root_hex(leaves: &[String]) -> String {
if leaves.is_empty() {
return sha256_hex("");
}
let mut layer: Vec<String> = leaves.to_vec();
while layer.len() > 1 {
if layer.len() % 2 != 0 {
let last = layer.last().cloned().unwrap_or_default();
layer.push(last);
}
let mut next = Vec::with_capacity(layer.len() / 2);
for pair in layer.chunks(2) {
next.push(sha256_hex(&format!("{}{}", pair[0], pair[1])));
}
layer = next;
}
layer.into_iter().next().unwrap_or_default()
}
#[must_use]
pub fn anchor_leaf_input(record_hash: &str, signature_hex: &str) -> String {
sha256_hex(&format!("{record_hash}|{signature_hex}"))
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_KEY: &str = "0bd1c44170ca3d916648a983dcdb8583d22f2da5b29fdd5ede4b38e805435577";
fn test_attestation() -> RecordAttestation {
let payload = attestation_payload("codex", "mem-1", "hash-1", "ses-1", 1_700_000_000);
let (pk, sig) = sign_attestation(&payload, TEST_KEY).unwrap();
RecordAttestation {
domain: ATTESTATION_DOMAIN.to_string(),
galaxy: "codex".to_string(),
memory_id: "mem-1".to_string(),
record_hash: "hash-1".to_string(),
agent_id: "ses-1".to_string(),
timestamp: 1_700_000_000,
public_key_hex: pk,
signature_hex: sig,
}
}
#[test]
fn payload_is_domain_prefixed_and_deterministic() {
let a = attestation_payload("codex", "m", "h", "a", 1);
let b = attestation_payload("codex", "m", "h", "a", 1);
assert_eq!(a, b);
assert!(a.starts_with("wm-record-attestation/v1|"));
assert_ne!(a, attestation_payload("codex", "m", "h", "a", 2));
}
#[test]
fn hkdf_attestation_subkey_is_domain_separated_and_verifies() {
let derived = derive_attestation_key_hex(TEST_KEY).expect("valid root material");
assert_eq!(derived.len(), 64);
assert_eq!(derive_attestation_key_hex(TEST_KEY).unwrap(), derived);
let payload = attestation_payload("codex", "mem-1", "hash-1", "ses-1", 1_700_000_000);
let (pk, sig) = sign_attestation_from_root(&payload, TEST_KEY).unwrap();
assert_eq!(
sign_attestation_from_root(&payload, TEST_KEY).unwrap(),
(pk.clone(), sig.clone())
);
assert_ne!(pk, sign_attestation(&payload, TEST_KEY).unwrap().0);
let mut att = test_attestation();
att.public_key_hex = pk;
att.signature_hex = sig;
assert!(
verify_attestation(&att),
"a derived-lineage attestation must verify against its recorded pubkey"
);
}
#[test]
fn attestation_key_follows_the_canonical_root_convention() {
let canonical = derive_attestation_key_hex(TEST_KEY).unwrap();
let raw_lineage = hex_of(&hkdf32(TEST_KEY.as_bytes(), RECORD_ATTESTATION_INFO));
assert_ne!(
canonical, raw_lineage,
"64-hex root material must be hex-decoded, not read as ASCII"
);
assert!(derive_attestation_key_hex("short-legacy-test-key").is_none());
}
#[test]
fn sign_and_verify_roundtrip() {
assert!(verify_attestation(&test_attestation()));
}
#[test]
fn tampered_record_hash_rejected() {
let mut att = test_attestation();
att.record_hash = "forged".to_string();
assert!(!verify_attestation(&att));
}
#[test]
fn wrong_domain_rejected() {
let mut att = test_attestation();
att.domain = "mesh-heartbeat".to_string();
assert!(!verify_attestation(&att));
}
#[test]
fn bad_key_material_returns_none() {
let payload = attestation_payload("codex", "m", "h", "a", 1);
assert!(sign_attestation(&payload, "zz").is_none());
assert!(sign_attestation(&payload, "abcd").is_none());
assert!(sign_attestation(&payload, "").is_none());
}
#[test]
fn merkle_root_matches_karma_convention() {
assert_eq!(
merkle_root_hex(&[]),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(merkle_root_hex(&["abc".to_string()]), "abc");
assert_eq!(
merkle_root_hex(&["a".to_string(), "b".to_string()]),
sha256_hex("ab")
);
let three = merkle_root_hex(&["a".to_string(), "b".to_string(), "c".to_string()]);
let level1 = [sha256_hex("ab"), sha256_hex("cc")];
assert_eq!(three, sha256_hex(&format!("{}{}", level1[0], level1[1])));
assert_eq!(
merkle_root_hex(&["x".to_string(), "y".to_string()]),
merkle_root_hex(&["x".to_string(), "y".to_string()])
);
}
#[test]
fn keys_are_structured() {
let id = MemoryId::nil();
let key = String::from_utf8(attestation_key(Galaxy::Codex, id)).unwrap();
assert!(key.starts_with("att:codex:"));
assert_eq!(String::from_utf8(attestation_prefix()).unwrap(), "att:");
}
}