Merge pull request #6283 from mpg/driver-only-hashes-wrap-up

Driver only hashes wrap-up
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index cea12f1..319f02d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,8 +19,6 @@
 1. Write a test which shows that the bug was fixed or that the feature works as expected.
 1. Send a pull request (PR) and work with us until it gets merged and published. Contributions may need some modifications, so a few rounds of review and fixing may be necessary. We will include your name in the ChangeLog :)
 1. For quick merging, the contribution should be short, and concentrated on a single feature or topic. The larger the contribution is, the longer it would take to review it and merge it.
-1. All new files should include the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) standard license header where possible.
-1. Ensure that each commit has at least one `Signed-off-by:` line from the committer. If anyone else contributes to the commit, they should also add their own `Signed-off-by:` line. By adding this line, contributor(s) certify that the contribution is made under the terms of the [Developer Certificate of Origin](dco.txt). The contribution licensing is described in the [License section of the README](README.md#License).
 
 Backwards Compatibility
 -----------------------
@@ -79,3 +77,12 @@
 1. If needed, a Readme file is advised.
 1. If a [Knowledge Base (KB)](https://tls.mbed.org/kb) article should be added, write this as a comment in the PR description.
 1. A [ChangeLog](https://github.com/Mbed-TLS/mbedtls/blob/development/ChangeLog.d/00README.md) entry should be added for this contribution.
+
+License and Copyright
+---------------------
+
+All new files should include the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) standard license header where possible. For licensing details, please see the [License section of the README](README.md#License).
+
+The copyright on contributions is retained by the original authors of the code. Where possible for new files, this should be noted in a comment at the top of the file in the form: "Copyright The Mbed TLS Contributors".
+
+When contributing code to us, the committer and all authors are required to make the submission under the terms of the [Developer Certificate of Origin](dco.txt), confirming that the code submitted can (legally) become part of the project, and be subject to the same Apache 2.0 license. This is done by including the standard Git `Signed-off-by:` line in every commit message. If more than one person contributed to the commit, they should also add their own `Signed-off-by:` line.
diff --git a/include/mbedtls/mbedtls_config.h b/include/mbedtls/mbedtls_config.h
index f6ecdbf..8359a9f 100644
--- a/include/mbedtls/mbedtls_config.h
+++ b/include/mbedtls/mbedtls_config.h
@@ -1551,6 +1551,26 @@
 //#define MBEDTLS_SSL_TLS1_3_COMPATIBILITY_MODE
 
 /**
+ * \def MBEDTLS_SSL_TLS1_3_TICKET_AGE_TOLERANCE
+ *
+ * Maximum time difference in milliseconds tolerated between the age of a
+ * ticket from the server and client point of view.
+ * From the client point of view, the age of a ticket is the time difference
+ * between the time when the client proposes to the server to use the ticket
+ * (time of writing of the Pre-Shared Key Extension including the ticket) and
+ * the time the client received the ticket from the server.
+ * From the server point of view, the age of a ticket is the time difference
+ * between the time when the server receives a proposition from the client
+ * to use the ticket and the time when the ticket was created by the server.
+ * The server age is expected to be always greater than the client one and
+ * MBEDTLS_SSL_TLS1_3_TICKET_AGE_TOLERANCE defines the
+ * maximum difference tolerated for the server to accept the ticket.
+ * This is not used in TLS 1.2.
+ *
+ */
+#define MBEDTLS_SSL_TLS1_3_TICKET_AGE_TOLERANCE 6000
+
+/**
  * \def MBEDTLS_SSL_TLS1_3_TICKET_NONCE_LENGTH
  *
  * Size in bytes of a ticket nonce. This is not used in TLS 1.2.
diff --git a/library/ssl_misc.h b/library/ssl_misc.h
index edbf446..2e35e6c 100644
--- a/library/ssl_misc.h
+++ b/library/ssl_misc.h
@@ -2457,16 +2457,6 @@
 #endif
 
 #if defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED)
-/* Check if we have any PSK to offer, returns 0 if PSK is available.
- * Assign the psk and ticket if pointers are present.
- */
-MBEDTLS_CHECK_RETURN_CRITICAL
-int mbedtls_ssl_get_psk_to_offer(
-        const mbedtls_ssl_context *ssl,
-        int *psk_type,
-        const unsigned char **psk, size_t *psk_len,
-        const unsigned char **psk_identity, size_t *psk_identity_len );
-
 /**
  * \brief Given an SSL context and its associated configuration, write the TLS
  *        1.3 specific Pre-Shared key extension.
diff --git a/library/ssl_tls13_client.c b/library/ssl_tls13_client.c
index 505f8dd..33e8cc6 100644
--- a/library/ssl_tls13_client.c
+++ b/library/ssl_tls13_client.c
@@ -664,10 +664,59 @@
     ssl->handshake->extensions_present |= MBEDTLS_SSL_EXT_PSK_KEY_EXCHANGE_MODES;
     return ( 0 );
 }
-#endif /* MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED */
+
+/* Check if we have any PSK to offer, returns 0 if a PSK is available. */
+MBEDTLS_CHECK_RETURN_CRITICAL
+static int ssl_tls13_get_psk_to_offer(
+        const mbedtls_ssl_context *ssl,
+        int *psk_type,
+        const unsigned char **psk, size_t *psk_len,
+        const unsigned char **psk_identity, size_t *psk_identity_len )
+{
+    *psk = NULL;
+    *psk_len = 0;
+    *psk_identity = NULL;
+    *psk_identity_len = 0;
+    *psk_type = MBEDTLS_SSL_TLS1_3_PSK_EXTERNAL;
+
+#if defined(MBEDTLS_SSL_SESSION_TICKETS)
+    /* Check if a ticket has been configured. */
+    if( ssl->session_negotiate != NULL &&
+        ssl->session_negotiate->ticket != NULL )
+    {
+#if defined(MBEDTLS_HAVE_TIME)
+        mbedtls_time_t now = mbedtls_time( NULL );
+        if( ssl->session_negotiate->ticket_received <= now &&
+            (uint64_t)( now - ssl->session_negotiate->ticket_received )
+                    <= ssl->session_negotiate->ticket_lifetime )
+        {
+            *psk_type = MBEDTLS_SSL_TLS1_3_PSK_RESUMPTION;
+            *psk = ssl->session_negotiate->resumption_key;
+            *psk_len = ssl->session_negotiate->resumption_key_len;
+            *psk_identity = ssl->session_negotiate->ticket;
+            *psk_identity_len = ssl->session_negotiate->ticket_len;
+            return( 0 );
+        }
+#endif /* MBEDTLS_HAVE_TIME */
+        MBEDTLS_SSL_DEBUG_MSG( 3, ( "ticket expired" ) );
+    }
+#endif
+
+    /* Check if an external PSK has been configured. */
+    if( ssl->conf->psk != NULL )
+    {
+        *psk = ssl->conf->psk;
+        *psk_len = ssl->conf->psk_len;
+        *psk_identity = ssl->conf->psk_identity;
+        *psk_identity_len = ssl->conf->psk_identity_len;
+        return( 0 );
+    }
+
+    return( MBEDTLS_ERR_ERROR_GENERIC_ERROR );
+}
 
 /*
- * mbedtls_ssl_tls13_write_pre_shared_key_ext() structure:
+ * mbedtls_ssl_tls13_write_identities_of_pre_shared_key_ext() structure:
  *
  * struct {
  *   opaque identity<1..2^16-1>;
@@ -689,9 +738,6 @@
  * } PreSharedKeyExtension;
  *
  */
-
-#if defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED)
-
 int mbedtls_ssl_tls13_write_identities_of_pre_shared_key_ext(
     mbedtls_ssl_context *ssl,
     unsigned char *buf, unsigned char *end,
@@ -725,9 +771,8 @@
      *   configured, offer that.
      * - Otherwise, skip the PSK extension.
      */
-
-    if( mbedtls_ssl_get_psk_to_offer( ssl, &psk_type, &psk, &psk_len,
-                                      &psk_identity, &psk_identity_len ) != 0 )
+    if( ssl_tls13_get_psk_to_offer( ssl, &psk_type, &psk, &psk_len,
+                                    &psk_identity, &psk_identity_len ) != 0 )
     {
         MBEDTLS_SSL_DEBUG_MSG( 3, ( "skip pre_shared_key extensions" ) );
         return( 0 );
@@ -757,6 +802,26 @@
             break;
         }
     }
+    else
+#if defined(MBEDTLS_SSL_SESSION_TICKETS)
+    if( psk_type == MBEDTLS_SSL_TLS1_3_PSK_RESUMPTION )
+    {
+#if defined(MBEDTLS_HAVE_TIME)
+        mbedtls_time_t now = mbedtls_time( NULL );
+
+        obfuscated_ticket_age =
+            ( (uint32_t)( now - ssl->session_negotiate->ticket_received ) * 1000 )
+              + ssl->session_negotiate->ticket_age_add;
+#endif
+    }
+    else
+#endif /* MBEDTLS_SSL_SESSION_TICKETS */
+    {
+        MBEDTLS_SSL_DEBUG_MSG( 1, ( "write_identities_of_pre_shared_key_ext: "
+                                    "should never happen" ) );
+        return( MBEDTLS_ERR_SSL_INTERNAL_ERROR );
+    }
+
 
     ciphersuite_info = mbedtls_ssl_ciphersuite_from_id(
             ssl->session_negotiate->ciphersuite );
@@ -831,8 +896,8 @@
     unsigned char transcript[MBEDTLS_MD_MAX_SIZE];
     size_t transcript_len;
 
-    if( mbedtls_ssl_get_psk_to_offer( ssl, &psk_type, &psk, &psk_len,
-                                      &psk_identity, &psk_identity_len ) != 0 )
+    if( ssl_tls13_get_psk_to_offer( ssl, &psk_type, &psk, &psk_len,
+                                    &psk_identity, &psk_identity_len ) != 0 )
     {
         return( MBEDTLS_ERR_SSL_INTERNAL_ERROR );
     }
@@ -1266,15 +1331,15 @@
     size_t psk_len;
     const unsigned char *psk_identity;
     size_t psk_identity_len;
-
+    int psk_type;
 
     /* Check which PSK we've offered.
      *
      * NOTE: Ultimately, we want to offer multiple PSKs, and in this
      *       case, we need to iterate over them here.
      */
-    if( mbedtls_ssl_get_psk_to_offer( ssl, NULL, &psk, &psk_len,
-                                      &psk_identity, &psk_identity_len ) != 0 )
+    if( ssl_tls13_get_psk_to_offer( ssl, &psk_type, &psk, &psk_len,
+                                    &psk_identity, &psk_identity_len ) != 0 )
     {
         /* If we haven't offered a PSK, the server must not send
          * a PSK identity extension. */
@@ -1622,16 +1687,19 @@
         /* Only the pre_shared_key extension was received */
         case MBEDTLS_SSL_EXT_PRE_SHARED_KEY:
             handshake->key_exchange_mode = MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK;
+            MBEDTLS_SSL_DEBUG_MSG( 2, ( "key exchange mode: psk" ) );
             break;
 
         /* Only the key_share extension was received */
         case MBEDTLS_SSL_EXT_KEY_SHARE:
             handshake->key_exchange_mode = MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_EPHEMERAL;
+            MBEDTLS_SSL_DEBUG_MSG( 2, ( "key exchange mode: ephemeral" ) );
             break;
 
         /* Both the pre_shared_key and key_share extensions were received */
         case ( MBEDTLS_SSL_EXT_PRE_SHARED_KEY | MBEDTLS_SSL_EXT_KEY_SHARE ):
             handshake->key_exchange_mode = MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_EPHEMERAL;
+            MBEDTLS_SSL_DEBUG_MSG( 2, ( "key exchange mode: psk_ephemeral" ) );
             break;
 
         /* Neither pre_shared_key nor key_share extension was received */
@@ -1819,7 +1887,12 @@
          */
         switch( extension_type )
         {
+            case MBEDTLS_TLS_EXT_SERVERNAME:
+                MBEDTLS_SSL_DEBUG_MSG( 3, ( "found server_name extension" ) );
 
+                /* The server_name extension should be an empty extension */
+
+                break;
             case MBEDTLS_TLS_EXT_SUPPORTED_GROUPS:
                 MBEDTLS_SSL_DEBUG_MSG( 3, ( "found extensions supported groups" ) );
                 break;
@@ -2237,11 +2310,11 @@
     if( ret != 0 )
         return( ret );
 
-    ret = mbedtls_ssl_tls13_generate_resumption_master_secret( ssl );
+    ret = mbedtls_ssl_tls13_compute_resumption_master_secret( ssl );
     if( ret != 0 )
     {
         MBEDTLS_SSL_DEBUG_RET( 1,
-                "mbedtls_ssl_tls13_generate_resumption_master_secret ", ret );
+                "mbedtls_ssl_tls13_compute_resumption_master_secret ", ret );
         return ( ret );
     }
 
@@ -2405,6 +2478,9 @@
         return( ret );
     }
 
+    /* session has been updated, allow export */
+    session->exported = 0;
+
     return( 0 );
 }
 
diff --git a/library/ssl_tls13_generic.c b/library/ssl_tls13_generic.c
index 6f60fab..f74cb40 100644
--- a/library/ssl_tls13_generic.c
+++ b/library/ssl_tls13_generic.c
@@ -1504,41 +1504,4 @@
 }
 #endif /* MBEDTLS_ECDH_C */
 
-#if defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED)
-/* Check if we have any PSK to offer, returns 0 if PSK is available.
- * Assign the psk and ticket if pointers are present.
- */
-int mbedtls_ssl_get_psk_to_offer(
-        const mbedtls_ssl_context *ssl,
-        int *psk_type,
-        const unsigned char **psk, size_t *psk_len,
-        const unsigned char **psk_identity, size_t *psk_identity_len )
-{
-    int ptrs_present = 0;
-
-    if( psk_type != NULL && psk != NULL && psk_len != NULL &&
-        psk_identity != NULL && psk_identity_len != NULL )
-    {
-        ptrs_present = 1;
-    }
-
-    /* Check if an external PSK has been configured. */
-    if( ssl->conf->psk != NULL )
-    {
-        if( ptrs_present )
-        {
-            *psk_type = MBEDTLS_SSL_TLS1_3_PSK_EXTERNAL;
-            *psk = ssl->conf->psk;
-            *psk_len = ssl->conf->psk_len;
-            *psk_identity = ssl->conf->psk_identity;
-            *psk_identity_len = ssl->conf->psk_identity_len;
-        }
-
-        return( 0 );
-    }
-
-    return( 1 );
-}
-#endif /* MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED */
-
 #endif /* MBEDTLS_SSL_TLS_C && MBEDTLS_SSL_PROTO_TLS1_3 */
diff --git a/library/ssl_tls13_keys.c b/library/ssl_tls13_keys.c
index 32a4f2a..48de3d0 100644
--- a/library/ssl_tls13_keys.c
+++ b/library/ssl_tls13_keys.c
@@ -1504,12 +1504,43 @@
     return( ret );
 }
 
