Adding ECC KEM

NIIBE Yutaka <[email protected]>
Newsgroups gmane.comp.encryption.gpg.libgcrypt.devel
Message-ID <[email protected]>
Hello,

In the task T6755, we introduced KEM API.  ML-KEM is added.

Today, I'd like to propose adding ECC KEM implementation in the API.
The intention of mine is use in gpg-agent to support PQC (task T7014).

Attached is a patch adding ECC KEM for X25519.
--

_______________________________________________
Gcrypt-devel mailing list
[email protected]
https://lists.gnupg.org/mailman/listinfo/gcrypt-devel
0001-cipher-kem-Add-ECC-KEM-for-X25519.patch (text/x-diff, 35.6 KB)
>From 5da6c63fed34f6027a9531780252f0f54087c379 Mon Sep 17 00:00:00 2001
From: NIIBE Yutaka <[email protected]>
Date: Thu, 28 Mar 2024 11:52:55 +0900
Subject: [PATCH] cipher:kem: Add ECC KEM for X25519.

* cipher/Makefile.am (libcipher_la_SOURCES): Add kem-ecc.{c,h}.
* cipher/kem-ecc.c: New.
* cipher/kem-ecc.h: New.
* cipher/kem.c (_gcry_kem_keypair): Dispatch to _gcry_ecc_raw_keypair.
(_gcry_kem_encap): Dispatch to _gcry_ecc_raw_encap,
_gcry_ecc_dhkem_encap, _gcry_openpgp_kem_encap, and
_gcry_cms_kem_encap.
(_gcry_kem_decap): Dispatch to _gcry_ecc_raw_decap,
_gcry_ecc_dhkem_decap, _gcry_openpgp_kem_decap, and
_gcry_cms_kem_decap.
* src/gcrypt.h.in: Add constants for ECC KEM.
* tests/t-kem.c (test_kem_raw_x25519, test_kem_dhkem_x25519)
(test_kem_openpgp_x25519, test_kem_cms_x25519): New.
(check_kem, main): Add tests for ECC KEM.

--

GnuPG-bug-id: 6755
Signed-off-by: NIIBE Yutaka <[email protected]>
---
 cipher/Makefile.am |   2 +-
 cipher/kem-ecc.c   | 445 +++++++++++++++++++++++++++++++++++++++++++++
 cipher/kem-ecc.h   |  47 +++++
 cipher/kem.c       |  51 ++++--
 src/gcrypt.h.in    |  31 +++-
 tests/t-kem.c      | 337 +++++++++++++++++++++++++++++++++-
 6 files changed, 890 insertions(+), 23 deletions(-)
 create mode 100644 cipher/kem-ecc.c
 create mode 100644 cipher/kem-ecc.h

diff --git a/cipher/Makefile.am b/cipher/Makefile.am
index 760bdce5..6a533a25 100644
--- a/cipher/Makefile.am
+++ b/cipher/Makefile.am
@@ -60,7 +60,7 @@ libcipher_la_SOURCES = \
 	mac.c mac-internal.h \
 	mac-hmac.c mac-cmac.c mac-gmac.c mac-poly1305.c \
 	poly1305.c poly1305-internal.h \
-	kem.c sntrup761.c sntrup761.h kyber.c kyber.h \
+	kem.c sntrup761.c sntrup761.h kyber.c kyber.h kem-ecc.c kem-ecc.h \
 	kdf.c kdf-internal.h \
 	bithelp.h  \
 	bufhelp.h  \
