Skip to content

Commit e1c4620

Browse files
committed
first python API proposition
first round-trip tests feat: made asn1 structures readable refacto: adapted existing functions accordingly feat/pkcs12: added symmetric_decrypt feat: deserialize 3 possible encodings feat: handling AES-128 & AES-256 CBC feat: raise error when no recipient is found feat/pkcs7: added decanonicalize function feat/asn1: added decode_der_data feat/pkcs7: added smime_enveloped_decode tests are the round-trip (encrypt & decrypt)
1 parent 914b1d2 commit e1c4620

File tree

8 files changed

+497
-29
lines changed

8 files changed

+497
-29
lines changed

src/cryptography/hazmat/bindings/_rust/pkcs7.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ def sign_and_serialize(
2222
encoding: serialization.Encoding,
2323
options: typing.Iterable[pkcs7.PKCS7Options],
2424
) -> bytes: ...
25+
def deserialize_and_decrypt(
26+
decryptor: pkcs7.PKCS7EnvelopeDecryptor,
27+
encoding: serialization.Encoding,
28+
options: typing.Iterable[pkcs7.PKCS7Options],
29+
) -> bytes: ...
2530
def load_pem_pkcs7_certificates(
2631
data: bytes,
2732
) -> list[x509.Certificate]: ...

src/cryptography/hazmat/primitives/serialization/pkcs7.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,111 @@ def encrypt(
263263
return rust_pkcs7.encrypt_and_serialize(self, encoding, options)
264264

265265

266+
class PKCS7EnvelopeDecryptor:
267+
def __init__(
268+
self,
269+
*,
270+
_data: bytes | None = None,
271+
_recipient: x509.Certificate | None = None,
272+
_private_key: rsa.RSAPrivateKey | None = None,
273+
):
274+
from cryptography.hazmat.backends.openssl.backend import (
275+
backend as ossl,
276+
)
277+
278+
if not ossl.rsa_encryption_supported(padding=padding.PKCS1v15()):
279+
raise UnsupportedAlgorithm(
280+
"RSA with PKCS1 v1.5 padding is not supported by this version"
281+
" of OpenSSL.",
282+
_Reasons.UNSUPPORTED_PADDING,
283+
)
284+
self._data = _data
285+
self._recipient = _recipient
286+
self._private_key = _private_key
287+
288+
def set_data(self, data: bytes) -> PKCS7EnvelopeDecryptor:
289+
_check_byteslike("data", data)
290+
if self._data is not None:
291+
raise ValueError("data may only be set once")
292+
293+
return PKCS7EnvelopeDecryptor(
294+
_data=data,
295+
_recipient=self._recipient,
296+
_private_key=self._private_key,
297+
)
298+
299+
def set_recipient(
300+
self, certificate: x509.Certificate
301+
) -> PKCS7EnvelopeDecryptor:
302+
if self._recipient is not None:
303+
raise ValueError("recipient may only be set once")
304+
305+
if not isinstance(certificate, x509.Certificate):
306+
raise TypeError("certificate must be a x509.Certificate")
307+
308+
if not isinstance(certificate.public_key(), rsa.RSAPublicKey):
309+
raise TypeError("Only RSA keys are supported at this time.")
310+
311+
return PKCS7EnvelopeDecryptor(
312+
_data=self._data,
313+
_recipient=certificate,
314+
_private_key=self._private_key,
315+
)
316+
317+
def set_private_key(
318+
self, private_key: rsa.RSAPrivateKey
319+
) -> PKCS7EnvelopeDecryptor:
320+
if self._private_key is not None:
321+
raise ValueError("private key may only be set once")
322+
323+
return PKCS7EnvelopeDecryptor(
324+
_data=self._data,
325+
_recipient=self._recipient,
326+
_private_key=private_key,
327+
)
328+
329+
def decrypt(
330+
self,
331+
encoding: serialization.Encoding,
332+
options: typing.Iterable[PKCS7Options],
333+
) -> bytes:
334+
if self._data is None:
335+
raise ValueError("You must add data to decrypt")
336+
if self._recipient is None:
337+
raise ValueError("You must add a recipient to decrypt")
338+
if self._private_key is None:
339+
raise ValueError("You must add a private key to decrypt")
340+
options = list(options)
341+
if not all(isinstance(x, PKCS7Options) for x in options):
342+
raise ValueError("options must be from the PKCS7Options enum")
343+
if encoding not in (
344+
serialization.Encoding.PEM,
345+
serialization.Encoding.DER,
346+
serialization.Encoding.SMIME,
347+
):
348+
raise ValueError(
349+
"Must be PEM, DER, or SMIME from the Encoding enum"
350+
)
351+
352+
# Only allow options that make sense for encryption
353+
if any(
354+
opt not in [PKCS7Options.Text, PKCS7Options.Binary]
355+
for opt in options
356+
):
357+
raise ValueError(
358+
"Only the following options are supported for encryption: "
359+
"Text, Binary"
360+
)
361+
elif PKCS7Options.Text in options and PKCS7Options.Binary in options:
362+
# OpenSSL accepts both options at the same time, but ignores Text.
363+
# We fail defensively to avoid unexpected outputs.
364+
raise ValueError(
365+
"Cannot use Binary and Text options at the same time"
366+
)
367+
368+
return rust_pkcs7.deserialize_and_decrypt(self, encoding, options)
369+
370+
266371
def _smime_signed_encode(
267372
data: bytes, signature: bytes, micalg: str, text_mode: bool
268373
) -> bytes:
@@ -328,6 +433,13 @@ def _smime_enveloped_encode(data: bytes) -> bytes:
328433
return m.as_bytes(policy=m.policy.clone(linesep="\n", max_line_length=0))
329434

330435

436+
def _smime_enveloped_decode(data: bytes) -> bytes:
437+
m = email.message_from_bytes(data)
438+
if m.get_content_type() != "application/pkcs7-mime":
439+
raise ValueError("Not an S/MIME enveloped message")
440+
return bytes(m.get_payload(decode=True))
441+
442+
331443
class OpenSSLMimePart(email.message.MIMEPart):
332444
# A MIMEPart subclass that replicates OpenSSL's behavior of not including
333445
# a newline if there are no headers.

src/rust/cryptography-x509/src/pkcs7.rs

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ pub const PKCS7_SIGNED_DATA_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840,
99
pub const PKCS7_ENVELOPED_DATA_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 7, 3);
1010
pub const PKCS7_ENCRYPTED_DATA_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 7, 6);
1111

