Skip to main content

ring_native_ossl/
agreement.rs

1//! Ephemeral Diffie-Hellman key agreement, mirroring `ring::agreement`.
2//!
3//! Supports X25519, ECDH on P-256, and ECDH on P-384.  Each key agreement
4//! is one-shot: the private key is consumed by [`agree_ephemeral`] and cannot
5//! be reused.  EC peer public keys must use the uncompressed point format
6//! (first byte `0x04`); compressed points are rejected before any OpenSSL
7//! operation is attempted.
8
9use crate::error::Unspecified;
10use native_ossl::params::ParamBuilder;
11use native_ossl::pkey::{DeriveCtx, KeygenCtx, Pkey, Private, Public};
12use std::ffi::CStr;
13
14use crate::spki::{P256_SPKI_HEADER, P384_SPKI_HEADER, X25519_SPKI_HEADER};
15
16// ── Algorithm descriptor ──────────────────────────────────────────────────────
17
18/// ECDH algorithm descriptor (mirrors `ring::agreement::Algorithm`).
19#[derive(Debug)]
20pub struct Algorithm {
21    keygen_name: &'static CStr,
22    group_param: Option<&'static CStr>,
23    spki_header: &'static [u8],
24    raw_key_len: usize,
25}
26
27pub static X25519: Algorithm = Algorithm {
28    keygen_name: c"X25519",
29    group_param: None,
30    spki_header: X25519_SPKI_HEADER,
31    raw_key_len: 32,
32};
33
34pub static ECDH_P256: Algorithm = Algorithm {
35    keygen_name: c"EC",
36    group_param: Some(c"P-256"),
37    spki_header: P256_SPKI_HEADER,
38    raw_key_len: 65,
39};
40
41pub static ECDH_P384: Algorithm = Algorithm {
42    keygen_name: c"EC",
43    group_param: Some(c"P-384"),
44    spki_header: P384_SPKI_HEADER,
45    raw_key_len: 97,
46};
47
48// ── EphemeralPrivateKey ───────────────────────────────────────────────────────
49
50/// An ephemeral private key for key agreement (mirrors `ring::agreement::EphemeralPrivateKey`).
51pub struct EphemeralPrivateKey {
52    alg: &'static Algorithm,
53    priv_key: Pkey<Private>,
54    pub_key_bytes: Vec<u8>,
55}
56
57impl EphemeralPrivateKey {
58    /// Generate a new ephemeral private key.
59    ///
60    /// # Errors
61    ///
62    /// Returns `Unspecified` if OpenSSL key generation fails.
63    pub fn generate(
64        alg: &'static Algorithm,
65        _rng: &dyn crate::rand::SecureRandom,
66    ) -> Result<Self, Unspecified> {
67        let mut ctx = KeygenCtx::new(alg.keygen_name).map_err(|_| Unspecified)?;
68
69        if let Some(group_name) = alg.group_param {
70            let params = ParamBuilder::new()
71                .and_then(|b| b.push_utf8_string(c"group", group_name))
72                .and_then(ParamBuilder::build)
73                .map_err(|_| Unspecified)?;
74            ctx.set_params(&params).map_err(|_| Unspecified)?;
75        }
76
77        let priv_key = ctx.generate().map_err(|_| Unspecified)?;
78
79        let spki = priv_key.public_key_to_der().map_err(|_| Unspecified)?;
80        let raw_pub = spki
81            .get(alg.spki_header.len()..)
82            .ok_or(Unspecified)?
83            .to_vec();
84
85        Ok(Self {
86            alg,
87            priv_key,
88            pub_key_bytes: raw_pub,
89        })
90    }
91
92    /// Return the public key corresponding to this private key.
93    ///
94    /// # Errors
95    ///
96    /// Returns `Unspecified` if OpenSSL fails to export the public key.
97    pub fn compute_public_key(&self) -> Result<PublicKey, Unspecified> {
98        Ok(PublicKey {
99            alg: self.alg,
100            bytes: self.pub_key_bytes.clone(),
101        })
102    }
103
104    #[must_use]
105    pub fn algorithm(&self) -> &'static Algorithm {
106        self.alg
107    }
108}
109
110impl std::fmt::Debug for EphemeralPrivateKey {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.debug_struct("EphemeralPrivateKey")
113            .field("alg", &self.alg)
114            .finish_non_exhaustive()
115    }
116}
117
118// ── PublicKey ─────────────────────────────────────────────────────────────────
119
120/// Public key derived from an ephemeral private key (mirrors `ring::agreement::PublicKey`).
121#[derive(Debug)]
122pub struct PublicKey {
123    alg: &'static Algorithm,
124    bytes: Vec<u8>,
125}
126
127impl PublicKey {
128    #[must_use]
129    pub fn algorithm(&self) -> &'static Algorithm {
130        self.alg
131    }
132}
133
134impl AsRef<[u8]> for PublicKey {
135    fn as_ref(&self) -> &[u8] {
136        &self.bytes
137    }
138}
139
140// ── UnparsedPublicKey ─────────────────────────────────────────────────────────
141
142/// A peer's raw public key bytes (mirrors `ring::agreement::UnparsedPublicKey`).
143#[derive(Debug)]
144pub struct UnparsedPublicKey<B> {
145    alg: &'static Algorithm,
146    bytes: B,
147}
148
149impl<B: AsRef<[u8]>> UnparsedPublicKey<B> {
150    pub fn new(algorithm: &'static Algorithm, bytes: B) -> Self {
151        Self {
152            alg: algorithm,
153            bytes,
154        }
155    }
156
157    pub fn algorithm(&self) -> &'static Algorithm {
158        self.alg
159    }
160}
161
162impl<B: AsRef<[u8]>> AsRef<[u8]> for UnparsedPublicKey<B> {
163    fn as_ref(&self) -> &[u8] {
164        self.bytes.as_ref()
165    }
166}
167
168// ── agree_ephemeral ───────────────────────────────────────────────────────────
169
170/// Perform one-shot ECDH key agreement (mirrors `ring::agreement::agree_ephemeral`).
171///
172/// Calls `kdf` with the raw shared secret bytes if ECDH succeeds.  On ECDH
173/// failure, returns `Err(error_value)`.  If `kdf` returns an error, that error
174/// is propagated directly.
175///
176/// # Errors
177///
178/// Returns `Err(error_value)` if the ECDH operation fails, or the error from `kdf`.
179#[allow(clippy::needless_pass_by_value)]
180pub fn agree_ephemeral<B, F, R, E>(
181    my_private_key: EphemeralPrivateKey,
182    peer_public_key: &UnparsedPublicKey<B>,
183    error_value: E,
184    kdf: F,
185) -> Result<R, E>
186where
187    B: AsRef<[u8]>,
188    F: FnOnce(&[u8]) -> Result<R, E>,
189{
190    match do_ecdh(
191        &my_private_key.priv_key,
192        my_private_key.alg,
193        peer_public_key,
194    ) {
195        Ok(secret) => kdf(&secret),
196        Err(()) => Err(error_value),
197    }
198}
199
200fn do_ecdh<B>(
201    priv_key: &Pkey<Private>,
202    alg: &'static Algorithm,
203    peer_public_key: &UnparsedPublicKey<B>,
204) -> Result<Vec<u8>, ()>
205where
206    B: AsRef<[u8]>,
207{
208    let peer_bytes = peer_public_key.bytes.as_ref();
209    if peer_bytes.len() != alg.raw_key_len {
210        return Err(());
211    }
212    // EC uncompressed-point format: first byte must be 0x04.
213    if alg.group_param.is_some() && peer_bytes.first() != Some(&0x04) {
214        return Err(());
215    }
216
217    let mut spki = alg.spki_header.to_vec();
218    spki.extend_from_slice(peer_bytes);
219
220    let peer_key = Pkey::<Public>::from_der(&spki).map_err(|_| ())?;
221
222    let mut derive = DeriveCtx::new(priv_key).map_err(|_| ())?;
223    derive.set_peer(&peer_key).map_err(|_| ())?;
224
225    let len = derive.derive_len().map_err(|_| ())?;
226    let mut secret = vec![0u8; len];
227    derive.derive(&mut secret).map_err(|_| ())?;
228    Ok(secret)
229}