Skip to content

Convert JWT signatures to the correct format #44

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

Merged
merged 1 commit into from May 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Convert JWT signatures to the correct format.
`key.sign` returns an ASN.1-encoded, DER-formatted signature, but
RFC 7518, section 3.4 says we should include R and S directly, in that
order, and in big-endian form.
  • Loading branch information
Kit Cambridge committed May 7, 2017
commit 5eba0bdb9da3f5c79c5e43116c04f78930fbbb71
16 changes: 8 additions & 8 deletions python/py_vapid/jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
from cryptography.hazmat.primitives.asymmetric import ec, utils
from cryptography.hazmat.primitives import hashes

from py_vapid.utils import b64urldecode, b64urlencode
from py_vapid.utils import b64urldecode, b64urlencode, num_to_bytes


def extract_signature(auth):
"""Fix the JWT auth token

convert a ecdsa integer pair into an OpenSSL DER pair.
"""
Extracts the payload and signature from a JWT, converting the
signature from the JOSE format (RFC 7518, section 3.4) to the
RFC 3279 format that Cryptography requires.

:param auth: A JWT Authorization Token.
:type auth: str
Expand All @@ -23,7 +24,7 @@ def extract_signature(auth):
payload, asig = auth.encode('utf8').rsplit(b'.', 1)
sig = b64urldecode(asig)
if len(sig) != 64:
return payload, sig
raise InvalidSignature()

encoded = utils.encode_dss_signature(
s=int(binascii.hexlify(sig[32:]), 16),
Expand All @@ -35,8 +36,6 @@ def extract_signature(auth):
def decode(token, key):
"""Decode a web token into an assertion dictionary

This attempts to rectify both ecdsa and openssl generated signatures.

:param token: VAPID auth token
:type token: str
:param key: bitarray containing the public key
Expand Down Expand Up @@ -84,5 +83,6 @@ def sign(claims, key):
separators=(',', ':')).encode('utf8'))
token = "{}.{}".format(header, claims)
rsig = key.sign(token.encode('utf8'), ec.ECDSA(hashes.SHA256()))
sig = b64urlencode(rsig)
(r, s) = utils.decode_dss_signature(rsig)
sig = b64urlencode(num_to_bytes(r) + num_to_bytes(s))
return "{}.{}".format(token, sig)
24 changes: 1 addition & 23 deletions python/py_vapid/tests/test_vapid.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
from nose.tools import eq_, ok_
from mock import patch, Mock

from cryptography.hazmat.primitives.asymmetric import ec, utils
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes

from py_vapid import Vapid01, Vapid02, VapidException
from py_vapid.jwt import decode
from py_vapid.utils import b64urldecode

# This is a private key in DER form.
T_DER = """
Expand Down Expand Up @@ -174,27 +173,6 @@ def test_sign_02(self):
for k in claims:
eq_(t_val[k], claims[k])

def test_alt_sign(self):
"""ecdsa uses a raw key pair to sign, openssl uses a DER."""
v = Vapid01.from_file("/tmp/private")
claims = {"aud": "https://example.com",
"sub": "mailto:[email protected]",
"foo": "extra value"}
# Get a signed token.
result = v.sign(claims)
# Convert the dss into raw.
auth, sig = result.get('Authorization').split(' ')[1].rsplit('.', 1)
ss = utils.decode_dss_signature(b64urldecode(sig.encode('utf8')))
new_sig = binascii.b2a_base64(
binascii.unhexlify("%064x%064x" % ss)
).strip().strip(b'=').decode()
new_auth = auth + '.' + new_sig
# phew, all that done, now check
pkey = result.get("Crypto-Key").split('=')[1]
items = decode(new_auth, pkey)

eq_(items, claims)

def test_bad_sign(self):
v = Vapid01.from_file("/tmp/private")
self.assertRaises(VapidException,
Expand Down
14 changes: 14 additions & 0 deletions python/py_vapid/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import base64
import binascii


def b64urldecode(data):
Expand All @@ -23,3 +24,16 @@ def b64urlencode(data):

"""
return base64.urlsafe_b64encode(data).replace(b'=', b'').decode('utf8')


def num_to_bytes(n):
"""Returns the byte representation of an integer, in big-endian order.

:param n: The integer to encode.
:type n: int

:returns bytes

"""
h = '%x' % n
return binascii.unhexlify('0' * (len(h) % 2) + h)