Open jrconlin opened 7 years ago
Ok, after lots of fun reasons to start a significant drinking habit, I've got code that converts between ecdsa's signature method and cryptography/libssl. Note: this only decodes, since that's all I needed to do. Encoding would be very similar, however.
import base64
import binascii
import json
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from pyasn1.type import univ, namedtype
from pyasn1.codec.der.encoder import encode
def repad(string):
# type: (str) -> str
"""Adds padding to strings for base64 decoding"""
if len(string) % 4:
string += '===='[len(string) % 4:]
return string
def raw_sig_to_der(auth):
# type: (str) -> str
"""Fix the JWT auth token.
The `ecdsa` library signs using a raw, 32octet pair of values (r,s).
Cryptography, which uses OpenSSL, uses a DER sequence of (s, r).
This function converts the raw ecdsa to DER.
"""
payload, asig = auth.rsplit(".", 1)
# s00per l33t signature check.
sig = base64.urlsafe_b64decode(repad(asig).encode('utf8'))
if len(sig) != 64:
return auth
# ecdsa and openssl transpose the "r" and "s" values of the signatures.
# for ecdsa signature is (r, s)
# for openssl, signature is (s, r)
# It's ok, though, because neither label them, even though openssl
# uses namedtypes, with the names set to ""
#
# Oh frabjous day!
ds = univ.Sequence(
componentType=namedtype.NamedTypes()
).setComponents(
univ.Integer(int(binascii.hexlify(sig[:32]), 16)),
univ.Integer(int(binascii.hexlify(sig[32:]), 16)),
)
encoded = encode(ds)
new_sig = base64.urlsafe_b64encode(encoded)
return "{}.{}".format(payload, new_sig)
def decode(token, key=None, *args, **kwargs):
# type (str, str) -> dict()
"""Decode a web token into a assertion dictionary.
This attempts to rectify both ecdsa and openssl generated
signatures. We use the built-in cryptography library since it wraps
libssl and is faster than the python only approach.
This raises an InvalidSignature exception if the signature fails.
"""
# convert the signature if needed.
token = raw_sig_to_der(token)
sig_material, signature = token.rsplit('.', 1)
# Note: key is already converted from base64 to a byte string
pkey = ec.EllipticCurvePublicNumbers.from_encoded_point(
ec.SECP256R1(),
key
).public_key(default_backend())
# eventually this would be a map of algs to signing algorithms.
verifier = pkey.verifier(
base64.urlsafe_b64decode(repad(signature.encode())),
ec.ECDSA(hashes.SHA256()))
verifier.update(sig_material.encode())
# This will raise an InvalidSignature exception if failure.
# It will be captured externally.
verifier.verify()
return json.loads(
base64.urlsafe_b64decode(repad(sig_material.split('.')[1])))
Hi,
While attempting to convert from jose to using python cryptography, I came across an issue that may impact your library. It appears that OpenSSL (libssl 1.0.2g+) return an EC signature that's a DER. It appears that it's a Sequence NamedTypes of Integers. You can spot his because the decoded signature is not 64 octets (it can vary between 69-71 or so). Unfortunately, since it's a NamedTypes Sequence, you can't just do quick surgery to extract the two 32octet values. Instead, you'll need to do a length check on the signature and potentially run it through an ASN1 parser. I've already filed a similar "heads up" issue with ecdsa, although I'm not sure where the best fix might be.
It's kinda/sorta a problem here because you can create a valid EC signature using openssl that decrypts just fine but would fail here. (One presumes the opposite may also be true since I don't know if JWT enforces the signature encoding format.)