1// These functions are designed for use in testing other parts of the code.
2
3package testsuite
4
5import (
6	"bufio"
7	"encoding/json"
8	"errors"
9	"fmt"
10	"io/ioutil"
11	"os"
12	"os/exec"
13	"strconv"
14	"strings"
15	"testing"
16	"time"
17
18	"github.com/cloudflare/cfssl/config"
19	"github.com/cloudflare/cfssl/csr"
20)
21
22// CFSSLServerData is the data with which a server is initialized. These fields
23// can be left empty if desired. Any empty fields passed in to StartServer will
24// lead to the server being initialized with the default values defined by the
25// 'cfssl serve' command.
26type CFSSLServerData struct {
27	CA        []byte
28	CABundle  []byte
29	CAKey     []byte
30	IntBundle []byte
31}
32
33// CFSSLServer is the type returned by StartCFSSLServer. It serves as a handle
34// to a running CFSSL server.
35type CFSSLServer struct {
36	process   *os.Process
37	tempFiles []string
38}
39
40// StartCFSSLServer creates a local server listening on the given address and
41// port number. Both the address and port number are assumed to be valid.
42func StartCFSSLServer(address string, portNumber int, serverData CFSSLServerData) (*CFSSLServer, error) {
43	// This value is explained below.
44	startupTime := time.Second
45
46	// We return this when an error occurs.
47	nilServer := &CFSSLServer{nil, nil}
48
49	args := []string{"serve", "-address", address, "-port", strconv.Itoa(portNumber)}
50	var tempCAFile, tempCABundleFile, tempCAKeyFile, tempIntBundleFile string
51	var err error
52	var tempFiles []string
53	if len(serverData.CA) > 0 {
54		tempCAFile, err = createTempFile(serverData.CA)
55		tempFiles = append(tempFiles, tempCAFile)
56		args = append(args, "-ca")
57		args = append(args, tempCAFile)
58	}
59	if len(serverData.CABundle) > 0 {
60		tempCABundleFile, err = createTempFile(serverData.CABundle)
61		tempFiles = append(tempFiles, tempCABundleFile)
62		args = append(args, "-ca-bundle")
63		args = append(args, tempCABundleFile)
64	}
65	if len(serverData.CAKey) > 0 {
66		tempCAKeyFile, err = createTempFile(serverData.CAKey)
67		tempFiles = append(tempFiles, tempCAKeyFile)
68		args = append(args, "-ca-key")
69		args = append(args, tempCAKeyFile)
70	}
71	if len(serverData.IntBundle) > 0 {
72		tempIntBundleFile, err = createTempFile(serverData.IntBundle)
73		tempFiles = append(tempFiles, tempIntBundleFile)
74		args = append(args, "-int-bundle")
75		args = append(args, tempIntBundleFile)
76	}
77	// If an error occurred in the creation of any file, return an error.
78	if err != nil {
79		for _, file := range tempFiles {
80			os.Remove(file)
81		}
82		return nilServer, err
83	}
84
85	command := exec.Command("cfssl", args...)
86
87	stdErrPipe, err := command.StderrPipe()
88	if err != nil {
89		for _, file := range tempFiles {
90			os.Remove(file)
91		}
92		return nilServer, err
93	}
94
95	err = command.Start()
96	if err != nil {
97		for _, file := range tempFiles {
98			os.Remove(file)
99		}
100		return nilServer, err
101	}
102
103	// We check to see if the address given is already in use. There is no way
104	// to do this other than to just wait and see if an error message pops up.
105	// Therefore we wait for startupTime, and if we don't see an error message
106	// by then, we deem the server ready and return.
107
108	errorOccurred := make(chan bool)
109	go func() {
110		scanner := bufio.NewScanner(stdErrPipe)
111		for scanner.Scan() {
112			line := scanner.Text()
113			if strings.Contains(line, "address already in use") {
114				errorOccurred <- true
115			}
116		}
117	}()
118
119	select {
120	case <-errorOccurred:
121		for _, file := range tempFiles {
122			os.Remove(file)
123		}
124		return nilServer, errors.New(
125			"Error occurred on server: address " + address + ":" +
126				strconv.Itoa(portNumber) + " already in use.")
127	case <-time.After(startupTime):
128		return &CFSSLServer{command.Process, tempFiles}, nil
129	}
130}
131
132// Kill a running CFSSL server.
133func (server *CFSSLServer) Kill() error {
134	for _, file := range server.tempFiles {
135		os.Remove(file)
136	}
137	return server.process.Kill()
138}
139
140// CreateCertificateChain creates a chain of certificates from a slice of
141// requests. The first request is the root certificate and the last is the
142// leaf. The chain is returned as a slice of PEM-encoded bytes.
143func CreateCertificateChain(requests []csr.CertificateRequest) (certChain []byte, key []byte, err error) {
144	// Create the root certificate using the first request. This will be
145	// self-signed.
146	certChain = make([]byte, 0)
147	rootCert, prevKey, err := CreateSelfSignedCert(requests[0])
148	if err != nil {
149		return nil, nil, err
150	}
151	certChain = append(certChain, rootCert...)
152
153	// For each of the next requests, create a certificate signed by the
154	// previous certificate.
155	prevCert := rootCert
156	for _, request := range requests[1:] {
157		cert, key, err := SignCertificate(request, prevCert, prevKey)
158		if err != nil {
159			return nil, nil, err
160		}
161		certChain = append(certChain, byte('\n'))
162		certChain = append(certChain, cert...)
163		prevCert = cert
164		prevKey = key
165	}
166
167	return certChain, key, nil
168}
169
170// CreateSelfSignedCert creates a self-signed certificate from a certificate
171// request. This function just calls the CLI "gencert" command.
172func CreateSelfSignedCert(request csr.CertificateRequest) (encodedCert, encodedKey []byte, err error) {
173	// Marshall the request into JSON format and write it to a temporary file.
174	jsonBytes, err := json.Marshal(request)
175	if err != nil {
176		return nil, nil, err
177	}
178	tempFile, err := createTempFile(jsonBytes)
179	if err != nil {
180		os.Remove(tempFile)
181		return nil, nil, err
182	}
183
184	// Create the certificate with the CLI tools.
185	command := exec.Command("cfssl", "gencert", "-initca", tempFile)
186	CLIOutput, err := command.CombinedOutput()
187	if err != nil {
188		os.Remove(tempFile)
189		return nil, nil, fmt.Errorf("%v - CLI output: %s", err, string(CLIOutput))
190	}
191	err = checkCLIOutput(CLIOutput)
192	if err != nil {
193		os.Remove(tempFile)
194		return nil, nil, err
195	}
196
197	encodedCert, err = cleanCLIOutput(CLIOutput, "cert")
198	if err != nil {
199		os.Remove(tempFile)
200		return nil, nil, err
201	}
202	encodedKey, err = cleanCLIOutput(CLIOutput, "key")
203	if err != nil {
204		os.Remove(tempFile)
205		return nil, nil, err
206	}
207
208	os.Remove(tempFile)
209
210	return encodedCert, encodedKey, nil
211}
212
213// SignCertificate uses a certificate (input as signerCert) to create a signed
214// certificate for the input request.
215func SignCertificate(request csr.CertificateRequest, signerCert, signerKey []byte) (encodedCert, encodedKey []byte, err error) {
216	// Marshall the request into JSON format and write it to a temporary file.
217	jsonBytes, err := json.Marshal(request)
218	if err != nil {
219		return nil, nil, err
220	}
221	tempJSONFile, err := createTempFile(jsonBytes)
222	if err != nil {
223		os.Remove(tempJSONFile)
224		return nil, nil, err
225	}
226
227	// Create a CSR file with the CLI tools.
228	command := exec.Command("cfssl", "genkey", tempJSONFile)
229	CLIOutput, err := command.CombinedOutput()
230	if err != nil {
231		os.Remove(tempJSONFile)
232		return nil, nil, fmt.Errorf("%v - CLI output: %s", err, string(CLIOutput))
233	}
234	err = checkCLIOutput(CLIOutput)
235	if err != nil {
236		os.Remove(tempJSONFile)
237		return nil, nil, err
238	}
239	encodedCSR, err := cleanCLIOutput(CLIOutput, "csr")
240	if err != nil {
241		os.Remove(tempJSONFile)
242		return nil, nil, err
243	}
244	encodedCSRKey, err := cleanCLIOutput(CLIOutput, "key")
245	if err != nil {
246		os.Remove(tempJSONFile)
247		return nil, nil, err
248	}
249
250	// Now we write this encoded CSR and its key to file.
251	tempCSRFile, err := createTempFile(encodedCSR)
252	if err != nil {
253		os.Remove(tempJSONFile)
254		os.Remove(tempCSRFile)
255		return nil, nil, err
256	}
257
258	// We also need to write the signer's certficate and key to temporary files.
259	tempSignerCertFile, err := createTempFile(signerCert)
260	if err != nil {
261		os.Remove(tempJSONFile)
262		os.Remove(tempCSRFile)
263		os.Remove(tempSignerCertFile)
264		return nil, nil, err
265	}
266	tempSignerKeyFile, err := createTempFile(signerKey)
267	if err != nil {
268		os.Remove(tempJSONFile)
269		os.Remove(tempCSRFile)
270		os.Remove(tempSignerCertFile)
271		os.Remove(tempSignerKeyFile)
272		return nil, nil, err
273	}
274
275	// Now we use the signer's certificate and key file along with the CSR file
276	// to sign a certificate for the input request. We use the CLI tools to do
277	// this.
278	command = exec.Command(
279		"cfssl",
280		"sign",
281		"-ca", tempSignerCertFile,
282		"-ca-key", tempSignerKeyFile,
283		"-hostname", request.CN,
284		tempCSRFile,
285	)
286	CLIOutput, err = command.CombinedOutput()
287	if err != nil {
288		return nil, nil, fmt.Errorf("%v - CLI output: %s", err, string(CLIOutput))
289	}
290
291	err = checkCLIOutput(CLIOutput)
292	if err != nil {
293		return nil, nil, fmt.Errorf("%v - CLI output: %s", err, string(CLIOutput))
294	}
295
296	encodedCert, err = cleanCLIOutput(CLIOutput, "cert")
297	if err != nil {
298		return nil, nil, err
299	}
300
301	// Clean up.
302	os.Remove(tempJSONFile)
303	os.Remove(tempCSRFile)
304	os.Remove(tempSignerCertFile)
305	os.Remove(tempSignerKeyFile)
306
307	return encodedCert, encodedCSRKey, nil
308}
309
310// Creates a temporary file with the given data. Returns the file name.
311func createTempFile(data []byte) (fileName string, err error) {
312	// Avoid overwriting a file in the currect directory by choosing an unused
313	// file name.
314	baseName := "temp"
315	tempFileName := baseName
316	tryIndex := 0
317	for {
318		if _, err := os.Stat(tempFileName); err == nil {
319			tempFileName = baseName + strconv.Itoa(tryIndex)
320			tryIndex++
321		} else {
322			break
323		}
324	}
325
326	readWritePermissions := os.FileMode(0664)
327	err = ioutil.WriteFile(tempFileName, data, readWritePermissions)
328	if err != nil {
329		return "", err
330	}
331
332	return tempFileName, nil
333}
334
335// Checks the CLI Output for failure.
336func checkCLIOutput(CLIOutput []byte) error {
337	outputString := string(CLIOutput)
338	// Proper output will contain the substring "---BEGIN" somewhere
339	failureOccurred := !strings.Contains(outputString, "---BEGIN")
340	if failureOccurred {
341		return errors.New("Failure occurred during CLI execution: " + outputString)
342	}
343	return nil
344}
345
346// Returns the cleaned up PEM encoding for the item specified (for example,
347// 'cert' or 'key').
348func cleanCLIOutput(CLIOutput []byte, item string) (cleanedOutput []byte, err error) {
349	outputString := string(CLIOutput)
350	// The keyword will be surrounded by quotes.
351	itemString := "\"" + item + "\""
352	// We should only search for the keyword beyond this point.
353	eligibleSearchIndex := strings.Index(outputString, "{")
354	outputString = outputString[eligibleSearchIndex:]
355	// Make sure the item is present in the output.
356	if strings.Index(outputString, itemString) == -1 {
357		return nil, errors.New("Item " + item + " not found in CLI Output")
358	}
359	// We add 2 for the [:"] that follows the item
360	startIndex := strings.Index(outputString, itemString) + len(itemString) + 2
361	outputString = outputString[startIndex:]
362	endIndex := strings.Index(outputString, "\\n\"")
363	outputString = outputString[:endIndex]
364	outputString = strings.Replace(outputString, "\\n", "\n", -1)
365
366	return []byte(outputString), nil
367}
368
369// NewConfig returns a config object from the data passed.
370func NewConfig(t *testing.T, configBytes []byte) *config.Config {
371	conf, err := config.LoadConfig([]byte(configBytes))
372	if err != nil {
373		t.Fatal("config loading error:", err)
374	}
375	if !conf.Valid() {
376		t.Fatal("config is not valid")
377	}
378	return conf
379}
380
381// CSRTest holds information about CSR test files.
382type CSRTest struct {
383	File    string
384	KeyAlgo string
385	KeyLen  int
386	// Error checking function
387	ErrorCallback func(*testing.T, error)
388}
389
390// CSRTests define a set of CSR files for testing.
391var CSRTests = []CSRTest{
392	{
393		File:          "../../signer/local/testdata/rsa2048.csr",
394		KeyAlgo:       "rsa",
395		KeyLen:        2048,
396		ErrorCallback: nil,
397	},
398	{
399		File:          "../../signer/local/testdata/rsa3072.csr",
400		KeyAlgo:       "rsa",
401		KeyLen:        3072,
402		ErrorCallback: nil,
403	},
404	{
405		File:          "../../signer/local/testdata/rsa4096.csr",
406		KeyAlgo:       "rsa",
407		KeyLen:        4096,
408		ErrorCallback: nil,
409	},
410	{
411		File:          "../../signer/local/testdata/ecdsa256.csr",
412		KeyAlgo:       "ecdsa",
413		KeyLen:        256,
414		ErrorCallback: nil,
415	},
416	{
417		File:          "../../signer/local/testdata/ecdsa384.csr",
418		KeyAlgo:       "ecdsa",
419		KeyLen:        384,
420		ErrorCallback: nil,
421	},
422	{
423		File:          "../../signer/local/testdata/ecdsa521.csr",
424		KeyAlgo:       "ecdsa",
425		KeyLen:        521,
426		ErrorCallback: nil,
427	},
428}
429