Bug 1387820 - WebAuthn WD-05 Get Assertion Data Fix r=keeler
authorJ.C. Jones <jjones@mozilla.com>
Wed, 09 Aug 2017 20:05:23 -0700
changeset 374432 d95683eef9ba92f79901d099631fcd2b733d4c15
parent 374431 e95831a33a2ef6dd321fe896ac86c0e0bcab1e32
child 374433 c73631b194bbedf4a20a7dcb9c648b25d178e7c5
push id93678
push userarchaeopteryx@coole-files.de
push dateSat, 12 Aug 2017 23:17:05 +0000
treeherdermozilla-inbound@a79ccbfacad8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskeeler
bugs1387820, 20170505
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1387820 - WebAuthn WD-05 Get Assertion Data Fix r=keeler The WebAuthn WD-05 specification's Get Assertion method defines the returned AuthenticatorAssertionResponse as providing ClientData, AuthenticatorData, and the Signature from the Authenticator. Our implementation is incorrectly setting AuthenticatorData and Signature: AuthenticatorData as a structure is intended to mirror the structure from the AuthenticatorData [1] section of the Attestation CBOR Object [2] in the MakeCredential method, which we weren't doing _at all_. This is clarified in the editor's draft of the specification, soon to be WD-06. Signature for U2F Authenticators is defined as the "attestation signature", [3] which is under-specified and we assumed would be the raw output from the U2F Authenticator [4]. This should instead be the raw ANSI X9.62 signature with no additional bytes. [5] [1] https://www.w3.org/TR/2017/WD-webauthn-20170505/#sec-authenticator-data [2] https://www.w3.org/TR/2017/WD-webauthn-20170505/#sec-attestation-data [3] https://www.w3.org/TR/2017/WD-webauthn-20170505/#fido-u2f-attestation [4] https://lists.w3.org/Archives/Public/public-webauthn/2017Aug/0078.html [5] https://bugzilla.mozilla.org/show_bug.cgi?id=1387820#c4 MozReview-Commit-ID: DTIOILfS4pK
dom/webauthn/WebAuthnManager.cpp
dom/webauthn/WebAuthnUtil.cpp
dom/webauthn/WebAuthnUtil.h
dom/webauthn/tests/test_webauthn_loopback.html
dom/webauthn/tests/u2futil.js
--- a/dom/webauthn/WebAuthnManager.cpp
+++ b/dom/webauthn/WebAuthnManager.cpp
@@ -780,67 +780,55 @@ WebAuthnManager::FinishMakeCredential(ns
   }
 
   CryptoBuffer rpIdHashBuf;
   if (!rpIdHashBuf.Assign(mInfo.ref().RpIdHash())) {
     Cancel(NS_ERROR_OUT_OF_MEMORY);
     return;
   }
 
-  CryptoBuffer authenticatorDataBuf;
-  rv = U2FAssembleAuthenticatorData(authenticatorDataBuf, rpIdHashBuf,
-                                    signatureBuf);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    Cancel(NS_ERROR_OUT_OF_MEMORY);
-    return;
-  }
-
   // Construct the public key object
   CryptoBuffer pubKeyObj;
   rv = CBOREncodePublicKeyObj(pubKeyBuf, pubKeyObj);
   if (NS_FAILED(rv)) {
     Cancel(rv);
     return;
   }
 
