*: developer machine HSPKI credentials

In addition to k8s certificates, prodaccess now issues HSPKI
certificates, with DN=$username.sso.hswaw.net. These are installed into
XDG_CONFIG_HOME (or os equiv).

//go/pki will now automatically attempt to load these certificates. This
means you can now run any pki-dependant tool with -hspki_disable, and
with automatic mTLS!

Change-Id: I5b28e193e7c968d621bab0d42aabd6f0510fed6d
diff --git a/go/pki/BUILD.bazel b/go/pki/BUILD.bazel
index 5bc7522..f2eae41 100644
--- a/go/pki/BUILD.bazel
+++ b/go/pki/BUILD.bazel
@@ -2,7 +2,10 @@
 
 go_library(
     name = "go_default_library",
-    srcs = ["grpc.go"],
+    srcs = [
+        "grpc.go",
+        "locate.go",
+    ],
     importpath = "code.hackerspace.pl/hscloud/go/pki",
     visibility = ["//visibility:public"],
     deps = [
diff --git a/go/pki/grpc.go b/go/pki/grpc.go
index 1720ad8..313f4a9 100644
--- a/go/pki/grpc.go
+++ b/go/pki/grpc.go
@@ -20,7 +20,6 @@
 	"crypto/x509"
 	"flag"
 	"fmt"
-	"io/ioutil"
 	"strings"
 
 	"github.com/golang/glog"
@@ -210,18 +209,19 @@
 		return []grpc.ServerOption{}
 	}
 
-	serverCert, err := tls.LoadX509KeyPair(flagCertificatePath, flagKeyPath)
+	loc, err := loadCredentials()
+	if err != nil {
+		glog.Exitf("WithServerHSPKI: loadCredentials: %v", err)
+	}
+
+	serverCert, err := tls.X509KeyPair(loc.cert, loc.key)
 	if err != nil {
 		glog.Exitf("WithServerHSPKI: cannot load service certificate/key: %v", err)
 	}
 
 	certPool := x509.NewCertPool()
-	ca, err := ioutil.ReadFile(flagCAPath)
-	if err != nil {
-		glog.Exitf("WithServerHSPKI: cannot load CA certificate: %v", err)
-	}
-	if ok := certPool.AppendCertsFromPEM(ca); !ok {
-		glog.Exitf("WithServerHSPKI: cannot use CA certificate: %v", err)
+	if ok := certPool.AppendCertsFromPEM(loc.ca); !ok {
+		glog.Exitf("WithServerHSPKI: cannot use CA certificate")
 	}
 
 	creds := grpc.Creds(credentials.NewTLS(&tls.Config{
@@ -243,16 +243,17 @@
 		return grpc.WithInsecure()
 	}
 
-	certPool := x509.NewCertPool()
-	ca, err := ioutil.ReadFile(flagCAPath)
+	loc, err := loadCredentials()
 	if err != nil {
-		glog.Exitf("WithClientHSPKI: cannot load CA certificate: %v", err)
-	}
-	if ok := certPool.AppendCertsFromPEM(ca); !ok {
-		glog.Exitf("WithClientHSPKI: cannot use CA certificate: %v", err)
+		glog.Exitf("WithServerHSPKI: loadCredentials: %v", err)
 	}
 
-	clientCert, err := tls.LoadX509KeyPair(flagCertificatePath, flagKeyPath)
+	certPool := x509.NewCertPool()
+	if ok := certPool.AppendCertsFromPEM(loc.ca); !ok {
+		glog.Exitf("WithServerHSPKI: cannot use CA certificate")
+	}
+
+	clientCert, err := tls.X509KeyPair(loc.cert, loc.key)
 	if err != nil {
 		glog.Exitf("WithClientHSPKI: cannot load service certificate/key: %v", err)
 	}
diff --git a/go/pki/locate.go b/go/pki/locate.go
new file mode 100644
index 0000000..e48e013
--- /dev/null
+++ b/go/pki/locate.go
@@ -0,0 +1,88 @@
+package pki
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	"github.com/golang/glog"
+)
+
+// DeveloperCredentialsLocation returns the path containing HSPKI credentials
+// on developer machines. These are provisioned by //cluster/prodaccess, and
+// are used if available.
+func DeveloperCredentialsLocation() (string, error) {
+	cfgDir, err := os.UserConfigDir()
+	if err != nil {
+		glog.Exitf("UserConfigDir: %w", err)
+	}
+
+	return fmt.Sprintf("%s/hspki", cfgDir), nil
+}
+
+type creds struct {
+	ca   []byte
+	cert []byte
+	key  []byte
+}
+
+func loadDeveloperCredentials() (*creds, error) {
+	path, err := DeveloperCredentialsLocation()
+	if err != nil {
+		return nil, fmt.Errorf("DeveloperCredentialsLocation: %w")
+	}
+
+	c := creds{}
+	for _, el := range []struct {
+		target *[]byte
+		path   string
+	}{
+		{&c.ca, path + "/" + "ca.crt"},
+		{&c.cert, path + "/" + "tls.crt"},
+		{&c.key, path + "/" + "tls.key"},
+	} {
+		data, err := ioutil.ReadFile(el.path)
+		if err != nil {
+			return nil, fmt.Errorf("ReadFile(%q): %w", el.path, err)
+		}
+		*el.target = data
+	}
+
+	return &c, nil
+}
+
+func loadFlagCredentials() (*creds, error) {
+	c := creds{}
+	for _, el := range []struct {
+		target *[]byte
+		path   string
+	}{
+		{&c.ca, flagCAPath},
+		{&c.cert, flagCertificatePath},
+		{&c.key, flagKeyPath},
+	} {
+		data, err := ioutil.ReadFile(el.path)
+		if err != nil {
+			return nil, fmt.Errorf("ReadFile(%q): %w", el.path, err)
+		}
+		*el.target = data
+	}
+
+	return &c, nil
+}
+
+func loadCredentials() (*creds, error) {
+	dev, err := loadDeveloperCredentials()
+	if err == nil {
+		return dev, nil
+	}
+	glog.Warningf("Could not load developer PKI credentials: %v", err)
+
+	fl, err := loadFlagCredentials()
+	if err == nil {
+		return fl, err
+	}
+	glog.Warningf("Could not load flag-defined PKI credentials: %v", err)
+
+	return nil, fmt.Errorf("could not load any credentials")
+}