فهرست منبع

Merge pull request #554 from rod-hynes/master

New TLS profile; data corruption mitigation
Rod Hynes 5 سال پیش
والد
کامیت
7fdf25b60b

+ 2 - 0
psiphon/common/protocol/protocol.go

@@ -264,6 +264,7 @@ const (
 	TLS_PROFILE_CHROME_62  = "Chrome-62"
 	TLS_PROFILE_CHROME_70  = "Chrome-70"
 	TLS_PROFILE_CHROME_72  = "Chrome-72"
+	TLS_PROFILE_CHROME_83  = "Chrome-83"
 	TLS_PROFILE_FIREFOX_55 = "Firefox-55"
 	TLS_PROFILE_FIREFOX_56 = "Firefox-56"
 	TLS_PROFILE_FIREFOX_65 = "Firefox-65"
@@ -277,6 +278,7 @@ var SupportedTLSProfiles = TLSProfiles{
 	TLS_PROFILE_CHROME_62,
 	TLS_PROFILE_CHROME_70,
 	TLS_PROFILE_CHROME_72,
+	TLS_PROFILE_CHROME_83,
 	TLS_PROFILE_FIREFOX_55,
 	TLS_PROFILE_FIREFOX_56,
 	TLS_PROFILE_FIREFOX_65,

+ 160 - 36
psiphon/dataStore.go

@@ -51,7 +51,7 @@ var (
 	datastoreAffinityServerEntryIDKey           = []byte("affinityServerEntryID")
 	datastorePersistentStatTypeRemoteServerList = string(datastoreRemoteServerListStatsBucket)
 	datastorePersistentStatTypeFailedTunnel     = string(datastoreFailedTunnelStatsBucket)
-	datastoreServerEntryFetchGCThreshold        = 20
+	datastoreServerEntryFetchGCThreshold        = 10
 
 	datastoreMutex    sync.RWMutex
 	activeDatastoreDB *datastoreDB
@@ -711,6 +711,7 @@ func (iterator *ServerEntryIterator) Next() (*protocol.ServerEntry, error) {
 		iterator.serverEntryIndex += 1
 
 		serverEntry = nil
+		doDeleteServerEntry := false
 
 		err = datastoreView(func(tx *datastoreTx) error {
 			serverEntries := tx.bucket(datastoreServerEntriesBucket)
@@ -719,16 +720,57 @@ func (iterator *ServerEntryIterator) Next() (*protocol.ServerEntry, error) {
 				return nil
 			}
 
+			// When the server entry has a signature and the signature verification
+			// public key is configured, perform a signature verification, which will
+			// detect data corruption of most server entry fields. When the check
+			// fails, the server entry is deleted and skipped and iteration continues.
+			//
+			// This prevents wasteful, time-consuming dials in cases where the server
+			// entry is intact except for a bit flip in the obfuscation key, for
+			// example. A delete is triggered also in the case where the server entry
+			// record fails to unmarshal.
+
+			if iterator.config.ServerEntrySignaturePublicKey != "" {
+
+				var serverEntryFields protocol.ServerEntryFields
+				err = json.Unmarshal(value, &serverEntryFields)
+				if err != nil {
+					doDeleteServerEntry = true
+					NoticeWarning(
+						"ServerEntryIterator.Next: unmarshal failed: %s",
+						errors.Trace(err))
+
+					// Do not stop iterating.
+					return nil
+				}
+
+				if serverEntryFields.HasSignature() {
+					err = serverEntryFields.VerifySignature(
+						iterator.config.ServerEntrySignaturePublicKey)
+					if err != nil {
+						doDeleteServerEntry = true
+						NoticeWarning(
+							"ServerEntryIterator.Next: verify signature failed: %s",
+							errors.Trace(err))
+
+						// Do not stop iterating.
+						return nil
+					}
+				}
+			}
+
 			// Must unmarshal here as slice is only valid within transaction.
 			err = json.Unmarshal(value, &serverEntry)
 
 			if err != nil {
-				// In case of data corruption or a bug causing this condition,
-				// do not stop iterating.
 				serverEntry = nil
+				doDeleteServerEntry = true
 				NoticeWarning(
-					"ServerEntryIterator.Next: json.Unmarshal failed: %s",
+					"ServerEntryIterator.Next: unmarshal failed: %s",
 					errors.Trace(err))
+
+				// Do not stop iterating.
+				return nil
 			}
 
 			return nil
@@ -737,6 +779,11 @@ func (iterator *ServerEntryIterator) Next() (*protocol.ServerEntry, error) {
 			return nil, errors.Trace(err)
 		}
 
+		if doDeleteServerEntry {
+			deleteServerEntry(iterator.config, serverEntryID)
+			continue
+		}
+
 		if serverEntry == nil {
 			// In case of data corruption or a bug causing this condition,
 			// do not stop iterating.
@@ -916,48 +963,24 @@ func pruneServerEntry(config *Config, serverEntryTag string) error {
 		// refer to the same server IP. Only delete the server entry record when its
 		// tag matches the pruned tag. Otherwise, the server entry record is
 		// associated with another tag. The pruned tag is still deleted.
-		deleteServerEntry := (serverEntry.Tag == serverEntryTag)
+		doDeleteServerEntry := (serverEntry.Tag == serverEntryTag)
 
 		err = serverEntryTags.delete(serverEntryTagBytes)
 		if err != nil {
 			errors.Trace(err)
 		}
 
-		if deleteServerEntry {
+		if doDeleteServerEntry {
 
-			err = serverEntries.delete(serverEntryID)
+			err = deleteServerEntryHelper(
+				config,
+				serverEntryID,
+				serverEntries,
+				keyValues,
+				dialParameters)
 			if err != nil {
 				errors.Trace(err)
 			}
-
-			affinityServerEntryID := keyValues.get(datastoreAffinityServerEntryIDKey)
-			if bytes.Equal(affinityServerEntryID, serverEntryID) {
-				err = keyValues.delete(datastoreAffinityServerEntryIDKey)
-				if err != nil {
-					return errors.Trace(err)
-				}
-				err = keyValues.delete(datastoreLastServerEntryFilterKey)
-				if err != nil {
-					return errors.Trace(err)
-				}
-			}
-
-			// TODO: expose boltdb Seek functionality to skip to first matching record.
-			cursor := dialParameters.cursor()
-			defer cursor.close()
-			foundFirstMatch := false
-			for key, _ := cursor.first(); key != nil; key, _ = cursor.next() {
-				// Dial parameters key has serverID as a prefix; see makeDialParametersKey.
-				if bytes.HasPrefix(key, serverEntryID) {
-					foundFirstMatch = true
-					err := dialParameters.delete(key)
-					if err != nil {
-						return errors.Trace(err)
-					}
-				} else if foundFirstMatch {
-					break
-				}
-			}
 		}
 
 		// Tombstones prevent reimporting pruned server entries. Tombstone
@@ -980,6 +1003,102 @@ func pruneServerEntry(config *Config, serverEntryTag string) error {
 	})
 }
 
+// DeleteServerEntry deletes the specified server entry and associated data.
+func DeleteServerEntry(config *Config, ipAddress string) {
+
+	serverEntryID := []byte(ipAddress)
+
+	// For notices, we cannot assume we have a valid server entry tag value to
+	// log, as DeleteServerEntry is called when a server entry fails to unmarshal
+	// or fails signature verification.
+
+	err := deleteServerEntry(config, serverEntryID)
+	if err != nil {
+		NoticeWarning("DeleteServerEntry failed: %s", errors.Trace(err))
+		return
+	}
+	NoticeInfo("Server entry deleted")
+}
+
+func deleteServerEntry(config *Config, serverEntryID []byte) error {
+
+	return datastoreUpdate(func(tx *datastoreTx) error {
+
+		serverEntries := tx.bucket(datastoreServerEntriesBucket)
+		serverEntryTags := tx.bucket(datastoreServerEntryTagsBucket)
+		keyValues := tx.bucket(datastoreKeyValueBucket)
+		dialParameters := tx.bucket(datastoreDialParametersBucket)
+
+		err := deleteServerEntryHelper(
+			config,
+			serverEntryID,
+			serverEntries,
+			keyValues,
+			dialParameters)
+		if err != nil {
+			errors.Trace(err)
+		}
+
+		// Remove any tags pointing to the deleted server entry.
+		cursor := serverEntryTags.cursor()
+		defer cursor.close()
+		for key, value := cursor.first(); key != nil; key, value = cursor.next() {
+			if bytes.Equal(value, serverEntryID) {
+				err := serverEntryTags.delete(key)
+				if err != nil {
+					return errors.Trace(err)
+				}
+			}
+		}
+
+		return nil
+	})
+}
+
+func deleteServerEntryHelper(
+	config *Config,
+	serverEntryID []byte,
+	serverEntries *datastoreBucket,
+	keyValues *datastoreBucket,
+	dialParameters *datastoreBucket) error {
+
+	err := serverEntries.delete(serverEntryID)
+	if err != nil {
+		errors.Trace(err)
+	}
+
+	affinityServerEntryID := keyValues.get(datastoreAffinityServerEntryIDKey)
+	if bytes.Equal(affinityServerEntryID, serverEntryID) {
+		err = keyValues.delete(datastoreAffinityServerEntryIDKey)
+		if err != nil {
+			return errors.Trace(err)
+		}
+		err = keyValues.delete(datastoreLastServerEntryFilterKey)
+		if err != nil {
+			return errors.Trace(err)
+		}
+	}
+
+	// TODO: expose boltdb Seek functionality to skip to first matching record.
+	cursor := dialParameters.cursor()
+	defer cursor.close()
+	foundFirstMatch := false
+	for key, _ := cursor.first(); key != nil; key, _ = cursor.next() {
+		// Dial parameters key has serverID as a prefix; see makeDialParametersKey.
+		if bytes.HasPrefix(key, serverEntryID) {
+			foundFirstMatch = true
+			err := dialParameters.delete(key)
+			if err != nil {
+				return errors.Trace(err)
+			}
+		} else if foundFirstMatch {
+			break
+		}
+	}
+
+	return nil
+}
+
 func scanServerEntries(scanner func(*protocol.ServerEntry)) error {
 	err := datastoreView(func(tx *datastoreTx) error {
 		bucket := tx.bucket(datastoreServerEntriesBucket)
@@ -1627,6 +1746,11 @@ func GetDialParameters(serverIPAddress, networkID string) (*DialParameters, erro
 		return nil, nil
 	}
 
+	// Note: unlike with server entries, this record is not deleted when the
+	// unmarshal fails, as the caller should proceed with the dial without dial
+	// parameters; and when when the dial succeeds, new dial parameters will be
+	// written over this record.
+
 	var dialParams *DialParameters
 	err = json.Unmarshal(data, &dialParams)
 	if err != nil {

+ 4 - 2
psiphon/tlsDialer.go

@@ -271,6 +271,8 @@ func getUTLSClientHelloID(
 		return utls.HelloChrome_70, nil, nil
 	case protocol.TLS_PROFILE_CHROME_72:
 		return utls.HelloChrome_72, nil, nil
+	case protocol.TLS_PROFILE_CHROME_83:
+		return utls.HelloChrome_83, nil, nil
 	case protocol.TLS_PROFILE_FIREFOX_55:
 		return utls.HelloFirefox_55, nil, nil
 	case protocol.TLS_PROFILE_FIREFOX_56:
@@ -309,8 +311,8 @@ func getClientHelloVersion(
 		utls.HelloChrome_62, utls.HelloFirefox_55, utls.HelloFirefox_56:
 		return protocol.TLS_VERSION_12, nil
 
-	case utls.HelloChrome_70, utls.HelloChrome_72, utls.HelloFirefox_65,
-		utls.HelloGolang:
+	case utls.HelloChrome_70, utls.HelloChrome_72, utls.HelloChrome_83,
+		utls.HelloFirefox_65, utls.HelloGolang:
 		return protocol.TLS_VERSION_13, nil
 	}
 

+ 1 - 0
vendor/github.com/refraction-networking/utls/README.md

@@ -66,6 +66,7 @@ This is not a problem, if you fully control the server and turn unsupported thin
 | Chrome 62     | no       | no         | ChannelID              | [0a4a74aeebd1bb66](https://tlsfingerprint.io/id/0a4a74aeebd1bb66) |
 | Chrome 70     | no       | no         | ChannelID, Encrypted Certs | [bc4c7e42f4961cd7](https://tlsfingerprint.io/id/bc4c7e42f4961cd7) |
 | Chrome 72     | no       | no         | ChannelID, Encrypted Certs | [bbf04e5f1881f506](https://tlsfingerprint.io/id/bbf04e5f1881f506) |
+| Chrome 83     | no       | no         | ChannelID, Encrypted Certs | [9c673fd64a32c8dc](https://tlsfingerprint.io/id/9c673fd64a32c8dc) |
 | Firefox 56    | very low | no         | None                   | [c884bad7f40bee56](https://tlsfingerprint.io/id/c884bad7f40bee56) |
 | Firefox 65    | very low | no         | MaxRecordSize                   | [6bfedc5d5c740d58](https://tlsfingerprint.io/id/6bfedc5d5c740d58) |
 | iOS 11.1      | low** | no         | None                   | [71a81bafd58e1301](https://tlsfingerprint.io/id/71a81bafd58e1301) |

+ 2 - 1
vendor/github.com/refraction-networking/utls/u_common.go

@@ -145,11 +145,12 @@ var (
 	HelloFirefox_63   = ClientHelloID{helloFirefox, "63", nil}
 	HelloFirefox_65   = ClientHelloID{helloFirefox, "65", nil}
 
-	HelloChrome_Auto = HelloChrome_72
+	HelloChrome_Auto = HelloChrome_83
 	HelloChrome_58   = ClientHelloID{helloChrome, "58", nil}
 	HelloChrome_62   = ClientHelloID{helloChrome, "62", nil}
 	HelloChrome_70   = ClientHelloID{helloChrome, "70", nil}
 	HelloChrome_72   = ClientHelloID{helloChrome, "72", nil}
+	HelloChrome_83   = ClientHelloID{helloChrome, "83", nil}
 
 	HelloIOS_Auto = HelloIOS_12_1
 	HelloIOS_11_1 = ClientHelloID{helloIOS, "111", nil} // legacy "111" means 11.1

+ 72 - 0
vendor/github.com/refraction-networking/utls/u_parrots.go

@@ -212,6 +212,78 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
 				&UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle},
 			},
 		}, nil
+	case HelloChrome_83:
+		return ClientHelloSpec{
+			CipherSuites: []uint16{
+				GREASE_PLACEHOLDER,
+				TLS_AES_128_GCM_SHA256,
+				TLS_AES_256_GCM_SHA384,
+				TLS_CHACHA20_POLY1305_SHA256,
+				TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+				TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+				TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+				TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+				TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+				TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+				TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+				TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+				TLS_RSA_WITH_AES_128_GCM_SHA256,
+				TLS_RSA_WITH_AES_256_GCM_SHA384,
+				TLS_RSA_WITH_AES_128_CBC_SHA,
+				TLS_RSA_WITH_AES_256_CBC_SHA,
+			},
+			CompressionMethods: []byte{
+				0x00, // compressionNone
+			},
+			Extensions: []TLSExtension{
+				&UtlsGREASEExtension{},
+				&SNIExtension{},
+				&UtlsExtendedMasterSecretExtension{},
+				&RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient},
+				&SupportedCurvesExtension{[]CurveID{
+					CurveID(GREASE_PLACEHOLDER),
+					X25519,
+					CurveP256,
+					CurveP384,
+				}},
+				&SupportedPointsExtension{SupportedPoints: []byte{
+					0x00, // pointFormatUncompressed
+				}},
+				&SessionTicketExtension{},
+				&ALPNExtension{AlpnProtocols: []string{"h2", "http/1.1"}},
+				&StatusRequestExtension{},
+				&SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []SignatureScheme{
+					ECDSAWithP256AndSHA256,
+					PSSWithSHA256,
+					PKCS1WithSHA256,
+					ECDSAWithP384AndSHA384,
+					PSSWithSHA384,
+					PKCS1WithSHA384,
+					PSSWithSHA512,
+					PKCS1WithSHA512,
+				}},
+				&SCTExtension{},
+				&KeyShareExtension{[]KeyShare{
+					{Group: CurveID(GREASE_PLACEHOLDER), Data: []byte{0}},
+					{Group: X25519},
+				}},
+				&PSKKeyExchangeModesExtension{[]uint8{
+					PskModeDHE,
+				}},
+				&SupportedVersionsExtension{[]uint16{
+					GREASE_PLACEHOLDER,
+					VersionTLS13,
+					VersionTLS12,
+					VersionTLS11,
+					VersionTLS10,
+				}},
+				&FakeCertCompressionAlgsExtension{[]CertCompressionAlgo{
+					CertCompressionBrotli,
+				}},
+				&UtlsGREASEExtension{},
+				&UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle},
+			},
+		}, nil
 	case HelloFirefox_55, HelloFirefox_56:
 		return ClientHelloSpec{
 			TLSVersMax: VersionTLS12,

+ 3 - 3
vendor/vendor.json

@@ -490,10 +490,10 @@
 			"revisionTime": "2019-09-09T20:29:46Z"
 		},
 		{
-			"checksumSHA1": "lovHeRrkvnia8JnLwyeIBYLa1Zk=",
+			"checksumSHA1": "1HFgQE+SHogT82ebPmvCUnGwrxo=",
 			"path": "github.com/refraction-networking/utls",
-			"revision": "43c36d3c1f57546d5cbb05c066df7b5a78686c51",
-			"revisionTime": "2019-09-09T20:06:33Z"
+			"revision": "ada0bb9b38a0975b15bb4591cd4a939fe74d1a1b",
+			"revisionTime": "2020-06-01T20:02:09Z"
 		},
 		{
 			"checksumSHA1": "Fn9JW8u40ABN9Uc9wuvquuyOB+8=",