diff --git a/cipher/kem-ecc.c b/cipher/kem-ecc.c
new file mode 100644
index 00000000..3559f7f3
--- /dev/null
+++ b/cipher/kem-ecc.c
@@ -0,0 +1,445 @@
+/* kem-ecc.c - Key Encapsulation Mechanism with ECC
+ * Copyright (C) 2024 g10 Code GmbH
+ *
+ * This file is part of Libgcrypt.
+ *
+ * Libgcrypt is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser general Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * Libgcrypt is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see <https://www.gnu.org/licenses/>.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+#include "g10lib.h"
+#include "cipher.h"
+
+#include "kem-ecc.h"
+
+static void
+ecc_tweak_bits (int curveid, unsigned char *dst, const unsigned char *src)
+{
+  if (curveid == GCRY_ECC_CURVE25519)
+    {
+      memcpy (dst, src, 32);
+      dst[0] &= 0xf8;
+      dst[31] &= 0x7f;
+      dst[31] |= 0x40;
+    }
+  else if (curveid == GCRY_ECC_CURVE448)
+    {
+      memcpy (dst, src, 56);
+      dst[0] &= 0xfc;
+      dst[55] |= 0x80;
+    }
+}
+
+static gpg_err_code_t
+ecc_mul_point (int curveid, unsigned char *result,
+               const unsigned char *scalar, const unsigned char *point)
+{
+  unsigned char scalar_fixed[56]; /* Enough space for X448 */
+
+  /* Tweak the bits, so that it can be compatible to X25519/X448
+     functions.  */
+  ecc_tweak_bits (curveid, scalar_fixed, scalar);
+  return _gcry_ecc_mul_point (curveid, result, scalar_fixed, point);
+}
+
+
+/* The generator of Curve25519.  */
+static const unsigned char curve25519_G[32] = { 0x09 };
+/* The generator of Curve448.  */
+static const unsigned char curve448_G[56] = { 0x05 };
+
+gpg_err_code_t
+_gcry_ecc_raw_keypair (int curveid, void *pubkey, void *seckey)
+{
+  unsigned char *seckey_byte = seckey;
+  unsigned int len = _gcry_ecc_get_algo_keylen (curveid);
+  const unsigned char *G;
+
+  _gcry_randomize (seckey, len, GCRY_STRONG_RANDOM);
+
+  /* Existing ECC applications with libgcrypt (like gpg-agent in
+     GnuPG) assumes that scalar is tweaked at key generation time.
+     For the possible use case where generated key with this routine
+     may be used with those, we put compatibile behavior here.  */
+  ecc_tweak_bits (curveid, seckey_byte, seckey_byte);
+
+  if (curveid == GCRY_ECC_CURVE25519)
+    G = curve25519_G;
+  else
+    G = curve448_G;
+
+  return ecc_mul_point (curveid, pubkey, seckey_byte, G);
+}
+
+gpg_err_code_t
+_gcry_ecc_raw_encap (int curveid, const void *pubkey, void *ciphertext,
+                     void *shared)
+{
+  gpg_err_code_t err;
+  unsigned char seckey_ephemeral[32];
+  void *pubkey_ephemeral = ciphertext;
+
+  err = _gcry_ecc_raw_keypair (curveid, pubkey_ephemeral, seckey_ephemeral);
+  if (err)
+    return err;
+
+  /* Do ECDH.  */
+  return ecc_mul_point (curveid, shared, seckey_ephemeral, pubkey);
+}
+
+gpg_err_code_t
+_gcry_ecc_raw_decap (int curveid, const void *seckey, const void *ciphertext,
+                     void *shared)
+{
+  /* Do ECDH.  */
+  return ecc_mul_point (curveid, shared, seckey, ciphertext);
+}
+
+
+static gpg_err_code_t
+ecc_dhkem_kdf (const unsigned char *ecdh, const unsigned char *ciphertext,
+               const unsigned char *pubkey, void *shared)
+{
+  gpg_err_code_t err;
+  unsigned char *p;
+  unsigned char labeled_ikm[7+5+7+32];
+  unsigned char labeled_info[2+7+5+13+32+32];
+  gcry_kdf_hd_t hd;
+  unsigned long param[1] = { 32 }; /* output-len */
+
+  p = labeled_ikm;
+  memcpy (p, "HPKE-v1", 7);
+  p += 7;
+  memcpy (p, "KEM\x00\x20", 5); /* suite_id */
+  p += 5;
+  memcpy (p, "eae_prk", 7);
+  p += 7;
+  memcpy (p, ecdh, 32);
+
+  p = labeled_info;
+  memcpy (p, "\x00\x20", 2);    /* length */
+  p += 2;
+  memcpy (p, "HPKE-v1", 7);
+  p += 7;
+  memcpy (p, "KEM\x00\x20", 5); /* suite_id */
+  p += 5;
+  memcpy (p, "shared_secret", 13);
+  p += 13;
+  /* kem_context */
+  memcpy (p, ciphertext, 32);
+  p += 32;
+  memcpy (p, pubkey, 32);
+  p += 32;
+
+  err = _gcry_kdf_open (&hd, GCRY_KDF_HKDF, GCRY_MAC_HMAC_SHA256, param, 1,
+                        labeled_ikm, sizeof (labeled_ikm),
+                        NULL, 0, NULL, 0, labeled_info, sizeof (labeled_info));
+  if (err)
+    return err;
+
+  err = _gcry_kdf_compute (hd, NULL);
+  if (!err)
+    err = _gcry_kdf_final (hd, 32, shared);
+  _gcry_kdf_close (hd);
+  return err;
+}
+
+
+gpg_err_code_t
+_gcry_ecc_dhkem_encap (int algo, const void *pubkey, void *ciphertext,
+                       void *shared)
+{
+  gpg_err_code_t err;
+  int curveid;
+  unsigned char ecdh[32];
+  unsigned char seckey_ephemeral[32];
+  void *pubkey_ephemeral = ciphertext;
+
+  if (algo != GCRY_KEM_DHKEM25519)
+    return GPG_ERR_UNKNOWN_ALGORITHM;
+
+  /* From here, it's only for the DHKEM(X25519, HKDF-SHA256).  */
+  curveid = GCRY_ECC_CURVE25519;
+
+  err = _gcry_ecc_raw_keypair (curveid, pubkey_ephemeral, seckey_ephemeral);
+  if (err)
+    return err;
+
+  /* Do ECDH.  */
+  err = ecc_mul_point (curveid, ecdh, seckey_ephemeral, pubkey);
+  if (err)
+    return err;
+
+  return ecc_dhkem_kdf (ecdh, ciphertext, pubkey, shared);
+}
+
+gpg_err_code_t
+_gcry_ecc_dhkem_decap (int algo, const void *seckey, const void *ciphertext,
+                       void *shared, const void *optional)
+{
+  gpg_err_code_t err;
+  int curveid;
+  unsigned char ecdh[32];
+  unsigned char pubkey_computed[32];
+  const unsigned char *pubkey;
+
+  if (algo != GCRY_KEM_DHKEM25519)
+    return GPG_ERR_UNKNOWN_ALGORITHM;
+
+  /* From here, it's only for the DHKEM(X25519, HKDF-SHA256).  */
+  curveid = GCRY_ECC_CURVE25519;
+
+  if (optional)
+    pubkey = optional;
+  else
+    {
+      err = ecc_mul_point (curveid, pubkey_computed, seckey, curve25519_G);
+      if (err)
+        return err;
+
+      pubkey = pubkey_computed;
+    }
+
+  /* Do ECDH.  */
+  err = ecc_mul_point (curveid, ecdh, seckey, ciphertext);
+  if (err)
+    return err;
+
+  return ecc_dhkem_kdf (ecdh, ciphertext, pubkey, shared);
+}
+
+
+static gpg_err_code_t
+openpgp_kem_kdf (const unsigned char *ecdh, size_t ecdh_len,
+                 const unsigned char *kdf_param, void *shared)
+{
+  gpg_err_code_t err;
+  gcry_kdf_hd_t hd;
+  unsigned long param[1];
+  int curve_oid_len;
+  int hash_id;
+  int kek_id;
+  size_t z_len;
+  size_t kdf_param_len;
+
+  if (kdf_param == NULL)
+    return GPG_ERR_INV_VALUE;
+
+  curve_oid_len = kdf_param[0];
+  hash_id = kdf_param[1+curve_oid_len+3];
+  kek_id = kdf_param[1+curve_oid_len+4];
+  kdf_param_len = 1+curve_oid_len+5+20+20;
+
+  err = _gcry_cipher_algo_info (kek_id, GCRYCTL_GET_KEYLEN, NULL, &z_len);
+  if (err)
+    return err;
+
+  param[0] = z_len;
+
+  err = _gcry_kdf_open (&hd, GCRY_KDF_ONESTEP_KDF, hash_id, param, 1,
+                        ecdh, ecdh_len,
+                        NULL, 0, NULL, 0, kdf_param, kdf_param_len);
+  if (err)
+    return err;
+
+  err = _gcry_kdf_compute (hd, NULL);
+  if (!err)
+    err = _gcry_kdf_final (hd, z_len, shared);
+  _gcry_kdf_close (hd);
+
+  return err;
+}
+
+/* In OpenPGP v4, 0x40 is prepended to the native encoding of public
+   key.  Here, PUBKEY and CIPHERTEXT are native representation sans
+   the prefix.  */
+gpg_err_code_t
+_gcry_openpgp_kem_encap (int algo, const void *pubkey, void *ciphertext,
+                         void *shared, const void *optional)
+{
+  gpg_err_code_t err;
+  int curveid;
+  unsigned char ecdh[32];
+  const unsigned char *kdf_param = optional;
+  unsigned char seckey_ephemeral[32];
+  void *pubkey_ephemeral = ciphertext;
+
+  if (algo != GCRY_KEM_OPENPGP_X25519)
+    return GPG_ERR_UNKNOWN_ALGORITHM;
+
+  /* From here, it's only for the OpenPGP KEM(Curve25519, One-Step KDF).  */
+  curveid = GCRY_ECC_CURVE25519;
+
+  err = _gcry_ecc_raw_keypair (curveid, pubkey_ephemeral, seckey_ephemeral);
+  if (err)
+    return err;
+
+  /* Do ECDH.  */
+  err = ecc_mul_point (curveid, ecdh, seckey_ephemeral, pubkey);
+  if (err)
+    return err;
+
+  return openpgp_kem_kdf (ecdh, sizeof (ecdh), kdf_param, shared);
+}
+
+/* In OpenPGP v4, secret key is represented with big-endian MPI.
+   Here, SECKEY is native fixed size little-endian representation.
+   CIPHERTEXT is native representation sans the prefix 0x40.
+  */
+gpg_err_code_t
+_gcry_openpgp_kem_decap (int algo, const void *seckey, const void *ciphertext,
+                         void *shared, const void *optional)
+{
+  gpg_err_code_t err;
+  int curveid;
+  unsigned char ecdh[32];
+  const unsigned char *kdf_param = optional;
+
+  if (algo != GCRY_KEM_OPENPGP_X25519)
+    return GPG_ERR_UNKNOWN_ALGORITHM;
+
+  /* From here, it's only for the OpenPGP KEM(Curve25519, One-Step KDF).  */
+  curveid = GCRY_ECC_CURVE25519;
+
+  /* Do ECDH.  */
+  err = ecc_mul_point (curveid, ecdh, seckey, ciphertext);
+  if (err)
+    return err;
+
+  return openpgp_kem_kdf (ecdh, sizeof (ecdh), kdf_param, shared);
+}
+
+
+static gpg_err_code_t
+cms_kem_kdf (int kdf_id, int hash_id,
+             const unsigned char *ecdh, size_t ecdh_len,
+             const unsigned char *sharedinfo, void *shared)
+{
+  gpg_err_code_t err;
+  gcry_kdf_hd_t hd;
+  unsigned long param[1];
+  unsigned int sharedinfolen;
+  const unsigned char *supppubinfo;
+  unsigned int keylen;
+
+  /*
+   *  ECC-CMS-SharedInfo ::= SEQUENCE {
+   *      keyInfo         AlgorithmIdentifier,
+   *      entityUInfo [0] EXPLICIT OCTET STRING OPTIONAL,
+   *      suppPubInfo [2] EXPLICIT OCTET STRING  }
+   */
+  if (!sharedinfo)
+    return GPG_ERR_INV_VALUE;
+
+  if (sharedinfo[0] != 0x30 /* Constructed | SEQUENCE */
+      || sharedinfo[1] >= 0x80)
+    return GPG_ERR_INV_VALUE;
+
+  sharedinfolen = sharedinfo[1] + 2;
+
+  /* Extract KEYLEN for keywrap from suppPubInfo.  */
+  supppubinfo = sharedinfo + sharedinfolen - 8;
+  if (supppubinfo[0] != 0xA2 /* CLASS_CONTEXT | Constructed | 2 */
+      || supppubinfo[1] != 6
+      || supppubinfo[2] != 0x04 /* OCTET STRING */
+      || supppubinfo[3] != 4)
+    return GPG_ERR_INV_VALUE;
+
+  keylen = ((((supppubinfo[4] << 24) | (supppubinfo[5] << 16)
+              | (supppubinfo[6] << 8) | supppubinfo[7])) + 7) / 8;
+
+  param[0] = keylen;
+
+  err = _gcry_kdf_open (&hd, kdf_id, hash_id, param, 1,
+                        ecdh, ecdh_len,
+                        NULL, 0, NULL, 0, sharedinfo, sharedinfolen);
+  if (err)
+    return err;
+
+  err = _gcry_kdf_compute (hd, NULL);
+  if (!err)
+    err = _gcry_kdf_final (hd, keylen, shared);
+  _gcry_kdf_close (hd);
+
+  return err;
+}
+
+gpg_err_code_t
+_gcry_cms_kem_encap (int algo, const void *pubkey, void *ciphertext,
+                     void *shared, const void *optional)
+{
+  gpg_err_code_t err;
+  int curveid;
+  unsigned char ecdh[32];
+  const unsigned char *sharedinfo = optional;
+  unsigned char seckey_ephemeral[32];
+  void *pubkey_ephemeral = ciphertext;
+  int kdf_method;
+
+  if (algo == GCRY_KEM_CMS_X25519_X963_SHA256)
+    kdf_method = GCRY_KDF_X963_KDF;
+  else if (algo == GCRY_KEM_CMS_X25519_HKDF_SHA256)
+    kdf_method = GCRY_KDF_HKDF;
+  else
+    return GPG_ERR_UNKNOWN_ALGORITHM;
+
+  curveid = GCRY_ECC_CURVE25519;
+
+  err = _gcry_ecc_raw_keypair (curveid, pubkey_ephemeral, seckey_ephemeral);
+  if (err)
+    return err;
+
+  /* Do ECDH.  */
+  err = ecc_mul_point (curveid, ecdh, seckey_ephemeral, pubkey);
+  if (err)
+    return err;
+
+  return cms_kem_kdf (kdf_method, GCRY_MD_SHA256, ecdh, sizeof (ecdh),
+                      sharedinfo, shared);
+}
+
+gpg_err_code_t
+_gcry_cms_kem_decap (int algo, const void *seckey, const void *ciphertext,
+                     void *shared, const void *optional)
+{
+  gpg_err_code_t err;
+  int curveid;
+  unsigned char ecdh[32];
+  const unsigned char *sharedinfo = optional;
+  int kdf_method;
+
+  if (algo == GCRY_KEM_CMS_X25519_X963_SHA256)
+    kdf_method = GCRY_KDF_X963_KDF;
+  else if (algo == GCRY_KEM_CMS_X25519_HKDF_SHA256)
+    kdf_method = GCRY_KDF_HKDF;
+  else
+    return GPG_ERR_UNKNOWN_ALGORITHM;
+
+  curveid = GCRY_ECC_CURVE25519;
+
+  /* Do ECDH.  */
+  err = ecc_mul_point (curveid, ecdh, seckey, ciphertext);
+  if (err)
+    return err;
+
+  return cms_kem_kdf (kdf_method, GCRY_MD_SHA256, ecdh, sizeof (ecdh),
+                      sharedinfo, shared);
+}
diff --git a/cipher/kem-ecc.h b/cipher/kem-ecc.h
new file mode 100644
index 00000000..79a6925e
--- /dev/null
+++ b/cipher/kem-ecc.h
@@ -0,0 +1,47 @@
+/* kem-ecc.h - Key Encapsulation Mechanism with ECC
+ * Copyright (C) 2024 g10 Code GmbH
+ *
+ * This file is part of Libgcrypt.
+ *
+ * Libgcrypt is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser general Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * Libgcrypt is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see <https://www.gnu.org/licenses/>.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ */
+
+gpg_err_code_t _gcry_ecc_raw_keypair (int algo, void *pubkey, void *seckey);
+gpg_err_code_t _gcry_ecc_raw_encap (int curveid, const void *pubkey,
+                                    void *ciphertext, void *shared);
+gpg_err_code_t _gcry_ecc_raw_decap (int curveid, const void *seckey,
+                                    const void *ciphertext, void *shared);
+
+gpg_err_code_t _gcry_ecc_dhkem_encap (int algo, const void *pubkey,
+                                      void *ciphertext,
+                                      void *shared);
+gpg_err_code_t _gcry_ecc_dhkem_decap (int algo, const void *seckey,
+                                      const void *ciphertext,
+                                      void *shared, const void *optional);
+
+gpg_err_code_t _gcry_openpgp_kem_encap (int algo, const void *pubkey,
+                                        void *ciphertext,
+                                        void *shared, const void *optional);
+gpg_err_code_t _gcry_openpgp_kem_decap (int algo, const void *seckey,
+                                        const void *ciphertext,
+                                        void *shared, const void *optional);
+
+gpg_err_code_t _gcry_cms_kem_encap (int algo, const void *pubkey,
+                                    void *ciphertext,
+                                    void *shared, const void *optional);
+gpg_err_code_t _gcry_cms_kem_decap (int algo, const void *seckey,
+                                    const void *ciphertext,
+                                    void *shared, const void *optional);
diff --git a/cipher/kem.c b/cipher/kem.c
index 4511b230..c58123f9 100644
--- a/cipher/kem.c
+++ b/cipher/kem.c
@@ -29,6 +29,7 @@
 #include "cipher.h"
 #include "sntrup761.h"
 #include "kyber.h"
