// run_cavp.go processes CAVP input files and generates suitable response
// files, optionally comparing the results against the provided FAX files.
package main

import (
	"bufio"
	"errors"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
)

var (
	oraclePath = flag.String("oracle-bin", "", "Path to the oracle binary")
	suiteDir   = flag.String("suite-dir", "", "Base directory containing the CAVP test suite")
	noFAX      = flag.Bool("no-fax", false, "Skip comparing against FAX files")
	android    = flag.Bool("android", false, "Run tests via ADB")
)

const (
	androidTmpPath       = "/data/local/tmp/"
	androidCAVPPath      = androidTmpPath + "cavp"
	androidLibCryptoPath = androidTmpPath + "libcrypto.so"
)

// test describes a single request file.
type test struct {
	// inFile is the base of the filename without an extension, i.e.
	// “ECBMCT128”.
	inFile string
	// args are the arguments (not including the input filename) to the
	// oracle binary.
	args []string
	// noFAX, if true, indicates that the output cannot be compared against
	// the FAX file. (E.g. because the primitive is non-deterministic.)
	noFAX bool
}

// nextLineState can be used by FAX next-line function to store state.
type nextLineState struct {
	// State used by the KAS test.
	nextIsIUTHash bool
}

// testSuite describes a series of tests that are handled by a single oracle
// binary.
type testSuite struct {
	// directory is the name of the directory in the CAVP input, i.e. “AES”.
	directory string
	// suite names the test suite to pass as the first command-line argument.
	suite string
	// nextLineFunc, if not nil, is the function used to read the next line
	// from the FAX file. This can be used to skip lines and/or mutate them
	// as needed. The second argument can be used by the scanner to store
	// state, if needed. If isWildcard is true on return then line is not
	// meaningful and any line from the response file should be accepted.
	nextLineFunc func(*bufio.Scanner, *nextLineState) (line string, isWildcard, ok bool)
	tests        []test
}

func (t *testSuite) getDirectory() string {
	return filepath.Join(*suiteDir, t.directory)
}

var aesGCMTests = testSuite{
	"AES_GCM",
	"aes_gcm",
	nil,
	[]test{
		{"gcmDecrypt128", []string{"dec", "aes-128-gcm"}, false},
		{"gcmDecrypt192", []string{"dec", "aes-192-gcm"}, false},
		{"gcmDecrypt256", []string{"dec", "aes-256-gcm"}, false},
		{"gcmEncryptExtIV128", []string{"enc", "aes-128-gcm"}, false},
		{"gcmEncryptExtIV192", []string{"enc", "aes-192-gcm"}, false},
		{"gcmEncryptExtIV256", []string{"enc", "aes-256-gcm"}, false},
	},
}

