auths-core 0.1.12

Core cryptography and keychain integration for Auths
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
//! Apple Secure Enclave P-256 key storage backend.
//!
//! Uses CryptoKit via a Swift FFI bridge. Private keys never leave the SE
//! hardware. Signing triggers Touch ID / Face ID. Key handles (encrypted
//! blobs) are stored as files in `~/.auths/se_keys/`.

use std::collections::HashMap;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::sync::Mutex;

use log::debug;

use crate::error::AgentError;
use crate::signing::{PassphraseProvider, SecureSigner};
use crate::storage::keychain::{IdentityDID, KeyAlias, KeyRole, KeyStorage};

// FFI declarations — symbols from the Swift static library
unsafe extern "C" {
    fn se_is_available() -> bool;
    fn se_create_key(
        out_handle: *mut u8,
        out_handle_len: *mut usize,
        out_pubkey: *mut u8,
        out_pubkey_len: *mut usize,
    ) -> i32;
    fn se_sign(
        handle: *const u8,
        handle_len: usize,
        msg: *const u8,
        msg_len: usize,
        out_sig: *mut u8,
        out_sig_len: *mut usize,
    ) -> i32;
    fn se_load_key(
        handle: *const u8,
        handle_len: usize,
        out_pubkey: *mut u8,
        out_pubkey_len: *mut usize,
    ) -> i32;
}

/// Check if Secure Enclave hardware is available.
pub fn is_available() -> bool {
    unsafe { se_is_available() }
}

fn se_error(code: i32) -> AgentError {
    match code {
        1 => AgentError::BackendUnavailable {
            backend: "secure-enclave",
            reason: "Secure Enclave not available".into(),
        },
        2 => AgentError::SigningFailed("biometric authentication failed or cancelled".into()),
        3 => AgentError::SigningFailed("Secure Enclave key operation failed".into()),
        _ => AgentError::SigningFailed(format!("Secure Enclave error code {code}")),
    }
}

/// Metadata stored alongside the SE key handle file.
#[derive(serde::Serialize, serde::Deserialize)]
struct KeyMetadata {
    identity_did: String,
    role: String,
}

/// Secure Enclave key storage backend.
///
/// Keys are generated inside the SE hardware and never leave it. The
/// `dataRepresentation` (an encrypted opaque blob) is stored as a file
/// in `~/.auths/se_keys/<alias>.se`. Only the same SE hardware can use it.
pub struct SecureEnclaveKeyStorage {
    keys_dir: PathBuf,
    /// Cache of loaded key handles to avoid re-reading files
    handle_cache: Mutex<HashMap<String, Vec<u8>>>,
}

impl SecureEnclaveKeyStorage {
    /// Create a new SE storage backend.
    ///
    /// Args:
    /// * `auths_home`: Path to `~/.auths` directory.
    pub fn new(auths_home: &std::path::Path) -> Result<Self, AgentError> {
        if !is_available() {
            return Err(AgentError::BackendUnavailable {
                backend: "secure-enclave",
                reason: "Secure Enclave not available on this hardware".into(),
            });
        }
        let keys_dir = auths_home.join("se_keys");
        if !keys_dir.exists() {
            fs::create_dir_all(&keys_dir).map_err(|e| {
                AgentError::IO(std::io::Error::other(format!(
                    "failed to create SE keys directory: {e}"
                )))
            })?;
            fs::set_permissions(&keys_dir, fs::Permissions::from_mode(0o700)).map_err(|e| {
                AgentError::IO(std::io::Error::other(format!(
                    "failed to set SE keys directory permissions: {e}"
                )))
            })?;
        }
        Ok(Self {
            keys_dir,
            handle_cache: Mutex::new(HashMap::new()),
        })
    }

    fn handle_path(&self, alias: &KeyAlias) -> PathBuf {
        self.keys_dir.join(format!("{}.se", alias.as_str()))
    }

    fn meta_path(&self, alias: &KeyAlias) -> PathBuf {
        self.keys_dir.join(format!("{}.meta.json", alias.as_str()))
    }

    /// A SOFTWARE key stored under this backend (e.g. a delegated agent's
    /// Ed25519 key, which the enclave cannot hold). The blob is the caller's
    /// passphrase-encrypted key material, stored verbatim.
    fn swkey_path(&self, alias: &KeyAlias) -> PathBuf {
        self.keys_dir.join(format!("{}.swkey", alias.as_str()))
    }

    fn load_handle(&self, alias: &KeyAlias) -> Result<Vec<u8>, AgentError> {
        let mut cache = self.handle_cache.lock().unwrap_or_else(|e| e.into_inner());
        if let Some(h) = cache.get(alias.as_str()) {
            return Ok(h.clone());
        }
        let path = self.handle_path(alias);
        let handle = fs::read(&path).map_err(|e| {
            AgentError::StorageError(format!(
                "SE key handle not found for '{}': {e}",
                alias.as_str()
            ))
        })?;
        cache.insert(alias.as_str().to_string(), handle.clone());
        Ok(handle)
    }
}

