diff --git a/pkcs7/Cargo.toml b/pkcs7/Cargo.toml index 034ab416b..e7c9483bc 100644 --- a/pkcs7/Cargo.toml +++ b/pkcs7/Cargo.toml @@ -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" diff --git a/pkcs7/src/certificate_choices.rs b/pkcs7/src/certificate_choices.rs new file mode 100644 index 000000000..dfc6bb7ca --- /dev/null +++ b/pkcs7/src/certificate_choices.rs @@ -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>), +} diff --git a/pkcs7/src/cms_version.rs b/pkcs7/src/cms_version.rs new file mode 100644 index 000000000..574f6e179 --- /dev/null +++ b/pkcs7/src/cms_version.rs @@ -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, +} diff --git a/pkcs7/src/content_info.rs b/pkcs7/src/content_info.rs index ec906b1dc..8b0489bf9 100644 --- a/pkcs7/src/content_info.rs +++ b/pkcs7/src/content_info.rs @@ -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>), + /// Content type `signed-data` + SignedData(Option>), + /// 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::>( + 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 { diff --git a/pkcs7/src/encapsulated_content_info.rs b/pkcs7/src/encapsulated_content_info.rs new file mode 100644 index 000000000..b5b190585 --- /dev/null +++ b/pkcs7/src/encapsulated_content_info.rs @@ -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>, +} diff --git a/pkcs7/src/lib.rs b/pkcs7/src/lib.rs index f17105740..d3f05303f 100644 --- a/pkcs7/src/lib.rs +++ b/pkcs7/src/lib.rs @@ -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; diff --git a/pkcs7/src/revocation_info_choices.rs b/pkcs7/src/revocation_info_choices.rs new file mode 100644 index 000000000..a332313c4 --- /dev/null +++ b/pkcs7/src/revocation_info_choices.rs @@ -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>; + +/// ```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 { + Ok(Ordering::Equal) + } +} diff --git a/pkcs7/src/signed_data_content.rs b/pkcs7/src/signed_data_content.rs new file mode 100644 index 000000000..ed55b5988 --- /dev/null +++ b/pkcs7/src/signed_data_content.rs @@ -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>; + +/// ```text +/// CertificateSet ::= SET OF CertificateChoices +/// ``` +type CertificateSet<'a> = SetOfVec>; + +/// 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>, + + /// crls + #[asn1(context_specific = "1", optional = "true", tag_mode = "IMPLICIT")] + pub crls: Option>, + + /// signer info + pub signer_infos: SignerInfos<'a>, +} diff --git a/pkcs7/src/signer_info.rs b/pkcs7/src/signer_info.rs new file mode 100644 index 000000000..2c1dc1215 --- /dev/null +++ b/pkcs7/src/signer_info.rs @@ -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; + +/// ```text +/// UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute +/// ``` +type UnsignedAttributes<'a> = SetOfVec; + +/// ```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` 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>, + + /// 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>, +} + +// TODO: figure out what ordering makes sense - if any +impl ValueOrd for SignerInfo<'_> { + fn value_cmp(&self, _other: &Self) -> der::Result { + Ok(Ordering::Equal) + } +} diff --git a/pkcs7/tests/content_tests.rs b/pkcs7/tests/content_tests.rs index be8dca179..c404adf3d 100644 --- a/pkcs7/tests/content_tests.rs +++ b/pkcs7/tests/content_tests.rs @@ -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::() + .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::() + .expect("Content should be in the correct format: OctetStringRef"); + + assert_eq!(ver, CmsVersion::V1) + } + _ => panic!("expected ContentInfo::SignedData(Some(_))"), + } +} diff --git a/pkcs7/tests/examples/apple_mdm_signature_der.bin b/pkcs7/tests/examples/apple_mdm_signature_der.bin new file mode 100644 index 000000000..63e0f46b7 Binary files /dev/null and b/pkcs7/tests/examples/apple_mdm_signature_der.bin differ diff --git a/pkcs7/tests/examples/scep_der.bin b/pkcs7/tests/examples/scep_der.bin new file mode 100644 index 000000000..5df43e980 Binary files /dev/null and b/pkcs7/tests/examples/scep_der.bin differ