Skip to content

Commit

Permalink
Merge pull request #61 from microsoft/dev/dagood/gcmtls13
Browse files Browse the repository at this point in the history
Add NewGCMTLS13 with TLS 1.3 additional data size verification
  • Loading branch information
dagood authored Jan 11, 2024
2 parents f18e1f2 + cf2e349 commit c559cb8
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 55 deletions.
78 changes: 68 additions & 10 deletions openssl/aes.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,16 +263,38 @@ func (c *aesCTR) finalize() {
C.go_openssl_EVP_CIPHER_CTX_free(c.ctx)
}

type cipherGCMTLS uint8

const (
cipherGCMTLSNone cipherGCMTLS = iota
cipherGCMTLS12
cipherGCMTLS13
)

type aesGCM struct {
ctx C.GO_EVP_CIPHER_CTX_PTR
tls bool
ctx C.GO_EVP_CIPHER_CTX_PTR
tls cipherGCMTLS
// minNextNonce is the minimum value that the next nonce can be, enforced by
// all TLS modes.
minNextNonce uint64
// mask is the nonce mask used in TLS 1.3 mode.
mask uint64
// maskInitialized is true if mask has been initialized. This happens during
// the first Seal. The initialized mask may be 0. Used by TLS 1.3 mode.
maskInitialized bool
}

const (
gcmTagSize = 16
gcmStandardNonceSize = 12
gcmTlsAddSize = 13
// TLS 1.2 additional data is constructed as:
//
// additional_data = seq_num(8) + TLSCompressed.type(1) + TLSCompressed.version(2) + TLSCompressed.length(2);
gcmTls12AddSize = 13
// TLS 1.3 additional data is constructed as:
//
// additional_data = TLSCiphertext.opaque_type(1) || TLSCiphertext.legacy_record_version(2) || TLSCiphertext.length(2)
gcmTls13AddSize = 5
gcmTlsFixedNonceSize = 4
)

Expand All @@ -297,7 +319,7 @@ func (c *aesCipher) NewGCM(nonceSize, tagSize int) (cipher.AEAD, error) {
if tagSize != gcmTagSize {
return cipher.NewGCMWithTagSize(&noGCM{c}, tagSize)
}
return c.newGCM(false)
return c.newGCM(cipherGCMTLSNone)
}

// NewGCMTLS returns a GCM cipher specific to TLS
Expand All @@ -307,10 +329,20 @@ func NewGCMTLS(c cipher.Block) (cipher.AEAD, error) {
}

func (c *aesCipher) NewGCMTLS() (cipher.AEAD, error) {
return c.newGCM(true)
return c.newGCM(cipherGCMTLS12)
}

// NewGCMTLS13 returns a GCM cipher specific to TLS 1.3 and should not be used
// for non-TLS purposes.
func NewGCMTLS13(c cipher.Block) (cipher.AEAD, error) {
return c.(*aesCipher).NewGCMTLS13()
}

func (c *aesCipher) NewGCMTLS13() (cipher.AEAD, error) {
return c.newGCM(cipherGCMTLS13)
}

