diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2023-03-08 11:21:41 +0100 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2023-03-08 13:06:44 +0100 |
commit | 336a1562bfe3ad5fe32b5e3017c88197f5bad0fa (patch) | |
tree | ec7f433a8ab5cf97a508848c38be5cc6ae9685cf /openpgp/src/crypto/backend/botan/asymmetric.rs | |
parent | 07bb232c30851bff0b0f315fbc088af6f8e6911a (diff) |
openpgp: Add a new backend based on the Botan cryptographic library.
Diffstat (limited to 'openpgp/src/crypto/backend/botan/asymmetric.rs')
-rw-r--r-- | openpgp/src/crypto/backend/botan/asymmetric.rs | 670 |
1 files changed, 670 insertions, 0 deletions
diff --git a/openpgp/src/crypto/backend/botan/asymmetric.rs b/openpgp/src/crypto/backend/botan/asymmetric.rs new file mode 100644 index 00000000..4cc2da5b --- /dev/null +++ b/openpgp/src/crypto/backend/botan/asymmetric.rs @@ -0,0 +1,670 @@ +//! Hold the implementation of [`Signer`] and [`Decryptor`] for [`KeyPair`]. +//! +//! [`Signer`]: super::super::asymmetric::Signer +//! [`Decryptor`]: super::super::asymmetric::Decryptor +//! [`KeyPair`]: super::super::asymmetric::KeyPair + +use std::time::SystemTime; + +use botan::{ + RandomNumberGenerator, + Pubkey, + Privkey, +}; + +use crate::{ + Error, + Result, + crypto::{ + asymmetric::{KeyPair, Decryptor, Signer}, + mem::Protected, + mpi::{self, MPI, ProtectedMPI, PublicKey}, + SessionKey, + }, + packet::{ + key::{self, Key4, SecretParts}, + Key, + }, + types::{ + Curve, + HashAlgorithm, + PublicKeyAlgorithm, + SymmetricAlgorithm, + }, +}; + +// CONFIDENTIALITY: Botan clears the MPIs after use. +impl TryFrom<&ProtectedMPI> for botan::MPI { + type Error = anyhow::Error; + fn try_from(mpi: &ProtectedMPI) -> anyhow::Result<botan::MPI> { + Ok(botan::MPI::new_from_bytes(mpi.value())?) + } +} + +impl TryFrom<&botan::MPI> for ProtectedMPI { + type Error = anyhow::Error; + fn try_from(bn: &botan::MPI) -> anyhow::Result<Self> { + Ok(bn.to_bin()?.into()) + } +} + +impl TryFrom<botan::MPI> for ProtectedMPI { + type Error = anyhow::Error; + fn try_from(bn: botan::MPI) -> anyhow::Result<Self> { + Ok(bn.to_bin()?.into()) + } +} + +impl TryFrom<&MPI> for botan::MPI { + type Error = anyhow::Error; + fn try_from(mpi: &MPI) -> anyhow::Result<botan::MPI> { + Ok(botan::MPI::new_from_bytes(mpi.value())?) + } +} + +impl TryFrom<&botan::MPI> for MPI { + type Error = anyhow::Error; + fn try_from(bn: &botan::MPI) -> anyhow::Result<Self> { + Ok(bn.to_bin()?.into()) + } +} + +impl TryFrom<botan::MPI> for MPI { + type Error = anyhow::Error; + fn try_from(bn: botan::MPI) -> anyhow::Result<Self> { + Ok(bn.to_bin()?.into()) + } +} + +impl Signer for KeyPair { + fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole> { + KeyPair::public(self) + } + + fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) + -> Result<mpi::Signature> + { + use crate::PublicKeyAlgorithm::*; + + let mut rng = RandomNumberGenerator::new_userspace()?; + + self.secret().map(|secret| { + #[allow(deprecated)] + match (self.public().pk_algo(), self.public().mpis(), secret) + { + (RSASign, + PublicKey::RSA { e, .. }, + mpi::SecretKeyMaterial::RSA { p, q, .. }) | + (RSAEncryptSign, + PublicKey::RSA { e, .. }, + mpi::SecretKeyMaterial::RSA { p, q, .. }) => { + let secret = Privkey::load_rsa(&p.try_into()?, &q.try_into()?, + &e.try_into()?)?; + let sig = secret.sign( + digest, + &format!("PKCS1v15(Raw,{})", hash_algo.botan_name()?), + &mut rng)?; + Ok(mpi::Signature::RSA { + s: MPI::new(&sig), + }) + }, + + (DSA, + PublicKey::DSA { p, q, g, .. }, + mpi::SecretKeyMaterial::DSA { x }) => { + let secret = Privkey::load_dsa(&p.try_into()?, &q.try_into()?, + &g.try_into()?, &x.try_into()?)?; + let size = q.value().len(); + let truncated_digest = &digest[..size.min(digest.len())]; + let sig = secret.sign(truncated_digest, "Raw", &mut rng)?; + + if sig.len() != size * 2 { + return Err(Error::MalformedMPI( + format!("Expected signature with length {}, got {}", + size * 2, sig.len())).into()); + } + + Ok(mpi::Signature::DSA { + r: MPI::new(&sig[..size]), + s: MPI::new(&sig[size..]), + }) + }, + + (EdDSA, + PublicKey::EdDSA { curve, .. }, + mpi::SecretKeyMaterial::EdDSA { scalar }) => match curve { + Curve::Ed25519 => { + let size = 32; + let scalar = scalar.value_padded(size); + let secret = Privkey::load_ed25519(&scalar)?; + let sig = secret.sign(digest, "", &mut rng)?; + + if sig.len() != size * 2 { + return Err(Error::MalformedMPI( + format!("Expected signature with length {}, got {}", + size * 2, sig.len())).into()); + } + + Ok(mpi::Signature::EdDSA { + r: MPI::new(&sig[..size]), + s: MPI::new(&sig[size..]), + }) + }, + _ => Err( + Error::UnsupportedEllipticCurve(curve.clone()).into()), + }, + + (ECDSA, + PublicKey::ECDSA { curve, .. }, + mpi::SecretKeyMaterial::ECDSA { scalar }) => { + let size = curve.field_size()?; + let secret = Privkey::load_ecdsa( + &scalar.try_into()?, curve.botan_name()?)?; + let sig = secret.sign(digest, "Raw", &mut rng)?; + + if sig.len() != size * 2 { + return Err(Error::MalformedMPI( + format!("Expected signature with length {}, got {}", + size * 2, sig.len())).into()); + } + + Ok(mpi::Signature::ECDSA { + r: MPI::new(&sig[..size]), + s: MPI::new(&sig[size..]), + }) + }, + + (pk_algo, _, _) => Err(Error::InvalidOperation(format!( + "unsupported combination of algorithm {:?}, key {:?}, \ + and secret key {:?}", + pk_algo, self.public(), self.secret())).into()), + }}) + } +} + +impl Decryptor for KeyPair { + fn public(&self) -> &Key<key::PublicParts, key::UnspecifiedRole> { + KeyPair::public(self) + } + + fn decrypt(&mut self, ciphertext: &mpi::Ciphertext, + plaintext_len: Option<usize>) + -> Result<SessionKey> + { + fn bad(e: impl ToString) -> anyhow::Error { + // XXX: Not a great error to return. + Error::MalformedMessage(e.to_string()).into() + } + + self.secret().map( + |secret| Ok(match (self.public().mpis(), secret, ciphertext) + { + (PublicKey::RSA { e, .. }, + mpi::SecretKeyMaterial::RSA { p, q, .. }, + mpi::Ciphertext::RSA { c }) => { + let secret = Privkey::load_rsa(&p.try_into()?, &q.try_into()?, + &e.try_into()?)?; + secret.decrypt(c.value(), "PKCS1v15")?.into() + }, + + (PublicKey::ElGamal{ p, g, .. }, + mpi::SecretKeyMaterial::ElGamal{ x }, + mpi::Ciphertext::ElGamal{ e, c }) => { + // OpenPGP encodes E and C separately, but our + // cryptographic library expects them to be + // concatenated. + let size = p.value().len(); + let mut ctxt = Vec::with_capacity(2 * size); + + // We need to zero-pad them at the front, because + // the MPI encoding drops leading zero bytes. + ctxt.extend_from_slice(&e.value_padded(size).map_err(bad)?); + ctxt.extend_from_slice(&c.value_padded(size).map_err(bad)?); + + let secret = + Privkey::load_elgamal(&p.try_into()?, &g.try_into()?, + &x.try_into()?)?; + secret.decrypt(&ctxt, "PKCS1v15")?.into() + }, + + (PublicKey::ECDH{ .. }, + mpi::SecretKeyMaterial::ECDH { .. }, + mpi::Ciphertext::ECDH { .. }) => + crate::crypto::ecdh::decrypt(self.public(), secret, ciphertext)?, + + (public, secret, ciphertext) => + return Err(Error::InvalidOperation(format!( + "unsupported combination of key pair {:?}/{:?} \ + and ciphertext {:?}", + public, secret, ciphertext)).into()), + })) + } +} + + +impl<P: key::KeyParts, R: key::KeyRole> Key<P, R> { + /// Encrypts the given data with this key. + pub fn encrypt(&self, data: &SessionKey) -> Result<mpi::Ciphertext> { + use crate::PublicKeyAlgorithm::*; + + #[allow(deprecated)] + match (self.pk_algo(), self.mpis()) { + (RSAEncryptSign, mpi::PublicKey::RSA { e, n }) | + (RSAEncrypt, mpi::PublicKey::RSA { e, n }) => { + // The ciphertext has the length of the modulus. + let ciphertext_len = n.value().len(); + if data.len() + 11 > ciphertext_len { + return Err(Error::InvalidArgument( + "Plaintext data too large".into()).into()); + } + + let mut rng = RandomNumberGenerator::new_userspace()?; + let pk = + Pubkey::load_rsa(&n.try_into()?, &e.try_into()?)?; + let esk = pk.encrypt(data, "PKCS1v15", &mut rng)?; + Ok(mpi::Ciphertext::RSA { + c: MPI::new(&esk), + }) + }, + + (ElGamalEncryptSign, mpi::PublicKey::ElGamal { p, g, y }) | + (ElGamalEncrypt, mpi::PublicKey::ElGamal { p, g, y }) => { + // OpenPGP encodes E and C separately, but our + // cryptographic library concatenates them. + let size = p.value().len(); + + let mut rng = RandomNumberGenerator::new_userspace()?; + let pk = + Pubkey::load_elgamal(&p.try_into()?, &g.try_into()?, + &y.try_into()?)?; + let esk = pk.encrypt(data, "PKCS1v15", &mut rng)?; + + if esk.len() != size * 2 { + return Err(Error::MalformedMPI( + format!("Expected ciphertext with length {}, got {}", + size * 2, esk.len())).into()); + } + + Ok(mpi::Ciphertext::ElGamal { + e: MPI::new(&esk[..size]), + c: MPI::new(&esk[size..]), + }) + }, + + (ECDH, mpi::PublicKey::ECDH { .. }) => + crate::crypto::ecdh::encrypt(self.parts_as_public(), data), + + _ => return Err(Error::MalformedPacket(format!( + "unsupported combination of key {} and mpis {:?}.", + self.pk_algo(), self.mpis())).into()), + } + } + + /// Verifies the given signature. + pub fn verify(&self, sig: &mpi::Signature, hash_algo: HashAlgorithm, + digest: &[u8]) -> Result<()> + { + use crate::crypto::mpi::Signature; + + fn bad(e: impl ToString) -> anyhow::Error { + Error::BadSignature(e.to_string()).into() + } + + let ok = match (self.mpis(), sig) { + (PublicKey::RSA { e, n }, Signature::RSA { s }) => { + let pk = Pubkey::load_rsa(&n.try_into()?, &e.try_into()?)?; + pk.verify(digest, s.value(), + &format!("PKCS1v15(Raw,{})", hash_algo.botan_name()?))? + }, + (PublicKey::DSA { y, q, p, g }, Signature::DSA { s, r }) => { + // OpenPGP encodes R and S separately, but our + // cryptographic library expects them to be + // concatenated. + let size = q.value().len(); + let mut sig = Vec::with_capacity(2 * size); + + // We need to zero-pad them at the front, because + // the MPI encoding drops leading zero bytes. + sig.extend_from_slice(&r.value_padded(size).map_err(bad)?); + sig.extend_from_slice(&s.value_padded(size).map_err(bad)?); + + let pk = Pubkey::load_dsa(&p.try_into()?, &q.try_into()?, + &g.try_into()?, &y.try_into()?)?; + let truncated_digest = &digest[..size.min(digest.len())]; + pk.verify(truncated_digest, &sig, "Raw").unwrap() + }, + (PublicKey::EdDSA { curve, q }, Signature::EdDSA { r, s }) => + match curve { + Curve::Ed25519 => { + if q.value().get(0).map(|&b| b != 0x40).unwrap_or(true) { + return Err(Error::MalformedPacket( + "Invalid point encoding".into()).into()); + } + + // OpenPGP encodes R and S separately, but our + // cryptographic library expects them to be + // concatenated. + let mut sig = Vec::with_capacity(64); + + // We need to zero-pad them at the front, because + // the MPI encoding drops leading zero bytes. + sig.extend_from_slice(&r.value_padded(32).map_err(bad)?); + sig.extend_from_slice(&s.value_padded(32).map_err(bad)?); + + let pk = Pubkey::load_ed25519(&q.value()[1..])?; + pk.verify(digest, &sig, "")? + }, + _ => return + Err(Error::UnsupportedEllipticCurve(curve.clone()).into()), + }, + (PublicKey::ECDSA { curve, q }, Signature::ECDSA { s, r }) => + { + // OpenPGP encodes R and S separately, but our + // cryptographic library expects them to be + // concatenated. + let size = curve.field_size()?; + let mut sig = Vec::with_capacity(2 * size); + + // We need to zero-pad them at the front, because + // the MPI encoding drops leading zero bytes. + sig.extend_from_slice(&r.value_padded(size).map_err(bad)?); + sig.extend_from_slice(&s.value_padded(size).map_err(bad)?); + + let (x, y) = q.decode_point(curve)?; + let pk = Pubkey::load_ecdsa(&botan::MPI::new_from_bytes(&x)?, + &botan::MPI::new_from_bytes(&y)?, + curve.botan_name()?)?; + pk.verify(digest, &sig, "Raw")? + }, + _ => return Err(Error::MalformedPacket(format!( + "unsupported combination of key {} and signature {:?}.", + self.pk_algo(), sig)).into()), + }; + + if ok { + Ok(()) + } else { + Err(Error::ManipulatedMessage.into()) + } + } +} + +impl<R> Key4<SecretParts, R> + where R: key::KeyRole, +{ + /// Creates a new OpenPGP secret key packet for an existing X25519 key. + /// + /// The ECDH key will use hash algorithm `hash` and symmetric + /// algorithm `sym`. If one or both are `None` secure defaults + /// will be used. The key will have it's creation date set to + /// `ctime` or the current time if `None` is given. + pub fn import_secret_cv25519<H, S, T>(private_key: &[u8], + hash: H, sym: S, ctime: T) + -> Result<Self> where H: Into<Option<HashAlgorithm>>, + S: Into<Option<SymmetricAlgorithm>>, + T: Into<Option<SystemTime>> + { + let secret = Privkey::load_x25519(private_key)?; + let public = secret.pubkey()?.get_x25519_key()?; + let mut secret = secret.get_x25519_key()?; + + // OpenPGP stores the secret in reverse order. + secret.reverse(); + + use crate::crypto::ecdh; + Self::with_secret( + ctime.into().unwrap_or_else(crate::now), + PublicKeyAlgorithm::ECDH, + mpi::PublicKey::ECDH { + curve: Curve::Cv25519, + hash: hash.into().unwrap_or_else( + || ecdh::default_ecdh_kdf_hash(&Curve::Cv25519)), + sym: sym.into().unwrap_or_else( + || ecdh::default_ecdh_kek_cipher(&Curve::Cv25519)), + q: MPI::new_compressed_point(&public), + }, + mpi::SecretKeyMaterial::ECDH { + scalar: secret.into(), + }.into()) + } + + /// Creates a new OpenPGP secret key packet for an existing Ed25519 key. + /// + /// The ECDH key will use hash algorithm `hash` and symmetric + /// algorithm `sym`. If one or both are `None` secure defaults + /// will be used. The key will have it's creation date set to + /// `ctime` or the current time if `None` is given. + pub fn import_secret_ed25519<T>(private_key: &[u8], ctime: T) + -> Result<Self> where T: Into<Option<SystemTime>> + { + let secret = Privkey::load_ed25519(private_key)?; + let (public, secret) = secret.get_ed25519_key()?; + + Self::with_secret( + ctime.into().unwrap_or_else(crate::now), + PublicKeyAlgorithm::EdDSA, + mpi::PublicKey::EdDSA { + curve: Curve::Ed25519, + q: MPI::new_compressed_point(&public), + }, + mpi::SecretKeyMaterial::EdDSA { + scalar: secret.into(), + }.into()) + } + + /// Creates a new OpenPGP public key packet for an existing RSA key. + /// + /// The RSA key will use public exponent `e` and modulo `n`. The key will + /// have it's creation date set to `ctime` or the current time if `None` + /// is given. + #[allow(clippy::many_single_char_names)] + pub fn import_secret_rsa<T>(d: &[u8], p: &[u8], q: &[u8], ctime: T) + -> Result<Self> where T: Into<Option<SystemTime>> + { + let d = botan::MPI::new_from_bytes(d)?; + let p = botan::MPI::new_from_bytes(p)?; + let q = botan::MPI::new_from_bytes(q)?; + + // Compute e ≡ d⁻¹ (mod 𝜙). + let phi = p.mp_sub_u32(1)?.mp_mul(&q.mp_sub_u32(1)?)?; + let e = botan::MPI::modular_inverse(&d, &phi)?; + + let secret = Privkey::load_rsa(&p.try_into()?, &q.try_into()?, + &e.try_into()?)?; + + let (public, secret) = rsa_rfc4880(secret)?; + Self::with_secret( + crate::now(), + PublicKeyAlgorithm::RSAEncryptSign, + public, secret.into()) + } + + /// Generates a new RSA key with a public modulos of size `bits`. + pub fn generate_rsa(bits: usize) -> Result<Self> { + let mut rng = RandomNumberGenerator::new_userspace()?; + let secret = Privkey::create("RSA", &format!("{}", bits), &mut rng)?; + + let (public, secret) = rsa_rfc4880(secret)?; + Self::with_secret( + crate::now(), + PublicKeyAlgorithm::RSAEncryptSign, + public, secret.into()) + } + + /// Generates a new ECC key over `curve`. + /// + /// If `for_signing` is false a ECDH key, if it's true either a + /// EdDSA or ECDSA key is generated. Giving `for_signing == true` and + /// `curve == Cv25519` will produce an error. Likewise + /// `for_signing == false` and `curve == Ed25519` will produce an error. + pub fn generate_ecc(for_signing: bool, curve: Curve) -> Result<Self> { + use crate::PublicKeyAlgorithm::*; + + let mut rng = RandomNumberGenerator::new_userspace()?; + let hash = crate::crypto::ecdh::default_ecdh_kdf_hash(&curve); + let sym = crate::crypto::ecdh::default_ecdh_kek_cipher(&curve); + let field_sz_bits = match curve { + Curve::Ed25519 => 256, // Handled differently. + Curve::Cv25519 => 256, // Handled differently. + Curve::NistP256 => 256, + Curve::NistP384 => 384, + Curve::NistP521 => 521, + _ => return + Err(Error::UnsupportedEllipticCurve(curve).into()), + }; + + let (mpis, secret, pk_algo) = match (curve.clone(), for_signing) { + (Curve::Ed25519, true) => { + let secret = Privkey::create("Ed25519", "", &mut rng)?; + let (public, secret) = secret.get_ed25519_key()?; + + let public_mpis = PublicKey::EdDSA { + curve: Curve::Ed25519, + q: MPI::new_compressed_point(&public), + }; + let private_mpis = mpi::SecretKeyMaterial::EdDSA { + scalar: secret.into(), + }; + + (public_mpis, private_mpis.into(), EdDSA) + }, + + (Curve::Cv25519, false) => { + let secret = Privkey::create("Curve25519", "", &mut rng)?; + let public = secret.pubkey()?.get_x25519_key()?; + let mut secret: Protected = secret.get_x25519_key()?.into(); + + // Clamp the scalar. X25519 does the clamping + // implicitly, but OpenPGP's ECDH over Curve25519 + // requires the secret to be clamped. + secret[0] &= 0b1111_1000; + secret[31] &= !0b1000_0000; + secret[31] |= 0b0100_0000; + + // Reverse the scalar. See + // https://lists.gnupg.org/pipermail/gnupg-devel/2018-February/033437.html. + secret.reverse(); + + let public_mpis = PublicKey::ECDH { + curve: Curve::Cv25519, + q: MPI::new_compressed_point(&public), + hash, + sym, + }; + let private_mpis = mpi::SecretKeyMaterial::ECDH { + scalar: secret.into(), + }; + + (public_mpis, private_mpis.into(), ECDH) + }, + + (Curve::NistP256, true) | + (Curve::NistP384, true) | + (Curve::NistP521, true) => { + let secret = Privkey::create("ECDSA", curve.botan_name()?, + &mut rng)?; + let public = secret.pubkey()?; + + let public_mpis = mpi::PublicKey::ECDSA { + curve, + q: MPI::new_point(&public.get_field("public_x")?.to_bin()?, + &public.get_field("public_y")?.to_bin()?, + field_sz_bits), + }; + let private_mpis = mpi::SecretKeyMaterial::ECDSA { + scalar: secret.get_field("x")?.try_into()?, + }; + + (public_mpis, private_mpis.into(), ECDSA) + }, + + (Curve::NistP256, false) | + (Curve::NistP384, false) | + (Curve::NistP521, false) => { + let secret = Privkey::create("ECDH", curve.botan_name()?, + &mut rng)?; + let public = secret.pubkey()?; + + let public_mpis = mpi::PublicKey::ECDH { + curve, + q: MPI::new_point(&public.get_field("public_x")?.to_bin()?, + &public.get_field("public_y")?.to_bin()?, + field_sz_bits), + hash, + sym, + }; + let private_mpis = mpi::SecretKeyMaterial::ECDH { + scalar: secret.get_field("x")?.try_into()?, + }; + + (public_mpis, private_mpis.into(), ECDH) + }, + + (cv, _) => { + return Err(Error::UnsupportedEllipticCurve(cv).into()); + } + }; + + Self::with_secret( + crate::now(), + pk_algo, + mpis, + secret) + } +} + +/// Returns an RSA secret key in the format that OpenPGP +/// expects. +fn rsa_rfc4880(secret: Privkey) -> Result<(mpi::PublicKey, + mpi::SecretKeyMaterial)> +{ + let public = secret.pubkey()?; + + let e = public.get_field("e")?; + let n = public.get_field("n")?; + let d = secret.get_field("d")?; + let p = secret.get_field("p")?; + let q = secret.get_field("q")?; + + let (p, q, u) = + if p.compare(&q)? == std::cmp::Ordering::Less { + let u = botan::MPI::modular_inverse(&p, &q)?; + (p, q, u) + } else { + let c = secret.get_field("c")?; + (q, p, c) + }; + + let public = mpi::PublicKey::RSA { + e: e.try_into()?, + n: n.try_into()?, + }; + let secret = mpi::SecretKeyMaterial::RSA { + d: d.try_into()?, + p: p.try_into()?, + q: q.try_into()?, + u: u.try_into()?, + }; + + Ok((public, secret)) +} + +impl Curve { + /// Returns the name of the algorithm for use with Botan's + /// constructor. + pub(crate) fn botan_name(&self) -> Result<&'static str> { + use Curve::*; + match self { + NistP256 => Ok("secp256r1"), + NistP384 => Ok("secp384r1"), + NistP521 => Ok("secp521r1"), + BrainpoolP256 => Ok("brainpool256r1"), + Unknown(_) if self.is_brainpoolp384() => Ok("brainpool384r1"), + BrainpoolP512 => Ok("brainpool512r1"), + Ed25519 | // Handled differently. + Cv25519 | // Handled differently. + Unknown(_) => + Err(Error::UnsupportedEllipticCurve(self.clone()).into()), + } + } +} |