Skip to content

Commit

Permalink
Begin migrating PKCS#12 serialization to Rust
Browse files Browse the repository at this point in the history
For now, only handle unencrypted cert-only PKCS#12.
  • Loading branch information
alex committed Mar 22, 2024
1 parent 8bd15a1 commit 42e5b82
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 22 deletions.
5 changes: 5 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/pkcs12.pyi
Expand Up @@ -33,3 +33,8 @@ def load_pkcs12(
password: bytes | None,
backend: typing.Any = None,
) -> PKCS12KeyAndCertificates: ...
def serialize_key_and_certificates(
name: bytes | None,
cert: x509.Certificate | None,
cas: typing.Iterable[x509.Certificate | PKCS12Certificate] | None,
) -> bytes: ...
5 changes: 5 additions & 0 deletions src/cryptography/hazmat/primitives/serialization/pkcs12.py
Expand Up @@ -167,6 +167,11 @@ def serialize_key_and_certificates(
if key is None and cert is None and not cas:
raise ValueError("You must supply at least one of key, cert, or cas")

if key is None and isinstance(
encryption_algorithm, serialization.NoEncryption
):
return rust_pkcs12.serialize_key_and_certificates(name, cert, cas)

from cryptography.hazmat.backends.openssl.backend import backend

return backend.serialize_key_and_certificates_to_pkcs12(
Expand Down
19 changes: 19 additions & 0 deletions src/rust/cryptography-x509/src/common.rs
Expand Up @@ -414,6 +414,25 @@ impl<'a> asn1::SimpleAsn1Writable for UnvalidatedVisibleString<'a> {
}
}

/// A BMPString ASN.1 element, where it is stored as a UTF-8 string in memory.
pub struct Utf8StoredBMPString<'a>(pub &'a str);

impl<'a> Utf8StoredBMPString<'a> {
pub fn new(s: &'a str) -> Self {
Utf8StoredBMPString(s)
}
}

impl<'a> asn1::SimpleAsn1Writable for Utf8StoredBMPString<'a> {
const TAG: asn1::Tag = asn1::BMPString::TAG;
fn write_data(&self, writer: &mut asn1::WriteBuf) -> asn1::WriteResult {
for ch in self.0.encode_utf16() {
writer.push_slice(&ch.to_be_bytes())?;
}
Ok(())
}
}

#[derive(Clone)]
pub struct WithTlv<'a, T> {
tlv: asn1::Tlv<'a>,
Expand Down
37 changes: 19 additions & 18 deletions src/rust/cryptography-x509/src/pkcs12.rs
Expand Up @@ -2,67 +2,68 @@
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
// for complete details.

use crate::common::Utf8StoredBMPString;
use crate::pkcs7;

pub const CERT_BAG_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 12, 10, 1, 3);
pub const KEY_BAG_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 12, 10, 1, 1);
pub const X509_CERTIFICATE_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 9, 22, 1);
pub const FRIENDLY_NAME_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 9, 20);

// #[derive(asn1::Asn1Write)]
#[derive(asn1::Asn1Write)]
pub struct Pfx<'a> {
pub version: u8,
pub auth_safe: pkcs7::ContentInfo<'a>,
pub mac_data: Option<MacData<'a>>,
}

// #[derive(asn1::Asn1Write)]
#[derive(asn1::Asn1Write)]
pub struct MacData<'a> {
pub mac: pkcs7::DigestInfo<'a>,
pub salt: &'a [u8],
// #[default(1u64)]
#[default(1u64)]
pub iterations: u64,
}

// #[derive(asn1::Asn1Write)]
#[derive(asn1::Asn1Write)]
pub struct SafeBag<'a> {
pub _bag_id: asn1::DefinedByMarker<asn1::ObjectIdentifier>,
// #[defined_by(_bag_id)]
#[defined_by(_bag_id)]
pub bag_value: asn1::Explicit<BagValue<'a>, 0>,
// pub attributes: Option<asn1::SetOfWriter<'a, Attribute<'a>>>,
pub attributes: Option<asn1::SetOfWriter<'a, Attribute<'a>, Vec<Attribute<'a>>>>,
}

// #[derive(asn1::Asn1Write)]
#[derive(asn1::Asn1Write)]
pub struct Attribute<'a> {
pub _attr_id: asn1::DefinedByMarker<asn1::ObjectIdentifier>,
// #[defined_by(_attr_id)]
#[defined_by(_attr_id)]
pub attr_values: AttributeSet<'a>,
}

