diff --git a/UnitTest1/unittest1.cpp b/UnitTest1/unittest1.cpp
index 3bc169ac33fa17c69c6897f583b026dcf515bd56..336afbfda5090cfd10a069885e09dede2c49f3bd 100644
--- a/UnitTest1/unittest1.cpp
+++ b/UnitTest1/unittest1.cpp
@@ -686,6 +686,13 @@ namespace UnitTest1
 
             Assert::AreEqual(ret, 0);
         }
+
+        TEST_METHOD(new_rotated_key)
+        {
+            int ret = new_rotated_key_test();
+
+            Assert::AreEqual(ret, 0);
+        }
         
         TEST_METHOD(stress)
         {
diff --git a/picoquic/picoquic_internal.h b/picoquic/picoquic_internal.h
index 40b73237b3fef2b423102628a3f12c5d7e31a229..d096d56650a041df7bad1765ccc6541cf41e5e2d 100644
--- a/picoquic/picoquic_internal.h
+++ b/picoquic/picoquic_internal.h
@@ -582,6 +582,8 @@ typedef struct st_picoquic_cnx_t {
 
     picoquic_stream_head tls_stream[PICOQUIC_NUMBER_OF_EPOCHS]; /* Separate input/output from each epoch */
     picoquic_crypto_context_t crypto_context[PICOQUIC_NUMBER_OF_EPOCHS]; /* Encryption and decryption objects */
+    picoquic_crypto_context_t crypto_context_old; /* Old encryption and decryption context after key rotation */
+    picoquic_crypto_context_t crypto_context_new; /* New encryption and decryption context just before key rotation */
 
     /* Liveness detection */
     uint64_t latest_progress_time; /* last local time at which the connection progressed */
diff --git a/picoquic/quicctx.c b/picoquic/quicctx.c
index 1f317e8fdecc277d5af99b2d19b4f9cf7c923912..e2b2dd9c95c371e84e53ef4e51dd70640e71358e 100644
--- a/picoquic/quicctx.c
+++ b/picoquic/quicctx.c
@@ -1724,6 +1724,8 @@ int picoquic_reset_cnx(picoquic_cnx_t* cnx, uint64_t current_time)
         picoquic_crypto_context_free(&cnx->crypto_context[k]);
     }
 
+    picoquic_crypto_context_free(&cnx->crypto_context_new);
+
     if (ret == 0) {
         ret = picoquic_setup_initial_traffic_keys(cnx);
     }
@@ -1840,6 +1842,8 @@ void picoquic_delete_cnx(picoquic_cnx_t* cnx)
             picoquic_crypto_context_free(&cnx->crypto_context[i]);
         }
 