var aesTests = testSuite{
	"AES",
	"aes",
	nil,
	[]test{
		{"CBCGFSbox128", []string{"kat", "aes-128-cbc"}, false},
		{"CBCGFSbox192", []string{"kat", "aes-192-cbc"}, false},
		{"CBCGFSbox256", []string{"kat", "aes-256-cbc"}, false},
		{"CBCKeySbox128", []string{"kat", "aes-128-cbc"}, false},
		{"CBCKeySbox192", []string{"kat", "aes-192-cbc"}, false},
		{"CBCKeySbox256", []string{"kat", "aes-256-cbc"}, false},
		{"CBCMMT128", []string{"kat", "aes-128-cbc"}, false},
		{"CBCMMT192", []string{"kat", "aes-192-cbc"}, false},
		{"CBCMMT256", []string{"kat", "aes-256-cbc"}, false},
		{"CBCVarKey128", []string{"kat", "aes-128-cbc"}, false},
		{"CBCVarKey192", []string{"kat", "aes-192-cbc"}, false},
		{"CBCVarKey256", []string{"kat", "aes-256-cbc"}, false},
		{"CBCVarTxt128", []string{"kat", "aes-128-cbc"}, false},
		{"CBCVarTxt192", []string{"kat", "aes-192-cbc"}, false},
		{"CBCVarTxt256", []string{"kat", "aes-256-cbc"}, false},
		{"ECBGFSbox128", []string{"kat", "aes-128-ecb"}, false},
		{"ECBGFSbox192", []string{"kat", "aes-192-ecb"}, false},
		{"ECBGFSbox256", []string{"kat", "aes-256-ecb"}, false},
		{"ECBKeySbox128", []string{"kat", "aes-128-ecb"}, false},
		{"ECBKeySbox192", []string{"kat", "aes-192-ecb"}, false},
		{"ECBKeySbox256", []string{"kat", "aes-256-ecb"}, false},
		{"ECBMMT128", []string{"kat", "aes-128-ecb"}, false},
		{"ECBMMT192", []string{"kat", "aes-192-ecb"}, false},
		{"ECBMMT256", []string{"kat", "aes-256-ecb"}, false},
		{"ECBVarKey128", []string{"kat", "aes-128-ecb"}, false},
		{"ECBVarKey192", []string{"kat", "aes-192-ecb"}, false},
		{"ECBVarKey256", []string{"kat", "aes-256-ecb"}, false},
		{"ECBVarTxt128", []string{"kat", "aes-128-ecb"}, false},
		{"ECBVarTxt192", []string{"kat", "aes-192-ecb"}, false},
		{"ECBVarTxt256", []string{"kat", "aes-256-ecb"}, false},
		// AES Monte-Carlo tests
		{"ECBMCT128", []string{"mct", "aes-128-ecb"}, false},
		{"ECBMCT192", []string{"mct", "aes-192-ecb"}, false},
		{"ECBMCT256", []string{"mct", "aes-256-ecb"}, false},
		{"CBCMCT128", []string{"mct", "aes-128-cbc"}, false},
		{"CBCMCT192", []string{"mct", "aes-192-cbc"}, false},
		{"CBCMCT256", []string{"mct", "aes-256-cbc"}, false},
	},
}

var ecdsa2KeyPairTests = testSuite{
	"ECDSA2",
	"ecdsa2_keypair",
	nil,
	[]test{{"KeyPair", nil, true}},
}

var ecdsa2PKVTests = testSuite{
	"ECDSA2",
	"ecdsa2_pkv",
	nil,
	[]test{{"PKV", nil, false}},
}

var ecdsa2SigGenTests = testSuite{
	"ECDSA2",
	"ecdsa2_siggen",
	nil,
	[]test{
		{"SigGen", []string{"SigGen"}, true},
		{"SigGenComponent", []string{"SigGenComponent"}, true},
	},
}

var ecdsa2SigVerTests = testSuite{
	"ECDSA2",
	"ecdsa2_sigver",
	nil,
	[]test{{"SigVer", nil, false}},
}

var rsa2KeyGenTests = testSuite{
	"RSA2",
	"rsa2_keygen",
	nil,
	[]test{
		{"KeyGen_RandomProbablyPrime3_3", nil, true},
	},
}

var rsa2SigGenTests = testSuite{
	"RSA2",
	"rsa2_siggen",
	nil,
	[]test{
		{"SigGen15_186-3", []string{"pkcs15"}, true},
		{"SigGenPSS_186-3", []string{"pss"}, true},
	},
}

var rsa2SigVerTests = testSuite{
	"RSA2",
	"rsa2_sigver",
	func(s *bufio.Scanner, state *nextLineState) (string, bool, bool) {
		for {
			if !s.Scan() {
				return "", false, false
			}

			line := s.Text()
			if strings.HasPrefix(line, "p = ") || strings.HasPrefix(line, "d = ") || strings.HasPrefix(line, "SaltVal = ") || strings.HasPrefix(line, "EM with ") {
				continue
			}
			if strings.HasPrefix(line, "q = ") {
				// Skip the "q = " line and an additional blank line.
				if !s.Scan() ||
					len(strings.TrimSpace(s.Text())) > 0 {
					return "", false, false
				}
				continue
			}
			return line, false, true
		}
	},
	[]test{
		{"SigVer15_186-3", []string{"pkcs15"}, false},
		{"SigVerPSS_186-3", []string{"pss"}, false},
	},
}

var hmacTests = testSuite{
	"HMAC",
	"hmac",
	nil,
	[]test{{"HMAC", nil, false}},
}

