diff --git a/api/api.yaml b/api/api.yaml index eeeafc590..f27bbce8d 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -1989,6 +1989,111 @@ paths: '500': $ref: '#/components/responses/500' + # Verification + + /v2/identities/{identifier}/verification: + get: + summary: Check Verification Response or Provide Query + operationId: CheckVerification + description: | + Checks if a verification response already exists for the given identifier. + If no response is found, it returns the verification query request to be completed by the user. + tags: + - Verification + parameters: + - name: identifier + in: path + required: true + description: User's DID identifier + schema: + type: string + - $ref: '#/components/parameters/verificationQueryId' + responses: + '200': + description: Verification Response or Query Request + content: + application/json: + schema: + $ref: '#/components/schemas/CheckVerificationResponse' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + '500': + $ref: '#/components/responses/500' + + post: + summary: Create a Verification query + operationId: CreateVerification + description: | + Endpoint to create a verification query. + tags: + - Verification + parameters: + - name: identifier + in: path + required: true + description: Issuer's DID Identifier + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateVerificationQueryRequest' + responses: + '201': + description: Verification query created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CreateVerificationQueryResponse' + '400': + $ref: '#/components/responses/400' + '500': + $ref: '#/components/responses/500' + + /v2/identities/{identifier}/verification/callback: + post: + summary: Submit Verification Response + operationId: SubmitVerificationResponse + description: | + Endpoint to submit a verification response for the given verification query request. + The response will be validated and stored in the verification_responses table. + tags: + - Verification + parameters: + - name: identifier + in: path + required: true + description: User's DID identifier + schema: + type: string + - $ref: '#/components/parameters/verificationQueryId' + requestBody: + required: true + content: + text/plain: + schema: + type: string + example: jwz-token + responses: + '200': + description: Verification response submitted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/VerificationResponseStatus' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + '500': + $ref: '#/components/responses/500' + + + components: securitySchemes: basicAuth: @@ -3164,6 +3269,96 @@ components: name: protocol path: github.com/iden3/iden3comm/v2/protocol + #Verification + CreateVerificationQueryRequest: + type: object + required: + - chain_id + - skip_revocation_check + - scopes + properties: + chain_id: + type: integer + example: 1 + skip_revocation_check: + type: boolean + example: false + scopes: + type: array + items: + type: object + additionalProperties: true + description: A dynamic JSON object representing a scope. + description: An array of dynamic JSON objects for scopes. + + CreateVerificationQueryResponse: + type: object + required: + - verificationQueryId + properties: + verificationQueryId: + type: string + description: The ID of the created verification query. + + VerificationResponse: + type: object + required: + - verification_scope_id + - user_did + - response + - pass + properties: + verification_scope_id: + type: string + description: Scope ID for the verification query. + user_did: + type: string + description: Decentralized identifier of the user. + response: + type: object + description: The response from the user as a JSON object. + pass: + type: boolean + description: Indicates if the verification passed. + + VerificationQueryRequest: + type: object + required: + - verification_query_id + - scopes + properties: + verification_query_id: + type: string + description: The ID of the verification query. + scopes: + type: object + additionalProperties: true + description: "Dynamic JSON object for scopes" + + CheckVerificationResponse: + type: object + description: Response that includes either a verification response or a verification query request. + properties: + verification_response: + $ref: '#/components/schemas/VerificationResponse' + verification_query_request: + $ref: '#/components/schemas/VerificationQueryRequest' + additionalProperties: false + + VerificationResponseStatus: + type: object + required: + - status + - pass + properties: + status: + type: string + enum: [ submitted, validated, error ] + description: The status of the submitted verification response. + pass: + type: boolean + description: Whether the query response passed the check + CreateDisplayMethodRequest: type: object required: @@ -3273,6 +3468,7 @@ components: example: "Iden3ReverseSparseMerkleTreeProof" enum: [ Iden3commRevocationStatusV1.0, Iden3ReverseSparseMerkleTreeProof, Iden3OnchainSparseMerkleTreeProof2023 ] + parameters: credentialStatusType: name: credentialStatusType @@ -3367,6 +3563,17 @@ components: schema: type: string + verificationQueryId: + name: id + in: query + required: true + description: The verification query ID to check for a response + schema: + type: string + x-go-type: uuid.UUID + x-go-type-import: + name: uuid + path: github.com/google/uuid responses: '400': diff --git a/cmd/platform/main.go b/cmd/platform/main.go index 0193638a2..893cc9f59 100644 --- a/cmd/platform/main.go +++ b/cmd/platform/main.go @@ -115,6 +115,7 @@ func main() { sessionRepository := repositories.NewSessionCached(cachex) keyRepository := repositories.NewKey(*storage) paymentsRepo := repositories.NewPayment(*storage) + verificationRepository := repositories.NewVerification(*storage) // services initialization mtService := services.NewIdentityMerkleTrees(mtRepository) @@ -141,7 +142,7 @@ func main() { return } - verificationKeyLoader := &authLoaders.FSKeyLoader{Dir: cfg.Circuit.Path + "/authV2"} + verificationKeyLoader := &authLoaders.FSKeyLoader{Dir: cfg.Circuit.Path + "/verification_keys"} verifier, err := auth.NewVerifier(verificationKeyLoader, networkResolver.GetStateResolvers(), auth.WithDIDResolver(universalDIDResolverHandler)) if err != nil { log.Error(ctx, "failed init verifier", "err", err) @@ -170,6 +171,8 @@ func main() { } accountService := services.NewAccountService(*networkResolver) + verificationService := services.NewVerificationService(networkResolver, cachex, verificationRepository, verifier) + publisherGateway, err := gateways.NewPublisherEthGateway(*networkResolver, keyStore, cfg.PublishingKeyPath) if err != nil { log.Error(ctx, "error creating publish gateway", "err", err) @@ -205,7 +208,7 @@ func main() { api.HandlerWithOptions( api.NewStrictHandlerWithOptions( - api.NewServer(cfg, identityService, accountService, connectionsService, claimsService, qrService, publisher, packageManager, *networkResolver, serverHealth, schemaService, linkService, displayMethodService, keyService, paymentService), + api.NewServer(cfg, identityService, accountService, connectionsService, claimsService, qrService, publisher, packageManager, *networkResolver, serverHealth, schemaService, linkService, displayMethodService, keyService, paymentService, verificationService), middlewares(ctx, cfg.HTTPBasicAuth), api.StrictHTTPServerOptions{ RequestErrorHandlerFunc: errors.RequestErrorHandlerFunc, diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 99caa0726..fb10b8c4d 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -125,6 +125,13 @@ const ( StateTransactionStatusPublished StateTransactionStatus = "published" ) +// Defines values for VerificationResponseStatusStatus. +const ( + Error VerificationResponseStatusStatus = "error" + Submitted VerificationResponseStatusStatus = "submitted" + Validated VerificationResponseStatusStatus = "validated" +) + // Defines values for GetConnectionsParamsSort. const ( GetConnectionsParamsSortCreatedAt GetConnectionsParamsSort = "createdAt" @@ -232,6 +239,12 @@ type BasicMessage struct { Type string `json:"type"` } +// CheckVerificationResponse Response that includes either a verification response or a verification query request. +type CheckVerificationResponse struct { + VerificationQueryRequest *VerificationQueryRequest `json:"verification_query_request,omitempty"` + VerificationResponse *VerificationResponse `json:"verification_response,omitempty"` +} + // ConnectionsPaginated defines model for ConnectionsPaginated. type ConnectionsPaginated struct { Items GetConnectionsResponse `json:"items"` @@ -372,6 +385,21 @@ type CreatePaymentRequestResponse struct { UserDID string `json:"userDID"` } +// CreateVerificationQueryRequest defines model for CreateVerificationQueryRequest. +type CreateVerificationQueryRequest struct { + ChainId int `json:"chain_id"` + + // Scopes An array of dynamic JSON objects for scopes. + Scopes []map[string]interface{} `json:"scopes"` + SkipRevocationCheck bool `json:"skip_revocation_check"` +} + +// CreateVerificationQueryResponse defines model for CreateVerificationQueryResponse. +type CreateVerificationQueryResponse struct { + // VerificationQueryId The ID of the created verification query. + VerificationQueryId string `json:"verificationQueryId"` +} + // Credential defines model for Credential. type Credential struct { Id string `json:"id"` @@ -753,6 +781,42 @@ type UUIDResponse struct { // UUIDString defines model for UUIDString. type UUIDString = string +// VerificationQueryRequest defines model for VerificationQueryRequest. +type VerificationQueryRequest struct { + // Scopes Dynamic JSON object for scopes + Scopes map[string]interface{} `json:"scopes"` + + // VerificationQueryId The ID of the verification query. + VerificationQueryId string `json:"verification_query_id"` +} + +// VerificationResponse defines model for VerificationResponse. +type VerificationResponse struct { + // Pass Indicates if the verification passed. + Pass bool `json:"pass"` + + // Response The response from the user as a JSON object. + Response map[string]interface{} `json:"response"` + + // UserDid Decentralized identifier of the user. + UserDid string `json:"user_did"` + + // VerificationScopeId Scope ID for the verification query. + VerificationScopeId string `json:"verification_scope_id"` +} + +// VerificationResponseStatus defines model for VerificationResponseStatus. +type VerificationResponseStatus struct { + // Pass Whether the query response passed the check + Pass bool `json:"pass"` + + // Status The status of the submitted verification response. + Status VerificationResponseStatusStatus `json:"status"` +} + +// VerificationResponseStatusStatus The status of the submitted verification response. +type VerificationResponseStatusStatus string + // Id defines model for id. type Id = uuid.UUID @@ -777,6 +841,9 @@ type PathNonce = int64 // SessionID defines model for sessionID. type SessionID = uuid.UUID +// VerificationQueryId defines model for verificationQueryId. +type VerificationQueryId = uuid.UUID + // N400 defines model for 400. type N400 = GenericErrorMessage @@ -992,6 +1059,21 @@ type GetStateTransactionsParamsFilter string // GetStateTransactionsParamsSort defines parameters for GetStateTransactions. type GetStateTransactionsParamsSort string +// CheckVerificationParams defines parameters for CheckVerification. +type CheckVerificationParams struct { + // Id The verification query ID to check for a response + Id VerificationQueryId `form:"id" json:"id"` +} + +// SubmitVerificationResponseTextBody defines parameters for SubmitVerificationResponse. +type SubmitVerificationResponseTextBody = string + +// SubmitVerificationResponseParams defines parameters for SubmitVerificationResponse. +type SubmitVerificationResponseParams struct { + // Id The verification query ID to check for a response + Id VerificationQueryId `form:"id" json:"id"` +} + // GetQrFromStoreParams defines parameters for GetQrFromStore. type GetQrFromStoreParams struct { Id *uuid.UUID `form:"id,omitempty" json:"id,omitempty"` @@ -1069,6 +1151,12 @@ type ImportSchemaJSONRequestBody = ImportSchemaRequest // UpdateSchemaJSONRequestBody defines body for UpdateSchema for application/json ContentType. type UpdateSchemaJSONRequestBody UpdateSchemaJSONBody +// CreateVerificationJSONRequestBody defines body for CreateVerification for application/json ContentType. +type CreateVerificationJSONRequestBody = CreateVerificationQueryRequest + +// SubmitVerificationResponseTextRequestBody defines body for SubmitVerificationResponse for text/plain ContentType. +type SubmitVerificationResponseTextRequestBody = SubmitVerificationResponseTextBody + // ServerInterface represents all server handlers. type ServerInterface interface { // Healthcheck @@ -1245,6 +1333,15 @@ type ServerInterface interface { // Get Identity State Transactions // (GET /v2/identities/{identifier}/state/transactions) GetStateTransactions(w http.ResponseWriter, r *http.Request, identifier PathIdentifier, params GetStateTransactionsParams) + // Check Verification Response or Provide Query + // (GET /v2/identities/{identifier}/verification) + CheckVerification(w http.ResponseWriter, r *http.Request, identifier string, params CheckVerificationParams) + // Create a Verification query + // (POST /v2/identities/{identifier}/verification) + CreateVerification(w http.ResponseWriter, r *http.Request, identifier string) + // Submit Verification Response + // (POST /v2/identities/{identifier}/verification/callback) + SubmitVerificationResponse(w http.ResponseWriter, r *http.Request, identifier string, params SubmitVerificationResponseParams) // Payments Configuration // (GET /v2/payment/settings) GetPaymentSettings(w http.ResponseWriter, r *http.Request) @@ -1611,6 +1708,24 @@ func (_ Unimplemented) GetStateTransactions(w http.ResponseWriter, r *http.Reque w.WriteHeader(http.StatusNotImplemented) } +// Check Verification Response or Provide Query +// (GET /v2/identities/{identifier}/verification) +func (_ Unimplemented) CheckVerification(w http.ResponseWriter, r *http.Request, identifier string, params CheckVerificationParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Create a Verification query +// (POST /v2/identities/{identifier}/verification) +func (_ Unimplemented) CreateVerification(w http.ResponseWriter, r *http.Request, identifier string) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Submit Verification Response +// (POST /v2/identities/{identifier}/verification/callback) +func (_ Unimplemented) SubmitVerificationResponse(w http.ResponseWriter, r *http.Request, identifier string, params SubmitVerificationResponseParams) { + w.WriteHeader(http.StatusNotImplemented) +} + // Payments Configuration // (GET /v2/payment/settings) func (_ Unimplemented) GetPaymentSettings(w http.ResponseWriter, r *http.Request) { @@ -3852,6 +3967,117 @@ func (siw *ServerInterfaceWrapper) GetStateTransactions(w http.ResponseWriter, r handler.ServeHTTP(w, r) } +// CheckVerification operation middleware +func (siw *ServerInterfaceWrapper) CheckVerification(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "identifier" ------------- + var identifier string + + err = runtime.BindStyledParameterWithOptions("simple", "identifier", chi.URLParam(r, "identifier"), &identifier, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "identifier", Err: err}) + return + } + + // Parameter object where we will unmarshal all parameters from the context + var params CheckVerificationParams + + // ------------- Required query parameter "id" ------------- + + if paramValue := r.URL.Query().Get("id"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "id"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "id", r.URL.Query(), ¶ms.Id) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CheckVerification(w, r, identifier, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// CreateVerification operation middleware +func (siw *ServerInterfaceWrapper) CreateVerification(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "identifier" ------------- + var identifier string + + err = runtime.BindStyledParameterWithOptions("simple", "identifier", chi.URLParam(r, "identifier"), &identifier, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "identifier", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateVerification(w, r, identifier) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// SubmitVerificationResponse operation middleware +func (siw *ServerInterfaceWrapper) SubmitVerificationResponse(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "identifier" ------------- + var identifier string + + err = runtime.BindStyledParameterWithOptions("simple", "identifier", chi.URLParam(r, "identifier"), &identifier, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "identifier", Err: err}) + return + } + + // Parameter object where we will unmarshal all parameters from the context + var params SubmitVerificationResponseParams + + // ------------- Required query parameter "id" ------------- + + if paramValue := r.URL.Query().Get("id"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "id"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "id", r.URL.Query(), ¶ms.Id) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.SubmitVerificationResponse(w, r, identifier, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetPaymentSettings operation middleware func (siw *ServerInterfaceWrapper) GetPaymentSettings(w http.ResponseWriter, r *http.Request) { @@ -4250,6 +4476,15 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/v2/identities/{identifier}/state/transactions", wrapper.GetStateTransactions) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/v2/identities/{identifier}/verification", wrapper.CheckVerification) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/v2/identities/{identifier}/verification", wrapper.CreateVerification) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/v2/identities/{identifier}/verification/callback", wrapper.SubmitVerificationResponse) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/v2/payment/settings", wrapper.GetPaymentSettings) }) @@ -6759,6 +6994,133 @@ func (response GetStateTransactions500JSONResponse) VisitGetStateTransactionsRes return json.NewEncoder(w).Encode(response) } +type CheckVerificationRequestObject struct { + Identifier string `json:"identifier"` + Params CheckVerificationParams +} + +type CheckVerificationResponseObject interface { + VisitCheckVerificationResponse(w http.ResponseWriter) error +} + +type CheckVerification200JSONResponse CheckVerificationResponse + +func (response CheckVerification200JSONResponse) VisitCheckVerificationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type CheckVerification400JSONResponse struct{ N400JSONResponse } + +func (response CheckVerification400JSONResponse) VisitCheckVerificationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CheckVerification404JSONResponse struct{ N404JSONResponse } + +func (response CheckVerification404JSONResponse) VisitCheckVerificationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type CheckVerification500JSONResponse struct{ N500JSONResponse } + +func (response CheckVerification500JSONResponse) VisitCheckVerificationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type CreateVerificationRequestObject struct { + Identifier string `json:"identifier"` + Body *CreateVerificationJSONRequestBody +} + +type CreateVerificationResponseObject interface { + VisitCreateVerificationResponse(w http.ResponseWriter) error +} + +type CreateVerification201JSONResponse CreateVerificationQueryResponse + +func (response CreateVerification201JSONResponse) VisitCreateVerificationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type CreateVerification400JSONResponse struct{ N400JSONResponse } + +func (response CreateVerification400JSONResponse) VisitCreateVerificationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CreateVerification500JSONResponse struct{ N500JSONResponse } + +func (response CreateVerification500JSONResponse) VisitCreateVerificationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type SubmitVerificationResponseRequestObject struct { + Identifier string `json:"identifier"` + Params SubmitVerificationResponseParams + Body *SubmitVerificationResponseTextRequestBody +} + +type SubmitVerificationResponseResponseObject interface { + VisitSubmitVerificationResponseResponse(w http.ResponseWriter) error +} + +type SubmitVerificationResponse200JSONResponse VerificationResponseStatus + +func (response SubmitVerificationResponse200JSONResponse) VisitSubmitVerificationResponseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type SubmitVerificationResponse400JSONResponse struct{ N400JSONResponse } + +func (response SubmitVerificationResponse400JSONResponse) VisitSubmitVerificationResponseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type SubmitVerificationResponse404JSONResponse struct{ N404JSONResponse } + +func (response SubmitVerificationResponse404JSONResponse) VisitSubmitVerificationResponseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type SubmitVerificationResponse500JSONResponse struct{ N500JSONResponse } + +func (response SubmitVerificationResponse500JSONResponse) VisitSubmitVerificationResponseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type GetPaymentSettingsRequestObject struct { } @@ -7092,6 +7454,15 @@ type StrictServerInterface interface { // Get Identity State Transactions // (GET /v2/identities/{identifier}/state/transactions) GetStateTransactions(ctx context.Context, request GetStateTransactionsRequestObject) (GetStateTransactionsResponseObject, error) + // Check Verification Response or Provide Query + // (GET /v2/identities/{identifier}/verification) + CheckVerification(ctx context.Context, request CheckVerificationRequestObject) (CheckVerificationResponseObject, error) + // Create a Verification query + // (POST /v2/identities/{identifier}/verification) + CreateVerification(ctx context.Context, request CreateVerificationRequestObject) (CreateVerificationResponseObject, error) + // Submit Verification Response + // (POST /v2/identities/{identifier}/verification/callback) + SubmitVerificationResponse(ctx context.Context, request SubmitVerificationResponseRequestObject) (SubmitVerificationResponseResponseObject, error) // Payments Configuration // (GET /v2/payment/settings) GetPaymentSettings(ctx context.Context, request GetPaymentSettingsRequestObject) (GetPaymentSettingsResponseObject, error) @@ -8814,6 +9185,101 @@ func (sh *strictHandler) GetStateTransactions(w http.ResponseWriter, r *http.Req } } +// CheckVerification operation middleware +func (sh *strictHandler) CheckVerification(w http.ResponseWriter, r *http.Request, identifier string, params CheckVerificationParams) { + var request CheckVerificationRequestObject + + request.Identifier = identifier + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.CheckVerification(ctx, request.(CheckVerificationRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CheckVerification") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(CheckVerificationResponseObject); ok { + if err := validResponse.VisitCheckVerificationResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// CreateVerification operation middleware +func (sh *strictHandler) CreateVerification(w http.ResponseWriter, r *http.Request, identifier string) { + var request CreateVerificationRequestObject + + request.Identifier = identifier + + var body CreateVerificationJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.CreateVerification(ctx, request.(CreateVerificationRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateVerification") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(CreateVerificationResponseObject); ok { + if err := validResponse.VisitCreateVerificationResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// SubmitVerificationResponse operation middleware +func (sh *strictHandler) SubmitVerificationResponse(w http.ResponseWriter, r *http.Request, identifier string, params SubmitVerificationResponseParams) { + var request SubmitVerificationResponseRequestObject + + request.Identifier = identifier + request.Params = params + + data, err := io.ReadAll(r.Body) + if err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) + return + } + body := SubmitVerificationResponseTextRequestBody(data) + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.SubmitVerificationResponse(ctx, request.(SubmitVerificationResponseRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "SubmitVerificationResponse") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(SubmitVerificationResponseResponseObject); ok { + if err := validResponse.VisitSubmitVerificationResponseResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetPaymentSettings operation middleware func (sh *strictHandler) GetPaymentSettings(w http.ResponseWriter, r *http.Request) { var request GetPaymentSettingsRequestObject diff --git a/internal/api/main_test.go b/internal/api/main_test.go index 84f8adc22..f7c4f89e1 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -8,6 +8,8 @@ import ( "github.com/go-chi/chi/v5" "github.com/hashicorp/vault/api" + auth "github.com/iden3/go-iden3-auth/v2" + authLoaders "github.com/iden3/go-iden3-auth/v2/loaders" "github.com/iden3/go-iden3-core/v2/w3c" "github.com/iden3/iden3comm/v2" "github.com/iden3/iden3comm/v2/packers" @@ -27,6 +29,7 @@ import ( "github.com/polygonid/sh-id-platform/internal/loader" "github.com/polygonid/sh-id-platform/internal/log" "github.com/polygonid/sh-id-platform/internal/network" + "github.com/polygonid/sh-id-platform/internal/packagemanager" "github.com/polygonid/sh-id-platform/internal/payments" "github.com/polygonid/sh-id-platform/internal/providers" "github.com/polygonid/sh-id-platform/internal/pubsub" @@ -122,6 +125,7 @@ func TestMain(m *testing.M) { cfg.ServerUrl = "https://testing.env" cfg.Ethereum = cfgForTesting.Ethereum cfg.UniversalLinks = config.UniversalLinks{BaseUrl: "https://testing.env"} + cfg.Circuit.Path = "../../pkg/credentials/circuits" schemaLoader = loader.NewDocumentLoader(ipfsGatewayURL, false) m.Run() } @@ -239,6 +243,7 @@ type repos struct { revocation ports.RevocationRepository displayMethod ports.DisplayMethodRepository keyRepository ports.KeyRepository + verification ports.VerificationRepository } type servicex struct { @@ -282,6 +287,7 @@ func newTestServer(t *testing.T, st *db.Storage) *testServer { revocation: repositories.NewRevocation(), displayMethod: repositories.NewDisplayMethod(*st), keyRepository: repositories.NewKey(*st), + verification: repositories.NewVerification(*st), } pubSub := pubsub.NewMock() @@ -367,7 +373,19 @@ func newTestServer(t *testing.T, st *db.Storage) *testServer { accountService := services.NewAccountService(*networkResolver) linkService := services.NewLinkService(storage, claimsService, qrService, repos.claims, repos.links, repos.schemas, schemaLoader, repos.sessions, pubSub, identityService, *networkResolver, cfg.UniversalLinks) keyService := services.NewKey(keyStore, claimsService, repos.keyRepository) - server := NewServer(&cfg, identityService, accountService, connectionService, claimsService, qrService, NewPublisherMock(), NewPackageManagerMock(), *networkResolver, nil, schemaService, linkService, displayMethodService, keyService, paymentService) + + universalDIDResolverUrl := auth.UniversalResolverURL + if cfg.UniversalDIDResolver.UniversalResolverURL != nil && *cfg.UniversalDIDResolver.UniversalResolverURL != "" { + universalDIDResolverUrl = *cfg.UniversalDIDResolver.UniversalResolverURL + } + universalDIDResolverHandler := packagemanager.NewUniversalDIDResolverHandler(universalDIDResolverUrl) + verificationKeyLoader := &authLoaders.FSKeyLoader{Dir: cfg.Circuit.Path + "/authV2"} + verifier, err := auth.NewVerifier(verificationKeyLoader, networkResolver.GetStateResolvers(), auth.WithDIDResolver(universalDIDResolverHandler)) + require.NoError(t, err) + + verificationService := services.NewVerificationService(networkResolver, cachex, repos.verification, verifier) + + server := NewServer(&cfg, identityService, accountService, connectionService, claimsService, qrService, NewPublisherMock(), NewPackageManagerMock(), *networkResolver, nil, schemaService, linkService, displayMethodService, keyService, paymentService, verificationService) return &testServer{ Server: server, diff --git a/internal/api/server.go b/internal/api/server.go index ebc41877c..2b41321eb 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -32,10 +32,11 @@ type Server struct { paymentService ports.PaymentService displayMethodService ports.DisplayMethodService keyService ports.KeyService + verificationService ports.VerificationService } // NewServer is a Server constructor -func NewServer(cfg *config.Configuration, identityService ports.IdentityService, accountService ports.AccountService, connectionsService ports.ConnectionService, claimsService ports.ClaimService, qrService ports.QrStoreService, publisherGateway ports.Publisher, packageManager *iden3comm.PackageManager, networkResolver network.Resolver, health *health.Status, schemaService ports.SchemaService, linkService ports.LinkService, displayMethodService ports.DisplayMethodService, keyService ports.KeyService, paymentService ports.PaymentService) *Server { +func NewServer(cfg *config.Configuration, identityService ports.IdentityService, accountService ports.AccountService, connectionsService ports.ConnectionService, claimsService ports.ClaimService, qrService ports.QrStoreService, publisherGateway ports.Publisher, packageManager *iden3comm.PackageManager, networkResolver network.Resolver, health *health.Status, schemaService ports.SchemaService, linkService ports.LinkService, displayMethodService ports.DisplayMethodService, keyService ports.KeyService, paymentService ports.PaymentService, verificationService ports.VerificationService) *Server { return &Server{ cfg: cfg, accountService: accountService, @@ -52,6 +53,7 @@ func NewServer(cfg *config.Configuration, identityService ports.IdentityService, displayMethodService: displayMethodService, keyService: keyService, paymentService: paymentService, + verificationService: verificationService, } } diff --git a/internal/api/verification.go b/internal/api/verification.go new file mode 100644 index 000000000..a1ed5061d --- /dev/null +++ b/internal/api/verification.go @@ -0,0 +1,146 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/iden3/go-iden3-core/v2/w3c" + + "github.com/polygonid/sh-id-platform/internal/log" +) + +// CreateVerification a VerificationQuery using verificationService +func (s *Server) CreateVerification(ctx context.Context, request CreateVerificationRequestObject) (CreateVerificationResponseObject, error) { + issuerDID, err := w3c.ParseDID(request.Identifier) + if err != nil { + log.Error(ctx, "parsing issuer did", "err", err, "did", request.Identifier) + return CreateVerification400JSONResponse{N400JSONResponse{Message: "invalid issuer did"}}, nil + } + + verificationQuery, err := s.verificationService.CreateVerificationQuery(ctx, *issuerDID, request.Body.ChainId, request.Body.SkipRevocationCheck, request.Body.Scopes, s.cfg.ServerUrl) + if err != nil { + log.Error(ctx, "creating verification query", "err", err) + return CreateVerification500JSONResponse{N500JSONResponse{Message: err.Error()}}, nil + } + + return CreateVerification201JSONResponse{ + VerificationQueryId: verificationQuery.ID.String(), + }, nil +} + +// CheckVerification returns CheckVerificationResponse or Provided Query +func (s *Server) CheckVerification(ctx context.Context, request CheckVerificationRequestObject) (CheckVerificationResponseObject, error) { + // Parse and validate issuer DID + issuerDID, err := w3c.ParseDID(request.Identifier) + if err != nil { + log.Error(ctx, "parsing issuer did", "err", err, "did", request.Identifier) + return CheckVerification400JSONResponse{N400JSONResponse{Message: "invalid issuer did"}}, nil + } + + verificationQueryID := request.Params.Id + + // Use the VerificationService to check for existing response or query + response, query, err := s.verificationService.GetVerificationStatus(ctx, *issuerDID, verificationQueryID) + if err != nil { + // if error is not found, return 404 + if err.Error() == "verification query not found" { + log.Error(ctx, "checking verification, not found", "err", err, "issuerDID", issuerDID, "id", verificationQueryID) + return CheckVerification404JSONResponse{N404JSONResponse{Message: "Verification query not found"}}, nil + } else { + log.Error(ctx, "checking verification", "err", err, "issuerDID", issuerDID, "id", verificationQueryID) + return CheckVerification500JSONResponse{N500JSONResponse{Message: err.Error()}}, nil + } + } + + // Check if a response exists + if response != nil { + var jsonResponse map[string]interface{} + + // Marshal the interface{} value to JSON bytes + responseBytes, err := json.Marshal(response.Response.Get()) + if err != nil { + return CheckVerification500JSONResponse{ + N500JSONResponse{Message: fmt.Sprintf("failed to marshal response: %v", err)}, + }, nil + } + + // Unmarshal the JSON bytes into map[string]interface{} + if err := json.Unmarshal(responseBytes, &jsonResponse); err != nil { + return CheckVerification500JSONResponse{ + N500JSONResponse{Message: fmt.Sprintf("failed to unmarshal response: %v", err)}, + }, nil + } + + return CheckVerification200JSONResponse{ + VerificationResponse: &VerificationResponse{ + VerificationScopeId: response.VerificationQueryID.String(), + UserDid: response.UserDID, + Response: jsonResponse, // Safely populated JSON object + Pass: response.Pass, + }, + }, nil + } + + // Check if a query exists + if query != nil { + var scopeJSON map[string]interface{} + + // Marshal the interface{} value to JSON bytes + scopeBytes, err := json.Marshal(query.Scope.Get()) + if err != nil { + return CheckVerification500JSONResponse{ + N500JSONResponse{Message: fmt.Sprintf("failed to marshal scope: %v", err)}, + }, nil + } + + // Unmarshal the JSON bytes into map[string]interface{} + if err := json.Unmarshal(scopeBytes, &scopeJSON); err != nil { + return CheckVerification500JSONResponse{ + N500JSONResponse{Message: fmt.Sprintf("failed to unmarshal scope: %v", err)}, + }, nil + } + + return CheckVerification200JSONResponse{ + VerificationQueryRequest: &VerificationQueryRequest{ + VerificationQueryId: query.ID.String(), + Scopes: scopeJSON, // Safely populated JSON object + }, + }, nil + } + + // Return 404 if neither response nor query exists + return CheckVerification404JSONResponse{N404JSONResponse{Message: "Verification query not found"}}, nil +} + +// SubmitVerificationResponse returns a VerificationResponse +func (s *Server) SubmitVerificationResponse(ctx context.Context, request SubmitVerificationResponseRequestObject) (SubmitVerificationResponseResponseObject, error) { + // Unmarshal user DID and response data + issuerDID, err := w3c.ParseDID(request.Identifier) + if err != nil { + log.Error(ctx, "submitting verification query response.. Parsing did", "err", err, "issuerDID", issuerDID) + return SubmitVerificationResponse400JSONResponse{ + N400JSONResponse{ + Message: "invalid did", + }, + }, err + } + token := request.Body + + // Submit the verification response using VerificationService + response, err := s.verificationService.SubmitVerificationResponse(ctx, request.Params.Id, *issuerDID, *token, s.cfg.ServerUrl) + if err != nil { + log.Error(ctx, "failed to submit verification response", "err", err) + return SubmitVerificationResponse500JSONResponse{ + N500JSONResponse{ + Message: "failed to submit verification response", + }, + }, nil + } + + // Construct and return the response status + return SubmitVerificationResponse200JSONResponse{ + Status: Submitted, + Pass: response.Pass, + }, nil +} diff --git a/internal/api/verification_test.go b/internal/api/verification_test.go new file mode 100644 index 000000000..69075b2c1 --- /dev/null +++ b/internal/api/verification_test.go @@ -0,0 +1,215 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/uuid" + core "github.com/iden3/go-iden3-core/v2" + "github.com/jackc/pgtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/polygonid/sh-id-platform/internal/common" + "github.com/polygonid/sh-id-platform/internal/core/domain" + "github.com/polygonid/sh-id-platform/internal/db/tests" + "github.com/polygonid/sh-id-platform/internal/repositories" +) + +func TestServer_CreateVerification(t *testing.T) { + server := newTestServer(t, nil) + handler := getHandler(context.Background(), server) + + type expected struct { + httpCode int + message *string + } + type testConfig struct { + name string + auth func() (string, string) + identifier string + body CreateVerificationQueryRequest + expected expected + } + + idFromString, err := core.IDFromString("x2Uw18ATvY7mEsgfrrDipBmQQdPWAao4NmF56wGvp") + require.NoError(t, err) + + did, err := core.ParseDIDFromID(idFromString) + require.NoError(t, err) + + identity := &domain.Identity{ + Identifier: did.String(), + } + fixture := repositories.NewFixture(storage) + fixture.CreateIdentity(t, identity) + + for _, tc := range []testConfig{ + { + name: "Valid request", + auth: authOk, + identifier: did.String(), + body: CreateVerificationQueryRequest{ + ChainId: 137, + SkipRevocationCheck: false, + Scopes: []map[string]interface{}{ + { + "circuitId": "credentialAtomicQuerySigV2", + "id": 1, + "params": map[string]interface{}{ + "nullifierSessionID": "123456789", + }, + "query": nil, + }, + }, + }, + expected: expected{ + httpCode: http.StatusCreated, + }, + }, + { + name: "Invalid identifier", + auth: authOk, + identifier: "invalid-identifier", + expected: expected{ + httpCode: http.StatusBadRequest, + message: common.ToPointer("invalid issuer did"), + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + rr := httptest.NewRecorder() + url := fmt.Sprintf("/v2/identities/%s/verification", tc.identifier) + req, err := http.NewRequest("POST", url, tests.JSONBody(t, tc.body)) + req.SetBasicAuth(tc.auth()) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + + require.Equal(t, tc.expected.httpCode, rr.Code) + if tc.expected.httpCode == http.StatusCreated { + var response CreateVerificationQueryResponse + assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response)) + assert.NotNil(t, response.VerificationQueryId) + } else if tc.expected.httpCode == http.StatusBadRequest { + var response CreateVerification400JSONResponse + assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response)) + assert.Equal(t, *tc.expected.message, response.Message) + } + }) + } +} + +func TestServer_CheckVerification(t *testing.T) { + server := newTestServer(t, nil) + handler := getHandler(context.Background(), server) + + idFromString, err := core.IDFromString("x2Uw18ATvY7mEsgfrrDipBmQQdPWAao4NmF56wGvp") + require.NoError(t, err) + + did, err := core.ParseDIDFromID(idFromString) + require.NoError(t, err) + + // Mock data setup + queryWithoutResponse := domain.VerificationQuery{ + ID: uuid.New(), + IssuerDID: did.String(), + ChainID: 137, + SkipCheckRevocation: false, + // CreatedAt: time.Now(), + } + queryWithResponse := domain.VerificationQuery{ + ID: uuid.New(), + IssuerDID: did.String(), + ChainID: 137, + SkipCheckRevocation: false, + // CreatedAt: time.Now(), + } + + responseJson := pgtype.JSONB{ + Bytes: []byte(`{"foo":"bar"}`), + Status: pgtype.Present, + } + + response := domain.VerificationResponse{ + ID: uuid.New(), + VerificationQueryID: queryWithResponse.ID, + UserDID: did.String(), + Response: &responseJson, + Pass: true, + // CreatedAt: time.Now(), + } + + fixture := repositories.NewFixture(storage) + fixture.CreateVerificationQuery(t, *did, queryWithoutResponse) + fixture.CreateVerificationQuery(t, *did, queryWithResponse) + fixture.CreateVerificationResponse(t, queryWithResponse.ID, response) + + type expected struct { + httpCode int + responseType string + } + type testConfig struct { + name string + auth func() (string, string) + identifier string + queryID string + expected expected + } + + for _, tc := range []testConfig{ + { + name: "Verification response exists", + auth: authOk, + identifier: did.String(), + queryID: queryWithResponse.ID.String(), + expected: expected{ + httpCode: http.StatusOK, + responseType: "VerificationResponse", + }, + }, + { + name: "Verification query exists without response", + auth: authOk, + identifier: did.String(), + queryID: queryWithoutResponse.ID.String(), + expected: expected{ + httpCode: http.StatusOK, + responseType: "VerificationQueryRequest", + }, + }, + { + name: "Query not found", + auth: authOk, + identifier: did.String(), + queryID: uuid.New().String(), + expected: expected{ + httpCode: http.StatusNotFound, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + rr := httptest.NewRecorder() + url := fmt.Sprintf("/v2/identities/%s/verification?id=%s", tc.identifier, tc.queryID) + req, err := http.NewRequest("GET", url, nil) + req.SetBasicAuth(tc.auth()) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + require.Equal(t, tc.expected.httpCode, rr.Code) + if tc.expected.httpCode == http.StatusOK { + var response CheckVerificationResponse + assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response)) + if tc.expected.responseType == "VerificationResponse" { + assert.NotNil(t, response.VerificationResponse) + assert.Nil(t, response.VerificationQueryRequest) + } else if tc.expected.responseType == "VerificationQueryRequest" { + assert.NotNil(t, response.VerificationQueryRequest) + assert.Nil(t, response.VerificationResponse) + } + } + }) + } +} diff --git a/internal/core/domain/verification.go b/internal/core/domain/verification.go new file mode 100644 index 000000000..ba883e179 --- /dev/null +++ b/internal/core/domain/verification.go @@ -0,0 +1,28 @@ +package domain + +import ( + "time" + + "github.com/google/uuid" + "github.com/jackc/pgtype" +) + +// VerificationQuery holds the verification data +type VerificationQuery struct { + ID uuid.UUID + IssuerDID string + ChainID int + SkipCheckRevocation bool + Scope *pgtype.JSONB `json:"scopes"` + CreatedAt time.Time +} + +// VerificationResponse holds the verification response data +type VerificationResponse struct { + ID uuid.UUID + VerificationQueryID uuid.UUID + UserDID string + Response *pgtype.JSONB `json:"response"` + Pass bool + CreatedAt time.Time +} diff --git a/internal/core/ports/verification_repository.go b/internal/core/ports/verification_repository.go new file mode 100644 index 000000000..6dc291edc --- /dev/null +++ b/internal/core/ports/verification_repository.go @@ -0,0 +1,19 @@ +package ports + +import ( + "context" + + "github.com/google/uuid" + "github.com/iden3/go-iden3-core/v2/w3c" + + "github.com/polygonid/sh-id-platform/internal/core/domain" +) + +// VerificationRepository is a repository for verification queries +type VerificationRepository interface { + Save(ctx context.Context, issuerID w3c.DID, query domain.VerificationQuery) (uuid.UUID, error) + Get(ctx context.Context, issuerID w3c.DID, id uuid.UUID) (*domain.VerificationQuery, error) + GetAll(ctx context.Context, issuerID w3c.DID) ([]domain.VerificationQuery, error) + AddResponse(ctx context.Context, queryID uuid.UUID, response domain.VerificationResponse) (uuid.UUID, error) + GetVerificationResponse(ctx context.Context, queryID uuid.UUID) (*domain.VerificationResponse, error) +} diff --git a/internal/core/ports/verification_service.go b/internal/core/ports/verification_service.go new file mode 100644 index 000000000..698f75192 --- /dev/null +++ b/internal/core/ports/verification_service.go @@ -0,0 +1,17 @@ +package ports + +import ( + "context" + + "github.com/google/uuid" + "github.com/iden3/go-iden3-core/v2/w3c" + + "github.com/polygonid/sh-id-platform/internal/core/domain" +) + +// VerificationService interface +type VerificationService interface { + CreateVerificationQuery(ctx context.Context, issuerID w3c.DID, chainID int, skipCheckRevocation bool, scopes []map[string]interface{}, serverURL string) (*domain.VerificationQuery, error) + GetVerificationStatus(ctx context.Context, issuerID w3c.DID, verificationQueryID uuid.UUID) (*domain.VerificationResponse, *domain.VerificationQuery, error) + SubmitVerificationResponse(ctx context.Context, verificationQueryID uuid.UUID, issuerID w3c.DID, token string, serverURL string) (*domain.VerificationResponse, error) +} diff --git a/internal/core/services/verification.go b/internal/core/services/verification.go new file mode 100644 index 000000000..53aaee46e --- /dev/null +++ b/internal/core/services/verification.go @@ -0,0 +1,235 @@ +package services + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "time" + + "github.com/google/uuid" + auth "github.com/iden3/go-iden3-auth/v2" + "github.com/iden3/go-iden3-auth/v2/pubsignals" + "github.com/iden3/go-iden3-core/v2/w3c" + protocol "github.com/iden3/iden3comm/v2/protocol" + "github.com/jackc/pgtype" + + "github.com/polygonid/sh-id-platform/internal/cache" + "github.com/polygonid/sh-id-platform/internal/core/domain" + "github.com/polygonid/sh-id-platform/internal/core/ports" + "github.com/polygonid/sh-id-platform/internal/log" + "github.com/polygonid/sh-id-platform/internal/network" + "github.com/polygonid/sh-id-platform/internal/repositories" +) + +// Scope is a property of VerificationQuery, and it's required for the authRequest +type scope struct { + CircuitId string `json:"circuitId"` + Id uint32 `json:"id"` + Params json.RawMessage `json:"params,omitempty"` + Query json.RawMessage `json:"query"` +} + +const ( + stateTransitionDelay = time.Minute * 5 + defaultReason = "for testing purposes" + defaultBigIntBase = 10 + defaultExpiration time.Duration = 0 +) + +var errVerificationKeyNotFound = errors.New("authRequest not found in the cache") + +// VerificationService can verify responses to verification queries +type VerificationService struct { + networkResolver *network.Resolver + store cache.Cache + repo ports.VerificationRepository + verifier *auth.Verifier +} + +// NewVerificationService creates a new instance of VerificationService +func NewVerificationService(networkResolver *network.Resolver, store cache.Cache, repo ports.VerificationRepository, verifier *auth.Verifier) *VerificationService { + return &VerificationService{ + networkResolver: networkResolver, + store: store, + verifier: verifier, + repo: repo, + } +} + +// CreateVerificationQuery creates and saves a new verification query in the database. +func (vs *VerificationService) CreateVerificationQuery(ctx context.Context, issuerID w3c.DID, chainID int, skipCheckRevocation bool, scopes []map[string]interface{}, serverURL string) (*domain.VerificationQuery, error) { + var scopeJSON pgtype.JSONB + err := scopeJSON.Set(scopes) + if err != nil { + return nil, fmt.Errorf("failed to set scope JSON: %w", err) + } + + verificationQuery := domain.VerificationQuery{ + ID: uuid.New(), + IssuerDID: issuerID.String(), + ChainID: chainID, + SkipCheckRevocation: skipCheckRevocation, + Scope: &scopeJSON, + CreatedAt: time.Now(), + } + + queryID, err := vs.repo.Save(ctx, issuerID, verificationQuery) + if err != nil { + return nil, fmt.Errorf("failed to save verification query: %w", err) + } + + verificationQuery.ID = queryID + + authRequest, err := vs.getAuthRequestOffChain(ctx, &verificationQuery, serverURL) + if err != nil { + return nil, fmt.Errorf("failed to generate auth request: %w", err) + } + + if err := vs.store.Set(ctx, vs.key(queryID), authRequest, defaultExpiration); err != nil { + log.Error(ctx, "error storing verification query request", "id", queryID.String(), "error", err) + return nil, err + } + + return &verificationQuery, nil +} + +// GetVerificationStatus checks if a verification response already exists for a given verification query ID and userDID. +// If no response exists, it returns the verification query. +func (vs *VerificationService) GetVerificationStatus(ctx context.Context, issuerID w3c.DID, verificationQueryID uuid.UUID) (*domain.VerificationResponse, *domain.VerificationQuery, error) { + query, err := vs.repo.Get(ctx, issuerID, verificationQueryID) + if err != nil { + if err == repositories.ErrVerificationQueryNotFound { + return nil, nil, err + } + return nil, nil, fmt.Errorf("failed to get verification query: %w", err) + } + + response, err := vs.repo.GetVerificationResponse(ctx, verificationQueryID) + if err == nil && response != nil { + return response, nil, nil + } + + return nil, query, nil +} + +// SubmitVerificationResponse checks if a verification response passes a verify check and saves result +func (vs *VerificationService) SubmitVerificationResponse(ctx context.Context, verificationQueryID uuid.UUID, issuerID w3c.DID, token string, serverURL string) (*domain.VerificationResponse, error) { + // check cache for existing authRequest + var authRequest protocol.AuthorizationRequestMessage + if found := vs.store.Get(ctx, vs.key(verificationQueryID), &authRequest); !found { + log.Error(ctx, "authRequest not found in the cache", "id", verificationQueryID.String()) + return nil, errVerificationKeyNotFound + } + + // perform verification + authRespMsg, err := vs.verifier.FullVerify(ctx, token, + authRequest, + pubsignals.WithAcceptedStateTransitionDelay(stateTransitionDelay)) + if err != nil { + log.Error(ctx, "failed to verify", "verificationQueryID", verificationQueryID, "err", err) + return nil, fmt.Errorf("failed to verify token: %w", err) + } + + // prepare to save result + var jsonbResponse pgtype.JSONB + err = jsonbResponse.Set(authRespMsg) + if err != nil { + return nil, fmt.Errorf("failed to set JSONB value: %w", err) + } + + response := domain.VerificationResponse{ + ID: uuid.New(), // Generate a new UUID for the response + VerificationQueryID: verificationQueryID, + UserDID: authRespMsg.From, + Response: &jsonbResponse, + Pass: true, // Assuming this field is present to indicate verification success + CreatedAt: time.Now(), + } + + // Save the verification response to the repository + responseID, err := vs.repo.AddResponse(ctx, verificationQueryID, response) + if err != nil { + return nil, fmt.Errorf("failed to add verification response: %w", err) + } + + response.ID = responseID + return &response, nil +} + +func (s *VerificationService) key(id uuid.UUID) string { + return "issuer-node:qr-code:" + id.String() +} + +func (vs *VerificationService) getAuthRequestOffChain(ctx context.Context, req *domain.VerificationQuery, serverURL string) (protocol.AuthorizationRequestMessage, error) { + id := uuid.NewString() + authReq := auth.CreateAuthorizationRequest(vs.getReason(nil), req.IssuerDID, vs.getUri(serverURL, req.IssuerDID, req.ID)) + authReq.ID = id + authReq.ThreadID = id + authReq.To = "" + + var scopes []scope + if req.Scope.Status == pgtype.Present { + err := json.Unmarshal(req.Scope.Bytes, &scopes) + if err != nil { + log.Error(ctx, "failed to unmarshal scope", "error", err) + return protocol.AuthorizationRequestMessage{}, fmt.Errorf("failed to unmarshal scope: %w", err) + } + } + + for _, scope := range scopes { + var query map[string]interface{} + err := json.Unmarshal(scope.Query, &query) + if err != nil { + return protocol.AuthorizationRequestMessage{}, err + } + + mtpProofRequest := protocol.ZeroKnowledgeProofRequest{ + ID: scope.Id, + CircuitID: scope.CircuitId, + Query: query, + } + if scope.Params != nil { + var paramsObj map[string]interface{} + err := json.Unmarshal(scope.Params, ¶msObj) + if err != nil { + return protocol.AuthorizationRequestMessage{}, err + } + params, err := vs.getParams(paramsObj) + if err != nil { + return protocol.AuthorizationRequestMessage{}, err + } + + mtpProofRequest.Params = params + } + authReq.Body.Scope = append(authReq.Body.Scope, mtpProofRequest) + } + return authReq, nil +} + +func (vs *VerificationService) getReason(reason *string) string { + if reason == nil { + return "for testing purposes" + } + return *reason +} + +func (vs *VerificationService) getParams(params map[string]interface{}) (map[string]interface{}, error) { + val, ok := params["nullifierSessionID"] + if !ok { + return nil, errors.New("nullifierSessionID is empty") + } + + nullifierSessionID := new(big.Int) + if _, ok := nullifierSessionID.SetString(val.(string), defaultBigIntBase); !ok { + return nil, errors.New("nullifierSessionID is not a valid big integer") + } + + return map[string]interface{}{"nullifierSessionId": nullifierSessionID.String()}, nil +} + +func (vs *VerificationService) getUri(serverURL string, issuerDID string, verificationQueryID uuid.UUID) string { + path := fmt.Sprintf(`/v2/identities/%s/verification/callback?id=%s`, issuerDID, verificationQueryID) + return fmt.Sprintf("%s%s", serverURL, path) +} diff --git a/internal/db/schema/migrations/202411200837121_add_verification_tables.sql b/internal/db/schema/migrations/202411200837121_add_verification_tables.sql new file mode 100644 index 000000000..a2de10f05 --- /dev/null +++ b/internal/db/schema/migrations/202411200837121_add_verification_tables.sql @@ -0,0 +1,30 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS verification_queries ( + id uuid NOT NULL PRIMARY KEY, + issuer_id text NOT NULL, + chain_id integer NOT NULL, + scope jsonb NULL, + skip_check_revocation boolean NOT NULL, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT verification_queries_indentities_id_fk foreign key (issuer_id) references identities (identifier) +); + +CREATE TABLE IF NOT EXISTS verification_responses ( + id uuid NOT NULL PRIMARY KEY, + verification_query_id uuid NOT NULL, + user_did text NOT NULL, + response jsonb NOT NULL, + pass boolean NOT NULL, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT verification_responses_verification_queries_id_fk foreign key (verification_query_id) references verification_queries (id) +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE verification_responses DROP CONSTRAINT verification_responses_verification_scopes_id_fk; +ALTER TABLE verification_queries DROP CONSTRAINT verification_queries_indentities_id_fk; +DROP TABLE IF EXISTS verification_queries; +DROP TABLE IF EXISTS verification_responses; +-- +goose StatementEnd \ No newline at end of file diff --git a/internal/repositories/fixture.go b/internal/repositories/fixture.go index 0a631d836..197c033c1 100644 --- a/internal/repositories/fixture.go +++ b/internal/repositories/fixture.go @@ -19,6 +19,7 @@ type Fixture struct { schemaRepository ports.SchemaRepository identityStateRepository ports.IdentityStateRepository paymentRepository ports.PaymentRepository + verificationRepository ports.VerificationRepository } // NewFixture - constructor @@ -31,6 +32,7 @@ func NewFixture(storage *db.Storage) *Fixture { schemaRepository: NewSchema(*storage), identityStateRepository: NewIdentityState(), paymentRepository: NewPayment(*storage), + verificationRepository: NewVerification(*storage), } } diff --git a/internal/repositories/verification.go b/internal/repositories/verification.go new file mode 100644 index 000000000..6b1e7d817 --- /dev/null +++ b/internal/repositories/verification.go @@ -0,0 +1,144 @@ +package repositories + +import ( + "context" + "errors" + "strings" + + "github.com/google/uuid" + "github.com/iden3/go-iden3-core/v2/w3c" + "github.com/jackc/pgconn" + "github.com/jackc/pgtype" + + "github.com/polygonid/sh-id-platform/internal/core/domain" + "github.com/polygonid/sh-id-platform/internal/db" +) + +var ( + // ErrVerificationQueryNotFound is returned when a verification query is not found + ErrVerificationQueryNotFound = errors.New("verification query not found") + // ErrVerificationResponseNotFound is returned when a verification query is not found + ErrVerificationResponseNotFound = errors.New("verification response not found") +) + +// VerificationRepository is a repository for verification queries +type VerificationRepository struct { + conn db.Storage +} + +// NewVerification creates a new VerificationRepository +func NewVerification(conn db.Storage) *VerificationRepository { + return &VerificationRepository{conn: conn} +} + +// Save stores a verification query in the database +func (r *VerificationRepository) Save(ctx context.Context, issuerID w3c.DID, query domain.VerificationQuery) (uuid.UUID, error) { + sql := `INSERT INTO verification_queries (id, issuer_id, chain_id, scope, skip_check_revocation) + VALUES($1, $2, $3, $4, $5) ON CONFLICT (id) DO + UPDATE SET issuer_id=$2, chain_id=$3, scope=$4, skip_check_revocation=$5 + RETURNING id` + + if query.Scope == nil { + query.Scope = &pgtype.JSONB{ + Status: pgtype.Null, + } + } + + var queryID uuid.UUID + if err := r.conn.Pgx.QueryRow(ctx, sql, query.ID, issuerID.String(), query.ChainID, query.Scope, query.SkipCheckRevocation).Scan(&queryID); err != nil { + return uuid.Nil, err + } + return queryID, nil +} + +// Get returns a verification query by issuer and id +func (r *VerificationRepository) Get(ctx context.Context, issuerID w3c.DID, id uuid.UUID) (*domain.VerificationQuery, error) { + sql := `SELECT id, issuer_id, chain_id, scope, skip_check_revocation, created_at + FROM verification_queries + WHERE issuer_id = $1 and id = $2` + + var query domain.VerificationQuery + err := r.conn.Pgx.QueryRow(ctx, sql, issuerID.String(), id).Scan(&query.ID, &query.IssuerDID, &query.ChainID, &query.Scope, &query.SkipCheckRevocation, &query.CreatedAt) + if err != nil { + if strings.Contains(err.Error(), "no rows in result set") { + return nil, ErrVerificationQueryNotFound + } + return nil, err + } + + if query.Scope == nil { + query.Scope = &pgtype.JSONB{ + Status: pgtype.Null, + } + } + + return &query, nil +} + +// GetAll returns all verification queries for a given issuer +func (r *VerificationRepository) GetAll(ctx context.Context, issuerID w3c.DID) ([]domain.VerificationQuery, error) { + sql := `SELECT id, issuer_id, chain_id, scope, skip_check_revocation, created_at + FROM verification_queries + WHERE issuer_id = $1` + + rows, err := r.conn.Pgx.Query(ctx, sql, issuerID.String()) + if err != nil { + return nil, err + } + defer rows.Close() + + var queries []domain.VerificationQuery + for rows.Next() { + var query domain.VerificationQuery + err = rows.Scan(&query.ID, &query.IssuerDID, &query.ChainID, &query.Scope, &query.SkipCheckRevocation, &query.CreatedAt) + if err != nil { + return nil, err + } + queries = append(queries, query) + } + return queries, nil +} + +// AddResponse stores a verification response in the database +func (r *VerificationRepository) AddResponse(ctx context.Context, queryID uuid.UUID, response domain.VerificationResponse) (uuid.UUID, error) { + sql := `INSERT INTO verification_responses (id, verification_query_id, user_did, response, pass) + VALUES($1, $2, $3, $4, $5) ON CONFLICT (id) DO + UPDATE SET user_did=$3, response=$4, pass=$5 + RETURNING id` + + var responseID uuid.UUID + err := r.conn.Pgx.QueryRow(ctx, sql, response.ID, queryID, response.UserDID, response.Response, response.Pass).Scan(&responseID) + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == foreignKeyViolationErrorCode { + return uuid.Nil, ErrVerificationQueryNotFound + } + return uuid.Nil, err + } + return responseID, nil +} + +// GetVerificationResponse returns a verification response by scopeID and userDID +func (r *VerificationRepository) GetVerificationResponse(ctx context.Context, queryID uuid.UUID) (*domain.VerificationResponse, error) { + sql := `SELECT id, verification_query_id, user_did, response, pass, created_at + FROM verification_responses + WHERE verification_query_id = $1` + + var response domain.VerificationResponse + err := r.conn.Pgx.QueryRow(ctx, sql, queryID).Scan( + &response.ID, + &response.VerificationQueryID, + &response.UserDID, + &response.Response, + &response.Pass, + &response.CreatedAt, + ) + if err != nil { + if strings.Contains(err.Error(), "no rows in result set") { + return nil, ErrVerificationResponseNotFound + } + return nil, err + } + + return &response, nil +} diff --git a/internal/repositories/verification_test.go b/internal/repositories/verification_test.go new file mode 100644 index 000000000..487d58bdf --- /dev/null +++ b/internal/repositories/verification_test.go @@ -0,0 +1,351 @@ +package repositories + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/google/uuid" + "github.com/iden3/go-iden3-core/v2/w3c" + "github.com/jackc/pgtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/polygonid/sh-id-platform/internal/core/domain" +) + +func TestSaveVerificationQuery(t *testing.T) { + ctx := context.Background() + didStr := "did:iden3:polygon:amoy:x9b7eWa8k5rTuDBiWPHou4AvCAd1XTRfxx2uQCni8" + verificationRepository := NewVerification(*storage) + + _, err := storage.Pgx.Exec(ctx, "INSERT INTO identities (identifier, keytype) VALUES ($1, $2)", didStr, "BJJ") + assert.NoError(t, err) + + did, err := w3c.ParseDID(didStr) + require.NoError(t, err) + + t.Run("should save the verification", func(t *testing.T) { + scope := pgtype.JSONB{} + err = scope.Set(`[{"ID": 1, "circuitID": "credentialAtomicQuerySigV2", "query": {"context": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", "allowedIssuers": ["*"], "type": "KYCAgeCredential", "credentialSubject": {"birthday": {"$eq": 19791109}}}}]`) + require.NoError(t, err) + verificationQuery := domain.VerificationQuery{ + ID: uuid.New(), + ChainID: 8002, + SkipCheckRevocation: false, + Scope: &scope, + } + + verificationQueryID, err := verificationRepository.Save(ctx, *did, verificationQuery) + require.NoError(t, err) + assert.Equal(t, verificationQuery.ID, verificationQueryID) + }) +} + +//nolint:all +func TestGetVerification(t *testing.T) { + ctx := context.Background() + didStr := "did:iden3:polygon:amoy:xBdqiqz3yVT79NEAuNaqKSDZ6a5V6q8Ph66i5d2tT" + verificationRepository := NewVerification(*storage) + + _, err := storage.Pgx.Exec(ctx, "INSERT INTO identities (identifier, keytype) VALUES ($1, $2)", didStr, "BJJ") + assert.NoError(t, err) + + did, err := w3c.ParseDID(didStr) + require.NoError(t, err) + + t.Run("should get the verification", func(t *testing.T) { + scope := pgtype.JSONB{} + err = scope.Set(`[{"ID": 1, "circuitID": "credentialAtomicQuerySigV2", "query": {"context": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", "allowedIssuers": ["*"], "type": "KYCAgeCredential", "credentialSubject": {"birthday": {"$eq": 19791109}}}}]`) + require.NoError(t, err) + verificationQuery := domain.VerificationQuery{ + ID: uuid.New(), + ChainID: 8002, + SkipCheckRevocation: false, + Scope: &scope, + } + + verificationQueryID, err := verificationRepository.Save(ctx, *did, verificationQuery) + require.NoError(t, err) + assert.Equal(t, verificationQuery.ID, verificationQueryID) + + verificationQueryFromDB, err := verificationRepository.Get(ctx, *did, verificationQueryID) + require.NoError(t, err) + assert.Equal(t, verificationQuery.ID, verificationQueryFromDB.ID) + assert.Equal(t, verificationQuery.ChainID, verificationQueryFromDB.ChainID) + assert.Equal(t, verificationQuery.SkipCheckRevocation, verificationQueryFromDB.SkipCheckRevocation) + assert.NotNil(t, verificationQueryFromDB.Scope) + + var res []map[string]interface{} + require.NoError(t, json.Unmarshal(verificationQuery.Scope.Bytes, &res)) + assert.Equal(t, 1, len(res)) + assert.Equal(t, 1, int(res[0]["ID"].(float64))) + assert.Equal(t, "credentialAtomicQuerySigV2", res[0]["circuitID"]) + assert.Equal(t, "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", res[0]["query"].(map[string]interface{})["context"]) + assert.Equal(t, []interface{}{"*"}, res[0]["query"].(map[string]interface{})["allowedIssuers"]) + assert.Equal(t, "KYCAgeCredential", res[0]["query"].(map[string]interface{})["type"]) + assert.Equal(t, 19791109, int(res[0]["query"].(map[string]interface{})["credentialSubject"].(map[string]interface{})["birthday"].(map[string]interface{})["$eq"].(float64))) + }) + + t.Run("should not get the verification", func(t *testing.T) { + verificationQueryFromDB, err := verificationRepository.Get(ctx, *did, uuid.New()) + require.Error(t, err) + require.Equal(t, ErrVerificationQueryNotFound, err) + assert.Nil(t, verificationQueryFromDB) + }) +} + +//nolint:all +func TestUpdateVerificationQuery(t *testing.T) { + ctx := context.Background() + didStr := "did:iden3:polygon:amoy:x7tz1NB9fy4GJJW1oQV1wGYpuratuApN8FWEQVKZP" + verificationRepository := NewVerification(*storage) + + _, err := storage.Pgx.Exec(ctx, "INSERT INTO identities (identifier, keytype) VALUES ($1, $2)", didStr, "BJJ") + assert.NoError(t, err) + + did, err := w3c.ParseDID(didStr) + require.NoError(t, err) + + t.Run("should update the verification query", func(t *testing.T) { + credentialSubject1 := pgtype.JSONB{} + err = credentialSubject1.Set(`{"birthday": {"$eq": 19791109}}`) + credentialSubject2 := pgtype.JSONB{} + err = credentialSubject2.Set(`{"position": {"$eq": 1}}`) + require.NoError(t, err) + + scope := pgtype.JSONB{} + err = scope.Set(`[{"ID": 1, "circuitID": "credentialAtomicQuerySigV2", "query": {"context": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", "allowedIssuers": ["*"], "type": "KYCAgeCredential", "credentialSubject": {"birthday": {"$eq": 19791109}}}}]`) + require.NoError(t, err) + scope2 := pgtype.JSONB{} + err = scope2.Set(`[{"ID": 1,"circuitID": "credentialAtomicQueryV3-beta.1","query": {"context": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld","allowedIssuers": ["*"],"type": "KYCAgeCredential","credentialSubject": {"birthday": {"$eq": 19791109}}}}]`) + require.NoError(t, err) + verificationQuery := domain.VerificationQuery{ + ID: uuid.New(), + ChainID: 8002, + SkipCheckRevocation: false, + Scope: &scope, + } + + verificationQueryID, err := verificationRepository.Save(ctx, *did, verificationQuery) + require.NoError(t, err) + assert.Equal(t, verificationQuery.ID, verificationQueryID) + + verificationQuery.Scope = &scope2 + verificationQuery.SkipCheckRevocation = true + verificationQuery.ChainID = 137 + _, err = verificationRepository.Save(ctx, *did, verificationQuery) + require.NoError(t, err) + + verificationQueryFromDB, err := verificationRepository.Get(ctx, *did, verificationQueryID) + require.NoError(t, err) + assert.Equal(t, verificationQuery.ID, verificationQueryFromDB.ID) + assert.Equal(t, verificationQuery.ChainID, verificationQueryFromDB.ChainID) + assert.Equal(t, verificationQuery.SkipCheckRevocation, verificationQueryFromDB.SkipCheckRevocation) + assert.NotNil(t, verificationQueryFromDB.Scope) + + var res []map[string]interface{} + require.NoError(t, json.Unmarshal(verificationQueryFromDB.Scope.Bytes, &res)) + assert.Equal(t, 1, len(res)) + assert.Equal(t, 1, int(res[0]["ID"].(float64))) + assert.Equal(t, "credentialAtomicQueryV3-beta.1", res[0]["circuitID"]) + assert.Equal(t, "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", res[0]["query"].(map[string]interface{})["context"]) + assert.Equal(t, []interface{}{"*"}, res[0]["query"].(map[string]interface{})["allowedIssuers"]) + assert.Equal(t, "KYCAgeCredential", res[0]["query"].(map[string]interface{})["type"]) + assert.Equal(t, 19791109, int(res[0]["query"].(map[string]interface{})["credentialSubject"].(map[string]interface{})["birthday"].(map[string]interface{})["$eq"].(float64))) + }) +} + +//nolint:all +func TestGetAllVerification(t *testing.T) { + ctx := context.Background() + didStr := "did:iden3:polygon:amoy:xCu8Cshrj4oegWRabzGtbKzqUFtXN85x8XkCPdREU" + verificationRepository := NewVerification(*storage) + + _, err := storage.Pgx.Exec(ctx, "INSERT INTO identities (identifier, keytype) VALUES ($1, $2)", didStr, "BJJ") + assert.NoError(t, err) + + did, err := w3c.ParseDID(didStr) + require.NoError(t, err) + + t.Run("GetAll", func(t *testing.T) { + scope := pgtype.JSONB{} + err = scope.Set(`[{"ID": 1, "circuitID": "credentialAtomicQuerySigV2", "query": {"context": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", "allowedIssuers": ["*"], "type": "KYCAgeCredential", "credentialSubject": {"birthday": {"$eq": 19791109}}}}]`) + require.NoError(t, err) + + scope2 := pgtype.JSONB{} + err = scope2.Set(`[{"ID": 2,"circuitID": "credentialAtomicQueryV3-beta.1","query": {"context": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld","allowedIssuers": ["*"],"type": "KYCAgeCredential","credentialSubject": {"birthday": {"$eq": 19791109}}}}]`) + + verificationQuery1 := domain.VerificationQuery{ + ID: uuid.New(), + ChainID: 8002, + SkipCheckRevocation: false, + Scope: &scope, + } + + verificationQuery2 := domain.VerificationQuery{ + ID: uuid.New(), + ChainID: 8002, + SkipCheckRevocation: false, + Scope: &scope2, + } + verificationQueryID1, err := verificationRepository.Save(ctx, *did, verificationQuery1) + require.NoError(t, err) + assert.Equal(t, verificationQuery1.ID, verificationQueryID1) + + verificationQueryID2, err := verificationRepository.Save(ctx, *did, verificationQuery2) + require.NoError(t, err) + assert.Equal(t, verificationQuery2.ID, verificationQueryID2) + + verificationQueryFromDB, err := verificationRepository.GetAll(ctx, *did) + require.NoError(t, err) + assert.Equal(t, 2, len(verificationQueryFromDB)) + + var resVerificationQuery1 []map[string]interface{} + require.NoError(t, json.Unmarshal(verificationQueryFromDB[0].Scope.Bytes, &resVerificationQuery1)) + + assert.Equal(t, verificationQuery1.ID, verificationQueryFromDB[0].ID) + assert.Equal(t, verificationQuery1.ChainID, verificationQueryFromDB[0].ChainID) + assert.Equal(t, verificationQuery1.SkipCheckRevocation, verificationQueryFromDB[0].SkipCheckRevocation) + assert.Equal(t, 1, len(resVerificationQuery1)) + assert.Equal(t, 1, int(resVerificationQuery1[0]["ID"].(float64))) + assert.Equal(t, "credentialAtomicQuerySigV2", resVerificationQuery1[0]["circuitID"]) + assert.Equal(t, "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", resVerificationQuery1[0]["query"].(map[string]interface{})["context"]) + assert.Equal(t, []interface{}{"*"}, resVerificationQuery1[0]["query"].(map[string]interface{})["allowedIssuers"]) + assert.Equal(t, "KYCAgeCredential", resVerificationQuery1[0]["query"].(map[string]interface{})["type"]) + assert.Equal(t, 19791109, int(resVerificationQuery1[0]["query"].(map[string]interface{})["credentialSubject"].(map[string]interface{})["birthday"].(map[string]interface{})["$eq"].(float64))) + + var resVerificationQuery2 []map[string]interface{} + require.NoError(t, json.Unmarshal(verificationQueryFromDB[1].Scope.Bytes, &resVerificationQuery2)) + + assert.Equal(t, verificationQuery2.ID, verificationQueryFromDB[1].ID) + assert.Equal(t, verificationQuery2.ChainID, verificationQueryFromDB[1].ChainID) + assert.Equal(t, verificationQuery2.SkipCheckRevocation, verificationQueryFromDB[1].SkipCheckRevocation) + assert.Equal(t, 1, len(resVerificationQuery2)) + assert.Equal(t, 2, int(resVerificationQuery2[0]["ID"].(float64))) + assert.Equal(t, "credentialAtomicQueryV3-beta.1", resVerificationQuery2[0]["circuitID"]) + assert.Equal(t, "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", resVerificationQuery2[0]["query"].(map[string]interface{})["context"]) + assert.Equal(t, []interface{}{"*"}, resVerificationQuery2[0]["query"].(map[string]interface{})["allowedIssuers"]) + assert.Equal(t, "KYCAgeCredential", resVerificationQuery2[0]["query"].(map[string]interface{})["type"]) + assert.Equal(t, 19791109, int(resVerificationQuery2[0]["query"].(map[string]interface{})["credentialSubject"].(map[string]interface{})["birthday"].(map[string]interface{})["$eq"].(float64))) + }) +} + +func TestAddVerification(t *testing.T) { + ctx := context.Background() + didStr := "did:iden3:polygon:amoy:xCd1tRmXnqbgiT3QC2CuDddUoHK4S9iXwq5xFDJGb" + verificationRepository := NewVerification(*storage) + + _, err := storage.Pgx.Exec(ctx, "INSERT INTO identities (identifier, keytype) VALUES ($1, $2)", didStr, "BJJ") + assert.NoError(t, err) + + did, err := w3c.ParseDID(didStr) + require.NoError(t, err) + + t.Run("should add a response to verification", func(t *testing.T) { + credentialSubject := pgtype.JSONB{} + err = credentialSubject.Set(`{"birthday": {"$eq": 19791109}}`) + require.NoError(t, err) + + scope := pgtype.JSONB{} + err = scope.Set(`[{"ID": 1, "circuitID": "credentialAtomicQuerySigV2", "query": {"context": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", "allowedIssuers": ["*"], "type": "KYCAgeCredential", "credentialSubject": {"birthday": {"$eq": 19791109}}}}]`) + require.NoError(t, err) + verificationQuery := domain.VerificationQuery{ + ID: uuid.New(), + ChainID: 8002, + SkipCheckRevocation: false, + Scope: &scope, + } + + verificationQueryID, err := verificationRepository.Save(ctx, *did, verificationQuery) + require.NoError(t, err) + assert.Equal(t, verificationQuery.ID, verificationQueryID) + + verificationQueryFromDB, err := verificationRepository.Get(ctx, *did, verificationQueryID) + require.NoError(t, err) + assert.Equal(t, verificationQuery.ID, verificationQueryFromDB.ID) + + response := pgtype.JSONB{} + err = response.Set(`{"something": {"proof": 1}}`) + require.NoError(t, err) + verificationResponse := domain.VerificationResponse{ + ID: uuid.New(), + VerificationQueryID: verificationQueryFromDB.ID, + UserDID: "did:iden3:privado:main:2SizDYDWBViKXRfp1VgUAMqhz5SDvP7D1MYiPfwJV3", + Response: &response, + } + + responseID, err := verificationRepository.AddResponse(ctx, verificationQueryFromDB.ID, verificationResponse) + require.NoError(t, err) + assert.Equal(t, verificationResponse.ID, responseID) + }) + + t.Run("should get an error", func(t *testing.T) { + response := pgtype.JSONB{} + err = response.Set(`{"something": {"proof": 1}}`) + require.NoError(t, err) + verificationResponse := domain.VerificationResponse{ + ID: uuid.New(), + UserDID: "did:iden3:privado:main:2SizDYDWBViKXRfp1VgUAMqhz5SDvP7D1MYiPfwJV3", + Response: &response, + } + responseID, err := verificationRepository.AddResponse(ctx, uuid.New(), verificationResponse) + require.Error(t, err) + require.True(t, errors.Is(err, ErrVerificationQueryNotFound)) + assert.Equal(t, uuid.Nil, responseID) + }) +} + +func TestGetVerificationResponse(t *testing.T) { + ctx := context.Background() + didStr := "did:iden3:polygon:amoy:xCd1tRmXnqbgiT3QC2CuDddUoHK4S9iXwq5xFDJGb" + verificationRepository := NewVerification(*storage) + + did, err := w3c.ParseDID(didStr) + require.NoError(t, err) + + t.Run("should get a response to verification", func(t *testing.T) { + credentialSubject := pgtype.JSONB{} + err = credentialSubject.Set(`{"birthday": {"$eq": 19791109}}`) + require.NoError(t, err) + + scope := pgtype.JSONB{} + err = scope.Set(`[{"ID": 1, "circuitID": "credentialAtomicQuerySigV2", "query": {"context": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", "allowedIssuers": ["*"], "type": "KYCAgeCredential", "credentialSubject": {"birthday": {"$eq": 19791109}}}}]`) + require.NoError(t, err) + verificationQuery := domain.VerificationQuery{ + ID: uuid.New(), + ChainID: 8002, + SkipCheckRevocation: false, + Scope: nil, + } + + verificationQueryID, err := verificationRepository.Save(ctx, *did, verificationQuery) + require.NoError(t, err) + assert.Equal(t, verificationQuery.ID, verificationQueryID) + + verificationQueryFromDB, err := verificationRepository.Get(ctx, *did, verificationQueryID) + require.NoError(t, err) + assert.Equal(t, verificationQuery.ID, verificationQueryFromDB.ID) + + response := pgtype.JSONB{} + err = response.Set(`{"something": {"proof": 1}}`) + require.NoError(t, err) + verificationResponse := domain.VerificationResponse{ + ID: uuid.New(), + VerificationQueryID: verificationQueryFromDB.ID, + UserDID: "did:iden3:privado:main:2SizDYDWBVi", + Response: &response, + } + + responseID, err := verificationRepository.AddResponse(ctx, verificationQueryFromDB.ID, verificationResponse) + require.NoError(t, err) + assert.Equal(t, verificationResponse.ID, responseID) + + verificationResponseFromDB, err := verificationRepository.GetVerificationResponse(ctx, verificationQueryFromDB.ID) + require.NoError(t, err) + assert.Equal(t, verificationResponse.ID, verificationResponseFromDB.ID) + assert.Equal(t, verificationResponse.UserDID, verificationResponseFromDB.UserDID) + assert.NotNil(t, verificationResponseFromDB.Response) + }) +} diff --git a/internal/repositories/verification_test_fixture.go b/internal/repositories/verification_test_fixture.go new file mode 100644 index 000000000..c9c36ede3 --- /dev/null +++ b/internal/repositories/verification_test_fixture.go @@ -0,0 +1,27 @@ +package repositories + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/iden3/go-iden3-core/v2/w3c" + "github.com/stretchr/testify/assert" + + "github.com/polygonid/sh-id-platform/internal/core/domain" +) + +// CreateVerificationQuery creates a new query +func (f *Fixture) CreateVerificationQuery(t *testing.T, issuerID w3c.DID, query domain.VerificationQuery) { + t.Helper() + _, err := f.verificationRepository.Save(context.Background(), issuerID, query) + assert.NoError(t, err, "Failed to create verification query") +} + +// CreateVerificationResponse creates a new response for a query +func (f *Fixture) CreateVerificationResponse(t *testing.T, queryID uuid.UUID, response domain.VerificationResponse) { + t.Helper() + responseID, err := f.verificationRepository.AddResponse(context.Background(), queryID, response) + assert.NoError(t, err, "Failed to create verification response") + assert.NotEmpty(t, responseID, "Response ID should not be empty") +} diff --git a/pkg/credentials/circuits/verification_keys/authV2.json b/pkg/credentials/circuits/verification_keys/authV2.json new file mode 100644 index 000000000..b229c431b --- /dev/null +++ b/pkg/credentials/circuits/verification_keys/authV2.json @@ -0,0 +1,104 @@ +{ + "protocol": "groth16", + "curve": "bn128", + "nPublic": 3, + "vk_alpha_1": [ + "20491192805390485299153009773594534940189261866228447918068658471970481763042", + "9383485363053290200918347156157836566562967994039712273449902621266178545958", + "1" + ], + "vk_beta_2": [ + [ + "6375614351688725206403948262868962793625744043794305715222011528459656738731", + "4252822878758300859123897981450591353533073413197771768651442665752259397132" + ], + [ + "10505242626370262277552901082094356697409835680220590971873171140371331206856", + "21847035105528745403288232691147584728191162732299865338377159692350059136679" + ], + [ + "1", + "0" + ] + ], + "vk_gamma_2": [ + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634" + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531" + ], + [ + "1", + "0" + ] + ], + "vk_delta_2": [ + [ + "15934125614912710821614323121670433574627734468332981610453472911976383177228", + "13386788725021602198567425385006899728203544659933593917276469726154154017730" + ], + [ + "8759505107016263108323717548646403750748432711908544803866765373342463765424", + "13205305607413475134301212820100793870092003365382735436692046794406857938024" + ], + [ + "1", + "0" + ] + ], + "vk_alphabeta_12": [ + [ + [ + "2029413683389138792403550203267699914886160938906632433982220835551125967885", + "21072700047562757817161031222997517981543347628379360635925549008442030252106" + ], + [ + "5940354580057074848093997050200682056184807770593307860589430076672439820312", + "12156638873931618554171829126792193045421052652279363021382169897324752428276" + ], + [ + "7898200236362823042373859371574133993780991612861777490112507062703164551277", + "7074218545237549455313236346927434013100842096812539264420499035217050630853" + ] + ], + [ + [ + "7077479683546002997211712695946002074877511277312570035766170199895071832130", + "10093483419865920389913245021038182291233451549023025229112148274109565435465" + ], + [ + "4595479056700221319381530156280926371456704509942304414423590385166031118820", + "19831328484489333784475432780421641293929726139240675179672856274388269393268" + ], + [ + "11934129596455521040620786944827826205713621633706285934057045369193958244500", + "8037395052364110730298837004334506829870972346962140206007064471173334027475" + ] + ] + ], + "IC": [ + [ + "12385314984359904314257455036963499193805822249900169493212773820637861017270", + "13455871848617958073752171682190449799364399689372987044617812281838570851280", + "1" + ], + [ + "1493564767784757620464057507283285365409721187164502463730502309417194080296", + "6377944811748764752279954590131952700069491229367911408873461121555475171995", + "1" + ], + [ + "17810471156883173964067651564103955395454521925125801510057769541384109536787", + "5548963437503981062668882632052452068705295424483999545932010198708798592260", + "1" + ], + [ + "13853274336731202523728826661915506795333516652854674163618978302237601632434", + "15420320918214290109713867361085955935385737854012308761626909938871786338011", + "1" + ] + ] +} \ No newline at end of file diff --git a/pkg/credentials/circuits/verification_keys/credentialAtomicQuerySigV2.json b/pkg/credentials/circuits/verification_keys/credentialAtomicQuerySigV2.json new file mode 100644 index 000000000..65fc8b5aa --- /dev/null +++ b/pkg/credentials/circuits/verification_keys/credentialAtomicQuerySigV2.json @@ -0,0 +1,474 @@ +{ + "protocol": "groth16", + "curve": "bn128", + "nPublic": 77, + "vk_alpha_1": [ + "20491192805390485299153009773594534940189261866228447918068658471970481763042", + "9383485363053290200918347156157836566562967994039712273449902621266178545958", + "1" + ], + "vk_beta_2": [ + [ + "6375614351688725206403948262868962793625744043794305715222011528459656738731", + "4252822878758300859123897981450591353533073413197771768651442665752259397132" + ], + [ + "10505242626370262277552901082094356697409835680220590971873171140371331206856", + "21847035105528745403288232691147584728191162732299865338377159692350059136679" + ], + [ + "1", + "0" + ] + ], + "vk_gamma_2": [ + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634" + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531" + ], + [ + "1", + "0" + ] + ], + "vk_delta_2": [ + [ + "94086484245612286241184073311986077918425211631674599706523220655149342939", + "9066276963657894725757757487225537372173116727614372653696187043001221518998" + ], + [ + "3327364283265612114403912214795791325281389316262923243431510926287241284864", + "14359594691603603310505775002171568388983231776478551485379790891637661560036" + ], + [ + "1", + "0" + ] + ], + "vk_alphabeta_12": [ + [ + [ + "2029413683389138792403550203267699914886160938906632433982220835551125967885", + "21072700047562757817161031222997517981543347628379360635925549008442030252106" + ], + [ + "5940354580057074848093997050200682056184807770593307860589430076672439820312", + "12156638873931618554171829126792193045421052652279363021382169897324752428276" + ], + [ + "7898200236362823042373859371574133993780991612861777490112507062703164551277", + "7074218545237549455313236346927434013100842096812539264420499035217050630853" + ] + ], + [ + [ + "7077479683546002997211712695946002074877511277312570035766170199895071832130", + "10093483419865920389913245021038182291233451549023025229112148274109565435465" + ], + [ + "4595479056700221319381530156280926371456704509942304414423590385166031118820", + "19831328484489333784475432780421641293929726139240675179672856274388269393268" + ], + [ + "11934129596455521040620786944827826205713621633706285934057045369193958244500", + "8037395052364110730298837004334506829870972346962140206007064471173334027475" + ] + ] + ], + "IC": [ + [ + "82858741371846418102390975654111682337499684659569272071403747441178775094", + "4286912121744489803335872688967541084060786471335097977275283322008748340788", + "1" + ], + [ + "2949292283195261203307481545407020289663288337904117332935641624369320632240", + "4895868961372318516427313051068589418900672425971040539274101100343980810774", + "1" + ], + [ + "4283138832985371893627053916321257875901863688069679108552360851257151597412", + "12301445626126932387280175924125479080740113735715055950924670609729124190207", + "1" + ], + [ + "11403809675918608249249090476998815481360885938257614779638377601415472359725", + "10370383889744288167730128292551983273910856935841190320403487139321945926331", + "1" + ], + [ + "146046474957242547231947902105279535658945942556306365105970828598652582527", + "19316391077135032736180617854925909709408388622932141258157985352356274532963", + "1" + ], + [ + "19392288121313736624150032390760791125928224365133434019677559994823666881089", + "16678649776488360981633823784602747916355420844155287963893744273416034228821", + "1" + ], + [ + "17700244988293823646570920639274788254338288455367989082935279790186368535035", + "9321729693794775981089306312375631163270588069455137620052333218103779355408", + "1" + ], + [ + "6304242739428065891345661929109377623420995931327428546798256748327063014748", + "10780954993779202198998690148500166760839127869483280185479097694857479543720", + "1" + ], + [ + "15890615372155272172271387761362992284949685750813721764642195772457152019165", + "16926364370539068089248210833120023205713972276708964692683476649740531300907", + "1" + ], + [ + "8982718515991384638670171335434300220939254007691698916481977516501727365353", + "19933274338973273645600209826788464024915126306902861441259660444347103717011", + "1" + ], + [ + "13624813441697027269419893255557978801536200641544726966141640956028191608323", + "14568380736652140809608442081925020674177705472939839035342120687477258992462", + "1" + ], + [ + "7287001243757652147041978755465708957323122739823909998863775376965394099650", + "8194529377789992862935644002017654759465911480466382895596098893998234618850", + "1" + ], + [ + "15672609491810685215081462552663762421452118634631233092085621737595234267652", + "2904325643405367825992406778236164206511504644734317609359066533215328475457", + "1" + ], + [ + "915419396354435358079156433856200252628133349566107753168383098995762031654", + "7640503160883357453422046063266912192061137337928989362670501928245645451659", + "1" + ], + [ + "14233801682464959754511413505122573904411742927995284652060293502655728201014", + "16894634587173766457107177463154201142416235583146961458016001995514241600797", + "1" + ], + [ + "865637081385889540821011982382409477264012478881380726208863605069542609927", + "2851681816679478967076297609298375717665401236464133109800669778140378240569", + "1" + ], + [ + "14583623854715049637997176036159291974860261686624791798735868439140771963929", + "6184396208904580798818208852802256520753949645935054330532118256917410168729", + "1" + ], + [ + "7340832411020963493191281149327227244123195086114916154649774645956361514808", + "15074654768397897175094344665304877372038748718786237401937087336643412854581", + "1" + ], + [ + "19226322861612796019283465663964339481922670365980090908059633666493003526920", + "4784769513565224451763088600366892575717723263716353119873721504223139128187", + "1" + ], + [ + "4012352361946387705819729346828208464133226668583289635600033112445603056095", + "16057116501864906353475966636435162596350251241825748835463929049616401925621", + "1" + ], + [ + "17699015327677133433762523603786186670660923725134988336625372092121404781352", + "20779678450565816831584776337126804401686084032081969015904070067714797666579", + "1" + ], + [ + "7110239219642171000502622847102158151023059843711231146792904825853085995446", + "13160112334708882478456144620242292286930388110897910022916805337362636596223", + "1" + ], + [ + "18572966839469907925154251914990082086889869375548359485092736092666901883458", + "2717317841156606824568938974067202962925196229862100442399526546853487033983", + "1" + ], + [ + "18220496879414149572832879223762015215122990853285228408008689887370853115800", + "20936442965528445732757109001578889091199378707564445483449611217812774527300", + "1" + ], + [ + "12232077851023924549383437896602730279522249237260024995597446142400396114435", + "6563047663035805798395574221466154696714942622556424013116465603874844109521", + "1" + ], + [ + "12109591576870421145669126698637125260126292886167173093152618373092533930060", + "4838150475568627509687345029028244688244087639289765225238388542584247749631", + "1" + ], + [ + "11686953332615374166413400118993933874772588790879032818717674616364110119003", + "997777557714243682580030314729391031075903187098529888297775331753993942129", + "1" + ], + [ + "17856019272291166215824899590072473034447333075695359997293931405627546425641", + "20847076087560230989541971055803462020430493691416327989998676442287723694850", + "1" + ], + [ + "3719233446259090602031823947643646288707755415354997947878759420203354178997", + "8365343103859542659965068947238411148203250330874262347086737251125778319916", + "1" + ], + [ + "11510913293380810758607591029811272398780473934028209478267236534219159061926", + "21487940007144748529535209675246355785366971140524949831759963463815763342086", + "1" + ], + [ + "1320866581863043338512589290197401952944129485739535713341560521800016476945", + "2493558738654724256478579784099858256156921863307444062868905210334526715644", + "1" + ], + [ + "17921519492985568040647785518984679305231500205599581370398502174410641627915", + "7509747881493316986520702491460493363464636273816319210975041613994864176359", + "1" + ], + [ + "3430712548343353484542829031470422149014035558383953986112467142255149482517", + "14550495557052814428641743686592474039195116676265817238933308829823014113648", + "1" + ], + [ + "15404982231804436812797545928371130848106957647851884888070752463417657014850", + "4611196330294175143659018144962441564350289800068036864557333922145119754928", + "1" + ], + [ + "15263701692315698223820596911784671235943493678301007311780033091137667408294", + "604902718763398072835765087427553474161374630152901938069702903322739720901", + "1" + ], + [ + "20244489449718281224771972382454688977944613519370226595741543942193818865707", + "19005325000779061572105038900407210855167193186325179384744370456674531846752", + "1" + ], + [ + "17905734409676470389691612532757732246970469909527519932162818478447075527708", + "14786147027096263915511297996292826847659087062694857723516642286999030404099", + "1" + ], + [ + "13170413525057177027118218806689559566136892856728098698757124284101508780041", + "21138656039297587744247525334341823578219773225144796878223492790449265984236", + "1" + ], + [ + "17336985422666489495917434298152259228601087998778158909593038947037047437034", + "17640592272124615371604215593775167886611551365178888179149933534238084536035", + "1" + ], + [ + "13115497681355919008843671294553575734811415225866638680245661303800809177454", + "11101396909544211706087319187611419845994320778819031170997345351820871301500", + "1" + ], + [ + "5062223967757742841418833629304894504824711871486969640422734949713212037564", + "5995745391558201393058938510743066155455293405187491263678494062578649392537", + "1" + ], + [ + "10769448662035944503588756967807107248531839233480897019708757162346507533856", + "17683311668907780400377940051769789887732541265829205574046296993672760170234", + "1" + ], + [ + "18909222506084760520118400904387222253545995580483849260301107047056625024809", + "4739939423481558802886855387063931149245914588665635776355445541037716874191", + "1" + ], + [ + "237258354423629009139604512345017099458104648744590151045267949108836136046", + "8398477677610482726525801716151367352299183871166696265021289782422497361573", + "1" + ], + [ + "21614899156845209369731734601334455382216240765307343061569649838763541939674", + "19737392631718395415895070591908882100913238716433370918711613904255554425863", + "1" + ], + [ + "11583018052272400568802079643913527066888050377899679393339799714495380661286", + "18731342198059952174455684263813783627914005022231316888089150078895916324047", + "1" + ], + [ + "1800355436595083773914109207841543211385162586213879300974211146987134123202", + "13948211457427522096477007682338343714710607448417503368661334375180794438646", + "1" + ], + [ + "12380151495516055715423974838724751018509187196044814610299179374420946317150", + "15604417067169603747798399311766223987091027902169069371143436781729299657321", + "1" + ], + [ + "15586796314575179431439068400563064768255110456857949233401991221744972388290", + "6869029658475871438252091552567541171158916510785162784566374960655529514541", + "1" + ], + [ + "9454259555969046405835114554840573800894460654638881697884905397762877591671", + "15649504506074833196199868121136622617218927420004842806076140919475722131828", + "1" + ], + [ + "10896479167947716665298047581223743449173173843441842077252741245744005867502", + "19466843112239886558867945418277977036987004293914397183376561940538353431523", + "1" + ], + [ + "21093729272413526398302027094496286062183285233963676469751711501691030254808", + "5616302988725848953380515455651462166436866513193168038457367334604897142643", + "1" + ], + [ + "9162526928276135317390642844528140151219934473125754385250619863694467527200", + "2704506477496217427068410968125378104897220198385366379780167188967641057115", + "1" + ], + [ + "2611703957607573998187347531547663551663457287415444888228409918854708173921", + "16503183711195222950599750913705688322318608987024302010766325034010085416942", + "1" + ], + [ + "11144191723062275260143093129552752703141874383079896857211309008338415817157", + "16781709265403451651508663495722707593571471440254748275969368360531033950981", + "1" + ], + [ + "16812964624232834362158259778644306304990898865384873094141859868591196349911", + "13922581405000464856568387775868339864861250228661168312914123793152293400444", + "1" + ], + [ + "7757513072166428505849658731723708923254564692050157984152317397804988815683", + "10596938418787218858764915380828895440118767426036644517687291298143168844310", + "1" + ], + [ + "776963606617916676505305527193906634173028085645966206654629945039284611047", + "10912266632936528293222094364131226803541316295737642239229481058897029295382", + "1" + ], + [ + "16373521723700098997684769703648529905132042031831109132237638798191456108024", + "11103461808206383275179890639579495748275557033587467729454195698739957928818", + "1" + ], + [ + "12824183511411995663633068443784430488868221252426812112737116307753322360649", + "16623259457237877334961209654722086328478084586497591770992246134732053248864", + "1" + ], + [ + "4440204567609318598244879167103963411704795015851735842148385354806120614776", + "439254538132916792300814825054431063060942088144912111780551400772555518726", + "1" + ], + [ + "12550163747027957708679416133057345303366416863124571412695635212042254660231", + "8088733548936346769418714493856651195585281586259855084725506021681259615995", + "1" + ], + [ + "601695970176955369889380617598670451586934521316364158397268786862817318324", + "8177591911722905772853175277543921812590123868553367351838538360889195534445", + "1" + ], + [ + "21420783745411266284334793892673128470907336626528172424997282710543407678562", + "815896668203600154924756799739505300427469299180041621997534085009087797462", + "1" + ], + [ + "5985108217900335996495740885137329434596043615956659372441003017431913541049", + "12400136587102116035870838109370183374536730415523630490626751552251898583723", + "1" + ], + [ + "19164864123547614924145477596845278488377820092261133057871046801697026504830", + "9058096638083409870599642989053208885699231157685473105338357045834902352657", + "1" + ], + [ + "5625208601612291266473410363212052500521534296123171244974430101397304128598", + "20032223272677310984797975395155189489741584704900355338539941675727215575834", + "1" + ], + [ + "19722119258760509259002736097441021306020824312273305874398252765749858625383", + "21476284183336273518938142350498253130780475874465538560639647841713873409967", + "1" + ], + [ + "16214582901276753122992657437576487460434296014671145602237720787074011098320", + "14638014587762532725758011227519785515588402732574136404578917712064567856285", + "1" + ], + [ + "21242009997783861714463438145352831066649780964520311892324662965942254722206", + "17000102442647549832409557151546317734552217997144722436132435910128720474199", + "1" + ], + [ + "3901540629460292660569097126645101425134163883949632616895230751480848213369", + "15462697656409662538566017032586328247938424512432827105504267029364730924622", + "1" + ], + [ + "13589690411326512053318122778970095131969288693012374077657080517788789407345", + "3256080065084142457753800788670092375263661907651111479712628200718368447047", + "1" + ], + [ + "80867775087231075346193177024922594793617447013874226020788299753946699495", + "18692751692376633351750143580480221941277275632746814828053833446777751678844", + "1" + ], + [ + "12208564838188569361021574820173956567515251256529477811689323941001064824558", + "14719028828276200987004700519998864320546393568625959778339171591168779343802", + "1" + ], + [ + "11487733918945328878091426687312208948265605735490434820651127086706421059773", + "12884027668625422693735810500338290593888443327897978424106732856151809328547", + "1" + ], + [ + "20632552305904865953323352410960265689821616276406781145263037394521803318607", + "2807465385289781642965528502072388424070031841212619812619243814164168949956", + "1" + ], + [ + "15762698488851251645720011145875054676705544433397646684075263620983096891945", + "1298669502852138604153414592011225832117552495360099510102598347062068301160", + "1" + ], + [ + "11129488032579072454261600806944244717606891316794143410356402368489165589130", + "17038312956321424279807397639106394935887287800511404015727988253317547071041", + "1" + ] + ] +} \ No newline at end of file