From fa02696c21d705a1963fa94fa60187dda2917e3b Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Mon, 30 Dec 2024 00:17:08 +0900 Subject: [PATCH 1/2] supported x509 certification for session cookie --- src/jwk-fetcher.ts | 42 +++++++++-- src/jws-verifier.ts | 2 +- src/jwt-decoder.ts | 4 -- src/token-verifier.ts | 2 +- src/x509.ts | 142 ++++++++++++++++++++++++++++++++++++++ tests/jwk-fetcher.test.ts | 51 +++++++++++++- tests/jwk-utils.ts | 3 +- tests/x509.test.ts | 74 ++++++++++++++++++++ 8 files changed, 305 insertions(+), 15 deletions(-) create mode 100644 src/x509.ts create mode 100644 tests/x509.test.ts diff --git a/src/jwk-fetcher.ts b/src/jwk-fetcher.ts index 9629ad7..2ba7c33 100644 --- a/src/jwk-fetcher.ts +++ b/src/jwk-fetcher.ts @@ -1,6 +1,6 @@ -import type { JsonWebKeyWithKid } from './jwt-decoder'; import type { KeyStorer } from './key-store'; import { isNonNullObject, isObject, isURL } from './validator'; +import { jwkFromX509 } from './x509'; export interface KeyFetcher { fetchPublicKeys(): Promise>; @@ -24,6 +24,22 @@ export const isJWKMetadata = (value: any): value is JWKMetadata => { return keys.length === filtered.length; }; +export const isX509Certificates = (value: any): value is Record => { + if (!isNonNullObject(value)) { + return false; + } + const values = Object.values(value); + if (values.length === 0) { + return false; + } + for (const v of values) { + if (typeof v !== 'string' || v === '') { + return false; + } + } + return true; +}; + /** * Class to fetch public keys from a client certificates URL. */ @@ -54,20 +70,32 @@ export class UrlKeyFetcher implements KeyFetcher { throw new Error(errorMessage + text); } - const publicKeys = await resp.json(); - if (!isJWKMetadata(publicKeys)) { - throw new Error(`The public keys are not an object or null: "${publicKeys}`); - } + const json = await resp.json(); + const publicKeys = await this.retrievePublicKeys(json); const cacheControlHeader = resp.headers.get('cache-control'); // store the public keys cache in the KV store. const maxAge = parseMaxAge(cacheControlHeader); if (!isNaN(maxAge) && maxAge > 0) { - await this.keyStorer.put(JSON.stringify(publicKeys.keys), maxAge); + await this.keyStorer.put(JSON.stringify(publicKeys), maxAge); } - return publicKeys.keys; + return publicKeys; + } + + private async retrievePublicKeys(json: unknown): Promise> { + if (isX509Certificates(json)) { + const jwks: JsonWebKeyWithKid[] = []; + for (const [kid, x509] of Object.entries(json)) { + jwks.push(await jwkFromX509(kid, x509)); + } + return jwks; + } + if (!isJWKMetadata(json)) { + throw new Error(`The public keys are not an object or null: "${json}`); + } + return json.keys; } } diff --git a/src/jws-verifier.ts b/src/jws-verifier.ts index 3885acd..a574ce1 100644 --- a/src/jws-verifier.ts +++ b/src/jws-verifier.ts @@ -1,7 +1,7 @@ import { JwtError, JwtErrorCode } from './errors'; import type { KeyFetcher } from './jwk-fetcher'; import { HTTPFetcher, UrlKeyFetcher } from './jwk-fetcher'; -import type { JsonWebKeyWithKid, RS256Token } from './jwt-decoder'; +import type { RS256Token } from './jwt-decoder'; import type { KeyStorer } from './key-store'; import { isNonNullObject } from './validator'; diff --git a/src/jwt-decoder.ts b/src/jwt-decoder.ts index 528be40..5f74683 100644 --- a/src/jwt-decoder.ts +++ b/src/jwt-decoder.ts @@ -7,10 +7,6 @@ export interface TokenDecoder { decode(token: string): Promise; } -export interface JsonWebKeyWithKid extends JsonWebKey { - kid: string; -} - export type DecodedHeader = { kid: string; alg: 'RS256' } & Record; export type DecodedPayload = { diff --git a/src/token-verifier.ts b/src/token-verifier.ts index e3e90f3..13a743e 100644 --- a/src/token-verifier.ts +++ b/src/token-verifier.ts @@ -432,7 +432,7 @@ export function baseCreateIdTokenVerifier( } // URL containing the public keys for Firebase session cookies. -const SESSION_COOKIE_CERT_URL = 'https://identitytoolkit.googleapis.com/v1/sessionCookiePublicKeys'; +const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys'; /** * User facing token information related to the Firebase session cookie. diff --git a/src/x509.ts b/src/x509.ts new file mode 100644 index 0000000..4a15c70 --- /dev/null +++ b/src/x509.ts @@ -0,0 +1,142 @@ +import { decodeBase64 } from './base64'; + +/** + * Parses a sequence of ASN.1 elements from a given Uint8Array. + * Internally, this function repeatedly calls `parseElement` on + * the subarray until the entire sequence is consumed, returning + * an array of parsed elements. + */ +function getElement(seq: Uint8Array) { + const result = []; + let next = 0; + + while (next < seq.length) { + // Parse one ASN.1 element from the remaining subarray + const nextPart = parseElement(seq.subarray(next)); + result.push(nextPart); + // Advance the pointer by the element's total byte length + next += nextPart.byteLength; + } + return result; +} + +/** + * Parses a single ASN.1 element (in DER encoding) from the given byte array. + * + * Each element consists of: + * 1) Tag (possibly multiple bytes if 0x1f is encountered) + * 2) Length (short form or long form, possibly indefinite) + * 3) Contents (the data payload) + * + * Returns an object containing: + * - byteLength: total size (in bytes) of this element (including tag & length) + * - contents: Uint8Array of just the element's contents + * - raw: Uint8Array of the entire element (tag + length + contents) + */ +function parseElement(bytes: Uint8Array) { + let position = 0; + + // --- Parse Tag --- + // The tag is in the lower 5 bits (0x1f). If it's 0x1f, it indicates a multi-byte tag. + let tag = bytes[0] & 0x1f; + position++; + if (tag === 0x1f) { + tag = 0; + // Continue reading the tag bytes while each byte >= 0x80 + while (bytes[position] >= 0x80) { + tag = tag * 128 + bytes[position] - 0x80; + position++; + } + tag = tag * 128 + bytes[position] - 0x80; + position++; + } + + // --- Parse Length --- + let length = 0; + // Short-form length: if less than 0x80, it's the length itself + if (bytes[position] < 0x80) { + length = bytes[position]; + position++; + } else if (length === 0x80) { + // Indefinite length form: scan until 0x00 0x00 + length = 0; + while (bytes[position + length] !== 0 || bytes[position + length + 1] !== 0) { + if (length > bytes.byteLength) { + throw new TypeError('invalid indefinite form length'); + } + length++; + } + const byteLength = position + length + 2; + return { + byteLength, + contents: bytes.subarray(position, position + length), + raw: bytes.subarray(0, byteLength), + }; + } else { + // Long-form length: the lower 7 bits of this byte indicates how many bytes follow for length + const numberOfDigits = bytes[position] & 0x7f; + position++; + length = 0; + // Accumulate the length from these "numberOfDigits" bytes + for (let i = 0; i < numberOfDigits; i++) { + length = length * 256 + bytes[position]; + position++; + } + } + + // The total byte length of this element (tag + length + contents) + const byteLength = position + length; + return { + byteLength, + contents: bytes.subarray(position, byteLength), + raw: bytes.subarray(0, byteLength), + }; +} + +/** + * Extracts the SubjectPublicKeyInfo (SPKI) portion from a DER-encoded X.509 certificate. + * + * Steps: + * 1) Parse the entire certificate as an ASN.1 SEQUENCE. + * 2) Retrieve the TBS (To-Be-Signed) Certificate, which is the first element. + * 3) Parse the TBS Certificate to get its internal fields (version, serial, issuer, etc.). + * 4) Depending on whether the version field is present (tag = 0xa0), the SPKI is either + * at index 6 or 5 (skipping version if absent). + * 5) Finally, encode the raw SPKI bytes in CryptoKey and return. + */ +async function spkiFromX509(buf: Uint8Array): Promise { + // Parse the top-level ASN.1 structure, then get the top-level contents + // which typically contain [ TBS Certificate, signatureAlgorithm, signature ]. + // Retrieve TBS Certificate as [0], then parse TBS Certificate further. + const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents); + + // In the TBS Certificate, check whether the first element (index 0) is a version field (tag=0xa0). + // If it is, the SubjectPublicKeyInfo is the 7th element (index 6). + // Otherwise, it is the 6th element (index 5). + const spki = tbsCertificate[tbsCertificate[0].raw[0] === 0xa0 ? 6 : 5].raw; + return await crypto.subtle.importKey( + 'spki', + spki, + { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256', + }, + true, + ['verify'] + ); +} + +export async function jwkFromX509(kid: string, x509: string): Promise { + const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, ''); + const raw = decodeBase64(pem); + const spki = await spkiFromX509(raw); + const { kty, alg, n, e } = await crypto.subtle.exportKey('jwk', spki); + return { + kid, + use: 'sig', + kty, + alg, + n, + e, + }; +} diff --git a/tests/jwk-fetcher.test.ts b/tests/jwk-fetcher.test.ts index 5b57031..de88328 100644 --- a/tests/jwk-fetcher.test.ts +++ b/tests/jwk-fetcher.test.ts @@ -1,7 +1,7 @@ import { Miniflare } from 'miniflare'; import { describe, it, expect, vi } from 'vitest'; import type { Fetcher } from '../src/jwk-fetcher'; -import { isJWKMetadata, parseMaxAge, UrlKeyFetcher } from '../src/jwk-fetcher'; +import { isJWKMetadata, isX509Certificates, parseMaxAge, UrlKeyFetcher } from '../src/jwk-fetcher'; import { WorkersKVStore } from '../src/key-store'; class HTTPMockFetcher implements Fetcher { @@ -205,3 +205,52 @@ describe('isJWKMetadata', () => { expect(isJWKMetadata({ keys: [{ kid: 'string' }, {}] })).toBe(false); }); }); + +describe('isX509Certificates', () => { + it('should return true for valid X509 certificates', () => { + const validX509 = { + cert1: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6', + cert2: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz7', + }; + expect(isX509Certificates(validX509)).toBe(true); + }); + + it('should return false for null', () => { + expect(isX509Certificates(null)).toBe(false); + }); + + it('should return false for undefined', () => { + expect(isX509Certificates(undefined)).toBe(false); + }); + + it('should return false for non-object', () => { + expect(isX509Certificates('string')).toBe(false); + expect(isX509Certificates(123)).toBe(false); + expect(isX509Certificates(true)).toBe(false); + }); + + it('should return false for object with non-string values', () => { + const invalidX509 = { + cert1: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6', + cert2: 123, + }; + expect(isX509Certificates(invalidX509)).toBe(false); + }); + + it('should return false for object with empty values', () => { + const invalidX509 = { + cert1: '', + cert2: '', + }; + expect(isX509Certificates(invalidX509)).toBe(false); + }); + + it('should return false for object with mixed valid and invalid values', () => { + const invalidX509 = { + cert1: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6', + cert2: 123, + cert3: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz7', + }; + expect(isX509Certificates(invalidX509)).toBe(false); + }); +}); diff --git a/tests/jwk-utils.ts b/tests/jwk-utils.ts index 08046d0..cc968f0 100644 --- a/tests/jwk-utils.ts +++ b/tests/jwk-utils.ts @@ -1,8 +1,9 @@ import { encodeBase64Url, encodeObjectBase64Url } from '../src/base64'; import type { KeyFetcher } from '../src/jwk-fetcher'; import { rs256alg } from '../src/jws-verifier'; -import type { DecodedHeader, DecodedPayload, JsonWebKeyWithKid } from '../src/jwt-decoder'; +import type { DecodedHeader, DecodedPayload } from '../src/jwt-decoder'; import { utf8Encoder } from '../src/utf8'; +import type { JsonWebKeyWithKid } from '@cloudflare/workers-types'; export class TestingKeyFetcher implements KeyFetcher { constructor( diff --git a/tests/x509.test.ts b/tests/x509.test.ts new file mode 100644 index 0000000..6cf04e7 --- /dev/null +++ b/tests/x509.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest'; +import { jwkFromX509 } from '../src/x509'; +import type { JsonWebKeyWithKid } from '@cloudflare/workers-types'; + +const relyingpartyPublicKeys: Record = { + 'bZ-_5g': + '-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIEHMOyTzANBgkqhkiG9w0BAQsFADAzMQ8wDQYDVQQDEwZH\naXRraXQxEzARBgNVBAoTCkdvb2dsZSBJbmMxCzAJBgNVBAYTAlVTMB4XDTI0MTIw\nNjAyMjUzM1oXDTI1MTIwMTAyMjUzM1owMzEPMA0GA1UEAxMGR2l0a2l0MRMwEQYD\nVQQKEwpHb29nbGUgSW5jMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBANyVAukjUNaA9z55PCK3I803APf7g5o+x+h89pOhFQdeHZd+\nAMamdbtLsbmgfZ0lxTaIAbaEdWW9ZLFTLbsO9F8Vc38n0goxdnngS85d1stih0Wm\nY2p04qAkuyFpjVMGLoTtcep9rguc+0UuDTBya0PsEsltE0Dgt8HVGl8ZFnF4QNY4\ncIFl/JTF0JPpGxCj801L/Za+KMneni1bMPxdn7NThoVbw39MVqdIYTjnWxFnDnZ5\nUSLIxOhFHtCaf6kQw3uNkykiZiM90XzADEb3RU1uShEweuSh/W890tnG8uXOe34/\nMVSRvwcbD4sJvMC4EKsYzzJ4mN/i4GC5gHfSjyMCAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCB4AwDQYJ\nKoZIhvcNAQELBQADggEBAJCNjAzki0cnWXSZZ17xVFCqbJwDjaubMVSPrGBvdiZP\nQyDNQc9BNKO57ui/rK1KhPh3TA/g5BElVjqsJa19WcUh9klG0bCRQyuiKyMg56WG\nIv/z7exyj5AeF8/Tk2pLws5LhhBpKN3gfiX+IIr+1dM2iaBN0PTk7+nUo2Yf7Zwg\nYlxXke96Xdi7mKNjT+0gd8C0D0A4PZeqKvEfACwHfbXc5jdIDmhl2yVbuNfAMAGR\nmJYbg3tMyH1gG+Yey8M87cPdUaivprn2wvBPAJAAsV6umdKlJgN/Qd7zZ7irGgfQ\nZMT3wPNaSlwZRbMow3gQCnwUAiAM6uTju32jbbhQE/8=\n-----END CERTIFICATE-----\n', + _aLBDQ: + '-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIEBlNmITANBgkqhkiG9w0BAQsFADAzMQ8wDQYDVQQDEwZH\naXRraXQxEzARBgNVBAoTCkdvb2dsZSBJbmMxCzAJBgNVBAYTAlVTMB4XDTI0MDcx\nMTA5MzgzNFoXDTI1MDcwNjA5MzgzNFowMzEPMA0GA1UEAxMGR2l0a2l0MRMwEQYD\nVQQKEwpHb29nbGUgSW5jMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAMI0H9SjPV26sipRjPTl6HR6Q8pOQ/6GsfR2HOLT5bkpc0cq\n8NE0XqWDxf0g/mhVb3O5b/jN4q3LWFaVeISshcJINE7zKH+zKJhYwR8L79+fjzNN\noEwx4FESepTUHVW7qC49U1oac30oMD4c5ZCuCZ7OTgboASu2MOctR/p8+R6jsvdL\nfSpg4CKKy8AlUT+liuXpv4Oaptpp6murTmkDEqVJoLJKMpn0yiTPalAh8C514KXx\n3SxhPAOm5plsVxYGBYZ3XBOaB+Uu0xZi5HNYnSJmo9TihF/RWvE2mzE4IE8VA2YI\nZBNctcuCk2No/r9W/2gTF6e2YbrjfvkD0Vg+ovkCAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCB4AwDQYJ\nKoZIhvcNAQELBQADggEBADeGUKr/lCSUM/0Q5gxvcocmxtoD4WOylGs0dBPd/Gs2\nUZ55sXJXzmf4QUwLAOMBzHwr8mGcOYVtuXuOKLxPB/rNhFM+A841ZBTXsOFPRPXo\npi+fyNd0bDOG70MmSUx0LyHooj4cFHS/2mErOSjk70C43DqcTrPwLuUexiyCZVz3\nFQHJlc+PeHUd4ly4sAtCKNpTopc+VbEctMfsVHXuFDyw8DRP1G5T7+JfIwIyJ20J\n7pok5/SkEL1nr/BwGUcj/AxskHIFvAJuF1BJjxow0sRxbrChzLJYhZHdN7dvPNqq\neV+H6nbsUskrdeLWmzIR2xa4BIbygdCKJohuTn8ZYRU=\n-----END CERTIFICATE-----\n', + usAeNA: + '-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIENHxoNzANBgkqhkiG9w0BAQsFADAzMQ8wDQYDVQQDEwZH\naXRraXQxEzARBgNVBAoTCkdvb2dsZSBJbmMxCzAJBgNVBAYTAlVTMB4XDTI0MDgx\nNjA5NDUxMFoXDTI1MDgxMTA5NDUxMFowMzEPMA0GA1UEAxMGR2l0a2l0MRMwEQYD\nVQQKEwpHb29nbGUgSW5jMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAMJXF9Y6Db3yIKjXDWvApd+ITJVWm9jQ2uhcD7TaMtYsjwiw\nd4TRETSZS/PC/01lm7smQbtBUEzewM9dGFtuZHisp6IFPsbRaYHYwU4nVn60YMYs\n9iQ5epppqCft/rVLBe5sBUESIS9su5IuCeTCPgzGicFqppmYBhiPSI7T7ztBoSSX\ng8eLajZb9/GqpjdQq6bLIbDLXsL4urMDskneh447UWqKyKr2mkZVsPxjnOfCjHby\n5kfJsuzF6wdyhRXBZ3SE791+RjGA/a07caDKQXDDfwkJ+Ilg/qdcDybzGg6My/D/\nK4imOyxmhLKKL1NA9RUG/WDGsiSozKelz1juGt8CAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCB4AwDQYJ\nKoZIhvcNAQELBQADggEBAC277O98HLdEabZdFcLIvapRGN69K7LvM8zKp9L2ZGYb\ndyxrReKvE8+5Oc1Da2CsTbGgMxe70XZMdKIElPaX6hOGXbLBjcfFzy7uZtyZthyh\njVC1zBifCWNVdRzaJpRNJ0Jt6ysX6EVeeL7hoZ3IcME9CfIu2CVKLMbRrykt3PfG\ntbCFTq+G7NSOCgf5fWvH8IFZInjiVM3CcGMsPfmnoIpIyc60Hx0WUJQ7kCkoW1DC\n61MxA3A8kugVK47/y42NM+ej7OthtSH4gVOybZBcYrmmFiAb4O/Ijcgt4GFC147x\n8KNG2B4OAH9l3EcKjPmrdkjIaJOR5mq7S3bnxO5PxPs=\n-----END CERTIFICATE-----\n', + KRSnmg: + '-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIEZk8pnzANBgkqhkiG9w0BAQsFADAzMQ8wDQYDVQQDEwZH\naXRraXQxEzARBgNVBAoTCkdvb2dsZSBJbmMxCzAJBgNVBAYTAlVTMB4XDTI0MDky\nMTE3MjcwN1oXDTI1MDkxNjE3MjcwN1owMzEPMA0GA1UEAxMGR2l0a2l0MRMwEQYD\nVQQKEwpHb29nbGUgSW5jMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAMN5WgJ7EQbhDF8UquawppXBt9gvjbL6gnfVXaQi5KvrSp+P\nPffa3UBipxezgwjGfSfp7z02HZike5bKBSIa6sGWxoDfejLyz2lkRDGpdv0vtJdt\nC9b2xqIZ2jq0UD1Vn6aWGEE+y0mvp1QEWTRK5vF6bI/QGNwRuFIGSi1Sb8KVraFW\nIgw4RsS+B5aJlZqE8leHhjO1l5NJkWEh/uwwUKFs+dpWV/9SoBKrDTyPDBt0ZvF5\nYo8Xs5PxVIoEr38JysLZpJ6AWXXLIQN3mdGBd4Wm73o5MW39vObzgsJhgZ4+0jjV\ntWVUL+KpV3mSLaUxpjGa1Fz5qKXyqRfWwSSx3DcCAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCB4AwDQYJ\nKoZIhvcNAQELBQADggEBAHhffK+9nIRkRjuuiie8Eht+OpsOj3BdybN1b//rvLkh\nQtjInKHCWk/PGF5gJoWKIPhAXTGmzh9VBZRMWccBpeh55tC8ZYUu6Pb3V6zTH8vl\nF0VbAEP1mRfkB1x92PugSx7//TPHT1fQ/sKkydWKdTKw4u6DSO3uubm2yZde8OBr\n4JWPFRZKShprumQEM2ZlvbddusOasEiK9u4tbq2XM0ySoxxJix4QlTtU1NQ0noIU\nrYksonUE/hOgl3N1rU2Z7Hx04Ig6XzTBltYv4JVCg5nslbsJE6XjcrzhBaIo+Pe6\n60rol7aw3BYuk1pZVouz4xgRjVfeVQVV48Wk+JTbA1Q=\n-----END CERTIFICATE-----\n', + '-WZpKQ': + '-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIEN2AQGjANBgkqhkiG9w0BAQsFADAzMQ8wDQYDVQQDEwZH\naXRraXQxEzARBgNVBAoTCkdvb2dsZSBJbmMxCzAJBgNVBAYTAlVTMB4XDTI0MTAz\nMTAxMTcwOFoXDTI1MTAyNjAxMTcwOFowMzEPMA0GA1UEAxMGR2l0a2l0MRMwEQYD\nVQQKEwpHb29nbGUgSW5jMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBANfahbn31sd4bntOODPKabKHU/eeqMiXOsaiai5JvIDQS5oL\nbWSkjs0L5A04kzPaETpRQCYXzEF2Ntad96fVpESAUhXD6DUuJarlAOOQyvF0FFtC\noWwfaqnDFbkB9n8v6sK9K9XcmTInp+FocJ5T+JOGGeZNQp6Bvfz6Yejwrg2kCamo\nXj0W7WeMThJURvd0k3ntxyJtpyoH43Ljci7+ZBhgtN3HyewNruqqFTQFfxzDjPok\n1pGDW8YxeQAZVlegg9hl/UBo1yja+rSJYP9T5XrAXgMBAEyicIRORMAPi0nRahO0\nQCLOtjLMEfc/JM6s5MR4G6LPOyp3SMk1Xbmn0vcCAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCB4AwDQYJ\nKoZIhvcNAQELBQADggEBAHHi7pUulvBqERPkntC3NrXW/Ceo+KKMUB4NwB3kpV0D\n5Nwa2yA6B3Q3Sr1yBLhVcxNR+sXct6YbfXQ2mF5wq/JGAxkPqXH7lmPGDULWKwRt\nY82aTNfT71tkx5NAcXSNwrf0GN6WPkjZ038BKUSC99McJI32f4iGKFei+tNWOd3H\nypYkm4tkXMbEISNb5KfLnoHYWryoFoPR1ZOrnmohv82S3kXt37fkB24eCJC46Pxy\nqF/e/SyJEQHY7RAW4rMfs8HPWJZRUmIhzxKa3PKmrcORvS97al9qY5KbKwlCfWIP\nHdPy1htgk4Jt8yOkQe6YnFUOWOx9Yxj6dCxV1BfNYg8=\n-----END CERTIFICATE-----\n', +}; + +const sessionCookiePublicKeys: Record = { + 'bZ-_5g': { + kty: 'RSA', + alg: 'RS256', + use: 'sig', + kid: 'bZ-_5g', + n: '3JUC6SNQ1oD3Pnk8IrcjzTcA9_uDmj7H6Hz2k6EVB14dl34AxqZ1u0uxuaB9nSXFNogBtoR1Zb1ksVMtuw70XxVzfyfSCjF2eeBLzl3Wy2KHRaZjanTioCS7IWmNUwYuhO1x6n2uC5z7RS4NMHJrQ-wSyW0TQOC3wdUaXxkWcXhA1jhwgWX8lMXQk-kbEKPzTUv9lr4oyd6eLVsw_F2fs1OGhVvDf0xWp0hhOOdbEWcOdnlRIsjE6EUe0Jp_qRDDe42TKSJmIz3RfMAMRvdFTW5KETB65KH9bz3S2cby5c57fj8xVJG_BxsPiwm8wLgQqxjPMniY3-LgYLmAd9KPIw', + e: 'AQAB', + }, + _aLBDQ: { + kty: 'RSA', + alg: 'RS256', + use: 'sig', + kid: '_aLBDQ', + n: 'wjQf1KM9XbqyKlGM9OXodHpDyk5D_oax9HYc4tPluSlzRyrw0TRepYPF_SD-aFVvc7lv-M3irctYVpV4hKyFwkg0TvMof7MomFjBHwvv35-PM02gTDHgURJ6lNQdVbuoLj1TWhpzfSgwPhzlkK4Jns5OBugBK7Yw5y1H-nz5HqOy90t9KmDgIorLwCVRP6WK5em_g5qm2mnqa6tOaQMSpUmgskoymfTKJM9qUCHwLnXgpfHdLGE8A6bmmWxXFgYFhndcE5oH5S7TFmLkc1idImaj1OKEX9Fa8TabMTggTxUDZghkE1y1y4KTY2j-v1b_aBMXp7ZhuuN--QPRWD6i-Q', + e: 'AQAB', + }, + usAeNA: { + kty: 'RSA', + alg: 'RS256', + use: 'sig', + kid: 'usAeNA', + n: 'wlcX1joNvfIgqNcNa8Cl34hMlVab2NDa6FwPtNoy1iyPCLB3hNERNJlL88L_TWWbuyZBu0FQTN7Az10YW25keKynogU-xtFpgdjBTidWfrRgxiz2JDl6mmmoJ-3-tUsF7mwFQRIhL2y7ki4J5MI-DMaJwWqmmZgGGI9IjtPvO0GhJJeDx4tqNlv38aqmN1CrpsshsMtewvi6swOySd6HjjtRaorIqvaaRlWw_GOc58KMdvLmR8my7MXrB3KFFcFndITv3X5GMYD9rTtxoMpBcMN_CQn4iWD-p1wPJvMaDozL8P8riKY7LGaEsoovU0D1FQb9YMayJKjMp6XPWO4a3w', + e: 'AQAB', + }, + KRSnmg: { + kty: 'RSA', + alg: 'RS256', + use: 'sig', + kid: 'KRSnmg', + n: 'w3laAnsRBuEMXxSq5rCmlcG32C-NsvqCd9VdpCLkq-tKn48999rdQGKnF7ODCMZ9J-nvPTYdmKR7lsoFIhrqwZbGgN96MvLPaWREMal2_S-0l20L1vbGohnaOrRQPVWfppYYQT7LSa-nVARZNErm8Xpsj9AY3BG4UgZKLVJvwpWtoVYiDDhGxL4HlomVmoTyV4eGM7WXk0mRYSH-7DBQoWz52lZX_1KgEqsNPI8MG3Rm8Xlijxezk_FUigSvfwnKwtmknoBZdcshA3eZ0YF3habvejkxbf285vOCwmGBnj7SONW1ZVQv4qlXeZItpTGmMZrUXPmopfKpF9bBJLHcNw', + e: 'AQAB', + }, + '-WZpKQ': { + kty: 'RSA', + alg: 'RS256', + use: 'sig', + kid: '-WZpKQ', + n: '19qFuffWx3hue044M8ppsodT956oyJc6xqJqLkm8gNBLmgttZKSOzQvkDTiTM9oROlFAJhfMQXY21p33p9WkRIBSFcPoNS4lquUA45DK8XQUW0KhbB9qqcMVuQH2fy_qwr0r1dyZMien4WhwnlP4k4YZ5k1CnoG9_Pph6PCuDaQJqahePRbtZ4xOElRG93STee3HIm2nKgfjcuNyLv5kGGC03cfJ7A2u6qoVNAV_HMOM-iTWkYNbxjF5ABlWV6CD2GX9QGjXKNr6tIlg_1PlesBeAwEATKJwhE5EwA-LSdFqE7RAIs62MswR9z8kzqzkxHgbos87KndIyTVduafS9w', + e: 'AQAB', + }, +}; + +describe('jwkFromX509', () => { + const testCases = Object.entries(relyingpartyPublicKeys).map(([kid, x509]) => ({ + kid, + x509, + expectedJwk: sessionCookiePublicKeys[kid], + })); + + testCases.forEach(({ kid, x509, expectedJwk }) => { + it(`should convert x509 certificate to JWK for kid: ${kid}`, async () => { + const jwk = await jwkFromX509(kid, x509); + expect(jwk).toEqual(expectedJwk); + }); + }); +}); From 43802530646681b6e4a7d07724eca074d8e4ae00 Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Mon, 30 Dec 2024 00:18:13 +0900 Subject: [PATCH 2/2] updated examples --- example/index.ts | 6 +++--- example/wrangler.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/index.ts b/example/index.ts index 84ed247..37b2eb7 100644 --- a/example/index.ts +++ b/example/index.ts @@ -72,7 +72,7 @@ app.get('/admin/login', async c => {