var shaTests = testSuite{
	"SHA",
	"sha",
	nil,
	[]test{
		{"SHA1LongMsg", []string{"SHA1"}, false},
		{"SHA1ShortMsg", []string{"SHA1"}, false},
		{"SHA224LongMsg", []string{"SHA224"}, false},
		{"SHA224ShortMsg", []string{"SHA224"}, false},
		{"SHA256LongMsg", []string{"SHA256"}, false},
		{"SHA256ShortMsg", []string{"SHA256"}, false},
		{"SHA384LongMsg", []string{"SHA384"}, false},
		{"SHA384ShortMsg", []string{"SHA384"}, false},
		{"SHA512LongMsg", []string{"SHA512"}, false},
		{"SHA512ShortMsg", []string{"SHA512"}, false},
	},
}

var shaMonteTests = testSuite{
	"SHA",
	"sha_monte",
	nil,
	[]test{
		{"SHA1Monte", []string{"SHA1"}, false},
		{"SHA224Monte", []string{"SHA224"}, false},
		{"SHA256Monte", []string{"SHA256"}, false},
		{"SHA384Monte", []string{"SHA384"}, false},
		{"SHA512Monte", []string{"SHA512"}, false},
	},
}

var ctrDRBGTests = testSuite{
	"DRBG800-90A",
	"ctr_drbg",
	nil,
	[]test{{"CTR_DRBG", nil, false}},
}

var tdesTests = testSuite{
	"TDES",
	"tdes",
	nil,
	[]test{
		{"TCBCMMT2", []string{"kat", "des-ede-cbc"}, false},
		{"TCBCMMT3", []string{"kat", "des-ede3-cbc"}, false},
		{"TCBCMonte2", []string{"mct", "des-ede3-cbc"}, false},
		{"TCBCMonte3", []string{"mct", "des-ede3-cbc"}, false},
		{"TCBCinvperm", []string{"kat", "des-ede3-cbc"}, false},
		{"TCBCpermop", []string{"kat", "des-ede3-cbc"}, false},
		{"TCBCsubtab", []string{"kat", "des-ede3-cbc"}, false},
		{"TCBCvarkey", []string{"kat", "des-ede3-cbc"}, false},
		{"TCBCvartext", []string{"kat", "des-ede3-cbc"}, false},
		{"TECBMMT2", []string{"kat", "des-ede"}, false},
		{"TECBMMT3", []string{"kat", "des-ede3"}, false},
		{"TECBMonte2", []string{"mct", "des-ede3"}, false},
		{"TECBMonte3", []string{"mct", "des-ede3"}, false},
		{"TECBinvperm", []string{"kat", "des-ede3"}, false},
		{"TECBpermop", []string{"kat", "des-ede3"}, false},
		{"TECBsubtab", []string{"kat", "des-ede3"}, false},
		{"TECBvarkey", []string{"kat", "des-ede3"}, false},
		{"TECBvartext", []string{"kat", "des-ede3"}, false},
	},
}

var keyWrapTests = testSuite{
	"KeyWrap38F",
	"keywrap",
	nil,
	[]test{
		{"KW_AD_128", []string{"dec", "128"}, false},
		{"KW_AD_192", []string{"dec", "192"}, false},
		{"KW_AD_256", []string{"dec", "256"}, false},
		{"KW_AE_128", []string{"enc", "128"}, false},
		{"KW_AE_192", []string{"enc", "192"}, false},
		{"KW_AE_256", []string{"enc", "256"}, false},
		{"KWP_AD_128", []string{"dec-pad", "128"}, false},
		{"KWP_AD_192", []string{"dec-pad", "192"}, false},
		{"KWP_AD_256", []string{"dec-pad", "256"}, false},
		{"KWP_AE_128", []string{"enc-pad", "128"}, false},
		{"KWP_AE_192", []string{"enc-pad", "192"}, false},
		{"KWP_AE_256", []string{"enc-pad", "256"}, false},
	},
}

