Bug 1191936 - Implement SPKI/PKCS#8/JWK import/export for RSA-PSS r=rbarnes
authorTim Taubert <ttaubert@mozilla.com>
Wed, 14 Oct 2015 13:38:05 +0200
changeset 318135 29a076e8bc0d803b73533c95def47b7bdaf93dc9
parent 318134 1fba432e1cab3cefc95fca1553d9fbd850fb73c8
child 318136 92e279613f0e6d98c0d206a5d9e7f7106a9dea2c
push id5913
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 16:57:49 +0000
treeherdermozilla-beta@dcaf0a6fa115 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrbarnes
bugs1191936
milestone47.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 1191936 - Implement SPKI/PKCS#8/JWK import/export for RSA-PSS r=rbarnes
dom/crypto/KeyAlgorithmProxy.cpp
dom/crypto/WebCryptoCommon.h
dom/crypto/WebCryptoTask.cpp
dom/crypto/test/test-vectors.js
dom/crypto/test/test_WebCrypto_RSA_PSS.html
--- a/dom/crypto/KeyAlgorithmProxy.cpp
+++ b/dom/crypto/KeyAlgorithmProxy.cpp
@@ -188,16 +188,29 @@ KeyAlgorithmProxy::JwkAlg() const
       return NS_LITERAL_STRING(JWK_ALG_RSA_OAEP_256);
     } else if (hashName.EqualsLiteral(WEBCRYPTO_ALG_SHA384)) {
       return NS_LITERAL_STRING(JWK_ALG_RSA_OAEP_384);
     } else if (hashName.EqualsLiteral(WEBCRYPTO_ALG_SHA512)) {
       return NS_LITERAL_STRING(JWK_ALG_RSA_OAEP_512);
     }
   }
 
+  if (mName.EqualsLiteral(WEBCRYPTO_ALG_RSA_PSS)) {
+    nsString hashName = mRsa.mHash.mName;
+    if (hashName.EqualsLiteral(WEBCRYPTO_ALG_SHA1)) {
+      return NS_LITERAL_STRING(JWK_ALG_PS1);
+    } else if (hashName.EqualsLiteral(WEBCRYPTO_ALG_SHA256)) {
+      return NS_LITERAL_STRING(JWK_ALG_PS256);
+    } else if (hashName.EqualsLiteral(WEBCRYPTO_ALG_SHA384)) {
+      return NS_LITERAL_STRING(JWK_ALG_PS384);
+    } else if (hashName.EqualsLiteral(WEBCRYPTO_ALG_SHA512)) {
+      return NS_LITERAL_STRING(JWK_ALG_PS512);
+    }
+  }
+
   return nsString();
 }
 
 CK_MECHANISM_TYPE
 KeyAlgorithmProxy::GetMechanism(const KeyAlgorithm& aAlgorithm)
 {
   // For everything but HMAC, the name determines the mechanism
   // HMAC is handled by the specialization below
--- a/dom/crypto/WebCryptoCommon.h
+++ b/dom/crypto/WebCryptoCommon.h
@@ -83,16 +83,20 @@
 #define JWK_ALG_RS1                 "RS1"      // RSASSA-PKCS1
 #define JWK_ALG_RS256               "RS256"
 #define JWK_ALG_RS384               "RS384"
 #define JWK_ALG_RS512               "RS512"
 #define JWK_ALG_RSA_OAEP            "RSA-OAEP" // RSA-OAEP
 #define JWK_ALG_RSA_OAEP_256        "RSA-OAEP-256"
 #define JWK_ALG_RSA_OAEP_384        "RSA-OAEP-384"
 #define JWK_ALG_RSA_OAEP_512        "RSA-OAEP-512"
+#define JWK_ALG_PS1                 "PS1"      // RSA-PSS
+#define JWK_ALG_PS256               "PS256"
+#define JWK_ALG_PS384               "PS384"
+#define JWK_ALG_PS512               "PS512"
 #define JWK_ALG_ECDSA_P_256         "ES256"
 #define JWK_ALG_ECDSA_P_384         "ES384"
 #define JWK_ALG_ECDSA_P_521         "ES521"
 
 // JWK usages
 #define JWK_USE_ENC                 "enc"
 #define JWK_USE_SIG                 "sig"
 
@@ -201,16 +205,18 @@ MapAlgorithmNameToMechanism(const nsStri
   } else if (aName.EqualsLiteral(WEBCRYPTO_ALG_SHA512)) {
     mechanism = CKM_SHA512;
   } else if (aName.EqualsLiteral(WEBCRYPTO_ALG_PBKDF2)) {
     mechanism = CKM_PKCS5_PBKD2;
   } else if (aName.EqualsLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1)) {
     mechanism = CKM_RSA_PKCS;
   } else if (aName.EqualsLiteral(WEBCRYPTO_ALG_RSA_OAEP)) {
     mechanism = CKM_RSA_PKCS_OAEP;
+  } else if (aName.EqualsLiteral(WEBCRYPTO_ALG_RSA_PSS)) {
+    mechanism = CKM_RSA_PKCS_PSS;
   } else if (aName.EqualsLiteral(WEBCRYPTO_ALG_ECDH)) {
     mechanism = CKM_ECDH1_DERIVE;
   } else if (aName.EqualsLiteral(WEBCRYPTO_ALG_DH)) {
     mechanism = CKM_DH_PKCS_DERIVE;
   }
 
   return mechanism;
 }
