Skip to content

Commit a8ed0fe

Browse files
authored
pkcs8: initial EncryptedPrivateKeyInfo support (#262)
Adds support for parsing PKCS#8 `EncryptedPrivateKeyInfo`. Includes tests with DER and PEM-encoded encrypted private keys, using semi-modern PKCS#5 v2 encryption algorithms (AES-CBC and HMAC-SHA-256). Does not actually implement support for decrypting/encrypting private keys, nor does it add a `document` type for heap-backed storage. This is all punted on for now to add an initial type. The plan is to open tracking issues for adding support, deciding on which encryption algorithms we want to support, and then following up on actual support in subsequent PRs, most likely after the next `cipher` release. Making all of this work required making the `parameters` field of `spki::AlgorithmIdentifier` a bona fide `Any` type as the parameters for encrypted keys include things like a salt/nonce.
1 parent 5f319dd commit a8ed0fe

File tree

13 files changed

+180
-117
lines changed

13 files changed

+180
-117
lines changed

der/src/asn1/any.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,16 @@ impl<'a> Any<'a> {
4747
self.value.len()
4848
}
4949

50-
/// Is the value of this [`Any`] type empty?
50+
/// Is the body of this [`Any`] type empty?
5151
pub fn is_empty(self) -> bool {
5252
self.value.is_empty()
5353
}
5454

55+
/// Is this value an ASN.1 NULL value?
56+
pub fn is_null(self) -> bool {
57+
Null::try_from(self).is_ok()
58+
}
59+
5560
/// Get the raw value for this [`Any`] type as a byte slice.
5661
pub fn as_bytes(self) -> &'a [u8] {
5762
self.value.as_bytes()
@@ -68,6 +73,7 @@ impl<'a> Any<'a> {
6873
}
6974

7075
/// Attempt to decode an ASN.1 `NULL` value.
76+
#[deprecated(since = "0.2.4", note = "Please use the `is_null` function instead")]
7177
pub fn null(self) -> Result<Null> {
7278
self.try_into()
7379
}

der/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ impl From<ErrorKind> for Error {
7171
}
7272
}
7373

74-
impl From<core::convert::Infallible> for Error {
74+
impl From<Infallible> for Error {
7575
fn from(_: Infallible) -> Error {
7676
unreachable!()
7777
}

der/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@
315315
html_root_url = "https://docs.rs/der/0.2.3"
316316
)]
317317
#![forbid(unsafe_code)]
318-
#![warn(missing_docs, rust_2018_idioms)]
318+
#![warn(missing_docs, rust_2018_idioms, unused_qualifications)]
319319

320320
#[cfg(feature = "alloc")]
321321
extern crate alloc;

pkcs8/src/document.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::{fs, path::Path, str};
1616
#[cfg(feature = "pem")]
1717
use {crate::pem, alloc::string::String, core::str::FromStr};
1818

19-
/// PKCS#8 private key document
19+
/// PKCS#8 private key document.
2020
///
2121
/// This type provides storage for a PKCS#8 private key encoded as ASN.1 DER
2222
/// with the invariant that the contained-document is "well-formed", i.e. it
@@ -155,7 +155,7 @@ impl FromStr for PrivateKeyDocument {
155155
}
156156
}
157157

158-
/// SPKI public key document
158+
/// SPKI public key document.
159159
///
160160
/// This type provides storage for a SPKI public key encoded as ASN.1 DER with
161161
/// the invariant that the contained-document is "well-formed", i.e. it will

pkcs8/src/lib.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
//! Private-Key Information Syntax Specification (as defined in [RFC 5208]).
33
//!
44
//! # About
5-
//!
65
//! This is a minimalistic library targeting `no_std` platforms and small code
76
//! size. It supports decoding/encoding of the following types without the use
87
//! of a heap:
@@ -21,7 +20,6 @@
2120
//! documents from "PEM encoding" format as defined in RFC 7468.
2221
//!
2322
//! # Supported Algorithms
24-
//!
2523
//! This crate has been tested against keys generated by OpenSSL for the
2624
//! following algorithms:
2725
//!
@@ -32,7 +30,11 @@
3230
//! It may work with other algorithms which use an optional OID for
3331
//! [`AlgorithmIdentifier`] parameters.
3432
//!
35-
//! Encrypted private keys are presently unsupported.
33+
//! # Encrypted Private Key Support
34+
//! [`EncryptedPrivateKeyInfo`] supports decoding/encoding encrypted PKCS#8
35+
//! private keys.
36+
//!
37+
//! However, support for actually decrypting/encrypting them remains TBD.
3638
//!
3739
//! # Minimum Supported Rust Version
3840
//!
@@ -68,11 +70,11 @@ mod pem;
6870

