Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pkcs8/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
125 changes: 100 additions & 25 deletions pkcs8/src/private_key_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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<Params, Key, PubKey> {
pub struct PrivateKeyInfo<Params, Key, PubKey, Attr> {
/// X.509 `AlgorithmIdentifier` for the private key type.
pub algorithm: AlgorithmIdentifier<Params>,

Expand All @@ -103,9 +104,12 @@ pub struct PrivateKeyInfo<Params, Key, PubKey> {

/// Public key data, optionally available if version is V2.
pub public_key: Option<PubKey>,

/// Attributes
pub attributes: Option<Attr>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read the RFC5208 correctly, attributes are defined as:

   Attributes ::= SET OF Attribute

I believe the Attribute itself comes from X501 (section 8.2):

Attribute {ATTRIBUTE:SupportedAttributes} ::= SEQUENCE {
 type ATTRIBUTE.&id({SupportedAttributes}),
 values SET SIZE (0..MAX) OF ATTRIBUTE.&Type({SupportedAttributes}{@type}),
 valuesWithContext SET SIZE (1..MAX) OF SEQUENCE {
 value ATTRIBUTE.&Type({SupportedAttributes}{@type}),
 contextList SET SIZE (1..MAX) OF Context,
 ...} OPTIONAL,
 ... }

I believe this is the same structure as the one in x509: https://docs.rs/x509-cert/latest/x509_cert/attr/struct.Attribute.html

Ideally we'd have an AttributeRef<'a> and an Attribute somewhere we could reuse, and attributes would be SetOf::<AttributeRef<'a>>, sadly we don't have that.

But I'd love if we could drop the changes on the der side. I'm not sure I agree with the fallback on Raw bytes really.

Something like:

    #[derive(Copy, Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)]
    pub struct AttributeTypeAndValueRef<'a> {
        pub oid: ObjectIdentifier,
        pub value: AnyRef<'a>,
    }
    #[derive(Copy, Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)]
    pub struct AttributeTypeAndValue {
        pub oid: ObjectIdentifier,
        pub value: Any,
    }

    trait SetOfAttrLike {}
    impl SetOfAttrLike for SetOf::<AttributeTypeAndValueRef<'_>> {}
    impl SetOfAttrLike for SetOfVec::<AttributeTypeAndValue> {}

Maybe?

Copy link
Copy Markdown
Member

@tarcieri tarcieri Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At one point we had an x501 crate to define it: #377

I think it would be annoying to add a whole separate dependency to pkcs8 just to provide Attribute though, unless it were optional.

I agree this PR seems a bit large solely for accomplishing the stated goal, though a SetOfRef could potentially be useful for other purposes, but should probably be added in its own separate PR to der first before we go that way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the superseding RFC 5958 it says:
The attributes field uses the class ATTRIBUTE which is restricted by the OneAsymmetricKeyAttributes information object set. OneAsymmetricKeyAttributes is an open ended set in this document. Others documents can constrain these values.

But I agree, that restricting to x509 attributes is a good choice here.

Restricting to AttributeTypeAndValue will not be sufficient, as e.g. OpenSSL parsing routines for PrivateKeyInfo expect an Attribute (with a set of values). Also, the problem with SetOf in the no_std case is, that it always requires a const generic size N, which then spreads over PrivateKeyInfo everywhere.

Should I open up a separate PR to implement SetOfRef first?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, splitting out the SetOfRef stuff so it can be reviewed separately would be great.

I think it's fine to have duplication for now, and if it becomes a problem we can revive the x501 crate (which I guess was removed in #445)

}

impl<Params, Key, PubKey> PrivateKeyInfo<Params, Key, PubKey> {
impl<Params, Key, PubKey, Attr> PrivateKeyInfo<Params, Key, PubKey, Attr> {
/// Create a new PKCS#8 [`PrivateKeyInfo`] message.
///
/// This is a helper method which initializes `attributes` and `public_key`
Expand All @@ -115,6 +119,7 @@ impl<Params, Key, PubKey> PrivateKeyInfo<Params, Key, PubKey> {
algorithm,
private_key,
public_key: None,
attributes: None,
}
}

Expand All @@ -130,13 +135,15 @@ impl<Params, Key, PubKey> PrivateKeyInfo<Params, Key, PubKey> {
}
}

impl<'a, Params, Key, PubKey> PrivateKeyInfo<Params, Key, PubKey>
impl<'a, Params, Key, PubKey, Attr> PrivateKeyInfo<Params, Key, PubKey, Attr>
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.
Expand Down Expand Up @@ -170,7 +177,7 @@ where
}
}

impl<'a, Params, Key, PubKey> PrivateKeyInfo<Params, Key, PubKey>
impl<'a, Params, Key, PubKey, Attr> PrivateKeyInfo<Params, Key, PubKey, Attr>
where
Params: der::Choice<'a> + Encode,
PubKey: BitStringLike,
Expand All @@ -188,11 +195,30 @@ where
}
}

impl<'a, Params, Key, PubKey> DecodeValue<'a> for PrivateKeyInfo<Params, Key, PubKey>
impl<'a, Params, Key, PubKey, Attr> PrivateKeyInfo<Params, Key, PubKey, Attr>
where
Params: der::Choice<'a> + Encode,
Attr: SetOfLike<'a>,
{
/// Get a `SET OF` representation of the attributes, if present.
fn attributes_string(&self) -> Option<ContextSpecific<SetOfRef<'a, Attr::Item>>> {
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<Params, Key, PubKey, Attr>
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;

Expand All @@ -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::<Attr>(ATTRIBUTES_TAG, TagMode::Implicit)?;

let public_key = reader.context_specific::<PubKey>(PUBLIC_KEY_TAG, TagMode::Implicit)?;

Expand All @@ -226,49 +251,58 @@ where
algorithm,
private_key,
public_key,
attributes,
})
}
}