12-
#[derive(asn1::Asn1Write)]
12+
#[derive(asn1::Asn1Write, asn1::Asn1Read)]
1313
pub struct ContentInfo<'a> {
1414
pub _content_type: asn1::DefinedByMarker<asn1::ObjectIdentifier>,
1515

1616
#[defined_by(_content_type)]
1717
pub content: Content<'a>,
1818
}
1919

20-
#[derive(asn1::Asn1DefinedByWrite)]
20+
#[derive(asn1::Asn1DefinedByWrite, asn1::Asn1DefinedByRead)]
2121
pub enum Content<'a> {
2222
#[defined_by(PKCS7_ENVELOPED_DATA_OID)]
2323
EnvelopedData(asn1::Explicit<Box<EnvelopedData<'a>>, 0>),
@@ -29,22 +29,38 @@ pub enum Content<'a> {
2929
EncryptedData(asn1::Explicit<EncryptedData<'a>, 0>),
3030
}
3131

32-
#[derive(asn1::Asn1Write)]
32+
#[derive(asn1::Asn1Write, asn1::Asn1Read)]
3333
pub struct SignedData<'a> {
3434
pub version: u8,
35-
pub digest_algorithms: asn1::SetOfWriter<'a, common::AlgorithmIdentifier<'a>>,
35+
pub digest_algorithms: common::Asn1ReadableOrWritable<
36+
asn1::SetOf<'a, common::AlgorithmIdentifier<'a>>,
37+
asn1::SetOfWriter<'a, common::AlgorithmIdentifier<'a>>,
38+
>,
3639
pub content_info: ContentInfo<'a>,
3740
#[implicit(0)]
38-
pub certificates: Option<asn1::SetOfWriter<'a, &'a certificate::Certificate<'a>>>,
41+
pub certificates: Option<
42+
common::Asn1ReadableOrWritable<
43+
asn1::SetOf<'a, certificate::Certificate<'a>>,
44+
asn1::SetOfWriter<'a, &'a certificate::Certificate<'a>>,
45+
>,
46+
>,
3947

4048
// We don't ever supply any of these, so for now, don't fill out the fields.
4149
#[implicit(1)]
42-
pub crls: Option<asn1::SetOfWriter<'a, asn1::Sequence<'a>>>,
43-
44-
pub signer_infos: asn1::SetOfWriter<'a, SignerInfo<'a>>,
50+
pub crls: Option<
51+
common::Asn1ReadableOrWritable<
52+
asn1::SetOf<'a, asn1::Sequence<'a>>,
53+
asn1::SetOfWriter<'a, asn1::Sequence<'a>>,
54+
>,
55+
>,
56+
57+
pub signer_infos: common::Asn1ReadableOrWritable<
58+
asn1::SetOf<'a, SignerInfo<'a>>,
59+
asn1::SetOfWriter<'a, SignerInfo<'a>>,
60+
>,
4561
}
4662

