| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- /*
- * Copyright (c) 2016, Psiphon Inc.
- * All rights reserved.
- *
- * 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 <http://www.gnu.org/licenses/>.
- *
- */
- package server
- import (
- "crypto/x509"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "strconv"
- "strings"
- "time"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
- )
- 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
- }
|