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