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

Add support for ES256 #51

Merged
merged 8 commits into from
Feb 11, 2021
Merged

Add support for ES256 #51

merged 8 commits into from
Feb 11, 2021

Conversation

jannispinter
Copy link
Contributor

This PR adds support for ES256 and is based on #8 by @yonran.

Compared to #8, I have added several tests and made sure that everything passes without any errors in tox for both Python 2.7 and Python 3.7. In order to make it more useful, an implementation for JWA was added as well (note that #8 only added EC support for JWK). It is now possible to create a valid JWS with ES256, ES384 and ES512.

Finally, I have fixed several minor bugs and glitches that existed in #8. One obstacle was that P-521 coordinates are encoded in different lengths (depending on the most significant byte being 0x00 and other factors) and are not encoded fixed length as it happens to be in P-256 and P-384. The previous code failed for P-521 most of the time.

This is my first attempt to add EC support to josepy and I am very happy to get comments on this and improve it until it is ready to be merged.

@bmw
Copy link
Member

bmw commented Jul 16, 2019

Thanks for the PR @jannispinter! I'll try and take an in-depth look at this soon.

In the meantime, if you have any questions about specific pieces of this PR, let me know and I can probably give you some feedback sooner.

@harrie91
Copy link

Hello there,
just wondering if there are any plans to accept this PR in the near future.
I'd like to see EC support in certbot.

@bmw
Copy link
Member

bmw commented Oct 17, 2019

We probably won't have time to review it for at least a few more weeks, but I too would like to see this PR merged sooner rather than later.

@hansjoachimknobloch
Copy link

hansjoachimknobloch commented Jan 9, 2020

Is there anything that can be done to advance the status this pull request?

@bmw
Copy link
Member

bmw commented Jan 9, 2020

Not really. If anyone sees/has any problems with the PR, it may be useful to suggest changes, but this is still blocked on someone from the Certbot team having time to review it.

We're planning on doing a thorough sweep of all of our community PRs in the next 6 weeks or so though so hopefully we can get this merged then.

@hansjoachimknobloch
Copy link

Great news! If there should be any issues during the review, don't hesitate to contact me and Jannis, the original submitter of the PR and former colleague of mine.

@bmw bmw added the priority: unplanned Work that we believe should be done, but does not have a higher priority. label Mar 25, 2020
Copy link
Contributor

@atombrella atombrella left a comment

Choose a reason for hiding this comment

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

I had a cursory look at this PR, and have a few comments. I think it appears to follow all of the specification in https://tools.ietf.org/html/rfc7518 @hansjoachimknobloch @jannispinter


