Tidy up Rust HPKE binding.

The doc test didn't compile and needs support for generating keys in
order to be a good example. Also bind all the AEADs that we support, and
have the encapsulated key be a second return value when creating a
sender, rather than carrying it in memory for the whole lifetime.

Change-Id: I1533560a925d4e239eedbfa0f4213d9e79085b77
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/66527
Reviewed-by: Bob Beck <bbe@google.com>
Commit-Queue: Adam Langley <agl@google.com>
diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c
index ff8b17b..9fe4c3c 100644
--- a/crypto/hpke/hpke.c
+++ b/crypto/hpke/hpke.c
@@ -355,6 +355,8 @@
 void EVP_HPKE_KEY_move(EVP_HPKE_KEY *out, EVP_HPKE_KEY *in) {
   EVP_HPKE_KEY_cleanup(out);
   // For now, |EVP_HPKE_KEY| is trivially movable.
+  // Note that Rust may move this structure. See
+  // bssl-crypto/src/scoped.rs:EvpHpkeKey.
   OPENSSL_memcpy(out, in, sizeof(EVP_HPKE_KEY));
   EVP_HPKE_KEY_zero(in);
 }
diff --git a/rust/bssl-crypto/src/hpke.rs b/rust/bssl-crypto/src/hpke.rs
index bd4e8fd..2725b3f 100644
--- a/rust/bssl-crypto/src/hpke.rs
+++ b/rust/bssl-crypto/src/hpke.rs
@@ -15,61 +15,168 @@
 
 //! 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.
+//! HPKE provides public key encryption of arbitrary-length messages. It
+//! establishes contexts that produce/consume an ordered sequence of
+//! ciphertexts that are both encrypted and authenticated.
 //!
 //! See RFC 9180 for more details.
 //!
-//! Note that key generation is currently not supported.
-//!
 //! ```
-//! use bssl_crypto::hpke::{Params, RecipientContext, SenderContext};
+//! use bssl_crypto::hpke;
 //!
-//! let params = Params::new_from_rfc_ids(32, 1, 1).unwrap();
-//! let recipient_pub_key = ...;
-//! let info = ...;
-//! let mut sender_ctx =
-//!     SenderContext::new(&params, &recipient_pub_key, &info).unwrap();
+//! let kem = hpke::Kem::X25519HkdfSha256;
+//! let (pub_key, priv_key) = kem.generate_keypair();
+//! // Distribute `pub_key` to people who want to send you messages.
 //!
-//! let pt = b"plaintext";
-//! let ad = b"associated_data";
-//! let ct = sender_ctx.seal(pt, ad);
+//! // On the sending side...
+//! let params = hpke::Params::new(kem, hpke::Kdf::HkdfSha256, hpke::Aead::Aes128Gcm);
+//! let info : &[u8] = b"mutual context";
+//! let (mut sender_ctx, encapsulated_key) =
+//!     hpke::SenderContext::new(&params, &pub_key, info).unwrap();
+//! // Transmit the `encapsulated_key` to the receiver, followed by one or
+//! // more ciphertexts...
+//! let aad = b"associated_data";
+//! let plaintext1 : &[u8] = b"plaintext1";
+//! let msg1 = sender_ctx.seal(plaintext1, aad);
+//! let plaintext2 : &[u8] = b"plaintext2";
+//! let msg2 = sender_ctx.seal(plaintext2, aad);
 //!
-//! let recipient_priv_key = ...;
-//! let mut recipient_ctx = RecipientContext::new(
+//! // On the receiving side...
+//! let mut recipient_ctx = hpke::RecipientContext::new(
 //!     &params,
-//!     &recipient_priv_key,
-//!     &sender_ctx.encapsulated_key(),
-//!     &info,
+//!     &priv_key,
+//!     &encapsulated_key,
+//!     info,
 //! ).unwrap();
 //!
-//! let got_pt = recipient_ctx.open(&ct, ad);
+//! let received_plaintext1 = recipient_ctx.open(&msg1, aad).unwrap();
+//! assert_eq!(plaintext1, &received_plaintext1);
+//! let received_plaintext2 = recipient_ctx.open(&msg2, aad).unwrap();
+//! assert_eq!(plaintext2, &received_plaintext2);
+//!
+//! // Messages must be processed in order, so trying to `open` the second
+//! // message first will fail.
+//! let mut recipient_ctx = hpke::RecipientContext::new(
+//!     &params,
+//!     &priv_key,
+//!     &encapsulated_key,
+//!     info,
+//! ).unwrap();
+//!
+//! let received_plaintext2 = recipient_ctx.open(&msg2, aad);
+//! assert!(received_plaintext2.is_none());
 //! ```
 
 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)]
