Browse Source

Update TLS profiles

- Use updated utls
- Add more utls ClientHellos
- Add LimitTLSProfiles tactics parameter
Rod Hynes 7 years ago
parent
commit
a383a6397f

+ 18 - 0
psiphon/common/parameters/clientParameters.go

@@ -84,6 +84,7 @@ const (
 	PrioritizeTunnelProtocols                      = "PrioritizeTunnelProtocols"
 	PrioritizeTunnelProtocolsCandidateCount        = "PrioritizeTunnelProtocolsCandidateCount"
 	LimitTunnelProtocols                           = "LimitTunnelProtocols"
+	LimitTLSProfiles                               = "LimitTLSProfiles"
 	TunnelOperateShutdownTimeout                   = "TunnelOperateShutdownTimeout"
 	TunnelPortForwardDialTimeout                   = "TunnelPortForwardDialTimeout"
 	TunnelRateLimits                               = "TunnelRateLimits"
@@ -207,6 +208,8 @@ var defaultClientParameters = map[string]struct {
 	PrioritizeTunnelProtocolsCandidateCount: {value: 10, minimum: 0},
 	LimitTunnelProtocols:                    {value: protocol.TunnelProtocols{}},
 
+	LimitTLSProfiles: {value: protocol.TLSProfiles{}},
+
 	AdditionalCustomHeaders: {value: make(http.Header)},
 
 	// Speed test and SSH keep alive padding is intended to frustrate
@@ -459,6 +462,14 @@ func (p *ClientParameters) Set(
 					}
 					return nil, common.ContextError(err)
 				}
+			case protocol.TLSProfiles:
+				err := v.Validate()
+				if err != nil {
+					if skipOnError {
+						continue
+					}
+					return nil, common.ContextError(err)
+				}
 			}
 
 			// Enforce any minimums. Assumes defaultClientParameters[name]
@@ -643,6 +654,13 @@ func (p *ClientParametersSnapshot) TunnelProtocols(name string) protocol.TunnelP
 	return value
 }
 
