1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
2use chrono::{DateTime, TimeZone, Utc};
3use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey, SIGNATURE_LENGTH};
4
5pub const TOKEN_PREFIX: &str = "ENT1-";
6const TOKEN_VERSION: u8 = 1;
7const MAX_NAME_BYTES: usize = 1024;
8
9const EMBEDDED_PUBLIC_KEY_HEX: &str =
17 "2ee366ddb411a7181b40c57b2469902be5f36984817adadf56aa0c4fd2dc0589";
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ManualLicense {
21 pub name: String,
22 pub issued_at: DateTime<Utc>,
23}
24
25pub fn looks_like_manual_token(s: &str) -> bool {
26 s.trim().starts_with(TOKEN_PREFIX)
27}
28
29pub fn sign(signing_key: &SigningKey, license: &ManualLicense) -> Result<String, String> {
30 let message = encode_message(license)?;
31 let signature = signing_key.sign(&message);
32 let mut wire = Vec::with_capacity(message.len() + SIGNATURE_LENGTH);
33 wire.extend_from_slice(&message);
34 wire.extend_from_slice(&signature.to_bytes());
35 Ok(format!("{TOKEN_PREFIX}{}", URL_SAFE_NO_PAD.encode(&wire)))
36}
37
38pub fn verify(token: &str) -> Result<ManualLicense, String> {
39 verify_with(token, &embedded_verifying_key()?)
40}
41
42pub fn verify_with(token: &str, verifying_key: &VerifyingKey) -> Result<ManualLicense, String> {
43 let trimmed = token.trim();
44 let payload = trimmed
45 .strip_prefix(TOKEN_PREFIX)
46 .ok_or_else(|| "not an Entracte manual token".to_string())?;
47 let wire = URL_SAFE_NO_PAD
48 .decode(payload)
49 .map_err(|e| format!("manual token is not valid base64: {e}"))?;
50 if wire.len() < SIGNATURE_LENGTH {
51 return Err("manual token is truncated".to_string());
52 }
53 let split = wire.len() - SIGNATURE_LENGTH;
54 let (message, sig_bytes) = wire.split_at(split);
55 let signature_array: [u8; SIGNATURE_LENGTH] = sig_bytes
56 .try_into()
57 .map_err(|_| "manual token signature has wrong length".to_string())?;
58 let signature = Signature::from_bytes(&signature_array);
59 verifying_key
60 .verify(message, &signature)
61 .map_err(|_| "manual token signature does not verify".to_string())?;
62 decode_message(message)
63}
64
65pub(crate) fn encode_message(license: &ManualLicense) -> Result<Vec<u8>, String> {
66 let name_bytes = license.name.as_bytes();
67 if name_bytes.len() > MAX_NAME_BYTES {
68 return Err(format!(
69 "name is {} bytes; manual tokens cap at {MAX_NAME_BYTES}",
70 name_bytes.len()
71 ));
72 }
73 let issued = license.issued_at.timestamp();
74 let mut out = Vec::with_capacity(11 + name_bytes.len());
75 out.push(TOKEN_VERSION);
76 out.extend_from_slice(&issued.to_be_bytes());
77 out.extend_from_slice(&(name_bytes.len() as u16).to_be_bytes());
78 out.extend_from_slice(name_bytes);
79 Ok(out)
80}
81
82fn decode_message(bytes: &[u8]) -> Result<ManualLicense, String> {
83 if bytes.len() < 11 {
84 return Err("manual token payload is truncated".to_string());
85 }
86 let version = bytes[0];
87 if version != TOKEN_VERSION {
88 return Err(format!("unsupported manual token version {version}"));
89 }
90 let mut ts_buf = [0u8; 8];
91 ts_buf.copy_from_slice(&bytes[1..9]);
92 let issued = i64::from_be_bytes(ts_buf);
93 let mut len_buf = [0u8; 2];
94 len_buf.copy_from_slice(&bytes[9..11]);
95 let name_len = u16::from_be_bytes(len_buf) as usize;
96 if bytes.len() != 11 + name_len {
97 return Err("manual token payload length mismatch".to_string());
98 }
99 if name_len > MAX_NAME_BYTES {
100 return Err("manual token name exceeds maximum length".to_string());
101 }
102 let name = std::str::from_utf8(&bytes[11..])
103 .map_err(|_| "manual token name is not valid UTF-8".to_string())?
104 .to_string();
105 let issued_at = Utc
106 .timestamp_opt(issued, 0)
107 .single()
108 .ok_or_else(|| "manual token issued_at is out of range".to_string())?;
109 Ok(ManualLicense { name, issued_at })
110}
111
112fn embedded_verifying_key() -> Result<VerifyingKey, String> {
113 parse_verifying_key_hex(EMBEDDED_PUBLIC_KEY_HEX)
114}
115
116fn parse_verifying_key_hex(hex_str: &str) -> Result<VerifyingKey, String> {
117 let bytes = hex::decode(hex_str)
118 .map_err(|e| format!("embedded manual-license public key is not valid hex: {e}"))?;
119 let array: [u8; 32] = bytes
120 .as_slice()
121 .try_into()
122 .map_err(|_| "embedded manual-license public key must be 32 bytes".to_string())?;
123 if array == [0u8; 32] {
124 return Err(
125 "manual licence verification disabled: placeholder public key not replaced".to_string(),
126 );
127 }
128 VerifyingKey::from_bytes(&array)
129 .map_err(|e| format!("embedded manual-license public key is not a valid Ed25519 key: {e}"))
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 fn fresh_keypair() -> (SigningKey, VerifyingKey) {
137 let mut seed = [0u8; 32];
138 getrandom::getrandom(&mut seed).unwrap();
139 let signing = SigningKey::from_bytes(&seed);
140 let verifying = signing.verifying_key();
141 (signing, verifying)
142 }
143
144 #[test]
145 fn looks_like_manual_token_matches_prefix() {
146 assert!(looks_like_manual_token("ENT1-abc"));
147 assert!(looks_like_manual_token(" ENT1-abc "));
148 assert!(!looks_like_manual_token("XXXX-1111-2222-3333"));
149 assert!(!looks_like_manual_token(""));
150 }
151
152 #[test]
153 fn sign_then_verify_round_trip() {
154 let (sk, vk) = fresh_keypair();
155 let license = ManualLicense {
156 name: "Jane Doe".to_string(),
157 issued_at: Utc.timestamp_opt(1_700_000_000, 0).single().unwrap(),
158 };
159 let token = sign(&sk, &license).unwrap();
160 assert!(token.starts_with(TOKEN_PREFIX));
161 let got = verify_with(&token, &vk).unwrap();
162 assert_eq!(got, license);
163 }
164
165 #[test]
166 fn verify_rejects_token_signed_with_other_key() {
167 let (sk_a, _) = fresh_keypair();
168 let (_, vk_b) = fresh_keypair();
169 let license = ManualLicense {
170 name: "Contributor".to_string(),
171 issued_at: Utc::now(),
172 };
173 let token = sign(&sk_a, &license).unwrap();
174 let err = verify_with(&token, &vk_b).unwrap_err();
175 assert!(err.contains("does not verify"), "got: {err}");
176 }
177
178 #[test]
179 fn verify_rejects_tampered_name() {
180 let (sk, vk) = fresh_keypair();
181 let license = ManualLicense {
182 name: "Alice".to_string(),
183 issued_at: Utc.timestamp_opt(1_700_000_000, 0).single().unwrap(),
184 };
185 let token = sign(&sk, &license).unwrap();
186 let mut wire = URL_SAFE_NO_PAD
188 .decode(token.strip_prefix(TOKEN_PREFIX).unwrap())
189 .unwrap();
190 wire[12] ^= 0x01;
191 let tampered = format!("{TOKEN_PREFIX}{}", URL_SAFE_NO_PAD.encode(&wire));
192 let err = verify_with(&tampered, &vk).unwrap_err();
193 assert!(err.contains("does not verify"), "got: {err}");
194 }
195
196 #[test]
197 fn verify_rejects_wrong_prefix() {
198 let (_, vk) = fresh_keypair();
199 let err = verify_with("LEMON-1234", &vk).unwrap_err();
200 assert!(err.contains("not an Entracte manual token"), "got: {err}");
201 }
202
203 #[test]
204 fn verify_rejects_truncated_token() {
205 let (_, vk) = fresh_keypair();
206 let err = verify_with("ENT1-aGVsbG8", &vk).unwrap_err();
207 assert!(err.contains("truncated"), "got: {err}");
208 }
209
210 #[test]
211 fn parse_verifying_key_rejects_all_zero_placeholder() {
212 let err = parse_verifying_key_hex(
213 "0000000000000000000000000000000000000000000000000000000000000000",
214 )
215 .unwrap_err();
216 assert!(err.contains("placeholder"), "got: {err}");
217 }
218
219 #[test]
220 fn parse_verifying_key_rejects_wrong_length() {
221 let err = parse_verifying_key_hex("deadbeef").unwrap_err();
222 assert!(err.contains("32 bytes"), "got: {err}");
223 }
224
225 #[test]
226 fn encode_message_caps_name_length() {
227 let huge = "a".repeat(MAX_NAME_BYTES + 1);
228 let err = encode_message(&ManualLicense {
229 name: huge,
230 issued_at: Utc::now(),
231 })
232 .unwrap_err();
233 assert!(err.contains("cap"), "got: {err}");
234 }
235
236 fn sign_raw_message(sk: &SigningKey, message: &[u8]) -> String {
237 let signature = sk.sign(message);
238 let mut wire = Vec::with_capacity(message.len() + SIGNATURE_LENGTH);
239 wire.extend_from_slice(message);
240 wire.extend_from_slice(&signature.to_bytes());
241 format!("{TOKEN_PREFIX}{}", URL_SAFE_NO_PAD.encode(&wire))
242 }
243
244 #[test]
245 fn verify_rejects_payload_shorter_than_header() {
246 let (sk, vk) = fresh_keypair();
247 let token = sign_raw_message(&sk, &[0x01, 0, 0, 0, 0]);
248 let err = verify_with(&token, &vk).unwrap_err();
249 assert!(err.contains("truncated"), "got: {err}");
250 }
251
252 #[test]
253 fn verify_rejects_unknown_version_byte() {
254 let (sk, vk) = fresh_keypair();
255 let mut message = vec![0x02_u8];
256 message.extend_from_slice(&0_i64.to_be_bytes());
257 message.extend_from_slice(&0_u16.to_be_bytes());
258 let token = sign_raw_message(&sk, &message);
259 let err = verify_with(&token, &vk).unwrap_err();
260 assert!(err.contains("unsupported"), "got: {err}");
261 }
262
263 #[test]
264 fn verify_rejects_name_length_mismatch() {
265 let (sk, vk) = fresh_keypair();
266 let mut message = vec![TOKEN_VERSION];
267 message.extend_from_slice(&0_i64.to_be_bytes());
268 message.extend_from_slice(&50_u16.to_be_bytes());
269 let token = sign_raw_message(&sk, &message);
270 let err = verify_with(&token, &vk).unwrap_err();
271 assert!(err.contains("length mismatch"), "got: {err}");
272 }
273
274 #[test]
275 fn verify_rejects_name_exceeding_max() {
276 let (sk, vk) = fresh_keypair();
279 let oversized = (MAX_NAME_BYTES + 1) as u16;
280 let mut message = vec![TOKEN_VERSION];
281 message.extend_from_slice(&0_i64.to_be_bytes());
282 message.extend_from_slice(&oversized.to_be_bytes());
283 message.extend(std::iter::repeat_n(b'a', oversized as usize));
284 let token = sign_raw_message(&sk, &message);
285 let err = verify_with(&token, &vk).unwrap_err();
286 assert!(err.contains("maximum length"), "got: {err}");
287 }
288}