Skip to main content

datumesh_core/
sign.rs

1//! Signing receipts (feature `sign`): [`SigningKey`] and [`ReceiptBuilder`].
2//!
3//! Ed25519 is RFC 8032 pure Ed25519, deterministic, built on the crate's existing
4//! `curve25519-dalek` and `sha2`. The seed is not zeroised when a key is dropped (that would
5//! need another dependency); keep keys in memory only as long as needed.
6
7use core::fmt;
8
9use crate::crypto::{key_id, SecretKey};
10use crate::error::Result;
11use crate::json::Value;
12use crate::lexical::to_hex;
13use crate::model::{args_to_value, Args, Cell, Scalar};
14use crate::receipt::Receipt;
15
16/// An Ed25519 signing key, from its 32-byte seed (RFC 8032 5.1.5).
17///
18/// `Debug` shows the key id, never the seed.
19#[derive(Clone)]
20pub struct SigningKey {
21    secret: SecretKey,
22    key_id: String,
23}
24
25impl SigningKey {
26    /// The key whose seed is `seed`.
27    pub fn from_seed(seed: &[u8; 32]) -> SigningKey {
28        let secret = SecretKey::from_seed(seed);
29        let key_id = key_id(&secret.public);
30        SigningKey { secret, key_id }
31    }
32
33    /// The 32-byte public key, as a key document's `public` holds it in hex (9.2).
34    pub fn public_key(&self) -> [u8; 32] {
35        self.secret.public
36    }
37
38    /// The key id (9.1): the first 16 hex digits of SHA-256 over the public key.
39    pub fn key_id(&self) -> &str {
40        &self.key_id
41    }
42}
43
44impl fmt::Debug for SigningKey {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.debug_struct("SigningKey").field("key_id", &self.key_id).finish_non_exhaustive()
47    }
48}
49
50/// The members of a receipt before it is signed; [`sign`](Self::sign) is the only way to
51/// turn it into a [`Receipt`].
52///
53/// Every member of 8.1 has a setter except `proto` (always `datumesh/1`), `type` (the
54/// value's type), `sig` (made by `sign`) and `computed_by`: that one is derived, as
55/// `<signer host>#<key id of the signing key>`, so a receipt naming another key than the
56/// one that signed it cannot be built. `inputs` defaults to empty (a stored cell); every
57/// other member must be set.
58///
59/// Nothing is repaired: `sign` checks the members exactly as [`Receipt::from_json`] checks a
60/// parsed receipt (steps 2-4 of 10.1) and returns the error it would report.
61#[derive(Clone, Debug, Default)]
62pub struct ReceiptBuilder {
63    cell: Option<Cell>,
64    value: Option<Scalar>,
65    valid: Option<Args>,
66    asof: Option<String>,
67    tx: Option<String>,
68    issued: Option<String>,
69    publisher: Option<String>,
70    signer_host: Option<String>,
71    inputs: Vec<String>,
72    licence: Option<String>,
73    summary: Option<String>,
74}
75
76impl ReceiptBuilder {
77    /// A builder with no members set.
78    pub fn new() -> ReceiptBuilder {
79        ReceiptBuilder::default()
80    }
81
82    /// The cell answered (no slices).
83    pub fn cell(mut self, cell: Cell) -> Self {
84        self.cell = Some(cell);
85        self
86    }
87
88    /// The value; also sets `type`.
89    pub fn value(mut self, value: Scalar) -> Self {
90        self.value = Some(value);
91        self
92    }
93
94    /// The valid-time arguments, echoed from the cell's arguments (`{}` if none).
95    pub fn valid(mut self, valid: Args) -> Self {
96        self.valid = Some(valid);
97        self
98    }
99
100    /// The transaction time answered as of.
101    pub fn asof(mut self, asof: impl Into<String>) -> Self {
102        self.asof = Some(asof.into());
103        self
104    }
105
106    /// When the value was recorded.
107    pub fn tx(mut self, tx: impl Into<String>) -> Self {
108        self.tx = Some(tx.into());
109        self
110    }
111
112    /// When the receipt is signed.
113    pub fn issued(mut self, issued: impl Into<String>) -> Self {
114        self.issued = Some(issued.into());
115        self
116    }
117
118    /// The namespace that owns the function; must equal the cell's `ns`.
119    pub fn publisher(mut self, publisher: impl Into<String>) -> Self {
120        self.publisher = Some(publisher.into());
121        self
122    }
123
124    /// The host publishing the signing key: `computed_by` is `<host>#<key id>`.
125    pub fn signer_host(mut self, host: impl Into<String>) -> Self {
126        self.signer_host = Some(host.into());
127        self
128    }
129
130    /// The inputs entries, receipt hashes and slice entries (`slice:<slice id>:<members
131    /// hash>`, see [`crate::Manifest::entry`]); must be strictly ascending.
132    pub fn inputs<I, S>(mut self, inputs: I) -> Self
133    where
134        I: IntoIterator<Item = S>,
135        S: Into<String>,
136    {
137        self.inputs = inputs.into_iter().map(Into::into).collect();
138        self
139    }
140
141    /// An SPDX identifier or an https URL.
142    pub fn licence(mut self, licence: impl Into<String>) -> Self {
143        self.licence = Some(licence.into());
144        self
145    }
146
147    /// One human sentence saying what the value is.
148    pub fn summary(mut self, summary: impl Into<String>) -> Self {
149        self.summary = Some(summary.into());
150        self
151    }
152
153    /// Checks the members as [`Receipt::from_json`] does, then signs the payload (8.3) with
154    /// `key`. The receipt's `computed_by` is `<signer host>#<key.key_id()>`.
155    ///
156    /// Errors: `E_SCHEMA` for a member not set (path `$`), otherwise the code and path
157    /// `from_json` reports for the same members.
158    pub fn sign(&self, key: &SigningKey) -> Result<Receipt> {
159        let s = |x: &Option<String>| x.as_ref().map(|t| Value::String(t.clone()));
160        let computed_by = self.signer_host.as_ref().map(|h| Value::String(format!("{h}#{}", key.key_id())));
161        let (ty, value) = match &self.value {
162            Some(v) => {
163                let raw = match v {
164                    Scalar::Boolean(b) => Value::Bool(*b),
165                    other => Value::String(other.lexical().to_string()),
166                };
167                (Some(Value::String(v.ty().as_str().into())), Some(raw))
168            }
169            None => (None, None),
170        };
171        // Any well-formed placeholder: `sig` is not part of the payload.
172        let placeholder = "0".repeat(128);
173        let members: Vec<(&str, Option<Value>)> = vec![
174            ("asof", s(&self.asof)),
175            ("cell", self.cell.as_ref().map(Cell::to_value)),
176            ("computed_by", computed_by),
177            ("inputs", Some(Value::Array(self.inputs.iter().map(|h| Value::String(h.clone())).collect()))),
178            ("issued", s(&self.issued)),
179            ("licence", s(&self.licence)),
180            ("proto", Some(Value::String("datumesh/1".into()))),
181            ("publisher", s(&self.publisher)),
182            ("sig", Some(Value::String(placeholder))),
183            ("summary", s(&self.summary)),
184            ("tx", s(&self.tx)),
185            ("type", ty),
186            ("valid", self.valid.as_ref().map(args_to_value)),
187            ("value", value),
188        ];
189        let object = members.into_iter().filter_map(|(k, v)| v.map(|v| (k.to_string(), v))).collect();
190        let unsigned = Receipt::from_value(&Value::Object(object))?;
191        let sig = key.secret.sign(unsigned.payload());
192        Ok(unsigned.with_sig(to_hex(&sig)))
193    }
194}