From 131dd2a35145a1db1a45ab76764f32cbbca3fd43 Mon Sep 17 00:00:00 2001 From: NIIBE Yutaka Date: Fri, 5 Apr 2024 14:17:25 +0900 Subject: [PATCH] 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 --- agent/agent.h | 12 ++ agent/command.c | 45 ++++++- agent/pkdecrypt.c | 312 +++++++++++++++++++++++++++++++++++++++++++++- 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 []\n" + "PKDECRYPT [--kem[=] []\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 #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; +}