boulder/cmd/boulder-wfe2/main_test.go

264 lines
9.0 KiB
Go

package main
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"strings"
"testing"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/test"
)
func TestLoadChain_Valid(t *testing.T) {
issuer, chainPEM, err := loadChain([]string{
"../../test/test-ca-cross.pem",
"../../test/test-root2.pem",
})
test.AssertNotError(t, err, "Should load valid chain")
expectedIssuer, err := core.LoadCert("../../test/test-ca-cross.pem")
test.AssertNotError(t, err, "Failed to load test issuer")
chainIssuerPEM, rest := pem.Decode(chainPEM)
test.AssertNotNil(t, chainIssuerPEM, "Failed to decode chain PEM")
parsedIssuer, err := x509.ParseCertificate(chainIssuerPEM.Bytes)
test.AssertNotError(t, err, "Failed to parse chain PEM")
// The three versions of the intermediate (the one loaded by us, the one
// returned by loadChain, and the one parsed from the chain) should be equal.
test.AssertByteEquals(t, issuer.Raw, expectedIssuer.Raw)
test.AssertByteEquals(t, parsedIssuer.Raw, expectedIssuer.Raw)
// The chain should contain nothing else.
rootIssuerPEM, _ := pem.Decode(rest)
if rootIssuerPEM != nil {
t.Error("Expected chain PEM to contain one cert and nothing else")
}
}
func TestLoadChain_TooShort(t *testing.T) {
_, _, err := loadChain([]string{"/path/to/one/cert.pem"})
test.AssertError(t, err, "Should reject too-short chain")
}
func TestLoadChain_Unloadable(t *testing.T) {
_, _, err := loadChain([]string{
"does-not-exist.pem",
"../../test/test-root2.pem",
})
test.AssertError(t, err, "Should reject unloadable chain")
_, _, err = loadChain([]string{
"../../test/test-ca-cross.pem",
"does-not-exist.pem",
})
test.AssertError(t, err, "Should reject unloadable chain")
invalidPEMFile, _ := ioutil.TempFile("", "invalid.pem")
err = ioutil.WriteFile(invalidPEMFile.Name(), []byte(""), 0640)
test.AssertNotError(t, err, "Error writing invalid PEM tmp file")
_, _, err = loadChain([]string{
invalidPEMFile.Name(),
"../../test/test-root2.pem",
})
test.AssertError(t, err, "Should reject unloadable chain")
}
func TestLoadChain_InvalidSig(t *testing.T) {
_, _, err := loadChain([]string{
"../../test/test-root2.pem",
"../../test/test-ca-cross.pem",
})
test.AssertError(t, err, "Should reject invalid signature")
}
func TestLoadChain_NoRoot(t *testing.T) {
// TODO(#5251): Implement this when we have a hierarchy which includes two
// CA certs, neither of which is a root.
}
func TestLoadCertificateChains(t *testing.T) {
// Read some cert bytes to use for expected chain content
certBytesA, err := ioutil.ReadFile("../../test/test-ca.pem")
test.AssertNotError(t, err, "Error reading../../test/test-ca.pem")
certBytesB, err := ioutil.ReadFile("../../test/test-ca2.pem")
test.AssertNotError(t, err, "Error reading../../test/test-ca2.pem")
// Make a .pem file with invalid contents
invalidPEMFile, _ := ioutil.TempFile("", "invalid.pem")
err = ioutil.WriteFile(invalidPEMFile.Name(), []byte(""), 0640)
test.AssertNotError(t, err, "Error writing invalid PEM tmp file")
// Make a .pem file with a valid cert but also some leftover bytes
leftoverPEMFile, _ := ioutil.TempFile("", "leftovers.pem")
leftovers := "vegan curry, cold rice, soy milk"
leftoverBytes := append(certBytesA, []byte(leftovers)...)
err = ioutil.WriteFile(leftoverPEMFile.Name(), leftoverBytes, 0640)
test.AssertNotError(t, err, "Error writing leftover PEM tmp file")
// Make a .pem file that is test-ca2.pem but with Windows/DOS CRLF line
// endings
crlfPEM, _ := ioutil.TempFile("", "crlf.pem")
crlfPEMBytes := []byte(strings.Replace(string(certBytesB), "\n", "\r\n", -1))
err = ioutil.WriteFile(crlfPEM.Name(), crlfPEMBytes, 0640)
test.AssertNotError(t, err, "ioutil.WriteFile failed")
// Make a .pem file that is test-ca.pem but with no trailing newline
abruptPEM, _ := ioutil.TempFile("", "abrupt.pem")
abruptPEMBytes := certBytesA[:len(certBytesA)-1]
err = ioutil.WriteFile(abruptPEM.Name(), abruptPEMBytes, 0640)
test.AssertNotError(t, err, "ioutil.WriteFile failed")
testCases := []struct {
Name string
Input map[string][]string
ExpectedMap map[issuance.IssuerNameID][]byte
ExpectedError error
AllowEmptyChain bool
}{
{
Name: "No input",
Input: nil,
},
{
Name: "AIA Issuer without chain files",
Input: map[string][]string{
"http://break.the.chain.com": {},
},
ExpectedError: fmt.Errorf(
"CertificateChain entry for AIA issuer url \"http://break.the.chain.com\" " +
"has no chain file names configured"),
},
{
Name: "Missing chain file",
Input: map[string][]string{
"http://where.is.my.mind": {"/tmp/does.not.exist.pem"},
},
ExpectedError: fmt.Errorf("CertificateChain entry for AIA issuer url \"http://where.is.my.mind\" " +
"has an invalid chain file: \"/tmp/does.not.exist.pem\" - error reading " +
"contents: open /tmp/does.not.exist.pem: no such file or directory"),
},
{
Name: "PEM chain file with Windows CRLF line endings",
Input: map[string][]string{
"http://windows.sad.zone": {crlfPEM.Name()},
},
ExpectedError: fmt.Errorf("CertificateChain entry for AIA issuer url \"http://windows.sad.zone\" "+
"has an invalid chain file: %q - contents had CRLF line endings", crlfPEM.Name()),
},
{
Name: "Invalid PEM chain file",
Input: map[string][]string{
"http://ok.go": {invalidPEMFile.Name()},
},
ExpectedError: fmt.Errorf(
"CertificateChain entry for AIA issuer url \"http://ok.go\" has an "+
"invalid chain file: %q - contents did not decode as PEM",
invalidPEMFile.Name()),
},
{
Name: "PEM chain file that isn't a cert",
Input: map[string][]string{
"http://not-a-cert.com": {"../../test/test-root.key"},
},
ExpectedError: fmt.Errorf(
"CertificateChain entry for AIA issuer url \"http://not-a-cert.com\" has " +
"an invalid chain file: \"../../test/test-root.key\" - PEM block type " +
"incorrect, found \"PRIVATE KEY\", expected \"CERTIFICATE\""),
},
{
Name: "PEM chain file with leftover bytes",
Input: map[string][]string{
"http://tasty.leftovers.com": {leftoverPEMFile.Name()},
},
ExpectedError: fmt.Errorf(
"CertificateChain entry for AIA issuer url \"http://tasty.leftovers.com\" "+
"has an invalid chain file: %q - PEM contents had unused remainder input "+
"(%d bytes)",
leftoverPEMFile.Name(),
len([]byte(leftovers)),
),
},
{
Name: "One PEM file chain",
Input: map[string][]string{
"http://single-cert-chain.com": {"../../test/test-ca.pem"},
},
ExpectedMap: map[issuance.IssuerNameID][]byte{
issuance.IssuerNameID(37287262753088952): []byte(fmt.Sprintf("\n%s", string(certBytesA))),
},
},
{
Name: "Two PEM file chain",
Input: map[string][]string{
"http://two-cert-chain.com": {"../../test/test-ca.pem", "../../test/test-ca2.pem"},
},
ExpectedMap: map[issuance.IssuerNameID][]byte{
issuance.IssuerNameID(37287262753088952): []byte(fmt.Sprintf("\n%s\n%s", string(certBytesA), string(certBytesB))),
},
},
{
Name: "One PEM file chain, no trailing newline",
Input: map[string][]string{
"http://single-cert-chain.nonewline.com": {abruptPEM.Name()},
},
ExpectedMap: map[issuance.IssuerNameID][]byte{
// NOTE(@cpu): There should be a trailing \n added by the WFE that we
// expect in the format specifier below.
issuance.IssuerNameID(37287262753088952): []byte(fmt.Sprintf("\n%s\n", string(abruptPEMBytes))),
},
},
{
Name: "Two PEM file chain, don't require at least one chain",
AllowEmptyChain: true,
Input: map[string][]string{
"http://two-cert-chain.com": {"../../test/test-ca.pem", "../../test/test-ca2.pem"},
},
ExpectedMap: map[issuance.IssuerNameID][]byte{
issuance.IssuerNameID(37287262753088952): []byte(fmt.Sprintf("\n%s\n%s", string(certBytesA), string(certBytesB))),
},
},
{
Name: "Empty chain, don't require at least one chain",
AllowEmptyChain: true,
Input: map[string][]string{
"http://two-cert-chain.com": {},
},
ExpectedMap: map[issuance.IssuerNameID][]byte{},
},
{
Name: "Empty chain",
Input: map[string][]string{
"http://two-cert-chain.com": {},
},
ExpectedError: fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has no chain "+
"file names configured",
"http://two-cert-chain.com"),
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
resultMap, issuers, err := loadCertificateChains(tc.Input, !tc.AllowEmptyChain)
if tc.ExpectedError == nil && err != nil {
t.Errorf("Expected nil error, got %#v\n", err)
} else if tc.ExpectedError != nil && err == nil {
t.Errorf("Expected non-nil error, got nil err")
} else if tc.ExpectedError != nil {
test.AssertEquals(t, err.Error(), tc.ExpectedError.Error())
}
test.AssertEquals(t, len(resultMap), len(tc.ExpectedMap))
test.AssertEquals(t, len(issuers), len(tc.ExpectedMap))
for nameid, chain := range resultMap {
test.Assert(t, bytes.Equal(chain, tc.ExpectedMap[nameid]), "Chain bytes did not match expected")
}
})
}
}