From 93bda45ae797120efdac9a86786c8f5ce4ec58ee Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 15:02:09 +0200 Subject: [PATCH 01/12] fix: restore fragment decoder links Fixes: https://github.com/jsonwebtoken/jsonwebtoken.github.io/issues/880 Fixes: https://github.com/jsonwebtoken/jsonwebtoken.github.io/issues/942 Signed-off-by: Filip Skokan --- e2e/decoder-fragment-links.spec.ts | 251 +++++++++++++++ .../components/token-decoder.component.tsx | 49 +-- .../services/decoder-fragment.service.ts | 107 +++++++ .../decoder/services/decoder.store.ts | 80 ++++- .../decoder/services/token-decoder.service.ts | 47 +++ tests/decoder-fragment.service.test.ts | 214 +++++++++++++ tests/decoder.store.test.ts | 303 ++++++++++++++++++ 7 files changed, 1009 insertions(+), 42 deletions(-) create mode 100644 e2e/decoder-fragment-links.spec.ts create mode 100644 src/features/decoder/services/decoder-fragment.service.ts create mode 100644 tests/decoder-fragment.service.test.ts create mode 100644 tests/decoder.store.test.ts diff --git a/e2e/decoder-fragment-links.spec.ts b/e2e/decoder-fragment-links.spec.ts new file mode 100644 index 000000000..fb1f9d029 --- /dev/null +++ b/e2e/decoder-fragment-links.spec.ts @@ -0,0 +1,251 @@ +import { expect, test, type Page } from "@playwright/test"; +import { dataTestidDictionary } from "@/libs/testing/data-testid.dictionary"; +import type { + JwtDictionaryModel, + JwtSignedWithDigitalModel, + UnsecuredJwtModel, +} from "./e2e.models"; +import jwts from "./jwt.json" with { type: "json" }; + +type FragmentEntry = readonly [name: string, value: string]; +type PublicKeyFormat = "JWK" | "PEM"; + +const E2E_BASE_URL = "http://localhost:1234"; +const TestJwts = (jwts as JwtDictionaryModel).byAlgorithm; +const unsecuredJwt = (TestJwts.none as UnsecuredJwtModel).jwt; +const ps256Jwt = TestJwts.PS256 as JwtSignedWithDigitalModel; + +// Regression fixture from https://github.com/jsonwebtoken/jsonwebtoken.github.io/issues/942 +const ISSUE_942_TOKEN = + "eyJraWQiOiIyNTkzYzg0YzMyYzhlMzFlNDY2YjZlOGZiOGJjNzQ0MGE4MTU5MDFjYjFhNjU4NjkwM2Y1ZDIwOWRiODUyYjgyIiwiYWxnIjoiUFMyNTYifQ.eyJzdWIiOiJKZExrU1dHY2YwbWZIeE16Wk5vdEgiLCJhdWQiOiJodHRwczovL210bHMtYXMub3BlbmZpbmFuY2UtZGV2LnFpdGVjaC5hcHAvdG9rZW4iLCJuYmYiOjE3Nzc5Mzg0NTEsImlzcyI6IkpkTGtTV0djZjBtZkh4TXpaTm90SCIsImV4cCI6MTc3NzkzODUxMSwiaWF0IjoxNzc3OTM4NDUxLCJqdGkiOiJhMERyeFJNMHJZRVB1NDg4Y1lCNSJ9.AF-dA2khA6eKzWYD61emzXsAi_-EZ3iy-sIoDj6JB32iM1a89_7iljYFWQRohAWkxlkkQTD4zOVu-uy878M3BBKradLYeSjZ07izGsvJ3F9nz4EdRAkEsy00gOMMpvgi9dFUfNGGoeVMPX8o6ymc1epCqOZBSBleT7khHnYuL6TxYrPz4dPkTpPvNdxEc-1pMJx-60khjlBRdoz0ZPq3iDfKr4tEDKNIjUvoFsT0i7DFLnVlUTYp8lAnQGpTMyqJceRzG9oMfrfUwn73aIUVg4ccMJyNQTc0aYTVYEjHD1TN-YtbqPmXzjyA_35HV4fWACa3x7blCmnx3JuoHPXZog"; +const ISSUE_942_PUBLIC_KEY = JSON.stringify({ + kty: "RSA", + e: "AQAB", + use: "sig", + kid: "2593c84c32c8e31e466b6e8fb8bc7440a815901cb1a6586903f5d209db852b82", + alg: "PS256", + n: "vf-KwU0bhvI0XaBvLd3vs3GFRv2NA4GMfpjr-2wtNfNiTVedxhTJpC5vlG601k0CyS3zMjnfSVLorN-bXhiyWnYzK6ZDRkZn1tFriWPCu39gdHN7DWLQ_GtlacgsEQDyPf8vhHn8_Xj19ZAE08PPnFR51mVfNwkSpAQNAm0wjaTBL2FVBOHK_vcGZ-svlc8gXK-NfhGBZRP9agmWZs5qPwLV1fpPWsul38lU6S1KbE92aKT00O8DxNS8VdiEx9qJ6Msjy7cP8xOG-EfzFT9XGpZIHma_5M8OpgIfkhrJvkNCQz1-i-uwI2eMw-qppZgDJZ7a5wTOamOAVI0sdTrFCw", +}); + +const createFragment = (entries: readonly FragmentEntry[], legacy = false) => { + const params = new URLSearchParams(); + + for (const [name, value] of entries) { + params.append(name, value); + } + + return `#${legacy ? "debugger-io?" : ""}${params.toString()}`; +}; + +const gotoFragment = async ( + page: Page, + entries: readonly FragmentEntry[], + legacy = false, +) => { + await page.goto(`${E2E_BASE_URL}/${createFragment(entries, legacy)}`); +}; + +const expectTokenLoaded = async (page: Page, token: string) => { + await expect( + page + .getByTestId(dataTestidDictionary.decoder.jwtEditor.id) + .getByRole("textbox"), + ).toHaveValue(token); +}; + +const expectPublicKeyLoaded = async ({ + page, + publicKey, + publicKeyFormat, +}: { + page: Page; + publicKey: string; + publicKeyFormat: PublicKeyFormat; +}) => { + const jwtEditor = page.getByTestId(dataTestidDictionary.decoder.jwtEditor.id); + const publicKeyEditor = page.getByTestId( + dataTestidDictionary.decoder.secretKeyEditor.id, + ); + const publicKeyRegion = page + .getByRole("region") + .filter({ has: publicKeyEditor }); + + await expect(publicKeyEditor.getByRole("textbox")).toHaveValue(publicKey); + await expect( + publicKeyEditor.getByTestId( + dataTestidDictionary.decoder.secretKeyEditor.statusBar.success.id, + ), + ).toBeVisible(); + await expect( + publicKeyRegion.getByText(publicKeyFormat, { exact: true }), + ).toBeVisible(); + await expect( + jwtEditor.getByTestId( + dataTestidDictionary.decoder.jwtEditor.notificationBar.success.id, + ), + ).toBeVisible(); + await expect( + page.getByText(/Unable to retrieve public key from issuer/), + ).toHaveCount(0); +}; + +const expectLegacyFragmentNormalized = async ({ + page, + token, + publicKey, + unrelatedValue, +}: { + page: Page; + token: string; + publicKey: string; + unrelatedValue?: string; +}) => { + await expect + .poll(() => page.evaluate(() => window.location.hash)) + .not.toContain("debugger-io?"); + + const hash = await page.evaluate(() => window.location.hash); + const params = new URLSearchParams(hash.slice(1)); + + expect(params.get("token")).toBe(token); + expect(params.get("publicKey")).toBe(publicKey); + + if (unrelatedValue !== undefined) { + expect(params.get("context")).toBe(unrelatedValue); + } +}; + +test.describe("decoder fragment token aliases", () => { + for (const alias of ["id_token", "access_token", "value", "token"]) { + test(`loads a JWT from #${alias}`, async ({ page }) => { + await gotoFragment(page, [[alias, unsecuredJwt]]); + + await expectTokenLoaded(page, unsecuredJwt); + }); + } + + test("loads a token after unrelated fragment parameters", async ({ + page, + }) => { + await gotoFragment(page, [ + ["context", "preserved"], + ["token", unsecuredJwt], + ]); + + await expectTokenLoaded(page, unsecuredJwt); + expect(new URL(page.url()).hash).toContain("context=preserved&token="); + }); + + test("uses the historical alias priority deterministically", async ({ + page, + }) => { + const createUnsecuredJwt = (source: string) => + `${Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url")}.${Buffer.from(JSON.stringify({ source })).toString("base64url")}.`; + const tokens = { + token: createUnsecuredJwt("token"), + value: createUnsecuredJwt("value"), + accessToken: createUnsecuredJwt("access_token"), + idToken: createUnsecuredJwt("id_token"), + }; + + await gotoFragment(page, [ + ["token", tokens.token], + ["value", tokens.value], + ["access_token", tokens.accessToken], + ["id_token", tokens.idToken], + ]); + + await expectTokenLoaded(page, tokens.idToken); + }); +}); + +test.describe("decoder fragment public keys", () => { + const cases = [ + { + syntax: "canonical", + keyFormat: "PEM", + fixture: ps256Jwt.withPemKey, + legacy: false, + }, + { + syntax: "canonical", + keyFormat: "JWK", + fixture: ps256Jwt.withJwkKey, + legacy: false, + }, + { + syntax: "legacy", + keyFormat: "PEM", + fixture: ps256Jwt.withPemKey, + legacy: true, + }, + { + syntax: "legacy", + keyFormat: "JWK", + fixture: ps256Jwt.withJwkKey, + legacy: true, + }, + ] as const; + + for (const { syntax, keyFormat, fixture, legacy } of cases) { + test(`loads and verifies a ${keyFormat} key from a ${syntax} fragment`, async ({ + page, + }) => { + const publicKey = + typeof fixture.publicKey === "string" + ? fixture.publicKey + : JSON.stringify(fixture.publicKey); + const entries: FragmentEntry[] = [ + ["token", fixture.jwt], + ["publicKey", publicKey], + ]; + + if (legacy) { + entries.push(["context", "preserved"]); + } + + await gotoFragment(page, entries, legacy); + + await expectTokenLoaded(page, fixture.jwt); + await expectPublicKeyLoaded({ + page, + publicKey, + publicKeyFormat: keyFormat, + }); + + if (legacy) { + await expectLegacyFragmentNormalized({ + page, + token: fixture.jwt, + publicKey, + unrelatedValue: "preserved", + }); + } + }); + } + + test("loads and verifies the exact legacy fragment from issue #942", async ({ + page, + }) => { + await gotoFragment( + page, + [ + ["token", ISSUE_942_TOKEN], + ["publicKey", ISSUE_942_PUBLIC_KEY], + ], + true, + ); + + await expectTokenLoaded(page, ISSUE_942_TOKEN); + await expectPublicKeyLoaded({ + page, + publicKey: ISSUE_942_PUBLIC_KEY, + publicKeyFormat: "JWK", + }); + await expectLegacyFragmentNormalized({ + page, + token: ISSUE_942_TOKEN, + publicKey: ISSUE_942_PUBLIC_KEY, + }); + }); +}); diff --git a/src/features/decoder/components/token-decoder.component.tsx b/src/features/decoder/components/token-decoder.component.tsx index 27faea8e3..3dc95fe01 100644 --- a/src/features/decoder/components/token-decoder.component.tsx +++ b/src/features/decoder/components/token-decoder.component.tsx @@ -5,10 +5,10 @@ import { DecodedHeaderOutputComponent } from "@/features/decoder/components/deco import { DecodedPayloadOutputComponent } from "@/features/decoder/components/decoded-payload-output.component"; import { JwtInputComponent } from "@/features/decoder/components/jwt-input.component"; import { SecretKeyInputComponent } from "@/features/decoder/components/secret-key-input.component"; +import { parseDecoderFragment } from "@/features/decoder/services/decoder-fragment.service"; import { useDecoderStore } from "@/features/decoder/services/decoder.store"; import { useRouter } from "next/navigation"; import { - SupportedTokenHashParamValues, WARNING_PARAM_KEY, WARNING_PARAM_VALUE, } from "@/libs/config/project.constants"; @@ -49,48 +49,29 @@ export const TokenDecoderComponent: React.FC = ({ const decoderInputs$ = useDebuggerStore((state) => state.decoderInputs$); const loadDecoderInputs = useDecoderStore((state) => state.loadDecoderInputs); - const handleJwtChange$ = useDecoderStore((state) => state.handleJwtChange); + const loadDecoderUrlInputs = useDecoderStore( + (state) => state.loadDecoderUrlInputs, + ); const showUseHashWarning$ = useDecoderStore( - (state) => state.showUseHashWarning + (state) => state.showUseHashWarning, ); useEffect(() => { - const handleHashChange = () => { - const hash = window.location.hash.substring(1); - - if (hash.includes("debugger-io?token=")) { - const debugHash = window.location.hash - .substring(1) - .replace("debugger-io?", ""); - const debugHashParams = new URLSearchParams(debugHash); - const token = debugHashParams.get(SupportedTokenHashParamValues.TOKEN); - - const currentUrl = window.location.href.split("#")[0]; - - if (!(currentUrl && token)) { - return; - } - - const newUrl = `${currentUrl}#${SupportedTokenHashParamValues.TOKEN}=${token}`; - - window.location.replace(newUrl); + const handleHashChange = async () => { + const { jwt, publicKey, publicKeyFormat, normalizedHash } = + parseDecoderFragment(window.location.hash); + if (!jwt) { return; } - if (!hash.includes("=")) { - return; + if (normalizedHash) { + const url = new URL(window.location.href); + url.hash = normalizedHash; + window.history.replaceState(window.history.state, "", url.toString()); } - const hashParams = new URLSearchParams(hash); - - Object.values(SupportedTokenHashParamValues).forEach((hashParamKey) => { - const token = hashParams.get(hashParamKey); - - if (token) { - handleJwtChange$(token); - } - }); + await loadDecoderUrlInputs({ jwt, publicKey, publicKeyFormat }); }; const handleWarning = () => { @@ -128,7 +109,7 @@ export const TokenDecoderComponent: React.FC = ({ return () => { window.removeEventListener("hashchange", handleHashChange); }; - }, [handleJwtChange$, router, showUseHashWarning$]); + }, [loadDecoderUrlInputs, router, showUseHashWarning$]); useEffect(() => { if (isMounted.current) { diff --git a/src/features/decoder/services/decoder-fragment.service.ts b/src/features/decoder/services/decoder-fragment.service.ts new file mode 100644 index 000000000..5cf554c1e --- /dev/null +++ b/src/features/decoder/services/decoder-fragment.service.ts @@ -0,0 +1,107 @@ +import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; + +const TOKEN_PARAM_PRIORITY = [ + "id_token", + "access_token", + "value", + "token", +] as const; +const LEGACY_TOKEN_PARAM = "debugger-io?token"; +const PUBLIC_KEY_PARAM = "publicKey"; + +const TOKEN_PARAMS = new Set([ + ...TOKEN_PARAM_PRIORITY, + LEGACY_TOKEN_PARAM, +]); + +export type DecoderFragmentResult = { + jwt?: string; + publicKey?: string; + publicKeyFormat?: AsymmetricKeyFormatValues; + normalizedHash?: string; +}; + +const inferPublicKeyFormat = (publicKey: string): AsymmetricKeyFormatValues => { + try { + const parsedPublicKey: unknown = JSON.parse(publicKey); + + if ( + parsedPublicKey !== null && + typeof parsedPublicKey === "object" && + !Array.isArray(parsedPublicKey) + ) { + return AsymmetricKeyFormatValues.JWK; + } + } catch { + // Non-JSON public keys are handled as PEM. + } + + return AsymmetricKeyFormatValues.PEM; +}; + +const normalizeLegacyFragment = ( + params: URLSearchParams, + jwt: string, + publicKey?: string, +): string => { + const normalizedParams = new URLSearchParams(); + + normalizedParams.set("token", jwt); + + if (publicKey) { + normalizedParams.set(PUBLIC_KEY_PARAM, publicKey); + } + + params.forEach((value, key) => { + if (TOKEN_PARAMS.has(key) || key === PUBLIC_KEY_PARAM) { + return; + } + + normalizedParams.append(key, value); + }); + + return `#${normalizedParams.toString()}`; +}; + +export const parseDecoderFragment = (hash: string): DecoderFragmentResult => { + if (!hash.startsWith("#")) { + return {}; + } + + const params = new URLSearchParams(hash.slice(1)); + let jwt: string | undefined; + + for (const tokenParam of TOKEN_PARAM_PRIORITY) { + const candidate = params.get(tokenParam); + + if (candidate) { + jwt = candidate; + break; + } + } + + const legacyJwt = params.get(LEGACY_TOKEN_PARAM) || undefined; + const usesLegacySyntax = jwt === undefined && legacyJwt !== undefined; + + if (usesLegacySyntax) { + jwt = legacyJwt; + } + + const publicKey = params.get(PUBLIC_KEY_PARAM) || undefined; + if (!jwt) { + return {}; + } + + const result: DecoderFragmentResult = { jwt }; + + if (publicKey) { + result.publicKey = publicKey; + result.publicKeyFormat = inferPublicKeyFormat(publicKey); + } + + if (usesLegacySyntax) { + result.normalizedHash = normalizeLegacyFragment(params, jwt, publicKey); + } + + return result; +}; diff --git a/src/features/decoder/services/decoder.store.ts b/src/features/decoder/services/decoder.store.ts index 9eb5da529..fa0993f35 100644 --- a/src/features/decoder/services/decoder.store.ts +++ b/src/features/decoder/services/decoder.store.ts @@ -39,6 +39,8 @@ export const DEFAULT_PAYLOAD = { export const DEFAULT_DECODED_PAYLOAD = JSON.stringify(DEFAULT_PAYLOAD, null, 2); +let latestJwtChangeRequestId = 0; + export type DecoderStoreState = { jwt: string; alg: string; @@ -67,21 +69,26 @@ export type DecoderStoreState = { }; type DecoderStoreActions = { - selectDecodingExample: (algorithm: string) => void; - handleJwtChange: (newToken: string) => void; + selectDecodingExample: (algorithm: string) => Promise; + handleJwtChange: (newToken: string) => Promise; + loadDecoderUrlInputs: (params: { + jwt: string; + publicKey?: string; + publicKeyFormat?: AsymmetricKeyFormatValues; + }) => Promise; handleSymmetricSecretKeyChange: (newSymmetricSecretKey: string) => void; handleSymmetricSecretKeyEncodingChange: ( - newSymmetricSecretKey: EncodingValues + newSymmetricSecretKey: EncodingValues, ) => void; handleAsymmetricPublicKeyChange: (newAsymmetricPublicKey: string) => void; handleAsymmetricPublicKeyFormatChange: ( - newFormat: AsymmetricKeyFormatValues + newFormat: AsymmetricKeyFormatValues, ) => void; resetControlledSymmetricSecretKey: () => void; resetControlledAsymmetricPublicKey: () => void; showUseHashWarning: () => void; hideUseHashWarning: () => void; - loadDecoderInputs: (params: DecoderInputsModel) => void; + loadDecoderInputs: (params: DecoderInputsModel) => Promise; }; export const initialState: DecoderStoreState = { @@ -109,11 +116,20 @@ export const useDecoderStore = create()( subscribeWithSelector((set, get) => ({ ...initialState, selectDecodingExample: async (algorithm) => { + const requestId = ++latestJwtChangeRequestId; const update = await TokenDecoderService.selectDecodingExample(algorithm); - set(update); + if (requestId !== latestJwtChangeRequestId) { + return; + } + + set({ + ...update, + isLoading: false, + }); }, handleJwtChange: async (newToken) => { + const requestId = ++latestJwtChangeRequestId; const { alg, symmetricSecretKey, @@ -137,6 +153,46 @@ export const useDecoderStore = create()( newToken, }); + if (requestId !== latestJwtChangeRequestId) { + return; + } + + set({ + ...update, + isLoading: false, + }); + }, + loadDecoderUrlInputs: async ({ jwt, publicKey, publicKeyFormat }) => { + const requestId = ++latestJwtChangeRequestId; + const { + alg, + symmetricSecretKey, + symmetricSecretKeyEncoding, + asymmetricPublicKey, + asymmetricPublicKeyFormat, + } = get(); + + set({ + isLoading: true, + decodedHeader: "", + decodedPayload: "", + }); + + const update = await TokenDecoderService.loadDecoderUrlInputs({ + alg, + symmetricSecretKey, + symmetricSecretKeyEncoding, + asymmetricPublicKey, + asymmetricPublicKeyFormat, + jwt, + publicKey, + publicKeyFormat, + }); + + if (requestId !== latestJwtChangeRequestId) { + return; + } + set({ ...update, isLoading: false, @@ -215,9 +271,17 @@ export const useDecoderStore = create()( }); }, loadDecoderInputs: async (params) => { + const requestId = ++latestJwtChangeRequestId; const update = await TokenDecoderService.loadDecoderInputs(params); - set(update); + if (requestId !== latestJwtChangeRequestId) { + return; + } + + set({ + ...update, + isLoading: false, + }); }, - })) + })), ); diff --git a/src/features/decoder/services/token-decoder.service.ts b/src/features/decoder/services/token-decoder.service.ts index d04b58d6a..c258c8a41 100644 --- a/src/features/decoder/services/token-decoder.service.ts +++ b/src/features/decoder/services/token-decoder.service.ts @@ -343,6 +343,53 @@ class _TokenDecoderService { return stateUpdate; } + async loadDecoderUrlInputs({ + alg, + symmetricSecretKey, + symmetricSecretKeyEncoding, + asymmetricPublicKey, + asymmetricPublicKeyFormat, + jwt, + publicKey, + publicKeyFormat, + }: { + alg: string; + symmetricSecretKey: string; + symmetricSecretKeyEncoding: EncodingValues; + asymmetricPublicKey: string; + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues; + jwt: string; + publicKey?: string; + publicKeyFormat?: AsymmetricKeyFormatValues; + }): Promise> { + const cleanJwtInput = extractJwt(jwt); + const validateJwtFormatResult = validateJwtFormat(cleanJwtInput); + + if ( + publicKey?.trim() && + publicKeyFormat && + validateJwtFormatResult.isOk() && + validateJwtFormatResult.value.type === JwtTypeValues.DigitallySigned + ) { + return this.loadDecoderInputs({ + algType: SigningAlgCategoryValues.ASYMMETRIC, + alg: validateJwtFormatResult.value.signingAlgorithm, + jwt: cleanJwtInput, + asymmetricPublicKey: publicKey, + asymmetricPublicKeyFormat: publicKeyFormat, + }); + } + + return this.handleJwtChange({ + alg, + symmetricSecretKey, + symmetricSecretKeyEncoding, + asymmetricPublicKey, + asymmetricPublicKeyFormat, + newToken: jwt, + }); + } + async handleJwtChange({ alg, symmetricSecretKey, diff --git a/tests/decoder-fragment.service.test.ts b/tests/decoder-fragment.service.test.ts new file mode 100644 index 000000000..5a644a557 --- /dev/null +++ b/tests/decoder-fragment.service.test.ts @@ -0,0 +1,214 @@ +import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; +import { parseDecoderFragment } from "@/features/decoder/services/decoder-fragment.service"; +import { describe, expect, test } from "vitest"; + +describe("parseDecoderFragment", () => { + test.each(["token", "id_token", "access_token", "value"])( + "parses the %s token alias", + (tokenParam) => { + expect( + parseDecoderFragment(`#${tokenParam}=header.payload.signature`), + ).toEqual({ + jwt: "header.payload.signature", + }); + }, + ); + + test("parses a token after unrelated fragment parameters", () => { + expect( + parseDecoderFragment( + "#foo=bar&returnTo=%2Faccount&token=header.payload.signature", + ), + ).toEqual({ + jwt: "header.payload.signature", + }); + }); + + test("uses the historical token alias priority", () => { + expect( + parseDecoderFragment( + "#token=token&value=value&access_token=access&id_token=id", + ), + ).toEqual({ + jwt: "id", + }); + }); + + test("skips an empty higher-priority alias", () => { + expect( + parseDecoderFragment("#id_token=&access_token=access&token=token"), + ).toEqual({ + jwt: "access", + }); + }); + + test("uses the first repeated token value", () => { + expect(parseDecoderFragment("#token=first&token=second")).toEqual({ + jwt: "first", + }); + }); + + test("does not fall through to a later repeated value when the first is empty", () => { + expect(parseDecoderFragment("#token=&token=second")).toEqual({}); + }); + + test("decodes encoded reserved characters", () => { + expect( + parseDecoderFragment( + "#token=header.payload.signature&publicKey=line%201%0Aline%2B2%26line%3D3", + ), + ).toEqual({ + jwt: "header.payload.signature", + publicKey: "line 1\nline+2&line=3", + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }); + }); + + test("infers JWK for a valid non-array JSON object", () => { + const publicKey = JSON.stringify({ + kty: "RSA", + e: "AQAB", + n: "example", + }); + const params = new URLSearchParams({ + token: "header.payload.signature", + publicKey, + }); + + expect(parseDecoderFragment(`#${params.toString()}`)).toEqual({ + jwt: "header.payload.signature", + publicKey, + publicKeyFormat: AsymmetricKeyFormatValues.JWK, + }); + }); + + test.each([ + ["an invalid JSON value", "{invalid"], + ["a JSON array", "[]"], + ["JSON null", "null"], + ["a JSON string", '"key"'], + ])("infers PEM for %s", (_description, publicKey) => { + const params = new URLSearchParams({ + token: "header.payload.signature", + publicKey, + }); + + expect(parseDecoderFragment(`#${params.toString()}`)).toEqual({ + jwt: "header.payload.signature", + publicKey, + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }); + }); + + test("uses the first repeated public key value", () => { + expect( + parseDecoderFragment("#token=jwt&publicKey=first&publicKey=second"), + ).toEqual({ + jwt: "jwt", + publicKey: "first", + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }); + }); + + test("does not fall through to a later repeated public key when the first is empty", () => { + expect( + parseDecoderFragment("#token=jwt&publicKey=&publicKey=second"), + ).toEqual({ + jwt: "jwt", + }); + }); + + test("treats the publicKey parameter name as case-sensitive", () => { + expect(parseDecoderFragment("#token=jwt&publickey=ignored")).toEqual({ + jwt: "jwt", + }); + }); + + test("parses and normalizes a legacy fragment with a PEM public key", () => { + const publicKey = + "-----BEGIN PUBLIC KEY-----\nline+one/line=two\n-----END PUBLIC KEY-----"; + const params = new URLSearchParams({ + "debugger-io?token": "header.payload.signature", + publicKey, + foo: "bar", + }); + const result = parseDecoderFragment(`#${params.toString()}`); + + expect(result).toEqual({ + jwt: "header.payload.signature", + publicKey, + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + normalizedHash: `#${new URLSearchParams({ + token: "header.payload.signature", + publicKey, + foo: "bar", + }).toString()}`, + }); + }); + + test("parses and normalizes a legacy fragment with a JWK", () => { + const publicKey = JSON.stringify({ + kty: "EC", + crv: "P-256", + x: "example-x", + y: "example-y", + }); + const params = new URLSearchParams({ + "debugger-io?token": "header.payload.signature", + publicKey, + }); + const normalizedParams = new URLSearchParams({ + token: "header.payload.signature", + publicKey, + }); + + expect(parseDecoderFragment(`#${params.toString()}`)).toEqual({ + jwt: "header.payload.signature", + publicKey, + publicKeyFormat: AsymmetricKeyFormatValues.JWK, + normalizedHash: `#${normalizedParams.toString()}`, + }); + }); + + test("preserves repeated unrelated parameters when normalizing legacy syntax", () => { + expect( + parseDecoderFragment( + "#foo=first&debugger-io?token=jwt&foo=second&publicKey=key", + ), + ).toEqual({ + jwt: "jwt", + publicKey: "key", + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + normalizedHash: "#token=jwt&publicKey=key&foo=first&foo=second", + }); + }); + + test("prefers a regular alias over legacy syntax without normalizing", () => { + expect( + parseDecoderFragment("#debugger-io?token=legacy&value=regular"), + ).toEqual({ + jwt: "regular", + }); + }); + + test("does not normalize regular fragment syntax", () => { + expect(parseDecoderFragment("#token=jwt")).toEqual({ + jwt: "jwt", + }); + }); + + test.each([ + "", + "#", + "#foo=bar", + "#publicKey=key", + "#token=", + "#debugger-io?token=", + ])("returns no decoder inputs for %j", (hash) => { + expect(parseDecoderFragment(hash)).toEqual({}); + }); + + test("does not parse query-string input", () => { + expect(parseDecoderFragment("?token=jwt")).toEqual({}); + }); +}); diff --git a/tests/decoder.store.test.ts b/tests/decoder.store.test.ts new file mode 100644 index 000000000..111360a2d --- /dev/null +++ b/tests/decoder.store.test.ts @@ -0,0 +1,303 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { + DefaultTokensValues, + DefaultTokenWithKeysModel, +} from "@/features/common/values/default-tokens.values"; +import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; +import { JwtSignatureStatusValues } from "@/features/common/values/jwt-signature-status.values"; +import { SigningAlgCategoryValues } from "@/features/common/values/signing-alg-category.values"; +import { + DecoderStoreState, + initialState, + useDecoderStore, +} from "@/features/decoder/services/decoder.store"; +import { TokenDecoderService } from "@/features/decoder/services/token-decoder.service"; + +const rs256 = DefaultTokensValues.RS256 as DefaultTokenWithKeysModel; + +const deferred = () => { + let resolve!: (value: T) => void; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + + return { promise, resolve }; +}; + +describe("loadDecoderUrlInputs", () => { + beforeEach(() => { + vi.restoreAllMocks(); + useDecoderStore.setState(initialState); + }); + + test("loads a digitally signed JWT and explicit key together", async () => { + const publicKey = JSON.stringify(rs256.jwk); + const loadDecoderInputs = vi + .spyOn(TokenDecoderService, "loadDecoderInputs") + .mockResolvedValue({ + jwt: rs256.token, + alg: "RS256", + asymmetricPublicKey: publicKey, + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.JWK, + controlledAsymmetricPublicKey: { + id: 1, + value: publicKey, + format: AsymmetricKeyFormatValues.JWK, + }, + signatureStatus: JwtSignatureStatusValues.VALID, + }); + const handleJwtChange = vi.spyOn(TokenDecoderService, "handleJwtChange"); + + await useDecoderStore.getState().loadDecoderUrlInputs({ + jwt: rs256.token, + publicKey, + publicKeyFormat: AsymmetricKeyFormatValues.JWK, + }); + + expect(loadDecoderInputs).toHaveBeenCalledOnce(); + expect(loadDecoderInputs).toHaveBeenCalledWith({ + algType: SigningAlgCategoryValues.ASYMMETRIC, + alg: "RS256", + jwt: rs256.token, + asymmetricPublicKey: publicKey, + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.JWK, + }); + expect(handleJwtChange).not.toHaveBeenCalled(); + expect(useDecoderStore.getState()).toMatchObject({ + jwt: rs256.token, + asymmetricPublicKey: publicKey, + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.JWK, + isLoading: false, + }); + }); + + test.each([ + { + name: "the public key is missing", + jwt: rs256.token, + publicKey: undefined, + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }, + { + name: "the public key is blank", + jwt: rs256.token, + publicKey: " \n ", + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }, + { + name: "the public key format is missing", + jwt: rs256.token, + publicKey: rs256.publicKey, + publicKeyFormat: undefined, + }, + { + name: "the JWT is invalid", + jwt: "not-a-jwt", + publicKey: rs256.publicKey, + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }, + { + name: "the JWT is not digitally signed", + jwt: DefaultTokensValues.HS256.token, + publicKey: rs256.publicKey, + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }, + ])("preserves normal JWT handling when $name", async (params) => { + const loadDecoderInputs = vi.spyOn( + TokenDecoderService, + "loadDecoderInputs", + ); + const handleJwtChange = vi + .spyOn(TokenDecoderService, "handleJwtChange") + .mockResolvedValue({ jwt: params.jwt }); + + await useDecoderStore.getState().loadDecoderUrlInputs(params); + + expect(loadDecoderInputs).not.toHaveBeenCalled(); + expect(handleJwtChange).toHaveBeenCalledOnce(); + expect(handleJwtChange).toHaveBeenCalledWith({ + alg: initialState.alg, + symmetricSecretKey: initialState.symmetricSecretKey, + symmetricSecretKeyEncoding: initialState.symmetricSecretKeyEncoding, + asymmetricPublicKey: initialState.asymmetricPublicKey, + asymmetricPublicKeyFormat: initialState.asymmetricPublicKeyFormat, + newToken: params.jwt, + }); + expect(useDecoderStore.getState()).toMatchObject({ + jwt: params.jwt, + isLoading: false, + }); + }); + + test.each([ + { + name: "PEM", + publicKey: "not a PEM public key", + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }, + { + name: "JWK", + publicKey: JSON.stringify({ not: "a JWK" }), + publicKeyFormat: AsymmetricKeyFormatValues.JWK, + }, + ])( + "keeps an invalid explicit $name key controlled and bypasses discovery", + async ({ publicKey, publicKeyFormat }) => { + const loadDecoderInputs = vi.spyOn( + TokenDecoderService, + "loadDecoderInputs", + ); + const handleJwtChange = vi.spyOn(TokenDecoderService, "handleJwtChange"); + + await useDecoderStore.getState().loadDecoderUrlInputs({ + jwt: rs256.token, + publicKey, + publicKeyFormat, + }); + + expect(loadDecoderInputs).toHaveBeenCalledOnce(); + expect(handleJwtChange).not.toHaveBeenCalled(); + expect(useDecoderStore.getState()).toMatchObject({ + jwt: rs256.token, + asymmetricPublicKey: publicKey, + asymmetricPublicKeyFormat: publicKeyFormat, + controlledAsymmetricPublicKey: { + value: publicKey, + format: publicKeyFormat, + }, + isLoading: false, + }); + expect(useDecoderStore.getState().verificationInputErrors).not.toBeNull(); + }, + ); + + test("keeps the explicit key controlled when signature verification fails", async () => { + const signatureSeparatorIndex = rs256.token.lastIndexOf("."); + const signature = rs256.token.slice(signatureSeparatorIndex + 1); + const invalidSignatureJwt = `${rs256.token.slice( + 0, + signatureSeparatorIndex + 1, + )}${signature.startsWith("A") ? "B" : "A"}${signature.slice(1)}`; + + await useDecoderStore.getState().loadDecoderUrlInputs({ + jwt: invalidSignatureJwt, + publicKey: rs256.publicKey, + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }); + + expect(useDecoderStore.getState()).toMatchObject({ + jwt: invalidSignatureJwt, + asymmetricPublicKey: rs256.publicKey, + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.PEM, + controlledAsymmetricPublicKey: { + value: rs256.publicKey, + format: AsymmetricKeyFormatValues.PEM, + }, + signatureStatus: JwtSignatureStatusValues.INVALID, + isLoading: false, + }); + expect(useDecoderStore.getState().verificationInputErrors).not.toBeNull(); + }); + + test("does not let an earlier JWT change overwrite a newer URL load", async () => { + const earlier = deferred>(); + const newer = deferred>(); + + vi.spyOn(TokenDecoderService, "handleJwtChange").mockReturnValueOnce( + earlier.promise, + ); + vi.spyOn(TokenDecoderService, "loadDecoderUrlInputs").mockReturnValueOnce( + newer.promise, + ); + + const earlierLoad = useDecoderStore + .getState() + .handleJwtChange("earlier.jwt"); + const newerLoad = useDecoderStore.getState().loadDecoderUrlInputs({ + jwt: "newer.jwt", + }); + + newer.resolve({ + jwt: "newer.jwt", + asymmetricPublicKey: "newer key", + }); + await newerLoad; + + earlier.resolve({ + jwt: "earlier.jwt", + asymmetricPublicKey: "earlier key", + }); + await earlierLoad; + + expect(useDecoderStore.getState()).toMatchObject({ + jwt: "newer.jwt", + asymmetricPublicKey: "newer key", + isLoading: false, + }); + }); + + test("does not let an earlier URL load overwrite newer decoder inputs", async () => { + const earlier = deferred>(); + const newer = deferred>(); + + vi.spyOn(TokenDecoderService, "loadDecoderUrlInputs").mockReturnValueOnce( + earlier.promise, + ); + vi.spyOn(TokenDecoderService, "loadDecoderInputs").mockReturnValueOnce( + newer.promise, + ); + + const earlierLoad = useDecoderStore.getState().loadDecoderUrlInputs({ + jwt: "earlier.jwt", + }); + const newerLoad = useDecoderStore.getState().loadDecoderInputs({ + algType: SigningAlgCategoryValues.NONE, + alg: "none", + jwt: "newer.jwt", + }); + + newer.resolve({ jwt: "newer.jwt" }); + await newerLoad; + + earlier.resolve({ jwt: "earlier.jwt" }); + await earlierLoad; + + expect(useDecoderStore.getState()).toMatchObject({ + jwt: "newer.jwt", + isLoading: false, + }); + }); + + test("keeps loading while a newer URL load is still pending", async () => { + const earlier = deferred>(); + const newer = deferred>(); + const loadDecoderUrlInputs = vi.spyOn( + TokenDecoderService, + "loadDecoderUrlInputs", + ); + + loadDecoderUrlInputs + .mockReturnValueOnce(earlier.promise) + .mockReturnValueOnce(newer.promise); + + const earlierLoad = useDecoderStore.getState().loadDecoderUrlInputs({ + jwt: "earlier.jwt", + }); + const newerLoad = useDecoderStore.getState().loadDecoderUrlInputs({ + jwt: "newer.jwt", + }); + + earlier.resolve({ jwt: "earlier.jwt" }); + await earlierLoad; + + expect(useDecoderStore.getState().isLoading).toBe(true); + + newer.resolve({ jwt: "newer.jwt" }); + await newerLoad; + + expect(useDecoderStore.getState()).toMatchObject({ + jwt: "newer.jwt", + isLoading: false, + }); + }); +}); From 2630d4e77fd453fd21d48c9e0c00491a4eeb201c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 15:08:57 +0200 Subject: [PATCH 02/12] feat: render ML-DSA library support Signed-off-by: Filip Skokan --- .../library-card/library-card.component.tsx | 12 ++++++++++++ src/features/libraries/models/library.model.ts | 3 +++ 2 files changed, 15 insertions(+) diff --git a/src/features/libraries/components/library-card/library-card.component.tsx b/src/features/libraries/components/library-card/library-card.component.tsx index 1d875f12b..628e4e76e 100644 --- a/src/features/libraries/components/library-card/library-card.component.tsx +++ b/src/features/libraries/components/library-card/library-card.component.tsx @@ -229,6 +229,18 @@ export const LibraryCardComponent: React.FC = ({ + + + {minimumVersion && ( diff --git a/src/features/libraries/models/library.model.ts b/src/features/libraries/models/library.model.ts index 6ba39c231..8ad9bedef 100644 --- a/src/features/libraries/models/library.model.ts +++ b/src/features/libraries/models/library.model.ts @@ -27,6 +27,9 @@ export interface LibraryModel { es256k?: boolean; ed25519?: boolean; ed448?: boolean; + "ml-dsa-44"?: boolean; + "ml-dsa-65"?: boolean; + "ml-dsa-87"?: boolean; }; authorUrl: string | null; authorName: string; From bc972c022df3853b9cbe5bf2097b6762e605c842 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 15:26:31 +0200 Subject: [PATCH 03/12] fix: remove Ed448 from debugger Signed-off-by: Filip Skokan --- src/features/common/services/jwt.service.ts | 16 ---------- src/features/common/services/utils.ts | 4 +-- ...-alg-header-parameter-values.dictionary.ts | 6 ---- .../debugger-alg-picker.component.tsx | 32 ++++++------------- src/scripts/generate-test-jwts.ts | 4 --- 5 files changed, 10 insertions(+), 52 deletions(-) diff --git a/src/features/common/services/jwt.service.ts b/src/features/common/services/jwt.service.ts index a56d14a73..2284c4c6b 100644 --- a/src/features/common/services/jwt.service.ts +++ b/src/features/common/services/jwt.service.ts @@ -1421,19 +1421,3 @@ export const isEd25519Supported = async (): Promise => { return false; } }; - -export const isEd448Supported = async (): Promise => { - try { - await window.crypto.subtle.generateKey( - { - name: algDictionary.Ed448, - }, - true, - ["sign", "verify"], - ); - - return true; - } catch (e) { - return false; - } -}; diff --git a/src/features/common/services/utils.ts b/src/features/common/services/utils.ts index fd047777a..3b54edd71 100644 --- a/src/features/common/services/utils.ts +++ b/src/features/common/services/utils.ts @@ -244,7 +244,5 @@ export const safePublicKeyToPem = fromThrowable( ); export const getAlgName = (value: string) => { - return value === algDictionary.Ed25519 || value === algDictionary.Ed448 - ? algDictionary.EdDSA - : value; + return value === algDictionary.Ed25519 ? algDictionary.EdDSA : value; }; diff --git a/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts b/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts index beca2e97e..943a1f581 100644 --- a/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts +++ b/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts @@ -27,7 +27,6 @@ export interface JwsAlgHeaderParameterValuesDictionaryModel { PS384: JwsAlgHeaderParameterModel; PS512: JwsAlgHeaderParameterModel; Ed25519: JwsAlgHeaderParameterModel; - Ed448: JwsAlgHeaderParameterModel; }; } @@ -61,10 +60,6 @@ export const jwsExampleAlgHeaderParameterValuesDictionary: JwsAlgHeaderParameter name: "EdDSA (Ed25519)", description: "Edwards-curve Digital Signature Algorithm", }, - Ed448: { - name: "EdDSA (Ed448)", - description: "Edwards-curve Digital Signature Algorithm", - }, }, }; @@ -133,5 +128,4 @@ export const algDictionary = { ECDSA: "ECDSA", EdDSA: "EdDSA", Ed25519: "Ed25519", - Ed448: "Ed448", }; diff --git a/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx b/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx index 2ea12e1d5..0fac18f20 100644 --- a/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx +++ b/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx @@ -4,7 +4,6 @@ import { getPickersUiDictionary } from "@/features/localization/services/ui-lang import { useEncoderStore } from "@/features/encoder/services/encoder.store"; import { isEd25519Supported, - isEd448Supported, isP521Supported, } from "@/features/common/services/jwt.service"; import { useDecoderStore } from "@/features/decoder/services/decoder.store"; @@ -58,29 +57,28 @@ export const WidgetAlgPickerComponent: React.FC< elementType: "span", preventFocusOnPress: true, }, - buttonRef + buttonRef, ); const [pickerState, setPickerState] = useState( - PickerStates.IDLE + PickerStates.IDLE, ); const [capabilities, setCapabilities] = useState({ canUseEs512: false, canUseEd25519: false, - canUseEd448: false, isLoading: true, }); - const { canUseEs512, canUseEd25519, canUseEd448, isLoading } = capabilities; + const { canUseEs512, canUseEd25519, isLoading } = capabilities; const dictionary = getPickersUiDictionary(languageCode); const selectEncoderAlg$ = useEncoderStore( - (state) => state.selectEncodingExample + (state) => state.selectEncodingExample, ); const selectDecoderAlg$ = useDecoderStore( - (state) => state.selectDecodingExample + (state) => state.selectDecodingExample, ); const selectExample = (value: string) => { @@ -99,16 +97,14 @@ export const WidgetAlgPickerComponent: React.FC< useEffect(() => { (async function checkCapabilities() { - const [canUseEs512, canUseEd25519, canUseEd448] = await Promise.all([ + const [canUseEs512, canUseEd25519] = await Promise.all([ isP521Supported(), isEd25519Supported(), - isEd448Supported(), ]); setCapabilities({ canUseEs512, canUseEd25519, - canUseEd448, isLoading: false, }); })(); @@ -136,13 +132,13 @@ export const WidgetAlgPickerComponent: React.FC< value: key, label: value.name, }; - } + }, ); }, []); const asymmetricAlgOptions: DebuggerPickerOptionModel[] = useMemo(() => { const digitalSignatureEntries = Object.entries( - jwsExampleAlgHeaderParameterValuesDictionary.digitalSignature + jwsExampleAlgHeaderParameterValuesDictionary.digitalSignature, ); const asymmetricAlgOptions: DebuggerPickerOptionModel[] = []; @@ -172,16 +168,6 @@ export const WidgetAlgPickerComponent: React.FC< continue; } - if (key === algDictionary.Ed448) { - asymmetricAlgOptions.push({ - value: key, - label: value.name, - isDisabled: !canUseEd448, - }); - - continue; - } - asymmetricAlgOptions.push({ value: key, label: value.name, @@ -189,7 +175,7 @@ export const WidgetAlgPickerComponent: React.FC< } return asymmetricAlgOptions; - }, [canUseEd25519, canUseEd448, canUseEs512]); + }, [canUseEd25519, canUseEs512]); const algOptions = useMemo(() => { return [...noneAlgOptions, ...symmetricAlgOptions, ...asymmetricAlgOptions]; diff --git a/src/scripts/generate-test-jwts.ts b/src/scripts/generate-test-jwts.ts index a7c1c40bf..67aea2031 100644 --- a/src/scripts/generate-test-jwts.ts +++ b/src/scripts/generate-test-jwts.ts @@ -141,10 +141,6 @@ async function generateKeys(algorithm: string): Promise { if (algorithm === "Ed25519") { keyPair = generateKeyPairSync("ed25519"); } - if (algorithm === "Ed448") { - // Support for Ed448 - keyPair = generateKeyPairSync("ed448"); - } if (!keyPair) { throw new Error(`Unsupported algorithm: ${algorithm}`); From d098c4183b1aab98995cf8d619453e8febfd3181 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 15:43:16 +0200 Subject: [PATCH 04/12] chore: update jose to 6.2.5 Signed-off-by: Filip Skokan --- package-lock.json | 9 +++++---- package.json | 2 +- src/features/common/services/jwt.service.ts | 12 ++++++------ src/features/common/services/utils.ts | 4 ++-- src/features/decoder/services/public-key.service.ts | 4 ++-- src/scripts/generate-test-jwts.ts | 3 +-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 54f963475..91cfa9a13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "bowser": "^2.11.0", "clsx": "^2.1.1", "dotenv": "^16.4.5", - "jose": "^5.2.3", + "jose": "^6.2.5", "js-cookie": "^3.0.5", "loglevel": "^1.9.1", "nanoid": "^5.0.9", @@ -9047,9 +9047,10 @@ } }, "node_modules/jose": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz", - "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.5.tgz", + "integrity": "sha512-2E5L2yRp03FnwreJLJX8/r7mHiZICCf8kG7fAsTWkSQTDAcc46NIZoQLKy+EJ8sPoJlxyS4OQR5H70LjIZZlIQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } diff --git a/package.json b/package.json index d815aafe9..c3d481e1c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "bowser": "^2.11.0", "clsx": "^2.1.1", "dotenv": "^16.4.5", - "jose": "^5.2.3", + "jose": "^6.2.5", "js-cookie": "^3.0.5", "loglevel": "^1.9.1", "nanoid": "^5.0.9", diff --git a/src/features/common/services/jwt.service.ts b/src/features/common/services/jwt.service.ts index 2284c4c6b..bcc96296d 100644 --- a/src/features/common/services/jwt.service.ts +++ b/src/features/common/services/jwt.service.ts @@ -16,8 +16,8 @@ import { CompactSign, CompactVerifyResult, importPKCS8, + type CryptoKey, type JWK, - KeyLike, } from "jose"; import nodeForge from "node-forge"; import { EncodingValues } from "@/features/common/values/encoding.values"; @@ -615,9 +615,9 @@ export async function validateAsymmetricKey({ asymmetricPublicKeyFormat, }: { alg: string; - asymmetricPublicKey: KeyLike | string; + asymmetricPublicKey: CryptoKey | string; asymmetricPublicKeyFormat: AsymmetricKeyFormatValues; -}): Promise> { +}): Promise> { const spki = `-----BEGIN PUBLIC KEY-----`; const pkcs1 = `-----BEGIN RSA PUBLIC KEY-----`; const x509 = `-----BEGIN CERTIFICATE-----`; @@ -783,7 +783,7 @@ export async function validateAsymmetricKey({ type VerifyDigitallySignedJwtParams = { jwt: string; alg: string; - asymmetricPublicKey: KeyLike | string; + asymmetricPublicKey: CryptoKey | string; asymmetricPublicKeyFormat: AsymmetricKeyFormatValues; }; @@ -924,7 +924,7 @@ export const getAsymmetricKeyCryptoKey = async ( key: string, alg: string, keyFormat: AsymmetricKeyFormatValues, -): Promise> => { +): Promise> => { if (keyFormat === AsymmetricKeyFormatValues.PEM) { if (key.startsWith("-----BEGIN RSA PRIVATE KEY-----")) { const safePrivateKeyFromPemResult = safePrivateKeyFromPem(key); @@ -1043,7 +1043,7 @@ const signValue = (compactSign: CompactSign, key: Uint8Array) => { const signValueWithPrivateKey = ( compactSign: CompactSign, - key: KeyLike | Uint8Array, + key: CryptoKey | Uint8Array, ) => { return fromPromise(compactSign.sign(key), (e) => { console.error(e); diff --git a/src/features/common/services/utils.ts b/src/features/common/services/utils.ts index 3b54edd71..dba1138d9 100644 --- a/src/features/common/services/utils.ts +++ b/src/features/common/services/utils.ts @@ -11,7 +11,7 @@ import { importJWK, importSPKI, importX509, - KeyLike, + type CryptoKey, } from "jose"; import nodeForge from "node-forge"; import base64url from "base64url"; @@ -205,7 +205,7 @@ export const safeImportJWK = fromAsyncThrowable(importJWK, (e) => { }); }); -export const safeCompactVerify = (jws: string, key: Uint8Array | KeyLike) => { +export const safeCompactVerify = (jws: string, key: Uint8Array | CryptoKey) => { return fromPromise(compactVerify(jws, key), (e) => { if (e instanceof Error) { if (e.name === "JWSSignatureVerificationFailed") { diff --git a/src/features/decoder/services/public-key.service.ts b/src/features/decoder/services/public-key.service.ts index 34a83d33b..c5797adbc 100644 --- a/src/features/decoder/services/public-key.service.ts +++ b/src/features/decoder/services/public-key.service.ts @@ -1,4 +1,4 @@ -import { createRemoteJWKSet, exportJWK, KeyLike } from "jose"; +import { createRemoteJWKSet, exportJWK, type CryptoKey } from "jose"; import { DecodedJwtHeaderModel, DecodedJwtPayloadModel, @@ -79,7 +79,7 @@ const safeCreateUrl = (url: string): Result => { } }; -const safeExportJWK = (key: KeyLike | Uint8Array) => { +const safeExportJWK = (key: CryptoKey | Uint8Array) => { return fromPromise(exportJWK(key), (e) => { console.error(e); diff --git a/src/scripts/generate-test-jwts.ts b/src/scripts/generate-test-jwts.ts index 67aea2031..a47d2a1c5 100644 --- a/src/scripts/generate-test-jwts.ts +++ b/src/scripts/generate-test-jwts.ts @@ -16,7 +16,6 @@ import { CompactJWSHeaderParameters, CompactSign, exportJWK, - KeyLike, UnsecuredJWT, } from "jose"; import { EncodingValues } from "@/features/common/values/encoding.values"; @@ -69,7 +68,7 @@ async function createJWT({ }: { header: CompactJWSHeaderParameters; payload: object; - secret: Uint8Array | KeyLike; + secret: Uint8Array; }): Promise { try { const jwsPayload = JSON.stringify(payload); From 4c4b1e0b2ab5b7c6cb01c0c90174095e0c80ec1b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 15:49:45 +0200 Subject: [PATCH 05/12] refactor: use Ed25519 in debugger, still support pasted EdDSA Signed-off-by: Filip Skokan --- e2e/jwt.json | 8 +-- src/features/common/services/jwt.service.ts | 12 ++--- src/features/common/services/utils.ts | 5 -- .../common/values/default-tokens.values.ts | 2 +- ...-alg-header-parameter-values.dictionary.ts | 7 ++- .../decoder/services/token-decoder.service.ts | 3 +- .../encoder/services/token-encoder.service.ts | 3 +- src/scripts/generate-test-jwts.ts | 6 +-- tests/jwt.service.test.ts | 54 ++++++++++++++++++- 9 files changed, 73 insertions(+), 27 deletions(-) diff --git a/e2e/jwt.json b/e2e/jwt.json index 41bcd8e77..f4705ab12 100644 --- a/e2e/jwt.json +++ b/e2e/jwt.json @@ -366,16 +366,16 @@ "Ed25519": { "type": "digital", "withPemKey": { - "header": "{\n \"alg\": \"EdDSA\",\n \"type\": \"JWT\"\n}", + "header": "{\n \"alg\": \"Ed25519\",\n \"type\": \"JWT\"\n}", "payload": "{\n \"name\": \"Eric Arthur Blair\",\n \"given_name\": \"Eric\",\n \"middle_name\": \"Arthur\",\n \"family_name\": \"Blair\",\n \"nickname\": \"George Orwell\",\n \"preferred_username\": \"Orwell\",\n \"profile\": \"https://en.wikipedia.org/wiki/George_Orwell\",\n \"picture\": \"https://upload.wikimedia.org/wikipedia/commons/7/7e/George_Orwell_press_photo.jpg\",\n \"website\": \"https://www.orwellfoundation.com/the-orwell-foundation/orwell/books-by-orwell/\",\n \"birthdate\": -2099242800,\n \"locale\": \"en-GB\",\n \"updated_at\": 441781200,\n \"zoneinfo\": \"Europe/London\"\n}", "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAjz/HpwFQrkra31uAtkUHyFv36FccSdMncXP2LKV0cUc=\n-----END PUBLIC KEY-----", "publicKeyFormat": "PEM", "privateKey": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIE0t8fJoUmw1UA6ep5r5WpOIcA+uGPv+MOt5y5xbwZWa\n-----END PRIVATE KEY-----", "privateKeyFormat": "PEM", - "jwt": "eyJhbGciOiJFZERTQSIsInR5cGUiOiJKV1QifQ.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.axG8VrQJ6QF7nLMj5MISmuXc0mZYm3VdcKk2BpU3vYgD0hi91gegw_rb3MFZu3gJHY0K2L5EUYPFM93KklPEBQ" + "jwt": "eyJhbGciOiJFZDI1NTE5IiwidHlwZSI6IkpXVCJ9.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.ymupeGgBkXyS4CeWt1q_N3q9SIXcDVL-wbW0-7asb8jVvSjxaoXjiU3BGsE-KoUsTrwLJvLyNEAGW_JfMejeAA" }, "withJwkKey": { - "header": "{\n \"alg\": \"EdDSA\",\n \"type\": \"JWT\"\n}", + "header": "{\n \"alg\": \"Ed25519\",\n \"type\": \"JWT\"\n}", "payload": "{\n \"name\": \"Eric Arthur Blair\",\n \"given_name\": \"Eric\",\n \"middle_name\": \"Arthur\",\n \"family_name\": \"Blair\",\n \"nickname\": \"George Orwell\",\n \"preferred_username\": \"Orwell\",\n \"profile\": \"https://en.wikipedia.org/wiki/George_Orwell\",\n \"picture\": \"https://upload.wikimedia.org/wikipedia/commons/7/7e/George_Orwell_press_photo.jpg\",\n \"website\": \"https://www.orwellfoundation.com/the-orwell-foundation/orwell/books-by-orwell/\",\n \"birthdate\": -2099242800,\n \"locale\": \"en-GB\",\n \"updated_at\": 441781200,\n \"zoneinfo\": \"Europe/London\"\n}", "publicKey": { "crv": "Ed25519", @@ -390,7 +390,7 @@ "kty": "OKP" }, "privateKeyFormat": "JWK", - "jwt": "eyJhbGciOiJFZERTQSIsInR5cGUiOiJKV1QifQ.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.axG8VrQJ6QF7nLMj5MISmuXc0mZYm3VdcKk2BpU3vYgD0hi91gegw_rb3MFZu3gJHY0K2L5EUYPFM93KklPEBQ" + "jwt": "eyJhbGciOiJFZDI1NTE5IiwidHlwZSI6IkpXVCJ9.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.ymupeGgBkXyS4CeWt1q_N3q9SIXcDVL-wbW0-7asb8jVvSjxaoXjiU3BGsE-KoUsTrwLJvLyNEAGW_JfMejeAA" } } } diff --git a/src/features/common/services/jwt.service.ts b/src/features/common/services/jwt.service.ts index bcc96296d..f4e616ac7 100644 --- a/src/features/common/services/jwt.service.ts +++ b/src/features/common/services/jwt.service.ts @@ -22,7 +22,6 @@ import { import nodeForge from "node-forge"; import { EncodingValues } from "@/features/common/values/encoding.values"; import { - getAlgName, getOperationException, safeBase64url, safeBase64urlToBuffer, @@ -82,11 +81,11 @@ export function isHmacAlg(algorithm: string): boolean { export function isDigitalSignatureAlg(algorithm: string): boolean { const alg = jwsAlgHeaderParameterValuesDictionary.digitalSignature[algorithm]; - return !!alg || algorithm === algDictionary.EdDSA; + return !!alg; } export const getAlgSize = (value: string): Result<{ size: number }, string> => { - const algorithm = getAlgName(value); + const algorithm = value; if (!isSupportedAlg(algorithm)) { return err( @@ -96,15 +95,16 @@ export const getAlgSize = (value: string): Result<{ size: number }, string> => { const algRegex = /^([a-zA-Z]+)(\d+)$/; const match = algorithm.match(algRegex); - const isEdDSA = algorithm === algDictionary.EdDSA; + const isEdwardsCurveAlgorithm = + algorithm === algDictionary.EdDSA || algorithm === algDictionary.Ed25519; const isNone = algorithm === algDictionary.NONE; if (isNone) { return err(`Can't calculate length for Unsecured JWT.`); } - if (isEdDSA) { - return err(`Can't calculate length from EdDSA algorithm.`); + if (isEdwardsCurveAlgorithm) { + return err(`Can't calculate length from ${algorithm} algorithm.`); } if (!match) { diff --git a/src/features/common/services/utils.ts b/src/features/common/services/utils.ts index dba1138d9..9eadfc4cd 100644 --- a/src/features/common/services/utils.ts +++ b/src/features/common/services/utils.ts @@ -15,7 +15,6 @@ import { } from "jose"; import nodeForge from "node-forge"; import base64url from "base64url"; -import { algDictionary } from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; export const extractJwt = (value: string): string => { if (!value) { @@ -242,7 +241,3 @@ export const safePublicKeyToPem = fromThrowable( return "Unable to transform public key to PEM string."; }, ); - -export const getAlgName = (value: string) => { - return value === algDictionary.Ed25519 ? algDictionary.EdDSA : value; -}; diff --git a/src/features/common/values/default-tokens.values.ts b/src/features/common/values/default-tokens.values.ts index a35c3fc0e..1879a3793 100644 --- a/src/features/common/values/default-tokens.values.ts +++ b/src/features/common/values/default-tokens.values.ts @@ -269,7 +269,7 @@ export const DefaultTokensValues: DefaultTokensModel = { }, Ed25519: { token: - "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.JkKWCY39IdWEQttmdqR7VdsvT-_QxheW_eb0S5wr_j83ltux_JDUIXs7a3Dtn3xuqzuhetiuJrWIvy5TzimeCg", + "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.IiFCYED1bkN9FL4H3UG884v2Wxmp_0SDfaaaRipa2N1QcjevsxA0WNRYnFMqPHf4tpNw93fL7PZs4az9kH0NBQ", privateKey: edPrivateKey, publicKey: edPublicKey, publicKeyFormat: AsymmetricKeyFormatValues.PEM, diff --git a/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts b/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts index 943a1f581..a33dd0d28 100644 --- a/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts +++ b/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts @@ -57,7 +57,7 @@ export const jwsExampleAlgHeaderParameterValuesDictionary: JwsAlgHeaderParameter PS384: { name: "PS384", description: "HMAC using SHA-256" }, PS512: { name: "PS512", description: "HMAC using SHA-256" }, Ed25519: { - name: "EdDSA (Ed25519)", + name: "Ed25519", description: "Edwards-curve Digital Signature Algorithm", }, }, @@ -86,6 +86,7 @@ export interface JwsExampleAlgHeaderParameterValuesDictionaryModel { PS384: JwsAlgHeaderParameterModel; PS512: JwsAlgHeaderParameterModel; EdDSA: JwsAlgHeaderParameterModel; + Ed25519: JwsAlgHeaderParameterModel; }; } @@ -119,6 +120,10 @@ export const jwsAlgHeaderParameterValuesDictionary: JwsExampleAlgHeaderParameter name: "EdDSA", description: "Edwards-curve Digital Signature Algorithm", }, + Ed25519: { + name: "Ed25519", + description: "Edwards-curve Digital Signature Algorithm", + }, }, }; diff --git a/src/features/decoder/services/token-decoder.service.ts b/src/features/decoder/services/token-decoder.service.ts index c258c8a41..0e1672ccc 100644 --- a/src/features/decoder/services/token-decoder.service.ts +++ b/src/features/decoder/services/token-decoder.service.ts @@ -6,7 +6,6 @@ import { } from "@/features/common/values/default-tokens.values"; import { extractJwt, - getAlgName, operationExceptionDictionary, } from "@/features/common/services/utils"; import { @@ -72,7 +71,7 @@ class _TokenDecoderService { const defaultToken = DefaultTokensValues[algorithmPickerOptionValue] || DEFAULT_JWT; - const algorithm = getAlgName(algorithmPickerOptionValue); + const algorithm = algorithmPickerOptionValue; if (isNoneAlg(algorithm)) { const token = defaultToken as DefaultUnsecuredTokenModel; diff --git a/src/features/encoder/services/token-encoder.service.ts b/src/features/encoder/services/token-encoder.service.ts index e35044c5a..3b3af6a53 100644 --- a/src/features/encoder/services/token-encoder.service.ts +++ b/src/features/encoder/services/token-encoder.service.ts @@ -25,7 +25,6 @@ import { EncodingValues } from "@/features/common/values/encoding.values"; import { err, ok, Result } from "neverthrow"; import { DebuggerErrorModel } from "@/features/common/models/debugger-error.model"; import { - getAlgName, safeJsonParse, safeJsonStringify, } from "@/features/common/services/utils"; @@ -83,7 +82,7 @@ class _TokenEncoderService { const jwt = DefaultTokensValues[algorithmPickerOptionValue]; - const algorithm = getAlgName(algorithmPickerOptionValue); + const algorithm = algorithmPickerOptionValue; const header = isNoneAlg(algorithm) ? { diff --git a/src/scripts/generate-test-jwts.ts b/src/scripts/generate-test-jwts.ts index a47d2a1c5..91eda28a2 100644 --- a/src/scripts/generate-test-jwts.ts +++ b/src/scripts/generate-test-jwts.ts @@ -21,7 +21,6 @@ import { import { EncodingValues } from "@/features/common/values/encoding.values"; import { writeFileSync } from "node:fs"; import { join } from "path"; -import { getAlgName } from "@/features/common/services/utils"; import { createPrivateKey } from "node:crypto"; import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; import { @@ -166,10 +165,9 @@ async function generateKeys(algorithm: string): Promise { (async function () { for (let i = 0; i < algs.length; i++) { const alg = algs[i]; - const cleanAlg = getAlgName(alg); const header = { - alg: cleanAlg, + alg, type: "JWT", }; @@ -257,7 +255,7 @@ async function generateKeys(algorithm: string): Promise { } as JwtDictionaryEntryModel; } - if (isDigitalSignatureAlg(cleanAlg)) { + if (isDigitalSignatureAlg(alg)) { const { privateKey, privateJWK, publicKey, publicJWK } = await generateKeys(alg); diff --git a/tests/jwt.service.test.ts b/tests/jwt.service.test.ts index 6eee17792..e19a64e46 100644 --- a/tests/jwt.service.test.ts +++ b/tests/jwt.service.test.ts @@ -1,9 +1,16 @@ import { describe, expect, test } from "vitest"; -import { DefaultTokensValues } from "@/features/common/values/default-tokens.values"; -import { validateJwtFormat } from "@/features/common/services/jwt.service"; +import { + DefaultTokensValues, + DefaultTokenWithKeysModel, +} from "@/features/common/values/default-tokens.values"; +import { + validateJwtFormat, + verifyDigitallySignedJwt, +} from "@/features/common/services/jwt.service"; import { JwtTypeValues } from "@/features/common/values/jwt-type.values"; import { DebuggerTaskValues } from "@/features/common/values/debugger-task.values"; import { DebuggerInputValues } from "@/features/common/values/debugger-input.values"; +import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; describe("validateJwtFormat", () => { const tokenHS256 = DefaultTokensValues.HS256.token; @@ -277,3 +284,46 @@ describe("validateJwtFormat", () => { ); }); }); + +describe("Ed25519", () => { + const token = DefaultTokensValues.Ed25519 as DefaultTokenWithKeysModel; + const legacyEdDsaToken = + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.JkKWCY39IdWEQttmdqR7VdsvT-_QxheW_eb0S5wr_j83ltux_JDUIXs7a3Dtn3xuqzuhetiuJrWIvy5TzimeCg"; + + test("uses the Ed25519 identifier for new examples", () => { + const result = validateJwtFormat(token.token); + + expect(result.isOk()).toBe(true); + expect(result.unwrapOr(null)).toMatchObject({ + signingAlgorithm: "Ed25519", + decoded: { + header: { + alg: "Ed25519", + }, + }, + }); + }); + + test("continues to validate and verify pasted EdDSA tokens", async () => { + const validationResult = validateJwtFormat(legacyEdDsaToken); + + expect(validationResult.isOk()).toBe(true); + expect(validationResult.unwrapOr(null)).toMatchObject({ + signingAlgorithm: "EdDSA", + decoded: { + header: { + alg: "EdDSA", + }, + }, + }); + + const verificationResult = await verifyDigitallySignedJwt({ + jwt: legacyEdDsaToken, + alg: "EdDSA", + asymmetricPublicKey: token.publicKey, + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.PEM, + }); + + expect(verificationResult.unwrapOr(false)).toBe(true); + }); +}); From 7f78c48db862f4c7548bce25fe133590a7017282 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 16:06:04 +0200 Subject: [PATCH 06/12] refactor: remove P-521 and Ed25519 detection Signed-off-by: Filip Skokan --- src/features/common/services/jwt.service.ts | 33 -------- ...-alg-header-parameter-values.dictionary.ts | 2 - .../debugger-alg-picker.component.tsx | 75 +++---------------- 3 files changed, 9 insertions(+), 101 deletions(-) diff --git a/src/features/common/services/jwt.service.ts b/src/features/common/services/jwt.service.ts index f4e616ac7..deb346961 100644 --- a/src/features/common/services/jwt.service.ts +++ b/src/features/common/services/jwt.service.ts @@ -1388,36 +1388,3 @@ export const checkHmacSecretLength = ( message: `A key of ${algSize} bits or larger MUST be used with HS${algSize} as specified on [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518#section-3.2).`, }); }; - -export const isP521Supported = async (): Promise => { - try { - await window.crypto.subtle.generateKey( - { - name: algDictionary.ECDSA, - namedCurve: "P-521", - }, - true, - ["sign", "verify"], - ); - - return true; - } catch (e) { - return false; - } -}; - -export const isEd25519Supported = async (): Promise => { - try { - await window.crypto.subtle.generateKey( - { - name: algDictionary.Ed25519, - }, - true, - ["sign", "verify"], - ); - - return true; - } catch (e) { - return false; - } -}; diff --git a/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts b/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts index a33dd0d28..6f2e170f0 100644 --- a/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts +++ b/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts @@ -129,8 +129,6 @@ export const jwsAlgHeaderParameterValuesDictionary: JwsExampleAlgHeaderParameter export const algDictionary = { NONE: "none", - ES512: "ES512", - ECDSA: "ECDSA", EdDSA: "EdDSA", Ed25519: "Ed25519", }; diff --git a/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx b/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx index 0fac18f20..8e47c0250 100644 --- a/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx +++ b/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx @@ -1,19 +1,12 @@ -import React, { useEffect, useMemo, useRef, useState } from "react"; +import React, { useMemo, useRef, useState } from "react"; import styles from "./debugger-alg-picker.module.scss"; import { getPickersUiDictionary } from "@/features/localization/services/ui-language-dictionary.service"; import { useEncoderStore } from "@/features/encoder/services/encoder.store"; -import { - isEd25519Supported, - isP521Supported, -} from "@/features/common/services/jwt.service"; import { useDecoderStore } from "@/features/decoder/services/decoder.store"; import { useDebuggerStore } from "@/features/debugger/services/debugger.store"; import { DebuggerWidgetValues } from "@/features/common/values/debugger-widget.values"; import { DebuggerPickerComponent } from "@/features/common/components/debugger-picker/debugger-picker.component"; -import { - algDictionary, - jwsExampleAlgHeaderParameterValuesDictionary, -} from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; +import { jwsExampleAlgHeaderParameterValuesDictionary } from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; import { useButton } from "@react-aria/button"; import { DebuggerPickerOptionModel } from "@/features/common/models/debugger-picker-option.model"; @@ -63,13 +56,6 @@ export const WidgetAlgPickerComponent: React.FC< const [pickerState, setPickerState] = useState( PickerStates.IDLE, ); - const [capabilities, setCapabilities] = useState({ - canUseEs512: false, - canUseEd25519: false, - isLoading: true, - }); - - const { canUseEs512, canUseEd25519, isLoading } = capabilities; const dictionary = getPickersUiDictionary(languageCode); @@ -95,21 +81,6 @@ export const WidgetAlgPickerComponent: React.FC< setPickerState(PickerStates.IDLE); }; - useEffect(() => { - (async function checkCapabilities() { - const [canUseEs512, canUseEd25519] = await Promise.all([ - isP521Supported(), - isEd25519Supported(), - ]); - - setCapabilities({ - canUseEs512, - canUseEd25519, - isLoading: false, - }); - })(); - }, []); - const noneAlgOptions: DebuggerPickerOptionModel[] = useMemo(() => { return Object.entries( jwsExampleAlgHeaderParameterValuesDictionary.unsecured, @@ -137,45 +108,17 @@ export const WidgetAlgPickerComponent: React.FC< }, []); const asymmetricAlgOptions: DebuggerPickerOptionModel[] = useMemo(() => { - const digitalSignatureEntries = Object.entries( + return Object.entries( jwsExampleAlgHeaderParameterValuesDictionary.digitalSignature, - ); - - const asymmetricAlgOptions: DebuggerPickerOptionModel[] = []; - - for (let i = 0; i < digitalSignatureEntries.length; i++) { - const entry = digitalSignatureEntries[i]; - + ).map((entry) => { const [key, value] = entry; - if (key === algDictionary.ES512) { - asymmetricAlgOptions.push({ - value: key, - label: value.name, - isDisabled: !canUseEs512, - }); - - continue; - } - - if (key === algDictionary.Ed25519) { - asymmetricAlgOptions.push({ - value: key, - label: value.name, - isDisabled: !canUseEd25519, - }); - - continue; - } - - asymmetricAlgOptions.push({ + return { value: key, label: value.name, - }); - } - - return asymmetricAlgOptions; - }, [canUseEd25519, canUseEs512]); + }; + }); + }, []); const algOptions = useMemo(() => { return [...noneAlgOptions, ...symmetricAlgOptions, ...asymmetricAlgOptions]; @@ -185,7 +128,7 @@ export const WidgetAlgPickerComponent: React.FC<
From 10ef6ac7ef63bec6117d59b7e0bcdb3860f2dda0 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 16:38:30 +0200 Subject: [PATCH 07/12] feat: add ML-DSA debugger support Signed-off-by: Filip Skokan --- e2e/debugger.spec.ts | 35 +++- e2e/decoder.spec.ts | 27 ++- e2e/e2e.models.ts | 3 + e2e/e2e.utils.ts | 10 + e2e/encoder.spec.ts | 21 +- e2e/jwt.json | 90 +++++++++ package-lock.json | 24 +-- package.json | 2 +- playwright.config.ts | 7 +- src/features/common/services/jwt.service.ts | 29 ++- .../common/values/default-tokens.values.ts | 2 + ...-alg-header-parameter-values.dictionary.ts | 39 ++++ .../values/ml-dsa-default-tokens.values.ts | 183 ++++++++++++++++++ .../debugger-alg-picker.component.tsx | 36 +++- src/scripts/generate-test-jwts.ts | 17 ++ tests/jwt.service.test.ts | 134 +++++++++++-- 16 files changed, 607 insertions(+), 52 deletions(-) create mode 100644 src/features/common/values/ml-dsa-default-tokens.values.ts diff --git a/e2e/debugger.spec.ts b/e2e/debugger.spec.ts index ba77657cb..0ec8e7143 100644 --- a/e2e/debugger.spec.ts +++ b/e2e/debugger.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from "@playwright/test"; import { dataTestidDictionary } from "@/libs/testing/data-testid.dictionary"; +import { getPickersUiDictionary } from "@/features/localization/services/ui-language-dictionary.service"; import { checkHeaderEditorStatusBarMessage, checkJwtEditorNotificationBarMessage, @@ -8,9 +9,11 @@ import { E2E_BASE_URL, expectToBeNonNull, getLang, + hasMlDsaSupport, switchToEncoderTab, } from "./e2e.utils"; import { MessageStatusValue, MessageTypeValue } from "./e2e.values"; +import { mlDsaAlgorithms } from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; test("Can load decoded header and decoded payload from the JWT Decoder as inputs into the JWT Encoder", async ({ page, @@ -189,4 +192,34 @@ test("Can decode a single jwt piece and load it as input into the jwt encoded he ); await expect(encoderSecretKeyEditor).not.toBeVisible(); -}); \ No newline at end of file +}); + +test("ML-DSA examples are enabled only when the browser supports them", async ({ + browserName, + page, +}) => { + await page.goto(E2E_BASE_URL); + + const algorithmPicker = page.getByRole("combobox", { + name: "Debugger picker", + }); + const algorithmPickerRegion = page + .getByRole("region") + .filter({ has: algorithmPicker }); + const lang = await getLang(page); + expectToBeNonNull(lang); + const pickersUiDictionary = getPickersUiDictionary(lang); + await expect(algorithmPickerRegion).toHaveAttribute("aria-busy", "false"); + await page + .getByText(pickersUiDictionary.exampleAlgPicker.defaultValue) + .click(); + + for (const algorithm of mlDsaAlgorithms) { + const isSupported = await hasMlDsaSupport(page, algorithm); + + expect(isSupported).toBe(browserName === "chromium"); + await expect( + page.getByRole("option", { name: algorithm, exact: true }), + ).toHaveAttribute("aria-disabled", String(!isSupported)); + } +}); diff --git a/e2e/decoder.spec.ts b/e2e/decoder.spec.ts index 460cc6e61..5eb467788 100644 --- a/e2e/decoder.spec.ts +++ b/e2e/decoder.spec.ts @@ -23,11 +23,13 @@ import { getDecoderJwtEditor, getDecoderJwtEditorInput, getLang, + hasMlDsaSupport, } from "./e2e.utils"; import { MessageStatusValue, MessageTypeValue } from "./e2e.values"; import { JwtDictionaryModel, JwtSignedWithDigitalModel } from "./e2e.models"; import jwts from "./jwt.json" with { type: "json" }; import { EncodingValues } from "@/features/common/values/encoding.values"; +import { isMlDsaAlgorithm } from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; const TestJwts = (jwts as JwtDictionaryModel).byAlgorithm; @@ -138,15 +140,21 @@ test.describe("Can generate JWT examples", () => { test(`can generate a JWT decoder example for ${option}`, async ({ page, }) => { - if (option === "Ed25519") { - return; + if (isMlDsaAlgorithm(option)) { + test.skip( + !(await hasMlDsaSupport(page, option)), + "ML-DSA is not supported by this browser", + ); } const lang = await getLang(page); expectToBeNonNull(lang); const decoderWidget = page.getByTestId(dataTestidDictionary.decoder.id); - await page.getByRole("listbox").getByRole("option", { name: option }).click(); + await page + .getByRole("listbox") + .getByRole("option", { name: option }) + .dispatchEvent("click"); const targetToken = DefaultTokensValues[option]; @@ -242,8 +250,11 @@ test.describe("decode JWTs", () => { options.forEach((option) => { test(`Can input a JWT signed with ${option}`, async ({ page }) => { - if (option === "Ed25519") { - return; + if (isMlDsaAlgorithm(option)) { + test.skip( + !(await hasMlDsaSupport(page, option)), + "ML-DSA is not supported by this browser", + ); } const lang = await getLang(page); @@ -544,7 +555,11 @@ test.describe("decode JWTs", () => { expect(decodedPayload).toBe(entrywithJwkKey.payload); } - if (option.includes("ES") || option.includes("PS")) { + if ( + option.includes("ES") || + option.includes("PS") || + isMlDsaAlgorithm(option) + ) { await checkJwtEditorStatusBarMessage({ page, type: MessageTypeValue.SUCCESS, diff --git a/e2e/e2e.models.ts b/e2e/e2e.models.ts index 5a988370a..10f910f85 100644 --- a/e2e/e2e.models.ts +++ b/e2e/e2e.models.ts @@ -70,5 +70,8 @@ export interface JwtDictionaryModel { PS384: JwtDictionaryEntryModel; PS512: JwtDictionaryEntryModel; Ed25519: JwtDictionaryEntryModel; + "ML-DSA-44": JwtDictionaryEntryModel; + "ML-DSA-65": JwtDictionaryEntryModel; + "ML-DSA-87": JwtDictionaryEntryModel; }; } diff --git a/e2e/e2e.utils.ts b/e2e/e2e.utils.ts index 03e16a10d..c3603b7b7 100644 --- a/e2e/e2e.utils.ts +++ b/e2e/e2e.utils.ts @@ -1,5 +1,6 @@ import { expect, Page } from "@playwright/test"; import { dataTestidDictionary } from "@/libs/testing/data-testid.dictionary"; +import type { MlDsaAlgorithm } from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; import { MessageStatusValue, MessageTypeValue } from "./e2e.values"; export const E2E_BASE_URL = "http://localhost:1234"; @@ -7,6 +8,15 @@ export const E2E_BASE_URL = "http://localhost:1234"; export const getLang = async (page: Page) => await page.locator("html").getAttribute("lang"); +export const hasMlDsaSupport = async ( + page: Page, + algorithm: MlDsaAlgorithm, +): Promise => + await page.evaluate((algorithm) => { + // @ts-expect-error -- SubtleCrypto.supports is not yet in TypeScript's DOM types. + return SubtleCrypto.supports?.("generateKey", algorithm) ?? false; + }, algorithm); + export function expectToBeDefined(value: T | undefined): asserts value is T { expect(value).toBeDefined(); } diff --git a/e2e/encoder.spec.ts b/e2e/encoder.spec.ts index 2478909d7..e9ea6e84f 100644 --- a/e2e/encoder.spec.ts +++ b/e2e/encoder.spec.ts @@ -13,6 +13,7 @@ import { E2E_BASE_URL, expectToBeNonNull, getLang, + hasMlDsaSupport, switchToEncoderTab, } from "./e2e.utils"; import jwts from "./jwt.json" with { type: "json" }; @@ -28,6 +29,7 @@ import { } from "@/features/common/services/jwt.service"; import { MessageStatusValue, MessageTypeValue } from "./e2e.values"; import { EncodingValues } from "@/features/common/values/encoding.values"; +import { isMlDsaAlgorithm } from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; const TestJwts = (jwts as JwtDictionaryModel).byAlgorithm; @@ -257,12 +259,18 @@ test.describe("Generate JWT encoding examples", () => { test(`can generate a JWT encoding example for ${option}`, async ({ page, }) => { - if (option === "Ed25519") { - return; + if (isMlDsaAlgorithm(option)) { + test.skip( + !(await hasMlDsaSupport(page, option)), + "ML-DSA is not supported by this browser", + ); } const encoder = page.getByTestId(dataTestidDictionary.encoder.id); - await page.getByRole("listbox").getByRole("option", { name: option }).click(); + await page + .getByRole("listbox") + .getByRole("option", { name: option }) + .dispatchEvent("click"); const jwtOutput = encoder .getByTestId(dataTestidDictionary.encoder.jwt.id) @@ -301,8 +309,11 @@ test.describe("encode JWTs", () => { options.forEach((option) => { test(`Can encode and sign a JWT with ${option}`, async ({ page }) => { - if (option === "Ed25519") { - return; + if (isMlDsaAlgorithm(option)) { + test.skip( + !(await hasMlDsaSupport(page, option)), + "ML-DSA is not supported by this browser", + ); } const encoderWidget = page.getByTestId(dataTestidDictionary.encoder.id); diff --git a/e2e/jwt.json b/e2e/jwt.json index f4705ab12..3e6f9147b 100644 --- a/e2e/jwt.json +++ b/e2e/jwt.json @@ -392,6 +392,96 @@ "privateKeyFormat": "JWK", "jwt": "eyJhbGciOiJFZDI1NTE5IiwidHlwZSI6IkpXVCJ9.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.ymupeGgBkXyS4CeWt1q_N3q9SIXcDVL-wbW0-7asb8jVvSjxaoXjiU3BGsE-KoUsTrwLJvLyNEAGW_JfMejeAA" } + }, + "ML-DSA-44": { + "type": "digital", + "withPemKey": { + "header": "{\n \"alg\": \"ML-DSA-44\",\n \"type\": \"JWT\"\n}", + "payload": "{\n \"name\": \"Eric Arthur Blair\",\n \"given_name\": \"Eric\",\n \"middle_name\": \"Arthur\",\n \"family_name\": \"Blair\",\n \"nickname\": \"George Orwell\",\n \"preferred_username\": \"Orwell\",\n \"profile\": \"https://en.wikipedia.org/wiki/George_Orwell\",\n \"picture\": \"https://upload.wikimedia.org/wikipedia/commons/7/7e/George_Orwell_press_photo.jpg\",\n \"website\": \"https://www.orwellfoundation.com/the-orwell-foundation/orwell/books-by-orwell/\",\n \"birthdate\": -2099242800,\n \"locale\": \"en-GB\",\n \"updated_at\": 441781200,\n \"zoneinfo\": \"Europe/London\"\n}", + "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIFMjALBglghkgBZQMEAxEDggUhAJjthJPvDqbRWNuef7X5sDe3w68W53+wWmH5\n23lBY3JxiVF5C+6xlFlw5wyv6MKOzCEP1KCja9MCSFf3QM/4ZvVcxZfQ/m3al6Fi\nFlpNIB04MYLb2i88knGYa2vMecSxAg4FZthrIxN2X1wr7U1PgJgfUxNLNWFZ4CLY\nRTZRWL0bVTcT064pVurOxyz+WrIRyi5DXdR8eEKhpUh9FAkZa22JF2UDuQOSCiOt\npBCAA25OAvLdxrU5JhPz5aZgBF29rBQLyGgi7SyEk/P2xgUvf65e/ugQiu+RrNag\n1TRG8F8IlF1aaoi8QmpsV4S78TmqrUGfwkZ9pdPnHR++bZkOiF2iK+icuHzoHz6J\nRGTTSj2Mz1HRAWKPzUjeKMyomStexvZLqhnI1ZuoaggF4nYH7NWq0brWw4ulGtxs\nCbbXdKS5RdZ+kruOOOj/McjnsmjOPrYSlyxbBogItEBG1BnCesygzTemf8LYPfaz\nMITgSiiqIRVMFq0hI2kBCn1NFCDV0lGmMdz4IttyVUO0JuwGc7NtBaAMrGEZsoWB\nDHlwDPUnagSLdG8oCT22lwkFbOUP9QBlAfAl1YYEp8VwdYhk1VgeMgpKoQB2I3LC\nA7Fgu+YIqbWPzPYOcfaVHfBYx3uKTcZ2xh1XklXCgII7LKYJ7pMEYoQ6QDH7LelO\ntYLM8C0HUpOYm82HYzJUCIbk2KOiSwKyweX0ddqIIqICHbZ4H85Ib4J+eLfcFOqQ\nAo/OP70CLVTOUqlALB+DZlNNWK0r4rD561kv37fb6Sm+/fEWdW/uPHV9Pf4UGzO9\nSZG1iEOhcXxgvDeW9n35FMw22CWVCq3N9mrc6mn6B7zpCJJwCQGQNiuwwp9OHnjr\npmDNVrjmW/Atinkw0/oDb9x9kb0T/rIbTpLZlfZX7zwIjbOKHKTbsGl2VgXLlDr/\nyCF0tda18mMuV0ZUso2LSB5JwcTWTCXUtYRYrNH+QftBYcQJy8r9WK8WWQUefplC\n8o8Q4n2tpr6Y2pZRqgjgwyzNjfXDK2DtkPdntQ0blGeK+3YLja/CVhaaV/vvkxLZ\n1hxTDoi3MHDSRDi493rdNdVeRZo1go/YdOVdY8/Hx1b2IhMjmg48AvRAwDzpLZOX\nYNtQxwnwkstXRT2sVUU9zpJJykKwwtKnPm2nMuum5df289onPxzXahCzQu4ovsmH\nv//VLDIjdBpsMuRiGFquPTRkYf0MCzgGB40pyUDPGIq34dx14lXiGmnKUqYAX/24\nMkbGqoiMqNN/crBkvpA4qSRahc5M4MdntJbJ8iMtg8gZ16ecjZxbRstkLBIY5yFm\n8pVbQ7tJTzfW75O3HCcvvWMH4DQpvx7W07Zb/X4254MdjKrXskA3+jPUCF9jNfDX\nLzxdguXpWixgWz2kfZK9FIkepfdn7Bx7qjfDwJ0AYok2EwjiBP5aLCYVViLTFbr7\ngOEbRLZK9lMHK59a83SERiPNyWXbvJOGr/e7ixTLMhjl0zLJORQAb+USfo8cCrtk\nxtidaXkamQxz4lD2weU7mCTa8irFBFB8r5MY3HjvQQU2Cj0ME0+0Q+egH6hiDJkb\nSTIbptQVyyTTCFLFMCXmeyWlZeH8bNP6qUEiEl/Cvlai7A4Kg7cPkXIqAzzNn2V6\nO8yFZxmAmu8gF2jSmoqGrdfCQ9a2xAz4mlK1YRMfYnnQFx/D+qOKCOsOcoMncPUO\nAPsWG5g+J5UdHruiZjt/5J4GEiXkqBYlMg1qqGzaAfjLib1F1uo=\n-----END PUBLIC KEY-----", + "publicKeyFormat": "PEM", + "privateKey": "-----BEGIN PRIVATE KEY-----\nMDQCAQAwCwYJYIZIAWUDBAMRBCKAIK7Up5YK2a6Q0+H5Z25no39FdDzqQzEdsLO8\nYHqTubBw\n-----END PRIVATE KEY-----", + "privateKeyFormat": "PEM", + "jwt": "eyJhbGciOiJNTC1EU0EtNDQiLCJ0eXBlIjoiSldUIn0.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.NlrB2KwhTOvnaOL0A8jYWUg6vf4AFJyuaWb51q5Ee7bgwa-8-sxv2sISHvSMd39Pzvycd7mwg13FuuOY6pEvXOVo7RPeFwcDr09L0r8622bXM79gAump7ckuaeE_aSWhrAevp3DZPMIuJnCOBn7ggnlpPne3CdixYx4DIF7r6jMixh7hxCAAleocUP6Y-FIdo50Mhm50PzXqeOC4hQGBrmnmSWsbfvKzSK4gFgFjQtQR4lh6PkqAOM6LKwEFJEmUnzBpolWJCkpmIwc08O0o5OsB2b7l6ARTQWJnHT88gOhKiyIiw1DzsIC1ilEbJv_e1D7CSnkoe5eCcBFUdlyUSoXgaevFpmgzKOm2131_wviO1gObwNK6xCBbVRtpyFszfSAe-lv29k14g5alq8-8eEwnPJB_OWI0Jnn4jQsLIwBdb9hJ4P2fUbz8ZOX1JhOSmoqYznXzII4JxesGO4omd3a5SDLo-bxwt5zKZkeVjA-24lF7zolnHprZS-SP1FS0gWXHvbRK4LVf_gyss2Zqu5SOKKUKEHFxqHr5Ez1oaUdnjFDi11dqJ4Xjj04jDkqZSf6HZd5dPkTNNy3LxPeoBBTtJ21lqa8Du5VWgCF-scb14DdNyqQhUDbSg8jdMOGAvv6KGm6N3gFpOptPrbh1BTzVIOAORFRrajnCilQxXnFXpfZFnwxejP-wajP1_bnXpusM44fBWmFvbY0k_kXl9yEQYsk_vcyD8qfDCXj5sG6j5q20pGgonhXcrehJgyLnTjbBbYNT3-eOcwHRO5K4epsEOBGqAr9TXWOPpe0GvazdJthxZB-_J7PQt3wWmhidZWFYF6JQh-iV-qVGtGhR6rUcnzo2s_EFzZg9Sptu4M6bJdGTWBmkWLYMj34vMGxTA1Fz2QhvQ1Z83NIDhLhONQqTKEk_wD8mY9xjtedwMSZ5xxn9XhOYXTAxSDxFUNksZFFIvzDmVj1DXJ-D1JxxcrIOLg1BQDAQjCpjYfieOLUKc11mKfv7DEEOVmubA2eDj0a_VXqkpIm6I7IDfshmFRhfdWAFbJ-eOFgzdbh_BnVXvqHDbzVn2rq7cen9VnBM1ennIApryjYJl5aQOXh4uKS4HuICe3a-44TnlPlnXLQ5gIm1TqObOVtH1q5uBFu6oyI1Tt7j9ymzH1YRBtsd9RhNGUHS5TrqgyFABs-Pc9Vjce4UfoN1hwsTCbD2A-0tHH6h3PV4CKTgQgfuu2-F2o-O7dWzBvqQV_ZuCsSftdpXHhXeUBkcN5n35KsTx2uvH_KHVySSgHwoSS9-sKzT6wkvsWekOB1u8VWEP7OVJ8EIUj20QjqtpWqNiVTU6A5v9ZvY3UjtLqzI4wifIpIS0uQnePpBy_3z0KQpy1hl0leIx9FStSBrL4caGVECSk2B5v5T20oCKCz1hoWY66lrbZJKNVDsucYfbvb0sce7eB4gg8A2K8RHIeJaGXZgytYOnhpZp08r3fy6ab51LPcdPp7S6WywIF-RJCW0xSxM9tb-LiOHaajaTr86jfzYhbVxDEg2iIA83qfTev8Wl_OpOu6k38Tvt6qgJ3iTQ0xvoVbrW2vjAvCRipujaor_XPCAKm1Nx2LemQf5DquTIkhUn58eRkbTzVccRUp6IZnChPjE7o6l3FZ3aijjy6ZJvdLnV9TfPIdfl1hxMsyXJPdaVdMQNjUTX3trg6tNPcDYZ8Wn09RCnd-KSyKFHSPo1n10k6lfgBFX4-Q-Rn5IzkQwxAABamYbeKmPVM7-ZwN7pz5-8EsbIur9oyC7EM9sozEsFTE2uwd8vGkKUuWmEOWbgwBns40kCXtKmv1AlPK6s-pNjE4woLEO54jiSbRkfUoMCloLUXdm8DgV0s3u9Ns1kt4GAi0kCfTcj2w7upPxeOPbCczuN25qU9ad-BJYXKx0yhePV277DsyNuVksn7mYNU4GIixA-LufdT0M-HcE6RwKe-STysstQJvuxRXwybcSF9y8-oP5fKvDAL2P6klik2cHFptSghNpwfv-qeqPiEc10jau4YxYWKRFJaqN48YIeV9ZlTxyHicalqkZb3Tos0MZcWaV9zZBWyniTgmJfIhpXUUIDMCQNioBeWTor8u6Kux4Y3POPskzobt71zpcJp4j5TfdDZj1iecdKRw6PbeNMiGE42XteOSS0ZZrwjqIUV_ydY26BF78OOBkqCg6j2sssgL9EMXlIdeYIQzeMWy10KSdRJIO2KMCouXquAVQEVXwyOoB1S4qiJrABZfV1WLCD1A-HCyWdjVJUdK5Efw8NrfZ664UpaTDC-Dyzk4dDat3wJiinPfPLjjcPf8jJgFtphl6vsNbZF3U0YgJEGYIg19RKnmrzR4mTXOcnuSMMVHU0TCv_fhECyeIRPI5txGkb2A8unevp9j4gDHxpKi5Jm9Ou9G3q4QnuEQ29MjBLwxBJl8duzYWS_Ank16gCr1Z3XY8XaE5nz0gNA7bGB-cHmRk0KTiq0AZ6wFIaoVqxSFZpdXeS06XLzGiJlz7tKQKr653difvdzhdKXBrIaQX8neCKZTsxfosxKB47f9JjIWv97N889GCdXWu45dQKCqLI0vfCWrzuPoWWInKL-XiPT12AFgLV5bylW5VEEdFIhfy9M-D_Rr8D5_5UpxSh9t7vne3UkF9Fh8B3RwOP-e93t8Ybkgu1EhA3Q-Dzp-PIV_lqTUIIDE9soHmGh-VGPP9R9IDa-yxmyTmBHKz8MqxFBFyZ6sgmJ4gL9NR8X-cPM6bCCIUaQdp4PGZ-kPgpqe1tvAfvo7BBlOE7SOI0XX-ehk9uzQ0hN6r4kDpFX5U7dpplEqSuM_gU_A7greVBkkB13HsgFNg1kRv4Ky4yjkDeAE9tD5gWjG7yu4ieS5LM5Qy5rmvSMDEuUjKwUqb_VPtK0CLMLwmtH_-oGE_BVkRUk0eZgwfwhf16zFMOq3IAHLqlwsnUxx6vsNUMQEKeFxvEVb6b8MJscGzENywkyHuprdn5cPZxZiozYKyoOSiJerHFa_WxQTxrrZVNF0RF02nmPWs357Dbl9RRYz_576d_aLhHX82HmLS1r8oLayafklgCkCU-a2oRk42eO5cNE5-HBMkJI27wnyUo6X9rLsZHCk8YYyUq67zAw8bNzo8QFVYZ2uGi5yjtcfs8jY4c3Z-hYyhv8HI1woNExonQFlbYWJweXt8gYONm6Czuc_W5PYAAAAAAAAAAAAAAAAAAAodKUI" + }, + "withJwkKey": { + "header": "{\n \"alg\": \"ML-DSA-44\",\n \"type\": \"JWT\"\n}", + "payload": "{\n \"name\": \"Eric Arthur Blair\",\n \"given_name\": \"Eric\",\n \"middle_name\": \"Arthur\",\n \"family_name\": \"Blair\",\n \"nickname\": \"George Orwell\",\n \"preferred_username\": \"Orwell\",\n \"profile\": \"https://en.wikipedia.org/wiki/George_Orwell\",\n \"picture\": \"https://upload.wikimedia.org/wikipedia/commons/7/7e/George_Orwell_press_photo.jpg\",\n \"website\": \"https://www.orwellfoundation.com/the-orwell-foundation/orwell/books-by-orwell/\",\n \"birthdate\": -2099242800,\n \"locale\": \"en-GB\",\n \"updated_at\": 441781200,\n \"zoneinfo\": \"Europe/London\"\n}", + "publicKey": { + "kty": "AKP", + "pub": "mO2Ek-8OptFY255_tfmwN7fDrxbnf7BaYfnbeUFjcnGJUXkL7rGUWXDnDK_owo7MIQ_UoKNr0wJIV_dAz_hm9VzFl9D-bdqXoWIWWk0gHTgxgtvaLzyScZhra8x5xLECDgVm2GsjE3ZfXCvtTU-AmB9TE0s1YVngIthFNlFYvRtVNxPTrilW6s7HLP5ashHKLkNd1Hx4QqGlSH0UCRlrbYkXZQO5A5IKI62kEIADbk4C8t3GtTkmE_PlpmAEXb2sFAvIaCLtLIST8_bGBS9_rl7-6BCK75Gs1qDVNEbwXwiUXVpqiLxCamxXhLvxOaqtQZ_CRn2l0-cdH75tmQ6IXaIr6Jy4fOgfPolEZNNKPYzPUdEBYo_NSN4ozKiZK17G9kuqGcjVm6hqCAXidgfs1arRutbDi6Ua3GwJttd0pLlF1n6Su4446P8xyOeyaM4-thKXLFsGiAi0QEbUGcJ6zKDNN6Z_wtg99rMwhOBKKKohFUwWrSEjaQEKfU0UINXSUaYx3Pgi23JVQ7Qm7AZzs20FoAysYRmyhYEMeXAM9SdqBIt0bygJPbaXCQVs5Q_1AGUB8CXVhgSnxXB1iGTVWB4yCkqhAHYjcsIDsWC75giptY_M9g5x9pUd8FjHe4pNxnbGHVeSVcKAgjsspgnukwRihDpAMfst6U61gszwLQdSk5ibzYdjMlQIhuTYo6JLArLB5fR12ogiogIdtngfzkhvgn54t9wU6pACj84_vQItVM5SqUAsH4NmU01YrSvisPnrWS_ft9vpKb798RZ1b-48dX09_hQbM71JkbWIQ6FxfGC8N5b2ffkUzDbYJZUKrc32atzqafoHvOkIknAJAZA2K7DCn04eeOumYM1WuOZb8C2KeTDT-gNv3H2RvRP-shtOktmV9lfvPAiNs4ocpNuwaXZWBcuUOv_IIXS11rXyYy5XRlSyjYtIHknBxNZMJdS1hFis0f5B-0FhxAnLyv1YrxZZBR5-mULyjxDifa2mvpjallGqCODDLM2N9cMrYO2Q92e1DRuUZ4r7dguNr8JWFppX---TEtnWHFMOiLcwcNJEOLj3et011V5FmjWCj9h05V1jz8fHVvYiEyOaDjwC9EDAPOktk5dg21DHCfCSy1dFPaxVRT3OkknKQrDC0qc-bacy66bl1_bz2ic_HNdqELNC7ii-yYe__9UsMiN0Gmwy5GIYWq49NGRh_QwLOAYHjSnJQM8Yirfh3HXiVeIaacpSpgBf_bgyRsaqiIyo039ysGS-kDipJFqFzkzgx2e0lsnyIy2DyBnXp5yNnFtGy2QsEhjnIWbylVtDu0lPN9bvk7ccJy-9YwfgNCm_HtbTtlv9fjbngx2MqteyQDf6M9QIX2M18NcvPF2C5elaLGBbPaR9kr0UiR6l92fsHHuqN8PAnQBiiTYTCOIE_losJhVWItMVuvuA4RtEtkr2Uwcrn1rzdIRGI83JZdu8k4av97uLFMsyGOXTMsk5FABv5RJ-jxwKu2TG2J1peRqZDHPiUPbB5TuYJNryKsUEUHyvkxjceO9BBTYKPQwTT7RD56AfqGIMmRtJMhum1BXLJNMIUsUwJeZ7JaVl4fxs0_qpQSISX8K-VqLsDgqDtw-RcioDPM2fZXo7zIVnGYCa7yAXaNKaioat18JD1rbEDPiaUrVhEx9iedAXH8P6o4oI6w5ygydw9Q4A-xYbmD4nlR0eu6JmO3_kngYSJeSoFiUyDWqobNoB-MuJvUXW6g", + "alg": "ML-DSA-44" + }, + "publicKeyFormat": "JWK", + "privateKey": { + "priv": "rtSnlgrZrpDT4flnbmejf0V0POpDMR2ws7xgepO5sHA", + "kty": "AKP", + "pub": "mO2Ek-8OptFY255_tfmwN7fDrxbnf7BaYfnbeUFjcnGJUXkL7rGUWXDnDK_owo7MIQ_UoKNr0wJIV_dAz_hm9VzFl9D-bdqXoWIWWk0gHTgxgtvaLzyScZhra8x5xLECDgVm2GsjE3ZfXCvtTU-AmB9TE0s1YVngIthFNlFYvRtVNxPTrilW6s7HLP5ashHKLkNd1Hx4QqGlSH0UCRlrbYkXZQO5A5IKI62kEIADbk4C8t3GtTkmE_PlpmAEXb2sFAvIaCLtLIST8_bGBS9_rl7-6BCK75Gs1qDVNEbwXwiUXVpqiLxCamxXhLvxOaqtQZ_CRn2l0-cdH75tmQ6IXaIr6Jy4fOgfPolEZNNKPYzPUdEBYo_NSN4ozKiZK17G9kuqGcjVm6hqCAXidgfs1arRutbDi6Ua3GwJttd0pLlF1n6Su4446P8xyOeyaM4-thKXLFsGiAi0QEbUGcJ6zKDNN6Z_wtg99rMwhOBKKKohFUwWrSEjaQEKfU0UINXSUaYx3Pgi23JVQ7Qm7AZzs20FoAysYRmyhYEMeXAM9SdqBIt0bygJPbaXCQVs5Q_1AGUB8CXVhgSnxXB1iGTVWB4yCkqhAHYjcsIDsWC75giptY_M9g5x9pUd8FjHe4pNxnbGHVeSVcKAgjsspgnukwRihDpAMfst6U61gszwLQdSk5ibzYdjMlQIhuTYo6JLArLB5fR12ogiogIdtngfzkhvgn54t9wU6pACj84_vQItVM5SqUAsH4NmU01YrSvisPnrWS_ft9vpKb798RZ1b-48dX09_hQbM71JkbWIQ6FxfGC8N5b2ffkUzDbYJZUKrc32atzqafoHvOkIknAJAZA2K7DCn04eeOumYM1WuOZb8C2KeTDT-gNv3H2RvRP-shtOktmV9lfvPAiNs4ocpNuwaXZWBcuUOv_IIXS11rXyYy5XRlSyjYtIHknBxNZMJdS1hFis0f5B-0FhxAnLyv1YrxZZBR5-mULyjxDifa2mvpjallGqCODDLM2N9cMrYO2Q92e1DRuUZ4r7dguNr8JWFppX---TEtnWHFMOiLcwcNJEOLj3et011V5FmjWCj9h05V1jz8fHVvYiEyOaDjwC9EDAPOktk5dg21DHCfCSy1dFPaxVRT3OkknKQrDC0qc-bacy66bl1_bz2ic_HNdqELNC7ii-yYe__9UsMiN0Gmwy5GIYWq49NGRh_QwLOAYHjSnJQM8Yirfh3HXiVeIaacpSpgBf_bgyRsaqiIyo039ysGS-kDipJFqFzkzgx2e0lsnyIy2DyBnXp5yNnFtGy2QsEhjnIWbylVtDu0lPN9bvk7ccJy-9YwfgNCm_HtbTtlv9fjbngx2MqteyQDf6M9QIX2M18NcvPF2C5elaLGBbPaR9kr0UiR6l92fsHHuqN8PAnQBiiTYTCOIE_losJhVWItMVuvuA4RtEtkr2Uwcrn1rzdIRGI83JZdu8k4av97uLFMsyGOXTMsk5FABv5RJ-jxwKu2TG2J1peRqZDHPiUPbB5TuYJNryKsUEUHyvkxjceO9BBTYKPQwTT7RD56AfqGIMmRtJMhum1BXLJNMIUsUwJeZ7JaVl4fxs0_qpQSISX8K-VqLsDgqDtw-RcioDPM2fZXo7zIVnGYCa7yAXaNKaioat18JD1rbEDPiaUrVhEx9iedAXH8P6o4oI6w5ygydw9Q4A-xYbmD4nlR0eu6JmO3_kngYSJeSoFiUyDWqobNoB-MuJvUXW6g", + "alg": "ML-DSA-44" + }, + "privateKeyFormat": "JWK", + "jwt": "eyJhbGciOiJNTC1EU0EtNDQiLCJ0eXBlIjoiSldUIn0.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.bnJWtn6ksD1jZ3wUVlNnMSKuorBsoN___gKGaRGhasbhn316qh_uLg9ehVRRJ7Hi94MZwsc7DUDpNMl8Da-DFBTrUg__TLv2Y79Gt-NtiaHAXyq_4t-Fz0cRtNNwwK-72dCjJf9N2Mv6HbIAPc9pPD0rFsDuoH42PeBc2D94insklnbBYGXBTLuTdQdsPMpnSddY3wtlgKFp1GJizG0QDPKj_ryhpc6ToBpw1J6_viWjCwJw3SBduh1TcVlCMHE_punYwiXuXyh0m1QKCmCHzsUQYo_PE3Zyd2VOuz5MbU43FEnWgJvJCLSCkDVXCFYIz4aPh8b7NH-LUJw_418GCre_j0jvN2OhsbFuHU0C3_3IlHnJvk4YHlNHDLOY7QIyZI7OFpwT6wgzPh_n9LNdBO6TyET5L-A2sOBfY3LLhd_0Ha2piGzJBCvFpD_re1eIxU3a1i8xbWZLXnUgD0pTDXbyvZNRMuEJCNIcRQvaWDVzVVnB3TH1ZcELb0lvcatlS1rTTU5cQQVujluCsvDjva5l8F9IED7G_ZKspQf7Cat8Sne1coqFu78hk3kZZRBOwH0gdWWZ8MAAH1MRQWVrA6bot32wRcIbv95OYw2yj9_vv-2FgVJQnIM4gfE3TWT4jsMacEjOd9g631FWkcrtw85aWg2G9UaA-d0Ihil61_JtfrQS7DWHcxv7sRcLc4TfTzULmVtqP2Ok1D5GdsDFWPP0vDmfd00qgR0OJ4-JtBHVHxe-bm-W9dSIpV10qICN-T9un8X0bWgtJOu21dJ8DC3mLD-Y2eZu9Ff8ElSpXbLIHmu-TI_oX4PZNl4wvtsA2kAMzX9ltazm95Udo07cahMlTNAdMQbc6AAnczFri-h1kyZSKOR3QOW_QnoaXwrwNRI1cfVr80WDd0UjRl1POPPiCluuBmPdDRxtr8BxyP3SBGXuKI13s6zFWRmQu5R6nXLGhaf7TWtIqz23YJNQyuO9LwdhP6AZkpp35vDpGIID15DBXZXHJvxMWtf2XZmrSP_5XdHoocc4sZKkIHgDXvcI3RfEPm4YoANMjjwTZDC38xfDdljuE6ehi0-OjXCh8MS2_sv8dqwaxlgmHmAzrIrF2D-xo8pFK-OQkS9C98dPsCkNZHwkjeXrwS8K8ErVX33j7c3dkVpBY1XBlwk7TNTFV6ua85NnOx87tuhQw42IhNYa61f7Vwk1EXG0kTMTb1UVAiBl3VqqoN8pp4v_UYHF1bf5M1FB9F579hf1EVzb2reJyNbVsKKzXznNJACM6LJr8Ztxaez8nO7sldU9x_zKgAhTPuDGkfQU81hL3OvhGNgK1HnQCO6vtMRGbkt_haJufnJIt6PFEFE2I8rjxTG1TZB3raIHUTP-ECaZy1PDbsYWbgyaOF8HwzppJOt4HBmJUKyixoxf8okKEEr5sLNow0TBA7JKA7mq3_z29mBydyaTBJRVn5cKRSUAwrNjFkLp7rY3Ok4QKGrFaLNXXwcrQg_nsg9E78lkQ9aRnWuhcUbrmtTLYqiTiBLtCqJ0FA6wl32gxgzfFI4K9r3awK7UpYawnMu7QRvyMQ1Y4wP5Giz5YlWbr7MUnA06u9voHQ5tiprOngBKjceonK360vij3zEzCL1fczlTX48q1bF7aDsGLngM-82RnOtvnONhmOfn7Vlqg0EtV3nCu1coNpQFgmKukliW8P7sRwC3Df5QHtS0yvuzANaii3qL8p6YuDI2EhBYgWSWyC8O6RjRAk5BZMeG0koWAGIgKVjoOltd4TpqYAjRdDlt4wxBM69ifYpvr1_pVOfQDet8xPIJTEv3uLrIc15XuzkQyABh7lhhKlKXvgdwgiXG791_dQw3u7wh8a6LX6ya461DKeyPVS5Q27lysDx5N34oNcLOyZJEKjJOfUQLpfl-_tRwd8EU_VyAXLslc76AtKf6mGdCRl-xkWR4gKLKZ5hczBNCuKyp2DAj10Rgb87S7u5L2Al_ZUA5QCRvH1runGXnn59mnMGbjAmx5UnHPn1k3yh-SAX8z8tyAsSk6FoP9OGJQDUTXWURP4C_Td4SKzvkBAxyRhStNHsqCRBBt0hHoMKDkd1exmFgxItG-e8Zlbom1vqBXZnR9e-ic13xlmOaNuz2yYTWkGfhwGab6cGZzzYujrAqXvIyN191AOTZJToRoBvPaMVFjAfQPqXUf6IW9hamr9PaE4TUwIawPCmY-bgOnZuPICdjTwUbmB_oghkHmWte-TNDq837FLizN4GsNivXM30GDDPchLs-DOjkJ2H_fvIGwAdQc9hOgwrjcmTkQjXoFaI6TqjD2zCXxnNghlQCb40m0pi3dF_KhmnNj2ycl2Uz0jg7XOIe-4fJdFGvFsRNydIelZ0rse6QlHulUmtDCb97YAmntbt63KJLLTQjhhFz3E7Ugj7AAHpzdcAGO93Iy27sv8aG29-3Rie00qsvvg4IO1XalKleHwJOpAR3gwHG8Sk86Jix-7jQsqMCDVAexMrzODeru-JOKZl7qXTSP8WDkNKwBoZZj4xaLyuzbNN5FGDKmj4yTmP2DArvrqz-A3DLgoUaYDOq5IyfcOWaWUVrji7qrirTikhlsgKgdTI6-8YTROAUEd1dXimdt6_msh4mlz7GCU3jqnfp1Yu41c8d9By7SrBpAif-R8L0PTFXV3HATzNwQAdTKRluLhHbeIs3nOshr9cLWkhUKBfHY4aCnecotUvEeiw0VJIitFYlYyLnWYKBk7v0D5lr43m_MNYzJNyzgUiPQyy8C6Unk3mwC7-pT-OnqEeKt2U2c-4HUVK00JoEKzPwyXA2PnYy_p-JGSt_6FfQwm_7YqgK0xYF7-Iave4yci0JMsXaQhtn0eg32Y1BdUu9MfluwxM04iMwo3kTLsYZCHTGMqPtsHnwkulZoMQPL8PFwb90gGiPigoPvMrAb9lNGcIgJMFAOFcDOLlHncNfPH5esuxVSNRwVJNE8D96vgA4SwYJb88BFT8ZlF5v179pfJLXE2YUcgmiSUXpah0-k6zlpq5ohb_pjNKDqmRFqtLEn5Ehp6Z2sJWpBzryaKrpefm-j0ZwCtd1N-nLq4UgRoQk87rezie1mreaycCSf9RG9AKQspUVGFR2d3mcoMjY3fn-CREnNUxPYnd8jI61xBIaGx1ceH-Or7_K0NLe4efr7fsoLkZtdXqJjLC1xMj4_QAAAAAAAAAAAAAAAAAAAAAAAAAAAA0aLTs" + } + }, + "ML-DSA-65": { + "type": "digital", + "withPemKey": { + "header": "{\n \"alg\": \"ML-DSA-65\",\n \"type\": \"JWT\"\n}", + "payload": "{\n \"name\": \"Eric Arthur Blair\",\n \"given_name\": \"Eric\",\n \"middle_name\": \"Arthur\",\n \"family_name\": \"Blair\",\n \"nickname\": \"George Orwell\",\n \"preferred_username\": \"Orwell\",\n \"profile\": \"https://en.wikipedia.org/wiki/George_Orwell\",\n \"picture\": \"https://upload.wikimedia.org/wikipedia/commons/7/7e/George_Orwell_press_photo.jpg\",\n \"website\": \"https://www.orwellfoundation.com/the-orwell-foundation/orwell/books-by-orwell/\",\n \"birthdate\": -2099242800,\n \"locale\": \"en-GB\",\n \"updated_at\": 441781200,\n \"zoneinfo\": \"Europe/London\"\n}", + "publicKey": "-----BEGIN PUBLIC KEY-----\n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\njVqKAAiz\n-----END PUBLIC KEY-----", + "publicKeyFormat": "PEM", + "privateKey": "-----BEGIN PRIVATE KEY-----\nMDQCAQAwCwYJYIZIAWUDBAMSBCKAIIgy1GRQ1cU35YZhQ9AW7naYyvEtNzXUGuhz\n/bE/c+De\n-----END PRIVATE KEY-----", + "privateKeyFormat": "PEM", + "jwt": "eyJhbGciOiJNTC1EU0EtNjUiLCJ0eXBlIjoiSldUIn0.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.dC_aE8jTY5k1iEm9sD1ktxHvw91Ey7zcXoAQgw601Hjo0Y4QU8GXCCjxHNFLgWTDIC3fxGHKILRhlpO2stJ0Y9Wa1uCez_IL5AvqnxJwTz9DPEc32k_nn0iIi-i59rGf44KOPg6qQAdLIW9b1fIaSziDzy9RqCjG8q4yFGc4rcIqoLO8oLbHR53l7fUFOqcxY1Hcu6Q95JnFSp3ABlcZSYDwrGDaDncXW6TdVinTdkwRKuulk6w0xW5aqNeZDKF5G3oz5_bXO2iUNJrnSzRX7cXdq09gd8HinoY8D5Qed4HyDEyhnvDr6a6Fr5EPCXpCtgbR7KcTogBs-Wu3VyXuImUvWrhaymOROyOZCH3wve824C7AXuk1GZlfQqzziDzRmvMgjgFo068279furhBINjYNCAe9xdE-tEy71t9AyIcUKVUmIpali285BH8rpr2KdrlB50aWBVxi6LrOnIL9td9FBhVO7D9EDHqzO68J5eNPGi2OD26d25UIFXfZM4ThF3i4XsTfmi0AwfoyiW-6SVogUH5namin4W6roeS09pDHLhvTjcpNukgd93QDQQzpi-_w3-4ZlMHA0ZZwhZmmyGl448DZ_QjcARUUuA9VGXvEdlIk3F6aiEdf6FfqUZBqpnSG_GVUooVS_WgUAPIFUP9HmbRxwblya2un5c13lX54dg_3p7z9GPuJDJ3MwE4DvMB4yqNVJMTXW1Olkci2htP3uFhg9uQWrdlx3moQ7O0pWULq5FeNVPbhNQF1W3Fy1zySGOftwHgvcOd10C30IDpNkOwsYl4GDbaIqMZlVfWZub8r35QxMtJdTIdLSh6CiD41VtxDwvflPattJXIWnvjm7AOLMG_kJaLF12jMPoXCYqfW0N5plH4GfxZce4GC89vuByUsnWJSXjNvb0VW10O660pxVfj5-fFDz6U9Hsn2dS7ctzLFeoWmWFpFnG2IOWWZCQWFfQUZOUC_OHjlAXQBg-PYEpWP-cbBCmLlnHLQ_8XZdXH22PN3aVP9tUmWln_SnFQLS7FENF72ziGkwbUXfVItp9TZ8JgdSje8EZWtwwDxqGN8rV0ys7B5aI-6dGhU2q9BSSPunW4p4_llk3zxEap8zYZLxsL1WaRmut9GUYtuSaD8aXClL8AXveWx0NlFdoq23P3o2yClPAHZwPFoP2YVo0mFM-a8bYbjOABgg-7R0_27BpD4_klJ9SbPJqP6mXzH5_SZ8quY6GiqXPEyufz5MGLsa1Q5keoKe9pIJBF3aqIQCIe96Yjn6jcFKShIx4ZrrHxQQ0fUkGJ3WnRm1cgtD0VvfnhkNrIokik-xrFRQYN_fPRYbYqOdjYRvodaiiucOqpUC6IRBKn7_1k1LOGnAlwltIa2Gkum_MCIWnEcg78P9FDBvkUxZJB7xvFd7SzmHZ7IjmRDoD7DypEW-NoDgvHWmNUUmkKIBYGkSFOKmqrF_UqV34fGcfEbzqxKJaDExgo222j_jMkrpRRtlHmOGGiv4ggfNuoszxEXeoLkevITsPZgcuE2_Rs3zh0NUPJMQF9FQt5vx0ZOVDATL8_yMMcESjV7Xl-ugir7sjzB3sGUauxeYlOuc6ZA8ZXjTIiAuij-nBpFUtY8aBxI_RGn_KqTmSp1M-w9_VdhqmDe2AcnhcJ4rwuCo6oG46i9l25Gx5l3Tw9BAuz-N3geYaNvAyCuPO_Czk7zTBgkfLBXvGYjoDwai-3bCgXtOGJKepzyNYQDaZPiF-3SxCDm2nTDe_W_v7ME3NIOozjI9RpxVfJVVQ2sSHHM3tikjcF8QSot6G0eq8Jst6INVKRtgv3uKTw4TmpO_Vfru0N2lBRXCKS7957qk8QJFVPrh5ss-SIIhSFLRwPr2qjhc7CEc0yVjHStHTv0C0iF2t6iaFmbM2YMHWTh6hHDTvi1azIM3WqPlnewZDW7CwZ3Ch2LAsPTq8EojT-IO13rFX6iIS6v_rBhhZ1IsSvOu-q4ZHuLPIctDxDjDXZ6exg9_JOjMBMSDG7fK6mGZwvAogkIDn4UPvUGKpLjmELKqZ3u8P9yZ8zoJX64Y8CRdjad3L2wngOqmpDdIkH__04GyrTTe3yqvuoqxoZyJXzmER_fDXFCtqm6xzCxsZnHU08eXQ7IeLoAq1Vjewpmk64PUF09759thX9xUFSqmfgoXE2tn6NZEopu-83hLGPbjPVmdNY70LPB2_qaiLlE_w6Bp6lPt4Y8LY14xs13ARbtYnqgP-UdQzCgG5htJ9sjbRKNIsgH7-UG5ioOrPOcWUdVB9agt5O85kpi_I3QOyWXL_M9dloMP5oc7kQzIkNOJ8KczF39jRwzbvhUejjV3O2Yat1x2JNJZigcjnsdUQFOotFSf5XwBR5K6uk9Y1SIIL-7QYZMC-BolW4PsVfYkIefLm0fu3DBeHSkXRbrVFXz4N9cxjqMRPRlqqO02ggvm0Isze5YslYyeKJlfOkvblA8Wa5yJvjypF4LlJ7_GMydGs8ZVTpSFEd6DIcy9jWrW1eVu4VubKdcgaVZ_a4-TqZbNkcKmDJiUAmVrbw1_SbF1h887ery7P-Tr_XZgrdeki7ZDLfibuFLvvt-sodJNeU2ZUcywi_-lzxtArfK95gwuzPqAaGVkBM5fEi5KLANsUKOxfG2a_beOqzO0r9Qcjrkt7llq7It5pAqTSOzZgCNZapmdAt0mI7cb0GbmZhvomsTvXcwdHdBnhnEuRgqpQlZqnhPC73RxVX87IapLpP9erCigVmysvMtc17xXKvLbrg24y7CAShSdBVyejAbVvVFlIzc1HBwCecimyIqsNmhNknImNdk3Uml65JxSfzpmCojwi5V3oXhux6KJYdSRztCc4yaFbF3qX-1g9kyyRjtaxZiC9gcb2s0DXjv6m1xfdK6gS8IpA9d7ouW_3A3bZoa5Olcc94RIGLH2ZsuuUh7PqXwMoSg1XGWOXfJNFKjux28dVd6BIqlX4jmrgq39b1P-FTsmYD60HNM_L-OhJfBTUpBOQrC1EB01Trv36S8MJlxomZ-7nbEfg74bkdPEpDW91kyh-0g1H-3VFlDiOM9K91fn4XhJvLGkjXnAbK5Cy9tz1ZWC1SjhrjTKpAxSFIVDULduS1-LDtulYB_wKDwfHxevjvOJGxjDRw-KaIeqpj1m6xnT6PCI_6pon3_fwCmfpVTNFui3tDt8-0LfJlab3qrEOwkuqE0LA4mUCp-y4pUUPFSTGj0rcEkpPquaz-M27IaJsP1pXmOHsMMrFRzZa4RXIG5L65gqbNAknB5LYZAcOTd4iXqNH7TIyyHKVwY7jqP1NSJoPhSxKM9UUXL61L9CUKfjtrwI6cbFUPvHaOqi1WRkxse0p20RsgWf90e_qeMZ-EiOI6RUvqv-LGwOgJzvR2IKiubC1Pp8nA4NH05fZtcH7PThL9oBN6GK9gHoBqCXlCxrTuA6g1xEpp1wDf9fk_1BLM7y52uLU7uz4CBuSEbludxvKhK7UStE_wKcjbHXU1v0u2XEBEalVQqohpEeHLSJ2aq04ZiWbhDQCI53SAWfi9bz4GznPnFEY5XKK7NvdqsGZyEuvyeuQZRHxmfCUDiUv39dFXZr7vtOjZmaLgG0xhnVPYyUzAaux4iSN2OP7UnQCDqDqUoKDD7DQgW1Rv3stEaMnlS5n44-WP6r33DyA6fS_e2oxFZuf62Cz7OzZrVpocKv7rKZ-9CnMht4Ann2sM44HIEVwDvHbvbu1X8e4QLwbeCCUc0_HHlJFad_cTRSHfuNe4wsHoJu4rcyQyM-N4NGf1Ip98AM5qSLtQ57KO-mQ068oM410cMomcjfnIiB8txw12haO70E9W8oGObUD9KD8gCixYQd1M86YynMQEXfAYRHznbNm2NuRQV2ByneJiXnoLEC0Hi4nd4DpBUWMc_57JkjjB97mCWsY4ixsxEtRLbanXARGfM3S457wuuK5XGFTCItvREOCZHcvdlnouhQsei1JiiwE0E7NOXoAoV7qN6fC39pokI8x6E6dxzlNcRY9QSaFwTYtWoPHAaOuOetewadwaBT4XQCc38KLMicBfziATlFRSZk40XJm1KHXulMskncxjkIHG2Zw1UGx3eUrY1LNTO6RoY1tqgfb5EoEevRvUtTnfrJkdr2_ERGckyfjbwCXpwDfoe-TfDR0uXnD0GvDzEjYstoPm2n5FQMtNiJrjoy6wDOX6HbD4oTRLwSEPnsx63UHlzdf_9uUTjA2RT5JqmyTCSyADSRqKHsw_u_Hze63m8yCXFigwpTmnmXlY9FqLM1Brc0qu-Tkw0pzMFLa-BbiXuS-3BT41l3qsJBvDpPFnRqTIqMUu2wQYNQEZOXnqNEhUgMjlIYIqow9vfGX2e1AYOPn-oqa3nHEu1vwAAAAAAAAAAAAAAAAAABQ0ZHSUp" + }, + "withJwkKey": { + "header": "{\n \"alg\": \"ML-DSA-65\",\n \"type\": \"JWT\"\n}", + "payload": "{\n \"name\": \"Eric Arthur Blair\",\n \"given_name\": \"Eric\",\n \"middle_name\": \"Arthur\",\n \"family_name\": \"Blair\",\n \"nickname\": \"George Orwell\",\n \"preferred_username\": \"Orwell\",\n \"profile\": \"https://en.wikipedia.org/wiki/George_Orwell\",\n \"picture\": \"https://upload.wikimedia.org/wikipedia/commons/7/7e/George_Orwell_press_photo.jpg\",\n \"website\": \"https://www.orwellfoundation.com/the-orwell-foundation/orwell/books-by-orwell/\",\n \"birthdate\": -2099242800,\n \"locale\": \"en-GB\",\n \"updated_at\": 441781200,\n \"zoneinfo\": \"Europe/London\"\n}", + "publicKey": { + "kty": "AKP", + "pub": "t69qTlRcbwoiniMSZ-YMYc_wF7OqeppUavtF0u57Y0Qr8ihvReVCBWm4e5Ck-Tp3PLRy_0DH8ymCr9KnsGC_FhlPoXsu3ih9zFf5fBK5D8enRwXxPig4CFXp_XIuNi9ZGUvispJ53I6G3wKhhCkkeYqkxtkr9k0aqXoCSaMABFyvaQ3mNBfAF8h48Uzoq1ODth_GsBi0Zy_dbrPD62VT0jJ3DNk1jxvkUl-U7O3cP5sgIyh2B9QXFqLxj2GF6YnAz8Afa10XiXcdvFn5d--NielyAmpvidE5GWY24SgPKHoO7QUjjpze3EulnocprxzFf_GNuCXe63gvLXDfQ9AFsv_mGC2jm_ZUcFJBQG0W6eq_KHICr7qKuHPBeDkoxpUse_AYRWHZMKuQ4-zu8ZueN-PhqRVpYj2Sgx8mzZfM1MO21d9cEl8DoCon4hpB3O6XP1mDwNBbHQM-2NHVHU7wzWO64rLP0vbaOoNNiCADvlmwTvmUwpvGWpSXbHVlth5AbkVZgxbJPISaXrEfiIFs51Nubks5c9wXVKtOnT9x_Kacjwe5DjwFFNFGfHD5hKAKmw-r5Fjwy8BTw1WvukOSOGTfB_Si2X_hHJN7Rx24uu6haBzKNM56x7cyTxOyG4yhDJjOX-I1NlktJKcEA_FAHqpYw8Uvu8JNG7TL_tGmqj5XgrECAn_mrjlPrfBj2nK-7wclHwaFOVsXEN9mxZSLzMKYXq0hqaSU2rWE2aO-yvuigYR_WLTJlyV6-zfXQX3q8C01Szc8Dl53V5u8VBj7a9STxpccB2Lku1z4PGN_x0MbQy8VEq5NaBLEcgidHxtoeCjdblyXLIWpeFkdwmdaR07616qYNzh_fcvN-JWEpKQ3oD2wBFtWD-113pDWOkntpks2I6IsaXrGKvkDiHn0feFVJqf88HIxUQ8jzi4eGrIqU2Nh0sknINX-JNvtTJ19bz2nlgXOepHosU7LTW7ltj5nzwEIEduvBZSDl2wbj4fToNq-MS92L3Q5LD12v6n3J7qGOv05FdgfpZo1P3ltia0589RaVhXlVPm19cwMvw8u5l86lkCI239_VTgUBKlw6dL_C1x2XBSnIFVwEuONmH9Uv-ni2rbtOPHy6WJvcEa5It8cEc10oFi8xfrF_sv6d24PWvY2surFTPTqVHROon5FFYclLDFrN5TLKekCXKq99k7xJvJ6e4bJG5BzlDR6cmlso8wh7EhUixpaHOoOKz82_MjaNU-BVCXbyS2wR7dKDJ1O1pP8K7fO2mv-sBvZDbvmy3nB2b69YVqBCWI9mT_YCfHZxVgeDpSVlf8DpSLDzLr30UDRJt8vQpXNHpdeMCmTF62FF0M7GcYb4HGxWqRDgAQFYSH7XTU0lBsRIGD5QtDaxqdCccsIBYNakQP8YFGuxrAYD4OhVQ7V_TY5kwGFBNPTnK8YJL5Mg0b2zoPk8EQrTcGj5ur6enNP_fOCybMU2ZrHqREGHGtksiaVD3CgTFx5AgZJNLONxkehSZ6tTvxgztoyXzIZXPuhWkLV8tB_A-foZcAOamG9k-ifWnTM2yfN1bm80HchTwAzm7QDP_f6edQsiGF3nQ8ikqWLAWrh714xL_1mdajxvj9xqgyEG8Ho4Ddih7heacfCcOYbnK350_xm60o_p-J1QQraNyxLr1V2rkvdMhUFcAzqNzZZcS8txQwrFI5kTG5_aKy_P84MeB2xCL_Y2xmrHpm8Gkhj-EGnIPytQABtsLFzkjM2o9woD-O0QmWqSvZrgN065AgrxFUAzbmnolKuIRiBNwu44_Fa6Iucqi7vXsN41dJYYA5P8bVYCQk3_SXvH6sCdh2cxlKAScKXITak2RrcflyRGdP1D54Qsfm5zaE-l1OiFdCm_0UI0qqBCQZ2yiSXfgXyV-p1R6cuPNHXz_GFna3QpL94mI_pPdg5MhAF4yWdSGzDWV4lBdVel8XVFWUTWQ8HSYRbAtaKlMhLheYnUa50jDK7X1Tsw5ja1cV-sFgsNH8uYj3d_vc0Ntq9GQLYkOduxknDlyheVMAIGpyS53mVPlABjzFD1dArOBT3HTteI8k7n1MAefvzdm6o6A3a8BN9VrPxApje-LQzDWiYY-p3sJIXVevycHC1lcxmxoXUjWkerX8kGKIYaqN1CI693HbXFZmOLSv_TnjBzjSVdSiuTTxVwgTRPxdkG8pQ6gCUkgBSckDlZCJkjOGR_PNOr5sV1lkw0xKuYdLRWGSN5XC7bL-EtF5tGP2k2KwsI82Kr5GH74lWJTgpxiRkirqxzZPJ8Q9Rb8znVzo8PgubNTZ5qHsgjMtzRkW2JazBl75nQ6RFfebHYfRlMw512a0c8RjJ2n3gadVJvXwXO2nq9l3au9Ux7HCMv4G9zPmJo5PdKlRpiYK-fHbbkIgEWl32twmX-yIy4uvvucDVFerpIuxYR7y_FY_yWwv185E_OtwEEp63UPUO1KR9HjS5cql35BWY0mRHBUE7LDQtuE4BLFUQyUyFWj_HQ8xWNpnbVwKCU_l8S3iGJ2zWB42Yo_T19X_-JrVR2y8ONitNyBMZ-lWqJW3_bgtx_iIhgI1fb2uJCEnfOal_pUWNWooACLM", + "alg": "ML-DSA-65" + }, + "publicKeyFormat": "JWK", + "privateKey": { + "priv": "iDLUZFDVxTflhmFD0BbudpjK8S03NdQa6HP9sT9z4N4", + "kty": "AKP", + "pub": "t69qTlRcbwoiniMSZ-YMYc_wF7OqeppUavtF0u57Y0Qr8ihvReVCBWm4e5Ck-Tp3PLRy_0DH8ymCr9KnsGC_FhlPoXsu3ih9zFf5fBK5D8enRwXxPig4CFXp_XIuNi9ZGUvispJ53I6G3wKhhCkkeYqkxtkr9k0aqXoCSaMABFyvaQ3mNBfAF8h48Uzoq1ODth_GsBi0Zy_dbrPD62VT0jJ3DNk1jxvkUl-U7O3cP5sgIyh2B9QXFqLxj2GF6YnAz8Afa10XiXcdvFn5d--NielyAmpvidE5GWY24SgPKHoO7QUjjpze3EulnocprxzFf_GNuCXe63gvLXDfQ9AFsv_mGC2jm_ZUcFJBQG0W6eq_KHICr7qKuHPBeDkoxpUse_AYRWHZMKuQ4-zu8ZueN-PhqRVpYj2Sgx8mzZfM1MO21d9cEl8DoCon4hpB3O6XP1mDwNBbHQM-2NHVHU7wzWO64rLP0vbaOoNNiCADvlmwTvmUwpvGWpSXbHVlth5AbkVZgxbJPISaXrEfiIFs51Nubks5c9wXVKtOnT9x_Kacjwe5DjwFFNFGfHD5hKAKmw-r5Fjwy8BTw1WvukOSOGTfB_Si2X_hHJN7Rx24uu6haBzKNM56x7cyTxOyG4yhDJjOX-I1NlktJKcEA_FAHqpYw8Uvu8JNG7TL_tGmqj5XgrECAn_mrjlPrfBj2nK-7wclHwaFOVsXEN9mxZSLzMKYXq0hqaSU2rWE2aO-yvuigYR_WLTJlyV6-zfXQX3q8C01Szc8Dl53V5u8VBj7a9STxpccB2Lku1z4PGN_x0MbQy8VEq5NaBLEcgidHxtoeCjdblyXLIWpeFkdwmdaR07616qYNzh_fcvN-JWEpKQ3oD2wBFtWD-113pDWOkntpks2I6IsaXrGKvkDiHn0feFVJqf88HIxUQ8jzi4eGrIqU2Nh0sknINX-JNvtTJ19bz2nlgXOepHosU7LTW7ltj5nzwEIEduvBZSDl2wbj4fToNq-MS92L3Q5LD12v6n3J7qGOv05FdgfpZo1P3ltia0589RaVhXlVPm19cwMvw8u5l86lkCI239_VTgUBKlw6dL_C1x2XBSnIFVwEuONmH9Uv-ni2rbtOPHy6WJvcEa5It8cEc10oFi8xfrF_sv6d24PWvY2surFTPTqVHROon5FFYclLDFrN5TLKekCXKq99k7xJvJ6e4bJG5BzlDR6cmlso8wh7EhUixpaHOoOKz82_MjaNU-BVCXbyS2wR7dKDJ1O1pP8K7fO2mv-sBvZDbvmy3nB2b69YVqBCWI9mT_YCfHZxVgeDpSVlf8DpSLDzLr30UDRJt8vQpXNHpdeMCmTF62FF0M7GcYb4HGxWqRDgAQFYSH7XTU0lBsRIGD5QtDaxqdCccsIBYNakQP8YFGuxrAYD4OhVQ7V_TY5kwGFBNPTnK8YJL5Mg0b2zoPk8EQrTcGj5ur6enNP_fOCybMU2ZrHqREGHGtksiaVD3CgTFx5AgZJNLONxkehSZ6tTvxgztoyXzIZXPuhWkLV8tB_A-foZcAOamG9k-ifWnTM2yfN1bm80HchTwAzm7QDP_f6edQsiGF3nQ8ikqWLAWrh714xL_1mdajxvj9xqgyEG8Ho4Ddih7heacfCcOYbnK350_xm60o_p-J1QQraNyxLr1V2rkvdMhUFcAzqNzZZcS8txQwrFI5kTG5_aKy_P84MeB2xCL_Y2xmrHpm8Gkhj-EGnIPytQABtsLFzkjM2o9woD-O0QmWqSvZrgN065AgrxFUAzbmnolKuIRiBNwu44_Fa6Iucqi7vXsN41dJYYA5P8bVYCQk3_SXvH6sCdh2cxlKAScKXITak2RrcflyRGdP1D54Qsfm5zaE-l1OiFdCm_0UI0qqBCQZ2yiSXfgXyV-p1R6cuPNHXz_GFna3QpL94mI_pPdg5MhAF4yWdSGzDWV4lBdVel8XVFWUTWQ8HSYRbAtaKlMhLheYnUa50jDK7X1Tsw5ja1cV-sFgsNH8uYj3d_vc0Ntq9GQLYkOduxknDlyheVMAIGpyS53mVPlABjzFD1dArOBT3HTteI8k7n1MAefvzdm6o6A3a8BN9VrPxApje-LQzDWiYY-p3sJIXVevycHC1lcxmxoXUjWkerX8kGKIYaqN1CI693HbXFZmOLSv_TnjBzjSVdSiuTTxVwgTRPxdkG8pQ6gCUkgBSckDlZCJkjOGR_PNOr5sV1lkw0xKuYdLRWGSN5XC7bL-EtF5tGP2k2KwsI82Kr5GH74lWJTgpxiRkirqxzZPJ8Q9Rb8znVzo8PgubNTZ5qHsgjMtzRkW2JazBl75nQ6RFfebHYfRlMw512a0c8RjJ2n3gadVJvXwXO2nq9l3au9Ux7HCMv4G9zPmJo5PdKlRpiYK-fHbbkIgEWl32twmX-yIy4uvvucDVFerpIuxYR7y_FY_yWwv185E_OtwEEp63UPUO1KR9HjS5cql35BWY0mRHBUE7LDQtuE4BLFUQyUyFWj_HQ8xWNpnbVwKCU_l8S3iGJ2zWB42Yo_T19X_-JrVR2y8ONitNyBMZ-lWqJW3_bgtx_iIhgI1fb2uJCEnfOal_pUWNWooACLM", + "alg": "ML-DSA-65" + }, + "privateKeyFormat": "JWK", + "jwt": "eyJhbGciOiJNTC1EU0EtNjUiLCJ0eXBlIjoiSldUIn0.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.ycLftmYG0laDXuDXtNJN1Im_sOvna1_oMPxw-J3eUYacdeIY1A6z80MgXUzUlP3ZpUuoJSAfQKr1fBELhk0-_u427pU_-kri6Pc_MTCtRSRziclbBoB-LztDfsbBpJyL6GVVFlrV_Jm97YdbY9n2t_0OWUmF-NTimjlJBNqGALesbyLfR05fGB8rqQgZ5-qSq5u0Tz9dhwQcO19zfdd07EFPnaJ82IEN-JylykPhvzgrliSmVWLRTtxws1kcWL8mVSFivW9aAjOctsCqrSdwUmvoSJsMrzycu-52TXJivjSlOBu4MntllwYJ8mLNkiyZq4qWMloCeTX9rWKnnfMU3edSAgGe_JrClJjTYaMYgl2KY7JosHWCdSGDBLPAZ_GsOehu3IUpWKDGBAFvkJelPKIMqHnXo5V-Su6l0y_4RJovP7UtGK1mp6NigX5RXolNeCrQIPcIN_TS1LICNciTGOc2nZqmQE5A_nyi0xoesIpQAZFdSlDrzQtQJMH_HYvqOOFu1refJVoRLSDXlS1nYI4aagW6NLnCM9ob7KaEv4aDxdzxyOSskGcCmuD8TqgaJltESCqSLp1JCm9dunRuR6SgcuOhuwV-YVruAqM9Hn9Z00BIG-EYALFjV-1sUKUkQUTWdH6AZ3J9w5yC25CgvJDBSzZCRnd-j_GVG1BEPwWduouCiOX2nSCEjvFCVcJvcLJfpb3qOEf7i0NQm0EIorvIBJFZSKGZglDrpWuyUwlqYJM6IwDSUsSq29oe7l_yR13eBtRjtUo1iBWErLeKYhfWPE55HFKkjaZFaO40vaGXTH3TC95e6OA-gDXqZPen04XtFItxhXCX-1HRwzIVWg4DMlOLctj8rNAGYMDaepHHIah60nfeS7W5Uznsx9_xL3oYN-TmUOWA6PLB8S1H97Q8KLHlgxSIhZCiRvJztoSQfgays8NnuSnJXs1JeXpjB4zCuJyaUH0oTcx-AP1_zMAj64WkBdlicbPH7GLuU11VAQTkC1KREtKKfIAT__uaw5ULIwgG1YnFT4E2Rr1ZaxbX3aQw6XpeySF_9g5j6t4CFOUz2RwjYwNoDLY6zpsV77OqHHgp7HtTfZy8NiO83ssKmN8gRIi7RD_TGfLIqZQTo6f13qNWDSNMqXvahm4xwq3hieIyWoEw4ANgJTnM0Vvt-VuDE4yY5x8O-aeEzb0uzI2TpSD5P12Q2z-x_ak4FukMlLsG8Qmo-KM7rRjkJixxKj6zvDdhSvs8D4BaMj5yz8cL8YKhSFgfn7PPjXJS3A_aox_3pPv5jobBf0d5tt77piAeELQccXXoLOqUArqE9NWynv8-dqp0qrDlaFbTXcWvytM7s8xkVEk10XqEAMfHLxBwxcM7Lk7DQy9hmwnxTB2y6NeaQOTs1VhA10reQSOavZ0aLf27WLOPIuXwpc9C6ryBBi82sbdKvqJwTp8inBlXlWpgjhJawHcKuRkh3jkZCyzvYzINqm3M_NHAThOrokkRVxUJ7My8gb422WXirJDfGgzIkmX3zkT3NzUWJZ0fp3khTTQxGP5Y5AishydQs99hc45D70it_dpZ6aGkKmQDsMXSqBLLIMIbFJ0jS7bM-dDSmqh1JETo174DAJqphDymOyse5VGxO-0Xjj7f_Ku5cp3FWdaixqLnyRGu_OmEK7DB6F5xj8KY8ME0Yic58TQult_QTrH29wab7ka3o-IQr_dpKR58URHZL_Ba2jxKgyT3e3LKxJ9Savp7BBCZ7nTYNQ7N504H5Jzeamyvie0QeGW7mgByF2ub744BzuH_C9tO8JGR_71I_XgsHDaE3djUnYz7Z1z_OQHGLgb7EwHc_vyGYFuSv8dfb4wmBuMDi5rFshThiODMhM2kGOyXSNkAGwmsqDYPcxKcGx6P2W3t4Pvzu_BQocsvT5ITcqHQktC0MQhbrouqe2M4JzvFi7Gn0nKER_CJERXXYVP8z9qG5T0c0iYB1cSEJl5T0zTv_FeQ96TyLOw8ZndvqgSfCSGLlrgb6cIZfv7nWHrO0oS_hOs68zvIuD9pNsuxjGkSA-RL5AOibtvaaaJMymG327shps636xfSg-gv4lloDiLfu5uZHYWFtMm-6AANgqlYl7PnaX0lNNeR5W53Zexc6C0nvAwlQ9JdzkHXiKJUKMA6AmQXje5aHK0_MKROscNQykKU0UjlSMKhHC7iifE5QYYVeI4AepA2UIqH4JY3-Dtx6HIjwShLI85gcHbU8hKHSQ7nje6DB2bBEzdis2hvKURrdqVtiUIzr2z3TenTvdTxBYNF0LxtxXLi0WnfRDOGorYccO_6ggzwY0PEAnVb2UD9noKzbmeLffvx5sMhg3zBQ931nR9_IDBi7FcGShwnTS49355ZjbmNOxsZYD6kDv4fmBv6roFCkmf13rdAkjYwf_iKtQP0_wd6yy7HcJrBMkbHZRHdE-xEBbm6i7pEdwb3QcbgS1vemEvzKQfQZm9EI2PayyTI0LkEMBRiyTCmD5AWPzVlmGWE9QszUqJq1pwnsBPp7WZ3b41Y57hRPTY0-TDWQ5czWLEoB4s0crzZGNWHjS8RxiDg_lkwBWme9DrE1ZLw6cDZJE0kSEeMh26zTGzl0cMqxNhOChnKuuT4afGv_WlMWhVkqaMYT4Jd6KPkZjktJAp3FkzBPxR8IZJfO4B0J--L0vFbGixap9jt1zV8BhH1pNSqQaSNRNmE18WHFJ0IctP2Bx-TVswViZVgjn9kwyVCA100aiU60ei6SMR5U5L1T67F4i982YAgMd-pvccyVwBc3BpmaZGqHdR0hZuVcxjexnDyymoNSGJujBO1LL071Xv_k3-T2miZmY9WY_t9bCPKzGog50slrQtTk0DtfryBt0wyqt-9OxZaxeYL2Fs0lTYurgz3OeSP87R_p7H4iGPJ5ZMwKkKWi3BsslmUkvV5B1oIjKJNqWtY7Nn0Qv-B6_zbwRwwMWhuyQp4ZdGVKAUkcExIvnrGttSaT72qmyP2VcRwJo0sbljisAnYBwdFF1pHglqdvABzUAoUf8hognS3N4KYnHdV57mvgQkbe4QuksTAP989xLK7lfHYq7VwevpgtWpx5GnDLKh6Z6IpnGlvKMJwJqmaDBM2X_rme3qDpJWbca570RZ45WPu3_0QP9SteGdSVfWlppOV6Dsu-VW0h55Xi2aNlzppmEzylOi4rI56Fyif6CJ8o5gDrogVP1pSSAQZPyNR5ugkkI6bpxWLjdL0_HHx5ZJv0iscJg-b-gJzXE9H7z5jkZFHpETjvw27ODd-SZCdx9xBxSTXEW4L0213d2C9jsvy4z1IyAW0kA5rtmlfOnI-kdu9L_7_IpQkY2HFnJScZ-E4UbYNUYhQG0QamOwlTMpEFgo_vSyys68BIYZCZfR9FsQ0tpbsBhw8lfwyAp_fXEpZC2qyy8_3r5gWmeHFt4Yq4J2Lfxer5hZgoc1kcyaGgj38mu3R0rfSI7FII0VqtZNGaMDkKDCELTKuWQxr2W7N8S2fXyOAigTaIIdVXlpNfpItCPPR8LRRNj8jJpZHojoIvfESz13cj6yd6HKvT-tQrXpnnuv5Fi3gtFzll91brTKLPNB2OVcGLNbdUy1aqR_2-3ny7Z4dj1sH8qDVxeela4B4GIpkuSeZbJl9yNIev61HuwjhRS2f7n4iYzM5pYDleM0b-YO5ulo8wEh0PPdHVeSXEA7WNWtGpR48wAmP7d_yGG0I6SQbPz9UHXVeShNoiFbHAIBqAw6-n5nZhDdG3SSGWPs45Kw4DMaWZgHnk11YYpv3sMPKo6rccKIZxLAxov74io6WtiKia8aX7YXj1gKR55k5rHcR0ShafG9S2P-DqhZcSYJpdidkDEiZI_M6fRlGe5zXLmBUs4ke6K1OA0WuAapsbSiIdWpUsrH1OEpy4R5_eK5FHTo2kBJFivX9rXbx2cqKJYZpMOX3vD6xCLd-GtnvIPadQ-F-Swl_vEdShTFxZAg602EnH_Dp2xuyuxonVGW_ts1SpKMzNRuIEqwsMAOWr7ZSyBacInjlDmc3YnODBYBOsGlFHeyMB9cxPXlSEXruhqJOXtN71tobzaKasvrgg3nC1qY1EUhKcjxFOfygxsxKImQd5peX_VnrLGeZVB4D9nG8EYIaCojMheeq3Ey2rCwqipMaHnw5TwmbiphBtRhXIaIZY7AjIcMnYS4yj5nnbIvhcmOis97q2j12wgTFJFgqPOLZ9ZJTrQEFQJht2K3DYVxl7KDwNMOc4GANtBLw-7WdyFd13BOphLrqJcgLYKC5LAMK_tMPletHy7V7VnLUmeVd-Nt192KJjzAx6sy0Tgu9JYQigp-g0z1GYXZ6hZypz90RFltihoyS6gI9UneQ4AUIJj1OhOAeQGh0mAAAAAAAAAAAAAAAAAAABQ8XHSQp" + } + }, + "ML-DSA-87": { + "type": "digital", + "withPemKey": { + "header": "{\n \"alg\": \"ML-DSA-87\",\n \"type\": \"JWT\"\n}", + "payload": "{\n \"name\": \"Eric Arthur Blair\",\n \"given_name\": \"Eric\",\n \"middle_name\": \"Arthur\",\n \"family_name\": \"Blair\",\n \"nickname\": \"George Orwell\",\n \"preferred_username\": \"Orwell\",\n \"profile\": \"https://en.wikipedia.org/wiki/George_Orwell\",\n \"picture\": \"https://upload.wikimedia.org/wikipedia/commons/7/7e/George_Orwell_press_photo.jpg\",\n \"website\": \"https://www.orwellfoundation.com/the-orwell-foundation/orwell/books-by-orwell/\",\n \"birthdate\": -2099242800,\n \"locale\": \"en-GB\",\n \"updated_at\": 441781200,\n \"zoneinfo\": \"Europe/London\"\n}", + "publicKey": "-----BEGIN PUBLIC KEY-----\n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\nQKI0RpBWHmr+mWuNEW8xetqt51TJcw==\n-----END PUBLIC KEY-----", + "publicKeyFormat": "PEM", + "privateKey": "-----BEGIN PRIVATE KEY-----\nMDQCAQAwCwYJYIZIAWUDBAMTBCKAIJa7HpfHmbZgKurNlGuxCzEfz3LsCiy2gx5Q\nLM+9z9Qx\n-----END PRIVATE KEY-----", + "privateKeyFormat": "PEM", + "jwt": "eyJhbGciOiJNTC1EU0EtODciLCJ0eXBlIjoiSldUIn0.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.0AAV-VN_2hP87KAj3XI64-CGKnyN6-Ply0eoOPD6Qd5_2SMffii44e50iAmoCCtYjcH8AahpNfQHHuf_yEaWWWdofWbgkHf2aTPwNgBThI9ZRD_BKXoYo-YyompBHfYgoHnqQt9CWLn1n4emgbw2jsb7U-NjDvOySug_89q7Ju2fCLj-GQG_CzbDVx4XrIZCyuxRhkCG8xecxr4FFYLZnmeFVo9msu_bRncLTsuaYXYo2MgooYLC3RJYF-SCfmTMZTLvFroM562_RNGL9sInYTe3ozNaeb8b81OfJqdekGJVDhfpu9pfqJUB51sjMqloGYk6BpftaFDKbOOvNZADTkDGmYmahZhgAjP20HdQxYLOeU4vPjy5Svk2WAjiBxVGewNxJjnrr-FRdJxGp63MFOLcd82JnzijX5TG_sCni9tEcqRbOx4sm94T0BhxsOBmdJJf5SHhtiLBdAH_9CO5zYQJ8yqx8FaDzWrqPFRNkFcLlUa7NmSy63ERqTI6ibcm_uGo2QI4UdjB3JjST2Y9ibUa0Wj0ejDENDPS7Rh4_q93973JbEHpXKxtPEtHsUfT7Ijde5eQjwNbOHLJIdVOUXuyO0MI3Ipo_rneEYkYmfdBQYi_lHi2RPB6N-g0jomfuK17C6V6Dnspm-s9y61i85vYx_s_oa6o8zKwqdVxyXUpEiyrLAAnLGghZfDY-dpbNZBeL8dG-9yZ90NYLmmOBufQRDx_Jg5Vtsm3NbF_MEX0l2j9naOXnTwbCU_cncz4x6BMvBrD6RuED_ogpKAHfbi9-JyLa4_4T2GybHr-DRbvuEHkk0_jDfWnSEWc_vj6zZ-vAeP9mYa8XpM_Am36DvRV7VPrUbkJjG6brybzPbt-qWqKuIZgM8Ip33LN6jNNBjH6PpJDwsNGwCqiGs3n6GsTWXeJbf16oFGZCC81coYqlYbnSTxhpulaRGf6CgAui2aCDsnXt_qqqZ8ZKCA74WtQ8LeFwGaX9OXkWL3Rh5bdEAPtDJJA-HK60RIiw5r1TAGQadBNNhMmtv6fmkyWHerHrvSpVzWseHtHZX4q7KfDni-XUzMYu3r6wbYLxgrK_vdawp-DCxdLROYh1Q55wlOjJoHP4Kfo7kiCf-gqaPNa4YM_7UC8lZn9ua1npoJC7F55mw2AOy1y_v4eMoXYsqoypMQAj7ILmVp64bS1A1IYSIeV6BriZwwsAC5uGDJT8nsqxsE5wmrnDfZ16u5vAAf1pk9csMrzscDwwHlc5ZuSYc2LLVdDsOix7JCzVrnRpEgL0eaKHVjC-xnnas3_oT8fWBhymY4-aBc-4WiZ4U8UR-zZNJrBZEDdLlZobawWPZQ_-x2ybYC1-pw1JqCc-UMDjcg8U2VQI7BPhZPDnccEJvduUyh7-H4JRML44h-F-jI6rQvuP8v6YfnJyqhIjuIaUcEuM0DfJkxUMCe2SZfZ12zXke_MK_4dYrtBKhMolhdXzwbkZmjfSOYS-OMv0b_pipEu5FhOZSAHyns8Fv75RgzHk1rc30sTIdi7GlRdz7NIyzSE-Y9RpWPSdkoLJPTvk75YJjZ2XoaOexG7ijZQrjsp2GtaTlnF-7w0u-N5NR3n1D0WsNv9yDD3xky7nJ5sVY2oRSbguV7ODiXzX9zzo3bQkLgt6F62C9_BkeVBXO6G6UBOYjnZjhWf75w1btx0EWjpy7GKcvnORepPofabKvf1sLVoh96iCyfJd6Pels1DkDEnH7aUzT37yiFXzqW_cvQunM_R8yBedzb2ZCSNtxVzL7bDKkLZqMww3X9IFXGsAS0U_491qHt8biFsWG87aHN8UZIQtwpFuu1gDH5cDSZ-St5HvYHyJruw7mn74nGZIrN8MUFQS10rm76hvhbnt4dRtiLVdm8RxVUUz2SzcTz-Wf9VouVlFiDeiAxSkJkAJ4Kn4Ic5EPNa1NhWu_f3TKWBNXHywTOJMT3NFNEVqQXc2mu_lAuvRo52e-1nMQA0Ryg4QktOpBnAr98_F03dsx1l8UUHab95Z3CtAveRxhPxN8GRnOHJ2zA0Rlnt79hUwOCMi_nq0xKlAWPBFSLwVMNkZFftIw0MajJOO6R-KSzOW_zFuxHqJa0PrOwrk3C5GVNvCZPTLX86Bu1b1vWBvsztYU0zgHS6azalI4L73nm14ARLwynthqwF8iwMqwtbI8i1KlMdCWYBb-thQqBXypqq_6bEhT77HQ1tnW5WBzKgLfR8AXSyUmw-e_qOFhHKY0IXWdjcgA9sDay805qauYZFi0Q1HM9kSiC4aGo65Wg2jUijOhy-DRTYcvb3dQE_kYYoN2DaMeOxU24CcR45M6LIzMzv0gGiW2ptHdEclRgLVtThaJ5x0OKMQiBEKVoWkD927sx44YstmFsaeAHkZXe57wNSzl7MHAfFjmXi_ylzHaRxUl_fmdixGLSWSkNLWCjX4Neq5MdC4TLLbHYKl7ZdxkXgDOaTQVjWaWsWsQoiFWIBBCt2X-TFxWu6s33AjI83VTWnRJwn7P2oyE665uYc2Ab5Qfa9UZTAMrjL1cBuoZR-DOTwyQNJ9iF_m4sSd5VebNvAJWrZgq_sMo19FkxmpAzkN4R5Q2Ziwx5eCaCrt8vqTD2l7TUoqmCeMPgmdITfnnEfsA4CR7rmsl3HPYw4pjgyJJYMtS7Xyy2gUO1bdy6H5yUzc5mJ1ygEC4Qk7afFVdizWcVN_oTNYSfFs2jA5Pp3krT_jEGd7e4ie4LZ5SA_HgPpmeEMrwjTKSMOB6PQ-LYqLryFLqq0j4mCjABtHgOaehkH2w_A8yIL312idhkRx5yGKXgWtvys3-oXG2Ir9pJIDuTJduTEqOVyA0abin9xc-e4sk7uDTDjGDQ1mAhfmQukgG636COVMJozcnYkxxnld27tiCskFIE9DrfS6xAIUB3axIVsZQCkAFH9Lxe390NTehpGgVermCeFDTB6AWZsNN71lkC7W6SzIfCHBptdRLnFnCm9cKB8fbRrZoaWDVGq_3HAzzxF_Jk5NFpIHPXXPSFoRZDvl4dkdH5qi5KswYwNk9P-bGE5v6uswbu4UFaRaoNQuK5mUHZlMxtOR2k1KyQA2HAmO3BWbliBiE43s-eLvQlx9kSIsTirKOCiI-gl6OAZLDO0BocH6Bxo8G948cQNKRvfG96B4_XcKSn-MVp5OmB44A-3Vzzb5vzGq_cv7vSXtKHohT_tgBLyL50EcjfP9fEdYSF7TJFgOBM4YZmRNa3xNSxojDn_KfepAxTekALwNMyFRv-EhNBvrpJoLe_VLHDQHFg9xHZ9g-M5R1HSSEvcbGNtvD_DDyzo7UCU1DzX5QbjS3hul1PwShEzNAv9xyYYyujBi7xaPkuRignq50KV7pa9KBPbYFPybqo4VeN09yM8zT5mqDa3H1Q1_B-QgR_ow3LrHYp1Ry-W3YHl_gmOfCEDuDDE6xBIwFNaK41c50dOuh67kcQ5AZrTYgKfROcLUsU2CuE9oF2f5pYM5sT2vSMqq83Ns4lY6fyh0qwcMitnxrlT-YDcO3JCFe68hRbs0KM6cKD5I8ztztjKYvEIPk4-jt6WGNI85Oe5gXRxP_e1zT454ALGrLrD5r2Cpd2pbOlM3rRr5VhYhzp84acAvjDxktR3dzfc6RW0nahM4s3sB4NqxYTH852r7OOU41mCqzO2ZMkUy7WbZDd1FlYynJ0-zq8-N9Ar-riiHTMmCl6ft5pk1tdKLId9Z7SaqAlZlsWvxDdpiyhuAZofljmwZjNxGIWl3zgB3QkAz67F8-KufxnAUokn2NxcnPB6CT5YIp-64IWreHt1BOKtIhfJXNP-UK34_hWI2lBtEzUbsyGHvg3hDe5AMNyYibmoeK5gjbq3z1KiI8Y5RxtwzLRNgi0CheG9IQkI7jXml3VXWoy1Z9-jAzJ-WMOHpHzeFutZN84y1fWqTYtw79VtWq4cepH_HutUcWl4Y3_OXbn9Fmk4zQ9PaKckhBuU-oESEMvWBKUbleblNLxPgpHdXx94Yr8A8kK0kuO80nFRxQTYcRWXwVS0KAavcxx013Hl694wCpvzfxt_Vm9-5oaB10BxHcq3E7B0MkwYktje5CMrB8JCrFfBUGBa-BAmIf9LlcFBjJh59fqtVLmILHftxnddD6qODfj3tBg8btn630UUS6mkVp3kvxjxLnaYaTQGIk9qpmyJR57TGRqWST4pSUJVUkenkcXoyJNXUH9BM8Xb25xHJyATNLeg30xXiAJLKBoTN3nVdV8j7JwUTvDm2GJZyt0TxP_1pZsnO-_Q-wR0qGuoisqq4i91nd3ij9KixMTCLz7J1NXuBPEowr7RDcK0n2TOJl6BlhJjnUpu5OMFu6XFTPOUGrc-wezNmcPEmD3Kv-MSURraeSKi8wvtLtCGuNgNIOu04mC_ZDpIpt7J0G1Rnced8mgZtGHxB4rm4r8r7i1qNv6QzhNop1ANzvKe2noavv4PWKmJTLz7uZ-slGXYqjk4C_abl4pmHqTqtImrz0HhSqO0QXMY2X2HscEFEXJq5HGC6LWwm7fTB7TkqOW2YTxKO_JcM47xD4z7kRIc_aPHP0wy2NBtnC1FhhuXQia34ZWPlbasl-V7-NsbA8CcU3H_XQX4y_euyyaNRToZvSBUQYge0YjjLK59rb8eo6GVQu2cNdS5Qt3veJqB9dCCqNQDdTCtymYdkp7vHuFdbR2dou5NfTXshpxW8N2nQbKgxXI5vScOuGszzY_duknMneGJbpSDV-snV4v-pDrucHTM0bkKNUo0ZYJfog96ohoYdUvrxLqqs4pkVck6hc9_basqOomS5dtfk40t620WxD2bGwEL1MuxWaFjY0kl-GygODwFR1VshZLrtektVY7d-PpSBtjxnNzvqaLFMhyvO_dtBiV0Q8nMiipS82Um-rvDDOK7FLDLieyJICLQbPH8uYFtU66vCweCz87DqDZJprq1gapgchiZt4_TkOVR-2uHwgMhi53nubuJH4WT6Vpx8LrIPmjuWVnXEykd4DMmlq2VY8otZUOGac9Afc52eNtGk35EDy6flexr9fnILIjtJzFrAKjhM5Y9ihlt9GOV87eDjZHzZkfhoaOhgCXbMITFG6_yISlPHG6kiZc4V-0yllHYtBTpq-rpEqHsI8rNvzhlAyaDkxYyfiFZWdL5H8Ulr6CnLgGCQxFH0xL8x2JjxN8wY_Oqs8dafvFQWasff8sTklkBgCZEkujh7SxE_mvTdwU5pg18dIf-sq-2pj7ibEMTJ3lBkffNUmHSB5Tvh0T7Jb6Nhj82zMLQX5CMLIMvcwcwjtXj4cHiNo9J_008-_QdIrhN_U3y987f1xaLqCUExujDapwwkzd-yAlwdaVbxgPLv4oYYL4oW3sPASyGl-IJRamZgACbBtDF943RRRjxRYA8TnLxIwqv65cDszm9RkLVcgeehvqq8pTDaW_hyTtzYle-ZSlw2C-IZXR0GgOU26C005QH3VGKs4RO81-Bh6z0bx2qVuzMeO7uX4jHUyb7HfSRzvoYiY6QTtxqaLNWwueABTPUxDV_04WsFksTpbTSZDphkTSD9CNKn8hf3iEUCgBG_CjyOclcdxn70r2g1DmZ0dfnNAVcKRPW7tvr0MwdmcT1YM0NvqBvZlo4OmYa7AhDFzMv1xQHXj4vB7AjktilGB3u0y7mRUdJQPhVr40sSEaRAQ-NtIy5iag_V7Jz0LnfkORW-d_mrFH13h3afDw2ENelH3I6N8xTxkA4pUcEMBwK_YnJaI8G03CL5sA4fA38H7Y0UNLMkr-gwYpPkguz29VJhpHeQDmhxWlVfCQIkXO_OH4ODXvoZtl2Egw6Asn7CPYY6NzsuYgD2gr9VIQ84WOAfzOaFeKvwcPKQ8Y3rKpB0kZQcmDJx6UPc-GkUmwq9cdXnT1wfCozfQ1U0uYKVLotlaCazpkC6kPku2lnITJHgLZQWXeI_XdZ7wsQD2VAkDrDLE1a7Rt52HHohyWeaNFAqSEi5RPZHIWiORlDIVjrFOyT-9elySv2MNl7znWYd6X76VRnCNWjx1pnqAUKb_0W1k-f6ZVN6UYKNUGs5vgGFSIpOEF-vPkCJFhykZirstXe7u8BD12gs7ja4PQ0QUdkcNLZ6e8TKTM9iJy2w9HS4OHn_mmMMTk9UV5qhYfc7QAAAAAGDxskLTs9Rw" + }, + "withJwkKey": { + "header": "{\n \"alg\": \"ML-DSA-87\",\n \"type\": \"JWT\"\n}", + "payload": "{\n \"name\": \"Eric Arthur Blair\",\n \"given_name\": \"Eric\",\n \"middle_name\": \"Arthur\",\n \"family_name\": \"Blair\",\n \"nickname\": \"George Orwell\",\n \"preferred_username\": \"Orwell\",\n \"profile\": \"https://en.wikipedia.org/wiki/George_Orwell\",\n \"picture\": \"https://upload.wikimedia.org/wikipedia/commons/7/7e/George_Orwell_press_photo.jpg\",\n \"website\": \"https://www.orwellfoundation.com/the-orwell-foundation/orwell/books-by-orwell/\",\n \"birthdate\": -2099242800,\n \"locale\": \"en-GB\",\n \"updated_at\": 441781200,\n \"zoneinfo\": \"Europe/London\"\n}", + "publicKey": { + "kty": "AKP", + "pub": "KkoQfrENvajTEhgmkFYF9eiZWPtkNsl91rU3a8lJyMtsQSjThqzTBWLsMTKHkUjLCVtHiUW7KfTv_yQvoHRxTCSY4kV1PH13nM20SnAVC4tj7-AwBFYe65jY4fyE2xSxHGZlJllDKe__qIY2egriWvjSU1bl4P0yFXoxpjCzsn7koJymWcVv2RL9Y8UhvOiYv_8RivWlmd6XREacjfMhmZ3TyGT5W6uiA7yQrV9_PRcZ9jJk32G3_yAhee_0nc3BbQGsE2id-C1iRMhPK0NTZkaHCMYmOmO9tXCitFOzYIAgIq7ABZ5D1pKEkY6J0DiWuRcUoJuStyutZhLiDOKadWUgTW8DdMtuZv4jmOq-KovdoCahl3_emlbfUnpSq7RL6h4hK4mWP91Zr5D6vrzt3Oc3z_vb9g5VeussKW0HMUXpT3vYJNSkD8YfrBRkv3OVw9k1BQeRyr_8CyJ7UaHw3uyRUOqpuHcNGDx0NeftU9G25MW8_quUceJqfl8BQyfn2Eq0bQB9SBMEx5tfd-I2JyEgpw4M30BAxiog_n6AQoV9px3kDBFs9U90u33Xa_SJL72P_7rH97Ft4PzsCE-PW6HWdmEJSFQhm2dyCBsm_HBGooAra7X2EDkS9V7L7QmzMko2VE_zzbdZT11QPaXsk0pSe3Xfb_fg9mF18BumQU6AZ29oln6COQV8sqRGyc6U6kkuCjW_EhplXCMcEUdvJDbG-hMz5Hlzw1OmBV6AQMxTK6qbuDi_IsOjQiV-wcbJ23e3mzAZz3HddqTXwVSu8Rd-qgzOow8Gjgxx7mSV2Qos0AccidilICnQPZjlf2I1tiFzGqcNbmo-Z4Ef90K18T4azXfaOTxXqnh-rpqOj-9CNtW3JxxgHHBi5WzMLiLN5kdi5dDvdfMN6IOpb5L7wZBUzneew8_W5Xjg71MAM6iR9eq0o0RMrN0gv3cl8k2pb4iFDT-mgTHg_qTGplmabigJs3fN7OtlwwroKWZRgPqFxDtiioYvZzRZInAOWv1t1yFbjzX97qAaZX1o5InU5Lvx0bgZOtzwCjuGKluH8o6s4UeWgRsmoh2G4nSUp2MZQsci6V6IhBTjixBIHF23vgR7SySoHNjSuJSUU2MKbNHZHjkdv1qOZmZzdpFQXA_ZtyY6PnLjvzS8IZyzbGh7vcssO0HHksAYyOmH1e2Ax-2P2mctCk-XgPpKUO8TJDk5lLUTnDv8cNLaKUHeHdck7ZREfC5DMRhZibUitHvxEgTifYafOAxmLOqV2ZI81hCksGZaN6juEsOxcyK1AmCHnFFcx1oJQGx4JfnUgV9rjLAlHXAiH6y9e8ijsKqEuFDAcULEtIDbY5utTDBDVCZ8uVs_Zo8-SEAYjHOphpVYsUqBk7zqKJHToSKEBKL3bbMDYgMqhXXn0Q4VW4XL-Ek4d5JCVo4noCBLMFDldt0MtfigHdT7G7I-TTkjUgGFX3orFQVcVQ15FupmyrTQnPrd0OCJuEI3xvBlOsJK_ueT5JzSM5R-3kkZkiejkFB5vPYHVcTLHZ0JQ5Pwu2R4osAjpW5iI0F0boALxmePUsrQir-zA1OiJax8CggE6AZa0oWoEJL534dK5MplU9DoM3FEjA8EZPhDzwalswe1zrJq8BTPKzbJJWdStTZvTQ9xKUnWKf_S8YF9TpPU5djjaM1GxZCTz8FFL6X92bXqHTCALrdoUEQeAdhdSYbXEHg6k4iWwRJqhHo53YN_uQBt3yabxW8uEvcuC3PsGzNczcrAnqoXs8gtU9DnHSHJVdwJ3-jZ3wyiN46u2gk_WctWKsXvE7FkFP4_4x9H6qdUGA3qZJL7Q69EVqg1yBrt3AcvuWs1wbIWKjVYTmFNELJ4yVWHq1c_Jt_2ANlwQoALJfetJXu6TLxalisXCuDmXZtBrm1slU0JQcBiG5HBO5poSOpYJt2unq0JzyRNmb_dl1WyuzY0belIPulRUTSSWswfQRBLvkY6euWCBYPZ-pK3mfnX-VnxOoz7nbJQvHtq7MvNt_5y_0No-0ulgBbFSLZG1LspN96xVVHyEUaRtmXWPc1fyMvSETzpsPjRBCIQVsVq0dLzzWqUohPnDF6WaspRQl32_iW00Qgr7gufdDhYIII2lsyRbhpbxoPQsjM7mj5G3CB73MOHnzTci2ETAVv8fmie27NQRJjuLvWzLJ5jDNwoIqLsDRX8-Zvzd9XxS1ioKjDYl23Cji-xORp3czobjzpHTLsFlF77To7Zu2vdRSa2-ae9CcVQS4EFGoSf6QX2IaAL5uUpEbvpuUTcO03oZLfQtdBErmccazaAbyHn3S2oYo5JQ9b7lWe8hScFy_Sd8_MxIGjVWiOXJdJmvuxcIQ0377jwsNalk7G8yaUJQ4UJFZdkVjRMA-Q5m06JHmR_P2RJ4CebAU8Y5kg2IJKW_RtjNSJcgMxUK_v0xb7F0upUXbpbSxdnjqoBITl8LH2Z_i5x434N3iNKF_JFqhYd6xD5SJJwCy_hJTMIo7vU7MByfBCY0y_zLNSm8xHuAbjAO3mMkeu4pMA2d578RhKAPT9pwSDYmh9fUbVqRQDDyX0kwPB9Ye60DdePfiAZPW0TxCHwBL0PUoMm9QX0LT1OkKZbrH-iqdrbHKSO2UFwBkJn4q85VA4yOSae4tzjL68E_ZgrU0AxRmASM4_WHbZSDbkwmon7IfzcuYCxMvU94Z962eVbkeEsu3GtcYxgzTJMqgjG0CeiPzHOUg9l9l8wF8on4DIBKhTzzwirvEJA7G4hm3-WVX5vVEpnr6kEXPEZZtHeHKgeXKDhkwcdE4PaRqRJOSJ4IqdZBx192jpZnBGwy5G3M589Pf838vEBr46XEgxR9s6OAsP8W1bYLy9uLYjeZBZWLw8_0dDDnNcsmt0ThuU7zV6LiSIFiWWgxFCicv53vAApZpQnfhmVQKOojPlO4MLPxh5Ndl7M2q4uOjkCOigTGMTHAsMgtJUdva3HriJiHD-Zn56qYZBaQDOzM8Oe-dCtTUiVUkK4NlmLOpItAIoj8uzux56QE5mLoh_4eeU4gUFykSB8m4I384Dhmg9Nl20whKnanQRN2ydmHa0rcT4tOXTLC5YT1nWvlV2SCYjFsJQMPny_rjCCeI04xXVIEL7wYabae5rwWVIMAexeCGbOD42wPKxB_HhBoINwUkXeEpXCqriRACImR5E-GAqH0CO90mkRxEg9FMWvAOEZv18OkllC_7s0VF5A4Q4OQk5LNhZ2FMTiaJYFMyqQWOzSD_W7x8b7BMB9EWLjAGVEqZm2z3VN1OpbXsngG3kjq5b62EDhtRKXQ3AGcBJhLH2mKQ9zwt5trpKO6RPk-1k_3ynT-nZ3RBU5LWeAPYlgKqP92w9DhORXJ2INLK5XL_KWql7x7BiokFhtnYwS0Gjt_qJdexzN6RUVyYtAojRGkFYeav6Za40RbzF62q3nVMlz", + "alg": "ML-DSA-87" + }, + "publicKeyFormat": "JWK", + "privateKey": { + "priv": "lrsel8eZtmAq6s2Ua7ELMR_PcuwKLLaDHlAsz73P1DE", + "kty": "AKP", + "pub": "KkoQfrENvajTEhgmkFYF9eiZWPtkNsl91rU3a8lJyMtsQSjThqzTBWLsMTKHkUjLCVtHiUW7KfTv_yQvoHRxTCSY4kV1PH13nM20SnAVC4tj7-AwBFYe65jY4fyE2xSxHGZlJllDKe__qIY2egriWvjSU1bl4P0yFXoxpjCzsn7koJymWcVv2RL9Y8UhvOiYv_8RivWlmd6XREacjfMhmZ3TyGT5W6uiA7yQrV9_PRcZ9jJk32G3_yAhee_0nc3BbQGsE2id-C1iRMhPK0NTZkaHCMYmOmO9tXCitFOzYIAgIq7ABZ5D1pKEkY6J0DiWuRcUoJuStyutZhLiDOKadWUgTW8DdMtuZv4jmOq-KovdoCahl3_emlbfUnpSq7RL6h4hK4mWP91Zr5D6vrzt3Oc3z_vb9g5VeussKW0HMUXpT3vYJNSkD8YfrBRkv3OVw9k1BQeRyr_8CyJ7UaHw3uyRUOqpuHcNGDx0NeftU9G25MW8_quUceJqfl8BQyfn2Eq0bQB9SBMEx5tfd-I2JyEgpw4M30BAxiog_n6AQoV9px3kDBFs9U90u33Xa_SJL72P_7rH97Ft4PzsCE-PW6HWdmEJSFQhm2dyCBsm_HBGooAra7X2EDkS9V7L7QmzMko2VE_zzbdZT11QPaXsk0pSe3Xfb_fg9mF18BumQU6AZ29oln6COQV8sqRGyc6U6kkuCjW_EhplXCMcEUdvJDbG-hMz5Hlzw1OmBV6AQMxTK6qbuDi_IsOjQiV-wcbJ23e3mzAZz3HddqTXwVSu8Rd-qgzOow8Gjgxx7mSV2Qos0AccidilICnQPZjlf2I1tiFzGqcNbmo-Z4Ef90K18T4azXfaOTxXqnh-rpqOj-9CNtW3JxxgHHBi5WzMLiLN5kdi5dDvdfMN6IOpb5L7wZBUzneew8_W5Xjg71MAM6iR9eq0o0RMrN0gv3cl8k2pb4iFDT-mgTHg_qTGplmabigJs3fN7OtlwwroKWZRgPqFxDtiioYvZzRZInAOWv1t1yFbjzX97qAaZX1o5InU5Lvx0bgZOtzwCjuGKluH8o6s4UeWgRsmoh2G4nSUp2MZQsci6V6IhBTjixBIHF23vgR7SySoHNjSuJSUU2MKbNHZHjkdv1qOZmZzdpFQXA_ZtyY6PnLjvzS8IZyzbGh7vcssO0HHksAYyOmH1e2Ax-2P2mctCk-XgPpKUO8TJDk5lLUTnDv8cNLaKUHeHdck7ZREfC5DMRhZibUitHvxEgTifYafOAxmLOqV2ZI81hCksGZaN6juEsOxcyK1AmCHnFFcx1oJQGx4JfnUgV9rjLAlHXAiH6y9e8ijsKqEuFDAcULEtIDbY5utTDBDVCZ8uVs_Zo8-SEAYjHOphpVYsUqBk7zqKJHToSKEBKL3bbMDYgMqhXXn0Q4VW4XL-Ek4d5JCVo4noCBLMFDldt0MtfigHdT7G7I-TTkjUgGFX3orFQVcVQ15FupmyrTQnPrd0OCJuEI3xvBlOsJK_ueT5JzSM5R-3kkZkiejkFB5vPYHVcTLHZ0JQ5Pwu2R4osAjpW5iI0F0boALxmePUsrQir-zA1OiJax8CggE6AZa0oWoEJL534dK5MplU9DoM3FEjA8EZPhDzwalswe1zrJq8BTPKzbJJWdStTZvTQ9xKUnWKf_S8YF9TpPU5djjaM1GxZCTz8FFL6X92bXqHTCALrdoUEQeAdhdSYbXEHg6k4iWwRJqhHo53YN_uQBt3yabxW8uEvcuC3PsGzNczcrAnqoXs8gtU9DnHSHJVdwJ3-jZ3wyiN46u2gk_WctWKsXvE7FkFP4_4x9H6qdUGA3qZJL7Q69EVqg1yBrt3AcvuWs1wbIWKjVYTmFNELJ4yVWHq1c_Jt_2ANlwQoALJfetJXu6TLxalisXCuDmXZtBrm1slU0JQcBiG5HBO5poSOpYJt2unq0JzyRNmb_dl1WyuzY0belIPulRUTSSWswfQRBLvkY6euWCBYPZ-pK3mfnX-VnxOoz7nbJQvHtq7MvNt_5y_0No-0ulgBbFSLZG1LspN96xVVHyEUaRtmXWPc1fyMvSETzpsPjRBCIQVsVq0dLzzWqUohPnDF6WaspRQl32_iW00Qgr7gufdDhYIII2lsyRbhpbxoPQsjM7mj5G3CB73MOHnzTci2ETAVv8fmie27NQRJjuLvWzLJ5jDNwoIqLsDRX8-Zvzd9XxS1ioKjDYl23Cji-xORp3czobjzpHTLsFlF77To7Zu2vdRSa2-ae9CcVQS4EFGoSf6QX2IaAL5uUpEbvpuUTcO03oZLfQtdBErmccazaAbyHn3S2oYo5JQ9b7lWe8hScFy_Sd8_MxIGjVWiOXJdJmvuxcIQ0377jwsNalk7G8yaUJQ4UJFZdkVjRMA-Q5m06JHmR_P2RJ4CebAU8Y5kg2IJKW_RtjNSJcgMxUK_v0xb7F0upUXbpbSxdnjqoBITl8LH2Z_i5x434N3iNKF_JFqhYd6xD5SJJwCy_hJTMIo7vU7MByfBCY0y_zLNSm8xHuAbjAO3mMkeu4pMA2d578RhKAPT9pwSDYmh9fUbVqRQDDyX0kwPB9Ye60DdePfiAZPW0TxCHwBL0PUoMm9QX0LT1OkKZbrH-iqdrbHKSO2UFwBkJn4q85VA4yOSae4tzjL68E_ZgrU0AxRmASM4_WHbZSDbkwmon7IfzcuYCxMvU94Z962eVbkeEsu3GtcYxgzTJMqgjG0CeiPzHOUg9l9l8wF8on4DIBKhTzzwirvEJA7G4hm3-WVX5vVEpnr6kEXPEZZtHeHKgeXKDhkwcdE4PaRqRJOSJ4IqdZBx192jpZnBGwy5G3M589Pf838vEBr46XEgxR9s6OAsP8W1bYLy9uLYjeZBZWLw8_0dDDnNcsmt0ThuU7zV6LiSIFiWWgxFCicv53vAApZpQnfhmVQKOojPlO4MLPxh5Ndl7M2q4uOjkCOigTGMTHAsMgtJUdva3HriJiHD-Zn56qYZBaQDOzM8Oe-dCtTUiVUkK4NlmLOpItAIoj8uzux56QE5mLoh_4eeU4gUFykSB8m4I384Dhmg9Nl20whKnanQRN2ydmHa0rcT4tOXTLC5YT1nWvlV2SCYjFsJQMPny_rjCCeI04xXVIEL7wYabae5rwWVIMAexeCGbOD42wPKxB_HhBoINwUkXeEpXCqriRACImR5E-GAqH0CO90mkRxEg9FMWvAOEZv18OkllC_7s0VF5A4Q4OQk5LNhZ2FMTiaJYFMyqQWOzSD_W7x8b7BMB9EWLjAGVEqZm2z3VN1OpbXsngG3kjq5b62EDhtRKXQ3AGcBJhLH2mKQ9zwt5trpKO6RPk-1k_3ynT-nZ3RBU5LWeAPYlgKqP92w9DhORXJ2INLK5XL_KWql7x7BiokFhtnYwS0Gjt_qJdexzN6RUVyYtAojRGkFYeav6Za40RbzF62q3nVMlz", + "alg": "ML-DSA-87" + }, + "privateKeyFormat": "JWK", + "jwt": "eyJhbGciOiJNTC1EU0EtODciLCJ0eXBlIjoiSldUIn0.eyJuYW1lIjoiRXJpYyBBcnRodXIgQmxhaXIiLCJnaXZlbl9uYW1lIjoiRXJpYyIsIm1pZGRsZV9uYW1lIjoiQXJ0aHVyIiwiZmFtaWx5X25hbWUiOiJCbGFpciIsIm5pY2tuYW1lIjoiR2VvcmdlIE9yd2VsbCIsInByZWZlcnJlZF91c2VybmFtZSI6Ik9yd2VsbCIsInByb2ZpbGUiOiJodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9HZW9yZ2VfT3J3ZWxsIiwicGljdHVyZSI6Imh0dHBzOi8vdXBsb2FkLndpa2ltZWRpYS5vcmcvd2lraXBlZGlhL2NvbW1vbnMvNy83ZS9HZW9yZ2VfT3J3ZWxsX3ByZXNzX3Bob3RvLmpwZyIsIndlYnNpdGUiOiJodHRwczovL3d3dy5vcndlbGxmb3VuZGF0aW9uLmNvbS90aGUtb3J3ZWxsLWZvdW5kYXRpb24vb3J3ZWxsL2Jvb2tzLWJ5LW9yd2VsbC8iLCJiaXJ0aGRhdGUiOi0yMDk5MjQyODAwLCJsb2NhbGUiOiJlbi1HQiIsInVwZGF0ZWRfYXQiOjQ0MTc4MTIwMCwiem9uZWluZm8iOiJFdXJvcGUvTG9uZG9uIn0.53v04JgipZT1630Uo3yQzjpsYgz7fo9BAs3jwwy41qKWgpByJX6Dsw7BsyZyPVj1DerOvHljgE2qI61kUciYmUqX44ZYIWnMNzOVRX9FbyRbncmR6JKs4J_XlFMtMaVOjq_WgYpRycxqrpok9NU7wM1jkgoNq4KgBaNLFlx0FmQUJB_RBZORy_uR1PFH7USAm_84_62Wynj6UTHGnxZwcOOHYBTtm-K7InctNXztAqfwXZJ53EeD42_-2jhT6glPZhRqA_BRG3NKU1vMfXy9fOkptvaM4r4Tyl0nTkD2FwSnVOa9j40FfGPDIUGFHTDxVbB3YquymoHjCD4xxXzv9mojKaf-wRFzc4-_HsPMPYPczNrVPzvAv5FjGx5tePd7tM9Ru5PuHMZN1BPhEN15ttK1tK8hpU02HM2PzGtJzVehkbmgUxkXMAOf5IVq3CyFZqQWsHJBdLNQVCq9JgRI77YL3YmSFYlT4qgrhfvJv-MBP5L8RKARIBrr1e6xOg0tDZsJfP665C0VSor_gFuhQVz7PPz-jW4fDl9hhbSAaS-OBnz8E9c6kSiaueCTsXCUPY5zQIO_60aOZmsOkWhhfm1PMf00WBNaxq1vFJ_FQupUPiSb99YWbZQA6Rf2frM-7KMGLf-Zxjbnz9Av5iaz-K2sGbtRlIbXnYxy_xWTBpD5mqQrePGuuaGoYySJCCaVEu17XMp__kgmDoaCyPcBNDafxyKvcB1Ekbkh8iGsCysyPVs9Xdkzl3Puk0s7R-dkVbULYkG2IAdCKmTHSpP7-mtUdbZE-rYz39cREmDuaSTUqOJlMKLwfZxU2TpvOeKcJbiuBL22AtwRtx5ZzehK0jOqimjUnp0_z2dYC9WL1loMh8TXba6i3sV3uja_XmQO8u8hXL1uWYW1FMflgTFKxAc3bVPPYN23mWWm5L0OYbfc_vIEosw8vXtD_qF0QPCSPFRcdlO2Gwn4ATY59_A9RsJ-4W7dvNj4RysDHzeb85UdOXhBbql69Cb1lMiAzUunWb__ZUhJcLtc7PDt7NR6Z-eRM8iWey2Oh7evDYvAlubD4YwybI0p8cOItQvNHFLzSGWfEBhsycoc9BC7n6SFafI_l8VhMagIFZ76JWc3fAnqqdTzYTRU3P53kYiw5aVmbkCvqPT580EWgRHNo5Ykni_Apg2LlHKOUX8OclfuQtkRGM45bmFrubQq5_e2D2X-8mQuF3WH0uKwSFjS9tsJQIVRor8z9UI02Ho1Pm3nslMfzrt1LKS7dG-JkBc1dHZpOFPG8jjBSxK9N3xc7XoCRBigWK_nommBDx9LIGY8C8KhvUoEEaV--b8aOAoARFvrPWLU8xH1QrQBJbDcTZg_rSwtCHljq3I13QF-lRdvMUweyRWp1_f0oOJLb3O-RGPWV9xTuZQo66bugucYUNFJDxI8a4nqQ3Uj2KwoIWNyBaQJ6YaqwQbIJIbn9Cz_fSL283GFlQglVns3uLRYoKoA13hLlPE-Gl2t3KdJFFCllTyBDh3wlFGf3vGurGAB0v9iaBCw8ZbSy8bIuvRHUL9ovprOkGyS2T0nlqBReMziGEzghhERw_8Fq_hMtYkG27RLqy2nJ9aohXT_3AxnyjxfF2WR8kW19DHxCuGVVE0CwR_H7Vm-T9v_MCloC9vrCnZ-nAYHwmTqX8Trl-G9iId8dmSw1232MwtAidiqQuSfDVEGUefntFZjQYMzUryWYqXYwJ9v_zknHs73nGuGyF1_aueuEuhhHSp6cn89rW16m7KMtTTAVbvkCB-l2j5h46ZaCS8PoOv_bhlKPeiuC_-PcT55Wdb8xk365a9cGeLa8sJ1_YuAJwnqQKTizFKDQV57fyDiB99VZ5fyPhyO3db-UjFpcAaMGNybuTBa2DiDIhF2mxKxKqznI8vjybcJDPuuviLPcMYClWCt27KDBeFFcrU4xljScd683qsOJsWlsp5ImZzICLJQRhLSn2ZTjHYG6fIpeMJVkEkZwZvjC-DM0lMKGdJrDlXM4sB_35Sg1QIbMty6qP-5DiTd0wCpk1XAaHmKWnLSHZS-9fxnfdMWKkIEeYLxCeYLgxg04mm8QnMvXNLQ99uWD_FyckRXUkXD19MXlTAgHHTU9cxjg1_MUtj_ZMu5t3JjfJVJLYL8ooXiQPO5YydTWpOqaUaDjS-Jhi7jdsVCIYEzgagcHnRx9C_KkURKoT45Z33eKQFuUN9P10xdFLgbW-ErgIOmQGc5VvfT-vdb5MhELdFiFDNTG_hm8oit0m7BBmtRYFwBXT3yro3yaLv7LSE1Kjtlhw-_4QTpEzJhLQz98dbWcARb7cEn86nQExuGx_Jxv84IyuoBYdli3zN06ev-Z5OX7lKfjEe3RN2J2UK-SLUll3N9HaPsmY_YbwKLiBczTvr76vnSfblROxyH-75cUvjqXiaPU1gYCmY05AuPxrZc_7EwzfiORFieoghB1LHfctjxhxaRuWDfiN1gREFesRkWVcrawE3Xqxv3dYHV51lbxsGrv4JPnmnlQyb1cLE5Fx6sYBJempl9Ry4w_kefaUl_EFtDULuCv_tgLor4whKv5O2Rq4-zRWbsfHt-iWCe_6ZfE0t-R2G7OK5Cx8enJd5c4ydduAHVeNA4CduS8gLJ0s9qATNmzb3x6hvIWWF1pwX67CqExcLUIVaIIznd3fGAp5iTbb6nqZZvJaaCFd5DHUbCOhNmU76ue14O-8GI7LUV4ajCI4gUzkZlbO7Np184y0Sv-bhxLXeeguIXal8PR3a-veEKOUZV9jnQPxN90E4jRAM3K0zWG9PTiXf5aGgZdoZHBox-eCfJHbYcAqC_Y0-_lgp2iaF35PF5qau0ZV0uUUVZnTDyPqliQAY2HNKp-uDolfoGRpclOT9xwBVR8vJB_oR2CtTKOX7g6NHdVhUWqxsxP23S6OmSbes-KdcDTnT1p95dk-wUomIF7oxTiBJOgLgQklJ2QRI-qBDBdiIABiGPQVVqpK3PM08vJaOYNvP9RoMk2scIRefbcikB5CulaTz69fcLzmHPVlLPvZOzptqNI0RORX27lFjYwHhrwyUjdxr0TvfJxa3wMOTenFCq7dtwmfg_v_wsQRb4eU_WGUYEBFbdWBMzNSXykjmzuGzwYd03Zn87jSzdXRGdlNdNuuYTHuTNg0fNF5XL_wMWwtTbWUUDwuNnLkEj-N3TbgyUHpjBwFb_4CM3ChY0vS4QbcA--ZVXzJn8sxSYSgIOBMfXiQ5y7A2KVlxpscU13aVoVgKafdkOcqt-FSuZvp5iT3SqicE-1uQVuE6Fy7wAWj6nU2X0iNIvh7mLqaFvsriGsEXe2yfiDa8DY9Pccz9zf13bwssBxddnSEEzJ6nK1jV4hkgc0GX-_e-7nya_nmN2TQMJ9t0AM7c_YiBV4sE4VOade5_LFRt9YT2q3rTmORatZRYw4IVZqLpZLarskwXItZiREflaFsQOlUwysDJO9rPjfkYPDsZ91tDIfbRDLDn-cHb9q81IUnswFf1vsO7vYHTmGkfSDnUbPyrSH2OTGtGpt5JtjMXa01c1YsICGv88BeIFFCMKq6vukd1DJ0E7o_4WSERmL5T4XfjZDsHO6Vp6aXqfnyiErU9TJG1eCk2QDKl4Mlav_2TBPYPC34277jLj093BemNje8PKYR9euYF_myua9Olt8oy1XY7SlBOtsChdZ7XznGl91sub8TjO1QlITyng4r8Jjwek5JupAPrM4qU5mHT3J2t20NlMiiiy-69eJltfqOTIcilIA68z0S4AjHUXZfXLqcY7ygTBn53gnXsjbSP8gKtwt3m79wuSR3C0WFGqs3my93o-T3scZDxF1WpIe7gSErnjwjx-WCHBB8DyKtNGwZtufJPCiUinqX47Act8Od2PwOhExCLGRCeY5XyeGgmv7VkdJ1GVpb26kIjQB6uECsRT3vAeyzUkhvSzE48vRTEzXvQvp42GGwUd6DtpNxoXAIWpyfg1EBM1t1eoMdDJH3FxuvXuItaPjvUnY7eMGkW5ybxiNNbf3Ravcj6O8QPYRY8DKz4RcR9psNdAQoywzp01kHLuW8BMt1cdk2ow02WEKXkQb1gFlQvnalaas27rpUKAiWbS8Nr7Ps6VW5gXuaQRgRvfUU4GQvuEmp1l2YAaPPdouPkkDXSQyP_MQ8KY3zIv81-6kvQufsFCnuAWLPX936I0CAXYQE-fi77g6yiJ2-TCuXQzBRuzBlwBxQLPKgjpjva4e3noiQisE-ZMo8swqYNsGkwoOrxZ4JG38tUySEQgWRwfxTMnqev4PcOTfcA4pS0LuRjNfRwg1g0NzhTcVBd3VntKSFhAvfyAOJbMQwPq0jAqa5fT0DaSyPp-IzcN9q8Mvrvgyp0PaKOnUIoNZO01kn7p1RbWgvqVBeiIkAsLpHO2imYyImQgCpcT1_CVwnugUQKXTZFPSgIfg7gAmaiU4M0Ie9KWI-jjSK797ET83E9IGx2rgFZ-7oK3XTXHAnLkEin6nDY9DjcsAR2AWNHslg8fCzC4BhN5aFo-MLCWVaCgLLeE3d5_DqFKR9s6QCky8qZ8N53ilM_iWpnJgCXbKjGB6WyVI5PI4WBAocJhtEf1_fbMG2hmcuVai_mym759OURYDgHXHCtFkIDxp1I-56-fVhI8_dOUY-cczXKlXpy-PTa0IOOPkmqgDqoagSylwuZ6erkhDDx53dlHDyNumIDlVKMtSnQvIErNSz9345sSdykG7PRFVlp6G-z1dBqYufkgq669UG0JZlnEO1vLFLttHd29zQ5WftBMvkxNc54WE9MY2K--R1kQGi1kd2k-CnK2-3nzuaxIiXksVWW0hQmB_f06e5D2IwEd1JSJU_Z1WXnVg_RXYGhRgAxkpS5ai0tAr1Su3favx-ESL9SoZ-5xrHHQ5gcgLyf85qE1biRgU1_JtJnMDqoQV1L0I1lKw1qQ4Sf6BuWD5hS-cyLfapNxt5d8VcsysU0hZrgnQ62NNdGAhLnP9NZ3fI44SQ4w9MaXjBhbssaPLMNNSi15fNZTd3FRtzfLZWYESojxDCd7juHn89SkmxHi1ATuVj1_jdgs3CDfly7eUOFmsawWmt15FG8z4jQRCalPnV6APsy6gMgp5lkfY8y6GmrGnum1nXjqDDibRY0reDEH9IBX8kCPHqokOj6pWYME1et6RTqeDDrpXRD08SoQ5kme1X1hnpnuuAKS9rxJY1bYVdqgmgz06ov2BEj9tJ1mEjyoaHVZJXgNTJm7gIUwuV1RtPMLLKNehHSVyoN-lmeA8bg2XEQnwkze_iMLJvlmfAQXu9vc1dJZeD3AotkfEmtBJZ0JylbamelMKaBeq2sYzXgcXv6RDLAk7yw3894OoA__7nguOR7AicdpQwimxZEvAlsYvzGz6Vdy9DtCBugk91tD8WzqR6-aDewMVMl0Ceto82hXbB2TBFiTH9wc24y74syav_DNDJFZTsS-pqqtsOVT0NBh-LYfS1rE1TgXKzdjplRVHDMalWT1Li34ny3pXopPHTkJncCCMPsDQkGZaDhFdKJyvl4N8kdZnsvw0PAfIQp19zlGjDthIBhyry92cbY0YqmCq5-PnLVTaS8TMPVbV8NuewKvgn2wHsPhN4Gxd8NJggtZd_ifXoEriXwP5r5bqSynS3gcfKCXk-ovkUUk0WJR_BYAIynoU1Hjz8YjU_IJOtSWXoSfRgjHrawkQFzVEG-n5wLfvHPYa52cDFpISOQzBH6w4gv3i-tbqEkB567zoCOqSLKtl4unD07-8eijuY8KYD-IIzhsrYOVIJgzU3rLWnLV1hKeveD1dJ-oC84-3Et_7_IYfEmkyBHg1ko5xCKea4XSpu76UPLN2yg_TIkFcXUAV8BNkV7h-jRG2j3ueU6fBMclD7dfg01VtaW0tN-Oz9RNDppr4gjZETZDhleJ13Wc8WUmdCe7SSEXqVvr__kNTj2XCZk2i2lvu4urP2dseKzZoSkNzjk14ALqxPgsb4H4nZGoUuK79kKu2655WSGhUItxt1qRbf3NxnVla45ggRJxIIndwq0vKo4W9OQCQE1PV25wcn3B-jFCcpGps8jbDT1LgbsWHyl2orO7xR8nW12An7bS5wUTH0tgZazA2RcmO0CBm9zk8yErLVyInMHP-v8AAAAAAAALExggKTI7RQ" + } } } } diff --git a/package-lock.json b/package-lock.json index 91cfa9a13..d73cc8bfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "zustand": "^4.5.2" }, "devDependencies": { - "@playwright/test": "^1.49.1", + "@playwright/test": "^1.61.1", "@types/node": "^20", "@types/node-forge": "^1.3.11", "@types/react": "^18", @@ -2944,13 +2944,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", - "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.49.1" + "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -10711,13 +10711,13 @@ } }, "node_modules/playwright": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", - "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.1" + "playwright-core": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -10730,9 +10730,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", - "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", "devOptional": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index c3d481e1c..ad2038edb 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "zustand": "^4.5.2" }, "devDependencies": { - "@playwright/test": "^1.49.1", + "@playwright/test": "^1.61.1", "@types/node": "^20", "@types/node-forge": "^1.3.11", "@types/react": "^18", diff --git a/playwright.config.ts b/playwright.config.ts index 62cec27f7..dd9335147 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -36,7 +36,12 @@ export default defineConfig({ projects: [ { name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + use: { + ...devices['Desktop Chrome'], + launchOptions: { + args: ['--enable-features=WebCryptoPQC'], + }, + }, }, { diff --git a/src/features/common/services/jwt.service.ts b/src/features/common/services/jwt.service.ts index deb346961..8359d5b25 100644 --- a/src/features/common/services/jwt.service.ts +++ b/src/features/common/services/jwt.service.ts @@ -45,7 +45,9 @@ import { } from "@/features/common/models/debugger-error.model"; import { algDictionary, + isMlDsaAlgorithm, jwsAlgHeaderParameterValuesDictionary, + type MlDsaAlgorithm, } from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; import { JwtHeaderDecoderSchema, @@ -84,6 +86,11 @@ export function isDigitalSignatureAlg(algorithm: string): boolean { return !!alg; } +export const isMlDsaSupported = (algorithm: MlDsaAlgorithm): boolean => { + // @ts-expect-error -- SubtleCrypto.supports is not yet in TypeScript's DOM types. + return SubtleCrypto.supports?.("generateKey", algorithm) ?? false; +}; + export const getAlgSize = (value: string): Result<{ size: number }, string> => { const algorithm = value; @@ -103,7 +110,7 @@ export const getAlgSize = (value: string): Result<{ size: number }, string> => { return err(`Can't calculate length for Unsecured JWT.`); } - if (isEdwardsCurveAlgorithm) { + if (isEdwardsCurveAlgorithm || isMlDsaAlgorithm(algorithm)) { return err(`Can't calculate length from ${algorithm} algorithm.`); } @@ -856,7 +863,15 @@ export const getSymmetricSecretKeyByteArray = ( return ok(safeTextEncodeResult.value); }; -export const getJwk = ({ alg, use, key_ops, ext, ...jwk }: JWK): JWK => jwk; +export const getJwk = ({ use, key_ops, ext, ...jwk }: JWK): JWK => { + if (jwk.kty === "AKP") { + return jwk; + } + + const { alg, ...key } = jwk; + + return key; +}; const safePrivateKeyFromPem = fromThrowable( nodeForge.pki.privateKeyFromPem, @@ -988,7 +1003,7 @@ export const getAsymmetricKeyCryptoKey = async ( const parsedKey = safeJsonParseResult.value; const jwk = getJwk(parsedKey); - if (!("kty" in jwk) && !("d" in jwk)) { + if (!("kty" in jwk) && !("d" in jwk) && !("priv" in jwk)) { return err( `The provided key is not a valid JWK. Ensure it is a correctly formatted JSON Web Key (JWK) as defined on [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4).`, ); @@ -1000,9 +1015,13 @@ export const getAsymmetricKeyCryptoKey = async ( ); } - if (!("d" in jwk)) { + const privateKeyParameter = jwk.kty === "AKP" ? "priv" : "d"; + + if (!(privateKeyParameter in jwk)) { return err( - `Private key is not a private JWK. The 'd' parameter must be present as defined on [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518#section-6).`, + jwk.kty === "AKP" + ? `Private key is not a private JWK. The 'priv' parameter must be present as defined in [RFC 9964](https://www.rfc-editor.org/rfc/rfc9964.html#section-3).` + : `Private key is not a private JWK. The 'd' parameter must be present as defined on [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518#section-6).`, ); } diff --git a/src/features/common/values/default-tokens.values.ts b/src/features/common/values/default-tokens.values.ts index 1879a3793..4f88ce5cd 100644 --- a/src/features/common/values/default-tokens.values.ts +++ b/src/features/common/values/default-tokens.values.ts @@ -3,6 +3,7 @@ import { EncodingValues } from "@/features/common/values/encoding.values"; import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; +import { MlDsaDefaultTokensValues } from "@/features/common/values/ml-dsa-default-tokens.values"; const rsaPrivateKey = `-----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj @@ -276,4 +277,5 @@ export const DefaultTokensValues: DefaultTokensModel = { privateKeyFormat: AsymmetricKeyFormatValues.PEM, jwk: edJwk, }, + ...MlDsaDefaultTokensValues, }; diff --git a/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts b/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts index 6f2e170f0..f29f558e9 100644 --- a/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts +++ b/src/features/common/values/jws-alg-header-parameter-values.dictionary.ts @@ -4,6 +4,15 @@ export interface JwsAlgHeaderParameterModel { curves?: string[]; } +export const mlDsaAlgorithms = ["ML-DSA-44", "ML-DSA-65", "ML-DSA-87"] as const; + +export type MlDsaAlgorithm = (typeof mlDsaAlgorithms)[number]; + +export const isMlDsaAlgorithm = ( + algorithm: string, +): algorithm is MlDsaAlgorithm => + mlDsaAlgorithms.includes(algorithm as MlDsaAlgorithm); + export interface JwsAlgHeaderParameterValuesDictionaryModel { unsecured: { [index: string]: JwsAlgHeaderParameterModel; @@ -27,6 +36,9 @@ export interface JwsAlgHeaderParameterValuesDictionaryModel { PS384: JwsAlgHeaderParameterModel; PS512: JwsAlgHeaderParameterModel; Ed25519: JwsAlgHeaderParameterModel; + "ML-DSA-44": JwsAlgHeaderParameterModel; + "ML-DSA-65": JwsAlgHeaderParameterModel; + "ML-DSA-87": JwsAlgHeaderParameterModel; }; } @@ -60,6 +72,18 @@ export const jwsExampleAlgHeaderParameterValuesDictionary: JwsAlgHeaderParameter name: "Ed25519", description: "Edwards-curve Digital Signature Algorithm", }, + "ML-DSA-44": { + name: "ML-DSA-44", + description: "Module-Lattice-Based Digital Signature Algorithm", + }, + "ML-DSA-65": { + name: "ML-DSA-65", + description: "Module-Lattice-Based Digital Signature Algorithm", + }, + "ML-DSA-87": { + name: "ML-DSA-87", + description: "Module-Lattice-Based Digital Signature Algorithm", + }, }, }; @@ -87,6 +111,9 @@ export interface JwsExampleAlgHeaderParameterValuesDictionaryModel { PS512: JwsAlgHeaderParameterModel; EdDSA: JwsAlgHeaderParameterModel; Ed25519: JwsAlgHeaderParameterModel; + "ML-DSA-44": JwsAlgHeaderParameterModel; + "ML-DSA-65": JwsAlgHeaderParameterModel; + "ML-DSA-87": JwsAlgHeaderParameterModel; }; } @@ -124,6 +151,18 @@ export const jwsAlgHeaderParameterValuesDictionary: JwsExampleAlgHeaderParameter name: "Ed25519", description: "Edwards-curve Digital Signature Algorithm", }, + "ML-DSA-44": { + name: "ML-DSA-44", + description: "Module-Lattice-Based Digital Signature Algorithm", + }, + "ML-DSA-65": { + name: "ML-DSA-65", + description: "Module-Lattice-Based Digital Signature Algorithm", + }, + "ML-DSA-87": { + name: "ML-DSA-87", + description: "Module-Lattice-Based Digital Signature Algorithm", + }, }, }; diff --git a/src/features/common/values/ml-dsa-default-tokens.values.ts b/src/features/common/values/ml-dsa-default-tokens.values.ts new file mode 100644 index 000000000..4035cebbe --- /dev/null +++ b/src/features/common/values/ml-dsa-default-tokens.values.ts @@ -0,0 +1,183 @@ +// Yes, all keys here are supposed to be exposed, you can commit +// them safely to GitHub. + +import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; + +export const MlDsaDefaultTokensValues = { + "ML-DSA-44": { + token: + "eyJhbGciOiJNTC1EU0EtNDQiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.2-Zq6jjXnpkghrB-crJ2_SdZ-pCroi_4ABd0Tg2xmrgj_uxz43G8gM6_xKUleXBYlq6JRHA8Cmxf1UrQoD0WaKGB10dYIDXmo1gLD1SL3PjeMbyi9-uo0Z_l7SSchIT5abkhnz8PCH4Qgl3MkCJB0w2Hz4Th82n0VGh0R4x7SoFSZf2omPkFo-_S3sVuniOZkA1O75lYPDLoG45dahm-yunAZ1l6lq1mtgwfvlvdSna_ku3FJHH0g0G_V7bmZQs7nbS17CCA09KLquXSeFYIfASCdxiSKrmApTeeYvFP_IzIHiwfAxX9W6W0lIwxGPYPgFnDnrHV2aMXQriRRsJNBKkE6MdC9tOp_FJUcz7vipB9bU06bCW1X-9-Sphk5q98ImWj1OLJxwYG5zC27CNEBWLKWeoMNJIm8qsZrE7AQcibcIQK-GAswgS5GW3BZ3RfAL5IXcBnjCeNJAEf_npWV_yCi-tk9Z5wVFf2vcejPTQkMpY1Zn_cEFRLRAPCSJzySeHMyFRbfRa_SOPlPTnlEhB-avFhE5e4QTKJchS6gv9uUPD02iMDexos1tYimUJZNigq4V9bq8C8n4xoopHE37bTTxL0i7UInZfqf-g_bpa8J2zOlHfZfEvfvDhpyKWYCAJUCNKsUXgDaUcv8fgx70PfxnQ1kELx-Vh2N0DQU_kdifZjA3kzWg7R4bqzV-jFhHkm3Vyj0wXi4hBDcJ0KK-cd9gtcyE8nBfGAe6iO0SF7E88iiQDi6PlinF2Ge5ndedgTaGSt3EOW8jkMYUvAN0CvacE4oVGkztuZRFxqgF-FO0kBr2P2Jbn-lAImzvGFxCCbd75ySFkLk4zQnaMbQP3YHHUw5rYTSP3fk_kxC_leW_xN1n2cpWWGNn5HUiFHUU2zM4y-2EnomfhpOAwq7uEJ30y9BnVdRYEhDTD3k-Pk8RY_wvOiOwohNq2GevN3WV97mv54hsRdi3qFFLpVT2fKCM8_6RckmKJ5UbFzHTKy1XNHHG2ZLkN78pmxlhG6myoEewEW5C_N42zAh0BvEDbRHOWvRSZqk0GOM9L9kXqQG5TTLHJqPfL-3bHBSYKxCH0jTOsUWlS3yZFw0GkpAgMm1zh0Z7lMzQpYOS6udcbCX9PwWrafYgeMX1xsAZ8p2pqTrFrF5xtjiiQxNXL225MmIe1RC1ZvESj9rh-ZofySuYsPVhRXdcfjcK8N6RQOIR7cOH9g0qgekl84_-c5IX6OvCctiY9AZnB76dbX4k7R7F-KMv-GCiTguP9veKI3HVEYazYU_NLUAhBXViDz0vtlbyPGGFfG80aSmdFpDNPHxvTUYaL2qH1yOiuSujf_KS-G06yrQ2xo8pYkVMie1j5M2Mqdnrp0r5bU2ERe5Sr5w4f7rQfR-bwswlrFrB0yQugBSxco74AtB0FlrCoqTQVDBTYKn9m0IL5s0ZtaBjmy8cimpYPINp_vq6lOIjygeLnaaH8yiPnuX6D7gELGmPWU0-8KZXwtb-rmi5Mb3TdJNgt7fCZk9R8roEd2RLxA2id9PTV7Kmb6t3t-40ANwBmAXHeMaZFGhN9pLJfKGTqoToBkYsFUCZNXHjhM2q3NHkX8N0PpRv0Zg5LdEGpQ7XUdwMVhvajSBFOn-AGQJwCLzwCV1N4DyS690EHOGMcQf_JU8JP67rNWV8d0a91xhEonaoG9eUwj-U19M48KV-yO1MEc5p4qglB9I4AqXCQwdv2sltN2heBRfTTjyW61dcYR2wJ8xICs-RdKeboq6PLE_m8qy3G1Xs1TbUJSDSFVcUwsCA8CjEAmwkSZSL5F8ainGujfLzOq6G-YP-dgg5Fcv-mAw5mE5OWQtTerOa1qSgZDl_wR85lcWo3o9gGjC1-0zaL0QHwOUOoOsN_c80mcLIrLS2fhNDB9mhaSZ5jvbeBBfXWu0xNMpm5uHNNpcn02oiGuMrZJy7cNwJ9LntmzO6EFIxj7SxQnlH8giKfS4TYeVESNrcOJNoIhUrg_AZLFBLMIaWA2vb1yYtQ2Gebm8SjVw4R2MxClIkDIbvb_JQx4tSRxB8ZDgPlstrlEAfpCxnMydcJO9er6655W1Zo8hj3AiWzt13ACArGZtSmWs40Z5GgwMEAOtWyckKTy9CJmqYO76eB-0IoAVIUTH0_p2LhfbcdUGuRXhIuYH_xosqzT0bg2M1pK1Us-5c5mpigu-sXv0CDGiJEoR72pl-9hy4V1D4mZktcswbtdSaPN-Sng7rgSwprp4gTV-GXv0pxVgLWWqKwnrJucTa011sTjBcflrtwCkoA5VzcCUw5aulZ5XPLJdRebYs-Nm_O6KIq2vlG-CiDGb2WEXKcCmDJvcgEDt8PPs1zm8RwntWqvtdQN-8kjeXCjDoQwOHc8MFQJAbSVtbdfowuLat_-8nCwarddFzjAsPQlqKQlFABtzx7NdG-D1dRpKyabV__rhXbeF72XnnJGEw9WEOyZSXCT9EXVBdD9eJpWifcmh_efbVbFnHulnQO6jCmEOAFa5K9JZc3Xu_JuLCA3g27CByIKIlGUkOqVlqPudr6b8vKMc6MaLIB-jjA0nkd8yax-YhViFDr-34xOupuwAe7Zgm6WLnw1yTuqaprnvo409y0C9rWbYi8YNFEgNukyBFAm28x-jYM1Ri_Kf6lmwE_PFWvIJf1L3035510Ei56czhJuVtBcoMd9TXYa-tQhSskZL_4__CbkOB9fpovYvSiht7r1QXpk833npAnXVdgzry4SAKORSTT_x9IXs3gNKYjcOAsXoHY7L5DvEbWoRMYQSARbMJRbSd1qAw8SOhek1PcYpEeedTnF_VzeMcG468OjBYx3QhSAhMXYIKJjQXeQCm3oGob51WlEKmcRk7eN4_88hLAJCfIbGLQm76fAvV97onBBJtt9ywL6Z-h06_7Udun3WApHWkvsyF4CKbPfLeOzfZs05TJvfc-kEFJAYygsmdkUJO-DvVUJIRnFW_LwbD3A9ZPYKBecKQP-NOo8Ae7S2WUKHx0dzJ6Uqg9N2nOx8lwaWLGZ3Z4cjtgoQHkVLOQ_2ZqJYETe1uNEYg4IhqlIL8BvGoXch6mLeGk8NEVaGBgg39wXRnweZhe8yx703-EBCyAuMTI8S26DhZmjqre73OH-ACJNY3BxhIuZoaTC6fb7BQwTLEZdY2Z2d3qGt7nG1dj7AQUNFiU7YG6PkJmdoa_r_wAAAAAAAAAAAAAAABMiNEQ", + privateKey: `-----BEGIN PRIVATE KEY----- +MDQCAQAwCwYJYIZIAWUDBAMRBCKAIK7Up5YK2a6Q0+H5Z25no39FdDzqQzEdsLO8 +YHqTubBw +-----END PRIVATE KEY-----`, + privateKeyFormat: AsymmetricKeyFormatValues.PEM, + jwk: { + kty: "AKP", + pub: "mO2Ek-8OptFY255_tfmwN7fDrxbnf7BaYfnbeUFjcnGJUXkL7rGUWXDnDK_owo7MIQ_UoKNr0wJIV_dAz_hm9VzFl9D-bdqXoWIWWk0gHTgxgtvaLzyScZhra8x5xLECDgVm2GsjE3ZfXCvtTU-AmB9TE0s1YVngIthFNlFYvRtVNxPTrilW6s7HLP5ashHKLkNd1Hx4QqGlSH0UCRlrbYkXZQO5A5IKI62kEIADbk4C8t3GtTkmE_PlpmAEXb2sFAvIaCLtLIST8_bGBS9_rl7-6BCK75Gs1qDVNEbwXwiUXVpqiLxCamxXhLvxOaqtQZ_CRn2l0-cdH75tmQ6IXaIr6Jy4fOgfPolEZNNKPYzPUdEBYo_NSN4ozKiZK17G9kuqGcjVm6hqCAXidgfs1arRutbDi6Ua3GwJttd0pLlF1n6Su4446P8xyOeyaM4-thKXLFsGiAi0QEbUGcJ6zKDNN6Z_wtg99rMwhOBKKKohFUwWrSEjaQEKfU0UINXSUaYx3Pgi23JVQ7Qm7AZzs20FoAysYRmyhYEMeXAM9SdqBIt0bygJPbaXCQVs5Q_1AGUB8CXVhgSnxXB1iGTVWB4yCkqhAHYjcsIDsWC75giptY_M9g5x9pUd8FjHe4pNxnbGHVeSVcKAgjsspgnukwRihDpAMfst6U61gszwLQdSk5ibzYdjMlQIhuTYo6JLArLB5fR12ogiogIdtngfzkhvgn54t9wU6pACj84_vQItVM5SqUAsH4NmU01YrSvisPnrWS_ft9vpKb798RZ1b-48dX09_hQbM71JkbWIQ6FxfGC8N5b2ffkUzDbYJZUKrc32atzqafoHvOkIknAJAZA2K7DCn04eeOumYM1WuOZb8C2KeTDT-gNv3H2RvRP-shtOktmV9lfvPAiNs4ocpNuwaXZWBcuUOv_IIXS11rXyYy5XRlSyjYtIHknBxNZMJdS1hFis0f5B-0FhxAnLyv1YrxZZBR5-mULyjxDifa2mvpjallGqCODDLM2N9cMrYO2Q92e1DRuUZ4r7dguNr8JWFppX---TEtnWHFMOiLcwcNJEOLj3et011V5FmjWCj9h05V1jz8fHVvYiEyOaDjwC9EDAPOktk5dg21DHCfCSy1dFPaxVRT3OkknKQrDC0qc-bacy66bl1_bz2ic_HNdqELNC7ii-yYe__9UsMiN0Gmwy5GIYWq49NGRh_QwLOAYHjSnJQM8Yirfh3HXiVeIaacpSpgBf_bgyRsaqiIyo039ysGS-kDipJFqFzkzgx2e0lsnyIy2DyBnXp5yNnFtGy2QsEhjnIWbylVtDu0lPN9bvk7ccJy-9YwfgNCm_HtbTtlv9fjbngx2MqteyQDf6M9QIX2M18NcvPF2C5elaLGBbPaR9kr0UiR6l92fsHHuqN8PAnQBiiTYTCOIE_losJhVWItMVuvuA4RtEtkr2Uwcrn1rzdIRGI83JZdu8k4av97uLFMsyGOXTMsk5FABv5RJ-jxwKu2TG2J1peRqZDHPiUPbB5TuYJNryKsUEUHyvkxjceO9BBTYKPQwTT7RD56AfqGIMmRtJMhum1BXLJNMIUsUwJeZ7JaVl4fxs0_qpQSISX8K-VqLsDgqDtw-RcioDPM2fZXo7zIVnGYCa7yAXaNKaioat18JD1rbEDPiaUrVhEx9iedAXH8P6o4oI6w5ygydw9Q4A-xYbmD4nlR0eu6JmO3_kngYSJeSoFiUyDWqobNoB-MuJvUXW6g", + alg: "ML-DSA-44", + }, + publicKey: `-----BEGIN PUBLIC KEY----- +MIIFMjALBglghkgBZQMEAxEDggUhAJjthJPvDqbRWNuef7X5sDe3w68W53+wWmH5 +23lBY3JxiVF5C+6xlFlw5wyv6MKOzCEP1KCja9MCSFf3QM/4ZvVcxZfQ/m3al6Fi +FlpNIB04MYLb2i88knGYa2vMecSxAg4FZthrIxN2X1wr7U1PgJgfUxNLNWFZ4CLY +RTZRWL0bVTcT064pVurOxyz+WrIRyi5DXdR8eEKhpUh9FAkZa22JF2UDuQOSCiOt +pBCAA25OAvLdxrU5JhPz5aZgBF29rBQLyGgi7SyEk/P2xgUvf65e/ugQiu+RrNag +1TRG8F8IlF1aaoi8QmpsV4S78TmqrUGfwkZ9pdPnHR++bZkOiF2iK+icuHzoHz6J +RGTTSj2Mz1HRAWKPzUjeKMyomStexvZLqhnI1ZuoaggF4nYH7NWq0brWw4ulGtxs +CbbXdKS5RdZ+kruOOOj/McjnsmjOPrYSlyxbBogItEBG1BnCesygzTemf8LYPfaz +MITgSiiqIRVMFq0hI2kBCn1NFCDV0lGmMdz4IttyVUO0JuwGc7NtBaAMrGEZsoWB +DHlwDPUnagSLdG8oCT22lwkFbOUP9QBlAfAl1YYEp8VwdYhk1VgeMgpKoQB2I3LC +A7Fgu+YIqbWPzPYOcfaVHfBYx3uKTcZ2xh1XklXCgII7LKYJ7pMEYoQ6QDH7LelO +tYLM8C0HUpOYm82HYzJUCIbk2KOiSwKyweX0ddqIIqICHbZ4H85Ib4J+eLfcFOqQ +Ao/OP70CLVTOUqlALB+DZlNNWK0r4rD561kv37fb6Sm+/fEWdW/uPHV9Pf4UGzO9 +SZG1iEOhcXxgvDeW9n35FMw22CWVCq3N9mrc6mn6B7zpCJJwCQGQNiuwwp9OHnjr +pmDNVrjmW/Atinkw0/oDb9x9kb0T/rIbTpLZlfZX7zwIjbOKHKTbsGl2VgXLlDr/ +yCF0tda18mMuV0ZUso2LSB5JwcTWTCXUtYRYrNH+QftBYcQJy8r9WK8WWQUefplC +8o8Q4n2tpr6Y2pZRqgjgwyzNjfXDK2DtkPdntQ0blGeK+3YLja/CVhaaV/vvkxLZ +1hxTDoi3MHDSRDi493rdNdVeRZo1go/YdOVdY8/Hx1b2IhMjmg48AvRAwDzpLZOX +YNtQxwnwkstXRT2sVUU9zpJJykKwwtKnPm2nMuum5df289onPxzXahCzQu4ovsmH +v//VLDIjdBpsMuRiGFquPTRkYf0MCzgGB40pyUDPGIq34dx14lXiGmnKUqYAX/24 +MkbGqoiMqNN/crBkvpA4qSRahc5M4MdntJbJ8iMtg8gZ16ecjZxbRstkLBIY5yFm +8pVbQ7tJTzfW75O3HCcvvWMH4DQpvx7W07Zb/X4254MdjKrXskA3+jPUCF9jNfDX +LzxdguXpWixgWz2kfZK9FIkepfdn7Bx7qjfDwJ0AYok2EwjiBP5aLCYVViLTFbr7 +gOEbRLZK9lMHK59a83SERiPNyWXbvJOGr/e7ixTLMhjl0zLJORQAb+USfo8cCrtk +xtidaXkamQxz4lD2weU7mCTa8irFBFB8r5MY3HjvQQU2Cj0ME0+0Q+egH6hiDJkb +STIbptQVyyTTCFLFMCXmeyWlZeH8bNP6qUEiEl/Cvlai7A4Kg7cPkXIqAzzNn2V6 +O8yFZxmAmu8gF2jSmoqGrdfCQ9a2xAz4mlK1YRMfYnnQFx/D+qOKCOsOcoMncPUO +APsWG5g+J5UdHruiZjt/5J4GEiXkqBYlMg1qqGzaAfjLib1F1uo= +-----END PUBLIC KEY-----`, + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }, + "ML-DSA-65": { + token: + "eyJhbGciOiJNTC1EU0EtNjUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.yZdPf0PSdNF6krArHNRqTHm89X3gDh1tYIQkxKlrnUz2dtHyCdk0nROz0Ch91wpOH1igHYwGAe5BuccerB6NaLhAtwWePfDnGP3ysXek_pfCDndH85DR8KZzXaxfH9f8jJXBNkjBrCcWC2gZvLNGIHwUMiAWVloRjsJgArmKdwuQkErVU5aK1ZOh8nIEdP6M6Mojy0n2DOQXhbtKh4S9msQyAjQLO6NlJ6SGAiwHyX4enh1JB27LMsMdXECBf3jnQr6Zr5cN3v_uNM-WL03ktKkxsYiCCf-KPElmt7CQCN6jwgak-vm0ggZ5YOz6uJy32dNHG4yF6nH2JYWxh2v1n2ymfODC0xKrBSrsX3nCT3C_clVMYFzV_va_EN3Jo4V8o915hFRpyCBei1upv5-UWfjobBg6VBqq-QmtNwp30O5k213Cdv-n2kJ3rxxZFh_vsGCBVhQHwQIREGa5hcwMH-1rO9l1wR3OH6U7GCFZEiTIDh6sigM3gCYffOvjBC6rx-YpLhN4oIeJh3UQahhO1YucQeAiOOL_UO0NuS0TOPt31ua_Gob7d1kkgI-063JW2Gg5lFLQtlnEuGki5_t-VSLFbVv0rFCV9TL73zy-vLYIrha0dLyaaXmVzto1FpjCP_vU_j_dTpVtKVqY5vpcF_7Q2Bl7IeY7v8Qrx3J0Tv3rdpAlqgSJDo-1blqHZfEJRcG62YPMnMR_dnPEwIq9yUI5v6gIrbtyEILoe1LSrJWPoyLOX1yWOaM2qHMYrmWExZrlB9rJmoH1cA7RU02WLBX0BU2NvRLgta_whcKRESTpwX_cOFAPIW9hGrp3YS2GViigWmLJGv2irZ9lm4CpUtzykI7_HSuPsDej1rf5NXa0-vemEi6spl41UwnDv7lXS-RMlIjgndaMSoZfVD-zMMNmovZKfPhNg5gJQXairnVflJgj0R5BFk8BQoApXSmEFS7bvzGBFkDXtt3hKhzcb1LvOl3SVVwXH1fD79kECxCC1YhEcg7yj5monvSWG25NJuJUbhiXFkVw0ZmlOAgj8oqF5qa9egVYvPYAWNaBAFiuL3tSP0Rc9Dur8nSPYn0lhNSMGz41fUzBJg0hHAlQJKrMQ9kzQ4Cr9pXxw22_-EjG-WTbu-Z_QTL_GTxgNzUauTyzN2nheQm1UvjO3gdH0Pyfw0r9Yp4DxmHSkqO-0oTLmsrNI7hL8G5I3C1FouTs4bNYxqi8y2NvIduMZHX3JeYh64yW5SzRUKDerc3QbnDCSULDhE0qu-Lfx4xrSpCxeikeQuNFPLevkR0BO5puYG2o9CEt0fH-RxYbq9TaoZ2127mUkD1_-RX4Stod9A_s4eEAVpCE6B96TvQWpnNNB_Wevc0MLIZbJ96ZyQu_DJiqa7UU_e4E-IYlFkh2tdb03UMYR1aiSIdZMNjGV5FTRIB8JoGCAdZcvB0ymQ32UcaaOg4fWGA8PUHTvjgRBlf4NTT_IlhVDUTFmI2l0Ut7_KQcvZOjC5edXkz40-BPiJ2fP3PMmf-o1xysqHYgU9Oh2je5P8T5KO8YwZlcRjYq7O8QrHLHesnz9VrgMK9MdHVJWjoEAjxjwIGnCvj4Tb-XuO8yqJGJOrU29hsea_q5G5MnL40mtTS1WTSPCaUYZWBDfepRf1CJ85bgp450bjheK1WuDSKkk6fuJFchbnwcgFn_L-jJuFZBAHUZ2_nmaW1NSz3UThO-kLa5NlLs2agT8H9QZvnSFBArZOp7TBniyZc9EnYl9bT_jMu9Ksza9As02LhpfJiOIhrzqUfynsU1e1-wWAH3leIQl_cbsRCebQYz_0xN0tif_u9JkmKAkf9zfJ_rJiaJWWDY8m8JeYVrjZGWNucH1kR6E_-HzVIHOgqaH0VnHZPWD9ZATKFOMRiWduro693g_bf-uxeX5I0WGu94bvlltf_vTk3e5rseA2iIw1Atx3ST2BblJExZYSYs9PFQfAWVHEJHnid622e8VveSbRghcfjNynfkqHM0mGn9D3npaKcM2jpuZ0cy_KxlF8cy7Z4J6ahEnMHlnzJEpnAycTudrnmfJWRhotPOjFyeznCR-juLrwhFlWk-9HDBmnpkHhZGsZKLSW9OgxUk1hB7nPhM_VyTK2mAa7a7iYtYn7yLZsjlHPuA2G53TumlEWsZutgrgFZN7Nz29h3_rYWD7OYkxAQCeMtAeDV8HLl5k2MvkeymdDrjckaxxGKXlqYmISfk5peUyUpyuZn0Z72xmQrG-pKpZVM4dYCk3X_XrxlbGnoBi6gne0oxYuI39gLCKWF9HHUuX0-R-yBdXbrVRTWubdqCbIrRxZhwN9p2c5laHHAIRKMPBB1wrrodgCVGbRdjoaL2gcKYcz8zkXrYrLjwBEubCg47ypy6o24wCqwFUlBu0jm389-scjkkeOFzlUep6JEqPg2qEcMpDTBF83mPqGySMO2qzwIRLg6oaJVRlaI7hquT0ZrI4nCkyfUEnSaCHzlKiZyime8eRE6pQpY2EjUWjYv4XKbkODdp_XZajfrGRNNu6skh3LHAVGQeikbTBPSRkfl92nUHRCTiUJljFk_nj-Ss4rMfpjBs_Wvd4B877HDQ5LVZrXAqgZlnn1l6b8O8K_XnlLHA6nTlUpreBo-Mi8It2Ca9LbqvORuAmTl0yxoazKMwhVdHe1BvSZGfcEk6U0-lIoDjMBipoZ1v3ZOS1WkdbpR7qeT_-SnZ30wjgjWg-heFP1xQtk-n3WyHiyvah0APtXSesM87eUSx7xYGiQcDlVpr9b8UhgZN0MjamOrrgWcDEGo1UHfbwrNd0IelWcmHgjMs92aBQasMhqmEknXULvLO-GaKGte0TVrn6AY10HXL2E4fi1OhMPG-kgl0vLdLQc-QLCcaIWOOeVMaIf0p49vnvIIXxBq1Dom5uZkyqYEbE7JYOhFDG9y1P-js7v1y7V9TDPPvx1dweiDnNOJEsVzCR2i5qHCcwkwYe-xgYjU89IVDikJy7OEx1hYiQQWsTlcpD70Cqf6LCaKl0S0YqcvKKNyVncvv5Z2f2KW8GmI9vVK1IusqzRv9i5tDG9uJt8HKgQyfb8IE5kM-KdXyacHOg2zwI7g32kG4-jjkdxNInEUhCCdI5L9i9qU2jh-vDryevelzb_YVEmFza2CBskMq5MbwDcbw5GP0Hv59Y3BAx8eHCD439NVXrCNzq0MUaQ19nRbcu4s9GjbfsHw5DtQATONeJrX59cB9PAe_FrCMyyHrr6p169kqlN7K_FngADcuFywP3equ7FN7vs-VPxXCgBj672VhDbr9HB7Lr5kBIdFeCdB3y1Ez0j10ETcKi-O5oU4x8bN2YFBrgfi46t_ad02-Vp6Uvl-PCE896AYDBGmyYld1vf_ebM5440aeVADx-Gw8E0d1E4K-Mto5U7rx_fYMPYwCUl8AZF2N3EdcJId3BShWNik6FurFMtjogQkZ1IFgSbDBHJFxOBnVWvZmCmiF7N2Nm6FcYMdIv3mSlmltm6HlLdHGYBXCdGadXm91lRnyoUorgAV__sjOLtTb3j3p3FRptOG1Hy3XmQ86p5LQT1Xk-FBh1_j0djf0aWHTERRbEOrTvUXA1aOmxq-1-3Me3Cr1JL8Ns69kSCq7O5V6iAB2zvA9og378zst3M1TgJCvoR1wWL8WimJsDq01yx4YG2IUMgpQhw8KppIunBeBeZzVlX7YUrA0-DPw9OOa6RHfWuLdcUF8bfdXew6liHmLqzywUktaocZue3bv1zBKCAtHd300RawIFKrBwG-pqaKWOuibWADMXDbc358Jn2HEcwgHF2GJ9k6i2PKBiuToKlFLaD0eZPihKrELz1r5E70s_Aj0KMXBGWqeyMRMwHcpRW79B4lEQYSmnWIrpPDo577P9ksmYWggPbqa1hDx_9RE1TcPt30WL4C9Xs13Ms7zuXtFUXYsCqrK5j_fRxt1GVa7ZcCeiyLEsK3Mh653fWmkOWnsSuaS80gg8RUclUvVwqOx7Y5pewr9hhE-r-Uib5dwbNtZpNsLo_gbJ3Pjm5zz_MJ9ukIcSOAsUKUN8fpfmm1wzpYQav0vOAQ5sVflHmAvQzFwJnmzAURWLa192EQsT8DxjLcsDqhfjhTi1Id6uyn1etzZUSHJ-lEtKTcgQGX_IlfWi4zexSdth5f1AvTobdjd4HMf4BIQ9O8_vAHll5nfW6nYW8oTgVIhafDqOUur5WKWVLdmtgDQbB3Yt9WUQDvtFMSfS8x_EyhHqYgZYfO5bdGVwTcrixfFdlrI87RdMMT6GF5vaRjzoP6Yuy6Awe6qwuZKG-w3j8kJNcZn6d7ZYfauCykKP2j-shcuYmaCl8HU7Qo7QESf3t_u8gY1RFaP1eDj8xxIUWd4Bw5e9v8jV3-FoQAAAAAAAAAAAAAAAAAACBEaHyQp", + privateKey: `-----BEGIN PRIVATE KEY----- +MDQCAQAwCwYJYIZIAWUDBAMSBCKAIIgy1GRQ1cU35YZhQ9AW7naYyvEtNzXUGuhz +/bE/c+De +-----END PRIVATE KEY-----`, + privateKeyFormat: AsymmetricKeyFormatValues.PEM, + jwk: { + kty: "AKP", + pub: "t69qTlRcbwoiniMSZ-YMYc_wF7OqeppUavtF0u57Y0Qr8ihvReVCBWm4e5Ck-Tp3PLRy_0DH8ymCr9KnsGC_FhlPoXsu3ih9zFf5fBK5D8enRwXxPig4CFXp_XIuNi9ZGUvispJ53I6G3wKhhCkkeYqkxtkr9k0aqXoCSaMABFyvaQ3mNBfAF8h48Uzoq1ODth_GsBi0Zy_dbrPD62VT0jJ3DNk1jxvkUl-U7O3cP5sgIyh2B9QXFqLxj2GF6YnAz8Afa10XiXcdvFn5d--NielyAmpvidE5GWY24SgPKHoO7QUjjpze3EulnocprxzFf_GNuCXe63gvLXDfQ9AFsv_mGC2jm_ZUcFJBQG0W6eq_KHICr7qKuHPBeDkoxpUse_AYRWHZMKuQ4-zu8ZueN-PhqRVpYj2Sgx8mzZfM1MO21d9cEl8DoCon4hpB3O6XP1mDwNBbHQM-2NHVHU7wzWO64rLP0vbaOoNNiCADvlmwTvmUwpvGWpSXbHVlth5AbkVZgxbJPISaXrEfiIFs51Nubks5c9wXVKtOnT9x_Kacjwe5DjwFFNFGfHD5hKAKmw-r5Fjwy8BTw1WvukOSOGTfB_Si2X_hHJN7Rx24uu6haBzKNM56x7cyTxOyG4yhDJjOX-I1NlktJKcEA_FAHqpYw8Uvu8JNG7TL_tGmqj5XgrECAn_mrjlPrfBj2nK-7wclHwaFOVsXEN9mxZSLzMKYXq0hqaSU2rWE2aO-yvuigYR_WLTJlyV6-zfXQX3q8C01Szc8Dl53V5u8VBj7a9STxpccB2Lku1z4PGN_x0MbQy8VEq5NaBLEcgidHxtoeCjdblyXLIWpeFkdwmdaR07616qYNzh_fcvN-JWEpKQ3oD2wBFtWD-113pDWOkntpks2I6IsaXrGKvkDiHn0feFVJqf88HIxUQ8jzi4eGrIqU2Nh0sknINX-JNvtTJ19bz2nlgXOepHosU7LTW7ltj5nzwEIEduvBZSDl2wbj4fToNq-MS92L3Q5LD12v6n3J7qGOv05FdgfpZo1P3ltia0589RaVhXlVPm19cwMvw8u5l86lkCI239_VTgUBKlw6dL_C1x2XBSnIFVwEuONmH9Uv-ni2rbtOPHy6WJvcEa5It8cEc10oFi8xfrF_sv6d24PWvY2surFTPTqVHROon5FFYclLDFrN5TLKekCXKq99k7xJvJ6e4bJG5BzlDR6cmlso8wh7EhUixpaHOoOKz82_MjaNU-BVCXbyS2wR7dKDJ1O1pP8K7fO2mv-sBvZDbvmy3nB2b69YVqBCWI9mT_YCfHZxVgeDpSVlf8DpSLDzLr30UDRJt8vQpXNHpdeMCmTF62FF0M7GcYb4HGxWqRDgAQFYSH7XTU0lBsRIGD5QtDaxqdCccsIBYNakQP8YFGuxrAYD4OhVQ7V_TY5kwGFBNPTnK8YJL5Mg0b2zoPk8EQrTcGj5ur6enNP_fOCybMU2ZrHqREGHGtksiaVD3CgTFx5AgZJNLONxkehSZ6tTvxgztoyXzIZXPuhWkLV8tB_A-foZcAOamG9k-ifWnTM2yfN1bm80HchTwAzm7QDP_f6edQsiGF3nQ8ikqWLAWrh714xL_1mdajxvj9xqgyEG8Ho4Ddih7heacfCcOYbnK350_xm60o_p-J1QQraNyxLr1V2rkvdMhUFcAzqNzZZcS8txQwrFI5kTG5_aKy_P84MeB2xCL_Y2xmrHpm8Gkhj-EGnIPytQABtsLFzkjM2o9woD-O0QmWqSvZrgN065AgrxFUAzbmnolKuIRiBNwu44_Fa6Iucqi7vXsN41dJYYA5P8bVYCQk3_SXvH6sCdh2cxlKAScKXITak2RrcflyRGdP1D54Qsfm5zaE-l1OiFdCm_0UI0qqBCQZ2yiSXfgXyV-p1R6cuPNHXz_GFna3QpL94mI_pPdg5MhAF4yWdSGzDWV4lBdVel8XVFWUTWQ8HSYRbAtaKlMhLheYnUa50jDK7X1Tsw5ja1cV-sFgsNH8uYj3d_vc0Ntq9GQLYkOduxknDlyheVMAIGpyS53mVPlABjzFD1dArOBT3HTteI8k7n1MAefvzdm6o6A3a8BN9VrPxApje-LQzDWiYY-p3sJIXVevycHC1lcxmxoXUjWkerX8kGKIYaqN1CI693HbXFZmOLSv_TnjBzjSVdSiuTTxVwgTRPxdkG8pQ6gCUkgBSckDlZCJkjOGR_PNOr5sV1lkw0xKuYdLRWGSN5XC7bL-EtF5tGP2k2KwsI82Kr5GH74lWJTgpxiRkirqxzZPJ8Q9Rb8znVzo8PgubNTZ5qHsgjMtzRkW2JazBl75nQ6RFfebHYfRlMw512a0c8RjJ2n3gadVJvXwXO2nq9l3au9Ux7HCMv4G9zPmJo5PdKlRpiYK-fHbbkIgEWl32twmX-yIy4uvvucDVFerpIuxYR7y_FY_yWwv185E_OtwEEp63UPUO1KR9HjS5cql35BWY0mRHBUE7LDQtuE4BLFUQyUyFWj_HQ8xWNpnbVwKCU_l8S3iGJ2zWB42Yo_T19X_-JrVR2y8ONitNyBMZ-lWqJW3_bgtx_iIhgI1fb2uJCEnfOal_pUWNWooACLM", + alg: "ML-DSA-65", + }, + publicKey: `-----BEGIN PUBLIC KEY----- +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 +jVqKAAiz +-----END PUBLIC KEY-----`, + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }, + "ML-DSA-87": { + token: + "eyJhbGciOiJNTC1EU0EtODciLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.1M1WfzcVsnHsqsUW6RJVfO7EAYtlxNflKkasRxleal-w_6i1_IRv--ySl67mwIQ2Y2efWtXKR8-5dYMhyTbo_Sdm1WzlOG6XrkeIlO6cNt1fpt6DvwXCAeP-h8t6slgyLK2zLiz9ntY0FUFVUxPNlIK5l0cXlA3DozxpkVZH-TCD0-OWBk1fzaGEvDNCF7h63CRgtXFByxmCnDIm7mtMah_onA97Miile2oci3jMWMptmKWgqLjJZO2C6EzuhkeArJh5Fq_8Www7qFFh_SP6pHBizk-D60_CVXvsNCFms98z7H1cT63b3fQu1t-TOBCzbjGEKU5Td6cM7qE8TUGr0Cm5e8FD9Ce9zgJSIr748iApXA37W9Gceh1Y2VINbG0byzAaQpCqzl5o69n2M9YShXXoV5kvAkN-YmZBAyY_65hLR3jlgl9luk-uUKh59Cr7-j4tkKBj3eho1gLOIFo37v7QTJsxS3FGUgVcmmxrdZ0z_adbaLs3xT4eX7L64sJ0Jek3iod820Nj7iVkHw8WiiKeiydckSrdgbUhyTYf713IIWHxuX1ePwTu9dZOJnUpN2cvCLh1xGE09bjaeFl3lEBhCvAj1juyGDAQ7Rzt8WgUUQ4gz2o-4u5zSexu52kLMtqeknSBOP6YVQ3qGBBJhmvHRrTsAEDWn1SBMm5hM-7y5RC17pbmctWQEdpuIu_F5P0xc_-mHJAGaGTSr01vSlOzR6xjBZVHQcu4S76iZjIfgYOfCdWLdHN1nDYsaBrF1kwR9rwejiMYUaC4qQLRTbao8hMbusb0zDtmQFJQ8wmwoWnwZxXkRSgL-P2nNkd67KRscM7RVYEbdMoCROLlLjOcWClP9P9NcpyHbvpVBoREL8o9hJleHcZ5Zn6EU8Ace8eo6TfaPECPjJnEsmuKAQmQ_C8_AyyaYvqd2k0CFV-SP7oKnz8L_1rQTC0FN0xgvG7_6zkV_eah0hvvDiUA3eVAStLn14oV6A9aCqLDNTqtHSaBZgJvCEC7riKA1MWshOSAemtcoNTudukhq7teg1fslQpbsS0qMjHORUL9ncxdTr-8f93Jp1bfzafnojlaaQxinP1TKmFFz4cE8hBKyZoHxH7UHWa81QvAZW69Ua52DT2M2EZXcMVdds8ogMMB5pcNwFIdPQuNTf1zCpU3LJz08ZC8NZJq-tmY0DgeT6XYDeddqLSR2Q985mHmWjNKijDjcNzqvcaUFOjU7aBr0wrHb4su7uFEcMn1sNKvbZOQLe1aDdt8UVvG0rlkgNGdOpehcZBrXhFPdFkS1GwScXB_iSYVKi4uCYAK2BHq6k2sYtRSK9F-_T5svnSP4gf96-bFLNJYPTA72uQx7yxJxRPgL6uj19mspZQCjX1phDfs2lqMTxt1g3mRZjah62AOCRkYIw4TEbxSEs4eoLus9U4KQH1pzNkEtzVcy4lur0JEjr_WQhxHVcS1XhmxwJLHzMnV89W1MyooDR7hyhhUJUh9yS5DlrDUbhhtS6D7qqfc8bhz_L4lR0mj761sENze4gELSlotzHYw7hfFU34toFU4i-Smjq658Av2sSYej1-HPYmpkxLGpxQyFnCf2f5FwbQVCeZW3paThvSi5qORpmgmJbz14OS9kB_h6tnJBzIlnqKfgid6dj6seYqXBt-aeqVLjNIvCo-mLDHkVC1fR-qUlri0r4frP6WEtB2_lGx3itqklhlTywa4dmPWjmwt3b4MpSf5Di91BZ_0mWvPjPf7-EME1lMotDax9FYlm_CejyI8WDxdMqNHIQ-ytAtATGjv1N3ulucpt2Ft5adYH_IiWNKOCyLapA5ykl75kOeV6nTO0i-C7trXTYfO1LVJDMdveHRAiL5JeOkVOvv2eWGV-3A9Um5de0Hr-1nkgwg7W5Q4e41ogNntn3O9Y_vhZ1Rh_sBx6z5HZQSCA5qi-gVgUirDuVLeozDyxKvHcNhJvNCwKaHNzjtHh3c-g0bEn-WKDMQBCDmIShquj-GOx0UEXzOavW4zExEVuJTjaoRH_M6SfYiveavaS2A7e60ZUih-TA8NYQviLchMhO7Eycweoy0OmWjAAhqSiox6YuVnO-aDVj9UTwsTx7XOGuC9hcvAZsbpz6HI_VtnX0NAyKAbwSE8iVvP3KH0PmwRK7qtwVvng-E4qmt6hSQHCfGBfwgTfSEjOwPLN3qOFrIPikPGHygBTuZi5BYtU552FgYMoPxsRi5px4_7peNdkyVeQlYTHlA30D22hmVSavBgJu0SYyQL4tRyYcKUv6JacX3WVhhFNBrnM171PIQ9KVCudD2uXdcoxxtxGmGAUVyrT0DwFq5scqR78DmRqFgarE-X3YeDjtnROR1kQlwXE80i0dXw_r9tRd9CapSrrYP10lYeHev-XwgL9zvjuav16wNGolcL75HVJv6sJJFmWTMOv41KvzFtsCj9B9LWDsLeVYl9wxIeK05GksHG--4K-Ej4CV5AYDBDUpZCQ1PURxAXFJULLIx-cqNF0zZW0uqbA_zfLqAyRtIHtg1IUWGRxnZApEld81kB-c4DoMI38hxwRvLeraa3vBd4Rrq4xNyjSEGojXKXtfeN7EjYpthvT3-L-SYf1kRyWo6DRvwsTrAXKPVbQvwwh3M3BDY3Bqx5iZPBzM2fotmlCG-uhC2gl8A5NlBm1fGBi6SAh-nuIy9_at3hl73i9rVpoEco7oBQ7zKmwUC9nu_FVou0uhVwOWcOG2EWdpcJ6kvwOOl7ZhTHRSkzed98KLT-LY1yso70bQrKbWOB1BHA8KiTUfem0cZ0fs8prwRPSmtm7az2H32Fxzp9rK8klMx72s8g1dkCMEJNv-ROC3Bw_3Y5KPxeTdT3R7FI5Q-JLt-cMEZUMKDFcIYBlv0I0yiOeD1xR4Sh7L-bWbtMTN3Am_q2w3ucgsqGmowAtUrj5zPu5UGvc6boVp1gTd6h62oiHszYRPRTykHKrE91gDEFQHfGKetL6yApT-_ggI2jUW3Z6O_9xqP53UuUJ5yTj0CaKoW_m9HDNs415uSct925eeYyAH6ER2KYJpVahTeTfqf-Bli6ApsaNJHTjQWJUuS4KhDyxrdjHSwo6bpZGqzLLFP7TiisMg5pJTa3vfocdbuel2UQ3rBovvmgLi5Y6a6t8mUgywkyqGcVAIeOnPrDR04xGVpTt1CN15yQQdnDaNe1hlVYr4uHs6mafaQAOGEf_c4J_z-er_VYDfCj0oAh2H8v8bLXMRdG2jESV5rYQBqkNdDih4XuNSp-5kOj0RUBjR4YcYDi2HnTZh8wvahenuvCMnSotkOozkqokdh-LvpSRp-cMHo-8z0TcVl5r1kcVpAcBOxqTocnUf6SKQ0FTENJRqP2corpDskQ3b8Ps2NA-2RPul9rdnV8JkapzPM38OY1qgeiYBWf11L6Qr1oq2-2FTOZXnuO7pG3lGRDg894a6yjUqe_PsBnLUJu34Ew-LaVScqOMwVfHR0rKPWJv2AdEqfZaFYfvm_BaAEUNAPOuenkKghLvBY6xftl7uEINUu5ujxHkyqaiELd6VCsriLXEElNNsYefjq9x7XP-84nAWTXWkwAlN9uP9sd8ogLLm3OoefB4hJw4EHBYZIJMVm-rNYzHhoNmtJ3Nsdjlc1LcwkKBuSUfSoah5eQQH_DFcVl4n_rSrUt-3y7uuglanOVWRB44_Ev3uoXiXMyqM-qT_bdzUdWORztgPHMQqZ03KrgEDSLGBLSNhQhTPzbYcizL-4wG4IJcgJUTc3iZSoI0PIQhApK-pgfay9DCu4l-C_NrKU7-5InUCvvi3cHwMSutKv9uFb_wqxyp00lQEN-S4jWNw67oLSLGVmAtXaDG12DOFB1dBPee9Np5BhTA8cNTu4oVR52HuFKKTw5Cmd-k1UzJp73Wt7XcokK1fIWETTJEbZqHl6zxKUhR8AedAI1HO0V4ai1rApjwHgMJWEAq9sNFT6FOGk8QKM9FqWHMzw-iyX6FiFyIHLoYDhwtfycNDgp7kCVE0p4gc_y1-Bvuo-yUpq-IKHIzfvF8PYjLVdfIWacjfm-rQLtk6PCxzbC02FBKUmDKiqaQvKknQgfvA4eYnqUYryc7B9_28X6F6gY8e_nh03DxLj_Dvud5GmDc3b-6pyArJXFQKcSqGXp9PaUDtv6QuxGmMIql8o23mHARyXY2VSxBPtU7qeSUG8WPD-T-Rmxp8LWN6QZ45EWGh25pE8qAU5Y_7SkTTYa4RBRsPQIuXpgm-AyCcmovsTK-Bv54WAaz5Eik4Lws9vCapS4N8t4oK_GazInrJIDHG1nTleX04th8Ck8It0xsJDimzdvXfS4jTf8KcqDZd6z-hR80wyIxWb9WRmjTUR3JLqnaguEw67ol-nqozHMIFfrjXl93yxU7JOERD785dqe1zeFo2lZ9tyBk7XYYllMyIG_sCdESBUN925O6HJ9lsVlgigXuKfWvpo8nl6CB9MpdBNHRPXUFIzO7aBVlIVuD8U7yUUWw_2wWs-BEEA-9AA4IRwBlneipt3T2EJHLQW9fR6yvSEqTI6sfir62u0_tmCFgx3g9Mgcj-9Sxb5M7hJZqU7nkyYaZ6CRIyCpBdC3jF2SntBLujrLOMl9r8BjQ59ivc-6h3_PCQl_oiPuaPSOyRTmFPnKSWCadk2XJsiKVaWBVnRkkJ7KEQMG-z8iJkn16q8cgtDiojRA-A8KDHXDSX54gE5NY9vuA5uan81YXZA-KGdv7npbNUxwmpVpPyIiLL74f2OKLRfQcQxLiDCKX7djs5ItF1jkPOfVaRd7QxcgaOa737KZD5ApJj4hMoCsREWdOmecQao7MVrZ8c8AI44ZW2gpGhYYyvZmJSZqYCoCtPK8isqqoIxrlxBkvUHXd-SvNllyJpI605lotxBBCY3c4Ikv1xv-UFA-qwFTkroV9rQaHF-hIF1z8OdhFbHNFAlD6cIbh4VSc-AvUyfJIxWe9Hcex63wKpPyuR8lMFuTCLEhwBclQ9_JVXVmgImjJ4Fct3SRt7WTexnk3sLFrswz2oFKqYdzaX1qeHaqzkV9DCBJMgwGD8ZLB-TNxMcwmHr3IRLFbhjMVTV47UZKUn7IrSl8w9B2OQcjv8gk7b7jGqdW_nSSwqXNaIgctLIgjeKo-6sKoAqVZch1RqnNbGP2iEA4kVxZFl_-TvCqMvsrgrC88ECeqPsB1kBxuTaHiryEzMhNCS3HEcBaYCsFmJ4ldkE7G3S8es15lCFor9ojXC9oPS1hrTm7y7yPGuLsm_cgJEufVGBtxXwmuwiLA1cJfN82DBmBPeZSz5Zpz70_SmsQNwy9nxr8deNztD8zYHU0o0ITuQHAcenXrV6BXC5AhHGGmvBhbzsv8J8VSbY_AKAoaeR4k5Vc0iFgMQYv083cJ1EoEMuCxxyj4_jnrh02LUZtP3eSxnkng7pAexcZPsCj4pHjhvLW_3SlEWGDQuBlJQYfWC-hHuth4VgvYwmvXAEBQF-zgq1gHzpcdU69glxR41Dma9Rb9Gcvcv9qsrpeLc4u_TTSVmomp3QBivdNWZABi-y35KYYeyTvrffsECFEw9h2SEK-hQPpbKcBC9M8YvO-HscQzxMlvKh0JN1L8r6QYWGro_J44M-MbfHE5K4RJJEVg24s2NIxPE95bUEZgR0qc58gQMohbuibGlqsS0cho0ywVtRwjbOXLlhGromqF4gDtizDOujxIxB2OXQKGk9XdxzLTIeSrH66zjYd0-Kko-tAyv8nY3OBvB6auQsitMwzzM9Kq0bvs_PvTK-8fcatcIEHNm_7OpjlYak4x2Fa2brAzbhWy5CuLICXmY5yGO2-05_9TJtafxLMFFASbcV-E29_clc66jJJuPPcpodDvxzja-5T8RbrMbEdatR69Ve-ONM4MUcqocNcl5SSPHOWVcYWi1yS5KySRkCC0Pru2G_NZYCPDhIS6EI1S3WDXPDpRxkS4JkUAcWoROFCnoLSYHGa5gA3XhT9jX0veEMoLTJZnTPSWKaYC6zybYY0cXaCPiAyOEpBpA-oWq3LJ3ioqP8i34TfB2mzFNX9EncMJ4aotMz0NztFeam0uM77NFlswtzg9wEIFDxL4essNKU_T4KUrvX9AgcIHT95vMHKKVHV7wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEBceISgxNQ", + privateKey: `-----BEGIN PRIVATE KEY----- +MDQCAQAwCwYJYIZIAWUDBAMTBCKAIJa7HpfHmbZgKurNlGuxCzEfz3LsCiy2gx5Q +LM+9z9Qx +-----END PRIVATE KEY-----`, + privateKeyFormat: AsymmetricKeyFormatValues.PEM, + jwk: { + kty: "AKP", + pub: "KkoQfrENvajTEhgmkFYF9eiZWPtkNsl91rU3a8lJyMtsQSjThqzTBWLsMTKHkUjLCVtHiUW7KfTv_yQvoHRxTCSY4kV1PH13nM20SnAVC4tj7-AwBFYe65jY4fyE2xSxHGZlJllDKe__qIY2egriWvjSU1bl4P0yFXoxpjCzsn7koJymWcVv2RL9Y8UhvOiYv_8RivWlmd6XREacjfMhmZ3TyGT5W6uiA7yQrV9_PRcZ9jJk32G3_yAhee_0nc3BbQGsE2id-C1iRMhPK0NTZkaHCMYmOmO9tXCitFOzYIAgIq7ABZ5D1pKEkY6J0DiWuRcUoJuStyutZhLiDOKadWUgTW8DdMtuZv4jmOq-KovdoCahl3_emlbfUnpSq7RL6h4hK4mWP91Zr5D6vrzt3Oc3z_vb9g5VeussKW0HMUXpT3vYJNSkD8YfrBRkv3OVw9k1BQeRyr_8CyJ7UaHw3uyRUOqpuHcNGDx0NeftU9G25MW8_quUceJqfl8BQyfn2Eq0bQB9SBMEx5tfd-I2JyEgpw4M30BAxiog_n6AQoV9px3kDBFs9U90u33Xa_SJL72P_7rH97Ft4PzsCE-PW6HWdmEJSFQhm2dyCBsm_HBGooAra7X2EDkS9V7L7QmzMko2VE_zzbdZT11QPaXsk0pSe3Xfb_fg9mF18BumQU6AZ29oln6COQV8sqRGyc6U6kkuCjW_EhplXCMcEUdvJDbG-hMz5Hlzw1OmBV6AQMxTK6qbuDi_IsOjQiV-wcbJ23e3mzAZz3HddqTXwVSu8Rd-qgzOow8Gjgxx7mSV2Qos0AccidilICnQPZjlf2I1tiFzGqcNbmo-Z4Ef90K18T4azXfaOTxXqnh-rpqOj-9CNtW3JxxgHHBi5WzMLiLN5kdi5dDvdfMN6IOpb5L7wZBUzneew8_W5Xjg71MAM6iR9eq0o0RMrN0gv3cl8k2pb4iFDT-mgTHg_qTGplmabigJs3fN7OtlwwroKWZRgPqFxDtiioYvZzRZInAOWv1t1yFbjzX97qAaZX1o5InU5Lvx0bgZOtzwCjuGKluH8o6s4UeWgRsmoh2G4nSUp2MZQsci6V6IhBTjixBIHF23vgR7SySoHNjSuJSUU2MKbNHZHjkdv1qOZmZzdpFQXA_ZtyY6PnLjvzS8IZyzbGh7vcssO0HHksAYyOmH1e2Ax-2P2mctCk-XgPpKUO8TJDk5lLUTnDv8cNLaKUHeHdck7ZREfC5DMRhZibUitHvxEgTifYafOAxmLOqV2ZI81hCksGZaN6juEsOxcyK1AmCHnFFcx1oJQGx4JfnUgV9rjLAlHXAiH6y9e8ijsKqEuFDAcULEtIDbY5utTDBDVCZ8uVs_Zo8-SEAYjHOphpVYsUqBk7zqKJHToSKEBKL3bbMDYgMqhXXn0Q4VW4XL-Ek4d5JCVo4noCBLMFDldt0MtfigHdT7G7I-TTkjUgGFX3orFQVcVQ15FupmyrTQnPrd0OCJuEI3xvBlOsJK_ueT5JzSM5R-3kkZkiejkFB5vPYHVcTLHZ0JQ5Pwu2R4osAjpW5iI0F0boALxmePUsrQir-zA1OiJax8CggE6AZa0oWoEJL534dK5MplU9DoM3FEjA8EZPhDzwalswe1zrJq8BTPKzbJJWdStTZvTQ9xKUnWKf_S8YF9TpPU5djjaM1GxZCTz8FFL6X92bXqHTCALrdoUEQeAdhdSYbXEHg6k4iWwRJqhHo53YN_uQBt3yabxW8uEvcuC3PsGzNczcrAnqoXs8gtU9DnHSHJVdwJ3-jZ3wyiN46u2gk_WctWKsXvE7FkFP4_4x9H6qdUGA3qZJL7Q69EVqg1yBrt3AcvuWs1wbIWKjVYTmFNELJ4yVWHq1c_Jt_2ANlwQoALJfetJXu6TLxalisXCuDmXZtBrm1slU0JQcBiG5HBO5poSOpYJt2unq0JzyRNmb_dl1WyuzY0belIPulRUTSSWswfQRBLvkY6euWCBYPZ-pK3mfnX-VnxOoz7nbJQvHtq7MvNt_5y_0No-0ulgBbFSLZG1LspN96xVVHyEUaRtmXWPc1fyMvSETzpsPjRBCIQVsVq0dLzzWqUohPnDF6WaspRQl32_iW00Qgr7gufdDhYIII2lsyRbhpbxoPQsjM7mj5G3CB73MOHnzTci2ETAVv8fmie27NQRJjuLvWzLJ5jDNwoIqLsDRX8-Zvzd9XxS1ioKjDYl23Cji-xORp3czobjzpHTLsFlF77To7Zu2vdRSa2-ae9CcVQS4EFGoSf6QX2IaAL5uUpEbvpuUTcO03oZLfQtdBErmccazaAbyHn3S2oYo5JQ9b7lWe8hScFy_Sd8_MxIGjVWiOXJdJmvuxcIQ0377jwsNalk7G8yaUJQ4UJFZdkVjRMA-Q5m06JHmR_P2RJ4CebAU8Y5kg2IJKW_RtjNSJcgMxUK_v0xb7F0upUXbpbSxdnjqoBITl8LH2Z_i5x434N3iNKF_JFqhYd6xD5SJJwCy_hJTMIo7vU7MByfBCY0y_zLNSm8xHuAbjAO3mMkeu4pMA2d578RhKAPT9pwSDYmh9fUbVqRQDDyX0kwPB9Ye60DdePfiAZPW0TxCHwBL0PUoMm9QX0LT1OkKZbrH-iqdrbHKSO2UFwBkJn4q85VA4yOSae4tzjL68E_ZgrU0AxRmASM4_WHbZSDbkwmon7IfzcuYCxMvU94Z962eVbkeEsu3GtcYxgzTJMqgjG0CeiPzHOUg9l9l8wF8on4DIBKhTzzwirvEJA7G4hm3-WVX5vVEpnr6kEXPEZZtHeHKgeXKDhkwcdE4PaRqRJOSJ4IqdZBx192jpZnBGwy5G3M589Pf838vEBr46XEgxR9s6OAsP8W1bYLy9uLYjeZBZWLw8_0dDDnNcsmt0ThuU7zV6LiSIFiWWgxFCicv53vAApZpQnfhmVQKOojPlO4MLPxh5Ndl7M2q4uOjkCOigTGMTHAsMgtJUdva3HriJiHD-Zn56qYZBaQDOzM8Oe-dCtTUiVUkK4NlmLOpItAIoj8uzux56QE5mLoh_4eeU4gUFykSB8m4I384Dhmg9Nl20whKnanQRN2ydmHa0rcT4tOXTLC5YT1nWvlV2SCYjFsJQMPny_rjCCeI04xXVIEL7wYabae5rwWVIMAexeCGbOD42wPKxB_HhBoINwUkXeEpXCqriRACImR5E-GAqH0CO90mkRxEg9FMWvAOEZv18OkllC_7s0VF5A4Q4OQk5LNhZ2FMTiaJYFMyqQWOzSD_W7x8b7BMB9EWLjAGVEqZm2z3VN1OpbXsngG3kjq5b62EDhtRKXQ3AGcBJhLH2mKQ9zwt5trpKO6RPk-1k_3ynT-nZ3RBU5LWeAPYlgKqP92w9DhORXJ2INLK5XL_KWql7x7BiokFhtnYwS0Gjt_qJdexzN6RUVyYtAojRGkFYeav6Za40RbzF62q3nVMlz", + alg: "ML-DSA-87", + }, + publicKey: `-----BEGIN PUBLIC KEY----- +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 +QKI0RpBWHmr+mWuNEW8xetqt51TJcw== +-----END PUBLIC KEY-----`, + publicKeyFormat: AsymmetricKeyFormatValues.PEM, + }, +}; diff --git a/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx b/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx index 8e47c0250..4ff066f41 100644 --- a/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx +++ b/src/features/debugger/components/debugger-alg-picker/debugger-alg-picker.component.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import styles from "./debugger-alg-picker.module.scss"; import { getPickersUiDictionary } from "@/features/localization/services/ui-language-dictionary.service"; import { useEncoderStore } from "@/features/encoder/services/encoder.store"; @@ -6,9 +6,14 @@ import { useDecoderStore } from "@/features/decoder/services/decoder.store"; import { useDebuggerStore } from "@/features/debugger/services/debugger.store"; import { DebuggerWidgetValues } from "@/features/common/values/debugger-widget.values"; import { DebuggerPickerComponent } from "@/features/common/components/debugger-picker/debugger-picker.component"; -import { jwsExampleAlgHeaderParameterValuesDictionary } from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; +import { + isMlDsaAlgorithm, + jwsExampleAlgHeaderParameterValuesDictionary, + type MlDsaAlgorithm, +} from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; import { useButton } from "@react-aria/button"; import { DebuggerPickerOptionModel } from "@/features/common/models/debugger-picker-option.model"; +import { isMlDsaSupported } from "@/features/common/services/jwt.service"; enum PickerStates { IDLE = "IDLE", @@ -56,6 +61,28 @@ export const WidgetAlgPickerComponent: React.FC< const [pickerState, setPickerState] = useState( PickerStates.IDLE, ); + const [mlDsaSupport, setMlDsaSupport] = useState<{ + isLoading: boolean; + algorithms: Record; + }>({ + isLoading: true, + algorithms: { + "ML-DSA-44": false, + "ML-DSA-65": false, + "ML-DSA-87": false, + }, + }); + + useEffect(() => { + setMlDsaSupport({ + isLoading: false, + algorithms: { + "ML-DSA-44": isMlDsaSupported("ML-DSA-44"), + "ML-DSA-65": isMlDsaSupported("ML-DSA-65"), + "ML-DSA-87": isMlDsaSupported("ML-DSA-87"), + }, + }); + }, []); const dictionary = getPickersUiDictionary(languageCode); @@ -116,9 +143,10 @@ export const WidgetAlgPickerComponent: React.FC< return { value: key, label: value.name, + isDisabled: isMlDsaAlgorithm(key) && !mlDsaSupport.algorithms[key], }; }); - }, []); + }, [mlDsaSupport.algorithms]); const algOptions = useMemo(() => { return [...noneAlgOptions, ...symmetricAlgOptions, ...asymmetricAlgOptions]; @@ -128,7 +156,7 @@ export const WidgetAlgPickerComponent: React.FC<
diff --git a/src/scripts/generate-test-jwts.ts b/src/scripts/generate-test-jwts.ts index 91eda28a2..3966bc4cb 100644 --- a/src/scripts/generate-test-jwts.ts +++ b/src/scripts/generate-test-jwts.ts @@ -2,6 +2,7 @@ import { DefaultTokensValues } from "@/features/common/values/default-tokens.val import { generateKeyPairSync, JsonWebKeyInput, + KeyObject, PrivateKeyInput, randomBytes, } from "crypto"; @@ -23,6 +24,7 @@ import { writeFileSync } from "node:fs"; import { join } from "path"; import { createPrivateKey } from "node:crypto"; import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; +import { isMlDsaAlgorithm } from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; import { JwtDictionaryEntryModel, JwtSignedWithDigitalModel, @@ -102,6 +104,13 @@ interface KeyPair { privateJWK: any; } +const generateMlDsaKeyPair = generateKeyPairSync as unknown as ( + type: string, +) => { + publicKey: KeyObject; + privateKey: KeyObject; +}; + async function generateKeys(algorithm: string): Promise { let keyPair; @@ -139,6 +148,9 @@ async function generateKeys(algorithm: string): Promise { if (algorithm === "Ed25519") { keyPair = generateKeyPairSync("ed25519"); } + if (isMlDsaAlgorithm(algorithm)) { + keyPair = generateMlDsaKeyPair(algorithm.toLowerCase()); + } if (!keyPair) { throw new Error(`Unsupported algorithm: ${algorithm}`); @@ -147,6 +159,11 @@ async function generateKeys(algorithm: string): Promise { const publicJWK = await exportJWK(keyPair.publicKey); const privateJWK = await exportJWK(keyPair.privateKey); + if (isMlDsaAlgorithm(algorithm)) { + publicJWK.alg = algorithm; + privateJWK.alg = algorithm; + } + const publicKeyPEM = keyPair.publicKey .export({ type: "spki", format: "pem" }) .toString(); diff --git a/tests/jwt.service.test.ts b/tests/jwt.service.test.ts index e19a64e46..a65ea4d25 100644 --- a/tests/jwt.service.test.ts +++ b/tests/jwt.service.test.ts @@ -1,9 +1,12 @@ -import { describe, expect, test } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { DefaultTokensValues, DefaultTokenWithKeysModel, } from "@/features/common/values/default-tokens.values"; import { + getAsymmetricKeyCryptoKey, + getJwk, + isMlDsaSupported, validateJwtFormat, verifyDigitallySignedJwt, } from "@/features/common/services/jwt.service"; @@ -11,6 +14,7 @@ import { JwtTypeValues } from "@/features/common/values/jwt-type.values"; import { DebuggerTaskValues } from "@/features/common/values/debugger-task.values"; import { DebuggerInputValues } from "@/features/common/values/debugger-input.values"; import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; +import { mlDsaAlgorithms } from "@/features/common/values/jws-alg-header-parameter-values.dictionary"; describe("validateJwtFormat", () => { const tokenHS256 = DefaultTokensValues.HS256.token; @@ -55,7 +59,7 @@ describe("validateJwtFormat", () => { }, signingAlgorithm: "HS256", type: JwtTypeValues.MACed, - }) + }), ); const result2 = validateJwtFormat(tokenHS384); @@ -74,7 +78,7 @@ describe("validateJwtFormat", () => { iat: 1516239022, }, }, - }) + }), ); const result3 = validateJwtFormat(tokenHS512); @@ -93,7 +97,7 @@ describe("validateJwtFormat", () => { iat: 1516239022, }, }, - }) + }), ); const result4 = validateJwtFormat(tokenRS256); @@ -115,7 +119,7 @@ describe("validateJwtFormat", () => { iat: 1516239022, }, }, - }) + }), ); const result5 = validateJwtFormat(tokenRS384); @@ -134,7 +138,7 @@ describe("validateJwtFormat", () => { iat: 1516239022, }, }, - }) + }), ); const result6 = validateJwtFormat(tokenRS512); @@ -153,7 +157,7 @@ describe("validateJwtFormat", () => { iat: 1516239022, }, }, - }) + }), ); const result7 = validateJwtFormat(unsecured); @@ -173,7 +177,7 @@ describe("validateJwtFormat", () => { "http://example.com/is_root": true, }, }, - }) + }), ); }); @@ -186,7 +190,7 @@ describe("validateJwtFormat", () => { task: DebuggerTaskValues.DECODE, input: DebuggerInputValues.JWT, message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, - }) + }), ); const result2 = validateJwtFormat(invalidToken2); @@ -197,7 +201,7 @@ describe("validateJwtFormat", () => { task: DebuggerTaskValues.DECODE, input: DebuggerInputValues.JWT, message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, - }) + }), ); const result3 = validateJwtFormat(invalidToken3); @@ -208,7 +212,7 @@ describe("validateJwtFormat", () => { task: DebuggerTaskValues.DECODE, input: DebuggerInputValues.JWT, message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, - }) + }), ); const result4 = validateJwtFormat(invalidToken4); @@ -219,7 +223,7 @@ describe("validateJwtFormat", () => { task: DebuggerTaskValues.DECODE, input: DebuggerInputValues.JWT, message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, - }) + }), ); const result5 = validateJwtFormat(invalidToken5); @@ -230,7 +234,7 @@ describe("validateJwtFormat", () => { task: DebuggerTaskValues.DECODE, input: DebuggerInputValues.JWT, message: `JWT must not be empty.`, - }) + }), ); const result6 = validateJwtFormat(invalidToken6); @@ -241,7 +245,7 @@ describe("validateJwtFormat", () => { task: DebuggerTaskValues.DECODE, input: DebuggerInputValues.JWT, message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, - }) + }), ); const result7 = validateJwtFormat(invalidToken7); @@ -258,7 +262,7 @@ describe("validateJwtFormat", () => { }, payload: "test", }, - }) + }), ); const result8 = validateJwtFormat(invalidToken8); @@ -269,7 +273,7 @@ describe("validateJwtFormat", () => { task: DebuggerTaskValues.DECODE, input: DebuggerInputValues.JWT, message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, - }) + }), ); const result9 = validateJwtFormat(invalidToken9); @@ -280,7 +284,7 @@ describe("validateJwtFormat", () => { task: DebuggerTaskValues.DECODE, input: DebuggerInputValues.JWT, message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, - }) + }), ); }); }); @@ -327,3 +331,99 @@ describe("Ed25519", () => { expect(verificationResult.unwrapOr(false)).toBe(true); }); }); + +describe("ML-DSA", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + test("recognizes all three algorithm identifiers", () => { + for (const algorithm of mlDsaAlgorithms) { + const header = Buffer.from( + JSON.stringify({ alg: algorithm, typ: "JWT" }), + ).toString("base64url"); + const payload = Buffer.from(JSON.stringify({ sub: "1234" })).toString( + "base64url", + ); + const result = validateJwtFormat(`${header}.${payload}.AA`); + + expect(result.isOk()).toBe(true); + expect(result.unwrapOr(null)).toMatchObject({ + signingAlgorithm: algorithm, + type: JwtTypeValues.DigitallySigned, + }); + } + }); + + test("detects support separately for each algorithm", () => { + const supports = vi.fn( + (_operation: "generateKey", algorithm: string) => + algorithm !== "ML-DSA-65", + ); + vi.stubGlobal("SubtleCrypto", { supports }); + + expect(isMlDsaSupported("ML-DSA-44")).toBe(true); + expect(isMlDsaSupported("ML-DSA-65")).toBe(false); + expect(isMlDsaSupported("ML-DSA-87")).toBe(true); + expect(supports).toHaveBeenNthCalledWith(1, "generateKey", "ML-DSA-44"); + expect(supports).toHaveBeenNthCalledWith(2, "generateKey", "ML-DSA-65"); + expect(supports).toHaveBeenNthCalledWith(3, "generateKey", "ML-DSA-87"); + }); + + test("reports unsupported when SubtleCrypto.supports is unavailable", () => { + vi.stubGlobal("SubtleCrypto", {}); + + for (const algorithm of mlDsaAlgorithms) { + expect(isMlDsaSupported(algorithm)).toBe(false); + } + }); + + test("preserves AKP algorithm parameters while normalizing JWKs", () => { + expect( + getJwk({ + kty: "AKP", + alg: "ML-DSA-44", + pub: "public", + priv: "private", + use: "sig", + key_ops: ["sign"], + ext: true, + }), + ).toStrictEqual({ + kty: "AKP", + alg: "ML-DSA-44", + pub: "public", + priv: "private", + }); + + expect( + getJwk({ + kty: "RSA", + alg: "RS256", + n: "modulus", + e: "AQAB", + }), + ).toStrictEqual({ + kty: "RSA", + n: "modulus", + e: "AQAB", + }); + }); + + test("requires the AKP private seed parameter", async () => { + const result = await getAsymmetricKeyCryptoKey( + JSON.stringify({ + kty: "AKP", + alg: "ML-DSA-44", + pub: "public", + }), + "ML-DSA-44", + AsymmetricKeyFormatValues.JWK, + ); + + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr()).toContain( + "The 'priv' parameter must be present", + ); + }); +}); From 14cae64cb1d6ea224d3b95935d9abbad1b22cd39 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 17:01:06 +0200 Subject: [PATCH 08/12] feat: re-encode keys when changing formats Signed-off-by: Filip Skokan --- e2e/decoder.spec.ts | 34 +++- e2e/encoder.spec.ts | 36 +++- src/features/common/services/jwt.service.ts | 156 +++++++++++++++++- ...en-decoder-key-format-picker.component.tsx | 18 +- .../decoder/services/decoder.store.ts | 19 ++- .../decoder/services/token-decoder.service.ts | 33 +++- ...en-encoder-key-format-picker.component.tsx | 20 +-- .../encoder/services/encoder.store.ts | 26 ++- .../encoder/services/token-encoder.service.ts | 31 +++- tests/decoder.store.test.ts | 69 ++++++++ tests/encoder.store.test.ts | 79 +++++++++ tests/jwt.service.test.ts | 71 ++++++++ 12 files changed, 523 insertions(+), 69 deletions(-) create mode 100644 tests/encoder.store.test.ts diff --git a/e2e/decoder.spec.ts b/e2e/decoder.spec.ts index 5eb467788..9e70cd6bd 100644 --- a/e2e/decoder.spec.ts +++ b/e2e/decoder.spec.ts @@ -516,15 +516,15 @@ test.describe("decode JWTs", () => { }) .click(); - await checkSecretKeyDecoderEditorStatusBarMessage({ - page, - type: MessageTypeValue.ERROR, - status: MessageStatusValue.VISIBLE, - }); - - await secretKeyEditorInput.fill( - JSON.stringify(entrywithJwkKey.publicKey, null, 2) - ); + await expect + .poll(async () => { + try { + return JSON.parse(await secretKeyEditorInput.inputValue()); + } catch { + return null; + } + }) + .toStrictEqual(entrywithJwkKey.publicKey); await checkJwtEditorStatusBarMessage({ page, @@ -553,6 +553,22 @@ test.describe("decode JWTs", () => { expect(decodedHeader).toBe(entrywithJwkKey.header); expect(decodedPayload).toBe(entrywithJwkKey.payload); + + await formatPicker.click(); + + await page + .getByRole("option", { + name: entry.publicKeyFormat, + }) + .click(); + + await expect(secretKeyEditorInput).toHaveValue(entry.publicKey); + + await checkSecretKeyDecoderEditorStatusBarMessage({ + page, + type: MessageTypeValue.SUCCESS, + status: MessageStatusValue.VISIBLE, + }); } if ( diff --git a/e2e/encoder.spec.ts b/e2e/encoder.spec.ts index e9ea6e84f..04893625a 100644 --- a/e2e/encoder.spec.ts +++ b/e2e/encoder.spec.ts @@ -474,15 +474,15 @@ test.describe("encode JWTs", () => { }) .click(); - await checkSecretKeyEncoderEditorStatusBarMessage({ - page, - type: MessageTypeValue.ERROR, - status: MessageStatusValue.VISIBLE, - }); - - await secretKeyEditorInput.fill( - JSON.stringify(tokenWithJwkKey.privateKey, null, 2) - ); + await expect + .poll(async () => { + try { + return JSON.parse(await secretKeyEditorInput.inputValue()); + } catch { + return null; + } + }) + .toStrictEqual(tokenWithJwkKey.privateKey); await checkSecretKeyEncoderEditorStatusBarMessage({ page, @@ -498,6 +498,24 @@ test.describe("encode JWTs", () => { ); } + await formatPicker.click(); + + await page + .getByRole("option", { + name: tokenWithPemKey.privateKeyFormat, + }) + .click(); + + await expect(secretKeyEditorInput).toHaveValue( + tokenWithPemKey.privateKey + ); + + await checkSecretKeyEncoderEditorStatusBarMessage({ + page, + type: MessageTypeValue.SUCCESS, + status: MessageStatusValue.VISIBLE, + }); + return; } }); diff --git a/src/features/common/services/jwt.service.ts b/src/features/common/services/jwt.service.ts index 8359d5b25..39c6410a9 100644 --- a/src/features/common/services/jwt.service.ts +++ b/src/features/common/services/jwt.service.ts @@ -15,6 +15,9 @@ import { CompactJWSHeaderParameters, CompactSign, CompactVerifyResult, + exportJWK, + exportPKCS8, + exportSPKI, importPKCS8, type CryptoKey, type JWK, @@ -620,10 +623,12 @@ export async function validateAsymmetricKey({ alg, asymmetricPublicKey, asymmetricPublicKeyFormat, + extractable, }: { alg: string; asymmetricPublicKey: CryptoKey | string; asymmetricPublicKeyFormat: AsymmetricKeyFormatValues; + extractable?: boolean; }): Promise> { const spki = `-----BEGIN PUBLIC KEY-----`; const pkcs1 = `-----BEGIN RSA PUBLIC KEY-----`; @@ -646,6 +651,7 @@ export async function validateAsymmetricKey({ const safeImportX509Result = await safeImportX509( asymmetricPublicKey, alg, + extractable === undefined ? undefined : { extractable }, ); if (safeImportX509Result.isErr()) { @@ -665,6 +671,7 @@ export async function validateAsymmetricKey({ const safeImportSPKIResult = await safeImportSPKI( asymmetricPublicKey, alg, + extractable === undefined ? undefined : { extractable }, ); if (safeImportSPKIResult.isErr()) { @@ -706,7 +713,11 @@ export async function validateAsymmetricKey({ const spkiKey = PublicKeyToPemResult.value; - const safeImportSPKIResult = await safeImportSPKI(spkiKey, alg); + const safeImportSPKIResult = await safeImportSPKI( + spkiKey, + alg, + extractable === undefined ? undefined : { extractable }, + ); if (safeImportSPKIResult.isErr()) { return err({ @@ -746,7 +757,11 @@ export async function validateAsymmetricKey({ }); } - const safeImportJWKResult = await safeImportJWK(parsedPublicKey, alg); + const safeImportJWKResult = await safeImportJWK( + parsedPublicKey, + alg, + extractable === undefined ? undefined : { extractable }, + ); if (safeImportJWKResult.isErr()) { return err({ @@ -939,6 +954,7 @@ export const getAsymmetricKeyCryptoKey = async ( key: string, alg: string, keyFormat: AsymmetricKeyFormatValues, + extractable?: boolean, ): Promise> => { if (keyFormat === AsymmetricKeyFormatValues.PEM) { if (key.startsWith("-----BEGIN RSA PRIVATE KEY-----")) { @@ -979,7 +995,11 @@ export const getAsymmetricKeyCryptoKey = async ( } if (key.startsWith("-----BEGIN")) { - const safeImportPKCS8Result = await safeImportPKCS8(key, alg); + const safeImportPKCS8Result = await safeImportPKCS8( + key, + alg, + extractable === undefined ? undefined : { extractable }, + ); if (safeImportPKCS8Result.isErr()) { return err(safeImportPKCS8Result.error); @@ -1025,7 +1045,11 @@ export const getAsymmetricKeyCryptoKey = async ( ); } - const safeImportJWKResult = await safeImportJWK(jwk, alg); + const safeImportJWKResult = await safeImportJWK( + jwk, + alg, + extractable === undefined ? undefined : { extractable }, + ); if (safeImportJWKResult.isErr()) { return err(safeImportJWKResult.error); @@ -1039,6 +1063,130 @@ export const getAsymmetricKeyCryptoKey = async ( return err("Missing or incorrect private key format."); }; +const safeExportJWK = fromAsyncThrowable(exportJWK, (e) => + getOperationException({ + e, + defaultMessage: "Cannot export key as JWK.", + }), +); + +const safeExportPKCS8 = fromAsyncThrowable(exportPKCS8, (e) => + getOperationException({ + e, + defaultMessage: "Cannot export private key as PEM-encoded PKCS#8.", + }), +); + +const safeExportSPKI = fromAsyncThrowable(exportSPKI, (e) => + getOperationException({ + e, + defaultMessage: "Cannot export public key as PEM-encoded SPKI.", + }), +); + +const exportAsymmetricKey = async ({ + key, + keyType, + targetFormat, +}: { + key: CryptoKey | Uint8Array; + keyType: "private" | "public"; + targetFormat: AsymmetricKeyFormatValues; +}): Promise> => { + if (key instanceof Uint8Array) { + return err("Symmetric keys cannot be converted to an asymmetric format."); + } + + if (targetFormat === AsymmetricKeyFormatValues.JWK) { + const exportResult = await safeExportJWK(key); + + if (exportResult.isErr()) { + return err(exportResult.error); + } + + const stringifyResult = safeJsonStringify(getJwk(exportResult.value)); + + return stringifyResult.isErr() + ? err(stringifyResult.error) + : ok(stringifyResult.value); + } + + const exportResult = + keyType === "private" + ? await safeExportPKCS8(key) + : await safeExportSPKI(key); + + return exportResult.isErr() + ? err(exportResult.error) + : ok(exportResult.value); +}; + +export const convertAsymmetricPrivateKeyFormat = async ({ + alg, + key, + sourceFormat, + targetFormat, +}: { + alg: string; + key: string; + sourceFormat: AsymmetricKeyFormatValues; + targetFormat: AsymmetricKeyFormatValues; +}): Promise> => { + if (!key || sourceFormat === targetFormat) { + return ok(key); + } + + const importResult = await getAsymmetricKeyCryptoKey( + key, + alg, + sourceFormat, + true, + ); + + if (importResult.isErr()) { + return err(importResult.error); + } + + return exportAsymmetricKey({ + key: importResult.value, + keyType: "private", + targetFormat, + }); +}; + +export const convertAsymmetricPublicKeyFormat = async ({ + alg, + key, + sourceFormat, + targetFormat, +}: { + alg: string; + key: string; + sourceFormat: AsymmetricKeyFormatValues; + targetFormat: AsymmetricKeyFormatValues; +}): Promise> => { + if (!key || sourceFormat === targetFormat) { + return ok(key); + } + + const importResult = await validateAsymmetricKey({ + alg, + asymmetricPublicKey: key, + asymmetricPublicKeyFormat: sourceFormat, + extractable: true, + }); + + if (importResult.isErr()) { + return err(importResult.error.message); + } + + return exportAsymmetricKey({ + key: importResult.value, + keyType: "public", + targetFormat, + }); +}; + const createCompactSignObject = fromThrowable( (payload: Uint8Array) => new CompactSign(payload), () => "Unable to create object to build and sign Compact JWS strings", diff --git a/src/features/decoder/components/token-decoder-key-format-picker.component.tsx b/src/features/decoder/components/token-decoder-key-format-picker.component.tsx index b43f87556..c05c004bc 100644 --- a/src/features/decoder/components/token-decoder-key-format-picker.component.tsx +++ b/src/features/decoder/components/token-decoder-key-format-picker.component.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import styles from "./token-decoder-key-format-picker.module.scss"; import { useDecoderStore } from "@/features/decoder/services/decoder.store"; import { getPickersUiDictionary } from "@/features/localization/services/ui-language-dictionary.service"; @@ -17,23 +17,9 @@ export const TokenDecoderKeyFormatPickerComponent: React.FC< const handlePublicKeyFormatChange = useDecoderStore( (state) => state.handleAsymmetricPublicKeyFormatChange, ); - const controlledAsymmetricPublicKey = useDecoderStore( - (state) => state.controlledAsymmetricPublicKey, - ); - - const [keyFormat, setKeyFormat] = useState( - AsymmetricKeyFormatValues.PEM, - ); - - useEffect(() => { - if (controlledAsymmetricPublicKey) { - setKeyFormat(controlledAsymmetricPublicKey.format); - } - }, [controlledAsymmetricPublicKey]); + const keyFormat = useDecoderStore((state) => state.asymmetricPublicKeyFormat); const handlePrivateKeyFormatChange = (value: string) => { - setKeyFormat(value as AsymmetricKeyFormatValues); - handlePublicKeyFormatChange(value as AsymmetricKeyFormatValues); }; diff --git a/src/features/decoder/services/decoder.store.ts b/src/features/decoder/services/decoder.store.ts index fa0993f35..3cd2c7ac4 100644 --- a/src/features/decoder/services/decoder.store.ts +++ b/src/features/decoder/services/decoder.store.ts @@ -83,7 +83,7 @@ type DecoderStoreActions = { handleAsymmetricPublicKeyChange: (newAsymmetricPublicKey: string) => void; handleAsymmetricPublicKeyFormatChange: ( newFormat: AsymmetricKeyFormatValues, - ) => void; + ) => Promise; resetControlledSymmetricSecretKey: () => void; resetControlledAsymmetricPublicKey: () => void; showUseHashWarning: () => void; @@ -232,16 +232,29 @@ export const useDecoderStore = create()( set(update); }, handleAsymmetricPublicKeyFormatChange: async (newFormat) => { - const { jwt, alg, asymmetricPublicKey } = get(); + const { jwt, alg, asymmetricPublicKey, asymmetricPublicKeyFormat } = + get(); const update = await TokenDecoderService.handleAsymmetricPublicKeyFormatChange({ jwt, alg, asymmetricPublicKey, - asymmetricPublicKeyFormat: newFormat, + sourceFormat: asymmetricPublicKeyFormat, + targetFormat: newFormat, }); + const currentState = get(); + + if ( + currentState.jwt !== jwt || + currentState.alg !== alg || + currentState.asymmetricPublicKey !== asymmetricPublicKey || + currentState.asymmetricPublicKeyFormat !== asymmetricPublicKeyFormat + ) { + return; + } + set(update); }, resetControlledSymmetricSecretKey: () => diff --git a/src/features/decoder/services/token-decoder.service.ts b/src/features/decoder/services/token-decoder.service.ts index 0e1672ccc..b8c9577ee 100644 --- a/src/features/decoder/services/token-decoder.service.ts +++ b/src/features/decoder/services/token-decoder.service.ts @@ -9,6 +9,7 @@ import { operationExceptionDictionary, } from "@/features/common/services/utils"; import { + convertAsymmetricPublicKeyFormat, getStringifiedHeaderAndPayload, isDigitalSignatureAlg, isHmacAlg, @@ -930,15 +931,37 @@ class _TokenDecoderService { jwt, alg, asymmetricPublicKey, - asymmetricPublicKeyFormat, + sourceFormat, + targetFormat, }: { jwt: string; alg: string; asymmetricPublicKey: string; - asymmetricPublicKeyFormat: AsymmetricKeyFormatValues; + sourceFormat: AsymmetricKeyFormatValues; + targetFormat: AsymmetricKeyFormatValues; }): Promise> { + const conversionResult = await convertAsymmetricPublicKeyFormat({ + alg, + key: asymmetricPublicKey, + sourceFormat, + targetFormat, + }); + + if (conversionResult.isErr()) { + return { + verificationInputErrors: [conversionResult.error], + }; + } + + const convertedPublicKey = conversionResult.value; const stateUpdate: Partial = { - asymmetricPublicKeyFormat: asymmetricPublicKeyFormat, + asymmetricPublicKey: convertedPublicKey, + asymmetricPublicKeyFormat: targetFormat, + controlledAsymmetricPublicKey: { + id: new Date().valueOf(), + value: convertedPublicKey, + format: targetFormat, + }, verificationInputErrors: null, signatureStatus: JwtSignatureStatusValues.UNKNOWN, signatureWarnings: null, @@ -948,8 +971,8 @@ class _TokenDecoderService { await this.handleAsymmetricPublicKeyHelper({ jwt: jwt, alg: alg, - asymmetricPublicKey: asymmetricPublicKey, - asymmetricPublicKeyFormat: asymmetricPublicKeyFormat, + asymmetricPublicKey: convertedPublicKey, + asymmetricPublicKeyFormat: targetFormat, }); if (handleAsymmetricPublicKeyHelperResult.isErr()) { diff --git a/src/features/encoder/components/token-encoder-key-format-picker.component.tsx b/src/features/encoder/components/token-encoder-key-format-picker.component.tsx index a005f448a..9b1c96cc6 100644 --- a/src/features/encoder/components/token-encoder-key-format-picker.component.tsx +++ b/src/features/encoder/components/token-encoder-key-format-picker.component.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import styles from "./token-encoder-key-format-picker.module.scss"; import { useEncoderStore } from "@/features/encoder/services/encoder.store"; import { getPickersUiDictionary } from "@/features/localization/services/ui-language-dictionary.service"; @@ -15,25 +15,13 @@ export const TokenEncoderKeyFormatPickerComponent: React.FC< const dictionary = getPickersUiDictionary(languageCode); const handlePrivateKeyFormatChange = useEncoderStore( - (state) => state.handleAsymmetricPrivateKeyFormatChange + (state) => state.handleAsymmetricPrivateKeyFormatChange, ); - const controlledAsymmetricPrivateKey$ = useEncoderStore( - (state) => state.controlledAsymmetricPrivateKey + const keyFormat = useEncoderStore( + (state) => state.asymmetricPrivateKeyFormat, ); - const [keyFormat, setKeyFormat] = useState( - AsymmetricKeyFormatValues.PEM - ); - - useEffect(() => { - if (controlledAsymmetricPrivateKey$) { - setKeyFormat(controlledAsymmetricPrivateKey$.format); - } - }, [controlledAsymmetricPrivateKey$]); - const onPrivateKeyFormatChange = (value: string) => { - setKeyFormat(value as AsymmetricKeyFormatValues); - handlePrivateKeyFormatChange(value as AsymmetricKeyFormatValues); }; diff --git a/src/features/encoder/services/encoder.store.ts b/src/features/encoder/services/encoder.store.ts index ba83a3c01..475b8334a 100644 --- a/src/features/encoder/services/encoder.store.ts +++ b/src/features/encoder/services/encoder.store.ts @@ -61,7 +61,7 @@ type EncoderStoreActions = { handleAsymmetricPrivateKeyChange: (newPrivateKey: string) => void; handleAsymmetricPrivateKeyFormatChange: ( newFormat: AsymmetricKeyFormatValues, - ) => void; + ) => Promise; resetControlledHeader: () => void; setControlledPayload: (value: string) => void; setControlledHeader: (value: string) => void; @@ -211,16 +211,36 @@ export const useEncoderStore = create()( set(update); }, handleAsymmetricPrivateKeyFormatChange: async (newFormat) => { - const { header, payload, asymmetricPrivateKey } = get(); + const { + alg, + header, + payload, + asymmetricPrivateKey, + asymmetricPrivateKeyFormat, + } = get(); const update = await TokenEncoderService.handleAsymmetricPrivateKeyFormatChange({ + alg, header, payload, asymmetricPrivateKey, - asymmetricPrivateKeyFormat: newFormat, + sourceFormat: asymmetricPrivateKeyFormat, + targetFormat: newFormat, }); + const currentState = get(); + + if ( + currentState.alg !== alg || + currentState.header !== header || + currentState.payload !== payload || + currentState.asymmetricPrivateKey !== asymmetricPrivateKey || + currentState.asymmetricPrivateKeyFormat !== asymmetricPrivateKeyFormat + ) { + return; + } + set(update); }, setControlledHeader: (value) => diff --git a/src/features/encoder/services/token-encoder.service.ts b/src/features/encoder/services/token-encoder.service.ts index 3b3af6a53..a91aae357 100644 --- a/src/features/encoder/services/token-encoder.service.ts +++ b/src/features/encoder/services/token-encoder.service.ts @@ -10,6 +10,7 @@ import { } from "@/features/common/models/decoded-token.model"; import { checkHmacSecretLength, + convertAsymmetricPrivateKeyFormat, createUnsecuredJwt, getAlgSize, getValidatedEncoderHeader, @@ -1335,15 +1336,37 @@ class _TokenEncoderService { } async handleAsymmetricPrivateKeyFormatChange(params: { + alg: string; header: string; payload: string; asymmetricPrivateKey: string; - asymmetricPrivateKeyFormat: AsymmetricKeyFormatValues; + sourceFormat: AsymmetricKeyFormatValues; + targetFormat: AsymmetricKeyFormatValues; }): Promise> { + const conversionResult = await convertAsymmetricPrivateKeyFormat({ + alg: params.alg, + key: params.asymmetricPrivateKey, + sourceFormat: params.sourceFormat, + targetFormat: params.targetFormat, + }); + + if (conversionResult.isErr()) { + return { + signingErrors: [conversionResult.error], + }; + } + + const asymmetricPrivateKey = conversionResult.value; const stateUpdate: Partial = { jwt: "", exampleAlg: "", - asymmetricPrivateKeyFormat: params.asymmetricPrivateKeyFormat, + asymmetricPrivateKey, + asymmetricPrivateKeyFormat: params.targetFormat, + controlledAsymmetricPrivateKey: { + id: new Date().valueOf(), + value: asymmetricPrivateKey, + format: params.targetFormat, + }, encodingWarnings: null, signingErrors: null, }; @@ -1352,8 +1375,8 @@ class _TokenEncoderService { await this.processAsymmetricPrivateKey({ header: params.header, payload: params.payload, - asymmetricPrivateKey: params.asymmetricPrivateKey, - asymmetricPrivateKeyFormat: params.asymmetricPrivateKeyFormat, + asymmetricPrivateKey, + asymmetricPrivateKeyFormat: params.targetFormat, }); if (processAsymmetricPrivateKeyResult.isErr()) { diff --git a/tests/decoder.store.test.ts b/tests/decoder.store.test.ts index 111360a2d..21eb1d9ef 100644 --- a/tests/decoder.store.test.ts +++ b/tests/decoder.store.test.ts @@ -14,6 +14,7 @@ import { import { TokenDecoderService } from "@/features/decoder/services/token-decoder.service"; const rs256 = DefaultTokensValues.RS256 as DefaultTokenWithKeysModel; +const rs256PublicJwk = rs256.jwk as { n: string; e: string }; const deferred = () => { let resolve!: (value: T) => void; @@ -301,3 +302,71 @@ describe("loadDecoderUrlInputs", () => { }); }); }); + +describe("handleAsymmetricPublicKeyFormatChange", () => { + beforeEach(() => { + useDecoderStore.setState({ + ...initialState, + jwt: rs256.token, + alg: "RS256", + asymmetricPublicKey: rs256.publicKey, + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.PEM, + signatureStatus: JwtSignatureStatusValues.VALID, + }); + }); + + test("re-encodes the public key and verifies atomically", async () => { + await useDecoderStore + .getState() + .handleAsymmetricPublicKeyFormatChange(AsymmetricKeyFormatValues.JWK); + + const jwkState = useDecoderStore.getState(); + + expect(JSON.parse(jwkState.asymmetricPublicKey)).toStrictEqual({ + kty: "RSA", + n: rs256PublicJwk.n, + e: rs256PublicJwk.e, + }); + expect(jwkState).toMatchObject({ + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.JWK, + controlledAsymmetricPublicKey: { + value: jwkState.asymmetricPublicKey, + format: AsymmetricKeyFormatValues.JWK, + }, + signatureStatus: JwtSignatureStatusValues.VALID, + verificationInputErrors: null, + }); + + await useDecoderStore + .getState() + .handleAsymmetricPublicKeyFormatChange(AsymmetricKeyFormatValues.PEM); + + expect(useDecoderStore.getState()).toMatchObject({ + asymmetricPublicKey: rs256.publicKey, + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.PEM, + controlledAsymmetricPublicKey: { + value: rs256.publicKey, + format: AsymmetricKeyFormatValues.PEM, + }, + signatureStatus: JwtSignatureStatusValues.VALID, + verificationInputErrors: null, + }); + }); + + test("keeps the current key and format when conversion fails", async () => { + useDecoderStore.setState({ + asymmetricPublicKey: "not a PEM key", + }); + + await useDecoderStore + .getState() + .handleAsymmetricPublicKeyFormatChange(AsymmetricKeyFormatValues.JWK); + + expect(useDecoderStore.getState()).toMatchObject({ + asymmetricPublicKey: "not a PEM key", + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.PEM, + signatureStatus: JwtSignatureStatusValues.VALID, + }); + expect(useDecoderStore.getState().verificationInputErrors).not.toBeNull(); + }); +}); diff --git a/tests/encoder.store.test.ts b/tests/encoder.store.test.ts new file mode 100644 index 000000000..7a2b73b16 --- /dev/null +++ b/tests/encoder.store.test.ts @@ -0,0 +1,79 @@ +import { beforeEach, describe, expect, test } from "vitest"; +import { + DefaultTokensValues, + DefaultTokenWithKeysModel, +} from "@/features/common/values/default-tokens.values"; +import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; +import { + initialState, + useEncoderStore, +} from "@/features/encoder/services/encoder.store"; + +const rs256 = DefaultTokensValues.RS256 as DefaultTokenWithKeysModel; + +describe("handleAsymmetricPrivateKeyFormatChange", () => { + beforeEach(() => { + useEncoderStore.setState({ + ...initialState, + alg: "RS256", + header: JSON.stringify({ alg: "RS256", typ: "JWT" }), + asymmetricPrivateKey: rs256.privateKey, + asymmetricPrivateKeyFormat: AsymmetricKeyFormatValues.PEM, + }); + }); + + test("re-encodes the private key and signs atomically", async () => { + await useEncoderStore + .getState() + .handleAsymmetricPrivateKeyFormatChange(AsymmetricKeyFormatValues.JWK); + + const jwkState = useEncoderStore.getState(); + const jwk = JSON.parse(jwkState.asymmetricPrivateKey); + + expect(jwk).toMatchObject({ + kty: "RSA", + d: expect.any(String), + }); + expect(jwkState).toMatchObject({ + asymmetricPrivateKeyFormat: AsymmetricKeyFormatValues.JWK, + controlledAsymmetricPrivateKey: { + value: jwkState.asymmetricPrivateKey, + format: AsymmetricKeyFormatValues.JWK, + }, + signingErrors: null, + }); + expect(jwkState.jwt).toMatch( + /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, + ); + + await useEncoderStore + .getState() + .handleAsymmetricPrivateKeyFormatChange(AsymmetricKeyFormatValues.PEM); + + expect(useEncoderStore.getState()).toMatchObject({ + asymmetricPrivateKey: rs256.privateKey, + asymmetricPrivateKeyFormat: AsymmetricKeyFormatValues.PEM, + controlledAsymmetricPrivateKey: { + value: rs256.privateKey, + format: AsymmetricKeyFormatValues.PEM, + }, + signingErrors: null, + }); + }); + + test("keeps the current key and format when conversion fails", async () => { + useEncoderStore.setState({ + asymmetricPrivateKey: "not a PEM key", + }); + + await useEncoderStore + .getState() + .handleAsymmetricPrivateKeyFormatChange(AsymmetricKeyFormatValues.JWK); + + expect(useEncoderStore.getState()).toMatchObject({ + asymmetricPrivateKey: "not a PEM key", + asymmetricPrivateKeyFormat: AsymmetricKeyFormatValues.PEM, + }); + expect(useEncoderStore.getState().signingErrors).not.toBeNull(); + }); +}); diff --git a/tests/jwt.service.test.ts b/tests/jwt.service.test.ts index a65ea4d25..b34203f23 100644 --- a/tests/jwt.service.test.ts +++ b/tests/jwt.service.test.ts @@ -4,6 +4,8 @@ import { DefaultTokenWithKeysModel, } from "@/features/common/values/default-tokens.values"; import { + convertAsymmetricPrivateKeyFormat, + convertAsymmetricPublicKeyFormat, getAsymmetricKeyCryptoKey, getJwk, isMlDsaSupported, @@ -332,6 +334,75 @@ describe("Ed25519", () => { }); }); +describe("asymmetric key format conversion", () => { + const token = DefaultTokensValues.RS256 as DefaultTokenWithKeysModel; + const publicJwk = token.jwk as { n: string; e: string }; + + test("converts private keys between PEM and JWK", async () => { + const jwkResult = await convertAsymmetricPrivateKeyFormat({ + alg: "RS256", + key: token.privateKey, + sourceFormat: AsymmetricKeyFormatValues.PEM, + targetFormat: AsymmetricKeyFormatValues.JWK, + }); + + expect(jwkResult.isOk()).toBe(true); + expect(JSON.parse(jwkResult._unsafeUnwrap())).toMatchObject({ + kty: "RSA", + d: expect.any(String), + n: expect.any(String), + e: expect.any(String), + }); + + const pemResult = await convertAsymmetricPrivateKeyFormat({ + alg: "RS256", + key: jwkResult._unsafeUnwrap(), + sourceFormat: AsymmetricKeyFormatValues.JWK, + targetFormat: AsymmetricKeyFormatValues.PEM, + }); + + expect(pemResult._unsafeUnwrap()).toMatch( + /^-----BEGIN PRIVATE KEY-----[\s\S]+-----END PRIVATE KEY-----$/, + ); + }); + + test("converts public keys between PEM and JWK", async () => { + const jwkResult = await convertAsymmetricPublicKeyFormat({ + alg: "RS256", + key: token.publicKey, + sourceFormat: AsymmetricKeyFormatValues.PEM, + targetFormat: AsymmetricKeyFormatValues.JWK, + }); + + expect(jwkResult.isOk()).toBe(true); + expect(JSON.parse(jwkResult._unsafeUnwrap())).toStrictEqual({ + kty: "RSA", + n: publicJwk.n, + e: publicJwk.e, + }); + + const pemResult = await convertAsymmetricPublicKeyFormat({ + alg: "RS256", + key: jwkResult._unsafeUnwrap(), + sourceFormat: AsymmetricKeyFormatValues.JWK, + targetFormat: AsymmetricKeyFormatValues.PEM, + }); + + expect(pemResult._unsafeUnwrap()).toBe(token.publicKey); + }); + + test("does not relabel an invalid key", async () => { + const result = await convertAsymmetricPublicKeyFormat({ + alg: "RS256", + key: "not a PEM key", + sourceFormat: AsymmetricKeyFormatValues.PEM, + targetFormat: AsymmetricKeyFormatValues.JWK, + }); + + expect(result.isErr()).toBe(true); + }); +}); + describe("ML-DSA", () => { afterEach(() => { vi.unstubAllGlobals(); From f4160c329aa28a96d28a4df2b55521edcecc380d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 17:08:48 +0200 Subject: [PATCH 09/12] feat: format JWK public keys Signed-off-by: Filip Skokan --- e2e/decoder-fragment-links.spec.ts | 20 +++++++++- e2e/decoder.spec.ts | 23 ++++++++++- .../components/secret-key-input.component.tsx | 24 +++++++---- ...erification-public-key-input.component.tsx | 39 ++++++++++++++++-- .../services/public-key-input.service.ts | 26 ++++++++++++ tests/public-key-input.service.test.ts | 40 +++++++++++++++++++ 6 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 src/features/decoder/services/public-key-input.service.ts create mode 100644 tests/public-key-input.service.test.ts diff --git a/e2e/decoder-fragment-links.spec.ts b/e2e/decoder-fragment-links.spec.ts index fb1f9d029..ba4d23340 100644 --- a/e2e/decoder-fragment-links.spec.ts +++ b/e2e/decoder-fragment-links.spec.ts @@ -69,8 +69,26 @@ const expectPublicKeyLoaded = async ({ const publicKeyRegion = page .getByRole("region") .filter({ has: publicKeyEditor }); + const expectedPublicKey = + publicKeyFormat === "JWK" + ? JSON.stringify(JSON.parse(publicKey), null, 2) + : publicKey; + + await expect(publicKeyEditor.getByRole("textbox")).toHaveValue( + expectedPublicKey, + ); + + if (publicKeyFormat === "JWK") { + await expect( + publicKeyEditor.locator("pre .token.property").first(), + ).toBeVisible(); + await expect( + publicKeyEditor.locator("pre .token.string").first(), + ).toBeVisible(); + } else { + await expect(publicKeyEditor.locator("pre")).toHaveCount(0); + } - await expect(publicKeyEditor.getByRole("textbox")).toHaveValue(publicKey); await expect( publicKeyEditor.getByTestId( dataTestidDictionary.decoder.secretKeyEditor.statusBar.success.id, diff --git a/e2e/decoder.spec.ts b/e2e/decoder.spec.ts index 9e70cd6bd..a282d142a 100644 --- a/e2e/decoder.spec.ts +++ b/e2e/decoder.spec.ts @@ -526,6 +526,21 @@ test.describe("decode JWTs", () => { }) .toStrictEqual(entrywithJwkKey.publicKey); + await expect + .poll(async () => { + const value = await secretKeyEditorInput.inputValue(); + + try { + return value === JSON.stringify(JSON.parse(value), null, 2); + } catch { + return false; + } + }) + .toBe(true); + await expect( + secretKeyEditor.locator("pre .token.property").first(), + ).toBeVisible(); + await checkJwtEditorStatusBarMessage({ page, type: MessageTypeValue.SUCCESS, @@ -607,8 +622,14 @@ test.describe("decode JWTs", () => { .click(); await secretKeyEditorInput.fill( - JSON.stringify(entrywithJwkKey.publicKey, null, 2) + JSON.stringify(entrywithJwkKey.publicKey), + ); + await expect(secretKeyEditorInput).toHaveValue( + JSON.stringify(entrywithJwkKey.publicKey, null, 2), ); + await expect( + secretKeyEditor.locator("pre .token.property").first(), + ).toBeVisible(); await checkJwtEditorStatusBarMessage({ page, diff --git a/src/features/decoder/components/secret-key-input.component.tsx b/src/features/decoder/components/secret-key-input.component.tsx index 35a937b5c..c1f80d8eb 100644 --- a/src/features/decoder/components/secret-key-input.component.tsx +++ b/src/features/decoder/components/secret-key-input.component.tsx @@ -17,7 +17,7 @@ import { CardToolbarComponent } from "@/features/common/components/card-toolbar/ import { CardToolbarCopyButtonComponent } from "@/features/common/components/card-toolbar-buttons/card-toolbar-copy-button/card-toolbar-copy-button.component"; import { CardToolbarClearButtonComponent } from "@/features/common/components/card-toolbar-buttons/card-toolbar-clear-button/card-toolbar-clear-button.component"; import { NOOP_ALG } from "@/features/common/values/constants"; - +import { formatPublicKeyInput } from "@/features/decoder/services/public-key-input.service"; type SecretKeyInputComponentProps = { languageCode: string; @@ -41,6 +41,9 @@ export const SecretKeyInputComponent: React.FC< ); const alg$ = useDecoderStore((state) => state.alg); + const asymmetricPublicKeyFormat$ = useDecoderStore( + (state) => state.asymmetricPublicKeyFormat, + ); const verificationInputErrors$ = useDecoderStore( (state) => state.verificationInputErrors, ); @@ -69,7 +72,12 @@ export const SecretKeyInputComponent: React.FC< useEffect(() => { if (controlledAsymmetricPublicKey) { - setPublicKey(controlledAsymmetricPublicKey.value.trim()); + setPublicKey( + formatPublicKeyInput( + controlledAsymmetricPublicKey.value, + controlledAsymmetricPublicKey.format, + ), + ); } }, [controlledAsymmetricPublicKey]); @@ -97,12 +105,11 @@ export const SecretKeyInputComponent: React.FC< handleSymmetricSecretKeyChange$(cleanValue); }; - const handleAsymmetricPublicKeyChange = async ( - e: ChangeEvent, - ) => { - const key = e.target.value; - - const cleanValue = key.trim(); + const handleAsymmetricPublicKeyChange = async (key: string) => { + const cleanValue = formatPublicKeyInput( + key, + asymmetricPublicKeyFormat$, + ); setPublicKey(cleanValue); @@ -173,6 +180,7 @@ export const SecretKeyInputComponent: React.FC< {isDigitalSignatureAlg(alg$) && ( diff --git a/src/features/decoder/components/signature-verification-public-key-input.component.tsx b/src/features/decoder/components/signature-verification-public-key-input.component.tsx index 7b48b66ce..b5a0e64d1 100644 --- a/src/features/decoder/components/signature-verification-public-key-input.component.tsx +++ b/src/features/decoder/components/signature-verification-public-key-input.component.tsx @@ -1,18 +1,51 @@ import React, { ChangeEvent } from "react"; import { WidgetTextareaComponent } from "@/features/common/components/widget-textarea/widget-textarea.component"; +import { highlight, languages } from "prismjs"; +import "prismjs/components/prism-json"; +import { EditorComponent } from "@/features/common/components/code-editor/editor.component"; +import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; interface SignatureVerificationPublicKeyInputComponentProps { publicKey: string; - handlePublicKeyChange: (e: ChangeEvent) => void; + publicKeyFormat: AsymmetricKeyFormatValues; + handlePublicKeyChange: (value: string) => void; placeholder: string; } export const SignatureVerificationPublicKeyInputComponent: React.FC< SignatureVerificationPublicKeyInputComponentProps -> = ({ publicKey, handlePublicKeyChange, placeholder }) => { +> = ({ + publicKey, + publicKeyFormat, + handlePublicKeyChange, + placeholder, +}) => { + if (publicKeyFormat === AsymmetricKeyFormatValues.JWK) { + return ( + highlight(code, languages.json, "json")} + padding="1rem" + placeholder={placeholder} + style={{ + fontFamily: '"Roboto Mono", monospace', + fontSize: 14, + lineHeight: 1.4, + border: "none", + outline: "none", + }} + /> + ); + } + + const handleChange = (event: ChangeEvent) => { + handlePublicKeyChange(event.target.value); + }; + return ( diff --git a/src/features/decoder/services/public-key-input.service.ts b/src/features/decoder/services/public-key-input.service.ts new file mode 100644 index 000000000..8483c0f5f --- /dev/null +++ b/src/features/decoder/services/public-key-input.service.ts @@ -0,0 +1,26 @@ +import { + parseStringIntoValidJsonObject, + stringifyJsonObject, +} from "@/features/common/services/jwt.service"; +import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; + +export const formatPublicKeyInput = ( + value: string, + format: AsymmetricKeyFormatValues, +): string => { + const trimmedValue = value.trim(); + + if (format !== AsymmetricKeyFormatValues.JWK) { + return trimmedValue; + } + + const parseResult = parseStringIntoValidJsonObject(trimmedValue); + + if (parseResult.isErr()) { + return trimmedValue; + } + + const stringifyResult = stringifyJsonObject(parseResult.value); + + return stringifyResult.isOk() ? stringifyResult.value : trimmedValue; +}; diff --git a/tests/public-key-input.service.test.ts b/tests/public-key-input.service.test.ts new file mode 100644 index 000000000..3be42fa26 --- /dev/null +++ b/tests/public-key-input.service.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "vitest"; +import { formatPublicKeyInput } from "@/features/decoder/services/public-key-input.service"; +import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; + +describe("formatPublicKeyInput", () => { + test("formats valid JWK objects", () => { + expect( + formatPublicKeyInput( + '{"kty":"RSA","n":"modulus","e":"AQAB"}', + AsymmetricKeyFormatValues.JWK, + ), + ).toBe( + JSON.stringify( + { + kty: "RSA", + n: "modulus", + e: "AQAB", + }, + null, + 2, + ), + ); + }); + + test.each(["not JSON", "[]", "null"])( + "leaves invalid JWK input unchanged: %s", + (value) => { + expect( + formatPublicKeyInput(value, AsymmetricKeyFormatValues.JWK), + ).toBe(value); + }, + ); + + test("leaves PEM input unchanged", () => { + const pem = + "-----BEGIN PUBLIC KEY-----\npublic key\n-----END PUBLIC KEY-----"; + + expect(formatPublicKeyInput(pem, AsymmetricKeyFormatValues.PEM)).toBe(pem); + }); +}); From 4c01593470600ce5742521a6965877d4513b323b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 17:09:59 +0200 Subject: [PATCH 10/12] feat: enable ML-DSA origin trial Signed-off-by: Filip Skokan --- e2e/metadata.spec.ts | 17 +++++++++++++++++ .../page-layout/page-layout.component.tsx | 9 +++++++++ 2 files changed, 26 insertions(+) create mode 100644 e2e/metadata.spec.ts diff --git a/e2e/metadata.spec.ts b/e2e/metadata.spec.ts new file mode 100644 index 000000000..89a724844 --- /dev/null +++ b/e2e/metadata.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from "@playwright/test"; +import { E2E_BASE_URL } from "./e2e.utils"; + +const EXPECTED_WEB_CRYPTO_PQC_ORIGIN_TRIAL_TOKEN = + "AnishhJF49gYd6FyezhA4gjpUPHOtlKPeoII1gtrsUrI6G1WVbIvL1AkNsBe9GQgen/8TIHHpWa8I8nFjbZObwMAAABneyJvcmlnaW4iOiJodHRwczovL3d3dy5qd3QuaW86NDQzIiwiZmVhdHVyZSI6IldlYkNyeXB0b0FkZGl0aW9uYWxBbGdvcml0aG1zMjAyNjA2IiwiZXhwaXJ5IjoxNzkyNDU0NDAwfQ=="; + +test("includes the WebCrypto PQC origin-trial token", async ({ page }) => { + await page.goto(E2E_BASE_URL); + + const meta = page.locator('head meta[http-equiv="origin-trial"]'); + + await expect(meta).toHaveCount(1); + await expect(meta).toHaveAttribute( + "content", + EXPECTED_WEB_CRYPTO_PQC_ORIGIN_TRIAL_TOKEN, + ); +}); diff --git a/src/features/common/components/layout/page-layout/page-layout.component.tsx b/src/features/common/components/layout/page-layout/page-layout.component.tsx index e9b0182e1..0d3301918 100644 --- a/src/features/common/components/layout/page-layout/page-layout.component.tsx +++ b/src/features/common/components/layout/page-layout/page-layout.component.tsx @@ -6,6 +6,11 @@ import { FooterComponent } from "@/features/common/components/footer/footer.comp import { ThemeCookieValues } from "@/features/common/values/theme.values"; import { PageHeaderComponent } from "@/features/common/components/layout/page-header/page-header.component"; +// WebCrypto PQC origin trial: +// https://developer.chrome.com/origintrials/#/view_trial/1379790335835635713 +const WEB_CRYPTO_PQC_ORIGIN_TRIAL_TOKEN = + "AnishhJF49gYd6FyezhA4gjpUPHOtlKPeoII1gtrsUrI6G1WVbIvL1AkNsBe9GQgen/8TIHHpWa8I8nFjbZObwMAAABneyJvcmlnaW4iOiJodHRwczovL3d3dy5qd3QuaW86NDQzIiwiZmVhdHVyZSI6IldlYkNyeXB0b0FkZGl0aW9uYWxBbGdvcml0aG1zMjAyNjA2IiwiZXhwaXJ5IjoxNzkyNDU0NDAwfQ=="; + interface LayoutComponentProps extends PropsWithChildren { languageCode: string; themeCode: ThemeCookieValues; @@ -21,6 +26,10 @@ export const PageLayoutComponent: React.FC = ({ return ( + {languageCode === "ja" && ( )} From 7cc962bd359a657168dfb26ab1fa42010b2e5556 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 30 Jul 2026 17:41:07 +0200 Subject: [PATCH 11/12] test: stabilize key format picker interactions Signed-off-by: Filip Skokan --- e2e/decoder.spec.ts | 53 ++++++++++++++++++++++++++++++--------------- e2e/encoder.spec.ts | 29 +++++++++++++++++-------- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/e2e/decoder.spec.ts b/e2e/decoder.spec.ts index a282d142a..725a0590d 100644 --- a/e2e/decoder.spec.ts +++ b/e2e/decoder.spec.ts @@ -504,17 +504,22 @@ test.describe("decode JWTs", () => { status: MessageStatusValue.VISIBLE, }); - const formatPicker = page.locator( - ".react-select__single-value" - ); - - await formatPicker.click(); - + const pemFormatPickerControl = page + .locator(".react-select__single-value") + .filter({ hasText: entry.publicKeyFormat }) + .locator( + 'xpath=ancestor::div[contains(@class, "react-select__control")]' + ); + await pemFormatPickerControl.dispatchEvent("mousedown", { + button: 0, + }); await page + .getByRole("listbox") .getByRole("option", { name: entrywithJwkKey.publicKeyFormat, + exact: true, }) - .click(); + .dispatchEvent("click"); await expect .poll(async () => { @@ -569,13 +574,22 @@ test.describe("decode JWTs", () => { expect(decodedHeader).toBe(entrywithJwkKey.header); expect(decodedPayload).toBe(entrywithJwkKey.payload); - await formatPicker.click(); - + const jwkFormatPickerControl = page + .locator(".react-select__single-value") + .filter({ hasText: entrywithJwkKey.publicKeyFormat }) + .locator( + 'xpath=ancestor::div[contains(@class, "react-select__control")]' + ); + await jwkFormatPickerControl.dispatchEvent("mousedown", { + button: 0, + }); await page + .getByRole("listbox") .getByRole("option", { name: entry.publicKeyFormat, + exact: true, }) - .click(); + .dispatchEvent("click"); await expect(secretKeyEditorInput).toHaveValue(entry.publicKey); @@ -609,17 +623,22 @@ test.describe("decode JWTs", () => { status: MessageStatusValue.VISIBLE, }); - const formatPicker = page.locator( - ".react-select__single-value" - ); - - await formatPicker.click(); - + const pemFormatPickerControl = page + .locator(".react-select__single-value") + .filter({ hasText: entry.publicKeyFormat }) + .locator( + 'xpath=ancestor::div[contains(@class, "react-select__control")]' + ); + await pemFormatPickerControl.dispatchEvent("mousedown", { + button: 0, + }); await page + .getByRole("listbox") .getByRole("option", { name: entrywithJwkKey.publicKeyFormat, + exact: true, }) - .click(); + .dispatchEvent("click"); await secretKeyEditorInput.fill( JSON.stringify(entrywithJwkKey.publicKey), diff --git a/e2e/encoder.spec.ts b/e2e/encoder.spec.ts index 04893625a..711db294a 100644 --- a/e2e/encoder.spec.ts +++ b/e2e/encoder.spec.ts @@ -463,16 +463,20 @@ test.describe("encode JWTs", () => { ); } - const formatPicker = page.locator( - ".react-select__single-value" - ); - await formatPicker.click(); - + const pemFormatPickerControl = page + .locator(".react-select__single-value") + .filter({ hasText: tokenWithPemKey.privateKeyFormat }) + .locator( + 'xpath=ancestor::div[contains(@class, "react-select__control")]' + ); + await pemFormatPickerControl.dispatchEvent("mousedown", { button: 0 }); await page + .getByRole("listbox") .getByRole("option", { name: tokenWithJwkKey.privateKeyFormat, + exact: true, }) - .click(); + .dispatchEvent("click"); await expect .poll(async () => { @@ -498,13 +502,20 @@ test.describe("encode JWTs", () => { ); } - await formatPicker.click(); - + const jwkFormatPickerControl = page + .locator(".react-select__single-value") + .filter({ hasText: tokenWithJwkKey.privateKeyFormat }) + .locator( + 'xpath=ancestor::div[contains(@class, "react-select__control")]' + ); + await jwkFormatPickerControl.dispatchEvent("mousedown", { button: 0 }); await page + .getByRole("listbox") .getByRole("option", { name: tokenWithPemKey.privateKeyFormat, + exact: true, }) - .click(); + .dispatchEvent("click"); await expect(secretKeyEditorInput).toHaveValue( tokenWithPemKey.privateKey From 687466ad88647277a09bf98fae6379023916f71d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 31 Jul 2026 13:36:46 +0200 Subject: [PATCH 12/12] fix: include all library filter options Signed-off-by: Filip Skokan --- e2e/libraries.spec.ts | 23 ++++ src/app/[language]/libraries/page.tsx | 28 ++--- src/data/libraries-next.json | 2 +- .../library-card/library-card.component.tsx | 98 ++++------------- .../libraries/models/library.model.ts | 34 +----- .../values/library-support.values.ts | 102 ++++++++++++++++++ tests/library-support.values.test.ts | 26 +++++ 7 files changed, 185 insertions(+), 128 deletions(-) create mode 100644 e2e/libraries.spec.ts create mode 100644 src/features/libraries/values/library-support.values.ts create mode 100644 tests/library-support.values.test.ts diff --git a/e2e/libraries.spec.ts b/e2e/libraries.spec.ts new file mode 100644 index 000000000..2f70c7135 --- /dev/null +++ b/e2e/libraries.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from "@playwright/test"; +import { E2E_BASE_URL } from "./e2e.utils"; + +test("filters libraries by ML-DSA algorithm", async ({ page }) => { + await page.goto(`${E2E_BASE_URL}/libraries`); + + const filterBy = page.getByRole("combobox", { + name: "Debugger picker", + }); + await expect(filterBy).toBeVisible(); + await page.getByText("All", { exact: true }).click(); + + for (const algorithm of ["ML-DSA-44", "ML-DSA-65", "ML-DSA-87"]) { + await expect( + page.getByRole("option", { name: algorithm, exact: true }), + ).toBeVisible(); + } + + await page.getByRole("option", { name: "ML-DSA-44", exact: true }).click(); + + await expect(page).toHaveURL(`${E2E_BASE_URL}/libraries?algorithm=ml-dsa-44`); + await expect(page.getByText("panva/jose", { exact: true })).toHaveCount(4); +}); diff --git a/src/app/[language]/libraries/page.tsx b/src/app/[language]/libraries/page.tsx index 08de17764..56074711d 100644 --- a/src/app/[language]/libraries/page.tsx +++ b/src/app/[language]/libraries/page.tsx @@ -19,6 +19,10 @@ import { siteTree } from "@/features/seo/site-tree"; import { getAuth0Dictionary } from "@/features/localization/services/ui-language-dictionary.service"; import { LibraryModel } from "@/features/libraries/models/library.model"; import { LibrariesDictionaryModel } from "@/features/localization/models/libraries-dictionary.model"; +import { + libraryAlgorithmDefinitions, + librarySupportDefinitions, +} from "@/features/libraries/values/library-support.values"; export async function generateMetadata({ params: { language }, @@ -55,31 +59,19 @@ export default function Libraries({ const support = searchParams?.support; const query = programmingLanguage ?? algorithm ?? support ?? ""; const dictionary = JSON.parse(source) as LibraryDictionaryModel; - const allOptions = Object.keys(Object.values(dictionary)[0].libs[0].support); - const indexAlgorithmStart = allOptions.findIndex( - (option) => option === "hs256" - ); const categoryOptions: { id: string; name: string }[] = Object.values( - dictionary + dictionary, ).map((library) => ({ id: library.id, name: library.name, })); - const supportOptions: { value: string; label: string }[] = allOptions - .slice(0, indexAlgorithmStart) - .map((key) => ({ - value: key, - label: key.toUpperCase(), - })); + const supportOptions: { value: string; label: string }[] = + librarySupportDefinitions.map(({ value, label }) => ({ value, label })); - const algorithmOptions: { value: string; label: string }[] = allOptions - .slice(indexAlgorithmStart) - .map((key) => ({ - value: key, - label: key.toUpperCase(), - })); + const algorithmOptions: { value: string; label: string }[] = + libraryAlgorithmDefinitions.map(({ value, label }) => ({ value, label })); const categories: LibraryCategoryModel[] = programmingLanguage && programmingLanguage !== "all" @@ -91,7 +83,7 @@ export default function Libraries({ const filteredCategories = categoryToFilter ? categories.map((category) => { const filteredLibs = category.libs.filter( - (lib) => lib.support[categoryToFilter] + (lib) => lib.support[categoryToFilter], ); return { ...category, diff --git a/src/data/libraries-next.json b/src/data/libraries-next.json index 83156759e..f7dc4fdd6 100644 --- a/src/data/libraries-next.json +++ b/src/data/libraries-next.json @@ -2144,7 +2144,7 @@ "ps256": true, "ps384": true, "ps512": true, - "EdDSA": true + "eddsa": true }, "authorUrl": "https://github.com/inverno-io", "authorName": "Inverno Framework", diff --git a/src/features/libraries/components/library-card/library-card.component.tsx b/src/features/libraries/components/library-card/library-card.component.tsx index 628e4e76e..bdff1ad0c 100644 --- a/src/features/libraries/components/library-card/library-card.component.tsx +++ b/src/features/libraries/components/library-card/library-card.component.tsx @@ -14,6 +14,10 @@ import { GithubIconComponent } from "@/features/libraries/components/github-icon import rehypeRaw from "rehype-raw"; import { LibrariesDictionaryModel } from "@/features/localization/models/libraries-dictionary.model"; import Link from "next/link"; +import { + libraryAlgorithmDefinitions, + librarySupportDefinitions, +} from "@/features/libraries/values/library-support.values"; interface ConfigItemComponentProps { isSupported: boolean | undefined; @@ -118,7 +122,7 @@ export const LibraryCardComponent: React.FC = ({ {library.gitHubRepoPath || library.altRepoPath || name}
-
+
{authorUrl ? ( = ({
    - - - - - - - - - - - + {librarySupportDefinitions.map(({ value, cardLabel, code }) => ( + + ))}
    - - - - - - - - - - - - - - - - - - - + {libraryAlgorithmDefinitions.map(({ value, label }) => ( + + ))}
{minimumVersion && ( diff --git a/src/features/libraries/models/library.model.ts b/src/features/libraries/models/library.model.ts index 8ad9bedef..497330690 100644 --- a/src/features/libraries/models/library.model.ts +++ b/src/features/libraries/models/library.model.ts @@ -1,36 +1,8 @@ +import type { LibrarySupport } from "@/features/libraries/values/library-support.values"; + export interface LibraryModel { minimumVersion: string | null; - support: { - sign: boolean; - verify: boolean; - iss: boolean; - sub: boolean; - aud: boolean; - exp: boolean; - nbf: boolean; - iat: boolean; - jti: boolean; - typ?: boolean; - hs256: boolean; - hs384: boolean; - hs512: boolean; - rs256: boolean; - rs384: boolean; - rs512: boolean; - es256: boolean; - es384: boolean; - es512: boolean; - ps256?: boolean; - ps384?: boolean; - ps512?: boolean; - eddsa?: boolean; - es256k?: boolean; - ed25519?: boolean; - ed448?: boolean; - "ml-dsa-44"?: boolean; - "ml-dsa-65"?: boolean; - "ml-dsa-87"?: boolean; - }; + support: LibrarySupport; authorUrl: string | null; authorName: string; gitHubRepoPath: string | null; diff --git a/src/features/libraries/values/library-support.values.ts b/src/features/libraries/values/library-support.values.ts new file mode 100644 index 000000000..6fb64c640 --- /dev/null +++ b/src/features/libraries/values/library-support.values.ts @@ -0,0 +1,102 @@ +interface LibrarySupportDefinition { + value: string; + label: string; + cardLabel: string; + code: string | null; +} + +interface LibraryAlgorithmDefinition { + value: string; + label: string; +} + +export const librarySupportDefinitions = [ + { + value: "sign", + label: "SIGN", + cardLabel: "Sign", + code: null, + }, + { + value: "verify", + label: "VERIFY", + cardLabel: "Verify", + code: null, + }, + { + value: "iss", + label: "ISS", + cardLabel: "check", + code: "iss", + }, + { + value: "sub", + label: "SUB", + cardLabel: "check", + code: "sub", + }, + { + value: "aud", + label: "AUD", + cardLabel: "check", + code: "aud", + }, + { + value: "exp", + label: "EXP", + cardLabel: "check", + code: "exp", + }, + { + value: "nbf", + label: "NBF", + cardLabel: "check", + code: "nbf", + }, + { + value: "iat", + label: "IAT", + cardLabel: "check", + code: "iat", + }, + { + value: "jti", + label: "JTI", + cardLabel: "check", + code: "jti", + }, + { + value: "typ", + label: "TYP", + cardLabel: "check", + code: "typ", + }, +] as const satisfies readonly LibrarySupportDefinition[]; + +export const libraryAlgorithmDefinitions = [ + { value: "hs256", label: "HS256" }, + { value: "hs384", label: "HS384" }, + { value: "hs512", label: "HS512" }, + { value: "rs256", label: "RS256" }, + { value: "rs384", label: "RS384" }, + { value: "rs512", label: "RS512" }, + { value: "es256", label: "ES256" }, + { value: "es256k", label: "ES256K" }, + { value: "es384", label: "ES384" }, + { value: "es512", label: "ES512" }, + { value: "ps256", label: "PS256" }, + { value: "ps384", label: "PS384" }, + { value: "ps512", label: "PS512" }, + { value: "eddsa", label: "EdDSA" }, + { value: "ed25519", label: "Ed25519" }, + { value: "ed448", label: "Ed448" }, + { value: "ml-dsa-44", label: "ML-DSA-44" }, + { value: "ml-dsa-65", label: "ML-DSA-65" }, + { value: "ml-dsa-87", label: "ML-DSA-87" }, +] as const satisfies readonly LibraryAlgorithmDefinition[]; + +type LibrarySupportKey = + | (typeof librarySupportDefinitions)[number]["value"] + | (typeof libraryAlgorithmDefinitions)[number]["value"]; + +export type LibrarySupport = Partial>; diff --git a/tests/library-support.values.test.ts b/tests/library-support.values.test.ts new file mode 100644 index 000000000..97e17c85d --- /dev/null +++ b/tests/library-support.values.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "vitest"; +import libraries from "@/data/libraries-next.json"; +import type { LibraryDictionaryModel } from "@/features/libraries/models/library-dictionary.model"; +import { + libraryAlgorithmDefinitions, + librarySupportDefinitions, +} from "@/features/libraries/values/library-support.values"; + +const dictionary = libraries as LibraryDictionaryModel; + +describe("library support definitions", () => { + test("cover every support field in the library data", () => { + const definedValues = [ + ...librarySupportDefinitions, + ...libraryAlgorithmDefinitions, + ].map(({ value }) => value); + const dataValues = new Set( + Object.values(dictionary).flatMap(({ libs }) => + libs.flatMap(({ support }) => Object.keys(support)), + ), + ); + + expect(new Set(definedValues).size).toBe(definedValues.length); + expect(Array.from(dataValues).sort()).toEqual([...definedValues].sort()); + }); +});