From 902e68de060fbd75e8a8d93d30a3978abc6f975b Mon Sep 17 00:00:00 2001
From: Dmitry Pankratov <dmitry@pankratov.net>
Date: Sun, 15 Sep 2024 21:15:37 +0200
Subject: [PATCH] Added CMS support

---
 security-framework-sys/src/certificate.rs     |  18 +
 security-framework-sys/src/cms.rs             | 263 +++++++
 security-framework-sys/src/lib.rs             |   1 +
 security-framework/src/cms.rs                 | 645 ++++++++++++++++++
 security-framework/src/lib.rs                 |   2 +
 security-framework/test/cms/encrypted.p7m     | Bin 0 -> 422 bytes
 security-framework/test/cms/keystore.p12      | Bin 0 -> 4109 bytes
 .../test/cms/signed-encrypted.p7m             | Bin 0 -> 1802 bytes
 security-framework/test/cms/signed.p7m        | Bin 0 -> 1402 bytes
 systest/build.rs                              |   2 +
 systest/src/main.rs                           |   1 +
 11 files changed, 932 insertions(+)
 create mode 100644 security-framework-sys/src/cms.rs
 create mode 100644 security-framework/src/cms.rs
 create mode 100644 security-framework/test/cms/encrypted.p7m
 create mode 100644 security-framework/test/cms/keystore.p12
 create mode 100644 security-framework/test/cms/signed-encrypted.p7m
 create mode 100644 security-framework/test/cms/signed.p7m

