Skip to main content

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    #[error("no key bundles found")]
199    KeyBundlesNotFound,
200}
201
202#[cfg(test)]
203mod tests {
204    use crate::crypto::Rng;
205    use crate::crypto::x25519::SecretKey;
206    use crate::crypto::xeddsa::xeddsa_sign;
207    use crate::key_bundle::{Lifetime, LongTermKeyBundle, OneTimePreKey, PreKey};
208    use crate::traits::KeyBundle;
209
210    use super::OneTimeKeyBundle;
211
212    #[test]
213    fn verify() {
214        let rng = Rng::from_seed([1; 32]);
215
216        let secret_key = SecretKey::from_bytes(rng.random_array().unwrap());
217        let identity_key = secret_key.verifying_key().unwrap();
218
219        let signed_prekey_secret = SecretKey::from_bytes(rng.random_array().unwrap());
220        let signed_prekey = PreKey::new(
221            signed_prekey_secret.verifying_key().unwrap(),
222            Lifetime::default(),
223        );
224        let prekey_signature = xeddsa_sign(signed_prekey.as_bytes(), &secret_key, &rng).unwrap();
225
226        let onetime_prekey_secret = SecretKey::from_bytes(rng.random_array().unwrap());
227        let onetime_prekey = OneTimePreKey::new(onetime_prekey_secret.verifying_key().unwrap(), 1);
228
229        // Valid key-bundles.
230        assert!(
231            OneTimeKeyBundle::new(
232                identity_key,
233                signed_prekey,
234                prekey_signature,
235                Some(onetime_prekey.clone()),
236            )
237            .verify()
238            .is_ok()
239        );
240        assert!(
241            LongTermKeyBundle::new(identity_key, signed_prekey, prekey_signature)
242                .verify()
243                .is_ok()
244        );
245
246        // Invalid lifetime of pre-key.
247        let signed_prekey = PreKey::new(
248            signed_prekey_secret.verifying_key().unwrap(),
249            Lifetime::from_range(0, 0),
250        );
251        assert!(
252            OneTimeKeyBundle::new(
253                identity_key,
254                signed_prekey,
255                prekey_signature,
256                Some(onetime_prekey.clone()),
257            )
258            .verify()
259            .is_err()
260        );
261        assert!(
262            LongTermKeyBundle::new(identity_key, signed_prekey, prekey_signature)
263                .verify()
264                .is_err()
265        );
266
267        // Invalid signature of pre-key.
268        let prekey_signature = xeddsa_sign(b"wrong payload", &secret_key, &rng).unwrap();
269        assert!(
270            OneTimeKeyBundle::new(
271                identity_key,
272                signed_prekey,
273                prekey_signature,
274                Some(onetime_prekey.clone()),
275            )
276            .verify()
277            .is_err()
278        );
279        assert!(
280            LongTermKeyBundle::new(identity_key, signed_prekey, prekey_signature)
281                .verify()
282                .is_err()
283        );
284    }
285}