Skip to main content

clawft_kernel/licence/
approval.rs

1//! Operator hash approval (ADR-106 section 4, step 7).
2//!
3//! Additive and content-addressed: keyed by (mesh_id, cog, version, sha256
4//! set), no `seq`. A duplicate is idempotent and a new approval never
5//! replaces an older one. Withdrawal is the existing `ArtifactHash`
6//! revocation of the artifact's BLAKE3.
7
8use ed25519_dalek::SigningKey;
9use serde::{Deserialize, Serialize};
10
11use super::binding::require_operator;
12use super::{
13    APPROVAL_DOMAIN, LicenceError, MeshId, SignedEnvelope, envelope_key, parse_canonical,
14    sha256_hex, sign_envelope, valid_hex32, valid_token, verify_envelope,
15};
16use crate::workload_pkg::TrustAnchors;
17
18/// Most sha256 entries one approval lists.
19pub const MAX_APPROVAL_HASHES: usize = 32;
20
21/// An operator's approval to run these binaries of one cog version.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct Approval {
25    /// Always 1.
26    pub v: u32,
27    /// The mesh, as [`MeshId::to_hex`].
28    pub mesh_id: String,
29    /// Cog id.
30    pub cog_id: String,
31    /// Cog version.
32    pub version: String,
33    /// sha256 of each approved binary: lower-case hex, sorted, no duplicates.
34    pub sha256: Vec<String>,
35    /// When the operator signed it, unix seconds.
36    pub approved_at: u64,
37}
38
39impl Approval {
40    /// The content key (provenance `approval_id`): a hash of mesh, cog,
41    /// version and the sorted sha256 set. `approved_at` is not part of it, so
42    /// re-signing the same content later is a duplicate.
43    pub fn content_key(&self) -> String {
44        let mut s = format!(
45            "weft-licence-v1/approval-key\n{}\n{}\n{}",
46            self.mesh_id, self.cog_id, self.version
47        );
48        for h in &self.sha256 {
49            s.push('\n');
50            s.push_str(h);
51        }
52        sha256_hex(s.as_bytes())
53    }
54
55    /// True when `sha256` is one of the approved binaries.
56    pub fn covers(&self, sha256: &str) -> bool {
57        self.sha256.iter().any(|h| h == sha256)
58    }
59}
60
61/// A signed [`Approval`].
62pub type SignedApproval = SignedEnvelope;
63
64/// Sign `approval` as the operator. The sha256 list is sorted and
65/// de-duplicated first so there is one canonical spelling.
66pub fn sign_approval(
67    approval: &Approval,
68    operator: &SigningKey,
69) -> Result<SignedApproval, LicenceError> {
70    let mut a = approval.clone();
71    a.sha256.sort();
72    a.sha256.dedup();
73    sign_envelope(APPROVAL_DOMAIN, &a, operator)
74}
75
76/// Verify `signed`: pinned operator key, strict signature, canonical payload,
77/// well-formed fields, and `mesh_id` equal to `local` (A2).
78pub fn verify_approval(
79    signed: &SignedApproval,
80    anchors: &TrustAnchors,
81    local: &MeshId,
82) -> Result<Approval, LicenceError> {
83    let a = verify_approval_signature(signed, anchors)?;
84    if a.mesh_id != local.to_hex() {
85        return Err(LicenceError::WrongMesh);
86    }
87    Ok(a)
88}
89
90/// Signature and shape only, with no mesh check (store load, and the CLI's
91/// check of an orphaned approval before it re-signs it for the new mesh).
92pub fn verify_approval_signature(
93    signed: &SignedApproval,
94    anchors: &TrustAnchors,
95) -> Result<Approval, LicenceError> {
96    let pk = envelope_key(signed)?;
97    require_operator(anchors, &pk)?;
98    verify_envelope(APPROVAL_DOMAIN, signed, &pk)?;
99    let a: Approval = parse_canonical(&signed.payload)?;
100    let sorted = a.sha256.windows(2).all(|w| w[0] < w[1]);
101    if a.v != 1
102        || !valid_hex32(&a.mesh_id)
103        || !valid_token(&a.cog_id)
104        || !valid_token(&a.version)
105        || a.sha256.is_empty()
106        || a.sha256.len() > MAX_APPROVAL_HASHES
107        || !sorted
108        || !a.sha256.iter().all(|h| valid_hex32(h))
109    {
110        return Err(LicenceError::Malformed("approval field".into()));
111    }
112    Ok(a)
113}