Browse Source

Merge branch 'master' of https://github.com/Psiphon-Labs/psiphon-tunnel-core

Rod Hynes 9 năm trước cách đây
mục cha
commit
4cc54a41ab
2 tập tin đã thay đổi với 278 bổ sung4 xóa
  1. 12 1
      psiphon/server/api.go
  2. 266 3
      psiphon/server/safetyNet.go

+ 12 - 1
psiphon/server/api.go

@@ -338,12 +338,23 @@ func clientVerificationAPIRequestHandler(
 		return nil, psiphon.ContextError(err)
 	}
 
+	logFields := getRequestLogFields(
+		support,
+		"client_verification",
+		geoIPData,
+		params,
+		baseRequestParams)
+
 	var verified bool
+	var safetyNetCheckLogs LogFields
 	switch normalizeClientPlatform(clientPlatform) {
 	case CLIENT_PLATFORM_ANDROID:
-		verified = verifySafetyNetPayload(verificationData)
+		verified, safetyNetCheckLogs = verifySafetyNetPayload(verificationData)
+		logFields["safetynet_check"] = safetyNetCheckLogs
 	}
 
+	log.WithContextFields(logFields).Info("API event")
+
 	if verified {
 		// TODO: change throttling treatment
 	}

+ 266 - 3
psiphon/server/safetyNet.go

@@ -19,9 +19,272 @@
 
 package server
 
