Reworking bssl_crypto: ECDH

This change also adds scoped.rs, which contains scoped objects for
`EVP_PKEY` and `EC_KEY`, for when we're holding temporary objects of
those types.

Due to an accident with git rebase, it also renames `crypto_memcmp` to
`constant_time_compare` and promotes it to the top-level of the crate.

Change-Id: I629c051a244e3f9dcf64d8a36846528f10a31f50
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/65174
Reviewed-by: Bob Beck <bbe@google.com>
diff --git a/rust/bssl-crypto/src/bn.rs b/rust/bssl-crypto/src/bn.rs
deleted file mode 100644
index 35a196a..0000000
--- a/rust/bssl-crypto/src/bn.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-/* Copyright (c) 2023, 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.
- */
-
-use crate::{CSlice, ForeignType};
-
-pub(crate) struct BigNum {
-    ptr: *mut bssl_sys::BIGNUM,
-}
-
-// Safety: Implementation ensures `from_ptr(x).as_ptr() == x`
-unsafe impl ForeignType for BigNum {
-    type CType = bssl_sys::BIGNUM;
-
-    unsafe fn from_ptr(ptr: *mut Self::CType) -> Self {
-        Self { ptr }
-    }
-
-    fn as_ptr(&self) -> *mut Self::CType {
-        self.ptr
-    }
-}
-
-impl BigNum {
-    pub(crate) fn new() -> Self {
-        // Safety: There are no preconditions for BN_new()
-        unsafe { Self::from_ptr(bssl_sys::BN_new()) }
-    }
-}
-
-impl From<&[u8]> for BigNum {
-    fn from(value: &[u8]) -> Self {
-        let value_ffi = CSlice(value);
-        // Safety:
-        // - `value` is a CSlice from safe Rust.
-        // - The `ret` argument can be null to request allocating a new result.
-        let ptr = unsafe {
-            bssl_sys::BN_bin2bn(value_ffi.as_ptr(), value_ffi.len(), core::ptr::null_mut())
-        };
-        assert!(!ptr.is_null());
-        Self { ptr }
-    }
-}
-
-impl Drop for BigNum {
-    fn drop(&mut self) {
-        // Safety: `self.ptr` is owned by `self`.
-        unsafe { bssl_sys::BN_free(self.ptr) }
-    }
-}
diff --git a/rust/bssl-crypto/src/ec.rs b/rust/bssl-crypto/src/ec.rs
index 8bd8bda..4730643 100644
--- a/rust/bssl-crypto/src/ec.rs
+++ b/rust/bssl-crypto/src/ec.rs
@@ -13,409 +13,544 @@
  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-//! `EcKey` and `EcGroup` structs for working with elliptic curve cryptography. This module is
-//! intended for internal use within this crate only, to create higher-level abstractions suitable
-//! to be exposed externally.
+//! Definitions of NIST elliptic curves.
+//!
+//! If you're looking for curve25519, see the `x25519` and `ed25519` modules.
 
-use alloc::{borrow::ToOwned, vec, vec::Vec};
-use core::{borrow::Borrow, fmt::Debug, ops::Deref, panic};
+// This module is substantially internal-only and is only public for the
+// [`Curve`] trait, which is shared by ECDH and ECDSA.
 
-use crate::{bn::BigNum, CSlice, CSliceMut, ForeignType, ForeignTypeRef};
+use crate::{cbb_to_buffer, parse_with_cbs, scoped, sealed, Buffer, FfiSlice};
+use alloc::fmt::Debug;
+use core::ptr::{null, null_mut};
 
