From 3250cf27eb7e281e0befc294834b56920850428d Mon Sep 17 00:00:00 2001 From: Cindy Lin Date: Tue, 6 Feb 2024 16:14:15 -0800 Subject: [PATCH] Implement Hybrid Public Key Encryption in Rust. Change-Id: I52a179005f889f96c7ec621757666d02da7f434c Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/66048 Reviewed-by: Adam Langley Commit-Queue: Adam Langley --- rust/bssl-crypto/src/hpke.rs | 503 +++++++++++++++++++++++++++++++++ rust/bssl-crypto/src/lib.rs | 1 + rust/bssl-crypto/src/scoped.rs | 50 ++++ 3 files changed, 554 insertions(+) create mode 100644 rust/bssl-crypto/src/hpke.rs diff --git a/rust/bssl-crypto/src/hpke.rs b/rust/bssl-crypto/src/hpke.rs new file mode 100644 index 000000000..bd4e8fd38 --- /dev/null +++ b/rust/bssl-crypto/src/hpke.rs @@ -0,0 +1,503 @@ +/* Copyright (c) 2024, Google Inc. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +//! Hybrid Public Key Encryption +//! +//! HPKE provides a variant of public key encryption of arbitrary-sized plaintexts +//! for a recipient public key. It works for any combination of an asymmetric key +//! encapsulation mechanism (KEM), key derivation function (KDF), and authenticated +//! encryption with additional data (AEAD) function. +//! +//! See RFC 9180 for more details. +//! +//! Note that key generation is currently not supported. +//! +//! ``` +//! use bssl_crypto::hpke::{Params, RecipientContext, SenderContext}; +//! +//! let params = Params::new_from_rfc_ids(32, 1, 1).unwrap(); +//! let recipient_pub_key = ...; +//! let info = ...; +//! let mut sender_ctx = +//! SenderContext::new(¶ms, &recipient_pub_key, &info).unwrap(); +//! +//! let pt = b"plaintext"; +//! let ad = b"associated_data"; +//! let ct = sender_ctx.seal(pt, ad); +//! +//! let recipient_priv_key = ...; +//! let mut recipient_ctx = RecipientContext::new( +//! ¶ms, +//! &recipient_priv_key, +//! &sender_ctx.encapsulated_key(), +//! &info, +//! ).unwrap(); +//! +//! let got_pt = recipient_ctx.open(&ct, ad); +//! ``` + +use crate::{scoped, with_output_vec, with_output_vec_fallible, FfiSlice}; +use alloc::vec::Vec; + +/// Supported KEM algorithms with values detailed in RFC 9180. +#[derive(PartialEq)] +#[allow(missing_docs)] +pub enum Kem { + X25519HkdfSha256 = 32, +} + +/// Supported KDF algorithms with values detailed in RFC 9180. +#[derive(PartialEq)] +#[allow(missing_docs)] +pub enum Kdf { + HkdfSha256 = 1, +} + +/// Supported AEAD algorithms with values detailed in RFC 9180. +#[derive(PartialEq)] +#[allow(missing_docs)] +pub enum Aead { + Aes128Gcm = 1, +} + +/// Maximum length of the encapsulated key for all currently supported KEMs. +const MAX_ENCAPSULATED_KEY_LEN: usize = bssl_sys::EVP_HPKE_MAX_ENC_LENGTH as usize; + +/// HPKE parameters, including KEM, KDF, and AEAD. +pub struct Params { + kem: *const bssl_sys::EVP_HPKE_KEM, + kdf: *const bssl_sys::EVP_HPKE_KDF, + aead: *const bssl_sys::EVP_HPKE_AEAD, +} + +impl Params { + /// New Params from KEM, KDF, and AEAD enums. + pub fn new(kem: Kem, kdf: Kdf, aead: Aead) -> Option { + if kem != Kem::X25519HkdfSha256 || kdf != Kdf::HkdfSha256 || aead != Aead::Aes128Gcm { + return None; + } + // Safety: EVP_hpke_x25519_hkdf_sha256, EVP_hpke_hkdf_sha256, and EVP_hpke_aes_128_gcm + // initialize structs containing constants and cannot return an error. + unsafe { + Some(Self { + kem: bssl_sys::EVP_hpke_x25519_hkdf_sha256() as *const bssl_sys::EVP_HPKE_KEM, + kdf: bssl_sys::EVP_hpke_hkdf_sha256() as *const bssl_sys::EVP_HPKE_KDF, + aead: bssl_sys::EVP_hpke_aes_128_gcm() as *const bssl_sys::EVP_HPKE_AEAD, + }) + } + } + + /// New Params from KEM, KDF, and AEAD IDs as detailed in RFC 9180. + pub fn new_from_rfc_ids(kem: u16, kdf: u16, aead: u16) -> Option { + if kem != Kem::X25519HkdfSha256 as u16 + || kdf != Kdf::HkdfSha256 as u16 + || aead != Aead::Aes128Gcm as u16 + { + return None; + } + // Safety: EVP_hpke_x25519_hkdf_sha256, EVP_hpke_hkdf_sha256, and EVP_hpke_aes_128_gcm + // initialize structs containing constants and cannot return an error. + unsafe { + Some(Self { + kem: bssl_sys::EVP_hpke_x25519_hkdf_sha256() as *const bssl_sys::EVP_HPKE_KEM, + kdf: bssl_sys::EVP_hpke_hkdf_sha256() as *const bssl_sys::EVP_HPKE_KDF, + aead: bssl_sys::EVP_hpke_aes_128_gcm() as *const bssl_sys::EVP_HPKE_AEAD, + }) + } + } +} + +/// HPKE recipient context. Callers may use `open()` to decrypt messages from the sender. +pub struct RecipientContext { + ctx: scoped::EvpHpkeCtx, +} + +/// HPKE sender context. Callers may use `seal()` to encrypt messages for the recipient. +pub struct SenderContext { + ctx: RecipientContext, + encapsulated_key: Vec, +} + +impl SenderContext { + /// New implements the SetupBaseS HPKE operation, which encapsulates a shared secret for + /// `recipient_pub_key` and sets up a sender context. These are stored and returned in the + /// newly created SenderContext. + /// + /// Note that `recipient_pub_key` may be invalid, in which case this function will return an + /// error. + /// + /// On success, callers may use `seal()` to encrypt messages for the recipient. + pub fn new(params: &Params, recipient_pub_key: &[u8], info: &[u8]) -> Option { + let mut ctx = scoped::EvpHpkeCtx::new(); + unsafe { + with_output_vec_fallible(MAX_ENCAPSULATED_KEY_LEN, |enc_key_buf| { + let mut enc_key_len = 0usize; + // Safety: EVP_HPKE_CTX_setup_sender + // - is called with context created from EVP_HPKE_CTX_new, + // - is called with valid buffers with corresponding pointer and length, and + // - returns 0 on error. + let result = bssl_sys::EVP_HPKE_CTX_setup_sender( + ctx.as_mut_ffi_ptr(), + enc_key_buf, + &mut enc_key_len, + MAX_ENCAPSULATED_KEY_LEN, + params.kem, + params.kdf, + params.aead, + recipient_pub_key.as_ffi_ptr(), + recipient_pub_key.len(), + info.as_ffi_ptr(), + info.len(), + ); + if result == 1 { + Some(enc_key_len) + } else { + None + } + }) + } + .map(|enc_key| Self { + ctx: RecipientContext { ctx }, + encapsulated_key: enc_key, + }) + } + + /// Seal encrypts `pt` and returns the resulting ciphertext, which is authenticated with `aad`. + /// + /// Note that HPKE encryption is stateful and ordered. The sender's first call to `seal()` must + /// correspond to the recipient's first call to `open()`, etc. + /// + /// This function panics if adding the `pt` length and bssl_sys::EVP_HPKE_CTX_max_overhead + /// overflows. + pub fn seal(&mut self, pt: &[u8], aad: &[u8]) -> Vec { + self.ctx.seal(pt, aad) + } + + #[allow(missing_docs)] + pub fn encapsulated_key(&self) -> &[u8] { + &self.encapsulated_key + } +} + +impl RecipientContext { + /// New implements the SetupBaseR HPKE operation, which decapsulates the shared secret in + /// `encapsulated_key` with `recipient_priv_key` and sets up a recipient context. These are + /// stored and returned in the newly created RecipientContext. + /// + /// Note that `encapsulated_key` may be invalid, in which case this function will return an + /// error. + /// + /// On success, callers may use `open()` to decrypt messages from the sender. + pub fn new( + params: &Params, + recipient_priv_key: &[u8], + encapsulated_key: &[u8], + info: &[u8], + ) -> Option { + let mut hpke_key = scoped::EvpHpkeKey::new(); + + // Safety: EVP_HPKE_KEY_init returns 0 on error. + let result = unsafe { + bssl_sys::EVP_HPKE_KEY_init( + hpke_key.as_mut_ffi_ptr(), + params.kem, + recipient_priv_key.as_ffi_ptr(), + recipient_priv_key.len(), + ) + }; + if result != 1 { + return None; + } + + let mut ctx = scoped::EvpHpkeCtx::new(); + + // Safety: EVP_HPKE_CTX_setup_recipient + // - is called with context created from EVP_HPKE_CTX_new, + // - is called with HPKE key created from EVP_HPKE_KEY_init, + // - is called with valid buffers with corresponding pointer and length, and + // - returns 0 on error. + let result = unsafe { + bssl_sys::EVP_HPKE_CTX_setup_recipient( + ctx.as_mut_ffi_ptr(), + hpke_key.as_ffi_ptr(), + params.kdf, + params.aead, + encapsulated_key.as_ffi_ptr(), + encapsulated_key.len(), + info.as_ffi_ptr(), + info.len(), + ) + }; + if result == 1 { + Some(Self { ctx }) + } else { + None + } + } + + /// Seal encrypts `pt` and returns the resulting ciphertext, which is authenticated with `aad`. + /// + /// Note that HPKE encryption is stateful and ordered. The sender's first call to `seal()` must + /// correspond to the recipient's first call to `open()`, etc. + /// + /// This function panics if adding the `pt` length and bssl_sys::EVP_HPKE_CTX_max_overhead + /// overflows. + pub fn seal(&mut self, pt: &[u8], aad: &[u8]) -> Vec { + // Safety: EVP_HPKE_CTX_max_overhead panics if ctx is not set up as a sender. + #[allow(clippy::expect_used)] + let max_out_len = pt + .len() + .checked_add(unsafe { bssl_sys::EVP_HPKE_CTX_max_overhead(self.ctx.as_mut_ffi_ptr()) }) + .expect("Maximum output length calculation overflow"); + unsafe { + with_output_vec(max_out_len, |out_buf| { + let mut out_len = 0usize; + // Safety: EVP_HPKE_CTX_seal + // - is called with context created from EVP_HPKE_CTX_new and + // - is called with valid buffers with corresponding pointer and length. + let result = bssl_sys::EVP_HPKE_CTX_seal( + self.ctx.as_mut_ffi_ptr(), + out_buf, + &mut out_len, + max_out_len, + pt.as_ffi_ptr(), + pt.len(), + aad.as_ffi_ptr(), + aad.len(), + ); + assert_eq!(result, 1); + out_len + }) + } + } + + /// Open authenticates `aad` and decrypts `ct`. It returns an error on failure. + /// + /// Note that HPKE encryption is stateful and ordered. The sender's first call to `seal()` must + /// correspond to the recipient's first call to `open()`, etc. + pub fn open(&mut self, ct: &[u8], aad: &[u8]) -> Option> { + let max_out_len = ct.len(); + unsafe { + with_output_vec_fallible(max_out_len, |out_buf| { + let mut out_len = 0usize; + // Safety: EVP_HPKE_CTX_open + // - is called with context created from EVP_HPKE_CTX_new and + // - is called with valid buffers with corresponding pointer and length. + let result = bssl_sys::EVP_HPKE_CTX_open( + self.ctx.as_mut_ffi_ptr(), + out_buf, + &mut out_len, + max_out_len, + ct.as_ffi_ptr(), + ct.len(), + aad.as_ffi_ptr(), + aad.len(), + ); + if result == 1 { + Some(out_len) + } else { + None + } + }) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_helpers::decode_hex; + + struct TestVector { + kem_id: u16, + kdf_id: u16, + aead_id: u16, + info: [u8; 20], + seed_for_testing: [u8; 32], // skEm + recipient_pub_key: [u8; 32], // pkRm + recipient_priv_key: [u8; 32], // skRm + encapsulated_key: [u8; 32], // enc + plaintext: [u8; 29], // pt + associated_data: [u8; 7], // aad + ciphertext: [u8; 45], // ct + } + + // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1 + fn x25519_hkdf_sha256_hkdf_sha256_aes_128_gcm() -> TestVector { + TestVector { + kem_id: 32, + kdf_id: 1, + aead_id: 1, + info: decode_hex("4f6465206f6e2061204772656369616e2055726e"), + seed_for_testing: decode_hex("52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736"), + recipient_pub_key: decode_hex("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d"), + recipient_priv_key: decode_hex("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8"), + encapsulated_key: decode_hex("37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431"), + plaintext: decode_hex("4265617574792069732074727574682c20747275746820626561757479"), + associated_data: decode_hex("436f756e742d30"), + ciphertext: decode_hex("f938558b5d72f1a23810b4be2ab4f84331acc02fc97babc53a52ae8218a355a96d8770ac83d07bea87e13c512a"), + } + } + + #[test] + fn seal_and_open() { + let vec: TestVector = x25519_hkdf_sha256_hkdf_sha256_aes_128_gcm(); + let params = Params::new_from_rfc_ids(vec.kem_id, vec.kdf_id, vec.aead_id).unwrap(); + + let mut sender_ctx = + SenderContext::new(¶ms, &vec.recipient_pub_key, &vec.info).unwrap(); + + let mut recipient_ctx = RecipientContext::new( + ¶ms, + &vec.recipient_priv_key, + &sender_ctx.encapsulated_key(), + &vec.info, + ) + .unwrap(); + + let pt = b"plaintext"; + let ad = b"associated_data"; + let mut prev_ct: Vec = Vec::new(); + for _ in 0..10 { + let ct = sender_ctx.seal(pt, ad); + assert_ne!(ct, prev_ct); + prev_ct = ct.clone(); + + let got_pt = recipient_ctx.open(&ct, ad).unwrap(); + assert_eq!(got_pt, pt); + } + } + + fn new_sender_context_for_testing( + params: &Params, + recipient_pub_key: &[u8], + info: &[u8], + seed_for_testing: &[u8], + ) -> Option { + let mut ctx = scoped::EvpHpkeCtx::new(); + + unsafe { + with_output_vec_fallible(MAX_ENCAPSULATED_KEY_LEN, |enc_key_buf| { + let mut enc_key_len = 0usize; + // Safety: EVP_HPKE_CTX_setup_sender_with_seed_for_testing + // - is called with context created from EVP_HPKE_CTX_new, + // - is called with valid buffers with corresponding pointer and length, and + // - returns 0 on error. + let result = bssl_sys::EVP_HPKE_CTX_setup_sender_with_seed_for_testing( + ctx.as_mut_ffi_ptr(), + enc_key_buf, + &mut enc_key_len, + MAX_ENCAPSULATED_KEY_LEN, + params.kem, + params.kdf, + params.aead, + recipient_pub_key.as_ffi_ptr(), + recipient_pub_key.len(), + info.as_ffi_ptr(), + info.len(), + seed_for_testing.as_ffi_ptr(), + seed_for_testing.len(), + ); + if result == 1 { + Some(enc_key_len) + } else { + None + } + }) + } + .map(|enc_key| SenderContext { + ctx: RecipientContext { ctx }, + encapsulated_key: enc_key, + }) + } + + #[test] + fn seal_with_vector() { + let vec: TestVector = x25519_hkdf_sha256_hkdf_sha256_aes_128_gcm(); + let params = Params::new_from_rfc_ids(vec.kem_id, vec.kdf_id, vec.aead_id).unwrap(); + + let mut ctx = new_sender_context_for_testing( + ¶ms, + &vec.recipient_pub_key, + &vec.info, + &vec.seed_for_testing, + ) + .unwrap(); + + assert_eq!(ctx.encapsulated_key, vec.encapsulated_key.to_vec()); + + let ciphertext = ctx.seal(&vec.plaintext, &vec.associated_data); + assert_eq!(ciphertext, vec.ciphertext.to_vec()); + } + + #[test] + fn open_with_vector() { + let vec: TestVector = x25519_hkdf_sha256_hkdf_sha256_aes_128_gcm(); + let params = Params::new_from_rfc_ids(vec.kem_id, vec.kdf_id, vec.aead_id).unwrap(); + + let mut ctx = RecipientContext::new( + ¶ms, + &vec.recipient_priv_key, + &vec.encapsulated_key, + &vec.info, + ) + .unwrap(); + + let plaintext = ctx.open(&vec.ciphertext, &vec.associated_data).unwrap(); + assert_eq!(plaintext, vec.plaintext.to_vec()); + } + + #[test] + fn params_new() { + assert!(Params::new(Kem::X25519HkdfSha256, Kdf::HkdfSha256, Aead::Aes128Gcm).is_some()); + } + + #[test] + fn params_new_from_rfc_ids() { + let vec: TestVector = x25519_hkdf_sha256_hkdf_sha256_aes_128_gcm(); + assert!(Params::new_from_rfc_ids(vec.kem_id, vec.kdf_id, vec.aead_id).is_some()); + } + + #[test] + fn disallowed_params_fail() { + let vec: TestVector = x25519_hkdf_sha256_hkdf_sha256_aes_128_gcm(); + + assert!(Params::new_from_rfc_ids(0, vec.kdf_id, vec.aead_id).is_none()); + assert!(Params::new_from_rfc_ids(vec.kem_id, 0, vec.aead_id).is_none()); + assert!(Params::new_from_rfc_ids(vec.kem_id, vec.kdf_id, 0).is_none()); + assert!(Params::new_from_rfc_ids( + vec.kem_id, + vec.kdf_id, + bssl_sys::EVP_HPKE_AES_256_GCM as u16 + ) + .is_none()); + } + + #[test] + fn bad_recipient_pub_key_fails() { + let vec: TestVector = x25519_hkdf_sha256_hkdf_sha256_aes_128_gcm(); + let params = Params::new_from_rfc_ids(vec.kem_id, vec.kdf_id, vec.aead_id).unwrap(); + + assert!(SenderContext::new(¶ms, b"", &vec.info).is_none()); + } + + #[test] + fn bad_recipient_priv_key_fails() { + let vec: TestVector = x25519_hkdf_sha256_hkdf_sha256_aes_128_gcm(); + let params = Params::new_from_rfc_ids(vec.kem_id, vec.kdf_id, vec.aead_id).unwrap(); + + assert!(RecipientContext::new(¶ms, b"", &vec.encapsulated_key, &vec.info).is_none()); + } +} diff --git a/rust/bssl-crypto/src/lib.rs b/rust/bssl-crypto/src/lib.rs index 5a6e58afb..42ae7fc7a 100644 --- a/rust/bssl-crypto/src/lib.rs +++ b/rust/bssl-crypto/src/lib.rs @@ -47,6 +47,7 @@ pub mod ecdsa; pub mod ed25519; pub mod hkdf; pub mod hmac; +pub mod hpke; pub mod rsa; pub mod x25519; diff --git a/rust/bssl-crypto/src/scoped.rs b/rust/bssl-crypto/src/scoped.rs index 391a0c6af..17d331a1e 100644 --- a/rust/bssl-crypto/src/scoped.rs +++ b/rust/bssl-crypto/src/scoped.rs @@ -67,6 +67,56 @@ impl Drop for EcKey { } } +/// A scoped `EVP_HPKE_CTX`. +pub struct EvpHpkeCtx(*mut bssl_sys::EVP_HPKE_CTX); + +impl EvpHpkeCtx { + pub fn new() -> Self { + let ptr = unsafe { bssl_sys::EVP_HPKE_CTX_new() }; + // `ptr` is only NULL if we're out of memory, which this crate + // doesn't handle. + assert!(!ptr.is_null()); + EvpHpkeCtx(ptr) + } + + pub fn as_mut_ffi_ptr(&mut self) -> *mut bssl_sys::EVP_HPKE_CTX { + self.0 + } +} + +impl Drop for EvpHpkeCtx { + fn drop(&mut self) { + unsafe { bssl_sys::EVP_HPKE_CTX_free(self.0) } + } +} + +/// A scoped `EVP_HPKE_KEY`. +pub struct EvpHpkeKey(*mut bssl_sys::EVP_HPKE_KEY); + +impl EvpHpkeKey { + pub fn new() -> Self { + let ptr = unsafe { bssl_sys::EVP_HPKE_KEY_new() }; + // `ptr` is only NULL if we're out of memory, which this crate + // doesn't handle. + assert!(!ptr.is_null()); + EvpHpkeKey(ptr) + } + + pub fn as_ffi_ptr(&mut self) -> *const bssl_sys::EVP_HPKE_KEY { + self.0 + } + + pub fn as_mut_ffi_ptr(&mut self) -> *mut bssl_sys::EVP_HPKE_KEY { + self.0 + } +} + +impl Drop for EvpHpkeKey { + fn drop(&mut self) { + unsafe { bssl_sys::EVP_HPKE_KEY_free(self.0) } + } +} + /// A scoped `BIGNUM`. pub struct Bignum(bssl_sys::BIGNUM);