Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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());
    }
}