264 lines
9.0 KiB
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")
|
|
}
|
|
})
|
|
}
|
|
}
|