// #[derive(asn1::Asn1DefinedByWrite)]
#[derive(asn1::Asn1DefinedByWrite)]
pub enum AttributeSet<'a> {
// #[defined_by(FRIENDLY_NAME_OID)]
FriendlyName(asn1::SetOfWriter<'a, asn1::BMPString<'a>>),
#[defined_by(FRIENDLY_NAME_OID)]
FriendlyName(asn1::SetOfWriter<'a, Utf8StoredBMPString<'a>, [Utf8StoredBMPString<'a>; 1]>),
}

// #[derive(asn1::Asn1DefinedByWrite)]
#[derive(asn1::Asn1DefinedByWrite)]
pub enum BagValue<'a> {
// #[defined_by(CERT_BAG_OID)]
#[defined_by(CERT_BAG_OID)]
CertBag(CertBag<'a>),

// #[defined_by(KEY_BAG_OID)]
#[defined_by(KEY_BAG_OID)]
KeyBag(asn1::Tlv<'a>),
}

// #[derive(asn1::Asn1Write)]
#[derive(asn1::Asn1Write)]
pub struct CertBag<'a> {
pub _cert_id: asn1::DefinedByMarker<asn1::ObjectIdentifier>,
// #[defined_by(_cert_id)]
#[defined_by(_cert_id)]
pub cert_value: asn1::Explicit<CertType<'a>, 0>,
}

// #[derive(asn1::Asn1DefinedByWrite)]
#[derive(asn1::Asn1DefinedByWrite)]
pub enum CertType<'a> {
// #[defined_by(X509_CERTIFICATE_OID)]
#[defined_by(X509_CERTIFICATE_OID)]
X509(asn1::OctetStringEncoded<crate::certificate::Certificate<'a>>),
}
2 changes: 1 addition & 1 deletion src/rust/cryptography-x509/src/pkcs7.rs
Expand Up @@ -59,7 +59,7 @@ pub struct IssuerAndSerialNumber<'a> {
pub serial_number: asn1::BigInt<'a>,
}

// #[derive(asn1::Asn1Write)]
#[derive(asn1::Asn1Write)]
pub struct DigestInfo<'a> {
pub algorithm: common::AlgorithmIdentifier<'a>,
pub digest: &'a [u8],
Expand Down
160 changes: 157 additions & 3 deletions src/rust/src/pkcs12.rs
Expand Up @@ -2,11 +2,12 @@
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
// for complete details.

use crate::backend::keys;
use crate::backend::{hashes, hmac, keys};
use crate::buf::CffiBuf;
use crate::error::CryptographyResult;
use crate::x509::certificate::Certificate;
use crate::{types, x509};
use cryptography_x509::common::Utf8StoredBMPString;
use pyo3::IntoPy;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
Expand Down Expand Up @@ -76,9 +77,8 @@ impl PKCS12Certificate {
const KDF_ENCRYPTION_KEY_ID: u8 = 1;
#[allow(dead_code)]
const KDF_IV_ID: u8 = 2;
#[allow(dead_code)]
const KDF_MAC_KEY_ID: u8 = 3;
#[allow(dead_code)]

fn pkcs12_kdf(
pass: &[u8],
salt: &[u8],
Expand Down Expand Up @@ -179,6 +179,156 @@ fn pkcs12_kdf(
Ok(result)
}

fn friendly_name_attributes(
friendly_name: Option<&[u8]>,
) -> CryptographyResult<
Option<
asn1::SetOfWriter<
'_,
cryptography_x509::pkcs12::Attribute<'_>,
Vec<cryptography_x509::pkcs12::Attribute<'_>>,
>,
>,
> {
if let Some(name) = friendly_name {
let name_str = std::str::from_utf8(name).map_err(|_| {
pyo3::exceptions::PyValueError::new_err("friendly_name must be valid UTF-8")
})?;

Ok(Some(asn1::SetOfWriter::new(vec![
cryptography_x509::pkcs12::Attribute {
_attr_id: asn1::DefinedByMarker::marker(),
attr_values: cryptography_x509::pkcs12::AttributeSet::FriendlyName(
asn1::SetOfWriter::new([Utf8StoredBMPString::new(name_str)]),
),
},
])))
} else {
Ok(None)
}
}

fn cert_to_bag<'a>(
cert: &'a Certificate,
friendly_name: Option<&'a [u8]>,
) -> CryptographyResult<cryptography_x509::pkcs12::SafeBag<'a>> {
Ok(cryptography_x509::pkcs12::SafeBag {
_bag_id: asn1::DefinedByMarker::marker(),
bag_value: asn1::Explicit::new(cryptography_x509::pkcs12::BagValue::CertBag(
cryptography_x509::pkcs12::CertBag {
_cert_id: asn1::DefinedByMarker::marker(),
cert_value: asn1::Explicit::new(cryptography_x509::pkcs12::CertType::X509(
asn1::OctetStringEncoded::new(cert.raw.borrow_dependent().clone()),
)),
},
)),
attributes: friendly_name_attributes(friendly_name)?,
})
}

fn decode_encryption_algorithm(
py: pyo3::Python<'_>,
) -> CryptographyResult<(&[u8], &pyo3::PyAny, u64)> {
let default_hmac_alg = types::SHA256.get(py)?.call0()?;
let default_hmac_kdf_iter = 2048;

Ok((b"", default_hmac_alg, default_hmac_kdf_iter))
}

