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 }