Skip to main content

clasp_registry/
entity.rs

1//! Entity types for the CLASP registry
2
3use ed25519_dalek::{SigningKey, VerifyingKey};
4use rand::rngs::OsRng;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fmt;
8use std::time::SystemTime;
9
10use crate::error::{RegistryError, Result};
11
12/// Entity ID format: "clasp:<base58-ed25519-pubkey-prefix>"
13/// Uses first 16 bytes of the 32-byte public key for a shorter but still unique ID.
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct EntityId(String);
16
17impl EntityId {
18    /// Create an EntityId from a public key
19    pub fn from_public_key(key: &[u8]) -> Result<Self> {
20        if key.len() != 32 {
21            return Err(RegistryError::InvalidKey(format!(
22                "expected 32-byte Ed25519 public key, got {} bytes",
23                key.len()
24            )));
25        }
26        let encoded = bs58::encode(&key[..16]).into_string();
27        Ok(Self(format!("clasp:{}", encoded)))
28    }
29
30    /// Parse an EntityId from string
31    pub fn parse(s: &str) -> Result<Self> {
32        if !s.starts_with("clasp:") {
33            return Err(RegistryError::InvalidId(format!(
34                "entity ID must start with 'clasp:', got: {}",
35                s
36            )));
37        }
38        let suffix = &s[6..];
39        // Validate base58
40        bs58::decode(suffix)
41            .into_vec()
42            .map_err(|e| RegistryError::InvalidId(format!("invalid base58 in entity ID: {}", e)))?;
43        Ok(Self(s.to_string()))
44    }
45
46    /// Get the raw string value
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52impl fmt::Display for EntityId {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        write!(f, "{}", self.0)
55    }
56}
57
58impl From<EntityId> for String {
59    fn from(id: EntityId) -> Self {
60        id.0
61    }
62}
63
64/// Type of entity in the registry
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "lowercase")]
67pub enum EntityType {
68    Device,
69    User,
70    Service,
71    Router,
72}
73
74impl fmt::Display for EntityType {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            EntityType::Device => write!(f, "device"),
78            EntityType::User => write!(f, "user"),
79            EntityType::Service => write!(f, "service"),
80            EntityType::Router => write!(f, "router"),
81        }
82    }
83}
84
85/// Status of an entity
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
87#[serde(rename_all = "lowercase")]
88pub enum EntityStatus {
89    #[default]
90    Active,
91    Suspended,
92    Revoked,
93}
94
95impl fmt::Display for EntityStatus {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match self {
98            EntityStatus::Active => write!(f, "active"),
99            EntityStatus::Suspended => write!(f, "suspended"),
100            EntityStatus::Revoked => write!(f, "revoked"),
101        }
102    }
103}
104
105/// A registered entity in the CLASP network
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct Entity {
108    pub id: EntityId,
109    pub entity_type: EntityType,
110    pub name: String,
111    #[serde(with = "hex_bytes")]
112    pub public_key: Vec<u8>,
113    pub created_at: SystemTime,
114    #[serde(default)]
115    pub metadata: HashMap<String, String>,
116    #[serde(default)]
117    pub tags: Vec<String>,
118    #[serde(default)]
119    pub namespaces: Vec<String>,
120    #[serde(default)]
121    pub scopes: Vec<String>,
122    #[serde(default)]
123    pub status: EntityStatus,
124}
125
126impl Entity {
127    /// Check if this entity is currently active
128    pub fn is_active(&self) -> bool {
129        self.status == EntityStatus::Active
130    }
131}
132
133/// An entity keypair (private + public key)
134pub struct EntityKeypair {
135    pub entity_id: EntityId,
136    pub signing_key: SigningKey,
137    pub verifying_key: VerifyingKey,
138}
139
140impl EntityKeypair {
141    /// Generate a new random keypair
142    pub fn generate() -> Result<Self> {
143        let signing_key = SigningKey::generate(&mut OsRng);
144        let verifying_key = signing_key.verifying_key();
145        let entity_id = EntityId::from_public_key(verifying_key.as_bytes())?;
146
147        Ok(Self {
148            entity_id,
149            signing_key,
150            verifying_key,
151        })
152    }
153
154    /// Create from an existing signing key
155    pub fn from_signing_key(signing_key: SigningKey) -> Result<Self> {
156        let verifying_key = signing_key.verifying_key();
157        let entity_id = EntityId::from_public_key(verifying_key.as_bytes())?;
158
159        Ok(Self {
160            entity_id,
161            signing_key,
162            verifying_key,
163        })
164    }
165
166    /// Get the public key bytes
167    pub fn public_key_bytes(&self) -> &[u8] {
168        self.verifying_key.as_bytes()
169    }
170
171    /// Create an Entity from this keypair
172    pub fn to_entity(&self, entity_type: EntityType, name: String) -> Entity {
173        Entity {
174            id: self.entity_id.clone(),
175            entity_type,
176            name,
177            public_key: self.verifying_key.as_bytes().to_vec(),
178            created_at: SystemTime::now(),
179            metadata: HashMap::new(),
180            tags: Vec::new(),
181            namespaces: Vec::new(),
182            scopes: Vec::new(),
183            status: EntityStatus::Active,
184        }
185    }
186}
187
188impl fmt::Debug for EntityKeypair {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        f.debug_struct("EntityKeypair")
191            .field("entity_id", &self.entity_id)
192            .field("verifying_key", &"[redacted]")
193            .finish()
194    }
195}
196
197/// Serde helper for hex-encoded byte arrays
198mod hex_bytes {
199    use serde::{self, Deserialize, Deserializer, Serializer};
200
201    pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
202    where
203        S: Serializer,
204    {
205        let hex_string: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
206        serializer.serialize_str(&hex_string)
207    }
208
209    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
210    where
211        D: Deserializer<'de>,
212    {
213        let s = String::deserialize(deserializer)?;
214        (0..s.len())
215            .step_by(2)
216            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(serde::de::Error::custom))
217            .collect()
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_entity_id_from_public_key() {
227        let keypair = EntityKeypair::generate().unwrap();
228        let id = &keypair.entity_id;
229        assert!(id.as_str().starts_with("clasp:"));
230        assert!(id.as_str().len() > 10);
231    }
232
233    #[test]
234    fn test_entity_id_parse() {
235        let keypair = EntityKeypair::generate().unwrap();
236        let id_str = keypair.entity_id.as_str();
237        let parsed = EntityId::parse(id_str).unwrap();
238        assert_eq!(parsed, keypair.entity_id);
239    }
240
241    #[test]
242    fn test_entity_id_parse_invalid() {
243        assert!(EntityId::parse("invalid").is_err());
244        assert!(EntityId::parse("clasp:!!!").is_err());
245    }
246
247    #[test]
248    fn test_keypair_generate() {
249        let kp1 = EntityKeypair::generate().unwrap();
250        let kp2 = EntityKeypair::generate().unwrap();
251        assert_ne!(kp1.entity_id, kp2.entity_id);
252    }
253
254    #[test]
255    fn test_to_entity() {
256        let keypair = EntityKeypair::generate().unwrap();
257        let entity = keypair.to_entity(EntityType::Device, "test-device".to_string());
258        assert_eq!(entity.id, keypair.entity_id);
259        assert_eq!(entity.entity_type, EntityType::Device);
260        assert_eq!(entity.name, "test-device");
261        assert!(entity.is_active());
262    }
263
264    #[test]
265    fn test_entity_status() {
266        let keypair = EntityKeypair::generate().unwrap();
267        let mut entity = keypair.to_entity(EntityType::User, "test-user".to_string());
268        assert!(entity.is_active());
269
270        entity.status = EntityStatus::Suspended;
271        assert!(!entity.is_active());
272
273        entity.status = EntityStatus::Revoked;
274        assert!(!entity.is_active());
275    }
276}