+#include "kem-ecc.h"
 
 
 /* Information about the the KEM algoithms for use by the s-expression
@@ -114,9 +115,14 @@ _gcry_kem_keypair (int algo,
       kyber_keypair (algo, pubkey, seckey);
       return 0;
 
+    case GCRY_KEM_RAW_X25519:
     case GCRY_KEM_DHKEM25519:
     case GCRY_KEM_OPENPGP_X25519:
-      return GPG_ERR_NOT_IMPLEMENTED;
+    case GCRY_KEM_CMS_X25519_X963_SHA256:
+    case GCRY_KEM_CMS_X25519_HKDF_SHA256:
+      return _gcry_ecc_raw_keypair (GCRY_ECC_CURVE25519, pubkey, seckey);
+    default:
+      return GPG_ERR_UNKNOWN_ALGORITHM;
     }
 
   return GPG_ERR_UNKNOWN_ALGORITHM;
@@ -140,7 +146,7 @@ _gcry_kem_encap (int algo,
           || shared_len != GCRY_KEM_SNTRUP761_SHARED_LEN)
         return GPG_ERR_INV_VALUE;
       sntrup761_enc (ciphertext, shared, pubkey, NULL, sntrup761_random);
-      return GPG_ERR_NO_ERROR;
+      return 0;
 
     case GCRY_KEM_MLKEM512:
     case GCRY_KEM_MLKEM768:
@@ -148,16 +154,26 @@ _gcry_kem_encap (int algo,
       if (optional != NULL)
         return GPG_ERR_INV_VALUE;
       kyber_encap (algo, ciphertext, shared, pubkey);
-      return GPG_ERR_NO_ERROR;
+      return 0;
 
+    case GCRY_KEM_RAW_X25519:
+      if (optional != NULL)
+        return GPG_ERR_INV_VALUE;
+      return _gcry_ecc_raw_encap (GCRY_ECC_CURVE25519, pubkey, ciphertext,
+                                  shared);
     case GCRY_KEM_DHKEM25519:
       if (optional != NULL)
         return GPG_ERR_INV_VALUE;
-      return GPG_ERR_NOT_IMPLEMENTED;
-
+      return _gcry_ecc_dhkem_encap (algo, pubkey, ciphertext, shared);
     case GCRY_KEM_OPENPGP_X25519:
-      return GPG_ERR_NOT_IMPLEMENTED;
-
+      return _gcry_openpgp_kem_encap (algo, pubkey, ciphertext, shared,
+                                      optional);
+    case GCRY_KEM_CMS_X25519_X963_SHA256:
+    case GCRY_KEM_CMS_X25519_HKDF_SHA256:
+      return _gcry_cms_kem_encap (algo, pubkey, ciphertext, shared,
+                                  optional);
+    default:
+      return GPG_ERR_UNKNOWN_ALGORITHM;
     }
   return GPG_ERR_UNKNOWN_ALGORITHM;
 }
@@ -180,7 +196,7 @@ _gcry_kem_decap (int algo,
           || shared_len != GCRY_KEM_SNTRUP761_SHARED_LEN)
         return GPG_ERR_INV_VALUE;
       sntrup761_dec (shared, ciphertext, seckey);
-      return GPG_ERR_NO_ERROR;
+      return 0;
 
     case GCRY_KEM_MLKEM512:
     case GCRY_KEM_MLKEM768:
@@ -188,12 +204,25 @@ _gcry_kem_decap (int algo,
       if (optional != NULL)
         return GPG_ERR_INV_VALUE;
       kyber_decap (algo, shared, ciphertext, seckey);
-      return GPG_ERR_NO_ERROR;
+      return 0;
 
+    case GCRY_KEM_RAW_X25519:
+      if (optional != NULL)
+        return GPG_ERR_INV_VALUE;
+      return _gcry_ecc_raw_decap (GCRY_ECC_CURVE25519, seckey, ciphertext,
+                                  shared);
     case GCRY_KEM_DHKEM25519:
+      return _gcry_ecc_dhkem_decap (algo, seckey, ciphertext, shared,
+                                    optional);
     case GCRY_KEM_OPENPGP_X25519:
-      return GPG_ERR_NOT_IMPLEMENTED;
-
+      return _gcry_openpgp_kem_decap (algo, seckey, ciphertext, shared,
+                                      optional);
+    case GCRY_KEM_CMS_X25519_X963_SHA256:
+    case GCRY_KEM_CMS_X25519_HKDF_SHA256:
+      return _gcry_cms_kem_decap (algo, seckey, ciphertext, shared,
+                                  optional);
+    default:
+      return GPG_ERR_UNKNOWN_ALGORITHM;
     }
   return GPG_ERR_UNKNOWN_ALGORITHM;
 }
diff --git a/src/gcrypt.h.in b/src/gcrypt.h.in
index a9d6b87f..0593b677 100644
--- a/src/gcrypt.h.in
+++ b/src/gcrypt.h.in
@@ -1714,8 +1714,15 @@ enum gcry_kem_algos
     GCRY_KEM_MLKEM512,       /* aka Kyber512  */
     GCRY_KEM_MLKEM768,       /* aka Kyber768  */
     GCRY_KEM_MLKEM1024,      /* aka Kyber1024 */
