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}