You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

attestation_safetynet.go 6.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. package protocol
  2. import (
  3. "bytes"
  4. "crypto/sha256"
  5. "crypto/x509"
  6. "encoding/base64"
  7. "fmt"
  8. "time"
  9. "github.com/duo-labs/webauthn/metadata"
  10. jwt "github.com/golang-jwt/jwt/v4"
  11. "github.com/mitchellh/mapstructure"
  12. )
  13. var safetyNetAttestationKey = "android-safetynet"
  14. func init() {
  15. RegisterAttestationFormat(safetyNetAttestationKey, verifySafetyNetFormat)
  16. }
  17. type SafetyNetResponse struct {
  18. Nonce string `json:"nonce"`
  19. TimestampMs int64 `json:"timestampMs"`
  20. ApkPackageName string `json:"apkPackageName"`
  21. ApkDigestSha256 string `json:"apkDigestSha256"`
  22. CtsProfileMatch bool `json:"ctsProfileMatch"`
  23. ApkCertificateDigestSha256 []interface{} `json:"apkCertificateDigestSha256"`
  24. BasicIntegrity bool `json:"basicIntegrity"`
  25. }
  26. // Thanks to @koesie10 and @herrjemand for outlining how to support this type really well
  27. // §8.5. Android SafetyNet Attestation Statement Format https://w3c.github.io/webauthn/#android-safetynet-attestation
  28. // When the authenticator in question is a platform-provided Authenticator on certain Android platforms, the attestation
  29. // statement is based on the SafetyNet API. In this case the authenticator data is completely controlled by the caller of
  30. // the SafetyNet API (typically an application running on the Android platform) and the attestation statement only provides
  31. // some statements about the health of the platform and the identity of the calling application. This attestation does not
  32. // provide information regarding provenance of the authenticator and its associated data. Therefore platform-provided
  33. // authenticators SHOULD make use of the Android Key Attestation when available, even if the SafetyNet API is also present.
  34. func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
  35. // The syntax of an Android Attestation statement is defined as follows:
  36. // $$attStmtType //= (
  37. // fmt: "android-safetynet",
  38. // attStmt: safetynetStmtFormat
  39. // )
  40. // safetynetStmtFormat = {
  41. // ver: text,
  42. // response: bytes
  43. // }
  44. // §8.5.1 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
  45. // the contained fields.
  46. // We have done this
  47. // §8.5.2 Verify that response is a valid SafetyNet response of version ver.
  48. version, present := att.AttStatement["ver"].(string)
  49. if !present {
  50. return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet")
  51. }
  52. if version == "" {
  53. return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Not a proper version for SafetyNet")
  54. }
  55. // TODO: provide user the ability to designate their supported versions
  56. response, present := att.AttStatement["response"].([]byte)
  57. if !present {
  58. return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response")
  59. }
  60. token, err := jwt.Parse(string(response), func(token *jwt.Token) (interface{}, error) {
  61. chain := token.Header["x5c"].([]interface{})
  62. o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string))))
  63. n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string)))
  64. cert, err := x509.ParseCertificate(o[:n])
  65. return cert.PublicKey, err
  66. })
  67. if err != nil {
  68. return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
  69. }
  70. // marshall the JWT payload into the safetynet response json
  71. var safetyNetResponse SafetyNetResponse
  72. err = mapstructure.Decode(token.Claims, &safetyNetResponse)
  73. if err != nil {
  74. return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err))
  75. }
  76. // §8.5.3 Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation
  77. // of authenticatorData and clientDataHash.
  78. nonceBuffer := sha256.Sum256(append(att.RawAuthData, clientDataHash...))
  79. nonceBytes, err := base64.StdEncoding.DecodeString(safetyNetResponse.Nonce)
  80. if !bytes.Equal(nonceBuffer[:], nonceBytes) || err != nil {
  81. return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response")
  82. }
  83. // §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
  84. certChain := token.Header["x5c"].([]interface{})
  85. l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string))))
  86. n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string)))
  87. if err != nil {
  88. return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
  89. }
  90. attestationCert, err := x509.ParseCertificate(l[:n])
  91. if err != nil {
  92. return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
  93. }
  94. // §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com"
  95. err = attestationCert.VerifyHostname("attest.android.com")
  96. if err != nil {
  97. return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
  98. }
  99. // §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
  100. if !safetyNetResponse.CtsProfileMatch {
  101. return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false")
  102. }
  103. // Verify sanity of timestamp in the payload
  104. now := time.Now()
  105. oneMinuteAgo := now.Add(-time.Minute)
  106. t := time.Unix(safetyNetResponse.TimestampMs/1000, 0)
  107. if t.After(now) {
  108. // zero tolerance for post-dated timestamps
  109. return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time")
  110. } else if t.Before(oneMinuteAgo) {
  111. // allow old timestamp for testing purposes
  112. // TODO: Make this user configurable
  113. msg := "SafetyNet response with timestamp before one minute ago"
  114. if metadata.Conformance {
  115. return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails(msg)
  116. }
  117. }
  118. // §8.5.7 If successful, return implementation-specific values representing attestation type Basic and attestation
  119. // trust path attestationCert.
  120. return "Basic attestation with SafetyNet", nil, nil
  121. }