use rustlavel_core::{Error, Result};
type Limbs = Vec<u32>;
const SHA256_DIGEST_INFO: [u8; 19] = [
0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
0x05, 0x00, 0x04, 0x20,
];
const MAX_MODULUS_BYTES: usize = 1024;
pub fn verify_pkcs1_sha256(n: &[u8], e: &[u8], message: &[u8], signature: &[u8]) -> bool {
let Ok(recovered) = recover(n, e, signature) else {
return false;
};
let Ok(expected) = pkcs1_block(recovered.len(), message) else {
return false;
};
recovered == expected
}
fn recover(n: &[u8], e: &[u8], signature: &[u8]) -> Result<Vec<u8>> {
let modulus = trimmed(n);
let length = modulus.len();
if !(64..=MAX_MODULUS_BYTES).contains(&length) {
return Err(Error::msg("the RSA modulus is not a plausible length"));
}
if modulus.last().is_none_or(|byte| byte % 2 == 0) {
return Err(Error::msg("the RSA modulus is even"));
}
if signature.len() != length {
return Err(Error::msg("the signature is not the length of the modulus"));
}
let n = from_be(&modulus);
let s = from_be(signature);
if cmp(&s, &n) != std::cmp::Ordering::Less {
return Err(Error::msg("the signature is not smaller than the modulus"));
}
Ok(to_be(&mod_pow(&s, &from_be(&trimmed(e)), &n), length))
}
fn pkcs1_block(length: usize, message: &[u8]) -> Result<Vec<u8>> {
let digest = crate::ceremony::sha256(message);
let tail = SHA256_DIGEST_INFO.len() + digest.len();
if length < tail + 11 {
return Err(Error::msg("the RSA modulus is too small for a SHA-256 signature"));
}
let mut block = Vec::with_capacity(length);
block.push(0x00);
block.push(0x01);
block.resize(length - tail - 1, 0xff);
block.push(0x00);
block.extend_from_slice(&SHA256_DIGEST_INFO);
block.extend_from_slice(&digest);
Ok(block)
}
fn trimmed(bytes: &[u8]) -> Vec<u8> {
let start = bytes.iter().position(|byte| *byte != 0).unwrap_or(bytes.len());
bytes[start..].to_vec()
}
fn from_be(bytes: &[u8]) -> Limbs {
let mut limbs = Vec::with_capacity(bytes.len().div_ceil(4));
for chunk in bytes.rchunks(4) {
let mut limb = 0u32;
for byte in chunk {
limb = (limb << 8) | u32::from(*byte);
}
limbs.push(limb);
}
trim(&mut limbs);
limbs
}
fn to_be(limbs: &Limbs, length: usize) -> Vec<u8> {
let mut out = vec![0u8; length];
for (index, limb) in limbs.iter().enumerate() {
for byte in 0..4 {
let position = index * 4 + byte;
if position < length {
out[length - 1 - position] = (limb >> (byte * 8)) as u8;
}
}
}
out
}
fn trim(limbs: &mut Limbs) {
while limbs.last() == Some(&0) {
limbs.pop();
}
}
fn cmp(a: &Limbs, b: &Limbs) -> std::cmp::Ordering {
a.len().cmp(&b.len()).then_with(|| a.iter().rev().cmp(b.iter().rev()))
}
fn mul(a: &Limbs, b: &Limbs) -> Limbs {
if a.is_empty() || b.is_empty() {
return Vec::new();
}
let mut out = vec![0u32; a.len() + b.len()];
for (i, x) in a.iter().enumerate() {
let mut carry = 0u64;
for (j, y) in b.iter().enumerate() {
let total = u64::from(*x) * u64::from(*y) + u64::from(out[i + j]) + carry;
out[i + j] = total as u32;
carry = total >> 32;
}
out[i + b.len()] = carry as u32;
}
trim(&mut out);
out
}
fn rem(a: &Limbs, m: &Limbs) -> Limbs {
if cmp(a, m) == std::cmp::Ordering::Less {
return a.clone();
}
if m.len() == 1 {
let divisor = u64::from(m[0]);
let mut remainder = 0u64;
for limb in a.iter().rev() {
remainder = ((remainder << 32) | u64::from(*limb)) % divisor;
}
let mut out = vec![remainder as u32];
trim(&mut out);
return out;
}
let shift = m[m.len() - 1].leading_zeros();
let divisor = shift_left(m, shift);
let mut u = shift_left(a, shift);
u.push(0);
let n = divisor.len();
let top = u64::from(divisor[n - 1]);
let next = u64::from(divisor[n - 2]);
for j in (0..u.len() - n).rev() {
let numerator = (u64::from(u[j + n]) << 32) | u64::from(u[j + n - 1]);
let mut q = numerator / top;
let mut r = numerator % top;
while q > u32::MAX.into() || q * next > ((r << 32) | u64::from(u[j + n - 2])) {
q -= 1;
r += top;
if r > u32::MAX.into() {
break;
}
}
let mut borrow = 0i64;
let mut carry = 0u64;
for (i, limb) in divisor.iter().enumerate() {
let product = q * u64::from(*limb) + carry;
carry = product >> 32;
let difference = i64::from(u[j + i]) - (product & 0xffff_ffff) as i64 - borrow;
u[j + i] = difference as u32;
borrow = if difference < 0 { 1 } else { 0 };
}
let difference = i64::from(u[j + n]) - carry as i64 - borrow;
u[j + n] = difference as u32;
if difference < 0 {
let mut carry = 0u64;
for (i, limb) in divisor.iter().enumerate() {
let total = u64::from(u[j + i]) + u64::from(*limb) + carry;
u[j + i] = total as u32;
carry = total >> 32;
}
u[j + n] = (u64::from(u[j + n]).wrapping_add(carry)) as u32;
}
}
u.truncate(n);
trim(&mut u);
shift_right(&u, shift)
}
fn shift_left(limbs: &Limbs, bits: u32) -> Limbs {
if bits == 0 {
return limbs.clone();
}
let mut out = Vec::with_capacity(limbs.len() + 1);
let mut carry = 0u32;
for limb in limbs {
out.push((limb << bits) | carry);
carry = *limb >> (32 - bits);
}
if carry != 0 {
out.push(carry);
}
trim(&mut out);
out
}
fn shift_right(limbs: &Limbs, bits: u32) -> Limbs {
if bits == 0 {
return limbs.clone();
}
let mut out = vec![0u32; limbs.len()];
for index in 0..limbs.len() {
let high = if index + 1 < limbs.len() { limbs[index + 1] } else { 0 };
out[index] = (limbs[index] >> bits) | (high << (32 - bits));
}
trim(&mut out);
out
}
fn mod_pow(base: &Limbs, exponent: &Limbs, modulus: &Limbs) -> Limbs {
let Some(top) = exponent.last() else {
return vec![1];
};
let mut result = rem(&vec![1u32], modulus);
let mut started = false;
for (index, limb) in exponent.iter().enumerate().rev() {
let bits = if index == exponent.len() - 1 { 32 - top.leading_zeros() } else { 32 };
for bit in (0..bits).rev() {
if started {
result = rem(&mul(&result, &result), modulus);
}
if (limb >> bit) & 1 == 1 {
result = if started { rem(&mul(&result, base), modulus) } else { rem(base, modulus) };
started = true;
}
}
}
result
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
fn big(hex: &str) -> Vec<u8> {
(0..hex.len()).step_by(2).map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap()).collect()
}
pub(crate) fn openssl_modulus() -> Vec<u8> {
big(concat!(
"bf6a84cda51ec4dbc0595805673e0b1561ecd2e499241eecfde6ac5d190316e8",
"f2aa5154a2ef79e79b6a351b5244de8b6735931319eb774d63639f7e57c4155d",
"a2e5eca90f2c306861f551ab2c0d2760ab59a0f01663f3c4d79dfbd56a926591",
"0abc40114a67867efd6b1ccedc487b58a52b582e2ee4504d4a13c0fbf789125c",
"d6938fa922e105e3e6f7d6b0f782e546487ebc2bea8381754ddd4232a1a076a5",
"40e372db9d99300265c3caccf27c8c85e4f237e0ae9bfc64a5da805c6b6b40ca",
"1ecaf64a7917143fc7664cdaae6fb756a69a5cd4a44529803e076ebc19987932",
"202b9d6740075366777ab49fa9a59491edef8d1abeb7febd37ca6ec7af7b8227",
))
}
pub(crate) fn openssl_signature() -> Vec<u8> {
big(concat!(
"8b76e5275436aa22d274bc0a8c080305814b197c6e433f3aac32959c77e332d5",
"a01d0a2c5b5a2934ecf5abf92f7a1233b204d2801a56a33a77334c9ef9bc957e",
"89607f74efecc963df93fc30731eefabfe87723f4652579b5369b3178f39d9b0",
"159b9f5d35c06b1e41d169c5f253fe5291255ae255c6dcd3af835f8a7a107357",
"6744fbad4c56dc3a8c2cd984c29ac1b47eab37e9700ef3d2f19b99b5431d7010",
"0d525e12bf1e60fc2373c1ec674b21e4eaf3344cc987a883227b6bd7b4ef0302",
"6b26aca9f6479c53702a7aa69589c39b30c17e62dd1e361a14315d529bf19a9e",
"6a046866921ca35bbc9eb559d5d4e3eb8c08f6a0276842e4c96051002a09ef1b",
))
}
#[test]
fn modular_exponentiation_agrees_with_arithmetic_done_by_hand() {
let result = mod_pow(&from_be(&[7]), &from_be(&[11]), &from_be(&[13]));
assert_eq!(to_be(&result, 1), vec![2]);
let result = mod_pow(&from_be(&[2]), &from_be(&[0, 0, 0, 0, 64]), &from_be(&[0xff, 0xff, 0xff, 0xff]));
assert_eq!(to_be(&result, 4), vec![0, 0, 0, 1]);
}
#[test]
fn remainder_matches_a_known_multi_limb_division() {
let a = from_be(&[0xff; 16]);
let m = from_be(&[0x01, 0x00, 0x00, 0x00, 0x01]);
assert_eq!(rem(&a, &m), Vec::<u32>::new());
}
#[test]
fn a_real_signature_verifies_and_a_tampered_one_does_not() {
let n = big(concat!(
"c4f8e9e0dc1a1b8d0b19b4c68b1f0c1c8e59e2e8a6de6d8b5f2c6c6f7a5f3e4d",
"9b2a1c0f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a3928170615f4e3",
"d2c1b0a99887766554433221100ffeeddccbbaa99887766554433221100fedcb",
"a987654321fedcba9876543210fedcba9876543210fedcba9876543210fedcb9",
"8a7b6c5d4e3f20112233445566778899aabbccddeeff00112233445566778899",
"aabbccddeeff00112233445566778899aabbccddeeff0011223344556677889b",
"1122334455667788990011223344556677889900112233445566778899001122",
"33445566778899001122334455667788990011223344556677889900112233f5",
));
assert!(!verify_pkcs1_sha256(&n, &[0x01, 0x00, 0x01], b"hello", &[0x42; 256]));
assert!(!verify_pkcs1_sha256(&n, &[0x01, 0x00, 0x01], b"hello", &[0x00; 256]));
assert!(!verify_pkcs1_sha256(&n, &[0x01, 0x00, 0x01], b"hello", &[0x42; 128]));
}
#[test]
fn a_signature_openssl_made_verifies_and_stops_verifying_when_touched() {
let n = openssl_modulus();
let signature = openssl_signature();
let message = b"rustlavel webauthn rs256 test vector";
let e = [0x01, 0x00, 0x01];
assert!(verify_pkcs1_sha256(&n, &e, message, &signature), "a genuine signature was refused");
let mut tampered = signature.clone();
tampered[200] ^= 0x01;
assert!(!verify_pkcs1_sha256(&n, &e, message, &tampered));
assert!(!verify_pkcs1_sha256(&n, &e, b"rustlavel webauthn rs256 test vectof", &signature));
let mut other = n.clone();
other[128] ^= 0x01;
assert!(!verify_pkcs1_sha256(&other, &e, message, &signature));
assert!(!verify_pkcs1_sha256(&n, &[0x03], message, &signature));
}
#[test]
fn the_padding_block_is_the_shape_rfc_8017_describes() {
let block = pkcs1_block(256, b"hello").unwrap();
assert_eq!(block.len(), 256);
assert_eq!(&block[..2], &[0x00, 0x01]);
assert!(block[2..204].iter().all(|byte| *byte == 0xff), "the padding is not all 0xff");
assert_eq!(block[204], 0x00);
assert_eq!(&block[205..224], &SHA256_DIGEST_INFO);
assert_eq!(&block[224..], &crate::ceremony::sha256(b"hello"));
}
#[test]
fn a_modulus_that_is_too_small_or_too_large_is_refused() {
assert!(!verify_pkcs1_sha256(&[0xff; 32], &[0x01, 0x00, 0x01], b"hi", &[0x01; 32]));
assert!(!verify_pkcs1_sha256(&vec![0xff; 2048], &[0x01, 0x00, 0x01], b"hi", &vec![0x01; 2048]));
}
}