660d1eb39d
- `sq sign --signature-file` now takes a value specifying where the signature should be written to. It conflicts with `--output`. This aligns `sq sign` with `sq verify`. - Fixes #444.
1121 lines
38 KiB
Rust
1121 lines
38 KiB
Rust
use std::fs::{self, File};
|
||
use std::io;
|
||
|
||
use tempfile::TempDir;
|
||
|
||
use sequoia_openpgp as openpgp;
|
||
use openpgp::Fingerprint;
|
||
use openpgp::KeyHandle;
|
||
use openpgp::Result;
|
||
use openpgp::{Packet, PacketPile, Cert};
|
||
use openpgp::cert::CertBuilder;
|
||
use openpgp::crypto::KeyPair;
|
||
use openpgp::packet::key::SecretKeyMaterial;
|
||
use openpgp::packet::signature::subpacket::NotationData;
|
||
use openpgp::packet::signature::subpacket::NotationDataFlags;
|
||
use openpgp::types::{CompressionAlgorithm, SignatureType};
|
||
use openpgp::parse::Parse;
|
||
use openpgp::policy::StandardPolicy;
|
||
use openpgp::serialize::stream::{Message, Signer, Compressor, LiteralWriter};
|
||
use openpgp::serialize::Serialize;
|
||
|
||
use super::common::*;
|
||
|
||
const P: &StandardPolicy = &StandardPolicy::new();
|
||
|
||
#[test]
|
||
fn sq_sign() {
|
||
let sq = Sq::new();
|
||
let tmp_dir = TempDir::new().unwrap();
|
||
let sig = tmp_dir.path().join("sig0");
|
||
|
||
// Sign message.
|
||
sq.sign(artifact("keys/dennis-simon-anton-private.pgp"), None,
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"), sig.as_path());
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 3);
|
||
if let Packet::OnePassSig(ref ops) = packets[0] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::Literal(_) = packets[1] {
|
||
// Do nothing.
|
||
} else {
|
||
panic!("expected literal");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[2] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n"));
|
||
|
||
// Verify signed message.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/dennis-simon-anton.pgp")],
|
||
Verify::Message, &sig, None);
|
||
}
|
||
|
||
#[test]
|
||
fn sq_sign_with_notations() {
|
||
let sq = Sq::new();
|
||
let tmp_dir = TempDir::new().unwrap();
|
||
let sig = tmp_dir.path().join("sig0");
|
||
|
||
// Sign message.
|
||
sq.sign_args(&["--signature-notation", "foo", "bar",
|
||
"--signature-notation", "!foo", "xyzzy",
|
||
"--signature-notation", "hello@example.org", "1234567890"],
|
||
artifact("keys/dennis-simon-anton-private.pgp"), None,
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"), sig.as_path());
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 3);
|
||
if let Packet::OnePassSig(ref ops) = packets[0] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::Literal(_) = packets[1] {
|
||
// Do nothing.
|
||
} else {
|
||
panic!("expected literal");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[2] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
|
||
eprintln!("{:?}", sig);
|
||
|
||
let hr = NotationDataFlags::empty().set_human_readable();
|
||
let notations = &mut [
|
||
(NotationData::new("foo", "bar", hr.clone()), false),
|
||
(NotationData::new("foo", "xyzzy", hr.clone()), false),
|
||
(NotationData::new("hello@example.org", "1234567890", hr), false)
|
||
];
|
||
|
||
for n in sig.notation_data() {
|
||
if n.name() == "salt@notations.sequoia-pgp.org" {
|
||
continue;
|
||
}
|
||
|
||
for (m, found) in notations.iter_mut() {
|
||
if n == m {
|
||
assert!(!*found);
|
||
*found = true;
|
||
}
|
||
}
|
||
}
|
||
for (n, found) in notations.iter() {
|
||
assert!(found, "Missing: {:?}", n);
|
||
}
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n"));
|
||
|
||
// Verify signed message.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/dennis-simon-anton.pgp"),
|
||
"--known-notation", "foo"],
|
||
Verify::Message, &sig, None);
|
||
}
|
||
|
||
#[test]
|
||
fn sq_sign_append() {
|
||
let sq = Sq::new();
|
||
let tmp_dir = TempDir::new().unwrap();
|
||
let sig0 = tmp_dir.path().join("sig0");
|
||
|
||
// Sign message.
|
||
sq.sign(artifact("keys/dennis-simon-anton-private.pgp"), None,
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"), sig0.as_path());
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig0).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 3);
|
||
if let Packet::OnePassSig(ref ops) = packets[0] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::Literal(_) = packets[1] {
|
||
// Do nothing.
|
||
} else {
|
||
panic!("expected literal");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[2] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig0).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n"));
|
||
|
||
// Verify signed message.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/dennis-simon-anton.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
|
||
// Now add a second signature with --append.
|
||
let sig1 = tmp_dir.path().join("sig1");
|
||
sq.sign_args(
|
||
&["--append"],
|
||
artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp"),
|
||
None, sig0.as_path(), sig1.as_path());
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig1).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 5);
|
||
if let Packet::OnePassSig(ref ops) = packets[0] {
|
||
assert!(! ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::OnePassSig(ref ops) = packets[1] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::Literal(_) = packets[2] {
|
||
// Do nothing.
|
||
} else {
|
||
panic!("expected literal");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[3] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 0);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[4] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 0);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig1).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n"));
|
||
|
||
// Verify both signatures of the signed message.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/dennis-simon-anton.pgp")],
|
||
Verify::Message, &sig1, None);
|
||
sq.verify(&["--signer-file", &artifact_s("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")],
|
||
Verify::Message, &sig1, None);
|
||
}
|
||
|
||
#[test]
|
||
#[allow(unreachable_code)]
|
||
fn sq_sign_append_on_compress_then_sign() {
|
||
let sq = Sq::new();
|
||
let tmp_dir = TempDir::new().unwrap();
|
||
let sig0 = tmp_dir.path().join("sig0");
|
||
|
||
// This is quite an odd scheme, so we need to create such a
|
||
// message by foot.
|
||
let tsk = Cert::from_file(&artifact("keys/dennis-simon-anton-private.pgp"))
|
||
.unwrap();
|
||
let key = tsk.keys().with_policy(P, None).for_signing().next().unwrap().key();
|
||
let sec = match key.optional_secret() {
|
||
Some(SecretKeyMaterial::Unencrypted(ref u)) => u.clone(),
|
||
_ => unreachable!(),
|
||
};
|
||
let keypair = KeyPair::new(key.clone(), sec).unwrap();
|
||
let message = Message::new(File::create(&sig0).unwrap());
|
||
let signer = Signer::new(message, keypair)
|
||
.creation_time(sq.now())
|
||
.build().unwrap();
|
||
let compressor = Compressor::new(signer)
|
||
.algo(CompressionAlgorithm::Uncompressed)
|
||
.build().unwrap();
|
||
let mut literal = LiteralWriter::new(compressor).build()
|
||
.unwrap();
|
||
io::copy(
|
||
&mut File::open(&artifact("messages/a-cypherpunks-manifesto.txt")).unwrap(),
|
||
&mut literal)
|
||
.unwrap();
|
||
literal.finalize()
|
||
.unwrap();
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig0).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 3);
|
||
if let Packet::OnePassSig(ref ops) = packets[0] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::CompressedData(_) = packets[1] {
|
||
// Do nothing.
|
||
} else {
|
||
panic!("expected compressed data");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[2] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
// Verify signed message.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/dennis-simon-anton.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
|
||
// Now add a second signature with --append.
|
||
let sig1 = tmp_dir.path().join("sig1");
|
||
sq.command()
|
||
.arg("sign")
|
||
.arg("--message")
|
||
.arg("--append")
|
||
.arg("--signer-file").arg(artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp"))
|
||
.arg("--output")
|
||
.arg(&*sig1.to_string_lossy())
|
||
.arg(&*sig0.to_string_lossy())
|
||
.assert()
|
||
.failure(); // XXX: Currently, this is not implemented.
|
||
|
||
// XXX: Currently, this is not implemented in sq.
|
||
return;
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig1).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 5);
|
||
if let Packet::OnePassSig(ref ops) = packets[0] {
|
||
assert!(! ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::OnePassSig(ref ops) = packets[1] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::CompressedData(_) = packets[2] {
|
||
// Do nothing.
|
||
} else {
|
||
panic!("expected compressed data");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[3] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 0);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[4] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 0);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig1).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n"));
|
||
|
||
// Verify both signatures of the signed message.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/dennis-simon-anton.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
sq.verify(&["--signer-file", &artifact_s("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
}
|
||
|
||
#[test]
|
||
fn sq_sign_detached() {
|
||
let sq = Sq::new();
|
||
let tmp_dir = TempDir::new().unwrap();
|
||
let sig = tmp_dir.path().join("sig0");
|
||
|
||
// Sign detached.
|
||
sq.sign_detached(&[],
|
||
artifact("keys/dennis-simon-anton-private.pgp"),
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"),
|
||
sig.as_path());
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 1);
|
||
if let Packet::Signature(ref sig) = packets[0] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP SIGNATURE-----\n\n"));
|
||
|
||
// Verify detached.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/dennis-simon-anton.pgp")],
|
||
Verify::SignatureFile(sig),
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"), None);
|
||
}
|
||
|
||
#[test]
|
||
fn sq_sign_detached_append() {
|
||
let sq = Sq::new();
|
||
let tmp_dir = TempDir::new().unwrap();
|
||
let sig = tmp_dir.path().join("sig0");
|
||
|
||
// Sign detached.
|
||
sq.sign_detached(&[],
|
||
artifact("keys/dennis-simon-anton-private.pgp"),
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"),
|
||
sig.as_path());
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 1);
|
||
if let Packet::Signature(ref sig) = packets[0] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP SIGNATURE-----\n\n"));
|
||
|
||
// Verify detached.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/dennis-simon-anton.pgp")],
|
||
Verify::SignatureFile(sig.clone()),
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"), None);
|
||
|
||
// Check that we don't blindly overwrite signatures.
|
||
sq.try_sign_detached(&[],
|
||
artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp"),
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"),
|
||
sig.as_path())
|
||
.unwrap_err();
|
||
|
||
// Now add a second signature with --append.
|
||
eprintln!("now");
|
||
sq.sign_detached(&["--append"],
|
||
artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp"),
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"),
|
||
sig.as_path());
|
||
eprintln!("done");
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 2);
|
||
if let Packet::Signature(ref sig) = packets[0] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[1] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP SIGNATURE-----\n\n"));
|
||
|
||
// Verify both detached signatures.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/dennis-simon-anton.pgp")],
|
||
Verify::SignatureFile(sig.clone()),
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"), None);
|
||
sq.verify(&["--signer-file", &artifact_s("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")],
|
||
Verify::SignatureFile(sig.clone()),
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"), None);
|
||
|
||
// Finally, check that we don't truncate the file if something
|
||
// goes wrong.
|
||
sq.command()
|
||
.arg("sign")
|
||
.arg("--signature-file")
|
||
.arg("--append")
|
||
.arg("--signer-file") // Not a private key => signing will fail.
|
||
.arg(&artifact("keys/erika-corinna-daniela-simone-antonia-nistp521.pgp"))
|
||
.args(["--output", &sig.to_string_lossy()])
|
||
.arg(&artifact("messages/a-cypherpunks-manifesto.txt"))
|
||
.assert()
|
||
.failure();
|
||
|
||
// Check that the content is still sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 2);
|
||
if let Packet::Signature(ref sig) = packets[0] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[1] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
}
|
||
|
||
// Notarizations ahead.
|
||
|
||
#[ignore]
|
||
#[test]
|
||
fn sq_sign_append_a_notarization() {
|
||
let sq = Sq::new();
|
||
let tmp_dir = TempDir::new().unwrap();
|
||
let sig0 = tmp_dir.path().join("sig0");
|
||
|
||
// Now add a third signature with --append to a notarized message.
|
||
sq.sign_detached(
|
||
&["--append"],
|
||
artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp"),
|
||
&artifact("messages/signed-1-notarized-by-ed25519.pgp"),
|
||
sig0.as_path());
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig0).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 7);
|
||
if let Packet::OnePassSig(ref ops) = packets[0] {
|
||
assert!(! ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::OnePassSig(ref ops) = packets[1] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::OnePassSig(ref ops) = packets[2] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::Literal(_) = packets[3] {
|
||
// Do nothing.
|
||
} else {
|
||
panic!("expected literal");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[4] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 0);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[5] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 1);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[6] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 1);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig0).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n"));
|
||
|
||
// Verify both notarizations and the signature.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/neal.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
sq.verify(&["--signer-file", &artifact_s("keys/emmelie-dorothea-dina-samantha-awina-ed25519.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
sq.verify(&["--signer-file", &artifact_s("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
}
|
||
|
||
#[ignore]
|
||
#[test]
|
||
fn sq_sign_notarize() {
|
||
let sq = Sq::new();
|
||
let tmp_dir = TempDir::new().unwrap();
|
||
let sig0 = tmp_dir.path().join("sig0");
|
||
|
||
// Now add a third signature with --append to a notarized message.
|
||
sq.sign_args(
|
||
&["--notarize"],
|
||
artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp"),
|
||
None,
|
||
&artifact("messages/signed-1.gpg"),
|
||
sig0.as_path());
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig0).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 5);
|
||
if let Packet::OnePassSig(ref ops) = packets[0] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::OnePassSig(ref ops) = packets[1] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::Literal(_) = packets[2] {
|
||
// Do nothing.
|
||
} else {
|
||
panic!("expected literal");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[3] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 0);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[4] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 1);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig0).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n"));
|
||
|
||
// Verify both notarizations and the signature.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/neal.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
sq.verify(&["--signer-file", &artifact_s("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
}
|
||
|
||
#[ignore]
|
||
#[test]
|
||
fn sq_sign_notarize_a_notarization() {
|
||
let sq = Sq::new();
|
||
let tmp_dir = TempDir::new().unwrap();
|
||
let sig0 = tmp_dir.path().join("sig0");
|
||
|
||
// Now add a third signature with --append to a notarized message.
|
||
sq.sign_args(
|
||
&["--notarize"],
|
||
artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp"),
|
||
None,
|
||
&artifact("messages/signed-1-notarized-by-ed25519.pgp"),
|
||
sig0.as_path());
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&sig0).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 7);
|
||
if let Packet::OnePassSig(ref ops) = packets[0] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::OnePassSig(ref ops) = packets[1] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::OnePassSig(ref ops) = packets[2] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::Literal(_) = packets[3] {
|
||
// Do nothing.
|
||
} else {
|
||
panic!("expected literal");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[4] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 0);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[5] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 1);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[6] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
assert_eq!(sig.level(), 2);
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&sig0).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n"));
|
||
|
||
// Verify both notarizations and the signature.
|
||
sq.verify(&["--signer-file", &artifact_s("keys/neal.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
sq.verify(&["--signer-file", &artifact_s("keys/emmelie-dorothea-dina-samantha-awina-ed25519.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
sq.verify(&["--signer-file", &artifact_s("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")],
|
||
Verify::Message, &sig0, None);
|
||
}
|
||
|
||
#[test]
|
||
fn sq_multiple_signers() -> Result<()> {
|
||
let sq = Sq::new();
|
||
let tmp = TempDir::new()?;
|
||
|
||
let gen = |userid: &str| {
|
||
CertBuilder::new()
|
||
.add_signing_subkey()
|
||
.add_userid(userid)
|
||
.generate().map(|(key, _rev)| key)
|
||
};
|
||
|
||
let alice = gen("<alice@some.org>")?;
|
||
let alice_pgp = tmp.path().join("alice.pgp");
|
||
let mut file = File::create(&alice_pgp)?;
|
||
alice.as_tsk().serialize(&mut file)?;
|
||
|
||
let bob = gen("<bob@some.org>")?;
|
||
let bob_pgp = tmp.path().join("bob.pgp");
|
||
let mut file = File::create(&bob_pgp)?;
|
||
bob.as_tsk().serialize(&mut file)?;
|
||
|
||
// Sign message.
|
||
let assertion = sq.command()
|
||
.args([
|
||
"sign",
|
||
"--signer-file", alice_pgp.to_str().unwrap(),
|
||
"--signer-file", &bob_pgp.to_str().unwrap(),
|
||
"--signature-file=-",
|
||
])
|
||
.write_stdin(&b"foo"[..])
|
||
.assert().try_success()?;
|
||
|
||
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout);
|
||
|
||
let pp = PacketPile::from_bytes(&*stdout)?;
|
||
|
||
assert_eq!(pp.children().count(), 2,
|
||
"expected two packets");
|
||
|
||
let mut sigs: Vec<Fingerprint> = pp.children().map(|p| {
|
||
if let &Packet::Signature(ref sig) = p {
|
||
if let Some(KeyHandle::Fingerprint(fpr))
|
||
= sig.get_issuers().into_iter().next()
|
||
{
|
||
fpr
|
||
} else {
|
||
panic!("No issuer fingerprint subpacket!");
|
||
}
|
||
} else {
|
||
panic!("Expected a signature, got: {:?}", pp);
|
||
}
|
||
}).collect();
|
||
sigs.sort();
|
||
|
||
let alice_sig_fpr = alice.with_policy(P, None)?
|
||
.keys().for_signing().next().unwrap().fingerprint();
|
||
let bob_sig_fpr = bob.with_policy(P, None)?
|
||
.keys().for_signing().next().unwrap().fingerprint();
|
||
|
||
let mut expected = vec![
|
||
alice_sig_fpr,
|
||
bob_sig_fpr,
|
||
];
|
||
expected.sort();
|
||
|
||
assert_eq!(sigs, expected);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sq_sign_using_cert_store() -> Result<()> {
|
||
let sq = Sq::new();
|
||
let dir = TempDir::new()?;
|
||
|
||
let certd = dir.path().join("cert.d").display().to_string();
|
||
std::fs::create_dir(&certd).expect("mkdir works");
|
||
|
||
let msg_pgp = dir.path().join("msg.pgp");
|
||
|
||
// Generate a key.
|
||
let (alice, alice_pgp, _rev) =
|
||
sq.key_generate(&["--expiration", "never"], &["<alice@example.org>"]);
|
||
|
||
// Import it.
|
||
let mut cmd = sq.command();
|
||
cmd.args(["--cert-store", &certd,
|
||
"cert", "import"]);
|
||
cmd.arg(&alice_pgp);
|
||
sq.run(cmd, true);
|
||
|
||
// Sign a message.
|
||
sq.sign(&alice_pgp, None,
|
||
&artifact("messages/a-cypherpunks-manifesto.txt"),
|
||
msg_pgp.as_path());
|
||
|
||
// Check that the content is sane.
|
||
let packets: Vec<Packet> =
|
||
PacketPile::from_file(&msg_pgp).unwrap().into_children().collect();
|
||
assert_eq!(packets.len(), 3);
|
||
if let Packet::OnePassSig(ref ops) = packets[0] {
|
||
assert!(ops.last());
|
||
assert_eq!(ops.typ(), SignatureType::Binary);
|
||
} else {
|
||
panic!("expected one pass signature");
|
||
}
|
||
if let Packet::Literal(_) = packets[1] {
|
||
// Do nothing.
|
||
} else {
|
||
panic!("expected literal");
|
||
}
|
||
if let Packet::Signature(ref sig) = packets[2] {
|
||
assert_eq!(sig.typ(), SignatureType::Binary);
|
||
|
||
let alice_signer = alice.with_policy(P, None)?
|
||
.keys().for_signing().next().expect("have one");
|
||
assert_eq!(sig.get_issuers().into_iter().next(),
|
||
Some(KeyHandle::from(alice_signer.fingerprint())));
|
||
} else {
|
||
panic!("expected signature");
|
||
}
|
||
|
||
let content = fs::read(&msg_pgp).unwrap();
|
||
assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n"));
|
||
|
||
// Verify the signed message. First, we specify the certificate
|
||
// explicitly.
|
||
sq.verify(&["--signer-file", &alice_pgp.display().to_string()],
|
||
Verify::Message, &msg_pgp, None);
|
||
|
||
// Verify the signed message. Now, we don't specify the
|
||
// certificate or use a certificate store.
|
||
let result = sq.verify_maybe(&[], Verify::Message, &msg_pgp, None);
|
||
assert!(result.is_err());
|
||
let err = result.unwrap_err().to_string();
|
||
assert!(err.contains("missing certificate."), "{}", err);
|
||
assert!(err.contains("Error: Verification failed") , "{}", err);
|
||
|
||
// Now we use the certificate store.
|
||
let result = sq.verify_maybe(
|
||
&["--cert-store", &certd], Verify::Message, &msg_pgp, None);
|
||
assert!(result.is_err());
|
||
let err = result.unwrap_err().to_string();
|
||
|
||
// The default trust model says that certificates from the
|
||
// certificate store are not authenticated.
|
||
assert!(err.contains("the certificate can't be authenticated."),
|
||
"{}", err);
|
||
assert!(err.contains("Error: Verification failed"),
|
||
"{}", err);
|
||
|
||
// Now we use the certificate store *and* specify the certificate.
|
||
let mut cmd = sq.command();
|
||
cmd.arg("--cert-store").arg(&certd)
|
||
.arg("verify").arg("--message")
|
||
.arg("--signer").arg(&alice.fingerprint().to_string())
|
||
.arg(&msg_pgp);
|
||
let output = cmd.output().expect("success");
|
||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||
assert!(output.status.success(),
|
||
"stdout:\n{}\nstderr: {}", stdout, stderr);
|
||
|
||
// The default trust model says that certificates from the
|
||
// certificate store are not authenticated.
|
||
assert!(stderr.contains("Authenticated signature made by "),
|
||
"stdout:\n{}\nstderr: {}", stdout, stderr);
|
||
assert!(stderr.contains("1 authenticated signature."),
|
||
"stdout:\n{}\nstderr: {}", stdout, stderr);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// Verify signatures using the web of trust to authenticate the
|
||
// signers.
|
||
#[test]
|
||
fn sq_verify_wot() -> Result<()> {
|
||
let sq = Sq::new();
|
||
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 bob_pgp = dir.path().join("bob.pgp").display().to_string();
|
||
let carol_pgp = dir.path().join("carol.pgp").display().to_string();
|
||
let dave_pgp = dir.path().join("dave.pgp").display().to_string();
|
||
let msg_pgp = dir.path().join("msg.pgp").display().to_string();
|
||
|
||
// Generates a key.
|
||
//
|
||
// If cert_store is not `None`, then the resulting certificate is also
|
||
// imported.
|
||
let sq_gen_key = |sq: &Sq, userids: &[&str], file_out: &str|
|
||
-> Cert
|
||
{
|
||
let (cert, file, _) = sq.key_generate(&[], userids);
|
||
sq.cert_import(&file);
|
||
fs::rename(file, file_out).unwrap();
|
||
cert
|
||
};
|
||
|
||
// Verifies a signed message.
|
||
let sq_verify = |sq: &Sq,
|
||
trust_roots: &[&str],
|
||
signer_files: &[&str],
|
||
msg_pgp: &str|
|
||
{
|
||
let mut cmd = sq.command();
|
||
for trust_root in trust_roots {
|
||
cmd.args(&["--trust-root", trust_root]);
|
||
}
|
||
cmd.arg("verify").arg("--message");
|
||
for signer_file in signer_files {
|
||
cmd.args(&["--signer-file", signer_file]);
|
||
}
|
||
cmd.arg(msg_pgp);
|
||
let output = sq.run(cmd, None);
|
||
|
||
(output.status.clone(),
|
||
String::from_utf8_lossy(&output.stdout).to_string(),
|
||
String::from_utf8_lossy(&output.stderr).to_string())
|
||
};
|
||
|
||
// Certifies a binding.
|
||
//
|
||
// The certification is imported into the cert store.
|
||
let sq_certify = |sq: &Sq,
|
||
certifier: &str, cert: &str, userid: &str,
|
||
trust_amount: Option<usize>|
|
||
{
|
||
let mut extra_args = Vec::new();
|
||
let trust_amount_;
|
||
if let Some(trust_amount) = trust_amount {
|
||
extra_args.push("--amount");
|
||
trust_amount_ = format!("{}", trust_amount);
|
||
extra_args.push(&trust_amount_);
|
||
}
|
||
|
||
let certification = sq.scratch_file(Some(&format!(
|
||
"certification {} {} by {}", cert, userid, certifier)[..]));
|
||
|
||
let cert = if let Ok(kh) = cert.parse::<KeyHandle>() {
|
||
kh.into()
|
||
} else {
|
||
FileOrKeyHandle::FileOrStdin(cert.into())
|
||
};
|
||
|
||
sq.pki_vouch_add(&extra_args, certifier, cert, &[userid],
|
||
Some(certification.as_path()));
|
||
sq.cert_import(&certification);
|
||
};
|
||
|
||
let alice = sq_gen_key(&sq, &[ "<alice@example.org>" ], &alice_pgp);
|
||
let bob = sq_gen_key(&sq, &[ "<bob@example.org>" ], &bob_pgp);
|
||
let carol = sq_gen_key(&sq, &[ "<carol@example.org>" ], &carol_pgp);
|
||
let dave = sq_gen_key(&sq, &[ "<dave@example.org>" ], &dave_pgp);
|
||
|
||
let alice_fpr = alice.fingerprint().to_string();
|
||
let bob_fpr = bob.fingerprint().to_string();
|
||
let carol_fpr = carol.fingerprint().to_string();
|
||
let dave_fpr = dave.fingerprint().to_string();
|
||
|
||
// Sign a message.
|
||
sq.command()
|
||
.arg("sign")
|
||
.arg("--message")
|
||
.args(["--signer-file", &bob_pgp])
|
||
.args(["--signer-file", &carol_pgp])
|
||
.args(["--signer-file", &dave_pgp])
|
||
.args(["--output", &msg_pgp])
|
||
.arg(&artifact("messages/a-cypherpunks-manifesto.txt"))
|
||
.assert()
|
||
.success();
|
||
|
||
// When designating the signers using a file, the signers are
|
||
// fully trusted.
|
||
{
|
||
let output = sq_verify(&sq, &[], &[&bob_pgp], &msg_pgp);
|
||
assert!(output.0.success());
|
||
let output = sq_verify(&sq, &[], &[&carol_pgp], &msg_pgp);
|
||
assert!(output.0.success());
|
||
let output = sq_verify(&sq, &[], &[&dave_pgp], &msg_pgp);
|
||
assert!(output.0.success());
|
||
|
||
// Alice did not sign it so this should fail.
|
||
let output = sq_verify(&sq, &[], &[&alice_pgp], &msg_pgp);
|
||
assert!(! output.0.success());
|
||
|
||
// But, one authenticated signature is enough.
|
||
let output = sq_verify(&sq, &[], &[&alice_pgp, &bob_pgp], &msg_pgp);
|
||
assert!(output.0.success());
|
||
}
|
||
|
||
// When the signers' certificates are found in the cert store, and
|
||
// they can't be authenticated with the web of trust, the
|
||
// verification will fail.
|
||
{
|
||
let output = sq_verify(&sq, &[], &[], &msg_pgp);
|
||
assert!(! output.0.success(),
|
||
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
|
||
assert!(output.2.contains("the certificate can't be authenticated."),
|
||
"stdout:\n{}\nstderr:\n{}",
|
||
output.1, output.2);
|
||
|
||
// Specifying a trust root won't help if there is no path to a
|
||
// signer.
|
||
let output = sq_verify(&sq, &[&alice_fpr], &[], &msg_pgp);
|
||
assert!(! output.0.success(),
|
||
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
|
||
assert!(output.2.contains("the certificate can't be authenticated."),
|
||
"stdout:\n{}\nstderr:\n{}",
|
||
output.1, output.2);
|
||
}
|
||
|
||
// A trust root can certify itself
|
||
{
|
||
let output = sq_verify(&sq, &[&bob_fpr], &[], &msg_pgp);
|
||
assert!(output.0.success(),
|
||
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
|
||
assert!(output.2.contains("Authenticated signature made by "),
|
||
"stdout:\n{}\nstderr:\n{}",
|
||
output.1, output.2);
|
||
|
||
let output = sq_verify(
|
||
&sq, &[&alice_fpr, &bob_fpr], &[], &msg_pgp);
|
||
assert!(output.0.success(),
|
||
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
|
||
assert!(output.2.contains("Authenticated signature made by "),
|
||
"stdout:\n{}\nstderr:\n{}",
|
||
output.1, output.2);
|
||
}
|
||
|
||
// Have Alice partially certify Bob, and make Alice the trust
|
||
// root. The signature should still be bad.
|
||
{
|
||
sq_certify(&sq, &alice_pgp,
|
||
&bob.fingerprint().to_string(), "<bob@example.org>",
|
||
Some(90));
|
||
let output = sq_verify(&sq, &[&alice_fpr], &[], &msg_pgp);
|
||
assert!(! output.0.success(),
|
||
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
|
||
assert!(output.2.contains("the certificate can't be authenticated."),
|
||
"stdout:\n{}\nstderr:\n{}",
|
||
output.1, output.2);
|
||
}
|
||
|
||
// Have Alice also partially certify Carol, and make Alice the
|
||
// trust root. Bob and Carol combined don't (currently) make the
|
||
// signature good.
|
||
{
|
||
sq_certify(&sq, &alice_pgp,
|
||
&carol_fpr, "<carol@example.org>",
|
||
Some(60));
|
||
let output = sq_verify(&sq, &[&alice_fpr], &[], &msg_pgp);
|
||
assert!(! output.0.success(),
|
||
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
|
||
assert!(output.2.contains("the certificate can't be authenticated."),
|
||
"stdout:\n{}\nstderr:\n{}",
|
||
output.1, output.2);
|
||
assert!(output.2.contains("3 unauthenticated signatures"),
|
||
"stdout:\n{}\nstderr:\n{}",
|
||
output.1, output.2);
|
||
}
|
||
|
||
// Have Alice fully certify Dave, and make Alice the trust root.
|
||
// Now the signature will be considered verified.
|
||
{
|
||
sq_certify(&sq, &alice_pgp,
|
||
&dave_fpr, "<dave@example.org>",
|
||
None);
|
||
let output = sq_verify(&sq, &[&alice_fpr], &[], &msg_pgp);
|
||
assert!(output.0.success(),
|
||
"stdout:\n{}\nstderr:\n{}", output.1, output.2);
|
||
assert!(output.2.contains("Authenticated signature made by "),
|
||
"stdout:\n{}\nstderr:\n{}",
|
||
output.1, output.2);
|
||
assert!(output.2.contains("1 authenticated signature, 2 unauthenticated signatures"),
|
||
"stdout:\n{}\nstderr:\n{}",
|
||
output.1, output.2);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sq_sign_keyring() {
|
||
// Check that we can provide the secret key material via
|
||
// --keyring.
|
||
|
||
let sq = Sq::new();
|
||
|
||
let (_alice, alice_pgp, _alice_rev)
|
||
= sq.key_generate(&[], &["alice"]);
|
||
|
||
let mut alice_pub = alice_pgp.clone();
|
||
alice_pub.set_extension("pub");
|
||
|
||
sq.key_delete(&alice_pgp, Some(&*alice_pub));
|
||
|
||
// We pass the secret key material via --keyring. This should
|
||
// work.
|
||
let mut cmd = sq.command();
|
||
cmd.arg("--keyring").arg(&alice_pgp)
|
||
.arg("sign")
|
||
.arg("--message")
|
||
.arg("--signer-file").arg(&alice_pub);
|
||
|
||
sq.run(cmd, Some(true));
|
||
|
||
// If we don't pass the secret key material, this should fail.
|
||
let mut cmd = sq.command();
|
||
cmd.arg("sign")
|
||
.arg("--message")
|
||
.arg("--signer-file").arg(&alice_pub);
|
||
|
||
sq.run(cmd, Some(false));
|
||
}
|
||
|
||
/// Creates a text signature.
|
||
#[test]
|
||
fn sq_sign_mode_text() -> Result<()> {
|
||
let sq = Sq::new();
|
||
|
||
let (_alice, alice_pgp, _alice_rev)
|
||
= sq.key_generate(&[], &["alice"]);
|
||
|
||
let data_n = sq.scratch_dir().join("data_n");
|
||
std::fs::write(&data_n, "Hello\n")?;
|
||
|
||
let sig = sq.scratch_dir().join("sig");
|
||
sq.sign_detached(&["--mode=text"], alice_pgp.as_path(),
|
||
data_n.as_path(), sig.as_path());
|
||
|
||
let verify_args = ["--signer-file", &alice_pgp.display().to_string()];
|
||
sq.verify(&verify_args, Verify::SignatureFile(sig.clone()), &data_n, None);
|
||
|
||
let data_rn = sq.scratch_dir().join("data_rn");
|
||
std::fs::write(&data_rn, "Hello\r\n")?;
|
||
sq.verify(&verify_args, Verify::SignatureFile(sig.clone()), &data_rn, None);
|
||
|
||
Ok(())
|
||
}
|