diff --git a/ChangeLog b/ChangeLog index a0c5991ba9ebc53ccdccd2ff2ee234b27fb20443..0df8124377a2273ef00cefd5c2706e16dacc17eb 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 0000000000000000000000000000000000000000..9e20d65e896e753a3b88d345e198080438c15ebe --- /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 f86e9344bdd98819cbaa24c1a8b6bd32d66e602b..fc97d7f22df52b440dd77b212802e87b10161958 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 2736fe10e3896d013f51c16c8a8aeb5bbc93dcc7..5817be0e7e87d45ef6d60790b6c619af3db26f4a 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, } }