Skip to main content

macula_rust/
binding.rs

1//! TLS and CONNECT bindings and status statements, as macula_key_bindings and
2//! macula-go make and check them. A binding ties a station's TLS leaf, or a
3//! node's CONNECT key, to an identity key for up to 7 days; a status
4//! statement keeps a binding in force for up to an hour. Each travels as
5//! `{tbs, signature}`, the signature over its label, a zero byte and the tbs
6//! bytes; a verifier checks the signature over the bytes it received first,
7//! and only then decodes them.
8
9use std::fmt;
10
11use sha2::{Digest, Sha384};
12
13use crate::cbor::{self, Value};
14use crate::node_key::{node_id_of, verify, KeyError, NodeKey};
15use crate::profile::Profile;
16
17const LABEL_BINDING_TLS: &str = "MACULA-PQ-BINDING-TLS-V1";
18const LABEL_BINDING_CONNECT: &str = "MACULA-PQ-BINDING-CONNECT-V1";
19const LABEL_STATUS: &str = "MACULA-PQ-STATUS-V1";
20const MAX_BINDING_MS: i64 = 7 * 24 * 60 * 60 * 1000;
21const MAX_STATUS_MS: i64 = 60 * 60 * 1000;
22const TOLERANCE_MS: i64 = 5 * 60 * 1000;
23const MAX_PROTOCOL_INT: i64 = 1 << 53;
24
25const BINDING_FIELDS: [&str; 9] = [
26    "label",
27    "node_id",
28    "use",
29    "subject_hash",
30    "binding_id",
31    "not_before",
32    "not_after",
33    "hash_alg",
34    "sig_alg",
35];
36const STATUS_FIELDS: [&str; 6] = [
37    "label",
38    "node_id",
39    "binding_hash",
40    "issued_at",
41    "expires_at",
42    "sig_alg",
43];
44
45/// What a binding binds to the identity key.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum BindingUse {
48    /// A station's TLS key, by the leaf certificate it presents.
49    Tls,
50    /// A CONNECT key.
51    Connect,
52}
53
54impl BindingUse {
55    fn name(self) -> &'static str {
56        match self {
57            BindingUse::Tls => "tls",
58            BindingUse::Connect => "connect",
59        }
60    }
61
62    fn label(self) -> &'static str {
63        match self {
64            BindingUse::Tls => LABEL_BINDING_TLS,
65            BindingUse::Connect => LABEL_BINDING_CONNECT,
66        }
67    }
68}
69
70/// The refusals of the binding and status checks, named as macula names them.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum BindingError {
73    /// A signed structure of the wrong shape: not exactly `{tbs, signature}`,
74    /// or a tbs that the decoding rule refuses, with a key its structure does
75    /// not define, or a field of the wrong type, length or range.
76    Malformed,
77    /// A binding whose signature does not verify under its use's label.
78    BindingSignatureInvalid,
79    /// A binding whose label or use is another use's.
80    WrongUse,
81    /// A binding whose subject is not the leaf or CONNECT key it came with.
82    KeyMismatch,
83    /// A binding more than 5 minutes past its not_after.
84    Expired,
85    /// A binding more than 5 minutes before its not_before.
86    NotYetValid,
87    /// A binding or statement naming a node_id other than the identity key's.
88    NodeIdMismatch,
89    /// A status statement whose signature does not verify.
90    StatusSignatureInvalid,
91    /// A status statement for another binding.
92    StatusBindingMismatch,
93    /// A status statement more than 5 minutes past its expiry.
94    StatusExpired,
95    /// A status statement issued more than 5 minutes ahead.
96    StatusFutureDated,
97    /// A window a verifier would refuse: negative, backwards, at 2^53 or later,
98    /// or longer than 7 days for a binding and an hour for a statement.
99    ValidityWindow,
100    /// The signing key could not sign, or is not an identity key.
101    Key(KeyError),
102}
103
104impl fmt::Display for BindingError {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            BindingError::Malformed => f.write_str("malformed signed structure"),
108            BindingError::BindingSignatureInvalid => {
109                f.write_str("the binding's signature does not verify")
110            }
111            BindingError::WrongUse => f.write_str("the binding is for another use"),
112            BindingError::KeyMismatch => f.write_str("the binding binds another key"),
113            BindingError::Expired => f.write_str("the binding has expired"),
114            BindingError::NotYetValid => f.write_str("the binding is not valid yet"),
115            BindingError::NodeIdMismatch => f.write_str("the structure names another node_id"),
116            BindingError::StatusSignatureInvalid => {
117                f.write_str("the status statement's signature does not verify")
118            }
119            BindingError::StatusBindingMismatch => {
120                f.write_str("the status statement is for another binding")
121            }
122            BindingError::StatusExpired => f.write_str("the status statement has expired"),
123            BindingError::StatusFutureDated => {
124                f.write_str("the status statement is dated in the future")
125            }
126            BindingError::ValidityWindow => {
127                f.write_str("a validity period outside what a verifier accepts")
128            }
129            BindingError::Key(e) => write!(f, "{e}"),
130        }
131    }
132}
133
134impl std::error::Error for BindingError {}
135
136impl From<KeyError> for BindingError {
137    fn from(e: KeyError) -> Self {
138        BindingError::Key(e)
139    }
140}
141
142/// A signed structure as it travels: tbs, the deterministic CBOR of its
143/// fields, and a signature over its label, a zero byte and tbs.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct SignedTbs {
146    pub tbs: Vec<u8>,
147    pub signature: Vec<u8>,
148}
149
150impl SignedTbs {
151    /// The structure as the map `{tbs, signature}`.
152    pub fn to_value(&self) -> Value {
153        Value::Map(vec![
154            (Value::text("tbs"), Value::Bytes(self.tbs.clone())),
155            (
156                Value::text("signature"),
157                Value::Bytes(self.signature.clone()),
158            ),
159        ])
160    }
161
162    /// The structure in `value`, which must be a map of exactly `tbs` and
163    /// `signature`, both byte strings.
164    pub fn from_value(value: &Value) -> Result<SignedTbs, BindingError> {
165        match value {
166            Value::Map(pairs) if pairs.len() == 2 => {
167                match (value.get("tbs"), value.get("signature")) {
168                    (Some(Value::Bytes(tbs)), Some(Value::Bytes(signature))) => Ok(SignedTbs {
169                        tbs: tbs.clone(),
170                        signature: signature.clone(),
171                    }),
172                    _ => Err(BindingError::Malformed),
173                }
174            }
175            _ => Err(BindingError::Malformed),
176        }
177    }
178}
179
180/// What a verified binding says.
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct BindingInfo {
183    pub use_: BindingUse,
184    pub node_id: [u8; 32],
185    pub not_after: i64,
186}
187
188/// Binds the TLS key of the leaf certificate a listener presents to
189/// `identity_key`, by the SHA-384 of the leaf's DER, from `not_before` to
190/// `not_after` in milliseconds.
191pub fn tls_binding(
192    identity_key: &NodeKey,
193    leaf_der: &[u8],
194    not_before: i64,
195    not_after: i64,
196) -> Result<SignedTbs, BindingError> {
197    issue_binding(
198        identity_key,
199        BindingUse::Tls,
200        &sha384(leaf_der),
201        not_before,
202        not_after,
203    )
204}
205
206/// Binds a CONNECT key, by the SHA-384 of the key as carried, to
207/// `identity_key`, from `not_before` to `not_after` in milliseconds.
208pub fn connect_binding(
209    identity_key: &NodeKey,
210    connect_key: &[u8],
211    not_before: i64,
212    not_after: i64,
213) -> Result<SignedTbs, BindingError> {
214    issue_binding(
215        identity_key,
216        BindingUse::Connect,
217        &sha384(connect_key),
218        not_before,
219        not_after,
220    )
221}
222
223fn issue_binding(
224    identity_key: &NodeKey,
225    use_: BindingUse,
226    subject_hash: &[u8; 48],
227    not_before: i64,
228    not_after: i64,
229) -> Result<SignedTbs, BindingError> {
230    let node_id = identity_key.node_id()?;
231    if !within_window(not_before, not_after, MAX_BINDING_MS) {
232        return Err(BindingError::ValidityWindow);
233    }
234    let mut binding_id = [0u8; 16];
235    aws_lc_rs::rand::fill(&mut binding_id)
236        .map_err(|_| BindingError::Key(KeyError::RandomnessUnavailable))?;
237    let tbs = encode(Value::Map(vec![
238        text("label", use_.label()),
239        bytes("node_id", &node_id),
240        text("use", use_.name()),
241        bytes("subject_hash", subject_hash),
242        bytes("binding_id", &binding_id),
243        int("not_before", not_before),
244        int("not_after", not_after),
245        text("hash_alg", "SHA-384"),
246        text("sig_alg", identity_key.profile().sig_alg()),
247    ]));
248    sign_tbs(identity_key, use_.label(), tbs)
249}
250
251/// Keeps `binding` in force from `issued_at` to `expires_at` in milliseconds,
252/// at most an hour, signed by `identity_key`.
253pub fn status_statement(
254    identity_key: &NodeKey,
255    binding: &SignedTbs,
256    issued_at: i64,
257    expires_at: i64,
258) -> Result<SignedTbs, BindingError> {
259    let node_id = identity_key.node_id()?;
260    if !within_window(issued_at, expires_at, MAX_STATUS_MS) {
261        return Err(BindingError::ValidityWindow);
262    }
263    let tbs = encode(Value::Map(vec![
264        text("label", LABEL_STATUS),
265        bytes("node_id", &node_id),
266        bytes("binding_hash", &sha384(&binding.tbs)),
267        int("issued_at", issued_at),
268        int("expires_at", expires_at),
269        text("sig_alg", identity_key.profile().sig_alg()),
270    ]));
271    sign_tbs(identity_key, LABEL_STATUS, tbs)
272}
273
274/// Checks `binding` against the identity key as carried, under `profile`, and
275/// the leaf certificate this connection presented, at `now_ms` with 5 minutes
276/// of tolerance.
277pub fn verify_tls_binding(
278    binding: &SignedTbs,
279    identity_key: &[u8],
280    profile: Profile,
281    leaf_der: &[u8],
282    now_ms: i64,
283) -> Result<BindingInfo, BindingError> {
284    verify_binding(
285        binding,
286        identity_key,
287        profile,
288        BindingUse::Tls,
289        &sha384(leaf_der),
290        now_ms,
291    )
292}
293
294/// Checks `binding` against the identity key as carried, under `profile`, and
295/// the CONNECT key as carried, at `now_ms` with 5 minutes of tolerance.
296pub fn verify_connect_binding(
297    binding: &SignedTbs,
298    identity_key: &[u8],
299    profile: Profile,
300    connect_key: &[u8],
301    now_ms: i64,
302) -> Result<BindingInfo, BindingError> {
303    verify_binding(
304        binding,
305        identity_key,
306        profile,
307        BindingUse::Connect,
308        &sha384(connect_key),
309        now_ms,
310    )
311}
312
313/// The signature over the tbs bytes as received first, then the tbs decoded,
314/// then its fields in macula's order: shape, use, node_id, subject,
315/// not_before, not_after.
316fn verify_binding(
317    binding: &SignedTbs,
318    identity_key: &[u8],
319    profile: Profile,
320    use_: BindingUse,
321    subject_hash: &[u8; 48],
322    now_ms: i64,
323) -> Result<BindingInfo, BindingError> {
324    if !verify(
325        &labelled(use_.label(), &binding.tbs),
326        &binding.signature,
327        identity_key,
328        profile,
329    ) {
330        return Err(BindingError::BindingSignatureInvalid);
331    }
332    let fields = decode_tbs(&binding.tbs, &BINDING_FIELDS).ok_or(BindingError::Malformed)?;
333    let parsed = well_formed_binding(&fields, profile).ok_or(BindingError::Malformed)?;
334    if parsed.label != use_.label() || parsed.use_ != use_.name() {
335        return Err(BindingError::WrongUse);
336    }
337    if parsed.node_id != node_id_of(identity_key, profile) {
338        return Err(BindingError::NodeIdMismatch);
339    }
340    if &parsed.subject_hash != subject_hash {
341        return Err(BindingError::KeyMismatch);
342    }
343    if now_ms + TOLERANCE_MS < parsed.not_before {
344        return Err(BindingError::NotYetValid);
345    }
346    if now_ms - TOLERANCE_MS > parsed.not_after {
347        return Err(BindingError::Expired);
348    }
349    Ok(BindingInfo {
350        use_,
351        node_id: parsed.node_id,
352        not_after: parsed.not_after,
353    })
354}
355
356struct BindingTbs<'a> {
357    label: &'a str,
358    use_: &'a str,
359    node_id: [u8; 32],
360    subject_hash: [u8; 48],
361    not_before: i64,
362    not_after: i64,
363}
364
365fn well_formed_binding<'a>(f: &'a Fields, profile: Profile) -> Option<BindingTbs<'a>> {
366    let binding_id = field_bytes(f, "binding_id")?;
367    let not_before = protocol_int(f, "not_before")?;
368    let not_after = protocol_int(f, "not_after")?;
369    let well_formed = binding_id.len() == 16
370        && within_window(not_before, not_after, MAX_BINDING_MS)
371        && field_text(f, "hash_alg")? == "SHA-384"
372        && field_text(f, "sig_alg")? == profile.sig_alg();
373    well_formed.then_some(BindingTbs {
374        label: field_text(f, "label")?,
375        use_: field_text(f, "use")?,
376        node_id: field_array(f, "node_id")?,
377        subject_hash: field_array(f, "subject_hash")?,
378        not_before,
379        not_after,
380    })
381}
382
383/// Checks a status statement for the binding it came with, against the
384/// identity key as carried, under `profile`, at `now_ms` with 5 minutes of
385/// tolerance, and returns when the statement expires. It checks that the
386/// statement names that binding, not the binding itself: a caller verifies
387/// the binding too.
388pub fn verify_status(
389    statement: &SignedTbs,
390    binding: &SignedTbs,
391    identity_key: &[u8],
392    profile: Profile,
393    now_ms: i64,
394) -> Result<i64, BindingError> {
395    if !verify(
396        &labelled(LABEL_STATUS, &statement.tbs),
397        &statement.signature,
398        identity_key,
399        profile,
400    ) {
401        return Err(BindingError::StatusSignatureInvalid);
402    }
403    let fields = decode_tbs(&statement.tbs, &STATUS_FIELDS).ok_or(BindingError::Malformed)?;
404    let issued_at = protocol_int(&fields, "issued_at").ok_or(BindingError::Malformed)?;
405    let expires_at = protocol_int(&fields, "expires_at").ok_or(BindingError::Malformed)?;
406    let node_id: [u8; 32] = field_array(&fields, "node_id").ok_or(BindingError::Malformed)?;
407    let binding_hash: [u8; 48] =
408        field_array(&fields, "binding_hash").ok_or(BindingError::Malformed)?;
409    let well_formed = field_text(&fields, "label") == Some(LABEL_STATUS)
410        && within_window(issued_at, expires_at, MAX_STATUS_MS)
411        && field_text(&fields, "sig_alg") == Some(profile.sig_alg());
412    if !well_formed {
413        return Err(BindingError::Malformed);
414    }
415    if node_id != node_id_of(identity_key, profile) {
416        return Err(BindingError::NodeIdMismatch);
417    }
418    if binding_hash != sha384(&binding.tbs) {
419        return Err(BindingError::StatusBindingMismatch);
420    }
421    if issued_at > now_ms + TOLERANCE_MS {
422        return Err(BindingError::StatusFutureDated);
423    }
424    if now_ms - TOLERANCE_MS > expires_at {
425        return Err(BindingError::StatusExpired);
426    }
427    Ok(expires_at)
428}
429
430/// A tbs's fields by name.
431type Fields = std::collections::HashMap<String, Value>;
432
433/// `tbs` decoded under the decoding rule, when it is a map whose keys are
434/// exactly `names`, all text.
435fn decode_tbs(tbs: &[u8], names: &[&str]) -> Option<Fields> {
436    let Value::Map(pairs) = cbor::decode(tbs).ok()? else {
437        return None;
438    };
439    if pairs.len() != names.len() {
440        return None;
441    }
442    let mut fields = Fields::with_capacity(pairs.len());
443    for (key, value) in pairs {
444        let Value::Text(name) = key else {
445            return None;
446        };
447        fields.insert(name, value);
448    }
449    names
450        .iter()
451        .all(|n| fields.contains_key(*n))
452        .then_some(fields)
453}
454
455/// Whether `from` and `to` are a validity period a verifier accepts: `from`
456/// at least 0, `to` no earlier than `from` and below 2^53, and at most `max`
457/// apart.
458fn within_window(from: i64, to: i64, max: i64) -> bool {
459    from >= 0 && from <= to && to < MAX_PROTOCOL_INT && to - from <= max
460}
461
462fn protocol_int(f: &Fields, name: &str) -> Option<i64> {
463    match f.get(name)? {
464        Value::Int(n) => i64::try_from(*n).ok(),
465        _ => None,
466    }
467}
468
469fn field_text<'a>(f: &'a Fields, name: &str) -> Option<&'a str> {
470    match f.get(name)? {
471        Value::Text(t) => Some(t),
472        _ => None,
473    }
474}
475
476fn field_bytes<'a>(f: &'a Fields, name: &str) -> Option<&'a [u8]> {
477    match f.get(name)? {
478        Value::Bytes(b) => Some(b),
479        _ => None,
480    }
481}
482
483fn field_array<const N: usize>(f: &Fields, name: &str) -> Option<[u8; N]> {
484    field_bytes(f, name)?.try_into().ok()
485}
486
487fn sign_tbs(key: &NodeKey, label: &str, tbs: Vec<u8>) -> Result<SignedTbs, BindingError> {
488    let signature = key.sign(&labelled(label, &tbs))?;
489    Ok(SignedTbs { tbs, signature })
490}
491
492/// `label`, a zero byte and `tbs`: what a binding or status statement signs.
493fn labelled(label: &str, tbs: &[u8]) -> Vec<u8> {
494    let mut out = Vec::with_capacity(label.len() + 1 + tbs.len());
495    out.extend_from_slice(label.as_bytes());
496    out.push(0);
497    out.extend_from_slice(tbs);
498    out
499}
500
501fn sha384(bytes: &[u8]) -> [u8; 48] {
502    Sha384::digest(bytes).into()
503}
504
505/// A map of text keys and protocol values always encodes: every integer here
506/// is an i64.
507fn encode(value: Value) -> Vec<u8> {
508    cbor::encode(&value).expect("text-keyed fields of i64 integers always encode")
509}
510
511fn text(name: &str, value: &str) -> (Value, Value) {
512    (Value::text(name), Value::text(value))
513}
514
515fn bytes(name: &str, value: &[u8]) -> (Value, Value) {
516    (Value::text(name), Value::Bytes(value.to_vec()))
517}
518
519fn int(name: &str, value: i64) -> (Value, Value) {
520    (Value::text(name), Value::Int(i128::from(value)))
521}