Skip to content

Commit 4bac500

Browse files
authored
[BACK-2163] API for Generating Challenge Values for App Attestation or App Assertion (#617)
* [BACK-2164] Add attestation verification route. (#618) * [BACK-2295] Implement API to fetch pump certificates (#659)
1 parent 7c84e46 commit 4bac500

File tree

103 files changed

+34977
-19
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+34977
-19
lines changed

Diff for: Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ format-write-changed:
115115
imports: goimports
116116
@echo "goimports -d -e -local 'github.com/tidepool-org/platform'"
117117
@cd $(ROOT_DIRECTORY) && \
118-
O=`find . -not -path './.gvm_local/*' -not -path './vendor/*' -not -path '**/test/mock.go' -not -name '**_gen.go' -name '*.go' -type f -exec goimports -d -e -local 'github.com/tidepool-org/platform' {} \; 2>&1` && \
118+
O=`find . -not -path './.gvm_local/*' -not -path './vendor/*' -not -path '**/test/mock.go' -not -name '*mock.go' -not -name '**_gen.go' -name '*.go' -type f -exec goimports -d -e -local 'github.com/tidepool-org/platform' {} \; 2>&1` && \
119119
[ -z "$${O}" ] || (echo "$${O}" && exit 1)
120120

121121
imports-write: goimports

Diff for: appvalidate/appvalidate.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Package appvalidate handles the logic for validating whether an app is a
2+
// valid instance of your app via Apple's App Attest service.
3+
package appvalidate
4+
5+
import (
6+
"regexp"
7+
"time"
8+
9+
"github.com/tidepool-org/platform/structure"
10+
11+
structValidator "github.com/tidepool-org/platform/structure/validator"
12+
)
13+
14+
//go:generate mockgen -build_flags=--mod=mod -destination=./mock.go -package=appvalidate github.com/tidepool-org/platform/appvalidate Repository,ChallengeGenerator
15+
16+
var (
17+
// base64 regex that supports base64.URLEncoding ("+/" replaced by "-_") or base64.StdEncoding. Used for base64 payloads like the attestation and assertion object.
18+
base64Chars = regexp.MustCompile("^(?:[A-Za-z0-9+/\\-_]{4})*(?:[A-Za-z0-9+/\\-_]{2}==|[A-Za-z0-9+/\\-_]{3}=)?$")
19+
)
20+
21+
// AppValidation represents the entire state of a person's attestation /
22+
// assertion status that determines if they are using a legitimate instance
23+
// of an iOS app.
24+
type AppValidation struct {
25+
UserID string `json:"userId" bson:"userId,omitempty"`
26+
KeyID string `json:"keyId" bson:"keyId,omitempty"`
27+
PublicKey string `json:"-" bson:"publicKey,omitempty"`
28+
Verified bool `json:"verified" bson:"verified"`
29+
FraudAssessmentReceipt string `json:"-" bson:"fraudAssessmentReceipt,omitempty"`
30+
AttestationChallenge string `json:"-" bson:"attestationChallenge,omitempty"`
31+
AssertionVerifiedTime *time.Time `json:"-" bson:"assertionVerifiedTime,omitempty"`
32+
AssertionChallenge string `json:"-" bson:"assertionChallenge,omitempty"`
33+
AttestationVerifiedTime *time.Time `json:"-" bson:"attestationVerifiedTime"`
34+
AssertionCounter uint32 `json:"assertionCounter" bson:"assertionCounter"`
35+
}
36+
37+
// NewAppValidation creates a new AppValidation from a ChallengeCreate. Once a
38+
// person starts the attestation process by requesting an attestation
39+
// challenge, a new AppValidation needs to be persisted to keep track of the
40+
// progress and state of the attestation and future assertions.
41+
func NewAppValidation(attestChallenge string, create *ChallengeCreate) (*AppValidation, error) {
42+
if err := structValidator.New().Validate(create); err != nil {
43+
return nil, err
44+
}
45+
validation := AppValidation{
46+
UserID: create.UserID,
47+
KeyID: create.KeyID,
48+
AttestationChallenge: attestChallenge,
49+
}
50+
if err := structValidator.New().Validate(&validation); err != nil {
51+
return nil, err
52+
}
53+
return &validation, nil
54+
}
55+
56+
func (av *AppValidation) Validate(v structure.Validator) {
57+
v.String("attestationChallenge", &av.AttestationChallenge).NotEmpty()
58+
v.String("userId", &av.UserID).NotEmpty()
59+
v.String("keyId", &av.KeyID).NotEmpty()
60+
}

Diff for: appvalidate/assertion.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package appvalidate
2+
3+
import (
4+
"encoding/json"
5+
"time"
6+
7+
"github.com/tidepool-org/platform/structure"
8+
9+
appAssert "github.com/bas-d/appattest/assertion"
10+
appUtils "github.com/bas-d/appattest/utils"
11+
)
12+
13+
// AssertionVerify is the expected request body used by clients to complete
14+
// the assertion process. Assertion can only be done after attestation is
15+
// completed. The Assertion should be the base64 encoding of the binary CBOR
16+
// data returned from the iOS APIs.
17+
type AssertionVerify struct {
18+
UserID string `json:"-"`
19+
KeyID string `json:"keyId"`
20+
ClientData AssertionClientData `json:"clientData"`
21+
Assertion string `json:"assertion"`
22+
}
23+
24+
// AssertionUpdate contains the assertion fields to update in an AppValidation
25+
// to pass to a repository.
26+
type AssertionUpdate struct {
27+
Challenge string `bson:"assertionChallenge,omitempty"`
28+
VerifiedTime time.Time `bson:"assertionVerifiedTime,omitempty"`
29+
AssertionCounter uint32 `bson:"assertionCounter,omitempty"`
30+
}
31+
32+
type AssertionResponse struct {
33+
Data any `json:"data"`
34+
}
35+
36+
type AssertionClientData struct {
37+
Challenge string `json:"challenge"`
38+
Partner string `json:"partner"` // Which partner are we requesting a secret from
39+
PartnerData json.RawMessage `json:"partnerData"` // Data to send to partner - This is a RawMessage because it is partner specific. The validation of this is delayed until later.
40+
}
41+
42+
func NewAssertionVerify(userID string) *AssertionVerify {
43+
return &AssertionVerify{
44+
UserID: userID,
45+
}
46+
}
47+
48+
func (av *AssertionVerify) Validate(v structure.Validator) {
49+
v.String("assertion", &av.Assertion).NotEmpty().Matches(base64Chars)
50+
v.String("clientData.challenge", &av.ClientData.Challenge).NotEmpty()
51+
v.String("clientData.partner", &av.ClientData.Partner).OneOf(partners...)
52+
53+
v.String("userId", &av.UserID).NotEmpty()
54+
v.String("keyId", &av.KeyID).NotEmpty()
55+
}
56+
57+
func transformAssertion(av *AssertionVerify) (*appAssert.AuthenticatorAssertionResponse, error) {
58+
clientDataRaw, err := json.Marshal(av.ClientData)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
var assertion appUtils.URLEncodedBase64
64+
assertionRaw := b64StdEncodingToURLEncoding(av.Assertion)
65+
if err := assertion.UnmarshalJSON([]byte(assertionRaw)); err != nil {
66+
return nil, err
67+
}
68+
69+
return &appAssert.AuthenticatorAssertionResponse{
70+
RawClientData: appUtils.URLEncodedBase64(clientDataRaw),
71+
Assertion: assertion,
72+
}, nil
73+
}

Diff for: appvalidate/attestation.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package appvalidate
2+
3+
import (
4+
"encoding/base64"
5+
"time"
6+
7+
"github.com/tidepool-org/platform/structure"
8+
9+
appAttest "github.com/bas-d/appattest/attestation"
10+
appUtils "github.com/bas-d/appattest/utils"
11+
)
12+
13+
// AttestationVerify is the request body used to validate an app's
14+
// attestation. It is decoded from a JSON object.
15+
// https://developer.apple.com/documentation/devicecheck/establishing_your_app_s_integrity#3561588
16+
// KeyID and AttestationObject is data returned by the iOS APIs.
17+
// Attestation will be returned in CBOR format and should be base64
18+
// encoded before sending.
19+
type AttestationVerify struct {
20+
Attestation string `json:"attestation"`
21+
Challenge string `json:"challenge"`
22+
KeyID string `json:"keyId"`
23+
UserID string `json:"-"`
24+
}
25+
26+
func NewAttestationVerify(userID string) *AttestationVerify {
27+
return &AttestationVerify{
28+
UserID: userID,
29+
}
30+
}
31+
32+
func (av *AttestationVerify) Validate(v structure.Validator) {
33+
v.String("challenge", &av.Challenge).NotEmpty()
34+
v.String("attestation", &av.Attestation).Matches(base64Chars)
35+
v.String("userId", &av.UserID).NotEmpty()
36+
v.String("keyId", &av.KeyID).NotEmpty()
37+
}
38+
39+
type AttestationUpdate struct {
40+
PublicKey string `bson:"publicKey,omitempty"`
41+
Verified bool `bson:"verified"`
42+
FraudAssessmentReceipt string `bson:"fraudAssessmentReceipt,omitempty"`
43+
VerifiedTime time.Time `bson:"attestationVerifiedTime"`
44+
}
45+
46+
func (au *AttestationUpdate) Validate(v structure.Validator) {
47+
v.String("publicKey", &au.PublicKey).NotEmpty()
48+
v.String("fraudAssessmentReceipt", &au.FraudAssessmentReceipt).NotEmpty()
49+
v.Time("assertionVerifiedTime", &au.VerifiedTime).NotZero()
50+
}
51+
52+
func transformAttestation(av *AttestationVerify) (*appAttest.AuthenticatorAttestationResponse, error) {
53+
// The appattest library expects all the data to use base64 URLEncoding when the data from IOS is base64 StdEncoding so convert first.
54+
55+
clientDataRaw := make([]byte, base64.RawURLEncoding.EncodedLen(len([]byte(av.Challenge))))
56+
base64.RawURLEncoding.Encode(clientDataRaw, []byte(av.Challenge))
57+
var clientData appUtils.URLEncodedBase64
58+
if err := clientData.UnmarshalJSON(clientDataRaw); err != nil {
59+
return nil, err
60+
}
61+
62+
attestationRaw := b64StdEncodingToURLEncoding(av.Attestation)
63+
var attestation appUtils.URLEncodedBase64
64+
if err := attestation.UnmarshalJSON([]byte(attestationRaw)); err != nil {
65+
return nil, err
66+
}
67+
68+
return &appAttest.AuthenticatorAttestationResponse{
69+
ClientData: clientData,
70+
KeyID: av.KeyID,
71+
AttestationObject: attestation,
72+
}, nil
73+
}

Diff for: appvalidate/base64.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package appvalidate
2+
3+
import (
4+
"strings"
5+
)
6+
7+
func b64StdEncodingToURLEncoding(s string) string {
8+
return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(s, "+", "-"), "/", "_"), "=", "\"")
9+
}

