client.go

  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.
 17package client
 18
 19import (
 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
 35const signAPI = "EnterpriseCertSigner.Sign"
 36const certificateChainAPI = "EnterpriseCertSigner.CertificateChain"
 37const publicKeyAPI = "EnterpriseCertSigner.Public"
 38const encryptAPI = "EnterpriseCertSigner.Encrypt"
 39const decryptAPI = "EnterpriseCertSigner.Decrypt"
 40
 41// A Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser.
 42type Connection struct {
 43	io.ReadCloser
 44	io.WriteCloser
 45}
 46
 47// Close closes c's underlying ReadCloser and WriteCloser.
 48func (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
 57func 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.
 66type 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.
 72type 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.
 78type 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.
 84type 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.
 92func (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.
 98func (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.
114func (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.
119func (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.
128func (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.
134func (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.
141var 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.
150func 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}