var kasTests = testSuite{
	"KAS",
	"kas",
	func(s *bufio.Scanner, state *nextLineState) (line string, isWildcard, ok bool) {
		for {
			// If the response file will include the IUT hash next,
			// return a wildcard signal because this cannot be
			// matched against the FAX file.
			if state.nextIsIUTHash {
				state.nextIsIUTHash = false
				return "", true, true
			}

			if !s.Scan() {
				return "", false, false
			}

			line := s.Text()
			if strings.HasPrefix(line, "deCAVS = ") || strings.HasPrefix(line, "Z = ") {
				continue
			}
			if strings.HasPrefix(line, "CAVSHashZZ = ") {
				state.nextIsIUTHash = true
			}
			return line, false, true
		}
	},
	[]test{
		{"KASFunctionTest_ECCEphemeralUnified_NOKC_ZZOnly_init", []string{"function"}, true},
		{"KASFunctionTest_ECCEphemeralUnified_NOKC_ZZOnly_resp", []string{"function"}, true},
		{"KASValidityTest_ECCEphemeralUnified_NOKC_ZZOnly_init", []string{"validity"}, false},
		{"KASValidityTest_ECCEphemeralUnified_NOKC_ZZOnly_resp", []string{"validity"}, false},
	},
}

var tlsKDFTests = testSuite{
	"KDF135",
	"tlskdf",
	nil,
	[]test{
		{"tls", nil, false},
	},
}

var testSuites = []*testSuite{
	&aesGCMTests,
	&aesTests,
	&ctrDRBGTests,
	&ecdsa2KeyPairTests,
	&ecdsa2PKVTests,
	&ecdsa2SigGenTests,
	&ecdsa2SigVerTests,
	&hmacTests,
	&keyWrapTests,
	&rsa2KeyGenTests,
	&rsa2SigGenTests,
	&rsa2SigVerTests,
	&shaTests,
	&shaMonteTests,
	&tdesTests,
	&kasTests,
	&tlsKDFTests,
}

// testInstance represents a specific test in a testSuite.
type testInstance struct {
	suite     *testSuite
	testIndex int
}

func worker(wg *sync.WaitGroup, work <-chan testInstance) {
	defer wg.Done()

	for ti := range work {
		test := ti.suite.tests[ti.testIndex]

		if err := doTest(ti.suite, test); err != nil {
			fmt.Fprintf(os.Stderr, "%s\n", err)
			os.Exit(2)
		}

		if !*noFAX && !test.noFAX {
			if err := compareFAX(ti.suite, test); err != nil {
				fmt.Fprintf(os.Stderr, "%s\n", err)
				os.Exit(3)
			}
		}
	}
}

func checkAndroidPrereqs() error {
	// The cavp binary, and a matching libcrypto.so, are required to be placed
	// in /data/local/tmp before running this script.
	if err := exec.Command("adb", "shell", "ls", androidCAVPPath).Run(); err != nil {
		return errors.New("failed to list cavp binary; ensure that adb works and cavp binary is in place: " + err.Error())
	}
	if err := exec.Command("adb", "shell", "ls", androidLibCryptoPath).Run(); err != nil {
		return errors.New("failed to list libcrypto.so; ensure that library is in place: " + err.Error())
	}
	return nil
}

func main() {
	flag.Parse()

	if *android {
		if err := checkAndroidPrereqs(); err != nil {
			fmt.Fprintf(os.Stderr, "%s\n", err)
			os.Exit(1)
		}
	} else if len(*oraclePath) == 0 {
		fmt.Fprintf(os.Stderr, "Must give -oracle-bin\n")
		os.Exit(1)
	}

	work := make(chan testInstance)
	var wg sync.WaitGroup

	numWorkers := runtime.NumCPU()
	if *android {
		numWorkers = 1
	}

	for i := 0; i < numWorkers; i++ {
		wg.Add(1)
		go worker(&wg, work)
	}

	for _, suite := range testSuites {
		for i := range suite.tests {
			work <- testInstance{suite, i}
		}
	}

	close(work)
	wg.Wait()
}

