forked from quic-go/quic-go
The hostname is set to tls.Config.ServerName in the client already, thus we don't have to read that value again when verifying the certificate.
349 lines
11 KiB
Go
349 lines
11 KiB
Go
package crypto
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"math/big"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/lucas-clemente/quic-go/qerr"
|
|
"github.com/lucas-clemente/quic-go/testdata"
|
|
|
|
. "github.com/onsi/ginkgo"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Cert Manager", func() {
|
|
var cm *certManager
|
|
var key1, key2 *rsa.PrivateKey
|
|
var cert1, cert2 []byte
|
|
|
|
BeforeEach(func() {
|
|
var err error
|
|
cm = NewCertManager(nil).(*certManager)
|
|
key1, err = rsa.GenerateKey(rand.Reader, 768)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
key2, err = rsa.GenerateKey(rand.Reader, 768)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
template := &x509.Certificate{SerialNumber: big.NewInt(1)}
|
|
cert1, err = x509.CreateCertificate(rand.Reader, template, template, &key1.PublicKey, key1)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
cert2, err = x509.CreateCertificate(rand.Reader, template, template, &key2.PublicKey, key2)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("saves a client TLS config", func() {
|
|
tlsConf := &tls.Config{ServerName: "quic.clemente.io"}
|
|
cm = NewCertManager(tlsConf).(*certManager)
|
|
Expect(cm.config.ServerName).To(Equal("quic.clemente.io"))
|
|
})
|
|
|
|
It("errors when given invalid data", func() {
|
|
err := cm.SetData([]byte("foobar"))
|
|
Expect(err).To(MatchError(qerr.Error(qerr.InvalidCryptoMessageParameter, "Certificate data invalid")))
|
|
})
|
|
|
|
It("gets the common certificate hashes", func() {
|
|
ccs := cm.GetCommonCertificateHashes()
|
|
Expect(ccs).ToNot(BeEmpty())
|
|
})
|
|
|
|
Context("setting the data", func() {
|
|
It("decompresses a certificate chain", func() {
|
|
chain := [][]byte{cert1, cert2}
|
|
compressed, err := compressChain(chain, nil, nil)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
err = cm.SetData(compressed)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(cm.chain[0].Raw).To(Equal(cert1))
|
|
Expect(cm.chain[1].Raw).To(Equal(cert2))
|
|
})
|
|
|
|
It("errors if it can't decompress the chain", func() {
|
|
err := cm.SetData([]byte("invalid data"))
|
|
Expect(err).To(MatchError(qerr.Error(qerr.InvalidCryptoMessageParameter, "Certificate data invalid")))
|
|
})
|
|
|
|
It("errors if it can't parse a certificate", func() {
|
|
chain := [][]byte{[]byte("cert1"), []byte("cert2")}
|
|
compressed, err := compressChain(chain, nil, nil)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
err = cm.SetData(compressed)
|
|
_, ok := err.(asn1.StructuralError)
|
|
Expect(ok).To(BeTrue())
|
|
})
|
|
})
|
|
|
|
Context("getting the leaf cert", func() {
|
|
It("gets it", func() {
|
|
xcert1, err := x509.ParseCertificate(cert1)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
xcert2, err := x509.ParseCertificate(cert2)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
cm.chain = []*x509.Certificate{xcert1, xcert2}
|
|
leafCert := cm.GetLeafCert()
|
|
Expect(leafCert).To(Equal(cert1))
|
|
})
|
|
|
|
It("returns nil if the chain hasn't been set yet", func() {
|
|
leafCert := cm.GetLeafCert()
|
|
Expect(leafCert).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("getting the leaf cert hash", func() {
|
|
It("calculates the FVN1a 64 hash", func() {
|
|
cm.chain = make([]*x509.Certificate, 1)
|
|
cm.chain[0] = &x509.Certificate{
|
|
Raw: []byte("test fnv hash"),
|
|
}
|
|
hash, err := cm.GetLeafCertHash()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
// hash calculated on http://www.nitrxgen.net/hashgen/
|
|
Expect(hash).To(Equal(uint64(0x4770f6141fa0f5ad)))
|
|
})
|
|
|
|
It("errors if the certificate chain is not loaded", func() {
|
|
_, err := cm.GetLeafCertHash()
|
|
Expect(err).To(MatchError(errNoCertificateChain))
|
|
})
|
|
})
|
|
|
|
Context("verifying the server config signature", func() {
|
|
It("returns false when the chain hasn't been set yet", func() {
|
|
valid := cm.VerifyServerProof([]byte("proof"), []byte("chlo"), []byte("scfg"))
|
|
Expect(valid).To(BeFalse())
|
|
})
|
|
|
|
It("verifies the signature", func() {
|
|
chlo := []byte("client hello")
|
|
scfg := []byte("server config data")
|
|
xcert1, err := x509.ParseCertificate(cert1)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
cm.chain = []*x509.Certificate{xcert1}
|
|
proof, err := signServerProof(&tls.Certificate{PrivateKey: key1}, chlo, scfg)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
valid := cm.VerifyServerProof(proof, chlo, scfg)
|
|
Expect(valid).To(BeTrue())
|
|
})
|
|
|
|
It("rejects an invalid signature", func() {
|
|
xcert1, err := x509.ParseCertificate(cert1)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
cm.chain = []*x509.Certificate{xcert1}
|
|
valid := cm.VerifyServerProof([]byte("invalid proof"), []byte("chlo"), []byte("scfg"))
|
|
Expect(valid).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Context("verifying the certificate chain", func() {
|
|
generateCertificate := func(template, parent *x509.Certificate, pubKey *rsa.PublicKey, privKey *rsa.PrivateKey) *x509.Certificate {
|
|
certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pubKey, privKey)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
cert, err := x509.ParseCertificate(certDER)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
return cert
|
|
}
|
|
|
|
getCertificate := func(template *x509.Certificate) (*rsa.PrivateKey, *x509.Certificate) {
|
|
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
return key, generateCertificate(template, template, &key.PublicKey, key)
|
|
}
|
|
|
|
It("accepts a valid certificate", func() {
|
|
cc := NewCertChain(testdata.GetTLSConfig()).(*certChain)
|
|
tlsCert, err := cc.getCertForSNI("quic.clemente.io")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
for _, data := range tlsCert.Certificate {
|
|
var cert *x509.Certificate
|
|
cert, err = x509.ParseCertificate(data)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
cm.chain = append(cm.chain, cert)
|
|
}
|
|
err = cm.Verify("quic.clemente.io")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("doesn't accept an expired certificate", func() {
|
|
if runtime.GOOS == "windows" {
|
|
// certificate validation works different on windows, see https://golang.org/src/crypto/x509/verify.go line 238
|
|
Skip("windows")
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
NotBefore: time.Now().Add(-25 * time.Hour),
|
|
NotAfter: time.Now().Add(-time.Hour),
|
|
}
|
|
_, leafCert := getCertificate(template)
|
|
|
|
cm.chain = []*x509.Certificate{leafCert}
|
|
err := cm.Verify("")
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.(x509.CertificateInvalidError).Reason).To(Equal(x509.Expired))
|
|
})
|
|
|
|
It("doesn't accept a certificate that is not yet valid", func() {
|
|
if runtime.GOOS == "windows" {
|
|
// certificate validation works different on windows, see https://golang.org/src/crypto/x509/verify.go line 238
|
|
Skip("windows")
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
NotBefore: time.Now().Add(time.Hour),
|
|
NotAfter: time.Now().Add(25 * time.Hour),
|
|
}
|
|
_, leafCert := getCertificate(template)
|
|
|
|
cm.chain = []*x509.Certificate{leafCert}
|
|
err := cm.Verify("")
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.(x509.CertificateInvalidError).Reason).To(Equal(x509.Expired))
|
|
})
|
|
|
|
It("doesn't accept an certificate for the wrong hostname", func() {
|
|
if runtime.GOOS == "windows" {
|
|
// certificate validation works different on windows, see https://golang.org/src/crypto/x509/verify.go line 238
|
|
Skip("windows")
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
Subject: pkix.Name{CommonName: "google.com"},
|
|
}
|
|
_, leafCert := getCertificate(template)
|
|
|
|
cm.chain = []*x509.Certificate{leafCert}
|
|
err := cm.Verify("quic.clemente.io")
|
|
Expect(err).To(HaveOccurred())
|
|
_, ok := err.(x509.HostnameError)
|
|
Expect(ok).To(BeTrue())
|
|
})
|
|
|
|
It("errors if the chain hasn't been set yet", func() {
|
|
err := cm.Verify("example.com")
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
|
|
// this tests relies on LetsEncrypt not being contained in the Root CAs
|
|
It("rejects valid certificate with missing certificate chain", func() {
|
|
if runtime.GOOS == "windows" {
|
|
Skip("LetsEncrypt Root CA is included in Windows")
|
|
}
|
|
|
|
cert := testdata.GetCertificate()
|
|
xcert, err := x509.ParseCertificate(cert.Certificate[0])
|
|
Expect(err).ToNot(HaveOccurred())
|
|
cm.chain = []*x509.Certificate{xcert}
|
|
err = cm.Verify("quic.clemente.io")
|
|
_, ok := err.(x509.UnknownAuthorityError)
|
|
Expect(ok).To(BeTrue())
|
|
})
|
|
|
|
It("doesn't do any certificate verification if InsecureSkipVerify is set", func() {
|
|
if runtime.GOOS == "windows" {
|
|
// certificate validation works different on windows, see https://golang.org/src/crypto/x509/verify.go line 238
|
|
Skip("windows")
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
}
|
|
|
|
_, leafCert := getCertificate(template)
|
|
cm.config = &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
}
|
|
cm.chain = []*x509.Certificate{leafCert}
|
|
err := cm.Verify("quic.clemente.io")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("uses the time specified in a client TLS config", func() {
|
|
if runtime.GOOS == "windows" {
|
|
// certificate validation works different on windows, see https://golang.org/src/crypto/x509/verify.go line 238
|
|
Skip("windows")
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
NotBefore: time.Now().Add(-25 * time.Hour),
|
|
NotAfter: time.Now().Add(-23 * time.Hour),
|
|
Subject: pkix.Name{CommonName: "quic.clemente.io"},
|
|
}
|
|
_, leafCert := getCertificate(template)
|
|
cm.chain = []*x509.Certificate{leafCert}
|
|
cm.config = &tls.Config{
|
|
Time: func() time.Time { return time.Now().Add(-24 * time.Hour) },
|
|
}
|
|
err := cm.Verify("quic.clemente.io")
|
|
_, ok := err.(x509.UnknownAuthorityError)
|
|
Expect(ok).To(BeTrue())
|
|
})
|
|
|
|
It("rejects certificates that are expired at the time specified in a client TLS config", func() {
|
|
if runtime.GOOS == "windows" {
|
|
// certificate validation works different on windows, see https://golang.org/src/crypto/x509/verify.go line 238
|
|
Skip("windows")
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
}
|
|
_, leafCert := getCertificate(template)
|
|
cm.chain = []*x509.Certificate{leafCert}
|
|
cm.config = &tls.Config{
|
|
Time: func() time.Time { return time.Now().Add(-24 * time.Hour) },
|
|
}
|
|
err := cm.Verify("quic.clemente.io")
|
|
Expect(err.(x509.CertificateInvalidError).Reason).To(Equal(x509.Expired))
|
|
})
|
|
|
|
It("uses the Root CA given in the client config", func() {
|
|
if runtime.GOOS == "windows" {
|
|
// certificate validation works different on windows, see https://golang.org/src/crypto/x509/verify.go line 238
|
|
Skip("windows")
|
|
}
|
|
|
|
templateRoot := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
IsCA: true,
|
|
BasicConstraintsValid: true,
|
|
}
|
|
rootKey, rootCert := getCertificate(templateRoot)
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
Subject: pkix.Name{CommonName: "google.com"},
|
|
}
|
|
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
leafCert := generateCertificate(template, rootCert, &key.PublicKey, rootKey)
|
|
|
|
rootCAPool := x509.NewCertPool()
|
|
rootCAPool.AddCert(rootCert)
|
|
|
|
cm.chain = []*x509.Certificate{leafCert}
|
|
cm.config = &tls.Config{
|
|
RootCAs: rootCAPool,
|
|
}
|
|
err = cm.Verify("google.com")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
})
|
|
})
|