transport/tlscommon/tls.go (188 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package tlscommon
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/elastic/elastic-agent-libs/logp"
)
const logSelector = "tls"
// LoadCertificate will load a certificate from disk and return a tls.Certificate or error
func LoadCertificate(config *CertificateConfig) (*tls.Certificate, error) {
if err := config.Validate(); err != nil {
return nil, err
}
certificate := config.Certificate
key := config.Key
if certificate == "" {
return nil, nil
}
log := logp.NewLogger(logSelector)
passphrase := config.Passphrase
if passphrase == "" && config.PassphrasePath != "" {
p, err := os.ReadFile(config.PassphrasePath)
if err != nil {
return nil, fmt.Errorf("unable to read passphrase_file: %w", err)
}
passphrase = string(p)
}
certPEM, err := ReadPEMFile(log, certificate, passphrase)
if err != nil {
log.Errorf("Failed reading certificate file %v: %+v", certificate, err)
return nil, fmt.Errorf("%w %v", err, certificate)
}
keyPEM, err := ReadPEMFile(log, key, passphrase)
if err != nil {
log.Errorf("Failed reading key file: %+v", err)
return nil, fmt.Errorf("%w %v", err, key)
}
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
log.Errorf("Failed loading client certificate %+v", err)
return nil, err
}
// Do not log the key if it was provided as a string in the configuration to avoid
// leaking private keys in the debug logs. Log when the key is a file path.
if IsPEMString(key) {
log.Debugf("Loading certificate: %v with key from PEM string in config", certificate)
} else {
log.Debugf("Loading certificate: %v and key %v", certificate, key)
}
return &cert, nil
}
// ReadPEMFile reads a PEM formatted string either from disk or passed as a plain text starting with a "-"
// and decrypt it with the provided password and return the raw content.
func ReadPEMFile(log *logp.Logger, s, passphrase string) ([]byte, error) {
pass := []byte(passphrase)
var blocks []*pem.Block
r, err := NewPEMReader(s)
if err != nil {
return nil, err
}
defer r.Close()
content, err := io.ReadAll(r)
if err != nil {
return nil, err
}
var errs error
for len(content) > 0 {
var block *pem.Block
block, content = pem.Decode(content)
if block == nil {
if len(blocks) == 0 {
return nil, errors.New("no pem file")
}
break
}
switch {
case x509.IsEncryptedPEMBlock(block): //nolint: staticcheck // deprecated, we have to get rid of it
block, err := decryptPKCS1Key(*block, pass)
if err != nil {
log.Errorf("Dropping encrypted pem block with private key, block type '%s': %s", block.Type, err)
errs = errors.Join(errs, err)
continue
}
blocks = append(blocks, &block)
case block.Type == "ENCRYPTED PRIVATE KEY":
block, err := decryptPKCS8Key(*block, pass)
if err != nil {
log.Errorf("Dropping encrypted pem block with private key, block type '%s', could not decrypt as PKCS8: %s", block.Type, err)
errs = errors.Join(errs, err)
continue
}
blocks = append(blocks, &block)
default:
blocks = append(blocks, block)
}
}
if len(blocks) == 0 {
return nil, errors.Join(errors.New("no PEM blocks"), errs)
}
// re-encode available, decrypted blocks
buffer := bytes.NewBuffer(nil)
for _, block := range blocks {
err := pem.Encode(buffer, block)
if err != nil {
return nil, err
}
}
return buffer.Bytes(), nil
}
// LoadCertificateAuthorities read the slice of CAcert and return a Certpool.
func LoadCertificateAuthorities(CAs []string) (*x509.CertPool, []error) {
errors := []error{}
if len(CAs) == 0 {
return nil, nil
}
log := logp.NewLogger(logSelector)
roots := x509.NewCertPool()
for _, s := range CAs {
r, err := NewPEMReader(s)
if err != nil {
log.Errorf("Failed reading CA certificate: %+v", err)
errors = append(errors, fmt.Errorf("%w reading %v", err, r))
continue
}
defer r.Close()
pemData, err := io.ReadAll(r)
if err != nil {
log.Errorf("Failed reading CA certificate: %+v", err)
errors = append(errors, fmt.Errorf("%w reading %v", err, r))
continue
}
if ok := roots.AppendCertsFromPEM(pemData); !ok {
log.Error("Failed to add CA to the cert pool, CA is not a valid PEM document")
errors = append(errors, fmt.Errorf("%w adding %v to the list of known CAs", ErrNotACertificate, r))
continue
}
log.Debugf("Successfully loaded CA certificate: %v", r)
}
return roots, errors
}
func extractMinMaxVersion(versions []TLSVersion) (uint16, uint16) {
if len(versions) == 0 {
versions = TLSDefaultVersions
}
minVersion := uint16(0xffff)
maxVersion := uint16(0)
for _, version := range versions {
v := uint16(version)
if v < minVersion {
minVersion = v
}
if v > maxVersion {
maxVersion = v
}
}
return minVersion, maxVersion
}
// ResolveTLSVersion takes the integer representation and return the name.
func ResolveTLSVersion(v uint16) string {
return TLSVersion(v).String()
}
// ResolveCipherSuite takes the integer representation and return the cipher name.
func ResolveCipherSuite(cipher uint16) string {
return CipherSuite(cipher).String()
}
// PEMReader allows to read a certificate in PEM format either through the disk or from a string.
type PEMReader struct {
reader io.ReadCloser
debugStr string
}
// NewPEMReader returns a new PEMReader.
func NewPEMReader(certificate string) (*PEMReader, error) {
if IsPEMString(certificate) {
return &PEMReader{reader: io.NopCloser(strings.NewReader(certificate)), debugStr: "inline"}, nil
}
r, err := os.Open(certificate)
if err != nil {
return nil, err
}
return &PEMReader{reader: r, debugStr: certificate}, nil
}
// Close closes the target io.ReadCloser.
func (p *PEMReader) Close() error {
return p.reader.Close()
}
// Read read bytes from the io.ReadCloser.
func (p *PEMReader) Read(b []byte) (n int, err error) {
return p.reader.Read(b)
}
func (p *PEMReader) String() string {
return p.debugStr
}
// IsPEMString returns true if the provided string match a PEM formatted certificate. try to pem decode to validate.
func IsPEMString(s string) bool {
// Trim the certificates to make sure we tolerate any yaml weirdness, we assume that the string starts
// with "-" and let further validation verifies the PEM format.
return strings.HasPrefix(strings.TrimSpace(s), "-")
}