Skip to main content

rfc9162_tiles/
checkpoint.rs

1//! The C2SP checkpoint (<https://c2sp.org/tlog-checkpoint>) as a signed note
2//! (<https://c2sp.org/signed-note>), and the witness cosignature
3//! (<https://c2sp.org/tlog-cosignature>) over it.
4//!
5//! A checkpoint is three lines, the origin, the decimal size and the base64
6//! root, plus optional extension lines. A note is that text, a blank line
7//! and signature lines `— <name> base64(key id || signature)`. A log signs
8//! the note text with Ed25519 under key id
9//! `SHA-256(name || 0x0A || 0x01 || public key)[:4]`; a witness cosigns the
10//! lines `cosignature/v1`, `time <posix seconds>` and the checkpoint lines,
11//! under key id `SHA-256(name || 0x0A || 0x04 || public key)[:4]`, with the
12//! eight-byte big-endian timestamp between key id and signature.
13
14use 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
26/// The signature type byte of an Ed25519 log signature.
27pub const ED25519_LOG: u8 = 0x01;
28/// The signature type byte of an Ed25519 witness cosignature.
29pub const ED25519_COSIGNATURE: u8 = 0x04;
30
31/// The key id of a key under a name: the first four bytes of
32/// `SHA-256(name || 0x0A || type || public key)`.
33#[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
43/// A key name: non-empty, no spaces, no `+`.
44fn 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/// What a log publishes: its origin, its size and the root at that size.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Checkpoint {
54    /// The log's origin, a schema-less URL such as `example.com/log`.
55    pub origin: String,
56    /// Entries the root covers.
57    pub size: u64,
58    /// The root at that size.
59    pub root: Hash,
60    /// Further non-empty lines, in order.
61    pub extensions: Vec<String>,
62}
63
64impl Checkpoint {
65    /// A checkpoint over `origin`, `size` and `root`, without extensions.
66    #[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    /// The note text: origin, size, base64 root and extension lines, each
77    /// newline-terminated.
78    #[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    /// The checkpoint a note text holds.
94    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/// One signature line of a note.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct NoteSignature {
135    /// The key name on the line.
136    pub name: String,
137    /// The first four bytes of the decoded payload.
138    pub key_id: [u8; 4],
139    /// The rest of the payload: the signature, or for a cosignature the
140    /// timestamp and the signature.
141    pub payload: Vec<u8>,
142}
143
144/// A note: text, a blank line, signature lines.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct Note {
147    /// The signed text, newline-terminated.
148    pub text: String,
149    /// The signature lines, in order.
150    pub signatures: Vec<NoteSignature>,
151}
152
153const DASH: &str = "\u{2014} ";
154
155impl Note {
156    /// The note as text.
157    #[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    /// The note `text` spells: the text up to the last blank line, then one
174    /// signature per line. Control characters other than newline, a text
175    /// without a trailing newline, and a malformed signature line are refused.
176    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
221/// A log's signing key under its name (the origin, by the checkpoint spec).
222pub struct Signer {
223    name: String,
224    key: SigningKey,
225    key_id: [u8; 4],
226}
227
228impl Signer {
229    /// A signer from a key; `name` is checked as a key name.
230    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    /// A signer from a PKCS#8 PEM file (`-----BEGIN PRIVATE KEY-----`).
241    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    /// A signer from PKCS#8 PEM text.
247    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    /// The key name.
254    #[must_use]
255    pub fn name(&self) -> &str {
256        &self.name
257    }
258
259    /// The verifier for this key.
260    #[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    /// The note over `checkpoint` with this key's signature line.
271    #[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/// A public key under a name, with the id a verifier matches lines on.
287#[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    /// A verifier for `kind` (`ED25519_LOG` or `ED25519_COSIGNATURE`) under
297    /// `name`.
298    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    /// A verifier from the signed-note key text
312    /// `<name>+<hex key id>+base64(<type> || <public key>)`.
313    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    /// The signed-note key text of this verifier.
335    #[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    /// The key name.
348    #[must_use]
349    pub fn name(&self) -> &str {
350        &self.name
351    }
352
353    /// The key id.
354    #[must_use]
355    pub fn key_id(&self) -> [u8; 4] {
356        self.key_id
357    }
358
359    /// The signature type byte.
360    #[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
378/// Checks a note's log signatures against `trusted` log keys: a line whose
379/// name and key id match a trusted key has to verify, lines from unknown
380/// keys are ignored, and at least one trusted key has to have signed.
381/// Returns the trusted keys that verified, in note order.
382pub fn verify<'a>(note: &Note, trusted: &'a [Verifier]) -> Result<Vec<&'a Verifier>> {
383    let mut verified = Vec::new();
384    for sig in &note.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/// A witness cosignature on a checkpoint.
402#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct Cosignature {
404    /// The witness key name.
405    pub name: String,
406    /// Posix seconds the witness signed at.
407    pub timestamp: u64,
408}
409
410/// The message a cosignature/v1 signs over the checkpoint text at `timestamp`.
411#[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/// A cosignature line's payload, after the key id: the eight-byte big-endian
417/// timestamp and the Ed25519 signature.
418#[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
425/// Checks the cosignature/v1 lines of `note` against `witnesses`: a line whose
426/// name and key id match a witness has to verify over `cosignature/v1`,
427/// `time <t>` and the note text. Returns each verified witness with its
428/// timestamp, in note order; lines from unknown keys are ignored. A trusted
429/// witness that signed nothing is not an error; a witness line that does not
430/// verify is.
431pub fn verify_cosignatures(note: &Note, witnesses: &[Verifier]) -> Result<Vec<Cosignature>> {
432    let mut verified = Vec::new();
433    for sig in &note.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(&note.text, timestamp), signature)?;
443            verified.push(Cosignature {
444                name: witness.name.clone(),
445                timestamp,
446            });
447        }
448    }
449    Ok(verified)
450}
451
452/// A witness key that cosigns checkpoints; the counterpart of
453/// [`verify_cosignatures`], for logs that also act as their own witness and
454/// for tests.
455pub struct Cosigner {
456    name: String,
457    key: SigningKey,
458    key_id: [u8; 4],
459}
460
461impl Cosigner {
462    /// A cosigner from a key under `name`.
463    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    /// The verifier for this witness key.
474    #[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    /// Adds this witness's cosignature line at `timestamp` to `note`.
485    pub fn cosign(&self, note: &mut Note, timestamp: u64) {
486        let signature = self.key.sign(&cosignature_message(&note.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}