use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fmt;
use std::time::SystemTime;
use std::time::Duration;
use sequoia_openpgp as openpgp;
use openpgp::cert::prelude::*;
use openpgp::KeyHandle;
use openpgp::packet::UserID;
use openpgp::regex::RegexSet;
use openpgp::packet::Signature;
use openpgp::policy::HashAlgoSecurity;
use crate::CertSynopsis;
use crate::format_time;
use crate::Result;
use crate::RevocationStatus;
use crate::TRACE;
#[non_exhaustive]
#[derive(thiserror::Error, Debug)]
pub enum CertificationError {
#[error("{0}: invalid, missing creation time")]
MissingCreationTime(Certification),
#[error("{0}: policy violation")]
InvalidCertification(Certification, #[source] anyhow::Error),
#[error("{0}: issuer revoked the certification")]
IssuerRevoked(Certification),
#[error("{0}: certification created after reference time ({time})",
time=format_time(.1))]
BornLater(Certification, SystemTime),
#[error("{0}: certification expired ({time1}) as of reference time ({time2})",
time1=format_time(.1), time2=format_time(.2))]
CertificationExpired(Certification, SystemTime, SystemTime),
#[error("{0}: target is not live \
as of the certification time ({time})",
time=format_time(.1))]
TargetNotLive(Certification, SystemTime, #[source] anyhow::Error),
#[error("{0}: target certificate is not valid \
as of the certification time ({time})",
time=format_time(.1))]
TargetNotValid(Certification, SystemTime, #[source] anyhow::Error),
#[error("{0}: issuer certificate is hard revoked: {1} ({msg})",
msg=String::from_utf8_lossy(.2))]
IssuerHardRevoked(Certification,
openpgp::types::ReasonForRevocation, Vec<u8>),
#[error("{0}: issuer certificate is soft revoked \
as of the certification time ({time}): {2} ({msg})",
time=format_time(.1),
msg=String::from_utf8_lossy(.3))]
IssuerSoftRevoked(Certification, SystemTime,
openpgp::types::ReasonForRevocation, Vec<u8>),
#[error("{0}: target certificate is hard revoked: {1} ({msg})",
msg=String::from_utf8_lossy(.2))]
TargetHardRevoked(Certification,
openpgp::types::ReasonForRevocation, Vec<u8>),
#[error("{0}: target certificate is soft revoked \
as of the certification time ({time}): {2} ({msg})",
time=format_time(.1),
msg=String::from_utf8_lossy(.3))]
TargetSoftRevoked(Certification, SystemTime,
openpgp::types::ReasonForRevocation, Vec<u8>),
}
#[derive(Debug, Clone, Copy, Eq)]
pub enum Depth {
Unconstrained,
Limit(usize),
}
impl Depth {
pub fn new<I>(depth: I) -> Self
where I: Into<Option<usize>>
{
if let Some(d) = depth.into() {
Depth::Limit(d)
} else {
Depth::Unconstrained
}
}
pub fn unconstrained() -> Self {
Depth::Unconstrained
}
pub fn is_unconstrained(&self) -> bool {
matches!(self, Depth::Unconstrained)
}
pub fn can_introduce(&self) -> bool {
match self {
Depth::Unconstrained => true,
Depth::Limit(d) if *d > 0 => true,
_ => false
}
}
pub fn limit(&self) -> Option<usize> {
match self {
Depth::Unconstrained => None,
Depth::Limit(d) => Some(*d),
}
}
pub fn decrease(&self, value: usize) -> Depth {
match self {
Depth::Unconstrained => {
Depth::Unconstrained
}
Depth::Limit(d) => {
assert!(*d >= value);
Depth::Limit(d - value)
}
}
}
}
impl fmt::Display for Depth {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Depth::Unconstrained => write!(f, "unconstrained"),
Depth::Limit(d) => write!(f, "{}", d),
}
}
}
impl From<usize> for Depth {
fn from(d: usize) -> Self {
Depth::new(d)
}
}
impl From<Option<usize>> for Depth {
fn from(d: Option<usize>) -> Self {
Depth::new(d)
}
}
impl PartialOrd for Depth {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Depth {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(Depth::Unconstrained, Depth::Unconstrained) => Ordering::Equal,
(Depth::Limit(_), Depth::Unconstrained) => Ordering::Less,
(Depth::Unconstrained, Depth::Limit(_)) => Ordering::Greater,
(Depth::Limit(x), Depth::Limit(y)) => x.cmp(&y),
}
}
}
impl PartialEq for Depth {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
#[derive(Clone)]
pub struct Certification {
issuer: CertSynopsis,
target: CertSynopsis,
userid: Option<UserID>,
creation_time: SystemTime,
expiration_time: Option<SystemTime>,
exportable: bool,
amount: usize,
depth: Depth,
re_set: Option<RegexSet>,
re_bytes: Vec<Vec<u8>>,
digest_prefix: Option<[u8; 2]>,
}
impl<'a> From<(&'a ValidCert<'a>, &'a ValidCert<'a>, &'a Signature)>
for Certification
{
fn from(x: (&ValidCert, &ValidCert, &Signature)) -> Self {
Certification::from_signature(
x.0,
x.1.primary_userid().ok().map(|ua| ua.userid().clone()),
x.1,
x.2)
}
}
impl PartialEq for Certification {
fn eq(&self, other: &Self) -> bool {
self.issuer.fingerprint() == other.issuer.fingerprint()
&& self.target.fingerprint() == other.target.fingerprint()
&& self.userid == other.userid
&& self.creation_time == other.creation_time
&& self.expiration_time == other.expiration_time
&& self.exportable == other.exportable
&& self.amount == other.amount
&& self.depth == other.depth
&& self.re_bytes == other.re_bytes
}
}
impl fmt::Display for Certification {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}by {} on {} at {}",
if let Some(digest_prefix) = self.digest_prefix {
format!("{:02X}{:02X} ",
digest_prefix[0], digest_prefix[1])
} else {
"".to_string()
},
self.issuer.keyid(),
self.target.keyid(),
format_time(&self.creation_time))
}
}
impl fmt::Debug for Certification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Certification")
.field("issuer", &self.issuer.fingerprint())
.field("target", &self.target)
.field("userid",
&self.userid.as_ref().map(|uid| {
String::from_utf8_lossy(uid.value()).into_owned()
})
.unwrap_or_else(|| "<None>".into()))
.field("creation time",
&self.creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_else(|_| Duration::new(0, 0)))
.field("expiration time",
&if let Some(e) = self.expiration_time {
format!("{:?}",
e
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_else(|_| Duration::new(0, 0)))
} else {
"never".to_string()
})
.field("amount", &self.amount)
.field("depth", &self.depth)
.field("regexes",
&if let Some(re_set) = self.re_set.as_ref() {
if re_set.matches_everything() {
String::from("*")
} else {
format!("{:?}", &re_set)
}
} else {
String::from("<invalid RE>")
})
.finish()
}
}
impl Certification {
pub fn new<C1, U, C2>(issuer: C1,
userid: Option<U>,
target: C2,
creation_time: SystemTime)
-> Self
where C1: Into<CertSynopsis>,
U: Into<UserID>,
C2: Into<CertSynopsis>,
{
let issuer = issuer.into();
let target = target.into();
Certification {
issuer: issuer,
userid: userid.map(Into::into),
target: target,
creation_time: creation_time,
expiration_time: None,
exportable: true,
depth: Depth::new(0),
amount: 120,
re_set: Some(RegexSet::everything()),
re_bytes: Vec::new(),
digest_prefix: None,
}
}
pub fn from_signature<C1, U, C2>(issuer: C1,
userid: Option<U>,
target: C2,
sig: &Signature)
-> Self
where C1: Into<CertSynopsis>,
U: Into<UserID>,
C2: Into<CertSynopsis>,
{
let (d, a, r) = if let Some((d, a)) = sig.trust_signature()
{
(d as usize,
a as usize,
Some(sig.regular_expressions()))
} else {
(0, 120, None)
};
let mut c = Self::new(issuer, userid, target,
sig.signature_creation_time()
.unwrap_or(std::time::UNIX_EPOCH))
.set_amount(a)
.set_depth(Depth::new(if d == 255 { None } else { Some(d) }));
if let Some(r) = r {
let r: Vec<&[u8]> = r.collect();
c = c.set_regular_expressions(r.iter().cloned());
c.re_bytes = r.into_iter().map(<[u8]>::to_vec).collect();
}
if let Some(e) = sig.signature_expiration_time() {
c = c.set_expiration_time(Some(e));
}
c = c.set_exportable(sig.exportable_certification().unwrap_or(true));
c.digest_prefix = Some(*sig.digest_prefix());
c
}
pub fn try_from_signature(possible_issuer: &ValidCert,
ua: Option<&UserIDAmalgamation>,
target: &ValidCert,
certification: &Signature)
-> Result<Self>
{
tracer!(TRACE, "Certification::try_from_signature");
let reference_time = target.time();
let certification_time =
if let Some(t) = certification.signature_creation_time() {
t
} else {
return Err(CertificationError::MissingCreationTime(
(possible_issuer, target, certification).into()).into());
};
let verify = |possible_issuer: &ValidCert| -> Result<Certification>
{
if let Err(err) = target.policy()
.signature(
certification, HashAlgoSecurity::CollisionResistance)
{
return Err(CertificationError::InvalidCertification(
(possible_issuer, target, certification).into(),
err).into());
}
certification
.clone()
.verify_signature(possible_issuer.primary_key().key())?;
let possible_issuer_then
= possible_issuer.clone().with_policy(
possible_issuer.policy(), certification_time)?;
if let Err(err) = possible_issuer_then.alive() {
t!("Skipping certification {:02X}{:02X}: issuer \
was not alive at certification time.",
certification.digest_prefix()[0],
certification.digest_prefix()[1]);
return Err(err.context(
"issuer not alive at certification time"));
}
let rs = possible_issuer_then.revocation_status();
if let openpgp::types::RevocationStatus::Revoked(ref revs) = rs {
let reason = revs.iter().next().expect("have one")
.reason_for_revocation();
let msg = reason
.map(|r| r.1.to_vec())
.unwrap_or(Vec::new());
let code = reason
.map(|r| r.0)
.unwrap_or(openpgp::types::ReasonForRevocation::Unspecified);
match RevocationStatus::from(rs) {
RevocationStatus::Hard => {
t!("Skipping certification {:02X}{:02X}: issuer \
was hard revoked.",
certification.digest_prefix()[0],
certification.digest_prefix()[1]);
return Err(CertificationError::IssuerHardRevoked(
(possible_issuer, target, certification).into(),
code, msg).into());
}
RevocationStatus::Soft(rev_time) => {
if rev_time <= certification_time {
t!("Skipping certification {:02X}{:02X}: issuer \
was soft revoked at certification time.",
certification.digest_prefix()[0],
certification.digest_prefix()[1]);
return Err(CertificationError::IssuerSoftRevoked(
(possible_issuer, target, certification).into(),
certification_time, code, msg).into());
}
}
RevocationStatus::NotAsFarAsWeKnow => unreachable!(),
}
}
let issuer: KeyHandle
= possible_issuer.fingerprint().into();
if let Some(ua) = ua {
for rev in ua.other_revocations() {
if target.policy()
.signature(
rev, HashAlgoSecurity::CollisionResistance)
.is_err()
{
continue;
}
if let Some(rev_time) = rev.signature_creation_time() {
if rev_time > reference_time {
continue;
}
if rev_time <= certification_time {
continue;
}
} else {
continue;
};
if rev.get_issuers().iter().any(|kh| {
kh.aliases(&issuer)
}) {
if let Ok(()) = rev
.clone()
.verify_signature(possible_issuer.primary_key().key())
{
t!("issuer revoked certification, ignoring");
return Err(
CertificationError::IssuerRevoked((
possible_issuer, target, certification).into())
.into());
}
}
}
}
let (depth, amount, re_set) = if let Some((d, a))
= certification.trust_signature()
{
(d, a, RegexSet::from_signature(certification)
.expect("internal error"))
} else {
(0, 120, RegexSet::everything())
};
t!("<{}, {}> {} <{}, {}> \
(depth: {}, amount: {}, scope: {:?})",
possible_issuer.cert().keyid(),
possible_issuer
.primary_userid()
.map(|ua| {
String::from_utf8_lossy(ua.userid().value()).into_owned()
})
.unwrap_or("[no User ID]".into()),
if depth > 0 {
"tsigned"
} else {
"certified"
},
target.keyid(),
ua.map(|ua| String::from_utf8_lossy(ua.userid().value()))
.unwrap_or(Cow::Borrowed("(delegation)")),
depth,
amount,
if re_set.matches_everything() {
"*".into()
} else {
format!("{:?}", re_set)
});
Ok(Certification::from_signature(
possible_issuer, ua.map(|ua| ua.userid().clone()), target,
certification))
};
if reference_time < certification_time {
t!("Skipping certification {:02X}{:02X}: \
created ({:?}) after reference time ({:?}).",
certification.digest_prefix()[0],
certification.digest_prefix()[1],
certification_time, reference_time);
return Err(CertificationError::BornLater(
(possible_issuer, target, certification).into(),
reference_time).into());
}
if let Some(e) = certification.signature_expiration_time() {
if e <= reference_time {
t!("Skipping certification {:02X}{:02X}: \
expired ({:?}) as of reference time ({:?}).",
certification.digest_prefix()[0],
certification.digest_prefix()[1],
e, reference_time);
return Err(CertificationError::CertificationExpired(
(possible_issuer, target, certification).into(),
e, reference_time).into());
}
}
let target_then =
match target.clone()
.with_policy(target.policy(), certification_time)
{
Ok(vc) => vc,
Err(err) => {
t!("Skipping certification {:02X}{:02X}: target \
was not valid at certification time: {}.",
certification.digest_prefix()[0],
certification.digest_prefix()[1],
err);
return Err(CertificationError::TargetNotValid(
(possible_issuer, target, certification).into(),
certification_time, err).into());
}
};
if let Err(err) = target_then.alive() {
t!("Skipping certification {:02X}{:02X}: target \
not alive at certification time: {}.",
certification.digest_prefix()[0],
certification.digest_prefix()[1],
err);
return Err(CertificationError::TargetNotLive(
(possible_issuer, target, certification).into(),
certification_time, err).into());
}
let rs = target_then.revocation_status();
if let openpgp::types::RevocationStatus::Revoked(ref revs) = rs {
let reason = revs.iter().next().expect("have one")
.reason_for_revocation();
let msg = reason
.map(|r| r.1.to_vec())
.unwrap_or(Vec::new());
let code = reason
.map(|r| r.0)
.unwrap_or(openpgp::types::ReasonForRevocation::Unspecified);
match RevocationStatus::from(rs) {
RevocationStatus::Hard => {
t!("Skipping certification {:02X}{:02X}: target \
was hard revoked at certification time.",
certification.digest_prefix()[0],
certification.digest_prefix()[1]);
return Err(CertificationError::TargetHardRevoked(
(possible_issuer, target, certification).into(),
code, msg).into());
}
RevocationStatus::Soft(rev_time) => {
if rev_time <= certification_time {
t!("Skipping certification {:02X}{:02X}: target \
was soft revoked at certification time.",
certification.digest_prefix()[0],
certification.digest_prefix()[1]);
return Err(CertificationError::TargetSoftRevoked(
(possible_issuer, target, certification).into(),
certification_time, code, msg).into());
}
}
RevocationStatus::NotAsFarAsWeKnow => unreachable!(),
}
}
match verify(&possible_issuer) {
Ok(certification) => {
t!("Using certification \
by {} for <{:?}, {}> at {:?}: \
{}/{}.",
possible_issuer,
ua.map(|ua| String::from_utf8_lossy(ua.userid().value()))
.unwrap_or(Cow::Borrowed("(delegation)")),
target.keyid(),
certification.creation_time(),
certification.depth(),
certification.amount());
Ok(certification)
}
Err(err) => {
t!("Invalid certification {:02X}{:02X} \
by {} for <{:?}, {}>: {}",
certification.digest_prefix()[0],
certification.digest_prefix()[1],
possible_issuer,
ua.map(|ua| String::from_utf8_lossy(ua.userid().value()))
.unwrap_or(Cow::Borrowed("(delegation)")),
target.keyid(),
err);
Err(err)
}
}
}
pub fn issuer(&self) -> &CertSynopsis {
&self.issuer
}
pub fn target(&self) -> &CertSynopsis {
&self.target
}
pub fn userid(&self) -> Option<&UserID> {
self.userid.as_ref()
}
pub fn creation_time(&self) -> SystemTime {
self.creation_time
}
pub fn expiration_time(&self) -> Option<SystemTime> {
self.expiration_time
}
pub fn set_expiration_time<I>(mut self, expiration_time: I) -> Self
where I: Into<Option<SystemTime>>
{
self.expiration_time = expiration_time.into();
self
}
pub fn exportable(&self) -> bool {
self.exportable
}
pub fn set_exportable(mut self, exportable: bool) -> Self {
self.exportable = exportable;
self
}
pub fn amount(&self) -> usize {
self.amount
}
pub fn set_amount(mut self, amount: usize) -> Self {
self.amount = amount;
self
}
pub fn depth(&self) -> Depth {
self.depth
}
pub fn set_depth<I>(mut self, depth: I) -> Self
where I: Into<Depth>
{
self.depth = depth.into();
self
}
pub fn regular_expressions(&self) -> Option<&RegexSet> {
self.re_set.as_ref()
}
pub fn regular_expressions_bytes(&self) -> &[Vec<u8>] {
&self.re_bytes[..]
}
pub fn set_regular_expressions<'a>(mut self,
re_set: impl Iterator<Item=&'a [u8]>)
-> Self
{
let regexes: Vec<&[u8]> = re_set.collect();
self.re_set = RegexSet::from_bytes(®exes).ok();
self.re_bytes = regexes.into_iter().map(Into::into).collect();
self
}
pub fn digest_prefix(&self) -> Option<&[u8; 2]> {
self.digest_prefix.as_ref()
}
}
#[derive(Clone)]
pub struct CertificationSet {
issuer: CertSynopsis,
target: CertSynopsis,
reference_time: SystemTime,
certifications: HashMap<Option<UserID>, Vec<Certification>>,
}
impl CertificationSet {
pub(crate) fn empty<I, T>(issuer: I, target: T,
reference_time: SystemTime)
-> Self
where I: Into<CertSynopsis>,
T: Into<CertSynopsis>,
{
Self {
issuer: issuer.into(),
target: target.into(),
reference_time: reference_time,
certifications: HashMap::new(),
}
}
pub fn from_certification(certification: Certification,
reference_time: SystemTime) -> Self
{
let mut cs = CertificationSet::empty(
certification.issuer.clone(),
certification.target.clone(),
reference_time);
cs.add(certification);
cs
}
pub fn from_certifications(mut certifications: Vec<Certification>,
reference_time: SystemTime) -> Vec<Self>
{
if certifications.is_empty() {
return Vec::new();
}
certifications.retain(|c| {
c.creation_time <= reference_time
&& c.expiration_time.map(|e| e > reference_time).unwrap_or(true)
});
certifications.sort_unstable_by(|a, b| {
a.issuer().fingerprint().cmp(&b.issuer().fingerprint())
.then(a.target().fingerprint().cmp(&b.target().fingerprint()))
.then(a.userid().cmp(&b.userid()))
.then(a.creation_time().cmp(&b.creation_time()).reverse())
});
let mut cs = Vec::new();
let mut acc: Vec<(Option<UserID>, Vec<Certification>)>
= Vec::with_capacity(certifications.len().min(4));
for certification in certifications.into_iter() {
let group = if let Some(last) = acc.last() {
last
} else {
acc.push((certification.userid().map(Clone::clone),
vec![ certification ]));
continue;
};
let group_issuer = group.1[0].issuer();
let group_target = group.1[0].target();
let group_userid = group.0.as_ref();
let group_certification_time = group.1[0].creation_time();
if group_issuer.fingerprint()
== certification.issuer().fingerprint()
&& group_target.fingerprint()
== certification.target().fingerprint()
{
if group_userid == certification.userid() {
if group_certification_time
== certification.creation_time()
{
acc[0].1.push(certification);
} else {
assert!(certification.creation_time()
< group_certification_time);
}
} else {
acc.push((certification.userid().map(Clone::clone),
vec![ certification ]));
}
} else {
let issuer = acc[0].1[0].issuer().clone();
let target = acc[0].1[0].target().clone();
cs.push(
CertificationSet {
issuer,
target,
reference_time,
certifications: HashMap::from_iter(acc),
});
acc = Vec::new();
acc.push((certification.userid().map(Clone::clone),
vec![ certification ]));
}
}
let issuer = acc[0].1[0].issuer().clone();
let target = acc[0].1[0].target().clone();
cs.push(
CertificationSet {
issuer,
target,
reference_time,
certifications: HashMap::from_iter(acc),
});
cs
}
pub fn issuer(&self) -> &CertSynopsis {
&self.issuer
}
pub fn target(&self) -> &CertSynopsis {
&self.target
}
pub fn reference_time(&self) -> SystemTime {
self.reference_time
}
pub(crate) fn add(&mut self, certification: Certification) {
if let Some((_, cs)) = self.certifications.iter().next() {
for c in cs {
assert_eq!(certification.issuer.fingerprint(),
c.issuer.fingerprint());
assert_eq!(certification.target.fingerprint(),
c.target.fingerprint());
}
}
match self.certifications.entry(certification.userid.clone()) {
e @ Entry::Occupied(_) => {
e.and_modify(|e| e.push(certification));
}
e @ Entry::Vacant(_) => {
e.or_insert([ certification ].into());
}
}
}
pub(crate) fn merge(&mut self, other: Self) {
assert_eq!(self.issuer.fingerprint(), other.issuer.fingerprint());
assert_eq!(self.target.fingerprint(), other.target.fingerprint());
assert_eq!(self.reference_time, other.reference_time);
for (_, cs) in other.certifications.into_iter() {
for c in cs {
self.add(c);
}
}
}
pub fn certifications(&self)
-> impl Iterator<Item=(Option<&UserID>, &[Certification])>
{
self.certifications.iter().map(|(userid, c)| (userid.as_ref(), &c[..]))
}
pub fn into_certifications(self)
-> impl Iterator<Item=Certification>
{
self.certifications.into_iter()
.flat_map(|(_userid, c)| c.into_iter())
}
}
#[cfg(test)]
mod test {
use super::*;
use std::iter;
use std::time::Duration;
use std::time::SystemTime;
use sequoia_openpgp as openpgp;
use openpgp::Fingerprint;
use openpgp::Result;
use crate::CertSynopsis;
use crate::Depth;
#[test]
fn depth() -> Result<()> {
assert_eq!(Depth::new(0), Depth::new(0));
assert_eq!(Depth::new(10), Depth::new(10));
assert_eq!(Depth::new(None), Depth::new(None));
assert!(Depth::new(0) < Depth::new(1));
assert!(Depth::new(1) < Depth::new(10));
assert!(Depth::new(10) < Depth::new(None));
assert!(Depth::new(255) < Depth::new(None));
assert!(Depth::new(1000) < Depth::new(None));
assert!(Depth::new(1) > Depth::new(0));
assert!(Depth::new(10) > Depth::new(1));
assert!(Depth::new(None) > Depth::new(10));
assert!(Depth::new(None) > Depth::new(255));
assert!(Depth::new(None) > Depth::new(1000));
assert_eq!(std::cmp::min(Depth::new(0), Depth::new(10)),
Depth::new(0));
assert_eq!(std::cmp::min(Depth::new(0), Depth::new(None)),
Depth::new(0));
assert_eq!(std::cmp::min(Depth::new(1000), Depth::new(None)),
Depth::new(1000));
assert_eq!(std::cmp::min(Depth::new(10), Depth::new(0)),
Depth::new(0));
assert_eq!(std::cmp::min(Depth::new(None), Depth::new(0)),
Depth::new(0));
assert_eq!(std::cmp::min(Depth::new(None), Depth::new(1000)),
Depth::new(1000));
assert_eq!(std::cmp::min(Depth::new(None), Depth::new(None)),
Depth::new(None));
Ok(())
}
#[test]
fn certification_set_from_certifications() -> Result<()> {
use openpgp::types::RevocationStatus;
let alice_fpr: Fingerprint =
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
.parse().expect("valid fingerprint");
let alice_uid = UserID::from("<alice@example.org>");
let alice = CertSynopsis::new(
alice_fpr.clone(), None,
RevocationStatus::NotAsFarAsWeKnow.into(),
iter::once((alice_uid.clone(), SystemTime::now())));
let bob_fpr: Fingerprint =
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
.parse().expect("valid fingerprint");
let bob_uid = UserID::from("<bob@example.org>");
let bob = CertSynopsis::new(
bob_fpr.clone(), None,
RevocationStatus::NotAsFarAsWeKnow.into(),
iter::once((bob_uid.clone(), SystemTime::now())));
let carol_fpr: Fingerprint =
"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
.parse().expect("valid fingerprint");
let carol_uid = UserID::from("<carol@example.org>");
let carol = CertSynopsis::new(
carol_fpr.clone(), None,
RevocationStatus::NotAsFarAsWeKnow.into(),
iter::once((carol_uid.clone(), SystemTime::now())));
let t = SystemTime::now();
let certifications = vec![
Certification::new(alice.clone(),
Some(bob_uid.clone()),
bob.clone(),
t),
Certification::new(alice.clone(),
Some(bob_uid.clone()),
bob.clone(),
t),
Certification::new(alice.clone(),
Some(bob_uid.clone()),
bob.clone(),
t - Duration::new(1, 0)),
Certification::new(alice.clone(),
Some(bob_uid.clone()),
bob.clone(),
t + Duration::new(1, 0)),
Certification::new(alice.clone(),
Some(carol_uid.clone()),
carol.clone(),
t),
Certification::new(bob.clone(),
Some(carol_uid.clone()),
carol.clone(),
t),
Certification::new(bob.clone(),
Some(alice_uid.clone()),
carol.clone(),
t),
];
let mut cs = CertificationSet::from_certifications(certifications, t);
assert_eq!(cs.len(), 3);
cs.sort_by_key(|c| {
(c.issuer().fingerprint(), c.target().fingerprint())
});
assert_eq!(cs[0].issuer().fingerprint(), alice_fpr);
assert_eq!(cs[0].target().fingerprint(), bob_fpr);
assert_eq!(cs[0].certifications().count(), 1);
assert_eq!(cs[0].certifications().next().unwrap().1.len(), 2);
assert_eq!(cs[1].issuer().fingerprint(), alice_fpr);
assert_eq!(cs[1].target().fingerprint(), carol_fpr);
assert_eq!(cs[1].certifications().count(), 1);
assert_eq!(cs[1].certifications().next().unwrap().1.len(), 1);
assert_eq!(cs[2].issuer().fingerprint(), bob_fpr);
assert_eq!(cs[2].target().fingerprint(), carol_fpr);
assert_eq!(cs[2].certifications().count(), 2);
assert_eq!(cs[2].certifications().next().unwrap().1.len(), 1);
assert_eq!(cs[2].certifications().nth(1).unwrap().1.len(), 1);
Ok(())
}
}