use std::time::Duration; use openpgp::parse::Parse; use openpgp::types::ReasonForRevocation; use openpgp::types::RevocationStatus; use openpgp::types::SignatureType; use openpgp::Cert; use openpgp::Result; use sequoia_openpgp as openpgp; use super::common::artifact; use super::common::compare_notations; use super::common::FileOrKeyHandle; use super::common::Sq; use super::common::NULL_POLICY; use super::common::STANDARD_POLICY; #[test] fn sq_key_revoke() -> Result<()> { let sq = Sq::new(); let time = sq.now(); let (_cert, cert_path, _cert_rev) = sq.key_generate(&[], &["alice"]); let message = "message"; // revoke for various reasons, with or without notations added, or // with a revocation whose reference time is one hour after the // creation of the certificate for ((reason, reason_str, notations, revocation_time), cert_path) in [ ( ReasonForRevocation::KeyCompromised, "compromised", &[][..], None, ), ( ReasonForRevocation::KeyCompromised, "compromised", &[][..], Some(time + Duration::new(60 * 60, 0)), ), ( ReasonForRevocation::KeyCompromised, "compromised", &[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")][..], None, ), (ReasonForRevocation::KeyRetired, "retired", &[][..], None), ( ReasonForRevocation::KeyRetired, "retired", &[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")][..], None, ), (ReasonForRevocation::KeySuperseded, "superseded", &[][..], None), ( ReasonForRevocation::KeySuperseded, "superseded", &[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")][..], None, ), (ReasonForRevocation::Unspecified, "unspecified", &[][..], None), ( ReasonForRevocation::Unspecified, "unspecified", &[][..], Some(time + Duration::new(60 * 60, 0)), ), ( ReasonForRevocation::Unspecified, "unspecified", &[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")][..], None, ), ].into_iter().flat_map(|test| { [ // A normal key. (test, cert_path.clone()), // A key that uses SHA-1. (test, artifact("keys/only-sha1-priv.pgp")), ] }) { eprintln!("=========================="); eprintln!("reason: {}, message: {}, notations: {:?}, time: {:?}", reason, reason_str, notations, revocation_time); let cert = Cert::from_file(&cert_path).expect("valid cert"); for keystore in [false, true].into_iter() { eprintln!("--------------------------"); eprintln!("keystore: {}", keystore); let revocation = sq.scratch_file(Some(&format!( "revocation_{}_{}_{}.rev", reason_str, if notations.is_empty() { "no_notations" } else { "notations" }, if revocation_time.is_some() { "time" } else { "no_time" } )[..])); if keystore { // When using the keystore, we need to import the key. sq.key_import(&cert_path); } let updated = sq.key_revoke( if keystore { FileOrKeyHandle::from(cert.key_handle()) } else { FileOrKeyHandle::from(&cert_path) }, None, reason_str, message, None, notations, Some(revocation.as_path())); if let RevocationStatus::Revoked(sigs) = updated.revocation_status(STANDARD_POLICY, None) { assert_eq!(sigs.len(), 1); let sig = sigs.into_iter().next().unwrap(); // the issuer is the certificate owner assert_eq!( sig.get_issuers().into_iter().next(), Some(cert.key_handle()) ); let revoked_cert = cert.clone().insert_packets(sig.clone()).unwrap(); let status = revoked_cert .revocation_status( STANDARD_POLICY, revocation_time.map(Into::into)); println!("{:?}", sig); println!("{:?}", status); // Verify the revocation. assert!(matches!(status, RevocationStatus::Revoked(_))); // it is a key revocation assert_eq!(sig.typ(), SignatureType::KeyRevocation); // our reason for revocation and message matches assert_eq!( sig.reason_for_revocation(), Some((reason, message.as_bytes())) ); // the notations of the revocation match the ones // we passed in compare_notations(sig, notations)?; } else { panic!("Not revoked"); } } } Ok(()) } #[test] fn sq_key_revoke_thirdparty() -> Result<()> { let sq = Sq::new(); let time = sq.now(); let (_cert, cert_path, _cert_rev) = sq.key_generate(&[], &["alice"]); let (_thirdparty_cert, thirdparty_path, _cert_rev) = sq.key_generate(&[], &["bob "]); let message = "message"; // revoke for various reasons, with or without notations added, or with // a revocation whose reference time is one hour after the creation of the // certificate for ((reason, reason_str, notations, revocation_time), cert_path, thirdparty_path) in [ ( ReasonForRevocation::KeyCompromised, "compromised", &[][..], None, ), ( ReasonForRevocation::KeyCompromised, "compromised", &[][..], Some(time + Duration::new(60 * 60, 0)), ), ( ReasonForRevocation::KeyCompromised, "compromised", &[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")][..], None, ), (ReasonForRevocation::KeyRetired, "retired", &[][..], None), ( ReasonForRevocation::KeyRetired, "retired", &[][..], Some(time + Duration::new(60 * 60, 0)), ), ( ReasonForRevocation::KeyRetired, "retired", &[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")][..], None, ), (ReasonForRevocation::KeySuperseded, "superseded", &[][..], None), ( ReasonForRevocation::KeySuperseded, "superseded", &[][..], Some(time + Duration::new(60 * 60, 0)), ), ( ReasonForRevocation::KeySuperseded, "superseded", &[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")][..], None, ), (ReasonForRevocation::Unspecified, "unspecified", &[][..], None), ( ReasonForRevocation::Unspecified, "unspecified", &[][..], Some(time + Duration::new(60 * 60, 0)), ), ( ReasonForRevocation::Unspecified, "unspecified", &[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")][..], None, ), ].into_iter().flat_map(|test| { [ // Two valid keys. (test, cert_path.clone(), thirdparty_path.clone()), // The revokee is invalid (SHA-1). (test, artifact("keys/only-sha1-priv.pgp"), thirdparty_path.clone()), // The revoker is invalid (SHA-1). (test, cert_path.clone(), artifact("keys/only-sha1-priv.pgp")), ] }) { let cert = Cert::from_file(&cert_path).expect("valid cert"); let thirdparty_cert = Cert::from_file(&thirdparty_path).expect("valid cert"); let thirdparty_valid_cert = thirdparty_cert .with_policy(NULL_POLICY, Some(time.into()))?; let thirdparty_fingerprint = &thirdparty_valid_cert.clone().fingerprint(); for keystore in [false, true].into_iter() { let revocation = sq.scratch_file(Some(&format!( "revocation_{}_{}_{}.rev", reason_str, if ! notations.is_empty() { "no_notations" } else { "notations" }, if revocation_time.is_some() { "time" } else { "no_time" } )[..])); if keystore { // When using the keystore, we need to import the key. sq.cert_import(&cert_path); sq.key_import(&thirdparty_path); } let revocation_cert = sq.key_revoke( if keystore { FileOrKeyHandle::from(cert.key_handle()) } else { FileOrKeyHandle::from(&cert_path) }, if keystore { FileOrKeyHandle::from(thirdparty_cert.key_handle()) } else { FileOrKeyHandle::from(&thirdparty_path) }, reason_str, message, None, notations, Some(revocation.as_path())); assert!(! revocation_cert.is_tsk()); // evaluate revocation status let status = revocation_cert.revocation_status( STANDARD_POLICY, revocation_time.map(Into::into)); if let RevocationStatus::CouldBe(sigs) = status { // there is only one signature packet assert_eq!(sigs.len(), 1); let sig = sigs.into_iter().next().unwrap(); // it is a key revocation assert_eq!(sig.typ(), SignatureType::KeyRevocation); // the issuer is a thirdparty revoker assert_eq!( sig.get_issuers().into_iter().next().as_ref(), Some(&thirdparty_fingerprint.clone().into()) ); // the revocation can be verified if sig .clone() .verify_primary_key_revocation( &thirdparty_cert.primary_key(), &cert.primary_key(), ) .is_err() { panic!("revocation is not valid") } // our reason for revocation and message matches assert_eq!( sig.reason_for_revocation(), Some((reason, message.as_bytes())) ); // the notations of the revocation match the ones // we passed in compare_notations(sig, notations)?; } else { panic!("there are no signatures in {:?}", status); } } } Ok(()) }