Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Managers: Ability to call GetCertificate from external certificate sources #163

Merged
merged 12 commits into from
Feb 17, 2022
6 changes: 3 additions & 3 deletions account.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (am *ACMEManager) loadAccount(ca, email string) (acme.Account, error) {
if err != nil {
return acct, err
}
acct.PrivateKey, err = decodePrivateKey(keyBytes)
acct.PrivateKey, err = PEMDecodePrivateKey(keyBytes)
if err != nil {
return acct, fmt.Errorf("could not decode account's private key: %v", err)
}
Expand Down Expand Up @@ -129,7 +129,7 @@ func (am *ACMEManager) lookUpAccount(ctx context.Context, privateKeyPEM []byte)
return acme.Account{}, fmt.Errorf("creating ACME client: %v", err)
}

privateKey, err := decodePrivateKey([]byte(privateKeyPEM))
privateKey, err := PEMDecodePrivateKey([]byte(privateKeyPEM))
if err != nil {
return acme.Account{}, fmt.Errorf("decoding private key: %v", err)
}
Expand Down Expand Up @@ -157,7 +157,7 @@ func (am *ACMEManager) saveAccount(ca string, account acme.Account) error {
if err != nil {
return err
}
keyBytes, err := encodePrivateKey(account.PrivateKey)
keyBytes, err := PEMEncodePrivateKey(account.PrivateKey)
if err != nil {
return err
}
Expand Down
21 changes: 16 additions & 5 deletions certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ type Certificate struct {
issuerKey string
}

// Empty returns true if the certificate struct is not filled out; at
// least the tls.Certificate.Certificate field is expected to be set.
func (cert Certificate) Empty() bool {
return len(cert.Certificate.Certificate) == 0
}

// NeedsRenewal returns true if the certificate is
// expiring soon (according to cfg) or has expired.
func (cert Certificate) NeedsRenewal(cfg *Config) bool {
Expand Down Expand Up @@ -251,11 +257,15 @@ func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error {
// the leaf cert should be the one for the site; we must set
// the tls.Certificate.Leaf field so that TLS handshakes are
// more efficient
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
return err
leaf := cert.Certificate.Leaf
if leaf == nil {
var err error
leaf, err = x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
return err
}
cert.Certificate.Leaf = leaf
}
cert.Certificate.Leaf = leaf

// for convenience, we do want to assemble all the
// subjects on the certificate into one list
Expand Down Expand Up @@ -393,9 +403,10 @@ func SubjectIsInternal(subj string) bool {
// states that IP addresses must match exactly, but this function
// does not attempt to distinguish IP addresses from internal or
// external DNS names that happen to look like IP addresses.
// It uses DNS wildcard matching logic.
// It uses DNS wildcard matching logic and is case-insensitive.
// https://tools.ietf.org/html/rfc2818#section-3.1
func MatchWildcard(subject, wildcard string) bool {
subject, wildcard = strings.ToLower(subject), strings.ToLower(wildcard)
if subject == wildcard {
return true
}
Expand Down
12 changes: 8 additions & 4 deletions certificates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,27 @@ func TestUnexportedGetCertificate(t *testing.T) {
cfg := &Config{certCache: certCache}

// When cache is empty
if _, matched, defaulted := cfg.getCertificate(&tls.ClientHelloInfo{ServerName: "example.com"}); matched || defaulted {
if _, matched, defaulted := cfg.getCertificateFromCache(&tls.ClientHelloInfo{ServerName: "example.com"}); matched || defaulted {
t.Errorf("Got a certificate when cache was empty; matched=%v, defaulted=%v", matched, defaulted)
}

// When cache has one certificate in it
firstCert := Certificate{Names: []string{"example.com"}}
certCache.cache["0xdeadbeef"] = firstCert
certCache.cacheIndex["example.com"] = []string{"0xdeadbeef"}
if cert, matched, defaulted := cfg.getCertificate(&tls.ClientHelloInfo{ServerName: "example.com"}); !matched || defaulted || cert.Names[0] != "example.com" {
if cert, matched, defaulted := cfg.getCertificateFromCache(&tls.ClientHelloInfo{ServerName: "example.com"}); !matched || defaulted || cert.Names[0] != "example.com" {
t.Errorf("Didn't get a cert for 'example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
}

// When retrieving wildcard certificate
certCache.cache["0xb01dface"] = Certificate{Names: []string{"*.example.com"}}
certCache.cacheIndex["*.example.com"] = []string{"0xb01dface"}
if cert, matched, defaulted := cfg.getCertificate(&tls.ClientHelloInfo{ServerName: "sub.example.com"}); !matched || defaulted || cert.Names[0] != "*.example.com" {
if cert, matched, defaulted := cfg.getCertificateFromCache(&tls.ClientHelloInfo{ServerName: "sub.example.com"}); !matched || defaulted || cert.Names[0] != "*.example.com" {
t.Errorf("Didn't get wildcard cert for 'sub.example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
}

// When no certificate matches and SNI is provided, return no certificate (should be TLS alert)
if cert, matched, defaulted := cfg.getCertificate(&tls.ClientHelloInfo{ServerName: "nomatch"}); matched || defaulted {
if cert, matched, defaulted := cfg.getCertificateFromCache(&tls.ClientHelloInfo{ServerName: "nomatch"}); matched || defaulted {
t.Errorf("Expected matched=false, defaulted=false; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert)
}
}
Expand Down Expand Up @@ -190,10 +190,14 @@ func TestMatchWildcard(t *testing.T) {
expect bool
}{
{"hostname", "hostname", true},
{"HOSTNAME", "hostname", true},
{"hostname", "HOSTNAME", true},
{"foo.localhost", "foo.localhost", true},
{"foo.localhost", "bar.localhost", false},
{"foo.localhost", "*.localhost", true},
{"bar.localhost", "*.localhost", true},
{"FOO.LocalHost", "*.localhost", true},
{"Bar.localhost", "*.LOCALHOST", true},
{"foo.bar.localhost", "*.localhost", false},
{".localhost", "*.localhost", false},
{"foo.localhost", "foo.*", false},
Expand Down
12 changes: 12 additions & 0 deletions certmagic.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,18 @@ type Revoker interface {
Revoke(ctx context.Context, cert CertificateResource, reason int) error
}

// CertificateManager is a type that manages certificates (keeps them renewed)
// such that we can get certificates during TLS handshakes to immediately serve
// to clients.
//
// TODO: This is an EXPERIMENTAL API. It is subject to change/removal.
type CertificateManager interface {
// GetCertificate returns the certificate to use to complete the handshake.
// Since this is called during every TLS handshake, it must be very fast and not block.
// Returning (nil, nil) is valid and is simply treated as a no-op.
GetCertificate(context.Context, *tls.ClientHelloInfo) (*tls.Certificate, error)
}

// KeyGenerator can generate a private key.
type KeyGenerator interface {
// GenerateKey generates a private key. The returned
Expand Down
19 changes: 14 additions & 5 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,21 @@ type Config struct {
// Adds the must staple TLS extension to the CSR.
MustStaple bool

// The source for getting new certificates; the
// default Issuer is ACMEManager. If multiple
// Sources for getting new, managed certificates;
// the default Issuer is ACMEManager. If multiple
// issuers are specified, they will be tried in
// turn until one succeeds.
Issuers []Issuer

// Sources for getting new, unmanaged certificates.
// They will be invoked only during TLS handshakes
// before on-demand certificate management occurs,
// for certificates that are not already loaded into
// the in-memory cache.
//
// TODO: EXPERIMENTAL: subject to change and/or removal.
Managers []CertificateManager

// The source of new private keys for certificates;
// the default KeySource is StandardKeyGenerator.
KeySource KeyGenerator
Expand Down Expand Up @@ -499,7 +508,7 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
if err != nil {
return err
}
privKeyPEM, err = encodePrivateKey(privKey)
privKeyPEM, err = PEMEncodePrivateKey(privKey)
if err != nil {
return err
}
Expand Down Expand Up @@ -605,7 +614,7 @@ func (cfg *Config) reusePrivateKey(domain string) (privKey crypto.PrivateKey, pr
}

// we loaded a private key; try decoding it so we can use it
privKey, err = decodePrivateKey(privKeyPEM)
privKey, err = PEMDecodePrivateKey(privKeyPEM)
if err != nil {
return nil, nil, nil, err
}
Expand Down Expand Up @@ -722,7 +731,7 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
zap.Duration("remaining", timeLeft))
}

privateKey, err := decodePrivateKey(certRes.PrivateKeyPEM)
privateKey, err := PEMDecodePrivateKey(certRes.PrivateKeyPEM)
if err != nil {
return err
}
Expand Down
16 changes: 10 additions & 6 deletions crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ import (
"golang.org/x/net/idna"
)

// encodePrivateKey marshals a EC or RSA private key into a PEM-encoded array of bytes.
func encodePrivateKey(key crypto.PrivateKey) ([]byte, error) {
// PEMEncodePrivateKey marshals a private key into a PEM-encoded block.
// The private key must be one of *ecdsa.PrivateKey, *rsa.PrivateKey, or
// *ed25519.PrivateKey.
func PEMEncodePrivateKey(key crypto.PrivateKey) ([]byte, error) {
var pemType string
var keyBytes []byte
switch key := key.(type) {
Expand Down Expand Up @@ -65,11 +67,13 @@ func encodePrivateKey(key crypto.PrivateKey) ([]byte, error) {
return pem.EncodeToMemory(&pemKey), nil
}

// decodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
// PEMDecodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
// Borrowed from Go standard library, to handle various private key and PEM block types.
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238)
func decodePrivateKey(keyPEMBytes []byte) (crypto.Signer, error) {
func PEMDecodePrivateKey(keyPEMBytes []byte) (crypto.Signer, error) {
// Modified from original:
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238

keyBlockDER, _ := pem.Decode(keyPEMBytes)

if keyBlockDER == nil {
Expand Down
10 changes: 5 additions & 5 deletions crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ func TestEncodeDecodeRSAPrivateKey(t *testing.T) {
}

// test save
savedBytes, err := encodePrivateKey(privateKey)
savedBytes, err := PEMEncodePrivateKey(privateKey)
if err != nil {
t.Fatal("error saving private key:", err)
}

// test load
loadedKey, err := decodePrivateKey(savedBytes)
loadedKey, err := PEMDecodePrivateKey(savedBytes)
if err != nil {
t.Error("error loading private key:", err)
}

// test load (should fail)
_, err = decodePrivateKey(savedBytes[2:])
_, err = PEMDecodePrivateKey(savedBytes[2:])
if err == nil {
t.Error("loading private key should have failed")
}
Expand All @@ -63,13 +63,13 @@ func TestSaveAndLoadECCPrivateKey(t *testing.T) {
}

// test save
savedBytes, err := encodePrivateKey(privateKey)
savedBytes, err := PEMEncodePrivateKey(privateKey)
if err != nil {
t.Fatal("error saving private key:", err)
}

// test load
loadedKey, err := decodePrivateKey(savedBytes)
loadedKey, err := PEMDecodePrivateKey(savedBytes)
if err != nil {
t.Error("error loading private key:", err)
}
Expand Down
Loading