6971
pub use crate::{
7072
error::{Error, Result},
71-
private_key_info::PrivateKeyInfo,
73+
private_key_info::{encrypted::EncryptedPrivateKeyInfo, PrivateKeyInfo},
7274
traits::{FromPrivateKey, FromPublicKey},
7375
};
7476
pub use der::{self, ObjectIdentifier};
75-
pub use spki::{AlgorithmIdentifier, AlgorithmParameters, SubjectPublicKeyInfo};
77+
pub use spki::{AlgorithmIdentifier, SubjectPublicKeyInfo};
7678

7779
#[cfg(feature = "alloc")]
7880
pub use crate::{

pkcs8/src/private_key_info.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! PKCS#8 `PrivateKeyInfo`.
22
3+
pub(crate) mod encrypted;
4+
35
use crate::{AlgorithmIdentifier, Error, Result};
46
use core::{convert::TryFrom, fmt};
57
use der::{Decodable, Encodable, Message};
@@ -16,13 +18,12 @@ use {crate::pem, zeroize::Zeroizing};
1618
/// RFC 5208 designates `0` as the only valid version for PKCS#8 documents
1719
const VERSION: u8 = 0;
1820

19-
/// PKCS#8 `PrivateKeyInfo`
21+
/// PKCS#8 `PrivateKeyInfo`.
2022
///
2123
/// ASN.1 structure containing an [`AlgorithmIdentifier`] and private key
2224
/// data in an algorithm specific format.
2325
///
24-
/// Described in RFC 5208 Section 5:
25-
/// <https://tools.ietf.org/html/rfc5208#section-5>
26+
/// Described in [RFC 5208 Section 5]:
2627
///
2728
/// ```text
2829
/// PrivateKeyInfo ::= SEQUENCE {
@@ -39,13 +40,18 @@ const VERSION: u8 = 0;
3940
///
4041
/// Attributes ::= SET OF Attribute
4142
/// ```
42-
#[derive(Copy, Clone)]
43+
///
44+
/// [RFC 5208 Section 5]: https://tools.ietf.org/html/rfc5208#section-5
45+
#[derive(Clone)]
4346
pub struct PrivateKeyInfo<'a> {
44-
/// X.509 [`AlgorithmIdentifier`]
45-
pub algorithm: AlgorithmIdentifier,
47+
/// X.509 [`AlgorithmIdentifier`] for the private key type
48+
pub algorithm: AlgorithmIdentifier<'a>,
4649

4750
/// Private key data
4851
pub private_key: &'a [u8],
52+
// TODO(tarcieri): support for `Attributes` (are they used in practice?)
53+
// PKCS#9 describes the possible attributes: https://tools.ietf.org/html/rfc2985
54+
// Workaround for stripping attributes: https://stackoverflow.com/a/48039151
4955
}
5056

5157
impl<'a> PrivateKeyInfo<'a> {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//! PKCS#8 `EncryptedPrivateKeyInfo`
2+
3+
use crate::{AlgorithmIdentifier, Error, Result};
4+
use core::convert::TryFrom;
5+
use der::{Decodable, Encodable, Message};
6+
7+
/// PKCS#8 `EncryptedPrivateKeyInfo`.
8+
///
9+
/// ASN.1 structure containing an [`AlgorithmIdentifier`] identifying a
10+
/// symmetric encryption scheme and encrypted private key data.
11+
///
12+
/// ## Encryption algorithm support
13+
///
14+
/// tl;dr: none yet!
15+
///
16+
/// This crate does not (yet) support decrypting/encrypting private key data.
17+
///
18+
/// [PKCS#5 v1.5] supports several password-based encryption algorithms,
19+
/// including `PBE-SHA1-3DES`.
20+
///
21+
/// [PKCS#5 v2] adds support for AES encryption with iterated PRFs
22+
/// such as `hmacWithSHA256`.
23+
///
24+
/// We may consider adding support for these in future releases of this crate.
25+
///
26+
/// ## Schema
27+
/// Structure described in [RFC 5208 Section 6]:
28+
///
29+
/// ```text
30+
/// EncryptedPrivateKeyInfo ::= SEQUENCE {
31+
/// encryptionAlgorithm EncryptionAlgorithmIdentifier,
32+
/// encryptedData EncryptedData }
33+
///
34+
/// EncryptionAlgorithmIdentifier ::= AlgorithmIdentifier
35+
///
36+
/// EncryptedData ::= OCTET STRING
37+
/// ```
38+
///
39+
/// [RFC 5208 Section 6]: https://tools.ietf.org/html/rfc5208#section-6
40+
/// [PKCS#5 v1.5]: https://tools.ietf.org/html/rfc2898
41+
/// [PKCS#5 v2]: https://tools.ietf.org/html/rfc8018
42+
#[derive(Copy, Clone)]
43+
pub struct EncryptedPrivateKeyInfo<'a> {
44+
/// [`AlgorithmIdentifier`] for the symmetric encryption algorithm used to
45+
/// encrypt the `encrypted_data` field.
46+
pub encryption_algorithm: AlgorithmIdentifier<'a>,
47+
48+
/// Private key data
49+
pub encrypted_data: &'a [u8],
50+
}
51+
52+
impl<'a> TryFrom<&'a [u8]> for EncryptedPrivateKeyInfo<'a> {
53+
type Error = Error;
54+
55+
fn try_from(bytes: &'a [u8]) -> Result<Self> {
56+
Ok(Self::from_bytes(bytes)?)
57+
}
58+
}
59+
60+
impl<'a> TryFrom<der::Any<'a>> for EncryptedPrivateKeyInfo<'a> {
61+
type Error = der::Error;
62+
63+
fn try_from(any: der::Any<'a>) -> der::Result<EncryptedPrivateKeyInfo<'a>> {
64+
any.sequence(|decoder| {
65+
Ok(Self {
66+
encryption_algorithm: decoder.decode()?,
67+
encrypted_data: decoder.octet_string()?.as_bytes(),
68+
})
69+
})
70+
}
71+
}
72+
73+
impl<'a> Message<'a> for EncryptedPrivateKeyInfo<'a> {
74+
fn fields<F, T>(&self, f: F) -> der::Result<T>
75+
where
76+
F: FnOnce(&[&dyn Encodable]) -> der::Result<T>,
77+
{
78+
f(&[
79+
&self.encryption_algorithm,
80+
&der::OctetString::new(self.encrypted_data)?,
81+
])
82+
}
83+
}

