Skip to main content

securedrop_protocol_minimal/
sign.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3use core::marker::PhantomData;
4
5use anyhow::Error;
6use rand_core::CryptoRng;
7use serde::de::Error as _;
8
9use crate::primitives::provider;
10
11const KEY_LEN_ED25519: usize = 32;
12
13const SIG_LEN_ED25519: usize = 64;
14
15// Sealing module: prevents external crates from implementing `DomainTag`.
16#[cfg(not(hax))]
17mod private {
18    pub trait Sealed {}
19}
20
21/// Marker trait for signature domain separation.
22///
23/// Each impl encodes the ASCII tag that is prepended to every signing preimage
24/// in that domain: `len(tag) || tag || msg`  (see footnote in the spec).
25#[cfg(not(hax))]
26pub trait DomainTag: private::Sealed {
27    #[doc(hidden)]
28    fn tag() -> &'static [u8];
29}
30#[cfg(hax)]
31pub trait DomainTag {
32    #[doc(hidden)]
33    fn tag() -> &'static [u8];
34}
35
36/// Journalist self-signature over long-term public keys (step 3.1).
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct JournalistLongTermKey;
39
40/// Journalist self-signature over ephemeral key bundles (step 3.2).
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct JournalistEphemeralKey;
43
44/// Newsroom signature over a journalist's verifying key (steps 3.1, 5).
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct NewsroomOnJournalist;
47
48/// FPF signature over the newsroom's verifying key (step 2).
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct FpfOnNewsroom;
51
52#[cfg(not(hax))]
53mod sealed_impls {
54    use super::*;
55
56    impl private::Sealed for JournalistLongTermKey {}
57    impl private::Sealed for JournalistEphemeralKey {}
58    impl private::Sealed for NewsroomOnJournalist {}
59    impl private::Sealed for FpfOnNewsroom {}
60}
61
62impl DomainTag for JournalistLongTermKey {
63    fn tag() -> &'static [u8] {
64        b"j-sig-ltk"
65    }
66}
67impl DomainTag for JournalistEphemeralKey {
68    fn tag() -> &'static [u8] {
69        b"j-sig-eph"
70    }
71}
72impl DomainTag for NewsroomOnJournalist {
73    fn tag() -> &'static [u8] {
74        b"nr-sig"
75    }
76}
77impl DomainTag for FpfOnNewsroom {
78    fn tag() -> &'static [u8] {
79        b"fpf-sig-nr"
80    }
81}
82
83/// An Ed25519 signature carrying its domain at the type level.
84///
85/// A `Signature<D>` can only be verified against a message using the same
86/// domain `D`, making cross-domain misuse a compile error rather than a
87/// runtime failure.
88pub struct Signature<D: DomainTag> {
89    bytes: [u8; SIG_LEN_ED25519],
90    // `PhantomData<D>` rather than `PhantomData<fn() -> D>`: the function type
91    // has no decidable equality in F*, which blocks `t_Signature` extraction.
92    _phantom: PhantomData<D>,
93}
94
95impl<D: DomainTag> Copy for Signature<D> {}
96impl<D: DomainTag> Clone for Signature<D> {
97    fn clone(&self) -> Self {
98        *self
99    }
100}
101
102// hax struggles with the debug format function signature, but it is
103// debug only, so we can exclude it from extraction
104#[cfg_attr(hax, hax_lib::exclude)]
105impl<D: DomainTag> core::fmt::Debug for Signature<D> {
106    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
107        f.debug_tuple("Signature").field(&self.bytes).finish()
108    }
109}
110impl<D: DomainTag> PartialEq for Signature<D> {
111    fn eq(&self, other: &Self) -> bool {
112        self.bytes == other.bytes
113    }
114}
115// `Signature<D>` carries `PhantomData<fn() -> D>`, a function type with no
116// decidable equality in F*; the `Eq` marker would force `t_Signature` to be an
117// eqtype and fail extraction. We only need value equality (`PartialEq`, above).
118#[cfg(not(hax))]
119impl<D: DomainTag> Eq for Signature<D> {}
120
121impl<D: DomainTag> Signature<D> {
122    pub const LEN: usize = SIG_LEN_ED25519;
123
124    /// Reconstruct a [`Signature`] from its serialization.
125    pub fn from_bytes(bytes: [u8; SIG_LEN_ED25519]) -> Self {
126        Self {
127            bytes,
128            _phantom: PhantomData,
129        }
130    }
131
132    /// The byte serialization of this signature.
133    pub fn as_bytes(&self) -> [u8; SIG_LEN_ED25519] {
134        self.bytes
135    }
136}
137
138#[cfg_attr(hax, hax_lib::exclude)]
139impl<D: DomainTag> serde::Serialize for Signature<D> {
140    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
141        ser.serialize_str(&hex::encode(self.bytes))
142    }
143}
144
145#[cfg_attr(hax, hax_lib::exclude)]
146impl<'de, D: DomainTag> serde::Deserialize<'de> for Signature<D> {
147    fn deserialize<De: serde::Deserializer<'de>>(de: De) -> Result<Self, De::Error> {
148        let s = String::deserialize(de)?;
149        let mut bytes = [0u8; SIG_LEN_ED25519];
150        hex::decode_to_slice(s.trim(), &mut bytes).map_err(De::Error::custom)?;
151        Ok(Self::from_bytes(bytes))
152    }
153}
154
155/// Construct the tagged signing preimage: `len(tag) || tag || msg`.
156#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
157fn tagged_preimage<D: DomainTag>(msg: &[u8]) -> Vec<u8> {
158    let tag = D::tag();
159    #[cfg(not(hax))]
160    {
161        debug_assert!(tag.len() <= 255, "tag length exceeds u8::MAX");
162        debug_assert!(tag.is_ascii(), "tag contains non-ASCII bytes");
163    }
164    let mut preimage = Vec::with_capacity(1 + tag.len() + msg.len());
165    preimage.push(tag.len() as u8);
166    preimage.extend_from_slice(tag);
167    preimage.extend_from_slice(msg);
168    preimage
169}
170
171/// An Ed25519 verification key.
172#[derive(Copy, Clone)]
173pub struct VerifyingKey([u8; KEY_LEN_ED25519]);
174
175/// An Ed25519 signing key.
176pub(crate) struct SigningSecretKey([u8; KEY_LEN_ED25519]);
177
178impl VerifyingKey {
179    pub const LEN: usize = KEY_LEN_ED25519;
180
181    pub(crate) fn as_bytes(&self) -> &[u8; KEY_LEN_ED25519] {
182        &self.0
183    }
184
185    pub fn from_bytes(bytes: [u8; KEY_LEN_ED25519]) -> Self {
186        Self(bytes)
187    }
188}
189
190impl SigningSecretKey {
191    pub(crate) fn as_bytes(&self) -> &[u8; KEY_LEN_ED25519] {
192        &self.0
193    }
194
195    pub(crate) fn from_bytes(bytes: [u8; KEY_LEN_ED25519]) -> Self {
196        Self(bytes)
197    }
198}
199
200pub struct SigningKey {
201    pub vk: VerifyingKey,
202    sk: SigningSecretKey,
203}
204
205// hax struggles with the debug format function signature, but it is
206// debug only, so we can exclude it from extraction
207#[cfg_attr(hax, hax_lib::exclude)]
208impl core::fmt::Debug for SigningKey {
209    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
210        f.debug_struct("SigningKey")
211            .field("vk", &self.vk)
212            .finish_non_exhaustive()
213    }
214}
215
216// hax struggles with the debug format function signature, but it is
217// debug only, so we can exclude it from extraction
218#[cfg_attr(hax, hax_lib::exclude)]
219impl core::fmt::Debug for VerifyingKey {
220    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
221        f.debug_tuple("VerifyingKey")
222            .field(&self.into_bytes())
223            .finish()
224    }
225}
226
227impl SigningKey {
228    pub const SEED_LEN: usize = KEY_LEN_ED25519;
229
230    /// Generate a signing key from the supplied `rng`.
231    pub fn new<R: CryptoRng>(rng: &mut R) -> Result<SigningKey, Error> {
232        let (sk, vk) = provider::ed25519::keygen(rng)?;
233        Ok(SigningKey {
234            vk: VerifyingKey(vk),
235            sk: SigningSecretKey(sk),
236        })
237    }
238
239    /// Sign `msg` in domain `D`, returning a `Signature<D>`.
240    ///
241    /// The actual preimage is `len(tag) || tag || msg` where `tag = D::TAG`.
242    pub fn sign<D: DomainTag>(&self, msg: &[u8]) -> Signature<D> {
243        let preimage = tagged_preimage::<D>(msg);
244        let bytes = provider::ed25519::sign(&preimage, self.sk.as_bytes());
245        Signature::from_bytes(bytes)
246    }
247
248    pub(crate) fn as_bytes(&self) -> [u8; Self::SEED_LEN] {
249        *self.sk.as_bytes()
250    }
251
252    pub(crate) fn from_seed(seed: [u8; Self::SEED_LEN]) -> Self {
253        let mut pk = [0u8; KEY_LEN_ED25519];
254        provider::ed25519::secret_to_public(&mut pk, &seed);
255        Self {
256            vk: VerifyingKey(pk),
257            sk: SigningSecretKey(seed),
258        }
259    }
260}
261
262impl VerifyingKey {
263    /// Get the raw bytes of this verification key.
264    pub fn into_bytes(self) -> [u8; KEY_LEN_ED25519] {
265        self.0
266    }
267
268    /// Verify `sig` over `msg`. The domain is determined by the type of `sig`.
269    ///
270    /// Returns an error if the signature is invalid.
271    pub fn verify<D: DomainTag>(&self, msg: &[u8], sig: &Signature<D>) -> Result<(), Error> {
272        let preimage = tagged_preimage::<D>(msg);
273        provider::ed25519::verify(&preimage, self.as_bytes(), &sig.bytes)
274            .map_err(|_| anyhow::anyhow!("Signature verification failed"))
275    }
276}
277
278#[cfg_attr(hax, hax_lib::exclude)]
279impl serde::Serialize for VerifyingKey {
280    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
281        ser.serialize_str(&hex::encode(self.0.as_ref()))
282    }
283}
284
285#[cfg_attr(hax, hax_lib::exclude)]
286impl<'de> serde::Deserialize<'de> for VerifyingKey {
287    fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
288        let s = String::deserialize(de)?;
289        let mut bytes = [0u8; KEY_LEN_ED25519];
290        hex::decode_to_slice(s.trim(), &mut bytes).map_err(D::Error::custom)?;
291        Ok(Self::from_bytes(bytes))
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use getrandom;
299    use proptest::prelude::*;
300    use rand_chacha::ChaCha20Rng;
301    use rand_core::SeedableRng;
302
303    fn get_rng() -> ChaCha20Rng {
304        let mut seed = [0u8; 32];
305        getrandom::fill(&mut seed).expect("OS random source failed");
306        ChaCha20Rng::from_seed(seed)
307    }
308
309    proptest! {
310        #[test]
311        fn test_sign_verify_roundtrip(msg in proptest::collection::vec(any::<u8>(), 0..100)) {
312            let mut rng = get_rng();
313            let signing_key = SigningKey::new(&mut rng).unwrap();
314            let sig: Signature<JournalistLongTermKey> = signing_key.sign(&msg);
315            assert!(signing_key.vk.verify(&msg, &sig).is_ok());
316        }
317    }
318
319    proptest! {
320        #[test]
321        fn test_verify_fails_with_wrong_message(
322            msg1 in proptest::collection::vec(any::<u8>(), 0..100),
323            msg2 in proptest::collection::vec(any::<u8>(), 0..100)
324        ) {
325            if msg1 == msg2 {
326                return Ok(());
327            }
328            let mut rng = get_rng();
329            let signing_key = SigningKey::new(&mut rng).unwrap();
330            let sig: Signature<JournalistLongTermKey> = signing_key.sign(&msg1);
331            assert!(signing_key.vk.verify(&msg2, &sig).is_err());
332        }
333    }
334
335    proptest! {
336        #[test]
337        fn test_signature_byte_roundtrip(msg in proptest::collection::vec(any::<u8>(), 0..100)) {
338            let mut rng = get_rng();
339            let signing_key = SigningKey::new(&mut rng).unwrap();
340            let sig: Signature<JournalistLongTermKey> = signing_key.sign(&msg);
341            let sig2 = Signature::<JournalistLongTermKey>::from_bytes(sig.as_bytes());
342            prop_assert!(signing_key.vk.verify(&msg, &sig2).is_ok());
343        }
344    }
345
346    proptest! {
347        #[test]
348        fn test_verifying_key_byte_roundtrip(msg in proptest::collection::vec(any::<u8>(), 0..100)) {
349            let mut rng = get_rng();
350            let signing_key = SigningKey::new(&mut rng).unwrap();
351            let sig: Signature<JournalistLongTermKey> = signing_key.sign(&msg);
352            let vk = VerifyingKey::from_bytes(signing_key.vk.into_bytes());
353            prop_assert!(vk.verify(&msg, &sig).is_ok());
354        }
355    }
356
357    proptest! {
358        #[test]
359        fn test_verify_fails_with_wrong_key(msg in proptest::collection::vec(any::<u8>(), 0..100)) {
360            let mut rng = get_rng();
361            let key1 = SigningKey::new(&mut rng).unwrap();
362            let key2 = SigningKey::new(&mut rng).unwrap();
363            let sig: Signature<JournalistLongTermKey> = key1.sign(&msg);
364            assert!(key2.vk.verify(&msg, &sig).is_err());
365        }
366    }
367
368    proptest! {
369        #[test]
370        fn test_domain_separation(msg in proptest::collection::vec(any::<u8>(), 0..100)) {
371            let mut rng = get_rng();
372            let signing_key = SigningKey::new(&mut rng).unwrap();
373            let sig: Signature<JournalistLongTermKey> = signing_key.sign(&msg);
374            let cross_domain_sig: Signature<JournalistEphemeralKey> =
375                Signature::from_bytes(sig.bytes);
376            assert!(signing_key.vk.verify(&msg, &cross_domain_sig).is_err());
377        }
378    }
379}