-int mbedtls_ssl_tls13_generate_resumption_master_secret(
-    mbedtls_ssl_context *ssl )
+int mbedtls_ssl_tls13_compute_resumption_master_secret( mbedtls_ssl_context *ssl )
 {
+    int ret = MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED;
+    mbedtls_md_type_t md_type;
+    mbedtls_ssl_handshake_params *handshake = ssl->handshake;
+    unsigned char transcript[MBEDTLS_TLS1_3_MD_MAX_SIZE];
+    size_t transcript_len;
+
+    MBEDTLS_SSL_DEBUG_MSG( 2,
+        ( "=> mbedtls_ssl_tls13_compute_resumption_master_secret" ) );
+
+    md_type = handshake->ciphersuite_info->mac;
+
+    ret = mbedtls_ssl_get_handshake_transcript( ssl, md_type,
+                                                transcript, sizeof( transcript ),
+                                                &transcript_len );
+    if( ret != 0 )
+        return( ret );
+
+    ret = mbedtls_ssl_tls13_derive_resumption_master_secret(
+                              mbedtls_psa_translate_md( md_type ),
+                              handshake->tls13_master_secrets.app,
+                              transcript, transcript_len,
+                              &ssl->session_negotiate->app_secrets );
+    if( ret != 0 )
+        return( ret );
+
     /* Erase master secrets */
-    mbedtls_platform_zeroize( &ssl->handshake->tls13_master_secrets,
-                              sizeof( ssl->handshake->tls13_master_secrets ) );
+    mbedtls_platform_zeroize( &handshake->tls13_master_secrets,
+                              sizeof( handshake->tls13_master_secrets ) );
+
+    MBEDTLS_SSL_DEBUG_BUF( 4, "Resumption master secret",
+             ssl->session_negotiate->app_secrets.resumption_master_secret,
+             PSA_HASH_LENGTH( mbedtls_psa_translate_md( md_type ) ) ) ;
+
+    MBEDTLS_SSL_DEBUG_MSG( 2,
+        ( "<= mbedtls_ssl_tls13_compute_resumption_master_secret" ) );
     return( 0 );
 }
 
diff --git a/library/ssl_tls13_keys.h b/library/ssl_tls13_keys.h
index d82bf7a..f3bdf37 100644
--- a/library/ssl_tls13_keys.h
+++ b/library/ssl_tls13_keys.h
@@ -636,8 +636,7 @@
  * \returns    A negative error code on failure.
  */
 MBEDTLS_CHECK_RETURN_CRITICAL
