sequoia_openpgp/crypto/asymmetric.rs
1//! Asymmetric crypto operations.
2
3use crate::packet::{self, key, Key};
4use crate::crypto::SessionKey;
5use crate::crypto::mpi;
6use crate::types::{
7 Curve,
8 HashAlgorithm,
9 PublicKeyAlgorithm,
10 SymmetricAlgorithm,
11};
12
13use crate::{Error, Result};
14
15/// Creates a signature.
16///
17/// Used in the streaming [`Signer`], the methods binding components
18/// to certificates (e.g. [`UserID::bind`]), [`SignatureBuilder`]'s
19/// signing functions (e.g. [`SignatureBuilder::sign_standalone`]),
20/// and likely many more places.
21///
22/// [`Signer`]: crate::serialize::stream::Signer
23/// [`UserID::bind`]: crate::packet::UserID::bind()
24/// [`SignatureBuilder`]: crate::packet::signature::SignatureBuilder
25/// [`SignatureBuilder::sign_standalone`]: crate::packet::signature::SignatureBuilder::sign_standalone()
26///
27/// This is a low-level mechanism to produce an arbitrary OpenPGP
28/// signature. Using this trait allows Sequoia to perform all
29/// operations involving signing to use a variety of secret key
30/// storage mechanisms (e.g. smart cards).
31///
32/// A signer consists of the public key and a way of creating a
33/// signature. This crate implements `Signer` for [`KeyPair`], which
34/// is a tuple containing the public and unencrypted secret key in
35/// memory. Other crates may provide their own implementations of
36/// `Signer` to utilize keys stored in various places. Currently, the
37/// following implementations exist:
38///
39/// - [`KeyPair`]: In-memory keys.
40/// - [`sequoia_rpc::gnupg::KeyPair`]: Connects to the `gpg-agent`.
41///
42/// [`sequoia_rpc::gnupg::KeyPair`]: https://docs.sequoia-pgp.org/sequoia_ipc/gnupg/struct.KeyPair.html
43pub trait Signer {
44 /// Returns a reference to the public key.
45 fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole>;
46
47 /// Returns a list of hashes that this signer accepts.
48 ///
49 /// Some cryptographic libraries or hardware modules support signing digests
50 /// produced with only a limited set of hashing algorithms. This function
51 /// indicates to callers which algorithm digests are supported by this signer.
52 ///
53 /// The default implementation of this function allows all hash algorithms to
54 /// be used. Provide an explicit implementation only when a smaller subset
55 /// of hashing algorithms is valid for this `Signer` implementation.
56 fn acceptable_hashes(&self) -> &[HashAlgorithm] {
57 crate::crypto::hash::default_hashes_sorted()
58 }
59
60 /// Creates a signature over the `digest` produced by `hash_algo`.
61 fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8])
62 -> Result<mpi::Signature>;
63}
64
65impl Signer for Box<dyn Signer> {
66 fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole> {
67 self.as_ref().public()
68 }
69
70 fn acceptable_hashes(&self) -> &[HashAlgorithm] {
71 self.as_ref().acceptable_hashes()
72 }
73
74 fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8])
75 -> Result<mpi::Signature> {
76 self.as_mut().sign(hash_algo, digest)
77 }
78}
79
80impl Signer for Box<dyn Signer + Send + Sync> {
81 fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole> {
82 self.as_ref().public()
83 }
84
85 fn acceptable_hashes(&self) -> &[HashAlgorithm] {
86 self.as_ref().acceptable_hashes()
87 }
88
89 fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8])
90 -> Result<mpi::Signature> {
91 self.as_mut().sign(hash_algo, digest)
92 }
93}
94
95/// Decrypts a message.
96///
97/// Used by [`PKESK::decrypt`] to decrypt session keys.
98///
99/// [`PKESK::decrypt`]: crate::packet::PKESK#method.decrypt
100///
101/// This is a low-level mechanism to decrypt an arbitrary OpenPGP
102/// ciphertext. Using this trait allows Sequoia to perform all
103/// operations involving decryption to use a variety of secret key
104/// storage mechanisms (e.g. smart cards).
105///
106/// A decryptor consists of the public key and a way of decrypting a
107/// session key. This crate implements `Decryptor` for [`KeyPair`],
108/// which is a tuple containing the public and unencrypted secret key
109/// in memory. Other crates may provide their own implementations of
110/// `Decryptor` to utilize keys stored in various places. Currently, the
111/// following implementations exist:
112///
113/// - [`KeyPair`]: In-memory keys.
114/// - [`sequoia_rpc::gnupg::KeyPair`]: Connects to the `gpg-agent`.
115///
116/// [`sequoia_rpc::gnupg::KeyPair`]: https://docs.sequoia-pgp.org/sequoia_ipc/gnupg/struct.KeyPair.html
117pub trait Decryptor {
118 /// Returns a reference to the public key.
119 fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole>;
120
121 /// Decrypts `ciphertext`, returning the plain session key.
122 fn decrypt(&mut self, ciphertext: &mpi::Ciphertext,
123 plaintext_len: Option<usize>)
124 -> Result<SessionKey>;
125}
126
127impl Decryptor for Box<dyn Decryptor> {
128 fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole> {
129 self.as_ref().public()
130 }
131
132 fn decrypt(&mut self, ciphertext: &mpi::Ciphertext,
133 plaintext_len: Option<usize>)
134 -> Result<SessionKey> {
135 self.as_mut().decrypt(ciphertext, plaintext_len)
136 }
137}
138
139impl Decryptor for Box<dyn Decryptor + Send + Sync> {
140 fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole> {
141 self.as_ref().public()
142 }
143
144 fn decrypt(&mut self, ciphertext: &mpi::Ciphertext,
145 plaintext_len: Option<usize>)
146 -> Result<SessionKey> {
147 self.as_mut().decrypt(ciphertext, plaintext_len)
148 }
149}
150
151/// A cryptographic key pair.
152///
153/// A `KeyPair` is a combination of public and secret key. If both
154/// are available in memory, a `KeyPair` is a convenient
155/// implementation of [`Signer`] and [`Decryptor`].
156///
157///
158/// # Examples
159///
160/// ```
161/// # fn main() -> sequoia_openpgp::Result<()> {
162/// use sequoia_openpgp as openpgp;
163/// use openpgp::types::Curve;
164/// use openpgp::cert::prelude::*;
165/// use openpgp::packet::prelude::*;
166///
167/// // Conveniently create a KeyPair from a bare key:
168/// let keypair =
169/// Key4::<_, key::UnspecifiedRole>::generate_ecc(false, Curve::Cv25519)?
170/// .into_keypair()?;
171///
172/// // Or from a query over a certificate:
173/// let (cert, _) =
174/// CertBuilder::general_purpose(Some("alice@example.org"))
175/// .generate()?;
176/// let keypair =
177/// cert.keys().unencrypted_secret().nth(0).unwrap().key().clone()
178/// .into_keypair()?;
179/// # Ok(()) }
180/// ```
181#[derive(Clone)]
182pub struct KeyPair {
183 public: Key<key::PublicParts, key::UnspecifiedRole>,
184 secret: packet::key::Unencrypted,
185}
186assert_send_and_sync!(KeyPair);
187
188impl KeyPair {
189 /// Creates a new key pair.
190 pub fn new(public: Key<key::PublicParts, key::UnspecifiedRole>,
191 secret: packet::key::Unencrypted)
192 -> Result<Self>
193 {
194 Ok(Self {
195 public,
196 secret,
197 })
198 }
199
200 /// Returns a reference to the public key.
201 pub fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole> {
202 &self.public
203 }
204
205 /// Returns a reference to the secret key.
206 pub fn secret(&self) -> &packet::key::Unencrypted {
207 &self.secret
208 }
209}
210
211impl From<KeyPair> for Key<key::SecretParts, key::UnspecifiedRole> {
212 fn from(p: KeyPair) -> Self {
213 let (key, secret) = (p.public, p.secret);
214 key.add_secret(secret.into()).0
215 }
216}
217
218impl Signer for KeyPair {
219 fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole> {
220 KeyPair::public(self)
221 }
222
223 fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8])
224 -> Result<mpi::Signature>
225 {
226 use crate::crypto::backend::{Backend, interface::Asymmetric};
227
228 self.secret().map(|secret| {
229 #[allow(deprecated)]
230 match (self.public().pk_algo(), self.public().mpis(), secret) {
231 (PublicKeyAlgorithm::Ed25519,
232 mpi::PublicKey::Ed25519 { a },
233 mpi::SecretKeyMaterial::Ed25519 { x }) => {
234 Ok(mpi::Signature::Ed25519 {
235 s: Box::new(Backend::ed25519_sign(x, a, digest)?),
236 })
237 },
238
239 (PublicKeyAlgorithm::Ed448,
240 mpi::PublicKey::Ed448 { a },
241 mpi::SecretKeyMaterial::Ed448 { x }) => {
242 Ok(mpi::Signature::Ed448 {
243 s: Box::new(Backend::ed448_sign(x, a, digest)?),
244 })
245 },
246
247 (PublicKeyAlgorithm::MLDSA65_Ed25519,
248 mpi::PublicKey::MLDSA65_Ed25519 {
249 eddsa: eddsa_pub, ..
250 },
251 mpi::SecretKeyMaterial::MLDSA65_Ed25519 {
252 eddsa: eddsa_sec, mldsa: mldsa_sec,
253 }) => Ok(mpi::Signature::MLDSA65_Ed25519 {
254 eddsa: Box::new(Backend::ed25519_sign(
255 eddsa_sec, eddsa_pub, digest)?),
256 mldsa: Backend::mldsa65_sign(
257 mldsa_sec, digest)?,
258 }),
259
260 (PublicKeyAlgorithm::MLDSA87_Ed448,
261 mpi::PublicKey::MLDSA87_Ed448 {
262 eddsa: eddsa_pub, ..
263 },
264 mpi::SecretKeyMaterial::MLDSA87_Ed448 {
265 eddsa: eddsa_sec, mldsa: mldsa_sec,
266 }) => Ok(mpi::Signature::MLDSA87_Ed448 {
267 eddsa: Box::new(Backend::ed448_sign(
268 eddsa_sec, eddsa_pub, digest)?),
269 mldsa: Backend::mldsa87_sign(
270 mldsa_sec, digest)?,
271 }),
272
273 (PublicKeyAlgorithm::SLHDSA128s,
274 mpi::PublicKey::SLHDSA128s { .. },
275 mpi::SecretKeyMaterial::SLHDSA128s { secret }) =>
276 Ok(mpi::Signature::SLHDSA128s {
277 sig: Backend::slhdsa128s_sign(secret, digest)?,
278 }),
279
280 (PublicKeyAlgorithm::SLHDSA128f,
281 mpi::PublicKey::SLHDSA128f { .. },
282 mpi::SecretKeyMaterial::SLHDSA128f { secret }) =>
283 Ok(mpi::Signature::SLHDSA128f {
284 sig: Backend::slhdsa128f_sign(secret, digest)?,
285 }),
286
287 (PublicKeyAlgorithm::SLHDSA256s,
288 mpi::PublicKey::SLHDSA256s { .. },
289 mpi::SecretKeyMaterial::SLHDSA256s { secret }) =>
290 Ok(mpi::Signature::SLHDSA256s {
291 sig: Backend::slhdsa256s_sign(secret, digest)?,
292 }),
293
294 (PublicKeyAlgorithm::EdDSA,
295 mpi::PublicKey::EdDSA { curve, q },
296 mpi::SecretKeyMaterial::EdDSA { scalar }) => match curve {
297 Curve::Ed25519 => {
298 let public = q.decode_point(&Curve::Ed25519)?.0
299 .try_into()?;
300 let secret = scalar.value_padded(32);
301 let sig =
302 Backend::ed25519_sign(&secret, &public, digest)?;
303 Ok(mpi::Signature::EdDSA {
304 r: mpi::MPI::new(&sig[..32]),
305 s: mpi::MPI::new(&sig[32..]),
306 })
307 },
308 _ => Err(
309 Error::UnsupportedEllipticCurve(curve.clone()).into()),
310 },
311
312 (PublicKeyAlgorithm::DSA,
313 mpi::PublicKey::DSA { p, q, g, y },
314 mpi::SecretKeyMaterial::DSA { x }) => {
315 let (r, s) = Backend::dsa_sign(x, p, q, g, y, digest)?;
316 Ok(mpi::Signature::DSA { r, s })
317 },
318
319 (_algo, _public, secret) =>
320 self.sign_backend(secret, hash_algo, digest),
321 }
322 })
323 }
324}
325
326impl Decryptor for KeyPair {
327 fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole> {
328 KeyPair::public(self)
329 }
330
331 fn decrypt(&mut self,
332 ciphertext: &mpi::Ciphertext,
333 plaintext_len: Option<usize>)
334 -> Result<SessionKey>
335 {
336 use crate::crypto::ecdh::aes_key_unwrap;
337 use crate::crypto::backend::{Backend, interface::{Asymmetric, Kdf}};
338
339 self.secret().map(|secret| {
340 #[allow(non_snake_case)]
341 match (self.public().mpis(), secret, ciphertext) {
342 (mpi::PublicKey::X25519 { u: U },
343 mpi::SecretKeyMaterial::X25519 { x },
344 mpi::Ciphertext::X25519 { e: E, key }) => {
345 // Compute the shared point S = xE;
346 let S = Backend::x25519_shared_point(x, E)?;
347
348 // Compute the wrap key.
349 let wrap_algo = SymmetricAlgorithm::AES128;
350 let mut ikm: SessionKey = vec![0; 32 + 32 + 32].into();
351
352 // Yes clippy, this operation will always return
353 // zero. This is the intended outcome. Chill.
354 #[allow(clippy::erasing_op)]
355 ikm[0 * 32..1 * 32].copy_from_slice(&E[..]);
356 ikm[1 * 32..2 * 32].copy_from_slice(&U[..]);
357 ikm[2 * 32..3 * 32].copy_from_slice(&S[..]);
358 let mut kek = vec![0; wrap_algo.key_size()?].into();
359 Backend::hkdf_sha256(&ikm, None, b"OpenPGP X25519",
360 &mut kek)?;
361
362 Ok(aes_key_unwrap(wrap_algo, kek.as_protected(),
363 key)?.into())
364 },
365
366 (mpi::PublicKey::X448 { u: U },
367 mpi::SecretKeyMaterial::X448 { x },
368 mpi::Ciphertext::X448 { e: E, key }) => {
369 // Compute the shared point S = xE;
370 let S = Backend::x448_shared_point(x, E)?;
371
372 // Compute the wrap key.
373 let wrap_algo = SymmetricAlgorithm::AES256;
374 let mut ikm: SessionKey = vec![0; 56 + 56 + 56].into();
375
376 // Yes clippy, this operation will always return
377 // zero. This is the intended outcome. Chill.
378 #[allow(clippy::erasing_op)]
379 ikm[0 * 56..1 * 56].copy_from_slice(&E[..]);
380 ikm[1 * 56..2 * 56].copy_from_slice(&U[..]);
381 ikm[2 * 56..3 * 56].copy_from_slice(&S[..]);
382 let mut kek = vec![0; wrap_algo.key_size()?].into();
383 Backend::hkdf_sha512(&ikm, None, b"OpenPGP X448",
384 &mut kek)?;
385
386 Ok(aes_key_unwrap(wrap_algo, kek.as_protected(),
387 key)?.into())
388 },
389
390 (mpi::PublicKey::ECDH { curve: Curve::Cv25519, .. },
391 mpi::SecretKeyMaterial::ECDH { scalar, },
392 mpi::Ciphertext::ECDH { e, .. }) =>
393 {
394 // Get the public part V of the ephemeral key.
395 let V = e.decode_point(&Curve::Cv25519)?.0;
396
397 // X25519 expects the private key to be exactly 32
398 // bytes long but OpenPGP allows leading zeros to
399 // be stripped. Padding has to be unconditional;
400 // otherwise we have a secret-dependent branch.
401 let mut r = scalar.value_padded(32);
402
403 // Reverse the scalar. See
404 // https://lists.gnupg.org/pipermail/gnupg-devel/2018-February/033437.html
405 r.reverse();
406
407 // Compute the shared point S = rV = rvG, where
408 // (r, R) is the recipient's key pair.
409 let S = Backend::x25519_shared_point(&r, &V.try_into()?)?;
410
411 crate::crypto::ecdh::decrypt_unwrap(
412 self.public(), &S, ciphertext, plaintext_len)
413 },
414
415 (
416 mpi::PublicKey::MLKEM768_X25519 {
417 ecdh: ecdh_public, ..
418 },
419 mpi::SecretKeyMaterial::MLKEM768_X25519 {
420 ecdh: ecdh_secret, mlkem: mlkem_secret,
421 },
422 mpi::Ciphertext::MLKEM768_X25519 {
423 ecdh: ecdh_ciphertext, mlkem: mlkem_ciphertext, esk,
424 },
425 ) => {
426 let ecdh_keyshare = Backend::x25519_shared_point(
427 ecdh_secret, ecdh_ciphertext)?;
428
429 let mlkem_keyshare = Backend::mlkem768_decapsulate(
430 mlkem_secret, mlkem_ciphertext)?;
431
432 let kek = multi_key_combine(
433 &mlkem_keyshare,
434 &ecdh_keyshare,
435 ecdh_ciphertext.as_ref(),
436 ecdh_public.as_ref(),
437 PublicKeyAlgorithm::MLKEM768_X25519)?;
438
439 Ok(aes_key_unwrap(SymmetricAlgorithm::AES256,
440 kek.as_protected(),
441 esk)?.into())
442 },
443
444 (
445 mpi::PublicKey::MLKEM1024_X448 {
446 ecdh: ecdh_public, ..
447 },
448 mpi::SecretKeyMaterial::MLKEM1024_X448 {
449 ecdh: ecdh_secret, mlkem: mlkem_secret,
450 },
451 mpi::Ciphertext::MLKEM1024_X448 {
452 ecdh: ecdh_ciphertext, mlkem: mlkem_ciphertext, esk,
453 },
454 ) => {
455 let ecdh_keyshare = Backend::x448_shared_point(
456 ecdh_secret, ecdh_ciphertext)?;
457
458 let mlkem_keyshare = Backend::mlkem1024_decapsulate(
459 mlkem_secret, mlkem_ciphertext)?;
460
461 let kek = multi_key_combine(
462 &mlkem_keyshare,
463 &ecdh_keyshare,
464 ecdh_ciphertext.as_ref(),
465 ecdh_public.as_ref(),
466 PublicKeyAlgorithm::MLKEM1024_X448)?;
467
468 Ok(aes_key_unwrap(SymmetricAlgorithm::AES256,
469 kek.as_protected(),
470 esk)?.into())
471 },
472
473 (_public, secret, _ciphertext) =>
474 self.decrypt_backend(secret, ciphertext, plaintext_len),
475 }
476 })
477 }
478}
479
480/// Combines PQC and classical algorithms.
481///
482/// See [Section 4.2.1 of draft-ietf-openpgp-pqc-08].
483///
484/// [Section 4.2.1 of draft-ietf-openpgp-pqc-08]: https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-08.html#kem-key-combiner
485pub(crate) fn multi_key_combine(mlkem_key: &[u8],
486 ecdh_key: &[u8],
487 ecdh_ciphertext: &[u8],
488 ecdh_public: &[u8],
489 pk_algo: PublicKeyAlgorithm)
490 -> Result<SessionKey>
491{
492 // multiKeyCombine(
493 // mlkemKeyShare, ecdhKeyShare,
494 // ecdhCipherText, ecdhPublicKey,
495 // algId
496 // )
497 //
498 // Input:
499 // mlkemKeyShare - the ML-KEM key share encoded as an octet string
500 // ecdhKeyShare - the ECDH key share encoded as an octet string
501 // ecdhCipherText - the ECDH ciphertext encoded as an octet string
502 // ecdhPublicKey - the ECDH public key of the recipient as an octet string
503 // algId - the OpenPGP algorithm ID of the public-key encryption algorithm
504 //
505 // KEK = SHA3-256(
506 // mlkemKeyShare || ecdhKeyShare ||
507 // ecdhCipherText || ecdhPublicKey ||
508 // algId || domSep || len(domSep)
509 // )
510
511 let mut hash = HashAlgorithm::SHA3_256.context()?.for_digest();
512 hash.update(mlkem_key);
513 hash.update(ecdh_key);
514 hash.update(ecdh_ciphertext);
515 hash.update(ecdh_public);
516 hash.update(&[pk_algo.into()]);
517 // Domain separation and length octet.
518 hash.update(b"OpenPGPCompositeKDFv1\x15");
519
520 let mut kek = SessionKey::from(vec![0; 32]);
521 hash.digest(&mut kek)?;
522 Ok(kek)
523}