cratestack_cose/keys/external.rs
1//! A signer whose key lives outside this process (cratestack#1007).
2//!
3//! [`ExternalSigner::esp256`] for a P-256 key in a platform keystore, and
4//! [`ExternalSigner::ed25519`] for an Ed25519 key behind a callback (a Node
5//! `KeyObject` that cannot be exported, a KMS, a hardware token).
6
7use std::fmt;
8use std::future::Future;
9use std::sync::Arc;
10
11use cratestack_core::CratestackError;
12
13use super::external_signature::{ed25519_signature, raw_signature};
14use super::traits::CoseSigner;
15use super::verify::esp256_low_s;
16use super::verify_key::CoseVerifyKey;
17use crate::alg::CoseAlg;
18use crate::maybe_send::{BoxFuture, MaybeSend, MaybeSendSync};
19use crate::thumbprint::KID_LEN;
20
21#[cfg(not(target_arch = "wasm32"))]
22type SignFn =
23 dyn Fn(Vec<u8>) -> BoxFuture<'static, Result<Vec<u8>, CratestackError>> + Send + Sync + 'static;
24#[cfg(target_arch = "wasm32")]
25type SignFn = dyn Fn(Vec<u8>) -> BoxFuture<'static, Result<Vec<u8>, CratestackError>> + 'static;
26
27/// A signer that hands the bytes to a callback: a key in the Android Keystore
28/// or the iOS Secure Enclave, a KMS, a hardware token.
29///
30/// The key is never exported; the callback is asked to sign the complete
31/// `Sig_structure` and nothing else. On `wasm32` neither the callback nor its
32/// future needs to be `Send` (see [`CoseSigner`]).
33///
34/// An Ed25519 key behind a callback, with the platform's own primitive
35/// standing in for the key store (here, `Ed25519Signer`):
36///
37/// ```
38/// use cratestack_cose::{CoseAlg, CoseSigner, Ed25519Signer, ExternalSigner};
39///
40/// let key = Ed25519Signer::from_seed(&[7; 32]);
41/// let public = key.verify_key().ed25519_bytes().unwrap();
42/// let signer = ExternalSigner::ed25519(&public, move |tbs| {
43/// let key = key.clone();
44/// async move { key.sign(&tbs).await }
45/// })
46/// .unwrap();
47/// assert_eq!(signer.alg(), CoseAlg::Ed25519);
48/// assert_eq!(signer.kid().len(), 8);
49/// ```
50///
51/// A P-256 key:
52///
53/// ```
54/// use cratestack_cose::{CoseAlg, CoseSigner, ExternalSigner, P256Signer};
55///
56/// // A stand-in for a platform keystore.
57/// let key = P256Signer::from_scalar(&[7; 32]).unwrap();
58/// let public = key.verify_key().p256_sec1_uncompressed().unwrap();
59/// let signer = ExternalSigner::esp256(&public, move |tbs| {
60/// let key = key.clone();
61/// async move { key.sign(&tbs).await }
62/// })
63/// .unwrap();
64/// assert_eq!(signer.alg(), CoseAlg::Esp256);
65/// assert_eq!(signer.kid().len(), 8);
66/// ```
67#[derive(Clone)]
68pub struct ExternalSigner {
69 alg: CoseAlg,
70 kid: [u8; KID_LEN],
71 /// The public half the callback is held to: a signature it returns that
72 /// does not verify under it is refused here, not left to fail at the
73 /// peer as the coarse, unexplained `401`.
74 key: CoseVerifyKey,
75 sign: Arc<SignFn>,
76}
77
78impl ExternalSigner {
79 /// An ESP256 (`-9`) signer for the P-256 key whose public half is
80 /// `public_key_sec1` (compressed or uncompressed SEC1). The `kid` is the
81 /// key's RFC 9679 thumbprint prefix, computed here so the caller cannot
82 /// get it wrong.
83 ///
84 /// `sign` receives the full to-be-signed bytes and must sign them with
85 /// ECDSA over SHA-256 (`SHA256withECDSA` on Android, `ecdsaSignatureMessageX962SHA256`
86 /// on iOS: both hash for you, so pass the bytes as given, never a
87 /// digest). It may return the signature DER encoded, which is what both
88 /// platform keystores produce, or as the raw 64-byte `r ‖ s`. A DER
89 /// signature is converted; the envelope then normalises to low-`s`. A
90 /// signature that does not verify under `public_key_sec1` is refused
91 /// here, on every call, instead of failing at the peer as the coarse `401`.
92 ///
93 /// Fails with `CratestackError::Validation` if `public_key_sec1` is not a
94 /// point on the curve.
95 pub fn esp256<F, Fut>(public_key_sec1: &[u8], sign: F) -> Result<Self, CratestackError>
96 where
97 F: Fn(Vec<u8>) -> Fut + MaybeSendSync + 'static,
98 Fut: Future<Output = Result<Vec<u8>, CratestackError>> + MaybeSend + 'static,
99 {
100 let key = CoseVerifyKey::p256_sec1(public_key_sec1)?;
101 let kid = key.kid();
102 Ok(Self {
103 alg: CoseAlg::Esp256,
104 kid,
105 key,
106 sign: Arc::new(move |tbs| Box::pin(sign(tbs))),
107 })
108 }
109
110 /// An Ed25519 (`-19`) signer for the key whose public half is
111 /// `public_key`. The `kid` is the key's RFC 9679 thumbprint prefix,
112 /// computed here so the caller cannot get it wrong.
113 ///
114 /// `sign` receives the full to-be-signed bytes and must return the
115 /// 64-byte Ed25519 signature over exactly those bytes (pure Ed25519, as
116 /// RFC 8032 defines it, never a pre-hash of them), which is what a Node
117 /// `crypto.sign(null, tbs, key)` or a KMS `ED25519` key produces. Any
118 /// other length is refused as the signer's failure, and so is a
119 /// signature that does not verify under `public_key`, which is checked
120 /// here on every call instead of failing at the peer as the coarse `401`.
121 ///
122 /// Fails with `CratestackError::Validation` if `public_key` is not a
123 /// point encoding.
124 pub fn ed25519<F, Fut>(public_key: &[u8; 32], sign: F) -> Result<Self, CratestackError>
125 where
126 F: Fn(Vec<u8>) -> Fut + MaybeSendSync + 'static,
127 Fut: Future<Output = Result<Vec<u8>, CratestackError>> + MaybeSend + 'static,
128 {
129 let key = CoseVerifyKey::ed25519(public_key)?;
130 let kid = key.kid();
131 Ok(Self {
132 alg: CoseAlg::Ed25519,
133 kid,
134 key,
135 sign: Arc::new(move |tbs| Box::pin(sign(tbs))),
136 })
137 }
138}
139
140impl fmt::Debug for ExternalSigner {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 f.debug_struct("ExternalSigner")
143 .field("alg", &self.alg)
144 .field("kid", &self.kid)
145 .finish_non_exhaustive()
146 }
147}
148
149#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
150#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
151impl CoseSigner for ExternalSigner {
152 fn alg(&self) -> CoseAlg {
153 self.alg
154 }
155
156 fn kid(&self) -> &[u8] {
157 &self.kid
158 }
159
160 async fn sign(&self, to_be_signed: &[u8]) -> Result<Vec<u8>, CratestackError> {
161 let signature = (self.sign)(to_be_signed.to_vec()).await?;
162 let signature = match self.alg {
163 CoseAlg::Ed25519 => ed25519_signature(signature)?,
164 _ => raw_signature(&signature)?,
165 };
166 // ESP256 is checked as the envelope will send it, with a low `s`.
167 let checked = match self.alg {
168 CoseAlg::Ed25519 => Some(signature.clone()),
169 _ => esp256_low_s(&signature),
170 };
171 match checked {
172 Some(checked) if self.key.verify_message(self.alg, to_be_signed, &checked) => {
173 Ok(signature)
174 }
175 _ => Err(CratestackError::Internal(
176 "the external signer's signature does not verify under the public key it was \
177 built with"
178 .to_owned(),
179 )),
180 }
181 }
182}