diff --git a/internal/crypto/null_aead.go b/internal/crypto/null_aead.go index fa1ccb7b..27158bee 100644 --- a/internal/crypto/null_aead.go +++ b/internal/crypto/null_aead.go @@ -3,9 +3,9 @@ package crypto import "github.com/lucas-clemente/quic-go/internal/protocol" // NewNullAEAD creates a NullAEAD -func NewNullAEAD(p protocol.Perspective, v protocol.VersionNumber) AEAD { +func NewNullAEAD(p protocol.Perspective, connID protocol.ConnectionID, v protocol.VersionNumber) (AEAD, error) { if v.UsesTLS() { - return &nullAEADFNV64a{} + return newNullAEADAESGCM(connID, p) } - return &nullAEADFNV128a{perspective: p} + return &nullAEADFNV128a{perspective: p}, nil } diff --git a/internal/crypto/null_aead_aesgcm.go b/internal/crypto/null_aead_aesgcm.go new file mode 100644 index 00000000..a647ad7f --- /dev/null +++ b/internal/crypto/null_aead_aesgcm.go @@ -0,0 +1,44 @@ +package crypto + +import ( + "crypto" + "encoding/binary" + + "github.com/bifurcation/mint" + "github.com/lucas-clemente/quic-go/internal/protocol" +) + +var quicVersion1Salt = []byte{0xaf, 0xc8, 0x24, 0xec, 0x5f, 0xc7, 0x7e, 0xca, 0x1e, 0x9d, 0x36, 0xf3, 0x7f, 0xb2, 0xd4, 0x65, 0x18, 0xc3, 0x66, 0x39} + +func newNullAEADAESGCM(connectionID protocol.ConnectionID, pers protocol.Perspective) (AEAD, error) { + clientSecret, serverSecret := computeSecrets(connectionID) + + var mySecret, otherSecret []byte + if pers == protocol.PerspectiveClient { + mySecret = clientSecret + otherSecret = serverSecret + } else { + mySecret = serverSecret + otherSecret = clientSecret + } + + myKey, myIV := computeNullAEADKeyAndIV(mySecret) + otherKey, otherIV := computeNullAEADKeyAndIV(otherSecret) + + return NewAEADAESGCM(otherKey, myKey, otherIV, myIV) +} + +func computeSecrets(connectionID protocol.ConnectionID) (clientSecret, serverSecret []byte) { + connID := make([]byte, 8) + binary.BigEndian.PutUint64(connID, uint64(connectionID)) + cleartextSecret := mint.HkdfExtract(crypto.SHA256, []byte(quicVersion1Salt), connID) + clientSecret = mint.HkdfExpandLabel(crypto.SHA256, cleartextSecret, "QUIC client cleartext Secret", []byte{}, crypto.SHA256.Size()) + serverSecret = mint.HkdfExpandLabel(crypto.SHA256, cleartextSecret, "QUIC server cleartext Secret", []byte{}, crypto.SHA256.Size()) + return +} + +func computeNullAEADKeyAndIV(secret []byte) (key, iv []byte) { + key = mint.HkdfExpandLabel(crypto.SHA256, secret, "key", nil, 16) + iv = mint.HkdfExpandLabel(crypto.SHA256, secret, "iv", nil, 12) + return +} diff --git a/internal/crypto/null_aead_aesgcm_test.go b/internal/crypto/null_aead_aesgcm_test.go new file mode 100644 index 00000000..f5d8a49e --- /dev/null +++ b/internal/crypto/null_aead_aesgcm_test.go @@ -0,0 +1,84 @@ +package crypto + +import ( + "github.com/lucas-clemente/quic-go/internal/protocol" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("NullAEAD using AES-GCM", func() { + // values taken from https://github.com/quicwg/base-drafts/wiki/Test-Vector-for-the-Clear-Text-AEAD-key-derivation + Context("using the test vector from the QUIC WG Wiki", func() { + connID := protocol.ConnectionID(0x8394c8f03e515708) + + It("computes the secrets", func() { + clientSecret, serverSecret := computeSecrets(connID) + Expect(clientSecret).To(Equal([]byte{ + 0x31, 0xba, 0x96, 0x68, 0x73, 0xf7, 0xf4, 0x53, + 0xe6, 0xc8, 0xa1, 0xbf, 0x78, 0xed, 0x70, 0x13, + 0xfa, 0xd8, 0x3f, 0xfc, 0xee, 0xfc, 0x95, 0x68, + 0x81, 0xcd, 0x24, 0x1c, 0x0a, 0xe3, 0xa7, 0xa6, + })) + Expect(serverSecret).To(Equal([]byte{ + 0x91, 0xa9, 0xe4, 0x22, 0x2c, 0xcb, 0xb9, 0xa9, + 0x8f, 0x14, 0xc8, 0xe1, 0xbe, 0xfd, 0x6a, 0x79, + 0xf0, 0x4e, 0x42, 0xa2, 0x4f, 0xbe, 0xb4, 0x83, + 0x1f, 0x50, 0x26, 0x80, 0x7a, 0xe8, 0x4c, 0xc3, + })) + }) + + It("computes the client key and IV", func() { + clientSecret, _ := computeSecrets(connID) + key, iv := computeNullAEADKeyAndIV(clientSecret) + Expect(key).To(Equal([]byte{ + 0x2e, 0xbd, 0x78, 0x00, 0xdb, 0xed, 0x20, 0x10, + 0xe5, 0xa2, 0x1c, 0x4a, 0xd2, 0x4b, 0x4e, 0xc3, + })) + Expect(iv).To(Equal([]byte{ + 0x55, 0x44, 0x0d, 0x5f, 0xf7, 0x50, 0x3d, 0xe4, + 0x99, 0x7b, 0xfd, 0x6b, + })) + }) + + It("computes the server key and IV", func() { + _, serverSecret := computeSecrets(connID) + key, iv := computeNullAEADKeyAndIV(serverSecret) + Expect(key).To(Equal([]byte{ + 0xc8, 0xea, 0x1b, 0xc1, 0x71, 0xe5, 0x2b, 0xae, + 0x71, 0xfb, 0x78, 0x39, 0x52, 0xc7, 0xb8, 0xfc, + })) + Expect(iv).To(Equal([]byte{ + 0x57, 0x82, 0x3b, 0x85, 0x2c, 0x7e, 0xf9, 0xe3, + 0x80, 0x2b, 0x69, 0x0b, + })) + }) + }) + + It("seals and opens", func() { + connectionID := protocol.ConnectionID(0x1234567890) + clientAEAD, err := newNullAEADAESGCM(connectionID, protocol.PerspectiveClient) + Expect(err).ToNot(HaveOccurred()) + serverAEAD, err := newNullAEADAESGCM(connectionID, protocol.PerspectiveServer) + Expect(err).ToNot(HaveOccurred()) + + clientMessage := clientAEAD.Seal(nil, []byte("foobar"), 42, []byte("aad")) + m, err := serverAEAD.Open(nil, clientMessage, 42, []byte("aad")) + Expect(err).ToNot(HaveOccurred()) + Expect(m).To(Equal([]byte("foobar"))) + serverMessage := serverAEAD.Seal(nil, []byte("raboof"), 99, []byte("daa")) + m, err = clientAEAD.Open(nil, serverMessage, 99, []byte("daa")) + Expect(err).ToNot(HaveOccurred()) + Expect(m).To(Equal([]byte("raboof"))) + }) + + It("doesn't work if initialized with different connection IDs", func() { + clientAEAD, err := newNullAEADAESGCM(1, protocol.PerspectiveClient) + Expect(err).ToNot(HaveOccurred()) + serverAEAD, err := newNullAEADAESGCM(2, protocol.PerspectiveServer) + Expect(err).ToNot(HaveOccurred()) + + clientMessage := clientAEAD.Seal(nil, []byte("foobar"), 42, []byte("aad")) + _, err = serverAEAD.Open(nil, clientMessage, 42, []byte("aad")) + Expect(err).To(MatchError("cipher: message authentication failed")) + }) +}) diff --git a/internal/crypto/null_aead_fnv128a_test.go b/internal/crypto/null_aead_fnv128a_test.go index 9fbc2a77..1a8fcc6a 100644 --- a/internal/crypto/null_aead_fnv128a_test.go +++ b/internal/crypto/null_aead_fnv128a_test.go @@ -15,8 +15,8 @@ var _ = Describe("NullAEAD using FNV128a", func() { var aeadClient AEAD BeforeEach(func() { - aeadServer = NewNullAEAD(protocol.PerspectiveServer, protocol.Version37) - aeadClient = NewNullAEAD(protocol.PerspectiveClient, protocol.Version37) + aeadServer = &nullAEADFNV128a{protocol.PerspectiveServer} + aeadClient = &nullAEADFNV128a{protocol.PerspectiveClient} }) It("seals and opens, client => server", func() { diff --git a/internal/crypto/null_aead_fnv64a.go b/internal/crypto/null_aead_fnv64a.go deleted file mode 100644 index e3a8cde2..00000000 --- a/internal/crypto/null_aead_fnv64a.go +++ /dev/null @@ -1,50 +0,0 @@ -package crypto - -import ( - "encoding/binary" - "errors" - "hash/fnv" - - "github.com/lucas-clemente/quic-go/internal/protocol" -) - -type nullAEADFNV64a struct{} - -var _ AEAD = &nullAEADFNV64a{} - -// Open and verify the ciphertext -func (n *nullAEADFNV64a) Open(dst, src []byte, packetNumber protocol.PacketNumber, associatedData []byte) ([]byte, error) { - if len(src) < 8 { - return nil, errors.New("NullAEAD: ciphertext cannot be less than 8 bytes long") - } - data := src[:len(src)-8] - - hash := fnv.New64a() - hash.Write(associatedData) - hash.Write(data) - - if hash.Sum64() != binary.BigEndian.Uint64(src[len(src)-8:]) { - return nil, errors.New("NullAEAD: failed to authenticate received data") - } - return data, nil -} - -// Seal writes hash and ciphertext to the buffer -func (n *nullAEADFNV64a) Seal(dst, src []byte, packetNumber protocol.PacketNumber, associatedData []byte) []byte { - if cap(dst) < 8+len(src) { - dst = make([]byte, 8+len(src)) - } else { - dst = dst[:8+len(src)] - } - - hash := fnv.New64a() - hash.Write(associatedData) - hash.Write(src) - copy(dst, src) - binary.BigEndian.PutUint64(dst[len(src):], hash.Sum64()) - return dst -} - -func (n *nullAEADFNV64a) Overhead() int { - return 8 -} diff --git a/internal/crypto/null_aead_fnv64a_test.go b/internal/crypto/null_aead_fnv64a_test.go deleted file mode 100644 index 6d466d34..00000000 --- a/internal/crypto/null_aead_fnv64a_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package crypto - -import ( - "encoding/binary" - "hash/fnv" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("NullAEAD using FNV128a", func() { - var hash64 []byte - var aead AEAD - aad := []byte("All human beings are born free and equal in dignity and rights.") - plainText := []byte("They are endowed with reason and conscience and should act towards one another in a spirit of brotherhood.") - - BeforeEach(func() { - aead = &nullAEADFNV64a{} - hash := fnv.New64a() - hash.Write(aad) - hash.Write(plainText) - hash64 = make([]byte, 8) - binary.BigEndian.PutUint64(hash64, hash.Sum64()) - }) - - It("opens", func() { - data, err := aead.Open(nil, append(plainText, hash64...), 0, aad) - Expect(err).ToNot(HaveOccurred()) - Expect(data).To(Equal(plainText)) - }) - - It("fails", func() { - _, err := aead.Open(nil, append(plainText, hash64...), 0, append(aad, []byte{0x42}...)) - Expect(err).To(MatchError("NullAEAD: failed to authenticate received data")) - }) - - It("rejects short ciphertexts", func() { - _, err := aead.Open(nil, []byte{1, 2, 3, 4, 5, 6, 7}, 0, []byte{}) - Expect(err).To(MatchError("NullAEAD: ciphertext cannot be less than 8 bytes long")) - }) - - It("opens empty messages", func() { - hash := fnv.New64a() - h := make([]byte, 8) - binary.BigEndian.PutUint64(h, hash.Sum64()) - data, err := aead.Open(nil, h, 0, []byte{}) - Expect(err).ToNot(HaveOccurred()) - Expect(data).To(BeEmpty()) - }) - - It("seals", func() { - sealed := aead.Seal(nil, plainText, 0, aad) - Expect(sealed).To(Equal(append(plainText, hash64...))) - Expect(sealed).To(HaveLen(len(plainText) + aead.Overhead())) - }) - - It("seals in-place", func() { - buf := make([]byte, 6, 6+8) - copy(buf, []byte("foobar")) - res := aead.Seal(buf[0:0], buf, 0, nil) - // buf = buf[:8+6] - Expect(buf[:6]).To(Equal([]byte("foobar"))) - // Expect(res[:6]).To(Equal([]byte("foobar"))) - Expect(buf[0 : 6+8]).To(Equal(res)) - }) -}) diff --git a/internal/crypto/null_aead_test.go b/internal/crypto/null_aead_test.go index 092dd9b9..73de963c 100644 --- a/internal/crypto/null_aead_test.go +++ b/internal/crypto/null_aead_test.go @@ -8,9 +8,10 @@ import ( var _ = Describe("NullAEAD", func() { It("selects the right FVN variant", func() { - Expect(NewNullAEAD(protocol.PerspectiveClient, protocol.Version39)).To(Equal(&nullAEADFNV128a{ + connID := protocol.ConnectionID(0x42) + Expect(NewNullAEAD(protocol.PerspectiveClient, connID, protocol.Version39)).To(Equal(&nullAEADFNV128a{ perspective: protocol.PerspectiveClient, })) - Expect(NewNullAEAD(protocol.PerspectiveClient, protocol.VersionTLS)).To(Equal(&nullAEADFNV64a{})) + Expect(NewNullAEAD(protocol.PerspectiveClient, connID, protocol.VersionTLS)).To(BeAssignableToTypeOf(&aeadAESGCM{})) }) }) diff --git a/internal/handshake/crypto_setup_client.go b/internal/handshake/crypto_setup_client.go index 55055c93..7e114721 100644 --- a/internal/handshake/crypto_setup_client.go +++ b/internal/handshake/crypto_setup_client.go @@ -76,6 +76,10 @@ func NewCryptoSetupClient( aeadChanged chan<- protocol.EncryptionLevel, negotiatedVersions []protocol.VersionNumber, ) (CryptoSetup, error) { + nullAEAD, err := crypto.NewNullAEAD(protocol.PerspectiveClient, connID, version) + if err != nil { + return nil, err + } return &cryptoSetupClient{ cryptoStream: cryptoStream, hostname: hostname, @@ -85,7 +89,7 @@ func NewCryptoSetupClient( params: params, keyDerivation: crypto.DeriveQuicCryptoAESKeys, keyExchange: getEphermalKEX, - nullAEAD: crypto.NewNullAEAD(protocol.PerspectiveClient, version), + nullAEAD: nullAEAD, paramsChan: paramsChan, aeadChanged: aeadChanged, negotiatedVersions: negotiatedVersions, diff --git a/internal/handshake/crypto_setup_server.go b/internal/handshake/crypto_setup_server.go index 0db2c09b..37bc7b44 100644 --- a/internal/handshake/crypto_setup_server.go +++ b/internal/handshake/crypto_setup_server.go @@ -78,6 +78,10 @@ func NewCryptoSetup( paramsChan chan<- TransportParameters, aeadChanged chan<- protocol.EncryptionLevel, ) (CryptoSetup, error) { + nullAEAD, err := crypto.NewNullAEAD(protocol.PerspectiveServer, connID, version) + if err != nil { + return nil, err + } return &cryptoSetupServer{ cryptoStream: cryptoStream, connID: connID, @@ -87,7 +91,7 @@ func NewCryptoSetup( scfg: scfg, keyDerivation: crypto.DeriveQuicCryptoAESKeys, keyExchange: getEphermalKEX, - nullAEAD: crypto.NewNullAEAD(protocol.PerspectiveServer, version), + nullAEAD: nullAEAD, params: params, acceptSTKCallback: acceptSTK, sentSHLO: make(chan struct{}), diff --git a/internal/handshake/crypto_setup_tls.go b/internal/handshake/crypto_setup_tls.go index e360fbf6..398bebb4 100644 --- a/internal/handshake/crypto_setup_tls.go +++ b/internal/handshake/crypto_setup_tls.go @@ -32,6 +32,7 @@ type cryptoSetupTLS struct { // NewCryptoSetupTLSServer creates a new TLS CryptoSetup instance for a server func NewCryptoSetupTLSServer( cryptoStream io.ReadWriter, + connID protocol.ConnectionID, tlsConfig *tls.Config, params *TransportParameters, paramsChan chan<- TransportParameters, @@ -49,10 +50,15 @@ func NewCryptoSetupTLSServer( return nil, err } + nullAEAD, err := crypto.NewNullAEAD(protocol.PerspectiveServer, connID, version) + if err != nil { + return nil, err + } + return &cryptoSetupTLS{ perspective: protocol.PerspectiveServer, tls: &mintController{mintConn}, - nullAEAD: crypto.NewNullAEAD(protocol.PerspectiveServer, version), + nullAEAD: nullAEAD, keyDerivation: crypto.DeriveAESKeys, aeadChanged: aeadChanged, }, nil @@ -61,7 +67,8 @@ func NewCryptoSetupTLSServer( // NewCryptoSetupTLSClient creates a new TLS CryptoSetup instance for a client func NewCryptoSetupTLSClient( cryptoStream io.ReadWriter, - hostname string, // only needed for the client + connID protocol.ConnectionID, + hostname string, tlsConfig *tls.Config, params *TransportParameters, paramsChan chan<- TransportParameters, @@ -81,10 +88,15 @@ func NewCryptoSetupTLSClient( return nil, err } + nullAEAD, err := crypto.NewNullAEAD(protocol.PerspectiveClient, connID, version) + if err != nil { + return nil, err + } + return &cryptoSetupTLS{ perspective: protocol.PerspectiveClient, tls: &mintController{mintConn}, - nullAEAD: crypto.NewNullAEAD(protocol.PerspectiveClient, version), + nullAEAD: nullAEAD, keyDerivation: crypto.DeriveAESKeys, aeadChanged: aeadChanged, }, nil diff --git a/internal/handshake/crypto_setup_tls_test.go b/internal/handshake/crypto_setup_tls_test.go index bb0f378a..d950c11a 100644 --- a/internal/handshake/crypto_setup_tls_test.go +++ b/internal/handshake/crypto_setup_tls_test.go @@ -47,6 +47,7 @@ var _ = Describe("TLS Crypto Setup", func() { aeadChanged = make(chan protocol.EncryptionLevel, 2) csInt, err := NewCryptoSetupTLSServer( nil, + 1, testdata.GetTLSConfig(), &TransportParameters{}, paramsChan, diff --git a/packet_unpacker_test.go b/packet_unpacker_test.go index e7150ac0..10a60cd8 100644 --- a/packet_unpacker_test.go +++ b/packet_unpacker_test.go @@ -17,12 +17,14 @@ type mockAEAD struct { } func (m *mockAEAD) Open(dst, src []byte, packetNumber protocol.PacketNumber, associatedData []byte) ([]byte, protocol.EncryptionLevel, error) { - nullAEAD := crypto.NewNullAEAD(protocol.PerspectiveClient, protocol.VersionWhatever) + nullAEAD, err := crypto.NewNullAEAD(protocol.PerspectiveClient, 0x1337, protocol.VersionWhatever) + Expect(err).ToNot(HaveOccurred()) res, err := nullAEAD.Open(dst, src, packetNumber, associatedData) return res, m.encLevelOpen, err } func (m *mockAEAD) Seal(dst, src []byte, packetNumber protocol.PacketNumber, associatedData []byte) ([]byte, protocol.EncryptionLevel) { - nullAEAD := crypto.NewNullAEAD(protocol.PerspectiveServer, protocol.VersionWhatever) + nullAEAD, err := crypto.NewNullAEAD(protocol.PerspectiveServer, 0x1337, protocol.VersionWhatever) + Expect(err).ToNot(HaveOccurred()) return nullAEAD.Seal(dst, src, packetNumber, associatedData), protocol.EncryptionUnspecified } diff --git a/server_test.go b/server_test.go index abd60a20..fd95182b 100644 --- a/server_test.go +++ b/server_test.go @@ -185,8 +185,9 @@ var _ = Describe("Server", func() { It("closes and deletes sessions", func() { serv.deleteClosedSessionsAfter = time.Second // make sure that the nil value for the closed session doesn't get deleted in this test - nullAEAD := crypto.NewNullAEAD(protocol.PerspectiveServer, protocol.VersionWhatever) - err := serv.handlePacket(nil, nil, append(firstPacket, nullAEAD.Seal(nil, nil, 0, firstPacket)...)) + nullAEAD, err := crypto.NewNullAEAD(protocol.PerspectiveServer, connID, protocol.VersionWhatever) + Expect(err).ToNot(HaveOccurred()) + err = serv.handlePacket(nil, nil, append(firstPacket, nullAEAD.Seal(nil, nil, 0, firstPacket)...)) Expect(err).ToNot(HaveOccurred()) Expect(serv.sessions).To(HaveLen(1)) Expect(serv.sessions[connID]).ToNot(BeNil()) @@ -199,8 +200,9 @@ var _ = Describe("Server", func() { It("deletes nil session entries after a wait time", func() { serv.deleteClosedSessionsAfter = 25 * time.Millisecond - nullAEAD := crypto.NewNullAEAD(protocol.PerspectiveServer, protocol.VersionWhatever) - err := serv.handlePacket(nil, nil, append(firstPacket, nullAEAD.Seal(nil, nil, 0, firstPacket)...)) + nullAEAD, err := crypto.NewNullAEAD(protocol.PerspectiveServer, connID, protocol.VersionWhatever) + Expect(err).ToNot(HaveOccurred()) + err = serv.handlePacket(nil, nil, append(firstPacket, nullAEAD.Seal(nil, nil, 0, firstPacket)...)) Expect(err).ToNot(HaveOccurred()) Expect(serv.sessions).To(HaveLen(1)) Expect(serv.sessions).To(HaveKey(connID)) diff --git a/session.go b/session.go index 11998fe5..7f413cde 100644 --- a/session.go +++ b/session.go @@ -212,6 +212,7 @@ func (s *session) setup( if s.version.UsesTLS() { s.cryptoSetup, err = handshake.NewCryptoSetupTLSServer( s.cryptoStream, + s.connectionID, tlsConf, transportParams, paramsChan, @@ -238,6 +239,7 @@ func (s *session) setup( if s.version.UsesTLS() { s.cryptoSetup, err = handshake.NewCryptoSetupTLSClient( s.cryptoStream, + s.connectionID, hostname, tlsConf, transportParams,