Swap `ring` for our own in-crate RSA validator
authorMatt Corallo <git@bluematt.me>
Sun, 3 Mar 2024 15:05:08 +0000 (15:05 +0000)
committerMatt Corallo <git@bluematt.me>
Wed, 3 Apr 2024 09:14:45 +0000 (09:14 +0000)
While `ring` is great, it struggles with platform support and has a
fairly involved dependency tree due to its reliance on C backends.

Further, while the `RustCrypto` org tries to stick to Rust, in
doing so it takes on more (unnecessary) dependencies and has a
particularly unusable MSRV policy. Finally, its contributor base
has historically not been particularly friendly.

Thus, sadly, there's not really a good option for doing RSA
validation using a third-party crate.

Instead, we go our own way here, adding an in-crate RSA validator.

src/crypto/hash.rs
src/crypto/mod.rs
src/crypto/rsa.rs [new file with mode: 0644]
src/validation.rs

index 2f86c8982aeec9e815f38589635f6aa54670d1b6..a74000d2ef43412c0f5d8875b525d9ca12375d69 100644 (file)
@@ -12,7 +12,6 @@ pub(crate) enum Hasher {
        Sha1(<Sha1 as Hash>::Engine),
        Sha256(<Sha256 as Hash>::Engine),
        Sha384(<Sha384 as Hash>::Engine),
-       #[allow(unused)]
        Sha512(<Sha512 as Hash>::Engine),
 }
 
@@ -38,7 +37,6 @@ impl Hasher {
        pub(crate) fn sha1() -> Hasher { Hasher::Sha1(Sha1::engine()) }
        pub(crate) fn sha256() -> Hasher { Hasher::Sha256(Sha256::engine()) }
        pub(crate) fn sha384() -> Hasher { Hasher::Sha384(Sha384::engine()) }
-       #[allow(unused)]
        pub(crate) fn sha512() -> Hasher { Hasher::Sha512(Sha512::engine()) }
 
        pub(crate) fn update(&mut self, buf: &[u8]) {
index 06d13f2245e0510ee9185a4ea29a1a4fa4d08adc..24f6121a24513cbe687f17c617d7613dd9403662 100644 (file)
@@ -21,3 +21,4 @@
 
 pub mod bigint;
 pub mod hash;
+pub mod rsa;
diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs
new file mode 100644 (file)
index 0000000..42cc9b9
--- /dev/null
@@ -0,0 +1,67 @@
+//! A simple RSA implementation which handles DNSSEC RSA validation
+
+use super::bigint::*;
+
+fn bytes_to_rsa_mod_exp_modlen(pubkey: &[u8]) -> Result<(U4096, u32, usize), ()> {
+       if pubkey.len() <= 3 { return Err(()); }
+
+       let mut pos = 0;
+       let exponent_length;
+       if pubkey[0] == 0 {
+               exponent_length = ((pubkey[1] as usize) << 8) | (pubkey[2] as usize);
+               pos += 3;
+       } else {
+               exponent_length = pubkey[0] as usize;
+               pos += 1;
+       }
+
+       if pubkey.len() <= pos + exponent_length { return Err(()); }
+       if exponent_length > 4 { return Err(()); }
+       let mut exp_bytes = [0; 4];
+       exp_bytes[4 - exponent_length..].copy_from_slice(&pubkey[pos..pos + exponent_length]);
+       let exp = u32::from_be_bytes(exp_bytes);
+
+       let mod_bytes = &pubkey[pos + exponent_length..];
+       let modlen = pubkey.len() - pos - exponent_length;
+       let modulus = U4096::from_be_bytes(mod_bytes)?;
+       Ok((modulus, exp, modlen))
+}
+
+/// Validates the given RSA signature against the given RSA public key (up to 4096-bit, in
+/// DNSSEC-encoded form) and given message digest.
+pub fn validate_rsa(pk: &[u8], sig_bytes: &[u8], hash_input: &[u8]) -> Result<(), ()> {
+       let (modulus, exponent, modulus_byte_len) = bytes_to_rsa_mod_exp_modlen(pk)?;
+       if modulus_byte_len > 512 { /* implied by the U4096, but explicit here */ return Err(()); }
+       let sig = U4096::from_be_bytes(sig_bytes)?;
+
+       if sig > modulus { return Err(()); }
+
+       // From https://www.rfc-editor.org/rfc/rfc5702#section-3.1
+       const SHA256_PFX: [u8; 20] = hex_lit::hex!("003031300d060960864801650304020105000420");
+       const SHA512_PFX: [u8; 20] = hex_lit::hex!("003051300d060960864801650304020305000440");
+       let pfx = if hash_input.len() == 512 / 8 { &SHA512_PFX } else { &SHA256_PFX };
+
+       if 512 - 2 - SHA256_PFX.len() <= hash_input.len() { return Err(()); }
+       let mut hash_bytes = [0; 512];
+       let mut hash_write_pos = 512 - hash_input.len();
+       hash_bytes[hash_write_pos..].copy_from_slice(&hash_input);
+       hash_write_pos -= pfx.len();
+       hash_bytes[hash_write_pos..hash_write_pos + pfx.len()].copy_from_slice(pfx);
+       while 512 + 1 - hash_write_pos < modulus_byte_len {
+               hash_write_pos -= 1;
+               hash_bytes[hash_write_pos] = 0xff;
+       }
+       hash_bytes[hash_write_pos] = 1;
+       let hash = U4096::from_be_bytes(&hash_bytes)?;
+
+       if hash > modulus { return Err(()); }
+
+       // While modulus could be even, if it were we'd have already factored the modulus (one of the
+       // primes is two!), so we don't particularly care if we fail spuriously for such spurious keys.
+       let res = sig.expmod_odd_mod(exponent, &modulus)?;
+       if res == hash {
+               Ok(())
+       } else {
+               Err(())
+       }
+}
index e6b585fd44921c70b3c48a4f4e742f81423186bc..2fb97448d7f0f65e870cfd15481676f08dad84cb 100644 (file)
@@ -47,27 +47,6 @@ pub enum ValidationError {
        Invalid,
 }
 
-pub(crate) fn bytes_to_rsa_pk<'a>(pubkey: &'a [u8])
--> Result<signature::RsaPublicKeyComponents<&'a [u8]>, ()> {
-       if pubkey.len() <= 3 { return Err(()); }
-
-       let mut pos = 0;
-       let exponent_length;
-       if pubkey[0] == 0 {
-               exponent_length = ((pubkey[1] as usize) << 8) | (pubkey[2] as usize);
-               pos += 3;
-       } else {
-               exponent_length = pubkey[0] as usize;
-               pos += 1;
-       }
-
-       if pubkey.len() <= pos + exponent_length { return Err(()); }
-       Ok(signature::RsaPublicKeyComponents {
-               n: &pubkey[pos + exponent_length..],
-               e: &pubkey[pos..pos + exponent_length]
-       })
-}
-
 fn verify_rrsig<'a, RR: Record, Keys>(sig: &RRSig, dnskeys: Keys, mut records: Vec<&RR>)
 -> Result<(), ValidationError>
 where Keys: IntoIterator<Item = &'a DnsKey> {
@@ -82,6 +61,15 @@ where Keys: IntoIterator<Item = &'a DnsKey> {
                        if dnskey.flags & 0b1_0000_0000 == 0 { continue; }
                        if dnskey.alg != sig.alg { continue; }
 
+                       let mut hash_ctx = match sig.alg {
+                               8 => crypto::hash::Hasher::sha256(),
+                               10 => crypto::hash::Hasher::sha512(),
+                               13 => crypto::hash::Hasher::sha256(),
+                               14 => crypto::hash::Hasher::sha384(),
+                               15 => crypto::hash::Hasher::sha512(),
+                               _ => return Err(ValidationError::UnsupportedAlgorithm),
+                       };
+
                        let mut signed_data = Vec::with_capacity(2048);
                        signed_data.extend_from_slice(&sig.ty.to_be_bytes());
                        signed_data.extend_from_slice(&sig.alg.to_be_bytes());
@@ -120,13 +108,9 @@ where Keys: IntoIterator<Item = &'a DnsKey> {
 
                        let sig_validation = match sig.alg {
                                8|10 => {
-                                       let alg = if sig.alg == 8 {
-                                               &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY
-                                       } else {
-                                               &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY
-                                       };
-                                       bytes_to_rsa_pk(&dnskey.pubkey).map_err(|_| ValidationError::Invalid)?
-                                               .verify(alg, &signed_data, &sig.signature)
+                                       hash_ctx.update(&signed_data);
+                                       let hash = hash_ctx.finish();
+                                       crypto::rsa::validate_rsa(&dnskey.pubkey, &sig.signature, hash.as_ref())
                                                .map_err(|_| ValidationError::Invalid)
                                },
                                13|14 => {