From 9582ba1a068402e880a1cf3e5b4bb481733953fb Mon Sep 17 00:00:00 2001 From: Justus Winter Date: Thu, 18 Jan 2024 12:51:18 +0100 Subject: openpgp: Serialize TSKs without secrets with appropriate label. - Previously, we emitted the PRIVATE KEY label when armoring TSKs that don't in fact contain secrets, confusing users. - See https://gitlab.com/sequoia-pgp/sequoia-sq/-/issues/14 - Fixes #1075. --- openpgp/src/serialize/cert.rs | 34 ++++++++++++++++++++++++++++++++++ openpgp/src/serialize/cert_armored.rs | 13 ++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) (limited to 'openpgp') diff --git a/openpgp/src/serialize/cert.rs b/openpgp/src/serialize/cert.rs index 2702fda6..764a16ac 100644 --- a/openpgp/src/serialize/cert.rs +++ b/openpgp/src/serialize/cert.rs @@ -280,6 +280,17 @@ impl<'a> TSK<'a> { } } + /// Returns whether we're emitting secret (sub)key packets when + /// serializing. + /// + /// Computes whether we have secrets, taking the filter into + /// account, and whether we are emitting stubs. This can be used + /// to determine the correct armor label to use. + pub(crate) fn emits_secret_key_packets(&self) -> bool { + self.emit_stubs + || self.cert.keys().secret().any(|skb| (self.filter)(skb.key())) + } + /// Filters which secret keys to export using the given predicate. /// /// Note that the given filter replaces any existing filter. @@ -985,4 +996,27 @@ mod test { Ok(()) } + + #[test] + fn issue_1075() -> Result<()> { + let cert = Cert::from_bytes(crate::tests::key("testy.pgp"))?; + let tsk = Cert::from_bytes(crate::tests::key("testy-private.pgp"))?; + + // Filters out secrets. + let no_secrets = |_| false; + + assert!(! cert.as_tsk().emits_secret_key_packets()); + assert!(cert.as_tsk().emit_secret_key_stubs(true) + .emits_secret_key_packets()); + assert!(tsk.as_tsk().emits_secret_key_packets()); + assert!(! tsk.as_tsk().set_filter(no_secrets) + .emits_secret_key_packets()); + + assert!(cert.armored().to_vec()? != tsk.as_tsk().armored().to_vec()?); + assert_eq!(cert.armored().to_vec()?, + cert.as_tsk().armored().to_vec()?); + assert_eq!(cert.armored().to_vec()?, + tsk.as_tsk().set_filter(no_secrets).armored().to_vec()?); + Ok(()) + } } diff --git a/openpgp/src/serialize/cert_armored.rs b/openpgp/src/serialize/cert_armored.rs index a3060e8d..63642954 100644 --- a/openpgp/src/serialize/cert_armored.rs +++ b/openpgp/src/serialize/cert_armored.rs @@ -143,8 +143,11 @@ impl<'a> Encoder<'a> { let (prelude, headers) = match self { Encoder::Cert(cert) => (armor::Kind::PublicKey, cert.armor_headers()), - Encoder::TSK(ref tsk) => - (armor::Kind::SecretKey, tsk.cert.armor_headers()), + Encoder::TSK(tsk) => if tsk.emits_secret_key_packets() { + (armor::Kind::SecretKey, tsk.cert.armor_headers()) + } else { + (armor::Kind::PublicKey, tsk.cert.armor_headers()) + }, }; // Convert the Vec into Vec<(&str, &str)> @@ -202,7 +205,11 @@ impl<'a> MarshalInto for Encoder<'a> { let word = match self { Self::Cert(_) => "PUBLIC", - Self::TSK(_) => "PRIVATE", + Self::TSK(tsk) => if tsk.emits_secret_key_packets() { + "PRIVATE" + } else { + "PUBLIC" + }, }.len(); "-----BEGIN PGP ".len() + word + " KEY BLOCK-----\n\n".len() -- cgit v1.2.3