def test_sign_no_private_part(self):
from josepy.jwa import ES256
self.assertRaises(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can be on one line.

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 by a5fb321

@@ -248,7 +225,7 @@ def fields_from_json(cls, jobj):

key = rsa.RSAPrivateNumbers(
p, q, d, dp, dq, qi, public_numbers).private_key(
default_backend())
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the reason for this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My IDE must have auto-formatted this. I will revert this change.

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 by 2c3cabe

'x': public.x,
'y': public.y,
})
params = dict((key, self._encode_param(value))
Copy link
Contributor

Choose a reason for hiding this comment

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

dict comprehensions are faster, and probably also more Pythonic. It works in Python 2.

{key: self._encode_param(value) for key, value in six.iteritems(params)}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Fixed by 6e8d041

MHcCAQEEIMUXjUASqqk7YRvKE5gXYaeBEGCJkitXVan2tssjJ9q3oAoGCCqGSM49
AwEHoUQDQgAEjjQtV+fA7J/tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5UQ8DDz/zPs
9gqwcfqGUZKWxbEWgWXv7S8zRBEZuae8Jw==
-----END EC PRIVATE KEY-----
Copy link
Contributor

Choose a reason for hiding this comment

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

Just out of curiosity, how did you generate these keys? I think the README file should be updated with the command.

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 generated them using OpenSSL
openssl ecparam -name prime256v1 -genkey -out ec_p256_key.pem
openssl ecparam -name secp384r1 -genkey -out ec_p384_key.pem
openssl ecparam -name secp521r1 -genkey -out ec_p521_key.pem

I will update the file testdata/README accordingly.

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 by 2f8bebe

elif isinstance(self.key._wrapped, ec.EllipticCurvePrivateKey):
private = self.key.private_numbers()
public = self.key.public_key().public_numbers()
params.update({
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just do params['d'] = private.private_value ? It's ~3 times faster.

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 by 6e8d041


def test_repr(self):
self.assertTrue(repr(self.p256_key).startswith(
'<ComparableECKey(<cryptography.hazmat.'))
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't it possible to add in the entire string/value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, unfortunately it will return the memory address as well which is different on every run:
<ComparableECKey(<cryptography.hazmat.backends.openssl.ec._EllipticCurvePrivateKey object at 0x6a0be0debd50>)>

@jannispinter
Copy link
Contributor Author

Thank you @atombrella for your review! Please let me know if there is anything else that needs to be fixed with this PR.

def sign(self, key, msg):
"""Sign the ``msg`` using ``key``."""
# If cryptography library supports new style api (v1.4 and later)
new_api = hasattr(key, 'sign')
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just inline this?

@atombrella
Copy link
Contributor

@jannispinter You're welcome. I'm not a member of the Certbot organization, but hopefully either @bmw @adferrand or @ohemorange can have a look at this. I had a look at the RFC, and I think this PR implements everything mentioned about EC.

Could you please also add an entry to CHANGELOG.md? I think 1.6.0 should be added as a header, with an undecided release date.

@atombrella
Copy link
Contributor

And maybe the draft status is no longer necessary?

@commonism
Copy link
Contributor

Works for me.
Using this branch to implement EC account keys server side.

One thing to add -

from josepy.jwa import (
HS256,
HS384,
HS512,
JWASignature,
PS256,
PS384,
PS512,
RS256,
RS384,
RS512,
)

The exports need to be updated.

diff --git a/src/josepy/__init__.py b/src/josepy/__init__.py
index 5f81e20f..4ceb7eda 100644
--- a/src/josepy/__init__.py
+++ b/src/josepy/__init__.py
@@ -65,6 +65,9 @@ from josepy.jwa import (
     RS256,
     RS384,
     RS512,
+    ES256,
+    ES384,
+    ES512,
 )

@commonism
Copy link
Contributor

While I was successful having client talking to a server using the library, I was not able to talk to other servers using ES256 keys or have clients with ES256 account key capability (cpu.acmeshell) talk to the server.

There is a problem when verifying the ECDSA signature:

# print('\n'.join([i.reason_text.decode() for i in _errors_with_text(errors)]))
error:0D07207B:asn1 encoding routines:ASN1_get_object:header too long
error:0D068066:asn1 encoding routines:asn1_check_tlen:bad object header
error:0D07803A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error

The josepy-ES powered server expects the signature in a different format.

The signature format is wrong, the library does not use the r || s format as defined in:
https://tools.ietf.org/html/rfc7515#appendix-A.3 .
Basically the same as mentioned here: funcool/buddy-sign#62 .

ECDSA_do_sign & ECDSA_do_verify exist to work with signatures without ASN1 - https://www.openssl.org/docs/man1.1.0/man3/ECDSA_verify.html .
Current cryptography lacks these exports: https://github.com/pyca/cryptography/blob/master/src/_cffi_src/openssl/ecdsa.py
Splitting the concat binary signature into r & s to use ECDSA_do_verify is shown here: https://github.com/Spomky-Labs/jose/blob/master/src/Algorithm/Signature/ECDSA.php#L101

@commonism
Copy link
Contributor

commonism commented Jan 24, 2021

I propose using asn1crypto to convert the ecdsa asn1 structures as required:

--- a/src/josepy/jwa.py	2020-12-28 20:03:20.515373893 +0100
+++ b/src/josepy/jwa.py	2021-01-24 13:15:02.416541955 +0100
@@ -6,6 +6,8 @@
@@ -5,6 +5,9 @@
 """
 import abc
 import logging
+import math
+
+from asn1crypto.core import SequenceOf, Integer
 
 import cryptography.exceptions
 from cryptography.hazmat.backends import default_backend
@@ -164,11 +167,20 @@
 class _JWAEC(JWASignature):
     kty = jwk.JWKEC
 
+    class RSSignature(SequenceOf):
+        _child_spec = Integer
+
     def __init__(self, name, hash_):
         super(_JWAEC, self).__init__(name)
         self.hash = hash_()
 
     def sign(self, key, msg):
+        sig = self._sign(key, msg)
+        seq = _JWAEC.RSSignature.load(sig)
+        return (r := seq[0].native).to_bytes(length=math.ceil(r.bit_length() / 8), byteorder='big') +\
+            (s := seq[1].native).to_bytes(length=math.ceil(s.bit_length() / 8), byteorder='big')
+
+    def _sign(self, key, msg):
         """Sign the ``msg`` using ``key``."""
         # If cryptography library supports new style api (v1.4 and later)
         new_api = hasattr(key, 'sign')
@@ -190,6 +202,14 @@
             raise errors.Error(str(error))
 
     def verify(self, key, msg, sig):
+        rlen = math.ceil(key._wrapped.key_size/8)
+        seq = _JWAEC.RSSignature([Integer(int.from_bytes(sig[0:rlen], byteorder="big")),
+                           Integer(int.from_bytes(sig[rlen:],
+                                                  byteorder="big"))])
+        asn1sig = seq.dump()
+        return self._verify(key, msg, asn1sig)
+
+    def _verify(self, key, msg, sig):
         """Verify the ``msg` and ``sig`` using ``key``."""
         # If cryptography library supports new style api (v1.4 and later)
         new_api = hasattr(key, 'verify')

@adferrand
Copy link
Collaborator

adferrand commented Feb 10, 2021

Hello @commonism! Thanks a lot for your help. If you want, you can also submit a PR based on this one, and I can merge it ultimately without squash to preserve the authorship from @jannispinter and you. Otherwise I think I can push commits on this PR while crediting you.

About the problematic of the signature format, I am quite hesitant to add a new dependency to josepy. How would it be complex to convert the ASN.1 format into the R || S format without adding asn1crypto?

By the way @atombrella, thanks a lot for your help in reviewing this PR, and of course @jannispinter and @yonran thanks a lot for all the work in the first place!

To clarify a little what is at stake on this PR, on top of what you propose in certbot/certbot#8569 @commonism, this would enable the possibility in Certbot to use ECDSA keys for the ACME accounts, for what Let'sEncrypt folks would be quite grateful.

@adferrand
Copy link
Collaborator

After a discussion with cryptography maintainers as mentionned above, in fact you can avoid a dependency on asn1crypto by using the decode_dss_signature method from cryptography.

https://cryptography.io/en/latest/hazmat/primitives/asymmetric/utils.html#cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature

You will get a tuple (R,S) from it given the bytes generated by the sig method on the EC key instance.

@adferrand adferrand changed the title WIP: Add support for ES256 Add support for ES256 Feb 11, 2021
@adferrand
Copy link
Collaborator

I went ahead and applied the modifications proposed by @commonism, but using the methods provided by cryptography to encode/decode signatures from ASN.1 format.

adferrand
adferrand previously approved these changes Feb 11, 2021
Copy link
Collaborator

@adferrand adferrand left a comment

Choose a reason for hiding this comment

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

OK, this LGTM now!

I merge instead of squashing to preserve authorship.

@adferrand adferrand force-pushed the ec-support branch 3 times, most recently from d7742cf to 7e19927 Compare February 11, 2021 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: unplanned Work that we believe should be done, but does not have a higher priority.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants