Skip to content

Commit

Permalink
Issue #108: Mark ApiGateway certificates as CA certs
Browse files Browse the repository at this point in the history
Rebased with master and integrated the change for issue #108
which allows upgrading of client auth certificates to CA
certificates. This is a workaround for the AWS API GW client
auth certificates which don't have the flags set to work with Go.

The configuration for 1.2 differs that the aws.apigw.cert.cn
property was dropped in favor of a caupgcn parameter for the
newly introduced certificate sources.
  • Loading branch information
magiconair committed Jul 12, 2016
1 parent 7110aae commit 21f1a00
Show file tree
Hide file tree
Showing 16 changed files with 293 additions and 148 deletions.
3 changes: 2 additions & 1 deletion cert/consul_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
type ConsulSource struct {
CertURL string
ClientCAURL string
CAUpgradeCN string
}

const kvURLPrefix = "/v1/kv/"
Expand Down Expand Up @@ -56,7 +57,7 @@ func (s ConsulSource) LoadClientCAs() (*x509.CertPool, error) {
pemBlocks, _, err := getCerts(client, key, 0)
return pemBlocks, err
}
return newCertPool(key, load)
return newCertPool(key, s.CAUpgradeCN, load)
}

func (s ConsulSource) Certificates() chan []tls.Certificate {
Expand Down
3 changes: 2 additions & 1 deletion cert/file_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ type FileSource struct {
CertFile string
KeyFile string
ClientAuthFile string
CAUpgradeCN string
}

func (s FileSource) LoadClientCAs() (*x509.CertPool, error) {
return newCertPool(s.ClientAuthFile, func(path string) (map[string][]byte, error) {
return newCertPool(s.ClientAuthFile, s.CAUpgradeCN, func(path string) (map[string][]byte, error) {
if s.ClientAuthFile == "" {
return nil, nil
}
Expand Down
3 changes: 2 additions & 1 deletion cert/http_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import (
type HTTPSource struct {
CertURL string
ClientCAURL string
CAUpgradeCN string
Refresh time.Duration
}

func (s HTTPSource) LoadClientCAs() (*x509.CertPool, error) {
return newCertPool(s.ClientCAURL, loadURL)
return newCertPool(s.ClientCAURL, s.CAUpgradeCN, loadURL)
}

func (s HTTPSource) Certificates() chan []tls.Certificate {
Expand Down
30 changes: 23 additions & 7 deletions cert/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cert
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
Expand Down Expand Up @@ -174,7 +175,7 @@ func replaceSuffix(s string, oldSuffix, newSuffix string) string {

// newCertPool creates a new x509.CertPool by loading the
// PEM blocks from loadFn(path) and adding them to a CertPool.
func newCertPool(path string, loadFn func(path string) (pemBlocks map[string][]byte, err error)) (*x509.CertPool, error) {
func newCertPool(path string, caUpgradeCN string, loadFn func(path string) (pemBlocks map[string][]byte, err error)) (*x509.CertPool, error) {
pemBlocks, err := loadFn(path)
if err != nil {
return nil, err
Expand All @@ -184,14 +185,29 @@ func newCertPool(path string, loadFn func(path string) (pemBlocks map[string][]b
return nil, nil
}

x := x509.NewCertPool()
for name, pemBlock := range pemBlocks {
if !x.AppendCertsFromPEM(pemBlock) {
log.Printf("[WARN] cert: Could not add client CA certificate from %s", name)
continue
pool := x509.NewCertPool()
for _, pemBlock := range pemBlocks {
for p, rest := pem.Decode(pemBlock); p != nil; p, rest = pem.Decode(rest) {
cert, err := x509.ParseCertificate(p.Bytes)
if err != nil {
return nil, err
}
upgradeCACertificate(cert, caUpgradeCN)
pool.AddCert(cert)
}
}

log.Printf("[INFO] cert: Load client CA certs from %s", path)
return x, nil
return pool, nil
}

// upgradeCACertificate upgrades a certificate to a self-signing CA certificate if the CN matches.
// Issue #108: Allow generated AWS API Gateway certs to be used for client cert authentication
func upgradeCACertificate(cert *x509.Certificate, caUpgradeCN string) {
if caUpgradeCN != "" && caUpgradeCN == cert.Issuer.CommonName {
cert.BasicConstraintsValid = true
cert.IsCA = true
cert.KeyUsage = x509.KeyUsageCertSign
log.Print("[INFO] cert: Upgrading cert %s to CA cert", cert.Issuer.CommonName)
}
}
74 changes: 73 additions & 1 deletion cert/load_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package cert

import "testing"
import (
"crypto/x509"
"encoding/pem"
"testing"
)

func TestBase(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -34,3 +38,71 @@ func TestReplaceSuffix(t *testing.T) {
t.Errorf("got %q want %q", got, want)
}
}

func TestUpgradeCACertificate(t *testing.T) {
// generated at
// https://eu-west-1.console.aws.amazon.com/apigateway/home?region=eu-west-1#/client-certificates
const awsApiGWCert = `
-----BEGIN CERTIFICATE-----
MIIC6DCCAdCgAwIBAgIIZAgycYqDRqQwDQYJKoZIhvcNAQELBQAwNDELMAkGA1UE
BhMCVVMxEDAOBgNVBAcTB1NlYXR0bGUxEzARBgNVBAMTCkFwaUdhdGV3YXkwHhcN
MTYwNzEyMTkzMTMwWhcNMTcwNzEyMTkzMTMwWjA0MQswCQYDVQQGEwJVUzEQMA4G
A1UEBxMHU2VhdHRsZTETMBEGA1UEAxMKQXBpR2F0ZXdheTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAM/a0LKQd/obIwcKu09EjlHP4b7QqmK/JnJfd1Eq
m6We85FGu+26s7+Bpw1xiyK2jFuzQ4JFyXVkWLJH8e3Mp7P91MvJ1x6UCRk+Fz6Q
Lauw5SBVmDO5CauB4NcICTYEeTT3c0m8t6sDpHf+DHZC87gq9rhBggKXfNO3ntWw
Kq2uGscvnOz2/n2XIucFf2U7GI/cOapGXvIyrB5e/swSCyNkgOJ2HekzWjprxSs5
zu9JSOIzejgm8+/nnPOO9ycVrjN3qazUEXfF1QdvZeNCZ9GL6ZICAYo9xnnNLJnW
6p5d0Fw6U+V/nlNpgCB5djTwXaY51ScoW/i3ukHBZe9QIEcCAwEAATANBgkqhkiG
9w0BAQsFAAOCAQEAzwUJlSv/9XVoeCbot+3mdviZI5B7VnEKGl2Oam1fQzGZkkzB
kqBgtRrHux3BRxPRqS4jM4akdplFhejHExVatOxfS+DEXzFefi+aMb7qApB1YjV/
5FIIQdZaVOlw2KIRXCy04nxrKJmJ1T5RCkYC80dYpNfmDb5REUtp8jU78/Schsx7
0nCsrWkBSO1QtR4NnBlHbEM+imh3aCQz23SUK5Q/NTe4r2pu0zUl5b2YNgefvWle
7fe6T137rmhji9K+tYNznLGk0XmiguQPM2qJLxqeVQsA32wUbbSIFWH+KsXRPfpU
n/iFVG4Y6zyXQY2RzTt+ZB2VPR72X4wqS9fBeQ==
-----END CERTIFICATE-----`

p, rest := pem.Decode([]byte(awsApiGWCert))
if len(rest) > 0 {
t.Fatal("want only one cert")
}
cert, err := x509.ParseCertificate(p.Bytes)
if err != nil {
t.Fatal(err)
}

// check that cert does not have the flags set
if got, want := cert.BasicConstraintsValid, false; got != want {
t.Fatal("got %v want %v", got, want)
}
if got, want := cert.IsCA, false; got != want {
t.Fatal("got %v want %v", got, want)
}
if got, want := cert.KeyUsage, x509.KeyUsage(0); got != want {
t.Fatal("got %v want %v", got, want)
}

// run upgrade with not-matching CN expecting no change
upgradeCACertificate(cert, "no match")
if got, want := cert.BasicConstraintsValid, false; got != want {
t.Fatal("got %v want %v", got, want)
}
if got, want := cert.IsCA, false; got != want {
t.Fatal("got %v want %v", got, want)
}
if got, want := cert.KeyUsage, x509.KeyUsage(0); got != want {
t.Fatal("got %v want %v", got, want)
}

// run upgrade with matching CN
upgradeCACertificate(cert, "ApiGateway")
if got, want := cert.BasicConstraintsValid, true; got != want {
t.Fatal("got %v want %v", got, want)
}
if got, want := cert.IsCA, true; got != want {
t.Fatal("got %v want %v", got, want)
}
if got, want := cert.KeyUsage, x509.KeyUsageCertSign; got != want {
t.Fatal("got %v want %v", got, want)
}
}
3 changes: 2 additions & 1 deletion cert/path_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ type PathSource struct {
Path string
CertPath string
ClientCAPath string
CAUpgradeCN string
Refresh time.Duration
}

func (s PathSource) LoadClientCAs() (*x509.CertPool, error) {
path := makePath(s.Path, s.ClientCAPath, DefaultClientCAPath)
return newCertPool(path, loadPath)
return newCertPool(path, s.CAUpgradeCN, loadPath)
}

func (s PathSource) Certificates() chan []tls.Certificate {
Expand Down
51 changes: 51 additions & 0 deletions cert/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package cert
import (
"crypto/tls"
"crypto/x509"
"fmt"

"github.com/eBay/fabio/config"
)

// Source provides the interface for dynamic certificate sources.
Expand All @@ -19,6 +22,54 @@ type Source interface {
LoadClientCAs() (*x509.CertPool, error)
}

// NewSource generates a cert source from the config options.
func NewSource(cfg config.CertSource) (Source, error) {
switch cfg.Type {
case "file":
return FileSource{
CertFile: cfg.CertPath,
KeyFile: cfg.KeyPath,
ClientAuthFile: cfg.ClientCAPath,
CAUpgradeCN: cfg.CAUpgradeCN,
}, nil

case "path":
return PathSource{
CertPath: cfg.CertPath,
ClientCAPath: cfg.ClientCAPath,
CAUpgradeCN: cfg.CAUpgradeCN,
Refresh: cfg.Refresh,
}, nil

case "http":
return HTTPSource{
CertURL: cfg.CertPath,
ClientCAURL: cfg.ClientCAPath,
CAUpgradeCN: cfg.CAUpgradeCN,
Refresh: cfg.Refresh,
}, nil

case "consul":
return ConsulSource{
CertURL: cfg.CertPath,
ClientCAURL: cfg.ClientCAPath,
CAUpgradeCN: cfg.CAUpgradeCN,
}, nil

case "vault":
return VaultSource{
// TODO(fs): configure Addr but not token
CertPath: cfg.CertPath,
ClientCAPath: cfg.ClientCAPath,
CAUpgradeCN: cfg.CAUpgradeCN,
Refresh: cfg.Refresh,
}, nil

default:
return nil, fmt.Errorf("invalid certificate source %q", cfg.Type)
}
}

// TLSConfig creates a tls.Config which sets the
// GetCertificate field to a certificate store
// which uses the given source to update the
Expand Down
87 changes: 87 additions & 0 deletions cert/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,97 @@ import (
"testing"
"time"

"github.com/eBay/fabio/config"
consulapi "github.com/hashicorp/consul/api"
vaultapi "github.com/hashicorp/vault/api"
"github.com/pascaldekloe/goe/verify"
)

func TestNewSource(t *testing.T) {
certsource := func(typ string) config.CertSource {
return config.CertSource{
Type: typ,
Name: "name",
CertPath: "cert",
KeyPath: "key",
ClientCAPath: "clientca",
CAUpgradeCN: "upgcn",
Refresh: 3 * time.Second,
Header: http.Header{"A": []string{"b"}},
}
}
tests := []struct {
cfg config.CertSource
src Source
err string
}{
{
cfg: config.CertSource{
Type: "invalid",
},
src: nil,
err: `invalid certificate source "invalid"`,
},
{
cfg: certsource("file"),
src: FileSource{
CertFile: "cert",
KeyFile: "key",
ClientAuthFile: "clientca",
CAUpgradeCN: "upgcn",
},
},
{
cfg: certsource("path"),
src: PathSource{
CertPath: "cert",
ClientCAPath: "clientca",
CAUpgradeCN: "upgcn",
Refresh: 3 * time.Second,
},
},
{
cfg: certsource("http"),
src: HTTPSource{
CertURL: "cert",
ClientCAURL: "clientca",
CAUpgradeCN: "upgcn",
Refresh: 3 * time.Second,
},
},
{
cfg: certsource("consul"),
src: ConsulSource{
CertURL: "cert",
ClientCAURL: "clientca",
CAUpgradeCN: "upgcn",
},
},
{
cfg: certsource("vault"),
src: VaultSource{
CertPath: "cert",
ClientCAPath: "clientca",
CAUpgradeCN: "upgcn",
Refresh: 3 * time.Second,
},
},
}

for i, tt := range tests {
var errmsg string
src, err := NewSource(tt.cfg)
if err != nil {
errmsg = err.Error()
}
if got, want := errmsg, tt.err; got != want {
t.Fatalf("%d: got %q want %q", i, got, want)
}
got, want := src, tt.src
verify.Values(t, "src", got, want)
}
}

type StaticSource struct {
cert tls.Certificate
}
Expand Down
Loading

0 comments on commit 21f1a00

Please sign in to comment.