Skip to content
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

Add NewGCMTLS13 with TLS 1.3 additional data size verification #61

Merged
merged 2 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading