Add an option to sq link add to temporarily accept a binding

- Add an option to `sq link add`, `--temporary`, to temporarily
    accept a binding.

  - This creates a fully trusted certification that expires after a
    week, and a second certification that is one second older, which
    doesn't expire, but is only partially trusted (trust amount = 40)
    so that the user remembers this decision.
This commit is contained in:
Neal H. Walfield 2023-04-05 17:16:37 +02:00
parent 96a65b4b97
commit 4ae448cef8
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
3 changed files with 226 additions and 89 deletions

View File

@ -1,5 +1,5 @@
use std::borrow::Cow;
use std::time::SystemTime;
use std::time::{Duration, SystemTime};
use anyhow::Context;
@ -445,30 +445,6 @@ pub fn add(mut config: Config, c: link::AddCommand)
// Creation time.
builder = builder.set_signature_creation_time(config.time)?;
match (expires, expires_in) {
(None, None) =>
// Default expiration: never.
(),
(Some(t), None) if t == "never" =>
// The default is no expiration; there is nothing to do.
(),
(Some(t), None) => {
let expiration = SystemTime::from(
crate::parse_iso8601(
&t, chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())?);
let validity = expiration.duration_since(config.time)?;
builder = builder.set_signature_validity_period(validity)?;
},
(None, Some(d)) if d == "never" =>
// The default is no expiration; there is nothing to do.
(),
(None, Some(d)) => {
let d = parse_duration(&d)?;
builder = builder.set_signature_validity_period(d)?;
},
(Some(_), Some(_)) => unreachable!("conflicting args"),
}
let notations = parse_notations(c.notation)?;
for (critical, n) in notations {
builder = builder.add_notation(
@ -478,6 +454,45 @@ pub fn add(mut config: Config, c: link::AddCommand)
critical)?;
};
let builders: Vec<SignatureBuilder> = 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(
config.time - Duration::new(1, 0))?;
partial = partial.set_trust_signature(trust_depth, 40)?;
builder = builder.set_signature_validity_period(
Duration::new(7 * 24 * 60 * 60, 0))?;
vec![ builder, partial ]
} else if let Some(t) = expires {
if t == "never" {
// The default is no expiration; there is nothing to do.
} else {
let expiration = SystemTime::from(
crate::parse_iso8601(
&t, chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())?);
let validity = expiration.duration_since(config.time)?;
builder = builder.set_signature_validity_period(validity)?;
}
vec![ builder ]
} else if let Some(d) = expires_in {
if d == "never" {
// The default is no expiration; there is nothing to do.
} else {
let d = parse_duration(&d)?;
builder = builder.set_signature_validity_period(d)?;
}
vec![ builder ]
} else {
// The default is no expiration; there is nothing to do.
vec![ builder ]
};
// Sign it.
let signers = get_certification_keys(
&[trust_root], &config.policy, None, Some(config.time), None)
@ -535,11 +550,14 @@ pub fn add(mut config: Config, c: link::AddCommand)
let changed = diff_link(
&active_certification,
&builder, config.time);
&builders[0], config.time);
if ! changed && config.force {
eprintln!(" Link parameters are unchanged, but \
updating anyway as \"--force\" was specified.");
} else if c.temporary {
eprintln!(" Creating a temporary link, \
which expires in a week.");
} else if ! changed {
eprintln!(" Link parameters are unchanged, no update \
needed (specify \"--force\" to update anyway).");
@ -556,16 +574,25 @@ pub fn add(mut config: Config, c: link::AddCommand)
eprintln!("Linking {} and {:?}.",
cert.fingerprint(), userid_str());
let sig = builder.clone().sign_userid_binding(
&mut signer,
cert.primary_key().key(),
&userid)
.with_context(|| {
format!("Creating certification for {:?}", 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>>>()?;
eprintln!();
Ok(vec![ Packet::from(userid.clone()), Packet::from(sig) ])
let mut packets = vec![ Packet::from(userid.clone()) ];
packets.append(&mut sigs);
Ok(packets)
})
.collect::<Result<Vec<Vec<Packet>>>>()?
.into_iter()

View File

@ -131,7 +131,8 @@ $ sq link add --ca --amount 60 0123456789ABCDEF
$ sq link retract 0123456789ABCDEF
",
)]
#[clap(group(ArgGroup::new("expiration-group").args(&["expires", "expires_in"])))]
#[clap(group(ArgGroup::new("expiration-group")
.args(&["expires", "expires_in", "temporary"])))]
pub struct AddCommand {
#[clap(
short = 'd',
@ -203,6 +204,19 @@ pub struct AddCommand {
being human readable."
)]
pub notation: Vec<String>,
#[clap(
long = "temporary",
conflicts_with_all = &[ "amount" ],
help = "Temporarily accepts the binding",
long_help =
"Temporarily accepts the binding. Creates a fully
trust link between a certificate and one or more
User IDs for a week. After that, the link is
automatically downgraded to a partially trusted link
(trust = 40).",
)]
pub temporary: bool,
#[clap(
long = "expires",
value_name = "TIME",

