Skip to main content

mssql_auth/
key_unwrap.rs

1//! RSA-OAEP key unwrapping for Always Encrypted.
2//!
3//! This module implements the RSA-OAEP algorithm used to decrypt Column Encryption Keys (CEKs)
4//! that are encrypted with Column Master Keys (CMKs). The encrypted CEK arrives
5//! in the canonical Microsoft envelope (see [`crate::cek_envelope`]); its
6//! signature is verified against the CMK public key before unwrapping, as the
7//! reference implementation requires.
8//!
9//! ## RSA-OAEP Parameters
10//!
11//! The `RSA_OAEP` algorithm in Always Encrypted CMK metadata means OAEP with
12//! SHA-1 and MGF1-SHA-1 — the reference implementation wraps CEKs with
13//! `RSAEncryptionPadding.OaepSHA1` (dotnet/SqlClient
14//! `EncryptedColumnEncryptionKeyParameters.cs`). The envelope signature, by
15//! contrast, uses PKCS#1 v1.5 over SHA-256.
16//!
17//! - **Hash function**: SHA-1
18//! - **MGF**: MGF1-SHA-1
19//! - **Label**: Empty
20
21use rsa::{
22    Oaep, Pkcs1v15Sign, RsaPrivateKey, pkcs1::DecodeRsaPrivateKey, pkcs8::DecodePrivateKey,
23    traits::PublicKeyParts,
24};
25use sha1::Sha1;
26use sha2::Sha256;
27
28use crate::cek_envelope;
29use crate::encryption::EncryptionError;
30
31/// RSA-OAEP key unwrapper for decrypting Column Encryption Keys.
32pub struct RsaKeyUnwrapper {
33    private_key: RsaPrivateKey,
34}
35
36impl RsaKeyUnwrapper {
37    /// Create a new unwrapper from a PEM-encoded RSA private key.
38    ///
39    /// Supports both PKCS#1 and PKCS#8 formats.
40    ///
41    /// # Arguments
42    ///
43    /// * `pem` - PEM-encoded RSA private key
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if the key cannot be parsed.
48    pub fn from_pem(pem: &str) -> Result<Self, EncryptionError> {
49        // Try PKCS#8 format first
50        let private_key = RsaPrivateKey::from_pkcs8_pem(pem)
51            .or_else(|_| RsaPrivateKey::from_pkcs1_pem(pem))
52            .map_err(|e| {
53                EncryptionError::CmkError(format!("Failed to parse RSA private key: {e}"))
54            })?;
55
56        Ok(Self { private_key })
57    }
58
59    /// Create a new unwrapper from DER-encoded RSA private key bytes.
60    ///
61    /// # Arguments
62    ///
63    /// * `der` - DER-encoded RSA private key (PKCS#8 format)
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if the key cannot be parsed.
68    pub fn from_der(der: &[u8]) -> Result<Self, EncryptionError> {
69        let private_key = RsaPrivateKey::from_pkcs8_der(der)
70            .or_else(|_| RsaPrivateKey::from_pkcs1_der(der))
71            .map_err(|e| {
72                EncryptionError::CmkError(format!("Failed to parse RSA private key: {e}"))
73            })?;
74
75        Ok(Self { private_key })
76    }
77
78    /// Create a new unwrapper from an existing RSA private key.
79    pub fn from_key(private_key: RsaPrivateKey) -> Self {
80        Self { private_key }
81    }
82
83    /// Decrypt a Column Encryption Key (CEK) using RSA-OAEP.
84    ///
85    /// Parses the canonical envelope, verifies its signature against this
86    /// CMK's public key, and unwraps the CEK.
87    ///
88    /// # Arguments
89    ///
90    /// * `encrypted_cek` - The encrypted CEK envelope
91    ///
92    /// # Returns
93    ///
94    /// The decrypted CEK (32 bytes for AES-256).
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if:
99    /// - The encrypted CEK envelope is invalid
100    /// - The ciphertext or signature length does not match the RSA key size
101    /// - Signature verification fails
102    /// - RSA decryption fails
103    pub fn decrypt_cek(&self, encrypted_cek: &[u8]) -> Result<Vec<u8>, EncryptionError> {
104        let envelope = cek_envelope::parse(encrypted_cek)?;
105
106        let key_size = self.private_key.size();
107        if envelope.ciphertext.len() != key_size {
108            return Err(EncryptionError::CekDecryptionFailed(format!(
109                "CEK ciphertext length {} does not match RSA key size {key_size}",
110                envelope.ciphertext.len()
111            )));
112        }
113        if envelope.signature.len() != key_size {
114            return Err(EncryptionError::CekDecryptionFailed(format!(
115                "CEK signature length {} does not match RSA key size {key_size}",
116                envelope.signature.len()
117            )));
118        }
119
120        self.private_key
121            .to_public_key()
122            .verify(
123                Pkcs1v15Sign::new::<Sha256>(),
124                &envelope.signed_digest(),
125                envelope.signature,
126            )
127            .map_err(|_| {
128                EncryptionError::CekDecryptionFailed(
129                    "CEK envelope signature verification failed".into(),
130                )
131            })?;
132
133        // Decrypt using RSA-OAEP-SHA1 (what RSA_OAEP means in AE metadata)
134        let padding = Oaep::new::<Sha1>();
135        let decrypted = self
136            .private_key
137            .decrypt(padding, envelope.ciphertext)
138            .map_err(|e| {
139                EncryptionError::CekDecryptionFailed(format!("RSA-OAEP decryption failed: {e}"))
140            })?;
141
142        Ok(decrypted)
143    }
144
145    /// Decrypt raw RSA-OAEP ciphertext (without SQL Server header).
146    ///
147    /// Use this when you have just the RSA ciphertext without the SQL Server envelope.
148    pub fn decrypt_raw(&self, ciphertext: &[u8]) -> Result<Vec<u8>, EncryptionError> {
149        let padding = Oaep::new::<Sha1>();
150        self.private_key.decrypt(padding, ciphertext).map_err(|e| {
151            EncryptionError::CekDecryptionFailed(format!("RSA-OAEP decryption failed: {e}"))
152        })
153    }
154
155    /// Get the RSA key size in bits.
156    pub fn key_bits(&self) -> usize {
157        self.private_key.size() * 8
158    }
159}
160
161/// Create a signed encrypted-CEK envelope for testing.
162///
163/// Builds the canonical envelope and signs it with the given CMK private key,
164/// as standard provisioning tools do.
165#[cfg(test)]
166#[allow(clippy::expect_used)]
167pub fn create_test_encrypted_cek(
168    cmk: &RsaPrivateKey,
169    key_path: &str,
170    ciphertext: &[u8],
171) -> Vec<u8> {
172    use sha2::Digest;
173
174    let mut envelope = cek_envelope::build_signed_portion(key_path, ciphertext);
175    let digest: [u8; 32] = Sha256::digest(&envelope).into();
176    let signature = cmk
177        .sign(Pkcs1v15Sign::new::<Sha256>(), &digest)
178        .expect("test CMK signs");
179    envelope.extend_from_slice(&signature);
180    envelope
181}
182
183#[cfg(test)]
184#[allow(clippy::unwrap_used, clippy::expect_used)]
185mod tests {
186    use super::*;
187    use rsa::{RsaPrivateKey, pkcs8::EncodePrivateKey};
188
189    fn generate_test_key() -> RsaPrivateKey {
190        let mut rng = rand::thread_rng();
191        RsaPrivateKey::new(&mut rng, 2048).unwrap()
192    }
193
194    #[test]
195    fn test_key_unwrapper_from_pem_pkcs8() {
196        let key = generate_test_key();
197        let pem = key.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF).unwrap();
198
199        let unwrapper = RsaKeyUnwrapper::from_pem(&pem).unwrap();
200        assert_eq!(unwrapper.key_bits(), 2048);
201    }
202
203    #[test]
204    fn test_decrypt_raw() {
205        let key = generate_test_key();
206        let unwrapper = RsaKeyUnwrapper::from_key(key.clone());
207
208        // Encrypt a test CEK
209        let test_cek = [0x42u8; 32]; // Test CEK
210        let public_key = key.to_public_key();
211        let padding = Oaep::new::<Sha1>();
212        let mut rng = rand::thread_rng();
213        let ciphertext = public_key.encrypt(&mut rng, padding, &test_cek).unwrap();
214
215        // Decrypt and verify
216        let decrypted = unwrapper.decrypt_raw(&ciphertext).unwrap();
217        assert_eq!(decrypted, test_cek);
218    }
219
220    #[test]
221    fn test_decrypt_cek_full_flow() {
222        let key = generate_test_key();
223        let unwrapper = RsaKeyUnwrapper::from_key(key.clone());
224
225        // Generate a test CEK (32 bytes for AES-256)
226        let test_cek = [0x55u8; 32];
227
228        // Encrypt the CEK with RSA-OAEP
229        let public_key = key.to_public_key();
230        let padding = Oaep::new::<Sha1>();
231        let mut rng = rand::thread_rng();
232        let rsa_ciphertext = public_key.encrypt(&mut rng, padding, &test_cek).unwrap();
233
234        // Create a canonical signed envelope
235        let encrypted_cek =
236            create_test_encrypted_cek(&key, "CurrentUser/My/TestCert", &rsa_ciphertext);
237
238        // Decrypt and verify
239        let decrypted = unwrapper.decrypt_cek(&encrypted_cek).unwrap();
240        assert_eq!(decrypted, test_cek);
241    }
242
243    #[test]
244    fn test_decrypt_cek_rejects_tampered_envelope() {
245        let key = generate_test_key();
246        let unwrapper = RsaKeyUnwrapper::from_key(key.clone());
247
248        let test_cek = [0x55u8; 32];
249        let public_key = key.to_public_key();
250        let padding = Oaep::new::<Sha1>();
251        let mut rng = rand::thread_rng();
252        let rsa_ciphertext = public_key.encrypt(&mut rng, padding, &test_cek).unwrap();
253
254        let mut encrypted_cek = create_test_encrypted_cek(&key, "Test", &rsa_ciphertext);
255        // Flip one ciphertext bit: the signature must now fail to verify.
256        encrypted_cek[20] ^= 0x01;
257
258        let err = unwrapper.decrypt_cek(&encrypted_cek).unwrap_err();
259        assert!(err.to_string().contains("signature verification failed"));
260    }
261
262    #[test]
263    fn test_decrypt_cek_rejects_wrong_signer() {
264        let key = generate_test_key();
265        let unwrapper = RsaKeyUnwrapper::from_key(key.clone());
266
267        let test_cek = [0x55u8; 32];
268        let public_key = key.to_public_key();
269        let padding = Oaep::new::<Sha1>();
270        let mut rng = rand::thread_rng();
271        let rsa_ciphertext = public_key.encrypt(&mut rng, padding, &test_cek).unwrap();
272
273        // Envelope signed by a DIFFERENT key than the CMK.
274        let other_key = generate_test_key();
275        let encrypted_cek = create_test_encrypted_cek(&other_key, "Test", &rsa_ciphertext);
276
277        let err = unwrapper.decrypt_cek(&encrypted_cek).unwrap_err();
278        assert!(err.to_string().contains("signature verification failed"));
279    }
280}