+#[derive(Clone, Copy)]
 pub enum Kem {
+    #[allow(missing_docs)]
     X25519HkdfSha256 = 32,
 }
 
+impl Kem {
+    fn as_ffi_ptr(&self) -> *const bssl_sys::EVP_HPKE_KEM {
+        // Safety: this function returns a pointer to static data.
+        unsafe {
+            match self {
+                Kem::X25519HkdfSha256 => bssl_sys::EVP_hpke_x25519_hkdf_sha256(),
+            }
+        }
+    }
+
+    /// Generate a public and private key for this KEM.
+    pub fn generate_keypair(&self) -> (Vec<u8>, Vec<u8>) {
+        let mut key = scoped::EvpHpkeKey::new();
+        // Safety: `key` and `self` must be valid and this function doesn't
+        // take ownership of either.
+        let ret =
+            unsafe { bssl_sys::EVP_HPKE_KEY_generate(key.as_mut_ffi_ptr(), self.as_ffi_ptr()) };
+        // Key generation currently never fails, and out-of-memory is not
+        // handled by this crate.
+        assert_eq!(ret, 1);
+
+        fn get_value_from_key(
+            key: &scoped::EvpHpkeKey,
+            accessor: unsafe extern "C" fn(
+                *const bssl_sys::EVP_HPKE_KEY,
+                // Output buffer.
+                *mut u8,
+                // Number of bytes written.
+                *mut usize,
+                // Maximum output size.
+                usize,
+            ) -> core::ffi::c_int,
+            max_len: usize,
+        ) -> Vec<u8> {
+            unsafe {
+                with_output_vec(max_len, |out| {
+                    let mut out_len = 0usize;
+                    let ret = accessor(key.as_ffi_ptr(), out, &mut out_len, max_len);
+                    // If `max_len` is correct then these functions never fail.
+                    assert_eq!(ret, 1);
+                    assert!(out_len <= max_len);
+                    // Safety: `out_len` bytes have been written, as required.
+                    out_len
+                })
+            }
+        }
+
+        let pub_key = get_value_from_key(
+            &key,
+            bssl_sys::EVP_HPKE_KEY_public_key,
+            bssl_sys::EVP_HPKE_MAX_PUBLIC_KEY_LENGTH as usize,
+        );
+        let priv_key = get_value_from_key(
+            &key,
+            bssl_sys::EVP_HPKE_KEY_private_key,
+            bssl_sys::EVP_HPKE_MAX_PRIVATE_KEY_LENGTH as usize,
+        );
+        (pub_key, priv_key)
+    }
+}
+
 /// Supported KDF algorithms with values detailed in RFC 9180.
-#[derive(PartialEq)]
-#[allow(missing_docs)]
+#[derive(Clone, Copy)]
 pub enum Kdf {
+    #[allow(missing_docs)]
     HkdfSha256 = 1,
 }
 
 /// Supported AEAD algorithms with values detailed in RFC 9180.
-#[derive(PartialEq)]
+#[derive(Clone, Copy)]
 #[allow(missing_docs)]
 pub enum Aead {
     Aes128Gcm = 1,
+    Aes256Gcm = 2,
+    Chacha20Poly1305 = 3,
+}
+
+impl Aead {
+    fn from_rfc_id(n: u16) -> Option<Aead> {
+        let ret = match n {
+            1 => Aead::Aes128Gcm,
+            2 => Aead::Aes256Gcm,
+            3 => Aead::Chacha20Poly1305,
+            _ => return None,
+        };
+        // The mapping above must agree with the values in the enum.
+        assert_eq!(n, ret as u16);
+        Some(ret)
+    }
+
+    fn as_ffi_ptr(&self) -> *const bssl_sys::EVP_HPKE_AEAD {
+        // Safety: these functions all return pointers to static data.
+        unsafe {
+            match self {
+                Aead::Aes128Gcm => bssl_sys::EVP_hpke_aes_128_gcm(),
+                Aead::Aes256Gcm => bssl_sys::EVP_hpke_aes_256_gcm(),
+                Aead::Chacha20Poly1305 => bssl_sys::EVP_hpke_chacha20_poly1305(),
+            }
+        }
+    }
 }
 
 /// Maximum length of the encapsulated key for all currently supported KEMs.
