открытый ключ x509 для JWK в среде web-workers

#javascript #firebase #web-standards #webcrypto-api #cloudflare-workers

# #javascript #firebase #веб-стандарты #webcrypto-api #cloudflare-workers

Вопрос:

Я хочу проверить «токен firebase JWT» в среде «Cloudflare workers».

Проблема в том, что firebase-auth не предоставляет стандарт /.well-known/jwks.json , скорее они предоставляют x806 public key certificate формат (pem)

Я использую «Webcrypto API» для выполнения криптографической работы, вот что я задумал

 // 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
);
 

Я попробовал несколько пакетов на Github, все библиотеки, которые я нашел, либо не работают, либо используют nodejs API (например, buffer), который не будет работать в среде CF

Может ли кто-нибудь указать мне, как

  • преобразуйте открытый ключ firebase в JWK или
  • преобразуйте открытый ключ в другие стандарты ( "raw" | "pkcs8" | "spki" ), которые importKey могут принимать

ПРИМЕЧАНИЕ: мы находимся в среде «CF Workers», поэтому все API-интерфейсы «nodejs» не работают

Спасибо

Ответ №1:

Ключевым моментом здесь (так сказать) является то, что закрытые ключи формата PEM основаны на двоичном формате PKCS # 8. Формат «PEM» означает, что базовые двоичные данные были закодированы в кодировке base64 и имели комментарии, подобные --- BEGIN PRIVATE KEY --- добавленным. WebCrypto может понимать двоичный формат PKCS # 8, но не обрабатывает PEM. К счастью, расшифровать PEM вручную не так уж сложно.

Вот некоторый код от реального рабочего Cloudflare.

 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"]);
 

Обратите внимание, что PEM используется для переноса множества различных форматов. PKCS # 8 является общим для закрытых ключей. SPKI является общим для открытых ключей (и WebCrypto это тоже поддерживает). Сертификаты — это еще один формат, который, я не думаю, что WebCrypto может читать напрямую.

Ответ №2:

Я не уверен, что у вас есть в наличии с CF workers, но, возможно, с этого стоит начать:

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

Поскольку вы не можете использовать Buffer и потенциально не можете использовать криптографическую библиотеку узла, вам придется найти замену этой getCertThumbprint функции. Но все, что он делает, это создает хэш sha1 certDer , а base64 кодирует его, так что это, вероятно, не составит труда.

ОБНОВЛЕНИЕ: Это может сработать как замена getCertThumbprint . Я провел небольшое тестирование, и, похоже, он возвращает те же значения, что и выше, но я не использовал его для проверки JWT.

 const sha1 = require('sha1')

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