From 981d4e7fb878fa9fde25c1a8746c838bda141511 Mon Sep 17 00:00:00 2001 From: Lucas Clemente Date: Tue, 31 May 2016 23:06:38 +0200 Subject: [PATCH] add support for ECDSA private keys fixes #158 --- crypto/proof_rsa.go | 95 ------------------ crypto/proof_rsa_test.go | 131 ------------------------- crypto/proof_source.go | 97 ++++++++++++++++++ crypto/proof_source_test.go | 190 ++++++++++++++++++++++++++++++++++++ server.go | 2 +- session_test.go | 2 +- 6 files changed, 289 insertions(+), 228 deletions(-) delete mode 100644 crypto/proof_rsa.go delete mode 100644 crypto/proof_rsa_test.go create mode 100644 crypto/proof_source.go create mode 100644 crypto/proof_source_test.go diff --git a/crypto/proof_rsa.go b/crypto/proof_rsa.go deleted file mode 100644 index 1db0a244..00000000 --- a/crypto/proof_rsa.go +++ /dev/null @@ -1,95 +0,0 @@ -package crypto - -import ( - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/tls" - "errors" - "strings" -) - -// rsaSigner stores a key and a certificate for the server proof -type rsaSigner struct { - config *tls.Config -} - -// NewRSASigner loads the key and cert from files -func NewRSASigner(tlsConfig *tls.Config) (Signer, error) { - return &rsaSigner{config: tlsConfig}, nil -} - -// SignServerProof signs CHLO and server config for use in the server proof -func (kd *rsaSigner) SignServerProof(sni string, chlo []byte, serverConfigData []byte) ([]byte, error) { - cert, err := kd.getCertForSNI(sni) - if err != nil { - return nil, err - } - key, ok := cert.PrivateKey.(*rsa.PrivateKey) - if !ok { - return nil, errors.New("only RSA keys are supported for now") - } - - hash := sha256.New() - if len(chlo) > 0 { - hash.Write([]byte("QUIC CHLO and server config signature\x00")) - chloHash := sha256.Sum256(chlo) - hash.Write([]byte{32, 0, 0, 0}) - hash.Write(chloHash[:]) - } else { - // TODO: Remove when we drop support for version 30 - hash.Write([]byte("QUIC server config signature\x00")) - } - hash.Write(serverConfigData) - return rsa.SignPSS( - rand.Reader, - key, - crypto.SHA256, - hash.Sum(nil), - &rsa.PSSOptions{SaltLength: 32}, - ) -} - -// GetCertsCompressed gets the certificate in the format described by the QUIC crypto doc -func (kd *rsaSigner) GetCertsCompressed(sni string, pCommonSetHashes, pCachedHashes []byte) ([]byte, error) { - cert, err := kd.getCertForSNI(sni) - if err != nil { - return nil, err - } - return compressChain(cert.Certificate, pCommonSetHashes, pCachedHashes) -} - -// GetLeafCert gets the leaf certificate -func (kd *rsaSigner) GetLeafCert(sni string) ([]byte, error) { - cert, err := kd.getCertForSNI(sni) - if err != nil { - return nil, err - } - return cert.Certificate[0], nil -} - -func (kd *rsaSigner) getCertForSNI(sni string) (*tls.Certificate, error) { - if kd.config.GetCertificate != nil { - cert, err := kd.config.GetCertificate(&tls.ClientHelloInfo{ServerName: sni}) - if err != nil { - return nil, err - } - if cert != nil { - return cert, nil - } - } - if len(kd.config.NameToCertificate) != 0 { - if cert, ok := kd.config.NameToCertificate[sni]; ok { - return cert, nil - } - wildcardSNI := "*" + strings.TrimLeftFunc(sni, func(r rune) bool { return r != '.' }) - if cert, ok := kd.config.NameToCertificate[wildcardSNI]; ok { - return cert, nil - } - } - if len(kd.config.Certificates) != 0 { - return &kd.config.Certificates[0], nil - } - return nil, errors.New("no matching certificate found") -} diff --git a/crypto/proof_rsa_test.go b/crypto/proof_rsa_test.go deleted file mode 100644 index 63c0eab1..00000000 --- a/crypto/proof_rsa_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package crypto - -import ( - "bytes" - "compress/flate" - "compress/zlib" - "crypto" - "crypto/rsa" - "crypto/tls" - - "github.com/lucas-clemente/quic-go/testdata" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("ProofRsa", func() { - It("compresses certs", func() { - cert := []byte{0xde, 0xca, 0xfb, 0xad} - certZlib := &bytes.Buffer{} - z, err := zlib.NewWriterLevelDict(certZlib, flate.BestCompression, certDictZlib) - Expect(err).ToNot(HaveOccurred()) - z.Write([]byte{0x04, 0x00, 0x00, 0x00}) - z.Write(cert) - z.Close() - kd := &rsaSigner{ - config: &tls.Config{ - Certificates: []tls.Certificate{ - {Certificate: [][]byte{cert}}, - }, - }, - } - certCompressed, err := kd.GetCertsCompressed("", nil, nil) - Expect(err).ToNot(HaveOccurred()) - Expect(certCompressed).To(Equal(append([]byte{ - 0x01, 0x00, - 0x08, 0x00, 0x00, 0x00, - }, certZlib.Bytes()...))) - }) - - It("gives valid signatures", func() { - key := testdata.GetTLSConfig().Certificates[0].PrivateKey.(*rsa.PrivateKey).Public().(*rsa.PublicKey) - kd, err := NewRSASigner(testdata.GetTLSConfig()) - Expect(err).ToNot(HaveOccurred()) - signature, err := kd.SignServerProof("", []byte{'C', 'H', 'L', 'O'}, []byte{'S', 'C', 'F', 'G'}) - Expect(err).ToNot(HaveOccurred()) - // Generated with: - // ruby -e 'require "digest"; p Digest::SHA256.digest("QUIC CHLO and server config signature\x00" + "\x20\x00\x00\x00" + Digest::SHA256.digest("CHLO") + "SCFG")' - data := []byte("W\xA6\xFC\xDE\xC7\xD2>c\xE6\xB5\xF6\tq\x9E|<~1\xA33\x01\xCA=\x19\xBD\xC1\xE4\xB0\xBA\x9B\x16%") - err = rsa.VerifyPSS(key, crypto.SHA256, data, signature, &rsa.PSSOptions{SaltLength: 32}) - Expect(err).ToNot(HaveOccurred()) - }) - - It("gives valid signatures for version 30", func() { - key := testdata.GetTLSConfig().Certificates[0].PrivateKey.(*rsa.PrivateKey).Public().(*rsa.PublicKey) - kd, err := NewRSASigner(testdata.GetTLSConfig()) - Expect(err).ToNot(HaveOccurred()) - signature, err := kd.SignServerProof("", nil, []byte{'S', 'C', 'F', 'G'}) - Expect(err).ToNot(HaveOccurred()) - // Generated with: - // ruby -e 'require "digest"; p Digest::SHA256.digest("QUIC server config signature\x00" + "SCFG")' - data := []byte("\x1D\xBB\v\xE9\x14\xD5Q\v\x83\xDB\xA7\x91\xB7\xDAO\xC2\xD3\xE6\xCC\xB2\xE8\xC3QW\x86\t\xB4\b6\x9C\x91C") - err = rsa.VerifyPSS(key, crypto.SHA256, data, signature, &rsa.PSSOptions{SaltLength: 32}) - Expect(err).ToNot(HaveOccurred()) - }) - - Context("retrieving certificate", func() { - var ( - signer *rsaSigner - config *tls.Config - cert tls.Certificate - ) - - BeforeEach(func() { - cert = testdata.GetCertificate() - config = &tls.Config{} - signer = &rsaSigner{config: config} - }) - - It("errors without certificates", func() { - _, err := signer.getCertForSNI("") - Expect(err).To(MatchError("no matching certificate found")) - }) - - It("uses first certificate in config.Certificates", func() { - config.Certificates = []tls.Certificate{cert} - cert, err := signer.getCertForSNI("") - Expect(err).ToNot(HaveOccurred()) - Expect(cert.PrivateKey).ToNot(BeNil()) - Expect(cert.Certificate[0]).ToNot(BeNil()) - }) - - It("uses NameToCertificate entries", func() { - config.NameToCertificate = map[string]*tls.Certificate{ - "quic.clemente.io": &cert, - } - cert, err := signer.getCertForSNI("quic.clemente.io") - Expect(err).ToNot(HaveOccurred()) - Expect(cert.PrivateKey).ToNot(BeNil()) - Expect(cert.Certificate[0]).ToNot(BeNil()) - }) - - It("uses NameToCertificate entries with wildcard", func() { - config.NameToCertificate = map[string]*tls.Certificate{ - "*.clemente.io": &cert, - } - cert, err := signer.getCertForSNI("quic.clemente.io") - Expect(err).ToNot(HaveOccurred()) - Expect(cert.PrivateKey).ToNot(BeNil()) - Expect(cert.Certificate[0]).ToNot(BeNil()) - }) - - It("uses GetCertificate", func() { - config.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - Expect(clientHello.ServerName).To(Equal("quic.clemente.io")) - return &cert, nil - } - cert, err := signer.getCertForSNI("quic.clemente.io") - Expect(err).ToNot(HaveOccurred()) - Expect(cert.PrivateKey).ToNot(BeNil()) - Expect(cert.Certificate[0]).ToNot(BeNil()) - }) - - It("gets leaf certificates", func() { - config.Certificates = []tls.Certificate{cert} - cert2, err := signer.GetLeafCert("") - Expect(err).ToNot(HaveOccurred()) - Expect(cert2).To(Equal(cert.Certificate[0])) - }) - }) -}) diff --git a/crypto/proof_source.go b/crypto/proof_source.go new file mode 100644 index 00000000..38a7276a --- /dev/null +++ b/crypto/proof_source.go @@ -0,0 +1,97 @@ +package crypto + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "errors" + "strings" +) + +// proofSource stores a key and a certificate for the server proof +type proofSource struct { + config *tls.Config +} + +// NewProofSource loads the key and cert from files +func NewProofSource(tlsConfig *tls.Config) (Signer, error) { + return &proofSource{config: tlsConfig}, nil +} + +// SignServerProof signs CHLO and server config for use in the server proof +func (ps *proofSource) SignServerProof(sni string, chlo []byte, serverConfigData []byte) ([]byte, error) { + cert, err := ps.getCertForSNI(sni) + if err != nil { + return nil, err + } + + hash := sha256.New() + if len(chlo) > 0 { + hash.Write([]byte("QUIC CHLO and server config signature\x00")) + chloHash := sha256.Sum256(chlo) + hash.Write([]byte{32, 0, 0, 0}) + hash.Write(chloHash[:]) + } else { + // TODO: Remove when we drop support for version 30 + hash.Write([]byte("QUIC server config signature\x00")) + } + hash.Write(serverConfigData) + + key, ok := cert.PrivateKey.(crypto.Signer) + if !ok { + return nil, errors.New("expected PrivateKey to implement crypto.Signer") + } + + opts := crypto.SignerOpts(crypto.SHA256) + + if _, ok = key.(*rsa.PrivateKey); ok { + opts = &rsa.PSSOptions{SaltLength: 32, Hash: crypto.SHA256} + } + + return key.Sign(rand.Reader, hash.Sum(nil), opts) +} + +// GetCertsCompressed gets the certificate in the format described by the QUIC crypto doc +func (ps *proofSource) GetCertsCompressed(sni string, pCommonSetHashes, pCachedHashes []byte) ([]byte, error) { + cert, err := ps.getCertForSNI(sni) + if err != nil { + return nil, err + } + return compressChain(cert.Certificate, pCommonSetHashes, pCachedHashes) +} + +// GetLeafCert gets the leaf certificate +func (ps *proofSource) GetLeafCert(sni string) ([]byte, error) { + cert, err := ps.getCertForSNI(sni) + if err != nil { + return nil, err + } + return cert.Certificate[0], nil +} + +func (ps *proofSource) getCertForSNI(sni string) (*tls.Certificate, error) { + if ps.config.GetCertificate != nil { + cert, err := ps.config.GetCertificate(&tls.ClientHelloInfo{ServerName: sni}) + if err != nil { + return nil, err + } + if cert != nil { + return cert, nil + } + } + if len(ps.config.NameToCertificate) != 0 { + if cert, ok := ps.config.NameToCertificate[sni]; ok { + return cert, nil + } + wildcardSNI := "*" + strings.TrimLeftFunc(sni, func(r rune) bool { return r != '.' }) + if cert, ok := ps.config.NameToCertificate[wildcardSNI]; ok { + return cert, nil + } + } + if len(ps.config.Certificates) != 0 { + return &ps.config.Certificates[0], nil + } + return nil, errors.New("no matching certificate found") +} diff --git a/crypto/proof_source_test.go b/crypto/proof_source_test.go new file mode 100644 index 00000000..dab0609f --- /dev/null +++ b/crypto/proof_source_test.go @@ -0,0 +1,190 @@ +package crypto + +import ( + "bytes" + "compress/flate" + "compress/zlib" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "encoding/asn1" + "math/big" + + "github.com/lucas-clemente/quic-go/testdata" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type ecdsaSignature struct { + R, S *big.Int +} + +var _ = Describe("ProofRsa", func() { + It("compresses certs", func() { + cert := []byte{0xde, 0xca, 0xfb, 0xad} + certZlib := &bytes.Buffer{} + z, err := zlib.NewWriterLevelDict(certZlib, flate.BestCompression, certDictZlib) + Expect(err).ToNot(HaveOccurred()) + z.Write([]byte{0x04, 0x00, 0x00, 0x00}) + z.Write(cert) + z.Close() + kd := &proofSource{ + config: &tls.Config{ + Certificates: []tls.Certificate{ + {Certificate: [][]byte{cert}}, + }, + }, + } + certCompressed, err := kd.GetCertsCompressed("", nil, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(certCompressed).To(Equal(append([]byte{ + 0x01, 0x00, + 0x08, 0x00, 0x00, 0x00, + }, certZlib.Bytes()...))) + }) + + Context("when using RSA", func() { + It("gives valid signatures", func() { + key := testdata.GetTLSConfig().Certificates[0].PrivateKey.(*rsa.PrivateKey).Public().(*rsa.PublicKey) + kd, err := NewProofSource(testdata.GetTLSConfig()) + Expect(err).ToNot(HaveOccurred()) + signature, err := kd.SignServerProof("", []byte{'C', 'H', 'L', 'O'}, []byte{'S', 'C', 'F', 'G'}) + Expect(err).ToNot(HaveOccurred()) + // Generated with: + // ruby -e 'require "digest"; p Digest::SHA256.digest("QUIC CHLO and server config signature\x00" + "\x20\x00\x00\x00" + Digest::SHA256.digest("CHLO") + "SCFG")' + data := []byte("W\xA6\xFC\xDE\xC7\xD2>c\xE6\xB5\xF6\tq\x9E|<~1\xA33\x01\xCA=\x19\xBD\xC1\xE4\xB0\xBA\x9B\x16%") + err = rsa.VerifyPSS(key, crypto.SHA256, data, signature, &rsa.PSSOptions{SaltLength: 32}) + Expect(err).ToNot(HaveOccurred()) + }) + + It("gives valid signatures for version 30", func() { + key := testdata.GetTLSConfig().Certificates[0].PrivateKey.(*rsa.PrivateKey).Public().(*rsa.PublicKey) + kd, err := NewProofSource(testdata.GetTLSConfig()) + Expect(err).ToNot(HaveOccurred()) + signature, err := kd.SignServerProof("", nil, []byte{'S', 'C', 'F', 'G'}) + Expect(err).ToNot(HaveOccurred()) + // Generated with: + // ruby -e 'require "digest"; p Digest::SHA256.digest("QUIC server config signature\x00" + "SCFG")' + data := []byte("\x1D\xBB\v\xE9\x14\xD5Q\v\x83\xDB\xA7\x91\xB7\xDAO\xC2\xD3\xE6\xCC\xB2\xE8\xC3QW\x86\t\xB4\b6\x9C\x91C") + err = rsa.VerifyPSS(key, crypto.SHA256, data, signature, &rsa.PSSOptions{SaltLength: 32}) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("when using ECDSA", func() { + var ( + key crypto.Signer + config *tls.Config + ) + + BeforeEach(func() { + var err error + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + Expect(err).NotTo(HaveOccurred()) + config = &tls.Config{ + Certificates: []tls.Certificate{ + {PrivateKey: key}, + }, + } + }) + + It("gives valid signatures", func() { + kd, err := NewProofSource(config) + Expect(err).ToNot(HaveOccurred()) + signature, err := kd.SignServerProof("", []byte{'C', 'H', 'L', 'O'}, []byte{'S', 'C', 'F', 'G'}) + Expect(err).ToNot(HaveOccurred()) + // Generated with: + // ruby -e 'require "digest"; p Digest::SHA256.digest("QUIC CHLO and server config signature\x00" + "\x20\x00\x00\x00" + Digest::SHA256.digest("CHLO") + "SCFG")' + data := []byte("W\xA6\xFC\xDE\xC7\xD2>c\xE6\xB5\xF6\tq\x9E|<~1\xA33\x01\xCA=\x19\xBD\xC1\xE4\xB0\xBA\x9B\x16%") + s := &ecdsaSignature{} + _, err = asn1.Unmarshal(signature, s) + Expect(err).NotTo(HaveOccurred()) + b := ecdsa.Verify(key.Public().(*ecdsa.PublicKey), data, s.R, s.S) + Expect(b).To(BeTrue()) + }) + + It("gives valid signatures for version 30", func() { + kd, err := NewProofSource(config) + Expect(err).ToNot(HaveOccurred()) + signature, err := kd.SignServerProof("", nil, []byte{'S', 'C', 'F', 'G'}) + Expect(err).ToNot(HaveOccurred()) + // Generated with: + // ruby -e 'require "digest"; p Digest::SHA256.digest("QUIC server config signature\x00" + "SCFG")' + data := []byte("\x1D\xBB\v\xE9\x14\xD5Q\v\x83\xDB\xA7\x91\xB7\xDAO\xC2\xD3\xE6\xCC\xB2\xE8\xC3QW\x86\t\xB4\b6\x9C\x91C") + s := &ecdsaSignature{} + _, err = asn1.Unmarshal(signature, s) + Expect(err).NotTo(HaveOccurred()) + b := ecdsa.Verify(key.Public().(*ecdsa.PublicKey), data, s.R, s.S) + Expect(b).To(BeTrue()) + }) + }) + + Context("retrieving certificate", func() { + var ( + signer *proofSource + config *tls.Config + cert tls.Certificate + ) + + BeforeEach(func() { + cert = testdata.GetCertificate() + config = &tls.Config{} + signer = &proofSource{config: config} + }) + + It("errors without certificates", func() { + _, err := signer.getCertForSNI("") + Expect(err).To(MatchError("no matching certificate found")) + }) + + It("uses first certificate in config.Certificates", func() { + config.Certificates = []tls.Certificate{cert} + cert, err := signer.getCertForSNI("") + Expect(err).ToNot(HaveOccurred()) + Expect(cert.PrivateKey).ToNot(BeNil()) + Expect(cert.Certificate[0]).ToNot(BeNil()) + }) + + It("uses NameToCertificate entries", func() { + config.NameToCertificate = map[string]*tls.Certificate{ + "quic.clemente.io": &cert, + } + cert, err := signer.getCertForSNI("quic.clemente.io") + Expect(err).ToNot(HaveOccurred()) + Expect(cert.PrivateKey).ToNot(BeNil()) + Expect(cert.Certificate[0]).ToNot(BeNil()) + }) + + It("uses NameToCertificate entries with wildcard", func() { + config.NameToCertificate = map[string]*tls.Certificate{ + "*.clemente.io": &cert, + } + cert, err := signer.getCertForSNI("quic.clemente.io") + Expect(err).ToNot(HaveOccurred()) + Expect(cert.PrivateKey).ToNot(BeNil()) + Expect(cert.Certificate[0]).ToNot(BeNil()) + }) + + It("uses GetCertificate", func() { + config.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + Expect(clientHello.ServerName).To(Equal("quic.clemente.io")) + return &cert, nil + } + cert, err := signer.getCertForSNI("quic.clemente.io") + Expect(err).ToNot(HaveOccurred()) + Expect(cert.PrivateKey).ToNot(BeNil()) + Expect(cert.Certificate[0]).ToNot(BeNil()) + }) + + It("gets leaf certificates", func() { + config.Certificates = []tls.Certificate{cert} + cert2, err := signer.GetLeafCert("") + Expect(err).ToNot(HaveOccurred()) + Expect(cert2).To(Equal(cert.Certificate[0])) + }) + }) +}) diff --git a/server.go b/server.go index 4999382d..99107917 100644 --- a/server.go +++ b/server.go @@ -37,7 +37,7 @@ type Server struct { // NewServer makes a new server func NewServer(addr string, tlsConfig *tls.Config, cb StreamCallback) (*Server, error) { - signer, err := crypto.NewRSASigner(tlsConfig) + signer, err := crypto.NewProofSource(tlsConfig) if err != nil { return nil, err } diff --git a/session_test.go b/session_test.go index 2b0b25dc..3f35a9dd 100644 --- a/session_test.go +++ b/session_test.go @@ -47,7 +47,7 @@ var _ = Describe("Session", func() { streamCallbackCalled = false closeCallbackCalled = false - signer, err := crypto.NewRSASigner(testdata.GetTLSConfig()) + signer, err := crypto.NewProofSource(testdata.GetTLSConfig()) Expect(err).ToNot(HaveOccurred()) kex, err := crypto.NewCurve25519KEX() Expect(err).NotTo(HaveOccurred())