secureconnect_cert.go 3.5 KB

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