use std::{
convert::TryInto,
env, fs,
io::{self, Read, Write},
path::{Path, PathBuf},
};
use fd_lock::RwLock;
use tempfile::NamedTempFile;
use crate::SPECIAL_NAMES;
use crate::{pgp, InternalError};
use crate::{Data, Error, Result, Tag};
const PATH_PREFIX_LEN: usize = 2;
#[derive(Debug)]
pub struct CertD {
base: PathBuf,
}
impl CertD {
pub fn new() -> Result<CertD> {
CertD::with_base_dir(
env::var_os("PGP_CERT_D")
.map(Into::into)
.unwrap_or_else(CertD::default_location),
)
}
fn default_location() -> PathBuf {
dirs::data_dir()
.expect("Unsupported platform")
.join("pgp.cert.d")
}
pub fn with_base_dir<P: AsRef<Path>>(base: P) -> Result<CertD> {
Ok(CertD {
base: base.as_ref().into(),
})
}
pub fn get_base_dir(&self) -> &Path {
&self.base
}
fn get_path_by_fp(&self, fingerprint: &str) -> Result<PathBuf> {
if fingerprint.len() != 40 {
return Err(Error::BadName);
}
if fingerprint.chars().any(|c| !c.is_ascii_hexdigit()) {
return Err(Error::BadName);
}
let fingerprint = fingerprint.to_ascii_lowercase();
Ok(self.base.join(&fingerprint[..2]).join(&fingerprint[2..]))
}
fn get_fp_by_path(
&self,
path: &Path,
) -> std::result::Result<String, InternalError> {
let path = if path.is_absolute() {
path.strip_prefix(&self.base)
.map_err(|_| InternalError::PathNotInStore)?
} else {
path
};
if !self.base.join(path).is_file() {
return Err(InternalError::BadFingerprintPath);
}
if path.components().count() != 2 {
return Err(InternalError::BadFingerprintPath);
}
let components =
path.components().map(|c| c.as_os_str()).collect::<Vec<_>>();
if components.iter().any(|c| !c.is_ascii()) {
return Err(InternalError::BadFingerprintPath);
}
let head = components[0].to_string_lossy();
if head.len() != PATH_PREFIX_LEN {
return Err(InternalError::BadFingerprintPath);
}
let tail = components[1].to_string_lossy();
if tail.len() != pgp::FP_LEN_CHARS_V4 - PATH_PREFIX_LEN
&& tail.len() != pgp::FP_LEN_CHARS_V5 - PATH_PREFIX_LEN
{
return Err(InternalError::BadFingerprintPath);
}
Ok(head.to_string() + &tail)
}
fn get_path_by_special(&self, special: &str) -> Result<PathBuf> {
let special = special.to_lowercase();
if SPECIAL_NAMES.binary_search(&special.as_ref()).is_ok() {
Ok(self.base.join(special))
} else {
Err(Error::BadName)
}
}
pub fn get(&self, name: &str) -> Result<Option<(Tag, Data)>> {
let path = self.get_path(name)?;
match fs::File::open(path) {
Ok(mut f) => {
let tag = f.metadata()?.try_into()?;
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
Ok(Some((tag, buf.into())))
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn get_if_changed(
&self,
since: Tag,
name: &str,
) -> Result<Option<(Tag, Data)>> {
let path = self.get_path(name)?;
match fs::File::open(path) {
Ok(mut f) => {
let tag = f.metadata()?.try_into()?;
if since == tag {
Ok(None) } else {
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
Ok(Some((tag, buf.into())))
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn get_path(&self, name: &str) -> Result<PathBuf> {
self.get_path_by_fp(name)
.or_else(|_| self.get_path_by_special(name))
}
pub fn insert<M>(&self, data: Data, merge: M) -> Result<(Tag, Data)>
where
M: FnOnce(Data, Option<Data>) -> Result<Data>,
{
let blocking = true;
let name = pgp::fingerprint(data.as_ref())?;
self.insert_impl(&name, data, merge, blocking)
}
pub fn try_insert<M>(&self, data: Data, merge: M) -> Result<(Tag, Data)>
where
M: FnOnce(Data, Option<Data>) -> Result<Data>,
{
let blocking = false;
let name = pgp::fingerprint(data.as_ref())?;
self.insert_impl(&name, data, merge, blocking)
}
pub fn insert_special<M>(
&self,
special_name: &str,
data: Data,
merge: M,
) -> Result<(Tag, Data)>
where
M: FnOnce(Data, Option<Data>) -> Result<Data>,
{
let blocking = true;
pgp::plausible_tsk_or_tpk(&data)?;
self.insert_impl(special_name, data, merge, blocking)
}
pub fn try_insert_special<M>(
&self,
special_name: &str,
data: Data,
merge: M,
) -> Result<(Tag, Data)>
where
M: FnOnce(Data, Option<Data>) -> Result<Data>,
{
let blocking = false;
pgp::plausible_tsk_or_tpk(&data)?;
self.insert_impl(special_name, data, merge, blocking)
}
fn insert_impl<M>(
&self,
name: &str,
data: Data,
merge: M,
blocking: bool,
) -> Result<(Tag, Data)>
where
M: FnOnce(Data, Option<Data>) -> Result<Data>,
{
let target_path = self.get_path(name)?;
fs::create_dir_all(target_path.parent().expect("at least one leg"))?;
let mut lf = RwLock::new(self.idempotent_create_lockfile()?);
let lock = if blocking {
lf.write()?
} else {
lf.try_write()?
};
let old_cert = self.get(name)?.map(|(_, cert)| cert);
let new_cert = merge(data, old_cert)?;
{
let mut tmp = NamedTempFile::new_in(&self.base)?;
tmp.write_all(new_cert.as_ref())?;
tmp.persist(&target_path).map_err(|e| e.error)?;
}
let tag = fs::File::open(&target_path)?.metadata()?.try_into()?;
drop(lock);
Ok((tag, new_cert))
}
pub fn iter_fingerprints(&self) -> Result<impl Iterator<Item = String> + '_> {
Ok(fs::read_dir(&self.base)?
.filter_map(|toplevel| toplevel.ok())
.filter(|toplevel| {
toplevel.file_type().map(|t| t.is_dir()).unwrap_or(false)
&& toplevel.file_name().len() == 2
})
.flat_map(|toplevel| fs::read_dir(toplevel.path()))
.flatten()
.filter_map(|entry| entry.ok())
.map(move |entry| self.get_fp_by_path(&entry.path()).unwrap()))
}
pub fn iter(
&self,
) -> Result<impl Iterator<Item = (String, Tag, Data)> + '_> {
Ok(self.iter_fingerprints()?.filter_map(move |fp| {
self.get(&fp)
.ok()
.flatten()
.map(|(tag, data)| (fp, tag, data))
}))
}
fn idempotent_create_lockfile(&self) -> Result<std::fs::File> {
let lock_path = self.base.join("writelock");
std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(lock_path)
.map_err(Into::into)
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_fs::prelude::*;
use predicates::prelude::*;
use crate::TRUST_ROOT;
fn test_base() -> assert_fs::TempDir {
let base = assert_fs::TempDir::new().unwrap();
match std::env::var_os("CERTD_TEST_PERSIST") {
Some(_) => {
eprintln!("Test base dir: {}", &base.path().to_string_lossy());
base.into_persistent()
}
None => base,
}
}
struct Testdata<'a> {
data: &'a [u8],
fingerprint: &'a str,
}
impl Testdata<'_> {
fn path(&self) -> String {
[&self.fingerprint[..2], &self.fingerprint[2..]].join("/")
}
fn add_to_certd(&self, base: &assert_fs::TempDir) {
base.child(self.path()).write_binary(self.data).unwrap();
}
}
static ALICE: Testdata = Testdata {
fingerprint: "eb85bb5fa33a75e15e944e63f231550c4f47e38e",
data: include_bytes!("../../testdata/alice.asc"),
};
static BOB: Testdata = Testdata {
fingerprint: "d1a66e1a23b182c9980f788cfbfcc82a015e7330",
data: include_bytes!("../../testdata/bob.asc"),
};
static TESTY: Testdata = Testdata {
fingerprint: "39d100ab67d5bd8c04010205fb3751f1587daef1",
data: include_bytes!("../../testdata/testy-new.pgp"),
};
fn setup_testdir(
testdata: &[&Testdata],
) -> Result<(assert_fs::TempDir, CertD)> {
let base = test_base();
for t in testdata.iter() {
t.add_to_certd(&base);
}
let trust_root_data = include_bytes!("../../testdata/sender.pgp");
base.child("trust-root")
.write_binary(trust_root_data)
.unwrap();
let certd = CertD::with_base_dir(&base)?;
Ok((base, certd))
}
#[test]
fn get_fp() -> std::result::Result<(), Box<dyn std::error::Error>> {
let data = include_bytes!("../../testdata/testy-new.pgp");
let base = test_base();
base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1")
.write_binary(data)
.unwrap();
let certd = CertD::with_base_dir(&base)?;
let (tag, cert) = certd
.get("39d100ab67d5bd8c04010205fb3751f1587daef1")?
.unwrap();
assert_eq!(cert.as_ref(), data);
assert!(certd
.get_if_changed(tag, "39d100ab67d5bd8c04010205fb3751f1587daef1")?
.is_none());
base.close().unwrap();
Ok(())
}
#[test]
fn get_special() -> std::result::Result<(), Box<dyn std::error::Error>> {
let data = include_bytes!("../../testdata/sender.pgp");
let base = test_base();
base.child("trust-root").write_binary(data).unwrap();
let certd = CertD::with_base_dir(&base)?;
let (tag, cert) = certd.get(TRUST_ROOT)?.unwrap();
assert_eq!(cert.as_ref(), data);
assert!(certd.get_if_changed(tag, TRUST_ROOT)?.is_none());
base.close().unwrap();
Ok(())
}
#[test]
fn get_not_found() -> Result<()> {
let base = test_base();
let certd = CertD::with_base_dir(&base)?;
let result = certd.get("39d100ab67d5bd8c04010205fb3751f1587daef1");
assert!(matches!(result, Ok(None)));
Ok(())
}
#[test]
fn insert_locked() -> Result<()> {
let data = include_bytes!("../../testdata/testy-new.pgp");
let base = test_base();
let file = base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1");
file.assert(predicate::path::missing());
let certd = CertD::with_base_dir(&base)?;
let f = |new: Data, old: Option<Data>| {
assert!(old.is_none());
Ok(new)
};
let mut lf = RwLock::new(certd.idempotent_create_lockfile()?);
let _lock = lf.write()?;
let result = certd.try_insert(data.to_vec().into_boxed_slice(), &f);
match result.unwrap_err() {
Error::IoError(e) if e.kind() == io::ErrorKind::WouldBlock => {
Ok(())
}
e => Err(e),
}
}
#[test]
fn insert_special_locked() -> Result<()> {
let data = include_bytes!("../../testdata/sender.pgp");
let base = test_base();
let file = base.child("trust-root");
file.assert(predicate::path::missing());
let certd = CertD::with_base_dir(&base)?;
let f = |new: Data, old: Option<Data>| {
assert!(old.is_none());
Ok(new)
};
let mut lock = RwLock::new(certd.idempotent_create_lockfile()?);
let _lock = lock.write()?;
let result = certd.try_insert_special(
TRUST_ROOT,
data.to_vec().into_boxed_slice(),
&f,
);
match result.unwrap_err() {
Error::IoError(e) if e.kind() == io::ErrorKind::WouldBlock => {
Ok(())
}
e => Err(e),
}
}
#[test]
fn insert_new() -> Result<()> {
let data = include_bytes!("../../testdata/testy-new.pgp");
let base = test_base();
let file = base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1");
file.assert(predicate::path::missing());
let certd = CertD::with_base_dir(&base)?;
let f = |new: Data, old: Option<Data>| {
assert!(old.is_none());
Ok(new)
};
let (_, inserted) =
certd.insert(data.to_vec().into_boxed_slice(), &f)?;
file.assert(data.as_ref());
assert_eq!(inserted, data.to_vec().into_boxed_slice());
Ok(())
}
#[test]
fn insert_special_new() -> Result<()> {
let data = include_bytes!("../../testdata/sender.pgp");
let base = test_base();
let file = base.child("trust-root");
file.assert(predicate::path::missing());
let certd = CertD::with_base_dir(&base)?;
let f = |new: Data, old: Option<Data>| {
assert!(old.is_none());
Ok(new)
};
let (_, inserted) = certd.insert_special(
"trust-root",
data.to_vec().into_boxed_slice(),
&f,
)?;
file.assert(data.as_ref());
assert_eq!(inserted, data.to_vec().into_boxed_slice());
Ok(())
}
#[test]
fn insert_update() -> std::result::Result<(), Box<dyn std::error::Error>> {
let data = include_bytes!("../../testdata/testy-new.pgp");
let base = test_base();
let file = base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1");
file.touch().unwrap();
file.assert(predicate::str::is_empty());
let certd = CertD::with_base_dir(&base)?;
let f = |new: Data, old: Option<Data>| {
assert!(old.is_some());
Ok(new)
};
let (_, inserted) =
certd.insert(data.to_vec().into_boxed_slice(), &f)?;
file.assert(data.as_ref());
assert_eq!(inserted, data.to_vec().into_boxed_slice());
Ok(())
}
#[test]
fn insert_special_update(
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let data = include_bytes!("../../testdata/sender.pgp");
let base = test_base();
let file = base.child("trust-root");
file.touch().unwrap();
file.assert(predicate::str::is_empty());
let certd = CertD::with_base_dir(&base)?;
let f = |new: Data, old: Option<Data>| {
assert!(old.is_some());
Ok(new)
};
let (_, inserted) = certd.insert_special(
TRUST_ROOT,
data.to_vec().into_boxed_slice(),
&f,
)?;
file.assert(data.as_ref());
assert_eq!(inserted, data.to_vec().into_boxed_slice());
Ok(())
}
#[test]
fn insert_get() -> std::result::Result<(), Box<dyn std::error::Error>> {
let data = include_bytes!("../../testdata/testy-new.pgp");
let base = test_base();
let certd = CertD::with_base_dir(&base)?;
let f = |new: Data, old: Option<Data>| {
assert!(old.is_none());
Ok(new)
};
certd.insert(data.to_vec().into_boxed_slice(), &f)?;
let (_, cert) = certd
.get("39d100ab67d5bd8c04010205fb3751f1587daef1")?
.unwrap();
assert_eq!(cert.as_ref(), data);
Ok(())
}
#[test]
fn get_path_by_fp() -> Result<()> {
let base = test_base();
let certd = CertD::with_base_dir(&base)?;
let expected = base
.path()
.join("39")
.join("d100ab67d5bd8c04010205fb3751f1587daef1");
let fingerprint = "39d100ab67d5bd8c04010205fb3751f1587daef1";
assert_eq!(certd.get_path_by_fp(fingerprint)?, expected);
let fingerprint = "39D100AB67D5BD8C04010205FB3751F1587DAEF1";
assert_eq!(certd.get_path_by_fp(fingerprint)?, expected);
let fingerprint = "39D100ab67D5bD8C04010205FB3751f1587DAeF1";
assert_eq!(certd.get_path_by_fp(fingerprint)?, expected);
Ok(())
}
#[test]
fn get_path_by_fp_negative() -> Result<()> {
let base = test_base();
let certd = CertD::with_base_dir(&base)?;
let fingerprint = "";
let result = certd.get_path_by_fp(fingerprint);
assert!(matches!(result.unwrap_err(), Error::BadName));
let fingerprint = "39d100ab67d5bd8c04010205fb3751f1587daef";
let result = certd.get_path_by_fp(fingerprint);
assert!(matches!(result.unwrap_err(), Error::BadName));
let fingerprint = "peter";
let result = certd.get_path_by_fp(fingerprint);
assert!(matches!(result.unwrap_err(), Error::BadName));
Ok(())
}
#[test]
fn get_path_by_special() -> Result<()> {
let base = test_base();
let certd = CertD::with_base_dir(&base)?;
let expected = base.path().join(TRUST_ROOT);
let name = "trust-root";
assert_eq!(certd.get_path_by_special(name)?, expected);
let name = "TRUST-ROOT";
assert_eq!(certd.get_path_by_special(name)?, expected);
let name = "TrUsT-RooT";
assert_eq!(certd.get_path_by_special(name)?, expected);
Ok(())
}
#[test]
fn get_path_by_special_negative() -> Result<()> {
let base = test_base();
let certd = CertD::with_base_dir(&base)?;
let name = "";
let result = certd.get_path_by_special(name);
assert!(matches!(result.unwrap_err(), Error::BadName));
let name = "mySpecialName";
let result = certd.get_path_by_special(name);
assert!(matches!(result.unwrap_err(), Error::BadName));
Ok(())
}
#[test]
fn iter_fingerprints() -> Result<()> {
use std::collections::HashSet;
let (_base, certd) = setup_testdir(&[&ALICE, &BOB, &TESTY])?;
let iter_fp = certd.iter_fingerprints()?;
let fps = iter_fp.collect::<HashSet<_>>();
let expected: HashSet<_> =
[ALICE.fingerprint, BOB.fingerprint, TESTY.fingerprint]
.iter()
.map(|&s| s.to_owned())
.collect();
assert_eq!(expected, fps);
Ok(())
}
#[test]
fn iter() -> Result<()> {
use std::collections::HashSet;
let (_base, certd) = setup_testdir(&[&ALICE, &BOB, &TESTY])?;
let mut expected: HashSet<_> = [&ALICE, &BOB, &TESTY]
.iter()
.map(|&s| {
(
s.fingerprint.to_owned(),
certd.get(s.fingerprint).unwrap().unwrap().0,
s.data.to_vec().into_boxed_slice(),
)
})
.collect();
for item in certd.iter()? {
assert!(expected.contains(&item));
expected.remove(&item);
}
assert!(expected.is_empty());
Ok(())
}
#[test]
fn base_path() -> Result<()> {
let base = assert_fs::TempDir::new().unwrap();
let certd = CertD::with_base_dir(&base)?;
assert_eq!(certd.get_base_dir(), base.path());
Ok(())
}
}