Refactor sq pki certify, sq pki link add and sq pki link retract.

- Pull similar functionality out of the implementation of `sq pki
    certify`, `sq pki link add`, and `sq pki link retract`, and put it
    in a new module, `common::pki::certify`.

  - This slightly changes the human readable output.
This commit is contained in:
Neal H. Walfield 2024-10-10 14:25:44 +02:00
parent 56b8065b82
commit 4a3c360f41
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
7 changed files with 545 additions and 600 deletions

View File

@ -77,7 +77,7 @@ pub fn dispatch(sq: Sq, command: SqCommand) -> Result<()>
///
/// Note: if `n` User IDs are provided, then the returned vector has
/// `n` elements.
fn active_certification(
pub fn active_certification(
sq: &Sq,
cert: &Cert, userids: Vec<UserID>,
issuer: &Key<openpgp::packet::key::PublicParts,

View File

@ -1,27 +1,36 @@
use std::fmt;
use std::sync::Arc;
use chrono::DateTime;
use chrono::Utc;
use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use openpgp::packet::UserID;
use openpgp::Result;
use openpgp::packet::prelude::*;
use openpgp::packet::signature::subpacket::NotationDataFlags;
use openpgp::serialize::Serialize;
use openpgp::types::SignatureType;
use openpgp::types::KeyFlags;
use sequoia_cert_store as cert_store;
use cert_store::StoreUpdate;
use crate::Sq;
use crate::parse_notations;
use crate::cli::pki::certify;
use crate::cli::types::FileOrStdin;
use crate::cli::types::FileStdinOrKeyHandle;
use crate::commands::FileOrStdout;
use crate::parse_notations;
/// How to match on user IDs.
#[derive(Debug)]
enum UserIDQuery {
/// Exact match.
Exact(String),
/// Match on the email address.
Email(String),
}
impl fmt::Display for UserIDQuery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UserIDQuery::Exact(q) => f.write_str(q),
UserIDQuery::Email(q) => write!(f, "<{}>", q),
}
}
}
pub fn certify(sq: Sq, mut c: certify::Command)
-> Result<()>
@ -35,6 +44,9 @@ pub fn certify(sq: Sq, mut c: certify::Command)
panic!("clap enforces --certifier or --certifier-file is set");
};
let certifier = sq.lookup_one(
certifier, Some(KeyFlags::empty().set_certification()), true)?;
// XXX: Change this interface: it's dangerous to guess whether an
// identifier is a file or a key handle.
let cert = if let Ok(kh) = c.certificate.parse::<KeyHandle>() {
@ -51,36 +63,16 @@ pub fn certify(sq: Sq, mut c: certify::Command)
}
}
let userid = c.userid;
let certifier = sq.lookup_one(
certifier, Some(KeyFlags::empty().set_certification()), true)?;
let cert = sq.lookup_one(cert, None, true)?;
let trust_depth: u8 = c.depth;
let trust_amount: u8 = c.amount.amount();
let regex = c.regex;
if trust_depth == 0 && !regex.is_empty() {
return Err(
anyhow::format_err!("A regex only makes sense \
if the trust depth is greater than 0"));
}
let local = c.local;
let non_revocable = c.non_revocable;
let time = sq.time;
let vc = cert.with_policy(sq.policy, Some(time))?;
let query = if c.email {
UserIDQuery::Email(userid)
} else {
UserIDQuery::Exact(userid)
};
let vc = cert.with_policy(sq.policy, Some(sq.time))?;
// Find the matching User ID.
let query = if c.email {
UserIDQuery::Email(c.userid)
} else {
UserIDQuery::Exact(c.userid)
};
let mut userids = Vec::new();
for ua in vc.userids() {
match &query {
@ -133,125 +125,22 @@ pub fn certify(sq: Sq, mut c: certify::Command)
return Err(anyhow::format_err!("No matching User ID found"));
};
// Get the signer to certify with.
let mut signer = sq.get_certification_key(certifier, None)?;
let notations = parse_notations(&c.notation)?;
// Create the certifications.
let mut new_packets: Vec<Packet> = Vec::new();
for userid in userids {
let mut builder
= SignatureBuilder::new(SignatureType::GenericCertification);
if trust_depth != 0 || trust_amount != 120 {
builder = builder.set_trust_signature(trust_depth, trust_amount)?;
}
for regex in &regex {
builder = builder.add_regular_expression(regex)?;
}
if local {
builder = builder.set_exportable_certification(false)?;
}
if non_revocable {
builder = builder.set_revocable(false)?;
}
// Creation time.
builder = builder.set_signature_creation_time(time)?;
if let Some(validity) = c
.expiration
.as_duration(DateTime::<Utc>::from(sq.time))?
{
builder = builder.set_signature_validity_period(validity)?;
}
let notations = parse_notations(&c.notation)?;
for (critical, n) in notations {
builder = builder.add_notation(
n.name(),
n.value(),
NotationDataFlags::empty().set_human_readable(),
critical)?;
};
// Sign it.
let certification = builder
.sign_userid_binding(
&mut signer,
cert.primary_key().component(),
&userid)?;
new_packets.push(userid.into());
new_packets.push(certification.into());
}
let cert = cert.insert_packets(new_packets)?;
if let Some(output) = c.output {
// And export it.
let path = output.path().map(Clone::clone);
let mut message = output.create_pgp_safe(
&sq,
c.binary,
sequoia_openpgp::armor::Kind::PublicKey,
)?;
cert.serialize(&mut message)?;
message.finalize()?;
if let Some(path) = path {
sq.hint(format_args!(
"Updated certificate written to {}. \
To make the update effective, it has to be published \
so that others can find it, for example using:",
path.display()))
.sq().arg("network").arg("keyserver").arg("publish")
.arg_value("--file", path.display())
.done();
} else {
sq.hint(format_args!(
"To make the update effective, it has to be published \
so that others can find it."));
}
} else {
// Import it.
let cert_store = sq.cert_store_or_else()?;
let fipr = cert.fingerprint();
if let Err(err) = cert_store.update(Arc::new(cert.into())) {
wprintln!("Error importing updated cert: {}", err);
return Err(err);
} else {
sq.hint(format_args!(
"Imported updated cert into the cert store. \
To make the update effective, it has to be published \
so that others can find it, for example using:"))
.sq().arg("network").arg("keyserver").arg("publish")
.arg_value("--cert", fipr)
.done();
}
}
Ok(())
}
/// How to match on user IDs.
#[derive(Debug)]
enum UserIDQuery {
/// Exact match.
Exact(String),
/// Match on the email address.
Email(String),
}
impl fmt::Display for UserIDQuery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UserIDQuery::Exact(q) => f.write_str(q),
UserIDQuery::Email(q) => write!(f, "<{}>", q),
}
}
crate::common::pki::certify::certify(
&sq,
true, // Always recreate.
&certifier,
&cert,
&userids[..],
c.add_userid,
true, // User supplied user IDs.
&[(c.amount, c.expiration)],
c.depth,
&c.regex[..],
c.local,
c.non_revocable,
&notations[..],
c.output,
c.binary)
}