Diff for: appvalidate/challenge.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package appvalidate
2+
3+
import (
4+
"github.com/tidepool-org/platform/id"
5+
"github.com/tidepool-org/platform/structure"
6+
)
7+
8+
// ChallengeCreate is the expected request body used to create an attestation
9+
// or assertion challenge.
10+
type ChallengeCreate struct {
11+
UserID string `json:"-"` // json ignored because taken from request.Details and not from user supplied input.
12+
KeyID string `json:"keyId"`
13+
}
14+
15+
// ChallengeResult is the response to a successful request with
16+
// ChallengeCreate
17+
type ChallengeResult struct {
18+
Challenge string `json:"challenge"`
19+
}
20+
21+
func NewChallengeCreate(userID string) *ChallengeCreate {
22+
return &ChallengeCreate{
23+
UserID: userID,
24+
}
25+
}
26+
27+
func (c *ChallengeCreate) Validate(v structure.Validator) {
28+
v.String("userId", &c.UserID).NotEmpty()
29+
v.String("keyId", &c.KeyID).NotEmpty()
30+
}
31+
32+
type ChallengeGenerator interface {
33+
GenerateChallenge(size int) (string, error)
34+
}
35+
36+
type challengeGenerator struct{}
37+
38+
func NewChallengeGenerator() ChallengeGenerator {
39+
return challengeGenerator{}
40+
}
41+
42+
func (c challengeGenerator) GenerateChallenge(size int) (string, error) {
43+
return id.New(size)
44+
}

0 commit comments

Comments
 (0)