#[derive(pyo3::FromPyObject)]
enum CertificateOrPKCS12Certificate {
Certificate(pyo3::Py<Certificate>),
PKCS12Certificate(pyo3::Py<PKCS12Certificate>),
}

#[pyo3::prelude::pyfunction]
#[pyo3(signature = (name, cert, cas))]
fn serialize_key_and_certificates<'p>(
py: pyo3::Python<'p>,
name: Option<&[u8]>,
cert: Option<&Certificate>,
cas: Option<&pyo3::PyAny>,
) -> CryptographyResult<&'p pyo3::types::PyBytes> {
let (password, mac_algorithm, mac_kdf_iter) = decode_encryption_algorithm(py)?;

let mut auth_safe_contents = vec![];
let cert_bag_contents;
let mut ca_certs = vec![];
if cert.is_some() || cas.is_some() {
let mut cert_bags = vec![];

if let Some(cert) = cert {
cert_bags.push(cert_to_bag(cert, name)?);
}

if let Some(cas) = cas {
for cert in cas.iter()? {
ca_certs.push(cert?.extract::<CertificateOrPKCS12Certificate>()?);
}

for cert in &ca_certs {
let bag = match cert {
CertificateOrPKCS12Certificate::Certificate(c) => cert_to_bag(c.get(), None)?,
CertificateOrPKCS12Certificate::PKCS12Certificate(c) => cert_to_bag(
c.get().certificate.get(),
c.get().friendly_name.as_ref().map(|v| v.as_bytes(py)),
)?,
};
cert_bags.push(bag);
}
}

cert_bag_contents = asn1::write_single(&asn1::SequenceOfWriter::new(cert_bags))?;
auth_safe_contents.push(cryptography_x509::pkcs7::ContentInfo {
_content_type: asn1::DefinedByMarker::marker(),
content: cryptography_x509::pkcs7::Content::Data(Some(asn1::Explicit::new(
&cert_bag_contents,
))),
});
}
let auth_safe_content = asn1::write_single(&asn1::SequenceOfWriter::new(auth_safe_contents))?;

let salt = types::OS_URANDOM.get(py)?.call1((8,))?.extract::<&[u8]>()?;
let mac_algorithm_md = hashes::message_digest_from_algorithm(py, mac_algorithm)?;
let mac_key = pkcs12_kdf(
password,
salt,
KDF_MAC_KEY_ID,
mac_kdf_iter,
mac_algorithm_md.size(),
mac_algorithm_md,
)?;
let mac_digest = {
let mut h = hmac::Hmac::new_bytes(py, &mac_key, mac_algorithm)?;
h.update_bytes(&auth_safe_content)?;
h.finalize(py)?
};
let mac_algorithm_identifier = crate::x509::ocsp::HASH_NAME_TO_ALGORITHM_IDENTIFIERS
[mac_algorithm
.getattr(pyo3::intern!(py, "name"))?
.extract::<&str>()?]
.clone();

let p12 = cryptography_x509::pkcs12::Pfx {
version: 3,
auth_safe: cryptography_x509::pkcs7::ContentInfo {
_content_type: asn1::DefinedByMarker::marker(),
content: cryptography_x509::pkcs7::Content::Data(Some(asn1::Explicit::new(
&auth_safe_content,
))),
},
mac_data: Some(cryptography_x509::pkcs12::MacData {
mac: cryptography_x509::pkcs7::DigestInfo {
algorithm: mac_algorithm_identifier,
digest: mac_digest.as_bytes(),
},
salt,
iterations: mac_kdf_iter,
}),
};
Ok(pyo3::types::PyBytes::new(py, &asn1::write_single(&p12)?))
}

fn decode_p12(
data: CffiBuf<'_>,
password: Option<CffiBuf<'_>>,
Expand Down Expand Up @@ -314,6 +464,10 @@ pub(crate) fn create_submodule(py: pyo3::Python<'_>) -> pyo3::PyResult<&pyo3::pr

submod.add_function(pyo3::wrap_pyfunction!(load_key_and_certificates, submod)?)?;
submod.add_function(pyo3::wrap_pyfunction!(load_pkcs12, submod)?)?;
submod.add_function(pyo3::wrap_pyfunction!(
serialize_key_and_certificates,
submod
)?)?;

submod.add_class::<PKCS12Certificate>()?;

Expand Down
2 changes: 2 additions & 0 deletions src/rust/src/types.rs
Expand Up @@ -343,6 +343,8 @@ pub static EXTENDABLE_OUTPUT_FUNCTION: LazyPyImport = LazyPyImport::new(
);
pub static SHA1: LazyPyImport =
LazyPyImport::new("cryptography.hazmat.primitives.hashes", &["SHA1"]);
pub static SHA256: LazyPyImport =
LazyPyImport::new("cryptography.hazmat.primitives.hashes", &["SHA256"]);

pub static PREHASHED: LazyPyImport = LazyPyImport::new(
"cryptography.hazmat.primitives.asymmetric.utils",
Expand Down

0 comments on commit 42e5b82

Please sign in to comment.