Skip to main content

qv_core/
issuance.rs

1use chacha20poly1305::{XChaCha20Poly1305, Key, XNonce, KeyInit, AeadInPlace};
2use rand::rngs::OsRng;
3use rand::RngCore;
4
5use crate::claims::Claims;
6use crate::crypto::{QVSigningKey, sha3_256, sign};
7use crate::error::{QVError, QVResult};
8use crate::mutation::{MutationChain, certify_entropy};
9use crate::crypto::SuiteId;
10use crate::token::{QVRawToken, QVTokenHeader, TokenType};
11
12/// Parameters for issuing a new token.
13pub struct IssueParams<'a> {
14    pub suite:        SuiteId,
15    pub token_type:   TokenType,
16    pub ttl_secs:     u32,
17    pub device_fp:    Option<[u8; 32]>,
18    pub claims:       &'a Claims,
19    pub signing_key:  &'a QVSigningKey,
20    pub encrypt_key:  &'a [u8; 32], // XChaCha20-Poly1305 symmetric key
21    pub chain:        &'a mut MutationChain,
22}
23
24/// Issue a new Sigvault token (ML-DSA-87 default path).
25pub fn issue_token(p: IssueParams<'_>) -> QVResult<QVRawToken> {
26    let (shell, msg) = prepare_unsigned(
27        p.suite, p.token_type, p.ttl_secs, p.device_fp,
28        p.claims, p.encrypt_key, p.chain,
29    )?;
30    let signature = sign(p.signing_key, &msg)?;
31    Ok(QVRawToken { header: shell.header, encrypted_payload: shell.encrypted_payload, signature })
32}
33
34/// Shared pre-sign pipeline: timestamp, nonce, entropy, chain advance,
35/// encrypt payload, build header, compute the byte-range the signature
36/// must cover. Suite-specific signing plugs in on top.
37pub fn prepare_unsigned(
38    suite: crate::crypto::SuiteId,
39    token_type: TokenType,
40    ttl_secs: u32,
41    device_fp_opt: Option<[u8; 32]>,
42    claims: &crate::claims::Claims,
43    encrypt_key: &[u8; 32],
44    chain: &mut MutationChain,
45) -> QVResult<(QVRawToken, Vec<u8>)> {
46    let issued_at = std::time::SystemTime::now()
47        .duration_since(std::time::UNIX_EPOCH)
48        .map_err(|e| QVError::SerializationError(e.to_string()))?
49        .as_micros() as u64;
50
51    let mut nonce = [0u8; 32];
52    OsRng.fill_bytes(&mut nonce);
53    certify_entropy(&nonce)?;
54
55    let device_fp = device_fp_opt.unwrap_or_else(|| sha3_256(&nonce));
56
57    let _ = chain.advance();
58    let mutation_ctr = chain.current_counter();
59
60    let plaintext = claims.encode()?;
61    let encrypted_payload = encrypt_payload(&plaintext, encrypt_key, &nonce)?;
62
63    let header = QVTokenHeader {
64        suite, token_type, issued_at, ttl: ttl_secs,
65        nonce, device_fp, mutation_ctr,
66    };
67
68    let shell = QVRawToken { header, encrypted_payload, signature: Vec::new() };
69    let msg = shell.signed_bytes();
70    Ok((shell, msg))
71}
72
73/// Issue a Falcon-512 token (suite 0x10).
74#[cfg(feature = "falcon")]
75pub fn issue_token_falcon512(
76    token_type: TokenType,
77    ttl_secs: u32,
78    device_fp: Option<[u8; 32]>,
79    claims: &crate::claims::Claims,
80    signing_key: &crate::falcon::falcon512::QVFalcon512SigningKey,
81    encrypt_key: &[u8; 32],
82    chain: &mut MutationChain,
83) -> QVResult<QVRawToken> {
84    let (shell, msg) = prepare_unsigned(
85        crate::crypto::SuiteId::Falcon512, token_type, ttl_secs,
86        device_fp, claims, encrypt_key, chain,
87    )?;
88    let signature = crate::falcon::falcon512::sign(signing_key, &msg)?;
89    Ok(QVRawToken { header: shell.header, encrypted_payload: shell.encrypted_payload, signature })
90}
91
92/// Issue a Falcon-1024 token (suite 0x11).
93#[cfg(feature = "falcon")]
94pub fn issue_token_falcon1024(
95    token_type: TokenType,
96    ttl_secs: u32,
97    device_fp: Option<[u8; 32]>,
98    claims: &crate::claims::Claims,
99    signing_key: &crate::falcon::falcon1024::QVFalcon1024SigningKey,
100    encrypt_key: &[u8; 32],
101    chain: &mut MutationChain,
102) -> QVResult<QVRawToken> {
103    let (shell, msg) = prepare_unsigned(
104        crate::crypto::SuiteId::Falcon1024, token_type, ttl_secs,
105        device_fp, claims, encrypt_key, chain,
106    )?;
107    let signature = crate::falcon::falcon1024::sign(signing_key, &msg)?;
108    Ok(QVRawToken { header: shell.header, encrypted_payload: shell.encrypted_payload, signature })
109}
110
111/// XChaCha20-Poly1305 AEAD encrypt.
112/// The 24-byte XChaCha nonce is derived via SHA3-256(token_nonce)[..24].
113fn encrypt_payload(plaintext: &[u8], key: &[u8; 32], token_nonce: &[u8; 32]) -> QVResult<Vec<u8>> {
114    let digest = sha3_256(token_nonce);
115    let xchacha_nonce = XNonce::from_slice(&digest[..24]);
116    let cipher = XChaCha20Poly1305::new(Key::from_slice(key));
117
118    let mut buf = plaintext.to_vec();
119    cipher
120        .encrypt_in_place(xchacha_nonce, b"", &mut buf)
121        .map_err(|_| QVError::DecryptionFailed)?;
122    Ok(buf)
123}
124
125/// XChaCha20-Poly1305 AEAD decrypt (exposed for verify layer).
126pub fn decrypt_payload(ciphertext: &[u8], key: &[u8; 32], token_nonce: &[u8; 32]) -> QVResult<Vec<u8>> {
127    let digest = sha3_256(token_nonce);
128    let xchacha_nonce = XNonce::from_slice(&digest[..24]);
129    let cipher = XChaCha20Poly1305::new(Key::from_slice(key));
130
131    let mut buf = ciphertext.to_vec();
132    cipher
133        .decrypt_in_place(xchacha_nonce, b"", &mut buf)
134        .map_err(|_| QVError::DecryptionFailed)?;
135    Ok(buf)
136}