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  }