...

Source file src/github.com/googleapis/enterprise-certificate-proxy/client/client.go

Documentation: github.com/googleapis/enterprise-certificate-proxy/client

     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  

View as plain text