client.go raw

   1  // Copyright 2022 Google LLC.
   2  // Licensed under the Apache License, Version 2.0 (the "License");
   3  // you may not use this file except in compliance with the License.
   4  // You may obtain a copy of the License at
   5  //
   6  //     https://www.apache.org/licenses/LICENSE-2.0
   7  //
   8  // Unless required by applicable law or agreed to in writing, software
   9  // distributed under the License is distributed on an "AS IS" BASIS,
  10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11  // See the License for the specific language governing permissions and
  12  // limitations under the License.
  13  
  14  // Package client is a cross-platform client for the signer binary (a.k.a."EnterpriseCertSigner").
  15  //
  16  // The signer binary is OS-specific, but exposes a standard set of APIs for the client to use.
  17  package client
  18  
  19  import (
  20  	"crypto"
  21  	"crypto/ecdsa"
  22  	"crypto/rsa"
  23  	"crypto/x509"
  24  	"encoding/gob"
  25  	"errors"
  26  	"fmt"
  27  	"io"
  28  	"net/rpc"
  29  	"os"
  30  	"os/exec"
  31  
  32  	"github.com/googleapis/enterprise-certificate-proxy/client/util"
  33  )
  34  
  35  const signAPI = "EnterpriseCertSigner.Sign"
  36  const certificateChainAPI = "EnterpriseCertSigner.CertificateChain"
  37  const publicKeyAPI = "EnterpriseCertSigner.Public"
  38  const encryptAPI = "EnterpriseCertSigner.Encrypt"
  39  const decryptAPI = "EnterpriseCertSigner.Decrypt"
  40  
  41  // A Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser.
  42  type Connection struct {
  43  	io.ReadCloser
  44  	io.WriteCloser
  45  }
  46  
  47  // Close closes c's underlying ReadCloser and WriteCloser.
  48  func (c *Connection) Close() error {
  49  	rerr := c.ReadCloser.Close()
  50  	werr := c.WriteCloser.Close()
  51  	if rerr != nil {
  52  		return rerr
  53  	}
  54  	return werr
  55  }
  56  
  57  func init() {
  58  	gob.Register(crypto.SHA256)
  59  	gob.Register(crypto.SHA384)
  60  	gob.Register(crypto.SHA512)
  61  	gob.Register(&rsa.PSSOptions{})
  62  	gob.Register(&rsa.OAEPOptions{})
  63  }
  64  
  65  // SignArgs contains arguments for a Sign API call.
  66  type SignArgs struct {
  67  	Digest []byte            // The content to sign.
  68  	Opts   crypto.SignerOpts // Options for signing. Must implement HashFunc().
  69  }
  70  
  71  // EncryptArgs contains arguments for an Encrypt API call.
  72  type EncryptArgs struct {
  73  	Plaintext []byte // The plaintext to encrypt.
  74  	Opts      any    // Options for encryption. Ex: an instance of crypto.Hash.
  75  }
  76  
  77  // DecryptArgs contains arguments to for a Decrypt API call.
  78  type DecryptArgs struct {
  79  	Ciphertext []byte               // The ciphertext to decrypt.
  80  	Opts       crypto.DecrypterOpts // Options for decryption. Ex: an instance of *rsa.OAEPOptions.
  81  }
  82  
  83  // Key implements credential.Credential by holding the executed signer subprocess.
  84  type Key struct {
  85  	cmd       *exec.Cmd        // Pointer to the signer subprocess.
  86  	client    *rpc.Client      // Pointer to the rpc client that communicates with the signer subprocess.
  87  	publicKey crypto.PublicKey // Public key of loaded certificate.
  88  	chain     [][]byte         // Certificate chain of loaded certificate.
  89  }
  90  
  91  // CertificateChain returns the credential as a raw X509 cert chain. This contains the public key.
  92  func (k *Key) CertificateChain() [][]byte {
  93  	return k.chain
  94  }
  95  
  96  // Close closes the RPC connection and kills the signer subprocess.
  97  // Call this to free up resources when the Key object is no longer needed.
  98  func (k *Key) Close() error {
  99  	if err := k.cmd.Process.Kill(); err != nil {
 100  		return fmt.Errorf("failed to kill signer process: %w", err)
 101  	}
 102  	// Wait for cmd to exit and release resources. Since the process is forcefully killed, this
 103  	// will return a non-nil error (varies by OS), which we will ignore.
 104  	_ = k.cmd.Wait()
 105  	// The Pipes connecting the RPC client should have been closed when the signer subprocess was killed.
 106  	// Calling `k.client.Close()` before `k.cmd.Process.Kill()` or `k.cmd.Wait()` _will_ cause a segfault.
 107  	if err := k.client.Close(); err.Error() != "close |0: file already closed" {
 108  		return fmt.Errorf("failed to close RPC connection: %w", err)
 109  	}
 110  	return nil
 111  }
 112  
 113  // Public returns the public key for this Key.
 114  func (k *Key) Public() crypto.PublicKey {
 115  	return k.publicKey
 116  }
 117  
 118  // Sign signs a message digest, using the specified signer opts. Implements crypto.Signer interface.
 119  func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signed []byte, err error) {
 120  	if opts != nil && opts.HashFunc() != 0 && len(digest) != opts.HashFunc().Size() {
 121  		return nil, fmt.Errorf("Digest length of %v bytes does not match Hash function size of %v bytes", len(digest), opts.HashFunc().Size())
 122  	}
 123  	err = k.client.Call(signAPI, SignArgs{Digest: digest, Opts: opts}, &signed)
 124  	return
 125  }
 126  
 127  // Encrypt encrypts a plaintext msg into ciphertext, using the specified encrypt opts.
 128  func (k *Key) Encrypt(_ io.Reader, msg []byte, opts any) (ciphertext []byte, err error) {
 129  	err = k.client.Call(encryptAPI, EncryptArgs{Plaintext: msg, Opts: opts}, &ciphertext)
 130  	return
 131  }
 132  
 133  // Decrypt decrypts a ciphertext msg into plaintext, using the specified decrypter opts. Implements crypto.Decrypter interface.
 134  func (k *Key) Decrypt(_ io.Reader, msg []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) {
 135  	err = k.client.Call(decryptAPI, DecryptArgs{Ciphertext: msg, Opts: opts}, &plaintext)
 136  	return
 137  }
 138  
 139  // ErrCredUnavailable is a sentinel error that indicates ECP Cred is unavailable,
 140  // possibly due to missing config or missing binary path.
 141  var ErrCredUnavailable = errors.New("Cred is unavailable")
 142  
 143  // Cred spawns a signer subprocess that listens on stdin/stdout to perform certificate
 144  // related operations, including signing messages with the private key.
 145  //
 146  // The signer binary path is read from the specified configFilePath, if provided.
 147  // Otherwise, use the default config file path.
 148  //
 149  // The config file also specifies which certificate the signer should use.
 150  func Cred(configFilePath string) (*Key, error) {
 151  	if configFilePath == "" {
 152  		envFilePath := util.GetConfigFilePathFromEnv()
 153  		if envFilePath != "" {
 154  			configFilePath = envFilePath
 155  		} else {
 156  			configFilePath = util.GetDefaultConfigFilePath()
 157  		}
 158  	}
 159  	enterpriseCertSignerPath, err := util.LoadSignerBinaryPath(configFilePath)
 160  	if err != nil {
 161  		if errors.Is(err, util.ErrConfigUnavailable) {
 162  			return nil, ErrCredUnavailable
 163  		}
 164  		return nil, err
 165  	}
 166  	k := &Key{
 167  		cmd: exec.Command(enterpriseCertSignerPath, configFilePath),
 168  	}
 169  
 170  	// Redirect errors from subprocess to parent process.
 171  	k.cmd.Stderr = os.Stderr
 172  
 173  	// RPC client will communicate with subprocess over stdin/stdout.
 174  	kin, err := k.cmd.StdinPipe()
 175  	if err != nil {
 176  		return nil, err
 177  	}
 178  	kout, err := k.cmd.StdoutPipe()
 179  	if err != nil {
 180  		return nil, err
 181  	}
 182  	k.client = rpc.NewClient(&Connection{kout, kin})
 183  
 184  	if err := k.cmd.Start(); err != nil {
 185  		return nil, fmt.Errorf("starting enterprise cert signer subprocess: %w", err)
 186  	}
 187  
 188  	if err := k.client.Call(certificateChainAPI, struct{}{}, &k.chain); err != nil {
 189  		return nil, fmt.Errorf("failed to retrieve certificate chain: %w", err)
 190  	}
 191  
 192  	var publicKeyBytes []byte
 193  	if err := k.client.Call(publicKeyAPI, struct{}{}, &publicKeyBytes); err != nil {
 194  		return nil, fmt.Errorf("failed to retrieve public key: %w", err)
 195  	}
 196  
 197  	publicKey, err := x509.ParsePKIXPublicKey(publicKeyBytes)
 198  	if err != nil {
 199  		return nil, fmt.Errorf("failed to parse public key: %w", err)
 200  	}
 201  
 202  	var ok bool
 203  	k.publicKey, ok = publicKey.(crypto.PublicKey)
 204  	if !ok {
 205  		return nil, fmt.Errorf("invalid public key type: %T", publicKey)
 206  	}
 207  
 208  	switch pub := k.publicKey.(type) {
 209  	case *rsa.PublicKey:
 210  		if pub.Size() < 256 {
 211  			return nil, fmt.Errorf("RSA modulus size is less than 2048 bits: %v", pub.Size()*8)
 212  		}
 213  	case *ecdsa.PublicKey:
 214  	default:
 215  		return nil, fmt.Errorf("unsupported public key type: %v", pub)
 216  	}
 217  
 218  	return k, nil
 219  }
 220