use std::sync::Arc;
use async_trait::async_trait;
use ed25519_dalek::SigningKey;
use serde::{Deserialize, Serialize};
use super::request::{CLOCK_FLOOR_SECS, LicenceRequest, MAX_REQUEST_BODY, sign_request};
use super::{SignedGrant, valid_token};
pub type ClockMs = Arc<dyn Fn() -> u64 + Send + Sync>;
pub fn system_clock_ms() -> ClockMs {
Arc::new(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
.unwrap_or(0)
})
}
pub const CHECKOUT_PATH: &str = "/licence/v1/checkout";
pub const ARTIFACT_PATH: &str = "/licence/v1/artifact/";
pub const GRANTS_PATH: &str = "/licence/v1/grants";
pub const RENEW_PATH: &str = "/licence/v1/renew";
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GrantsPage {
pub grants: Vec<SignedGrant>,
pub next: u64,
pub more: bool,
}
pub const MAX_RESPONSE_BODY: usize = 256 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CheckoutWire {
pub request_id: String,
pub cog_id: String,
pub version: String,
pub arch: String,
}
impl CheckoutWire {
pub fn validate(&self) -> Result<(), String> {
for (what, v) in [
("request_id", &self.request_id),
("cog_id", &self.cog_id),
("version", &self.version),
("arch", &self.arch),
] {
if !valid_token(v) {
return Err(format!("{what} is not a valid token"));
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LicenceResponse {
pub status: u16,
pub body: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum LicenceClientError {
#[error("licence link: {0}")]
Transport(String),
#[error("licence refused ({status}): {code}")]
Refused {
status: u16,
code: String,
},
#[error("bad licence response: {0}")]
BadResponse(String),
}
#[async_trait]
pub trait LicenceTransport: Send + Sync + 'static {
async fn call(&self, req: LicenceRequest) -> Result<LicenceResponse, LicenceClientError>;
}
#[async_trait]
pub trait LicenceClient: Send + Sync + 'static {
async fn checkout(&self, req: &CheckoutWire) -> Result<SignedGrant, LicenceClientError>;
async fn artifact(&self, blake3_hex: &str, max_len: u64) -> Result<Vec<u8>, LicenceClientError>;
async fn grants_since(&self, since: u64) -> Result<Vec<SignedGrant>, LicenceClientError>;
async fn renew(&self) -> Result<GrantsPage, LicenceClientError> {
Err(LicenceClientError::BadResponse("renew is not supported by this client".into()))
}
async fn grants_page(&self, since: u64) -> Result<GrantsPage, LicenceClientError> {
let grants = self.grants_since(since).await?;
Ok(GrantsPage { grants, next: since, more: false })
}
async fn release(&self, _cog_id: &str, _version: &str) -> Result<GrantsPage, LicenceClientError> {
Err(LicenceClientError::BadResponse("release is not supported by this client".into()))
}
}
#[derive(Serialize)]
struct ReleaseBody<'a> {
release: [ReleaseRef<'a>; 1],
}
#[derive(Serialize)]
struct ReleaseRef<'a> {
cog_id: &'a str,
version: &'a str,
}
#[derive(Deserialize)]
struct GrantBody {
grant: SignedGrant,
}
#[derive(Deserialize)]
struct GrantsBody {
grants: Vec<SignedGrant>,
#[serde(default)]
next: u64,
#[serde(default)]
more: bool,
}
#[derive(Deserialize)]
struct ErrorBody {
error: String,
}
pub struct SignedLicenceClient<T: LicenceTransport> {
key: SigningKey,
node: String,
seed_device_id: String,
transport: T,
clock: ClockMs,
}
impl<T: LicenceTransport> SignedLicenceClient<T> {
pub fn new(
key: SigningKey,
node: impl Into<String>,
seed_device_id: impl Into<String>,
transport: T,
clock: ClockMs,
) -> Arc<Self> {
Arc::new(Self { key, node: node.into(), seed_device_id: seed_device_id.into(), transport, clock })
}
pub fn for_binding(
key: SigningKey,
binding: &super::BindingRecord,
transport: T,
clock: ClockMs,
) -> Arc<Self> {
Self::new(key, binding.steward_node_id.clone(), binding.device_id.clone(), transport, clock)
}
async fn signed(
&self,
method: &str,
path: &str,
body: Vec<u8>,
max_body: usize,
) -> Result<Vec<u8>, LicenceClientError> {
if body.len() > MAX_REQUEST_BODY {
return Err(LicenceClientError::BadResponse("request body too large".into()));
}
let ts_ms = (self.clock)();
if ts_ms / 1000 < CLOCK_FLOOR_SECS {
return Err(LicenceClientError::Refused { status: 0, code: "clock_not_set".into() });
}
let nonce = request_nonce();
let req = sign_request(&self.key, &self.node, &self.seed_device_id, method, path, body, ts_ms, &nonce);
let resp = self.transport.call(req).await?;
if resp.status != 200 {
let code = serde_json::from_slice::<ErrorBody>(&resp.body)
.map(|e| e.error)
.unwrap_or_else(|_| "unknown_error".into());
return Err(LicenceClientError::Refused { status: resp.status, code });
}
if resp.body.len() > max_body {
return Err(LicenceClientError::BadResponse("response body too large".into()));
}
Ok(resp.body)
}
}
fn bad(e: serde_json::Error) -> LicenceClientError {
LicenceClientError::BadResponse(e.to_string())
}
#[async_trait]
impl<T: LicenceTransport> LicenceClient for SignedLicenceClient<T> {
async fn checkout(&self, req: &CheckoutWire) -> Result<SignedGrant, LicenceClientError> {
let body = serde_json::to_vec(req).map_err(bad)?;
let out = self.signed("POST", CHECKOUT_PATH, body, MAX_RESPONSE_BODY).await?;
Ok(serde_json::from_slice::<GrantBody>(&out).map_err(bad)?.grant)
}
async fn artifact(&self, blake3_hex: &str, max_len: u64) -> Result<Vec<u8>, LicenceClientError> {
if !super::valid_hex32(blake3_hex) {
return Err(LicenceClientError::BadResponse("artifact id is not a blake3".into()));
}
let path = format!("{ARTIFACT_PATH}{blake3_hex}");
self.signed("GET", &path, Vec::new(), usize::try_from(max_len).unwrap_or(usize::MAX)).await
}
async fn grants_since(&self, since: u64) -> Result<Vec<SignedGrant>, LicenceClientError> {
Ok(self.grants_page(since).await?.grants)
}
async fn grants_page(&self, since: u64) -> Result<GrantsPage, LicenceClientError> {
let path = format!("{GRANTS_PATH}?since={since}");
let out = self.signed("GET", &path, Vec::new(), MAX_RESPONSE_BODY).await?;
let b = serde_json::from_slice::<GrantsBody>(&out).map_err(bad)?;
Ok(GrantsPage { grants: b.grants, next: b.next, more: b.more })
}
async fn renew(&self) -> Result<GrantsPage, LicenceClientError> {
let out = self.signed("POST", RENEW_PATH, Vec::new(), MAX_RESPONSE_BODY).await?;
let b = serde_json::from_slice::<GrantsBody>(&out).map_err(bad)?;
Ok(GrantsPage { grants: b.grants, next: b.next, more: false })
}
async fn release(&self, cog_id: &str, version: &str) -> Result<GrantsPage, LicenceClientError> {
if !valid_token(cog_id) || !valid_token(version) || version == "latest" {
return Err(LicenceClientError::BadResponse("release needs a cog id and an exact version".into()));
}
let body = serde_json::to_vec(&ReleaseBody { release: [ReleaseRef { cog_id, version }] }).map_err(bad)?;
let out = self.signed("POST", RENEW_PATH, body, MAX_RESPONSE_BODY).await?;
let b = serde_json::from_slice::<GrantsBody>(&out).map_err(bad)?;
Ok(GrantsPage { grants: b.grants, next: b.next, more: false })
}
}
#[cfg(test)]
pub(super) fn request_nonce_for_tests() -> String {
request_nonce()
}
fn request_nonce() -> String {
use rand::RngCore;
let mut b = [0u8; 16];
rand::rngs::OsRng.fill_bytes(&mut b);
crate::workload_pkg::codec::hex_encode(&b)
}