+// TLSProfiles returns a protocol.TLSProfiles parameter value.
+func (p *ClientParametersSnapshot) TLSProfiles(name string) protocol.TLSProfiles {
+	value := protocol.TLSProfiles{}
+	p.getValue(name, &value)
+	return value
+}
+
 // DownloadURLs returns a DownloadURLs parameter value.
 func (p *ClientParametersSnapshot) DownloadURLs(name string) DownloadURLs {
 	value := DownloadURLs{}

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

@@ -140,6 +140,37 @@ func UseClientTunnelProtocol(
 	return false
 }
 
+const (
+	TLS_PROFILE_IOS_1131   = "iOS-Safari-11.3.1"
+	TLS_PROFILE_ANDROID_60 = "Android-6.0"
+	TLS_PROFILE_ANDROID_51 = "Android-5.1"
+	TLS_PROFILE_CHROME_58  = "Chrome-58"
+	TLS_PROFILE_CHROME_57  = "Chrome-57"
+	TLS_PROFILE_FIREFOX_56 = "Firefox-56"
+	TLS_PROFILE_RANDOMIZED = "Randomized"
+)
+
+var SupportedTLSProfiles = TLSProfiles{
+	TLS_PROFILE_IOS_1131,
+	TLS_PROFILE_ANDROID_60,
+	TLS_PROFILE_ANDROID_51,
+	TLS_PROFILE_CHROME_58,
+	TLS_PROFILE_CHROME_57,
+	TLS_PROFILE_FIREFOX_56,
+	TLS_PROFILE_RANDOMIZED,
+}
+
+type TLSProfiles []string
+
+func (profiles TLSProfiles) Validate() error {
+	for _, p := range profiles {
+		if !common.Contains(SupportedTLSProfiles, p) {
+			return common.ContextError(fmt.Errorf("invalid TLS profile: %s", p))
+		}
+	}
+	return nil
+}
+
 type HandshakeResponse struct {
 	SSHSessionID           string              `json:"ssh_session_id"`
 	Homepages              []string            `json:"homepages"`

+ 31 - 20
psiphon/tlsDialer.go

@@ -148,13 +148,6 @@ type CustomTLSConfig struct {
 	ObfuscatedSessionTicketKey string
 }
 
-const (
-	tlsProfileAndroid    = "Android-6.0"
-	tlsProfileChrome     = "Chrome-62"
-	tlsProfileFirefox    = "Firefox-56"
-	tlsProfileRandomized = "Randomized"
-)
-
 func SelectTLSProfile(
 	useIndistinguishableTLS bool,
 	tunnelProtocol string,
@@ -162,18 +155,30 @@ func SelectTLSProfile(
 
 	if useIndistinguishableTLS {
 
-		tlsProfiles := []string{
-			tlsProfileAndroid,
-			tlsProfileChrome,
-			tlsProfileFirefox,
+		limitTLSProfiles := clientParameters.Get().TLSProfiles(parameters.LimitTLSProfiles)
+
+		tlsProfiles := make([]string, 0)
+
+		for _, tlsProfile := range protocol.SupportedTLSProfiles {
+
+			if len(limitTLSProfiles) > 0 &&
+				!common.Contains(limitTLSProfiles, tlsProfile) {
+				continue
+			}
+
+			if tunnelProtocol == protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET &&
+				tlsProfile == protocol.TLS_PROFILE_RANDOMIZED {
+				// This TLS profile doesn't support session tickets
+				continue
+			}
+
+			tlsProfiles = append(tlsProfiles, tlsProfile)
 		}
 
-		if tunnelProtocol != protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET {
-			// The following TLS profiles don't support session tickets
-			tlsProfiles = append(tlsProfiles, tlsProfileRandomized)
+		if len(limitTLSProfiles) == 0 {
+			return ""
 		}
 
-		// TODO: weighted selection parameter
 		choice, _ := common.MakeSecureRandomInt(len(tlsProfiles))
 
 		return tlsProfiles[choice]
@@ -184,13 +189,19 @@ func SelectTLSProfile(
 
 func getClientHelloID(tlsProfile string) utls.ClientHelloID {
 	switch tlsProfile {
-	case tlsProfileAndroid:
+	case protocol.TLS_PROFILE_IOS_1131:
+		return utls.HelloiOSSafari_11_3_1
+	case protocol.TLS_PROFILE_ANDROID_60:
 		return utls.HelloAndroid_6_0_Browser
-	case tlsProfileChrome:
-		return utls.HelloChrome_62
-	case tlsProfileFirefox:
+	case protocol.TLS_PROFILE_ANDROID_51:
+		return utls.HelloAndroid_5_1_Browser
+	case protocol.TLS_PROFILE_CHROME_58:
+		return utls.HelloChrome_58
+	case protocol.TLS_PROFILE_CHROME_57:
+		return utls.HelloChrome_57
+	case protocol.TLS_PROFILE_FIREFOX_56:
 		return utls.HelloFirefox_56
-	case tlsProfileRandomized:
+	case protocol.TLS_PROFILE_RANDOMIZED:
 		return utls.HelloRandomized
 	default:
 		return utls.HelloGolang

+ 27 - 1
vendor/github.com/Psiphon-Labs/utls/u_common.go

@@ -5,6 +5,8 @@
 package tls
 
 import (
+	"crypto/hmac"
+	"crypto/sha512"
 	"fmt"
 )
 
@@ -25,6 +27,10 @@ const (
 	OLD_TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256   = uint16(0xcc13)
 	OLD_TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = uint16(0xcc14)
 
+	DISABLED_TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = uint16(0xc024)
+	DISABLED_TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384   = uint16(0xc028)
+	DISABLED_TLS_RSA_WITH_AES_256_CBC_SHA256         = uint16(0x003d)
+
 	FAKE_OLD_TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = uint16(0xcc15) // we can try to craft these ciphersuites
 	FAKE_TLS_DHE_RSA_WITH_AES_128_GCM_SHA256           = uint16(0x009e) // from existing pieces, if needed
 
@@ -66,6 +72,7 @@ const (
 	helloFirefox    = "Firefox"
 	helloChrome     = "Chrome"
 	helloAndroid    = "Android"
+	helloiOSSafari  = "iOSSafari"
 )
 
 const (
@@ -95,23 +102,42 @@ var (
 	HelloFirefox_56                 = ClientHelloID{helloFirefox, 56}
 
 	HelloChrome_Auto ClientHelloID = ClientHelloID{helloChrome, helloAutoVers}
+	HelloChrome_57   ClientHelloID = ClientHelloID{helloChrome, 57}
 	HelloChrome_58   ClientHelloID = ClientHelloID{helloChrome, 58}
 	HelloChrome_62   ClientHelloID = ClientHelloID{helloChrome, 62}
 
 	HelloAndroid_Auto        ClientHelloID = ClientHelloID{helloAndroid, helloAutoVers}
 	HelloAndroid_6_0_Browser ClientHelloID = ClientHelloID{helloAndroid, 23}
 	HelloAndroid_5_1_Browser ClientHelloID = ClientHelloID{helloAndroid, 22}
+
+	HelloiOSSafari_11_3_1 ClientHelloID = ClientHelloID{helloiOSSafari, 1131}
 )
 
+// utlsMacSHA384 returns a SHA-384.
+func utlsMacSHA384(version uint16, key []byte) macFunction {
+	return tls10MAC{hmac.New(sha512.New384, key)}
+}
+
 var utlsSupportedSignatureAlgorithms []signatureAndHash
 var utlsSupportedCipherSuites []*cipherSuite
 
 func init() {
 	utlsSupportedSignatureAlgorithms = append(supportedSignatureAlgorithms,
 		[]signatureAndHash{{disabledHashSHA512, signatureRSA}, {disabledHashSHA512, signatureECDSA}}...)
+
 	utlsSupportedCipherSuites = append(cipherSuites, []*cipherSuite{
 		{OLD_TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, 32, 0, 12, ecdheRSAKA,
 			suiteECDHE | suiteTLS12 | suiteDefaultOff, nil, nil, aeadChaCha20Poly1305},
 		{OLD_TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, 32, 0, 12, ecdheECDSAKA,
-			suiteECDHE | suiteECDSA | suiteTLS12 | suiteDefaultOff, nil, nil, aeadChaCha20Poly1305}}...)
+			suiteECDHE | suiteECDSA | suiteTLS12 | suiteDefaultOff, nil, nil, aeadChaCha20Poly1305},
+
+		// The following weak ciphersuites are enabled for maximum compatibility,
+		// given that we establish secure connections within the utls connection.
+		{DISABLED_TLS_RSA_WITH_AES_256_CBC_SHA256, 32, 32, 16, rsaKA,
+			suiteTLS12 | suiteDefaultOff, cipherAES, macSHA256, nil},
+		{DISABLED_TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, 32, 48, 16, ecdheECDSAKA,
+			suiteECDHE | suiteECDSA | suiteTLS12 | suiteDefaultOff | suiteSHA384, cipherAES, utlsMacSHA384, nil},
+		{DISABLED_TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, 32, 48, 16, ecdheRSAKA,
+			suiteECDHE | suiteTLS12 | suiteDefaultOff | suiteSHA384, cipherAES, utlsMacSHA384, nil},
+	}...)
 }

+ 33 - 4
vendor/github.com/Psiphon-Labs/utls/u_conn.go

@@ -26,6 +26,25 @@ type UConn struct {
 	HandshakeState ClientHandshakeState
 
 	HandshakeStateBuilt bool
+
+	// IncludeEmptySNI indicates to include an SNI extension when the
+	// ServerName is "". This is non-standard behavior. Common TLS
+	// implementations (Go, BoringSSL, etc.) omit the SNI extention in
+	// this case.
+	//
+	// One concrete instance is when the remote host name is an IP address;
+	// https://tools.ietf.org/html/rfc6066#section-3 prohibits an SNI with an
+	// IP address.
+	//
+	// Go's hostnameInSNI sets the ServerName to "":
+	// https://github.com/golang/go/blob/release-branch.go1.9/src/crypto/tls/handshake_client.go#L804
+	//
+	// And then omits the SNI extension:
+	// https://github.com/golang/go/blob/release-branch.go1.9/src/crypto/tls/handshake_messages.go#L150
+	//
+	// IncludeEmptySNI is set to true for test runs, as test data expects
+	// empty SNI extensions.
+	IncludeEmptySNI bool
 }
 
 // UClient returns a new uTLS client, with behavior depending on clientHelloID.
@@ -365,9 +384,19 @@ func (uconn *UConn) MarshalClientHello() error {
 		2 + len(hello.CipherSuites)*2 +
 		1 + len(hello.CompressionMethods)
 
+	extensions := make([]TLSExtension, 0, len(uconn.Extensions))
+	for _, ext := range uconn.Extensions {
+		if SNI, ok := ext.(*SNIExtension); !ok ||
+			len(SNI.ServerName) > 0 ||
+			uconn.IncludeEmptySNI {
+
+			extensions = append(extensions, ext)
+		}
+	}
+
 	extensionsLen := 0
 	var paddingExt *utlsPaddingExtension
-	for _, ext := range uconn.Extensions {
+	for _, ext := range extensions {
 		if pe, ok := ext.(*utlsPaddingExtension); !ok {
 			// If not padding - just add length of extension to total length
 			extensionsLen += ext.Len()
@@ -388,7 +417,7 @@ func (uconn *UConn) MarshalClientHello() error {
 	}
 
 	helloLen := headerLength
-	if len(uconn.Extensions) > 0 {
+	if len(extensions) > 0 {
 		helloLen += 2 + extensionsLen // 2 bytes for extensions' length
 	}
 
@@ -415,9 +444,9 @@ func (uconn *UConn) MarshalClientHello() error {
 	binary.Write(bufferedWriter, binary.BigEndian, uint8(len(hello.CompressionMethods)))
 	binary.Write(bufferedWriter, binary.BigEndian, hello.CompressionMethods)
 
-	if len(uconn.Extensions) > 0 {
+	if len(extensions) > 0 {
 		binary.Write(bufferedWriter, binary.BigEndian, uint16(extensionsLen))
-		for _, ext := range uconn.Extensions {
+		for _, ext := range extensions {
 			bufferedWriter.ReadFrom(ext)
 		}
 	}

+ 121 - 17
vendor/github.com/Psiphon-Labs/utls/u_parrots.go

@@ -31,7 +31,12 @@ func (uconn *UConn) generateClientHelloConfig(id ClientHelloID) error {
 	case HelloChrome_62:
 		fallthrough
 	case HelloChrome_58:
-		return uconn.parrotChrome_58()
+		return uconn.parrotChrome_5x(false)
+	case HelloChrome_57:
+		return uconn.parrotChrome_5x(true)
+
+	case HelloiOSSafari_11_3_1:
+		return uconn.parrotiOSSafari_11_3_1()
 
 	case HelloRandomizedALPN:
 		return uconn.parrotRandomizedALPN()
@@ -233,6 +238,7 @@ func (uconn *UConn) parrotAndroid_6_0() error {
 	}
 	return nil
 }
+
 func (uconn *UConn) parrotAndroid_5_1() error {
 	hello := uconn.HandshakeState.Hello
 	session := uconn.HandshakeState.Session
@@ -309,7 +315,7 @@ func (uconn *UConn) parrotAndroid_5_1() error {
 	return nil
 }
 
-func (uconn *UConn) parrotChrome_58() error {
+func (uconn *UConn) parrotChrome_5x(includeNonStandardChaChaCiphers bool) error {
 	hello := uconn.HandshakeState.Hello
 	session := uconn.HandshakeState.Session
 
@@ -318,21 +324,42 @@ func (uconn *UConn) parrotChrome_58() error {
 		return err
 	}
 
-	hello.CipherSuites = []uint16{
-		GetBoringGREASEValue(hello.Random, ssl_grease_cipher),
-		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,
-		TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+	if includeNonStandardChaChaCiphers {
+		hello.CipherSuites = []uint16{
+			GetBoringGREASEValue(hello.Random, ssl_grease_cipher),
+			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,
+			OLD_TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
+			OLD_TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
+			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,
+			TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+		}
+	} else {
+		hello.CipherSuites = []uint16{
+			GetBoringGREASEValue(hello.Random, ssl_grease_cipher),
+			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,
+			TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+		}
 	}
 
 	grease_ext1 := GetBoringGREASEValue(hello.Random, ssl_grease_extension1)
@@ -393,6 +420,83 @@ func (uconn *UConn) parrotChrome_58() error {
 	return nil
 }
 
+func (uconn *UConn) parrotiOSSafari_11_3_1() error {
+	hello := uconn.HandshakeState.Hello
+	session := uconn.HandshakeState.Session
+
+	hello.CipherSuites = []uint16{
+		TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+		TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+		DISABLED_TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,
+		TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
+		TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+		TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+		TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+		TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+		TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+		DISABLED_TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,
+		TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
+		TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+		TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+		TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+		TLS_RSA_WITH_AES_256_GCM_SHA384,
+		TLS_RSA_WITH_AES_128_GCM_SHA256,
+		DISABLED_TLS_RSA_WITH_AES_256_CBC_SHA256,
+		TLS_RSA_WITH_AES_128_CBC_SHA256,
+		TLS_RSA_WITH_AES_256_CBC_SHA,
+		TLS_RSA_WITH_AES_128_CBC_SHA,
+	}
+	err := uconn.fillClientHelloHeader()
+	if err != nil {
+		return err
+	}
+
+	reneg := RenegotiationInfoExtension{renegotiation: RenegotiateOnceAsClient}
+	sni := SNIExtension{uconn.config.ServerName}
+	ems := utlsExtendedMasterSecretExtension{}
+	sessionTicket := SessionTicketExtension{Session: session}
+	if session != nil {
+		sessionTicket.Session = session
+		if len(session.SessionTicket()) > 0 {
+			sessionId := sha256.Sum256(session.SessionTicket())
+			hello.SessionId = sessionId[:]
+		}
+	}
+	sigAndHash := SignatureAlgorithmsExtension{SignatureAndHashes: []SignatureAndHash{
+		{hashSHA256, signatureECDSA},
+		fakeRsaPssSha256,
+		{hashSHA256, signatureRSA},
+		{hashSHA384, signatureECDSA},
+		fakeRsaPssSha384,
+		{hashSHA384, signatureRSA},
+		fakeRsaPssSha512,
+		{disabledHashSHA512, signatureRSA},
+		{hashSHA1, signatureRSA},
+	},
+	}
+	status := StatusRequestExtension{}
+	npn := NPNExtension{}
+	sct := SCTExtension{}
+	alpn := ALPNExtension{AlpnProtocols: []string{"h2", "h2-16", "h2-15", "h2-14", "spdy/3.1", "spdy/3", "http/1.1"}}
+	points := SupportedPointsExtension{SupportedPoints: []byte{pointFormatUncompressed}}
+	curves := SupportedCurvesExtension{[]CurveID{X25519, CurveP256, CurveP384, CurveP521}}
+
+	uconn.Extensions = []TLSExtension{
+		&reneg,
+		&sni,
+		&ems,
+		&sessionTicket,
+		&sigAndHash,
+		&status,
+		&npn,
+		&sct,
+		&alpn,
+		&points,
+		&curves,
+	}
+	return nil
+}
+
 func (uconn *UConn) parrotRandomizedALPN() error {
 	err := uconn.parrotRandomizedNoALPN()
 	if len(uconn.config.NextProtos) == 0 {

+ 3 - 3
vendor/vendor.json

@@ -33,10 +33,10 @@
 			"revisionTime": "2017-02-28T16:03:01Z"
 		},
 		{
-			"checksumSHA1": "b+QHJfOODtYVqeN0Uyo/EGDxrTo=",
+			"checksumSHA1": "vlwXc96qUPfAMxyDMrzCX4cDYHA=",
 			"path": "github.com/Psiphon-Labs/utls",
-			"revision": "1f81de88145c342aad771f4f630012618faaffa7",
-			"revisionTime": "2018-04-25T19:07:11Z"
+			"revision": "6ee8d02ba07905b8b18e2d8e3446892be54f5393",
+			"revisionTime": "2018-05-16T17:21:43Z"
 		},
 		{
 			"checksumSHA1": "zaEXXT0xMkEADcxW9GvBK0iYe1A=",