-
Notifications
You must be signed in to change notification settings - Fork 255
Implement PyNaCl backend for Ed25519 keys (part of RFC 8037) #100
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
Closed
Closed
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
28e7b9d
Add backend and tests for Ed25519 keys (RFC8037)
blag 181781f
Integrate Ed25519 key backend into JWK
blag 16b4948
Refactor to make PyNaCl an optional dependency
blag 888323c
One more test
blag af5ed97
Remove references to nacl.encoding, handle decoding ourselves
blag File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import base64 | ||
|
|
||
| import six | ||
|
|
||
| from jose.backends.base import Key | ||
| from jose.constants import ALGORITHMS, USAGES | ||
| from jose.exceptions import JWKError | ||
|
|
||
| from nacl.exceptions import BadSignatureError | ||
| from nacl.signing import SigningKey, VerifyKey | ||
|
|
||
|
|
||
| class Ed25519Key(Key): | ||
| def __init__(self, key, algorithm, use=None): | ||
| if algorithm not in ALGORITHMS.ED: | ||
| raise JWKError('hash_alg: %s is not a valid Ed25519 hash algorithm' % algorithm) | ||
|
|
||
| # TODO: Validate Ed25519 hash algorithms | ||
| self._algorithm = algorithm | ||
|
|
||
| if isinstance(key, dict): | ||
| self._prepared_key = self._process_jwk(key) | ||
| return | ||
|
|
||
| if isinstance(key, (SigningKey, VerifyKey)): | ||
| self._prepared_key = key | ||
| return | ||
|
|
||
| if isinstance(key, six.string_types): | ||
| key = key.encode('utf-8') + b'==' | ||
|
|
||
| if isinstance(key, six.binary_type): | ||
| if use is None: | ||
| raise JWKError("The 'use' parameter is required when deserializing an Ed25519 key " | ||
| "from a string or bytes") | ||
|
|
||
| if use == USAGES.PUBLIC: | ||
| decoded_key_bytes = base64.urlsafe_b64decode(key) | ||
| self._prepared_key = VerifyKey(decoded_key_bytes) | ||
| elif use == USAGES.PRIVATE: | ||
| decoded_key_bytes = base64.urlsafe_b64decode(key) | ||
| self._prepared_key = SigningKey(decoded_key_bytes) | ||
| else: | ||
| raise JWKError("The 'use' parameter must either be 'public' or 'private', not %s" % use) | ||
| return | ||
|
|
||
| raise JWKError('Unable to parse an Ed25519_JWK from key: %s' % key) | ||
|
|
||
| def _process_jwk(self, jwk_dict): | ||
| if not jwk_dict.get('kty') == 'OKP': | ||
| raise JWKError("Incorrect key type. Expected: 'OKP', Received: %s" % jwk_dict.get('kty')) | ||
|
|
||
| if not jwk_dict.get('crv') == 'Ed25519': | ||
| raise JWKError("Incorrect key subtype. Expected 'Ed25519', Received %s" % jwk_dict.get('crv')) | ||
|
|
||
| if 'd' in jwk_dict: | ||
| # d indicates private key | ||
| d = jwk_dict.get('d').encode('utf-8') + b'==' | ||
| decoded_d_bytes = base64.urlsafe_b64decode(d) | ||
| return SigningKey(decoded_d_bytes) | ||
| else: | ||
| # no d indicates public key | ||
| x = jwk_dict.get('x').encode('utf-8') + b'==' | ||
| decoded_x_bytes = base64.urlsafe_b64decode(x) | ||
| return VerifyKey(decoded_x_bytes) | ||
|
|
||
| def sign(self, msg): | ||
| if isinstance(msg, six.string_types): | ||
| msg = msg.encode('utf-8') | ||
| return self._prepared_key.sign(msg) | ||
|
|
||
| def verify(self, msg, sig=None): | ||
| try: | ||
| self._prepared_key.verify(msg, sig) | ||
| return True | ||
| except BadSignatureError: | ||
| return False | ||
|
|
||
| def is_public(self): | ||
| return isinstance(self._prepared_key, VerifyKey) | ||
|
|
||
| def public_key(self): | ||
| if isinstance(self._prepared_key, VerifyKey): | ||
| return self | ||
| return self.__class__(self._prepared_key.verify_key, self._algorithm) | ||
|
|
||
| def to_pem(self, *args, **kwargs): | ||
| # Serializing Ed25519 keys is not yet supported anywhere in Python | ||
| # AFAICT, so instead of creating our own format, we simply prevent | ||
| # anybody from serializing to PEM. | ||
| raise NotImplementedError("Cannot serialize Ed25519 keys yet") | ||
|
|
||
| def to_dict(self): | ||
| public_key = self.public_key() | ||
|
|
||
| data = { | ||
| 'alg': self._algorithm, | ||
| 'kty': 'OKP', | ||
| 'crv': 'Ed25519', | ||
| 'x': base64.urlsafe_b64encode(bytes(public_key._prepared_key)).decode('utf-8'), | ||
| } | ||
|
|
||
| if not self.is_public(): | ||
| data.update({'d': base64.urlsafe_b64encode(bytes(self._prepared_key)).decode('utf-8')}) | ||
|
|
||
| return data |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ six | |
| rsa | ||
| ecdsa | ||
| pyasn1 | ||
| pynacl | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
|
|
||
| import base64 | ||
|
|
||
| from jose.backends.nacl_backend import Ed25519Key | ||
| from jose.constants import ALGORITHMS, USAGES | ||
| from jose.exceptions import JWKError | ||
|
|
||
| from nacl.signing import SigningKey, VerifyKey | ||
|
|
||
| import pytest | ||
|
|
||
|
|
||
| SIGNING_KEY = "npAVhmIfq2byvIzcmgS5cguKCv2Nw8Seqa1Fku00LoE=" | ||
| signing_key_bytes = base64.urlsafe_b64decode(SIGNING_KEY.encode('utf-8')) | ||
| nacl_signing_key = SigningKey(signing_key_bytes) | ||
| nacl_verify_key = nacl_signing_key.verify_key | ||
| VERIFY_KEY = base64.urlsafe_b64encode(bytes(nacl_verify_key)) | ||
|
|
||
|
|
||
| class TestEd25519Algorithm: | ||
|
|
||
| @pytest.mark.parametrize("alg", ALGORITHMS.ED) | ||
| @pytest.mark.parametrize("use", USAGES.ALL) | ||
| def test_Ed25519_key(self, alg, use): | ||
| assert Ed25519Key(SIGNING_KEY, algorithm=alg, use=use)._prepared_key | ||
| assert Ed25519Key(SIGNING_KEY.encode('utf-8'), algorithm=alg, use=use)._prepared_key | ||
| # With Ed25519, there is no difference between seeds for private and public keys, and ALL | ||
| # 256-bit values are valid seeds, so since we have already tested with private key seeds, | ||
| # we do not need to also test for public key seeds | ||
|
|
||
| @pytest.mark.parametrize("alg", ALGORITHMS.ED) | ||
| def test_Ed25519_signing_key(self, alg): | ||
| assert Ed25519Key(nacl_signing_key, algorithm=alg)._prepared_key | ||
| assert not Ed25519Key(nacl_signing_key, algorithm=alg).is_public() | ||
|
|
||
| @pytest.mark.parametrize("alg", ALGORITHMS.ED) | ||
| def test_Ed25519_verify_key(self, alg): | ||
| assert Ed25519Key(nacl_verify_key, algorithm=alg)._prepared_key | ||
| assert Ed25519Key(nacl_verify_key, algorithm=alg).is_public() | ||
|
|
||
| def test_Ed25519_key_unknown_object(self): | ||
| with pytest.raises(JWKError): | ||
| Ed25519Key(object(), algorithm=ALGORITHMS.EdDSA) | ||
|
|
||
| @pytest.mark.parametrize("use", USAGES.ALL) | ||
| def test_Ed25519_key_bad_alg(self, use): | ||
| with pytest.raises(JWKError): | ||
| Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.ES256, use=use) | ||
|
|
||
| @pytest.mark.parametrize("alg", ALGORITHMS.ED) | ||
| def test_Ed25519_key_bad_use(self, alg): | ||
| with pytest.raises(JWKError): | ||
| Ed25519Key(SIGNING_KEY, algorithm=alg) | ||
|
|
||
| with pytest.raises(JWKError): | ||
| Ed25519Key(SIGNING_KEY, algorithm=alg, use=None) | ||
|
|
||
| with pytest.raises(JWKError): | ||
| Ed25519Key(SIGNING_KEY, algorithm=alg, use='bad_usage') | ||
|
|
||
| def test_get_verify_key(self): | ||
| signing_key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE) | ||
|
|
||
| assert not signing_key.is_public() | ||
|
|
||
| verify_key = signing_key.public_key() # public_key is part of the Key API | ||
| verify_key2 = verify_key.public_key() | ||
|
|
||
| assert verify_key.is_public() | ||
| assert verify_key is verify_key2 | ||
|
|
||
| def test_to_pem(self): | ||
| signing_key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE) | ||
|
|
||
| with pytest.raises(NotImplementedError): | ||
| signing_key.to_pem() | ||
|
|
||
| def test_verify_key_to_pem(self): | ||
| signing_key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE) | ||
| verify_key = signing_key.public_key() | ||
|
|
||
| with pytest.raises(NotImplementedError): | ||
| verify_key.to_pem() | ||
|
|
||
| def assert_parameters(self, as_dict, private): | ||
| assert isinstance(as_dict, dict) | ||
|
|
||
| # Public parameters should always be there | ||
| assert 'x' in as_dict | ||
|
|
||
| if private: | ||
| # Private parameters as well | ||
| assert 'd' in as_dict | ||
| else: | ||
| # Private parameters should be absent | ||
| assert 'd' not in as_dict | ||
|
|
||
| def assert_roundtrip(self, key, use): | ||
| assert Ed25519Key(key.to_dict(), ALGORITHMS.EdDSA, use=use).to_dict() == key.to_dict() | ||
|
|
||
| def test_signing_key_to_dict(self): | ||
| key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE) | ||
|
|
||
| self.assert_parameters(key.to_dict(), private=True) | ||
| self.assert_roundtrip(key, use=USAGES.PRIVATE) | ||
|
|
||
| def test_verify_key_to_dict(self): | ||
| key = Ed25519Key(VERIFY_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PUBLIC) | ||
|
|
||
| self.assert_parameters(key.to_dict(), private=False) | ||
| self.assert_roundtrip(key, use=USAGES.PUBLIC) | ||
|
|
||
| def test_verify_key_from_bad_dict(self): | ||
| key = Ed25519Key(VERIFY_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PUBLIC) | ||
| key_dict = key.to_dict() | ||
|
|
||
| bad_key_dict = key_dict.copy() | ||
| bad_key_dict['kty'] = "SOMETHING_ELSE" | ||
|
|
||
| with pytest.raises(JWKError): | ||
| Ed25519Key(bad_key_dict, algorithm=ALGORITHMS.EdDSA) | ||
|
|
||
| bad_key_dict = key_dict.copy() | ||
| bad_key_dict['crv'] = "SOMETHING_ELSE" | ||
|
|
||
| with pytest.raises(JWKError): | ||
| Ed25519Key(bad_key_dict, algorithm=ALGORITHMS.EdDSA) | ||
|
|
||
| def test_signing_bytes_parity(self): | ||
| signing_key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE) | ||
| verify_key = signing_key.public_key() | ||
|
|
||
| msg = b'test' | ||
| smsg = signing_key.sign(msg) # -> signature + cleartext message | ||
| bad_smsg_message = bytes([(b + 1) if b < 255 else 0 for b in bytearray(smsg.message)]) | ||
| bad_smsg_signature = bytes([(b + 1) if b < 255 else 0 for b in bytearray(smsg.signature)]) | ||
| bad_smsg = bad_smsg_signature + bad_smsg_message | ||
|
|
||
| assert verify_key.verify(smsg) | ||
| assert verify_key.verify(smsg.signature + smsg.message) | ||
| assert verify_key.verify(smsg.message, smsg.signature) | ||
|
|
||
| assert not verify_key.verify(smsg.message, bad_smsg_signature) | ||
| assert not verify_key.verify(bad_smsg_message, smsg.signature) | ||
| assert not verify_key.verify(bad_smsg_message, bad_smsg_signature) | ||
| assert not verify_key.verify(bad_smsg) | ||
|
|
||
| def test_signing_string_parity(self): | ||
| signing_key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE) | ||
| verify_key = signing_key.public_key() | ||
|
|
||
| msg = 'test' | ||
| smsg = signing_key.sign(msg) # -> signature + cleartext message | ||
| bad_smsg_message = bytes([(b + 1) if b < 255 else 0 for b in bytearray(smsg.message)]) | ||
| bad_smsg_signature = bytes([(b + 1) if b < 255 else 0 for b in bytearray(smsg.signature)]) | ||
| bad_smsg = bad_smsg_signature + bad_smsg_message | ||
|
|
||
| assert verify_key.verify(smsg) | ||
| assert verify_key.verify(smsg.signature + smsg.message) | ||
| assert verify_key.verify(smsg.message, smsg.signature) | ||
|
|
||
| assert not verify_key.verify(smsg.message, bad_smsg_signature) | ||
| assert not verify_key.verify(bad_smsg_message, smsg.signature) | ||
| assert not verify_key.verify(bad_smsg_message, bad_smsg_signature) | ||
| assert not verify_key.verify(bad_smsg) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.