47-
#[derive(asn1::Asn1Write)]
63+
#[derive(asn1::Asn1Write, asn1::Asn1Read)]
4864
pub struct SignerInfo<'a> {
4965
pub version: u8,
5066
pub issuer_and_serial_number: IssuerAndSerialNumber<'a>,
@@ -59,42 +75,45 @@ pub struct SignerInfo<'a> {
5975
pub unauthenticated_attributes: Option<csr::Attributes<'a>>,
6076
}
6177

62-
#[derive(asn1::Asn1Write)]
78+
#[derive(asn1::Asn1Write, asn1::Asn1Read)]
6379
pub struct EnvelopedData<'a> {
6480
pub version: u8,
65-
pub recipient_infos: asn1::SetOfWriter<'a, RecipientInfo<'a>>,
81+
pub recipient_infos: common::Asn1ReadableOrWritable<
82+
asn1::SetOf<'a, RecipientInfo<'a>>,
83+
asn1::SetOfWriter<'a, RecipientInfo<'a>>,
84+
>,
6685
pub encrypted_content_info: EncryptedContentInfo<'a>,
6786
}
6887

69-
#[derive(asn1::Asn1Write)]
88+
#[derive(asn1::Asn1Write, asn1::Asn1Read)]
7089
pub struct RecipientInfo<'a> {
7190
pub version: u8,
7291
pub issuer_and_serial_number: IssuerAndSerialNumber<'a>,
7392
pub key_encryption_algorithm: common::AlgorithmIdentifier<'a>,
7493
pub encrypted_key: &'a [u8],
7594
}
7695

77-
#[derive(asn1::Asn1Write)]
96+
#[derive(asn1::Asn1Write, asn1::Asn1Read)]
7897
pub struct IssuerAndSerialNumber<'a> {
7998
pub issuer: name::Name<'a>,
8099
pub serial_number: asn1::BigInt<'a>,
81100
}
82101

83-
#[derive(asn1::Asn1Write)]
102+
#[derive(asn1::Asn1Write, asn1::Asn1Read)]
84103
pub struct EncryptedData<'a> {
85104
pub version: u8,
86105
pub encrypted_content_info: EncryptedContentInfo<'a>,
87106
}
88107

89-
#[derive(asn1::Asn1Write)]
108+
#[derive(asn1::Asn1Write, asn1::Asn1Read)]
90109
pub struct EncryptedContentInfo<'a> {
91110
pub content_type: asn1::ObjectIdentifier,
92111
pub content_encryption_algorithm: common::AlgorithmIdentifier<'a>,
93112
#[implicit(0)]
94113
pub encrypted_content: Option<&'a [u8]>,
95114
}
96115

97-
#[derive(asn1::Asn1Write)]
116+
#[derive(asn1::Asn1Write, asn1::Asn1Read)]
98117
pub struct DigestInfo<'a> {
99118
pub algorithm: common::AlgorithmIdentifier<'a>,
100119
pub digest: &'a [u8],

