Skip to content

Commit 12bc3e8

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 8eed98f commit 12bc3e8

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
@@ -674,13 +674,13 @@ fn compute_pkcs7_signature_algorithm<'p>(
674674
}
675675

676676
#[pyo3::pyfunction]
677-
#[pyo3(signature = (signature, content, certificate, options))]
677+
#[pyo3(signature = (signature, content = None, certificate = None, options = None))]
678678
fn verify_smime<'p>(
679679
py: pyo3::Python<'p>,
680680
signature: &[u8],
681681
content: Option<&[u8]>,
682-
certificate: pyo3::Bound<'p, x509::certificate::Certificate>,
683-
options: &pyo3::Bound<'p, pyo3::types::PyList>,
682+
certificate: Option<pyo3::Bound<'p, x509::certificate::Certificate>>,
683+
options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>,
684684
) -> CryptographyResult<()> {
685685
// Parse the email
686686
let py_content_and_signature = types::SMIME_SIGNED_DECODE.get(py)?.call1((signature,))?;
@@ -708,13 +708,13 @@ fn verify_smime<'p>(
708708
}
709709

710710
#[pyo3::pyfunction]
711-
#[pyo3(signature = (signature, content, certificate, options))]
711+
#[pyo3(signature = (signature, content = None, certificate = None, options = None))]
712712
fn verify_pem<'p>(
713713
py: pyo3::Python<'p>,
714714
signature: &[u8],
715715
content: Option<&[u8]>,
716-
certificate: pyo3::Bound<'p, x509::certificate::Certificate>,
717-
options: &pyo3::Bound<'p, pyo3::types::PyList>,
716+
certificate: Option<pyo3::Bound<'p, x509::certificate::Certificate>>,
717+
options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>,
718718
) -> CryptographyResult<()> {
719719
let pem_str = std::str::from_utf8(signature)
720720
.map_err(|_| pyo3::exceptions::PyValueError::new_err("Invalid PEM data"))?;
@@ -734,15 +734,19 @@ fn verify_pem<'p>(
734734
}
735735

736736
#[pyo3::pyfunction]
737-
#[pyo3(signature = (signature, content, certificate, options))]
737+
#[pyo3(signature = (signature, content = None, certificate = None, options = None))]
738738
fn verify_der<'p>(
739739
py: pyo3::Python<'p>,
740740
signature: &[u8],
741741
content: Option<&[u8]>,
742-
certificate: pyo3::Bound<'p, x509::certificate::Certificate>,
743-
options: &pyo3::Bound<'p, pyo3::types::PyList>,
742+
certificate: Option<pyo3::Bound<'p, x509::certificate::Certificate>>,
743+
options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>,
744744
) -> CryptographyResult<()> {
745745
// Check the verify options
746+
let options = match options {
747+
Some(options) => options,
748+
None => &pyo3::types::PyList::empty(py),
749+
};
746750
check_verify_options(py, options)?;
747751

748752
// Verify the data
@@ -752,57 +756,116 @@ fn verify_der<'p>(
752756
// Extract signed data
753757
let signed_data = signed_data.into_inner();
754758

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

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

807870
// Verify the certificate
808871
if !options.contains(types::PKCS7_NO_VERIFY.get(py)?)? {
@@ -843,11 +906,12 @@ fn check_verify_options<'p>(
843906

844907
// Check if any option is not PKCS7Options::NoVerify
845908
let no_verify_option = types::PKCS7_NO_VERIFY.get(py)?;
909+
let no_sigs_option = types::PKCS7_NO_SIGS.get(py)?;
846910
for opt in options.iter() {
847-
if !opt.eq(no_verify_option.clone())? {
911+
if opt.ne(no_verify_option.clone())? & opt.ne(no_sigs_option.clone())? {
848912
return Err(CryptographyError::from(
849913
pyo3::exceptions::PyValueError::new_err(
850-
"Only the following options are supported for verification: NoVerify",
914+
"Only the following options are supported for verification: NoVerify, NoSigs",
851915
),
852916
));
853917
}

src/rust/src/types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,10 @@ pub static PKCS7_NO_VERIFY: LazyPyImport = LazyPyImport::new(
358358
"cryptography.hazmat.primitives.serialization.pkcs7",
359359
&["PKCS7Options", "NoVerify"],
360360
);
361+
pub static PKCS7_NO_SIGS: LazyPyImport = LazyPyImport::new(
362+
"cryptography.hazmat.primitives.serialization.pkcs7",
363+
&["PKCS7Options", "NoSigs"],
364+
);
361365

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

0 commit comments

Comments
 (0)