Bug 1401803 - WebAuthn types need to return ArrayBuffers r?keeler
The Web Authentication types, by spec, return ArrayBuffer objects, while we
were returning a concrete Uint8Array. This is a fairly straightforward change
to add functionality to CryptoBuffer and the WebIDL types, however it's a
substantial change to the tests.
Frankly, the tests just could use another pass of clean-up now, since this is
a lot of relative ugliness added in. I refactored tab_webauthn_success.html
pretty heavily -- since it was also fairly ugly to start -- but I decided to go
with a lighter touch on the other tests.
MozReview-Commit-ID: 9vb1wdLo3SI
--- a/dom/crypto/CryptoBuffer.cpp
+++ b/dom/crypto/CryptoBuffer.cpp
@@ -156,16 +156,22 @@ CryptoBuffer::ToSECItem(PLArenaPool *aAr
}
JSObject*
CryptoBuffer::ToUint8Array(JSContext* aCx) const
{
return Uint8Array::Create(aCx, Length(), Elements());
}
+JSObject*
+CryptoBuffer::ToArrayBuffer(JSContext* aCx) const
+{
+ return ArrayBuffer::Create(aCx, Length(), Elements());
+}
+
bool
CryptoBuffer::ToNewUnsignedBuffer(uint8_t** aBuf, uint32_t* aBufLen) const
{
MOZ_ASSERT(aBuf);
MOZ_ASSERT(aBufLen);
uint32_t dataLen = Length();
uint8_t* tmp = reinterpret_cast<uint8_t*>(moz_xmalloc(dataLen));
--- a/dom/crypto/CryptoBuffer.h
+++ b/dom/crypto/CryptoBuffer.h
@@ -42,16 +42,17 @@ public:
aArray.ComputeLengthAndData();
return Assign(aArray.Data(), aArray.Length());
}
nsresult FromJwkBase64(const nsString& aBase64);
nsresult ToJwkBase64(nsString& aBase64) const;
bool ToSECItem(PLArenaPool* aArena, SECItem* aItem) const;
JSObject* ToUint8Array(JSContext* aCx) const;
+ JSObject* ToArrayBuffer(JSContext* aCx) const;
bool ToNewUnsignedBuffer(uint8_t** aBuf, uint32_t* aBufLen) const;
bool GetBigIntValue(unsigned long& aRetVal);
};
} // namespace dom
} // namespace mozilla
--- a/dom/webauthn/AuthenticatorAssertionResponse.cpp
+++ b/dom/webauthn/AuthenticatorAssertionResponse.cpp
@@ -54,17 +54,17 @@ AuthenticatorAssertionResponse::WrapObje
return AuthenticatorAssertionResponseBinding::Wrap(aCx, this, aGivenProto);
}
void
AuthenticatorAssertionResponse::GetAuthenticatorData(JSContext* aCx,
JS::MutableHandle<JSObject*> aRetVal)
{
if (!mAuthenticatorDataCachedObj) {
- mAuthenticatorDataCachedObj = mAuthenticatorData.ToUint8Array(aCx);
+ mAuthenticatorDataCachedObj = mAuthenticatorData.ToArrayBuffer(aCx);
}
aRetVal.set(mAuthenticatorDataCachedObj);
}
nsresult
AuthenticatorAssertionResponse::SetAuthenticatorData(CryptoBuffer& aBuffer)
{
if (NS_WARN_IF(!mAuthenticatorData.Assign(aBuffer))) {
@@ -73,17 +73,17 @@ AuthenticatorAssertionResponse::SetAuthe
return NS_OK;
}
void
AuthenticatorAssertionResponse::GetSignature(JSContext* aCx,
JS::MutableHandle<JSObject*> aRetVal)
{
if (!mSignatureCachedObj) {
- mSignatureCachedObj = mSignature.ToUint8Array(aCx);
+ mSignatureCachedObj = mSignature.ToArrayBuffer(aCx);
}
aRetVal.set(mSignatureCachedObj);
}
nsresult
AuthenticatorAssertionResponse::SetSignature(CryptoBuffer& aBuffer)
{
if (NS_WARN_IF(!mSignature.Assign(aBuffer))) {
--- a/dom/webauthn/AuthenticatorAttestationResponse.cpp
+++ b/dom/webauthn/AuthenticatorAttestationResponse.cpp
@@ -51,17 +51,17 @@ AuthenticatorAttestationResponse::WrapOb
return AuthenticatorAttestationResponseBinding::Wrap(aCx, this, aGivenProto);
}
void
AuthenticatorAttestationResponse::GetAttestationObject(JSContext* aCx,
JS::MutableHandle<JSObject*> aRetVal)
{
if (!mAttestationObjectCachedObj) {
- mAttestationObjectCachedObj = mAttestationObject.ToUint8Array(aCx);
+ mAttestationObjectCachedObj = mAttestationObject.ToArrayBuffer(aCx);
}
aRetVal.set(mAttestationObjectCachedObj);
}
nsresult
AuthenticatorAttestationResponse::SetAttestationObject(CryptoBuffer& aBuffer)
{
if (NS_WARN_IF(!mAttestationObject.Assign(aBuffer))) {
--- a/dom/webauthn/AuthenticatorResponse.cpp
+++ b/dom/webauthn/AuthenticatorResponse.cpp
@@ -53,17 +53,17 @@ AuthenticatorResponse::WrapObject(JSCont
return AuthenticatorResponseBinding::Wrap(aCx, this, aGivenProto);
}
void
AuthenticatorResponse::GetClientDataJSON(JSContext* aCx,
JS::MutableHandle<JSObject*> aRetVal)
{
if (!mClientDataJSONCachedObj) {
- mClientDataJSONCachedObj = mClientDataJSON.ToUint8Array(aCx);
+ mClientDataJSONCachedObj = mClientDataJSON.ToArrayBuffer(aCx);
}
aRetVal.set(mClientDataJSONCachedObj);
}
nsresult
AuthenticatorResponse::SetClientDataJSON(CryptoBuffer& aBuffer)
{
if (NS_WARN_IF(!mClientDataJSON.Assign(aBuffer))) {
--- a/dom/webauthn/PublicKeyCredential.cpp
+++ b/dom/webauthn/PublicKeyCredential.cpp
@@ -50,17 +50,17 @@ PublicKeyCredential::WrapObject(JSContex
return PublicKeyCredentialBinding::Wrap(aCx, this, aGivenProto);
}
void
PublicKeyCredential::GetRawId(JSContext* aCx,
JS::MutableHandle<JSObject*> aRetVal)
{
if (!mRawIdCachedObj) {
- mRawIdCachedObj = mRawId.ToUint8Array(aCx);
+ mRawIdCachedObj = mRawId.ToArrayBuffer(aCx);
}
aRetVal.set(mRawIdCachedObj);
}
already_AddRefed<AuthenticatorResponse>
PublicKeyCredential::Response() const
{
RefPtr<AuthenticatorResponse> temp(mResponse);
--- a/dom/webauthn/tests/browser/browser.ini
+++ b/dom/webauthn/tests/browser/browser.ini
@@ -1,9 +1,9 @@
[DEFAULT]
support-files =
- frame_webauthn_success.html
+ tab_webauthn_success.html
../cbor/*
../pkijs/*
../u2futil.js
skip-if = !e10s
[browser_webauthn_telemetry.js]
--- a/dom/webauthn/tests/browser/browser_webauthn_telemetry.js
+++ b/dom/webauthn/tests/browser/browser_webauthn_telemetry.js
@@ -56,17 +56,17 @@ async function executeTestPage(aUri) {
return BrowserTestUtils.removeTab(gBrowser.selectedTab);
}
}
add_task(async function test_loopback() {
// These tests can't run simultaneously as the preference changes will race.
// So let's run them sequentially here, but in an async function so we can
// use await.
- const testPage = "https://example.com/browser/dom/webauthn/tests/browser/frame_webauthn_success.html";
+ const testPage = "https://example.com/browser/dom/webauthn/tests/browser/tab_webauthn_success.html";
{
cleanupTelemetry();
// Enable the soft token, and execute a simple end-to-end test
Services.prefs.setBoolPref("security.webauth.webauthn", true);
Services.prefs.setBoolPref("security.webauth.webauthn_enable_softtoken", true);
Services.prefs.setBoolPref("security.webauth.webauthn_enable_usbtoken", false);
await executeTestPage(testPage);
rename from dom/webauthn/tests/browser/frame_webauthn_success.html
rename to dom/webauthn/tests/browser/tab_webauthn_success.html
--- a/dom/webauthn/tests/browser/frame_webauthn_success.html
+++ b/dom/webauthn/tests/browser/tab_webauthn_success.html
@@ -27,102 +27,93 @@ window.crypto.getRandomValues(gAssertion
function signalCompletion(aText) {
console.log(aText)
let result = document.createElement('h1');
result.id = "result";
result.textContent = aText;
document.body.append(result);
}
-function util_decodeCreatedCredential(aCredInfo) {
- /* PublicKeyCredential : Credential
- - rawId: Key Handle buffer pulled from U2F Register() Response
- - response : AuthenticatorAttestationResponse : AuthenticatorResponse
- - attestationObject: CBOR object
- - clientDataJSON: serialized JSON
- - clientExtensionResults: (not yet supported)
- */
-
- return webAuthnDecodeAttestation(aCredInfo.response.attestationObject.buffer)
- .then(function(decodedResult) {
- aCredInfo.publicKeyHandle = decodedResult.publicKeyHandle;
- aCredInfo.attestationObject = decodedResult.attestationObject;
- return aCredInfo;
- });
-}
-
-function util_checkAssertionAndSigValid(aPublicKey, aAssertion) {
- /* PublicKeyCredential : Credential
- - rawId: ID of Credential from AllowList that succeeded
- - response : AuthenticatorAssertionResponse : AuthenticatorResponse
- - clientDataJSON: serialized JSON
- - authenticatorData: RP ID Hash || U2F Sign() Response
- - signature: U2F Sign() Response
- */
-
- // Parse the signature data
- if (aAssertion.response.signature[0] != 0x01) {
- throw "User presence byte not set";
- }
-
- let presenceAndCounter = aAssertion.response.signature.slice(0,5);
- let signatureValue = aAssertion.response.signature.slice(5);
-
- let rpIdHash = aAssertion.response.authenticatorData.slice(0,32);
-
- // Assemble the signed data and verify the signature
- return deriveAppAndChallengeParam(window.location.host, aAssertion.response.clientDataJSON)
- .then(function(aParams) {
- console.log(aParams.appParam, rpIdHash, presenceAndCounter, aParams.challengeParam);
- console.log("ClientData buffer: ", hexEncode(aAssertion.response.clientDataJSON));
- console.log("ClientDataHash: ", hexEncode(aParams.challengeParam));
- return assembleSignedData(aParams.appParam, presenceAndCounter, aParams.challengeParam);
- })
- .then(function(aSignedData) {
- console.log(aPublicKey, aSignedData, signatureValue);
- return verifySignature(aPublicKey, aSignedData, signatureValue);
- })
-}
-
-let rp = {id: document.domain, name: "none", icon: "none"};
-let user = {id: "none", name: "none", icon: "none", displayName: "none"};
-let param = {type: "public-key", algorithm: "ES256"};
+let gState = {};
let makeCredentialOptions = {
- rp: rp,
- user: user,
+ rp: {id: document.domain, name: "none", icon: "none"},
+ user: {id: "none", name: "none", icon: "none", displayName: "none"},
challenge: gCredentialChallenge,
- parameters: [param]
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ parameters: [{type: "public-key", algorithm: "ES256"}],
};
-let credm = navigator.credentials;
-credm.create({publicKey: makeCredentialOptions})
-.then(util_decodeCreatedCredential)
+navigator.credentials.create({publicKey: makeCredentialOptions})
+.then(function (aNewCredentialInfo) {
+ gState.credential = aNewCredentialInfo;
+
+ return webAuthnDecodeCBORAttestation(aNewCredentialInfo.response.attestationObject);
+})
.then(function testAssertion(aCredInfo) {
+ gState.publicKeyHandle = aCredInfo.authDataObj.publicKeyHandle;
+
let newCredential = {
type: "public-key",
- id: Uint8Array.from(aCredInfo.rawId),
+ id: new Uint8Array(gState.credential.rawId),
transports: ["usb"],
}
let publicKeyCredentialRequestOptions = {
challenge: gAssertionChallenge,
timeout: 5000, // the minimum timeout is actually 15 seconds
rpId: document.domain,
allowList: [newCredential]
};
- return credm.get({publicKey: publicKeyCredentialRequestOptions})
- .then(function(aAssertion) {
- /* Pass along the pubKey. */
- return util_checkAssertionAndSigValid(aCredInfo.publicKeyHandle, aAssertion);
+ return navigator.credentials.get({publicKey: publicKeyCredentialRequestOptions});
+})
+.then(function(aAssertion) {
+ let clientData = JSON.parse(buffer2string(aAssertion.response.clientDataJSON));
+
+ gState.assertion = aAssertion;
+
+ return webAuthnDecodeAuthDataArray(new Uint8Array(aAssertion.response.authenticatorData));
+})
+.then(function(aAttestationObj) {
+ gState.attestation = aAttestationObj;
+
+ // Make sure the RP ID hash matches what we calculate.
+ return crypto.subtle.digest("SHA-256", string2buffer(document.domain))
+ .then(function(calculatedRpIdHash) {
+ let calcHashStr = bytesToBase64UrlSafe(new Uint8Array(calculatedRpIdHash));
+ let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(aAttestationObj.authDataObj.rpIdHash));
+
+ if (calcHashStr != providedHashStr) {
+ return Promise.reject("Calculated RP ID hash must match what the browser derived.");
+ }
+ return Promise.resolve(aAttestationObj);
});
})
+.then(function(aAttestation) {
+ if (new Uint8Array(aAttestation.flags) != flag_TUP) {
+ return Promise.reject("Assertion's user presence byte not set correctly.");
+ }
+
+ let clientDataJSON = gState.assertion.response.clientDataJSON;
+ return deriveAppAndChallengeParam(document.domain, clientDataJSON, aAttestation);
+})
+.then(function(aParams) {
+ return assembleSignedData(aParams.appParam, aParams.attestation.flags,
+ aParams.attestation.counter, aParams.challengeParam);
+})
+.then(function(aSignedData) {
+ let signature = gState.assertion.response.signature;
+ console.log(gState.publicKeyHandle, aSignedData, signature);
+ return verifySignature(gState.publicKeyHandle, aSignedData, new Uint8Array(signature));
+})
.then(function(aSigVerifyResult) {
signalCompletion("Signing signature verified: " + aSigVerifyResult);
+ gState = {};
})
-.catch(function failure(aReason) {
+.catch(function(aReason) {
signalCompletion("Failure: " + aReason);
+ gState = {};
});
</script>
</body>
</html>
\ No newline at end of file
--- a/dom/webauthn/tests/test_webauthn_loopback.html
+++ b/dom/webauthn/tests/test_webauthn_loopback.html
@@ -48,46 +48,49 @@ function() {
- response : AuthenticatorAttestationResponse : AuthenticatorResponse
- attestationObject: CBOR object
- clientDataJSON: serialized JSON
- clientExtensionResults: (not yet supported)
*/
is(aCredInfo.type, "public-key", "Credential type must be public-key")
- ok(aCredInfo.rawId.length > 0, "Key ID exists");
+ ok(aCredInfo.rawId.byteLength > 0, "Key ID exists");
is(aCredInfo.id, bytesToBase64UrlSafe(aCredInfo.rawId), "Encoded Key ID and Raw Key ID match");
ok(aCredInfo.rawId === aCredInfo.rawId, "PublicKeyCredential.RawID is SameObject");
ok(aCredInfo.response === aCredInfo.response, "PublicKeyCredential.Response is SameObject");
ok(aCredInfo.response.clientDataJSON === aCredInfo.response.clientDataJSON, "PublicKeyCredential.Response.ClientDataJSON is SameObject");
ok(aCredInfo.response.attestationObject === aCredInfo.response.attestationObject, "PublicKeyCredential.Response.AttestationObject is SameObject");
let clientData = JSON.parse(buffer2string(aCredInfo.response.clientDataJSON));
is(clientData.challenge, bytesToBase64UrlSafe(gCredentialChallenge), "Challenge is correct");
is(clientData.origin, window.location.origin, "Origin is correct");
is(clientData.hashAlg, "SHA-256", "Hash algorithm is correct");
- return webAuthnDecodeCBORAttestation(aCredInfo.response.attestationObject.buffer)
- .then(function(decodedResult) {
+ return webAuthnDecodeCBORAttestation(aCredInfo.response.attestationObject)
+ .then(function(aAttestationObj) {
// Make sure the RP ID hash matches what we calculate.
return crypto.subtle.digest("SHA-256", string2buffer(document.domain))
- .then(function(calculatedHash) {
- is(bytesToBase64(new Uint8Array(calculatedHash)), bytesToBase64(decodedResult.rpIdHash),
+ .then(function(calculatedRpIdHash) {
+ let calcHashStr = bytesToBase64UrlSafe(new Uint8Array(calculatedRpIdHash));
+ let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(aAttestationObj.authDataObj.rpIdHash));
+
+ is(calcHashStr, providedHashStr,
"Calculated RP ID hash must match what the browser derived.");
- return Promise.resolve(decodedResult);
+ return Promise.resolve(aAttestationObj);
});
})
- .then(function(decodedResult) {
- ok(decodedResult.flags == (flag_TUP | flag_AT),
+ .then(function(aAttestationObj) {
+ ok(aAttestationObj.authDataObj.flags == (flag_TUP | flag_AT),
"User presence and Attestation Object must both be set");
aCredInfo.clientDataObj = clientData;
- aCredInfo.publicKeyHandle = decodedResult.publicKeyHandle;
- aCredInfo.attestationObject = decodedResult.attestationObject;
+ aCredInfo.publicKeyHandle = aAttestationObj.authDataObj.publicKeyHandle;
+ aCredInfo.attestationObject = aAttestationObj.authDataObj.attestationAuthData;
return aCredInfo;
});
}
function checkAssertionAndSigValid(aPublicKey, aAssertion) {
/* PublicKeyCredential : Credential
- rawId: ID of Credential from AllowList that succeeded
- id: Key Handle buffer in base64url form, should == rawId
@@ -95,33 +98,33 @@ function() {
- response : AuthenticatorAssertionResponse : AuthenticatorResponse
- clientDataJSON: serialized JSON
- authenticatorData: RP ID Hash || U2F Sign() Response
- signature: U2F Sign() Response
*/
is(aAssertion.type, "public-key", "Credential type must be public-key")
- ok(aAssertion.rawId.length > 0, "Key ID exists");
- is(aAssertion.id, bytesToBase64UrlSafe(aAssertion.rawId), "Encoded Key ID and Raw Key ID match");
+ ok(aAssertion.rawId.byteLength > 0, "Key ID exists");
+ is(aAssertion.id, bytesToBase64UrlSafe(new Uint8Array(aAssertion.rawId)), "Encoded Key ID and Raw Key ID match");
ok(aAssertion.response.authenticatorData === aAssertion.response.authenticatorData, "AuthenticatorAssertionResponse.AuthenticatorData is SameObject");
ok(aAssertion.response.signature === aAssertion.response.signature, "AuthenticatorAssertionResponse.Signature is SameObject");
- ok(aAssertion.response.authenticatorData.length > 0, "Authenticator data exists");
+ ok(aAssertion.response.authenticatorData.byteLength > 0, "Authenticator data exists");
let clientData = JSON.parse(buffer2string(aAssertion.response.clientDataJSON));
is(clientData.challenge, bytesToBase64UrlSafe(gAssertionChallenge), "Challenge is correct");
is(clientData.origin, window.location.origin, "Origin is correct");
is(clientData.hashAlg, "SHA-256", "Hash algorithm is correct");
- return webAuthnDecodeAttestation(aAssertion.response.authenticatorData)
- .then(function(decodedResult) {
- ok(decodedResult.flags == flag_TUP, "User presence must be the only flag set");
- is(decodedResult.counter.length, 4, "Counter must be 4 bytes");
- return deriveAppAndChallengeParam(window.location.host, aAssertion.response.clientDataJSON, decodedResult)
+ return webAuthnDecodeAuthDataArray(aAssertion.response.authenticatorData)
+ .then(function(aAttestation) {
+ ok(new Uint8Array(aAttestation.flags) == flag_TUP, "User presence must be the only flag set");
+ is(aAttestation.counter.byteLength, 4, "Counter must be 4 bytes");
+ return deriveAppAndChallengeParam(window.location.host, aAssertion.response.clientDataJSON, aAttestation)
})
.then(function(aParams) {
console.log(aParams);
console.log("ClientData buffer: ", hexEncode(aAssertion.response.clientDataJSON));
console.log("ClientDataHash: ", hexEncode(aParams.challengeParam));
return assembleSignedData(aParams.appParam, aParams.attestation.flags,
aParams.attestation.counter, aParams.challengeParam);
})
@@ -154,17 +157,17 @@ function() {
let rp = {id: document.domain, name: "none", icon: "none"};
let user = {id: "none", name: "none", icon: "none", displayName: "none"};
let param = {type: "public-key", algorithm: "ES256"};
let makeCredentialOptions = {
rp: rp,
user: user,
challenge: gCredentialChallenge,
parameters: [param],
- excludeList: [{type: "public-key", id: Uint8Array.from(aCredInfo.rawId),
+ excludeList: [{type: "public-key", id: new Uint8Array(aCredInfo.rawId),
transports: ["usb"]}]
};
credm.create({publicKey: makeCredentialOptions})
.then(function() {
// We should have errored here!
ok(false, "The excludeList didn't stop a duplicate being created!");
SimpleTest.finish();
})
@@ -172,17 +175,17 @@ function() {
ok(aReason.toString().startsWith("NotAllowedError"), "Expect NotAllowedError, got " + aReason);
testAssertion(aCredInfo);
});
}
function testAssertion(aCredInfo) {
let newCredential = {
type: "public-key",
- id: Uint8Array.from(aCredInfo.rawId),
+ id: new Uint8Array(aCredInfo.rawId),
transports: ["usb"],
}
let publicKeyCredentialRequestOptions = {
challenge: gAssertionChallenge,
timeout: 5000, // the minimum timeout is actually 15 seconds
rpId: document.domain,
allowList: [newCredential]
--- a/dom/webauthn/tests/test_webauthn_sameorigin.html
+++ b/dom/webauthn/tests/test_webauthn_sameorigin.html
@@ -37,17 +37,17 @@
ok(aResult.toString().startsWith("SecurityError"), "Expecting a SecurityError");
return Promise.resolve();
}
function keepThisPublicKeyCredential(aIdentifier) {
return function(aPublicKeyCredential) {
gTrackedCredential[aIdentifier] = {
type: "public-key",
- id: Uint8Array.from(aPublicKeyCredential.rawId),
+ id: new Uint8Array(aPublicKeyCredential.rawId),
transports: [ "usb" ],
}
return Promise.resolve(aPublicKeyCredential);
}
}
function runTests() {
is(navigator.authentication, undefined, "navigator.authentication does not exist any longer");
--- a/dom/webauthn/tests/u2futil.js
+++ b/dom/webauthn/tests/u2futil.js
@@ -77,25 +77,29 @@ function local_finished() {
}
function string2buffer(str) {
return (new Uint8Array(str.length)).map((x, i) => str.charCodeAt(i));
}
function buffer2string(buf) {
let str = "";
- buf.map(x => str += String.fromCharCode(x));
+ if (!(buf.constructor === Uint8Array)) {
+ buf = new Uint8Array(buf);
+ }
+ buf.map(function(x){ return str += String.fromCharCode(x) });
return str;
}
function bytesToBase64(u8a){
let CHUNK_SZ = 0x8000;
let c = [];
- for (let i = 0; i < u8a.length; i += CHUNK_SZ) {
- c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ)));
+ let array = new Uint8Array(u8a);
+ for (let i = 0; i < array.length; i += CHUNK_SZ) {
+ c.push(String.fromCharCode.apply(null, array.subarray(i, i + CHUNK_SZ)));
}
return window.btoa(c.join(""));
}
function base64ToBytes(b64encoded) {
return new Uint8Array(window.atob(b64encoded).split("").map(function(c) {
return c.charCodeAt(0);
}));
@@ -129,66 +133,66 @@ function hexEncode(buf) {
function hexDecode(str) {
return new Uint8Array(str.match(/../g).map(x => parseInt(x, 16)));
}
function webAuthnDecodeCBORAttestation(aCborAttBuf) {
let attObj = CBOR.decode(aCborAttBuf);
console.log(":: Attestation CBOR Object ::");
if (!("authData" in attObj && "fmt" in attObj && "attStmt" in attObj)) {
- throw "Invalid CBOR Attestation Object";
+ return Promise.reject("Invalid CBOR Attestation Object");
}
if (!("sig" in attObj.attStmt && "x5c" in attObj.attStmt)) {
- throw "Invalid CBOR Attestation Statement";
+ return Promise.reject("Invalid CBOR Attestation Statement");
}
- return webAuthnDecodeAttestation(attObj.authData)
- .then(function (aAttestationObj) {
- aAttestationObj.attestationObject = attObj;
- return Promise.resolve(aAttestationObj);
+ return webAuthnDecodeAuthDataArray(new Uint8Array(attObj.authData))
+ .then(function (aAuthDataObj) {
+ attObj.authDataObj = aAuthDataObj;
+ return Promise.resolve(attObj);
});
}
-function webAuthnDecodeAttestation(aAuthData) {
+function webAuthnDecodeAuthDataArray(aAuthData) {
let rpIdHash = aAuthData.slice(0, 32);
let flags = aAuthData.slice(32, 33);
let counter = aAuthData.slice(33, 37);
- console.log(":: Attestation Object Data ::");
+ console.log(":: Authenticator Data ::");
console.log("RP ID Hash: " + hexEncode(rpIdHash));
console.log("Counter: " + hexEncode(counter) + " Flags: " + flags);
if ((flags & flag_AT) == 0x00) {
// No Attestation Data, so we're done.
return Promise.resolve({
rpIdHash: rpIdHash,
flags: flags,
counter: counter,
});
}
if (aAuthData.length < 38) {
- throw "Attestation Data flag was set, but not enough data passed in!";
+ return Promise.reject("Authenticator Data flag was set, but not enough data passed in!");
}
let attData = {};
attData.aaguid = aAuthData.slice(37, 53);
attData.credIdLen = (aAuthData[53] << 8) + aAuthData[54];
attData.credId = aAuthData.slice(55, 55 + attData.credIdLen);
- console.log(":: Attestation Data ::");
+ console.log(":: Authenticator Data ::");
console.log("AAGUID: " + hexEncode(attData.aaguid));
cborPubKey = aAuthData.slice(55 + attData.credIdLen);
var pubkeyObj = CBOR.decode(cborPubKey.buffer);
if (!("alg" in pubkeyObj && "x" in pubkeyObj && "y" in pubkeyObj)) {
- throw "Invalid CBOR Public Key Object";
+ return Promise.reject("Invalid CBOR Public Key Object");
}
if (pubkeyObj.alg != "ES256") {
- throw "Unexpected public key algorithm";
+ return Promise.reject("Unexpected public key algorithm");
}
let pubKeyBytes = assemblePublicKeyBytesData(pubkeyObj.x, pubkeyObj.y);
console.log(":: CBOR Public Key Object Data ::");
console.log("Algorithm: " + pubkeyObj.alg);
console.log("X: " + pubkeyObj.x);
console.log("Y: " + pubkeyObj.y);
console.log("Uncompressed (hex): " + hexEncode(pubKeyBytes));
@@ -244,47 +248,52 @@ function assemblePublicKeyBytesData(xCoo
keyBytes[0] = 0x04;
xCoord.map((x, i) => keyBytes[1 + i] = x);
yCoord.map((x, i) => keyBytes[33 + i] = x);
return keyBytes;
}
function assembleSignedData(appParam, flags, counter, challengeParam) {
let signedData = new Uint8Array(32 + 1 + 4 + 32);
- appParam.map((x, i) => signedData[0 + i] = x);
- signedData[32] = flags;
- counter.map((x, i) => signedData[33 + i] = x);
- challengeParam.map((x, i) => signedData[37 + i] = x);
+ new Uint8Array(appParam).map((x, i) => signedData[0 + i] = x);
+ signedData[32] = new Uint8Array(flags)[0];
+ new Uint8Array(counter).map((x, i) => signedData[33 + i] = x);
+ new Uint8Array(challengeParam).map((x, i) => signedData[37 + i] = x);
return signedData;
}
function assembleRegistrationSignedData(appParam, challengeParam, keyHandle, pubKey) {
let signedData = new Uint8Array(1 + 32 + 32 + keyHandle.length + 65);
signedData[0] = 0x00;
- appParam.map((x, i) => signedData[1 + i] = x);
- challengeParam.map((x, i) => signedData[33 + i] = x);
- keyHandle.map((x, i) => signedData[65 + i] = x);
- pubKey.map((x, i) => signedData[65 + keyHandle.length + i] = x);
+ new Uint8Array(appParam).map((x, i) => signedData[1 + i] = x);
+ new Uint8Array(challengeParam).map((x, i) => signedData[33 + i] = x);
+ new Uint8Array(keyHandle).map((x, i) => signedData[65 + i] = x);
+ new Uint8Array(pubKey).map((x, i) => signedData[65 + keyHandle.length + i] = x);
return signedData;
}
function sanitizeSigArray(arr) {
// ECDSA signature fields into WebCrypto must be exactly 32 bytes long, so
// this method strips leading padding bytes, if added, and also appends
// padding zeros, if needed.
if (arr.length > 32) {
arr = arr.slice(arr.length - 32)
}
let ret = new Uint8Array(32);
ret.set(arr, ret.length - arr.length);
return ret;
}
function verifySignature(key, data, derSig) {
- let sigAsn1 = org.pkijs.fromBER(derSig.buffer);
+ if (derSig.byteLength < 70) {
+ console.log("bad sig: " + hexEncode(new Uint8Array(derSig)))
+ return Promise.reject("Invalid signature length: " + derSig.byteLength);
+ }
+
+ let sigAsn1 = org.pkijs.fromBER(derSig);
let sigR = new Uint8Array(sigAsn1.result.value_block.value[0].value_block.value_hex);
let sigS = new Uint8Array(sigAsn1.result.value_block.value[1].value_block.value_hex);
// The resulting R and S values from the ASN.1 Sequence must be fit into 32
// bytes. Sometimes they have leading zeros, sometimes they're too short, it
// all depends on what lib generated the signature.
let R = sanitizeSigArray(sigR);
let S = sanitizeSigArray(sigS);