Validating JWT with root certificate

419 views Asked by At

I'm trying to load the FIDO Alliance Metadata in Python using JWCrypto, but I always get a jwcrypto.jws.InvalidJWSSignature('Verification failed') error.

FIDO Alliance provides an endpoint with authenticator metadata as they state here. The data is wrapped in a signed JWT token. They do not directly provide a public key, they however link to a GlobalSign root certificate they use.

How can I properly load and validate the JWT when I don't have their public key, only the certificate?

I tried deriving the public key from the certificate and using that to deserialize the JWT, but JWCrypto complains about invalid signature.

import requests
import jwcrypto.jwt, jwcrypto.jwk
import cryptography.x509
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.serialization
import cryptography.hazmat.primitives.ciphers.algorithms

# Download certificate
root_cert = requests.get("http://secure.globalsign.com/cacert/root-r3.crt").content.strip()

# Get public key from certificate
root_cert = cryptography.x509.load_der_x509_certificate(root_cert)
public_key_pem = root_cert.public_key().public_bytes(
    encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM,
    format=cryptography.hazmat.primitives.serialization.PublicFormat.PKCS1,
)
public_key = jwcrypto.jwk.JWK.from_pem(public_key_pem)

# Download JWT
signed_data = requests.get("https://mds3.fidoalliance.org/").content.strip()

# Deserialize
jwt = jwcrypto.jwt.JWT(jwt=signed_data.decode("ascii"), key=public_key)
print(jwt.claims)
1

There are 1 answers

0
MinistrChleba On

The problem with my approach was deriving the public key from the root certificate, while the JWT is signed by the leaf certificate, which is actually included in the JWT x5c header (see more in rfc7515).

So to verify the JWT signature, one needs to derive a public key from the leaf certificate and use that one:

import base64
import requests
import jwcrypto.jwt, jwcrypto.jwk
import cryptography.x509
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat


# Load and deserialize JWT
jwt = requests.get("https://mds3.fidoalliance.org/").content.strip()
jwt = jwcrypto.jwt.JWT(jwt=jwt.decode("ascii"))

# Deserialize the leaf certificate
trust_path = jwt.token.jose_header.get("x5c", [])
leaf_cert = cryptography.x509.load_der_x509_certificate(
    base64.b64decode(trust_path[0]))

# Derive public key and convert to JWK
public_key = leaf_cert.public_key()
public_key = public_key.public_bytes(Encoding.PEM, PublicFormat.PKCS1)
public_key = jwcrypto.jwk.JWK.from_pem(public_key)

# Validate JWT and access claims
jwt.validate(public_key)
print(jwt.claims)

That was all I needed!


Additional info: The root certificate is only useful for verifying the certificate chain:

...
from cryptography.hazmat.primitives.asymmetric import padding

# Load and deserialize root certificate
root_cert = requests.get("http://secure.globalsign.com/cacert/root-r3.crt").content.strip()

# Build certificate chain
trust_path = jwt.token.jose_header.get("x5c", [])
trust_path = [
    cryptography.x509.load_der_x509_certificate(base64.b64decode(cert))
    for cert in trust_path
]
trust_path.append(cryptography.x509.load_der_x509_certificate(root_cert))

# Certificate chain verification
for i in range(len(trust_path) - 1):
    issuer_certificate = trust_path[i + 1]
    subject_certificate = trust_path[i]
    issuer_public_key = issuer_certificate.public_key()
    issuer_public_key.verify(
        subject_certificate.signature,
        subject_certificate.tbs_certificate_bytes,
        padding.PKCS1v15(),
        subject_certificate.signature_hash_algorithm,
    )