Skip to main content

securedrop_protocol_minimal/
sign.rs

1use alloc::vec::Vec;
2use core::marker::PhantomData;
3
4use anyhow::Error;
5use libcrux_ed25519::{SigningKey as LibCruxSigningKey, VerificationKey as LibCruxVerifyingKey};
6use rand_core::CryptoRng;
7
8// Sealing module: prevents external crates from implementing `DomainTag`.
9mod private {
10    pub trait Sealed {}
11}
12
13/// Marker trait for signature domain separation.
14///
15/// Each impl encodes the ASCII tag that is prepended to every signing preimage
16/// in that domain: `len(tag) || tag || msg`  (see footnote in the spec).
17pub trait DomainTag: private::Sealed {
18    #[doc(hidden)]
19    const TAG: &'static [u8];
20}
21
22/// Journalist self-signature over long-term public keys (step 3.1).
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct JournalistLongTermKey;
25
26/// Journalist self-signature over ephemeral key bundles (step 3.2).
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct JournalistEphemeralKey;
29
30/// Newsroom signature over a journalist's verifying key (steps 3.1, 5).
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct NewsroomOnJournalist;
33
34/// FPF signature over the newsroom's verifying key (step 2).
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct FpfOnNewsroom;
37
38impl private::Sealed for JournalistLongTermKey {}
39impl private::Sealed for JournalistEphemeralKey {}
40impl private::Sealed for NewsroomOnJournalist {}
41impl private::Sealed for FpfOnNewsroom {}
42
43impl DomainTag for JournalistLongTermKey {
44    const TAG: &'static [u8] = b"j-sig-ltk";
45}
46impl DomainTag for JournalistEphemeralKey {
47    const TAG: &'static [u8] = b"j-sig-eph";
48}
49impl DomainTag for NewsroomOnJournalist {
50    const TAG: &'static [u8] = b"nr-sig";
51}
52impl DomainTag for FpfOnNewsroom {
53    const TAG: &'static [u8] = b"fpf-sig-nr";
54}
55
56/// An Ed25519 signature carrying its domain at the type level.
57///
58/// A `Signature<D>` can only be verified against a message using the same
59/// domain `D`, making cross-domain misuse a compile error rather than a
60/// runtime failure.
61pub struct Signature<D: DomainTag> {
62    bytes: [u8; 64],
63    _phantom: PhantomData<fn() -> D>,
64}
65
66impl<D: DomainTag> Copy for Signature<D> {}
67impl<D: DomainTag> Clone for Signature<D> {
68    fn clone(&self) -> Self {
69        *self
70    }
71}
72impl<D: DomainTag> core::fmt::Debug for Signature<D> {
73    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
74        f.debug_tuple("Signature").field(&self.bytes).finish()
75    }
76}
77impl<D: DomainTag> PartialEq for Signature<D> {
78    fn eq(&self, other: &Self) -> bool {
79        self.bytes == other.bytes
80    }
81}
82impl<D: DomainTag> Eq for Signature<D> {}
83
84impl<D: DomainTag> Signature<D> {
85    pub(crate) fn from_bytes(bytes: [u8; 64]) -> Self {
86        Self {
87            bytes,
88            _phantom: PhantomData,
89        }
90    }
91}
92
93/// Construct the tagged signing preimage: `len(tag) || tag || msg`.
94fn tagged_preimage<D: DomainTag>(msg: &[u8]) -> Vec<u8> {
95    let tag = D::TAG;
96    debug_assert!(tag.len() <= 255, "tag length exceeds u8::MAX");
97    debug_assert!(tag.is_ascii(), "tag contains non-ASCII bytes");
98    let mut preimage = Vec::with_capacity(1 + tag.len() + msg.len());
99    preimage.push(tag.len() as u8);
100    preimage.extend_from_slice(tag);
101    preimage.extend_from_slice(msg);
102    preimage
103}
104
105/// An Ed25519 signing key.
106pub struct SigningKey {
107    pub vk: VerifyingKey,
108    sk: LibCruxSigningKey,
109}
110
111impl core::fmt::Debug for SigningKey {
112    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
113        f.debug_struct("SigningKey")
114            .field("vk", &self.vk)
115            .finish_non_exhaustive()
116    }
117}
118
119/// An Ed25519 verification key.
120#[derive(Copy, Clone)]
121pub struct VerifyingKey(LibCruxVerifyingKey);
122
123impl core::fmt::Debug for VerifyingKey {
124    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
125        f.debug_tuple("VerifyingKey")
126            .field(&self.into_bytes())
127            .finish()
128    }
129}
130
131impl SigningKey {
132    /// Generate a signing key from the supplied `rng`.
133    pub fn new(mut rng: &mut impl CryptoRng) -> Result<SigningKey, Error> {
134        let (sk, vk) = libcrux_ed25519::generate_key_pair(&mut rng)
135            .map_err(|_| anyhow::anyhow!("Key generation failed"))?;
136        Ok(SigningKey {
137            vk: VerifyingKey(vk),
138            sk,
139        })
140    }
141
142    /// Sign `msg` in domain `D`, returning a `Signature<D>`.
143    ///
144    /// The actual preimage is `len(tag) || tag || msg` where `tag = D::TAG`.
145    pub fn sign<D: DomainTag>(&self, msg: &[u8]) -> Signature<D> {
146        let preimage = tagged_preimage::<D>(msg);
147        let bytes = libcrux_ed25519::sign(&preimage, self.sk.as_ref())
148            .expect("Signing should not fail with valid key");
149        Signature::from_bytes(bytes)
150    }
151}
152
153impl VerifyingKey {
154    /// Get the raw bytes of this verification key.
155    pub fn into_bytes(self) -> [u8; 32] {
156        self.0.into_bytes()
157    }
158
159    /// Verify `sig` over `msg`. The domain is determined by the type of `sig`.
160    ///
161    /// Returns an error if the signature is invalid.
162    pub fn verify<D: DomainTag>(&self, msg: &[u8], sig: &Signature<D>) -> Result<(), Error> {
163        let preimage = tagged_preimage::<D>(msg);
164        libcrux_ed25519::verify(&preimage, self.0.as_ref(), &sig.bytes)
165            .map_err(|_| anyhow::anyhow!("Signature verification failed"))
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use getrandom;
173    use proptest::prelude::*;
174    use rand_chacha::ChaCha20Rng;
175    use rand_core::SeedableRng;
176
177    fn get_rng() -> ChaCha20Rng {
178        let mut seed = [0u8; 32];
179        getrandom::fill(&mut seed).expect("OS random source failed");
180        ChaCha20Rng::from_seed(seed)
181    }
182
183    proptest! {
184        #[test]
185        fn test_sign_verify_roundtrip(msg in proptest::collection::vec(any::<u8>(), 0..100)) {
186            let mut rng = get_rng();
187            let signing_key = SigningKey::new(&mut rng).unwrap();
188            let sig: Signature<JournalistLongTermKey> = signing_key.sign(&msg);
189            assert!(signing_key.vk.verify(&msg, &sig).is_ok());
190        }
191    }
192
193    proptest! {
194        #[test]
195        fn test_verify_fails_with_wrong_message(
196            msg1 in proptest::collection::vec(any::<u8>(), 0..100),
197            msg2 in proptest::collection::vec(any::<u8>(), 0..100)
198        ) {
199            if msg1 == msg2 {
200                return Ok(());
201            }
202            let mut rng = get_rng();
203            let signing_key = SigningKey::new(&mut rng).unwrap();
204            let sig: Signature<JournalistLongTermKey> = signing_key.sign(&msg1);
205            assert!(signing_key.vk.verify(&msg2, &sig).is_err());
206        }
207    }
208
209    proptest! {
210        #[test]
211        fn test_verify_fails_with_wrong_key(msg in proptest::collection::vec(any::<u8>(), 0..100)) {
212            let mut rng = get_rng();
213            let key1 = SigningKey::new(&mut rng).unwrap();
214            let key2 = SigningKey::new(&mut rng).unwrap();
215            let sig: Signature<JournalistLongTermKey> = key1.sign(&msg);
216            assert!(key2.vk.verify(&msg, &sig).is_err());
217        }
218    }
219
220    proptest! {
221        #[test]
222        fn test_domain_separation(msg in proptest::collection::vec(any::<u8>(), 0..100)) {
223            let mut rng = get_rng();
224            let signing_key = SigningKey::new(&mut rng).unwrap();
225            let sig: Signature<JournalistLongTermKey> = signing_key.sign(&msg);
226            let cross_domain_sig: Signature<JournalistEphemeralKey> =
227                Signature::from_bytes(sig.bytes);
228            assert!(signing_key.vk.verify(&msg, &cross_domain_sig).is_err());
229        }
230    }
231}