-int mbedtls_ssl_tls13_generate_resumption_master_secret(
-    mbedtls_ssl_context *ssl );
+int mbedtls_ssl_tls13_compute_resumption_master_secret( mbedtls_ssl_context *ssl );
 
 /**
  * \brief Calculate the verify_data value for the client or server TLS 1.3
diff --git a/library/ssl_tls13_server.c b/library/ssl_tls13_server.c
index a10e59b..e185dc1 100644
--- a/library/ssl_tls13_server.c
+++ b/library/ssl_tls13_server.c
@@ -121,14 +121,170 @@
 
 #define SSL_TLS1_3_OFFERED_PSK_NOT_MATCH   1
 #define SSL_TLS1_3_OFFERED_PSK_MATCH       0
+
+#if defined(MBEDTLS_SSL_SESSION_TICKETS)
+
+MBEDTLS_CHECK_RETURN_CRITICAL
+static int ssl_tls13_offered_psks_check_identity_match_ticket(
+                                            mbedtls_ssl_context *ssl,
+                                            const unsigned char *identity,
+                                            size_t identity_len,
+                                            uint32_t obfuscated_ticket_age,
+                                            mbedtls_ssl_session *session )
+{
+    int ret = MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED;
+    unsigned char *ticket_buffer;
+#if defined(MBEDTLS_HAVE_TIME)
+    mbedtls_time_t now;
+    uint64_t age_in_s;
+    int64_t age_diff_in_ms;
+#endif
+
+    ((void) obfuscated_ticket_age);
+
+    MBEDTLS_SSL_DEBUG_MSG( 2, ( "=> check_identity_match_ticket" ) );
+
+    /* Ticket parser is not configured, Skip */
+    if( ssl->conf->f_ticket_parse == NULL || identity_len == 0 )
+        return( 0 );
+
+    /* We create a copy of the encrypted ticket since the ticket parsing
+     * function is allowed to use its input buffer as an output buffer
+     * (in-place decryption). We do, however, need the original buffer for
+     * computing the PSK binder value.
+     */
+    ticket_buffer = mbedtls_calloc( 1, identity_len );
+    if( ticket_buffer == NULL )
+    {
+        MBEDTLS_SSL_DEBUG_MSG( 1, ( "buffer too small" ) );
+        return ( MBEDTLS_ERR_SSL_ALLOC_FAILED );
+    }
+    memcpy( ticket_buffer, identity, identity_len );
+
+    if( ( ret = ssl->conf->f_ticket_parse( ssl->conf->p_ticket,
+                                           session,
+                                           ticket_buffer, identity_len ) ) != 0 )
+    {
+        if( ret == MBEDTLS_ERR_SSL_INVALID_MAC )
+            MBEDTLS_SSL_DEBUG_MSG( 3, ( "ticket is not authentic" ) );
+        else if( ret == MBEDTLS_ERR_SSL_SESSION_TICKET_EXPIRED )
+            MBEDTLS_SSL_DEBUG_MSG( 3, ( "ticket is expired" ) );
+        else
+            MBEDTLS_SSL_DEBUG_RET( 1, "ticket_parse", ret );
+    }
+
+    /* We delete the temporary buffer */
+    mbedtls_free( ticket_buffer );
+
+    if( ret != 0 )
+        goto exit;
+
+    ret = MBEDTLS_ERR_SSL_SESSION_TICKET_EXPIRED;
+#if defined(MBEDTLS_HAVE_TIME)
+    now = mbedtls_time( NULL );
+
+    if( now < session->start )
+    {
+        MBEDTLS_SSL_DEBUG_MSG(
+            3, ( "Ticket expired: now=%" MBEDTLS_PRINTF_LONGLONG
+                    ", start=%" MBEDTLS_PRINTF_LONGLONG,
+                    (long long)now, (long long)session->start ) );
+        goto exit;
+    }
+
+    age_in_s = (uint64_t)( now - session->start );
+
+    /* RFC 8446 section 4.6.1
+     *
+     * Servers MUST NOT use any value greater than 604800 seconds (7 days).
+     *
+     * RFC 8446 section 4.2.11.1
+     *
+     * Clients MUST NOT attempt to use tickets which have ages greater than
+     * the "ticket_lifetime" value which was provided with the ticket.
+     *
+     * For time being, the age MUST be less than 604800 seconds (7 days).
+     */
+    if( age_in_s > 604800 )
+    {
+        MBEDTLS_SSL_DEBUG_MSG(
+            3, ( "Ticket expired: Ticket age exceed limitation ticket_age=%lu",
+                 (long unsigned int)age_in_s ) );
+        goto exit;
+    }
+
+    /* RFC 8446 section 4.2.10
+     *
+     * For PSKs provisioned via NewSessionTicket, a server MUST validate that
+     * the ticket age for the selected PSK identity (computed by subtracting
+     * ticket_age_add from PskIdentity.obfuscated_ticket_age modulo 2^32) is
+     * within a small tolerance of the time since the ticket was issued.
+     *
+     * NOTE: When `now == session->start`, `age_diff_in_ms` may be negative
+     *       as the age units are different on the server (s) and in the
+     *       client (ms) side. Add a -1000 ms tolerance window to take this
+     *       into account.
+     */
+    age_diff_in_ms = age_in_s * 1000;
+    age_diff_in_ms -= ( obfuscated_ticket_age - session->ticket_age_add );
+    if( age_diff_in_ms <= -1000 ||
+        age_diff_in_ms > MBEDTLS_SSL_TLS1_3_TICKET_AGE_TOLERANCE )
+    {
+        MBEDTLS_SSL_DEBUG_MSG(
+            3, ( "Ticket expired: Ticket age outside tolerance window "
+                     "( diff=%d )", (int)age_diff_in_ms ) );
+        goto exit;
+    }
+
+    ret = 0;
+
+#endif /* MBEDTLS_HAVE_TIME */
+
+exit:
+    if( ret != 0 )
+        mbedtls_ssl_session_free( session );
+
+    MBEDTLS_SSL_DEBUG_MSG( 2, ( "<= check_identity_match_ticket" ) );
+    return( ret );
+}
+#endif /* MBEDTLS_SSL_SESSION_TICKETS */
+
 MBEDTLS_CHECK_RETURN_CRITICAL
 static int ssl_tls13_offered_psks_check_identity_match(
                mbedtls_ssl_context *ssl,
                const unsigned char *identity,
                size_t identity_len,
-               int *psk_type )
+               uint32_t obfuscated_ticket_age,
+               int *psk_type,
+               mbedtls_ssl_session *session )
 {
+    ((void) session);
+    ((void) obfuscated_ticket_age);
     *psk_type = MBEDTLS_SSL_TLS1_3_PSK_EXTERNAL;
+
+    MBEDTLS_SSL_DEBUG_BUF( 4, "identity", identity, identity_len );
+    ssl->handshake->resume = 0;
+
+#if defined(MBEDTLS_SSL_SESSION_TICKETS)
+    if( ssl_tls13_offered_psks_check_identity_match_ticket(
+            ssl, identity, identity_len, obfuscated_ticket_age,
+            session ) == SSL_TLS1_3_OFFERED_PSK_MATCH )
+    {
+        ssl->handshake->resume = 1;
+        *psk_type = MBEDTLS_SSL_TLS1_3_PSK_RESUMPTION;
+        mbedtls_ssl_set_hs_psk( ssl,
+                                session->resumption_key,
+                                session->resumption_key_len );
+
+        MBEDTLS_SSL_DEBUG_BUF( 4, "Ticket-resumed PSK:",
+                               session->resumption_key,
+                               session->resumption_key_len );
+        MBEDTLS_SSL_DEBUG_MSG( 4, ( "ticket: obfuscated_ticket_age: %u",
+                                    (unsigned)obfuscated_ticket_age ) );
+        return( SSL_TLS1_3_OFFERED_PSK_MATCH );
+    }
+#endif /* MBEDTLS_SSL_SESSION_TICKETS */
+
     /* Check identity with external configured function */
     if( ssl->conf->f_psk != NULL )
     {
@@ -256,6 +412,7 @@
     return( MBEDTLS_ERR_SSL_HANDSHAKE_FAILURE );
 }
 
+#if defined(MBEDTLS_SSL_SESSION_TICKETS)
 MBEDTLS_CHECK_RETURN_CRITICAL
 static int ssl_tls13_select_ciphersuite_for_resumption(
                mbedtls_ssl_context *ssl,
@@ -265,15 +422,46 @@
                uint16_t *selected_ciphersuite,
                const mbedtls_ssl_ciphersuite_t **selected_ciphersuite_info )
 {
-    ((void) ssl);
-    ((void) session);
-    ((void) cipher_suites);
-    ((void) cipher_suites_end);
+
     *selected_ciphersuite = 0;
     *selected_ciphersuite_info = NULL;
-    return( MBEDTLS_ERR_SSL_FEATURE_UNAVAILABLE );
+    for( const unsigned char *p = cipher_suites; p < cipher_suites_end; p += 2 )
+    {
+        uint16_t cipher_suite = MBEDTLS_GET_UINT16_BE( p, 0 );
+        const mbedtls_ssl_ciphersuite_t *ciphersuite_info;
+
+        if( cipher_suite != session->ciphersuite )
+            continue;
+
+        ciphersuite_info = ssl_tls13_validate_peer_ciphersuite( ssl,
+                                                                cipher_suite );
+        if( ciphersuite_info == NULL )
+            continue;
+
+        *selected_ciphersuite = cipher_suite;
+        *selected_ciphersuite_info = ciphersuite_info;
+
+        return( 0 );
+    }
+
+    return( MBEDTLS_ERR_SSL_HANDSHAKE_FAILURE );
 }
 
+MBEDTLS_CHECK_RETURN_CRITICAL
+static int ssl_tls13_session_copy_ticket( mbedtls_ssl_session *dst,
+                                          const mbedtls_ssl_session *src )
+{
+    dst->ticket_age_add = src->ticket_age_add;
+    dst->ticket_flags = src->ticket_flags;
+    dst->resumption_key_len = src->resumption_key_len;
+    if( src->resumption_key_len == 0 )
+        return( MBEDTLS_ERR_SSL_INTERNAL_ERROR );
+    memcpy( dst->resumption_key, src->resumption_key, src->resumption_key_len );
+
+    return( 0 );
+}
+#endif /* MBEDTLS_SSL_SESSION_TICKETS */
+
 /* Parser for pre_shared_key extension in client hello
  *    struct {
  *        opaque identity<1..2^16-1>;
@@ -343,17 +531,23 @@
     {
         const unsigned char *identity;
         size_t identity_len;
+        uint32_t obfuscated_ticket_age;
         const unsigned char *binder;
         size_t binder_len;
         int ret = MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED;
         int psk_type;
         uint16_t cipher_suite;
         const mbedtls_ssl_ciphersuite_t *ciphersuite_info;
+#if defined(MBEDTLS_SSL_SESSION_TICKETS)
+        mbedtls_ssl_session session;
+        mbedtls_ssl_session_init( &session );
+#endif
 
         MBEDTLS_SSL_CHK_BUF_READ_PTR( p_identity_len, identities_end, 2 + 1 + 4 );
         identity_len = MBEDTLS_GET_UINT16_BE( p_identity_len, 0 );
         identity = p_identity_len + 2;
         MBEDTLS_SSL_CHK_BUF_READ_PTR( identity, identities_end, identity_len + 4 );
+        obfuscated_ticket_age = MBEDTLS_GET_UINT32_BE( identity , identity_len );
         p_identity_len += identity_len + 6;
 
         MBEDTLS_SSL_CHK_BUF_READ_PTR( p_binder_len, binders_end, 1 + 32 );
@@ -367,7 +561,8 @@
             continue;
 
         ret = ssl_tls13_offered_psks_check_identity_match(
-                                    ssl, identity, identity_len, &psk_type );
+                  ssl, identity, identity_len, obfuscated_ticket_age,
+                  &psk_type, &session );
         if( ret != SSL_TLS1_3_OFFERED_PSK_MATCH )
             continue;
 
@@ -380,9 +575,15 @@
                             &cipher_suite, &ciphersuite_info );
                 break;
             case MBEDTLS_SSL_TLS1_3_PSK_RESUMPTION:
+#if defined(MBEDTLS_SSL_SESSION_TICKETS)
                 ret = ssl_tls13_select_ciphersuite_for_resumption(
-                            ssl, ciphersuites, ciphersuites_end, NULL,
+                            ssl, ciphersuites, ciphersuites_end, &session,
                             &cipher_suite, &ciphersuite_info );
+                if( ret != 0 )
+                    mbedtls_ssl_session_free( &session );
+#else
+                ret = MBEDTLS_ERR_SSL_FEATURE_UNAVAILABLE;
+#endif
                 break;
             default:
                 return( MBEDTLS_ERR_SSL_INTERNAL_ERROR );
@@ -406,6 +607,9 @@
             /* For security reasons, the handshake should be aborted when we
              * fail to validate a binder value. See RFC 8446 section 4.2.11.2
              * and appendix E.6. */
+#if defined(MBEDTLS_SSL_SESSION_TICKETS)
+            mbedtls_ssl_session_free( &session );
+#endif
             MBEDTLS_SSL_DEBUG_MSG( 3, ( "Invalid binder." ) );
             MBEDTLS_SSL_DEBUG_RET( 1,
                 "ssl_tls13_offered_psks_check_binder_match" , ret );
@@ -418,11 +622,20 @@
         matched_identity = identity_id;
 
         /* Update handshake parameters */
-        ssl->session_negotiate->ciphersuite = cipher_suite;
         ssl->handshake->ciphersuite_info = ciphersuite_info;
+        ssl->session_negotiate->ciphersuite = cipher_suite;
         MBEDTLS_SSL_DEBUG_MSG( 2, ( "overwrite ciphersuite: %04x - %s",
                                     cipher_suite, ciphersuite_info->name ) );
-
+#if defined(MBEDTLS_SSL_SESSION_TICKETS)
+        if( psk_type == MBEDTLS_SSL_TLS1_3_PSK_RESUMPTION )
+        {
+            ret = ssl_tls13_session_copy_ticket( ssl->session_negotiate,
+                                                 &session );
+            mbedtls_ssl_session_free( &session );
+            if( ret != 0 )
+                return( ret );
+        }
+#endif /* MBEDTLS_SSL_SESSION_TICKETS */
     }
 
     if( p_identity_len != identities_end || p_binder_len != binders_end )
@@ -2366,11 +2579,11 @@
     if( ret != 0 )
         return( ret );
 
-    ret = mbedtls_ssl_tls13_generate_resumption_master_secret( ssl );
+    ret = mbedtls_ssl_tls13_compute_resumption_master_secret( ssl );
     if( ret != 0 )
     {
         MBEDTLS_SSL_DEBUG_RET( 1,
-            "mbedtls_ssl_tls13_generate_resumption_master_secret ", ret );
+            "mbedtls_ssl_tls13_compute_resumption_master_secret", ret );
     }
 
     mbedtls_ssl_handshake_set_state( ssl, MBEDTLS_SSL_HANDSHAKE_WRAPUP );
diff --git a/programs/ssl/ssl_client2.c b/programs/ssl/ssl_client2.c
index da85eb0..6beaa12 100644
--- a/programs/ssl/ssl_client2.c
+++ b/programs/ssl/ssl_client2.c
@@ -657,6 +657,58 @@
 }
 #endif /* MBEDTLS_SSL_DTLS_CONNECTION_ID */
 
+static int ssl_save_session_serialize( mbedtls_ssl_context *ssl,
+                                       unsigned char **session_data,
+                                       size_t *session_data_len )
+{
+    int ret = MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED;
+    mbedtls_ssl_session exported_session;
+
+    /* free any previously saved data */
+    if( *session_data != NULL )
+    {
+        mbedtls_platform_zeroize( *session_data, *session_data_len );
+        mbedtls_free( *session_data );
+        *session_data = NULL;
+        *session_data_len = 0;
+    }
+
+    mbedtls_ssl_session_init( &exported_session );
+    ret = mbedtls_ssl_get_session( ssl, &exported_session );
+    if( ret != 0 )
+    {
+        mbedtls_printf(
+            "failed\n  ! mbedtls_ssl_get_session() returned -%#02x\n",
+            (unsigned) -ret );
+        goto exit;
+    }
+
+    /* get size of the buffer needed */
+    mbedtls_ssl_session_save( &exported_session, NULL, 0, session_data_len );
+    *session_data = mbedtls_calloc( 1, *session_data_len );
+    if( *session_data == NULL )
+    {
+        mbedtls_printf( " failed\n  ! alloc %u bytes for session data\n",
+                        (unsigned) *session_data_len );
+        ret = MBEDTLS_ERR_SSL_ALLOC_FAILED;
+        goto exit;
+    }
+
+    /* actually save session data */
+    if( ( ret = mbedtls_ssl_session_save( &exported_session,
+                                          *session_data, *session_data_len,
+                                          session_data_len ) ) != 0 )
+    {
+        mbedtls_printf( " failed\n  ! mbedtls_ssl_session_saved returned -0x%04x\n\n",
+                        (unsigned int) -ret );
+        goto exit;
+    }
+
+exit:
+    mbedtls_ssl_session_free( &exported_session );
+    return( ret );
+}
+
 int main( int argc, char *argv[] )
 {
     int ret = 0, len, tail_len, i, written, frags, retry_left;
@@ -2360,57 +2412,21 @@
         }
     }
 #endif /* MBEDTLS_SSL_DTLS_SRTP */