@@ -83,63 +190,44 @@
 }
 
 impl Params {
-    /// New Params from KEM, KDF, and AEAD enums.
-    pub fn new(kem: Kem, kdf: Kdf, aead: Aead) -> Option<Self> {
-        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.
+    /// New `Params` from KEM, KDF, and AEAD enums.
+    pub fn new(_kem: Kem, _kdf: Kdf, aead: Aead) -> Self {
+        // Safety: EVP_hpke_x25519_hkdf_sha256 and EVP_hpke_hkdf_sha256 just
+        // return pointers to static data.
         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,
-            })
+            Self {
+                // Only one KEM and KDF are supported thus far.
+                kem: bssl_sys::EVP_hpke_x25519_hkdf_sha256(),
+                kdf: bssl_sys::EVP_hpke_hkdf_sha256(),
+                aead: aead.as_ffi_ptr(),
+            }
         }
     }
 
-    /// 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<Self> {
-        if kem != Kem::X25519HkdfSha256 as u16
-            || kdf != Kdf::HkdfSha256 as u16
-            || aead != Aead::Aes128Gcm as u16
-        {
+    /// New `Params` from KEM, KDF, and AEAD IDs as detailed in RFC 9180.
+    pub fn new_from_rfc_ids(kem_id: u16, kdf_id: u16, aead_id: u16) -> Option<Self> {
+        let kem = Kem::X25519HkdfSha256;
+        let kdf = Kdf::HkdfSha256;
+        let aead = Aead::from_rfc_id(aead_id)?;
+
+        if kem_id != kem as u16 || kdf_id != kdf 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,
-            })
-        }
+        Some(Self::new(kem, kdf, 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<u8>,
-}
+pub struct SenderContext(scoped::EvpHpkeCtx);
 
 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.
+    /// Performs the SetupBaseS HPKE operation and returns a sender context
+    /// plus an encapsulated shared secret for `recipient_pub_key`.
     ///
-    /// Note that `recipient_pub_key` may be invalid, in which case this function will return an
-    /// error.
+    /// Returns `None` if `recipient_pub_key` is invalid.
     ///
     /// On success, callers may use `seal()` to encrypt messages for the recipient.
-    pub fn new(params: &Params, recipient_pub_key: &[u8], info: &[u8]) -> Option<Self> {
+    pub fn new(params: &Params, recipient_pub_key: &[u8], info: &[u8]) -> Option<(Self, Vec<u8>)> {
         let mut ctx = scoped::EvpHpkeCtx::new();
         unsafe {
             with_output_vec_fallible(MAX_ENCAPSULATED_KEY_LEN, |enc_key_buf| {
@@ -148,7 +236,7 @@
                 // - 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(
+                let ret = bssl_sys::EVP_HPKE_CTX_setup_sender(
                     ctx.as_mut_ffi_ptr(),
                     enc_key_buf,
                     &mut enc_key_len,
@@ -161,36 +249,56 @@
                     info.as_ffi_ptr(),
                     info.len(),
                 );
-                if result == 1 {
+                if ret == 1 {
                     Some(enc_key_len)
                 } else {
                     None
                 }
             })
         }
-        .map(|enc_key| Self {
-            ctx: RecipientContext { ctx },
-            encapsulated_key: enc_key,
-        })
+        .map(|enc_key| (Self(ctx), enc_key))
     }
 
-    /// Seal encrypts `pt` and returns the resulting ciphertext, which is authenticated with `aad`.
+    /// Seal encrypts `plaintext`, and authenticates `aad`, returning the resulting ciphertext.
     ///
     /// 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<u8> {
-        self.ctx.seal(pt, aad)
-    }
-
-    #[allow(missing_docs)]
-    pub fn encapsulated_key(&self) -> &[u8] {
-        &self.encapsulated_key
+    /// This function panics if adding the `plaintext` length and
+    /// `bssl_sys::EVP_HPKE_CTX_max_overhead` overflows.
+    pub fn seal(&mut self, plaintext: &[u8], aad: &[u8]) -> Vec<u8> {
+        // Safety: EVP_HPKE_CTX_max_overhead panics if ctx is not set up as a sender.
+        #[allow(clippy::expect_used)]
+        let max_out_len = plaintext
+            .len()
+            .checked_add(unsafe { bssl_sys::EVP_HPKE_CTX_max_overhead(self.0.as_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.0.as_mut_ffi_ptr(),
+                    out_buf,
+                    &mut out_len,
+                    max_out_len,
+                    plaintext.as_ffi_ptr(),
+                    plaintext.len(),
+                    aad.as_ffi_ptr(),
+                    aad.len(),
+                );
+                assert_eq!(result, 1);
+                out_len
+            })
+        }
     }
 }
 
+/// HPKE recipient context. Callers may use `open()` to decrypt messages from the sender.
+pub struct RecipientContext(scoped::EvpHpkeCtx);
+
 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
@@ -241,54 +349,18 @@
             )
         };
         if result == 1 {
-            Some(Self { ctx })
+            Some(Self(ctx))
         } else {
             None
         }
     }
 
