Skip to main content

cratestack_cose/keys/
signers.rs

1//! In-process COSE_Sign1 signers. A key held in a KMS or an HSM implements
2//! [`CoseSigner`] itself instead.
3
4use std::fmt;
5
6use cratestack_core::CratestackError;
7// `ed25519-dalek` 3 and `p256` 0.14 both re-export `signature` 3's traits.
8use ed25519_dalek::Signer as _;
9use ed25519_dalek::ed25519::signature::MultipartSigner as _;
10use p256::ecdsa::signature::DigestSigner as _;
11use sha2::{Digest, Sha256};
12
13use super::traits::CoseSigner;
14use super::verify_key::CoseVerifyKey;
15use crate::alg::CoseAlg;
16use crate::thumbprint::KID_LEN;
17
18/// An Ed25519 (`-19`) signer. Ed25519 is deterministic, so the shared
19/// vectors compare its output byte for byte.
20///
21/// It signs the to-be-signed structure in pieces, like the HMAC and ESP256
22/// signers, without the envelope building it in one buffer (maintainer
23/// decision on cratestack#1005, 2026-09-25). PureEdDSA (RFC 8032) is not a
24/// pre-hash scheme: it hashes the message twice, once for the nonce `r` and
25/// once for the challenge `k`. `ed25519-dalek`'s `MultipartSigner` runs both
26/// passes over the same slices, the computation its `hazmat` module exposes
27/// as `raw_sign_byupdate`, but with the secret expanded the standard way
28/// inside the crate. Reaching it through the safe trait needs no `hazmat`
29/// feature, and so no code here derives an `ExpandedSecretKey` itself,
30/// which is the misuse that module warns can leak the key. The result is
31/// byte for byte what signing the concatenation gives
32/// (`tests/ed25519_streaming.rs` checks every vector and a range of sizes).
33#[derive(Clone)]
34pub struct Ed25519Signer {
35    key: ed25519_dalek::SigningKey,
36    kid: [u8; KID_LEN],
37}
38
39impl Ed25519Signer {
40    /// The signer for the RFC 8032 secret key `seed` (32 random bytes). The
41    /// key is expanded here, and wiped when the signer is dropped.
42    pub fn from_seed(seed: &[u8; 32]) -> Self {
43        let key = ed25519_dalek::SigningKey::from_bytes(seed);
44        let kid = CoseVerifyKey::from_ed25519(key.verifying_key()).kid();
45        Self { key, kid }
46    }
47
48    /// The matching verification key, for a resolver.
49    pub fn verify_key(&self) -> CoseVerifyKey {
50        CoseVerifyKey::from_ed25519(self.key.verifying_key())
51    }
52}
53
54impl fmt::Debug for Ed25519Signer {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.debug_struct("Ed25519Signer")
57            .field("kid", &self.kid)
58            .finish_non_exhaustive()
59    }
60}
61
62#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
63#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
64impl CoseSigner for Ed25519Signer {
65    fn alg(&self) -> CoseAlg {
66        CoseAlg::Ed25519
67    }
68
69    fn kid(&self) -> &[u8] {
70        &self.kid
71    }
72
73    async fn sign(&self, to_be_signed: &[u8]) -> Result<Vec<u8>, CratestackError> {
74        Ok(self.key.sign(to_be_signed).to_bytes().to_vec())
75    }
76
77    /// Both PureEdDSA passes over the pieces (see the type's docs).
78    fn sign_chunks(&self, to_be_signed: &[&[u8]]) -> Option<Result<Vec<u8>, CratestackError>> {
79        Some(
80            self.key
81                .try_multipart_sign(to_be_signed)
82                .map(|signature| signature.to_bytes().to_vec())
83                .map_err(|_| CratestackError::Internal("Ed25519 signing failed".to_owned())),
84        )
85    }
86}
87
88/// An ESP256 (`-9`) signer from a 32-byte private scalar.
89///
90/// Signatures are deterministic (RFC 6979), so this signer's output is
91/// reproducible. What it returns may have a high `s`: the envelope
92/// normalises every ESP256 signature to low-`s` after the signer returns,
93/// whichever signer it was (this one, a KMS, WebCrypto), because the
94/// verifier accepts low-`s` only. ECDSA verifiers do not care how `k` was
95/// chosen, so a randomized signature from a KMS verifies the same way once
96/// normalised.
97#[derive(Clone)]
98pub struct P256Signer {
99    key: p256::ecdsa::SigningKey,
100    kid: [u8; KID_LEN],
101}
102
103impl P256Signer {
104    /// Fails with `CratestackError::Validation` for a scalar that is zero
105    /// or not below the group order.
106    pub fn from_scalar(scalar: &[u8; 32]) -> Result<Self, CratestackError> {
107        let key = p256::ecdsa::SigningKey::from_slice(scalar)
108            .map_err(|_| CratestackError::Validation("invalid P-256 private key".to_owned()))?;
109        let kid = CoseVerifyKey::from_p256(*key.verifying_key()).kid();
110        Ok(Self { key, kid })
111    }
112
113    /// The matching verification key, for a resolver.
114    pub fn verify_key(&self) -> CoseVerifyKey {
115        CoseVerifyKey::from_p256(*self.key.verifying_key())
116    }
117
118    fn sign_with(
119        &self,
120        feed: impl Fn(&mut Sha256),
121    ) -> Result<p256::ecdsa::Signature, CratestackError> {
122        self.key
123            .try_sign_digest(|digest: &mut Sha256| {
124                feed(digest);
125                Ok(())
126            })
127            .map_err(|_| CratestackError::Internal("P-256 signing failed".to_owned()))
128    }
129}
130
131impl fmt::Debug for P256Signer {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        f.debug_struct("P256Signer")
134            .field("kid", &self.kid)
135            .finish_non_exhaustive()
136    }
137}
138
139#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
140#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
141impl CoseSigner for P256Signer {
142    fn alg(&self) -> CoseAlg {
143        CoseAlg::Esp256
144    }
145
146    fn kid(&self) -> &[u8] {
147        &self.kid
148    }
149
150    async fn sign(&self, to_be_signed: &[u8]) -> Result<Vec<u8>, CratestackError> {
151        let signature = self.sign_with(|digest| Digest::update(digest, to_be_signed))?;
152        Ok(signature.to_bytes().to_vec())
153    }
154
155    /// ESP256 signs SHA-256 of the message, so the structure is hashed in
156    /// pieces. The result is the one [`sign`](Self::sign) gives for
157    /// the concatenation (RFC 6979 derives `k` from the same digest).
158    fn sign_chunks(&self, to_be_signed: &[&[u8]]) -> Option<Result<Vec<u8>, CratestackError>> {
159        Some(
160            self.sign_with(|digest| {
161                for chunk in to_be_signed {
162                    Digest::update(digest, chunk);
163                }
164            })
165            .map(|signature| signature.to_bytes().to_vec()),
166        )
167    }
168}