Skip to content

Commit 88972a9

Browse files
authored
Add from_jwk to Ed25519Algorithm (Support kty: OKP). (jpadilla#623)
* Support from_jwk on Ed25519Algorithm. * Update CHANGELOG.
1 parent f6d4bbf commit 88972a9

File tree

5 files changed

+110
-0
lines changed

5 files changed

+110
-0
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Added
2323
- Add caching by default to PyJWKClient `#611 <https://github.com/jpadilla/pyjwt/pull/611>`__
2424
- Add missing exceptions.InvalidKeyError to jwt module __init__ imports `#620 <https://github.com/jpadilla/pyjwt/pull/620>`__
2525
- Add support for ES256K algorithm `#629 <https://github.com/jpadilla/pyjwt/pull/629>`__
26+
- Add `from_jwk()` to Ed25519Algorithm `#621 <https://github.com/jpadilla/pyjwt/pull/621>`__
2627

2728
`v2.0.1 <https://github.com/jpadilla/pyjwt/compare/2.0.0...2.0.1>`__
2829
--------------------------------------------------------------------

jwt/algorithms.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,3 +586,34 @@ def verify(self, msg, key, sig):
586586
return True # If no exception was raised, the signature is valid.
587587
except cryptography.exceptions.InvalidSignature:
588588
return False
589+
590+
@staticmethod
591+
def from_jwk(jwk):
592+
try:
593+
if isinstance(jwk, str):
594+
obj = json.loads(jwk)
595+
elif isinstance(jwk, dict):
596+
obj = jwk
597+
else:
598+
raise ValueError
599+
except ValueError:
600+
raise InvalidKeyError("Key is not valid JSON")
601+
602+
if obj.get("kty") != "OKP":
603+
raise InvalidKeyError("Not an Octet Key Pair")
604+
605+
curve = obj.get("crv")
606+
if curve != "Ed25519":
607+
raise InvalidKeyError(f"Invalid curve: {curve}")
608+
609+
if "x" not in obj:
610+
raise InvalidKeyError('OKP should have "x" parameter')
611+
x = base64url_decode(obj.get("x"))
612+
613+
try:
614+
if "d" not in obj:
615+
return Ed25519PublicKey.from_public_bytes(x)
616+
d = base64url_decode(obj.get("d"))
617+
return Ed25519PrivateKey.from_private_bytes(d)
618+
except ValueError as err:
619+
raise InvalidKeyError("Invalid key parameter") from err
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"kty":"OKP",
3+
"crv":"Ed25519",
4+
"d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
5+
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"kty":"OKP",
3+
"crv":"Ed25519",
4+
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
5+
}

tests/test_algorithms.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,3 +733,70 @@ def test_ed25519_prepare_key_should_be_idempotent(self):
733733
jwt_pub_key_second = algo.prepare_key(jwt_pub_key_first)
734734

735735
assert jwt_pub_key_first == jwt_pub_key_second
736+
737+
def test_ed25519_jwk_private_key_should_parse_and_verify(self):
738+
algo = Ed25519Algorithm()
739+
740+
with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile:
741+
key = algo.from_jwk(keyfile.read())
742+
743+
signature = algo.sign(b"Hello World!", key)
744+
assert algo.verify(b"Hello World!", key.public_key(), signature)
745+
746+
def test_ed25519_jwk_public_key_should_parse_and_verify(self):
747+
algo = Ed25519Algorithm()
748+
749+
with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile:
750+
priv_key = algo.from_jwk(keyfile.read())
751+
752+
with open(key_path("jwk_okp_pub_Ed25519.json")) as keyfile:
753+
pub_key = algo.from_jwk(keyfile.read())
754+
755+
signature = algo.sign(b"Hello World!", priv_key)
756+
assert algo.verify(b"Hello World!", pub_key, signature)
757+
758+
def test_ed25519_jwk_fails_on_invalid_json(self):
759+
algo = Ed25519Algorithm()
760+
761+
with open(key_path("jwk_okp_pub_Ed25519.json")) as keyfile:
762+
valid_pub = json.loads(keyfile.read())
763+
with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile:
764+
valid_key = json.loads(keyfile.read())
765+
766+
# Invalid instance type
767+
with pytest.raises(InvalidKeyError):
768+
algo.from_jwk(123)
769+
770+
# Invalid JSON
771+
with pytest.raises(InvalidKeyError):
772+
algo.from_jwk("<this isn't json>")
773+
774+
# Invalid kty, not "OKP"
775+
v = valid_pub.copy()
776+
v["kty"] = "oct"
777+
with pytest.raises(InvalidKeyError):
778+
algo.from_jwk(v)
779+
780+
# Invalid crv, not "Ed25519"
781+
v = valid_pub.copy()
782+
v["crv"] = "P-256"
783+
with pytest.raises(InvalidKeyError):
784+
algo.from_jwk(v)
785+
786+
# Missing x
787+
v = valid_pub.copy()
788+
del v["x"]
789+
with pytest.raises(InvalidKeyError):
790+
algo.from_jwk(v)
791+
792+
# Invalid x
793+
v = valid_pub.copy()
794+
v["x"] = "123"
795+
with pytest.raises(InvalidKeyError):
796+
algo.from_jwk(v)
797+
798+
# Invalid d
799+
v = valid_key.copy()
800+
v["d"] = "123"
801+
with pytest.raises(InvalidKeyError):
802+
algo.from_jwk(v)

0 commit comments

Comments
 (0)