use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Tlv {
pub tag: u8,
pub start: usize,
pub end: usize,
pub next: usize,
}
impl Tlv {
pub fn contents<'a>(&self, data: &'a [u8]) -> &'a [u8] {
&data[self.start..self.end]
}
pub fn element<'a>(&self, data: &'a [u8], header_start: usize) -> &'a [u8] {
&data[header_start..self.end]
}
pub fn len(&self) -> usize {
self.end - self.start
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub mod tag {
pub const INTEGER: u8 = 0x02;
pub const BIT_STRING: u8 = 0x03;
pub const OCTET_STRING: u8 = 0x04;
pub const OID: u8 = 0x06;
pub const SEQUENCE: u8 = 0x30;
pub const SET: u8 = 0x31;
pub const CONTEXT_0: u8 = 0xa0;
pub const CONTEXT_3: u8 = 0xa3;
}
pub fn read(data: &[u8], at: usize) -> Result<Tlv> {
let tag = *data
.get(at)
.ok_or_else(|| malformed("element runs past the end"))?;
let first = *data
.get(at + 1)
.ok_or_else(|| malformed("element has no length"))?;
let (length, header) = if first < 0x80 {
(first as usize, 2)
} else {
let count = (first & 0x7f) as usize;
if count == 0 || count > 4 {
return Err(malformed("unsupported DER length encoding"));
}
let bytes = data
.get(at + 2..at + 2 + count)
.ok_or_else(|| malformed("truncated DER length"))?;
(
bytes
.iter()
.fold(0usize, |value, byte| (value << 8) | *byte as usize),
2 + count,
)
};
let start = at + header;
let end = start
.checked_add(length)
.ok_or_else(|| malformed("DER length overflows"))?;
if end > data.len() {
return Err(malformed("DER element extends past the input"));
}
Ok(Tlv {
tag,
start,
end,
next: end,
})
}
fn read_tagged(data: &[u8], at: usize, expected: u8) -> Result<Tlv> {
let tlv = read(data, at)?;
if tlv.tag != expected {
return Err(malformed(format!(
"expected tag 0x{expected:02x} at offset {at}, found 0x{:02x}",
tlv.tag
)));
}
Ok(tlv)
}
fn malformed(detail: impl Into<String>) -> Error {
Error::format(format!("malformed certificate: {}", detail.into()))
}
#[derive(Debug, Clone)]
pub struct Certificate<'a> {
pub serial_number: &'a [u8],
pub issuer: &'a [u8],
pub subject: &'a [u8],
pub subject_public_key: &'a [u8],
pub subject_key_identifier: Option<&'a [u8]>,
pub common_name: Option<String>,
}
const OID_SUBJECT_KEY_IDENTIFIER: [u8; 3] = [0x55, 0x1d, 0x0e];
const OID_COMMON_NAME: [u8; 3] = [0x55, 0x04, 0x03];
impl<'a> Certificate<'a> {
pub fn parse(data: &'a [u8]) -> Result<Self> {
let certificate = read_tagged(data, 0, tag::SEQUENCE)?;
let tbs = read_tagged(data, certificate.start, tag::SEQUENCE)?;
let mut at = tbs.start;
let first = read(data, at)?;
if first.tag == tag::CONTEXT_0 {
at = first.next;
}
let serial = read_tagged(data, at, tag::INTEGER)?;
let signature = read_tagged(data, serial.next, tag::SEQUENCE)?;
let issuer_at = signature.next;
let issuer = read_tagged(data, issuer_at, tag::SEQUENCE)?;
let validity = read_tagged(data, issuer.next, tag::SEQUENCE)?;
let subject_at = validity.next;
let subject = read_tagged(data, subject_at, tag::SEQUENCE)?;
let spki = read_tagged(data, subject.next, tag::SEQUENCE)?;
let algorithm = read_tagged(data, spki.start, tag::SEQUENCE)?;
let public_key = read_tagged(data, algorithm.next, tag::BIT_STRING)?;
let subject_public_key = data
.get(public_key.start + 1..public_key.end)
.ok_or_else(|| malformed("empty subject public key"))?;
Ok(Self {
serial_number: serial.contents(data),
issuer: issuer.element(data, issuer_at),
subject: subject.element(data, subject_at),
subject_public_key,
subject_key_identifier: subject_key_identifier(data, &tbs, public_key.next)?,
common_name: common_name(data, subject.contents(data)),
})
}
pub fn public_key_hash(&self) -> [u8; 20] {
use sha1::Digest as _;
sha1::Sha1::digest(self.subject_public_key).into()
}
}
fn subject_key_identifier<'a>(
data: &'a [u8],
tbs: &Tlv,
mut at: usize,
) -> Result<Option<&'a [u8]>> {
while at < tbs.end {
let field = read(data, at)?;
if field.tag != tag::CONTEXT_3 {
at = field.next;
continue;
}
let extensions = read_tagged(data, field.start, tag::SEQUENCE)?;
let mut extension_at = extensions.start;
while extension_at < extensions.end {
let extension = read_tagged(data, extension_at, tag::SEQUENCE)?;
let oid = read_tagged(data, extension.start, tag::OID)?;
let mut value_at = oid.next;
let value = loop {
let next = read(data, value_at)?;
if next.tag == tag::OCTET_STRING {
break next;
}
value_at = next.next;
if value_at >= extension.end {
return Err(malformed("extension has no value"));
}
};
if oid.contents(data) == OID_SUBJECT_KEY_IDENTIFIER {
let inner = read_tagged(data, value.start, tag::OCTET_STRING)?;
return Ok(Some(inner.contents(data)));
}
extension_at = extension.next;
}
return Ok(None);
}
Ok(None)
}
fn common_name(data: &[u8], name_contents: &[u8]) -> Option<String> {
let base = name_contents.as_ptr() as usize - data.as_ptr() as usize;
let mut at = base;
let end = base + name_contents.len();
while at < end {
let rdn = read(data, at).ok()?;
if rdn.tag == tag::SET {
let mut pair_at = rdn.start;
while pair_at < rdn.end {
let pair = read(data, pair_at).ok()?;
let oid = read(data, pair.start).ok()?;
if oid.tag == tag::OID && oid.contents(data) == OID_COMMON_NAME {
let value = read(data, oid.next).ok()?;
return Some(String::from_utf8_lossy(value.contents(data)).into_owned());
}
pair_at = pair.next;
}
}
at = rdn.next;
}
None
}
#[derive(Debug, Clone)]
pub struct PrivateKeyInfo<'a> {
pub algorithm_oid: &'a [u8],
pub private_key: &'a [u8],
}
pub const OID_RSA_ENCRYPTION: [u8; 9] = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
pub const OID_EC_PUBLIC_KEY: [u8; 7] = [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01];
impl<'a> PrivateKeyInfo<'a> {
pub fn parse(data: &'a [u8]) -> Result<Self> {
let info = read_tagged(data, 0, tag::SEQUENCE)?;
let version = read_tagged(data, info.start, tag::INTEGER)?;
let algorithm = read_tagged(data, version.next, tag::SEQUENCE)?;
let oid = read_tagged(data, algorithm.start, tag::OID)?;
let key = read_tagged(data, algorithm.next, tag::OCTET_STRING)?;
Ok(Self {
algorithm_oid: oid.contents(data),
private_key: key.contents(data),
})
}
pub fn is_rsa(&self) -> bool {
self.algorithm_oid == OID_RSA_ENCRYPTION
}
pub fn is_ec(&self) -> bool {
self.algorithm_oid == OID_EC_PUBLIC_KEY
}
pub fn rsa_key_size_in_bits(&self) -> Result<u32> {
if !self.is_rsa() {
return Err(malformed("key size is only computed for RSA keys"));
}
let sequence = read_tagged(self.private_key, 0, tag::SEQUENCE)?;
let version = read_tagged(self.private_key, sequence.start, tag::INTEGER)?;
let modulus = read_tagged(self.private_key, version.next, tag::INTEGER)?;
let bytes = modulus.contents(self.private_key);
let significant = bytes
.iter()
.position(|byte| *byte != 0)
.unwrap_or(bytes.len());
Ok(((bytes.len() - significant) * 8) as u32)
}
}
pub fn pem_or_der(data: &[u8]) -> Result<Vec<u8>> {
let text = match std::str::from_utf8(data) {
Ok(text) if text.contains("-----BEGIN") => text,
_ => return Ok(data.to_vec()),
};
let body: String = text
.lines()
.skip_while(|line| !line.starts_with("-----BEGIN"))
.skip(1)
.take_while(|line| !line.starts_with("-----END"))
.collect();
use base64::Engine as _;
base64::engine::general_purpose::STANDARD
.decode(body.trim())
.map_err(|error| malformed(format!("invalid base64 in PEM: {error}")))
}
#[cfg(test)]
mod tests {
use super::*;
const CERTIFICATE: &str = concat!(
"MIICzDCCAbQCCQDjBOPvAqI4rTANBgkqhkiG9w0BAQsFADAoMRkwFwYDVQQDDBBrYyBpZGVudGl0",
"eSB0ZXN0MQswCQYDVQQKDAJrYzAeFw0yNjA3MjUxMjAzMTJaFw0zNjA3MjIxMjAzMTJaMCgxGTAX",
"BgNVBAMMEGtjIGlkZW50aXR5IHRlc3QxCzAJBgNVBAoMAmtjMIIBIjANBgkqhkiG9w0BAQEFAAOC",
"AQ8AMIIBCgKCAQEAovSsY3lsY/7mA8gwXu4KAiEgI2Gv4+nEifdtGQFOMOMPl7EG9mqZo3A7SD6B",
"HyZFwGdfkZvP4uUXCM2z54EbV64FdpyFGll96yIgW6nTy7WOHHL3s8myi1uWVWd0hfxYnJ9FRimV",
"o4Y6mQK6anZs/WeiUKR+nQYSydYiCYJEzRY7xZVrSDrd2gcxzQ14okhx7VoWKN3pJhpT5Ot6HnvZ",
"PUTPpacEEXDNnHSVlOF5wK1rAejp8X7FOgqBfbNKY8WJgPbtOqY5luv72PbBWJ4ueFLc3LlbQOVe",
"GFqyIV4JqlADewaHNV4E1ZRHG369fZuHC/3/mWmHbphRBwz4ZyNZ1QIDAQABMA0GCSqGSIb3DQEB",
"CwUAA4IBAQBXNRJTBjM45WJb/8yJyBF7VplpoV0UAUdLoEGV4u7yOZ0E4CVixq7WIy0Z443EtFjc",
"0j5reSenUntTkDL82YlhRJQW0swfDuapNquBz/HD3GZviPzVboI7hgpimZdKV085iS3OmYP++HM4",
"iZWFXA4FqQmZMa8BqArugAroPM2HH3/1ZmskhibcAaXx5zBK2kHAAMo9eFHujEaGyQaHMQKeyUNo",
"aQmcoSi099cPIyLD1DJ2G2ue1WSZVqKNEvZ6ueXKLOT9pO8rKlKD3QRCdaV2hpxn4o+6h8c7/o86",
"JkwsmuiKjVzhZLc3d0/r9OsTObFwOhBDzb0dyX581TQEOd0l",
);
fn certificate() -> Vec<u8> {
use base64::Engine as _;
base64::engine::general_purpose::STANDARD
.decode(CERTIFICATE)
.unwrap()
}
#[test]
fn certificate_fields_match_the_record_macos_wrote() {
let der = certificate();
let parsed = Certificate::parse(&der).unwrap();
assert_eq!(hex::encode(parsed.serial_number), "00e304e3ef02a238ad");
assert_eq!(parsed.subject.len(), 42);
assert_eq!(parsed.issuer, parsed.subject, "self-signed");
assert_eq!(
hex::encode(&parsed.subject[..24]),
"30283119301706035504030c106b63206964656e74697479"
);
assert_eq!(
hex::encode(parsed.public_key_hash()),
"665e69a00dd3a6d68498c89ef3263702a475066d"
);
assert_eq!(
parsed.subject_key_identifier, None,
"this certificate has no SKI"
);
assert_eq!(parsed.common_name.as_deref(), Some("kc identity test"));
}
#[test]
fn the_public_key_hash_covers_the_bit_string_not_the_whole_spki() {
use sha1::Digest as _;
let der = certificate();
let parsed = Certificate::parse(&der).unwrap();
assert_eq!(parsed.subject_public_key.len(), 270);
assert_eq!(
parsed.subject_public_key[0], 0x30,
"an RSAPublicKey SEQUENCE"
);
let spki_hash: [u8; 20] = sha1::Sha1::digest(&der[..]).into();
assert_ne!(parsed.public_key_hash(), spki_hash);
}
#[test]
fn lengths_are_read_in_both_short_and_long_form() {
let short = [0x02u8, 0x01, 0x07];
let tlv = read(&short, 0).unwrap();
assert_eq!(tlv.tag, tag::INTEGER);
assert_eq!(tlv.contents(&short), &[0x07]);
let mut long = vec![0x04u8, 0x82, 0x01, 0x00];
long.extend(std::iter::repeat_n(0xabu8, 256));
let tlv = read(&long, 0).unwrap();
assert_eq!(tlv.len(), 256);
assert_eq!(tlv.next, 260);
}
#[test]
fn malformed_input_is_an_error_not_a_panic() {
assert!(read(&[], 0).is_err());
assert!(read(&[0x30], 0).is_err());
assert!(read(&[0x30, 0x05, 0x00], 0).is_err(), "length past the end");
assert!(
read(&[0x30, 0x89, 0, 0, 0, 0, 0, 0, 0, 0, 0], 0).is_err(),
"9-byte length"
);
assert!(Certificate::parse(b"not a certificate").is_err());
assert!(PrivateKeyInfo::parse(&[0x30, 0x00]).is_err());
}
#[test]
fn a_pkcs8_key_that_is_not_a_key_is_rejected() {
assert!(PrivateKeyInfo::parse(&[]).is_err());
assert!(PrivateKeyInfo::parse(&[0x30, 0x03, 0x02, 0x01, 0x00]).is_err());
let certificate = certificate();
assert!(PrivateKeyInfo::parse(&certificate).is_err());
}
#[test]
fn pem_and_der_are_both_accepted() {
let der = certificate();
assert_eq!(pem_or_der(&der).unwrap(), der, "DER passes through");
use base64::Engine as _;
let encoded = base64::engine::general_purpose::STANDARD.encode(&der);
let pem = format!(
"-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
encoded
.as_bytes()
.chunks(64)
.map(|line| String::from_utf8_lossy(line).into_owned())
.collect::<Vec<_>>()
.join("\n")
);
assert_eq!(pem_or_der(pem.as_bytes()).unwrap(), der);
assert!(
pem_or_der(b"-----BEGIN CERTIFICATE-----\n!!!\n-----END CERTIFICATE-----").is_err()
);
}
}