diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts index b52c2e019..b7f843092 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts @@ -7,6 +7,7 @@ import { assertRejects, } from '@std/assert'; import { returnsNext, stub } from '@std/testing/mock'; +import { lessThan, parse } from '@std/semver'; import { verifyAuthenticationResponse } from './verifyAuthenticationResponse.ts'; @@ -838,6 +839,69 @@ Deno.test('should throw when topOrigin is set despite crossOrigin being false', } }); +Deno.test( + 'should verify ML-DSA-44 authentication response', + /** + * ML-DSA is only supported in Deno v2.8.2+ + */ + { ignore: lessThan(parse(Deno.version.deno), parse('2.8.2')) }, + async () => { + const verification = await verifyAuthenticationResponse({ + response: { + id: '-EM9FDFIdFVeqWdTycRjoZVN2ZS4vnVE-MBpg7k0pl4jpuqj4GnMCW3Wqlm2WWI2PQ', + rawId: '-EM9FDFIdFVeqWdTycRjoZVN2ZS4vnVE-MBpg7k0pl4jpuqj4GnMCW3Wqlm2WWI2PQ', + response: { + authenticatorData: 'dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAACA', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiSmkxNTk3MWpTRVNhOWhhQ1VZYjdzX3BNaFY4RE5Od1lUOFdiNXpiRW8xNTFBYjdzX011VC1fTUlqbm91c2ZhRjJRM2VtRkF4N0drcFhrVFVtTWljVFEiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0', + signature: + 'e7L-Xli-2lj9ZlP2s26sbrvFGLkVrz74BZnDsLW-7HOhj7AcEl5Zgtm3VLvLtcrfqyKE0PTuFrswsikm7t6ddhxXphxWcSo4ggarl6ODQk8NdPCYoFhoK8qwpqKZKmJAl9xDsJE1HAudrWLgq_747JV4QmGLizK0_oJgGM7WLd5xVYvKsl14odBFjU_ZBCrjB0UHIMg8aAq1727yZnY1eiNeF_sEmci_pigYCo-MbxbHmQWPp-U75sGSPPfK0soN2-29_aIxRO4Fg8P37WrwVUrEFdG2PFNgAhcM-ljjyv3mkCfsLUiQNuS-a0cn6MeygREc2HBwE6ChS351-dpNTbkfnb-o1fA6suP1sh3-i7YZrEn9e2J7UZxIAEJPpmuKxYFA4Fj0lAGUhi3lvnkWPOnS8BUjPr5q5z5iEbyL4MokDP75G723Tyy-5L8u1pLmlSLiuwvuW5MBkEhjVVj0RpVSnCoqzwE9A9ZmqZx5wv5gQEi4hAA64mSoXGdUZ5EGkPGrrIDjGyIOrLjuHOSZ6hjyioZvMA1nCQJ76oaL5-Pn1FR1VTurI5ccTWrDAo5sHuo8uGjx9bsyy_aMT3Nzosu29PArTm0AkJFd7INXky0L9itCmhujSnali9zTO8UuwV0G8sZeB2BG4VZN2nkjT1Ib8VeBnSMTlIFVOI2JHlD9kePZuV3nCuAvK8j5qH5OPJoEeJzuxGHP2k7f0941kzyW9sjBaD90HEusVGGgST0qWigEKU3kKaO_Du5ZngcqlmKnnFVKXQk5mEV5nFs9ia76sKe9FKYUp_ZxsVFATcjXETW5GuXF1qIj0ZTCSmhn8V_cqEH29fQWyy9qxNa6kkKc4koZUP36B-h5yVawIDdfyjl-VueUvUyEunPv1EyqXQaf9bo-WThxD_5v3Bd2sYTOI__0PIsUvCASjZJMQU4jpwyXoR2EsLWDRD4fsAxLmdao0iXNxdlH0Ys2MqkXkkMbIylccEHkFjbm_VB5tPYQkFRqqRX13KUyYqqTpaT6MdD8IpltlzJxcNLd9mazUvOfSaf5ho2FFtv8TMubekKU8b92MoPQpjeS1DJ9y2pvMrtIiZP0Lm0WTeniN6luRfUN-4v5GU6FPajkOPLNV9OXJKLREhrA_SvbDldSF9RtWZOqIk1WeTlbnEWlejtwWFoLSScCSfExu6bu_vv9NKK-E8mTWF8_f4bCvlZQp58BEsTHrZuBiQzH34Z5wPeOZuuQlqbAquIS4_W6z_XmNW-d4FhIp2U3y9sYC7wpo1M7N7MB3HKwAliPVgsNHBRI4ZLZ-dL3FCyCMThKJqQMNrMcRif_Mm5Du--Atjn1UH2u2gAxiBA6IY7uSlSn-OJEO-m8qeif8zsdvVhXtJMxNAWZHhQ4QuRFgjuDgxy1nVuhGHdmXi6tseiiC2NQ9iqGuBRetexfz84R93RVSbKMkYlvBU1KPe8ARVf_N52C1KC9F2b3Uo5To9iD2lXShcsGkQkcAX5gjmhy4jrmTv5-pUJYAHa6A9Vorr59D7-Y-CVvVX59YJB9-kMT8wzHQdj2XimbcLKnS5Z4BKsMMEIt01LVkdHcBP9tKBQ20e-Kmf25wsUr9TqFa7ukQEhfLwgflIBbobAJoGKFC2_3fIKaEBuoAOoErASxPClLNAbBqG1JAdrAq9Ki3WC46aN4b-Q6ykfbk2azLAxOzFftJuhLWGLLCkOxbxjfaUrRJ51h8Dwrpy2xBT1qWurNnfFTrzScouK-R8G4SfsyaSiejiaLYLsWZVCcpeH_S8cqQuBFCMpQfxiPn2reOgMFhSzbdSDkzwUTQsjGq94QTs7bdS0LlyRb4OUa0s3szGSIa7n4vQ10uc9gzHlDxEqgaKSpPlVDyvZs58GC3PCZ2HJiiVbuc508_rV1xuydd7asPlyaOAMXImKPxp7d6rAGLbDOQOKS4U9sr6wKQVPnfPqg7TvmtuXTJS5Y_M6mutU7Bn8y6qmbjt5EtoETTSORHfx71ySMLZ8zxveJdsaNow6lfjvI0myk8oSDIucRar1j9G2m13B3K2Kr0URBweH6JkJz3Z7mYFTe09B6GMzIOcPoaYzzJP_PSrpuAfvb6V0AwVCX_HixF2qkCvcdrLyvGaGkkkYh32T2Renu7QWj8Wz06MvimWYCA4pB8SPJpjyw8mNZHOWJXgkI8hgD90O_rDF8mhEIMbDtfTZdOPjekS1a7-LNUGM6ajWLzDehU5YQBzTuGwgoPd2RV0E68iYR6QplHTmhh5vToa7eHvbQrYn8NUzJ6CP5YXcoxl7H9HsQ3AXDHmCtZ3e5p4FLV-Lz64_hVJWaTLOgHecFGAFqXMmnp1BtoKlzwbMnXaFMVaT1T7CkC_XZsoggQA1WFO3vFuXpnw4D6BPNGTmEZrEmINmfBVeFHB4SHDPJXDYX4wwTK8kgUpCHSI8ozIYFy0nw4uJqhkAYjXnvbEeCPsPkf7SPGS7xujgIdlYbtizeg-op2ZyI020Jt-hx2GogXRD_bsNcHaToWZ90fTI8M_Y1-F8iMJG4OnxinHlHnTj7R6wRuM2AZ4-Ov_yhd9w6yXenoKh56RReHCbYAvGCt3aDDyOrcX9WX7VePrBHH3C9ubCwj3PNcuP16or5ho6XRNlXC1s63J99dgi42FWatXeYdUvvcmK7fKxFZWSCXko-cArTT1KqgucxXg8wMk7gaGSwfb2j3pNt1hf4y5MOJQ-HbS0uhuywUbBiHBe8ns6FzUJpM4T8sNXAfbslBk1nIYU9BCMn1Veqw8puCYcwkjWJgxrKU_d2Jx0b8DKpIbbdFKkrdR4vAGRTJ74IgWPuk7wZTSWCddJAB4Q1PU1nbXO3MsxxzlQrWXY1jD9Zp3E69NEUss4qMTT6u5W-RG6RB6ge6sOt47l40v3IO-1LgsCwJtFyQzks0msArf0MSSQ9HreubNnjYqaMOqgUleX4-a0P8BhQwOhwt7C6zyGCnbcBiQx0RWAs0mvf-k5mgqn9Ij5mpGoGVV5L4OhBdY6pm51h7v02bgoWlzUzZHImgBhQtkx0jlBM9XeCIo4t8EQ4ZbqGmLsbj3CTu7KZbc9uQJkWyXSov2WWMvzZfOgCHbizXOGazd47v44BDRMXIiUmJzI1ZXiFkqPB0NftBw80ZnCHlpeq1Nzq8_QQHiM2R1SEsc_T1QQcSlJTVmBpdnqesbv9AAAAAAAAAAAAAAAAAAAAAAAAAAAAABMhLDo', + userHandle: 'd2ViYXV0aG5pby1tbC1kc2EtNDQ', + }, + type: 'public-key', + clientExtensionResults: {}, + }, + expectedChallenge: + 'Ji15971jSESa9haCUYb7s_pMhV8DNNwYT8Wb5zbEo151Ab7s_MuT-_MIjnousfaF2Q3emFAx7GkpXkTUmMicTQ', + expectedOrigin: 'https://webauthn.io', + expectedRPID: 'webauthn.io', + credential: { + id: '', + counter: 7, + publicKey: isoBase64URL.toBuffer( + 'owEHAzgvIFkFIC4AIUrgARve17AEk0W30POluaL08p91eLXkktSjmAlmZdNTWhtUFj3wkseZEt4xpmWarG28Za86i7yq-B4df3uOuq3zQVTKOQUWJLWGJ3-wUUuyywPtkdgSqzQdcli6xMgwnVqh9r6FVL9Xp7x3kgjUVDqhux_k1D2d4ts2zqi1rUrSF6FNX139g3dd1VnUNQrMLdrwohR9CmE0fZ6Am4Df_OV2JxOrUEPzMFi5SeBcrU1oSj2lX_91gY179PO0wIOtTa1KzWvwOYa_KjOj9Ow16AtmsXrcpL-jYW4_bFn4kpT9G-vDG4qPFDpint62g0DDjEt7JrF288aIZXOpsbVmnjw2_O_5pFFvFpH32gD7_NdmvE6PSymNxPcTCnMzY3xv5wJXiEDhO21E85n78Oay4k7PzWHvzQxlJldIYw-9TfKZXqZa6sIbE-LyZj_Y2FV1Owd4WLvKCNcO-IIP3XFcZ7__XPZtAsBTJ5Z5w18jRnlMNKTygva-F2Ec65tA2skED9PnVyS_WjtZN5VjbhuU-D9DIDXEgUjitdcXWbCruDjxaBwjuDFXOI9cYdp4n-KWCZGJdX9QFHDGkvX6zDXupFrFV1q1JeKCayuMJjL3Z44AMF2UtjNODzhlviE8neX5NSfXdf36FWGFER6D6YCGGvooW8EBCx8OLPRNGGwoKBrEflr_ISYIdyw8-rDkAG0-bka_ulzfg8uTY8BXNu0HhqsteUPni4HlhUXMb0yI1DbLi5hTTkpBEBfmjzTJ8JMDe9sOOaqU2PrOzvIs5c7fx_VBqQZbF6amei2Y41okZJWwW0LWNvL2JQ_Yj9deHMczichCHWVX3uCL-SfPL3AaLeWLPjTAejU-H1Lnn2jWQeHtiRxBL1eleZNmJVqFbrgclcMXirM6rrmPrsbFe41fDF3Hm1KgcKkpZMPSICijfDCT4csVeLDxmsg9aDYwboxigOVHZa-zAmePLBZrPJIWDNEHNBG9CdEG-RfeshvnRbPerB1zLzA9jP-Jj55_Xd4igau4FEc7dLWgyn2b2Q3aMAaDnKCzEScd301WeuZtutm6flzqDPCUTJnoniUHuO__bALWkzIxe5rHW6wQ_wPBEX32bQNN-gtI6_yiw-UTwu3egro3tDp7ZzHkMSslF9FHD7divbmeEzsE8N4iOwO5kWFt2jY9VpjGXAhyCcGZtWU68SzllOpWzvuacFjlE5KZ_c4nHhYdaphJAjXvbkog-vGUwjffCXe9gQhIliwPzREtccZdgyLKiBAlypp0pwVKe6disU9-2kflk_BXPRf1PkBEqO41ySFZWLb6eSij9FrIXtPAo4RFmeKPLoYT-ce3gi8_XftVv7MDl9s0hoFlgh5vTh1xMdpxEt-6BxdesEF3zJycxNY4QFVkUKE78geXogQFz2QE7kW4ncTXjq4IydHOKX9Bp2P8uGcCJ6dzW3PFE-Zurf1klV-rkvT7xE-Tds7CPeWkrRr_Ckhn6rQ2Z3-Sjz5bgIRHiBnd0iZfm6ZgD77nVHY7ztaSmUQ7JWbeFSz0eoYExgXi7HfSdV77DlHxIjcNlrSh58SGWfkSwUVboOUJKy_B3EbBDeweqn1pf7QIjAJnYL7WiogmAku2UxEBQijtPAusmyhLf0_aTEFFc3zdGutHim3dzAKfJucy2aBm8ViQxY_U1N26WVO6sfui7dZVhqkQniZLCq8N_xqEMqWV6utksRHOvITvB_SqmeDacy2ZfiSogU8K5G0', + ), + }, + }); + + assert(verification.verified); + }, +); + +// Deno.test( +// 'should verify ML-DSA-65 authentication response', +// /** +// * ML-DSA is only supported in Deno v2.8.2+ +// */ +// { ignore: lessThan(parse(Deno.version.deno), parse('2.8.2')) }, +// async () => { +// const verification = await verifyAuthenticationResponse({}); + +// assert(verification.verified); +// }); + +// Deno.test( +// 'should verify ML-DSA-87 authentication response', +// /** +// * ML-DSA is only supported in Deno v2.8.2+ +// */ +// { ignore: lessThan(parse(Deno.version.deno), parse('2.8.2')) }, +// async () => { +// const verification = await verifyAuthenticationResponse({}); + +// assert(verification.verified); +// }); + /** * Assertion examples below */ diff --git a/packages/server/src/helpers/cose.ts b/packages/server/src/helpers/cose.ts index 212a15c8e..2aa2efe39 100644 --- a/packages/server/src/helpers/cose.ts +++ b/packages/server/src/helpers/cose.ts @@ -62,6 +62,16 @@ export type COSEPublicKeyRSA = COSEPublicKey & { set(key: COSEKEYS.e, value: Uint8Array_): void; }; +/** + * Values specific for AKP (ML-DSA) public keys + */ +export type COSEPublicKeyAKP = COSEPublicKey & { + // Getters + get(key: COSEKEYS.pub): Uint8Array_ | undefined; + // Setters + set(key: COSEKEYS.pub, value: Uint8Array_): void; +}; + /** * A type guard for determining if a COSE public key is an OKP key pair */ @@ -92,6 +102,16 @@ export function isCOSEPublicKeyRSA( return isCOSEKty(kty) && kty === COSEKTY.RSA; } +/** + * A type guard for determining if a COSE public key is an AKP key pair + */ +export function isCOSEPublicKeyAKP( + cosePublicKey: COSEPublicKey, +): cosePublicKey is COSEPublicKeyAKP { + const kty = cosePublicKey.get(COSEKEYS.kty); + return isCOSEKty(kty) && kty === COSEKTY.AKP; +} + /** * COSE Keys * @@ -106,6 +126,7 @@ export enum COSEKEYS { y = -3, n = -1, e = -2, + pub = -1, } /** @@ -117,6 +138,7 @@ export enum COSEKTY { OKP = 1, EC2 = 2, RSA = 3, + AKP = 7, } export function isCOSEKty(kty: number | undefined): kty is COSEKTY { @@ -154,6 +176,9 @@ export enum COSEALG { PS384 = -38, PS512 = -39, ES256K = -47, + ML_DSA_44 = -48, + ML_DSA_65 = -49, + ML_DSA_87 = -50, RS256 = -257, RS384 = -258, RS512 = -259, diff --git a/packages/server/src/helpers/iso/isoCrypto/importKey.ts b/packages/server/src/helpers/iso/isoCrypto/importJWKKey.ts similarity index 64% rename from packages/server/src/helpers/iso/isoCrypto/importKey.ts rename to packages/server/src/helpers/iso/isoCrypto/importJWKKey.ts index bfe8f66eb..d047d371c 100644 --- a/packages/server/src/helpers/iso/isoCrypto/importKey.ts +++ b/packages/server/src/helpers/iso/isoCrypto/importJWKKey.ts @@ -1,6 +1,10 @@ import { getWebCrypto } from './getWebCrypto.ts'; -export async function importKey(opts: { +/** + * Convert a JWK-formatted public key into a WebCrypto CryptoKey that can be used for + * signature verification + */ +export async function importJWKKey(opts: { keyData: JsonWebKey; algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams; }): Promise { @@ -8,7 +12,5 @@ export async function importKey(opts: { const { keyData, algorithm } = opts; - return WebCrypto.subtle.importKey('jwk', keyData, algorithm, false, [ - 'verify', - ]); + return WebCrypto.subtle.importKey('jwk', keyData, algorithm, false, ['verify']); } diff --git a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts index be5527478..3e49eb47a 100644 --- a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts +++ b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts @@ -1,26 +1,32 @@ import { COSEALG } from '../../cose.ts'; -import { SubtleCryptoKeyAlgName } from './structs.ts'; +import type { SubtleCryptoKeyAlgName } from './structs.ts'; /** * Convert a COSE alg ID into a corresponding key algorithm string value that WebCrypto APIs expect + * + * Unless otherwise specified, mappings were referenced from + * https://w3c.github.io/webcrypto/#jwk-mapping-alg */ export function mapCoseAlgToWebCryptoKeyAlgName( alg: COSEALG, ): SubtleCryptoKeyAlgName { if ([COSEALG.EdDSA].indexOf(alg) >= 0) { return 'Ed25519'; - } else if ( - [COSEALG.ES256, COSEALG.ES384, COSEALG.ES512, COSEALG.ES256K].indexOf( - alg, - ) >= 0 - ) { + } else if ([COSEALG.ES256, COSEALG.ES384, COSEALG.ES512, COSEALG.ES256K].indexOf(alg) >= 0) { return 'ECDSA'; - } else if ( - [COSEALG.RS256, COSEALG.RS384, COSEALG.RS512, COSEALG.RS1].indexOf(alg) >= 0 - ) { + } else if ([COSEALG.RS256, COSEALG.RS384, COSEALG.RS512, COSEALG.RS1].indexOf(alg) >= 0) { return 'RSASSA-PKCS1-v1_5'; } else if ([COSEALG.PS256, COSEALG.PS384, COSEALG.PS512].indexOf(alg) >= 0) { return 'RSA-PSS'; + } else if ([COSEALG.ML_DSA_44].indexOf(alg) >= 0) { + // https://wicg.github.io/webcrypto-modern-algos/#ml-dsa-registration + return 'ML-DSA-44'; + } else if ([COSEALG.ML_DSA_65].indexOf(alg) >= 0) { + // https://wicg.github.io/webcrypto-modern-algos/#ml-dsa-registration + return 'ML-DSA-65'; + } else if ([COSEALG.ML_DSA_87].indexOf(alg) >= 0) { + // https://wicg.github.io/webcrypto-modern-algos/#ml-dsa-registration + return 'ML-DSA-87'; } throw new Error( diff --git a/packages/server/src/helpers/iso/isoCrypto/structs.ts b/packages/server/src/helpers/iso/isoCrypto/structs.ts index 2b667d94b..3610b467f 100644 --- a/packages/server/src/helpers/iso/isoCrypto/structs.ts +++ b/packages/server/src/helpers/iso/isoCrypto/structs.ts @@ -3,5 +3,8 @@ export type SubtleCryptoCrv = 'P-256' | 'P-384' | 'P-521' | 'Ed25519'; export type SubtleCryptoKeyAlgName = | 'ECDSA' | 'Ed25519' + | 'ML-DSA-44' + | 'ML-DSA-65' + | 'ML-DSA-87' | 'RSASSA-PKCS1-v1_5' | 'RSA-PSS'; diff --git a/packages/server/src/helpers/iso/isoCrypto/verify.ts b/packages/server/src/helpers/iso/isoCrypto/verify.ts index 42c268b79..cba21fc87 100644 --- a/packages/server/src/helpers/iso/isoCrypto/verify.ts +++ b/packages/server/src/helpers/iso/isoCrypto/verify.ts @@ -3,10 +3,12 @@ import { COSEKEYS, type COSEPublicKey, isCOSECrv, + isCOSEPublicKeyAKP, isCOSEPublicKeyEC2, isCOSEPublicKeyOKP, isCOSEPublicKeyRSA, } from '../../cose.ts'; +import { verifyAKP } from './verifyAKP.ts'; import { verifyEC2 } from './verifyEC2.ts'; import { verifyRSA } from './verifyRSA.ts'; import { verifyOKP } from './verifyOKP.ts'; @@ -40,6 +42,8 @@ export function verify(opts: { return verifyRSA({ cosePublicKey, signature, data, shaHashOverride }); } else if (isCOSEPublicKeyOKP(cosePublicKey)) { return verifyOKP({ cosePublicKey, signature, data }); + } else if (isCOSEPublicKeyAKP(cosePublicKey)) { + return verifyAKP({ cosePublicKey, signature, data }); } const kty = cosePublicKey.get(COSEKEYS.kty); diff --git a/packages/server/src/helpers/iso/isoCrypto/verifyAKP.ts b/packages/server/src/helpers/iso/isoCrypto/verifyAKP.ts new file mode 100644 index 000000000..e64dd0b9d --- /dev/null +++ b/packages/server/src/helpers/iso/isoCrypto/verifyAKP.ts @@ -0,0 +1,68 @@ +import { COSEKEYS, type COSEPublicKeyAKP, isCOSEAlg } from '../../cose.ts'; +import { isoBase64URL } from '../../index.ts'; +import { importJWKKey } from './importJWKKey.ts'; +import { getWebCrypto } from './getWebCrypto.ts'; +import { mapCoseAlgToWebCryptoKeyAlgName } from './mapCoseAlgToWebCryptoKeyAlgName.ts'; +import type { Base64URLString, Uint8Array_ } from '../../../types/index.ts'; + +/** AKP, a.k.a ML-DSA */ +export async function verifyAKP(opts: { + cosePublicKey: COSEPublicKeyAKP; + signature: Uint8Array_; + data: Uint8Array_; +}): Promise { + const { cosePublicKey, signature, data } = opts; + + const WebCrypto = await getWebCrypto(); + + const alg = cosePublicKey.get(COSEKEYS.alg); + const pub = cosePublicKey.get(COSEKEYS.pub); + + if (!alg) { + throw new Error('Public key was missing alg (AKP)'); + } + + if (!isCOSEAlg(alg)) { + throw new Error(`Public key had invalid alg ${alg} (AKP)`); + } + + if (!pub) { + throw new Error('Public key was missing pub (AKP)'); + } + + const webCryptoAlg = mapCoseAlgToWebCryptoKeyAlgName(alg); + + // See https://wicg.github.io/webcrypto-modern-algos/#ml-dsa-operations-import-key -> "jwk" + const keyData: JsonWebKeyAKP = { + kty: 'AKP', + alg: webCryptoAlg, + pub: isoBase64URL.fromBuffer(pub), + ext: false, + }; + + /** + * ML-DSA support is pretty cutting edge, so take some steps to help RP's understand this instead + * of them getting a `NotSupportedError: Unrecognized algorithm name` and trying to interpret it. + */ + let key: Awaited>; + try { + key = await importJWKKey({ keyData, algorithm: webCryptoAlg }); + } catch (err) { + const _err = err as Error; + if (_err.name === 'NotSupportedError') { + throw new Error( + `This runtime's WebCrypto.subtle does not support use of ${webCryptoAlg}. See the \`cause\` property of this error for more info`, + { cause: err }, + ); + } else { + throw err; + } + } + + return WebCrypto.subtle.verify(webCryptoAlg, key, signature, data); +} + +type JsonWebKeyAKP = JsonWebKey & { + // https://www.rfc-editor.org/rfc/rfc9964.html#section-8.1.6.1 + pub?: Base64URLString; +}; diff --git a/packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts b/packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts index 3a8c9a979..f1e5c3091 100644 --- a/packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts +++ b/packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts @@ -1,6 +1,6 @@ import { type COSEALG, COSECRV, COSEKEYS, type COSEPublicKeyEC2 } from '../../cose.ts'; import { mapCoseAlgToWebCryptoAlg } from './mapCoseAlgToWebCryptoAlg.ts'; -import { importKey } from './importKey.ts'; +import { importJWKKey } from './importJWKKey.ts'; import { isoBase64URL } from '../index.ts'; import type { SubtleCryptoCrv } from './structs.ts'; import { getWebCrypto } from './getWebCrypto.ts'; @@ -71,10 +71,7 @@ export async function verifyEC2(opts: { namedCurve: _crv, }; - const key = await importKey({ - keyData, - algorithm: keyAlgorithm, - }); + const key = await importJWKKey({ keyData, algorithm: keyAlgorithm }); // Determine which SHA algorithm to use for signature verification let subtleAlg = mapCoseAlgToWebCryptoAlg(alg); diff --git a/packages/server/src/helpers/iso/isoCrypto/verifyOKP.ts b/packages/server/src/helpers/iso/isoCrypto/verifyOKP.ts index 7b71737cd..13324c728 100644 --- a/packages/server/src/helpers/iso/isoCrypto/verifyOKP.ts +++ b/packages/server/src/helpers/iso/isoCrypto/verifyOKP.ts @@ -1,7 +1,7 @@ import { COSECRV, COSEKEYS, type COSEPublicKeyOKP, isCOSEAlg } from '../../cose.ts'; import { isoBase64URL } from '../../index.ts'; import type { SubtleCryptoCrv } from './structs.ts'; -import { importKey } from './importKey.ts'; +import { importJWKKey } from './importJWKKey.ts'; import { getWebCrypto } from './getWebCrypto.ts'; import type { Uint8Array_ } from '../../../types/index.ts'; @@ -56,10 +56,7 @@ export async function verifyOKP(opts: { namedCurve: _crv, }; - const key = await importKey({ - keyData, - algorithm: keyAlgorithm, - }); + const key = await importJWKKey({ keyData, algorithm: keyAlgorithm }); const verifyAlgorithm: AlgorithmIdentifier = { name: _crv, diff --git a/packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts b/packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts index 39a6d2a47..31988f4bc 100644 --- a/packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts +++ b/packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts @@ -1,6 +1,6 @@ import { type COSEALG, COSEKEYS, type COSEPublicKeyRSA, isCOSEAlg } from '../../cose.ts'; import { mapCoseAlgToWebCryptoAlg } from './mapCoseAlgToWebCryptoAlg.ts'; -import { importKey } from './importKey.ts'; +import { importJWKKey } from './importJWKKey.ts'; import { isoBase64URL } from '../index.ts'; import { mapCoseAlgToWebCryptoKeyAlgName } from './mapCoseAlgToWebCryptoKeyAlgName.ts'; import { getWebCrypto } from './getWebCrypto.ts'; @@ -99,10 +99,7 @@ export async function verifyRSA(opts: { ); } - const key = await importKey({ - keyData, - algorithm: keyAlgorithm, - }); + const key = await importJWKKey({ keyData, algorithm: keyAlgorithm }); return WebCrypto.subtle.verify(verifyAlgorithm, key, signature, data); } diff --git a/packages/server/src/registration/generateRegistrationOptions.ts b/packages/server/src/registration/generateRegistrationOptions.ts index 12fc23c2f..64a5c54c8 100644 --- a/packages/server/src/registration/generateRegistrationOptions.ts +++ b/packages/server/src/registration/generateRegistrationOptions.ts @@ -15,7 +15,7 @@ import { isoBase64URL, isoUint8Array } from '../helpers/iso/index.ts'; export type GenerateRegistrationOptionsOpts = Parameters[0]; /** - * Supported crypto algo identifiers + * Supported crypto algo identifiers. * See https://w3c.github.io/webauthn/#sctn-alg-identifier * and https://www.iana.org/assignments/cose/cose.xhtml#algorithms */ @@ -32,6 +32,12 @@ export const supportedCOSEAlgorithmIdentifiers: COSEAlgorithmIdentifier[] = [ -38, // RSASSA-PSS w/ SHA-512 -39, + // ML-DSA-44 + -48, + // ML-DSA-65 + -49, + // ML-DSA-87 + -50, // RSASSA-PKCS1-v1_5 w/ SHA-256 -257, // RSASSA-PKCS1-v1_5 w/ SHA-384 diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts index 80803b30c..b78510890 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.test.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts @@ -1107,6 +1107,79 @@ Deno.test('should skip CTS check when enforcement option is false', async () => mockDate.restore(); }); + +Deno.test('should verify ML-DSA-44 registration response', async () => { + /** TODO: Replace this test with one that has an actual attestation statement (when I can publicly do so) */ + const verification = await verifyRegistrationResponse({ + response: { + id: '-EM9FDFIdFVeqWdTycRjoZVN2ZS4vnVE-MBpg7k0pl4jpuqj4GnMCW3Wqlm2WWI2PQ', + rawId: '-EM9FDFIdFVeqWdTycRjoZVN2ZS4vnVE-MBpg7k0pl4jpuqj4GnMCW3Wqlm2WWI2PQ', + response: { + attestationObject: + 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkFoHSm6pITyZwvdLIkkrMgz0AmKpTBqVCgOX8pJQtghB7wxQAAAAQAAAAAAAAAAAAAAAAAAAAAADH4Qz0UMUh0VV6pZ1PJxGOhlU3ZlLi-dUT4wGmDuTSmXiOm6qPgacwJbdaqWbZZYjY9owEHAzgvIFkFIC4AIUrgARve17AEk0W30POluaL08p91eLXkktSjmAlmZdNTWhtUFj3wkseZEt4xpmWarG28Za86i7yq-B4df3uOuq3zQVTKOQUWJLWGJ3-wUUuyywPtkdgSqzQdcli6xMgwnVqh9r6FVL9Xp7x3kgjUVDqhux_k1D2d4ts2zqi1rUrSF6FNX139g3dd1VnUNQrMLdrwohR9CmE0fZ6Am4Df_OV2JxOrUEPzMFi5SeBcrU1oSj2lX_91gY179PO0wIOtTa1KzWvwOYa_KjOj9Ow16AtmsXrcpL-jYW4_bFn4kpT9G-vDG4qPFDpint62g0DDjEt7JrF288aIZXOpsbVmnjw2_O_5pFFvFpH32gD7_NdmvE6PSymNxPcTCnMzY3xv5wJXiEDhO21E85n78Oay4k7PzWHvzQxlJldIYw-9TfKZXqZa6sIbE-LyZj_Y2FV1Owd4WLvKCNcO-IIP3XFcZ7__XPZtAsBTJ5Z5w18jRnlMNKTygva-F2Ec65tA2skED9PnVyS_WjtZN5VjbhuU-D9DIDXEgUjitdcXWbCruDjxaBwjuDFXOI9cYdp4n-KWCZGJdX9QFHDGkvX6zDXupFrFV1q1JeKCayuMJjL3Z44AMF2UtjNODzhlviE8neX5NSfXdf36FWGFER6D6YCGGvooW8EBCx8OLPRNGGwoKBrEflr_ISYIdyw8-rDkAG0-bka_ulzfg8uTY8BXNu0HhqsteUPni4HlhUXMb0yI1DbLi5hTTkpBEBfmjzTJ8JMDe9sOOaqU2PrOzvIs5c7fx_VBqQZbF6amei2Y41okZJWwW0LWNvL2JQ_Yj9deHMczichCHWVX3uCL-SfPL3AaLeWLPjTAejU-H1Lnn2jWQeHtiRxBL1eleZNmJVqFbrgclcMXirM6rrmPrsbFe41fDF3Hm1KgcKkpZMPSICijfDCT4csVeLDxmsg9aDYwboxigOVHZa-zAmePLBZrPJIWDNEHNBG9CdEG-RfeshvnRbPerB1zLzA9jP-Jj55_Xd4igau4FEc7dLWgyn2b2Q3aMAaDnKCzEScd301WeuZtutm6flzqDPCUTJnoniUHuO__bALWkzIxe5rHW6wQ_wPBEX32bQNN-gtI6_yiw-UTwu3egro3tDp7ZzHkMSslF9FHD7divbmeEzsE8N4iOwO5kWFt2jY9VpjGXAhyCcGZtWU68SzllOpWzvuacFjlE5KZ_c4nHhYdaphJAjXvbkog-vGUwjffCXe9gQhIliwPzREtccZdgyLKiBAlypp0pwVKe6disU9-2kflk_BXPRf1PkBEqO41ySFZWLb6eSij9FrIXtPAo4RFmeKPLoYT-ce3gi8_XftVv7MDl9s0hoFlgh5vTh1xMdpxEt-6BxdesEF3zJycxNY4QFVkUKE78geXogQFz2QE7kW4ncTXjq4IydHOKX9Bp2P8uGcCJ6dzW3PFE-Zurf1klV-rkvT7xE-Tds7CPeWkrRr_Ckhn6rQ2Z3-Sjz5bgIRHiBnd0iZfm6ZgD77nVHY7ztaSmUQ7JWbeFSz0eoYExgXi7HfSdV77DlHxIjcNlrSh58SGWfkSwUVboOUJKy_B3EbBDeweqn1pf7QIjAJnYL7WiogmAku2UxEBQijtPAusmyhLf0_aTEFFc3zdGutHim3dzAKfJucy2aBm8ViQxY_U1N26WVO6sfui7dZVhqkQniZLCq8N_xqEMqWV6utksRHOvITvB_SqmeDacy2ZfiSogU8K5G2ha2NyZWRQcm90ZWN0Ag', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiNGw4ZGlzVjZWaXRHQ2dfRUp2Q054N1Y5MlFMdEJuX1JZcTl0VEJFWjdqNEI1aFpJOWtpakoySW5MeFFOUllWbGdMUk9GM25majgwWWk3TVBoWFF3anciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ', + }, + clientExtensionResults: {}, + type: 'public-key', + }, + expectedChallenge: + '4l8disV6VitGCg_EJvCNx7V92QLtBn_RYq9tTBEZ7j4B5hZI9kijJ2InLxQNRYVlgLROF3nfj80Yi7MPhXQwjw', + expectedOrigin: 'https://webauthn.io', + expectedRPID: 'webauthn.io', + }); + + assert(verification.verified); +}); + +Deno.test('should verify ML-DSA-65 registration response', async () => { + /** TODO: Replace this test with one that has an actual attestation statement (when I can publicly do so) */ + const verification = await verifyRegistrationResponse({ + response: { + id: 'S903soghFo9Bmu9i4Styf5hLEPFkxu_Ma8Nm65BiZdBt1pGqF4dB2cth6wknrCMk6A', + rawId: 'S903soghFo9Bmu9i4Styf5hLEPFkxu_Ma8Nm65BiZdBt1pGqF4dB2cth6wknrCMk6A', + response: { + attestationObject: + 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkIIHSm6pITyZwvdLIkkrMgz0AmKpTBqVCgOX8pJQtghB7wxQAAAAIAAAAAAAAAAAAAAAAAAAAAADFL3TeyiCEWj0Ga72LhK3J_mEsQ8WTG78xrw2brkGJl0G3WkaoXh0HZy2HrCSesIyToowEHAzgwIFkHoNsFUCfZJHghYZMYm8P_ANG6RtvE1VKrK5_ER7yE512V3UmGjIlYVOSIMebtBorke1TBvjuBi8PBKAQs4w16SJ7ijjT8r9ClCt_aHcFDTjz49s3jLIwqL08wPD1qJy6vrdkTES-mrU6rJG16RAhInZh4ie98DGZhBBaOUBIrnIy9QvceP9Qi8-4GBLcBiS-cL_da94kK6XZJXJLy1Qm-LfDPdsoph0YHGSfueYBJy41Tpu6oU4QStJ90r_E86dxQj_nd0uDPuwbje_lIrF0sbedrkNXVOwh00m-PK05fmwb-wpUUGsRq2o7gvm3oCMSxm-1dwYHKdBWjf8y_E34kiQ_ZeZTsA1yXSKci7kCEGGXqLDIE0chka6hY6xZZlEA89Cbqv_HPAN29t0_70144qDuXb4nLfQ7XY-uoS5OBP_W0rkc3lc3r0spe0c9ZO8lHEGFbX8PyP98gkM2z61kDstai23k6OjLTKMvuulKNYbTxCLTj37BpiNxnqqq5bs0dkG9eO5yHpqBwH4sm4qUXRsG-ZS9_TvfiEtKahjU5uAhZWOYs71CcCIfSKSD_swPg-7VKUwzSpYq2BwlExdBpDHL-ghogYAOgf5FNs9fUNHLRmFpOUDnmUAeX7iNrtA7Tu3BygNv8JWUCTuWllzT2GQpD70xJMAjKxMkvXrqqCfJrFTQt0FscjqjZqcP8xyiaCo0nPILaGChuakL0XBgh-Fi5YkulKsaDd5orLnka4063lpn0jdxvy1yk0p4tfFRmEBnankCmk44ZvbKJQRMsDcydqgjK0vpQxVWbf1r-73GHNvliWw1ajYmqUe2uwDGdLBBCUbDeqcmrRJYnX6g8W-vlh3VLWGBlHodtGWUEkJxY7RmArf2UeEBDGWj3QoGJTfQYUzXg91Y2fSmgi2b3ZPzUg8h92JKMCpbvJr6PLANCFGf3RviLYnNuU0Zn8czC80tsUtxkkzLN0iFkRvT_WPha6lbuwIlMKNXkXRbkph79wSBasp81aqzgPd4Yq9LDrZIrNI6q7ZbYGoF03C0Digqdj_f6aUxxIteIuCWm-z_xHY9Czo4VHo0V8YnTFyZj3HDX1S8NZ_odvP_GZcmvgLmUhsWRmMxs2WEJFDrcgFR-tK9KLYf29gGI0lPsSJtnho_GLUKX5-tdojlBVEcVO6rPieS8-bCu8B4BeyduYAnDgHdxBAUBfm2um7ZIDTl_n6g-bZb-TxMvsGbIO3NK99QylHqSRFK68PX-yvDZjsZlblKZSe8WsSG_RVNrWGYiKjVpsqZy61eXoSE5ySCOQvAdlpTEdb7F5C1_USkpdyuyR5BGXtFizf2V60wDPbjOrr3Yz1CuUB_DRDvbvc4GKIAzCZDR6MC3oIJjQ2Z81pzv8ZrXq5LhhSdda2TTBtckM-BRyLJHh7ZkHY03DFLqfGSn-KUzGlk2fUWFdJ4zlT4AquxJTKJYBQlWt6tOgGV8MB5j5DUuwgbw9a7dx_jRW8AZps4O96y85c-rQS7e9g5noatqxq1vZp6X-vTTMZXF-nx1sgSHEgoN5bm30fsrMP0MvaahDZaEIV3o8kj7o-uRM8VGyXVHm3qhtQdYIwzrjWBCA96t7x75zgmgxIWJbzIgfay3WHeguFIwW844OaO_zq3C43tDCXU0ahWiTjEPeYqAVDGlifqOPEFpiO5t9lvwjU5upQybkhnwLzFvaUVCgzGu_2bm7geO5NphNee8lz7Rbi4r8VBCzDYBLcjeqMiUCduCQH8y3mo3Z3F4V0qqAUHM6w_hMr8V9K_mGNeSYDEv52bBHxfywJkcqSTq3TVkPXicWfb1VHJroZ47LdAQ-jlGJaTnJEfHeY5VHHjy9gQ6_px7iZ9JggN0SIJDbSdHXHxkdFbSgMnEHo8N3YFoB-oEO5AKX1CJ3kELg8gO-nybVMC4ktDqk5dAAglVcp5GQC8gY_EP92LAXt4MFHRkM9g4zgZHVlqKLqbPPYngB9nOAM-8rZGo3Nez0zB7m5EFDkxV8PoD0k2omrJEqWrSDd9swG6kFiH4-5MV-8csaka-PO0kwS-KZmbh7R-OhJlmKujJw6nxYDfUaPeXQs6QsFqU83BZxl5gZGr8LPmqP83i1tEk1RQd_pwFa-ks7GmMvWQuyK2PnaW9cXfH-0-Z-jetukzac8KqTOkdedYmeTPW9oHlGmPvAEBvoGNQeUzw5Q_uwBv60LdbEpy61iiTQNEy7eKDxylHJld41P9vTTh1om6TEWGmHJ1BQ5YLoZ1E_X4dnMZGZUtcK4Ym_81jHMdRw4NkdQhPtPFe5kfPYMdIn_QeAIVIS_KPyo07t_9BhrDfRBirl_BfAGpEdVuwZQYO-x-jZ-0WGXptAp1TpmAVjD2Y-Gwx4xRL-Y-M7NTaTAeZpEB6_UdQU8UUL_DfQLxlF6dE4Px_0xmPAXBNtZJIWcdOIOIpnjP2vvT39x4SgoS1Ij7udQZUoidhA6M6_bwOT3uimKVm31vLH-tN_N3eMgQj8Db2Wj6htO0Ysk58ueKGN17UruWYPnktWiR-M3hZs5mJKlxHeVwTEcZgkIqYM14Hg0IB8M41ICy1vhrEzO9aQ1Cwn36srly2-h_coWtjcmVkUHJvdGVjdAI', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiclFNM19IdHdCNjNnbDhFb0xxck00aVBDbVEzc2lXOVU3UnV0bnc5cUdNWlQ4bFdBRVRJYVpjRkV3NmoyUXc0MGZER1pXNFFyYmlzQmpvUmJlWGlkTHciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0', + }, + type: 'public-key', + clientExtensionResults: {}, + }, + expectedChallenge: + 'rQM3_HtwB63gl8EoLqrM4iPCmQ3siW9U7Rutnw9qGMZT8lWAETIaZcFEw6j2Qw40fDGZW4QrbisBjoRbeXidLw', + expectedOrigin: 'https://webauthn.io', + expectedRPID: 'webauthn.io', + }); + + assert(verification.verified); +}); + +Deno.test('should verify ML-DSA-87 registration response', async () => { + /** TODO: Replace this test with one that has an actual attestation statement (when I can publicly do so) */ + const verification = await verifyRegistrationResponse({ + response: { + id: 'OsaaaMgQ7ihU9iAzryPBOLK3PYsghC98pX4ZaDzXzY1NsiXgH-afxzClNy3oRPK1YA', + rawId: 'OsaaaMgQ7ihU9iAzryPBOLK3PYsghC98pX4ZaDzXzY1NsiXgH-afxzClNy3oRPK1YA', + response: { + attestationObject: + 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkKoHSm6pITyZwvdLIkkrMgz0AmKpTBqVCgOX8pJQtghB7wxQAAAAEAAAAAAAAAAAAAAAAAAAAAADE6xppoyBDuKFT2IDOvI8E4src9iyCEL3ylfhloPNfNjU2yJeAf5p_HMKU3LehE8rVgowEHAzgxIFkKIPB266vR3jhFEvswhiiULf6og2993LKO8euC16EFiq3z9bud2jKbB6Zw5xm7vOQUIZyCsN18qxjPRoI142swvpDA4rApAGZb028Qgyrz4eQTfueiBsy-GRJ5IN2rsVlFcz3enbYWgh_UWQDSMA9wNfTPJZPz6T-z8KTz60szO8Mn4UG1kLQ5YSV47b6LqJVnrsWzTFe0nylAkpHMtBHS3UNjhoEYNc6HyL9ehBoLGs3Z0IPusTYdcZ8LA4icQsoX3X8XEORW4MV5x1l9qwC_6-iZ4rAsbSUsUMDyB_h-FKn7TP9X8x_IA6fCDuUIL2vMWwVj-yNtVbw_NqJkm5OePjPQzh04F6wxLd4y0qeLZA7ycjADjACLKa_IJVFMgxx_7nJ2pbHKOYfqJzAnCg9nqMUVeonWBunJatFABgwKR4-cNzV6pcn1bL4haHbHdblbZ7f0nv7-DPz75BaZhDaurOBLBHGDh5bC856v0EkPOfRO81pKBa-EyT3OkZ7qejOMeEZpbvhCvECisLwbKo8lcAR0gaqHLNGEXcqNUFXVbLzdfcrDd_FMD-kBByAlGdMswYANrFpLVpmkJEGQ_Eqow99ZcyBvdwxtxCQT3X4i3rFRhB8FGP9JAkODC67U-EWIMyWF2VIseVZkxMngwsZAm327jFIoOfupWeUGjpGORhOvI6OPA2VzR6wrFjkfsM_6bt8VMHIpo5lUtGMKND5K2M3fRCFvQqPWW7MOQ83b-6qfa-GcGKzVfojGd6uwVJjgDDJldMMve8OCTktiwIJ4h-n5ObGRvlJQ7etgycCwXINUkb1KFZovgUQnujhjK4f0M0Ib76scKCmYX_UtexSCf09jkwQRgtBh3BnzJrgILfErMmfK2Z1z5wWUV4MX2oXcNkWyGGH1kCLnzXu1qveFsry7Hf27DyceNIAzuqkrtcsJgw7ePhXHz0AgdYo8qyQmXSPfhAEWiwtCZ5MbUKmxn-8pGiRKqUtbzpYTNwTpj2PdpzK1zvAAHubIxi5XXi_JEJL8h2HOmkRJLLo26GDfNV0n0SRGhujW65LNdAU7ggrgzEcdbkRPNX0vsyax0ZBr9HXs-GnlR_4ySugcgGVIdj5xv7xsvDnfy-bVPMLgOP1nCLvZROJwMAA3XOAD9SKjsQMLKQjsiZHIelWzOxHrr360wowj3jclxQUD9XgDtNfff6MjiS3rsBO05ECyF3bMcsjsRak7GStJ2MFBAbdESryKek1z-2g4ahqUuO0LGeX7Klzi110XpqN9VYrrvUNP6seqm_V48PinvDDPsfsHqC2ZFPRtWv3rcZt6RbA9KLsqXqWphMqr3MZm8k69E2ZiWn-wFHXMBWTKzrRxtiBOHk4mcygTOIn2AsjVMO8GCglyJlUluN0eQWiapr48lnE3EetPNGxq86wn28wPQhC56jcLkNHq-RDEjycvFzUzgoa4i1mJCW038vHGPH8-CLYGggPeSOnL-zZMRtw-jDaMR5HHc95FlsTLlMVbpnb-eCGT2-khduVx4HdpdoTJHfT4PNQTTVnVY6N5vUP9x5jXq6p7Zwvl_VpWR9jxZZcO1KIoi5ozIgE5XqNF-NKumoouAfpZWxauhdkBgUL86WnH3zQah8hHY1d7QeZGcfo4fx8S0NiTsVa9wIat3BBlck3HmwbduMP7Web7Rhf_Pj3lfhVrZ0p5nKFizbacQcK4mVwCM8MPaFEN6xAIwqLzFYWIwc_sUKxRq2WLg3RvIbBl0-TesJfzSPWMJGcq4iropcn-JW8XFQVJyf4KBFuwImpXq1wrOalOjIgAk5nGiYxLxK3feeOoG22uBNkfg_qJfqzGX_o3BMaXZyvydAQCY36z2SbFEme3LxsdC-vYUi6iVuxVdpL2gXm3OtNiI4iGvBNoJvGzHZwSh0-GpZbqdN_wZMFimvz8R75O2PPXSkmyLyDmNxDaSGsnZeQ4BCyD63YLnhFlWzG7VBIdAr570KEz9dhU6ozJ6iP_mtETAImGWB733xuIeZ2D3Aj-Q9l2s2xfPKqPXkvOe2OjC288A86T5l5_9mANfFgfLCjdsMwSQau_EOkrvmPkaHacBoWPfPMWwyBJSnF6jNPHoaRBtFba6Ld-f4ThJyaza52JLza1xIviFn37XzFCvLRXrpI6wghURUP2vk-dMs1l8hQRH3RIvcBIqctikAL3NuY8otXhQUw-1QjkIv2llYeXARAhE-2bIvATrf7FrVN4hJBbU3uesAyYvpG9NN8XGztoptOotFP_pYH67yUaiYJXmnPTzWEzk0F5kUhZTvy7ZlwKMv2EINrXDc0q1Dx0MJnJSe02PQr2C0RzR0zkTi00-y104KimThbs7QLRgnke7BlaJtgJH7T-KETvLqs6Ogpptzntt6whM8lesXUvQr5jm3l0WLCSo1W8-q-Vxfo8v6cbkHxsP_xoM43uOsklCuypxyVfnBRVGsgL9W5_7TaZTWMdfoak9fwMjd8j1lu6oXmOiu96TQ8vpETLuN4A1AOnFS1grl1dBZ-kUoKV_vu4ZFDkacK20S8Q5X-WcM5zKmpVUWL3oyWg3VNxSDu_16LLPQ7pYfgS9dNftCHnOjYUgY2Co_xD0ejB83G-a3aC-OSBwo7NtOHTfh6gysbSzBwsJ0Tw9GyCRRLqpu4YsWGkJXiblfGI-2zH8aGm0GZLz84p8FqRCipTbMt8yjFjKDoCp3iBpUTfMs3uON28otSpkgF5XPIwihK7O0Fu8tw-DMLXbbxMDxyAhto3xGb7Q1KT_-hc4SrghI4dy_Evm095K6Fr3sHsQ_oVck2xd71r4e_uPCXFpiHbvImwtuE94XyGgy9y1l2TvRa6P1ZpKh-ccxv_v1TZdf3gIn04M7kio46WfIiURcS6aoVfCef1pUpGRcVgxnwWrJw_8tu7MZUukXmM3SA7dezKIe446WE6BiXay7TNBu9yThxldO7ooK6udljaap3pEyejRWNJ83zSVv5OwjlvDtC4joCHJ6BejJh60OM0KeiMniTJftrV4F3ZRMgn7MdbJtUYOY4SoodNAfPEBr5n_6n7ZCT4uBAsLtyYwHmzjIR1qZxOVfH98C2xOrp6ZWFMiAlrJkPDoiD8sMMB6oWA2aT_vPcixKBQqNqrkeKScusIiThNCTrlK5AkDe_AqUEW1qu8E6Qbb-7YLCrhqIbMmg5eGKXZ7c9j94jd-rShqUz2seWaLAXkKi-VMsUdTnlgvQ-A_6IRd8wM30BpVQeDuS-AE3cfnw8poVsuaPsMU5me9Ro1um1GUEU8mLzSw6ZMgHlbFapubropKvU-2-s0nozxQzs7bh4NkPF5gZ1zuf7xNXcPV3uodzJnmtBNn7lcfJK69serkxLEKOa7IiS2osGQlxDh9mwwMip7fuVPhN9ZWadhWnrFlWfdKzv082WxErNeM_SslKaeg8UJPzRYllQ1szvS3pYyt8u3JSX2z6FrY3JlZFByb3RlY3QC', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSnNHc0J0SE1ZWEFSVUlvUjFySHZhbTYzWERkUkR6YUxLYm5kcnNjcGdVMHFLRnBKQTB4ZXV2X191ZmtfNW1naklhYkVabmVPOVYzMllXeU1zaFdtRUEiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0', + }, + type: 'public-key', + clientExtensionResults: {}, + }, + expectedChallenge: + 'JsGsBtHMYXARUIoR1rHvam63XDdRDzaLKbndrscpgU0qKFpJA0xeuv__ufk_5mgjIabEZneO9V32YWyMshWmEA', + expectedOrigin: 'https://webauthn.io', + expectedRPID: 'webauthn.io', + }); + + assert(verification.verified); +}); + /** * Various Attestations Below */