p2panda_encryption/key_bundle/
key_bundle.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6use crate::crypto::x25519::PublicKey;
7use crate::crypto::xeddsa::{XEdDSAError, XSignature, xeddsa_verify};
8use crate::key_bundle::{Lifetime, LifetimeError, OneTimePreKey, OneTimePreKeyId, PreKey};
9use crate::traits::KeyBundle;
10
11/// Key-bundle with public keys to be used exactly _once_.
12///
13/// Note that while pre-keys are signed for X3DH, bundles should be part of an authenticated
14/// messaging format where the whole payload (and thus it's lifetime and one-time pre-key) is
15/// signed by the same identity to prevent replay and impersonation attacks.
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct OneTimeKeyBundle {
18    identity_key: PublicKey,
19    signed_prekey: PreKey,
20    prekey_signature: XSignature,
21    onetime_prekey: Option<OneTimePreKey>,
22}
23
24impl OneTimeKeyBundle {
25    pub fn new(
26        identity_key: PublicKey,
27        signed_prekey: PreKey,
28        prekey_signature: XSignature,
29        onetime_prekey: Option<OneTimePreKey>,
30    ) -> Self {
31        Self {
32            identity_key,
33            signed_prekey,
34            prekey_signature,
35            onetime_prekey,
36        }
37    }
38}
39
40impl KeyBundle for OneTimeKeyBundle {
41    fn identity_key(&self) -> &PublicKey {
42        &self.identity_key
43    }
44
45    fn signed_prekey(&self) -> &PublicKey {
46        self.signed_prekey.key()
47    }
48
49    fn onetime_prekey(&self) -> Option<&PublicKey> {
50        self.onetime_prekey.as_ref().map(|key| key.key())
51    }
52
53    fn onetime_prekey_id(&self) -> Option<OneTimePreKeyId> {
54        self.onetime_prekey.as_ref().map(|key| key.id())
55    }
56
57    fn lifetime(&self) -> &Lifetime {
58        self.signed_prekey.lifetime()
59    }
60
61    fn verify(&self) -> Result<(), KeyBundleError> {
62        // Check lifetime.
63        self.signed_prekey.verify_lifetime()?;
64
65        // Check signature.
66        xeddsa_verify(
67            self.signed_prekey.as_bytes(),
68            &self.identity_key,
69            &self.prekey_signature,
70        )?;
71
72        Ok(())
73    }
74}
75
76/// Key-bundle with public keys to be used until the pre-key expired.
77///
78/// Note that while pre-keys are signed for X3DH, bundles should be part of an authenticated
79/// messaging format where the whole payload (and thus it's lifetime) is signed by the same
80/// identity to prevent replay and impersonation attacks.
81#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
82pub struct LongTermKeyBundle {
83    identity_key: PublicKey,
84    signed_prekey: PreKey,
85    prekey_signature: XSignature,
86}
87
88impl LongTermKeyBundle {
89    pub fn new(
90        identity_key: PublicKey,
91        signed_prekey: PreKey,
92        prekey_signature: XSignature,
93    ) -> Self {
94        Self {
95            identity_key,
96            signed_prekey,
97            prekey_signature,
98        }
99    }
100}
101
102impl KeyBundle for LongTermKeyBundle {
103    fn identity_key(&self) -> &PublicKey {
104        &self.identity_key
105    }
106
107    fn signed_prekey(&self) -> &PublicKey {
108        self.signed_prekey.key()
109    }
110
111    fn onetime_prekey(&self) -> Option<&PublicKey> {
112        // No one-time pre-key in long-term key bundle.
113        None
114    }
115
116    fn onetime_prekey_id(&self) -> Option<OneTimePreKeyId> {
117        // No one-time pre-key in long-term key bundle.
118        None
119    }
120
121    fn lifetime(&self) -> &Lifetime {
122        self.signed_prekey.lifetime()
123    }
124
125    fn verify(&self) -> Result<(), KeyBundleError> {
126        // Check lifetime.
127        self.signed_prekey.verify_lifetime()?;
128
129        // Check signature.
130        xeddsa_verify(
131            self.signed_prekey.as_bytes(),
132            &self.identity_key,
133            &self.prekey_signature,
134        )?;
135
136        Ok(())
137    }
138}
139
140/// Helper method to identify the "latest" (valid and with furthest expiry date) key bundle from a
141/// set. Returns `None` if no valid bundle was given.
142pub fn latest_key_bundle<'a, KB>(bundles: &'a [KB]) -> Option<&'a KB>
143where
144    KB: KeyBundle,
145{
146    let mut latest: Option<&'a KB> = None;
147
148    for bundle in bundles {
149        // Remove all prekeys which are _too early_ or _too late_ (expired).
150        //
151        //                   Now
152        // too late --> [---] |
153        //                    | [----] <-- too early
154        //              [-----|----] <-- valid
155        //                    |
156        //
157        //                  t -->
158        //
159        if bundle.lifetime().verify().is_err() {
160            continue;
161        }
162
163        // Of all other, valid ones, find the one which has the "furthest" expiry date and is
164        // therefore the "latest" key bundle.
165        //
166        //                   Now
167        //                    |
168        //                  [-|---------]
169        //              [-----|------------] <-- "latest"
170        //          [---------|-----]
171        //                    |
172        //
173        //                  t -->
174        //
175        match latest {
176            Some(current_bundle) => {
177                if bundle.lifetime() > current_bundle.lifetime() {
178                    latest = Some(bundle);
179                }
180            }
181            None => {
182                latest = Some(bundle);
183            }
184        }
185    }
186
187    latest
188}
189
190#[derive(Debug, Error)]
191pub enum KeyBundleError {
192    #[error(transparent)]
193    XEdDSA(#[from] XEdDSAError),
194
195    #[error(transparent)]
196    Lifetime(#[from] LifetimeError),
197}
198
199#[cfg(test)]
200mod tests {
201    use crate::crypto::Rng;
202    use crate::crypto::x25519::SecretKey;
203    use crate::crypto::xeddsa::xeddsa_sign;
204    use crate::key_bundle::{Lifetime, LongTermKeyBundle, OneTimePreKey, PreKey};
205    use crate::traits::KeyBundle;
206
207    use super::OneTimeKeyBundle;
208
209    #[test]
210    fn verify() {
211        let rng = Rng::from_seed([1; 32]);
212
213        let secret_key = SecretKey::from_bytes(rng.random_array().unwrap());
214        let identity_key = secret_key.public_key().unwrap();
215
216        let signed_prekey_secret = SecretKey::from_bytes(rng.random_array().unwrap());
217        let signed_prekey = PreKey::new(
218            signed_prekey_secret.public_key().unwrap(),
219            Lifetime::default(),
220        );
221        let prekey_signature = xeddsa_sign(signed_prekey.as_bytes(), &secret_key, &rng).unwrap();
222
223        let onetime_prekey_secret = SecretKey::from_bytes(rng.random_array().unwrap());
224        let onetime_prekey = OneTimePreKey::new(onetime_prekey_secret.public_key().unwrap(), 1);
225
226        // Valid key-bundles.
227        assert!(
228            OneTimeKeyBundle::new(
229                identity_key,
230                signed_prekey,
231                prekey_signature,
232                Some(onetime_prekey.clone()),
233            )
234            .verify()
235            .is_ok()
236        );
237        assert!(
238            LongTermKeyBundle::new(identity_key, signed_prekey, prekey_signature)
239                .verify()
240                .is_ok()
241        );
242
243        // Invalid lifetime of pre-key.
244        let signed_prekey = PreKey::new(
245            signed_prekey_secret.public_key().unwrap(),
246            Lifetime::from_range(0, 0),
247        );
248        assert!(
249            OneTimeKeyBundle::new(
250                identity_key,
251                signed_prekey,
252                prekey_signature,
253                Some(onetime_prekey.clone()),
254            )
255            .verify()
256            .is_err()
257        );
258        assert!(
259            LongTermKeyBundle::new(identity_key, signed_prekey, prekey_signature)
260                .verify()
261                .is_err()
262        );
263
264        // Invalid signature of pre-key.
265        let prekey_signature = xeddsa_sign(b"wrong payload", &secret_key, &rng).unwrap();
266        assert!(
267            OneTimeKeyBundle::new(
268                identity_key,
269                signed_prekey,
270                prekey_signature,
271                Some(onetime_prekey.clone()),
272            )
273            .verify()
274            .is_err()
275        );
276        assert!(
277            LongTermKeyBundle::new(identity_key, signed_prekey, prekey_signature)
278                .verify()
279                .is_err()
280        );
281    }
282}