use blake2::{
digest::{consts::U32, Mac},
Blake2sMac,
};
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct EntityId(pub [u8; 32]);
impl EntityId {
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[inline]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[inline]
#[expect(
clippy::unwrap_used,
reason = "blake2s_hash returns [u8; 32]; slicing [0..8].try_into::<[u8; 8]>() is statically infallible"
)]
pub fn origin_hash(&self) -> u64 {
let hash = self.blake2s_hash(b"net-origin-v1");
u64::from_le_bytes(hash[0..8].try_into().unwrap())
}
#[inline]
#[expect(
clippy::unwrap_used,
reason = "blake2s_hash returns [u8; 32]; slicing [0..8].try_into::<[u8; 8]>() is statically infallible"
)]
pub fn node_id(&self) -> u64 {
let hash = self.blake2s_hash(b"net-node-id-v1");
u64::from_le_bytes(hash[0..8].try_into().unwrap())
}
pub fn verifying_key(&self) -> Result<VerifyingKey, EntityError> {
VerifyingKey::from_bytes(&self.0).map_err(|_| EntityError::InvalidPublicKey)
}
pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), EntityError> {
let vk = self.verifying_key()?;
vk.verify_strict(message, signature)
.map_err(|_| EntityError::InvalidSignature)
}
#[expect(
clippy::expect_used,
reason = "Blake2sMac::new_from_slice rejects only keys longer than 32 bytes; domain labels are short compile-time-constant slices"
)]
fn blake2s_hash(&self, label: &[u8]) -> [u8; 32] {
let mut mac = <Blake2sMac<U32> as Mac>::new_from_slice(label)
.expect("BLAKE2s accepts variable-length keys");
Mac::update(&mut mac, &self.0);
let result = mac.finalize().into_bytes();
let mut out = [0u8; 32];
out.copy_from_slice(&result);
out
}
}
impl std::fmt::Debug for EntityId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "EntityId({})", hex_short(&self.0))
}
}
impl std::fmt::Display for EntityId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex_short(&self.0))
}
}
impl serde::Serialize for EntityId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if serializer.is_human_readable() {
serializer.serialize_str(&hex::encode(self.0))
} else {
serializer.serialize_bytes(&self.0)
}
}
}
impl<'de> serde::Deserialize<'de> for EntityId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
if deserializer.is_human_readable() {
let hex_str = String::deserialize(deserializer)?;
let bytes = hex::decode(&hex_str).map_err(serde::de::Error::custom)?;
if bytes.len() != 32 {
return Err(serde::de::Error::custom("entity_id must be 32 bytes"));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(EntityId(arr))
} else {
let bytes = <Vec<u8>>::deserialize(deserializer)?;
if bytes.len() != 32 {
return Err(serde::de::Error::custom("entity_id must be 32 bytes"));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(EntityId(arr))
}
}
}
pub struct EntityKeypair {
signing_key: Option<SigningKey>,
entity_id: EntityId,
origin_hash: u64,
node_id: u64,
}
impl EntityKeypair {
pub fn generate() -> Self {
let mut rng_bytes = [0u8; 32];
if let Err(e) = getrandom::fill(&mut rng_bytes) {
eprintln!(
"FATAL: EntityKeypair::generate getrandom failure ({e:?}); aborting to avoid weak ed25519 secret"
);
std::process::abort();
}
let signing_key = SigningKey::from_bytes(&rng_bytes);
for byte in rng_bytes.iter_mut() {
unsafe { std::ptr::write_volatile(byte, 0) };
}
Self::from_signing_key(signing_key)
}
pub fn from_signing_key(signing_key: SigningKey) -> Self {
let verifying_key = signing_key.verifying_key();
let entity_id = EntityId::from_bytes(verifying_key.to_bytes());
let origin_hash = entity_id.origin_hash();
let node_id = entity_id.node_id();
Self {
signing_key: Some(signing_key),
entity_id,
origin_hash,
node_id,
}
}
pub fn from_bytes(secret: [u8; 32]) -> Self {
let signing_key = SigningKey::from_bytes(&secret);
Self::from_signing_key(signing_key)
}
pub fn public_only(entity_id: EntityId) -> Self {
let origin_hash = entity_id.origin_hash();
let node_id = entity_id.node_id();
Self {
signing_key: None,
entity_id,
origin_hash,
node_id,
}
}
#[inline]
pub fn entity_id(&self) -> &EntityId {
&self.entity_id
}
#[inline]
pub fn origin_hash(&self) -> u64 {
self.origin_hash
}
#[inline]
pub fn node_id(&self) -> u64 {
self.node_id
}
#[inline]
pub fn is_read_only(&self) -> bool {
self.signing_key.is_none()
}
#[inline]
pub fn sign(&self, message: &[u8]) -> Signature {
match &self.signing_key {
Some(sk) => sk.sign(message),
None => panic!(
"EntityKeypair::sign called on public-only keypair — use try_sign \
or check is_read_only() before signing",
),
}
}
#[inline]
pub fn try_sign(&self, message: &[u8]) -> Result<Signature, EntityError> {
self.signing_key
.as_ref()
.map(|sk| sk.sign(message))
.ok_or(EntityError::ReadOnly)
}
pub fn secret_bytes(&self) -> &[u8; 32] {
match &self.signing_key {
Some(sk) => sk.as_bytes(),
None => panic!(
"EntityKeypair::secret_bytes called on public-only keypair — use \
try_secret_bytes or check is_read_only()",
),
}
}
pub fn try_secret_bytes(&self) -> Result<&[u8; 32], EntityError> {
self.signing_key
.as_ref()
.map(|sk| sk.as_bytes())
.ok_or(EntityError::ReadOnly)
}
pub fn zeroize(&mut self) {
let _ = self.signing_key.take();
}
}
impl Clone for EntityKeypair {
fn clone(&self) -> Self {
match &self.signing_key {
Some(sk) => Self::from_signing_key(sk.clone()),
None => Self::public_only(self.entity_id.clone()),
}
}
}
impl std::fmt::Debug for EntityKeypair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let secret_marker = if self.signing_key.is_some() {
"[REDACTED]"
} else {
"[PUBLIC-ONLY]"
};
f.debug_struct("EntityKeypair")
.field("entity_id", &self.entity_id)
.field("origin_hash", &format!("{:08x}", self.origin_hash))
.field("node_id", &format!("{:016x}", self.node_id))
.field("secret", &secret_marker)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EntityError {
InvalidPublicKey,
InvalidSignature,
ReadOnly,
}
impl std::fmt::Display for EntityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidPublicKey => write!(f, "invalid public key"),
Self::InvalidSignature => write!(f, "invalid signature"),
Self::ReadOnly => write!(f, "keypair is public-only; signing is not available"),
}
}
}
impl std::error::Error for EntityError {}
fn hex_short(bytes: &[u8]) -> String {
bytes
.iter()
.take(8)
.map(|b| format!("{:02x}", b))
.collect::<String>()
+ "..."
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keypair_generate() {
let kp1 = EntityKeypair::generate();
let kp2 = EntityKeypair::generate();
assert_ne!(kp1.entity_id(), kp2.entity_id());
assert_ne!(kp1.origin_hash(), kp2.origin_hash());
assert_ne!(kp1.node_id(), kp2.node_id());
}
#[test]
fn test_keypair_from_bytes_deterministic() {
let secret = [0x42u8; 32];
let kp1 = EntityKeypair::from_bytes(secret);
let kp2 = EntityKeypair::from_bytes(secret);
assert_eq!(kp1.entity_id(), kp2.entity_id());
assert_eq!(kp1.origin_hash(), kp2.origin_hash());
assert_eq!(kp1.node_id(), kp2.node_id());
}
#[test]
fn test_sign_verify() {
let kp = EntityKeypair::generate();
let message = b"hello, mesh";
let signature = kp.sign(message);
assert!(kp.entity_id().verify(message, &signature).is_ok());
}
#[test]
fn test_verify_wrong_message() {
let kp = EntityKeypair::generate();
let signature = kp.sign(b"correct message");
assert_eq!(
kp.entity_id().verify(b"wrong message", &signature),
Err(EntityError::InvalidSignature)
);
}
#[test]
fn test_verify_wrong_key() {
let kp1 = EntityKeypair::generate();
let kp2 = EntityKeypair::generate();
let message = b"hello";
let signature = kp1.sign(message);
assert_eq!(
kp2.entity_id().verify(message, &signature),
Err(EntityError::InvalidSignature)
);
}
#[test]
fn test_origin_hash_nonzero() {
let kp = EntityKeypair::generate();
assert_eq!(kp.origin_hash(), kp.entity_id().origin_hash());
}
#[test]
fn test_node_id_nonzero() {
let kp = EntityKeypair::generate();
assert_eq!(kp.node_id(), kp.entity_id().node_id());
}
#[test]
fn test_clone_preserves_identity() {
let kp = EntityKeypair::generate();
let kp2 = kp.clone();
assert_eq!(kp.entity_id(), kp2.entity_id());
assert_eq!(kp.origin_hash(), kp2.origin_hash());
let sig = kp2.sign(b"test");
assert!(kp.entity_id().verify(b"test", &sig).is_ok());
}
#[test]
fn test_entity_id_display() {
let kp = EntityKeypair::generate();
let display = format!("{}", kp.entity_id());
assert!(display.ends_with("..."));
assert!(display.len() > 4);
}
#[test]
fn public_only_preserves_identity_queries() {
let full = EntityKeypair::generate();
let public = EntityKeypair::public_only(full.entity_id().clone());
assert_eq!(public.entity_id(), full.entity_id());
assert_eq!(public.origin_hash(), full.origin_hash());
assert_eq!(public.node_id(), full.node_id());
assert!(public.is_read_only());
assert!(!full.is_read_only());
}
#[test]
fn public_only_try_sign_returns_read_only() {
let public = EntityKeypair::public_only(EntityKeypair::generate().entity_id().clone());
let err = public.try_sign(b"payload").expect_err("must refuse");
assert_eq!(err, EntityError::ReadOnly);
}
#[test]
fn public_only_try_secret_bytes_returns_read_only() {
let public = EntityKeypair::public_only(EntityKeypair::generate().entity_id().clone());
let err = public.try_secret_bytes().expect_err("must refuse");
assert_eq!(err, EntityError::ReadOnly);
}
#[test]
#[should_panic(expected = "public-only keypair")]
fn public_only_sign_panics() {
let public = EntityKeypair::public_only(EntityKeypair::generate().entity_id().clone());
let _ = public.sign(b"payload");
}
#[test]
#[should_panic(expected = "public-only keypair")]
fn public_only_secret_bytes_panics() {
let public = EntityKeypair::public_only(EntityKeypair::generate().entity_id().clone());
let _ = public.secret_bytes();
}
#[test]
fn zeroize_converts_full_to_public_only() {
let mut kp = EntityKeypair::generate();
let entity_before = kp.entity_id().clone();
assert!(!kp.is_read_only());
let sig = kp.try_sign(b"pre").expect("full keypair signs");
assert!(entity_before.verify(b"pre", &sig).is_ok());
kp.zeroize();
assert!(kp.is_read_only());
assert_eq!(kp.entity_id(), &entity_before);
assert_eq!(
kp.try_sign(b"post")
.expect_err("post-zeroize signing must fail"),
EntityError::ReadOnly,
);
}
#[test]
fn zeroize_is_idempotent() {
let mut kp = EntityKeypair::generate();
kp.zeroize();
kp.zeroize();
assert!(kp.is_read_only());
}
#[test]
fn try_sign_on_full_keypair_matches_sign() {
let kp = EntityKeypair::generate();
let try_sig = kp.try_sign(b"m").expect("try_sign");
let plain_sig = kp.sign(b"m");
assert_eq!(try_sig.to_bytes(), plain_sig.to_bytes());
}
#[test]
fn clone_of_public_only_is_public_only() {
let public = EntityKeypair::public_only(EntityKeypair::generate().entity_id().clone());
let cloned = public.clone();
assert!(cloned.is_read_only());
assert_eq!(cloned.entity_id(), public.entity_id());
}
}