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