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;
const MASK_PACKET_FORMAT: u8 = 0b0100_0000;
const MASK_HIGH_BIT: u8 = 0b1000_0000;
const MASK_TAG_NEW: u8 = 0b0011_1111;
const MASK_TAG_OLD: u8 = 0b0011_1100;
const SHIFT_TAG_OLD: usize = 2;
const MASK_LENGTH_OLD: u8 = 0b0000_0011;
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;
const PACKET_TAG_SECRET_KEY: u8 = 5;
const PACKET_TAG_PUBLIC_KEY: u8 = 6;
const ARMOR_HEADER_PUBLIC_KEY: &[u8] = b"-----BEGIN PGP PUBLIC KEY BLOCK-----";
struct HeaderData {
header_len: usize,
body_len: u32,
packet_tag: u8,
}
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)
}
fn parse_header(bytes: &[u8]) -> Result<HeaderData> {
if bytes.len() < 32 + 2 {
return Err(Error::NotEnoughData);
};
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;
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 => {
return Err(Error::UnsupportedLengthEncoding);
}
};
HeaderData {
header_len,
body_len,
packet_tag,
}
} else {
let packet_tag = (bytes[0] & MASK_TAG_OLD) >> SHIFT_TAG_OLD;
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 => {
return Err(Error::UnsupportedLengthEncoding);
}
_ => unreachable!(),
};
HeaderData {
header_len,
body_len,
packet_tag,
}
};
Ok(header_data)
}
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)?;
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();
hasher.update([0x99u8]);
let length = <u16>::try_from(body.len())
.map_err(|_| Error::PublicKeyPacketTooLong)?;
hasher.update(length.to_be_bytes());
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
})
}
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)))
}
}
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)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Not enough data")]
NotEnoughData,
#[error("Public key packet too long")]
PublicKeyPacketTooLong,
#[error("Unsupported packet type for fingerprint computation, found {0}")]
UnsupportedPacketForFingerprint(String),
#[error("Unsupported packet type")]
UnsupportedPacket,
#[error("Unsupported length encoding")]
UnsupportedLengthEncoding,
#[error("Unsupported key version: {0}")]
UnsupportedKeyVersion(u8),
#[error("Not a PGP packet")]
UnsupportedData,
#[error("Armored data unsupported")]
UnsupportedArmor,
#[error("{0} is not a valid fingerprint")]
InvalidFingerprint(String),
#[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() {
assert_eq!(fingerprint(ALICE.data).unwrap(), ALICE.fingerprint);
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() {
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
));
}
}