provenance/src/der.rs
//! The few DER moves the verifier needs that a parser does not give: the exact
//! TBS bytes a certificate signature covers, and the precertificate TBS a CT log
//! signed (the certificate's TBS with the SCT extension taken out).
use crate::error::{ProvenanceError, Result};
/// One DER element.
#[derive(Debug, Clone, Copy)]
pub struct Tlv<'a> {
pub tag: u8,
pub content: &'a [u8],
/// The whole element, header included.
pub raw: &'a [u8],
}
fn bad(why: &str) -> ProvenanceError {
ProvenanceError::Malformed(format!("DER: {why}"))
}
/// Read one element from the front of `input`; returns it and the rest.
pub fn read(input: &[u8]) -> Result<(Tlv<'_>, &[u8])> {
if input.len() < 2 {
return Err(bad("truncated header"));
}
let tag = input[0];
if tag & 0x1f == 0x1f {
return Err(bad("multi-byte tags are not used here"));
}
let (len, hdr) = match input[1] {
l if l < 0x80 => (l as usize, 2),
0x80 => return Err(bad("indefinite length")),
l => {
let n = (l & 0x7f) as usize;
if n > 4 || input.len() < 2 + n {
return Err(bad("length too long"));
}
let mut v = 0usize;
for b in &input[2..2 + n] {
v = (v << 8) | *b as usize;
}
(v, 2 + n)
}
};
let end = hdr.checked_add(len).ok_or_else(|| bad("length overflow"))?;
if input.len() < end {
return Err(bad("truncated content"));
}
Ok((
Tlv {
tag,
content: &input[hdr..end],
raw: &input[..end],
},
&input[end..],
))
}
/// Every element inside a constructed element's content.
pub fn children(mut content: &[u8]) -> Result<Vec<Tlv<'_>>> {
let mut out = Vec::new();
while !content.is_empty() {
let (t, rest) = read(content)?;
out.push(t);
content = rest;
}
Ok(out)
}
/// Encode one element.
pub fn encode(tag: u8, content: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
let n = content.len();
if n < 0x80 {
out.push(n as u8);
} else {
let bytes: Vec<u8> = n
.to_be_bytes()
.iter()
.copied()
.skip_while(|b| *b == 0)
.collect();
out.push(0x80 | bytes.len() as u8);
out.extend(bytes);
}
out.extend_from_slice(content);
out
}
/// A certificate's parts: the TBS element, the signature algorithm OID (content
/// bytes) and the signature value (without the unused-bits byte).
pub struct CertParts<'a> {
pub tbs: Tlv<'a>,
pub sig_alg_oid: &'a [u8],
pub signature: &'a [u8],
}
/// Split a DER certificate.
pub fn cert_parts(der: &[u8]) -> Result<CertParts<'_>> {
let (cert, _) = read(der)?;
let kids = children(cert.content)?;
if kids.len() != 3 || kids[2].tag != 0x03 || kids[2].content.is_empty() {
return Err(bad("not a certificate"));
}
let alg = children(kids[1].content)?;
let oid = alg
.first()
.filter(|t| t.tag == 0x06)
.ok_or_else(|| bad("no signature algorithm"))?;
Ok(CertParts {
tbs: kids[0],
sig_alg_oid: oid.content,
signature: &kids[2].content[1..],
})
}
/// The DER of a dotted OID's content bytes (`1.3.6.1.4.1.11129.2.4.2`).
pub fn oid_bytes(dotted: &str) -> Vec<u8> {
let parts: Vec<u64> = dotted.split('.').filter_map(|p| p.parse().ok()).collect();
let mut out = vec![(parts[0] * 40 + parts[1]) as u8];
for &p in &parts[2..] {
let mut stack = vec![(p & 0x7f) as u8];
let mut v = p >> 7;
while v > 0 {
stack.push(0x80 | (v & 0x7f) as u8);
v >>= 7;
}
stack.reverse();
out.extend(stack);
}
out
}
/// The TBS bytes a CT log signed for this certificate: its TBS with the
/// extension `oid` (the SCT list) removed, re-encoded.
pub fn tbs_without_extension(cert_der: &[u8], oid: &str) -> Result<Vec<u8>> {
let parts = cert_parts(cert_der)?;
let want = oid_bytes(oid);
let mut tbs_content = Vec::new();
for kid in children(parts.tbs.content)? {
if kid.tag != 0xa3 {
tbs_content.extend_from_slice(kid.raw);
continue;
}
let (seq, _) = read(kid.content)?;
let mut exts = Vec::new();
for ext in children(seq.content)? {
let fields = children(ext.content)?;
let is_it = fields
.first()
.is_some_and(|f| f.tag == 0x06 && f.content == want.as_slice());
if !is_it {
exts.extend_from_slice(ext.raw);
}
}
tbs_content.extend(encode(0xa3, &encode(0x30, &exts)));
}
Ok(encode(0x30, &tbs_content))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn oids_encode_like_the_spec() {
assert_eq!(
oid_bytes("1.2.840.10045.4.3.2"),
vec![0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02]
);
assert_eq!(
oid_bytes("1.3.6.1.4.1.57264.1.8"),
vec![0x2b, 6, 1, 4, 1, 0x83, 0xbf, 0x30, 1, 8]
);
}
#[test]
fn long_lengths_round_trip() {
let content = vec![7u8; 300];
let enc = encode(0x04, &content);
let (t, rest) = read(&enc).unwrap();
assert!(rest.is_empty());
assert_eq!(t.content, content.as_slice());
}
}