Skip to content

Commit

Permalink
Add support for deterministic ECDSA (RFC 6979)
Browse files Browse the repository at this point in the history
  • Loading branch information
facutuesca committed Feb 8, 2024
1 parent 315c31c commit c4f3bd9
Show file tree
Hide file tree
Showing 10 changed files with 2,991 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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-2:

Expand Down
2 changes: 2 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Asymmetric ciphers
* X25519 and X448 test vectors from :rfc:`7748`.
* RSA OAEP with custom label from the `BoringSSL evp tests`_.
* Ed448 test vectors from :rfc:`8032`.
* Deterministic ECDSA (:rfc:`6979`) from `OpenSSL's RFC 6979 test vectors`_.


Custom asymmetric vectors
Expand Down Expand Up @@ -1092,3 +1093,4 @@ header format (substituting the correct information):
.. _`dkg's additional OCB3 vectors`: https://gitlab.com/dkg/ocb-test-vectors
.. _`OpenSSL's OCB vectors`: https://github.com/openssl/openssl/commit/2f19ab18a29cf9c82cdd68bc8c7e5be5061b19be
.. _`badkeys`: https://github.com/vcsjones/badkeys/tree/50f1cc5f8d13bf3a2046d689f6452decb15d9c3c
.. _`OpenSSL's RFC 6979 test vectors`: https://github.com/openssl/openssl/blob/01690a7ff36c4d18c48b301cdf375c954105a1d9/test/recipes/30-test_evp_data/evppkey_ecdsa_rfc6979.txt
10 changes: 10 additions & 0 deletions docs/hazmat/primitives/asymmetric/ec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -548,6 +548,12 @@ def ed448_supported(self) -> bool:
and not self._lib.CRYPTOGRAPHY_IS_BORINGSSL
)

def ecdsa_deterministic_supported(self) -> bool:
return (
self._lib.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
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
23 changes: 22 additions & 1 deletion src/rust/src/backend/ec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,35 @@ 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()?;
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
Expand Down
69 changes: 69 additions & 0 deletions tests/hazmat/primitives/test_ec.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
from cryptography import exceptions, utils, x509
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 @@ -26,6 +30,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 @@ -507,6 +512,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"
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 = dict()

return vectors


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

0 comments on commit c4f3bd9

Please sign in to comment.