Repository: scionproto/scion
Pull request #3847 (2020-08-07 09:03:28)

trust: create TLS configuration based on CPPKI
Add a crypto manager that provides the necessary callbacks to create a SCION CPPKI backed `tls.Conifg`.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/3847)
<!-- Reviewable:end -->



Patch:
diff --git a/go/pkg/trust/BUILD.bazel b/go/pkg/trust/BUILD.bazel
index 997d2523f..587873f2c 100644
--- a/go/pkg/trust/BUILD.bazel
+++ b/go/pkg/trust/BUILD.bazel
@@ -16,6 +16,7 @@ go_library(
         "signer.go",
         "signer_gen.go",
         "store.go",
+        "tls_handshake.go",
         "verifier.go",
     ],
     importpath = "github.com/scionproto/scion/go/pkg/trust",
@@ -55,6 +56,7 @@ go_test(
         "signer_gen_test.go",
         "signer_test.go",
         "store_test.go",
+        "tls_handshake_test.go",
         "verifier_test.go",
     ],
     data = glob(["testdata/**"]),
diff --git a/go/pkg/trust/mock_trust/trust.go b/go/pkg/trust/mock_trust/trust.go
index 1f4fd3399..5b079e540 100644
--- a/go/pkg/trust/mock_trust/trust.go
+++ b/go/pkg/trust/mock_trust/trust.go
@@ -1,5 +1,5 @@
 // Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/scionproto/scion/go/pkg/trust (interfaces: DB,Fetcher,Inspector,KeyRing,Provider,Recurser,Router,RPC)
+// Source: github.com/scionproto/scion/go/pkg/trust (interfaces: DB,Fetcher,Inspector,KeyRing,Provider,Recurser,Router,RPC,X509KeyPairLoader)
 
 // Package mock_trust is a generated GoMock package.
 package mock_trust
@@ -7,14 +7,16 @@ package mock_trust
 import (
 	context "context"
 	crypto "crypto"
+	tls "crypto/tls"
 	x509 "crypto/x509"
+	net "net"
+	reflect "reflect"
+
 	gomock "github.com/golang/mock/gomock"
 	addr "github.com/scionproto/scion/go/lib/addr"
 	cert_mgmt "github.com/scionproto/scion/go/lib/ctrl/cert_mgmt"
 	cppki "github.com/scionproto/scion/go/lib/scrypto/cppki"
 	trust "github.com/scionproto/scion/go/pkg/trust"
-	net "net"
-	reflect "reflect"
 )
 
 // MockDB is a mock of DB interface
@@ -491,3 +493,41 @@ func (mr *MockRPCMockRecorder) GetTRC(arg0, arg1, arg2, arg3 interface{}) *gomoc
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTRC", reflect.TypeOf((*MockRPC)(nil).GetTRC), arg0, arg1, arg2, arg3)
 }
