123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- package protocol
-
- import (
- "bytes"
- "crypto/sha256"
- "crypto/x509"
- "encoding/base64"
- "fmt"
- "time"
-
- "github.com/duo-labs/webauthn/metadata"
-
- jwt "github.com/golang-jwt/jwt/v4"
- "github.com/mitchellh/mapstructure"
- )
-
- var safetyNetAttestationKey = "android-safetynet"
-
- func init() {
- RegisterAttestationFormat(safetyNetAttestationKey, verifySafetyNetFormat)
- }
-
- type SafetyNetResponse struct {
- Nonce string `json:"nonce"`
- TimestampMs int64 `json:"timestampMs"`
- ApkPackageName string `json:"apkPackageName"`
- ApkDigestSha256 string `json:"apkDigestSha256"`
- CtsProfileMatch bool `json:"ctsProfileMatch"`
- ApkCertificateDigestSha256 []interface{} `json:"apkCertificateDigestSha256"`
- BasicIntegrity bool `json:"basicIntegrity"`
- }
-
- // Thanks to @koesie10 and @herrjemand for outlining how to support this type really well
-
- // §8.5. Android SafetyNet Attestation Statement Format https://w3c.github.io/webauthn/#android-safetynet-attestation
- // When the authenticator in question is a platform-provided Authenticator on certain Android platforms, the attestation
- // statement is based on the SafetyNet API. In this case the authenticator data is completely controlled by the caller of
- // the SafetyNet API (typically an application running on the Android platform) and the attestation statement only provides
- // some statements about the health of the platform and the identity of the calling application. This attestation does not
- // provide information regarding provenance of the authenticator and its associated data. Therefore platform-provided
- // authenticators SHOULD make use of the Android Key Attestation when available, even if the SafetyNet API is also present.
- func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
- // The syntax of an Android Attestation statement is defined as follows:
- // $$attStmtType //= (
- // fmt: "android-safetynet",
- // attStmt: safetynetStmtFormat
- // )
-
- // safetynetStmtFormat = {
- // ver: text,
- // response: bytes
- // }
-
- // §8.5.1 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
- // the contained fields.
-
- // We have done this
- // §8.5.2 Verify that response is a valid SafetyNet response of version ver.
- version, present := att.AttStatement["ver"].(string)
- if !present {
- return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet")
- }
-
- if version == "" {
- return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Not a proper version for SafetyNet")
- }
-
- // TODO: provide user the ability to designate their supported versions
-
- response, present := att.AttStatement["response"].([]byte)
- if !present {
- return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response")
- }
-
- token, err := jwt.Parse(string(response), func(token *jwt.Token) (interface{}, error) {
- chain := token.Header["x5c"].([]interface{})
- o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string))))
- n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string)))
- cert, err := x509.ParseCertificate(o[:n])
- return cert.PublicKey, err
- })
- if err != nil {
- return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
- }
-
- // marshall the JWT payload into the safetynet response json
- var safetyNetResponse SafetyNetResponse
- err = mapstructure.Decode(token.Claims, &safetyNetResponse)
- if err != nil {
- return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err))
- }
-
- // §8.5.3 Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation
- // of authenticatorData and clientDataHash.
- nonceBuffer := sha256.Sum256(append(att.RawAuthData, clientDataHash...))
- nonceBytes, err := base64.StdEncoding.DecodeString(safetyNetResponse.Nonce)
- if !bytes.Equal(nonceBuffer[:], nonceBytes) || err != nil {
- return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response")
- }
-
- // §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
- certChain := token.Header["x5c"].([]interface{})
- l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string))))
- n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string)))
- if err != nil {
- return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
- }
- attestationCert, err := x509.ParseCertificate(l[:n])
- if err != nil {
- return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
- }
-
- // §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com"
- err = attestationCert.VerifyHostname("attest.android.com")
- if err != nil {
- return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
- }
-
- // §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
- if !safetyNetResponse.CtsProfileMatch {
- return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false")
- }
-
- // Verify sanity of timestamp in the payload
- now := time.Now()
- oneMinuteAgo := now.Add(-time.Minute)
- t := time.Unix(safetyNetResponse.TimestampMs/1000, 0)
- if t.After(now) {
- // zero tolerance for post-dated timestamps
- return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time")
- } else if t.Before(oneMinuteAgo) {
- // allow old timestamp for testing purposes
- // TODO: Make this user configurable
- msg := "SafetyNet response with timestamp before one minute ago"
- if metadata.Conformance {
- return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails(msg)
- }
- }
-
- // §8.5.7 If successful, return implementation-specific values representing attestation type Basic and attestation
- // trust path attestationCert.
- return "Basic attestation with SafetyNet", nil, nil
- }
|