Skip to main content

hardware_enclave/
signing.rs

1// Copyright 2026 Jay Gowdy
2// SPDX-License-Identifier: MIT
3
4use crate::internal::app_storage::{AppSigningBackend, BackendKind};
5
6use crate::internal::core::types::{AccessPolicy, KeyType};
7
8use crate::error::{Error, Result};
9use crate::types::{KeyInfo, PresenceOptions};
10
11/// Handle to a signing backend. Obtained from `create_signer()`.
12///
13/// Multi-key: each method takes a `label` parameter. The factory
14/// initializes the backend and ensures the `default_key_label` exists.
15pub struct SignerHandle {
16    backend: AppSigningBackend,
17    backend_kind: BackendKind,
18}
19
20impl std::fmt::Debug for SignerHandle {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        f.debug_struct("SignerHandle")
23            .field("backend_kind", &self.backend_kind)
24            .finish()
25    }
26}
27
28impl SignerHandle {
29    pub(crate) fn new(backend: AppSigningBackend, backend_kind: BackendKind) -> Self {
30        Self {
31            backend,
32            backend_kind,
33        }
34    }
35
36    /// Generate a new P-256 signing key. Returns uncompressed SEC1 public key.
37    ///
38    /// # Errors
39    ///
40    /// - [`Error::DuplicateLabel`] if a key with this label already exists.
41    /// - [`Error::InvalidLabel`] if the label is empty, too long, or contains illegal characters.
42    /// - [`Error::PolicyNotSupported`] if the backend cannot enforce the requested `AccessPolicy`.
43    /// - [`Error::RequiresSigning`] if `policy` requires a code-signed binary.
44    /// - [`Error::KeyOperation`] for underlying hardware or I/O failures.
45    pub fn generate_key(&self, label: &str, policy: AccessPolicy) -> Result<Vec<u8>> {
46        // On macOS, BiometricOnly and PasswordOnly are hardware-enforced by the SE.
47        // On other platforms these policies are not hardware-enforceable; the backend
48        // will either apply a best-effort equivalent (Windows Hello UX) or reject.
49        // We do not reject here — let the backend decide based on its configuration.
50        self.backend
51            .key_manager()
52            .generate(label, KeyType::Signing, policy)
53            .map_err(Error::from)
54    }
55
56    /// Return the uncompressed SEC1 public key for an existing key.
57    pub fn public_key(&self, label: &str) -> Result<Vec<u8>> {
58        self.backend
59            .key_manager()
60            .public_key(label)
61            .map_err(Error::from)
62    }
63
64    /// Sign `data` (SHA-256 applied internally). Returns a DER-encoded ECDSA
65    /// P-256 signature.
66    ///
67    /// # Errors
68    ///
69    /// - [`Error::KeyNotFound`] if no key with this label exists.
70    /// - [`Error::AuthDenied`] if the keychain ACL denies access to the wrapping key.
71    /// - [`Error::AuthRequired`] if the device is locked or the GUI session is absent.
72    /// - [`Error::UserCancelled`] if the user dismissed a biometric prompt.
73    /// - [`Error::SignFailed`] for underlying hardware or crypto failures.
74    pub fn sign(&self, label: &str, data: &[u8]) -> Result<Vec<u8>> {
75        self.backend.signer().sign(label, data).map_err(Error::from)
76    }
77
78    /// Sign `data` with an optional user-presence prompt.
79    ///
80    /// - `PresenceMode::Strict` on a platform where `presence_available()` is false
81    ///   returns `Error::PresenceNotAvailable`.
82    /// - `PresenceMode::Cached` or `PresenceMode::None` always falls through to a
83    ///   plain sign on non-macOS platforms (no error).
84    ///
85    /// # Errors
86    ///
87    /// - [`Error::PresenceNotAvailable`] if `opts.mode` is `Strict` and the platform
88    ///   has no user-presence support.
89    /// - [`Error::KeyNotFound`] if no key with this label exists.
90    /// - [`Error::AuthDenied`] if the keychain ACL denies access to the wrapping key.
91    /// - [`Error::AuthRequired`] if the device is locked or the GUI session is absent.
92    /// - [`Error::UserCancelled`] if the user dismissed a biometric prompt.
93    /// - [`Error::SignFailed`] for underlying hardware or crypto failures.
94    pub fn sign_with_presence(
95        &self,
96        label: &str,
97        data: &[u8],
98        opts: &PresenceOptions,
99    ) -> Result<Vec<u8>> {
100        use crate::internal::core::types::PresenceMode;
101        if opts.mode == PresenceMode::Strict && !self.presence_available() {
102            return Err(Error::PresenceNotAvailable);
103        }
104        self.backend
105            .signer()
106            .sign_with_presence(label, data, opts.mode, opts.cache_ttl_secs, &opts.reason)
107            .map_err(Error::from)
108    }
109
110    /// True when the current platform supports presence prompting.
111    pub fn presence_available(&self) -> bool {
112        #[cfg(target_os = "macos")]
113        return crate::internal::apple::touch_id_available();
114        #[cfg(not(target_os = "macos"))]
115        false
116    }
117
118    /// List all signing keys in this app's key store.
119    pub fn list_keys(&self) -> Result<Vec<KeyInfo>> {
120        let labels = self
121            .backend
122            .key_manager()
123            .list_keys()
124            .map_err(Error::from)?;
125        let mut infos = Vec::with_capacity(labels.len());
126        for label in labels {
127            if let Ok(pub_key) = self.backend.key_manager().public_key(&label) {
128                infos.push(KeyInfo {
129                    label,
130                    key_type: KeyType::Signing,
131                    access_policy: None, // access_policy requires metadata read; not yet implemented
132                    public_key: pub_key,
133                });
134            }
135        }
136        Ok(infos)
137    }
138
139    /// Permanently delete a signing key and its metadata.
140    pub fn delete_key(&self, label: &str) -> Result<()> {
141        self.backend
142            .key_manager()
143            .delete_key(label)
144            .map_err(Error::from)
145    }
146
147    /// Return `true` if a key with this label exists.
148    pub fn key_exists(&self, label: &str) -> Result<bool> {
149        self.backend
150            .key_manager()
151            .key_exists(label)
152            .map_err(Error::from)
153    }
154
155    /// Atomically rename a signing key.
156    pub fn rename_key(&self, old_label: &str, new_label: &str) -> Result<()> {
157        self.backend
158            .key_manager()
159            .rename_key(old_label, new_label)
160            .map_err(Error::from)
161    }
162
163    /// Evict the cached wrapping-key / LAContext for `label`, forcing the next sign to
164    /// re-authenticate. Has no effect on platforms without presence caching.
165    pub fn evict_presence_cache(&self, label: &str) {
166        self.backend.signer().evict_wrapping_key_cache(label);
167    }
168
169    /// Which hardware security backend backs this handle.
170    pub fn backend_kind(&self) -> BackendKind {
171        self.backend_kind
172    }
173}
174
175#[cfg(test)]
176#[allow(clippy::unwrap_used)]
177mod tests {
178    use crate::types::AccessPolicy;
179
180    #[test]
181    fn key_info_access_policy_is_option() {
182        // BLK-12 / SG-3: verify KeyInfo.access_policy is Option<AccessPolicy>
183        // and list_keys() returns it as None (until metadata read is implemented).
184        // This is a compile-time check — if the field type changed, this won't compile.
185        let _: Option<AccessPolicy> = None; // type assertion
186    }
187}