securedrop_protocol_minimal/
sign.rs1use 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
8mod private {
10 pub trait Sealed {}
11}
12
13pub trait DomainTag: private::Sealed {
18 #[doc(hidden)]
19 const TAG: &'static [u8];
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct JournalistLongTermKey;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct JournalistEphemeralKey;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct NewsroomOnJournalist;
33
34#[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
56pub 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
93fn 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
105pub 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#[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 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 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 pub fn into_bytes(self) -> [u8; 32] {
156 self.0.into_bytes()
157 }
158
159 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}