secureconnect_cert.go raw

   1  // Copyright 2022 Google LLC.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  // Package cert contains certificate tools for Google API clients.
   6  // This package is intended to be used with crypto/tls.Config.GetClientCertificate.
   7  //
   8  // The certificates can be used to satisfy Google's Endpoint Validation.
   9  // See https://cloud.google.com/endpoint-verification/docs/overview
  10  //
  11  // This package is not intended for use by end developers. Use the
  12  // google.golang.org/api/option package to configure API clients.
  13  package cert
  14  
  15  import (
  16  	"crypto/tls"
  17  	"crypto/x509"
  18  	"encoding/json"
  19  	"errors"
  20  	"fmt"
  21  	"os"
  22  	"os/exec"
  23  	"os/user"
  24  	"path/filepath"
  25  	"sync"
  26  	"time"
  27  )
  28  
  29  const (
  30  	metadataPath = ".secureConnect"
  31  	metadataFile = "context_aware_metadata.json"
  32  )
  33  
  34  type secureConnectSource struct {
  35  	metadata secureConnectMetadata
  36  
  37  	// Cache the cert to avoid executing helper command repeatedly.
  38  	cachedCertMutex sync.Mutex
  39  	cachedCert      *tls.Certificate
  40  }
  41  
  42  type secureConnectMetadata struct {
  43  	Cmd []string `json:"cert_provider_command"`
  44  }
  45  
  46  // NewSecureConnectSource creates a certificate source using
  47  // the Secure Connect Helper and its associated metadata file.
  48  //
  49  // The configFilePath points to the location of the context aware metadata file.
  50  // If configFilePath is empty, use the default context aware metadata location.
  51  func NewSecureConnectSource(configFilePath string) (Source, error) {
  52  	if configFilePath == "" {
  53  		user, err := user.Current()
  54  		if err != nil {
  55  			// Error locating the default config means Secure Connect is not supported.
  56  			return nil, errSourceUnavailable
  57  		}
  58  		configFilePath = filepath.Join(user.HomeDir, metadataPath, metadataFile)
  59  	}
  60  
  61  	file, err := os.ReadFile(configFilePath)
  62  	if err != nil {
  63  		if errors.Is(err, os.ErrNotExist) {
  64  			// Config file missing means Secure Connect is not supported.
  65  			return nil, errSourceUnavailable
  66  		}
  67  		return nil, err
  68  	}
  69  
  70  	var metadata secureConnectMetadata
  71  	if err := json.Unmarshal(file, &metadata); err != nil {
  72  		return nil, fmt.Errorf("cert: could not parse JSON in %q: %w", configFilePath, err)
  73  	}
  74  	if err := validateMetadata(metadata); err != nil {
  75  		return nil, fmt.Errorf("cert: invalid config in %q: %w", configFilePath, err)
  76  	}
  77  	return (&secureConnectSource{
  78  		metadata: metadata,
  79  	}).getClientCertificate, nil
  80  }
  81  
  82  func validateMetadata(metadata secureConnectMetadata) error {
  83  	if len(metadata.Cmd) == 0 {
  84  		return errors.New("empty cert_provider_command")
  85  	}
  86  	return nil
  87  }
  88  
  89  func (s *secureConnectSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
  90  	s.cachedCertMutex.Lock()
  91  	defer s.cachedCertMutex.Unlock()
  92  	if s.cachedCert != nil && !isCertificateExpired(s.cachedCert) {
  93  		return s.cachedCert, nil
  94  	}
  95  	// Expand OS environment variables in the cert provider command such as "$HOME".
  96  	for i := 0; i < len(s.metadata.Cmd); i++ {
  97  		s.metadata.Cmd[i] = os.ExpandEnv(s.metadata.Cmd[i])
  98  	}
  99  	command := s.metadata.Cmd
 100  	data, err := exec.Command(command[0], command[1:]...).Output()
 101  	if err != nil {
 102  		return nil, err
 103  	}
 104  	cert, err := tls.X509KeyPair(data, data)
 105  	if err != nil {
 106  		return nil, err
 107  	}
 108  	s.cachedCert = &cert
 109  	return &cert, nil
 110  }
 111  
 112  // isCertificateExpired returns true if the given cert is expired or invalid.
 113  func isCertificateExpired(cert *tls.Certificate) bool {
 114  	if len(cert.Certificate) == 0 {
 115  		return true
 116  	}
 117  	parsed, err := x509.ParseCertificate(cert.Certificate[0])
 118  	if err != nil {
 119  		return true
 120  	}
 121  	return time.Now().After(parsed.NotAfter)
 122  }
 123