Bug 1381190 - Change to COSE Algorithm identifiers for WebAuthn r=qdot,ttaubert
authorJ.C. Jones <jjones@mozilla.com>
Thu, 12 Oct 2017 15:21:06 -0700
changeset 386739 35f1751b91a9fff2c6f4649ce90aec5d1eb72976
parent 386738 c09ea1671fc337f30941d52e64588f76af7096ef
child 386740 215bc8a3310cde31dd6f09c72f8e5c84a904023f
push id32701
push userarchaeopteryx@coole-files.de
push dateWed, 18 Oct 2017 09:44:15 +0000
treeherdermozilla-central@8b57edba9837 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersqdot, ttaubert
bugs1381190, 1409220
milestone58.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 1381190 - Change to COSE Algorithm identifiers for WebAuthn r=qdot,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);
@@ -360,85 +338,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