use ::der::asn1::{ObjectIdentifier, OctetStringRef};
use ::der::{Decode as _, Encode as _};
use p256::elliptic_curve::sec1::{FromSec1Point, ModulusSize, ToSec1Point};
use p256::elliptic_curve::{AffinePoint, CurveArithmetic, FieldBytesSize, PublicKey, SecretKey};
use super::der::{Reader, fixtures};
use super::oid;
use super::types::{AlgId, Label};
use crate::error::{Error, Result};
use crate::jwk::{EcCurve, Jwk, KeyMaterial, OkpCurve};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Pkcs8,
Spki,
Sec1,
Pkcs1,
}
impl Kind {
const ALL: [Kind; 4] = [Kind::Pkcs8, Kind::Spki, Kind::Sec1, Kind::Pkcs1];
fn of_file(name: &str) -> Kind {
if name.contains("pkcs8") {
Kind::Pkcs8
} else if name.contains("spki") {
Kind::Spki
} else if name.contains("sec1") {
Kind::Sec1
} else if name.contains("pkcs1") {
Kind::Pkcs1
} else {
panic!("{name}: no container in the name")
}
}
fn of_label(label: Label) -> Option<Kind> {
match label {
Label::PrivateKey => Some(Kind::Pkcs8),
Label::PublicKey => Some(Kind::Spki),
Label::EcPrivateKey => Some(Kind::Sec1),
Label::RsaPublic => Some(Kind::Pkcs1),
Label::RsaPrivate | Label::EncryptedPrivateKey | Label::Other => None,
}
}
}
fn our_import(kind: Kind, der: &[u8]) -> Result<Jwk> {
match kind {
Kind::Pkcs8 => {
let parts = super::pkcs8::decode(der)?;
match parts.alg {
AlgId::Ec(_) => super::ec::from_pkcs8(parts),
AlgId::Ed25519 | AlgId::X25519 => super::okp::from_pkcs8(parts),
AlgId::RsaEncryption => super::rsa::from_pkcs8(parts),
}
}
Kind::Spki => {
let parts = super::spki::decode(der)?;
match parts.alg {
AlgId::Ec(_) => super::ec::from_spki(parts),
AlgId::Ed25519 | AlgId::X25519 => super::okp::from_spki(parts),
AlgId::RsaEncryption => super::rsa::from_spki(parts),
}
}
Kind::Sec1 => super::ec::from_sec1(der),
Kind::Pkcs1 => super::rsa::from_pkcs1(der),
}
}
#[derive(Debug, Default, PartialEq, Eq)]
struct Fields {
version: Option<u8>,
algorithm: Option<Vec<u8>>,
private_key: Option<Vec<u8>>,
public_key: Option<Vec<u8>>,
ec: Option<EcFields>,
curve_private_key: Option<Vec<u8>>,
rsa: Option<(Vec<u8>, Vec<u8>)>,
}
#[derive(Debug, PartialEq, Eq)]
struct EcFields {
private_key: Vec<u8>,
parameters: Option<Vec<u8>>,
public_key: Option<Vec<u8>>,
}
fn our_fields(kind: Kind, der: &[u8]) -> Result<Fields> {
let mut fields = Fields::default();
match kind {
Kind::Pkcs8 => {
let parts = super::pkcs8::decode(der)?;
fields.version = Some(parts.version);
fields.algorithm = Some(oid::algorithm_identifier(parts.alg).to_vec());
fields.private_key = Some(parts.private_key.to_vec());
fields.public_key = parts.public_key.map(<[u8]>::to_vec);
match parts.alg {
AlgId::Ec(_) => fields.ec = Some(our_ec_fields(parts.private_key)?),
AlgId::Ed25519 | AlgId::X25519 => {
let mut reader = Reader::new(parts.private_key)?;
let d = reader.octet_string()?;
reader.finish()?;
fields.curve_private_key = Some(d.to_vec());
}
AlgId::RsaEncryption => return Err(Error::UnsupportedAlgorithm),
}
}
Kind::Spki => {
let parts = super::spki::decode(der)?;
fields.algorithm = Some(oid::algorithm_identifier(parts.alg).to_vec());
fields.public_key = Some(parts.subject_public_key.to_vec());
if parts.alg == AlgId::RsaEncryption {
let key = super::pkcs1::decode_public(parts.subject_public_key)?;
fields.rsa = Some((key.n.to_vec(), key.e.to_vec()));
}
}
Kind::Sec1 => fields.ec = Some(our_ec_fields(der)?),
Kind::Pkcs1 => {
let (n, e) = match super::pkcs1::decode(der)? {
super::pkcs1::Pkcs1Key::Public(key) => (key.n, key.e),
super::pkcs1::Pkcs1Key::Private(key) => (key.n, key.e),
};
fields.rsa = Some((n.to_vec(), e.to_vec()));
}
}
Ok(fields)
}
fn our_ec_fields(der: &[u8]) -> Result<EcFields> {
let key = super::sec1::decode(der)?;
Ok(EcFields {
private_key: key.private_key.to_vec(),
parameters: key.parameters.map(|crv| oid::named_curve(crv).to_vec()),
public_key: key.public_key.map(<[u8]>::to_vec),
})
}
const ID_EC_PUBLIC_KEY: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
const SECP256R1: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7");
const SECP384R1: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.34");
const SECP521R1: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.35");
const SECP256K1: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.10");
const ID_X25519: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.110");
const ID_ED25519: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.112");
const RSA_ENCRYPTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
fn curve_oid(crv: EcCurve) -> ObjectIdentifier {
match crv {
EcCurve::P256 => SECP256R1,
EcCurve::P384 => SECP384R1,
EcCurve::P521 => SECP521R1,
EcCurve::Secp256k1 => SECP256K1,
}
}
fn curve_of_oid(named: &ObjectIdentifier) -> Option<EcCurve> {
[
EcCurve::P256,
EcCurve::P384,
EcCurve::P521,
EcCurve::Secp256k1,
]
.into_iter()
.find(|crv| curve_oid(*crv) == *named)
}
type OracleResult<T> = core::result::Result<T, String>;
fn oracle_err(e: impl core::fmt::Display) -> String {
e.to_string()
}
fn oracle_fields(kind: Kind, der: &[u8]) -> OracleResult<Fields> {
let mut fields = Fields::default();
match kind {
Kind::Pkcs8 => {
let info = ::pkcs8::PrivateKeyInfoRef::from_der(der).map_err(oracle_err)?;
fields.version = Some(u8::from(info.version()));
fields.algorithm = Some(info.algorithm.to_der().map_err(oracle_err)?);
let private_key = info.private_key.as_bytes();
fields.private_key = Some(private_key.to_vec());
fields.public_key = info
.public_key
.map(|key| key.as_bytes().map(<[u8]>::to_vec).ok_or("unused bits"))
.transpose()?;
let alg = info.algorithm.oid;
if alg == ID_EC_PUBLIC_KEY {
fields.ec = Some(oracle_ec_fields(private_key)?);
} else if alg == ID_ED25519 || alg == ID_X25519 {
let d = <&OctetStringRef>::from_der(private_key).map_err(oracle_err)?;
fields.curve_private_key = Some(d.as_bytes().to_vec());
}
}
Kind::Spki => {
let info = ::spki::SubjectPublicKeyInfoRef::from_der(der).map_err(oracle_err)?;
fields.algorithm = Some(info.algorithm.to_der().map_err(oracle_err)?);
let key = info.subject_public_key.as_bytes().ok_or("unused bits")?;
fields.public_key = Some(key.to_vec());
if info.algorithm.oid == RSA_ENCRYPTION {
fields.rsa = Some(oracle_rsa(key)?);
}
}
Kind::Sec1 => fields.ec = Some(oracle_ec_fields(der)?),
Kind::Pkcs1 => fields.rsa = Some(oracle_rsa(der)?),
}
Ok(fields)
}
fn oracle_ec_fields(der: &[u8]) -> OracleResult<EcFields> {
let key = ::sec1::EcPrivateKey::from_der(der).map_err(oracle_err)?;
let parameters = key
.parameters
.map(|parameters| {
parameters
.named_curve()
.ok_or_else(|| "not a namedCurve".to_owned())
.and_then(|named| named.to_der().map_err(oracle_err))
})
.transpose()?;
Ok(EcFields {
private_key: key.private_key.to_vec(),
parameters,
public_key: key.public_key.map(<[u8]>::to_vec),
})
}
fn oracle_rsa(der: &[u8]) -> OracleResult<(Vec<u8>, Vec<u8>)> {
let key = ::pkcs1::RsaPublicKeyRef::from_der(der).map_err(oracle_err)?;
Ok((
key.modulus.as_bytes().to_vec(),
key.public_exponent.as_bytes().to_vec(),
))
}
#[derive(Debug, PartialEq, Eq)]
enum Values {
Ec {
crv: EcCurve,
x: Vec<u8>,
y: Vec<u8>,
d: Option<Vec<u8>>,
},
Okp {
crv: OkpCurve,
x: Vec<u8>,
d: Option<Vec<u8>>,
},
Rsa {
n: Vec<u8>,
e: Vec<u8>,
},
}
fn values_of(jwk: &Jwk) -> Values {
match jwk.material() {
KeyMaterial::Ec(key) => Values::Ec {
crv: key.crv(),
x: key.x().to_vec(),
y: key.y().to_vec(),
d: key.d().map(<[u8]>::to_vec),
},
KeyMaterial::Okp(key) => Values::Okp {
crv: key.crv(),
x: key.x().to_vec(),
d: key.d().map(<[u8]>::to_vec),
},
KeyMaterial::Rsa(key) => Values::Rsa {
n: key.n().to_vec(),
e: key.e().to_vec(),
},
other => panic!("not a PEM/DER key type: {:?}", other.key_type()),
}
}
enum OracleAlg {
Ec(Option<EcCurve>),
Ed25519,
X25519,
Rsa,
Other,
}
fn oracle_alg(algorithm: &[u8]) -> OracleResult<OracleAlg> {
let alg = ::spki::AlgorithmIdentifierRef::from_der(algorithm).map_err(oracle_err)?;
Ok(if alg.oid == ID_EC_PUBLIC_KEY {
let named = alg
.parameters
.and_then(|parameters| parameters.decode_as::<ObjectIdentifier>().ok());
OracleAlg::Ec(named.as_ref().and_then(curve_of_oid))
} else if alg.oid == ID_ED25519 {
OracleAlg::Ed25519
} else if alg.oid == ID_X25519 {
OracleAlg::X25519
} else if alg.oid == RSA_ENCRYPTION {
OracleAlg::Rsa
} else {
OracleAlg::Other
})
}
type Affine = (Vec<u8>, Vec<u8>);
fn point_on<C>(octets: &[u8]) -> OracleResult<Affine>
where
C: CurveArithmetic,
AffinePoint<C>: FromSec1Point<C> + ToSec1Point<C>,
FieldBytesSize<C>: ModulusSize,
{
let public = PublicKey::<C>::from_sec1_bytes(octets).map_err(|_| "point rejected")?;
affine(&public)
}
fn affine<C>(public: &PublicKey<C>) -> OracleResult<Affine>
where
C: CurveArithmetic,
AffinePoint<C>: FromSec1Point<C> + ToSec1Point<C>,
FieldBytesSize<C>: ModulusSize,
{
let point = public.to_sec1_point(false);
match (point.x(), point.y()) {
(Some(x), Some(y)) => Ok((x.to_vec(), y.to_vec())),
_ => Err("identity".to_owned()),
}
}
fn secret_on<C>(d: &[u8]) -> OracleResult<(Vec<u8>, Affine)>
where
C: CurveArithmetic,
AffinePoint<C>: FromSec1Point<C> + ToSec1Point<C>,
FieldBytesSize<C>: ModulusSize,
{
let secret = SecretKey::<C>::from_slice(d).map_err(|_| "scalar rejected")?;
Ok((secret.to_bytes().to_vec(), affine(&secret.public_key())?))
}
fn oracle_point(crv: EcCurve, octets: &[u8]) -> OracleResult<Affine> {
match crv {
EcCurve::P256 => point_on::<p256::NistP256>(octets),
EcCurve::P384 => point_on::<p384::NistP384>(octets),
EcCurve::P521 => point_on::<p521::NistP521>(octets),
EcCurve::Secp256k1 => point_on::<k256::Secp256k1>(octets),
}
}
fn oracle_secret(crv: EcCurve, d: &[u8]) -> OracleResult<(Vec<u8>, Affine)> {
match crv {
EcCurve::P256 => secret_on::<p256::NistP256>(d),
EcCurve::P384 => secret_on::<p384::NistP384>(d),
EcCurve::P521 => secret_on::<p521::NistP521>(d),
EcCurve::Secp256k1 => secret_on::<k256::Secp256k1>(d),
}
}
fn oracle_ec_private(crv: EcCurve, d: &[u8], embedded: Option<&[u8]>) -> OracleResult<Values> {
let (d, derived) = oracle_secret(crv, d)?;
let (x, y) = match embedded {
Some(octets) => oracle_point(crv, octets)?,
None => derived,
};
Ok(Values::Ec {
crv,
x,
y,
d: Some(d),
})
}
fn oracle_okp(crv: OkpCurve, d: Option<&[u8]>, x: Option<&[u8]>) -> OracleResult<Values> {
let d: Option<[u8; 32]> = d
.map(|d| d.try_into().map_err(|_| "secret is not 32 octets"))
.transpose()?;
let x: Option<[u8; 32]> = x
.map(|x| x.try_into().map_err(|_| "public key is not 32 octets"))
.transpose()?;
let x = match (crv, x, d) {
(OkpCurve::Ed25519, Some(x), _) => ed25519_dalek::VerifyingKey::from_bytes(&x)
.map_err(oracle_err)?
.to_bytes(),
(OkpCurve::Ed25519, None, Some(d)) => ed25519_dalek::SigningKey::from_bytes(&d)
.verifying_key()
.to_bytes(),
(OkpCurve::X25519, Some(x), _) => x,
(OkpCurve::X25519, None, Some(d)) => {
x25519_dalek::PublicKey::from(&x25519_dalek::StaticSecret::from(d)).to_bytes()
}
(_, None, None) => return Err("no key".to_owned()),
};
Ok(Values::Okp {
crv,
x: x.to_vec(),
d: d.map(|d| d.to_vec()),
})
}
fn oracle_key(kind: Kind, der: &[u8]) -> OracleResult<Option<Values>> {
let fields = oracle_fields(kind, der)?;
let alg = match &fields.algorithm {
Some(algorithm) => Some(oracle_alg(algorithm)?),
None => None,
};
let values = match (kind, alg) {
(Kind::Pkcs8, Some(OracleAlg::Ec(Some(crv)))) => {
let ec = fields.ec.as_ref().ok_or("no ECPrivateKey")?;
let embedded = fields.public_key.as_deref().or(ec.public_key.as_deref());
Some(oracle_ec_private(crv, &ec.private_key, embedded)?)
}
(Kind::Pkcs8, Some(OracleAlg::Ed25519)) => Some(oracle_okp(
OkpCurve::Ed25519,
fields.curve_private_key.as_deref(),
fields.public_key.as_deref(),
)?),
(Kind::Pkcs8, Some(OracleAlg::X25519)) => Some(oracle_okp(
OkpCurve::X25519,
fields.curve_private_key.as_deref(),
fields.public_key.as_deref(),
)?),
(Kind::Spki, Some(OracleAlg::Ec(Some(crv)))) => {
let (x, y) = oracle_point(crv, fields.public_key.as_deref().unwrap_or_default())?;
Some(Values::Ec { crv, x, y, d: None })
}
(Kind::Spki, Some(OracleAlg::Ed25519)) => Some(oracle_okp(
OkpCurve::Ed25519,
None,
fields.public_key.as_deref(),
)?),
(Kind::Spki, Some(OracleAlg::X25519)) => Some(oracle_okp(
OkpCurve::X25519,
None,
fields.public_key.as_deref(),
)?),
(Kind::Spki, Some(OracleAlg::Rsa)) | (Kind::Pkcs1, None) => {
let (n, e) = fields.rsa.ok_or("no RSAPublicKey")?;
Some(Values::Rsa { n, e })
}
(Kind::Sec1, None) => {
let ec = fields.ec.as_ref().ok_or("no ECPrivateKey")?;
let named = match &ec.parameters {
Some(named) => Some(ObjectIdentifier::from_der(named).map_err(oracle_err)?),
None => None,
};
match named.as_ref().and_then(curve_of_oid) {
Some(crv) => Some(oracle_ec_private(
crv,
&ec.private_key,
ec.public_key.as_deref(),
)?),
None => None,
}
}
_ => None,
};
Ok(values)
}
fn raw(dir: &str, name: &str) -> Vec<u8> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data/keys")
.join(dir)
.join(name);
std::fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()))
}
fn positive_fixtures() -> Vec<(&'static str, String)> {
["openssl", "rfc"]
.into_iter()
.flat_map(|dir| {
fixtures::files(dir, |name| !name.starts_with("reject-"))
.into_iter()
.map(move |name| (dir, name))
})
.collect()
}
fn load_positive(dir: &str, name: &str) -> Vec<u8> {
let bytes = raw(dir, name);
if !name.ends_with(".pem") {
return bytes;
}
let text = core::str::from_utf8(&bytes).unwrap();
let (label, der) = super::pem::decode(text).unwrap_or_else(|e| panic!("{name}: {e:?}"));
let (oracle_label, oracle_der) =
pem_rfc7468::decode_vec(&bytes).unwrap_or_else(|e| panic!("{name}: oracle: {e}"));
assert_eq!(label.as_str(), Some(oracle_label), "{dir}/{name}: label");
assert_eq!(*der, oracle_der, "{dir}/{name}: octets");
assert_eq!(
Kind::of_label(label),
Some(Kind::of_file(name)),
"{dir}/{name}: label and container"
);
der.to_vec()
}
#[test]
fn positive_fixtures_decode_to_the_same_fields_in_the_oracles() {
let fixtures = positive_fixtures();
for (dir, name) in &fixtures {
let der = load_positive(dir, name);
let kind = Kind::of_file(name);
let ours = our_fields(kind, &der).unwrap_or_else(|e| panic!("{dir}/{name}: {e:?}"));
let theirs = oracle_fields(kind, &der).unwrap_or_else(|e| panic!("{dir}/{name}: {e}"));
assert_eq!(ours, theirs, "{dir}/{name}");
let jwk = our_import(kind, &der).unwrap_or_else(|e| panic!("{dir}/{name}: {e:?}"));
let values = oracle_key(kind, &der)
.unwrap_or_else(|e| panic!("{dir}/{name}: oracle: {e}"))
.unwrap_or_else(|| panic!("{dir}/{name}: the oracle cannot check it"));
assert_eq!(values_of(&jwk), values, "{dir}/{name}");
}
assert_eq!(fixtures.len(), 75, "{fixtures:?}");
}
struct NegativeEntry {
id: String,
file: String,
base: String,
expect: String,
}
fn negative_entries() -> Vec<NegativeEntry> {
let manifest = crate::json::parse(&raw("negative", "manifest.json")).unwrap();
let field = |entry: &crate::json::Value, name: &str| {
entry
.get(name)
.and_then(crate::json::Value::as_str)
.unwrap()
.to_owned()
};
manifest
.as_array()
.unwrap()
.iter()
.map(|entry| NegativeEntry {
id: field(entry, "id"),
file: field(entry, "file"),
base: field(entry, "base"),
expect: field(entry, "expect"),
})
.collect()
}
fn negative_kind(entry: &NegativeEntry) -> Kind {
match entry.id.as_str() {
"N67" | "N68" => Kind::Pkcs8,
"N69" => Kind::Sec1,
_ => Kind::of_file(&entry.base),
}
}
fn has_rsa_private_base(entry: &NegativeEntry) -> bool {
entry.base.starts_with("reject-rsa") && entry.base.contains("-private-")
}
fn has_code<T>(result: &Result<T>, expect: &str) -> bool {
matches!(
(result, expect),
(Err(Error::InvalidKey(_)), "InvalidKey")
| (Err(Error::UnsupportedAlgorithm), "UnsupportedAlgorithm")
)
}
const NEGATIVE_STRICTER_THAN_ORACLE: &[(&str, &str)] = &[
(
"N20",
"id-Ed25519 with NULL parameters: RFC 8410 ยง3 \"the parameters MUST be absent\"; \
AlgorithmIdentifiers match their template byte for byte (ADR 0018 ยง2, ยง4 step 3)",
),
(
"N21",
"id-Ed448: not a JOSE curve, UnsupportedAlgorithm (ADR 0018 ยง4 step 1)",
),
(
"N22",
"OID subidentifier with a leading 0x80 octet: X.690 ยง8.19.2 requires the fewest \
octets; const-oid 0.10 decodes it (ADR 0018 ยง2 minimal OID subidentifiers)",
),
(
"N32",
"id-ecPublicKey without parameters: RFC 5480 ยง2.1.1 parameters \"MUST always be \
present\" as a namedCurve (ADR 0018 ยง4 step 3)",
),
(
"N33",
"namedCurve secp224r1: not a JOSE curve, UnsupportedAlgorithm (ADR 0018 ยง4 step 2)",
),
(
"N37",
"id-ecPublicKey with NULL (implicitCurve) parameters: RFC 5480 ยง2.1.1 \"MUST NOT be \
used in PKIX\" (ADR 0018 ยง4 step 3)",
),
(
"N43",
"a 31-octet EC privateKey: RFC 5915 ยง3 fixes ceiling(log2(n)/8) octets and RFC 7518 \
ยง6.2.2.1 the full coordinate length; SecretKey::from_slice left-pads it \
(ADR 0018 ยง2 strict acceptance, ยง3 validation)",
),
(
"N46",
"standalone EC PRIVATE KEY without [0]: RFC 5915 ยง3 \"MUST always include the \
parameters field\"; without it the curve is unknown (ADR 0018 ยง3 table)",
),
(
"N50",
"inner ECPrivateKey [0] names another curve than the PKCS#8 AlgorithmIdentifier: \
RFC 5915 ยง1 and ADR 0018 ยง3 consistency (\"SEC1 [0] must equal the PKCS#8 \
AlgorithmIdentifier curve\")",
),
(
"N52",
"inner ECPrivateKey [1] is another key's point: an embedded publicKey must be the \
one derived from the private key (ADR 0018 ยง3 consistency, ADR 0043 ยง3)",
),
(
"N60",
"rsaEncryption without NULL parameters: RFC 3279 ยง2.3.1 \"MUST have ASN.1 type \
NULL\" (ADR 0018 ยง4 step 3)",
),
(
"N63",
"publicExponent 1: RFC 8017 ยง3.1 needs an odd e with 3 <= e < n, checked by \
Jwk::from_object (ADR 0018 ยง3 validation); the oracle has no RSA primitive",
),
(
"N64",
"a 1024-bit modulus: RFC 7518 ยง3.3 and ยง4.2 need at least 2048 bits (ADR 0007, \
ADR 0018 ยง3 validation); the oracle has no RSA primitive",
),
(
"N67",
"PKCS#8 with rsaEncryption: no RSA private keys, UnsupportedAlgorithm before \
privateKey is opened (ADR 0021, ADR 0018 ยง4)",
),
];
#[test]
fn negative_vectors_are_rejected_and_every_oracle_acceptance_is_listed() {
let mut oracle_accepts = Vec::new();
let mut count = 0;
for entry in negative_entries()
.iter()
.filter(|e| e.file.ends_with(".der"))
{
let der = raw("negative", &entry.file);
let kind = negative_kind(entry);
let ours = our_import(kind, &der);
assert!(
has_code(&ours, &entry.expect),
"{}: expected {}, got {:?}",
entry.id,
entry.expect,
ours.map(|_| "a key")
);
if let Ok(values) = oracle_key(kind, &der) {
oracle_accepts.push((entry.id.clone(), values.is_some()));
}
count += 1;
}
let listed: Vec<&str> = NEGATIVE_STRICTER_THAN_ORACLE
.iter()
.map(|(id, _)| *id)
.collect();
let accepted: Vec<&str> = oracle_accepts.iter().map(|(id, _)| id.as_str()).collect();
assert_eq!(accepted, listed, "{oracle_accepts:?}");
assert_eq!(count, 50);
}
fn our_import_file(name: &str, bytes: &[u8]) -> Result<Jwk> {
if !name.ends_with(".pem") {
return our_import(Kind::of_file(name), bytes);
}
let text = core::str::from_utf8(bytes).map_err(|_| Error::InvalidKey("not UTF-8"))?;
let (label, der) = super::pem::decode(text)?;
if label == Label::RsaPrivate {
return Jwk::from_pem(text);
}
let kind = Kind::of_label(label).ok_or(Error::UnsupportedAlgorithm)?;
our_import(kind, &der)
}
fn oracle_file(name: &str, bytes: &[u8]) -> OracleResult<bool> {
if !name.ends_with(".pem") {
return oracle_key(Kind::of_file(name), bytes).map(|_| true);
}
let (label, der) = pem_rfc7468::decode_vec(bytes).map_err(oracle_err)?;
match Kind::of_label(Label::from_text(label)) {
Some(kind) => oracle_key(kind, &der).map(|_| true),
None => Ok(false),
}
}
const WHY_CURVE: &str = "a namedCurve outside the JOSE set (RFC 7518 ยง6.2.1.1, RFC 8812 ยง3.1): UnsupportedAlgorithm (ADR 0018 ยง4 step 2)";
const WHY_CERT: &str = "a CERTIFICATE label names no key structure: refused from the BEGIN line, UnsupportedAlgorithm (ADR 0018 ยง2, ยง4); pem-rfc7468 decodes any label";
const WHY_EXPLICIT: &str = "specifiedCurve parameters: RFC 5480 ยง2.1.1 \"MUST NOT be used in PKIX\", UnsupportedAlgorithm (ADR 0018 ยง4 step 2)";
const WHY_ENC: &str = "ENCRYPTED PRIVATE KEY (RFC 5958 ยง3): encrypted PKCS#8 is unsupported, refused from the BEGIN line (ADR 0018 ยง2, ยง4)";
const WHY_X448: &str =
"Ed448/X448 (RFC 8410 ยง3): not JOSE curves in 0.1.0, UnsupportedAlgorithm (ADR 0018 ยง4 step 1)";
const WHY_RSA1024: &str = "a 1024-bit RSA modulus: RFC 7518 ยง3.3 and ยง4.2 need at least 2048 bits (ADR 0007, ADR 0018 ยง3 validation); the oracle has no RSA primitive";
const WHY_RSA1024_PRIVATE: &str = "a 1024-bit RSA private key: RFC 7518 ยง3.3 and ยง4.2 need at least 2048 bits (ADR 0007); InvalidKey";
const WHY_RSA_MULTI: &str = "a three-prime RSA private key (RFC 8017 App. A.1.2 version multi): unsupported, UnsupportedAlgorithm (ADR 0067 ยง3)";
const WHY_PSS: &str = "id-RSASSA-PSS (RFC 4055 ยง3.1): an unsupported key OID, UnsupportedAlgorithm (ADR 0018 ยง4 step 1)";
const REJECT_FIXTURES_STRICTER_THAN_ORACLE: &[(&str, &str)] = &[
("reject-ec-brainpoolp256r1-pkcs8.pem", WHY_CURVE),
("reject-ec-brainpoolp256r1-spki.pem", WHY_CURVE),
("reject-ec-p256-cert.pem", WHY_CERT),
("reject-ec-p256-explicit-pkcs8.pem", WHY_EXPLICIT),
("reject-ec-p256-explicit-spki.pem", WHY_EXPLICIT),
("reject-ec-p256-pkcs8-encrypted.pem", WHY_ENC),
("reject-ed25519-pkcs8-encrypted.pem", WHY_ENC),
("reject-ed448-pkcs8.pem", WHY_X448),
("reject-ed448-spki.pem", WHY_X448),
("reject-rsa1024-pkcs1-pub.pem", WHY_RSA1024),
("reject-rsa1024-private-pkcs8.pem", WHY_RSA1024_PRIVATE),
("reject-rsa1024-spki.pem", WHY_RSA1024),
("reject-rsa2048-3prime-private-pkcs1.pem", WHY_RSA_MULTI),
("reject-rsa2048-3prime-private-pkcs8.pem", WHY_RSA_MULTI),
("reject-rsa2048-pkcs8-encrypted.pem", WHY_ENC),
("reject-rsapss2048-private-pkcs8.pem", WHY_PSS),
("reject-rsapss2048-spki.pem", WHY_PSS),
("reject-x448-pkcs8.pem", WHY_X448),
(
"reject-rfc9500-testRSA1024-private-pkcs1.pem",
WHY_RSA1024_PRIVATE,
),
];
const REJECT_FIXTURES_IMPORTED_SINCE_ADR_0067: &[&str] = &[
"reject-rsa2048-private-pkcs1.der",
"reject-rsa2048-private-pkcs1.pem",
"reject-rsa2048-private-pkcs8.der",
"reject-rsa2048-private-pkcs8.pem",
"reject-rsa3072-private-pkcs8.pem",
"reject-rsa4096-private-pkcs8.pem",
"reject-rfc9500-testRSA2048-private-pkcs1.pem",
"reject-rfc9500-testRSA4096-private-pkcs1.pem",
];
#[test]
fn reject_fixtures_are_rejected_and_every_oracle_acceptance_is_listed() {
let mut oracle_accepts = Vec::new();
let mut count = 0;
for dir in ["openssl", "rfc"] {
for name in fixtures::files(dir, |name| name.starts_with("reject-")) {
let bytes = raw(dir, &name);
let ours = our_import_file(&name, &bytes);
count += 1;
if REJECT_FIXTURES_IMPORTED_SINCE_ADR_0067.contains(&name.as_str()) {
let key = ours.unwrap_or_else(|e| panic!("{dir}/{name}: {e:?}"));
assert!(!key.is_public(), "{dir}/{name}");
continue;
}
let expect = if name.contains("rsa1024-pkcs1-pub")
|| name.contains("rsa1024-spki")
|| name.contains("rsa1024-private")
|| name.contains("testRSA1024")
|| name.contains("six-dash")
{
"InvalidKey"
} else {
"UnsupportedAlgorithm"
};
assert!(
has_code(&ours, expect),
"{dir}/{name}: expected {expect}, got {:?}",
ours.map(|_| "a key")
);
if let Ok(checked) = oracle_file(&name, &bytes) {
oracle_accepts.push((name.clone(), checked));
}
}
}
let listed: Vec<&str> = REJECT_FIXTURES_STRICTER_THAN_ORACLE
.iter()
.map(|(name, _)| *name)
.collect();
let accepted: Vec<&str> = oracle_accepts.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(accepted, listed, "{oracle_accepts:?}");
assert_eq!(count, 33);
}
fn oracle_ec_algorithm(curve: &ObjectIdentifier) -> ::spki::AlgorithmIdentifierRef<'_> {
::spki::AlgorithmIdentifierRef {
oid: ID_EC_PUBLIC_KEY,
parameters: Some(curve.into()),
}
}
fn seed_stream(seed: &[u8; 32], len: usize) -> Vec<u8> {
use sha2::Digest as _;
let mut stream = Vec::with_capacity(len + 64);
let mut counter = 0u64;
while stream.len() < len {
let block = sha2::Sha512::new()
.chain_update(seed)
.chain_update(counter.to_be_bytes())
.finalize();
stream.extend_from_slice(&block);
counter += 1;
}
stream.truncate(len);
stream
}
fn generated_ec(crv: EcCurve, seed: &[u8; 32]) -> Jwk {
for len in [16 * 1024, 1024 * 1024] {
let mut fixed = crate::rng::insecure_test_vectors::FixedRng::new(seed_stream(seed, len));
let rng = &mut crate::rng::CryptoRngAdapter::fixed(&mut fixed);
match super::generate::generate_ec(crv, rng) {
Err(Error::RandomnessUnavailable) => continue,
other => return other.unwrap(),
}
}
panic!("{crv:?}: 1 MiB of seed stream ran dry")
}
fn generated_okp(crv: OkpCurve, seed: &[u8; 32]) -> Jwk {
let mut fixed = crate::rng::insecure_test_vectors::FixedRng::new(seed_stream(seed, 32));
super::generate::generate_okp(crv, &mut crate::rng::CryptoRngAdapter::fixed(&mut fixed))
.unwrap()
}
fn check_generated_ec(jwk: &Jwk) {
let KeyMaterial::Ec(key) = jwk.material() else {
panic!("an EC key")
};
let values = values_of(jwk);
let crv = key.crv();
let d = key.d().unwrap();
let point = [&[0x04][..], key.x(), key.y()].concat();
let curve = curve_oid(crv);
let spki = super::ec::to_spki(key).unwrap();
let oracle_spki = ::spki::SubjectPublicKeyInfoRef {
algorithm: oracle_ec_algorithm(&curve),
subject_public_key: ::der::asn1::BitStringRef::from_bytes(&point).unwrap(),
}
.to_der()
.unwrap();
assert_eq!(*spki, oracle_spki, "SPKI");
let public = Values::Ec {
crv,
x: key.x().to_vec(),
y: key.y().to_vec(),
d: None,
};
assert_eq!(oracle_key(Kind::Spki, &spki), Ok(Some(public)));
let sec1 = super::ec::to_sec1(key).unwrap();
let oracle_sec1 = ::sec1::EcPrivateKey {
private_key: d,
parameters: Some(::sec1::EcParameters::NamedCurve(curve)),
public_key: Some(&point),
}
.to_der()
.unwrap();
assert_eq!(*sec1, oracle_sec1, "SEC1");
assert_eq!(
oracle_key(Kind::Sec1, &sec1).as_ref(),
Ok(&Some(values_ec(jwk)))
);
let pkcs8 = super::ec::to_pkcs8(key).unwrap();
let oracle_pkcs8 = |parameters: Option<::sec1::EcParameters>| {
let inner = ::sec1::EcPrivateKey {
private_key: d,
parameters,
public_key: Some(&point),
}
.to_der()
.unwrap();
::pkcs8::PrivateKeyInfoRef::new(
oracle_ec_algorithm(&curve),
OctetStringRef::new(&inner).unwrap(),
)
.to_der()
.unwrap()
};
assert_eq!(*pkcs8, oracle_pkcs8(None), "PKCS#8");
assert_eq!(
oracle_key(Kind::Pkcs8, &pkcs8).as_ref(),
Ok(&Some(values_ec(jwk)))
);
let with_curve = oracle_pkcs8(Some(::sec1::EcParameters::NamedCurve(curve)));
assert_ne!(*pkcs8, with_curve);
assert_eq!(
values_of(&our_import(Kind::Pkcs8, &with_curve).unwrap()),
values
);
for (kind, der) in [(Kind::Sec1, &sec1), (Kind::Pkcs8, &pkcs8)] {
assert_eq!(
values_of(&our_import(kind, der).unwrap()),
values,
"{kind:?}"
);
}
assert!(our_import(Kind::Spki, &spki).unwrap().is_public());
}
fn values_ec(jwk: &Jwk) -> Values {
values_of(jwk)
}
fn check_generated_okp(jwk: &Jwk) {
let KeyMaterial::Okp(key) = jwk.material() else {
panic!("an OKP key")
};
let values = values_of(jwk);
let algorithm = ::spki::AlgorithmIdentifierRef {
oid: match key.crv() {
OkpCurve::Ed25519 => ID_ED25519,
OkpCurve::X25519 => ID_X25519,
},
parameters: None,
};
let spki = super::okp::to_spki(key).unwrap();
let oracle_spki = ::spki::SubjectPublicKeyInfoRef {
algorithm,
subject_public_key: ::der::asn1::BitStringRef::from_bytes(key.x()).unwrap(),
}
.to_der()
.unwrap();
assert_eq!(*spki, oracle_spki, "SPKI");
let public = Values::Okp {
crv: key.crv(),
x: key.x().to_vec(),
d: None,
};
assert_eq!(oracle_key(Kind::Spki, &spki), Ok(Some(public)));
let pkcs8 = super::okp::to_pkcs8(key).unwrap();
let curve_private_key = OctetStringRef::new(key.d().unwrap())
.unwrap()
.to_der()
.unwrap();
let oracle_pkcs8 = ::pkcs8::PrivateKeyInfoRef::new(
algorithm,
OctetStringRef::new(&curve_private_key).unwrap(),
)
.to_der()
.unwrap();
assert_eq!(*pkcs8, oracle_pkcs8, "PKCS#8");
assert_eq!(
oracle_key(Kind::Pkcs8, &pkcs8).as_ref(),
Ok(&Some(values_of(jwk)))
);
assert_eq!(values_of(&our_import(Kind::Pkcs8, &pkcs8).unwrap()), values);
assert!(our_import(Kind::Spki, &spki).unwrap().is_public());
}
mod generated {
use proptest::prelude::*;
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn p256_keys_encode_and_decode_like_the_oracles(seed in any::<[u8; 32]>()) {
check_generated_ec(&generated_ec(EcCurve::P256, &seed));
}
#[test]
fn p384_keys_encode_and_decode_like_the_oracles(seed in any::<[u8; 32]>()) {
check_generated_ec(&generated_ec(EcCurve::P384, &seed));
}
#[test]
fn p521_keys_encode_and_decode_like_the_oracles(seed in any::<[u8; 32]>()) {
check_generated_ec(&generated_ec(EcCurve::P521, &seed));
}
#[test]
fn secp256k1_keys_encode_and_decode_like_the_oracles(seed in any::<[u8; 32]>()) {
check_generated_ec(&generated_ec(EcCurve::Secp256k1, &seed));
}
#[test]
fn ed25519_keys_encode_and_decode_like_the_oracles(seed in any::<[u8; 32]>()) {
check_generated_okp(&generated_okp(OkpCurve::Ed25519, &seed));
}
#[test]
fn x25519_keys_encode_and_decode_like_the_oracles(seed in any::<[u8; 32]>()) {
check_generated_okp(&generated_okp(OkpCurve::X25519, &seed));
}
}
}
fn der_length(len: usize) -> Vec<u8> {
match len {
0..=0x7F => vec![len as u8],
_ => {
let octets: Vec<u8> = len
.to_be_bytes()
.into_iter()
.skip_while(|octet| *octet == 0)
.collect();
[&[0x80 | octets.len() as u8][..], &octets].concat()
}
}
}
fn tlv(tag: u8, contents: &[u8]) -> Vec<u8> {
[&[tag][..], &der_length(contents.len()), contents].concat()
}
fn read_one<'a, T>(input: &'a [u8], read: impl FnOnce(&mut Reader<'a>) -> Result<T>) -> Result<T> {
let mut reader = Reader::new(input)?;
let value = read(&mut reader)?;
reader.finish()?;
Ok(value)
}
fn oid_beyond_const_oid(contents: &[u8]) -> bool {
let mut wide = false;
let mut value = 0u128;
for &octet in &contents[1..] {
value = (value << 7) | u128::from(octet & 0x7F);
wide |= value > u128::from(u32::MAX);
if octet & 0x80 == 0 {
value = 0;
}
value &= u128::from(u64::MAX);
}
contents.len() > 39 || contents[0] >= 0x78 || wide
}
fn oid_has_padded_subidentifier(contents: &[u8]) -> bool {
let mut at_start = true;
contents.iter().any(|&octet| {
let padded = at_start && octet == 0x80;
at_start = octet & 0x80 == 0;
padded
})
}
fn compare_typed_readers(input: &[u8]) {
use ::der::asn1::{BitStringRef, IntRef, Null, UintRef};
let ours = read_one(input, |r| r.integer());
let theirs = IntRef::from_der(input);
assert_eq!(ours.is_ok(), theirs.is_ok(), "INTEGER {input:02x?}");
if let Ok(int) = theirs {
assert_eq!(int.to_der().unwrap(), input, "INTEGER re-encoding");
}
let ours = read_one(input, |r| r.unsigned_integer());
let theirs = UintRef::from_der(input);
assert_eq!(
ours.ok(),
theirs.ok().map(|uint| uint.as_bytes()),
"unsigned INTEGER {input:02x?}"
);
let ours = read_one(input, |r| r.octet_string());
let theirs = <&OctetStringRef>::from_der(input);
assert_eq!(
ours.ok(),
theirs.ok().map(OctetStringRef::as_bytes),
"OCTET STRING {input:02x?}"
);
let ours = read_one(input, |r| r.bit_string());
let theirs = BitStringRef::from_der(input);
match (&ours, &theirs) {
(Ok(octets), Ok(bits)) => {
assert_eq!(bits.unused_bits(), 0, "BIT STRING {input:02x?}");
assert_eq!(Some(*octets), bits.as_bytes(), "BIT STRING {input:02x?}");
assert_eq!(bits.to_der().unwrap(), input, "BIT STRING re-encoding");
}
(Err(_), Ok(bits)) => assert_ne!(bits.unused_bits(), 0, "BIT STRING {input:02x?}"),
(Ok(_), Err(_)) => panic!("BIT STRING laxer than der: {input:02x?}"),
(Err(_), Err(_)) => {}
}
let ours = read_one(input, |r| r.null());
assert_eq!(
ours.is_ok(),
Null::from_der(input).is_ok(),
"NULL {input:02x?}"
);
let ours = read_one(input, |r| r.oid());
let theirs = <ObjectIdentifier>::from_der(input);
match (&ours, &theirs) {
(Ok(encoding), Ok(oid)) => assert_eq!(oid.to_der().unwrap(), *encoding, "OID"),
(Ok(_), Err(_)) => assert!(
oid_beyond_const_oid(&input[2..]),
"OID laxer than der: {input:02x?}"
),
(Err(_), Ok(_)) => assert!(
oid_has_padded_subidentifier(oid_contents(input)),
"OID stricter than der, undocumented: {input:02x?}"
),
(Err(_), Err(_)) => {}
}
}
fn oid_contents(input: &[u8]) -> &[u8] {
&input[2..]
}
#[test]
fn oid_reader_differences_with_const_oid_are_the_documented_ones() {
let wider = [
tlv(0x06, &[0x78]),
tlv(0x06, &[0x88, 0x37]),
tlv(0x06, &[0x2B, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F]),
tlv(0x06, &[0x2B; 40]),
];
for input in &wider {
let contents = &input[1 + der_length(input.len() - 2).len()..];
assert!(read_one(input, |r| r.oid()).is_ok(), "{input:02x?}");
assert!(<ObjectIdentifier>::from_der(input).is_err(), "{input:02x?}");
assert!(oid_beyond_const_oid(contents), "{input:02x?}");
}
let padded = tlv(0x06, &[0x2B, 0x80, 0x65, 0x70]);
assert!(read_one(&padded, |r| r.oid()).is_err());
assert!(<ObjectIdentifier>::from_der(&padded).is_ok());
for template in [
oid::OID_EC_PUBLIC_KEY,
oid::OID_P256,
oid::OID_P384,
oid::OID_P521,
oid::OID_SECP256K1,
oid::OID_X25519,
oid::OID_ED25519,
oid::OID_RSA_ENCRYPTION,
] {
compare_typed_readers(template);
}
}
mod reader {
use proptest::collection::vec;
use proptest::prelude::*;
use super::*;
fn element() -> impl Strategy<Value = Vec<u8>> {
let tag = prop::sample::select(vec![
0x02u8, 0x03, 0x04, 0x05, 0x06, 0x22, 0x23, 0x24, 0x25, 0x26, 0x1F, 0x30, 0x81,
]);
let lead = prop::sample::select(vec![
vec![],
vec![0x00],
vec![0xFF],
vec![0x80],
vec![0x00, 0x80],
vec![0xFF, 0x7F],
vec![0x07],
vec![0x2B],
vec![0x2B, 0x80],
]);
(
tag,
lead,
vec(any::<u8>(), 0..40),
0u8..6,
vec(any::<u8>(), 0..2),
)
.prop_map(|(tag, lead, rest, form, trailing)| {
let contents = [lead, rest].concat();
let len = contents.len();
let length = match form {
0 | 1 => der_length(len),
2 => [&[0x81][..], &[len as u8]].concat(),
3 => der_length(len + 1),
4 => der_length(len.saturating_sub(1)),
_ => vec![0x80],
};
let mut out = [&[tag][..], &length, &contents].concat();
if form == 1 {
out.extend_from_slice(&trailing);
}
out
})
}
fn object_identifier() -> impl Strategy<Value = Vec<u8>> {
let arc = prop_oneof![0u64..128, 128u64..1 << 21, any::<u64>()];
(vec((arc, any::<bool>()), 1..6), 0u8..128).prop_map(|(arcs, root)| {
let mut contents = vec![root];
for (value, pad) in arcs {
let mut groups = Vec::new();
let mut v = value;
loop {
groups.push((v & 0x7F) as u8);
v >>= 7;
if v == 0 {
break;
}
}
if pad && groups.len() < 10 {
groups.push(0);
}
for (i, group) in groups.iter().enumerate().rev() {
contents.push(if i == 0 { *group } else { group | 0x80 });
}
}
tlv(0x06, &contents)
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn typed_readers_agree_with_der(input in element()) {
compare_typed_readers(&input);
}
#[test]
fn oid_reader_agrees_with_const_oid_but_for_documented_cases(
input in object_identifier()
) {
compare_typed_readers(&input);
}
}
}
fn length_fields(buf: &[u8]) -> Vec<(usize, usize, usize)> {
let mut out = Vec::new();
let mut stack = vec![(0usize, buf.len(), 0u8)];
while let Some((start, end, depth)) = stack.pop() {
let Ok(mut reader) = Reader::new(&buf[start..end]) else {
continue;
};
let mut at = start;
while let Ok(element) = reader.element() {
let header = element.encoding.len() - element.contents.len();
out.push((at + 1, header - 1, element.contents.len()));
let contents = at + header;
let contents_end = contents + element.contents.len();
if depth < 8 {
if element.tag & 0x20 != 0 || element.tag == 0x04 {
stack.push((contents, contents_end, depth + 1));
} else if matches!(element.tag, 0x03 | 0x81) && contents < contents_end {
stack.push((contents + 1, contents_end, depth + 1));
}
}
at = contents_end;
}
}
out
}
#[derive(Debug, Clone)]
enum Mutation {
Flip(proptest::sample::Index, u8),
Insert(proptest::sample::Index, u8),
Delete(proptest::sample::Index),
Length(proptest::sample::Index, u8),
}
fn apply(seed: &[u8], mutations: &[Mutation]) -> Vec<u8> {
let mut m = seed.to_vec();
for mutation in mutations {
match mutation {
Mutation::Flip(at, bit) if !m.is_empty() => {
let i = at.index(m.len());
m[i] ^= 1 << bit;
}
Mutation::Insert(at, octet) => m.insert(at.index(m.len() + 1), *octet),
Mutation::Delete(at) if !m.is_empty() => {
m.remove(at.index(m.len()));
}
Mutation::Length(at, form) => {
let fields = length_fields(&m);
if fields.is_empty() {
continue;
}
let (start, count, len) = fields[at.index(fields.len())];
let replacement = match form {
0 if len < 0x80 => vec![0x81, len as u8],
0 => {
let mut long = der_length(len);
long[0] += 1;
long.insert(1, 0x00);
long
}
1 => der_length(len + 1),
2 => der_length(len.saturating_sub(1)),
_ => vec![0x80],
};
m.splice(start..start + count, replacement);
}
_ => {}
}
}
m
}
fn without_attributes(der: &[u8]) -> Vec<u8> {
let mut outer = Reader::new(der).unwrap();
let mut fields = outer.sequence().unwrap();
let mut body = Vec::new();
while let Ok(element) = fields.element() {
if element.tag != super::der::CONTEXT_0 {
body.extend_from_slice(element.encoding);
}
}
tlv(super::der::SEQUENCE, &body)
}
fn check_canonical(m: &[u8]) -> bool {
let accepted = Kind::ALL.iter().any(|kind| our_fields(*kind, m).is_ok());
if let Ok(parts) = super::pkcs8::decode(m) {
assert_eq!(
*super::pkcs8::encode(&parts).unwrap(),
without_attributes(m)
);
if let (AlgId::Ec(_), Ok(inner)) = (parts.alg, super::sec1::decode(parts.private_key)) {
assert_eq!(*super::sec1::encode(&inner).unwrap(), parts.private_key);
}
}
if let Ok(parts) = super::spki::decode(m) {
assert_eq!(*super::spki::encode(&parts).unwrap(), m);
}
if let Ok(key) = super::sec1::decode(m) {
assert_eq!(*super::sec1::encode(&key).unwrap(), m);
}
match super::pkcs1::decode(m) {
Ok(super::pkcs1::Pkcs1Key::Public(key)) => {
assert_eq!(*super::pkcs1::encode_public(key.n, key.e).unwrap(), m);
}
Ok(super::pkcs1::Pkcs1Key::Private(key)) => {
assert_eq!(*super::pkcs1::encode_private(&key).unwrap(), m);
}
Err(_) => {}
}
if let Ok(key) = super::pkcs1::decode_public(m) {
assert_eq!(*super::pkcs1::encode_public(key.n, key.e).unwrap(), m);
}
for kind in Kind::ALL {
if let Ok(fields) = our_fields(kind, m) {
assert_eq!(oracle_fields(kind, m), Ok(fields), "{kind:?} {m:02x?}");
}
if our_import(kind, m).is_ok() {
assert!(
oracle_key(kind, m).is_ok(),
"{kind:?} laxer than the oracle: {m:02x?}"
);
}
}
accepted
}
#[test]
fn mutated_containers_re_encode_exactly_when_accepted() {
use proptest::collection::vec;
use proptest::prelude::*;
use proptest::test_runner::{Config, TestRunner};
let mut seeds: Vec<Vec<u8>> = positive_fixtures()
.iter()
.map(|(dir, name)| load_positive(dir, name))
.collect();
seeds.sort();
seeds.dedup();
for seed in &seeds {
assert!(check_canonical(seed));
}
let mutation = prop_oneof![
(any::<proptest::sample::Index>(), 0u8..8).prop_map(|(at, bit)| Mutation::Flip(at, bit)),
(any::<proptest::sample::Index>(), any::<u8>())
.prop_map(|(at, octet)| Mutation::Insert(at, octet)),
any::<proptest::sample::Index>().prop_map(Mutation::Delete),
(any::<proptest::sample::Index>(), 0u8..4)
.prop_map(|(at, form)| Mutation::Length(at, form)),
];
let strategy = (0..seeds.len(), vec(mutation, 1..4));
let mut runner = TestRunner::new(Config::with_cases(1000));
let accepted = core::cell::Cell::new(0u32);
runner
.run(&strategy, |(seed, mutations)| {
if check_canonical(&apply(&seeds[seed], &mutations)) {
accepted.set(accepted.get() + 1);
}
Ok(())
})
.unwrap();
assert!(accepted.get() >= 50, "only {} accepted", accepted.get());
}
const WRITTEN_LABELS: [Label; 4] = [
Label::PrivateKey,
Label::PublicKey,
Label::EcPrivateKey,
Label::RsaPublic,
];
fn our_pem(text: &[u8]) -> Option<(&'static str, Vec<u8>)> {
let text = core::str::from_utf8(text).ok()?;
let (label, der) = super::pem::decode(text).ok()?;
Some((label.as_str()?, der.to_vec()))
}
fn oracle_pem(text: &[u8]) -> Option<(String, Vec<u8>)> {
pem_rfc7468::decode_vec(text)
.ok()
.map(|(label, der)| (label.to_owned(), der))
}
fn trim_pem(text: &[u8]) -> &[u8] {
let is_ws = |c: &u8| matches!(c, b' ' | b'\t' | b'\r' | b'\n');
let start = text.iter().position(|c| !is_ws(c)).unwrap_or(text.len());
let end = text
.iter()
.rposition(|c| !is_ws(c))
.map_or(start, |i| i + 1);
&text[start..end]
}
fn check_pem(text: &[u8]) {
let Some(ours) = our_pem(text) else {
return;
};
let theirs = oracle_pem(text).or_else(|| oracle_pem(trim_pem(text)));
let theirs = theirs.unwrap_or_else(|| {
panic!(
"PEM laxer than pem-rfc7468: {:?}",
String::from_utf8_lossy(text)
)
});
assert_eq!((ours.0, &ours.1), (theirs.0.as_str(), &theirs.1));
}
const PEM_VECTOR_DIFFERENCES: &[(&str, bool, bool, &str)] = &[
(
"A03",
true,
false,
"blank lines after the END line: polyjose takes any whitespace around the block \
(ADR 0018 ยง2); pem-rfc7468 at most one eol. The trimmed block decodes alike \
(check_pem)",
),
(
"P07",
false,
true,
"explanatory text before the BEGIN line: rejected (ADR 0018 ยง2; RFC 7468 ยง2 \
permits it, ยง3 lets the parser choose); pem-rfc7468 skips a preamble",
),
(
"P10",
false,
true,
"a lower-case label: RFC 7468 ยง2 labels are \"formally case-sensitive, \
uppercase\" and polyjose knows only the exact upper-case labels (ADR 0018 ยง2); \
pem-rfc7468 takes any labelchar",
),
(
"P13",
false,
true,
"label DSA PRIVATE KEY: names no supported structure, UnsupportedAlgorithm from \
the BEGIN line (ADR 0018 ยง2, ยง4); pem-rfc7468 decodes any label",
),
];
#[test]
fn pem_vectors_differ_from_pem_rfc7468_only_as_listed() {
let mut vectors: Vec<(String, Vec<u8>)> = negative_entries()
.into_iter()
.filter(|entry| entry.file.ends_with(".pem"))
.map(|entry| (entry.id, raw("negative", &entry.file)))
.collect();
for name in fixtures::files("rfc", |name| name.starts_with("reject-rfc8410")) {
let bytes = raw("rfc", &name);
vectors.push((name, bytes));
}
let mut differences = Vec::new();
for (id, text) in &vectors {
let ours = our_pem(text);
let theirs = oracle_pem(text);
match (&ours, &theirs) {
(Some(ours), Some(theirs)) => {
assert_eq!((ours.0, &ours.1), (theirs.0.as_str(), &theirs.1), "{id}");
}
(None, None) => {}
_ => differences.push((id.clone(), ours.is_some(), theirs.is_some())),
}
check_pem(text);
}
let listed: Vec<(String, bool, bool)> = PEM_VECTOR_DIFFERENCES
.iter()
.map(|(id, ours, theirs, _)| ((*id).to_owned(), *ours, *theirs))
.collect();
assert_eq!(differences, listed);
assert_eq!(vectors.len(), 23);
}
mod pem_properties {
use proptest::collection::vec;
use proptest::prelude::*;
use super::*;
#[derive(Debug, Clone)]
enum Edit {
Replace(prop::sample::Index, u8),
Insert(prop::sample::Index, u8),
Delete(prop::sample::Index),
Prefix(Vec<u8>),
Suffix(Vec<u8>),
LineEndings(u8),
}
const ALPHABET: &[u8] = b"\n\r \t-AZaz09+/=:x\0";
fn interesting() -> impl Strategy<Value = u8> {
prop::sample::select(ALPHABET)
}
fn edit() -> impl Strategy<Value = Edit> {
prop_oneof![
(any::<prop::sample::Index>(), interesting()).prop_map(|(at, c)| Edit::Replace(at, c)),
(any::<prop::sample::Index>(), interesting()).prop_map(|(at, c)| Edit::Insert(at, c)),
any::<prop::sample::Index>().prop_map(Edit::Delete),
vec(interesting(), 1..4).prop_map(Edit::Prefix),
vec(interesting(), 1..4).prop_map(Edit::Suffix),
(0u8..2).prop_map(Edit::LineEndings),
]
}
fn apply_edits(text: &[u8], edits: &[Edit]) -> Vec<u8> {
let mut t = text.to_vec();
for edit in edits {
match edit {
Edit::Replace(at, c) if !t.is_empty() => {
let i = at.index(t.len());
t[i] = *c;
}
Edit::Insert(at, c) => t.insert(at.index(t.len() + 1), *c),
Edit::Delete(at) if !t.is_empty() => {
t.remove(at.index(t.len()));
}
Edit::Prefix(p) => t.splice(0..0, p.iter().copied()).for_each(drop),
Edit::Suffix(p) => t.extend_from_slice(p),
Edit::LineEndings(form) => {
let eol: &[u8] = if *form == 0 { b"\r\n" } else { b"\r" };
t = t
.iter()
.flat_map(|&c| if c == b'\n' { eol.to_vec() } else { vec![c] })
.collect();
}
_ => {}
}
}
t
}
fn seeds() -> Vec<Vec<u8>> {
let mut seeds: Vec<Vec<u8>> = positive_fixtures()
.into_iter()
.filter(|(_, name)| name.ends_with(".pem"))
.map(|(dir, name)| raw(dir, &name))
.collect();
seeds.extend(
negative_entries()
.into_iter()
.filter(|entry| entry.id.starts_with('A'))
.map(|entry| raw("negative", &entry.file)),
);
seeds
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn encoded_pem_is_what_pem_rfc7468_writes_and_reads(
der in vec(any::<u8>(), 1..700),
label in prop::sample::select(WRITTEN_LABELS.to_vec()),
) {
let text = super::super::pem::encode(label, &der).unwrap();
let label_text = label.as_str().unwrap();
let oracle = pem_rfc7468::encode_string(label_text, pem_rfc7468::LineEnding::LF, &der)
.unwrap();
prop_assert_eq!(text.as_str(), oracle.as_str());
let (decoded_label, decoded) = pem_rfc7468::decode_vec(text.as_bytes()).unwrap();
prop_assert_eq!(decoded_label, label_text);
prop_assert_eq!(&decoded, &der);
let (our_label, our_der) = super::super::pem::decode(&text).unwrap();
prop_assert_eq!(our_label, label);
prop_assert_eq!(&*our_der, &der);
}
}
#[test]
fn edited_pem_is_never_laxer_than_pem_rfc7468() {
use proptest::test_runner::{Config, TestRunner};
let seeds = seeds();
for seed in &seeds {
check_pem(seed);
}
let strategy = (0..seeds.len(), vec(edit(), 1..4));
let mut runner = TestRunner::new(Config::with_cases(1000));
let accepted = core::cell::Cell::new(0u32);
runner
.run(&strategy, |(seed, edits)| {
let text = apply_edits(&seeds[seed], &edits);
check_pem(&text);
if our_pem(&text).is_some() {
accepted.set(accepted.get() + 1);
}
Ok(())
})
.unwrap();
assert!(accepted.get() >= 50, "only {} accepted", accepted.get());
}
}
const _: () = {
const fn zeroize_on_drop<T: zeroize::ZeroizeOnDrop + ?Sized>() {}
const fn borrowed_view<T: Copy>() {}
zeroize_on_drop::<zeroize::Zeroizing<Vec<u8>>>();
zeroize_on_drop::<zeroize::Zeroizing<String>>();
zeroize_on_drop::<Jwk>();
zeroize_on_drop::<KeyMaterial>();
zeroize_on_drop::<crate::jwk::EcKey>();
zeroize_on_drop::<crate::jwk::OkpKey>();
borrowed_view::<super::types::Pkcs8Parts<'_>>();
borrowed_view::<super::sec1::EcPrivateKey<'_>>();
};
type Secret = Result<zeroize::Zeroizing<Vec<u8>>>;
type LabelledSecret = Result<(Label, zeroize::Zeroizing<Vec<u8>>)>;
const _: fn(&str) -> LabelledSecret = super::pem::decode;
const _: fn(Label, &[u8]) -> Result<zeroize::Zeroizing<String>> = super::pem::encode;
const _: fn(&super::types::Pkcs8Parts<'_>) -> Secret = super::pkcs8::encode;
const _: fn(&super::sec1::EcPrivateKey<'_>) -> Secret = super::sec1::encode;
const _: fn(&crate::jwk::EcKey) -> Secret = super::ec::to_pkcs8;
const _: fn(&crate::jwk::EcKey) -> Secret = super::ec::to_sec1;
const _: fn(&crate::jwk::OkpKey) -> Secret = super::okp::to_pkcs8;
const MARKER: &[u8; 16] = b"POLYJOSE-MARKER!";
fn marker_secret(len: usize) -> Vec<u8> {
let (prefix, rest): (&[u8], usize) = if len == 66 {
(&[0x00, 0x01], 64)
} else {
(&[], len)
};
[prefix, &MARKER.repeat(rest.div_ceil(16))[..rest]].concat()
}
fn leak_forms(secret: &[u8]) -> Vec<String> {
use core::fmt::Write as _;
let mut forms = vec![String::from_utf8_lossy(MARKER).into_owned()];
for window in secret.windows(8) {
let mut lower = String::new();
for octet in window {
write!(lower, "{octet:02x}").unwrap();
}
forms.push(lower.to_uppercase());
forms.push(lower);
let decimal = format!("{window:?}");
forms.push(decimal[1..decimal.len() - 1].to_owned());
let hex = format!("{window:02x?}");
forms.push(hex[1..hex.len() - 1].to_owned());
if window.iter().all(u8::is_ascii_graphic) {
forms.push(String::from_utf8_lossy(window).into_owned());
}
}
let base64url = crate::util::b64url_encode(secret);
let mut base64 = String::new();
crate::util::b64_encode_append(secret, &mut base64);
for encoded in [base64url, base64] {
let chars: Vec<char> = encoded.chars().collect();
for window in chars.windows(11) {
forms.push(window.iter().collect());
}
}
forms
}
fn render_der(der: &[u8], import: Option<Kind>, out: &mut std::collections::BTreeSet<String>) {
fn push<T>(
out: &mut std::collections::BTreeSet<String>,
result: &Result<T>,
ok: Option<String>,
) {
match result {
Err(error) => {
out.insert(format!("{error:?}"));
out.insert(format!("{error}"));
}
Ok(_) => {
out.extend(ok);
}
}
}
let pkcs8 = super::pkcs8::decode(der);
push(
out,
&pkcs8,
pkcs8.as_ref().ok().map(|parts| format!("{parts:?}")),
);
if let Ok(parts) = &pkcs8 {
let inner = super::sec1::decode(parts.private_key);
push(
out,
&inner,
inner.as_ref().ok().map(|key| format!("{key:?}")),
);
}
let spki = super::spki::decode(der);
push(
out,
&spki,
spki.as_ref().ok().map(|parts| format!("{parts:?}")),
);
let sec1 = super::sec1::decode(der);
push(out, &sec1, sec1.as_ref().ok().map(|key| format!("{key:?}")));
push(out, &super::pkcs1::decode(der), None);
push(out, &super::pkcs1::decode_public(der), None);
let kinds: Vec<Kind> = import.map_or(Kind::ALL.to_vec(), |kind| vec![kind]);
for kind in kinds {
let jwk = our_import(kind, der);
push(out, &jwk, jwk.as_ref().ok().map(|jwk| format!("{jwk:?}")));
}
}
fn render_pem(text: &[u8], out: &mut std::collections::BTreeSet<String>) {
if let Ok(text) = core::str::from_utf8(text) {
match super::pem::decode(text) {
Err(error) => {
out.insert(format!("{error:?}"));
out.insert(format!("{error}"));
}
Ok((label, der)) => {
out.insert(format!("{label:?}"));
if let Some(kind) = Kind::of_label(label) {
render_der(&der, Some(kind), out);
}
}
}
}
}
fn private_key_of(kind: Kind, der: &[u8]) -> Option<Vec<u8>> {
match values_of(&our_import(kind, der).ok()?) {
Values::Ec { d, .. } | Values::Okp { d, .. } => d,
Values::Rsa { .. } => None,
}
}
fn marker_keys() -> Vec<(Kind, Vec<u8>, Vec<u8>)> {
let mut keys = Vec::new();
for alg in [AlgId::Ed25519, AlgId::X25519] {
let d = marker_secret(32);
let curve_private_key = tlv(super::der::OCTET_STRING, &d);
let v1 = super::pkcs8::encode(&super::types::Pkcs8Parts {
version: 0,
alg,
private_key: &curve_private_key,
public_key: None,
})
.unwrap();
let Values::Okp { x, .. } = values_of(&our_import(Kind::Pkcs8, &v1).unwrap()) else {
panic!("an OKP key")
};
let v2 = super::pkcs8::encode(&super::types::Pkcs8Parts {
version: 1,
alg,
private_key: &curve_private_key,
public_key: Some(&x),
})
.unwrap();
keys.push((Kind::Pkcs8, v1.to_vec(), d.clone()));
keys.push((Kind::Pkcs8, v2.to_vec(), d));
}
for crv in [
EcCurve::P256,
EcCurve::P384,
EcCurve::P521,
EcCurve::Secp256k1,
] {
let d = marker_secret(crv.coordinate_len());
let bare = super::sec1::encode(&super::sec1::EcPrivateKey {
private_key: &d,
parameters: Some(crv),
public_key: None,
})
.unwrap();
let jwk = super::ec::from_sec1(&bare).unwrap();
let KeyMaterial::Ec(key) = jwk.material() else {
panic!("an EC key")
};
keys.push((
Kind::Sec1,
super::ec::to_sec1(key).unwrap().to_vec(),
d.clone(),
));
keys.push((Kind::Pkcs8, super::ec::to_pkcs8(key).unwrap().to_vec(), d));
}
keys
}
#[test]
fn no_error_or_debug_output_shows_private_key_octets() {
use std::collections::BTreeSet;
type Input = (Option<Kind>, Vec<u8>, Option<Vec<u8>>, bool);
let mut inputs: Vec<Input> = Vec::new();
for (kind, der, d) in marker_keys() {
let text = super::pem::encode(
if kind == Kind::Sec1 {
Label::EcPrivateKey
} else {
Label::PrivateKey
},
&der,
)
.unwrap();
inputs.push((None, text.as_bytes().to_vec(), Some(d.clone()), false));
inputs.push((Some(kind), der, Some(d), true));
}
for (dir, name) in positive_fixtures() {
let kind = Kind::of_file(&name);
let der = load_positive(dir, &name);
let d = private_key_of(kind, &der);
if name.ends_with(".pem") {
inputs.push((None, raw(dir, &name), d.clone(), false));
}
inputs.push((Some(kind), der, d, false));
}
for entry in negative_entries() {
if has_rsa_private_base(&entry) {
continue;
}
let bytes = raw("negative", &entry.file);
let base_kind = Kind::of_file(&entry.base);
let d = private_key_of(base_kind, &load_positive("rfc", &entry.base));
if entry.file.ends_with(".pem") {
inputs.push((None, bytes, d, false));
} else {
inputs.push((Some(negative_kind(&entry)), bytes, d, true));
}
}
for dir in ["openssl", "rfc"] {
for name in fixtures::files(dir, |name| name.starts_with("reject-")) {
let bytes = raw(dir, &name);
let kind = (!name.ends_with(".pem")).then(|| Kind::of_file(&name));
inputs.push((kind, bytes, None, false));
}
}
let mut rendered: Vec<(Vec<u8>, BTreeSet<String>)> = Vec::new();
let mut details = BTreeSet::new();
for (kind, bytes, secret, flip) in &inputs {
let mut out = BTreeSet::new();
let mut variants: Vec<Vec<u8>> = (0..=bytes.len()).map(|n| bytes[..n].to_vec()).collect();
if *flip {
for i in 0..bytes.len() {
for bit in [0x01, 0x80] {
let mut flipped = bytes.clone();
flipped[i] ^= bit;
variants.push(flipped);
}
}
}
for variant in &variants {
match kind {
Some(kind) => render_der(variant, Some(*kind), &mut out),
None => render_pem(variant, &mut out),
}
}
details.extend(out.iter().filter(|s| s.starts_with("invalid key")).cloned());
rendered.push((secret.clone().unwrap_or_default(), out));
}
for (secret, texts) in &rendered {
let forms = leak_forms(secret);
for text in texts {
for form in &forms {
assert!(!text.contains(form.as_str()), "{text:?} shows {form:?}");
}
}
}
assert!(details.len() >= 40, "{details:#?}");
}
#[test]
fn rsa_private_key_octets_never_reach_an_error() {
use std::collections::BTreeSet;
let check = |name: &str, bytes: &[u8]| {
let der = if name.ends_with(".pem") {
::pem_rfc7468::decode_vec(bytes).unwrap().1
} else {
bytes.to_vec()
};
let mut out = BTreeSet::new();
if name.ends_with(".pem") {
render_pem(bytes, &mut out);
}
render_der(&der, None, &mut out);
for n in (0..=der.len()).step_by(7) {
render_der(&der[..n], None, &mut out);
}
let forms = leak_forms(&der[40..]);
for text in &out {
for form in &forms {
assert!(
!text.contains(form.as_str()),
"{name}: {text:?} shows {form:?}"
);
}
}
};
let mut checked = 0;
for dir in ["openssl", "rfc"] {
let names = fixtures::files(dir, |name| {
name.starts_with("reject-") && name.contains("rsa") && name.contains("private")
});
for name in names {
check(&name, &raw(dir, &name));
checked += 1;
}
}
let relabelled: Vec<NegativeEntry> = negative_entries()
.into_iter()
.filter(has_rsa_private_base)
.collect();
let ids: Vec<&str> = relabelled.iter().map(|entry| entry.id.as_str()).collect();
assert_eq!(ids, ["N68", "N69", "P17", "P18"]);
for entry in &relabelled {
check(&entry.file, &raw("negative", &entry.file));
checked += 1;
}
assert!(checked >= 14, "only {checked} RSA private inputs");
}