secureconnect_cert.go 3.4 KB

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