diff --git a/security-framework-sys/src/certificate.rs b/security-framework-sys/src/certificate.rs
index 13d60e78..4d38ca2a 100644
--- a/security-framework-sys/src/certificate.rs
+++ b/security-framework-sys/src/certificate.rs
@@ -36,6 +36,24 @@ extern "C" {
     #[cfg(target_os = "macos")]
     pub static kSecPropertyTypeDate: CFStringRef;
 
+    // certificate policies
+    pub static kSecPolicyAppleX509Basic: CFStringRef;
+    pub static kSecPolicyAppleSSL: CFStringRef;
+    pub static kSecPolicyAppleSMIME: CFStringRef;
+    pub static kSecPolicyAppleEAP: CFStringRef;
+    pub static kSecPolicyAppleIPsec: CFStringRef;
+    #[cfg(target_os = "macos")]
+    pub static kSecPolicyApplePKINITClient: CFStringRef;
+    #[cfg(target_os = "macos")]
+    pub static kSecPolicyApplePKINITServer: CFStringRef;
+    pub static kSecPolicyAppleCodeSigning: CFStringRef;
+    pub static kSecPolicyMacAppStoreReceipt: CFStringRef;
+    pub static kSecPolicyAppleIDValidation: CFStringRef;
+    pub static kSecPolicyAppleTimeStamping: CFStringRef;
+    pub static kSecPolicyAppleRevocation: CFStringRef;
+    pub static kSecPolicyApplePassbookSigning: CFStringRef;
+    pub static kSecPolicyApplePayIssuerEncryption: CFStringRef;
+
     pub fn SecCertificateGetTypeID() -> CFTypeID;
     pub fn SecCertificateCreateWithData(
         allocator: CFAllocatorRef,
diff --git a/security-framework-sys/src/cms.rs b/security-framework-sys/src/cms.rs
new file mode 100644
index 00000000..ea398403
--- /dev/null
+++ b/security-framework-sys/src/cms.rs
@@ -0,0 +1,263 @@
+//! Cryptographic Message Syntax support
+
+use std::os::raw::c_void;
+
+use core_foundation_sys::array::CFArrayRef;
+use core_foundation_sys::base::{Boolean, CFTypeID, CFTypeRef, OSStatus};
+use core_foundation_sys::data::CFDataRef;
+use core_foundation_sys::date::CFAbsoluteTime;
+use core_foundation_sys::string::CFStringRef;
+
+use crate::base::SecCertificateRef;
+use crate::trust::SecTrustRef;
+
+pub enum OpaqueCMSEncoderRef {}
+pub type CMSEncoderRef = *mut OpaqueCMSEncoderRef;
+
+pub enum OpaqueCMSDecoderRef {}
+pub type CMSDecoderRef = *mut OpaqueCMSEncoderRef;
+
+#[repr(i32)]
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+pub enum CMSSignerStatus {
+    kCMSSignerUnsigned = 0,
+    kCMSSignerValid = 1,
+    kCMSSignerNeedsDetachedContent = 2,
+    kCMSSignerInvalidSignature = 3,
+    kCMSSignerInvalidCert = 4,
+    kCMSSignerInvalidIndex = 5,
+}
+
+pub type CMSSignedAttributes  = u32;
+pub const kCMSAttrNone: CMSSignedAttributes = 0x0000;
+pub const kCMSAttrSmimeCapabilities: CMSSignedAttributes = 0x0001;
+pub const kCMSAttrSmimeEncryptionKeyPrefs: CMSSignedAttributes = 0x0002;
+pub const kCMSAttrSmimeMSEncryptionKeyPrefs: CMSSignedAttributes = 0x0004;
+pub const kCMSAttrSigningTime: CMSSignedAttributes = 0x0008;
+pub const kCMSAttrAppleCodesigningHashAgility: CMSSignedAttributes = 0x0010;
+pub const kCMSAttrAppleCodesigningHashAgilityV2: CMSSignedAttributes = 0x0020;
+pub const kCMSAttrAppleExpirationTime: CMSSignedAttributes = 0x0040;
+
+#[repr(i32)]
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+pub enum CMSCertificateChainMode {
+    kCMSCertificateNone = 0,
+    kCMSCertificateSignerOnly = 1,
+    kCMSCertificateChain = 2,
+    kCMSCertificateChainWithRoot = 3,
+    kCMSCertificateChainWithRootOrFail = 4,
+}
+
+extern "C" {
+
+    // CMS decoder
+
+    pub fn CMSDecoderGetTypeID() -> CFTypeID;
+
+    pub fn CMSDecoderCreate(output: *mut CMSDecoderRef) -> OSStatus;
+
+    pub fn CMSDecoderUpdateMessage(
+        decoder: CMSDecoderRef,
+        msg_bytes: *const c_void,
+        msg_bytes_len: usize,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderFinalizeMessage(decoder: CMSDecoderRef) -> OSStatus;
+
+    pub fn CMSDecoderSetDetachedContent(
+        decoder: CMSDecoderRef,
+        detached_content: CFDataRef,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopyDetachedContent(
+        decoder: CMSDecoderRef,
+        detached_content_out: *mut CFDataRef,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderGetNumSigners(
+        decoder: CMSDecoderRef,
+        num_signers_out: *mut usize,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopySignerStatus(
+        decoder: CMSDecoderRef,
+        signer_index: usize,
+        policy_or_array: CFTypeRef,
+        evaluate_sec_trust: Boolean,
+        signer_status_out: *mut CMSSignerStatus,
+        sec_trust_out: *mut SecTrustRef,
+        cert_verify_result_code_out: *mut OSStatus,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopySignerEmailAddress(
+        decoder: CMSDecoderRef,
+        signer_index: usize,
+        signer_email_address_out: *mut CFStringRef,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopySignerCert(
+        decoder: CMSDecoderRef,
+        signer_index: usize,
+        signer_cert_out: *mut SecCertificateRef,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderIsContentEncrypted(
+        decoder: CMSDecoderRef,
+        is_encrypted_out: *mut Boolean,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopyEncapsulatedContentType(
+        decoder: CMSDecoderRef,
+        content_type_out: *mut CFDataRef,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopyAllCerts(
+        decoder: CMSDecoderRef,
+        certs_out: *mut CFArrayRef,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopyContent(
+        decoder: CMSDecoderRef,
+        content_out: *mut CFDataRef,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopySignerSigningTime(
+        decoder: CMSDecoderRef,
+        signer_index: usize,
+        sign_time_out: *mut CFAbsoluteTime,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopySignerTimestamp(
+        decoder: CMSDecoderRef,
+        signer_index: usize,
+        timestamp: *mut CFAbsoluteTime,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopySignerTimestampWithPolicy(
+        decoder: CMSDecoderRef,
+        timestamp_policy: CFTypeRef,
+        signer_index: usize,
+        timestamp: *mut CFAbsoluteTime,
+    ) -> OSStatus;
+
+    pub fn CMSDecoderCopySignerTimestampCertificates(
+        decoder: CMSDecoderRef,
+        signer_index: usize,
+        certificate_refs: *mut CFArrayRef,
+    ) -> OSStatus;
+
+
+    // CMS encoder
+
+    pub static kCMSEncoderDigestAlgorithmSHA1: CFStringRef;
+    pub static kCMSEncoderDigestAlgorithmSHA256: CFStringRef;
+
+    pub fn CMSEncoderGetTypeID() -> CFTypeID;
+
+    pub fn CMSEncoderCreate(encoder_out: *mut CMSEncoderRef) -> OSStatus;
+
+    pub fn CMSEncoderSetSignerAlgorithm(
+        encoder: CMSEncoderRef,
+        digest_alogrithm: CFStringRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderAddSigners(
+        encoder: CMSEncoderRef,
+        signer_or_array: CFTypeRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderCopySigners(
+        encoder: CMSEncoderRef,
+        signers_out: *mut CFArrayRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderAddRecipients(
+        encoder: CMSEncoderRef,
+        recipient_or_array: CFTypeRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderCopyRecipients(
+        encoder: CMSEncoderRef,
+        recipients_out: *mut CFArrayRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderSetHasDetachedContent(
+        encoder: CMSEncoderRef,
+        detached_content: Boolean,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderGetHasDetachedContent(
+        encoder: CMSEncoderRef,
+        detached_content_out: *mut Boolean,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderSetEncapsulatedContentTypeOID(
+        encoder: CMSEncoderRef,
+        content_type_oid: CFTypeRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderCopyEncapsulatedContentType(
+        encoder: CMSEncoderRef,
+        content_type_out: *mut CFDataRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderAddSupportingCerts(
+        encoder: CMSEncoderRef,
+        cert_or_array: CFTypeRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderCopySupportingCerts(
+        encoder: CMSEncoderRef,
+        certs_out: *mut CFArrayRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderAddSignedAttributes(
+        encoder: CMSEncoderRef,
+        signed_attributes: CMSSignedAttributes,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderSetCertificateChainMode(
+        encoder: CMSEncoderRef,
+        chain_mode: CMSCertificateChainMode,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderGetCertificateChainMode(
+        encoder: CMSEncoderRef,
+        chain_mode_out: *mut CMSCertificateChainMode,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderUpdateContent(
+        encoder: CMSEncoderRef,
+        content: *const c_void,
+        content_len: usize,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderCopyEncodedContent(
+        encoder: CMSEncoderRef,
+        encoded_content_out: *mut CFDataRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncodeContent(
+        signers: CFTypeRef,
+        recipients: CFTypeRef,
+        content_type_oid: CFTypeRef,
+        detached_content: Boolean,
+        signed_attributes: CMSSignedAttributes,
+        content: *const c_void,
+        content_len: usize,
+        encoded_content_out: *mut CFDataRef,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderCopySignerTimestamp(
+        encoder: CMSEncoderRef,
+        signer_index: usize,
+        timestamp: *mut CFAbsoluteTime,
+    ) -> OSStatus;
+
+    pub fn CMSEncoderCopySignerTimestampWithPolicy(
+        encoder: CMSEncoderRef,
+        timestamp_policy: CFTypeRef,
+        signer_index: usize,
+        timestamp: *mut CFAbsoluteTime,
+    ) -> OSStatus;
+}
diff --git a/security-framework-sys/src/lib.rs b/security-framework-sys/src/lib.rs
index 0cb6a611..8240c86c 100644
--- a/security-framework-sys/src/lib.rs
+++ b/security-framework-sys/src/lib.rs
@@ -17,6 +17,7 @@ pub mod certificate;
 #[cfg(target_os = "macos")]
 pub mod certificate_oids;
 pub mod cipher_suite;
+pub mod cms;
 #[cfg(target_os = "macos")]
 pub mod code_signing;
 #[cfg(target_os = "macos")]
diff --git a/security-framework/src/cms.rs b/security-framework/src/cms.rs
new file mode 100644
index 00000000..1c4562d3
--- /dev/null
+++ b/security-framework/src/cms.rs
@@ -0,0 +1,645 @@
+//! Cryptographic Message Syntax support
+
+use std::{fmt, ptr};
+
+use core_foundation::array::CFArray;
+use core_foundation::data::CFData;
+use core_foundation::string::CFString;
+use core_foundation_sys::array::CFArrayRef;
+use core_foundation_sys::base::{OSStatus, TCFTypeRef};
+use core_foundation_sys::data::CFDataRef;
+use core_foundation_sys::date::CFAbsoluteTime;
+use core_foundation_sys::string::CFStringRef;
+use security_framework_sys::cms::*;
+use security_framework_sys::trust::SecTrustRef;
+
+use crate::base::Result;
+use crate::certificate::SecCertificate;
+use crate::core_foundation::base::TCFType;
+use crate::cvt;
+use crate::policy::SecPolicy;
+use crate::trust::SecTrust;
+
+pub use decoder::CMSDecoder;
+
+pub use encoder::cms_encode_content;
+pub use encoder::CMS_DIGEST_ALGORITHM_SHA1;
+pub use encoder::CMS_DIGEST_ALGORITHM_SHA256;
+pub use encoder::CMSEncoder;
+pub use encoder::SignedAttributes;
+
+mod encoder {
+    use super::*;
+    use crate::identity::SecIdentity;
+
+    /// SHA1 digest algorithm
+    pub const CMS_DIGEST_ALGORITHM_SHA1: &str = "sha1";
+    /// SHA256 digest algorithm
+    pub const CMS_DIGEST_ALGORITHM_SHA256: &str = "sha256";
+
+    bitflags::bitflags! {
+        /// Optional attributes you can add to a signed message
+        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+        pub struct SignedAttributes: CMSSignedAttributes {
+            /// Identify signature, encryption, and digest algorithms supported by the encoder
+            const SMIME_CAPABILITIES = kCMSAttrSmimeCapabilities;
+            /// Indicate that the signing certificate included with the message is the preferred one for S/MIME encryption
+            const SMIME_ENCRYPTION_KEY_PREFS = kCMSAttrSmimeEncryptionKeyPrefs;
+            /// Indicate that the signing certificate included with the message is the preferred one for S/MIME encryption,
+            /// but using an attribute object identifier (OID) preferred by Microsoft
+            const SMIME_MS_ENCRYPTION_KEY_PREFS = kCMSAttrSmimeMSEncryptionKeyPrefs;
+            /// Include the signing time
+            const SIGNING_TIME =  kCMSAttrSigningTime;
+            /// Include Apple codesigning hash agility
+            const APPLE_CODESIGNING_HASH_AGILITY = kCMSAttrAppleCodesigningHashAgility;
+            /// Include Apple codesigning hash agility, version 2
+            const APPLE_CODESIGNING_HASH_AGILITY_V2 = kCMSAttrAppleCodesigningHashAgilityV2;
+            /// Include the expiration time
+            const APPLE_EXPIRATION_TIME = kCMSAttrAppleExpirationTime;
+        }
+    }
+
+
+    declare_TCFType! {
+        /// A type representing CMS encoder
+        CMSEncoder, CMSEncoderRef
+    }
+    impl_TCFType!(CMSEncoder, CMSEncoderRef, CMSEncoderGetTypeID);
+
+    unsafe impl Sync for CMSEncoder {}
+    unsafe impl Send for CMSEncoder {}
+
+    impl fmt::Debug for CMSEncoder {
+        #[cold]
+        fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
+            fmt.debug_struct("CMSEncoder").finish()
+        }
+    }
+
+    impl CMSEncoder {
+        /// Create a new instance of `CMSEncoder`
+        pub fn create() -> Result<Self> {
+            let mut inner: CMSEncoderRef = ptr::null_mut();
+            cvt(unsafe { CMSEncoderCreate(&mut inner) })?;
+            Ok(Self(inner))
+        }
+
+        /// Sets the digest algorithm to use for the signer.
+        /// Can be one of the predefined types:
+        ///
+        /// * `kCMSEncoderDigestAlgorithmSHA1`
+        /// * `kCMSEncoderDigestAlgorithmSHA256`
+        pub fn set_signer_algorithm(&self, digest_algorithm: &str) -> Result<()> {
+            let alg = CFString::new(digest_algorithm);
+
+            cvt(unsafe { CMSEncoderSetSignerAlgorithm(self.0, alg.as_concrete_TypeRef()) })?;
+            Ok(())
+        }
+
+        /// Specify signers of the CMS message; implies that the message will be signed
+        pub fn add_signers(&self, signers: &[SecIdentity]) -> Result<()> {
+            let signers = CFArray::from_CFTypes(signers);
+            cvt(unsafe {
+                CMSEncoderAddSigners(
+                    self.0,
+                    if signers.is_empty() { ptr::null() } else { signers.as_CFTypeRef() })
+            })?;
+            Ok(())
+        }
+
+        /// Obtains the array of signers specified with the `add_signers` function
+        pub fn get_signers(&self) -> Result<Vec<SecIdentity>> {
+            let mut out: CFArrayRef = ptr::null_mut();
+            cvt(unsafe { CMSEncoderCopySigners(self.0, &mut out) })?;
+
+            if out.is_null() {
+                Ok(Vec::new())
+            } else {
+                let array = unsafe { CFArray::<SecIdentity>::wrap_under_create_rule(out) };
+                Ok(array.into_iter().map(|c| c.clone()).collect())
+            }
+        }
+
+        /// Specifies a message is to be encrypted and specifies the recipients of the message
+        pub fn add_recipients(&self, recipients: &[SecCertificate]) -> Result<()> {
+            let recipients = CFArray::from_CFTypes(recipients);
+            cvt(unsafe {
+                CMSEncoderAddRecipients(
+                    self.0,
+                    if recipients.is_empty() { ptr::null() } else { recipients.as_CFTypeRef() })
+            })?;
+            Ok(())
+        }
+
+        /// Obtains the array of recipients specified with the `add_recipients` function
+        pub fn get_recipients(&self) -> Result<Vec<SecCertificate>> {
+            let mut out: CFArrayRef = ptr::null_mut();
+            cvt(unsafe { CMSEncoderCopyRecipients(self.0, &mut out) })?;
+
+            if out.is_null() {
+                Ok(Vec::new())
+            } else {
+                let array = unsafe { CFArray::<SecCertificate>::wrap_under_create_rule(out) };
+                Ok(array.into_iter().map(|c| c.clone()).collect())
+            }
+        }
+
+        /// Specifies whether the signed data is to be separate from the message
+        pub fn set_has_detached_content(&self, has_detached_content: bool) -> Result<()> {
+            cvt(unsafe { CMSEncoderSetHasDetachedContent(self.0, has_detached_content.into()) })?;
+            Ok(())
+        }
+
+        /// Indicates whether the message is to have detached content
+        pub fn get_has_detached_content(&self) -> Result<bool> {
+            let mut has_detached_content = 0;
+            cvt(unsafe { CMSEncoderGetHasDetachedContent(self.0, &mut has_detached_content) })?;
+            Ok(has_detached_content != 0)
+        }
+
+        /// Specifies an object identifier for the encapsulated data of a signed message
+        pub fn set_encapsulated_content_type_oid(&self, oid: &str) -> Result<()> {
+            let oid = CFString::new(oid);
+            cvt(unsafe { CMSEncoderSetEncapsulatedContentTypeOID(self.0, oid.as_CFTypeRef()) })?;
+            Ok(())
+        }
+
+        /// Obtains the object identifier for the encapsulated data of a signed message
+        pub fn get_encapsulated_content_type(&self) -> Result<Vec<u8>> {
+            let mut out: CFDataRef = ptr::null_mut();
+            cvt(unsafe { CMSEncoderCopyEncapsulatedContentType(self.0, &mut out) })?;
+            Ok(unsafe { CFData::wrap_under_create_rule(out).to_vec() })
+        }
+
+        /// Adds certificates to a message
+        pub fn add_supporting_certs(&self, certs: &[SecCertificate]) -> Result<()> {
+            let certs = CFArray::from_CFTypes(certs);
+            cvt(unsafe {
+                CMSEncoderAddSupportingCerts(
+                    self.0,
+                    if !certs.is_empty() { certs.as_CFTypeRef() } else { ptr::null() })
+            })?;
+            Ok(())
+        }
+
+        /// Obtains the certificates added to a message with `add_supporting_certs`
+        pub fn get_supporting_certs(&self) -> Result<Vec<SecCertificate>> {
+            let mut out: CFArrayRef = ptr::null_mut();
+            cvt(unsafe { CMSEncoderCopySupportingCerts(self.0, &mut out) })?;
+
+            if out.is_null() {
+                Ok(Vec::new())
+            } else {
+                let array = unsafe { CFArray::<SecCertificate>::wrap_under_create_rule(out) };
+                Ok(array.into_iter().map(|c| c.clone()).collect())
+            }
+        }
+
+        /// Specifies attributes for a signed message
+        pub fn add_signed_attributes(&self, signed_attributes: SignedAttributes) -> Result<()> {
+            cvt(unsafe { CMSEncoderAddSignedAttributes(self.0, signed_attributes.bits()) })?;
+            Ok(())
+        }
+
+        /// Specifies which certificates to include in a signed CMS message
+        pub fn set_certificate_chain_mode(
+            &self,
+            certificate_chain_mode: CMSCertificateChainMode) -> Result<()>
+        {
+            cvt(unsafe { CMSEncoderSetCertificateChainMode(self.0, certificate_chain_mode) })?;
+            Ok(())
+        }
+
+        /// Obtains a constant that indicates which certificates are to be included in a signed CMS message
+        pub fn get_certificate_chain_mode(&self) -> Result<CMSCertificateChainMode> {
+            let mut out = CMSCertificateChainMode::kCMSCertificateNone;
+            cvt(unsafe { CMSEncoderGetCertificateChainMode(self.0, &mut out) })?;
+            Ok(out)
+        }
+
+        /// Feeds content bytes into the encoder
+        pub fn update_content(&self, content: &[u8]) -> Result<()> {
+            cvt(unsafe { CMSEncoderUpdateContent(self.0, content.as_ptr() as _, content.len()) })?;
+            Ok(())
+        }
+
+        /// Finishes encoding the message and obtains the encoded result
+        pub fn get_encoded_content(&self) -> Result<Vec<u8>> {
+            let mut out: CFDataRef = ptr::null_mut();
+            cvt(unsafe { CMSEncoderCopyEncodedContent(self.0, &mut out) })?;
+            Ok(unsafe { CFData::wrap_under_create_rule(out).to_vec() })
+        }
+
+        /// Returns the timestamp of a signer of a CMS message, if present
+        pub fn get_signer_timestamp(&self, signer_index: usize) -> Result<CFAbsoluteTime> {
+            let mut out = CFAbsoluteTime::default();
+            cvt(unsafe { CMSEncoderCopySignerTimestamp(self.0, signer_index, &mut out) })?;
+            Ok(out)
+        }
+
+        /// Returns the timestamp of a signer of a CMS message using a particular policy, if present
+        pub fn get_signer_timestamp_with_policy(
+            &self,
+            timestamp_policy: Option<CFStringRef>,
+            signer_index: usize) -> Result<CFAbsoluteTime>
+        {
+            let mut out = CFAbsoluteTime::default();
+            cvt(unsafe {
+                CMSEncoderCopySignerTimestampWithPolicy(
+                    self.0,
+                    timestamp_policy.map(|p| p.as_void_ptr()).unwrap_or(ptr::null()),
+                    signer_index,
+                    &mut out)
+            })?;
+
+            Ok(out)
+        }
+    }
+
+    /// Encodes a message and obtains the result in one high-level function call
+    pub fn cms_encode_content(
+        signers: &[SecIdentity],
+        recipients: &[SecCertificate],
+        content_type_oid: Option<&str>,
+        detached_content: bool,
+        signed_attributes: SignedAttributes,
+        content: &[u8],
+    ) -> Result<Vec<u8>> {
+        let mut out: CFDataRef = ptr::null_mut();
+        let signers = CFArray::from_CFTypes(signers);
+        let recipients = CFArray::from_CFTypes(recipients);
+        let content_type_oid = content_type_oid.map(CFString::new);
+
+        cvt(unsafe {
+            CMSEncodeContent(
+                if signers.is_empty() { ptr::null() } else { signers.as_CFTypeRef() },
+                if recipients.is_empty() { ptr::null() } else { recipients.as_CFTypeRef() },
+                content_type_oid.as_ref().map(|oid| oid.as_CFTypeRef()).unwrap_or(ptr::null()),
+                detached_content.into(),
+                signed_attributes.bits(),
+                content.as_ptr() as _,
+                content.len(),
+                &mut out,
+            )
+        })?;
+
+        Ok(unsafe { CFData::wrap_under_create_rule(out).to_vec() })
+    }
+}
+
+mod decoder {
+    use super::*;
+
+    /// Holds a result of the `CMSDecoder::get_signer_status` function
+    pub struct SignerStatus {
+        /// Signature status
+        pub signer_status: CMSSignerStatus,
+        /// Trust instance that was used to verify the signer’s certificate
+        pub sec_trust: SecTrust,
+        /// Result of the certificate verification
+        pub cert_verify_result: Result<()>,
+    }
+
+    declare_TCFType! {
+        /// A type representing CMS Decoder
+        CMSDecoder, CMSDecoderRef
+    }
+    impl_TCFType!(CMSDecoder, CMSDecoderRef, CMSDecoderGetTypeID);
+
+    unsafe impl Sync for CMSDecoder {}
+    unsafe impl Send for CMSDecoder {}
+
+    impl fmt::Debug for CMSDecoder {
+        #[cold]
+        fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
+            fmt.debug_struct("CMSDecoder").finish()
+        }
+    }
+
+    impl CMSDecoder {
+        /// Create a new instance of `CMSDecoder`
+        pub fn create() -> Result<Self> {
+            let mut inner: CMSDecoderRef = ptr::null_mut();
+            cvt(unsafe { CMSDecoderCreate(&mut inner) })?;
+            Ok(Self(inner))
+        }
+
+        /// Feeds raw bytes of the message to be decoded into the decoder
+        pub fn update_message(
+            &self,
+            message: &[u8],
+        ) -> Result<()> {
+            cvt(unsafe { CMSDecoderUpdateMessage(self.0, message.as_ptr() as _, message.len()) })?;
+            Ok(())
+        }
+
+        /// Indicates that there is no more data to decode
+        pub fn finalize_message(&self) -> Result<()> {
+            cvt(unsafe { CMSDecoderFinalizeMessage(self.0) })?;
+            Ok(())
+        }
+
+        /// Specifies the message’s detached content, if any
+        pub fn set_detached_content(&self, detached_content: &[u8]) -> Result<()> {
+            let data = CFData::from_buffer(detached_content);
+            cvt(unsafe { CMSDecoderSetDetachedContent(self.0, data.as_concrete_TypeRef()) })?;
+            Ok(())
+        }
+
+        /// Obtains the detached content specified with the `set_detached_content` function
+        pub fn get_detached_content(&self) -> Result<Vec<u8>> {
+            unsafe {
+                let mut out: CFDataRef = ptr::null_mut();
+                cvt(CMSDecoderCopyDetachedContent(self.0, &mut out))?;
+                if out.is_null() {
+                    Ok(Vec::new())
+                } else {
+                    Ok(CFData::wrap_under_create_rule(out).to_vec())
+                }
+            }
+        }
+
+        /// Obtains the number of signers of a message
+        pub fn get_num_signers(&self) -> Result<usize> {
+            let mut out = 0;
+            cvt(unsafe { CMSDecoderGetNumSigners(self.0, &mut out) })?;
+            Ok(out)
+        }
+
+        /// Obtains the status of a CMS message’s signature
+        pub fn get_signer_status(
+            &self,
+            signer_index: usize,
+            policies: &[SecPolicy]) -> Result<SignerStatus>
+        {
+            let policies = CFArray::from_CFTypes(policies);
+
+            let mut signer_status = CMSSignerStatus::kCMSSignerUnsigned;
+            let mut sec_trust: SecTrustRef = ptr::null_mut();
+            let mut verify_result = OSStatus::default();
+
+            cvt(unsafe {
+                CMSDecoderCopySignerStatus(
+                    self.0,
+                    signer_index,
+                    if policies.is_empty() { ptr::null() } else { policies.as_CFTypeRef() },
+                    true.into(),
+                    &mut signer_status,
+                    &mut sec_trust,
+                    &mut verify_result,
+                )
+            })?;
+
+            Ok(SignerStatus {
+                signer_status,
+                sec_trust: unsafe { SecTrust::wrap_under_create_rule(sec_trust) },
+                cert_verify_result: cvt(verify_result),
+            })
+        }
+
+        /// Obtains the email address of the specified signer of a CMS message
+        pub fn get_signer_email_address(&self, signer_index: usize) -> Result<String> {
+            let mut out: CFStringRef = ptr::null_mut();
+            cvt(unsafe { CMSDecoderCopySignerEmailAddress(self.0, signer_index, &mut out) })?;
+            Ok(unsafe { CFString::wrap_under_create_rule(out).to_string() })
+        }
+
+        /// Determines whether a CMS message was encrypted
+        pub fn is_content_encrypted(&self) -> Result<bool> {
+            let mut out = 0;
+            cvt(unsafe { CMSDecoderIsContentEncrypted(self.0, &mut out) })?;
+            Ok(out != 0)
+        }
+
+        /// Obtains the object identifier for the encapsulated data of a signed message
+        pub fn get_encapsulated_content_type(&self) -> Result<Vec<u8>> {
+            let mut out: CFDataRef = ptr::null_mut();
+            if out.is_null() {
+                Ok(Vec::new())
+            } else {
+                cvt(unsafe { CMSDecoderCopyEncapsulatedContentType(self.0, &mut out) })?;
+                Ok(unsafe { CFData::wrap_under_create_rule(out).to_vec() })
+            }
+        }
+
+        /// Obtains an array of all of the certificates in a message
+        pub fn get_all_certs(&self) -> Result<Vec<SecCertificate>> {
+            let mut out: CFArrayRef = ptr::null_mut();
+            cvt(unsafe { CMSDecoderCopyAllCerts(self.0, &mut out) })?;
+
+            if out.is_null() {
+                Ok(Vec::new())
+            } else {
+                let array = unsafe { CFArray::<SecCertificate>::wrap_under_create_rule(out) };
+                Ok(array.into_iter().map(|c| c.clone()).collect())
+            }
+        }
+
+        /// Obtains the message content, if any
+        pub fn get_content(&self) -> Result<Vec<u8>> {
+            let mut out: CFDataRef = ptr::null_mut();
+
+            cvt(unsafe { CMSDecoderCopyContent(self.0, &mut out) })?;
+
+            if out.is_null() {
+                Ok(Vec::new())
+            } else {
+                Ok(unsafe { CFData::wrap_under_create_rule(out).to_vec() })
+            }
+        }
+
+        /// Obtains the signing time of a CMS message, if present
+        pub fn get_signer_signing_time(&self, signer_index: usize) -> Result<CFAbsoluteTime> {
+            let mut out = CFAbsoluteTime::default();
+            cvt(unsafe { CMSDecoderCopySignerSigningTime(self.0, signer_index, &mut out) })?;
+            Ok(out)
+        }
+
+        /// Returns the timestamp of a signer of a CMS message, if present
+        pub fn get_signer_timestamp(&self, signer_index: usize) -> Result<CFAbsoluteTime> {
+            let mut out = CFAbsoluteTime::default();
+            cvt(unsafe { CMSDecoderCopySignerTimestamp(self.0, signer_index, &mut out) })?;
+            Ok(out)
+        }
+
+        /// Returns the timestamp of a signer of a CMS message using a given policy, if present
+        pub fn get_signer_timestamp_with_policy(
+            &self,
+            timestamp_policy: Option<CFStringRef>,
+            signer_index: usize) -> Result<CFAbsoluteTime>
+        {
+            let mut out = CFAbsoluteTime::default();
+            cvt(unsafe {
+                CMSDecoderCopySignerTimestampWithPolicy(
+                    self.0,
+                    timestamp_policy.map(|p| p.as_void_ptr()).unwrap_or(ptr::null()),
+                    signer_index,
+                    &mut out)
+            })?;
+
+            Ok(out)
+        }
+
+        /// Returns an array containing the certificates from a timestamp response
+        pub fn get_signer_timestamp_certificates(
+            &self,
+            signer_index: usize) -> Result<Vec<SecCertificate>>
+        {
+            let mut out: CFArrayRef = ptr::null_mut();
+            cvt(unsafe { CMSDecoderCopySignerTimestampCertificates(self.0, signer_index, &mut out) })?;
+
+            if out.is_null() {
+                Ok(Vec::new())
+            } else {
+                let array = unsafe { CFArray::<SecCertificate>::wrap_under_create_rule(out) };
+                Ok(array.into_iter().map(|c| c.clone()).collect())
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use security_framework_sys::cms::CMSSignerStatus;
+    use crate::cms::{cms_encode_content, CMSDecoder, CMSEncoder, SignedAttributes};
+    use crate::import_export::{ImportedIdentity, Pkcs12ImportOptions};
+    use crate::policy::SecPolicy;
+
+    const KEYSTORE: &[u8] = include_bytes!("../test/cms/keystore.p12");
+    const ENCRYPTED_CMS: &[u8] = include_bytes!("../test/cms/encrypted.p7m");
+    const SIGNED_ENCRYPTED_CMS: &[u8] = include_bytes!("../test/cms/signed-encrypted.p7m");
+
+    fn import_keystore() -> Vec<ImportedIdentity> {
+        let mut import_opts = Pkcs12ImportOptions::new();
+        import_opts.passphrase("cms").import(KEYSTORE).unwrap()
+    }
+
+    #[test]
+    fn test_decode_encrypted() {
+        let _identities = import_keystore();
+
+        let decoder = CMSDecoder::create().unwrap();
+        decoder.update_message(ENCRYPTED_CMS).unwrap();
+        decoder.finalize_message().unwrap();
+
+        assert!(decoder.is_content_encrypted().unwrap());
+        assert_eq!(decoder.get_content().unwrap(), b"encrypted message\n");
+        assert_eq!(decoder.get_all_certs().unwrap().len(), 0);
+        assert_eq!(decoder.get_num_signers().unwrap(), 0);
+    }
+
+    #[test]
+    fn test_decode_signed_and_encrypted() {
+        let _identities = import_keystore();
+
+        let decoder = CMSDecoder::create().unwrap();
+        decoder.update_message(SIGNED_ENCRYPTED_CMS).unwrap();
+        decoder.finalize_message().unwrap();
+
+        assert!(decoder.is_content_encrypted().unwrap());
+
+        let signed_content = decoder.get_content().unwrap();
+
+        let decoder2 = CMSDecoder::create().unwrap();
+        decoder2.update_message(&signed_content).unwrap();
+        decoder2.finalize_message().unwrap();
+        assert_eq!(decoder2.get_content().unwrap(), b"encrypted message\n");
+        assert_eq!(decoder2.get_num_signers().unwrap(), 1);
+
+        let policies = vec![SecPolicy::create_x509()];
+        let status = decoder2.get_signer_status(0, &policies).unwrap();
+        assert!(status.cert_verify_result.is_err());
+        assert_eq!(status.signer_status, CMSSignerStatus::kCMSSignerInvalidCert);
+    }
+
+    #[test]
+    fn test_encode_encrypted() {
+        let identities = import_keystore();
+
+        let chain = identities
+            .iter()
+            .filter_map(|id| id.cert_chain.as_ref())
+            .next()
+            .unwrap();
+
+        let message = cms_encode_content(
+            &[],
+            &chain[0..1],
+            None,
+            false,
+            SignedAttributes::empty(),
+            b"encrypted message\n",
+        ).unwrap();
+
+        let decoder = CMSDecoder::create().unwrap();
+        decoder.update_message(&message).unwrap();
+        decoder.finalize_message().unwrap();
+        assert_eq!(decoder.get_content().unwrap(), b"encrypted message\n");
+    }
+
+    #[test]
+    fn test_encode_signed_encrypted() {
+        let identities = import_keystore();
+
+        let chain = identities
+            .iter()
+            .filter_map(|id| id.cert_chain.as_ref())
+            .next()
+            .unwrap();
+
+        let identity = identities
+            .iter()
+            .filter_map(|id| id.identity.as_ref())
+            .next()
+            .unwrap();
+
+        let message = cms_encode_content(
+            &[identity.clone()],
+            &chain[0..1],
+            None,
+            false,
+            SignedAttributes::empty(),
+            b"encrypted message\n",
+        ).unwrap();
+
+        let decoder = CMSDecoder::create().unwrap();
+        decoder.update_message(&message).unwrap();
+        decoder.finalize_message().unwrap();
+        assert_eq!(decoder.get_content().unwrap(), b"encrypted message\n");
+        assert_eq!(decoder.get_num_signers().unwrap(), 1);
+    }
+
+    #[test]
+    fn test_encode_with_cms_encoder() {
+        let identities = import_keystore();
+
+        let chain = identities
+            .iter()
+            .filter_map(|id| id.cert_chain.as_ref())
+            .next()
+            .unwrap();
+
+        let identity = identities
+            .iter()
+            .filter_map(|id| id.identity.as_ref())
+            .next()
+            .unwrap();
+
+        let encoder = CMSEncoder::create().unwrap();
+
+        let message = cms_encode_content(
+            &[identity.clone()],
+            &chain[0..1],
+            None,
+            false,
+            SignedAttributes::empty(),
+            b"encrypted message\n",
+        ).unwrap();
+
+        let decoder = CMSDecoder::create().unwrap();
+        decoder.update_message(&message).unwrap();
+        decoder.finalize_message().unwrap();
+        assert_eq!(decoder.get_content().unwrap(), b"encrypted message\n");
+        assert_eq!(decoder.get_num_signers().unwrap(), 1);
+    }
+}
diff --git a/security-framework/src/lib.rs b/security-framework/src/lib.rs
index 81631afe..f6fdd054 100644
--- a/security-framework/src/lib.rs
+++ b/security-framework/src/lib.rs
@@ -57,6 +57,8 @@ pub mod secure_transport;
 pub mod trust;
 #[cfg(target_os = "macos")]
 pub mod trust_settings;
+#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos", target_os = "visionos"))]
+pub mod cms;
 
 #[cfg(target_os = "macos")]
 trait Pkcs12ImportOptionsInternals {
diff --git a/security-framework/test/cms/encrypted.p7m b/security-framework/test/cms/encrypted.p7m
new file mode 100644
index 0000000000000000000000000000000000000000..f1bc0ba3c1072a9942a2b339c0d0fb5af6d17d1e
GIT binary patch
literal 422
zcmXqLVqC<=snzDu_MMlJoq0hM<79&-#(pM72E!&sD<IPh$TW~M5Hb`n;A3MBWnmU(
zcTCPrRd9A>;?VmzSz)uK{u~2dxDkwutPCtoj0{TGAKl%kGRJz7Mx;WKN6D8hS>I0T
z>x!S<^<ObmF^zZEwTe=&i;aDo41QdgAiXYYXWz6%Im<F1PUqZl=K7QGZ+jV@E$8vd
zykhxf(#z7wWgWj?#HdVlesl2tnGmMV`C1DFmS)9^I!>|_?TneKa<OEmd$eNY+0Kw7
z|9sA?6n|}CJ(Tn~=|!aB)l3o9j{3*U-?tdOO*+qZ?UT!Lv#P1p@jP|K?;>|vUbUH~
zGVR5^d$L>p9f<MU!TVxqklL;_-8#J5D{WYxo8-7WZcR3gw060evQO70rTqZ6yEXUp
zP8XdsH6g!iY^xNC-wPFXJE<;sZmH`0Wr@q82FZ55XWPCwzg4+t-~$g5c18nPHqL}L
z55`nx7Dg==0kycQ4TXOv>~%3L-)Lx&y{19ooWsgwz9ma-&7<7}LfoXcUQLskvvl`k
O`ySOhVSammy#N5=ZLd85

literal 0
HcmV?d00001

diff --git a/security-framework/test/cms/keystore.p12 b/security-framework/test/cms/keystore.p12
new file mode 100644
index 0000000000000000000000000000000000000000..0031baf27100488825cd17e4f8acad00a5764051
GIT binary patch
literal 4109
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
Bw(I}^

literal 0
HcmV?d00001

diff --git a/security-framework/test/cms/signed-encrypted.p7m b/security-framework/test/cms/signed-encrypted.p7m
new file mode 100644
index 0000000000000000000000000000000000000000..895c7e3093151159d874b3e0d5a5058659657411
GIT binary patch
literal 1802
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

literal 0
HcmV?d00001

diff --git a/security-framework/test/cms/signed.p7m b/security-framework/test/cms/signed.p7m
new file mode 100644
index 0000000000000000000000000000000000000000..bff2ce69f63a2f83824bdae294b3789e4e67de53
GIT binary patch
literal 1402
zcmXqLVl89i)N1o+`_9YA&a|M3HQk_zHJOQ#(U8}Gn~gJ}&4V$OnT3hbKoOygae)Yn
zP-<RsQDs3%YKlT`YH@L5dMej~CZ-<-O-x@5ni%gaU}j=uV&c&II9Xw{rT!cPUbt>X
zZdL{ZAwvNJJ~rl17G_~~$K>2p1!qSCIdNVi69Y>_GXrx&a|44YAlDL!Yrqe75m*mP
za&EC9&>AIVa~W9~n41{+8GzzkOihf843oY}9?f8T^MB$Kaiec4(uST>RzF|&;EKd~
zhJ1CinMzZ7EuYxjiZ%P&@gU53d0wN<=88uL_nhBic>X1~0#n^*Y1{5^Wp6L<>~8H~
z(7fPO<YXXq^=Lga_mkV<Y=JMH{n^42Yq9w21B3b>vYR*S`F-rlU6R&+w!p7*-Vw9o
zfx>d}>25RD{^a==u;=zS&0v*Z3%&MuM?^KsZsjlw{?hZVvix-mi*di<aV0*lA8u3b
zb-ntoc3h&+PJ30yTiqj^r)8CE>}`3(s`h=5H!ks?Gre76&&5e;n_l|OGE91|xw~4|
zHfND<%1$vp-K|Y~7uN(>g(pU}RVsYqtpAiNT;Ly=&&|Zl$iTQb(7@k-2N=|{!i<dn
zS(pqM3}it(J{B<+kz>1hX5Fu36o37~K3Hwnf1B6IhZh249VpGu!otkNyu&~k#8GA8
zG2mk3&}L&~Wo2h(K@N0aWC8=7ks+PE?nSoKoNmJ}Y6_e;g`D3i&c9*8Ao<<WE+Aam
zHN{M7U-z6i2g4`(-Uf-gJ+>1|`LggtKWm}xD~@p6a&`IF4mJngHXc$u5>Q(BGR-*8
z!qhnBVCBcE4-a(?>Ym{?lAqpF=$0GhcSfa6f7^RgE&d~Q->#OIx}~P;1Sj0&-oK&p
z==3)Im5asq&E@bq{jc)MVbibP#_QGdxwxtpHdi`q_!yVfoyW6}Gsw}$@9tUl^`%<t
z7&leVU-oaO=BhW%EK2WqB3^n%YD?~3Y8x!*KWXdH(4YQOC(F9++hugp>d%F(s}4Q5
zI@K<?A!?;tFJn{eDJOTuXZp8dZpXf_E&Ook{WrrVCOu%<*8*l7137HT8!0a!XR8H`
zPYfjBd4rSL5GV=Djf@5|a1JM{AwMjC7@8QE7+FRc=p$rV43$_E&icB#2+jUFXWyo~
zp(~la0w!@dIkK@9O`YQ+`uLf*%R_@oga&@Y9D^*R49=*9%n?K8FoEJ8;wENxV6KI+
znV6Uu8h~j5!Dn<ZU}t01=3{1JM-|sVPBOrJ%F@Kh(7pI}d(&F;bIz<*47-e#GkRZG
zKK}Fkr1BP{!t3nM#da#4`u}bI!xv#QI~|TjXB}OzC;#rA<=T8+tF~RqpD#aug4?VZ
zpU|2%F`F_w?^c=Mu6=e_|MAP7+_lpuv|##`cT=k4e8Lu|&v^OUn!T>z@SzDW0=p6q
znK7LcFMXiX8PsvUPA*GQ`MCC%`TLIuZ(Kg*`pQVom)c2eN;}T*TAVz8e$lQunVs_(
z`4%z%-=^?=x}N&e&*Gl?_bj{SZ?OqjTr4&HAXlNoYDcFxQLXyRQeLUw))hE-R9EMn
vCbw#}LWlXSopT-<I;p2}Yd<cDSf<k9bBVuG$bkLk>`T0MG3zI-&@lu68F>3k

literal 0
HcmV?d00001

diff --git a/systest/build.rs b/systest/build.rs
index 2cb63ba1..1d2b9116 100644
--- a/systest/build.rs
+++ b/systest/build.rs
@@ -32,6 +32,8 @@ fn main() {
         .header("Security/SecRandom.h")
         .header("Security/SecureTransport.h")
         .header("Security/SecTrust.h")
+        .header("Security/CMSEncoder.h")
+        .header("Security/CMSDecoder.h")
         .flag("-Wno-deprecated-declarations")
         .type_name(|name, _, _| name.to_string())
         .skip_signededness(|s| s.ends_with("Ref") || s.ends_with("Func"))
diff --git a/systest/src/main.rs b/systest/src/main.rs
index 331dc973..55178724 100644
--- a/systest/src/main.rs
+++ b/systest/src/main.rs
@@ -39,5 +39,6 @@ use security_framework_sys::secure_transport::*;
 use security_framework_sys::transform::*;
 use security_framework_sys::trust::*;
 use security_framework_sys::trust_settings::*;
+use security_framework_sys::cms::*;
 
 include!(concat!(env!("OUT_DIR"), "/all.rs"));