Skip to main content

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}