diff --git a/tests/fixtures/attestation/android-key.json b/tests/fixtures/attestation/android-key.json new file mode 100644 index 0000000..91b2ffd --- /dev/null +++ b/tests/fixtures/attestation/android-key.json @@ -0,0 +1,19 @@ +{ + "format": "android-key", + "aaguid": "550e4b54-aa47-409f-9a95-1ab76c130131", + "source": "FIDO Alliance Conformance Test Tools, server tests", + "capturedAt": "2026-09-03T03:46:46.273Z", + "expectedChallenge": "pKuACh7MupbQPXSBmHahZWd9LYhgxv2cJxmte37jq9M", + "expectedOrigin": "http://localhost:5313", + "expectedRPID": "localhost", + "response": { + "id": "PDoDtPnEp-VsQNfkN5XkhSXMt_tLF_CSv1QAYKNX3nU", + "rawId": "PDoDtPnEp-VsQNfkN5XkhSXMt_tLF_CSv1QAYKNX3nU", + "response": { + "attestationObject": "o2NmbXRrYW5kcm9pZC1rZXlnYXR0U3RtdKNjYWxnJmNzaWdYRzBFAiEA99JlisDUICeAhoc_tG1GcoPNwtAchBIDcC5KfTriqKQCIACwbG8wDd0aKpDFU3YyOKxvXfT3CTPkxHaLiKnHjod5Y3g1Y4JZAxswggMXMIICvaADAgECAgEBMAoGCCqGSM49BAMCMIHkMUUwQwYDVQQDDDxGQUtFIEFuZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gSW50ZXJtZWRpYXRlIEZBS0UxMTAvBgkqhkiG9w0BCQEWImNvbmZvcm1hbmNlLXRvb2xzQGZpZG9hbGxpYW5jZS5vcmcxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMCAXDTcwMDIwMTAwMDAwMFoYDzIwOTkwMTMxMjM1OTU5WjApMScwJQYDVQQDDB5GQUtFIEFuZHJvaWQgS2V5c3RvcmUgS2V5IEZBS0UwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARJnVTz1FIwyJpDfGpgZC8jYVsSfx2uRtw2KaJrWJHXEIjyfvSOUUcEqe3NX9rhZTDOsnDm-bOVGu_ARv2XejLjo4IBFjCCARIwCwYDVR0PBAQDAgeAMIHhBgorBgEEAdZ5AgERBIHSMIHPAgECCgEAAgEBCgEABCB8JLtuL53X2lvPuEiiex_vfN93kielJhQoLEJt2FuaKgQAMGm_hT0IAgYBXtPjz6C_hUVZBFcwVTEvMC0EKGNvbS5hbmRyb2lkLmtleXN0b3JlLmFuZHJvaWRrZXlzdG9yZWRlbW8CAQExIgQgdM_LUHSI9SkQhZHHpQWRnzJ3MvvB2ANSauqYAAbS2JgwMqEFMQMCAQKiAwIBA6MEAgIBAKUFMQMCAQSqAwIBAb-DeAMCAQK_hT4DAgEAv4U_AgUAMB8GA1UdIwQYMBaAFKPSqizvDYzyJALVHLRgvL9qWyQUMAoGCCqGSM49BAMCA0gAMEUCIDvdAMEwn-vbyFa1hmBc5Of_Zng6k5JRrNEiMPHLExg9AiEA50rMSNKApTfXZSNdwyKQKe8EMYd-99zMQKiH_4z8J59ZAxgwggMUMIICuqADAgECAgECMAoGCCqGSM49BAMCMIHcMT0wOwYDVQQDDDRGQUtFIEFuZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gUm9vdCBGQUtFMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDAeFw0xOTA0MjUwNTQ5MzJaFw00NjA5MTAwNTQ5MzJaMIHkMUUwQwYDVQQDDDxGQUtFIEFuZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gSW50ZXJtZWRpYXRlIEZBS0UxMTAvBgkqhkiG9w0BCQEWImNvbmZvcm1hbmNlLXRvb2xzQGZpZG9hbGxpYW5jZS5vcmcxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq1BhK2JNF7vDtRsESTsFSQuMH4udvPN5st7coHxSode2DdMhddwrft28JtsI1V-G9nG2lNwwTaSiioxOA6b1x6NjMGEwDwYDVR0TAQH_BAUwAwEB_zAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0OBBYEFKPSqizvDYzyJALVHLRgvL9qWyQUMB8GA1UdIwQYMBaAFFKaGzLgVqrNUQ_vX4A3BovykSMdMAoGCCqGSM49BAMCA0gAMEUCIQCxby5AL7R6zm05jfD9PsiyE-Pej_8HkhBGwerwiVkofQIgbvlDqKwni3BeFosThvROS4tp13uLjNqupmyhpJZfLvloYXV0aERhdGFYpUmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjxQAAAB1VDktUqkdAn5qVGrdsEwExACA8OgO0-cSn5WxA1-Q3leSFJcy3-0sX8JK_VABgo1fedaUBAgMmIAEhWCBJnVTz1FIwyJpDfGpgZC8jYVsSfx2uRtw2KaJrWJHXECJYIIjyfvSOUUcEqe3NX9rhZTDOsnDm-bOVGu_ARv2XejLjoA", + "clientDataJSON": "eyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUzMTMiLCJjaGFsbGVuZ2UiOiJwS3VBQ2g3TXVwYlFQWFNCbUhhaFpXZDlMWWhneHYyY0p4bXRlMzdqcTlNIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9" + }, + "getClientExtensionResults": {}, + "type": "public-key" + } +} diff --git a/tests/fixtures/attestation/fido-u2f.json b/tests/fixtures/attestation/fido-u2f.json new file mode 100644 index 0000000..43de888 --- /dev/null +++ b/tests/fixtures/attestation/fido-u2f.json @@ -0,0 +1,19 @@ +{ + "format": "fido-u2f", + "aaguid": "00000000-0000-0000-0000-000000000000", + "source": "FIDO Alliance Conformance Test Tools, server tests", + "capturedAt": "2026-09-03T03:46:43.152Z", + "expectedChallenge": "byIfux6An_GWzzJaNupDrMfp959R2dOSJwXtcU92EbQ", + "expectedOrigin": "http://localhost:5313", + "expectedRPID": "localhost", + "response": { + "id": "GGAimCmxEBUciX7R7ygFE9pW-2T7LJGc3IqcKhJlwZw", + "rawId": "GGAimCmxEBUciX7R7ygFE9pW-2T7LJGc3IqcKhJlwZw", + "response": { + "attestationObject": "o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgH5pNLysrINX2cw1wv1XzHpeVUuX9I9wmhCP-V5p8TEsCIQCFbmyTpReCyN89vR8EMwzJWRGJ8u8mu5aEi3JXFzGTt2N4NWOBWQQvMIIEKzCCAhOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDMxNjE0MzUyN1oXDTI4MDMxMzE0MzUyN1owgawxIzAhBgNVBAMMGkZJRE8yIEJBVENIIEtFWSBwcmltZTI1NnYxMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETzpeXqtsH7yul_bfZEmWdix773IAQCp2xvIw9lVvF6qZm1l_xL9Qiq-OnvDNAT9aub0nkUvwgEN4y8yxG4m1RqMsMCowCQYDVR0TBAIwADAdBgNVHQ4EFgQUVk33wPjGVbahH2xNGfO_QeL9AXkwDQYJKoZIhvcNAQEFBQADggIBAI-_jI31FB-8J2XxzBXMuI4Yg-vAtq07ABHJqnQpUmt8lpOzmvJ0COKcwtq_7bpsgSVBJ26zhnyWcm1q8V0ZbxUvN2kH8N7nteIGn-CJOJkHDII-IbiH4-TUQCJjuCB52duUWL0fGVw2R13J6V-K7U5r0OWBzmtmwwiRVTggVbjDpbx2oqGAwzupG3RmBFDX1M92s3tgywnLr-e6NZal5yZdS8VblJGjswDZbdY-Qobo2DCN6vxvn5TVkukAHiArjpBBpAmuQfKa52vqSCYRpTCm57fQUZ1c1n29OsvDw1x9ckyH8j_9Xgk0AG-MlQ9Rdg3hCb7LkSPvC_zYDeS2Cj_yFw6OWahnnIRwO6t4UtLuRAkLrjP1T7nk0zu1whwj7YEwtva45niWWh6rdyg_SZlfsph3o_MZN5DwKaSrUaEO6b-numELH5GWjjiPgfgPKkIof-D40xaKUFBpNJzorQkAZCJWuHvXRpBZWFVh_UhNlGhX0mhz2yFlBrujYa9BgvIkdJ8Keok6qfAn-r5EEFXcSI8vGY7OEF01QKXVpu8-FW0uSxtQ991AcFD6KjvR51l7e61visUgduhZRIq9bYzeCIxnK5Jhm3o_NJE2bOp2NmVwVe4kjuJX87wo3Ba41bXgwIpdiLWyWJhSHPmJI_1ibRTZ5XO92xbPPSnnkXrFaGF1dGhEYXRhWKRJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0EAAAACAAAAAAAAAAAAAAAAAAAAAAAgGGAimCmxEBUciX7R7ygFE9pW-2T7LJGc3IqcKhJlwZylAQIDJiABIVggyofFJsu2PrVjfPS76gN0NxpbsgTxX5cIIhS3OUnbgT8iWCBbu-VQOsrmUXo1LvxGeL6vSY42ytwzUme5zwZPsg4xiA", + "clientDataJSON": "eyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUzMTMiLCJjaGFsbGVuZ2UiOiJieUlmdXg2QW5fR1d6ekphTnVwRHJNZnA5NTlSMmRPU0p3WHRjVTkyRWJRIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9" + }, + "getClientExtensionResults": {}, + "type": "public-key" + } +} diff --git a/tests/fixtures/attestation/packed.json b/tests/fixtures/attestation/packed.json new file mode 100644 index 0000000..4d83306 --- /dev/null +++ b/tests/fixtures/attestation/packed.json @@ -0,0 +1,19 @@ +{ + "format": "packed", + "aaguid": "326adcf0-0cef-46d0-9392-98d6c4a84a72", + "source": "FIDO Alliance Conformance Test Tools, server tests", + "capturedAt": "2026-09-03T03:46:30.985Z", + "expectedChallenge": "46x_N9S6COjmbfaZJiNW1BhrNFUxTTsNIdml_M4yQkc", + "expectedOrigin": "http://localhost:5313", + "expectedRPID": "localhost", + "response": { + "id": "uUlnOWAB3Y3w_aDc7d3XvRiV3Hj5AXCoG6m8NeXCLc0", + "rawId": "uUlnOWAB3Y3w_aDc7d3XvRiV3Hj5AXCoG6m8NeXCLc0", + "response": { + "attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEgwRgIhAL0BCUFTPa9I7bGp06Ye6r60szwZz_Sn-EGJIf4oUP6ZAiEA3IxyAAC7nJVqUzBuJHQjcmtoVNkNLM5obX7NIo9guDJjeDVjgVkERTCCBEEwggIpoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwgaExGDAWBgNVBAMMD0ZJRE8yIFRFU1QgUk9PVDExMC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9yZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEMMAoGA1UECwwDQ1dHMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDAeFw0xODA1MjMxNDM5NDNaFw0yODA1MjAxNDM5NDNaMIHCMSMwIQYDVQQDDBpGSURPMiBCQVRDSCBLRVkgcHJpbWUyNTZ2MTExMC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9yZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1ZMRIwEAYDVQQHDAlXYWtlZmllbGQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARPOl5eq2wfvK6X9t9kSZZ2LHvvcgBAKnbG8jD2VW8XqpmbWX_Ev1CKr46e8M0BP1q5vSeRS_CAQ3jLzLEbibVGoywwKjAJBgNVHRMEAjAAMB0GA1UdDgQWBBRWTffA-MZVtqEfbE0Z879B4v0BeTANBgkqhkiG9w0BAQsFAAOCAgEAh92gn_ggiQXRLUHOCaTp1HpcBhsOw8ZwTKJBxwYK8ycQ5_QRXBcMRi8axVexH2HoUDTg_u-DkvH2UYGYjX_RAxgGIh4dPgrKXwVndtMwiI5QnQwXMocKtzyyeuSQv6INwk_QCuJL5LOAyPtNUWMTb_UvCcdYWjtZYFOeYQSK9T_6dtWSp6XAhIT4wf3CBaxyai-YiRn3nfi154vUrqtuDh56eODK7-Iezg9npbucln3XxW_kRhtk2FERSBmBoo7IotPd8NGTATnwUvt16vw6x3mW2a6zZGOOeYCQmeXlfNza7fSff1BdFWR5f4cJ0gFAv297Tf5dGZQvZD3DcyQ9OJeJ3RQQ9inX0Nhxk1-6cm1i2e8h9gTN7otjqYmnGjs3ezhPdax2AdrmckO43YNuchfTPECPTRzP4rQo3QbwGLeEAk_HV-oJmYiBkdhf2F2QLMm7SdeqZ1Jjg1W1vNJT288vj1EGF-_aKXg_bujAaK86_YNPBJaW9Rdw4EnfFUi5bEdkD5ZSpeAHCQzCDn2RzkBjs2rTFe4qRFUWtC-RZ4wFqRx70jXLIw-ArpeetpjtzJSNqQsqPlEvpyMxuV2ZjnruA2_ysP3RDzqNs7R8JVNKiie0RAbG7et43ULZcC7oix8JKYsJ6wDmX8Gyy7vWM-LS9XiZUH37sEvwKJbM-xxoYXV0aERhdGFYpUmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjxQAAAFMyatzwDO9G0JOSmNbEqEpyACC5SWc5YAHdjfD9oNzt3de9GJXcePkBcKgbqbw15cItzaUBAgMmIAEhWCCJ3L-pgI-f6hUa3_yLZ1CrV5AyAMhzfpMYUXJJeZpA4CJYIHYX9UBilVL9_diddpxtz146YKclDHem1ft9Uy9gISHAoA", + "clientDataJSON": "eyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUzMTMiLCJjaGFsbGVuZ2UiOiI0NnhfTjlTNkNPam1iZmFaSmlOVzFCaHJORlV4VFRzTklkbWxfTTR5UWtjIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9" + }, + "getClientExtensionResults": {}, + "type": "public-key" + } +} diff --git a/tests/fixtures/attestation/tpm.json b/tests/fixtures/attestation/tpm.json new file mode 100644 index 0000000..62dfe6b --- /dev/null +++ b/tests/fixtures/attestation/tpm.json @@ -0,0 +1,19 @@ +{ + "format": "tpm", + "aaguid": "a7d6d93a-8a0d-11e8-9a94-a6cf71072f73", + "source": "FIDO Alliance Conformance Test Tools, server tests", + "capturedAt": "2026-09-03T03:46:44.775Z", + "expectedChallenge": "LlG8LXfoBjXEHLTBKUudiTwRix47-i20qHEKdtWm-RM", + "expectedOrigin": "http://localhost:5313", + "expectedRPID": "localhost", + "response": { + "id": "VMFQzNkgboHu6y-Hs9BvAZ1LCjcrulttSeq2YNw1nvw", + "rawId": "VMFQzNkgboHu6y-Hs9BvAZ1LCjcrulttSeq2YNw1nvw", + "response": { + "attestationObject": "o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzkBAGNzaWdZAQB1SMdVfBninqZEB6EbR0QB2LEQGeHPhtOckZxuA38tikuydjv7c7QXWlc5kmVR8N7MEG2W9zs3lE89teub2dpzi5bBG4yMpJ8kVyr7HcggUkBJI_sqs_dnDy_6jWT1vUt2p4hJ59Gcp2yT_iDsXeVeGdAjrYvTI18_fBRITGcWxzrr2A0tRStyFKNNVYQN3XbNd6h_rg_bfC1DEmqmQPAHjdWXkaoHHLMm66RxUkZZfs3Z_zbl2DnBieXyKn6v9prJaIYeIw2dmfeovg6pd81lqJS1b3vcXp46fJ4rZ_VuZIQn2uljiXbAMI2kQzrHh6CQFw67kU1APHG2G3_TBw-8Y3ZlcmMyLjBjeDVjglkFDTCCBQkwggPxoAMCAQICDwTU0QE5E7SFz47yWEUqHzANBgkqhkiG9w0BAQsFADBBMT8wPQYDVQQDEzZOQ1UtTlRDLUtFWUlELUZGOTkwMzM4RTE4NzA3OUE2Q0Q2QTAzQURDNTcyMzc0NDVGNkE0OUEwHhcNMjUwOTAzMDM0NjQzWhcNMjcwOTAzMDM0NjQzWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArA0ESqJYrg4REF8hpebm3TCAE1E54v_EvV5dhQklI961WCCt_5j2J2dVeOtSN4_JmQNFOJhkJ52fpYNeoyu_dbEx7FsDIjWmwdi_pXNO8CmbePBIVvKMeJbuqjAPgxrMxTtggYKJDrdjaPoHBVomrt_6T55PpYvxTCRGLG7Ylc_0jFJLLWvxEubhwdi2chPHhyNgKF1AFZ1FnZH8N7Brw_aGq_4cn3YG5q5eOKUvMDFIaxL5Vf7cYyqZR4-mpkht8KAHtNI-Amuby3XeQSuQ4WaD4acRyO0qbQLyfNizGl4eMAysxX3ZcJyK43_vJvvCZT6QMe5XCLH6_J-F2MsyowIDAQABo4ICPTCCAjkwDgYDVR0PAQH_BAQDAgeAMAwGA1UdEwEB_wQCMAAwewYDVR0gAQH_BHEwbzBtBgkrBgEEAYI3FR8wYDBeBggrBgEFBQcCAjBSHlAARgBBAEsARQAgAEYASQBEAE8AIABUAEMAUABBACAAVAByAHUAcwB0AGUAZAAgAFAAbABhAHQAZgBvAHIAbQAgAEkAZABlAG4AdABpAHQAeTAQBgNVHSUECTAHBgVngQUIAzBKBgNVHREBAf8EQDA-pDwwOjE4MA4GBWeBBQIDDAVpZDoxMzAQBgVngQUCAgwHTlBDVDZ4eDAUBgVngQUCAQwLaWQ6RkZGRkYxRDAwHwYDVR0jBBgwFoAUdOhwbuNi8U8_KoCvb3uGHTvHco0wHQYDVR0OBBYEFJGAsnI2ZklZnEKGEKn3PDD0mQBGMIGBBggrBgEFBQcBAQR1MHMwcQYIKwYBBQUHMAKGZWh0dHBzOi8vcGtpLmNlcnRpbmZyYS5maWRvYWxsaWFuY2Uub3JnL3RwbS9OQ1UtTlRDLUtFWUlELUZGOTkwMzM4RTE4NzA3OUE2Q0Q2QTAzQURDNTcyMzc0NDVGNkE0OUEuY3J0MHoGA1UdHwRzMHEwb6BtoGuGaWh0dHBzOi8vcGtpLmNlcnRpbmZyYS5maWRvYWxsaWFuY2Uub3JnL3RwbS9jcmwvTkNVLU5UQy1LRVlJRC1GRjk5MDMzOEUxODcwNzlBNkNENkEwM0FEQzU3MjM3NDQ1RjZBNDlBLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAzNACKofmiN1SepPjVS1iyM14uNtAf6KM4y8TfXGA7iMA05GRCp6Gp90h_ZYNaOGivsrBeyF3gD-TOme6l6EsQLW7Bme32tnW-FdfIoVU_5pyhPj-LBXxpOehuBtwRp5B5x6pFcEL314JTvFhGo1M20ufRQY-IXEvES9N9lc2zBzyFFpDUKkIxRY-eAoPKF0LO1pZRi0S0zefUufIjBHQOme7OVipyrcpL4Z-Gx1A4r4NhWELy34fNjNokWfTWlb7xO2dMgIoFCEFT3iNB0J6-oNiM8pEtNKlldzK4fqLnGaOyjcIcFUfh81HTBhLSPU0NikWGS89_zHxRiFdWX6kdlkGMDCCBiwwggQUoAMCAQICDwRfOPZjZRSeuzRzUQmrKDANBgkqhkiG9w0BAQsFADCBvzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1ZMRIwEAYDVQQHDAlXYWtlZmllbGQxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxDDAKBgNVBAsMA0NXRzE2MDQGA1UEAwwtRklETyBGYWtlIFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE4MTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMB4XDTIyMDIwMTAwMDAwMFoXDTM1MDEzMTIzNTk1OVowQTE_MD0GA1UEAxM2TkNVLU5UQy1LRVlJRC1GRjk5MDMzOEUxODcwNzlBNkNENkEwM0FEQzU3MjM3NDQ1RjZBNDlBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1z5zfRGlB378sKiIsltfPe3--pyI4ii_CN7Pv38O1WCLwb0-H608v6ULS54UYExI9wp39EN1IV7ZAJhB317QO2IXGtr65YZrSiUjWEcjtsrYbM0BlJMoRvCCql_y9cxD7tI9qODo9_FI4sf82PbJ3q2Q2RRJ2Ua1Z9jxcYHef6r76e3dQd1RBYS2BShN7lpsFewbsxBK5M2hp3e_q-T60t3RbjiK8zqlsWoVQdyHHyYYsCkKmOVVet9E5oKhz0T3WEb0H5Y296QnR0zqQnrnwfLvKxxDol2ogSiViiMK1ZpY2bBx-_744TUDH_hRvbiCXeGkH0Q5wELOlvhQWYthQQIDAQABo4IBoDCCAZwwCwYDVR0PBAQDAgGGMBYGA1UdIAQPMA0wCwYJKwYBBAGCNxUfMBsGA1UdJQQUMBIGCSsGAQQBgjcVJAYFZ4EFCAMwEgYDVR0TAQH_BAgwBgEB_wIBADAdBgNVHQ4EFgQUdOhwbuNi8U8_KoCvb3uGHTvHco0wHwYDVR0jBBgwFoAUQxEWmZrunVA3wk_-4lsVyQkzPzIwfQYDVR0fBHYwdDByoHCgboZsaHR0cHM6Ly9wa2kuY2VydGluZnJhLmZpZG9hbGxpYW5jZS5vcmcvdHBtL2NybC9GSURPJTIwRmFrZSUyMFRQTSUyMFJvb3QlMjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIwMTguY3JsMIGEBggrBgEFBQcBAQR4MHYwdAYIKwYBBQUHMAKGaGh0dHBzOi8vcGtpLmNlcnRpbmZyYS5maWRvYWxsaWFuY2Uub3JnL3RwbS9GSURPJTIwRmFrZSUyMFRQTSUyMFJvb3QlMjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIwMTguY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBLI3MbuqcaT_BuAE4WKBcIucXm6Ff3e8pYWeiOFW7eSJnmBFIVlUXhXwasoufracT09_aiSE-goQC48VzFpJZXaYzO4DBCOR-VZj5IBfYDYNKQyzD28EZPWmFs5BAJnDh3pBwxfYtNZuxx0MARBJ1DC9v_kQJ4HZGS0xh8Is2f6rKNRb5L1D7iBZfkVzXSNMp_1OVpXR8eNqwutZDE_h7Nj74aMTMKuthL7AHcn2ZazDiIKLM5CO9D0f0N9sruICaoo7ScRZ7jeTR5YzPq30GN4iMeVSO_v_vx1QoCADpt2y-91rbfF0qGtXJUEsON0oyiF6AzCyafMlw7P-E9IbNwKFWELYFZlFeMawtyHcJNC0hLbmwuh9pUSdckWvnYQZW99EtoDsS014PV9Svw8Jp0z2opGA5EaaNFl4klp2-tsn_nF3lXs21NJuKwY2Ghwhcj0YeYz2kv71JRL1NTZEbk961eXqMx2oIyZ-t15OAExPa1Cg6aVsYT78ugIhI-3AXg_JbscqYJs0047NPZpjw6g2a0_fpuHWTyFzhmI4kDxPXRsw_GutWGC3sBJ2_QT9pfYQJx9krZKvRs1XpJfA8gvZWDZEYo2d1wctCyOCMVfYt6KQQAXgBCYVP1NwONLh3ttxBVb4HfSKLZpkhDjkzTNZWjlISg2NLyXwqkhPmT-GdwdWJBcmVhWQE2AAEACwAGBHIAIJ3_y_NsODrmmfuYaNxty4nXFTiEvigDkiwSQVi_rSKuABAAEAgAAAAAAAEAqVH2jmtxtJuq9BOldIaHNib4jT1J2ivX9EP-uxj28M_uuXcUCInzPuksVs-fMBM5_Ift4tCA7E9QJCaNfYQdSYCNtP5nXqn59HdzTP1If8zJhX-b-GJ7RUTNDVoYcYHTnJ07IwD4xSCrJC24_wv_wpHNIhYiCAsOb8H3vBQ3Po39CltTR-pFqxcPFiaymKwbLtdHg1loa7V4ImxGnxu4xa_nCaxfWJwWvsM7qRSn5ef3XFGX8i5Y30t0hJiNkitQT9xjLOm1qFCvD8Qqi9MdEsO6hFPYEX3SILcwhUMT9qb1NuuccLACtVFmXcwi1TRieYq8uNIWMW9q74I0hqKyp2hjZXJ0SW5mb1it_1RDR4AXACIACxHmjtRNtTcuFCluL4Ssx4OYdRiBkh4w_CKgb4tzx5RTACCnUjtx4ni16ldKD4RZkaYI221-Y14-9GrCOse1_Tj-1gAAAAFHcBdIVWl7S8aFYKUBc375jTRWVfsAIgALNYvIHQ6HMHmkabSl06H2sAux90aXU1-2_4dYihdC0fwAIgALUhiv0Sf1aFI8QRpjFDnL4ZkiAJxoPj6DGwWiKttacjRoYXV0aERhdGFZAWhJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY8UAAAAep9bZOooNEeialKbPcQcvcwAgVMFQzNkgboHu6y-Hs9BvAZ1LCjcrulttSeq2YNw1nvykAQMDOQEAIFkBAKlR9o5rcbSbqvQTpXSGhzYm-I09Sdor1_RD_rsY9vDP7rl3FAiJ8z7pLFbPnzATOfyH7eLQgOxPUCQmjX2EHUmAjbT-Z16p-fR3c0z9SH_MyYV_m_hie0VEzQ1aGHGB05ydOyMA-MUgqyQtuP8L_8KRzSIWIggLDm_B97wUNz6N_QpbU0fqRasXDxYmspisGy7XR4NZaGu1eCJsRp8buMWv5wmsX1icFr7DO6kUp-Xn91xRl_IuWN9LdISYjZIrUE_cYyzptahQrw_EKovTHRLDuoRT2BF90iC3MIVDE_am9TbrnHCwArVRZl3MItU0YnmKvLjSFjFvau-CNIaisqchQwEAAaA", + "clientDataJSON": "eyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUzMTMiLCJjaGFsbGVuZ2UiOiJMbEc4TFhmb0JqWEVITFRCS1V1ZGlUd1JpeDQ3LWkyMHFIRUtkdFdtLVJNIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9" + }, + "getClientExtensionResults": {}, + "type": "public-key" + } +} diff --git a/tests/unit/services/attestationFormats.spec.ts b/tests/unit/services/attestationFormats.spec.ts new file mode 100644 index 0000000..3b05f4c --- /dev/null +++ b/tests/unit/services/attestationFormats.spec.ts @@ -0,0 +1,218 @@ +/* + * Copyright © 2026 Fells Code, LLC + * Licensed under the GNU Affero General Public License v3.0 + * See LICENSE file in the project root for full license information + */ + +import { + MetadataService, + SettingsService, + verifyRegistrationResponse, +} from '@simplewebauthn/server'; +import { isoCBOR } from '@simplewebauthn/server/helpers'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; + +import { SUPPORTED_ALGORITHM_IDS } from '../../../src/lib/webauthnAlgorithms'; + +// The library's own verifiers are what a conformance run exercises, and the +// shared setup stubs them out. +vi.unmock('@simplewebauthn/server'); + +const FIXTURE_DIR = path.join( + path.dirname(fileURLToPath(import.meta.url)), + '../../fixtures/attestation', +); + +interface Fixture { + format: string; + aaguid: string; + expectedChallenge: string; + expectedOrigin: string; + expectedRPID: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + response: any; +} + +function load(name: string): Fixture { + return JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, `${name}.json`), 'utf8')); +} + +function verify(fixture: Fixture) { + return verifyRegistrationResponse({ + response: fixture.response, + expectedChallenge: fixture.expectedChallenge, + expectedOrigin: fixture.expectedOrigin, + expectedRPID: fixture.expectedRPID, + requireUserVerification: false, + supportedAlgorithmIDs: SUPPORTED_ALGORITHM_IDS, + }); +} + +/** + * The same statement with one byte of the signature flipped. + * + * Re-encoding the attestation object rather than corrupting the base64url keeps + * the response well formed, so what the verifier rejects is the cryptography and + * not the framing. + */ +function withTamperedSignature(fixture: Fixture): Fixture { + const decoded = isoCBOR.decodeFirst>( + new Uint8Array(Buffer.from(fixture.response.response.attestationObject, 'base64url')), + ); + + const attStmt = decoded.get('attStmt') as Map; + const signature = Uint8Array.from(attStmt.get('sig') as Uint8Array); + signature[signature.length - 1] ^= 0xff; + attStmt.set('sig', signature); + + const reencoded = isoCBOR.encode(decoded as never); + + return { + ...fixture, + response: { + ...fixture.response, + response: { + ...fixture.response.response, + attestationObject: Buffer.from(reencoded).toString('base64url'), + }, + }, + }; +} + +/** The same statement with the authenticator data corrupted instead. */ +function withTamperedAuthData(fixture: Fixture): Fixture { + const decoded = isoCBOR.decodeFirst>( + new Uint8Array(Buffer.from(fixture.response.response.attestationObject, 'base64url')), + ); + + const authData = Uint8Array.from(decoded.get('authData') as Uint8Array); + // The RP ID hash, which the relying party check catches ahead of any + // signature. The signature counter looks like the more natural target and is + // the wrong one: fido-u2f signs an explicit list of fields rather than the + // authenticator data as a whole, and the counter is not on that list, so + // moving it verifies happily. Signature coverage is what the tampered + // signature case above exercises. + authData[0] ^= 0xff; + decoded.set('authData', authData); + + return { + ...fixture, + response: { + ...fixture.response, + response: { + ...fixture.response.response, + attestationObject: Buffer.from(isoCBOR.encode(decoded as never)).toString('base64url'), + }, + }, + }; +} + +async function refuses(fixture: Fixture) { + try { + const result = await verify(fixture); + return result.verified === false; + } catch { + return true; + } +} + +describe('attestation formats, against statements captured from the FIDO conformance tools', () => { + // FIDO Server Requirements v2.3 names Packed, TPM, U2F and Android SafetyNet. + // SafetyNet is absent on purpose, see the final describe block for why. + describe.each([ + { name: 'packed', aaguid: '326adcf0-0cef-46d0-9392-98d6c4a84a72' }, + { name: 'tpm', aaguid: 'a7d6d93a-8a0d-11e8-9a94-a6cf71072f73' }, + { name: 'fido-u2f', aaguid: '00000000-0000-0000-0000-000000000000' }, + ])('$name', ({ name, aaguid }) => { + it('verifies, and reports the format and model it came from', async () => { + const result = await verify(load(name)); + + expect(result.verified).toBe(true); + expect(result.registrationInfo?.fmt).toBe(name); + expect(result.registrationInfo?.aaguid).toBe(aaguid); + }); + + it('is refused once the attestation signature is tampered with', async () => { + expect(await refuses(withTamperedSignature(load(name)))).toBe(true); + }); + + it('is refused once the authenticator data is tampered with', async () => { + expect(await refuses(withTamperedAuthData(load(name)))).toBe(true); + }); + }); + + describe('android-key, whose chain only validates against the conformance roots', () => { + let productionRoots: string[]; + + beforeAll(() => { + productionRoots = SettingsService.getRootCertificates({ identifier: 'android-key' }); + }); + + afterAll(() => { + SettingsService.setRootCertificates({ + identifier: 'android-key', + certificates: productionRoots, + }); + }); + + it('is refused against the real Google roots', async () => { + SettingsService.setRootCertificates({ + identifier: 'android-key', + certificates: productionRoots, + }); + + await expect(verify(load('android-key'))).rejects.toThrow(/root certificate/i); + }); + + it('verifies once the preset roots are cleared, which is what conformance mode does', async () => { + SettingsService.setRootCertificates({ identifier: 'android-key', certificates: [] }); + + const result = await verify(load('android-key')); + + expect(result.verified).toBe(true); + expect(result.registrationInfo?.fmt).toBe('android-key'); + }); + }); + + describe('an authenticator the metadata service does not list', () => { + // No mdsServers and no statements: the blob is never fetched, and the + // fixture's model is absent by construction rather than by network luck. + async function initializeMetadata(verificationMode: 'strict' | 'permissive') { + await MetadataService.initialize({ mdsServers: [], statements: [], verificationMode }); + } + + it('is refused when requireKnownAuthenticator is on', async () => { + await initializeMetadata('strict'); + + await expect(verify(load('packed'))).rejects.toThrow(/No metadata statement found/i); + }); + + it('registers without a metadata statement when requireKnownAuthenticator is off', async () => { + await initializeMetadata('permissive'); + + const result = await verify(load('packed')); + + expect(result.verified).toBe(true); + await expect(MetadataService.getStatement(load('packed').aaguid)).resolves.toBeUndefined(); + }); + }); + + describe('android-safetynet', () => { + // Not covered, and not coverable by a fixture. Two independent reasons: + // + // The conformance tools never send one. Three runs on 2026-09-02 and + // 2026-09-03, with every test group selected, produced no SafetyNet + // statement at all, though its metadata statement loads with the rest. + // + // Even given one, the verifier refuses a SafetyNet statement more than + // sixty seconds after the authenticator produced it, so a committed fixture + // would be dead on arrival and could only pass with a faked clock, which + // proves nothing about the format. + // + // Tracked on #210 rather than left implicit here. + it.todo('is exercised against a real statement'); + }); +});