certificate: Implement GetCertificate callback

This commit is contained in:
Adnan Maolood 2021-02-19 18:37:28 -05:00
parent 76967dad2e
commit 66e03ef1e4
2 changed files with 155 additions and 94 deletions

View File

@ -14,103 +14,9 @@ import (
"math/big" "math/big"
"net" "net"
"os" "os"
"path/filepath"
"strings"
"sync"
"time" "time"
) )
// Dir represents a directory of certificates.
// The zero value for Dir is an empty directory ready to use.
//
// Dir is safe for concurrent use by multiple goroutines.
type Dir struct {
certs map[string]tls.Certificate
path *string
mu sync.RWMutex
}
// Add adds a certificate for the given scope to the directory.
// It tries to parse the certificate if it is not already parsed.
func (d *Dir) Add(scope string, cert tls.Certificate) error {
d.mu.Lock()
defer d.mu.Unlock()
if d.certs == nil {
d.certs = map[string]tls.Certificate{}
}
// Parse certificate if not already parsed
if cert.Leaf == nil {
parsed, err := x509.ParseCertificate(cert.Certificate[0])
if err == nil {
cert.Leaf = parsed
}
}
if d.path != nil {
// Escape slash character
scope = strings.ReplaceAll(scope, "/", ":")
certPath := filepath.Join(*d.path, scope+".crt")
keyPath := filepath.Join(*d.path, scope+".key")
if err := Write(cert, certPath, keyPath); err != nil {
return err
}
}
d.certs[scope] = cert
return nil
}
// Lookup returns the certificate for the provided scope.
func (d *Dir) Lookup(scope string) (tls.Certificate, bool) {
d.mu.RLock()
defer d.mu.RUnlock()
cert, ok := d.certs[scope]
return cert, ok
}
// Entries returns a map of hostnames to certificates.
func (d *Dir) Entries() map[string]tls.Certificate {
certs := map[string]tls.Certificate{}
for key := range d.certs {
certs[key] = d.certs[key]
}
return certs
}
// Load loads certificates from the provided path.
// Add will write certificates to this path.
//
// The directory should contain certificates and private keys
// named scope.crt and scope.key respectively, where scope is
// the scope of the certificate.
func (d *Dir) Load(path string) error {
matches, err := filepath.Glob(filepath.Join(path, "*.crt"))
if err != nil {
return err
}
for _, crtPath := range matches {
keyPath := strings.TrimSuffix(crtPath, ".crt") + ".key"
cert, err := tls.LoadX509KeyPair(crtPath, keyPath)
if err != nil {
continue
}
scope := strings.TrimSuffix(filepath.Base(crtPath), ".crt")
// Unescape slash character
scope = strings.ReplaceAll(scope, ":", "/")
d.Add(scope, cert)
}
d.SetPath(path)
return nil
}
// SetPath sets the directory path.
// Add will write certificates to this path.
func (d *Dir) SetPath(path string) {
d.mu.Lock()
defer d.mu.Unlock()
d.path = &path
}
// CreateOptions configures the creation of a TLS certificate. // CreateOptions configures the creation of a TLS certificate.
type CreateOptions struct { type CreateOptions struct {
// Subject Alternate Name values. // Subject Alternate Name values.

155
certificate/store.go Normal file
View File

@ -0,0 +1,155 @@
package certificate
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"path/filepath"
"strings"
"sync"
"time"
)
// Store represents a certificate store.
// The zero value for Store is an empty store ready to use.
//
// Store is safe for concurrent use by multiple goroutines.
type Store struct {
// CreateCertificate, if not nil, is called to create a new certificate
// to replace a missing or expired certificate.
CreateCertificate func(scope string) (tls.Certificate, error)
certs map[string]tls.Certificate
path string
mu sync.RWMutex
}
// Register registers the provided scope in the certificate store.
// The certificate will be created upon calling GetCertificate.
func (s *Store) Register(scope string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.certs == nil {
s.certs = make(map[string]tls.Certificate)
}
s.certs[scope] = tls.Certificate{}
}
// Add adds a certificate for the given scope to the certificate store.
func (s *Store) Add(scope string, cert tls.Certificate) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.certs == nil {
s.certs = make(map[string]tls.Certificate)
}
// Parse certificate if not already parsed
if cert.Leaf == nil {
parsed, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return err
}
cert.Leaf = parsed
}
if s.path != "" {
// Escape slash character
path := strings.ReplaceAll(scope, "/", ":")
certPath := filepath.Join(s.path, path+".crt")
keyPath := filepath.Join(s.path, path+".key")
if err := Write(cert, certPath, keyPath); err != nil {
return err
}
}
s.certs[scope] = cert
return nil
}
// Lookup returns the certificate for the provided scope.
func (s *Store) Lookup(scope string) (tls.Certificate, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
cert, ok := s.certs[scope]
return cert, ok
}
// GetCertificate retrieves the certificate for the given scope.
// If the retrieved certificate is expired or the scope is registered but
// has no certificate, it calls CreateCertificate to create a new certificate.
func (s *Store) GetCertificate(scope string) (*tls.Certificate, error) {
cert, ok := s.Lookup(scope)
if !ok {
// Try wildcard
wildcard := strings.SplitN(scope, ".", 2)
if len(wildcard) == 2 {
cert, ok = s.Lookup("*." + wildcard[1])
}
}
if !ok {
return nil, errors.New("unrecognized scope")
}
// If the certificate is empty or expired, generate a new one.
// TODO: Add sane defaults for certificate generation
if cert.Leaf == nil || cert.Leaf.NotAfter.Before(time.Now()) {
if s.CreateCertificate != nil {
cert, err := s.CreateCertificate(scope)
if err != nil {
return nil, err
}
if err := s.Add(scope, cert); err != nil {
return nil, fmt.Errorf("failed to write new certificate for %s: %w", scope, err)
}
return &cert, nil
}
return nil, errors.New("no suitable certificate found")
}
return &cert, nil
}
// Load loads certificates from the provided path.
// New certificates will be written to this path.
//
// The path should lead to a directory containing certificates
// and private keys named "scope.crt" and "scope.key" respectively,
// where "scope" is the scope of the certificate.
func (s *Store) Load(path string) error {
matches, err := filepath.Glob(filepath.Join(path, "*.crt"))
if err != nil {
return err
}
for _, crtPath := range matches {
keyPath := strings.TrimSuffix(crtPath, ".crt") + ".key"
cert, err := tls.LoadX509KeyPair(crtPath, keyPath)
if err != nil {
continue
}
scope := strings.TrimSuffix(filepath.Base(crtPath), ".crt")
// Unescape slash character
scope = strings.ReplaceAll(scope, ":", "/")
s.Add(scope, cert)
}
s.SetPath(path)
return nil
}
// Entries returns a map of scopes to certificates.
func (s *Store) Entries() map[string]tls.Certificate {
s.mu.RLock()
defer s.mu.RUnlock()
certs := make(map[string]tls.Certificate)
for key := range s.certs {
certs[key] = s.certs[key]
}
return certs
}
// SetPath sets the path that new certificates will be written to.
func (s *Store) SetPath(path string) {
s.mu.Lock()
defer s.mu.Unlock()
s.path = path
}