Bug 1381190 - Change to COSE Algorithm identifiers for WebAuthn r?ttaubert draft
authorJ.C. Jones <jjones@mozilla.com>
Thu, 12 Oct 2017 15:21:06 -0700
changeset 681137 4d1ab816d3196472ddd8bf7e12cfe4e3d94f8537
parent 679624 8a99e4686f4df575d15c28a68f97ef05a73d740c
child 736098 42a4e453bd83cdcd18e91001f6ac4cda9d7ac306
push id84769
push userbmo:jjones@mozilla.com
push dateTue, 17 Oct 2017 01:45:45 +0000
reviewersttaubert
bugs1381190, 1409220
milestone58.0a1
Bug 1381190 - Change to COSE Algorithm identifiers for WebAuthn r?ttaubert The WD-06 (and later) WebAuthn specs choose to move to integer algorithm identifiers for the signatures [1], with a handful of algorithms identified [2]. U2F devices only support ES256 (e.g., COSE ID "-7"), so that's all that is implemented here. Note that the spec also now requires that we accept empty lists of parameters, and in that case, the RP says they aren't picky, so this changes what happens when the parameter list is empty (but still aborts when the list is non-empty but doesn't have anything we can use) [3]. There's a follow-on to move parameter-validation logic into the U2FTokenManager in Bug 1409220. [1] https://w3c.github.io/webauthn/#dictdef-publickeycredentialparameters [2] https://w3c.github.io/webauthn/#alg-identifier [3] https://w3c.github.io/webauthn/#createCredential bullet #12 MozReview-Commit-ID: KgL7mQ9u1uq
dom/webauthn/WebAuthnCoseIdentifiers.h
dom/webauthn/WebAuthnManager.cpp
dom/webauthn/tests/browser/tab_webauthn_success.html
dom/webauthn/tests/test_webauthn_loopback.html
dom/webauthn/tests/test_webauthn_make_credential.html
dom/webauthn/tests/test_webauthn_no_token.html
dom/webauthn/tests/test_webauthn_sameorigin.html
dom/webauthn/tests/u2futil.js
dom/webidl/WebAuthentication.webidl
new file mode 100644
--- /dev/null
+++ b/dom/webauthn/WebAuthnCoseIdentifiers.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_WebAuthnCoseIdentifiers_h
+#define mozilla_dom_WebAuthnCoseIdentifiers_h
+
+#include "mozilla/dom/WebCryptoCommon.h"
+
+namespace mozilla {
+namespace dom {
+
+// From https://www.iana.org/assignments/cose/cose.xhtml#algorithms
+enum class CoseAlgorithmIdentifier : int32_t {
+  ES256 = -7
+};
+
+static nsresult
+CoseAlgorithmToWebCryptoId(const int32_t& aId, /* out */ nsString& aName)
+{
+  switch(static_cast<CoseAlgorithmIdentifier>(aId)) {
+    case CoseAlgorithmIdentifier::ES256:
+      aName.AssignLiteral(JWK_ALG_ECDSA_P_256);
+      break;
+    default:
+      return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+  }
+  return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_WebAuthnCoseIdentifiers_h
--- a/dom/webauthn/WebAuthnManager.cpp
+++ b/dom/webauthn/WebAuthnManager.cpp
@@ -3,26 +3,26 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "hasht.h"
 #include "nsICryptoHash.h"
 #include "nsNetCID.h"
 #include "nsThreadUtils.h"
+#include "WebAuthnCoseIdentifiers.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/dom/AuthenticatorAttestationResponse.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PWebAuthnTransaction.h"
 #include "mozilla/dom/U2FUtil.h"
 #include "mozilla/dom/WebAuthnCBORUtil.h"
 #include "mozilla/dom/WebAuthnManager.h"
 #include "mozilla/dom/WebAuthnTransactionChild.h"
 #include "mozilla/dom/WebAuthnUtil.h"
-#include "mozilla/dom/WebCryptoCommon.h"
 #include "mozilla/ipc/BackgroundChild.h"
 #include "mozilla/ipc/PBackgroundChild.h"
 
 using namespace mozilla::ipc;
 
 namespace mozilla {
 namespace dom {
 
@@ -48,38 +48,16 @@ NS_NAMED_LITERAL_STRING(kVisibilityChang
 
 NS_IMPL_ISUPPORTS(WebAuthnManager, nsIIPCBackgroundChildCreateCallback,
                   nsIDOMEventListener);
 
 /***********************************************************************
  * Utility Functions
  **********************************************************************/
 
-template<class OOS>
-static nsresult
-GetAlgorithmName(const OOS& aAlgorithm,
-                 /* out */ nsString& aName)
-{
-  if (aAlgorithm.IsString()) {
-    // If string, then treat as algorithm name
-    aName.Assign(aAlgorithm.GetAsString());
-  } else {
-    // TODO: Coerce to string and extract name. See WebCryptoTask.cpp
-  }
-
-  // Only ES256 is currently supported
-  if (NORMALIZED_EQUALS(aName, JWK_ALG_ECDSA_P_256)) {
-    aName.AssignLiteral(JWK_ALG_ECDSA_P_256);
-  } else {
-    return NS_ERROR_DOM_SYNTAX_ERR;
-  }
-
-  return NS_OK;
-}
-
 static nsresult
 AssembleClientData(const nsAString& aOrigin, const CryptoBuffer& aChallenge,
                    /* out */ nsACString& aJsonOut)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   nsString challengeBase64;
   nsresult rv = aChallenge.ToJwkBase64(challengeBase64);
@@ -357,85 +335,50 @@ WebAuthnManager::MakeCredential(nsPIDOMW
   }
 
   srv = HashCString(hashService, rpId, rpIdHash);
   if (NS_WARN_IF(NS_FAILED(srv))) {
     promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
     return promise.forget();
   }
 
-  // Process each element of cryptoParameters using the following steps, to
-  // produce a new sequence normalizedParameters.
-  nsTArray<PublicKeyCredentialParameters> normalizedParams;
+
+  // TODO: Move this logic into U2FTokenManager in Bug 1409220.
+
+  // Process each element of mPubKeyCredParams using the following steps, to
+  // produce a new sequence acceptableParams.
+  nsTArray<PublicKeyCredentialParameters> acceptableParams;
   for (size_t a = 0; a < aOptions.mPubKeyCredParams.Length(); ++a) {
     // Let current be the currently selected element of
-    // cryptoParameters.
+    // mPubKeyCredParams.
 
     // If current.type does not contain a PublicKeyCredentialType
     // supported by this implementation, then stop processing current and move
-    // on to the next element in cryptoParameters.
+    // on to the next element in mPubKeyCredParams.
     if (aOptions.mPubKeyCredParams[a].mType != PublicKeyCredentialType::Public_key) {
       continue;
     }
 
-    // Let normalizedAlgorithm be the result of normalizing an algorithm using
-    // the procedure defined in [WebCryptoAPI], with alg set to
-    // current.algorithm and op set to 'generateKey'. If an error occurs during
-    // this procedure, then stop processing current and move on to the next
-    // element in cryptoParameters.
-
     nsString algName;
-    if (NS_FAILED(GetAlgorithmName(aOptions.mPubKeyCredParams[a].mAlg,
-                                   algName))) {
+    if (NS_FAILED(CoseAlgorithmToWebCryptoId(aOptions.mPubKeyCredParams[a].mAlg,
+                                             algName))) {
       continue;
     }
 
-    // Add a new object of type PublicKeyCredentialParameters to
-    // normalizedParameters, with type set to current.type and algorithm set to
-    // normalizedAlgorithm.
-    PublicKeyCredentialParameters normalizedObj;
-    normalizedObj.mType = aOptions.mPubKeyCredParams[a].mType;
-    normalizedObj.mAlg.SetAsString().Assign(algName);
-
-    if (!normalizedParams.AppendElement(normalizedObj, mozilla::fallible)){
+    if (!acceptableParams.AppendElement(aOptions.mPubKeyCredParams[a],
+                                        mozilla::fallible)){
       promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
       return promise.forget();
     }
   }
 
-  // If normalizedAlgorithm is empty and cryptoParameters was not empty, cancel
+  // If acceptableParams is empty and mPubKeyCredParams was not empty, cancel
   // the timer started in step 2, reject promise with a DOMException whose name
   // is "NotSupportedError", and terminate this algorithm.
-  if (normalizedParams.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) {
-    promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
-    return promise.forget();
-  }
-
-  // TODO: The following check should not be here. This is checking for
-  // parameters specific to the soft key, and should be put in the soft key
-  // manager in the parent process. Still need to serialize
-  // PublicKeyCredentialParameters first.
-
-  // Check if at least one of the specified combinations of
-  // PublicKeyCredentialParameters and cryptographic parameters is supported. If
-  // not, return an error code equivalent to NotSupportedError and terminate the
-  // operation.
-
-  bool isValidCombination = false;
-
-  for (size_t a = 0; a < normalizedParams.Length(); ++a) {
-    if (normalizedParams[a].mType == PublicKeyCredentialType::Public_key &&
-        normalizedParams[a].mAlg.IsString() &&
-        normalizedParams[a].mAlg.GetAsString().EqualsLiteral(
-          JWK_ALG_ECDSA_P_256)) {
-      isValidCombination = true;
-      break;
-    }
-  }
-  if (!isValidCombination) {
+  if (acceptableParams.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) {
     promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return promise.forget();
   }
 
   // If excludeList is undefined, set it to the empty list.
   //
   // If extensions was specified, process any extensions supported by this
   // client platform, to produce the extension data that needs to be sent to the
--- a/dom/webauthn/tests/browser/tab_webauthn_success.html
+++ b/dom/webauthn/tests/browser/tab_webauthn_success.html
@@ -33,17 +33,17 @@ function signalCompletion(aText) {
 }
 
 let gState = {};
 let makeCredentialOptions = {
   rp: {id: document.domain, name: "none", icon: "none"},
   user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
   challenge: gCredentialChallenge,
   timeout: 5000, // the minimum timeout is actually 15 seconds
-  pubKeyCredParams: [{type: "public-key", alg: "ES256"}],
+  pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
 };
 
 navigator.credentials.create({publicKey: makeCredentialOptions})
 .then(function (aNewCredentialInfo) {
   gState.credential = aNewCredentialInfo;
 
   return webAuthnDecodeCBORAttestation(aNewCredentialInfo.response.attestationObject);
 })
--- a/dom/webauthn/tests/test_webauthn_loopback.html
+++ b/dom/webauthn/tests/test_webauthn_loopback.html
@@ -133,17 +133,17 @@ function() {
       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 = {name: "none", icon: "none", displayName: "none"};
-    let param = {type: "public-key", alg: "ES256"};
+    let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
     let makeCredentialOptions = {
       rp: rp,
       user: user,
       challenge: gCredentialChallenge,
       pubKeyCredParams: [param]
     };
     credm.create({publicKey: makeCredentialOptions})
     .then(decodeCreatedCredential)
@@ -152,17 +152,17 @@ function() {
       ok(false, aReason);
       SimpleTest.finish();
     });
   }
 
   function testMakeDuplicate(aCredInfo) {
     let rp = {id: document.domain, name: "none", icon: "none"};
     let user = {name: "none", icon: "none", displayName: "none"};
-    let param = {type: "public-key", alg: "ES256"};
+    let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
     let makeCredentialOptions = {
       rp: rp,
       user: user,
       challenge: gCredentialChallenge,
       pubKeyCredParams: [param],
       excludeCredentials: [{type: "public-key", id: new Uint8Array(aCredInfo.rawId),
                      transports: ["usb"]}]
     };
--- a/dom/webauthn/tests/test_webauthn_make_credential.html
+++ b/dom/webauthn/tests/test_webauthn_make_credential.html
@@ -57,18 +57,18 @@
 
       let credm = navigator.credentials;
 
       let gCredentialChallenge = new Uint8Array(16);
       window.crypto.getRandomValues(gCredentialChallenge);
 
       let rp = {id: document.domain, name: "none", icon: "none"};
       let user = {id: new Uint8Array(64), name: "none", icon: "none", displayName: "none"};
-      let param = {type: "public-key", alg: "es256"};
-      let unsupportedParam = {type: "public-key", alg: "3DES"};
+      let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
+      let unsupportedParam = {type: "public-key", alg: cose_alg_ECDSA_w_SHA512};
       let badParam = {type: "SimplePassword", alg: "MaxLength=2"};
 
       var testFuncs = [
         // Test basic good call
         function() {
           let makeCredentialOptions = {
             rp: rp, user: user, challenge: gCredentialChallenge, pubKeyCredParams: [param]
           };
@@ -82,24 +82,25 @@
           let makeCredentialOptions = {
             challenge: gCredentialChallenge, pubKeyCredParams: [param]
           };
           return credm.create({publicKey: makeCredentialOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectTypeError);
         },
 
-        // Test without a parameter
+        // Test without any parameters; this is acceptable meaning the RP ID is
+        // happy to take any credential type
         function() {
           let makeCredentialOptions = {
             rp: rp, user: user, challenge: gCredentialChallenge, pubKeyCredParams: []
           };
           return credm.create({publicKey: makeCredentialOptions})
-                      .then(arrivingHereIsBad)
-                      .catch(expectNotSupportedError);
+                      .then(arrivingHereIsGood)
+                      .catch(arrivingHereIsBad);
         },
 
         // Test without a parameter array at all
         function() {
           let makeCredentialOptions = {
             rp: rp, user: user, challenge: gCredentialChallenge
           };
           return credm.create({publicKey: makeCredentialOptions})
--- a/dom/webauthn/tests/test_webauthn_no_token.html
+++ b/dom/webauthn/tests/test_webauthn_no_token.html
@@ -40,17 +40,17 @@ function() {
   let credentialId = new Uint8Array(128);
   window.crypto.getRandomValues(credentialId);
 
   testMakeCredential();
 
   function testMakeCredential() {
     let rp = {id: document.domain, name: "none", icon: "none"};
     let user = {name: "none", icon: "none", displayName: "none"};
-    let param = {type: "public-key", alg: "es256"};
+    let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
     let makeCredentialOptions = {
       rp: rp, user: user, challenge: credentialChallenge, pubKeyCredParams: [param]
     };
     credm.create({publicKey: makeCredentialOptions})
     .then(function(aResult) {
       ok(false, "Should have failed.");
       testAssertion();
     })
--- a/dom/webauthn/tests/test_webauthn_sameorigin.html
+++ b/dom/webauthn/tests/test_webauthn_sameorigin.html
@@ -56,17 +56,17 @@
       isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist");
 
       let credm = navigator.credentials;
 
       let chall = new Uint8Array(16);
       window.crypto.getRandomValues(chall);
 
       let user = {id: new Uint8Array(16), name: "none", icon: "none", displayName: "none"};
-      let param = {type: "public-key", alg: "Es256"};
+      let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
 
       var testFuncs = [
         function() {
           // Test basic good call
           let rp = {id: document.domain};
           let makeCredentialOptions = {
             rp: rp, user: user, challenge: chall, pubKeyCredParams: [param]
           };
--- a/dom/webauthn/tests/u2futil.js
+++ b/dom/webauthn/tests/u2futil.js
@@ -1,16 +1,19 @@
 // Used by local_addTest() / local_completeTest()
 var _countCompletions = 0;
 var _expectedCompletions = 0;
 
 const flag_TUP = 0x01;
 const flag_UV = 0x04;
 const flag_AT = 0x40;
 
+const cose_alg_ECDSA_w_SHA256 = -7;
+const cose_alg_ECDSA_w_SHA512 = -36;
+
 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 {
--- a/dom/webidl/WebAuthentication.webidl
+++ b/dom/webidl/WebAuthentication.webidl
@@ -36,17 +36,17 @@ interface AuthenticatorAttestationRespon
 interface AuthenticatorAssertionResponse : AuthenticatorResponse {
     [SameObject] readonly attribute ArrayBuffer      authenticatorData;
     [SameObject] readonly attribute ArrayBuffer      signature;
     readonly attribute DOMString                     userId;
 };
 
 dictionary PublicKeyCredentialParameters {
     required PublicKeyCredentialType  type;
-    required WebAuthnAlgorithmID      alg; // Switch to COSE in Bug 1381190
+    required COSEAlgorithmIdentifier  alg;
 };
 
 dictionary MakePublicKeyCredentialOptions {
     required PublicKeyCredentialRpEntity   rp;
     required PublicKeyCredentialUserEntity user;
 
     required BufferSource                            challenge;
     required sequence<PublicKeyCredentialParameters> pubKeyCredParams;
@@ -119,11 +119,9 @@ enum AuthenticatorTransport {
     "nfc",
     "ble"
 };
 
 typedef long COSEAlgorithmIdentifier;
 
 typedef sequence<AAGUID>      AuthenticatorSelectionList;
 
-typedef BufferSource      AAGUID;
-
-typedef (boolean or DOMString) WebAuthnAlgorithmID; // Switch to COSE in Bug 1381190
+typedef BufferSource      AAGUID;
\ No newline at end of file