acvp: add XTS support.

Since we don't have XTS in the FIPS module, this change uses
testmodulewrapper for testing.

Change-Id: I82117472ea4288d017983fe9cc11d4ba808a972a
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/45064
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/util/fipstools/acvp/ACVP.md b/util/fipstools/acvp/ACVP.md
index 4a93e87..a477492 100644
--- a/util/fipstools/acvp/ACVP.md
+++ b/util/fipstools/acvp/ACVP.md
@@ -59,6 +59,8 @@
 | AES-KW/seal          | (dummy), key, plaintext, (dummy), (dummy) | Ciphertext |
 | AES-KWP/open         | (dummy), key, ciphertext, (dummy), (dummy) | One-byte success flag, plaintext or empty |
 | AES-KWP/seal         | (dummy), key, plaintext, (dummy), (dummy) | Ciphertext |
+| AES-XTS/decrypt      | Key, ciphertext, tweak | Plaintext |
+| AES-XTS/encrypt      | Key, plaintext, tweak | Ciphertext |
 | AES/decrypt          | Key, input block, num iterations¹ | Result, Previous result |
 | AES/encrypt          | Key, input block, num iterations¹ | Result, Previous result |
 | CMAC-AES             | Number output bytes, key, message | MAC |
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index c0d48b7..785e7b2 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -80,6 +80,7 @@
 		"ACVP-AES-ECB":   &blockCipher{"AES", 16, 2, true, false, iterateAES},
 		"ACVP-AES-CBC":   &blockCipher{"AES-CBC", 16, 2, true, true, iterateAESCBC},
 		"ACVP-AES-CTR":   &blockCipher{"AES-CTR", 16, 1, false, true, nil},
+		"ACVP-AES-XTS":   &xts{},
 		"ACVP-TDES-ECB":  &blockCipher{"3DES-ECB", 8, 3, true, false, iterate3DES},
 		"ACVP-TDES-CBC":  &blockCipher{"3DES-CBC", 8, 3, true, true, iterate3DESCBC},
 		"ACVP-AES-GCM":   &aead{"AES-GCM", false},
