sequoia-git 0.5.0

A tool for managing and enforcing a commit signing policy.
Documentation
use std::fs;
use std::time::Duration;
use std::time::SystemTime;

use sequoia_openpgp as openpgp;
use openpgp::Packet;
use openpgp::policy::StandardPolicy;

const P: &StandardPolicy = &StandardPolicy::new();

mod common;
use common::Environment;

#[test]
fn expired_certificate() -> anyhow::Result<()> {
    if ! Environment::check_for_faketime()? {
        // faketime tests are disabled.
        return Ok(());
    }

    // Consider:
    //
    // Alice is authorized to add commits at time t0.
    let t0 = SystemTime::now() - Duration::new(80 * 60, 0);
    //
    // At t1, she adds a commit (c1).
    let t1 = t0 + Duration::new(30 * 60, 0);
    // At t2, her certificate expires.
    //
    // Note: This corresponds to the current time, because
    // set_expiration_time doesn't let us override the signature
    // creation time.  See:
    //
    //   https://gitlab.com/sequoia-pgp/sequoia/-/issues/1154
    let t2 = t1 + Duration::new(50 * 60, 0);
    //
    // At t3, she adds a commit (c2) that updates her certificate.
    let t3 = t2 + Duration::new(70 * 60, 0);

    let future = t3 + Duration::new(110 * 60, 0);
    //
    // The policy in c1 is used to authenticate c2, but the
    // certificate in c1's policy is expired.  In this case, the
    // certificates from c2's policy should be used to update the
    // certificates in c1's policy.

    let mut e = Environment::at(t0.clone())?;
    let p = e.git_state();

    let (alice, alice_pgp) = e.gen(
        "alice",
        t0.clone(),
        t2.duration_since(t1.clone()).expect("valid"));
    let alice_fpr = &alice.fingerprint().to_string()[..];

    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice_pgp,
        "--sign-commit"
    ])?;
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Initial commit (@ t0).",
    ])?;
    let root = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;

    // Add another commit at t1.
    e.time(t1);

    fs::write(p.join("2"), "2.")?;
    e.git(&["add", "2"])?;
    e.git(&[
        "commit",
        "-m", "@ t1",
        &format!("-S{}", alice_fpr),
    ])?;
    let c2 = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root, &c2])?;

    // Try adding a commit after her certificate expires.  This should fail.
    e.time(t3);

    fs::write(p.join("3"), "3.")?;
    e.git(&["add", "3"])?;
    assert!(e.git(&[
        "commit",
        "-m", "@ t3.",
        &format!("-S{}", alice_fpr),
    ]).is_err());


    // Extend Alice's expiration.
    let (alice2, _) = {
        let vc = alice.with_policy(P, e.time.clone()).expect("valid cert");

        let mut primary_signer = alice.primary_key().key().clone()
            .parts_into_secret()?.into_keypair()?;

        // We really want to set the new signatures' creation time to t2

        let mut packets = Vec::new();
        for ka in vc.keys() {
            let mut subkey_signer = if ka.for_signing() {
                Some(ka.key().clone().parts_into_secret()?.into_keypair()?)
            } else {
                None
            };

            let sigs = ka.set_expiration_time(
                &mut primary_signer,
                if let Some(subkey_signer) = subkey_signer.as_mut() {
                    Some(subkey_signer)
                } else {
                    None
                },
                Some(future.clone()))
                .expect(&format!("can update expiration of {}", ka.key().fingerprint()));
            if ka.key().fingerprint() == alice.fingerprint() {
                packets.push(Packet::from(alice.primary_key().key().clone()));
            } else {
                packets.push(Packet::from(ka.key().clone().role_into_subordinate()));
            }
            packets.extend(sigs.into_iter().map(|sig| Packet::from(sig)));
        }

        alice.clone().insert_packets(packets).expect("can insert packets")
    };

    e.import(&alice2).expect("can import");

    // Now we should be able to create the commit.
    assert!(e.git(&[
        "commit",
        "-m", "@ t3.",
        &format!("-S{}", alice_fpr),
    ]).is_ok());
    let c3_1 = e.git_current_commit()?;

    // But we can't verify it, because the certificate in the policy
    // is expired!
    e.git(&["log"])?;
    assert!(e.sq_git(&["log", "--trust-root", &root, &c3_1]).is_err());

    // Reset to c2.
    e.git(&["reset", "--hard", &c2])?;
    assert_eq!(c2, e.git_current_commit()?);

    // Update the policy (by looking in the local certificate store).
    assert!(e.sq_git(&["policy", "sync", "--disable-keyservers"]).is_ok());

    e.git(&["add", "openpgp-policy.toml"])?;

    // Now we should be able to create the commit.
    assert!(e.git(&[
        "commit",
        "-m", "@ t3 (try two).",
        &format!("-S{}", alice_fpr),
    ]).is_ok());
    let c3_2 = e.git_current_commit()?;

    // And we can verify it, because the updated certificate is in the
    // new commit's policy file.
    e.git(&["log"])?;
    assert!(e.sq_git(&["log", "--trust-root", &root, &c3_2]).is_ok());

    Ok(())
}