Skip to main content

securedrop_protocol_minimal/
sign.rs

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