impl KeyStorage for SecureEnclaveKeyStorage {
    fn store_key(
        &self,
        alias: &KeyAlias,
        identity_did: &IdentityDID,
        role: KeyRole,
        _encrypted_key_data: &[u8],
    ) -> Result<(), AgentError> {
        // NON-EMPTY material is a SOFTWARE key the caller already encrypted (a
        // delegated agent's Ed25519 key, an imported key, a pairing-born device
        // key): store it verbatim. The enclave cannot hold it, and silently
        // minting an unrelated hardware key here would DISCARD the only copy of
        // a key the KEL already anchored — the delegation would "succeed" with
        // its private key lost.
        if !_encrypted_key_data.is_empty() {
            let path = self.swkey_path(alias);
            fs::write(&path, _encrypted_key_data).map_err(|e| {
                AgentError::IO(std::io::Error::other(format!(
                    "failed to write software key item: {e}"
                )))
            })?;
            fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).ok();
            let meta = KeyMetadata {
                identity_did: identity_did.to_string(),
                role: format!("{role:?}"),
            };
            let meta_json = serde_json::to_string_pretty(&meta).unwrap_or_default();
            fs::write(self.meta_path(alias), meta_json).ok();
            return Ok(());
        }

        // EMPTY material is the hardware-creation convention (identity init on
        // this backend passes no blob): generate the key inside the enclave.
        let mut handle_buf = vec![0u8; 512];
        let mut handle_len: usize = 0;
        let mut pubkey_buf = vec![0u8; 65];
        let mut pubkey_len: usize = 0;

        let code = unsafe {
            se_create_key(
                handle_buf.as_mut_ptr(),
                &mut handle_len,
                pubkey_buf.as_mut_ptr(),
                &mut pubkey_len,
            )
        };
        if code != 0 {
            return Err(se_error(code));
        }

        handle_buf.truncate(handle_len);

        // Write handle file
        let path = self.handle_path(alias);
        fs::write(&path, &handle_buf).map_err(|e| {
            AgentError::IO(std::io::Error::other(format!(
                "failed to write SE key handle: {e}"
            )))
        })?;
        fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).ok();

        // Write metadata
        let meta = KeyMetadata {
            identity_did: identity_did.to_string(),
            role: format!("{role:?}"),
        };
        let meta_json = serde_json::to_string_pretty(&meta).unwrap_or_default();
        fs::write(self.meta_path(alias), meta_json).ok();

        // Cache the handle
        let mut cache = self.handle_cache.lock().unwrap_or_else(|e| e.into_inner());
        cache.insert(alias.as_str().to_string(), handle_buf);

        debug!(
            "SE key created for alias '{}', pubkey {} bytes",
            alias.as_str(),
            pubkey_len
        );
        Ok(())
    }

    fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
        // A software item (delegated/imported key) loads its encrypted blob; a
        // hardware key loads its opaque handle. Handle wins if both exist.
        let handle = if self.handle_path(alias).exists() {
            self.load_handle(alias)?
        } else {
            let sw = self.swkey_path(alias);
            fs::read(&sw).map_err(|e| {
                AgentError::StorageError(format!(
                    "SE key handle not found for '{}': {e}",
                    alias.as_str()
                ))
            })?
        };

        // Read metadata. A key whose identity binding is missing must not load under a
        // placeholder DID — without it we cannot prove which identity the key belongs to.
        let meta_path = self.meta_path(alias);
        if !meta_path.exists() {
            return Err(AgentError::SecurityError(format!(
                "secure-enclave key '{}' has no identity metadata",
                alias.as_str()
            )));
        }
        let json = fs::read_to_string(&meta_path)
            .map_err(|e| AgentError::StorageError(format!("SE key metadata read failed: {e}")))?;
        let meta: KeyMetadata = serde_json::from_str(&json).map_err(|e| {
            AgentError::KeyDeserializationError(format!("SE key metadata parse failed: {e}"))
        })?;

        let identity_did = IdentityDID::parse(&meta.identity_did).map_err(|e| {
            AgentError::SecurityError(format!(
                "secure-enclave metadata for '{}' has a malformed identity DID: {e}",
                alias.as_str()
            ))
        })?;
        let role = if meta.role.contains("NextRotation") {
            KeyRole::NextRotation
        } else if meta.role.contains("DelegatedAgent") {
            KeyRole::DelegatedAgent
        } else {
            KeyRole::Primary
        };

        Ok((identity_did, role, handle))
    }

    fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
        let path = self.handle_path(alias);
        if path.exists() {
            fs::remove_file(&path).map_err(|e| {
                AgentError::IO(std::io::Error::other(format!(
                    "failed to delete SE key handle: {e}"
                )))
            })?;
        }
        let sw_path = self.swkey_path(alias);
        if sw_path.exists() {
            fs::remove_file(&sw_path).map_err(|e| {
                AgentError::IO(std::io::Error::other(format!(
                    "failed to delete software key item: {e}"
                )))
            })?;
        }
        let meta_path = self.meta_path(alias);
        if meta_path.exists() {
            fs::remove_file(&meta_path).ok();
        }
        let mut cache = self.handle_cache.lock().unwrap_or_else(|e| e.into_inner());
        cache.remove(alias.as_str());
        Ok(())
    }

    fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
        let mut aliases = Vec::new();
        if let Ok(entries) = fs::read_dir(&self.keys_dir) {
            for entry in entries.flatten() {
                let name = entry.file_name().to_string_lossy().to_string();
                if let Some(alias) = name.strip_suffix(".se") {
                    #[allow(clippy::disallowed_methods)]
                    // INVARIANT: alias comes from a filename we created in store_key
                    aliases.push(KeyAlias::new_unchecked(alias));
                } else if let Some(alias) = name.strip_suffix(".swkey") {
                    #[allow(clippy::disallowed_methods)]
                    // INVARIANT: alias comes from a filename we created in store_key
                    aliases.push(KeyAlias::new_unchecked(alias));
                }
            }
        }
        Ok(aliases)
    }

    fn list_aliases_for_identity(
        &self,
        identity_did: &IdentityDID,
    ) -> Result<Vec<KeyAlias>, AgentError> {
        let all = self.list_aliases()?;
        let mut matching = Vec::new();
        for alias in all {
            if let Ok(meta_str) = fs::read_to_string(self.meta_path(&alias))
                && let Ok(meta) = serde_json::from_str::<KeyMetadata>(&meta_str)
                && meta.identity_did == identity_did.as_str()
            {
                matching.push(alias);
            }
        }
        Ok(matching)
    }

    fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
        let (did, _, _) = self.load_key(alias)?;
        Ok(did)
    }

    fn backend_name(&self) -> &'static str {
        "secure-enclave"
    }

    fn is_hardware_backend(&self) -> bool {
        true
    }

    fn export_public_key(&self, alias: &KeyAlias) -> Result<Vec<u8>, AgentError> {
        let handle = self.load_handle(alias)?;
        public_key_from_handle(&handle)
    }

    fn sign_raw(&self, alias: &KeyAlias, message: &[u8]) -> Result<Vec<u8>, AgentError> {
        let handle = self.load_handle(alias)?;
        sign_with_handle(&handle, message)
    }

    fn rebind_identity(
        &self,
        alias: &KeyAlias,
        identity_did: &IdentityDID,
    ) -> Result<(), AgentError> {
        let (_, role, _) = self.load_key(alias)?;
        let meta = KeyMetadata {
            identity_did: identity_did.to_string(),
            role: format!("{role:?}"),
        };
        let meta_json = serde_json::to_string_pretty(&meta)
            .map_err(|e| AgentError::StorageError(format!("SE metadata serialize: {e}")))?;
        fs::write(self.meta_path(alias), meta_json).map_err(|e| {
            AgentError::IO(std::io::Error::other(format!(
                "failed to rewrite SE key metadata: {e}"
            )))
        })?;
        Ok(())
    }
}

