Skip to main content

keylight/
verifier.rs

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
15/// Decode standard or url-safe base64, tolerating missing padding.
16fn 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
28/// Verify a lease against a trusted `kid -> raw ed25519 public key (base64)` map.
29pub 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    // A real round-trip is covered by the conformance gate (tests/conformance.rs).
64    #[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(); // kid unknown, but expired is computed regardless
99        assert!(!verify_lease(&lease, &keys, 1000 + 200, SKEW_SECONDS).expired);
100        assert!(verify_lease(&lease, &keys, 1000 + 400, SKEW_SECONDS).expired);
101    }
102}