+        picoquic_crypto_context_free(&cnx->crypto_context_new);
+
         for (picoquic_packet_context_enum pc = 0;
             pc < picoquic_nb_packet_context; pc++) {
             picoquic_reset_packet_context(cnx, pc);
diff --git a/picoquic/tls_api.c b/picoquic/tls_api.c
index 114fd79167587b1ed80ef7f42718a0937ffac372..f2c29a2f825a000f0a226680833c12dd9d9ad190 100644
--- a/picoquic/tls_api.c
+++ b/picoquic/tls_api.c
@@ -56,6 +56,8 @@ typedef struct st_picoquic_tls_ctx_t {
     uint8_t ext_received[128];
     size_t ext_received_length;
     int ext_received_return;
+    uint8_t app_secret_enc[PTLS_MAX_SECRET_SIZE];
+    uint8_t app_secret_dec[PTLS_MAX_SECRET_SIZE];
 } picoquic_tls_ctx_t;
 
 int picoquic_receive_transport_extensions(picoquic_cnx_t* cnx, int extension_mode,
@@ -110,7 +112,7 @@ void picoquic_provide_received_transport_extensions(picoquic_cnx_t* cnx,
     int* ext_received_return,
     int* client_mode)
 {
-    picoquic_tls_ctx_t* ctx = cnx->tls_ctx;
+    picoquic_tls_ctx_t* ctx = (picoquic_tls_ctx_t*)cnx->tls_ctx;
 
     *ext_received = ctx->ext_received;
     *ext_received_length = ctx->ext_received_length;
@@ -634,10 +636,9 @@ static int picoquic_set_pn_enc_from_secret(void ** v_pn_enc, ptls_cipher_suite_t
 }
 
 
-static int picoquic_set_key_from_secret(picoquic_cnx_t* cnx, ptls_cipher_suite_t * cipher, int is_enc, size_t epoch, const void *secret)
+static int picoquic_set_key_from_secret(picoquic_cnx_t* cnx, ptls_cipher_suite_t * cipher, int is_enc, picoquic_crypto_context_t * ctx, const void *secret)
 {
     int ret = 0;
-    picoquic_crypto_context_t * ctx = &cnx->crypto_context[epoch];
 
     if (is_enc != 0) {
         ret = picoquic_set_aead_from_secret(&ctx->aead_encrypt, cipher, is_enc, secret);
@@ -686,6 +687,7 @@ typedef struct st_picoquic_update_traffic_key_t {
 static int picoquic_update_traffic_key_callback(ptls_update_traffic_key_t * self, ptls_t *tls, int is_enc, size_t epoch, const void *secret)
 {
     picoquic_cnx_t* cnx = (picoquic_cnx_t*)*ptls_get_data_ptr(tls);
+    picoquic_tls_ctx_t * tls_ctx = (picoquic_tls_ctx_t *)cnx->tls_ctx;
     ptls_context_t* ctx = (ptls_context_t*)cnx->quic->tls_master_ctx;
     ptls_cipher_suite_t * cipher = ptls_get_cipher(tls);
     UNREFERENCED_PARAMETER(self);
@@ -694,7 +696,11 @@ static int picoquic_update_traffic_key_callback(ptls_update_traffic_key_t * self
     debug_dump(secret, (int)cipher->hash->digest_size);
 #endif
 
-    int ret = picoquic_set_key_from_secret(cnx, cipher, is_enc, epoch, secret);
+    int ret = picoquic_set_key_from_secret(cnx, cipher, is_enc, &cnx->crypto_context[epoch], secret);
+
+    if (ret == 0 && epoch == 3) {
+        memcpy((is_enc) ? tls_ctx->app_secret_enc : tls_ctx->app_secret_dec, secret, cipher->aead->key_size);
+    }
 
     if (ctx->log_secret) {
         static const char *log_labels[2][4] = {
@@ -798,17 +804,99 @@ int picoquic_setup_initial_traffic_keys(picoquic_cnx_t* cnx)
         }
 
         if (ret == 0) {
-            ret = picoquic_set_key_from_secret(cnx, &cipher, 1, 0, secret1);
+            ret = picoquic_set_key_from_secret(cnx, &cipher, 1, &cnx->crypto_context[0], secret1);
         }
 
         if (ret == 0) {
-            ret = picoquic_set_key_from_secret(cnx, &cipher, 0, 0, secret2);
+            ret = picoquic_set_key_from_secret(cnx, &cipher, 0, &cnx->crypto_context[0], secret2);
         }
     }
 
     return ret;
 }
 
+/*
+ * Key rotation.
+ *
+ * The old keys get moved to the old crypto context.
+ * The secrets are rotated.
+ * The new context gets informed.
+ *
+ * The key update is defined in RFC 8446 section 7.2 as:
+ * application_traffic_secret_N+1 =
+ *         HKDF-Expand-Label(application_traffic_secret_N,
+ *                            "traffic upd", "", Hash.length)
+  * Label: PICOQUIC_LABEL_TRAFFIC_UPDATE
+ */
+static int picoquic_rotate_app_secret(ptls_cipher_suite_t * cipher, uint8_t * secret)
+{
+    int ret = 0;
+    uint8_t new_secret[PTLS_MAX_SECRET_SIZE];
+
+    ret = ptls_hkdf_expand_label(cipher->hash, new_secret,
+        cipher->aead->ctr_cipher->key_size, ptls_iovec_init(secret, cipher->aead->ctr_cipher->key_size),
+        PICOQUIC_LABEL_TRAFFIC_UPDATE, ptls_iovec_init(NULL, 0), PICOQUIC_LABEL_QUIC_BASE);
+    if (ret == 0) {
+        memcpy(secret, new_secret, cipher->aead->ctr_cipher->key_size);
+    }
+
+    return ret;
+}
+
+
+uint8_t * picoquic_get_app_secret(picoquic_cnx_t* cnx, int is_enc)
+{
+    picoquic_tls_ctx_t * tls_ctx = (picoquic_tls_ctx_t *)cnx->tls_ctx;
+
+    return (is_enc) ?tls_ctx->app_secret_enc:tls_ctx->app_secret_dec;
+}
+
+size_t picoquic_get_app_secret_size(picoquic_cnx_t* cnx)
+{
+    picoquic_tls_ctx_t * tls_ctx = (picoquic_tls_ctx_t *)cnx->tls_ctx;
+
+    ptls_cipher_suite_t * cipher = ptls_get_cipher(tls_ctx->tls);
+
+    return (cipher->aead->ctr_cipher->key_size);
+}
+
+int picoquic_compute_new_rotated_keys(picoquic_cnx_t * cnx)
+{
+    int ret = 0;
+    picoquic_tls_ctx_t * tls_ctx = (picoquic_tls_ctx_t *)cnx->tls_ctx;
+    ptls_cipher_suite_t * cipher = ptls_get_cipher(tls_ctx->tls);
+
+    /* Verify that the previous transition is complete */
+    if (cnx->crypto_context_new.aead_decrypt != NULL ||
+        cnx->crypto_context_new.aead_encrypt != NULL ||
+        cnx->crypto_context_new.pn_dec != NULL ||
+        cnx->crypto_context_new.pn_enc != NULL) {
+        ret = -1;
+    }
+
+    /* Recompute the secrets */
+    if (ret == 0) {
+        ret = picoquic_rotate_app_secret(cipher, tls_ctx->app_secret_enc);
+    }
+
+    if (ret == 0) {
+        ret = picoquic_set_key_from_secret(cnx, cipher, 1, &cnx->crypto_context_new, tls_ctx->app_secret_enc);
+    }
+
+    if (ret == 0) {
+        ret = picoquic_rotate_app_secret(cipher, tls_ctx->app_secret_dec);
+    }
+    if (ret == 0) {
+        ret = picoquic_set_key_from_secret(cnx, cipher, 0, &cnx->crypto_context_new, tls_ctx->app_secret_dec);
+    }
+
+    return 0;
+}
+
+/*
+ * Release the crypto context, and the associated keys.
+ */
+
 void picoquic_crypto_context_free(picoquic_crypto_context_t * ctx)
 {
     if (ctx->aead_encrypt != NULL) {
diff --git a/picoquic/tls_api.h b/picoquic/tls_api.h
index 665d171bead6b5d69c66bf07e596b292042231e1..7a01f6e42dd2b969c5ef541c7991626bafe1a3cc 100644
--- a/picoquic/tls_api.h
+++ b/picoquic/tls_api.h
@@ -29,6 +29,8 @@
 #define PICOQUIC_LABEL_INITIAL_CLIENT "client in"
 #define PICOQUIC_LABEL_INITIAL_SERVER "server in"
 
+#define PICOQUIC_LABEL_TRAFFIC_UPDATE "traffic upd"
+
 #define PICOQUIC_LABEL_KEY "key"
 #define PICOQUIC_LABEL_IV "iv"
 #define PICOQUIC_LABEL_PN "pn"
@@ -93,6 +95,10 @@ int picoquic_setup_initial_secrets(
 
 int picoquic_setup_initial_traffic_keys(picoquic_cnx_t* cnx);
 
+uint8_t * picoquic_get_app_secret(picoquic_cnx_t* cnx, int is_enc);
+size_t picoquic_get_app_secret_size(picoquic_cnx_t* cnx);
+int picoquic_compute_new_rotated_keys(picoquic_cnx_t * cnx);
+
 void picoquic_crypto_context_free(picoquic_crypto_context_t * ctx);
 
 void * picoquic_setup_test_aead_context(int is_encrypt, const uint8_t * secret);
@@ -123,4 +129,5 @@ int picoquic_tls_client_authentication_activated(picoquic_quic_t* quic);
 int picoquic_get_retry_token(picoquic_quic_t* quic, uint8_t * base, size_t len, uint8_t * cid, uint8_t cid_len,
     uint8_t * token, uint8_t token_length);
 
+
 #endif /* TLS_API_H */
diff --git a/picoquic_t/picoquic_t.c b/picoquic_t/picoquic_t.c
index 5330e7f8504fc438fd7664d39293dff3c5febb8f..2d97918a007e8bfd1a7960e722443b9c40d1c764 100644
--- a/picoquic_t/picoquic_t.c
+++ b/picoquic_t/picoquic_t.c
@@ -138,6 +138,7 @@ static const picoquic_test_def_t test_table[] = {
     { "retire_cnxid", retire_cnxid_test },
     { "server_busy", server_busy_test },
     { "initial_close", initial_close_test },
+    { "new_rotated_key", new_rotated_key_test},
     { "stress", stress_test },
     { "fuzz", fuzz_test },
     { "fuzz_initial", fuzz_initial_test}
diff --git a/picoquictest/picoquictest.h b/picoquictest/picoquictest.h
index 0a64395d7273fa687047d336223d30517c35775e..c3cfd67a067b034c0bbca9275799990c1ede49cb 100644
--- a/picoquictest/picoquictest.h
+++ b/picoquictest/picoquictest.h
@@ -136,6 +136,7 @@ int retire_cnxid_test();
 int server_busy_test();
 int initial_close_test();
 int fuzz_initial_test();
+int new_rotated_key_test();
 
 #ifdef __cplusplus
 }
diff --git a/picoquictest/tls_api_test.c b/picoquictest/tls_api_test.c
index 280b16d777e9181b12b30e4653fb24ba8824cf62..0972f41d0335fc2c12284eb610d23f96617483cf 100644
--- a/picoquictest/tls_api_test.c
+++ b/picoquictest/tls_api_test.c
@@ -4287,5 +4287,75 @@ int initial_close_test()
         test_ctx = NULL;
     }
 
+    return ret;
+}
+
+/*
+ * Test that rotated keys are computed in a compatible way on client and server.
+ */
+
+int new_rotated_key_test()
+{
+    uint64_t loss_mask = 0;
+    uint64_t simulated_time = 0;
+    picoquic_test_tls_api_ctx_t* test_ctx = NULL;
+    int ret = tls_api_init_ctx(&test_ctx, 0, PICOQUIC_TEST_SNI, PICOQUIC_TEST_ALPN, &simulated_time, NULL, 0, 0, 0);
+
+    if (ret == 0) {
+        ret = tls_api_connection_loop(test_ctx, &loss_mask, 0, &simulated_time);
+    }
+
+    if (ret == 0) {
+        ret = wait_application_pn_enc_ready(test_ctx, &simulated_time);
+    }
+
+
+    for (int i = 1; ret == 0 && i <= 3; i++) {
+        if (ret == 0)
+        {
+            /* Try to compute rotated keys on server */
+            ret = picoquic_compute_new_rotated_keys(test_ctx->cnx_server);
+            if (ret != 0) {
+                DBG_PRINTF("Could not rotate server key, ret: %x\n", ret);
+            }
+        }
+
+        if (ret == 0)
+        {
+            /* Try to compute rotated keys on client */
+            ret = picoquic_compute_new_rotated_keys(test_ctx->cnx_client);
+            if (ret != 0) {
+                DBG_PRINTF("Could not rotate server key, round %d, ret: %x\n", i, ret);
+            }
+        }
+
+        if (ret == 0)
+        {
+            /* Compare server encryption and client decryption */
+            size_t key_size = picoquic_get_app_secret_size(test_ctx->cnx_client);
+
+            if (key_size != picoquic_get_app_secret_size(test_ctx->cnx_server)) {
+                DBG_PRINTF("Round %d. Key sizes dont match, client: %d, server: %d\n", i, key_size, picoquic_get_app_secret_size(test_ctx->cnx_server));
+                ret = -1;
+            }
+            else if (memcmp(picoquic_get_app_secret(test_ctx->cnx_server, 1), picoquic_get_app_secret(test_ctx->cnx_client, 0), key_size) != 0) {
+                DBG_PRINTF("Round %d. Server encryption secret does not match client decryption secret\n", i);
+                ret = -1;
+            }
+            else if (memcmp(picoquic_get_app_secret(test_ctx->cnx_server, 0), picoquic_get_app_secret(test_ctx->cnx_client, 1), key_size) != 0) {
+                DBG_PRINTF("Round %d. Server decryption secret does not match client encryption secret\n", i);
+                ret = -1;
+            }
+        }
+
+        picoquic_crypto_context_free(&test_ctx->cnx_server->crypto_context_new);
+        picoquic_crypto_context_free(&test_ctx->cnx_client->crypto_context_new);
+    }
+
+    if (test_ctx != NULL) {
+        tls_api_delete_ctx(test_ctx);
+        test_ctx = NULL;
+    }
+
     return ret;
 }
\ No newline at end of file