openpgp-cert-d 0.3.3

Shared OpenPGP Certificate Directory
Documentation
use sha1collisiondetection::{Output, Sha1CD};
use std::{
    borrow::Cow,
    convert::{TryFrom, TryInto},
};

pub(crate) const FINGERPRINT_LEN_CHARS_V4: usize = 40;
pub(crate) const FINGERPRINT_LEN_CHARS_V6: usize = 64;

// Bit 6 of the packet header's first octet denotes the packet format (see 4.2)
const MASK_PACKET_FORMAT: u8 = 0b0100_0000;
// The high bit (bit 7) of the packet header's first octet must be one.
const MASK_HIGH_BIT: u8 = 0b1000_0000;
// In the new packet format, bits 5-0 hold the packet tag.
const MASK_TAG_NEW: u8 = 0b0011_1111;
// In the old packet format, bits 5-2 of the packet header's first octet hold
// the packet tag.
const MASK_TAG_OLD: u8 = 0b0011_1100;
// In the old packet format, after masking, shift to get the tag.
const SHIFT_TAG_OLD: usize = 2;
// In the old packet format, bits 1-0 of the packet header's first octet hold
// the length type.
const MASK_LENGTH_OLD: u8 = 0b0000_0011;
// Old packet format's length types:
const LENGTH_TYPE_OLD_ONE_OCTET: u8 = 0;
const LENGTH_TYPE_OLD_TWO_OCTETS: u8 = 1;
const LENGTH_TYPE_OLD_FOUR_OCTETS: u8 = 2;
const LENGTH_TYPE_OLD_INDETERMINATE: u8 = 3;
// Packet tag that denotes a Secret-Key packet (5.5.1.1)
const PACKET_TAG_SECRET_KEY: u8 = 5;
// Packet tag that denotes a Public-Key packet (5.5.1.1)
const PACKET_TAG_PUBLIC_KEY: u8 = 6;

const ARMOR_HEADER_PUBLIC_KEY: &[u8] = b"-----BEGIN PGP PUBLIC KEY BLOCK-----";

// Data from the packet header, only what is necessary for further processing.
struct HeaderData {
    header_len: usize,
    body_len: u32,
    packet_tag: u8,
}

/// Compute the fingerprint of an OpenPGP TPK.
/// This minimal implementation keeps close to the RFC.
///
/// The TPK's fingerprint is the fingerprint of its public key packet, which
/// must also be the first packet.
/// Subkey fingerprints are not supported.
//
// We do not compute fingerprints of TSKs, it's not possible in the general
// case without algorithm specific handling.
// See https://gitlab.com/openpgp-wg/rfc4880bis/-/issues/43
pub(crate) fn fingerprint(bytes: &[u8]) -> Result<String> {
    let header_data = parse_header(bytes)?;
    if header_data.packet_tag != PACKET_TAG_PUBLIC_KEY {
        return Err(Error::UnsupportedPacketForFingerprint(format!(
            "{}",
            header_data.packet_tag
        )));
    }
    compute_fingerprint(bytes, header_data.header_len, header_data.body_len)
}

