Skip to main content

weft_licence_wire/
binding.rs

1//! Binding record v2 (ADR-106 section 3): one Seed, one mesh.
2
3use ed25519_dalek::SigningKey;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    BINDING_DOMAIN, LicenceError, MAX_UNIX_TIME, SignedEnvelope, envelope_key, parse_canonical, sign_envelope,
8    valid_hex32, valid_token, verify_envelope,
9};
10
11/// Whether a binding binds or unbinds.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum BindState {
15    /// The Seed is bound to the mesh.
16    Bound,
17    /// The binding is withdrawn; every grant under its key stops.
18    Unbound,
19}
20
21/// The operator-signed Seed to mesh binding.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct BindingRecord {
25    /// Always 2.
26    pub v: u32,
27    /// The Seed's device id.
28    pub device_id: String,
29    /// The Seed's device public key, 64 hex chars.
30    pub device_pubkey: String,
31    /// The mesh, as `MeshId::to_hex`.
32    pub mesh_id: String,
33    /// The grant key `weft-licence` signs grants with, 64 hex chars.
34    pub grant_pubkey: String,
35    /// The steward node's id.
36    pub steward_node_id: String,
37    /// The steward's request-signing key, 64 hex chars.
38    pub steward_pubkey: String,
39    /// Bound or unbound.
40    pub state: BindState,
41    /// Strictly increasing per mesh; a higher `seq` replaces a lower one.
42    pub seq: u64,
43    /// When the operator signed it, unix seconds.
44    pub bound_at: u64,
45}
46
47/// A signed [`BindingRecord`].
48pub type SignedBinding = SignedEnvelope;
49
50/// Sign `record` as the operator.
51pub fn sign_binding(
52    record: &BindingRecord,
53    operator: &SigningKey,
54) -> Result<SignedBinding, LicenceError> {
55    sign_envelope(BINDING_DOMAIN, record, operator)
56}
57
58/// Signature and shape only, with no mesh check. `operator_ok` says whether
59/// a signer key is a pinned operator key; the caller owns that policy.
60pub fn verify_binding_signature(
61    signed: &SignedBinding,
62    operator_ok: &dyn Fn(&[u8; 32]) -> bool,
63) -> Result<BindingRecord, LicenceError> {
64    let pk = envelope_key(signed)?;
65    if !operator_ok(&pk) {
66        return Err(LicenceError::UntrustedKey);
67    }
68    verify_envelope(BINDING_DOMAIN, signed, &pk)?;
69    let rec: BindingRecord = parse_canonical(&signed.payload)?;
70    if rec.v != 2 {
71        return Err(LicenceError::Malformed("binding version".into()));
72    }
73    let hex_ok = [&rec.device_pubkey, &rec.mesh_id, &rec.grant_pubkey, &rec.steward_pubkey]
74        .iter()
75        .all(|s| valid_hex32(s));
76    if !hex_ok || !valid_token(&rec.device_id) || !valid_token(&rec.steward_node_id) {
77        return Err(LicenceError::Malformed("binding field".into()));
78    }
79    if rec.bound_at > MAX_UNIX_TIME {
80        return Err(LicenceError::Malformed("binding bounds".into()));
81    }
82    Ok(rec)
83}