Skip to content

Commit

Permalink
Add support for client certificates
Browse files Browse the repository at this point in the history
Adds support for usings client certificates to authenticate against
service endpoints. It also enables users to allow insecure TLS
connections to service endpoints.

Fixes open-policy-agent#684

Signed-off-by: Kim Christensen <[email protected]>
  • Loading branch information
kichristensen authored and tsandall committed Oct 25, 2018
1 parent 318bb20 commit 6f3f5b3
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 19 deletions.
109 changes: 102 additions & 7 deletions plugins/rest/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ package rest
import (
"bytes"
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
Expand All @@ -21,26 +27,105 @@ import (

// Config represents configuration for a REST client.
type Config struct {
Name string `json:"name"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Credentials struct {
Name string `json:"name"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
AllowInsureTLS bool `json:"allow_insecure_tls,omitempty"`
Credentials struct {
Bearer *struct {
Scheme string `json:"scheme,omitempty"`
Token string `json:"token"`
} `json:"bearer,omitempty"`
ClientTLS *struct {
Cert string `json:"cert"`
PrivateKey string `json:"private_key"`
PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"`
} `json:"client_tls,omitempty"`
} `json:"credentials"`
}

func (c *Config) validateAndInjectDefaults() error {
func (c *Config) validateAndInjectDefaults() (*tls.Config, error) {
c.URL = strings.TrimRight(c.URL, "/")
_, err := url.Parse(c.URL)
if err != nil {
return nil, err
}
if c.Credentials.Bearer != nil {
if c.Credentials.Bearer.Scheme == "" {
c.Credentials.Bearer.Scheme = "Bearer"
}
}
return err
tlsConfig := &tls.Config{
InsecureSkipVerify: c.AllowInsureTLS,
}
if c.Credentials.ClientTLS != nil {
if err := c.readCertificate(tlsConfig); err != nil {
return nil, err
}
}
return tlsConfig, err
}

func (c *Config) readCertificate(t *tls.Config) error {
if c.Credentials.ClientTLS.Cert == "" {
return errors.New("client certificate is needed when client TLS is enabled")
}
if c.Credentials.ClientTLS.PrivateKey == "" {
return errors.New("private key is needed when client TLS is enabled")
}

var keyPEMBlock []byte
data, err := ioutil.ReadFile(c.Credentials.ClientTLS.PrivateKey)
if err != nil {
return err
}

block, _ := pem.Decode(data)
if block == nil {
return errors.New("PEM data could not be found")
}

if x509.IsEncryptedPEMBlock(block) {
if c.Credentials.ClientTLS.PrivateKeyPassphrase == "" {
return errors.New("client certificate passphrase is need, because the certificate is password encrypted")
}
block, err := x509.DecryptPEMBlock(block, []byte(c.Credentials.ClientTLS.PrivateKeyPassphrase))
if err != nil {
return err
}
key, err := x509.ParsePKCS8PrivateKey(block)
if err != nil {
key, err = x509.ParsePKCS1PrivateKey(block)
if err != nil {
return fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8; parse error: %v", err)
}
}
rsa, ok := key.(*rsa.PrivateKey)
if !ok {
return errors.New("private key is invalid")
}
keyPEMBlock = pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(rsa),
},
)
} else {
keyPEMBlock = data
}

certPEMBlock, err := ioutil.ReadFile(c.Credentials.ClientTLS.Cert)
if err != nil {
return err
}

cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return err
}

t.Certificates = []tls.Certificate{cert}
return nil
}

// Client implements an HTTP/REST client for communicating with remote
Expand All @@ -61,7 +146,17 @@ func New(config []byte) (Client, error) {
return Client{}, err
}

return Client{config: parsedConfig}, parsedConfig.validateAndInjectDefaults()
tlsConfig, err := parsedConfig.validateAndInjectDefaults()
if err != nil {
return Client{}, err
}

return Client{
config: parsedConfig,
Client: http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
},
}, nil
}

// Service returns the name of the service this Client is configured for.
Expand Down
168 changes: 156 additions & 12 deletions plugins/rest/rest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ package rest