View File

@ -1,30 +1,24 @@
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use std::time::Duration;
use anyhow::Context;
use chrono::DateTime;
use chrono::Utc;
use sequoia_openpgp as openpgp;
use openpgp::Result;
use openpgp::cert::prelude::*;
use openpgp::packet::prelude::*;
use openpgp::packet::signature::subpacket::NotationDataFlags;
use openpgp::types::RevocationStatus;
use openpgp::types::SignatureType;
use sequoia_cert_store as cert_store;
use cert_store::Store;
use cert_store::StoreUpdate;
use cert_store::store::UserIDQueryParams;
use crate::Sq;
use crate::commands::active_certification;
use crate::commands::pki::TrustAmount;
use crate::parse_notations;
use crate::print_error_chain;
use crate::cli::pki::link;
use crate::cli::types::Expiration;
/// Checks that the search terms provided to --userid, --email, and
/// patterns match known User IDs.
@ -201,124 +195,6 @@ pub fn check_userids(sq: &Sq, cert: &Cert, self_signed: bool,
}
}
// Returns whether two signatures have the same parameters.
//
// This does some normalization and only considers things that are
// relevant to links.
fn diff_link(sq: &Sq, old: &Signature, new: &SignatureBuilder, new_ct: SystemTime)
-> bool
{
make_qprintln!(sq.quiet);
let mut changed = false;
let a_expiration = old.signature_expiration_time();
let b_expiration = if let Some(vp) = new.signature_validity_period() {
Some(new_ct + vp)
} else {
None
};
if a_expiration != b_expiration {
changed = true;
qprintln!(
" Updating expiration time: {} -> {}.",
if let Some(a_expiration) = a_expiration {
chrono::DateTime::<chrono::offset::Utc>::from(
a_expiration).to_string()
} else {
"no expiration".to_string()
},
if let Some(b_expiration) = b_expiration {
chrono::DateTime::<chrono::offset::Utc>::from(
b_expiration).to_string()
} else {
"no expiration".to_string()
});
}
let (a_depth, a_amount) = old.trust_signature().unwrap_or((0, 120));
let (b_depth, b_amount) = new.trust_signature().unwrap_or((0, 120));
if a_amount != b_amount {
changed = true;
qprintln!(" Updating trust amount: {} -> {}.",
a_amount, b_amount);
}
if a_depth != b_depth {
changed = true;
qprintln!(" Update trust depth: {} -> {}.",
a_depth, b_depth);
}
let mut a_regex: Vec<_> = old.regular_expressions().collect();
a_regex.sort();
a_regex.dedup();
let mut b_regex: Vec<_> = new.regular_expressions().collect();
b_regex.sort();
b_regex.dedup();
if a_regex != b_regex {
changed = true;
qprintln!(" Updating regular expressions:");
let a_regex: Vec<String> = a_regex.into_iter()
.enumerate()
.map(|(i, r)| {
format!("{}. {:?}",
i + 1, String::from_utf8_lossy(r))
})
.collect();
qprintln!(" Current link:\n {}",
a_regex.join("\n "));
let b_regex: Vec<String> = b_regex.into_iter()
.enumerate()
.map(|(i, r)| {
format!("{}. {:?}",
i + 1, String::from_utf8_lossy(r))
})
.collect();
qprintln!(" Updated link:\n {}",
b_regex.join("\n "));
}
let a_notations: Vec<_> = old.notation_data()
.filter(|n| n.name() != "salt@notations.sequoia-pgp.org")
.collect();
let b_notations: Vec<_> = new.notation_data()
.filter(|n| n.name() != "salt@notations.sequoia-pgp.org")
.collect();
if a_notations != b_notations {
changed = true;
qprintln!(" Updating notations.");
let a_notations: Vec<String> = a_notations.into_iter()
.enumerate()
.map(|(i, n)| {
format!("{}. {:?}", i + 1, n)
})
.collect();
qprintln!(" Current link:\n {}",
a_notations.join("\n "));
let b_notations: Vec<String> = b_notations.into_iter()
.enumerate()
.map(|(i, n)| {
format!("{}. {:?}", i + 1, n)
})
.collect();
qprintln!(" Updated link:\n {}",
b_notations.join("\n "));
}
let a_exportable = old.exportable_certification().unwrap_or(true);
let b_exportable = new.exportable_certification().unwrap_or(true);
if a_exportable != b_exportable {
changed = true;
qprintln!(" Updating exportable flag: {} -> {}.",
a_exportable, b_exportable);
}
changed
}
pub fn link(sq: Sq, c: link::Command) -> Result<()> {
use link::Subcommands::*;
match c.subcommand {
@ -332,8 +208,6 @@ pub fn link(sq: Sq, c: link::Command) -> Result<()> {
pub fn add(sq: Sq, c: link::AddCommand)
-> Result<()>
{
make_qprintln!(sq.quiet);
let trust_root = sq.local_trust_root()?;
let trust_root = trust_root.to_cert()?;
@ -381,7 +255,6 @@ pub fn add(sq: Sq, c: link::AddCommand)
} else {
0
};
let trust_amount: u8 = c.amount.amount();
let mut regex = c.regex;
if trust_depth == 0 && !regex.is_empty() {
@ -430,189 +303,49 @@ pub fn add(sq: Sq, c: link::AddCommand)
}
}
// Create the certification.
let mut builder
= SignatureBuilder::new(SignatureType::GenericCertification);
if trust_depth != 0 || trust_amount != 120 {
builder = builder.set_trust_signature(trust_depth, trust_amount)?;
}
for regex in regex {
builder = builder.add_regular_expression(regex)?;
}
builder = builder.set_exportable_certification(false)?;
// Creation time.
builder = builder.set_signature_creation_time(sq.time)?;
let notations = parse_notations(c.notation)?;
for (critical, n) in notations {
builder = builder.add_notation(
n.name(),
n.value(),
NotationDataFlags::empty().set_human_readable(),
critical)?;
};
let builders: Vec<SignatureBuilder> = if c.temporary {
let templates = if c.temporary {
// Make the partially trusted link one second younger. When
// the fully trusted link expired, then this link will come
// into effect. If the user has fully linked the binding in
// the meantime, then this won't override that, which is
// exactly what we want.
let mut partial = builder.clone();
partial = partial.set_signature_creation_time(
sq.time - Duration::new(1, 0))?;
partial = partial.set_trust_signature(trust_depth, 40)?;
let week = Duration::new(7 * 24 * 60 * 60, 0);
builder = builder.set_signature_validity_period(
Duration::new(7 * 24 * 60 * 60, 0))?;
vec![ builder, partial ]
vec![
(TrustAmount::Other(40), c.expiration),
(c.amount, Expiration::Duration(week)),
]
} else {
if let Some(validity) = c
.expiration
.as_duration(DateTime::<Utc>::from(sq.time))? {
builder = builder.set_signature_validity_period(validity)?;
}
vec![ builder ]
vec![
(c.amount, c.expiration),
]
};
// Sign it.
let mut signer = sq.get_certification_key(trust_root, None)
.context("Looking up local trust root")?;
let certifications = active_certification(
&sq, &cert, userids,
signer.public())
.into_iter()
.map(|(userid, active_certification)| {
let userid_str = || String::from_utf8_lossy(userid.value());
if let Some(ua) = vc.userids().find(|ua| ua.userid() == &userid) {
if let RevocationStatus::Revoked(_) = ua.revocation_status() {
// It's revoked.
if user_supplied_userids {
// It was explicitly mentioned. Return an
// error.
return Err(anyhow::anyhow!(
"Can't link {:?} with {}, it's revoked",
userid_str(), cert.fingerprint()));
} else {
// We're just considering valid, self-signed
// User IDs. Silently, skip it.
return Ok(vec![]);
}
}
} else {
qprintln!("Note: {:?} is NOT a self signed User ID. \
If this was a mistake, use \
`sq pki link retract {} \"{}\"` to undo it.",
userid_str(), cert.fingerprint(), userid);
}
if let Some(active_certification) = active_certification {
let active_certification_ct
= active_certification.signature_creation_time()
.expect("valid signature");
let retracted = matches!(active_certification.trust_signature(),
Some((_depth, 0)));
if retracted {
qprintln!("{}, {} was retracted at {}.",
cert.fingerprint(), userid_str(),
chrono::DateTime::<chrono::offset::Utc>::from(
active_certification_ct));
} else {
qprintln!("{}, {} was already linked at {}.",
cert.fingerprint(), userid_str(),
chrono::DateTime::<chrono::offset::Utc>::from(
active_certification_ct));
}
let changed = diff_link(
&sq,
&active_certification,
&builders[0], sq.time);
if ! changed && c.recreate {
qprintln!(" Link parameters are unchanged, but \
updating anyway as \"--recreate\" was specified.");
} else if c.temporary {
qprintln!(" Creating a temporary link, \
which expires in a week.");
} else if ! changed {
qprintln!(" Link parameters are unchanged, no update \
needed (specify \"--recreate\" to update anyway).");
// Return a signature packet to indicate that we
// processed something. But don't return a
// signature.
return Ok(vec![ Packet::from(userid.clone()) ]);
} else {
qprintln!(" Link parameters changed, updating link.");
}
}
qprintln!("Linking {} and {:?}.",
cert.fingerprint(), userid_str());
let mut sigs = builders.iter()
.map(|builder| {
builder.clone().sign_userid_binding(
&mut signer,
cert.primary_key().key(),
&userid)
.with_context(|| {
format!("Creating certification for {:?}",
userid_str())
})
.map(Into::into)
})
.collect::<Result<Vec<Packet>>>()?;
qprintln!();
let mut packets = vec![ Packet::from(userid.clone()) ];
packets.append(&mut sigs);
Ok(packets)
})
.collect::<Result<Vec<Vec<Packet>>>>()?
.into_iter()
.flatten()
.collect::<Vec<Packet>>();
if certifications.is_empty() {
return Err(anyhow::anyhow!(
"Can't link {} to anything. The certificate has no self-signed \
User IDs and you didn't specify any User IDs to link to it.",
cert.fingerprint()));
}
if certifications.iter().all(|p| matches!(p, Packet::UserID(_))) {
// There are no signatures to insert. We're done.
return Ok(());
}
let cert = cert.insert_packets(certifications.clone())?;
let cert_store = sq.cert_store_or_else()?;
cert_store.update(Arc::new(cert.into()))
.with_context(|| format!("Updating {}", c.certificate))?;
Ok(())
crate::common::pki::certify::certify(
&sq,
c.recreate, // Recreate.
&trust_root,
&cert,
&userids[..],
true, // Add userid.
user_supplied_userids,
&templates,
trust_depth,
&regex[..],
true, // Local.
false, // Non-revocable.
&notations[..],
None, // Output.
false) // Binary.
}
pub fn retract(sq: Sq, c: link::RetractCommand)
-> Result<()>
{
make_qprintln!(sq.quiet);
let trust_root = sq.local_trust_root()?;
let trust_root = trust_root.to_cert()?;
let trust_root_kh = trust_root.key_handle();
let cert = sq.lookup_one(&c.certificate, None, true)?;
@ -620,142 +353,34 @@ pub fn retract(sq: Sq, c: link::RetractCommand)
check_userids(&sq, &cert, false, &c.userid, &c.email, &c.pattern)
.context("sq pki link retract: Invalid User IDs")?;
// Nothing was specified. Retract all known User IDs.
if userids.is_empty() {
let user_supplied_userids = if userids.is_empty() {
// Nothing was specified. Retract all known User IDs.
let vc = cert.with_policy(sq.policy, Some(sq.time))?;
userids = vc.userids().map(|ua| ua.userid().clone()).collect();
}
// Create the certification.
let mut builder
= SignatureBuilder::new(SignatureType::GenericCertification);
builder = builder.set_trust_signature(0, 0)?;
builder = builder.set_exportable_certification(false)?;
// Creation time.
builder = builder.set_signature_creation_time(sq.time)?;
let notations = parse_notations(c.notation)?;
for (critical, n) in notations {
builder = builder.add_notation(
n.name(),
n.value(),
NotationDataFlags::empty().set_human_readable(),
critical)?;
false
} else {
true
};
// Sign it.
let mut signer = sq.get_certification_key(trust_root, None)
.context("Looking up local trust root")?;
let notations = parse_notations(c.notation)?;
let certifications = active_certification(
&sq, &cert, userids, signer.public())
.into_iter()
.map(|(userid, active_certification)| {
let userid_str = || String::from_utf8_lossy(userid.value());
if let Some(ua) = cert.userids().find(|ua| ua.userid() == &userid) {
if ! ua.certifications().any(|c| {
c.get_issuers().into_iter()
.any(|issuer| issuer.aliases(&trust_root_kh))
})
{
qprintln!("You never linked {:?} to {}, \
no need to retract it.",
userid_str(), cert.fingerprint());
return Ok(vec![]);
}
}
if let Some(active_certification) = active_certification {
let active_certification_ct
= active_certification.signature_creation_time()
.expect("valid signature");
let retracted = matches!(active_certification.trust_signature(),
Some((_depth, 0)));
if retracted {
qprintln!("{}, {} was already retracted at {}.",
cert.fingerprint(), userid_str(),
chrono::DateTime::<chrono::offset::Utc>::from(
active_certification_ct));
} else {
qprintln!("{}, {} was linked at {}.",
cert.fingerprint(), userid_str(),
chrono::DateTime::<chrono::offset::Utc>::from(
active_certification_ct));
}
let changed = diff_link(
&sq,
&active_certification,
&builder, sq.time);
if ! changed && c.recreate {
qprintln!(" Link parameters are unchanged, but \
updating anyway as \"--recreate\" was specified.");
} else if ! changed {
qprintln!(" Link parameters are unchanged, no update \
needed (specify \"--recreate\" to update anyway).");
// Return a signature packet to indicate that we
// processed something. But don't return a
// signature.
return Ok(vec![ Packet::from(userid.clone()) ]);
} else {
qprintln!(" Link parameters changed, updating link.");
}
} else if c.recreate {
qprintln!("There is no link to retract between {} and {:?}, \
retracting anyways as \"--recreate\" was specified.",
cert.fingerprint(), userid_str());
} else {
qprintln!("There is no link to retract between {} and {:?} \
(specify \"--recreate\" to mark as retracted anyways).",
cert.fingerprint(), userid_str());
// Return a signature packet to indicate that we
// processed something. But don't return a
// signature.
return Ok(vec![ Packet::from(userid.clone()) ]);
}
qprintln!("Breaking link between {} and {:?}.",
cert.fingerprint(), userid_str());
// XXX: If we already have exactly this signature (modulo
// the creation time), then don't add it! Note: it is
// explicitly NOT enough to check that there is a
// certification from the local trust root.
let sig = builder.clone().sign_userid_binding(
&mut signer,
cert.primary_key().key(),
&userid)
.with_context(|| {
format!("Creating certification for {:?}", userid_str())
})?;
Ok(vec![ Packet::from(userid.clone()), Packet::from(sig) ])
})
.collect::<Result<Vec<Vec<Packet>>>>()?
.into_iter()
.flatten()
.collect::<Vec<Packet>>();
if certifications.is_empty() {
qprintln!("Nothing to retract.");
return Ok(());
}
let cert = cert.insert_packets(certifications.clone())?;
let cert_store = sq.cert_store_or_else()?;
cert_store.update(Arc::new(cert.into()))
.with_context(|| format!("Updating {}", c.certificate))?;
Ok(())
crate::common::pki::certify::certify(
&sq,
c.recreate, // Recreate.
&trust_root,
&cert,
&userids[..],
false, // Add userid.
user_supplied_userids,
&[(TrustAmount::None, Expiration::Never)],
0,
&[][..],
true, // Local.
false, // Non-revocable.
&notations[..],
None, // Output.
false) // Binary.
}
pub fn list(sq: Sq, c: link::ListCommand)

View File

@ -17,6 +17,7 @@ pub use revoke::RevocationOutput;
pub mod key;
pub mod password;
pub mod pki;
pub mod userid;
pub const NULL_POLICY: &NullPolicy = &NullPolicy::new();

1
src/common/pki.rs Normal file
View File

@ -0,0 +1 @@
pub mod certify;

429
src/common/pki/certify.rs Normal file
View File

@ -0,0 +1,429 @@
use std::sync::Arc;
use std::time::Duration;
use std::time::SystemTime;
use anyhow::Context;
use chrono::DateTime;
use chrono::Utc;
use sequoia_openpgp as openpgp;
use openpgp::Cert;
use openpgp::Result;
use openpgp::cert::amalgamation::ValidAmalgamation;
use openpgp::packet::prelude::*;
use openpgp::packet::signature::subpacket::NotationData;
use openpgp::packet::signature::subpacket::NotationDataFlags;
use openpgp::serialize::Serialize;
use openpgp::types::RevocationStatus;
use openpgp::types::SignatureType;
use sequoia_cert_store as cert_store;
use cert_store::StoreUpdate;
use crate::Sq;
use crate::cli::types::Expiration;
use crate::cli::types::FileOrStdout;
use crate::cli::types::TrustAmount;
use crate::commands::active_certification;
// Returns whether two certifications have the same parameters.
//
// This does some normalization and only considers things that are
// relevant to certifications:
//
// - Expiration time
// - Trust depth
// - Trust amount
// - Regular expressions
// - Notations
// - Exportable
pub fn diff_certification(sq: &Sq, old: &Signature, new: &SignatureBuilder,
new_ct: SystemTime)
-> bool
{
make_qprintln!(sq.quiet);
let mut changed = false;
let a_expiration = old.signature_expiration_time();
let b_expiration = if let Some(vp) = new.signature_validity_period() {
Some(new_ct + vp)
} else {
None
};
if a_expiration != b_expiration {
changed = true;
qprintln!(
" Updating expiration time: {} -> {}.",
if let Some(a_expiration) = a_expiration {
chrono::DateTime::<chrono::offset::Utc>::from(
a_expiration).to_string()
} else {
"no expiration".to_string()
},
if let Some(b_expiration) = b_expiration {
chrono::DateTime::<chrono::offset::Utc>::from(
b_expiration).to_string()
} else {
"no expiration".to_string()
});
}
let (a_depth, a_amount) = old.trust_signature().unwrap_or((0, 120));
let (b_depth, b_amount) = new.trust_signature().unwrap_or((0, 120));
if a_amount != b_amount {
changed = true;
qprintln!(" Updating trust amount: {} -> {}.",
a_amount, b_amount);
}
if a_depth != b_depth {
changed = true;
qprintln!(" Updating trust depth: {} -> {}.",
a_depth, b_depth);
}
let mut a_regex: Vec<_> = old.regular_expressions().collect();
a_regex.sort();
a_regex.dedup();
let mut b_regex: Vec<_> = new.regular_expressions().collect();
b_regex.sort();
b_regex.dedup();
if a_regex != b_regex {
changed = true;
qprintln!(" Updating regular expressions:");
let a_regex: Vec<String> = a_regex.into_iter()
.enumerate()
.map(|(i, r)| {
format!("{}. {:?}",
i + 1, String::from_utf8_lossy(r))
})
.collect();
qprintln!(" Current certification:\n {}",
a_regex.join("\n "));
let b_regex: Vec<String> = b_regex.into_iter()
.enumerate()
.map(|(i, r)| {
format!("{}. {:?}",
i + 1, String::from_utf8_lossy(r))
})
.collect();
qprintln!(" New certification:\n {}",
b_regex.join("\n "));
}
let a_notations: Vec<_> = old.notation_data()
.filter(|n| n.name() != "salt@notations.sequoia-pgp.org")
.collect();
let b_notations: Vec<_> = new.notation_data()
.filter(|n| n.name() != "salt@notations.sequoia-pgp.org")
.collect();
if a_notations != b_notations {
changed = true;
qprintln!(" Updating notations.");
let a_notations: Vec<String> = a_notations.into_iter()
.enumerate()
.map(|(i, n)| {
format!("{}. {:?}", i + 1, n)
})
.collect();
qprintln!(" Current certification:\n {}",
a_notations.join("\n "));
let b_notations: Vec<String> = b_notations.into_iter()
.enumerate()
.map(|(i, n)| {
format!("{}. {:?}", i + 1, n)
})
.collect();
qprintln!(" Updated certification:\n {}",
b_notations.join("\n "));
}
let a_exportable = old.exportable_certification().unwrap_or(true);
let b_exportable = new.exportable_certification().unwrap_or(true);
if a_exportable != b_exportable {
changed = true;
qprintln!(" Updating exportable flag: {} -> {}.",
a_exportable, b_exportable);
}
changed
}
/// This function is used for certifications and retractions.
///
/// If the trust amount is 0, the operation is interpreted as a
/// retraction and the wording is changed accordingly.
pub fn certify(sq: &Sq,
recreate: bool,
certifier: &Cert,
cert: &Cert,
userids: &[UserID],
add_userid: bool,
user_supplied_userids: bool,
templates: &[(TrustAmount<u8>, Expiration)],
trust_depth: u8,
regex: &[String],
local: bool,
non_revocable: bool,
notations: &[(bool, NotationData)],
output: Option<FileOrStdout>,
binary: bool)
-> Result<()>
{
assert!(templates.len() > 0);
make_qprintln!(sq.quiet);
if trust_depth == 0 && !regex.is_empty() {
return Err(
anyhow::format_err!("A regex constraint only makes sense \
if the trust depth is greater than 0"));
}
let vc = cert.with_policy(sq.policy, sq.time)?;
// Get the signer to certify with.
let mut signer = sq.get_certification_key(certifier, None)?;
let mut base
= SignatureBuilder::new(SignatureType::GenericCertification);
for regex in regex {
base = base.add_regular_expression(regex)?;
}
if local {
base = base.set_exportable_certification(false)?;
}
if non_revocable {
base = base.set_revocable(false)?;
}
for (critical, n) in notations {
base = base.add_notation(
n.name(),
n.value(),
NotationDataFlags::empty().set_human_readable(),
*critical)?;
};
let mut retract = false;
let mut builders = Vec::with_capacity(templates.len());
for (i, (trust_amount, expiration)) in templates.into_iter().enumerate() {
let mut builder = base.clone();
let trust_amount: u8 = trust_amount.amount();
if trust_amount == 0 {
retract = true;
}
if trust_depth != 0 || trust_amount != 120 {
builder = builder.set_trust_signature(
trust_depth, trust_amount)?;
}
// Creation time.
//
// If we should make two certifications, then the first one
// should be at `sq.time - 1`, and the second one at
// `sq.time`. That is, the first one is a second earlier.
let backdate = Duration::new((templates.len() - 1 - i) as u64, 0);
let ct = sq.time - backdate;
builder = builder.set_signature_creation_time(ct)?;
// Expiration.
if let Some(validity) = expiration
.as_duration(DateTime::<Utc>::from(sq.time))?
{
builder = builder.set_signature_validity_period(validity)?;
}
builders.push(builder);
}
// Get the active certification as of the reference time.
let certifications = active_certification(
&sq, &cert, userids.to_vec(),
certifier.primary_key().key().role_as_unspecified())
.into_iter()
.map(|(userid, active_certification)| {
let userid_str = || String::from_utf8_lossy(userid.value());
if let Some(ua) = vc.userids().find(|ua| ua.userid() == &userid) {
if retract {
// Check if we certified it.
if ! ua.certifications().any(|c| {
c.get_issuers().into_iter()
.any(|issuer| issuer.aliases(&certifier.key_handle()))
})
{
qprintln!("You never certified {:?} for {}, \
there is nothing to retract.",
userid_str(), cert.fingerprint());
return Ok(vec![ Packet::from(userid.clone()) ]);
}
} else {
if let RevocationStatus::Revoked(_) = ua.revocation_status() {
// It's revoked.
if user_supplied_userids {
// It was explicitly mentioned. Return an
// error.
return Err(anyhow::anyhow!(
"Can't certify {:?} for {}, it's revoked",
userid_str(), cert.fingerprint()));
} else {
// We're just considering valid, self-signed
// user IDs. Silently, skip it.
return Ok(vec![]);
}
}
}
} else if ! add_userid {
if retract {
qprintln!("You never certified {:?} for {}, \
there is nothing to retract.",
userid_str(), cert.fingerprint());
// Return a signature packet to indicate that we
// processed something. But don't return a
// signature.
return Ok(vec![ Packet::from(userid.clone()) ]);
} else {
return Err(anyhow::anyhow!(
"{:?} is NOT a self signed user ID. \
Supply \"--add-userid\" to certify it anyways.",
userid_str()));
}
}
if let Some(active_certification) = active_certification {
let active_certification_ct
= active_certification.signature_creation_time()
.expect("valid signature");
let retracted = matches!(active_certification.trust_signature(),
Some((_depth, 0)));
if retracted {
qprintln!("A prior certification for {}, {} was retracted at {}.",
cert.fingerprint(), userid_str(),
chrono::DateTime::<chrono::offset::Utc>::from(
active_certification_ct));
} else {
qprintln!("{}, {} was previously certified at {}.",
cert.fingerprint(), userid_str(),
chrono::DateTime::<chrono::offset::Utc>::from(
active_certification_ct));
}
let changed = diff_certification(
&sq,
&active_certification,
&builders[0], sq.time);
if ! changed {
qprintln!(" Certification parameters are unchanged.");
if ! recreate {
// Return a signature packet to indicate that we
// processed something. But don't return a
// signature.
return Ok(vec![ Packet::from(userid.clone()) ]);
}
} else {
qprintln!(" Parameters changed, creating a new certification.");
}
}
qprintln!("Certifying {:?} for {}.",
userid_str(), cert.fingerprint());
let mut sigs = builders.iter()
.map(|builder| {
builder.clone().sign_userid_binding(
&mut signer,
cert.primary_key().key(),
&userid)
.with_context(|| {
format!("Creating certification for {:?}",
userid_str())
})
.map(Into::into)
})
.collect::<Result<Vec<Packet>>>()?;
qprintln!();
let mut packets = vec![ Packet::from(userid.clone()) ];
packets.append(&mut sigs);
Ok(packets)
})
.collect::<Result<Vec<Vec<Packet>>>>()?
.into_iter()
.flatten()
.collect::<Vec<Packet>>();
if certifications.is_empty() {
return Err(anyhow::anyhow!(
"Can't certify {}. The certificate has no self-signed \
user IDs and you didn't specify any user IDs to certify.",
cert.fingerprint()));
}
if certifications.iter().all(|p| matches!(p, Packet::UserID(_))) {
// There are no signatures to insert. We're done.
return Ok(());
}
let cert = cert.clone().insert_packets(certifications)?;
if let Some(output) = output {
// And export it.
let path = output.path().map(Clone::clone);
let mut message = output.create_pgp_safe(
&sq,
binary,
sequoia_openpgp::armor::Kind::PublicKey,
)?;
cert.serialize(&mut message)?;
message.finalize()?;
if ! local {
if let Some(path) = path {
sq.hint(format_args!(
"Updated certificate written to {}. \
To make the update effective, it has to be published \
so that others can find it, for example using:",
path.display()))
.sq().arg("network").arg("keyserver").arg("publish")
.arg_value("--file", path.display())
.done();
} else {
sq.hint(format_args!(
"To make the update effective, it has to be published \
so that others can find it."));
}
}
} else {
// Import it.
let cert_store = sq.cert_store_or_else()?;
let fipr = cert.fingerprint();
if let Err(err) = cert_store.update(Arc::new(cert.into())) {
wprintln!("Error importing updated cert: {}", err);
return Err(err);
} else if ! local {
sq.hint(format_args!(
"Imported updated cert into the cert store. \
To make the update effective, it has to be published \
so that others can find it, for example using:"))
.sq().arg("network").arg("keyserver").arg("publish")
.arg_value("--cert", fipr)
.done();
}
}
Ok(())
}

View File

@ -443,7 +443,7 @@ fn sq_pki_link_update_detection() -> Result<()> {
// Retract it. There is nothing to retract (but this doesn't fail).
let output = sq_retract(&sq, &alice_fpr, &[]);
assert!(output.2.contains("You never linked"),
assert!(output.2.contains("You never certified"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, true);
@ -454,44 +454,44 @@ fn sq_pki_link_update_detection() -> Result<()> {
// As no parameters changed, this should succeeded, but no
// certification should be written.
let output = sq_link(&sq, &alice_fpr, &[], &["--all"], true);
assert!(output.2.contains("Link parameters are unchanged, no update needed"),
assert!(output.2.contains("Certification parameters are unchanged"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, true);
// Make Alice a CA.
let output = sq_link(&sq, &alice_fpr, &[],
&["--ca", "*", "--all"], true);
assert!(output.2.contains("was already linked"),
assert!(output.2.contains("was previously"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, false);
let output = sq_link(&sq, &alice_fpr, &[],
&["--ca", "*", "--all"], true);
assert!(output.2.contains("Link parameters are unchanged, no update needed"),
assert!(output.2.contains("Certification parameters are unchanged"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, true);
// Make her a partially trusted CA.
let output = sq_link(&sq, &alice_fpr, &[],
&["--amount", "30", "--all"], true);
assert!(output.2.contains("was already linked"),
assert!(output.2.contains("was previously"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, false);
let output = sq_link(&sq, &alice_fpr, &[],
&["--amount", "30", "--all"], true);
assert!(output.2.contains("Link parameters are unchanged, no update needed"),
assert!(output.2.contains("Certification parameters are unchanged"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, true);
// Retract the link.
let output = sq_retract(&sq, &alice_fpr, &[]);
assert!(output.2.contains("was linked at"),
assert!(output.2.contains("was previously"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, false);
let output = sq_retract(&sq, &alice_fpr, &[]);
assert!(output.2.contains("Link parameters are unchanged, no update needed"),
assert!(output.2.contains("Certification parameters are unchanged"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, true);
@ -505,31 +505,31 @@ fn sq_pki_link_update_detection() -> Result<()> {
let output = sq_link(&sq, &alice_fpr, &[],
&["--depth", "10", "--amount", "10", "--all"], true);
assert!(output.2.contains("Link parameters are unchanged, no update needed"),
assert!(output.2.contains("Certification parameters are unchanged"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, true);
// Use a notation.
let output = sq_link(&sq, &alice_fpr, &[],
&["--notation", "foo", "10", "--all"], true);
assert!(output.2.contains("was already linked"),
assert!(output.2.contains("was previously"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, false);
let output = sq_link(&sq, &alice_fpr, &[],
&["--notation", "foo", "10", "--all"], true);
assert!(output.2.contains("Link parameters are unchanged, no update needed"),
assert!(output.2.contains("Certification parameters are unchanged"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, true);
// The default link again.
let output = sq_link(&sq, &alice_fpr, &[], &["--all"], true);
assert!(output.2.contains("was already linked"),
assert!(output.2.contains("was previously"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, false);
let output = sq_link(&sq, &alice_fpr, &[], &["--all"], true);
assert!(output.2.contains("Link parameters are unchanged, no update needed"),
assert!(output.2.contains("Certification parameters are unchanged"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, true);
@ -577,7 +577,7 @@ fn sq_pki_link_add_temporary() -> Result<()> {
sq_verify(&sq, None, &[], &[], &alice_sig_file, 0, 1);
let output = sq_link(&sq, &alice_fpr, &[], &["--temporary", "--all"], true);
assert!(output.2.contains("Linking "),
assert!(output.2.contains("Certifying "),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, false);
@ -598,7 +598,7 @@ fn sq_pki_link_add_temporary() -> Result<()> {
// Now mark it as fully trusted. It should be trusted now, in 6
// days and in 8 days.
let output = sq_link(&sq, &alice_fpr, &[], &["--all"], true);
assert!(output.2.contains("was already linked"),
assert!(output.2.contains("was previously"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
eprintln!("{:?}", output);
let bytes = compare(bytes, &alice_cert_pgp, false);