1use std::path::Path;
15
16use ed25519_dalek::pkcs8::DecodePrivateKey;
17use ed25519_dalek::{
18 Signature as EdSignature, Signer as _, SigningKey, Verifier as _, VerifyingKey,
19};
20use sha2::{Digest, Sha256};
21
22use crate::base64;
23use crate::error::{Error, Result};
24use crate::tree::Hash;
25
26pub const ED25519_LOG: u8 = 0x01;
28pub const ED25519_COSIGNATURE: u8 = 0x04;
30
31#[must_use]
34pub fn key_id(name: &str, kind: u8, public_key: &[u8]) -> [u8; 4] {
35 let mut hasher = Sha256::new();
36 hasher.update(name.as_bytes());
37 hasher.update([0x0A, kind]);
38 hasher.update(public_key);
39 let digest = hasher.finalize();
40 [digest[0], digest[1], digest[2], digest[3]]
41}
42
43fn check_name(name: &str) -> Result<()> {
45 if name.is_empty() || name.chars().any(|c| c.is_whitespace() || c == '+') {
46 return Err(Error::Format(format!("key name {name:?}")));
47 }
48 Ok(())
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Checkpoint {
54 pub origin: String,
56 pub size: u64,
58 pub root: Hash,
60 pub extensions: Vec<String>,
62}
63
64impl Checkpoint {
65 #[must_use]
67 pub fn new(origin: &str, size: u64, root: Hash) -> Self {
68 Self {
69 origin: origin.to_string(),
70 size,
71 root,
72 extensions: Vec::new(),
73 }
74 }
75
76 #[must_use]
79 pub fn to_text(&self) -> String {
80 let mut text = format!(
81 "{}\n{}\n{}\n",
82 self.origin,
83 self.size,
84 base64::encode(&self.root)
85 );
86 for line in &self.extensions {
87 text.push_str(line);
88 text.push('\n');
89 }
90 text
91 }
92
93 pub fn parse(text: &str) -> Result<Self> {
95 let body = text
96 .strip_suffix('\n')
97 .ok_or_else(|| Error::Format("checkpoint does not end in a newline".to_string()))?;
98 let mut lines = body.split('\n');
99 let origin = lines
100 .next()
101 .filter(|l| !l.is_empty())
102 .ok_or_else(|| Error::Format("checkpoint has no origin".to_string()))?;
103 let size = lines
104 .next()
105 .ok_or_else(|| Error::Format("checkpoint has no size".to_string()))?;
106 if size != "0" && (size.starts_with('0') || size.is_empty()) {
107 return Err(Error::Format(format!("size {size:?} is not canonical")));
108 }
109 let size: u64 = size
110 .parse()
111 .map_err(|_| Error::Format(format!("size {size:?} is not a decimal")))?;
112 let root = lines
113 .next()
114 .and_then(base64::decode)
115 .filter(|r| r.len() == 32)
116 .ok_or_else(|| Error::Format("root is not a base64 32-byte hash".to_string()))?;
117 let mut hash = [0u8; 32];
118 hash.copy_from_slice(&root);
119 let extensions: Vec<String> = lines.map(str::to_string).collect();
120 if extensions.iter().any(String::is_empty) {
121 return Err(Error::Format("an extension line is empty".to_string()));
122 }
123 Ok(Self {
124 origin: origin.to_string(),
125 size,
126 root: hash,
127 extensions,
128 })
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct NoteSignature {
135 pub name: String,
137 pub key_id: [u8; 4],
139 pub payload: Vec<u8>,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct Note {
147 pub text: String,
149 pub signatures: Vec<NoteSignature>,
151}
152
153const DASH: &str = "\u{2014} ";
154
155impl Note {
156 #[must_use]
158 pub fn to_text(&self) -> String {
159 let mut out = self.text.clone();
160 out.push('\n');
161 for sig in &self.signatures {
162 let mut payload = sig.key_id.to_vec();
163 payload.extend_from_slice(&sig.payload);
164 out.push_str(DASH);
165 out.push_str(&sig.name);
166 out.push(' ');
167 out.push_str(&base64::encode(&payload));
168 out.push('\n');
169 }
170 out
171 }
172
173 pub fn parse(text: &str) -> Result<Self> {
177 if text.chars().any(|c| c.is_control() && c != '\n') {
178 return Err(Error::Format("note holds a control character".to_string()));
179 }
180 let split = text
181 .rfind("\n\n")
182 .ok_or_else(|| Error::Format("note has no blank line".to_string()))?;
183 let body = &text[..=split];
184 let rest = &text[split + 2..];
185 if body.len() < 2 {
186 return Err(Error::Format("note has no text".to_string()));
187 }
188 let mut signatures = Vec::new();
189 for line in rest.split_terminator('\n') {
190 let line = line
191 .strip_prefix(DASH)
192 .ok_or_else(|| Error::Format(format!("signature line {line:?}")))?;
193 let (name, encoded) = line
194 .split_once(' ')
195 .ok_or_else(|| Error::Format(format!("signature line {line:?}")))?;
196 check_name(name)?;
197 let payload = base64::decode(encoded)
198 .filter(|p| p.len() > 4)
199 .ok_or_else(|| Error::Format(format!("signature payload for {name}")))?;
200 let mut key_id = [0u8; 4];
201 key_id.copy_from_slice(&payload[..4]);
202 signatures.push(NoteSignature {
203 name: name.to_string(),
204 key_id,
205 payload: payload[4..].to_vec(),
206 });
207 }
208 if signatures.is_empty() {
209 return Err(Error::Format("note has no signature".to_string()));
210 }
211 if !rest.ends_with('\n') {
212 return Err(Error::Format("note does not end in a newline".to_string()));
213 }
214 Ok(Self {
215 text: body.to_string(),
216 signatures,
217 })
218 }
219}
220
221pub struct Signer {
223 name: String,
224 key: SigningKey,
225 key_id: [u8; 4],
226}
227
228impl Signer {
229 pub fn new(name: &str, key: SigningKey) -> Result<Self> {
231 check_name(name)?;
232 let key_id = key_id(name, ED25519_LOG, key.verifying_key().as_bytes());
233 Ok(Self {
234 name: name.to_string(),
235 key,
236 key_id,
237 })
238 }
239
240 pub fn from_pem_file(name: &str, path: &Path) -> Result<Self> {
242 let pem = std::fs::read_to_string(path)?;
243 Self::from_pem(name, &pem)
244 }
245
246 pub fn from_pem(name: &str, pem: &str) -> Result<Self> {
248 let key = SigningKey::from_pkcs8_pem(pem)
249 .map_err(|e| Error::Format(format!("signing key: {e}")))?;
250 Self::new(name, key)
251 }
252
253 #[must_use]
255 pub fn name(&self) -> &str {
256 &self.name
257 }
258
259 #[must_use]
261 pub fn verifier(&self) -> Verifier {
262 Verifier {
263 name: self.name.clone(),
264 kind: ED25519_LOG,
265 key: self.key.verifying_key(),
266 key_id: self.key_id,
267 }
268 }
269
270 #[must_use]
272 pub fn sign(&self, checkpoint: &Checkpoint) -> Note {
273 let text = checkpoint.to_text();
274 let signature = self.key.sign(text.as_bytes());
275 Note {
276 text,
277 signatures: vec![NoteSignature {
278 name: self.name.clone(),
279 key_id: self.key_id,
280 payload: signature.to_bytes().to_vec(),
281 }],
282 }
283 }
284}
285
286#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct Verifier {
289 name: String,
290 kind: u8,
291 key: VerifyingKey,
292 key_id: [u8; 4],
293}
294
295impl Verifier {
296 pub fn new(name: &str, kind: u8, key: VerifyingKey) -> Result<Self> {
299 check_name(name)?;
300 if kind != ED25519_LOG && kind != ED25519_COSIGNATURE {
301 return Err(Error::Format(format!("signature type {kind:#04x}")));
302 }
303 Ok(Self {
304 name: name.to_string(),
305 kind,
306 key_id: key_id(name, kind, key.as_bytes()),
307 key,
308 })
309 }
310
311 pub fn parse(text: &str) -> Result<Self> {
314 let mut parts = text.splitn(3, '+');
315 let (Some(name), Some(id_hex), Some(encoded)) = (parts.next(), parts.next(), parts.next())
316 else {
317 return Err(Error::Format(format!("verifier key {text:?}")));
318 };
319 let bytes = base64::decode(encoded)
320 .filter(|b| b.len() == 33)
321 .ok_or_else(|| Error::Format(format!("verifier key {text:?}")))?;
322 let key = VerifyingKey::from_bytes(bytes[1..].try_into().expect("32 bytes"))
323 .map_err(|e| Error::Format(format!("verifier key: {e}")))?;
324 let verifier = Self::new(name, bytes[0], key)?;
325 if id_hex.len() != 8 || rfc9162::hex(&verifier.key_id) != id_hex.to_ascii_lowercase() {
326 return Err(Error::Format(format!(
327 "verifier key id {id_hex} is not the key's {}",
328 rfc9162::hex(&verifier.key_id)
329 )));
330 }
331 Ok(verifier)
332 }
333
334 #[must_use]
336 pub fn to_text(&self) -> String {
337 let mut bytes = vec![self.kind];
338 bytes.extend_from_slice(self.key.as_bytes());
339 format!(
340 "{}+{}+{}",
341 self.name,
342 rfc9162::hex(&self.key_id),
343 base64::encode(&bytes)
344 )
345 }
346
347 #[must_use]
349 pub fn name(&self) -> &str {
350 &self.name
351 }
352
353 #[must_use]
355 pub fn key_id(&self) -> [u8; 4] {
356 self.key_id
357 }
358
359 #[must_use]
361 pub fn kind(&self) -> u8 {
362 self.kind
363 }
364
365 fn matches(&self, sig: &NoteSignature) -> bool {
366 sig.name == self.name && sig.key_id == self.key_id
367 }
368
369 fn check(&self, message: &[u8], signature: &[u8]) -> Result<()> {
370 let signature = EdSignature::from_slice(signature)
371 .map_err(|_| Error::Signature(format!("{}: not an Ed25519 signature", self.name)))?;
372 self.key
373 .verify(message, &signature)
374 .map_err(|_| Error::Signature(format!("{}: signature does not verify", self.name)))
375 }
376}
377
378pub fn verify<'a>(note: &Note, trusted: &'a [Verifier]) -> Result<Vec<&'a Verifier>> {
383 let mut verified = Vec::new();
384 for sig in ¬e.signatures {
385 for key in trusted
386 .iter()
387 .filter(|k| k.kind == ED25519_LOG && k.matches(sig))
388 {
389 key.check(note.text.as_bytes(), &sig.payload)?;
390 verified.push(key);
391 }
392 }
393 if verified.is_empty() {
394 return Err(Error::Signature(
395 "no trusted key signed the note".to_string(),
396 ));
397 }
398 Ok(verified)
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct Cosignature {
404 pub name: String,
406 pub timestamp: u64,
408}
409
410#[must_use]
412pub fn cosignature_message(checkpoint_text: &str, timestamp: u64) -> Vec<u8> {
413 format!("cosignature/v1\ntime {timestamp}\n{checkpoint_text}").into_bytes()
414}
415
416#[must_use]
419pub fn cosignature_payload(timestamp: u64, signature: &[u8]) -> Vec<u8> {
420 let mut payload = timestamp.to_be_bytes().to_vec();
421 payload.extend_from_slice(signature);
422 payload
423}
424
425pub fn verify_cosignatures(note: &Note, witnesses: &[Verifier]) -> Result<Vec<Cosignature>> {
432 let mut verified = Vec::new();
433 for sig in ¬e.signatures {
434 for witness in witnesses
435 .iter()
436 .filter(|w| w.kind == ED25519_COSIGNATURE && w.matches(sig))
437 {
438 let (stamp, signature) = sig.payload.split_at_checked(8).ok_or_else(|| {
439 Error::Signature(format!("{}: cosignature payload is short", witness.name))
440 })?;
441 let timestamp = u64::from_be_bytes(stamp.try_into().expect("eight bytes"));
442 witness.check(&cosignature_message(¬e.text, timestamp), signature)?;
443 verified.push(Cosignature {
444 name: witness.name.clone(),
445 timestamp,
446 });
447 }
448 }
449 Ok(verified)
450}
451
452pub struct Cosigner {
456 name: String,
457 key: SigningKey,
458 key_id: [u8; 4],
459}
460
461impl Cosigner {
462 pub fn new(name: &str, key: SigningKey) -> Result<Self> {
464 check_name(name)?;
465 let key_id = key_id(name, ED25519_COSIGNATURE, key.verifying_key().as_bytes());
466 Ok(Self {
467 name: name.to_string(),
468 key,
469 key_id,
470 })
471 }
472
473 #[must_use]
475 pub fn verifier(&self) -> Verifier {
476 Verifier {
477 name: self.name.clone(),
478 kind: ED25519_COSIGNATURE,
479 key: self.key.verifying_key(),
480 key_id: self.key_id,
481 }
482 }
483
484 pub fn cosign(&self, note: &mut Note, timestamp: u64) {
486 let signature = self.key.sign(&cosignature_message(¬e.text, timestamp));
487 note.signatures.push(NoteSignature {
488 name: self.name.clone(),
489 key_id: self.key_id,
490 payload: cosignature_payload(timestamp, &signature.to_bytes()),
491 });
492 }
493}