Bug 1688685 - land NSS 92dcda94c1d4 UPGRADE_NSS_RELEASE, r=bbeurdouche
authorKevin Jacobs <kjacobs@mozilla.com>
Tue, 26 Jan 2021 15:30:01 +0000
changeset 564715 a13d49d1516bc9a262987f1ce7ef001f75a797c8
parent 564714 cc5a06a0c509f8757532bffae9260c4bd3c20408
child 564716 9c9c3f218fa44ae7a7e9090c98a1dffec30cc9ca
push id135000
push usercbrindusan@mozilla.com
push dateTue, 26 Jan 2021 16:30:45 +0000
treeherderautoland@9c9c3f218fa4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbbeurdouche
bugs1688685, 1686134, 1678398, 1681585
milestone87.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 1688685 - land NSS 92dcda94c1d4 UPGRADE_NSS_RELEASE, r=bbeurdouche 2021-01-22 Kevin Jacobs <kjacobs@mozilla.com> * automation/abi-check/previous-nss-release, lib/nss/nss.h, lib/softoken/softkver.h, lib/util/nssutil.h: Set version numbers to 3.62 Beta [680ec01577b9] 2021-01-23 Kevin Jacobs <kjacobs@mozilla.com> * tests/chains/scenarios/nameconstraints.cfg, tests/libpkix/certs/NameConstraints.ipaca.cert, tests/libpkix/certs/NameConstraints.ocsp1.cert: Bug 1686134 - Renew two chains libpkix test certificates. r=rrelyea [3ddcd845704c] 2021-01-25 Kevin Jacobs <kjacobs@mozilla.com> * gtests/common/testvectors/hpke-vectors.h, gtests/pk11_gtest/pk11_hpke_unittest.cc, lib/pk11wrap/pk11hpke.c, lib/pk11wrap/pk11hpke.h, lib/pk11wrap/pk11pub.h: Bug 1678398 - Update HPKE to draft-07. r=mt This patch updates HPKE to draft-07. A few other minor changes are included: - Refactor HPKE gtests for increased parameterized testing. - Replace memcpy calls with PORT_Memcpy - Serialization tweaks to make way for context Export/Import (D99277). This should not be landed without an ECH update, as fixed ECH test vectors will otherwise fail to decrypt. [e0bf8cadadc7] * automation/abi-check/expected-report-libnss3.so.txt, gtests/pk11_gtest/pk11_hpke_unittest.cc, lib/nss/nss.def, lib/pk11wrap/pk11hpke.c, lib/pk11wrap/pk11pub.h: Bug 1678398 - Add Export/Import functions for HPKE context. r=mt This patch adds and exports two new HPKE functions: `PK11_HPKE_ExportContext` and `PK11_HPKE_ImportContext`, which are used to export a serialized HPKE context, then later reimport that context and resume Open and Export operations. Only receiver contexts are currently supported for export (see the rationale in pk11pub.h). One other change introduced here is that `PK11_HPKE_GetEncapPubKey` now works as expected on the receiver side. If the `wrapKey` argument is provided to the Export/Import functions, then the symmetric keys are wrapped with AES Key Wrap with Padding (SP800-38F, 6.3) prior to serialization. [8bcd12ab3b34] * automation/abi-check/expected-report-libssl3.so.txt, gtests/ssl_gtest/libssl_internals.c, gtests/ssl_gtest/libssl_internals.h, gtests/ssl_gtest/ssl_extension_unittest.cc, gtests/ssl_gtest/tls_ech_unittest.cc, lib/ssl/ssl3con.c, lib/ssl/ssl3ext.c, lib/ssl/ssl3ext.h, lib/ssl/sslexp.h, lib/ssl/sslimpl.h, lib/ssl/sslsecur.c, lib/ssl/sslsock.c, lib/ssl/sslt.h, lib/ssl/tls13con.c, lib/ssl/tls13con.h, lib/ssl/tls13ech.c, lib/ssl/tls13ech.h, lib/ssl/tls13exthandle.c, lib/ssl/tls13exthandle.h, lib/ssl/tls13hashstate.c, lib/ssl/tls13hashstate.h: Bug 1681585 - Update ECH to Draft-09. r=mt This patch updates ECH implementation to draft-09. Changes of note are: - Acceptance signal derivation is now based on the handshake secret. - `config_id` hint changes from 32B to 8B, trial decryption added on the server. - Duplicate code in HRR cookie handling has been consolidated into `tls13_HandleHrrCookie`. - `ech_is_inner` extension is added, which causes a server to indicate ECH acceptance. - Per the above, support signaling ECH acceptance when acting as a backend server in split-mode (i.e. when there is no other local Encrypted Client Hello state). [ed07a2e2a124] 2021-01-24 Kevin Jacobs <kjacobs@mozilla.com> * cmd/selfserv/selfserv.c: Bug 1681585 - Add ECH support to selfserv. r=mt Usage example: mkdir dbdir && cd dbdir certutil -N -d . certutil -S -s "CN=ech-public.com" -n ech-public.com -x -t "C,C,C" -m 1234 -d . certutil -S -s "CN=ech-private-backend.com" -n ech-private- backend.com -x -t "C,C,C" -m 2345 -d . ../dist/Debug/bin/selfserv -a ech-public.com -a ech-private-backend.com -n ech-public.com -n ech- private-backend.com -p 8443 -d dbdir/ -X publicname:ech-public.com (Copy echconfig from selfserv output and paste into the below command) ../dist/Debug/bin/tstclnt -D -p 8443 -v -A tests/ssl/sslreq.dat -h ech-private-backend.com -o -N <echconfig> -v [92dcda94c1d4] Differential Revision: https://phabricator.services.mozilla.com/D102982
build/moz.configure/nss.configure
security/nss/TAG-INFO
security/nss/automation/abi-check/expected-report-libnss3.so.txt
security/nss/automation/abi-check/expected-report-libssl3.so.txt
security/nss/automation/abi-check/previous-nss-release
security/nss/cmd/selfserv/selfserv.c
security/nss/coreconf/coreconf.dep
security/nss/gtests/common/testvectors/hpke-vectors.h
security/nss/gtests/pk11_gtest/pk11_hpke_unittest.cc
security/nss/gtests/ssl_gtest/libssl_internals.c
security/nss/gtests/ssl_gtest/libssl_internals.h
security/nss/gtests/ssl_gtest/ssl_extension_unittest.cc
security/nss/gtests/ssl_gtest/tls_ech_unittest.cc
security/nss/lib/nss/nss.def
security/nss/lib/nss/nss.h
security/nss/lib/pk11wrap/pk11hpke.c
security/nss/lib/pk11wrap/pk11hpke.h
security/nss/lib/pk11wrap/pk11pub.h
security/nss/lib/softoken/softkver.h
security/nss/lib/ssl/ssl3con.c
security/nss/lib/ssl/ssl3ext.c
security/nss/lib/ssl/ssl3ext.h
security/nss/lib/ssl/sslexp.h
security/nss/lib/ssl/sslimpl.h
security/nss/lib/ssl/sslsecur.c
security/nss/lib/ssl/sslsock.c
security/nss/lib/ssl/sslt.h
security/nss/lib/ssl/tls13con.c
security/nss/lib/ssl/tls13con.h
security/nss/lib/ssl/tls13ech.c
security/nss/lib/ssl/tls13ech.h
security/nss/lib/ssl/tls13exthandle.c
security/nss/lib/ssl/tls13exthandle.h
security/nss/lib/ssl/tls13hashstate.c
security/nss/lib/ssl/tls13hashstate.h
security/nss/lib/util/nssutil.h
security/nss/tests/chains/scenarios/nameconstraints.cfg
security/nss/tests/libpkix/certs/NameConstraints.ipaca.cert
security/nss/tests/libpkix/certs/NameConstraints.ocsp1.cert
--- a/build/moz.configure/nss.configure
+++ b/build/moz.configure/nss.configure
@@ -4,17 +4,17 @@
 # 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/.
 
 option("--with-system-nss", help="Use system NSS")
 
 imply_option("--with-system-nspr", True, when="--with-system-nss")
 
 nss_pkg = pkg_check_modules(
-    "NSS", "nss >= 3.61", when="--with-system-nss", config=False
+    "NSS", "nss >= 3.62", when="--with-system-nss", config=False
 )
 
 set_config("MOZ_SYSTEM_NSS", True, when="--with-system-nss")
 
 
 @depends(nss_pkg, check_build_environment)
 def nss_config(nss_pkg, build_env):
     cflags = ["-I%s" % os.path.join(build_env.dist, "include", "nss")]
--- a/security/nss/TAG-INFO
+++ b/security/nss/TAG-INFO
@@ -1,1 +1,1 @@
-NSS_3_61_RTM
\ No newline at end of file
+92dcda94c1d4
\ No newline at end of file
--- a/security/nss/automation/abi-check/expected-report-libnss3.so.txt
+++ b/security/nss/automation/abi-check/expected-report-libnss3.so.txt
@@ -0,0 +1,5 @@
+
+2 Added functions:
+
+  [A] 'function SECStatus PK11_HPKE_ExportContext(const HpkeContext*, PK11SymKey*, SECItem**)'    {PK11_HPKE_ExportContext@@NSS_3.62}
+  [A] 'function HpkeContext* PK11_HPKE_ImportContext(const SECItem*, PK11SymKey*)'    {PK11_HPKE_ImportContext@@NSS_3.62}
--- a/security/nss/automation/abi-check/expected-report-libssl3.so.txt
+++ b/security/nss/automation/abi-check/expected-report-libssl3.so.txt
@@ -0,0 +1,11 @@
+
+1 function with some indirect sub-type change:
+
+  [C] 'function SECStatus SSL_HandshakeNegotiatedExtension(PRFileDesc*, SSLExtensionType, PRBool*)' at sslreveal.c:72:1 has some indirect sub-type changes:
+    parameter 2 of type 'typedef SSLExtensionType' has sub-type changes:
+      underlying type 'enum __anonymous_enum__' at sslt.h:519:1 changed:
+        type size hasn't changed
+        1 enumerator insertion:
+          '__anonymous_enum__::ssl_tls13_ech_is_inner_xtn' value '55817'
+        1 enumerator change:
+          '__anonymous_enum__::ssl_tls13_encrypted_client_hello_xtn' from value '65032' to '65033' at sslt.h:519:1
--- a/security/nss/automation/abi-check/previous-nss-release
+++ b/security/nss/automation/abi-check/previous-nss-release
@@ -1,1 +1,1 @@
-NSS_3_60_BRANCH
+NSS_3_61_BRANCH
--- a/security/nss/cmd/selfserv/selfserv.c
+++ b/security/nss/cmd/selfserv/selfserv.c
@@ -37,16 +37,17 @@
 #include "secitem.h"
 #include "nss.h"
 #include "ssl.h"
 #include "sslproto.h"
 #include "sslexp.h"
 #include "cert.h"
 #include "certt.h"
 #include "ocsp.h"
+#include "nssb64.h"
 
 #ifndef PORT_Sprintf
 #define PORT_Sprintf sprintf
 #endif
 
 #ifndef PORT_Strstr
 #define PORT_Strstr strstr
 #endif
@@ -135,16 +136,17 @@ static PRBool noDelay;
 static int requestCert;
 static int verbose;
 static SECItem bigBuf;
 static int configureDHE = -1;        /* -1: don't configure, 0 disable, >=1 enable*/
 static int configureReuseECDHE = -1; /* -1: don't configure, 0 refresh, >=1 reuse*/
 static int configureWeakDHE = -1;    /* -1: don't configure, 0 disable, >=1 enable*/
 SECItem psk = { siBuffer, NULL, 0 };
 SECItem pskLabel = { siBuffer, NULL, 0 };
+char *echParamsStr = NULL;
 
 static PRThread *acceptorThread;
 
 static PRLogModuleInfo *lm;
 
 #define PRINTF   \
     if (verbose) \
     printf
@@ -242,17 +244,24 @@ PrintParameterUsage()
         "   The argument is a comma separated list of exporters in the form:\n"
         "     LABEL[:OUTPUT-LENGTH[:CONTEXT]]\n"
         "   where LABEL and CONTEXT can be either a free-form string or\n"
         "   a hex string if it is preceded by \"0x\"; OUTPUT-LENGTH\n"
         "   is a decimal integer.\n"
         "-z Configure a TLS 1.3 External PSK with the given hex string for a key.\n"
         "   To specify a label, use ':' as a delimiter. For example:\n"
         "   0xAAAABBBBCCCCDDDD:mylabel. Otherwise, the default label of\n"
-        "  'Client_identity' will be used.\n",
+        "  'Client_identity' will be used.\n"
+        "-X Configure the server for ECH via the given <ECHParams>.  ECHParams\n"
+        "   are expected in one of two formats:\n"
+        "      1. A string containing the ECH public name prefixed by the substring\n"
+        "         \"publicname:\". For example, \"publicname:example.com\". In this mode,\n"
+        "         an ephemeral ECH keypair is generated and ECHConfigs are printed to stdout.\n"
+        "      2. As a Base64 tuple of <ECHRawPrivateKey> || <ECHConfigs>. In this mode, the\n"
+        "         raw private key is used to bootstrap the HPKE context.\n",
         stderr);
 }
 
 static void
 Usage(const char *progName)
 {
     PrintUsageHeader(progName);
     PrintParameterUsage();
@@ -1868,16 +1877,206 @@ importPsk(PRFileDesc *model_sock)
 
     SECStatus rv = SSL_AddExternalPsk(model_sock, symKey,
                                       (const PRUint8 *)pskLabel.data,
                                       pskLabel.len, ssl_hash_sha256);
     PK11_FreeSymKey(symKey);
     return rv;
 }
 
+static SECStatus
+configureEchWithPublicName(PRFileDesc *model_sock, const char *public_name)
+{
+    SECStatus rv;
+
+#define OID_LEN 65
+    unsigned char paramBuf[OID_LEN];
+    SECItem ecParams = { siBuffer, paramBuf, sizeof(paramBuf) };
+    SECKEYPublicKey *pubKey = NULL;
+    SECKEYPrivateKey *privKey = NULL;
+    SECOidData *oidData;
+    char *echConfigBase64 = NULL;
+    PRUint8 configBuf[1000];
+    unsigned int len = 0;
+    unsigned int echCipherSuite = ((unsigned int)HpkeKdfHkdfSha256 << 16) |
+                                  HpkeAeadChaCha20Poly1305;
+    PK11SlotInfo *slot = PK11_GetInternalKeySlot();
+    if (!slot) {
+        errWarn("PK11_GetInternalKeySlot failed");
+        return SECFailure;
+    }
+
+    oidData = SECOID_FindOIDByTag(SEC_OID_CURVE25519);
+    if (oidData && (2 + oidData->oid.len) < sizeof(paramBuf)) {
+        ecParams.data[0] = SEC_ASN1_OBJECT_ID;
+        ecParams.data[1] = oidData->oid.len;
+        memcpy(ecParams.data + 2, oidData->oid.data, oidData->oid.len);
+        ecParams.len = oidData->oid.len + 2;
+    } else {
+        errWarn("SECOID_FindOIDByTag failed");
+        goto loser;
+    }
+    privKey = PK11_GenerateKeyPair(slot, CKM_EC_KEY_PAIR_GEN, &ecParams,
+                                   &pubKey, PR_FALSE, PR_FALSE, NULL);
+
+    if (!privKey || !pubKey) {
+        errWarn("Failed to generate ECH keypair");
+        goto loser;
+    }
+    rv = SSL_EncodeEchConfig(echParamsStr, &echCipherSuite, 1,
+                             HpkeDhKemX25519Sha256, pubKey, 50,
+                             configBuf, &len, sizeof(configBuf));
+    if (rv != SECSuccess) {
+        errWarn("SSL_EncodeEchConfig failed");
+        goto loser;
+    }
+
+    rv = SSL_SetServerEchConfigs(model_sock, pubKey, privKey, configBuf, len);
+    if (rv != SECSuccess) {
+        errWarn("SSL_SetServerEchConfigs failed");
+        goto loser;
+    }
+
+    SECItem echConfigItem = { siBuffer, configBuf, len };
+    echConfigBase64 = NSSBase64_EncodeItem(NULL, NULL, 0, &echConfigItem);
+    if (!echConfigBase64) {
+        errWarn("NSSBase64_EncodeItem failed");
+        goto loser;
+    }
+
+    // Remove the newline characters that NSSBase64_EncodeItem unhelpfully inserts.
+    char *newline = strstr(echConfigBase64, "\r\n");
+    if (newline) {
+        memmove(newline, newline + 2, strlen(newline + 2) + 1);
+    }
+
+    printf("%s\n", echConfigBase64);
+    PORT_Free(echConfigBase64);
+    SECKEY_DestroyPrivateKey(privKey);
+    SECKEY_DestroyPublicKey(pubKey);
+    PK11_FreeSlot(slot);
+    return SECSuccess;
+
+loser:
+    PORT_Free(echConfigBase64);
+    SECKEY_DestroyPrivateKey(privKey);
+    SECKEY_DestroyPublicKey(pubKey);
+    PK11_FreeSlot(slot);
+    return SECFailure;
+}
+
+static SECStatus
+configureEchWithData(PRFileDesc *model_sock)
+{
+/* The input should be a Base64-encoded ECHKey struct:
+     *  struct {
+     *     opaque sk<0..2^16-1>;
+     *     ECHConfig config<0..2^16>; // draft-ietf-tls-esni-09
+     * } ECHKey;
+     *
+     * This is not a standardized format, rather it's designed for
+     * interoperability with https://github.com/xvzcf/tls-interop-runner.
+     */
+
+#define REMAINING_BYTES(rdr, buf) \
+    buf->len - (rdr - buf->data)
+
+    SECStatus rv;
+    size_t len;
+    unsigned char *reader;
+    PK11SlotInfo *slot = NULL;
+    SECItem *decoded = NULL;
+    SECItem *pkcs8Key = NULL;
+    SECKEYPublicKey *pk = NULL;
+    SECKEYPrivateKey *sk = NULL;
+
+    decoded = NSSBase64_DecodeBuffer(NULL, NULL, echParamsStr, PORT_Strlen(echParamsStr));
+    if (!decoded || decoded->len < 2) {
+        errWarn("Couldn't decode ECHParams");
+        goto loser;
+    };
+    reader = decoded->data;
+
+    len = (*(reader++) << 8);
+    len |= *(reader++);
+    if (len > (REMAINING_BYTES(reader, decoded) - 2)) {
+        errWarn("Bad ECHParams encoding");
+        goto loser;
+    }
+    /* Importing a raw KEM private key is generally awful,
+     * however since we only support X25519, we can hardcode
+     * all the OID data. */
+    const PRUint8 pkcs8Start[] = { 0x30, 0x67, 0x02, 0x01, 0x00, 0x30, 0x14, 0x06,
+                                   0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01,
+                                   0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA,
+                                   0x47, 0x0F, 0x01, 0x04, 0x4C, 0x30, 0x4A, 0x02,
+                                   0x01, 0x01, 0x04, 0x20 };
+    const PRUint8 pkcs8End[] = { 0xA1, 0x23, 0x03, 0x21, 0x00, 0x00, 0x00, 0x00,
+                                 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                 0x00, 0x00, 0x00, 0x00, 0x00 };
+    pkcs8Key = SECITEM_AllocItem(NULL, NULL, sizeof(pkcs8Start) + len + sizeof(pkcs8End));
+    if (!pkcs8Key) {
+        goto loser;
+    }
+    PORT_Memcpy(pkcs8Key->data, pkcs8Start, sizeof(pkcs8Start));
+    PORT_Memcpy(&pkcs8Key->data[sizeof(pkcs8Start)], reader, len);
+    PORT_Memcpy(&pkcs8Key->data[sizeof(pkcs8Start) + len], pkcs8End, sizeof(pkcs8End));
+    reader += len;
+
+    /* Convert the key bytes to key handles */
+    slot = PK11_GetInternalKeySlot();
+    rv = PK11_ImportDERPrivateKeyInfoAndReturnKey(
+        slot, pkcs8Key, NULL, NULL, PR_FALSE, PR_FALSE, KU_ALL, &sk, NULL);
+    if (rv != SECSuccess || !sk) {
+        errWarn("ECH key import failed");
+        goto loser;
+    }
+    pk = SECKEY_ConvertToPublicKey(sk);
+    if (!pk) {
+        errWarn("ECH key conversion failed");
+        goto loser;
+    }
+
+    /* Remainder is the ECHConfig. */
+    rv = SSL_SetServerEchConfigs(model_sock, pk, sk, reader,
+                                 REMAINING_BYTES(reader, decoded));
+    if (rv != SECSuccess) {
+        errWarn("SSL_SetServerEchConfigs failed");
+        goto loser;
+    }
+
+    PK11_FreeSlot(slot);
+    SECKEY_DestroyPrivateKey(sk);
+    SECKEY_DestroyPublicKey(pk);
+    SECITEM_FreeItem(pkcs8Key, PR_TRUE);
+    SECITEM_FreeItem(decoded, PR_TRUE);
+    return SECSuccess;
+loser:
+    if (slot) {
+        PK11_FreeSlot(slot);
+    }
+    SECKEY_DestroyPrivateKey(sk);
+    SECKEY_DestroyPublicKey(pk);
+    SECITEM_FreeItem(pkcs8Key, PR_TRUE);
+    SECITEM_FreeItem(decoded, PR_TRUE);
+    return SECFailure;
+}
+
+static SECStatus
+configureEch(PRFileDesc *model_sock)
+{
+    if (!PORT_Strncmp(echParamsStr, "publicname:", PORT_Strlen("publicname:"))) {
+        return configureEchWithPublicName(model_sock,
+                                          &echParamsStr[PORT_Strlen("publicname:")]);
+    }
+    return configureEchWithData(model_sock);
+}
+
 void
 server_main(
     PRFileDesc *listen_sock,
     SECKEYPrivateKey **privKey,
     CERTCertificate **cert,
     const char *expectedHostNameVal)
 {
     int i;
@@ -2084,16 +2283,23 @@ server_main(
 
     if (psk.data) {
         rv = importPsk(model_sock);
         if (rv != SECSuccess) {
             errExit("importPsk failed");
         }
     }
 
+    if (echParamsStr) {
+        rv = configureEch(model_sock);
+        if (rv != SECSuccess) {
+            errExit("configureEch failed");
+        }
+    }
+
     if (MakeCertOK)
         SSL_BadCertHook(model_sock, myBadCertHandler, NULL);
 
     /* end of ssl configuration. */
 
     /* Now, do the accepting, here in the main thread. */
     rv = do_accepts(listen_sock, model_sock);
 
@@ -2327,17 +2533,17 @@ main(int argc, char **argv)
     PR_Init(PR_SYSTEM_THREAD, PR_PRIORITY_NORMAL, 1);
     SSL_VersionRangeGetSupported(ssl_variant_stream, &enabledVersions);
 
     /* please keep this list of options in ASCII collating sequence.
     ** numbers, then capital letters, then lower case, alphabetical.
     ** XXX: 'B', and 'q' were used in the past but removed
     **      in 3.28, please leave some time before resuing those. */
     optstate = PL_CreateOptState(argc, argv,
-                                 "2:A:C:DEGH:I:J:L:M:NP:QRS:T:U:V:W:YZa:bc:d:e:f:g:hi:jk:lmn:op:rst:uvw:x:yz:");
+                                 "2:A:C:DEGH:I:J:L:M:NP:QRS:T:U:V:W:X:YZa:bc:d:e:f:g:hi:jk:lmn:op:rst:uvw:x:yz:");
     while ((status = PL_GetNextOpt(optstate)) == PL_OPT_OK) {
         ++optionsFound;
         switch (optstate->option) {
             case '2':
                 fileName = optstate->value;
                 break;
 
             case 'A':
@@ -2594,19 +2800,27 @@ main(int argc, char **argv)
                 if (rv != SECSuccess) {
                     PL_DestroyOptState(optstate);
                     fprintf(stderr, "Bad exporter specified.\n");
                     fprintf(stderr, "Run '%s -h' for usage information.\n", progName);
                     exit(5);
                 }
                 break;
 
+            case 'X':
+                echParamsStr = PORT_Strdup(optstate->value);
+                if (echParamsStr == NULL) {
+                    PL_DestroyOptState(optstate);
+                    fprintf(stderr, "echParamsStr copy failed.\n");
+                    exit(5);
+                }
+                break;
             default:
             case '?':
-                fprintf(stderr, "Unrecognized or bad option specified.\n");
+                fprintf(stderr, "Unrecognized or bad option specified: %c\n", optstate->option);
                 fprintf(stderr, "Run '%s -h' for usage information.\n", progName);
                 exit(4);
                 break;
         }
     }
     PL_DestroyOptState(optstate);
     if (status == PL_OPT_BAD) {
         fprintf(stderr, "Unrecognized or bad option specified.\n");
@@ -2916,16 +3130,17 @@ cleanup:
     if (enabledGroups) {
         PORT_Free(enabledGroups);
     }
     if (antiReplay) {
         SSL_ReleaseAntiReplayContext(antiReplay);
     }
     SECITEM_ZfreeItem(&psk, PR_FALSE);
     SECITEM_ZfreeItem(&pskLabel, PR_FALSE);
+    PORT_Free(echParamsStr);
     if (NSS_Shutdown() != SECSuccess) {
         SECU_PrintError(progName, "NSS_Shutdown");
         if (loggerThread) {
             PR_JoinThread(loggerThread);
         }
         PR_Cleanup();
         exit(1);
     }
--- a/security/nss/coreconf/coreconf.dep
+++ b/security/nss/coreconf/coreconf.dep
@@ -5,8 +5,9 @@
 
 /*
  * A dummy header file that is a dependency for all the object files.
  * Used to force a full recompilation of NSS in Mozilla's Tinderbox
  * depend builds.  See comments in rules.mk.
  */
 
 #error "Do not include this header file."
+
--- a/security/nss/gtests/common/testvectors/hpke-vectors.h
+++ b/security/nss/gtests/common/testvectors/hpke-vectors.h
@@ -47,187 +47,197 @@ typedef struct hpke_vector_str {
 const hpke_vector kHpkeTestVectors[] = {
     // A.1. DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM, Base mode
     {0,
      static_cast<HpkeModeId>(0),
      static_cast<HpkeKemId>(32),
      static_cast<HpkeKdfId>(1),
      static_cast<HpkeAeadId>(1),
      "4f6465206f6e2061204772656369616e2055726e",
-     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a"
-     "02010104208c490e5b0c7dbe0c6d2192484d2b7a0423b3b4544f2481095a9"
-     "9dbf238fb350fa1230321008a07563949fac6232936ed6f36c4fa735930ecd"
-     "eaef6734e314aeac35a56fd0a",
-     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a"
-     "02010104205a8aa0d2476b28521588e0c704b14db82cdd4970d340d293a957"
-     "6deaee9ec1c7a1230321008756e2580c07c1d2ffcb662f5fadc6d6ff13da85"
-     "abd7adfecf984aaa102c1269",
+     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a02010104206"
+     "cee2e2755790708a2a1be22667883a5e3f9ec52810404a0d889a0ed3e28de00a123032100"
+     "950897e0d37a8bdb0f2153edf5fa580a64b399c39fbb3d014f80983352a63617",
+     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a0201010420e"
+     "caf25b8485bcf40b9f013dbb96a6230f25733b8435bba0997a1dedbc7f78806a123032100"
+     "a5912b20892e36905bac635267e2353d58f8cc7525271a2bf57b9c48d2ec2c07",
      "",
      "",
-     "8a07563949fac6232936ed6f36c4fa735930ecdeaef6734e314aeac35a56fd0a",
-     "550ee0b7ec1ea2532f2e2bac87040a4c",
-     "2b855847756795a57229559a",
+     "950897e0d37a8bdb0f2153edf5fa580a64b399c39fbb3d014f80983352a63617",
+     "e20cee1bf5392ad2d3a442e231f187ae",
+     "5d99b2f03c452f7a9441933a",
      {// Encryptions
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d30",
-       "971ba65db526758ea30ae748cd769bc8d90579b62a037816057f24ce4274"
-       "16bd47c05ed1c2446ac8e19ec9ae79"},
+       "9418f1ae06eddc43aa911032aed4a951754ee2286a786733761857f8d96a7ec8d852da9"
+       "3bc5eeab49623344aba"},
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d31",
-       "f18f1ec397667ca069b9a6ee0bebf0890cd5caa34bb9875b3600ca0142cb"
-       "a774dd35f2aafd79a02a08ca5f2806"},
+       "74d69c61899b9158bb50e95d92fbad106f612ea67c61b3c4bef65c8bf3dc18e17bf41ec"
+       "4c408688aae58358d0e"},
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d32",
-       "51a8dea350fe6e753f743ec17c956de4cbdfa35f3018fc6a12752c51d137"
-       "2c5093959f18c7253da9c953c6cfbe"}},
+       "e6602db9be05d81c4ab8fa621bc35993a7b759851075a34b3bffd257340011c70c9fa1f"
+       "5c11868a076fc3adb3b"}},
      {// Exports
-      {"436f6e746578742d30", 32,
-       "0df04ac640d34a56561419bab20a68e6b7331070208004f89c7b973f4c47"
-       "2e92"},
-      {"436f6e746578742d31", 32,
-       "723c2c8f80e6b827e72bd8e80973a801a05514afe3d4bc46e82e505dceb9"
-       "53aa"},
-      {"436f6e746578742d32", 32,
-       "38010c7d5d81093a11b55e2403a258e9a195bcf066817b332dd996b0a9bc"
-       "bc9a"},
-      {"436f6e746578742d33", 32,
-       "ebf6ab4c3186131de9b2c3c0bc3e2ad21dfcbc4efaf050cd0473f5b1535a"
-       "8b6d"},
-      {"436f6e746578742d34", 32,
-       "c4823eeb3efd2d5216b2d3b16e542bf57470dc9b9ea9af6bce85b151a358"
-       "9d90"}}},
+      {"", 32,
+       "be82c06bd83fd6edd74385de5a70859b9e03def4c7bb224a10cfae86087f8a25"},
+      {"00", 32,
+       "82cbfd3c2b2db75e2311d457e569cf12b6387eb4309bca8e77adb2f2b599fc85"},
+      {"54657374436f6e74657874", 32,
+       "c8387c1e6ec4f026c7f3577e3f29df51f46161295eec84c4f64a9174f7b64e4f"}}},
 
     // A.1. DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM, PSK mode
     {1,
      static_cast<HpkeModeId>(1),
      static_cast<HpkeKemId>(32),
      static_cast<HpkeKdfId>(1),
      static_cast<HpkeAeadId>(1),
      "4f6465206f6e2061204772656369616e2055726e",
-     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a020"
-     "1010420e7d2b539792a48a24451303ccd0cfe77176b6cb06823c439edfd217458"
-     "a1398aa12303210008d39d3e7f9b586341b6004dafba9679d2bd9340066edb247"
-     "e3e919013efcd0f",
-     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a020"
-     "10104204b41ef269169090551fcea177ecdf622bca86d82298e21cd93119b804c"
-     "cc5eaba123032100a5c85773bed3a831e7096f7df4ff5d1d8bac48fc97bfac366"
-     "141efab91892a3a",
-     "5db3b80a81cb63ca59470c83414ef70a",
+     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a02010104204"
+     "c1feed23e15ec6a55b8457e0c0f42a3a1ab3ccc309b7cbb7ac6165fc657bd3ba123032100"
+     "f16fa9440b2cb36c855b4b82fb87e1c02ce656dd132f7a7aec739294b6912768",
+     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a02010104208"
+     "e5430f0d821407670e5e3f6eecc9f52b2cad27b15a5fad1f3d05359ae30d81ca123032100"
+     "13c789187a2dda71889e4b98dc5443624ae68f309cea91865561cfa207586e3a",
+     "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82",
      "456e6e796e20447572696e206172616e204d6f726961",
-     "08d39d3e7f9b586341b6004dafba9679d2bd9340066edb247e3e919013efcd0f",
-     "811e9b2d7a10f4f9d58786bf8a534ca6",
-     "b79b0c5a8c3808e238b10411",
+     "f16fa9440b2cb36c855b4b82fb87e1c02ce656dd132f7a7aec739294b6912768",
+     "70030b55bfb737d4f4355cf62302d281",
+     "746d5e6255902701c3e0b99f",
      {// Encryptions
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d30",
-       "fb68f911b4e4033d1547f646ea30c9cee987fb4b4a8c30918e5de6e96de32fc"
-       "63466f2fc05e09aeff552489741"},
+       "63f7ed3d99e625d4a7373982b5f04daf0c3dfff39cac4b38eeb9d5c225cc3183bdbc91a"
+       "053db9b195319cc8c45"},
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d31",
-       "85e7472fbb7e2341af35fb2a0795df9a85caa99a8f584056b11d452bc160470"
-       "672e297f9892ce2c5020e794ae1"},
+       "65e7160f80fdf47893a5abe1edcff46c85899f04acb97882e194ce6d4fceec2dc4cb2d3"
+       "abe5d969880722859b2"},
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d32",
-       "74229b7491102bcf94cf7633888bc48baa4e5a73cc544bfad4ff61585506fac"
-       "b44b359ade03c0b2b35c6430e4c"}},
+       "915e08e6e340fca64982e90ad93490826bfb74af8f48062212c87105dad2b7569c83688"
+       "e564ed5862592b77cdc"}},
      {// Exports
-      {"436f6e746578742d30", 32,
-       "bd292b132fae00243851451c3f3a87e9e11c3293c14d61b114b7e12e07245ffd"},
-      {"436f6e746578742d31", 32,
-       "695de26bc9336caee01cb04826f6e224f4d2108066ab17fc18f0c993dce05f24"},
-      {"436f6e746578742d32", 32,
-       "c53f26ef1bf4f5fd5469d807c418a0e103d035c76ccdbc6afb5bc42b24968f6c"},
-      {"436f6e746578742d33", 32,
-       "8cea4a595dfe3de84644ca8ea7ea9401a345f0db29bb4beebc2c471afc602ec4"},
-      {"436f6e746578742d34", 32,
-       "e6313f12f6c2054c69018f273211c54fcf2439d90173392eaa34b4caac929068"}}},
-
-    // A.2. DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305, Base mode
+      {"", 32,
+       "7c40ceb745e14d19fceeac6e4756c796957fe5ff28709198c3f8cbdb5d368fe1"},
+      {"00", 32,
+       "1ef0fd07bd40326f1b88f3545c92969cff202ca7186b9fd1315241f93fcc2edf"},
+      {"54657374436f6e74657874", 32,
+       "997368419db9490aa96c977cdd90bda8fd6234054d4add3d2f31aaaa2f8c1172"}}},
+    // A.2. DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305, Base
+    // mode
     {2,
      static_cast<HpkeModeId>(0),
      static_cast<HpkeKemId>(32),
      static_cast<HpkeKdfId>(1),
      static_cast<HpkeAeadId>(3),
      "4f6465206f6e2061204772656369616e2055726e",
-     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a020"
-     "10104205006a9a0f0138b9b5d577ed4a67c4f795aee8fc146ac63d7a4167765be"
-     "3ad7dca123032100716281787b035b2fee90455d951fa70b3db6cc92f13bedfd7"
-     "58c3487994b7020",
-     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a020"
-     "101042062139576dcbf9878ccd56262d1b28dbea897821c03370d81971513cc74"
-     "aea3ffa1230321001ae26f65041b36ad69eb392c198bfd33df1c6ff17a910cb3e"
-     "49db7506b6a4e7f",
+     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a0201010420e"
+     "fda8f0538ce6ab9f165aae26e02ad96dcb1775b248267174aeb3d140e002ee3a123032100"
+     "1440805f4e60cbd34835baf0813c3071d17def1dbd8c04e75889bb2271d7823a",
+     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a02010104201"
+     "4365bb26500e7cf263720c4ab04bd45b8e146b4f724facd1fa01d58b63975e4a123032100"
+     "26147d5c2978bccc3cc03a4f9ac607560b5d83f852be4e9024f2cb7207d4c30e",
      "",
      "",
-     "716281787b035b2fee90455d951fa70b3db6cc92f13bedfd758c3487994b7020",
-     "1d5e71e2885ddadbcc479798cc65ea74d308f2a9e99c0cc7fe480adce66b5722",
-     "8354a7fcfef97d4bbef6d24e",
+     "1440805f4e60cbd34835baf0813c3071d17def1dbd8c04e75889bb2271d7823a",
+     "a17448a542d0d6d75e3b21be0a1f68607904b4802c6b19a7e7e90976aa00a5c8",
+     "6f6b832dba944a91e5684514",
      {// Encryptions
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d30",
-       "fa4632a400962c98143e58450e75d879365359afca81a5f5b5997c6555647ec"
-       "302045a80c57d3e2c2abe7e1ced"},
+       "1b9ce69bd0e6b4242ac2dd841ef093fc9dfa9e684f81c2d1778fd3268ca5aa7d612cd87"
+       "f72acd2aeaee084dee2"},
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d31",
-       "8313fcbf760714f5a93b6864820e48dcec3ddd476ad4408ff1c1a1f7bfb8cb8"
-       "699fada4a9e59bf8086eb1c0635"},
+       "f041fb8de275b5319587269cb39190029906b9267eb5619b7bec8a5e0b3b3a0bead1696"
+       "17f2c4d45d028b1b654"},
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d32",
-       "020f2856d95b85e1def9549bf327c484d327616f1e213045f117be4c287571a"
-       "b983958f74766cbc6f8197c8d8d"}},
+       "0042c74002608a20e432ee9628e84cba76482aca29359e93d60067371be547355acca2c"
+       "271a2072b85a77a6237"}},
      {// Exports
-      {"436f6e746578742d30", 32,
-       "22bbe971392c685b55e13544cdaf976f36b89dc1dbe1296c2884971a5aa9e331"},
-      {"436f6e746578742d31", 32,
-       "5c0fa72053a2622d8999b726446db9ef743e725e2cb040afac2d83eae0d41981"},
-      {"436f6e746578742d32", 32,
-       "72b0f9999fd37ac2b948a07dadd01132587501a5a9460d596c1f7383299a2442"},
-      {"436f6e746578742d33", 32,
-       "73d2308ed5bdd63aacd236effa0db2d3a30742b6293a924d95a372e76d90486b"},
-      {"436f6e746578742d34", 32,
-       "d4f8878dbc471935e86cdee08746e53837bbb4b6013003bebb0bc1cc3e074085"}}},
-
-    // A.2. DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305, PSK mode
+      {"", 32,
+       "996dc6fda1dc47e687613e0e221d64a3598e1ead9585177d22f230716569c04d"},
+      {"00", 32,
+       "6d07b4e3e06ace3dc3f1b2a0826a0f896aa828769ff993c2e3829ae40325c27d"},
+      {"54657374436f6e74657874", 32,
+       "bb69068c4f7767331512d375e4ab0ca0c6c51446040096ea0ae1cc3f9a3f54bd"}}},
+    // A.2. DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305, PSK
+    // mode
     {3,
      static_cast<HpkeModeId>(1),
      static_cast<HpkeKemId>(32),
      static_cast<HpkeKdfId>(1),
      static_cast<HpkeAeadId>(3),
      "4f6465206f6e2061204772656369616e2055726e",
-     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a020"
-     "10104204bfdb62b95ae2a1f29f20ea49e24aa2673e0d240c6e967f668f55ed5de"
-     "e996dca123032100f4639297e3305b03d34dd5d86522ddc6ba11a608a0003670a"
-     "30734823cdd3763",
-     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a020"
-     "1010420a6ab4e1bb782d580d837843089d65ebe271a0ee9b5a951777cecf1293c"
-     "58c150a123032100c49b46ed73ecb7d3a6a3e44f54b8f00f9ab872b57dd79ded6"
-     "6d7231a14c64144",
-     "5db3b80a81cb63ca59470c83414ef70a",
+     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a0201010420d"
+     "b1c9dfba77e1e3b8687ea18af207cffca803bdd983f955376b8271ef9c78a46a123032100"
+     "8e4b29035c22b67b3a7a0f5a52f12b3ab17a9ae1f0c63b029137ba09f420224a",
+     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a02010104204"
+     "e335da3ec60e68c156586b8217de6801cb83b5a4de413645fcb112c00b2228ba123032100"
+     "94ea1227a357dfd3548aadb9ef19d9974add594871498e123390a8bcb4db5d51",
+     "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82",
      "456e6e796e20447572696e206172616e204d6f726961",
-     "f4639297e3305b03d34dd5d86522ddc6ba11a608a0003670a30734823cdd3763",
-     "396c06a52b39d0930594aa2c6944561cc1741f638557a12bef1c1cad349157c9",
-     "baa4ecf96b5d6d536d0d7210",
+     "8e4b29035c22b67b3a7a0f5a52f12b3ab17a9ae1f0c63b029137ba09f420224a",
+     "a603fe0f9897dc6ce042a467d6bd430a01cd679e930f1b5706ad425e4153496d",
+     "318e48afae42913a928146e6",
      {// Encryptions
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d30",
-       "f97ca72675b8199e8ffec65b4c200d901110b177b246f241b6f9716fb60b35b"
-       "32a6d452675534b591e8141468a"},
+       "c87f8158a501c7a2f31708bbdba10f9c5ad035624c3153eeb028e65b82f41f38cbe1cd9"
+       "aafb10e502d328b83c1"},
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d31",
-       "57796e2b9dd0ddf807f1a7cb5884dfc50e61468c4fd69fa03963731e51674ca"
-       "88fee94eeac3290734e1627ded6"},
+       "aef7a0b0e3a58b177dac9628439b44d1e706724e265ab3b46d791612b51637342479ad9"
+       "45607b8b54112bd8c86"},
       {"4265617574792069732074727574682c20747275746820626561757479",
        "436f756e742d32",
-       "b514150af1057151687d0036a9b4a3ad50fb186253f839d8433622baa85719e"
-       "d5d2532017a0ce7b9ca0007f276"}},
+       "c00884a5c658213bd4381d65b54d93682692fef9408a6e437a97a904267727269b242d3"
+       "d81725ad8f0c764e082"}},
      {// Exports
-      {"436f6e746578742d30", 32,
-       "735400cd9b9193daffe840f412074728ade6b1978e9ae27957aacd588dbd7c9e"},
-      {"436f6e746578742d31", 32,
-       "cf4e351e1943d171ff2d88726f18160086ecbec52a8151dba8cf5ba0737a6097"},
-      {"436f6e746578742d32", 32,
-       "8e23b44d4f23dd906d1c100580a670d171132c9786212c4ca2876a1541a84fae"},
-      {"436f6e746578742d33", 32,
-       "56252a940ece53d4013eb619b444ee1d019a08eec427ded2b6dbf24be624a4a0"},
-      {"436f6e746578742d34", 32,
-       "fc6cdca9ce8ab062401478ffd16ee1c07e2b15d7c781d4227f07c6043d937fad"}}}};
-
+      {"", 32,
+       "23c31ee2757bbecf105f74c90bf1e640b6ddc545dc8d80b1abbf2aa9dd1786ce"},
+      {"00", 32,
+       "05af7597519945fe8443f7cb84cdb651a8dd18cd7bbbd65d31095d3c69c1257e"},
+      {"54657374436f6e74657874", 32,
+       "5814619f842c7c328c9657854154e51b581c7bbd3b646bd773be67f93900a109"}}},
+    // DHKEM(X25519, HKDF-SHA256), HKDF-SHA512, ChaCha20Poly1305, Base mode
+    // Tests KEM.hash != KDF.hash.
+    {4,
+     static_cast<HpkeModeId>(0),
+     static_cast<HpkeKemId>(32),
+     static_cast<HpkeKdfId>(3),
+     static_cast<HpkeAeadId>(3),
+     "4f6465206f6e2061204772656369616e2055726e",
+     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a02010104200"
+     "6e74abcc8b65671d1ef4a6cb273662c6a3b3ff6590852bfebc7bc94887f5c4ea123032100"
+     "de2746f66f3e14a3389f570e8f8cc1de4e39a89d1cbb445fad711d7acf407e15",
+     "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a02010104202"
+     "dc14b2f31b233963f0a2d9a836072f29666fdea84a5893d30254deb9183e0a9a123032100"
+     "318f92c9e96142c4ce9a06ea04f7099698ee4160044f2db585d9e2b02abd6041",
+     "",
+     "",
+     "de2746f66f3e14a3389f570e8f8cc1de4e39a89d1cbb445fad711d7acf407e15",
+     "4a54adb318d8a420506b0473815a32c2b1923a936fa7c735c8a038a38fcc80d2",
+     "9c6d83a59628e7327d19a3d8",
+     {// Encryptions
+      {"4265617574792069732074727574682c20747275746820626561757479",
+       "436f756e742d30",
+       "59cbc98df2d7640598377e3184e07c008dea1c264c72a8414028715960ab6d6909a3110"
+       "e633a23baf8b9b5e2f1"},
+      {"4265617574792069732074727574682c20747275746820626561757479",
+       "436f756e742d31",
+       "c9a4b68ea349eea9fdf499f7577c9325e9b76f24308a81ac5dfbbee3489dd41c85d7fb3"
+       "5e585859ea5c790f155"},
+      {"4265617574792069732074727574682c20747275746820626561757479",
+       "436f756e742d32",
+       "5d9f717b192b43bea1f6bd25ee63d7b88b06019132c31a4e262a4c1d4f01c7bd70d00df"
+       "0e2f858cf654ae86447"}},
+     {// Exports
+      {"", 32,
+       "97b0ac016b9dedb5f115cf6fd24b927f8e75b48a2ab6069efe7fec6a18ff4272"},
+      {"00", 32,
+       "c994b47854104e476d9e47bb15f9fb66f4879f68bc89a4cfccc259e80a30c913"},
+      {"54657374436f6e74657874", 32,
+       "9199e5beeda45397b1bbee3dd13ad1afbd2963f83d9e5ebdf1e23b6c7e012317"}}},
+};
 #endif  // hpke_vectors_h__
--- a/security/nss/gtests/pk11_gtest/pk11_hpke_unittest.cc
+++ b/security/nss/gtests/pk11_gtest/pk11_hpke_unittest.cc
@@ -17,46 +17,18 @@
 #include "util.h"
 
 namespace nss_test {
 
 /* See note in pk11pub.h. */
 #ifdef NSS_ENABLE_DRAFT_HPKE
 #include "cpputil.h"
 
-class Pkcs11HpkeTest : public ::testing::TestWithParam<hpke_vector> {
+class HpkeTest {
  protected:
-  void ReadVector(const hpke_vector &vec) {
-    ScopedPK11SymKey vec_psk;
-    if (!vec.psk.empty()) {
-      ASSERT_FALSE(vec.psk_id.empty());
-      vec_psk_id = hex_string_to_bytes(vec.psk_id);
-
-      std::vector<uint8_t> psk_bytes = hex_string_to_bytes(vec.psk);
-      SECItem psk_item = {siBuffer, toUcharPtr(psk_bytes.data()),
-                          static_cast<unsigned int>(psk_bytes.size())};
-      ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
-      ASSERT_TRUE(slot);
-      PK11SymKey *psk_key =
-          PK11_ImportSymKey(slot.get(), CKM_HKDF_KEY_GEN, PK11_OriginUnwrap,
-                            CKA_WRAP, &psk_item, nullptr);
-      ASSERT_NE(nullptr, psk_key);
-      vec_psk_key.reset(psk_key);
-    }
-
-    vec_pkcs8_r = hex_string_to_bytes(vec.pkcs8_r);
-    vec_pkcs8_e = hex_string_to_bytes(vec.pkcs8_e);
-    vec_key = hex_string_to_bytes(vec.key);
-    vec_nonce = hex_string_to_bytes(vec.nonce);
-    vec_enc = hex_string_to_bytes(vec.enc);
-    vec_info = hex_string_to_bytes(vec.info);
-    vec_encryptions = vec.encrypt_vecs;
-    vec_exports = vec.export_vecs;
-  }
-
   void CheckEquality(const std::vector<uint8_t> &expected, SECItem *actual) {
     if (!actual) {
       EXPECT_TRUE(expected.empty());
       return;
     }
     std::vector<uint8_t> vact(actual->data, actual->data + actual->len);
     EXPECT_EQ(expected, vact);
   }
@@ -97,184 +69,289 @@ class Pkcs11HpkeTest : public ::testing:
     }
     SECItem *raw = PK11_GetKeyData(expected);
     ASSERT_NE(nullptr, raw);
     ASSERT_NE(nullptr, raw->data);
     std::vector<uint8_t> expected_vec(raw->data, raw->data + raw->len);
     CheckEquality(expected_vec, actual);
   }
 
-  void SetupS(const ScopedHpkeContext &cx, const ScopedSECKEYPublicKey &pkE,
-              const ScopedSECKEYPrivateKey &skE,
-              const ScopedSECKEYPublicKey &pkR,
-              const std::vector<uint8_t> &info) {
-    SECItem info_item = {siBuffer, toUcharPtr(vec_info.data()),
-                         static_cast<unsigned int>(vec_info.size())};
-    SECStatus rv =
-        PK11_HPKE_SetupS(cx.get(), pkE.get(), skE.get(), pkR.get(), &info_item);
-    EXPECT_EQ(SECSuccess, rv);
-  }
-
-  void SetupR(const ScopedHpkeContext &cx, const ScopedSECKEYPublicKey &pkR,
-              const ScopedSECKEYPrivateKey &skR,
-              const std::vector<uint8_t> &enc,
-              const std::vector<uint8_t> &info) {
-    SECItem enc_item = {siBuffer, toUcharPtr(enc.data()),
-                        static_cast<unsigned int>(enc.size())};
-    SECItem info_item = {siBuffer, toUcharPtr(vec_info.data()),
-                         static_cast<unsigned int>(vec_info.size())};
-    SECStatus rv =
-        PK11_HPKE_SetupR(cx.get(), pkR.get(), skR.get(), &enc_item, &info_item);
-    EXPECT_EQ(SECSuccess, rv);
-  }
-
   void Seal(const ScopedHpkeContext &cx, std::vector<uint8_t> &aad_vec,
-            std::vector<uint8_t> &pt_vec, SECItem **out_ct) {
+            std::vector<uint8_t> &pt_vec, std::vector<uint8_t> &out_sealed) {
     SECItem aad_item = {siBuffer, toUcharPtr(aad_vec.data()),
                         static_cast<unsigned int>(aad_vec.size())};
     SECItem pt_item = {siBuffer, toUcharPtr(pt_vec.data()),
                        static_cast<unsigned int>(pt_vec.size())};
 
-    SECStatus rv = PK11_HPKE_Seal(cx.get(), &aad_item, &pt_item, out_ct);
-    EXPECT_EQ(SECSuccess, rv);
+    SECItem *sealed_item = nullptr;
+    EXPECT_EQ(SECSuccess,
+              PK11_HPKE_Seal(cx.get(), &aad_item, &pt_item, &sealed_item));
+    ASSERT_NE(nullptr, sealed_item);
+    ScopedSECItem sealed(sealed_item);
+    out_sealed.assign(sealed->data, sealed->data + sealed->len);
   }
 
   void Open(const ScopedHpkeContext &cx, std::vector<uint8_t> &aad_vec,
-            std::vector<uint8_t> &ct_vec, SECItem **out_pt) {
+            std::vector<uint8_t> &ct_vec, std::vector<uint8_t> &out_opened) {
     SECItem aad_item = {siBuffer, toUcharPtr(aad_vec.data()),
                         static_cast<unsigned int>(aad_vec.size())};
     SECItem ct_item = {siBuffer, toUcharPtr(ct_vec.data()),
                        static_cast<unsigned int>(ct_vec.size())};
-    SECStatus rv = PK11_HPKE_Open(cx.get(), &aad_item, &ct_item, out_pt);
-    EXPECT_EQ(SECSuccess, rv);
+    SECItem *opened_item = nullptr;
+    EXPECT_EQ(SECSuccess,
+              PK11_HPKE_Open(cx.get(), &aad_item, &ct_item, &opened_item));
+    ASSERT_NE(nullptr, opened_item);
+    ScopedSECItem opened(opened_item);
+    out_opened.assign(opened->data, opened->data + opened->len);
+  }
+
+  void SealOpen(const ScopedHpkeContext &sender,
+                const ScopedHpkeContext &receiver, std::vector<uint8_t> &msg,
+                std::vector<uint8_t> &aad, const std::vector<uint8_t> *expect) {
+    std::vector<uint8_t> sealed;
+    std::vector<uint8_t> opened;
+    Seal(sender, aad, msg, sealed);
+    if (expect) {
+      EXPECT_EQ(*expect, sealed);
+    }
+    Open(receiver, aad, sealed, opened);
+    EXPECT_EQ(msg, opened);
+  }
+
+  void ExportSecret(const ScopedHpkeContext &receiver,
+                    ScopedPK11SymKey &exported) {
+    std::vector<uint8_t> context = {'c', 't', 'x', 't'};
+    SECItem context_item = {siBuffer, context.data(),
+                            static_cast<unsigned int>(context.size())};
+    PK11SymKey *tmp_exported = nullptr;
+    ASSERT_EQ(SECSuccess, PK11_HPKE_ExportSecret(receiver.get(), &context_item,
+                                                 64, &tmp_exported));
+    exported.reset(tmp_exported);
+  }
+
+  void ExportImportRecvContext(ScopedHpkeContext &scoped_cx,
+                               PK11SymKey *wrapping_key) {
+    SECItem *tmp_exported = nullptr;
+    EXPECT_EQ(SECSuccess, PK11_HPKE_ExportContext(scoped_cx.get(), wrapping_key,
+                                                  &tmp_exported));
+    EXPECT_NE(nullptr, tmp_exported);
+    ScopedSECItem context(tmp_exported);
+    scoped_cx.reset();
+
+    HpkeContext *tmp_imported =
+        PK11_HPKE_ImportContext(context.get(), wrapping_key);
+    EXPECT_NE(nullptr, tmp_imported);
+    scoped_cx.reset(tmp_imported);
+  }
+
+  bool GenerateKeyPair(ScopedSECKEYPublicKey &pub_key,
+                       ScopedSECKEYPrivateKey &priv_key) {
+    ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
+    if (!slot) {
+      ADD_FAILURE() << "Couldn't get slot";
+      return false;
+    }
+
+    unsigned char param_buf[65];
+    SECItem ecdsa_params = {siBuffer, param_buf, sizeof(param_buf)};
+    SECOidData *oid_data = SECOID_FindOIDByTag(SEC_OID_CURVE25519);
+    if (!oid_data) {
+      ADD_FAILURE() << "Couldn't get oid_data";
+      return false;
+    }
+    ecdsa_params.data[0] = SEC_ASN1_OBJECT_ID;
+    ecdsa_params.data[1] = oid_data->oid.len;
+    memcpy(ecdsa_params.data + 2, oid_data->oid.data, oid_data->oid.len);
+    ecdsa_params.len = oid_data->oid.len + 2;
+
+    SECKEYPublicKey *pub_tmp;
+    SECKEYPrivateKey *priv_tmp;
+    priv_tmp =
+        PK11_GenerateKeyPair(slot.get(), CKM_EC_KEY_PAIR_GEN, &ecdsa_params,
+                             &pub_tmp, PR_FALSE, PR_TRUE, nullptr);
+    if (!pub_tmp || !priv_tmp) {
+      ADD_FAILURE() << "PK11_GenerateKeyPair failed";
+      return false;
+    }
+
+    pub_key.reset(pub_tmp);
+    priv_key.reset(priv_tmp);
+    return true;
+  }
+
+  void SetUpEphemeralContexts(ScopedHpkeContext &sender,
+                              ScopedHpkeContext &receiver,
+                              HpkeModeId mode = HpkeModeBase,
+                              HpkeKemId kem = HpkeDhKemX25519Sha256,
+                              HpkeKdfId kdf = HpkeKdfHkdfSha256,
+                              HpkeAeadId aead = HpkeAeadAes128Gcm) {
+    // Generate a PSK, if the mode calls for it.
+    PRUint8 psk_id_buf[] = {'p', 's', 'k', '-', 'i', 'd'};
+    SECItem psk_id = {siBuffer, psk_id_buf, sizeof(psk_id_buf)};
+    SECItem *psk_id_item = (mode == HpkeModePsk) ? &psk_id : nullptr;
+    ScopedPK11SymKey psk;
+    if (mode == HpkeModePsk) {
+      ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
+      ASSERT_TRUE(slot);
+      PK11SymKey *tmp_psk =
+          PK11_KeyGen(slot.get(), CKM_HKDF_DERIVE, nullptr, 16, nullptr);
+      ASSERT_NE(nullptr, tmp_psk);
+      psk.reset(tmp_psk);
+    }
+
+    std::vector<uint8_t> info = {'t', 'e', 's', 't', '-', 'i', 'n', 'f', 'o'};
+    SECItem info_item = {siBuffer, info.data(),
+                         static_cast<unsigned int>(info.size())};
+    sender.reset(PK11_HPKE_NewContext(kem, kdf, aead, psk.get(), psk_id_item));
+    receiver.reset(
+        PK11_HPKE_NewContext(kem, kdf, aead, psk.get(), psk_id_item));
+    ASSERT_TRUE(sender);
+    ASSERT_TRUE(receiver);
+
+    ScopedSECKEYPublicKey pub_key_r;
+    ScopedSECKEYPrivateKey priv_key_r;
+    ASSERT_TRUE(GenerateKeyPair(pub_key_r, priv_key_r));
+    EXPECT_EQ(SECSuccess, PK11_HPKE_SetupS(sender.get(), nullptr, nullptr,
+                                           pub_key_r.get(), &info_item));
+
+    const SECItem *enc = PK11_HPKE_GetEncapPubKey(sender.get());
+    EXPECT_NE(nullptr, enc);
+    EXPECT_EQ(SECSuccess, PK11_HPKE_SetupR(
+                              receiver.get(), pub_key_r.get(), priv_key_r.get(),
+                              const_cast<SECItem *>(enc), &info_item));
+  }
+};
+
+class TestVectors : public HpkeTest,
+                    public ::testing::TestWithParam<hpke_vector> {
+ protected:
+  void ReadVector(const hpke_vector &vec) {
+    ScopedPK11SymKey vec_psk;
+    if (!vec.psk.empty()) {
+      ASSERT_FALSE(vec.psk_id.empty());
+      vec_psk_id = hex_string_to_bytes(vec.psk_id);
+
+      std::vector<uint8_t> psk_bytes = hex_string_to_bytes(vec.psk);
+      SECItem psk_item = {siBuffer, toUcharPtr(psk_bytes.data()),
+                          static_cast<unsigned int>(psk_bytes.size())};
+      ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
+      ASSERT_TRUE(slot);
+      PK11SymKey *psk_key =
+          PK11_ImportSymKey(slot.get(), CKM_HKDF_KEY_GEN, PK11_OriginUnwrap,
+                            CKA_WRAP, &psk_item, nullptr);
+      ASSERT_NE(nullptr, psk_key);
+      vec_psk_key.reset(psk_key);
+    }
+
+    vec_pkcs8_r = hex_string_to_bytes(vec.pkcs8_r);
+    vec_pkcs8_e = hex_string_to_bytes(vec.pkcs8_e);
+    vec_key = hex_string_to_bytes(vec.key);
+    vec_nonce = hex_string_to_bytes(vec.nonce);
+    vec_enc = hex_string_to_bytes(vec.enc);
+    vec_info = hex_string_to_bytes(vec.info);
+    vec_encryptions = vec.encrypt_vecs;
+    vec_exports = vec.export_vecs;
   }
 
   void TestExports(const ScopedHpkeContext &sender,
                    const ScopedHpkeContext &receiver) {
-    SECStatus rv;
-
     for (auto &vec : vec_exports) {
       std::vector<uint8_t> context = hex_string_to_bytes(vec.ctxt);
       std::vector<uint8_t> expected = hex_string_to_bytes(vec.exported);
       SECItem context_item = {siBuffer, toUcharPtr(context.data()),
                               static_cast<unsigned int>(context.size())};
       PK11SymKey *actual_r = nullptr;
       PK11SymKey *actual_s = nullptr;
-      rv = PK11_HPKE_ExportSecret(sender.get(), &context_item, vec.len,
-                                  &actual_s);
-      ASSERT_EQ(SECSuccess, rv);
-      rv = PK11_HPKE_ExportSecret(receiver.get(), &context_item, vec.len,
-                                  &actual_r);
-      ASSERT_EQ(SECSuccess, rv);
+      ASSERT_EQ(SECSuccess, PK11_HPKE_ExportSecret(sender.get(), &context_item,
+                                                   vec.len, &actual_s));
+      ASSERT_EQ(SECSuccess,
+                PK11_HPKE_ExportSecret(receiver.get(), &context_item, vec.len,
+                                       &actual_r));
       ScopedPK11SymKey scoped_act_s(actual_s);
       ScopedPK11SymKey scoped_act_r(actual_r);
       CheckEquality(expected, scoped_act_s.get());
       CheckEquality(expected, scoped_act_r.get());
     }
   }
 
   void TestEncryptions(const ScopedHpkeContext &sender,
                        const ScopedHpkeContext &receiver) {
     for (auto &enc_vec : vec_encryptions) {
       std::vector<uint8_t> msg = hex_string_to_bytes(enc_vec.pt);
       std::vector<uint8_t> aad = hex_string_to_bytes(enc_vec.aad);
       std::vector<uint8_t> expect_ct = hex_string_to_bytes(enc_vec.ct);
-      SECItem *act_ct = nullptr;
-      Seal(sender, aad, msg, &act_ct);
-      CheckEquality(expect_ct, act_ct);
-      ScopedSECItem scoped_ct(act_ct);
-
-      SECItem *act_pt = nullptr;
-      Open(receiver, aad, expect_ct, &act_pt);
-      CheckEquality(msg, act_pt);
-      ScopedSECItem scoped_pt(act_pt);
+      SealOpen(sender, receiver, msg, aad, &expect_ct);
     }
   }
 
   void ImportKeyPairs(const ScopedHpkeContext &sender,
                       const ScopedHpkeContext &receiver) {
     ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
     if (!slot) {
       ADD_FAILURE() << "No slot";
       return;
     }
 
     SECItem pkcs8_e_item = {siBuffer, toUcharPtr(vec_pkcs8_e.data()),
                             static_cast<unsigned int>(vec_pkcs8_e.size())};
     SECKEYPrivateKey *sk_e = nullptr;
-    SECStatus rv = PK11_ImportDERPrivateKeyInfoAndReturnKey(
-        slot.get(), &pkcs8_e_item, nullptr, nullptr, false, false, KU_ALL,
-        &sk_e, nullptr);
-    EXPECT_EQ(SECSuccess, rv);
+    EXPECT_EQ(SECSuccess, PK11_ImportDERPrivateKeyInfoAndReturnKey(
+                              slot.get(), &pkcs8_e_item, nullptr, nullptr,
+                              false, false, KU_ALL, &sk_e, nullptr));
     skE_derived.reset(sk_e);
     SECKEYPublicKey *pk_e = SECKEY_ConvertToPublicKey(skE_derived.get());
     ASSERT_NE(nullptr, pk_e);
     pkE_derived.reset(pk_e);
 
     SECItem pkcs8_r_item = {siBuffer, toUcharPtr(vec_pkcs8_r.data()),
                             static_cast<unsigned int>(vec_pkcs8_r.size())};
     SECKEYPrivateKey *sk_r = nullptr;
-    rv = PK11_ImportDERPrivateKeyInfoAndReturnKey(
-        slot.get(), &pkcs8_r_item, nullptr, nullptr, false, false, KU_ALL,
-        &sk_r, nullptr);
-    EXPECT_EQ(SECSuccess, rv);
+    EXPECT_EQ(SECSuccess, PK11_ImportDERPrivateKeyInfoAndReturnKey(
+                              slot.get(), &pkcs8_r_item, nullptr, nullptr,
+                              false, false, KU_ALL, &sk_r, nullptr));
     skR_derived.reset(sk_r);
     SECKEYPublicKey *pk_r = SECKEY_ConvertToPublicKey(skR_derived.get());
     ASSERT_NE(nullptr, pk_r);
     pkR_derived.reset(pk_r);
   }
 
+  void SetupS(const ScopedHpkeContext &cx, const ScopedSECKEYPublicKey &pkE,
+              const ScopedSECKEYPrivateKey &skE,
+              const ScopedSECKEYPublicKey &pkR,
+              const std::vector<uint8_t> &info) {
+    SECItem info_item = {siBuffer, toUcharPtr(vec_info.data()),
+                         static_cast<unsigned int>(vec_info.size())};
+    EXPECT_EQ(SECSuccess, PK11_HPKE_SetupS(cx.get(), pkE.get(), skE.get(),
+                                           pkR.get(), &info_item));
+  }
+
+  void SetupR(const ScopedHpkeContext &cx, const ScopedSECKEYPublicKey &pkR,
+              const ScopedSECKEYPrivateKey &skR,
+              const std::vector<uint8_t> &enc,
+              const std::vector<uint8_t> &info) {
+    SECItem enc_item = {siBuffer, toUcharPtr(enc.data()),
+                        static_cast<unsigned int>(enc.size())};
+    SECItem info_item = {siBuffer, toUcharPtr(vec_info.data()),
+                         static_cast<unsigned int>(vec_info.size())};
+    EXPECT_EQ(SECSuccess, PK11_HPKE_SetupR(cx.get(), pkR.get(), skR.get(),
+                                           &enc_item, &info_item));
+  }
+
   void SetupSenderReceiver(const ScopedHpkeContext &sender,
                            const ScopedHpkeContext &receiver) {
     SetupS(sender, pkE_derived, skE_derived, pkR_derived, vec_info);
     uint8_t buf[32];  // Curve25519 only, fixed size.
     SECItem encap_item = {siBuffer, const_cast<uint8_t *>(buf), sizeof(buf)};
-    SECStatus rv = PK11_HPKE_Serialize(pkE_derived.get(), encap_item.data,
-                                       &encap_item.len, encap_item.len);
-    ASSERT_EQ(SECSuccess, rv);
+    ASSERT_EQ(SECSuccess,
+              PK11_HPKE_Serialize(pkE_derived.get(), encap_item.data,
+                                  &encap_item.len, encap_item.len));
     CheckEquality(vec_enc, &encap_item);
     SetupR(receiver, pkR_derived, skR_derived, vec_enc, vec_info);
   }
 
-  bool GenerateKeyPair(ScopedSECKEYPublicKey &pub_key,
-                       ScopedSECKEYPrivateKey &priv_key) {
-    unsigned char param_buf[65];
-
-    ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
-    if (!slot) {
-      ADD_FAILURE() << "Couldn't get slot";
-      return false;
-    }
-
-    SECItem ecdsa_params = {siBuffer, param_buf, sizeof(param_buf)};
-    SECOidData *oid_data = SECOID_FindOIDByTag(SEC_OID_CURVE25519);
-    if (!oid_data) {
-      ADD_FAILURE() << "Couldn't get oid_data";
-      return false;
-    }
-
-    ecdsa_params.data[0] = SEC_ASN1_OBJECT_ID;
-    ecdsa_params.data[1] = oid_data->oid.len;
-    memcpy(ecdsa_params.data + 2, oid_data->oid.data, oid_data->oid.len);
-    ecdsa_params.len = oid_data->oid.len + 2;
-    SECKEYPublicKey *pub_tmp;
-    SECKEYPrivateKey *priv_tmp;
-    priv_tmp =
-        PK11_GenerateKeyPair(slot.get(), CKM_EC_KEY_PAIR_GEN, &ecdsa_params,
-                             &pub_tmp, PR_FALSE, PR_TRUE, nullptr);
-    if (!pub_tmp || !priv_tmp) {
-      ADD_FAILURE() << "PK11_GenerateKeyPair failed";
-      return false;
-    }
-
-    pub_key.reset(pub_tmp);
-    priv_key.reset(priv_tmp);
-    return true;
-  }
-
   void RunTestVector(const hpke_vector &vec) {
     ReadVector(vec);
     SECItem psk_id_item = {siBuffer, toUcharPtr(vec_psk_id.data()),
                            static_cast<unsigned int>(vec_psk_id.size())};
     PK11SymKey *psk = vec_psk_key ? vec_psk_key.get() : nullptr;
     SECItem *psk_id = psk ? &psk_id_item : nullptr;
 
     ScopedHpkeContext sender(
@@ -302,22 +379,41 @@ class Pkcs11HpkeTest : public ::testing:
   std::vector<hpke_encrypt_vector> vec_encryptions;
   std::vector<hpke_export_vector> vec_exports;
   ScopedSECKEYPublicKey pkE_derived;
   ScopedSECKEYPublicKey pkR_derived;
   ScopedSECKEYPrivateKey skE_derived;
   ScopedSECKEYPrivateKey skR_derived;
 };
 
-TEST_P(Pkcs11HpkeTest, TestVectors) { RunTestVector(GetParam()); }
+TEST_P(TestVectors, TestVectors) { RunTestVector(GetParam()); }
 
-INSTANTIATE_TEST_SUITE_P(Pkcs11HpkeTests, Pkcs11HpkeTest,
+INSTANTIATE_TEST_SUITE_P(Pk11Hpke, TestVectors,
                          ::testing::ValuesIn(kHpkeTestVectors));
 
-TEST_F(Pkcs11HpkeTest, BadEncapsulatedPubKey) {
+class ModeParameterizedTest
+    : public HpkeTest,
+      public ::testing::TestWithParam<
+          std::tuple<HpkeModeId, HpkeKemId, HpkeKdfId, HpkeAeadId>> {};
+
+static const HpkeModeId kHpkeModesAll[] = {HpkeModeBase, HpkeModePsk};
+static const HpkeKemId kHpkeKemIdsAll[] = {HpkeDhKemX25519Sha256};
+static const HpkeKdfId kHpkeKdfIdsAll[] = {HpkeKdfHkdfSha256, HpkeKdfHkdfSha384,
+                                           HpkeKdfHkdfSha512};
+static const HpkeAeadId kHpkeAeadIdsAll[] = {HpkeAeadAes128Gcm,
+                                             HpkeAeadChaCha20Poly1305};
+
+INSTANTIATE_TEST_SUITE_P(
+    Pk11Hpke, ModeParameterizedTest,
+    ::testing::Combine(::testing::ValuesIn(kHpkeModesAll),
+                       ::testing::ValuesIn(kHpkeKemIdsAll),
+                       ::testing::ValuesIn(kHpkeKdfIdsAll),
+                       ::testing::ValuesIn(kHpkeAeadIdsAll)));
+
+TEST_F(ModeParameterizedTest, BadEncapsulatedPubKey) {
   ScopedHpkeContext sender(
       PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256,
                            HpkeAeadAes128Gcm, nullptr, nullptr));
   ScopedHpkeContext receiver(
       PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256,
                            HpkeAeadAes128Gcm, nullptr, nullptr));
 
   SECItem empty = {siBuffer, nullptr, 0};
@@ -326,167 +422,221 @@ TEST_F(Pkcs11HpkeTest, BadEncapsulatedPu
   SECItem long_encap = {siBuffer, buf, sizeof(buf)};
 
   SECKEYPublicKey *tmp_pub_key;
   ScopedSECKEYPublicKey pub_key;
   ScopedSECKEYPrivateKey priv_key;
   ASSERT_TRUE(GenerateKeyPair(pub_key, priv_key));
 
   // Decapsulating an empty buffer should fail.
-  SECStatus rv =
-      PK11_HPKE_Deserialize(sender.get(), empty.data, empty.len, &tmp_pub_key);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure, PK11_HPKE_Deserialize(sender.get(), empty.data,
+                                              empty.len, &tmp_pub_key));
   EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
 
-  // Decapsulating anything else will succeed, but the setup will fail.
-  rv = PK11_HPKE_Deserialize(sender.get(), short_encap.data, short_encap.len,
-                             &tmp_pub_key);
+  // Decapsulating anything short will succeed, but the setup will fail.
+  EXPECT_EQ(SECSuccess, PK11_HPKE_Deserialize(sender.get(), short_encap.data,
+                                              short_encap.len, &tmp_pub_key));
   ScopedSECKEYPublicKey bad_pub_key(tmp_pub_key);
-  EXPECT_EQ(SECSuccess, rv);
 
-  rv = PK11_HPKE_SetupS(receiver.get(), pub_key.get(), priv_key.get(),
-                        bad_pub_key.get(), &empty);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure,
+            PK11_HPKE_SetupS(receiver.get(), pub_key.get(), priv_key.get(),
+                             bad_pub_key.get(), &empty));
   EXPECT_EQ(SEC_ERROR_INVALID_KEY, PORT_GetError());
 
   // Test the same for a receiver.
-  rv = PK11_HPKE_SetupR(sender.get(), pub_key.get(), priv_key.get(), &empty,
-                        &empty);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure, PK11_HPKE_SetupR(sender.get(), pub_key.get(),
+                                         priv_key.get(), &empty, &empty));
   EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
-
-  rv = PK11_HPKE_SetupR(sender.get(), pub_key.get(), priv_key.get(),
-                        &short_encap, &empty);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure, PK11_HPKE_SetupR(sender.get(), pub_key.get(),
+                                         priv_key.get(), &short_encap, &empty));
   EXPECT_EQ(SEC_ERROR_INVALID_KEY, PORT_GetError());
 
   // Encapsulated key too long
-  rv = PK11_HPKE_Deserialize(sender.get(), long_encap.data, long_encap.len,
-                             &tmp_pub_key);
+  EXPECT_EQ(SECSuccess, PK11_HPKE_Deserialize(sender.get(), long_encap.data,
+                                              long_encap.len, &tmp_pub_key));
   bad_pub_key.reset(tmp_pub_key);
-  EXPECT_EQ(SECSuccess, rv);
-
-  rv = PK11_HPKE_SetupS(receiver.get(), pub_key.get(), priv_key.get(),
-                        bad_pub_key.get(), &empty);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure,
+            PK11_HPKE_SetupS(receiver.get(), pub_key.get(), priv_key.get(),
+                             bad_pub_key.get(), &empty));
   EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
 
-  rv = PK11_HPKE_SetupR(sender.get(), pub_key.get(), priv_key.get(),
-                        &long_encap, &empty);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure, PK11_HPKE_SetupR(sender.get(), pub_key.get(),
+                                         priv_key.get(), &long_encap, &empty));
   EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
 }
 
-// Vectors used fixed keypairs on each end. Make sure the
-// ephemeral (particularly sender) path works.
-TEST_F(Pkcs11HpkeTest, EphemeralKeys) {
-  unsigned char info[] = {"info"};
-  unsigned char msg[] = {"secret"};
-  unsigned char aad[] = {"aad"};
-  SECItem info_item = {siBuffer, info, sizeof(info)};
-  SECItem msg_item = {siBuffer, msg, sizeof(msg)};
-  SECItem aad_item = {siBuffer, aad, sizeof(aad)};
+TEST_P(ModeParameterizedTest, ContextExportImportEncrypt) {
+  std::vector<uint8_t> msg = {'s', 'e', 'c', 'r', 'e', 't'};
+  std::vector<uint8_t> aad = {'a', 'a', 'd'};
+
+  ScopedHpkeContext sender;
+  ScopedHpkeContext receiver;
+  SetUpEphemeralContexts(sender, receiver, std::get<0>(GetParam()),
+                         std::get<1>(GetParam()), std::get<2>(GetParam()),
+                         std::get<3>(GetParam()));
+  SealOpen(sender, receiver, msg, aad, nullptr);
+  ExportImportRecvContext(receiver, nullptr);
+  SealOpen(sender, receiver, msg, aad, nullptr);
+}
+
+TEST_P(ModeParameterizedTest, ContextExportImportExport) {
+  ScopedHpkeContext sender;
+  ScopedHpkeContext receiver;
+  ScopedPK11SymKey sender_export;
+  ScopedPK11SymKey receiver_export;
+  ScopedPK11SymKey receiver_reexport;
+  SetUpEphemeralContexts(sender, receiver, std::get<0>(GetParam()),
+                         std::get<1>(GetParam()), std::get<2>(GetParam()),
+                         std::get<3>(GetParam()));
+  ExportSecret(sender, sender_export);
+  ExportSecret(receiver, receiver_export);
+  CheckEquality(sender_export.get(), receiver_export.get());
+  ExportImportRecvContext(receiver, nullptr);
+  ExportSecret(receiver, receiver_reexport);
+  CheckEquality(receiver_export.get(), receiver_reexport.get());
+}
+
+TEST_P(ModeParameterizedTest, ContextExportImportWithWrap) {
+  std::vector<uint8_t> msg = {'s', 'e', 'c', 'r', 'e', 't'};
+  std::vector<uint8_t> aad = {'a', 'a', 'd'};
+
+  // Generate a wrapping key, then use it for export.
+  ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
+  ASSERT_TRUE(slot);
+  ScopedPK11SymKey kek(
+      PK11_KeyGen(slot.get(), CKM_AES_CBC, nullptr, 16, nullptr));
+  ASSERT_NE(nullptr, kek);
+
+  ScopedHpkeContext sender;
+  ScopedHpkeContext receiver;
+  SetUpEphemeralContexts(sender, receiver, std::get<0>(GetParam()),
+                         std::get<1>(GetParam()), std::get<2>(GetParam()),
+                         std::get<3>(GetParam()));
+  SealOpen(sender, receiver, msg, aad, nullptr);
+  ExportImportRecvContext(receiver, kek.get());
+  SealOpen(sender, receiver, msg, aad, nullptr);
+}
+
+TEST_P(ModeParameterizedTest, ExportSenderContext) {
+  std::vector<uint8_t> msg = {'s', 'e', 'c', 'r', 'e', 't'};
+  std::vector<uint8_t> aad = {'a', 'a', 'd'};
 
-  ScopedHpkeContext sender(
-      PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256,
-                           HpkeAeadAes128Gcm, nullptr, nullptr));
-  ScopedHpkeContext receiver(
-      PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256,
-                           HpkeAeadAes128Gcm, nullptr, nullptr));
-  ASSERT_TRUE(sender);
-  ASSERT_TRUE(receiver);
+  ScopedHpkeContext sender;
+  ScopedHpkeContext receiver;
+  SetUpEphemeralContexts(sender, receiver, std::get<0>(GetParam()),
+                         std::get<1>(GetParam()), std::get<2>(GetParam()),
+                         std::get<3>(GetParam()));
+
+  SECItem *tmp_exported = nullptr;
+  EXPECT_EQ(SECFailure,
+            PK11_HPKE_ExportContext(sender.get(), nullptr, &tmp_exported));
+  EXPECT_EQ(nullptr, tmp_exported);
+  EXPECT_EQ(SEC_ERROR_NOT_A_RECIPIENT, PORT_GetError());
+}
 
-  ScopedSECKEYPublicKey pub_key_r;
-  ScopedSECKEYPrivateKey priv_key_r;
-  ASSERT_TRUE(GenerateKeyPair(pub_key_r, priv_key_r));
+TEST_P(ModeParameterizedTest, ContextUnwrapBadKey) {
+  std::vector<uint8_t> msg = {'s', 'e', 'c', 'r', 'e', 't'};
+  std::vector<uint8_t> aad = {'a', 'a', 'd'};
+
+  // Generate a wrapping key, then use it for export.
+  ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
+  ASSERT_TRUE(slot);
+  ScopedPK11SymKey kek(
+      PK11_KeyGen(slot.get(), CKM_AES_CBC, nullptr, 16, nullptr));
+  ASSERT_NE(nullptr, kek);
+  ScopedPK11SymKey not_kek(
+      PK11_KeyGen(slot.get(), CKM_AES_CBC, nullptr, 16, nullptr));
+  ASSERT_NE(nullptr, not_kek);
+  ScopedHpkeContext sender;
+  ScopedHpkeContext receiver;
 
-  SECStatus rv = PK11_HPKE_SetupS(sender.get(), nullptr, nullptr,
-                                  pub_key_r.get(), &info_item);
-  EXPECT_EQ(SECSuccess, rv);
+  SetUpEphemeralContexts(sender, receiver, std::get<0>(GetParam()),
+                         std::get<1>(GetParam()), std::get<2>(GetParam()),
+                         std::get<3>(GetParam()));
+
+  SECItem *tmp_exported = nullptr;
+  EXPECT_EQ(SECSuccess,
+            PK11_HPKE_ExportContext(receiver.get(), kek.get(), &tmp_exported));
+  EXPECT_NE(nullptr, tmp_exported);
+  ScopedSECItem context(tmp_exported);
+
+  EXPECT_EQ(nullptr, PK11_HPKE_ImportContext(context.get(), not_kek.get()));
+  EXPECT_EQ(SEC_ERROR_BAD_DATA, PORT_GetError());
+}
 
-  const SECItem *enc = PK11_HPKE_GetEncapPubKey(sender.get());
-  EXPECT_NE(nullptr, enc);
-  rv = PK11_HPKE_SetupR(receiver.get(), pub_key_r.get(), priv_key_r.get(),
-                        const_cast<SECItem *>(enc), &info_item);
-  EXPECT_EQ(SECSuccess, rv);
+TEST_P(ModeParameterizedTest, EphemeralKeys) {
+  std::vector<uint8_t> msg = {'s', 'e', 'c', 'r', 'e', 't'};
+  std::vector<uint8_t> aad = {'a', 'a', 'd'};
+  SECItem msg_item = {siBuffer, msg.data(),
+                      static_cast<unsigned int>(msg.size())};
+  SECItem aad_item = {siBuffer, aad.data(),
+                      static_cast<unsigned int>(aad.size())};
+  ScopedHpkeContext sender;
+  ScopedHpkeContext receiver;
+  SetUpEphemeralContexts(sender, receiver, std::get<0>(GetParam()),
+                         std::get<1>(GetParam()), std::get<2>(GetParam()),
+                         std::get<3>(GetParam()));
 
+  SealOpen(sender, receiver, msg, aad, nullptr);
+
+  // Seal for negative tests
   SECItem *tmp_sealed = nullptr;
-  rv = PK11_HPKE_Seal(sender.get(), &aad_item, &msg_item, &tmp_sealed);
-  EXPECT_EQ(SECSuccess, rv);
+  SECItem *tmp_unsealed = nullptr;
+  EXPECT_EQ(SECSuccess,
+            PK11_HPKE_Seal(sender.get(), &aad_item, &msg_item, &tmp_sealed));
+  ASSERT_NE(nullptr, tmp_sealed);
   ScopedSECItem sealed(tmp_sealed);
 
-  SECItem *tmp_unsealed = nullptr;
-  rv = PK11_HPKE_Open(receiver.get(), &aad_item, sealed.get(), &tmp_unsealed);
-  EXPECT_EQ(SECSuccess, rv);
-  CheckEquality(&msg_item, tmp_unsealed);
-  ScopedSECItem unsealed(tmp_unsealed);
-
-  // Once more
-  tmp_sealed = nullptr;
-  rv = PK11_HPKE_Seal(sender.get(), &aad_item, &msg_item, &tmp_sealed);
-  EXPECT_EQ(SECSuccess, rv);
-  ASSERT_NE(nullptr, sealed);
-  sealed.reset(tmp_sealed);
-  tmp_unsealed = nullptr;
-  rv = PK11_HPKE_Open(receiver.get(), &aad_item, sealed.get(), &tmp_unsealed);
-  EXPECT_EQ(SECSuccess, rv);
-  CheckEquality(&msg_item, tmp_unsealed);
-  unsealed.reset(tmp_unsealed);
-
-  // Seal for negative tests
-  tmp_sealed = nullptr;
-  tmp_unsealed = nullptr;
-  rv = PK11_HPKE_Seal(sender.get(), &aad_item, &msg_item, &tmp_sealed);
-  EXPECT_EQ(SECSuccess, rv);
-  ASSERT_NE(nullptr, sealed);
-  sealed.reset(tmp_sealed);
-
   // Drop AAD
-  rv = PK11_HPKE_Open(receiver.get(), nullptr, sealed.get(), &tmp_unsealed);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure, PK11_HPKE_Open(receiver.get(), nullptr, sealed.get(),
+                                       &tmp_unsealed));
+  EXPECT_EQ(SEC_ERROR_BAD_DATA, PORT_GetError());
   EXPECT_EQ(nullptr, tmp_unsealed);
 
   // Modify AAD
   aad_item.data[0] ^= 0xff;
-  rv = PK11_HPKE_Open(receiver.get(), &aad_item, sealed.get(), &tmp_unsealed);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure, PK11_HPKE_Open(receiver.get(), &aad_item, sealed.get(),
+                                       &tmp_unsealed));
+  EXPECT_EQ(SEC_ERROR_BAD_DATA, PORT_GetError());
   EXPECT_EQ(nullptr, tmp_unsealed);
   aad_item.data[0] ^= 0xff;
 
   // Modify ciphertext
   sealed->data[0] ^= 0xff;
-  rv = PK11_HPKE_Open(receiver.get(), &aad_item, sealed.get(), &tmp_unsealed);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure, PK11_HPKE_Open(receiver.get(), &aad_item, sealed.get(),
+                                       &tmp_unsealed));
+  EXPECT_EQ(SEC_ERROR_BAD_DATA, PORT_GetError());
   EXPECT_EQ(nullptr, tmp_unsealed);
   sealed->data[0] ^= 0xff;
 
-  rv = PK11_HPKE_Open(receiver.get(), &aad_item, sealed.get(), &tmp_unsealed);
-  EXPECT_EQ(SECSuccess, rv);
+  EXPECT_EQ(SECSuccess, PK11_HPKE_Open(receiver.get(), &aad_item, sealed.get(),
+                                       &tmp_unsealed));
   EXPECT_NE(nullptr, tmp_unsealed);
-  unsealed.reset(tmp_unsealed);
+  ScopedSECItem unsealed(tmp_unsealed);
+  CheckEquality(&msg_item, unsealed.get());
 }
 
-TEST_F(Pkcs11HpkeTest, InvalidContextParams) {
+TEST_F(ModeParameterizedTest, InvalidContextParams) {
   HpkeContext *cx =
-      PK11_HPKE_NewContext(static_cast<HpkeKemId>(1), HpkeKdfHkdfSha256,
+      PK11_HPKE_NewContext(static_cast<HpkeKemId>(0xff), HpkeKdfHkdfSha256,
                            HpkeAeadChaCha20Poly1305, nullptr, nullptr);
   EXPECT_EQ(nullptr, cx);
   EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
 
-  cx = PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, static_cast<HpkeKdfId>(2),
+  cx = PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, static_cast<HpkeKdfId>(0xff),
                             HpkeAeadChaCha20Poly1305, nullptr, nullptr);
   EXPECT_EQ(nullptr, cx);
   EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
   cx = PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256,
-                            static_cast<HpkeAeadId>(4), nullptr, nullptr);
+                            static_cast<HpkeAeadId>(0xff), nullptr, nullptr);
   EXPECT_EQ(nullptr, cx);
   EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
 }
 
-TEST_F(Pkcs11HpkeTest, InvalidReceiverKeyType) {
+TEST_F(ModeParameterizedTest, InvalidReceiverKeyType) {
   ScopedHpkeContext sender(
       PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256,
                            HpkeAeadChaCha20Poly1305, nullptr, nullptr));
   ASSERT_TRUE(!!sender);
 
   ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
   if (!slot) {
     ADD_FAILURE() << "No slot";
@@ -502,19 +652,18 @@ TEST_F(Pkcs11HpkeTest, InvalidReceiverKe
   ScopedSECKEYPrivateKey priv_key(
       PK11_GenerateKeyPair(slot.get(), CKM_RSA_PKCS_KEY_PAIR_GEN, &rsa_param,
                            &pub_tmp, PR_FALSE, PR_FALSE, nullptr));
   ASSERT_NE(nullptr, priv_key);
   ASSERT_NE(nullptr, pub_tmp);
   pub_key.reset(pub_tmp);
 
   SECItem info_item = {siBuffer, nullptr, 0};
-  SECStatus rv = PK11_HPKE_SetupS(sender.get(), nullptr, nullptr, pub_key.get(),
-                                  &info_item);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure, PK11_HPKE_SetupS(sender.get(), nullptr, nullptr,
+                                         pub_key.get(), &info_item));
   EXPECT_EQ(SEC_ERROR_BAD_KEY, PORT_GetError());
 
   // Try with an unexpected curve
   StackSECItem ecParams;
   SECOidData *oidData = SECOID_FindOIDByTag(SEC_OID_ANSIX962_EC_PRIME256V1);
   ASSERT_NE(oidData, nullptr);
   if (!SECITEM_AllocItem(nullptr, &ecParams, (2 + oidData->oid.len))) {
     FAIL() << "Couldn't allocate memory for OID.";
@@ -524,23 +673,22 @@ TEST_F(Pkcs11HpkeTest, InvalidReceiverKe
   memcpy(ecParams.data + 2, oidData->oid.data, oidData->oid.len);
 
   priv_key.reset(PK11_GenerateKeyPair(slot.get(), CKM_EC_KEY_PAIR_GEN,
                                       &ecParams, &pub_tmp, PR_FALSE, PR_FALSE,
                                       nullptr));
   ASSERT_NE(nullptr, priv_key);
   ASSERT_NE(nullptr, pub_tmp);
   pub_key.reset(pub_tmp);
-  rv = PK11_HPKE_SetupS(sender.get(), nullptr, nullptr, pub_key.get(),
-                        &info_item);
-  EXPECT_EQ(SECFailure, rv);
+  EXPECT_EQ(SECFailure, PK11_HPKE_SetupS(sender.get(), nullptr, nullptr,
+                                         pub_key.get(), &info_item));
   EXPECT_EQ(SEC_ERROR_BAD_KEY, PORT_GetError());
 }
 #else
-TEST(Pkcs11HpkeTest, EnsureNotImplemented) {
+TEST(HpkeTest, EnsureNotImplemented) {
   ScopedHpkeContext cx(
       PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256,
                            HpkeAeadChaCha20Poly1305, nullptr, nullptr));
   EXPECT_FALSE(cx.get());
   EXPECT_EQ(SEC_ERROR_INVALID_ALGORITHM, PORT_GetError());
 }
 #endif  // NSS_ENABLE_DRAFT_HPKE
 
--- a/security/nss/gtests/ssl_gtest/libssl_internals.c
+++ b/security/nss/gtests/ssl_gtest/libssl_internals.c
@@ -489,11 +489,32 @@ SECStatus SSLInt_SetRawEchConfigForRetry
   sslSocket *ss = ssl_FindSocket(fd);
   if (!ss) {
     return SECFailure;
   }
 
   sslEchConfig *cfg = (sslEchConfig *)PR_LIST_HEAD(&ss->echConfigs);
   SECITEM_FreeItem(&cfg->raw, PR_FALSE);
   SECITEM_AllocItem(NULL, &cfg->raw, len);
-  memcpy(cfg->raw.data, buf, len);
+  PORT_Memcpy(cfg->raw.data, buf, len);
   return SECSuccess;
 }
+
+// Zero the echConfig.config_id for all configured echConfigs.
+// This mimics a collision on the 8B config ID so that we can
+// test trial decryption.
+SECStatus SSLInt_ZeroEchConfigIds(PRFileDesc *fd) {
+  if (!fd) {
+    return SECFailure;
+  }
+  sslSocket *ss = ssl_FindSocket(fd);
+  if (!ss) {
+    return SECFailure;
+  }
+
+  for (PRCList *cur_p = PR_LIST_HEAD(&ss->echConfigs); cur_p != &ss->echConfigs;
+       cur_p = PR_NEXT_LINK(cur_p)) {
+    PORT_Memset(((sslEchConfig *)cur_p)->configId, 0,
+                sizeof(((sslEchConfig *)cur_p)->configId));
+  }
+
+  return SECSuccess;
+}
--- a/security/nss/gtests/ssl_gtest/libssl_internals.h
+++ b/security/nss/gtests/ssl_gtest/libssl_internals.h
@@ -46,10 +46,10 @@ SECStatus SSLInt_SetSocketMaxEarlyDataSi
 SECStatus SSLInt_TweakChannelInfoForDC(PRFileDesc *fd, PRBool changeAuthKeyBits,
                                        PRBool changeScheme);
 SECStatus SSLInt_SetDCAdvertisedSigSchemes(PRFileDesc *fd,
                                            const SSLSignatureScheme *schemes,
                                            uint32_t num_sig_schemes);
 SECStatus SSLInt_RemoveServerCertificates(PRFileDesc *fd);
 SECStatus SSLInt_SetRawEchConfigForRetry(PRFileDesc *fd, const uint8_t *buf,
                                          size_t len);
-
-#endif  // ndef libssl_internals_h_
+SECStatus SSLInt_ZeroEchConfigIds(PRFileDesc *fd);
+#endif  // ifndef libssl_internals_h_
--- a/security/nss/gtests/ssl_gtest/ssl_extension_unittest.cc
+++ b/security/nss/gtests/ssl_gtest/ssl_extension_unittest.cc
@@ -1093,32 +1093,16 @@ TEST_P(TlsExtensionTest13, HrrThenRemove
 
 TEST_P(TlsExtensionTest13, HrrThenRemoveSupportedGroups) {
   ExpectAlert(server_, kTlsAlertMissingExtension);
   HrrThenRemoveExtensionsTest(ssl_supported_groups_xtn,
                               SSL_ERROR_MISSING_EXTENSION_ALERT,
                               SSL_ERROR_MISSING_SUPPORTED_GROUPS_EXTENSION);
 }
 
-#ifdef NSS_ENABLE_DRAFT_HPKE
-TEST_P(TlsExtensionTest13, HrrThenRemoveEch) {
-  if (variant_ == ssl_variant_datagram) {
-    // ECH not supported in DTLS.
-    GTEST_SKIP();
-  }
-
-  EnsureTlsSetup();
-  SetupEch(client_, server_);
-  ExpectAlert(server_, kTlsAlertIllegalParameter);
-  HrrThenRemoveExtensionsTest(ssl_tls13_encrypted_client_hello_xtn,
-                              SSL_ERROR_ILLEGAL_PARAMETER_ALERT,
-                              SSL_ERROR_BAD_2ND_CLIENT_HELLO);
-}
-#endif
-
 TEST_P(TlsExtensionTest13, EmptyVersionList) {
   static const uint8_t ext[] = {0x00, 0x00};
   ConnectWithBogusVersionList(ext, sizeof(ext));
 }
 
 TEST_P(TlsExtensionTest13, OddVersionList) {
   static const uint8_t ext[] = {0x00, 0x01, 0x00};
   ConnectWithBogusVersionList(ext, sizeof(ext));
--- a/security/nss/gtests/ssl_gtest/tls_ech_unittest.cc
+++ b/security/nss/gtests/ssl_gtest/tls_ech_unittest.cc
@@ -14,19 +14,19 @@
 #include "tls_agent.h"
 #include "tls_connect.h"
 #include "util.h"
 
 namespace nss_test {
 
 class TlsAgentEchTest : public TlsAgentTestClient13 {
  protected:
-  void InstallEchConfig(const DataBuffer& record, PRErrorCode err = 0) {
-    SECStatus rv =
-        SSL_SetClientEchConfigs(agent_->ssl_fd(), record.data(), record.len());
+  void InstallEchConfig(const DataBuffer& echconfig, PRErrorCode err = 0) {
+    SECStatus rv = SSL_SetClientEchConfigs(agent_->ssl_fd(), echconfig.data(),
+                                           echconfig.len());
     if (err == 0) {
       ASSERT_EQ(SECSuccess, rv);
     } else {
       ASSERT_EQ(SECFailure, rv);
       ASSERT_EQ(err, PORT_GetError());
     }
   }
 };
@@ -138,28 +138,16 @@ class TlsConnectStreamTls13Ech : public 
     ASSERT_EQ(SECSuccess,
               SSL_SetClientEchConfigs(client_->ssl_fd(), retry_configs.data,
                                       retry_configs.len));
     client_->ExpectEch();
     server_->ExpectEch();
     Connect();
   }
 
- private:
-  // Testing certan invalid CHInner configurations is tricky, particularly
-  // since the CHOuter forms AAD and isn't available in filters. Instead of
-  // generating these inputs on the fly, use a fixed server keypair so that
-  // the input can be generated once (e.g. via a debugger) and replayed in
-  // each invocation of the test.
-  std::string kFixedServerPubkey =
-      "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a"
-      "02010104205a8aa0d2476b28521588e0c704b14db82cdd4970d340d293a957"
-      "6deaee9ec1c7a1230321008756e2580c07c1d2ffcb662f5fadc6d6ff13da85"
-      "abd7adfecf984aaa102c1269";
-
   void ImportFixedEchKeypair(ScopedSECKEYPublicKey& pub,
                              ScopedSECKEYPrivateKey& priv) {
     ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
     if (!slot) {
       ADD_FAILURE() << "No slot";
       return;
     }
     std::vector<uint8_t> pkcs8_r = hex_string_to_bytes(kFixedServerPubkey);
@@ -171,26 +159,40 @@ class TlsConnectStreamTls13Ech : public 
                               slot.get(), &pkcs8_r_item, nullptr, nullptr,
                               false, false, KU_ALL, &tmp_priv, nullptr));
     priv.reset(tmp_priv);
     SECKEYPublicKey* tmp_pub = SECKEY_ConvertToPublicKey(tmp_priv);
     pub.reset(tmp_pub);
     ASSERT_NE(nullptr, tmp_pub);
   }
 
+ private:
+  // Testing certan invalid CHInner configurations is tricky, particularly
+  // since the CHOuter forms AAD and isn't available in filters. Instead of
+  // generating these inputs on the fly, use a fixed server keypair so that
+  // the input can be generated once (e.g. via a debugger) and replayed in
+  // each invocation of the test.
+  std::string kFixedServerPubkey =
+      "3067020100301406072a8648ce3d020106092b06010401da470f01044c304a"
+      "02010104205a8aa0d2476b28521588e0c704b14db82cdd4970d340d293a957"
+      "6deaee9ec1c7a1230321008756e2580c07c1d2ffcb662f5fadc6d6ff13da85"
+      "abd7adfecf984aaa102c1269";
+
   void SetMutualEchConfigs(ScopedSECKEYPublicKey& pub,
                            ScopedSECKEYPrivateKey& priv) {
-    DataBuffer record;
+    DataBuffer echconfig;
     TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kDefaultSuites,
-                                          kPublicName, 100, record, pub, priv);
+                                          kPublicName, 100, echconfig, pub,
+                                          priv);
     ASSERT_EQ(SECSuccess,
               SSL_SetServerEchConfigs(server_->ssl_fd(), pub.get(), priv.get(),
-                                      record.data(), record.len()));
-    ASSERT_EQ(SECSuccess, SSL_SetClientEchConfigs(client_->ssl_fd(),
-                                                  record.data(), record.len()));
+                                      echconfig.data(), echconfig.len()));
+    ASSERT_EQ(SECSuccess,
+              SSL_SetClientEchConfigs(client_->ssl_fd(), echconfig.data(),
+                                      echconfig.len()));
   }
 };
 
 static void CheckCertVerifyPublicName(TlsAgent* agent) {
   agent->UpdatePreliminaryChannelInfo();
   EXPECT_NE(0U, (agent->pre_info().valuesSet & ssl_preinfo_ech));
   EXPECT_EQ(agent->GetEchExpected(), agent->pre_info().echAccepted);
 
@@ -219,188 +221,205 @@ static SECStatus AuthCompleteFail(TlsAge
 
 TEST_P(TlsAgentEchTest, EchConfigsSupportedYesNo) {
   if (variant_ == ssl_variant_datagram) {
     GTEST_SKIP();
   }
 
   // ECHConfig 2 cipher_suites are unsupported.
   const std::string mixed =
-      "0086FE08003F000B7075626C69632E6E616D6500203BB6D46C201B820F1AE4AFD4DEC304"
-      "444156E4E04D1BF0FFDA7783B6B457F75600200008000100030001000100640000FE0800"
+      "0086FE09003F000B7075626C69632E6E616D6500203BB6D46C201B820F1AE4AFD4DEC304"
+      "444156E4E04D1BF0FFDA7783B6B457F75600200008000100030001000100640000FE0900"
       "3F000B7075626C69632E6E616D6500203BB6D46C201B820F1AE4AFD4DEC304444156E4E0"
       "4D1BF0FFDA7783B6B457F756002000080001FFFFFFFF000100640000";
   std::vector<uint8_t> config = hex_string_to_bytes(mixed);
-  DataBuffer record(config.data(), config.size());
+  DataBuffer echconfig(config.data(), config.size());
 
   EnsureInit();
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
-  InstallEchConfig(record, 0);
+  InstallEchConfig(echconfig, 0);
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_EQ(TlsAgent::STATE_CONNECTING, agent_->state());
   ASSERT_TRUE(filter->captured());
 }
 
 TEST_P(TlsAgentEchTest, EchConfigsSupportedNoYes) {
   if (variant_ == ssl_variant_datagram) {
     GTEST_SKIP();
   }
 
   // ECHConfig 1 cipher_suites are unsupported.
   const std::string mixed =
-      "0086FE08003F000B7075626C69632E6E616D6500203BB6D46C201B820F1AE4AFD4DEC304"
-      "444156E4E04D1BF0FFDA7783B6B457F756002000080001FFFFFFFF000100640000FE0800"
+      "0086FE09003F000B7075626C69632E6E616D6500203BB6D46C201B820F1AE4AFD4DEC304"
+      "444156E4E04D1BF0FFDA7783B6B457F756002000080001FFFFFFFF000100640000FE0900"
       "3F000B7075626C69632E6E616D6500203BB6D46C201B820F1AE4AFD4DEC304444156E4E0"
       "4D1BF0FFDA7783B6B457F75600200008000100030001000100640000";
   std::vector<uint8_t> config = hex_string_to_bytes(mixed);
-  DataBuffer record(config.data(), config.size());
+  DataBuffer echconfig(config.data(), config.size());
 
   EnsureInit();
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
-  InstallEchConfig(record, 0);
+  InstallEchConfig(echconfig, 0);
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_EQ(TlsAgent::STATE_CONNECTING, agent_->state());
   ASSERT_TRUE(filter->captured());
 }
 
 TEST_P(TlsAgentEchTest, EchConfigsSupportedNoNo) {
   if (variant_ == ssl_variant_datagram) {
     GTEST_SKIP();
   }
 
   // ECHConfig 1 and 2 cipher_suites are unsupported.
   const std::string unsupported =
-      "0086FE08003F000B7075626C69632E6E616D6500203BB6D46C201B820F1AE4AFD4DEC304"
-      "444156E4E04D1BF0FFDA7783B6B457F756002000080001FFFF0001FFFF00640000FE0800"
+      "0086FE09003F000B7075626C69632E6E616D6500203BB6D46C201B820F1AE4AFD4DEC304"
+      "444156E4E04D1BF0FFDA7783B6B457F756002000080001FFFF0001FFFF00640000FE0900"
       "3F000B7075626C69632E6E616D6500203BB6D46C201B820F1AE4AFD4DEC304444156E4E0"
       "4D1BF0FFDA7783B6B457F75600200008FFFF0003FFFF000100640000";
   std::vector<uint8_t> config = hex_string_to_bytes(unsupported);
-  DataBuffer record(config.data(), config.size());
+  DataBuffer echconfig(config.data(), config.size());
 
   EnsureInit();
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
-  InstallEchConfig(record, SEC_ERROR_INVALID_ARGS);
+  InstallEchConfig(echconfig, SEC_ERROR_INVALID_ARGS);
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_EQ(TlsAgent::STATE_CONNECTING, agent_->state());
   ASSERT_FALSE(filter->captured());
 }
 
 TEST_P(TlsAgentEchTest, ShortEchConfig) {
   EnsureInit();
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kDefaultSuites,
-                                        kPublicName, 100, record, pub, priv);
-  record.Truncate(record.len() - 1);
-  InstallEchConfig(record, SEC_ERROR_BAD_DATA);
+                                        kPublicName, 100, echconfig, pub, priv);
+  echconfig.Truncate(echconfig.len() - 1);
+  InstallEchConfig(echconfig, SEC_ERROR_BAD_DATA);
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_EQ(TlsAgent::STATE_CONNECTING, agent_->state());
   ASSERT_FALSE(filter->captured());
 }
 
 TEST_P(TlsAgentEchTest, LongEchConfig) {
   EnsureInit();
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kDefaultSuites,
-                                        kPublicName, 100, record, pub, priv);
-  record.Write(record.len(), 1, 1);  // Append one byte
-  InstallEchConfig(record, SEC_ERROR_BAD_DATA);
+                                        kPublicName, 100, echconfig, pub, priv);
+  echconfig.Write(echconfig.len(), 1, 1);  // Append one byte
+  InstallEchConfig(echconfig, SEC_ERROR_BAD_DATA);
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_EQ(TlsAgent::STATE_CONNECTING, agent_->state());
   ASSERT_FALSE(filter->captured());
 }
 
 TEST_P(TlsAgentEchTest, UnsupportedEchConfigVersion) {
   EnsureInit();
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   static const uint8_t bad_version[] = {0xff, 0xff};
   DataBuffer bad_ver_buf(bad_version, sizeof(bad_version));
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kDefaultSuites,
-                                        kPublicName, 100, record, pub, priv);
-  record.Splice(bad_ver_buf, 2, 2);
-  InstallEchConfig(record, SEC_ERROR_INVALID_ARGS);
+                                        kPublicName, 100, echconfig, pub, priv);
+  echconfig.Splice(bad_ver_buf, 2, 2);
+  InstallEchConfig(echconfig, SEC_ERROR_INVALID_ARGS);
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_EQ(TlsAgent::STATE_CONNECTING, agent_->state());
   ASSERT_FALSE(filter->captured());
 }
 
 TEST_P(TlsAgentEchTest, UnsupportedHpkeKem) {
   EnsureInit();
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   // SSL_EncodeEchConfig encodes without validation.
   TlsConnectTestBase::GenerateEchConfig(static_cast<HpkeKemId>(0xff),
                                         kDefaultSuites, kPublicName, 100,
-                                        record, pub, priv);
-  InstallEchConfig(record, SEC_ERROR_INVALID_ARGS);
+                                        echconfig, pub, priv);
+  InstallEchConfig(echconfig, SEC_ERROR_INVALID_ARGS);
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_EQ(TlsAgent::STATE_CONNECTING, agent_->state());
   ASSERT_FALSE(filter->captured());
 }
 
 TEST_P(TlsAgentEchTest, EchRejectIgnoreAllUnknownSuites) {
   EnsureInit();
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kBogusSuite,
-                                        kPublicName, 100, record, pub, priv);
-  InstallEchConfig(record, SEC_ERROR_INVALID_ARGS);
+                                        kPublicName, 100, echconfig, pub, priv);
+  InstallEchConfig(echconfig, SEC_ERROR_INVALID_ARGS);
+  EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
+                                                 PR_FALSE));  // Don't GREASE
+  auto filter = MakeTlsFilter<TlsExtensionCapture>(
+      agent_, ssl_tls13_encrypted_client_hello_xtn);
+  agent_->Handshake();
+  ASSERT_FALSE(filter->captured());
+}
+
+TEST_P(TlsAgentEchTest, EchConfigRejectEmptyPublicName) {
+  EnsureInit();
+  ScopedSECKEYPublicKey pub;
+  ScopedSECKEYPrivateKey priv;
+  DataBuffer echconfig;
+  TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kBogusSuite, "",
+                                        100, echconfig, pub, priv);
+  InstallEchConfig(echconfig, SSL_ERROR_RX_MALFORMED_ECH_CONFIG);
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_FALSE(filter->captured());
 }
 
 TEST_F(TlsConnectStreamTls13, EchAcceptIgnoreSingleUnknownSuite) {
   EnsureTlsSetup();
-  DataBuffer record;
+  DataBuffer echconfig;
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256,
                                         kUnknownFirstSuite, kPublicName, 100,
-                                        record, pub, priv);
-  ASSERT_EQ(SECSuccess, SSL_SetClientEchConfigs(client_->ssl_fd(),
-                                                record.data(), record.len()));
+                                        echconfig, pub, priv);
+  ASSERT_EQ(SECSuccess,
+            SSL_SetClientEchConfigs(client_->ssl_fd(), echconfig.data(),
+                                    echconfig.len()));
   ASSERT_EQ(SECSuccess,
             SSL_SetServerEchConfigs(server_->ssl_fd(), pub.get(), priv.get(),
-                                    record.data(), record.len()));
+                                    echconfig.data(), echconfig.len()));
 
   client_->ExpectEch();
   server_->ExpectEch();
   Connect();
 }
 
 TEST_P(TlsAgentEchTest, ApiInvalidArgs) {
   EnsureInit();
@@ -483,170 +502,231 @@ TEST_P(TlsAgentEchTest, NoEarlyRetryConf
   EnsureInit();
   StackSECItem retry_configs;
   EXPECT_EQ(SECFailure,
             SSL_GetEchRetryConfigs(agent_->ssl_fd(), &retry_configs));
   EXPECT_EQ(SSL_ERROR_HANDSHAKE_NOT_COMPLETED, PORT_GetError());
 
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kDefaultSuites,
-                                        kPublicName, 100, record, pub, priv);
-  InstallEchConfig(record, 0);
+                                        kPublicName, 100, echconfig, pub, priv);
+  InstallEchConfig(echconfig, 0);
 
   EXPECT_EQ(SECFailure,
             SSL_GetEchRetryConfigs(agent_->ssl_fd(), &retry_configs));
   EXPECT_EQ(SSL_ERROR_HANDSHAKE_NOT_COMPLETED, PORT_GetError());
 }
 
 TEST_P(TlsAgentEchTest, NoSniSoNoEch) {
   EnsureInit();
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kDefaultSuites,
-                                        kPublicName, 100, record, pub, priv);
+                                        kPublicName, 100, echconfig, pub, priv);
   SSL_SetURL(agent_->ssl_fd(), "");
-  InstallEchConfig(record, 0);
+  InstallEchConfig(echconfig, 0);
   SSL_SetURL(agent_->ssl_fd(), "");
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_FALSE(filter->captured());
 }
 
 TEST_P(TlsAgentEchTest, NoEchConfigSoNoEch) {
   EnsureInit();
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_FALSE(filter->captured());
 }
 
 TEST_P(TlsAgentEchTest, EchConfigDuplicateExtensions) {
   EnsureInit();
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kDefaultSuites,
-                                        kPublicName, 100, record, pub, priv);
+                                        kPublicName, 100, echconfig, pub, priv);
 
   static const uint8_t duped_xtn[] = {0x00, 0x08, 0x00, 0x01, 0x00,
                                       0x00, 0x00, 0x01, 0x00, 0x00};
   DataBuffer buf(duped_xtn, sizeof(duped_xtn));
-  record.Truncate(record.len() - 2);
-  record.Append(buf);
+  echconfig.Truncate(echconfig.len() - 2);
+  echconfig.Append(buf);
   uint32_t len;
-  ASSERT_TRUE(record.Read(0, 2, &len));
+  ASSERT_TRUE(echconfig.Read(0, 2, &len));
   len += buf.len() - 2;
   DataBuffer new_len;
   ASSERT_TRUE(new_len.Write(0, len, 2));
-  record.Splice(new_len, 0, 2);
+  echconfig.Splice(new_len, 0, 2);
   new_len.Truncate(0);
 
-  ASSERT_TRUE(record.Read(4, 2, &len));
+  ASSERT_TRUE(echconfig.Read(4, 2, &len));
   len += buf.len() - 2;
   ASSERT_TRUE(new_len.Write(0, len, 2));
-  record.Splice(new_len, 4, 2);
+  echconfig.Splice(new_len, 4, 2);
 
-  InstallEchConfig(record, SEC_ERROR_EXTENSION_VALUE_INVALID);
+  InstallEchConfig(echconfig, SEC_ERROR_EXTENSION_VALUE_INVALID);
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(agent_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
       agent_, ssl_tls13_encrypted_client_hello_xtn);
   agent_->Handshake();
   ASSERT_EQ(TlsAgent::STATE_CONNECTING, agent_->state());
   ASSERT_FALSE(filter->captured());
 }
 
 // Test an encoded ClientHelloInner containing an extra extensionType
 // in outer_extensions, for which there is no corresponding (uncompressed)
 // extension in ClientHelloOuter.
 TEST_F(TlsConnectStreamTls13Ech, EchOuterExtensionsReferencesMissing) {
   std::string ch =
-      "01000170030374d616d97efe591bf9bee4496bcc1118145b4dd02f7d1ff979fd0cf61749"
-      "a91e0000061301130313020100014100000010000e00000b7075626c69632e6e616d65ff"
+      "010001580303dfff91b5e1ba00f29d2338419b3abf125ee1051a942ae25163bbf609a1ea"
+      "11920000061301130313020100012900000010000e00000b7075626c69632e6e616d65ff"
       "01000100000a00140012001d00170018001901000101010201030104003300260024001d"
-      "00204f346f86351b077492c83564c909d1aaab4f6f3ee2566af0e90a4684c793805d002b"
+      "0020d94c1590c261e9ea8ae55bc9581f397cc598115f8b70aec1b0236f4c8c555537002b"
       "0003020304000d0018001604030503060302030804080508060401050106010201002d00"
-      "020101001c00024001fe0800b30001000320a10698ccbd4bd86df91f617e58dd2ca96b8b"
-      "a5f058dd5c5ab1ca9750ef9d28c70020924764b36fe5d4a985f9857ceb75edb10b5f4b5b"
-      "f9d59290db70743e3c582163006acea5d7785cc506ecf5c859a9cad18f2b1df1a32231fe"
-      "0330471ee0e88ece9047e6491a381bfabed58f7fc542f0ba78eb55030bcfe1d400f67275"
-      "eac8619d1e4237e9d6176dd4eb54f3f25865686756f313a4ba47901c83e5ad5413609d39"
-      "816346b940115fd68e534609";
+      "020101001c00024001fe09009b0001000308fde4163c5c6e8bb6002067a895efa2721c88"
+      "63ecfa1bea1e520ae6f6cf938e3e37802688f7a83a871a04006aa693f053f87db87cf82a"
+      "7caa20670d79b92ccda97893fdf99352fc766fb3dd5570948311dddb6d41214234fae585"
+      "e354a048c072b3fb00a0a64e8e089e4a90152ee91a2c5b947c99d3dcebfb6334453b023d"
+      "4d725010996a290a0552e4b238ec91c21440adc0d51a4435";
   ReplayChWithMalformedInner(ch, kTlsAlertIllegalParameter,
                              SSL_ERROR_RX_MALFORMED_ECH_EXTENSION,
                              SSL_ERROR_ILLEGAL_PARAMETER_ALERT);
 }
 
 // Drop supported_versions from CHInner, make sure we don't negotiate 1.2+ECH.
 TEST_F(TlsConnectStreamTls13Ech, EchVersion12Inner) {
   std::string ch =
-      "0100017003034dd5bf4c12835e9be21f983953720e3595b3a8eeb4a44467678caceb7727"
-      "3be90000061301130313020100014100000010000e00000b7075626c69632e6e616d65ff"
+      "0100015103038fbe6f75b0123116fa5c4eccf0cf26c17ab1ded5529307e419c036ac7e9c"
+      "e8e30000061301130313020100012200000010000e00000b7075626c69632e6e616d65ff"
       "01000100000a00140012001d00170018001901000101010201030104003300260024001d"
-      "0020af7b976cdf69ffcd494ca5a93ae3ecde692b09be518ee033aad908c45b82c368002b"
+      "002078d644583b4f056bec4d8ae9bddd383aed6eb7cdb3294f88b0e37a4f26a02549002b"
       "0003020304000d0018001604030503060302030804080508060401050106010201002d00"
-      "020101001c0002400100150003000000fe0800ac0001000320a10698ccbd4bd86df91f61"
-      "7e58dd2ca96b8ba5f058dd5c5ab1ca9750ef9d28c70020f5ece4c187b76f7e3d467c7506"
-      "215e73c27c918cd863c0e80d76a7987ec274320063e037492868eff5296a22dc50885e9d"
-      "f6964a5e26546f1bada043f8834988dfea5394b4c45a4d0b3afc52142d33f94161135a63"
-      "ed3c1b63f60d8133fb1cff17e1f9ced6c871984e412ed8ddb0f487c4d09d7aea80488004"
-      "c45a17cd3b5cdca316155fdb";
+      "020101001c00024001fe0900940001000308fde4163c5c6e8bb600208958e66d1d4bbd46"
+      "4792f392e119dbce91ee3e65067899b45c83855dae61e67a00637df038e7b35483786707"
+      "dd1b25be5cd3dd07f1ca4b33a3595ddb959e5c0da3d2f0b3314417614968691700c05232"
+      "07c729b34f3b5de62728b3cb6b45b00e6f94b204a9504d0e7e24c66f42aacc73591c86ef"
+      "571e61cebd6ba671081150a2dae89e7493";
   ReplayChWithMalformedInner(ch, kTlsAlertProtocolVersion,
                              SSL_ERROR_UNSUPPORTED_VERSION,
                              SSL_ERROR_PROTOCOL_VERSION_ALERT);
 }
 
 // Use CHInner supported_versions to negotiate 1.2.
 TEST_F(TlsConnectStreamTls13Ech, EchVersion12InnerSupportedVersions) {
   std::string ch =
-      "010001700303845c298db4017d2ed2584284b90e4ecba57a63663560c57aa0b1ac51203d"
-      "c8560000061301130313020100014100000010000e00000b7075626c69632e6e616d65ff"
+      "01000158030378a601a3f12229e53e0b8d92c3599bf1782e8261d2ecaec9bbe595d4c901"
+      "98770000061301130313020100012900000010000e00000b7075626c69632e6e616d65ff"
       "01000100000a00140012001d00170018001901000101010201030104003300260024001d"
-      "00203356719e88b539645438f645916aeeffe93c38803a59d6997938aa98eefbcf64002b"
+      "00201c8017d6970f3a92ac1c9919c3a26788052f84599fb0c3cb7bd381304148724e002b"
       "0003020304000d0018001604030503060302030804080508060401050106010201002d00"
-      "020101001c00024001fe0800b30001000320a10698ccbd4bd86df91f617e58dd2ca96b8b"
-      "a5f058dd5c5ab1ca9750ef9d28c700208412c945c53624bcace5eda0dc1ad300a1620e86"
-      "5a0f4a27755a3477b115b65b006abf1dfd77ddc1b80c5976732174a5fe7ebcf9ff1a548b"
-      "097daa12a37f3e32a613a0798544ba1d96239431bc807ddd9055ac3fb3e32b2eb42cec30"
-      "e915357418a953027d73020fd739287414205349eeff376dd464750ca70a965141a88800"
-      "6a043fe1d6d882d9a2c2f6f3";
+      "020101001c00024001fe09009b0001000308fde4163c5c6e8bb60020f7347d34f125e866"
+      "76b1cdc43455c6c00918a3c8a961335e1b9aa864da2b5313006a21e6ad81533e90cea24e"
+      "c2c3656f6b53114b4c63bf89462696f1c8ad4e1193d87062a5537edbe83c9b35c41e9763"
+      "1d2333270854758ee02548afb7f2264f904474465415a5085024487f22b017208e250ca4"
+      "7902d61d98fbd1cb8afc0a14dcd70a68343cf67c258758d9";
   ReplayChWithMalformedInner(ch, kTlsAlertProtocolVersion,
                              SSL_ERROR_UNSUPPORTED_VERSION,
                              SSL_ERROR_PROTOCOL_VERSION_ALERT);
 }
 
-// Replay a CH for which the ECH Inner lacks the required
-// empty ECH extension.
+// Replay a CH for which CHInner lacks the required ech_is_inner extension.
 TEST_F(TlsConnectStreamTls13Ech, EchInnerMissingEmptyEch) {
   std::string ch =
-      "0100017103032bf866cbd6d4abdec8ce23107eaef9af51b644043953e3b70f2f28f1898e"
-      "87880000061301130313020100014200000010000e00000b7075626c69632e6e616d65ff"
+      "010001540303033b3284790ada882445bfb38b8af3509659033c931e6ae97febbaa62b19"
+      "b4ac0000061301130313020100012500000010000e00000b7075626c69632e6e616d65ff"
       "01000100000a00140012001d00170018001901000101010201030104003300260024001d"
-      "00208f614d3017575332ca009a42d33bcaf876b4ba6d44b052e8019c31f6f1559e41002b"
+      "00209d1ed410ccb05ce9e424f52b1be3599bcc1efb0913ae14a24d9a69cbfbc39744002b"
+      "0003020304000d0018001604030503060302030804080508060401050106010201002d00"
+      "020101001c00024001fe0900970001000308fde4163c5c6e8bb600206321bdc543a23d47"
+      "7a7104ba69177cb722927c6c485117df4a077b8e82167f0b0066103d9aac7e5fc4ef990b"
+      "2ce38593589f7f6ba043847d7db6c9136adb811f63b956d56e6ca8cbe6864e3fc43a3bc5"
+      "94a332d4d63833e411c89ef14af63b5cd18c7adee99ffd1ad3112449ea18d6650bbaca66"
+      "528f7e4146fafbf338c27cf89b145a55022b26a3";
+  ReplayChWithMalformedInner(ch, kTlsAlertIllegalParameter,
+                             SSL_ERROR_MISSING_ECH_EXTENSION,
+                             SSL_ERROR_ILLEGAL_PARAMETER_ALERT);
+}
+
+// Replay a CH for which CHInner contains both an ECH and ech_is_inner
+// extension.
+TEST_F(TlsConnectStreamTls13Ech, InnerWithEchAndEchIsInner) {
+  std::string ch =
+      "0100015c030383fb49c98b62bcdf04cbbae418dd684f8f9512f40fca6861ba40555269a9"
+      "789f0000061301130313020100012d00000010000e00000b7075626c69632e6e616d65ff"
+      "01000100000a00140012001d00170018001901000101010201030104003300260024001d"
+      "00201e3d35a6755b7dddf7e481359429e9677baaa8dd99569c2bf0b0f7ea56e68b12002b"
       "0003020304000d0018001604030503060302030804080508060401050106010201002d00"
-      "020101001c000240010015000100fe0800af0001000320a10698ccbd4bd86df91f617e58"
-      "dd2ca96b8ba5f058dd5c5ab1ca9750ef9d28c70020da1d5d9f183a5d5e49892e38eaae5e"
-      "9e3e6c5d404a5fdb672ca37f9cebabd57400660ea1d61917cc1049aab22506078ccecfc4"
-      "16a364a1beaa8915b250bb86ac2c725698c3c641830c4aa4e8b7f50152b5732b29b1ac43"
-      "45c97fc018855fd68e5600d0ef188e905b69997c3711b0ec0114a857177df728c7b84f52"
-      "2923f932838f7f15bb22644fd4";
-  ReplayChWithMalformedInner(ch, kTlsAlertDecodeError,
-                             SSL_ERROR_MISSING_ECH_EXTENSION,
-                             SSL_ERROR_DECODE_ERROR_ALERT);
+      "020101001c00024001fe09009f0001000308fde4163c5c6e8bb6002090110b89c1ba6618"
+      "942ea7aae8c472c22e97f10bef7dd490bee50cc108082b48006eed016fa2b3e3419cf5ef"
+      "9b41ab9ecffa84a4b60e2f4cc710cf31c739d1f6f88b48207aaf7ccabdd744a25a8f2a38"
+      "029d1b133e9d990681cf08c07a255d9242b3a002bc0865935cbb609b2b1996fab0626cb0"
+      "2ece6544bbde0d3218333ffd95c383a41854b76b1a254bb346a2702b";
+  ReplayChWithMalformedInner(ch, kTlsAlertIllegalParameter,
+                             SSL_ERROR_RX_MALFORMED_CLIENT_HELLO,
+                             SSL_ERROR_ILLEGAL_PARAMETER_ALERT);
+}
+
+TEST_F(TlsConnectStreamTls13, OuterWithEchAndEchIsInner) {
+  static uint8_t empty_buf[1] = {0};
+  DataBuffer empty(empty_buf, 0);
+
+  EnsureTlsSetup();
+  EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(client_->ssl_fd(), PR_TRUE));
+  MakeTlsFilter<TlsExtensionAppender>(client_, kTlsHandshakeClientHello,
+                                      ssl_tls13_ech_is_inner_xtn, empty);
+  ConnectExpectAlert(server_, kTlsAlertIllegalParameter);
+  client_->CheckErrorCode(SSL_ERROR_ILLEGAL_PARAMETER_ALERT);
+  server_->CheckErrorCode(SSL_ERROR_RX_UNEXPECTED_EXTENSION);
+}
+
+// Apply two ECHConfigs on the server. They are identical with the exception
+// of the public key: the first ECHConfig contains a public key for which we
+// lack the private value. Use an SSLInt function to zero all the config_ids
+// (client and server), then confirm that trial decryption works.
+TEST_F(TlsConnectStreamTls13Ech, EchConfigsTrialDecrypt) {
+  ScopedSECKEYPublicKey pub;
+  ScopedSECKEYPrivateKey priv;
+  EnsureTlsSetup();
+  ImportFixedEchKeypair(pub, priv);
+
+  const std::string two_configs_str =
+      "007EFE09003B000B7075626C69632E6E616D650020111111111111111111111111111111"
+      "1111111111111111111111111111111111002000040001000100640000fe09003B000B70"
+      "75626C69632E6E616D6500208756E2580C07C1D2FFCB662F5FADC6D6FF13DA85ABD7ADFE"
+      "CF984AAA102C1269002000040001000100640000";
+  const std::string second_config_str =
+      "003FFE09003B000B7075626C69632E6E616D6500208756E2580C07C1D2FFCB662F5FADC6"
+      "D6FF13DA85ABD7ADFECF984AAA102C1269002000040001000100640000";
+  std::vector<uint8_t> two_configs = hex_string_to_bytes(two_configs_str);
+  std::vector<uint8_t> second_config = hex_string_to_bytes(second_config_str);
+  ASSERT_EQ(SECSuccess,
+            SSL_SetServerEchConfigs(server_->ssl_fd(), pub.get(), priv.get(),
+                                    two_configs.data(), two_configs.size()));
+  ASSERT_EQ(SECSuccess,
+            SSL_SetClientEchConfigs(client_->ssl_fd(), second_config.data(),
+                                    second_config.size()));
+
+  ASSERT_EQ(SECSuccess, SSLInt_ZeroEchConfigIds(client_->ssl_fd()));
+  ASSERT_EQ(SECSuccess, SSLInt_ZeroEchConfigIds(server_->ssl_fd()));
+  client_->ExpectEch();
+  server_->ExpectEch();
+  Connect();
 }
 
 // An empty config_id should prompt an alert. We don't support
 // Optional Configuration Identifiers.
 TEST_F(TlsConnectStreamTls13, EchRejectEmptyConfigId) {
   static const uint8_t junk[16] = {0};
   DataBuffer junk_buf(junk, sizeof(junk));
   DataBuffer ech_xtn;
@@ -764,52 +844,89 @@ SSLHelloRetryRequestAction RetryEchHello
   EXPECT_EQ(0U, clientTokenLen);
   return firstHello ? ssl_hello_retry_request : ssl_hello_retry_accept;
 }
 
 // Generate HRR on CH1 Inner
 TEST_F(TlsConnectStreamTls13, EchAcceptWithHrr) {
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   ConfigureSelfEncrypt();
   EnsureTlsSetup();
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kDefaultSuites,
-                                        kPublicName, 100, record, pub, priv);
+                                        kPublicName, 100, echconfig, pub, priv);
   ASSERT_EQ(SECSuccess,
             SSL_SetServerEchConfigs(server_->ssl_fd(), pub.get(), priv.get(),
-                                    record.data(), record.len()));
-  ASSERT_EQ(SECSuccess, SSL_SetClientEchConfigs(client_->ssl_fd(),
-                                                record.data(), record.len()));
+                                    echconfig.data(), echconfig.len()));
+  ASSERT_EQ(SECSuccess,
+            SSL_SetClientEchConfigs(client_->ssl_fd(), echconfig.data(),
+                                    echconfig.len()));
   client_->ExpectEch();
   server_->ExpectEch();
   client_->SetAuthCertificateCallback(AuthCompleteSuccess);
 
   size_t cb_called = 0;
   EXPECT_EQ(SECSuccess, SSL_HelloRetryRequestCallback(
                             server_->ssl_fd(), RetryEchHello, &cb_called));
 
   // Start the handshake.
   client_->StartConnect();
   server_->StartConnect();
   client_->Handshake();
   server_->Handshake();
   MakeNewServer();
   ASSERT_EQ(SECSuccess,
             SSL_SetServerEchConfigs(server_->ssl_fd(), pub.get(), priv.get(),
-                                    record.data(), record.len()));
+                                    echconfig.data(), echconfig.len()));
   client_->ExpectEch();
   server_->ExpectEch();
   client_->SetAuthCertificateCallback(AuthCompleteSuccess);
   Handshake();
   EXPECT_EQ(1U, cb_called);
   CheckConnected();
   SendReceive();
 }
 
+// Send GREASE ECH in CH1. CH2 must send exactly the same GREASE ECH contents.
+TEST_F(TlsConnectStreamTls13, GreaseEchHrrMatches) {
+  ConfigureSelfEncrypt();
+  EnsureTlsSetup();
+  size_t cb_called = 0;
+  EXPECT_EQ(SECSuccess, SSL_HelloRetryRequestCallback(
+                            server_->ssl_fd(), RetryEchHello, &cb_called));
+
+  EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(client_->ssl_fd(),
+                                                 PR_TRUE));  // GREASE
+  auto capture = MakeTlsFilter<TlsExtensionCapture>(
+      client_, ssl_tls13_encrypted_client_hello_xtn);
+
+  // Start the handshake.
+  client_->StartConnect();
+  server_->StartConnect();
+  client_->Handshake();  // Send CH1
+  EXPECT_TRUE(capture->captured());
+  DataBuffer ch1_grease = capture->extension();
+
+  server_->Handshake();
+  MakeNewServer();
+  capture = MakeTlsFilter<TlsExtensionCapture>(
+      client_, ssl_tls13_encrypted_client_hello_xtn);
+
+  EXPECT_FALSE(capture->captured());
+  client_->Handshake();  // Send CH2
+  EXPECT_TRUE(capture->captured());
+  EXPECT_EQ(ch1_grease, capture->extension());
+
+  EXPECT_EQ(1U, cb_called);
+  server_->StartConnect();
+  Handshake();
+  CheckConnected();
+}
+
 // Fail to decrypt CH2. Unlike CH1, this generates an alert.
 TEST_F(TlsConnectStreamTls13, EchFailDecryptCH2) {
   EnsureTlsSetup();
   SetupEch(client_, server_);
   size_t cb_called = 0;
   EXPECT_EQ(SECSuccess, SSL_HelloRetryRequestCallback(
                             server_->ssl_fd(), RetryEchHello, &cb_called));
 
@@ -843,63 +960,91 @@ TEST_F(TlsConnectStreamTls13, EchHrrChan
                                                  PR_TRUE));  // GREASE
   client_->StartConnect();
   server_->StartConnect();
   client_->Handshake();
   server_->Handshake();
   MakeNewServer();
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(client_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
-  ExpectAlert(server_, kTlsAlertIllegalParameter);
+  ExpectAlert(server_, kTlsAlertMissingExtension);
   Handshake();
-  client_->CheckErrorCode(SSL_ERROR_ILLEGAL_PARAMETER_ALERT);
+  client_->CheckErrorCode(SSL_ERROR_MISSING_EXTENSION_ALERT);
   server_->CheckErrorCode(SSL_ERROR_BAD_2ND_CLIENT_HELLO);
   EXPECT_EQ(1U, cb_called);
 }
 
 TEST_F(TlsConnectStreamTls13, EchHrrChangeCh2OfferingNY) {
   ConfigureSelfEncrypt();
   EnsureTlsSetup();
+  SetupEch(client_, server_);
   size_t cb_called = 0;
   EXPECT_EQ(SECSuccess, SSL_HelloRetryRequestCallback(
                             server_->ssl_fd(), RetryEchHello, &cb_called));
 
-  EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(client_->ssl_fd(),
-                                                 PR_FALSE));  // Don't GREASE
+  MakeTlsFilter<TlsExtensionDropper>(client_,
+                                     ssl_tls13_encrypted_client_hello_xtn);
   // Start the handshake.
   client_->StartConnect();
   server_->StartConnect();
   client_->Handshake();
   server_->Handshake();
   MakeNewServer();
-  EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(client_->ssl_fd(),
-                                                 PR_TRUE));  // Send GREASE
+  client_->ClearFilter();  // Let the second ECH offering through.
+  ExpectAlert(server_, kTlsAlertIllegalParameter);
+  Handshake();
+  client_->CheckErrorCode(SSL_ERROR_ILLEGAL_PARAMETER_ALERT);
+  server_->CheckErrorCode(SSL_ERROR_BAD_2ND_CLIENT_HELLO);
+  EXPECT_EQ(1U, cb_called);
+}
+
+// Change the ECHCipherSuite between CH1 and CH2. Expect alert.
+TEST_F(TlsConnectStreamTls13, EchHrrChangeCipherSuite) {
+  ConfigureSelfEncrypt();
+  EnsureTlsSetup();
+  SetupEch(client_, server_);
+
+  size_t cb_called = 0;
+  EXPECT_EQ(SECSuccess, SSL_HelloRetryRequestCallback(
+                            server_->ssl_fd(), RetryEchHello, &cb_called));
+  // Start the handshake and trigger HRR.
+  client_->StartConnect();
+  server_->StartConnect();
+  client_->Handshake();
+  server_->Handshake();
+  MakeNewServer();
+
+  // Damage the first byte of the ciphersuite (offset 0)
+  MakeTlsFilter<TlsExtensionDamager>(client_,
+                                     ssl_tls13_encrypted_client_hello_xtn, 0);
+
   ExpectAlert(server_, kTlsAlertIllegalParameter);
   Handshake();
   client_->CheckErrorCode(SSL_ERROR_ILLEGAL_PARAMETER_ALERT);
   server_->CheckErrorCode(SSL_ERROR_BAD_2ND_CLIENT_HELLO);
   EXPECT_EQ(1U, cb_called);
 }
 
 // Configure an external PSK. Generate an HRR off CH1Inner (which contains
 // the PSK extension). Use the same PSK in CH2 and connect.
 TEST_F(TlsConnectStreamTls13, EchAcceptWithHrrAndPsk) {
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   ConfigureSelfEncrypt();
   EnsureTlsSetup();
 
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kDefaultSuites,
-                                        kPublicName, 100, record, pub, priv);
+                                        kPublicName, 100, echconfig, pub, priv);
   ASSERT_EQ(SECSuccess,
             SSL_SetServerEchConfigs(server_->ssl_fd(), pub.get(), priv.get(),
-                                    record.data(), record.len()));
-  ASSERT_EQ(SECSuccess, SSL_SetClientEchConfigs(client_->ssl_fd(),
-                                                record.data(), record.len()));
+                                    echconfig.data(), echconfig.len()));
+  ASSERT_EQ(SECSuccess,
+            SSL_SetClientEchConfigs(client_->ssl_fd(), echconfig.data(),
+                                    echconfig.len()));
   client_->ExpectEch();
   server_->ExpectEch();
 
   size_t cb_called = 0;
   EXPECT_EQ(SECSuccess, SSL_HelloRetryRequestCallback(
                             server_->ssl_fd(), RetryEchHello, &cb_called));
 
   static const uint8_t key_buf[16] = {0};
@@ -917,17 +1062,17 @@ TEST_F(TlsConnectStreamTls13, EchAcceptW
   // Start the handshake.
   client_->StartConnect();
   server_->StartConnect();
   client_->Handshake();
   server_->Handshake();
   MakeNewServer();
   ASSERT_EQ(SECSuccess,
             SSL_SetServerEchConfigs(server_->ssl_fd(), pub.get(), priv.get(),
-                                    record.data(), record.len()));
+                                    echconfig.data(), echconfig.len()));
   client_->ExpectEch();
   server_->ExpectEch();
   EXPECT_EQ(SECSuccess,
             SSL_AddExternalPsk0Rtt(server_->ssl_fd(), key.get(),
                                    reinterpret_cast<const uint8_t*>(label),
                                    strlen(label), ssl_hash_sha256, 0, 1000));
   server_->ExpectPsk();
   Handshake();
@@ -935,17 +1080,17 @@ TEST_F(TlsConnectStreamTls13, EchAcceptW
   CheckConnected();
   SendReceive();
 }
 
 // Generate an HRR on CHOuter. Reject ECH on the second CH.
 TEST_F(TlsConnectStreamTls13Ech, EchRejectWithHrr) {
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   ConfigureSelfEncrypt();
   EnsureTlsSetup();
   SetupForEchRetry();
 
   size_t cb_called = 0;
   EXPECT_EQ(SECSuccess, SSL_HelloRetryRequestCallback(
                             server_->ssl_fd(), RetryEchHello, &cb_called));
   client_->SetAuthCertificateCallback(AuthCompleteSuccess);
@@ -961,29 +1106,30 @@ TEST_F(TlsConnectStreamTls13Ech, EchReje
   ExpectAlert(client_, kTlsAlertEchRequired);
   Handshake();
   client_->CheckErrorCode(SSL_ERROR_ECH_RETRY_WITHOUT_ECH);
   server_->ExpectReceiveAlert(kTlsAlertEchRequired, kTlsAlertFatal);
   server_->Handshake();
   EXPECT_EQ(1U, cb_called);
 }
 
-// Reject ECH on CH1 and (HRR) CH2. PSKs are no longer allowed
-// in CHOuter, but can still make sure the handshake succeeds.
-// (prompting ech_required at the completion).
+// Reject ECH on CH1 and CH2. PSKs are no longer allowed
+// in CHOuter, but we can still make sure the handshake succeeds.
+// This prompts an ech_required alert when the handshake completes.
 TEST_F(TlsConnectStreamTls13, EchRejectWithHrrAndPsk) {
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   ConfigureSelfEncrypt();
   EnsureTlsSetup();
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kDefaultSuites,
-                                        kPublicName, 100, record, pub, priv);
-  ASSERT_EQ(SECSuccess, SSL_SetClientEchConfigs(client_->ssl_fd(),
-                                                record.data(), record.len()));
+                                        kPublicName, 100, echconfig, pub, priv);
+  ASSERT_EQ(SECSuccess,
+            SSL_SetClientEchConfigs(client_->ssl_fd(), echconfig.data(),
+                                    echconfig.len()));
 
   size_t cb_called = 0;
   EXPECT_EQ(SECSuccess, SSL_HelloRetryRequestCallback(
                             server_->ssl_fd(), RetryEchHello, &cb_called));
 
   // Add a PSK to both endpoints.
   static const uint8_t key_buf[16] = {0};
   SECItem key_item = {siBuffer, const_cast<uint8_t*>(&key_buf[0]),
@@ -1108,48 +1254,48 @@ TEST_F(TlsConnectStreamTls13, EchZeroRtt
   server_->ExpectReceiveAlert(kTlsAlertCloseNotify, kTlsAlertWarning);
 }
 
 // Test a critical extension in ECHConfig
 TEST_F(TlsConnectStreamTls13, EchRejectUnknownCriticalExtension) {
   EnsureTlsSetup();
   ScopedSECKEYPublicKey pub;
   ScopedSECKEYPrivateKey priv;
-  DataBuffer record;
+  DataBuffer echconfig;
   DataBuffer crit_rec;
   DataBuffer len_buf;
   uint64_t tmp;
 
   static const uint8_t crit_extensions[] = {0x00, 0x04, 0xff, 0xff, 0x00, 0x00};
   static const uint8_t extensions[] = {0x00, 0x04, 0x7f, 0xff, 0x00, 0x00};
   DataBuffer crit_exts(crit_extensions, sizeof(crit_extensions));
   DataBuffer non_crit_exts(extensions, sizeof(extensions));
 
   TlsConnectTestBase::GenerateEchConfig(HpkeDhKemX25519Sha256, kSuiteChaCha,
-                                        kPublicName, 100, record, pub, priv);
-  record.Truncate(record.len() - 2);  // Eat the empty extensions.
-  crit_rec.Assign(record);
+                                        kPublicName, 100, echconfig, pub, priv);
+  echconfig.Truncate(echconfig.len() - 2);  // Eat the empty extensions.
+  crit_rec.Assign(echconfig);
   ASSERT_TRUE(crit_rec.Read(0, 2, &tmp));
   len_buf.Write(0, tmp + crit_exts.len() - 2, 2);  // two bytes of length
   crit_rec.Splice(len_buf, 0, 2);
   len_buf.Truncate(0);
 
   ASSERT_TRUE(crit_rec.Read(4, 2, &tmp));
   len_buf.Write(0, tmp + crit_exts.len() - 2, 2);  // two bytes of length
   crit_rec.Append(crit_exts);
   crit_rec.Splice(len_buf, 4, 2);
   len_buf.Truncate(0);
 
-  ASSERT_TRUE(record.Read(0, 2, &tmp));
+  ASSERT_TRUE(echconfig.Read(0, 2, &tmp));
   len_buf.Write(0, tmp + non_crit_exts.len() - 2, 2);
-  record.Append(non_crit_exts);
-  record.Splice(len_buf, 0, 2);
-  ASSERT_TRUE(record.Read(4, 2, &tmp));
+  echconfig.Append(non_crit_exts);
+  echconfig.Splice(len_buf, 0, 2);
+  ASSERT_TRUE(echconfig.Read(4, 2, &tmp));
   len_buf.Write(0, tmp + non_crit_exts.len() - 2, 2);
-  record.Splice(len_buf, 4, 2);
+  echconfig.Splice(len_buf, 4, 2);
 
   EXPECT_EQ(SECFailure,
             SSL_SetClientEchConfigs(client_->ssl_fd(), crit_rec.data(),
                                     crit_rec.len()));
   EXPECT_EQ(SEC_ERROR_UNKNOWN_CRITICAL_EXTENSION, PORT_GetError());
   EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(client_->ssl_fd(),
                                                  PR_FALSE));  // Don't GREASE
   auto filter = MakeTlsFilter<TlsExtensionCapture>(
@@ -1157,18 +1303,19 @@ TEST_F(TlsConnectStreamTls13, EchRejectU
   StartConnect();
   client_->Handshake();
   ASSERT_EQ(TlsAgent::STATE_CONNECTING, client_->state());
   ASSERT_FALSE(filter->captured());
 
   // Now try a variant with non-critical extensions, it should work.
   Reset();
   EnsureTlsSetup();
-  EXPECT_EQ(SECSuccess, SSL_SetClientEchConfigs(client_->ssl_fd(),
-                                                record.data(), record.len()));
+  EXPECT_EQ(SECSuccess,
+            SSL_SetClientEchConfigs(client_->ssl_fd(), echconfig.data(),
+                                    echconfig.len()));
   filter = MakeTlsFilter<TlsExtensionCapture>(
       client_, ssl_tls13_encrypted_client_hello_xtn);
   StartConnect();
   client_->Handshake();
   ASSERT_EQ(TlsAgent::STATE_CONNECTING, client_->state());
   ASSERT_TRUE(filter->captured());
 }
 
@@ -1572,16 +1719,44 @@ TEST_F(TlsConnectStreamTls13, EchOuterEx
                                       ssl_tls13_outer_extensions_xtn,
                                       outer_buf);
 
   ConnectExpectAlert(server_, kTlsAlertUnsupportedExtension);
   client_->CheckErrorCode(SSL_ERROR_UNSUPPORTED_EXTENSION_ALERT);
   server_->CheckErrorCode(SSL_ERROR_RX_MALFORMED_CLIENT_HELLO);
 }
 
+// At draft-09: If a CH containing the ech_is_inner extension is received, the
+// server acts as backend server in split-mode by responding with the ECH
+// acceptance signal. The signal value itself depends on the handshake secret,
+// which we've broken by appending ech_is_inner. For now, just check that the
+// server negotiates ech_is_inner (which is what triggers sending the signal).
+TEST_F(TlsConnectStreamTls13, EchBackendAcceptance) {
+  DataBuffer ch_buf;
+  static uint8_t empty_buf[1] = {0};
+  DataBuffer empty(empty_buf, 0);
+
+  EnsureTlsSetup();
+  StartConnect();
+  EXPECT_EQ(SECSuccess, SSL_EnableTls13GreaseEch(client_->ssl_fd(), PR_FALSE));
+  MakeTlsFilter<TlsExtensionAppender>(client_, kTlsHandshakeClientHello,
+                                      ssl_tls13_ech_is_inner_xtn, empty);
+
+  EXPECT_EQ(SECSuccess, SSL_EnableTls13BackendEch(server_->ssl_fd(), PR_TRUE));
+  client_->Handshake();
+  server_->Handshake();
+
+  ExpectAlert(client_, kTlsAlertBadRecordMac);
+  client_->Handshake();
+  EXPECT_EQ(TlsAgent::STATE_ERROR, client_->state());
+  EXPECT_EQ(PR_TRUE, SSLInt_ExtensionNegotiated(server_->ssl_fd(),
+                                                ssl_tls13_ech_is_inner_xtn));
+  server_->ExpectReceiveAlert(kTlsAlertCloseNotify, kTlsAlertWarning);
+}
+
 INSTANTIATE_TEST_SUITE_P(EchAgentTest, TlsAgentEchTest,
                          ::testing::Combine(TlsConnectTestBase::kTlsVariantsAll,
                                             TlsConnectTestBase::kTlsV13));
 #else
 
 TEST_P(TlsAgentEchTest, NoEchWithoutHpke) {
   EnsureInit();
   uint8_t non_null[1];
--- a/security/nss/lib/nss/nss.def
+++ b/security/nss/lib/nss/nss.def
@@ -1208,8 +1208,15 @@ PK11_ImportDataKey;
 ;+    global:
 CERT_AddCertToListHeadWithData;
 CERT_AddCertToListTailWithData;
 PK11_PubWrapSymKeyWithMechanism;
 PK11_PubUnwrapSymKeyWithMechanism;
 ;+    local:
 ;+       *;
 ;+};
+;+NSS_3.62 { 	# NSS 3.62 release
+;+    global:
+PK11_HPKE_ExportContext;
+PK11_HPKE_ImportContext;
+;+    local:
+;+       *;
+;+};
\ No newline at end of file
--- a/security/nss/lib/nss/nss.h
+++ b/security/nss/lib/nss/nss.h
@@ -17,22 +17,22 @@
 
 /*
  * NSS's major version, minor version, patch level, build number, and whether
  * this is a beta release.
  *
  * The format of the version string should be
  *     "<major version>.<minor version>[.<patch level>[.<build number>]][ <ECC>][ <Beta>]"
  */
-#define NSS_VERSION "3.61" _NSS_CUSTOMIZED
+#define NSS_VERSION "3.62" _NSS_CUSTOMIZED " Beta"
 #define NSS_VMAJOR 3
-#define NSS_VMINOR 61
+#define NSS_VMINOR 62
 #define NSS_VPATCH 0
 #define NSS_VBUILD 0
-#define NSS_BETA PR_FALSE
+#define NSS_BETA PR_TRUE
 
 #ifndef RC_INVOKED
 
 #include "seccomon.h"
 
 typedef struct NSSInitParametersStr NSSInitParameters;
 
 /*
--- a/security/nss/lib/pk11wrap/pk11hpke.c
+++ b/security/nss/lib/pk11wrap/pk11hpke.c
@@ -1,10 +1,10 @@
 /*
- * draft-irtf-cfrg-hpke-05
+ * draft-irtf-cfrg-hpke-07
  *
  * 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 "keyhi.h"
 #include "pkcs11t.h"
@@ -48,22 +48,34 @@ PK11_HPKE_DestroyContext(HpkeContext *cx
 }
 const SECItem *
 PK11_HPKE_GetEncapPubKey(const HpkeContext *cx)
 {
     PORT_SetError(SEC_ERROR_INVALID_ALGORITHM);
     return NULL;
 }
 SECStatus
+PK11_HPKE_ExportContext(const HpkeContext *cx, PK11SymKey *wrapKey, SECItem **serialized)
+{
+    PORT_SetError(SEC_ERROR_INVALID_ALGORITHM);
+    return SECFailure;
+}
+SECStatus
 PK11_HPKE_ExportSecret(const HpkeContext *cx, const SECItem *info,
                        unsigned int L, PK11SymKey **outKey)
 {
     PORT_SetError(SEC_ERROR_INVALID_ALGORITHM);
     return SECFailure;
 }
+HpkeContext *
+PK11_HPKE_ImportContext(const SECItem *serialized, PK11SymKey *wrapKey)
+{
+    PORT_SetError(SEC_ERROR_INVALID_ALGORITHM);
+    return NULL;
+}
 SECStatus
 PK11_HPKE_Open(HpkeContext *cx, const SECItem *aad, const SECItem *ct,
                SECItem **outPt)
 {
     PORT_SetError(SEC_ERROR_INVALID_ALGORITHM);
     return SECFailure;
 }
 SECStatus
@@ -89,55 +101,60 @@ SECStatus
 PK11_HPKE_SetupR(HpkeContext *cx, const SECKEYPublicKey *pkR, SECKEYPrivateKey *skR,
                  const SECItem *enc, const SECItem *info)
 {
     PORT_SetError(SEC_ERROR_INVALID_ALGORITHM);
     return SECFailure;
 }
 
 #else
-static const char *DRAFT_LABEL = "HPKE-05 ";
+#define SERIALIZATION_VERSION 1
+
+static const char *DRAFT_LABEL = "HPKE-07";
 static const char *EXP_LABEL = "exp";
 static const char *HPKE_LABEL = "HPKE";
 static const char *INFO_LABEL = "info_hash";
 static const char *KEM_LABEL = "KEM";
 static const char *KEY_LABEL = "key";
-static const char *NONCE_LABEL = "nonce";
+static const char *NONCE_LABEL = "base_nonce";
 static const char *PSK_ID_LABEL = "psk_id_hash";
-static const char *PSK_LABEL = "psk_hash";
 static const char *SECRET_LABEL = "secret";
 static const char *SEC_LABEL = "sec";
 static const char *EAE_PRK_LABEL = "eae_prk";
 static const char *SH_SEC_LABEL = "shared_secret";
 
 struct HpkeContextStr {
     const hpkeKemParams *kemParams;
     const hpkeKdfParams *kdfParams;
     const hpkeAeadParams *aeadParams;
     PRUint8 mode;               /* Base and PSK modes supported. */
     SECItem *encapPubKey;       /* Marshalled public key, sent to receiver. */
-    SECItem *nonce;             /* Deterministic nonce for AEAD. */
+    SECItem *baseNonce;         /* Deterministic nonce for AEAD. */
     SECItem *pskId;             /* PSK identifier (non-secret). */
     PK11Context *aeadContext;   /* AEAD context used by Seal/Open. */
     PRUint64 sequenceNumber;    /* seqNo for decrypt IV construction. */
     PK11SymKey *sharedSecret;   /* ExtractAndExpand output key. */
     PK11SymKey *key;            /* Key used with the AEAD. */
     PK11SymKey *exporterSecret; /* Derivation key for ExportSecret. */
     PK11SymKey *psk;            /* PSK imported by the application. */
 };
 
 static const hpkeKemParams kemParams[] = {
     /* KEM, Nsk, Nsecret, Npk, oidTag, Hash mechanism  */
     { HpkeDhKemX25519Sha256, 32, 32, 32, SEC_OID_CURVE25519, CKM_SHA256 },
 };
 
+#define MAX_WRAPPED_EXP_LEN 72   // Largest kdfParams->Nh + 8
 static const hpkeKdfParams kdfParams[] = {
     /* KDF, Nh, mechanism  */
     { HpkeKdfHkdfSha256, SHA256_LENGTH, CKM_SHA256 },
+    { HpkeKdfHkdfSha384, SHA384_LENGTH, CKM_SHA384 },
+    { HpkeKdfHkdfSha512, SHA512_LENGTH, CKM_SHA512 },
 };
+#define MAX_WRAPPED_KEY_LEN 40   // Largest aeadParams->Nk + 8
 static const hpkeAeadParams aeadParams[] = {
     /* AEAD, Nk, Nn, tagLen, mechanism  */
     { HpkeAeadAes128Gcm, 16, 12, 16, CKM_AES_GCM },
     { HpkeAeadChaCha20Poly1305, 32, 12, 16, CKM_CHACHA20_POLY1305 },
 };
 
 static inline const hpkeKemParams *
 kemId2Params(HpkeKemId kemId)
@@ -151,16 +168,20 @@ kemId2Params(HpkeKemId kemId)
 }
 
 static inline const hpkeKdfParams *
 kdfId2Params(HpkeKdfId kdfId)
 {
     switch (kdfId) {
         case HpkeKdfHkdfSha256:
             return &kdfParams[0];
+        case HpkeKdfHkdfSha384:
+            return &kdfParams[1];
+        case HpkeKdfHkdfSha512:
+            return &kdfParams[2];
         default:
             return NULL;
     }
 }
 
 static const inline hpkeAeadParams *
 aeadId2Params(HpkeAeadId aeadId)
 {
@@ -169,26 +190,40 @@ aeadId2Params(HpkeAeadId aeadId)
             return &aeadParams[0];
         case HpkeAeadChaCha20Poly1305:
             return &aeadParams[1];
         default:
             return NULL;
     }
 }
 
-static SECStatus
-encodeShort(PRUint32 val, PRUint8 *b)
+static PRUint8 *
+encodeNumber(PRUint64 value, PRUint8 *b, size_t count)
 {
-    if (val > 0xFFFF || !b) {
-        PORT_SetError(SEC_ERROR_INVALID_ARGS);
-        return SECFailure;
+    PRUint64 encoded;
+    PORT_Assert(b && count > 0 && count <= sizeof(encoded));
+
+    encoded = PR_htonll(value);
+    PORT_Memcpy(b, ((unsigned char *)(&encoded)) + (sizeof(encoded) - count),
+                count);
+    return b + count;
+}
+
+static PRUint8 *
+decodeNumber(PRUint64 *value, PRUint8 *b, size_t count)
+{
+    unsigned int i;
+    PRUint64 number = 0;
+    PORT_Assert(b && value && count <= sizeof(*value));
+
+    for (i = 0; i < count; i++) {
+        number = (number << 8) + b[i];
     }
-    b[0] = (val >> 8) & 0xff;
-    b[1] = val & 0xff;
-    return SECSuccess;
+    *value = number;
+    return b + count;
 }
 
 SECStatus
 PK11_HPKE_ValidateParameters(HpkeKemId kemId, HpkeKdfId kdfId, HpkeAeadId aeadId)
 {
     /* If more variants are added, ensure the combination is also
      * legal. For now it is, since only the AEAD may vary. */
     const hpkeKemParams *kem = kemId2Params(kemId);
@@ -263,30 +298,270 @@ PK11_HPKE_DestroyContext(HpkeContext *cx
         PK11_DestroyContext((PK11Context *)cx->aeadContext, PR_TRUE);
         cx->aeadContext = NULL;
     }
     PK11_FreeSymKey(cx->exporterSecret);
     PK11_FreeSymKey(cx->sharedSecret);
     PK11_FreeSymKey(cx->key);
     PK11_FreeSymKey(cx->psk);
     SECITEM_FreeItem(cx->pskId, PR_TRUE);
-    SECITEM_FreeItem(cx->nonce, PR_TRUE);
+    SECITEM_FreeItem(cx->baseNonce, PR_TRUE);
     SECITEM_FreeItem(cx->encapPubKey, PR_TRUE);
     cx->exporterSecret = NULL;
     cx->sharedSecret = NULL;
     cx->key = NULL;
     cx->psk = NULL;
     cx->pskId = NULL;
-    cx->nonce = NULL;
+    cx->baseNonce = NULL;
     cx->encapPubKey = NULL;
     if (freeit) {
         PORT_ZFree(cx, sizeof(HpkeContext));
     }
 }
 
+/* Export Format:
+    struct {
+        uint8 serilizationVersion;
+        uint8 hpkeVersion;
+        uint16 kemId;
+        uint16 kdfId;
+        uint16 aeadId;
+        uint16 modeId;
+        uint64 sequenceNumber;
+        opaque senderPubKey<1..2^16-1>;
+        opaque baseNonce<1..2^16-1>;
+        opaque key<1..2^16-1>;
+        opaque exporterSecret<1..2^16-1>;
+    } HpkeSerializedContext
+*/
+#define EXPORTED_CTX_BASE_LEN 26 /* Fixed size plus 2B for each variable. */
+#define REMAINING_BYTES(walker, buf) \
+    buf->len - (walker - buf->data)
+SECStatus
+PK11_HPKE_ExportContext(const HpkeContext *cx, PK11SymKey *wrapKey, SECItem **serialized)
+{
+    SECStatus rv;
+    size_t allocLen;
+    PRUint8 *walker;
+    SECItem *keyBytes = NULL;      // Maybe wrapped
+    SECItem *exporterBytes = NULL; // Maybe wrapped
+    SECItem *serializedCx = NULL;
+    PRUint8 wrappedKeyBytes[MAX_WRAPPED_KEY_LEN] = { 0 };
+    PRUint8 wrappedExpBytes[MAX_WRAPPED_EXP_LEN] = { 0 };
+    SECItem wrappedKey = { siBuffer, wrappedKeyBytes, sizeof(wrappedKeyBytes) };
+    SECItem wrappedExp = { siBuffer, wrappedExpBytes, sizeof(wrappedExpBytes) };
+
+    CHECK_FAIL_ERR((!cx || !cx->aeadContext || !serialized), SEC_ERROR_INVALID_ARGS);
+    CHECK_FAIL_ERR((cx->aeadContext->operation != (CKA_NSS_MESSAGE | CKA_DECRYPT)),
+                   SEC_ERROR_NOT_A_RECIPIENT);
+
+    /* If a wrapping key was provided, do the wrap first
+     * so that we know what size to allocate. */
+    if (wrapKey) {
+        rv = PK11_WrapSymKey(CKM_AES_KEY_WRAP_KWP, NULL, wrapKey,
+                             cx->key, &wrappedKey);
+        CHECK_RV(rv);
+        rv = PK11_WrapSymKey(CKM_AES_KEY_WRAP_KWP, NULL, wrapKey,
+                             cx->exporterSecret, &wrappedExp);
+        CHECK_RV(rv);
+
+        keyBytes = &wrappedKey;
+        exporterBytes = &wrappedExp;
+    } else {
+        rv = PK11_ExtractKeyValue(cx->key);
+        CHECK_RV(rv);
+        keyBytes = PK11_GetKeyData(cx->key);
+        CHECK_FAIL(!keyBytes);
+        PORT_Assert(keyBytes->len == cx->aeadParams->Nk);
+
+        rv = PK11_ExtractKeyValue(cx->exporterSecret);
+        CHECK_RV(rv);
+        exporterBytes = PK11_GetKeyData(cx->exporterSecret);
+        CHECK_FAIL(!exporterBytes);
+        PORT_Assert(exporterBytes->len == cx->kdfParams->Nh);
+    }
+
+    allocLen = EXPORTED_CTX_BASE_LEN + cx->baseNonce->len + cx->encapPubKey->len;
+    allocLen += wrapKey ? wrappedKey.len : cx->aeadParams->Nk;
+    allocLen += wrapKey ? wrappedExp.len : cx->kdfParams->Nh;
+
+    serializedCx = SECITEM_AllocItem(NULL, NULL, allocLen);
+    CHECK_FAIL(!serializedCx);
+
+    walker = &serializedCx->data[0];
+    *(walker)++ = (PRUint8)SERIALIZATION_VERSION;
+    *(walker)++ = (PRUint8)HPKE_DRAFT_VERSION;
+
+    walker = encodeNumber(cx->kemParams->id, walker, 2);
+    walker = encodeNumber(cx->kdfParams->id, walker, 2);
+    walker = encodeNumber(cx->aeadParams->id, walker, 2);
+    walker = encodeNumber(cx->mode, walker, 2);
+    walker = encodeNumber(cx->sequenceNumber, walker, 8);
+
+    /* sender public key, serialized. */
+    walker = encodeNumber(cx->encapPubKey->len, walker, 2);
+    PORT_Memcpy(walker, cx->encapPubKey->data, cx->encapPubKey->len);
+    walker += cx->encapPubKey->len;
+
+    /* base nonce */
+    walker = encodeNumber(cx->baseNonce->len, walker, 2);
+    PORT_Memcpy(walker, cx->baseNonce->data, cx->baseNonce->len);
+    walker += cx->baseNonce->len;
+
+    /* key. */
+    walker = encodeNumber(keyBytes->len, walker, 2);
+    PORT_Memcpy(walker, keyBytes->data, keyBytes->len);
+    walker += keyBytes->len;
+
+    /* exporter_secret. */
+    walker = encodeNumber(exporterBytes->len, walker, 2);
+    PORT_Memcpy(walker, exporterBytes->data, exporterBytes->len);
+    walker += exporterBytes->len;
+
+    CHECK_FAIL_ERR(REMAINING_BYTES(walker, serializedCx) != 0,
+                   SEC_ERROR_LIBRARY_FAILURE);
+    *serialized = serializedCx;
+
+CLEANUP:
+    if (rv != SECSuccess) {
+        SECITEM_ZfreeItem(serializedCx, PR_TRUE);
+    }
+    return rv;
+}
+
+HpkeContext *
+PK11_HPKE_ImportContext(const SECItem *serialized, PK11SymKey *wrapKey)
+{
+    SECStatus rv = SECSuccess;
+    HpkeContext *cx = NULL;
+    PRUint8 *walker;
+    PRUint64 tmpn;
+    PRUint8 tmp8;
+    HpkeKemId kem;
+    HpkeKdfId kdf;
+    HpkeAeadId aead;
+    PK11SlotInfo *slot = NULL;
+    PK11SymKey *tmpKey = NULL;
+    SECItem tmpItem = { siBuffer, NULL, 0 };
+    SECItem emptyItem = { siBuffer, NULL, 0 };
+
+    CHECK_FAIL_ERR((!serialized || !serialized->data || serialized->len == 0),
+                   SEC_ERROR_INVALID_ARGS);
+    CHECK_FAIL_ERR((serialized->len < EXPORTED_CTX_BASE_LEN), SEC_ERROR_BAD_DATA);
+
+    walker = serialized->data;
+
+    tmp8 = *(walker++);
+    CHECK_FAIL_ERR((tmp8 != SERIALIZATION_VERSION), SEC_ERROR_BAD_DATA);
+    tmp8 = *(walker++);
+    CHECK_FAIL_ERR((tmp8 != HPKE_DRAFT_VERSION), SEC_ERROR_INVALID_ALGORITHM);
+
+    walker = decodeNumber(&tmpn, walker, 2);
+    kem = (HpkeKemId)tmpn;
+
+    walker = decodeNumber(&tmpn, walker, 2);
+    kdf = (HpkeKdfId)tmpn;
+
+    walker = decodeNumber(&tmpn, walker, 2);
+    aead = (HpkeAeadId)tmpn;
+
+    /* Create context. We'll manually set the mode, though we
+     * no longer have the PSK and have no need for it. */
+    cx = PK11_HPKE_NewContext(kem, kdf, aead, NULL, NULL);
+    CHECK_FAIL(!cx);
+
+    walker = decodeNumber(&tmpn, walker, 2);
+    CHECK_FAIL_ERR((tmpn != HpkeModeBase && tmpn != HpkeModePsk),
+                   SEC_ERROR_BAD_DATA);
+    cx->mode = (HpkeModeId)tmpn;
+
+    walker = decodeNumber(&cx->sequenceNumber, walker, 8);
+    slot = PK11_GetBestSlot(CKM_HKDF_DERIVE, NULL);
+    CHECK_FAIL(!slot);
+
+    /* Import sender public key (serialized). */
+    walker = decodeNumber(&tmpn, walker, 2);
+    CHECK_FAIL_ERR(tmpn >= REMAINING_BYTES(walker, serialized),
+                   SEC_ERROR_BAD_DATA);
+    tmpItem.data = walker;
+    tmpItem.len = tmpn;
+    cx->encapPubKey = SECITEM_DupItem(&tmpItem);
+    CHECK_FAIL(!cx->encapPubKey);
+    walker += tmpItem.len;
+
+    /* Import base_nonce. */
+    walker = decodeNumber(&tmpn, walker, 2);
+    CHECK_FAIL_ERR(tmpn != cx->aeadParams->Nn, SEC_ERROR_BAD_DATA);
+    CHECK_FAIL_ERR(tmpn >= REMAINING_BYTES(walker, serialized),
+                   SEC_ERROR_BAD_DATA);
+    tmpItem.data = walker;
+    tmpItem.len = tmpn;
+    cx->baseNonce = SECITEM_DupItem(&tmpItem);
+    CHECK_FAIL(!cx->baseNonce);
+    walker += tmpItem.len;
+
+    /* Import key */
+    walker = decodeNumber(&tmpn, walker, 2);
+    CHECK_FAIL_ERR(tmpn >= REMAINING_BYTES(walker, serialized),
+                   SEC_ERROR_BAD_DATA);
+    tmpItem.data = walker;
+    tmpItem.len = tmpn;
+    walker += tmpItem.len;
+    if (wrapKey) {
+        cx->key = PK11_UnwrapSymKey(wrapKey, CKM_AES_KEY_WRAP_KWP,
+                                    NULL, &tmpItem, cx->aeadParams->mech,
+                                    CKA_NSS_MESSAGE | CKA_DECRYPT, 0);
+        CHECK_FAIL(!cx->key);
+    } else {
+        CHECK_FAIL_ERR(tmpn != cx->aeadParams->Nk, SEC_ERROR_BAD_DATA);
+        tmpKey = PK11_ImportSymKey(slot, cx->aeadParams->mech,
+                                   PK11_OriginUnwrap, CKA_NSS_MESSAGE | CKA_DECRYPT,
+                                   &tmpItem, NULL);
+        CHECK_FAIL(!tmpKey);
+        cx->key = tmpKey;
+    }
+
+    /* Import exporter_secret. */
+    walker = decodeNumber(&tmpn, walker, 2);
+    CHECK_FAIL_ERR(tmpn != REMAINING_BYTES(walker, serialized),
+                   SEC_ERROR_BAD_DATA);
+    tmpItem.data = walker;
+    tmpItem.len = tmpn;
+    walker += tmpItem.len;
+
+    if (wrapKey) {
+        cx->exporterSecret = PK11_UnwrapSymKey(wrapKey, CKM_AES_KEY_WRAP_KWP,
+                                               NULL, &tmpItem, cx->kdfParams->mech,
+                                               CKM_HKDF_DERIVE, 0);
+        CHECK_FAIL(!cx->exporterSecret);
+    } else {
+        CHECK_FAIL_ERR(tmpn != cx->kdfParams->Nh, SEC_ERROR_BAD_DATA);
+        tmpKey = PK11_ImportSymKey(slot, CKM_HKDF_DERIVE, PK11_OriginUnwrap,
+                                   CKA_DERIVE, &tmpItem, NULL);
+        CHECK_FAIL(!tmpKey);
+        cx->exporterSecret = tmpKey;
+    }
+
+    cx->aeadContext = PK11_CreateContextBySymKey(cx->aeadParams->mech,
+                                                 CKA_NSS_MESSAGE | CKA_DECRYPT,
+                                                 cx->key, &emptyItem);
+
+CLEANUP:
+    if (rv != SECSuccess) {
+        PK11_FreeSymKey(tmpKey);
+        PK11_HPKE_DestroyContext(cx, PR_TRUE);
+        cx = NULL;
+    }
+    if (slot) {
+        PK11_FreeSlot(slot);
+    }
+
+    return cx;
+}
+
 SECStatus
 PK11_HPKE_Serialize(const SECKEYPublicKey *pk, PRUint8 *buf, unsigned int *len, unsigned int maxLen)
 {
     if (!pk || !len || pk->keyType != ecKey) {
         PORT_SetError(SEC_ERROR_INVALID_ARGS);
         return SECFailure;
     }
 
@@ -342,17 +617,17 @@ PK11_HPKE_Deserialize(const HpkeContext 
 
     // Create parameters.
     CHECK_FAIL(!SECITEM_AllocItem(pubKey->arena, &pubKey->u.ec.DEREncodedParams,
                                   2 + oidData->oid.len));
 
     // Set parameters.
     pubKey->u.ec.DEREncodedParams.data[0] = SEC_ASN1_OBJECT_ID;
     pubKey->u.ec.DEREncodedParams.data[1] = oidData->oid.len;
-    memcpy(pubKey->u.ec.DEREncodedParams.data + 2, oidData->oid.data, oidData->oid.len);
+    PORT_Memcpy(pubKey->u.ec.DEREncodedParams.data + 2, oidData->oid.data, oidData->oid.len);
     *outPubKey = pubKey;
 
 CLEANUP:
     if (rv != SECSuccess) {
         SECKEY_DestroyPublicKey(pubKey);
     }
     return rv;
 };
@@ -398,17 +673,17 @@ pk11_hpke_GenerateKeyPair(const HpkeCont
     CHECK_FAIL_ERR(!oidData, SEC_ERROR_INVALID_ALGORITHM);
     ecp.data = PORT_Alloc(2 + oidData->oid.len);
     CHECK_FAIL(!ecp.data);
 
     ecp.len = 2 + oidData->oid.len;
     ecp.type = siDEROID;
     ecp.data[0] = SEC_ASN1_OBJECT_ID;
     ecp.data[1] = oidData->oid.len;
-    memcpy(&ecp.data[2], oidData->oid.data, oidData->oid.len);
+    PORT_Memcpy(&ecp.data[2], oidData->oid.data, oidData->oid.len);
 
     slot = PK11_GetBestSlot(CKM_EC_KEY_PAIR_GEN, NULL);
     CHECK_FAIL(!slot);
 
     privKey = PK11_GenerateKeyPair(slot, CKM_EC_KEY_PAIR_GEN, &ecp, &pubKey,
                                    PR_FALSE, PR_TRUE, NULL);
     CHECK_FAIL_ERR((!privKey || !pubKey), SEC_ERROR_KEYGEN_FAIL);
     PORT_Assert(rv == SECSuccess);
@@ -428,31 +703,31 @@ CLEANUP:
 }
 
 static inline SECItem *
 pk11_hpke_MakeExtractLabel(const char *prefix, unsigned int prefixLen,
                            const char *label, unsigned int labelLen,
                            const SECItem *suiteId, const SECItem *ikm)
 {
     SECItem *out = NULL;
-    size_t off = 0;
+    PRUint8 *walker;
     out = SECITEM_AllocItem(NULL, NULL, prefixLen + labelLen + suiteId->len + (ikm ? ikm->len : 0));
     if (!out) {
         return NULL;
     }
 
-    memcpy(&out->data[off], prefix, prefixLen);
-    off += prefixLen;
-    memcpy(&out->data[off], suiteId->data, suiteId->len);
-    off += suiteId->len;
-    memcpy(&out->data[off], label, labelLen);
-    off += labelLen;
+    walker = out->data;
+    PORT_Memcpy(walker, prefix, prefixLen);
+    walker += prefixLen;
+    PORT_Memcpy(walker, suiteId->data, suiteId->len);
+    walker += suiteId->len;
+    PORT_Memcpy(walker, label, labelLen);
+    walker += labelLen;
     if (ikm && ikm->data) {
-        memcpy(&out->data[off], ikm->data, ikm->len);
-        off += ikm->len;
+        PORT_Memcpy(walker, ikm->data, ikm->len);
     }
 
     return out;
 }
 
 static SECStatus
 pk11_hpke_LabeledExtractData(const HpkeContext *cx, SECItem *salt,
                              const SECItem *suiteId, const char *label,
@@ -469,17 +744,17 @@ pk11_hpke_LabeledExtractData(const HpkeC
     SECItem paramsItem = { siBuffer, (unsigned char *)&params,
                            sizeof(params) };
     PORT_Assert(cx && ikm && label && labelLen && out && suiteId);
 
     labeledIkm = pk11_hpke_MakeExtractLabel(DRAFT_LABEL, strlen(DRAFT_LABEL), label, labelLen, suiteId, ikm);
     CHECK_FAIL(!labeledIkm);
     params.bExtract = CK_TRUE;
     params.bExpand = CK_FALSE;
-    params.prfHashMechanism = cx->kemParams->hashMech;
+    params.prfHashMechanism = cx->kdfParams->mech;
     params.ulSaltType = salt ? CKF_HKDF_SALT_DATA : CKF_HKDF_SALT_NULL;
     params.pSalt = salt ? (CK_BYTE_PTR)salt->data : NULL;
     params.ulSaltLen = salt ? salt->len : 0;
     params.pInfo = labeledIkm->data;
     params.ulInfoLen = labeledIkm->len;
 
     slot = PK11_GetBestSlot(CKM_EC_KEY_PAIR_GEN, NULL);
     CHECK_FAIL(!slot);
@@ -506,17 +781,17 @@ CLEANUP:
     if (slot) {
         PK11_FreeSlot(slot);
     }
     return rv;
 }
 
 static SECStatus
 pk11_hpke_LabeledExtract(const HpkeContext *cx, PK11SymKey *salt,
-                         const SECItem *suiteId, const char *label,
+                         const SECItem *suiteId, const char *label, CK_MECHANISM_TYPE hashMech,
                          unsigned int labelLen, PK11SymKey *ikm, PK11SymKey **out)
 {
     SECStatus rv = SECSuccess;
     SECItem *innerLabel = NULL;
     PK11SymKey *labeledIkm = NULL;
     PK11SymKey *prk = NULL;
     CK_HKDF_PARAMS params = { 0 };
     CK_KEY_DERIVATION_STRING_DATA labelData;
@@ -532,17 +807,17 @@ pk11_hpke_LabeledExtract(const HpkeConte
     labelDataItem.data = (PRUint8 *)&labelData;
     labelDataItem.len = sizeof(labelData);
     labeledIkm = PK11_Derive(ikm, CKM_CONCATENATE_DATA_AND_BASE,
                              &labelDataItem, CKM_GENERIC_SECRET_KEY_GEN, CKA_DERIVE, 0);
     CHECK_FAIL(!labeledIkm);
 
     params.bExtract = CK_TRUE;
     params.bExpand = CK_FALSE;
-    params.prfHashMechanism = cx->kemParams->hashMech;
+    params.prfHashMechanism = hashMech;
     params.ulSaltType = salt ? CKF_HKDF_SALT_KEY : CKF_HKDF_SALT_NULL;
     params.hSaltKey = salt ? PK11_GetSymKeyHandle(salt) : CK_INVALID_HANDLE;
 
     prk = PK11_Derive(labeledIkm, CKM_HKDF_DERIVE, &paramsItem,
                       CKM_HKDF_DERIVE, CKA_DERIVE, 0);
     CHECK_FAIL(!prk);
     *out = prk;
 
@@ -550,56 +825,55 @@ CLEANUP:
     PK11_FreeSymKey(labeledIkm);
     SECITEM_ZfreeItem(innerLabel, PR_TRUE);
     return rv;
 }
 
 static SECStatus
 pk11_hpke_LabeledExpand(const HpkeContext *cx, PK11SymKey *prk, const SECItem *suiteId,
                         const char *label, unsigned int labelLen, const SECItem *info,
-                        unsigned int L, PK11SymKey **outKey, SECItem **outItem)
+                        unsigned int L, CK_MECHANISM_TYPE hashMech, PK11SymKey **outKey,
+                        SECItem **outItem)
 {
-    SECStatus rv;
+    SECStatus rv = SECSuccess;
     CK_MECHANISM_TYPE keyMech;
     CK_MECHANISM_TYPE deriveMech;
     CK_HKDF_PARAMS params = { 0 };
     PK11SymKey *derivedKey = NULL;
     SECItem *labeledInfoItem = NULL;
     SECItem paramsItem = { siBuffer, (unsigned char *)&params,
                            sizeof(params) };
     SECItem *derivedKeyData;
     PRUint8 encodedL[2];
-    size_t off = 0;
+    PRUint8 *walker = encodedL;
     size_t len;
     PORT_Assert(cx && prk && label && (!!outKey != !!outItem));
 
-    rv = encodeShort(L, encodedL);
-    CHECK_RV(rv);
-
+    walker = encodeNumber(L, walker, 2);
     len = info ? info->len : 0;
     len += sizeof(encodedL) + strlen(DRAFT_LABEL) + suiteId->len + labelLen;
     labeledInfoItem = SECITEM_AllocItem(NULL, NULL, len);
     CHECK_FAIL(!labeledInfoItem);
 
-    memcpy(&labeledInfoItem->data[off], encodedL, sizeof(encodedL));
-    off += sizeof(encodedL);
-    memcpy(&labeledInfoItem->data[off], DRAFT_LABEL, strlen(DRAFT_LABEL));
-    off += strlen(DRAFT_LABEL);
-    memcpy(&labeledInfoItem->data[off], suiteId->data, suiteId->len);
-    off += suiteId->len;
-    memcpy(&labeledInfoItem->data[off], label, labelLen);
-    off += labelLen;
+    walker = labeledInfoItem->data;
+    PORT_Memcpy(walker, encodedL, sizeof(encodedL));
+    walker += sizeof(encodedL);
+    PORT_Memcpy(walker, DRAFT_LABEL, strlen(DRAFT_LABEL));
+    walker += strlen(DRAFT_LABEL);
+    PORT_Memcpy(walker, suiteId->data, suiteId->len);
+    walker += suiteId->len;
+    PORT_Memcpy(walker, label, labelLen);
+    walker += labelLen;
     if (info) {
-        memcpy(&labeledInfoItem->data[off], info->data, info->len);
-        off += info->len;
+        PORT_Memcpy(walker, info->data, info->len);
     }
 
     params.bExtract = CK_FALSE;
     params.bExpand = CK_TRUE;
-    params.prfHashMechanism = cx->kemParams->hashMech;
+    params.prfHashMechanism = hashMech;
     params.ulSaltType = CKF_HKDF_SALT_NULL;
     params.pInfo = labeledInfoItem->data;
     params.ulInfoLen = labeledInfoItem->len;
     deriveMech = outItem ? CKM_HKDF_DATA : CKM_HKDF_DERIVE;
     /* If we're expanding to the encryption key use the appropriate mechanism. */
     keyMech = (label && !strcmp(KEY_LABEL, label)) ? cx->aeadParams->mech : CKM_HKDF_DERIVE;
 
     derivedKey = PK11_Derive(prk, deriveMech, &paramsItem, keyMech, CKA_DERIVE, L);
@@ -630,29 +904,32 @@ CLEANUP:
 static SECStatus
 pk11_hpke_ExtractAndExpand(const HpkeContext *cx, PK11SymKey *ikm,
                            const SECItem *kemContext, PK11SymKey **out)
 {
     SECStatus rv;
     PK11SymKey *eaePrk = NULL;
     PK11SymKey *sharedSecret = NULL;
     PRUint8 suiteIdBuf[5];
+    PRUint8 *walker;
     PORT_Memcpy(suiteIdBuf, KEM_LABEL, strlen(KEM_LABEL));
     SECItem suiteIdItem = { siBuffer, suiteIdBuf, sizeof(suiteIdBuf) };
     PORT_Assert(cx && ikm && kemContext && out);
 
-    rv = encodeShort(cx->kemParams->id, &suiteIdBuf[3]);
-    CHECK_RV(rv);
+    walker = &suiteIdBuf[3];
+    walker = encodeNumber(cx->kemParams->id, walker, 2);
 
     rv = pk11_hpke_LabeledExtract(cx, NULL, &suiteIdItem, EAE_PRK_LABEL,
-                                  strlen(EAE_PRK_LABEL), ikm, &eaePrk);
+                                  cx->kemParams->hashMech, strlen(EAE_PRK_LABEL),
+                                  ikm, &eaePrk);
     CHECK_RV(rv);
 
     rv = pk11_hpke_LabeledExpand(cx, eaePrk, &suiteIdItem, SH_SEC_LABEL, strlen(SH_SEC_LABEL),
-                                 kemContext, cx->kemParams->Nsecret, &sharedSecret, NULL);
+                                 kemContext, cx->kemParams->Nsecret, cx->kemParams->hashMech,
+                                 &sharedSecret, NULL);
     CHECK_RV(rv);
     *out = sharedSecret;
 
 CLEANUP:
     if (rv != SECSuccess) {
         PK11_FreeSymKey(sharedSecret);
     }
     PK11_FreeSymKey(eaePrk);
@@ -693,17 +970,17 @@ pk11_hpke_Encap(HpkeContext *cx, const S
     CHECK_RV(rv);
 
     rv = PK11_HPKE_Serialize(pkR, NULL, &tmpLen, 0);
     CHECK_RV(rv);
 
     kemContext = SECITEM_AllocItem(NULL, NULL, cx->encapPubKey->len + tmpLen);
     CHECK_FAIL(!kemContext);
 
-    memcpy(kemContext->data, cx->encapPubKey->data, cx->encapPubKey->len);
+    PORT_Memcpy(kemContext->data, cx->encapPubKey->data, cx->encapPubKey->len);
     rv = PK11_HPKE_Serialize(pkR, &kemContext->data[cx->encapPubKey->len], &tmpLen, tmpLen);
     CHECK_RV(rv);
 
     rv = pk11_hpke_ExtractAndExpand(cx, dh, kemContext, &cx->sharedSecret);
     CHECK_RV(rv);
 
 CLEANUP:
     if (rv != SECSuccess) {
@@ -718,35 +995,35 @@ CLEANUP:
 
 SECStatus
 PK11_HPKE_ExportSecret(const HpkeContext *cx, const SECItem *info, unsigned int L,
                        PK11SymKey **out)
 {
     SECStatus rv;
     PK11SymKey *exported;
     PRUint8 suiteIdBuf[10];
+    PRUint8 *walker;
     PORT_Memcpy(suiteIdBuf, HPKE_LABEL, strlen(HPKE_LABEL));
     SECItem suiteIdItem = { siBuffer, suiteIdBuf, sizeof(suiteIdBuf) };
 
     /* Arbitrary info length limit well under the specified max. */
     if (!cx || !info || (!info->data && info->len) || info->len > 0xFFFF ||
         !L || (L > 255 * cx->kdfParams->Nh)) {
         PORT_SetError(SEC_ERROR_INVALID_ARGS);
         return SECFailure;
     }
 
-    rv = encodeShort(cx->kemParams->id, &suiteIdBuf[4]);
-    CHECK_RV(rv);
-    rv = encodeShort(cx->kdfParams->id, &suiteIdBuf[6]);
-    CHECK_RV(rv);
-    rv = encodeShort(cx->aeadParams->id, &suiteIdBuf[8]);
-    CHECK_RV(rv);
+    walker = &suiteIdBuf[4];
+    walker = encodeNumber(cx->kemParams->id, walker, 2);
+    walker = encodeNumber(cx->kdfParams->id, walker, 2);
+    walker = encodeNumber(cx->aeadParams->id, walker, 2);
 
     rv = pk11_hpke_LabeledExpand(cx, cx->exporterSecret, &suiteIdItem, SEC_LABEL,
-                                 strlen(SEC_LABEL), info, L, &exported, NULL);
+                                 strlen(SEC_LABEL), info, L, cx->kdfParams->mech,
+                                 &exported, NULL);
     CHECK_RV(rv);
     *out = exported;
 
 CLEANUP:
     return rv;
 }
 
 static SECStatus
@@ -780,22 +1057,28 @@ pk11_hpke_Decap(HpkeContext *cx, const S
 
     /* kem_context = concat(enc, pkRm) */
     rv = PK11_HPKE_Serialize(pkR, NULL, &tmpLen, 0);
     CHECK_RV(rv);
 
     kemContext = SECITEM_AllocItem(NULL, NULL, encS->len + tmpLen);
     CHECK_FAIL(!kemContext);
 
-    memcpy(kemContext->data, encS->data, encS->len);
+    PORT_Memcpy(kemContext->data, encS->data, encS->len);
     rv = PK11_HPKE_Serialize(pkR, &kemContext->data[encS->len], &tmpLen,
                              kemContext->len - encS->len);
     CHECK_RV(rv);
     rv = pk11_hpke_ExtractAndExpand(cx, dh, kemContext, &cx->sharedSecret);
     CHECK_RV(rv);
+
+    /* Store the sender serialized public key, which
+     * may be required by application use cases. */
+    cx->encapPubKey = SECITEM_DupItem(encS);
+    CHECK_FAIL(!cx->encapPubKey);
+
 CLEANUP:
     if (rv != SECSuccess) {
         PK11_FreeSymKey(cx->sharedSecret);
         cx->sharedSecret = NULL;
     }
     PK11_FreeSymKey(dh);
     SECKEY_DestroyPublicKey(pkS);
     SECITEM_FreeItem(encR, PR_TRUE);
@@ -804,111 +1087,108 @@ CLEANUP:
 }
 
 const SECItem *
 PK11_HPKE_GetEncapPubKey(const HpkeContext *cx)
 {
     if (!cx) {
         return NULL;
     }
-    /* Will be NULL on receiver. */
     return cx->encapPubKey;
 }
 
 static SECStatus
 pk11_hpke_KeySchedule(HpkeContext *cx, const SECItem *info)
 {
     SECStatus rv;
     SECItem contextItem = { siBuffer, NULL, 0 };
     unsigned int len;
     unsigned int off;
-    PK11SymKey *pskHash = NULL;
     PK11SymKey *secret = NULL;
     SECItem *pskIdHash = NULL;
     SECItem *infoHash = NULL;
     PRUint8 suiteIdBuf[10];
+    PRUint8 *walker;
     PORT_Memcpy(suiteIdBuf, HPKE_LABEL, strlen(HPKE_LABEL));
     SECItem suiteIdItem = { siBuffer, suiteIdBuf, sizeof(suiteIdBuf) };
     PORT_Assert(cx && info && cx->psk && cx->pskId);
 
-    rv = encodeShort(cx->kemParams->id, &suiteIdBuf[4]);
-    CHECK_RV(rv);
-    rv = encodeShort(cx->kdfParams->id, &suiteIdBuf[6]);
-    CHECK_RV(rv);
-    rv = encodeShort(cx->aeadParams->id, &suiteIdBuf[8]);
-    CHECK_RV(rv);
+    walker = &suiteIdBuf[4];
+    walker = encodeNumber(cx->kemParams->id, walker, 2);
+    walker = encodeNumber(cx->kdfParams->id, walker, 2);
+    walker = encodeNumber(cx->aeadParams->id, walker, 2);
 
     rv = pk11_hpke_LabeledExtractData(cx, NULL, &suiteIdItem, PSK_ID_LABEL,
                                       strlen(PSK_ID_LABEL), cx->pskId, &pskIdHash);
     CHECK_RV(rv);
     rv = pk11_hpke_LabeledExtractData(cx, NULL, &suiteIdItem, INFO_LABEL,
                                       strlen(INFO_LABEL), info, &infoHash);
     CHECK_RV(rv);
 
     // Make the context string
     len = sizeof(cx->mode) + pskIdHash->len + infoHash->len;
     CHECK_FAIL(!SECITEM_AllocItem(NULL, &contextItem, len));
     off = 0;
-    memcpy(&contextItem.data[off], &cx->mode, sizeof(cx->mode));
+    PORT_Memcpy(&contextItem.data[off], &cx->mode, sizeof(cx->mode));
     off += sizeof(cx->mode);
-    memcpy(&contextItem.data[off], pskIdHash->data, pskIdHash->len);
+    PORT_Memcpy(&contextItem.data[off], pskIdHash->data, pskIdHash->len);
     off += pskIdHash->len;
-    memcpy(&contextItem.data[off], infoHash->data, infoHash->len);
+    PORT_Memcpy(&contextItem.data[off], infoHash->data, infoHash->len);
     off += infoHash->len;
 
     // Compute the keys
-    rv = pk11_hpke_LabeledExtract(cx, NULL, &suiteIdItem, PSK_LABEL,
-                                  strlen(PSK_LABEL), cx->psk, &pskHash);
-    CHECK_RV(rv);
-    rv = pk11_hpke_LabeledExtract(cx, pskHash, &suiteIdItem, SECRET_LABEL,
-                                  strlen(SECRET_LABEL), cx->sharedSecret, &secret);
+    rv = pk11_hpke_LabeledExtract(cx, cx->sharedSecret, &suiteIdItem, SECRET_LABEL,
+                                  cx->kdfParams->mech, strlen(SECRET_LABEL),
+                                  cx->psk, &secret);
     CHECK_RV(rv);
     rv = pk11_hpke_LabeledExpand(cx, secret, &suiteIdItem, KEY_LABEL, strlen(KEY_LABEL),
-                                 &contextItem, cx->aeadParams->Nk, &cx->key, NULL);
+                                 &contextItem, cx->aeadParams->Nk, cx->kdfParams->mech,
+                                 &cx->key, NULL);
     CHECK_RV(rv);
     rv = pk11_hpke_LabeledExpand(cx, secret, &suiteIdItem, NONCE_LABEL, strlen(NONCE_LABEL),
-                                 &contextItem, cx->aeadParams->Nn, NULL, &cx->nonce);
+                                 &contextItem, cx->aeadParams->Nn, cx->kdfParams->mech,
+                                 NULL, &cx->baseNonce);
     CHECK_RV(rv);
     rv = pk11_hpke_LabeledExpand(cx, secret, &suiteIdItem, EXP_LABEL, strlen(EXP_LABEL),
-                                 &contextItem, cx->kdfParams->Nh, &cx->exporterSecret, NULL);
+                                 &contextItem, cx->kdfParams->Nh, cx->kdfParams->mech,
+                                 &cx->exporterSecret, NULL);
     CHECK_RV(rv);
 
 CLEANUP:
     /* If !SECSuccess, callers will tear down the context. */
-    PK11_FreeSymKey(pskHash);
     PK11_FreeSymKey(secret);
     SECITEM_FreeItem(&contextItem, PR_FALSE);
     SECITEM_FreeItem(infoHash, PR_TRUE);
     SECITEM_FreeItem(pskIdHash, PR_TRUE);
     return rv;
 }
 
 SECStatus
 PK11_HPKE_SetupR(HpkeContext *cx, const SECKEYPublicKey *pkR, SECKEYPrivateKey *skR,
                  const SECItem *enc, const SECItem *info)
 {
     SECStatus rv;
-    SECItem nullParams = { siBuffer, NULL, 0 };
+    SECItem empty = { siBuffer, NULL, 0 };
 
     CHECK_FAIL_ERR((!cx || !skR || !info || !enc || !enc->data || !enc->len),
                    SEC_ERROR_INVALID_ARGS);
     /* Already setup */
     CHECK_FAIL_ERR((cx->aeadContext), SEC_ERROR_INVALID_STATE);
 
     rv = pk11_hpke_Decap(cx, pkR, skR, enc);
     CHECK_RV(rv);
     rv = pk11_hpke_KeySchedule(cx, info);
     CHECK_RV(rv);
 
     /* Store the key context for subsequent calls to Open().
      * PK11_CreateContextBySymKey refs the key internally. */
     PORT_Assert(cx->key);
     cx->aeadContext = PK11_CreateContextBySymKey(cx->aeadParams->mech,
                                                  CKA_NSS_MESSAGE | CKA_DECRYPT,
-                                                 cx->key, &nullParams);
+                                                 cx->key, &empty);
     CHECK_FAIL_ERR((!cx->aeadContext), SEC_ERROR_LIBRARY_FAILURE);
 
 CLEANUP:
     if (rv != SECSuccess) {
         /* Clear everything past NewContext. */
         PK11_HPKE_DestroyContext(cx, PR_FALSE);
     }
     return rv;
@@ -968,47 +1248,47 @@ PK11_HPKE_Seal(HpkeContext *cx, const SE
 {
     SECStatus rv;
     PRUint8 ivOut[12] = { 0 };
     SECItem *ct = NULL;
     size_t maxOut;
     unsigned char tagBuf[HASH_LENGTH_MAX];
     size_t tagLen;
     unsigned int fixedBits;
-    PORT_Assert(cx->nonce->len == sizeof(ivOut));
-    memcpy(ivOut, cx->nonce->data, cx->nonce->len);
+    PORT_Assert(cx->baseNonce->len == sizeof(ivOut));
+    PORT_Memcpy(ivOut, cx->baseNonce->data, cx->baseNonce->len);
 
     /* aad may be NULL, PT may be zero-length but not NULL. */
     if (!cx || !cx->aeadContext ||
         (aad && aad->len && !aad->data) ||
         !pt || (pt->len && !pt->data) ||
         !out) {
         PORT_SetError(SEC_ERROR_INVALID_ARGS);
         return SECFailure;
     }
 
     tagLen = cx->aeadParams->tagLen;
     maxOut = pt->len + tagLen;
-    fixedBits = (cx->nonce->len - 8) * 8;
+    fixedBits = (cx->baseNonce->len - 8) * 8;
     ct = SECITEM_AllocItem(NULL, NULL, maxOut);
     CHECK_FAIL(!ct);
 
     rv = PK11_AEADOp(cx->aeadContext,
                      CKG_GENERATE_COUNTER_XOR, fixedBits,
                      ivOut, sizeof(ivOut),
                      aad ? aad->data : NULL,
                      aad ? aad->len : 0,
                      ct->data, (int *)&ct->len, maxOut,
                      tagBuf, tagLen,
                      pt->data, pt->len);
     CHECK_RV(rv);
     CHECK_FAIL_ERR((ct->len > maxOut - tagLen), SEC_ERROR_LIBRARY_FAILURE);
 
     /* Append the tag to the ciphertext. */
-    memcpy(&ct->data[ct->len], tagBuf, tagLen);
+    PORT_Memcpy(&ct->data[ct->len], tagBuf, tagLen);
     ct->len += tagLen;
     *out = ct;
 
 CLEANUP:
     if (rv != SECSuccess) {
         SECITEM_ZfreeItem(ct, PR_TRUE);
     }
     return rv;
@@ -1018,27 +1298,27 @@ CLEANUP:
  * decrypt (i.e. it uses the nonce input, as provided, as the IV).
  * The sequence number is kept independently on each endpoint and
  * the XORed IV is not transmitted, so we have to do our own IV
  * construction XOR outside of the token. */
 static SECStatus
 pk11_hpke_makeIv(HpkeContext *cx, PRUint8 *iv, size_t ivLen)
 {
     unsigned int counterLen = sizeof(cx->sequenceNumber);
-    PORT_Assert(cx->nonce->len == ivLen);
+    PORT_Assert(cx->baseNonce->len == ivLen);
     PORT_Assert(counterLen == 8);
     if (cx->sequenceNumber == PR_UINT64(0xffffffffffffffff)) {
         /* Overflow */
         PORT_SetError(SEC_ERROR_INVALID_KEY);
         return SECFailure;
     }
 
-    memcpy(iv, cx->nonce->data, cx->nonce->len);
+    PORT_Memcpy(iv, cx->baseNonce->data, cx->baseNonce->len);
     for (size_t i = 0; i < counterLen; i++) {
-        iv[cx->nonce->len - 1 - i] ^=
+        iv[cx->baseNonce->len - 1 - i] ^=
             PORT_GET_BYTE_BE(cx->sequenceNumber,
                              counterLen - 1 - i, counterLen);
     }
     return SECSuccess;
 }
 
 SECStatus
 PK11_HPKE_Open(HpkeContext *cx, const SECItem *aad,
@@ -1077,9 +1357,10 @@ PK11_HPKE_Open(HpkeContext *cx, const SE
     *out = pt;
 
 CLEANUP:
     if (rv != SECSuccess) {
         SECITEM_ZfreeItem(pt, PR_TRUE);
     }
     return rv;
 }
+
 #endif // NSS_ENABLE_DRAFT_HPKE
--- a/security/nss/lib/pk11wrap/pk11hpke.h
+++ b/security/nss/lib/pk11wrap/pk11hpke.h
@@ -4,17 +4,17 @@
 
 #ifndef _PK11_HPKE_H_
 #define _PK11_HPKE_H_ 1
 
 #include "blapit.h"
 #include "seccomon.h"
 
 #ifdef NSS_ENABLE_DRAFT_HPKE
-#define HPKE_DRAFT_VERSION 5
+#define HPKE_DRAFT_VERSION 7
 
 #define CLEANUP                    \
     PORT_Assert(rv == SECSuccess); \
     cleanup
 
 /* Error code must already be set.  */
 #define CHECK_RV(rv)          \
     if ((rv) != SECSuccess) { \
@@ -37,23 +37,25 @@
 
 #endif /* NSS_ENABLE_DRAFT_HPKE */
 
 typedef enum {
     HpkeModeBase = 0,
     HpkeModePsk = 1,
 } HpkeModeId;
 
-/* https://tools.ietf.org/html/draft-irtf-cfrg-hpke-05#section-7.1 */
+/* https://tools.ietf.org/html/draft-irtf-cfrg-hpke-07#section-7.1 */
 typedef enum {
     HpkeDhKemX25519Sha256 = 0x20,
 } HpkeKemId;
 
 typedef enum {
     HpkeKdfHkdfSha256 = 1,
+    HpkeKdfHkdfSha384 = 2,
+    HpkeKdfHkdfSha512 = 3,
 } HpkeKdfId;
 
 typedef enum {
     HpkeAeadAes128Gcm = 1,
     HpkeAeadChaCha20Poly1305 = 3,
 } HpkeAeadId;
 
 typedef struct hpkeKemParamsStr {
--- a/security/nss/lib/pk11wrap/pk11pub.h
+++ b/security/nss/lib/pk11wrap/pk11pub.h
@@ -723,17 +723,17 @@ CERTCertList *PK11_ListCertsInSlot(PK11S
 CERTSignedCrl *PK11_ImportCRL(PK11SlotInfo *slot, SECItem *derCRL, char *url,
                               int type, void *wincx, PRInt32 importOptions, PLArenaPool *arena, PRInt32 decodeOptions);
 CK_BBOOL PK11_HasAttributeSet(PK11SlotInfo *slot,
                               CK_OBJECT_HANDLE id,
                               CK_ATTRIBUTE_TYPE type,
                               PRBool haslock /* must be set to PR_FALSE */);
 
 /**********************************************************************
- *                   Hybrid Public Key Encryption  (draft-05)
+ *                   Hybrid Public Key Encryption  (draft-07)
  **********************************************************************/
 /*
  * NOTE: All HPKE functions will fail with SEC_ERROR_INVALID_ALGORITHM
  * unless NSS is compiled with NSS_ENABLE_DRAFT_HPKE while spec (and
  * implementation) is in draft. The eventual RFC number is an input to
  * the key schedule, so applications opting into this MUST be prepared for
  * outputs to change when the implementation is updated or finalized. */
 
@@ -741,19 +741,38 @@ CK_BBOOL PK11_HasAttributeSet(PK11SlotIn
  * underlying PK11 functions take them as non-const. To avoid lying to
  * the application with a cast, this idiosyncrasy is exposed. */
 SECStatus PK11_HPKE_ValidateParameters(HpkeKemId kemId, HpkeKdfId kdfId, HpkeAeadId aeadId);
 HpkeContext *PK11_HPKE_NewContext(HpkeKemId kemId, HpkeKdfId kdfId, HpkeAeadId aeadId,
                                   PK11SymKey *psk, const SECItem *pskId);
 SECStatus PK11_HPKE_Deserialize(const HpkeContext *cx, const PRUint8 *enc,
                                 unsigned int encLen, SECKEYPublicKey **outPubKey);
 void PK11_HPKE_DestroyContext(HpkeContext *cx, PRBool freeit);
-const SECItem *PK11_HPKE_GetEncapPubKey(const HpkeContext *cx);
+
+/* Serialize an initialized receiver context. This only retains the keys and
+ * associated information necessary to resume Export and Open operations after
+ * import. Serialization is currently supported for receiver contexts only.
+ * This is done for two reasons: 1) it avoids having to move the encryption
+ * sequence number outside of the token (or adding encryption context
+ * serialization support to softoken), and 2) we don't have to worry about IV
+ * reuse due to sequence number cloning.
+ *
+ * |wrapKey| is required when exporting in FIPS mode. If exported with a
+ * wrapping key, that same key must be provided to the import function,
+ * otherwise behavior is undefined.
+ *
+ * Even when exported with key wrap, HPKE expects the nonce to also be kept
+ * secret and that value is not protected by wrapKey. Applications are
+ * responsible for maintaining the confidentiality of the exported information.
+ */
+SECStatus PK11_HPKE_ExportContext(const HpkeContext *cx, PK11SymKey *wrapKey, SECItem **serialized);
 SECStatus PK11_HPKE_ExportSecret(const HpkeContext *cx, const SECItem *info, unsigned int L,
                                  PK11SymKey **outKey);
+const SECItem *PK11_HPKE_GetEncapPubKey(const HpkeContext *cx);
+HpkeContext *PK11_HPKE_ImportContext(const SECItem *serialized, PK11SymKey *wrapKey);
 SECStatus PK11_HPKE_Open(HpkeContext *cx, const SECItem *aad, const SECItem *ct, SECItem **outPt);
 SECStatus PK11_HPKE_Seal(HpkeContext *cx, const SECItem *aad, const SECItem *pt, SECItem **outCt);
 SECStatus PK11_HPKE_Serialize(const SECKEYPublicKey *pk, PRUint8 *buf, unsigned int *len, unsigned int maxLen);
 SECStatus PK11_HPKE_SetupS(HpkeContext *cx, const SECKEYPublicKey *pkE, SECKEYPrivateKey *skE,
                            SECKEYPublicKey *pkR, const SECItem *info);
 SECStatus PK11_HPKE_SetupR(HpkeContext *cx, const SECKEYPublicKey *pkR, SECKEYPrivateKey *skR,
                            const SECItem *enc, const SECItem *info);
 
--- a/security/nss/lib/softoken/softkver.h
+++ b/security/nss/lib/softoken/softkver.h
@@ -12,16 +12,16 @@
 
 /*
  * Softoken's major version, minor version, patch level, build number,
  * and whether this is a beta release.
  *
  * The format of the version string should be
  *     "<major version>.<minor version>[.<patch level>[.<build number>]][ <ECC>][ <Beta>]"
  */
-#define SOFTOKEN_VERSION "3.61" SOFTOKEN_ECC_STRING
+#define SOFTOKEN_VERSION "3.62" SOFTOKEN_ECC_STRING " Beta"
 #define SOFTOKEN_VMAJOR 3
-#define SOFTOKEN_VMINOR 61
+#define SOFTOKEN_VMINOR 62
 #define SOFTOKEN_VPATCH 0
 #define SOFTOKEN_VBUILD 0
-#define SOFTOKEN_BETA PR_FALSE
+#define SOFTOKEN_BETA PR_TRUE
 
 #endif /* _SOFTKVER_H_ */
--- a/security/nss/lib/ssl/ssl3con.c
+++ b/security/nss/lib/ssl/ssl3con.c
@@ -65,16 +65,19 @@ static SECStatus ssl3_HandlePostHelloHan
                                                       PRUint32 length);
 static SECStatus ssl3_FlushHandshakeMessages(sslSocket *ss, PRInt32 flags);
 static CK_MECHANISM_TYPE ssl3_GetHashMechanismByHashType(SSLHashType hashType);
 static CK_MECHANISM_TYPE ssl3_GetMgfMechanismByHashType(SSLHashType hash);
 PRBool ssl_IsRsaPssSignatureScheme(SSLSignatureScheme scheme);
 PRBool ssl_IsRsaeSignatureScheme(SSLSignatureScheme scheme);
 PRBool ssl_IsRsaPkcs1SignatureScheme(SSLSignatureScheme scheme);
 PRBool ssl_IsDsaSignatureScheme(SSLSignatureScheme scheme);
+static SECStatus ssl3_UpdateDefaultHandshakeHashes(sslSocket *ss,
+                                                   const unsigned char *b,
+                                                   unsigned int l);
 
 const PRUint8 ssl_hello_retry_random[] = {
     0xCF, 0x21, 0xAD, 0x74, 0xE5, 0x9A, 0x61, 0x11,
     0xBE, 0x1D, 0x8C, 0x02, 0x1E, 0x65, 0xB8, 0x91,
     0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB, 0x8C, 0x5E,
     0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C
 };
 PR_STATIC_ASSERT(PR_ARRAY_SIZE(ssl_hello_retry_random) == SSL3_RANDOM_LENGTH);
@@ -3843,30 +3846,39 @@ ssl3_InitHandshakeHashes(sslSocket *ss)
                 ssl_MapLowLevelError(SSL_ERROR_SHA_DIGEST_FAILURE);
                 return SECFailure;
             }
         }
     }
 
     if (ss->ssl3.hs.hashType != handshake_hash_record &&
         ss->ssl3.hs.messages.len > 0) {
-        if (ssl3_UpdateHandshakeHashes(ss, ss->ssl3.hs.messages.buf,
-                                       ss->ssl3.hs.messages.len) != SECSuccess) {
+        /* When doing ECH, ssl3_UpdateHandshakeHashes will store outer messages into
+         * the both the outer and inner transcripts. ssl3_UpdateDefaultHandshakeHashes
+         * uses only the default context (which is the outer when doing ECH). */
+        if (ssl3_UpdateDefaultHandshakeHashes(ss, ss->ssl3.hs.messages.buf,
+                                              ss->ssl3.hs.messages.len) != SECSuccess) {
             return SECFailure;
         }
-        sslBuffer_Clear(&ss->ssl3.hs.messages);
+        /* When doing ECH, deriving accept_confirmation requires all messages
+         * up to SH, then a synthetic SH. Don't free the buffers just yet. */
+        if (!ss->ssl3.hs.echHpkeCtx) {
+            sslBuffer_Clear(&ss->ssl3.hs.messages);
+        }
     }
     if (ss->ssl3.hs.shaEchInner &&
         ss->ssl3.hs.echInnerMessages.len > 0) {
         if (PK11_DigestOp(ss->ssl3.hs.shaEchInner, ss->ssl3.hs.echInnerMessages.buf,
                           ss->ssl3.hs.echInnerMessages.len) != SECSuccess) {
             ssl_MapLowLevelError(SSL_ERROR_DIGEST_FAILURE);
             return SECFailure;
         }
-        sslBuffer_Clear(&ss->ssl3.hs.echInnerMessages);
+        if (!ss->ssl3.hs.echHpkeCtx) {
+            sslBuffer_Clear(&ss->ssl3.hs.echInnerMessages);
+        }
     }
 
     return SECSuccess;
 }
 
 void
 ssl3_RestartHandshakeHashes(sslSocket *ss)
 {
@@ -3888,110 +3900,126 @@ ssl3_RestartHandshakeHashes(sslSocket *s
         ss->ssl3.hs.shaEchInner = NULL;
     }
     if (ss->ssl3.hs.shaPostHandshake) {
         PK11_DestroyContext(ss->ssl3.hs.shaPostHandshake, PR_TRUE);
         ss->ssl3.hs.shaPostHandshake = NULL;
     }
 }
 
-/* For TLS 1.3 EncryptedClientHello, add the provided buffer to the
- * given hash context. This is only needed for the initial CH,
- * after which ssl3_UpdateHandshakeHashes will update both contexts
- * until ssl3_CoalesceEchHandshakeHashes. */
-SECStatus
-ssl3_UpdateExplicitHandshakeTranscript(sslSocket *ss, const unsigned char *b,
-                                       unsigned int l, sslBuffer *target)
-{
+/* Add the provided bytes to the handshake hash context. When doing
+ * TLS 1.3 ECH, |target| may be provided to specify only the inner/outer
+ * transcript, else the input is added to both contexts. This happens
+ * only on the client. On the server, only the default context is used. */
+SECStatus
+ssl3_UpdateHandshakeHashesInt(sslSocket *ss, const unsigned char *b,
+                              unsigned int l, sslBuffer *target)
+{
+
+    SECStatus rv = SECSuccess;
+    PRBool explicit = (target != NULL);
+    PRBool appendToEchInner = !ss->sec.isServer &&
+                              ss->ssl3.hs.echHpkeCtx &&
+                              !explicit;
     PORT_Assert(ss->opt.noLocks || ssl_HaveSSL3HandshakeLock(ss));
-    PORT_Assert(ss->vrange.max >= SSL_LIBRARY_VERSION_TLS_1_3);
-    if (ss->sec.isServer) {
-        /* Only the client maintains two states at the outset. */
-        PORT_Assert(target != &ss->ssl3.hs.echInnerMessages);
-    }
-    return sslBuffer_Append(target, b, l);
-}
-static SECStatus
-ssl3_UpdateOuterHandshakeHashes(sslSocket *ss, const unsigned char *b,
-                                unsigned int l)
-{
-    return ssl3_UpdateExplicitHandshakeTranscript(ss, b, l,
-                                                  &ss->ssl3.hs.messages);
-}
-static SECStatus
-ssl3_UpdateInnerHandshakeHashes(sslSocket *ss, const unsigned char *b,
-                                unsigned int l)
-{
-    return ssl3_UpdateExplicitHandshakeTranscript(ss, b, l,
-                                                  &ss->ssl3.hs.echInnerMessages);
-}
-/*
- * Handshake messages
- */
-/* Called from  ssl3_InitHandshakeHashes()
-**      ssl3_AppendHandshake()
-**      ssl3_HandleV2ClientHello()
-**      ssl3_HandleHandshakeMessage()
-** Caller must hold the ssl3Handshake lock.
-*/
-SECStatus
-ssl3_UpdateHandshakeHashes(sslSocket *ss, const unsigned char *b, unsigned int l)
-{
-    SECStatus rv = SECSuccess;
-
-    PORT_Assert(ss->opt.noLocks || ssl_HaveSSL3HandshakeLock(ss));
-
+    PORT_Assert(target != &ss->ssl3.hs.echInnerMessages ||
+                !ss->sec.isServer);
+
+    if (target == NULL) {
+        /* Default context. */
+        target = &ss->ssl3.hs.messages;
+    }
     /* With TLS 1.3, and versions TLS.1.1 and older, we keep the hash(es)
      * always up to date. However, we must initially buffer the handshake
      * messages, until we know what to do.
      * If ss->ssl3.hs.hashType != handshake_hash_unknown,
      * it means we know what to do. We calculate (hash our input),
      * and we stop appending to the buffer.
      *
      * With TLS 1.2, we always append all handshake messages,
      * and never update the hash, because the hash function we must use for
      * certificate_verify might be different from the hash function we use
      * when signing other handshake hashes. */
-
     if (ss->ssl3.hs.hashType == handshake_hash_unknown ||
         ss->ssl3.hs.hashType == handshake_hash_record) {
-        rv = sslBuffer_Append(&ss->ssl3.hs.messages, b, l);
+        rv = sslBuffer_Append(target, b, l);
         if (rv != SECSuccess) {
             return SECFailure;
         }
-        if (!ss->sec.isServer && ss->ssl3.hs.echHpkeCtx) {
-            return ssl3_UpdateInnerHandshakeHashes(ss, b, l);
+        if (appendToEchInner) {
+            return sslBuffer_Append(&ss->ssl3.hs.echInnerMessages, b, l);
         }
         return SECSuccess;
     }
 
     PRINT_BUF(90, (ss, "handshake hash input:", b, l));
 
     if (ss->ssl3.hs.hashType == handshake_hash_single) {
         PORT_Assert(ss->version >= SSL_LIBRARY_VERSION_TLS_1_3);
-        rv = PK11_DigestOp(ss->ssl3.hs.sha, b, l);
-        if (rv != SECSuccess) {
-            ssl_MapLowLevelError(SSL_ERROR_DIGEST_FAILURE);
-            return rv;
+        if (target == &ss->ssl3.hs.messages) {
+            rv = PK11_DigestOp(ss->ssl3.hs.sha, b, l);
+            if (rv != SECSuccess) {
+                ssl_MapLowLevelError(SSL_ERROR_DIGEST_FAILURE);
+                return rv;
+            }
+        }
+        if (ss->ssl3.hs.shaEchInner &&
+            (target == &ss->ssl3.hs.echInnerMessages || !explicit)) {
+            rv = PK11_DigestOp(ss->ssl3.hs.shaEchInner, b, l);
+            if (rv != SECSuccess) {
+                ssl_MapLowLevelError(SSL_ERROR_DIGEST_FAILURE);
+                return rv;
+            }
         }
     } else if (ss->ssl3.hs.hashType == handshake_hash_combo) {
         rv = PK11_DigestOp(ss->ssl3.hs.md5, b, l);
         if (rv != SECSuccess) {
             ssl_MapLowLevelError(SSL_ERROR_MD5_DIGEST_FAILURE);
             return rv;
         }
         rv = PK11_DigestOp(ss->ssl3.hs.sha, b, l);
         if (rv != SECSuccess) {
             ssl_MapLowLevelError(SSL_ERROR_SHA_DIGEST_FAILURE);
             return rv;
         }
     }
     return rv;
 }
 
+static SECStatus
+ssl3_UpdateDefaultHandshakeHashes(sslSocket *ss, const unsigned char *b,
+                                  unsigned int l)
+{
+    return ssl3_UpdateHandshakeHashesInt(ss, b, l,
+                                         &ss->ssl3.hs.messages);
+}
+
+static SECStatus
+ssl3_UpdateInnerHandshakeHashes(sslSocket *ss, const unsigned char *b,
+                                unsigned int l)
+{
+    return ssl3_UpdateHandshakeHashesInt(ss, b, l,
+                                         &ss->ssl3.hs.echInnerMessages);
+}
+
+/*
+ * Handshake messages
+ */
+/* Called from  ssl3_InitHandshakeHashes()
+**      ssl3_AppendHandshake()
+**      ssl3_HandleV2ClientHello()
+**      ssl3_HandleHandshakeMessage()
+** Caller must hold the ssl3Handshake lock.
+*/
+SECStatus
+ssl3_UpdateHandshakeHashes(sslSocket *ss, const unsigned char *b, unsigned int l)
+{
+    return ssl3_UpdateHandshakeHashesInt(ss, b, l, NULL);
+}
+
 SECStatus
 ssl3_UpdatePostHandshakeHashes(sslSocket *ss, const unsigned char *b, unsigned int l)
 {
     SECStatus rv = SECSuccess;
 
     PORT_Assert(ss->opt.noLocks || ssl_HaveSSL3HandshakeLock(ss));
 
     PRINT_BUF(90, (ss, "post handshake hash input:", b, l));
@@ -5508,17 +5536,17 @@ ssl3_SendClientHello(sslSocket *ss, sslC
             }
         }
         rv = ssl3_AppendHandshake(ss, chBuf.buf, chBuf.len);
     } else {
         rv = tls13_ConstructClientHelloWithEch(ss, sid, !requestingResume, &chBuf, &extensionBuf);
         if (rv != SECSuccess) {
             goto loser; /* code set */
         }
-        rv = ssl3_UpdateOuterHandshakeHashes(ss, chBuf.buf, chBuf.len);
+        rv = ssl3_UpdateDefaultHandshakeHashes(ss, chBuf.buf, chBuf.len);
         if (rv != SECSuccess) {
             goto loser; /* code set */
         }
 
         if (IS_DTLS(ss)) {
             rv = dtls_StageHandshakeMessage(ss);
             if (rv != SECSuccess) {
                 goto loser;
@@ -7059,45 +7087,41 @@ ssl3_HandleServerHello(sslSocket *ss, PR
     if (isHelloRetry) {
         rv = tls13_HandleHelloRetryRequest(ss, savedMsg, savedLength);
         if (rv != SECSuccess) {
             goto loser;
         }
         return SECSuccess;
     }
 
-    rv = tls13_MaybeHandleEchSignal(ss);
-    if (rv != SECSuccess) {
-        goto alert_loser;
-    }
-
     rv = ssl3_HandleParsedExtensions(ss, ssl_hs_server_hello);
     ssl3_DestroyRemoteExtensions(&ss->ssl3.hs.remoteExtensions);
     if (rv != SECSuccess) {
         goto alert_loser;
     }
 
     rv = ssl_HashHandshakeMessage(ss, ssl_hs_server_hello,
                                   savedMsg, savedLength);
     if (rv != SECSuccess) {
         goto loser;
     }
 
     if (ss->version >= SSL_LIBRARY_VERSION_TLS_1_3) {
-        rv = tls13_HandleServerHelloPart2(ss);
+        rv = tls13_HandleServerHelloPart2(ss, savedMsg, savedLength);
         if (rv != SECSuccess) {
             errCode = PORT_GetError();
             goto loser;
         }
     } else {
         rv = ssl3_HandleServerHelloPart2(ss, &sidBytes, &errCode);
         if (rv != SECSuccess)
             goto loser;
     }
 
+    ss->ssl3.hs.preliminaryInfo |= ssl_preinfo_ech;
     return SECSuccess;
 
 alert_loser:
     (void)SSL3_SendAlert(ss, alert_fatal, desc);
 
 loser:
     /* Clean up the temporary pointer to the handshake buffer. */
     ss->xtnData.signedCertTimestamps.len = 0;
@@ -8623,23 +8647,16 @@ ssl_GenerateServerRandom(sslSocket *ss)
     SECStatus rv;
     PRUint8 *downgradeSentinel;
 
     rv = ssl3_GetNewRandom(ss->ssl3.hs.server_random);
     if (rv != SECSuccess) {
         return SECFailure;
     }
 
-    if (ss->ssl3.hs.echAccepted) {
-        rv = tls13_WriteServerEchSignal(ss);
-        if (rv != SECSuccess) {
-            return SECFailure;
-        }
-    }
-
     if (ss->version == ss->vrange.max) {
         return SECSuccess;
     }
 #ifdef DTLS_1_3_DRAFT_VERSION
     if (IS_DTLS(ss)) {
         return SECSuccess;
     }
 #endif
@@ -9770,16 +9787,25 @@ ssl_ConstructServerHello(sslSocket *ss, 
     }
     if (SSL_BUFFER_LEN(extensionBuf)) {
         rv = sslBuffer_AppendBufferVariable(messageBuf, extensionBuf, 2);
         if (rv != SECSuccess) {
             return SECFailure;
         }
     }
 
+    if (!helloRetry && ssl3_ExtensionNegotiated(ss, ssl_tls13_ech_is_inner_xtn)) {
+        /* Signal ECH acceptance if we handled handled both CHOuter/CHInner (i.e.
+         * in shared mode), or if we received a CHInner in split/backend mode. */
+        if (ss->ssl3.hs.echAccepted || ss->opt.enableTls13BackendEch) {
+            return tls13_WriteServerEchSignal(ss, SSL_BUFFER_BASE(messageBuf),
+                                              SSL_BUFFER_LEN(messageBuf));
+        }
+    }
+
     return SECSuccess;
 }
 
 /* The negotiated version number has been already placed in ss->version.
 **
 ** Called from:  ssl3_HandleClientHello                     (resuming session),
 **  ssl3_SendServerHelloSequence <- ssl3_HandleClientHello   (new session),
 **  ssl3_SendServerHelloSequence <- ssl3_HandleV2ClientHello (new session)
@@ -12279,17 +12305,17 @@ ssl_HashHandshakeMessage(sslSocket *ss, 
                                        b, length, ssl3_UpdateHandshakeHashes);
 }
 
 SECStatus
 ssl_HashHandshakeMessageDefault(sslSocket *ss, SSLHandshakeType ct,
                                 const PRUint8 *b, PRUint32 length)
 {
     return ssl_HashHandshakeMessageInt(ss, ct, ss->ssl3.hs.recvMessageSeq,
-                                       b, length, ssl3_UpdateOuterHandshakeHashes);
+                                       b, length, ssl3_UpdateDefaultHandshakeHashes);
 }
 SECStatus
 ssl_HashHandshakeMessageEchInner(sslSocket *ss, SSLHandshakeType ct,
                                  const PRUint8 *b, PRUint32 length)
 {
     return ssl_HashHandshakeMessageInt(ss, ct, ss->ssl3.hs.recvMessageSeq,
                                        b, length, ssl3_UpdateInnerHandshakeHashes);
 }
@@ -13842,16 +13868,17 @@ ssl3_DestroySSL3Info(sslSocket *ss)
     tls13_DestroyEarlyData(&ss->ssl3.hs.bufferedEarlyData);
 
     /* Destroy TLS 1.3 PSKs. */
     tls13_DestroyPskList(&ss->ssl3.hs.psks);
 
     /* TLS 1.3 ECH state. */
     PK11_HPKE_DestroyContext(ss->ssl3.hs.echHpkeCtx, PR_TRUE);
     PORT_Free((void *)ss->ssl3.hs.echPublicName); /* CONST */
+    sslBuffer_Clear(&ss->ssl3.hs.greaseEchBuf);
 }
 
 /*
  * parse the policy value for a single algorithm in a cipher_suite,
  *   return TRUE if we disallow by the cipher suite by policy
  *   (we don't have to parse any more algorithm policies on this cipher suite),
  *  otherwise return FALSE.
  *   1. If we don't have the required policy, disable by default, disallow by
--- a/security/nss/lib/ssl/ssl3ext.c
+++ b/security/nss/lib/ssl/ssl3ext.c
@@ -10,16 +10,17 @@
 
 #include "nssrenam.h"
 #include "nss.h"
 #include "pk11pub.h"
 #include "ssl.h"
 #include "sslimpl.h"
 #include "sslproto.h"
 #include "ssl3exthandle.h"
+#include "tls13ech.h"
 #include "tls13err.h"
 #include "tls13exthandle.h"
 #include "tls13subcerts.h"
 
 /* Callback function that handles a received extension. */
 typedef SECStatus (*ssl3ExtensionHandlerFunc)(const sslSocket *ss,
                                               TLSExtensionData *xtnData,
                                               SECItem *data);
@@ -49,16 +50,17 @@ static const ssl3ExtensionHandler client
     { ssl_signed_cert_timestamp_xtn, &ssl3_ServerHandleSignedCertTimestampXtn },
     { ssl_delegated_credentials_xtn, &tls13_ServerHandleDelegatedCredentialsXtn },
     { ssl_tls13_key_share_xtn, &tls13_ServerHandleKeyShareXtn },
     { ssl_tls13_pre_shared_key_xtn, &tls13_ServerHandlePreSharedKeyXtn },
     { ssl_tls13_early_data_xtn, &tls13_ServerHandleEarlyDataXtn },
     { ssl_tls13_psk_key_exchange_modes_xtn, &tls13_ServerHandlePskModesXtn },
     { ssl_tls13_cookie_xtn, &tls13_ServerHandleCookieXtn },
     { ssl_tls13_post_handshake_auth_xtn, &tls13_ServerHandlePostHandshakeAuthXtn },
+    { ssl_tls13_ech_is_inner_xtn, &tls13_ServerHandleEchIsInnerXtn },
     { ssl_record_size_limit_xtn, &ssl_HandleRecordSizeLimitXtn },
     { 0, NULL }
 };
 
 /* These two tables are used by the client, to handle server hello
  * extensions. */
 static const ssl3ExtensionHandler serverHelloHandlersTLS[] = {
     { ssl_server_name_xtn, &ssl3_HandleServerNameXtn },
@@ -1015,22 +1017,18 @@ ssl3_DestroyExtensionData(TLSExtensionDa
     SECITEM_FreeItem(&xtnData->applicationToken, PR_FALSE);
     if (xtnData->certReqAuthorities.arena) {
         PORT_FreeArena(xtnData->certReqAuthorities.arena, PR_FALSE);
         xtnData->certReqAuthorities.arena = NULL;
     }
     PORT_Free(xtnData->advertised);
     tls13_DestroyDelegatedCredential(xtnData->peerDelegCred);
 
-    /* ECH State */
-    SECITEM_FreeItem(&xtnData->innerCh, PR_FALSE);
-    SECITEM_FreeItem(&xtnData->echSenderPubKey, PR_FALSE);
-    SECITEM_FreeItem(&xtnData->echConfigId, PR_FALSE);
-    SECITEM_FreeItem(&xtnData->echRetryConfigs, PR_FALSE);
-    xtnData->echRetryConfigsValid = PR_FALSE;
+    tls13_DestroyEchXtnState(xtnData->ech);
+    xtnData->ech = NULL;
 }
 
 /* Free everything that has been allocated and then reset back to
  * the starting state. */
 void
 ssl3_ResetExtensionData(TLSExtensionData *xtnData, const sslSocket *ss)
 {
     ssl3_DestroyExtensionData(xtnData);
--- a/security/nss/lib/ssl/ssl3ext.h
+++ b/security/nss/lib/ssl/ssl3ext.h
@@ -126,23 +126,19 @@ struct TLSExtensionDataStr {
      * |tls13_MaybeSetDelegatedCredential|.
      */
     PRBool sendingDelegCredToPeer;
 
     /* A non-owning reference to the selected PSKs. MUST NOT be freed directly,
      * rather through tls13_DestoryPskList(). */
     sslPsk *selectedPsk;
 
-    /* ECH working state. */
-    SECItem innerCh;             /* Server: "payload value of ClientECH. */
-    SECItem echSenderPubKey;     /* Server: "enc value of ClientECH, required for CHInner decryption. */
-    SECItem echConfigId;         /* Server: "config_id" value of ClientECH.  */
-    PRUint32 echCipherSuite;     /* Server: "cipher_suite" value of ClientECH. */
-    SECItem echRetryConfigs;     /* Client: Retry_configs from ServerEncryptedCH. */
-    PRBool echRetryConfigsValid; /* Client: Permits retry_configs to be extracted. */
+    /* ECH working state. Non-null when a valid Encrypted Client Hello extension
+     * was received. */
+    sslEchXtnState *ech;
 };
 
 typedef struct TLSExtensionStr {
     PRCList link;  /* The linked list link */
     PRUint16 type; /* Extension type */
     SECItem data;  /* Pointers into the handshake data. */
 } TLSExtension;
 
--- a/security/nss/lib/ssl/sslexp.h
+++ b/security/nss/lib/ssl/sslexp.h
@@ -504,16 +504,24 @@ typedef SECStatus(PR_CALLBACK *SSLResump
 
 /* If |enabled|, a GREASE ECH extension will be sent in every ClientHello,
  * unless a valid and supported ECHConfig is configured to the socket
  * (in which case real ECH takes precedence). If |!enabled|, it is not sent.*/
 #define SSL_EnableTls13GreaseEch(fd, enabled)        \
     SSL_EXPERIMENTAL_API("SSL_EnableTls13GreaseEch", \
                          (PRFileDesc * _fd, PRBool _enabled), (fd, enabled))
 
+/* If |enabled|, a server receiving a Client Hello containing the ech_is_inner
+ * (and not encrypted_client_hello) extension will respond with the ECH
+ * acceptance signal. This signals the client to continue with the inner
+ * transcript rather than outer. */
+#define SSL_EnableTls13BackendEch(fd, enabled)        \
+    SSL_EXPERIMENTAL_API("SSL_EnableTls13BackendEch", \
+                         (PRFileDesc * _fd, PRBool _enabled), (fd, enabled))
+
 /* Called by the client after an initial ECH connection fails with
  * SSL_ERROR_ECH_RETRY_WITH_ECH. Returns compatible ECHConfigs, which
  * are configured via SetClientEchConfigs for an ECH retry attempt.
  * These configs MUST NOT be used for more than the single retry
  * attempt. Subsequent connections MUST use advertised ECHConfigs. */
 #define SSL_GetEchRetryConfigs(fd, out)            \
     SSL_EXPERIMENTAL_API("SSL_GetEchRetryConfigs", \
                          (PRFileDesc * _fd,        \
--- a/security/nss/lib/ssl/sslimpl.h
+++ b/security/nss/lib/ssl/sslimpl.h
@@ -31,19 +31,19 @@
 #include "prthread.h"
 #include "prclist.h"
 #include "private/pprthred.h"
 
 #include "sslt.h" /* for some formerly private types, now public */
 
 typedef struct sslSocketStr sslSocket;
 typedef struct sslNamedGroupDefStr sslNamedGroupDef;
-typedef struct sslEsniKeysStr sslEsniKeys;
 typedef struct sslEchConfigStr sslEchConfig;
 typedef struct sslEchConfigContentsStr sslEchConfigContents;
+typedef struct sslEchXtnStateStr sslEchXtnState;
 typedef struct sslPskStr sslPsk;
 typedef struct sslDelegatedCredentialStr sslDelegatedCredential;
 typedef struct sslEphemeralKeyPairStr sslEphemeralKeyPair;
 typedef struct TLS13KeyShareEntryStr TLS13KeyShareEntry;
 
 #include "sslencode.h"
 #include "sslexp.h"
 #include "ssl3ext.h"
@@ -282,16 +282,17 @@ typedef struct sslOptionsStr {
     unsigned int enableDtlsShortHeader : 1;
     unsigned int enableHelloDowngradeCheck : 1;
     unsigned int enableV2CompatibleHello : 1;
     unsigned int enablePostHandshakeAuth : 1;
     unsigned int enableDelegatedCredentials : 1;
     unsigned int enableDtls13VersionCompat : 1;
     unsigned int suppressEndOfEarlyData : 1;
     unsigned int enableTls13GreaseEch : 1;
+    unsigned int enableTls13BackendEch : 1;
 } sslOptions;
 
 typedef enum { sslHandshakingUndetermined = 0,
                sslHandshakingAsClient,
                sslHandshakingAsServer
 } sslHandshakingType;
 
 #define SSL_LOCK_RANK_SPEC 255
@@ -743,16 +744,17 @@ typedef struct SSL3HandshakeStateStr {
     PRCList dtlsRcvdHandshake; /* Handshake records we have received
                                 * used to generate ACKs. */
 
     /* TLS 1.3 ECH state. */
     PRBool echAccepted;        /* Client/Server: True if we've commited to using CHInner. */
     HpkeContext *echHpkeCtx;   /* Client/Server: HPKE context for ECH. */
     const char *echPublicName; /* Client: If rejected, the ECHConfig.publicName to
                                 * use for certificate verification. */
+    sslBuffer greaseEchBuf;    /* Client: Remember GREASE ECH, as advertised, for CH2 (HRR case). */
 
 } SSL3HandshakeState;
 
 #define SSL_ASSERT_HASHES_EMPTY(ss)                                  \
     do {                                                             \
         PORT_Assert(ss->ssl3.hs.hashType == handshake_hash_unknown); \
         PORT_Assert(ss->ssl3.hs.messages.len == 0);                  \
         PORT_Assert(ss->ssl3.hs.echInnerMessages.len == 0);          \
@@ -1117,18 +1119,19 @@ struct sslSocketStr {
     /* True when the current session is a stateless resume. */
     PRBool statelessResume;
     TLSExtensionData xtnData;
 
     /* Whether we are doing stream or datagram mode */
     SSLProtocolVariant protocolVariant;
 
     /* TLS 1.3 Encrypted Client Hello. */
-    PRCList echConfigs;           /* Client/server: Must not change while hs is in-progress. */
-    SECKEYPublicKey *echPubKey;   /* Server: The ECH keypair used in HPKE setup */
+    PRCList echConfigs;           /* Client/server: Must not change while hs
+                                   * is in-progress. */
+    SECKEYPublicKey *echPubKey;   /* Server: The ECH keypair used in HPKE. */
     SECKEYPrivateKey *echPrivKey; /* As above. */
 
     /* Anti-replay for TLS 1.3 0-RTT. */
     SSLAntiReplayContext *antiReplay;
 
     /* An out-of-band PSK. */
     sslPsk *psk;
 };
@@ -1943,16 +1946,18 @@ SECStatus SSLExp_CreateVariantMaskingCon
 SECStatus SSLExp_CreateMask(SSLMaskingContext *ctx, const PRUint8 *sample,
                             unsigned int sampleLen, PRUint8 *mask,
                             unsigned int len);
 
 SECStatus SSLExp_DestroyMaskingContext(SSLMaskingContext *ctx);
 
 SECStatus SSLExp_EnableTls13GreaseEch(PRFileDesc *fd, PRBool enabled);
 
+SECStatus SSLExp_EnableTls13BackendEch(PRFileDesc *fd, PRBool enabled);
+
 SEC_END_PROTOS
 
 #if defined(XP_UNIX) || defined(XP_OS2) || defined(XP_BEOS)
 #define SSL_GETPID getpid
 #elif defined(WIN32)
 extern int __cdecl _getpid(void);
 #define SSL_GETPID _getpid
 #else
--- a/security/nss/lib/ssl/sslsecur.c
+++ b/security/nss/lib/ssl/sslsecur.c
@@ -178,16 +178,17 @@ SSL_ResetHandshake(PRFileDesc *s, PRBool
     tls13_ResetHandshakePsks(ss, &ss->ssl3.hs.psks);
 
     if (ss->ssl3.hs.echHpkeCtx) {
         PK11_HPKE_DestroyContext(ss->ssl3.hs.echHpkeCtx, PR_TRUE);
         ss->ssl3.hs.echHpkeCtx = NULL;
         PORT_Assert(ss->ssl3.hs.echPublicName);
         PORT_Free((void *)ss->ssl3.hs.echPublicName); /* CONST */
         ss->ssl3.hs.echPublicName = NULL;
+        sslBuffer_Clear(&ss->ssl3.hs.greaseEchBuf);
     }
 
     if (!ss->TCPconnected)
         ss->TCPconnected = (PR_SUCCESS == ssl_DefGetpeername(ss, &addr));
 
 loser:
     SSL_UNLOCK_WRITER(ss);
     SSL_UNLOCK_READER(ss);
--- a/security/nss/lib/ssl/sslsock.c
+++ b/security/nss/lib/ssl/sslsock.c
@@ -88,17 +88,18 @@ static sslOptions ssl_defaults = {
     .enable0RttData = PR_FALSE,
     .enableTls13CompatMode = PR_FALSE,
     .enableDtls13VersionCompat = PR_FALSE,
     .enableDtlsShortHeader = PR_FALSE,
     .enableHelloDowngradeCheck = PR_FALSE,
     .enableV2CompatibleHello = PR_FALSE,
     .enablePostHandshakeAuth = PR_FALSE,
     .suppressEndOfEarlyData = PR_FALSE,
-    .enableTls13GreaseEch = PR_FALSE
+    .enableTls13GreaseEch = PR_FALSE,
+    .enableTls13BackendEch = PR_FALSE
 };
 
 /*
  * default range of enabled SSL/TLS protocols
  */
 static SSLVersionRange versions_defaults_stream = {
     SSL_LIBRARY_VERSION_TLS_1_0,
     SSL_LIBRARY_VERSION_TLS_1_3
@@ -4288,16 +4289,17 @@ struct {
     EXP(CreateAntiReplayContext),
     EXP(CreateMask),
     EXP(CreateMaskingContext),
     EXP(CreateVariantMaskingContext),
     EXP(DelegateCredential),
     EXP(DestroyAead),
     EXP(DestroyMaskingContext),
     EXP(DestroyResumptionTokenInfo),
+    EXP(EnableTls13BackendEch),
     EXP(EnableTls13GreaseEch),
     EXP(EncodeEchConfig),
     EXP(GetCurrentEpoch),
     EXP(GetEchRetryConfigs),
     EXP(GetExtensionSupport),
     EXP(GetResumptionTokenInfo),
     EXP(HelloRetryRequestCallback),
     EXP(InstallExtensionHooks),
@@ -4367,16 +4369,27 @@ SSLExp_EnableTls13GreaseEch(PRFileDesc *
     if (!ss) {
         return SECFailure;
     }
     ss->opt.enableTls13GreaseEch = enabled;
     return SECSuccess;
 }
 
 SECStatus
+SSLExp_EnableTls13BackendEch(PRFileDesc *fd, PRBool enabled)
+{
+    sslSocket *ss = ssl_FindSocket(fd);
+    if (!ss) {
+        return SECFailure;
+    }
+    ss->opt.enableTls13BackendEch = enabled;
+    return SECSuccess;
+}
+
+SECStatus
 SSLExp_SetDtls13VersionWorkaround(PRFileDesc *fd, PRBool enabled)
 {
     sslSocket *ss = ssl_FindSocket(fd);
     if (!ss) {
         return SECFailure;
     }
     ss->opt.enableDtls13VersionCompat = enabled;
     return SECSuccess;
--- a/security/nss/lib/ssl/sslt.h
+++ b/security/nss/lib/ssl/sslt.h
@@ -540,18 +540,19 @@ typedef enum {
     ssl_tls13_ticket_early_data_info_xtn = 46, /* Deprecated. */
     ssl_tls13_certificate_authorities_xtn = 47,
     ssl_tls13_post_handshake_auth_xtn = 49,
     ssl_signature_algorithms_cert_xtn = 50,
     ssl_tls13_key_share_xtn = 51,
     ssl_next_proto_nego_xtn = 13172, /* Deprecated. */
     ssl_renegotiation_info_xtn = 0xff01,
     ssl_tls13_short_header_xtn = 0xff03, /* Deprecated. */
+    ssl_tls13_ech_is_inner_xtn = 0xda09,
     ssl_tls13_outer_extensions_xtn = 0xfd00,
-    ssl_tls13_encrypted_client_hello_xtn = 0xfe08,
+    ssl_tls13_encrypted_client_hello_xtn = 0xfe09,
     ssl_tls13_encrypted_sni_xtn = 0xffce, /* Deprecated. */
 } SSLExtensionType;
 
 /* This is the old name for the supported_groups extensions. */
 #define ssl_elliptic_curves_xtn ssl_supported_groups_xtn
 
 /* SSL_MAX_EXTENSIONS includes the maximum number of extensions that are
  * supported for any single message type.  That is, a ClientHello; ServerHello
--- a/security/nss/lib/ssl/tls13con.c
+++ b/security/nss/lib/ssl/tls13con.c
@@ -56,17 +56,17 @@ static SECStatus tls13_HandleCertificate
 static SECStatus tls13_RecoverWrappedSharedSecret(sslSocket *ss,
                                                   sslSessionID *sid);
 static SECStatus
 tls13_DeriveSecretWrap(sslSocket *ss, PK11SymKey *key,
                        const char *prefix,
                        const char *suffix,
                        const char *keylogLabel,
                        PK11SymKey **dest);
-static SECStatus
+SECStatus
 tls13_DeriveSecret(sslSocket *ss, PK11SymKey *key,
                    const char *label,
                    unsigned int labelLen,
                    const SSL3Hashes *hashes,
                    PK11SymKey **dest,
                    SSLHashType hash);
 static SECStatus tls13_SendEndOfEarlyData(sslSocket *ss);
 static SECStatus tls13_HandleEndOfEarlyData(sslSocket *ss, const PRUint8 *b,
@@ -1179,59 +1179,72 @@ tls13_DeriveEarlySecrets(sslSocket *ss)
     if (rv != SECSuccess) {
         return SECFailure;
     }
 
     return SECSuccess;
 }
 
 static SECStatus
-tls13_ComputeHandshakeSecrets(sslSocket *ss)
+tls13_ComputeHandshakeSecret(sslSocket *ss)
 {
     SECStatus rv;
     PK11SymKey *derivedSecret = NULL;
     PK11SymKey *newSecret = NULL;
-
-    SSL_TRC(5, ("%d: TLS13[%d]: compute handshake secrets (%s)",
+    SSL_TRC(5, ("%d: TLS13[%d]: compute handshake secret (%s)",
                 SSL_GETPID(), ss->fd, SSL_ROLE(ss)));
 
     /* If no PSK, generate the default early secret. */
     if (!ss->ssl3.hs.currentSecret) {
         PORT_Assert(!ss->xtnData.selectedPsk);
         rv = tls13_HkdfExtract(NULL, NULL,
                                tls13_GetHash(ss), &ss->ssl3.hs.currentSecret);
         if (rv != SECSuccess) {
             return SECFailure;
         }
     }
     PORT_Assert(ss->ssl3.hs.currentSecret);
     PORT_Assert(ss->ssl3.hs.dheSecret);
 
-    /* Expand before we extract. */
+    /* Derive-Secret(., "derived", "") */
     rv = tls13_DeriveSecretNullHash(ss, ss->ssl3.hs.currentSecret,
                                     kHkdfLabelDerivedSecret,
                                     strlen(kHkdfLabelDerivedSecret),
                                     &derivedSecret, tls13_GetHash(ss));
     if (rv != SECSuccess) {
         LOG_ERROR(ss, SEC_ERROR_LIBRARY_FAILURE);
         return rv;
     }
 
+    /* HKDF-Extract(ECDHE, .) = Handshake Secret */
     rv = tls13_HkdfExtract(derivedSecret, ss->ssl3.hs.dheSecret,
                            tls13_GetHash(ss), &newSecret);
     PK11_FreeSymKey(derivedSecret);
-
     if (rv != SECSuccess) {
         LOG_ERROR(ss, SEC_ERROR_LIBRARY_FAILURE);
         return rv;
     }
+
+    PK11_FreeSymKey(ss->ssl3.hs.currentSecret);
+    ss->ssl3.hs.currentSecret = newSecret;
+    return SECSuccess;
+}
+
+static SECStatus
+tls13_ComputeHandshakeSecrets(sslSocket *ss)
+{
+    SECStatus rv;
+    PK11SymKey *derivedSecret = NULL;
+    PK11SymKey *newSecret = NULL;
+
     PK11_FreeSymKey(ss->ssl3.hs.dheSecret);
     ss->ssl3.hs.dheSecret = NULL;
-    PK11_FreeSymKey(ss->ssl3.hs.currentSecret);
-    ss->ssl3.hs.currentSecret = newSecret;
+
+    SSL_TRC(5, ("%d: TLS13[%d]: compute handshake secrets (%s)",
+                SSL_GETPID(), ss->fd, SSL_ROLE(ss)));
 
     /* Now compute |*HsTrafficSecret| */
     rv = tls13_DeriveSecretWrap(ss, ss->ssl3.hs.currentSecret,
                                 kHkdfLabelClient,
                                 kHkdfLabelHandshakeTrafficSecret,
                                 keylogLabelClientHsTrafficSecret,
                                 &ss->ssl3.hs.clientHsTrafficSecret);
     if (rv != SECSuccess) {
@@ -1860,32 +1873,26 @@ tls13_HandleClientHelloPart2(sslSocket *
             !ss->xtnData.cookie.len) {
             FATAL_ERROR(ss, SSL_ERROR_MISSING_COOKIE_EXTENSION,
                         missing_extension);
             goto loser;
         }
         PRINT_BUF(50, (ss, "Client sent cookie",
                        ss->xtnData.cookie.data, ss->xtnData.cookie.len));
 
-        rv = tls13_RecoverHashState(ss, ss->xtnData.cookie.data,
-                                    ss->xtnData.cookie.len,
-                                    &previousCipherSuite,
-                                    &previousGroup,
-                                    &previousEchOffered);
+        rv = tls13_HandleHrrCookie(ss, ss->xtnData.cookie.data,
+                                   ss->xtnData.cookie.len,
+                                   &previousCipherSuite,
+                                   &previousGroup,
+                                   &previousEchOffered,
+                                   NULL, NULL, NULL, NULL, PR_TRUE);
         if (rv != SECSuccess) {
             FATAL_ERROR(ss, SSL_ERROR_BAD_2ND_CLIENT_HELLO, illegal_parameter);
             goto loser;
         }
-
-        /* CH1/CH2 must either both include ECH, or both exclude it. */
-        if ((ss->xtnData.echConfigId.len > 0) != previousEchOffered) {
-            FATAL_ERROR(ss, SSL_ERROR_BAD_2ND_CLIENT_HELLO,
-                        illegal_parameter);
-            goto loser;
-        }
     }
 
     /* Now merge the ClientHello into the hash state. */
     rv = ssl_HashHandshakeMessage(ss, ssl_hs_client_hello, msg, len);
     if (rv != SECSuccess) {
         FATAL_ERROR(ss, SEC_ERROR_LIBRARY_FAILURE, internal_error);
         goto loser;
     }
@@ -1931,16 +1938,23 @@ tls13_HandleClientHelloPart2(sslSocket *
             goto loser;
         }
         if (!clientShare) {
             FATAL_ERROR(ss, SSL_ERROR_BAD_2ND_CLIENT_HELLO,
                         illegal_parameter);
             goto loser;
         }
 
+        /* CH1/CH2 must either both include ECH, or both exclude it. */
+        if (previousEchOffered != (ss->xtnData.ech != NULL)) {
+            FATAL_ERROR(ss, SSL_ERROR_BAD_2ND_CLIENT_HELLO,
+                        previousEchOffered ? missing_extension : illegal_parameter);
+            goto loser;
+        }
+
         /* If we requested a new key share, check that the client provided just
          * one of the right type. */
         if (previousGroup) {
             if (PR_PREV_LINK(&ss->xtnData.remoteKeyShares) !=
                 PR_NEXT_LINK(&ss->xtnData.remoteKeyShares)) {
                 FATAL_ERROR(ss, SSL_ERROR_BAD_2ND_CLIENT_HELLO,
                             illegal_parameter);
                 goto loser;
@@ -2820,16 +2834,21 @@ tls13_SendServerHelloSequence(sslSocket 
 
     rv = ssl3_RegisterExtensionSender(ss, &ss->xtnData,
                                       ssl_tls13_supported_versions_xtn,
                                       tls13_ServerSendSupportedVersionsXtn);
     if (rv != SECSuccess) {
         return SECFailure;
     }
 
+    rv = tls13_ComputeHandshakeSecret(ss);
+    if (rv != SECSuccess) {
+        return SECFailure; /* error code is set. */
+    }
+
     rv = ssl3_SendServerHello(ss);
     if (rv != SECSuccess) {
         return rv; /* err code is set. */
     }
 
     if (ss->ssl3.hs.fakeSid.len) {
         PORT_Assert(!IS_DTLS(ss));
         SECITEM_FreeItem(&ss->ssl3.hs.fakeSid, PR_FALSE);
@@ -2904,17 +2923,17 @@ tls13_SendServerHelloSequence(sslSocket 
 
     /* Here we set a baseline value for our RTT estimation.
      * This value is updated when we get a response from the client. */
     ss->ssl3.hs.rttEstimate = ssl_Time(ss);
     return SECSuccess;
 }
 
 SECStatus
-tls13_HandleServerHelloPart2(sslSocket *ss)
+tls13_HandleServerHelloPart2(sslSocket *ss, const PRUint8 *savedMsg, PRUint32 savedLength)
 {
     SECStatus rv;
     sslSessionID *sid = ss->sec.ci.sid;
     SSL3Statistics *ssl3stats = SSL_GetStatistics();
 
     if (ssl3_ExtensionNegotiated(ss, ssl_tls13_pre_shared_key_xtn)) {
         PORT_Assert(!PR_CLIST_IS_EMPTY(&ss->ssl3.hs.psks));
         PORT_Assert(ss->xtnData.selectedPsk);
@@ -2986,16 +3005,27 @@ tls13_HandleServerHelloPart2(sslSocket *
         sid->peerCert = CERT_DupCertificate(ss->sec.peerCert);
     }
     sid->version = ss->version;
 
     rv = tls13_HandleServerKeyShare(ss);
     if (rv != SECSuccess) {
         return SECFailure;
     }
+
+    rv = tls13_ComputeHandshakeSecret(ss);
+    if (rv != SECSuccess) {
+        return SECFailure; /* error code is set. */
+    }
+
+    rv = tls13_MaybeHandleEchSignal(ss, savedMsg, savedLength);
+    if (rv != SECSuccess) {
+        return SECFailure; /* error code is set. */
+    }
+
     rv = tls13_ComputeHandshakeSecrets(ss);
     if (rv != SECSuccess) {
         return SECFailure; /* error code is set. */
     }
 
     if (ss->ssl3.hs.zeroRttState == ssl_0rtt_sent) {
         /* When we send 0-RTT, we saved the null spec in case we needed it to
          * send another ClientHello in response to a HelloRetryRequest.  Now
@@ -4955,25 +4985,26 @@ tls13_FinishHandshake(sslSocket *ss)
     /* Don't need this. */
     PK11_FreeSymKey(ss->ssl3.hs.clientHsTrafficSecret);
     ss->ssl3.hs.clientHsTrafficSecret = NULL;
     PK11_FreeSymKey(ss->ssl3.hs.serverHsTrafficSecret);
     ss->ssl3.hs.serverHsTrafficSecret = NULL;
 
     TLS13_SET_HS_STATE(ss, idle_handshake);
 
-    if (offeredEch &&
-        !ssl3_ExtensionNegotiated(ss, ssl_tls13_encrypted_client_hello_xtn)) {
+    PORT_Assert(ss->ssl3.hs.echAccepted ==
+                ssl3_ExtensionNegotiated(ss, ssl_tls13_encrypted_client_hello_xtn));
+    if (offeredEch && !ss->ssl3.hs.echAccepted) {
         SSL3_SendAlert(ss, alert_fatal, ech_required);
 
-        /* "If [one, none] of the values contains a supported version, the client can
+        /* "If [one, none] of the retry_configs contains a supported version, the client can
          * regard ECH as securely [replaced, disabled] by the server." */
-        if (ss->xtnData.echRetryConfigs.len) {
+        if (ss->xtnData.ech && ss->xtnData.ech->retryConfigs.len) {
             PORT_SetError(SSL_ERROR_ECH_RETRY_WITH_ECH);
-            ss->xtnData.echRetryConfigsValid = PR_TRUE;
+            ss->xtnData.ech->retryConfigsValid = PR_TRUE;
         } else {
             PORT_SetError(SSL_ERROR_ECH_RETRY_WITHOUT_ECH);
         }
         return SECFailure;
     }
 
     ssl_FinishHandshake(ss);
 
@@ -5515,16 +5546,17 @@ static const struct {
                                certificate) },
     { ssl_delegated_credentials_xtn, _M2(client_hello, certificate) },
     { ssl_tls13_cookie_xtn, _M2(client_hello, hello_retry_request) },
     { ssl_tls13_certificate_authorities_xtn, _M1(certificate_request) },
     { ssl_tls13_supported_versions_xtn, _M3(client_hello, server_hello,
                                             hello_retry_request) },
     { ssl_record_size_limit_xtn, _M2(client_hello, encrypted_extensions) },
     { ssl_tls13_encrypted_client_hello_xtn, _M2(client_hello, encrypted_extensions) },
+    { ssl_tls13_ech_is_inner_xtn, _M1(client_hello) },
     { ssl_tls13_outer_extensions_xtn, _M_NONE /* Encoding/decoding only */ },
     { ssl_tls13_post_handshake_auth_xtn, _M1(client_hello) }
 };
 
 tls13ExtensionStatus
 tls13_ExtensionStatus(PRUint16 extension, SSLHandshakeType message)
 {
     unsigned int i;
--- a/security/nss/lib/ssl/tls13con.h
+++ b/security/nss/lib/ssl/tls13con.h
@@ -71,17 +71,17 @@ PRBool tls13_AllowPskCipher(const sslSoc
 PRBool tls13_PskSuiteEnabled(sslSocket *ss);
 SECStatus tls13_WriteExtensionsWithBinder(sslSocket *ss, sslBuffer *extensions,
                                           sslBuffer *chBuf);
 SECStatus tls13_HandleClientHelloPart2(sslSocket *ss,
                                        const SECItem *suites,
                                        sslSessionID *sid,
                                        const PRUint8 *msg,
                                        unsigned int len);
-SECStatus tls13_HandleServerHelloPart2(sslSocket *ss);
+SECStatus tls13_HandleServerHelloPart2(sslSocket *ss, const PRUint8 *savedMsg, PRUint32 savedLength);
 SECStatus tls13_HandlePostHelloHandshakeMessage(sslSocket *ss, PRUint8 *b,
                                                 PRUint32 length);
 SECStatus tls13_ConstructHelloRetryRequest(sslSocket *ss,
                                            ssl3CipherSuite cipherSuite,
                                            const sslNamedGroupDef *selectedGroup,
                                            PRUint8 *cookie,
                                            unsigned int cookieLen,
                                            sslBuffer *buffer);
--- a/security/nss/lib/ssl/tls13ech.c
+++ b/security/nss/lib/ssl/tls13ech.c
@@ -9,24 +9,32 @@
 #include "pk11hpke.h"
 #include "ssl.h"
 #include "sslproto.h"
 #include "sslimpl.h"
 #include "selfencrypt.h"
 #include "ssl3exthandle.h"
 #include "tls13ech.h"
 #include "tls13exthandle.h"
+#include "tls13hashstate.h"
 #include "tls13hkdf.h"
 
 extern SECStatus
-ssl3_UpdateExplicitHandshakeTranscript(sslSocket *ss, const unsigned char *b,
-                                       unsigned int l, sslBuffer *transcriptBuf);
+ssl3_UpdateHandshakeHashesInt(sslSocket *ss, const unsigned char *b,
+                              unsigned int l, sslBuffer *transcriptBuf);
 extern SECStatus
 ssl3_HandleClientHelloPreamble(sslSocket *ss, PRUint8 **b, PRUint32 *length, SECItem *sidBytes,
                                SECItem *cookieBytes, SECItem *suites, SECItem *comps);
+extern SECStatus
+tls13_DeriveSecret(sslSocket *ss, PK11SymKey *key,
+                   const char *label,
+                   unsigned int labelLen,
+                   const SSL3Hashes *hashes,
+                   PK11SymKey **dest,
+                   SSLHashType hash);
 
 void
 tls13_DestroyEchConfig(sslEchConfig *config)
 {
     if (!config) {
         return;
     }
     SECITEM_FreeItem(&config->contents.publicKey, PR_FALSE);
@@ -43,16 +51,29 @@ tls13_DestroyEchConfigs(PRCList *list)
     PRCList *cur_p;
     while (!PR_CLIST_IS_EMPTY(list)) {
         cur_p = PR_LIST_TAIL(list);
         PR_REMOVE_LINK(cur_p);
         tls13_DestroyEchConfig((sslEchConfig *)cur_p);
     }
 }
 
+void
+tls13_DestroyEchXtnState(sslEchXtnState *state)
+{
+    if (!state) {
+        return;
+    }
+    SECITEM_FreeItem(&state->innerCh, PR_FALSE);
+    SECITEM_FreeItem(&state->senderPubKey, PR_FALSE);
+    SECITEM_FreeItem(&state->configId, PR_FALSE);
+    SECITEM_FreeItem(&state->retryConfigs, PR_FALSE);
+    PORT_ZFree(state, sizeof(*state));
+}
+
 SECStatus
 tls13_CopyEchConfigs(PRCList *oConfigs, PRCList *configs)
 {
     SECStatus rv;
     sslEchConfig *config;
     sslEchConfig *newConfig = NULL;
 
     for (PRCList *cur_p = PR_LIST_HEAD(oConfigs);
@@ -81,16 +102,17 @@ tls13_CopyEchConfigs(PRCList *oConfigs, 
                               &config->contents.suites);
         if (rv != SECSuccess) {
             goto loser;
         }
         newConfig->contents.kemId = config->contents.kemId;
         newConfig->contents.kdfId = config->contents.kdfId;
         newConfig->contents.aeadId = config->contents.aeadId;
         newConfig->contents.maxNameLen = config->contents.maxNameLen;
+        newConfig->version = config->version;
         PORT_Memcpy(newConfig->configId, config->configId, sizeof(newConfig->configId));
         PR_APPEND_LINK(&newConfig->link, configs);
     }
     return SECSuccess;
 
 loser:
     tls13_DestroyEchConfig(newConfig);
     tls13_DestroyEchConfigs(configs);
@@ -122,17 +144,17 @@ tls13_DigestEchConfig(const sslEchConfig
     PORT_Assert(cfg->contents.kdfId == HpkeKdfHkdfSha256);
     params.bExtract = CK_TRUE;
     params.bExpand = CK_TRUE;
     params.prfHashMechanism = CKM_SHA256;
     params.ulSaltType = CKF_HKDF_SALT_NULL;
     params.pInfo = CONST_CAST(CK_BYTE, hHkdfInfoEchConfigID);
     params.ulInfoLen = strlen(hHkdfInfoEchConfigID);
     derived = PK11_DeriveWithFlags(configKey, CKM_HKDF_DATA,
-                                   &paramsi, CKM_HKDF_DERIVE, CKA_DERIVE, 32,
+                                   &paramsi, CKM_HKDF_DERIVE, CKA_DERIVE, 8,
                                    CKF_SIGN | CKF_VERIFY);
 
     rv = PK11_ExtractKeyValue(derived);
     if (rv != SECSuccess) {
         goto loser;
     }
 
     derivedItem = PK11_GetKeyData(derived);
@@ -177,28 +199,30 @@ tls13_DecodeEchConfigContents(const sslR
     sslReader extensionReader;
     PRBool hasValidSuite = PR_FALSE;
 
     /* Parse the public_name. */
     rv = sslRead_ReadVariable(&configReader, 2, &tmpBuf);
     if (rv != SECSuccess) {
         goto loser;
     }
-    /* Make sure the public name doesn't contain any NULLs.
-     * TODO: Just store the SECItem instead. */
+
+    if (tmpBuf.len == 0) {
+        PORT_SetError(SSL_ERROR_RX_MALFORMED_ECH_CONFIG);
+        goto loser;
+    }
     for (tmpn = 0; tmpn < tmpBuf.len; tmpn++) {
         if (tmpBuf.buf[tmpn] == '\0') {
             PORT_SetError(SSL_ERROR_RX_MALFORMED_ECH_CONFIG);
             goto loser;
         }
     }
 
     contents.publicName = PORT_ZAlloc(tmpBuf.len + 1);
     if (!contents.publicName) {
-        PORT_SetError(SSL_ERROR_RX_MALFORMED_ECH_CONFIG);
         goto loser;
     }
     PORT_Memcpy(contents.publicName, (PRUint8 *)tmpBuf.buf, tmpBuf.len);
 
     /* Public key. */
     rv = sslRead_ReadVariable(&configReader, 2, &tmpBuf);
     if (rv != SECSuccess) {
         goto loser;
@@ -425,18 +449,18 @@ SSLExp_EncodeEchConfig(const char *publi
 {
     SECStatus rv;
     unsigned int savedOffset;
     unsigned int len;
     sslBuffer b = SSL_BUFFER_EMPTY;
     PRUint8 tmpBuf[66]; // Large enough for an EC public key, currently only X25519.
     unsigned int tmpLen;
 
-    if (!publicName || PORT_Strlen(publicName) == 0 || !hpkeSuites ||
-        hpkeSuiteCount == 0 || !pubKey || maxNameLen == 0 || !out || !outlen) {
+    if (!publicName || !hpkeSuites || hpkeSuiteCount == 0 ||
+        !pubKey || maxNameLen == 0 || !out || !outlen) {
         PORT_SetError(SEC_ERROR_INVALID_ARGS);
         return SECFailure;
     }
 
     rv = sslBuffer_Skip(&b, 2, NULL);
     if (rv != SECSuccess) {
         goto loser;
     }
@@ -530,22 +554,28 @@ SSLExp_GetEchRetryConfigs(PRFileDesc *fd
     }
     ss = ssl_FindSocket(fd);
     if (!ss) {
         SSL_DBG(("%d: SSL[%d]: bad socket in %s",
                  SSL_GETPID(), fd, __FUNCTION__));
         PORT_SetError(SEC_ERROR_INVALID_ARGS);
         return SECFailure;
     }
-    if (!ss->xtnData.echRetryConfigsValid) {
+
+    /* We don't distinguish between "handshake completed
+     * without retry configs", and "handshake not completed".
+     * An application should only call this after receiving a
+     * RETRY_WITH_ECH error code, which implies retry_configs. */
+    if (!ss->xtnData.ech || !ss->xtnData.ech->retryConfigsValid) {
         PORT_SetError(SSL_ERROR_HANDSHAKE_NOT_COMPLETED);
         return SECFailure;
     }
+
     /* May be empty. */
-    rv = SECITEM_CopyItem(NULL, &out, &ss->xtnData.echRetryConfigs);
+    rv = SECITEM_CopyItem(NULL, &out, &ss->xtnData.ech->retryConfigs);
     if (rv == SECFailure) {
         return SECFailure;
     }
     *retryConfigs = out;
     return SECSuccess;
 }
 
 SECStatus
@@ -566,18 +596,18 @@ SSLExp_RemoveEchConfigs(PRFileDesc *fd)
         return SECFailure;
     }
 
     if (!PR_CLIST_IS_EMPTY(&ss->echConfigs)) {
         tls13_DestroyEchConfigs(&ss->echConfigs);
     }
 
     /* Also remove any retry_configs and handshake context. */
-    if (ss->xtnData.echRetryConfigs.len) {
-        SECITEM_FreeItem(&ss->xtnData.echRetryConfigs, PR_FALSE);
+    if (ss->xtnData.ech && ss->xtnData.ech->retryConfigs.len) {
+        SECITEM_FreeItem(&ss->xtnData.ech->retryConfigs, PR_FALSE);
     }
 
     if (ss->ssl3.hs.echHpkeCtx) {
         PK11_HPKE_DestroyContext(ss->ssl3.hs.echHpkeCtx, PR_TRUE);
         ss->ssl3.hs.echHpkeCtx = NULL;
     }
     PORT_Free(CONST_CAST(char, ss->ssl3.hs.echPublicName));
     ss->ssl3.hs.echPublicName = NULL;
@@ -699,24 +729,17 @@ SSLExp_SetClientEchConfigs(PRFileDesc *f
  * keypair and the HPKE context */
 SECStatus
 tls13_ClientSetupEch(sslSocket *ss, sslClientHelloType type)
 {
     SECStatus rv;
     HpkeContext *cx = NULL;
     SECKEYPublicKey *pkR = NULL;
     SECItem hpkeInfo = { siBuffer, NULL, 0 };
-    PK11SymKey *hrrPsk = NULL;
     sslEchConfig *cfg = NULL;
-    const SECItem kEchHrrInfoItem = { siBuffer,
-                                      (unsigned char *)kHpkeInfoEchHrr,
-                                      strlen(kHpkeInfoEchHrr) };
-    const SECItem kEchHrrPskLabelItem = { siBuffer,
-                                          (unsigned char *)kHpkeLabelHrrPsk,
-                                          strlen(kHpkeLabelHrrPsk) };
 
     if (PR_CLIST_IS_EMPTY(&ss->echConfigs) ||
         !ssl_ShouldSendSNIExtension(ss, ss->url) ||
         IS_DTLS(ss)) {
         return SECSuccess;
     }
 
     /* Maybe apply our own priority if >1. For now, we only support
@@ -734,30 +757,22 @@ tls13_ClientSetupEch(sslSocket *ss, sslC
 
     switch (type) {
         case client_hello_initial:
             PORT_Assert(!ss->ssl3.hs.echHpkeCtx && !ss->ssl3.hs.echPublicName);
             cx = PK11_HPKE_NewContext(cfg->contents.kemId, cfg->contents.kdfId,
                                       cfg->contents.aeadId, NULL, NULL);
             break;
         case client_hello_retry:
-            PORT_Assert(ss->ssl3.hs.echHpkeCtx && ss->ssl3.hs.echPublicName);
-            rv = PK11_HPKE_ExportSecret(ss->ssl3.hs.echHpkeCtx,
-                                        &kEchHrrInfoItem, 32, &hrrPsk);
-            if (rv != SECSuccess) {
-                goto loser;
+            if (!ss->ssl3.hs.echHpkeCtx || !ss->ssl3.hs.echPublicName) {
+                FATAL_ERROR(ss, SEC_ERROR_LIBRARY_FAILURE, internal_error);
+                return SECFailure;
             }
-
-            PK11_HPKE_DestroyContext(ss->ssl3.hs.echHpkeCtx, PR_TRUE);
-            PORT_Free((void *)ss->ssl3.hs.echPublicName); /* CONST */
-            ss->ssl3.hs.echHpkeCtx = NULL;
-            ss->ssl3.hs.echPublicName = NULL;
-            cx = PK11_HPKE_NewContext(cfg->contents.kemId, cfg->contents.kdfId,
-                                      cfg->contents.aeadId, hrrPsk, &kEchHrrPskLabelItem);
-            break;
+            /* Nothing else to do. */
+            return SECSuccess;
         default:
             PORT_Assert(0);
             goto loser;
     }
     if (!cx) {
         goto loser;
     }
 
@@ -774,47 +789,44 @@ tls13_ClientSetupEch(sslSocket *ss, sslC
     PORT_Memcpy(&hpkeInfo.data[strlen(kHpkeInfoEch) + 1], cfg->raw.data, cfg->raw.len);
 
     /* Setup with an ephemeral sender keypair. */
     rv = PK11_HPKE_SetupS(cx, NULL, NULL, pkR, &hpkeInfo);
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    if (!ss->ssl3.hs.helloRetry) {
-        rv = ssl3_GetNewRandom(ss->ssl3.hs.client_inner_random);
-        if (rv != SECSuccess) {
-            goto loser; /* code set */
-        }
+    rv = ssl3_GetNewRandom(ss->ssl3.hs.client_inner_random);
+    if (rv != SECSuccess) {
+        goto loser; /* code set */
     }
 
     /* If ECH is rejected, the application will use SSLChannelInfo
      * to fetch this field and perform cert chain verification. */
     ss->ssl3.hs.echPublicName = PORT_Strdup(cfg->contents.publicName);
     if (!ss->ssl3.hs.echPublicName) {
         goto loser;
     }
 
     ss->ssl3.hs.echHpkeCtx = cx;
-    PK11_FreeSymKey(hrrPsk);
     SECKEY_DestroyPublicKey(pkR);
     SECITEM_FreeItem(&hpkeInfo, PR_FALSE);
     return SECSuccess;
 
 loser:
     PK11_HPKE_DestroyContext(cx, PR_TRUE);
-    PK11_FreeSymKey(hrrPsk);
     SECKEY_DestroyPublicKey(pkR);
     SECITEM_FreeItem(&hpkeInfo, PR_FALSE);
+    PORT_Assert(PORT_GetError() != 0);
     return SECFailure;
 }
 
 /*
  *  enum {
- *     encrypted_client_hello(0xfe08), (65535)
+ *     encrypted_client_hello(0xfe09), (65535)
  *  } ExtensionType;
  *
  *  struct {
  *      HpkeKdfId kdf_id;
  *      HpkeAeadId aead_id;
  *  } ECHCipherSuite;
  *  struct {
  *     ECHCipherSuite cipher_suite;
@@ -866,183 +878,70 @@ tls13_EncryptClientHello(sslSocket *ss, 
     rv = sslBuffer_AppendNumber(chInner, cfg->contents.kdfId, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
     rv = sslBuffer_AppendNumber(chInner, cfg->contents.aeadId, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
-    rv = sslBuffer_AppendVariable(chInner, cfg->configId, sizeof(cfg->configId), 1);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-    rv = sslBuffer_AppendVariable(chInner, hpkeEnc->data, hpkeEnc->len, 2);
-    if (rv != SECSuccess) {
-        goto loser;
+
+    if (!ss->ssl3.hs.helloRetry) {
+        rv = sslBuffer_AppendVariable(chInner, cfg->configId, sizeof(cfg->configId), 1);
+        if (rv != SECSuccess) {
+            goto loser;
+        }
+        rv = sslBuffer_AppendVariable(chInner, hpkeEnc->data, hpkeEnc->len, 2);
+        if (rv != SECSuccess) {
+            goto loser;
+        }
+    } else {
+        /* one byte for empty configId, two for empty Enc. */
+        rv = sslBuffer_AppendNumber(chInner, 0, 3);
+        if (rv != SECSuccess) {
+            goto loser;
+        }
     }
     rv = sslBuffer_AppendVariable(chInner, chCt->data, chCt->len, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
     SECITEM_FreeItem(chCt, PR_TRUE);
     return SECSuccess;
 
 loser:
     SECITEM_FreeItem(chCt, PR_TRUE);
     return SECFailure;
 }
 
 SECStatus
-tls13_GetMatchingEchConfig(const sslSocket *ss, HpkeKdfId kdf, HpkeAeadId aead,
-                           const SECItem *configId, sslEchConfig **cfg)
+tls13_GetMatchingEchConfigs(const sslSocket *ss, HpkeKdfId kdf, HpkeAeadId aead,
+                            const SECItem *configId, const sslEchConfig *cur, sslEchConfig **next)
 {
-    sslEchConfig *candidate;
     PRINT_BUF(50, (ss, "Server GetMatchingEchConfig with digest:",
                    configId->data, configId->len));
 
-    for (PRCList *cur_p = PR_LIST_HEAD(&ss->echConfigs);
+    /* If |cur|, resume the search at that node, else the list head. */
+    for (PRCList *cur_p = cur ? ((PRCList *)cur)->next : PR_LIST_HEAD(&ss->echConfigs);
          cur_p != &ss->echConfigs;
          cur_p = PR_NEXT_LINK(cur_p)) {
         sslEchConfig *echConfig = (sslEchConfig *)cur_p;
         if (configId->len != sizeof(echConfig->configId) ||
             PORT_Memcmp(echConfig->configId, configId->data, sizeof(echConfig->configId))) {
             continue;
         }
-        candidate = (sslEchConfig *)PR_LIST_HEAD(&ss->echConfigs);
-        if (candidate->contents.aeadId != aead ||
-            candidate->contents.kdfId != kdf) {
-            continue;
+        if (echConfig->contents.aeadId == aead &&
+            echConfig->contents.kdfId == kdf) {
+            *next = echConfig;
+            return SECSuccess;
         }
-        *cfg = candidate;
-        return SECSuccess;
-    }
-
-    SSL_TRC(50, ("%d: TLS13[%d]: Server found no matching ECHConfig",
-                 SSL_GETPID(), ss->fd));
-
-    *cfg = NULL;
-    return SECSuccess;
-}
-
-/* This is unfortunate in that it requires a second decryption of the cookie.
- * This is largely copied from tls13hashstate.c as HRR handling is still in flux.
- * TODO: Consolidate this code no later than -09. */
-/* struct {
- *     uint8 indicator = 0xff;            // To disambiguate from tickets.
- *     uint16 cipherSuite;                // Selected cipher suite.
- *     uint16 keyShare;                   // Requested key share group (0=none)
- *     opaque applicationToken<0..65535>; // Application token
- *     opaque echHrrPsk<0..255>;          // Encrypted ClientHello HRR PSK
- *     opaque echConfigId<0..255>;        // ECH config ID selected in CH1, to decrypt the CH2 ECH payload.
- *     opaque ch_hash[rest_of_buffer];    // H(ClientHello)
- * } CookieInner;
- */
-SECStatus
-tls13_GetEchInfoFromCookie(sslSocket *ss, const TLSExtension *hrrCookie, PK11SymKey **echHrrPsk, SECItem *echConfigId)
-{
-    SECStatus rv;
-    PK11SymKey *hrrKey = NULL;
-    PRUint64 tmpn;
-    sslReadBuffer tmpReader = { 0 };
-    PK11SlotInfo *slot = NULL;
-    unsigned char plaintext[1024];
-    unsigned int plaintextLen = 0;
-    SECItem hrrPskItem = { siBuffer, NULL, 0 };
-    SECItem hrrCookieData = { siBuffer, NULL, 0 };
-    SECItem saveHrrCookieData = hrrCookieData;
-    SECItem previousEchConfigId = { siBuffer, NULL, 0 };
-
-    /* Copy the extension data so as to not consume it in the handler.
-     * The extension handler walks the pointer, so save a copy to free. */
-    rv = SECITEM_CopyItem(NULL, &hrrCookieData, &hrrCookie->data);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-    saveHrrCookieData = hrrCookieData;
-
-    rv = tls13_ServerHandleCookieXtn(ss, &ss->xtnData, &hrrCookieData);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-
-    rv = ssl_SelfEncryptUnprotect(ss, ss->xtnData.cookie.data, ss->xtnData.cookie.len,
-                                  plaintext, &plaintextLen, sizeof(plaintext));
-    if (rv != SECSuccess) {
-        goto loser;
     }
 
-    sslReader reader = SSL_READER(plaintext, plaintextLen);
-
-    /* Should start with 0xff. */
-    rv = sslRead_ReadNumber(&reader, 1, &tmpn);
-    if ((rv != SECSuccess) || (tmpn != 0xff)) {
-        rv = SECFailure;
-        goto loser;
-    }
-    rv = sslRead_ReadNumber(&reader, 2, &tmpn);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-    /* The named group, if any. */
-    rv = sslRead_ReadNumber(&reader, 2, &tmpn);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-    /* Application token. */
-    rv = sslRead_ReadNumber(&reader, 2, &tmpn);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-    rv = sslRead_Read(&reader, tmpn, &tmpReader);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-
-    /* ECH Config ID */
-    rv = sslRead_ReadVariable(&reader, 1, &tmpReader);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-    rv = SECITEM_MakeItem(NULL, &previousEchConfigId,
-                          tmpReader.buf, tmpReader.len);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-
-    /* ECH HRR key. */
-    rv = sslRead_ReadVariable(&reader, 1, &tmpReader);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-    if (tmpReader.len) {
-        slot = PK11_GetInternalSlot();
-        if (!slot) {
-            rv = SECFailure;
-            goto loser;
-        }
-        hrrPskItem.len = tmpReader.len;
-        hrrPskItem.data = CONST_CAST(PRUint8, tmpReader.buf);
-        hrrKey = PK11_ImportSymKey(slot, CKM_HKDF_KEY_GEN, PK11_OriginUnwrap,
-                                   CKA_DERIVE, &hrrPskItem, NULL);
-        PK11_FreeSlot(slot);
-        if (!hrrKey) {
-            rv = SECFailure;
-            goto loser;
-        }
-    }
-    *echConfigId = previousEchConfigId;
-    *echHrrPsk = hrrKey;
-    SECITEM_FreeItem(&saveHrrCookieData, PR_FALSE);
+    *next = NULL;
     return SECSuccess;
-
-loser:
-    SECITEM_FreeItem(&previousEchConfigId, PR_FALSE);
-    SECITEM_FreeItem(&saveHrrCookieData, PR_FALSE);
-    return SECFailure;
 }
 
 /* Given a CH with extensions, copy from the start up to the extensions
  * into |writer| and return the extensions themselves in |extensions|.
  * If |explicitSid|, place this value into |writer| as the SID. Else,
  * the sid is copied from |reader| to |writer|. */
 static SECStatus
 tls13_CopyChPreamble(sslReader *reader, const SECItem *explicitSid, sslBuffer *writer, sslReadBuffer *extensions)
@@ -1105,41 +1004,75 @@ tls13_CopyChPreamble(sslReader *reader, 
     if (SSL_READER_REMAINING(reader) != 0) {
         PORT_SetError(SSL_ERROR_RX_MALFORMED_ECH_EXTENSION);
         return SECFailure;
     }
 
     return SECSuccess;
 }
 
+/*
+ *   struct {
+ *      HpkeKdfId kdfId;               // ClientECH.cipher_suite.kdf
+ *      HpkeAeadId aeadId;             // ClientECH.cipher_suite.aead
+ *      opaque config_id<0..255>;      // ClientECH.config_id
+ *      opaque enc<1..2^16-1>;         // ClientECH.enc
+ *      opaque outer_hello<1..2^24-1>;
+ *   } ClientHelloOuterAAD;
+ */
 static SECStatus
-tls13_MakeChOuterAAD(const SECItem *outer, sslBuffer *outerAAD)
+tls13_MakeChOuterAAD(sslSocket *ss, const SECItem *outer, SECItem *outerAAD)
 {
     SECStatus rv;
     sslBuffer aad = SSL_BUFFER_EMPTY;
-    sslReadBuffer aadXtns;
+    sslReadBuffer aadXtns = { 0 };
     sslReader chReader = SSL_READER(outer->data, outer->len);
     PRUint64 tmpn;
-    sslReadBuffer tmpvar;
+    sslReadBuffer tmpvar = { 0 };
     unsigned int offset;
-    unsigned int preambleLen;
+    unsigned int savedOffset;
+    PORT_Assert(ss->xtnData.ech);
+
+    rv = sslBuffer_AppendNumber(&aad, ss->xtnData.ech->kdfId, 2);
+    if (rv != SECSuccess) {
+        goto loser;
+    }
+    rv = sslBuffer_AppendNumber(&aad, ss->xtnData.ech->aeadId, 2);
+    if (rv != SECSuccess) {
+        goto loser;
+    }
 
-    rv = sslBuffer_Skip(&aad, 4, NULL);
+    if (!ss->ssl3.hs.helloRetry) {
+        rv = sslBuffer_AppendVariable(&aad, ss->xtnData.ech->configId.data,
+                                      ss->xtnData.ech->configId.len, 1);
+        if (rv != SECSuccess) {
+            goto loser;
+        }
+        rv = sslBuffer_AppendVariable(&aad, ss->xtnData.ech->senderPubKey.data,
+                                      ss->xtnData.ech->senderPubKey.len, 2);
+    } else {
+        /* 1B config_id length, 2B enc length. */
+        rv = sslBuffer_AppendNumber(&aad, 0, 3);
+    }
+    if (rv != SECSuccess) {
+        goto loser;
+    }
+
+    /* Skip 3 bytes for the CHOuter length. */
+    rv = sslBuffer_Skip(&aad, 3, &savedOffset);
     if (rv != SECSuccess) {
         goto loser;
     }
 
     /* aad := preamble, aadXtn := extensions */
     rv = tls13_CopyChPreamble(&chReader, NULL, &aad, &aadXtns);
     if (rv != SECSuccess) {
         goto loser;
     }
-
     sslReader xtnsReader = SSL_READER(aadXtns.buf, aadXtns.len);
-    preambleLen = SSL_BUFFER_LEN(&aad);
 
     /* Save room for extensions length. */
     rv = sslBuffer_Skip(&aad, 2, &offset);
     if (rv != SECSuccess) {
         goto loser;
     }
 
     /* Append each extension, minus encrypted_client_hello_xtn. */
@@ -1160,107 +1093,96 @@ tls13_MakeChOuterAAD(const SECItem *oute
             }
             rv = sslBuffer_AppendVariable(&aad, tmpvar.buf, tmpvar.len, 2);
             if (rv != SECSuccess) {
                 goto loser;
             }
         }
     }
 
-    rv = sslBuffer_InsertNumber(&aad, offset, SSL_BUFFER_LEN(&aad) - preambleLen - 2, 2);
+    rv = sslBuffer_InsertLength(&aad, offset, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    /* Give it a message header. */
-    rv = sslBuffer_InsertNumber(&aad, 0, ssl_hs_client_hello, 1);
+    rv = sslBuffer_InsertLength(&aad, savedOffset, 3);
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    rv = sslBuffer_InsertLength(&aad, 1, 3);
-    if (rv != SECSuccess) {
-        goto loser;
-    }
-    *outerAAD = aad;
+    outerAAD->data = aad.buf;
+    outerAAD->len = aad.len;
     return SECSuccess;
 
 loser:
     sslBuffer_Clear(&aad);
     return SECFailure;
 }
 
 SECStatus
-tls13_OpenClientHelloInner(sslSocket *ss, const SECItem *outer, sslEchConfig *cfg, PK11SymKey *echHrrPsk, SECItem **chInner)
+tls13_OpenClientHelloInner(sslSocket *ss, const SECItem *outer, const SECItem *outerAAD, sslEchConfig *cfg, SECItem **chInner)
 {
     SECStatus rv;
-    sslBuffer outerAAD = SSL_BUFFER_EMPTY;
     HpkeContext *cx = NULL;
     SECItem *decryptedChInner = NULL;
     SECItem hpkeInfo = { siBuffer, NULL, 0 };
-    SECItem outerAADItem = { siBuffer, NULL, 0 };
-    const SECItem kEchHrrPskLabelItem = { siBuffer,
-                                          (unsigned char *)kHpkeLabelHrrPsk,
-                                          strlen(kHpkeLabelHrrPsk) };
     SSL_TRC(50, ("%d: TLS13[%d]: Server opening ECH Inner%s", SSL_GETPID(),
                  ss->fd, ss->ssl3.hs.helloRetry ? " after HRR" : ""));
 
-    cx = PK11_HPKE_NewContext(cfg->contents.kemId, cfg->contents.kdfId,
-                              cfg->contents.aeadId, echHrrPsk,
-                              echHrrPsk ? &kEchHrrPskLabelItem : NULL);
-    if (!cx) {
-        goto loser;
+    if (!ss->ssl3.hs.helloRetry) {
+        PORT_Assert(!ss->ssl3.hs.echHpkeCtx);
+        cx = PK11_HPKE_NewContext(cfg->contents.kemId, cfg->contents.kdfId,
+                                  cfg->contents.aeadId, NULL, NULL);
+        if (!cx) {
+            goto loser;
+        }
+
+        if (!SECITEM_AllocItem(NULL, &hpkeInfo, strlen(kHpkeInfoEch) + 1 + cfg->raw.len)) {
+            goto loser;
+        }
+        PORT_Memcpy(&hpkeInfo.data[0], kHpkeInfoEch, strlen(kHpkeInfoEch));
+        PORT_Memset(&hpkeInfo.data[strlen(kHpkeInfoEch)], 0, 1);
+        PORT_Memcpy(&hpkeInfo.data[strlen(kHpkeInfoEch) + 1], cfg->raw.data, cfg->raw.len);
+
+        rv = PK11_HPKE_SetupR(cx, ss->echPubKey, ss->echPrivKey,
+                              &ss->xtnData.ech->senderPubKey, &hpkeInfo);
+        if (rv != SECSuccess) {
+            goto loser; /* code set */
+        }
+    } else {
+        PORT_Assert(ss->ssl3.hs.echHpkeCtx);
+        cx = ss->ssl3.hs.echHpkeCtx;
     }
 
-    if (!SECITEM_AllocItem(NULL, &hpkeInfo, strlen(kHpkeInfoEch) + 1 + cfg->raw.len)) {
-        goto loser;
-    }
-    PORT_Memcpy(&hpkeInfo.data[0], kHpkeInfoEch, strlen(kHpkeInfoEch));
-    PORT_Memset(&hpkeInfo.data[strlen(kHpkeInfoEch)], 0, 1);
-    PORT_Memcpy(&hpkeInfo.data[strlen(kHpkeInfoEch) + 1], cfg->raw.data, cfg->raw.len);
-
-    rv = PK11_HPKE_SetupR(cx, ss->echPubKey, ss->echPrivKey,
-                          &ss->xtnData.echSenderPubKey, &hpkeInfo);
-    if (rv != SECSuccess) {
-        goto loser; /* code set */
-    }
-
-    rv = tls13_MakeChOuterAAD(outer, &outerAAD);
-    if (rv != SECSuccess) {
-        goto loser; /* code set */
-    }
-
-    outerAADItem.data = outerAAD.buf;
-    outerAADItem.len = outerAAD.len;
-
 #ifndef UNSAFE_FUZZER_MODE
-    rv = PK11_HPKE_Open(cx, &outerAADItem, &ss->xtnData.innerCh, &decryptedChInner);
+    rv = PK11_HPKE_Open(cx, outerAAD, &ss->xtnData.ech->innerCh, &decryptedChInner);
     if (rv != SECSuccess) {
         goto loser; /* code set */
     }
 #else
-    rv = SECITEM_CopyItem(NULL, decryptedChInner, &ss->xtnData.innerCh);
+    rv = SECITEM_CopyItem(NULL, decryptedChInner, &ss->xtnData.ech->innerCh);
     if (rv != SECSuccess) {
         goto loser;
     }
     decryptedChInner->len -= 16; /* Fake tag */
 #endif
 
     /* Stash the context, we may need it for HRR. */
     ss->ssl3.hs.echHpkeCtx = cx;
     *chInner = decryptedChInner;
     SECITEM_FreeItem(&hpkeInfo, PR_FALSE);
-    sslBuffer_Clear(&outerAAD);
     return SECSuccess;
 
 loser:
     SECITEM_FreeItem(decryptedChInner, PR_TRUE);
-    PK11_HPKE_DestroyContext(cx, PR_TRUE);
     SECITEM_FreeItem(&hpkeInfo, PR_FALSE);
-    sslBuffer_Clear(&outerAAD);
+    if (cx != ss->ssl3.hs.echHpkeCtx) {
+        /* Don't double-free if it's already global. */
+        PK11_HPKE_DestroyContext(cx, PR_TRUE);
+    }
     return SECFailure;
 }
 
 /* Given a buffer of extensions prepared for CHOuter, translate those extensions to a
  * buffer suitable for CHInner. This is intended to be called twice: once without
  * compression for the transcript hash and binders, and once with compression for
  * encoding the actual CHInner value. On the first run, if |inOutPskXtn| and
  * chOuterXtnsBuf contains a PSK extension, remove it and return in the outparam.
@@ -1284,17 +1206,17 @@ tls13_ConstructInnerExtensionsFromOuter(
     unsigned int tmpLen;
     unsigned int srcXtnBase; /* To truncate CHOuter and remove the PSK extension. */
     SSL_TRC(50, ("%d: TLS13[%d]: Constructing ECH inner extensions %s compression",
                  SSL_GETPID(), compress ? "with" : "without"));
 
     /* When offering the "encrypted_client_hello" extension in its
      * ClientHelloOuter, the client MUST also offer an empty
      * "encrypted_client_hello" extension in its ClientHelloInner. */
-    rv = sslBuffer_AppendNumber(chInnerXtns, ssl_tls13_encrypted_client_hello_xtn, 2);
+    rv = sslBuffer_AppendNumber(chInnerXtns, ssl_tls13_ech_is_inner_xtn, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
     rv = sslBuffer_AppendNumber(chInnerXtns, 0, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
 
@@ -1503,19 +1425,21 @@ SECStatus
 tls13_ConstructClientHelloWithEch(sslSocket *ss, const sslSessionID *sid, PRBool freshSid,
                                   sslBuffer *chOuter, sslBuffer *chOuterXtnsBuf)
 {
     SECStatus rv;
     sslBuffer chInner = SSL_BUFFER_EMPTY;
     sslBuffer encodedChInner = SSL_BUFFER_EMPTY;
     sslBuffer chInnerXtns = SSL_BUFFER_EMPTY;
     sslBuffer pskXtn = SSL_BUFFER_EMPTY;
-    sslBuffer outerAAD = SSL_BUFFER_EMPTY;
+    sslBuffer aad = SSL_BUFFER_EMPTY;
     unsigned int encodedChLen;
     unsigned int preambleLen;
+    const SECItem *hpkeEnc = NULL;
+    unsigned int savedOffset;
     SSL_TRC(50, ("%d: TLS13[%d]: Constructing ECH inner", SSL_GETPID()));
 
     /* Create the full (uncompressed) inner extensions and steal any PSK extension.
      * NB: Neither chOuterXtnsBuf nor chInnerXtns are length-prefixed. */
     rv = tls13_ConstructInnerExtensionsFromOuter(ss, chOuterXtnsBuf, &chInnerXtns,
                                                  &pskXtn, PR_FALSE);
     if (rv != SECSuccess) {
         goto loser; /* code set */
@@ -1546,63 +1470,100 @@ tls13_ConstructClientHelloWithEch(sslSoc
         PORT_Memcpy(pskXtn.buf, &chInnerXtns.buf[chInnerXtns.len - pskXtn.len], pskXtn.len);
     } else {
         rv = sslBuffer_AppendBufferVariable(&chInner, &chInnerXtns, 2);
     }
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    rv = ssl3_UpdateExplicitHandshakeTranscript(ss, chInner.buf, chInner.len,
-                                                &ss->ssl3.hs.echInnerMessages);
+    rv = ssl3_UpdateHandshakeHashesInt(ss, chInner.buf, chInner.len,
+                                       &ss->ssl3.hs.echInnerMessages);
     if (rv != SECSuccess) {
         goto loser; /* code set */
     }
 
     /* Un-append the extensions, then append compressed via Encoded. */
     SSL_BUFFER_LEN(&chInner) = preambleLen;
     sslBuffer_Clear(&chInnerXtns);
     rv = tls13_ConstructInnerExtensionsFromOuter(ss, chOuterXtnsBuf,
                                                  &chInnerXtns, &pskXtn, PR_TRUE);
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    /* TODO: Pad CHInner */
     rv = tls13_EncodeClientHelloInner(ss, &chInner, &chInnerXtns, &encodedChInner);
     if (rv != SECSuccess) {
         goto loser;
     }
 
     /* Pad the outer prior to appending ECH (for the AAD).
      * Encoded extension size is (echCipherSuite + enc + configId + payload + tag).
      * Post-encryption, we'll assert that this was correct. */
-    encodedChLen = 4 + 33 + 34 + 2 + encodedChInner.len + 16;
+    encodedChLen = 4 + 1 + 2 + 2 + encodedChInner.len + 16;
+    if (!ss->ssl3.hs.helloRetry) {
+        encodedChLen += 8 + 32; /* configId || enc */
+    }
     rv = ssl_InsertPaddingExtension(ss, chOuter->len + encodedChLen, chOuterXtnsBuf);
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    /* Make the ClientHelloOuterAAD value, which is complete
-     * chOuter minus encrypted_client_hello xtn. */
-    rv = sslBuffer_Append(&outerAAD, chOuter->buf, chOuter->len);
+    PORT_Assert(!PR_CLIST_IS_EMPTY(&ss->echConfigs));
+    sslEchConfig *cfg = (sslEchConfig *)PR_LIST_HEAD(&ss->echConfigs);
+    rv = sslBuffer_AppendNumber(&aad, cfg->contents.kdfId, 2);
+    if (rv != SECSuccess) {
+        goto loser;
+    }
+    rv = sslBuffer_AppendNumber(&aad, cfg->contents.aeadId, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
-    rv = sslBuffer_AppendBufferVariable(&outerAAD, chOuterXtnsBuf, 2);
+
+    if (!ss->ssl3.hs.helloRetry) {
+        rv = sslBuffer_AppendVariable(&aad, cfg->configId, sizeof(cfg->configId), 1);
+        if (rv != SECSuccess) {
+            goto loser;
+        }
+        hpkeEnc = PK11_HPKE_GetEncapPubKey(ss->ssl3.hs.echHpkeCtx);
+        if (!hpkeEnc) {
+            FATAL_ERROR(ss, SEC_ERROR_LIBRARY_FAILURE, internal_error);
+            goto loser;
+        }
+        rv = sslBuffer_AppendVariable(&aad, hpkeEnc->data, hpkeEnc->len, 2);
+    } else {
+        /* 1B config_id length, 2B enc length. */
+        rv = sslBuffer_AppendNumber(&aad, 0, 3);
+    }
     if (rv != SECSuccess) {
         goto loser;
     }
-    rv = sslBuffer_InsertLength(&outerAAD, 1, 3);
+
+    rv = sslBuffer_Skip(&aad, 3, &savedOffset);
+    if (rv != SECSuccess) {
+        goto loser;
+    }
+
+    /* Skip the handshake header. */
+    PORT_Assert(chOuter->len > 4);
+    rv = sslBuffer_Append(&aad, &chOuter->buf[4], chOuter->len - 4);
+    if (rv != SECSuccess) {
+        goto loser;
+    }
+    rv = sslBuffer_AppendBufferVariable(&aad, chOuterXtnsBuf, 2);
+    if (rv != SECSuccess) {
+        goto loser;
+    }
+    rv = sslBuffer_InsertLength(&aad, savedOffset, 3);
     if (rv != SECSuccess) {
         goto loser;
     }
 
     /* Insert the encrypted_client_hello xtn and coalesce. */
-    rv = tls13_EncryptClientHello(ss, &outerAAD, &encodedChInner);
+    rv = tls13_EncryptClientHello(ss, &aad, &encodedChInner);
     if (rv != SECSuccess) {
         goto loser;
     }
     PORT_Assert(encodedChLen == encodedChInner.len);
 
     rv = ssl3_EmplaceExtension(ss, chOuterXtnsBuf, ssl_tls13_encrypted_client_hello_xtn,
                                encodedChInner.buf, encodedChInner.len, PR_TRUE);
     if (rv != SECSuccess) {
@@ -1613,74 +1574,133 @@ tls13_ConstructClientHelloWithEch(sslSoc
     if (rv != SECSuccess) {
         goto loser;
     }
 
     rv = sslBuffer_AppendBufferVariable(chOuter, chOuterXtnsBuf, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
+    sslBuffer_Clear(&chInner);
+    sslBuffer_Clear(&encodedChInner);
+    sslBuffer_Clear(&chInnerXtns);
+    sslBuffer_Clear(&pskXtn);
+    sslBuffer_Clear(&aad);
+    return SECSuccess;
 
 loser:
     sslBuffer_Clear(&chInner);
     sslBuffer_Clear(&encodedChInner);
     sslBuffer_Clear(&chInnerXtns);
     sslBuffer_Clear(&pskXtn);
-    sslBuffer_Clear(&outerAAD);
-    return rv;
+    sslBuffer_Clear(&aad);
+    PORT_Assert(PORT_GetError() != 0);
+    return SECFailure;
 }
 
+/* Compute the ECH signal using the transcript (up to, excluding) Server Hello.
+ * We'll append an artificial SH (ServerHelloECHConf). The server sources
+ * this transcript prefix from ss->ssl3.hs.messages, as it never uses
+ * ss->ssl3.hs.echInnerMessages. The client uses the inner transcript, echInnerMessages. */
 static SECStatus
-tls13_ComputeEchSignal(sslSocket *ss, PRUint8 *out)
+tls13_ComputeEchSignal(sslSocket *ss, const PRUint8 *sh, unsigned int shLen, PRUint8 *out)
 {
     SECStatus rv;
-    PRUint8 derived[64];
-    SECItem randItem = { siBuffer,
-                         ss->sec.isServer ? ss->ssl3.hs.client_random : ss->ssl3.hs.client_inner_random,
-                         SSL3_RANDOM_LENGTH };
-    SSLHashType hashAlg = tls13_GetHash(ss);
-    PK11SymKey *extracted = NULL;
-    PK11SymKey *randKey = NULL;
-    PK11SlotInfo *slot = PK11_GetInternalSlot();
-    if (!slot) {
+    PK11SymKey *confirmationKey = NULL;
+    sslBuffer confMsgs = SSL_BUFFER_EMPTY;
+    sslBuffer *chSource = ss->sec.isServer ? &ss->ssl3.hs.messages : &ss->ssl3.hs.echInnerMessages;
+    SSL3Hashes hashes;
+    SECItem *confirmationBytes;
+    unsigned int offset = sizeof(SSL3ProtocolVersion) +
+                          SSL3_RANDOM_LENGTH - TLS13_ECH_SIGNAL_LEN;
+    PORT_Assert(sh && shLen > offset);
+    PORT_Assert(TLS13_ECH_SIGNAL_LEN <= SSL3_RANDOM_LENGTH);
+
+    rv = sslBuffer_AppendBuffer(&confMsgs, chSource);
+    if (rv != SECSuccess) {
         goto loser;
     }
 
-    randKey = PK11_ImportDataKey(slot, CKM_HKDF_DATA, PK11_OriginUnwrap,
-                                 CKA_DERIVE, &randItem, NULL);
-    if (!randKey) {
+    /* Re-create the message header. */
+    rv = sslBuffer_AppendNumber(&confMsgs, ssl_hs_server_hello, 1);
+    if (rv != SECSuccess) {
         goto loser;
     }
 
-    rv = tls13_HkdfExtract(NULL, randKey, hashAlg, &extracted);
+    rv = sslBuffer_AppendNumber(&confMsgs, shLen, 3);
+    if (rv != SECSuccess) {
+        goto loser;
+    }
+
+    /* Copy the version and 24B of server_random. */
+    rv = sslBuffer_Append(&confMsgs, sh, offset);
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    rv = tls13_HkdfExpandLabelRaw(extracted, hashAlg, ss->ssl3.hs.server_random, 24,
-                                  kHkdfInfoEchConfirm, strlen(kHkdfInfoEchConfirm),
-                                  ss->protocolVariant, derived, TLS13_ECH_SIGNAL_LEN);
+    /* Zero the signal placeholder. */
+    rv = sslBuffer_AppendNumber(&confMsgs, 0, TLS13_ECH_SIGNAL_LEN);
+    if (rv != SECSuccess) {
+        goto loser;
+    }
+    offset += TLS13_ECH_SIGNAL_LEN;
+
+    /* Use the remainder of SH. */
+    rv = sslBuffer_Append(&confMsgs, &sh[offset], shLen - offset);
+    if (rv != SECSuccess) {
+        goto loser;
+    }
+
+    rv = tls13_ComputeHash(ss, &hashes, confMsgs.buf, confMsgs.len,
+                           tls13_GetHash(ss));
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    PORT_Memcpy(out, derived, TLS13_ECH_SIGNAL_LEN);
+    /*  accept_confirmation =
+     *          Derive-Secret(Handshake Secret,
+     *                        "ech accept confirmation",
+     *                        ClientHelloInner...ServerHelloECHConf)
+     */
+    rv = tls13_DeriveSecret(ss, ss->ssl3.hs.currentSecret,
+                            kHkdfInfoEchConfirm, strlen(kHkdfInfoEchConfirm),
+                            &hashes, &confirmationKey, tls13_GetHash(ss));
+    if (rv != SECSuccess) {
+        return SECFailure;
+    }
+
+    rv = PK11_ExtractKeyValue(confirmationKey);
+    if (rv != SECSuccess) {
+        goto loser;
+    }
+    confirmationBytes = PK11_GetKeyData(confirmationKey);
+    if (!confirmationBytes) {
+        rv = SECFailure;
+        PORT_SetError(SSL_ERROR_ECH_FAILED);
+        goto loser;
+    }
+    if (confirmationBytes->len < TLS13_ECH_SIGNAL_LEN) {
+        FATAL_ERROR(ss, SEC_ERROR_LIBRARY_FAILURE, internal_error);
+        goto loser;
+    }
     SSL_TRC(50, ("%d: TLS13[%d]: %s computed ECH signal", SSL_GETPID(), ss->fd, SSL_ROLE(ss)));
     PRINT_BUF(50, (ss, "", out, TLS13_ECH_SIGNAL_LEN));
-    PK11_FreeSymKey(extracted);
-    PK11_FreeSymKey(randKey);
-    PK11_FreeSlot(slot);
+
+    PORT_Memcpy(out, confirmationBytes->data, TLS13_ECH_SIGNAL_LEN);
+    PK11_FreeSymKey(confirmationKey);
+    sslBuffer_Clear(&confMsgs);
+    sslBuffer_Clear(&ss->ssl3.hs.messages);
+    sslBuffer_Clear(&ss->ssl3.hs.echInnerMessages);
     return SECSuccess;
 
 loser:
-    PK11_FreeSymKey(extracted);
-    PK11_FreeSymKey(randKey);
-    if (slot) {
-        PK11_FreeSlot(slot);
-    }
+    PK11_FreeSymKey(confirmationKey);
+    sslBuffer_Clear(&confMsgs);
+    sslBuffer_Clear(&ss->ssl3.hs.messages);
+    sslBuffer_Clear(&ss->ssl3.hs.echInnerMessages);
     return SECFailure;
 }
 
 /* Called just prior to padding the CH. Use the size of the CH to estimate
  * the size of a corresponding ECH extension, then add it to the buffer. */
 SECStatus
 tls13_MaybeGreaseEch(sslSocket *ss, unsigned int preambleLen, sslBuffer *buf)
 {
@@ -1690,26 +1710,35 @@ tls13_MaybeGreaseEch(sslSocket *ss, unsi
     unsigned int payloadLen;
     HpkeAeadId aead;
     PK11SlotInfo *slot = NULL;
     PK11SymKey *hmacPrk = NULL;
     PK11SymKey *derivedData = NULL;
     SECItem *rawData;
     CK_HKDF_PARAMS params;
     SECItem paramsi;
+    /* 1B aead determinant (don't send), 8B config_id, 32B enc, payload */
+    const int kNonPayloadLen = 41;
 
     if (!ss->opt.enableTls13GreaseEch || ss->ssl3.hs.echHpkeCtx) {
         return SECSuccess;
     }
 
     if (ss->vrange.max < SSL_LIBRARY_VERSION_TLS_1_3 ||
         IS_DTLS(ss)) {
         return SECSuccess;
     }
 
+    /* In draft-09, CH2 sends exactly the same GREASE ECH extension. */
+    if (ss->ssl3.hs.helloRetry) {
+        return ssl3_EmplaceExtension(ss, buf, ssl_tls13_encrypted_client_hello_xtn,
+                                     ss->ssl3.hs.greaseEchBuf.buf,
+                                     ss->ssl3.hs.greaseEchBuf.len, PR_TRUE);
+    }
+
     /* Compress the extensions for payload length. */
     rv = tls13_ConstructInnerExtensionsFromOuter(ss, buf, &chInnerXtns,
                                                  NULL, PR_TRUE);
     if (rv != SECSuccess) {
         goto loser; /* Code set */
     }
     payloadLen = preambleLen + 2 /* Xtns len */ + chInnerXtns.len - 4 /* msg header */;
     payloadLen += 16; /* Aead tag */
@@ -1729,33 +1758,32 @@ tls13_MaybeGreaseEch(sslSocket *ss, unsi
     params.bExpand = CK_TRUE;
     params.prfHashMechanism = CKM_SHA256;
     params.pInfo = NULL;
     params.ulInfoLen = 0;
     paramsi.data = (unsigned char *)&params;
     paramsi.len = sizeof(params);
     derivedData = PK11_DeriveWithFlags(hmacPrk, CKM_HKDF_DATA,
                                        &paramsi, CKM_HKDF_DATA,
-                                       CKA_DERIVE, 65 + payloadLen,
+                                       CKA_DERIVE, kNonPayloadLen + payloadLen,
                                        CKF_VERIFY);
     if (!derivedData) {
         goto loser;
     }
 
     rv = PK11_ExtractKeyValue(derivedData);
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    /* 1B aead determinant (don't send), 32B config_id, 32B enc, payload */
     rawData = PK11_GetKeyData(derivedData);
     if (!rawData) {
         goto loser;
     }
-    PORT_Assert(rawData->len == 65 + payloadLen);
+    PORT_Assert(rawData->len == kNonPayloadLen + payloadLen);
 
     /* struct {
        HpkeKdfId kdf_id;
        HpkeAeadId aead_id;
        opaque config_id<0..255>;
        opaque enc<1..2^16-1>;
        opaque payload<1..2^16-1>;
     } ClientECH; */
@@ -1768,49 +1796,51 @@ tls13_MaybeGreaseEch(sslSocket *ss, unsi
 
     /* HpkeAeadAes128Gcm = 1, HpkeAeadChaCha20Poly1305 = 3, */
     aead = (rawData->data[0] & 1) ? HpkeAeadAes128Gcm : HpkeAeadChaCha20Poly1305;
     rv = sslBuffer_AppendNumber(&greaseBuf, aead, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    rv = sslBuffer_AppendVariable(&greaseBuf, &rawData->data[1], 32, 1);
+    /* config_id, 8B */
+    rv = sslBuffer_AppendVariable(&greaseBuf, &rawData->data[1], 8, 1);
     if (rv != SECSuccess) {
         goto loser;
     }
 
     /* enc len is fixed 32B for X25519. */
-    rv = sslBuffer_AppendVariable(&greaseBuf, &rawData->data[33], 32, 2);
+    rv = sslBuffer_AppendVariable(&greaseBuf, &rawData->data[9], 32, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
 
-    rv = sslBuffer_AppendVariable(&greaseBuf, &rawData->data[65], payloadLen, 2);
+    rv = sslBuffer_AppendVariable(&greaseBuf, &rawData->data[kNonPayloadLen], payloadLen, 2);
     if (rv != SECSuccess) {
         goto loser;
     }
 
     /* Mark ECH as advertised so that we can validate any response.
-     * We'll use echHpkeCtx to determine if we sent real or GREASE ECH.
-     * TODO: Maybe a broader need to similarly track GREASED extensions? */
+     * We'll use echHpkeCtx to determine if we sent real or GREASE ECH. */
     rv = ssl3_EmplaceExtension(ss, buf, ssl_tls13_encrypted_client_hello_xtn,
                                greaseBuf.buf, greaseBuf.len, PR_TRUE);
     if (rv != SECSuccess) {
         goto loser;
     }
-    sslBuffer_Clear(&greaseBuf);
+
+    /* Stash the GREASE ECH extension - in the case of HRR, CH2 must echo it. */
+    ss->ssl3.hs.greaseEchBuf = greaseBuf;
+
     sslBuffer_Clear(&chInnerXtns);
     PK11_FreeSymKey(hmacPrk);
     PK11_FreeSymKey(derivedData);
     PK11_FreeSlot(slot);
     return SECSuccess;
 
 loser:
-    sslBuffer_Clear(&greaseBuf);
     sslBuffer_Clear(&chInnerXtns);
     PK11_FreeSymKey(hmacPrk);
     PK11_FreeSymKey(derivedData);
     if (slot) {
         PK11_FreeSlot(slot);
     }
     return SECFailure;
 }
@@ -1856,25 +1886,21 @@ tls13_MaybeHandleEch(sslSocket *ss, cons
         rv = ssl3_HandleClientHelloPreamble(ss, &b, &length, &tmpSid,
                                             &tmpCookie, &tmpSuites, &tmpComps);
         if (rv != SECSuccess) {
             goto loser; /* code set, alert sent. */
         }
 
         /* Since in Outer we explicitly call the ECH handler, do the same on Inner.
          * Extensions are already parsed in tls13_MaybeAcceptEch. */
-        echExtension = ssl3_FindExtension(ss, ssl_tls13_encrypted_client_hello_xtn);
+        echExtension = ssl3_FindExtension(ss, ssl_tls13_ech_is_inner_xtn);
         if (!echExtension) {
-            FATAL_ERROR(ss, SSL_ERROR_MISSING_ECH_EXTENSION, decode_error);
+            FATAL_ERROR(ss, SSL_ERROR_MISSING_ECH_EXTENSION, illegal_parameter);
             goto loser;
         }
-        rv = tls13_ServerHandleEchXtn(ss, &ss->xtnData, &echExtension->data);
-        if (rv != SECSuccess) {
-            goto loser; /* code set, alert sent. */
-        }
 
         versionExtension = ssl3_FindExtension(ss, ssl_tls13_supported_versions_xtn);
         if (!versionExtension) {
             FATAL_ERROR(ss, SSL_ERROR_UNSUPPORTED_VERSION, protocol_version);
             goto loser;
         }
         rv = tls13_NegotiateVersion(ss, versionExtension);
         if (rv != SECSuccess) {
@@ -1890,51 +1916,64 @@ tls13_MaybeHandleEch(sslSocket *ss, cons
         *sidBytes = tmpSid;
         *suites = tmpSuites;
         *echInner = tmpEchInner;
     }
     return SECSuccess;
 
 loser:
     SECITEM_FreeItem(tmpEchInner, PR_TRUE);
+    PORT_Assert(PORT_GetError() != 0);
     return SECFailure;
 }
 
 SECStatus
-tls13_MaybeHandleEchSignal(sslSocket *ss)
+tls13_MaybeHandleEchSignal(sslSocket *ss, const PRUint8 *sh, PRUint32 shLen)
 {
     SECStatus rv;
     PRUint8 computed[TLS13_ECH_SIGNAL_LEN];
     const PRUint8 *signal = &ss->ssl3.hs.server_random[SSL3_RANDOM_LENGTH - TLS13_ECH_SIGNAL_LEN];
     PORT_Assert(!ss->sec.isServer);
 
     /* If !echHpkeCtx, we either didn't advertise or sent GREASE ECH. */
-    if (ss->ssl3.hs.echHpkeCtx) {
-        PORT_Assert(ssl3_ExtensionAdvertised(ss, ssl_tls13_encrypted_client_hello_xtn));
-        rv = tls13_ComputeEchSignal(ss, computed);
-        if (rv != SECSuccess) {
+    if (!ss->ssl3.hs.echHpkeCtx) {
+        ss->ssl3.hs.preliminaryInfo |= ssl_preinfo_ech;
+        return SECSuccess;
+    }
+
+    PORT_Assert(ssl3_ExtensionAdvertised(ss, ssl_tls13_encrypted_client_hello_xtn));
+    rv = tls13_ComputeEchSignal(ss, sh, shLen, computed);
+    if (rv != SECSuccess) {
+        return SECFailure;
+    }
+
+    ss->ssl3.hs.echAccepted = !PORT_Memcmp(computed, signal, TLS13_ECH_SIGNAL_LEN);
+    ss->ssl3.hs.preliminaryInfo |= ssl_preinfo_ech;
+    if (ss->ssl3.hs.echAccepted) {
+        if (ss->version < SSL_LIBRARY_VERSION_TLS_1_3) {
+            FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_SERVER_HELLO, illegal_parameter);
             return SECFailure;
         }
+        if (ss->ssl3.hs.helloRetry && ss->sec.isServer) {
+            /* Enc and ConfigId are stored in the cookie and must not
+                * be included in CH2.ClientECH. */
+            if (ss->xtnData.ech->senderPubKey.len || ss->xtnData.ech->configId.len) {
+                ssl3_ExtSendAlert(ss, alert_fatal, illegal_parameter);
+                PORT_SetError(SSL_ERROR_BAD_2ND_CLIENT_HELLO);
+                return SECFailure;
+            }
+        }
 
-        ss->ssl3.hs.echAccepted = !PORT_Memcmp(computed, signal, TLS13_ECH_SIGNAL_LEN);
-        if (ss->ssl3.hs.echAccepted) {
-            if (ss->version < SSL_LIBRARY_VERSION_TLS_1_3) {
-                FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_SERVER_HELLO, illegal_parameter);
-                return SECFailure;
-            }
-            ss->xtnData.negotiated[ss->xtnData.numNegotiated++] = ssl_tls13_encrypted_client_hello_xtn;
-            PORT_Memcpy(ss->ssl3.hs.client_random, ss->ssl3.hs.client_inner_random, SSL3_RANDOM_LENGTH);
-        }
-        /* If rejected, leave echHpkeCtx and echPublicName for rejection paths. */
-        ssl3_CoalesceEchHandshakeHashes(ss);
-        SSL_TRC(50, ("%d: TLS13[%d]: ECH %s accepted by server",
-                     SSL_GETPID(), ss->fd, ss->ssl3.hs.echAccepted ? "is" : "is not"));
+        ss->xtnData.negotiated[ss->xtnData.numNegotiated++] = ssl_tls13_encrypted_client_hello_xtn;
+        PORT_Memcpy(ss->ssl3.hs.client_random, ss->ssl3.hs.client_inner_random, SSL3_RANDOM_LENGTH);
     }
-
-    ss->ssl3.hs.preliminaryInfo |= ssl_preinfo_ech;
+    /* If rejected, leave echHpkeCtx and echPublicName for rejection paths. */
+    ssl3_CoalesceEchHandshakeHashes(ss);
+    SSL_TRC(50, ("%d: TLS13[%d]: ECH %s accepted by server",
+                 SSL_GETPID(), ss->fd, ss->ssl3.hs.echAccepted ? "is" : "is not"));
     return SECSuccess;
 }
 
 static SECStatus
 tls13_UnencodeChInner(sslSocket *ss, const SECItem *sidBytes, SECItem **echInner)
 {
     SECStatus rv;
     sslReadBuffer outerExtensionsList;
@@ -2087,116 +2126,154 @@ loser:
 
 SECStatus
 tls13_MaybeAcceptEch(sslSocket *ss, const SECItem *sidBytes, const PRUint8 *chOuter,
                      unsigned int chOuterLen, SECItem **chInner)
 {
     SECStatus rv;
     SECItem outer = { siBuffer, CONST_CAST(PRUint8, chOuter), chOuterLen };
     SECItem *decryptedChInner = NULL;
-    PK11SymKey *echHrrPsk = NULL;
     SECItem hrrCh1ConfigId = { siBuffer, NULL, 0 };
-    HpkeKdfId kdf;
-    HpkeAeadId aead;
+    SECItem outerAAD = { siBuffer, NULL, 0 };
+    SECItem cookieData = { siBuffer, NULL, 0 };
+    HpkeContext *ch1EchHpkeCtx = NULL;
+    HpkeKdfId echKdfId;
+    HpkeAeadId echAeadId;
     sslEchConfig *candidate = NULL; /* non-owning */
     TLSExtension *hrrXtn;
-    SECItem *configId = ss->ssl3.hs.helloRetry ? &hrrCh1ConfigId : &ss->xtnData.echConfigId;
-    if (!ss->xtnData.innerCh.len) {
+
+    if (!ss->xtnData.ech) {
         return SECSuccess;
     }
 
-    PORT_Assert(ss->xtnData.echSenderPubKey.data);
-    PORT_Assert(ss->xtnData.echConfigId.data);
-    PORT_Assert(ss->xtnData.echCipherSuite);
+    PORT_Assert(ss->xtnData.ech->innerCh.data);
 
     if (ss->ssl3.hs.helloRetry) {
+        PORT_Assert(!ss->ssl3.hs.echHpkeCtx);
         hrrXtn = ssl3_FindExtension(ss, ssl_tls13_cookie_xtn);
         if (!hrrXtn) {
             /* If the client doesn't echo cookie, we can't decrypt. */
             return SECSuccess;
         }
 
-        rv = tls13_GetEchInfoFromCookie(ss, hrrXtn, &echHrrPsk, &hrrCh1ConfigId);
+        PORT_Assert(!ss->xtnData.ech->configId.data);
+        PORT_Assert(!ss->ssl3.hs.echHpkeCtx);
+
+        PRUint8 *tmp = hrrXtn->data.data;
+        PRUint32 len = hrrXtn->data.len;
+        rv = ssl3_ExtConsumeHandshakeVariable(ss, &cookieData, 2,
+                                              &tmp, &len);
         if (rv != SECSuccess) {
-            /* If we failed due to an issue with the cookie, continue without
-             * ECH and let the HRR code handle the problem. */
-            goto exit_success;
+            return SECFailure;
         }
 
-        /* No CH1 config_id means ECH wasn't advertised in CH1.
-         * No CH1 HRR PSK means that ECH was not accepted in CH1, and the
-         * HRR was generated off CH1Outer. */
-        if (hrrCh1ConfigId.len == 0) {
+        /* Extract ECH info without restoring hash state. If there's
+         * something wrong with the cookie, continue without ECH
+         * and let HRR code handle the problem. */
+        rv = tls13_HandleHrrCookie(ss, cookieData.data, cookieData.len,
+                                   NULL, NULL, NULL, &echKdfId, &echAeadId,
+                                   &hrrCh1ConfigId, &ch1EchHpkeCtx, PR_FALSE);
+        if (rv != SECSuccess) {
+            return SECSuccess;
+        }
+
+        ss->xtnData.ech->configId = hrrCh1ConfigId;
+        ss->ssl3.hs.echHpkeCtx = ch1EchHpkeCtx;
+
+        if (echKdfId != ss->xtnData.ech->kdfId ||
+            echAeadId != ss->xtnData.ech->aeadId) {
             FATAL_ERROR(ss, SSL_ERROR_BAD_2ND_CLIENT_HELLO,
                         illegal_parameter);
-            goto loser;
+            return SECFailure;
         }
-        if (!echHrrPsk) {
-            goto exit_success;
+
+        if (!ss->ssl3.hs.echHpkeCtx) {
+            return SECSuccess;
         }
     }
-    kdf = (HpkeKdfId)(ss->xtnData.echCipherSuite & 0xFFFF);
-    aead = (HpkeAeadId)(((ss->xtnData.echCipherSuite) >> 16) & 0xFFFF);
-    rv = tls13_GetMatchingEchConfig(ss, kdf, aead, configId, &candidate);
+
+    /* Cookie data was good, proceed with ECH. */
+    PORT_Assert(ss->xtnData.ech->configId.data);
+    rv = tls13_GetMatchingEchConfigs(ss, ss->xtnData.ech->kdfId, ss->xtnData.ech->aeadId,
+                                     &ss->xtnData.ech->configId, candidate, &candidate);
     if (rv != SECSuccess) {
-        goto loser;
+        FATAL_ERROR(ss, SEC_ERROR_LIBRARY_FAILURE, internal_error);
+        return SECFailure;
     }
-    if (!candidate || candidate->contents.kdfId != kdf ||
-        candidate->contents.aeadId != aead) {
-        /* Send retry_configs if we have any.
-         * This does *not* count as negotiating ECH. */
-        rv = ssl3_RegisterExtensionSender(ss, &ss->xtnData,
-                                          ssl_tls13_encrypted_client_hello_xtn,
-                                          tls13_ServerSendEchXtn);
-        goto exit_success;
+
+    if (candidate) {
+        rv = tls13_MakeChOuterAAD(ss, &outer, &outerAAD);
+        if (rv != SECSuccess) {
+            return SECFailure;
+        }
     }
 
-    rv = tls13_OpenClientHelloInner(ss, &outer, candidate, echHrrPsk, &decryptedChInner);
-    if (rv != SECSuccess) {
+    while (candidate) {
+        rv = tls13_OpenClientHelloInner(ss, &outer, &outerAAD, candidate, &decryptedChInner);
+        if (rv != SECSuccess) {
+            /* Get the next matching config */
+            rv = tls13_GetMatchingEchConfigs(ss, ss->xtnData.ech->kdfId, ss->xtnData.ech->aeadId,
+                                             &ss->xtnData.ech->configId, candidate, &candidate);
+            if (rv != SECSuccess) {
+                FATAL_ERROR(ss, SEC_ERROR_LIBRARY_FAILURE, internal_error);
+                SECITEM_FreeItem(&outerAAD, PR_FALSE);
+                return SECFailure;
+            }
+            continue;
+        }
+        break;
+    }
+    SECITEM_FreeItem(&outerAAD, PR_FALSE);
+
+    if (rv != SECSuccess || !decryptedChInner) {
         if (ss->ssl3.hs.helloRetry) {
-            FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_ESNI_EXTENSION, decrypt_error);
-            goto loser;
+            FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_ECH_EXTENSION, decrypt_error);
+            return SECFailure;
         } else {
-            rv = ssl3_RegisterExtensionSender(ss, &ss->xtnData,
-                                              ssl_tls13_encrypted_client_hello_xtn,
-                                              tls13_ServerSendEchXtn);
-            goto exit_success;
+            /* Send retry_configs (if we have any) when we fail to decrypt or
+            * found no candidates. This does *not* count as negotiating ECH. */
+            return ssl3_RegisterExtensionSender(ss, &ss->xtnData,
+                                                ssl_tls13_encrypted_client_hello_xtn,
+                                                tls13_ServerSendEchXtn);
         }
     }
+
     SSL_TRC(20, ("%d: TLS13[%d]: Successfully opened ECH inner CH",
                  SSL_GETPID(), ss->fd));
     ss->ssl3.hs.echAccepted = PR_TRUE;
 
     /* Stash the CHOuter extensions. They're not yet handled (only parsed). If
      * the CHInner contains outer_extensions_xtn, we'll need to reference them. */
     ssl3_MoveRemoteExtensions(&ss->ssl3.hs.echOuterExtensions, &ss->ssl3.hs.remoteExtensions);
 
     rv = tls13_UnencodeChInner(ss, sidBytes, &decryptedChInner);
     if (rv != SECSuccess) {
         SECITEM_FreeItem(decryptedChInner, PR_TRUE);
-        goto loser; /* code set */
+        return SECFailure; /* code set */
     }
     *chInner = decryptedChInner;
-
-exit_success:
-    PK11_FreeSymKey(echHrrPsk);
-    SECITEM_FreeItem(&hrrCh1ConfigId, PR_FALSE);
     return SECSuccess;
-
-loser:
-    PK11_FreeSymKey(echHrrPsk);
-    SECITEM_FreeItem(&hrrCh1ConfigId, PR_FALSE);
-    return SECFailure;
 }
 
 SECStatus
-tls13_WriteServerEchSignal(sslSocket *ss)
+tls13_WriteServerEchSignal(sslSocket *ss, PRUint8 *sh, unsigned int shLen)
 {
     SECStatus rv;
     PRUint8 signal[TLS13_ECH_SIGNAL_LEN];
-    rv = tls13_ComputeEchSignal(ss, signal);
+    PRUint8 *msg_random = &sh[sizeof(SSL3ProtocolVersion)];
+
+    PORT_Assert(shLen > sizeof(SSL3ProtocolVersion) + SSL3_RANDOM_LENGTH);
+    PORT_Assert(ss->version >= SSL_LIBRARY_VERSION_TLS_1_3);
+
+    rv = tls13_ComputeEchSignal(ss, sh, shLen, signal);
     if (rv != SECSuccess) {
         return SECFailure;
     }
-    PRUint8 *dest = &ss->ssl3.hs.server_random[SSL3_RANDOM_LENGTH - TLS13_ECH_SIGNAL_LEN];
+    PRUint8 *dest = &msg_random[SSL3_RANDOM_LENGTH - TLS13_ECH_SIGNAL_LEN];
     PORT_Memcpy(dest, signal, TLS13_ECH_SIGNAL_LEN);
+
+    /* Keep the socket copy consistent. */
+    PORT_Assert(0 == memcmp(msg_random, &ss->ssl3.hs.server_random, SSL3_RANDOM_LENGTH - TLS13_ECH_SIGNAL_LEN));
+    dest = &ss->ssl3.hs.server_random[SSL3_RANDOM_LENGTH - TLS13_ECH_SIGNAL_LEN];
+    PORT_Memcpy(dest, signal, TLS13_ECH_SIGNAL_LEN);
+
     return SECSuccess;
 }
--- a/security/nss/lib/ssl/tls13ech.h
+++ b/security/nss/lib/ssl/tls13ech.h
@@ -6,32 +6,30 @@
  * 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 __tls13ech_h_
 #define __tls13ech_h_
 
 #include "pk11hpke.h"
 
-/* draft-08, shared-mode only.
+/* draft-09, supporting shared-mode and split-mode as a backend server only.
  * Notes on the implementation status:
  * - Padding (https://tools.ietf.org/html/draft-ietf-tls-esni-08#section-6.2),
  *   is not implemented (see bug 1677181).
  * - When multiple ECHConfigs are provided by the server, the first compatible
  *   config is selected by the client. Ciphersuite choices are limited and only
  *   the AEAD may vary (AES-128-GCM or ChaCha20Poly1305).
  * - Some of the buffering (construction/compression/decompression) could likely
  *   be optimized, but the spec is still evolving so that work is deferred.
  */
-#define TLS13_ECH_VERSION 0xfe08
+#define TLS13_ECH_VERSION 0xfe09
 #define TLS13_ECH_SIGNAL_LEN 8
 
 static const char kHpkeInfoEch[] = "tls ech";
-static const char kHpkeInfoEchHrr[] = "tls ech hrr key";
-static const char kHpkeLabelHrrPsk[] = "hrr key";
 static const char hHkdfInfoEchConfigID[] = "tls ech config id";
 static const char kHkdfInfoEchConfirm[] = "ech accept confirmation";
 
 struct sslEchConfigContentsStr {
     char *publicName;
     SECItem publicKey; /* NULL on server. Use the keypair in sslEchConfig instead. */
     HpkeKemId kemId;
     HpkeKdfId kdfId;
@@ -40,21 +38,33 @@ struct sslEchConfigContentsStr {
                      * suite is placed in kdfId and aeadId. */
     PRUint16 maxNameLen;
     /* No supported extensions. */
 };
 
 struct sslEchConfigStr {
     PRCList link;
     SECItem raw;
-    PRUint8 configId[32];
+    PRUint8 configId[8];
     PRUint16 version;
     sslEchConfigContents contents;
 };
 
+struct sslEchXtnStateStr {
+    SECItem innerCh;          /* Server: ClientECH.payload */
+    SECItem senderPubKey;     /* Server: ClientECH.enc */
+    SECItem configId;         /* Server: ClientECH.config_id  */
+    HpkeKdfId kdfId;          /* Server: ClientECH.cipher_suite.kdf */
+    HpkeAeadId aeadId;        /* Server: ClientECH.cipher_suite.aead */
+    SECItem retryConfigs;     /* Client: ServerECH.retry_configs*/
+    PRBool retryConfigsValid; /* Client: Extraction of retry_configss is allowed.
+                               *  This is set once the handshake completes (having
+                               *  verified to the ECHConfig public name). */
+};
+
 SECStatus SSLExp_EncodeEchConfig(const char *publicName, const PRUint32 *hpkeSuites,
                                  unsigned int hpkeSuiteCount, HpkeKemId kemId,
                                  const SECKEYPublicKey *pubKey, PRUint16 maxNameLen,
                                  PRUint8 *out, unsigned int *outlen, unsigned int maxlen);
 SECStatus SSLExp_GetEchRetryConfigs(PRFileDesc *fd, SECItem *retryConfigs);
 SECStatus SSLExp_SetClientEchConfigs(PRFileDesc *fd, const PRUint8 *echConfigs,
                                      unsigned int echConfigsLen);
 SECStatus SSLExp_SetServerEchConfigs(PRFileDesc *fd,
@@ -64,19 +74,20 @@ SECStatus SSLExp_RemoveEchConfigs(PRFile
 
 SECStatus tls13_ClientSetupEch(sslSocket *ss, sslClientHelloType type);
 SECStatus tls13_ConstructClientHelloWithEch(sslSocket *ss, const sslSessionID *sid,
                                             PRBool freshSid, sslBuffer *chOuterBuf,
                                             sslBuffer *chInnerXtnsBuf);
 SECStatus tls13_CopyEchConfigs(PRCList *oconfigs, PRCList *configs);
 SECStatus tls13_DecodeEchConfigs(const SECItem *data, PRCList *configs);
 void tls13_DestroyEchConfigs(PRCList *list);
+void tls13_DestroyEchXtnState(sslEchXtnState *state);
 SECStatus tls13_GetMatchingEchConfig(const sslSocket *ss, HpkeKdfId kdf, HpkeAeadId aead,
                                      const SECItem *configId, sslEchConfig **cfg);
 SECStatus tls13_MaybeHandleEch(sslSocket *ss, const PRUint8 *msg, PRUint32 msgLen, SECItem *sidBytes,
                                SECItem *comps, SECItem *cookieBytes, SECItem *suites, SECItem **echInner);
-SECStatus tls13_MaybeHandleEchSignal(sslSocket *ss);
+SECStatus tls13_MaybeHandleEchSignal(sslSocket *ss, const PRUint8 *savedMsg, PRUint32 savedLength);
 SECStatus tls13_MaybeAcceptEch(sslSocket *ss, const SECItem *sidBytes, const PRUint8 *chOuter,
                                unsigned int chOuterLen, SECItem **chInner);
 SECStatus tls13_MaybeGreaseEch(sslSocket *ss, unsigned int prefixLen, sslBuffer *buf);
-SECStatus tls13_WriteServerEchSignal(sslSocket *ss);
+SECStatus tls13_WriteServerEchSignal(sslSocket *ss, PRUint8 *sh, unsigned int shLen);
 
 #endif
--- a/security/nss/lib/ssl/tls13exthandle.c
+++ b/security/nss/lib/ssl/tls13exthandle.c
@@ -1206,29 +1206,35 @@ tls13_ServerSendHrrCookieXtn(const sslSo
 SECStatus
 tls13_ClientHandleEchXtn(const sslSocket *ss, TLSExtensionData *xtnData,
                          SECItem *data)
 {
     SECStatus rv;
     PRCList parsedConfigs;
     PR_INIT_CLIST(&parsedConfigs);
 
+    PORT_Assert(!xtnData->ech);
+    xtnData->ech = PORT_ZNew(sslEchXtnState);
+    if (!xtnData->ech) {
+        return SECFailure;
+    }
+
     /* Parse the list to determine 1) That the configs are valid
      * and properly encoded, and 2) If any are compatible. */
     rv = tls13_DecodeEchConfigs(data, &parsedConfigs);
     if (rv == SECFailure) {
         ssl3_ExtSendAlert(ss, alert_fatal, decode_error);
         PORT_SetError(SSL_ERROR_RX_MALFORMED_ECH_CONFIG);
         return SECFailure;
     }
-    /* Don't mark ECH negotiated on retry. Save the the raw
-     * configs so the application can retry. If we sent GREASE
-     * ECH (no echHpkeCtx), don't apply returned retry_configs. */
+    /* Don't mark ECH negotiated on rejection with retry_config.
+     * Save the the raw configs so the application can retry. If
+     * we sent GREASE ECH (no echHpkeCtx), don't apply retry_configs. */
     if (ss->ssl3.hs.echHpkeCtx && !PR_CLIST_IS_EMPTY(&parsedConfigs)) {
-        rv = SECITEM_CopyItem(NULL, &xtnData->echRetryConfigs, data);
+        rv = SECITEM_CopyItem(NULL, &xtnData->ech->retryConfigs, data);
     }
     tls13_DestroyEchConfigs(&parsedConfigs);
 
     return rv;
 }
 
 /* Indicates support for the delegated credentials extension. This should be
  * hooked while processing the ClientHello. */
@@ -1460,24 +1466,32 @@ tls13_ServerHandleEchXtn(const sslSocket
 
     /* Ignore it if not doing 1.3+. If we have no ECHConfigs,
      * proceed to save the config_id for HRR validation. */
     if (ss->version < SSL_LIBRARY_VERSION_TLS_1_3 ||
         IS_DTLS(ss)) {
         return SECSuccess;
     }
 
-    /* On CHInner, the extension must be empty. */
-    if (ss->ssl3.hs.echAccepted && data->len > 0) {
+    if (ss->ssl3.hs.echAccepted) {
+        ssl3_ExtSendAlert(ss, alert_fatal, illegal_parameter);
+        PORT_SetError(SSL_ERROR_RX_UNEXPECTED_EXTENSION);
+        return SECFailure;
+    }
+
+    if (ssl3_FindExtension(CONST_CAST(sslSocket, ss), ssl_tls13_ech_is_inner_xtn)) {
         ssl3_ExtSendAlert(ss, alert_fatal, illegal_parameter);
-        PORT_SetError(SSL_ERROR_RX_MALFORMED_ECH_EXTENSION);
+        PORT_SetError(SSL_ERROR_RX_UNEXPECTED_EXTENSION);
         return SECFailure;
-    } else if (ss->ssl3.hs.echAccepted) {
-        xtnData->negotiated[xtnData->numNegotiated++] = ssl_tls13_encrypted_client_hello_xtn;
-        return SECSuccess;
+    }
+
+    PORT_Assert(!xtnData->ech);
+    xtnData->ech = PORT_ZNew(sslEchXtnState);
+    if (!xtnData->ech) {
+        return SECFailure;
     }
 
     /* Parse the KDF and AEAD. */
     rv = ssl3_ExtConsumeHandshakeNumber(ss, &tmp, 2,
                                         &data->data, &data->len);
     if (rv != SECSuccess) {
         goto alert_loser;
     }
@@ -1498,48 +1512,84 @@ tls13_ServerHandleEchXtn(const sslSocket
 
     /* enc */
     rv = ssl3_ExtConsumeHandshakeVariable(ss, &senderPubKey, 2,
                                           &data->data, &data->len);
     if (rv != SECSuccess) {
         goto alert_loser;
     }
 
-    /* payload */
+    /* payload, which must be final and non-empty. */
     rv = ssl3_ExtConsumeHandshakeVariable(ss, &encryptedCh, 2,
                                           &data->data, &data->len);
     if (rv != SECSuccess) {
         goto alert_loser;
     }
-
-    if (data->len) {
-        goto alert_loser;
-    }
-
-    /* All fields required. */
-    if (!configId.len || !senderPubKey.len || !encryptedCh.len) {
+    if (data->len || !encryptedCh.len) {
         goto alert_loser;
     }
 
-    rv = SECITEM_CopyItem(NULL, &xtnData->echSenderPubKey, &senderPubKey);
+    if (!ss->ssl3.hs.helloRetry) {
+        /* In the real ECH HRR case, config_id and enc should be empty. This
+         * is checked after acceptance, because it might be GREASE ECH. */
+        if (!configId.len || !senderPubKey.len) {
+            goto alert_loser;
+        }
+
+        rv = SECITEM_CopyItem(NULL, &xtnData->ech->senderPubKey, &senderPubKey);
+        if (rv == SECFailure) {
+            return SECFailure;
+        }
+
+        rv = SECITEM_CopyItem(NULL, &xtnData->ech->configId, &configId);
+        if (rv == SECFailure) {
+            return SECFailure;
+        }
+    }
+
+    rv = SECITEM_CopyItem(NULL, &xtnData->ech->innerCh, &encryptedCh);
     if (rv == SECFailure) {
         return SECFailure;
     }
-
-    rv = SECITEM_CopyItem(NULL, &xtnData->innerCh, &encryptedCh);
-    if (rv == SECFailure) {
-        return SECFailure;
-    }
-
-    rv = SECITEM_CopyItem(NULL, &xtnData->echConfigId, &configId);
-    if (rv == SECFailure) {
-        return SECFailure;
-    }
-    xtnData->echCipherSuite = (aead & 0xFFFF) << 16 | (kdf & 0xFFFF);
+    xtnData->ech->kdfId = kdf;
+    xtnData->ech->aeadId = aead;
 
     /* Not negotiated until tls13_MaybeAcceptEch. */
     return SECSuccess;
 
 alert_loser:
     ssl3_ExtSendAlert(ss, alert_fatal, decode_error);
     PORT_SetError(SSL_ERROR_RX_MALFORMED_ECH_EXTENSION);
     return SECFailure;
 }
+
+SECStatus
+tls13_ServerHandleEchIsInnerXtn(const sslSocket *ss,
+                                TLSExtensionData *xtnData,
+                                SECItem *data)
+{
+    SSL_TRC(3, ("%d: TLS13[%d]: handle ech_is_inner extension",
+                SSL_GETPID(), ss->fd));
+
+    if (data->len) {
+        PORT_SetError(SSL_ERROR_RX_MALFORMED_ECH_EXTENSION);
+        return SECFailure;
+    }
+
+    if (ssl3_FindExtension(CONST_CAST(sslSocket, ss), ssl_tls13_encrypted_client_hello_xtn)) {
+        ssl3_ExtSendAlert(ss, alert_fatal, illegal_parameter);
+        PORT_SetError(SSL_ERROR_RX_UNEXPECTED_EXTENSION);
+        return SECFailure;
+    }
+
+    /* Consider encrypted_client_hello_xtn negotiated if we performed the
+     * CHOuter decryption. This is only supported in shared mode, so we'll also
+     * handle ech_is_inner in that case. We might, however, receive a CHInner
+     * that was forwarded by a different client-facing server. In this case,
+     * mark ech_is_inner as negotiated, which triggers sending of the ECH
+     * acceptance signal. ech_is_inner_xtn being negotiated does not imply
+     * that any other ECH state actually exists. */
+    if (ss->ssl3.hs.echAccepted) {
+        xtnData->negotiated[xtnData->numNegotiated++] = ssl_tls13_encrypted_client_hello_xtn;
+    }
+    xtnData->negotiated[xtnData->numNegotiated++] = ssl_tls13_ech_is_inner_xtn;
+    return SECSuccess;
+}
--- a/security/nss/lib/ssl/tls13exthandle.h
+++ b/security/nss/lib/ssl/tls13exthandle.h
@@ -89,16 +89,19 @@ PRUint32 tls13_SizeOfKeyShareEntry(const
 SECStatus tls13_EncodeKeyShareEntry(sslBuffer *buf, SSLNamedGroup group,
                                     SECKEYPublicKey *pubKey);
 SECStatus tls13_ServerHandleEchXtn(const sslSocket *ss, TLSExtensionData *xtnData,
                                    SECItem *data);
 SECStatus tls13_ServerSendEchXtn(const sslSocket *ss, TLSExtensionData *xtnData,
                                  sslBuffer *buf, PRBool *added);
 SECStatus tls13_ClientHandleEchXtn(const sslSocket *ss, TLSExtensionData *xtnData,
                                    SECItem *data);
+SECStatus tls13_ServerHandleEchIsInnerXtn(const sslSocket *ss,
+                                          TLSExtensionData *xtnData,
+                                          SECItem *data);
 SECStatus tls13_ClientSendPostHandshakeAuthXtn(const sslSocket *ss,
                                                TLSExtensionData *xtnData,
                                                sslBuffer *buf, PRBool *added);
 SECStatus tls13_ServerHandlePostHandshakeAuthXtn(const sslSocket *ss,
                                                  TLSExtensionData *xtnData,
                                                  SECItem *data);
 SECStatus tls13_ClientHandleDelegatedCredentialsXtn(const sslSocket *ss,
                                                     TLSExtensionData *xtnData,
--- a/security/nss/lib/ssl/tls13hashstate.c
+++ b/security/nss/lib/ssl/tls13hashstate.c
@@ -19,99 +19,97 @@
 /*
  * The cookie is structured as a self-encrypted structure with the
  * inner value being.
  *
  * struct {
  *     uint8 indicator = 0xff;            // To disambiguate from tickets.
  *     uint16 cipherSuite;                // Selected cipher suite.
  *     uint16 keyShare;                   // Requested key share group (0=none)
+ *     HpkeKdfId kdfId;                   // ECH KDF (uint16)
+ *     HpkeAeadId aeadId;                 // ECH AEAD (uint16)
+ *     opaque echConfigId<0..255>;        // ECH config_id
+ *     opaque echHpkeCtx<0..65535>;       // ECH serialized HPKE context
  *     opaque applicationToken<0..65535>; // Application token
- *     echConfigId<0..255>;               // Encrypted Client Hello config_id
- *     echHrrPsk<0..255>;                 // Encrypted Client Hello HRR PSK
  *     opaque ch_hash[rest_of_buffer];    // H(ClientHello)
  * } CookieInner;
  *
  * An empty echConfigId means that ECH was not offered in the first ClientHello.
  * An empty echHrrPsk means that ECH was not accepted in CH1.
  */
 SECStatus
 tls13_MakeHrrCookie(sslSocket *ss, const sslNamedGroupDef *selectedGroup,
                     const PRUint8 *appToken, unsigned int appTokenLen,
                     PRUint8 *buf, unsigned int *len, unsigned int maxlen)
 {
     SECStatus rv;
     SSL3Hashes hashes;
     PRUint8 cookie[1024];
     sslBuffer cookieBuf = SSL_BUFFER(cookie);
     static const PRUint8 indicator = 0xff;
-    SECItem hrrNonceInfoItem = { siBuffer, (unsigned char *)kHpkeInfoEchHrr,
-                                 strlen(kHpkeInfoEchHrr) };
-    PK11SymKey *echHrrPsk = NULL;
-    SECItem *rawEchPsk = NULL;
+    SECItem *echHpkeCtx = NULL;
 
     /* Encode header. */
     rv = sslBuffer_Append(&cookieBuf, &indicator, 1);
     if (rv != SECSuccess) {
         return SECFailure;
     }
     rv = sslBuffer_AppendNumber(&cookieBuf, ss->ssl3.hs.cipher_suite, 2);
     if (rv != SECSuccess) {
         return SECFailure;
     }
     rv = sslBuffer_AppendNumber(&cookieBuf,
                                 selectedGroup ? selectedGroup->name : 0, 2);
     if (rv != SECSuccess) {
         return SECFailure;
     }
 
-    /* Application token. */
-    rv = sslBuffer_AppendVariable(&cookieBuf, appToken, appTokenLen, 2);
-    if (rv != SECSuccess) {
-        return SECFailure;
-    }
+    if (ss->xtnData.ech) {
+        rv = sslBuffer_AppendNumber(&cookieBuf, ss->xtnData.ech->kdfId, 2);
+        if (rv != SECSuccess) {
+            return SECFailure;
+        }
+        rv = sslBuffer_AppendNumber(&cookieBuf, ss->xtnData.ech->aeadId, 2);
+        if (rv != SECSuccess) {
+            return SECFailure;
+        }
 
-    /* Received ECH config_id, regardless of acceptance or possession
-     * of a matching ECHConfig. If rejecting ECH, this is essentially a boolean
-     * indicating that ECH was offered in CH1. If accepting ECH, this config_id
-     * will be used for the ECH decryption in CH2. */
-    if (ss->xtnData.echConfigId.len) {
-        rv = sslBuffer_AppendVariable(&cookieBuf, ss->xtnData.echConfigId.data,
-                                      ss->xtnData.echConfigId.len, 1);
-    } else {
-        PORT_Assert(!ssl3_FindExtension(ss, ssl_tls13_encrypted_client_hello_xtn));
-        rv = sslBuffer_AppendNumber(&cookieBuf, 0, 1);
-    }
-    if (rv != SECSuccess) {
-        return SECFailure;
-    }
-
-    /* Extract and encode the ech-hrr-key, if ECH was accepted
-     * (i.e. an Open() succeeded. */
-    if (ss->ssl3.hs.echAccepted) {
-        rv = PK11_HPKE_ExportSecret(ss->ssl3.hs.echHpkeCtx, &hrrNonceInfoItem, 32, &echHrrPsk);
+        /* Received ECH config_id, regardless of acceptance or possession
+         * of a matching ECHConfig. */
+        PORT_Assert(ss->xtnData.ech->configId.len == 8);
+        rv = sslBuffer_AppendVariable(&cookieBuf, ss->xtnData.ech->configId.data,
+                                      ss->xtnData.ech->configId.len, 1);
         if (rv != SECSuccess) {
             return SECFailure;
         }
-        rv = PK11_ExtractKeyValue(echHrrPsk);
+
+        /* There might be no HPKE Context, e.g. when we lack a matching ECHConfig. */
+        if (ss->ssl3.hs.echHpkeCtx) {
+            rv = PK11_HPKE_ExportContext(ss->ssl3.hs.echHpkeCtx, NULL, &echHpkeCtx);
+            if (rv != SECSuccess) {
+                return SECFailure;
+            }
+            rv = sslBuffer_AppendVariable(&cookieBuf, echHpkeCtx->data, echHpkeCtx->len, 2);
+            SECITEM_ZfreeItem(echHpkeCtx, PR_TRUE);
+        } else {
+            /* Zero length HPKE context. */
+            rv = sslBuffer_AppendNumber(&cookieBuf, 0, 2);
+        }
         if (rv != SECSuccess) {
-            PK11_FreeSymKey(echHrrPsk);
             return SECFailure;
         }
-        rawEchPsk = PK11_GetKeyData(echHrrPsk);
-        if (!rawEchPsk) {
-            PK11_FreeSymKey(echHrrPsk);
+    } else {
+        rv = sslBuffer_AppendNumber(&cookieBuf, 0, 7);
+        if (rv != SECSuccess) {
             return SECFailure;
         }
-        rv = sslBuffer_AppendVariable(&cookieBuf, rawEchPsk->data, rawEchPsk->len, 1);
-        PK11_FreeSymKey(echHrrPsk);
-    } else {
-        /* Zero length ech_hrr_key. */
-        rv = sslBuffer_AppendNumber(&cookieBuf, 0, 1);
     }
+
+    /* Application token. */
+    rv = sslBuffer_AppendVariable(&cookieBuf, appToken, appTokenLen, 2);
     if (rv != SECSuccess) {
         return SECFailure;
     }
 
     /* Compute and encode hashes. */
     rv = tls13_ComputeHandshakeHashes(ss, &hashes);
     if (rv != SECSuccess) {
         return SECFailure;
@@ -126,33 +124,44 @@ tls13_MakeHrrCookie(sslSocket *ss, const
                                 buf, len, maxlen);
     if (rv != SECSuccess) {
         return SECFailure;
     }
 
     return SECSuccess;
 }
 
-/* Recover the hash state from the cookie. */
+/* Given a cookie and cookieLen, decrypt and parse, returning
+ * any values that were requested via the "previous_" params. If
+ * recoverHashState is true, the transcript state is recovered */
 SECStatus
-tls13_RecoverHashState(sslSocket *ss,
-                       unsigned char *cookie, unsigned int cookieLen,
-                       ssl3CipherSuite *previousCipherSuite,
-                       const sslNamedGroupDef **previousGroup,
-                       PRBool *previousEchOffered)
+tls13_HandleHrrCookie(sslSocket *ss,
+                      unsigned char *cookie, unsigned int cookieLen,
+                      ssl3CipherSuite *previousCipherSuite,
+                      const sslNamedGroupDef **previousGroup,
+                      PRBool *previousEchOffered,
+                      HpkeKdfId *previousEchKdfId,
+                      HpkeAeadId *previousEchAeadId,
+                      SECItem *previousEchConfigId,
+                      HpkeContext **previousEchHpkeCtx,
+                      PRBool recoverHashState)
 {
     SECStatus rv;
     unsigned char plaintext[1024];
     unsigned int plaintextLen = 0;
     sslBuffer messageBuf = SSL_BUFFER_EMPTY;
-    sslReadBuffer echPskBuf;
-    sslReadBuffer echConfigIdBuf;
+    sslReadBuffer echHpkeBuf = { 0 };
+    sslReadBuffer echConfigIdBuf = { 0 };
     PRUint64 sentinel;
     PRUint64 cipherSuite;
+    HpkeContext *hpkeContext = NULL;
+    HpkeKdfId echKdfId;
+    HpkeAeadId echAeadId;
     PRUint64 group;
+    PRUint64 tmp64;
     const sslNamedGroupDef *selectedGroup;
     PRUint64 appTokenLen;
 
     rv = ssl_SelfEncryptUnprotect(ss, cookie, cookieLen,
                                   plaintext, &plaintextLen, sizeof(plaintext));
     if (rv != SECSuccess) {
         return SECFailure;
     }
@@ -175,16 +184,43 @@ tls13_RecoverHashState(sslSocket *ss,
     /* The named group, if any. */
     rv = sslRead_ReadNumber(&reader, 2, &group);
     if (rv != SECSuccess) {
         FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
         return SECFailure;
     }
     selectedGroup = ssl_LookupNamedGroup(group);
 
+    /* ECH Ciphersuite */
+    rv = sslRead_ReadNumber(&reader, 2, &tmp64);
+    if (rv != SECSuccess) {
+        FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
+        return SECFailure;
+    }
+    echKdfId = (HpkeKdfId)tmp64;
+
+    rv = sslRead_ReadNumber(&reader, 2, &tmp64);
+    if (rv != SECSuccess) {
+        FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
+        return SECFailure;
+    }
+    echAeadId = (HpkeAeadId)tmp64;
+
+    /* ECH Config ID and HPKE context may be empty. */
+    rv = sslRead_ReadVariable(&reader, 1, &echConfigIdBuf);
+    if (rv != SECSuccess) {
+        FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
+        return SECFailure;
+    }
+    rv = sslRead_ReadVariable(&reader, 2, &echHpkeBuf);
+    if (rv != SECSuccess) {
+        FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
+        return SECFailure;
+    }
+
     /* Application token. */
     PORT_Assert(ss->xtnData.applicationToken.len == 0);
     rv = sslRead_ReadNumber(&reader, 2, &appTokenLen);
     if (rv != SECSuccess) {
         FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
         return SECFailure;
     }
     if (SECITEM_AllocItem(NULL, &ss->xtnData.applicationToken,
@@ -197,60 +233,85 @@ tls13_RecoverHashState(sslSocket *ss,
     rv = sslRead_Read(&reader, appTokenLen, &appTokenReader);
     if (rv != SECSuccess) {
         FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
         return SECFailure;
     }
     PORT_Assert(appTokenReader.len == appTokenLen);
     PORT_Memcpy(ss->xtnData.applicationToken.data, appTokenReader.buf, appTokenLen);
 
-    /* ECH Config ID, which may be empty. */
-    rv = sslRead_ReadVariable(&reader, 1, &echConfigIdBuf);
-    if (rv != SECSuccess) {
-        FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
-        return SECFailure;
-    }
-    /* ECH HRR PSK, if present, is already used by tls13_GetEchInfoFromCookie */
-    rv = sslRead_ReadVariable(&reader, 1, &echPskBuf);
-    if (rv != SECSuccess) {
-        FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
-        return SECFailure;
-    }
+    /* The remainder is the hash. */
+    if (recoverHashState) {
+        unsigned int hashLen = SSL_READER_REMAINING(&reader);
+        if (hashLen != tls13_GetHashSize(ss)) {
+            FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
+            return SECFailure;
+        }
+
+        /* Now reinject the message. */
+        SSL_ASSERT_HASHES_EMPTY(ss);
+        rv = ssl_HashHandshakeMessageInt(ss, ssl_hs_message_hash, 0,
+                                         SSL_READER_CURRENT(&reader), hashLen,
+                                         ssl3_UpdateHandshakeHashes);
+        if (rv != SECSuccess) {
+            return SECFailure;
+        }
 
-    /* The remainder is the hash. */
-    unsigned int hashLen = SSL_READER_REMAINING(&reader);
-    if (hashLen != tls13_GetHashSize(ss)) {
-        FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_CLIENT_HELLO, illegal_parameter);
-        return SECFailure;
+        /* And finally reinject the HRR. */
+        rv = tls13_ConstructHelloRetryRequest(ss, cipherSuite,
+                                              selectedGroup,
+                                              cookie, cookieLen,
+                                              &messageBuf);
+        if (rv != SECSuccess) {
+            return SECFailure;
+        }
+
+        rv = ssl_HashHandshakeMessageInt(ss, ssl_hs_server_hello, 0,
+                                         SSL_BUFFER_BASE(&messageBuf),
+                                         SSL_BUFFER_LEN(&messageBuf),
+                                         ssl3_UpdateHandshakeHashes);
+        sslBuffer_Clear(&messageBuf);
+        if (rv != SECSuccess) {
+            return SECFailure;
+        }
     }
 
-    /* Now reinject the message. */
-    SSL_ASSERT_HASHES_EMPTY(ss);
-    rv = ssl_HashHandshakeMessageInt(ss, ssl_hs_message_hash, 0,
-                                     SSL_READER_CURRENT(&reader), hashLen,
-                                     ssl3_UpdateHandshakeHashes);
-    if (rv != SECSuccess) {
-        return SECFailure;
+    if (previousEchHpkeCtx && echHpkeBuf.len) {
+        const SECItem hpkeItem = { siBuffer, CONST_CAST(unsigned char, echHpkeBuf.buf),
+                                   echHpkeBuf.len };
+        hpkeContext = PK11_HPKE_ImportContext(&hpkeItem, NULL);
+        if (!hpkeContext) {
+            FATAL_ERROR(ss, PORT_GetError(), internal_error);
+            return SECFailure;
+        }
+    }
+
+    if (previousEchConfigId && echConfigIdBuf.len) {
+        SECItem tmp = { siBuffer, NULL, 0 };
+        rv = SECITEM_MakeItem(NULL, &tmp, echConfigIdBuf.buf, echConfigIdBuf.len);
+        if (rv != SECSuccess) {
+            PK11_HPKE_DestroyContext(hpkeContext, PR_TRUE);
+            FATAL_ERROR(ss, PORT_GetError(), internal_error);
+            return SECFailure;
+        }
+        *previousEchConfigId = tmp;
     }
 
-    /* And finally reinject the HRR. */
-    rv = tls13_ConstructHelloRetryRequest(ss, cipherSuite,
-                                          selectedGroup,
-                                          cookie, cookieLen,
-                                          &messageBuf);
-    if (rv != SECSuccess) {
-        return SECFailure;
+    if (previousEchKdfId) {
+        *previousEchKdfId = echKdfId;
+    }
+    if (previousEchAeadId) {
+        *previousEchAeadId = echAeadId;
+    }
+    if (previousEchHpkeCtx) {
+        *previousEchHpkeCtx = hpkeContext;
     }
-
-    rv = ssl_HashHandshakeMessageInt(ss, ssl_hs_server_hello, 0,
-                                     SSL_BUFFER_BASE(&messageBuf),
-                                     SSL_BUFFER_LEN(&messageBuf),
-                                     ssl3_UpdateHandshakeHashes);
-    sslBuffer_Clear(&messageBuf);
-    if (rv != SECSuccess) {
-        return SECFailure;
+    if (previousCipherSuite) {
+        *previousCipherSuite = cipherSuite;
     }
-
-    *previousCipherSuite = cipherSuite;
-    *previousGroup = selectedGroup;
-    *previousEchOffered = echConfigIdBuf.len > 0;
+    if (previousGroup) {
+        *previousGroup = selectedGroup;
+    }
+    if (previousEchOffered) {
+        *previousEchOffered = echConfigIdBuf.len > 0;
+    }
     return SECSuccess;
 }
--- a/security/nss/lib/ssl/tls13hashstate.h
+++ b/security/nss/lib/ssl/tls13hashstate.h
@@ -12,14 +12,19 @@
 #include "ssl.h"
 #include "sslt.h"
 #include "sslimpl.h"
 
 SECStatus tls13_MakeHrrCookie(sslSocket *ss, const sslNamedGroupDef *selectedGroup,
                               const PRUint8 *appToken, unsigned int appTokenLen,
                               PRUint8 *buf, unsigned int *len, unsigned int maxlen);
 SECStatus tls13_GetHrrCookieLength(sslSocket *ss, unsigned int *length);
-SECStatus tls13_RecoverHashState(sslSocket *ss,
-                                 unsigned char *cookie, unsigned int cookieLen,
-                                 ssl3CipherSuite *previousCipherSuite,
-                                 const sslNamedGroupDef **previousGroup,
-                                 PRBool *previousEchOffered);
+SECStatus tls13_HandleHrrCookie(sslSocket *ss,
+                                unsigned char *cookie, unsigned int cookieLen,
+                                ssl3CipherSuite *previousCipherSuite,
+                                const sslNamedGroupDef **previousGroup,
+                                PRBool *previousEchOffered,
+                                HpkeKdfId *previousEchKdfId,
+                                HpkeAeadId *previousEchAeadId,
+                                SECItem *previousEchConfigId,
+                                HpkeContext **previousEchHpkeCtx,
+                                PRBool recoverHashState);
 #endif
--- a/security/nss/lib/util/nssutil.h
+++ b/security/nss/lib/util/nssutil.h
@@ -14,22 +14,22 @@
 
 /*
  * NSS utilities's major version, minor version, patch level, build number,
  * and whether this is a beta release.
  *
  * The format of the version string should be
  *     "<major version>.<minor version>[.<patch level>[.<build number>]][ <Beta>]"
  */
-#define NSSUTIL_VERSION "3.61"
+#define NSSUTIL_VERSION "3.62 Beta"
 #define NSSUTIL_VMAJOR 3
-#define NSSUTIL_VMINOR 61
+#define NSSUTIL_VMINOR 62
 #define NSSUTIL_VPATCH 0
 #define NSSUTIL_VBUILD 0
-#define NSSUTIL_BETA PR_FALSE
+#define NSSUTIL_BETA PR_TRUE
 
 SEC_BEGIN_PROTOS
 
 /*
  * Returns a const string of the UTIL library version.
  */
 extern const char *NSSUTIL_GetVersion(void);
 
--- a/security/nss/tests/chains/scenarios/nameconstraints.cfg
+++ b/security/nss/tests/chains/scenarios/nameconstraints.cfg
@@ -154,17 +154,25 @@ verify NameConstraints.server17:x
 # Subject: "C = US, ST=CA, O=Foo CN=foo.example.com"
 verify NameConstraints.dcissblocked:x
   result fail
 
 # Subject: "C = US, ST=CA, O=Foo CN=foo.example.fr"
 verify NameConstraints.dcissallowed:x
   result pass
 
-# Subject: "O = IPA.LOCAL 201901211552, CN = OCSP Subsystem"
+# Subject: "O = IPA.LOCAL 20200120, CN = OCSP and IPSEC"
+# EKUs: OCSPSigning,ipsecUser
 #
 # This tests that a non server certificate (i.e. id-kp-serverAuth
 # not present in EKU) does *NOT* have CN treated as dnsName for
-# purposes of Name Constraints validation
+# purposes of Name Constraints validation (certificateUsageStatusResponder)
+# https://hg.mozilla.org/projects/nss/rev/0b30eb1c3650
 verify NameConstraints.ocsp1:x
   usage 10
   result pass
 
+# This tests that a non server certificate (i.e. id-kp-serverAuth
+# not present in EKU) does *NOT* have CN treated as dnsName for
+# purposes of Name Constraints validation (certificateUsageIPsec)
+verify NameConstraints.ocsp1:x
+ usage 12
+ result pass
index 6c7d68c770062d12f3a90693142e0a08f0f1e8d5..4a451f3429d25ab6d3a9cb00b2f005118f7cac08
GIT binary patch
literal 1000
zc$_n6Vt!)K#B^o>GZP~d6DPy|^k+KryEhsc@Un4gwRyCC=VfH%W@Rw&GL$xuWMd9x
z;Sv_|3~<!*@ppFgQ7|$vGBhwWGBD&e-~>ss2{VNT8_F5TfH=&;qRy#BC7EfN$%!SY
z3XY{E8Tmz-C6xvW;=EvOMg|6kmc}Mg68uIWGmMNZp#oI3t%*?y*)xo+49rc8{0u;G
zE~X|%Muunk-)qu+wolJ?ih9>R{cU{u@tq7TGL;!q|2M`O`ln3W)iI~KxSsngW3iFc
zYUkqxCTxac8$bK7++WiARI2d(riXt5^PQ*gw<I%q&Wbu?c!s6^_Jp&+GXmXbq^zvV
z=7~QO)cV76<%`~sx04!~pDTQRpnD_l-}!oD&825ftv?zW9ToN=Vx^4FshxJmKfF+S
zqo!b`)^Fb%Kk54g1JTexlPj4r+fVl@Uk)}&E0b_r=IZpU`E0BA^hKBWISNjs<jim4
zp0HA;g8i?+hw9*zq<IDYY&E54z9rdMon0&R@h|J6^@&SZd3S$Qzp8$7z4}70be4~Y
zQm-e}{d}AKWTU+1v-zz|%!~|-iyKcFG#)qL0fwZkFbk^zGb7`F16dG{k420{#Ad?9
zmtC!HFC<Qs{54{AY^)6Y*J@A&l2&FZHYjYss$ikt5+7>DWD`>A1Syo213HpL$UtBL
zp8-!3mtJN;qFzpZa$*iz+GPgCa<4#o^@LqbyO!KynYSuxMQ6hEnB(0m?RRnqI!w%9
z%1{ej<~f^NQ>H4F|JuB10SA~&tXIa`FY^7}($Kfaq)dDHuP?85oVz&FP%np}Hqh*<
zS>lv*B}<j{mn5`r=Wl<Zd5pu^<Zbx{mh>fen09H#Y3-_ea#fS}&~M)NyH|bsUo?4{
zig<nAx*}O=&+YT4DTVy_ob1|I@ZEUz%Xghdo0rVyK9VI;KKW2ZXkOWrU-P3>H|{um
zO*ymVa>U}!2(L>bqFtx{+<qZ4<y`vKLzXj(E(e}U7S-EkI7|PNOz*bF$+i}Mf{h!r
j6?Uu@{y1BN@53%7e}M(_q!+uVPP*$ADfU_;!LbGa(z9}P
index ce7325fca9c56337d866b05cfbd42baa97944685..817faafe3d2b5cd197a5c1dbeaf1db48ff37e689
GIT binary patch
literal 956
zc$_n6V%}lU#I#@mGZP~d6C<AiFB_*;n@8JsUPeZ4Rt5tvLums^Hs(+kE@2_h07pF^
ze`iM@1tS9^Ljyx214C{DPLL#<FjHu-p`3vXh{G%_>YQ3sl9`s7oLG{o;8<FckzbTq
zQfZ(d&I{HC)NN#FY!M~FZv<omjWLA^7&xOEh}UXy15t==eE!bC0SbwEDGHte!LH5*
zO^iy&zF=f!U~XdMX8?+GF*PwVGMt(r$feo7sNHMfUa68FYLE7xWN`Ix*r~W-f@kna
zmBn3iB}xO+@7}3DrN2b;<>&hMvSqblTGrYVLbWEo7R%(>5qad7w~<5UoGC{oziYg8
z{Ln1#um7_Ckf6=d14m*-LVw8f?%^%VOYA?8$2{rFmo?jYTTaJFo_zLjWn<j67#XJO
zB9olE*9|t=e`I(%Z}+ozlV&Un`Dc6iOK-5QpX#G|%cbvL-r}`&E5Dw4wJe+MZ?^#9
z4R11v42;bunK3GM9@Y5raCP-`8OGa3H2ONv`E2;}KJ&||qH7M-H&S=Dz3Az&{4pWz
zdQtH9TfS#r_lq;XuGkmBo7u~KuXf)jiCObjPMQ0diJ6gsadG2XgT__hz?KzeVKQJa
z-~%!F85#exurM>R9WYP;@l`>5F#{1c4sA9@R#tXqPB@F*Ko+Eek420{M0JWnc1~{8
zG!a?xXhSjC4N^IYI}EI0N*Nhh^bK?kv|)S$#y0tkl9B=|ef`XWMBU^>Js_o*lb@WJ
zqo16ppPyV@V8GAD2~*6<!URl+sOgd!6p=gj?wBdDU{{3sJI4*vmmNK}FPyh)QK@$8
z1Ad)`@(XVs2+jC*C$C6XqjP7=`8PMF@$qZMH*%fi6u-o^X^qS-=kWIIiq#2U45XKF
zvYedZd`$52o9;Xv`O~Kko@+5&lCdD^WcuXh|7NVGk3VW@U%c*d_dL-J+A75->~$V3
zJ5%X+?UvWY!@JlX9*hl{De*#b`q#~Id#tB9Y}k6*=HJF_uksXU$0e(byOwWxxLh@0
zI{(_wuDaZM;<CTo8tkLLzrJ()35&hc|Eo_uPtDuusjPN>-+#TM@)fTw-dILgTI)s|
psb(dWh29UC)snpDpk&AN$)_J|Z<iB^o)u8^@X6wGk!~UP006YvRQ3P>