+
+// MockX509KeyPairLoader is a mock of X509KeyPairLoader interface
+type MockX509KeyPairLoader struct {
+	ctrl     *gomock.Controller
+	recorder *MockX509KeyPairLoaderMockRecorder
+}
+
+// MockX509KeyPairLoaderMockRecorder is the mock recorder for MockX509KeyPairLoader
+type MockX509KeyPairLoaderMockRecorder struct {
+	mock *MockX509KeyPairLoader
+}
+
+// NewMockX509KeyPairLoader creates a new mock instance
+func NewMockX509KeyPairLoader(ctrl *gomock.Controller) *MockX509KeyPairLoader {
+	mock := &MockX509KeyPairLoader{ctrl: ctrl}
+	mock.recorder = &MockX509KeyPairLoaderMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockX509KeyPairLoader) EXPECT() *MockX509KeyPairLoaderMockRecorder {
+	return m.recorder
+}
+
+// LoadX509KeyPair mocks base method
+func (m *MockX509KeyPairLoader) LoadX509KeyPair() (*tls.Certificate, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LoadX509KeyPair")
+	ret0, _ := ret[0].(*tls.Certificate)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// LoadX509KeyPair indicates an expected call of LoadX509KeyPair
+func (mr *MockX509KeyPairLoaderMockRecorder) LoadX509KeyPair() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadX509KeyPair", reflect.TypeOf((*MockX509KeyPairLoader)(nil).LoadX509KeyPair))
+}
diff --git a/go/pkg/trust/tls_handshake.go b/go/pkg/trust/tls_handshake.go
new file mode 100644
index 000000000..842edb70c
--- /dev/null
+++ b/go/pkg/trust/tls_handshake.go
@@ -0,0 +1,109 @@
+// Copyright 2020 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package trust
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"time"
+
+	"github.com/scionproto/scion/go/lib/scrypto/cppki"
+	"github.com/scionproto/scion/go/lib/serrors"
+)
+
+const defaultTimeout = 5 * time.Second
+
+// X509KeyPairLoader provides a certificate to be presented during TLS handshake.
+type X509KeyPairLoader interface {
+	LoadX509KeyPair() (*tls.Certificate, error)
+}
+
+// TLSCryptoManager implements callbacks which will be called during TLS handshake.
+type TLSCryptoManager struct {
+	Loader  X509KeyPairLoader
+	DB      DB
+	Timeout time.Duration
+}
+
+// NewTLSCryptoManager returns a new instance with the defaultTimeout.
+func NewTLSCryptoManager(loader X509KeyPairLoader, db DB) *TLSCryptoManager {
+	return &TLSCryptoManager{
+		DB:      db,
+		Loader:  loader,
+		Timeout: defaultTimeout,
+	}
+}
+
+// GetCertificate retrieves a certificate to be presented during TLS handshake.
+func (m *TLSCryptoManager) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
+	c, err := m.Loader.LoadX509KeyPair()
+	if err != nil {
+		return nil, serrors.WrapStr("loading server key pair", err)
+	}
+	return c, nil
+}
+
+// GetClientCertificate retrieves a client certificate to be presented during TLS handshake.
+func (m *TLSCryptoManager) GetClientCertificate(_ *tls.CertificateRequestInfo) (*tls.Certificate,
+	error) {
+	c, err := m.Loader.LoadX509KeyPair()
+	if err != nil {
+		return nil, serrors.WrapStr("loading client key pair", err)
+	}
+	return c, nil
+}
+
+// VerifyPeerCertificate verifies the certificate presented by the peer during TLS handshake,
+// based on the TRC.
+func (m *TLSCryptoManager) VerifyPeerCertificate(rawCerts [][]byte,
+	_ [][]*x509.Certificate) error {
+	chain := make([]*x509.Certificate, len(rawCerts))
+	for i, asn1Data := range rawCerts {
+		cert, err := x509.ParseCertificate(asn1Data)
+		if err != nil {
+			return serrors.WrapStr("parsing peer certificate", err)
+		}
+		chain[i] = cert
+	}
+	ia, err := cppki.ExtractIA(chain[0].Subject)
+	if err != nil {
+		return serrors.WrapStr("extracting ISD-AS from peer certificate", err)
+	} else if ia == nil {
+		return serrors.New("ISD-AS no present in peer certificate")
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), m.Timeout)
+	defer cancel()
+	trcs, _, err := activeTRCs(ctx, m.DB, ia.I)
+	if err != nil {
+		return serrors.WrapStr("loading TRCs", err)
+	}
+	if err := verifyChain(chain, trcs); err != nil {
+		return serrors.WrapStr("verifying chains", err)
+	}
+	return nil
+}
+
+func verifyChain(chain []*x509.Certificate, trcs []cppki.SignedTRC) error {
+	var errs serrors.List
+	for _, trc := range trcs {
+		if err := cppki.VerifyChain(chain, cppki.VerifyOptions{TRC: &trc.TRC}); err != nil {
+			errs = append(errs, err)
+			continue
+		}
+		return nil
+	}
+	return errs.ToError()
+}