src/rust/src/asn1.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,31 @@ pub(crate) fn encode_der_data<'p>(
114114
}
115115
}
116116

117+
pub(crate) fn decode_der_data<'p>(
118+
py: pyo3::Python<'p>,
119+
pem_tag: String,
120+
data: Vec<u8>,
121+
encoding: &pyo3::Bound<'p, pyo3::PyAny>,
122+
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
123+
if encoding.is(&types::ENCODING_DER.get(py)?) {
124+
Ok(pyo3::types::PyBytes::new_bound(py, &data))
125+
} else if encoding.is(&types::ENCODING_PEM.get(py)?) {
126+
let pem_str = std::str::from_utf8(&data)
127+
.map_err(|_| pyo3::exceptions::PyValueError::new_err("Invalid PEM data"))?;
128+
let pem = pem::parse(pem_str)
129+
.map_err(|_| pyo3::exceptions::PyValueError::new_err("Failed to parse PEM data"))?;
130+
if pem.tag() != pem_tag {
131+
return Err(pyo3::exceptions::PyValueError::new_err("PEM tag mismatch").into());
132+
}
133+
Ok(pyo3::types::PyBytes::new_bound(py, pem.contents()))
134+
} else {
135+
Err(
136+
pyo3::exceptions::PyTypeError::new_err("encoding must be Encoding.DER or Encoding.PEM")
137+
.into(),
138+
)
139+
}
140+
}
141+
117142
#[pyo3::pyfunction]
118143
fn encode_dss_signature<'p>(
119144
py: pyo3::Python<'p>,

src/rust/src/pkcs12.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use crate::backend::{ciphers, hashes, hmac, kdf, keys};
66
use crate::buf::CffiBuf;
77
use crate::error::{CryptographyError, CryptographyResult};
8-
use crate::padding::PKCS7PaddingContext;
8+
use crate::padding::{PKCS7PaddingContext, PKCS7UnpaddingContext};
99
use crate::x509::certificate::Certificate;
1010
use crate::{types, x509};
1111
use cryptography_x509::common::Utf8StoredBMPString;
@@ -107,6 +107,40 @@ pub(crate) fn symmetric_encrypt(
107107
Ok(ciphertext)
108108
}
109109

110+
pub(crate) fn symmetric_decrypt(
111+
py: pyo3::Python<'_>,
112+
algorithm: pyo3::Bound<'_, pyo3::PyAny>,
113+
mode: pyo3::Bound<'_, pyo3::PyAny>,
114+
data: &[u8],
115+
) -> CryptographyResult<Vec<u8>> {
116+
let block_size = algorithm
117+
.getattr(pyo3::intern!(py, "block_size"))?
118+
.extract()?;
119+
120+
let mut cipher =
121+
ciphers::CipherContext::new(py, algorithm, mode, openssl::symm::Mode::Decrypt)?;
122+
123+
// Decrypt the data
124+
let mut decrypted_data = vec![0; data.len() + (block_size / 8)];
125+
let count = cipher.update_into(py, data, &mut decrypted_data)?;
126+
let final_block = cipher.finalize(py)?;
127+
assert!(final_block.as_bytes().is_empty());
128+
decrypted_data.truncate(count);
129+
130+
// Unpad the data
131+
let mut unpadder = PKCS7UnpaddingContext::new(block_size);
132+
let unpadded_first_blocks = unpadder.update(py, CffiBuf::from_bytes(py, &decrypted_data))?;
133+
let unpadded_last_block = unpadder.finalize(py)?;
134+
135+
let unpadded_data = [
136+
unpadded_first_blocks.as_bytes(),
137+
unpadded_last_block.as_bytes(),
138+
]
139+
.concat();
140+
141+
Ok(unpadded_data)
142+
}
143+
110144
enum EncryptionAlgorithm {
111145
PBESv1SHA1And3KeyTripleDESCBC,
112146
PBESv2SHA256AndAES256CBC,

0 commit comments

Comments
 (0)