github.com/Venafi/vcert/v5@v5.10.2/pkg/playbook/util/capistore/powershell.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 capistore
    20  
    21  import (
    22  	"bytes"
    23  	_ "embed"
    24  	"fmt"
    25  	"os"
    26  	"os/exec"
    27  	"strings"
    28  
    29  	"github.com/google/uuid"
    30  	"github.com/pkg/errors"
    31  	"go.uber.org/zap"
    32  )
    33  
    34  var (
    35  	//go:embed embedded/install-cert.ps1
    36  	installCertScript string
    37  	//go:embed embedded/retrieve-cert.ps1
    38  	retrieveCertScript string
    39  )
    40  
    41  // PowerShell represents the powershell program in Windows. It is used to execute any script on it
    42  type PowerShell struct {
    43  	powerShell string
    44  }
    45  
    46  // NewPowerShell creates new session
    47  func NewPowerShell() *PowerShell {
    48  	ps, err := exec.LookPath("powershell.exe")
    49  	if err != nil {
    50  		zap.L().Fatal("could not find powershell path", zap.Error(err))
    51  	}
    52  	return &PowerShell{
    53  		powerShell: ps,
    54  	}
    55  }
    56  
    57  // InstallCertificateToCAPI takes a config object  and uses it to install a new certificate in the local machine CAPI store
    58  func (ps PowerShell) InstallCertificateToCAPI(config InstallationConfig) error {
    59  	pfxPath := fmt.Sprintf("%s\\%s", os.TempDir(), uuid.NewString())
    60  
    61  	// verify friendly name doesn't have command injection
    62  	err := containsInjectableData(config.FriendlyName)
    63  	if err != nil {
    64  		m := "failed to install certificate because of invalid characters in friendlyName"
    65  		zap.L().Error(m)
    66  		return errors.WithMessagef(err, m)
    67  	}
    68  
    69  	err = os.WriteFile(pfxPath, config.PFX, 0600)
    70  	if err != nil {
    71  		zap.L().Error("could not create certificate temp file", zap.Error(err))
    72  		return err
    73  	}
    74  
    75  	defer func() {
    76  		if delErr := os.RemoveAll(pfxPath); delErr != nil {
    77  			// Failing to delete a file containing a private key should be considered an error
    78  			zap.L().Error("failed to delete temporary certificate file", zap.Error(delErr))
    79  		}
    80  	}()
    81  
    82  	params := map[string]string{
    83  		"certPath":        pfxPath,
    84  		"friendlyName":    config.FriendlyName,
    85  		"isNonExportable": psBool(config.IsNonExportable),
    86  		"password":        config.Password,
    87  		"storeName":       config.StoreName,
    88  		"storeLocation":   config.StoreLocation,
    89  	}
    90  
    91  	stdout, err := ps.executeScript(installCertScript, "install-cert", params)
    92  	if err != nil {
    93  		m := "failed to install certificate into CAPI"
    94  		zap.L().Error(m, zap.String("stdout", stdout), zap.Error(err))
    95  		return errors.WithMessagef(err, "%s, stdout: '%s'", m, stdout)
    96  	}
    97  
    98  	return err
    99  }
   100  
   101  // RetrieveCertificateFromCAPI looks for a certificate in the CAPI store config.CertStore that matches the given config.FriendlyName.
   102  // If found, it returns the certificate in PEM format as a string
   103  func (ps PowerShell) RetrieveCertificateFromCAPI(config InstallationConfig) (string, error) {
   104  	zap.L().Info("retrieving certificate from CAPI Store", zap.String("friendlyName", config.FriendlyName))
   105  
   106  	// verify friendly name doesn't have command injection
   107  	err := containsInjectableData(config.FriendlyName)
   108  	if err != nil {
   109  		m := "failed to retrieve certificate because of invalid characters in friendlyName"
   110  		zap.L().Error(m)
   111  		return "", errors.WithMessagef(err, m)
   112  	}
   113  
   114  	params := map[string]string{
   115  		"friendlyName":  config.FriendlyName,
   116  		"storeName":     config.StoreName,
   117  		"storeLocation": config.StoreLocation,
   118  	}
   119  
   120  	stdout, err := ps.executeScript(retrieveCertScript, "retrieve-cert", params)
   121  	if err != nil {
   122  		m := "failed to install certificate into CAPI"
   123  		zap.L().Error(m, zap.String("stdout", stdout), zap.Error(err))
   124  		return "", errors.WithMessagef(err, "%s, stdout: '%s'", m, stdout)
   125  	}
   126  
   127  	//Certificate not found, return empty string
   128  	notFound := fmt.Sprintf("certificate not found: %s", config.FriendlyName)
   129  	if strings.Contains(stdout, notFound) {
   130  		return "", nil
   131  	}
   132  
   133  	return stdout, nil
   134  }
   135  
   136  // ExecuteScript runs the specified powershell script function found within the script.
   137  // String parameters can be specified as named arguments to the function.
   138  // Parameters have a limited size, large parameters should be first read from disk to avoid command size limits.
   139  func (ps PowerShell) executeScript(script, functionName string, parameters map[string]string) (string, error) {
   140  	scriptFile := fmt.Sprintf("venafi-winrm-execute-%s.ps1", uuid.NewString())
   141  
   142  	scriptPath := fmt.Sprintf("%s\\%s", os.TempDir(), scriptFile)
   143  
   144  	err := copyScript(script, scriptPath)
   145  	if err != nil {
   146  		m := "failed to copy script"
   147  		zap.L().Error(m)
   148  		return "", errors.WithMessagef(err, m)
   149  	}
   150  	defer func() {
   151  		if removeErr := os.RemoveAll(scriptPath); removeErr != nil {
   152  			zap.L().Warn("failed to remove powershell script from host", zap.Error(removeErr))
   153  		}
   154  	}()
   155  
   156  	stdout, err := ps.runScript(scriptPath, functionName, parameters)
   157  	if err != nil {
   158  		m := "failed to run script function"
   159  		zap.L().Error(m, zap.String("functionName", functionName), zap.String("stdout", stdout), zap.Error(err))
   160  		return "", errors.WithMessagef(err, "%s %q", m, functionName)
   161  	}
   162  
   163  	return stdout, nil
   164  }
   165  
   166  func (ps PowerShell) runScript(scriptPath, functionName string, parameters map[string]string) (string, error) {
   167  
   168  	builder := strings.Builder{}
   169  	builder.WriteString(fmt.Sprintf("powershell -command \". %s; %s", scriptPath, functionName))
   170  	for paramName, value := range parameters {
   171  		builder.WriteString(fmt.Sprintf(" -%s %s", paramName, quoteIfNeeded(value)))
   172  	}
   173  	builder.WriteString("\"")
   174  
   175  	script := builder.String()
   176  
   177  	cmd := exec.Command(ps.powerShell, script)
   178  	var stdOut, stdError bytes.Buffer
   179  	cmd.Stdout = &stdOut
   180  	cmd.Stderr = &stdError
   181  	err := cmd.Run()
   182  
   183  	errString := "failed to run script file"
   184  	if len(stdError.String()) != 0 {
   185  		zap.L().Error(errString, zap.String("stderr", stdError.String()))
   186  		return "", fmt.Errorf("%s: %s", errString, stdError.String())
   187  	}
   188  
   189  	if err != nil {
   190  		zap.L().Error(errString, zap.Error(err))
   191  		return "", fmt.Errorf("%s: %w", errString, err)
   192  	}
   193  
   194  	return stdOut.String(), nil
   195  }