Skip to content

Commit

Permalink
Support for ECDSA deterministic signing (RFC 6979) (pyca#10369)
Browse files Browse the repository at this point in the history
* Add support for deterministic ECDSA (RFC 6979)
  • Loading branch information
facutuesca authored Feb 26, 2024
1 parent 4bfd321 commit 0a1098f
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 3 deletions.
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:
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

0 comments on commit 0a1098f

Please sign in to comment.