genericapiserver: extract CA cert from server cert and SNI cert chains

This commit is contained in:
Dr. Stefan Schimanski 2016-12-20 15:13:32 +01:00
parent 856c3623b5
commit 680d02a3ec
2 changed files with 208 additions and 38 deletions

View File

@ -75,24 +75,46 @@ func (s *SecureServingInfo) NewSelfClientConfig(token string) (*restclient.Confi
BearerToken: token, BearerToken: token,
} }
// find certificate for host: either explicitly given, from the server cert bundle or one of the SNI certs // find certificate for host: either explicitly given, from the server cert bundle or one of the SNI certs,
// but only return CA:TRUE certificates.
var derCA []byte var derCA []byte
if s.CACert != nil { if s.CACert != nil {
derCA = s.CACert.Certificate[0] derCA = s.CACert.Certificate[0]
} }
if derCA == nil && net.ParseIP(host) == nil {
if cert, found := s.SNICerts[host]; found {
chain, err := parseChain(cert.Certificate)
if err != nil {
return nil, fmt.Errorf("failed to parse SNI certificate for host %q: %v", host, err)
}
if trustedChain(chain) {
return clientConfig, nil
}
ca, err := findCA(chain)
if err != nil {
return nil, fmt.Errorf("no CA certificate found in SNI server certificate bundle for host %q: %v", host, err)
}
derCA = ca.Raw
}
}
if derCA == nil && s.Cert != nil { if derCA == nil && s.Cert != nil {
x509Cert, err := x509.ParseCertificate(s.Cert.Certificate[0]) chain, err := parseChain(s.Cert.Certificate)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse server certificate: %v", err) return nil, fmt.Errorf("failed to parse server certificate: %v", err)
} }
if (net.ParseIP(host) != nil && certMatchesIP(x509Cert, host)) || certMatchesName(x509Cert, host) { if (net.ParseIP(host) != nil && certMatchesIP(chain[0], host)) || certMatchesName(chain[0], host) {
derCA = s.Cert.Certificate[0] if trustedChain(chain) {
return clientConfig, nil
} }
ca, err := findCA(chain)
if err != nil {
return nil, fmt.Errorf("no CA certificate found in server certificate bundle: %v", err)
} }
if derCA == nil && net.ParseIP(host) == nil { derCA = ca.Raw
if cert, found := s.SNICerts[host]; found {
derCA = cert.Certificate[0]
} }
} }
if derCA == nil { if derCA == nil {
@ -107,6 +129,41 @@ func (s *SecureServingInfo) NewSelfClientConfig(token string) (*restclient.Confi
return clientConfig, nil return clientConfig, nil
} }
func trustedChain(chain []*x509.Certificate) bool {
intermediates := x509.NewCertPool()
for _, cert := range chain[1:] {
intermediates.AddCert(cert)
}
_, err := chain[0].Verify(x509.VerifyOptions{
Intermediates: intermediates,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
})
return err == nil
}
func parseChain(bss [][]byte) ([]*x509.Certificate, error) {
var result []*x509.Certificate
for _, bs := range bss {
x509Cert, err := x509.ParseCertificate(bs)
if err != nil {
return nil, err
}
result = append(result, x509Cert)
}
return result, nil
}
func findCA(chain []*x509.Certificate) (*x509.Certificate, error) {
for _, cert := range chain {
if cert.IsCA {
return cert, nil
}
}
return nil, fmt.Errorf("no certificate with CA:TRUE found in chain")
}
func (s *ServingInfo) NewSelfClientConfig(token string) (*restclient.Config, error) { func (s *ServingInfo) NewSelfClientConfig(token string) (*restclient.Config, error) {
if s == nil { if s == nil {
return nil, nil return nil, nil

View File

@ -25,13 +25,17 @@ import (
"io/ioutil" "io/ioutil"
"net" "net"
"os" "os"
"reflect"
"strconv"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
"k8s.io/kubernetes/pkg/genericapiserver/options" "k8s.io/kubernetes/pkg/genericapiserver/options"
utilcert "k8s.io/kubernetes/pkg/util/cert" utilcert "k8s.io/kubernetes/pkg/util/cert"
"k8s.io/kubernetes/pkg/util/config" "k8s.io/kubernetes/pkg/util/config"
"k8s.io/kubernetes/pkg/version"
) )
type TestCertSpec struct { type TestCertSpec struct {
@ -229,23 +233,26 @@ NextTest:
} }
func TestServerRunWithSNI(t *testing.T) { func TestServerRunWithSNI(t *testing.T) {
tests := []struct { tests := map[string]struct {
Cert TestCertSpec Cert TestCertSpec
SNICerts []NamedTestCertSpec SNICerts []NamedTestCertSpec
ExpectedCertIndex int ExpectedCertIndex int
// passed in the client hello info, "localhost" if unset // passed in the client hello info, "localhost" if unset
ServerName string ServerName string
// optional ip or hostname to pass to NewSelfClientConfig
SelfClientBindAddressOverride string
ExpectSelfClientError bool
}{ }{
{ "only one cert": {
// only one cert
Cert: TestCertSpec{ Cert: TestCertSpec{
host: "localhost", host: "localhost",
ips: []string{"127.0.0.1"},
}, },
ExpectedCertIndex: -1, ExpectedCertIndex: -1,
}, },
{ "cert with multiple alternate names": {
// cert with multiple alternate names
Cert: TestCertSpec{ Cert: TestCertSpec{
host: "localhost", host: "localhost",
names: []string{"test.com"}, names: []string{"test.com"},
@ -254,10 +261,10 @@ func TestServerRunWithSNI(t *testing.T) {
ExpectedCertIndex: -1, ExpectedCertIndex: -1,
ServerName: "test.com", ServerName: "test.com",
}, },
{ "one SNI and the default cert with the same name": {
// one SNI and the default cert with the same name
Cert: TestCertSpec{ Cert: TestCertSpec{
host: "localhost", host: "localhost",
ips: []string{"127.0.0.1"},
}, },
SNICerts: []NamedTestCertSpec{ SNICerts: []NamedTestCertSpec{
{ {
@ -268,10 +275,10 @@ func TestServerRunWithSNI(t *testing.T) {
}, },
ExpectedCertIndex: 0, ExpectedCertIndex: 0,
}, },
{ "matching SNI cert": {
// matching SNI cert
Cert: TestCertSpec{ Cert: TestCertSpec{
host: "localhost", host: "localhost",
ips: []string{"127.0.0.1"},
}, },
SNICerts: []NamedTestCertSpec{ SNICerts: []NamedTestCertSpec{
{ {
@ -283,13 +290,12 @@ func TestServerRunWithSNI(t *testing.T) {
ExpectedCertIndex: 0, ExpectedCertIndex: 0,
ServerName: "test.com", ServerName: "test.com",
}, },
{ "matching IP in SNI cert and the server cert": {
// matching IP in SNI cert and the server cert. But IPs must not be // IPs must not be passed via SNI. Hence, the ServerName in the
// passed via SNI. Hence, the ServerName in the HELLO packet is empty // HELLO packet is empty and the server should select the non-SNI cert.
// and the server should select the non-SNI cert.
Cert: TestCertSpec{ Cert: TestCertSpec{
host: "localhost", host: "localhost",
ips: []string{"10.0.0.1"}, ips: []string{"10.0.0.1", "127.0.0.1"},
}, },
SNICerts: []NamedTestCertSpec{ SNICerts: []NamedTestCertSpec{
{ {
@ -302,10 +308,10 @@ func TestServerRunWithSNI(t *testing.T) {
ExpectedCertIndex: -1, ExpectedCertIndex: -1,
ServerName: "10.0.0.1", ServerName: "10.0.0.1",
}, },
{ "wildcards": {
// wildcards
Cert: TestCertSpec{ Cert: TestCertSpec{
host: "localhost", host: "localhost",
ips: []string{"127.0.0.1"},
}, },
SNICerts: []NamedTestCertSpec{ SNICerts: []NamedTestCertSpec{
{ {
@ -318,6 +324,79 @@ func TestServerRunWithSNI(t *testing.T) {
ExpectedCertIndex: 0, ExpectedCertIndex: 0,
ServerName: "www.test.com", ServerName: "www.test.com",
}, },
"loopback: IP for loopback client on SNI cert": {
Cert: TestCertSpec{
host: "localhost",
},
SNICerts: []NamedTestCertSpec{
{
TestCertSpec: TestCertSpec{
host: "test.com",
ips: []string{"127.0.0.1"},
},
},
},
ExpectedCertIndex: -1,
ExpectSelfClientError: true,
},
"loopback: IP for loopback client on server and SNI cert": {
Cert: TestCertSpec{
ips: []string{"127.0.0.1"},
host: "localhost",
},
SNICerts: []NamedTestCertSpec{
{
TestCertSpec: TestCertSpec{
host: "test.com",
ips: []string{"127.0.0.1"},
},
},
},
ExpectedCertIndex: -1,
},
"loopback: bind to 0.0.0.0 => loopback uses localhost; localhost on server cert": {
Cert: TestCertSpec{
host: "localhost",
},
SNICerts: []NamedTestCertSpec{
{
TestCertSpec: TestCertSpec{
host: "test.com",
},
},
},
ExpectedCertIndex: -1,
SelfClientBindAddressOverride: "0.0.0.0",
},
"loopback: bind to 0.0.0.0 => loopback uses localhost; localhost on SNI cert": {
Cert: TestCertSpec{
host: "test.com",
},
SNICerts: []NamedTestCertSpec{
{
TestCertSpec: TestCertSpec{
host: "localhost",
},
},
},
ExpectedCertIndex: 0,
SelfClientBindAddressOverride: "0.0.0.0",
},
"loopback: bind to 0.0.0.0 => loopback uses localhost; localhost on server and SNI cert": {
Cert: TestCertSpec{
host: "localhost",
},
SNICerts: []NamedTestCertSpec{
{
TestCertSpec: TestCertSpec{
host: "localhost",
},
},
},
ExpectedCertIndex: 0,
SelfClientBindAddressOverride: "0.0.0.0",
},
} }
tempDir, err := ioutil.TempDir("", "") tempDir, err := ioutil.TempDir("", "")
@ -327,16 +406,16 @@ func TestServerRunWithSNI(t *testing.T) {
defer os.RemoveAll(tempDir) defer os.RemoveAll(tempDir)
NextTest: NextTest:
for i, test := range tests { for title, test := range tests {
// create server cert // create server cert
serverCertBundleFile, serverKeyFile, err := createTestCertFiles(tempDir, test.Cert) serverCertBundleFile, serverKeyFile, err := createTestCertFiles(tempDir, test.Cert)
if err != nil { if err != nil {
t.Errorf("%d - failed to create server cert: %v", i, err) t.Errorf("%q - failed to create server cert: %v", title, err)
continue NextTest continue NextTest
} }
ca, err := caCertFromBundle(serverCertBundleFile) ca, err := caCertFromBundle(serverCertBundleFile)
if err != nil { if err != nil {
t.Errorf("%d - failed to extract ca cert from server cert bundle: %v", i, err) t.Errorf("%q - failed to extract ca cert from server cert bundle: %v", title, err)
continue NextTest continue NextTest
} }
caCerts := []*x509.Certificate{ca} caCerts := []*x509.Certificate{ca}
@ -345,7 +424,7 @@ NextTest:
var namedCertKeys []config.NamedCertKey var namedCertKeys []config.NamedCertKey
serverSig, err := certFileSignature(serverCertBundleFile, serverKeyFile) serverSig, err := certFileSignature(serverCertBundleFile, serverKeyFile)
if err != nil { if err != nil {
t.Errorf("%d - failed to get server cert signature: %v", i, err) t.Errorf("%q - failed to get server cert signature: %v", title, err)
continue NextTest continue NextTest
} }
signatures := map[string]int{ signatures := map[string]int{
@ -354,7 +433,7 @@ NextTest:
for j, c := range test.SNICerts { for j, c := range test.SNICerts {
certBundleFile, keyFile, err := createTestCertFiles(tempDir, c.TestCertSpec) certBundleFile, keyFile, err := createTestCertFiles(tempDir, c.TestCertSpec)
if err != nil { if err != nil {
t.Errorf("%d - failed to create SNI cert %d: %v", i, j, err) t.Errorf("%q - failed to create SNI cert %d: %v", title, j, err)
continue NextTest continue NextTest
} }
@ -366,7 +445,7 @@ NextTest:
ca, err := caCertFromBundle(certBundleFile) ca, err := caCertFromBundle(certBundleFile)
if err != nil { if err != nil {
t.Errorf("%d - failed to extract ca cert from SNI cert %d: %v", i, j, err) t.Errorf("%q - failed to extract ca cert from SNI cert %d: %v", title, j, err)
continue NextTest continue NextTest
} }
caCerts = append(caCerts, ca) caCerts = append(caCerts, ca)
@ -374,7 +453,7 @@ NextTest:
// store index in namedCertKeys with the signature as the key // store index in namedCertKeys with the signature as the key
sig, err := certFileSignature(certBundleFile, keyFile) sig, err := certFileSignature(certBundleFile, keyFile)
if err != nil { if err != nil {
t.Errorf("%d - failed get SNI cert %d signature: %v", i, j, err) t.Errorf("%q - failed get SNI cert %d signature: %v", title, j, err)
continue NextTest continue NextTest
} }
signatures[sig] = j signatures[sig] = j
@ -386,6 +465,9 @@ NextTest:
etcdserver, config, _ := setUp(t) etcdserver, config, _ := setUp(t)
defer etcdserver.Terminate(t) defer etcdserver.Terminate(t)
v := version.Get()
config.Version = &v
config.EnableIndex = true config.EnableIndex = true
_, err = config.ApplySecureServingOptions(&options.SecureServingOptions{ _, err = config.ApplySecureServingOptions(&options.SecureServingOptions{
ServingOptions: options.ServingOptions{ ServingOptions: options.ServingOptions{
@ -401,14 +483,14 @@ NextTest:
SNICertKeys: namedCertKeys, SNICertKeys: namedCertKeys,
}) })
if err != nil { if err != nil {
t.Errorf("%d - failed applying the SecureServingOptions: %v", i, err) t.Errorf("%q - failed applying the SecureServingOptions: %v", title, err)
continue NextTest continue NextTest
} }
config.InsecureServingInfo = nil config.InsecureServingInfo = nil
s, err := config.Complete().New() s, err := config.Complete().New()
if err != nil { if err != nil {
t.Errorf("%d - failed creating the server: %v", i, err) t.Errorf("%q - failed creating the server: %v", title, err)
continue NextTest continue NextTest
} }
@ -416,7 +498,7 @@ NextTest:
s.SecureServingInfo.BindAddress = "127.0.0.1:0" s.SecureServingInfo.BindAddress = "127.0.0.1:0"
if err := s.serveSecurely(stopCh); err != nil { if err := s.serveSecurely(stopCh); err != nil {
t.Errorf("%d - failed running the server: %v", i, err) t.Errorf("%q - failed running the server: %v", title, err)
continue NextTest continue NextTest
} }
@ -434,7 +516,7 @@ NextTest:
ServerName: test.ServerName, // used for SNI in the client HELLO packet ServerName: test.ServerName, // used for SNI in the client HELLO packet
}) })
if err != nil { if err != nil {
t.Errorf("%d - failed to connect: %v", i, err) t.Errorf("%q - failed to connect: %v", title, err)
continue NextTest continue NextTest
} }
@ -442,13 +524,44 @@ NextTest:
sig := x509CertSignature(conn.ConnectionState().PeerCertificates[0]) sig := x509CertSignature(conn.ConnectionState().PeerCertificates[0])
gotCertIndex, found := signatures[sig] gotCertIndex, found := signatures[sig]
if !found { if !found {
t.Errorf("%d - unknown signature returned from server: %s", i, sig) t.Errorf("%q - unknown signature returned from server: %s", title, sig)
} }
if gotCertIndex != test.ExpectedCertIndex { if gotCertIndex != test.ExpectedCertIndex {
t.Errorf("%d - expected cert index %d, got cert index %d", i, test.ExpectedCertIndex, gotCertIndex) t.Errorf("%q - expected cert index %d, got cert index %d", title, test.ExpectedCertIndex, gotCertIndex)
} }
conn.Close() conn.Close()
// check that the loopback client can connect
host := "127.0.0.1"
if len(test.SelfClientBindAddressOverride) != 0 {
host = test.SelfClientBindAddressOverride
}
config.SecureServingInfo.ServingInfo.BindAddress = net.JoinHostPort(host, strconv.Itoa(s.effectiveSecurePort))
cfg, err := config.SecureServingInfo.NewSelfClientConfig("some-token")
if test.ExpectSelfClientError {
if err == nil {
t.Errorf("%q - expected error creating loopback client config", title)
}
continue NextTest
}
if err != nil {
t.Errorf("%q - failed creating loopback client config: %v", title, err)
continue NextTest
}
client, err := clientset.NewForConfig(cfg)
if err != nil {
t.Errorf("%q - failed to create loopback client: %v", title, err)
continue NextTest
}
got, err := client.ServerVersion()
if err != nil {
t.Errorf("%q - failed to connect with loopback client: %v", title, err)
continue NextTest
}
if expected := &v; !reflect.DeepEqual(got, expected) {
t.Errorf("%q - loopback client didn't get correct version info: expected=%v got=%v", title, expected, got)
}
} }
} }