Skip to content

Commit 1e92f20

Browse files
committed
feat: functions now have optional keyword arguments
certificate is now optional feat: handling RSA case feat: No signature parameter adapted tests accordingly
1 parent f2f9be4 commit 1e92f20

File tree

8 files changed

+313
-129
lines changed

8 files changed

+313
-129
lines changed

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,21 @@ def sign_and_serialize(
4444
) -> bytes: ...
4545
def verify_der(
4646
signature: bytes,
47-
content: bytes | None,
48-
certificate: x509.Certificate,
49-
options: Iterable[pkcs7.PKCS7Options],
47+
content: bytes | None = None,
48+
certificate: x509.Certificate | None = None,
49+
options: Iterable[pkcs7.PKCS7Options] | None = None,
5050
) -> None: ...
5151
def verify_pem(
5252
signature: bytes,
53-
content: bytes | None,
54-
certificate: x509.Certificate,
55-
options: Iterable[pkcs7.PKCS7Options],
53+
content: bytes | None = None,
54+
certificate: x509.Certificate | None = None,
55+
options: Iterable[pkcs7.PKCS7Options] | None = None,
5656
) -> None: ...
5757
def verify_smime(
5858
signature: bytes,
59-
content: bytes | None,
60-
certificate: x509.Certificate,
61-
options: Iterable[pkcs7.PKCS7Options],
59+
content: bytes | None = None,
60+
certificate: x509.Certificate | None = None,
61+
options: Iterable[pkcs7.PKCS7Options] | None = None,
6262
) -> None: ...
6363
def load_pem_pkcs7_certificates(
6464
data: bytes,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class PKCS7Options(utils.Enum):
5353
NoAttributes = "Don't embed authenticatedAttributes"
5454
NoCerts = "Don't embed signer certificate"
5555
NoVerify = "Don't verify signers certificate"
56+
NoSigs = "Don't verify signature"
5657

5758

5859
class PKCS7SignatureBuilder:

src/rust/src/pkcs7.rs

Lines changed: 116 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -670,13 +670,13 @@ fn compute_pkcs7_signature_algorithm<'p>(
670670
}
671671

672672
#[pyo3::pyfunction]
673-
#[pyo3(signature = (signature, content, certificate, options))]
673+
#[pyo3(signature = (signature, content = None, certificate = None, options = None))]
674674
fn verify_smime<'p>(
675675
py: pyo3::Python<'p>,
676676
signature: &[u8],
677677
content: Option<&[u8]>,
678-
certificate: pyo3::Bound<'p, x509::certificate::Certificate>,
679-
options: &pyo3::Bound<'p, pyo3::types::PyList>,
678+
certificate: Option<pyo3::Bound<'p, x509::certificate::Certificate>>,
679+
options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>,
680680
) -> CryptographyResult<()> {
681681
// Parse the email
682682
let py_content_and_signature = types::SMIME_SIGNED_DECODE.get(py)?.call1((signature,))?;
@@ -704,13 +704,13 @@ fn verify_smime<'p>(
704704
}
705705

706706
#[pyo3::pyfunction]
707-
#[pyo3(signature = (signature, content, certificate, options))]
707+
#[pyo3(signature = (signature, content = None, certificate = None, options = None))]
708708
fn verify_pem<'p>(
709709
py: pyo3::Python<'p>,
710710
signature: &[u8],
711711
content: Option<&[u8]>,
712-
certificate: pyo3::Bound<'p, x509::certificate::Certificate>,
713-
options: &pyo3::Bound<'p, pyo3::types::PyList>,
712+
certificate: Option<pyo3::Bound<'p, x509::certificate::Certificate>>,
713+
options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>,
714714
) -> CryptographyResult<()> {
715715
let pem_str = std::str::from_utf8(signature)
716716
.map_err(|_| pyo3::exceptions::PyValueError::new_err("Invalid PEM data"))?;
@@ -730,15 +730,19 @@ fn verify_pem<'p>(
730730
}
731731

