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