1use crate::lease::Lease;
2use base64::Engine;
3use ed25519_dalek::{Signature, Verifier as _, VerifyingKey};
4use std::collections::HashMap;
5
6pub const SKEW_SECONDS: i64 = 300;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct VerifyResult {
10 pub kid_known: bool,
11 pub signature_valid: bool,
12 pub expired: bool,
13}
14
15fn b64_decode(s: &str) -> Option<Vec<u8>> {
17 let norm: String = s.chars().filter(|c| !c.is_whitespace()).collect();
18 let norm = norm.replace('-', "+").replace('_', "/");
19 let padded = match norm.len() % 4 {
20 0 => norm,
21 n => format!("{norm}{}", "=".repeat(4 - n)),
22 };
23 base64::engine::general_purpose::STANDARD
24 .decode(padded.as_bytes())
25 .ok()
26}
27
28pub fn verify_lease(
30 lease: &Lease,
31 trusted_keys: &HashMap<String, String>,
32 now_seconds: i64,
33 skew_seconds: i64,
34) -> VerifyResult {
35 let expired = now_seconds > lease.expires_at + skew_seconds;
36 let pub_b64 = match trusted_keys.get(&lease.kid) {
37 Some(p) => p,
38 None => {
39 return VerifyResult {
40 kid_known: false,
41 signature_valid: false,
42 expired,
43 }
44 }
45 };
46 let signature_valid = (|| -> Option<bool> {
47 let pk_bytes: [u8; 32] = b64_decode(pub_b64)?.try_into().ok()?;
48 let vk = VerifyingKey::from_bytes(&pk_bytes).ok()?;
49 let sig = Signature::from_slice(&b64_decode(&lease.signature)?).ok()?;
50 Some(vk.verify(lease.payload().as_bytes(), &sig).is_ok())
51 })()
52 .unwrap_or(false);
53 VerifyResult {
54 kid_known: true,
55 signature_valid,
56 expired,
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 #[test]
65 fn unknown_kid_short_circuits() {
66 let lease = Lease {
67 kid: "k9".into(),
68 license_key_hash: "a".into(),
69 instance_id: "i".into(),
70 issued_at: 0,
71 expires_at: 100,
72 status: "active".into(),
73 signature: "x".into(),
74 entitlements: vec![],
75 };
76 let r = verify_lease(&lease, &HashMap::new(), 50, SKEW_SECONDS);
77 assert_eq!(
78 r,
79 VerifyResult {
80 kid_known: false,
81 signature_valid: false,
82 expired: false
83 }
84 );
85 }
86 #[test]
87 fn expiry_uses_skew() {
88 let lease = Lease {
89 kid: "k1".into(),
90 license_key_hash: "a".into(),
91 instance_id: "i".into(),
92 issued_at: 0,
93 expires_at: 1000,
94 status: "active".into(),
95 signature: "x".into(),
96 entitlements: vec![],
97 };
98 let keys = HashMap::new(); assert!(!verify_lease(&lease, &keys, 1000 + 200, SKEW_SECONDS).expired);
100 assert!(verify_lease(&lease, &keys, 1000 + 400, SKEW_SECONDS).expired);
101 }
102}