--- a/dom/crypto/WebCryptoTask.cpp
+++ b/dom/crypto/WebCryptoTask.cpp
@@ -1663,17 +1663,18 @@ public:
   {
     ImportKeyTask::Init(aCx, aFormat, aAlgorithm, aExtractable, aKeyUsages);
     if (NS_FAILED(mEarlyRv)) {
       return;
     }
 
     // If this is RSA with a hash, cache the hash name
     if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1) ||
-        mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSA_OAEP)) {
+        mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSA_OAEP) ||
+        mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSA_PSS)) {
       RootedDictionary<RsaHashedImportParams> params(aCx);
       mEarlyRv = Coerce(aCx, params, aAlgorithm);
       if (NS_FAILED(mEarlyRv)) {
         mEarlyRv = NS_ERROR_DOM_DATA_ERR;
         return;
       }
 
       mEarlyRv = GetAlgorithmName(aCx, params.mHash, mHashName);
@@ -1765,17 +1766,18 @@ private:
     // Check permissions for the requested operation
     if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSA_OAEP)) {
       if ((mKey->GetKeyType() == CryptoKey::PUBLIC &&
            mKey->HasUsageOtherThan(CryptoKey::ENCRYPT | CryptoKey::WRAPKEY)) ||
           (mKey->GetKeyType() == CryptoKey::PRIVATE &&
            mKey->HasUsageOtherThan(CryptoKey::DECRYPT | CryptoKey::UNWRAPKEY))) {
         return NS_ERROR_DOM_DATA_ERR;
       }
-    } else if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1)) {
+    } else if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1) ||
+               mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSA_PSS)) {
       if ((mKey->GetKeyType() == CryptoKey::PUBLIC &&
            mKey->HasUsageOtherThan(CryptoKey::VERIFY)) ||
           (mKey->GetKeyType() == CryptoKey::PRIVATE &&
            mKey->HasUsageOtherThan(CryptoKey::SIGN))) {
         return NS_ERROR_DOM_DATA_ERR;
       }
     }
 
@@ -3331,17 +3333,18 @@ WebCryptoTask::CreateImportKeyTask(JSCon
       algName.EqualsLiteral(WEBCRYPTO_ALG_AES_GCM) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_AES_KW) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_PBKDF2) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_HKDF) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_HMAC)) {
     return new ImportSymmetricKeyTask(aCx, aFormat, aKeyData, aAlgorithm,
                                       aExtractable, aKeyUsages);
   } else if (algName.EqualsLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1) ||
-             algName.EqualsLiteral(WEBCRYPTO_ALG_RSA_OAEP)) {
+             algName.EqualsLiteral(WEBCRYPTO_ALG_RSA_OAEP) ||
+             algName.EqualsLiteral(WEBCRYPTO_ALG_RSA_PSS)) {
     return new ImportRsaKeyTask(aCx, aFormat, aKeyData, aAlgorithm,
                                 aExtractable, aKeyUsages);
   } else if (algName.EqualsLiteral(WEBCRYPTO_ALG_ECDH) ||
              algName.EqualsLiteral(WEBCRYPTO_ALG_ECDSA)) {
     return new ImportEcKeyTask(aCx, aFormat, aKeyData, aAlgorithm,
                                aExtractable, aKeyUsages);
   } else if (algName.EqualsLiteral(WEBCRYPTO_ALG_DH)) {
     return new ImportDhKeyTask(aCx, aFormat, aKeyData, aAlgorithm,
@@ -3377,16 +3380,17 @@ WebCryptoTask::CreateExportKeyTask(const
   if (algName.EqualsLiteral(WEBCRYPTO_ALG_AES_CBC) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_AES_CTR) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_AES_GCM) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_AES_KW) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_PBKDF2) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_HMAC) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_RSA_OAEP) ||
