@ -17,7 +17,11 @@
package certs
package certs
import (
import (
"context"
"crypto/tls"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
"os"
"path/filepath"
"path/filepath"
"sync"
"sync"
@ -26,177 +30,317 @@ import (
"github.com/rjeczalik/notify"
"github.com/rjeczalik/notify"
)
)
// A Certs represents a certificate manager able to watch certificate
// LoadX509KeyPairFunc is a function that parses a private key and
// and key pairs for changes.
// certificate file and returns a TLS certificate on success.
type Certs struct {
type LoadX509KeyPairFunc func ( certFile , keyFile string ) ( tls . Certificate , error )
sync . RWMutex
// user input params.
// GetCertificateFunc is a callback that allows a TLS stack deliver different
certFile string
// certificates based on the client trying to establish a TLS connection.
keyFile string
//
loadCert LoadX509KeyPairFunc
// For example, a GetCertificateFunc can return different TLS certificates depending
// upon the TLS SNI sent by the client.
// points to the latest certificate.
type GetCertificateFunc func ( hello * tls . ClientHelloInfo ) ( * tls . Certificate , error )
cert * tls . Certificate
// Manager is a TLS certificate manager that can handle multiple certificates.
// internal param to track for events, also
// When a client tries to establish a TLS connection, Manager will try to
// used to close the watcher.
// pick a certificate that can be validated by the client.
e chan notify . EventInfo
//
// For instance, if the client specifies a TLS SNI then Manager will try to
// find the corresponding certificate. If there is no such certificate it
// will fallback to the certificate named public.crt.
//
// Manager will automatically reload certificates if the corresponding file changes.
type Manager struct {
lock sync . RWMutex
certificates map [ pair ] * tls . Certificate // Mapping: certificate file name => TLS certificates
defaultCert pair
loadX509KeyPair LoadX509KeyPairFunc
events chan notify . EventInfo
ctx context . Context
}
}
// LoadX509KeyPairFunc - provides a type for custom cert loader function.
// pair represents a certificate and private key file tuple.
type LoadX509KeyPairFunc func ( certFile , keyFile string ) ( tls . Certificate , error )
type pair struct {
KeyFile string
CertFile string
}
// New initializes a new certs monitor.
// NewManager returns a new Manager that handles one certificate specified via
func New ( certFile , keyFile string , loadCert LoadX509KeyPairFunc ) ( * Certs , error ) {
// the certFile and keyFile. It will use the loadX509KeyPair function to (re)load
certFileIsLink , err := checkSymlink ( certFile )
// certificates.
//
// The certificate loaded from certFile is considered the default certificate.
// If a client does not send the TLS SNI extension then Manager will return
// this certificate.
func NewManager ( ctx context . Context , certFile , keyFile string , loadX509KeyPair LoadX509KeyPairFunc ) ( manager * Manager , err error ) {
certFile , err = filepath . Abs ( certFile )
if err != nil {
if err != nil {
return nil , err
return nil , err
}
}
keyFileIsLink , err := checkSymlink ( keyFile )
keyFile , err = filepath . Abs ( keyFile )
if err != nil {
if err != nil {
return nil , err
return nil , err
}
}
c := & Certs {
certFile : certFile ,
keyFile : keyFile ,
loadCert : loadCert ,
// Make the channel buffered to ensure no event is dropped. Notify will drop
// an event if the receiver is not able to keep up the sending pace.
e : make ( chan notify . EventInfo , 1 ) ,
}
if certFileIsLink && keyFileIsLink {
manager = & Manager {
if err := c . watchSymlinks ( ) ; err != nil {
certificates : map [ pair ] * tls . Certificate { } ,
return nil , err
defaultCert : pair {
}
KeyFile : keyFile ,
} else {
CertFile : certFile ,
if err := c . watch ( ) ; err != nil {
} ,
return nil , err
loadX509KeyPair : loadX509KeyPair ,
}
events : make ( chan notify . EventInfo , 1 ) ,
ctx : ctx ,
}
}
if err := manager . AddCertificate ( certFile , keyFile ) ; err != nil {
return c , nil
return nil , err
}
func checkSymlink ( file string ) ( bool , error ) {
st , err := os . Lstat ( file )
if err != nil {
return false , err
}
}
return st . Mode ( ) & os . ModeSymlink == os . ModeSymlink , nil
go manager . watchFileEvents ( )
return manager , nil
}
}
// watchSymlinks reloads symlinked files since fsnotify cannot watch
// AddCertificate adds the TLS certificate in certFile resp. keyFile
// on symbolic links.
// to the Manager.
func ( c * Certs ) watchSymlinks ( ) ( err error ) {
//
cert , err := c . loadCert ( c . certFile , c . keyFile )
// If there is already a certificate with the same base name it will be
// replaced by the newly added one.
func ( m * Manager ) AddCertificate ( certFile , keyFile string ) ( err error ) {
certFile , err = filepath . Abs ( certFile )
if err != nil {
if err != nil {
return err
return err
}
}
c . Lock ( )
keyFile , err = filepath . Abs ( keyFile )
c . cert = & cert
if err != nil {
c . Unlock ( )
go func ( ) {
for {
select {
case <- c . e :
// Once stopped exits this routine.
return
case <- time . After ( 24 * time . Hour ) :
cert , cerr := c . loadCert ( c . certFile , c . keyFile )
if cerr != nil {
continue
}
c . Lock ( )
c . cert = & cert
c . Unlock ( )
}
}
} ( )
return nil
}
// watch starts watching for changes to the certificate
// and key files. On any change the certificate and key
// are reloaded. If there is an issue the loading will fail
// and the old (if any) certificates and keys will continue
// to be used.
func ( c * Certs ) watch ( ) ( err error ) {
defer func ( ) {
if err != nil {
// Stop any watches previously setup after an error.
notify . Stop ( c . e )
}
} ( )
// Windows doesn't allow for watching file changes but instead allows
// for directory changes only, while we can still watch for changes
// on files on other platforms. Watch parent directory on all platforms
// for simplicity.
if err = notify . Watch ( filepath . Dir ( c . certFile ) , c . e , eventWrite ... ) ; err != nil {
return err
return err
}
}
if err = notify . Watch ( filepath . Dir ( c . keyFile ) , c . e , eventWrite ... ) ; err != nil {
certFileIsLink , err := isSymlink ( certFile )
if err != nil {
return err
return err
}
}
cert , err := c . loadCert ( c . certFile , c . keyFile )
keyFileIsLink , err := isSymlink ( keyFile )
if err != nil {
if err != nil {
return err
return err
}
}
c . Lock ( )
if certFileIsLink && ! keyFileIsLink {
c . cert = & cert
return fmt . Errorf ( "certs: '%s' is a symlink but '%s' is a regular file" , certFile , keyFile )
c . Unlock ( )
}
if keyFileIsLink && ! certFileIsLink {
return fmt . Errorf ( "certs: '%s' is a symlink but '%s' is a regular file" , keyFile , certFile )
}
certificate , err := m . loadX509KeyPair ( certFile , keyFile )
if err != nil {
if err != nil {
return err
return err
}
}
go c . run ( )
// We set the certificate leaf to the actual certificate such that
// we don't have to do the parsing (multiple times) when matching the
// certificate to the client hello. This a performance optimisation.
if certificate . Leaf == nil {
certificate . Leaf , err = x509 . ParseCertificate ( certificate . Certificate [ 0 ] )
if err != nil {
return err
}
}
p := pair {
CertFile : certFile ,
KeyFile : keyFile ,
}
m . lock . Lock ( )
defer m . lock . Unlock ( )
// We don't allow IP SANs in certificates - except for the "default" certificate
// which is, by convention, the first certificate added to the manager. The problem
// with allowing IP SANs in more than one certificate is that the manager usually can't
// match the client SNI to a SAN since the SNI is meant to communicate the destination
// host name and clients will not set the SNI to an IP address.
// Allowing multiple certificates with IP SANs lead to errors that confuses users - like:
// "It works for `https://instance.minio.local` but not for `https://10.0.2.1`"
if len ( m . certificates ) > 0 && len ( certificate . Leaf . IPAddresses ) > 0 {
return errors . New ( "cert: certificate must not contain any IP SANs: only the default certificate may contain IP SANs" )
}
m . certificates [ p ] = & certificate
if certFileIsLink && keyFileIsLink {
go m . watchSymlinks ( certFile , keyFile )
} else {
// Windows doesn't allow for watching file changes but instead allows
// for directory changes only, while we can still watch for changes
// on files on other platforms. Watch parent directory on all platforms
// for simplicity.
if err = notify . Watch ( filepath . Dir ( certFile ) , m . events , eventWrite ... ) ; err != nil {
return err
}
if err = notify . Watch ( filepath . Dir ( keyFile ) , m . events , eventWrite ... ) ; err != nil {
return err
}
}
return nil
return nil
}
}
func ( c * Certs ) run ( ) {
// watchSymlinks starts an endless loop reloading the
for event := range c . e {
// certFile and keyFile periodically.
base := filepath . Base ( event . Path ( ) )
func ( m * Manager ) watchSymlinks ( certFile , keyFile string ) {
if isWriteEvent ( event . Event ( ) ) {
for {
certChanged := base == filepath . Base ( c . certFile )
select {
keyChanged := base == filepath . Base ( c . keyFile )
case <- m . ctx . Done ( ) :
if certChanged || keyChanged {
return // Once stopped exits this routine.
cert , err := c . loadCert ( c . certFile , c . keyFile )
case <- time . After ( 24 * time . Hour ) :
certificate , err := m . loadX509KeyPair ( certFile , keyFile )
if err != nil {
continue
}
if certificate . Leaf == nil { // This is a performance optimisation
certificate . Leaf , err = x509 . ParseCertificate ( certificate . Certificate [ 0 ] )
if err != nil {
if err != nil {
// ignore the error continue to use
// old certificates.
continue
continue
}
}
c . Lock ( )
c . cert = & cert
c . Unlock ( )
}
}
p := pair {
CertFile : certFile ,
KeyFile : keyFile ,
}
m . lock . Lock ( )
m . certificates [ p ] = & certificate
m . lock . Unlock ( )
}
}
}
}
}
}
// GetCertificateFunc provides a GetCertificate type for custom client implementations.
// watchFileEvents starts an endless loop waiting for file systems events.
type GetCertificateFunc func ( hello * tls . ClientHelloInfo ) ( * tls . Certificate , error )
// Once an event occurs it reloads the private key and certificate that
// has changed, if any.
func ( m * Manager ) watchFileEvents ( ) {
for {
select {
case <- m . ctx . Done ( ) :
return
case event := <- m . events :
if ! isWriteEvent ( event . Event ( ) ) {
continue
}
for pair := range m . certificates {
if p := event . Path ( ) ; pair . KeyFile == p || pair . CertFile == p {
certificate , err := m . loadX509KeyPair ( pair . CertFile , pair . KeyFile )
if err != nil {
continue
}
if certificate . Leaf == nil { // This is performance optimisation
certificate . Leaf , err = x509 . ParseCertificate ( certificate . Certificate [ 0 ] )
if err != nil {
continue
}
}
m . lock . Lock ( )
m . certificates [ pair ] = & certificate
m . lock . Unlock ( )
}
}
}
}
}
// GetCertificate returns a TLS certificate based on the client hello.
//
// It tries to find a certificate that would be accepted by the client
// according to the client hello. However, if no certificate can be
// found GetCertificate returns the certificate loaded from the
// Public file.
func ( m * Manager ) GetCertificate ( hello * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
m . lock . RLock ( )
defer m . lock . RUnlock ( )
// If the client does not send a SNI we return the "default"
// certificate. A client may not send a SNI - e.g. when trying
// to connect to an IP directly (https://<ip>:<port>).
//
// In this case we don't know which the certificate the client
// asks for. It may be a public-facing certificate issued by a
// public CA or an internal certificate containing internal domain
// names.
// Now, we should not serve "the first" certificate that would be
// accepted by the client based on the Client Hello. Otherwise, we
// may expose an internal certificate to the client that contains
// internal domain names. That way we would disclose internal
// infrastructure details.
//
// Therefore, we serve the "default" certificate - which by convention
// is the first certificate added to the Manager. It's the calling code's
// responsibility to ensure that the "public-facing" certificate is used
// when creating a Manager instance.
if hello . ServerName == "" {
certificate := m . certificates [ m . defaultCert ]
return certificate , nil
}
// Optimization: If there is just one certificate, always serve that one.
if len ( m . certificates ) == 1 {
for _ , certificate := range m . certificates {
return certificate , nil
}
}
// GetCertificate returns the loaded certificate for use by
// Iterate over all certificates and return the first one that would
// the TLSConfig fields GetCertificate field in a http.Server.
// be accepted by the peer (TLS client) based on the client hello.
func ( c * Certs ) GetCertificate ( hello * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
// In particular, the client usually specifies the requested host/domain
c . RLock ( )
// via SNI.
defer c . RUnlock ( )
//
return c . cert , nil
// Note: The certificate.Leaf should be non-nil and contain the actual
// client certificate of MinIO that should be presented to the peer (TLS client).
// Otherwise, the leaf certificate has to be parsed again - which is kind of
// expensive and may cause a performance issue. For more information, check the
// docs of tls.ClientHelloInfo.SupportsCertificate.
for _ , certificate := range m . certificates {
if err := hello . SupportsCertificate ( certificate ) ; err == nil {
return certificate , nil
}
}
return nil , errors . New ( "certs: no server certificate is supported by peer" )
}
}
// GetClientCertificate returns the loaded certificate for use by
// GetClientCertificate returns a TLS certificate for mTLS based on the
// the TLSConfig fields GetClientCertificate field in a http.Server.
// certificate request.
func ( c * Certs ) GetClientCertificate ( _ * tls . CertificateRequestInfo ) ( * tls . Certificate , error ) {
//
c . RLock ( )
// It tries to find a certificate that would be accepted by the server
defer c . RUnlock ( )
// according to the certificate request. However, if no certificate can be
return c . cert , nil
// found GetClientCertificate returns the certificate loaded from the
// Public file.
func ( m * Manager ) GetClientCertificate ( reqInfo * tls . CertificateRequestInfo ) ( * tls . Certificate , error ) {
m . lock . RLock ( )
defer m . lock . RUnlock ( )
// Optimization: If there is just one certificate, always serve that one.
if len ( m . certificates ) == 1 {
for _ , certificate := range m . certificates {
return certificate , nil
}
}
// Iterate over all certificates and return the first one that would
// be accepted by the peer (TLS server) based on reqInfo.
//
// Note: The certificate.Leaf should be non-nil and contain the actual
// client certificate of MinIO that should be presented to the peer (TLS server).
// Otherwise, the leaf certificate has to be parsed again - which is kind of
// expensive and may cause a performance issue. For more information, check the
// docs of tls.CertificateRequestInfo.SupportsCertificate.
for _ , certificate := range m . certificates {
if err := reqInfo . SupportsCertificate ( certificate ) ; err == nil {
return certificate , nil
}
}
return nil , errors . New ( "certs: no client certificate is supported by peer" )
}
}
// Stop tells loader to stop watching for changes to the
// isSymlink returns true if the given file
// certificate and key files.
// is a symbolic link.
func ( c * Certs ) Stop ( ) {
func isSymlink ( file string ) ( bool , error ) {
if c != nil {
st , err := os . Lstat ( file )
notify . Stop ( c . e )
if err != nil {
return false , err
}
}
return st . Mode ( ) & os . ModeSymlink == os . ModeSymlink , nil
}
}