impl<'a, Params, Key, PubKey> EncodeValue for PrivateKeyInfo<Params, Key, PubKey>
impl<'a, Params, Key, PubKey, Attr> EncodeValue for PrivateKeyInfo<Params, Key, PubKey, Attr>
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<Length> {
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()?
}

fn encode_value(&self, writer: &mut impl Writer) -> der::Result<()> {
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<Params, Key, PubKey>
impl<'a, Params, Key, PubKey, Attr> Sequence<'a> for PrivateKeyInfo<Params, Key, PubKey, Attr>
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<Params, Key, PubKey>
impl<'a, Params, Key, PubKey, Attr> TryFrom<&'a [u8]> for PrivateKeyInfo<Params, Key, PubKey, Attr>
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;

Expand All @@ -277,63 +311,72 @@ where
}
}

impl<Params, Key, PubKey> fmt::Debug for PrivateKeyInfo<Params, Key, PubKey>
impl<Params, Key, PubKey, Attr> fmt::Debug for PrivateKeyInfo<Params, Key, PubKey, Attr>
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<PrivateKeyInfo<Params, Key, PubKey>> for SecretDocument
impl<'a, Params, Key, PubKey, Attr> TryFrom<PrivateKeyInfo<Params, Key, PubKey, Attr>>
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<Params, Key, PubKey>) -> Result<SecretDocument> {
fn try_from(private_key: PrivateKeyInfo<Params, Key, PubKey, Attr>) -> Result<SecretDocument> {
SecretDocument::try_from(&private_key)
}
}

#[cfg(feature = "alloc")]
impl<'a, Params, Key, PubKey> TryFrom<&PrivateKeyInfo<Params, Key, PubKey>> for SecretDocument
impl<'a, Params, Key, PubKey, Attr> TryFrom<&PrivateKeyInfo<Params, Key, PubKey, Attr>>
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<Params, Key, PubKey>) -> Result<SecretDocument> {
fn try_from(private_key: &PrivateKeyInfo<Params, Key, PubKey, Attr>) -> Result<SecretDocument> {
Ok(Self::encode_msg(private_key)?)
}
}

#[cfg(feature = "pem")]
impl<Params, Key, PubKey> PemLabel for PrivateKeyInfo<Params, Key, PubKey> {
impl<Params, Key, PubKey, Attr> PemLabel for PrivateKeyInfo<Params, Key, PubKey, Attr> {
const PEM_LABEL: &'static str = "PRIVATE KEY";
}

#[cfg(feature = "subtle")]
impl<Params, Key, PubKey> ConstantTimeEq for PrivateKeyInfo<Params, Key, PubKey>
impl<Params, Key, PubKey, Attr> ConstantTimeEq for PrivateKeyInfo<Params, Key, PubKey, Attr>
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
Expand All @@ -346,32 +389,35 @@ where
}

#[cfg(feature = "subtle")]
impl<Params, Key, PubKey> Eq for PrivateKeyInfo<Params, Key, PubKey>
impl<Params, Key, PubKey, Attr> Eq for PrivateKeyInfo<Params, Key, PubKey, Attr>
where
Params: Eq,
Key: AsRef<[u8]> + Eq,
PubKey: Eq,
Attr: Eq,
{
}

#[cfg(feature = "subtle")]
impl<Params, Key, PubKey> PartialEq for PrivateKeyInfo<Params, Key, PubKey>
impl<Params, Key, PubKey, Attr> PartialEq for PrivateKeyInfo<Params, Key, PubKey, Attr>
where
Params: Eq,
Key: PartialEq + AsRef<[u8]>,
PubKey: PartialEq,
Attr: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}

/// [`PrivateKeyInfo`] with [`AnyRef`] algorithm parameters, and `&[u8]` key.
pub type PrivateKeyInfoRef<'a> = PrivateKeyInfo<AnyRef<'a>, &'a OctetStringRef, BitStringRef<'a>>;
pub type PrivateKeyInfoRef<'a> =
PrivateKeyInfo<AnyRef<'a>, &'a OctetStringRef, BitStringRef<'a>, SetOfRef<'a, Attribute>>;

/// [`PrivateKeyInfo`] with [`Any`] algorithm parameters, and `Box<[u8]>` key.
#[cfg(feature = "alloc")]
pub type PrivateKeyInfoOwned = PrivateKeyInfo<Any, OctetString, BitString>;
pub type PrivateKeyInfoOwned = PrivateKeyInfo<Any, OctetString, BitString, SetOfVec<Attribute>>;

/// [`BitStringLike`] marks object that will act like a BitString.
///
Expand All @@ -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::*;
Expand All @@ -399,13 +461,25 @@ mod allocating {
}
}

impl<'a, T> SetOfLike<'a> for &'a SetOfVec<T>
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 {
PrivateKeyInfoOwned {
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(),
}
}
}
Expand All @@ -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(),
}
}
}
Expand Down
Binary file added pkcs8/tests/examples/p256-priv-attributes.der
Binary file not shown.
Loading
Loading