1use std::sync::Arc;
10
11use async_trait::async_trait;
12use ed25519_dalek::SigningKey;
13use serde::{Deserialize, Serialize};
14
15use super::request::{CLOCK_FLOOR_SECS, LicenceRequest, MAX_REQUEST_BODY, sign_request};
16use super::{SignedGrant, valid_token};
17
18pub type ClockMs = Arc<dyn Fn() -> u64 + Send + Sync>;
20
21pub fn system_clock_ms() -> ClockMs {
23 Arc::new(|| {
24 std::time::SystemTime::now()
25 .duration_since(std::time::UNIX_EPOCH)
26 .map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
27 .unwrap_or(0)
28 })
29}
30
31pub const CHECKOUT_PATH: &str = "/licence/v1/checkout";
33pub const ARTIFACT_PATH: &str = "/licence/v1/artifact/";
35pub const GRANTS_PATH: &str = "/licence/v1/grants";
37pub const RENEW_PATH: &str = "/licence/v1/renew";
39
40#[derive(Debug, Clone, PartialEq, Eq, Default)]
42pub struct GrantsPage {
43 pub grants: Vec<SignedGrant>,
45 pub next: u64,
47 pub more: bool,
49}
50pub const MAX_RESPONSE_BODY: usize = 256 * 1024;
52
53#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
56#[serde(deny_unknown_fields)]
57pub struct CheckoutWire {
58 pub request_id: String,
60 pub cog_id: String,
62 pub version: String,
64 pub arch: String,
66}
67
68impl CheckoutWire {
69 pub fn validate(&self) -> Result<(), String> {
71 for (what, v) in [
72 ("request_id", &self.request_id),
73 ("cog_id", &self.cog_id),
74 ("version", &self.version),
75 ("arch", &self.arch),
76 ] {
77 if !valid_token(v) {
78 return Err(format!("{what} is not a valid token"));
79 }
80 }
81 Ok(())
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct LicenceResponse {
88 pub status: u16,
90 pub body: Vec<u8>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
96pub enum LicenceClientError {
97 #[error("licence link: {0}")]
99 Transport(String),
100 #[error("licence refused ({status}): {code}")]
103 Refused {
104 status: u16,
106 code: String,
108 },
109 #[error("bad licence response: {0}")]
111 BadResponse(String),
112}
113
114#[async_trait]
116pub trait LicenceTransport: Send + Sync + 'static {
117 async fn call(&self, req: LicenceRequest) -> Result<LicenceResponse, LicenceClientError>;
119}
120
121#[async_trait]
123pub trait LicenceClient: Send + Sync + 'static {
124 async fn checkout(&self, req: &CheckoutWire) -> Result<SignedGrant, LicenceClientError>;
127 async fn artifact(&self, blake3_hex: &str, max_len: u64) -> Result<Vec<u8>, LicenceClientError>;
130 async fn grants_since(&self, since: u64) -> Result<Vec<SignedGrant>, LicenceClientError>;
132 async fn renew(&self) -> Result<GrantsPage, LicenceClientError> {
136 Err(LicenceClientError::BadResponse("renew is not supported by this client".into()))
137 }
138 async fn grants_page(&self, since: u64) -> Result<GrantsPage, LicenceClientError> {
140 let grants = self.grants_since(since).await?;
141 Ok(GrantsPage { grants, next: since, more: false })
142 }
143 async fn release(&self, _cog_id: &str, _version: &str) -> Result<GrantsPage, LicenceClientError> {
147 Err(LicenceClientError::BadResponse("release is not supported by this client".into()))
148 }
149}
150
151#[derive(Serialize)]
153struct ReleaseBody<'a> {
154 release: [ReleaseRef<'a>; 1],
155}
156
157#[derive(Serialize)]
158struct ReleaseRef<'a> {
159 cog_id: &'a str,
160 version: &'a str,
161}
162
163#[derive(Deserialize)]
164struct GrantBody {
165 grant: SignedGrant,
166}
167
168#[derive(Deserialize)]
169struct GrantsBody {
170 grants: Vec<SignedGrant>,
171 #[serde(default)]
172 next: u64,
173 #[serde(default)]
174 more: bool,
175}
176
177#[derive(Deserialize)]
178struct ErrorBody {
179 error: String,
180}
181
182pub struct SignedLicenceClient<T: LicenceTransport> {
184 key: SigningKey,
185 node: String,
186 seed_device_id: String,
188 transport: T,
189 clock: ClockMs,
190}
191
192impl<T: LicenceTransport> SignedLicenceClient<T> {
193 pub fn new(
196 key: SigningKey,
197 node: impl Into<String>,
198 seed_device_id: impl Into<String>,
199 transport: T,
200 clock: ClockMs,
201 ) -> Arc<Self> {
202 Arc::new(Self { key, node: node.into(), seed_device_id: seed_device_id.into(), transport, clock })
203 }
204
205 pub fn for_binding(
208 key: SigningKey,
209 binding: &super::BindingRecord,
210 transport: T,
211 clock: ClockMs,
212 ) -> Arc<Self> {
213 Self::new(key, binding.steward_node_id.clone(), binding.device_id.clone(), transport, clock)
214 }
215
216 async fn signed(
217 &self,
218 method: &str,
219 path: &str,
220 body: Vec<u8>,
221 max_body: usize,
222 ) -> Result<Vec<u8>, LicenceClientError> {
223 if body.len() > MAX_REQUEST_BODY {
224 return Err(LicenceClientError::BadResponse("request body too large".into()));
225 }
226 let ts_ms = (self.clock)();
227 if ts_ms / 1000 < CLOCK_FLOOR_SECS {
229 return Err(LicenceClientError::Refused { status: 0, code: "clock_not_set".into() });
230 }
231 let nonce = request_nonce();
232 let req = sign_request(&self.key, &self.node, &self.seed_device_id, method, path, body, ts_ms, &nonce);
233 let resp = self.transport.call(req).await?;
234 if resp.status != 200 {
235 let code = serde_json::from_slice::<ErrorBody>(&resp.body)
236 .map(|e| e.error)
237 .unwrap_or_else(|_| "unknown_error".into());
238 return Err(LicenceClientError::Refused { status: resp.status, code });
239 }
240 if resp.body.len() > max_body {
241 return Err(LicenceClientError::BadResponse("response body too large".into()));
242 }
243 Ok(resp.body)
244 }
245}
246
247fn bad(e: serde_json::Error) -> LicenceClientError {
248 LicenceClientError::BadResponse(e.to_string())
249}
250
251#[async_trait]
252impl<T: LicenceTransport> LicenceClient for SignedLicenceClient<T> {
253 async fn checkout(&self, req: &CheckoutWire) -> Result<SignedGrant, LicenceClientError> {
254 let body = serde_json::to_vec(req).map_err(bad)?;
255 let out = self.signed("POST", CHECKOUT_PATH, body, MAX_RESPONSE_BODY).await?;
256 Ok(serde_json::from_slice::<GrantBody>(&out).map_err(bad)?.grant)
257 }
258
259 async fn artifact(&self, blake3_hex: &str, max_len: u64) -> Result<Vec<u8>, LicenceClientError> {
260 if !super::valid_hex32(blake3_hex) {
261 return Err(LicenceClientError::BadResponse("artifact id is not a blake3".into()));
262 }
263 let path = format!("{ARTIFACT_PATH}{blake3_hex}");
264 self.signed("GET", &path, Vec::new(), usize::try_from(max_len).unwrap_or(usize::MAX)).await
265 }
266
267 async fn grants_since(&self, since: u64) -> Result<Vec<SignedGrant>, LicenceClientError> {
268 Ok(self.grants_page(since).await?.grants)
269 }
270
271 async fn grants_page(&self, since: u64) -> Result<GrantsPage, LicenceClientError> {
272 let path = format!("{GRANTS_PATH}?since={since}");
273 let out = self.signed("GET", &path, Vec::new(), MAX_RESPONSE_BODY).await?;
274 let b = serde_json::from_slice::<GrantsBody>(&out).map_err(bad)?;
275 Ok(GrantsPage { grants: b.grants, next: b.next, more: b.more })
276 }
277
278 async fn renew(&self) -> Result<GrantsPage, LicenceClientError> {
279 let out = self.signed("POST", RENEW_PATH, Vec::new(), MAX_RESPONSE_BODY).await?;
280 let b = serde_json::from_slice::<GrantsBody>(&out).map_err(bad)?;
281 Ok(GrantsPage { grants: b.grants, next: b.next, more: false })
282 }
283
284 async fn release(&self, cog_id: &str, version: &str) -> Result<GrantsPage, LicenceClientError> {
285 if !valid_token(cog_id) || !valid_token(version) || version == "latest" {
286 return Err(LicenceClientError::BadResponse("release needs a cog id and an exact version".into()));
287 }
288 let body = serde_json::to_vec(&ReleaseBody { release: [ReleaseRef { cog_id, version }] }).map_err(bad)?;
289 let out = self.signed("POST", RENEW_PATH, body, MAX_RESPONSE_BODY).await?;
290 let b = serde_json::from_slice::<GrantsBody>(&out).map_err(bad)?;
291 Ok(GrantsPage { grants: b.grants, next: b.next, more: false })
292 }
293}
294
295#[cfg(test)]
297pub(super) fn request_nonce_for_tests() -> String {
298 request_nonce()
299}
300
301fn request_nonce() -> String {
302 use rand::RngCore;
303 let mut b = [0u8; 16];
304 rand::rngs::OsRng.fill_bytes(&mut b);
305 crate::workload_pkg::codec::hex_encode(&b)
306}