1use std::fs;
6use std::path::{Path, PathBuf};
7
8use chrono::{Duration, Utc};
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11use serde_json::{Map, Value, json};
12use uuid::Uuid;
13
14use crate::constants::{ACP_IDENTITY_VERSION, is_supported_trust_profile};
15use crate::crypto;
16use crate::errors::{AcpError, AcpResult};
17use crate::json_support;
18
19const IDENTITY_FILE_NAME: &str = "identity.json";
20const IDENTITY_DOC_FILE_NAME: &str = "identity_document.json";
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct AgentIdentity {
24 #[serde(rename = "agent_id")]
25 pub agent_id: String,
26 #[serde(rename = "signing_private_key")]
27 pub signing_private_key: String,
28 #[serde(rename = "signing_public_key")]
29 pub signing_public_key: String,
30 #[serde(rename = "encryption_private_key")]
31 pub encryption_private_key: String,
32 #[serde(rename = "encryption_public_key")]
33 pub encryption_public_key: String,
34 #[serde(rename = "signing_kid")]
35 pub signing_kid: String,
36 #[serde(rename = "encryption_kid")]
37 pub encryption_kid: String,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct AgentIdParts {
42 pub name: String,
43 pub domain: Option<String>,
44}
45
46#[derive(Debug, Clone)]
47pub struct IdentityBundle {
48 pub identity: AgentIdentity,
49 pub identity_document: Map<String, Value>,
50}
51
52impl AgentIdentity {
53 pub fn create(agent_id: &str) -> AcpResult<Self> {
54 parse_agent_id(agent_id)?;
55 let (signing_private_key, signing_public_key) = crypto::generate_ed25519_keypair();
56 let (encryption_private_key, encryption_public_key) = crypto::generate_x25519_keypair();
57 Ok(Self {
58 agent_id: agent_id.to_string(),
59 signing_private_key,
60 signing_public_key,
61 encryption_private_key,
62 encryption_public_key,
63 signing_kid: format!(
64 "sig-{}",
65 Uuid::new_v4()
66 .simple()
67 .to_string()
68 .chars()
69 .take(12)
70 .collect::<String>()
71 ),
72 encryption_kid: format!(
73 "enc-{}",
74 Uuid::new_v4()
75 .simple()
76 .to_string()
77 .chars()
78 .take(12)
79 .collect::<String>()
80 ),
81 })
82 }
83
84 #[allow(clippy::too_many_arguments)]
85 pub fn build_identity_document(
86 &self,
87 direct_endpoint: Option<&str>,
88 relay_hints: &[String],
89 trust_profile: &str,
90 capabilities: Option<&Map<String, Value>>,
91 valid_days: i64,
92 amqp_service: Option<&Map<String, Value>>,
93 mqtt_service: Option<&Map<String, Value>>,
94 http_security_profile: Option<&str>,
95 relay_security_profile: Option<&str>,
96 ) -> AcpResult<Map<String, Value>> {
97 if !is_supported_trust_profile(trust_profile) {
98 return Err(AcpError::Validation(format!(
99 "Unsupported trust profile: {trust_profile}"
100 )));
101 }
102
103 let mut service = Map::new();
104 if let Some(endpoint) = direct_endpoint {
105 service.insert(
106 "direct_endpoint".to_string(),
107 Value::String(endpoint.to_string()),
108 );
109 } else {
110 service.insert("direct_endpoint".to_string(), Value::Null);
111 }
112 service.insert(
113 "relay_hints".to_string(),
114 Value::Array(
115 relay_hints
116 .iter()
117 .map(|h| Value::String(h.clone()))
118 .collect(),
119 ),
120 );
121 if let Some(amqp) = amqp_service {
122 service.insert("amqp".to_string(), Value::Object(amqp.clone()));
123 }
124 if let Some(mqtt) = mqtt_service {
125 service.insert("mqtt".to_string(), Value::Object(mqtt.clone()));
126 }
127 if let (Some(endpoint), Some(profile)) = (direct_endpoint, http_security_profile) {
128 service.insert(
129 "http".to_string(),
130 json!({
131 "endpoint": endpoint,
132 "security_profile": profile,
133 }),
134 );
135 }
136 if let (Some(profile), Some(first_relay)) = (relay_security_profile, relay_hints.first()) {
137 service.insert(
138 "relay".to_string(),
139 json!({
140 "endpoint": first_relay,
141 "security_profile": profile,
142 }),
143 );
144 }
145
146 let mut document = Map::new();
147 document.insert(
148 "acp_identity_version".to_string(),
149 Value::String(ACP_IDENTITY_VERSION.to_string()),
150 );
151 document.insert("agent_id".to_string(), Value::String(self.agent_id.clone()));
152 document.insert(
153 "created_at".to_string(),
154 Value::String(Utc::now().to_rfc3339()),
155 );
156 document.insert(
157 "valid_until".to_string(),
158 Value::String((Utc::now() + Duration::days(valid_days.max(1))).to_rfc3339()),
159 );
160 document.insert(
161 "trust_profile".to_string(),
162 Value::String(trust_profile.to_string()),
163 );
164 document.insert(
165 "keys".to_string(),
166 json!({
167 "signing": {
168 "kid": self.signing_kid,
169 "alg": "Ed25519",
170 "public_key": self.signing_public_key
171 },
172 "encryption": {
173 "kid": self.encryption_kid,
174 "alg": "X25519",
175 "public_key": self.encryption_public_key
176 }
177 }),
178 );
179 document.insert("service".to_string(), Value::Object(service));
180 document.insert(
181 "capabilities".to_string(),
182 Value::Object(capabilities.cloned().unwrap_or_default()),
183 );
184
185 let unsigned = Value::Object(document.clone());
186 let signature = crypto::sign_bytes(
187 &json_support::canonical_json_bytes(&unsigned)?,
188 &self.signing_private_key,
189 )?;
190 document.insert(
191 "signature".to_string(),
192 json!({
193 "algorithm": "Ed25519",
194 "signed_by": self.signing_kid,
195 "value": signature,
196 }),
197 );
198 Ok(document)
199 }
200}
201
202pub fn verify_identity_document(identity_document: &Map<String, Value>) -> bool {
203 for required in ["agent_id", "keys", "service", "signature", "valid_until"] {
204 if !identity_document.contains_key(required) {
205 return false;
206 }
207 }
208 let Some(profile) = as_string(identity_document.get("trust_profile")) else {
209 return false;
210 };
211 if !is_supported_trust_profile(profile) {
212 return false;
213 }
214 let Some(valid_until) = as_string(identity_document.get("valid_until")) else {
215 return false;
216 };
217 let Ok(valid_until) = chrono::DateTime::parse_from_rfc3339(valid_until) else {
218 return false;
219 };
220 if valid_until <= Utc::now() {
221 return false;
222 }
223
224 let signature_value = identity_document
225 .get("signature")
226 .and_then(Value::as_object)
227 .and_then(|signature| signature.get("value"))
228 .and_then(Value::as_str)
229 .map(str::trim)
230 .filter(|value| !value.is_empty());
231 let signing_public = identity_document
232 .get("keys")
233 .and_then(Value::as_object)
234 .and_then(|keys| keys.get("signing"))
235 .and_then(Value::as_object)
236 .and_then(|signing| signing.get("public_key"))
237 .and_then(Value::as_str)
238 .map(str::trim)
239 .filter(|value| !value.is_empty());
240 let (Some(signature_value), Some(signing_public)) = (signature_value, signing_public) else {
241 return false;
242 };
243
244 let mut unsigned = identity_document.clone();
245 unsigned.remove("signature");
246 let Ok(bytes) = json_support::canonical_json_bytes(&Value::Object(unsigned)) else {
247 return false;
248 };
249 crypto::verify_signature(&bytes, signature_value, signing_public)
250}
251
252pub fn parse_agent_id(agent_id: &str) -> AcpResult<AgentIdParts> {
253 let regex = Regex::new(r"^agent:(?P<name>[^@]+)(?:@(?P<domain>.+))?$")
254 .map_err(|e| AcpError::Validation(format!("Invalid agent ID regex: {e}")))?;
255 let captures = regex
256 .captures(agent_id)
257 .ok_or_else(|| AcpError::Validation(format!("Invalid agent identifier: {agent_id}")))?;
258 let name = captures
259 .name("name")
260 .map(|m| m.as_str().to_string())
261 .ok_or_else(|| AcpError::Validation("agent id missing name".to_string()))?;
262 let domain = captures.name("domain").map(|m| m.as_str().to_string());
263 Ok(AgentIdParts { name, domain })
264}
265
266pub fn sanitize_agent_id(agent_id: &str) -> String {
267 agent_id
268 .chars()
269 .map(|c| {
270 if c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' {
271 c
272 } else {
273 '_'
274 }
275 })
276 .collect()
277}
278
279pub fn identity_path(storage_dir: &Path, agent_id: &str) -> PathBuf {
280 storage_dir.join(sanitize_agent_id(agent_id))
281}
282
283pub fn write_identity(
284 storage_dir: &Path,
285 identity: &AgentIdentity,
286 identity_document: &Map<String, Value>,
287) -> AcpResult<()> {
288 let path = identity_path(storage_dir, &identity.agent_id);
289 fs::create_dir_all(&path)?;
290 let identity_json = json_support::canonical_json_string(&serde_json::to_value(identity)?)?;
291 let doc_json = json_support::canonical_json_string(&Value::Object(identity_document.clone()))?;
292 fs::write(path.join(IDENTITY_FILE_NAME), identity_json)?;
293 fs::write(path.join(IDENTITY_DOC_FILE_NAME), doc_json)?;
294 Ok(())
295}
296
297pub fn read_identity(storage_dir: &Path, agent_id: &str) -> AcpResult<Option<IdentityBundle>> {
298 let path = identity_path(storage_dir, agent_id);
299 let identity_path = path.join(IDENTITY_FILE_NAME);
300 let doc_path = path.join(IDENTITY_DOC_FILE_NAME);
301 if !identity_path.exists() || !doc_path.exists() {
302 return Ok(None);
303 }
304 let identity = json_support::from_json::<AgentIdentity>(&fs::read_to_string(identity_path)?)?;
305 let identity_document = json_support::map_from_json(&fs::read_to_string(doc_path)?)?;
306 Ok(Some(IdentityBundle {
307 identity,
308 identity_document,
309 }))
310}
311
312fn as_string(value: Option<&Value>) -> Option<&str> {
313 value
314 .and_then(Value::as_str)
315 .map(str::trim)
316 .filter(|v| !v.is_empty())
317}