From 66fd3b5195536494f460f26e5a3501a8a41c2b0d Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Wed, 10 Jan 2018 21:50:17 +0700 Subject: [PATCH] expose the ConnectionState in the Session The ConnectionState contains basic details about the QUIC connection. --- Changelog.md | 1 + h2quic/server_test.go | 1 + interface.go | 6 +++++ internal/crypto/cert_manager.go | 5 ++++ internal/handshake/crypto_setup_client.go | 9 +++++++ .../handshake/crypto_setup_client_test.go | 26 +++++++++++++++++++ internal/handshake/crypto_setup_server.go | 14 +++++++++- .../handshake/crypto_setup_server_test.go | 20 ++++++++++++++ internal/handshake/crypto_setup_tls.go | 11 ++++++++ internal/handshake/crypto_setup_tls_test.go | 23 ++++++++++++++++ internal/handshake/interface.go | 11 ++++++++ internal/mocks/handshake/mint_tls.go | 12 +++++++++ mint_utils.go | 4 +++ packet_packer_test.go | 1 + server_test.go | 1 + session.go | 4 +++ 16 files changed, 148 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 04f984324..d7ff66793 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,7 @@ - The lower boundary for packets included in ACKs is now derived, and the value sent in STOP_WAITING frames is ignored. - Remove `DialNonFWSecure` and `DialAddrNonFWSecure`. +- Expose the `ConnectionState` in the `Session` (experimental API). ## v0.6.0 (2017-12-12) diff --git a/h2quic/server_test.go b/h2quic/server_test.go index cf32ed5f2..55ffd33d4 100644 --- a/h2quic/server_test.go +++ b/h2quic/server_test.go @@ -70,6 +70,7 @@ func (s *mockSession) RemoteAddr() net.Addr { func (s *mockSession) Context() context.Context { return s.ctx } +func (s *mockSession) ConnectionState() quic.ConnectionState { panic("not implemented") } var _ = Describe("H2 server", func() { var ( diff --git a/interface.go b/interface.go index 9a97f86d7..b0a18293d 100644 --- a/interface.go +++ b/interface.go @@ -19,6 +19,9 @@ type VersionNumber = protocol.VersionNumber // A Cookie can be used to verify the ownership of the client address. type Cookie = handshake.Cookie +// ConnectionState records basic details about the QUIC connection. +type ConnectionState = handshake.ConnectionState + // An ErrorCode is an application-defined error code. type ErrorCode = protocol.ApplicationErrorCode @@ -128,6 +131,9 @@ type Session interface { // The context is cancelled when the session is closed. // Warning: This API should not be considered stable and might change soon. Context() context.Context + // ConnectionState returns basic details about the QUIC connection. + // Warning: This API should not be considered stable and might change soon. + ConnectionState() ConnectionState } // Config contains all configuration data needed for a QUIC server or client. diff --git a/internal/crypto/cert_manager.go b/internal/crypto/cert_manager.go index 5aaa1877c..8b8c9faa8 100644 --- a/internal/crypto/cert_manager.go +++ b/internal/crypto/cert_manager.go @@ -18,6 +18,7 @@ type CertManager interface { GetLeafCertHash() (uint64, error) VerifyServerProof(proof, chlo, serverConfigData []byte) bool Verify(hostname string) error + GetChain() []*x509.Certificate } type certManager struct { @@ -54,6 +55,10 @@ func (c *certManager) SetData(data []byte) error { return nil } +func (c *certManager) GetChain() []*x509.Certificate { + return c.chain +} + func (c *certManager) GetCommonCertificateHashes() []byte { return getCommonCertificateHashes() } diff --git a/internal/handshake/crypto_setup_client.go b/internal/handshake/crypto_setup_client.go index 11e43e836..cb500b583 100644 --- a/internal/handshake/crypto_setup_client.go +++ b/internal/handshake/crypto_setup_client.go @@ -381,6 +381,15 @@ func (h *cryptoSetupClient) SetDiversificationNonce(data []byte) { h.divNonceChan <- data } +func (h *cryptoSetupClient) ConnectionState() ConnectionState { + h.mutex.Lock() + defer h.mutex.Unlock() + return ConnectionState{ + HandshakeComplete: h.forwardSecureAEAD != nil, + PeerCertificates: h.certManager.GetChain(), + } +} + func (h *cryptoSetupClient) sendCHLO() error { h.clientHelloCounter++ if h.clientHelloCounter > protocol.MaxClientHellos { diff --git a/internal/handshake/crypto_setup_client_test.go b/internal/handshake/crypto_setup_client_test.go index 51d5c4f6e..695fef616 100644 --- a/internal/handshake/crypto_setup_client_test.go +++ b/internal/handshake/crypto_setup_client_test.go @@ -2,6 +2,7 @@ package handshake import ( "bytes" + "crypto/x509" "encoding/binary" "errors" "fmt" @@ -10,6 +11,7 @@ import ( "github.com/lucas-clemente/quic-go/internal/crypto" "github.com/lucas-clemente/quic-go/internal/mocks/crypto" "github.com/lucas-clemente/quic-go/internal/protocol" + "github.com/lucas-clemente/quic-go/internal/testdata" "github.com/lucas-clemente/quic-go/internal/utils" "github.com/lucas-clemente/quic-go/qerr" . "github.com/onsi/ginkgo" @@ -34,6 +36,8 @@ type mockCertManager struct { commonCertificateHashes []byte + chain []*x509.Certificate + leafCert []byte leafCertHash uint64 leafCertHashError error @@ -45,6 +49,8 @@ type mockCertManager struct { verifyCalled bool } +var _ crypto.CertManager = &mockCertManager{} + func (m *mockCertManager) SetData(data []byte) error { m.setDataCalledWith = data return m.setDataError @@ -72,6 +78,10 @@ func (m *mockCertManager) Verify(hostname string) error { return m.verifyError } +func (m *mockCertManager) GetChain() []*x509.Certificate { + return m.chain +} + var _ = Describe("Client Crypto Setup", func() { var ( cs *cryptoSetupClient @@ -841,6 +851,22 @@ var _ = Describe("Client Crypto Setup", func() { }) }) + Context("reporting the connection state", func() { + It("reports the connection state before the handshake completes", func() { + chain := []*x509.Certificate{testdata.GetCertificate().Leaf} + certManager.chain = chain + state := cs.ConnectionState() + Expect(state.HandshakeComplete).To(BeFalse()) + Expect(state.PeerCertificates).To(Equal(chain)) + }) + + It("reports the connection state after the handshake completes", func() { + doSHLO() + state := cs.ConnectionState() + Expect(state.HandshakeComplete).To(BeTrue()) + }) + }) + Context("forcing encryption levels", func() { It("forces null encryption", func() { cs.nullAEAD.(*mockcrypto.MockAEAD).EXPECT().Seal(nil, []byte("foobar"), protocol.PacketNumber(4), []byte{}).Return([]byte("foobar unencrypted")) diff --git a/internal/handshake/crypto_setup_server.go b/internal/handshake/crypto_setup_server.go index 4bec2d4a7..7d5f32ee8 100644 --- a/internal/handshake/crypto_setup_server.go +++ b/internal/handshake/crypto_setup_server.go @@ -23,6 +23,8 @@ type KeyExchangeFunction func() crypto.KeyExchange // The CryptoSetupServer handles all things crypto for the Session type cryptoSetupServer struct { + mutex sync.RWMutex + connID protocol.ConnectionID remoteAddr net.Addr scfg *ServerConfig @@ -51,7 +53,7 @@ type cryptoSetupServer struct { params *TransportParameters - mutex sync.RWMutex + sni string // need to fill out the ConnectionState } var _ CryptoSetup = &cryptoSetupServer{} @@ -139,6 +141,7 @@ func (h *cryptoSetupServer) handleMessage(chloData []byte, cryptoData map[Tag][] if sni == "" { return false, qerr.Error(qerr.CryptoMessageParameterNotFound, "SNI required") } + h.sni = sni // prevent version downgrade attacks // see https://groups.google.com/a/chromium.org/forum/#!topic/proto-quic/N-de9j63tCk for a discussion and examples @@ -453,6 +456,15 @@ func (h *cryptoSetupServer) SetDiversificationNonce(data []byte) { panic("not needed for cryptoSetupServer") } +func (h *cryptoSetupServer) ConnectionState() ConnectionState { + h.mutex.Lock() + defer h.mutex.Unlock() + return ConnectionState{ + ServerName: h.sni, + HandshakeComplete: h.receivedForwardSecurePacket, + } +} + func (h *cryptoSetupServer) validateClientNonce(nonce []byte) error { if len(nonce) != 32 { return qerr.Error(qerr.InvalidCryptoMessageParameter, "invalid client nonce length") diff --git a/internal/handshake/crypto_setup_server_test.go b/internal/handshake/crypto_setup_server_test.go index 55b210320..9c855c0a6 100644 --- a/internal/handshake/crypto_setup_server_test.go +++ b/internal/handshake/crypto_setup_server_test.go @@ -661,6 +661,25 @@ var _ = Describe("Server Crypto Setup", func() { }) }) + Context("reporting the connection state", func() { + It("reports before the handshake completes", func() { + cs.sni = "server name" + state := cs.ConnectionState() + Expect(state.HandshakeComplete).To(BeFalse()) + Expect(state.ServerName).To(Equal("server name")) + }) + + It("reports after the handshake completes", func() { + doCHLO() + // receive a forward secure packet + cs.forwardSecureAEAD.(*mockcrypto.MockAEAD).EXPECT().Open(nil, []byte("forward secure encrypted"), protocol.PacketNumber(11), []byte{}) + _, _, err := cs.Open(nil, []byte("forward secure encrypted"), 11, []byte{}) + Expect(err).ToNot(HaveOccurred()) + state := cs.ConnectionState() + Expect(state.HandshakeComplete).To(BeTrue()) + }) + }) + Context("forcing encryption levels", func() { It("forces null encryption", func() { cs.nullAEAD.(*mockcrypto.MockAEAD).EXPECT().Seal(nil, []byte("foobar"), protocol.PacketNumber(11), []byte{}).Return([]byte("foobar unencrypted")) @@ -721,6 +740,7 @@ var _ = Describe("Server Crypto Setup", func() { Expect(err).ToNot(HaveOccurred()) Expect(done).To(BeFalse()) Expect(stream.dataWritten.Bytes()).To(ContainSubstring(string(validSTK))) + Expect(cs.sni).To(Equal("foo")) }) It("works with proper STK", func() { diff --git a/internal/handshake/crypto_setup_tls.go b/internal/handshake/crypto_setup_tls.go index f25bacad5..54dfe1c03 100644 --- a/internal/handshake/crypto_setup_tls.go +++ b/internal/handshake/crypto_setup_tls.go @@ -164,3 +164,14 @@ func (h *cryptoSetupTLS) DiversificationNonce() []byte { func (h *cryptoSetupTLS) SetDiversificationNonce([]byte) { panic("diversification nonce not needed for TLS") } + +func (h *cryptoSetupTLS) ConnectionState() ConnectionState { + h.mutex.Lock() + defer h.mutex.Unlock() + mintConnState := h.tls.ConnectionState() + return ConnectionState{ + // TODO: set the ServerName, once mint exports it + HandshakeComplete: h.aead != nil, + PeerCertificates: mintConnState.PeerCertificates, + } +} diff --git a/internal/handshake/crypto_setup_tls_test.go b/internal/handshake/crypto_setup_tls_test.go index 4b8a27256..f0293ba6a 100644 --- a/internal/handshake/crypto_setup_tls_test.go +++ b/internal/handshake/crypto_setup_tls_test.go @@ -66,6 +66,29 @@ var _ = Describe("TLS Crypto Setup", func() { Expect(handshakeEvent).To(Receive()) }) + Context("reporting the handshake state", func() { + It("reports before the handshake compeletes", func() { + cs.tls = mockhandshake.NewMockMintTLS(mockCtrl) + cs.tls.(*mockhandshake.MockMintTLS).EXPECT().ConnectionState().Return(mint.ConnectionState{}) + state := cs.ConnectionState() + Expect(state.HandshakeComplete).To(BeFalse()) + Expect(state.PeerCertificates).To(BeNil()) + }) + + It("reports after the handshake completes", func() { + cs.tls = mockhandshake.NewMockMintTLS(mockCtrl) + cs.tls.(*mockhandshake.MockMintTLS).EXPECT().ConnectionState().Return(mint.ConnectionState{}) + cs.tls.(*mockhandshake.MockMintTLS).EXPECT().Handshake().Return(mint.AlertNoAlert) + cs.tls.(*mockhandshake.MockMintTLS).EXPECT().State().Return(mint.StateServerConnected) + cs.keyDerivation = mockKeyDerivation + err := cs.HandleCryptoStream() + Expect(err).ToNot(HaveOccurred()) + state := cs.ConnectionState() + Expect(state.HandshakeComplete).To(BeTrue()) + Expect(state.PeerCertificates).To(BeNil()) + }) + }) + Context("escalating crypto", func() { doHandshake := func() { cs.tls = mockhandshake.NewMockMintTLS(mockCtrl) diff --git a/internal/handshake/interface.go b/internal/handshake/interface.go index fbb700627..34b955318 100644 --- a/internal/handshake/interface.go +++ b/internal/handshake/interface.go @@ -1,6 +1,7 @@ package handshake import ( + "crypto/x509" "io" "github.com/bifurcation/mint" @@ -29,6 +30,7 @@ type MintTLS interface { // additional methods Handshake() mint.Alert State() mint.State + ConnectionState() mint.ConnectionState SetCryptoStream(io.ReadWriter) SetExtensionHandler(mint.AppExtensionHandler) error @@ -41,8 +43,17 @@ type CryptoSetup interface { // TODO: clean up this interface DiversificationNonce() []byte // only needed for cryptoSetupServer SetDiversificationNonce([]byte) // only needed for cryptoSetupClient + ConnectionState() ConnectionState GetSealer() (protocol.EncryptionLevel, Sealer) GetSealerWithEncryptionLevel(protocol.EncryptionLevel) (Sealer, error) GetSealerForCryptoStream() (protocol.EncryptionLevel, Sealer) } + +// ConnectionState records basic details about the QUIC connection. +// Warning: This API should not be considered stable and might change soon. +type ConnectionState struct { + HandshakeComplete bool // handshake is complete + ServerName string // server name requested by client, if any (server side only) + PeerCertificates []*x509.Certificate // certificate chain presented by remote peer +} diff --git a/internal/mocks/handshake/mint_tls.go b/internal/mocks/handshake/mint_tls.go index 21d996e5a..b797949ec 100644 --- a/internal/mocks/handshake/mint_tls.go +++ b/internal/mocks/handshake/mint_tls.go @@ -48,6 +48,18 @@ func (mr *MockMintTLSMockRecorder) ComputeExporter(arg0, arg1, arg2 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ComputeExporter", reflect.TypeOf((*MockMintTLS)(nil).ComputeExporter), arg0, arg1, arg2) } +// ConnectionState mocks base method +func (m *MockMintTLS) ConnectionState() mint.ConnectionState { + ret := m.ctrl.Call(m, "ConnectionState") + ret0, _ := ret[0].(mint.ConnectionState) + return ret0 +} + +// ConnectionState indicates an expected call of ConnectionState +func (mr *MockMintTLSMockRecorder) ConnectionState() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectionState", reflect.TypeOf((*MockMintTLS)(nil).ConnectionState)) +} + // GetCipherSuite mocks base method func (m *MockMintTLS) GetCipherSuite() mint.CipherSuiteParams { ret := m.ctrl.Call(m, "GetCipherSuite") diff --git a/mint_utils.go b/mint_utils.go index 9764a70c7..596a4d940 100644 --- a/mint_utils.go +++ b/mint_utils.go @@ -56,6 +56,10 @@ func (mc *mintController) State() mint.State { return mc.conn.State().HandshakeState } +func (mc *mintController) ConnectionState() mint.ConnectionState { + return mc.conn.State() +} + func (mc *mintController) SetCryptoStream(stream io.ReadWriter) { mc.csc.SetStream(stream) } diff --git a/packet_packer_test.go b/packet_packer_test.go index 3bbeaed84..936d25546 100644 --- a/packet_packer_test.go +++ b/packet_packer_test.go @@ -50,6 +50,7 @@ func (m *mockCryptoSetup) GetSealerWithEncryptionLevel(protocol.EncryptionLevel) } func (m *mockCryptoSetup) DiversificationNonce() []byte { return m.divNonce } func (m *mockCryptoSetup) SetDiversificationNonce(divNonce []byte) { m.divNonce = divNonce } +func (m *mockCryptoSetup) ConnectionState() ConnectionState { panic("not implemented") } var _ = Describe("Packet packer", func() { var ( diff --git a/server_test.go b/server_test.go index 31ce2fbc0..b49c74ab4 100644 --- a/server_test.go +++ b/server_test.go @@ -62,6 +62,7 @@ func (s *mockSession) OpenStreamSync() (Stream, error) { panic("not implemented func (s *mockSession) LocalAddr() net.Addr { panic("not implemented") } func (s *mockSession) RemoteAddr() net.Addr { panic("not implemented") } func (*mockSession) Context() context.Context { panic("not implemented") } +func (*mockSession) ConnectionState() ConnectionState { panic("not implemented") } func (*mockSession) GetVersion() protocol.VersionNumber { return protocol.VersionWhatever } func (s *mockSession) handshakeStatus() <-chan error { return s.handshakeChan } func (*mockSession) getCryptoStream() cryptoStreamI { panic("not implemented") } diff --git a/session.go b/session.go index 1ed80132a..6c392938d 100644 --- a/session.go +++ b/session.go @@ -457,6 +457,10 @@ func (s *session) Context() context.Context { return s.ctx } +func (s *session) ConnectionState() ConnectionState { + return s.cryptoSetup.ConnectionState() +} + func (s *session) maybeResetTimer() { var deadline time.Time if s.config.KeepAlive && s.handshakeComplete && !s.keepAlivePingSent {