Skip to main content

ntex_tls/x509/
digest.rs

1//! Message digests used for certificate thumbprints.
2//!
3//! Windows uses the native CNG hash providers (`BCrypt`). Elsewhere rustls uses standalone `RustCrypto` hash implementations
4//! (independent of its TLS crypto provider); OpenSSL-only builds use OpenSSL.
5//!
6//! | Target  | Backend                 |
7//! |---------|-------------------------|
8//! | rustls, non-Windows | `sha1` / `sha2` |
9//! | OpenSSL only, non-Windows | OpenSSL (`tls_openssl::sha`) |
10//! | windows | CNG (`BCryptHashData`) |
11//!
12//! SHA-1 is provided only because the public API accepts 40 hex character
13//! thumbprints (`allowedThumbprints`) which are SHA-1 digests of the DER
14//! certificate. The public [`sha1()`] function is meant for identification only.
15//! Supporting SHA-1 thumbprints does not enable SHA-1 certificate signatures; their
16//! acceptance follows the trust backend's policy (see [`crate::x509::trust`]).
17
18/// Size of a SHA-1 digest in bytes.
19pub const SHA1_LEN: usize = 20;
20/// Size of a SHA-256 digest in bytes.
21pub const SHA256_LEN: usize = 32;
22
23/// Computes the SHA-1 digest of `data`.
24///
25/// Identification only (40 hex character thumbprints), see the module docs.
26#[must_use]
27pub fn sha1(data: &[u8]) -> [u8; SHA1_LEN] {
28    #[cfg(all(not(windows), not(feature = "rustls"), feature = "openssl"))]
29    {
30        tls_openssl::sha::sha1(data)
31    }
32    #[cfg(all(not(windows), feature = "rustls"))]
33    {
34        use sha1::Digest;
35        sha1::Sha1::digest(data).into()
36    }
37    #[cfg(windows)]
38    {
39        cng::hash(cng::BCRYPT_SHA1_ALG_HANDLE, data)
40    }
41}
42
43/// Computes the SHA-256 digest of `data`.
44#[must_use]
45pub fn sha256(data: &[u8]) -> [u8; SHA256_LEN] {
46    #[cfg(all(not(windows), not(feature = "rustls"), feature = "openssl"))]
47    {
48        tls_openssl::sha::sha256(data)
49    }
50    #[cfg(all(not(windows), feature = "rustls"))]
51    {
52        use sha2::Digest;
53        sha2::Sha256::digest(data).into()
54    }
55    #[cfg(windows)]
56    {
57        cng::hash(cng::BCRYPT_SHA256_ALG_HANDLE, data)
58    }
59}
60
61#[cfg(windows)]
62pub(crate) mod cng {
63    use std::ptr;
64
65    pub(crate) use windows_sys::Win32::Security::Cryptography::{
66        BCRYPT_ALG_HANDLE, BCRYPT_SHA1_ALG_HANDLE, BCRYPT_SHA256_ALG_HANDLE,
67        BCRYPT_SHA384_ALG_HANDLE, BCRYPT_SHA512_ALG_HANDLE,
68    };
69    use windows_sys::Win32::Security::Cryptography::{
70        BCRYPT_HASH_HANDLE, BCryptCreateHash, BCryptDestroyHash, BCryptFinishHash, BCryptHashData,
71    };
72
73    /// Destroys the hash object on every exit path.
74    struct Hash(BCRYPT_HASH_HANDLE);
75
76    impl Drop for Hash {
77        fn drop(&mut self) {
78            // SAFETY: the handle was created by `BCryptCreateHash` and is destroyed once.
79            unsafe { BCryptDestroyHash(self.0) };
80        }
81    }
82
83    fn check(status: i32, op: &str) {
84        assert!(status >= 0, "{op} failed: NTSTATUS {status:#010x}");
85    }
86
87    /// Hashes `data` with a CNG algorithm pseudo-handle producing an `N`-byte digest.
88    ///
89    /// # Panics
90    /// If CNG fails, which does not happen for the built-in SHA providers.
91    pub(crate) fn hash<const N: usize>(alg: BCRYPT_ALG_HANDLE, data: &[u8]) -> [u8; N] {
92        let mut handle = ptr::null_mut();
93        // SAFETY: pseudo-handles are always valid; CNG allocates the hash object itself
94        // because no object buffer is supplied.
95        check(
96            unsafe {
97                BCryptCreateHash(alg, &raw mut handle, ptr::null_mut(), 0, ptr::null(), 0, 0)
98            },
99            "BCryptCreateHash",
100        );
101        let hash = Hash(handle);
102        for chunk in data.chunks(u32::MAX as usize) {
103            // SAFETY: the hash handle is live and `chunk` is valid for its length,
104            // which fits in `u32`.
105            check(
106                unsafe {
107                    BCryptHashData(
108                        hash.0,
109                        chunk.as_ptr(),
110                        u32::try_from(chunk.len()).unwrap_or(u32::MAX),
111                        0,
112                    )
113                },
114                "BCryptHashData",
115            );
116        }
117        let mut out = [0; N];
118        let len = u32::try_from(N).expect("digest size fits in u32");
119        // SAFETY: the hash handle is live and `out` is valid for `N` bytes, the digest size.
120        check(
121            unsafe { BCryptFinishHash(hash.0, out.as_mut_ptr(), len, 0) },
122            "BCryptFinishHash",
123        );
124        out
125    }
126}
127
128/// Digest algorithm used for certificate thumbprints.
129#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
130pub enum Algorithm {
131    /// SHA-1, 20 bytes / 40 hex characters. Identification only.
132    Sha1,
133    /// SHA-256, 32 bytes / 64 hex characters.
134    Sha256,
135}
136
137impl Algorithm {
138    /// Selects the algorithm for a hex encoded thumbprint.
139    ///
140    /// A thumbprint of exactly 40 characters is SHA-1, anything else is SHA-256
141    /// (matches the behavior of the public `allowedThumbprints` API).
142    #[must_use]
143    pub const fn for_hex_thumbprint(thumbprint: &str) -> Self {
144        if thumbprint.len() == SHA1_LEN * 2 {
145            Algorithm::Sha1
146        } else {
147            Algorithm::Sha256
148        }
149    }
150
151    /// Digest size in bytes.
152    #[must_use]
153    pub const fn digest_len(self) -> usize {
154        match self {
155            Algorithm::Sha1 => SHA1_LEN,
156            Algorithm::Sha256 => SHA256_LEN,
157        }
158    }
159
160    /// Computes the digest of `data`.
161    #[must_use]
162    pub fn digest(self, data: &[u8]) -> Vec<u8> {
163        match self {
164            Algorithm::Sha1 => sha1(data).to_vec(),
165            Algorithm::Sha256 => sha256(data).to_vec(),
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    /// STATEMENT: Every selected thumbprint backend implements standard SHA-1 and SHA-256.
175    ///
176    /// HOW:
177    /// - Compare the empty and `abc` message hashes against fixed NIST vectors.
178    #[test]
179    fn nist_vectors() {
180        assert_eq!(
181            hex::encode(sha1(b"")),
182            "da39a3ee5e6b4b0d3255bfef95601890afd80709"
183        );
184        assert_eq!(
185            hex::encode(sha1(b"abc")),
186            "a9993e364706816aba3e25717850c26c9cd0d89d"
187        );
188        assert_eq!(
189            hex::encode(sha256(b"")),
190            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
191        );
192        assert_eq!(
193            hex::encode(sha256(b"abc")),
194            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
195        );
196    }
197
198    /// STATEMENT: Thumbprint selection and digest dispatch use the documented lengths.
199    ///
200    /// HOW:
201    /// - Select algorithms from hex lengths and compare dispatched hashes with direct calls.
202    #[test]
203    fn algorithm() {
204        assert_eq!(
205            Algorithm::for_hex_thumbprint(&"0".repeat(40)),
206            Algorithm::Sha1
207        );
208        assert_eq!(
209            Algorithm::for_hex_thumbprint(&"0".repeat(64)),
210            Algorithm::Sha256
211        );
212        assert_eq!(Algorithm::for_hex_thumbprint("abc"), Algorithm::Sha256);
213        assert_eq!(Algorithm::Sha1.digest_len(), 20);
214        assert_eq!(Algorithm::Sha256.digest_len(), 32);
215        assert_eq!(Algorithm::Sha1.digest(b"abc"), sha1(b"abc").to_vec());
216        assert_eq!(Algorithm::Sha256.digest(b"abc"), sha256(b"abc").to_vec());
217    }
218}