View File

@ -29,10 +29,15 @@ static TIME: OnceCell<Mutex<chrono::DateTime<chrono::Utc>>> = OnceCell::new();
fn tick() -> String {
let t = TIME.get_or_init(|| Mutex::new(chrono::Utc::now()));
let mut t = t.lock().unwrap();
*t = *t + chrono::Duration::seconds(1);
*t = *t + chrono::Duration::seconds(10);
t.format("%Y-%m-%dT%H:%M:%SZ").to_string()
}
// Returns the "current" time.
fn now() -> chrono::DateTime<chrono::Utc> {
*TIME.get_or_init(|| Mutex::new(chrono::Utc::now())).lock().unwrap()
}
// Imports a certificate.
fn sq_import(cert_store: &str, files: &[&str], stdin: Option<&str>)
{
@ -75,6 +80,7 @@ fn sq_gen_key(cert_store: Option<&str>, userids: &[&str], file: &str) -> Cert
// Verifies a signed message.
fn sq_verify(cert_store: Option<&str>,
time: Option<chrono::DateTime<chrono::Utc>>,
trust_roots: &[&str],
signer_files: &[&str],
msg_pgp: &str,
@ -89,7 +95,12 @@ fn sq_verify(cert_store: Option<&str>,
for trust_root in trust_roots {
cmd.args(&["--trust-root", trust_root]);
}
cmd.args(["verify", "--time", &tick()]);
let time = if let Some(time) = time {
time.format("%Y-%m-%dT%H:%M:%SZ").to_string()
} else {
tick()
};
cmd.args(["verify", "--time", &time]);
for signer_file in signer_files {
cmd.args(&["--signer-file", signer_file]);
}
@ -272,7 +283,7 @@ fn sq_link_add_retract() -> Result<()> {
// None of the certificates can be authenticated so verifying the
// messages should fail.
for data in data.iter() {
sq_verify(Some(&certd), &[], &[], &data.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &data.sig_file, 0, 1);
}
// Have Alice certify Bob as a trusted introducer and have Bob
@ -288,58 +299,58 @@ fn sq_link_add_retract() -> Result<()> {
// signatures Alice as the trust root. And Bob's and Carols' with
// Bob as the trust root.
sq_verify(Some(&certd), &[&alice_fpr], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), &[&alice_fpr], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), &[&alice_fpr], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), &[&alice_fpr], &[], &dave.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[&alice_fpr], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[&alice_fpr], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[&alice_fpr], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[&alice_fpr], &[], &dave.sig_file, 0, 1);
sq_verify(Some(&certd), &[&bob_fpr], &[], &alice.sig_file, 0, 1);
sq_verify(Some(&certd), &[&bob_fpr], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), &[&bob_fpr], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), &[&bob_fpr], &[], &dave.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[&bob_fpr], &[], &alice.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[&bob_fpr], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[&bob_fpr], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[&bob_fpr], &[], &dave.sig_file, 0, 1);
sq_verify(Some(&certd), &[&carol_fpr], &[], &alice.sig_file, 0, 1);
sq_verify(Some(&certd), &[&carol_fpr], &[], &bob.sig_file, 0, 1);
sq_verify(Some(&certd), &[&carol_fpr], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), &[&carol_fpr], &[], &dave.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[&carol_fpr], &[], &alice.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[&carol_fpr], &[], &bob.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[&carol_fpr], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[&carol_fpr], &[], &dave.sig_file, 0, 1);
sq_verify(Some(&certd), &[&dave_fpr], &[], &alice.sig_file, 0, 1);
sq_verify(Some(&certd), &[&dave_fpr], &[], &bob.sig_file, 0, 1);
sq_verify(Some(&certd), &[&dave_fpr], &[], &carol.sig_file, 0, 1);
sq_verify(Some(&certd), &[&dave_fpr], &[], &dave.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[&dave_fpr], &[], &alice.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[&dave_fpr], &[], &bob.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[&dave_fpr], &[], &carol.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[&dave_fpr], &[], &dave.sig_file, 1, 0);
// Let's accept Alice, but not (yet) as a trusted introducer. We
// should now be able to verify Alice's signature, but not Bob's.
sq_link(&certd, &alice_fpr, &[ &alice_userid ], &[], true);
sq_verify(Some(&certd), &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &bob.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &bob.sig_file, 0, 1);
// Accept Alice as a trusted introducer. We should be able to
// verify Alice, Bob, and Carol's signatures.
sq_link(&certd, &alice_fpr, &[ &alice_userid ], &["--ca", "*"], true);
sq_verify(Some(&certd), &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &dave.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &dave.sig_file, 0, 1);
// Retract the acceptance for Alice. If we don't specify a trust
// root, none of the signatures should verify.
sq_retract(&certd, &alice_fpr, &[ &alice_userid ]);
for data in data.iter() {
sq_verify(Some(&certd), &[], &[], &data.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &data.sig_file, 0, 1);
}
// Accept Alice as a trusted introducer again. We should be able
// to verify Alice, Bob, and Carol's signatures.
sq_link(&certd, &alice_fpr, &[ &alice_userid ], &["--ca", "*"], true);
sq_verify(Some(&certd), &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &dave.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &dave.sig_file, 0, 1);
// Have Bob certify Dave. Now Dave's signature should also
// verify.
@ -347,29 +358,29 @@ fn sq_link_add_retract() -> Result<()> {
&dave.cert.fingerprint().to_string(), dave_userid,
None, None);
sq_verify(Some(&certd), &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &dave.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &dave.sig_file, 1, 0);
// Change Alice's acceptance to just be a normal certification.
// We should only be able to verify her signature.
sq_link(&certd, &alice_fpr, &[ &alice_userid ], &[], true);
sq_verify(Some(&certd), &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &bob.sig_file, 0, 1);
sq_verify(Some(&certd), &[], &[], &carol.sig_file, 0, 1);
sq_verify(Some(&certd), &[], &[], &dave.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &bob.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &carol.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &dave.sig_file, 0, 1);
// Change Alice's acceptance to be a ca, but only for example.org,
// i.e., not for Dave.
sq_link(&certd, &alice_fpr, &[ &alice_userid ], &["--ca", "example.org"],
true);
sq_verify(Some(&certd), &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), &[], &[], &dave.sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &alice.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &bob.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &carol.sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &dave.sig_file, 0, 1);
@ -404,37 +415,37 @@ fn sq_link_add_retract() -> Result<()> {
// If we don't use --petname, than a self-signed User ID must
// exist.
sq_link(&certd, &ed_fpr, &[ "--userid", "bob@example.com" ], &[], false);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 0, 1);
sq_link(&certd, &ed_fpr, &[ "--email", "bob@example.com" ], &[], false);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 0, 1);
sq_link(&certd, &ed_fpr, &[ "bob@example.com" ], &[], false);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 0, 1);
// We should only create links if all the supplied User IDs are
// valid.
sq_link(&certd, &ed_fpr, &[
"--userid", "ed@some.org", "--userid", "bob@example.com"
], &[], false);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 0, 1);
sq_link(&certd, &ed_fpr, &[
"--userid", "ed@some.org", "--email", "bob@example.com"
], &[], false);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 0, 1);
sq_link(&certd, &ed_fpr, &[
"--userid", "ed@some.org", "bob@example.com"
], &[], false);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 0, 1);
// Pass an email address to --userid. This shouldn't match
// either.
sq_link(&certd, &ed_fpr, &[
"--userid", "ed@other.org"
], &[], false);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 0, 1);
// Link all User IDs individually.
sq_link(&certd, &ed_fpr, &[
@ -442,21 +453,21 @@ fn sq_link_add_retract() -> Result<()> {
"--email", "ed@example.org",
"--userid", "ed@some.org",
], &[], true);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 1, 0);
// Retract the links one at a time.
sq_retract(&certd, &ed_fpr, &[ "ed@other.org" ]);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 1, 0);
sq_retract(&certd, &ed_fpr, &[ "Ed <ed@example.org>" ]);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 1, 0);
sq_retract(&certd, &ed_fpr, &[ "Eddie <ed@example.org>" ]);
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 1, 0);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 1, 0);
sq_retract(&certd, &ed_fpr, &[ "ed@some.org" ]);
// Now the certificate should no longer be authenticated.
sq_verify(Some(&certd), &[], &[], &ed_sig_file, 0, 1);
sq_verify(Some(&certd), None, &[], &[], &ed_sig_file, 0, 1);
Ok(())
}
@ -587,3 +598,88 @@ fn sq_link_update_detection() -> Result<()> {
let _ = bytes;
Ok(())
}
// Check that sq link add --temporary works.
#[test]
fn sq_link_add_temporary() -> Result<()> {
let dir = TempDir::new()?;
let certd = dir.path().join("cert.d").display().to_string();
std::fs::create_dir(&certd).expect("mkdir works");
let alice_pgp = dir.path().join("alice.pgp").display().to_string();
let alice_userid = "<alice@example.org>";
let alice = sq_gen_key(Some(&certd), &[ alice_userid ], &alice_pgp);
let alice_fpr = alice.fingerprint().to_string();
let alice_cert_pgp = dir.path().join("cert.d")
.join(&alice_fpr[0..2].to_ascii_lowercase())
.join(&alice_fpr[2..].to_ascii_lowercase());
let alice_sig_file = dir.path().join("alice.sig").display().to_string();
Command::cargo_bin("sq")
.unwrap()
.arg("--no-cert-store")
.arg("sign")
.args(["--signer-file", &alice_pgp])
.args(["--output", &alice_sig_file])
.args(["--time", &tick()])
.arg(&artifact("messages/a-cypherpunks-manifesto.txt"))
.assert()
.success();
// Reads and returns file. Asserts that old and the new contexts
// are the same (or not).
let compare = |old: Vec<u8>, file: &Path, same: bool| -> Vec<u8> {
let new = std::fs::read(file).unwrap();
if same {
assert_eq!(old, new, "file unexpectedly changed");
} else {
assert_ne!(old, new, "file unexpectedly stayed the same");
}
new
};
let bytes = std::fs::read(&alice_cert_pgp).unwrap();
sq_verify(Some(&certd), None, &[], &[], &alice_sig_file, 0, 1);
let output = sq_link(&certd, &alice_fpr, &[], &["--temporary", "--all"], true);
assert!(output.2.contains("Linking "),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
let bytes = compare(bytes, &alice_cert_pgp, false);
// Now it is fully trusted.
sq_verify(Some(&certd), None, &[], &[], &alice_sig_file, 1, 0);
// In 6 days, too.
sq_verify(Some(&certd),
Some(now() + chrono::Duration::seconds(6 * 24 * 60 * 60)),
&[], &[], &alice_sig_file, 1, 0);
// But in 8 days it will only be partially trusted.
sq_verify(Some(&certd),
Some(now() + chrono::Duration::seconds(8 * 24 * 60 * 60)),
&[], &[], &alice_sig_file, 0, 1);
// Now mark it as fully trusted. It should be trusted now, in 6
// days and in 8 days.
let output = sq_link(&certd, &alice_fpr, &[], &["--all"], true);
assert!(output.2.contains("was already linked"),
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
eprintln!("{:?}", output);
let bytes = compare(bytes, &alice_cert_pgp, false);
sq_verify(Some(&certd), None, &[], &[], &alice_sig_file, 1, 0);
sq_verify(Some(&certd),
Some(now() + chrono::Duration::seconds(6 * 24 * 60 * 60)),
&[], &[], &alice_sig_file, 1, 0);
sq_verify(Some(&certd),
Some(now() + chrono::Duration::seconds(8 * 24 * 60 * 60)),
&[], &[], &alice_sig_file, 1, 0);
let _bytes = bytes;
Ok(())
}