secureconnect_cert.go raw

   1  // Copyright 2023 Google LLC
   2  //
   3  // Licensed under the Apache License, Version 2.0 (the "License");
   4  // you may not use this file except in compliance with the License.
   5  // You may obtain a copy of the License at
   6  //
   7  //      http://www.apache.org/licenses/LICENSE-2.0
   8  //
   9  // Unless required by applicable law or agreed to in writing, software
  10  // distributed under the License is distributed on an "AS IS" BASIS,
  11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12  // See the License for the specific language governing permissions and
  13  // limitations under the License.
  14  
  15  package cert
  16  
  17  import (
  18  	"crypto/tls"
  19  	"crypto/x509"
  20  	"encoding/json"
  21  	"errors"
  22  	"fmt"
  23  	"os"
  24  	"os/exec"
  25  	"os/user"
  26  	"path/filepath"
  27  	"sync"
  28  	"time"
  29  )
  30  
  31  const (
  32  	metadataPath = ".secureConnect"
  33  	metadataFile = "context_aware_metadata.json"
  34  )
  35  
  36  type secureConnectSource struct {
  37  	metadata secureConnectMetadata
  38  
  39  	// Cache the cert to avoid executing helper command repeatedly.
  40  	cachedCertMutex sync.Mutex
  41  	cachedCert      *tls.Certificate
  42  }
  43  
  44  type secureConnectMetadata struct {
  45  	Cmd []string `json:"cert_provider_command"`
  46  }
  47  
  48  // NewSecureConnectProvider creates a certificate source using
  49  // the Secure Connect Helper and its associated metadata file.
  50  //
  51  // The configFilePath points to the location of the context aware metadata file.
  52  // If configFilePath is empty, use the default context aware metadata location.
  53  func NewSecureConnectProvider(configFilePath string) (Provider, error) {
  54  	if configFilePath == "" {
  55  		user, err := user.Current()
  56  		if err != nil {
  57  			// Error locating the default config means Secure Connect is not supported.
  58  			return nil, errSourceUnavailable
  59  		}
  60  		configFilePath = filepath.Join(user.HomeDir, metadataPath, metadataFile)
  61  	}
  62  
  63  	file, err := os.ReadFile(configFilePath)
  64  	if err != nil {
  65  		// Config file missing means Secure Connect is not supported.
  66  		// There are non-os.ErrNotExist errors that may be returned.
  67  		// (e.g. if the home directory is /dev/null, *nix systems will
  68  		// return ENOTDIR instead of ENOENT)
  69  		return nil, errSourceUnavailable
  70  	}
  71  
  72  	var metadata secureConnectMetadata
  73  	if err := json.Unmarshal(file, &metadata); err != nil {
  74  		return nil, fmt.Errorf("cert: could not parse JSON in %q: %w", configFilePath, err)
  75  	}
  76  	if err := validateMetadata(metadata); err != nil {
  77  		return nil, fmt.Errorf("cert: invalid config in %q: %w", configFilePath, err)
  78  	}
  79  	return (&secureConnectSource{
  80  		metadata: metadata,
  81  	}).getClientCertificate, nil
  82  }
  83  
  84  func validateMetadata(metadata secureConnectMetadata) error {
  85  	if len(metadata.Cmd) == 0 {
  86  		return errors.New("empty cert_provider_command")
  87  	}
  88  	return nil
  89  }
  90  
  91  func (s *secureConnectSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
  92  	s.cachedCertMutex.Lock()
  93  	defer s.cachedCertMutex.Unlock()
  94  	if s.cachedCert != nil && !isCertificateExpired(s.cachedCert) {
  95  		return s.cachedCert, nil
  96  	}
  97  	// Expand OS environment variables in the cert provider command such as "$HOME".
  98  	for i := 0; i < len(s.metadata.Cmd); i++ {
  99  		s.metadata.Cmd[i] = os.ExpandEnv(s.metadata.Cmd[i])
 100  	}
 101  	command := s.metadata.Cmd
 102  	data, err := exec.Command(command[0], command[1:]...).Output()
 103  	if err != nil {
 104  		return nil, err
 105  	}
 106  	cert, err := tls.X509KeyPair(data, data)
 107  	if err != nil {
 108  		return nil, err
 109  	}
 110  	s.cachedCert = &cert
 111  	return &cert, nil
 112  }
 113  
 114  // isCertificateExpired returns true if the given cert is expired or invalid.
 115  func isCertificateExpired(cert *tls.Certificate) bool {
 116  	if len(cert.Certificate) == 0 {
 117  		return true
 118  	}
 119  	parsed, err := x509.ParseCertificate(cert.Certificate[0])
 120  	if err != nil {
 121  		return true
 122  	}
 123  	return time.Now().After(parsed.NotAfter)
 124  }
 125