mpdavis / python-jose

A JOSE implementation in Python
MIT License
1.54k stars 234 forks source link

OpenSSL signatures return a DER format instead of Raw key #47

Open jrconlin opened 7 years ago

jrconlin commented 7 years ago

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.)

jrconlin commented 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])))