Test patch:
diff --git a/go/pkg/trust/tls_handshake_test.go b/go/pkg/trust/tls_handshake_test.go
new file mode 100644
index 000000000..e3aa697c7
--- /dev/null
+++ b/go/pkg/trust/tls_handshake_test.go
@@ -0,0 +1,130 @@
+// Copyright 2020 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package trust_test
+
+import (
+	"crypto/tls"
+	"net"
+	"testing"
+	"time"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/scionproto/scion/go/lib/scrypto/cppki"
+	"github.com/scionproto/scion/go/lib/xtest"
+	"github.com/scionproto/scion/go/pkg/trust"
+	"github.com/scionproto/scion/go/pkg/trust/mock_trust"
+)
+
+func TestTLSCryptoManagerVerifyPeerCertificate(t *testing.T) {
+	trc := xtest.LoadTRC(t, "testdata/common/trcs/ISD1-B1-S1.trc")
+	crt111File := "testdata/common/ISD1/ASff00_0_111/crypto/as/ISD1-ASff00_0_111.pem"
+
+	testCases := map[string]struct {
+		db        func(ctrl *gomock.Controller) trust.DB
+		assertErr assert.ErrorAssertionFunc
+	}{
+		"valid": {
+			db: func(ctrl *gomock.Controller) trust.DB {
+				db := mock_trust.NewMockDB(ctrl)
+				db.EXPECT().SignedTRC(gomock.Any(), gomock.Any()).Return(trc, nil)
+				return db
+			},
+			assertErr: assert.NoError,
+		},
+	}
+	for name, tc := range testCases {
+		name, tc := name, tc
+		t.Run(name, func(t *testing.T) {
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+
+			db := tc.db(ctrl)
+			mgr := trust.TLSCryptoManager{
+				DB:      db,
+				Timeout: 5 * time.Second,
+			}
+			rawChain := loadRawChain(t, crt111File)
+			err := mgr.VerifyPeerCertificate(rawChain, nil)
+			tc.assertErr(t, err)
+		})
+	}
+}
+
+func TestHandshake(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	trc := xtest.LoadTRC(t, "testdata/common/trcs/ISD1-B1-S1.trc")
+	crt111File := "testdata/common/ISD1/ASff00_0_111/crypto/as/ISD1-ASff00_0_111.pem"
+	key111File := "testdata/common/ISD1/ASff00_0_111/crypto/as/cp-as.key"
+	tlsCert, err := tls.LoadX509KeyPair(crt111File, key111File)
+	require.NoError(t, err)
+	chain, err := cppki.ReadPEMCerts(crt111File)
+	require.NoError(t, err)
+
+	db := mock_trust.NewMockDB(ctrl)
+	db.EXPECT().SignedTRC(gomock.Any(), gomock.Any()).MaxTimes(2).Return(trc, nil)
+	loader := mock_trust.NewMockX509KeyPairLoader(ctrl)
+	loader.EXPECT().LoadX509KeyPair().MaxTimes(2).Return(&tlsCert, nil)
+
+	mgr := trust.NewTLSCryptoManager(loader, db)
+	clientConn, serverConn := net.Pipe()
+	defer clientConn.Close()
+	defer serverConn.Close()
+
+	client := tls.Client(clientConn, &tls.Config{
+		InsecureSkipVerify:    true,
+		GetClientCertificate:  mgr.GetClientCertificate,
+		VerifyPeerCertificate: mgr.VerifyPeerCertificate,
+	})
+	server := tls.Server(serverConn, &tls.Config{
+		InsecureSkipVerify:    true,
+		GetCertificate:        mgr.GetCertificate,
+		VerifyPeerCertificate: mgr.VerifyPeerCertificate,
+		ClientAuth:            tls.RequireAnyClientCert,
+	})
+
+	connCheck := func(w, r net.Conn) {
+		msg := []byte("hello")
+
+		go func() {
+			_, err := w.Write(msg)
+			require.NoError(t, err)
+		}()
+
+		buf := make([]byte, 100)
+		n, err := r.Read(buf)
+		require.NoError(t, err)
+		require.Equal(t, msg, buf[:n])
+	}
+
+	connCheck(server, client)
+
+	assert.Equal(t, chain, client.ConnectionState().PeerCertificates)
+	assert.Equal(t, chain, server.ConnectionState().PeerCertificates)
+	assert.True(t, client.ConnectionState().HandshakeComplete)
+	assert.True(t, server.ConnectionState().HandshakeComplete)
+}
+
+func loadRawChain(t *testing.T, file string) [][]byte {
+	var chain [][]byte
+	for _, cert := range xtest.LoadChain(t, file) {
+		chain = append(chain, cert.Raw)
+	}
+	return chain
+}