// Extract information from the packet header: header length, body length and
// the packet tag.
fn parse_header(bytes: &[u8]) -> Result<HeaderData> {
    // Rough heuristic: The public key material needs to be at least 32 bytes
    // long, plus at least two bytes packet header.
    if bytes.len() < 32 + 2 {
        return Err(Error::NotEnoughData);
    };
    // The high bit of the CTB must be one. If it is not, the data may be ascii,
    // so we can check if it is armored.
    if bytes[0] & MASK_HIGH_BIT == 0 {
        if bytes.starts_with(ARMOR_HEADER_PUBLIC_KEY) {
            return Err(Error::UnsupportedArmor);
        } else {
            return Err(Error::UnsupportedData);
        }
    }
    let is_new_ctb = bytes[0] & MASK_PACKET_FORMAT != 0;
    let header_data = if is_new_ctb {
        let packet_tag = bytes[0] & MASK_TAG_NEW;
        // interpret length encoding according to 4.2.2
        let (header_len, body_len) = match bytes[1] {
            0..=191 => (2, bytes[1] as u32),
            192..=223 => (
                3,
                u16::from_be_bytes([bytes[1] - 192, bytes[2]]) as u32 + 192,
            ),
            255 => (
                6,
                u32::from_be_bytes((&bytes[2..=5]).try_into().unwrap()),
            ),
            224..=254 => {
                // do not handle partial length encoding
                return Err(Error::UnsupportedLengthEncoding);
            }
        };
        HeaderData {
            header_len,
            body_len,
            packet_tag,
        }
    } else {
        let packet_tag = (bytes[0] & MASK_TAG_OLD) >> SHIFT_TAG_OLD;
        // interpret length encoding according to 4.2.1
        let (header_len, body_len) = match bytes[0] & MASK_LENGTH_OLD {
            LENGTH_TYPE_OLD_ONE_OCTET => (2, bytes[1] as u32),
            LENGTH_TYPE_OLD_TWO_OCTETS => (
                3,
                u16::from_be_bytes((&bytes[1..=2]).try_into().unwrap()) as u32,
            ),
            LENGTH_TYPE_OLD_FOUR_OCTETS => {
                (5, u32::from_be_bytes((&bytes[1..=4]).try_into().unwrap()))
            }
            LENGTH_TYPE_OLD_INDETERMINATE => {
                // do not handle indeterminate length encoding
                return Err(Error::UnsupportedLengthEncoding);
            }
            _ => unreachable!(),
        };
        HeaderData {
            header_len,
            body_len,
            packet_tag,
        }
    };
    Ok(header_data)
}

// Computes the fingerprint of a Public-Key packet according to RFC 4880, 12.2.
// Makes no effort to assert that the bytes really are a Public-Key packet.
fn compute_fingerprint(
    bytes: &[u8],
    header_len: usize,
    body_len: u32,
) -> Result<String> {
    let body = &bytes
        .get(header_len..(header_len + body_len as usize))
        .ok_or(Error::NotEnoughData)?;

    // First byte is the packet version.
    match body[0] {
        4 => compute_fingerprint_v4(body),
        v => Err(Error::UnsupportedKeyVersion(v)),
    }
}

fn compute_fingerprint_v4(body: &[u8]) -> Result<String> {
    let mut hasher = Sha1CD::default();
    // RFC 4880, 12.2:
    // A V4 fingerprint is the 160-bit SHA-1 hash of the octet 0x99,
    hasher.update([0x99u8]);
    // followed by the two-octet packet length
    let length = <u16>::try_from(body.len())
        .map_err(|_| Error::PublicKeyPacketTooLong)?;
    hasher.update(length.to_be_bytes());
    // followed by the entire Public-Key packet starting with the version field.
    hasher.update(body);
    let mut result = Output::default();
    let _ = hasher.finalize_into_dirty_cd(&mut result);

    Ok(format_fingerprint(&result))
}

fn format_fingerprint(bytes: &[u8]) -> String {
    use std::fmt::Write;
    bytes
        .iter()
        .fold(
            String::with_capacity(40),
            |mut s, b| {
                write!(&mut s, "{:02x}", b).unwrap();
                s
            })
}

// Canonicalizes a fingerprint.
//
// Note: the input may not contain spaces.
pub(crate) fn canonicalize_fingerprint(fpr: &str) -> Result<Cow<str>> {
    if (fpr.len() == FINGERPRINT_LEN_CHARS_V4
        || fpr.len() == FINGERPRINT_LEN_CHARS_V6)
        && fpr.chars().all(|c| {
            c.is_ascii_hexdigit()
        })
    {
        if fpr.chars().all(|c| c.is_ascii_lowercase()) {
            Ok(Cow::Borrowed(fpr))
        } else {
            let mut fpr = fpr.to_string();
            fpr.make_ascii_lowercase();
            Ok(Cow::Owned(fpr))
        }
    } else {
        Err(Error::InvalidFingerprint(
            format!("{} is not a valid fingerprint", fpr)))
    }
}