732732
#[pyo3::pyfunction]
733-
#[pyo3(signature = (signature, content, certificate, options))]
733+
#[pyo3(signature = (signature, content = None, certificate = None, options = None))]
734734
fn verify_der<'p>(
735735
py: pyo3::Python<'p>,
736736
signature: &[u8],
737737
content: Option<&[u8]>,
738-
certificate: pyo3::Bound<'p, x509::certificate::Certificate>,
739-
options: &pyo3::Bound<'p, pyo3::types::PyList>,
738+
certificate: Option<pyo3::Bound<'p, x509::certificate::Certificate>>,
739+
options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>,
740740
) -> CryptographyResult<()> {
741741
// Check the verify options
742+
let options = match options {
743+
Some(options) => options,
744+
None => &pyo3::types::PyList::empty(py),
745+
};
742746
check_verify_options(py, options)?;
743747

744748
// Verify the data
@@ -748,57 +752,116 @@ fn verify_der<'p>(
748752
// Extract signed data
749753
let signed_data = signed_data.into_inner();
750754

751-
// Get recipients, and find the one matching with the signer infos (if any)
752-
// TODO: what to do for multiple certificates?
753-
let mut signer_infos = signed_data.signer_infos.unwrap_read().clone();
754-
let signer_certificate = certificate.get().raw.borrow_dependent();
755-
let signer_serial_number = signer_certificate.tbs_cert.serial;
756-
let signer_issuer = signer_certificate.tbs_cert.issuer.clone();
757-
let found_signer_info = signer_infos.find(|info| {
758-
info.issuer_and_serial_number.serial_number == signer_serial_number
759-
&& info.issuer_and_serial_number.issuer == signer_issuer
760-
});
761-
762-
// Raise error when no signer is found
763-
let signer_info = match found_signer_info {
764-
Some(info) => info,
755+
// Extract the signer certificate: either from given value, or from signed data
756+
let certificate = match certificate {
757+
Some(cert) => cert,
765758
None => {
766-
return Err(CryptographyError::from(
767-
pyo3::exceptions::PyValueError::new_err(
768-
"No signer found that matches the given certificate.",
769-
),
770-
));
759+
let certificates = signed_data.certificates;
760+
match certificates {
761+
Some(certificates) => {
762+
let mut certificates = certificates.unwrap_read().clone();
763+
match certificates.next() {
764+
Some(cert) => {
765+
let cert_bytes =
766+
pyo3::types::PyBytes::new(py, &asn1::write_single(&cert)?)
767+
.unbind();
768+
let py_cert = load_der_x509_certificate(py, cert_bytes, None)?;
769+
pyo3::Bound::new(py, py_cert)?
770+
}
771+
None => {
772+
return Err(CryptographyError::from(
773+
pyo3::exceptions::PyValueError::new_err(
774+
"The PKCS7 data does not contain any certificate.",
775+
),
776+
));
777+
}
778+
}
779+
}
780+
None => {
781+
return Err(CryptographyError::from(
782+
pyo3::exceptions::PyValueError::new_err(
783+
"The PKCS7 data does not contain any certificate.",
784+
),
785+
));
786+
}
787+
}
771788
}
772789
};
773790

774-
// Prepare the content: try to use the authenticated attributes, then the content stored
775-
// in the signed data, then the provided content. If None of these are available, raise
776-
// an error. TODO: what should the order be?
777-
let data = match signer_info.authenticated_attributes {
778-
Some(attrs) => Cow::Owned(asn1::write_single(&attrs)?),
779-
None => match content {
780-
Some(data) => Cow::Borrowed(data),
781-
None => match signed_data.content_info.content {
782-
pkcs7::Content::Data(Some(data)) => Cow::Borrowed(data.into_inner()),
791+
// Get recipients, and find the one matching with the signer infos (if any)
792+
// TODO: what to do for multiple certificates?
793+
if !options.contains(types::PKCS7_NO_SIGS.get(py)?)? {
794+
let mut signer_infos = signed_data.signer_infos.unwrap_read().clone();
795+
let signer_certificate = certificate.get().raw.borrow_dependent();
796+
let signer_serial_number = signer_certificate.tbs_cert.serial;
797+
let signer_issuer = signer_certificate.tbs_cert.issuer.clone();
798+
let found_signer_info = signer_infos.find(|info| {
799+
info.issuer_and_serial_number.serial_number == signer_serial_number
800+
&& info.issuer_and_serial_number.issuer == signer_issuer
801+
});
802+
803+
// Raise error when no signer is found
804+
let signer_info = match found_signer_info {
805+
Some(info) => info,
806+
None => {
807+
return Err(CryptographyError::from(
808+
pyo3::exceptions::PyValueError::new_err(
809+
"No signer found that matches the given certificate.",
810+
),
811+
));
812+
}
813+
};
814+
815+
// Prepare the content: try to use the authenticated attributes, then the content stored
816+
// in the signed data, then the provided content. If None of these are available, raise
817+
// an error. TODO: what should the order be?
818+
let data = match signer_info.authenticated_attributes {
819+
Some(attrs) => Cow::Owned(asn1::write_single(&attrs)?),
820+
None => match content {
821+
Some(data) => Cow::Borrowed(data),
822+
None => match signed_data.content_info.content {
823+
pkcs7::Content::Data(Some(data)) => Cow::Borrowed(data.into_inner()),
824+
_ => {
825+
return Err(CryptographyError::from(
826+
pyo3::exceptions::PyValueError::new_err(
827+
"No content stored in the signature or provided.",
828+
),
829+
));
830+
}
831+
},
832+
},
833+
};
834+
835+
// For RSA signatures (with no PSS padding), the OID is always the same no matter the
836+
// digest algorithm. We need to modify the algorithm identifier to add the hash
837+
// algorithm information. We are checking for RSA-256, which the S/MIME v3.2 RFC
838+
// specifies as MUST support (https://datatracker.ietf.org/doc/html/rfc5751#section-2.2)
839+
let signature_algorithm = match signer_info.digest_encryption_algorithm.oid() {
840+
&oid::RSA_OID => match signer_info.digest_algorithm.oid() {
841+
&oid::SHA256_OID => common::AlgorithmIdentifier {
842+
oid: asn1::DefinedByMarker::marker(),
843+
params: common::AlgorithmParameters::RsaWithSha256(Some(())),
844+
},
783845
_ => {
784846
return Err(CryptographyError::from(
785847
pyo3::exceptions::PyValueError::new_err(
786-
"No content stored in the signature or provided.",
848+
"Unsupported hash algorithm with RSA.",
787849
),
788-
));
850+
))
789851
}
790852
},
791-
},
792-
};
793-
794-
// Verify the signature
795-
x509::sign::verify_signature_with_signature_algorithm(
796-
py,
797-
certificate.call_method0(pyo3::intern!(py, "public_key"))?,
798-
&signer_info.digest_encryption_algorithm,
799-
signer_info.encrypted_digest,
800-
&data,
801-
)?;
853+
_ => signer_info.digest_encryption_algorithm,
854+
};
855+
856+
// Verify the signature
857+
x509::sign::verify_signature_with_signature_algorithm(
858+
py,
859+
certificate.call_method0(pyo3::intern!(py, "public_key"))?,
860+
&signature_algorithm,
861+
signer_info.encrypted_digest,
862+
&data,
863+
)?;
864+
}
802865

803866
// Verify the certificate
804867
if !options.contains(types::PKCS7_NO_VERIFY.get(py)?)? {
@@ -839,11 +902,12 @@ fn check_verify_options<'p>(
839902

840903
// Check if any option is not PKCS7Options::NoVerify
841904
let no_verify_option = types::PKCS7_NO_VERIFY.get(py)?;
905+
let no_sigs_option = types::PKCS7_NO_SIGS.get(py)?;
842906
for opt in options.iter() {
843-
if !opt.eq(no_verify_option.clone())? {
907+
if opt.ne(no_verify_option.clone())? & opt.ne(no_sigs_option.clone())? {
844908
return Err(CryptographyError::from(
845909
pyo3::exceptions::PyValueError::new_err(
846-
"Only the following options are supported for verification: NoVerify",
910+
"Only the following options are supported for verification: NoVerify, NoSigs",
847911
),
848912
));
849913
}

src/rust/src/types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,10 @@ pub static PKCS7_NO_VERIFY: LazyPyImport = LazyPyImport::new(
353353
"cryptography.hazmat.primitives.serialization.pkcs7",
354354
&["PKCS7Options", "NoVerify"],
355355
);
356+
pub static PKCS7_NO_SIGS: LazyPyImport = LazyPyImport::new(
357+
"cryptography.hazmat.primitives.serialization.pkcs7",
358+
&["PKCS7Options", "NoSigs"],
359+
);
356360

357361
pub static SMIME_ENVELOPED_ENCODE: LazyPyImport = LazyPyImport::new(
358362
"cryptography.hazmat.primitives.serialization.pkcs7",

0 commit comments

Comments
 (0)