-#[derive(Debug)]
-pub(crate) struct EcKey {
-    ptr: *mut bssl_sys::EC_KEY,
-}
-
-// Safety: Implementation ensures `from_ptr(x).as_ptr() == x`
-unsafe impl ForeignType for EcKey {
-    type CType = bssl_sys::EC_KEY;
-
-    unsafe fn from_ptr(ptr: *mut Self::CType) -> Self {
-        Self { ptr }
-    }
-
-    fn as_ptr(&self) -> *mut Self::CType {
-        self.ptr
-    }
-}
-
-// Safety:
-// - `EC_KEY`'s documentation says "A given object may be used concurrently on multiple threads by
-//   non-mutating functions, provided no other thread is concurrently calling a mutating function.",
-//   which matches Rust's aliasing rules.
-// - `ptr(&self)` and `ptr_mut(&mut self)` ensures that only a mutable reference can get a mutable
-//   `EC_KEY` pointer outside of this module.
-unsafe impl Send for EcKey {}
-
-impl Clone for EcKey {
-    fn clone(&self) -> Self {
-        // Safety:
-        // - EcKey makes sure self.ptr is a valid pointer.
-        let ptr = unsafe { bssl_sys::EC_KEY_dup(self.ptr) };
-        Self { ptr }
-    }
-}
-
-/// Error type returned when conversion to or from an `EcKey` failed.
-pub(crate) struct ConversionFailed;
-
-impl EcKey {
-    pub fn new_by_ec_group(ec_group: &EcGroupRef) -> Self {
-        // Safety: `EC_KEY_new` does not have preconditions
-        let eckey = unsafe { bssl_sys::EC_KEY_new() };
-        assert!(!eckey.is_null());
-        // Safety:
-        // - `eckey` is just allocated and doesn't have its group set yet
-        // - `EcGroup` ensures the `ptr` it contains is valid
-        unsafe {
-            assert_eq!(
-                bssl_sys::EC_KEY_set_group(eckey, ec_group.as_ptr()),
-                1,
-                "EC_KEY_set_group failed"
-            );
-        }
-        // Safety: `eckey` is allocated and null-checked
-        unsafe { Self::from_ptr(eckey) }
-    }
-
-    /// Try to create a public-key version of `EcKey` from the given `value`. Returns error if the
-    /// slice is not a valid representation of a public key for the given curve.
-    ///
-    /// `curve_nid` should be a value defined in `bssl_sys::NID_*`.
-    #[allow(clippy::panic)]
-    pub(crate) fn try_new_public_key_from_bytes(
-        ec_group: &EcGroupRef,
-        value: &[u8],
-    ) -> Result<Self, ConversionFailed> {
-        let eckey = Self::new_by_ec_group(ec_group);
-        let value_ffi = CSlice(value);
-
-        // Safety: The input slice `value_ffi` is a CSlice from safe Rust.
-        let result = unsafe {
-            bssl_sys::EC_KEY_oct2key(
-                eckey.ptr,
-                value_ffi.as_ptr(),
-                value_ffi.len(),
-                core::ptr::null_mut(),
-            )
-        };
-        match result {
-            0 => Err(ConversionFailed),
-            1 => Ok(eckey),
-            _ => panic!("Unexpected return value {result} from EC_KEY_oct2key"),
-        }
-    }
-
-    pub(crate) fn to_affine_coordinates(&self) -> (BigNum, BigNum) {
-        let ecpoint = unsafe { bssl_sys::EC_KEY_get0_public_key(self.ptr) };
-        let bn_x = BigNum::new();
-        let bn_y = BigNum::new();
-
-        // Safety:
-        // - `EcKey` and `BigNum` structs ensures validity of their pointers.
-        let result = unsafe {
-            bssl_sys::EC_POINT_get_affine_coordinates(
-                bssl_sys::EC_KEY_get0_group(self.ptr),
-                ecpoint,
-                bn_x.as_ptr(),
-                bn_y.as_ptr(),
-                core::ptr::null_mut(),
-            )
-        };
-        assert_eq!(
-            result, 1,
-            "bssl_sys::EC_POINT_get_affine_coordinates failed"
-        );
-        (bn_x, bn_y)
-    }
-
-    pub(crate) fn generate(ec_group: &EcGroupRef) -> Self {
-        let eckey = EcKey::new_by_ec_group(ec_group);
-        // Safety: `EcKey` ensures eckey.ptr is valid.
-        let result = unsafe { bssl_sys::EC_KEY_generate_key(eckey.as_ptr()) };
-        assert_eq!(result, 1, "bssl_sys::EC_KEY_generate_key failed");
-        eckey
-    }
-
-    pub(crate) fn try_new_public_key_from_affine_coordinates(
-        ec_group: &EcGroupRef,
-        x: &[u8],
-        y: &[u8],
-    ) -> Result<Self, ConversionFailed> {
-        let bn_x = BigNum::from(x);
-        let bn_y = BigNum::from(y);
-
-        let eckey = EcKey::new_by_ec_group(ec_group);
-        // Safety:
-        // - Wrapper classes `EcKey` and `BigNum` ensures validity of the pointers
-        let result = unsafe {
-            bssl_sys::EC_KEY_set_public_key_affine_coordinates(
-                eckey.as_ptr(),
-                bn_x.as_ptr(),
-                bn_y.as_ptr(),
-            )
-        };
-        if result == 1 {
-            Ok(eckey)
-        } else {
-            Err(ConversionFailed)
-        }
-    }
-
-    /// Tries to convert the given bytes into a private key contained within `EcKey`.
-    ///
-    /// `private_key_bytes` must be padded to the size of `curve_nid`'s group order, otherwise the
-    /// conversion will fail.
-    pub(crate) fn try_from_raw_bytes(
-        ec_group: &EcGroupRef,
-        private_key_bytes: &[u8],
-    ) -> Result<Self, ConversionFailed> {
-        let eckey = EcKey::new_by_ec_group(ec_group);
-        let private_key_bytes_ffi = CSlice(private_key_bytes);
-        // Safety:
-        // - `EcKey` ensures `eckey.ptr` is valid.
-        // - `private_key_bytes` is a CSlice from safe-rust.
-        let result = unsafe {
-            bssl_sys::EC_KEY_oct2priv(
-                eckey.as_ptr(),
-                private_key_bytes_ffi.as_ptr(),
-                private_key_bytes_ffi.len(),
-            )
-        };
-        if result != 1 {
-            return Err(ConversionFailed);
-        }
-
-        Ok(eckey)
-    }
-
-    /// Converts between the private key component of `eckey` and octet form. The octet form
-    /// consists of the content octets of the `privateKey` `OCTET STRING` in an `ECPrivateKey` ASN.1
-    /// structure
-    pub(crate) fn to_raw_bytes(&self) -> Vec<u8> {
-        let mut output = vec![0_u8; 66];
-        let mut private_key_bytes_ffi = CSliceMut::from(&mut output[..]);
-        // Safety:
-        // - `EcKey` ensures `self.ptr` is valid.
-        // - `private_key_bytes_ffi` is a CSliceMut we just allocated.
-        // - 66 bytes is guaranteed to be sufficient to store an EC private key
-        let num_octets_stored = unsafe {
-            bssl_sys::EC_KEY_priv2oct(
-                self.as_ptr(),
-                private_key_bytes_ffi.as_mut_ptr(),
-                private_key_bytes_ffi.len(),
-            )
-        };
-        // Safety: `EC_KEY_priv2oct` just wrote `num_octets_stored` into the buffer.
-        unsafe { output.set_len(num_octets_stored) }
-        output
-    }
-
-    pub(crate) fn public_key_eq(&self, other: &Self) -> bool {
-        let result = unsafe {
-            bssl_sys::EC_POINT_cmp(
-                bssl_sys::EC_KEY_get0_group(self.ptr),
-                bssl_sys::EC_KEY_get0_public_key(self.ptr),
-                bssl_sys::EC_KEY_get0_public_key(other.ptr),
-                core::ptr::null_mut(),
-            )
-        };
-        assert_ne!(result, -1, "bssl_sys::EC_POINT_cmp failed");
-        result == 0
-    }
-
-    pub(crate) fn to_vec(&self) -> Vec<u8> {
-        // Safety: `self.ptr` is owned by `self`
-        let ecgroup = unsafe { bssl_sys::EC_KEY_get0_group(self.ptr) };
-        let ecpoint = unsafe { bssl_sys::EC_KEY_get0_public_key(self.ptr) };
-        let conv_form = unsafe { bssl_sys::EC_KEY_get_conv_form(self.ptr) };
-        // Safety:
-        // - When passing null to EC_POINT_point2oct's `buf` argument, it returns the size of the
-        //   resulting buffer.
-        let output_size = unsafe {
-            bssl_sys::EC_POINT_point2oct(
-                ecgroup,
-                ecpoint,
-                conv_form,
-                core::ptr::null_mut(),
-                0,
-                core::ptr::null_mut(),
-            )
-        };
-        assert_ne!(output_size, 0, "bssl_sys::EC_POINT_point2oct failed");
-        let mut result_vec = Vec::<u8>::with_capacity(output_size);
-        let buf_len = unsafe {
-            bssl_sys::EC_POINT_point2oct(
-                ecgroup,
-                ecpoint,
-                conv_form,
-                result_vec.as_mut_ptr(),
-                output_size,
-                core::ptr::null_mut(),
-            )
-        };
-        assert_ne!(buf_len, 0, "bssl_sys::EC_POINT_point2oct failed");
-        // Safety: The length is what EC_POINT_point2oct just told us it filled into the buffer.
-        unsafe { result_vec.set_len(buf_len) }
-        result_vec
-    }
-}
-
-impl Drop for EcKey {
-    fn drop(&mut self) {
-        // Safety: `self.ptr` is owned by this struct
-        unsafe { bssl_sys::EC_KEY_free(self.ptr) }
-    }
-}
-
-/// Describes an elliptic curve.
-#[non_exhaustive]
-pub struct EcGroupRef;
-
-// Safety: Default implementation in ForeignTypeRef ensures the preconditions
-//   required by that trait holds.
-unsafe impl ForeignTypeRef for EcGroupRef {
-    type CType = bssl_sys::EC_GROUP;
-}
-
-impl Borrow<EcGroupRef> for EcGroup {
-    fn borrow(&self) -> &EcGroupRef {
-        unsafe { EcGroupRef::from_ptr(self.ptr) }
-    }
-}
-
-impl ToOwned for EcGroupRef {
-    type Owned = EcGroup;
-
-    fn to_owned(&self) -> Self::Owned {
-        // Safety: `EcGroupRef` is a valid pointer
-        let new_ec_group = unsafe { bssl_sys::EC_GROUP_dup(self.as_ptr()) };
-        assert!(!new_ec_group.is_null(), "EC_GROUP_dup failed");
-        EcGroup { ptr: new_ec_group }
-    }
-}
-
-impl AsRef<EcGroupRef> for EcGroup {
-    fn as_ref(&self) -> &EcGroupRef {
-        self.deref()
-    }
-}
-
-impl PartialEq for EcGroupRef {
-    fn eq(&self, other: &Self) -> bool {
-        // Safety:
-        // - Self and other are valid pointers since they come from `EcGroupRef`
-        // - Third argument is ignored
-        unsafe {
-            bssl_sys::EC_GROUP_cmp(
-                self.as_ptr(),
-                other.as_ptr(),
-                /* ignored */ core::ptr::null_mut(),
-            ) == 0
-        }
-    }
-}
-
-impl Eq for EcGroupRef {}
-
-pub struct EcGroup {
-    ptr: *mut bssl_sys::EC_GROUP,
-}
-
-impl Deref for EcGroup {
-    type Target = EcGroupRef;
-
-    fn deref(&self) -> &Self::Target {
-        unsafe { EcGroupRef::from_ptr(self.ptr) }
-    }
-}
-
-impl Drop for EcGroup {
-    fn drop(&mut self) {
-        unsafe { bssl_sys::EC_GROUP_free(self.ptr) }
-    }
-}
-
-/// An elliptic curve, used as the type parameter for [`PublicKey`] and [`PrivateKey`].
+/// An elliptic curve.
 pub trait Curve: Debug {
-    /// The size of the affine coordinates for this curve.
-    const AFFINE_COORDINATE_SIZE: usize;
-
-    /// Create a new [`EcGroup`] for this curve.
-    fn ec_group() -> &'static EcGroupRef;
+    #[doc(hidden)]
+    fn group(_: sealed::Sealed) -> Group;
 }
 
-/// The P-224 curve, corresponding to `NID_secp224r1`.
-#[derive(Debug)]
-pub struct P224;
-
-impl Curve for P224 {
-    const AFFINE_COORDINATE_SIZE: usize = 28;
-
-    fn ec_group() -> &'static EcGroupRef {
-        // Safety: EC_group_p224 does not have any preconditions
-        unsafe { EcGroupRef::from_ptr(bssl_sys::EC_group_p224() as *mut _) }
-    }
-}
-
-/// The P-256 curve, corresponding to `NID_X9_62_prime256v1`.
+/// The NIST P-256 curve, also called secp256r1.
 #[derive(Debug)]
 pub struct P256;
 
 impl Curve for P256 {
-    const AFFINE_COORDINATE_SIZE: usize = 32;
-
-    fn ec_group() -> &'static EcGroupRef {
-        // Safety: EC_group_p256 does not have any preconditions
-        unsafe { EcGroupRef::from_ptr(bssl_sys::EC_group_p256() as *mut _) }
+    fn group(_: sealed::Sealed) -> Group {
+        Group::P256
     }
 }
 
-/// The P-384 curve, corresponding to `NID_secp384r1`.
+/// The NIST P-384 curve, also called secp384r1.
 #[derive(Debug)]
 pub struct P384;
 
 impl Curve for P384 {
-    const AFFINE_COORDINATE_SIZE: usize = 48;
-
-    fn ec_group() -> &'static EcGroupRef {
-        // Safety: EC_group_p384 does not have any preconditions
-        unsafe { EcGroupRef::from_ptr(bssl_sys::EC_group_p384() as *mut _) }
+    fn group(_: sealed::Sealed) -> Group {
+        Group::P384
     }
 }
 
-/// The P-521 curve, corresponding to `NID_secp521r1`.
-#[derive(Debug)]
-pub struct P521;
+#[derive(Copy, Clone)]
+#[doc(hidden)]
+pub enum Group {
+    P256,
+    P384,
+}
 