impl SecureSigner for SecureEnclaveKeyStorage {
    fn sign_with_alias(
        &self,
        alias: &KeyAlias,
        _passphrase_provider: &dyn PassphraseProvider,
        message: &[u8],
    ) -> Result<Vec<u8>, AgentError> {
        let handle = self.load_handle(alias)?;
        sign_with_handle(&handle, message)
    }

    fn sign_for_identity(
        &self,
        identity_did: &IdentityDID,
        passphrase_provider: &dyn PassphraseProvider,
        message: &[u8],
    ) -> Result<Vec<u8>, AgentError> {
        let aliases = self.list_aliases_for_identity(identity_did)?;
        let alias = aliases.first().ok_or_else(|| {
            AgentError::StorageError(format!(
                "no SE key found for identity {}",
                identity_did.as_str()
            ))
        })?;
        self.sign_with_alias(alias, passphrase_provider, message)
    }
}

/// Get the compressed P-256 public key for an SE key handle.
pub fn public_key_from_handle(handle: &[u8]) -> Result<Vec<u8>, AgentError> {
    let mut pubkey_buf = vec![0u8; 65];
    let mut pubkey_len: usize = 0;

    let code = unsafe {
        se_load_key(
            handle.as_ptr(),
            handle.len(),
            pubkey_buf.as_mut_ptr(),
            &mut pubkey_len,
        )
    };
    if code != 0 {
        return Err(se_error(code));
    }

    pubkey_buf.truncate(pubkey_len);
    Ok(pubkey_buf)
}

/// Sign a message using an SE key handle. Triggers biometric prompt.
pub fn sign_with_handle(handle: &[u8], message: &[u8]) -> Result<Vec<u8>, AgentError> {
    let mut sig_buf = vec![0u8; 64];
    let mut sig_len: usize = 0;

    let code = unsafe {
        se_sign(
            handle.as_ptr(),
            handle.len(),
            message.as_ptr(),
            message.len(),
            sig_buf.as_mut_ptr(),
            &mut sig_len,
        )
    };
    if code != 0 {
        return Err(se_error(code));
    }

    sig_buf.truncate(sig_len);
    Ok(sig_buf)
}