diff --git a/Cargo.lock b/Cargo.lock index 73f180e05..295c4f854 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1108,6 +1108,7 @@ dependencies = [ "spki", "subtle", "tempfile", + "x509-cert", ] [[package]] diff --git a/pkcs8/Cargo.toml b/pkcs8/Cargo.toml index 12224c477..456212525 100644 --- a/pkcs8/Cargo.toml +++ b/pkcs8/Cargo.toml @@ -17,8 +17,9 @@ edition = "2024" rust-version = "1.85" [dependencies] -der = { version = "0.8.0-rc.12", features = ["oid"] } +der = { version = "0.8", features = ["oid"] } spki = "0.8" +x509-cert = { version = "0.3.0-rc.4", default-features = false } # optional dependencies rand_core = { version = "0.10", optional = true, default-features = false } diff --git a/pkcs8/src/private_key_info.rs b/pkcs8/src/private_key_info.rs index 9ee713acb..c7a66c4b8 100644 --- a/pkcs8/src/private_key_info.rs +++ b/pkcs8/src/private_key_info.rs @@ -3,16 +3,17 @@ use crate::{Error, Result, Version}; use core::fmt; use der::{ - Decode, DecodeValue, Encode, EncodeValue, FixedTag, Header, Length, Reader, Sequence, TagMode, - TagNumber, Writer, - asn1::{AnyRef, BitStringRef, ContextSpecific, OctetStringRef, SequenceRef}, + Decode, DecodeValue, DerOrd, Encode, EncodeValue, FixedTag, Header, Length, Reader, Sequence, + TagMode, TagNumber, Writer, + asn1::{AnyRef, BitStringRef, ContextSpecific, OctetStringRef, SetOfRef}, }; use spki::AlgorithmIdentifier; +use x509_cert::attr::Attribute; #[cfg(feature = "alloc")] use der::{ SecretDocument, - asn1::{Any, BitString, OctetString}, + asn1::{Any, BitString, OctetString, SetOfVec}, }; #[cfg(feature = "encryption")] @@ -94,7 +95,7 @@ const PUBLIC_KEY_TAG: TagNumber = TagNumber(1); /// [RFC 5208 Section 5]: https://tools.ietf.org/html/rfc5208#section-5 /// [RFC 5958 Section 2]: https://datatracker.ietf.org/doc/html/rfc5958#section-2 #[derive(Clone)] -pub struct PrivateKeyInfo { +pub struct PrivateKeyInfo { /// X.509 `AlgorithmIdentifier` for the private key type. pub algorithm: AlgorithmIdentifier, @@ -103,9 +104,12 @@ pub struct PrivateKeyInfo { /// Public key data, optionally available if version is V2. pub public_key: Option, + + /// Attributes + pub attributes: Option, } -impl PrivateKeyInfo { +impl PrivateKeyInfo { /// Create a new PKCS#8 [`PrivateKeyInfo`] message. /// /// This is a helper method which initializes `attributes` and `public_key` @@ -115,6 +119,7 @@ impl PrivateKeyInfo { algorithm, private_key, public_key: None, + attributes: None, } } @@ -130,13 +135,15 @@ impl PrivateKeyInfo { } } -impl<'a, Params, Key, PubKey> PrivateKeyInfo +impl<'a, Params, Key, PubKey, Attr> PrivateKeyInfo where Params: der::Choice<'a, Error = der::Error> + Encode, Key: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, Key: EncodeValue, PubKey: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, PubKey: BitStringLike, + Attr: DecodeValue<'a, Error = der::Error> + FixedTag + SetOfLike<'a> + 'a, + Attr::Item: Clone + Decode<'a> + DerOrd + Encode, { /// Encrypt this private key using a symmetric encryption key derived /// from the provided password. @@ -170,7 +177,7 @@ where } } -impl<'a, Params, Key, PubKey> PrivateKeyInfo +impl<'a, Params, Key, PubKey, Attr> PrivateKeyInfo where Params: der::Choice<'a> + Encode, PubKey: BitStringLike, @@ -188,11 +195,30 @@ where } } -impl<'a, Params, Key, PubKey> DecodeValue<'a> for PrivateKeyInfo +impl<'a, Params, Key, PubKey, Attr> PrivateKeyInfo +where + Params: der::Choice<'a> + Encode, + Attr: SetOfLike<'a>, +{ + /// Get a `SET OF` representation of the attributes, if present. + fn attributes_string(&self) -> Option>> { + self.attributes.as_ref().map(|attributes| { + let value = attributes.as_set_of(); + ContextSpecific { + tag_number: ATTRIBUTES_TAG, + tag_mode: TagMode::Implicit, + value, + } + }) + } +} + +impl<'a, Params, Key, PubKey, Attr> DecodeValue<'a> for PrivateKeyInfo where Params: der::Choice<'a, Error = der::Error> + Encode, Key: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, PubKey: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, + Attr: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, { type Error = der::Error; @@ -202,8 +228,7 @@ where let algorithm = reader.decode()?; let private_key = Key::decode(reader)?; - let _attributes = - reader.context_specific::<&SequenceRef>(ATTRIBUTES_TAG, TagMode::Implicit)?; + let attributes = reader.context_specific::(ATTRIBUTES_TAG, TagMode::Implicit)?; let public_key = reader.context_specific::(PUBLIC_KEY_TAG, TagMode::Implicit)?; @@ -226,20 +251,24 @@ where algorithm, private_key, public_key, + attributes, }) } } -impl<'a, Params, Key, PubKey> EncodeValue for PrivateKeyInfo +impl<'a, Params, Key, PubKey, Attr> EncodeValue for PrivateKeyInfo where Params: der::Choice<'a, Error = der::Error> + Encode, Key: EncodeValue + FixedTag, PubKey: BitStringLike, + Attr: SetOfLike<'a>, + Attr::Item: Clone + Decode<'a> + DerOrd + Encode, { fn value_len(&self) -> der::Result { self.version().encoded_len()? + self.algorithm.encoded_len()? + self.private_key.encoded_len()? + + self.attributes_string().encoded_len()? + self.public_key_bit_string().encoded_len()? } @@ -247,28 +276,33 @@ where self.version().encode(writer)?; self.algorithm.encode(writer)?; self.private_key.encode(writer)?; + self.attributes_string().encode(writer)?; self.public_key_bit_string().encode(writer)?; Ok(()) } } -impl<'a, Params, Key, PubKey> Sequence<'a> for PrivateKeyInfo +impl<'a, Params, Key, PubKey, Attr> Sequence<'a> for PrivateKeyInfo where Params: der::Choice<'a, Error = der::Error> + Encode, Key: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, Key: EncodeValue, PubKey: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, PubKey: BitStringLike, + Attr: DecodeValue<'a, Error = der::Error> + FixedTag + SetOfLike<'a> + 'a, + Attr::Item: Clone + Decode<'a> + DerOrd + Encode, { } -impl<'a, Params, Key, PubKey> TryFrom<&'a [u8]> for PrivateKeyInfo +impl<'a, Params, Key, PubKey, Attr> TryFrom<&'a [u8]> for PrivateKeyInfo where Params: der::Choice<'a, Error = der::Error> + Encode, Key: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, Key: EncodeValue, PubKey: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, PubKey: BitStringLike, + Attr: DecodeValue<'a, Error = der::Error> + FixedTag + SetOfLike<'a> + 'a, + Attr::Item: Clone + Decode<'a> + DerOrd + Encode, { type Error = Error; @@ -277,63 +311,72 @@ where } } -impl fmt::Debug for PrivateKeyInfo +impl fmt::Debug for PrivateKeyInfo where Params: fmt::Debug, PubKey: fmt::Debug, + Attr: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("PrivateKeyInfo") .field("version", &self.version()) .field("algorithm", &self.algorithm) + .field("Attributes", &self.attributes) .field("public_key", &self.public_key) .finish_non_exhaustive() } } #[cfg(feature = "alloc")] -impl<'a, Params, Key, PubKey> TryFrom> for SecretDocument +impl<'a, Params, Key, PubKey, Attr> TryFrom> + for SecretDocument where Params: der::Choice<'a, Error = der::Error> + Encode, Key: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, Key: EncodeValue, PubKey: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, PubKey: BitStringLike, + Attr: DecodeValue<'a, Error = der::Error> + FixedTag + SetOfLike<'a> + 'a, + Attr::Item: Clone + Decode<'a> + DerOrd + Encode, { type Error = Error; - fn try_from(private_key: PrivateKeyInfo) -> Result { + fn try_from(private_key: PrivateKeyInfo) -> Result { SecretDocument::try_from(&private_key) } } #[cfg(feature = "alloc")] -impl<'a, Params, Key, PubKey> TryFrom<&PrivateKeyInfo> for SecretDocument +impl<'a, Params, Key, PubKey, Attr> TryFrom<&PrivateKeyInfo> + for SecretDocument where Params: der::Choice<'a, Error = der::Error> + Encode, Key: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, Key: EncodeValue, PubKey: DecodeValue<'a, Error = der::Error> + FixedTag + 'a, PubKey: BitStringLike, + Attr: DecodeValue<'a, Error = der::Error> + FixedTag + SetOfLike<'a> + 'a, + Attr::Item: Clone + Decode<'a> + DerOrd + Encode, { type Error = Error; - fn try_from(private_key: &PrivateKeyInfo) -> Result { + fn try_from(private_key: &PrivateKeyInfo) -> Result { Ok(Self::encode_msg(private_key)?) } } #[cfg(feature = "pem")] -impl PemLabel for PrivateKeyInfo { +impl PemLabel for PrivateKeyInfo { const PEM_LABEL: &'static str = "PRIVATE KEY"; } #[cfg(feature = "subtle")] -impl ConstantTimeEq for PrivateKeyInfo +impl ConstantTimeEq for PrivateKeyInfo where Params: Eq, Key: PartialEq + AsRef<[u8]>, PubKey: PartialEq, + Attr: PartialEq, { fn ct_eq(&self, other: &Self) -> Choice { // NOTE: public fields are not compared in constant time @@ -346,20 +389,22 @@ where } #[cfg(feature = "subtle")] -impl Eq for PrivateKeyInfo +impl Eq for PrivateKeyInfo where Params: Eq, Key: AsRef<[u8]> + Eq, PubKey: Eq, + Attr: Eq, { } #[cfg(feature = "subtle")] -impl PartialEq for PrivateKeyInfo +impl PartialEq for PrivateKeyInfo where Params: Eq, Key: PartialEq + AsRef<[u8]>, PubKey: PartialEq, + Attr: PartialEq, { fn eq(&self, other: &Self) -> bool { self.ct_eq(other).into() @@ -367,11 +412,12 @@ where } /// [`PrivateKeyInfo`] with [`AnyRef`] algorithm parameters, and `&[u8]` key. -pub type PrivateKeyInfoRef<'a> = PrivateKeyInfo, &'a OctetStringRef, BitStringRef<'a>>; +pub type PrivateKeyInfoRef<'a> = + PrivateKeyInfo, &'a OctetStringRef, BitStringRef<'a>, SetOfRef<'a, Attribute>>; /// [`PrivateKeyInfo`] with [`Any`] algorithm parameters, and `Box<[u8]>` key. #[cfg(feature = "alloc")] -pub type PrivateKeyInfoOwned = PrivateKeyInfo; +pub type PrivateKeyInfoOwned = PrivateKeyInfo>; /// [`BitStringLike`] marks object that will act like a BitString. /// @@ -386,6 +432,22 @@ impl BitStringLike for BitStringRef<'_> { } } +pub trait SetOfLike<'a> { + type Item: Clone + DerOrd + Encode; + fn as_set_of(&self) -> SetOfRef<'a, Self::Item>; +} + +impl<'a, T> SetOfLike<'a> for SetOfRef<'a, T> +where + T: Clone + DerOrd + Encode, +{ + type Item = T; + + fn as_set_of(&self) -> SetOfRef<'a, T> { + SetOfRef::from(self) + } +} + #[cfg(feature = "alloc")] mod allocating { use super::*; @@ -399,6 +461,17 @@ mod allocating { } } + impl<'a, T> SetOfLike<'a> for &'a SetOfVec + where + T: Clone + DerOrd + Encode + 'a, + { + type Item = T; + + fn as_set_of(&self) -> SetOfRef<'a, T> { + self.owned_to_ref() + } + } + impl<'a> RefToOwned<'a> for PrivateKeyInfoRef<'a> { type Owned = PrivateKeyInfoOwned; fn ref_to_owned(&self) -> Self::Owned { @@ -406,6 +479,7 @@ mod allocating { algorithm: self.algorithm.ref_to_owned(), private_key: self.private_key.to_owned(), public_key: self.public_key.ref_to_owned(), + attributes: self.attributes.ref_to_owned(), } } } @@ -417,6 +491,7 @@ mod allocating { algorithm: self.algorithm.owned_to_ref(), private_key: self.private_key.borrow(), public_key: self.public_key.owned_to_ref(), + attributes: self.attributes.owned_to_ref(), } } } diff --git a/pkcs8/tests/examples/p256-priv-attributes.der b/pkcs8/tests/examples/p256-priv-attributes.der new file mode 100644 index 000000000..29569c190 Binary files /dev/null and b/pkcs8/tests/examples/p256-priv-attributes.der differ diff --git a/pkcs8/tests/private_key.rs b/pkcs8/tests/private_key.rs index 14e012b4e..ee6d7f37c 100644 --- a/pkcs8/tests/private_key.rs +++ b/pkcs8/tests/private_key.rs @@ -1,6 +1,6 @@ //! PKCS#8 private key tests -use der::asn1::{ObjectIdentifier, OctetStringRef}; +use der::asn1::{ObjectIdentifier, OctetStringRef, PrintableString}; use hex_literal::hex; use pkcs8::{PrivateKeyInfoRef, Version}; @@ -13,6 +13,9 @@ use der::{EncodePem, pem::LineEnding}; /// Elliptic Curve (P-256) PKCS#8 private key encoded as ASN.1 DER const EC_P256_DER_EXAMPLE: &[u8] = include_bytes!("examples/p256-priv.der"); +/// Elliptic Curve (P-256) PKCS#8 private key containing Attributes encoded as ASN.1 DER +const EC_P256_ATTRIBUTES_DER_EXAMPLE: &[u8] = include_bytes!("examples/p256-priv-attributes.der"); + /// Elliptic Curve (Bign P-256) PKCS#8 private key encoded as ASN.1 DER const EC_BIGN_P256_DER_EXAMPLE: &[u8] = include_bytes!("examples/bign256-priv.der"); @@ -73,6 +76,72 @@ fn decode_ec_p256_der() { ); } +#[test] +fn decode_ec_p256_attr_der() { + let pk = PrivateKeyInfoRef::try_from(EC_P256_ATTRIBUTES_DER_EXAMPLE).unwrap(); + + assert_eq!(pk.version(), Version::V2); + assert_eq!(pk.algorithm.oid, "1.2.840.10045.2.1".parse().unwrap()); + + assert_eq!( + pk.algorithm + .parameters + .unwrap() + .decode_as::() + .unwrap(), + "1.2.840.10045.3.1.7".parse().unwrap() + ); + + // Extracted with: + // $ openssl asn1parse -inform der -in tests/examples/p256-priv.der + assert_eq!( + pk.private_key.as_ref(), + &hex!("4BFAE55747FC35CA8F58CAD45B6C2827960007C790C25CD1662411EEDBDBA5F5")[..] + ); + + // Check for Common name OID + assert_eq!( + pk.attributes.as_ref().unwrap().get(0).unwrap().oid, + "2.5.4.3".parse().unwrap() + ); + + // Check for Organization OID + assert_eq!( + pk.attributes.as_ref().unwrap().get(1).unwrap().oid, + "2.5.4.10".parse().unwrap() + ); + + assert_eq!( + pk.attributes + .as_ref() + .unwrap() + .get(0) + .unwrap() + .values + .get(0) + .unwrap() + .decode_as::() + .unwrap() + .as_bytes(), + b"TestCN" + ); + + assert_eq!( + pk.attributes + .as_ref() + .unwrap() + .get(1) + .unwrap() + .values + .get(0) + .unwrap() + .decode_as::() + .unwrap() + .as_bytes(), + b"TestOrg" + ); +} + #[test] fn decode_ec_bignp256_der() { let pk = PrivateKeyInfoRef::try_from(EC_BIGN_P256_DER_EXAMPLE).unwrap();