transport: add cert package, default cert source

These certs can be used for mTLS (mutual TLS) connections when Endpoint
Verification is required. A metadata file in a well-known location
signals that a client certificate should be used when opening a
connection to a Google API server.

For more information on Endpoint Validation, see
https://cloud.google.com/endpoint-verification/docs/overview

In the future, the default cert source will be used automatically by
Google API clients. This is an extension of Application Default Credentials.

Change-Id: I66033a84fd735f1ee6dfffdf4f6c0cf4649acc5e
Reviewed-on: https://code-review.googlesource.com/c/google-api-go-client/+/50370
Reviewed-by: Chris Broadfoot <cbro@google.com>
diff --git a/transport/cert/default_cert.go b/transport/cert/default_cert.go
new file mode 100644
index 0000000..c03af65
--- /dev/null
+++ b/transport/cert/default_cert.go
@@ -0,0 +1,110 @@
+// 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 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"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"sync"
+)
+
+const (
+	metadataPath = ".secureConnect"
+	metadataFile = "context_aware_metadata.json"
+)
+
+var (
+	defaultSourceOnce sync.Once
+	defaultSource     Source
+	defaultSourceErr  error
+)
+
+// 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
+//
+// If that file does not exist, a nil source is returned.
+func DefaultSource() (Source, error) {
+	defaultSourceOnce.Do(func() {
+		defaultSource, defaultSourceErr = newSecureConnectSource()
+	})
+	return defaultSource, defaultSourceErr
+}
+
+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) {
+	// TODO(cbro): consider caching valid certificates rather than exec'ing every time.
+	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
+	}
+	return &cert, nil
+}
diff --git a/transport/cert/default_cert_test.go b/transport/cert/default_cert_test.go
new file mode 100644
index 0000000..0ec3c44
--- /dev/null
+++ b/transport/cert/default_cert_test.go
@@ -0,0 +1,53 @@
+// 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 (
+	"testing"
+)
+
+func TestGetClientCertificateSuccess(t *testing.T) {
+	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("want non-nil cert, got nil")
+	}
+	if cert.PrivateKey == nil {
+		t.Error("want non-nil PrivateKey, got nil")
+	}
+}
+
+func TestGetClientCertificateFailure(t *testing.T) {
+	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)
+	}
+}
diff --git a/transport/cert/testdata/testcert.pem b/transport/cert/testdata/testcert.pem
new file mode 100644
index 0000000..d15c396
--- /dev/null
+++ b/transport/cert/testdata/testcert.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ
+hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa
+rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv
+zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF
+MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW
+r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo
+k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G
+6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N
+MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW
+SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T
+xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi
+D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g==
+-----END PRIVATE KEY-----
\ No newline at end of file