Skip to content

Commit

Permalink
Update root certificate fetching to get the certs from the filesystem…
Browse files Browse the repository at this point in the history
… for boulder/pebble where possible
  • Loading branch information
eggsampler committed Feb 18, 2021
1 parent 95e063e commit 451f505
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 86 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ boulder_setup:
git clone --depth 1 https://github.com/letsencrypt/boulder.git $(BOULDER_PATH) \
|| (cd $(BOULDER_PATH); git checkout -f main && git reset --hard HEAD && git pull -q)
docker-compose -f $(BOULDER_PATH)/docker-compose.yml down
sed -i -e 's/test\/config$$/test\/config-next/' $(BOULDER_PATH)/docker-compose.yml
patch -p1 $(BOULDER_PATH)/docker-compose.yml boulder-docker-compose.diff
rm -rf $(BOULDER_PATH)/temp

# runs an instance of boulder
boulder_start:
Expand Down
51 changes: 28 additions & 23 deletions autocert.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (m *AutoCert) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,

// check if there's an existing cert
m.certLock.RLock()
existingCert := m.getExistingCert(name)
existingCert, _ := m.getExistingCert(name)
m.certLock.RUnlock()
if existingCert != nil {
return existingCert, nil
Expand Down Expand Up @@ -212,18 +212,16 @@ func (m *AutoCert) checkHost(name string) error {
return m.HostCheck(name)
}

func (m *AutoCert) getExistingCert(name string) *tls.Certificate {
func (m *AutoCert) getExistingCert(name string) (*tls.Certificate, error) {
// check for a stored cert
certData := m.getCache("cert", name)
if len(certData) == 0 {
// no cert
return nil
return nil, errors.New("autocert: no existing certificate")
}

privBlock, pubData := pem.Decode(certData)
if len(pubData) == 0 {
// no public key data (cert/issuer), ignore
return nil
return nil, errors.New("autocert: no public key data (cert/issuer)")
}

// decode pub chain
Expand All @@ -239,14 +237,12 @@ func (m *AutoCert) getExistingCert(name string) *tls.Certificate {
pub = append(pub, b.Bytes...)
}
if len(pubData) > 0 {
// leftover data in file - possibly corrupt, ignore
return nil
return nil, errors.New("autocert: leftover data in file - possibly corrupt")
}

certs, err := x509.ParseCertificates(pub)
if err != nil {
// bad certificates, ignore
return nil
return nil, fmt.Errorf("autocert: bad certificate: %v", err)
}

leaf := certs[0]
Expand All @@ -263,31 +259,40 @@ func (m *AutoCert) getExistingCert(name string) *tls.Certificate {
// add a root certificate if present
var roots *x509.CertPool
if m.RootCert != "" {
roots = x509.NewCertPool()
rootBlock, _ := pem.Decode([]byte(m.RootCert))
rootCert, err := x509.ParseCertificate(rootBlock.Bytes)
if err != nil {
return nil
block, rest := pem.Decode([]byte(m.RootCert))
for block != nil {
rootCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.New("autocert: error parsing root certificate")
}
if roots == nil {
roots = x509.NewCertPool()
}
roots.AddCert(rootCert)
block, rest = pem.Decode(rest)
}
roots.AddCert(rootCert)
}

if _, err := leaf.Verify(x509.VerifyOptions{DNSName: name, Intermediates: intermediates, Roots: roots}); err != nil {
// invalid certificates , ignore
return nil
opts := x509.VerifyOptions{
DNSName: name,
Intermediates: intermediates,
Roots: roots,
}

if _, err := leaf.Verify(opts); err != nil {
return nil, fmt.Errorf("autocert: unable to verify: %v", err)
}

privKey, err := x509.ParseECPrivateKey(privBlock.Bytes)
if err != nil {
// invalid private key, ignore
return nil
return nil, errors.New("autocert: invalid private key")
}

return &tls.Certificate{
Certificate: pubDER,
PrivateKey: privKey,
Leaf: leaf,
}
}, nil
}

func (m *AutoCert) issueCert(domainName string) (*tls.Certificate, error) {
Expand Down Expand Up @@ -426,5 +431,5 @@ func (m *AutoCert) issueCert(domainName string) (*tls.Certificate, error) {
}
m.putCache(certPem, "cert", domainName)

return m.getExistingCert(domainName), nil
return m.getExistingCert(domainName)
}
60 changes: 2 additions & 58 deletions autocert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ import (
"bytes"
"crypto/tls"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"mime"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"strings"
Expand Down Expand Up @@ -149,64 +145,12 @@ func TestAutoCert_checkHost(t *testing.T) {

func TestAutoCert_getExistingCert(t *testing.T) {
ac := AutoCert{}
if cert := ac.getExistingCert("fake"); cert != nil {
if cert, _ := ac.getExistingCert("fake"); cert != nil {
t.Fatalf("expected nil cert, got: %+v", cert)
}
}

func fetchRoot() []byte {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}

getBody := func(rootURL string) []byte {
resp, err := httpClient.Get(rootURL)
if err != nil {
return nil
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil
}
mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
panic(err)
}
switch mediaType {
case "application/pem-certificate-chain":
return body
case "application/pkix-cert":
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: body})
}
panic(rootURL + " unsupported content type: " + mediaType)
}

baseURL, err := url.Parse(testClient.Directory().URL)
if err != nil {
panic(err)
}

urls := []string{
fmt.Sprintf("%s://%s:%d/roots/0", baseURL.Scheme, baseURL.Hostname(), 15000),
fmt.Sprintf("%s://%s/acme/issuer-cert", baseURL.Scheme, baseURL.Host),
}

for _, u := range urls {
if root := getBody(u); len(root) > 0 {
return root
}
}

panic("no root certificate")
}

func TestAutoCert_GetCertificate2(t *testing.T) {

root := fetchRoot()

doPost := func(name string, req interface{}) {
Expand Down Expand Up @@ -235,7 +179,7 @@ func TestAutoCert_GetCertificate2(t *testing.T) {
},
}

cert, err := ac.GetCertificate(&tls.ClientHelloInfo{ServerName: "test.com"})
cert, err := ac.GetCertificate(&tls.ClientHelloInfo{ServerName: randString() + ".com"})
if err != nil {
t.Fatalf("error getting certificate: %v", err)
}
Expand Down
21 changes: 21 additions & 0 deletions boulder-docker-compose.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
diff --git a/docker-compose.yml b/docker-compose.yml
index 2c93b96..1b29cf2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,7 +5,7 @@ services:
image: letsencrypt/boulder-tools-go${TRAVIS_GO_VERSION:-1.15.5}:2020-11-20
environment:
- FAKE_DNS=10.77.77.77
- - BOULDER_CONFIG_DIR=test/config
+ - BOULDER_CONFIG_DIR=test/config-next
- GOFLAGS=-mod=vendor
# This is required so Python doesn't throw an error when printing
# non-ASCII to stdout.
@@ -17,6 +17,7 @@ services:
- INT_FILTER
- RACE
volumes:
+ - ./temp:/tmp
- .:/go/src/github.com/letsencrypt/boulder:cached
- ./.gocache:/root/.cache/go-build:cached
networks:
13 changes: 13 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto"
"crypto/hmac"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
Expand Down Expand Up @@ -74,6 +75,18 @@ func WithHTTPClient(httpClient *http.Client) OptionFunc {
}
}

// WithRootCerts sets the httpclient transport to use a given certpool for root certs
func WithRootCerts(pool *x509.CertPool) OptionFunc {
return func(client *Client) error {
client.httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: pool,
},
}
return nil
}
}

// NewAccountOptionFunc function prototype for passing options to NewClient
type NewAccountOptionFunc func(crypto.Signer, *Account, *NewAccountRequest, Client) error

Expand Down
98 changes: 94 additions & 4 deletions utility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
crand "crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
mrand "math/rand"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
)

type clientMeta struct {
Software string
RootCert string
Options []OptionFunc
}

Expand Down Expand Up @@ -53,15 +58,18 @@ func init() {
return
}

roots := fetchRoot()
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(roots)

directories := map[string]clientMeta{
"https://localhost:14000/dir": {
Software: clientPebble,
RootCert: "https://localhost:15000/roots/0",
Options: []OptionFunc{WithInsecureSkipVerify()},
Options: []OptionFunc{WithRootCerts(pool)},
},
"https://localhost:4431/directory": {
Software: clientBoulder,
Options: []OptionFunc{WithInsecureSkipVerify()},
Options: []OptionFunc{WithRootCerts(pool)},
},
"http://localhost:4001/directory": {
Software: clientBoulder,
Expand Down Expand Up @@ -312,3 +320,85 @@ func postChallenge(auth Authorization, chal Challenge) {
panic("post: unsupported challenge type: " + chal.Type)
}
}

func fetchRoot() []byte {
var certPaths []string
var certsPem []string

boulderPath := os.Getenv("BOULDER_PATH")
if boulderPath == "" {
home, _ := os.UserHomeDir()
if home == "" {
return nil
}
boulderPath = filepath.Join(home, "go", "src", "github.com", "letsencrypt", "boulder")
}

certPaths = append(certPaths, filepath.Join(boulderPath, "temp", "root-cert-ecdsa.pem"))
certPaths = append(certPaths, filepath.Join(boulderPath, "temp", "root-cert-rsa.pem"))

pebblePath := os.Getenv("PEBBLE_PATH")
if pebblePath == "" {
home, _ := os.UserHomeDir()
if home == "" {
return nil
}
pebblePath = filepath.Join(home, "go", "src", "github.com", "letsencrypt", "pebble")
}

// these certs are the ones used for the web server, not signing
certPaths = append(certPaths, filepath.Join(pebblePath, "test", "certs", "pebble.minica.pem"))
certPaths = append(certPaths, filepath.Join(pebblePath, "test", "certs", "localhost", "cert.pem"))

for _, v := range certPaths {
bPem, err := ioutil.ReadFile(v)
if err != nil {
log.Printf("error reading: %s", v)
continue
}
certsPem = append(certsPem, strings.TrimSpace(string(bPem)))
}

tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}

i := 0
for {
// these are the signing roots
pebbleRootURL := fmt.Sprintf("https://localhost:15000/roots/%d", i)
i++
resp, err := httpClient.Get(pebbleRootURL)
if err != nil {
break
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
break
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
break
}
mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
panic(err)
}
switch mediaType {
case "application/pem-certificate-chain":
certsPem = append(certsPem, strings.TrimSpace(string(body)))
case "application/pkix-cert":
bPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: body})
certsPem = append(certsPem, strings.TrimSpace(string(bPem)))
default:
panic(pebbleRootURL + " unsupported content type: " + mediaType)
}
}

if len(certsPem) == 0 {
return nil
}

return []byte(strings.Join(certsPem, "\n"))
}

0 comments on commit 451f505

Please sign in to comment.