diff --git a/util/fipstools/acvp/acvptool/subprocess/xts.go b/util/fipstools/acvp/acvptool/subprocess/xts.go
new file mode 100644
index 0000000..e3546a3
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/subprocess/xts.go
@@ -0,0 +1,148 @@
+// Copyright (c) 2021, Google Inc.
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+package subprocess
+
+import (
+	"encoding/binary"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+)
+
+// The following structures reflect the JSON of ACVP hash tests. See
+// https://pages.nist.gov/ACVP/draft-celi-acvp-symmetric.html
+
+type xtsTestVectorSet struct {
+	Groups []xtsTestGroup `json:"testGroups"`
+}
+
+type xtsTestGroup struct {
+	ID         uint64 `json:"tgId"`
+	Type       string `json:"testType"`
+	Direction  string `json:"direction"`
+	KeyLen     int    `json:"keyLen"`
+	PayloadLen int    `json:"payloadLen"`
+	Tests      []struct {
+		ID            uint64  `json:"tcId"`
+		KeyHex        string  `json:"key"`
+		PlaintextHex  string  `json:"pt"`
+		CiphertextHex string  `json:"ct"`
+		SectorNum     *uint64 `json:"sequenceNumber"`
+		TweakHex      *string `json:"tweakValue"`
+	} `json:"tests"`
+}
+
+type xtsTestGroupResponse struct {
+	ID    uint64            `json:"tgId"`
+	Tests []xtsTestResponse `json:"tests"`
+}
+
+type xtsTestResponse struct {
+	ID            uint64 `json:"tcId"`
+	PlaintextHex  string `json:"pt,omitempty"`
+	CiphertextHex string `json:"ct,omitempty"`
+}
+
+// xts implements an ACVP algorithm by making requests to the subprocess to
+// encrypt/decrypt with AES-XTS.
+type xts struct{}
+
+func (h *xts) Process(vectorSet []byte, m Transactable) (interface{}, error) {
+	var parsed xtsTestVectorSet
+	if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+		return nil, err
+	}
+
+	var ret []xtsTestGroupResponse
+	for _, group := range parsed.Groups {
+		response := xtsTestGroupResponse{
+			ID: group.ID,
+		}
+
+		if group.Type != "AFT" {
+			return nil, fmt.Errorf("unknown XTS test type %q", group.Type)
+		}
+
+		var decrypt bool
+		switch group.Direction {
+		case "encrypt":
+			decrypt = false
+		case "decrypt":
+			decrypt = true
+		default:
+			return nil, fmt.Errorf("unknown XTS direction %q", group.Direction)
+		}
+
+		funcName := "AES-XTS/" + group.Direction
+
+		for _, test := range group.Tests {
+			if group.KeyLen != len(test.KeyHex)*4/2 {
+				return nil, fmt.Errorf("test case %d/%d contains hex message of length %d but specifies a key length of %d (remember that XTS keys are twice the length of the underlying key size)", group.ID, test.ID, len(test.KeyHex), group.KeyLen)
+			}
+			key, err := hex.DecodeString(test.KeyHex)
+			if err != nil {
+				return nil, fmt.Errorf("failed to decode hex in test case %d/%d: %s", group.ID, test.ID, err)
+			}
+
+			var tweak [16]byte
+			if test.TweakHex != nil {
+				t, err := hex.DecodeString(*test.TweakHex)
+				if err != nil {
+					return nil, fmt.Errorf("failed to decode hex in test case %d/%d: %s", group.ID, test.ID, err)
+				}
+				if len(t) != len(tweak) {
+					return nil, fmt.Errorf("wrong tweak length (%d bytes) in test case %d/%d", len(t), group.ID, test.ID)
+				}
+				copy(tweak[:], t)
+			} else if test.SectorNum != nil {
+				// Sector numbers (or "sequence numbers", as NIST calls them) are turned
+				// into tweak values by encoding them in little-endian form. See IEEE
+				// 1619-2007, section 5.1.
+				binary.LittleEndian.PutUint64(tweak[:8], *test.SectorNum)
+			} else {
+				return nil, fmt.Errorf("neither sector number nor explicit tweak in test case %d/%d", group.ID, test.ID)
+			}
+
+			var msg []byte
+			if decrypt {
+				msg, err = hex.DecodeString(test.CiphertextHex)
+			} else {
+				msg, err = hex.DecodeString(test.PlaintextHex)
+			}
+
+			if err != nil {
+				return nil, fmt.Errorf("failed to decode hex in test case %d/%d: %s", group.ID, test.ID, err)
+			}
+
+			result, err := m.Transact(funcName, 1, key, msg, tweak[:])
+			if err != nil {
+				return nil, fmt.Errorf("submodule failed on test case %d/%d: %s", group.ID, test.ID, err)
+			}
+
+			testResponse := xtsTestResponse{ID: test.ID}
+			if decrypt {
+				testResponse.PlaintextHex = hex.EncodeToString(result[0])
+			} else {
+				testResponse.CiphertextHex = hex.EncodeToString(result[0])
+			}
+
+			response.Tests = append(response.Tests, testResponse)
+		}
+
+		ret = append(ret, response)
+	}
+
+	return ret, nil
+}
diff --git a/util/fipstools/acvp/acvptool/test/expected/ACVP-AES-XTS.bz2 b/util/fipstools/acvp/acvptool/test/expected/ACVP-AES-XTS.bz2
new file mode 100644
index 0000000..a197e27
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/expected/ACVP-AES-XTS.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/tests.json b/util/fipstools/acvp/acvptool/test/tests.json
index 842f3ff..f0bafd8 100644
--- a/util/fipstools/acvp/acvptool/test/tests.json
+++ b/util/fipstools/acvp/acvptool/test/tests.json
@@ -6,6 +6,7 @@
 {"Wrapper": "modulewrapper", "In": "vectors/ACVP-AES-GCM.bz2", "Out": "expected/ACVP-AES-GCM.bz2"},
 {"Wrapper": "modulewrapper", "In": "vectors/ACVP-AES-KW.bz2", "Out": "expected/ACVP-AES-KW.bz2"},
 {"Wrapper": "modulewrapper", "In": "vectors/ACVP-AES-KWP.bz2", "Out": "expected/ACVP-AES-KWP.bz2"},
