Skip to main content

wimsey_jose/
key.rs

1//! Signing and verifying keys for the algorithms WIMSE credentials use.
2
3use ed25519_dalek::Signer as _;
4use p256::ecdsa::signature::Verifier as _;
5
6use crate::error::JoseError;
7
8/// A JOSE signature algorithm this workspace can produce and verify.
9///
10/// Both are asymmetric and, crucially, both are **deterministic**: Ed25519 by
11/// construction (RFC 8032) and ECDSA P-256 through the RFC 6979 nonce the
12/// `ecdsa` crate derives from the key and message. That is what lets a
13/// conformance vector record signature bytes at all.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum Algorithm {
17    /// `EdDSA` over Ed25519 (RFC 8037).
18    EdDsa,
19    /// `ES256`: ECDSA using P-256 and SHA-256 (RFC 7518 Section 3.4).
20    Es256,
21}
22
23impl Algorithm {
24    /// The JOSE `alg` value.
25    #[must_use]
26    pub const fn as_str(self) -> &'static str {
27        match self {
28            Self::EdDsa => "EdDSA",
29            Self::Es256 => "ES256",
30        }
31    }
32
33    /// Parses a JOSE `alg` value.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`JoseError::ForbiddenAlg`] for `none`, a symmetric algorithm or
38    /// an encryption algorithm — the families a proof of possession may never
39    /// use — and [`JoseError::UnsupportedAlg`] for anything else this crate
40    /// cannot produce.
41    pub fn parse(alg: &str) -> Result<Self, JoseError> {
42        match alg {
43            "EdDSA" => Ok(Self::EdDsa),
44            "ES256" => Ok(Self::Es256),
45            other if crate::error::is_forbidden_alg(other) => Err(JoseError::ForbiddenAlg {
46                found: other.to_owned(),
47            }),
48            other => Err(JoseError::UnsupportedAlg {
49                found: other.to_owned(),
50            }),
51        }
52    }
53}
54
55/// The length of a JOSE signature for either supported algorithm.
56///
57/// Ed25519 signatures are 64 bytes (RFC 8032) and an ES256 signature is `R || S`
58/// with each half 32 bytes (RFC 7518 Section 3.4). They coincide, which lets the
59/// token formats carry one fixed-size signature. Adding ES384 or ES512 would
60/// break that assumption, and every use of this constant is where it breaks.
61pub const SIGNATURE_LEN: usize = 64;
62
63/// A private key that can sign.
64#[derive(Debug, Clone)]
65#[non_exhaustive]
66pub enum SigningKey {
67    /// An Ed25519 signing key.
68    Ed25519(Box<ed25519_dalek::SigningKey>),
69    /// A P-256 signing key.
70    P256(Box<p256::ecdsa::SigningKey>),
71}
72
73impl SigningKey {
74    /// Builds an Ed25519 signing key from its 32-byte seed.
75    #[must_use]
76    pub fn from_ed25519_seed(seed: &[u8; 32]) -> Self {
77        Self::Ed25519(Box::new(ed25519_dalek::SigningKey::from_bytes(seed)))
78    }
79
80    /// Builds a P-256 signing key from its 32-byte scalar.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`JoseError::InvalidKey`] if the bytes are not a valid non-zero
85    /// scalar below the curve order.
86    pub fn from_p256_scalar(scalar: &[u8; 32]) -> Result<Self, JoseError> {
87        p256::ecdsa::SigningKey::from_bytes(scalar.into())
88            .map(|key| Self::P256(Box::new(key)))
89            .map_err(|_| JoseError::InvalidKey)
90    }
91
92    /// The algorithm this key signs with.
93    #[must_use]
94    pub const fn algorithm(&self) -> Algorithm {
95        match self {
96            Self::Ed25519(_) => Algorithm::EdDsa,
97            Self::P256(_) => Algorithm::Es256,
98        }
99    }
100
101    /// The matching public key.
102    #[must_use]
103    pub fn verifying_key(&self) -> VerifyingKey {
104        match self {
105            Self::Ed25519(key) => VerifyingKey::Ed25519(Box::new(key.verifying_key())),
106            Self::P256(key) => VerifyingKey::P256(Box::new(*key.verifying_key())),
107        }
108    }
109
110    /// The private scalar or seed, as the 32 bytes it is stored from.
111    #[must_use]
112    pub fn to_bytes(&self) -> [u8; 32] {
113        match self {
114            Self::Ed25519(key) => key.to_bytes(),
115            Self::P256(key) => key.to_bytes().into(),
116        }
117    }
118
119    /// Signs `message`, producing [`SIGNATURE_LEN`] bytes.
120    ///
121    /// Deterministic for both algorithms, so the same key and message always
122    /// produce the same bytes.
123    #[must_use]
124    pub fn sign(&self, message: &[u8]) -> [u8; SIGNATURE_LEN] {
125        match self {
126            Self::Ed25519(key) => key.sign(message).to_bytes(),
127            Self::P256(key) => {
128                let signature: p256::ecdsa::Signature = key.sign(message);
129                signature.to_bytes().into()
130            }
131        }
132    }
133}
134
135/// A public key that can verify.
136#[derive(Debug, Clone, PartialEq, Eq)]
137#[non_exhaustive]
138pub enum VerifyingKey {
139    /// An Ed25519 verifying key.
140    Ed25519(Box<ed25519_dalek::VerifyingKey>),
141    /// A P-256 verifying key.
142    P256(Box<p256::ecdsa::VerifyingKey>),
143}
144
145impl VerifyingKey {
146    /// The algorithm this key verifies.
147    #[must_use]
148    pub const fn algorithm(&self) -> Algorithm {
149        match self {
150            Self::Ed25519(_) => Algorithm::EdDsa,
151            Self::P256(_) => Algorithm::Es256,
152        }
153    }
154
155    /// The raw public key bytes: the 32-byte Ed25519 point, or the 65-byte
156    /// uncompressed SEC1 encoding of a P-256 point.
157    ///
158    /// The two are different lengths and neither is self-describing, so
159    /// [`VerifyingKey::from_raw_bytes`] has to be told which algorithm it is
160    /// reading. Prefer a [`Jwk`](crate::Jwk) anywhere the algorithm has to
161    /// travel with the key.
162    #[must_use]
163    pub fn to_raw_bytes(&self) -> Vec<u8> {
164        match self {
165            Self::Ed25519(key) => key.to_bytes().to_vec(),
166            Self::P256(key) => key.to_sec1_point(false).as_bytes().to_vec(),
167        }
168    }
169
170    /// Parses raw public key bytes for `algorithm`.
171    ///
172    /// # Errors
173    ///
174    /// Returns [`JoseError::InvalidKey`] if the bytes are the wrong length for
175    /// the algorithm, or are not a valid point.
176    pub fn from_raw_bytes(algorithm: Algorithm, bytes: &[u8]) -> Result<Self, JoseError> {
177        match algorithm {
178            Algorithm::EdDsa => {
179                let bytes: [u8; 32] = bytes.try_into().map_err(|_| JoseError::InvalidKey)?;
180                ed25519_dalek::VerifyingKey::from_bytes(&bytes)
181                    .map(|key| Self::Ed25519(Box::new(key)))
182                    .map_err(|_| JoseError::InvalidKey)
183            }
184            Algorithm::Es256 => p256::ecdsa::VerifyingKey::from_sec1_bytes(bytes)
185                .map(|key| Self::P256(Box::new(key)))
186                .map_err(|_| JoseError::InvalidKey),
187        }
188    }
189
190    /// Verifies `signature` over `message`.
191    ///
192    /// # Errors
193    ///
194    /// Returns [`JoseError::InvalidSignature`] if it does not verify.
195    pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), JoseError> {
196        let signature: [u8; SIGNATURE_LEN] = signature
197            .try_into()
198            .map_err(|_| JoseError::InvalidSignature)?;
199        match self {
200            Self::Ed25519(key) => key
201                // `verify_strict` rejects small-order and torsion components,
202                // which plain `verify` accepts; two verifiers disagreeing about
203                // one signature is exactly the interop break to avoid.
204                .verify_strict(message, &ed25519_dalek::Signature::from_bytes(&signature))
205                .map_err(|_| JoseError::InvalidSignature),
206            Self::P256(key) => {
207                let signature = p256::ecdsa::Signature::from_bytes(&signature.into())
208                    .map_err(|_| JoseError::InvalidSignature)?;
209                key.verify(message, &signature)
210                    .map_err(|_| JoseError::InvalidSignature)
211            }
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::{Algorithm, SigningKey, VerifyingKey};
219    use crate::error::JoseError;
220
221    fn keys() -> [SigningKey; 2] {
222        [
223            SigningKey::from_ed25519_seed(&[7u8; 32]),
224            SigningKey::from_p256_scalar(&[7u8; 32]).unwrap(),
225        ]
226    }
227
228    #[test]
229    fn round_trips_for_both_algorithms() {
230        for key in keys() {
231            let signature = key.sign(b"payload");
232            assert!(key.verifying_key().verify(b"payload", &signature).is_ok());
233            assert!(key.verifying_key().verify(b"other", &signature).is_err());
234        }
235    }
236
237    // The whole conformance-vector design rests on this: a recorded signature is
238    // only a contract if signing is reproducible.
239    #[test]
240    fn signing_is_deterministic_for_both_algorithms() {
241        for key in keys() {
242            assert_eq!(key.sign(b"payload"), key.sign(b"payload"));
243        }
244    }
245
246    #[test]
247    fn a_signature_does_not_verify_under_the_other_algorithm() {
248        let [ed, p256] = keys();
249        let signature = ed.sign(b"payload");
250        assert!(p256.verifying_key().verify(b"payload", &signature).is_err());
251    }
252
253    #[test]
254    fn rejects_a_wrong_length_signature() {
255        for key in keys() {
256            let err = key.verifying_key().verify(b"payload", &[0u8; 32]);
257            assert!(matches!(err, Err(JoseError::InvalidSignature)));
258        }
259    }
260
261    #[test]
262    fn parses_the_supported_algorithms() {
263        assert_eq!(Algorithm::parse("EdDSA").unwrap(), Algorithm::EdDsa);
264        assert_eq!(Algorithm::parse("ES256").unwrap(), Algorithm::Es256);
265        assert_eq!(Algorithm::EdDsa.as_str(), "EdDSA");
266        assert_eq!(Algorithm::Es256.as_str(), "ES256");
267    }
268
269    // A forbidden algorithm and an unimplemented one are different failures: one
270    // is a spec violation, the other is this crate's limit.
271    #[test]
272    fn separates_forbidden_from_merely_unsupported() {
273        for forbidden in ["none", "HS256", "ECDH-ES+A128KW"] {
274            assert!(
275                matches!(
276                    Algorithm::parse(forbidden),
277                    Err(JoseError::ForbiddenAlg { .. })
278                ),
279                "{forbidden} must be forbidden"
280            );
281        }
282        assert!(matches!(
283            Algorithm::parse("ES384"),
284            Err(JoseError::UnsupportedAlg { .. })
285        ));
286    }
287
288    #[test]
289    fn rejects_an_invalid_p256_scalar() {
290        assert!(matches!(
291            SigningKey::from_p256_scalar(&[0u8; 32]),
292            Err(JoseError::InvalidKey)
293        ));
294    }
295
296    #[test]
297    fn reports_its_algorithm() {
298        let [ed, p256] = keys();
299        assert_eq!(ed.algorithm(), Algorithm::EdDsa);
300        assert_eq!(p256.algorithm(), Algorithm::Es256);
301        assert_eq!(ed.verifying_key().algorithm(), Algorithm::EdDsa);
302        assert_eq!(p256.verifying_key().algorithm(), Algorithm::Es256);
303    }
304
305    #[test]
306    fn keys_round_trip_through_their_bytes() {
307        let [ed, p256] = keys();
308        assert_eq!(
309            SigningKey::from_ed25519_seed(&ed.to_bytes()).verifying_key(),
310            ed.verifying_key()
311        );
312        assert_eq!(
313            SigningKey::from_p256_scalar(&p256.to_bytes())
314                .unwrap()
315                .verifying_key(),
316            p256.verifying_key()
317        );
318    }
319
320    #[test]
321    fn raw_public_key_bytes_round_trip() {
322        for key in keys() {
323            let public = key.verifying_key();
324            let raw = public.to_raw_bytes();
325            assert_eq!(
326                VerifyingKey::from_raw_bytes(public.algorithm(), &raw).unwrap(),
327                public
328            );
329        }
330        // The encodings are different lengths, which is why the algorithm has
331        // to be supplied rather than inferred.
332        let [ed, p256] = keys();
333        assert_eq!(ed.verifying_key().to_raw_bytes().len(), 32);
334        assert_eq!(p256.verifying_key().to_raw_bytes().len(), 65);
335    }
336
337    #[test]
338    fn rejects_raw_bytes_of_the_wrong_length() {
339        assert!(matches!(
340            VerifyingKey::from_raw_bytes(Algorithm::EdDsa, &[0u8; 65]),
341            Err(JoseError::InvalidKey)
342        ));
343        assert!(matches!(
344            VerifyingKey::from_raw_bytes(Algorithm::Es256, &[0u8; 32]),
345            Err(JoseError::InvalidKey)
346        ));
347    }
348
349    #[test]
350    fn verifying_keys_compare_by_value() {
351        let [ed, _] = keys();
352        let same: VerifyingKey = SigningKey::from_ed25519_seed(&[7u8; 32]).verifying_key();
353        assert_eq!(ed.verifying_key(), same);
354    }
355}