2021-02-19 23:37:28 +00:00
|
|
|
package certificate
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
2021-02-20 23:23:08 +00:00
|
|
|
"crypto/x509/pkix"
|
2021-02-19 23:37:28 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2021-02-28 23:56:22 +00:00
|
|
|
"io/fs"
|
|
|
|
"os"
|
2021-03-01 01:23:29 +00:00
|
|
|
"path"
|
2021-02-19 23:37:28 +00:00
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2021-02-28 23:56:22 +00:00
|
|
|
// A Store represents a TLS certificate store.
|
2021-02-19 23:37:28 +00:00
|
|
|
// The zero value for Store is an empty store ready to use.
|
|
|
|
//
|
2021-02-28 23:56:22 +00:00
|
|
|
// Store can be used to store server certificates.
|
|
|
|
// Servers should provide a hostname or wildcard pattern as a certificate scope.
|
|
|
|
// Servers will most likely use the methods Register, Load and Get.
|
|
|
|
//
|
|
|
|
// Store can also be used to store client certificates.
|
2021-03-01 01:23:29 +00:00
|
|
|
// Clients should provide the hostname and path of a URL as a certificate scope
|
|
|
|
// (without a trailing slash).
|
2021-02-28 23:56:22 +00:00
|
|
|
// Clients will most likely use the methods Add, Load, and Lookup.
|
2021-02-23 13:36:27 +00:00
|
|
|
//
|
2021-02-19 23:37:28 +00:00
|
|
|
// Store is safe for concurrent use by multiple goroutines.
|
|
|
|
type Store struct {
|
2021-02-28 23:56:22 +00:00
|
|
|
// CreateCertificate, if not nil, is called by Get to create a new
|
|
|
|
// certificate to replace a missing or expired certificate.
|
2021-02-23 13:36:27 +00:00
|
|
|
// The provided scope is suitable for use in a certificate's DNSNames.
|
2021-02-19 23:37:28 +00:00
|
|
|
CreateCertificate func(scope string) (tls.Certificate, error)
|
|
|
|
|
2021-02-28 23:56:22 +00:00
|
|
|
scopes map[string]struct{}
|
|
|
|
certs map[string]tls.Certificate
|
|
|
|
path string
|
|
|
|
mu sync.RWMutex
|
2021-02-19 23:37:28 +00:00
|
|
|
}
|
|
|
|
|
2021-02-23 13:36:27 +00:00
|
|
|
// Register registers the provided scope with the certificate store.
|
|
|
|
// The scope can either be a hostname or a wildcard pattern (e.g. "*.example.com").
|
|
|
|
// To accept all hostnames, use the special pattern "*".
|
2021-02-28 23:56:22 +00:00
|
|
|
//
|
|
|
|
// Calls to Get will only succeed for registered scopes.
|
|
|
|
// Other methods are not affected.
|
2021-02-19 23:37:28 +00:00
|
|
|
func (s *Store) Register(scope string) {
|
|
|
|
s.mu.Lock()
|
|
|
|
defer s.mu.Unlock()
|
2021-02-28 23:56:22 +00:00
|
|
|
if s.scopes == nil {
|
|
|
|
s.scopes = make(map[string]struct{})
|
2021-02-19 23:37:28 +00:00
|
|
|
}
|
2021-02-28 23:56:22 +00:00
|
|
|
s.scopes[scope] = struct{}{}
|
2021-02-19 23:37:28 +00:00
|
|
|
}
|
|
|
|
|
2021-02-28 23:56:22 +00:00
|
|
|
// Add registers the certificate for the given scope.
|
|
|
|
// If a certificate already exists for scope, Add will overwrite it.
|
2021-02-19 23:37:28 +00:00
|
|
|
func (s *Store) Add(scope string, cert tls.Certificate) error {
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-02-28 23:56:22 +00:00
|
|
|
if err := s.write(scope, cert); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
s.mu.Lock()
|
|
|
|
defer s.mu.Unlock()
|
|
|
|
if s.certs == nil {
|
|
|
|
s.certs = make(map[string]tls.Certificate)
|
|
|
|
}
|
|
|
|
s.certs[scope] = cert
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Store) write(scope string, cert tls.Certificate) error {
|
|
|
|
s.mu.RLock()
|
|
|
|
defer s.mu.RUnlock()
|
2021-02-19 23:37:28 +00:00
|
|
|
if s.path != "" {
|
2021-02-23 13:36:27 +00:00
|
|
|
certPath := filepath.Join(s.path, scope+".crt")
|
|
|
|
keyPath := filepath.Join(s.path, scope+".key")
|
2021-02-28 23:56:22 +00:00
|
|
|
|
|
|
|
dir := filepath.Dir(certPath)
|
|
|
|
os.MkdirAll(dir, 0755)
|
|
|
|
|
2021-02-19 23:37:28 +00:00
|
|
|
if err := Write(cert, certPath, keyPath); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-02-23 13:36:27 +00:00
|
|
|
// Get retrieves a certificate for the given hostname.
|
2021-02-23 14:03:38 +00:00
|
|
|
// If no matching scope has been registered, Get returns an error.
|
|
|
|
// Get generates new certificates as needed and rotates expired certificates.
|
2021-02-28 23:56:22 +00:00
|
|
|
// It calls CreateCertificate to create a new certificate if it is not nil,
|
|
|
|
// otherwise it creates certificates with a duration of 1 year.
|
2021-02-23 14:10:19 +00:00
|
|
|
//
|
|
|
|
// Get is suitable for use in a gemini.Server's GetCertificate field.
|
2021-02-23 13:36:27 +00:00
|
|
|
func (s *Store) Get(hostname string) (*tls.Certificate, error) {
|
2021-02-19 23:37:28 +00:00
|
|
|
s.mu.RLock()
|
|
|
|
defer s.mu.RUnlock()
|
2021-03-04 21:12:36 +00:00
|
|
|
_, ok := s.scopes[hostname]
|
2021-02-19 23:37:28 +00:00
|
|
|
if !ok {
|
|
|
|
// Try wildcard
|
2021-02-23 13:36:27 +00:00
|
|
|
wildcard := strings.SplitN(hostname, ".", 2)
|
2021-02-19 23:37:28 +00:00
|
|
|
if len(wildcard) == 2 {
|
2021-02-23 13:36:27 +00:00
|
|
|
hostname = "*." + wildcard[1]
|
2021-02-28 23:56:22 +00:00
|
|
|
_, ok = s.scopes[hostname]
|
2021-02-19 23:37:28 +00:00
|
|
|
}
|
|
|
|
}
|
2021-02-23 13:52:12 +00:00
|
|
|
if !ok {
|
|
|
|
// Try "*"
|
2021-02-28 23:56:22 +00:00
|
|
|
_, ok = s.scopes["*"]
|
2021-02-23 13:52:12 +00:00
|
|
|
}
|
2021-02-19 23:37:28 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("unrecognized scope")
|
|
|
|
}
|
|
|
|
|
2021-02-28 23:56:22 +00:00
|
|
|
cert := s.certs[hostname]
|
|
|
|
|
2021-02-19 23:37:28 +00:00
|
|
|
// If the certificate is empty or expired, generate a new one.
|
|
|
|
if cert.Leaf == nil || cert.Leaf.NotAfter.Before(time.Now()) {
|
2021-02-20 23:23:08 +00:00
|
|
|
var err error
|
2021-02-23 13:36:27 +00:00
|
|
|
cert, err = s.createCertificate(hostname)
|
2021-02-20 23:23:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-02-23 13:36:27 +00:00
|
|
|
if err := s.Add(hostname, cert); err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to add certificate for %s: %w", hostname, err)
|
2021-02-19 23:37:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &cert, nil
|
|
|
|
}
|
|
|
|
|
2021-02-28 23:56:22 +00:00
|
|
|
// Lookup returns the certificate for the provided scope.
|
2021-03-01 01:23:29 +00:00
|
|
|
// Lookup also checks for certificates in parent scopes.
|
|
|
|
// For example, given the scope "example.com/a/b/c", Lookup will first check
|
|
|
|
// "example.com/a/b/c", then "example.com/a/b", then "example.com/a", and
|
|
|
|
// finally "example.com" for a certificate. As a result, a certificate with
|
|
|
|
// scope "example.com" will match all scopes beginning with "example.com".
|
2021-02-28 23:56:22 +00:00
|
|
|
func (s *Store) Lookup(scope string) (tls.Certificate, bool) {
|
|
|
|
s.mu.RLock()
|
|
|
|
defer s.mu.RUnlock()
|
|
|
|
cert, ok := s.certs[scope]
|
2021-03-01 01:23:29 +00:00
|
|
|
if !ok {
|
|
|
|
scope = path.Dir(scope)
|
|
|
|
for scope != "." {
|
|
|
|
cert, ok = s.certs[scope]
|
|
|
|
if ok {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
scope = path.Dir(scope)
|
|
|
|
}
|
|
|
|
}
|
2021-02-28 23:56:22 +00:00
|
|
|
return cert, ok
|
|
|
|
}
|
|
|
|
|
2021-02-20 23:23:08 +00:00
|
|
|
func (s *Store) createCertificate(scope string) (tls.Certificate, error) {
|
|
|
|
if s.CreateCertificate != nil {
|
|
|
|
return s.CreateCertificate(scope)
|
|
|
|
}
|
|
|
|
return Create(CreateOptions{
|
|
|
|
DNSNames: []string{scope},
|
|
|
|
Subject: pkix.Name{
|
|
|
|
CommonName: scope,
|
|
|
|
},
|
|
|
|
Duration: 365 * 24 * time.Hour,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-02-19 23:37:28 +00:00
|
|
|
// 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 {
|
2021-02-28 23:56:22 +00:00
|
|
|
matches := findCertificates(path)
|
2021-02-19 23:37:28 +00:00
|
|
|
for _, crtPath := range matches {
|
|
|
|
keyPath := strings.TrimSuffix(crtPath, ".crt") + ".key"
|
|
|
|
cert, err := tls.LoadX509KeyPair(crtPath, keyPath)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2021-02-28 23:56:22 +00:00
|
|
|
|
|
|
|
scope := strings.TrimPrefix(crtPath, path)
|
|
|
|
scope = strings.TrimPrefix(scope, "/")
|
|
|
|
scope = strings.TrimSuffix(scope, ".crt")
|
2021-02-19 23:37:28 +00:00
|
|
|
s.Add(scope, cert)
|
|
|
|
}
|
|
|
|
s.SetPath(path)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-02-28 23:56:22 +00:00
|
|
|
func findCertificates(path string) (matches []string) {
|
|
|
|
filepath.Walk(path, func(path string, _ fs.FileInfo, err error) error {
|
|
|
|
if filepath.Ext(path) == ".crt" {
|
|
|
|
matches = append(matches, path)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-19 23:37:28 +00:00
|
|
|
// 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
|
|
|
|
}
|