Bug 1387820 - WebAuthn WD-05 Get Assertion Data Fix. r=keeler, a=lizzard
authorJ.C. Jones <jjones@mozilla.com>
Wed, 09 Aug 2017 20:05:23 -0700
changeset 423591 ddc8a44892574b4cc02d044693e7a4c918b4fd14
parent 423590 2da5df1a3bdf41e8e2f9645f8a9e1761b944fdd0
child 423592 0c44b874c9f5de687e4f3bb72c60da65d6c5c5c8
push id1517
push userjlorenzo@mozilla.com
push dateThu, 14 Sep 2017 16:50:54 +0000
treeherdermozilla-release@3b41fd564418 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskeeler, lizzard
bugs1387820, 20170505
milestone56.0
Bug 1387820 - WebAuthn WD-05 Get Assertion Data Fix. r=keeler, a=lizzard 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
@@ -724,67 +724,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;
   }
 
@@ -808,37 +796,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);
@@ -856,17 +857,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);