+{"Wrapper": "testmodulewrapper", "In": "vectors/ACVP-AES-XTS.bz2", "Out": "expected/ACVP-AES-XTS.bz2"},
 {"Wrapper": "modulewrapper", "In": "vectors/ACVP-TDES-CBC.bz2", "Out": "expected/ACVP-TDES-CBC.bz2"},
 {"Wrapper": "modulewrapper", "In": "vectors/ACVP-TDES-ECB.bz2", "Out": "expected/ACVP-TDES-ECB.bz2"},
 {"Wrapper": "modulewrapper", "In": "vectors/CMAC-AES.bz2", "Out": "expected/CMAC-AES.bz2"},
diff --git a/util/fipstools/acvp/acvptool/test/vectors/ACVP-AES-XTS.bz2 b/util/fipstools/acvp/acvptool/test/vectors/ACVP-AES-XTS.bz2
new file mode 100644
index 0000000..43bd027
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/vectors/ACVP-AES-XTS.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
index 284b5d3..00c32ab 100644
--- a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
+++ b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
@@ -6,6 +6,7 @@
 
 import (
 	"bytes"
+	"crypto/aes"
 	"crypto/hmac"
 	"crypto/rand"
 	"crypto/sha256"
@@ -14,11 +15,15 @@
 	"fmt"
 	"io"
 	"os"
+
+	"golang.org/x/crypto/xts"
 )
 
 var handlers = map[string]func([][]byte) error{
-	"getConfig":   getConfig,
-	"KDF-counter": kdfCounter,
+	"getConfig":       getConfig,
+	"KDF-counter":     kdfCounter,
+	"AES-XTS/encrypt": xtsEncrypt,
+	"AES-XTS/decrypt": xtsDecrypt,
 }
 
 func getConfig(args [][]byte) error {
@@ -47,6 +52,23 @@
 				32
 			]
 		}]
+	}, {
+		"algorithm": "ACVP-AES-XTS",
+		"revision": "1.0",
+		"direction": [
+		  "encrypt",
+		  "decrypt"
+		],
+		"keyLen": [
+		  128,
+		  256
+		],
+		"payloadLen": [
+		  1024
+		],
+		"tweakMode": [
+		  "number"
+		]
 	}
 ]`))
 }
@@ -122,6 +144,50 @@
 	return nil
 }
 
+func xtsEncrypt(args [][]byte) error {
+	return doXTS(args, false)
+}
+
+func xtsDecrypt(args [][]byte) error {
+	return doXTS(args, true)
+}
+
+func doXTS(args [][]byte, decrypt bool) error {
+	if len(args) != 3 {
+		return fmt.Errorf("XTS received %d args, wanted 3", len(args))
+	}
+	key := args[0]
+	msg := args[1]
+	tweak := args[2]
+
+	if len(msg)%16 != 0 {
+		return fmt.Errorf("XTS received %d-byte msg, need multiple of 16", len(msg))
+	}
+	if len(tweak) != 16 {
+		return fmt.Errorf("XTS received %d-byte tweak, wanted 16", len(tweak))
+	}
+
+	var zeros [8]byte
+	if !bytes.Equal(tweak[8:], zeros[:]) {
+		return errors.New("XTS received tweak with invalid structure. Ensure that configuration specifies a 'number' tweak")
+	}
+
+	sectorNum := binary.LittleEndian.Uint64(tweak[:8])
+
+	c, err := xts.NewCipher(aes.NewCipher, key)
+	if err != nil {
+		return err
+	}
+
+	if decrypt {
+		c.Decrypt(msg, msg, sectorNum)
+	} else {
+		c.Encrypt(msg, msg, sectorNum)
+	}
+
+	return reply(msg)
+}
+
 const (
 	maxArgs       = 8
 	maxArgLength  = 1 << 20