Skip to main content

aap_protocol/
identity.rs

1//! AAP Identity — cryptographic identity for an AI agent.
2//! Address format: `aap://org/type/name@semver`
3
4use chrono::{DateTime, Utc};
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::crypto::{KeyPair, signable, verify_signature};
10use crate::errors::{AAPError, Result};
11
12/// An AI agent's cryptographic identity.
13/// Signed by a human supervisor.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Identity {
16    pub aap_version: String,
17    pub id: String,
18    pub public_key: String,
19    pub parent: String,
20    pub scope: Vec<String>,
21    pub issued_at: DateTime<Utc>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub expires_at: Option<DateTime<Utc>>,
24    pub revoked: bool,
25    #[serde(skip_serializing_if = "HashMap::is_empty", default)]
26    pub metadata: HashMap<String, String>,
27    pub signature: String,
28}
29
30impl Identity {
31    /// Create and sign a new Identity.
32    /// `parent_kp` is the human supervisor's keypair — they sign the agent's identity.
33    pub fn new(
34        id: &str,
35        scope: Vec<String>,
36        agent_kp: &KeyPair,
37        parent_kp: &KeyPair,
38        parent_did: &str,
39    ) -> Result<Self> {
40        // Validate id format
41        let id_re = Regex::new(r"^aap://[a-z0-9\-\.]+/[a-z0-9\-]+/[a-z0-9\-\.]+@\d+\.\d+\.\d+$")
42            .unwrap();
43        if !id_re.is_match(id) {
44            return Err(AAPError::Validation {
45                field: "id".into(),
46                message: format!("invalid format: {id:?} — expected aap://org/type/name@semver"),
47            });
48        }
49        if scope.is_empty() {
50            return Err(AAPError::Validation {
51                field: "scope".into(),
52                message: "must contain at least one item".into(),
53            });
54        }
55        let scope_re = Regex::new(r"^[a-z]+:[a-z0-9_\-\*]+$").unwrap();
56        for s in &scope {
57            if !scope_re.is_match(s) {
58                return Err(AAPError::Validation {
59                    field: "scope".into(),
60                    message: format!("invalid item {s:?} — expected verb:resource"),
61                });
62            }
63        }
64
65        let mut identity = Self {
66            aap_version: "0.1".into(),
67            id: id.into(),
68            public_key: agent_kp.public_key_b64(),
69            parent: parent_did.into(),
70            scope,
71            issued_at: Utc::now(),
72            expires_at: None,
73            revoked: false,
74            metadata: HashMap::new(),
75            signature: String::new(),
76        };
77
78        let v = serde_json::to_value(&identity)?;
79        let data = signable(&v)?;
80        identity.signature = parent_kp.sign(&data);
81        Ok(identity)
82    }
83
84    /// Check if `action` is within this identity's scope.
85    pub fn allows_action(&self, action: &str) -> bool {
86        let (verb, resource) = action.split_once(':').unwrap_or((action, ""));
87        self.scope.iter().any(|s| {
88            let (sv, sr) = s.split_once(':').unwrap_or((s.as_str(), ""));
89            sv == verb && (sr == "*" || sr == resource)
90        })
91    }
92
93    /// Returns true if this identity has passed its expiry.
94    pub fn is_expired(&self) -> bool {
95        self.expires_at.map(|e| Utc::now() > e).unwrap_or(false)
96    }
97
98    /// Verify the signature against the parent's public key.
99    pub fn verify(&self, parent_public_key_b64: &str) -> Result<()> {
100        if self.revoked {
101            return Err(AAPError::Revocation { id: self.id.clone() });
102        }
103        let v = serde_json::to_value(self)?;
104        let data = signable(&v)?;
105        verify_signature(parent_public_key_b64, &data, &self.signature)
106    }
107}