Skip to content

Commit

Permalink
Move core loadChain functionality from boulder-wfe to issuance (#5271)
Browse files Browse the repository at this point in the history
loadChain is an unexported utility function recently added to
boulder-wfe to support the loading and validating of PEM files that
represent a certificate chain

This change moves the core loadChain functionality out of boulder-wfe to
a new exported LoadChain function in the Issuance package. All
boulder-wfe unit tests have been preserved and most of them have been
pared down and added to the Issuance package as well.

Blocks #1669
Fixes #5270
  • Loading branch information
beautifulentropy committed Feb 5, 2021
1 parent 0b1feda commit 82b200b
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 30 deletions.
37 changes: 7 additions & 30 deletions cmd/boulder-wfe2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"flag"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -250,41 +249,19 @@ func loadCertificateChains(chainConfig map[string][]string, requireAtLeastOneCha
// a root certificate, which the chain will be verified against, but which will
// not be included in the resulting chain.
func loadChain(certFiles []string) (*issuance.Certificate, []byte, error) {
if len(certFiles) < 2 {
return nil, nil, errors.New(
"each chain must have at least two certificates: an intermediate and a root")
}

// Pre-load all the certificates to make validation easier.
certs := make([]*x509.Certificate, len(certFiles))
var err error
for i := 0; i < len(certFiles); i++ {
certs[i], err = core.LoadCert(certFiles[i])
if err != nil {
return nil, nil, fmt.Errorf("failed to load certificate: %w", err)
}
certs, err := issuance.LoadChain(certFiles)
if err != nil {
return nil, nil, err
}

// Iterate over all certs except for the last, checking that their signature
// comes from the next cert in the list, and appending their pem to the buf.
// Iterate over all certs appending their pem to the buf.
var buf bytes.Buffer
for i := 0; i < len(certs)-1; i++ {
err = certs[i].CheckSignatureFrom(certs[i+1])
if err != nil {
return nil, nil, fmt.Errorf("failed to verify chain: %w", err)
}

for _, cert := range certs {
buf.Write([]byte("\n"))
buf.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certs[i].Raw}))
}

err = certs[len(certs)-1].CheckSignatureFrom(certs[len(certs)-1])
if err != nil {
return nil, nil, fmt.Errorf(
"final cert in chain must be a self-signed (used only for validation): %w", err)
buf.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))
}

return &issuance.Certificate{Certificate: certs[0]}, buf.Bytes(), nil
return certs[0], buf.Bytes(), nil
}

func setupWFE(c config, logger blog.Logger, stats prometheus.Registerer, clk clock.Clock) (core.RegistrationAuthority, core.StorageAuthority, noncepb.NonceServiceClient, map[string]noncepb.NonceServiceClient) {
Expand Down
42 changes: 42 additions & 0 deletions issuance/issuance.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,3 +626,45 @@ func RequestFromPrecert(precert *x509.Certificate, scts []ct.SignedCertificateTi
SCTList: scts,
}, nil
}

// LoadChain takes a list of filenames containing pem-formatted certificates,
// and returns a chain representing all of those certificates in order. It
// ensures that the resulting chain is valid. The final file is expected to be
// a root certificate, which the chain will be verified against, but which will
// not be included in the resulting chain.
func LoadChain(certFiles []string) ([]*Certificate, error) {
if len(certFiles) < 2 {
return nil, errors.New(
"each chain must have at least two certificates: an intermediate and a root")
}

// Pre-load all the certificates to make validation easier.
certs := make([]*x509.Certificate, len(certFiles))
var err error
for i := 0; i < len(certFiles); i++ {
certs[i], err = core.LoadCert(certFiles[i])
if err != nil {
return nil, fmt.Errorf("failed to load certificate: %w", err)
}
}

// Iterate over all certs except for the last, checking that their signature
// comes from the next cert in the list
chain := make([]*Certificate, len(certFiles)-1)
for i := 0; i < len(certs)-1; i++ {
err = certs[i].CheckSignatureFrom(certs[i+1])
if err != nil {
return nil, fmt.Errorf("failed to verify chain: %w", err)
}
// Add each cert to the chain.
chain[i] = &Certificate{certs[i]}
}

err = certs[len(certs)-1].CheckSignatureFrom(certs[len(certs)-1])
if err != nil {
return nil, fmt.Errorf(
"final cert in chain must be a self-signed (used only for validation): %w", err)
}

return chain, nil
}
54 changes: 54 additions & 0 deletions issuance/issuance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"io/ioutil"
"math/big"
"os"
"testing"
Expand All @@ -19,6 +20,7 @@ import (
ct "github.com/google/certificate-transparency-go"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/lint"
"github.com/letsencrypt/boulder/policyasn1"
"github.com/letsencrypt/boulder/test"
Expand Down Expand Up @@ -693,3 +695,55 @@ func TestIssueBadLint(t *testing.T) {
test.AssertError(t, err, "Issue didn't fail")
test.AssertEquals(t, err.Error(), "tbsCertificate linting failed: failed lints: w_ct_sct_policy_count_unsatisfied")
}

func TestLoadChain_Valid(t *testing.T) {
chain, 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")

chainIssuer := chain[0]
test.AssertNotNil(t, chainIssuer, "Failed to decode chain PEM")

test.AssertByteEquals(t, chainIssuer.Raw, expectedIssuer.Raw)
}

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")
}

0 comments on commit 82b200b

Please sign in to comment.