-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
System.Security.Cryptography.AesGcm - decryption allows truncated tags #71366
Comments
Tagging subscribers to this area: @dotnet/area-system-security, @vcsjones Issue Detailsvoid Main()
{
Span<byte> plaintext = Encoding.UTF8.GetBytes("[Hello World!]");
Span<byte> key = new byte[32];
Span<byte> nonce = new byte[12];
Span<byte> ciphertext = new byte[plaintext.Length];
Span<byte> tag = new byte[16];
using var gcm = new AesGcm(key);
Encoding.UTF8.GetString(plaintext).Dump();
gcm.Encrypt(nonce, plaintext, ciphertext, tag);
plaintext.Clear();
gcm.Decrypt(nonce, ciphertext, tag.Slice(0, 12), plaintext);
Encoding.UTF8.GetString(plaintext).Dump();
} Scenario:
Would that suggest that
|
Tag truncation is just how AES GCM supports short tags, as demonstrated by: using System.Security.Cryptography;
AesGcm aes = new AesGcm(new byte[128 / 8]);
byte[] nonce = new byte[96 / 8];
byte[] tag16 = new byte[16];
aes.Encrypt(nonce, new byte[32], new byte[32], tag16);
byte[] tag12 = new byte[12];
aes.Encrypt(nonce, new byte[32], new byte[32], tag12);
Console.WriteLine(Convert.ToHexString(tag16));
Console.WriteLine(Convert.ToHexString(tag12));
// Prints:
// 40490AF4805606B2A3A2E793E3500066
// 40490AF4805606B2A3A2E793 This is not a .NET-ism, either. For example, Ruby: require 'openssl'
cipher = OpenSSL::Cipher.new('aes-128-gcm').encrypt
cipher.key = "\0" * 16
cipher.iv = "\0" * 12
cipher.auth_data = ""
encrypted = cipher.update('a' * 32) + cipher.final
tag16 = cipher.auth_tag
cipher = OpenSSL::Cipher.new('aes-128-gcm').decrypt
cipher.key = "\0" * 16
cipher.iv = "\0" * 12
tag12 = tag16.slice(0, 12)
cipher.auth_tag = tag12
decrypted = cipher.update(encrypted) + cipher.final
puts decrypted Ruby will even let you truncate it below 12 octets. Since the AES-GCM implementation supports short tags, Consumers of |
Thanks for confirming that my understanding is correct. I see 2 issues:
Given that it seems option-1 is the only practical/reasonable choice, it would also need to be added to Finally, it might be a good idea to also document a caution that Thoughts? |
What GCM specification does
|
Proposed improvements to public class AesGcm
{
// existing ctors
public AesGcm(ReadOnlySpan<byte> key) { }
public AesGcm(byte[] key) { }
// new field
readonly int _fixedTagLength = 0;
// new ctor
public AesGcm(ReadOnlySpan<byte> key, int fixedTagLength)
{
_fixedTagLength = fixedTagLength; // also do [12-16] range check
}
// new ctor
public AesGcm(byte[] key, int fixedTagLength)
{
_fixedTagLength = fixedTagLength; // also do [12-16] range check
} Change internal implementation for static void CheckParameters(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> tag) by adding the following check to it: int fixedTagLength = _fixedTagLength;
if (fixedTagLength != 0 && fixedTagLength != tag.Length)
{
throw new ArgumentException(System.SR.Cryptography_InvalidTagLength, "tag");
} |
Example of a likely vulnerability in Microsoft's code due to incorrect assumptions of what MS domain: AzureAD If I traced their call-paths right, the I'm sure there are many more cases like this one, where the
|
We're exposing GCM in the same way the underlying providers are, which leaves the algorithms loose tag processing as an application problem. (I'd've included the tag length in the tag calculation, but I guess the GCM designers disagree) Generally the application context should know what the tag size is. In the case of JWA, it's supposed to always be 128 bits (https://datatracker.ietf.org/doc/html/rfc7518#section-4.7), which is up to the implementer to verify. Adding ctor overloads for the expected tag size is reasonable, and not limiting to anyone following the SP800-38D recommendation, and I guess helps provide a standardized exception message to anyone who chooses to utilize it. |
Thanks for the response. I would encourage .NET team to consider adding tag-length-fixing constructor overloads to Do note that the Just do a simple sanity test: ask any of your esteemed cryptography-aware colleagues the following simple question:
..and watch how many no's you're going to get. I believe it would be somewhat irresponsible to take the position that "loose AesGcm tag processing" is application's problem. Arguing that .NET team is merely exposing the deficiencies of underlying GCM providers is a dodge at best (at the very least you cannot vouch for all possible GCM providers on all supported current and future .NET-running platforms). However let me provide a specification-based argument:
What this means is that for a specific instance of AesGcm implemented to the spec: var one_gcm = new AesGcm(key); ... Ask any FIPS auditor/validator lab, and they should tell you that the GCM spec wording above is specifically intended to prevent tag truncations under a fixed key. The sister CCM spec (800-38C) which targets the same AEAD behavior has the same constraint:
Thus it is quite likely that the current status of |
I'm not quite sure I follow this logic.
The "selected" tag length just happens to be whatever CCM does not have this problem because CCM bakes the MAC length in to the control information block:
Where t is "The octet length of the MAC." CCM is an entirely different mode. While CCM and GCM are both AEADs I do not think it is entirely reasonable to consider their behaviors and design as interchangeable. GCM has no control information block. By your logic then, is OpenSSL not "spec compliant" for GCM? //tl;dr: OpenSSL encrypting with a 16 byte tag and decrypting with a 12 byte tag.
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
void dump(unsigned char* data, int len)
{
for (int i = 0; i < len; i++) {
printf("%02x", data[i]);
}
putchar('\n');
}
int main(int argc, char *argv[])
{
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
unsigned char key[16] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
unsigned char nonce[12] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
unsigned char plaintext[6] = "hello";
unsigned char decrypted[6] = { 0 };
unsigned char ciphertext[6] = { 0 };
unsigned char tag[16] = { 0 };
if (EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) {
printf("Could not initialize");
abort();
}
if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) {
printf("Could not set key or nonce");
abort();
}
int len = 0, ciphertextLength = 0;
if (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, 6) != 1) {
printf("Encrypt failed.");
abort();
}
ciphertextLength += len;
if (EVP_EncryptFinal_ex(ctx, ciphertext + len, &len) != 1) {
printf("Final failed.");
abort();
}
ciphertextLength += len;
if (ciphertextLength != 6) {
printf("6 bytes in should be 6 bytes out.");
abort();
}
printf("Ciphertext: ");
dump(ciphertext, ciphertextLength);
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, tag) != 1) {
printf("Could not get the tag.");
abort();
}
printf("Tag: ");
dump(tag, sizeof tag);
unsigned char truncatedTag[12] = { 0 };
memcpy(truncatedTag, tag, sizeof truncatedTag);
printf("Truncated Tag: ");
dump(truncatedTag, sizeof truncatedTag);
EVP_CIPHER_CTX_free(ctx);
ctx = EVP_CIPHER_CTX_new();
if (EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) {
printf("Could not initialize");
abort();
}
if (EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) {
printf("Could not set key or nonce");
abort();
}
assert(sizeof truncatedTag == 12);
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, sizeof truncatedTag, truncatedTag)) {
printf("Could not set truncated authentication tag.");
abort();
}
int plaintextLength = 0;
if (EVP_DecryptUpdate(ctx, decrypted, &len, ciphertext, ciphertextLength) != 1) {
printf("Decrypt failed.");
abort();
}
plaintextLength += len;
int result = EVP_DecryptFinal_ex(ctx, plaintext + plaintextLength, &len);
if (result > 0) {
printf("Decrypted successfully");
plaintextLength += len;
}
else {
printf("Decryption failed.");
}
} |
@vcsjones In your C/OpenSSL code you create one GCM context to encrypt (which produces 16-byte tag), then you free it, then you create another new GCM context to decrypt, then you use that 2nd context to successfully decrypt with a truncated 12-byte tag. This is not the correct comparison. I'm not very familiar with low-level OpenSSL APIs - perhaps OpenSSL cannot use the same GCM context to encrypt and decrypt, and thus requires a new context for each operation. Your Ruby example (which seems to wrap OpenSSL) also uses 2 different GCM contexts to encrypt and decrypt. The GCM spec is quite clear (Section-7 "GCM specification") that tag-length is a "prerequisite" (prerequisites are fixed across many invocations of Encrypt or Decrypt function), and must be associated with the key. The GCM decryption function specification (Section 7.2) is explicit in saying that one of its prerequisites is a "supported tag length OpenSSL low-level functions are a toolkit for "build-your-own-GCM" (here's AES, here's CTR-mode, here's GMAC, and here's XOR - go have fun). This is how Golang does it. This should have as much relevance to .NET as how OpenSSL does it (ie. not relevant, but at least Golang does it correctly, by binding tag-size to GCM context and having a default of tag-size=16). |
@sdrapkin Your insistence that our implementation is wrong and/or against spec isn't productive. We have the implementation we have, and we're not going to change it. The specific example that you cited from JWA nearly automatically requires separate contexts that are only tied together by a specification; and NIST often writes "this is how things are" which only means "this is the only acceptable use for US-Gov and impacted parties" (such as that a single RSA key should be PKCS1 xor PSS, or signing xor encryption... but our underlying providers have the same loosy-goosey model we have (which is what informed ours). The usability concern behind "let me tell you what tag size I intend to use at construction time to save me the redundancy of checking tag.Length in my code" is valid. So some new API is justified to enhance that scenario. The cryptography stack in .NET is also mostly intended as low-level primitives. It's a key. Use it however you want. We don't block duplicated nonces... that's an application problem. But many specs will tell you they MUST be unique. At some point things just become a caller/application problem. |
@bartonjs I understand. Thanks for reviewing this issue. Let's do whatever is productive. |
Security check such as tag size checks should not be left to user of libraries. The fact that the possibility of tag size truncation is not even mentioned in the API's clearly shows a lack of responsibility by the creators the API. API's should contain indication on how to use them correctly and securely. Just brushing this off as something that OpenSSL does as well is dishonest - creators or API's are encouraged to think on their own and know their field. Given the track record of OpenSSL implementation and documentation it is not even a good argument by itself. Note that the Java implementation defaults to a 128 bit tag size, and that you need to configure the tag size explicitly using That all said, I think adding the constructors with an explicit tag size is a necessity to create a secure API. Given that the current constructors do not indicate a tag size I would strongly suggest to both deprecate these constructors and provide a clear warning in the documentation. It might be worthwhile to think of a way (factory method?) that defaults to a 128 bit tag size. If the current constructors are not deprecated then devs will simply choose the easier, insecure option with fewer parameters. I do agree with bartonjs that it does make sense to think in solutions; the API cannot be changed in such a way that backwards compatibility is harmed. It may be that the current API is used for e.g. 12 byte tags, where the tag size is validated. |
The top post has been updated with an API proposal. |
Looks good as proposed. namespace System.Security.Cryptography;
public partial class AesGcm {
+ public AesGcm(byte[] key, int tagSizeInBytes);
+ public AesGcm(ReadOnlySpan<byte> key, int tagSizeInBytes);
+ [Obsolete("AesGcm should indicate the required tag size for encryption and decryption. Use a constructor that accepts the tag size.", DiagnosticId = "SYSLIB9999")]
public AesGcm(byte[] key);
+ [Obsolete("AesGcm should indicate the required tag size for encryption and decryption. Use a constructor that accepts the tag size.", DiagnosticId = "SYSLIB9999")]
public AesGcm(ReadOnlySpan<byte> key);
+ public int? TagSizeInBytes { get; }
} |
Added
Tagging @dotnet/compat for awareness of the breaking change. |
Breaking change doc: dotnet/docs#35338 |
Edited by @vcsjones
API Proposal:
As the issue outlines below, this proposes changes to
AesGcm
because it's current design makes it prone to misuse of allowing truncated authentication tags during decryption.This proposes new constructors that accept the authentication tag size. During decryption or encryption, the
tag
parameter must match this specified exactly. This means the caller must decide at instantiation what tag size is correct, not what callers ofEncrypt
orDecrypt
are provided.The current constructors will continue to have the same existing behavior. However, since this is an issue around misuse, this also proposes obsoleting the existing constructors to steer people toward using constructors which take the authentication tag size.
Original issue:
Scenario:
A naive (documentation-reading) user uses
AesGcm
to encrypt plaintext and produce ciphertext with the maximum allowed tag of 16 bytes (ie. naively expecting ~128 bits of tag strength). However theDecrypt
API is quite happy to accept truncated 12-byte tags, which means that the attacker can truncate all tags to 12 bytes for ~96 bits of tag strength (assuming decryption oracle is readily available).AesGcm
n
-bit tags provide onlyn-k
bits of authentication security for messages that are2^k
blocks longWould that suggest that
AesGcm
tags as implemented in .NET realistically provide close to96-32=64
bits of security? If yes - is anyone concerned about that? (both in terms of implementation/security and in terms of documentation). If no - please explain and help me understand what I'm missing. Thanks.The text was updated successfully, but these errors were encountered: