securedrop_protocol_minimal/
sign.rs1use 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#[cfg(not(hax))]
17mod private {
18 pub trait Sealed {}
19}
20
21#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct JournalistLongTermKey;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct JournalistEphemeralKey;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct NewsroomOnJournalist;
47
48#[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
83pub struct Signature<D: DomainTag> {
89 bytes: [u8; SIG_LEN_ED25519],
90 _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#[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#[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 pub fn from_bytes(bytes: [u8; SIG_LEN_ED25519]) -> Self {
126 Self {
127 bytes,
128 _phantom: PhantomData,
129 }
130 }
131
132 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#[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#[derive(Copy, Clone)]
173pub struct VerifyingKey([u8; KEY_LEN_ED25519]);
174
175pub(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#[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#[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 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 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 pub fn into_bytes(self) -> [u8; KEY_LEN_ED25519] {
265 self.0
266 }
267
268 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}