weft_licence_wire/
grant.rs1use std::collections::BTreeSet;
5
6use ed25519_dalek::SigningKey;
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 GRANT_DOMAIN, LicenceError, MAX_ARTIFACT_BYTES, MAX_GRANT_TTL_SECS, MAX_UNIX_TIME, MeshId,
11 SignedEnvelope, envelope_key, key_id,
12 parse_canonical, sha256_hex, sign_envelope, valid_hex32, valid_token, verify_envelope,
13};
14
15pub const MAX_GRANT_ARTIFACTS: usize = 8;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct GrantArtifact {
22 pub arch: String,
24 pub size: u64,
26 pub sha256: String,
28 pub blake3: String,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct LicenceRef {
36 pub ref_sha256: String,
38 pub expires: u64,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct CheckoutGrant {
46 pub v: u32,
48 pub grant_id: String,
50 pub mesh_id: String,
52 pub seed_device_id: String,
54 pub grant_key_id: String,
56 pub source: String,
58 pub registry: String,
60 pub cog_id: String,
62 pub version: String,
64 pub artifacts: Vec<GrantArtifact>,
66 pub manifest_sha256: String,
68 pub licence: LicenceRef,
70 pub seq: u64,
72 pub issued_at: u64,
74 pub expires_at: u64,
76}
77
78impl CheckoutGrant {
79 pub fn compute_id(&self) -> String {
81 let mut g = self.clone();
82 g.grant_id = String::new();
83 sha256_hex(serde_json::to_string(&g).unwrap_or_default().as_bytes())
84 }
85
86 pub fn is_withdrawal(&self) -> bool {
88 self.expires_at <= self.issued_at
89 }
90
91 pub fn artifact(&self, arch: &str) -> Option<&GrantArtifact> {
93 self.artifacts.iter().find(|a| a.arch == arch)
94 }
95
96 pub fn artifact_by_blake3(&self, blake3: &str) -> Option<&GrantArtifact> {
98 self.artifacts.iter().find(|a| a.blake3 == blake3)
99 }
100
101 pub fn arches(&self) -> BTreeSet<&str> {
103 self.artifacts.iter().map(|a| a.arch.as_str()).collect()
104 }
105}
106
107pub type SignedGrant = SignedEnvelope;
109
110pub fn sign_grant(grant: &CheckoutGrant, key: &SigningKey) -> Result<SignedGrant, LicenceError> {
113 let mut g = grant.clone();
114 g.artifacts.sort_by(|a, b| a.arch.cmp(&b.arch));
115 g.grant_key_id = key_id(&key.verifying_key().to_bytes());
116 g.grant_id = g.compute_id();
117 sign_envelope(GRANT_DOMAIN, &g, key)
118}
119
120pub fn verify_grant(
123 signed: &SignedGrant,
124 grant_pubkey: &[u8; 32],
125 local: &MeshId,
126) -> Result<CheckoutGrant, LicenceError> {
127 let g = verify_grant_signature(signed, grant_pubkey)?;
128 if g.mesh_id != local.to_hex() {
129 return Err(LicenceError::WrongMesh);
130 }
131 Ok(g)
132}
133
134pub fn verify_grant_signature(
136 signed: &SignedGrant,
137 grant_pubkey: &[u8; 32],
138) -> Result<CheckoutGrant, LicenceError> {
139 if envelope_key(signed)? != *grant_pubkey {
140 return Err(LicenceError::UntrustedKey);
141 }
142 verify_envelope(GRANT_DOMAIN, signed, grant_pubkey)?;
143 let g: CheckoutGrant = parse_canonical(&signed.payload)?;
144 check_shape(&g, grant_pubkey)?;
145 Ok(g)
146}
147
148fn check_shape(g: &CheckoutGrant, pk: &[u8; 32]) -> Result<(), LicenceError> {
149 let bad = |what: &str| Err(LicenceError::Malformed(format!("grant {what}")));
150 if g.v != 1 || g.source != "cognitum" {
151 return bad("version or source");
152 }
153 if g.grant_key_id != key_id(pk) {
154 return bad("key id");
155 }
156 if g.grant_id != g.compute_id() {
157 return bad("id");
158 }
159 let tokens = [&g.seed_device_id, &g.cog_id, &g.version];
160 if !tokens.iter().all(|s| valid_token(s)) || g.registry.is_empty() || g.registry.len() > 256 {
161 return bad("token");
162 }
163 let hexes = [&g.mesh_id, &g.manifest_sha256, &g.licence.ref_sha256];
164 if !hexes.iter().all(|s| valid_hex32(s)) {
165 return bad("hash");
166 }
167 if g.artifacts.is_empty() || g.artifacts.len() > MAX_GRANT_ARTIFACTS {
168 return bad("artifact count");
169 }
170 let sorted_unique = g.artifacts.windows(2).all(|w| w[0].arch < w[1].arch);
171 let each_ok = g
172 .artifacts
173 .iter()
174 .all(|a| valid_token(&a.arch) && valid_hex32(&a.sha256) && valid_hex32(&a.blake3));
175 if !sorted_unique || !each_ok {
176 return bad("artifacts");
177 }
178 let dup = |f: fn(&GrantArtifact) -> &String| {
179 let set: BTreeSet<&String> = g.artifacts.iter().map(f).collect();
180 set.len() != g.artifacts.len()
181 };
182 if dup(|a| &a.sha256) || dup(|a| &a.blake3) {
183 return bad("duplicate artifact hash");
184 }
185 if g.artifacts.iter().any(|a| a.size == 0 || a.size > MAX_ARTIFACT_BYTES) {
186 return bad("artifact size");
187 }
188 let printable = g.registry.bytes().all(|c| (0x20..=0x7e).contains(&c));
189 if !printable {
190 return bad("registry");
191 }
192 let times = [g.licence.expires, g.issued_at, g.expires_at];
193 if g.licence.expires == 0 || times.iter().any(|t| *t > MAX_UNIX_TIME) {
194 return bad("time");
195 }
196 if g.expires_at.saturating_sub(g.issued_at) > MAX_GRANT_TTL_SECS {
197 return Err(LicenceError::TtlTooLong);
198 }
199 Ok(())
200}