From 380e87ebf19c2ecbcd6dfcc977d70b5971559446 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Tue, 20 Sep 2016 21:26:43 +0200 Subject: [WIP] ssl: add AEAD auth tag validation [rv2] TODO - pull AEAD init/check outside function (for re-use with TLS1.3?) - fix sequence number for nonce, need to check what is wrong - more testing (+ extend test suite)? CCM seems to work. GCM auth check seems still broken. - test with older libgcrypt versions (currently tested with 1.7.6). Change-Id: I94dd2fd70e1281d85c954abfe523f7483d9ac68b --- epan/dissectors/packet-dtls.c | 9 ++- epan/dissectors/packet-ssl-utils.c | 156 ++++++++++++++++++++++++++----------- epan/dissectors/packet-ssl-utils.h | 18 ++++- epan/dissectors/packet-ssl.c | 16 ++-- 4 files changed, 141 insertions(+), 58 deletions(-) diff --git a/epan/dissectors/packet-dtls.c b/epan/dissectors/packet-dtls.c index 0ac6a94340..dd8905c968 100644 --- a/epan/dissectors/packet-dtls.c +++ b/epan/dissectors/packet-dtls.c @@ -568,8 +568,8 @@ dtls_is_null_cipher(guint cipher ) } static gboolean -decrypt_dtls_record(tvbuff_t *tvb, packet_info *pinfo, guint32 offset, - guint32 record_length, guint8 content_type, SslDecryptSession* ssl, +decrypt_dtls_record(tvbuff_t *tvb, packet_info *pinfo, guint32 offset, SslDecryptSession *ssl, + guint8 content_type, guint16 record_version, guint16 record_length, gboolean allow_fragments) { gboolean success; @@ -618,7 +618,8 @@ decrypt_dtls_record(tvbuff_t *tvb, packet_info *pinfo, guint32 offset, ssl_debug_printf("decrypt_dtls_record: no decoder available\n"); return FALSE; } - success = ssl_decrypt_record(ssl, decoder, content_type, tvb_get_ptr(tvb, offset, record_length), record_length, + success = ssl_decrypt_record(ssl, decoder, content_type, record_version, + tvb_get_ptr(tvb, offset, record_length), record_length, &dtls_compressed_data, &dtls_decrypted_data, &dtls_decrypted_data_avail) == 0; } else if (dtls_is_null_cipher(ssl->session.cipher)) { @@ -794,7 +795,7 @@ dissect_dtls_record(tvbuff_t *tvb, packet_info *pinfo, /* try to decrypt record on the first pass, if possible. Store decrypted * record for later usage (without having to decrypt again). */ if (ssl) { - decrypt_dtls_record(tvb, pinfo, offset, record_length, content_type, ssl, + decrypt_dtls_record(tvb, pinfo, offset, ssl, content_type, version, record_length, content_type == SSL_ID_APP_DATA || content_type == SSL_ID_HANDSHAKE); } decrypted = ssl_get_record_info(tvb, proto_dtls, pinfo, tvb_raw_offset(tvb)+offset, &record); diff --git a/epan/dissectors/packet-ssl-utils.c b/epan/dissectors/packet-ssl-utils.c index 76ff8f0865..76dd8d481a 100644 --- a/epan/dissectors/packet-ssl-utils.c +++ b/epan/dissectors/packet-ssl-utils.c @@ -57,6 +57,11 @@ #if defined(HAVE_LIBGNUTLS) && defined(HAVE_LIBGCRYPT) #include #endif +#if GCRYPT_VERSION_NUMBER >= 0x010600 /* 1.6.0 */ +/* Whether to provide support for authentication in addition to decryption. */ +#define HAVE_LIBGCRYPT_AEAD +#define HAVE_LIBGCRYPT_CCM +#endif /* Lookup tables {{{ */ const value_string ssl_version_short_names[] = { @@ -1842,7 +1847,19 @@ static gint ssl_cipher_init(gcry_cipher_hd_t *cipher, gint algo, guchar* sk, guchar* iv, gint mode) { - gint gcry_modes[]={GCRY_CIPHER_MODE_STREAM,GCRY_CIPHER_MODE_CBC,GCRY_CIPHER_MODE_CTR,GCRY_CIPHER_MODE_CTR,GCRY_CIPHER_MODE_CTR}; + gint gcry_modes[] = { + GCRY_CIPHER_MODE_STREAM, + GCRY_CIPHER_MODE_CBC, +#ifdef HAVE_LIBGCRYPT_AEAD + GCRY_CIPHER_MODE_GCM, + GCRY_CIPHER_MODE_CCM, + GCRY_CIPHER_MODE_CCM +#else + GCRY_CIPHER_MODE_CTR, + GCRY_CIPHER_MODE_CTR, + GCRY_CIPHER_MODE_CTR, +#endif + }; gint err; if (algo == -1) { /* NULL mode */ @@ -2686,12 +2703,12 @@ ssl_change_cipher(SslDecryptSession *ssl_session, gboolean server) } int -ssl_decrypt_record(SslDecryptSession*ssl, SslDecoder* decoder, gint ct, - const guchar* in, guint inl, StringInfo* comp_str _U_, StringInfo* out, guint* outl) +ssl_decrypt_record(SslDecryptSession *ssl,SslDecoder *decoder, guint8 ct, guint16 record_version, + const guchar *in, guint16 inl, StringInfo *comp_str, StringInfo *out_str, guint *outl) { ssl_debug_printf("ssl_decrypt_record: impossible without gnutls. ssl %p" - "decoder %p ct %d, in %p inl %d out %p outl %p\n", ssl, decoder, ct, - in, inl, out, outl); + "decoder %p ct %d, version %d in %p inl %d out %p outl %p\n", ssl, decoder, ct, + record_version, in, inl, out, outl); return 0; } /* }}} */ @@ -2847,7 +2864,7 @@ ssl_create_decoder(const SslCipherSuite *cipher_suite, gint cipher_algo, // decoders since "decryption" is easy for such ciphers. dec->mac_key.data = dec->_mac_key_or_write_iv; ssl_data_set(&dec->mac_key, mk, ssl_cipher_suite_dig(cipher_suite)->len); - } else if (mode == MODE_GCM || mode == MODE_CCM || mode == MODE_CCM_8) { + } else if (ssl_is_aead_cipher(mode)) { /* So far all AEAD ciphers derive the 4-byte implicit nonce part from the write IV */ dec->write_iv.data = dec->_mac_key_or_write_iv; ssl_data_set(&dec->write_iv, iv, 4); @@ -3153,7 +3170,7 @@ ssl_generate_keyring_material(SslDecryptSession*ssl_session) if (cipher_suite->mode == MODE_CBC) { write_iv_len = (guint)gcry_cipher_get_algo_blklen(cipher_algo); - } else if (cipher_suite->mode == MODE_GCM || cipher_suite->mode == MODE_CCM || cipher_suite->mode == MODE_CCM_8) { + } else if (ssl_is_aead_cipher(cipher_suite->mode)) { /* account for a four-byte salt for client and server side (from * client_write_IV and server_write_IV), see GCMNonce (RFC 5288) */ write_iv_len = 4; @@ -3562,11 +3579,13 @@ dtls_check_mac(SslDecoder*decoder, gint ct,int ver, guint8* data, /* Assume that we are called only for a non-NULL decoder which also means that * we have a non-NULL decoder->cipher_suite. */ int -ssl_decrypt_record(SslDecryptSession*ssl,SslDecoder* decoder, gint ct, - const guchar* in, guint inl, StringInfo* comp_str, StringInfo* out_str, guint* outl) +ssl_decrypt_record(SslDecryptSession *ssl,SslDecoder *decoder, guint8 ct, guint16 record_version, + const guchar *in, guint16 inl, StringInfo *comp_str, StringInfo *out_str, guint *outl) { guint pad, worklen, uncomplen; guint8 *mac; + guint auth_tag_len = 0; + gcry_error_t err; ssl_debug_printf("ssl_decrypt_record ciphertext len %d\n", inl); ssl_print_data("Ciphertext",in, inl); @@ -3618,9 +3637,7 @@ ssl_decrypt_record(SslDecryptSession*ssl,SslDecoder* decoder, gint ct, } /* Nonce for GenericAEADCipher */ - if (decoder->cipher_suite->mode == MODE_GCM || - decoder->cipher_suite->mode == MODE_CCM || - decoder->cipher_suite->mode == MODE_CCM_8) { + if (ssl_is_aead_cipher(decoder->cipher_suite->mode)) { /* 4 bytes write_iv, 8 bytes explicit_nonce, 4 bytes counter */ guchar gcm_nonce[16] = { 0 }; @@ -3630,65 +3647,98 @@ ssl_decrypt_record(SslDecryptSession*ssl,SslDecoder* decoder, gint ct, return -1; } + if (decoder->write_iv.data_len != 4) { + ssl_debug_printf("Unexpected implicit nonce length %d!\n", decoder->write_iv.data_len); + return -1; + } + + /* Implicit (4) and explicit (8) part of nonce. */ + memcpy(gcm_nonce, decoder->write_iv.data, decoder->write_iv.data_len); /* salt */ + memcpy(gcm_nonce + decoder->write_iv.data_len, in, SSL_EX_NONCE_LEN_GCM); + if (decoder->cipher_suite->mode == MODE_GCM) { - memcpy(gcm_nonce, decoder->write_iv.data, decoder->write_iv.data_len); /* salt */ - memcpy(gcm_nonce + decoder->write_iv.data_len, in, SSL_EX_NONCE_LEN_GCM); /* NIST SP 800-38D, sect. 7.2 says that the 32-bit counter part starts * at 1, and gets incremented before passing to the block cipher. */ gcm_nonce[4 + SSL_EX_NONCE_LEN_GCM + 3] = 2; + auth_tag_len = 16; } else { /* MODE_CCM and MODE_CCM_8 */ /* The nonce for CCM and GCM are the same, but the nonce is used as input * in the CCM algorithm described in RFC 3610. The nonce generated here is * the one from RFC 3610 sect 2.3. Encryption. */ +#ifndef HAVE_LIBGCRYPT_AEAD + memmove(gcm_nonce + 1, gcm_nonce, 4 + SSL_EX_NONCE_LEN_GCM); /* Flags: (L-1) ; L = 16 - 1 - nonceSize */ gcm_nonce[0] = 3 - 1; - /* struct { opaque salt[4]; opaque nonce_explicit[8] } CCMNonce (RFC 6655) */ - memcpy(gcm_nonce + 1, decoder->write_iv.data, decoder->write_iv.data_len); /* salt */ - memcpy(gcm_nonce + 1 + decoder->write_iv.data_len, in, SSL_EX_NONCE_LEN_GCM); gcm_nonce[4 + SSL_EX_NONCE_LEN_GCM + 3] = 1; +#endif + auth_tag_len = decoder->cipher_suite->mode == MODE_CCM_8 ? 8 : 16; + } + + if (inl < SSL_EX_NONCE_LEN_GCM + auth_tag_len) { + ssl_debug_printf("%s failed: missing tag of length %d, available %d\n", G_STRFUNC, auth_tag_len, inl); + return -1; + } + /* Decrypted fragment without explicit nonce and trailing auth tag. */ + worklen = inl - SSL_EX_NONCE_LEN_GCM - auth_tag_len; + +#ifdef HAVE_LIBGCRYPT_AEAD + gcry_cipher_reset(decoder->evp); + ssl_print_data("nonce", gcm_nonce, 12); + err = gcry_cipher_setiv(decoder->evp, gcm_nonce, 12); + if (err) { + ssl_debug_printf("%s failed to set nonce: %s\n", G_STRFUNC, gcry_strerror(err)); + return -1; + } + + if (decoder->cipher_suite->mode == MODE_CCM || decoder->cipher_suite->mode == MODE_CCM_8) { + guint64 lengths[3] = { + worklen, /* size of plaintext */ + 13, /* size of additional authenticated data */ + auth_tag_len + }; + gcry_cipher_ctl(decoder->evp, GCRYCTL_SET_CCM_LENGTHS, lengths, sizeof(lengths)); } - pad = gcry_cipher_setctr (decoder->evp, gcm_nonce, sizeof (gcm_nonce)); - if (pad != 0) { + guchar aad[13]; + // XXX sequence number is sometimes one too small, why? + phton64(aad, decoder->seq); /* record sequence number */ + aad[8] = ct; /* TLSCompressed.type */ + phton16(aad + 9, record_version); /* TLSCompressed.version */ + phton16(aad + 11, worklen); /* TLSCompressed.length */ + ssl_print_data("AAD", aad, sizeof(aad)); + err = gcry_cipher_authenticate(decoder->evp, aad, sizeof(aad)); + if (err) { + ssl_debug_printf("%s failed to set AAD: %s\n", G_STRFUNC, gcry_strerror(err)); + return -1; + } +#else + (void)record_version; + err = gcry_cipher_setctr(decoder->evp, gcm_nonce, sizeof(gcm_nonce)); + if (err != 0) { ssl_debug_printf("ssl_decrypt_record failed: failed to set CTR: %s %s\n", - gcry_strsource (pad), gcry_strerror (pad)); + gcry_strsource (err), gcry_strerror (err)); return -1; } +#endif inl -= SSL_EX_NONCE_LEN_GCM; in += SSL_EX_NONCE_LEN_GCM; + } else { + /* For other ciphers (like CBC), assume plaintext to have the same + * length as its input. Padding will be removed later. */ + worklen = inl; } /* First decrypt*/ - if ((pad = ssl_cipher_decrypt(&decoder->evp, out_str->data, out_str->data_len, in, inl))!= 0) { + memset(out_str->data, 0xcc, out_str->data_len); /// XXX remove me + if ((pad = ssl_cipher_decrypt(&decoder->evp, out_str->data, out_str->data_len, in, worklen)) != 0) { ssl_debug_printf("ssl_decrypt_record failed: ssl_cipher_decrypt: %s %s\n", gcry_strsource (pad), gcry_strerror (pad)); return -1; } - ssl_print_data("Plaintext", out_str->data, inl); - worklen=inl; + ssl_print_data("Plaintext", out_str->data, worklen); - /* RFC 5116 sect 5.1/5.3: AES128/256 GCM/CCM uses 16 bytes for auth tag - * RFC 6655 sect 6.1: AEAD_AES_128_CCM uses 16 bytes for auth tag */ - if (decoder->cipher_suite->mode == MODE_GCM || - decoder->cipher_suite->mode == MODE_CCM) { - if (worklen < 16) { - ssl_debug_printf("ssl_decrypt_record failed: missing tag, work %d\n", worklen); - return -1; - } - /* XXX - validate auth tag */ - worklen -= 16; - } - /* RFC 6655 sect 6.1: AEAD_AES_128_CCM_8 uses 8 bytes for auth tag */ - if (decoder->cipher_suite->mode == MODE_CCM_8) { - if (worklen < 8) { - ssl_debug_printf("ssl_decrypt_record failed: missing tag, work %d\n", worklen); - return -1; - } - /* XXX - validate auth tag */ - worklen -= 8; - } /* strip padding for GenericBlockCipher */ if (decoder->cipher_suite->mode == MODE_CBC) { @@ -3716,8 +3766,24 @@ ssl_decrypt_record(SslDecryptSession*ssl,SslDecoder* decoder, gint ct, } worklen-=ssl_cipher_suite_dig(decoder->cipher_suite)->len; mac = out_str->data + worklen; - } else /* if (decoder->cipher_suite->mode == MODE_GCM) */ { - /* GenericAEADCipher has no MAC */ + } else /* if (ssl_is_aead_cipher(decoder->cipher_suite->mode)) */ { + /* GenericAEADCipher has no MAC, check auth tag for authenticity. */ +#ifdef HAVE_LIBGCRYPT_AEAD + guchar auth_tag[16]; + err = gcry_cipher_gettag(decoder->evp, auth_tag, auth_tag_len); + if (err) { + ssl_debug_printf("%s failed: cannot obtain tag: %s %s\n", G_STRFUNC, + gcry_strsource(err), gcry_strerror(err)); + } else if (!memcmp(auth_tag, in + worklen, auth_tag_len)) { + ssl_debug_printf("%s auth tag OK\n", G_STRFUNC); + } else { + ssl_debug_printf("%s auth tag mismatch\n", G_STRFUNC); + ssl_print_data("auth_tag(expect)", auth_tag, auth_tag_len); + ssl_print_data("auth_tag(actual)", in + worklen, auth_tag_len); + } +#else + ssl_debug_printf("Libgcrypt is older than 1.6, unable to verify authenticity!\n"); +#endif goto skip_mac; } diff --git a/epan/dissectors/packet-ssl-utils.h b/epan/dissectors/packet-ssl-utils.h index 8760103922..50240b5351 100644 --- a/epan/dissectors/packet-ssl-utils.h +++ b/epan/dissectors/packet-ssl-utils.h @@ -268,6 +268,19 @@ typedef enum { MODE_CCM_8 /* AEAD_AES_{128,256}_CCM with 8 byte auth tag */ } ssl_cipher_mode_t; +static inline gboolean +ssl_is_aead_cipher(ssl_cipher_mode_t mode) +{ + switch (mode) { + case MODE_GCM: + case MODE_CCM: + case MODE_CCM_8: + return TRUE; + default: + return FALSE; + } +} + /* Explicit nonce length */ #define SSL_EX_NONCE_LEN_GCM 8 /* RFC 5288 - section 3 */ @@ -544,6 +557,7 @@ ssl_change_cipher(SslDecryptSession *ssl_session, gboolean server); @param ssl ssl_session the store all the session data @param decoder the stream decoder to be used @param ct the content type of this ssl record + @param record_version the version as contained in the record @param in a pointer to the ssl record to be decrypted @param inl the record length @param comp_str a pointer to the store the compression data @@ -551,8 +565,8 @@ ssl_change_cipher(SslDecryptSession *ssl_session, gboolean server); @param outl the decrypted data len @return 0 on success */ extern gint -ssl_decrypt_record(SslDecryptSession* ssl,SslDecoder* decoder, gint ct, - const guchar* in, guint inl, StringInfo* comp_str, StringInfo* out_str, guint* outl); +ssl_decrypt_record(SslDecryptSession *ssl, SslDecoder *decoder, guint8 ct, guint16 record_version, + const guchar *in, guint16 inl, StringInfo *comp_str, StringInfo *out_str, guint *outl); /* Common part bitween SSL and DTLS dissectors */ diff --git a/epan/dissectors/packet-ssl.c b/epan/dissectors/packet-ssl.c index ee016e389f..2a47cf7e03 100644 --- a/epan/dissectors/packet-ssl.c +++ b/epan/dissectors/packet-ssl.c @@ -884,8 +884,8 @@ dissect_ssl(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_) * length "ssl_decrypted_data_avail". */ static gboolean -decrypt_ssl3_record(tvbuff_t *tvb, packet_info *pinfo, guint32 offset, - guint32 record_length, guint8 content_type, SslDecryptSession *ssl, +decrypt_ssl3_record(tvbuff_t *tvb, packet_info *pinfo, guint32 offset, SslDecryptSession *ssl, + guint8 content_type, guint16 record_version, guint16 record_length, gboolean allow_fragments) { gboolean success; @@ -923,9 +923,9 @@ decrypt_ssl3_record(tvbuff_t *tvb, packet_info *pinfo, guint32 offset, /* run decryption and add decrypted payload to protocol data, if decryption * is successful*/ ssl_decrypted_data_avail = ssl_decrypted_data.data_len; - success = ssl_decrypt_record(ssl, decoder, - content_type, tvb_get_ptr(tvb, offset, record_length), - record_length, &ssl_compressed_data, &ssl_decrypted_data, &ssl_decrypted_data_avail) == 0; + success = ssl_decrypt_record(ssl, decoder, content_type, record_version, + tvb_get_ptr(tvb, offset, record_length), record_length, + &ssl_compressed_data, &ssl_decrypted_data, &ssl_decrypted_data_avail) == 0; /* */ if (!success) { /* save data to update IV if valid session key is obtained later */ @@ -1526,7 +1526,7 @@ dissect_ssl3_record(tvbuff_t *tvb, packet_info *pinfo, * } TLSPlaintext; */ guint32 record_length; - guint16 version; + guint16 record_version, version; guint8 content_type; guint8 next_byte; proto_tree *ti; @@ -1588,6 +1588,7 @@ dissect_ssl3_record(tvbuff_t *tvb, packet_info *pinfo, */ content_type = tvb_get_guint8(tvb, offset); version = tvb_get_ntohs(tvb, offset + 1); + record_version = version; record_length = tvb_get_ntohs(tvb, offset + 3); if (ssl_is_valid_content_type(content_type)) { @@ -1698,7 +1699,8 @@ dissect_ssl3_record(tvbuff_t *tvb, packet_info *pinfo, * used as 'key' to identify this record in the packet (we can have multiple * handshake records in the same frame). */ if (ssl) { - decrypt_ssl3_record(tvb, pinfo, offset, record_length, content_type, ssl, + decrypt_ssl3_record(tvb, pinfo, offset, ssl, + content_type, record_version, record_length, content_type == SSL_ID_APP_DATA || content_type == SSL_ID_HANDSHAKE); } -- cgit v1.2.1