1

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)
MinistrChleba
  • 31
  • 1
  • 5
  • 1
    Usually the data is not directly signed by the public key of the root CA certificate. The root cert is used to sign zero or more intermediate CA certificates, and CA certificate signs a leaf certificate, which signs the data, in this case a JWT. If you get the root certificate you've got the **trust anchor**. Now when you get the signed data which includes the leaf cert then you need to create a **trust path** towards the trust anchor, validate & verify all the certs, and then check the signature using the public key of the leaf certificate. – Maarten Bodewes Aug 24 '23 at 11:45
  • 2
    You will find an x5c parameter with a certificate chain in the JWT header. For the verification of the JWT the first certificate has to be used! Each subsequent certificate in the chain certifies the previous one, see RFC7515, sec. 4.1.6. The chain contains all certificates except the root certificate. – Topaco Aug 24 '23 at 13:20

1 Answers1

0

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

MinistrChleba
  • 31
  • 1
  • 5