// Check if the given data may plausibly be a TSK or TPK, i.e. starts with a
// Public-Key or Secret-Key packet.
pub(crate) fn plausible_tsk_or_tpk(bytes: &[u8]) -> Result<()> {
    let header_data = parse_header(bytes)?;
    if header_data.header_len + header_data.body_len as usize > bytes.len() {
        return Err(Error::NotEnoughData);
    }
    if header_data.packet_tag == PACKET_TAG_PUBLIC_KEY
        || header_data.packet_tag == PACKET_TAG_SECRET_KEY
    {
        Ok(())
    } else {
        Err(Error::UnsupportedPacket)
    }
}

/// Result specialization for this module.
pub type Result<T> = std::result::Result<T, Error>;

#[derive(thiserror::Error, Debug)]
pub enum Error {
    /// Expected to read more data.
    #[error("Not enough data")]
    NotEnoughData,
    /// Public key packet too long for fingerprint calculation.
    #[error("Public key packet too long")]
    PublicKeyPacketTooLong,
    /// Unhandled packet type for fingerprint.
    #[error("Unsupported packet type for fingerprint computation, found {0}")]
    UnsupportedPacketForFingerprint(String),
    /// Unhandled packet type for other uses.
    #[error("Unsupported packet type")]
    UnsupportedPacket,
    /// Unsupported length encoding.
    #[error("Unsupported length encoding")]
    UnsupportedLengthEncoding,
    /// Unsupported key version.
    #[error("Unsupported key version: {0}")]
    UnsupportedKeyVersion(u8),
    /// Not PGP data.
    #[error("Not a PGP packet")]
    UnsupportedData,
    /// Armored data.
    #[error("Armored data unsupported")]
    UnsupportedArmor,
    /// Invalid fingerprint.
    #[error("{0} is not a valid fingerprint")]
    InvalidFingerprint(String),
    /// Wrong certificate.
    #[error("Expected a certificate for {0}, found a certificate for {1}")]
    WrongCertificate(String, String),
}

#[cfg(test)]
mod tests {
    use super::*;

    struct Testdata<'a> {
        data: &'a [u8],
        fingerprint: &'a str,
    }

    static ALICE: Testdata = Testdata {
        fingerprint: "eb85bb5fa33a75e15e944e63f231550c4f47e38e",
        data: include_bytes!("../../testdata/alice.pgp"),
    };

    static SENDER_PUBLIC: Testdata = Testdata {
        fingerprint: "c9cecc00208658e6183da1c6ab27f5772e0e7843",
        data: include_bytes!("../../testdata/sender_public.pgp"),
    };

    #[test]
    fn compute_fingerprint() {
        // old ctb
        assert_eq!(fingerprint(ALICE.data).unwrap(), ALICE.fingerprint);
        // new ctb
        assert_eq!(
            fingerprint(SENDER_PUBLIC.data).unwrap(),
            SENDER_PUBLIC.fingerprint
        );
    }

    #[test]
    fn error_unsupported_armor() {
        let data = include_bytes!("../../testdata/alice.asc");
        assert!(matches!(
            fingerprint(data).unwrap_err(),
            Error::UnsupportedArmor
        ));
    }

    #[test]
    fn error_not_enough_data() {
        let data = &[17u8; 17];
        assert!(matches!(
            fingerprint(data).unwrap_err(),
            Error::NotEnoughData
        ));
    }

    #[test]
    fn error_unsupported_packet_for_fingerprint() {
        // Secret keys (packet tag 5) are unsupported
        let data = include_bytes!("../../testdata/sender.pgp");
        assert!(matches!(
            fingerprint(data).unwrap_err(),
            Error::UnsupportedPacketForFingerprint(p) if p == "5".to_string()
        ));
    }

    #[test]
    fn error_unsupported_data() {
        let data = b"Herr von Ribbeck auf Ribbeck im Havelland,
            Ein Birnbaum in seinem Garten stand,";
        assert!(matches!(
            fingerprint(data).unwrap_err(),
            Error::UnsupportedData
        ));
    }
}