Skip to content
This repository was archived by the owner on Dec 20, 2024. It is now read-only.

Commit e1ae4b8

Browse files
authored
Merge pull request #1181 from YanzheL/feature-https-mitm
feature: Hijack HTTPS by generating leaf TLS certs on the fly issued by user provided CA.
2 parents 6745cc8 + 96c3c82 commit e1ae4b8

File tree

4 files changed

+121
-2
lines changed

4 files changed

+121
-2
lines changed

dfdaemon/proxy/cert.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright The Dragonfly Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package proxy
18+
19+
import (
20+
"crypto"
21+
"crypto/rand"
22+
"crypto/tls"
23+
"crypto/x509"
24+
"crypto/x509/pkix"
25+
"errors"
26+
"math/big"
27+
"net"
28+
"time"
29+
30+
"github.com/sirupsen/logrus"
31+
)
32+
33+
type LeafCertSpec struct {
34+
publicKey crypto.PublicKey
35+
36+
privateKey crypto.PrivateKey
37+
38+
signatureAlgorithm x509.SignatureAlgorithm
39+
}
40+
41+
// genLeafCert generates a Leaf TLS certificate and sign it with given CA
42+
func genLeafCert(ca *tls.Certificate, leafCertSpec *LeafCertSpec, host string) (*tls.Certificate, error) {
43+
now := time.Now().Add(-1 * time.Hour).UTC()
44+
if !ca.Leaf.IsCA {
45+
return nil, errors.New("CA cert is not a CA")
46+
}
47+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
48+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
49+
if err != nil {
50+
logrus.Errorf("failed to generate serial number: %s", err)
51+
return nil, err
52+
}
53+
tmpl := &x509.Certificate{
54+
SerialNumber: serialNumber,
55+
Subject: pkix.Name{CommonName: host},
56+
NotBefore: now,
57+
NotAfter: now.Add(24 * time.Hour),
58+
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement,
59+
BasicConstraintsValid: true,
60+
SignatureAlgorithm: leafCertSpec.signatureAlgorithm,
61+
}
62+
ip := net.ParseIP(host)
63+
if ip == nil {
64+
tmpl.DNSNames = []string{host}
65+
} else {
66+
tmpl.IPAddresses = []net.IP{ip}
67+
}
68+
newCert, err := x509.CreateCertificate(rand.Reader, tmpl, ca.Leaf, leafCertSpec.publicKey, ca.PrivateKey)
69+
if err != nil {
70+
logrus.Errorf("failed to generate leaf cert %s", err)
71+
return nil, err
72+
}
73+
cert := new(tls.Certificate)
74+
cert.Certificate = append(cert.Certificate, newCert)
75+
cert.PrivateKey = leafCertSpec.privateKey
76+
cert.Leaf, _ = x509.ParseCertificate(newCert)
77+
return cert, nil
78+
}

dfdaemon/proxy/proxy.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package proxy
1818

1919
import (
2020
"crypto/tls"
21+
"crypto/x509"
2122
"fmt"
2223
"io"
2324
"net"
@@ -31,7 +32,7 @@ import (
3132
"github.com/dragonflyoss/Dragonfly/dfdaemon/downloader"
3233
"github.com/dragonflyoss/Dragonfly/dfdaemon/downloader/dfget"
3334
"github.com/dragonflyoss/Dragonfly/dfdaemon/transport"
34-
35+
"github.com/golang/groupcache/lru"
3536
"github.com/pkg/errors"
3637
"github.com/sirupsen/logrus"
3738
)
@@ -72,6 +73,11 @@ func WithCertFromFile(certFile, keyFile string) Option {
7273
return errors.Wrap(err, "load cert")
7374
}
7475
logrus.Infof("use self-signed certificate (%s, %s) for https hijacking", certFile, keyFile)
76+
leaf, err := x509.ParseCertificate(cert.Certificate[0])
77+
if err != nil {
78+
return errors.Wrap(err, "load leaf cert")
79+
}
80+
cert.Leaf = leaf
7581
p.cert = &cert
7682
return nil
7783
}
@@ -168,6 +174,8 @@ type Proxy struct {
168174
httpsHosts []*config.HijackHost
169175
// cert is the certificate used to hijack https proxy requests
170176
cert *tls.Certificate
177+
// certCache is a in-memory cache store for TLS certs used in HTTPS hijack. Lazy init.
178+
certCache *lru.Cache
171179
// directHandler are used to handle non proxy requests
172180
directHandler http.Handler
173181
// downloadFactory returns the downloader used for p2p downloading
@@ -314,7 +322,38 @@ func (proxy *Proxy) handleHTTPS(w http.ResponseWriter, r *http.Request) {
314322
logrus.Debugln("hijack https request to", r.Host)
315323

316324
sConfig := new(tls.Config)
317-
sConfig.Certificates = []tls.Certificate{*proxy.cert}
325+
if proxy.cert.Leaf != nil && proxy.cert.Leaf.IsCA {
326+
if proxy.certCache == nil { // Initialize proxy.certCache on first access. (Lazy init)
327+
proxy.certCache = lru.New(100) // Default max entries size = 100
328+
}
329+
logrus.Debugf("hijack https request with CA <%s>", proxy.cert.Leaf.Subject.CommonName)
330+
leafCertSpec := LeafCertSpec{
331+
proxy.cert.Leaf.PublicKey,
332+
proxy.cert.PrivateKey,
333+
proxy.cert.Leaf.SignatureAlgorithm}
334+
host, _, _ := net.SplitHostPort(r.Host)
335+
sConfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
336+
cConfig.ServerName = host
337+
logrus.Debugf("Generate temporal leaf TLS cert for ServerName <%s>, host <%s>", hello.ServerName, host)
338+
// It's assumed that `hello.ServerName` is always same as `host`, in practice.
339+
cacheKey := host
340+
cached, hit := proxy.certCache.Get(cacheKey)
341+
if hit && time.Now().Before(cached.(*tls.Certificate).Leaf.NotAfter) { // If cache hit and the cert is not expired
342+
logrus.Debugf("TLS Cache hit, cacheKey = <%s>", cacheKey)
343+
return cached.(*tls.Certificate), nil
344+
}
345+
cert, err := genLeafCert(proxy.cert, &leafCertSpec, host)
346+
if err == nil {
347+
// Put cert in cache only if there is no error. So all certs in cache are always valid.
348+
// But certs in cache maybe expired (After 24 hours, see the default duration of generated certs)
349+
proxy.certCache.Add(cacheKey, cert)
350+
}
351+
// If err != nil, means unrecoverable error happened in genLeafCert(...)
352+
return cert, err
353+
}
354+
} else {
355+
sConfig.Certificates = []tls.Certificate{*proxy.cert}
356+
}
318357

319358
sConn, err := handshake(w, sConfig)
320359
if err != nil {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/go-openapi/strfmt v0.0.0-20171222154016-4dd3d302e100
2020
github.com/go-openapi/swag v0.0.0-20170606142751-f3f9494671f9
2121
github.com/go-openapi/validate v0.0.0-20170705144413-8a82927c942c
22+
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef
2223
github.com/golang/mock v1.3.1
2324
github.com/gorilla/context v0.0.0-20181012153548-51ce91d2eadd // indirect
2425
github.com/gorilla/mux v1.5.0

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
6363
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
6464
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
6565
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
66+
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
6667
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
6768
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
6869
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=

0 commit comments

Comments
 (0)