// Copyright (c) 2019, 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 contains functionality to talk to a modulewrapper for // testing of various algorithm implementations. package subprocess import ( "encoding/binary" "encoding/json" "errors" "fmt" "io" "os" "os/exec" ) // Transactable provides an interface to allow test injection of transactions // that don't call a server. type Transactable interface { Transact(cmd string, expectedResults int, args ...[]byte) ([][]byte, error) TransactAsync(cmd string, expectedResults int, args [][]byte, callback func([][]byte) error) Barrier(callback func()) error Flush() error } // Subprocess is a "middle" layer that interacts with a FIPS module via running // a command and speaking a simple protocol over stdin/stdout. type Subprocess struct { cmd *exec.Cmd stdin io.WriteCloser stdout io.ReadCloser primitives map[string]primitive // supportsFlush is true if the modulewrapper indicated that it wants to receive flush commands. supportsFlush bool // pendingReads is a queue of expected responses. `readerRoutine` reads each response and calls the callback in the matching pendingRead. pendingReads chan pendingRead // readerFinished is a channel that is closed if `readerRoutine` has finished (e.g. because of a read error). readerFinished chan struct{} } // pendingRead represents an expected response from the modulewrapper. type pendingRead struct { // barrierCallback is called as soon as this pendingRead is the next in the queue, before any read from the modulewrapper. barrierCallback func() // callback is called with the result from the modulewrapper. If this is nil then no read is performed. callback func(result [][]byte) error // cmd is the command that requested this read for logging purposes. cmd string expectedNumResults int } // New returns a new Subprocess middle layer that runs the given binary. func New(path string) (*Subprocess, error) { cmd := exec.Command(path) cmd.Stderr = os.Stderr stdin, err := cmd.StdinPipe() if err != nil { return nil, err } stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } if err := cmd.Start(); err != nil { return nil, err } return NewWithIO(cmd, stdin, stdout), nil } // maxPending is the maximum number of requests that can be in the pipeline. const maxPending = 4096 // NewWithIO returns a new Subprocess middle layer with the given ReadCloser and // WriteCloser. The returned Subprocess will call Wait on the Cmd when closed. func NewWithIO(cmd *exec.Cmd, in io.WriteCloser, out io.ReadCloser) *Subprocess { m := &Subprocess{ cmd: cmd, stdin: in, stdout: out, pendingReads: make(chan pendingRead, maxPending), readerFinished: make(chan struct{}), } m.primitives = map[string]primitive{ "SHA-1": &hashPrimitive{"SHA-1", 20}, "SHA2-224": &hashPrimitive{"SHA2-224", 28}, "SHA2-256": &hashPrimitive{"SHA2-256", 32}, "SHA2-384": &hashPrimitive{"SHA2-384", 48}, "SHA2-512": &hashPrimitive{"SHA2-512", 64}, "SHA2-512/256": &hashPrimitive{"SHA2-512/256", 32}, "SHA3-224": &hashPrimitive{"SHA3-224", 28}, "SHA3-256": &hashPrimitive{"SHA3-256", 32}, "SHA3-384": &hashPrimitive{"SHA3-384", 48}, "SHA3-512": &hashPrimitive{"SHA3-512", 64}, "ACVP-AES-ECB": &blockCipher{"AES", 16, 2, true, false, iterateAES}, "ACVP-AES-CBC": &blockCipher{"AES-CBC", 16, 2, true, true, iterateAESCBC}, "ACVP-AES-CBC-CS3": &blockCipher{"AES-CBC-CS3", 16, 1, false, true, iterateAESCBC}, "ACVP-AES-CTR": &blockCipher{"AES-CTR", 16, 1, false, true, nil}, "ACVP-TDES-ECB": &blockCipher{"3DES-ECB", 8, 3, true, false, iterate3DES}, "ACVP-TDES-CBC": &blockCipher{"3DES-CBC", 8, 3, true, true, iterate3DESCBC}, "ACVP-AES-XTS": &xts{}, "ACVP-AES-GCM": &aead{"AES-GCM", false}, "ACVP-AES-GMAC": &aead{"AES-GCM", false}, "ACVP-AES-CCM": &aead{"AES-CCM", true}, "ACVP-AES-KW": &aead{"AES-KW", false}, "ACVP-AES-KWP": &aead{"AES-KWP", false}, "HMAC-SHA-1": &hmacPrimitive{"HMAC-SHA-1", 20}, "HMAC-SHA2-224": &hmacPrimitive{"HMAC-SHA2-224", 28}, "HMAC-SHA2-256": &hmacPrimitive{"HMAC-SHA2-256", 32}, "HMAC-SHA2-384": &hmacPrimitive{"HMAC-SHA2-384", 48}, "HMAC-SHA2-512": &hmacPrimitive{"HMAC-SHA2-512", 64}, "HMAC-SHA2-512/256": &hmacPrimitive{"HMAC-SHA2-512/256", 32}, "HMAC-SHA3-224": &hmacPrimitive{"HMAC-SHA3-224", 28}, "HMAC-SHA3-256": &hmacPrimitive{"HMAC-SHA3-256", 32}, "HMAC-SHA3-384": &hmacPrimitive{"HMAC-SHA3-384", 48}, "HMAC-SHA3-512": &hmacPrimitive{"HMAC-SHA3-512", 64}, "ctrDRBG": &drbg{"ctrDRBG", map[string]bool{"AES-128": true, "AES-192": true, "AES-256": true}}, "hmacDRBG": &drbg{"hmacDRBG", map[string]bool{"SHA-1": true, "SHA2-224": true, "SHA2-256": true, "SHA2-384": true, "SHA2-512": true}}, "KDF": &kdfPrimitive{}, "KDA": &hkdf{}, "TLS-v1.2": &tlsKDF{}, "TLS-v1.3": &tls13{}, "CMAC-AES": &keyedMACPrimitive{"CMAC-AES"}, "RSA": &rsa{}, "KAS-ECC-SSC": &kas{}, "KAS-FFC-SSC": &kasDH{}, } m.primitives["ECDSA"] = &ecdsa{"ECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives} m.primitives["EDDSA"] = &ecdsa{"ECDSA", map[string]bool{"ED-25519": true}, nil} go m.readerRoutine() return m } // Close signals the child process to exit and waits for it to complete. func (m *Subprocess) Close() { m.stdout.Close() m.stdin.Close() m.cmd.Wait() close(m.pendingReads) <-m.readerFinished } func (m *Subprocess) flush() error { if !m.supportsFlush { return nil } const cmd = "flush" buf := make([]byte, 8, 8+len(cmd)) binary.LittleEndian.PutUint32(buf, 1) binary.LittleEndian.PutUint32(buf[4:], uint32(len(cmd))) buf = append(buf, []byte(cmd)...) if _, err := m.stdin.Write(buf); err != nil { return err } return nil } func (m *Subprocess) enqueueRead(pending pendingRead) error { select { case <-m.readerFinished: panic("attempted to enqueue request after the reader failed") default: } select { case m.pendingReads <- pending: break default: // `pendingReads` is full. Ensure that the modulewrapper will process // some outstanding requests to free up space in the queue. if err := m.flush(); err != nil { return err } m.pendingReads <- pending } return nil } // TransactAsync performs a single request--response pair with the subprocess. // The callback will run at some future point, in a separate goroutine. All // callbacks will, however, be run in the order that TransactAsync was called. // Use Flush to wait for all outstanding callbacks. func (m *Subprocess) TransactAsync(cmd string, expectedNumResults int, args [][]byte, callback func(result [][]byte) error) { if err := m.enqueueRead(pendingRead{nil, callback, cmd, expectedNumResults}); err != nil { panic(err) } argLength := len(cmd) for _, arg := range args { argLength += len(arg) } buf := make([]byte, 4*(2+len(args)), 4*(2+len(args))+argLength) binary.LittleEndian.PutUint32(buf, uint32(1+len(args))) binary.LittleEndian.PutUint32(buf[4:], uint32(len(cmd))) for i, arg := range args { binary.LittleEndian.PutUint32(buf[4*(i+2):], uint32(len(arg))) } buf = append(buf, []byte(cmd)...) for _, arg := range args { buf = append(buf, arg...) } if _, err := m.stdin.Write(buf); err != nil { panic(err) } } // Flush tells the subprocess to complete all outstanding requests and waits // for all outstanding TransactAsync callbacks to complete. func (m *Subprocess) Flush() error { if m.supportsFlush { m.flush() } done := make(chan struct{}) if err := m.enqueueRead(pendingRead{barrierCallback: func() { close(done) }}); err != nil { return err } <-done return nil } // Barrier runs callback after all outstanding TransactAsync callbacks have // been run. func (m *Subprocess) Barrier(callback func()) error { return m.enqueueRead(pendingRead{barrierCallback: callback}) } func (m *Subprocess) Transact(cmd string, expectedNumResults int, args ...[]byte) ([][]byte, error) { done := make(chan struct{}) var result [][]byte m.TransactAsync(cmd, expectedNumResults, args, func(r [][]byte) error { result = r close(done) return nil }) if err := m.flush(); err != nil { return nil, err } select { case <-done: return result, nil case <-m.readerFinished: panic("was still waiting for a result when the reader finished") } } func (m *Subprocess) readerRoutine() { defer close(m.readerFinished) for pendingRead := range m.pendingReads { if pendingRead.barrierCallback != nil { pendingRead.barrierCallback() } if pendingRead.callback == nil { continue } result, err := m.readResult(pendingRead.cmd, pendingRead.expectedNumResults) if err != nil { panic(fmt.Errorf("failed to read from subprocess: %w", err)) } if err := pendingRead.callback(result); err != nil { panic(fmt.Errorf("result from subprocess was rejected: %w", err)) } } } func (m *Subprocess) readResult(cmd string, expectedNumResults int) ([][]byte, error) { buf := make([]byte, 4) if _, err := io.ReadFull(m.stdout, buf); err != nil { return nil, err } numResults := binary.LittleEndian.Uint32(buf) if int(numResults) != expectedNumResults { return nil, fmt.Errorf("expected %d results from %q but got %d", expectedNumResults, cmd, numResults) } buf = make([]byte, 4*numResults) if _, err := io.ReadFull(m.stdout, buf); err != nil { return nil, err } var resultsLength uint64 for i := uint32(0); i < numResults; i++ { resultsLength += uint64(binary.LittleEndian.Uint32(buf[4*i:])) } if resultsLength > (1 << 30) { return nil, fmt.Errorf("results too large (%d bytes)", resultsLength) } results := make([]byte, resultsLength) if _, err := io.ReadFull(m.stdout, results); err != nil { return nil, err } ret := make([][]byte, 0, numResults) var offset int for i := uint32(0); i < numResults; i++ { length := binary.LittleEndian.Uint32(buf[4*i:]) ret = append(ret, results[offset:offset+int(length)]) offset += int(length) } return ret, nil } // Config returns a JSON blob that describes the supported primitives. The // format of the blob is defined by ACVP. See // http://usnistgov.github.io/ACVP/artifacts/draft-fussell-acvp-spec-00.html#rfc.section.11.15.2.1 func (m *Subprocess) Config() ([]byte, error) { results, err := m.Transact("getConfig", 1) if err != nil { return nil, err } var config []struct { Algorithm string `json:"algorithm"` Features []string `json:"features"` } if err := json.Unmarshal(results[0], &config); err != nil { return nil, errors.New("failed to parse config response from wrapper: " + err.Error()) } for _, algo := range config { if algo.Algorithm == "acvptool" { for _, feature := range algo.Features { switch feature { case "batch": m.supportsFlush = true } } } else if _, ok := m.primitives[algo.Algorithm]; !ok { return nil, fmt.Errorf("wrapper config advertises support for unknown algorithm %q", algo.Algorithm) } } return results[0], nil } // Process runs a set of test vectors and returns the result. func (m *Subprocess) Process(algorithm string, vectorSet []byte) (any, error) { prim, ok := m.primitives[algorithm] if !ok { return nil, fmt.Errorf("unknown algorithm %q", algorithm) } ret, err := prim.Process(vectorSet, m) if err != nil { return nil, err } return ret, nil } type primitive interface { Process(vectorSet []byte, t Transactable) (any, error) } func uint32le(n uint32) []byte { var ret [4]byte binary.LittleEndian.PutUint32(ret[:], n) return ret[:] }