use std::collections::BTreeSet;
use ed25519_dalek::SigningKey;
use serde::{Deserialize, Serialize};
use crate::{
GRANT_DOMAIN, LicenceError, MAX_ARTIFACT_BYTES, MAX_GRANT_TTL_SECS, MAX_UNIX_TIME, MeshId,
SignedEnvelope, envelope_key, key_id,
parse_canonical, sha256_hex, sign_envelope, valid_hex32, valid_token, verify_envelope,
};
pub const MAX_GRANT_ARTIFACTS: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GrantArtifact {
pub arch: String,
pub size: u64,
pub sha256: String,
pub blake3: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LicenceRef {
pub ref_sha256: String,
pub expires: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CheckoutGrant {
pub v: u32,
pub grant_id: String,
pub mesh_id: String,
pub seed_device_id: String,
pub grant_key_id: String,
pub source: String,
pub registry: String,
pub cog_id: String,
pub version: String,
pub artifacts: Vec<GrantArtifact>,
pub manifest_sha256: String,
pub licence: LicenceRef,
pub seq: u64,
pub issued_at: u64,
pub expires_at: u64,
}
impl CheckoutGrant {
pub fn compute_id(&self) -> String {
let mut g = self.clone();
g.grant_id = String::new();
sha256_hex(serde_json::to_string(&g).unwrap_or_default().as_bytes())
}
pub fn is_withdrawal(&self) -> bool {
self.expires_at <= self.issued_at
}
pub fn artifact(&self, arch: &str) -> Option<&GrantArtifact> {
self.artifacts.iter().find(|a| a.arch == arch)
}
pub fn artifact_by_blake3(&self, blake3: &str) -> Option<&GrantArtifact> {
self.artifacts.iter().find(|a| a.blake3 == blake3)
}
pub fn arches(&self) -> BTreeSet<&str> {
self.artifacts.iter().map(|a| a.arch.as_str()).collect()
}
}
pub type SignedGrant = SignedEnvelope;
pub fn sign_grant(grant: &CheckoutGrant, key: &SigningKey) -> Result<SignedGrant, LicenceError> {
let mut g = grant.clone();
g.artifacts.sort_by(|a, b| a.arch.cmp(&b.arch));
g.grant_key_id = key_id(&key.verifying_key().to_bytes());
g.grant_id = g.compute_id();
sign_envelope(GRANT_DOMAIN, &g, key)
}
pub fn verify_grant(
signed: &SignedGrant,
grant_pubkey: &[u8; 32],
local: &MeshId,
) -> Result<CheckoutGrant, LicenceError> {
let g = verify_grant_signature(signed, grant_pubkey)?;
if g.mesh_id != local.to_hex() {
return Err(LicenceError::WrongMesh);
}
Ok(g)
}
pub fn verify_grant_signature(
signed: &SignedGrant,
grant_pubkey: &[u8; 32],
) -> Result<CheckoutGrant, LicenceError> {
if envelope_key(signed)? != *grant_pubkey {
return Err(LicenceError::UntrustedKey);
}
verify_envelope(GRANT_DOMAIN, signed, grant_pubkey)?;
let g: CheckoutGrant = parse_canonical(&signed.payload)?;
check_shape(&g, grant_pubkey)?;
Ok(g)
}
fn check_shape(g: &CheckoutGrant, pk: &[u8; 32]) -> Result<(), LicenceError> {
let bad = |what: &str| Err(LicenceError::Malformed(format!("grant {what}")));
if g.v != 1 || g.source != "cognitum" {
return bad("version or source");
}
if g.grant_key_id != key_id(pk) {
return bad("key id");
}
if g.grant_id != g.compute_id() {
return bad("id");
}
let tokens = [&g.seed_device_id, &g.cog_id, &g.version];
if !tokens.iter().all(|s| valid_token(s)) || g.registry.is_empty() || g.registry.len() > 256 {
return bad("token");
}
let hexes = [&g.mesh_id, &g.manifest_sha256, &g.licence.ref_sha256];
if !hexes.iter().all(|s| valid_hex32(s)) {
return bad("hash");
}
if g.artifacts.is_empty() || g.artifacts.len() > MAX_GRANT_ARTIFACTS {
return bad("artifact count");
}
let sorted_unique = g.artifacts.windows(2).all(|w| w[0].arch < w[1].arch);
let each_ok = g
.artifacts
.iter()
.all(|a| valid_token(&a.arch) && valid_hex32(&a.sha256) && valid_hex32(&a.blake3));
if !sorted_unique || !each_ok {
return bad("artifacts");
}
let dup = |f: fn(&GrantArtifact) -> &String| {
let set: BTreeSet<&String> = g.artifacts.iter().map(f).collect();
set.len() != g.artifacts.len()
};
if dup(|a| &a.sha256) || dup(|a| &a.blake3) {
return bad("duplicate artifact hash");
}
if g.artifacts.iter().any(|a| a.size == 0 || a.size > MAX_ARTIFACT_BYTES) {
return bad("artifact size");
}
let printable = g.registry.bytes().all(|c| (0x20..=0x7e).contains(&c));
if !printable {
return bad("registry");
}
let times = [g.licence.expires, g.issued_at, g.expires_at];
if g.licence.expires == 0 || times.iter().any(|t| *t > MAX_UNIX_TIME) {
return bad("time");
}
if g.expires_at.saturating_sub(g.issued_at) > MAX_GRANT_TTL_SECS {
return Err(LicenceError::TtlTooLong);
}
Ok(())
}