import (
"context"
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
Expand Down Expand Up @@ -55,6 +58,28 @@ func TestNew(t *testing.T) {
}
}`,
},
{
input: `{
"name": "foo",
"url": "http://localhost",
"credentials": {
"client_tls": {}
}
}`,
wantErr: true,
},
{
input: `{
"name": "foo",
"url": "http://localhost",
"credentials": {
"client_tls": {
"cert": "cert.pem"
}
}
}`,
wantErr: true,
},
}

var results []Client
Expand Down Expand Up @@ -127,14 +152,69 @@ func TestBearerToken(t *testing.T) {
}
}

func TestClientCert(t *testing.T) {
ts := testServer{
t: t,
tls: true,
expectClientCert: true,
}
ts.start()
defer ts.stop()
tmpPem, err := ioutil.TempFile("", "client.pem")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
defer os.Remove(tmpPem.Name())
tmpKey, err := ioutil.TempFile("", "client.key")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
defer os.Remove(tmpKey.Name())
if _, err := tmpPem.Write([]byte(ts.clientCertPem)); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if err := tmpPem.Close(); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if _, err := tmpKey.Write([]byte(ts.clientCertKey)); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if err := tmpKey.Close(); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
config := fmt.Sprintf(`{
"name": "foo",
"url": %q,
"allow_insecure_tls": true,
"credentials": {
"client_tls": {
"cert": %q,
"private_key": %q,
"private_key_passphrase": "secret",
}
}
}`, ts.server.URL, tmpPem.Name(), tmpKey.Name())
client, err := New([]byte(config))
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
ctx := context.Background()
if _, err := client.Do(ctx, "GET", "test"); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}

type testServer struct {
t *testing.T
server *httptest.Server
expPath string
expMethod string
expBearerToken string
expBearerScheme string
tls bool
t *testing.T
server *httptest.Server
expPath string
expMethod string
expBearerToken string
expBearerScheme string
tls bool
clientCertPem []byte
clientCertKey []byte
expectClientCert bool
}

func (t *testServer) handle(w http.ResponseWriter, r *http.Request) {
Expand All @@ -147,19 +227,83 @@ func (t *testServer) handle(w http.ResponseWriter, r *http.Request) {
if (t.expBearerToken != "" || t.expBearerScheme != "") && len(r.Header["Authorization"]) == 0 {
t.t.Fatal("Expected bearer token, but didn't get any")
}
auth := r.Header["Authorization"][0]
if t.expBearerScheme != "" && !strings.HasPrefix(auth, t.expBearerScheme) {
t.t.Fatalf("Expected bearer scheme %q, got authorization header %q", t.expBearerScheme, auth)
if len(r.Header["Authorization"]) > 0 {
auth := r.Header["Authorization"][0]
if t.expBearerScheme != "" && !strings.HasPrefix(auth, t.expBearerScheme) {
t.t.Fatalf("Expected bearer scheme %q, got authorization header %q", t.expBearerScheme, auth)
}
if t.expBearerToken != "" && !strings.HasSuffix(auth, t.expBearerToken) {
t.t.Fatalf("Expected bearer token %q, got authorization header %q", t.expBearerToken, auth)
}
}
if t.expBearerToken != "" && !strings.HasSuffix(auth, t.expBearerToken) {
t.t.Fatalf("Expected bearer token %q, got authorization header %q", t.expBearerToken, auth)
if t.expectClientCert {
if len(r.TLS.PeerCertificates) == 0 {
t.t.Fatal("Expected client certificate but didn't get any")
}
}
w.WriteHeader(200)
}

var (
clientCertPem = []byte(`-----BEGIN CERTIFICATE-----
MIIC/jCCAeYCAQEwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCc2UxEzARBgNV
BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0
ZDAeFw0xODEwMTQyMTMwNTVaFw0xOTEwMTQyMTMwNTVaMEUxCzAJBgNVBAYTAkFV
MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz
IFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPiWZCKrb
FIusaNlOJ4R41ARd63PVJglwYxxoOfBUVvmgh7Sq5ccQDWQvs5QpQSt6HcQHsoS+
behOxl13sW7UY2nQiBSmFqnd8PkgZg89q9tmk0cRdrl90crCs72Lt3t/AgRC1YEz
WQ7Fa2ig/k60ftwOq98Ogsjc6+/ToIiZD2BKy/3DHTl5TXNuPCSvZCKkFGM3zlse
H5UtY1ZaO5gFC+SotJ0RrGfEiJY6nuqXcMRHTj5NluGZhkQR/1TdHa9nAWG2TlUD
IEabN5yggtvH0Wz0lQD7okTyOOC+X9gUbOoILUR98SUytAYaiiPbAAlOpvdSjPS+
LzT22wBSPjRxAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAKMW44mCKXIjx7p+c8pn
qmmaioYtXrWeDE3/gAKhkB3Z4pY4ajEGogGNP19t2DoGDx7y2KJpMA777HoagclW
HFATMYN1J6YSkTrXFJnItvaQnv8mMqK4xR2kN4yO1CEITANakhu4pYZn0oU1sxEY
R3Cl3YMMR/OHoPpR2FKaX0G67xZZ2SXHf2jN2KsRV38PHfmb4ASX3Cbg6hzl1+du
ORxvL+DSwh2/n8Vdby0SdRQ7BxfqtSaIRogtScN2QzquaHeW1ErENfRqmeV/XHJr
1bmaSvfZe+CZnlLCeTlHcxu0i1fkdoYgi/oRWFPI1DBH6F1cGY+wWMuS4Job1zOK
OtQ=
-----END CERTIFICATE-----
`)
clientCertKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,64355BD1C400418C
6gIaC8pHDkQFFDBEInaH5V28EhU/d5lo+XUzGxPLdUKW2dgy+WgLq7apD0YwMh1N
8FEazXR1h++WihI1zNaoOdLyGjW1uyzi4RiuWpZPzbn4Ms4nPy8ZOEqMJZD2XTIG
19AibbBfUO9nmfs3G0afcgEM9QhWcij7DYO9QJBc3WEz5A4zC5jO+r2V4b5a2a1Q
UL0vWtuBEJ0XVaog9AGWOr4dV5rfpDIOKEMkGYURyUVe67AeL8Km5iuMdPyDuyjw
mEedculc15i3QwmVeJGNkhIbr8VRBQbzvdZlI4VeicFwLfhGS0pRTupKEVZuIpXI
rRWnCvCNJVIXIzqCWDknsJz4UrR8fXSpjuX3+R5XOJVvkgwlR5At5CbsVSu8T2NS
NAJzXLaqpl4QJCOIG4oTIZ9oZ8x/dcOE/ey8/TLXHknlKJvKfvvpEhIFcyKgF4p4
jJEaAE2o67V82hENZB8WhiXb5IaUFGPlii1B1L56mKnP8gv6HHBuxjmQ/DdzBOvZ
DVPyZ/Yy7VpcYr/5iGNEjEW9Td4apPAQTAkcEOrj9v5nslHUuyRnzC07hKa/gvG2
hrd9CgQ6ZmgT/AczASA0sbvliDwpSvublEAHiBtwWe5rmxxcFE937ljU/QdU6jiR
abxymx3gHZMIIG5YoqbXhuntYXeiZdiCTn62n9yO/7Lvps4kIBqL11fZenkuDxR9
QIDgoxzIZX3Ts16fUJdEVoPd0kLizuntuiNVUFukhKyz8eBzTI8xYgfnPjisxdEb
eU7Wzw/jntu5DHjUnREyiWLZK+MDCYk2wdlqMR4+4p0hWGBPgK2o9QNW2j1MePsO
pAhV0YBYKt1VMNrQv5M89DWkFFffuj7IZUsUPeO7A4Gs7NJ9eArjCKijKptX2Osw
Lh1Zb6assP6Mqd63UUINC31FQwwTjXjApA/sRYsVTQplbMJ1RVF4IRdH8K/HKVRW
1ACB+DZqDox5eS7xjxQ+tJozU1LdDOi8i7M673IFF7vAFzSHROsXgOkxZbVEEwrQ
F8rtogulYjgZpHEOWAcla961nE/j+wDzC8Uc9XNjvBnDyTeVqaA8aYzbDWOVTZ9n
i5HJgvaGdEQVt6tWGKDtGTUYLHHhXSslRKh77gprA2wuofR1qXzgEij2h7KIcoqA
kw/e2lwc4XFhU1/6mZSD8X3B8oOQQegv4h55xJzO7lZUNb2yXjAlm3HwD1tl3499
YfwbxGI9OAMomlq61W2rPDMWeDN0v9vSJ9iebE7rPe4A3RJwdfm2lYui0B+o/rLB
ppmX9Mv5LVFaXsDI4q41tziQOM26WhOzx+vF8h1l+aeTo5G3mTlT64+mJ26HcPDP
c+jtZ0vWdvf67HTncZxhoITFd5wKp2yru8wRTCT+VCSABZfMZQ6SYGyzVP+Wgf6t
U65k7iKsT2gUhk5QJIg0ZGvERDiGLXupcoyGhuoZhLm4HmmOZzvDx6f9VjM7Npt0
IJdvDV2sh3QXk4LTwn/0gCw+LxBBuubw3XKYyRKbzw6jYgqsazRNVn2zdkuchcc8
EnVu9NNEzAkTEEYIG99ECBmCIR9QknQXfqHRa5zNBndjBPJuOyVUwA==
-----END RSA PRIVATE KEY-----
`)
)

func (t *testServer) start() {
t.server = httptest.NewUnstartedServer(http.HandlerFunc(t.handle))
t.clientCertPem = clientCertPem
t.clientCertKey = clientCertKey
if t.tls {
t.server.TLS = &tls.Config{ClientAuth: tls.RequireAnyClientCert}
t.server.StartTLS()
} else {
t.server.Start()
Expand Down

0 comments on commit 6f3f5b3

Please sign in to comment.