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 }