From ccfe03c84e2356ae7520dd532216c895aba55331 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Tue, 16 Jul 2024 15:53:00 +0300 Subject: [PATCH] VerifyPassportV2 endpoint --- .../components/schemas/VerifyPassport.yaml | 2 +- ...c@balances@{nullifier}@verifypassport.yaml | 59 +++++++++ go.mod | 2 +- go.sum | 2 + .../service/handlers/verify_passport_v2.go | 123 ++++++++++++++++++ .../service/requests/verify_passport_v2.go | 30 +++++ internal/service/router.go | 6 + resources/model_verify_passport_attributes.go | 2 +- 8 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 docs/spec/paths/integrations@geo-points-svc@v2@public@balances@{nullifier}@verifypassport.yaml create mode 100644 internal/service/handlers/verify_passport_v2.go create mode 100644 internal/service/requests/verify_passport_v2.go diff --git a/docs/spec/components/schemas/VerifyPassport.yaml b/docs/spec/components/schemas/VerifyPassport.yaml index 2dd352e..7d0afdb 100644 --- a/docs/spec/components/schemas/VerifyPassport.yaml +++ b/docs/spec/components/schemas/VerifyPassport.yaml @@ -19,4 +19,4 @@ allOf: format: types.ZKProof description: | Query ZK passport verification proof. - Required for endpoint `/v2/balances/{nullifier}/verifypassport`. + Required for endpoint `/v1/balances/{nullifier}/verifypassport`. diff --git a/docs/spec/paths/integrations@geo-points-svc@v2@public@balances@{nullifier}@verifypassport.yaml b/docs/spec/paths/integrations@geo-points-svc@v2@public@balances@{nullifier}@verifypassport.yaml new file mode 100644 index 0000000..8ba9112 --- /dev/null +++ b/docs/spec/paths/integrations@geo-points-svc@v2@public@balances@{nullifier}@verifypassport.yaml @@ -0,0 +1,59 @@ +post: + tags: + - Points balance + summary: Verify passport V2 + description: | + Verify passport with JWT (verified: true). + JWT with this parameter can be obtained from the auth v1 and v2 service. + One passport can't be verified twice. + operationId: verifyPassportV2 + parameters: + - $ref: '#/components/parameters/pathNullifier' + - in: header + name: Signature + description: Signature of the request + required: true + schema: + type: string + pattern: '^[a-f0-9]{64}$' + requestBody: + required: true + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/VerifyPassport' + responses: + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/EventClaimingState' + 400: + $ref: '#/components/responses/invalidParameter' + 401: + $ref: '#/components/responses/invalidAuth' + 404: + description: Balance not exists. + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Errors' + 409: + description: Passport already verified or event absent for user. + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Errors' + 500: + $ref: '#/components/responses/internalError' diff --git a/go.mod b/go.mod index a625be7..c0d3d2f 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/google/jsonapi v1.0.0 github.com/iden3/go-rapidsnark/types v0.0.3 github.com/labstack/gommon v0.4.0 - github.com/rarimo/geo-auth-svc v0.2.0 + github.com/rarimo/geo-auth-svc v1.1.0 github.com/rarimo/saver-grpc-lib v1.0.0 github.com/rarimo/zkverifier-kit v1.1.1 github.com/rubenv/sql-migrate v1.6.1 diff --git a/go.sum b/go.sum index e6d92b2..ffd1448 100644 --- a/go.sum +++ b/go.sum @@ -2116,6 +2116,8 @@ github.com/rarimo/cosmos-sdk v0.46.7 h1:jU2PiWzc+19SF02cXM0O0puKPeH1C6Q6t2lzJ9s1 github.com/rarimo/cosmos-sdk v0.46.7/go.mod h1:fqKqz39U5IlEFb4nbQ72951myztsDzFKKDtffYJ63nk= github.com/rarimo/geo-auth-svc v0.2.0 h1:yQvcIBNx+Tc1jJdtpWDfyLc0HogU+okA08HEZ55wv5U= github.com/rarimo/geo-auth-svc v0.2.0/go.mod h1:SB4bo1xHYDAsBaQGX2+FoEgD3xxqYmcgr4XTTjy4/OM= +github.com/rarimo/geo-auth-svc v1.1.0 h1:3k1tTWAjtCBsnzlMb3aB+xgsFLEPUSmB3woME+q6tfk= +github.com/rarimo/geo-auth-svc v1.1.0/go.mod h1:JrpCGdT0xtAcWIKgPhxPHf7QCW4h845BXuh6M7NdQFw= github.com/rarimo/saver-grpc-lib v1.0.0 h1:MGUVjYg7unmodYczVsLqlqZNkT4CIgKqdo6aQtL1qdE= github.com/rarimo/saver-grpc-lib v1.0.0/go.mod h1:DpugWK5B7Hi0bdC3MPe/9FD2zCxaRwsyykdwxtF1Zgg= github.com/rarimo/zkverifier-kit v1.1.0-rc.1 h1:xtmrFEl7eLAE6mi7IQYOOMKFdwXC3gbe39fYQdvKVZg= diff --git a/internal/service/handlers/verify_passport_v2.go b/internal/service/handlers/verify_passport_v2.go new file mode 100644 index 0000000..c4b58c8 --- /dev/null +++ b/internal/service/handlers/verify_passport_v2.go @@ -0,0 +1,123 @@ +package handlers + +import ( + "fmt" + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/geo-auth-svc/pkg/auth" + "github.com/rarimo/geo-points-svc/internal/data" + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" + "github.com/rarimo/geo-points-svc/internal/service/requests" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func VerifyPassportV2(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewVerifyPassportV2(r) + if err != nil { + Log(r).WithError(err).Debug("Bad request") + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + var ( + nullifier = req.Data.ID + anonymousID = req.Data.Attributes.AnonymousId + log = Log(r).WithFields(map[string]any{ + "balance.nullifier": nullifier, + "balance.anonymous_id": anonymousID, + }) + + gotSig = r.Header.Get("Signature") + ) + + if !auth.Authenticates(UserClaims(r), auth.UserGrant(nullifier)) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + wantSig, err := SigCalculator(r).PassportVerificationSignature(req.Data.ID, anonymousID) + if err != nil { // must never happen due to preceding validation + Log(r).WithError(err).Error("Failed to calculate HMAC signature") + ape.RenderErr(w, problems.InternalError()) + return + } + + if gotSig != wantSig { + log.Warnf("Passport verification unauthorized access: HMAC signature mismatch: got %s, want %s", gotSig, wantSig) + ape.RenderErr(w, problems.Forbidden()) + return + } + + byNullifier, err := BalancesQ(r).FilterByNullifier(nullifier).FilterDisabled().Get() + if err != nil { + Log(r).WithError(err).Error("Failed to get balance by nullifier") + ape.RenderErr(w, problems.InternalError()) + return + } + + if byNullifier == nil { + Log(r).Debugf("Balance not found: nullifier=%s", nullifier) + ape.RenderErr(w, problems.NotFound()) + return + } + + if byNullifier.SharedHash != nil { + Log(r).Debugf("Already verified: nullifier=%s, AID=%s", nullifier, anonymousID) + ape.RenderErr(w, problems.Conflict()) + return + } + + byAnonymousID, err := BalancesQ(r).FilterByAnonymousID(anonymousID).Get() + if err != nil { + log.WithError(err).Error("Failed to get balance by anonymous ID") + ape.RenderErr(w, problems.InternalError()) + return + } + + if byAnonymousID != nil && byAnonymousID.Nullifier != byNullifier.Nullifier { + Log(r).Debugf("AnonymousID already used: nullifier=%s, AID=%s, AIDBalance=%+v", nullifier, anonymousID, byAnonymousID) + ape.RenderErr(w, problems.Conflict()) + return + } + + // UserClaims(r)[0] will not panic because of authorization validation + sharedHash := UserClaims(r)[0].SharedHash + if sharedHash == nil { + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "shared_hash": fmt.Errorf("not provided in JWT"), + })...) + return + } + + err = EventsQ(r).Transaction(func() error { + err = BalancesQ(r).FilterByNullifier(byNullifier.Nullifier).Update(map[string]any{ + data.ColSharedHash: *sharedHash, + data.ColAnonymousID: anonymousID, + data.ColIsVerified: true, + }) + if err != nil { + return fmt.Errorf("failed to update balance: %w", err) + } + + return doPassportScanUpdates(r, *byNullifier, anonymousID, sharedHash) + }) + if err != nil { + log.WithError(err).Error("Failed to execute transaction") + ape.RenderErr(w, problems.InternalError()) + return + } + + event, err := EventsQ(r).FilterByNullifier(byNullifier.Nullifier). + FilterByType(models.TypePassportScan). + FilterByStatus(data.EventClaimed). + Get() + if err != nil { + log.WithError(err).Error("Failed to get claimed event") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, newEventClaimingStateResponse(req.Data.ID, event != nil)) +} diff --git a/internal/service/requests/verify_passport_v2.go b/internal/service/requests/verify_passport_v2.go new file mode 100644 index 0000000..1843d5d --- /dev/null +++ b/internal/service/requests/verify_passport_v2.go @@ -0,0 +1,30 @@ +package requests + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/go-chi/chi" + val "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/geo-points-svc/resources" +) + +func NewVerifyPassportV2(r *http.Request) (req resources.VerifyPassportRequest, err error) { + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + return req, newDecodeError("body", err) + } + + req.Data.ID = strings.ToLower(req.Data.ID) + + return req, val.Errors{ + "data/id": val.Validate(req.Data.ID, + val.Required, + val.In(strings.ToLower(chi.URLParam(r, "nullifier"))), + val.Match(nullifierRegexp)), + "data/type": val.Validate(req.Data.Type, + val.Required, + val.In(resources.VERIFY_PASSPORT)), + "data/attributes/anonymous_id": val.Validate(req.Data.Attributes.AnonymousId, val.Required, val.Match(hex32bRegexp)), + }.Filter() +} diff --git a/internal/service/router.go b/internal/service/router.go index 5615f2d..7d19242 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -61,6 +61,12 @@ func Run(ctx context.Context, cfg config.Config) { }) }) + r.Route("/integrations/geo-points-svc/v1", func(r chi.Router) { + r.Route("/public", func(r chi.Router) { + r.Post("/balances/{nullifier}/verifypassport", handlers.VerifyPassportV2) + }) + }) + cfg.Log().Info("Service started") ape.Serve(ctx, r, cfg, ape.ServeOpts{}) } diff --git a/resources/model_verify_passport_attributes.go b/resources/model_verify_passport_attributes.go index 05bba01..62f6fdb 100644 --- a/resources/model_verify_passport_attributes.go +++ b/resources/model_verify_passport_attributes.go @@ -9,6 +9,6 @@ import "github.com/iden3/go-rapidsnark/types" type VerifyPassportAttributes struct { // Unique identifier of the passport. AnonymousId string `json:"anonymous_id"` - // Query ZK passport verification proof. Required for endpoint `/v2/balances/{nullifier}/verifypassport`. + // Query ZK passport verification proof. Required for endpoint `/v1/balances/{nullifier}/verifypassport`. Proof *types.ZKProof `json:"proof,omitempty"` }