Coverage Report

Created: 2026-05-18 06:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/git/checkouts/nss-rs-71e20fe79ef91440/9b94ca3/src/hp.rs
Line
Count
Source
1
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4
// option. This file may not be copied, modified, or distributed
5
// except according to those terms.
6
7
use std::{
8
    convert::TryFrom as _,
9
    fmt::{self, Debug},
10
    os::raw::{c_char, c_int, c_uint},
11
    ptr::{null, null_mut},
12
};
13
14
use pkcs11_bindings::{CKA_ENCRYPT, CKM_AES_ECB, CKM_CHACHA20};
15
16
use crate::{
17
    SECItemBorrowed,
18
    constants::{
19
        Cipher, TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256,
20
        Version,
21
    },
22
    err::{Error, Res, secstatus_to_res},
23
    p11::{
24
        CK_ATTRIBUTE_TYPE, CK_CHACHA20_PARAMS, CK_MECHANISM_TYPE, Context, PK11_CipherOp,
25
        PK11_CreateContextBySymKey, PK11_Encrypt, PK11_GetBlockSize, PK11SymKey, SymKey,
26
    },
27
};
28
29
experimental_api!(SSL_HkdfExpandLabelWithMech(
30
    version: Version,
31
    cipher: Cipher,
32
    prk: *mut PK11SymKey,
33
    handshake_hash: *const u8,
34
    handshake_hash_len: c_uint,
35
    label: *const c_char,
36
    label_len: c_uint,
37
    mech: CK_MECHANISM_TYPE,
38
    key_size: c_uint,
39
    secret: *mut *mut PK11SymKey,
40
));
41
42
/// Creates an AES-ECB `PK11Context` from a `SymKey`.
43
10.6k
fn make_aes_ctx(key: &SymKey) -> Res<Context> {
44
10.6k
    Context::from_ptr(unsafe {
45
10.6k
        PK11_CreateContextBySymKey(
46
10.6k
            CK_MECHANISM_TYPE::from(CKM_AES_ECB),
47
10.6k
            CK_ATTRIBUTE_TYPE::from(CKA_ENCRYPT),
48
10.6k
            **key,
49
10.6k
            SECItemBorrowed::make_empty().as_ref(),
50
        )
51
    })
52
10.6k
    .map_err(|_| Error::CipherInit)
53
10.6k
}
54
55
pub enum Key {
56
    /// AES-ECB header-protection context.  `PK11_CloneContext` is not supported for
57
    /// AES-ECB, so the `SymKey` is stored alongside `ctx` to enable duplication via
58
    /// `try_clone`.
59
    #[non_exhaustive]
60
    Aes { ctx: Context, key: SymKey },
61
    /// The `ChaCha20` mask invokes `PK11_Encrypt` on each call because the counter
62
    /// and nonce change per invocation.
63
    #[non_exhaustive]
64
    Chacha(SymKey),
65
}
66
67
impl Debug for Key {
68
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
69
0
        write!(f, "hp::Key")
70
0
    }
