• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2019 The BoringSSL Authors
2//
3// Permission to use, copy, modify, and/or distribute this software for any
4// purpose with or without fee is hereby granted, provided that the above
5// copyright notice and this permission notice appear in all copies.
6//
7// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
10// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
12// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
13// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
15// Package subprocess contains functionality to talk to a modulewrapper for
16// testing of various algorithm implementations.
17package subprocess
18
19import (
20	"encoding/binary"
21	"encoding/json"
22	"errors"
23	"fmt"
24	"io"
25	"os"
26	"os/exec"
27)
28
29// Transactable provides an interface to allow test injection of transactions
30// that don't call a server.
31type Transactable interface {
32	Transact(cmd string, expectedResults int, args ...[]byte) ([][]byte, error)
33	TransactAsync(cmd string, expectedResults int, args [][]byte, callback func([][]byte) error)
34	Barrier(callback func()) error
35	Flush() error
36}
37
38// Subprocess is a "middle" layer that interacts with a FIPS module via running
39// a command and speaking a simple protocol over stdin/stdout.
40type Subprocess struct {
41	cmd        *exec.Cmd
42	stdin      io.WriteCloser
43	stdout     io.ReadCloser
44	primitives map[string]primitive
45	// supportsFlush is true if the modulewrapper indicated that it wants to receive flush commands.
46	supportsFlush bool
47	// pendingReads is a queue of expected responses. `readerRoutine` reads each response and calls the callback in the matching pendingRead.
48	pendingReads chan pendingRead
49	// readerFinished is a channel that is closed if `readerRoutine` has finished (e.g. because of a read error).
50	readerFinished chan struct{}
51}
52
53// pendingRead represents an expected response from the modulewrapper.
54type pendingRead struct {
55	// barrierCallback is called as soon as this pendingRead is the next in the queue, before any read from the modulewrapper.
56	barrierCallback func()
57
58	// callback is called with the result from the modulewrapper. If this is nil then no read is performed.
59	callback func(result [][]byte) error
60	// cmd is the command that requested this read for logging purposes.
61	cmd                string
62	expectedNumResults int
63}
64
65// New returns a new Subprocess middle layer that runs the given binary.
66func New(path string) (*Subprocess, error) {
67	cmd := exec.Command(path)
68	cmd.Stderr = os.Stderr
69	stdin, err := cmd.StdinPipe()
70	if err != nil {
71		return nil, err
72	}
73	stdout, err := cmd.StdoutPipe()
74	if err != nil {
75		return nil, err
76	}
77
78	if err := cmd.Start(); err != nil {
79		return nil, err
80	}
81
82	return NewWithIO(cmd, stdin, stdout), nil
83}
84
85// maxPending is the maximum number of requests that can be in the pipeline.
86const maxPending = 4096
87
88// NewWithIO returns a new Subprocess middle layer with the given ReadCloser and
89// WriteCloser. The returned Subprocess will call Wait on the Cmd when closed.
90func NewWithIO(cmd *exec.Cmd, in io.WriteCloser, out io.ReadCloser) *Subprocess {
91	m := &Subprocess{
92		cmd:            cmd,
93		stdin:          in,
94		stdout:         out,
95		pendingReads:   make(chan pendingRead, maxPending),
96		readerFinished: make(chan struct{}),
97	}
98
99	m.primitives = map[string]primitive{
100		"SHA-1":             &hashPrimitive{"SHA-1", 20},
101		"SHA2-224":          &hashPrimitive{"SHA2-224", 28},
102		"SHA2-256":          &hashPrimitive{"SHA2-256", 32},
103		"SHA2-384":          &hashPrimitive{"SHA2-384", 48},
104		"SHA2-512":          &hashPrimitive{"SHA2-512", 64},
105		"SHA2-512/224":      &hashPrimitive{"SHA2-512/224", 28},
106		"SHA2-512/256":      &hashPrimitive{"SHA2-512/256", 32},
107		"SHA3-224":          &hashPrimitive{"SHA3-224", 28},
108		"SHA3-256":          &hashPrimitive{"SHA3-256", 32},
109		"SHA3-384":          &hashPrimitive{"SHA3-384", 48},
110		"SHA3-512":          &hashPrimitive{"SHA3-512", 64},
111		"SHAKE-128":         &shake{"SHAKE-128", 16},
112		"SHAKE-256":         &shake{"SHAKE-256", 32},
113		"ACVP-AES-ECB":      &blockCipher{"AES", 16, 2, true, false, iterateAES},
114		"ACVP-AES-CBC":      &blockCipher{"AES-CBC", 16, 2, true, true, iterateAESCBC},
115		"ACVP-AES-CBC-CS3":  &blockCipher{"AES-CBC-CS3", 16, 1, false, true, iterateAESCBC},
116		"ACVP-AES-CTR":      &blockCipher{"AES-CTR", 16, 1, false, true, nil},
117		"ACVP-TDES-ECB":     &blockCipher{"3DES-ECB", 8, 3, true, false, iterate3DES},
118		"ACVP-TDES-CBC":     &blockCipher{"3DES-CBC", 8, 3, true, true, iterate3DESCBC},
119		"ACVP-AES-XTS":      &xts{},
120		"ACVP-AES-GCM":      &aead{"AES-GCM", false},
121		"ACVP-AES-GMAC":     &aead{"AES-GCM", false},
122		"ACVP-AES-CCM":      &aead{"AES-CCM", true},
123		"ACVP-AES-KW":       &aead{"AES-KW", false},
124		"ACVP-AES-KWP":      &aead{"AES-KWP", false},
125		"HMAC-SHA-1":        &hmacPrimitive{"HMAC-SHA-1", 20},
126		"HMAC-SHA2-224":     &hmacPrimitive{"HMAC-SHA2-224", 28},
127		"HMAC-SHA2-256":     &hmacPrimitive{"HMAC-SHA2-256", 32},
128		"HMAC-SHA2-384":     &hmacPrimitive{"HMAC-SHA2-384", 48},
129		"HMAC-SHA2-512":     &hmacPrimitive{"HMAC-SHA2-512", 64},
130		"HMAC-SHA2-512/224": &hmacPrimitive{"HMAC-SHA2-512/224", 28},
131		"HMAC-SHA2-512/256": &hmacPrimitive{"HMAC-SHA2-512/256", 32},
132		"HMAC-SHA3-224":     &hmacPrimitive{"HMAC-SHA3-224", 28},
133		"HMAC-SHA3-256":     &hmacPrimitive{"HMAC-SHA3-256", 32},
134		"HMAC-SHA3-384":     &hmacPrimitive{"HMAC-SHA3-384", 48},
135		"HMAC-SHA3-512":     &hmacPrimitive{"HMAC-SHA3-512", 64},
136		"ctrDRBG":           &drbg{"ctrDRBG", map[string]bool{"AES-128": true, "AES-192": true, "AES-256": true}},
137		"hmacDRBG":          &drbg{"hmacDRBG", map[string]bool{"SHA-1": true, "SHA2-224": true, "SHA2-256": true, "SHA2-384": true, "SHA2-512": true, "SHA2-512/224": true, "SHA2-512/256": true, "SHA3-224": true, "SHA3-256": true, "SHA3-384": true, "SHA3-512": true}},
138		"KDF":               &kdfPrimitive{},
139		"KDA":               &hkdf{},
140		"TLS-v1.2":          &tlsKDF{},
141		"TLS-v1.3":          &tls13{},
142		"CMAC-AES":          &keyedMACPrimitive{"CMAC-AES"},
143		"RSA":               &rsa{},
144		"KAS-ECC-SSC":       &kas{},
145		"KAS-FFC-SSC":       &kasDH{},
146		"PBKDF":             &pbkdf{},
147		"ML-DSA":            &mldsa{},
148		"ML-KEM":            &mlkem{},
149		"SLH-DSA":           &slhdsa{},
150	}
151	m.primitives["ECDSA"] = &ecdsa{"ECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives}
152	m.primitives["DetECDSA"] = &ecdsa{"DetECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives}
153	m.primitives["EDDSA"] = &eddsa{"EDDSA", map[string]bool{"ED-25519": true}}
154
155	go m.readerRoutine()
156	return m
157}
158
159// Close signals the child process to exit and waits for it to complete.
160func (m *Subprocess) Close() {
161	m.stdout.Close()
162	m.stdin.Close()
163	m.cmd.Wait()
164	close(m.pendingReads)
165	<-m.readerFinished
166}
167
168func (m *Subprocess) flush() error {
169	if !m.supportsFlush {
170		return nil
171	}
172
173	const cmd = "flush"
174	buf := make([]byte, 8, 8+len(cmd))
175	binary.LittleEndian.PutUint32(buf, 1)
176	binary.LittleEndian.PutUint32(buf[4:], uint32(len(cmd)))
177	buf = append(buf, []byte(cmd)...)
178
179	if _, err := m.stdin.Write(buf); err != nil {
180		return err
181	}
182	return nil
183}
184
185func (m *Subprocess) enqueueRead(pending pendingRead) error {
186	select {
187	case <-m.readerFinished:
188		panic("attempted to enqueue request after the reader failed")
189	default:
190	}
191
192	select {
193	case m.pendingReads <- pending:
194		break
195	default:
196		// `pendingReads` is full. Ensure that the modulewrapper will process
197		// some outstanding requests to free up space in the queue.
198		if err := m.flush(); err != nil {
199			return err
200		}
201		m.pendingReads <- pending
202	}
203
204	return nil
205}
206
207// TransactAsync performs a single request--response pair with the subprocess.
208// The callback will run at some future point, in a separate goroutine. All
209// callbacks will, however, be run in the order that TransactAsync was called.
210// Use Flush to wait for all outstanding callbacks.
211func (m *Subprocess) TransactAsync(cmd string, expectedNumResults int, args [][]byte, callback func(result [][]byte) error) {
212	if err := m.enqueueRead(pendingRead{nil, callback, cmd, expectedNumResults}); err != nil {
213		panic(err)
214	}
215
216	argLength := len(cmd)
217	for _, arg := range args {
218		argLength += len(arg)
219	}
220
221	buf := make([]byte, 4*(2+len(args)), 4*(2+len(args))+argLength)
222	binary.LittleEndian.PutUint32(buf, uint32(1+len(args)))
223	binary.LittleEndian.PutUint32(buf[4:], uint32(len(cmd)))
224	for i, arg := range args {
225		binary.LittleEndian.PutUint32(buf[4*(i+2):], uint32(len(arg)))
226	}
227	buf = append(buf, []byte(cmd)...)
228	for _, arg := range args {
229		buf = append(buf, arg...)
230	}
231
232	if _, err := m.stdin.Write(buf); err != nil {
233		panic(err)
234	}
235}
236
237// Flush tells the subprocess to complete all outstanding requests and waits
238// for all outstanding TransactAsync callbacks to complete.
239func (m *Subprocess) Flush() error {
240	if m.supportsFlush {
241		m.flush()
242	}
243
244	done := make(chan struct{})
245	if err := m.enqueueRead(pendingRead{barrierCallback: func() {
246		close(done)
247	}}); err != nil {
248		return err
249	}
250
251	<-done
252	return nil
253}
254
255// Barrier runs callback after all outstanding TransactAsync callbacks have
256// been run.
257func (m *Subprocess) Barrier(callback func()) error {
258	return m.enqueueRead(pendingRead{barrierCallback: callback})
259}
260
261func (m *Subprocess) Transact(cmd string, expectedNumResults int, args ...[]byte) ([][]byte, error) {
262	done := make(chan struct{})
263	var result [][]byte
264	m.TransactAsync(cmd, expectedNumResults, args, func(r [][]byte) error {
265		result = r
266		close(done)
267		return nil
268	})
269
270	if err := m.flush(); err != nil {
271		return nil, err
272	}
273
274	select {
275	case <-done:
276		return result, nil
277	case <-m.readerFinished:
278		panic("was still waiting for a result when the reader finished")
279	}
280}
281
282func (m *Subprocess) readerRoutine() {
283	defer close(m.readerFinished)
284
285	for pendingRead := range m.pendingReads {
286		if pendingRead.barrierCallback != nil {
287			pendingRead.barrierCallback()
288		}
289
290		if pendingRead.callback == nil {
291			continue
292		}
293
294		result, err := m.readResult(pendingRead.cmd, pendingRead.expectedNumResults)
295		if err != nil {
296			panic(fmt.Errorf("failed to read from subprocess: %w", err))
297		}
298
299		if err := pendingRead.callback(result); err != nil {
300			panic(fmt.Errorf("result from subprocess was rejected: %w", err))
301		}
302	}
303}
304
305func (m *Subprocess) readResult(cmd string, expectedNumResults int) ([][]byte, error) {
306	buf := make([]byte, 4)
307
308	if _, err := io.ReadFull(m.stdout, buf); err != nil {
309		return nil, err
310	}
311
312	numResults := binary.LittleEndian.Uint32(buf)
313	if int(numResults) != expectedNumResults {
314		return nil, fmt.Errorf("expected %d results from %q but got %d", expectedNumResults, cmd, numResults)
315	}
316
317	buf = make([]byte, 4*numResults)
318	if _, err := io.ReadFull(m.stdout, buf); err != nil {
319		return nil, err
320	}
321
322	var resultsLength uint64
323	for i := uint32(0); i < numResults; i++ {
324		resultsLength += uint64(binary.LittleEndian.Uint32(buf[4*i:]))
325	}
326
327	if resultsLength > (1 << 30) {
328		return nil, fmt.Errorf("results too large (%d bytes)", resultsLength)
329	}
330
331	results := make([]byte, resultsLength)
332	if _, err := io.ReadFull(m.stdout, results); err != nil {
333		return nil, err
334	}
335
336	ret := make([][]byte, 0, numResults)
337	var offset int
338	for i := uint32(0); i < numResults; i++ {
339		length := binary.LittleEndian.Uint32(buf[4*i:])
340		ret = append(ret, results[offset:offset+int(length)])
341		offset += int(length)
342	}
343
344	return ret, nil
345}
346
347// Config returns a JSON blob that describes the supported primitives. The
348// format of the blob is defined by ACVP. See
349// http://usnistgov.github.io/ACVP/artifacts/draft-fussell-acvp-spec-00.html#rfc.section.11.15.2.1
350func (m *Subprocess) Config() ([]byte, error) {
351	results, err := m.Transact("getConfig", 1)
352	if err != nil {
353		return nil, err
354	}
355	var config []struct {
356		Algorithm string   `json:"algorithm"`
357		Features  []string `json:"features"`
358	}
359	if err := json.Unmarshal(results[0], &config); err != nil {
360		return nil, errors.New("failed to parse config response from wrapper: " + err.Error())
361	}
362	for _, algo := range config {
363		if algo.Algorithm == "acvptool" {
364			for _, feature := range algo.Features {
365				switch feature {
366				case "batch":
367					m.supportsFlush = true
368				}
369			}
370		} else if _, ok := m.primitives[algo.Algorithm]; !ok {
371			return nil, fmt.Errorf("wrapper config advertises support for unknown algorithm %q", algo.Algorithm)
372		}
373	}
374
375	return results[0], nil
376}
377
378// Process runs a set of test vectors and returns the result.
379func (m *Subprocess) Process(algorithm string, vectorSet []byte) (any, error) {
380	prim, ok := m.primitives[algorithm]
381	if !ok {
382		return nil, fmt.Errorf("unknown algorithm %q", algorithm)
383	}
384	ret, err := prim.Process(vectorSet, m)
385	if err != nil {
386		return nil, err
387	}
388	return ret, nil
389}
390
391type primitive interface {
392	Process(vectorSet []byte, t Transactable) (any, error)
393}
394
395func uint32le(n uint32) []byte {
396	var ret [4]byte
397	binary.LittleEndian.PutUint32(ret[:], n)
398	return ret[:]
399}
400