use anyhow::Result;
use fips204::traits::{SerDes, Signer, Verifier};
use saorsa_pqc::ml_dsa_65::{PrivateKey, PublicKey};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;
use tracing::{debug, warn};
use super::sites::SiteId;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NameRecord {
pub version: u8,
pub four_words: String,
pub site_id: SiteId,
pub public_key: Vec<u8>,
pub created_at: u64,
pub nonce: [u8; 32],
pub signature: Vec<u8>,
}
impl NameRecord {
pub fn new(four_words: String, public_key: &PublicKey) -> Self {
let site_id = SiteId::from_public_key(public_key);
let created_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_millis() as u64;
let mut nonce = [0u8; 32];
if getrandom::getrandom(&mut nonce).is_err() {
tracing::warn!("Failed to generate random nonce, using zeros");
}
Self {
version: 1,
four_words,
site_id,
public_key: public_key.clone().into_bytes().to_vec(),
created_at,
nonce,
signature: vec![],
}
}
fn to_sign_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.push(self.version);
bytes.extend_from_slice(self.four_words.as_bytes());
bytes.extend_from_slice(&self.site_id.hash);
bytes.extend_from_slice(&self.public_key);
bytes.extend_from_slice(&self.created_at.to_le_bytes());
bytes.extend_from_slice(&self.nonce);
bytes
}
pub fn sign(&mut self, signing_key: &PrivateKey) -> Result<()> {
let message = self.to_sign_bytes();
let signature = signing_key
.try_sign(&message, &[])
.map_err(|e| anyhow::anyhow!("ML-DSA-65 signing failed: {}", e))?;
self.signature = signature.to_vec();
Ok(())
}
pub fn verify(&self) -> Result<()> {
if self.public_key.len() != 1952 {
anyhow::bail!("Invalid public key size: {}", self.public_key.len());
}
let pk_hash = blake3::hash(&self.public_key);
if pk_hash.as_bytes() != &self.site_id.hash {
anyhow::bail!("Public key does not match site_id");
}
let pk_array: [u8; 1952] = self
.public_key
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Public key is not 1952 bytes"))?;
let public_key = PublicKey::try_from_bytes(pk_array)
.map_err(|e| anyhow::anyhow!("Invalid public key: {}", e))?;
if self.signature.len() != 3309 {
anyhow::bail!("Invalid signature size: {}", self.signature.len());
}
let sig_array: [u8; 3309] = self
.signature
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Failed to convert signature to array"))?;
let message = self.to_sign_bytes();
if !public_key.verify(&message, &sig_array, &[]) {
anyhow::bail!("Signature verification failed");
}
Ok(())
}
}
pub struct NameRegistry {
records: Arc<RwLock<HashMap<String, NameRecord>>>,
reverse_index: Arc<RwLock<HashMap<[u8; 32], String>>>,
}
impl NameRegistry {
pub fn new() -> Self {
Self {
records: Arc::new(RwLock::new(HashMap::new())),
reverse_index: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn register(&self, record: NameRecord) -> Result<bool> {
record.verify()?;
let mut records = self.records.write().await;
let mut reverse = self.reverse_index.write().await;
if let Some(existing) = records.get(&record.four_words) {
if existing.site_id == record.site_id {
records.insert(record.four_words.clone(), record.clone());
reverse.insert(record.site_id.hash, record.four_words.clone());
debug!("Updated name record: {}", record.four_words);
return Ok(true);
} else {
warn!(
"Name conflict: {} already claimed by {:?}",
record.four_words, existing.site_id
);
return Ok(false);
}
}
records.insert(record.four_words.clone(), record.clone());
reverse.insert(record.site_id.hash, record.four_words.clone());
debug!(
"Registered name: {} → {:?}",
record.four_words, record.site_id
);
Ok(true)
}
pub async fn resolve(&self, four_words: &str) -> Option<SiteId> {
let records = self.records.read().await;
records.get(four_words).map(|r| r.site_id.clone())
}
pub async fn reverse_lookup(&self, site_id: &SiteId) -> Option<String> {
let reverse = self.reverse_index.read().await;
reverse.get(&site_id.hash).cloned()
}
pub async fn list_names(&self) -> Vec<String> {
let records = self.records.read().await;
records.keys().cloned().collect()
}
}
impl Default for NameRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand_chacha::ChaCha20Rng;
use saorsa_pqc::ml_dsa_65::try_keygen_with_rng;
fn generate_test_keypair(seed: u64) -> (PrivateKey, PublicKey) {
let mut rng = ChaCha20Rng::seed_from_u64(seed);
let (pk, sk) = try_keygen_with_rng(&mut rng).expect("Failed to generate test keypair");
(sk, pk)
}
#[test]
fn test_name_record_creation_and_signing() {
let (sk, pk) = generate_test_keypair(1);
let mut record = NameRecord::new("ocean-forest-moon-star".to_string(), &pk);
assert_eq!(record.signature.len(), 0);
record.sign(&sk).expect("Failed to sign");
assert_eq!(record.signature.len(), 3309);
record.verify().expect("Verification failed");
}
#[test]
fn test_name_record_tamper_detection() {
let (sk, pk) = generate_test_keypair(2);
let mut record = NameRecord::new("test-name-here-now".to_string(), &pk);
record.sign(&sk).expect("Failed to sign");
record.four_words = "tampered-name-fake-bad".to_string();
assert!(record.verify().is_err(), "Tampered record should fail");
}
#[tokio::test]
async fn test_registry_tofu() {
let registry = NameRegistry::new();
let (sk1, pk1) = generate_test_keypair(3);
let (sk2, pk2) = generate_test_keypair(4);
let mut record1 = NameRecord::new("cool-site-here-now".to_string(), &pk1);
record1.sign(&sk1).unwrap();
assert!(registry.register(record1).await.unwrap());
let mut record2 = NameRecord::new("cool-site-here-now".to_string(), &pk2);
record2.sign(&sk2).unwrap();
assert!(
!registry.register(record2).await.unwrap(),
"TOFU should reject second claim"
);
}
#[tokio::test]
async fn test_registry_resolve() {
let registry = NameRegistry::new();
let (sk, pk) = generate_test_keypair(5);
let site_id = SiteId::from_public_key(&pk);
let mut record = NameRecord::new("my-awesome-site".to_string(), &pk);
record.sign(&sk).unwrap();
registry.register(record).await.unwrap();
let resolved = registry.resolve("my-awesome-site").await;
assert_eq!(resolved, Some(site_id.clone()));
let name = registry.reverse_lookup(&site_id).await;
assert_eq!(name, Some("my-awesome-site".to_string()));
}
#[tokio::test]
async fn test_registry_update_same_site() {
let registry = NameRegistry::new();
let (sk, pk) = generate_test_keypair(6);
let mut record1 = NameRecord::new("my-site".to_string(), &pk);
record1.sign(&sk).unwrap();
registry.register(record1).await.unwrap();
let mut record2 = NameRecord::new("my-site".to_string(), &pk);
record2.sign(&sk).unwrap();
assert!(
registry.register(record2).await.unwrap(),
"Same site should allow update"
);
let names = registry.list_names().await;
assert_eq!(names.len(), 1);
}
}