Mehari
Mehari

Reputation: 3267

x509 public key to JWK in web-workers environment

I want to verify "firebase JWT token" on "Cloudflare workers" environment.

The problem is firebase-auth doesn't provide the standard /.well-known/jwks.json,rather they provide x806 public key certificate (pem) format

I am using the "Webcrypto API" to do the Crypto work, here is what I am up to

// Get CryptoKey
const key = await crypto.subtle.importKey(
  "jwk", // it's possible to change this format if the pem can be changed to other standards
  jwk, //  ?? Here is the missing piece
  { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
  false,
  ["verify"]
);

// Verify
const success = await crypto.subtle.verify(
  "RSASSA-PKCS1-v1_5",
  key,
  signature, // JWT signature
  data // JWT payload
);

I have tried several packages on Github , all the libraries I found either doesn't work or use nodejs API (e.g buffer) which will not work on CF environment

Can someone point me how to

NB: we are on "CF Workers" environment so all "nodejs" apis doesn't work

Thanks

Upvotes: 1

Views: 1626

Answers (2)

Kenton Varda
Kenton Varda

Reputation: 45326

The key here (so to speak) is that PEM format private keys are based on PKCS #8 binary format. "PEM" format means that the underlying binary data has been base64-encoded and had comments like --- BEGIN PRIVATE KEY --- added. WebCrypto can understand PKCS #8 binary format, but does not handle PEM. Luckily, it's not too hard to decode PEM manually.

Here's some code, from a real production Cloudflare Worker.

let pem = "[your PEM string here]";

// Parse PEM base64 format into binary bytes.
// The first line removes comments and newlines to form one continuous
// base64 string, the second line decodes that to a Uint8Array.
let b64 = pem.split('\n').filter(line => !line.startsWith("--")).join("");
let bytes = new Uint8Array([...atob(b64)].map(c => c.charCodeAt(0)));

// Import key using WebCrypto API.
let key = await crypto.subtle.importKey("pkcs8", bytes,
    { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
    false, ["verify"]);

Note that PEM is used to wrap many different formats. PKCS #8 is common for private keys. SPKI is common for public keys (and WebCrypto supports that too). Certificates are yet another format, which I don't think WebCrypto can read directly.

Upvotes: 2

Cully
Cully

Reputation: 6975

I'm not sure what you have available with CF workers, but this might be something to start from:

const forge = require('node-forge')
const NodeRSA = require('node-rsa')
const {createHash} = require('crypto')
const base64url = require('base64url')

const getCertificateDer = certPem => {
    return forge.util.encode64(
        forge.asn1
            .toDer(forge.pki.certificateToAsn1(forge.pki.certificateFromPem(certPem)))
            .getBytes(),
    )
}

const getModulusExponent = certPem => {
    const nodeRsa = new NodeRSA()
    nodeRsa.importKey(certPem)

    const {n: modulus, e: exponent} = nodeRsa.exportKey('components-public')

    return {
        modulus,
        exponent,
    }
}

const getCertThumbprint = certDer => {
    const derBinaryStr = Buffer.from(certDer).toString('binary')

    const shasum = createHash('sha1')
    shasum.update(derBinaryStr)

    return shasum.digest('base64')
}

const getCertThumbprintEncoded = certDer => base64url.encode(getCertThumbprint(certDer))

const certPem = "<your pem certificate>"
const {modulus, exponent} = getModulusExponent(certPem)
const certDer = getCertificateDer(certPem)
const thumbprintEncoded = getCertThumbprintEncoded(certDer)

const jwksInfo = {
    alg: 'RSA256',
    kty: 'RSA',
    use: 'sig',
    x5c: [certDer],
    e: String(exponent),
    n: modulus.toString('base64'),
    kid: thumbprintEncoded,
    x5t: thumbprintEncoded,
}

Since you can't use Buffer and potentially can't use node's crypto library, you'll have to find a replacement for the getCertThumbprint function. But all it does is create a sha1 hash of certDer and base64 encodes it, so that probably won't be difficult.

UPDATE: This might work as a replacement for getCertThumbprint. I did a bit of testing and it seems to return the same values as the one above, but I haven't used it to verify a JWT.

const sha1 = require('sha1')

const getCertThumbprint = certDer => btoa(sha1(certDer))

Upvotes: 0

Related Questions