Skip to main content

kipuka_dogtag/
kem.rs

1//! ML-KEM encapsulation and AES Key Wrap for CRMF key archival.
2
3use crate::{DogtagError, DogtagResult};
4use zeroize::Zeroizing;
5
6/// Result of an ML-KEM encapsulation.
7pub struct KemEncapsulation {
8    /// KEM ciphertext — sent to the KRA alongside the wrapped key.
9    pub ciphertext: Vec<u8>,
10    /// Shared secret — used as the AES-KWP key to wrap the private key.
11    /// Wrapped in Zeroizing to clear from memory on drop.
12    pub shared_secret: Zeroizing<Vec<u8>>,
13}
14
15/// ML-KEM encapsulate against a transport cert's SPKI DER.
16///
17/// Calls `EVP_PKEY_encapsulate` via FFI (the `openssl` crate 0.10.x
18/// doesn't expose KEM operations yet). Returns the KEM ciphertext and
19/// the derived shared secret.
20pub fn ml_kem_encapsulate(transport_pub_key_der: &[u8]) -> DogtagResult<KemEncapsulation> {
21    unsafe {
22        let mut ptr = transport_pub_key_der.as_ptr();
23        let evp_pkey = openssl_sys::d2i_PUBKEY(
24            std::ptr::null_mut(),
25            &mut ptr,
26            transport_pub_key_der.len() as std::os::raw::c_long,
27        );
28        if evp_pkey.is_null() {
29            return Err(DogtagError::KraError(
30                "d2i_PUBKEY failed for transport key".into(),
31            ));
32        }
33
34        let ctx = openssl_sys::EVP_PKEY_CTX_new(evp_pkey, std::ptr::null_mut());
35        if ctx.is_null() {
36            openssl_sys::EVP_PKEY_free(evp_pkey);
37            return Err(DogtagError::KraError("EVP_PKEY_CTX_new failed".into()));
38        }
39
40        let rc = openssl_sys::EVP_PKEY_encapsulate_init(ctx, std::ptr::null());
41        if rc <= 0 {
42            openssl_sys::EVP_PKEY_CTX_free(ctx);
43            openssl_sys::EVP_PKEY_free(evp_pkey);
44            return Err(DogtagError::KraError(
45                "EVP_PKEY_encapsulate_init failed".into(),
46            ));
47        }
48
49        // Query output sizes
50        let mut ct_len: usize = 0;
51        let mut ss_len: usize = 0;
52        let rc = openssl_sys::EVP_PKEY_encapsulate(
53            ctx,
54            std::ptr::null_mut(),
55            &mut ct_len,
56            std::ptr::null_mut(),
57            &mut ss_len,
58        );
59        if rc <= 0 {
60            openssl_sys::EVP_PKEY_CTX_free(ctx);
61            openssl_sys::EVP_PKEY_free(evp_pkey);
62            return Err(DogtagError::KraError(
63                "EVP_PKEY_encapsulate size query failed".into(),
64            ));
65        }
66
67        let mut ciphertext = vec![0u8; ct_len];
68        let mut shared_secret = vec![0u8; ss_len];
69
70        let rc = openssl_sys::EVP_PKEY_encapsulate(
71            ctx,
72            ciphertext.as_mut_ptr(),
73            &mut ct_len,
74            shared_secret.as_mut_ptr(),
75            &mut ss_len,
76        );
77        openssl_sys::EVP_PKEY_CTX_free(ctx);
78        openssl_sys::EVP_PKEY_free(evp_pkey);
79
80        if rc <= 0 {
81            return Err(DogtagError::KraError("EVP_PKEY_encapsulate failed".into()));
82        }
83
84        ciphertext.truncate(ct_len);
85        shared_secret.truncate(ss_len);
86
87        tracing::info!(
88            ct_len = ciphertext.len(),
89            ss_len = shared_secret.len(),
90            "ML-KEM encapsulation complete"
91        );
92
93        Ok(KemEncapsulation {
94            ciphertext,
95            shared_secret: Zeroizing::new(shared_secret),
96        })
97    }
98}
99
100/// AES Key Wrap with Padding (RFC 5649) — wraps `plaintext` under `kek`.
101pub fn aes_kwp_wrap(kek: &[u8], plaintext: &[u8]) -> DogtagResult<Vec<u8>> {
102    unsafe {
103        let cipher_name = match kek.len() {
104            16 => c"AES-128-WRAP-PAD",
105            24 => c"AES-192-WRAP-PAD",
106            32 => c"AES-256-WRAP-PAD",
107            _ => {
108                return Err(DogtagError::KraError(format!(
109                    "Invalid KEK length {} (expected 16, 24, or 32)",
110                    kek.len()
111                )));
112            }
113        };
114
115        let cipher = openssl_sys::EVP_CIPHER_fetch(
116            std::ptr::null_mut(),
117            cipher_name.as_ptr(),
118            std::ptr::null(),
119        );
120        if cipher.is_null() {
121            return Err(DogtagError::KraError("AES-KWP cipher not available".into()));
122        }
123
124        let ctx = openssl_sys::EVP_CIPHER_CTX_new();
125        if ctx.is_null() {
126            openssl_sys::EVP_CIPHER_free(cipher as *mut _);
127            return Err(DogtagError::KraError("EVP_CIPHER_CTX_new failed".into()));
128        }
129
130        openssl_sys::EVP_CIPHER_CTX_set_flags(ctx, openssl_sys::EVP_CIPHER_CTX_FLAG_WRAP_ALLOW);
131
132        let rc = openssl_sys::EVP_EncryptInit_ex(
133            ctx,
134            cipher,
135            std::ptr::null_mut(),
136            kek.as_ptr(),
137            std::ptr::null(),
138        );
139        // cipher ref now held by ctx; free our fetch ref
140        openssl_sys::EVP_CIPHER_free(cipher as *mut _);
141        if rc != 1 {
142            openssl_sys::EVP_CIPHER_CTX_free(ctx);
143            return Err(DogtagError::KraError("EVP_EncryptInit_ex failed".into()));
144        }
145
146        let max_out = plaintext.len() + 32;
147        let mut output = vec![0u8; max_out];
148        let mut out_len: i32 = 0;
149
150        let rc = openssl_sys::EVP_EncryptUpdate(
151            ctx,
152            output.as_mut_ptr(),
153            &mut out_len,
154            plaintext.as_ptr(),
155            plaintext.len() as i32,
156        );
157        if rc != 1 {
158            openssl_sys::EVP_CIPHER_CTX_free(ctx);
159            return Err(DogtagError::KraError("EVP_EncryptUpdate failed".into()));
160        }
161
162        let mut final_len: i32 = 0;
163        let rc = openssl_sys::EVP_EncryptFinal_ex(
164            ctx,
165            output.as_mut_ptr().add(out_len as usize),
166            &mut final_len,
167        );
168        openssl_sys::EVP_CIPHER_CTX_free(ctx);
169
170        if rc != 1 {
171            return Err(DogtagError::KraError("EVP_EncryptFinal_ex failed".into()));
172        }
173
174        let total = (out_len + final_len) as usize;
175        output.truncate(total);
176
177        tracing::debug!(
178            kek_len = kek.len(),
179            plaintext_len = plaintext.len(),
180            wrapped_len = total,
181            "AES-KWP wrap complete"
182        );
183
184        Ok(output)
185    }
186}