Skip to main content

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}