diff --git a/crypto/key_derivation.go b/crypto/key_derivation.go new file mode 100644 index 000000000..c37f807e9 --- /dev/null +++ b/crypto/key_derivation.go @@ -0,0 +1,49 @@ +package crypto + +import ( + "github.com/bifurcation/mint" + "github.com/lucas-clemente/quic-go/internal/protocol" +) + +const ( + clientExporterLabel = "EXPORTER-QUIC client 1-RTT Secret" + serverExporterLabel = "EXPORTER-QUIC server 1-RTT Secret" +) + +// MintState contains +type MintState interface { + GetCipherSuite() mint.CipherSuiteParams + ComputeExporter(label string, context []byte, keyLength int) ([]byte, error) +} + +// DeriveAESKeys derives the AES keys and creates a matching AES-GCM AEAD instance +func DeriveAESKeys(ms MintState, pers protocol.Perspective) (AEAD, error) { + var myLabel, otherLabel string + if pers == protocol.PerspectiveClient { + myLabel = clientExporterLabel + otherLabel = serverExporterLabel + } else { + myLabel = serverExporterLabel + otherLabel = clientExporterLabel + } + myKey, myIV, err := computeKeyAndIV(ms, myLabel) + if err != nil { + return nil, err + } + otherKey, otherIV, err := computeKeyAndIV(ms, otherLabel) + if err != nil { + return nil, err + } + return NewAEADAESGCM(otherKey, myKey, otherIV, myIV) +} + +func computeKeyAndIV(ms MintState, label string) (key, iv []byte, err error) { + cs := ms.GetCipherSuite() + secret, err := ms.ComputeExporter(label, nil, cs.Hash.Size()) + if err != nil { + return nil, nil, err + } + key = mint.HkdfExpandLabel(cs.Hash, secret, "key", nil, cs.KeyLen) + iv = mint.HkdfExpandLabel(cs.Hash, secret, "iv", nil, cs.IvLen) + return key, iv, nil +} diff --git a/crypto/key_derivation_test.go b/crypto/key_derivation_test.go new file mode 100644 index 000000000..27419fcce --- /dev/null +++ b/crypto/key_derivation_test.go @@ -0,0 +1,62 @@ +package crypto + +import ( + "crypto" + "errors" + + "github.com/bifurcation/mint" + "github.com/lucas-clemente/quic-go/internal/protocol" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type mockMintState struct { + hash crypto.Hash + computerError error +} + +var _ MintState = &mockMintState{} + +func (s *mockMintState) GetCipherSuite() mint.CipherSuiteParams { + return mint.CipherSuiteParams{ + Hash: s.hash, + KeyLen: 32, + IvLen: 12, + } +} + +func (s *mockMintState) ComputeExporter(label string, context []byte, keyLength int) ([]byte, error) { + if s.computerError != nil { + return nil, s.computerError + } + return append([]byte(label), context...), nil +} + +var _ = Describe("Key Derivation", func() { + It("derives keys", func() { + clientAEAD, err := DeriveAESKeys(&mockMintState{hash: crypto.SHA256}, protocol.PerspectiveClient) + Expect(err).ToNot(HaveOccurred()) + serverAEAD, err := DeriveAESKeys(&mockMintState{hash: crypto.SHA256}, protocol.PerspectiveServer) + Expect(err).ToNot(HaveOccurred()) + ciphertext := clientAEAD.Seal(nil, []byte("foobar"), 0, []byte("aad")) + data, err := serverAEAD.Open(nil, ciphertext, 0, []byte("aad")) + Expect(err).ToNot(HaveOccurred()) + Expect(data).To(Equal([]byte("foobar"))) + }) + + It("fails when different hash functions are used", func() { + clientAEAD, err := DeriveAESKeys(&mockMintState{hash: crypto.SHA256}, protocol.PerspectiveClient) + Expect(err).ToNot(HaveOccurred()) + serverAEAD, err := DeriveAESKeys(&mockMintState{hash: crypto.SHA512}, protocol.PerspectiveServer) + Expect(err).ToNot(HaveOccurred()) + ciphertext := clientAEAD.Seal(nil, []byte("foobar"), 0, []byte("aad")) + _, err = serverAEAD.Open(nil, ciphertext, 0, []byte("aad")) + Expect(err).To(MatchError("cipher: message authentication failed")) + }) + + It("fails when computing the exporter fails", func() { + testErr := errors.New("test error") + _, err := DeriveAESKeys(&mockMintState{hash: crypto.SHA256, computerError: testErr}, protocol.PerspectiveClient) + Expect(err).To(MatchError(testErr)) + }) +})