-    /// Seal encrypts `pt` and returns the resulting ciphertext, which is authenticated with `aad`.
+    /// Open authenticates `aad` and decrypts `ciphertext`. 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.
-    ///
-    /// 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<u8> {
-        // 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<Vec<u8>> {
-        let max_out_len = ct.len();
+    pub fn open(&mut self, ciphertext: &[u8], aad: &[u8]) -> Option<Vec<u8>> {
+        let max_out_len = ciphertext.len();
         unsafe {
             with_output_vec_fallible(max_out_len, |out_buf| {
                 let mut out_len = 0usize;
@@ -296,12 +368,12 @@
                 // - 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(),
+                    self.0.as_mut_ffi_ptr(),
                     out_buf,
                     &mut out_len,
                     max_out_len,
-                    ct.as_ffi_ptr(),
-                    ct.len(),
+                    ciphertext.as_ffi_ptr(),
+                    ciphertext.len(),
                     aad.as_ffi_ptr(),
                     aad.len(),
                 );
@@ -351,32 +423,58 @@
         }
     }
 
+    // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.2
+    fn x25519_hkdf_sha256_hkdf_sha256_chacha20_poly1305() -> TestVector {
+        TestVector {
+            kem_id: 32,
+            kdf_id: 1,
+            aead_id: 3,
+            info: decode_hex("4f6465206f6e2061204772656369616e2055726e"),
+            seed_for_testing: decode_hex("f4ec9b33b792c372c1d2c2063507b684ef925b8c75a42dbcbf57d63ccd381600"),
+            recipient_pub_key: decode_hex("4310ee97d88cc1f088a5576c77ab0cf5c3ac797f3d95139c6c84b5429c59662a"),
+            recipient_priv_key: decode_hex("8057991eef8f1f1af18f4a9491d16a1ce333f695d4db8e38da75975c4478e0fb"),
+            encapsulated_key: decode_hex("1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a"),
+            plaintext: decode_hex("4265617574792069732074727574682c20747275746820626561757479"),
+            associated_data: decode_hex("436f756e742d30"),
+            ciphertext: decode_hex("1c5250d8034ec2b784ba2cfd69dbdb8af406cfe3ff938e131f0def8c8b60b4db21993c62ce81883d2dd1b51a28"),
+        }
+    }
+
     #[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();
+    fn all_algorithms() {
+        let kems = vec![Kem::X25519HkdfSha256];
+        let kdfs = vec![Kdf::HkdfSha256];
+        let aeads = vec![Aead::Aes128Gcm, Aead::Aes256Gcm, Aead::Chacha20Poly1305];
+        let plaintext: &[u8] = b"plaintext";
+        let aad: &[u8] = b"aad";
+        let info: &[u8] = b"info";
 
-        let mut sender_ctx =
-            SenderContext::new(&params, &vec.recipient_pub_key, &vec.info).unwrap();
+        for kem in &kems {
+            let (pub_key, priv_key) = kem.generate_keypair();
+            for kdf in &kdfs {
+                for aead in &aeads {
+                    let params =
+                        Params::new_from_rfc_ids(*kem as u16, *kdf as u16, *aead as u16).unwrap();
 
-        let mut recipient_ctx = RecipientContext::new(
-            &params,
-            &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<u8> = 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);
+                    let (mut send_ctx, encapsulated_key) =
+                        SenderContext::new(&params, &pub_key, info).unwrap();
+                    let mut recv_ctx =
+                        RecipientContext::new(&params, &priv_key, &encapsulated_key, info).unwrap();
+                    assert_eq!(
+                        plaintext,
+                        recv_ctx
+                            .open(send_ctx.seal(plaintext, aad).as_ref(), aad)
+                            .unwrap()
+                    );
+                    assert_eq!(
+                        plaintext,
+                        recv_ctx
+                            .open(send_ctx.seal(plaintext, aad).as_ref(), aad)
+                            .unwrap()
+                    );
+                    assert!(recv_ctx.open(b"nonsense", aad).is_none());
+                }
+            }
         }
     }
 
@@ -385,10 +483,10 @@
         recipient_pub_key: &[u8],
         info: &[u8],
         seed_for_testing: &[u8],
-    ) -> Option<SenderContext> {
+    ) -> (SenderContext, Vec<u8>) {
         let mut ctx = scoped::EvpHpkeCtx::new();
 
-        unsafe {
+        let encapsulated_key = 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
@@ -417,57 +515,51 @@
                 }
             })
         }
