Skip to content
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
f5b1d82
xaes-256-gcm with evp_cipher
ttungle96 Oct 14, 2025
3b7d63a
xaes-256-gcm with evp_cipher
ttungle96 Oct 15, 2025
7b2af15
xaes-256-gcm with evp_cipher
ttungle96 Oct 15, 2025
debc4f8
xaes-256-gcm with evp_cipher
ttungle96 Oct 15, 2025
3d22930
xaes-256-gcm with evp_cipher
ttungle96 Oct 15, 2025
f5e7eae
full test coverage
ttungle96 Oct 15, 2025
66a9190
full test coverage
ttungle96 Oct 15, 2025
aa801fe
increase test coverage
ttungle96 Oct 15, 2025
7c58649
Merge branch 'xaes-256-gcm' into xaes-256-gcm
ttungle96 Oct 15, 2025
c33f280
improve test coverage
ttungle96 Oct 15, 2025
4610415
Merge branch 'xaes-256-gcm' of https://github.com/ttungle96/aws-lc in…
ttungle96 Oct 15, 2025
f0c9e37
increase test coverage
ttungle96 Oct 15, 2025
39e6b19
improve test coverage
ttungle96 Oct 15, 2025
20feacc
Final
ttungle96 Oct 15, 2025
88051b8
Final
ttungle96 Oct 15, 2025
fc0ac84
Final
ttungle96 Oct 15, 2025
98e525b
Final
ttungle96 Oct 16, 2025
1d4b4da
Final
ttungle96 Oct 16, 2025
84f7160
resolve issues
ttungle96 Oct 28, 2025
556612a
update
ttungle96 Oct 28, 2025
2a2f9b2
update
ttungle96 Oct 28, 2025
7cd2e47
update
ttungle96 Oct 28, 2025
79084fd
add flag
ttungle96 Oct 28, 2025
f0f66cd
add flag
ttungle96 Oct 28, 2025
7086831
remove flag
ttungle96 Oct 28, 2025
c6e3cab
Final
ttungle96 Oct 28, 2025
900ede2
Final
ttungle96 Oct 28, 2025
569929a
Final
ttungle96 Oct 28, 2025
6ea5729
Final
ttungle96 Oct 28, 2025
49e14ec
Final
ttungle96 Oct 28, 2025
adb0153
Final
ttungle96 Oct 28, 2025
15e8f37
Final
ttungle96 Oct 28, 2025
d39bcdd
Final
ttungle96 Oct 28, 2025
6183644
Add xaes-256-gcm to speed.cc
ttungle96 Oct 29, 2025
3bcea85
Add evp-xaes-256-gcm to speed.cc
ttungle96 Oct 29, 2025
be8331e
Add evp-xaes-256-gcm to speed.cc
ttungle96 Oct 29, 2025
37e232b
Final
ttungle96 Oct 29, 2025
9d3438c
Resolve code review comments
ttungle96 Oct 31, 2025
9134a92
final
ttungle96 Oct 31, 2025
6275ef6
final
ttungle96 Oct 31, 2025
89d5521
final
ttungle96 Oct 31, 2025
eb6d0ca
final
ttungle96 Oct 31, 2025
2f06f29
final
ttungle96 Oct 31, 2025
a7b345e
final
ttungle96 Oct 31, 2025
b840a07
Change data type name of ctx
ttungle96 Oct 31, 2025
62fc5e3
BE
ttungle96 Oct 31, 2025
687727f
add comments, init 0
ttungle96 Nov 4, 2025
bb45cae
remove redundant check
ttungle96 Nov 4, 2025
f344725
Add test cases
ttungle96 Nov 4, 2025
0d9cfa0
update
ttungle96 Nov 4, 2025
f8c228a
Fix
ttungle96 Nov 5, 2025
51f65a8
Fix
ttungle96 Nov 5, 2025
335d49e
add comments
ttungle96 Nov 5, 2025
fc4b301
fix tests
ttungle96 Nov 5, 2025
d42c4c1
fix tests
ttungle96 Nov 5, 2025
a258173
final
ttungle96 Nov 5, 2025
a8bf03b
test remove padding
ttungle96 Nov 6, 2025
d2cbdd5
BE
ttungle96 Nov 6, 2025
c2cd9aa
BE
ttungle96 Nov 6, 2025
8dd784e
add alignment
ttungle96 Nov 6, 2025
13f55b7
BE
ttungle96 Nov 6, 2025
0a151a8
add comment
ttungle96 Nov 7, 2025
fc77e6c
add tests
ttungle96 Nov 7, 2025
7b0443e
Final
ttungle96 Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crypto/cipher_extra/cipher_extra.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ static const struct {
{NID_aes_256_ctr, "aes-256-ctr", EVP_aes_256_ctr},
{NID_aes_256_ecb, "aes-256-ecb", EVP_aes_256_ecb},
{NID_aes_256_gcm, "aes-256-gcm", EVP_aes_256_gcm},
{NID_xaes_256_gcm, "xaes-256-gcm", EVP_xaes_256_gcm},
{NID_aes_256_ofb128, "aes-256-ofb", EVP_aes_256_ofb},
{NID_aes_256_xts, "aes-256-xts", EVP_aes_256_xts},
{NID_chacha20_poly1305, "chacha20-poly1305", EVP_chacha20_poly1305},
Expand All @@ -114,7 +115,8 @@ static const struct {
{"aes128", "aes-128-cbc"},
{"id-aes128-gcm", "aes-128-gcm"},
{"id-aes192-gcm", "aes-192-gcm"},
{"id-aes256-gcm", "aes-256-gcm"}
{"id-aes256-gcm", "aes-256-gcm"},
{"id-xaes256-gcm", "xaes-256-gcm"}
};

const EVP_CIPHER *EVP_get_cipherbynid(int nid) {
Expand Down
100 changes: 100 additions & 0 deletions crypto/cipher_extra/cipher_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <openssl/span.h>
#include <openssl/digest.h>

#include "../internal.h"
#include "../test/file_test.h"
Expand Down Expand Up @@ -127,6 +128,8 @@ static const EVP_CIPHER *GetCipher(const std::string &name) {
return EVP_aes_192_ccm();
} else if (name == "AES-256-CCM") {
return EVP_aes_256_ccm();
} else if (name == "XAES-256-GCM") {
return EVP_xaes_256_gcm();
}
return nullptr;
}
Expand Down Expand Up @@ -1080,6 +1083,7 @@ TEST(CipherTest, GetCipher) {
test_get_cipher(NID_aes_256_ctr, "aes-256-ctr");
test_get_cipher(NID_aes_256_ecb, "aes-256-ecb");
test_get_cipher(NID_aes_256_gcm, "aes-256-gcm");
test_get_cipher(NID_xaes_256_gcm, "xaes-256-gcm");
test_get_cipher(NID_aes_256_ofb128, "aes-256-ofb");
test_get_cipher(NID_aes_256_xts, "aes-256-xts");
test_get_cipher(NID_chacha20_poly1305, "chacha20-poly1305");
Expand All @@ -1102,6 +1106,7 @@ TEST(CipherTest, GetCipher) {
test_get_cipher(NID_aes_128_gcm, "id-aes128-gcm");
test_get_cipher(NID_aes_192_gcm, "id-aes192-gcm");
test_get_cipher(NID_aes_256_gcm, "id-aes256-gcm");
test_get_cipher(NID_xaes_256_gcm, "id-xaes256-gcm");

// error case
EXPECT_FALSE(EVP_get_cipherbyname(nullptr));
Expand Down Expand Up @@ -1454,3 +1459,98 @@ TEST(CipherTest, Empty_EVP_CIPHER_CTX_V1187459157) {
CHECK_ERROR(EVP_DecryptUpdate(ctx.get(), out_vec.data(), &out_len, in_vec.data(), in_len), ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
CHECK_ERROR(EVP_DecryptFinal(ctx.get(), out_vec.data(), &out_len), ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
}

TEST(CipherTest, XAES_256_GCM_EVP_CIPHER) {
// Test invalid nonce sizes and key length
{
std::vector<uint8_t> key(32), nonce(24);

// XAES-256-GCM Encryption
bssl::UniquePtr<EVP_CIPHER_CTX> ctx(EVP_CIPHER_CTX_new());
ASSERT_TRUE(ctx);
ASSERT_TRUE(EVP_CipherInit_ex(ctx.get(), EVP_xaes_256_gcm(), NULL, NULL, NULL, 1));

// Valid nonce size: 20 bytes <= |N| <= 24 bytes
// Test invalid nonce size
ASSERT_TRUE(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_AEAD_SET_IVLEN, 19, NULL));
ASSERT_FALSE(EVP_CipherInit_ex(ctx.get(), NULL, NULL, key.data(), nonce.data(), -1));
ASSERT_TRUE(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_AEAD_SET_IVLEN, 25, NULL));
ASSERT_FALSE(EVP_CipherInit_ex(ctx.get(), NULL, NULL, key.data(), nonce.data(), -1));
ASSERT_TRUE(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_AEAD_SET_IVLEN, 24, NULL));

// Valid key length: 32 bytes
// Test invalid key length
ctx.get()->key_len = 24;
ASSERT_FALSE(EVP_CipherInit_ex(ctx.get(), NULL, NULL, key.data(), nonce.data(), -1));
}

// Source of multi-loop tests:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Are there KATs (known-answer tests) that test the streaming API, i.e. calling CipherUpdate multiple times?
  • are the shorter nonces tested at least with a self-consistency test, i.e. we can decrypt what we encrypt?
  • what does multi-loop here stand for?

Copy link
Author

@ttungle96 ttungle96 Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • As you see in my implementation, all functions of aes-gcm are reused, except the init one (aes_gcm_init is replaced by xaes_256_gcm_init). Invoking CipherUpdate again will have the same behavior as the first time it is called. The KATs available with too small plaintext sizes, so I do not need to call it multiple times. Maybe speedtool tests that behavior, and it is still fine.

  • I do not have KATs with shorter nonces. I can write tests if required. I'm not sure whether that is necessary since it is a small change:
    From:
    aes_gcm_init_key(ctx, derived_key, nonce + AES_GCM_NONCE_LENGTH, enc);
    To:
    aes_gcm_init_key(ctx, derived_key, nonce + ivlen - AES_GCM_NONCE_LENGTH, enc);

  • I tried to get all KATs available. Multi-loop tests are from: https://github.com/C2SP/C2SP/blob/main/XAES-256-GCM/go/XAES-256-GCM_test.go. I just rewrite it in C/C++.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see tests for the following usecases, unless you're planning on doing them in a subsequent PR (or point me to them if I missed them):

  • Are there KATs (known-answer tests) that test the streaming API, i.e. calling CipherUpdate multiple times?
  • are the shorter nonces tested at least with a self-consistency test, i.e. we can decrypt what we encrypt?

Copy link
Author

@ttungle96 ttungle96 Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// https://github.com/C2SP/C2SP/blob/main/XAES-256-GCM/go/XAES-256-GCM_test.go
const auto test = [](int n, const char *output) {
bssl::ScopedEVP_MD_CTX s;
ASSERT_TRUE(EVP_DigestInit(s.get(), EVP_shake128()));
bssl::ScopedEVP_MD_CTX d;
ASSERT_TRUE(EVP_DigestInit(d.get(), EVP_shake128()));

bssl::UniquePtr<EVP_CIPHER_CTX> ctx(EVP_CIPHER_CTX_new());
ASSERT_TRUE(ctx);
ASSERT_TRUE(EVP_CipherInit_ex(ctx.get(), EVP_xaes_256_gcm(), NULL, NULL, NULL, 1));
ASSERT_TRUE(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_AEAD_SET_IVLEN, 24, NULL));

bssl::UniquePtr<EVP_CIPHER_CTX> dctx(EVP_CIPHER_CTX_new());
ASSERT_TRUE(dctx);
ASSERT_TRUE(EVP_DecryptInit_ex(dctx.get(), EVP_xaes_256_gcm(), NULL, NULL, NULL));
ASSERT_TRUE(EVP_CIPHER_CTX_ctrl(dctx.get(), EVP_CTRL_AEAD_SET_IVLEN, 24, NULL));

std::vector<uint8_t> key(32), nonce(24), plaintext(256);
std::vector<uint8_t> aad(256), ciphertext(256), tag(16);
uint8_t plaintext_len = 0, aad_len = 0;

int tag_size = 16;

for(int i = 0; i < n; ++i) {
ASSERT_TRUE(EVP_DigestSqueeze(s.get(), key.data(), 32));
ASSERT_TRUE(EVP_DigestSqueeze(s.get(), nonce.data(), 24));
ASSERT_TRUE(EVP_DigestSqueeze(s.get(), &plaintext_len, 1));
ASSERT_TRUE(EVP_DigestSqueeze(s.get(), plaintext.data(), plaintext_len));
ASSERT_TRUE(EVP_DigestSqueeze(s.get(), &aad_len, 1));
ASSERT_TRUE(EVP_DigestSqueeze(s.get(), aad.data(), aad_len));

// XAES-256-GCM Encryption
ASSERT_TRUE(EVP_CipherInit_ex(ctx.get(), NULL, NULL, key.data(), nonce.data(), -1));
ASSERT_EQ(aad_len, EVP_Cipher(ctx.get(), NULL, aad.data(), aad_len));
int ciphertext_len = 0;
ASSERT_TRUE(EVP_CipherUpdate(ctx.get(), ciphertext.data(), &ciphertext_len,
plaintext.data(), plaintext_len));

int len = 0;
ASSERT_TRUE(EVP_CipherFinal_ex(ctx.get(), ciphertext.data() + ciphertext_len, &len));
ciphertext_len += len;
ASSERT_TRUE(EVP_DigestUpdate(d.get(), ciphertext.data(), ciphertext_len));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this line for as one as l.1532? It would be good if the flow here has comments as it is not very clear to me.

Copy link
Author

@ttungle96 ttungle96 Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already added the reference. The source of this test is at:
https://github.com/C2SP/C2SP/blob/main/XAES-256-GCM/go/XAES-256-GCM_test.go

I just rewrite it in C++. It uses hash function SHAKE128 to generate data. Also, verifying the ciphertext output is done by that hash function. It verifies the ciphertext output of each iteration, then compares the final digest output after 10K iterations, 1M iterations.

EVP_DigestUpdate(d.get(), ciphertext.data(), ciphertext_len);
It accumulates ciphertext.data() of ciphertext_len bytes into the digest output.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accumulation is necessary to match the final result in that linked file, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It is correct! The output of each iteration is accumulated into the final result of hash.


ASSERT_TRUE(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_AEAD_GET_TAG, tag_size, tag.data()));
ASSERT_TRUE(EVP_DigestUpdate(d.get(), tag.data(), tag_size));

// XAES-256-GCM Decryption
ASSERT_TRUE(EVP_DecryptInit_ex(dctx.get(), NULL, NULL, key.data(), nonce.data()));
ASSERT_TRUE(EVP_CIPHER_CTX_ctrl(dctx.get(), EVP_CTRL_AEAD_SET_TAG, tag_size, tag.data()));

std::vector<uint8_t> decrypted;
decrypted.resize(plaintext_len);
len = 0;
EVP_DecryptUpdate(dctx.get(), NULL, &len, aad.data(), aad_len);
ASSERT_TRUE(EVP_DecryptUpdate(dctx.get(), decrypted.data(), &len, ciphertext.data(), ciphertext_len));
ASSERT_TRUE(EVP_DecryptFinal(dctx.get(), decrypted.data() + len, &len));

ASSERT_EQ(Bytes(decrypted), Bytes(plaintext.data(), plaintext_len));
}
std::vector<uint8_t> expected;
ASSERT_TRUE(ConvertToBytes(&expected, output));
uint8_t got[32] = {0};
ASSERT_TRUE(EVP_DigestFinalXOF(d.get(), got, 32));
ASSERT_EQ(Bytes(got, 32), Bytes(expected));
};

test(10000, "e6b9edf2df6cec60c8cbd864e2211b597fb69a529160cd040d56c0c210081939");
test(1000000, "2163ae1445985a30b60585ee67daa55674df06901b890593e824b8a7c885ab15");
}
26 changes: 26 additions & 0 deletions crypto/cipher_extra/test/cipher_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,32 @@ AAD = feedfacedeadbeeffeedfacedeadbeefabaddad2
Tag = a44a8266ee1c8eb0c8b5d4cf5ae9f19b
Operation = InvalidDecrypt

