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 }