Add ecdh and P256 bindings to bssl-crypto

Bug: 285223043
Change-Id: Ia997b9765476d05c58649ee49ebf04905e65c478
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/60267
Reviewed-by: Bob Beck <bbe@google.com>
Commit-Queue: Bob Beck <bbe@google.com>
diff --git a/rust/bssl-crypto/src/bn.rs b/rust/bssl-crypto/src/bn.rs
new file mode 100644
index 0000000..35a196a
--- /dev/null
+++ b/rust/bssl-crypto/src/bn.rs
@@ -0,0 +1,61 @@
+/* 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
new file mode 100644
index 0000000..06f74d3
--- /dev/null
+++ b/rust/bssl-crypto/src/ec.rs
@@ -0,0 +1,421 @@
+/* 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.
+ */
+
+//! `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.
+
+use core::panic;
+use std::{borrow::Borrow, fmt::Debug, ops::Deref};
+
+use crate::{bn::BigNum, CSlice, CSliceMut, ForeignType, ForeignTypeRef};
+
+#[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`].
+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;
+}
+
+/// 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`.
+#[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 _) }
+    }
+}
+
+/// The P-384 curve, corresponding to `NID_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 _) }
+    }
+}
+
+/// The P-521 curve, corresponding to `NID_secp521r1`.
+#[derive(Debug)]
+pub struct P521;
+
+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 _) }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::ec::P521;
+
+    use super::{Curve, EcGroupRef, P256};
+
+    #[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);
+    }
+
+    #[test]
+    fn test_ec_group_not_equal() {
+        let group = P256::ec_group();
+        let group2 = P521::ec_group();
+        assert!(group != group2)
+    }
+}
diff --git a/rust/bssl-crypto/src/ecdh.rs b/rust/bssl-crypto/src/ecdh.rs
new file mode 100644
index 0000000..ec8944c
--- /dev/null
+++ b/rust/bssl-crypto/src/ecdh.rs
@@ -0,0 +1,412 @@
+/* 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 std::marker::PhantomData;
+
+use crate::{
+    ec::{Curve, EcKey},
+    pkey::{Pkey, PkeyCtx},
+    CSliceMut, ForeignType,
+};
+
+/// Private key used in a elliptic curve Diffie-Hellman.
+pub struct PrivateKey<C: Curve> {
+    /// An EcKey containing the private-public key pair
+    eckey: EcKey,
+    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)
+    }
+
+    /// Generate a new private key for use in a Diffie-Hellman key exchange.
+    pub fn generate() -> Self {
+        Self {
+            eckey: EcKey::generate(C::ec_group()),
+            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(),
+            marker: PhantomData,
+        }
+    }
+}
+
+/// 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
+    }
+}
+
+#[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,
+    };
+
+    #[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");
+
+        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();
+
+        assert_eq!(actual_shared_secret.0, expected_shared_secret);
+    }
+
+    #[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);
+    }
+
+    #[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());
+    }
+}
diff --git a/rust/bssl-crypto/src/hkdf.rs b/rust/bssl-crypto/src/hkdf.rs
index efd569b..8eadcd0 100644
--- a/rust/bssl-crypto/src/hkdf.rs
+++ b/rust/bssl-crypto/src/hkdf.rs
@@ -94,6 +94,12 @@
 }
 
 #[cfg(test)]
+#[allow(
+    clippy::expect_used,
+    clippy::panic,
+    clippy::indexing_slicing,
+    clippy::unwrap_used
+)]
 mod tests {
     use crate::hkdf::{HkdfSha256, HkdfSha512};
     use crate::test_helpers::{decode_hex, decode_hex_into_vec};
diff --git a/rust/bssl-crypto/src/lib.rs b/rust/bssl-crypto/src/lib.rs
index 99140b7..0ad352d 100644
--- a/rust/bssl-crypto/src/lib.rs
+++ b/rust/bssl-crypto/src/lib.rs
@@ -53,6 +53,13 @@
 /// Memory-manipulation operations.
 pub mod mem;
 
+/// Elliptic curve diffie-hellman operations.
+pub mod ecdh;
+
+pub(crate) mod bn;
+pub(crate) mod ec;
+pub(crate) mod pkey;
+
 #[cfg(test)]
 mod test_helpers;
 
@@ -111,7 +118,7 @@
 /// Implementations of `ForeignTypeRef` must guarantee the following:
 ///
 /// - `Self::from_ptr(x).as_ptr() == x`
-/// - `Self::from_mut_ptr(x).as_ptr() == x`
+/// - `Self::from_ptr_mut(x).as_ptr() == x`
 unsafe trait ForeignTypeRef: Sized {
     /// The raw C type.
     type CType;
@@ -144,3 +151,26 @@
         self as *const _ as *mut _
     }
 }
+
+/// A helper trait implemented by types which has an owned reference to foreign types.
+///
+/// # Safety
+///
+/// Implementations of `ForeignType` must guarantee the following:
+///
+/// - `Self::from_ptr(x).as_ptr() == x`
+unsafe trait ForeignType {
+    /// The raw C type.
+    type CType;
+
+    /// Constructs an instance of this type from its raw type.
+    ///
+    /// # Safety
+    ///
+    /// - `ptr` must be a valid, immutable, instance of `CType`.
+    /// - Ownership of `ptr` is passed to the implementation, and will free `ptr` when dropped.
+    unsafe fn from_ptr(ptr: *mut Self::CType) -> Self;
+
+    /// Returns a raw pointer to the wrapped value.
+    fn as_ptr(&self) -> *mut Self::CType;
+}
diff --git a/rust/bssl-crypto/src/pkey.rs b/rust/bssl-crypto/src/pkey.rs
new file mode 100644
index 0000000..f72c13c
--- /dev/null
+++ b/rust/bssl-crypto/src/pkey.rs
@@ -0,0 +1,102 @@
+/* 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};
+
+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/test_helpers.rs b/rust/bssl-crypto/src/test_helpers.rs
index ea2d9db..e4b4afb 100644
--- a/rust/bssl-crypto/src/test_helpers.rs
+++ b/rust/bssl-crypto/src/test_helpers.rs
@@ -13,6 +13,7 @@
  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#[allow(clippy::expect_used, clippy::unwrap_used, clippy::indexing_slicing)]
 pub(crate) fn decode_hex<const N: usize>(s: &str) -> [u8; N] {
     (0..s.len())
         .step_by(2)
@@ -23,6 +24,7 @@
         .unwrap()
 }
 
+#[allow(clippy::expect_used, clippy::unwrap_used, clippy::indexing_slicing)]
 pub(crate) fn decode_hex_into_vec(s: &str) -> Vec<u8> {
     (0..s.len())
         .step_by(2)