diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2024-04-10 14:28:35 +0200 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2024-04-10 14:28:35 +0200 |
commit | a01c8f10ad275cfbf1dac66491cda4a4f64a05a4 (patch) | |
tree | 6655c1e0d2a79e25f271ffe55edf25f4d7777806 | |
parent | e481a6e1b9ef04f58443b24a94f5b8bf1133987e (diff) |
openpgp: Add S2K::Implicit.
- For historical reasons, if the S2K usage octet is not a known S2K
mechanism, the octet denotes a symmetric algorithm used to
encrypt the key material with. In this case, the symmetric key is
the MD5 sum over the password. See section 5.5.3. Secret-Key
Packet Formats of RFC4880.While this is obviously not a great
choice, it is no worse than `S2K::Simple { hash: MD5 }`, since
it is equivalent to that.
- Model this by adding a new S2K variant.
- Notably, this fixes handling of packets with unknown S2K
mechanisms. Under the model of RFC4880, which we implement, any
unknown S2K mechanism is an implicit S2K, where the usage octet
denotes an unsupported symmetric algorithm. Using this will fail,
but we now can parse and serialize it correctly, and with them the
secret key packets they come in.
- Fixes #1095.
-rw-r--r-- | openpgp/NEWS | 1 | ||||
-rw-r--r-- | openpgp/src/crypto/s2k.rs | 18 | ||||
-rw-r--r-- | openpgp/src/parse.rs | 55 | ||||
-rw-r--r-- | openpgp/src/serialize.rs | 30 | ||||
-rw-r--r-- | openpgp/tests/data/keys/hardware-backed-secret.pgp | 14 |
5 files changed, 97 insertions, 21 deletions
diff --git a/openpgp/NEWS b/openpgp/NEWS index 7b0d62f9..95d629e4 100644 --- a/openpgp/NEWS +++ b/openpgp/NEWS @@ -5,6 +5,7 @@ * Changes in 1.20.0 ** New functionality + - S2K::Implicit - Signature::verify_signature * Changes in 1.19.0 ** Notable fixes diff --git a/openpgp/src/crypto/s2k.rs b/openpgp/src/crypto/s2k.rs index 9b7c24d6..fc4bdc90 100644 --- a/openpgp/src/crypto/s2k.rs +++ b/openpgp/src/crypto/s2k.rs @@ -78,6 +78,18 @@ pub enum S2K { hash: HashAlgorithm }, + /// Simply hashes the password using MD5 + /// + /// This mechanism uses neither iteration to increase the time it + /// takes to derive the key from the password nor does it salt the + /// password, as well as using a very weak and fast hash + /// algorithm. This makes dictionary attacks more feasible. + /// + /// This mechanism has been deprecated in RFC 2440. Do not use + /// this variant. + #[deprecated(note = "Use `S2K::Iterated`.")] + Implicit, + /// Private S2K algorithm. Private { /// Tag identifying the private algorithm. @@ -224,6 +236,7 @@ impl S2K { hash.update(&data[0..tail]); } } + S2K::Implicit | S2K::Unknown { .. } | &S2K::Private { .. } => unreachable!(), } @@ -234,6 +247,9 @@ impl S2K { Ok(ret.into()) }), + S2K::Implicit => S2K::Simple { + hash: HashAlgorithm::MD5, + }.derive_key(password, key_size), S2K::Unknown { tag, .. } | S2K::Private { tag, .. } => Err(Error::MalformedPacket( format!("Unknown S2K type {:#x}", tag)).into()), @@ -248,6 +264,7 @@ impl S2K { Simple { .. } | Salted { .. } | Iterated { .. } + | Implicit => true, S2K::Private { .. } | S2K::Unknown { .. } @@ -353,6 +370,7 @@ impl fmt::Display for S2K { salt[4], salt[5], salt[6], salt[7], hash_bytes)) } + S2K::Implicit => f.write_str("Implicit S2K"), S2K::Private { tag, parameters } => if let Some(p) = parameters.as_ref() { write!(f, "Private/Experimental S2K {}:{:?}", tag, p) diff --git a/openpgp/src/parse.rs b/openpgp/src/parse.rs index 6bcbf869..9ebea413 100644 --- a/openpgp/src/parse.rs +++ b/openpgp/src/parse.rs @@ -2358,14 +2358,19 @@ impl Key4<key::UnspecifiedParts, key::UnspecifiedRole> Some(mpi::SecretKeyChecksum::Sum16))); sec.into() } - // Encrypted & MD5 for key derivation: unsupported - 1..=253 => { - return php.fail("unsupported secret key encryption"); - } - // Encrypted, S2K & SHA-1 checksum - 254 | 255 => { - let sk: SymmetricAlgorithm = php_try!(php.parse_u8("sym_algo")).into(); - let s2k = php_try!(S2K::parse_v4(&mut php)); + // Encrypted, whether we support the S2K method or not. + _ => { + let sk: SymmetricAlgorithm = match s2k_usage { + 254 | 255 => + php_try!(php.parse_u8("sym_algo")).into(), + _ => s2k_usage.into(), + }; + let s2k = match s2k_usage { + 254 | 255 => php_try!(S2K::parse_v4(&mut php)), + _ => { + #[allow(deprecated)] S2K::Implicit + }, + }; let s2k_supported = s2k.is_supported(); let cipher = php_try!(php.parse_bytes_eof("encrypted_mpis")) @@ -2373,10 +2378,10 @@ impl Key4<key::UnspecifiedParts, key::UnspecifiedRole> crate::packet::key::Encrypted::new_raw( s2k, sk, - if s2k_usage == 254 { - Some(mpi::SecretKeyChecksum::SHA1) - } else { - Some(mpi::SecretKeyChecksum::Sum16) + match s2k_usage { + 254 => Some(mpi::SecretKeyChecksum::SHA1), + 255 => Some(mpi::SecretKeyChecksum::Sum16), + _ => Some(mpi::SecretKeyChecksum::Sum16), }, if s2k_supported { Ok(cipher) @@ -6757,4 +6762,30 @@ yCsBO81bKqlfklugX5yRX5qTopuXX6KbWpFZXKJXUlGSetb4dXm+gYFBCRcA Ok(()) } + /// Tests for issue 1095, parsing a secret key packet with an + /// unknown S2K mechanism. + #[test] + fn key_unknown_s2k() -> Result<()> { + let mut ppr = PacketParser::from_bytes( + crate::tests::key("hardware-backed-secret.pgp"))?; + let mut i = 0; + while let PacketParserResult::Some(pp) = ppr { + if i == 0 { + assert!(matches!(&pp.packet, Packet::SecretKey(_))); + } + if i == 3 { + assert!(matches!(&pp.packet, Packet::SecretSubkey(_))); + } + + // Make sure it roundtrips. + let p = &pp.packet; + let v = p.to_vec()?; + let q = Packet::from_bytes(&v)?; + assert_eq!(p, &q); + + ppr = pp.recurse()?.1; + i += 1; + } + Ok(()) + } } diff --git a/openpgp/src/serialize.rs b/openpgp/src/serialize.rs index 6dcc1845..89ca0edf 100644 --- a/openpgp/src/serialize.rs +++ b/openpgp/src/serialize.rs @@ -1319,6 +1319,7 @@ impl Marshal for S2K { w.write_all(&salt[..])?; w.write_all(&[S2K::encode_count(hash_bytes)?])?; } + S2K::Implicit => (), S2K::Private { tag, parameters } | S2K::Unknown { tag, parameters} => { w.write_all(&[*tag])?; @@ -1339,6 +1340,7 @@ impl MarshalInto for S2K { &S2K::Simple{ .. } => 2, &S2K::Salted{ .. } => 2 + 8, &S2K::Iterated{ .. } => 2 + 8 + 1, + S2K::Implicit => 0, S2K::Private { parameters, .. } | S2K::Unknown { parameters, .. } => 1 + parameters.as_ref().map(|p| p.len()).unwrap_or(0), @@ -2001,14 +2003,21 @@ impl<P, R> Marshal for Key4<P, R> mpis.serialize_with_checksum(o, SecretKeyChecksum::Sum16) })?, SecretKeyMaterial::Encrypted(ref e) => { - // S2K usage. - write_byte(o, match e.checksum() { - Some(SecretKeyChecksum::SHA1) => 254, - Some(SecretKeyChecksum::Sum16) => 255, - None => return Err(Error::InvalidOperation( - "In Key4 packets, encrypted secret keys must be \ - checksummed".into()).into()), - })?; + // S2K usage + #[allow(deprecated)] + if let S2K::Implicit = e.s2k() { + // When the legacy implicit S2K mechanism is + // in use, the symmetric algorithm octet below + // takes the place of the S2K usage octet. + } else { + write_byte(o, match e.checksum() { + Some(SecretKeyChecksum::SHA1) => 254, + Some(SecretKeyChecksum::Sum16) => 255, + None => return Err(Error::InvalidOperation( + "In Key4 packets, encrypted secret keys must be \ + checksummed".into()).into()), + })?; + } write_byte(o, e.algo().into())?; e.s2k().serialize(o)?; o.write_all(e.raw_ciphertext())?; @@ -2024,6 +2033,7 @@ impl<P, R> NetLength for Key4<P, R> where P: key::KeyParts, R: key::KeyRole, { + #[allow(deprecated)] fn net_len(&self) -> usize { let have_secret_key = P::significant_secrets() && self.has_secret(); @@ -2037,7 +2047,9 @@ impl<P, R> NetLength for Key4<P, R> u.map(|mpis| mpis.serialized_len()) + 2, // Two octet checksum. SecretKeyMaterial::Encrypted(ref e) => - 1 + e.s2k().serialized_len() + matches!(e.s2k(), S2K::Implicit) + .then_some(0).unwrap_or(1) + + e.s2k().serialized_len() + e.raw_ciphertext().len(), } } else { diff --git a/openpgp/tests/data/keys/hardware-backed-secret.pgp b/openpgp/tests/data/keys/hardware-backed-secret.pgp new file mode 100644 index 00000000..b211205a --- /dev/null +++ b/openpgp/tests/data/keys/hardware-backed-secret.pgp @@ -0,0 +1,14 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: from https://gitlab.com/dkg/openpgp-hardware-secrets/-/merge_requests/2 + +xTQEZgWtcxYJKwYBBAHaRw8BAQdAlLK6UPQsVHR2ETk1SwVIG3tBmpiEtikYYlCy +1TIiqzb8zR08aGFyZHdhcmUtc2VjcmV0QGV4YW1wbGUub3JnPsKNBBAWCAA1AhkB +BQJmBa1zAhsDCAsJCAcKDQwLBRUKCQgLAhYCFiEEXlP8Tur0WZR+f0I33/i9Uh4O +HEkACgkQ3/i9Uh4OHEnryAD8CzH2ajJvASp46ApfI4pLPY57rjBX++d/2FQPRyqG +HJUA/RLsNNgxiFYmK5cjtQe2/DgzWQ7R6PxPC6oa3XM7xPcCxzkEZgWtcxIKKwYB +BAGXVQEFAQEHQE1YXOKeaklwG01Yab4xopP9wbu1E+pCrP1xQpiFZW5KAwEIB/zC +eAQYFggAIAUCZgWtcwIbDBYhBF5T/E7q9FmUfn9CN9/4vVIeDhxJAAoJEN/4vVIe +DhxJVTgA/1WaFrKdP3AgL0Ffdooc5XXbjQsj0uHo6FZSHRI4pchMAQCyJnKQ3RvW +/0gm41JCqImyg2fxWG4hY0N5Q7Rc6PyzDQ== +=3w/O +-----END PGP PRIVATE KEY BLOCK----- |