-    GCRY_KEM_DHKEM25519,     /* DHKEM with X25519, HKDF, and SHA256 */
-    GCRY_KEM_OPENPGP_X25519  /* For OpenPGP with Curve25519 */
+    GCRY_KEM_RAW_X25519,  /* keypair, encaps/decaps with Identity KDF */
+    GCRY_KEM_DHKEM25519,  /* DHKEM with X25519, HKDF, and SHA256  */
+    GCRY_KEM_OPENPGP_X25519,  /* For OpenPGP, OPTIONAL (KDF parameters) is
+                                 required, which defines hash and
+                                 outputlen (shared secret length) */
+    GCRY_KEM_CMS_X25519_X963_SHA256, /* CMS with X25519, X963 and SHA256 */
+                          /* For CMS, OPTIONAL (sharedinfo) is required,
+                             which defines keylen for keywrap.  */
+    GCRY_KEM_CMS_X25519_HKDF_SHA256, /* CMS with X25519, HKDF and SHA256 */
   };
 
 /*
@@ -1730,26 +1737,40 @@ enum gcry_kem_algos
 #define GCRY_KEM_SNTRUP761_SECKEY_LEN   1763
 #define GCRY_KEM_SNTRUP761_PUBKEY_LEN   1158
 #define GCRY_KEM_SNTRUP761_ENCAPS_LEN   1039
+#define GCRY_KEM_SNTRUP761_CIPHER_LEN   GCRY_KEM_SNTRUP761_ENCAPS_LEN
 #define GCRY_KEM_SNTRUP761_SHARED_LEN   32
 
 #define GCRY_KEM_MLKEM512_SECKEY_LEN    (2*384+2*384+32+2*32)
 #define GCRY_KEM_MLKEM512_PUBKEY_LEN    (2*384+32)
 #define GCRY_KEM_MLKEM512_ENCAPS_LEN    (128+2*320)
+#define GCRY_KEM_MLKEM512_CIPHER_LEN    GCRY_KEM_MLKEM512_ENCAPS_LEN
 #define GCRY_KEM_MLKEM512_SHARED_LEN    32
 
 #define GCRY_KEM_MLKEM768_SECKEY_LEN    (3*384+3*384+32+2*32)
 #define GCRY_KEM_MLKEM768_PUBKEY_LEN    (3*384+32)
 #define GCRY_KEM_MLKEM768_ENCAPS_LEN    (128+3*320)
+#define GCRY_KEM_MLKEM768_CIPHER_LEN    GCRY_KEM_MLKEM768_ENCAPS_LEN
 #define GCRY_KEM_MLKEM768_SHARED_LEN    32
 
 #define GCRY_KEM_MLKEM1024_SECKEY_LEN   (4*384+4*384+32+2*32)
 #define GCRY_KEM_MLKEM1024_PUBKEY_LEN   (4*384+32)
 #define GCRY_KEM_MLKEM1024_ENCAPS_LEN   (160+4*352)
+#define GCRY_KEM_MLKEM1024_CIPHER_LEN   GCRY_KEM_MLKEM1024_ENCAPS_LEN
 #define GCRY_KEM_MLKEM1024_SHARED_LEN   32
 
-#define GCRY_KEM_DHKEM25519_SECKEY_LEN  32
-#define GCRY_KEM_DHKEM25519_PUBKEY_LEN  32
-#define GCRY_KEM_DHKEM25519_ENCAPS_LEN  32
+/* For ECC, seckey, pubkey, and ciphertext is defined by the cureve.  */
+#define GCRY_KEM_ECC_X25519_SECKEY_LEN  32
+#define GCRY_KEM_ECC_X25519_PUBKEY_LEN  32
+#define GCRY_KEM_ECC_X25519_ENCAPS_LEN  32
+#define GCRY_KEM_ECC_X25519_CIPHER_LEN  GCRY_KEM_ECC_X25519_ENCAPS_LEN
+/* And shared secret is specific to the protocol.  For OpenPGP and
+   CMS, it is determined by the OPTIONAL argument.  */
+#define GCRY_KEM_RAW_X25519_SHARED_LEN  32
+
+#define GCRY_KEM_DHKEM25519_SECKEY_LEN  GCRY_KEM_ECC_X25519_SECKEY_LEN
+#define GCRY_KEM_DHKEM25519_PUBKEY_LEN  GCRY_KEM_ECC_X25519_PUBKEY_LEN
+#define GCRY_KEM_DHKEM25519_ENCAPS_LEN  GCRY_KEM_ECC_X25519_ENCAPS_LEN
+#define GCRY_KEM_DHKEM25519_CIPHER_LEN  GCRY_KEM_DHKEM25519_ENCAPS_LEN
 #define GCRY_KEM_DHKEM25519_SHARED_LEN  32
 
 /* Generate a new key pair with ALGO.  */