-  // Format:
-  // 32 bytes: SHA256 of the RP ID
-  // 1 byte: flags (TUP & AT)
-  // 4 bytes: sign counter
-  // variable: attestation data struct
-  // - 16 bytes: AAGUID
-  // - 2 bytes: Length of Credential ID
-  // - L bytes: Credential ID
-  // - variable: CBOR-format public key
-  // variable: CBOR-format extension auth data (optional, not flagged)
-
-  mozilla::dom::CryptoBuffer authDataBuf;
-  if (NS_WARN_IF(!authDataBuf.SetCapacity(32 + 1 + 4 + aaguidBuf.Length() + 2 +
-                                          keyHandleBuf.Length() +
-                                          pubKeyObj.Length(),
-                                          mozilla::fallible))) {
+  // During create credential, counter is always 0 for U2F
+  // See https://github.com/w3c/webauthn/issues/507
+  mozilla::dom::CryptoBuffer counterBuf;
+  if (NS_WARN_IF(!counterBuf.SetCapacity(4, mozilla::fallible))) {
     Cancel(NS_ERROR_OUT_OF_MEMORY);
     return;
   }
+  counterBuf.AppendElement(0x00, mozilla::fallible);
+  counterBuf.AppendElement(0x00, mozilla::fallible);
+  counterBuf.AppendElement(0x00, mozilla::fallible);
+  counterBuf.AppendElement(0x00, mozilla::fallible);
 
-  authDataBuf.AppendElements(rpIdHashBuf, mozilla::fallible);
-  authDataBuf.AppendElement(FLAG_TUP | FLAG_AT, mozilla::fallible);
-  // During create credential, counter is always 0 for U2F
-  // See https://github.com/w3c/webauthn/issues/507
-  authDataBuf.AppendElement(0x00, mozilla::fallible);
-  authDataBuf.AppendElement(0x00, mozilla::fallible);
-  authDataBuf.AppendElement(0x00, mozilla::fallible);
-  authDataBuf.AppendElement(0x00, mozilla::fallible);
+  // Construct the Attestation Data, which slots into the end of the
+  // Authentication Data buffer.
+  CryptoBuffer attDataBuf;
+  rv = AssembleAttestationData(aaguidBuf, keyHandleBuf, pubKeyObj, attDataBuf);
+  if (NS_FAILED(rv)) {
+    Cancel(rv);
+    return;
+  }
 
-  authDataBuf.AppendElements(aaguidBuf, mozilla::fallible);
-  authDataBuf.AppendElement((keyHandleBuf.Length() >> 8) & 0xFF, mozilla::fallible);
-  authDataBuf.AppendElement((keyHandleBuf.Length() >> 0) & 0xFF, mozilla::fallible);
-  authDataBuf.AppendElements(keyHandleBuf, mozilla::fallible);
-  authDataBuf.AppendElements(pubKeyObj, mozilla::fallible);
+  mozilla::dom::CryptoBuffer authDataBuf;
+  rv = AssembleAuthenticatorData(rpIdHashBuf, FLAG_TUP, counterBuf, attDataBuf,
+                                 authDataBuf);
+  if (NS_FAILED(rv)) {
+    Cancel(rv);
+    return;
+  }
 
+  // The Authentication Data buffer gets CBOR-encoded with the Cert and
+  // Signature to build the Attestation Object.
   CryptoBuffer attObj;
   rv = CBOREncodeAttestationObj(authDataBuf, attestationCertBuf, signatureBuf,
                                 attObj);
   if (NS_FAILED(rv)) {
     Cancel(rv);
     return;
   }
 
@@ -864,37 +852,50 @@ WebAuthnManager::FinishMakeCredential(ns
 
 void
 WebAuthnManager::FinishGetAssertion(nsTArray<uint8_t>& aCredentialId,
                                     nsTArray<uint8_t>& aSigBuffer)
 {
   MOZ_ASSERT(mTransactionPromise);
   MOZ_ASSERT(mInfo.isSome());
 
-  CryptoBuffer signatureData;
-  if (NS_WARN_IF(!signatureData.Assign(aSigBuffer.Elements(), aSigBuffer.Length()))) {
+  CryptoBuffer tokenSignatureData;
+  if (NS_WARN_IF(!tokenSignatureData.Assign(aSigBuffer.Elements(),
+                                            aSigBuffer.Length()))) {
     Cancel(NS_ERROR_OUT_OF_MEMORY);
     return;
   }
 
   CryptoBuffer clientDataBuf;
   if (!clientDataBuf.Assign(mClientData.ref())) {
     Cancel(NS_ERROR_OUT_OF_MEMORY);
     return;
   }
 
   CryptoBuffer rpIdHashBuf;
   if (!rpIdHashBuf.Assign(mInfo.ref().RpIdHash())) {
     Cancel(NS_ERROR_OUT_OF_MEMORY);
     return;
   }
 
+  CryptoBuffer signatureBuf;
+  CryptoBuffer counterBuf;
+  uint8_t flags = 0;
+  nsresult rv = U2FDecomposeSignResponse(tokenSignatureData, flags, counterBuf,
+                                         signatureBuf);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    Cancel(rv);
+    return;
+  }
+
+  CryptoBuffer attestationDataBuf;
   CryptoBuffer authenticatorDataBuf;
-  nsresult rv = U2FAssembleAuthenticatorData(authenticatorDataBuf, rpIdHashBuf,
-                                             signatureData);
+  rv = AssembleAuthenticatorData(rpIdHashBuf, FLAG_TUP, counterBuf,
+                                 /* deliberately empty */ attestationDataBuf,
+                                 authenticatorDataBuf);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     Cancel(rv);
     return;
   }
 
   CryptoBuffer credentialBuf;
   if (!credentialBuf.Assign(aCredentialId)) {
     Cancel(NS_ERROR_OUT_OF_MEMORY);
@@ -912,17 +913,17 @@ WebAuthnManager::FinishGetAssertion(nsTA
 
   // Create a new PublicKeyCredential object named value and populate its fields
   // with the values returned from the authenticator as well as the
   // clientDataJSON computed earlier.
   RefPtr<AuthenticatorAssertionResponse> assertion =
     new AuthenticatorAssertionResponse(mCurrentParent);
   assertion->SetClientDataJSON(clientDataBuf);
   assertion->SetAuthenticatorData(authenticatorDataBuf);
-  assertion->SetSignature(signatureData);
+  assertion->SetSignature(signatureBuf);
 
   RefPtr<PublicKeyCredential> credential =
     new PublicKeyCredential(mCurrentParent);
   credential->SetId(credentialBase64Url);
   credential->SetType(NS_LITERAL_STRING("public-key"));
   credential->SetRawId(credentialBuf);
   credential->SetResponse(assertion);
 
--- a/dom/webauthn/WebAuthnUtil.cpp
+++ b/dom/webauthn/WebAuthnUtil.cpp
@@ -30,32 +30,100 @@ ReadToCryptoBuffer(pkix::Reader& aSrc, /
     if (!aDest.AppendElement(b, mozilla::fallible)) {
       return NS_ERROR_OUT_OF_MEMORY;
     }
   }
 
   return NS_OK;
 }
 
+// Format:
+// 32 bytes: SHA256 of the RP ID
+// 1 byte: flags (TUP & AT)
+// 4 bytes: sign counter
+// variable: attestation data struct
+// variable: CBOR-format extension auth data (optional, not flagged)
 nsresult
-U2FAssembleAuthenticatorData(/* out */ CryptoBuffer& aAuthenticatorData,
-                             const CryptoBuffer& aRpIdHash,
-                             const CryptoBuffer& aSignatureData)
+AssembleAuthenticatorData(const CryptoBuffer& rpIdHashBuf,
+                          const uint8_t flags,
+                          const CryptoBuffer& counterBuf,
+                          const CryptoBuffer& attestationDataBuf,
+                          /* out */ CryptoBuffer& authDataBuf)
 {
-  // The AuthenticatorData for U2F devices is the concatenation of the
-  // RP ID with the output of the U2F Sign operation.
-  if (aRpIdHash.Length() != 32) {
+  if (NS_WARN_IF(!authDataBuf.SetCapacity(32 + 1 + 4 + attestationDataBuf.Length(),
+                                          mozilla::fallible))) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+  if (rpIdHashBuf.Length() != 32 || counterBuf.Length() != 4) {
     return NS_ERROR_INVALID_ARG;
   }
 
-  if (!aAuthenticatorData.AppendElements(aRpIdHash, mozilla::fallible)) {
+  uint8_t flagSet = flags;
+  if (!attestationDataBuf.IsEmpty()) {
+    flagSet |= FLAG_AT;
+  }
+
+  authDataBuf.AppendElements(rpIdHashBuf, mozilla::fallible);
+  authDataBuf.AppendElement(flagSet, mozilla::fallible);
+  authDataBuf.AppendElements(counterBuf, mozilla::fallible);
+  authDataBuf.AppendElements(attestationDataBuf, mozilla::fallible);
+  return NS_OK;
+}
+
+// attestation data struct format:
+// - 16 bytes: AAGUID
+// - 2 bytes: Length of Credential ID
+// - L bytes: Credential ID
+// - variable: CBOR-format public key
+nsresult
+AssembleAttestationData(const CryptoBuffer& aaguidBuf,
+                        const CryptoBuffer& keyHandleBuf,
+                        const CryptoBuffer& pubKeyObj,
+                        /* out */ CryptoBuffer& attestationDataBuf)
+{
+  if (NS_WARN_IF(!attestationDataBuf.SetCapacity(aaguidBuf.Length() + 2 +
+                                                 keyHandleBuf.Length() +
+                                                 pubKeyObj.Length(),
+                                                 mozilla::fallible))) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+  if (keyHandleBuf.Length() > 0xFFFF) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  attestationDataBuf.AppendElements(aaguidBuf, mozilla::fallible);
+  attestationDataBuf.AppendElement((keyHandleBuf.Length() >> 8) & 0xFF,
+                                   mozilla::fallible);
+  attestationDataBuf.AppendElement((keyHandleBuf.Length() >> 0) & 0xFF,
+                                   mozilla::fallible);
+  attestationDataBuf.AppendElements(keyHandleBuf, mozilla::fallible);
+  attestationDataBuf.AppendElements(pubKeyObj, mozilla::fallible);
+  return NS_OK;
+}
+
+nsresult
+U2FDecomposeSignResponse(const CryptoBuffer& aResponse,
+                         /* out */ uint8_t& aFlags,
+                         /* out */ CryptoBuffer& aCounterBuf,
+                         /* out */ CryptoBuffer& aSignatureBuf)
+{
+  if (aResponse.Length() < 5) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  Span<const uint8_t> rspView = MakeSpan(aResponse);
+  aFlags = rspView[0];
+
+  if (NS_WARN_IF(!aCounterBuf.AppendElements(rspView.FromTo(1, 5),
+                                             mozilla::fallible))) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
-  if (!aAuthenticatorData.AppendElements(aSignatureData, mozilla::fallible)) {
+  if (NS_WARN_IF(!aSignatureBuf.AppendElements(rspView.From(5),
+                                               mozilla::fallible))) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
   return NS_OK;
 }
 
 nsresult
 U2FDecomposeRegistrationResponse(const CryptoBuffer& aResponse,
--- a/dom/webauthn/WebAuthnUtil.h
+++ b/dom/webauthn/WebAuthnUtil.h
@@ -12,19 +12,33 @@
  */
 
 #include "mozilla/dom/CryptoBuffer.h"
 #include "pkix/Input.h"
 
 namespace mozilla {
 namespace dom {
 nsresult
-U2FAssembleAuthenticatorData(/* out */ CryptoBuffer& aAuthenticatorData,
-                             const CryptoBuffer& aRpIdHash,
-                             const CryptoBuffer& aSignatureData);
+AssembleAuthenticatorData(const CryptoBuffer& rpIdHashBuf,
+                          const uint8_t flags,
+                          const CryptoBuffer& counterBuf,
+                          const CryptoBuffer& attestationDataBuf,
+                          /* out */ CryptoBuffer& authDataBuf);
+
+nsresult
+AssembleAttestationData(const CryptoBuffer& aaguidBuf,
+                        const CryptoBuffer& keyHandleBuf,
+                        const CryptoBuffer& pubKeyObj,
+                        /* out */ CryptoBuffer& attestationDataBuf);
+
+nsresult
+U2FDecomposeSignResponse(const CryptoBuffer& aResponse,
+                         /* out */ uint8_t& aFlags,
+                         /* out */ CryptoBuffer& aCounterBuf,
+                         /* out */ CryptoBuffer& aSignatureBuf);
 
 nsresult
 U2FDecomposeRegistrationResponse(const CryptoBuffer& aResponse,
                                  /* out */ CryptoBuffer& aPubKeyBuf,
                                  /* out */ CryptoBuffer& aKeyHandleBuf,
                                  /* out */ CryptoBuffer& aAttestationCertBuf,
                                  /* out */ CryptoBuffer& aSignatureBuf);
 
--- a/dom/webauthn/tests/test_webauthn_loopback.html
+++ b/dom/webauthn/tests/test_webauthn_loopback.html
@@ -58,18 +58,20 @@ function() {
 
     let clientData = JSON.parse(buffer2string(aCredInfo.response.clientDataJSON));
     is(clientData.challenge, bytesToBase64UrlSafe(gCredentialChallenge), "Challenge is correct");
     // WD-05 vs. WD-06: In WD-06, the second parameter should be "window.location.origin". Fix
     // this in Bug 1384776
     is(clientData.origin, document.domain, "Origin is correct");
     is(clientData.hashAlg, "S256", "Hash algorithm is correct");
 
-    return webAuthnDecodeAttestation(aCredInfo.response.attestationObject.buffer)
+    return webAuthnDecodeCBORAttestation(aCredInfo.response.attestationObject.buffer)
     .then(function(decodedResult) {
+      ok(decodedResult.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;
       return aCredInfo;
     });
   }
 
   function checkAssertionAndSigValid(aPublicKey, aAssertion) {
@@ -91,36 +93,32 @@ function() {
     ok(aAssertion.response.authenticatorData.length > 0, "Authenticator data exists");
     let clientData = JSON.parse(buffer2string(aAssertion.response.clientDataJSON));
     is(clientData.challenge, bytesToBase64UrlSafe(gAssertionChallenge), "Challenge is correct");
     // WD-05 vs. WD-06: In WD-06, the second parameter should be "window.location.origin". Fix
     // this in Bug 1384776
     is(clientData.origin, document.domain, "Origin is correct");
     is(clientData.hashAlg, "S256", "Hash algorithm is correct");
 
-    // 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)
+    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)
+    })
     .then(function(aParams) {
-      console.log(aParams.appParam, rpIdHash, presenceAndCounter, aParams.challengeParam);
+      console.log(aParams);
       console.log("ClientData buffer: ", hexEncode(aAssertion.response.clientDataJSON));
       console.log("ClientDataHash: ", hexEncode(aParams.challengeParam));
-      return assembleSignedData(aParams.appParam, presenceAndCounter, aParams.challengeParam);
+      return assembleSignedData(aParams.appParam, aParams.attestation.flags,
+                                aParams.attestation.counter, aParams.challengeParam);
     })
     .then(function(aSignedData) {
-      console.log(aPublicKey, aSignedData, signatureValue);
-      return verifySignature(aPublicKey, aSignedData, signatureValue);
+      console.log(aPublicKey, aSignedData, aAssertion.response.signature);
+      return verifySignature(aPublicKey, aSignedData, aAssertion.response.signature);
     })
   }
 
   function testMakeCredential() {
     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 = {
--- a/dom/webauthn/tests/u2futil.js
+++ b/dom/webauthn/tests/u2futil.js
@@ -1,12 +1,15 @@
 // Used by local_addTest() / local_completeTest()
 var _countCompletions = 0;
 var _expectedCompletions = 0;
 
+const flag_TUP = 0x01;
+const flag_AT = 0x40;
+
 function handleEventMessage(event) {
   if ("test" in event.data) {
     let summary = event.data.test + ": " + event.data.msg;
     log(event.data.status + ": " + summary);
     ok(event.data.status, summary);
   } else if ("done" in event.data) {
     SimpleTest.finish();
   } else {
@@ -122,40 +125,64 @@ function hexEncode(buf) {
               .map(x => ("0"+x.toString(16)).substr(-2))
               .join("");
 }
 
 function hexDecode(str) {
   return new Uint8Array(str.match(/../g).map(x => parseInt(x, 16)));
 }
 
-function webAuthnDecodeAttestation(aAttestationBuf) {
-  let attObj = CBOR.decode(aAttestationBuf);
-  console.log("Attestation CBOR Object:", attObj);
+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";
   }
   if (!("sig" in attObj.attStmt && "x5c" in attObj.attStmt)) {
     throw "Invalid CBOR Attestation Statement";
   }
 
-  let rpIdHash = attObj.authData.slice(0, 32);
-  let flags = attObj.authData.slice(32, 33);
-  let counter = attObj.authData.slice(33, 37);
-  let attData = {};
-  attData.aaguid = attObj.authData.slice(37, 53);
-  attData.credIdLen = (attObj.authData[53] << 8) + attObj.authData[54];
-  attData.credId = attObj.authData.slice(55, 55 + attData.credIdLen);
+  return webAuthnDecodeAttestation(attObj.authData)
+  .then(function (aAttestationObj) {
+    aAttestationObj.attestationObject = attObj;
+    return Promise.resolve(aAttestationObj);
+  });
+}
 
-  console.log(":: CBOR Attestation Object Data ::");
+function webAuthnDecodeAttestation(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("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!";
+  }
+
+  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("AAGUID: " + hexEncode(attData.aaguid));
 
-  cborPubKey = attObj.authData.slice(55 + attData.credIdLen);
+  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";
   }
   if (pubkeyObj.alg != "ES256") {
     throw "Unexpected public key algorithm";
   }
 
@@ -163,48 +190,51 @@ function webAuthnDecodeAttestation(aAtte
   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));
 
   return importPublicKey(pubKeyBytes)
   .then(function(aKeyHandle) {
-    return {
-      attestationObject: attObj,
+    return Promise.resolve({
+      rpIdHash: rpIdHash,
+      flags: flags,
+      counter: counter,
       attestationAuthData: attData,
       publicKeyBytes: pubKeyBytes,
       publicKeyHandle: aKeyHandle,
-    };
+    });
   });
 }
 
 function importPublicKey(keyBytes) {
   if (keyBytes[0] != 0x04 || keyBytes.byteLength != 65) {
     throw "Bad public key octet string";
   }
   var jwk = {
     kty: "EC",
     crv: "P-256",
     x: bytesToBase64UrlSafe(keyBytes.slice(1, 33)),
     y: bytesToBase64UrlSafe(keyBytes.slice(33))
   };
   return crypto.subtle.importKey("jwk", jwk, {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"])
 }
 
-function deriveAppAndChallengeParam(appId, clientData) {
+function deriveAppAndChallengeParam(appId, clientData, attestation) {
   var appIdBuf = string2buffer(appId);
   return Promise.all([
     crypto.subtle.digest("SHA-256", appIdBuf),
     crypto.subtle.digest("SHA-256", clientData)
   ])
   .then(function(digests) {
     return {
       appParam: new Uint8Array(digests[0]),
       challengeParam: new Uint8Array(digests[1]),
+      attestation: attestation
     };
   });
 }
 
 function assemblePublicKeyBytesData(xCoord, yCoord) {
   // Produce an uncompressed EC key point. These start with 0x04, and then
   // two 32-byte numbers denoting X and Y.
   if (xCoord.length != 32 || yCoord.length != 32) {
@@ -212,20 +242,21 @@ function assemblePublicKeyBytesData(xCoo
   }
   let keyBytes = new Uint8Array(65);
   keyBytes[0] = 0x04;
   xCoord.map((x, i) => keyBytes[1 + i] = x);
   yCoord.map((x, i) => keyBytes[33 + i] = x);
   return keyBytes;
 }
 
-function assembleSignedData(appParam, presenceAndCounter, challengeParam) {
+function assembleSignedData(appParam, flags, counter, challengeParam) {
   let signedData = new Uint8Array(32 + 1 + 4 + 32);
   appParam.map((x, i) => signedData[0 + i] = x);
-  presenceAndCounter.map((x, i) => signedData[32 + i] = x);
+  signedData[32] = flags;
+  counter.map((x, i) => signedData[33 + i] = x);
   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);