-    if( opt.reconnect != 0 )
+    if( opt.reconnect != 0 && ssl.tls_version != MBEDTLS_SSL_VERSION_TLS1_3 )
     {
         mbedtls_printf("  . Saving session for reuse..." );
         fflush( stdout );
 
         if( opt.reco_mode == 1 )
         {
-            mbedtls_ssl_session exported_session;
-
-            /* free any previously saved data */
-            if( session_data != NULL )
+            if( ( ret = ssl_save_session_serialize( &ssl,
+                            &session_data, &session_data_len ) ) != 0 )
             {
-                mbedtls_platform_zeroize( session_data, session_data_len );
-                mbedtls_free( session_data );
-                session_data = NULL;
-            }
-
-            mbedtls_ssl_session_init( &exported_session );
-            ret = mbedtls_ssl_get_session( &ssl, &exported_session );
-            if( ret != 0 )
-            {
-                mbedtls_printf(
-                    "failed\n  ! mbedtls_ssl_get_session() returned -%#02x\n",
-                    (unsigned) -ret );
-                goto exit;
-            }
-
-            /* get size of the buffer needed */
-            mbedtls_ssl_session_save( &exported_session, NULL, 0, &session_data_len );
-            session_data = mbedtls_calloc( 1, session_data_len );
-            if( session_data == NULL )
-            {
-                mbedtls_printf( " failed\n  ! alloc %u bytes for session data\n",
-                                (unsigned) session_data_len );
-                mbedtls_ssl_session_free( &exported_session );
-                ret = MBEDTLS_ERR_SSL_ALLOC_FAILED;
-                goto exit;
-            }
-
-            /* actually save session data */
-            if( ( ret = mbedtls_ssl_session_save( &exported_session,
-                                                  session_data, session_data_len,
-                                                  &session_data_len ) ) != 0 )
-            {
-                mbedtls_printf( " failed\n  ! mbedtls_ssl_session_saved returned -0x%04x\n\n",
+                mbedtls_printf( " failed\n  ! ssl_save_session_serialize returned -0x%04x\n\n",
                                 (unsigned int) -ret );
-                mbedtls_ssl_session_free( &exported_session );
                 goto exit;
             }
 
-            mbedtls_ssl_session_free( &exported_session );
         }
         else
         {
@@ -2700,6 +2716,40 @@
                         /* We were waiting for application data but got
                          * a NewSessionTicket instead. */
                         mbedtls_printf( " got new session ticket.\n" );
+                        if( opt.reconnect != 0 )
+                        {
+                            mbedtls_printf("  . Saving session for reuse..." );
+                            fflush( stdout );
+
+                            if( opt.reco_mode == 1 )
+                            {
+                                if( ( ret = ssl_save_session_serialize( &ssl,
+                                                &session_data, &session_data_len ) ) != 0 )
+                                {
+                                    mbedtls_printf( " failed\n  ! ssl_save_session_serialize returned -0x%04x\n\n",
+                                                    (unsigned int) -ret );
+                                    goto exit;
+                                }
+                            }
+                            else
+                            {
+                                if( ( ret = mbedtls_ssl_get_session( &ssl, &saved_session ) ) != 0 )
+                                {
+                                    mbedtls_printf( " failed\n  ! mbedtls_ssl_get_session returned -0x%x\n\n",
+                                                    (unsigned int) -ret );
+                                    goto exit;
+                                }
+                            }
+
+                            mbedtls_printf( " ok\n" );
+
+                            if( opt.reco_mode == 1 )
+                            {
+                                mbedtls_printf( "    [ Saved %u bytes of session data]\n",
+                                                (unsigned) session_data_len );
+                            }
+                        }
+
                         continue;
 #endif /* MBEDTLS_SSL_SESSION_TICKETS */
 
diff --git a/scripts/make_generated_files.bat b/scripts/make_generated_files.bat
index 662da98..e9d9275 100644
--- a/scripts/make_generated_files.bat
+++ b/scripts/make_generated_files.bat
@@ -10,4 +10,5 @@
 python scripts\generate_ssl_debug_helpers.py || exit /b 1

 perl scripts\generate_visualc_files.pl || exit /b 1

 python scripts\generate_psa_constants.py || exit /b 1

+python tests\scripts\generate_bignum_tests.py || exit /b 1

 python tests\scripts\generate_psa_tests.py || exit /b 1

diff --git a/scripts/mbedtls_dev/test_generation.py b/scripts/mbedtls_dev/test_generation.py
new file mode 100644
index 0000000..a88425f
--- /dev/null
+++ b/scripts/mbedtls_dev/test_generation.py
@@ -0,0 +1,219 @@
+"""Common test generation classes and main function.
+
+These are used both by generate_psa_tests.py and generate_bignum_tests.py.
+"""
+
+# Copyright The Mbed TLS Contributors
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import posixpath
+import re
+
+from abc import ABCMeta, abstractmethod
+from typing import Callable, Dict, Iterable, Iterator, List, Type, TypeVar
+
+from mbedtls_dev import build_tree
+from mbedtls_dev import test_case
+
+T = TypeVar('T') #pylint: disable=invalid-name
+
+
+class BaseTarget(metaclass=ABCMeta):
+    """Base target for test case generation.
+
+    Child classes of this class represent an output file, and can be referred
+    to as file targets. These indicate where test cases will be written to for
+    all subclasses of the file target, which is set by `target_basename`.
+
+    Attributes:
+        count: Counter for test cases from this class.
+        case_description: Short description of the test case. This may be
+            automatically generated using the class, or manually set.
+        dependencies: A list of dependencies required for the test case.
+        show_test_count: Toggle for inclusion of `count` in the test description.
+        target_basename: Basename of file to write generated tests to. This
+            should be specified in a child class of BaseTarget.
+        test_function: Test function which the class generates cases for.
+        test_name: A common name or description of the test function. This can
+            be `test_function`, a clearer equivalent, or a short summary of the
+            test function's purpose.
+    """
+    count = 0
+    case_description = ""
+    dependencies = [] # type: List[str]
+    show_test_count = True
+    target_basename = ""
+    test_function = ""
+    test_name = ""
+
+    def __new__(cls, *args, **kwargs):
+        # pylint: disable=unused-argument
+        cls.count += 1
+        return super().__new__(cls)
+
+    @abstractmethod
+    def arguments(self) -> List[str]:
+        """Get the list of arguments for the test case.
+
+        Override this method to provide the list of arguments required for
+        the `test_function`.
+
+        Returns:
+            List of arguments required for the test function.
+        """
+        raise NotImplementedError
+
+    def description(self) -> str:
+        """Create a test case description.
+
+        Creates a description of the test case, including a name for the test
+        function, an optional case count, and a description of the specific
+        test case. This should inform a reader what is being tested, and
+        provide context for the test case.
+
+        Returns:
+            Description for the test case.
+        """
+        if self.show_test_count:
+            return "{} #{} {}".format(
+                self.test_name, self.count, self.case_description
+                ).strip()
+        else:
+            return "{} {}".format(self.test_name, self.case_description).strip()
+
+
+    def create_test_case(self) -> test_case.TestCase:
+        """Generate TestCase from the instance."""
+        tc = test_case.TestCase()
+        tc.set_description(self.description())
+        tc.set_function(self.test_function)
+        tc.set_arguments(self.arguments())
+        tc.set_dependencies(self.dependencies)
+
+        return tc
+
+    @classmethod
+    @abstractmethod
+    def generate_function_tests(cls) -> Iterator[test_case.TestCase]:
+        """Generate test cases for the class test function.
+
+        This will be called in classes where `test_function` is set.
+        Implementations should yield TestCase objects, by creating instances
+        of the class with appropriate input data, and then calling
+        `create_test_case()` on each.
+        """
+        raise NotImplementedError
+
+    @classmethod
+    def generate_tests(cls) -> Iterator[test_case.TestCase]:
+        """Generate test cases for the class and its subclasses.
+
+        In classes with `test_function` set, `generate_function_tests()` is
+        called to generate test cases first.
+
+        In all classes, this method will iterate over its subclasses, and
+        yield from `generate_tests()` in each. Calling this method on a class X
+        will yield test cases from all classes derived from X.
+        """
+        if cls.test_function:
+            yield from cls.generate_function_tests()
+        for subclass in sorted(cls.__subclasses__(), key=lambda c: c.__name__):
+            yield from subclass.generate_tests()
+
+
+class TestGenerator:
+    """Generate test cases and write to data files."""
+    def __init__(self, options) -> None:
+        self.test_suite_directory = self.get_option(options, 'directory',
+                                                    'tests/suites')
+        # Update `targets` with an entry for each child class of BaseTarget.
+        # Each entry represents a file generated by the BaseTarget framework,
+        # and enables generating the .data files using the CLI.
+        self.targets.update({
+            subclass.target_basename: subclass.generate_tests
+            for subclass in BaseTarget.__subclasses__()
+        })
+
+    @staticmethod
+    def get_option(options, name: str, default: T) -> T:
+        value = getattr(options, name, None)
+        return default if value is None else value
+
+    def filename_for(self, basename: str) -> str:
+        """The location of the data file with the specified base name."""
+        return posixpath.join(self.test_suite_directory, basename + '.data')
+
+    def write_test_data_file(self, basename: str,
+                             test_cases: Iterable[test_case.TestCase]) -> None:
+        """Write the test cases to a .data file.
+
+        The output file is ``basename + '.data'`` in the test suite directory.
+        """
+        filename = self.filename_for(basename)
+        test_case.write_data_file(filename, test_cases)
+
+    # Note that targets whose names contain 'test_format' have their content
+    # validated by `abi_check.py`.
+    targets = {} # type: Dict[str, Callable[..., Iterable[test_case.TestCase]]]
+
+    def generate_target(self, name: str, *target_args) -> None:
+        """Generate cases and write to data file for a target.
+
+        For target callables which require arguments, override this function
+        and pass these arguments using super() (see PSATestGenerator).
+        """
+        test_cases = self.targets[name](*target_args)
+        self.write_test_data_file(name, test_cases)
+
+def main(args, description: str, generator_class: Type[TestGenerator] = TestGenerator):
+    """Command line entry point."""
+    parser = argparse.ArgumentParser(description=description)
+    parser.add_argument('--list', action='store_true',
+                        help='List available targets and exit')
+    parser.add_argument('--list-for-cmake', action='store_true',
+                        help='Print \';\'-separated list of available targets and exit')
+    parser.add_argument('--directory', metavar='DIR',
+                        help='Output directory (default: tests/suites)')
+    # The `--directory` option is interpreted relative to the directory from
+    # which the script is invoked, but the default is relative to the root of
+    # the mbedtls tree. The default should not be set above, but instead after
+    # `build_tree.chdir_to_root()` is called.
+    parser.add_argument('targets', nargs='*', metavar='TARGET',
+                        help='Target file to generate (default: all; "-": none)')
+    options = parser.parse_args(args)
+    build_tree.chdir_to_root()
+    generator = generator_class(options)
+    if options.list:
+        for name in sorted(generator.targets):
+            print(generator.filename_for(name))
+        return
+    # List in a cmake list format (i.e. ';'-separated)
+    if options.list_for_cmake:
+        print(';'.join(generator.filename_for(name)
+                       for name in sorted(generator.targets)), end='')
+        return
+    if options.targets:
+        # Allow "-" as a special case so you can run
+        # ``generate_xxx_tests.py - $targets`` and it works uniformly whether
+        # ``$targets`` is empty or not.
+        options.targets = [os.path.basename(re.sub(r'\.data\Z', r'', target))
+                           for target in options.targets
+                           if target != '-']
+    else:
+        options.targets = sorted(generator.targets)
+    for target in options.targets:
+        generator.generate_target(target)
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 6049b74..57cf977 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -20,24 +20,54 @@
 execute_process(
     COMMAND
         ${MBEDTLS_PYTHON_EXECUTABLE}
+        ${CMAKE_CURRENT_SOURCE_DIR}/../tests/scripts/generate_bignum_tests.py
+        --list-for-cmake
+        --directory suites
+    WORKING_DIRECTORY
+        ${CMAKE_CURRENT_SOURCE_DIR}/..
+    OUTPUT_VARIABLE
+        base_bignum_generated_data_files)
+
+execute_process(
+    COMMAND
+        ${MBEDTLS_PYTHON_EXECUTABLE}
         ${CMAKE_CURRENT_SOURCE_DIR}/../tests/scripts/generate_psa_tests.py
         --list-for-cmake
         --directory suites
     WORKING_DIRECTORY
         ${CMAKE_CURRENT_SOURCE_DIR}/..
     OUTPUT_VARIABLE
-        base_generated_data_files)
+        base_psa_generated_data_files)
 
 # Derive generated file paths in the build directory
-set(generated_data_files "")
-foreach(file ${base_generated_data_files})
-    list(APPEND generated_data_files ${CMAKE_CURRENT_BINARY_DIR}/${file})
+set(base_generated_data_files ${base_bignum_generated_data_files} ${base_psa_generated_data_files})
+set(bignum_generated_data_files "")
+set(psa_generated_data_files "")
+foreach(file ${base_bignum_generated_data_files})
+    list(APPEND bignum_generated_data_files ${CMAKE_CURRENT_BINARY_DIR}/${file})
+endforeach()
+foreach(file ${base_psa_generated_data_files})
+    list(APPEND psa_generated_data_files ${CMAKE_CURRENT_BINARY_DIR}/${file})
 endforeach()
 
 if(GEN_FILES)
     add_custom_command(
         OUTPUT
-            ${generated_data_files}
+            ${bignum_generated_data_files}
+        WORKING_DIRECTORY
+            ${CMAKE_CURRENT_SOURCE_DIR}/..
+        COMMAND
+            ${MBEDTLS_PYTHON_EXECUTABLE}
+            ${CMAKE_CURRENT_SOURCE_DIR}/../tests/scripts/generate_bignum_tests.py
+            --directory ${CMAKE_CURRENT_BINARY_DIR}/suites
+        DEPENDS
+            ${CMAKE_CURRENT_SOURCE_DIR}/../tests/scripts/generate_bignum_tests.py
+            ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/test_case.py
+            ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/test_generation.py
+    )
+    add_custom_command(
+        OUTPUT
+            ${psa_generated_data_files}
         WORKING_DIRECTORY
             ${CMAKE_CURRENT_SOURCE_DIR}/..
         COMMAND
@@ -50,6 +80,7 @@
             ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/macro_collector.py
             ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/psa_storage.py
             ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/test_case.py
+            ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/test_generation.py
             ${CMAKE_CURRENT_SOURCE_DIR}/../include/psa/crypto_config.h
             ${CMAKE_CURRENT_SOURCE_DIR}/../include/psa/crypto_values.h
             ${CMAKE_CURRENT_SOURCE_DIR}/../include/psa/crypto_extra.h
@@ -65,7 +96,8 @@
 # they can cause race conditions in parallel builds.
 # With this line, only 4 sub-makefiles include the above command, that reduces
 # the risk of a race.
-add_custom_target(test_suite_generated_data DEPENDS ${generated_data_files})
+add_custom_target(test_suite_bignum_generated_data DEPENDS ${bignum_generated_data_files})
+add_custom_target(test_suite_psa_generated_data DEPENDS ${psa_generated_data_files})
 # Test suites caught by SKIP_TEST_SUITES are built but not executed.
 # "foo" as a skip pattern skips "test_suite_foo" and "test_suite_foo.bar"
 # but not "test_suite_foobar".
@@ -82,23 +114,39 @@
 
     # Get the test names of the tests with generated .data files
     # from the generated_data_files list in parent scope.
-    set(generated_data_names "")
-    foreach(generated_data_file ${generated_data_files})
+    set(bignum_generated_data_names "")
+    set(psa_generated_data_names "")
+    foreach(generated_data_file ${bignum_generated_data_files})
         # Get the plain filename
         get_filename_component(generated_data_name ${generated_data_file} NAME)
         # Remove the ".data" extension
         get_name_without_last_ext(generated_data_name ${generated_data_name})
         # Remove leading "test_suite_"
         string(SUBSTRING ${generated_data_name} 11 -1 generated_data_name)
-        list(APPEND generated_data_names ${generated_data_name})
+        list(APPEND bignum_generated_data_names ${generated_data_name})
+    endforeach()
+    foreach(generated_data_file ${psa_generated_data_files})
+        # Get the plain filename
+        get_filename_component(generated_data_name ${generated_data_file} NAME)
+        # Remove the ".data" extension
+        get_name_without_last_ext(generated_data_name ${generated_data_name})
+        # Remove leading "test_suite_"
+        string(SUBSTRING ${generated_data_name} 11 -1 generated_data_name)
+        list(APPEND psa_generated_data_names ${generated_data_name})
     endforeach()
 
-    if(";${generated_data_names};" MATCHES ";${data_name};")
+    if(";${bignum_generated_data_names};" MATCHES ";${data_name};")
         set(data_file
             ${CMAKE_CURRENT_BINARY_DIR}/suites/test_suite_${data_name}.data)
+        set(dependency test_suite_bignum_generated_data)
+    elseif(";${psa_generated_data_names};" MATCHES ";${data_name};")
+        set(data_file
+            ${CMAKE_CURRENT_BINARY_DIR}/suites/test_suite_${data_name}.data)
+        set(dependency test_suite_psa_generated_data)
     else()
         set(data_file
             ${CMAKE_CURRENT_SOURCE_DIR}/suites/test_suite_${data_name}.data)
+        set(dependency test_suite_bignum_generated_data test_suite_psa_generated_data)
     endif()
 
     add_custom_command(
@@ -129,7 +177,7 @@
     )
 
     add_executable(test_suite_${data_name} test_suite_${data_name}.c $<TARGET_OBJECTS:mbedtls_test>)
-    add_dependencies(test_suite_${data_name} test_suite_generated_data)
+    add_dependencies(test_suite_${data_name} ${dependency})
     target_link_libraries(test_suite_${data_name} ${libs})
     # Include test-specific header files from ./include and private header
     # files (used by some invasive tests) from ../library. Public header
diff --git a/tests/Makefile b/tests/Makefile
index 0d08f84..8777ae9 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -66,28 +66,45 @@
 endif
 
 .PHONY: generated_files
-GENERATED_DATA_FILES := $(patsubst tests/%,%,$(shell \
+GENERATED_BIGNUM_DATA_FILES := $(patsubst tests/%,%,$(shell \
+	$(PYTHON) scripts/generate_bignum_tests.py --list || \
+	echo FAILED \
+))
+ifeq ($(GENERATED_BIGNUM_DATA_FILES),FAILED)
+$(error "$(PYTHON) scripts/generate_bignum_tests.py --list" failed)
+endif
+GENERATED_PSA_DATA_FILES := $(patsubst tests/%,%,$(shell \
 	$(PYTHON) scripts/generate_psa_tests.py --list || \
 	echo FAILED \
 ))
-ifeq ($(GENERATED_DATA_FILES),FAILED)
+ifeq ($(GENERATED_PSA_DATA_FILES),FAILED)
 $(error "$(PYTHON) scripts/generate_psa_tests.py --list" failed)
 endif
-GENERATED_FILES := $(GENERATED_DATA_FILES)
+GENERATED_FILES := $(GENERATED_PSA_DATA_FILES) $(GENERATED_BIGNUM_DATA_FILES)
 generated_files: $(GENERATED_FILES)
 
-# generate_psa_tests.py spends more time analyzing inputs than generating
-# outputs. Its inputs are the same no matter which files are being generated.
+# generate_bignum_tests.py and generate_psa_tests.py spend more time analyzing
+# inputs than generating outputs. Its inputs are the same no matter which files
+# are being generated.
 # It's rare not to want all the outputs. So always generate all of its outputs.
 # Use an intermediate phony dependency so that parallel builds don't run
 # a separate instance of the recipe for each output file.
-.SECONDARY: generated_psa_test_data
-$(GENERATED_DATA_FILES): generated_psa_test_data
+.SECONDARY: generated_bignum_test_data generated_psa_test_data
+$(GENERATED_BIGNUM_DATA_FILES): generated_bignum_test_data
+generated_bignum_test_data: scripts/generate_bignum_tests.py
+generated_bignum_test_data: ../scripts/mbedtls_dev/test_case.py
+generated_bignum_test_data: ../scripts/mbedtls_dev/test_generation.py
+generated_bignum_test_data:
+	echo "  Gen   $(GENERATED_BIGNUM_DATA_FILES)"
+	$(PYTHON) scripts/generate_bignum_tests.py
+
+$(GENERATED_PSA_DATA_FILES): generated_psa_test_data
 generated_psa_test_data: scripts/generate_psa_tests.py
 generated_psa_test_data: ../scripts/mbedtls_dev/crypto_knowledge.py
 generated_psa_test_data: ../scripts/mbedtls_dev/macro_collector.py
 generated_psa_test_data: ../scripts/mbedtls_dev/psa_storage.py
 generated_psa_test_data: ../scripts/mbedtls_dev/test_case.py
+generated_psa_test_data: ../scripts/mbedtls_dev/test_generation.py
 ## The generated file only depends on the options that are present in
 ## crypto_config.h, not on which options are set. To avoid regenerating this
 ## file all the time when switching between configurations, don't declare
@@ -98,7 +115,7 @@
 generated_psa_test_data: ../include/psa/crypto_extra.h
 generated_psa_test_data: suites/test_suite_psa_crypto_metadata.data
 generated_psa_test_data:
-	echo "  Gen   $(GENERATED_DATA_FILES) ..."
+	echo "  Gen   $(GENERATED_PSA_DATA_FILES) ..."
 	$(PYTHON) scripts/generate_psa_tests.py
 
 # A test application is built for each suites/test_suite_*.data file.
@@ -107,7 +124,7 @@
 DATA_FILES := $(wildcard suites/test_suite_*.data)
 # Make sure that generated data files are included even if they don't
 # exist yet when the makefile is parsed.
-DATA_FILES += $(filter-out $(DATA_FILES),$(GENERATED_DATA_FILES))
+DATA_FILES += $(filter-out $(DATA_FILES),$(GENERATED_FILES))
 APPS = $(basename $(subst suites/,,$(DATA_FILES)))
 
 # Construct executable name by adding OS specific suffix $(EXEXT).
diff --git a/tests/scripts/check-generated-files.sh b/tests/scripts/check-generated-files.sh
index 1736f24..3006ec7 100755
--- a/tests/scripts/check-generated-files.sh
+++ b/tests/scripts/check-generated-files.sh
@@ -126,4 +126,5 @@
 # the step that creates or updates these files.
 check scripts/generate_visualc_files.pl visualc/VS2010
 check scripts/generate_psa_constants.py programs/psa/psa_constant_names_generated.c
+check tests/scripts/generate_bignum_tests.py $(tests/scripts/generate_bignum_tests.py --list)
 check tests/scripts/generate_psa_tests.py $(tests/scripts/generate_psa_tests.py --list)
diff --git a/tests/scripts/generate_bignum_tests.py b/tests/scripts/generate_bignum_tests.py
new file mode 100755
index 0000000..ceafa4a
--- /dev/null
+++ b/tests/scripts/generate_bignum_tests.py
@@ -0,0 +1,238 @@
+#!/usr/bin/env python3
+"""Generate test data for bignum functions.
+
+With no arguments, generate all test data. With non-option arguments,
+generate only the specified files.
+
+Class structure:
+
+Child classes of test_generation.BaseTarget (file targets) represent an output
+file. These indicate where test cases will be written to, for all subclasses of
+this target. Multiple file targets should not reuse a `target_basename`.
+
+Each subclass derived from a file target can either be:
+  - A concrete class, representing a test function, which generates test cases.
+  - An abstract class containing shared methods and attributes, not associated
+        with a test function. An example is BignumOperation, which provides
+        common features used for bignum binary operations.
+
+Both concrete and abstract subclasses can be derived from, to implement
+additional test cases (see BignumCmp and BignumCmpAbs for examples of deriving
+from abstract and concrete classes).
+
+
+Adding test case generation for a function:
+
+A subclass representing the test function should be added, deriving from a
+file target such as BignumTarget. This test class must set/implement the
+following:
+  - test_function: the function name from the associated .function file.
+  - test_name: a descriptive name or brief summary to refer to the test
+        function.
+  - arguments(): a method to generate the list of arguments required for the
+        test_function.
+  - generate_function_test(): a method to generate TestCases for the function.
+        This should create instances of the class with required input data, and
+        call `.create_test_case()` to yield the TestCase.
+
+Additional details and other attributes/methods are given in the documentation
+of BaseTarget in test_generation.py.
+"""
+
+# Copyright The Mbed TLS Contributors
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import itertools
+import sys
+import typing
+
+from abc import ABCMeta, abstractmethod
+from typing import Iterator, List, Tuple, TypeVar
+
+import scripts_path # pylint: disable=unused-import
+from mbedtls_dev import test_case
+from mbedtls_dev import test_generation
+
+T = TypeVar('T') #pylint: disable=invalid-name
+
+def hex_to_int(val: str) -> int:
+    return int(val, 16) if val else 0
+
+def quote_str(val) -> str:
+    return "\"{}\"".format(val)
+
+def combination_pairs(values: List[T]) -> List[Tuple[T, T]]:
+    """Return all pair combinations from input values.
+
+    The return value is cast, as older versions of mypy are unable to derive
+    the specific type returned by itertools.combinations_with_replacement.
+    """
+    return typing.cast(
+        List[Tuple[T, T]],
+        list(itertools.combinations_with_replacement(values, 2))
+    )
+
+
+class BignumTarget(test_generation.BaseTarget, metaclass=ABCMeta):
+    #pylint: disable=abstract-method
+    """Target for bignum (mpi) test case generation."""
+    target_basename = 'test_suite_mpi.generated'
+
+
+class BignumOperation(BignumTarget, metaclass=ABCMeta):
+    """Common features for bignum binary operations.
+
+    This adds functionality common in binary operation tests. This includes
+    generation of case descriptions, using descriptions of values and symbols
+    to represent the operation or result.
+
+    Attributes:
+        symbol: Symbol used for the operation in case description.
+        input_values: List of values to use as test case inputs. These are
+            combined to produce pairs of values.
+        input_cases: List of tuples containing pairs of test case inputs. This
+            can be used to implement specific pairs of inputs.
+    """
+    symbol = ""
+    input_values = [
+        "", "0", "7b", "-7b",
+        "0000000000000000123", "-0000000000000000123",
+        "1230000000000000000", "-1230000000000000000"
+    ] # type: List[str]
+    input_cases = [] # type: List[Tuple[str, str]]
+
+    def __init__(self, val_a: str, val_b: str) -> None:
+        self.arg_a = val_a
+        self.arg_b = val_b
+        self.int_a = hex_to_int(val_a)
+        self.int_b = hex_to_int(val_b)
+
+    def arguments(self) -> List[str]:
+        return [quote_str(self.arg_a), quote_str(self.arg_b), self.result()]
+
+    def description(self) -> str:
+        """Generate a description for the test case.
+
+        If not set, case_description uses the form A `symbol` B, where symbol
+        is used to represent the operation. Descriptions of each value are
+        generated to provide some context to the test case.
+        """
+        if not self.case_description:
+            self.case_description = "{} {} {}".format(
+                self.value_description(self.arg_a),
+                self.symbol,
+                self.value_description(self.arg_b)
+            )
+        return super().description()
+
+    @abstractmethod
+    def result(self) -> str:
+        """Get the result of the operation.
+
+        This could be calculated during initialization and stored as `_result`
+        and then returned, or calculated when the method is called.
+        """
+        raise NotImplementedError
+
+    @staticmethod
+    def value_description(val) -> str:
+        """Generate a description of the argument val.
+
+        This produces a simple description of the value, which is used in test
+        case naming to add context.
+        """
+        if val == "":
+            return "0 (null)"
+        if val == "0":
+            return "0 (1 limb)"
+
+        if val[0] == "-":
+            tmp = "negative"
+            val = val[1:]
+        else:
+            tmp = "positive"
+        if val[0] == "0":
+            tmp += " with leading zero limb"
+        elif len(val) > 10:
+            tmp = "large " + tmp
+        return tmp
+
+    @classmethod
+    def get_value_pairs(cls) -> Iterator[Tuple[str, str]]:
+        """Generator to yield pairs of inputs.
+
+        Combinations are first generated from all input values, and then
+        specific cases provided.
+        """
+        yield from combination_pairs(cls.input_values)
+        yield from cls.input_cases
+
+    @classmethod
+    def generate_function_tests(cls) -> Iterator[test_case.TestCase]:
+        for a_value, b_value in cls.get_value_pairs():
+            cur_op = cls(a_value, b_value)
+            yield cur_op.create_test_case()
+
+
+class BignumCmp(BignumOperation):
+    """Test cases for bignum value comparison."""
+    count = 0
+    test_function = "mbedtls_mpi_cmp_mpi"
+    test_name = "MPI compare"
+    input_cases = [
+        ("-2", "-3"),
+        ("-2", "-2"),
+        ("2b4", "2b5"),
+        ("2b5", "2b6")
+        ]
+
+    def __init__(self, val_a, val_b) -> None:
+        super().__init__(val_a, val_b)
+        self._result = int(self.int_a > self.int_b) - int(self.int_a < self.int_b)
+        self.symbol = ["<", "==", ">"][self._result + 1]
+
+    def result(self) -> str:
+        return str(self._result)
+
+
+class BignumCmpAbs(BignumCmp):
+    """Test cases for absolute bignum value comparison."""
+    count = 0
+    test_function = "mbedtls_mpi_cmp_abs"
+    test_name = "MPI compare (abs)"
+
+    def __init__(self, val_a, val_b) -> None:
+        super().__init__(val_a.strip("-"), val_b.strip("-"))
+
+
+class BignumAdd(BignumOperation):
+    """Test cases for bignum value addition."""
+    count = 0
+    symbol = "+"
+    test_function = "mbedtls_mpi_add_mpi"
+    test_name = "MPI add"
+    input_cases = combination_pairs(
+        [
+            "1c67967269c6", "9cde3",
+            "-1c67967269c6", "-9cde3",
+        ]
+    )
+
+    def result(self) -> str:
+        return quote_str("{:x}".format(self.int_a + self.int_b))
+
+if __name__ == '__main__':
+    # Use the section of the docstring relevant to the CLI as description
+    test_generation.main(sys.argv[1:], "\n".join(__doc__.splitlines()[:4]))
diff --git a/tests/scripts/generate_psa_tests.py b/tests/scripts/generate_psa_tests.py
index 3d23edd..c788fd7 100755
--- a/tests/scripts/generate_psa_tests.py
+++ b/tests/scripts/generate_psa_tests.py
@@ -20,22 +20,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import argparse
 import enum
-import os
-import posixpath
 import re
 import sys
-from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional, TypeVar
+from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional
 
 import scripts_path # pylint: disable=unused-import
-from mbedtls_dev import build_tree
 from mbedtls_dev import crypto_knowledge
 from mbedtls_dev import macro_collector
 from mbedtls_dev import psa_storage
 from mbedtls_dev import test_case
-
-T = TypeVar('T') #pylint: disable=invalid-name
+from mbedtls_dev import test_generation
 
 
 def psa_want_symbol(name: str) -> str:
@@ -897,35 +892,11 @@
         yield from super().generate_all_keys()
         yield from self.all_keys_for_implicit_usage()
 
-class TestGenerator:
-    """Generate test data."""
-
-    def __init__(self, options) -> None:
-        self.test_suite_directory = self.get_option(options, 'directory',
-                                                    'tests/suites')
-        self.info = Information()
-
-    @staticmethod
-    def get_option(options, name: str, default: T) -> T:
-        value = getattr(options, name, None)
-        return default if value is None else value
-
-    def filename_for(self, basename: str) -> str:
-        """The location of the data file with the specified base name."""
-        return posixpath.join(self.test_suite_directory, basename + '.data')
-
-    def write_test_data_file(self, basename: str,
-                             test_cases: Iterable[test_case.TestCase]) -> None:
-        """Write the test cases to a .data file.
-
-        The output file is ``basename + '.data'`` in the test suite directory.
-        """
-        filename = self.filename_for(basename)
-        test_case.write_data_file(filename, test_cases)
-
+class PSATestGenerator(test_generation.TestGenerator):
+    """Test generator subclass including PSA targets and info."""
     # Note that targets whose names contain 'test_format' have their content
     # validated by `abi_check.py`.
-    TARGETS = {
+    targets = {
         'test_suite_psa_crypto_generate_key.generated':
         lambda info: KeyGenerate(info).test_cases_for_key_generation(),
         'test_suite_psa_crypto_not_supported.generated':
@@ -938,44 +909,12 @@
         lambda info: StorageFormatV0(info).all_test_cases(),
     } #type: Dict[str, Callable[[Information], Iterable[test_case.TestCase]]]
 
-    def generate_target(self, name: str) -> None:
-        test_cases = self.TARGETS[name](self.info)
-        self.write_test_data_file(name, test_cases)
+    def __init__(self, options):
+        super().__init__(options)
+        self.info = Information()
 
-def main(args):
-    """Command line entry point."""
-    parser = argparse.ArgumentParser(description=__doc__)
-    parser.add_argument('--list', action='store_true',
-                        help='List available targets and exit')
-    parser.add_argument('--list-for-cmake', action='store_true',
-                        help='Print \';\'-separated list of available targets and exit')
-    parser.add_argument('--directory', metavar='DIR',
-                        help='Output directory (default: tests/suites)')
-    parser.add_argument('targets', nargs='*', metavar='TARGET',
-                        help='Target file to generate (default: all; "-": none)')
-    options = parser.parse_args(args)
-    build_tree.chdir_to_root()
-    generator = TestGenerator(options)
-    if options.list:
-        for name in sorted(generator.TARGETS):
-            print(generator.filename_for(name))
-        return
-    # List in a cmake list format (i.e. ';'-separated)
-    if options.list_for_cmake:
-        print(';'.join(generator.filename_for(name)
-                       for name in sorted(generator.TARGETS)), end='')
-        return
-    if options.targets:
-        # Allow "-" as a special case so you can run
-        # ``generate_psa_tests.py - $targets`` and it works uniformly whether
-        # ``$targets`` is empty or not.
-        options.targets = [os.path.basename(re.sub(r'\.data\Z', r'', target))
-                           for target in options.targets
-                           if target != '-']
-    else:
-        options.targets = sorted(generator.TARGETS)
-    for target in options.targets:
-        generator.generate_target(target)
+    def generate_target(self, name: str, *target_args) -> None:
+        super().generate_target(name, self.info)
 
 if __name__ == '__main__':
-    main(sys.argv[1:])
+    test_generation.main(sys.argv[1:], __doc__, PSATestGenerator)
diff --git a/tests/ssl-opt.sh b/tests/ssl-opt.sh
index 9e14af1..84bcd3c 100755
--- a/tests/ssl-opt.sh
+++ b/tests/ssl-opt.sh
@@ -12096,7 +12096,7 @@
 run_test    "TLS 1.3, default suite, PSK" \
             "$P_SRV nbio=2 debug_level=5 force_version=tls13 psk=010203 psk_identity=0a0b0c tls13_kex_modes=psk" \
             "$P_CLI nbio=2 debug_level=5 force_version=tls13 psk=010203 psk_identity=0a0b0c tls13_kex_modes=psk" \
-            1 \
+            0 \
             -c "=> write client hello" \
             -c "client hello, adding pre_shared_key extension, omitting PSK binder list" \
             -c "client hello, adding psk_key_exchange_modes extension" \
@@ -12111,7 +12111,7 @@
 run_test    "TLS 1.3, default suite, PSK - openssl" \
             "$O_NEXT_SRV -msg -debug -tls1_3 -psk_identity 0a0b0c -psk 010203 -allow_no_dhe_kex -nocert" \
             "$P_CLI debug_level=4 psk=010203 psk_identity=0a0b0c tls13_kex_modes=psk" \
-            1 \
+            0 \
             -c "=> write client hello" \
             -c "client hello, adding pre_shared_key extension, omitting PSK binder list" \
             -c "client hello, adding psk_key_exchange_modes extension" \
@@ -12756,13 +12756,13 @@
 requires_config_enabled MBEDTLS_SSL_CLI_C
 requires_config_enabled MBEDTLS_SSL_TLS1_3_COMPATIBILITY_MODE
 run_test    "TLS 1.3: NewSessionTicket: Basic check, m->O" \
-            "$O_NEXT_SRV -msg -tls1_3 -no_resume_ephemeral -no_cache " \
-            "$P_CLI debug_level=4 reco_mode=1 reconnect=1" \
+            "$O_NEXT_SRV -msg -tls1_3 -no_resume_ephemeral -no_cache --num_tickets 4" \
+            "$P_CLI debug_level=1 reco_mode=1 reconnect=1" \
             0 \
             -c "Protocol is TLSv1.3" \
-            -c "MBEDTLS_SSL_NEW_SESSION_TICKET" \
             -c "got new session ticket." \
             -c "Saving session for reuse... ok" \
+            -c "Reconnecting with saved session" \
             -c "HTTP/1.0 200 ok"
 
 requires_gnutls_tls1_3
@@ -12771,27 +12771,15 @@
 requires_config_enabled MBEDTLS_DEBUG_C
 requires_config_enabled MBEDTLS_SSL_CLI_C
 run_test    "TLS 1.3: NewSessionTicket: Basic check, m->G" \
-            "$G_NEXT_SRV --priority=NORMAL:-VERS-ALL:+VERS-TLS1.3:+CIPHER-ALL:+PSK --disable-client-cert" \
-            "$P_CLI debug_level=4 reco_mode=1 reconnect=1" \
+            "$G_NEXT_SRV -d 10 --priority=NORMAL:-VERS-ALL:+VERS-TLS1.3:+CIPHER-ALL:+PSK --disable-client-cert" \
+            "$P_CLI debug_level=1 reco_mode=1 reconnect=1" \
             0 \
             -c "Protocol is TLSv1.3" \
-            -c "MBEDTLS_SSL_NEW_SESSION_TICKET" \
             -c "got new session ticket." \
             -c "Saving session for reuse... ok" \
-            -c "HTTP/1.0 200 OK"
-
-requires_openssl_tls1_3
-requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_3
-requires_config_enabled MBEDTLS_SSL_SESSION_TICKETS
-requires_config_enabled MBEDTLS_SSL_SRV_C
-requires_config_enabled MBEDTLS_DEBUG_C
-run_test    "TLS 1.3: NewSessionTicket: Basic check, O->m" \
-            "$P_SRV debug_level=4 crt_file=data_files/server5.crt key_file=data_files/server5.key force_version=tls13 tickets=1" \
-            "$O_NEXT_CLI -msg -debug -tls1_3 -no_middlebox" \
-            0 \
-            -s "=> write NewSessionTicket msg" \
-            -s "server state: MBEDTLS_SSL_NEW_SESSION_TICKET" \
-            -s "server state: MBEDTLS_SSL_NEW_SESSION_TICKET_FLUSH"
+            -c "Reconnecting with saved session" \
+            -c "HTTP/1.0 200 OK" \
+            -s "This is a resumed session"
 
 requires_gnutls_tls1_3
 requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_3
@@ -12800,12 +12788,16 @@
 requires_config_enabled MBEDTLS_DEBUG_C
 run_test    "TLS 1.3: NewSessionTicket: Basic check, G->m" \
             "$P_SRV debug_level=4 crt_file=data_files/server5.crt key_file=data_files/server5.key force_version=tls13 tickets=1" \
-            "$G_NEXT_CLI localhost -d 4 --priority=NORMAL:-VERS-ALL:+VERS-TLS1.3:%DISABLE_TLS13_COMPAT_MODE -V" \
+            "$G_NEXT_CLI localhost -d 4 --priority=NORMAL:-VERS-ALL:+VERS-TLS1.3:%DISABLE_TLS13_COMPAT_MODE -V -r" \
             0 \
+            -c "Connecting again- trying to resume previous session" \
+            -c "NEW SESSION TICKET (4) was received" \
             -s "=> write NewSessionTicket msg" \
             -s "server state: MBEDTLS_SSL_NEW_SESSION_TICKET" \
             -s "server state: MBEDTLS_SSL_NEW_SESSION_TICKET_FLUSH" \
-            -c "NEW SESSION TICKET (4) was received"
+            -s "key exchange mode: ephemeral" \
+            -s "key exchange mode: psk_ephemeral" \
+            -s "found pre_shared_key extension"
 
 requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_3
 requires_config_enabled MBEDTLS_SSL_SESSION_TICKETS
@@ -12817,13 +12809,17 @@
             "$P_CLI debug_level=4 reco_mode=1 reconnect=1" \
             0 \
             -c "Protocol is TLSv1.3" \
-            -c "MBEDTLS_SSL_NEW_SESSION_TICKET" \
             -c "got new session ticket." \
             -c "Saving session for reuse... ok" \
+            -c "Reconnecting with saved session" \
             -c "HTTP/1.0 200 OK"    \
             -s "=> write NewSessionTicket msg" \
             -s "server state: MBEDTLS_SSL_NEW_SESSION_TICKET" \
-            -s "server state: MBEDTLS_SSL_NEW_SESSION_TICKET_FLUSH"
+            -s "server state: MBEDTLS_SSL_NEW_SESSION_TICKET_FLUSH" \
+            -s "key exchange mode: ephemeral" \
+            -s "key exchange mode: psk_ephemeral" \
+            -s "found pre_shared_key extension"
+
 
 requires_openssl_tls1_3
 requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_2
diff --git a/tests/suites/test_suite_ecp.data b/tests/suites/test_suite_ecp.data
index 5332c07..4c0ed1c 100644
--- a/tests/suites/test_suite_ecp.data
+++ b/tests/suites/test_suite_ecp.data
@@ -309,6 +309,58 @@
 depends_on:MBEDTLS_ECP_DP_SECP521R1_ENABLED
 ecp_tls_write_read_point:MBEDTLS_ECP_DP_SECP521R1
 
+Check ECP group metadata #1 secp192k1 (SEC 2)
+depends_on:MBEDTLS_ECP_DP_SECP192K1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_SECP192K1:192:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"fffffffffffffffffffffffffffffffffffffffeffffee37":"000000000000000000000000000000000000000000000000":"000000000000000000000000000000000000000000000003":"db4ff10ec057e9ae26b07d0280b7f4341da5d1b1eae06c7d":"9b2f2f6d9c5628a7844163d015be86344082aa88d95e2f9d":"fffffffffffffffffffffffe26f2fc170f69466a74defd8d":18
+
+Check ECP group metadata #2 secp192r1 (SEC 2)
+depends_on:MBEDTLS_ECP_DP_SECP192R1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_SECP192R1:192:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"fffffffffffffffffffffffffffffffeffffffffffffffff":"":"64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1":"188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012":"07192b95ffc8da78631011ed6b24cdd573f977a11e794811":"ffffffffffffffffffffffff99def836146bc9b1b4d22831":19
+
+Check ECP group metadata #3 secp224k1 (SEC 2)
+depends_on:MBEDTLS_ECP_DP_SECP224K1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_SECP224K1:224:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"fffffffffffffffffffffffffffffffffffffffffffffffeffffe56d":"00000000000000000000000000000000000000000000000000000000":"00000000000000000000000000000000000000000000000000000005":"a1455b334df099df30fc28a169a467e9e47075a90f7e650eb6b7a45c":"7e089fed7fba344282cafbd6f7e319f7c0b0bd59e2ca4bdb556d61a5":"010000000000000000000000000001dce8d2ec6184caf0a971769fb1f7":20
+
+Check ECP group metadata #4 secp224r1 (SEC 2)
+depends_on:MBEDTLS_ECP_DP_SECP224R1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_SECP224R1:224:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"ffffffffffffffffffffffffffffffff000000000000000000000001":"":"b4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4":"b70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21":"bd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34":"ffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d":21
+
+Check ECP group metadata #5 secp256k1 (SEC 2)
+depends_on:MBEDTLS_ECP_DP_SECP256K1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_SECP256K1:256:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f":"0000000000000000000000000000000000000000000000000000000000000000":"0000000000000000000000000000000000000000000000000000000000000007":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798":"483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8":"fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141":22
+
+Check ECP group metadata #6 secp256r1 (SEC 2)
+depends_on:MBEDTLS_ECP_DP_SECP256R1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_SECP256R1:256:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"ffffffff00000001000000000000000000000000ffffffffffffffffffffffff":"":"5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b":"6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296":"4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5":"ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551":23
+
+Check ECP group metadata #7 secp384r1 (SEC 2)
+depends_on:MBEDTLS_ECP_DP_SECP384R1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_SECP384R1:384:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff":"":"b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef":"aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7":"3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f":"ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973":24
+
+Check ECP group metadata #8 secp521r1 (SEC 2)
+depends_on:MBEDTLS_ECP_DP_SECP521R1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_SECP521R1:521:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff":"":"0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00":"00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66":"011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650":"01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409":25
+
+Check ECP group metadata #9 bp256r1 (RFC 5639)
+depends_on:MBEDTLS_ECP_DP_BP256R1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_BP256R1:256:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"a9fb57dba1eea9bc3e660a909d838d726e3bf623d52620282013481d1f6e5377":"7d5a0975fc2c3057eef67530417affe7fb8055c126dc5c6ce94a4b44f330b5d9":"26dc5c6ce94a4b44f330b5d9bbd77cbf958416295cf7e1ce6bccdc18ff8c07b6":"8bd2aeb9cb7e57cb2c4b482ffc81b7afb9de27e1e3bd23c23a4453bd9ace3262":"547ef835c3dac4fd97f8461a14611dc9c27745132ded8e545c1d54c72f046997":"a9fb57dba1eea9bc3e660a909d838d718c397aa3b561a6f7901e0e82974856a7":26
+
+Check ECP group metadata #10 bp384r1 (RFC 5639)
+depends_on:MBEDTLS_ECP_DP_BP384R1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_BP384R1:384:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b412b1da197fb71123acd3a729901d1a71874700133107ec53":"7bc382c63d8c150c3c72080ace05afa0c2bea28e4fb22787139165efba91f90f8aa5814a503ad4eb04a8c7dd22ce2826":"04a8c7dd22ce28268b39b55416f0447c2fb77de107dcd2a62e880ea53eeb62d57cb4390295dbc9943ab78696fa504c11":"1d1c64f068cf45ffa2a63a81b7c13f6b8847a3e77ef14fe3db7fcafe0cbd10e8e826e03436d646aaef87b2e247d4af1e":"8abe1d7520f9c2a45cb1eb8e95cfd55262b70b29feec5864e19c054ff99129280e4646217791811142820341263c5315":"8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b31f166e6cac0425a7cf3ab6af6b7fc3103b883202e9046565":27
+
+Check ECP group metadata #11 bp512r1 (RFC 5639)
+depends_on:MBEDTLS_ECP_DP_BP512R1_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_BP512R1:512:MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:"aadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca703308717d4d9b009bc66842aecda12ae6a380e62881ff2f2d82c68528aa6056583a48f3":"7830a3318b603b89e2327145ac234cc594cbdd8d3df91610a83441caea9863bc2ded5d5aa8253aa10a2ef1c98b9ac8b57f1117a72bf2c7b9e7c1ac4d77fc94ca":"3df91610a83441caea9863bc2ded5d5aa8253aa10a2ef1c98b9ac8b57f1117a72bf2c7b9e7c1ac4d77fc94cadc083e67984050b75ebae5dd2809bd638016f723":"81aee4bdd82ed9645a21322e9c4c6a9385ed9f70b5d916c1b43b62eef4d0098eff3b1f78e2d0d48d50d1687b93b97d5f7c6d5047406a5e688b352209bcb9f822":"7dde385d566332ecc0eabfa9cf7822fdf209f70024a57b1aa000c55b881f8111b2dcde494a5f485e5bca4bd88a2763aed1ca2b2fa8f0540678cd1e0f3ad80892":"aadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca70330870553e5c414ca92619418661197fac10471db1d381085ddaddb58796829ca90069":28
+
+Check ECP group metadata #12 curve25519 (RFC 7748)
+depends_on:MBEDTLS_ECP_DP_CURVE25519_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_CURVE25519:256:MBEDTLS_ECP_TYPE_MONTGOMERY:"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed":"76d06":"":"9":"":"1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed":29
+
+Check ECP group metadata #13 curve448 (RFC 7748)
+depends_on:MBEDTLS_ECP_DP_CURVE448_ENABLED
+mbedtls_ecp_group_metadata:MBEDTLS_ECP_DP_CURVE448:448:MBEDTLS_ECP_TYPE_MONTGOMERY:"fffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffff":"262a6":"":"5":"":"3fffffffffffffffffffffffffffffffffffffffffffffffffffffff7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3":30
+
 ECP tls read group #1 (record too short)
 mbedtls_ecp_tls_read_group:"0313":MBEDTLS_ERR_ECP_BAD_INPUT_DATA:0:0
 
diff --git a/tests/suites/test_suite_ecp.function b/tests/suites/test_suite_ecp.function
index 65c7067..42d69b4 100644
--- a/tests/suites/test_suite_ecp.function
+++ b/tests/suites/test_suite_ecp.function
@@ -1,5 +1,7 @@
 /* BEGIN_HEADER */
 #include "mbedtls/ecp.h"
+#include "mbedtls/ecdsa.h"
+#include "mbedtls/ecdh.h"
 
 #include "ecp_invasive.h"
 
@@ -788,6 +790,124 @@
 }
 /* END_CASE */
 
+/* BEGIN_CASE depends_on:MBEDTLS_ECDH_C:MBEDTLS_ECDSA_C */
+void mbedtls_ecp_group_metadata( int id, int bit_size, int crv_type,
+                                 char* P, char* A, char* B,
+                                 char* G_x, char* G_y, char* N,
+                                 int tls_id )
+{
+    mbedtls_ecp_group grp, grp_read, grp_cpy;
+    const mbedtls_ecp_group_id *g_id;
+    mbedtls_ecp_group_id read_g_id;
+    const mbedtls_ecp_curve_info *crv, *crv_tls_id, *crv_name;
+
+    mbedtls_mpi exp_P, exp_A, exp_B, exp_G_x, exp_G_y, exp_N;
+
+    unsigned char buf[3], ecparameters[3] = { 3, 0, tls_id };
+    const unsigned char *vbuf = buf;
+    size_t olen;
+
+    mbedtls_ecp_group_init( &grp );
+    mbedtls_ecp_group_init( &grp_read );
+    mbedtls_ecp_group_init( &grp_cpy );
+
+    mbedtls_mpi_init( &exp_P );
+    mbedtls_mpi_init( &exp_A );
+    mbedtls_mpi_init( &exp_B );
+    mbedtls_mpi_init( &exp_G_x );
+    mbedtls_mpi_init( &exp_G_y );
+    mbedtls_mpi_init( &exp_N );
+
+    // Read expected parameters
+    TEST_EQUAL( mbedtls_test_read_mpi( &exp_P, P ), 0 );
+    TEST_EQUAL( mbedtls_test_read_mpi( &exp_A, A ), 0 );
+    TEST_EQUAL( mbedtls_test_read_mpi( &exp_G_x, G_x ), 0 );
+    TEST_EQUAL( mbedtls_test_read_mpi( &exp_N, N ), 0 );
+    TEST_EQUAL( mbedtls_test_read_mpi( &exp_B, B ), 0 );
+    TEST_EQUAL( mbedtls_test_read_mpi( &exp_G_y, G_y ), 0 );
+
+    // Convert exp_A to internal representation (A+2)/4
+    if( crv_type == MBEDTLS_ECP_TYPE_MONTGOMERY )
+    {
+        TEST_EQUAL( mbedtls_mpi_add_int( &exp_A, &exp_A, 2 ), 0 );
+        TEST_EQUAL( mbedtls_mpi_div_int( &exp_A, NULL, &exp_A, 4 ), 0 );
+    }
+
+    // Load group
+    TEST_EQUAL( mbedtls_ecp_group_load( &grp, id ), 0 );
+
+    // Compare group with expected parameters
+    // A is NULL for SECPxxxR1 curves
+    // B and G_y are NULL for curve25519 and curve448
+    TEST_EQUAL( mbedtls_mpi_cmp_mpi( &exp_P, &grp.P ), 0 );
+    if( *A != 0 )
+        TEST_EQUAL( mbedtls_mpi_cmp_mpi( &exp_A, &grp.A ), 0 );
+    if( *B != 0 )
+        TEST_EQUAL( mbedtls_mpi_cmp_mpi( &exp_B, &grp.B ), 0 );
+    TEST_EQUAL( mbedtls_mpi_cmp_mpi( &exp_G_x, &grp.G.X ), 0 );
+    if( *G_y != 0 )
+        TEST_EQUAL( mbedtls_mpi_cmp_mpi( &exp_G_y, &grp.G.Y ), 0 );
+    TEST_EQUAL( mbedtls_mpi_cmp_mpi( &exp_N, &grp.N ), 0 );
+
+    // Load curve info and compare with known values
+    crv = mbedtls_ecp_curve_info_from_grp_id( id );
+    TEST_EQUAL( crv->grp_id, id );
+    TEST_EQUAL( crv->bit_size, bit_size );
+    TEST_EQUAL( crv->tls_id, tls_id );
+
+    // Load curve from TLS ID and name, and compare IDs
+    crv_tls_id = mbedtls_ecp_curve_info_from_tls_id( crv->tls_id );
+    crv_name = mbedtls_ecp_curve_info_from_name( crv->name );
+    TEST_EQUAL( crv_tls_id->grp_id, id );
+    TEST_EQUAL( crv_name->grp_id, id );
+
+    // Validate write_group against test data
+    TEST_EQUAL( mbedtls_ecp_tls_write_group( &grp, &olen,
+                                             buf, sizeof( buf ) ),
+                0 );
+    TEST_EQUAL( mbedtls_test_hexcmp( buf, ecparameters, olen,
+                                     sizeof( ecparameters ) ),
+                0 );
+
+    // Read group from buffer and compare with expected ID
+    TEST_EQUAL( mbedtls_ecp_tls_read_group_id( &read_g_id, &vbuf, olen ),
+                0 );
+    TEST_EQUAL( read_g_id, id );
+    vbuf = buf;
+    TEST_EQUAL( mbedtls_ecp_tls_read_group( &grp_read, &vbuf, olen ),
+                0 );
+    TEST_EQUAL( grp_read.id, id );
+
+    // Check curve type, and if it can be used for ECDH/ECDSA
+    TEST_EQUAL( mbedtls_ecp_get_type( &grp ), crv_type );
+    TEST_EQUAL( mbedtls_ecdh_can_do( id ), 1 );
+    TEST_EQUAL( mbedtls_ecdsa_can_do( id ),
+                crv_type == MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS );
+
+    // Copy group and compare with original
+    TEST_EQUAL( mbedtls_ecp_group_copy( &grp_cpy, &grp ), 0 );
+    TEST_EQUAL( mbedtls_ecp_group_cmp( &grp, &grp_cpy ), 0 );
+
+    // Check curve is in curve list and group ID list
+    for( crv = mbedtls_ecp_curve_list(  );
+          crv->grp_id != MBEDTLS_ECP_DP_NONE &&
+          crv->grp_id != (unsigned) id;
+          crv++ );
+    TEST_EQUAL( crv->grp_id, id );
+    for( g_id = mbedtls_ecp_grp_id_list(  );
+         *g_id != MBEDTLS_ECP_DP_NONE && *g_id != (unsigned) id;
+         g_id++ );
+    TEST_EQUAL( *g_id, (unsigned) id );
+
+exit:
+    mbedtls_ecp_group_free( &grp ); mbedtls_ecp_group_free( &grp_cpy );
+    mbedtls_ecp_group_free( &grp_read );
+    mbedtls_mpi_free( &exp_P ); mbedtls_mpi_free( &exp_A );
+    mbedtls_mpi_free( &exp_B ); mbedtls_mpi_free( &exp_G_x );
+    mbedtls_mpi_free( &exp_G_y ); mbedtls_mpi_free( &exp_N );
+}
+/* END_CASE */
+
 /* BEGIN_CASE */
 void mbedtls_ecp_check_privkey( int id, char * key_hex, int ret )
 {