func doTest(suite *testSuite, test test) error {
	bin := *oraclePath
	var args []string

	if *android {
		bin = "adb"
		args = []string{"shell", "LD_LIBRARY_PATH=" + androidTmpPath, androidCAVPPath}
	}

	args = append(args, suite.suite)
	args = append(args, test.args...)
	reqPath := filepath.Join(suite.getDirectory(), "req", test.inFile+".req")
	var reqPathOnDevice string

	if *android {
		reqPathOnDevice = path.Join(androidTmpPath, test.inFile+".req")
		if err := exec.Command("adb", "push", reqPath, reqPathOnDevice).Run(); err != nil {
			return errors.New("failed to push request file: " + err.Error())
		}
		args = append(args, reqPathOnDevice)
	} else {
		args = append(args, reqPath)
	}

	respDir := filepath.Join(suite.getDirectory(), "resp")
	if err := os.Mkdir(respDir, 0755); err != nil && !os.IsExist(err) {
		return fmt.Errorf("cannot create resp directory: %s", err)
	}
	outPath := filepath.Join(respDir, test.inFile+".rsp")
	outFile, err := os.OpenFile(outPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
	if err != nil {
		return fmt.Errorf("cannot open output file for %q %q: %s", suite.getDirectory(), test.inFile, err)
	}
	defer outFile.Close()

	cmd := exec.Command(bin, args...)
	cmd.Stdout = outFile
	cmd.Stderr = os.Stderr

	cmdLine := strings.Join(append([]string{bin}, args...), " ")
	startTime := time.Now()
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("cannot run command for %q %q (%s): %s", suite.getDirectory(), test.inFile, cmdLine, err)
	}

	fmt.Printf("%s (%ds)\n", cmdLine, int(time.Since(startTime).Seconds()))

	if *android {
		exec.Command("adb", "shell", "rm", reqPathOnDevice).Run()
	}

	return nil
}

func canonicalizeLine(in string) string {
	if strings.HasPrefix(in, "Result = P (") {
		return "Result = P"
	}
	if strings.HasPrefix(in, "Result = F (") {
		return "Result = F"
	}
	return in
}

func compareFAX(suite *testSuite, test test) error {
	nextLineFunc := suite.nextLineFunc
	if nextLineFunc == nil {
		nextLineFunc = func(s *bufio.Scanner, state *nextLineState) (string, bool, bool) {
			if !s.Scan() {
				return "", false, false
			}
			return s.Text(), false, true
		}
	}

	respPath := filepath.Join(suite.getDirectory(), "resp", test.inFile+".rsp")
	respFile, err := os.Open(respPath)
	if err != nil {
		return fmt.Errorf("cannot read output of %q %q: %s", suite.getDirectory(), test.inFile, err)
	}
	defer respFile.Close()

	faxPath := filepath.Join(suite.getDirectory(), "fax", test.inFile+".fax")
	faxFile, err := os.Open(faxPath)
	if err != nil {
		return fmt.Errorf("cannot open fax file for %q %q: %s", suite.getDirectory(), test.inFile, err)
	}
	defer faxFile.Close()

	respScanner := bufio.NewScanner(respFile)
	faxScanner := bufio.NewScanner(faxFile)
	var nextLineState nextLineState

	lineNo := 0
	inHeader := true

	for respScanner.Scan() {
		lineNo++
		respLine := respScanner.Text()
		var faxLine string
		var isWildcard, ok bool

		if inHeader && (len(respLine) == 0 || respLine[0] == '#') {
			continue
		}

		for {
			haveFaxLine := false

			if inHeader {
				for {
					if faxLine, isWildcard, ok = nextLineFunc(faxScanner, &nextLineState); !ok {
						break
					}
					if len(faxLine) != 0 && faxLine[0] != '#' {
						haveFaxLine = true
						break
					}
				}

				inHeader = false
			} else {
				faxLine, isWildcard, haveFaxLine = nextLineFunc(faxScanner, &nextLineState)
			}

			if !haveFaxLine {
				// Ignore blank lines at the end of the generated file.
				if len(respLine) == 0 {
					break
				}
				return fmt.Errorf("resp file is longer than fax for %q %q", suite.getDirectory(), test.inFile)
			}

			if strings.HasPrefix(faxLine, " (Reason: ") {
				continue
			}

			break
		}

		if isWildcard || canonicalizeLine(faxLine) == canonicalizeLine(respLine) {
			continue
		}

		return fmt.Errorf("resp and fax differ at line %d for %q %q: %q vs %q", lineNo, suite.getDirectory(), test.inFile, respLine, faxLine)
	}

	if _, _, ok := nextLineFunc(faxScanner, &nextLineState); ok {
		return fmt.Errorf("fax file is longer than resp for %q %q", suite.getDirectory(), test.inFile)
	}

	return nil
}
