use clawft_types::placement::{FactsDelta, FactsError, NodeFacts};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use crate::node_registry::node_id_from_pubkey;
pub const FACTS_DOMAIN: &[u8] = b"weftos.node_facts.v1\0";
pub const DELTA_DOMAIN: &[u8] = b"weftos.node_facts_delta.v1\0";
pub const MAX_PAYLOAD_BYTES: usize = 256 * 1024;
pub const MAX_CLOCK_SKEW_SECS: u64 = 60;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum NodeFactsAdvertError {
#[error("invalid public key")]
BadPublicKey,
#[error("invalid signature length (want 64, got {0})")]
BadSignatureLen(usize),
#[error("signature verification failed")]
BadSignature,
#[error("payload too large ({0} bytes)")]
TooLarge(usize),
#[error("malformed payload: {0}")]
Malformed(String),
#[error("node_id {claimed} does not belong to the signing key ({derived})")]
NodeMismatch {
claimed: String,
derived: String,
},
#[error(transparent)]
Facts(#[from] FactsError),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedNodeFacts {
pub payload: String,
pub public_key: Vec<u8>,
pub signature: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedFactsDelta {
pub payload: String,
pub public_key: Vec<u8>,
pub signature: Vec<u8>,
}
fn signed_bytes(domain: &[u8], payload: &str) -> Vec<u8> {
let mut v = Vec::with_capacity(domain.len() + payload.len());
v.extend_from_slice(domain);
v.extend_from_slice(payload.as_bytes());
v
}
fn check_envelope(
domain: &[u8],
payload: &str,
public_key: &[u8],
signature: &[u8],
) -> Result<[u8; 32], NodeFactsAdvertError> {
let pk: [u8; 32] = public_key
.try_into()
.map_err(|_| NodeFactsAdvertError::BadPublicKey)?;
let sig: [u8; 64] = signature
.try_into()
.map_err(|_| NodeFactsAdvertError::BadSignatureLen(signature.len()))?;
if payload.len() > MAX_PAYLOAD_BYTES {
return Err(NodeFactsAdvertError::TooLarge(payload.len()));
}
let vk = VerifyingKey::from_bytes(&pk).map_err(|_| NodeFactsAdvertError::BadPublicKey)?;
vk.verify(&signed_bytes(domain, payload), &Signature::from_bytes(&sig))
.map_err(|_| NodeFactsAdvertError::BadSignature)?;
Ok(pk)
}
fn check_binding(node_id: &str, pk: &[u8; 32]) -> Result<(), NodeFactsAdvertError> {
let derived = node_id_from_pubkey(pk);
if node_id != derived {
return Err(NodeFactsAdvertError::NodeMismatch {
claimed: node_id.to_string(),
derived,
});
}
Ok(())
}
pub fn sign_node_facts(
facts: &NodeFacts,
key: &SigningKey,
) -> Result<SignedNodeFacts, NodeFactsAdvertError> {
facts.validate()?;
let pk = key.verifying_key().to_bytes();
check_binding(&facts.node_id, &pk)?;
let payload =
serde_json::to_string(facts).map_err(|e| NodeFactsAdvertError::Malformed(e.to_string()))?;
if payload.len() > MAX_PAYLOAD_BYTES {
return Err(NodeFactsAdvertError::TooLarge(payload.len()));
}
let signature = key.sign(&signed_bytes(FACTS_DOMAIN, &payload));
Ok(SignedNodeFacts {
payload,
public_key: pk.to_vec(),
signature: signature.to_bytes().to_vec(),
})
}
pub fn verify_node_facts(
signed: &SignedNodeFacts,
now: u64,
) -> Result<NodeFacts, NodeFactsAdvertError> {
let pk = check_envelope(
FACTS_DOMAIN,
&signed.payload,
&signed.public_key,
&signed.signature,
)?;
let facts: NodeFacts = serde_json::from_str(&signed.payload)
.map_err(|e| NodeFactsAdvertError::Malformed(e.to_string()))?;
check_binding(&facts.node_id, &pk)?;
facts.check_window(now, MAX_CLOCK_SKEW_SECS)?;
Ok(facts)
}
pub fn sign_facts_delta(
delta: &FactsDelta,
key: &SigningKey,
) -> Result<SignedFactsDelta, NodeFactsAdvertError> {
let pk = key.verifying_key().to_bytes();
check_binding(&delta.node_id, &pk)?;
let payload =
serde_json::to_string(delta).map_err(|e| NodeFactsAdvertError::Malformed(e.to_string()))?;
let signature = key.sign(&signed_bytes(DELTA_DOMAIN, &payload));
Ok(SignedFactsDelta {
payload,
public_key: pk.to_vec(),
signature: signature.to_bytes().to_vec(),
})
}
pub fn verify_facts_delta(
signed: &SignedFactsDelta,
now: u64,
) -> Result<(FactsDelta, [u8; 32]), NodeFactsAdvertError> {
let pk = check_envelope(
DELTA_DOMAIN,
&signed.payload,
&signed.public_key,
&signed.signature,
)?;
let delta: FactsDelta = serde_json::from_str(&signed.payload)
.map_err(|e| NodeFactsAdvertError::Malformed(e.to_string()))?;
check_binding(&delta.node_id, &pk)?;
if delta.issued_at > now.saturating_add(MAX_CLOCK_SKEW_SECS) {
return Err(FactsError::FromFuture {
issued_at: delta.issued_at,
now,
}
.into());
}
Ok((delta, pk))
}
#[cfg(test)]
#[path = "node_facts_advert_tests.rs"]
mod tests;