Skip to content

Commit

Permalink
XAES-256-GCM: preliminary draft
Browse files Browse the repository at this point in the history
  • Loading branch information
FiloSottile committed Jul 8, 2023
1 parent 4082f74 commit a3332f8
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 0 deletions.
45 changes: 45 additions & 0 deletions XAES-256-GCM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
This document specifies the XAES-256-GCM authenticated encryption with additional data algorithm, based on the composition of a standard NIST SP 800-108r1 KDF and the standard NIST AES-256-GCM AEAD.

The XAES-256-GCM inputs are a 256-bit key, a 192-bit nonce, a plaintext of up to approximately 64GiB, and additional data of up to 2 EiB.

Unlike AES-256-GCM, the XAES-256-GCM nonce can be randomly generated for a virtually unlimited number of messages (2⁸⁰ messages with collision risk 2⁻³²). Only a 256-bit key version is specified, which provides a comfortable multi-user security margin. Like AES-256-GCM, XAES-256-GCM is not nonce misuse-resistant, nor is it key-committing. Compared to AES-256-GCM, XAES-256-GCM requires three extra invocations of the AES-256 function, but has otherwise the same performance profile.

## Overview

XAES-256-GCM derives a subkey for use with AES-256-GCM from the input key and half the input nonce using a NIST SP 800-108r1 KDF.

The counter-based KDF (NIST SP 800-108r1, Section 4.1) is instantiated with CMAC-AES256 (NIST SP 800-38B) and the input key as *Kin*, the ASCII letter `X` (0x58) as *Label*, and the first 96 bits of the input nonce as *Context* (as recommended by NIST SP 800-38B, Section 4, point 4), to produce a 256-bit derived key.

Note that with a counter (*i*) size of 16 bits and omitting the optional *L* field, the AES-CMAC input totals 128 bits, which fits into a single block, mitigating the key control security issue described in NIST SP 800-38B, Section 6.7. It would in theory be possible to shrink *i* to 8 bits to fit a 8 bits *L*, but some implementations unfortunately fix the *L* size to 32 bits.

The derived key and the last 96 bits of the input nonce are used to encrypt the message with AES-256-GCM.

## Detailed key derivation algorithm

Inputs:

* 256-bit key *K*
* 192-bit nonce *N*

Outputs:

* 256-bit key *Kₓ*
* 96-bit nonce *Nₓ*

Algorithm:

1. *L* = AES-256ₖ(0¹²⁸)
2. If MSB₁(*L*) = 0, then *K1* = *L* << 1;
Else *K1* = (*L* << 1) ⊕ 0¹²⁰10000111
3. *M1* = 0x00 || 0x01 || `X` || 0x00 || *N*[:12]
4. *M2* = 0x00 || 0x02 || `X` || 0x00 || *N*[:12]
5. *Kₓ* = AES-256ₖ(*M1**K1*) || AES-256ₖ(*M2**K1*)
6. *Nₓ* = *N*[12:]

Steps 1 and 2 reproduce the CMAC subkey generation specified in NIST SP 800-38B, Section 6.1. Note that only *K1* is needed.

Steps 2 and 3 compose the PRF inputs for *i* = 1, 2 according to NIST SP 800-108r1, Section 4.1.

Step 5 applies CMAC twice to the two single-block messages to derive the KDF output.

*Kₓ* and *Nₓ* are then used as the AES-256-GCM key and nonce, respectively.
21 changes: 21 additions & 0 deletions XAES-256-GCM/openssl.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
int derive_key(unsigned char out[32], const unsigned char key[32], const unsigned char nonce[24]) {
EVP_KDF *kdf = EVP_KDF_fetch(NULL, "KBKDF", NULL);
EVP_KDF_CTX *kctx = EVP_KDF_CTX_new(kdf);
EVP_KDF_free(kdf);

OSSL_PARAM params[9], *p = params;
*p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_CIPHER, "AES256", 0);
*p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_MAC, "CMAC", 0);
*p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_MODE, "COUNTER", 0);
*p++ = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_KBKDF_USE_L, 0);
*p++ = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_KBKDF_R, 16);
*p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, key, sizeof(key));
*p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, "X", strlen("X"));
*p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_INFO, nonce[:12], 12);
*p = OSSL_PARAM_construct_end();

int res = EVP_KDF_derive(kctx, out, sizeof(out), params);

EVP_KDF_CTX_free(kctx);
return res;
}
84 changes: 84 additions & 0 deletions XAES-256-GCM/reference.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package xaes256gcm

import (
"crypto/aes"
"crypto/cipher"
"crypto/subtle"
"errors"
)

const (
KeySize = 32
NonceSize = 24
Overhead = 16
)

type xaes256gcm struct {
c cipher.Block
k1 [aes.BlockSize]byte
}

func New(key []byte) (cipher.AEAD, error) {
if len(key) != KeySize {
return nil, errors.New("xaes256gcm: bad key length")
}

x := new(xaes256gcm)

x.c, _ = aes.NewCipher(key)
x.c.Encrypt(x.k1[:], x.k1[:])

var msb byte
for i := len(x.k1) - 1; i >= 0; i-- {
msb = x.k1[i] >> 7
x.k1[i] = x.k1[i]<<1 | msb
}
x.k1[len(x.k1)-1] ^= msb * 0b10000111

return x, nil
}

func (*xaes256gcm) NonceSize() int {
return NonceSize
}

func (*xaes256gcm) Overhead() int {
return Overhead
}

func (x *xaes256gcm) deriveKey(nonce []byte) []byte {
k := make([]byte, 0, 2*aes.BlockSize)
k = append(k, 0, 1, 'X', 0)
k = append(k, nonce...)
k = append(k, 0, 2, 'X', 0)
k = append(k, nonce...)
subtle.XORBytes(k[:aes.BlockSize], k[:aes.BlockSize], x.k1[:])
subtle.XORBytes(k[aes.BlockSize:], k[aes.BlockSize:], x.k1[:])
x.c.Encrypt(k[:aes.BlockSize], k[:aes.BlockSize])
x.c.Encrypt(k[aes.BlockSize:], k[aes.BlockSize:])
return k
}

func (x *xaes256gcm) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
if len(nonce) != NonceSize {
panic("xaes256gcm: bad nonce length passed to Seal")
}

k, n := x.deriveKey(nonce[:12]), nonce[12:]
c, _ := aes.NewCipher(k)
a, _ := cipher.NewGCM(c)
return a.Seal(dst, n, plaintext, additionalData)
}

var errOpen = errors.New("xaes256gcm: message authentication failed")

func (x *xaes256gcm) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
if len(nonce) != NonceSize {
panic("xaes256gcm: bad nonce length passed to Open")
}

k, n := x.deriveKey(nonce[:12]), nonce[12:]
c, _ := aes.NewCipher(k)
a, _ := cipher.NewGCM(c)
return a.Open(dst, n, ciphertext, additionalData)
}

0 comments on commit a3332f8

Please sign in to comment.