summaryrefslogtreecommitdiffstats
path: root/agent
diff options
context:
space:
mode:
authorNIIBE Yutaka <gniibe@fsij.org>2024-04-05 07:17:25 +0200
committerNIIBE Yutaka <gniibe@fsij.org>2024-04-05 07:17:25 +0200
commit131dd2a35145a1db1a45ab76764f32cbbca3fd43 (patch)
tree7ef3fa4053fe64cc1e8eb265839c9c1a92343b81 /agent
parentgpg: Initial support for generating Kyber subkeys. (diff)
downloadgnupg2-131dd2a35145a1db1a45ab76764f32cbbca3fd43.tar.xz
gnupg2-131dd2a35145a1db1a45ab76764f32cbbca3fd43.zip
agent: Add initial support for hybrid ECC+PQC decryption with KEM.
* agent/agent.h (enum kemid): New. (agent_kem_decrypt): New. * agent/command.c (cmd_pkdecrypt): Support --kem option to call agent_kem_decrypt. * agent/pkdecrypt.c (reverse_buffer): New. (agent_hybrid_pgp_kem_decrypt): New. (agent_kem_decrypt): New. -- Now, it only supports X25519 + ML-KEM. GnuPG-bug-id: 7014 Signed-off-by: NIIBE Yutaka <gniibe@fsij.org>
Diffstat (limited to 'agent')
-rw-r--r--agent/agent.h12
-rw-r--r--agent/command.c45
-rw-r--r--agent/pkdecrypt.c312
3 files changed, 362 insertions, 7 deletions
diff --git a/agent/agent.h b/agent/agent.h
index b3d3c0407..af040b63f 100644
--- a/agent/agent.h
+++ b/agent/agent.h
@@ -560,6 +560,18 @@ gpg_error_t agent_pkdecrypt (ctrl_t ctrl, const char *desc_text,
const unsigned char *ciphertext, size_t ciphertextlen,
membuf_t *outbuf, int *r_padding);
+enum kemid
+ {
+ KEM_PQC_PGP,
+ KEM_PGP,
+ KEM_CMS,
+ };
+
+gpg_error_t agent_kem_decrypt (ctrl_t ctrl, const char *desc_text, int kemid,
+ const unsigned char *ct, size_t ctlen,
+ const unsigned char *option, size_t optionlen,
+ membuf_t *outbuf);
+
/*-- genkey.c --*/
#define CHECK_CONSTRAINTS_NOT_EMPTY 1
#define CHECK_CONSTRAINTS_NEW_SYMKEY 2
diff --git a/agent/command.c b/agent/command.c
index 5e74381ed..fd050ee6b 100644
--- a/agent/command.c
+++ b/agent/command.c
@@ -1049,10 +1049,14 @@ cmd_pksign (assuan_context_t ctx, char *line)
static const char hlp_pkdecrypt[] =
- "PKDECRYPT [<options>]\n"
+ "PKDECRYPT [--kem[=<kemid>] [<options>]\n"
"\n"
"Perform the actual decrypt operation. Input is not\n"
- "sensitive to eavesdropping.";
+ "sensitive to eavesdropping.\n"
+ "If the --kem option is used, decryption is done with the KEM,\n"
+ "inquiring upper-layer option, when needed. KEMID can be\n"
+ "specified with --kem option; Valid value is: PQC-PGP, PGP, or CMS.\n"
+ "Default is PQC-PGP.";
static gpg_error_t
cmd_pkdecrypt (assuan_context_t ctx, char *line)
{
@@ -1061,22 +1065,51 @@ cmd_pkdecrypt (assuan_context_t ctx, char *line)
unsigned char *value;
size_t valuelen;
membuf_t outbuf;
- int padding;
+ int padding = -1;
+ unsigned char *option = NULL;
+ size_t optionlen = 0;
+ const char *p;
+ int kemid = -1;
- (void)line;
+ p = has_option_name (line, "--kem");
+ if (p)
+ {
+ kemid = KEM_PQC_PGP;
+ if (*p++ == '=')
+ {
+ if (strcmp (p, "PQC-PGP"))
+ kemid = KEM_PQC_PGP;
+ else if (strcmp (p, "PGP"))
+ kemid = KEM_PGP;
+ else if (strcmp (p, "CMS"))
+ kemid = KEM_CMS;
+ else
+ return set_error (GPG_ERR_ASS_PARAMETER, "invalid KEM algorithm");
+ }
+ }
/* First inquire the data to decrypt */
rc = print_assuan_status (ctx, "INQUIRE_MAXLEN", "%u", MAXLEN_CIPHERTEXT);
if (!rc)
rc = assuan_inquire (ctx, "CIPHERTEXT",
&value, &valuelen, MAXLEN_CIPHERTEXT);
+ if (!rc && kemid > KEM_PQC_PGP)
+ rc = assuan_inquire (ctx, "OPTION",
+ &option, &optionlen, MAXLEN_CIPHERTEXT);
if (rc)
return rc;
init_membuf (&outbuf, 512);
- rc = agent_pkdecrypt (ctrl, ctrl->server_local->keydesc,
- value, valuelen, &outbuf, &padding);
+ if (kemid < 0)
+ rc = agent_pkdecrypt (ctrl, ctrl->server_local->keydesc,
+ value, valuelen, &outbuf, &padding);
+ else
+ {
+ rc = agent_kem_decrypt (ctrl, ctrl->server_local->keydesc, kemid,
+ value, valuelen, option, optionlen, &outbuf);
+ xfree (option);
+ }
xfree (value);
if (rc)
clear_outbuf (&outbuf);
diff --git a/agent/pkdecrypt.c b/agent/pkdecrypt.c
index c26f21d35..9d87e9fba 100644
--- a/agent/pkdecrypt.c
+++ b/agent/pkdecrypt.c
@@ -27,7 +27,7 @@
#include <sys/stat.h>
#include "agent.h"
-
+#include "../common/openpgpdefs.h"
/* DECRYPT the stuff in ciphertext which is expected to be a S-Exp.
Try to get the key from CTRL and write the decoded stuff back to
@@ -157,3 +157,313 @@ agent_pkdecrypt (ctrl_t ctrl, const char *desc_text,
xfree (shadow_info);
return err;
}
+
+
+/* Reverse BUFFER to change the endianness. */
+static void
+reverse_buffer (unsigned char *buffer, unsigned int length)
+{
+ unsigned int tmp, i;
+
+ for (i=0; i < length/2; i++)
+ {
+ tmp = buffer[i];
+ buffer[i] = buffer[length-1-i];
+ buffer[length-1-i] = tmp;
+ }
+}
+
+/* For hybrid PGP KEM (ECC+ML-KEM), decrypt CIPHERTEXT using KEM API.
+ First keygrip is for ECC, second keygrip is for PQC. CIPHERTEXT
+ should follow the format of:
+
+ (enc-val(pqc(s%m)(e%m)(k%m))))
+ s: encrypted session key
+ e: ECDH ciphertext
+ k: ML-KEM ciphertext
+
+ FIXME: For now, possibile keys on smartcard are not supported.
+ */
+static gpg_error_t
+agent_hybrid_pgp_kem_decrypt (ctrl_t ctrl, const char *desc_text,
+ gcry_sexp_t s_cipher, membuf_t *outbuf)
+{
+ gcry_sexp_t s_skey0 = NULL;
+ gcry_sexp_t s_skey1 = NULL;
+ unsigned char *shadow_info = NULL;
+ gpg_error_t err = 0;
+
+ unsigned int nbits;
+ const unsigned char *p;
+ size_t len;
+
+ gcry_mpi_t encrypted_sessionkey_mpi;
+ const unsigned char *encrypted_sessionkey;
+ size_t encrypted_sessionkey_len;
+
+ gcry_mpi_t ecc_sk_mpi;
+ unsigned char ecc_sk[32];
+ gcry_mpi_t ecc_pk_mpi;
+ unsigned char ecc_pk[32];
+ gcry_mpi_t ecc_ct_mpi;
+ const unsigned char *ecc_ct;
+ size_t ecc_ct_len;
+ unsigned char ecc_ecdh[32];
+ unsigned char ecc_ss[32];
+
+ gcry_mpi_t mlkem_sk_mpi;
+ gcry_mpi_t mlkem_ct_mpi;
+ const unsigned char *mlkem_sk;
+ const unsigned char *mlkem_ct;
+ unsigned char mlkem_ss[GCRY_KEM_MLKEM768_SHARED_LEN];
+
+ gcry_buffer_t iov[6];
+
+ unsigned char kekkey[32];
+ size_t kekkeylen = 32; /* AES-256 is mandatory */
+
+ gcry_cipher_hd_t hd;
+ unsigned char sessionkey[256];
+ size_t sessionkey_len;
+ const unsigned char fixedinfo[1] = { 105 };
+
+ err = agent_key_from_file (ctrl, NULL, desc_text,
+ ctrl->keygrip, &shadow_info,
+ CACHE_MODE_NORMAL, NULL, &s_skey0, NULL, NULL);
+ if (err)
+ {
+ log_error ("failed to read the secret key\n");
+ goto leave;
+ }
+
+ err = agent_key_from_file (ctrl, NULL, desc_text,
+ ctrl->keygrip1, &shadow_info,
+ CACHE_MODE_NORMAL, NULL, &s_skey1, NULL, NULL);
+ if (err)
+ {
+ log_error ("failed to read the another secret key\n");
+ goto leave;
+ }
+
+ /* Here assumes no smartcard, but private keys */
+
+ gcry_sexp_extract_param (s_cipher, NULL, "/e/k/s",
+ &ecc_ct_mpi,
+ &mlkem_ct_mpi,
+ &encrypted_sessionkey_mpi, NULL);
+
+ encrypted_sessionkey = gcry_mpi_get_opaque (encrypted_sessionkey_mpi, &nbits);
+ encrypted_sessionkey_len = (nbits+7)/8;
+ encrypted_sessionkey_len--;
+
+ if (encrypted_sessionkey[0] != encrypted_sessionkey_len)
+ {
+ err = GPG_ERR_INV_DATA;
+ goto leave;
+ }
+ encrypted_sessionkey++; /* Skip the length. */
+
+ if (encrypted_sessionkey[0] != CIPHER_ALGO_AES256)
+ {
+ err = GPG_ERR_INV_DATA;
+ goto leave;
+ }
+ encrypted_sessionkey_len--;
+ encrypted_sessionkey++; /* Skip the sym algo */
+
+ /* Fistly, ECC part. FIXME: For now, we assume X25519. */
+ gcry_sexp_extract_param (s_skey0, NULL, "/q/d",
+ &ecc_pk_mpi, &ecc_sk_mpi, NULL);
+ p = gcry_mpi_get_opaque (ecc_pk_mpi, &nbits);
+ len = (nbits+7)/8;
+ memcpy (ecc_pk, p+1, 32); /* Remove the 0x40 prefix */
+ p = gcry_mpi_get_opaque (ecc_sk_mpi, &nbits);
+ len = (nbits+7)/8;
+ if (len > 32)
+ {
+ err = GPG_ERR_INV_DATA;
+ goto leave;
+ }
+ memset (ecc_sk, 0, 32);
+ memcpy (ecc_sk + 32 - len, p, len);
+ reverse_buffer (ecc_sk, 32);
+ mpi_release (ecc_pk_mpi);
+ mpi_release (ecc_sk_mpi);
+
+ ecc_ct = gcry_mpi_get_opaque (ecc_ct_mpi, &nbits);
+ ecc_ct_len = (nbits+7)/8;
+ if (ecc_ct_len != 32)
+ {
+ err = GPG_ERR_INV_DATA;
+ goto leave;
+ }
+
+ err = gcry_kem_decap (GCRY_KEM_RAW_X25519, ecc_sk, 32, ecc_ct, ecc_ct_len,
+ ecc_ecdh, 32, NULL, 0);
+
+ iov[0].data = ecc_ecdh;
+ iov[0].off = 0;
+ iov[0].len = 32;
+ iov[1].data = (unsigned char *)ecc_ct;
+ iov[1].off = 0;
+ iov[1].len = 32;
+ iov[2].data = ecc_pk;
+ iov[2].off = 0;
+ iov[2].len = 32;
+ gcry_md_hash_buffers (GCRY_MD_SHA3_256, 0, ecc_ss, iov, 3);
+
+ /* Secondly, PQC part. For now, we assume ML-KEM. */
+ gcry_sexp_extract_param (s_skey1, NULL, "/s", &mlkem_sk_mpi, NULL);
+ mlkem_sk = gcry_mpi_get_opaque (mlkem_sk_mpi, &nbits);
+ len = (nbits+7)/8;
+ if (len != GCRY_KEM_MLKEM768_SECKEY_LEN)
+ {
+ err = GPG_ERR_INV_DATA;
+ goto leave;
+ }
+ mlkem_ct = gcry_mpi_get_opaque (mlkem_ct_mpi, &nbits);
+ len = (nbits+7)/8;
+ if (len != GCRY_KEM_MLKEM768_CIPHER_LEN)
+ {
+ err = GPG_ERR_INV_DATA;
+ goto leave;
+ }
+ err = gcry_kem_decap (GCRY_KEM_MLKEM768,
+ mlkem_sk, GCRY_KEM_MLKEM768_SECKEY_LEN,
+ mlkem_ct, GCRY_KEM_MLKEM768_CIPHER_LEN,
+ mlkem_ss, GCRY_KEM_MLKEM768_SHARED_LEN,
+ NULL, 0);
+
+ mpi_release (mlkem_sk_mpi);
+
+ /* Then, combine two shared secrets into one */
+
+ iov[0].data = "\x00\x00\x00\x01"; /* Counter */
+ iov[0].off = 0;
+ iov[0].len = 4;
+
+ iov[1].data = ecc_ss;
+ iov[1].off = 0;
+ iov[1].len = 32;
+
+ iov[2].data = (unsigned char *)ecc_ct;
+ iov[2].off = 0;
+ iov[2].len = 32;
+
+ iov[3].data = mlkem_ss;
+ iov[3].off = 0;
+ iov[3].len = GCRY_KEM_MLKEM768_SHARED_LEN;
+
+ iov[4].data = (unsigned char *)mlkem_ct;
+ iov[4].off = 0;
+ iov[4].len = GCRY_KEM_MLKEM768_ENCAPS_LEN;
+
+ iov[5].data = (unsigned char *)fixedinfo;
+ iov[5].off = 0;
+ iov[5].len = 1;
+
+ err = compute_kmac256 (kekkey, kekkeylen,
+ "OpenPGPCompositeKeyDerivationFunction", 37,
+ "KDF", 3, iov, 6);
+
+ mpi_release (ecc_ct_mpi);
+ mpi_release (mlkem_ct_mpi);
+
+ if (DBG_CRYPTO)
+ {
+ log_printhex (kekkey, kekkeylen, "KEK key: ");
+ }
+
+ err = gcry_cipher_open (&hd, GCRY_CIPHER_AES256,
+ GCRY_CIPHER_MODE_AESWRAP, 0);
+ if (err)
+ {
+ log_error ("ecdh failed to initialize AESWRAP: %s\n",
+ gpg_strerror (err));
+ mpi_release (encrypted_sessionkey_mpi);
+ goto leave;
+ }
+
+ err = gcry_cipher_setkey (hd, kekkey, kekkeylen);
+
+ sessionkey_len = encrypted_sessionkey_len - 8;
+ err = gcry_cipher_decrypt (hd, sessionkey, sessionkey_len,
+ encrypted_sessionkey, encrypted_sessionkey_len);
+ gcry_cipher_close (hd);
+
+ mpi_release (encrypted_sessionkey_mpi);
+
+ if (err)
+ {
+ log_error ("KEM decrypt failed: %s\n", gpg_strerror (err));
+ goto leave;
+ }
+
+ put_membuf_printf (outbuf,
+ "(5:value%u:", (unsigned int)sessionkey_len);
+ put_membuf (outbuf, sessionkey, sessionkey_len);
+ put_membuf (outbuf, ")", 2);
+
+ leave:
+ gcry_sexp_release (s_skey0);
+ gcry_sexp_release (s_skey1);
+ return err;
+}
+
+/* DECRYPT the encrypted stuff (like encrypted session key) in
+ CIPHERTEXT using KEM API, with KEMID. Keys (or a key) are
+ specified in CTRL. DESC_TEXT is used to retrieve private key.
+ OPTION can be specified for upper layer option for KEM. Decrypted
+ stuff (like session key) is written to OUTBUF.
+ */
+gpg_error_t
+agent_kem_decrypt (ctrl_t ctrl, const char *desc_text, int kemid,
+ const unsigned char *ciphertext, size_t ciphertextlen,
+ const unsigned char *option, size_t optionlen,
+ membuf_t *outbuf)
+{
+ gcry_sexp_t s_cipher = NULL;
+ gpg_error_t err = 0;
+
+ /* For now, only PQC-PGP is supported. */
+ if (kemid != KEM_PQC_PGP)
+ return gpg_error (GPG_ERR_UNSUPPORTED_ALGORITHM);
+
+ (void)optionlen;
+ if (kemid == KEM_PQC_PGP && option)
+ {
+ log_error ("PQC-PGP requires no option\n");
+ return gpg_error (GPG_ERR_INV_ARG);
+ }
+
+ if (!ctrl->have_keygrip)
+ {
+ log_error ("speculative decryption not yet supported\n");
+ return gpg_error (GPG_ERR_NO_SECKEY);
+ }
+
+ if (!ctrl->have_keygrip1)
+ {
+ log_error ("hybrid KEM requires two KEYGRIPs\n");
+ return gpg_error (GPG_ERR_NO_SECKEY);
+ }
+
+ err = gcry_sexp_sscan (&s_cipher, NULL, (char*)ciphertext, ciphertextlen);
+ if (err)
+ {
+ log_error ("failed to convert ciphertext: %s\n", gpg_strerror (err));
+ return gpg_error (GPG_ERR_INV_DATA);
+ }
+
+ if (DBG_CRYPTO)
+ {
+ log_printhex (ctrl->keygrip, 20, "keygrip:");
+ log_printhex (ctrl->keygrip1, 20, "keygrip1:");
+ log_printhex (ciphertext, ciphertextlen, "cipher: ");
+ }
+
+ err = agent_hybrid_pgp_kem_decrypt (ctrl, desc_text, s_cipher, outbuf);
+
+ gcry_sexp_release (s_cipher);
+ return err;
+}