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
13#[cfg(not(hax))]
15mod private {
16 pub trait Sealed {}
17}
18
19#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct JournalistLongTermKey;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct JournalistEphemeralKey;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct NewsroomOnJournalist;
45
46#[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
81pub struct Signature<D: DomainTag> {
87 bytes: [u8; 64],
88 _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#[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#[cfg(not(hax))]
117impl<D: DomainTag> Eq for Signature<D> {}
118
119impl<D: DomainTag> Signature<D> {
120 pub fn from_bytes(bytes: [u8; 64]) -> Self {
122 Self {
123 bytes,
124 _phantom: PhantomData,
125 }
126 }
127
128 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#[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#[derive(Copy, Clone)]
169pub struct VerifyingKey([u8; KEY_LEN_ED25519]);
170
171pub(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#[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#[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 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 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 pub fn into_bytes(self) -> [u8; 32] {
257 self.0
258 }
259
260 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}