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

Conversation

facutuesca
Copy link
Contributor

@facutuesca facutuesca commented Feb 8, 2024

Part of #9795. Now that the Rust bindings needed are in place (sfackler/rust-openssl#2144), this PR adds support for ECDSA deterministic signing (RFC 6979), a new feature in OpenSSL >= 3.2.0.

The way to enable it is by passing a new boolean parameter deterministic_signing to ec.ECDSA():

signature = private_key.sign(
    data,
    ec.ECDSA(hashes.SHA256(), deterministic_signing=True)
)
  • When using anything other than OpenSSL >= 3.2.0, this will raise an UnsupportedAlgorithm exception. It will also raise if FIPS is enabled.
  • The new deterministic_signing parameter defaults to False, so that old code that does not specify it maintains the same behavior.

@facutuesca facutuesca force-pushed the deterministic-nonce branch 7 times, most recently from 441b799 to c4f3bd9 Compare February 8, 2024 20:47
@facutuesca facutuesca marked this pull request as ready for review February 8, 2024 21:09
@alex alex added this to the Forty Third Release milestone Feb 10, 2024
@alex
Copy link
Member

alex commented Feb 19, 2024

rust-openssl has had a release.

Also can you please make a seperate PR adding the test vectors?

@facutuesca
Copy link
Contributor Author

Also can you please make a seperate PR adding the test vectors?

@alex : #10438

@facutuesca
Copy link
Contributor Author

facutuesca commented Feb 20, 2024

I rebased now that the test vectors are merged

@@ -274,14 +274,35 @@ impl ECPrivateKey {
));
}

let (data, _) = utils::calculate_digest_and_algorithm(
let (data, _algo) = utils::calculate_digest_and_algorithm(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is used, the name shouldn't have a leading prefix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's only used when OpenSSL >=3.2.0, so without it it complains when compiling with older OpenSSLs (or libre/boring). I could do something like this instead?:

cfg_if::cfg_if! {
    if #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]{
        let (data, _algo) = utils::calculate_digest_and_algorithm(
            py,
            data.as_bytes(),
            signature_algorithm.getattr(pyo3::intern!(py, "algorithm"))?,
        )?;
    } else {
        let (data, _) = utils::calculate_digest_and_algorithm(
            py,
            data.as_bytes(),
            signature_algorithm.getattr(pyo3::intern!(py, "algorithm"))?,
        )?;
    }    

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do let _ = algo; in the other branch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, that is a lot better! fixed

.extract()?;
cfg_if::cfg_if! {
if #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]{
match deterministic {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be an if, no need for a match

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines 292 to 293
let hash_function_name = _algo.getattr(pyo3::intern!(py, "name"))?.extract::<&str>()?;
let hash_function = openssl::md::Md::fetch(None, hash_function_name, None)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a function in the hashes module that you can use for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, but I'm having trouble constructing the appropriate type. The hashes function I tried to use returns a openssl::hash::MessageDigest:

pub(crate) fn message_digest_from_algorithm(
    py: pyo3::Python<'_>,
    algorithm: &pyo3::PyAny,
) -> CryptographyResult<openssl::hash::MessageDigest> {

The function I'm calling that uses the hash function takes an openssl::md::MdRef:

pub fn set_signature_md(&self, md: &MdRef) -> Result<(), ErrorStack> 

I can convert one to the other by adding unsafe code:

let hash_function = hashes::message_digest_from_algorithm(py, _algo)?;
unsafe {
    signer.set_signature_md(&openssl::md::Md::from_ptr(hash_function.as_ptr().cast_mut()))?;
}

But not sure if that's what we want (as opposed to directly using openssl::md::Md::fetch(), which already encapsulates the unsafe use)

tests/utils.py Outdated
elif not line:
if data:
vectors.append(data)
data = dict()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data = dict()
data = {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@@ -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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs should say that this has no impact on verification.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The new docstring now says:

    :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).

@Tarnum-tst
Copy link

Merge, please!

CHANGELOG.rst Outdated
@@ -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`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a link to the relevant code -- making ECDSA a link to the class should be enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@@ -274,14 +274,35 @@ impl ECPrivateKey {
));
}

let (data, _) = utils::calculate_digest_and_algorithm(
let (data, _algo) = utils::calculate_digest_and_algorithm(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do let _ = algo; in the other branch.

@@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_unsupported_deterministic_nonce(self, backend, subtests):
def test_unsupported_deterministic_nonce(self, backend):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

key = bytes("\n".join(vector["key"]), "utf-8")
if "digest_sign" in vector:
algorithm = vector["digest_sign"]
assert algorithm in supported_hash_algorithms
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert algorithm in supported_hash_algorithms

Not needed, in tests it's fine for the next line to raise KeyError

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

f" backend {backend}"
)

supported_hash_algorithms: typing.Dict[
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's simpler if you just make the values here instance of the hash algorithm, rather than instantiating them below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@facutuesca
Copy link
Contributor Author

Rebased now that #10486 is merged

@Tarnum-tst
Copy link

@facutuesca
openssl-sys is 0.9.101 in cryptography-openssl currently.

}
} else {
let _ = algo;
assert!(!deterministic);
Copy link
Member

@reaperhulk reaperhulk Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this assert won't trigger unless you do something dumb (instantiate ECDSA then set obj._deterministic = True yourself on non-OpenSSL 3.2.0) but I'd rather it wasn't possible to cause program termination even if you do something truly stupid.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I removed the assert and didn't add any code (therefore completely ignoring deterministic's value) as per @alex's suggestion.

@reaperhulk reaperhulk enabled auto-merge (squash) February 26, 2024 19:00
@reaperhulk reaperhulk merged commit 0a1098f into pyca:main Feb 26, 2024
56 checks passed
@facutuesca facutuesca deleted the deterministic-nonce branch February 26, 2024 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants