Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds SignedData type to pkcs7 #813

Merged
merged 17 commits into from
Dec 29, 2022
Merged
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 pkcs7/Cargo.toml
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ rust-version = "1.65"
[dependencies]
der = { version = "=0.7.0-pre", features = ["oid"], path = "../der" }
spki = { version = "=0.7.0-pre", path = "../spki" }
x509-cert = { version = "=0.2.0-pre", path = "../x509-cert" }

[dev-dependencies]
hex-literal = "0.3"
58 changes: 58 additions & 0 deletions pkcs7/src/certificate_choices.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! `CertificateChoices` [RFC 5652 10.2.2](https://datatracker.ietf.org/doc/html/rfc5652#section-10.2.2)
use der::{asn1::BitStringRef, AnyRef, Choice, Sequence, ValueOrd};
use spki::ObjectIdentifier;
use x509_cert::Certificate;

// TODO (smndtrl): Should come from x509 - for now I haven't found a test case in real world
type AttributeCertificateV1<'a> = BitStringRef<'a>;
type AttributeCertificateV2<'a> = BitStringRef<'a>;
type ExtendedCertificate<'a> = BitStringRef<'a>;

/// ```text
/// OtherCertificateFormat ::= SEQUENCE {
/// otherCertFormat OBJECT IDENTIFIER,
/// otherCert ANY DEFINED BY otherCertFormat }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Sequence, ValueOrd)]
pub struct OtherCertificateFormat<'a> {
other_cert_format: ObjectIdentifier,
other_cert: AnyRef<'a>,
}

