diff options
author | NIIBE Yutaka <[email protected]> | 2024-04-05 05:17:25 +0000 |
---|---|---|
committer | NIIBE Yutaka <[email protected]> | 2024-04-05 05:17:25 +0000 |
commit | 131dd2a35145a1db1a45ab76764f32cbbca3fd43 (patch) | |
tree | 7ef3fa4053fe64cc1e8eb265839c9c1a92343b81 /agent/pkdecrypt.c | |
parent | gpg: Initial support for generating Kyber subkeys. (diff) | |
download | gnupg-131dd2a35145a1db1a45ab76764f32cbbca3fd43.tar.gz gnupg-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 <[email protected]>
Diffstat (limited to 'agent/pkdecrypt.c')
-rw-r--r-- | agent/pkdecrypt.c | 312 |
1 files changed, 311 insertions, 1 deletions
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; +} |