Skip to content
This repository has been archived by the owner on Aug 7, 2023. It is now read-only.

Added Go server sample #28

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
646 changes: 646 additions & 0 deletions server/go/LICENSE

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions server/go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Go Server SafetyNet Samples
===================================

This sample demonstrates how to verify the response received from the SafetyNet service.

It shows how to extract the compatibility check response from the JWS message, validate its SSL certificate chain, hostname and signature.

This check can be done completely offline (See `offline_verify.go`) or by using the _Android Verification API_ to verify the content and signature of the response (see `ParseAndVerifyOnline`). This REST API requires you to register at the Google Developers console and register for an API key. Detailed steps are available [in the documentation] under _Validating the response with Google APIs_.


Note that this sample only provides a basic overview over the verification process and does not cover all possibilities. For example,it is reccomended to always verify the nonce in the request as well. This sample also does not show the app-to-server communication.

For more details, see the guide at https://developer.android.com/training/safetynet/index.html#verify-compat-check .

Next: Verify the response
-------------------------

This sample only demonstrates verification of the server response. The next step is to verify that the attestation response matches the request by verifying their contents, including the package name, digest, timestamp and nonce.

**This step is crucial. If you do not verify the response and request, you may be vulnerable to a [replay attack][replay-attack], in which an attacker can replay an old attestation response from a different device or app.**




Pre-requisites
--------------

- Go 1.14 or greater

Getting Started
---------------

This sample uses Go Modules dependencies.

go get
go build
./android-play-safetynet <TOKEN>

Online verification requires an API key for the _Android Verification API_. Follow the steps in [the documentation under "_Validating the response with Google APIs_"][key] and add the API key into the `API_KEY` field at the top of `online_verify.go`.

Runing the Samples
------------------
* Build and run the [Android component](../../android) of this sample.
* Retrieve a signed statement from the Android app and copy it to your machine. (You can use the "Share Result" option.)
* Build this server component and provide the signed statement as input.


License
-------

Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.

[key]: https://developer.android.com/training/safetynet/index.html#verify-compat-check "See Validating the response with Google APIs"
[replay-attack]:https://en.wikipedia.org/wiki/Replay_attack
71 changes: 71 additions & 0 deletions server/go/attestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"encoding/base64"
"errors"
"github.com/dgrijalva/jwt-go"
)

type AttestationHeader struct {
Algorithme string `json:"alg"`
AlgorCertificats []string `json:"x5c"`
}

func GetAttestationHeader(header map[string]interface{}) (*AttestationHeader, error) {
result := AttestationHeader{}
if algo, ok := header["alg"].(string); ok {
result.Algorithme = algo
} else {
return nil, errors.New("No alg header")
}
if certs, ok := header["x5c"]; ok {
if certsList, ok := certs.([]interface{}); ok {
for _, data := range certsList {
result.AlgorCertificats = append(result.AlgorCertificats, data.(string))
}
} else {
return nil, errors.New("Header x5c not a list")
}
} else {
return nil, errors.New("No x5c header")
}
return &result, nil
}

type AttestationStatement struct {
Nonce string `json:"nonce"`
TimestampMs int64 `json:"timestampMs"`
ApkPackageName string `json:"apkPackageName"`
ApkDigestSha256 string `json:"apkDigestSha256"`
CtsProfileMatch bool `json:"ctsProfileMatch"`
ApkCertificateDigestSha256 []string `json:"apkCertificateDigestSha256"`
BasicIntegrity bool `json:"basicIntegrity"`
EvaluationType string `json:"evaluationType"`
}

func (statement AttestationStatement) Valid() error {
vErr := new(jwt.ValidationError)

if vErr.Errors == 0 {
return nil
}

return vErr
}

func (statement AttestationStatement) ReadNonce() string {
decoded, _ := base64.StdEncoding.DecodeString(statement.Nonce)
return string(decoded)
}
8 changes: 8 additions & 0 deletions server/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module android-play-safetynet

go 1.14

require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/stretchr/testify v1.7.0
)
13 changes: 13 additions & 0 deletions server/go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
65 changes: 65 additions & 0 deletions server/go/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"fmt"
"os"
"strconv"
)

func main() {
allParams := os.Args
if len(allParams) != 2 {
fmt.Println("Usage: android-play-safetynet <signed attestation statement>")
return
}

stmt, err := ParseAndVerify(allParams[1])
if err != nil {
fmt.Println("Failure: Failed to parse and verify the attestation statement.")
fmt.Println("Failure detail: " + err.Error())
return
}

fmt.Println("The content of the attestation statement is:")

// Nonce that was submitted as part of this request.
fmt.Println("Nonce: " + stmt.ReadNonce())
// Timestamp of the request.
fmt.Println("Timestamp: " + strconv.FormatInt(stmt.TimestampMs, 10) + " ms")

if len(stmt.ApkPackageName) > 0 && len(stmt.ApkDigestSha256) > 0 {
// Package name and digest of APK that submitted this request. Note that these details
// may be omitted if the API cannot reliably determine the package information.
fmt.Println("APK package name: " + stmt.ApkPackageName)
fmt.Println("APK digest SHA256: " + stmt.ApkDigestSha256)
}
// Has the device a matching CTS profile?
ctsProfile := "FALSE"
if stmt.CtsProfileMatch {
ctsProfile = "TRUE"
}
fmt.Println("CTS profile match: " + ctsProfile)
// Has the device passed CTS (but the profile could not be verified on the server)?
hasBasicIntegrity := "FALSE"
if stmt.BasicIntegrity {
hasBasicIntegrity = "TRUE"
}
fmt.Println("Basic integrity match: " + hasBasicIntegrity)

fmt.Println("\n** This sample only shows how to verify the authenticity of an " +
"attestation response. Next, you must check that the server response matches the " +
"request by comparing the nonce, package name, timestamp and digest.")

}
59 changes: 59 additions & 0 deletions server/go/offline_verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"crypto/x509"
"encoding/pem"
"errors"
"github.com/dgrijalva/jwt-go"
)

func ParseAndVerify(signedAttestationStatment string) (*AttestationStatement, error) {

// Parse JSON Web Signature format.
token, _, err := new(jwt.Parser).ParseUnverified(signedAttestationStatment, &AttestationStatement{})
if err != nil {
return nil, errors.New("Failure: Parse token error : " + err.Error())
}

// Verify the signature of the JWS and retrieve the signature certificate.
header, err := GetAttestationHeader(token.Header)
if err != nil {
return nil, errors.New("Bad header format : " + err.Error())
}
cert := "-----BEGIN CERTIFICATE-----\n" + header.AlgorCertificats[0] + "\n-----END CERTIFICATE-----"
googlePublicKey, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
_, err = jwt.ParseWithClaims(signedAttestationStatment, &AttestationStatement{}, func(token *jwt.Token) (interface{}, error) {
return googlePublicKey, nil
})
if err != nil {
return nil, errors.New("Failure: Signature verification failed : " + err.Error())
}

// Verify the hostname of the certificate.
if !verifyHostname("attest.android.com", cert) {
return nil, errors.New("Failure: Certificate isn't issued for the hostname attest.android.com")
}

return token.Claims.(*AttestationStatement), nil
}

func verifyHostname(hostname, pemCertificat string) bool {
block, _ := pem.Decode([]byte(pemCertificat))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return false
}
return cert.Subject.CommonName == hostname
}
Loading