diff --git a/tests/t-kem.c b/tests/t-kem.c
index 3813d350..70f4d7ec 100644
--- a/tests/t-kem.c
+++ b/tests/t-kem.c
@@ -262,10 +262,279 @@ test_kem_mlkem1024 (int testno)
     }
 }
 
-#define SELECTED_ALGO_SNTRUP761 (1 << 0)
-#define SELECTED_ALGO_MLKEM512  (1 << 1)
-#define SELECTED_ALGO_MLKEM768  (1 << 2)
-#define SELECTED_ALGO_MLKEM1024 (1 << 3)
+
+static void
+test_kem_raw_x25519 (int testno)
+{
+  gcry_error_t err;
+  uint8_t pubkey[GCRY_KEM_ECC_X25519_PUBKEY_LEN];
+  uint8_t seckey[GCRY_KEM_ECC_X25519_SECKEY_LEN];
+  uint8_t ciphertext[GCRY_KEM_ECC_X25519_ENCAPS_LEN];
+  uint8_t key1[GCRY_KEM_RAW_X25519_SHARED_LEN];
+  uint8_t key2[GCRY_KEM_RAW_X25519_SHARED_LEN];
+
+  err = gcry_kem_keypair (GCRY_KEM_RAW_X25519,
+                          pubkey, GCRY_KEM_ECC_X25519_PUBKEY_LEN,
+                          seckey, GCRY_KEM_ECC_X25519_SECKEY_LEN);
+  if (err)
+    {
+      fail ("gcry_kem_keypair %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  err = gcry_kem_encap (GCRY_KEM_RAW_X25519,
+                        pubkey, GCRY_KEM_ECC_X25519_PUBKEY_LEN,
+                        ciphertext, GCRY_KEM_ECC_X25519_ENCAPS_LEN,
+                        key1, GCRY_KEM_RAW_X25519_SHARED_LEN,
+                        NULL, 0);
+  if (err)
+    {
+      fail ("gcry_kem_encap %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  err = gcry_kem_decap (GCRY_KEM_RAW_X25519,
+                        seckey, GCRY_KEM_ECC_X25519_SECKEY_LEN,
+                        ciphertext, GCRY_KEM_ECC_X25519_ENCAPS_LEN,
+                        key2, GCRY_KEM_RAW_X25519_SHARED_LEN,
+                        NULL, 0);
+  if (err)
+    {
+      fail ("gcry_kem_decap %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  if (memcmp (key1, key2, GCRY_KEM_RAW_X25519_SHARED_LEN) != 0)
+    {
+      size_t i;
+
+      fail ("raw-x25519 test %d failed: mismatch\n", testno);
+      fputs ("key1:", stderr);
+      for (i = 0; i < GCRY_KEM_RAW_X25519_SHARED_LEN; i++)
+        fprintf (stderr, " %02x", key1[i]);
+      putc ('\n', stderr);
+      fputs ("key2:", stderr);
+      for (i = 0; i < GCRY_KEM_RAW_X25519_SHARED_LEN; i++)
+        fprintf (stderr, " %02x", key2[i]);
+      putc ('\n', stderr);
+    }
+}
+
+
+static void
+test_kem_dhkem_x25519 (int testno)
+{
+  gcry_error_t err;
+  uint8_t pubkey[GCRY_KEM_DHKEM25519_PUBKEY_LEN];
+  uint8_t seckey[GCRY_KEM_DHKEM25519_SECKEY_LEN];
+  uint8_t ciphertext[GCRY_KEM_DHKEM25519_ENCAPS_LEN];
+  uint8_t key1[GCRY_KEM_DHKEM25519_SHARED_LEN];
+  uint8_t key2[GCRY_KEM_DHKEM25519_SHARED_LEN];
+
+  err = gcry_kem_keypair (GCRY_KEM_DHKEM25519,
+                          pubkey, GCRY_KEM_DHKEM25519_PUBKEY_LEN,
+                          seckey, GCRY_KEM_DHKEM25519_SECKEY_LEN);
+  if (err)
+    {
+      fail ("gcry_kem_keypair %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  err = gcry_kem_encap (GCRY_KEM_DHKEM25519,
+                        pubkey, GCRY_KEM_DHKEM25519_PUBKEY_LEN,
+                        ciphertext, GCRY_KEM_DHKEM25519_ENCAPS_LEN,
+                        key1, GCRY_KEM_DHKEM25519_SHARED_LEN,
+                        NULL, 0);
+  if (err)
+    {
+      fail ("gcry_kem_encap %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  err = gcry_kem_decap (GCRY_KEM_DHKEM25519,
+                        seckey, GCRY_KEM_DHKEM25519_SECKEY_LEN,
+                        ciphertext, GCRY_KEM_DHKEM25519_ENCAPS_LEN,
+                        key2, GCRY_KEM_DHKEM25519_SHARED_LEN,
+                        pubkey, GCRY_KEM_DHKEM25519_PUBKEY_LEN);
+  if (err)
+    {
+      fail ("gcry_kem_decap %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  if (memcmp (key1, key2, GCRY_KEM_DHKEM25519_SHARED_LEN) != 0)
+    {
+      size_t i;
+
+      fail ("dhkem-x25519 test %d failed: mismatch\n", testno);
+      fputs ("key1:", stderr);
+      for (i = 0; i < GCRY_KEM_DHKEM25519_SHARED_LEN; i++)
+        fprintf (stderr, " %02x", key1[i]);
+      putc ('\n', stderr);
+      fputs ("key2:", stderr);
+      for (i = 0; i < GCRY_KEM_DHKEM25519_SHARED_LEN; i++)
+        fprintf (stderr, " %02x", key2[i]);
+      putc ('\n', stderr);
+    }
+}
+
+/* In the following case, with AES128-keywrap, shared secret length is 16.  */
+#define MY_KEM_OPENPGP_X25519_SHARED_LEN 16
+
+static void
+test_kem_openpgp_x25519 (int testno)
+{
+  gcry_error_t err;
+  uint8_t pubkey[GCRY_KEM_ECC_X25519_PUBKEY_LEN];
+  uint8_t seckey[GCRY_KEM_ECC_X25519_SECKEY_LEN];
+  uint8_t ciphertext[GCRY_KEM_ECC_X25519_ENCAPS_LEN];
+  uint8_t key1[MY_KEM_OPENPGP_X25519_SHARED_LEN];
+  uint8_t key2[MY_KEM_OPENPGP_X25519_SHARED_LEN];
+  const uint8_t kdf_param[56] = {
+    0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55,
+    0x01, 0x05, 0x01,
+    /**/
+    0x12,
+    /**/
+    0x03, 0x01, 0x08 /*SHA256*/, 0x07 /* AES128 */,
+    /**/
+    0x41, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75,
+    0x73, 0x20, 0x53, 0x65, 0x6e, 0x64, 0x65, 0x72,
+    0x20, 0x20, 0x20, 0x20, /* "Anonymous Sender    " */
+    /**/
+    0x25, 0xd4, 0x45, 0xfa, 0xc1, 0x96, 0x49, 0xc4,
+    0x6a, 0x6b, 0x2f, 0xb3, 0xcd, 0xfc, 0x22, 0x19,
+    0xc5, 0x53, 0xd3, 0x92  /* public key fingerprint */
+  };
+
+  err = gcry_kem_keypair (GCRY_KEM_OPENPGP_X25519,
+                          pubkey, GCRY_KEM_ECC_X25519_PUBKEY_LEN,
+                          seckey, GCRY_KEM_ECC_X25519_SECKEY_LEN);
+  if (err)
+    {
+      fail ("gcry_kem_keypair %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  err = gcry_kem_encap (GCRY_KEM_OPENPGP_X25519,
+                        pubkey, GCRY_KEM_ECC_X25519_PUBKEY_LEN,
+                        ciphertext, GCRY_KEM_ECC_X25519_ENCAPS_LEN,
+                        key1, MY_KEM_OPENPGP_X25519_SHARED_LEN,
+                        kdf_param, sizeof (kdf_param));
+  if (err)
+    {
+      fail ("gcry_kem_encap %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  err = gcry_kem_decap (GCRY_KEM_OPENPGP_X25519,
+                        seckey, GCRY_KEM_ECC_X25519_SECKEY_LEN,
+                        ciphertext, GCRY_KEM_ECC_X25519_ENCAPS_LEN,
+                        key2, MY_KEM_OPENPGP_X25519_SHARED_LEN,
+                        kdf_param, sizeof (kdf_param));
+  if (err)
+    {
+      fail ("gcry_kem_decap %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  if (memcmp (key1, key2, MY_KEM_OPENPGP_X25519_SHARED_LEN) != 0)
+    {
+      size_t i;
+
+      fail ("openpgp-x25519 test %d failed: mismatch\n", testno);
+      fputs ("key1:", stderr);
+      for (i = 0; i < MY_KEM_OPENPGP_X25519_SHARED_LEN; i++)
+        fprintf (stderr, " %02x", key1[i]);
+      putc ('\n', stderr);
+      fputs ("key2:", stderr);
+      for (i = 0; i < MY_KEM_OPENPGP_X25519_SHARED_LEN; i++)
+        fprintf (stderr, " %02x", key2[i]);
+      putc ('\n', stderr);
+    }
+}
+
+
+/* In the following case, with AES128-keywrap, shared secret length is 16.  */
+#define MY_KEM_CMS_X25519_SHARED_LEN 16
+
+static void
+test_kem_cms_x25519 (int testno)
+{
+  gcry_error_t err;
+  uint8_t pubkey[GCRY_KEM_ECC_X25519_PUBKEY_LEN];
+  uint8_t seckey[GCRY_KEM_ECC_X25519_SECKEY_LEN];
+  uint8_t ciphertext[GCRY_KEM_ECC_X25519_ENCAPS_LEN];
+  uint8_t key1[MY_KEM_CMS_X25519_SHARED_LEN];
+  uint8_t key2[MY_KEM_CMS_X25519_SHARED_LEN];
+  const uint8_t sharedinfo[23] = {
+    0x30, 0x15 , /* SEQUENCE */
+          0x30, 0x0B, /* SEQUENCE */
+              /* OID */
+                0x06, 0x09, /* OBJECT_ID */
+                      0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x05,
+          /* CONTEXT 2*/
+          0xA2, 0x06,
+                      0x04, 0x04, /* OCTET STRING */
+                      0x00, 0x00, 0x00, 0x80
+  };
+
+  err = gcry_kem_keypair (GCRY_KEM_CMS_X25519_X963_SHA256,
+                          pubkey, GCRY_KEM_ECC_X25519_PUBKEY_LEN,
+                          seckey, GCRY_KEM_ECC_X25519_SECKEY_LEN);
+  if (err)
+    {
+      fail ("gcry_kem_keypair %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  err = gcry_kem_encap (GCRY_KEM_CMS_X25519_X963_SHA256,
+                        pubkey, GCRY_KEM_ECC_X25519_PUBKEY_LEN,
+                        ciphertext, GCRY_KEM_ECC_X25519_ENCAPS_LEN,
+                        key1, MY_KEM_CMS_X25519_SHARED_LEN,
+                        sharedinfo, sizeof (sharedinfo));
+  if (err)
+    {
+      fail ("gcry_kem_encap %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  err = gcry_kem_decap (GCRY_KEM_CMS_X25519_X963_SHA256,
+                        seckey, GCRY_KEM_ECC_X25519_SECKEY_LEN,
+                        ciphertext, GCRY_KEM_ECC_X25519_ENCAPS_LEN,
+                        key2, MY_KEM_CMS_X25519_SHARED_LEN,
+                        sharedinfo, sizeof (sharedinfo));
+  if (err)
+    {
+      fail ("gcry_kem_decap %d: %s", testno, gpg_strerror (err));
+      return;
+    }
+
+  if (memcmp (key1, key2, MY_KEM_CMS_X25519_SHARED_LEN) != 0)
+    {
+      size_t i;
+
+      fail ("openpgp-x25519 test %d failed: mismatch\n", testno);
+      fputs ("key1:", stderr);
+      for (i = 0; i < MY_KEM_CMS_X25519_SHARED_LEN; i++)
+        fprintf (stderr, " %02x", key1[i]);
+      putc ('\n', stderr);
+      fputs ("key2:", stderr);
+      for (i = 0; i < MY_KEM_CMS_X25519_SHARED_LEN; i++)
+        fprintf (stderr, " %02x", key2[i]);
+      putc ('\n', stderr);
+    }
+}
+
+
+#define SELECTED_ALGO_SNTRUP761  (1 << 0)
+#define SELECTED_ALGO_MLKEM512   (1 << 1)
+#define SELECTED_ALGO_MLKEM768   (1 << 2)
+#define SELECTED_ALGO_MLKEM1024  (1 << 3)
+#define SELECTED_ALGO_RAW_X25519 (1 << 4)
+#define SELECTED_ALGO_DHKEM25519 (1 << 5)
+#define SELECTED_ALGO_OPENPGP_X25519 (1 << 6)
+#define SELECTED_ALGO_CMS_X25519 (1 << 7)
 static unsigned int selected_algo;
 
 static void
@@ -306,6 +575,34 @@ check_kem (int n_loops)
       ntests += n_loops;
     }
 
+  if ((selected_algo & SELECTED_ALGO_RAW_X25519))
+    {
+      for (; testno < ntests + n_loops; testno++)
+        test_kem_raw_x25519 (testno);
+      ntests += n_loops;
+    }
+
+  if ((selected_algo & SELECTED_ALGO_DHKEM25519))
+    {
+      for (; testno < ntests + n_loops; testno++)
+        test_kem_dhkem_x25519 (testno);
+      ntests += n_loops;
+    }
+
+  if ((selected_algo & SELECTED_ALGO_OPENPGP_X25519))
+    {
+      for (; testno < ntests + n_loops; testno++)
+        test_kem_openpgp_x25519 (testno);
+      ntests += n_loops;
+    }
+
+  if ((selected_algo & SELECTED_ALGO_CMS_X25519))
+    {
+      for (; testno < ntests + n_loops; testno++)
+        test_kem_cms_x25519 (testno);
+      ntests += n_loops;
+    }
+
   show_note ("%d tests done\n", ntests);
 }
 
@@ -315,7 +612,6 @@ main (int argc, char **argv)
   int last_argc = -1;
   int n_loops = N_TESTS;
 
-  selected_algo = ~0;           /* Default is all algos.  */
 
   if (argc)
     {
@@ -323,6 +619,8 @@ main (int argc, char **argv)
       argv++;
     }
 
+  selected_algo = ~0;           /* Default is all algos.  */
+
   while (argc && last_argc != argc)
     {
       last_argc = argc;
@@ -343,7 +641,10 @@ main (int argc, char **argv)
                  "  --sntrup761     select SNTRUP761 algo\n"
                  "  --mlkem512      select MLKEM512 algo\n"
                  "  --mlkem768      select MLKEM768 algo\n"
-                 "  --mlkem1024     select MLKEM1024 algo\n",
+                 "  --mlkem1024     select MLKEM1024 algo\n"
+                 "  --dhkem25519    select DHKEM25519 algo\n"
+                 "  --pgp-x25519    select PGP_X25519 algo\n"
+                 "  --cms-x25519    select CMS_X25519_X963 algo\n",
                  stdout);
           exit (0);
         }
@@ -392,6 +693,30 @@ main (int argc, char **argv)
           argc--;
           argv++;
         }
+      else if (!strcmp (*argv, "--raw-x25519"))
+        {
+          selected_algo = SELECTED_ALGO_RAW_X25519;
+          argc--;
+          argv++;
+        }
+      else if (!strcmp (*argv, "--dhkem25519"))
+        {
+          selected_algo = SELECTED_ALGO_DHKEM25519;
+          argc--;
+          argv++;
+        }
+      else if (!strcmp (*argv, "--pgp-x25519"))
+        {
+          selected_algo = SELECTED_ALGO_OPENPGP_X25519;
+          argc--;
+          argv++;
+        }
+      else if (!strcmp (*argv, "--cms-x25519"))
+        {
+          selected_algo = SELECTED_ALGO_CMS_X25519;
+          argc--;
+          argv++;
+        }
       else if (!strncmp (*argv, "--", 2))
         die ("unknown option '%s'", *argv);
     }
-- 
2.39.2
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.