1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
9use serde::{Deserialize, Serialize};
10
11use crate::muragent::dsse::keyid_from_pubkey;
12
13pub const OFFICIAL_LICENSE_FORMAT: u32 = 1;
15
16pub const DISTRIBUTION_OFFICIAL: &str = "official";
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct OfficialLicense {
22 pub format_version: u32,
23 pub user_id: String,
25 pub item: String,
27 pub version: String,
29 pub expires_at: String,
31 pub signer_pubkey: String,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub sig: Option<String>,
36}
37
38pub fn license_sign_input(l: &OfficialLicense) -> Vec<u8> {
40 let mut unsigned = l.clone();
41 unsigned.sig = None;
42 serde_json::to_vec(&unsigned).expect("license serializes")
43}
44
45pub fn sign_license(l: &mut OfficialLicense, key: &SigningKey) {
47 use base64::{Engine, engine::general_purpose::STANDARD as B64};
48 l.signer_pubkey = B64.encode(key.verifying_key().as_bytes());
49 l.sig = None;
50 let sig: Signature = key.sign(&license_sign_input(l));
51 l.sig = Some(B64.encode(sig.to_bytes()));
52}
53
54pub fn verify_license_sig(l: &OfficialLicense) -> bool {
56 use base64::{Engine, engine::general_purpose::STANDARD as B64};
57 let Some(sig_b64) = &l.sig else { return false };
58 let Ok(pk_bytes) = B64.decode(&l.signer_pubkey) else {
59 return false;
60 };
61 let Ok(pk_arr) = <[u8; 32]>::try_from(pk_bytes.as_slice()) else {
62 return false;
63 };
64 let Ok(vk) = VerifyingKey::from_bytes(&pk_arr) else {
65 return false;
66 };
67 let Ok(sig_bytes) = B64.decode(sig_b64) else {
68 return false;
69 };
70 let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else {
71 return false;
72 };
73 vk.verify(&license_sign_input(l), &Signature::from_bytes(&sig_arr))
74 .is_ok()
75}
76
77pub fn license_key_fp(l: &OfficialLicense) -> Option<String> {
79 use base64::{Engine, engine::general_purpose::STANDARD as B64};
80 let pk = B64.decode(&l.signer_pubkey).ok()?;
81 let arr = <[u8; 32]>::try_from(pk.as_slice()).ok()?;
82 Some(keyid_from_pubkey(&arr))
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum LicenseCheck {
89 Ok,
90 BadSignature,
91 NotOfficialKey,
92 WrongUser,
93 WrongItem,
94}
95
96pub fn check_license(
100 l: &OfficialLicense,
101 expected_item: &str,
102 user_id: &str,
103 official_fp: &str,
104) -> LicenseCheck {
105 if !verify_license_sig(l) {
106 return LicenseCheck::BadSignature;
107 }
108 match license_key_fp(l) {
109 Some(fp) if fp == official_fp => {}
110 _ => return LicenseCheck::NotOfficialKey,
111 }
112 if l.item != expected_item {
113 return LicenseCheck::WrongItem;
114 }
115 if l.user_id != user_id {
116 return LicenseCheck::WrongUser;
117 }
118 LicenseCheck::Ok
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use ed25519_dalek::SigningKey;
125
126 fn test_license(key: &SigningKey) -> OfficialLicense {
127 let mut l = OfficialLicense {
128 format_version: OFFICIAL_LICENSE_FORMAT,
129 user_id: "user-123".into(),
130 item: "fleets/deep-research".into(),
131 version: "1.0.0".into(),
132 expires_at: "2027-01-01T00:00:00Z".into(),
133 signer_pubkey: String::new(),
134 sig: None,
135 };
136 sign_license(&mut l, key);
137 l
138 }
139
140 #[test]
141 fn sign_verify_roundtrip() {
142 let key = SigningKey::from_bytes(&[7u8; 32]);
143 let l = test_license(&key);
144 assert!(verify_license_sig(&l));
145 }
146
147 #[test]
148 fn tampered_field_fails_verify() {
149 let key = SigningKey::from_bytes(&[7u8; 32]);
150 let mut l = test_license(&key);
151 l.user_id = "someone-else".into();
152 assert!(!verify_license_sig(&l));
153 }
154
155 #[test]
156 fn unsigned_fails_verify() {
157 let key = SigningKey::from_bytes(&[7u8; 32]);
158 let mut l = test_license(&key);
159 l.sig = None;
160 assert!(!verify_license_sig(&l));
161 }
162
163 #[test]
164 fn check_license_matrix() {
165 let key = SigningKey::from_bytes(&[7u8; 32]);
166 let l = test_license(&key);
167 let fp = license_key_fp(&l).unwrap();
168 assert_eq!(
169 check_license(&l, "fleets/deep-research", "user-123", &fp),
170 LicenseCheck::Ok
171 );
172 assert_eq!(
173 check_license(&l, "fleets/deep-research", "user-999", &fp),
174 LicenseCheck::WrongUser
175 );
176 assert_eq!(
177 check_license(&l, "fleets/other", "user-123", &fp),
178 LicenseCheck::WrongItem
179 );
180 assert_eq!(
181 check_license(&l, "fleets/deep-research", "user-123", "ed25519-ffffffff"),
182 LicenseCheck::NotOfficialKey
183 );
184 let mut bad = l.clone();
185 bad.sig = Some("mtampered".into());
186 assert_eq!(
187 check_license(&bad, "fleets/deep-research", "user-123", &fp),
188 LicenseCheck::BadSignature
189 );
190 }
191}