mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
Moved from MailCraft's crates/assuan at a44a45013d1c. The code is unchanged apart from the crate name and doc wording that named the app. The S-expression reader, socket-file reader, limits and all 60 tests come with it.
330 lines
14 KiB
Rust
330 lines
14 KiB
Rust
use proptest::prelude::*;
|
|
|
|
use crate::fake::{Canned, FakeAgent};
|
|
use crate::{Digest, Error, ErrorKind, GpgAgent, HashAlgorithm, KeyStorage, Keygrip, PinentryHints, Protection, Sexp};
|
|
|
|
const GRIP: &str = "0123456789ABCDEF0123456789ABCDEF01234567";
|
|
const OTHER: &str = "89ABCDEF0123456789ABCDEF0123456789ABCDEF";
|
|
|
|
fn grip() -> Keygrip {
|
|
Keygrip::parse(GRIP).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn keygrips() {
|
|
assert_eq!(grip().to_string(), GRIP);
|
|
assert_eq!(Keygrip::parse(&GRIP.to_lowercase()), Some(grip()));
|
|
for bad in ["", "0123", &format!("{GRIP}0"), &GRIP.replace('A', "G"), "é123456789ABCDEF0123456789ABCDEF0123456"] {
|
|
assert_eq!(Keygrip::parse(bad), None, "{bad}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn only_sha2_digests_of_the_right_size() {
|
|
assert!(Digest::new(HashAlgorithm::Sha256, &[0; 32]).is_some());
|
|
assert!(Digest::new(HashAlgorithm::Sha384, &[0; 48]).is_some());
|
|
assert!(Digest::new(HashAlgorithm::Sha512, &[0; 64]).is_some());
|
|
// A SHA-1 or MD5 sized digest is no SHA-256 one.
|
|
assert!(Digest::new(HashAlgorithm::Sha256, &[0; 20]).is_none());
|
|
assert!(Digest::new(HashAlgorithm::Sha256, &[0; 16]).is_none());
|
|
assert!(Digest::new(HashAlgorithm::Sha512, &[0; 32]).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn have_key() {
|
|
let agent = FakeAgent::new()
|
|
.on(&format!("HAVEKEY {GRIP}"), "OK\n")
|
|
.on(&format!("HAVEKEY {GRIP}"), "ERR 67108881 No secret key <GPG Agent>\n")
|
|
.on(&format!("HAVEKEY {GRIP}"), "ERR 67109041 IPC call has been cancelled\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
assert!(agent.have_key(&grip()).unwrap());
|
|
assert!(!agent.have_key(&grip()).unwrap());
|
|
assert!(matches!(agent.have_key(&grip()), Err(Error::Server(_))));
|
|
}
|
|
|
|
#[test]
|
|
fn key_info_on_disk_and_on_a_card() {
|
|
let agent = FakeAgent::new()
|
|
.on(&format!("KEYINFO {GRIP}"), format!("S KEYINFO {GRIP} D - - 1 P - - -\nOK\n"))
|
|
.on(&format!("KEYINFO {GRIP}"), format!("S KEYINFO {GRIP} T D2760001240103040006123456780000 OPENPGP.1 - - - - -\nOK\n"))
|
|
.on(&format!("KEYINFO {GRIP}"), "ERR 67108891 Not found <GPG Agent>\n")
|
|
.on(&format!("KEYINFO {GRIP}"), format!("S KEYINFO {OTHER} D - - - P - - -\nOK\n"));
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
let disk = agent.key_info(&grip()).unwrap().unwrap();
|
|
assert_eq!(
|
|
(disk.storage, disk.passphrase_cached, disk.protection, disk.card_serial.as_deref()),
|
|
(KeyStorage::Disk, true, Protection::Protected, None)
|
|
);
|
|
let card = agent.key_info(&grip()).unwrap().unwrap();
|
|
assert_eq!(card.storage, KeyStorage::Smartcard);
|
|
assert_eq!(card.card_serial.as_deref(), Some("D2760001240103040006123456780000"));
|
|
assert_eq!(card.card_key.as_deref(), Some("OPENPGP.1"));
|
|
assert_eq!(card.protection, Protection::Unknown);
|
|
assert_eq!(agent.key_info(&grip()).unwrap(), None);
|
|
// An answer about another key is not taken for this one.
|
|
assert!(matches!(agent.key_info(&grip()), Err(Error::Protocol(_))));
|
|
}
|
|
|
|
#[test]
|
|
fn list_keys_skips_lines_it_cant_read() {
|
|
let agent = FakeAgent::new()
|
|
.on("KEYINFO --list", format!("S KEYINFO {GRIP} D - - - C - - -\nS KEYINFO nonsense\nS PROGRESS 1\nS KEYINFO {OTHER} X - - - - - - -\nOK\n"));
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
let keys = agent.list_keys().unwrap();
|
|
assert_eq!(keys.len(), 2);
|
|
assert_eq!(keys[0].protection, Protection::Clear);
|
|
assert_eq!(keys[1].storage, KeyStorage::Unknown);
|
|
}
|
|
|
|
#[test]
|
|
fn read_key() {
|
|
let agent = FakeAgent::new().on(&format!("READKEY {GRIP}"), "D (10:public-key(3:ecc(5:curve7:Ed25519)(1:q2:%0A%25)))\nOK\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
let key = agent.read_key(&grip()).unwrap();
|
|
assert_eq!(key.find("ecc").and_then(|e| e.value("q")), Some(&b"\n%"[..]));
|
|
}
|
|
|
|
#[test]
|
|
fn signing_a_digest_sends_the_key_the_description_and_the_hash() {
|
|
let digest = Digest::new(HashAlgorithm::Sha256, &[0xAB; 32]).unwrap();
|
|
let agent = FakeAgent::new()
|
|
.on(&format!("SIGKEY {GRIP}"), "OK\n")
|
|
.on("SETKEYDESC Sign+a+message+from+ann@example.org%0A100%25+sure", "OK\n")
|
|
.on(&format!("SETHASH 8 {}", "AB".repeat(32)), "OK\n")
|
|
.on("PKSIGN", "INQUIRE PINENTRY_LAUNCHED 1234 gtk2 1.2.1 - - -\n")
|
|
.on("END", "S PROGRESS x\nD (7:sig-val(3:rsa(1:s4:\x01\x02\x03\x04)))\nOK\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
let sig = agent.sign(&grip(), &digest, Some("Sign a message from ann@example.org\n100% sure")).unwrap();
|
|
assert_eq!(sig.algorithm, "rsa");
|
|
assert_eq!(sig.part("s"), Some(&b"\x01\x02\x03\x04"[..]));
|
|
assert_eq!(sig.part("r"), None);
|
|
let fake = agent.close();
|
|
assert!(fake.wrong.is_empty(), "{:?}", fake.wrong);
|
|
}
|
|
|
|
#[test]
|
|
fn an_ecdsa_signature_and_sha512() {
|
|
let digest = Digest::new(HashAlgorithm::Sha512, &[1; 64]).unwrap();
|
|
let agent = FakeAgent::new()
|
|
.on(&format!("SIGKEY {GRIP}"), "OK\n")
|
|
.on("SETHASH 10 *", "OK\n")
|
|
.on("PKSIGN", "D (7:sig-val(5:ecdsa(1:r1:%01)(1:s1:%02)))\nOK\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
let sig = agent.sign(&grip(), &digest, None).unwrap();
|
|
assert_eq!((sig.algorithm.as_str(), sig.part("r"), sig.part("s")), ("ecdsa", Some(&[1u8][..]), Some(&[2u8][..])));
|
|
}
|
|
|
|
#[test]
|
|
fn a_cancelled_pinentry_and_a_bad_signature() {
|
|
let digest = Digest::new(HashAlgorithm::Sha256, &[0; 32]).unwrap();
|
|
let agent = FakeAgent::new()
|
|
.on("SIGKEY *", "OK\n")
|
|
.on("SETHASH *", "OK\n")
|
|
.on("PKSIGN", "ERR 83886179 Operation cancelled <Pinentry>\n")
|
|
.on("SIGKEY *", "OK\n")
|
|
.on("SETHASH *", "OK\n")
|
|
.on("PKSIGN", "D (6:public)\nOK\n")
|
|
.on("SIGKEY *", "OK\n")
|
|
.on("SETHASH *", "OK\n")
|
|
.on("PKSIGN", "D (7:sig-val)\nOK\n")
|
|
.on("SIGKEY *", "OK\n")
|
|
.on("SETHASH *", "OK\n")
|
|
.on("PKSIGN", "D junk\nOK\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
let e = agent.sign(&grip(), &digest, None).unwrap_err();
|
|
assert_eq!(e.server().map(|e| e.kind()), Some(ErrorKind::Canceled));
|
|
for _ in 0..3 {
|
|
assert!(matches!(agent.sign(&grip(), &digest, None), Err(Error::Protocol(_))));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_inquiry_other_than_pinentry_launched_is_refused_during_signing() {
|
|
let digest = Digest::new(HashAlgorithm::Sha256, &[0; 32]).unwrap();
|
|
let agent = FakeAgent::new().on("SIGKEY *", "OK\n").on("SETHASH *", "OK\n").on("PKSIGN", "INQUIRE CIPHERTEXT\n").on("CAN", "ERR 99 cancelled\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
assert!(matches!(agent.sign(&grip(), &digest, None), Err(Error::Server(_))));
|
|
assert!(agent.close().done());
|
|
}
|
|
|
|
#[test]
|
|
fn a_too_long_description_is_never_sent() {
|
|
let digest = Digest::new(HashAlgorithm::Sha256, &[0; 32]).unwrap();
|
|
let mut agent = GpgAgent::open(FakeAgent::new()).unwrap();
|
|
assert!(matches!(agent.sign(&grip(), &digest, Some(&"x".repeat(301))), Err(Error::Refused(_))));
|
|
assert!(agent.close().sent == ["BYE"]);
|
|
}
|
|
|
|
fn ciphertext() -> Sexp {
|
|
crate::sexp::parse(b"(7:enc-val(3:rsa(1:a3:\n\r%)))").unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn decrypting_answers_the_ciphertext_inquiry() {
|
|
let agent = FakeAgent::new()
|
|
.on(&format!("SETKEY {GRIP}"), "OK\n")
|
|
.on("SETKEYDESC Open+a+message", "OK\n")
|
|
.on("PKDECRYPT", "S INQUIRE_MAXLEN 4096\nINQUIRE CIPHERTEXT\n")
|
|
.on("D (7:enc-val(3:rsa(1:a3:%0A%0D%25)))", "")
|
|
// gpg-agent ends the value with a NUL.
|
|
.on("END", "S PADDING 0\nD (5:value6:\x09key%0A!)\x00\nOK\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
let key = agent.decrypt(&grip(), &ciphertext(), Some("Open a message")).unwrap();
|
|
assert_eq!(key.bytes(), b"\x09key\n!");
|
|
assert_eq!(key.padding(), Some(0));
|
|
assert_eq!(format!("{key:?}"), "SessionKey(<6 bytes>)");
|
|
assert!(agent.close().wrong.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn a_decrypted_value_must_be_a_value() {
|
|
let agent = FakeAgent::new()
|
|
.on("SETKEY *", "OK\n")
|
|
.on("PKDECRYPT", "INQUIRE CIPHERTEXT\n")
|
|
.on("D *", "")
|
|
.on("END", "D (5:other1:x)\nOK\n")
|
|
.on("SETKEY *", "OK\n")
|
|
.on("PKDECRYPT", "INQUIRE CIPHERTEXT\n")
|
|
.on("D *", "")
|
|
.on("END", "D (5:value(1:x))\nOK\n")
|
|
.on("SETKEY *", "OK\n")
|
|
.on("PKDECRYPT", "ERR 67108881 No secret key\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
assert!(matches!(agent.decrypt(&grip(), &ciphertext(), None), Err(Error::Protocol(_))));
|
|
assert!(matches!(agent.decrypt(&grip(), &ciphertext(), None), Err(Error::Protocol(_))));
|
|
let e = agent.decrypt(&grip(), &ciphertext(), None).unwrap_err();
|
|
assert_eq!(e.server().map(|e| e.kind()), Some(ErrorKind::NoSecretKey));
|
|
// Only an enc-val is sent at all.
|
|
assert!(matches!(agent.decrypt(&grip(), &Sexp::token("x"), None), Err(Error::Refused(_))));
|
|
}
|
|
|
|
#[test]
|
|
fn pinentry_hints_become_options_and_a_refused_one_is_skipped() {
|
|
let env = |name: &str| match name {
|
|
"DISPLAY" => Some(":0".to_string()),
|
|
"TERM" => Some("xterm-256color".to_string()),
|
|
"XAUTHORITY" => Some("bad\nvalue".to_string()),
|
|
"WAYLAND_DISPLAY" => Some("wayland-0".to_string()),
|
|
"LC_CTYPE" => Some(String::new()),
|
|
_ => None,
|
|
};
|
|
let hints = PinentryHints::from_env(env);
|
|
let agent = FakeAgent::new()
|
|
.on("OPTION display=:0", "OK\n")
|
|
.on("OPTION ttytype=xterm-256color", "ERR 67109115 Unknown option\n")
|
|
.on("OPTION putenv=WAYLAND_DISPLAY=wayland-0", "OK\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
agent.set_pinentry_hints(&hints).unwrap();
|
|
assert!(agent.close().wrong.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn version_and_reset() {
|
|
let agent = FakeAgent::new().on("GETINFO version", "D 2.4.7\nOK\n").on("RESET", "OK\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
assert_eq!(agent.version().unwrap(), "2.4.7");
|
|
agent.reset().unwrap();
|
|
}
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn any_agent_never_panics(bytes in proptest::collection::vec(any::<u8>(), 0..4096)) {
|
|
let mut input = b"OK\n".to_vec();
|
|
input.extend_from_slice(&bytes);
|
|
let Ok(mut agent) = GpgAgent::open(Canned::new(&input)) else { return Ok(()) };
|
|
let digest = Digest::new(HashAlgorithm::Sha256, &[7; 32]).unwrap();
|
|
let _ = agent.key_info(&grip());
|
|
let _ = agent.list_keys();
|
|
let _ = agent.sign(&grip(), &digest, Some("x"));
|
|
let _ = agent.decrypt(&grip(), &ciphertext(), None);
|
|
let _ = agent.read_key(&grip());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_rsa_value_without_padding_status_keeps_its_frame_and_only_one_nul_is_taken_off() {
|
|
// gpg-agent 2.4 answers an OpenPGP RSA decryption so: no PADDING, the PKCS#1 v1.5 frame
|
|
// still on (its leading zero left out), and a NUL after the S-expression, escaped or not.
|
|
let frame = b"\x02\x11\x22\x33\x00\x09KEY";
|
|
let reply = |nul: &str| {
|
|
let mut r = b"D (5:value9:".to_vec();
|
|
r.extend_from_slice(frame);
|
|
r.extend_from_slice(format!("){nul}\nOK\n").as_bytes());
|
|
r
|
|
};
|
|
let agent = FakeAgent::new()
|
|
.on("SETKEY *", "OK\n")
|
|
.on("PKDECRYPT", "INQUIRE CIPHERTEXT\n")
|
|
.on("D *", "")
|
|
.on("END", reply("%00"))
|
|
.on("SETKEY *", "OK\n")
|
|
.on("PKDECRYPT", "INQUIRE CIPHERTEXT\n")
|
|
.on("D *", "")
|
|
.on("END", reply("\x00\x00"));
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
let key = agent.decrypt(&grip(), &ciphertext(), None).unwrap();
|
|
assert_eq!(key.padding(), None);
|
|
assert_eq!(key.bytes(), frame);
|
|
assert!(matches!(agent.decrypt(&grip(), &ciphertext(), None), Err(Error::Protocol(_))));
|
|
}
|
|
|
|
#[test]
|
|
fn more_keys_than_status_lines_an_answer_keeps() {
|
|
let mut lines = String::new();
|
|
for i in 0..(crate::MAX_KEYS + 500) {
|
|
lines.push_str(&format!("S KEYINFO {i:040X} D - - - P - - -\n"));
|
|
}
|
|
lines.push_str("OK\n");
|
|
let agent = FakeAgent::new().on("KEYINFO --list", lines).on("GETINFO version", "D 2.4.9\nOK\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
assert_eq!(agent.list_keys().unwrap().len(), crate::MAX_KEYS);
|
|
// The connection is still in step.
|
|
assert_eq!(agent.version().unwrap(), "2.4.9");
|
|
}
|
|
|
|
#[test]
|
|
fn each_signature_starts_with_reset_so_no_old_description_is_left() {
|
|
let digest = Digest::new(HashAlgorithm::Sha256, &[0; 32]).unwrap();
|
|
let agent = FakeAgent::new()
|
|
.on("SIGKEY *", "OK\n")
|
|
.on("SETKEYDESC Old+text", "OK\n")
|
|
.on("SETHASH *", "ERR 1 bad hash\n")
|
|
.on("SIGKEY *", "OK\n")
|
|
.on("SETHASH *", "OK\n")
|
|
.on("PKSIGN", "D (7:sig-val(3:rsa(1:s1:x)))\nOK\n")
|
|
.on("SETKEY *", "OK\n")
|
|
.on("PKDECRYPT", "ERR 99 cancelled\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
assert!(agent.sign(&grip(), &digest, Some("Old text")).is_err());
|
|
agent.sign(&grip(), &digest, None).unwrap();
|
|
assert!(agent.decrypt(&grip(), &ciphertext(), None).is_err());
|
|
let fake = agent.close();
|
|
assert!(fake.wrong.is_empty(), "{:?}", fake.wrong);
|
|
let firsts: Vec<&str> = fake.sent.iter().map(|l| l.split(' ').next().unwrap()).filter(|c| ["RESET", "SIGKEY", "SETKEY"].contains(c)).collect();
|
|
assert_eq!(firsts, ["RESET", "SIGKEY", "RESET", "SIGKEY", "RESET", "SETKEY"]);
|
|
}
|
|
|
|
#[test]
|
|
fn the_owner_wait_hook_is_on_only_for_the_commands_that_wait() {
|
|
let digest = Digest::new(HashAlgorithm::Sha256, &[0; 32]).unwrap();
|
|
let agent = FakeAgent::new()
|
|
.on("SIGKEY *", "OK\n")
|
|
.on("SETHASH *", "OK\n")
|
|
.on("PKSIGN", "INQUIRE PINENTRY_LAUNCHED 1\n")
|
|
.on("END", "ERR 99 cancelled\n")
|
|
.on("SETKEY *", "OK\n")
|
|
.on("PKDECRYPT", "INQUIRE CIPHERTEXT\n")
|
|
.on("D *", "")
|
|
.on("END", "D (5:value1:k)\nOK\n")
|
|
.on("HAVEKEY *", "OK\n");
|
|
let mut agent = GpgAgent::open(agent).unwrap();
|
|
agent.set_owner_wait(|fake: &mut FakeAgent, long| fake.long_wait = long);
|
|
assert!(agent.sign(&grip(), &digest, None).is_err());
|
|
agent.decrypt(&grip(), &ciphertext(), None).unwrap();
|
|
agent.have_key(&grip()).unwrap();
|
|
let fake = agent.close();
|
|
let long: Vec<&str> = fake.waits.iter().filter(|(_, long)| *long).map(|(l, _)| l.split(' ').next().unwrap()).collect();
|
|
assert_eq!(long, ["PKSIGN", "END", "PKDECRYPT", "D", "END"]);
|
|
assert!(!fake.long_wait);
|
|
}
|