use zeroize::Zeroizing;
use super::der::{self, Element, Reader, Writer};
use super::oid;
use super::types::{AlgId, SpkiParts};
use crate::error::{Error, Result};
use crate::jwk::EcCurve;
const CURVES: [EcCurve; 4] = [
EcCurve::P256,
EcCurve::P384,
EcCurve::P521,
EcCurve::Secp256k1,
];
pub(crate) fn read_algorithm_identifier(reader: &mut Reader<'_>) -> Result<AlgId> {
classify(reader.expect(der::SEQUENCE)?)
}
fn classify(element: Element<'_>) -> Result<AlgId> {
if let Some((alg, _)) = oid::ALGORITHM_IDENTIFIERS
.iter()
.find(|(_, template)| *template == element.encoding)
{
return Ok(*alg);
}
let mut fields = Reader::new(element.contents)?;
let algorithm = fields.oid()?;
let parameters = if fields.is_empty() {
None
} else {
Some(fields.element()?)
};
fields.finish()?;
if let Some(parameters) = parameters {
der::check_well_formed(parameters.encoding, false)?;
}
let mismatch = Err(Error::InvalidKey(
"AlgorithmIdentifier parameters do not match the algorithm",
));
match algorithm {
oid::OID_EC_PUBLIC_KEY => match parameters {
Some(curve) if curve.tag == der::SEQUENCE => Err(Error::UnsupportedAlgorithm),
Some(curve) if curve.tag == der::OID => match curve.encoding {
oid::OID_BRAINPOOL_P256R1 => Err(Error::UnsupportedAlgorithm),
named if CURVES.iter().any(|crv| oid::named_curve(*crv) == named) => mismatch,
_ => Err(Error::UnsupportedAlgorithm),
},
_ => mismatch,
},
oid::OID_ED25519 | oid::OID_X25519 | oid::OID_RSA_ENCRYPTION => mismatch,
oid::OID_ED448 | oid::OID_X448 | oid::OID_RSASSA_PSS => Err(Error::UnsupportedAlgorithm),
_ => Err(Error::UnsupportedAlgorithm),
}
}
pub(crate) fn decode(der: &[u8]) -> Result<SpkiParts<'_>> {
let mut outer = Reader::new(der)?;
let mut fields = outer.sequence()?;
outer.finish()?;
let alg = read_algorithm_identifier(&mut fields)?;
let subject_public_key = fields.bit_string()?;
fields.finish()?;
Ok(SpkiParts {
alg,
subject_public_key,
})
}
pub(crate) fn encode(parts: &SpkiParts<'_>) -> Result<Zeroizing<Vec<u8>>> {
let algorithm = oid::algorithm_identifier(parts.alg);
let body = algorithm.len() + der::bit_string_len(parts.subject_public_key.len());
let mut writer = Writer::new(der::encoded_len(body))?;
writer.header(der::SEQUENCE, body)?;
writer.raw(algorithm)?;
writer.bit_string(der::BIT_STRING, parts.subject_public_key)?;
writer.finish()
}
#[cfg(test)]
mod tests {
use super::super::der::fixtures;
use super::super::oid;
use super::*;
use crate::jwk::EcCurve;
fn classify_bytes(algorithm_identifier: &[u8]) -> Result<AlgId> {
let mut reader = Reader::new(algorithm_identifier)?;
let alg = read_algorithm_identifier(&mut reader)?;
reader.finish()?;
Ok(alg)
}
fn sequence(parts: &[&[u8]]) -> Vec<u8> {
let body = parts.concat();
let mut out = vec![0x30, u8::try_from(body.len()).unwrap()];
out.extend_from_slice(&body);
out
}
fn is_invalid<T>(result: &Result<T>) -> bool {
matches!(result, Err(Error::InvalidKey(_)))
}
fn is_unsupported<T>(result: &Result<T>) -> bool {
matches!(result, Err(Error::UnsupportedAlgorithm))
}
#[test]
fn every_template_classifies_to_its_algorithm() {
for (alg, template) in oid::ALGORITHM_IDENTIFIERS {
assert_eq!(classify_bytes(template).unwrap(), alg);
}
}
#[test]
fn unknown_or_unsupported_oids_are_unsupported() {
let dsa = [0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x38, 0x04, 0x01];
for alg in [
sequence(&[oid::OID_ED448]),
sequence(&[oid::OID_X448]),
sequence(&[oid::OID_RSASSA_PSS]),
sequence(&[oid::OID_RSASSA_PSS, &sequence(&[])]),
sequence(&[&dsa, &sequence(&[&[0x02, 0x01, 0x01]])]),
sequence(&[oid::OID_ED448, &[0x05, 0x00]]),
] {
assert!(is_unsupported(&classify_bytes(&alg)), "{alg:02x?}");
}
}
#[test]
fn unsupported_ec_parameters_are_unsupported() {
let secp224r1 = [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x21];
for alg in [
sequence(&[oid::OID_EC_PUBLIC_KEY, &sequence(&[&[0x02, 0x01, 0x01]])]),
sequence(&[oid::OID_EC_PUBLIC_KEY, oid::OID_BRAINPOOL_P256R1]),
sequence(&[oid::OID_EC_PUBLIC_KEY, &secp224r1]),
] {
assert!(is_unsupported(&classify_bytes(&alg)), "{alg:02x?}");
}
}
#[test]
fn supported_oids_with_other_parameters_are_invalid() {
for alg in [
sequence(&[oid::OID_ED25519, &[0x05, 0x00]]),
sequence(&[oid::OID_X25519, &[0x05, 0x00]]),
sequence(&[oid::OID_X25519, oid::OID_P256]),
sequence(&[oid::OID_RSA_ENCRYPTION]),
sequence(&[oid::OID_RSA_ENCRYPTION, &[0x02, 0x01, 0x00]]),
sequence(&[oid::OID_EC_PUBLIC_KEY]),
sequence(&[oid::OID_EC_PUBLIC_KEY, &[0x05, 0x00]]),
sequence(&[oid::OID_EC_PUBLIC_KEY, &[0x02, 0x01, 0x00]]),
] {
assert!(is_invalid(&classify_bytes(&alg)), "{alg:02x?}");
}
}
#[test]
fn malformed_algorithm_identifiers_are_invalid_whatever_the_oid() {
for alg in [
sequence(&[]),
sequence(&[&[0x05, 0x00]]),
sequence(&[oid::OID_ED448, &[0x05, 0x00], &[0x05, 0x00]]),
sequence(&[oid::OID_P256, oid::OID_P256, oid::OID_P256]),
sequence(&[&[0x06, 0x04, 0x2B, 0x80, 0x65, 0x70]]),
sequence(&[oid::OID_RSASSA_PSS, &sequence(&[&[0x02, 0x02, 0x00, 0x01]])]),
sequence(&[oid::OID_EC_PUBLIC_KEY, &sequence(&[&[0x24, 0x00]])]),
vec![0x31, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70],
] {
assert!(is_invalid(&classify_bytes(&alg)), "{alg:02x?}");
}
}
const POSITIVE: [(&str, &str, AlgId, usize, Option<usize>); 19] = [
(
"openssl",
"ec-p256-spki",
AlgId::Ec(EcCurve::P256),
65,
None,
),
(
"openssl",
"ec-p256-spki-compressed",
AlgId::Ec(EcCurve::P256),
33,
None,
),
(
"openssl",
"ec-p384-spki",
AlgId::Ec(EcCurve::P384),
97,
None,
),
(
"openssl",
"ec-p384-spki-compressed",
AlgId::Ec(EcCurve::P384),
49,
None,
),
(
"openssl",
"ec-p521-spki",
AlgId::Ec(EcCurve::P521),
133,
None,
),
(
"openssl",
"ec-p521-spki-compressed",
AlgId::Ec(EcCurve::P521),
67,
None,
),
(
"openssl",
"ec-secp256k1-spki",
AlgId::Ec(EcCurve::Secp256k1),
65,
None,
),
(
"openssl",
"ec-secp256k1-spki-compressed",
AlgId::Ec(EcCurve::Secp256k1),
33,
None,
),
("openssl", "ed25519-spki", AlgId::Ed25519, 32, None),
("openssl", "x25519-spki", AlgId::X25519, 32, None),
(
"openssl",
"rsa2048-spki",
AlgId::RsaEncryption,
270,
Some(2048),
),
(
"openssl",
"rsa3072-spki",
AlgId::RsaEncryption,
398,
Some(3072),
),
(
"openssl",
"rsa4096-spki",
AlgId::RsaEncryption,
526,
Some(4096),
),
(
"rfc",
"rfc7468-fig15-p384-spki",
AlgId::Ec(EcCurve::P384),
97,
None,
),
(
"rfc",
"rfc7517-a1-ec-p256-spki",
AlgId::Ec(EcCurve::P256),
65,
None,
),
(
"rfc",
"rfc7517-a1-rsa-spki",
AlgId::RsaEncryption,
270,
Some(2048),
),
("rfc", "rfc8037-a2-ed25519-spki", AlgId::Ed25519, 32, None),
("rfc", "rfc8037-a6-x25519-bob-spki", AlgId::X25519, 32, None),
("rfc", "rfc8037-a6-x25519-spki", AlgId::X25519, 32, None),
];
const RFC8410_SPKI: (&str, &str, AlgId, usize, Option<usize>) =
("rfc", "rfc8410-10.1-ed25519-spki", AlgId::Ed25519, 32, None);
fn positive() -> Vec<(&'static str, String, AlgId, usize, Option<usize>)> {
let mut out = Vec::new();
for (dir, stem, alg, octets, bits) in POSITIVE.into_iter().chain([RFC8410_SPKI]) {
let files = fixtures::files(dir, |name| {
name.strip_suffix(".der")
.or_else(|| name.strip_suffix(".pem"))
== Some(stem)
});
assert!(!files.is_empty(), "{dir}/{stem}");
out.extend(files.into_iter().map(|file| (dir, file, alg, octets, bits)));
}
out
}
fn modulus_bits(rsa_public_key: &[u8]) -> usize {
let mut outer = Reader::new(rsa_public_key).unwrap();
let mut fields = outer.sequence().unwrap();
outer.finish().unwrap();
let n = fields.unsigned_integer().unwrap();
fields.unsigned_integer().unwrap();
fields.finish().unwrap();
n.len() * 8 - n[0].leading_zeros() as usize
}
#[test]
fn decodes_every_positive_spki_fixture() {
for (dir, file, alg, octets, bits) in positive() {
let der = fixtures::load(dir, &file);
let parts = decode(&der).unwrap_or_else(|e| panic!("{file}: {e:?}"));
assert_eq!(parts.alg, alg, "{file}");
assert_eq!(parts.subject_public_key.len(), octets, "{file}");
if let Some(bits) = bits {
assert_eq!(modulus_bits(parts.subject_public_key), bits, "{file}");
}
}
}
#[test]
fn the_positive_table_covers_every_spki_fixture() {
for (dir, keep) in [
(
"openssl",
(|name: &str| name.contains("-spki") && !name.starts_with("reject-"))
as fn(&str) -> bool,
),
("rfc", |name: &str| {
name.contains("-spki") && !name.starts_with("reject-")
}),
] {
let listed = positive();
for file in fixtures::files(dir, keep) {
assert!(
listed.iter().any(|(d, f, ..)| *d == dir && *f == file),
"{dir}/{file} missing from POSITIVE"
);
}
}
}
#[test]
fn re_encoding_gives_the_input_bytes() {
for (dir, file, ..) in positive() {
let der = fixtures::load(dir, &file);
let parts = decode(&der).unwrap();
assert_eq!(&encode(&parts).unwrap()[..], &der[..], "{file}");
}
}
#[test]
fn subject_public_key_borrows_the_input() {
let der = fixtures::load("rfc", "rfc7517-a1-ec-p256-spki.der");
let parts = decode(&der).unwrap();
assert!(
der.as_ptr_range()
.contains(&parts.subject_public_key.as_ptr())
);
assert_eq!(parts.subject_public_key, &der[der.len() - 65..]);
}
#[test]
fn unused_bits_and_extra_fields_are_invalid() {
for id in ["N30", "N38"] {
assert!(is_invalid(&decode(&fixtures::negative(id))), "{id}");
}
}
#[test]
fn classifies_the_spki_negative_vectors() {
assert!(is_unsupported(&decode(&fixtures::negative("N33"))));
for id in ["N32", "N37", "N60"] {
assert!(is_invalid(&decode(&fixtures::negative(id))), "{id}");
}
}
#[test]
fn classifies_the_unsupported_openssl_spki_fixtures() {
for file in [
"reject-ed448-spki.pem",
"reject-rsapss2048-spki.pem",
"reject-ec-brainpoolp256r1-spki.pem",
"reject-ec-p256-explicit-spki.pem",
] {
let der = fixtures::load("openssl", file);
let result = decode(&der);
assert!(is_unsupported(&result), "{file}: {:?}", result.err());
}
}
#[test]
fn the_container_does_not_judge_key_contents() {
for id in [
"N31", "N34", "N35", "N36", "N61", "N62", "N63", "N64", "N66",
] {
let der = fixtures::negative(id);
assert!(decode(&der).is_ok(), "{id}");
}
}
#[test]
fn framing_errors_are_invalid() {
let der = fixtures::load("rfc", "rfc7517-a1-ec-p256-spki.der");
assert!(is_invalid(&decode(&der[..der.len() - 1])));
let mut trailing = der.clone();
trailing.push(0x00);
assert!(is_invalid(&decode(&trailing)));
assert!(is_invalid(&decode(&[])));
let mut octet = der.clone();
octet[der.len() - 68] = 0x04;
assert!(is_invalid(&decode(&octet)));
}
#[test]
fn encode_writes_the_template_and_the_key() {
let key = [0x11; 32];
let parts = SpkiParts {
alg: AlgId::X25519,
subject_public_key: &key,
};
let der = encode(&parts).unwrap();
let mut expected = vec![0x30, 0x2A];
expected.extend_from_slice(oid::ALG_X25519);
expected.extend_from_slice(&[0x03, 0x21, 0x00]);
expected.extend_from_slice(&key);
assert_eq!(&der[..], &expected[..]);
}
mod properties {
use proptest::prelude::*;
use proptest::sample::Index;
use super::super::super::der::fixtures;
use super::{decode, encode, positive};
fn corpus() -> Vec<Vec<u8>> {
positive()
.into_iter()
.map(|(dir, file, ..)| fixtures::load(dir, &file))
.collect()
}
fn canonical(input: &[u8]) -> Result<(), TestCaseError> {
if let Ok(parts) = decode(input) {
let again = encode(&parts);
prop_assert!(again.is_ok());
prop_assert_eq!(&again.unwrap()[..], input);
}
Ok(())
}
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 templated_keys_fail_or_are_canonical(
template in any::<Index>(),
key in proptest::collection::vec(any::<u8>(), 0..160),
) {
let (_, algorithm) = *template.get(&super::super::super::oid::ALGORITHM_IDENTIFIERS);
let mut body = algorithm.to_vec();
body.push(0x03);
body.extend_from_slice(&super::length(key.len()));
body.extend_from_slice(&key);
let mut der = vec![0x30];
der.extend_from_slice(&super::length(body.len()));
der.extend_from_slice(&body);
prop_assert_eq!(decode(&der).is_ok(), key.first() == Some(&0));
canonical(&der)?;
}
#[test]
fn single_byte_mutations_never_panic(fixture in proptest::sample::select(corpus()), at in any::<Index>(), byte in any::<u8>()) {
let mut der = fixture;
let i = at.index(der.len());
der[i] = byte;
canonical(&der)?;
}
}
}
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()],
_ => vec![0x82, u8::try_from(len >> 8).unwrap(), (len & 0xFF) as u8],
}
}
}