-        .map(|enc_key| SenderContext {
-            ctx: RecipientContext { ctx },
-            encapsulated_key: enc_key,
-        })
+        .unwrap();
+        (SenderContext(ctx), encapsulated_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();
+        for test in vec![
+            x25519_hkdf_sha256_hkdf_sha256_aes_128_gcm(),
+            x25519_hkdf_sha256_hkdf_sha256_chacha20_poly1305(),
+        ] {
+            let params = Params::new_from_rfc_ids(test.kem_id, test.kdf_id, test.aead_id).unwrap();
 
-        let mut ctx = new_sender_context_for_testing(
-            &params,
-            &vec.recipient_pub_key,
-            &vec.info,
-            &vec.seed_for_testing,
-        )
-        .unwrap();
+            let (mut ctx, encapsulated_key) = new_sender_context_for_testing(
+                &params,
+                &test.recipient_pub_key,
+                &test.info,
+                &test.seed_for_testing,
+            );
 
-        assert_eq!(ctx.encapsulated_key, vec.encapsulated_key.to_vec());
+            assert_eq!(encapsulated_key, test.encapsulated_key.to_vec());
 
-        let ciphertext = ctx.seal(&vec.plaintext, &vec.associated_data);
-        assert_eq!(ciphertext, vec.ciphertext.to_vec());
+            let ciphertext = ctx.seal(&test.plaintext, &test.associated_data);
+            assert_eq!(&ciphertext, test.ciphertext.as_ref());
+        }
     }
 
     #[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();
+        for test in vec![
+            x25519_hkdf_sha256_hkdf_sha256_aes_128_gcm(),
+            x25519_hkdf_sha256_hkdf_sha256_chacha20_poly1305(),
+        ] {
+            let params = Params::new_from_rfc_ids(test.kem_id, test.kdf_id, test.aead_id).unwrap();
 
-        let mut ctx = RecipientContext::new(
-            &params,
-            &vec.recipient_priv_key,
-            &vec.encapsulated_key,
-            &vec.info,
-        )
-        .unwrap();
+            let mut ctx = RecipientContext::new(
+                &params,
+                &test.recipient_priv_key,
+                &test.encapsulated_key,
+                &test.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());
+            let plaintext = ctx.open(&test.ciphertext, &test.associated_data).unwrap();
+            assert_eq!(&plaintext, test.plaintext.as_ref());
+        }
     }
 
     #[test]
@@ -477,12 +569,6 @@
         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]
diff --git a/rust/bssl-crypto/src/scoped.rs b/rust/bssl-crypto/src/scoped.rs
index 17d331a..8c6b21b 100644
--- a/rust/bssl-crypto/src/scoped.rs
+++ b/rust/bssl-crypto/src/scoped.rs
@@ -79,6 +79,10 @@
         EvpHpkeCtx(ptr)
     }
 
+    pub fn as_ffi_ptr(&self) -> *const bssl_sys::EVP_HPKE_CTX {
+        self.0
+    }
+
     pub fn as_mut_ffi_ptr(&mut self) -> *mut bssl_sys::EVP_HPKE_CTX {
         self.0
     }
@@ -91,29 +95,27 @@
 }
 
 /// A scoped `EVP_HPKE_KEY`.
-pub struct EvpHpkeKey(*mut bssl_sys::EVP_HPKE_KEY);
+pub struct EvpHpkeKey(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)
+        EvpHpkeKey(unsafe { initialized_struct(|ptr| bssl_sys::EVP_HPKE_KEY_zero(ptr)) })
     }
 
-    pub fn as_ffi_ptr(&mut self) -> *const bssl_sys::EVP_HPKE_KEY {
-        self.0
+    pub fn as_ffi_ptr(&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
+        &mut self.0
     }
 }
 
 impl Drop for EvpHpkeKey {
     fn drop(&mut self) {
-        unsafe { bssl_sys::EVP_HPKE_KEY_free(self.0) }
+        // unsafe: the only way to create a `EvpHpkeKey` is via `new` and that
+        // ensures that this structure is initialized.
+        unsafe { bssl_sys::EVP_HPKE_KEY_cleanup(&mut self.0) }
     }
 }