-func verifySafetyNetPayload(params requestJSONObject) bool {
+import (
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
 
-	// TODO: implement
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+)
 
-	return true
+const (
+	safetynetCN = "attest.android.com"
+	// Cert of the root certificate authority (GeoTrust Global CA)
+	// which signs the intermediate certificate from Google (GIAG2)
+	geotrustCert = "-----BEGIN CERTIFICATE-----\nMIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\nMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\nYWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG\nEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg\nR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9\n9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq\nfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv\niS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU\n1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+\nbw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW\nMPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA\nephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l\nuMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn\nZ57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS\ntQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF\nPseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un\nhw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV\n5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==\n-----END CERTIFICATE-----\n"
+	// base64 encoded sha256 hash of the license used to sign the android
+	// client (.apk) https://psiphon.ca/en/faq.html#authentic-android
+	//
+	// keytool -printcert -file CERT.RSA
+	// SHA256: 76:DB:EF:15:F6:77:26:D4:51:A1:23:59:B8:57:9C:0D:7A:9F:63:5D:52:6A:A3:74:24:DF:13:16:32:F1:78:10
+	//
+	// echo dtvvFfZ3JtRRoSNZuFecDXqfY11SaqN0JN8TFjLxeBA= | base64 -d | hexdump  -e '32/1 "%02X " "\n"'
+	// 76 DB EF 15 F6 77 26 D4 51 A1 23 59 B8 57 9C 0D 7A 9F 63 5D 52 6A A3 74 24 DF 13 16 32 F1 78 10
+	psiphon3Base64CertHash = "dtvvFfZ3JtRRoSNZuFecDXqfY11SaqN0JN8TFjLxeBA="
+)
+
+var psiphonApkPackagenames = []string{"com.psiphon3", "com.psiphon3.subscription"}
+
+type X5C []string
+
+type jwt struct {
+	status  int
+	payload string
+}
+
+func newJwt(token requestJSONObject) *jwt {
+	status, ok := token["status"].(float64)
+	if !ok {
+		return nil
+	}
+	payload, ok := token["payload"].(string)
+	if !ok {
+		return nil
+	}
+	return &jwt{
+		status:  int(status),
+		payload: payload,
+	}
+}
+
+type jwtHeader struct {
+	Algorithm string `json:"alg"`
+	CertChain X5C    `json:"x5c"`
+}
+
+func newJwtHeader(jsonBytes []byte) (jwtHeader, error) {
+	var header jwtHeader
+	err := json.Unmarshal(jsonBytes, &header)
+	return header, err
+}
+
+type jwtBody struct {
+	CtsProfileMatch            bool     `json:"ctsProfileMatch"`
+	TimestampMs                int      `json:"timestampMs"`
+	ApkDigestSha256            string   `json:"apkDigestSha256"`
+	ApkPackageName             string   `json:"apkPackageName"`
+	Extension                  string   `json:"extension"`
+	Nonce                      string   `json:"nonce"`
+	ApkCertificateDigestSha256 []string `json:"apkCertificateDigestSha256"`
+}
+
+func newJwtBody(jsonBytes []byte) (jwtBody, error) {
+	var body jwtBody
+	err := json.Unmarshal(jsonBytes, &body)
+	return body, err
+}
+
+// Verify x509 certificate chain
+func (x5c X5C) verifyCertChain() (leaf *x509.Certificate, validCN bool, err error) {
+	if len(x5c) == 0 || len(x5c) > 10 {
+		// OpenSSL's default maximum chain length is 10
+		return nil, false, fmt.Errorf("Invalid certchain length of %d\n", len(x5c))
+	}
+
+	// Parse leaf certificate
+	leafCertDer, err := base64.StdEncoding.DecodeString(x5c[0])
+	if err != nil {
+		return nil, false, err
+	}
+	leafCert, err := x509.ParseCertificate(leafCertDer)
+	if err != nil {
+		return nil, false, err
+	}
+
+	// Verify CN
+	if leafCert.Subject.CommonName == safetynetCN {
+		validCN = true
+	}
+
+	// Parse and add intermediate certificates
+	intermediates := x509.NewCertPool()
+	for i := 1; i < len(x5c); i++ {
+		intermediateCertDer, err := base64.StdEncoding.DecodeString(x5c[i])
+		if err != nil {
+			return leafCert, false, err
+		}
+
+		intermediateCert, err := x509.ParseCertificate(intermediateCertDer)
+		if err != nil {
+			return leafCert, false, err
+		}
+		intermediates.AddCert(intermediateCert)
+	}
+
+	// Parse and verify root cert
+	roots := x509.NewCertPool()
+	ok := roots.AppendCertsFromPEM([]byte(geotrustCert))
+	if !ok {
+		return leafCert, false, fmt.Errorf("Failed to append GEOTRUST cert\n")
+	}
+
+	// Verify leaf certificate
+	storeCtx := x509.VerifyOptions{
+		Intermediates: intermediates,
+		Roots:         roots,
+		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
+	}
+	_, err = leafCert.Verify(storeCtx)
+	if err != nil {
+		return leafCert, false, err
+	}
+
+	return leafCert, validCN, nil
+}
+
+func (body *jwtBody) verifyJWTBody() (validApkCert, validApkPackageName bool) {
+	// Verify apk certificate digest
+	if len(body.ApkCertificateDigestSha256) >= 1 && body.ApkCertificateDigestSha256[0] == psiphon3Base64CertHash {
+		validApkCert = true
+	}
+
+	// Verify apk package name
+	if psiphon.Contains(psiphonApkPackagenames, body.ApkPackageName) {
+		validApkPackageName = true
+	}
+
+	return
+}
+
+// Form log fields for debugging
+func errorLogFields(err error, params requestJSONObject) LogFields {
+	return LogFields{
+		"error_message": err.Error(),
+		"payload":       params,
+	}
+}
+
+// Convert error to string for logging
+func getError(err error) string {
+	if err == nil {
+		return ""
+	}
+	return err.Error()
+}
+
+// Validate JWT produced by safetynet
+func verifySafetyNetPayload(params requestJSONObject) (bool, LogFields) {
+
+	jwt := newJwt(params)
+	if jwt == nil {
+		// Malformed JWT
+		return false, errorLogFields(errors.New("Invalid request to client_verification, malformed jwt"), params)
+
+	}
+
+	statusStrings := map[int]string{
+		0: "API_REQUEST_OK",
+		1: "API_REQUEST_FAILED",
+		2: "API_CONNECT_FAILED",
+	}
+
+	statusString, ok := statusStrings[(*jwt).status]
+	if !ok {
+		statusString = "INVALID_STATUS: expected 0-2, got " + strconv.Itoa((*jwt).status)
+	}
+
+	// SafetyNet check failed
+	if (*jwt).status != 0 {
+		return false, errorLogFields(errors.New(statusString), params)
+	}
+
+	// Split into base64 encoded header, body, signature
+	jwtParts := strings.Split((*jwt).payload, ".")
+	if len(jwtParts) != 3 {
+		// Malformed payload
+		return false, errorLogFields(errors.New("Invalid request to client_verification, malformed jwt"), params)
+
+	}
+
+	// Decode header, body, signature
+	headerJson, err := base64.RawURLEncoding.DecodeString(jwtParts[0])
+	if err != nil {
+		return false, errorLogFields(err, params)
+	}
+	bodyJson, err := base64.RawURLEncoding.DecodeString(jwtParts[1])
+	if err != nil {
+		return false, errorLogFields(err, params)
+	}
+	signature, err := base64.RawURLEncoding.DecodeString(jwtParts[2])
+	if err != nil {
+		return false, errorLogFields(err, params)
+	}
+
+	// Extract header from json
+	header, err := newJwtHeader(headerJson)
+	if err != nil {
+		return false, errorLogFields(err, params)
+	}
+
+	// Verify certchain in header
+	leafCert, validCN, certChainErrors := header.CertChain.verifyCertChain()
+
+	var signatureErrors error
+	if leafCert == nil {
+		signatureErrors = errors.New("Failed to parse leaf certificate")
+	} else {
+		// Verify signature over header and body
+		signatureErrors = leafCert.CheckSignature(x509.SHA256WithRSA, []byte(jwtParts[0]+"."+jwtParts[1]), signature)
+	}
+
+	// Extract body from json
+	body, err := newJwtBody(bodyJson)
+	if err != nil {
+		return false, errorLogFields(err, params)
+	}
+
+	// Validate jwt payload
+	validApkCert, validApkPackageName := body.verifyJWTBody()
+
+	validCertChain := certChainErrors == nil
+	validSignature := signatureErrors == nil
+	verified := validCN && validApkCert && validApkPackageName && validCertChain && validSignature
+
+	// Generate logging information
+	logFields := LogFields{
+		"apk_certificate_digest_sha256": body.ApkCertificateDigestSha256[0],
+		"apk_digest_sha256":             body.ApkDigestSha256,
+		"apk_package_name":              body.ApkPackageName,
+		"certchain_errors":              getError(certChainErrors),
+		"cts_profile_match":             body.CtsProfileMatch,
+		"extension":                     body.Extension,
+		"nonce":                         body.Nonce,
+		"signature_errors":              getError(signatureErrors),
+		"status":                        strconv.Itoa((*jwt).status),
+		"status_string":                 statusString,
+		"valid_cn":                      validCN,
+		"valid_apk_cert":                validApkCert,
+		"valid_apk_packagename":         validApkPackageName,
+		"valid_certchain":               validCertChain,
+		"valid_signature":               validSignature,
+		"verification_timestamp":        time.Unix(0, int64(body.TimestampMs)*1e6).UTC().Format(time.RFC3339),
+		"verified":                      verified,
+	}
+
+	return verified, logFields
 }