Skip to main content

securedrop_protocol_minimal/
keys.rs

1mod newsroom;
2
3use rand_core::{CryptoRng, RngCore};
4
5use crate::sign::{
6    DomainTag, FpfOnNewsroom, JournalistEphemeralKey, JournalistLongTermKey, Signature, SigningKey,
7    VerifyingKey,
8};
9
10use crate::message::{MessageKeyPair, MessagePublicKey};
11use crate::metadata::{MetadataKeyPair, MetadataPublicKey};
12use crate::primitives::ristretto255::{DHPrivateKey, DHPublicKey};
13use alloc::string::String;
14use alloc::vec::Vec;
15use serde::de::Error as _;
16use serde::{Deserialize, Serialize};
17
18/// Generic KeyPair
19pub struct KeyPair<SK, PK> {
20    pub(crate) sk: SK,
21    pub(crate) pk: PK,
22}
23
24// silly name but include "fetch" for disambiguation with dh-akem.
25// eventually: ristretto255
26pub type DhFetchKeyPair = KeyPair<DHPrivateKey, DHPublicKey>;
27
28/// The public half of an ephemeral key bundle together with the journalist's
29/// self-signature over it.
30pub type SignedKeyBundlePublic = (KeyBundlePublic, Signature<JournalistEphemeralKey>);
31
32/// The public keys that make up one ephemeral key bundle
33#[derive(Debug, Clone)]
34#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
35pub struct KeyBundlePublic {
36    /// SD-APKE ephemeral key `pk_{J,i}^{APKE_E} = (pk1, pk2)`.
37    pub apke_pk: MessagePublicKey,
38    /// SD-PKE ephemeral key, used for metadata protection.
39    pub metadata_pk: MetadataPublicKey,
40}
41
42impl KeyBundlePublic {
43    /// Serialize the bundle public keys in canonical byte order for signing.
44    ///
45    /// Layout: `pk_{J,i}^{APKE_E}(DHKEM) || pk_{J,i}^{APKE_E}(ML-KEM) || pk_{J,i}^{PKE_E}(X-Wing)`
46    pub fn as_bytes(&self) -> Vec<u8> {
47        let mut out = Vec::new();
48        out.extend_from_slice(&self.apke_pk.as_bytes());
49        out.extend_from_slice(&self.metadata_pk.as_bytes());
50        out
51    }
52}
53
54pub(crate) struct MessageKeyBundle {
55    pub(crate) apke: MessageKeyPair,
56    pub(crate) metadata_kp: MetadataKeyPair,
57}
58
59impl MessageKeyBundle {
60    pub fn new(apke: MessageKeyPair, metadata_kp: MetadataKeyPair) -> Self {
61        Self { apke, metadata_kp }
62    }
63
64    pub(crate) fn public(&self) -> KeyBundlePublic {
65        KeyBundlePublic {
66            apke_pk: self.apke.public_key().clone(),
67            metadata_pk: self.metadata_kp.public_key().clone(),
68        }
69    }
70}
71
72pub(crate) struct SignedMessageKeyBundle {
73    pub(crate) bundle: MessageKeyBundle,
74    pub(crate) selfsig: Signature<JournalistEphemeralKey>,
75}
76
77#[derive(Debug, Clone)]
78pub(crate) struct LongtermKeyBundle {
79    pub(crate) apke: MessagePublicKey,
80    pub(crate) fetch_pk: DHPublicKey,
81}
82
83impl LongtermKeyBundle {
84    pub const LEN: usize = MessagePublicKey::LEN + DHPublicKey::LEN;
85
86    pub fn new(apke: MessagePublicKey, fetch_pk: DHPublicKey) -> Self {
87        Self { apke, fetch_pk }
88    }
89
90    /// Serialize long-term public keys into the canonical byte encoding.
91    ///
92    /// Byte layout (per spec ยง3.1): `pk_J^APKE || pk_J^fetch`
93    /// where `pk_J^APKE = pk_J^AKEM (DH-AKEM) || pk_J^PQ (ML-KEM)`
94    pub fn as_bytes(&self) -> [u8; Self::LEN] {
95        let apke_bytes = self.apke.as_bytes();
96        let fetch_bytes = self.fetch_pk.into_bytes();
97
98        let mut pubkey_bytes = [0u8; Self::LEN];
99        pubkey_bytes[..apke_bytes.len()].copy_from_slice(&apke_bytes);
100        pubkey_bytes[apke_bytes.len()..].copy_from_slice(&fetch_bytes);
101
102        pubkey_bytes
103    }
104}
105
106#[derive(Debug, Clone)]
107pub struct SignedLongtermKeyBundle {
108    pub bundle: LongtermKeyBundle,
109    pub selfsig: Signature<JournalistLongTermKey>,
110}
111
112impl SignedLongtermKeyBundle {
113    pub fn new(bundle: LongtermKeyBundle, selfsig: Signature<JournalistLongTermKey>) -> Self {
114        Self { bundle, selfsig }
115    }
116
117    pub fn as_bytes(&self) -> Vec<u8> {
118        let bundle_bytes = self.bundle.as_bytes();
119        let sig_bytes = self.selfsig.as_bytes();
120
121        let mut pubkey_bytes = Vec::with_capacity(bundle_bytes.len() + sig_bytes.len());
122        pubkey_bytes.extend_from_slice(&bundle_bytes);
123        pubkey_bytes.extend_from_slice(&sig_bytes);
124
125        pubkey_bytes
126    }
127
128    pub fn bundle_bytes(&self) -> [u8; LongtermKeyBundle::LEN] {
129        self.bundle.as_bytes()
130    }
131
132    pub fn apke(&self) -> &MessagePublicKey {
133        &self.bundle.apke
134    }
135
136    pub fn fetch_pk(&self) -> &DHPublicKey {
137        &self.bundle.fetch_pk
138    }
139}
140
141#[cfg_attr(hax, hax_lib::exclude)]
142impl Serialize for SignedLongtermKeyBundle {
143    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
144        ser.serialize_str(&hex::encode(self.as_bytes()))
145    }
146}
147
148#[cfg_attr(hax, hax_lib::exclude)]
149impl<'de> Deserialize<'de> for SignedLongtermKeyBundle {
150    fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
151        type Sig = Signature<JournalistLongTermKey>;
152        let s = String::deserialize(de)?;
153        let mut bytes = [0u8; LongtermKeyBundle::LEN + Sig::LEN];
154        hex::decode_to_slice(s.trim(), &mut bytes).map_err(D::Error::custom)?;
155
156        let offset = MessagePublicKey::LEN;
157        let apke = MessagePublicKey::from_bytes(&bytes[..offset]).map_err(D::Error::custom)?;
158        let mut fetch_key_bytes = [0u8; DHPublicKey::LEN];
159        fetch_key_bytes.copy_from_slice(&bytes[offset..offset + DHPublicKey::LEN]);
160        let fetch = DHPublicKey::decode(fetch_key_bytes).map_err(D::Error::custom)?;
161        let mut sig_bytes = [0u8; Sig::LEN];
162        sig_bytes.copy_from_slice(&bytes[offset + DHPublicKey::LEN..]);
163        Ok(Self {
164            bundle: LongtermKeyBundle::new(apke, fetch),
165            selfsig: Sig::from_bytes(sig_bytes),
166        })
167    }
168}
169
170#[derive(Clone, Debug)]
171#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
172pub struct Enrollment {
173    pub bundle: SignedLongtermKeyBundle,
174    // Journalist's long-term verification key, verified out of band.
175    pub verification_key: VerifyingKey,
176}
177
178// in memory session storage
179pub struct SessionStorage {
180    pub fpf_key: Option<VerifyingKey>,
181    pub nr_key: Option<VerifyingKey>,
182    pub fpf_signature: Option<Signature<FpfOnNewsroom>>,
183}
184
185/// A key pair for FPF (Freedom of the Press Foundation).
186pub struct FPFKeyPair {
187    sk: SigningKey,
188}
189
190// hax struggles with the debug format function signature, but it is
191// debug only, so we can exclude it from extraction
192#[cfg_attr(hax, hax_lib::exclude)]
193impl core::fmt::Debug for FPFKeyPair {
194    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
195        f.debug_struct("FPFKeyPair")
196            .field("vk", &self.sk.vk)
197            .finish_non_exhaustive()
198    }
199}
200
201impl FPFKeyPair {
202    /// Generate a new FPF key pair.
203    ///
204    /// # Errors
205    ///
206    /// Returns an error if the key generation fails.
207    pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Result<Self, anyhow::Error> {
208        let sk = SigningKey::new(rng)?;
209        Ok(Self { sk })
210    }
211
212    /// Returns the verification key.
213    pub fn verifying_key(&self) -> VerifyingKey {
214        self.sk.vk
215    }
216
217    /// Sign `msg` in domain `D` using the FPF signing key.
218    pub fn sign<D: DomainTag>(&self, msg: &[u8]) -> Signature<D> {
219        self.sk.sign(msg)
220    }
221
222    /// The FPF signing key used as a secret.
223    pub fn as_bytes(&self) -> [u8; SigningKey::SEED_LEN] {
224        self.sk.as_bytes()
225    }
226
227    /// Reconstruct an [`FPFKeyPair`] from its secret.
228    pub fn from_bytes(seed: [u8; SigningKey::SEED_LEN]) -> Self {
229        Self {
230            sk: SigningKey::from_seed(seed),
231        }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use proptest::prelude::*;
239
240    proptest! {
241        #[test]
242        fn fpf_keypair_seed_roundtrip(seed: [u8; SigningKey::SEED_LEN]) {
243            let kp = FPFKeyPair::from_bytes(seed);
244            prop_assert_eq!(kp.as_bytes(), seed);
245            let kp2 = FPFKeyPair::from_bytes(kp.as_bytes());
246            prop_assert_eq!(
247                kp.verifying_key().into_bytes(),
248                kp2.verifying_key().into_bytes()
249            );
250        }
251    }
252}
253
254pub use newsroom::NewsroomKeyPair;