71
}
72
73
impl Key {
74
    pub const SAMPLE_SIZE: usize = 16;
75
76
    /// QUIC-specific API for extracting a header-protection key.
77
    ///
78
    /// # Errors
79
    ///
80
    /// Errors if HKDF fails or if the label is too long to fit in a `c_uint`.
81
    ///
82
    /// # Panics
83
    ///
84
    /// When `cipher` is not known to this code.
85
10.6k
    pub fn extract(version: Version, cipher: Cipher, prk: &SymKey, label: &str) -> Res<Self> {
86
10.6k
        let l = label.as_bytes();
87
10.6k
        let mut secret: *mut PK11SymKey = null_mut();
88
89
10.6k
        let (mech, key_size) = match cipher {
90
10.6k
            TLS_AES_128_GCM_SHA256 => (CK_MECHANISM_TYPE::from(CKM_AES_ECB), 16),
91
0
            TLS_AES_256_GCM_SHA384 => (CK_MECHANISM_TYPE::from(CKM_AES_ECB), 32),
92
0
            TLS_CHACHA20_POLY1305_SHA256 => (CK_MECHANISM_TYPE::from(CKM_CHACHA20), 32),
93
0
            _ => unreachable!(),
94
        };
95
96
        // Note that this doesn't allow for passing null() for the handshake hash.
97
        // A zero-length slice produces an identical result.
98
        unsafe {
99
10.6k
            SSL_HkdfExpandLabelWithMech(
100
10.6k
                version,
101
10.6k
                cipher,
102
10.6k
                **prk,
103
10.6k
                null(),
104
                0,
105
10.6k
                l.as_ptr().cast(),
106
10.6k
                c_uint::try_from(l.len())?,
107
10.6k
                mech,
108
10.6k
                key_size,
109
10.6k
                &raw mut secret,
110
            )
111
0
        }?;
112
10.6k
        let key = SymKey::from_ptr(secret).or(Err(Error::Hkdf))?;
113
114
10.6k
        let res = match cipher {
115
            TLS_AES_128_GCM_SHA256 | TLS_AES_256_GCM_SHA384 => {
116
10.6k
                let ctx = make_aes_ctx(&key)?;
117
10.6k
                Self::Aes { ctx, key }
118
            }
119
0
            TLS_CHACHA20_POLY1305_SHA256 => Self::Chacha(key),
120
0
            _ => unreachable!(),
121
        };
122
123
10.6k
        debug_assert_eq!(
124
10.6k
            res.block_size(),
125
10.6k
            usize::try_from(unsafe { PK11_GetBlockSize(mech, null_mut()) })?
126
        );
127
10.6k
        Ok(res)
128
10.6k
    }
129
130
10.6k
    const fn block_size(&self) -> usize {
131
10.6k
        match self {
132
10.6k
            Self::Aes { .. } => 16,
133
0
            Self::Chacha(_) => 64,
134
        }
135
10.6k
    }
136
137
    /// Duplicate this key, creating a new independent instance.
138
    ///
139
    /// # Errors
140
    ///
141
    /// Errors if NSS context creation fails for AES keys.
142
0
    pub fn try_clone(&self) -> Res<Self> {
143
0
        match self {
144
0
            Self::Aes { key, .. } => {
145
0
                let key = key.clone();
146
0
                let ctx = make_aes_ctx(&key)?;
147
0
                Ok(Self::Aes { ctx, key })
148
            }
149
0
            Self::Chacha(k) => Ok(Self::Chacha(k.clone())),
150
        }
151
0
    }
152
153
    /// Generate a header protection mask for QUIC.
154
    ///
155
    /// # Errors
156
    ///
157
    /// An error is returned if the NSS functions fail.
158
    ///
159
    /// # Panics
160
    ///
161
    /// In debug builds, if NSS returns an unexpected output length.
162
4.70k
    pub fn mask(&self, sample: &[u8; Self::SAMPLE_SIZE]) -> Res<[u8; Self::SAMPLE_SIZE]> {
163
4.70k
        let mut output = [0; Self::SAMPLE_SIZE];
164
165
4.70k
        match self {
166
4.70k
            Self::Aes { ctx, .. } => {
167
4.70k
                let mut output_len: c_int = 0;
168
                // SAFETY: `Deref` on `Context` copies the raw `*mut PK11Context` pointer
169
                // value; no Rust reference to the pointee is created.  `Key` contains raw
170
                // pointers (`!Sync`), so concurrent invocations are impossible, and
171
                // AES-ECB full-block operations retain no inter-call state in the context.
172
4.70k
                secstatus_to_res(unsafe {
173
4.70k
                    PK11_CipherOp(
174
4.70k
                        **ctx,
175
4.70k
                        output.as_mut_ptr(),
176
4.70k
                        &raw mut output_len,
177
4.70k
                        c_int::try_from(output.len())?,
178
4.70k
                        sample.as_ptr().cast(),
179
4.70k
                        c_int::try_from(Self::SAMPLE_SIZE)?,
180
                    )
181
0
                })?;
182
4.70k
                debug_assert_eq!(usize::try_from(output_len)?, output.len());
183
4.70k
                Ok(output)
184
            }
185
186
0
            Self::Chacha(key) => {
187
0
                let params: CK_CHACHA20_PARAMS = CK_CHACHA20_PARAMS {
188
0
                    pBlockCounter: sample.as_ptr().cast_mut(),
189
0
                    blockCounterBits: 32,
190
0
                    pNonce: sample[4..].as_ptr().cast_mut(),
191
0
                    ulNonceBits: 96,
192
0
                };
193
0
                let mut output_len: c_uint = 0;
194
0
                let mut param_item = SECItemBorrowed::wrap_struct(&params)?;
195
0
                secstatus_to_res(unsafe {
196
0
                    PK11_Encrypt(
197
0
                        **key,
198
0
                        CK_MECHANISM_TYPE::from(CKM_CHACHA20),
199
0
                        std::ptr::from_mut(param_item.as_mut()),
200
0
                        output[..].as_mut_ptr(),
201
0
                        &raw mut output_len,
202
0
                        c_uint::try_from(output.len())?,
203
0
                        [0; Self::SAMPLE_SIZE].as_ptr(),
204
0
                        c_uint::try_from(Self::SAMPLE_SIZE)?,
205
                    )
206
0
                })?;
207
0
                debug_assert_eq!(usize::try_from(output_len)?, output.len());
208
0
                Ok(output)
209
            }
210
        }
211
4.70k
    }
212
}
213
214
#[cfg(test)]
215
#[cfg_attr(coverage_nightly, coverage(off))]
216
mod tests {
217
    use crate::{
218
        constants::{TLS_AES_128_GCM_SHA256, TLS_VERSION_1_3},
219
        hkdf,
220
        hp::Key,
221
    };
222
223
    #[test]
224
    fn debug_format() {
225
        test_fixture::fixture_init();
226
        let prk = hkdf::import_key(TLS_VERSION_1_3, &[0; 32]).unwrap();
227
        let key = Key::extract(TLS_VERSION_1_3, TLS_AES_128_GCM_SHA256, &prk, "test").unwrap();
228
        assert_eq!(format!("{key:?}"), "hp::Key");
229
    }
230
}