use core::fmt;
use zeroize::Zeroizing;
use super::der::{self, Reader, Writer};
use super::{oid, pkcs1};
use crate::error::{Error, Result};
use crate::jwk::EcCurve;
const EC_PRIVKEY_VER1: u8 = 1;
const CURVES: [EcCurve; 4] = [
EcCurve::P256,
EcCurve::P384,
EcCurve::P521,
EcCurve::Secp256k1,
];
#[derive(Clone, Copy)]
pub(crate) struct EcPrivateKey<'a> {
pub(crate) private_key: &'a [u8],
pub(crate) parameters: Option<EcCurve>,
pub(crate) public_key: Option<&'a [u8]>,
}
impl fmt::Debug for EcPrivateKey<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EcPrivateKey")
.field("private_key", &"<redacted>")
.field("parameters", &self.parameters)
.field("public_key_len", &self.public_key.map(<[u8]>::len))
.finish()
}
}
pub(crate) fn decode(der: &[u8]) -> Result<EcPrivateKey<'_>> {
let mut outer = Reader::new(der)?;
let mut fields = outer.sequence()?;
outer.finish()?;
if fields.small_unsigned()? != EC_PRIVKEY_VER1 {
return Err(Error::InvalidKey("ECPrivateKey version is not 1"));
}
let private_key = fields.octet_string()?;
let parameters = match fields.optional(der::CONTEXT_0)? {
Some(element) => Some(read_parameters(element.contents)?),
None => None,
};
let public_key = match fields.optional(der::CONTEXT_1)? {
Some(element) => {
let mut inner = Reader::new(element.contents)?;
let point = inner.bit_string()?;
inner.finish()?;
Some(point)
}
None => None,
};
fields.finish()?;
Ok(EcPrivateKey {
private_key,
parameters,
public_key,
})
}
pub(crate) fn decode_standalone(der: &[u8]) -> Result<EcPrivateKey<'_>> {
decode(der).map_err(|error| pkcs1::relabel_rsa_private_key(der, error))
}
fn read_parameters(contents: &[u8]) -> Result<EcCurve> {
let mut inner = Reader::new(contents)?;
let curve = match inner.peek_tag() {
Some(der::OID) => {
let named = inner.oid()?;
CURVES
.iter()
.find(|crv| oid::named_curve(**crv) == named)
.copied()
.ok_or(Error::UnsupportedAlgorithm)
}
Some(der::SEQUENCE) => {
let specified = inner.element()?;
der::check_well_formed(specified.encoding, false)?;
Err(Error::UnsupportedAlgorithm)
}
_ => {
inner.element()?;
Err(Error::InvalidKey(
"ECPrivateKey parameters are not a namedCurve",
))
}
};
inner.finish()?;
curve
}
pub(crate) fn encode(key: &EcPrivateKey<'_>) -> Result<Zeroizing<Vec<u8>>> {
let version = [EC_PRIVKEY_VER1];
let named = key.parameters.map(oid::named_curve);
let parameters_len = named.map_or(0, |named| der::encoded_len(named.len()));
let public_key_len = key.public_key.map_or(0, |point| {
der::encoded_len(der::bit_string_len(point.len()))
});
let body = der::unsigned_integer_len(&version)
+ der::encoded_len(key.private_key.len())
+ parameters_len
+ public_key_len;
let mut writer = Writer::new(der::encoded_len(body))?;
writer.header(der::SEQUENCE, body)?;
writer.unsigned_integer(&version)?;
writer.element(der::OCTET_STRING, key.private_key)?;
if let Some(named) = named {
writer.element(der::CONTEXT_0, named)?;
}
if let Some(point) = key.public_key {
writer.header(der::CONTEXT_1, der::bit_string_len(point.len()))?;
writer.bit_string(der::BIT_STRING, point)?;
}
writer.finish()
}
#[cfg(test)]
mod tests {
use super::super::der::fixtures;
use super::*;
fn invalid(result: Result<EcPrivateKey<'_>>) -> &'static str {
match result {
Err(Error::InvalidKey(detail)) => detail,
other => panic!("expected InvalidKey, got {other:?}"),
}
}
#[test]
fn rfc7517_a2_sec1_has_curve_key_and_point() {
let der = fixtures::load("rfc", "rfc7517-a2-ec-p256-sec1.der");
let key = decode(&der).unwrap();
assert_eq!(key.parameters, Some(EcCurve::P256));
assert_eq!(key.private_key.len(), 32);
let point = key.public_key.unwrap();
assert_eq!((point.len(), point[0]), (65, 0x04));
}
#[test]
fn openssl_sec1_files_decode_for_every_curve() {
for (name, crv, len) in [
("p256", EcCurve::P256, 32),
("p384", EcCurve::P384, 48),
("p521", EcCurve::P521, 66),
("secp256k1", EcCurve::Secp256k1, 32),
] {
let der = fixtures::load("openssl", &format!("ec-{name}-sec1.der"));
let key = decode(&der).unwrap();
assert_eq!(key.parameters, Some(crv), "{name}");
assert_eq!(key.private_key.len(), len, "{name}");
assert_eq!(key.public_key.map(<[u8]>::len), Some(1 + 2 * len), "{name}");
let der = fixtures::load("openssl", &format!("ec-{name}-sec1-nopub.pem"));
let key = decode(&der).unwrap();
assert_eq!(key.parameters, Some(crv), "{name}");
assert!(key.public_key.is_none(), "{name}");
}
}
#[test]
fn rfc7468_fig12_inner_key_has_no_parameters() {
let der = fixtures::load("rfc", "rfc7468-fig12-secp256k1-pkcs8.pem");
let parts = super::super::pkcs8::decode(&der).unwrap();
let key = decode(parts.private_key).unwrap();
assert_eq!(key.parameters, None);
assert_eq!(key.private_key.len(), 32);
}
#[test]
fn version_0_is_invalid() {
assert_eq!(
invalid(decode(&fixtures::negative("N40"))),
"ECPrivateKey version is not 1"
);
}
fn rsa_private_shape(version: u8, field: &[u8]) -> Vec<u8> {
let mut body = vec![0x02, 0x01, version];
for _ in 0..8 {
body.extend_from_slice(field);
}
let mut der = vec![0x30, u8::try_from(body.len()).unwrap()];
der.extend_from_slice(&body);
der
}
#[test]
fn a_pkcs1_rsa_private_key_read_as_standalone_sec1_is_unsupported() {
for (dir, file) in [
("openssl", "reject-rsa2048-private-pkcs1.der"),
("openssl", "reject-rsa2048-3prime-private-pkcs1.pem"),
("rfc", "reject-rfc9500-testRSA4096-private-pkcs1.pem"),
] {
let der = fixtures::load(dir, file);
let result = super::super::ec::from_sec1(&der);
assert_eq!(result.err(), Some(Error::UnsupportedAlgorithm), "{file}");
}
for (version, field) in [(0, &[0x02, 0x00][..]), (1, &[0x02, 0x02, 0xFF, 0x80][..])] {
let der = rsa_private_shape(version, field);
let result = super::super::ec::from_sec1(&der);
assert_eq!(result.err(), Some(Error::UnsupportedAlgorithm));
}
}
#[test]
fn only_the_standalone_decoder_relabels_an_rsa_private_key() {
let rsa = fixtures::load("openssl", "reject-rsa2048-private-pkcs1.der");
assert_eq!(invalid(decode(&rsa)), "ECPrivateKey version is not 1");
assert_eq!(
decode_standalone(&rsa).err(),
Some(Error::UnsupportedAlgorithm)
);
let ec = fixtures::load("rfc", "rfc7517-a2-ec-p256-pkcs8.der");
let alg = &ec[6..27]; assert_eq!(&alg[..2], &[0x30, 0x13]);
let mut body = vec![0x02, 0x01, 0x00];
body.extend_from_slice(alg);
body.extend_from_slice(&[0x04, 0x82]);
body.extend_from_slice(&u16::try_from(rsa.len()).unwrap().to_be_bytes());
body.extend_from_slice(&rsa);
let mut der = vec![0x30, 0x82];
der.extend_from_slice(&u16::try_from(body.len()).unwrap().to_be_bytes());
der.extend_from_slice(&body);
let parts = super::super::pkcs8::decode(&der).unwrap();
let result = super::super::ec::from_pkcs8(parts);
assert!(
matches!(result, Err(Error::InvalidKey(_))),
"{:?}",
result.err()
);
}
#[test]
fn standalone_sec1_keys_and_their_errors_are_unchanged() {
let mut inputs = vec![
fixtures::load("rfc", "rfc7517-a2-ec-p256-sec1.der"),
fixtures::load("openssl", "ec-p384-sec1.der"),
];
for id in ["N40", "N41", "N43", "N45", "N46", "N47"] {
inputs.push(fixtures::negative(id));
}
for der in &inputs {
assert_eq!(
format!("{:?}", decode_standalone(der)),
format!("{:?}", decode(der))
);
}
}
#[test]
fn implicit_parameters_tag_is_invalid() {
assert!(matches!(
decode(&fixtures::negative("N47")),
Err(Error::InvalidKey(_))
));
}
#[test]
fn missing_parameters_decode_as_none() {
let der = fixtures::negative("N46");
assert_eq!(decode(&der).unwrap().parameters, None);
}
#[test]
fn explicit_curve_parameters_are_unsupported() {
let der = fixtures::load("openssl", "reject-ec-p256-explicit-sec1.pem");
assert_eq!(decode(&der).unwrap_err(), Error::UnsupportedAlgorithm);
}
fn with_parameters(params: &[u8]) -> Vec<u8> {
let der = fixtures::load("rfc", "rfc7517-a2-ec-p256-sec1.der");
let (head, rest) = der.split_at(2 + 3 + 34);
assert_eq!(rest[..2], [0xA0, 0x0A]);
let tail = &rest[12..];
let mut body = head[2..].to_vec();
body.extend_from_slice(&[0xA0, u8::try_from(params.len()).unwrap()]);
body.extend_from_slice(params);
body.extend_from_slice(tail);
let mut out = vec![0x30, u8::try_from(body.len()).unwrap()];
out.extend_from_slice(&body);
out
}
#[test]
fn parameters_choice_is_classified() {
let secp224r1 = [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x21];
assert_eq!(
decode(&with_parameters(&secp224r1)).unwrap_err(),
Error::UnsupportedAlgorithm
);
assert_eq!(
invalid(decode(&with_parameters(&[0x05, 0x00]))),
"ECPrivateKey parameters are not a namedCurve"
);
let two = [oid::OID_P256, oid::OID_P256].concat();
assert!(matches!(
decode(&with_parameters(&two)),
Err(Error::InvalidKey(_))
));
assert!(matches!(
decode(&with_parameters(&[])),
Err(Error::InvalidKey(_))
));
for crv in CURVES {
let key = with_parameters(oid::named_curve(crv));
assert_eq!(decode(&key).unwrap().parameters, Some(crv));
}
}
#[test]
fn public_key_wrapper_must_be_one_whole_bit_string() {
let der = fixtures::load("rfc", "rfc7517-a2-ec-p256-sec1.der");
let at = der.len() - 66;
assert_eq!(der[at - 4..=at], [0xA1, 0x44, 0x03, 0x42, 0x00]);
let mut bad = der.clone();
bad[at] = 0x01;
assert_eq!(invalid(decode(&bad)), "DER BIT STRING has unused bits");
let mut implicit = der[..at - 4].to_vec();
implicit.extend_from_slice(&[0x81, 0x42]);
implicit.extend_from_slice(&der[at..]);
implicit[1] -= 2;
assert!(matches!(decode(&implicit), Err(Error::InvalidKey(_))));
}
#[test]
fn encode_reproduces_every_committed_sec1_file() {
let mut files = vec![
("rfc", "rfc7517-a2-ec-p256-sec1.der".to_owned()),
("rfc", "rfc9500-testECCP256-sec1.pem".to_owned()),
("rfc", "rfc9500-testECCP384-sec1.pem".to_owned()),
("rfc", "rfc9500-testECCP521-sec1.pem".to_owned()),
];
for name in ["p256", "p384", "p521", "secp256k1"] {
files.push(("openssl", format!("ec-{name}-sec1.der")));
files.push(("openssl", format!("ec-{name}-sec1-nopub.pem")));
}
for (dir, file) in files {
let der = fixtures::load(dir, &file);
let key = decode(&der).unwrap();
assert_eq!(*encode(&key).unwrap(), der, "{file}");
}
}
#[test]
fn encode_reproduces_the_pkcs8_inner_keys() {
let mut files = vec![
("rfc", "rfc7468-fig12-secp256k1-pkcs8.pem".to_owned()),
("rfc", "rfc7517-a2-ec-p256-pkcs8.der".to_owned()),
];
for name in ["p256", "p384", "p521", "secp256k1"] {
files.push(("openssl", format!("ec-{name}-pkcs8.der")));
}
for (dir, file) in files {
let der = fixtures::load(dir, &file);
let parts = super::super::pkcs8::decode(&der).unwrap();
let key = decode(parts.private_key).unwrap();
assert_eq!(key.parameters, None, "{file}");
assert!(key.public_key.is_some(), "{file}");
assert_eq!(*encode(&key).unwrap(), parts.private_key, "{file}");
}
}
#[test]
fn encode_matches_the_sec1_oracle() {
use sec1::der::Decode as _;
let der = fixtures::load("openssl", "ec-p521-sec1.der");
let oracle = sec1::EcPrivateKey::from_der(&der).unwrap();
let key = decode(&der).unwrap();
assert_eq!(key.private_key, oracle.private_key);
assert_eq!(key.public_key, oracle.public_key);
assert_eq!(
oracle
.parameters
.and_then(|p| p.named_curve())
.map(|o| o.to_string()),
Some("1.3.132.0.35".to_owned())
);
}
#[test]
fn encode_writes_long_form_lengths() {
let der = fixtures::load("rfc", "rfc9500-testECCP521-sec1.pem");
let encoded = encode(&decode(&der).unwrap()).unwrap();
assert_eq!(encoded[..3], [0x30, 0x81, 0xDC]);
}
#[test]
fn debug_redacts_the_private_key() {
let marker = [0xA5u8; 32];
let key = EcPrivateKey {
private_key: &marker,
parameters: Some(EcCurve::P256),
public_key: Some(&marker),
};
let shown = format!("{key:?} {key:#?}");
assert!(shown.contains("<redacted>"), "{shown}");
assert!(shown.contains("public_key_len: Some(32)"), "{shown}");
assert!(!shown.contains("165") && !shown.to_ascii_lowercase().contains("a5"));
}
}