Skip to main content

a1/
identity.rs

1pub mod narrowing;
2pub mod receipt;
3
4use ed25519_dalek::{Signature, Signer as DalekSigner, SigningKey, VerifyingKey};
5use rand::rngs::OsRng;
6use zeroize::ZeroizeOnDrop;
7
8/// Abstraction over an Ed25519 signing backend.
9///
10/// Implement this trait to integrate hardware security modules (HSMs),
11/// cloud key management services (AWS KMS, Azure Key Vault, HashiCorp Vault,
12/// Google Cloud KMS), or any other backend that can produce Ed25519 signatures
13/// without exposing raw private key bytes.
14///
15/// The in-process [`DyoloIdentity`] implements this trait and is the
16/// recommended default for development and testing. Production deployments
17/// should implement `Signer` over their KMS of choice.
18///
19/// # Security contract
20///
21/// Implementors MUST ensure that:
22/// - `verifying_key()` always returns the public key matching the private key
23///   used by `sign_message()`.
24/// - `sign_message()` MUST NOT pre-hash `msg`; the caller already applies
25///   domain separation before calling this function.
26///
27/// # Note on Async SDKs
28///
29/// If your KMS SDK is asynchronous (like `aws-sdk-kms` or Google Cloud KMS),
30/// do **not** block the thread using `block_on` inside this trait. Doing so
31/// can cause deadlocks on single-threaded runtimes and thread-pool exhaustion
32/// on multi-threaded ones.
33///
34/// Instead, implement the [`AsyncSigner`] trait and use `CertBuilder::sign_async`.
35///
36/// # Example — HashiCorp Vault Transit skeleton
37///
38/// ```rust,ignore
39/// use a1::Signer;
40/// use base64::{engine::general_purpose, Engine as _};
41///
42/// struct VaultSigner { vault_addr: String, key_name: String, public_key: VerifyingKey }
43///
44/// impl Signer for VaultSigner {
45///     fn verifying_key(&self) -> VerifyingKey { self.public_key }
46///
47///     fn sign_message(&self, msg: &[u8]) -> ed25519_dalek::Signature {
48///         let encoded = general_purpose::STANDARD.encode(msg);
49///         // POST /v1/transit/sign/{key_name} with {"input": encoded}
50///         // Parse "data.signature" from response, strip "vault:v1:" prefix,
51///         // base64-decode, convert to [u8; 64], return Signature::from_bytes(...)
52///         todo!()
53///     }
54/// }
55/// ```
56pub trait Signer: Send + Sync {
57    /// Return the Ed25519 public key corresponding to this signing backend.
58    fn verifying_key(&self) -> VerifyingKey;
59
60    /// Produce an Ed25519 signature over `msg`.
61    ///
62    /// `msg` is the raw domain-separated bytes produced internally by
63    /// [`DelegationCert::signable_bytes`] — never a pre-hashed digest.
64    ///
65    /// [`DelegationCert::signable_bytes`]: crate::cert::DelegationCert::signable_bytes
66    fn sign_message(&self, msg: &[u8]) -> Signature;
67}
68
69/// Asynchronous abstraction over an Ed25519 signing backend.
70///
71/// Use this trait to integrate cloud key management services (AWS KMS, Azure
72/// Key Vault, HashiCorp Vault, Google Cloud KMS) whose SDKs are strictly async.
73#[cfg(feature = "async")]
74#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
75#[async_trait::async_trait]
76pub trait AsyncSigner: Send + Sync {
77    /// Return the Ed25519 public key corresponding to this signing backend.
78    fn verifying_key(&self) -> VerifyingKey;
79
80    /// Produce an Ed25519 signature over `msg` asynchronously.
81    async fn sign_message(&self, msg: &[u8]) -> Signature;
82}
83
84/// In-process Ed25519 identity for development, testing, and single-process
85/// deployments.
86///
87/// The signing key is held in process memory and zeroized on drop.
88/// For production deployments at scale, implement [`Signer`] over your
89/// HSM or KMS so private key material never touches application memory.
90///
91/// # Cloning
92///
93/// This struct intentionally does **not** implement `Clone`. Copying private
94/// key material across memory locations defeats `ZeroizeOnDrop` protections.
95/// If you need to share an identity across multiple threads or async tasks,
96/// use the provided [`SharedIdentity`] convenience wrapper instead.
97#[derive(ZeroizeOnDrop)]
98pub struct DyoloIdentity {
99    signing_key: SigningKey,
100}
101
102impl DyoloIdentity {
103    /// Generate a new random identity using the OS entropy source.
104    pub fn generate() -> Self {
105        Self {
106            signing_key: SigningKey::generate(&mut OsRng),
107        }
108    }
109
110    /// Restore an identity from a 32-byte signing key seed.
111    ///
112    /// # Security
113    ///
114    /// Source these bytes from secure storage (e.g. an HSM export, an
115    /// encrypted secrets manager, or a PKCS#8 file). Zeroize the input
116    /// buffer after calling this function.
117    ///
118    /// ```rust,ignore
119    /// use zeroize::Zeroize;
120    /// let mut seed = fetch_seed_from_vault();
121    /// let identity = DyoloIdentity::from_signing_bytes(&seed);
122    /// seed.zeroize();
123    /// ```
124    pub fn from_signing_bytes(bytes: &[u8; 32]) -> Self {
125        Self {
126            signing_key: SigningKey::from_bytes(bytes),
127        }
128    }
129
130    /// Export the raw 32-byte signing key seed.
131    ///
132    /// # Security
133    ///
134    /// The returned value is a secret. Zeroize it after use.
135    /// Do not log, store unencrypted, or transmit across a network.
136    pub fn to_signing_bytes(&self) -> [u8; 32] {
137        self.signing_key.to_bytes()
138    }
139
140    /// The Ed25519 public key for this identity.
141    pub fn verifying_key(&self) -> VerifyingKey {
142        self.signing_key.verifying_key()
143    }
144
145    #[allow(dead_code)]
146    pub(crate) fn sign(&self, message: &[u8]) -> Signature {
147        self.signing_key.sign(message)
148    }
149}
150
151impl Signer for DyoloIdentity {
152    fn verifying_key(&self) -> VerifyingKey {
153        self.signing_key.verifying_key()
154    }
155
156    fn sign_message(&self, msg: &[u8]) -> Signature {
157        self.signing_key.sign(msg)
158    }
159}
160
161impl std::fmt::Debug for DyoloIdentity {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        let vk = self.verifying_key();
164        let b = vk.as_bytes();
165        write!(
166            f,
167            "DyoloIdentity(vk:{:02x}{:02x}{:02x}{:02x}…)",
168            b[0], b[1], b[2], b[3]
169        )
170    }
171}
172
173/// A thread-safe, clonable reference to a [`DyoloIdentity`].
174///
175/// Since [`DyoloIdentity`] cannot be cloned (to preserve zeroization guarantees),
176/// `SharedIdentity` provides an `Arc` wrapper that implements `Clone` and `Signer`.
177/// Useful for passing a single identity into multiple async workers.
178#[derive(Clone, Debug)]
179pub struct SharedIdentity(pub std::sync::Arc<DyoloIdentity>);
180
181impl Signer for SharedIdentity {
182    fn verifying_key(&self) -> VerifyingKey {
183        self.0.verifying_key()
184    }
185
186    fn sign_message(&self, msg: &[u8]) -> Signature {
187        Signer::sign_message(&*self.0, msg)
188    }
189}
190
191#[cfg(feature = "async")]
192#[async_trait::async_trait]
193impl AsyncSigner for SharedIdentity {
194    fn verifying_key(&self) -> VerifyingKey {
195        self.0.verifying_key()
196    }
197
198    async fn sign_message(&self, msg: &[u8]) -> Signature {
199        Signer::sign_message(&*self.0, msg)
200    }
201}
202
203#[cfg(feature = "async")]
204#[async_trait::async_trait]
205impl AsyncSigner for DyoloIdentity {
206    fn verifying_key(&self) -> VerifyingKey {
207        self.verifying_key()
208    }
209
210    async fn sign_message(&self, msg: &[u8]) -> Signature {
211        Signer::sign_message(self, msg)
212    }
213}
214
215/// A trait for HTTP clients to transmit signing requests to a KMS.
216#[cfg(feature = "async")]
217#[async_trait::async_trait]
218pub trait KmsHttpClient: Send + Sync {
219    /// Post a payload to the KMS endpoint and return the raw response bytes.
220    async fn post(
221        &self,
222        url: &str,
223        headers: &[(&str, &str)],
224        body: &[u8],
225    ) -> Result<Vec<u8>, crate::error::A1Error>;
226}
227
228/// Production-ready HashiCorp Vault Transit backend for A1 Passports.
229///
230/// Ensures private keys never touch application memory. Signatures are strictly
231/// bound to the `v2.8.0` cryptographic domain natively via the Vault `context`.
232#[cfg(feature = "async")]
233pub struct VaultSigner {
234    vault_addr: String,
235    token: String,
236    key_name: String,
237    public_key: VerifyingKey,
238    http_client: Box<dyn KmsHttpClient>,
239}
240
241#[cfg(feature = "async")]
242impl std::fmt::Debug for VaultSigner {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        f.debug_struct("VaultSigner")
245            .field("vault_addr", &self.vault_addr)
246            .field("key_name", &self.key_name)
247            .field("public_key", &hex::encode(self.public_key.as_bytes()))
248            .finish()
249    }
250}
251
252#[cfg(feature = "async")]
253impl VaultSigner {
254    pub fn new(
255        vault_addr: String,
256        token: String,
257        key_name: String,
258        public_key_hex: &str,
259        http_client: Box<dyn KmsHttpClient>,
260    ) -> Result<Self, crate::error::A1Error> {
261        let pk_bytes = hex::decode(public_key_hex)
262            .map_err(|_| crate::error::A1Error::WireFormatError("invalid hex".into()))?;
263        let public_key = VerifyingKey::from_bytes(
264            &pk_bytes
265                .try_into()
266                .map_err(|_| crate::error::A1Error::WireFormatError("must be 32 bytes".into()))?,
267        )
268        .map_err(|_| crate::error::A1Error::WireFormatError("invalid ed25519 key".into()))?;
269
270        Ok(Self {
271            vault_addr,
272            token,
273            key_name,
274            public_key,
275            http_client,
276        })
277    }
278}
279
280#[cfg(feature = "async")]
281#[async_trait::async_trait]
282impl AsyncSigner for VaultSigner {
283    fn verifying_key(&self) -> VerifyingKey {
284        self.public_key
285    }
286
287    async fn sign_message(&self, msg: &[u8]) -> Signature {
288        // Vault expects base64 encoded input for the transit engine
289        use base64::{engine::general_purpose, Engine as _};
290        let encoded_input = general_purpose::STANDARD.encode(msg);
291
292        // Structure the Vault Transit API request.
293        // The context parameter cryptographically binds the operation to the enforcer domain
294        // and persistently records the invocation marker in enterprise Vault audit logs.
295        let payload = format!(
296            "{{\"input\": \"{}\", \"context\": \"ZHlvbG9fdjIuOC4w\"}}",
297            encoded_input
298        );
299        let url = format!("{}/v1/transit/sign/{}", self.vault_addr, self.key_name);
300        let headers = [
301            ("X-Vault-Token", self.token.as_str()),
302            ("Content-Type", "application/json"),
303        ];
304
305        let resp_bytes = self
306            .http_client
307            .post(&url, &headers, payload.as_bytes())
308            .await
309            .expect("vault KMS post failed");
310
311        let resp_json: serde_json::Value =
312            serde_json::from_slice(&resp_bytes).expect("vault returned invalid JSON");
313
314        let sig_b64 = resp_json["data"]["signature"]
315            .as_str()
316            .expect("vault response missing data.signature")
317            .trim_start_matches("vault:v1:");
318
319        let sig_bytes = general_purpose::STANDARD
320            .decode(sig_b64)
321            .expect("vault signature base64 decode failed");
322
323        Signature::from_bytes(
324            &sig_bytes
325                .try_into()
326                .expect("vault signature must be 64 bytes"),
327        )
328    }
329}