# Source of test vectors:
# https://github.com/C2SP/C2SP/blob/main/XAES-256-GCM.md
Cipher = XAES-256-GCM
Key = 0101010101010101010101010101010101010101010101010101010101010101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you ensured these test vectors are being used, e.g. by inserting an error in one of them.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are being used in cipher_test.cc. I verified like what you said. I edited the test case to make it different and when I run test, it shows errors and fails the test.

IV = 424242424242424242424242424242424242424242424242
Plaintext = 48656c6c6f2c20584145532d3235362d47434d21
Ciphertext = 01e5f78bc99de880bd2eeff2870d361f0eab5b2f
AAD =
Tag = c55268f34b14045878fe3668db980319

Cipher = XAES-256-GCM
Key = 0101010101010101010101010101010101010101010101010101010101010101
IV = 4142434445464748494a4b4c4d4e4f505152535455565758
Plaintext = 584145532d3235362d47434d
Ciphertext = ce546ef63c9cc60765923609
AAD =
Tag = b33a9a1974e96e52daf2fcf7075e2271

Cipher = XAES-256-GCM
Key = 0303030303030303030303030303030303030303030303030303030303030303
IV = 4142434445464748494a4b4c4d4e4f505152535455565758
Plaintext = 584145532d3235362d47434d
Ciphertext = 986ec1832593df5443a17943
AAD = 633273702e6f72672f584145532d3235362d47434d
Tag = 7fd083bf3fdb41abd740a21f71eb769d

