<!DOCTYPE html> <meta charset=utf-8> <head> <script type="text/javascript" src="u2futil.js"></script> <script type="text/javascript" src="pkijs/common.js"></script> <script type="text/javascript" src="pkijs/asn1.js"></script> <script type="text/javascript" src="pkijs/x509_schema.js"></script> <script type="text/javascript" src="pkijs/x509_simpl.js"></script> </head> <body> <p>Register and Sign Test for FIDO Universal Second Factor</p> <script class="testbody" type="text/javascript"> "use strict"; var state = { // Raw messages regRequest: null, regResponse: null, regKey: null, signChallenge: null, signResponse: null, // Parsed values publicKey: null, keyHandle: null, // Constants version: "U2F_V2", appId: window.location.origin, }; SpecialPowers.pushPrefEnv({"set": [["security.webauth.u2f", true], ["security.webauth.u2f_enable_softtoken", true]]}, function() { local_isnot(window.u2f, undefined, "U2F API endpoint must exist"); local_isnot(window.u2f.register, undefined, "U2F Register API endpoint must exist"); local_isnot(window.u2f.sign, undefined, "U2F Sign API endpoint must exist"); testRegistering(); function testRegistering() { var challenge = new Uint8Array(16); window.crypto.getRandomValues(challenge); state.regRequest = { version: state.version, challenge: bytesToBase64UrlSafe(challenge), }; u2f.register(state.appId, [state.regRequest], [], function(regResponse) { state.regResponse = regResponse; local_is(regResponse.errorCode, 0, "The registration did not error"); local_isnot(regResponse.registrationData, undefined, "The registration did not provide registration data"); if (regResponse.errorCode > 0) { local_finished(); return; } // Parse the response data from the U2F token var registrationData = base64ToBytesUrlSafe(regResponse.registrationData); local_is(registrationData[0], 0x05, "Reserved byte is correct") state.publicKeyBytes = registrationData.slice(1, 66); var keyHandleLength = registrationData[66]; state.keyHandleBytes = registrationData.slice(67, 67 + keyHandleLength); state.keyHandle = bytesToBase64UrlSafe(state.keyHandleBytes); state.attestation = registrationData.slice(67 + keyHandleLength); local_is(state.attestation[0], 0x30, "Attestation Certificate has correct starting byte"); var asn1 = org.pkijs.fromBER(state.attestation.buffer); console.log(asn1); state.attestationCert = new org.pkijs.simpl.CERT({ schema: asn1.result }); console.log(state.attestationCert); state.attestationSig = state.attestation.slice(asn1.offset); local_is(state.attestationCert.subject.types_and_values[0].value.value_block.value, "Firefox U2F Soft Token", "Expected Subject"); local_is(state.attestationCert.issuer.types_and_values[0].value.value_block.value, "Firefox U2F Soft Token", "Expected Issuer"); local_is(state.attestationCert.notAfter.value - state.attestationCert.notBefore.value, 1000*60*60*48, "Valid 48 hours (in millis)"); // Verify that the clientData from the U2F token makes sense var clientDataJSON = ""; base64ToBytesUrlSafe(regResponse.clientData).map(x => clientDataJSON += String.fromCharCode(x)); var clientData = JSON.parse(clientDataJSON); local_is(clientData.typ, "navigator.id.finishEnrollment", "Register - Data type matches"); local_is(clientData.challenge, state.regRequest.challenge, "Register - Challenge matches"); local_is(clientData.origin, window.location.origin, "Register - Origins are the same"); // Verify the signature from the attestation certificate deriveAppAndChallengeParam(state.appId, string2buffer(clientDataJSON)) .then(function(params){ state.appParam = params.appParam; state.challengeParam = params.challengeParam; return state.attestationCert.getPublicKey(); }).then(function(attestationPublicKey) { var signedData = assembleRegistrationSignedData(state.appParam, state.challengeParam, state.keyHandleBytes, state.publicKeyBytes); return verifySignature(attestationPublicKey, signedData, state.attestationSig); }).then(function(verified) { console.log("No error verifying signature"); local_ok(verified, "Attestation Certificate signature verified") // Import the public key of the U2F token into WebCrypto return importPublicKey(state.publicKeyBytes) }).then(function(key) { state.publicKey = key; local_ok(true, "Imported public key") // Ensure the attestation certificate is properly self-signed return state.attestationCert.verify() }).then(function(){ local_ok(true, "Attestation Certificate verification successful"); // Continue test testReRegister() }).catch(function(err){ console.log(err); local_ok(false, "Attestation Certificate verification failed"); local_finished(); }); }); } function testReRegister() { state.regKey = { version: state.version, keyHandle: state.keyHandle, }; // Test that we don't re-register if we provide regKey as an // "already known" key handle. The U2F module should recognize regKey // as being usable and, thus, give back errorCode 4. u2f.register(state.appId, [state.regRequest], [state.regKey], function(regResponse) { // Since we attempted to register with state.regKey as a known key, expect // ineligible (=4). local_is(regResponse.errorCode, 4, "The re-registration should show device ineligible"); local_is(regResponse.registrationData, undefined, "The re-registration did not provide registration data"); // Continue test testSigning(); }); } function testSigning() { var challenge = new Uint8Array(16); window.crypto.getRandomValues(challenge); state.signChallenge = bytesToBase64UrlSafe(challenge); // Now try to sign the signature challenge u2f.sign(state.appId, state.signChallenge, [state.regKey], function(signResponse) { state.signResponse = signResponse; // Make sure this signature op worked, bailing early if it failed. local_is(signResponse.errorCode, 0, "The signing did not error"); local_isnot(signResponse.clientData, undefined, "The signing did not provide client data"); if (signResponse.errorCode > 0) { local_finished(); return; } // Decode the clientData that was returned from the module var clientDataJSON = ""; base64ToBytesUrlSafe(signResponse.clientData).map(x => clientDataJSON += String.fromCharCode(x)); var clientData = JSON.parse(clientDataJSON); local_is(clientData.typ, "navigator.id.getAssertion", "Sign - Data type matches"); local_is(clientData.challenge, state.signChallenge, "Sign - Challenge matches"); local_is(clientData.origin, window.location.origin, "Sign - Origins are the same"); // Parse the signature data var signatureData = base64ToBytesUrlSafe(signResponse.signatureData); if (signatureData[0] != 0x01) { throw "User presence byte not set"; } var presenceAndCounter = signatureData.slice(0,5); var signatureValue = signatureData.slice(5); // Assemble the signed data and verify the signature deriveAppAndChallengeParam(state.appId, string2buffer(clientDataJSON)) .then(function(params){ return assembleSignedData(params.appParam, presenceAndCounter, params.challengeParam); }) .then(function(signedData) { return verifySignature(state.publicKey, signedData, signatureValue); }) .then(function(verified) { console.log("No error verifying signing signature"); local_ok(verified, "Signing signature verified") local_finished(); }) .catch(function(err) { console.log(err); local_ok(false, "Signing signature invalid"); local_finished(); }); }); } }); </script> </body> </html>