Skip to content

Commit 0272c8f

Browse files
authored
crypto: Add support for exporting private keys behind a feature (#4574)
For our security-sensitve code, it is nice to have a compile-time guarantee that private key material can't be accessed or leaked, as there simply is no API provided to do so. However, certain utilities and code that runs outside of the TCB do require access to private key secrets, such as the upcoming vtpm_util for preprovisioning TPMs. Add a new feature to crypto to gate methods that allow such access to private keys, add a new helper to RSA to expose components in a more friendly format, and implement this method for all backends.
1 parent 6c0d131 commit 0272c8f

8 files changed

Lines changed: 212 additions & 25 deletions

File tree

‎support/crypto/Cargo.toml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ edition.workspace = true
77
rust-version.workspace = true
88

99
[features]
10+
# Allow exporting private keys.
11+
export_private = []
12+
1013
# Enable test-only helper functions.
11-
test_helpers = ["x509-cert?/builder"]
14+
test_helpers = ["export_private", "x509-cert?/builder"]
1215

1316
# If the chosen backend supports it, vendor it.
1417
# If the chosen backend is natively available, this feature is a no-op.
@@ -109,6 +112,8 @@ x509-cert.workspace = true
109112
[dev-dependencies]
110113
der = { workspace = true, features = ["derive", "alloc"] }
111114
hex.workspace = true
115+
pkcs1.workspace = true
116+
pkcs8 = { workspace = true, features = ["alloc"] }
112117
x509-cert = { workspace = true, features = ["builder"] }
113118

114119
[lints]

‎support/crypto/src/lib.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
//!
1010
//! It is explicitly specialized for the needs of the OpenVMM project and is
1111
//! not suitable for general-purpose use.
12+
//!
13+
//! # Private key export
14+
//!
15+
//! The `export_private` feature allows access to private key material through
16+
//! export methods. Exported bytes and components contain unencrypted secrets.
17+
//! Callers are responsible for protecting them and clearing them when no longer
18+
//! needed. The returned byte vectors are not automatically zeroized.
1219
1320
// UNSAFETY: calling BCrypt APIs on Windows, Security.framework APIs on macOS,
1421
// and defining/referencing an extern symbol for the backend-selection

‎support/crypto/src/rsa/mac.rs‎

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -206,22 +206,12 @@ impl RsaKeyPairInner {
206206
Ok(Self(handle))
207207
}
208208

209-
#[cfg(any(test, feature = "test_helpers"))]
209+
#[cfg(any(test, feature = "export_private"))]
210210
pub fn to_pkcs8_der(&self) -> Result<Vec<u8>, RsaError> {
211211
use der::asn1::OctetString;
212212
use pkcs8::spki::AlgorithmIdentifierOwned;
213213

214-
let mut error: CFErrorRef = ptr::null();
215-
// SAFETY: self.0.0 is a valid SecKeyRef.
216-
let data = unsafe { SecKeyCopyExternalRepresentation(self.0.0, &mut error) };
217-
if data.is_null() {
218-
// SAFETY: error is null or valid.
219-
return Err(unsafe { rsa_sec_err(error, "exporting the RSA private key") });
220-
}
221-
let data = CfHandle(data);
222-
// SAFETY: data.0 is a valid CFDataRef.
223-
let pkcs1_der = unsafe { cf_data_to_vec(data.0) };
224-
214+
let pkcs1_der = self.export_pkcs1_der()?;
225215
let pki = pkcs8::PrivateKeyInfoOwned {
226216
algorithm: AlgorithmIdentifierOwned {
227217
oid: pkcs1::ALGORITHM_OID,
@@ -235,6 +225,39 @@ impl RsaKeyPairInner {
235225
.map_err(|e| der_err(e, "encoding the PKCS#8 PrivateKeyInfo"))
236226
}
237227

228+
#[cfg(any(test, feature = "export_private"))]
229+
pub fn to_private_components(&self) -> Result<super::RsaPrivateKeyComponents, RsaError> {
230+
let der = self.export_pkcs1_der()?;
231+
let key = pkcs1::RsaPrivateKey::from_der(&der)
232+
.map_err(|e| der_err(e, "parsing the exported PKCS#1 RSA private key"))?;
233+
if key.other_prime_infos.is_some() {
234+
return Err(RsaError(crate::BackendError::Invalid(
235+
"multi-prime RSA private keys are not supported",
236+
)));
237+
}
238+
Ok(super::RsaPrivateKeyComponents {
239+
modulus: key.modulus.as_bytes().to_vec(),
240+
public_exponent: key.public_exponent.as_bytes().to_vec(),
241+
private_exponent: key.private_exponent.as_bytes().to_vec(),
242+
prime1: key.prime1.as_bytes().to_vec(),
243+
prime2: key.prime2.as_bytes().to_vec(),
244+
})
245+
}
246+
247+
#[cfg(any(test, feature = "export_private"))]
248+
fn export_pkcs1_der(&self) -> Result<Vec<u8>, RsaError> {
249+
let mut error: CFErrorRef = ptr::null();
250+
// SAFETY: self.0.0 is a valid SecKeyRef.
251+
let data = unsafe { SecKeyCopyExternalRepresentation(self.0.0, &mut error) };
252+
if data.is_null() {
253+
// SAFETY: error is null or valid.
254+
return Err(unsafe { rsa_sec_err(error, "exporting the RSA private key") });
255+
}
256+
let data = CfHandle(data);
257+
// SAFETY: data.0 is a valid CFDataRef.
258+
Ok(unsafe { cf_data_to_vec(data.0) })
259+
}
260+
238261
pub fn oaep_decrypt(
239262
&self,
240263
input: &[u8],

‎support/crypto/src/rsa/mod.rs‎

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,19 @@ impl RsaKeyPair {
7575
sys::RsaKeyPairInner::from_pkcs8_der(der).map(Self)
7676
}
7777

78-
#[cfg(any(test, feature = "test_helpers"))]
78+
#[cfg(any(test, feature = "export_private"))]
7979
/// Convert the RSA private key to PKCS#8 DER-encoded bytes.
8080
pub fn to_pkcs8_der(&self) -> Result<Vec<u8>, RsaError> {
8181
self.0.to_pkcs8_der()
8282
}
8383

84+
#[cfg(any(test, feature = "export_private"))]
85+
/// Returns the components of an RSA private key as big-endian
86+
/// byte vectors, without leading zero padding.
87+
pub fn to_private_components(&self) -> Result<RsaPrivateKeyComponents, RsaError> {
88+
self.0.to_private_components()
89+
}
90+
8491
/// Decrypt `input` using RSA-OAEP with the specified hash algorithm.
8592
pub fn oaep_decrypt(
8693
&self,
@@ -117,14 +124,34 @@ impl RsaKeyPair {
117124
pub struct RsaPublicKey(pub(crate) sys::RsaPublicKeyInner);
118125

119126
/// The public components of an RSA key.
127+
///
128+
/// All components are unsigned big-endian integers.
120129
#[derive(Debug, Clone, PartialEq, Eq)]
121130
pub struct RsaPublicKeyComponents {
122-
/// The RSA modulus as a big-endian byte vector.
131+
/// The RSA modulus, `n`.
123132
pub modulus: Vec<u8>,
124-
/// The RSA public exponent as a big-endian byte vector.
133+
/// The RSA public exponent, `e`.
125134
pub public_exponent: Vec<u8>,
126135
}
127136

137+
/// The full components of a two-prime RSA private key.
138+
///
139+
/// All components are unsigned big-endian integers.
140+
#[cfg(any(test, feature = "export_private"))]
141+
#[derive(Clone, PartialEq, Eq)]
142+
pub struct RsaPrivateKeyComponents {
143+
/// The RSA modulus, `n`.
144+
pub modulus: Vec<u8>,
145+
/// The RSA public exponent, `e`.
146+
pub public_exponent: Vec<u8>,
147+
/// The RSA private exponent, `d`.
148+
pub private_exponent: Vec<u8>,
149+
/// The first prime factor, `p`.
150+
pub prime1: Vec<u8>,
151+
/// The second prime factor, `q`.
152+
pub prime2: Vec<u8>,
153+
}
154+
128155
impl RsaPublicKey {
129156
/// Construct an RSA public key from a big-endian modulus `n` and
130157
/// big-endian public exponent `e`.
@@ -191,6 +218,7 @@ impl std::ops::Deref for RsaKeyPair {
191218
#[cfg(test)]
192219
mod tests {
193220
use super::*;
221+
use der::Decode;
194222

195223
/// Sign and verify a message of arbitrary (i.e. not pre-hashed) length.
196224
/// Both backends must hash the message internally before applying the
@@ -338,4 +366,28 @@ mod tests {
338366
assert!(!pkcs1_as_pss);
339367
assert!(!pss_as_pkcs1);
340368
}
369+
370+
#[test]
371+
fn private_components_match_pkcs8() {
372+
let key = RsaKeyPair::generate(2048).unwrap();
373+
let components = key.to_private_components().unwrap();
374+
let public = key.to_components();
375+
assert_eq!(components.modulus, public.modulus);
376+
assert_eq!(components.public_exponent, public.public_exponent);
377+
378+
let der = key.to_pkcs8_der().unwrap();
379+
let pki = pkcs8::PrivateKeyInfoRef::from_der(&der).unwrap();
380+
let decoded = pkcs1::RsaPrivateKey::from_der(pki.private_key.as_bytes()).unwrap();
381+
for (actual, expected) in [
382+
(&components.modulus, decoded.modulus),
383+
(&components.public_exponent, decoded.public_exponent),
384+
(&components.private_exponent, decoded.private_exponent),
385+
(&components.prime1, decoded.prime1),
386+
(&components.prime2, decoded.prime2),
387+
] {
388+
assert!(!actual.is_empty());
389+
assert_ne!(actual[0], 0);
390+
assert_eq!(actual.as_slice(), expected.as_bytes());
391+
}
392+
}
341393
}

‎support/crypto/src/rsa/ossl.rs‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,29 @@ impl RsaKeyPairInner {
3232
Ok(Self(pkey))
3333
}
3434

35-
#[cfg(any(test, feature = "test_helpers"))]
35+
#[cfg(any(test, feature = "export_private"))]
3636
pub fn to_pkcs8_der(&self) -> Result<Vec<u8>, RsaError> {
3737
self.0
3838
.private_key_to_pkcs8()
3939
.map_err(|e| err(e, "encoding the private key as PKCS#8 DER"))
4040
}
4141

42+
#[cfg(any(test, feature = "export_private"))]
43+
pub fn to_private_components(&self) -> Result<super::RsaPrivateKeyComponents, RsaError> {
44+
let rsa = self
45+
.0
46+
.rsa()
47+
.map_err(|e| err(e, "accessing the RSA private key"))?;
48+
49+
Ok(super::RsaPrivateKeyComponents {
50+
modulus: rsa.n().to_vec(),
51+
public_exponent: rsa.e().to_vec(),
52+
private_exponent: rsa.d().to_vec(),
53+
prime1: rsa.p().unwrap().to_vec(),
54+
prime2: rsa.q().unwrap().to_vec(),
55+
})
56+
}
57+
4258
pub fn oaep_decrypt(
4359
&self,
4460
input: &[u8],

‎support/crypto/src/rsa/rust.rs‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ impl RsaKeyPairInner {
3939
Ok(Self(parsed))
4040
}
4141

42-
#[cfg(any(test, feature = "test_helpers"))]
42+
#[cfg(any(test, feature = "export_private"))]
4343
pub fn to_pkcs8_der(&self) -> Result<Vec<u8>, RsaError> {
4444
use pkcs8::EncodePrivateKey;
4545
Ok(self
@@ -50,6 +50,25 @@ impl RsaKeyPairInner {
5050
.to_vec())
5151
}
5252

53+
#[cfg(any(test, feature = "export_private"))]
54+
pub fn to_private_components(&self) -> Result<super::RsaPrivateKeyComponents, RsaError> {
55+
use rsa::traits::PrivateKeyParts;
56+
57+
let [p, q] = self.0.primes() else {
58+
return Err(RsaError(
59+
rsa::Error::InvalidArguments,
60+
"exporting a two-prime RSA private key",
61+
));
62+
};
63+
Ok(super::RsaPrivateKeyComponents {
64+
modulus: self.0.n().to_be_bytes_trimmed_vartime().into_vec(),
65+
public_exponent: self.0.e().to_be_bytes_trimmed_vartime().into_vec(),
66+
private_exponent: self.0.d().to_be_bytes_trimmed_vartime().into_vec(),
67+
prime1: p.to_be_bytes_trimmed_vartime().into_vec(),
68+
prime2: q.to_be_bytes_trimmed_vartime().into_vec(),
69+
})
70+
}
71+
5372
pub fn oaep_decrypt(
5473
&self,
5574
input: &[u8],

‎support/crypto/src/rsa/symcrypt.rs‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ impl RsaKeyPairInner {
5353
Ok(Self(rsa))
5454
}
5555

56-
#[cfg(any(test, feature = "test_helpers"))]
56+
#[cfg(any(test, feature = "export_private"))]
5757
pub fn to_pkcs8_der(&self) -> Result<Vec<u8>, RsaError> {
5858
use der::Encode;
5959
use der::asn1::OctetString;
@@ -98,6 +98,21 @@ impl RsaKeyPairInner {
9898
.map_err(|e| der_err(e, "encoding the PKCS#8 PrivateKeyInfo"))
9999
}
100100

101+
#[cfg(any(test, feature = "export_private"))]
102+
pub fn to_private_components(&self) -> Result<super::RsaPrivateKeyComponents, RsaError> {
103+
let blob = self
104+
.0
105+
.export_key_pair_blob()
106+
.map_err(|e| err(e, "exporting the RSA key blob"))?;
107+
Ok(super::RsaPrivateKeyComponents {
108+
modulus: blob.modulus,
109+
public_exponent: blob.pub_exp,
110+
private_exponent: blob.private_exp,
111+
prime1: blob.p,
112+
prime2: blob.q,
113+
})
114+
}
115+
101116
pub fn oaep_decrypt(
102117
&self,
103118
input: &[u8],

‎support/crypto/src/rsa/win.rs‎

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,22 @@ use windows::Win32::Security::Cryptography::BCRYPT_RSAPRIVATE_BLOB;
2727
use windows::Win32::Security::Cryptography::BCRYPT_RSAPUBLIC_BLOB;
2828
use windows::Win32::Security::Cryptography::BCRYPT_RSAPUBLIC_MAGIC;
2929
use windows::Win32::Security::Cryptography::CNG_RSA_PRIVATE_KEY_BLOB;
30-
#[cfg(any(test, feature = "test_helpers"))]
30+
#[cfg(any(test, feature = "export_private"))]
3131
use windows::Win32::Security::Cryptography::CRYPT_ALGORITHM_IDENTIFIER;
3232
use windows::Win32::Security::Cryptography::CRYPT_DECODE_ALLOC_FLAG;
33-
#[cfg(any(test, feature = "test_helpers"))]
33+
#[cfg(any(test, feature = "export_private"))]
3434
use windows::Win32::Security::Cryptography::CRYPT_ENCODE_ALLOC_FLAG;
35-
#[cfg(any(test, feature = "test_helpers"))]
35+
#[cfg(any(test, feature = "export_private"))]
3636
use windows::Win32::Security::Cryptography::CRYPT_INTEGER_BLOB;
3737
use windows::Win32::Security::Cryptography::CRYPT_PRIVATE_KEY_INFO;
3838
use windows::Win32::Security::Cryptography::CryptDecodeObjectEx;
39-
#[cfg(any(test, feature = "test_helpers"))]
39+
#[cfg(any(test, feature = "export_private"))]
4040
use windows::Win32::Security::Cryptography::CryptEncodeObjectEx;
4141
use windows::Win32::Security::Cryptography::PKCS_7_ASN_ENCODING;
4242
use windows::Win32::Security::Cryptography::PKCS_PRIVATE_KEY_INFO;
4343
use windows::Win32::Security::Cryptography::X509_ASN_ENCODING;
4444
use windows::Win32::Security::Cryptography::szOID_RSA_RSA;
45-
#[cfg(any(test, feature = "test_helpers"))]
45+
#[cfg(any(test, feature = "export_private"))]
4646
use windows::core::PSTR;
4747

4848
fn err(err: windows_result::Error, op: &'static str) -> RsaError {
@@ -245,7 +245,7 @@ impl RsaKeyPairInner {
245245
Ok(Self(handle))
246246
}
247247

248-
#[cfg(any(test, feature = "test_helpers"))]
248+
#[cfg(any(test, feature = "export_private"))]
249249
pub fn to_pkcs8_der(&self) -> Result<Vec<u8>, RsaError> {
250250
// Step 1: export the key as a BCrypt full-private blob, which
251251
// contains every field needed by the PKCS#1 RSAPrivateKey ASN.1
@@ -324,6 +324,56 @@ impl RsaKeyPairInner {
324324
Ok(pkcs8_buf.as_bytes().to_vec())
325325
}
326326

327+
#[cfg(any(test, feature = "export_private"))]
328+
pub fn to_private_components(&self) -> Result<super::RsaPrivateKeyComponents, RsaError> {
329+
use windows::Win32::Security::Cryptography::BCRYPT_RSAFULLPRIVATE_MAGIC;
330+
use zerocopy::FromBytes;
331+
332+
let blob = export_key(&self.0, BCRYPT_RSAFULLPRIVATE_BLOB)?;
333+
334+
// BCRYPT_RSAKEY_BLOB consists of six native-endian u32 fields.
335+
let ([magic, _bits, cb_e, cb_n, cb_p, cb_q], mut rest) =
336+
<[u32; 6]>::read_from_prefix(&blob)
337+
.map_err(|_| invalid_err("exported RSA private key blob is too small"))?;
338+
if magic != BCRYPT_RSAFULLPRIVATE_MAGIC.0 {
339+
return Err(invalid_err(
340+
"exported RSA private key blob has an invalid magic value",
341+
));
342+
}
343+
let mut take_component = |size: u32| -> Result<Vec<u8>, RsaError> {
344+
if size == 0 {
345+
return Err(invalid_err(
346+
"exported RSA private key has an empty component",
347+
));
348+
}
349+
let (bytes, remaining) = rest
350+
.split_at_checked(size as usize)
351+
.ok_or_else(|| invalid_err("exported RSA private key blob is truncated"))?;
352+
rest = remaining;
353+
Ok(bytes.to_vec())
354+
};
355+
let public_exponent = take_component(cb_e)?;
356+
let modulus = take_component(cb_n)?;
357+
let prime1 = take_component(cb_p)?;
358+
let prime2 = take_component(cb_q)?;
359+
let _exponent1 = take_component(cb_p)?;
360+
let _exponent2 = take_component(cb_q)?;
361+
let _coefficient = take_component(cb_p)?;
362+
let private_exponent = take_component(cb_n)?;
363+
if !rest.is_empty() {
364+
return Err(invalid_err(
365+
"exported RSA private key blob has trailing bytes",
366+
));
367+
}
368+
Ok(super::RsaPrivateKeyComponents {
369+
modulus,
370+
public_exponent,
371+
private_exponent,
372+
prime1,
373+
prime2,
374+
})
375+
}
376+
327377
pub fn oaep_decrypt(
328378
&self,
329379
input: &[u8],

0 commit comments

Comments
 (0)