implement X25519MLKEM768
diff --git a/include/picotls.h b/include/picotls.h
index f3e783e..727ac60 100644
--- a/include/picotls.h
+++ b/include/picotls.h
@@ -160,6 +160,8 @@
 #define PTLS_GROUP_NAME_X25519 "x25519"
 #define PTLS_GROUP_X448 30
 #define PTLS_GROUP_NAME_X448 "x448"
+#define PTLS_GROUP_X25519MLKEM768 4588
+#define PTLS_GROUP_NAME_X25519MLKEM768 "X25519MLKEM768"
 
 /* signature algorithms */
 #define PTLS_SIGNATURE_RSA_PKCS1_SHA1 0x0201
diff --git a/include/picotls/openssl.h b/include/picotls/openssl.h
index 205500a..01af6a8 100644
--- a/include/picotls/openssl.h
+++ b/include/picotls/openssl.h
@@ -69,6 +69,12 @@
 #define PTLS_OPENSSL_HAVE_X25519 0
 #define PTLS_OPENSSL_HAS_X25519 0 /* deprecated; use HAVE_ */
 #endif
+#ifdef OPENSSL_IS_BORINGSSL
+#define PTLS_OPENSSL_HAVE_X25519MLKEM768 1
+extern ptls_key_exchange_algorithm_t ptls_openssl_x25519mlkem768;
+#else
+#define PTLS_OPENSSL_HAVE_X25519MLKEM768 0
+#endif
 
 /* when boringssl is used, existence of libdecrepit is assumed */
 #if !defined(OPENSSL_NO_BF) || defined(OPENSSL_IS_BORINGSSL)
diff --git a/lib/openssl.c b/lib/openssl.c
index 2833c32..fb9c3f9 100644
--- a/lib/openssl.c
+++ b/lib/openssl.c
@@ -52,6 +52,9 @@
 #ifdef OPENSSL_IS_BORINGSSL
 #include "./chacha20poly1305.h"
 #endif
+#if PTLS_OPENSSL_HAVE_X25519MLKEM768
+#include <openssl/mlkem.h>
+#endif
 #ifdef PTLS_HAVE_AEGIS
 #include "./libaegis.h"
 #endif
@@ -706,6 +709,135 @@
 
 #endif
 
+#if PTLS_OPENSSL_HAVE_X25519MLKEM768
+
+struct st_x25519mlkem768_context_t {
+    ptls_key_exchange_context_t super;
+    uint8_t pubkey[MLKEM768_PUBLIC_KEY_BYTES + X25519_PUBLIC_VALUE_LEN];
+    struct {
+        uint8_t x25519[X25519_PRIVATE_KEY_LEN];
+        struct MLKEM768_private_key mlkem;
+    } privkey;
+};
+
+static int x25519mlkem768_on_exchange(ptls_key_exchange_context_t **_ctx, int release, ptls_iovec_t *secret,
+                                      ptls_iovec_t ciphertext)
+{
+    struct st_x25519mlkem768_context_t *ctx = (void *)*_ctx;
+    int ret;
+
+    if (secret == NULL) {
+        ret = 0;
+        goto Exit;
+    }
+
+    *secret = ptls_iovec_init(NULL, 0);
+
+    /* validate length */
+    if (ciphertext.len != MLKEM768_CIPHERTEXT_BYTES + X25519_PUBLIC_VALUE_LEN) {
+        ret = PTLS_ALERT_DECRYPT_ERROR;
+        goto Exit;
+    }
+
+    /* appsocate memory */
+    secret->len = MLKEM_SHARED_SECRET_BYTES + X25519_SHARED_KEY_LEN;
+    if ((secret->base = malloc(secret->len)) == NULL) {
+        ret = PTLS_ERROR_NO_MEMORY;
+        goto Exit;
+    }
+
+    /* run key exchange */
+    if (!MLKEM768_decap(secret->base, ciphertext.base, MLKEM768_CIPHERTEXT_BYTES, &ctx->privkey.mlkem) ||
+        !X25519(secret->base + MLKEM_SHARED_SECRET_BYTES, ctx->privkey.x25519, ciphertext.base + MLKEM768_CIPHERTEXT_BYTES)) {
+        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+        goto Exit;
+    }
+    ret = 0;
+
+Exit:
+    if (secret != NULL && ret != 0) {
+        free(secret->base);
+        *secret = ptls_iovec_init(NULL, 0);
+    }
+    if (release) {
+        ptls_clear_memory(&ctx->privkey, sizeof(ctx->privkey));
+        free(ctx);
+        *_ctx = NULL;
+    }
+    return ret;
+}
+
+static int x25519mlkem768_create(ptls_key_exchange_algorithm_t *algo, ptls_key_exchange_context_t **_ctx)
+{
+    struct st_x25519mlkem768_context_t *ctx = NULL;
+
+    if ((ctx = malloc(sizeof(*ctx))) == NULL)
+        return PTLS_ERROR_NO_MEMORY;
+
+    ctx->super = (ptls_key_exchange_context_t){algo, ptls_iovec_init(ctx->pubkey, sizeof(ctx->pubkey)), x25519mlkem768_on_exchange};
+    MLKEM768_generate_key(ctx->pubkey, NULL, &ctx->privkey.mlkem);
+    X25519_keypair(ctx->pubkey + MLKEM768_PUBLIC_KEY_BYTES, ctx->privkey.x25519);
+
+    *_ctx = &ctx->super;
+    return 0;
+}
+
+static int x25519mlkem768_exchange(ptls_key_exchange_algorithm_t *algo, ptls_iovec_t *ciphertext, ptls_iovec_t *secret,
+                                   ptls_iovec_t peerkey)
+{
+    struct {
+        CBS cbs;
+        struct MLKEM768_public_key key;
+    } mlkem_peer;
+    uint8_t x25519_privkey[X25519_PRIVATE_KEY_LEN];
+    int ret;
+
+    *ciphertext = ptls_iovec_init(NULL, 0);
+    *secret = ptls_iovec_init(NULL, 0);
+
+    /* validate input length */
+    if (peerkey.len != MLKEM768_PUBLIC_KEY_BYTES + X25519_PUBLIC_VALUE_LEN) {
+        ret = PTLS_ALERT_DECODE_ERROR;
+        goto Exit;
+    }
+
+    /* allocate memory */
+    ciphertext->len = MLKEM768_CIPHERTEXT_BYTES + X25519_PUBLIC_VALUE_LEN;
+    if ((ciphertext->base = malloc(ciphertext->len)) == NULL) {
+        ret = PTLS_ERROR_NO_MEMORY;
+        goto Exit;
+    }
+    secret->len = MLKEM_SHARED_SECRET_BYTES + X25519_SHARED_KEY_LEN;
+    if ((secret->base = malloc(secret->len)) == NULL) {
+        ret = PTLS_ERROR_NO_MEMORY;
+        goto Exit;
+    }
+
+    /* run key exchange */
+    CBS_init(&mlkem_peer.cbs, peerkey.base, MLKEM768_PUBLIC_KEY_BYTES);
+    X25519_keypair(ciphertext->base + MLKEM768_CIPHERTEXT_BYTES, x25519_privkey);
+    if (!MLKEM768_parse_public_key(&mlkem_peer.key, &mlkem_peer.cbs) ||
+        !X25519(secret->base + MLKEM_SHARED_SECRET_BYTES, x25519_privkey, peerkey.base + MLKEM768_PUBLIC_KEY_BYTES)) {
+        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+        goto Exit;
+    }
+    MLKEM768_encap(ciphertext->base, secret->base, &mlkem_peer.key);
+
+    ret = 0;
+
+Exit:
+    if (ret != 0) {
+        free(ciphertext->base);
+        *ciphertext = ptls_iovec_init(NULL, 0);
+        free(secret->base);
+        *secret = ptls_iovec_init(NULL, 0);
+    }
+    ptls_clear_memory(&x25519_privkey, sizeof(x25519_privkey));
+    return ret;
+}
+
+#endif
+
 int ptls_openssl_create_key_exchange(ptls_key_exchange_context_t **ctx, EVP_PKEY *pkey)
 {
     int ret, id;
@@ -2063,6 +2195,12 @@
                                                      .exchange = evp_keyex_exchange,
                                                      .data = NID_X25519};
 #endif
