diff options
author | Paul Kehrer <paul.l.kehrer@gmail.com> | 2023-05-11 09:09:56 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-10 21:09:56 -0400 |
commit | 1ef3cdb616c7a304e75c89ad458e49c1fbd5943f (patch) | |
tree | a3b74f2436f731e98f760ea7169b4ba29b76867e | |
parent | cfee3c85a7d7e9c60f8041678c3070380ac3ca3d (diff) | |
download | cryptography-1ef3cdb616c7a304e75c89ad458e49c1fbd5943f.tar.gz |
support X.509 certificate PSS signing (#8888)
* support X.509 certificate PSS signing
no CSR, CRL, etc
* handle PSS.(MAX_LENGTH, DIGEST_LENGTH), review feedback
* name the kwarg
* test improvements
* skip if sha3 isn't supported
-rw-r--r-- | CHANGELOG.rst | 3 | ||||
-rw-r--r-- | docs/x509/reference.rst | 18 | ||||
-rw-r--r-- | src/cryptography/hazmat/bindings/_rust/x509.pyi | 2 | ||||
-rw-r--r-- | src/cryptography/x509/base.py | 14 | ||||
-rw-r--r-- | src/rust/cryptography-x509/src/common.rs | 18 | ||||
-rw-r--r-- | src/rust/src/pkcs7.rs | 17 | ||||
-rw-r--r-- | src/rust/src/x509/certificate.rs | 7 | ||||
-rw-r--r-- | src/rust/src/x509/crl.rs | 16 | ||||
-rw-r--r-- | src/rust/src/x509/csr.rs | 15 | ||||
-rw-r--r-- | src/rust/src/x509/ocsp_resp.rs | 15 | ||||
-rw-r--r-- | src/rust/src/x509/sign.rs | 145 | ||||
-rw-r--r-- | tests/x509/test_x509.py | 163 |
12 files changed, 405 insertions, 28 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d4fd57624..5073ce32b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,9 @@ Changelog * Added support for obtaining X.509 certificate signature algorithm parameters (including PSS) via :meth:`~cryptography.x509.Certificate.signature_algorithm_parameters`. +* Support signing :class:`~cryptography.hazmat.primitives.asymmetric.padding.PSS` + X.509 certificates via the new keyword-only argument ``rsa_padding`` on + :meth:`~cryptography.x509.CertificateBuilder.sign`. .. _v40-0-2: diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst index 647666c5c..e14c8ffc1 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -872,7 +872,7 @@ X.509 Certificate Builder :param critical: Set to ``True`` if the extension must be understood and handled by whoever reads the certificate. - .. method:: sign(private_key, algorithm) + .. method:: sign(private_key, algorithm, *, rsa_padding=None) Sign the certificate using the CA's private key. @@ -891,6 +891,22 @@ X.509 Certificate Builder :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` otherwise. + :param rsa_padding: + + .. versionadded:: 41.0.0 + + This is a keyword-only argument. If ``private_key`` is an + ``RSAPrivateKey`` then this can be set to either + :class:`~cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15` or + :class:`~cryptography.hazmat.primitives.asymmetric.padding.PSS` to sign + with those respective paddings. If this is ``None`` then RSA + keys will default to ``PKCS1v15`` padding. All other key types **must** + not pass a value other than ``None``. + + :type rsa_padding: ``None``, + :class:`~cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15`, + or :class:`~cryptography.hazmat.primitives.asymmetric.padding.PSS` + :returns: :class:`~cryptography.x509.Certificate` diff --git a/src/cryptography/hazmat/bindings/_rust/x509.pyi b/src/cryptography/hazmat/bindings/_rust/x509.pyi index 71c8d5c22..24b2f5e3a 100644 --- a/src/cryptography/hazmat/bindings/_rust/x509.pyi +++ b/src/cryptography/hazmat/bindings/_rust/x509.pyi @@ -6,6 +6,7 @@ import typing from cryptography import x509 from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric.padding import PSS, PKCS1v15 from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes def load_pem_x509_certificate(data: bytes) -> x509.Certificate: ... @@ -23,6 +24,7 @@ def create_x509_certificate( builder: x509.CertificateBuilder, private_key: PrivateKeyTypes, hash_algorithm: typing.Optional[hashes.HashAlgorithm], + padding: typing.Optional[typing.Union[PKCS1v15, PSS]], ) -> x509.Certificate: ... def create_x509_csr( builder: x509.CertificateSigningRequestBuilder, diff --git a/src/cryptography/x509/base.py b/src/cryptography/x509/base.py index 64453eb70..576385e08 100644 --- a/src/cryptography/x509/base.py +++ b/src/cryptography/x509/base.py @@ -924,6 +924,10 @@ class CertificateBuilder: private_key: CertificateIssuerPrivateKeyTypes, algorithm: typing.Optional[_AllowedHashTypes], backend: typing.Any = None, + *, + rsa_padding: typing.Optional[ + typing.Union[padding.PSS, padding.PKCS1v15] + ] = None, ) -> Certificate: """ Signs the certificate using the CA's private key. @@ -946,7 +950,15 @@ class CertificateBuilder: if self._public_key is None: raise ValueError("A certificate must have a public key") - return rust_x509.create_x509_certificate(self, private_key, algorithm) + if rsa_padding is not None: + if not isinstance(rsa_padding, (padding.PSS, padding.PKCS1v15)): + raise TypeError("Padding must be PSS or PKCS1v15") + if not isinstance(private_key, rsa.RSAPrivateKey): + raise TypeError("Padding is only supported for RSA keys") + + return rust_x509.create_x509_certificate( + self, private_key, algorithm, rsa_padding + ) class CertificateRevocationListBuilder: diff --git a/src/rust/cryptography-x509/src/common.rs b/src/rust/cryptography-x509/src/common.rs index d09971659..60856b7ef 100644 --- a/src/rust/cryptography-x509/src/common.rs +++ b/src/rust/cryptography-x509/src/common.rs @@ -6,7 +6,7 @@ use crate::oid; use asn1::Asn1DefinedByWritable; use std::marker::PhantomData; -#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Hash, Clone, Eq)] +#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Hash, Clone, Eq, Debug)] pub struct AlgorithmIdentifier<'a> { pub oid: asn1::DefinedByMarker<asn1::ObjectIdentifier>, #[defined_by(oid)] @@ -19,7 +19,7 @@ impl AlgorithmIdentifier<'_> { } } -#[derive(asn1::Asn1DefinedByRead, asn1::Asn1DefinedByWrite, PartialEq, Eq, Hash, Clone)] +#[derive(asn1::Asn1DefinedByRead, asn1::Asn1DefinedByWrite, PartialEq, Eq, Hash, Clone, Debug)] pub enum AlgorithmParameters<'a> { #[defined_by(oid::SHA1_OID)] Sha1(asn1::Null), @@ -31,6 +31,14 @@ pub enum AlgorithmParameters<'a> { Sha384(asn1::Null), #[defined_by(oid::SHA512_OID)] Sha512(asn1::Null), + #[defined_by(oid::SHA3_224_OID)] + Sha3_224(asn1::Null), + #[defined_by(oid::SHA3_256_OID)] + Sha3_256(asn1::Null), + #[defined_by(oid::SHA3_384_OID)] + Sha3_384(asn1::Null), + #[defined_by(oid::SHA3_512_OID)] + Sha3_512(asn1::Null), #[defined_by(oid::ED25519_OID)] Ed25519, @@ -225,7 +233,7 @@ pub const PSS_SHA1_HASH_ALG: AlgorithmIdentifier<'_> = AlgorithmIdentifier { // This is defined as an AlgorithmIdentifier in RFC 4055, // but the mask generation algorithm **must** contain an AlgorithmIdentifier // in its params, so we define it this way. -#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, Clone, PartialEq, Eq)] +#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, Clone, PartialEq, Eq, Debug)] pub struct MaskGenAlgorithm<'a> { pub oid: asn1::ObjectIdentifier, pub params: AlgorithmIdentifier<'a>, @@ -245,7 +253,7 @@ pub const PSS_SHA1_MASK_GEN_ALG: MaskGenAlgorithm<'_> = MaskGenAlgorithm { // mgf1SHA1Identifier, // saltLength [2] INTEGER DEFAULT 20, // trailerField [3] INTEGER DEFAULT 1 } -#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, Clone, PartialEq, Eq)] +#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, Clone, PartialEq, Eq, Debug)] pub struct RsaPssParameters<'a> { #[explicit(0)] #[default(PSS_SHA1_HASH_ALG)] @@ -258,7 +266,7 @@ pub struct RsaPssParameters<'a> { pub salt_length: u16, #[explicit(3)] #[default(1u8)] - _trailer_field: u8, + pub _trailer_field: u8, } /// A VisibleString ASN.1 element whose contents is not validated as meeting the diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index 6bc90173f..17a83fd16 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -130,7 +130,13 @@ fn sign_and_serialize<'p>( { ( None, - x509::sign::sign_data(py, py_private_key, py_hash_alg, &data_with_header)?, + x509::sign::sign_data( + py, + py_private_key, + py_hash_alg, + py.None().into_ref(py), + &data_with_header, + )?, ) } else { let mut authenticated_attrs = vec![]; @@ -175,7 +181,13 @@ fn sign_and_serialize<'p>( Some(common::Asn1ReadableOrWritable::new_write( asn1::SetOfWriter::new(authenticated_attrs), )), - x509::sign::sign_data(py, py_private_key, py_hash_alg, &signed_data)?, + x509::sign::sign_data( + py, + py_private_key, + py_hash_alg, + py.None().into_ref(py), + &signed_data, + )?, ) }; @@ -201,6 +213,7 @@ fn sign_and_serialize<'p>( py, py_private_key, py_hash_alg, + py.None().into_ref(py), )?, encrypted_digest: signature, unauthenticated_attributes: None, diff --git a/src/rust/src/x509/certificate.rs b/src/rust/src/x509/certificate.rs index dbe761fb9..f77f141fa 100644 --- a/src/rust/src/x509/certificate.rs +++ b/src/rust/src/x509/certificate.rs @@ -1002,8 +1002,10 @@ fn create_x509_certificate( builder: &pyo3::PyAny, private_key: &pyo3::PyAny, hash_algorithm: &pyo3::PyAny, + rsa_padding: &pyo3::PyAny, ) -> CryptographyResult<Certificate> { - let sigalg = x509::sign::compute_signature_algorithm(py, private_key, hash_algorithm)?; + let sigalg = + x509::sign::compute_signature_algorithm(py, private_key, hash_algorithm, rsa_padding)?; let serialization_mod = py.import(pyo3::intern!( py, "cryptography.hazmat.primitives.serialization" @@ -1056,7 +1058,8 @@ fn create_x509_certificate( }; let tbs_bytes = asn1::write_single(&tbs_cert)?; - let signature = x509::sign::sign_data(py, private_key, hash_algorithm, &tbs_bytes)?; + let signature = + x509::sign::sign_data(py, private_key, hash_algorithm, rsa_padding, &tbs_bytes)?; let data = asn1::write_single(&cryptography_x509::certificate::Certificate { tbs_cert, signature_alg: sigalg, diff --git a/src/rust/src/x509/crl.rs b/src/rust/src/x509/crl.rs index 6bb08779a..1331d3377 100644 --- a/src/rust/src/x509/crl.rs +++ b/src/rust/src/x509/crl.rs @@ -580,8 +580,12 @@ fn create_x509_crl( private_key: &pyo3::PyAny, hash_algorithm: &pyo3::PyAny, ) -> CryptographyResult<CertificateRevocationList> { - let sigalg = x509::sign::compute_signature_algorithm(py, private_key, hash_algorithm)?; - + let sigalg = x509::sign::compute_signature_algorithm( + py, + private_key, + hash_algorithm, + py.None().into_ref(py), + )?; let mut revoked_certs = vec![]; for py_revoked_cert in builder .getattr(pyo3::intern!(py, "_revoked_certificates"))? @@ -628,7 +632,13 @@ fn create_x509_crl( }; let tbs_bytes = asn1::write_single(&tbs_cert_list)?; - let signature = x509::sign::sign_data(py, private_key, hash_algorithm, &tbs_bytes)?; + let signature = x509::sign::sign_data( + py, + private_key, + hash_algorithm, + py.None().into_ref(py), + &tbs_bytes, + )?; let data = asn1::write_single(&crl::CertificateRevocationList { tbs_cert_list, signature_algorithm: sigalg, diff --git a/src/rust/src/x509/csr.rs b/src/rust/src/x509/csr.rs index 7ceed3511..110acf3a1 100644 --- a/src/rust/src/x509/csr.rs +++ b/src/rust/src/x509/csr.rs @@ -294,7 +294,12 @@ fn create_x509_csr( private_key: &pyo3::PyAny, hash_algorithm: &pyo3::PyAny, ) -> CryptographyResult<CertificateSigningRequest> { - let sigalg = x509::sign::compute_signature_algorithm(py, private_key, hash_algorithm)?; + let sigalg = x509::sign::compute_signature_algorithm( + py, + private_key, + hash_algorithm, + py.None().into_ref(py), + )?; let serialization_mod = py.import(pyo3::intern!( py, "cryptography.hazmat.primitives.serialization" @@ -364,7 +369,13 @@ fn create_x509_csr( }; let tbs_bytes = asn1::write_single(&csr_info)?; - let signature = x509::sign::sign_data(py, private_key, hash_algorithm, &tbs_bytes)?; + let signature = x509::sign::sign_data( + py, + private_key, + hash_algorithm, + py.None().into_ref(py), + &tbs_bytes, + )?; let data = asn1::write_single(&Csr { csr_info, signature_alg: sigalg, diff --git a/src/rust/src/x509/ocsp_resp.rs b/src/rust/src/x509/ocsp_resp.rs index 728eb92ce..f2a86241e 100644 --- a/src/rust/src/x509/ocsp_resp.rs +++ b/src/rust/src/x509/ocsp_resp.rs @@ -679,9 +679,20 @@ fn create_ocsp_response( )?, }; - let sigalg = x509::sign::compute_signature_algorithm(py, private_key, hash_algorithm)?; + let sigalg = x509::sign::compute_signature_algorithm( + py, + private_key, + hash_algorithm, + py.None().into_ref(py), + )?; let tbs_bytes = asn1::write_single(&tbs_response_data)?; - let signature = x509::sign::sign_data(py, private_key, hash_algorithm, &tbs_bytes)?; + let signature = x509::sign::sign_data( + py, + private_key, + hash_algorithm, + py.None().into_ref(py), + &tbs_bytes, + )?; if !responder_cert .call_method0(pyo3::intern!(py, "public_key"))? diff --git a/src/rust/src/x509/sign.rs b/src/rust/src/x509/sign.rs index 5c69ecedf..c0b0ec5de 100644 --- a/src/rust/src/x509/sign.rs +++ b/src/rust/src/x509/sign.rs @@ -4,7 +4,7 @@ use crate::error::{CryptographyError, CryptographyResult}; use crate::exceptions; -use cryptography_x509::common; +use cryptography_x509::{common, oid}; #[derive(Debug, PartialEq)] pub(crate) enum KeyType { @@ -119,14 +119,88 @@ fn identify_hash_type( } } +fn compute_pss_salt_length<'p>( + py: pyo3::Python<'p>, + private_key: &'p pyo3::PyAny, + hash_algorithm: &'p pyo3::PyAny, + rsa_padding: &'p pyo3::PyAny, +) -> pyo3::PyResult<u16> { + let padding_mod = py.import(pyo3::intern!( + py, + "cryptography.hazmat.primitives.asymmetric.padding" + ))?; + let maxlen = padding_mod.getattr(pyo3::intern!(py, "_MaxLength"))?; + let digestlen = padding_mod.getattr(pyo3::intern!(py, "_DigestLength"))?; + let py_saltlen = rsa_padding.getattr(pyo3::intern!(py, "_salt_length"))?; + if py_saltlen.is_instance(maxlen)? { + padding_mod + .getattr(pyo3::intern!(py, "calculate_max_pss_salt_length"))? + .call1((private_key, hash_algorithm))? + .extract::<u16>() + } else if py_saltlen.is_instance(digestlen)? { + hash_algorithm + .getattr(pyo3::intern!(py, "digest_size"))? + .extract::<u16>() + } else if py_saltlen.is_instance(py.get_type::<pyo3::types::PyLong>())? { + py_saltlen.extract::<u16>() + } else { + Err(pyo3::exceptions::PyTypeError::new_err( + "salt_length must be an int, MaxLength, or DigestLength.", + )) + } +} + pub(crate) fn compute_signature_algorithm<'p>( py: pyo3::Python<'p>, private_key: &'p pyo3::PyAny, hash_algorithm: &'p pyo3::PyAny, + rsa_padding: &'p pyo3::PyAny, ) -> pyo3::PyResult<common::AlgorithmIdentifier<'static>> { let key_type = identify_key_type(py, private_key)?; let hash_type = identify_hash_type(py, hash_algorithm)?; + let pss_type: &pyo3::types::PyType = py + .import(pyo3::intern!( + py, + "cryptography.hazmat.primitives.asymmetric.padding" + ))? + .getattr(pyo3::intern!(py, "PSS"))? + .extract()?; + // If this is RSA-PSS we need to compute the signature algorithm from the + // parameters provided in rsa_padding. + if !rsa_padding.is_none() && rsa_padding.is_instance(pss_type)? { + let hash_alg_params = identify_alg_params_for_hash_type(hash_type)?; + let hash_algorithm_id = common::AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params: hash_alg_params, + }; + let salt_length = compute_pss_salt_length(py, private_key, hash_algorithm, rsa_padding)?; + let py_mgf_alg = rsa_padding + .getattr(pyo3::intern!(py, "_mgf"))? + .getattr(pyo3::intern!(py, "_algorithm"))?; + let mgf_hash_type = identify_hash_type(py, py_mgf_alg)?; + let mgf_alg = common::AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params: identify_alg_params_for_hash_type(mgf_hash_type)?, + }; + let params = + common::AlgorithmParameters::RsaPss(Some(Box::new(common::RsaPssParameters { + hash_algorithm: hash_algorithm_id, + mask_gen_algorithm: common::MaskGenAlgorithm { + oid: oid::MGF1_OID, + params: mgf_alg, + }, + salt_length, + _trailer_field: 1, + }))); + + return Ok(common::AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params, + }); + } + // It's not an RSA PSS signature, so we compute the signature algorithm from + // the union of key type and hash type. match (key_type, hash_type) { (KeyType::Ed25519, HashType::None) => Ok(common::AlgorithmIdentifier { oid: asn1::DefinedByMarker::marker(), @@ -238,6 +312,7 @@ pub(crate) fn sign_data<'p>( py: pyo3::Python<'p>, private_key: &'p pyo3::PyAny, hash_algorithm: &'p pyo3::PyAny, + rsa_padding: &'p pyo3::PyAny, data: &[u8], ) -> pyo3::PyResult<&'p [u8]> { let key_type = identify_key_type(py, private_key)?; @@ -257,14 +332,17 @@ pub(crate) fn sign_data<'p>( private_key.call_method1(pyo3::intern!(py, "sign"), (data, ecdsa))? } KeyType::Rsa => { - let padding_mod = py.import(pyo3::intern!( - py, - "cryptography.hazmat.primitives.asymmetric.padding" - ))?; - let pkcs1v15 = padding_mod - .getattr(pyo3::intern!(py, "PKCS1v15"))? - .call0()?; - private_key.call_method1(pyo3::intern!(py, "sign"), (data, pkcs1v15, hash_algorithm))? + let mut padding = rsa_padding; + if padding.is_none() { + let padding_mod = py.import(pyo3::intern!( + py, + "cryptography.hazmat.primitives.asymmetric.padding" + ))?; + padding = padding_mod + .getattr(pyo3::intern!(py, "PKCS1v15"))? + .call0()?; + } + private_key.call_method1(pyo3::intern!(py, "sign"), (data, padding, hash_algorithm))? } KeyType::Dsa => { private_key.call_method1(pyo3::intern!(py, "sign"), (data, hash_algorithm))? @@ -435,10 +513,29 @@ fn identify_key_hash_type_for_algorithm_params( } } +fn identify_alg_params_for_hash_type( + hash_type: HashType, +) -> pyo3::PyResult<common::AlgorithmParameters<'static>> { + match hash_type { + HashType::Sha224 => Ok(common::AlgorithmParameters::Sha224(())), + HashType::Sha256 => Ok(common::AlgorithmParameters::Sha256(())), + HashType::Sha384 => Ok(common::AlgorithmParameters::Sha384(())), + HashType::Sha512 => Ok(common::AlgorithmParameters::Sha512(())), + HashType::Sha3_224 => Ok(common::AlgorithmParameters::Sha3_224(())), + HashType::Sha3_256 => Ok(common::AlgorithmParameters::Sha3_256(())), + HashType::Sha3_384 => Ok(common::AlgorithmParameters::Sha3_384(())), + HashType::Sha3_512 => Ok(common::AlgorithmParameters::Sha3_512(())), + HashType::None => Err(pyo3::exceptions::PyTypeError::new_err( + "Algorithm must be a registered hash algorithm, not None.", + )), + } +} + #[cfg(test)] mod tests { use super::{ - identify_key_hash_type_for_algorithm_params, py_hash_name_from_hash_type, HashType, KeyType, + identify_alg_params_for_hash_type, identify_key_hash_type_for_algorithm_params, + py_hash_name_from_hash_type, HashType, KeyType, }; use cryptography_x509::{common, oid}; @@ -604,6 +701,34 @@ mod tests { } #[test] + fn test_identify_alg_params_for_hash_type() { + for (hash, params) in [ + (HashType::Sha224, common::AlgorithmParameters::Sha224(())), + (HashType::Sha256, common::AlgorithmParameters::Sha256(())), + (HashType::Sha384, common::AlgorithmParameters::Sha384(())), + (HashType::Sha512, common::AlgorithmParameters::Sha512(())), + ( + HashType::Sha3_224, + common::AlgorithmParameters::Sha3_224(()), + ), + ( + HashType::Sha3_256, + common::AlgorithmParameters::Sha3_256(()), + ), + ( + HashType::Sha3_384, + common::AlgorithmParameters::Sha3_384(()), + ), + ( + HashType::Sha3_512, + common::AlgorithmParameters::Sha3_512(()), + ), + ] { + assert_eq!(identify_alg_params_for_hash_type(hash).unwrap(), params); + } + } + + #[test] fn test_py_hash_name_from_hash_type() { for (hash, name) in [ (HashType::Sha224, "SHA224"), diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py index b33e09ce5..19a854e24 100644 --- a/tests/x509/test_x509.py +++ b/tests/x509/test_x509.py @@ -765,6 +765,21 @@ class TestRSAPSSCertificate: cert.signature_hash_algorithm, ) + def test_load_pss_sha1_mgf1_sha1(self, backend): + cert = _load_cert( + os.path.join("x509", "ee-pss-sha1-cert.pem"), + x509.load_pem_x509_certificate, + ) + assert isinstance(cert, x509.Certificate) + pub_key = cert.public_key() + assert isinstance(pub_key, rsa.RSAPublicKey) + pss = cert.signature_algorithm_parameters + assert isinstance(pss, padding.PSS) + assert isinstance(pss._mgf, padding.MGF1) + assert isinstance(pss._mgf._algorithm, hashes.SHA1) + assert pss._salt_length == 20 + assert isinstance(cert.signature_hash_algorithm, hashes.SHA1) + def test_invalid_mgf(self, backend): cert = _load_cert( os.path.join("x509", "custom", "rsa_pss_cert_invalid_mgf.der"), @@ -2404,6 +2419,154 @@ class TestCertificateBuilder: # GENERALIZED TIME assert parsed.not_after_tag == 0x18 + @pytest.mark.parametrize( + ("alg", "mgf_alg"), + [ + (hashes.SHA512(), hashes.SHA256()), + (hashes.SHA3_512(), hashes.SHA3_256()), + ], + ) + def test_sign_pss( + self, rsa_key_2048: rsa.RSAPrivateKey, alg, mgf_alg, backend + ): + if not backend.signature_hash_supported(alg): + pytest.skip(f"{alg} signature not supported") + builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .public_key(rsa_key_2048.public_key()) + .serial_number(777) + .not_valid_before(datetime.datetime(2020, 1, 1)) + .not_valid_after(datetime.datetime(2038, 1, 1)) + ) + pss = padding.PSS( + mgf=padding.MGF1(mgf_alg), salt_length=alg.digest_size + ) + cert = builder.sign(rsa_key_2048, alg, rsa_padding=pss) + pk = cert.public_key() + assert isinstance(pk, rsa.RSAPublicKey) + assert isinstance(cert.signature_hash_algorithm, type(alg)) + cert_params = cert.signature_algorithm_parameters + assert isinstance(cert_params, padding.PSS) + assert cert_params._salt_length == pss._salt_length + assert isinstance(cert_params._mgf, padding.MGF1) + assert isinstance(cert_params._mgf._algorithm, type(mgf_alg)) + pk.verify( + cert.signature, + cert.tbs_certificate_bytes, + cert_params, + alg, + ) + + @pytest.mark.parametrize( + ("padding_len", "computed_len"), + [ + (padding.PSS.MAX_LENGTH, 222), + (padding.PSS.DIGEST_LENGTH, 32), + ], + ) + def test_sign_pss_length_options( + self, + rsa_key_2048: rsa.RSAPrivateKey, + padding_len, + computed_len, + backend, + ): + builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .public_key(rsa_key_2048.public_key()) + .serial_number(777) + .not_valid_before(datetime.datetime(2020, 1, 1)) + .not_valid_after(datetime.datetime(2038, 1, 1)) + ) + pss = padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), salt_length=padding_len + ) + cert = builder.sign(rsa_key_2048, hashes.SHA256(), rsa_padding=pss) + assert isinstance(cert.signature_algorithm_parameters, padding.PSS) + assert cert.signature_algorithm_parameters._salt_length == computed_len + + def test_sign_pss_auto_unsupported( + self, rsa_key_2048: rsa.RSAPrivateKey, backend + ): + builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .public_key(rsa_key_2048.public_key()) + .serial_number(777) + .not_valid_before(datetime.datetime(2020, 1, 1)) + .not_valid_after(datetime.datetime(2038, 1, 1)) + ) + pss = padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.AUTO + ) + with pytest.raises(TypeError): + builder.sign(rsa_key_2048, hashes.SHA256(), rsa_padding=pss) + + def test_sign_invalid_padding( + self, rsa_key_2048: rsa.RSAPrivateKey, backend + ): + builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .public_key(rsa_key_2048.public_key()) + .serial_number(777) + .not_valid_before(datetime.datetime(2020, 1, 1)) + .not_valid_after(datetime.datetime(2038, 1, 1)) + ) + with pytest.raises(TypeError): + builder.sign( + rsa_key_2048, + hashes.SHA256(), + rsa_padding=b"notapadding", # type: ignore[arg-type] + ) + eckey = ec.generate_private_key(ec.SECP256R1()) + with pytest.raises(TypeError): + builder.sign( + eckey, hashes.SHA256(), rsa_padding=padding.PKCS1v15() + ) + + def test_sign_pss_hash_none( + self, rsa_key_2048: rsa.RSAPrivateKey, backend + ): + builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .public_key(rsa_key_2048.public_key()) + .serial_number(777) + .not_valid_before(datetime.datetime(2020, 1, 1)) + .not_valid_after(datetime.datetime(2038, 1, 1)) + ) + pss = padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=32) + with pytest.raises(TypeError): + builder.sign(rsa_key_2048, None, rsa_padding=pss) + def test_no_subject_name(self, rsa_key_2048: rsa.RSAPrivateKey, backend): subject_private_key = rsa_key_2048 builder = ( |