securedrop_protocol_minimal/
sign.rs1use 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
11mod private {
13 pub trait Sealed {}
14}
15
16pub trait DomainTag: private::Sealed {
21 #[doc(hidden)]
22 const TAG: &'static [u8];
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct JournalistLongTermKey;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct JournalistEphemeralKey;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct NewsroomOnJournalist;
36
37#[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
59pub 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
96fn 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
108pub 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#[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 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 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 pub fn into_bytes(self) -> [u8; 32] {
159 self.0.into_bytes()
160 }
161
162 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}