safetyNet.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /*
  2. * Copyright (c) 2016, Psiphon Inc.
  3. * All rights reserved.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19. package server
  20. import (
  21. "crypto/x509"
  22. "encoding/base64"
  23. "encoding/json"
  24. "errors"
  25. "fmt"
  26. "strconv"
  27. "strings"
  28. "time"
  29. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
  30. )
  31. const (
  32. safetynetCN = "attest.android.com"
  33. // Cert of the root certificate authority (GeoTrust Global CA)
  34. // which signs the intermediate certificate from Google (GIAG2)
  35. 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"
  36. // base64 encoded sha256 hash of the license used to sign the android
  37. // client (.apk) https://psiphon.ca/en/faq.html#authentic-android
  38. //
  39. // keytool -printcert -file CERT.RSA
  40. // 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
  41. //
  42. // echo dtvvFfZ3JtRRoSNZuFecDXqfY11SaqN0JN8TFjLxeBA= | base64 -d | hexdump -e '32/1 "%02X " "\n"'
  43. // 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
  44. psiphon3Base64CertHash = "dtvvFfZ3JtRRoSNZuFecDXqfY11SaqN0JN8TFjLxeBA="
  45. )
  46. var psiphonApkPackagenames = []string{"com.psiphon3", "com.psiphon3.subscription"}
  47. type X5C []string
  48. type jwt struct {
  49. status int
  50. payload string
  51. }
  52. func newJwt(token requestJSONObject) *jwt {
  53. status, ok := token["status"].(float64)
  54. if !ok {
  55. return nil
  56. }
  57. payload, ok := token["payload"].(string)
  58. if !ok {
  59. return nil
  60. }
  61. return &jwt{
  62. status: int(status),
  63. payload: payload,
  64. }
  65. }
  66. type jwtHeader struct {
  67. Algorithm string `json:"alg"`
  68. CertChain X5C `json:"x5c"`
  69. }
  70. func newJwtHeader(jsonBytes []byte) (jwtHeader, error) {
  71. var header jwtHeader
  72. err := json.Unmarshal(jsonBytes, &header)
  73. return header, err
  74. }
  75. type jwtBody struct {
  76. CtsProfileMatch bool `json:"ctsProfileMatch"`
  77. TimestampMs int `json:"timestampMs"`
  78. ApkDigestSha256 string `json:"apkDigestSha256"`
  79. ApkPackageName string `json:"apkPackageName"`
  80. Extension string `json:"extension"`
  81. Nonce string `json:"nonce"`
  82. ApkCertificateDigestSha256 []string `json:"apkCertificateDigestSha256"`
  83. }
  84. func newJwtBody(jsonBytes []byte) (jwtBody, error) {
  85. var body jwtBody
  86. err := json.Unmarshal(jsonBytes, &body)
  87. return body, err
  88. }
  89. // Verify x509 certificate chain
  90. func (x5c X5C) verifyCertChain() (leaf *x509.Certificate, validCN bool, err error) {
  91. if len(x5c) == 0 || len(x5c) > 10 {
  92. // OpenSSL's default maximum chain length is 10
  93. return nil, false, fmt.Errorf("Invalid certchain length of %d\n", len(x5c))
  94. }
  95. // Parse leaf certificate
  96. leafCertDer, err := base64.StdEncoding.DecodeString(x5c[0])
  97. if err != nil {
  98. return nil, false, err
  99. }
  100. leafCert, err := x509.ParseCertificate(leafCertDer)
  101. if err != nil {
  102. return nil, false, err
  103. }
  104. // Verify CN
  105. if leafCert.Subject.CommonName == safetynetCN {
  106. validCN = true
  107. }
  108. // Parse and add intermediate certificates
  109. intermediates := x509.NewCertPool()
  110. for i := 1; i < len(x5c); i++ {
  111. intermediateCertDer, err := base64.StdEncoding.DecodeString(x5c[i])
  112. if err != nil {
  113. return leafCert, false, err
  114. }
  115. intermediateCert, err := x509.ParseCertificate(intermediateCertDer)
  116. if err != nil {
  117. return leafCert, false, err
  118. }
  119. intermediates.AddCert(intermediateCert)
  120. }
  121. // Parse and verify root cert
  122. roots := x509.NewCertPool()
  123. ok := roots.AppendCertsFromPEM([]byte(geotrustCert))
  124. if !ok {
  125. return leafCert, false, fmt.Errorf("Failed to append GEOTRUST cert\n")
  126. }
  127. // Verify leaf certificate
  128. storeCtx := x509.VerifyOptions{
  129. Intermediates: intermediates,
  130. Roots: roots,
  131. KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
  132. }
  133. _, err = leafCert.Verify(storeCtx)
  134. if err != nil {
  135. return leafCert, false, err
  136. }
  137. return leafCert, validCN, nil
  138. }
  139. func (body *jwtBody) verifyJWTBody() (validApkCert, validApkPackageName bool) {
  140. // Verify apk certificate digest
  141. if len(body.ApkCertificateDigestSha256) >= 1 && body.ApkCertificateDigestSha256[0] == psiphon3Base64CertHash {
  142. validApkCert = true
  143. }
  144. // Verify apk package name
  145. if psiphon.Contains(psiphonApkPackagenames, body.ApkPackageName) {
  146. validApkPackageName = true
  147. }
  148. return
  149. }
  150. // Form log fields for debugging
  151. func errorLogFields(err error, params requestJSONObject) LogFields {
  152. return LogFields{
  153. "error_message": err.Error(),
  154. "payload": params,
  155. }
  156. }
  157. // Convert error to string for logging
  158. func getError(err error) string {
  159. if err == nil {
  160. return ""
  161. }
  162. return err.Error()
  163. }
  164. // Validate JWT produced by safetynet
  165. func verifySafetyNetPayload(params requestJSONObject) (bool, LogFields) {
  166. jwt := newJwt(params)
  167. if jwt == nil {
  168. // Malformed JWT
  169. return false, errorLogFields(errors.New("Invalid request to client_verification, malformed jwt"), params)
  170. }
  171. statusStrings := map[int]string{
  172. 0: "API_REQUEST_OK",
  173. 1: "API_REQUEST_FAILED",
  174. 2: "API_CONNECT_FAILED",
  175. }
  176. statusString, ok := statusStrings[(*jwt).status]
  177. if !ok {
  178. statusString = "INVALID_STATUS: expected 0-2, got " + strconv.Itoa((*jwt).status)
  179. }
  180. // SafetyNet check failed
  181. if (*jwt).status != 0 {
  182. return false, errorLogFields(errors.New(statusString), params)
  183. }
  184. // Split into base64 encoded header, body, signature
  185. jwtParts := strings.Split((*jwt).payload, ".")
  186. if len(jwtParts) != 3 {
  187. // Malformed payload
  188. return false, errorLogFields(errors.New("Invalid request to client_verification, malformed jwt"), params)
  189. }
  190. // Decode header, body, signature
  191. headerJson, err := base64.RawURLEncoding.DecodeString(jwtParts[0])
  192. if err != nil {
  193. return false, errorLogFields(err, params)
  194. }
  195. bodyJson, err := base64.RawURLEncoding.DecodeString(jwtParts[1])
  196. if err != nil {
  197. return false, errorLogFields(err, params)
  198. }
  199. signature, err := base64.RawURLEncoding.DecodeString(jwtParts[2])
  200. if err != nil {
  201. return false, errorLogFields(err, params)
  202. }
  203. // Extract header from json
  204. header, err := newJwtHeader(headerJson)
  205. if err != nil {
  206. return false, errorLogFields(err, params)
  207. }
  208. // Verify certchain in header
  209. leafCert, validCN, certChainErrors := header.CertChain.verifyCertChain()
  210. var signatureErrors error
  211. if leafCert == nil {
  212. signatureErrors = errors.New("Failed to parse leaf certificate")
  213. } else {
  214. // Verify signature over header and body
  215. signatureErrors = leafCert.CheckSignature(x509.SHA256WithRSA, []byte(jwtParts[0]+"."+jwtParts[1]), signature)
  216. }
  217. // Extract body from json
  218. body, err := newJwtBody(bodyJson)
  219. if err != nil {
  220. return false, errorLogFields(err, params)
  221. }
  222. // Validate jwt payload
  223. validApkCert, validApkPackageName := body.verifyJWTBody()
  224. validCertChain := certChainErrors == nil
  225. validSignature := signatureErrors == nil
  226. verified := validCN && validApkCert && validApkPackageName && validCertChain && validSignature
  227. // Generate logging information
  228. logFields := LogFields{
  229. "apk_certificate_digest_sha256": body.ApkCertificateDigestSha256[0],
  230. "apk_digest_sha256": body.ApkDigestSha256,
  231. "apk_package_name": body.ApkPackageName,
  232. "certchain_errors": getError(certChainErrors),
  233. "cts_profile_match": body.CtsProfileMatch,
  234. "extension": body.Extension,
  235. "nonce": body.Nonce,
  236. "signature_errors": getError(signatureErrors),
  237. "status": strconv.Itoa((*jwt).status),
  238. "status_string": statusString,
  239. "valid_cn": validCN,
  240. "valid_apk_cert": validApkCert,
  241. "valid_apk_packagename": validApkPackageName,
  242. "valid_certchain": validCertChain,
  243. "valid_signature": validSignature,
  244. "verification_timestamp": time.Unix(0, int64(body.TimestampMs)*1e6).UTC().Format(time.RFC3339),
  245. "verified": verified,
  246. }
  247. return verified, logFields
  248. }