123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- // Copyright 2022 Google LLC.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- // Package cert contains certificate tools for Google API clients.
- // This package is intended to be used with crypto/tls.Config.GetClientCertificate.
- //
- // The certificates can be used to satisfy Google's Endpoint Validation.
- // See https://cloud.google.com/endpoint-verification/docs/overview
- //
- // This package is not intended for use by end developers. Use the
- // google.golang.org/api/option package to configure API clients.
- package cert
- import (
- "crypto/tls"
- "crypto/x509"
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "os/exec"
- "os/user"
- "path/filepath"
- "sync"
- "time"
- )
- const (
- metadataPath = ".secureConnect"
- metadataFile = "context_aware_metadata.json"
- )
- type secureConnectSource struct {
- metadata secureConnectMetadata
- // Cache the cert to avoid executing helper command repeatedly.
- cachedCertMutex sync.Mutex
- cachedCert *tls.Certificate
- }
- type secureConnectMetadata struct {
- Cmd []string `json:"cert_provider_command"`
- }
- // NewSecureConnectSource creates a certificate source using
- // the Secure Connect Helper and its associated metadata file.
- //
- // The configFilePath points to the location of the context aware metadata file.
- // If configFilePath is empty, use the default context aware metadata location.
- func NewSecureConnectSource(configFilePath string) (Source, error) {
- if configFilePath == "" {
- user, err := user.Current()
- if err != nil {
- // Error locating the default config means Secure Connect is not supported.
- return nil, errSourceUnavailable
- }
- configFilePath = filepath.Join(user.HomeDir, metadataPath, metadataFile)
- }
- file, err := os.ReadFile(configFilePath)
- if err != nil {
- if errors.Is(err, os.ErrNotExist) {
- // Config file missing means Secure Connect is not supported.
- return nil, errSourceUnavailable
- }
- return nil, err
- }
- var metadata secureConnectMetadata
- if err := json.Unmarshal(file, &metadata); err != nil {
- return nil, fmt.Errorf("cert: could not parse JSON in %q: %w", configFilePath, err)
- }
- if err := validateMetadata(metadata); err != nil {
- return nil, fmt.Errorf("cert: invalid config in %q: %w", configFilePath, err)
- }
- return (&secureConnectSource{
- metadata: metadata,
- }).getClientCertificate, nil
- }
- func validateMetadata(metadata secureConnectMetadata) error {
- if len(metadata.Cmd) == 0 {
- return errors.New("empty cert_provider_command")
- }
- return nil
- }
- func (s *secureConnectSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
- s.cachedCertMutex.Lock()
- defer s.cachedCertMutex.Unlock()
- if s.cachedCert != nil && !isCertificateExpired(s.cachedCert) {
- return s.cachedCert, nil
- }
- // Expand OS environment variables in the cert provider command such as "$HOME".
- for i := 0; i < len(s.metadata.Cmd); i++ {
- s.metadata.Cmd[i] = os.ExpandEnv(s.metadata.Cmd[i])
- }
- command := s.metadata.Cmd
- data, err := exec.Command(command[0], command[1:]...).Output()
- if err != nil {
- return nil, err
- }
- cert, err := tls.X509KeyPair(data, data)
- if err != nil {
- return nil, err
- }
- s.cachedCert = &cert
- return &cert, nil
- }
- // isCertificateExpired returns true if the given cert is expired or invalid.
- func isCertificateExpired(cert *tls.Certificate) bool {
- if len(cert.Certificate) == 0 {
- return true
- }
- parsed, err := x509.ParseCertificate(cert.Certificate[0])
- if err != nil {
- return true
- }
- return time.Now().After(parsed.NotAfter)
- }
|