commit c65aaf6407fe742baabd6ea4d6d92970ef0ba44f Author: Yawning Angel yawning@schwanenlied.me Date: Mon Feb 4 03:24:25 2019 +0000
transports/meeklite: Add a lightweight HPKP implementation
HPKP is effectively dead as far as a standard goes, but the idea has merit in certain use cases, this being one of them.
As a TLS MITM essentially will strip whatever obfuscation that the transport may provide, the digests of the SubjectPublicKeyInfo fields of the Tor Browser Azure meek host are now hardcoded.
The behavior can be disabled by passing `disableHPKP=true` on the bridge line, for cases where comaptibility is prefered over security. --- ChangeLog | 2 + transports/meeklite/hpkp_lite.go | 88 ++++++++++++++++++++++++++++++++++++++++ transports/meeklite/meek.go | 19 ++++++--- transports/meeklite/transport.go | 28 +++++++++++-- 4 files changed, 129 insertions(+), 8 deletions(-)
diff --git a/ChangeLog b/ChangeLog index a0c5991..0df8124 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,8 @@ Changes in version 0.0.9 - UNRELEASED: - Various meek_lite code cleanups and bug fixes. - Bug 29077: uTLS for ClientHello camouflage (meek_lite). - More fixes to HTTP Basic auth. + - (meek_lite) Pin the certificate chain public keys for the default + Tor Browser Azure bridge (meek_lite).
Changes in version 0.0.8 - 2019-01-20: - Bug 24793: Send the correct authorization HTTP header for basic auth. diff --git a/transports/meeklite/hpkp_lite.go b/transports/meeklite/hpkp_lite.go new file mode 100644 index 0000000..9e20d65 --- /dev/null +++ b/transports/meeklite/hpkp_lite.go @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019 Yawning Angel <yawning at schwanenlied dot me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package meeklite + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/base64" + + "golang.org/x/net/idna" +) + +var builtinPinDB *hpkpDatabase + +type hpkpDatabase struct { + pins map[string]map[string]bool +} + +func (db *hpkpDatabase) HasPins(host string) (string, bool) { + h, err := normalizeHost(host) + return h, (db.pins[host] != nil && err == nil) +} + +func (db *hpkpDatabase) Validate(host string, chains [][]*x509.Certificate) bool { + var ok bool + if host, ok = db.HasPins(host); !ok { + return false + } + + pins := db.pins[host] + for _, chain := range chains { + for _, cert := range chain { + derivedPin := sha256.Sum256(cert.RawSubjectPublicKeyInfo) + derivedPinEncoded := base64.StdEncoding.EncodeToString(derivedPin[:]) + if !pins[derivedPinEncoded] { + return false + } + } + } + + return true +} + +func (db *hpkpDatabase) Add(host string, pins []string) { + h, err := normalizeHost(host) + if err != nil { + panic("failed to add hpkp pin, invalid host: " + err.Error()) + } + + pinMap := make(map[string]bool) + for _, pin := range pins { + pinMap[pin] = true + } + + db.pins[h] = pinMap +} + +func normalizeHost(host string) (string, error) { + return idna.Lookup.ToASCII(host) +} + +func init() { + builtinPinDB = &hpkpDatabase{ + pins: make(map[string]map[string]bool), + } + + // Generated on 2019-02-04. + builtinPinDB.Add("ajax.aspnetcdn.com", []string{ + "PPjoAKk+kCVr9VNPXJkyHXEKnIyd5t5NqpPL3zCvJOE=", + "wBdPad95AU7OgLRs0FU/E6ILO1MSCM84kJ9y0H+TT7s=", + "Y9mvm0exBk1JoQ57f9Vm28jKo5lFm/woKcVxrYxu80o=", + }) +} diff --git a/transports/meeklite/meek.go b/transports/meeklite/meek.go index f86e934..fc97d7f 100644 --- a/transports/meeklite/meek.go +++ b/transports/meeklite/meek.go @@ -41,6 +41,7 @@ import ( gourl "net/url" "os" "runtime" + "strings" "sync" "time"
@@ -50,9 +51,10 @@ import ( )
const ( - urlArg = "url" - frontArg = "front" - utlsArg = "utls" + urlArg = "url" + frontArg = "front" + utlsArg = "utls" + disableHPKPArg = "disableHPKP"
maxChanBacklog = 16
@@ -76,7 +78,8 @@ type meekClientArgs struct { url *gourl.URL front string
- utls *utls.ClientHelloID + utls *utls.ClientHelloID + disableHPKP bool }
func (ca *meekClientArgs) Network() string { @@ -114,6 +117,12 @@ func newClientArgs(args *pt.Args) (ca *meekClientArgs, err error) { return nil, err }
+ // Parse the (optional) HPKP disable argument. + hpkpOpt, _ := args.Get(disableHPKPArg) + if strings.ToLower(hpkpOpt) == "true" { + ca.disableHPKP = true + } + return ca, nil }
@@ -358,7 +367,7 @@ func newMeekConn(network, addr string, dialFn base.DialFunc, ca *meekClientArgs) case nil: rt = &http.Transport{Dial: dialFn} default: - rt = newRoundTripper(dialFn, ca.utls) + rt = newRoundTripper(dialFn, ca.utls, ca.disableHPKP) }
conn := &meekConn{ diff --git a/transports/meeklite/transport.go b/transports/meeklite/transport.go index 2736fe1..5817be0 100644 --- a/transports/meeklite/transport.go +++ b/transports/meeklite/transport.go @@ -19,6 +19,7 @@ package meeklite
import ( "crypto/tls" + "crypto/x509" "errors" "fmt" "net" @@ -28,6 +29,7 @@ import ( "strings" "sync"
+ "gitlab.com/yawning/obfs4.git/common/log" "gitlab.com/yawning/obfs4.git/transports/base" utls "gitlab.com/yawning/utls.git" "golang.org/x/net/http2" @@ -64,7 +66,8 @@ type roundTripper struct { dialFn base.DialFunc transport http.RoundTripper
- initConn net.Conn + initConn net.Conn + disableHPKP bool }
func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { @@ -128,7 +131,25 @@ func (rt *roundTripper) dialTLS(network, addr string) (net.Conn, error) { host = addr }
- conn := utls.UClient(rawConn, &utls.Config{ServerName: host}, *rt.clientHelloID) + var verifyPeerCertificateFn func([][]byte, [][]*x509.Certificate) error + if !rt.disableHPKP { + if pinHost, ok := builtinPinDB.HasPins(host); ok { + if rt.transport == nil { + log.Debugf("meek_lite - HPKP enabled for host: %v", pinHost) + } + verifyPeerCertificateFn = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + if !builtinPinDB.Validate(pinHost, verifiedChains) { + log.Errorf("meek_lite - HPKP validation failure, potential MITM for host: %v", pinHost) + return fmt.Errorf("meek_lite: HPKP validation failure for host: %v", pinHost) + } + return nil + } + } + } else if rt.transport == nil { + log.Warnf("meek_lite - HPKP disabled for host: %v", host) + } + + conn := utls.UClient(rawConn, &utls.Config{ServerName: host, VerifyPeerCertificate: verifyPeerCertificateFn}, *rt.clientHelloID) if err = conn.Handshake(); err != nil { conn.Close() return nil, err @@ -170,10 +191,11 @@ func getDialTLSAddr(u *url.URL) string { return net.JoinHostPort(u.Host, strconv.Itoa(pInt)) }
-func newRoundTripper(dialFn base.DialFunc, clientHelloID *utls.ClientHelloID) http.RoundTripper { +func newRoundTripper(dialFn base.DialFunc, clientHelloID *utls.ClientHelloID, disableHPKP bool) http.RoundTripper { return &roundTripper{ clientHelloID: clientHelloID, dialFn: dialFn, + disableHPKP: disableHPKP, } }
tor-commits@lists.torproject.org