Skip to main content

actpub_httpsig/key/
rsa.rs

1//! RSA PKCS#1 v1.5 SHA-256 signing and verification, backed by [`aws_lc_rs`].
2//!
3//! RSA with PKCS#1 v1.5 SHA-256 is the original Cavage HTTP-Signatures
4//! algorithm used across the Fediverse (Mastodon, Pleroma, Lemmy, Misskey,
5//! …). `aws-lc-rs` provides a constant-time implementation that avoids
6//! [RUSTSEC-2023-0071] affecting the pure-Rust `rsa` crate.
7//!
8//! [RUSTSEC-2023-0071]: https://rustsec.org/advisories/RUSTSEC-2023-0071.html
9
10use std::fmt;
11
12use aws_lc_rs::encoding::AsDer;
13use aws_lc_rs::rand::SystemRandom;
14use aws_lc_rs::rsa::{KeyPair as RsaKeyPair, KeySize};
15use aws_lc_rs::signature::{self, KeyPair as SignatureKeyPair, UnparsedPublicKey};
16
17use crate::error::Error;
18
19/// Supported RSA key sizes. Fediverse actors use 2048 by default; Mastodon
20/// allows 4096 and other implementations occasionally go higher.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub enum RsaBits {
24    /// 2048-bit modulus, the Mastodon default.
25    Rsa2048,
26    /// 4096-bit modulus.
27    Rsa4096,
28}
29
30impl RsaBits {
31    /// Returns the numeric bit length.
32    #[must_use]
33    pub const fn as_u32(self) -> u32 {
34        match self {
35            Self::Rsa2048 => 2048,
36            Self::Rsa4096 => 4096,
37        }
38    }
39
40    const fn as_key_size(self) -> KeySize {
41        match self {
42            Self::Rsa2048 => KeySize::Rsa2048,
43            Self::Rsa4096 => KeySize::Rsa4096,
44        }
45    }
46}
47
48/// An RSA key pair capable of producing PKCS#1 v1.5 SHA-256 signatures.
49///
50/// Internally stores the `aws-lc-rs` `rsa::KeyPair` for signing, together
51/// with the original PKCS#8 DER so that the key can be serialised back
52/// out symmetrically (Mastodon and friends distribute PEM-wrapped
53/// PKCS#8). The modulus width in bits is cached for convenience.
54pub struct RsaSigningKey {
55    inner: RsaKeyPair,
56    pkcs8_der: Vec<u8>,
57    public_spki_der: Vec<u8>,
58    bits: u32,
59}
60
61impl RsaSigningKey {
62    /// Generates a fresh RSA key pair of the requested size.
63    ///
64    /// # Errors
65    ///
66    /// Returns [`Error::KeyGeneration`] on RNG or key-scheduling failure.
67    pub fn generate(bits: RsaBits) -> Result<Self, Error> {
68        let pair = RsaKeyPair::generate(bits.as_key_size())
69            .map_err(|_| Error::KeyGeneration("RSA generation failed"))?;
70        let pkcs8_der = pair
71            .as_der()
72            .map_err(|_| Error::KeyGeneration("RSA PKCS#8 v1 serialisation failed"))?
73            .as_ref()
74            .to_vec();
75        Self::build(pair, pkcs8_der, bits.as_u32())
76    }
77
78    /// Loads an RSA key pair from a PKCS#8 DER blob.
79    ///
80    /// Accepts any 256-bit-aligned modulus width in the
81    /// `2048..=8192` range, matching the backend's
82    /// `RSA_PKCS1_2048_8192_SHA256` verification profile. The lower
83    /// bound is the NIST SP 800-131A minimum and the upper bound is
84    /// the largest key size the backend supports; values outside the
85    /// range are rejected, as are odd widths that cannot represent
86    /// valid RSA moduli.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`Error::InvalidPkcs8`] if the DER cannot be decoded
91    /// as an RSA `PrivateKeyInfo`, and [`Error::UnsupportedRsaSize`]
92    /// for any other width.
93    pub fn from_pkcs8_der(der: &[u8]) -> Result<Self, Error> {
94        let pair =
95            RsaKeyPair::from_pkcs8(der).map_err(|e| Error::InvalidPkcs8(format!("RSA: {e}")))?;
96        let bits = u32::try_from(pair.public_modulus_len() * 8).unwrap_or(u32::MAX);
97        if !(2048..=8192).contains(&bits) || bits % 256 != 0 {
98            return Err(Error::UnsupportedRsaSize(bits));
99        }
100        Self::build(pair, der.to_vec(), bits)
101    }
102
103    /// Builds the struct, computing the SPKI DER once at construction.
104    fn build(pair: RsaKeyPair, pkcs8_der: Vec<u8>, bits: u32) -> Result<Self, Error> {
105        // `signature::KeyPair::public_key()` → `&rsa::PublicKey`, whose
106        // `AsDer<PublicKeyX509Der>` impl produces the SubjectPublicKeyInfo
107        // we distribute via `publicKey.publicKeyPem`.
108        let spki = pair
109            .public_key()
110            .as_der()
111            .map_err(|_| Error::Crypto("RSA SPKI serialisation failed"))?
112            .as_ref()
113            .to_vec();
114        Ok(Self {
115            inner: pair,
116            pkcs8_der,
117            public_spki_der: spki,
118            bits,
119        })
120    }
121
122    /// Returns the PKCS#8 v1 DER encoding of the private key.
123    #[must_use]
124    pub fn to_pkcs8_der(&self) -> &[u8] {
125        &self.pkcs8_der
126    }
127
128    /// Returns the modulus length in bits.
129    #[must_use]
130    pub const fn bits(&self) -> u32 {
131        self.bits
132    }
133
134    /// Returns the public half of this key pair.
135    #[must_use]
136    pub fn public_key(&self) -> RsaPublicKey {
137        RsaPublicKey {
138            spki_der: self.public_spki_der.clone(),
139        }
140    }
141
142    /// Signs `message` using RSA PKCS#1 v1.5 SHA-256.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`Error::Crypto`] if the low-level primitive fails, which
147    /// only happens on internal allocator exhaustion.
148    pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, Error> {
149        let rng = SystemRandom::new();
150        let mut sig = vec![0u8; self.inner.public_modulus_len()];
151        self.inner
152            .sign(&signature::RSA_PKCS1_SHA256, &rng, message, &mut sig)
153            .map_err(|_| Error::Crypto("RSA PKCS#1 SHA-256 signing failed"))?;
154        Ok(sig)
155    }
156}
157
158impl fmt::Debug for RsaSigningKey {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        f.debug_struct("RsaSigningKey")
161            .field("bits", &self.bits)
162            .finish_non_exhaustive()
163    }
164}
165
166/// The verifying half of an RSA key pair.
167#[derive(Clone)]
168pub struct RsaPublicKey {
169    spki_der: Vec<u8>,
170}
171
172impl RsaPublicKey {
173    /// Wraps a raw `SubjectPublicKeyInfo` DER blob.
174    ///
175    /// # Errors
176    ///
177    /// Returns [`Error::InvalidPkcs8`] if the DER is truncated.
178    pub fn from_spki_der(der: &[u8]) -> Result<Self, Error> {
179        if der.is_empty() {
180            return Err(Error::InvalidPkcs8("empty SPKI DER".into()));
181        }
182        Ok(Self {
183            spki_der: der.to_vec(),
184        })
185    }
186
187    /// Returns the `SubjectPublicKeyInfo` DER representation.
188    #[must_use]
189    pub fn as_spki_der(&self) -> &[u8] {
190        &self.spki_der
191    }
192
193    /// Verifies an RSA PKCS#1 v1.5 SHA-256 signature of `message`.
194    ///
195    /// Accepts signatures created by any SHA-256-based PKCS#1 v1.5 RSA key
196    /// in the 2048–8192 bit range, matching what `aws-lc-rs` considers
197    /// interoperable.
198    ///
199    /// # Errors
200    ///
201    /// Returns [`Error::VerificationFailed`] if the signature is invalid
202    /// or the key is malformed.
203    pub fn verify(&self, message: &[u8], signature_bytes: &[u8]) -> Result<(), Error> {
204        UnparsedPublicKey::new(
205            &signature::RSA_PKCS1_2048_8192_SHA256,
206            self.spki_der.as_slice(),
207        )
208        .verify(message, signature_bytes)
209        .map_err(|_| Error::VerificationFailed)
210    }
211}
212
213impl fmt::Debug for RsaPublicKey {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        f.debug_struct("RsaPublicKey")
216            .field("spki_bytes", &self.spki_der.len())
217            .finish_non_exhaustive()
218    }
219}
220
221impl PartialEq for RsaPublicKey {
222    fn eq(&self, other: &Self) -> bool {
223        self.spki_der == other.spki_der
224    }
225}
226
227impl Eq for RsaPublicKey {}
228
229#[cfg(test)]
230mod tests {
231    use pretty_assertions::assert_eq;
232
233    use super::*;
234
235    /// Generating a fresh 4096-bit RSA key is slow (~500 ms on CI), so we
236    /// use 2048 for speed unless a specific test exercises the larger size.
237    fn fresh_key() -> RsaSigningKey {
238        RsaSigningKey::generate(RsaBits::Rsa2048).expect("rng available")
239    }
240
241    #[test]
242    fn generate_then_sign_and_verify_roundtrips() {
243        let key = fresh_key();
244        let public = key.public_key();
245        let msg = b"ActivityPub inbox delivery";
246        let sig = key.sign(msg).expect("sign must succeed");
247        // 2048-bit key produces 256-byte signature.
248        assert_eq!(sig.len(), 256);
249        public.verify(msg, &sig).expect("signature must verify");
250    }
251
252    #[test]
253    fn tampered_message_fails_verification() {
254        let key = fresh_key();
255        let public = key.public_key();
256        let sig = key.sign(b"original message").expect("sign");
257        let err = public
258            .verify(b"tampered message", &sig)
259            .expect_err("tampered message must not verify");
260        assert!(matches!(err, Error::VerificationFailed));
261    }
262
263    #[test]
264    fn pkcs8_roundtrip_preserves_key() {
265        let original = fresh_key();
266        let reloaded =
267            RsaSigningKey::from_pkcs8_der(original.to_pkcs8_der()).expect("reload must succeed");
268        assert_eq!(original.bits(), reloaded.bits());
269        // Round-trip signing and cross-verification.
270        let msg = b"cross-verify me";
271        let sig = reloaded.sign(msg).expect("sign");
272        original
273            .public_key()
274            .verify(msg, &sig)
275            .expect("reloaded key must produce signatures the original can verify");
276    }
277
278    #[test]
279    fn rsa_bits_reports_correct_width() {
280        let key = fresh_key();
281        assert_eq!(key.bits(), 2048);
282    }
283}