Source file src/crypto/internal/cryptotest/aead.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cryptotest
     6  
     7  import (
     8  	"bytes"
     9  	"crypto/cipher"
    10  	"fmt"
    11  	"testing"
    12  )
    13  
    14  var lengths = []int{0, 156, 8192, 8193, 8208}
    15  
    16  // MakeAEAD returns a cipher.AEAD instance.
    17  //
    18  // Multiple calls to MakeAEAD must return equivalent instances, so for example
    19  // the key must be fixed.
    20  type MakeAEAD func() (cipher.AEAD, error)
    21  
    22  // TestAEAD performs a set of tests on cipher.AEAD implementations, checking
    23  // the documented requirements of NonceSize, Overhead, Seal and Open.
    24  func TestAEAD(t *testing.T, mAEAD MakeAEAD) {
    25  	aead, err := mAEAD()
    26  	if err != nil {
    27  		t.Fatal(err)
    28  	}
    29  
    30  	t.Run("Roundtrip", func(t *testing.T) {
    31  
    32  		// Test all combinations of plaintext and additional data lengths.
    33  		for _, ptLen := range lengths {
    34  			for _, adLen := range lengths {
    35  				t.Run(fmt.Sprintf("Plaintext-Length=%d,AddData-Length=%d", ptLen, adLen), func(t *testing.T) {
    36  					rng := newRandReader(t)
    37  
    38  					nonce := make([]byte, aead.NonceSize())
    39  					rng.Read(nonce)
    40  
    41  					before, addData := make([]byte, adLen), make([]byte, ptLen)
    42  					rng.Read(before)
    43  					rng.Read(addData)
    44  
    45  					ciphertext := sealMsg(t, aead, nil, nonce, before, addData)
    46  					after := openWithoutError(t, aead, nil, nonce, ciphertext, addData)
    47  
    48  					if !bytes.Equal(after, before) {
    49  						t.Errorf("plaintext is different after a seal/open cycle; got %s, want %s", truncateHex(after), truncateHex(before))
    50  					}
    51  				})
    52  			}
    53  		}
    54  	})
    55  
    56  	t.Run("OutOfBounds", func(t *testing.T) {
    57  		// v1.26.0 and earlier have a harmless ciphertext overread/write in
    58  		// short-tag (not FIPS 140 Approved) AES-GCM.
    59  		MustMinimumFIPS140ModuleVersion(t, "v1.28.0")
    60  		// boundaryLengths covers every offset within an AES block, as well as the tail
    61  		// of a multi-block message, since that's where implementations handle partial
    62  		// blocks, and are most likely to access memory past the end of a buffer.
    63  		var boundaryLengths = []int{
    64  			0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
    65  			16, 17, 18, 127, 128, 129, 130, 131, 156, 8193,
    66  		}
    67  		for _, ptLen := range boundaryLengths {
    68  			t.Run(fmt.Sprintf("Plaintext-Length=%d", ptLen), func(t *testing.T) {
    69  				rng := newRandReader(t)
    70  
    71  				nonce := make([]byte, aead.NonceSize())
    72  				rng.Read(nonce)
    73  
    74  				plaintext, addData := make([]byte, ptLen), make([]byte, 16)
    75  				rng.Read(plaintext)
    76  				rng.Read(addData)
    77  
    78  				// Run a seal/open cycle with every buffer placed against an
    79  				// inaccessible page, so that any access past the end of a
    80  				// buffer (Boundary=End) or before its start (Boundary=Start)
    81  				// faults, rather than silently landing in adjacent memory.
    82  				for _, boundary := range []string{"Start", "End"} {
    83  					t.Run("Boundary="+boundary, func(t *testing.T) {
    84  						guarded := func(b []byte) []byte {
    85  							start, end := BoundarySlices(t, len(b))
    86  							if boundary == "Start" {
    87  								copy(start, b)
    88  								return start
    89  							}
    90  							copy(end, b)
    91  							return end
    92  						}
    93  
    94  						src, ad := guarded(plaintext), guarded(addData)
    95  						dst := guarded(make([]byte, ptLen+aead.Overhead()))
    96  
    97  						ciphertext := sealMsg(t, aead, dst[:0], nonce, src, ad)
    98  
    99  						after := openWithoutError(t, aead, guarded(plaintext)[:0], nonce, ciphertext, ad)
   100  						if !bytes.Equal(after, plaintext) {
   101  							t.Errorf("plaintext is different after a seal/open cycle; got %s, want %s", truncateHex(after), truncateHex(plaintext))
   102  						}
   103  					})
   104  				}
   105  			})
   106  		}
   107  	})
   108  
   109  	t.Run("InputNotModified", func(t *testing.T) {
   110  
   111  		// Test all combinations of plaintext and additional data lengths.
   112  		for _, ptLen := range lengths {
   113  			for _, adLen := range lengths {
   114  				t.Run(fmt.Sprintf("Plaintext-Length=%d,AddData-Length=%d", ptLen, adLen), func(t *testing.T) {
   115  					t.Run("Seal", func(t *testing.T) {
   116  						rng := newRandReader(t)
   117  
   118  						nonce := make([]byte, aead.NonceSize())
   119  						rng.Read(nonce)
   120  
   121  						src, before := make([]byte, ptLen), make([]byte, ptLen)
   122  						rng.Read(src)
   123  						copy(before, src)
   124  
   125  						addData := make([]byte, adLen)
   126  						rng.Read(addData)
   127  
   128  						sealMsg(t, aead, nil, nonce, src, addData)
   129  						if !bytes.Equal(src, before) {
   130  							t.Errorf("Seal modified src; got %s, want %s", truncateHex(src), truncateHex(before))
   131  						}
   132  					})
   133  
   134  					t.Run("Open", func(t *testing.T) {
   135  						rng := newRandReader(t)
   136  
   137  						nonce := make([]byte, aead.NonceSize())
   138  						rng.Read(nonce)
   139  
   140  						plaintext, addData := make([]byte, ptLen), make([]byte, adLen)
   141  						rng.Read(plaintext)
   142  						rng.Read(addData)
   143  
   144  						// Record the ciphertext that shouldn't be modified as the input of
   145  						// Open.
   146  						ciphertext := sealMsg(t, aead, nil, nonce, plaintext, addData)
   147  						before := make([]byte, len(ciphertext))
   148  						copy(before, ciphertext)
   149  
   150  						openWithoutError(t, aead, nil, nonce, ciphertext, addData)
   151  						if !bytes.Equal(ciphertext, before) {
   152  							t.Errorf("Open modified src; got %s, want %s", truncateHex(ciphertext), truncateHex(before))
   153  						}
   154  					})
   155  				})
   156  			}
   157  		}
   158  	})
   159  
   160  	t.Run("BufferOverlap", func(t *testing.T) {
   161  
   162  		// Test all combinations of plaintext and additional data lengths.
   163  		for _, ptLen := range lengths {
   164  			if ptLen <= 1 { // We need enough room for an inexact overlap to occur.
   165  				continue
   166  			}
   167  			for _, adLen := range lengths {
   168  				t.Run(fmt.Sprintf("Plaintext-Length=%d,AddData-Length=%d", ptLen, adLen), func(t *testing.T) {
   169  					t.Run("Seal", func(t *testing.T) {
   170  						rng := newRandReader(t)
   171  
   172  						nonce := make([]byte, aead.NonceSize())
   173  						rng.Read(nonce)
   174  
   175  						// Make a buffer that can hold a plaintext and ciphertext as we
   176  						// overlap their slices to check for panic on inexact overlaps.
   177  						ctLen := ptLen + aead.Overhead()
   178  						buff := make([]byte, ptLen+ctLen)
   179  						rng.Read(buff)
   180  
   181  						addData := make([]byte, adLen)
   182  						rng.Read(addData)
   183  
   184  						// Make plaintext and dst slices point to same array with inexact overlap.
   185  						plaintext := buff[:ptLen]
   186  						dst := buff[1:1] // Shift dst to not start at start of plaintext.
   187  						mustPanic(t, "invalid buffer overlap", func() { sealMsg(t, aead, dst, nonce, plaintext, addData) })
   188  
   189  						// Only overlap on one byte
   190  						plaintext = buff[:ptLen]
   191  						dst = buff[ptLen-1 : ptLen-1]
   192  						mustPanic(t, "invalid buffer overlap", func() { sealMsg(t, aead, dst, nonce, plaintext, addData) })
   193  					})
   194  
   195  					t.Run("Open", func(t *testing.T) {
   196  						rng := newRandReader(t)
   197  
   198  						nonce := make([]byte, aead.NonceSize())
   199  						rng.Read(nonce)
   200  
   201  						// Create a valid ciphertext to test Open with.
   202  						plaintext := make([]byte, ptLen)
   203  						rng.Read(plaintext)
   204  						addData := make([]byte, adLen)
   205  						rng.Read(addData)
   206  						validCT := sealMsg(t, aead, nil, nonce, plaintext, addData)
   207  
   208  						// Make a buffer that can hold a plaintext and ciphertext as we
   209  						// overlap their slices to check for panic on inexact overlaps.
   210  						buff := make([]byte, ptLen+len(validCT))
   211  
   212  						// Make ciphertext and dst slices point to same array with inexact overlap.
   213  						ciphertext := buff[:len(validCT)]
   214  						copy(ciphertext, validCT)
   215  						dst := buff[1:1] // Shift dst to not start at start of ciphertext.
   216  						mustPanic(t, "invalid buffer overlap", func() { aead.Open(dst, nonce, ciphertext, addData) })
   217  
   218  						// Only overlap on one byte.
   219  						ciphertext = buff[:len(validCT)]
   220  						copy(ciphertext, validCT)
   221  						// Make sure it is the actual ciphertext being overlapped and not
   222  						// the hash digest which might be extracted/truncated in some
   223  						// implementations: Go one byte past the hash digest/tag and into
   224  						// the ciphertext.
   225  						beforeTag := len(validCT) - aead.Overhead()
   226  						dst = buff[beforeTag-1 : beforeTag-1]
   227  						mustPanic(t, "invalid buffer overlap", func() { aead.Open(dst, nonce, ciphertext, addData) })
   228  					})
   229  				})
   230  			}
   231  		}
   232  	})
   233  
   234  	t.Run("AppendDst", func(t *testing.T) {
   235  
   236  		// Test all combinations of plaintext and additional data lengths.
   237  		for _, ptLen := range lengths {
   238  			for _, adLen := range lengths {
   239  				t.Run(fmt.Sprintf("Plaintext-Length=%d,AddData-Length=%d", ptLen, adLen), func(t *testing.T) {
   240  
   241  					t.Run("Seal", func(t *testing.T) {
   242  						rng := newRandReader(t)
   243  
   244  						nonce := make([]byte, aead.NonceSize())
   245  						rng.Read(nonce)
   246  
   247  						shortBuff := []byte("a")
   248  						longBuff := make([]byte, 512)
   249  						rng.Read(longBuff)
   250  						prefixes := [][]byte{shortBuff, longBuff}
   251  
   252  						// Check each prefix gets appended to by Seal without altering them.
   253  						for _, prefix := range prefixes {
   254  							plaintext, addData := make([]byte, ptLen), make([]byte, adLen)
   255  							rng.Read(plaintext)
   256  							rng.Read(addData)
   257  							out := sealMsg(t, aead, prefix, nonce, plaintext, addData)
   258  
   259  							// Check that Seal didn't alter the prefix
   260  							if !bytes.Equal(out[:len(prefix)], prefix) {
   261  								t.Errorf("Seal alters dst instead of appending; got %s, want %s", truncateHex(out[:len(prefix)]), truncateHex(prefix))
   262  							}
   263  
   264  							if isDeterministic(aead) {
   265  								ciphertext := out[len(prefix):]
   266  								// Check that the appended ciphertext wasn't affected by the prefix
   267  								if expectedCT := sealMsg(t, aead, nil, nonce, plaintext, addData); !bytes.Equal(ciphertext, expectedCT) {
   268  									t.Errorf("Seal behavior affected by pre-existing data in dst; got %s, want %s", truncateHex(ciphertext), truncateHex(expectedCT))
   269  								}
   270  							}
   271  						}
   272  					})
   273  
   274  					t.Run("Open", func(t *testing.T) {
   275  						rng := newRandReader(t)
   276  
   277  						nonce := make([]byte, aead.NonceSize())
   278  						rng.Read(nonce)
   279  
   280  						shortBuff := []byte("a")
   281  						longBuff := make([]byte, 512)
   282  						rng.Read(longBuff)
   283  						prefixes := [][]byte{shortBuff, longBuff}
   284  
   285  						// Check each prefix gets appended to by Open without altering them.
   286  						for _, prefix := range prefixes {
   287  							before, addData := make([]byte, adLen), make([]byte, ptLen)
   288  							rng.Read(before)
   289  							rng.Read(addData)
   290  							ciphertext := sealMsg(t, aead, nil, nonce, before, addData)
   291  
   292  							out := openWithoutError(t, aead, prefix, nonce, ciphertext, addData)
   293  
   294  							// Check that Open didn't alter the prefix
   295  							if !bytes.Equal(out[:len(prefix)], prefix) {
   296  								t.Errorf("Open alters dst instead of appending; got %s, want %s", truncateHex(out[:len(prefix)]), truncateHex(prefix))
   297  							}
   298  
   299  							after := out[len(prefix):]
   300  							// Check that the appended plaintext wasn't affected by the prefix
   301  							if !bytes.Equal(after, before) {
   302  								t.Errorf("Open behavior affected by pre-existing data in dst; got %s, want %s", truncateHex(after), truncateHex(before))
   303  							}
   304  						}
   305  					})
   306  				})
   307  			}
   308  		}
   309  	})
   310  
   311  	t.Run("WrongNonce", func(t *testing.T) {
   312  		if aead.NonceSize() == 0 {
   313  			t.Skip("AEAD does not use a nonce")
   314  		}
   315  		// Test all combinations of plaintext and additional data lengths.
   316  		for _, ptLen := range lengths {
   317  			for _, adLen := range lengths {
   318  				t.Run(fmt.Sprintf("Plaintext-Length=%d,AddData-Length=%d", ptLen, adLen), func(t *testing.T) {
   319  					rng := newRandReader(t)
   320  
   321  					nonce := make([]byte, aead.NonceSize())
   322  					rng.Read(nonce)
   323  
   324  					plaintext, addData := make([]byte, ptLen), make([]byte, adLen)
   325  					rng.Read(plaintext)
   326  					rng.Read(addData)
   327  
   328  					ciphertext := sealMsg(t, aead, nil, nonce, plaintext, addData)
   329  
   330  					// Perturb the nonce and check for an error when Opening
   331  					alterNonce := make([]byte, aead.NonceSize())
   332  					copy(alterNonce, nonce)
   333  					alterNonce[len(alterNonce)-1] += 1
   334  					_, err := aead.Open(nil, alterNonce, ciphertext, addData)
   335  
   336  					if err == nil {
   337  						t.Errorf("Open did not error when given different nonce than Sealed with")
   338  					}
   339  				})
   340  			}
   341  		}
   342  	})
   343  
   344  	t.Run("WrongAddData", func(t *testing.T) {
   345  
   346  		// Test all combinations of plaintext and additional data lengths.
   347  		for _, ptLen := range lengths {
   348  			for _, adLen := range lengths {
   349  				if adLen == 0 {
   350  					continue
   351  				}
   352  
   353  				t.Run(fmt.Sprintf("Plaintext-Length=%d,AddData-Length=%d", ptLen, adLen), func(t *testing.T) {
   354  					rng := newRandReader(t)
   355  
   356  					nonce := make([]byte, aead.NonceSize())
   357  					rng.Read(nonce)
   358  
   359  					plaintext, addData := make([]byte, ptLen), make([]byte, adLen)
   360  					rng.Read(plaintext)
   361  					rng.Read(addData)
   362  
   363  					ciphertext := sealMsg(t, aead, nil, nonce, plaintext, addData)
   364  
   365  					// Perturb the Additional Data and check for an error when Opening
   366  					alterAD := make([]byte, adLen)
   367  					copy(alterAD, addData)
   368  					alterAD[len(alterAD)-1] += 1
   369  					_, err := aead.Open(nil, nonce, ciphertext, alterAD)
   370  
   371  					if err == nil {
   372  						t.Errorf("Open did not error when given different Additional Data than Sealed with")
   373  					}
   374  				})
   375  			}
   376  		}
   377  	})
   378  
   379  	t.Run("WrongCiphertext", func(t *testing.T) {
   380  
   381  		// Test all combinations of plaintext and additional data lengths.
   382  		for _, ptLen := range lengths {
   383  			for _, adLen := range lengths {
   384  
   385  				t.Run(fmt.Sprintf("Plaintext-Length=%d,AddData-Length=%d", ptLen, adLen), func(t *testing.T) {
   386  					rng := newRandReader(t)
   387  
   388  					nonce := make([]byte, aead.NonceSize())
   389  					rng.Read(nonce)
   390  
   391  					plaintext, addData := make([]byte, ptLen), make([]byte, adLen)
   392  					rng.Read(plaintext)
   393  					rng.Read(addData)
   394  
   395  					ciphertext := sealMsg(t, aead, nil, nonce, plaintext, addData)
   396  
   397  					// Perturb the ciphertext and check for an error when Opening
   398  					alterCT := make([]byte, len(ciphertext))
   399  					copy(alterCT, ciphertext)
   400  					alterCT[len(alterCT)-1] += 1
   401  					_, err := aead.Open(nil, nonce, alterCT, addData)
   402  
   403  					if err == nil {
   404  						t.Errorf("Open did not error when given different ciphertext than was produced by Seal")
   405  					}
   406  				})
   407  			}
   408  		}
   409  	})
   410  }
   411  
   412  // Helper function to Seal a plaintext with additional data. Checks that
   413  // ciphertext isn't bigger than the plaintext length plus Overhead()
   414  func sealMsg(t *testing.T, aead cipher.AEAD, ciphertext, nonce, plaintext, addData []byte) []byte {
   415  	t.Helper()
   416  
   417  	initialLen := len(ciphertext)
   418  
   419  	ciphertext = aead.Seal(ciphertext, nonce, plaintext, addData)
   420  
   421  	lenCT := len(ciphertext) - initialLen
   422  
   423  	// Appended ciphertext shouldn't ever be longer than the length of the
   424  	// plaintext plus Overhead
   425  	if lenCT > len(plaintext)+aead.Overhead() {
   426  		t.Errorf("length of ciphertext from Seal exceeds length of plaintext by more than Overhead(); got %d, want <=%d", lenCT, len(plaintext)+aead.Overhead())
   427  	}
   428  
   429  	return ciphertext
   430  }
   431  
   432  func isDeterministic(aead cipher.AEAD) bool {
   433  	// Check if the AEAD is deterministic by checking if the same plaintext
   434  	// encrypted with the same nonce and additional data produces the same
   435  	// ciphertext.
   436  	nonce := make([]byte, aead.NonceSize())
   437  	addData := []byte("additional data")
   438  	plaintext := []byte("plaintext")
   439  	ciphertext1 := aead.Seal(nil, nonce, plaintext, addData)
   440  	ciphertext2 := aead.Seal(nil, nonce, plaintext, addData)
   441  	return bytes.Equal(ciphertext1, ciphertext2)
   442  }
   443  
   444  // Helper function to Open and authenticate ciphertext. Checks that Open
   445  // doesn't error (assuming ciphertext was well-formed with corresponding nonce
   446  // and additional data).
   447  func openWithoutError(t *testing.T, aead cipher.AEAD, plaintext, nonce, ciphertext, addData []byte) []byte {
   448  	t.Helper()
   449  
   450  	plaintext, err := aead.Open(plaintext, nonce, ciphertext, addData)
   451  	if err != nil {
   452  		t.Fatalf("Open returned error on properly formed ciphertext; got \"%s\", want \"nil\"", err)
   453  	}
   454  
   455  	return plaintext
   456  }
   457  

View as plain text