Skip to main content

qv_core/
lib.rs

1pub mod claims;
2pub mod crypto;
3pub mod error;
4#[cfg(feature = "falcon")]
5pub mod falcon;
6pub mod issuance;
7pub mod mutation;
8pub mod token;
9pub mod verify;
10
11pub use claims::Claims;
12pub use crypto::{QVSigningKey, QVVerifyingKey, SuiteId, generate_keypair};
13pub use error::{QVError, QVResult};
14pub use issuance::{IssueParams, issue_token};
15#[cfg(feature = "falcon")]
16pub use issuance::{issue_token_falcon512, issue_token_falcon1024};
17pub use mutation::MutationChain;
18pub use token::{QVRawToken, QVTokenHeader, TokenType, VERSION, MAGIC};
19pub use verify::{verify_token, VerifyOutput};
20#[cfg(feature = "falcon")]
21pub use verify::{verify_token_falcon512, verify_token_falcon1024};
22
23#[cfg(test)]
24mod tests {
25    use super::*;
26
27    fn test_encrypt_key() -> [u8; 32] {
28        let mut k = [0u8; 32];
29        for (i, b) in k.iter_mut().enumerate() { *b = i as u8; }
30        k
31    }
32
33    #[test]
34    fn roundtrip_issue_verify() {
35        let (sk, vk) = generate_keypair().expect("keygen");
36        let ek = test_encrypt_key();
37        let mut chain = MutationChain::new([0xAB; 32]);
38
39        let mut claims = Claims::new();
40        claims.insert("sub", "user-123");
41        claims.insert("role", "admin");
42
43        let params = IssueParams {
44            suite: SuiteId::Dilithium5,
45            token_type: TokenType::Access,
46            ttl_secs: 3600,
47            device_fp: None,
48            claims: &claims,
49            signing_key: &sk,
50            encrypt_key: &ek,
51            chain: &mut chain,
52        };
53
54        let raw = issue_token(params).expect("issue");
55        let bytes = raw.to_bytes();
56        let parsed = QVRawToken::from_bytes(&bytes).expect("parse");
57
58        let verify_chain = MutationChain::from_state([0xAB; 32], 0);
59        let out = verify_token(&parsed, &vk, &ek, &verify_chain).expect("verify");
60
61        assert_eq!(out.claims.get("sub"), Some("user-123"));
62        assert_eq!(out.claims.get("role"), Some("admin"));
63    }
64
65    #[test]
66    fn expired_token_rejected() {
67        let (sk, vk) = generate_keypair().expect("keygen");
68        let ek = test_encrypt_key();
69        let mut chain = MutationChain::new([0x11; 32]);
70
71        let mut claims = Claims::new();
72        claims.insert("sub", "test");
73
74        let params = IssueParams {
75            suite: SuiteId::Dilithium5,
76            token_type: TokenType::Access,
77            ttl_secs: 0, // already expired
78            device_fp: None,
79            claims: &claims,
80            signing_key: &sk,
81            encrypt_key: &ek,
82            chain: &mut chain,
83        };
84
85        let raw = issue_token(params).expect("issue");
86        let bytes = raw.to_bytes();
87        let parsed = QVRawToken::from_bytes(&bytes).expect("parse");
88        let verify_chain = MutationChain::from_state([0x11; 32], 0);
89
90        // Allow up to 1s clock skew before calling expired.
91        std::thread::sleep(std::time::Duration::from_millis(1100));
92        let result = verify_token(&parsed, &vk, &ek, &verify_chain);
93        assert!(matches!(result, Err(QVError::Expired { .. })));
94    }
95
96    #[test]
97    fn tampered_signature_rejected() {
98        let (sk, vk) = generate_keypair().expect("keygen");
99        let ek = test_encrypt_key();
100        let mut chain = MutationChain::new([0x22; 32]);
101
102        let mut claims = Claims::new();
103        claims.insert("sub", "attacker");
104
105        let params = IssueParams {
106            suite: SuiteId::Dilithium5,
107            token_type: TokenType::Access,
108            ttl_secs: 3600,
109            device_fp: None,
110            claims: &claims,
111            signing_key: &sk,
112            encrypt_key: &ek,
113            chain: &mut chain,
114        };
115
116        let mut raw = issue_token(params).expect("issue");
117        // Flip a byte in the signature.
118        raw.signature[100] ^= 0xFF;
119
120        let verify_chain = MutationChain::from_state([0x22; 32], 0);
121        let result = verify_token(&raw, &vk, &ek, &verify_chain);
122        assert!(matches!(result, Err(QVError::SignatureInvalid)));
123    }
124
125    #[test]
126    fn replay_rejected() {
127        let (sk, vk) = generate_keypair().expect("keygen");
128        let ek = test_encrypt_key();
129        let mut chain = MutationChain::new([0x33; 32]);
130
131        let mut claims = Claims::new();
132        claims.insert("sub", "user");
133
134        let params = IssueParams {
135            suite: SuiteId::Dilithium5,
136            token_type: TokenType::Access,
137            ttl_secs: 3600,
138            device_fp: None,
139            claims: &claims,
140            signing_key: &sk,
141            encrypt_key: &ek,
142            chain: &mut chain,
143        };
144
145        let raw = issue_token(params).expect("issue");
146
147        // Verify chain already at counter 1 — token counter 1 should fail.
148        let advanced_chain = MutationChain::from_state([0x33; 32], 1);
149        let result = verify_token(&raw, &vk, &ek, &advanced_chain);
150        assert!(matches!(result, Err(QVError::ReplayDetected { .. })));
151    }
152
153    #[cfg(feature = "falcon")]
154    #[test]
155    fn falcon512_token_roundtrip() {
156        use crate::falcon::falcon512;
157
158        let (sk, vk) = falcon512::generate_keypair().expect("falcon keygen");
159        let ek = test_encrypt_key();
160        let mut chain = MutationChain::new([0x44; 32]);
161
162        let mut claims = Claims::new();
163        claims.insert("sub", "falcon-512-user");
164        claims.insert("role", "pilot");
165
166        let raw = issue_token_falcon512(
167            TokenType::Access, 3600, None, &claims, &sk, &ek, &mut chain,
168        ).expect("issue falcon-512");
169
170        // Falcon signatures are variable-length and dwarf-smaller than ML-DSA-87.
171        assert!(raw.signature.len() <= 666, "falcon-512 sig {} > 666", raw.signature.len());
172        assert!(raw.signature.len() < 4627 / 5, "expected 5x smaller than ML-DSA-87");
173        assert_eq!(raw.header.suite, SuiteId::Falcon512);
174
175        // Wire format survives round-trip.
176        let bytes  = raw.to_bytes();
177        let parsed = QVRawToken::from_bytes(&bytes).expect("parse");
178        assert_eq!(parsed.header.suite, SuiteId::Falcon512);
179
180        let verify_chain = MutationChain::from_state([0x44; 32], 0);
181        let out = verify_token_falcon512(&parsed, &vk, &ek, &verify_chain).expect("verify");
182        assert_eq!(out.claims.get("sub"),  Some("falcon-512-user"));
183        assert_eq!(out.claims.get("role"), Some("pilot"));
184    }
185
186    #[cfg(feature = "falcon")]
187    #[test]
188    fn falcon1024_token_roundtrip() {
189        use crate::falcon::falcon1024;
190
191        let (sk, vk) = falcon1024::generate_keypair().expect("falcon keygen");
192        let ek = test_encrypt_key();
193        let mut chain = MutationChain::new([0x55; 32]);
194
195        let mut claims = Claims::new();
196        claims.insert("sub", "falcon-1024-user");
197
198        let raw = issue_token_falcon1024(
199            TokenType::Service, 60, None, &claims, &sk, &ek, &mut chain,
200        ).expect("issue falcon-1024");
201
202        assert!(raw.signature.len() <= 1280);
203        assert_eq!(raw.header.suite, SuiteId::Falcon1024);
204
205        let bytes  = raw.to_bytes();
206        let parsed = QVRawToken::from_bytes(&bytes).expect("parse");
207
208        let verify_chain = MutationChain::from_state([0x55; 32], 0);
209        let out = verify_token_falcon1024(&parsed, &vk, &ek, &verify_chain).expect("verify");
210        assert_eq!(out.claims.get("sub"), Some("falcon-1024-user"));
211    }
212
213    #[cfg(feature = "falcon")]
214    #[test]
215    fn falcon_suite_mismatch_rejected() {
216        use crate::falcon::falcon512;
217
218        let (sk, _vk) = falcon512::generate_keypair().expect("keygen");
219        let (_sk1024, vk1024) = crate::falcon::falcon1024::generate_keypair().expect("keygen");
220        let ek = test_encrypt_key();
221        let mut chain = MutationChain::new([0x66; 32]);
222
223        let mut claims = Claims::new();
224        claims.insert("sub", "x");
225
226        let raw = issue_token_falcon512(
227            TokenType::Access, 3600, None, &claims, &sk, &ek, &mut chain,
228        ).expect("issue");
229
230        let verify_chain = MutationChain::from_state([0x66; 32], 0);
231        // Using the 1024 verifier on a 512 token must fail on the suite check.
232        let result = verify_token_falcon1024(&raw, &vk1024, &ek, &verify_chain);
233        assert!(matches!(result, Err(QVError::UnknownSuite(0x10))));
234    }
235
236    #[test]
237    fn claims_encode_decode_roundtrip() {
238        let mut c = Claims::new();
239        c.insert("iss", "qv.example.com");
240        c.insert("sub", "user-456");
241        c.insert("scope", "read:all");
242
243        let encoded = c.encode().expect("encode");
244        let decoded = Claims::decode(&encoded).expect("decode");
245
246        assert_eq!(decoded.get("iss"), Some("qv.example.com"));
247        assert_eq!(decoded.get("sub"), Some("user-456"));
248        assert_eq!(decoded.get("scope"), Some("read:all"));
249    }
250}