add support for ECDSA private keys

fixes #158
This commit is contained in:
Lucas Clemente
2016-05-31 23:06:38 +02:00
parent 8fa3e62de9
commit 981d4e7fb8
6 changed files with 289 additions and 228 deletions

View File

@@ -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")
}

View File

@@ -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]))
})
})
})

97
crypto/proof_source.go Normal file
View File

@@ -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")
}

190
crypto/proof_source_test.go Normal file
View File

@@ -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]))
})
})
})

View File

@@ -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
}

View File

@@ -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())