Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for ECDSA deterministic signing (RFC 6979) #10369

Merged
merged 10 commits into from
Feb 26, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ 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
:class:`~cryptography.hazmat.primitives.asymmetric.ec.ECDSA` (:rfc:`6979`)

.. _v42-0-5:

Expand Down
13 changes: 13 additions & 0 deletions docs/hazmat/primitives/asymmetric/ec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ 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`. This only impacts the signing
process, verification is not affected (the verification process
is the same for both deterministic and non-deterministic signed
messages).

.. 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
Expand Down
6 changes: 6 additions & 0 deletions src/cryptography/hazmat/backends/openssl/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions src/cryptography/hazmat/primitives/asymmetric/ec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -319,15 +320,34 @@ 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(
self,
) -> 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

Expand Down
2 changes: 1 addition & 1 deletion src/rust/cryptography-openssl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ rust-version = "1.65.0"
[dependencies]
cfg-if = "1"
openssl = "0.10.64"
ffi = { package = "openssl-sys", version = "0.9.99" }
ffi = { package = "openssl-sys", version = "0.9.101" }
foreign-types = "0.3"
foreign-types-shared = "0.1"
3 changes: 3 additions & 0 deletions src/rust/cryptography-openssl/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
25 changes: 23 additions & 2 deletions src/rust/src/backend/ec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,15 +273,36 @@ 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"))?,
)?;

let mut signer = openssl::pkey_ctx::PkeyCtx::new(&self.pkey)?;
signer.sign_init()?;
cfg_if::cfg_if! {
if #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]{
let deterministic: bool = signature_algorithm
.getattr(pyo3::intern!(py, "deterministic_signing"))?
.extract()?;
if deterministic {
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)?;
} else {
signer.set_nonce_type(openssl::pkey_ctx::NonceType::RANDOM_K)?;
}
} else {
let _ = algo;
}
}

// 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
Expand Down
67 changes: 67 additions & 0 deletions tests/hazmat/primitives/test_ec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
)
Expand Down Expand Up @@ -508,6 +513,68 @@ def test_signature_failures(self, backend, subtests):
signature, vector["message"], ec.ECDSA(hash_type())
)

def test_unsupported_deterministic_nonce(self, backend):
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 = {
"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:
reaperhulk marked this conversation as resolved.
Show resolved Hide resolved
with subtests.test():
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"]
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"
Expand Down
51 changes: 51 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

return vectors


def load_x963_vectors(vector_data):
"""
Loads data out of the X9.63 vector data
Expand Down