summaryrefslogtreecommitdiffstats
path: root/ipc
diff options
context:
space:
mode:
authorNora Widdecke <nora@sequoia-pgp.org>2021-04-23 16:01:18 +0200
committerNora Widdecke <nora@sequoia-pgp.org>2021-06-28 13:32:31 +0200
commit32c43e56eff29efbb8a3cdb9eb0413ff4f5126b4 (patch)
tree1747cae60039744d063553314707f335ab7c86ec /ipc
parent146176be2c1b5376a8dba0760aadcc1320b9e076 (diff)
ipc: Initial support for keybox files.
- Support for the GnuPG keybox format, based on keybox files created by GnuPG 2.2.23 and the way they are handled by the `kbxutil` program from that version of GnuPG. - Most structures that are not related to accessing the OpenPGP certs are not handled, like X.509 records and metadata sections. - Closes #252
Diffstat (limited to 'ipc')
-rw-r--r--ipc/src/keybox.rs515
-rw-r--r--ipc/src/lib.rs1
2 files changed, 516 insertions, 0 deletions
diff --git a/ipc/src/keybox.rs b/ipc/src/keybox.rs
new file mode 100644
index 00000000..f6308bba
--- /dev/null
+++ b/ipc/src/keybox.rs
@@ -0,0 +1,515 @@
+//! Support for the GnuPG keybox format.
+//!
+//! This implementation is based on keybox files created by GnuPG 2.2.23 and
+//! the way they are handled by the `kbxutil` program from that version of GnuPG.
+
+use buffered_reader::BufferedReader;
+
+use openpgp::cert::Cert;
+use openpgp::crypto::hash::Digest;
+use openpgp::parse::Parse;
+use openpgp::types::HashAlgorithm::SHA1;
+use openpgp::Result;
+use sequoia_openpgp as openpgp;
+
+use std::convert::TryInto;
+use std::fmt::Display;
+use std::io::Read;
+
+/// GnuPG Keybox
+///
+/// This implementation is based on keybox files created by GnuPG 2.2.23 and
+/// the way they are handled by the `kbxutil` program from that version of GnuPG.
+///
+/// For example, to extract all certs from a keybox while ignoring all other
+/// records:
+///
+/// ```rust
+/// # fn parse_keybox(reader: &mut dyn buffered_reader::BufferedReader<()>)
+/// # -> sequoia_openpgp::Result<Vec<sequoia_openpgp::Cert>> {
+/// use sequoia_ipc::keybox::{Keybox, KeyboxRecord};
+/// use sequoia_openpgp::{Cert, Result};
+/// use sequoia_openpgp::parse::Parse;
+///
+/// let kbx = Keybox::from_reader(reader)?;
+/// let certs = kbx
+/// // Keep only records which were parsed successfully.
+/// .filter_map(|kbx_record| kbx_record.ok())
+/// // Map the OpenPGP records to the contained certs.
+/// .filter_map(|kbx_record| {
+/// match kbx_record {
+/// KeyboxRecord::OpenPGP(r) => Some(r.cert()),
+/// _ => None,
+/// }
+/// }).collect::<Result<Vec<Cert>>>();
+/// certs
+/// # }
+/// ```
+pub struct Keybox<'a> {
+ reader: Box<dyn BufferedReader<()> + 'a>,
+}
+
+impl<'a> Keybox<'a> {
+ fn read_next_record(&mut self) -> Result<KeyboxRecord> {
+ // The first 4 bytes contain the record's length,
+ // bytes 5 and 6 the type and version.
+ let input = self
+ .reader
+ .data_hard(6)
+ .map_err(|e| Error::NotEnoughData(e.to_string()))?;
+ // input holds at least 4 bytes, so this cannot fail.
+ let len = u32::from_be_bytes(input[..4].try_into().unwrap()) as usize;
+
+ let content = self.reader.data_consume_hard(len)?;
+ let kbx_record = KeyboxRecord::new((&content[..len]).to_vec())?;
+ Ok(kbx_record)
+ }
+}
+
+impl<'a> Parse<'a, Keybox<'a>> for Keybox<'a> {
+ fn from_reader<R: 'a + Read + Send + Sync>(reader: R) -> Result<Self> {
+ let bio = buffered_reader::Generic::new(reader, None);
+ Ok(Keybox {
+ reader: Box::new(bio),
+ })
+ }
+}
+
+impl<'a> Iterator for Keybox<'a> {
+ type Item = Result<KeyboxRecord>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.reader.eof() {
+ None
+ } else {
+ Some(self.read_next_record())
+ }
+ }
+}
+
+/// Types of keybox records.
+///
+/// Note: This enum cannot be exhaustively matched to allow future extensions.
+#[non_exhaustive]
+#[derive(PartialEq, Eq, Debug)]
+pub enum KeyboxRecordType {
+ /// Header record type.
+ Header,
+ /// OpenPGP record type.
+ OpenPGP,
+ /// X.509 record type.
+ X509,
+ /// Catchall.
+ Unknown(u8),
+}
+
+impl From<u8> for KeyboxRecordType {
+ fn from(value: u8) -> Self {
+ match value {
+ 1 => KeyboxRecordType::Header,
+ 2 => KeyboxRecordType::OpenPGP,
+ 3 => KeyboxRecordType::X509,
+ v => KeyboxRecordType::Unknown(v),
+ }
+ }
+}
+
+impl Display for KeyboxRecordType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ KeyboxRecordType::Header => write!(f, "Header"),
+ KeyboxRecordType::OpenPGP => write!(f, "OpenPGP"),
+ KeyboxRecordType::X509 => write!(f, "X509"),
+ KeyboxRecordType::Unknown(v) => write!(f, "Unknown: {}", v),
+ }
+ }
+}
+
+/// Provides access to the fields shared by all keybox record types.
+impl KeyboxRecord {
+ fn bytes(&self) -> &[u8] {
+ match self {
+ KeyboxRecord::Header(h) => &h.bytes,
+ KeyboxRecord::OpenPGP(o) => &o.bytes,
+ KeyboxRecord::X509(x) => &x.bytes,
+ KeyboxRecord::Unknown(bytes) => bytes,
+ }
+ }
+
+ /// The first 4 bytes contain the record's length.
+ pub fn length_field(&self) -> u32 {
+ u32::from_be_bytes((&self.bytes()[..4]).try_into().unwrap())
+ }
+
+ /// The 5th byte contains the record's type.
+ pub fn typ(&self) -> KeyboxRecordType {
+ KeyboxRecordType::from(self.bytes()[4])
+ }
+
+ /// The 6th byte contains the record type's version.
+ pub fn version(&self) -> u8 {
+ self.bytes()[5]
+ }
+
+ fn new(bytes: Vec<u8>) -> Result<Self> {
+ if bytes.len() < 6 {
+ return Err(Error::NotEnoughData(
+ "A keybox record requires at least 6 bytes.".to_string(),
+ )
+ .into());
+ }
+
+ let record = KeyboxRecord::Unknown(bytes.clone());
+ match record.typ() {
+ KeyboxRecordType::Header => {
+ HeaderRecord::new(bytes).map(|r| KeyboxRecord::Header(r))
+ }
+ KeyboxRecordType::OpenPGP => {
+ OpenPGPRecordV1::new(&record).map(|r| KeyboxRecord::OpenPGP(r))
+ }
+ KeyboxRecordType::X509 => {
+ X509Record::new(bytes).map(|r| KeyboxRecord::X509(r))
+ }
+ KeyboxRecordType::Unknown(_) => Ok(record),
+ }
+ }
+}
+
+/// Keybox record
+///
+/// Holds the record's data and provides access to the fields shared by all
+/// record types.
+///
+/// Note: This enum cannot be exhaustively matched to allow future extensions.
+#[non_exhaustive]
+#[derive(PartialEq, Eq, Debug)]
+pub enum KeyboxRecord {
+ /// Header record.
+ Header(HeaderRecord),
+ /// OpenPGP record.
+ OpenPGP(OpenPGPRecordV1),
+ /// X.509 record.
+ X509(X509Record),
+ /// Catchall.
+ Unknown(Vec<u8>),
+}
+
+/// Keybox header record.
+///
+/// Contains general metadata of the keybox.
+#[derive(PartialEq, Eq, Debug)]
+pub struct HeaderRecord {
+ bytes: Vec<u8>,
+}
+
+impl HeaderRecord {
+ fn new(bytes: Vec<u8>) -> Result<Self> {
+ //TODO at least check length?
+ Ok(Self { bytes })
+ }
+
+ /// Flags field.
+ // Semantics unknown.
+ pub fn flags(&self) -> [u8; 2] {
+ self.bytes[0x6..=0x7].try_into().unwrap()
+ }
+
+ /// Checks that the magic number is correctly "KBXf".
+ pub fn check_magic(&self) -> bool {
+ let magic = &self.bytes[0x08..=0x0B];
+ magic == b"KBXf"
+ }
+
+ /// The unix timestamp when this keybox file was created.
+ pub fn created_at(&self) -> u32 {
+ u32::from_be_bytes((self.bytes[0x10..=0x13]).try_into().unwrap())
+ }
+
+ /// The unix timestamp when this keybox file was last maintained.
+ // Unsure what "last maintained" means. Not last modified, adding a key
+ // through gpg --import does not change it
+ pub fn last_maintained(&self) -> u32 {
+ u32::from_be_bytes((self.bytes[0x14..=0x17]).try_into().unwrap())
+ }
+}
+
+/// Kybox X.509 record
+///
+/// Unhandled, only exists for completeness.
+#[derive(PartialEq, Eq, Debug)]
+pub struct X509Record {
+ bytes: Vec<u8>,
+}
+
+impl X509Record {
+ fn new(bytes: Vec<u8>) -> Result<Self> {
+ //TODO at least check length?
+ Ok(Self { bytes })
+ }
+}
+
+/// Keybox OpenPGP record
+#[derive(PartialEq, Eq, Debug)]
+pub struct OpenPGPRecordV1 {
+ bytes: Vec<u8>,
+}
+
+impl OpenPGPRecordV1 {
+ fn new(record: &KeyboxRecord) -> Result<Self> {
+ // Check type and version
+ if record.typ() != KeyboxRecordType::OpenPGP || record.version() != 1 {
+ return Err(
+ Error::UnhandledRecord(record.typ(), record.version()).into()
+ );
+ }
+
+ // Check record header length
+ if record.bytes().len() < 0x10 {
+ return Err(Error::NotEnoughData(format!(
+ "OpenPGP record header is 16 bytes, got {}",
+ record.bytes().len()
+ ))
+ .into());
+ };
+
+ let record = OpenPGPRecordV1 {
+ bytes: record.bytes().to_vec(),
+ };
+
+ // Check checksum
+ if &record.checksum_field()[..] != record.compute_checksum()? {
+ return Err(Error::InvalidData("wrong checksum".to_string()).into());
+ }
+
+ Ok(record)
+ }
+
+ /// Flags field.
+ // Semantics unknown.
+ pub fn flags(&self) -> [u8; 2] {
+ self.bytes[0x6..=0x7].try_into().unwrap()
+ }
+
+ /// Data offset field.
+ pub fn data_offset(&self) -> usize {
+ u32::from_be_bytes((self.bytes[0x8..=0xB]).try_into().unwrap()) as usize
+ }
+
+ /// Data length field.
+ pub fn data_length(&self) -> usize {
+ u32::from_be_bytes((self.bytes[0xC..=0xF]).try_into().unwrap()) as usize
+ }
+
+ /// The record's contained raw data.
+ pub fn data_section(&self) -> Result<&[u8]> {
+ let data_end = self.data_offset() + self.data_length();
+ // Check if data length is correct
+ if self.bytes.len() < data_end {
+ return Err(Error::NotEnoughData(
+ "data section truncated".to_string(),
+ )
+ .into());
+ };
+ Ok(&self.bytes[self.data_offset()..data_end])
+ }
+
+ /// Metadata section, unhandled.
+ // Not handled, contains:
+ // Redundant data (fingerprints, keyids, userids) of the
+ // following cert,
+ // management fields (ownertrust, all-validity?)
+ // timestamps (recheck?, latest change?, creation date)
+ pub fn metadata_section(&self) -> &[u8] {
+ &self.bytes[0x10..self.data_offset()]
+ }
+
+ /// Checksum field.
+ ///
+ /// Contains a there's a SHA1 hash over the whole record.
+ pub fn checksum_field(&self) -> [u8; 20] {
+ let hash_offset = self.data_offset() + self.data_length();
+ self.bytes[hash_offset..hash_offset + 20]
+ .try_into()
+ .unwrap()
+ }
+
+ /// Compute the checksum
+ ///
+ /// Computes a SHA1 hash over the whole record.
+ pub fn compute_checksum(&self) -> Result<Vec<u8>> {
+ let hash_offset = self.data_offset() + self.data_length();
+ let (hashed_data, _hash) = &self.bytes.split_at(hash_offset);
+ let mut ctx = SHA1.context()?;
+ ctx.update(hashed_data);
+ ctx.into_digest()
+ }
+
+ /// Extract the cert from a keybox openpgp version 1 record.
+ /// Ignores metadata and flags stored in the record, but
+ /// checks the checksum.
+ pub fn cert(&self) -> Result<Cert> {
+ // At the end of the data section, there are 8 bytes following
+ // the cert that I don't understand.
+ // In my samples, there are two versions:
+ // "0xb006_0000_6770_6700" and
+ // "0xb006_0003_6770_6700".
+ // Note that b"gpg" == 0x677067. Maybe some kind of salt?
+ // Anyway, ignore those bytes.
+ let (cert_data, _trailer) = &self
+ .data_section()?
+ .split_at(self.data_section()?.len() - 8);
+ Cert::from_bytes(cert_data)
+ }
+}
+
+#[derive(thiserror::Error, Debug)]
+/// Errors used in this module.
+pub enum Error {
+ /// Not enough data
+ #[error("Not enough data: {0}")]
+ NotEnoughData(String),
+ /// Unhandled record
+ #[error("Unhandled record type: {0}, version {1}")]
+ UnhandledRecord(KeyboxRecordType, u8),
+ /// Invalid data
+ #[error("Invalid data: {0}")]
+ InvalidData(String),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[cfg(test)]
+ // Extract all certs from a keybox. Ignore other records.
+ fn parse_keybox(reader: &mut dyn BufferedReader<()>) -> Result<Vec<Cert>> {
+ let kbx = Keybox::from_reader(reader)?;
+ let certs = kbx
+ // Keep only records which were parsed successfully.
+ .filter_map(|kbx_record| kbx_record.ok())
+ // Map the OpenPGP records to the contained certs.
+ .filter_map(|kbx_record| match kbx_record {
+ KeyboxRecord::OpenPGP(r) => Some(r.cert()),
+ _ => None,
+ })
+ .collect::<Result<Vec<Cert>>>();
+ certs
+ }
+
+ #[test]
+ fn keybox_record() -> Result<()> {
+ let header_bytes = crate::tests::keybox("header_sample");
+ let header_kbx = KeyboxRecord::new(header_bytes.to_vec())?;
+ assert_eq!(header_kbx.typ(), header_bytes[4].into());
+
+ let openpgp_bytes = crate::tests::keybox("testy_openpgp");
+ let openpgp_kbx = KeyboxRecord::new(openpgp_bytes.to_vec())?;
+ assert_eq!(openpgp_kbx.typ(), openpgp_bytes[4].into());
+
+ let x509_bytes = crate::tests::keybox("testy_x509");
+ let x509_kbx = KeyboxRecord::new(x509_bytes.to_vec())?;
+ assert_eq!(x509_kbx.typ(), x509_bytes[4].into());
+
+ let too_short = &[1u8; 5];
+ assert!(KeyboxRecord::new(too_short.to_vec()).is_err());
+ Ok(())
+ }
+
+ #[test]
+ fn cert_from_openpgp_record() -> Result<()> {
+ let openpgp_bytes = crate::tests::keybox("testy_openpgp");
+ let kbx_record = KeyboxRecord::new(openpgp_bytes.to_vec())?;
+ let openpgp_record = match kbx_record {
+ KeyboxRecord::OpenPGP(r) => r,
+ _ => unreachable!(),
+ };
+ let cert = openpgp_record.cert().unwrap();
+ let testy = Cert::from_bytes(crate::tests::key("testy.pgp")).unwrap();
+ assert_eq!(cert, testy);
+ Ok(())
+ }
+
+ #[test]
+ fn cert_from_keybox() -> Result<()> {
+ let bytes = crate::tests::keybox("keybox.kbx");
+ let mut br = buffered_reader::Memory::new(bytes);
+ let certs = parse_keybox(&mut br)?;
+ let testy = Cert::from_bytes(crate::tests::key("testy.pgp"))?;
+ assert_eq!(certs[0], testy);
+ Ok(())
+ }
+
+ #[test]
+ fn openpgp_record() -> Result<()> {
+ let openpgp_bytes = crate::tests::keybox("testy_openpgp");
+ let kbx_record = KeyboxRecord::new(openpgp_bytes.to_vec())?;
+ assert_eq!(kbx_record.length_field(), 1428u32);
+ assert_eq!(kbx_record.typ(), KeyboxRecordType::OpenPGP);
+ assert_eq!(kbx_record.version(), 1u8);
+ let openpgp_record = match kbx_record {
+ KeyboxRecord::OpenPGP(r) => r,
+ _ => unreachable!(),
+ };
+ assert_eq!(openpgp_record.flags(), [0u8, 0u8]);
+ assert_eq!(openpgp_record.data_offset(), 126usize);
+ assert_eq!(openpgp_record.data_length(), 1282usize);
+ assert_eq!(
+ openpgp_record.metadata_section(),
+ [
+ 0, 2, 0, 28, 62, 136, 119, 200, 119, 39, 70, 146, 151, 81, 137,
+ 245, 208, 63, 111, 134, 82, 38, 254, 139, 0, 0, 0, 32, 0, 0, 0,
+ 0, 1, 241, 135, 87, 91, 212, 86, 68, 4, 101, 100, 193, 73, 226,
+ 17, 129, 102, 201, 38, 50, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 1,
+ 0, 12, 0, 0, 1, 158, 0, 0, 0, 36, 0, 0, 0, 0, 0, 2, 0, 4, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 129,
+ 142, 142, 0, 0, 0, 0
+ ]
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn openpgp_errors() -> Result<()> {
+ let openpgp_too_short = [0u8, 7u8, 1u8, 1u8, 2u8, 1u8, 1u8];
+ assert!(KeyboxRecord::new(openpgp_too_short.to_vec()).is_err());
+
+ let openpgp_unknown_version = [0u8, 7u8, 1u8, 1u8, 2u8, 7u8, 1u8];
+ assert!(KeyboxRecord::new(openpgp_unknown_version.to_vec()).is_err());
+
+ let mut openpgp_wrong_checksum = crate::tests::keybox("testy_openpgp").to_vec();
+ // set last byte (= last byte of checksum) to 0
+ if let Some(last) = openpgp_wrong_checksum.last_mut() {
+ *last = 0u8;
+ };
+ assert!(KeyboxRecord::new(openpgp_wrong_checksum.to_vec()).is_err());
+ Ok(())
+ }
+
+ #[test]
+ fn header_record() -> Result<()> {
+ let header_bytes = crate::tests::keybox("header_sample");
+ let kbx_record = KeyboxRecord::new(header_bytes.to_vec())?;
+ assert_eq!(kbx_record.length_field(), 32u32);
+ assert_eq!(kbx_record.typ(), KeyboxRecordType::Header);
+ assert_eq!(kbx_record.version(), 1u8);
+ let header_record = match kbx_record {
+ KeyboxRecord::Header(r) => r,
+ _ => unreachable!(),
+ };
+ assert!(header_record.check_magic());
+ assert_eq!(header_record.flags(), [0x00u8, 0x02u8]);
+ assert_eq!(header_record.created_at(), 0x6081_8e8eu32);
+ assert_eq!(header_record.last_maintained(), 0x6081_8e8eu32);
+ Ok(())
+ }
+
+ #[test]
+ fn x509_record() -> Result<()> {
+ let x509_bytes = crate::tests::keybox("testy_x509");
+ let kbx_record = KeyboxRecord::new(x509_bytes.to_vec())?;
+ assert_eq!(kbx_record.length_field(), 1704u32);
+ assert_eq!(kbx_record.typ(), KeyboxRecordType::X509);
+ assert_eq!(kbx_record.version(), 1u8);
+ Ok(())
+ }
+}
diff --git a/ipc/src/lib.rs b/ipc/src/lib.rs
index b0bb3dee..10b1b563 100644
--- a/ipc/src/lib.rs
+++ b/ipc/src/lib.rs
@@ -68,6 +68,7 @@ use sequoia_openpgp as openpgp;
#[macro_use] mod trace;
pub mod assuan;
pub mod gnupg;
+pub mod keybox;
mod keygrip;
pub use self::keygrip::Keygrip;
pub mod sexp;