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