func (c *aesCipher) newGCM(tls bool) (cipher.AEAD, error) {
func (c *aesCipher) newGCM(tls cipherGCMTLS) (cipher.AEAD, error) {
var cipher C.GO_EVP_CIPHER_PTR
switch len(c.key) * 8 {
case 128:
Expand Down Expand Up @@ -362,15 +394,41 @@ func (g *aesGCM) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
if len(dst)+len(plaintext)+gcmTagSize < len(dst) {
panic("cipher: message too large for buffer")
}
if g.tls {
if len(additionalData) != gcmTlsAddSize {
panic("cipher: incorrect additional data length given to GCM TLS")
if g.tls != cipherGCMTLSNone {
if g.tls == cipherGCMTLS12 && len(additionalData) != gcmTls12AddSize {
panic("cipher: incorrect additional data length given to GCM TLS 1.2")
} else if g.tls == cipherGCMTLS13 && len(additionalData) != gcmTls13AddSize {
panic("cipher: incorrect additional data length given to GCM TLS 1.3")
}
counter := bigUint64(nonce[gcmTlsFixedNonceSize:])
if g.tls == cipherGCMTLS13 {
// In TLS 1.3, the counter in the nonce has a mask and requires
// further decoding.
if !g.maskInitialized {
// According to TLS 1.3 nonce construction details at
// https://tools.ietf.org/html/rfc8446#section-5.3:
//
// the first record transmitted under a particular traffic
// key MUST use sequence number 0.
//
// The padded sequence number is XORed with [a mask].
//
// The resulting quantity (of length iv_length) is used as
// the per-record nonce.
//
// We need to convert from the given nonce to sequence numbers
// to keep track of minNextNonce and enforce the counter
// maximum. On the first call, we know counter^mask is 0^mask,
// so we can simply store it as the mask.
g.mask = counter
g.maskInitialized = true
}
counter ^= g.mask
}
// BoringCrypto enforces strictly monotonically increasing explicit nonces
// and to fail after 2^64 - 1 keys as per FIPS 140-2 IG A.5,
// but OpenSSL does not perform this check, so it is implemented here.
const maxUint64 = 1<<64 - 1
counter := bigUint64(nonce[gcmTlsFixedNonceSize:])
if counter == maxUint64 {
panic("cipher: nonce counter must be less than 2^64 - 1")
}
Expand Down
127 changes: 82 additions & 45 deletions openssl/aes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,51 +85,88 @@ func TestSealAndOpen_Empty(t *testing.T) {

func TestSealAndOpenTLS(t *testing.T) {
key := []byte("D249BF6DEC97B1EBD69BC4D6B3A3C49D")
ci, err := NewAESCipher(key)
if err != nil {
t.Fatal(err)
}
gcm, err := NewGCMTLS(ci)
if err != nil {
t.Fatal(err)
}
nonce := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
nonce1 := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
nonce9 := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9}
nonce10 := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10}
nonceMax := [12]byte{0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255}
plainText := []byte{0x01, 0x02, 0x03}
additionalData := make([]byte, 13)
additionalData[11] = byte(len(plainText) >> 8)
additionalData[12] = byte(len(plainText))
sealed := gcm.Seal(nil, nonce[:], plainText, additionalData)
assertPanic(t, func() {
gcm.Seal(nil, nonce[:], plainText, additionalData)
})
sealed1 := gcm.Seal(nil, nonce1[:], plainText, additionalData)
gcm.Seal(nil, nonce10[:], plainText, additionalData)
assertPanic(t, func() {
gcm.Seal(nil, nonce9[:], plainText, additionalData)
})
assertPanic(t, func() {
gcm.Seal(nil, nonceMax[:], plainText, additionalData)
})
if bytes.Equal(sealed, sealed1) {
t.Errorf("different nonces should produce different outputs\ngot: %#v\nexp: %#v", sealed, sealed1)
}
decrypted, err := gcm.Open(nil, nonce[:], sealed, additionalData)
if err != nil {
t.Error(err)
}
decrypted1, err := gcm.Open(nil, nonce1[:], sealed1, additionalData)
if err != nil {
t.Error(err)
}
if !bytes.Equal(decrypted, plainText) {
t.Errorf("unexpected decrypted result\ngot: %#v\nexp: %#v", decrypted, plainText)
}
if !bytes.Equal(decrypted, decrypted1) {
t.Errorf("unexpected decrypted result\ngot: %#v\nexp: %#v", decrypted, decrypted1)
tests := []struct {
name string
tls string
mask func(n *[12]byte)
}{
{"1.2", "1.2", nil},
{"1.3", "1.3", nil},
{"1.3_masked", "1.3", func(n *[12]byte) {
// Arbitrary mask in the high bits.
n[9] ^= 0x42
// Mask the very first bit. This makes sure that if Seal doesn't
// handle the mask, the counter appears to go backwards and panics
// when it shouldn't.
n[11] ^= 0x1
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ci, err := NewAESCipher(key)
if err != nil {
t.Fatal(err)
}
var gcm cipher.AEAD
switch tt.tls {
case "1.2":
gcm, err = NewGCMTLS(ci)
case "1.3":
gcm, err = NewGCMTLS13(ci)
}
if err != nil {
t.Fatal(err)
}
nonce := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
nonce1 := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
nonce9 := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9}
nonce10 := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10}
nonceMax := [12]byte{0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255}
if tt.mask != nil {
for _, m := range []*[12]byte{&nonce, &nonce1, &nonce9, &nonce10, &nonceMax} {
tt.mask(m)
}
}
plainText := []byte{0x01, 0x02, 0x03}
var additionalData []byte
switch tt.tls {
case "1.2":
additionalData = make([]byte, 13)
case "1.3":
additionalData = []byte{23, 3, 3, 0, 0}
}
additionalData[len(additionalData)-2] = byte(len(plainText) >> 8)
additionalData[len(additionalData)-1] = byte(len(plainText))
sealed := gcm.Seal(nil, nonce[:], plainText, additionalData)
assertPanic(t, func() {
gcm.Seal(nil, nonce[:], plainText, additionalData)
})
sealed1 := gcm.Seal(nil, nonce1[:], plainText, additionalData)
gcm.Seal(nil, nonce10[:], plainText, additionalData)
assertPanic(t, func() {
gcm.Seal(nil, nonce9[:], plainText, additionalData)
})
assertPanic(t, func() {
gcm.Seal(nil, nonceMax[:], plainText, additionalData)
})
if bytes.Equal(sealed, sealed1) {
t.Errorf("different nonces should produce different outputs\ngot: %#v\nexp: %#v", sealed, sealed1)
}
decrypted, err := gcm.Open(nil, nonce[:], sealed, additionalData)
if err != nil {
t.Error(err)
}
decrypted1, err := gcm.Open(nil, nonce1[:], sealed1, additionalData)
if err != nil {
t.Error(err)
}
if !bytes.Equal(decrypted, plainText) {
t.Errorf("unexpected decrypted result\ngot: %#v\nexp: %#v", decrypted, plainText)
}
if !bytes.Equal(decrypted, decrypted1) {
t.Errorf("unexpected decrypted result\ngot: %#v\nexp: %#v", decrypted, decrypted1)
}
})
}
}

Expand Down

0 comments on commit c559cb8

Please sign in to comment.