github.com/Venafi/vcert/v5@v5.10.2/pkg/playbook/app/installer/crypto.go (about)

     1  /*
     2   * Copyright 2023 Venafi, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *  http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package installer
    18  
    19  import (
    20  	"crypto"
    21  	// nolint:gosec // TODO: figure out a way to obtain cert thumbprint to remove the use of weak cryptographic primitive (G401)
    22  	"crypto/sha1"
    23  	"crypto/x509"
    24  	"encoding/hex"
    25  	"encoding/pem"
    26  	"fmt"
    27  	"os"
    28  	"strconv"
    29  	"strings"
    30  	"time"
    31  
    32  	"go.uber.org/zap"
    33  
    34  	"github.com/Venafi/vcert/v5/pkg/certificate"
    35  	"github.com/Venafi/vcert/v5/pkg/playbook/app/vcertutil"
    36  	"github.com/Venafi/vcert/v5/pkg/util"
    37  )
    38  
    39  // DayDuration represents a day (24 hours) in the Duration type
    40  const DayDuration = time.Hour * 24
    41  
    42  type Certificate struct {
    43  	X509cert   x509.Certificate
    44  	Thumbprint string
    45  }
    46  
    47  func getPrivateKey(privateKeyStr string, keyPassword string) (interface{}, error) {
    48  	//Getting Private Key
    49  	pkBlock, _ := pem.Decode([]byte(privateKeyStr))
    50  	if pkBlock == nil {
    51  		return nil, fmt.Errorf("missing Private Key PEM")
    52  	}
    53  
    54  	var err error
    55  	//Decrypting Private Key
    56  	pkDER := pkBlock.Bytes
    57  	if util.X509IsEncryptedPEMBlock(pkBlock) {
    58  		pkDER, err = util.X509DecryptPEMBlock(pkBlock, []byte(keyPassword))
    59  		if err != nil {
    60  			return nil, fmt.Errorf("private key decryption error: %w", err)
    61  		}
    62  	}
    63  
    64  	//Unmarshalling the Private Key
    65  	var privateKey interface{}
    66  	switch pkBlock.Type {
    67  	case "EC PRIVATE KEY":
    68  		privateKey, err = x509.ParseECPrivateKey(pkDER)
    69  		if err != nil {
    70  			privateKey, err = x509.ParsePKCS8PrivateKey(pkDER)
    71  		}
    72  	case "RSA PRIVATE KEY":
    73  		privateKey, err = x509.ParsePKCS1PrivateKey(pkDER)
    74  		if err != nil {
    75  			privateKey, err = x509.ParsePKCS8PrivateKey(pkDER)
    76  		}
    77  	default:
    78  		return nil, fmt.Errorf("unexpected Private Key type: %s", pkBlock.Type)
    79  	}
    80  
    81  	if err != nil {
    82  		return nil, fmt.Errorf("private key error: %w", err)
    83  	}
    84  
    85  	return privateKey, nil
    86  }
    87  
    88  func prepareCertificateForBundle(request certificate.Request, pcc certificate.PEMCollection, decryptPK bool) (*certificate.PEMCollection, error) {
    89  	// Private key generated locally. Need to add it to the PEM Collection
    90  	if request.CsrOrigin == certificate.LocalGeneratedCSR {
    91  		err := pcc.AddPrivateKey(request.PrivateKey, []byte(request.KeyPassword), "")
    92  		if err != nil {
    93  			return nil, err
    94  		}
    95  		zap.L().Debug("CSR Origin is [local]. Private Key added to PEM Collection")
    96  	}
    97  
    98  	// Key needs to be decrypted in order to create the bundle (PKCS12, JKS)
    99  	// Firefly does not encrypt Private Keys. Thus, Private Key should not be decrypted in that scenario
   100  	if pcc.PrivateKey != "" && decryptPK {
   101  
   102  		privateKey, err := vcertutil.DecryptPrivateKey(pcc.PrivateKey, request.KeyPassword)
   103  		if err != nil {
   104  			// there's chance we've got a private key that is not PKCS8
   105  			zap.L().Debug("PKCS8 decrypt failed for opening Private Key. Trying legacy decrypt")
   106  			privKey, err := getPrivateKey(pcc.PrivateKey, request.KeyPassword)
   107  			if err != nil {
   108  				return nil, err
   109  			}
   110  
   111  			var privKeyBlock *pem.Block
   112  
   113  			privKeyD := privKey.(crypto.Signer)
   114  			privKeyBlock, err = certificate.GetPrivateKeyPEMBock(privKeyD, util.LegacyPem)
   115  			if err != nil {
   116  				return nil, err
   117  			}
   118  			privEncodedPEM := pem.EncodeToMemory(privKeyBlock)
   119  			privKeyPEMString := string(privEncodedPEM)
   120  			pcc.PrivateKey = privKeyPEMString
   121  			privateKey = privKeyPEMString
   122  		}
   123  		pcc.PrivateKey = privateKey
   124  		zap.L().Debug("successfully decrypted Private Key")
   125  	}
   126  
   127  	return &pcc, nil
   128  }
   129  
   130  func loadPEMCertificate(certFile string) (*x509.Certificate, error) {
   131  	certData, err := os.ReadFile(certFile)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  
   136  	return parsePEMCertificate(certData)
   137  }
   138  
   139  func parsePEMCertificate(certData []byte) (*x509.Certificate, error) {
   140  	p, _ := pem.Decode(certData)
   141  	if p == nil {
   142  		return nil, fmt.Errorf("could not decode PEM data")
   143  	}
   144  	if p.Type != "CERTIFICATE" {
   145  		return nil, fmt.Errorf("certificate data does not contain a certificate")
   146  	}
   147  
   148  	cert, err := x509.ParseCertificate(p.Bytes)
   149  	if err != nil {
   150  		return nil, fmt.Errorf("could not parse certificate to X509 object: %w", err)
   151  	}
   152  
   153  	return cert, nil
   154  }
   155  
   156  func needRenewal(cert *x509.Certificate, renewBefore string) bool {
   157  	// if duration is 0 anything, then return false, auto-renewal is disabled
   158  	if renewBefore == "0" || strings.ToLower(renewBefore) == "disabled" {
   159  		zap.L().Warn("certificate expiring soon but automatic renewal disabled",
   160  			zap.String("certificate", cert.Subject.CommonName),
   161  			zap.String("expirationDate", cert.NotAfter.String()))
   162  		return false
   163  	}
   164  
   165  	timePostfix := renewBefore[len(renewBefore)-1:]
   166  
   167  	renew := renewBefore[:len(renewBefore)-1]
   168  	renewValue, err := strconv.ParseInt(renew, 10, 32)
   169  	if err != nil {
   170  		zap.L().Error("could not parse renewBefore value. Using default value [10%] instead",
   171  			zap.String("renewBefore", renewBefore), zap.Error(err))
   172  		// TODO: use real global default duration
   173  		timePostfix = "%"
   174  		renewValue = 10
   175  	}
   176  
   177  	// if duration is 0 anything, then return false, auto-renewal is disabled
   178  	if renewValue == 0 {
   179  		zap.L().Warn("certificate expiring soon but automatic renewal disabled",
   180  			zap.String("certificate", cert.Subject.CommonName),
   181  			zap.String("expirationDate", cert.NotAfter.String()))
   182  		return false
   183  	}
   184  
   185  	// Cert expired, renew
   186  	if cert.NotAfter.Before(time.Now()) {
   187  		zap.L().Debug("certificate is expired", zap.String("certificate", cert.Subject.CommonName))
   188  		return true
   189  	}
   190  
   191  	var timeToRenew time.Time
   192  
   193  	switch timePostfix {
   194  	case "d":
   195  		// operation happens in integers to avoid issues with linter and time.Duration struct
   196  		ns := DayDuration.Nanoseconds()
   197  		renewDuration := time.Duration(ns * renewValue)
   198  		timeToRenew = cert.NotAfter.Add(-renewDuration)
   199  	case "h":
   200  		ns := time.Hour.Nanoseconds()
   201  		renewDuration := time.Duration(ns * renewValue)
   202  		timeToRenew = cert.NotAfter.Add(-renewDuration)
   203  	case "%":
   204  		// Total # of ns in the whole certificate lifetime
   205  		nsCertValidity := cert.NotAfter.Sub(cert.NotBefore).Nanoseconds()
   206  
   207  		// if 10%, then renew when 90% of the validity time has elapsed
   208  		pct := float64(renewValue) / 100
   209  		renewDuration := time.Duration(float64(nsCertValidity) * pct)
   210  		timeToRenew = cert.NotAfter.Add(-renewDuration)
   211  	default:
   212  		zap.L().Warn("unknown duration postfix. Valid postfixes are: d (Days) and h (Hours): Using default [10%]",
   213  			zap.String("postfix", timePostfix))
   214  		// Total # of ns in the whole certificate lifetime
   215  		nsCertValidity := cert.NotAfter.Sub(cert.NotBefore).Nanoseconds()
   216  
   217  		// TODO: Respect global DefaultRenewal
   218  		renewDuration := time.Duration(float64(nsCertValidity) * float64(0.10))
   219  		timeToRenew = cert.NotAfter.Add(-renewDuration)
   220  	}
   221  
   222  	// Check certificate renew window
   223  	//Time now + renew window is bigger than cert expiration day? Then renew
   224  	if time.Now().After(timeToRenew) {
   225  		zap.L().Debug("certificate in renew window", zap.String("certificate", cert.Subject.CommonName))
   226  		return true
   227  	}
   228  
   229  	zap.L().Info(fmt.Sprintf("cert expires on %s and will auto-renew on %s", cert.NotAfter, timeToRenew),
   230  		zap.String("expirationDate", cert.NotAfter.String()), zap.String("renewDate", timeToRenew.String()))
   231  	return false
   232  }
   233  
   234  // CreateX509Cert takes a PEMCollection and creates an x509.Certificate object from it
   235  // Could also add the x509.Certificate object directly to the PEM collection in the original constructor
   236  func CreateX509Cert(pcc *certificate.PEMCollection, certReq *certificate.Request, decryptPK bool) (*Certificate, *certificate.PEMCollection, error) {
   237  	preparedPcc, err := prepareCertificateForBundle(*certReq, *pcc, decryptPK)
   238  
   239  	if err != nil {
   240  		return nil, nil, fmt.Errorf("could not prepare certificate and key: %w", err)
   241  	}
   242  
   243  	certBytes := []byte(pcc.Certificate)
   244  	x509Cert, err := parsePEMCertificate(certBytes)
   245  
   246  	if err != nil {
   247  		return nil, preparedPcc, err
   248  	}
   249  	// nolint:gosec // TODO: figure out a way to obtain cert thumbprint to remove the use of weak cryptographic primitive (G401)
   250  	thumbprint := sha1.Sum(x509Cert.Raw)
   251  	hexThumbprint := hex.EncodeToString(thumbprint[:])
   252  
   253  	cert := Certificate{*x509Cert, hexThumbprint}
   254  
   255  	return &cert, preparedPcc, nil
   256  }