Skip to content

Commit 797d29b

Browse files
authored
GetCertificate from external certificate sources (Managers) (#163)
This work made possible by Tailscale: https://tailscale.com - thank you to the Tailscale team! * Implement custom GetCertificate callback Useful if another entity is managing certificates and can provide its own dynamically during handshakes. * Refactor CustomGetCertificate into OnDemandConfig * Set certs to managed=true This is only sorta true, but it allows handshake-time maintenance of the certificates that are cached from CustomGetCertificate. Our background maintenance routine skips certs that are OnDemand so it should be fine. * Change CustomGetCertificate into interface value Instead of a function * Case-insensitive subject name comparison Hostnames are case-insensitive Also add context to GetCertificate * Export a couple of outrageously useful functions * Allow multiple custom certificate getters Also minor refactoring and enhancements * Fix tests * Rename Getter -> Manager; refactor And don't cache externally managed certs * Minor updates to comments
1 parent 134f039 commit 797d29b

8 files changed

+151
-48
lines changed

account.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (am *ACMEManager) loadAccount(ca, email string) (acme.Account, error) {
6262
if err != nil {
6363
return acct, err
6464
}
65-
acct.PrivateKey, err = decodePrivateKey(keyBytes)
65+
acct.PrivateKey, err = PEMDecodePrivateKey(keyBytes)
6666
if err != nil {
6767
return acct, fmt.Errorf("could not decode account's private key: %v", err)
6868
}
@@ -129,7 +129,7 @@ func (am *ACMEManager) lookUpAccount(ctx context.Context, privateKeyPEM []byte)
129129
return acme.Account{}, fmt.Errorf("creating ACME client: %v", err)
130130
}
131131

132-
privateKey, err := decodePrivateKey([]byte(privateKeyPEM))
132+
privateKey, err := PEMDecodePrivateKey([]byte(privateKeyPEM))
133133
if err != nil {
134134
return acme.Account{}, fmt.Errorf("decoding private key: %v", err)
135135
}
@@ -157,7 +157,7 @@ func (am *ACMEManager) saveAccount(ca string, account acme.Account) error {
157157
if err != nil {
158158
return err
159159
}
160-
keyBytes, err := encodePrivateKey(account.PrivateKey)
160+
keyBytes, err := PEMEncodePrivateKey(account.PrivateKey)
161161
if err != nil {
162162
return err
163163
}

certificates.go

+16-5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ type Certificate struct {
5757
issuerKey string
5858
}
5959

60+
// Empty returns true if the certificate struct is not filled out; at
61+
// least the tls.Certificate.Certificate field is expected to be set.
62+
func (cert Certificate) Empty() bool {
63+
return len(cert.Certificate.Certificate) == 0
64+
}
65+
6066
// NeedsRenewal returns true if the certificate is
6167
// expiring soon (according to cfg) or has expired.
6268
func (cert Certificate) NeedsRenewal(cfg *Config) bool {
@@ -251,11 +257,15 @@ func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error {
251257
// the leaf cert should be the one for the site; we must set
252258
// the tls.Certificate.Leaf field so that TLS handshakes are
253259
// more efficient
254-
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
255-
if err != nil {
256-
return err
260+
leaf := cert.Certificate.Leaf
261+
if leaf == nil {
262+
var err error
263+
leaf, err = x509.ParseCertificate(tlsCert.Certificate[0])
264+
if err != nil {
265+
return err
266+
}
267+
cert.Certificate.Leaf = leaf
257268
}
258-
cert.Certificate.Leaf = leaf
259269

260270
// for convenience, we do want to assemble all the
261271
// subjects on the certificate into one list
@@ -393,9 +403,10 @@ func SubjectIsInternal(subj string) bool {
393403
// states that IP addresses must match exactly, but this function
394404
// does not attempt to distinguish IP addresses from internal or
395405
// external DNS names that happen to look like IP addresses.
396-
// It uses DNS wildcard matching logic.
406+
// It uses DNS wildcard matching logic and is case-insensitive.
397407
// https://tools.ietf.org/html/rfc2818#section-3.1
398408
func MatchWildcard(subject, wildcard string) bool {
409+
subject, wildcard = strings.ToLower(subject), strings.ToLower(wildcard)
399410
if subject == wildcard {
400411
return true
401412
}

certificates_test.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,27 @@ func TestUnexportedGetCertificate(t *testing.T) {
2727
cfg := &Config{certCache: certCache}
2828

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

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

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

4949
// When no certificate matches and SNI is provided, return no certificate (should be TLS alert)
50-
if cert, matched, defaulted := cfg.getCertificate(&tls.ClientHelloInfo{ServerName: "nomatch"}); matched || defaulted {
50+
if cert, matched, defaulted := cfg.getCertificateFromCache(&tls.ClientHelloInfo{ServerName: "nomatch"}); matched || defaulted {
5151
t.Errorf("Expected matched=false, defaulted=false; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert)
5252
}
5353
}
@@ -190,10 +190,14 @@ func TestMatchWildcard(t *testing.T) {
190190
expect bool
191191
}{
192192
{"hostname", "hostname", true},
193+
{"HOSTNAME", "hostname", true},
194+
{"hostname", "HOSTNAME", true},
193195
{"foo.localhost", "foo.localhost", true},
194196
{"foo.localhost", "bar.localhost", false},
195197
{"foo.localhost", "*.localhost", true},
196198
{"bar.localhost", "*.localhost", true},
199+
{"FOO.LocalHost", "*.localhost", true},
200+
{"Bar.localhost", "*.LOCALHOST", true},
197201
{"foo.bar.localhost", "*.localhost", false},
198202
{".localhost", "*.localhost", false},
199203
{"foo.localhost", "foo.*", false},

certmagic.go

+12
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,18 @@ type Revoker interface {
374374
Revoke(ctx context.Context, cert CertificateResource, reason int) error
375375
}
376376

377+
// CertificateManager is a type that manages certificates (keeps them renewed)
378+
// such that we can get certificates during TLS handshakes to immediately serve
379+
// to clients.
380+
//
381+
// TODO: This is an EXPERIMENTAL API. It is subject to change/removal.
382+
type CertificateManager interface {
383+
// GetCertificate returns the certificate to use to complete the handshake.
384+
// Since this is called during every TLS handshake, it must be very fast and not block.
385+
// Returning (nil, nil) is valid and is simply treated as a no-op.
386+
GetCertificate(context.Context, *tls.ClientHelloInfo) (*tls.Certificate, error)
387+
}
388+
377389
// KeyGenerator can generate a private key.
378390
type KeyGenerator interface {
379391
// GenerateKey generates a private key. The returned

config.go

+14-5
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,21 @@ type Config struct {
7272
// Adds the must staple TLS extension to the CSR.
7373
MustStaple bool
7474

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

81+
// Sources for getting new, unmanaged certificates.
82+
// They will be invoked only during TLS handshakes
83+
// before on-demand certificate management occurs,
84+
// for certificates that are not already loaded into
85+
// the in-memory cache.
86+
//
87+
// TODO: EXPERIMENTAL: subject to change and/or removal.
88+
Managers []CertificateManager
89+
8190
// The source of new private keys for certificates;
8291
// the default KeySource is StandardKeyGenerator.
8392
KeySource KeyGenerator
@@ -499,7 +508,7 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
499508
if err != nil {
500509
return err
501510
}
502-
privKeyPEM, err = encodePrivateKey(privKey)
511+
privKeyPEM, err = PEMEncodePrivateKey(privKey)
503512
if err != nil {
504513
return err
505514
}
@@ -605,7 +614,7 @@ func (cfg *Config) reusePrivateKey(domain string) (privKey crypto.PrivateKey, pr
605614
}
606615

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

725-
privateKey, err := decodePrivateKey(certRes.PrivateKeyPEM)
734+
privateKey, err := PEMDecodePrivateKey(certRes.PrivateKeyPEM)
726735
if err != nil {
727736
return err
728737
}

crypto.go

+10-6
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ import (
3636
"golang.org/x/net/idna"
3737
)
3838

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

68-
// decodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
70+
// PEMDecodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
6971
// Borrowed from Go standard library, to handle various private key and PEM block types.
70-
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308
71-
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238)
72-
func decodePrivateKey(keyPEMBytes []byte) (crypto.Signer, error) {
72+
func PEMDecodePrivateKey(keyPEMBytes []byte) (crypto.Signer, error) {
73+
// Modified from original:
74+
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308
75+
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238
76+
7377
keyBlockDER, _ := pem.Decode(keyPEMBytes)
7478

7579
if keyBlockDER == nil {

crypto_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@ func TestEncodeDecodeRSAPrivateKey(t *testing.T) {
3333
}
3434

3535
// test save
36-
savedBytes, err := encodePrivateKey(privateKey)
36+
savedBytes, err := PEMEncodePrivateKey(privateKey)
3737
if err != nil {
3838
t.Fatal("error saving private key:", err)
3939
}
4040

4141
// test load
42-
loadedKey, err := decodePrivateKey(savedBytes)
42+
loadedKey, err := PEMDecodePrivateKey(savedBytes)
4343
if err != nil {
4444
t.Error("error loading private key:", err)
4545
}
4646

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

6565
// test save
66-
savedBytes, err := encodePrivateKey(privateKey)
66+
savedBytes, err := PEMEncodePrivateKey(privateKey)
6767
if err != nil {
6868
t.Fatal("error saving private key:", err)
6969
}
7070

7171
// test load
72-
loadedKey, err := decodePrivateKey(savedBytes)
72+
loadedKey, err := PEMDecodePrivateKey(savedBytes)
7373
if err != nil {
7474
t.Error("error loading private key:", err)
7575
}

0 commit comments

Comments
 (0)