diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2a529c2d7b804..3a686c83a6244 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,7 @@ Changelog and :class:`~cryptography.hazmat.primitives.ciphers.algorithms.ARC4` into :doc:`/hazmat/decrepit/index` and deprecated them in the ``cipher`` module. They will be removed from the ``cipher`` module in 48.0.0. +* Added support for deterministic ECDSA (:rfc:`6979`) .. _v42-0-3: diff --git a/docs/hazmat/primitives/asymmetric/ec.rst b/docs/hazmat/primitives/asymmetric/ec.rst index 75165b6a45364..b5b054e59d895 100644 --- a/docs/hazmat/primitives/asymmetric/ec.rst +++ b/docs/hazmat/primitives/asymmetric/ec.rst @@ -47,6 +47,16 @@ Elliptic Curve Signature Algorithms :param algorithm: An instance of :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`. + :param bool deterministic_signing: A boolean flag defaulting to ``False`` + that specifies whether the signing procedure should be deterministic + or not, as defined in :rfc:`6979`. + + .. versionadded:: 43.0.0 + + :raises cryptography.exceptions.UnsupportedAlgorithm: If + ``deterministic_signing`` is set to ``True`` and the version of + OpenSSL does not support ECDSA with deterministic signing. + .. doctest:: >>> from cryptography.hazmat.primitives import hashes diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 406b1ea990a26..eaaaf783f1c5c 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -358,6 +358,12 @@ def ed448_supported(self) -> bool: and not rust_openssl.CRYPTOGRAPHY_IS_BORINGSSL ) + def ecdsa_deterministic_supported(self) -> bool: + return ( + rust_openssl.CRYPTOGRAPHY_OPENSSL_320_OR_GREATER + and not self._fips_enabled + ) + def _zero_data(self, data, length: int) -> None: # We clear things this way because at the moment we're not # sure of a better way that can guarantee it overwrites the diff --git a/src/cryptography/hazmat/primitives/asymmetric/ec.py b/src/cryptography/hazmat/primitives/asymmetric/ec.py index b612b40149d4d..da1fbea13a6e1 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/ec.py +++ b/src/cryptography/hazmat/primitives/asymmetric/ec.py @@ -8,6 +8,7 @@ import typing from cryptography import utils +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons from cryptography.hazmat._oid import ObjectIdentifier from cryptography.hazmat.bindings._rust import openssl as rust_openssl from cryptography.hazmat.primitives import _serialization, hashes @@ -319,8 +320,21 @@ class ECDSA(EllipticCurveSignatureAlgorithm): def __init__( self, algorithm: asym_utils.Prehashed | hashes.HashAlgorithm, + deterministic_signing: bool = False, ): + from cryptography.hazmat.backends.openssl.backend import backend + + if ( + deterministic_signing + and not backend.ecdsa_deterministic_supported() + ): + raise UnsupportedAlgorithm( + "ECDSA with deterministic signature (RFC 6979) is not " + "supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) self._algorithm = algorithm + self._deterministic_signing = deterministic_signing @property def algorithm( @@ -328,6 +342,12 @@ def algorithm( ) -> asym_utils.Prehashed | hashes.HashAlgorithm: return self._algorithm + @property + def deterministic_signing( + self, + ) -> bool: + return self._deterministic_signing + generate_private_key = rust_openssl.ec.generate_private_key diff --git a/src/rust/cryptography-openssl/build.rs b/src/rust/cryptography-openssl/build.rs index 5e626f7de6141..87e1fa528b22f 100644 --- a/src/rust/cryptography-openssl/build.rs +++ b/src/rust/cryptography-openssl/build.rs @@ -12,6 +12,9 @@ fn main() { if version >= 0x3_00_00_00_0 { println!("cargo:rustc-cfg=CRYPTOGRAPHY_OPENSSL_300_OR_GREATER"); } + if version >= 0x3_02_00_00_0 { + println!("cargo:rustc-cfg=CRYPTOGRAPHY_OPENSSL_320_OR_GREATER"); + } } if env::var("DEP_OPENSSL_LIBRESSL_VERSION_NUMBER").is_ok() { diff --git a/src/rust/src/backend/ec.rs b/src/rust/src/backend/ec.rs index 624b753c07cba..5e1e657082719 100644 --- a/src/rust/src/backend/ec.rs +++ b/src/rust/src/backend/ec.rs @@ -274,7 +274,7 @@ impl ECPrivateKey { )); } - let (data, _) = utils::calculate_digest_and_algorithm( + let (data, _algo) = utils::calculate_digest_and_algorithm( py, data.as_bytes(), signature_algorithm.getattr(pyo3::intern!(py, "algorithm"))?, @@ -282,6 +282,27 @@ impl ECPrivateKey { let mut signer = openssl::pkey_ctx::PkeyCtx::new(&self.pkey)?; signer.sign_init()?; + let deterministic: bool = signature_algorithm + .getattr(pyo3::intern!(py, "deterministic_signing"))? + .extract()?; + cfg_if::cfg_if! { + if #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]{ + match deterministic { + true => { + let hash_function_name = _algo.getattr(pyo3::intern!(py, "name"))?.extract::<&str>()?; + let hash_function = openssl::md::Md::fetch(None, hash_function_name, None)?; + // Setting a deterministic nonce type requires to explicitly set the hash function. + // See https://github.com/openssl/openssl/issues/23205 + signer.set_signature_md(&hash_function)?; + signer.set_nonce_type(openssl::pkey_ctx::NonceType::DETERMINISTIC_K)?; + }, + false => signer.set_nonce_type(openssl::pkey_ctx::NonceType::RANDOM_K)?, + }; + } else { + assert!(!deterministic); + } + } + // TODO: This does an extra allocation and copy. This can't easily use // `PyBytes::new_with` because the exact length of the signature isn't // easily known a priori (if `r` or `s` has a leading 0, the signature diff --git a/tests/hazmat/primitives/test_ec.py b/tests/hazmat/primitives/test_ec.py index a558af3b9b709..9981353ce8427 100644 --- a/tests/hazmat/primitives/test_ec.py +++ b/tests/hazmat/primitives/test_ec.py @@ -16,6 +16,10 @@ from cryptography.hazmat.bindings._rust import openssl as rust_openssl from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.ec import ( + EllipticCurvePrivateKey, + EllipticCurvePublicKey, +) from cryptography.hazmat.primitives.asymmetric.utils import ( Prehashed, encode_dss_signature, @@ -27,6 +31,7 @@ load_fips_ecdsa_signing_vectors, load_kasvs_ecdh_vectors, load_nist_vectors, + load_rfc6979_vectors, load_vectors_from_file, raises_unsupported_algorithm, ) @@ -508,6 +513,70 @@ def test_signature_failures(self, backend, subtests): signature, vector["message"], ec.ECDSA(hash_type()) ) + def test_unsupported_deterministic_nonce(self, backend, subtests): + if backend.ecdsa_deterministic_supported(): + pytest.skip( + f"ECDSA deterministic signing is supported by this" + f" backend {backend}" + ) + with pytest.raises(exceptions.UnsupportedAlgorithm): + ec.ECDSA(hashes.SHA256(), deterministic_signing=True) + + def test_deterministic_nonce(self, backend, subtests): + if not backend.ecdsa_deterministic_supported(): + pytest.skip( + f"ECDSA deterministic signing is not supported by this" + f" backend {backend}" + ) + + supported_hash_algorithms: typing.Dict[ + str, typing.Type[hashes.HashAlgorithm] + ] = { + "SHA1": hashes.SHA1, + "SHA224": hashes.SHA224, + "SHA256": hashes.SHA256, + "SHA384": hashes.SHA384, + "SHA512": hashes.SHA512, + } + vectors = load_vectors_from_file( + os.path.join( + "asymmetric", "ECDSA", "RFC6979", "evppkey_ecdsa_rfc6979.txt" + ), + load_rfc6979_vectors, + ) + + for vector in vectors: + input = bytes(vector["input"], "utf-8") + output = bytes.fromhex(vector["output"]) + key = bytes("\n".join(vector["key"]), "utf-8") + if "digest_sign" in vector: + algorithm = vector["digest_sign"] + assert algorithm in supported_hash_algorithms + hash_algorithm = supported_hash_algorithms[algorithm] + algorithm = ec.ECDSA( + hash_algorithm(), + deterministic_signing=vector["deterministic_nonce"], + ) + private_key = serialization.load_pem_private_key( + key, password=None + ) + assert isinstance(private_key, EllipticCurvePrivateKey) + signature = private_key.sign(input, algorithm) + assert signature == output + else: + assert "digest_verify" in vector + algorithm = vector["digest_verify"] + assert algorithm in supported_hash_algorithms + hash_algorithm = supported_hash_algorithms[algorithm] + algorithm = ec.ECDSA(hash_algorithm()) + public_key = serialization.load_pem_public_key(key) + assert isinstance(public_key, EllipticCurvePublicKey) + if vector["verify_error"]: + with pytest.raises(exceptions.InvalidSignature): + public_key.verify(output, input, algorithm) + else: + public_key.verify(output, input, algorithm) + def test_sign(self, backend): _skip_curve_unsupported(backend, ec.SECP256R1()) message = b"one little message" diff --git a/tests/utils.py b/tests/utils.py index 595e8dc04e1cc..f8bbcbdb8b000 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -701,6 +701,57 @@ def load_kasvs_ecdh_vectors(vector_data): return vectors +def load_rfc6979_vectors(vector_data): + """ + Loads data out of the ECDSA and DSA RFC6979 vector files. + """ + vectors = [] + keys: typing.Dict[str, typing.List[str]] = dict() + reading_key = False + current_key_name = None + + data: typing.Dict[str, object] = dict() + for line in vector_data: + line = line.strip() + + if reading_key and current_key_name: + keys[current_key_name].append(line) + if line.startswith("-----END"): + reading_key = False + current_key_name = None + + if line.startswith("PrivateKey=") or line.startswith("PublicKey="): + reading_key = True + current_key_name = line.split("=")[1].strip() + keys[current_key_name] = [] + elif line.startswith("DigestSign = "): + data["digest_sign"] = line.split("=")[1].strip() + data["deterministic_nonce"] = False + elif line.startswith("DigestVerify = "): + data["digest_verify"] = line.split("=")[1].strip() + data["verify_error"] = False + elif line.startswith("Key = "): + key_name = line.split("=")[1].strip() + assert key_name in keys + data["key"] = keys[key_name] + elif line.startswith("NonceType = "): + nonce_type = line.split("=")[1].strip() + data["deterministic_nonce"] = nonce_type == "deterministic" + elif line.startswith("Input = "): + data["input"] = line.split("=")[1].strip(' "') + elif line.startswith("Output = "): + data["output"] = line.split("=")[1].strip() + elif line.startswith("Result = "): + data["verify_error"] = line.split("=")[1].strip() == "VERIFY_ERROR" + + elif not line: + if data: + vectors.append(data) + data = dict() + + return vectors + + def load_x963_vectors(vector_data): """ Loads data out of the X9.63 vector data