github.com/minio/console@v1.4.1/pkg/certs/certs.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package certs
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/tls"
    23  	"crypto/x509"
    24  	"encoding/pem"
    25  	"errors"
    26  	"fmt"
    27  	"os"
    28  	"path/filepath"
    29  	"strings"
    30  
    31  	"github.com/minio/cli"
    32  	xcerts "github.com/minio/pkg/v3/certs"
    33  	"github.com/minio/pkg/v3/env"
    34  	"github.com/mitchellh/go-homedir"
    35  )
    36  
    37  // ConfigDir - points to a user set directory.
    38  type ConfigDir struct {
    39  	Path string
    40  }
    41  
    42  // Get - returns current directory.
    43  func (dir *ConfigDir) Get() string {
    44  	return dir.Path
    45  }
    46  
    47  func getDefaultConfigDir() string {
    48  	homeDir, err := homedir.Dir()
    49  	if err != nil {
    50  		return ""
    51  	}
    52  	return filepath.Join(homeDir, DefaultConsoleConfigDir)
    53  }
    54  
    55  func getDefaultCertsDir() string {
    56  	return filepath.Join(getDefaultConfigDir(), CertsDir)
    57  }
    58  
    59  func getDefaultCertsCADir() string {
    60  	return filepath.Join(getDefaultCertsDir(), CertsCADir)
    61  }
    62  
    63  // isFile - returns whether given Path is a file or not.
    64  func isFile(path string) bool {
    65  	if fi, err := os.Stat(path); err == nil {
    66  		return fi.Mode().IsRegular()
    67  	}
    68  
    69  	return false
    70  }
    71  
    72  var (
    73  	// DefaultCertsDir certs directory.
    74  	DefaultCertsDir = &ConfigDir{Path: getDefaultCertsDir()}
    75  	// DefaultCertsCADir CA directory.
    76  	DefaultCertsCADir = &ConfigDir{Path: getDefaultCertsCADir()}
    77  	// GlobalCertsDir points to current certs directory set by user with --certs-dir
    78  	GlobalCertsDir = DefaultCertsDir
    79  	// GlobalCertsCADir points to relative Path to certs directory and is <value-of-certs-dir>/CAs
    80  	GlobalCertsCADir = DefaultCertsCADir
    81  )
    82  
    83  // ParsePublicCertFile - parses public cert into its *x509.Certificate equivalent.
    84  func ParsePublicCertFile(certFile string) (x509Certs []*x509.Certificate, err error) {
    85  	// Read certificate file.
    86  	var data []byte
    87  	if data, err = os.ReadFile(certFile); err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	// Trimming leading and tailing white spaces.
    92  	data = bytes.TrimSpace(data)
    93  
    94  	// Parse all certs in the chain.
    95  	current := data
    96  	for len(current) > 0 {
    97  		var pemBlock *pem.Block
    98  		if pemBlock, current = pem.Decode(current); pemBlock == nil {
    99  			return nil, fmt.Errorf("could not read PEM block from file %s", certFile)
   100  		}
   101  
   102  		var x509Cert *x509.Certificate
   103  		if x509Cert, err = x509.ParseCertificate(pemBlock.Bytes); err != nil {
   104  			return nil, err
   105  		}
   106  
   107  		x509Certs = append(x509Certs, x509Cert)
   108  	}
   109  
   110  	if len(x509Certs) == 0 {
   111  		return nil, fmt.Errorf("empty public certificate file %s", certFile)
   112  	}
   113  
   114  	return x509Certs, nil
   115  }
   116  
   117  // MkdirAllIgnorePerm attempts to create all directories, ignores any permission denied errors.
   118  func MkdirAllIgnorePerm(path string) error {
   119  	err := os.MkdirAll(path, 0o700)
   120  	if err != nil {
   121  		// It is possible in kubernetes like deployments this directory
   122  		// is already mounted and is not writable, ignore any write errors.
   123  		if os.IsPermission(err) {
   124  			err = nil
   125  		}
   126  	}
   127  	return err
   128  }
   129  
   130  func NewConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool, error) {
   131  	var dir string
   132  	var dirSet bool
   133  
   134  	switch {
   135  	case ctx.IsSet(option):
   136  		dir = ctx.String(option)
   137  		dirSet = true
   138  	case ctx.GlobalIsSet(option):
   139  		dir = ctx.GlobalString(option)
   140  		dirSet = true
   141  		// cli package does not expose parent's option option.  Below code is workaround.
   142  		if dir == "" || dir == getDefaultDir() {
   143  			dirSet = false // Unset to false since GlobalIsSet() true is a false positive.
   144  			if ctx.Parent().GlobalIsSet(option) {
   145  				dir = ctx.Parent().GlobalString(option)
   146  				dirSet = true
   147  			}
   148  		}
   149  	default:
   150  		// Neither local nor global option is provided.  In this case, try to use
   151  		// default directory.
   152  		dir = getDefaultDir()
   153  		if dir == "" {
   154  			return nil, false, fmt.Errorf("invalid arguments specified, %s option must be provided", option)
   155  		}
   156  	}
   157  
   158  	if dir == "" {
   159  		return nil, false, fmt.Errorf("empty directory, %s directory cannot be empty", option)
   160  	}
   161  
   162  	// Disallow relative paths, figure out absolute paths.
   163  	dirAbs, err := filepath.Abs(dir)
   164  	if err != nil {
   165  		return nil, false, fmt.Errorf("%w: Unable to fetch absolute path for %s=%s", err, option, dir)
   166  	}
   167  	if err = MkdirAllIgnorePerm(dirAbs); err != nil {
   168  		return nil, false, fmt.Errorf("%w: Unable to create directory specified %s=%s", err, option, dir)
   169  	}
   170  	return &ConfigDir{Path: dirAbs}, dirSet, nil
   171  }
   172  
   173  func getPublicCertFile() string {
   174  	return filepath.Join(GlobalCertsDir.Get(), PublicCertFile)
   175  }
   176  
   177  func getPrivateKeyFile() string {
   178  	return filepath.Join(GlobalCertsDir.Get(), PrivateKeyFile)
   179  }
   180  
   181  // EnvCertPassword is the environment variable which contains the password used
   182  // to decrypt the TLS private key. It must be set if the TLS private key is
   183  // password protected.
   184  const EnvCertPassword = "CONSOLE_CERT_PASSWD"
   185  
   186  // LoadX509KeyPair - load an X509 key pair (private key , certificate)
   187  // from the provided paths. The private key may be encrypted and is
   188  // decrypted using the ENV_VAR: MINIO_CERT_PASSWD.
   189  func LoadX509KeyPair(certFile, keyFile string) (tls.Certificate, error) {
   190  	certPEMBlock, err := os.ReadFile(certFile)
   191  	if err != nil {
   192  		return tls.Certificate{}, err
   193  	}
   194  	keyPEMBlock, err := os.ReadFile(keyFile)
   195  	if err != nil {
   196  		return tls.Certificate{}, err
   197  	}
   198  	key, rest := pem.Decode(keyPEMBlock)
   199  	if len(rest) > 0 {
   200  		return tls.Certificate{}, errors.New("the private key contains additional data")
   201  	}
   202  	// nolint:staticcheck // ignore SA1019
   203  	if x509.IsEncryptedPEMBlock(key) {
   204  		password := env.Get(EnvCertPassword, "")
   205  		if len(password) == 0 {
   206  			return tls.Certificate{}, errors.New("no password")
   207  		}
   208  		// nolint:staticcheck // ignore SA1019
   209  		decryptedKey, decErr := x509.DecryptPEMBlock(key, []byte(password))
   210  		if decErr != nil {
   211  			return tls.Certificate{}, decErr
   212  		}
   213  		keyPEMBlock = pem.EncodeToMemory(&pem.Block{Type: key.Type, Bytes: decryptedKey})
   214  	}
   215  	return tls.X509KeyPair(certPEMBlock, keyPEMBlock)
   216  }
   217  
   218  func GetTLSConfig() (x509Certs []*x509.Certificate, manager *xcerts.Manager, err error) {
   219  	ctx, cancel := context.WithCancel(context.Background())
   220  	defer cancel()
   221  
   222  	if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
   223  		return nil, nil, nil
   224  	}
   225  
   226  	if x509Certs, err = ParsePublicCertFile(getPublicCertFile()); err != nil {
   227  		return nil, nil, err
   228  	}
   229  
   230  	manager, err = xcerts.NewManager(ctx, getPublicCertFile(), getPrivateKeyFile(), LoadX509KeyPair)
   231  	if err != nil {
   232  		return nil, nil, err
   233  	}
   234  
   235  	// Console has support for multiple certificates. It expects the following structure:
   236  	// certs/
   237  	//  │
   238  	//  ├─ public.crt
   239  	//  ├─ private.key
   240  	//  │
   241  	//  ├─ example.com/
   242  	//  │   │
   243  	//  │   ├─ public.crt
   244  	//  │   └─ private.key
   245  	//  └─ foobar.org/
   246  	//     │
   247  	//     ├─ public.crt
   248  	//     └─ private.key
   249  	//  ...
   250  	//
   251  	// Therefore, we read all filenames in the cert directory and check
   252  	// for each directory whether it contains a public.crt and private.key.
   253  	// If so, we try to add it to certificate manager.
   254  	root, err := os.Open(GlobalCertsDir.Get())
   255  	if err != nil {
   256  		return nil, nil, err
   257  	}
   258  	defer root.Close()
   259  
   260  	files, err := root.Readdir(-1)
   261  	if err != nil {
   262  		return nil, nil, err
   263  	}
   264  	for _, file := range files {
   265  		// Ignore all
   266  		// - regular files
   267  		// - "CAs" directory
   268  		// - any directory which starts with ".."
   269  		if file.Mode().IsRegular() || file.Name() == "CAs" || strings.HasPrefix(file.Name(), "..") {
   270  			continue
   271  		}
   272  		if file.Mode()&os.ModeSymlink == os.ModeSymlink {
   273  			file, err = os.Stat(filepath.Join(root.Name(), file.Name()))
   274  			if err != nil {
   275  				// not accessible ignore
   276  				continue
   277  			}
   278  			if !file.IsDir() {
   279  				continue
   280  			}
   281  		}
   282  
   283  		var (
   284  			certFile = filepath.Join(root.Name(), file.Name(), PublicCertFile)
   285  			keyFile  = filepath.Join(root.Name(), file.Name(), PrivateKeyFile)
   286  		)
   287  		if !isFile(certFile) || !isFile(keyFile) {
   288  			continue
   289  		}
   290  		if err = manager.AddCertificate(certFile, keyFile); err != nil {
   291  			return nil, nil, fmt.Errorf("unable to load TLS certificate '%s,%s': %w", certFile, keyFile, err)
   292  		}
   293  	}
   294  	return x509Certs, manager, nil
   295  }
   296  
   297  func GetAllCertificatesAndCAs() (*x509.CertPool, []*x509.Certificate, *xcerts.Manager, error) {
   298  	// load all CAs from ~/.console/certs/CAs
   299  	rootCAs, err := xcerts.GetRootCAs(GlobalCertsCADir.Get())
   300  	if err != nil {
   301  		return nil, nil, nil, err
   302  	}
   303  	// load all certs from ~/.console/certs
   304  	publicCerts, certsManager, err := GetTLSConfig()
   305  	if err != nil {
   306  		return nil, nil, nil, err
   307  	}
   308  	if rootCAs == nil {
   309  		rootCAs = &x509.CertPool{}
   310  	}
   311  	// Add the public crts as part of root CAs to trust self.
   312  	for _, publicCrt := range publicCerts {
   313  		rootCAs.AddCert(publicCrt)
   314  	}
   315  	return rootCAs, publicCerts, certsManager, nil
   316  }
   317  
   318  // EnsureCertAndKey checks if both client certificate and key paths are provided
   319  func EnsureCertAndKey(clientCert, clientKey string) error {
   320  	if (clientCert != "" && clientKey == "") ||
   321  		(clientCert == "" && clientKey != "") {
   322  		return errors.New("cert and key must be specified as a pair")
   323  	}
   324  	return nil
   325  }