From 95d83cf906177fe9f00e88ae42d4c118c7db4371 Mon Sep 17 00:00:00 2001 From: Werner Koch Date: Thu, 23 Apr 2020 09:59:13 +0200 Subject: [PATCH] sm: Support decryption of ECDH data. * sm/decrypt.c: Include tlv.h. (string_from_gcry_buffer): New. (hash_ecc_cms_shared_info): New. (ecdh_decrypt): New. (prepare_decryption): Support ECDH. Add arg pk_algo. (gpgsm_decrypt): Lift some variables from an inner code block. -- Note: This has only been tested with a single messages created by OpenSSL and taken from the Mozilla bug tracker. In particular the code to included UserKeyingMaterial (ukm) has not been tested. GnuPG-bug-id: 4098 Signed-off-by: Werner Koch --- sm/decrypt.c | 396 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 362 insertions(+), 34 deletions(-) diff --git a/sm/decrypt.c b/sm/decrypt.c index db128af67..0bde2e387 100644 --- a/sm/decrypt.c +++ b/sm/decrypt.c @@ -32,6 +32,7 @@ #include "keydb.h" #include "../common/i18n.h" +#include "../common/tlv.h" #include "../common/compliance.h" struct decrypt_filter_parm_s @@ -51,11 +52,319 @@ struct decrypt_filter_parm_s }; +/* Return the hash algorithm's algo id from its name given in the + * non-null termnated string in (buffer,buflen). Returns 0 on failure + * or if the algo is not known. */ +static char * +string_from_gcry_buffer (gcry_buffer_t *buffer) +{ + char *string; + + string = xtrymalloc (buffer->len + 1); + if (!string) + return NULL; + memcpy (string, buffer->data, buffer->len); + string[buffer->len] = 0; + return string; +} + + +/* Helper to construct and hash the + * ECC-CMS-SharedInfo ::= SEQUENCE { + * keyInfo AlgorithmIdentifier, + * entityUInfo [0] EXPLICIT OCTET STRING OPTIONAL, + * suppPubInfo [2] EXPLICIT OCTET STRING } + * as described in RFC-5753, 7.2. */ +static gpg_error_t +hash_ecc_cms_shared_info (gcry_md_hd_t hash_hd, const char *wrap_algo_str, + unsigned int keylen, + const void *ukm, unsigned int ukmlen) +{ + gpg_error_t err; + void *p; + unsigned char *oid; + size_t n, oidlen, toidlen, tkeyinfo, tukmlen, tsupppubinfo; + unsigned char keylenbuf[6]; + membuf_t mb = MEMBUF_ZERO; + + err = ksba_oid_from_str (wrap_algo_str, &oid, &oidlen); + if (err) + return err; + toidlen = get_tlv_length (CLASS_UNIVERSAL, TAG_OBJECT_ID, 0, oidlen); + tkeyinfo = get_tlv_length (CLASS_UNIVERSAL, TAG_SEQUENCE, 1, toidlen); + + tukmlen = ukm? get_tlv_length (CLASS_CONTEXT, 0, 1, ukmlen) : 0; + + keylen *= 8; + keylenbuf[0] = TAG_OCTET_STRING; + keylenbuf[1] = 4; + keylenbuf[2] = (keylen >> 24); + keylenbuf[3] = (keylen >> 16); + keylenbuf[4] = (keylen >> 8); + keylenbuf[5] = keylen; + + tsupppubinfo = get_tlv_length (CLASS_CONTEXT, 2, 1, sizeof keylenbuf); + + put_tlv_to_membuf (&mb, CLASS_UNIVERSAL, TAG_SEQUENCE, 1, + tkeyinfo + tukmlen + tsupppubinfo); + put_tlv_to_membuf (&mb, CLASS_UNIVERSAL, TAG_SEQUENCE, 1, + toidlen); + put_tlv_to_membuf (&mb, CLASS_UNIVERSAL, TAG_OBJECT_ID, 0, oidlen); + put_membuf (&mb, oid, oidlen); + ksba_free (oid); + + if (ukm) + { + put_tlv_to_membuf (&mb, CLASS_CONTEXT, 0, 1, ukmlen); + put_membuf (&mb, ukm, ukmlen); + } + + put_tlv_to_membuf (&mb, CLASS_CONTEXT, 2, 1, sizeof keylenbuf); + put_membuf (&mb, keylenbuf, sizeof keylenbuf); + + p = get_membuf (&mb, &n); + if (!p) + return gpg_error_from_syserror (); + + gcry_md_write (hash_hd, p, n); + xfree (p); + return 0; +} + + +/* This function will modify SECRET. */ +static gpg_error_t +ecdh_decrypt (unsigned char *secret, size_t secretlen, + gcry_sexp_t enc_val, + unsigned char **r_result, unsigned int *r_resultlen) +{ + gpg_error_t err; + gcry_buffer_t ioarray[4] = { {0}, {0}, {0}, {0} }; + char *encr_algo_str = NULL; + char *wrap_algo_str = NULL; + int hash_algo, cipher_algo; + const unsigned char *ukm; /* Alias for ioarray[2]. */ + unsigned int ukmlen; + const unsigned char *data; /* Alias for ioarray[3]. */ + unsigned int datalen; + unsigned int keylen, hashlen; + unsigned char key[32]; + gcry_cipher_hd_t cipher_hd = NULL; + unsigned char *result = NULL; + unsigned int resultlen; + + *r_resultlen = 0; + *r_result = NULL; + + /* Extract X from SECRET; this is the actual secret. It must be in + * the format of: + * + * 04 || X || Y + * 40 || X + * 41 || X + */ + if (secretlen < 2) + return gpg_error (GPG_ERR_BAD_DATA); + if (*secret == 0x04) + { + secretlen--; + memmove (secret, secret+1, secretlen); + if ((secretlen & 1)) + return gpg_error (GPG_ERR_BAD_DATA); + secretlen /= 2; + } + else if (*secret == 0x40 || *secret == 0x41) + { + secretlen--; + memmove (secret, secret+1, secretlen); + } + else + return gpg_error (GPG_ERR_BAD_DATA); + if (!secretlen) + return gpg_error (GPG_ERR_BAD_DATA); + + if (DBG_CRYPTO) + log_printhex (secret, secretlen, "ECDH X ..:"); + + /* We have now the shared secret bytes in (SECRET,SECRETLEN). Now + * we will compute the KEK using a value dervied from the secret + * bytes. */ + err = gcry_sexp_extract_param (enc_val, "enc-val", + "&'encr-algo''wrap-algo''ukm'?s", + ioarray+0, ioarray+1, + ioarray+2, ioarray+3, NULL); + if (err) + { + log_error ("extracting ECDH parameter failed: %s\n", gpg_strerror (err)); + goto leave; + } + encr_algo_str = string_from_gcry_buffer (ioarray); + if (!encr_algo_str) + { + err = gpg_error_from_syserror (); + goto leave; + } + wrap_algo_str = string_from_gcry_buffer (ioarray+1); + if (!wrap_algo_str) + { + err = gpg_error_from_syserror (); + goto leave; + } + ukm = ioarray[2].data; + ukmlen = ioarray[2].len; + data = ioarray[3].data; + datalen = ioarray[3].len; + + /* Check parameters. */ + if (DBG_CRYPTO) + { + log_debug ("encr_algo: %s\n", encr_algo_str); + log_debug ("wrap_algo: %s\n", wrap_algo_str); + log_printhex (ukm, ukmlen, "ukm .....:"); + log_printhex (data, datalen, "data ....:"); + } + + if (!strcmp (encr_algo_str, "1.3.132.1.11.1")) + { + /* dhSinglePass-stdDH-sha256kdf-scheme */ + hash_algo = GCRY_MD_SHA256; + hashlen = 32; + } + else if (!strcmp (encr_algo_str, "1.3.132.1.11.2")) + { + /* dhSinglePass-stdDH-sha384kdf-scheme */ + hash_algo = GCRY_MD_SHA384; + hashlen = 48; + } + else if (!strcmp (encr_algo_str, "1.3.132.1.11.3")) + { + /* dhSinglePass-stdDH-sha512kdf-scheme */ + hash_algo = GCRY_MD_SHA512; + hashlen = 64; + } + else + { + err = gpg_error (GPG_ERR_PUBKEY_ALGO); + goto leave; + } + + if (!strcmp (wrap_algo_str, "2.16.840.1.101.3.4.1.5")) + { + cipher_algo = GCRY_CIPHER_AES128; + keylen = 16; + } + else if (!strcmp (wrap_algo_str, "2.16.840.1.101.3.4.1.25")) + { + cipher_algo = GCRY_CIPHER_AES192; + keylen = 24; + } + else if (!strcmp (wrap_algo_str, "2.16.840.1.101.3.4.1.45")) + { + cipher_algo = GCRY_CIPHER_AES256; + keylen = 32; + } + else + { + err = gpg_error (GPG_ERR_PUBKEY_ALGO); + goto leave; + } + + /* Derive a KEK (key wrapping key) using MESSAGE and SECRET_X. + * According to SEC1 3.6.1 we should check that + * SECRETLEN + UKMLEN + 4 < maxhashlen + * However, we have no practical limit on the hash length and thus + * there is no point in checking this. The second check that + * KEYLEN < hashlen*(2^32-1) + * is obviously also not needed. Because with our allowed + * parameters KEYLEN is always less or equal to HASHLEN so that we + * do not need to iterate at all. + */ + log_assert (gcry_md_get_algo_dlen (hash_algo) == hashlen); + { + gcry_md_hd_t hash_hd; + err = gcry_md_open (&hash_hd, hash_algo, 0); + if (err) + goto leave; + gcry_md_write(hash_hd, secret, secretlen); + gcry_md_write(hash_hd, "\x00\x00\x00\x01", 4); /* counter */ + err = hash_ecc_cms_shared_info (hash_hd, wrap_algo_str, keylen, + ukm, ukmlen); + gcry_md_final (hash_hd); + log_assert (keylen <= sizeof key && keylen <= hashlen); + memcpy (key, gcry_md_read (hash_hd, 0), keylen); + gcry_md_close (hash_hd); + if (err) + goto leave; + } + + if (DBG_CRYPTO) + log_printhex (key, keylen, "KEK .....:"); + + /* Unwrap the key. */ + if ((datalen % 8) || datalen < 16) + { + log_error ("can't use a shared secret of %u bytes for ecdh\n", datalen); + err = gpg_error (GPG_ERR_BAD_DATA); + goto leave; + } + + resultlen = datalen - 8; + result = xtrymalloc_secure (resultlen); + if (!result) + { + err = gpg_error_from_syserror (); + goto leave; + } + + err = gcry_cipher_open (&cipher_hd, cipher_algo, GCRY_CIPHER_MODE_AESWRAP, 0); + if (err) + { + log_error ("ecdh failed to initialize AESWRAP: %s\n", gpg_strerror (err)); + goto leave; + } + + err = gcry_cipher_setkey (cipher_hd, key, keylen); + wipememory (key, sizeof key); + if (err) + { + log_error ("ecdh failed in gcry_cipher_setkey: %s\n", gpg_strerror (err)); + goto leave; + } + + err = gcry_cipher_decrypt (cipher_hd, result, resultlen, data, datalen); + if (err) + { + log_error ("ecdh failed in gcry_cipher_decrypt: %s\n",gpg_strerror (err)); + goto leave; + } + + *r_resultlen = resultlen; + *r_result = result; + result = NULL; + + leave: + if (result) + { + wipememory (result, resultlen); + xfree (result); + } + gcry_cipher_close (cipher_hd); + xfree (encr_algo_str); + xfree (wrap_algo_str); + xfree (ioarray[0].data); + xfree (ioarray[1].data); + xfree (ioarray[2].data); + xfree (ioarray[3].data); + return err; +} + + /* Decrypt the session key and fill in the parm structure. The algo and the IV is expected to be already in PARM. */ static int -prepare_decryption (ctrl_t ctrl, const char *hexkeygrip, const char *desc, +prepare_decryption (ctrl_t ctrl, const char *hexkeygrip, + int pk_algo, const char *desc, ksba_const_sexp_t enc_val, struct decrypt_filter_parm_s *parm) { @@ -63,6 +372,8 @@ prepare_decryption (ctrl_t ctrl, const char *hexkeygrip, const char *desc, size_t n, seskeylen; int rc; + if (DBG_CRYPTO) + log_printcanon ("decrypting:", enc_val, 0); rc = gpgsm_agent_pkdecrypt (ctrl, hexkeygrip, desc, enc_val, &seskey, &seskeylen); if (rc) @@ -72,10 +383,31 @@ prepare_decryption (ctrl_t ctrl, const char *hexkeygrip, const char *desc, } if (DBG_CRYPTO) - log_printhex (seskey, seskeylen, "pkcs1 encoded session key:"); + log_printhex (seskey, seskeylen, "DEK frame:"); n=0; - if (seskeylen == 32 || seskeylen == 24 || seskeylen == 16) + if (pk_algo == GCRY_PK_ECC) + { + gcry_sexp_t s_enc_val; + unsigned char *decrypted; + unsigned int decryptedlen; + + rc = gcry_sexp_sscan (&s_enc_val, NULL, enc_val, + gcry_sexp_canon_len (enc_val, 0, NULL, NULL)); + if (rc) + goto leave; + + rc = ecdh_decrypt (seskey, seskeylen, s_enc_val, + &decrypted, &decryptedlen); + gcry_sexp_release (s_enc_val); + if (rc) + goto leave; + xfree (seskey); + seskey = decrypted; + seskeylen = decryptedlen; + + } + else if (seskeylen == 32 || seskeylen == 24 || seskeylen == 16) { /* Smells like an AES-128, 3-DES, or AES-256 key. This might * happen because a SC has already done the unpacking. A better @@ -115,7 +447,7 @@ prepare_decryption (ctrl_t ctrl, const char *hexkeygrip, const char *desc, } if (DBG_CRYPTO) - log_printhex (seskey+n, seskeylen-n, "session key:"); + log_printhex (seskey+n, seskeylen-n, "CEK .....:"); rc = gcry_cipher_open (&parm->hd, parm->algo, parm->mode, 0); if (rc) @@ -398,6 +730,9 @@ gpgsm_decrypt (ctrl_t ctrl, int in_fd, estream_t out_fp) char *desc = NULL; char kidbuf[16+1]; int tmp_rc; + ksba_cert_t cert = NULL; + unsigned int nbits; + int pk_algo = 0; *kidbuf = 0; @@ -410,8 +745,6 @@ gpgsm_decrypt (ctrl_t ctrl, int in_fd, estream_t out_fp) recp, gpg_strerror (tmp_rc)); else { - ksba_cert_t cert = NULL; - if (opt.verbose) { log_info ("recp %d - issuer: '%s'\n", @@ -480,34 +813,29 @@ gpgsm_decrypt (ctrl_t ctrl, int in_fd, estream_t out_fp) hexkeygrip = gpgsm_get_keygrip_hexstring (cert); desc = gpgsm_format_keydesc (cert); - { - unsigned int nbits; - int pk_algo = gpgsm_get_key_algo_info (cert, &nbits); + pk_algo = gpgsm_get_key_algo_info (cert, &nbits); - /* Check compliance. */ - if (!gnupg_pk_is_allowed (opt.compliance, - PK_USE_DECRYPTION, - pk_algo, NULL, nbits, NULL)) - { - char kidstr[10+1]; + /* Check compliance. */ + if (!gnupg_pk_is_allowed (opt.compliance, + PK_USE_DECRYPTION, + pk_algo, NULL, nbits, NULL)) + { + char kidstr[10+1]; - snprintf (kidstr, sizeof kidstr, "0x%08lX", - gpgsm_get_short_fingerprint (cert, NULL)); - log_info - (_("key %s is not suitable for decryption" - " in %s mode\n"), - kidstr, - gnupg_compliance_option_string (opt.compliance)); - rc = gpg_error (GPG_ERR_PUBKEY_ALGO); - goto oops; - } + snprintf (kidstr, sizeof kidstr, "0x%08lX", + gpgsm_get_short_fingerprint (cert, NULL)); + log_info (_("key %s is not suitable for decryption" + " in %s mode\n"), + kidstr, + gnupg_compliance_option_string(opt.compliance)); + rc = gpg_error (GPG_ERR_PUBKEY_ALGO); + goto oops; + } - /* Check that all certs are compliant with CO_DE_VS. */ - is_de_vs = - (is_de_vs - && gnupg_pk_is_compliant (CO_DE_VS, pk_algo, NULL, - nbits, NULL)); - } + /* Check that all certs are compliant with CO_DE_VS. */ + is_de_vs = (is_de_vs + && gnupg_pk_is_compliant (CO_DE_VS, pk_algo, + NULL, nbits, NULL)); oops: if (rc) @@ -521,15 +849,15 @@ gpgsm_decrypt (ctrl_t ctrl, int in_fd, estream_t out_fp) ksba_cert_release (cert); } - if (!hexkeygrip) + if (!hexkeygrip || !pk_algo) ; else if (!(enc_val = ksba_cms_get_enc_val (cms, recp))) log_error ("recp %d - error getting encrypted session key\n", recp); else { - rc = prepare_decryption (ctrl, - hexkeygrip, desc, enc_val, &dfparm); + rc = prepare_decryption (ctrl, hexkeygrip, pk_algo, + desc, enc_val, &dfparm); xfree (enc_val); if (rc) {