# local add-ons, primarily streaming ghash tests
# 128 bytes aad
Cipher = AES-128-GCM
Expand Down
167 changes: 164 additions & 3 deletions crypto/fipsmodule/cipher/e_aes.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
#include <openssl/mem.h>
#include <openssl/nid.h>
#include <openssl/rand.h>
#include <openssl/cmac.h>

#include <openssl/bytestring.h>
#include "../../internal.h"
Expand Down Expand Up @@ -353,9 +354,19 @@ static EVP_AES_GCM_CTX *aes_gcm_from_cipher_ctx(EVP_CIPHER_CTX *ctx) {

// |malloc| guarantees up to 4-byte alignment on 32-bit and 8-byte alignment
// on 64-bit systems, so we need to adjust to reach 16-byte alignment.
assert(ctx->cipher->ctx_size ==
sizeof(EVP_AES_GCM_CTX) + EVP_AES_GCM_CTX_PADDING);

switch(ctx->cipher->nid) {
// only execute assert checking with aes-gcm
case NID_aes_128_gcm:
case NID_aes_192_gcm:
case NID_aes_256_gcm:
assert(ctx->cipher->ctx_size ==
sizeof(EVP_AES_GCM_CTX) + EVP_AES_GCM_CTX_PADDING);
break;
// not execute assert checking with xaes-256-gcm
default:
break;
}

char *ptr = ctx->cipher_data;
#if defined(OPENSSL_32_BIT)
assert((uintptr_t)ptr % 4 == 0);
Expand Down Expand Up @@ -1745,3 +1756,153 @@ int EVP_has_aes_hardware(void) {
}

OPENSSL_MSVC_PRAGMA(warning(pop))

/* ---------------------------- XAES-256-GCM ----------------------------
Specification: https://github.com/C2SP/C2SP/blob/main/XAES-256-GCM.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're allowing the nonce size to vary, we need to reference the description that allows a nonce smaller than 24 bytes. I see you have it further below on l. 1849, but it would be good to add that reference here too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add it. It is a simple extension in the case without key commitment. We use the first 12 bytes of nonce for key derivation, and the bytes at [b-12: b] for IV in encryption, where 20 <= b <= 24 is the size of the input nonce.

-----------------------------------------------------------------------*/
#define XAES_256_GCM_CTX_OFFSET (sizeof(EVP_AES_GCM_CTX) + EVP_AES_GCM_CTX_PADDING)
#define XAES_256_GCM_KEY_LENGTH (AES_BLOCK_SIZE * 2)
#define XAES_256_GCM_KEY_COMMIT_SIZE (AES_BLOCK_SIZE * 2)
#define XAES_256_GCM_CMAC_INPUT_SIZE (AES_BLOCK_SIZE * 2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this would be

Suggested change
#define XAES_256_GCM_CMAC_INPUT_SIZE (AES_BLOCK_SIZE * 2)
#define XAES_256_GCM_CMAC_INPUT_SIZE (AES_BLOCK_SIZE)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This const is no longer used as I removed the code using CMAC available in AWS-LC. I'll remove it.

#define XAES_256_GCM_MAX_NONCE_SIZE (AES_GCM_NONCE_LENGTH * 2)
#define XAES_256_GCM_MIN_NONCE_SIZE (20)

typedef struct {
AES_KEY xaes_key;
uint8_t k1[AES_BLOCK_SIZE];
} XAES_256_GCM_CTX;

/*
The following function performs the step #2 of CMAC specified in:
https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38b.pdf#page=14
Only K1 is needed as the CMAC input is always a complete block.
Also note that K1 only depends on the input main key.
Pseudocode:
If MSB(L) = 0: K1 = L << 1;
Else: K1 = (L << 1) ^ (0x00, ..., 0x00, 0x87)
*/
#define BINARY_FIELD_MUL_X_128(out, in) \
do { \
unsigned i; \
/* Shift |in| to left, including carry. */ \
for (i = 0; i < 15; i++) { \
out[i] = (in[i] << 1) | (in[i+1] >> 7); \
} \
/* If MSB set fixup with R. */ \
const uint8_t carry = in[0] >> 7; \
out[i] = (in[i] << 1) ^ ((0 - carry) & 0x87); \
} while(0);

static int xaes_256_gcm_CMAC_derive_key(XAES_256_GCM_CTX *xaes_ctx,
const uint8_t* nonce, uint8_t *derived_key) {
uint8_t M1[AES_BLOCK_SIZE] = {0};
uint8_t M2[AES_BLOCK_SIZE] = {0};

M1[1] = 0x01;
M1[2] = 0x58;
OPENSSL_memcpy(M1 + 4, nonce, 12);
OPENSSL_memcpy(M2, M1, AES_BLOCK_SIZE);

M2[1] = 0x02;

for (size_t i = 0; i < AES_BLOCK_SIZE; i++) {
M1[i] ^= xaes_ctx->k1[i];
M2[i] ^= xaes_ctx->k1[i];
}

AES_encrypt(M1, derived_key, &xaes_ctx->xaes_key);
AES_encrypt(M2, derived_key + AES_BLOCK_SIZE, &xaes_ctx->xaes_key);

return 1;
}

static XAES_256_GCM_CTX *xaes_256_gcm_from_cipher_ctx(EVP_CIPHER_CTX *ctx) {
// XAES_256_GCM_CTX data is put at the rear of ctx->cipher_data
return (XAES_256_GCM_CTX *)((uint8_t*)ctx->cipher_data + XAES_256_GCM_CTX_OFFSET);
}

static int xaes_256_gcm_set_gcm_key(EVP_CIPHER_CTX *ctx, const uint8_t *nonce, int enc) {

EVP_AES_GCM_CTX *gctx = aes_gcm_from_cipher_ctx(ctx);

// Nonce size: 20 bytes <= |N| <= 24 bytes
if(gctx->ivlen < XAES_256_GCM_MIN_NONCE_SIZE ||
gctx->ivlen > XAES_256_GCM_MAX_NONCE_SIZE) {
OPENSSL_PUT_ERROR(CIPHER, CIPHER_R_INVALID_NONCE_SIZE);
return 0;
}

XAES_256_GCM_CTX *xaes_ctx = xaes_256_gcm_from_cipher_ctx(ctx);

uint8_t derived_key[XAES_256_GCM_KEY_LENGTH];

xaes_256_gcm_CMAC_derive_key(xaes_ctx, nonce, derived_key);

int ivlen = gctx->ivlen;

// AES-GCM uses a different size nonce than XAES-GCM,
// so to be able to call aes_gcm_init_key with ctx we temporarily
// set the nonce (iv) length to AES_GCM_NONCE_LENGTH.
gctx->ivlen = AES_GCM_NONCE_LENGTH;

// For nonce size < 24 bytes
// Reference: https://eprint.iacr.org/2025/758.pdf#page=24
aes_gcm_init_key(ctx, derived_key, nonce + ivlen - AES_GCM_NONCE_LENGTH, enc);

// Re-assign the original nonce size of XAES-256-GCM (20 <= |N| <= 24)
gctx->ivlen = ivlen;

return 1;
}

static int xaes_256_gcm_ctx_init(XAES_256_GCM_CTX *xaes_ctx, const uint8_t *key) {
static const uint8_t kZeroIn[AES_BLOCK_SIZE] = {0};
uint8_t L[AES_BLOCK_SIZE];
AES_set_encrypt_key(key, XAES_256_GCM_KEY_LENGTH << 3, &xaes_ctx->xaes_key);
AES_encrypt(kZeroIn, L, &xaes_ctx->xaes_key);
BINARY_FIELD_MUL_X_128(xaes_ctx->k1, L);
return 1;
}

// ------------------------------------------------------------------------------
// --------------- EVP_CIPHER XAES-256-GCM Without Key Commitment ---------------
// ------------------------------------------------------------------------------
static int xaes_256_gcm_init(EVP_CIPHER_CTX *ctx, const uint8_t *key,
const uint8_t *iv, int enc) {
// Key length: 32 bytes
if (ctx->key_len != XAES_256_GCM_KEY_LENGTH) {
OPENSSL_PUT_ERROR(CIPHER, CIPHER_R_BAD_KEY_LENGTH);
return 0;
}

XAES_256_GCM_CTX *xaes_ctx = xaes_256_gcm_from_cipher_ctx(ctx);

// When main key is provided, initialize the context and derive a subkey
if(key != NULL) {
xaes_256_gcm_ctx_init(xaes_ctx, key);
}

// If iv is provided, even if main key is not, derive a subkey
if(iv != NULL) {
return xaes_256_gcm_set_gcm_key(ctx, iv, enc);
}

return 1;
}

DEFINE_METHOD_FUNCTION(EVP_CIPHER, EVP_xaes_256_gcm) {
OPENSSL_memset(out, 0, sizeof(EVP_CIPHER));
out->nid = NID_xaes_256_gcm;
out->block_size = AES_BLOCK_SIZE;
out->key_len = XAES_256_GCM_KEY_LENGTH;
out->iv_len = XAES_256_GCM_MAX_NONCE_SIZE;
out->ctx_size = sizeof(EVP_AES_GCM_CTX) + EVP_AES_GCM_CTX_PADDING
+ sizeof(XAES_256_GCM_CTX);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this correct? why are we not storing everything in XAES_256_GCM_CTX?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is just style. Like I explained, I can declare a field of type EVP_AES_GCM_CTX inside XAES_256_GCM_CTX. Then, like in previous review, you require another function similar to aes_gcm_from_cipher_ctx to specify the start position of EVP_AES_GCM_CTX. Then I also have to see why a test case failed? Is that too cumbersome?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I understand it is that it differs from the AEAD implementation in #2652 in that, here, in the EVP API, we want to be able to do multiple cipher calls (complete with a tag, not streaming) using the same derived key but new (second half?) IV. In the AEAD implementation, every "seal" or "open" operation had a new IV that derived a new key so the gcm context was temporary. So here, the cipher call is pointing to aes_gcm_cipher and uses a gcm context.
I think it would be clearer if that context were part of the xaes context as dkostic suggests.
On another note, can the interface take a half IV for GCM and not re-derive the key? Or I'm wrong in assuming that that use-case is implemented?

Copy link
Author

@ttungle96 ttungle96 Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nebeid I have included EVP_AES_GCM_CTX inside XAES_256_GCM_CTX in the newest update. Please review again based on that. This will make the code longer since in EVP_AEAD implementation, XAES_256_GCM_CTX does not need EVP_AES_GCM_CTX inside. I will have to define another struct for AEAD case, then another aead_xaes_256_gcm_ctx_init(), which is totally the same as xaes_256_gcm_ctx_init(), but different by only one argument. Similarly for aead_xaes_256_gcm_CMAC_derive_key(), totally the same as xaes_256_gcm_CMAC_derive_key().

For your second question, currently it has not implemented yet since people have not agreed on that design. Furthermore, I tried to keep this PR as simple as possible for review. If that case is required, I'll fix and create another PR.

out->flags = EVP_CIPH_GCM_MODE | EVP_CIPH_CUSTOM_IV | EVP_CIPH_CUSTOM_COPY |
EVP_CIPH_FLAG_CUSTOM_CIPHER | EVP_CIPH_ALWAYS_CALL_INIT |
EVP_CIPH_CTRL_INIT | EVP_CIPH_FLAG_AEAD_CIPHER;
out->init = xaes_256_gcm_init;
out->cipher = aes_gcm_cipher;
out->cleanup = aes_gcm_cleanup;
out->ctrl = aes_gcm_ctrl;
}
Loading
Loading