Closed sdrapkin closed 1 year ago
Tagging subscribers to this area: @dotnet/area-system-security, @vcsjones See info in area-owners.md if you want to be subscribed.
Author: | sdrapkin |
---|---|
Assignees: | - |
Labels: | `area-System.Security` |
Milestone: | - |
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, Decrypt
can't tell the difference between a tag that was truncated legitimately or illegitimately.
Consumers of AesGcm
should ensure that the tag length that is being supplied is the expected length.
Since the AES-GCM implementation supports short tags,
Decrypt
can't tell the difference between a tag that was truncated legitimately or illegitimately. Consumers ofAesGcm
should ensure that the tag length that is being supplied is the expected length.
Thanks for confirming that my understanding is correct.
I see 2 issues:
AesGcm
consumers are expected to ensure for tag lengths. The description for the tag
parameter to .Decrypt
states: The authentication tag produced for this message during encryption
(which is clearly incorrect when truncated tags are accepted, since they were likely never produced by the Encryption).Decrypt
unsafe for direct exposure (ie. requires a tag-length validator in front of it). N must become part of the caller-state/protocol, since it is not part of the Encrypt
output.AesGcm
primitive API alone. Our key assumption is that ciphertext
and tag
are malleable (which is precisely what the tag is supposed to guard against). While the tag-length could be made non-malleable (option-1), ciphertext-length cannot be made non-malleable. While Microsoft (rightfully) does not prescribe how the tag and ciphertext are to be stored, in the vast majority of uses they are concatenated into one bytestream (order is not relevant). Thus, it is not possible to calculate the correct value of tag-length N from the (ciphertext+tag) bytestream. One approach is to call Decrypt
5 times for each of the 5 possible values of N (not an appealing idea for obvious reasons).Given that it seems option-1 is the only practical/reasonable choice, it would also need to be added to AesGcm
documentation (ie. guidance that N must be explicitly fixed for safe operation of Decrypt
, which includes tag validation).
Finally, it might be a good idea to also document a caution that AesGcm
tag of length N bytes should not be assumed to provide N*8 bits of authentication strength in all scenarios.
Thoughts?
What GCM specification does S.S.C.AesGcm
actually implement?
AEAD_AES_128_GCM
and AEAD_AES_256_GCM
. Both fix the tag-length to 16 bytes.[tag length t]
from among the supported choices shall be associated with each key." (which S.S.C.AesGcm
does not do)T
as the Encryption-step-produced tag, T'
as the computed Decryption-step-produced tag, and states that "If the two tags match (in both length and value), then the ciphertext is returned. Otherwise, the special symbol FAIL is returned." (which is obviously not how S.S.C.AesGcm
behaves)Proposed improvements to AesGcm
APIs (non-breaking, backwards-compatible):
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 AesGcm.Decrypt
does/doesn't do:
MS domain: AzureAD Repo: azure-activedirectory-identity-model-extensions-for-dotnet Code in question
If I traced their call-paths right, the authenticationTag
is coming straight from JWT token field, which is treated as any-length base64-encoded string. An attacker can be truncating all JWT tokens to 12-byte Auth-tags which reduces GCM tag strength to 96 bits of security at most. I doubt AzureAD team realizes this...
I'm sure there are many more cases like this one, where the authentication Tag
lives separately from ciphertext
(ie. not a mere take X fixed bytes from the end of ciphertext
slicing).
AesGcm
not having safe-by-default constructors (that default to fixed tag of 16 bytes) is going to be the cause of numerous security vulnerabilities. In the meantime I've created AesGcmStrict wrapper to help deal with this.
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 AesGcm
, based on @bartonjs reply.
Do note that the AesCcm
behavior does not have the same problem - ie. truncated AesCcm
tags (that are still length-permitted based on AesCcm.TagByteSizes
) will fail to decrypt. This creates 2 different (and undocumented) behaviors for AesGcm
and AesCcm
APIs in .NET.
Just do a simple sanity test: ask any of your esteemed cryptography-aware colleagues the following simple question:
I've got an
AesGcm
-encrypted ciphertext, and I'm trying to decrypt it with a truncated tag - will it work?
..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:
5.2.1.2 Output Data
t
is the bit-length of tag T
5.2.2 Authenticated Decryption Function
What this means is that for a specific instance of AesGcm implemented to the spec:
var one_gcm = new AesGcm(key);
...one_gcm.Decrypt(...)
MUST NOT accept tag-lengths that differ from those produced by one_gcm.Encrypt(...)
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:
Tlen
, is a parameter of the mode that shall be fixed for all invocations of CCM with the given key."Thus it is quite likely that the current status of AesGcm
in .NET is that it is not spec-compliant.
Thus it is quite likely that the current status of AesGcm in .NET is that it is not spec-compliant.
I'm not quite sure I follow this logic.
Given the selection of an approved block cipher, key, and an associated tag length, the inputs to
The "selected" tag length just happens to be whatever tag.Length
is.
CCM does not have this problem because CCM bakes the MAC length in to the control information block:
The leading octet of the first block of the formatting, B0, contains four flags for control information: two single bits, called Reserved and Adata, and two strings of three bits, to encode the values t and q.
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 t
associated with the key".
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 GcmParameterSpec
. So there are certainly implementations that handle this better. Of course, since the tag size is not mentioned explicitly in the list of Java supported algorithms, other providers within that eco-system could choose otherwise. There is also the issue that tag verification is not explicit in the Java API. The Java API is far from perfect, but it does show that other libraries do validate the complete tag.
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 needs-breaking-change-doc-created
label because this issue has the breaking-change
label.
needs-breaking-change-doc-created
label.Tagging @dotnet/compat for awareness of the breaking change.
Breaking change doc: https://github.com/dotnet/docs/issues/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.