ACME HTTP: Allow customizing HTTP client x509.CertPool (#571)
This commit updates `acme/http.go` to allow customizing the `*x509.CertPool` used by the `HTTPClient` by specifying the filepath of a custom CA certificate via the `CA_CERTIFICATE` environment variable. This allows developers to easily trust a non-standard CA when interacting with an ACME test server (e.g. Pebble): ``` CA_CERTIFICATE=~/go/src/github.com/letsencrypt/pebble/test/certs/pebble.minica.pem \ lego \ --server https://localhost:14000/dir \ --email foo@bar.com \ -d example.com \ run ```
This commit is contained in:
parent
5005315fff
commit
8f9e90b2a0
2 changed files with 164 additions and 15 deletions
69
acme/http.go
69
acme/http.go
|
@ -1,33 +1,44 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
|
||||
var UserAgent string
|
||||
var (
|
||||
// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
|
||||
UserAgent string
|
||||
|
||||
// HTTPClient is an HTTP client with a reasonable timeout value.
|
||||
var HTTPClient = http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 15 * time.Second,
|
||||
ResponseHeaderTimeout: 15 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
// HTTPClient is an HTTP client with a reasonable timeout value and
|
||||
// potentially a custom *x509.CertPool based on the caCertificatesEnvVar
|
||||
// environment variable (see the `initCertPool` function)
|
||||
HTTPClient = http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 15 * time.Second,
|
||||
ResponseHeaderTimeout: 15 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: initCertPool(),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultGoUserAgent is the Go HTTP package user agent string. Too
|
||||
|
@ -36,8 +47,36 @@ const (
|
|||
|
||||
// ourUserAgent is the User-Agent of this underlying library package.
|
||||
ourUserAgent = "xenolf-acme"
|
||||
|
||||
// caCertificatesEnvVar is the environment variable name that can be used to
|
||||
// specify the path to PEM encoded CA Certificates that can be used to
|
||||
// authenticate an ACME server with a HTTPS certificate not issued by a CA in
|
||||
// the system-wide trusted root list.
|
||||
caCertificatesEnvVar = "LEGO_CA_CERTIFICATES"
|
||||
)
|
||||
|
||||
// initCertPool creates a *x509.CertPool populated with the PEM certificates
|
||||
// found in the filepath specified in the caCertificatesEnvVar OS environment
|
||||
// variable. If the caCertificatesEnvVar is not set then initCertPool will
|
||||
// return nil. If there is an error creating a *x509.CertPool from the provided
|
||||
// caCertificatesEnvVar value then initCertPool will panic.
|
||||
func initCertPool() *x509.CertPool {
|
||||
if customCACertsPath := os.Getenv(caCertificatesEnvVar); customCACertsPath != "" {
|
||||
customCAs, err := ioutil.ReadFile(customCACertsPath)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error reading %s=%q: %v",
|
||||
caCertificatesEnvVar, customCACertsPath, err))
|
||||
}
|
||||
certPool := x509.NewCertPool()
|
||||
if ok := certPool.AppendCertsFromPEM(customCAs); !ok {
|
||||
panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v",
|
||||
caCertificatesEnvVar, customCACertsPath, err))
|
||||
}
|
||||
return certPool
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// httpHead performs a HEAD request with a proper User-Agent string.
|
||||
// The response body (resp.Body) is already closed when this function returns.
|
||||
func httpHead(url string) (resp *http.Response, err error) {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
@ -98,3 +100,111 @@ func TestUserAgent(t *testing.T) {
|
|||
t.Errorf("Expected custom UA to contain %s, got '%s'", UserAgent, ua)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitCertPool tests the http.go initCertPool function for customizing the
|
||||
// HTTP Client *x509.CertPool with an environment variable.
|
||||
func TestInitCertPool(t *testing.T) {
|
||||
// writeTemp creates a temp file with the given contents & prefix and returns
|
||||
// the file path. If an error occurs, t.Fatalf is called to end the test run.
|
||||
writeTemp := func(t *testing.T, contents, prefix string) string {
|
||||
t.Helper()
|
||||
tmpFile, err := ioutil.TempFile("", prefix)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempfile: %v", err)
|
||||
}
|
||||
err = ioutil.WriteFile(tmpFile.Name(), []byte(contents), 0700)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to write tempfile contents: %v", err)
|
||||
}
|
||||
return tmpFile.Name()
|
||||
}
|
||||
|
||||
invalidFileContents := "not a certificate"
|
||||
invalidFile := writeTemp(t, invalidFileContents, "invalid.pem")
|
||||
|
||||
// validFileContents is lifted from Pebble[0]. Generate your own CA cert with
|
||||
// MiniCA[1].
|
||||
// [0]: https://github.com/letsencrypt/pebble/blob/de6fa233ea1f283eeb9751d42c8e1ae72718c44e/test/certs/pebble.minica.pem
|
||||
// [1]: https://github.com/jsha/minica
|
||||
validFileContents := `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
|
||||
AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx
|
||||
MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ
|
||||
alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn
|
||||
Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu
|
||||
9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0
|
||||
toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3
|
||||
Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB
|
||||
AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
|
||||
BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v
|
||||
d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF
|
||||
WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll
|
||||
xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix
|
||||
Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82
|
||||
2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF
|
||||
p9BI7gVKtWSZYegicA==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
validFile := writeTemp(t, validFileContents, "valid.pem")
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
EnvVar string
|
||||
ExpectPanic bool
|
||||
ExpectNil bool
|
||||
}{
|
||||
// Setting the env var to a file that doesn't exist should panic
|
||||
{
|
||||
Name: "Env var with missing file",
|
||||
EnvVar: "not.a.real.file.pem",
|
||||
ExpectPanic: true,
|
||||
},
|
||||
// Setting the env var to a file that contains invalid content should panic
|
||||
{
|
||||
Name: "Env var with invalid content",
|
||||
EnvVar: invalidFile,
|
||||
ExpectPanic: true,
|
||||
},
|
||||
// Setting the env var to the empty string should not panic and should
|
||||
// return nil
|
||||
{
|
||||
Name: "No env var",
|
||||
EnvVar: "",
|
||||
ExpectPanic: false,
|
||||
ExpectNil: true,
|
||||
},
|
||||
// Setting the env var to a file that contains valid content should not
|
||||
// panic and should not return nil
|
||||
{
|
||||
Name: "Env var with valid content",
|
||||
EnvVar: validFile,
|
||||
ExpectPanic: false,
|
||||
ExpectNil: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
os.Setenv(caCertificatesEnvVar, tc.EnvVar)
|
||||
defer os.Setenv(caCertificatesEnvVar, "")
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r == nil && tc.ExpectPanic {
|
||||
t.Errorf("expected initCertPool() to panic, it did not")
|
||||
} else if r != nil && !tc.ExpectPanic {
|
||||
t.Errorf("expected initCertPool() to not panic, but it did")
|
||||
}
|
||||
}()
|
||||
|
||||
result := initCertPool()
|
||||
|
||||
if result == nil && !tc.ExpectNil {
|
||||
t.Errorf("initCertPool() returned nil, expected non-nil")
|
||||
} else if result != nil && tc.ExpectNil {
|
||||
t.Errorf("initCertPool() returned non-nil, expected nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue