github.com/Venafi/vcert/v5@v5.10.2/pkg/playbook/app/installer/capi.go (about) 1 //go:build windows 2 3 /* 4 * Copyright 2023 Venafi, Inc. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package installer 20 21 import ( 22 "fmt" 23 "strings" 24 25 "go.uber.org/zap" 26 27 "github.com/Venafi/vcert/v5/pkg/certificate" 28 "github.com/Venafi/vcert/v5/pkg/playbook/app/domain" 29 "github.com/Venafi/vcert/v5/pkg/playbook/app/vcertutil" 30 "github.com/Venafi/vcert/v5/pkg/playbook/util" 31 "github.com/Venafi/vcert/v5/pkg/playbook/util/capistore" 32 ) 33 34 // CAPIInstaller represents an installation that will happen in the Windows CAPI store 35 type CAPIInstaller struct { 36 domain.Installation 37 } 38 39 // NewCAPIInstaller returns a new installer of type CAPI with the values defined in inst 40 func NewCAPIInstaller(inst domain.Installation) CAPIInstaller { 41 return CAPIInstaller{inst} 42 } 43 44 // Check is the method in charge of making the validations to install a new certificate: 45 // 1. Does the certificate exists? > Install if it doesn't. 46 // 2. Does the certificate is about to expire? Renew if about to expire. 47 // Returns true if the certificate needs to be installed. 48 func (r CAPIInstaller) Check(renewBefore string, request domain.PlaybookRequest) (bool, error) { 49 zap.L().Info("checking certificate health", zap.String("format", r.Type.String()), zap.String("location", r.CAPILocation)) 50 51 // Get friendly name. If no friendly name is set, get CN from request as friendly name. 52 // NOTE: This functionality is deprecated, and in a future version will be removed, and CAPIFriendlyName will be req'd 53 friendlyName := r.CAPIFriendlyName 54 if friendlyName == "" { 55 friendlyName = request.Subject.CommonName 56 } 57 58 // Get location from CAPILocation. If CAPILocation is not set, check deprecated Location field 59 location := r.CAPILocation 60 if location == "" { 61 location = r.Location 62 } 63 64 storeLocation, storeName, err := getCertStore(location) 65 if err != nil { 66 zap.L().Error("failed to get certificate store", zap.Error(err)) 67 return true, err 68 } 69 70 config := capistore.InstallationConfig{ 71 FriendlyName: friendlyName, 72 StoreLocation: storeLocation, 73 StoreName: storeName, 74 } 75 76 ps := capistore.NewPowerShell() 77 78 certPem, err := ps.RetrieveCertificateFromCAPI(config) 79 if err != nil { 80 zap.L().Error("failed to retrieve certificate from CAPI store", zap.Error(err)) 81 return true, err 82 } 83 84 // Certificate was not found. 85 if certPem == "" { 86 zap.L().Info("certificate not found") 87 return true, nil 88 } 89 90 // Check certificate expiration 91 cert, err := parsePEMCertificate([]byte(certPem)) 92 if err != nil { 93 return false, err 94 } 95 96 // Check certificate expiration 97 renew := needRenewal(cert, renewBefore) 98 99 return renew, nil 100 } 101 102 // Backup takes the certificate request and backs up the current version prior to overwriting 103 func (r CAPIInstaller) Backup() error { 104 zap.L().Debug("certificate is backed up by default for CAPI") 105 return nil 106 } 107 108 // Install takes the certificate bundle and moves it to the location specified in the installer 109 func (r CAPIInstaller) Install(pcc certificate.PEMCollection) error { 110 zap.L().Debug("installing certificate", zap.String("location", r.CAPILocation)) 111 112 // Generate random password for temporary P12 bundle 113 bundlePassword := vcertutil.GeneratePassword() 114 115 content, err := packageAsPKCS12(pcc, bundlePassword, r.UseLegacyP12) 116 if err != nil { 117 zap.L().Error("could not package certificate as PKCS12", zap.Error(err)) 118 return err 119 } 120 121 // Get friendly name. If no friendly name is set, get CN from certificate as friendly name 122 friendlyName := r.CAPIFriendlyName 123 if friendlyName == "" { 124 friendlyName, err = getCertFriendlyName([]byte(pcc.Certificate)) 125 if err != nil { 126 return err 127 } 128 } 129 130 // Get location from CAPILocation. If CAPILocation is not set, check deprecated Location field 131 location := r.CAPILocation 132 if location == "" { 133 location = r.Location 134 } 135 136 storeLocation, storeName, err := getCertStore(location) 137 if err != nil { 138 zap.L().Error("failed to get certificate store", zap.Error(err)) 139 return err 140 } 141 142 config := capistore.InstallationConfig{ 143 PFX: content, 144 FriendlyName: friendlyName, 145 IsNonExportable: r.CAPIIsNonExportable, 146 Password: bundlePassword, 147 StoreLocation: storeLocation, 148 StoreName: storeName, 149 } 150 151 ps := capistore.NewPowerShell() 152 153 err = ps.InstallCertificateToCAPI(config) 154 if err != nil { 155 zap.L().Error("failed to install certificate in CAPI store", zap.Error(err)) 156 return err 157 } 158 159 return nil 160 } 161 162 // AfterInstallActions runs any instructions declared in the Installer on a terminal. 163 // 164 // No validations happen over the content of the AfterAction string, so caution is advised 165 func (r CAPIInstaller) AfterInstallActions() (string, error) { 166 zap.L().Debug("running after-install actions", zap.String("location", r.CAPILocation)) 167 168 result, err := util.ExecuteScript(r.AfterAction) 169 return result, err 170 } 171 172 // InstallValidationActions runs any instructions declared in the Installer on a terminal and expects 173 // "0" for successful validation and "1" for a validation failure 174 // No validations happen over the content of the InstallValidation string, so caution is advised 175 func (r CAPIInstaller) InstallValidationActions() (string, error) { 176 zap.L().Debug("running install validation actions", zap.String("location", r.CAPILocation)) 177 validationResult, err := util.ExecuteScript(r.InstallValidation) 178 if err != nil { 179 return "", err 180 } 181 182 return validationResult, err 183 } 184 185 func getCertStore(location string) (string, string, error) { 186 segments := strings.Split(location, "\\") 187 188 if len(segments) != 2 { 189 return "", "", fmt.Errorf("invalid CAPI location: '%s'. Should be in form of 'StoreLocation\\StoreName' (i.e. 'LocalMachine\\My')", location) 190 } 191 192 return segments[0], segments[1], nil 193 } 194 195 func getCertFriendlyName(cert []byte) (string, error) { 196 x509Cert, err := parsePEMCertificate(cert) 197 if err != nil { 198 return "", fmt.Errorf("failed to get friendly name from certificate: %w", err) 199 } 200 return x509Cert.Subject.CommonName, nil 201 }