-impl Curve for P521 {
-    const AFFINE_COORDINATE_SIZE: usize = 66;
-
-    fn ec_group() -> &'static EcGroupRef {
-        // Safety: EC_group_p521 does not have any preconditions
-        unsafe { EcGroupRef::from_ptr(bssl_sys::EC_group_p521() as *mut _) }
+impl Group {
+    fn as_ffi_ptr(self) -> *const bssl_sys::EC_GROUP {
+        // Safety: `group` is an address-space constant. These functions
+        // cannot fail and no resources need to be released in the future.
+        match self {
+            Group::P256 => unsafe { bssl_sys::EC_group_p256() },
+            Group::P384 => unsafe { bssl_sys::EC_group_p384() },
+        }
     }
 }
 
+/// Point is a valid, finite point on some curve.
+pub(crate) struct Point {
+    group: *const bssl_sys::EC_GROUP,
+    point: *mut bssl_sys::EC_POINT,
+}
+
+impl Point {
+    /// Construct an uninitialized curve point. This is not public and all
+    /// callers must ensure that the point is initialized before being returned.
+    fn new(group: Group) -> Self {
+        let group = group.as_ffi_ptr();
+        // Safety: `group` is valid because it was constructed just above.
+        let point = unsafe { bssl_sys::EC_POINT_new(group) };
+        // `EC_POINT_new` only fails if out of memory, which is not a case that
+        // is handled short of panicking.
+        assert!(!point.is_null());
+        Self { group, point }
+    }
+
+    /// Construct a point by multipling the curve's base point by the given
+    /// scalar.
+    unsafe fn from_scalar(group: Group, scalar: *const bssl_sys::BIGNUM) -> Option<Self> {
+        let point = Self::new(group);
+        // Safety: the members of `point` are valid by construction. `scalar`
+        // is assumed to be valid.
+        let result = unsafe {
+            bssl_sys::EC_POINT_mul(
+                point.group,
+                point.point,
+                scalar,
+                /*q=*/ null(),
+                /*m=*/ null(),
+                /*ctx=*/ null_mut(),
+            )
+        };
+        if result != 1 {
+            return None;
+        }
+        if 1 == unsafe { bssl_sys::EC_POINT_is_at_infinity(point.group, point.point) } {
+            return None;
+        }
+        Some(point)
+    }
+
+    /// Duplicate the given finite point.
+    unsafe fn clone_from_ptr(
+        group: *const bssl_sys::EC_GROUP,
+        point: *const bssl_sys::EC_POINT,
+    ) -> Point {
+        assert_eq!(0, unsafe {
+            bssl_sys::EC_POINT_is_at_infinity(group, point)
+        });
+
+        // Safety: we assume that the caller is passing valid pointers
+        let new_point = unsafe { bssl_sys::EC_POINT_dup(point, group) };
+        // `EC_POINT_dup` only fails if out of memory, which is not a case that
+        // is handled short of panicking.
+        assert!(!new_point.is_null());
+
+        Self {
+            group,
+            point: new_point,
+        }
+    }
+
+    pub fn as_ffi_ptr(&self) -> *const bssl_sys::EC_POINT {
+        self.point
+    }
+
+    /// Create a new point from an uncompressed X9.62 representation.
+    ///
+    /// (X9.62 is the standard representation of an elliptic-curve point that
+    /// starts with an 0x04 byte.)
+    pub fn from_x962_uncompressed(group: Group, x962: &[u8]) -> Option<Self> {
+        const UNCOMPRESSED: u8 =
+            bssl_sys::point_conversion_form_t::POINT_CONVERSION_UNCOMPRESSED as u8;
+        if x962.first()? != &UNCOMPRESSED {
+            return None;
+        }
+
+        let point = Self::new(group);
+        // Safety: `point` is valid by construction. `x962` is a valid memory
+        // buffer.
+        let result = unsafe {
+            bssl_sys::EC_POINT_oct2point(
+                point.group,
+                point.point,
+                x962.as_ffi_ptr(),
+                x962.len(),
+                /*bn_ctx=*/ null_mut(),
+            )
+        };
+        if result == 1 {
+            // X9.62 format cannot represent the point at infinity, so this
+            // should be moot, but `Point` must never contain infinity.
+            assert_eq!(0, unsafe {
+                bssl_sys::EC_POINT_is_at_infinity(point.group, point.point)
+            });
+            Some(point)
+        } else {
+            None
+        }
+    }
+
+    pub fn to_x962_uncompressed(&self) -> Buffer {
+        // Safety: arguments are valid, `EC_KEY` ensures that the the group is
+        // correct for the point, and a `Point` is always finite.
+        unsafe { to_x962_uncompressed(self.group, self.point) }
+    }
+
+    pub fn from_der_subject_public_key_info(group: Group, spki: &[u8]) -> Option<Self> {
+        let mut pkey = scoped::EvpPkey::from_ptr(parse_with_cbs(
+            spki,
+            // Safety: if called, `pkey` is the non-null result of `EVP_parse_public_key`.
+            |pkey| unsafe { bssl_sys::EVP_PKEY_free(pkey) },
+            // Safety: `cbs` is a valid pointer in this context.
+            |cbs| unsafe { bssl_sys::EVP_parse_public_key(cbs) },
+        )?);
+        let ec_key = unsafe { bssl_sys::EVP_PKEY_get0_EC_KEY(pkey.as_ffi_ptr()) };
+        if ec_key.is_null() {
+            // Not an ECC key.
+            return None;
+        }
+        let parsed_group = unsafe { bssl_sys::EC_KEY_get0_group(ec_key) };
+        if parsed_group != group.as_ffi_ptr() {
+            // ECC key for a different curve.
+            return None;
+        }
+        let point = unsafe { bssl_sys::EC_KEY_get0_public_key(ec_key) };
+        if point.is_null() {
+            return None;
+        }
+        // Safety: `ec_key` is still owned by `pkey` and doesn't need to be freed.
+        Some(unsafe { Self::clone_from_ptr(parsed_group, point) })
+    }
+
+    /// Calls `func` with an `EC_KEY` that contains a copy of this point.
+    pub fn with_point_as_ec_key<F, T>(&self, func: F) -> T
+    where
+        F: FnOnce(*mut bssl_sys::EC_KEY) -> T,
+    {
+        let mut ec_key = scoped::EcKey::new();
+        // Safety: `self.group` is always valid by construction and this doesn't
+        // pass ownership.
+        assert_eq!(1, unsafe {
+            bssl_sys::EC_KEY_set_group(ec_key.as_ffi_ptr(), self.group)
+        });
+        // Safety: `self.point` is always valid by construction and this doesn't
+        // pass ownership.
+        assert_eq!(1, unsafe {
+            bssl_sys::EC_KEY_set_public_key(ec_key.as_ffi_ptr(), self.point)
+        });
+        func(ec_key.as_ffi_ptr())
+    }
+
+    pub fn to_der_subject_public_key_info(&self) -> Buffer {
+        // Safety: `ec_key` is a valid pointer in this context.
+        self.with_point_as_ec_key(|ec_key| unsafe { to_der_subject_public_key_info(ec_key) })
+    }
+}
+
+impl Drop for Point {
+    fn drop(&mut self) {
+        // Safety: `self.point` must be valid because only valid `Point`s can
+        // be constructed. `self.group` does not need to be freed.
+        unsafe { bssl_sys::EC_POINT_free(self.point) }
+    }
+}
+
+/// Key holds both a public and private key. While BoringSSL allows an `EC_KEY`
+/// to also be a) empty, b) holding only a private scalar, or c) holding only
+// a public key, those cases are never exposed as a `Key`.
+pub(crate) struct Key(*mut bssl_sys::EC_KEY);
+
+impl Key {
+    /// Construct an uninitialized key. This is not public and all
+    /// callers must ensure that the key is initialized before being returned.
+    fn new(group: Group) -> Self {
+        let key = unsafe { bssl_sys::EC_KEY_new() };
+        // `EC_KEY_new` only fails if out of memory, which is not a case that
+        // is handled short of panicking.
+        assert!(!key.is_null());
+
+        // Setting the group on a fresh `EC_KEY` never fails.
+        assert_eq!(1, unsafe {
+            bssl_sys::EC_KEY_set_group(key, group.as_ffi_ptr())
+        });
+
+        Self(key)
+    }
+
+    pub fn as_ffi_ptr(&self) -> *const bssl_sys::EC_KEY {
+        self.0
+    }
+
+    /// Generate a random private key.
+    pub fn generate(group: Group) -> Self {
+        let key = Self::new(group);
+        // Generation only fails if out of memory, which is only handled by
+        // panicking.
+        assert_eq!(1, unsafe { bssl_sys::EC_KEY_generate_key(key.0) });
+        // `EC_KEY_generate_key` is documented as also setting the public key.
+        key
+    }
+
+    /// Construct a private key from a big-endian representation of the private
+    /// scalar. The scalar must be zero padded to the correct length for the
+    /// curve.
+    pub fn from_big_endian(group: Group, scalar: &[u8]) -> Option<Self> {
+        let key = Self::new(group);
+        // Safety: `key.0` is always valid by construction.
+        let result = unsafe { bssl_sys::EC_KEY_oct2priv(key.0, scalar.as_ffi_ptr(), scalar.len()) };
+        if result != 1 {
+            return None;
+        }
+
+        // BoringSSL allows an `EC_KEY` to have a private scalar without a
+        // public point, but `Key` is never exposed in that state.
+
+        // Safety: `key.0` is valid by construction. The returned value is
+        // still owned the `EC_KEY`.
+        let scalar = unsafe { bssl_sys::EC_KEY_get0_private_key(key.0) };
+        assert!(!scalar.is_null());
+
+        // Safety: `scalar` is a valid pointer.
+        let point = unsafe { Point::from_scalar(group, scalar)? };
+        // Safety: `key.0` is valid by construction, as is `point.point`. The
+        // point is copied into the `EC_KEY` so ownership isn't being moved.
+        let result = unsafe { bssl_sys::EC_KEY_set_public_key(key.0, point.point) };
+        // Setting the public key should only fail if out of memory, which this
+        // crate doesn't handle, or if the groups don't match, which is
+        // impossible.
+        assert_eq!(result, 1);
+
+        Some(key)
+    }
+
+    pub fn to_big_endian(&self) -> Buffer {
+        let mut ptr: *mut u8 = null_mut();
+        // Safety: `self.0` is valid by construction. If this returns non-zero
+        // then ptr holds ownership of a buffer.
+        let len = unsafe { bssl_sys::EC_KEY_priv2buf(self.0, &mut ptr) };
+        assert!(len != 0);
+        Buffer { ptr, len }
+    }
+
+    /// Parses an ECPrivateKey structure (from RFC 5915).
+    pub fn from_der_ec_private_key(group: Group, der: &[u8]) -> Option<Self> {
+        let key = parse_with_cbs(
+            der,
+            // Safety: in this context, `key` is the non-null result of
+            // `EC_KEY_parse_private_key`.
+            |key| unsafe { bssl_sys::EC_KEY_free(key) },
+            // Safety: `cbs` is valid per `parse_with_cbs` and `group` always
+            // returns a valid pointer.
+            |cbs| unsafe { bssl_sys::EC_KEY_parse_private_key(cbs, group.as_ffi_ptr()) },
+        )?;
+        Some(Self(key))
+    }
+
+    /// Serializes this private key as an ECPrivateKey structure from RFC 5915.
+    pub fn to_der_ec_private_key(&self) -> Buffer {
+        cbb_to_buffer(64, |cbb| unsafe {
+            // Safety: the `EC_KEY` is always valid so `EC_KEY_marshal_private_key`
+            // should only fail if out of memory, which this crate doesn't handle.
+            assert_eq!(
+                1,
+                bssl_sys::EC_KEY_marshal_private_key(
+                    cbb,
+                    self.0,
+                    bssl_sys::EC_PKEY_NO_PARAMETERS as u32
+                )
+            );
+        })
+    }
+
+    /// Parses a PrivateKeyInfo structure (from RFC 5208).
+    pub fn from_der_private_key_info(group: Group, der: &[u8]) -> Option<Self> {
+        let mut pkey = scoped::EvpPkey::from_ptr(parse_with_cbs(
+            der,
+            // Safety: in this context, `pkey` is the non-null result of
+            // `EVP_parse_private_key`.
+            |pkey| unsafe { bssl_sys::EVP_PKEY_free(pkey) },
+            // Safety: `cbs` is valid per `parse_with_cbs`.
+            |cbs| unsafe { bssl_sys::EVP_parse_private_key(cbs) },
+        )?);
+        let ec_key = unsafe { bssl_sys::EVP_PKEY_get1_EC_KEY(pkey.as_ffi_ptr()) };
+        if ec_key.is_null() {
+            return None;
+        }
+        // Safety: `ec_key` is now owned by this function.
+        let parsed_group = unsafe { bssl_sys::EC_KEY_get0_group(ec_key) };
+        if parsed_group == group.as_ffi_ptr() {
+            // Safety: parsing an EC_KEY always set the public key. It should
+            // be impossible for the public key to be infinity, but double-check.
+            let is_infinite = unsafe {
+                bssl_sys::EC_POINT_is_at_infinity(
+                    bssl_sys::EC_KEY_get0_group(ec_key),
+                    bssl_sys::EC_KEY_get0_public_key(ec_key),
+                )
+            };
+            if is_infinite == 0 {
+                // Safety: `EVP_PKEY_get1_EC_KEY` returned ownership, which we can move
+                // into the returned object.
+                return Some(Self(ec_key));
+            }
+        }
+        unsafe { bssl_sys::EC_KEY_free(ec_key) };
+        None
+    }
+
+    /// Serializes this private key as a PrivateKeyInfo structure from RFC 5208.
+    pub fn to_der_private_key_info(&self) -> Buffer {
+        let mut pkey = scoped::EvpPkey::new();
+        // Safety: `pkey` was just allocated above; the `EC_KEY` is valid by
+        // construction. This call takes a reference to the `EC_KEY` and so
+        // hasn't stolen ownership from `self`.
+        assert_eq!(1, unsafe {
+            bssl_sys::EVP_PKEY_set1_EC_KEY(pkey.as_ffi_ptr(), self.0)
+        });
+        cbb_to_buffer(64, |cbb| unsafe {
+            // `EVP_marshal_private_key` should always return one because this
+            // key is valid by construction.
+            assert_eq!(1, bssl_sys::EVP_marshal_private_key(cbb, pkey.as_ffi_ptr()));
+        })
+    }
+
+    pub fn to_point(&self) -> Point {
+        // Safety: `self.0` is valid by construction.
+        let group = unsafe { bssl_sys::EC_KEY_get0_group(self.0) };
+        let point = unsafe { bssl_sys::EC_KEY_get0_public_key(self.0) };
+        // A `Key` is never constructed without a public key.
+        assert!(!point.is_null());
+        // Safety: pointers are valid and `clone_from_ptr` doesn't take
+        // ownership.
+        unsafe { Point::clone_from_ptr(group, point) }
+    }
+
+    pub fn to_x962_uncompressed(&self) -> Buffer {
+        // Safety: `self.0` is valid by construction.
+        let group = unsafe { bssl_sys::EC_KEY_get0_group(self.0) };
+        let point = unsafe { bssl_sys::EC_KEY_get0_public_key(self.0) };
+        // Safety: arguments are valid, `EC_KEY` ensures that the the group is
+        // correct for the point, and a `Key` always holds a finite public point.
+        unsafe { to_x962_uncompressed(group, point) }
+    }
+
+    pub fn to_der_subject_public_key_info(&self) -> Buffer {
+        // Safety: `self.0` is always valid by construction.
+        unsafe { to_der_subject_public_key_info(self.0) }
+    }
+}
+
+impl Drop for Key {
+    fn drop(&mut self) {
+        // Safety: `self.0` must be valid because only valid `Key`s can
+        // be constructed.
+        unsafe { bssl_sys::EC_KEY_free(self.0) }
+    }
+}
+
+/// Serialize a finite point to uncompressed X9.62 format.
+///
+/// Callers must ensure that the arguments are valid, that the point has the
+/// specified group, and that the point is finite.
+unsafe fn to_x962_uncompressed(
+    group: *const bssl_sys::EC_GROUP,
+    point: *const bssl_sys::EC_POINT,
+) -> Buffer {
+    cbb_to_buffer(65, |cbb| unsafe {
+        // Safety: the caller must ensure that the arguments are valid.
+        let result = bssl_sys::EC_POINT_point2cbb(
+            cbb,
+            group,
+            point,
+            bssl_sys::point_conversion_form_t::POINT_CONVERSION_UNCOMPRESSED,
+            /*bn_ctx=*/ null_mut(),
+        );
+        // The public key is always finite, so `EC_POINT_point2cbb` only fails
+        // if out of memory, which isn't handled by this crate.
+        assert_eq!(result, 1);
+    })
+}
+
+unsafe fn to_der_subject_public_key_info(ec_key: *mut bssl_sys::EC_KEY) -> Buffer {
+    let mut pkey = scoped::EvpPkey::new();
+    // Safety: this takes a reference to `ec_key` and so doesn't steal ownership.
+    assert_eq!(1, unsafe {
+        bssl_sys::EVP_PKEY_set1_EC_KEY(pkey.as_ffi_ptr(), ec_key)
+    });
+    cbb_to_buffer(65, |cbb| unsafe {
+        // The arguments are valid so this will only fail if out of memory,
+        // which this crate doesn't handle.
+        assert_eq!(1, bssl_sys::EVP_marshal_public_key(cbb, pkey.as_ffi_ptr()));
+    })
+}
+
 #[cfg(test)]
 mod test {
-    use crate::ec::P521;
+    use super::*;
 
-    use super::{Curve, EcGroupRef, P256};
+    fn test_point_format<Serialize, Parse>(serialize_func: Serialize, parse_func: Parse)
+    where
+        Serialize: FnOnce(&Point) -> Buffer,
+        Parse: Fn(&[u8]) -> Option<Point>,
+    {
+        let key = Key::generate(Group::P256);
+        let point = key.to_point();
 
-    #[test]
-    fn test_ec_group_clone_and_eq() {
-        let group = P256::ec_group();
-        let group_clone = group.to_owned();
-        let group2: &EcGroupRef = &group_clone;
-        assert!(group == group2);
+        let mut vec = serialize_func(&point).as_ref().to_vec();
+        let point2 = parse_func(vec.as_slice()).unwrap();
+        assert_eq!(
+            point.to_x962_uncompressed().as_ref(),
+            point2.to_x962_uncompressed().as_ref()
+        );
+
+        assert!(parse_func(&vec.as_slice()[0..16]).is_none());
+
+        vec[10] ^= 1;
+        assert!(parse_func(vec.as_slice()).is_none());
+        vec[10] ^= 1;
+
+        assert!(parse_func(b"").is_none());
     }
 
     #[test]
-    fn test_ec_group_not_equal() {
-        let group = P256::ec_group();
-        let group2 = P521::ec_group();
-        assert!(group != group2)
+    fn x962() {
+        let x962 = b"\x04\x74\xcf\x69\xcb\xd1\x2b\x75\x07\x42\x85\xcf\x69\x6f\xc2\x56\x4b\x90\xe7\xeb\xbc\xd0\xe7\x20\x36\x86\x66\xbe\xcc\x94\x75\xa2\xa4\x4c\x2a\xf8\xa2\x56\xb8\x92\xb7\x7d\x17\xba\x97\x93\xbb\xf2\x9f\x52\x26\x7d\x90\xf9\x2c\x37\x26\x02\xbb\x4e\xd1\x89\x7c\xad\x54";
+        assert!(Point::from_x962_uncompressed(Group::P256, x962).is_some());
+
+        test_point_format(
+            |point| point.to_x962_uncompressed(),
+            |buf| Point::from_x962_uncompressed(Group::P256, buf),
+        );
+    }
+
+    #[test]
+    fn spki() {
+        test_point_format(
+            |point| point.to_der_subject_public_key_info(),
+            |buf| Point::from_der_subject_public_key_info(Group::P256, buf),
+        );
+    }
+
+    fn test_key_format<Serialize, Parse>(serialize_func: Serialize, parse_func: Parse)
+    where
+        Serialize: FnOnce(&Key) -> Buffer,
+        Parse: Fn(&[u8]) -> Option<Key>,
+    {
+        let key = Key::generate(Group::P256);
+
+        let vec = serialize_func(&key).as_ref().to_vec();
+        let key2 = parse_func(vec.as_slice()).unwrap();
+        assert_eq!(
+            key.to_x962_uncompressed().as_ref(),
+            key2.to_x962_uncompressed().as_ref()
+        );
+
+        assert!(parse_func(&vec.as_slice()[0..16]).is_none());
+        assert!(parse_func(b"").is_none());
+    }
+
+    #[test]
+    fn der_ec_private_key() {
+        test_key_format(
+            |key| key.to_der_ec_private_key(),
+            |buf| Key::from_der_ec_private_key(Group::P256, buf),
+        );
+    }
+
+    #[test]
+    fn der_private_key_info() {
+        test_key_format(
+            |key| key.to_der_private_key_info(),
+            |buf| Key::from_der_private_key_info(Group::P256, buf),
+        );
+    }
+
+    #[test]
+    fn big_endian() {
+        test_key_format(
+            |key| key.to_big_endian(),
+            |buf| Key::from_big_endian(Group::P256, buf),
+        );
     }
 }
diff --git a/rust/bssl-crypto/src/ecdh.rs b/rust/bssl-crypto/src/ecdh.rs
index aca711b..85476a9 100644
--- a/rust/bssl-crypto/src/ecdh.rs
+++ b/rust/bssl-crypto/src/ecdh.rs
@@ -13,403 +13,192 @@
  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+//! Elliptic Curve Diffie-Hellman operations.
+//!
+//! This module implements ECDH over the NIST curves P-256 and P-384.
+//!
+//! ```
+//! use bssl_crypto::{ecdh, ec::P256};
+//!
+//! let alice_private_key = ecdh::PrivateKey::<P256>::generate();
+//! let alice_public_key_serialized = alice_private_key.to_x962_uncompressed();
+//!
+//! // Somehow, Alice's public key is sent to Bob.
+//! let bob_private_key = ecdh::PrivateKey::<P256>::generate();
+//! let alice_public_key =
+//!     ecdh::PublicKey::<P256>::from_x962_uncompressed(
+//!         alice_public_key_serialized.as_ref())
+//!     .unwrap();
+//! let shared_key1 = bob_private_key.compute_shared_key(&alice_public_key);
+//!
+//! // Likewise, Alice gets Bob's public key and computes the same shared key.
+//! let bob_public_key = bob_private_key.to_public_key();
+//! let shared_key2 = alice_private_key.compute_shared_key(&bob_public_key);
+//! assert_eq!(shared_key1, shared_key2);
+//! ```
+
+use crate::{ec, sealed, with_output_vec, Buffer};
 use alloc::vec::Vec;
 use core::marker::PhantomData;
 
-use crate::{
-    ec::{Curve, EcKey},
-    pkey::{Pkey, PkeyCtx},
-    CSliceMut, ForeignType,
-};
-
-pub use crate::ec::P256;
-
-/// Private key used in a elliptic curve Diffie-Hellman.
-pub struct PrivateKey<C: Curve> {
-    /// An EcKey containing the private-public key pair
-    eckey: EcKey,
+/// An ECDH public key over the given curve.
+pub struct PublicKey<C: ec::Curve> {
+    point: ec::Point,
     marker: PhantomData<C>,
 }
 
-/// Error type for ECDH operations.
-#[derive(Debug)]
-pub enum Error {
-    /// Failed when trying to convert between representations.
-    ConversionFailed,
-    /// The Diffie-Hellman key exchange failed.
-    DiffieHellmanFailed,
-}
-
-impl<C: Curve> PrivateKey<C> {
-    /// Derives a shared secret from this private key and the given public key.
-    ///
-    /// # Panics
-    /// When `OUTPUT_SIZE` is insufficient to store the output of the shared secret.
-    #[allow(clippy::expect_used)]
-    pub fn diffie_hellman<const OUTPUT_SIZE: usize>(
-        &self,
-        other_public_key: &PublicKey<C>,
-    ) -> Result<SharedSecret<OUTPUT_SIZE>, Error> {
-        let pkey: Pkey = (&self.eckey).into();
-        let pkey_ctx = PkeyCtx::new(&pkey);
-        let other_pkey: Pkey = (&other_public_key.eckey).into();
-        let mut output = [0_u8; OUTPUT_SIZE];
-        pkey_ctx
-            .diffie_hellman(&other_pkey, CSliceMut(&mut output))
-            .map(|_| SharedSecret(output))
-            .map_err(|_| Error::DiffieHellmanFailed)
+impl<C: ec::Curve> PublicKey<C> {
+    /// Parse a public key in uncompressed X9.62 format. (This is the common
+    /// format for elliptic curve points beginning with an 0x04 byte.)
+    pub fn from_x962_uncompressed(x962: &[u8]) -> Option<Self> {
+        let point = ec::Point::from_x962_uncompressed(C::group(sealed::Sealed), x962)?;
+        Some(Self {
+            point,
+            marker: PhantomData,
+        })
     }
 
-    /// Generate a new private key for use in a Diffie-Hellman key exchange.
+    /// Serialize this key as uncompressed X9.62 format.
+    pub fn to_x962_uncompressed(&self) -> Buffer {
+        self.point.to_x962_uncompressed()
+    }
+}
+
+/// An ECDH private key over the given curve.
+pub struct PrivateKey<C: ec::Curve> {
+    key: ec::Key,
+    marker: PhantomData<C>,
+}
+
+impl<C: ec::Curve> PrivateKey<C> {
+    /// Generate a random private key.
     pub fn generate() -> Self {
         Self {
-            eckey: EcKey::generate(C::ec_group()),
+            key: ec::Key::generate(C::group(sealed::Sealed)),
             marker: PhantomData,
         }
     }
 
-    /// Tries to convert the given bytes into an private key.
-    ///
-    /// `private_key_bytes` is the octet form that consists of the content octets of the
-    /// `privateKey` `OCTET STRING` in an `ECPrivateKey` ASN.1 structure.
-    ///
-    /// Returns an error if the given bytes is not a valid representation of a P-256 private key.
-    pub fn from_private_bytes(private_key_bytes: &[u8]) -> Result<Self, Error> {
-        EcKey::try_from_raw_bytes(C::ec_group(), private_key_bytes)
-            .map(|eckey| Self {
-                eckey,
-                marker: PhantomData,
-            })
-            .map_err(|_| Error::ConversionFailed)
-    }
-
-    /// Serializes this private key as a big-endian integer, zero-padded to the size of key's group
-    /// order and returns the result.
-    pub fn to_bytes(&self) -> Vec<u8> {
-        self.eckey.to_raw_bytes()
-    }
-}
-
-impl<'a, C: Curve> From<&'a PrivateKey<C>> for PublicKey<C> {
-    fn from(value: &'a PrivateKey<C>) -> Self {
-        Self {
-            eckey: value.eckey.clone(),
+    /// Parse a `PrivateKey` from a zero-padded, big-endian representation of the secret scalar.
+    pub fn from_big_endian(scalar: &[u8]) -> Option<Self> {
+        let key = ec::Key::from_big_endian(C::group(sealed::Sealed), scalar)?;
+        Some(Self {
+            key,
             marker: PhantomData,
+        })
+    }
+
+    /// Return the private scalar as zero-padded, big-endian bytes.
+    pub fn to_big_endian(&self) -> Buffer {
+        self.key.to_big_endian()
+    }
+
+    /// Parse an ECPrivateKey structure (from RFC 5915). The key must be on the
+    /// specified curve.
+    pub fn from_der_ec_private_key(der: &[u8]) -> Option<Self> {
+        let key = ec::Key::from_der_ec_private_key(C::group(sealed::Sealed), der)?;
+        Some(Self {
+            key,
+            marker: PhantomData,
+        })
+    }
+
+    /// Serialize this private key as an ECPrivateKey structure (from RFC 5915).
+    pub fn to_der_ec_private_key(&self) -> Buffer {
+        self.key.to_der_ec_private_key()
+    }
+
+    /// Parse a PrivateKeyInfo structure (from RFC 5208). The key must be on the
+    /// specified curve.
+    pub fn from_der_private_key_info(der: &[u8]) -> Option<Self> {
+        let key = ec::Key::from_der_private_key_info(C::group(sealed::Sealed), der)?;
+        Some(Self {
+            key,
+            marker: PhantomData,
+        })
+    }
+
+    /// Serialize this private key as a PrivateKeyInfo structure (from RFC 5208).
+    pub fn to_der_private_key_info(&self) -> Buffer {
+        self.key.to_der_private_key_info()
+    }
+
+    /// Serialize the _public_ part of this key in uncompressed X9.62 format.
+    pub fn to_x962_uncompressed(&self) -> Buffer {
+        self.key.to_x962_uncompressed()
+    }
+
+    /// Compute the shared key between this private key and the given public key.
+    /// The result should be used with a key derivation function that includes
+    /// the two public keys.
+    pub fn compute_shared_key(&self, other_public_key: &PublicKey<C>) -> Vec<u8> {
+        // 384 bits is the largest curve supported. The buffer is sized to be
+        // larger than this so that truncation of the output can be noticed.
+        let max_output = 384 / 8 + 1;
+        unsafe {
+            with_output_vec(max_output, |out_buf| {
+                // Safety:
+                //   - `out_buf` points to at least `max_output` bytes, as
+                //     required.
+                //   - The `EC_POINT` and `EC_KEY` pointers are valid by construction.
+                let num_out_bytes = bssl_sys::ECDH_compute_key(
+                    out_buf as *mut core::ffi::c_void,
+                    max_output,
+                    other_public_key.point.as_ffi_ptr(),
+                    self.key.as_ffi_ptr(),
+                    None,
+                );
+                // Out of memory is not handled by this crate.
+                assert!(num_out_bytes > 0);
+                let num_out_bytes = num_out_bytes as usize;
+                // If the buffer was completely filled then it was probably
+                // truncated, which should never happen.
+                assert!(num_out_bytes < max_output);
+                num_out_bytes
+            })
         }
     }
-}
 
-/// A public key for elliptic curve.
-#[derive(Clone, Debug)]
-pub struct PublicKey<C: Curve> {
-    /// An EcKey containing the public key
-    eckey: EcKey,
-    marker: PhantomData<C>,
-}
-
-impl<C: Curve> Eq for PublicKey<C> {}
-
-impl<C: Curve> PartialEq for PublicKey<C> {
-    fn eq(&self, other: &Self) -> bool {
-        self.eckey.public_key_eq(&other.eckey)
-    }
-}
-
-impl<C: Curve> PublicKey<C> {
-    /// Converts this public key to its byte representation.
-    pub fn to_vec(&self) -> Vec<u8> {
-        self.eckey.to_vec()
-    }
-
-    /// Converts the given affine coordinates into a public key.
-    pub fn from_affine_coordinates<const AFFINE_COORDINATE_SIZE: usize>(
-        x: &[u8; AFFINE_COORDINATE_SIZE],
-        y: &[u8; AFFINE_COORDINATE_SIZE],
-    ) -> Result<Self, Error> {
-        assert_eq!(AFFINE_COORDINATE_SIZE, C::AFFINE_COORDINATE_SIZE);
-        EcKey::try_new_public_key_from_affine_coordinates(C::ec_group(), &x[..], &y[..])
-            .map(|eckey| Self {
-                eckey,
-                marker: PhantomData,
-            })
-            .map_err(|_| Error::ConversionFailed)
-    }
-
-    /// Converts this public key to its affine coordinates.
-    pub fn to_affine_coordinates<const AFFINE_COORDINATE_SIZE: usize>(
-        &self,
-    ) -> ([u8; AFFINE_COORDINATE_SIZE], [u8; AFFINE_COORDINATE_SIZE]) {
-        assert_eq!(AFFINE_COORDINATE_SIZE, C::AFFINE_COORDINATE_SIZE);
-        let (bn_x, bn_y) = self.eckey.to_affine_coordinates();
-
-        let mut x_bytes_uninit = core::mem::MaybeUninit::<[u8; AFFINE_COORDINATE_SIZE]>::uninit();
-        let mut y_bytes_uninit = core::mem::MaybeUninit::<[u8; AFFINE_COORDINATE_SIZE]>::uninit();
-        // Safety:
-        // - `BigNum` guarantees the validity of its ptr
-        // - The size of `x/y_bytes_uninit` and the length passed to `BN_bn2bin_padded` are both
-        //   `AFFINE_COORDINATE_SIZE`
-        let (result_x, result_y) = unsafe {
-            (
-                bssl_sys::BN_bn2bin_padded(
-                    x_bytes_uninit.as_mut_ptr() as *mut _,
-                    AFFINE_COORDINATE_SIZE,
-                    bn_x.as_ptr(),
-                ),
-                bssl_sys::BN_bn2bin_padded(
-                    y_bytes_uninit.as_mut_ptr() as *mut _,
-                    AFFINE_COORDINATE_SIZE,
-                    bn_y.as_ptr(),
-                ),
-            )
-        };
-        assert_eq!(result_x, 1, "bssl_sys::BN_bn2bin_padded failed");
-        assert_eq!(result_y, 1, "bssl_sys::BN_bn2bin_padded failed");
-
-        // Safety: Fields initialized by `BN_bn2bin_padded` above.
-        unsafe { (x_bytes_uninit.assume_init(), y_bytes_uninit.assume_init()) }
-    }
-}
-
-impl<C: Curve> TryFrom<&[u8]> for PublicKey<C> {
-    type Error = Error;
-
-    fn try_from(value: &[u8]) -> Result<Self, Error> {
-        EcKey::try_new_public_key_from_bytes(C::ec_group(), value)
-            .map(|eckey| Self {
-                eckey,
-                marker: PhantomData,
-            })
-            .map_err(|_| Error::ConversionFailed)
-    }
-}
-
-/// Shared secret derived from a Diffie-Hellman key exchange. Don't use the shared key directly,
-/// rather use a KDF and also include the two public values as inputs.
-pub struct SharedSecret<const SIZE: usize>(pub(crate) [u8; SIZE]);
-
-impl<const SIZE: usize> SharedSecret<SIZE> {
-    /// Gets a copy of the shared secret.
-    pub fn to_bytes(&self) -> [u8; SIZE] {
-        self.0
-    }
-
-    /// Gets a reference to the underlying data in this shared secret.
-    pub fn as_bytes(&self) -> &[u8; SIZE] {
-        &self.0
+    /// Return the public key corresponding to this private key.
+    pub fn to_public_key(&self) -> PublicKey<C> {
+        PublicKey {
+            point: self.key.to_point(),
+            marker: PhantomData,
+        }
     }
 }
 
 #[cfg(test)]
-#[allow(clippy::unwrap_used, clippy::expect_used)]
-mod tests {
-    use crate::{
-        ec::{Curve, P224, P256, P384, P521},
-        ecdh::{PrivateKey, PublicKey},
-        test_helpers::decode_hex,
-    };
+mod test {
+    use super::*;
+    use crate::ec::{P256, P384};
 
-    #[test]
-    fn p224_test_diffie_hellman() {
-        // From wycheproof ecdh_secp224r1_ecpoint_test.json, tcId 1
-        // sec1 public key manually extracted from the ASN encoded test data
-        let public_key_bytes: [u8; 57] = decode_hex(concat!(
-            "047d8ac211e1228eb094e285a957d9912e93deee433ed777440ae9fc719b01d0",
-            "50dfbe653e72f39491be87fb1a2742daa6e0a2aada98bb1aca",
-        ));
-        let private_key_bytes: [u8; 28] =
-            decode_hex("565577a49415ca761a0322ad54e4ad0ae7625174baf372c2816f5328");
-        let expected_shared_secret: [u8; 28] =
-            decode_hex("b8ecdb552d39228ee332bafe4886dbff272f7109edf933bc7542bd4f");
+    fn check_curve<C: ec::Curve>() {
+        let alice_private_key = PrivateKey::<C>::generate();
+        let alice_public_key = alice_private_key.to_public_key();
+        let alice_private_key =
+            PrivateKey::<C>::from_big_endian(alice_private_key.to_big_endian().as_ref()).unwrap();
+        let alice_private_key = PrivateKey::<C>::from_der_ec_private_key(
+            alice_private_key.to_der_ec_private_key().as_ref(),
+        )
+        .unwrap();
 
-        let public_key: PublicKey<P224> = (&public_key_bytes[..]).try_into().unwrap();
-        let private_key = PrivateKey::from_private_bytes(&private_key_bytes)
-            .expect("Input private key should be valid");
-        let actual_shared_secret = private_key.diffie_hellman(&public_key).unwrap();
+        let bob_private_key = PrivateKey::<C>::generate();
+        let bob_public_key = bob_private_key.to_public_key();
 
-        assert_eq!(actual_shared_secret.0, expected_shared_secret);
+        let shared_key1 = alice_private_key.compute_shared_key(&bob_public_key);
+        let shared_key2 = bob_private_key.compute_shared_key(&alice_public_key);
+
+        assert_eq!(shared_key1, shared_key2);
     }
 
     #[test]
-    fn p256_test_diffie_hellman() {
-        // From wycheproof ecdh_secp256r1_ecpoint_test.json, tcId 1
-        // sec1 public key manually extracted from the ASN encoded test data
-        let public_key_bytes: [u8; 65] = decode_hex(concat!(
-            "0462d5bd3372af75fe85a040715d0f502428e07046868b0bfdfa61d731afe44f",
-            "26ac333a93a9e70a81cd5a95b5bf8d13990eb741c8c38872b4a07d275a014e30cf",
-        ));
-        let private_key_bytes: [u8; 32] =
-            decode_hex("0612465c89a023ab17855b0a6bcebfd3febb53aef84138647b5352e02c10c346");
-        let expected_shared_secret: [u8; 32] =
-            decode_hex("53020d908b0219328b658b525f26780e3ae12bcd952bb25a93bc0895e1714285");
-
-        let public_key: PublicKey<P256> = (&public_key_bytes[..]).try_into().unwrap();
-        let private_key = PrivateKey::from_private_bytes(&private_key_bytes)
-            .expect("Input private key should be valid");
-        let actual_shared_secret = private_key.diffie_hellman(&public_key).unwrap();
-
-        assert_eq!(actual_shared_secret.0, expected_shared_secret);
+    fn p256() {
+        check_curve::<P256>();
     }
 
     #[test]
-    fn p384_test_diffie_hellman() {
-        // From wycheproof ecdh_secp384r1_ecpoint_test.json, tcId 1
-        // sec1 public key manually extracted from the ASN encoded test data
-        let public_key_bytes: [u8; 97] = decode_hex(concat!(
-            "04790a6e059ef9a5940163183d4a7809135d29791643fc43a2f17ee8bf677ab8",
-            "4f791b64a6be15969ffa012dd9185d8796d9b954baa8a75e82df711b3b56eadf",
-            "f6b0f668c3b26b4b1aeb308a1fcc1c680d329a6705025f1c98a0b5e5bfcb163caa",
-        ));
-        let private_key_bytes: [u8; 48] = decode_hex(concat!(
-            "766e61425b2da9f846c09fc3564b93a6f8603b7392c785165bf20da948c49fd1",
-            "fb1dee4edd64356b9f21c588b75dfd81"
-        ));
-        let expected_shared_secret: [u8; 48] = decode_hex(concat!(
-            "6461defb95d996b24296f5a1832b34db05ed031114fbe7d98d098f93859866e4",
-            "de1e229da71fef0c77fe49b249190135"
-        ));
-
-        let public_key: PublicKey<P384> = (&public_key_bytes[..]).try_into().unwrap();
-        let private_key = PrivateKey::from_private_bytes(&private_key_bytes)
-            .expect("Input private key should be valid");
-        let actual_shared_secret = private_key.diffie_hellman(&public_key).unwrap();
-
-        assert_eq!(actual_shared_secret.0, expected_shared_secret);
-    }
-
-    #[test]
-    fn p521_test_diffie_hellman() {
-        // From wycheproof ecdh_secp521r1_ecpoint_test.json, tcId 1
-        // sec1 public key manually extracted from the ASN encoded test data
-        let public_key_bytes: [u8; 133] = decode_hex(concat!(
-            "040064da3e94733db536a74a0d8a5cb2265a31c54a1da6529a198377fbd38575",
-            "d9d79769ca2bdf2d4c972642926d444891a652e7f492337251adf1613cf30779",
-            "99b5ce00e04ad19cf9fd4722b0c824c069f70c3c0e7ebc5288940dfa92422152",
-            "ae4a4f79183ced375afb54db1409ddf338b85bb6dbfc5950163346bb63a90a70",
-            "c5aba098f7",
-        ));
-        let private_key_bytes: [u8; 66] = decode_hex(concat!(
-            "01939982b529596ce77a94bc6efd03e92c21a849eb4f87b8f619d506efc9bb22",
-            "e7c61640c90d598f795b64566dc6df43992ae34a1341d458574440a7371f611c",
-            "7dcd"
-        ));
-        let expected_shared_secret: [u8; 66] = decode_hex(concat!(
-            "01f1e410f2c6262bce6879a3f46dfb7dd11d30eeee9ab49852102e1892201dd1",
-            "0f27266c2cf7cbccc7f6885099043dad80ff57f0df96acf283fb090de53df95f",
-            "7d87",
-        ));
-
-        let public_key: PublicKey<P521> = (&public_key_bytes[..]).try_into().unwrap();
-        let private_key = PrivateKey::from_private_bytes(&private_key_bytes)
-            .expect("Input private key should be valid");
-        let actual_shared_secret = private_key.diffie_hellman(&public_key).unwrap();
-
-        assert_eq!(actual_shared_secret.0, expected_shared_secret);
-    }
-
-    #[test]
-    fn p224_generate_diffie_hellman_matches() {
-        generate_diffie_hellman_matches::<P224, 28>()
-    }
-
-    #[test]
-    fn p256_generate_diffie_hellman_matches() {
-        generate_diffie_hellman_matches::<P256, 32>()
-    }
-
-    #[test]
-    fn p384_generate_diffie_hellman_matches() {
-        generate_diffie_hellman_matches::<P384, 48>()
-    }
-
-    #[test]
-    fn p521_generate_diffie_hellman_matches() {
-        generate_diffie_hellman_matches::<P521, 66>()
-    }
-
-    fn generate_diffie_hellman_matches<C: Curve, const OUTPUT_SIZE: usize>() {
-        let private_key_1 = PrivateKey::<C>::generate();
-        let private_key_2 = PrivateKey::<C>::generate();
-        let public_key_1 = PublicKey::from(&private_key_1);
-        let public_key_2 = PublicKey::from(&private_key_2);
-
-        let diffie_hellman_1 = private_key_1
-            .diffie_hellman::<OUTPUT_SIZE>(&public_key_2)
-            .unwrap();
-        let diffie_hellman_2 = private_key_2
-            .diffie_hellman::<OUTPUT_SIZE>(&public_key_1)
-            .unwrap();
-
-        assert_eq!(diffie_hellman_1.to_bytes(), diffie_hellman_2.to_bytes());
-    }
-
-    #[test]
-    fn p224_to_private_bytes() {
-        let private_key_bytes: [u8; 28] =
-            decode_hex("565577a49415ca761a0322ad54e4ad0ae7625174baf372c2816f5328");
-        let private_key = PrivateKey::<P224>::from_private_bytes(&private_key_bytes)
-            .expect("Input private key should be valid");
-        assert_eq!(&private_key.to_bytes()[..], &private_key_bytes[..]);
-    }
-
-    #[test]
-    fn p256_to_private_bytes() {
-        let private_key_bytes: [u8; 32] =
-            decode_hex("0612465c89a023ab17855b0a6bcebfd3febb53aef84138647b5352e02c10c346");
-        let private_key = PrivateKey::<P256>::from_private_bytes(&private_key_bytes)
-            .expect("Input private key should be valid");
-        assert_eq!(&private_key.to_bytes()[..], &private_key_bytes[..]);
-    }
-
-    #[test]
-    fn p384_to_private_bytes() {
-        let private_key_bytes: [u8; 48] = decode_hex(concat!(
-            "766e61425b2da9f846c09fc3564b93a6f8603b7392c785165bf20da948c49fd1",
-            "fb1dee4edd64356b9f21c588b75dfd81"
-        ));
-        let private_key = PrivateKey::<P384>::from_private_bytes(&private_key_bytes)
-            .expect("Input private key should be valid");
-        assert_eq!(&private_key.to_bytes()[..], &private_key_bytes[..]);
-    }
-
-    #[test]
-    fn p521_to_private_bytes() {
-        let private_key_bytes: [u8; 66] = decode_hex(concat!(
-            "01939982b529596ce77a94bc6efd03e92c21a849eb4f87b8f619d506efc9bb22",
-            "e7c61640c90d598f795b64566dc6df43992ae34a1341d458574440a7371f611c",
-            "7dcd",
-        ));
-        let private_key = PrivateKey::<P521>::from_private_bytes(&private_key_bytes)
-            .expect("Input private key should be valid");
-        assert_eq!(&private_key.to_bytes()[..], &private_key_bytes[..]);
-    }
-
-    #[test]
-    fn p224_affine_coordinates_test() {
-        affine_coordinates_test::<P224, { P224::AFFINE_COORDINATE_SIZE }>();
-    }
-
-    #[test]
-    fn p256_affine_coordinates_test() {
-        affine_coordinates_test::<P256, { P256::AFFINE_COORDINATE_SIZE }>();
-    }
-
-    #[test]
-    fn p384_affine_coordinates_test() {
-        affine_coordinates_test::<P384, { P384::AFFINE_COORDINATE_SIZE }>();
-    }
-
-    #[test]
-    fn p521_affine_coordinates_test() {
-        affine_coordinates_test::<P521, { P521::AFFINE_COORDINATE_SIZE }>();
-    }
-
-    fn affine_coordinates_test<C: Curve, const AFFINE_COORDINATE_SIZE: usize>() {
-        let private_key = PrivateKey::<C>::generate();
-        let public_key = PublicKey::from(&private_key);
-
-        let (x, y) = public_key.to_affine_coordinates::<AFFINE_COORDINATE_SIZE>();
-
-        let recreated_public_key = PublicKey::from_affine_coordinates(&x, &y);
-        assert_eq!(public_key, recreated_public_key.unwrap());
+    fn p384() {
+        check_curve::<P384>();
     }
 }
diff --git a/rust/bssl-crypto/src/lib.rs b/rust/bssl-crypto/src/lib.rs
index 38096c3..61a8dbc 100644
--- a/rust/bssl-crypto/src/lib.rs
+++ b/rust/bssl-crypto/src/lib.rs
@@ -55,15 +55,13 @@
 
 pub mod x25519;
 
-/// Memory-manipulation operations.
-pub mod mem;
-
-/// Elliptic curve diffie-hellman operations.
+pub mod ec;
 pub mod ecdh;
 
-pub(crate) mod bn;
-pub(crate) mod ec;
-pub(crate) mod pkey;
+mod scoped;
+
+mod mem;
+pub use mem::constant_time_compare;
 
 #[cfg(test)]
 mod test_helpers;
@@ -365,6 +363,85 @@
     Some(ret)
 }
 
+/// Buffer represents an owned chunk of memory on the BoringSSL heap.
+/// Call `as_ref()` to get a `&[u8]` from it.
+pub struct Buffer {
+    // This pointer is always allocated by BoringSSL and must be freed using
+    // `OPENSSL_free`.
+    pub(crate) ptr: *mut u8,
+    pub(crate) len: usize,
+}
+
+impl AsRef<[u8]> for Buffer {
+    fn as_ref(&self) -> &[u8] {
+        if self.len == 0 {
+            return &[];
+        }
+        // Safety: `ptr` and `len` describe a valid area of memory and `ptr`
+        // must be Rust-valid because `len` is non-zero.
+        unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
+    }
+}
+
+impl Drop for Buffer {
+    fn drop(&mut self) {
+        // Safety: `ptr` is owned by this object and is on the BoringSSL heap.
+        unsafe {
+            bssl_sys::OPENSSL_free(self.ptr as *mut core::ffi::c_void);
+        }
+    }
+}
+
+/// Calls `parse_func` with a `CBS` structure pointing at `data`.
+/// If that returns a null pointer then it returns [None].
+/// Otherwise, if there's still data left in CBS, it calls `free_func` on the
+/// pointer and returns [None]. Otherwise it returns the pointer.
+fn parse_with_cbs<T, Parse, Free>(data: &[u8], free_func: Free, parse_func: Parse) -> Option<*mut T>
+where
+    Parse: FnOnce(*mut bssl_sys::CBS) -> *mut T,
+    Free: FnOnce(*mut T),
+{
+    // Safety: type checking ensures that `cbs` is the correct size.
+    let mut cbs =
+        unsafe { initialized_struct(|cbs| bssl_sys::CBS_init(cbs, data.as_ffi_ptr(), data.len())) };
+    let ptr = parse_func(&mut cbs);
+    if ptr.is_null() {
+        return None;
+    }
+    // Safety: `cbs` is still valid after parsing.
+    if unsafe { bssl_sys::CBS_len(&cbs) } != 0 {
+        // Safety: `ptr` is still owned by this function.
+        free_func(ptr);
+        return None;
+    }
+    Some(ptr)
+}
+
+/// Calls `func` with a `CBB` pointer and returns a [Buffer] of the ultimate
+/// contents of that CBB.
+#[allow(clippy::unwrap_used)]
+fn cbb_to_buffer<F: FnOnce(*mut bssl_sys::CBB)>(initial_capacity: usize, func: F) -> Buffer {
+    // Safety: type checking ensures that `cbb` is the correct size.
+    let mut cbb = unsafe {
+        initialized_struct_fallible(|cbb| bssl_sys::CBB_init(cbb, initial_capacity) == 1)
+    }
+    // `CBB_init` only fails if out of memory, which isn't something that this crate handles.
+    .unwrap();
+    func(&mut cbb);
+
+    let mut ptr: *mut u8 = core::ptr::null_mut();
+    let mut len: usize = 0;
+    // `CBB_finish` only fails on programming error, which we convert into a
+    // panic.
+    assert_eq!(1, unsafe {
+        bssl_sys::CBB_finish(&mut cbb, &mut ptr, &mut len)
+    });
+
+    // Safety: `ptr` is on the BoringSSL heap and ownership is returned by
+    // `CBB_finish`.
+    Buffer { ptr, len }
+}
+
 /// Used to prevent external implementations of internal traits.
 mod sealed {
     pub struct Sealed;
diff --git a/rust/bssl-crypto/src/mem.rs b/rust/bssl-crypto/src/mem.rs
index a9031c4..9426dff 100644
--- a/rust/bssl-crypto/src/mem.rs
+++ b/rust/bssl-crypto/src/mem.rs
@@ -13,21 +13,23 @@
  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+use crate::FfiSlice;
+
 /// Returns true iff `a` and `b` contain the same bytes. It takes an amount of time dependent on the
 /// lengths, but independent of the contents of the slices `a` and `b`. The return type is a `bool`,
 /// since unlike `memcmp` in C this function cannot be used to put elements into a defined order.
-pub fn crypto_memcmp(a: &[u8], b: &[u8]) -> bool {
+pub fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
     if a.len() != b.len() {
         return false;
     }
-    if a.is_empty() && b.is_empty() {
+    if a.is_empty() {
         // Avoid FFI issues with empty slices that may potentially cause UB
         return true;
     }
     // Safety:
     // - The lengths of a and b are checked above.
     let result =
-        unsafe { bssl_sys::CRYPTO_memcmp(a.as_ptr() as *const _, b.as_ptr() as *const _, a.len()) };
+        unsafe { bssl_sys::CRYPTO_memcmp(a.as_ffi_void_ptr(), b.as_ffi_void_ptr(), a.len()) };
     result == 0
 }
 
@@ -37,26 +39,26 @@
 
     #[test]
     fn test_different_length() {
-        assert!(!crypto_memcmp(&[0, 1, 2], &[0]))
+        assert!(!constant_time_compare(&[0, 1, 2], &[0]))
     }
 
     #[test]
     fn test_same_length_different_content() {
-        assert!(!crypto_memcmp(&[0, 1, 2], &[1, 2, 3]))
+        assert!(!constant_time_compare(&[0, 1, 2], &[1, 2, 3]))
     }
 
     #[test]
     fn test_same_content() {
-        assert!(crypto_memcmp(&[0, 1, 2], &[0, 1, 2]))
+        assert!(constant_time_compare(&[0, 1, 2], &[0, 1, 2]))
     }
 
     #[test]
     fn test_empty_slices() {
-        assert!(crypto_memcmp(&[], &[]))
+        assert!(constant_time_compare(&[], &[]))
     }
 
     #[test]
     fn test_empty_slices_different() {
-        assert!(!crypto_memcmp(&[], &[0, 1, 2]))
+        assert!(!constant_time_compare(&[], &[0, 1, 2]))
     }
 }
diff --git a/rust/bssl-crypto/src/pkey.rs b/rust/bssl-crypto/src/pkey.rs
deleted file mode 100644
index aa60a9f..0000000
--- a/rust/bssl-crypto/src/pkey.rs
+++ /dev/null
@@ -1,102 +0,0 @@
-/* Copyright (c) 2023, 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.
- */
-
-//! `Pkey` and `PkeyCtx` classes for holding asymmetric keys. This module is intended for internal
-//! use within this crate only, to create higher-level abstractions suitable to be exposed
-//! externally.
-
-use crate::{ec::EcKey, CSliceMut, ForeignType};
-use alloc::{borrow::ToOwned, string::String};
-
-pub(crate) struct Pkey {
-    ptr: *mut bssl_sys::EVP_PKEY,
-}
-
-// Safety: Implementation ensures `from_ptr(x).as_ptr == x`
-unsafe impl ForeignType for Pkey {
-    type CType = bssl_sys::EVP_PKEY;
-
-    unsafe fn from_ptr(ptr: *mut Self::CType) -> Self {
-        Self { ptr }
-    }
-
-    fn as_ptr(&self) -> *mut Self::CType {
-        self.ptr
-    }
-}
-
-impl From<&EcKey> for Pkey {
-    fn from(eckey: &EcKey) -> Self {
-        // Safety: EVP_PKEY_new does not have any preconditions
-        let pkey = unsafe { bssl_sys::EVP_PKEY_new() };
-        assert!(!pkey.is_null());
-        // Safety:
-        // - pkey is just allocated and is null-checked
-        // - EcKey ensures eckey.ptr is valid during its lifetime
-        // - EVP_PKEY_set1_EC_KEY doesn't take ownership
-        let result = unsafe { bssl_sys::EVP_PKEY_set1_EC_KEY(pkey, eckey.as_ptr()) };
-        assert_eq!(result, 1, "bssl_sys::EVP_PKEY_set1_EC_KEY failed");
-        Self { ptr: pkey }
-    }
-}
-
-impl Drop for Pkey {
-    fn drop(&mut self) {
-        // Safety: `self.ptr` is owned by this struct
-        unsafe { bssl_sys::EVP_PKEY_free(self.ptr) }
-    }
-}
-
-pub(crate) struct PkeyCtx {
-    ptr: *mut bssl_sys::EVP_PKEY_CTX,
-}
-
-impl PkeyCtx {
-    pub fn new(pkey: &Pkey) -> Self {
-        // Safety:
-        // - `Pkey` ensures `pkey.ptr` is valid, and EVP_PKEY_CTX_new does not take ownership.
-        let pkeyctx = unsafe { bssl_sys::EVP_PKEY_CTX_new(pkey.ptr, core::ptr::null_mut()) };
-        assert!(!pkeyctx.is_null());
-        Self { ptr: pkeyctx }
-    }
-
-    #[allow(clippy::panic)]
-    pub(crate) fn diffie_hellman(
-        self,
-        other_public_key: &Pkey,
-        mut output: CSliceMut,
-    ) -> Result<(), String> {
-        let result = unsafe { bssl_sys::EVP_PKEY_derive_init(self.ptr) };
-        assert_eq!(result, 1, "bssl_sys::EVP_PKEY_derive_init failed");
-
-        let result = unsafe { bssl_sys::EVP_PKEY_derive_set_peer(self.ptr, other_public_key.ptr) };
-        assert_eq!(result, 1, "bssl_sys::EVP_PKEY_derive_set_peer failed");
-
-        let result =
-            unsafe { bssl_sys::EVP_PKEY_derive(self.ptr, output.as_mut_ptr(), &mut output.len()) };
-        match result {
-            0 => Err("bssl_sys::EVP_PKEY_derive failed".to_owned()),
-            1 => Ok(()),
-            _ => panic!("Unexpected result {result:?} from bssl_sys::EVP_PKEY_derive"),
-        }
-    }
-}
-
-impl Drop for PkeyCtx {
-    fn drop(&mut self) {
-        // Safety: self.ptr is owned by this struct
-        unsafe { bssl_sys::EVP_PKEY_CTX_free(self.ptr) }
-    }
-}
diff --git a/rust/bssl-crypto/src/scoped.rs b/rust/bssl-crypto/src/scoped.rs
new file mode 100644
index 0000000..fd889a5
--- /dev/null
+++ b/rust/bssl-crypto/src/scoped.rs
@@ -0,0 +1,66 @@
+/* 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.
+ */
+
+//! Helpers to ensure that some temporary objects are always freed.
+
+/// A scoped `EC_KEY`.
+pub struct EvpPkey(*mut bssl_sys::EVP_PKEY);
+
+impl EvpPkey {
+    pub fn new() -> Self {
+        let ptr = unsafe { bssl_sys::EVP_PKEY_new() };
+        // `ptr` is only NULL if we're out of memory, which this crate
+        // doesn't handle.
+        assert!(!ptr.is_null());
+        EvpPkey(ptr)
+    }
+
+    pub fn from_ptr(ptr: *mut bssl_sys::EVP_PKEY) -> Self {
+        EvpPkey(ptr)
+    }
+
+    pub fn as_ffi_ptr(&mut self) -> *mut bssl_sys::EVP_PKEY {
+        self.0
+    }
+}
+
+impl Drop for EvpPkey {
+    fn drop(&mut self) {
+        unsafe { bssl_sys::EVP_PKEY_free(self.0) }
+    }
+}
+
+/// A scoped `EC_KEY`.
+pub struct EcKey(*mut bssl_sys::EC_KEY);
+
+impl EcKey {
+    pub fn new() -> Self {
+        let ptr = unsafe { bssl_sys::EC_KEY_new() };
+        // `ptr` is only NULL if we're out of memory, which this crate
+        // doesn't handle.
+        assert!(!ptr.is_null());
+        EcKey(ptr)
+    }
+
+    pub fn as_ffi_ptr(&mut self) -> *mut bssl_sys::EC_KEY {
+        self.0
+    }
+}
+
+impl Drop for EcKey {
+    fn drop(&mut self) {
+        unsafe { bssl_sys::EC_KEY_free(self.0) }
+    }
+}