+#if PTLS_OPENSSL_HAVE_X25519MLKEM768
+ptls_key_exchange_algorithm_t ptls_openssl_x25519mlkem768 = {.id = PTLS_GROUP_X25519MLKEM768,
+                                                             .name = PTLS_GROUP_NAME_X25519MLKEM768,
+                                                             .create = x25519mlkem768_create,
+                                                             .exchange = x25519mlkem768_exchange};
+#endif
 ptls_key_exchange_algorithm_t *ptls_openssl_key_exchanges[] = {&ptls_openssl_secp256r1, NULL};
 ptls_cipher_algorithm_t ptls_openssl_aes128ecb = {
     "AES128-ECB",          PTLS_AES128_KEY_SIZE, PTLS_AES_BLOCK_SIZE, 0 /* iv size */, sizeof(struct cipher_context_t),
diff --git a/t/cli.c b/t/cli.c
index 1bfd80e..dc52bae 100644
--- a/t/cli.c
+++ b/t/cli.c
@@ -409,6 +409,9 @@
 #if PTLS_OPENSSL_HAVE_X25519
            ", X25519"
 #endif
+#if PTLS_OPENSSL_HAVE_X25519MLKEM768
+           ", X5519MLKEM768"
+#endif
            "\n"
            "Supported signature algorithms: rsa, secp256r1"
 #if PTLS_OPENSSL_HAVE_SECP384R1
@@ -559,6 +562,9 @@
 #if PTLS_OPENSSL_HAVE_X25519
                 MATCH(x25519);
 #endif
+#if PTLS_OPENSSL_HAVE_X25519MLKEM768
+                MATCH(x25519mlkem768);
+#endif
 #undef MATCH
                 if (algo == NULL) {
                     fprintf(stderr, "could not find key exchange: %s\n", optarg);
diff --git a/t/openssl.c b/t/openssl.c
index b8fe391..4accc2b 100644
--- a/t/openssl.c
+++ b/t/openssl.c
@@ -141,6 +141,10 @@
     subtest("x25519-to-minicrypto", test_key_exchange, &ptls_openssl_x25519, &ptls_minicrypto_x25519);
     subtest("x25519-from-minicrypto", test_key_exchange, &ptls_minicrypto_x25519, &ptls_openssl_x25519);
 #endif
+
+#if PTLS_OPENSSL_HAVE_X25519MLKEM768
+    subtest("x25519mlkem768", test_key_exchange, &ptls_openssl_x25519mlkem768, &ptls_openssl_x25519mlkem768);
+#endif
 }
 
 static void test_sign_verify(EVP_PKEY *key, const ptls_openssl_signature_scheme_t *schemes)