-
Notifications
You must be signed in to change notification settings - Fork 30
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
Conversation
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. |
Hello there, |
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. |
Is there anything that can be done to advance the status this pull request? |
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. |
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. |
There was a problem hiding this 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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by a5fb321
src/josepy/jwk.py
Outdated
@@ -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()) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by 2c3cabe
src/josepy/jwk.py
Outdated
'x': public.x, | ||
'y': public.y, | ||
}) | ||
params = dict((key, self._encode_param(value)) |
There was a problem hiding this comment.
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)}
There was a problem hiding this comment.
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----- |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by 2f8bebe
src/josepy/jwk.py
Outdated
elif isinstance(self.key._wrapped, ec.EllipticCurvePrivateKey): | ||
private = self.key.private_numbers() | ||
public = self.key.public_key().public_numbers() | ||
params.update({ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.')) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>)>
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') |
There was a problem hiding this comment.
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?
@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 |
And maybe the draft status is no longer necessary? |
Works for me. One thing to add - Lines 57 to 68 in c95193d
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,
) |
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:
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: 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 . |
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') |
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. |
After a discussion with You will get a tuple |
1d50dcd
to
f5dd050
Compare
I went ahead and applied the modifications proposed by @commonism, but using the methods provided by |
There was a problem hiding this 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.
d7742cf
to
7e19927
Compare
7e19927
to
40b7fc8
Compare
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
andES512
.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.