test_webauthn_serialization.html (22947B)
1 <!DOCTYPE html> 2 <meta charset=utf-8> 3 <head> 4 <title>Tests W3C Web Authentication Data Types Serialization</title> 5 <script src="/tests/SimpleTest/SimpleTest.js"></script> 6 <script type="text/javascript" src="u2futil.js"></script> 7 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 8 </head> 9 <body> 10 11 <h1>Tests W3C Web Authentication Data Types Serialization</h1> 12 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1823782">Mozilla Bug 1823782</a> 13 14 <script class="testbody" type="text/javascript"> 15 "use strict"; 16 17 const { Assert } = SpecialPowers.ChromeUtils.importESModule( 18 "resource://testing-common/Assert.sys.mjs" 19 ); 20 21 function arrayBufferEqualsArray(actual, expected, description) { 22 ok(actual instanceof ArrayBuffer, `${description} (actual should be array)`); 23 ok(expected instanceof Array, `${description} (expected should be array)`); 24 is(actual.byteLength, expected.length, `${description} (actual and expected should have same length)`); 25 let actualView = new Uint8Array(actual); 26 for (let i = 0; i < actualView.length; i++) { 27 if (actualView[i] != expected[i]) { 28 throw new Error(`actual and expected differ in byte ${i}: ${actualView[i]} vs ${expected[i]}`); 29 } 30 } 31 ok(true, description); 32 } 33 34 function isEmptyArray(arr, description) { 35 ok(arr instanceof Array, `${description} (expecting Array)`); 36 is(arr.length, 0, `${description} (array should be empty)`); 37 } 38 39 function stringArrayEquals(actual, expected, description) { 40 is(actual.length, expected.length, `${description} (actual and expected should have the same length)`); 41 for (let i = 0; i < actual.length; i++) { 42 if (actual[i] instanceof String) { 43 throw new Error(`actual[${i}] is not a string` + typeof actual[i]); 44 } 45 if (actual[i] !== expected[i]) { 46 throw new Error(`actual and expected differ in position ${i}: ${actual[i]} vs ${expected[i]}`); 47 } 48 } 49 } 50 51 function shouldThrow(func, expectedError, description) { 52 let threw = false; 53 try { 54 func(); 55 } catch (e) { 56 is(e.message, expectedError); 57 threw = true; 58 } 59 ok(threw, description); 60 } 61 62 add_task(function test_parseCreationOptionsFromJSON_minimal() { 63 let creationOptionsJSON = { 64 rp: { name: "Example" }, 65 user: { id: "-l3qNLTKJng", name: "username", displayName: "display name" }, 66 challenge: "XNJTTB3kfqk", 67 pubKeyCredParams: [], 68 }; 69 let creationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON); 70 is(Object.getOwnPropertyNames(creationOptions).length, 9, "creation options should have 9 properties"); 71 is(creationOptions.rp.id, undefined, "rp.id should be undefined"); 72 is(creationOptions.rp.name, "Example", "rp.name should be Example"); 73 arrayBufferEqualsArray(creationOptions.user.id, [ 250, 93, 234, 52, 180, 202, 38, 120 ], "user.id should be as expected"); 74 is(creationOptions.user.displayName, "display name", "user.displayName should be 'display name'"); 75 is(creationOptions.user.name, "username", "user.name should be username"); 76 arrayBufferEqualsArray(creationOptions.challenge, [ 92, 210, 83, 76, 29, 228, 126, 169 ], "challenge should be as expected"); 77 isEmptyArray(creationOptions.pubKeyCredParams, "pubKeyCredParams should be an empty array"); 78 is(creationOptions.timeout, undefined, "timeout should be undefined"); 79 isEmptyArray(creationOptions.excludeCredentials, "excludeCredentials should be an empty array"); 80 is(creationOptions.authenticatorSelection.authenticatorAttachment, undefined, "authenticatorSelection.authenticatorAttachment should be undefined"); 81 is(creationOptions.authenticatorSelection.residentKey, undefined, "creationOptions.authenticatorSelection.residentKey should be undefined"); 82 is(creationOptions.authenticatorSelection.requireResidentKey, false, "creationOptions.authenticatorSelection.requireResidentKey should be false"); 83 is(creationOptions.authenticatorSelection.userVerification, "preferred", "creationOptions.authenticatorSelection.userVerification should be preferred"); 84 is(creationOptions.attestation, "none", "attestation should be none"); 85 stringArrayEquals(creationOptions.hints, [], "hints should be an empty array"); 86 is(Object.getOwnPropertyNames(creationOptions.extensions).length, 0, "extensions should be an empty object"); 87 }); 88 89 add_task(function test_parseCreationOptionsFromJSON() { 90 let creationOptionsJSON = { 91 rp: { name: "Example", id: "example.com" }, 92 user: { id: "19TVpqBBOAM", name: "username2", displayName: "another display name" }, 93 challenge: "dR82FeUh5q4", 94 pubKeyCredParams: [{ type: "public-key", alg: -7 }], 95 timeout: 20000, 96 excludeCredentials: [{ type: "public-key", id: "TeM2k_di7Dk", transports: [ "usb" ]}], 97 authenticatorSelection: { authenticatorAttachment: "platform", residentKey: "required", requireResidentKey: true, userVerification: "discouraged" }, 98 hints: ["hybrid"], 99 attestation: "indirect", 100 attestationFormats: ["fido-u2f"], 101 extensions: { 102 appid: "https://www.example.com/appID", 103 credProps: true, 104 hmacCreateSecret: true, 105 minPinLength: true, 106 prf: { 107 eval: { 108 first: "Zmlyc3Q", 109 second: "c2Vjb25k" 110 }, 111 evalByCredential: { 112 "19TVpqBBOAM": { 113 first: "Zmlyc3Q", 114 second: "c2Vjb25k" 115 } 116 } 117 } 118 }, 119 }; 120 let creationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON); 121 is(Object.getOwnPropertyNames(creationOptions).length, 10, "creation options should have 10 properties"); 122 is(creationOptions.rp.name, "Example", "rp.name should be Example"); 123 is(creationOptions.rp.id, "example.com", "rp.id should be example.com"); 124 arrayBufferEqualsArray(creationOptions.user.id, [ 215, 212, 213, 166, 160, 65, 56, 3 ], "user.id should be as expected"); 125 is(creationOptions.user.displayName, "another display name", "user.displayName should be 'another display name'"); 126 is(creationOptions.user.name, "username2", "user.name should be username2"); 127 arrayBufferEqualsArray(creationOptions.challenge, [ 117, 31, 54, 21, 229, 33, 230, 174 ], "challenge should be as expected"); 128 is(creationOptions.pubKeyCredParams.length, 1, "pubKeyCredParams should have one element"); 129 is(creationOptions.pubKeyCredParams[0].type, "public-key", "pubKeyCredParams[0].type should be public-key"); 130 is(creationOptions.pubKeyCredParams[0].alg, -7, "pubKeyCredParams[0].alg should be -7"); 131 is(creationOptions.timeout, 20000, "timeout should be 20000"); 132 is(creationOptions.excludeCredentials.length, 1, "excludeCredentials should have one element"); 133 is(creationOptions.excludeCredentials[0].type, "public-key", "excludeCredentials[0].type should be public-key"); 134 arrayBufferEqualsArray(creationOptions.excludeCredentials[0].id, [ 77, 227, 54, 147, 247, 98, 236, 57 ], "excludeCredentials[0].id should be as expected"); 135 is(creationOptions.excludeCredentials[0].transports.length, 1, "excludeCredentials[0].transports should have one element"); 136 is(creationOptions.excludeCredentials[0].transports[0], "usb", "excludeCredentials[0].transports[0] should be usb"); 137 is(creationOptions.authenticatorSelection.authenticatorAttachment, "platform", "authenticatorSelection.authenticatorAttachment should be platform"); 138 is(creationOptions.authenticatorSelection.residentKey, "required", "creationOptions.authenticatorSelection.residentKey should be required"); 139 is(creationOptions.authenticatorSelection.requireResidentKey, true, "creationOptions.authenticatorSelection.requireResidentKey should be true"); 140 is(creationOptions.authenticatorSelection.userVerification, "discouraged", "creationOptions.authenticatorSelection.userVerification should be discouraged"); 141 stringArrayEquals(creationOptions.hints, creationOptionsJSON.hints, "creationOptions.hints should be as expected"); 142 is(creationOptions.attestation, "indirect", "attestation should be indirect"); 143 is(creationOptions.extensions.appid, "https://www.example.com/appID", "extensions.appid should be https://www.example.com/appID"); 144 is(creationOptions.extensions.credProps, true, "extensions.credProps should be true"); 145 is(creationOptions.extensions.hmacCreateSecret, true, "extensions.hmacCreateSecret should be true"); 146 is(creationOptions.extensions.minPinLength, true, "extensions.minPinLength should be true"); 147 arrayBufferEqualsArray(creationOptions.extensions.prf.eval.first, [102, 105, 114, 115, 116], "extensions.prf.eval.first should be 'first'"); 148 arrayBufferEqualsArray(creationOptions.extensions.prf.eval.second, [115, 101, 99, 111, 110, 100], "extensions.prf.eval.second should be 'second'"); 149 arrayBufferEqualsArray(creationOptions.extensions.prf.evalByCredential["19TVpqBBOAM"].first, [102, 105, 114, 115, 116], "extensions.prf.evalByCredential[\"19TVpqBBOAM\"].first should be 'first'"); 150 arrayBufferEqualsArray(creationOptions.extensions.prf.evalByCredential["19TVpqBBOAM"].second, [115, 101, 99, 111, 110, 100], "extensions.prf.evalByCredential[\"19TVpqBBOAM\"].second should be 'second'"); 151 }); 152 153 add_task(function test_parseCreationOptionsFromJSON_malformed() { 154 let userIdNotBase64 = { 155 rp: { name: "Example" }, 156 user: { id: "/not urlsafe base64+", name: "username", displayName: "display name" }, 157 challenge: "XNJTTB3kfqk", 158 pubKeyCredParams: [], 159 }; 160 shouldThrow( 161 () => { PublicKeyCredential.parseCreationOptionsFromJSON(userIdNotBase64); }, 162 "PublicKeyCredential.parseCreationOptionsFromJSON: could not decode user ID as urlsafe base64", 163 "should get encoding error if user.id is not urlsafe base64" 164 ); 165 166 let challengeNotBase64 = { 167 rp: { name: "Example" }, 168 user: { id: "-l3qNLTKJng", name: "username", displayName: "display name" }, 169 challenge: "this is not urlsafe base64!", 170 pubKeyCredParams: [], 171 }; 172 shouldThrow( 173 () => { PublicKeyCredential.parseCreationOptionsFromJSON(challengeNotBase64); }, 174 "PublicKeyCredential.parseCreationOptionsFromJSON: could not decode challenge as urlsafe base64", 175 "should get encoding error if challenge is not urlsafe base64" 176 ); 177 178 let excludeCredentialsIdNotBase64 = { 179 rp: { name: "Example", id: "example.com" }, 180 user: { id: "-l3qNLTKJng", name: "username", displayName: "display name" }, 181 challenge: "dR82FeUh5q4", 182 pubKeyCredParams: [{ type: "public-key", alg: -7 }], 183 timeout: 20000, 184 excludeCredentials: [{ type: "public-key", id: "@#$%&^", transports: [ "usb" ]}], 185 authenticatorselection: { authenticatorattachment: "platform", residentkey: "required", requireresidentkey: true, userverification: "discouraged" }, 186 hints: ["hybrid"], 187 attestation: "indirect", 188 attestationformats: ["fido-u2f"], 189 extensions: { appid: "https://www.example.com/appid", hmaccreatesecret: true }, 190 }; 191 shouldThrow( 192 () => { PublicKeyCredential.parseCreationOptionsFromJSON(excludeCredentialsIdNotBase64); }, 193 "PublicKeyCredential.parseCreationOptionsFromJSON: could not decode excluded credential ID as urlsafe base64", 194 "should get encoding error if excludeCredentials[0].id is not urlsafe base64" 195 ); 196 }); 197 198 add_task(function test_parseRequestOptionsFromJSON_minimal() { 199 let requestOptionsJSON = { 200 challenge: "3yW2WHD_jbU", 201 }; 202 let requestOptions = PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJSON); 203 is(Object.getOwnPropertyNames(requestOptions).length, 5, "request options should have 5 properties"); 204 arrayBufferEqualsArray(requestOptions.challenge, [ 223, 37, 182, 88, 112, 255, 141, 181 ], "challenge should be as expected"); 205 is(requestOptions.timeout, undefined, "timeout should be undefined"); 206 is(requestOptions.rpId, undefined, "rpId should be undefined"); 207 isEmptyArray(requestOptions.allowCredentials, "allowCredentials should be an empty array"); 208 is(requestOptions.userVerification, "preferred", "userVerification should be preferred"); 209 stringArrayEquals(requestOptions.hints, [], "hints should be an empty array"); 210 is(Object.getOwnPropertyNames(requestOptions.extensions).length, 0, "extensions should be an empty object"); 211 }); 212 213 add_task(function test_parseRequestOptionsFromJSON() { 214 let requestOptionsJSON = { 215 challenge: "QAfaZwEQCkQ", 216 timeout: 25000, 217 rpId: "example.com", 218 allowCredentials: [{type: "public-key", id: "BTBXXGuXRTk", transports: ["smart-card"] }], 219 userVerification: "discouraged", 220 hints: ["client-device"], 221 attestation: "enterprise", 222 attestationFormats: ["packed"], 223 extensions: { 224 appid: "https://www.example.com/anotherAppID", 225 prf: { 226 eval: { 227 first: "Zmlyc3Q", 228 second: "c2Vjb25k" 229 }, 230 evalByCredential: { 231 "19TVpqBBOAM": { 232 first: "Zmlyc3Q", 233 second: "c2Vjb25k" 234 } 235 } 236 } 237 }, 238 }; 239 let requestOptions = PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJSON); 240 is(Object.getOwnPropertyNames(requestOptions).length, 7, "request options should have 7 properties"); 241 arrayBufferEqualsArray(requestOptions.challenge, [ 64, 7, 218, 103, 1, 16, 10, 68 ], "challenge should be as expected"); 242 is(requestOptions.timeout, 25000, "timeout should be 25000"); 243 is(requestOptions.rpId, "example.com", "rpId should be example.com"); 244 is(requestOptions.allowCredentials.length, 1, "allowCredentials should have one element"); 245 is(requestOptions.allowCredentials[0].type, "public-key", "allowCredentials[0].type should be public-key"); 246 arrayBufferEqualsArray(requestOptions.allowCredentials[0].id, [ 5, 48, 87, 92, 107, 151, 69, 57 ], "allowCredentials[0].id should be as expected"); 247 is(requestOptions.allowCredentials[0].transports.length, 1, "allowCredentials[0].transports should have one element"); 248 is(requestOptions.allowCredentials[0].transports[0], "smart-card", "allowCredentials[0].transports[0] should be usb"); 249 is(requestOptions.userVerification, "discouraged", "userVerification should be discouraged"); 250 stringArrayEquals(requestOptions.hints, requestOptionsJSON.hints, "requestOptions.hints should be as expected"); 251 is(requestOptions.extensions.appid, "https://www.example.com/anotherAppID", "extensions.appid should be https://www.example.com/anotherAppID"); 252 arrayBufferEqualsArray(requestOptions.extensions.prf.eval.first, [102, 105, 114, 115, 116], "extensions.prf.eval.first should be 'first'"); 253 arrayBufferEqualsArray(requestOptions.extensions.prf.eval.second, [115, 101, 99, 111, 110, 100], "extensions.prf.eval.second should be 'second'"); 254 arrayBufferEqualsArray(requestOptions.extensions.prf.evalByCredential["19TVpqBBOAM"].first, [102, 105, 114, 115, 116], "extensions.prf.evalByCredential[\"19TVpqBBOAM\"].first should be 'first'"); 255 arrayBufferEqualsArray(requestOptions.extensions.prf.evalByCredential["19TVpqBBOAM"].second, [115, 101, 99, 111, 110, 100], "extensions.prf.evalByCredential[\"19TVpqBBOAM\"].second should be 'second'"); 256 }); 257 258 add_task(function test_parseRequestOptionsFromJSON_malformed() { 259 let challengeNotBase64 = { 260 challenge: "/not+urlsafe+base64/", 261 }; 262 shouldThrow( 263 () => { PublicKeyCredential.parseRequestOptionsFromJSON(challengeNotBase64); }, 264 "PublicKeyCredential.parseRequestOptionsFromJSON: could not decode challenge as urlsafe base64", 265 "should get encoding error if challenge is not urlsafe base64" 266 ); 267 268 let allowCredentialsIdNotBase64 = { 269 challenge: "QAfaZwEQCkQ", 270 timeout: 25000, 271 rpId: "example.com", 272 allowCredentials: [{type: "public-key", id: "not urlsafe base64", transports: ["smart-card"] }], 273 userVerification: "discouraged", 274 hints: ["client-device"], 275 attestation: "enterprise", 276 attestationFormats: ["packed"], 277 extensions: { appid: "https://www.example.com/anotherAppID" }, 278 }; 279 shouldThrow( 280 () => { PublicKeyCredential.parseRequestOptionsFromJSON(allowCredentialsIdNotBase64); }, 281 "PublicKeyCredential.parseRequestOptionsFromJSON: could not decode allowed credential ID as urlsafe base64", 282 "should get encoding error if allowCredentials[0].id is not urlsafe base64" 283 ); 284 }); 285 286 add_task(async () => { 287 await addVirtualAuthenticator(); 288 }); 289 290 function isUrlsafeBase64(urlsafeBase64) { 291 try { 292 atob(urlsafeBase64.replace(/_/g, "/").replace(/-/g, "+")); 293 return true; 294 } catch (_) {} 295 return false; 296 } 297 298 add_task(async function test_registrationResponse_toJSON() { 299 let publicKey = { 300 rp: {id: document.domain, name: "none", icon: "none"}, 301 user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}, 302 challenge: crypto.getRandomValues(new Uint8Array(16)), 303 pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], 304 authenticatorSelection: { residentKey: "discouraged" }, 305 extensions: { credProps: true } 306 }; 307 let registrationResponse = await navigator.credentials.create({publicKey}); 308 let registrationResponseJSON = registrationResponse.toJSON(); 309 is(Object.keys(registrationResponseJSON).length, 6, "registrationResponseJSON should have 6 properties"); 310 is(registrationResponseJSON.id, registrationResponseJSON.rawId, "registrationResponseJSON.id and rawId should be the same"); 311 ok(isUrlsafeBase64(registrationResponseJSON.id), "registrationResponseJSON.id should be urlsafe base64"); 312 is(Object.keys(registrationResponseJSON.response).length, 6, "registrationResponseJSON.response should have 6 properties"); 313 ok(isUrlsafeBase64(registrationResponseJSON.response.clientDataJSON), "registrationResponseJSON.response.clientDataJSON should be urlsafe base64"); 314 ok(isUrlsafeBase64(registrationResponseJSON.response.authenticatorData), "registrationResponseJSON.response.authenticatorData should be urlsafe base64"); 315 ok(isUrlsafeBase64(registrationResponseJSON.response.publicKey), "registrationResponseJSON.response.publicKey should be urlsafe base64"); 316 ok(isUrlsafeBase64(registrationResponseJSON.response.attestationObject), "registrationResponseJSON.response.attestationObject should be urlsafe base64"); 317 is(registrationResponseJSON.response.publicKeyAlgorithm, cose_alg_ECDSA_w_SHA256, "registrationResponseJSON.response.publicKeyAlgorithm should be ECDSA with SHA256 (COSE)"); 318 is(registrationResponseJSON.response.transports.length, 1, "registrationResponseJSON.response.transports.length should be 1"); 319 is(registrationResponseJSON.response.transports[0], "internal", "registrationResponseJSON.response.transports[0] should be internal"); 320 is(registrationResponseJSON.authenticatorAttachment, "platform", "registrationResponseJSON.authenticatorAttachment should be platform"); 321 is(registrationResponseJSON.clientExtensionResults?.credProps?.rk, false, "registrationResponseJSON.clientExtensionResults.credProps.rk should be false"); 322 is(registrationResponseJSON.type, "public-key", "registrationResponseJSON.type should be public-key"); 323 }); 324 325 add_task(async function test_assertionResponse_toJSON() { 326 let registrationRequest = { 327 publicKey: { 328 rp: {id: document.domain, name: "none", icon: "none"}, 329 user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}, 330 challenge: crypto.getRandomValues(new Uint8Array(16)), 331 pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], 332 extensions: { prf: { enabled: true } } 333 }, 334 }; 335 let registrationResponse = await navigator.credentials.create(registrationRequest); 336 337 let assertionRequest = { 338 publicKey: { 339 challenge: crypto.getRandomValues(new Uint8Array(16)), 340 allowCredentials: [{ type: "public-key", id: registrationResponse.rawId }], 341 extensions: { prf: { eval: { first: new Uint8Array([1,2,3,4,5]).buffer } } } 342 }, 343 }; 344 let assertionResponse = await navigator.credentials.get(assertionRequest); 345 let assertionResponseJSON = assertionResponse.toJSON(); 346 is(Object.keys(assertionResponseJSON).length, 6, "assertionResponseJSON should have 6 properties"); 347 is(assertionResponseJSON.id, assertionResponseJSON.rawId, "assertionResponseJSON.id and rawId should be the same"); 348 ok(isUrlsafeBase64(assertionResponseJSON.id), "assertionResponseJSON.id should be urlsafe base64"); 349 is(Object.keys(assertionResponseJSON.response).length, 3, "assertionResponseJSON.response should have 3 properties"); 350 ok(isUrlsafeBase64(assertionResponseJSON.response.clientDataJSON), "assertionResponseJSON.response.clientDataJSON should be urlsafe base64"); 351 ok(isUrlsafeBase64(assertionResponseJSON.response.authenticatorData), "assertionResponseJSON.response.authenticatorData should be urlsafe base64"); 352 ok(isUrlsafeBase64(assertionResponseJSON.response.signature), "assertionResponseJSON.response.signature should be urlsafe base64"); 353 is(assertionResponseJSON.authenticatorAttachment, "platform", "assertionResponseJSON.authenticatorAttachment should be platform"); 354 is(Object.keys(assertionResponseJSON.clientExtensionResults).length, 1, "assertionResponseJSON.clientExtensionResults should have one entry"); 355 ok(isUrlsafeBase64(assertionResponseJSON.clientExtensionResults.prf.results.first), "assertionResponseJSON.clientExtensionResults.prf.results should be urlsafe base64"); 356 is(assertionResponseJSON.clientExtensionResults.prf.results.first.length, 43, "assertionResponseJSON.clientExtensionResults.prf.results should be of length 43"); 357 is(assertionResponseJSON.type, "public-key", "assertionResponseJSON.type should be public-key"); 358 }); 359 </script> 360 361 </body> 362 </html>