/// ```text
/// CertificateChoices ::= CHOICE {
/// certificate Certificate,
/// extendedCertificate [0] IMPLICIT ExtendedCertificate, -- Obsolete
/// v1AttrCert [1] IMPLICIT AttributeCertificateV1, -- Obsolete
/// v2AttrCert [2] IMPLICIT AttributeCertificateV2,
/// other [3] IMPLICIT OtherCertificateFormat }
///
/// OtherCertificateFormat ::= SEQUENCE {
/// otherCertFormat OBJECT IDENTIFIER,
/// otherCert ANY DEFINED BY otherCertFormat }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Choice, ValueOrd)]
#[allow(clippy::large_enum_variant)]
pub enum CertificateChoices<'a> {
/// X.509 certificate
Certificate(Certificate<'a>),

/// PKCS #6 extended certificate (obsolete)
#[deprecated]
#[asn1(context_specific = "0", tag_mode = "IMPLICIT")]
ExtendedCertificate(ExtendedCertificate<'a>),

/// version 1 X.509 attribute certificate (ACv1) X.509-97 (obsolete)
#[deprecated]
#[asn1(context_specific = "1", tag_mode = "IMPLICIT")]
V1AttrCert(AttributeCertificateV1<'a>),

/// version 2 X.509 attribute certificate (ACv2) X.509-00
#[asn1(context_specific = "2", tag_mode = "IMPLICIT")]
V2AttrCert(AttributeCertificateV2<'a>),

/// any other certificate forma
#[asn1(context_specific = "3", tag_mode = "IMPLICIT")]
Other(OtherCertificateFormat<'a>),
}
29 changes: 29 additions & 0 deletions pkcs7/src/cms_version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! `CMSVersion` [RFC 5652 § 10.2.5](https://datatracker.ietf.org/doc/html/rfc5652#section-10.2.5)
use der::Enumerated;

/// The CMSVersion type gives a syntax version number, for compatibility
/// with future revisions of this specification.
/// ```text
/// CMSVersion ::= INTEGER
/// { v0(0), v1(1), v2(2), v3(3), v4(4), v5(5) }
/// ```
///
/// See [RFC 5652 10.2.5](https://datatracker.ietf.org/doc/html/rfc5652#section-10.2.5).
#[derive(Clone, Copy, Debug, Enumerated, Eq, PartialEq)]
#[asn1(type = "INTEGER")]
#[repr(u8)]
pub enum CmsVersion {
/// syntax version 0
V0 = 0,
/// syntax version 1
V1 = 1,
/// syntax version 2
V2 = 2,
/// syntax version 3
V3 = 3,
/// syntax version 4
V4 = 4,
/// syntax version 5
V5 = 5,
}
25 changes: 23 additions & 2 deletions pkcs7/src/content_info.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::{data_content::DataContent, encrypted_data_content::EncryptedDataContent, ContentType};
use crate::{
data_content::DataContent, encrypted_data_content::EncryptedDataContent,
signed_data_content::SignedDataContent, ContentType,
};

use der::{
asn1::{ContextSpecific, OctetStringRef},
@@ -22,8 +25,10 @@ pub enum ContentInfo<'a> {
/// Content type `encrypted-data`
EncryptedData(Option<EncryptedDataContent<'a>>),

/// Content type `signed-data`
SignedData(Option<SignedDataContent<'a>>),

/// Catch-all case for content types that are not explicitly supported
/// - signed-data
/// - enveloped-data
/// - signed-and-enveloped-data
/// - digested-data
@@ -36,6 +41,7 @@ impl<'a> ContentInfo<'a> {
match self {
Self::Data(_) => ContentType::Data,
Self::EncryptedData(_) => ContentType::EncryptedData,
Self::SignedData(_) => ContentType::SignedData,
Self::Other((content_type, _)) => *content_type,
}
}
@@ -52,6 +58,7 @@ impl<'a> ContentInfo<'a> {
match content_type {
ContentType::Data => ContentInfo::Data(None),
ContentType::EncryptedData => ContentInfo::EncryptedData(None),
ContentType::SignedData => ContentInfo::SignedData(None),
_ => ContentInfo::Other((content_type, None)),
}
}
@@ -76,6 +83,12 @@ impl<'a> DecodeValue<'a> for ContentInfo<'a> {
ContentType::EncryptedData => Ok(ContentInfo::EncryptedData(
reader.context_specific(CONTENT_TAG, TagMode::Explicit)?,
)),
ContentType::SignedData => Ok(ContentInfo::SignedData(
reader.context_specific::<SignedDataContent<'_>>(
CONTENT_TAG,
TagMode::Explicit,
)?,
)),
_ => Ok(ContentInfo::Other((
content_type,
reader
@@ -108,6 +121,14 @@ impl<'a> Sequence<'a> for ContentInfo<'a> {
value: *d,
}),
]),
Self::SignedData(data) => f(&[
&self.content_type(),
&data.as_ref().map(|d| ContextSpecific {
tag_number: CONTENT_TAG,
tag_mode: TagMode::Explicit,
value: d.clone(),
}),
]),
Self::Other((content_type, opt_oct_str)) => f(&[
content_type,
&opt_oct_str.as_ref().map(|d| ContextSpecific {
29 changes: 29 additions & 0 deletions pkcs7/src/encapsulated_content_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! `encapsulated-data` content type [RFC 5652 § 5.2](https://datatracker.ietf.org/doc/html/rfc5652#section-5.2)
use der::{AnyRef, Sequence};
use spki::ObjectIdentifier;

/// Encapsulated content information [RFC 5652 § 5.2](https://datatracker.ietf.org/doc/html/rfc5652#section-5.2)
///
/// ```text
/// EncapsulatedContentInfo ::= SEQUENCE {
/// eContentType ContentType,
/// eContent [0] EXPLICIT OCTET STRING OPTIONAL }
/// ```
/// Due to a difference in PKCS #7 and CMS the contents type can be either
/// ```text
/// content [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL
/// ```
/// or
/// ```text
/// eContent [0] EXPLICIT OCTET STRING OPTIONAL
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)]
pub struct EncapsulatedContentInfo<'a> {
/// indicates the type of content.
pub e_content_type: ObjectIdentifier,

/// encapsulated content
#[asn1(context_specific = "0", optional = "true", tag_mode = "EXPLICIT")]
pub e_content: Option<AnyRef<'a>>,
}
6 changes: 6 additions & 0 deletions pkcs7/src/lib.rs
Original file line number Diff line number Diff line change
@@ -13,9 +13,15 @@ mod content_type;

pub use crate::{content_info::ContentInfo, content_type::ContentType};

pub mod certificate_choices;
pub mod cms_version;
pub mod data_content;
pub mod encapsulated_content_info;
pub mod encrypted_data_content;
pub mod enveloped_data_content;
pub mod revocation_info_choices;
pub mod signed_data_content;
pub mod signer_info;

use der::asn1::ObjectIdentifier;

52 changes: 52 additions & 0 deletions pkcs7/src/revocation_info_choices.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! `RevocationInfoChoices` [RFC 5652 10.2.1](https://datatracker.ietf.org/doc/html/rfc5652#section-10.2.1)
use core::cmp::Ordering;

use der::{asn1::SetOfVec, AnyRef, Choice, Sequence, ValueOrd};
use spki::ObjectIdentifier;
use x509_cert::crl::CertificateList;

/// ```text
/// RevocationInfoChoices ::= SET OF RevocationInfoChoice
/// RevocationInfoChoice ::= CHOICE {
/// crl CertificateList,
/// other [1] IMPLICIT OtherRevocationInfoFormat }
/// OtherRevocationInfoFormat ::= SEQUENCE {
/// otherRevInfoFormat OBJECT IDENTIFIER,
/// otherRevInfo ANY DEFINED BY otherRevInfoFormat }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Choice)]
#[allow(clippy::large_enum_variant)]
pub enum RevocationInfoChoice<'a> {
/// The CertificateList type gives a certificate revocation list (CRL).
Crl(CertificateList<'a>),

/// The OtherRevocationInfoFormat alternative is provided to support any
/// other revocation information format without further modifications to
/// the CMS.
#[asn1(context_specific = "1", tag_mode = "IMPLICIT", constructed = "true")]
Other(OtherRevocationInfoFormat<'a>),
}

/// ```text
/// RevocationInfoChoices ::= SET OF RevocationInfoChoice
/// ```
pub type RevocationInfoChoices<'a> = SetOfVec<RevocationInfoChoice<'a>>;

/// ```text
/// OtherRevocationInfoFormat ::= SEQUENCE {
/// otherRevInfoFormat OBJECT IDENTIFIER,
/// otherRevInfo ANY DEFINED BY otherRevInfoFormat }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Sequence)]
pub struct OtherRevocationInfoFormat<'a> {
other_rev_info_format: ObjectIdentifier,
other_rev_info: AnyRef<'a>,
}

// TODO: figure out what ordering makes sense - if any
impl ValueOrd for RevocationInfoChoice<'_> {
fn value_cmp(&self, _other: &Self) -> der::Result<Ordering> {
Ok(Ordering::Equal)
}
}
58 changes: 58 additions & 0 deletions pkcs7/src/signed_data_content.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! `signed-data` content type [RFC 5652 § 5](https://datatracker.ietf.org/doc/html/rfc5652#section-5)
use crate::{
certificate_choices::CertificateChoices, cms_version::CmsVersion,
encapsulated_content_info::EncapsulatedContentInfo,
revocation_info_choices::RevocationInfoChoices, signer_info::SignerInfos,
};
use der::{asn1::SetOfVec, Sequence};
use spki::AlgorithmIdentifierRef;

/// ```text
/// DigestAlgorithmIdentifier ::= AlgorithmIdentifier
/// ```
type DigestAlgorithmIdentifier<'a> = AlgorithmIdentifierRef<'a>;

/// ```text
/// DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier
/// ```
type DigestAlgorithmIdentifiers<'a> = SetOfVec<DigestAlgorithmIdentifier<'a>>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I did something weird locally, but I'm surprised this isn't complaining about the lack of the alloc feature from the der crate. Did you build with all features enabled locally?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that you say it I'm wondering as well. I ran everything without additional features enabled.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, it built for me locally without additional features as well, strange.

Copy link
Member

@tarcieri tarcieri Dec 20, 2022

Choose a reason for hiding this comment

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

You're pulling in the x509-cert crate which unconditionally activates the alloc feature, and feature unification means it's activated here too.

It would probably be good to factor the relevant parts into a common crate which can be shared. We had the x501 crate for this purpose at one point, although I'm not sure that's quite what you need. Edit: oh wait, you need Certificate. Never mind.

Since that's the case, you might as well unconditionally link liballoc and activate the alloc feature of the der crate directly for clarity.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for explaining!

If there is interest in maintaining a non-alloc part of pkcs7, this is how I did it in my own changes:

[features]
alloc = ["der/alloc"]
std = ["alloc"]
signed-data = ["alloc", "dep:x509-cert"]

(with x509-cert then being marked as optional.)

Copy link
Member

Choose a reason for hiding this comment

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

I'm guessing that, like #765, this crate should probably move to all owned types and make alloc a hard dependency, since heapless support probably won't be practical for real-world use cases

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What should we do for now? I don't want to mix this PR with the larger effort of doing something like #765 for this crate as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm +1 on doing the owned refactor in a follow-up (I can help with that) to keep the diff small 🙂


/// ```text
/// CertificateSet ::= SET OF CertificateChoices
/// ```
type CertificateSet<'a> = SetOfVec<CertificateChoices<'a>>;

/// Signed-data content type [RFC 5652 § 5](https://datatracker.ietf.org/doc/html/rfc5652#section-5)
///
/// ```text
/// SignedData ::= SEQUENCE {
/// version CMSVersion,
/// digestAlgorithms DigestAlgorithmIdentifiers,
/// encapContentInfo EncapsulatedContentInfo,
/// certificates [0] IMPLICIT CertificateSet OPTIONAL,
/// crls [1] IMPLICIT RevocationInfoChoices OPTIONAL,
/// signerInfos SignerInfos }
/// ```
#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
pub struct SignedDataContent<'a> {
/// the syntax version number.
pub version: CmsVersion,

/// digest algorithm
pub digest_algorithms: DigestAlgorithmIdentifiers<'a>,

/// content
pub encap_content_info: EncapsulatedContentInfo<'a>,

/// certs
#[asn1(context_specific = "0", optional = "true", tag_mode = "IMPLICIT")]
pub certificates: Option<CertificateSet<'a>>,

/// crls
#[asn1(context_specific = "1", optional = "true", tag_mode = "IMPLICIT")]
pub crls: Option<RevocationInfoChoices<'a>>,

/// signer info
pub signer_infos: SignerInfos<'a>,
}
105 changes: 105 additions & 0 deletions pkcs7/src/signer_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! `SignerInfo` data type [RFC 5652 § 5.3](https://datatracker.ietf.org/doc/html/rfc5652#section-5.3)
use core::cmp::Ordering;

use crate::cms_version::CmsVersion;
use der::{
asn1::{OctetStringRef, SetOfVec},
Choice, Sequence, ValueOrd,
};
use spki::AlgorithmIdentifierRef;
use x509_cert::{
attr::Attribute, ext::pkix::SubjectKeyIdentifier, name::Name, serial_number::SerialNumber,
};

/// ```text
/// DigestAlgorithmIdentifier ::= AlgorithmIdentifier
/// ```
type DigestAlgorithmIdentifier<'a> = AlgorithmIdentifierRef<'a>;

/// ```text
/// SignatureAlgorithmIdentifier ::= AlgorithmIdentifier
/// ```
type SignatureAlgorithmIdentifier<'a> = AlgorithmIdentifierRef<'a>;

/// ```text
/// SignedAttributes ::= SET SIZE (1..MAX) OF Attribute
/// ```
type SignedAttributes<'a> = SetOfVec<Attribute>;

/// ```text
/// UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute
/// ```
type UnsignedAttributes<'a> = SetOfVec<Attribute>;

/// ```text
/// SignerIdentifier ::= CHOICE {
// issuerAndSerialNumber IssuerAndSerialNumber,
// subjectKeyIdentifier [0] SubjectKeyIdentifier }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Choice)]
pub enum SignerIdentifier<'a> {
/// issuer and serial number
IssuerAndSerialNumber(IssuerAndSerialNumber),

/// subject key identifier
#[asn1(context_specific = "0")]
SubjectKeyIdentifier(SubjectKeyIdentifier<'a>),
}

#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
#[allow(missing_docs)]
pub struct IssuerAndSerialNumber {
pub name: Name,
pub serial_number: SerialNumber,
}

/// ```text
/// SignerInfos ::= SET OF SignerInfo
/// ```
pub type SignerInfos<'a> = SetOfVec<SignerInfo<'a>>;

/// `SignerInfo` data type [RFC 5652 § 5.3](https://datatracker.ietf.org/doc/html/rfc5652#section-5.3)
///
/// ```text
/// SignerInfo ::= SEQUENCE {
/// version CMSVersion,
/// sid SignerIdentifier,
/// digestAlgorithm DigestAlgorithmIdentifier,
/// signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL,
/// signatureAlgorithm SignatureAlgorithmIdentifier,
/// signature SignatureValue,
/// unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL }
/// ```
#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
pub struct SignerInfo<'a> {
/// the syntax version number.
pub version: CmsVersion,

/// the signer identifier
pub sid: SignerIdentifier<'a>,

/// the message digest algorithm
pub digest_algorithm: DigestAlgorithmIdentifier<'a>,

/// the signed attributes
#[asn1(context_specific = "0", tag_mode = "IMPLICIT", optional = "true")]
pub signed_attributes: Option<SignedAttributes<'a>>,

/// the signature algorithm
pub signature_algorithm: SignatureAlgorithmIdentifier<'a>,

/// the signature for content or detached
pub signature: OctetStringRef<'a>,

/// the unsigned attributes
#[asn1(context_specific = "1", tag_mode = "IMPLICIT", optional = "true")]
pub unsigned_attributes: Option<UnsignedAttributes<'a>>,
}

// TODO: figure out what ordering makes sense - if any
impl ValueOrd for SignerInfo<'_> {
fn value_cmp(&self, _other: &Self) -> der::Result<Ordering> {
Ok(Ordering::Equal)
}
}
Comment on lines +100 to +105
Copy link
Member

Choose a reason for hiding this comment

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

You should be able to derive ValueOrd

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That'll end up requiring ValueOrd on CmsVersion and SignerIdentifier (which works nice with just adding ValueOrd there) but

the method `der_cmp` exists for enum `CmsVersion`, but its trait bounds were not satisfied
the following trait bounds were not satisfied:
`CmsVersion: ValueOrd`
which is required by `CmsVersion: DerOrd`

which ends up in when adding ValueOrd

invalid `asn1` attribute (valid options are `tag_mode`)

on CmsVersion

Copy link
Member

Choose a reason for hiding this comment

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

Alright, I can go ahead and merge and see if I can figure it out

Copy link
Member

Choose a reason for hiding this comment

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

Opened #825 to address this

63 changes: 61 additions & 2 deletions pkcs7/tests/content_tests.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
//! PKCS#7 example tests
use der::{
asn1::{ObjectIdentifier, OctetStringRef},
asn1::{ObjectIdentifier, OctetStringRef, SequenceRef},
Decode, SliceWriter,
};
use hex_literal::hex;
use pkcs7::{
cms_version::CmsVersion, encapsulated_content_info::EncapsulatedContentInfo,
encrypted_data_content::EncryptedDataContent, enveloped_data_content::EncryptedContentInfo,
ContentInfo, ContentType,
signed_data_content::SignedDataContent, ContentInfo, ContentType,
};
use spki::AlgorithmIdentifierRef;
use std::fs;
@@ -81,3 +82,61 @@ fn decode_encrypted_key_example() {

assert_eq!(encoded_content, bytes)
}

#[test]
fn decode_signed_mdm_example() {
let path = "./tests/examples/apple_mdm_signature_der.bin";
let bytes = fs::read(&path).expect(&format!("Failed to read from {}", &path));

let content = ContentInfo::from_der(&bytes).expect("expected valid data");

match content {
ContentInfo::SignedData(Some(SignedDataContent {
version: _,
digest_algorithms: _,
encap_content_info:
EncapsulatedContentInfo {
e_content_type: _,
e_content: Some(content),
},
certificates: _,
crls: _,
signer_infos: _,
})) => {
let _content = content
.decode_into::<SequenceRef>()
.expect("Content should be in the correct format: SequenceRef");
}
_ => panic!("expected ContentInfo::SignedData(Some(_))"),
}
}

#[test]
fn decode_signed_scep_example() {
let path = "./tests/examples/scep_der.bin";
let bytes = fs::read(&path).expect(&format!("Failed to read from {}", &path));

let content = ContentInfo::from_der(&bytes).expect("expected valid data");

match content {
ContentInfo::SignedData(Some(SignedDataContent {
version: ver,
digest_algorithms: _,
encap_content_info:
EncapsulatedContentInfo {
e_content_type: _,
e_content: Some(content),
},
certificates: _,
crls: _,
signer_infos: _,
})) => {
let _content = content
.decode_into::<OctetStringRef>()
.expect("Content should be in the correct format: OctetStringRef");

assert_eq!(ver, CmsVersion::V1)
}
_ => panic!("expected ContentInfo::SignedData(Some(_))"),
}
}
Binary file added pkcs7/tests/examples/apple_mdm_signature_der.bin
Binary file not shown.
Binary file added pkcs7/tests/examples/scep_der.bin
Binary file not shown.