askar_crypto/alg/
x25519.rs

1//! X25519 key exchange support on Curve25519
2
3use core::{
4    convert::{TryFrom, TryInto},
5    fmt::{self, Debug, Formatter},
6};
7
8use subtle::ConstantTimeEq;
9use x25519_dalek::{PublicKey, StaticSecret as SecretKey};
10use zeroize::Zeroizing;
11
12use super::{ed25519::Ed25519KeyPair, HasKeyAlg, HasKeyBackend, KeyAlg};
13use crate::{
14    buffer::{ArrayKey, WriteBuffer},
15    error::Error,
16    generic_array::typenum::{U32, U64},
17    jwk::{FromJwk, JwkEncoder, JwkParts, ToJwk},
18    kdf::KeyExchange,
19    random::KeyMaterial,
20    repr::{KeyGen, KeyMeta, KeyPublicBytes, KeySecretBytes, KeypairBytes, KeypairMeta},
21};
22
23// FIXME: reject low-order points?
24// <https://github.com/tendermint/tmkms/pull/279>
25// vs. <https://cr.yp.to/ecdh.html> which indicates that all points are safe for normal D-H.
26
27/// The length of a public key in bytes
28pub const PUBLIC_KEY_LENGTH: usize = 32;
29/// The length of a secret key in bytes
30pub const SECRET_KEY_LENGTH: usize = 32;
31/// The length of a keypair in bytes
32pub const KEYPAIR_LENGTH: usize = SECRET_KEY_LENGTH + PUBLIC_KEY_LENGTH;
33
34/// The 'kty' value of an X25519 JWK
35pub const JWK_KEY_TYPE: &str = "OKP";
36/// The 'crv' value of an X25519 JWK
37pub const JWK_CURVE: &str = "X25519";
38
39/// An X25519 public key or keypair
40#[derive(Clone)]
41pub struct X25519KeyPair {
42    // SECURITY: SecretKey (StaticSecret) zeroizes on drop
43    pub(crate) secret: Option<SecretKey>,
44    pub(crate) public: PublicKey,
45}
46
47impl X25519KeyPair {
48    #[inline(always)]
49    pub(crate) fn new(sk: Option<SecretKey>, pk: PublicKey) -> Self {
50        Self {
51            secret: sk,
52            public: pk,
53        }
54    }
55
56    #[inline]
57    pub(crate) fn from_secret_key(sk: SecretKey) -> Self {
58        let public = PublicKey::from(&sk);
59        Self {
60            secret: Some(sk),
61            public,
62        }
63    }
64
65    pub(crate) fn check_public_bytes(&self, pk: &[u8]) -> Result<(), Error> {
66        if self.public.as_bytes().ct_eq(pk).into() {
67            Ok(())
68        } else {
69            Err(err_msg!(InvalidKeyData, "invalid x25519 keypair"))
70        }
71    }
72}
73
74impl Debug for X25519KeyPair {
75    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
76        f.debug_struct("X25519KeyPair")
77            .field(
78                "secret",
79                if self.secret.is_some() {
80                    &"<secret>"
81                } else {
82                    &"None"
83                },
84            )
85            .field("public", &self.public)
86            .finish()
87    }
88}
89
90impl HasKeyBackend for X25519KeyPair {}
91
92impl HasKeyAlg for X25519KeyPair {
93    fn algorithm(&self) -> KeyAlg {
94        KeyAlg::X25519
95    }
96}
97
98impl KeyMeta for X25519KeyPair {
99    type KeySize = U32;
100}
101
102impl KeyGen for X25519KeyPair {
103    fn generate(rng: impl KeyMaterial) -> Result<Self, Error> {
104        let sk = ArrayKey::<U32>::generate(rng);
105        let sk = SecretKey::from(*<&[u8; SECRET_KEY_LENGTH]>::try_from(&sk).unwrap());
106        let pk = PublicKey::from(&sk);
107        Ok(Self::new(Some(sk), pk))
108    }
109}
110
111impl KeySecretBytes for X25519KeyPair {
112    fn from_secret_bytes(key: &[u8]) -> Result<Self, Error> {
113        let sk: &[u8; SECRET_KEY_LENGTH] = key.try_into().map_err(|_| err_msg!(InvalidKeyData))?;
114        Ok(Self::from_secret_key(SecretKey::from(*sk)))
115    }
116
117    fn with_secret_bytes<O>(&self, f: impl FnOnce(Option<&[u8]>) -> O) -> O {
118        if let Some(sk) = self.secret.as_ref() {
119            let b = Zeroizing::new(sk.to_bytes());
120            f(Some(&b[..]))
121        } else {
122            f(None)
123        }
124    }
125}
126
127impl KeypairMeta for X25519KeyPair {
128    type PublicKeySize = U32;
129    type KeypairSize = U64;
130}
131
132impl KeypairBytes for X25519KeyPair {
133    fn from_keypair_bytes(kp: &[u8]) -> Result<Self, Error> {
134        if kp.len() != KEYPAIR_LENGTH {
135            return Err(err_msg!(InvalidKeyData));
136        }
137        let result = Self::from_secret_bytes(&kp[..SECRET_KEY_LENGTH])?;
138        result.check_public_bytes(&kp[SECRET_KEY_LENGTH..])?;
139        Ok(result)
140    }
141
142    fn with_keypair_bytes<O>(&self, f: impl FnOnce(Option<&[u8]>) -> O) -> O {
143        if let Some(secret) = self.secret.as_ref() {
144            ArrayKey::<<Self as KeypairMeta>::KeypairSize>::temp(|arr| {
145                arr[..SECRET_KEY_LENGTH].copy_from_slice(secret.as_bytes());
146                arr[SECRET_KEY_LENGTH..].copy_from_slice(self.public.as_bytes());
147                f(Some(&*arr))
148            })
149        } else {
150            f(None)
151        }
152    }
153}
154
155impl KeyPublicBytes for X25519KeyPair {
156    fn from_public_bytes(key: &[u8]) -> Result<Self, Error> {
157        let pk: &[u8; PUBLIC_KEY_LENGTH] = key.try_into().map_err(|_| err_msg!(InvalidKeyData))?;
158        Ok(Self::new(None, PublicKey::from(*pk)))
159    }
160
161    fn with_public_bytes<O>(&self, f: impl FnOnce(&[u8]) -> O) -> O {
162        f(&self.public.to_bytes()[..])
163    }
164}
165
166impl ToJwk for X25519KeyPair {
167    fn encode_jwk(&self, enc: &mut dyn JwkEncoder) -> Result<(), Error> {
168        enc.add_str("crv", JWK_CURVE)?;
169        enc.add_str("kty", JWK_KEY_TYPE)?;
170        self.with_public_bytes(|buf| enc.add_as_base64("x", buf))?;
171        if enc.is_secret() {
172            self.with_secret_bytes(|buf| {
173                if let Some(sk) = buf {
174                    enc.add_as_base64("d", sk)
175                } else {
176                    Ok(())
177                }
178            })?;
179        }
180        Ok(())
181    }
182}
183
184impl FromJwk for X25519KeyPair {
185    fn from_jwk_parts(jwk: JwkParts<'_>) -> Result<Self, Error> {
186        if jwk.kty != JWK_KEY_TYPE {
187            return Err(err_msg!(InvalidKeyData, "Unsupported key type"));
188        }
189        if jwk.crv != JWK_CURVE {
190            return Err(err_msg!(InvalidKeyData, "Unsupported key algorithm"));
191        }
192        ArrayKey::<U32>::temp(|pk_arr| {
193            if jwk.x.decode_base64(pk_arr)? != pk_arr.len() {
194                Err(err_msg!(InvalidKeyData))
195            } else if jwk.d.is_some() {
196                ArrayKey::<U32>::temp(|sk_arr| {
197                    if jwk.d.decode_base64(sk_arr)? != sk_arr.len() {
198                        Err(err_msg!(InvalidKeyData))
199                    } else {
200                        let kp = X25519KeyPair::from_secret_bytes(sk_arr)?;
201                        kp.check_public_bytes(pk_arr)?;
202                        Ok(kp)
203                    }
204                })
205            } else {
206                X25519KeyPair::from_public_bytes(pk_arr)
207            }
208        })
209    }
210}
211
212impl KeyExchange for X25519KeyPair {
213    fn write_key_exchange(&self, other: &Self, out: &mut dyn WriteBuffer) -> Result<(), Error> {
214        match self.secret.as_ref() {
215            Some(sk) => {
216                let xk = sk.diffie_hellman(&other.public);
217                out.buffer_write(xk.as_bytes())?;
218                Ok(())
219            }
220            None => Err(err_msg!(MissingSecretKey)),
221        }
222    }
223}
224
225impl TryFrom<&Ed25519KeyPair> for X25519KeyPair {
226    type Error = Error;
227
228    fn try_from(value: &Ed25519KeyPair) -> Result<Self, Self::Error> {
229        Ok(value.to_x25519_keypair())
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use base64::Engine;
236
237    use super::*;
238    use crate::repr::ToPublicBytes;
239
240    #[test]
241    fn jwk_expected() {
242        // {
243        //   "kty": "OKP",
244        //   "d": "qL25gw-HkNJC9m4EsRzCoUx1KntjwHPzxo6a2xUcyFQ",
245        //   "use": "enc",
246        //   "crv": "X25519",
247        //   "x": "tGskN_ae61DP4DLY31_fjkbvnKqf-ze7kA6Cj2vyQxU"
248        // }
249        let test_pvt_b64 = "qL25gw-HkNJC9m4EsRzCoUx1KntjwHPzxo6a2xUcyFQ";
250        let test_pvt = base64::engine::general_purpose::URL_SAFE_NO_PAD
251            .decode(test_pvt_b64)
252            .unwrap();
253        let kp =
254            X25519KeyPair::from_secret_bytes(&test_pvt).expect("Error creating x25519 keypair");
255        let jwk = kp
256            .to_jwk_public(None)
257            .expect("Error converting public key to JWK");
258        let jwk = JwkParts::try_from_str(&jwk).expect("Error parsing JWK output");
259        assert_eq!(jwk.kty, JWK_KEY_TYPE);
260        assert_eq!(jwk.crv, JWK_CURVE);
261        assert_eq!(jwk.x, "tGskN_ae61DP4DLY31_fjkbvnKqf-ze7kA6Cj2vyQxU");
262        assert_eq!(jwk.d, None);
263        let pk_load = X25519KeyPair::from_jwk_parts(jwk).unwrap();
264        assert_eq!(kp.to_public_bytes(), pk_load.to_public_bytes());
265
266        let jwk = kp
267            .to_jwk_secret(None)
268            .expect("Error converting private key to JWK");
269        let jwk = JwkParts::from_slice(&jwk).expect("Error parsing JWK output");
270        assert_eq!(jwk.kty, JWK_KEY_TYPE);
271        assert_eq!(jwk.crv, JWK_CURVE);
272        assert_eq!(jwk.x, "tGskN_ae61DP4DLY31_fjkbvnKqf-ze7kA6Cj2vyQxU");
273        assert_eq!(jwk.d, test_pvt_b64);
274        let sk_load = X25519KeyPair::from_jwk_parts(jwk).unwrap();
275        assert_eq!(
276            kp.to_keypair_bytes().unwrap(),
277            sk_load.to_keypair_bytes().unwrap()
278        );
279    }
280
281    #[test]
282    fn key_exchange_random() {
283        let kp1 = X25519KeyPair::random().unwrap();
284        let kp2 = X25519KeyPair::random().unwrap();
285        assert_ne!(
286            kp1.to_keypair_bytes().unwrap(),
287            kp2.to_keypair_bytes().unwrap()
288        );
289
290        let xch1 = kp1.key_exchange_bytes(&kp2).unwrap();
291        let xch2 = kp2.key_exchange_bytes(&kp1).unwrap();
292        assert_eq!(xch1.len(), 32);
293        assert_eq!(xch1, xch2);
294    }
295
296    #[test]
297    fn round_trip_bytes() {
298        let kp = X25519KeyPair::random().unwrap();
299        let cmp = X25519KeyPair::from_keypair_bytes(&kp.to_keypair_bytes().unwrap()).unwrap();
300        assert_eq!(
301            kp.to_keypair_bytes().unwrap(),
302            cmp.to_keypair_bytes().unwrap()
303        );
304    }
305}