use zeroize::Zeroizing;
use super::der::{self, Reader, Writer};
use crate::error::{Error, Result};
#[derive(Clone, Copy)]
pub(crate) struct RsaPublicParts<'a> {
pub(crate) n: &'a [u8],
pub(crate) e: &'a [u8],
}
pub(crate) fn decode_public(der: &[u8]) -> Result<RsaPublicParts<'_>> {
let mut outer = Reader::new(der)?;
let fields = outer.sequence()?;
outer.finish()?;
read_public(fields)
}
pub(crate) fn decode(der: &[u8]) -> Result<RsaPublicParts<'_>> {
let mut outer = Reader::new(der)?;
let fields = outer.sequence()?;
outer.finish()?;
if is_rsa_private_key(fields) {
return Err(Error::UnsupportedAlgorithm);
}
read_public(fields)
}
pub(crate) fn is_rsa_private_key_der(der: &[u8]) -> bool {
let Ok(mut outer) = Reader::new(der) else {
return false;
};
let Ok(fields) = outer.sequence() else {
return false;
};
outer.finish().is_ok() && is_rsa_private_key(fields)
}
pub(crate) fn relabel_rsa_private_key(der: &[u8], error: Error) -> Error {
match error {
Error::InvalidKey(_) if is_rsa_private_key_der(der) => Error::UnsupportedAlgorithm,
other => other,
}
}
const PRIVATE_KEY_INTEGERS: usize = 8;
fn is_rsa_private_key(mut fields: Reader<'_>) -> bool {
let Ok(version) = fields.expect(der::INTEGER) else {
return false;
};
if !matches!(version.contents, [0x00] | [0x01]) {
return false;
}
(0..PRIVATE_KEY_INTEGERS).all(|_| {
fields
.element()
.is_ok_and(|element| element.tag == der::INTEGER)
})
}
fn read_public(mut fields: Reader<'_>) -> Result<RsaPublicParts<'_>> {
let n = fields.unsigned_integer()?;
let e = fields.unsigned_integer()?;
if !fields.is_empty() {
return Err(Error::InvalidKey(
"RSAPublicKey holds more than modulus and publicExponent",
));
}
Ok(RsaPublicParts { n, e })
}
pub(crate) fn encode_public(n: &[u8], e: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
let body = der::unsigned_integer_len(n) + der::unsigned_integer_len(e);
let mut writer = Writer::new(der::encoded_len(body))?;
writer.header(der::SEQUENCE, body)?;
writer.unsigned_integer(n)?;
writer.unsigned_integer(e)?;
writer.finish()
}
#[cfg(test)]
mod tests {
use super::super::der::fixtures;
use super::*;
use crate::util::b64url_decode;
fn is_invalid<T>(result: &Result<T>) -> bool {
matches!(result, Err(Error::InvalidKey(_)))
}
const A1_N: &str = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw";
fn public_fixtures() -> Vec<(&'static str, String)> {
let mut out = Vec::new();
for dir in ["openssl", "rfc"] {
for file in fixtures::files(dir, |name| {
name.contains("-pkcs1-pub") && !name.starts_with("reject-")
}) {
out.push((dir, file));
}
}
assert_eq!(
out.len(),
5,
"rsa2048 (der, pem), rsa4096 (pem), RFC 7517 A.1 (der, pem)"
);
out
}
#[test]
fn rfc7517_a1_rsa_public_key_decodes_to_its_n_and_e() {
let der = fixtures::load("rfc", "rfc7517-a1-rsa-pkcs1-pub.der");
let key = decode_public(&der).unwrap();
assert_eq!(key.n, b64url_decode(A1_N).unwrap().as_slice());
assert_eq!(key.e, &[0x01, 0x00, 0x01]);
assert_eq!(key.n.len(), 256);
assert!(key.n[0] & 0x80 != 0);
}
#[test]
fn every_public_fixture_decodes_and_re_encodes_to_its_bytes() {
for (dir, file) in public_fixtures() {
let der = fixtures::load(dir, &file);
let key = decode_public(&der).unwrap_or_else(|e| panic!("{file}: {e:?}"));
assert_ne!(key.n[0], 0, "{file}: n is minimal");
let again = encode_public(key.n, key.e).unwrap();
assert_eq!(&again[..], &der[..], "{file}");
}
}
#[test]
fn decode_borrows_the_input() {
let der = fixtures::load("rfc", "rfc7517-a1-rsa-pkcs1-pub.der");
let key = decode_public(&der).unwrap();
let range = der.as_ptr_range();
assert!(range.contains(&key.n.as_ptr()));
assert!(range.contains(&key.e.as_ptr()));
}
#[test]
fn encode_adds_the_sign_octet_only_where_needed() {
let der = encode_public(&[0x80, 0x01], &[0x00, 0x03]).unwrap();
assert_eq!(
&der[..],
&[0x30, 0x08, 0x02, 0x03, 0x00, 0x80, 0x01, 0x02, 0x01, 0x03]
);
let der = encode_public(&[0x7F], &[0x03]).unwrap();
assert_eq!(&der[..], &[0x30, 0x06, 0x02, 0x01, 0x7F, 0x02, 0x01, 0x03]);
}
#[test]
fn malformed_rsa_public_keys_are_invalid() {
for (what, der) in [
("empty", vec![]),
("empty SEQUENCE", vec![0x30, 0x00]),
("one INTEGER", vec![0x30, 0x03, 0x02, 0x01, 0x05]),
(
"three INTEGERs",
vec![
0x30, 0x09, 0x02, 0x01, 0x05, 0x02, 0x01, 0x03, 0x02, 0x01, 0x03,
],
),
(
"negative n",
vec![0x30, 0x06, 0x02, 0x01, 0x85, 0x02, 0x01, 0x03],
),
(
"negative e",
vec![0x30, 0x06, 0x02, 0x01, 0x05, 0x02, 0x01, 0x83],
),
(
"non-minimal n",
vec![0x30, 0x07, 0x02, 0x02, 0x00, 0x05, 0x02, 0x01, 0x03],
),
(
"n not an INTEGER",
vec![0x30, 0x06, 0x04, 0x01, 0x05, 0x02, 0x01, 0x03],
),
(
"trailing data",
vec![0x30, 0x06, 0x02, 0x01, 0x05, 0x02, 0x01, 0x03, 0x00],
),
(
"an uncompressed EC point",
vec![0x04, 0x01, 0x02, 0x03, 0x04],
),
] {
assert!(is_invalid(&decode_public(&der)), "{what}");
}
}
#[test]
fn the_rsa_public_key_negative_vectors_are_invalid() {
assert!(is_invalid(&decode_public(&fixtures::negative("N65"))));
}
fn sequence(parts: &[&[u8]]) -> Vec<u8> {
let body = parts.concat();
let mut out = vec![0x30];
match u8::try_from(body.len()).unwrap() {
len @ 0..=0x7F => out.push(len),
len => out.extend_from_slice(&[0x81, len]),
}
out.extend_from_slice(&body);
out
}
fn is_unsupported<T>(result: &Result<T>) -> bool {
matches!(result, Err(Error::UnsupportedAlgorithm))
}
const TWO_PRIME: &[u8] = &[0x02, 0x01, 0x00];
const MULTI: &[u8] = &[0x02, 0x01, 0x01];
const SMALL: &[u8] = &[0x02, 0x01, 0x05];
fn version_then(version: &[u8], field: &[u8], count: usize) -> Vec<u8> {
let mut parts = vec![version];
parts.extend(std::iter::repeat_n(field, count));
sequence(&parts)
}
#[test]
fn two_integers_are_an_rsa_public_key() {
for (dir, file) in public_fixtures() {
let der = fixtures::load(dir, &file);
let classified = decode(&der).unwrap_or_else(|e| panic!("{file}: {e:?}"));
let public = decode_public(&der).unwrap();
assert_eq!(classified.n, public.n, "{file}");
assert_eq!(classified.e, public.e, "{file}");
}
for first in [TWO_PRIME, MULTI] {
let der = sequence(&[first, SMALL]);
let key = decode(&der).unwrap();
assert_eq!(key.e, &[0x05]);
}
}
#[test]
fn rsa_private_key_fixtures_are_unsupported() {
let mut files: Vec<(&str, String)> = [
"reject-rsa2048-private-pkcs1.der",
"reject-rsa2048-private-pkcs1.pem",
"reject-rsa2048-3prime-private-pkcs1.pem",
]
.map(|file| ("openssl", file.to_owned()))
.into();
for bits in [1024, 2048, 4096] {
files.push((
"rfc",
format!("reject-rfc9500-testRSA{bits}-private-pkcs1.pem"),
));
}
for (dir, file) in files {
let der = fixtures::load(dir, &file);
assert!(is_unsupported(&decode(&der)), "{file}");
assert!(is_invalid(&decode_public(&der)), "{file}");
}
}
#[test]
fn rsa_private_key_shapes_are_unsupported() {
let other_prime_infos = sequence(&[&sequence(&[SMALL, SMALL, SMALL])]);
for (what, der) in [
("two-prime", version_then(TWO_PRIME, SMALL, 8)),
("multi", version_then(MULTI, SMALL, 8)),
("nine INTEGERs", version_then(TWO_PRIME, SMALL, 9)),
("multi with otherPrimeInfos", {
let mut parts = vec![MULTI];
parts.extend(std::iter::repeat_n(SMALL, 8));
parts.push(&other_prime_infos);
sequence(&parts)
}),
] {
assert!(is_unsupported(&decode(&der)), "{what}");
}
}
#[test]
fn the_private_integers_are_never_parsed() {
let empty: &[u8] = &[0x02, 0x00];
let non_minimal: &[u8] = &[0x02, 0x02, 0x00, 0x01];
let negative_padding: &[u8] = &[0x02, 0x02, 0xFF, 0x80];
for (what, der) in [
("empty INTEGERs", version_then(TWO_PRIME, empty, 8)),
("non-minimal INTEGERs", version_then(MULTI, non_minimal, 8)),
(
"redundant 0xFF",
version_then(TWO_PRIME, negative_padding, 8),
),
("high-tag-number garbage after the eighth", {
let mut parts = vec![TWO_PRIME];
parts.extend(std::iter::repeat_n(SMALL, 8));
parts.push(&[0x1F, 0xFF, 0xFF]);
sequence(&parts)
}),
] {
assert!(is_unsupported(&decode(&der)), "{what}");
}
}
#[test]
fn every_other_shape_is_invalid() {
let octets: &[u8] = &[0x04, 0x01, 0x00];
for (what, der) in [
("empty", vec![]),
("empty SEQUENCE", sequence(&[])),
("version alone", sequence(&[TWO_PRIME])),
(
"version and seven INTEGERs",
version_then(TWO_PRIME, SMALL, 7),
),
("version 2", version_then(&[0x02, 0x01, 0x02], SMALL, 8)),
("version -1", version_then(&[0x02, 0x01, 0xFF], SMALL, 8)),
(
"non-minimal version 0",
version_then(&[0x02, 0x02, 0x00, 0x00], SMALL, 8),
),
("version not an INTEGER", version_then(octets, SMALL, 8)),
("an OCTET STRING among the eight", {
let mut parts = vec![TWO_PRIME];
parts.extend(std::iter::repeat_n(SMALL, 7));
parts.push(octets);
sequence(&parts)
}),
(
"three INTEGERs, the first a modulus",
sequence(&[&[0x02, 0x01, 0x07], SMALL, SMALL]),
),
(
"not a SEQUENCE",
version_then(TWO_PRIME, SMALL, 8)[2..].to_vec(),
),
("trailing data after a private key", {
let mut der = version_then(TWO_PRIME, SMALL, 8);
der.push(0x00);
der
}),
] {
assert!(is_invalid(&decode(&der)), "{what}");
}
assert!(is_invalid(&decode(&fixtures::negative("N65"))));
}
#[test]
fn the_der_check_agrees_with_decode_on_every_shape() {
let octets: &[u8] = &[0x04, 0x01, 0x00];
let empty: &[u8] = &[0x02, 0x00];
let private = [
fixtures::load("openssl", "reject-rsa2048-private-pkcs1.der"),
fixtures::load("openssl", "reject-rsa2048-3prime-private-pkcs1.pem"),
version_then(TWO_PRIME, SMALL, 8),
version_then(MULTI, SMALL, 9),
version_then(TWO_PRIME, empty, 8),
];
let mut trailing = version_then(TWO_PRIME, SMALL, 8);
trailing.push(0x00);
let other = [
fixtures::load("rfc", "rfc7517-a1-rsa-pkcs1-pub.der"),
fixtures::load("openssl", "reject-rsa2048-private-pkcs8.der"),
fixtures::load("rfc", "rfc7517-a2-ec-p256-sec1.der"),
fixtures::load("rfc", "rfc8037-a1-ed25519-pkcs8.der"),
vec![],
sequence(&[]),
sequence(&[TWO_PRIME]),
version_then(TWO_PRIME, SMALL, 7),
version_then(&[0x02, 0x01, 0x02], SMALL, 8),
version_then(octets, SMALL, 8),
trailing,
];
for der in &private {
assert!(is_rsa_private_key_der(der));
assert!(is_unsupported(&decode(der)));
}
for der in &other {
assert!(!is_rsa_private_key_der(der));
assert!(!is_unsupported(&decode(der)));
}
}
#[test]
fn relabel_turns_only_an_invalid_rsa_private_key_into_unsupported() {
let private = fixtures::load("openssl", "reject-rsa2048-private-pkcs1.der");
let ed25519 = fixtures::load("rfc", "rfc8037-a1-ed25519-pkcs8.der");
let invalid = Error::InvalidKey("detail");
assert_eq!(
relabel_rsa_private_key(&private, invalid.clone()),
Error::UnsupportedAlgorithm
);
assert_eq!(relabel_rsa_private_key(&ed25519, invalid.clone()), invalid);
assert_eq!(
relabel_rsa_private_key(&private, Error::InvalidBase64),
Error::InvalidBase64
);
}
mod properties {
use proptest::prelude::*;
use super::{decode, decode_public, encode_public};
fn canonical(input: &[u8]) -> Result<(), TestCaseError> {
if let Ok(key) = decode_public(input) {
prop_assert_eq!(&encode_public(key.n, key.e).unwrap()[..], input);
}
if let Ok(key) = decode(input) {
prop_assert_eq!(&encode_public(key.n, key.e).unwrap()[..], input);
}
Ok(())
}
fn length(len: usize) -> Vec<u8> {
match len {
0..=0x7F => vec![u8::try_from(len).unwrap()],
0x80..=0xFF => vec![0x81, u8::try_from(len).unwrap()],
_ => {
let [high, low] = u16::try_from(len).unwrap().to_be_bytes();
vec![0x82, high, low]
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1_000))]
#[test]
fn arbitrary_bytes_fail_or_are_canonical(bytes in proptest::collection::vec(any::<u8>(), 0..256)) {
canonical(&bytes)?;
}
#[test]
fn any_two_magnitudes_round_trip(
n in proptest::collection::vec(any::<u8>(), 1..300),
e in proptest::collection::vec(any::<u8>(), 1..8),
) {
let der = encode_public(&n, &e).unwrap();
let key = decode(&der).unwrap();
let strip = |v: &[u8]| -> Vec<u8> {
let start = v.iter().position(|&b| b != 0).unwrap_or(v.len() - 1);
v[start..].to_vec()
};
prop_assert_eq!(key.n.to_vec(), strip(&n));
prop_assert_eq!(key.e.to_vec(), strip(&e));
canonical(&der)?;
}
#[test]
fn private_key_shapes_are_always_unsupported(
multi in any::<bool>(),
fields in proptest::collection::vec(proptest::collection::vec(any::<u8>(), 0..20), 8..12),
) {
let version: &[u8] = if multi { &[0x02, 0x01, 0x01] } else { &[0x02, 0x01, 0x00] };
let mut body = version.to_vec();
for contents in &fields {
body.push(0x02);
body.push(u8::try_from(contents.len()).unwrap());
body.extend_from_slice(contents);
}
let mut der = vec![0x30];
der.extend_from_slice(&length(body.len()));
der.extend_from_slice(&body);
prop_assert_eq!(decode(&der).err(), Some(crate::Error::UnsupportedAlgorithm));
}
}
}
}