Skip to main content

agent_id_core/
keys.rs

1//! Key management for AIP identities.
2//!
3//! # Security
4//!
5//! This module handles secret key material. Key security properties:
6//!
7//! - **Zeroization on drop**: Secret key bytes are automatically zeroed when
8//!   dropped. This is handled by `ed25519-dalek`'s `SigningKey` via its
9//!   `zeroize` feature, preventing leakage via memory dumps, swap files,
10//!   or cold boot attacks.
11//!
12//! - **No Debug leakage**: Keys implement Debug safely, showing only the
13//!   DID (public info), not secret material.
14
15use crate::{Did, Error, Result};
16use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
17use rand::rngs::OsRng;
18use std::fmt;
19
20/// A root identity key.
21///
22/// This is the primary key that defines an agent's identity.
23/// It should be stored securely and used sparingly.
24///
25/// # Security
26///
27/// - The inner `SigningKey` automatically zeroizes secret key bytes on drop
28///   (via ed25519-dalek's `zeroize` feature).
29/// - The `Debug` implementation only shows the DID (public info) to prevent
30///   accidental exposure of key material in logs.
31pub struct RootKey {
32    signing_key: SigningKey,
33}
34
35impl fmt::Debug for RootKey {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.debug_struct("RootKey")
38            .field("did", &self.did().to_string())
39            .finish_non_exhaustive()
40    }
41}
42
43impl RootKey {
44    /// Generate a new random root key.
45    pub fn generate() -> Self {
46        Self {
47            signing_key: SigningKey::generate(&mut OsRng),
48        }
49    }
50
51    /// Create from existing bytes.
52    ///
53    /// # Security
54    ///
55    /// The caller should zeroize the source bytes after this call
56    /// if they are no longer needed.
57    pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self> {
58        Ok(Self {
59            signing_key: SigningKey::from_bytes(bytes),
60        })
61    }
62
63    /// Get the DID for this root key.
64    pub fn did(&self) -> Did {
65        Did::new(self.signing_key.verifying_key())
66    }
67
68    /// Get the public verifying key.
69    pub fn verifying_key(&self) -> VerifyingKey {
70        self.signing_key.verifying_key()
71    }
72
73    /// Sign a message.
74    pub fn sign(&self, message: &[u8]) -> Signature {
75        self.signing_key.sign(message)
76    }
77
78    /// Get the secret key bytes.
79    ///
80    /// # Security Warning
81    ///
82    /// This returns raw secret key material. The caller is responsible for:
83    /// - Storing the bytes securely
84    /// - Zeroizing the bytes when no longer needed
85    /// - Not logging or printing these bytes
86    pub fn to_bytes(&self) -> [u8; 32] {
87        self.signing_key.to_bytes()
88    }
89}
90
91/// A session key delegated from a root key.
92///
93/// Used for day-to-day operations without exposing the root key.
94///
95/// # Security
96///
97/// - The inner `SigningKey` automatically zeroizes secret key bytes on drop.
98/// - Session keys should be short-lived and rotated frequently.
99/// - The `Debug` implementation only shows public info (root DID and pubkey
100///   fingerprint).
101pub struct SessionKey {
102    signing_key: SigningKey,
103    root_did: Did,
104}
105
106impl fmt::Debug for SessionKey {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        // Show a short fingerprint of the public key (first 8 chars of base58)
109        let pubkey_fingerprint = {
110            let full = self.public_key_base58();
111            if full.len() > 8 {
112                format!("{}...", &full[..8])
113            } else {
114                full
115            }
116        };
117
118        f.debug_struct("SessionKey")
119            .field("root_did", &self.root_did.to_string())
120            .field("pubkey", &pubkey_fingerprint)
121            .finish_non_exhaustive()
122    }
123}
124
125impl SessionKey {
126    /// Generate a new session key for a root identity.
127    pub fn generate(root_did: Did) -> Self {
128        Self {
129            signing_key: SigningKey::generate(&mut OsRng),
130            root_did,
131        }
132    }
133
134    /// Get the root DID this session key belongs to.
135    pub fn root_did(&self) -> &Did {
136        &self.root_did
137    }
138
139    /// Get the public verifying key.
140    pub fn verifying_key(&self) -> VerifyingKey {
141        self.signing_key.verifying_key()
142    }
143
144    /// Sign a message.
145    pub fn sign(&self, message: &[u8]) -> Signature {
146        self.signing_key.sign(message)
147    }
148
149    /// Get the public key as base58.
150    pub fn public_key_base58(&self) -> String {
151        bs58::encode(self.signing_key.verifying_key().as_bytes()).into_string()
152    }
153}
154
155/// Verify a signature against a public key.
156pub fn verify(public_key: &VerifyingKey, message: &[u8], signature: &Signature) -> Result<()> {
157    public_key
158        .verify(message, signature)
159        .map_err(|_| Error::InvalidSignature)
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_root_key_generation() {
168        let root = RootKey::generate();
169        let did = root.did();
170        assert!(did.to_string().starts_with("did:key:"));
171    }
172
173    #[test]
174    fn test_sign_verify() {
175        let root = RootKey::generate();
176        let message = b"hello world";
177        let signature = root.sign(message);
178
179        verify(&root.verifying_key(), message, &signature).unwrap();
180    }
181
182    #[test]
183    fn test_session_key() {
184        let root = RootKey::generate();
185        let session = SessionKey::generate(root.did());
186
187        assert_eq!(session.root_did(), &root.did());
188
189        let message = b"session message";
190        let sig = session.sign(message);
191        verify(&session.verifying_key(), message, &sig).unwrap();
192    }
193
194    #[test]
195    fn test_root_key_roundtrip() {
196        let root = RootKey::generate();
197        let bytes = root.to_bytes();
198        let restored = RootKey::from_bytes(&bytes).unwrap();
199
200        assert_eq!(root.did(), restored.did());
201    }
202
203    #[test]
204    fn test_root_key_debug_does_not_leak_secrets() {
205        let root = RootKey::generate();
206        let debug_output = format!("{:?}", root);
207
208        // Should contain the DID (public info)
209        assert!(debug_output.contains("did:key:"));
210
211        // Should NOT contain any of these patterns that would indicate leaked key material
212        assert!(
213            !debug_output.contains("FieldElement"),
214            "Debug output should not contain internal crypto field elements"
215        );
216        assert!(
217            !debug_output.contains("EdwardsPoint"),
218            "Debug output should not contain internal crypto types"
219        );
220        assert!(
221            !debug_output.to_lowercase().contains("secret"),
222            "Debug output should not reference 'secret'"
223        );
224
225        // Should use finish_non_exhaustive (indicated by "..")
226        assert!(
227            debug_output.contains(".."),
228            "Debug should indicate hidden fields with .."
229        );
230    }
231
232    #[test]
233    fn test_session_key_debug_does_not_leak_secrets() {
234        let root = RootKey::generate();
235        let session = SessionKey::generate(root.did());
236        let debug_output = format!("{:?}", session);
237
238        // Should contain root_did and truncated pubkey
239        assert!(debug_output.contains("root_did"));
240        assert!(debug_output.contains("pubkey"));
241
242        // Pubkey should be truncated (ends with ...)
243        assert!(
244            debug_output.contains("...\""),
245            "Public key should be truncated in debug output"
246        );
247
248        // Should NOT contain leaked key material
249        assert!(
250            !debug_output.contains("FieldElement"),
251            "Debug output should not contain internal crypto field elements"
252        );
253        assert!(
254            !debug_output.contains("EdwardsPoint"),
255            "Debug output should not contain internal crypto types"
256        );
257        assert!(
258            !debug_output.to_lowercase().contains("secret"),
259            "Debug output should not reference 'secret'"
260        );
261    }
262}