pkcs8/tests/encrypted_private_key.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//! Encrypted PKCS#8 private key tests.
2+
3+
use core::convert::TryFrom;
4+
use hex_literal::hex;
5+
use pkcs8::EncryptedPrivateKeyInfo;
6+
7+
/// Ed25519 PKCS#8 encrypted private key encoded as ASN.1 DER.
8+
///
9+
/// Generated using:
10+
///
11+
/// ```
12+
/// $ openssl pkcs8 -v2 aes-256-cbc -v2prf hmacWithSHA256 -topk8 -inform der -in ed25519-priv.der -outform der -out ed25519-priv-enc-v2.der
13+
/// ```
14+
const ED25519_DER_EXAMPLE: &[u8] = include_bytes!("examples/ed25519-priv-enc-v2.der");
15+
16+
/// Ed25519 PKCS#8 encrypted private key encoded as PEM.
17+
///
18+
/// Generated using:
19+
///
20+
/// ```
21+
/// $ openssl pkcs8 -v2 aes-256-cbc -v2prf hmacWithSHA256 -topk8 -in ed25519-priv.pem -out ed25519-priv-enc-v2.pem
22+
/// ```
23+
#[cfg(feature = "pem")]
24+
#[allow(dead_code)] // TODO(tarcieri): support for PEM-encoded `EncryptedPrivateKeyInfo`
25+
const ED25519_PEM_EXAMPLE: &str = include_str!("examples/ed25519-priv-enc-v2.pem");
26+
27+
/// Password used to encrypt the keys.
28+
#[allow(dead_code)] // TODO(tarcieri): decryption support
29+
const PASSWORD: &[u8] = b"hunter42"; // Bad password; don't actually use outside tests!
30+
31+
#[test]
32+
fn parse_ed25519_der_encrypted() {
33+
let pk = EncryptedPrivateKeyInfo::try_from(ED25519_DER_EXAMPLE).unwrap();
34+
35+
assert_eq!(
36+
pk.encryption_algorithm.oid,
37+
"1.2.840.113549.1.5.13".parse().unwrap()
38+
); // PBES2
39+
40+
// TODO(tarcieri): parse/extract params
41+
42+
// Extracted with:
43+
// $ openssl asn1parse -in tests/examples/ed25519-priv-enc-v2.der -inform der
44+
assert_eq!(
45+
pk.encrypted_data,
46+
&hex!("D0CD6C770F4BB87176422305C17401809E226674CE74185D221BFDAA95069890C8882FCE02B05D41BCBF54B035595BCD4154B32593708469B86AACF8815A7B2B")
47+
);
48+
}
158 Bytes
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
MIGbMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAjCBvGI26s6nAICCAAw
3+
DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEENob0oKOl/afgIhlBz+LijQEQGCN
4+
nJgmF4dhcJj8u9lsTX3NFR5i29Mqhc9ku/tDpP5PgOi57xIL6X+wdCCdyvJ5hhDy
5+
2zTzE5+kJ7Plewa+4tI=
6+
-----END ENCRYPTED PRIVATE KEY-----

0 commit comments

Comments
 (0)