+      algName.EqualsLiteral(WEBCRYPTO_ALG_RSA_PSS) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_ECDSA) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_ECDH) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_DH)) {
     return new ExportKeyTask(aFormat, aKey);
   }
 
   return new FailureTask(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
 }
@@ -3599,17 +3603,18 @@ WebCryptoTask::CreateUnwrapKeyTask(JSCon
       keyAlgName.EqualsASCII(WEBCRYPTO_ALG_AES_CTR) ||
       keyAlgName.EqualsASCII(WEBCRYPTO_ALG_AES_GCM) ||
       keyAlgName.EqualsASCII(WEBCRYPTO_ALG_HKDF) ||
       keyAlgName.EqualsASCII(WEBCRYPTO_ALG_HMAC)) {
     importTask = new ImportSymmetricKeyTask(aCx, aFormat,
                                             aUnwrappedKeyAlgorithm,
                                             aExtractable, aKeyUsages);
   } else if (keyAlgName.EqualsASCII(WEBCRYPTO_ALG_RSASSA_PKCS1) ||
-             keyAlgName.EqualsASCII(WEBCRYPTO_ALG_RSA_OAEP)) {
+             keyAlgName.EqualsASCII(WEBCRYPTO_ALG_RSA_OAEP) ||
+             keyAlgName.EqualsASCII(WEBCRYPTO_ALG_RSA_PSS)) {
     importTask = new ImportRsaKeyTask(aCx, aFormat,
                                       aUnwrappedKeyAlgorithm,
                                       aExtractable, aKeyUsages);
   } else {
     return new FailureTask(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
   }
 
   nsString unwrapAlgName;
--- a/dom/crypto/test/test-vectors.js
+++ b/dom/crypto/test/test-vectors.js
@@ -413,16 +413,96 @@ tv = {
     result: util.hex2abv(
       "354fe67b4a126d5d35fe36c777791a3f7ba13def484e2d3908aff722fad468fb" +
       "21696de95d0be911c2d3174f8afcc201035f7b6d8e69402de5451618c21a535f" +
       "a9d7bfc5b8dd9fc243f8cf927db31322d6e881eaa91a996170e657a05a266426" +
       "d98c88003f8477c1227094a0d9fa1e8c4024309ce1ecccb5210035d47ac72e8a"
     ),
   },
 
+  // RSA-PSS test vectors, pss-vect.txt, Example 1: A 1024-bit RSA Key Pair
+  // <ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1-vec.zip>
+  rsapss: {
+    pkcs8: util.hex2abv(
+      "30820275020100300d06092a864886f70d01010105000482025f3082025b0201" +
+      "0002818100a56e4a0e701017589a5187dc7ea841d156f2ec0e36ad52a44dfeb1" +
+      "e61f7ad991d8c51056ffedb162b4c0f283a12a88a394dff526ab7291cbb307ce" +
+      "abfce0b1dfd5cd9508096d5b2b8b6df5d671ef6377c0921cb23c270a70e2598e" +
+      "6ff89d19f105acc2d3f0cb35f29280e1386b6f64c4ef22e1e1f20d0ce8cffb22" +
+      "49bd9a2137020301000102818033a5042a90b27d4f5451ca9bbbd0b44771a101" +
+      "af884340aef9885f2a4bbe92e894a724ac3c568c8f97853ad07c0266c8c6a3ca" +
+      "0929f1e8f11231884429fc4d9ae55fee896a10ce707c3ed7e734e44727a39574" +
+      "501a532683109c2abacaba283c31b4bd2f53c3ee37e352cee34f9e503bd80c06" +
+      "22ad79c6dcee883547c6a3b325024100e7e8942720a877517273a356053ea2a1" +
+      "bc0c94aa72d55c6e86296b2dfc967948c0a72cbccca7eacb35706e09a1df55a1" +
+      "535bd9b3cc34160b3b6dcd3eda8e6443024100b69dca1cf7d4d7ec81e75b90fc" +
+      "ca874abcde123fd2700180aa90479b6e48de8d67ed24f9f19d85ba275874f542" +
+      "cd20dc723e6963364a1f9425452b269a6799fd024028fa13938655be1f8a159c" +
+      "baca5a72ea190c30089e19cd274a556f36c4f6e19f554b34c077790427bbdd8d" +
+      "d3ede2448328f385d81b30e8e43b2fffa02786197902401a8b38f398fa712049" +
+      "898d7fb79ee0a77668791299cdfa09efc0e507acb21ed74301ef5bfd48be455e" +
+      "aeb6e1678255827580a8e4e8e14151d1510a82a3f2e729024027156aba4126d2" +
+      "4a81f3a528cbfb27f56886f840a9f6e86e17a44b94fe9319584b8e22fdde1e5a" +
+      "2e3bd8aa5ba8d8584194eb2190acf832b847f13a3d24a79f4d"
+    ),
+    spki: util.hex2abv(
+      "30819f300d06092a864886f70d010101050003818d0030818902818100a56e4a" +
+      "0e701017589a5187dc7ea841d156f2ec0e36ad52a44dfeb1e61f7ad991d8c510" +
+      "56ffedb162b4c0f283a12a88a394dff526ab7291cbb307ceabfce0b1dfd5cd95" +
+      "08096d5b2b8b6df5d671ef6377c0921cb23c270a70e2598e6ff89d19f105acc2" +
+      "d3f0cb35f29280e1386b6f64c4ef22e1e1f20d0ce8cffb2249bd9a2137020301" +
+      "0001"
+    ),
+    data: util.hex2abv(
+      "cdc87da223d786df3b45e0bbbc721326d1ee2af806cc315475cc6f0d9c66e1b6" +
+      "2371d45ce2392e1ac92844c310102f156a0d8d52c1f4c40ba3aa65095786cb76" +
+      "9757a6563ba958fed0bcc984e8b517a3d5f515b23b8a41e74aa867693f90dfb0" +
+      "61a6e86dfaaee64472c00e5f20945729cbebe77f06ce78e08f4098fba41f9d61" +
+      "93c0317e8b60d4b6084acb42d29e3808a3bc372d85e331170fcbf7cc72d0b71c" +
+      "296648b3a4d10f416295d0807aa625cab2744fd9ea8fd223c42537029828bd16" +
+      "be02546f130fd2e33b936d2676e08aed1b73318b750a0167d0"
+    ),
+    sig: util.hex2abv(
+      "9074308fb598e9701b2294388e52f971faac2b60a5145af185df5287b5ed2887" +
+      "e57ce7fd44dc8634e407c8e0e4360bc226f3ec227f9d9e54638e8d31f5051215" +
+      "df6ebb9c2f9579aa77598a38f914b5b9c1bd83c4e2f9f382a0d0aa3542ffee65" +
+      "984a601bc69eb28deb27dca12c82c2d4c3f66cd500f1ff2b994d8a4e30cbb33c"
+    ),
+    salt: util.hex2abv(
+      "dee959c7e06411361420ff80185ed57f3e6776af"
+    ),
+    jwk_priv: {
+      kty: "RSA",
+      n: "pW5KDnAQF1iaUYfcfqhB0Vby7A42rVKkTf6x5h962ZHYxRBW_-2xYrTA8oOhK" +
+         "oijlN_1JqtykcuzB86r_OCx39XNlQgJbVsri2311nHvY3fAkhyyPCcKcOJZjm" +
+         "_4nRnxBazC0_DLNfKSgOE4a29kxO8i4eHyDQzoz_siSb2aITc",
+      e: "AQAB",
+      d: "M6UEKpCyfU9UUcqbu9C0R3GhAa-IQ0Cu-YhfKku-kuiUpySsPFaMj5eFOtB8A" +
+         "mbIxqPKCSnx6PESMYhEKfxNmuVf7olqEM5wfD7X5zTkRyejlXRQGlMmgxCcKr" +
+         "rKuig8MbS9L1PD7jfjUs7jT55QO9gMBiKtecbc7og1R8ajsyU",
+      p: "5-iUJyCod1Fyc6NWBT6iobwMlKpy1VxuhilrLfyWeUjApyy8zKfqyzVwbgmh3" +
+         "1WhU1vZs8w0Fgs7bc0-2o5kQw",
+      q: "tp3KHPfU1-yB51uQ_MqHSrzeEj_ScAGAqpBHm25I3o1n7ST58Z2FuidYdPVCz" +
+         "SDccj5pYzZKH5QlRSsmmmeZ_Q",
+      dp: "KPoTk4ZVvh-KFZy6ylpy6hkMMAieGc0nSlVvNsT24Z9VSzTAd3kEJ7vdjdPt" +
+          "4kSDKPOF2Bsw6OQ7L_-gJ4YZeQ",
+      dq: "Gos485j6cSBJiY1_t57gp3ZoeRKZzfoJ78DlB6yyHtdDAe9b_Ui-RV6utuFn" +
+          "glWCdYCo5OjhQVHRUQqCo_LnKQ",
+      qi: "JxVqukEm0kqB86Uoy_sn9WiG-ECp9uhuF6RLlP6TGVhLjiL93h5aLjvYqluo" +
+          "2FhBlOshkKz4MrhH8To9JKefTQ",
+    },
+    jwk_pub: {
+      kty: "RSA",
+      n: "pW5KDnAQF1iaUYfcfqhB0Vby7A42rVKkTf6x5h962ZHYxRBW_-2xYrTA8oOhK" +
+         "oijlN_1JqtykcuzB86r_OCx39XNlQgJbVsri2311nHvY3fAkhyyPCcKcOJZjm" +
+         "_4nRnxBazC0_DLNfKSgOE4a29kxO8i4eHyDQzoz_siSb2aITc",
+      e: "AQAB",
+    },
+  },
+
   key_wrap_known_answer: {
     key:          util.hex2abv("0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a"),
     wrapping_key: util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
     wrapping_iv:  util.hex2abv("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"),
     wrapped_key:  util.hex2abv("9ed0283a9a2b7e4292ebc5135e6342cc" +
                                "8a7f65802a1f6fd41bd3251c4da0c138")
   },
 
--- a/dom/crypto/test/test_WebCrypto_RSA_PSS.html
+++ b/dom/crypto/test/test_WebCrypto_RSA_PSS.html
@@ -68,16 +68,214 @@ TestArray.addTest(
 
     crypto.subtle.generateKey(alg, false, ["sign", "verify"])
       .then(setKey, error(that))
       .then(doSign, error(that))
       .then(doVerify, error(that))
       .then(complete(that, x => x), error(that))
   }
 );
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "RSA-PSS verify known signature (SHA-1, 1024-bit)",
+  function () {
+    var that = this;
+    var alg = {name: "RSA-PSS", hash: "SHA-1"};
+
+    function doVerify(x) {
+      var alg = {name: "RSA-PSS", saltLength: tv.rsapss.salt.byteLength};
+      return crypto.subtle.verify(alg, x, tv.rsapss.sig, tv.rsapss.data);
+    }
+
+    crypto.subtle.importKey("spki", tv.rsapss.spki, alg, false, ["verify"])
+      .then(doVerify, error(that))
+      .then(complete(that, x => x), error(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "RSA-PSS verify known signature (SHA-1, 1024-bit, JWK)",
+  function () {
+    var that = this;
+    var alg = {name: "RSA-PSS", hash: "SHA-1"};
+
+    function doVerify(x) {
+      var alg = {name: "RSA-PSS", saltLength: tv.rsapss.salt.byteLength};
+      return crypto.subtle.verify(alg, x, tv.rsapss.sig, tv.rsapss.data);
+    }
+
+    crypto.subtle.importKey("jwk", tv.rsapss.jwk_pub, alg, false, ["verify"])
+      .then(doVerify, error(that))
+      .then(complete(that, x => x), error(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "RSA-PSS import SPKI/PKCS#8 keys and sign/verify (SHA-1, 1024-bit)",
+  function () {
+    var that = this;
+    var alg = {name: "RSA-PSS", hash: "SHA-1"};
+
+    var privKey, pubKey;
+    function setKeys([pub, priv]) { pubKey = pub; privKey = priv; }
+    function doSign() {
+      var alg = {name: "RSA-PSS", saltLength: tv.rsapss.salt.byteLength};
+      return crypto.subtle.sign(alg, privKey, tv.rsapss.data);
+    }
+    function doVerify(x) {
+      var alg = {name: "RSA-PSS", saltLength: tv.rsapss.salt.byteLength};
+      return crypto.subtle.verify(alg, pubKey, x, tv.rsapss.data);
+    }
+
+    var spki =
+      crypto.subtle.importKey("spki", tv.rsapss.spki, alg, false, ["verify"]);
+    var pkcs8 =
+      crypto.subtle.importKey("pkcs8", tv.rsapss.pkcs8, alg, false, ["sign"]);
+
+    Promise.all([spki, pkcs8])
+      .then(setKeys, error(that))
+      .then(doSign, error(that))
+      .then(doVerify, error(that))
+      .then(complete(that, x => x), error(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "RSA-PSS import JWK keys and sign/verify (SHA-1, 1024-bit)",
+  function () {
+    var that = this;
+    var alg = {name: "RSA-PSS", hash: "SHA-1"};
+
+    var privKey, pubKey;
+    function setKeys([pub, priv]) { pubKey = pub; privKey = priv; }
+    function doSign() {
+      var alg = {name: "RSA-PSS", saltLength: tv.rsapss.salt.byteLength};
+      return crypto.subtle.sign(alg, privKey, tv.rsapss.data);
+    }
+    function doVerify(x) {
+      var alg = {name: "RSA-PSS", saltLength: tv.rsapss.salt.byteLength};
+      return crypto.subtle.verify(alg, pubKey, x, tv.rsapss.data);
+    }
+
+    var spki =
+      crypto.subtle.importKey("jwk", tv.rsapss.jwk_pub, alg, false, ["verify"]);
+    var pkcs8 =
+      crypto.subtle.importKey("jwk", tv.rsapss.jwk_priv, alg, false, ["sign"]);
+
+    Promise.all([spki, pkcs8])
+      .then(setKeys, error(that))
+      .then(doSign, error(that))
+      .then(doVerify, error(that))
+      .then(complete(that, x => x), error(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "RSA-PSS SPKI import/export (SHA-1, 1024-bit)",
+  function () {
+    var that = this;
+    var alg = {name: "RSA-PSS", hash: "SHA-1"};
+
+    function doExport(x) {
+      return crypto.subtle.exportKey("spki", x);
+    }
+
+    crypto.subtle.importKey("spki", tv.rsapss.spki, alg, true, ["verify"])
+      .then(doExport, error(that))
+      .then(memcmp_complete(that, tv.rsapss.spki), error(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "RSA-PSS PKCS#8 import/export (SHA-1, 1024-bit)",
+  function () {
+    var that = this;
+    var alg = {name: "RSA-PSS", hash: "SHA-1"};
+
+    function doExport(x) {
+      return crypto.subtle.exportKey("pkcs8", x);
+    }
+
+    crypto.subtle.importKey("pkcs8", tv.rsapss.pkcs8, alg, true, ["sign"])
+      .then(doExport, error(that))
+      .then(memcmp_complete(that, tv.rsapss.pkcs8), error(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "RSA-PSS JWK export a public key",
+  function () {
+    var that = this;
+    var alg = {name: "RSA-PSS", hash: "SHA-1"};
+    var jwk = tv.rsapss.jwk_pub;
+
+    function doExport(x) {
+      return crypto.subtle.exportKey("jwk", x);
+    }
+
+    crypto.subtle.importKey("jwk", jwk, alg, true, ["verify"])
+      .then(doExport)
+      .then(
+        complete(that, function(x) {
+          return hasBaseJwkFields(x) &&
+                 hasFields(x, ["n", "e"]) &&
+                 x.kty == "RSA" &&
+                 x.alg == "PS1" &&
+                 x.ext &&
+                 shallowArrayEquals(x.key_ops, ["verify"]) &&
+                 x.n == jwk.n &&
+                 x.e == jwk.e;
+          }),
+        error(that)
+      );
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "RSA-PSS JWK export a private key",
+  function () {
+    var that = this;
+    var alg = {name: "RSA-PSS", hash: "SHA-1"};
+    var jwk = tv.rsapss.jwk_priv;
+
+    function doExport(x) {
+      return crypto.subtle.exportKey("jwk", x);
+    }
+
+    crypto.subtle.importKey("jwk", jwk, alg, true, ["sign"])
+      .then(doExport)
+      .then(
+        complete(that, function(x) {
+          return hasBaseJwkFields(x) &&
+                 hasFields(x, ["n", "e", "d", "p", "q", "dp", "dq", "qi"]) &&
+                 x.kty == "RSA" &&
+                 x.alg == "PS1" &&
+                 x.ext &&
+                 shallowArrayEquals(x.key_ops, ["sign"]) &&
+                 x.n == jwk.n &&
+                 x.e == jwk.e &&
+                 x.d == jwk.d &&
+                 x.p == jwk.p &&
+                 x.q == jwk.q &&
+                 x.dp == jwk.dp &&
+                 x.dq == jwk.dq &&
+                 x.qi == jwk.qi;
+          }),
+        error(that)
+      );
+  }
+);
 /*]]>*/</script>
 </head>
 
 <body>
 
 <div id="content">
 	<div id="head">
 		<b>Web</b>Crypto<br>