Skip to main content

clawft_kernel/licence/
binding.rs

1//! Binding record v2 (ADR-106 section 3): one Seed, one mesh.
2
3pub use weft_licence_wire::{BindState, BindingRecord, SignedBinding, sign_binding};
4
5use super::{LicenceError, MeshId};
6use crate::workload_pkg::codec::hex_decode_exact;
7use crate::workload_pkg::{KeyOrigin, TrustAnchors};
8
9/// A key is acceptable only when pinned as an operator key.
10pub(crate) fn require_operator(anchors: &TrustAnchors, pk: &[u8; 32]) -> Result<(), LicenceError> {
11    match anchors.signer(pk) {
12        Some(k) if k.origin == KeyOrigin::Operator => Ok(()),
13        _ => Err(LicenceError::UntrustedKey),
14    }
15}
16
17/// The MEMBER verification profile: pinned operator signature, strict
18/// signature check, canonical payload, `mesh_id` equal to `local`. Members
19/// have no Seed link, so there is no age check and no identity match. The
20/// stateful rules (`seq` above the stored one, equal-`seq` conflicts) are the
21/// store's ([`super::CheckoutGrantStore::accept_binding`]).
22pub fn verify_binding_member(
23    signed: &SignedBinding,
24    anchors: &TrustAnchors,
25    local: &MeshId,
26) -> Result<BindingRecord, LicenceError> {
27    let rec = verify_binding_signature(signed, anchors)?;
28    if rec.mesh_id != local.to_hex() {
29        return Err(LicenceError::WrongMesh);
30    }
31    Ok(rec)
32}
33
34/// Signature and shape only, with no mesh check (store load re-verification).
35pub(crate) fn verify_binding_signature(
36    signed: &SignedBinding,
37    anchors: &TrustAnchors,
38) -> Result<BindingRecord, LicenceError> {
39    let rec = weft_licence_wire::verify_binding_signature(signed, &|pk| {
40        require_operator(anchors, pk).is_ok()
41    })?;
42    // The grant and steward keys must not be a pinned trust-anchor key, or a
43    // binding could hand an operator or release key the role of a Seed key.
44    for hex in [&rec.grant_pubkey, &rec.steward_pubkey] {
45        let pk = hex_decode_exact::<32>(hex).unwrap_or_default();
46        let anchored = anchors.signers.iter().chain(&anchors.cognitum).any(|k| k.public_key == pk);
47        if anchored {
48            return Err(LicenceError::Malformed("binding key is a trust anchor".into()));
49        }
50    }
51    Ok(rec)
52}
53
54/// Where the STEWARD profile (phase 1d) plugs in: the 600 s age window, the
55/// live `GET /api/v1/identity` match, the grant key fingerprint the operator
56/// confirmed. Runs after the member checks and before the record is stored.
57pub trait BindingExtraCheck {
58    /// Refuse the record with [`LicenceError::CheckFailed`].
59    fn check(&self, record: &BindingRecord) -> Result<(), LicenceError>;
60}
61
62/// The member profile adds nothing.
63#[derive(Debug, Default, Clone, Copy)]
64pub struct NoExtraChecks;
65
66impl BindingExtraCheck for NoExtraChecks {
67    fn check(&self, _: &BindingRecord) -> Result<(), LicenceError> {
68        Ok(())
69    }
70}
71
72/// What the node's mesh admission looks like when a binding arrives.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct AdmissionPosture {
75    /// Admission mode is `enforce` (not `off` or `observe`).
76    pub enforce: bool,
77    /// A governance verdict source is bound.
78    pub verdict_source_bound: bool,
79    /// `admission_open_membership` is on.
80    pub open_membership: bool,
81}
82
83impl AdmissionPosture {
84    /// A binding is accepted only under enforce, with a verdict source and
85    /// open membership off.
86    pub fn check(&self) -> Result<(), LicenceError> {
87        if self.open_membership {
88            Err(LicenceError::BindingRefused("open_membership"))
89        } else if !self.enforce {
90            Err(LicenceError::BindingRefused("admission_not_enforce"))
91        } else if !self.verdict_source_bound {
92            Err(LicenceError::BindingRefused("no_verdict_source"))
93        } else {
94            Ok(())
95        }
96    }
97}