diff --git a/go.mod b/go.mod index 470bd8a9231..3d79b98b876 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.15 require ( cloud.google.com/go/compute v1.6.1 github.com/google/go-cmp v0.5.8 + github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa github.com/googleapis/gax-go/v2 v2.4.0 go.opencensus.io v0.23.0 golang.org/x/net v0.0.0-20220607020251-c690dde0001d diff --git a/go.sum b/go.sum index 19fb9e52beb..351d84cf2b0 100644 --- a/go.sum +++ b/go.sum @@ -157,6 +157,8 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa h1:7MYGT2XEMam7Mtzv1yDUYXANedWvwk3HKkR3MyGowy8= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= diff --git a/transport/cert/default_cert.go b/transport/cert/default_cert.go index 04aefec0afa..21d0251531c 100644 --- a/transport/cert/default_cert.go +++ b/transport/cert/default_cert.go @@ -14,32 +14,19 @@ package cert import ( "crypto/tls" - "crypto/x509" - "encoding/json" "errors" - "fmt" - "io/ioutil" - "os" - "os/exec" - "os/user" - "path/filepath" "sync" - "time" -) - -const ( - metadataPath = ".secureConnect" - metadataFile = "context_aware_metadata.json" ) // defaultCertData holds all the variables pertaining to // the default certficate source created by DefaultSource. +// +// A singleton model is used to allow the source to be reused +// by the transport layer. type defaultCertData struct { - once sync.Once - source Source - err error - cachedCertMutex sync.Mutex - cachedCert *tls.Certificate + once sync.Once + source Source + err error } var ( @@ -49,93 +36,23 @@ var ( // Source is a function that can be passed into crypto/tls.Config.GetClientCertificate. type Source func(*tls.CertificateRequestInfo) (*tls.Certificate, error) -// DefaultSource returns a certificate source that execs the command specified -// in the file at ~/.secureConnect/context_aware_metadata.json +// errSourceUnavailable is a sentinel error to indicate certificate source is unavailable. +var errSourceUnavailable = errors.New("certificate source is unavailable") + +// DefaultSource returns a certificate source using the preferred EnterpriseCertificateProxySource. +// If EnterpriseCertificateProxySource is not available, fall back to the legacy SecureConnectSource. // -// If that file does not exist, a nil source is returned. +// If neither source is available (due to missing configurations), a nil Source and a nil Error are +// returned to indicate that a default certificate source is unavailable. func DefaultSource() (Source, error) { defaultCert.once.Do(func() { - defaultCert.source, defaultCert.err = newSecureConnectSource() + defaultCert.source, defaultCert.err = NewEnterpriseCertificateProxySource("") + if errors.Is(defaultCert.err, errSourceUnavailable) { + defaultCert.source, defaultCert.err = NewSecureConnectSource("") + if errors.Is(defaultCert.err, errSourceUnavailable) { + defaultCert.source, defaultCert.err = nil, nil + } + } }) return defaultCert.source, defaultCert.err } - -type secureConnectSource struct { - metadata secureConnectMetadata -} - -type secureConnectMetadata struct { - Cmd []string `json:"cert_provider_command"` -} - -// newSecureConnectSource creates a secureConnectSource by reading the well-known file. -func newSecureConnectSource() (Source, error) { - user, err := user.Current() - if err != nil { - // Ignore. - return nil, nil - } - filename := filepath.Join(user.HomeDir, metadataPath, metadataFile) - file, err := ioutil.ReadFile(filename) - if os.IsNotExist(err) { - // Ignore. - return nil, nil - } - if err != nil { - 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: %v", filename, err) - } - if err := validateMetadata(metadata); err != nil { - return nil, fmt.Errorf("cert: invalid config in %q: %v", filename, 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) { - defaultCert.cachedCertMutex.Lock() - defer defaultCert.cachedCertMutex.Unlock() - if defaultCert.cachedCert != nil && !isCertificateExpired(defaultCert.cachedCert) { - return defaultCert.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 { - // TODO(cbro): read stderr for error message? Might contain sensitive info. - return nil, err - } - cert, err := tls.X509KeyPair(data, data) - if err != nil { - return nil, err - } - defaultCert.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) -} diff --git a/transport/cert/default_cert_test.go b/transport/cert/default_cert_test.go deleted file mode 100644 index 2d7e333f332..00000000000 --- a/transport/cert/default_cert_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2020 Google LLC. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cert - -import ( - "bytes" - "testing" -) - -func TestGetClientCertificateSuccess(t *testing.T) { - defaultCert.cachedCert = nil - source := secureConnectSource{metadata: secureConnectMetadata{Cmd: []string{"cat", "testdata/testcert.pem"}}} - cert, err := source.getClientCertificate(nil) - if err != nil { - t.Error(err) - } - if cert.Certificate == nil { - t.Error("getClientCertificate: want non-nil Certificate, got nil") - } - if cert.PrivateKey == nil { - t.Error("getClientCertificate: want non-nil PrivateKey, got nil") - } -} - -func TestGetClientCertificateFailure(t *testing.T) { - defaultCert.cachedCert = nil - source := secureConnectSource{metadata: secureConnectMetadata{Cmd: []string{"cat"}}} - _, err := source.getClientCertificate(nil) - if err == nil { - t.Error("Expecting error.") - } - if got, want := err.Error(), "tls: failed to find any PEM data in certificate input"; got != want { - t.Errorf("getClientCertificate: want %v err, got %v", want, got) - } -} - -func TestValidateMetadataSuccess(t *testing.T) { - metadata := secureConnectMetadata{Cmd: []string{"cat", "testdata/testcert.pem"}} - err := validateMetadata(metadata) - if err != nil { - t.Error(err) - } -} - -func TestValidateMetadataFailure(t *testing.T) { - metadata := secureConnectMetadata{Cmd: []string{}} - err := validateMetadata(metadata) - if err == nil { - t.Error("validateMetadata: want non-nil err, got nil") - } - if want, got := "empty cert_provider_command", err.Error(); want != got { - t.Errorf("validateMetadata: want %v err, got %v", want, got) - } -} - -func TestIsCertificateExpiredTrue(t *testing.T) { - defaultCert.cachedCert = nil - source := secureConnectSource{metadata: secureConnectMetadata{Cmd: []string{"cat", "testdata/testcert.pem"}}} - cert, err := source.getClientCertificate(nil) - if err != nil { - t.Error(err) - } - if !isCertificateExpired(cert) { - t.Error("isCertificateExpired: want true, got false") - } -} - -func TestIsCertificateExpiredFalse(t *testing.T) { - defaultCert.cachedCert = nil - source := secureConnectSource{metadata: secureConnectMetadata{Cmd: []string{"cat", "testdata/nonexpiringtestcert.pem"}}} - cert, err := source.getClientCertificate(nil) - if err != nil { - t.Error(err) - } - if isCertificateExpired(cert) { - t.Error("isCertificateExpired: want false, got true") - } -} - -func TestCertificateCaching(t *testing.T) { - defaultCert.cachedCert = nil - source := secureConnectSource{metadata: secureConnectMetadata{Cmd: []string{"cat", "testdata/nonexpiringtestcert.pem"}}} - cert, err := source.getClientCertificate(nil) - if err != nil { - t.Error(err) - } - if cert == nil { - t.Error("getClientCertificate: want non-nil cert, got nil") - } - if defaultCert.cachedCert == nil { - t.Error("getClientCertificate: want non-nil defaultSourceCachedCert, got nil") - } - - source = secureConnectSource{metadata: secureConnectMetadata{Cmd: []string{"cat", "testdata/testcert.pem"}}} - cert, err = source.getClientCertificate(nil) - if err != nil { - t.Error(err) - } - if !bytes.Equal(cert.Certificate[0], defaultCert.cachedCert.Certificate[0]) { - t.Error("getClientCertificate: want cached Certificate, got different Certificate") - } - if cert.PrivateKey != defaultCert.cachedCert.PrivateKey { - t.Error("getClientCertificate: want cached PrivateKey, got different PrivateKey") - } -} diff --git a/transport/cert/enterprise_cert.go b/transport/cert/enterprise_cert.go new file mode 100644 index 00000000000..eaa52e07c08 --- /dev/null +++ b/transport/cert/enterprise_cert.go @@ -0,0 +1,56 @@ +// 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" + "errors" + "os" + + "github.com/googleapis/enterprise-certificate-proxy/client" +) + +type ecpSource struct { + key *client.Key +} + +// NewEnterpriseCertificateProxySource creates a certificate source +// using the Enterprise Certificate Proxy client, which delegates +// certifcate related operations to an OS-specific "signer binary" +// that communicates with the native keystore (ex. keychain on MacOS). +// +// The configFilePath points to a config file containing relevant parameters +// such as the certificate issuer and the location of the signer binary. +// If configFilePath is empty, the client will attempt to load the config from +// a well-known gcloud location. +func NewEnterpriseCertificateProxySource(configFilePath string) (Source, error) { + key, err := client.Cred(configFilePath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // Config file missing means Enterprise Certificate Proxy is not supported. + return nil, errSourceUnavailable + } + return nil, err + } + + return (&ecpSource{ + key: key, + }).getClientCertificate, nil +} + +func (s *ecpSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) { + var cert tls.Certificate + cert.PrivateKey = s.key + cert.Certificate = s.key.CertificateChain() + return &cert, nil +} diff --git a/transport/cert/enterprise_cert_test.go b/transport/cert/enterprise_cert_test.go new file mode 100644 index 00000000000..8f20887fd70 --- /dev/null +++ b/transport/cert/enterprise_cert_test.go @@ -0,0 +1,45 @@ +// 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 + +import ( + "errors" + "testing" +) + +func TestEnterpriseCertificateProxySource_ConfigMissing(t *testing.T) { + source, err := NewEnterpriseCertificateProxySource("missing.json") + if got, want := err, errSourceUnavailable; !errors.Is(err, errSourceUnavailable) { + t.Fatalf("NewEnterpriseCertificateProxySource: with missing config; got %v, want %v err", got, want) + } + if source != nil { + t.Errorf("NewEnterpriseCertificateProxySource: with missing config; got %v, want nil source", source) + } +} + +// This test launches a mock signer binary "test_signer.go" that uses a valid pem file. +func TestEnterpriseCertificateProxySource_GetClientCertificateSuccess(t *testing.T) { + source, err := NewEnterpriseCertificateProxySource("testdata/enterprise_certificate_config.json") + if err != nil { + t.Fatal(err) + } + cert, err := source(nil) + if err != nil { + t.Fatal(err) + } + if cert.Certificate == nil { + t.Error("getClientCertificate: got nil, want non-nil Certificate") + } + if cert.PrivateKey == nil { + t.Error("getClientCertificate: got nil, want non-nil PrivateKey") + } +} + +// This test launches a mock signer binary "test_signer.go" that uses an invalid pem file. +func TestEnterpriseCertificateProxySource_InitializationFailure(t *testing.T) { + _, err := NewEnterpriseCertificateProxySource("testdata/enterprise_certificate_config_invalid_pem.json") + if err == nil { + t.Error("NewEnterpriseCertificateProxySource: got nil, want non-nil err") + } +} diff --git a/transport/cert/secureconnect_cert.go b/transport/cert/secureconnect_cert.go new file mode 100644 index 00000000000..5913cab8017 --- /dev/null +++ b/transport/cert/secureconnect_cert.go @@ -0,0 +1,123 @@ +// 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" + "io/ioutil" + "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 := ioutil.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) +} diff --git a/transport/cert/secureconnect_cert_test.go b/transport/cert/secureconnect_cert_test.go new file mode 100644 index 00000000000..961477b5b46 --- /dev/null +++ b/transport/cert/secureconnect_cert_test.go @@ -0,0 +1,115 @@ +// 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 + +import ( + "bytes" + "errors" + "testing" +) + +func TestSecureConnectSource_ConfigMissing(t *testing.T) { + source, err := NewSecureConnectSource("missing.json") + if got, want := err, errSourceUnavailable; !errors.Is(err, errSourceUnavailable) { + t.Fatalf("NewSecureConnectSource: with missing config; got %v, want %v err", got, want) + } + if source != nil { + t.Errorf("NewSecureConnectSource: with missing config; got %v, want nil source", source) + } +} + +func TestSecureConnectSource_GetClientCertificateSuccess(t *testing.T) { + source, err := NewSecureConnectSource("testdata/context_aware_metadata.json") + if err != nil { + t.Fatal(err) + } + cert, err := source(nil) + if err != nil { + t.Error(err) + } + if cert.Certificate == nil { + t.Error("getClientCertificate: got nil, want non-nil Certificate") + } + if cert.PrivateKey == nil { + t.Error("getClientCertificate: got nil, want non-nil PrivateKey") + } +} + +func TestSecureConnectSource_GetClientCertificateFailure(t *testing.T) { + source, err := NewSecureConnectSource("testdata/context_aware_metadata_invalid_pem.json") + if err != nil { + t.Fatal(err) + } + _, err = source(nil) + if err == nil { + t.Error("getClientCertificate: got nil, want non-nil err") + } +} + +func TestSecureConnectSource_ValidateMetadataSuccess(t *testing.T) { + metadata := secureConnectMetadata{Cmd: []string{"cat", "testdata/testcert.pem"}} + err := validateMetadata(metadata) + if err != nil { + t.Error(err) + } +} + +func TestSecureConnectSource_ValidateMetadataFailure(t *testing.T) { + metadata := secureConnectMetadata{Cmd: []string{}} + err := validateMetadata(metadata) + if err == nil { + t.Error("validateMetadata: got nil, want non-nil err") + } + if got, want := err.Error(), "empty cert_provider_command"; got != want { + t.Errorf("validateMetadata: got %v, want %v err", got, want) + } +} + +func TestSecureConnectSource_IsCertificateExpiredTrue(t *testing.T) { + source, err := NewSecureConnectSource("testdata/context_aware_metadata.json") + if err != nil { + t.Fatal(err) + } + cert, err := source(nil) + if err != nil { + t.Error(err) + } + if !isCertificateExpired(cert) { + t.Error("isCertificateExpired: got false, want true") + } +} + +func TestSecureConnectSource_IsCertificateExpiredFalse(t *testing.T) { + source, err := NewSecureConnectSource("testdata/context_aware_metadata_nonexpiring_pem.json") + if err != nil { + t.Fatal(err) + } + cert, err := source(nil) + if err != nil { + t.Error(err) + } + if isCertificateExpired(cert) { + t.Error("isCertificateExpired: got true, want false") + } +} + +func TestCertificateCaching(t *testing.T) { + source := secureConnectSource{metadata: secureConnectMetadata{Cmd: []string{"cat", "testdata/nonexpiring.pem"}}} + cert, err := source.getClientCertificate(nil) + if err != nil { + t.Fatal(err) + } + if cert == nil { + t.Fatal("getClientCertificate: got nil, want non-nil cert") + } + if source.cachedCert == nil { + t.Fatal("getClientCertificate: got nil, want non-nil cachedCert") + } + if got, want := source.cachedCert.Certificate[0], cert.Certificate[0]; !bytes.Equal(got, want) { + t.Fatalf("getClientCertificate: got %v, want %v cached Certificate", got, want) + } + if got, want := source.cachedCert.PrivateKey, cert.PrivateKey; got != want { + t.Fatalf("getClientCertificate: got %v, want %v cached PrivateKey", got, want) + } +} diff --git a/transport/cert/testdata/context_aware_metadata.json b/transport/cert/testdata/context_aware_metadata.json new file mode 100644 index 00000000000..fbfdfb14272 --- /dev/null +++ b/transport/cert/testdata/context_aware_metadata.json @@ -0,0 +1,3 @@ +{ + "cert_provider_command":["cat", "testdata/testcert.pem"] +} diff --git a/transport/cert/testdata/context_aware_metadata_invalid_pem.json b/transport/cert/testdata/context_aware_metadata_invalid_pem.json new file mode 100644 index 00000000000..d297c2135be --- /dev/null +++ b/transport/cert/testdata/context_aware_metadata_invalid_pem.json @@ -0,0 +1,3 @@ +{ + "cert_provider_command":["cat", "testdata/invalid.pem"] +} diff --git a/transport/cert/testdata/context_aware_metadata_nonexpiring_pem.json b/transport/cert/testdata/context_aware_metadata_nonexpiring_pem.json new file mode 100644 index 00000000000..5876cd34437 --- /dev/null +++ b/transport/cert/testdata/context_aware_metadata_nonexpiring_pem.json @@ -0,0 +1,3 @@ +{ + "cert_provider_command":["cat", "testdata/nonexpiring.pem"] +} diff --git a/transport/cert/testdata/enterprise_certificate_config.json b/transport/cert/testdata/enterprise_certificate_config.json new file mode 100644 index 00000000000..be9f9a3e806 --- /dev/null +++ b/transport/cert/testdata/enterprise_certificate_config.json @@ -0,0 +1,8 @@ +{ + "cert_info": { + "issuer": "Test Issuer" + }, + "libs": { + "signer_binary": "./testdata/signer.sh" + } +} diff --git a/transport/cert/testdata/enterprise_certificate_config_invalid_pem.json b/transport/cert/testdata/enterprise_certificate_config_invalid_pem.json new file mode 100644 index 00000000000..5fc2dcfc8f9 --- /dev/null +++ b/transport/cert/testdata/enterprise_certificate_config_invalid_pem.json @@ -0,0 +1,8 @@ +{ + "cert_info": { + "issuer": "Test Issuer" + }, + "libs": { + "signer_binary": "./testdata/signer_invalid_pem.sh" + } +} diff --git a/transport/cert/testdata/invalid.pem b/transport/cert/testdata/invalid.pem new file mode 100644 index 00000000000..032255fbbdc --- /dev/null +++ b/transport/cert/testdata/invalid.pem @@ -0,0 +1,6 @@ +-----BEGIN CERTIFICATE----- +MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo +-----END PRIVATE KEY----- diff --git a/transport/cert/testdata/nonexpiringtestcert.pem b/transport/cert/testdata/nonexpiring.pem similarity index 100% rename from transport/cert/testdata/nonexpiringtestcert.pem rename to transport/cert/testdata/nonexpiring.pem diff --git a/transport/cert/testdata/signer.sh b/transport/cert/testdata/signer.sh new file mode 100755 index 00000000000..8a7b2192526 --- /dev/null +++ b/transport/cert/testdata/signer.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Copyright 2022 Google LLC. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +go run ../internal/ecp/test_signer.go testdata/testcert.pem diff --git a/transport/cert/testdata/signer_invalid_pem.sh b/transport/cert/testdata/signer_invalid_pem.sh new file mode 100755 index 00000000000..f97fb1489f9 --- /dev/null +++ b/transport/cert/testdata/signer_invalid_pem.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Copyright 2022 Google LLC. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +go run ../internal/ecp/test_signer.go testdata/invalid.pem diff --git a/transport/cert/testdata/testcert.pem b/transport/cert/testdata/testcert.pem index d15c396ba18..3f45e909126 100644 --- a/transport/cert/testdata/testcert.pem +++ b/transport/cert/testdata/testcert.pem @@ -18,4 +18,4 @@ MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g== ------END PRIVATE KEY----- \ No newline at end of file +-----END PRIVATE KEY----- diff --git a/transport/internal/ecp/test_signer.go b/transport/internal/ecp/test_signer.go new file mode 100644 index 00000000000..38425cf677b --- /dev/null +++ b/transport/internal/ecp/test_signer.go @@ -0,0 +1,101 @@ +// Copyright 2022 Google LLC. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// test_signer.go is a net/rpc server that listens on stdin/stdout, exposing +// mock methods for testing enterprise certificate proxy flow. +package main + +import ( + "crypto" + "crypto/tls" + "crypto/x509" + "io" + "io/ioutil" + "log" + "net/rpc" + "os" + "time" +) + +// SignArgs encapsulate the parameters for the Sign method. +type SignArgs struct { + Digest []byte + Opts crypto.SignerOpts +} + +// EnterpriseCertSigner exports RPC methods for signing. +type EnterpriseCertSigner struct { + cert *tls.Certificate +} + +// Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser. +type Connection struct { + io.ReadCloser + io.WriteCloser +} + +// Close closes c's underlying ReadCloser and WriteCloser. +func (c *Connection) Close() error { + rerr := c.ReadCloser.Close() + werr := c.WriteCloser.Close() + if rerr != nil { + return rerr + } + return werr +} + +// CertificateChain returns the credential as a raw X509 cert chain. This +// contains the public key. +func (k *EnterpriseCertSigner) CertificateChain(ignored struct{}, certificateChain *[][]byte) error { + *certificateChain = k.cert.Certificate + return nil +} + +// Public returns the first public key for this Key, in ASN.1 DER form. +func (k *EnterpriseCertSigner) Public(ignored struct{}, publicKey *[]byte) (err error) { + if len(k.cert.Certificate) == 0 { + return nil + } + cert, err := x509.ParseCertificate(k.cert.Certificate[0]) + if err != nil { + return err + } + *publicKey, err = x509.MarshalPKIXPublicKey(cert.PublicKey) + return err +} + +// Sign signs a message by encrypting a message digest. +func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) { + return nil +} + +func main() { + enterpriseCertSigner := new(EnterpriseCertSigner) + + data, err := ioutil.ReadFile(os.Args[1]) + if err != nil { + log.Fatalf("Error reading certificate: %v", err) + } + cert, _ := tls.X509KeyPair(data, data) + + enterpriseCertSigner.cert = &cert + + if err := rpc.Register(enterpriseCertSigner); err != nil { + log.Fatalf("Error registering net/rpc: %v", err) + } + + // If the parent process dies, we should exit. + // We can detect this by periodically checking if the PID of the parent + // process is 1 (https://stackoverflow.com/a/2035683). + go func() { + for { + if os.Getppid() == 1 { + log.Fatalln("Parent process died, exiting...") + } + time.Sleep(time.Second) + } + }() + + rpc.ServeConn(&Connection{os.Stdin, os.Stdout}) +}