Skip to content

Commit 4f3cdf7

Browse files
committed
Merge remote-tracking branch 'origin/master' into tk/update-guardrails
2 parents 49ad956 + 7703f6e commit 4f3cdf7

File tree

180 files changed

+38911
-4569
lines changed

Some content is hidden

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

180 files changed

+38911
-4569
lines changed

Makefile

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ else
1818
VERSION_BASE:=$(shell git describe --abbrev=0 --tags 2> /dev/null || echo 'v0.0.0')
1919
endif
2020
VERSION_BASE:=$(VERSION_BASE:v%=%)
21-
VERSION_SHORT_COMMIT:=$(shell git rev-parse --short HEAD)
22-
VERSION_FULL_COMMIT:=$(shell git rev-parse HEAD)
21+
VERSION_SHORT_COMMIT:=$(shell git rev-parse --short HEAD || echo "dev")
22+
VERSION_FULL_COMMIT:=$(shell git rev-parse HEAD || echo "dev")
2323
VERSION_PACKAGE:=$(REPOSITORY_PACKAGE)/application
2424

2525
GO_BUILD_FLAGS:=-buildvcs=false
@@ -30,6 +30,13 @@ TRANSFORM_GO_BUILD_CMD:=sed 's|\.\(.*\)\(/[^/]*\)/[^/]*|_bin\1\2\2 .\1\2/.|'
3030

3131
GO_BUILD_CMD:=go build $(GO_BUILD_FLAGS) $(GO_LD_FLAGS) -o
3232

33+
GINKGO_FLAGS += --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r
34+
GINKGO_CI_WATCH_FLAGS += --randomize-all --succinct --fail-on-pending --cover --trace --race
35+
GINKGO_CI_FLAGS += $(GINKGO_CI_WATCH_FLAGS) --randomize-suites --keep-going
36+
37+
GOTEST_PKGS ?= ./...
38+
GOTEST_FLAGS ?=
39+
3340
ifdef TRAVIS_BRANCH
3441
ifdef TRAVIS_COMMIT
3542
DOCKER:=true
@@ -115,7 +122,7 @@ format-write-changed:
115122
imports: goimports
116123
@echo "goimports -d -e -local 'github.com/tidepool-org/platform'"
117124
@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` && \
125+
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` && \
119126
[ -z "$${O}" ] || (echo "$${O}" && exit 1)
120127

121128
imports-write: goimports
@@ -192,28 +199,35 @@ service-restart-all:
192199
@cd $(ROOT_DIRECTORY) && for SERVICE in migrations tools; do $(MAKE) service-restart SERVICE="$${SERVICE}"; done
193200

194201
test: ginkgo
195-
@echo "ginkgo --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r $(TEST)"
196-
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r $(TEST)
202+
@echo "ginkgo $(GINKGO_FLAGS) $(TEST)"
203+
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo $(GINKGO_FLAGS) $(TEST)
197204

198205
test-until-failure: ginkgo
199-
@echo "ginkgo --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r -untilItFails $(TEST)"
200-
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r -untilItFails $(TEST)
206+
@echo "ginkgo $(GINKGO_FLAGS) -untilItFails $(TEST)"
207+
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo $(GINKGO_FLAGS) -untilItFails $(TEST)
201208

202209
test-watch: ginkgo
203-
@echo "ginkgo watch --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r $(TEST)"
204-
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo watch --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r $(TEST)
210+
@echo "ginkgo watch $(GINKGO_FLAGS) $(TEST)"
211+
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo watch $(GINKGO_FLAGS) $(TEST)
205212

206213
ci-test: ginkgo
207-
@echo "ginkgo --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r --randomize-suites --randomize-all --succinct --fail-on-pending --cover --trace --race --keep-going $(TEST)"
208-
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r --randomize-suites --randomize-all --succinct --fail-on-pending --cover --trace --race --keep-going $(TEST)
214+
@echo "ginkgo $(GINKGO_FLAGS) $(GINKGO_CI_FLAGS) $(TEST)"
215+
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo $(GINKGO_FLAGS) $(GINKGO_CI_FLAGS) $(TEST)
209216

210217
ci-test-until-failure: ginkgo
211-
@echo "ginkgo --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r --randomize-suites --randomize-all --succinct --fail-on-pending --cover --trace --race --keep-going -untilItFails $(TEST)"
212-
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r --randomize-suites --randomize-all --succinct --fail-on-pending --cover --trace --race --keep-going -untilItFails $(TEST)
218+
@echo "ginkgo $(GINKGO_FLAGS) $(GINKGO_CI_FLAGS) -untilItFails $(TEST)"
219+
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo $(GINKGO_FLAGS) $(GINKGO_CI_FLAGS) -untilItFails $(TEST)
213220

214221
ci-test-watch: ginkgo
215-
@echo "ginkgo watch --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r --randomize-all --succinct --fail-on-pending --cover --trace --race $(TEST)"
216-
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo watch --require-suite --poll-progress-after=10s --poll-progress-interval=20s -r --randomize-all --succinct --fail-on-pending --cover --trace --race $(TEST)
222+
@echo "ginkgo watch $(GINKGO_FLAGS) $(GINKGO_CI_WATCH_FLAGS) $(TEST)"
223+
@cd $(ROOT_DIRECTORY) && . ./env.test.sh && ginkgo watch $(GINKGO_FLAGS) $(GINKGO_CI_WATCH_FLAGS) $(TEST)
224+
225+
go-test:
226+
. ./env.test.sh && go test $(GOTEST_FLAGS) $(GOTEST_PKGS)
227+
228+
go-ci-test: GOTEST_FLAGS += -count=1 -race -shuffle=on -cover
229+
go-ci-test: GOTEST_PKGS = ./...
230+
go-ci-test: go-test
217231

218232
deploy: clean-deploy deploy-services deploy-migrations deploy-tools
219233

@@ -340,4 +354,4 @@ gopath-implode:
340354
deploy deploy-services deploy-migrations deploy-tools ci-deploy bundle-deploy \
341355
docker docker-build docker-push ci-docker \
342356
clean clean-bin clean-cover clean-debug clean-deploy clean-all pre-commit \
343-
gopath-implode
357+
gopath-implode go-test

appvalidate/appvalidate.go

Lines changed: 60 additions & 0 deletions
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+
}

appvalidate/assertion.go

Lines changed: 73 additions & 0 deletions
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+
}

appvalidate/attestation.go

Lines changed: 73 additions & 0 deletions
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+
}

appvalidate/base64.go

Lines changed: 9 additions & 0 deletions
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+
}

appvalidate/challenge.go

Lines changed: 44 additions & 0 deletions
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)