diff --git a/cmd/vc-rest/startcmd/start.go b/cmd/vc-rest/startcmd/start.go index bb8727f2e..75f70b67d 100644 --- a/cmd/vc-rest/startcmd/start.go +++ b/cmd/vc-rest/startcmd/start.go @@ -862,9 +862,10 @@ func buildEchoHandler( var verifyPresentationSvc verifypresentation.ServiceInterface verifyPresentationSvc = verifypresentation.New(&verifypresentation.Config{ - VcVerifier: verifyCredentialSvc, - DocumentLoader: documentLoader, - VDR: conf.VDR, + VcVerifier: verifyCredentialSvc, + DocumentLoader: documentLoader, + VDR: conf.VDR, + ClientAttestationService: clientAttestationService, }) if conf.IsTraceEnabled { diff --git a/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go b/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go index cf281986e..9d083a15c 100644 --- a/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go +++ b/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go @@ -32,6 +32,7 @@ import ( "github.com/trustbloc/vc-go/vermethod" jwssigner "github.com/trustbloc/vcs/component/wallet-cli/pkg/signer" + "github.com/trustbloc/vcs/component/wallet-cli/pkg/trustregistry" "github.com/trustbloc/vcs/component/wallet-cli/pkg/wallet" "github.com/trustbloc/vcs/pkg/doc/vc" vccrypto "github.com/trustbloc/vcs/pkg/doc/vc/crypto" @@ -39,7 +40,6 @@ import ( vcskms "github.com/trustbloc/vcs/pkg/kms" kmssigner "github.com/trustbloc/vcs/pkg/kms/signer" "github.com/trustbloc/vcs/pkg/observability/metrics/noop" - "github.com/trustbloc/vcs/pkg/service/trustregistry" ) const ( @@ -151,11 +151,10 @@ func (f *Flow) Run(ctx context.Context) error { slog.Info("Run Trust Registry Verifier validation", "url", trustRegistryURL) trustRegistry := trustregistry.New(&trustregistry.Config{ - TrustRegistryURL: trustRegistryURL, - HTTPClient: f.httpClient, + HTTPClient: f.httpClient, }) - if err = trustRegistry.ValidateVerifier(requestObject.ClientID, credentials); err != nil { + if err = trustRegistry.ValidateVerifier(trustRegistryURL, requestObject.ClientID, credentials); err != nil { return fmt.Errorf("trust registry verifier validation: %w", err) } } diff --git a/pkg/service/trustregistry/models.go b/component/wallet-cli/pkg/trustregistry/models.go similarity index 72% rename from pkg/service/trustregistry/models.go rename to component/wallet-cli/pkg/trustregistry/models.go index 2ef4d0df0..d5e16ced8 100644 --- a/pkg/service/trustregistry/models.go +++ b/component/wallet-cli/pkg/trustregistry/models.go @@ -11,12 +11,6 @@ type VerifierValidationConfig struct { Metadata []*CredentialMetadata `json:"metadata"` } -type PresentationValidationConfig struct { - PolicyID string `json:"policy_id"` - AttestationVC interface{} `json:"attestation_vc"` - Metadata []*CredentialMetadata `json:"metadata"` -} - type CredentialMetadata struct { CredentialID string `json:"credential_id"` Types []string `json:"types"` diff --git a/pkg/service/trustregistry/service.go b/component/wallet-cli/pkg/trustregistry/trustregistry.go similarity index 50% rename from pkg/service/trustregistry/service.go rename to component/wallet-cli/pkg/trustregistry/trustregistry.go index 2990fcd04..ad249ec44 100644 --- a/pkg/service/trustregistry/service.go +++ b/component/wallet-cli/pkg/trustregistry/trustregistry.go @@ -8,13 +8,12 @@ package trustregistry import ( "bytes" + "context" "encoding/json" "errors" "fmt" "net/http" - "github.com/samber/lo" - "github.com/trustbloc/logutil-go/pkg/log" "github.com/trustbloc/vc-go/verifiable" ) @@ -24,28 +23,21 @@ var ( logger = log.New("trustregistry") ) -const ( - walletAttestationVCType = "WalletAttestationCredential" -) - +type Config struct { + HTTPClient *http.Client +} type Service struct { - url string httpClient *http.Client } -type Config struct { - TrustRegistryURL string - HTTPClient *http.Client -} - func New(conf *Config) *Service { - return &Service{ - url: conf.TrustRegistryURL, - httpClient: conf.HTTPClient, - } + return &Service{httpClient: conf.HTTPClient} } -func (s *Service) ValidateVerifier(verifierDID string, presentationCredentials []*verifiable.Credential) error { +func (s *Service) ValidateVerifier( + policyURL, verifierDID string, + presentationCredentials []*verifiable.Credential, +) error { logger.Debug("ValidateVerifier begin") verifierValidationConfig := &VerifierValidationConfig{ VerifierDID: verifierDID, @@ -55,15 +47,15 @@ func (s *Service) ValidateVerifier(verifierDID string, presentationCredentials [ for i, credential := range presentationCredentials { content := credential.Contents() - verifierValidationConfig.Metadata[i] = s.getCredentialMetadata(content) + verifierValidationConfig.Metadata[i] = getTrustRegistryCredentialMetadata(content) } - req, err := json.Marshal(verifierValidationConfig) + reqPayload, err := json.Marshal(verifierValidationConfig) if err != nil { return fmt.Errorf("encode verifier config: %w", err) } - responseDecoded, err := s.doRequest(req) + responseDecoded, err := s.doTrustRegistryRequest(context.Background(), policyURL, reqPayload) if err != nil { return err } @@ -77,48 +69,15 @@ func (s *Service) ValidateVerifier(verifierDID string, presentationCredentials [ return nil } -func (s *Service) ValidatePresentation(policyID string, presentationCredentials []*verifiable.Credential) error { - logger.Debug("ValidatePresentation begin") - - presentationValidationConfig := &PresentationValidationConfig{ - PolicyID: policyID, - Metadata: make([]*CredentialMetadata, len(presentationCredentials)), - } - - for i, credential := range presentationCredentials { - content := credential.Contents() - - if lo.Contains(content.Types, walletAttestationVCType) { - attestationVC, err := credential.ToUniversalForm() - if err == nil { - presentationValidationConfig.AttestationVC = attestationVC - } - } - - presentationValidationConfig.Metadata[i] = s.getCredentialMetadata(content) - } - - req, err := json.Marshal(presentationValidationConfig) +func (s *Service) doTrustRegistryRequest(ctx context.Context, policyURL string, req []byte) (*Response, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodPost, policyURL, bytes.NewReader(req)) if err != nil { - return fmt.Errorf("encode presentation config: %w", err) - } - - responseDecoded, err := s.doRequest(req) - if err != nil { - return err + return nil, fmt.Errorf("create request: %w", err) } - if !responseDecoded.Allowed { - return ErrInteractionRestricted - } - - logger.Debug("ValidatePresentation succeed") - - return nil -} + request.Header.Add("content-type", "application/json") -func (s *Service) doRequest(req []byte) (*Response, error) { - resp, err := s.httpClient.Post(s.url, "application/json", bytes.NewReader(req)) //nolint:noctx + resp, err := s.httpClient.Do(request) if err != nil { return nil, fmt.Errorf("send request: %w", err) } @@ -138,7 +97,7 @@ func (s *Service) doRequest(req []byte) (*Response, error) { return responseDecoded, nil } -func (s *Service) getCredentialMetadata(content verifiable.CredentialContents) *CredentialMetadata { +func getTrustRegistryCredentialMetadata(content verifiable.CredentialContents) *CredentialMetadata { var iss, exp string if content.Issued != nil { iss = content.Issued.FormatToString() diff --git a/pkg/service/trustregistry/service_test.go b/component/wallet-cli/pkg/trustregistry/trustregistry_test.go similarity index 64% rename from pkg/service/trustregistry/service_test.go rename to component/wallet-cli/pkg/trustregistry/trustregistry_test.go index 2be4f6a66..c886a4c75 100644 --- a/pkg/service/trustregistry/service_test.go +++ b/component/wallet-cli/pkg/trustregistry/trustregistry_test.go @@ -1,9 +1,3 @@ -/* -Copyright SecureKey Technologies Inc. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - package trustregistry import ( @@ -14,11 +8,14 @@ import ( "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" - util "github.com/trustbloc/did-go/doc/util/time" "github.com/trustbloc/vc-go/verifiable" ) +const ( + walletAttestationVCType = "WalletAttestationCredential" +) + func TestService_ValidateVerifier(t *testing.T) { now := time.Now() handler := echo.New() @@ -137,115 +134,10 @@ func TestService_ValidateVerifier(t *testing.T) { tt.addTestCaseHandler(t, handler) s := &Service{ - url: tt.fields.url, httpClient: http.DefaultClient, } - err := s.ValidateVerifier(tt.args.verifierDID, tt.args.getPresentationCredentials(t, now)) - if (err != nil) != tt.wantErr { - t.Errorf("ValidateVerifier() error = %v, wantErr %v", err, tt.wantErr) - } - - if tt.wantErr { - assert.ErrorContains(t, err, tt.errContains) - } - }) - } -} - -func TestService_ValidatePresentation(t *testing.T) { - now := time.Now() - handler := echo.New() - - srv := httptest.NewServer(handler) - defer srv.Close() - - type fields struct { - url string - } - type args struct { - policyID string - getPresentationCredentials func(t *testing.T, now time.Time) []*verifiable.Credential - } - tests := []struct { - name string - addTestCaseHandler func(t *testing.T, e *echo.Echo) - fields fields - args args - wantErr bool - errContains string - }{ - { - name: "Success", - addTestCaseHandler: func(t *testing.T, e *echo.Echo) { - e.Add(http.MethodPost, "/testcase1", func(c echo.Context) error { - var got *PresentationValidationConfig - assert.NoError(t, c.Bind(&got)) - - attestationVCUniversalForm, err := getAttestationCredential(t, now).ToUniversalForm() - assert.NoError(t, err) - - expected := &PresentationValidationConfig{ - PolicyID: "policy1", - AttestationVC: attestationVCUniversalForm, - Metadata: getDefaultMetadata(t, now), - } - - assert.Equal(t, expected, got) - - return c.JSON(http.StatusOK, map[string]bool{"allowed": true}) - }) - }, - fields: fields{ - url: srv.URL + "/testcase1", - }, - args: args{ - policyID: "policy1", - getPresentationCredentials: getDefaultCredentials, - }, - wantErr: false, - }, - { - name: "httpClient.Post error", - addTestCaseHandler: func(t *testing.T, e *echo.Echo) {}, - fields: fields{ - url: "abcd", - }, - args: args{ - policyID: "policy1", - getPresentationCredentials: getDefaultCredentials, - }, - wantErr: true, - errContains: "send request:", - }, - { - name: "Interaction restricted error", - addTestCaseHandler: func(t *testing.T, e *echo.Echo) { - e.Add(http.MethodPost, "/testcase2", func(c echo.Context) error { - return c.JSON(http.StatusOK, map[string]bool{"allowed": false}) - }) - }, - fields: fields{ - url: srv.URL + "/testcase2", - }, - args: args{ - policyID: "policy1", - getPresentationCredentials: getDefaultCredentials, - }, - wantErr: true, - errContains: "interaction restricted", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.addTestCaseHandler(t, handler) - - s := New(&Config{ - TrustRegistryURL: tt.fields.url, - HTTPClient: http.DefaultClient, - }) - - err := s.ValidatePresentation(tt.args.policyID, tt.args.getPresentationCredentials(t, now)) + err := s.ValidateVerifier(tt.fields.url, tt.args.verifierDID, tt.args.getPresentationCredentials(t, now)) if (err != nil) != tt.wantErr { t.Errorf("ValidateVerifier() error = %v, wantErr %v", err, tt.wantErr) } diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vp.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vp.go index c5b24071a..0363f995a 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vp.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vp.go @@ -36,13 +36,13 @@ import ( "github.com/trustbloc/vc-go/verifiable" "github.com/trustbloc/vcs/component/wallet-cli/internal/httputil" + "github.com/trustbloc/vcs/component/wallet-cli/pkg/trustregistry" "github.com/trustbloc/vcs/pkg/doc/vc" vccrypto "github.com/trustbloc/vcs/pkg/doc/vc/crypto" vcs "github.com/trustbloc/vcs/pkg/doc/verifiable" vcskms "github.com/trustbloc/vcs/pkg/kms" "github.com/trustbloc/vcs/pkg/kms/signer" "github.com/trustbloc/vcs/pkg/observability/metrics/noop" - "github.com/trustbloc/vcs/pkg/service/trustregistry" ) type RPConfigOverride func(rpc *RPConfig) @@ -127,13 +127,12 @@ func (s *Service) RunOIDC4VPFlow(ctx context.Context, authorizationRequest strin log.Println("Run Trust Registry Verifier validation") trustRegistry := trustregistry.New(&trustregistry.Config{ - TrustRegistryURL: trustRegistryURL, - HTTPClient: s.httpClient, + HTTPClient: s.httpClient, }) credentials := s.vpFlowExecutor.requestPresentation[0].Credentials() - if err = trustRegistry.ValidateVerifier(s.vpFlowExecutor.requestObject.ClientID, credentials); err != nil { + if err = trustRegistry.ValidateVerifier(trustRegistryURL, s.vpFlowExecutor.requestObject.ClientID, credentials); err != nil { return fmt.Errorf("trust registry verifier validation: %w", err) } } diff --git a/pkg/internal/testutil/contexts/wallet_attestation_vc_v1.jsonld b/pkg/internal/testutil/contexts/wallet_attestation_vc_v1.jsonld new file mode 100644 index 000000000..27be95fd2 --- /dev/null +++ b/pkg/internal/testutil/contexts/wallet_attestation_vc_v1.jsonld @@ -0,0 +1,12 @@ +{ + "@context": { + "@version": 1.1, + "id": "@id", + "type": "@type", + "WalletAttestationCredential": "ex:WalletAttestationCredential", + + "key_type": "ex:key_type", + "user_authentication": "ex:user_authentication", + "assurance_level": "ex:assurance_level" + } +} \ No newline at end of file diff --git a/pkg/internal/testutil/document_loader.go b/pkg/internal/testutil/document_loader.go index 6606d7d76..65b93ceb2 100644 --- a/pkg/internal/testutil/document_loader.go +++ b/pkg/internal/testutil/document_loader.go @@ -33,6 +33,8 @@ var ( vcStatusList2021 []byte //go:embed contexts/vc-data-integrity-v1.jsonld vcDataIntegrity []byte + //go:embed contexts/wallet_attestation_vc_v1.jsonld + walletAttestationVC []byte ) type mockLDStoreProvider struct { @@ -87,6 +89,10 @@ func DocumentLoader(t *testing.T, extraContexts ...ldcontext.Document) *ld.Docum URL: "https://w3id.org/security/data-integrity/v1", Content: vcDataIntegrity, }, + ldcontext.Document{ + URL: "https://www.w3.org/2022/credentials/walletAttestation/v1", + Content: walletAttestationVC, + }, } loader, err := ld.NewDocumentLoader(ldStore, diff --git a/pkg/kms/mocks/kms_mocks.go b/pkg/kms/mocks/kms_mocks.go index d96a103f3..8ca0def08 100644 --- a/pkg/kms/mocks/kms_mocks.go +++ b/pkg/kms/mocks/kms_mocks.go @@ -10,7 +10,6 @@ import ( gomock "github.com/golang/mock/gomock" jwk "github.com/trustbloc/kms-go/doc/jose/jwk" kms "github.com/trustbloc/kms-go/spi/kms" - vc "github.com/trustbloc/vcs/pkg/doc/vc" verifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" ) diff --git a/pkg/observability/metrics/prometheus/provider.go b/pkg/observability/metrics/prometheus/provider.go index f1f13e772..3155efec3 100644 --- a/pkg/observability/metrics/prometheus/provider.go +++ b/pkg/observability/metrics/prometheus/provider.go @@ -121,6 +121,7 @@ func NewMetrics( metrics.ClientWellKnown, metrics.ClientCredentialVerifier, metrics.ClientDiscoverableClientIDScheme, metrics.ClientAttestationService, + metrics.TrustRegistryService, } pm := &PromMetrics{ diff --git a/pkg/observability/metrics/provider.go b/pkg/observability/metrics/provider.go index d7734bf6b..dc82c30a2 100644 --- a/pkg/observability/metrics/provider.go +++ b/pkg/observability/metrics/provider.go @@ -59,6 +59,7 @@ const ( ClientCredentialVerifier ClientID = "credential-verifier" //nolint:gosec ClientDiscoverableClientIDScheme ClientID = "discoverable-client-id-scheme" ClientAttestationService ClientID = "client-attestation-service" + TrustRegistryService ClientID = "trustregistry-service" ) // Provider is an interface for metrics provider. diff --git a/pkg/profile/api.go b/pkg/profile/api.go index 83eb0ac1b..43e72ffae 100644 --- a/pkg/profile/api.go +++ b/pkg/profile/api.go @@ -50,6 +50,7 @@ type Issuer struct { CredentialTemplates []*CredentialTemplate `json:"credentialTemplates,omitempty"` WebHook string `json:"webHook,omitempty"` CredentialMetaData *CredentialMetaData `json:"credentialMetadata"` + Policy Policy `json:"policy,omitempty"` } type CredentialTemplate struct { @@ -146,6 +147,13 @@ type Verifier struct { SigningDID *SigningDID `json:"signingDID,omitempty"` PresentationDefinitions []*presexch.PresentationDefinition `json:"presentationDefinitions,omitempty"` WebHook string `json:"webHook,omitempty"` + Policy Policy `json:"policy,omitempty"` +} + +// Policy represents profile configuration for Trust Registry policy. +type Policy struct { + // URL is the Trust Registry policy URL. + URL string `json:"url"` } // OIDC4VPConfig store config for verifier did that used to sign request object in oidc4vp process. diff --git a/pkg/service/clientattestation/client_attestation_service.go b/pkg/service/clientattestation/client_attestation_service.go index 9419754f1..187d73fb3 100644 --- a/pkg/service/clientattestation/client_attestation_service.go +++ b/pkg/service/clientattestation/client_attestation_service.go @@ -10,6 +10,7 @@ package clientattestation import ( "context" + "errors" "fmt" "net/http" "time" @@ -18,12 +19,8 @@ import ( "github.com/samber/lo" "github.com/trustbloc/vc-go/jwt" "github.com/trustbloc/vc-go/verifiable" - - profileapi "github.com/trustbloc/vcs/pkg/profile" ) -const WalletAttestationVCType = "WalletAttestationCredential" - type httpClient interface { Do(req *http.Request) (*http.Response, error) } @@ -32,6 +29,8 @@ type vcStatusVerifier interface { ValidateVCStatus(ctx context.Context, vcStatus *verifiable.TypedID, issuer *verifiable.Issuer) error } +type TrustRegistryPayloadBuilder func(attestationVC *verifiable.Credential, vp *verifiable.Presentation) ([]byte, error) + // Config defines configuration for Service. type Config struct { HTTPClient httpClient @@ -61,7 +60,12 @@ func NewService(config *Config) *Service { // ValidateAttestationJWTVP validates attestation VP in jwt_vp format. // //nolint:revive -func (s *Service) ValidateAttestationJWTVP(ctx context.Context, profile *profileapi.Issuer, jwtVP string) error { +func (s *Service) ValidateAttestationJWTVP( + ctx context.Context, + jwtVP string, + policyURL string, + payloadBuilder TrustRegistryPayloadBuilder, +) error { vp, err := verifiable.ParsePresentation( []byte(jwtVP), // The verification of proof is conducted manually, along with an extra verification to ensure that signer of @@ -73,20 +77,12 @@ func (s *Service) ValidateAttestationJWTVP(ctx context.Context, profile *profile return fmt.Errorf("parse attestation vp: %w", err) } - var vc *verifiable.Credential - - for _, credential := range vp.Credentials() { - content := credential.Contents() - - if lo.Contains(content.Types, WalletAttestationVCType) { - vc = credential - - break - } - } + attestationVC, found := lo.Find(vp.Credentials(), func(item *verifiable.Credential) bool { + return lo.Contains(item.Contents().Types, walletAttestationVCType) + }) - if vc == nil { - return fmt.Errorf("missing attestation vc") + if !found { + return errors.New("attestation vc is not supplied") } // validate attestation VC @@ -95,15 +91,15 @@ func (s *Service) ValidateAttestationJWTVP(ctx context.Context, profile *profile verifiable.WithJSONLDDocumentLoader(s.documentLoader), } - if err = vc.ValidateCredential(opts...); err != nil { + if err = attestationVC.ValidateCredential(opts...); err != nil { return fmt.Errorf("validate attestation vc: %w", err) } - if err = vc.CheckProof(opts...); err != nil { + if err = attestationVC.CheckProof(opts...); err != nil { return fmt.Errorf("check attestation vc proof: %w", err) } - vcc := vc.Contents() + vcc := attestationVC.Contents() if vcc.Expired != nil && time.Now().UTC().After(vcc.Expired.Time) { return fmt.Errorf("attestation vc is expired") @@ -119,7 +115,20 @@ func (s *Service) ValidateAttestationJWTVP(ctx context.Context, profile *profile return fmt.Errorf("validate attestation vc status: %w", err) } - // TODO: validate attestation vc in trust registry + var trustRegistryRequestBody []byte + trustRegistryRequestBody, err = payloadBuilder(attestationVC, vp) + if err != nil { + return fmt.Errorf("payload builder: %w", err) + } + + responseDecoded, err := s.doTrustRegistryRequest(ctx, policyURL, trustRegistryRequestBody) + if err != nil { + return err + } + + if !responseDecoded.Allowed { + return ErrInteractionRestricted + } return nil } diff --git a/pkg/service/clientattestation/client_attestation_service_test.go b/pkg/service/clientattestation/client_attestation_service_test.go index bba217d37..a92547390 100644 --- a/pkg/service/clientattestation/client_attestation_service_test.go +++ b/pkg/service/clientattestation/client_attestation_service_test.go @@ -9,11 +9,15 @@ package clientattestation_test import ( "context" "errors" + "net/http" + "net/http/httptest" "testing" "time" "github.com/golang/mock/gomock" "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" utiltime "github.com/trustbloc/did-go/doc/util/time" "github.com/trustbloc/kms-go/spi/kms" @@ -23,7 +27,6 @@ import ( "github.com/trustbloc/vc-go/verifiable" "github.com/trustbloc/vcs/pkg/internal/testutil" - profileapi "github.com/trustbloc/vcs/pkg/profile" "github.com/trustbloc/vcs/pkg/service/clientattestation" ) @@ -36,11 +39,16 @@ const ( ) func TestService_ValidateClientAttestationJWTVP(t *testing.T) { - httpClient := NewMockHTTPClient(gomock.NewController(t)) + now := time.Now().UTC() + handler := echo.New() + + srv := httptest.NewServer(handler) + defer srv.Close() + vcStatusVerifier := NewMockVCStatusVerifier(gomock.NewController(t)) var jwtVP string - var profile *profileapi.Issuer + var payloadBuilder clientattestation.TrustRegistryPayloadBuilder proofCreators, defaultProofChecker := testsupport.NewKMSSignersAndVerifier(t, []testsupport.SigningKey{ @@ -62,14 +70,16 @@ func TestService_ValidateClientAttestationJWTVP(t *testing.T) { tests := []struct { name string + url string setup func() check func(t *testing.T, err error) }{ { - name: "success", + name: "success OIDC4CI", + url: srv.URL + "/success_oidc4ci", setup: func() { // create wallet attestation VC with wallet DID as subject and attestation DID as issuer - attestationVC := createAttestationVC(t, attestationProofCreator, walletDID, false) + attestationVC := createAttestationVC(t, attestationProofCreator, walletDID, now, false) // prepare wallet attestation VP (in jwt_vp format) signed by wallet DID jwtVP = createAttestationVP(t, attestationVC, walletProofCreator) @@ -77,6 +87,70 @@ func TestService_ValidateClientAttestationJWTVP(t *testing.T) { proofChecker = defaultProofChecker vcStatusVerifier.EXPECT().ValidateVCStatus(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + payloadBuilder = clientattestation.IssuerInteractionTrustRegistryPayloadBuilder + + handler.Add(http.MethodPost, "/success_oidc4ci", func(c echo.Context) error { + var got *clientattestation.IssuerInteractionValidationConfig + assert.NoError(t, c.Bind(&got)) + + attestationVCUniversalForm, err := attestationVC.ToUniversalForm() + assert.NoError(t, err) + + expected := &clientattestation.IssuerInteractionValidationConfig{ + AttestationVC: attestationVCUniversalForm, + Metadata: []*clientattestation.CredentialMetadata{ + getAttestationVCMetadata(t, now, false), + }, + } + + assert.Equal(t, expected, got) + + return c.JSON(http.StatusOK, map[string]bool{"allowed": true}) + }) + }, + check: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "success OIDC4VP", + url: srv.URL + "/success_oidc4vp", + setup: func() { + // create wallet attestation VC with wallet DID as subject and attestation DID as issuer + attestationVC := createAttestationVC(t, attestationProofCreator, walletDID, now, false) + + // create dummy requested credential + requestedVC := createRequestedVC(t, now) + + // prepare wallet attestation VP (in jwt_vp format) signed by wallet DID + jwtVP = createAttestationVP(t, attestationVC, walletProofCreator, requestedVC) + + proofChecker = defaultProofChecker + + vcStatusVerifier.EXPECT().ValidateVCStatus(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + payloadBuilder = clientattestation.VerifierInteractionTrustRegistryPayloadBuilder + + handler.Add(http.MethodPost, "/success_oidc4vp", func(c echo.Context) error { + var got *clientattestation.VerifierInteractionValidationConfig + assert.NoError(t, c.Bind(&got)) + + attestationVCUniversalForm, err := attestationVC.ToUniversalForm() + assert.NoError(t, err) + + expected := &clientattestation.VerifierInteractionValidationConfig{ + AttestationVC: attestationVCUniversalForm, + Metadata: []*clientattestation.CredentialMetadata{ + getAttestationVCMetadata(t, now, false), + getRequestedVCMetadata(t, now), + }, + } + + assert.Equal(t, expected, got) + + return c.JSON(http.StatusOK, map[string]bool{"allowed": true}) + }) }, check: func(t *testing.T, err error) { require.NoError(t, err) @@ -96,7 +170,7 @@ func TestService_ValidateClientAttestationJWTVP(t *testing.T) { }, }, { - name: "missing attestation vc", + name: "attestation vc is not supplied", setup: func() { jwtVP = createAttestationVP(t, nil, walletProofCreator) @@ -105,13 +179,13 @@ func TestService_ValidateClientAttestationJWTVP(t *testing.T) { vcStatusVerifier.EXPECT().ValidateVCStatus(gomock.Any(), gomock.Any(), gomock.Any()).Times(0) }, check: func(t *testing.T, err error) { - require.ErrorContains(t, err, "missing attestation vc") + require.ErrorContains(t, err, "attestation vc is not supplied") }, }, { name: "attestation vc is expired", setup: func() { - attestationVC := createAttestationVC(t, attestationProofCreator, walletDID, true) + attestationVC := createAttestationVC(t, attestationProofCreator, walletDID, now, true) jwtVP = createAttestationVP(t, attestationVC, walletProofCreator) @@ -126,7 +200,7 @@ func TestService_ValidateClientAttestationJWTVP(t *testing.T) { { name: "attestation vc subject does not match vp signer", setup: func() { - attestationVC := createAttestationVC(t, attestationProofCreator, "invalid-subject", false) + attestationVC := createAttestationVC(t, attestationProofCreator, "invalid-subject", now, false) jwtVP = createAttestationVP(t, attestationVC, walletProofCreator) @@ -141,7 +215,7 @@ func TestService_ValidateClientAttestationJWTVP(t *testing.T) { { name: "fail to check attestation vc status", setup: func() { - attestationVC := createAttestationVC(t, attestationProofCreator, walletDID, false) + attestationVC := createAttestationVC(t, attestationProofCreator, walletDID, now, false) jwtVP = createAttestationVP(t, attestationVC, walletProofCreator) @@ -154,6 +228,75 @@ func TestService_ValidateClientAttestationJWTVP(t *testing.T) { require.ErrorContains(t, err, "validate attestation vc status") }, }, + { + name: "error payload builder", + setup: func() { + // create wallet attestation VC with wallet DID as subject and attestation DID as issuer + attestationVC := createAttestationVC(t, attestationProofCreator, walletDID, now, false) + + // prepare wallet attestation VP (in jwt_vp format) signed by wallet DID + jwtVP = createAttestationVP(t, attestationVC, walletProofCreator) + + proofChecker = defaultProofChecker + + vcStatusVerifier.EXPECT().ValidateVCStatus(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + payloadBuilder = func(_ *verifiable.Credential, _ *verifiable.Presentation) ([]byte, error) { + return nil, errors.New("some error") + } + }, + check: func(t *testing.T, err error) { + require.ErrorContains(t, err, "payload builder:") + }, + }, + { + name: "error doTrustRegistryRequest", + url: srv.URL + "/testcase2", + setup: func() { + // create wallet attestation VC with wallet DID as subject and attestation DID as issuer + attestationVC := createAttestationVC(t, attestationProofCreator, walletDID, now, false) + + // prepare wallet attestation VP (in jwt_vp format) signed by wallet DID + jwtVP = createAttestationVP(t, attestationVC, walletProofCreator) + + proofChecker = defaultProofChecker + + vcStatusVerifier.EXPECT().ValidateVCStatus(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + payloadBuilder = clientattestation.VerifierInteractionTrustRegistryPayloadBuilder + + handler.Add(http.MethodPost, "/testcase2", func(c echo.Context) error { + return c.NoContent(http.StatusBadRequest) + }) + }, + check: func(t *testing.T, err error) { + require.ErrorContains(t, err, "unexpected status code") + }, + }, + { + name: "Error interaction restricted", + url: srv.URL + "/testcase3", + setup: func() { + // create wallet attestation VC with wallet DID as subject and attestation DID as issuer + attestationVC := createAttestationVC(t, attestationProofCreator, walletDID, now, false) + + // prepare wallet attestation VP (in jwt_vp format) signed by wallet DID + jwtVP = createAttestationVP(t, attestationVC, walletProofCreator) + + proofChecker = defaultProofChecker + + vcStatusVerifier.EXPECT().ValidateVCStatus(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + payloadBuilder = clientattestation.VerifierInteractionTrustRegistryPayloadBuilder + + handler.Add(http.MethodPost, "/testcase3", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]bool{"allowed": false}) + }) + }, + check: func(t *testing.T, err error) { + require.ErrorIs(t, err, clientattestation.ErrInteractionRestricted) + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -162,12 +305,12 @@ func TestService_ValidateClientAttestationJWTVP(t *testing.T) { tt.check(t, clientattestation.NewService( &clientattestation.Config{ - HTTPClient: httpClient, + HTTPClient: http.DefaultClient, DocumentLoader: testutil.DocumentLoader(t), ProofChecker: proofChecker, VCStatusVerifier: vcStatusVerifier, }, - ).ValidateAttestationJWTVP(context.Background(), profile, jwtVP), + ).ValidateAttestationJWTVP(context.Background(), jwtVP, tt.url, payloadBuilder), ) }) } @@ -177,6 +320,45 @@ func createAttestationVC( t *testing.T, proofCreator jwt.ProofCreator, subject string, + now time.Time, + isExpired bool, +) *verifiable.Credential { + t.Helper() + + vcType := []string{verifiable.VCType, "WalletAttestationCredential"} + + vc := createVC(t, "attestationVCCredentialID", vcType, subject, attestationDID, now, isExpired) + + jwtVC, err := vc.CreateSignedJWTVC( + false, + verifiable.ECDSASecp256r1, + proofCreator, + attestationKeyID, + ) + require.NoError(t, err) + + return jwtVC +} + +func createRequestedVC( + t *testing.T, + now time.Time, +) *verifiable.Credential { + t.Helper() + + id := "requestedVCCredentialID" + types := []string{verifiable.VCType} + + return createVC(t, id, types, uuid.NewString(), walletDID, now.Round(time.Second), true) +} + +func createVC( + t *testing.T, + credentialID string, + types []string, + subjectID string, + issuerDID string, + now time.Time, isExpired bool, ) *verifiable.Credential { t.Helper() @@ -186,48 +368,38 @@ func createAttestationVC( verifiable.ContextURI, "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json", }, - ID: uuid.New().String(), - Types: []string{ - verifiable.VCType, - clientattestation.WalletAttestationVCType, - }, + ID: credentialID, + Types: types, Subject: []verifiable.Subject{ { - ID: subject, + ID: subjectID, }, }, Issuer: &verifiable.Issuer{ - ID: attestationDID, + ID: issuerDID, }, Issued: &utiltime.TimeWrapper{ - Time: time.Now(), + Time: now, }, } if isExpired { vcc.Expired = &utiltime.TimeWrapper{ - Time: time.Now().Add(-1 * time.Hour), + Time: now.Add(-time.Hour), } } vc, err := verifiable.CreateCredential(vcc, nil) require.NoError(t, err) - jwtVC, err := vc.CreateSignedJWTVC( - false, - verifiable.ECDSASecp256r1, - proofCreator, - attestationKeyID, - ) - require.NoError(t, err) - - return jwtVC + return vc } func createAttestationVP( t *testing.T, attestationVC *verifiable.Credential, proofCreator jwt.ProofCreator, + requestedVCs ...*verifiable.Credential, ) string { t.Helper() @@ -238,6 +410,8 @@ func createAttestationVP( vp.AddCredentials(attestationVC) } + vp.AddCredentials(requestedVCs...) + vp.ID = uuid.New().String() claims, err := vp.JWTClaims([]string{}, false) @@ -251,3 +425,37 @@ func createAttestationVP( return jws } + +func getAttestationVCMetadata(t *testing.T, now time.Time, expired bool) *clientattestation.CredentialMetadata { + t.Helper() + + var exp string + if expired { + exp = now.Add(-time.Hour).Format(time.RFC3339) + } + + return &clientattestation.CredentialMetadata{ + CredentialID: "attestationVCCredentialID", + Types: []string{ + "VerifiableCredential", + "WalletAttestationCredential", + }, + Issuer: attestationDID, + Issued: now.Format(time.RFC3339), + Expired: exp, + } +} + +func getRequestedVCMetadata(t *testing.T, now time.Time) *clientattestation.CredentialMetadata { + t.Helper() + + return &clientattestation.CredentialMetadata{ + CredentialID: "requestedVCCredentialID", + Types: []string{ + "VerifiableCredential", + }, + Issuer: walletDID, + Issued: now.Round(time.Second).Format(time.RFC3339), + Expired: now.Round(time.Second).Add(-time.Hour).Format(time.RFC3339), + } +} diff --git a/pkg/service/clientattestation/models.go b/pkg/service/clientattestation/models.go new file mode 100644 index 000000000..a00529b45 --- /dev/null +++ b/pkg/service/clientattestation/models.go @@ -0,0 +1,29 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package clientattestation + +type IssuerInteractionValidationConfig struct { + AttestationVC interface{} `json:"attestation_vc"` + Metadata []*CredentialMetadata `json:"metadata"` +} + +type VerifierInteractionValidationConfig struct { + AttestationVC interface{} `json:"attestation_vc"` + Metadata []*CredentialMetadata `json:"metadata"` +} + +type CredentialMetadata struct { + CredentialID string `json:"credential_id"` + Types []string `json:"types"` + Issuer string `json:"issuer"` + Issued string `json:"issued"` + Expired string `json:"expired"` +} + +type TrustRegistryResponse struct { + Allowed bool `json:"allowed"` +} diff --git a/pkg/service/clientattestation/trustregistry.go b/pkg/service/clientattestation/trustregistry.go new file mode 100644 index 000000000..ee21a5714 --- /dev/null +++ b/pkg/service/clientattestation/trustregistry.go @@ -0,0 +1,132 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package clientattestation + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/trustbloc/vc-go/verifiable" +) + +var ( + ErrInteractionRestricted = errors.New("interaction restricted") +) + +const ( + walletAttestationVCType = "WalletAttestationCredential" +) + +// TODO: update payloads +func IssuerInteractionTrustRegistryPayloadBuilder( + attestationVC *verifiable.Credential, + presentation *verifiable.Presentation, +) ([]byte, error) { + credentials := presentation.Credentials() + + presentationValidationConfig := &IssuerInteractionValidationConfig{ + Metadata: make([]*CredentialMetadata, len(credentials)), + } + + if uf, err := attestationVC.ToUniversalForm(); err == nil { + presentationValidationConfig.AttestationVC = uf + } + + for i, credential := range credentials { + content := credential.Contents() + + presentationValidationConfig.Metadata[i] = getCredentialMetadata(content) + } + + reqPayload, err := json.Marshal(presentationValidationConfig) + if err != nil { + return nil, fmt.Errorf("encode presentation config: %w", err) + } + + return reqPayload, nil +} + +// TODO: update payloads +func VerifierInteractionTrustRegistryPayloadBuilder( + attestationVC *verifiable.Credential, + presentation *verifiable.Presentation, +) ([]byte, error) { + credentials := presentation.Credentials() + + presentationValidationConfig := &VerifierInteractionValidationConfig{ + Metadata: make([]*CredentialMetadata, len(credentials)), + } + + if uf, err := attestationVC.ToUniversalForm(); err == nil { + presentationValidationConfig.AttestationVC = uf + } + + for i, credential := range credentials { + content := credential.Contents() + + presentationValidationConfig.Metadata[i] = getCredentialMetadata(content) + } + + reqPayload, err := json.Marshal(presentationValidationConfig) + if err != nil { + return nil, fmt.Errorf("encode presentation config: %w", err) + } + + return reqPayload, nil +} + +func (s *Service) doTrustRegistryRequest( + ctx context.Context, policyURL string, req []byte) (*TrustRegistryResponse, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodPost, policyURL, bytes.NewReader(req)) + if err != nil { + return nil, fmt.Errorf("create request: %w", err) + } + + request.Header.Add("content-type", "application/json") + + resp, err := s.httpClient.Do(request) + if err != nil { + return nil, fmt.Errorf("send request: %w", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var responseDecoded *TrustRegistryResponse + err = json.NewDecoder(resp.Body).Decode(&responseDecoded) + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + + return responseDecoded, nil +} + +func getCredentialMetadata(content verifiable.CredentialContents) *CredentialMetadata { + var iss, exp string + if content.Issued != nil { + iss = content.Issued.FormatToString() + } + + if content.Expired != nil { + exp = content.Expired.FormatToString() + } + + return &CredentialMetadata{ + CredentialID: content.ID, + Types: content.Types, + Issuer: content.Issuer.ID, + Issued: iss, + Expired: exp, + } +} diff --git a/pkg/service/clientattestation/trustregistry_test.go b/pkg/service/clientattestation/trustregistry_test.go new file mode 100644 index 000000000..8b19dd117 --- /dev/null +++ b/pkg/service/clientattestation/trustregistry_test.go @@ -0,0 +1,199 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package clientattestation + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + utiltime "github.com/trustbloc/did-go/doc/util/time" + "github.com/trustbloc/vc-go/verifiable" +) + +func TestService_doTrustRegistryRequest(t *testing.T) { + handler := echo.New() + + srv := httptest.NewServer(handler) + defer srv.Close() + + var reqBody []byte + var client httpClient + + tests := []struct { + name string + url string + setup func(t *testing.T) + check func(t *testing.T, trResponse *TrustRegistryResponse, err error) + }{ + { + name: "Success", + url: srv.URL + "/testcase1", + setup: func(t *testing.T) { + b, err := json.Marshal(map[string]string{"key": "value"}) + assert.NoError(t, err) + + reqBody = b + client = http.DefaultClient + + handler.Add(http.MethodPost, "/testcase1", func(c echo.Context) error { + assert.Equal(t, "application/json", c.Request().Header.Get("content-type")) + + var got map[string]string + assert.NoError(t, c.Bind(&got)) + assert.Equal(t, map[string]string{"key": "value"}, got) + + return c.JSON(http.StatusOK, map[string]bool{"allowed": true}) + }) + }, + check: func(t *testing.T, trResponse *TrustRegistryResponse, err error) { + assert.NoError(t, err) + assert.Equal(t, &TrustRegistryResponse{Allowed: true}, trResponse) + }, + }, + { + name: "Error create request", + url: " https://example.com", + setup: func(t *testing.T) { + client = &mockHTTPClient{} + }, + check: func(t *testing.T, trResponse *TrustRegistryResponse, err error) { + assert.ErrorContains(t, err, "create request") + assert.Nil(t, trResponse) + }, + }, + { + name: "Error do request", + url: srv.URL + "/testcase1", + setup: func(t *testing.T) { + client = &mockHTTPClient{} + }, + check: func(t *testing.T, trResponse *TrustRegistryResponse, err error) { + assert.ErrorContains(t, err, "send request: some error") + assert.Nil(t, trResponse) + }, + }, + { + name: "Error unexpected status code", + url: srv.URL + "/testcase2", + setup: func(t *testing.T) { + client = http.DefaultClient + + handler.Add(http.MethodPost, "/testcase2", func(c echo.Context) error { + return c.NoContent(http.StatusBadRequest) + }) + }, + check: func(t *testing.T, trResponse *TrustRegistryResponse, err error) { + assert.ErrorContains(t, err, "unexpected status code") + assert.Nil(t, trResponse) + }, + }, + { + name: "Error invalid response structure", + url: srv.URL + "/testcase3", + setup: func(t *testing.T) { + client = http.DefaultClient + + handler.Add(http.MethodPost, "/testcase3", func(c echo.Context) error { + assert.Equal(t, "application/json", c.Request().Header.Get("content-type")) + + return c.JSON(http.StatusOK, map[string]string{"allowed": "true"}) + }) + }, + check: func(t *testing.T, trResponse *TrustRegistryResponse, err error) { + assert.ErrorContains(t, err, "read response") + assert.Nil(t, trResponse) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup(t) + + rsp, err := (&Service{ + httpClient: client, + }).doTrustRegistryRequest(context.Background(), tt.url, reqBody) + + tt.check(t, rsp, err) + }) + } +} + +type mockHTTPClient struct { +} + +func (c *mockHTTPClient) Do(_ *http.Request) (*http.Response, error) { + return nil, errors.New("some error") +} + +func Test_getCredentialMetadata(t *testing.T) { + now := time.Now() + + type args struct { + content verifiable.CredentialContents + } + tests := []struct { + name string + args args + want *CredentialMetadata + }{ + { + name: "Success", + args: args{ + content: verifiable.CredentialContents{ + ID: "credentialID", + Types: []string{verifiable.VCType, "WalletAttestationCredential"}, + Issuer: &verifiable.Issuer{ID: "someIssuerID"}, + Issued: nil, + Expired: nil, + }, + }, + want: &CredentialMetadata{ + CredentialID: "credentialID", + Types: []string{verifiable.VCType, "WalletAttestationCredential"}, + Issuer: "someIssuerID", + Issued: "", + Expired: "", + }, + }, + { + name: "Success with iss and exp", + args: args{ + content: verifiable.CredentialContents{ + ID: "credentialID", + Types: []string{verifiable.VCType, "WalletAttestationCredential"}, + Issuer: &verifiable.Issuer{ID: "someIssuerID"}, + Issued: &utiltime.TimeWrapper{ + Time: now, + }, + Expired: &utiltime.TimeWrapper{ + Time: now.Add(time.Hour), + }, + }, + }, + want: &CredentialMetadata{ + CredentialID: "credentialID", + Types: []string{verifiable.VCType, "WalletAttestationCredential"}, + Issuer: "someIssuerID", + Issued: now.Format(time.RFC3339Nano), + Expired: now.Add(time.Hour).Format(time.RFC3339Nano), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := getCredentialMetadata(tt.args.content) + assert.Equalf(t, tt.want, actual, "getCredentialMetadata(%v)", tt.args.content) + }) + } +} diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index 457fc5d32..135ca1e10 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -30,6 +30,7 @@ import ( profileapi "github.com/trustbloc/vcs/pkg/profile" "github.com/trustbloc/vcs/pkg/restapi/resterr" "github.com/trustbloc/vcs/pkg/restapi/v1/common" + "github.com/trustbloc/vcs/pkg/service/clientattestation" ) const ( @@ -119,7 +120,11 @@ type jsonSchemaValidator interface { } type clientAttestationService interface { - ValidateAttestationJWTVP(ctx context.Context, profile *profileapi.Issuer, jwtVP string) error + ValidateAttestationJWTVP( + ctx context.Context, + jwtVP string, + policyURL string, + builder clientattestation.TrustRegistryPayloadBuilder) error } type ackStore interface { diff --git a/pkg/service/oidc4ci/oidc4ci_service_authenticate_client.go b/pkg/service/oidc4ci/oidc4ci_service_authenticate_client.go index 3db0d9a13..3e11fc5f8 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_authenticate_client.go +++ b/pkg/service/oidc4ci/oidc4ci_service_authenticate_client.go @@ -14,6 +14,7 @@ import ( "github.com/trustbloc/vcs/pkg/profile" "github.com/trustbloc/vcs/pkg/restapi/resterr" + "github.com/trustbloc/vcs/pkg/service/clientattestation" ) const attestJWTClientAuthType = "attest_jwt_client_auth" @@ -24,6 +25,10 @@ func (s *Service) AuthenticateClient( clientID, clientAssertionType, clientAssertion string) error { + if profile.Policy.URL == "" { + return nil + } + if profile.OIDCConfig == nil || !lo.Contains(profile.OIDCConfig.TokenEndpointAuthMethodsSupported, attestJWTClientAuthType) { return nil @@ -44,7 +49,11 @@ func (s *Service) AuthenticateClient( errors.New("client_assertion is required")) } - if err := s.clientAttestationService.ValidateAttestationJWTVP(ctx, profile, clientAssertion); err != nil { + if err := s.clientAttestationService.ValidateAttestationJWTVP( + ctx, + clientAssertion, + profile.Policy.URL, + clientattestation.IssuerInteractionTrustRegistryPayloadBuilder); err != nil { return resterr.NewCustomError(resterr.OIDCClientAuthenticationFailed, err) } diff --git a/pkg/service/oidc4ci/oidc4ci_service_authenticate_client_test.go b/pkg/service/oidc4ci/oidc4ci_service_authenticate_client_test.go index 67d091067..dfe048b52 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_authenticate_client_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_authenticate_client_test.go @@ -36,6 +36,7 @@ func TestService_AuthenticateClient(t *testing.T) { name: "success with client attestation jwt vp", setup: func() { profile = &profileapi.Issuer{ + Policy: profileapi.Policy{URL: "https://policy.example.com"}, OIDCConfig: &profileapi.OIDCConfig{ TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, }, @@ -48,15 +49,36 @@ func TestService_AuthenticateClient(t *testing.T) { clientAttestationService = NewMockClientAttestationService(gomock.NewController(t)) clientAttestationService.EXPECT().ValidateAttestationJWTVP( - gomock.Any(), - profile, + context.Background(), clientAssertion, + "https://policy.example.com", + gomock.Any(), ).Return(nil) }, check: func(t *testing.T, err error) { require.NoError(t, err) }, }, + { + name: "profile has no policy URL", + setup: func() { + profile = &profileapi.Issuer{ + Policy: profileapi.Policy{URL: ""}, + OIDCConfig: &profileapi.OIDCConfig{ + TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, + }, + } + + clientID = "client-id" + clientAssertionType = "attest_jwt_client_auth" + clientAssertion = "client-attestation-jwt-vp" + + clientAttestationService = NewMockClientAttestationService(gomock.NewController(t)) + }, + check: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, { name: "attest_jwt_client_auth not supported by profile", setup: func() { @@ -71,12 +93,6 @@ func TestService_AuthenticateClient(t *testing.T) { clientAssertion = "client-attestation-jwt-vp" clientAttestationService = NewMockClientAttestationService(gomock.NewController(t)) - - clientAttestationService.EXPECT().ValidateAttestationJWTVP( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Times(0) }, check: func(t *testing.T, err error) { require.NoError(t, err) @@ -86,6 +102,7 @@ func TestService_AuthenticateClient(t *testing.T) { name: "missing client_id", setup: func() { profile = &profileapi.Issuer{ + Policy: profileapi.Policy{URL: "https://policy.example.com"}, OIDCConfig: &profileapi.OIDCConfig{ TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, }, @@ -98,8 +115,9 @@ func TestService_AuthenticateClient(t *testing.T) { clientAttestationService = NewMockClientAttestationService(gomock.NewController(t)) clientAttestationService.EXPECT().ValidateAttestationJWTVP( - gomock.Any(), - gomock.Any(), + context.Background(), + clientAssertion, // + "", gomock.Any(), ).Times(0) }, @@ -111,6 +129,7 @@ func TestService_AuthenticateClient(t *testing.T) { name: "invalid client assertion type", setup: func() { profile = &profileapi.Issuer{ + Policy: profileapi.Policy{URL: "https://policy.example.com"}, OIDCConfig: &profileapi.OIDCConfig{ TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, }, @@ -123,8 +142,9 @@ func TestService_AuthenticateClient(t *testing.T) { clientAttestationService = NewMockClientAttestationService(gomock.NewController(t)) clientAttestationService.EXPECT().ValidateAttestationJWTVP( - gomock.Any(), - gomock.Any(), + context.Background(), + clientAssertion, // + "", gomock.Any(), ).Times(0) }, @@ -136,6 +156,7 @@ func TestService_AuthenticateClient(t *testing.T) { name: "empty client assertion", setup: func() { profile = &profileapi.Issuer{ + Policy: profileapi.Policy{URL: "https://policy.example.com"}, OIDCConfig: &profileapi.OIDCConfig{ TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, }, @@ -148,8 +169,9 @@ func TestService_AuthenticateClient(t *testing.T) { clientAttestationService = NewMockClientAttestationService(gomock.NewController(t)) clientAttestationService.EXPECT().ValidateAttestationJWTVP( - gomock.Any(), - gomock.Any(), + context.Background(), + clientAssertion, // + "", gomock.Any(), ).Times(0) }, @@ -161,6 +183,7 @@ func TestService_AuthenticateClient(t *testing.T) { name: "fail to validate client attestation jwt", setup: func() { profile = &profileapi.Issuer{ + Policy: profileapi.Policy{URL: "https://policy.example.com"}, OIDCConfig: &profileapi.OIDCConfig{ TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, }, @@ -173,8 +196,9 @@ func TestService_AuthenticateClient(t *testing.T) { clientAttestationService = NewMockClientAttestationService(gomock.NewController(t)) clientAttestationService.EXPECT().ValidateAttestationJWTVP( - gomock.Any(), - gomock.Any(), + context.Background(), + clientAssertion, // + "https://policy.example.com", gomock.Any(), ).Return(errors.New("validate error")) }, diff --git a/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go b/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go index 00152a609..3fe12b351 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go @@ -161,6 +161,7 @@ func TestExchangeCodeAuthenticateClientError(t *testing.T) { }, nil) profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profile.Issuer{ + Policy: profile.Policy{URL: "https://localhost/policy"}, OIDCConfig: &profile.OIDCConfig{ TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, }, diff --git a/pkg/service/oidc4ci/oidc4ci_service_test.go b/pkg/service/oidc4ci/oidc4ci_service_test.go index a850cd031..c47ea212c 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_test.go @@ -1157,6 +1157,7 @@ func TestValidatePreAuthCode(t *testing.T) { assert.NoError(t, err) profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profileapi.Issuer{ + Policy: profileapi.Policy{URL: "https://example.com"}, OIDCConfig: &profileapi.OIDCConfig{ TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, }, diff --git a/pkg/service/verifypresentation/testdata/client_attestation_vp.jsonld b/pkg/service/verifypresentation/testdata/client_attestation_vp.jsonld new file mode 100644 index 000000000..49e2eb934 --- /dev/null +++ b/pkg/service/verifypresentation/testdata/client_attestation_vp.jsonld @@ -0,0 +1,73 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://trustbloc.github.io/context/vc/examples-v1.jsonld", + "https://w3id.org/vc/status-list/2021/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5", + "type": "VerifiablePresentation", + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "id": "http://example.edu/credentials/58473", + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "issuer": "https://example.edu/issuers/14", + "issuanceDate": "2010-01-01T19:23:24Z", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "alumniOf": "Example University" + }, + "credentialStatus": { + "id": "urn:uuid:71d57ab4-9e3a-4267-a3fd-e64d661c0df3", + "statusListCredential": "https://test.com/2.json", + "statusListIndex": "17", + "statusPurpose": "revocation", + "type": "StatusList2021Entry" + }, + "proof": { + "type": "RsaSignature2018" + } + }, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://www.w3.org/2022/credentials/walletAttestation/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "id": "http://example.edu/credentials/58473", + "type": [ + "VerifiableCredential", + "WalletAttestationCredential" + ], + "issuer": "https://example.edu/issuers/14", + "issuanceDate": "2010-01-01T19:23:24Z", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "key_type": "key_type", + "user_authentication": "user_authentication", + "assurance_level": "assurance_level" + }, + "credentialStatus": { + "id": "urn:uuid:3cv7ab4-9e3a-4267-a3fd-e64d66f1c0df3", + "statusListCredential": "https://test.com/2.json", + "statusListIndex": "17", + "statusPurpose": "revocation", + "type": "StatusList2021Entry" + }, + "proof": { + "type": "RsaSignature2018" + } + } + ], + "holder": "did:trustblock:abc" +} diff --git a/pkg/service/verifypresentation/testdata/valid_vp.jsonld b/pkg/service/verifypresentation/testdata/requested_credentials_vp.jsonld similarity index 75% rename from pkg/service/verifypresentation/testdata/valid_vp.jsonld rename to pkg/service/verifypresentation/testdata/requested_credentials_vp.jsonld index fcc1c826b..34fdb95e2 100644 --- a/pkg/service/verifypresentation/testdata/valid_vp.jsonld +++ b/pkg/service/verifypresentation/testdata/requested_credentials_vp.jsonld @@ -16,7 +16,10 @@ "https://w3id.org/vc/status-list/2021/v1" ], "id": "http://example.edu/credentials/58473", - "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], "issuer": "https://example.edu/issuers/14", "issuanceDate": "2010-01-01T19:23:24Z", "credentialSubject": { @@ -24,11 +27,11 @@ "alumniOf": "Example University" }, "credentialStatus": { - "id": "urn:uuid:71d57ab4-9e3a-4267-a3fd-e64d661c0df3", - "statusListCredential": "https://test.com/2.json", - "statusListIndex": "17", - "statusPurpose": "revocation", - "type": "StatusList2021Entry" + "id": "urn:uuid:71d57ab4-9e3a-4267-a3fd-e64d661c0df3", + "statusListCredential": "https://test.com/2.json", + "statusListIndex": "17", + "statusPurpose": "revocation", + "type": "StatusList2021Entry" }, "proof": { "type": "RsaSignature2018" diff --git a/pkg/service/verifypresentation/verifypresentation_service.go b/pkg/service/verifypresentation/verifypresentation_service.go index de95c18c2..ed6eae343 100644 --- a/pkg/service/verifypresentation/verifypresentation_service.go +++ b/pkg/service/verifypresentation/verifypresentation_service.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -//go:generate mockgen -destination service_mocks_test.go -self_package mocks -package verifypresentation -source=verifypresentation_service.go -mock_names vcVerifier=MockVcVerifier +//go:generate mockgen -destination service_mocks_test.go -self_package mocks -package verifypresentation -source=verifypresentation_service.go -mock_names vcVerifier=MockVcVerifier,clientAttestationService=MockClientAttestationService package verifypresentation @@ -27,6 +27,11 @@ import ( "github.com/trustbloc/vcs/pkg/doc/vc/crypto" "github.com/trustbloc/vcs/pkg/internal/common/diddoc" profileapi "github.com/trustbloc/vcs/pkg/profile" + "github.com/trustbloc/vcs/pkg/service/clientattestation" +) + +const ( + walletAttestationVCType = "WalletAttestationCredential" ) type vcVerifier interface { @@ -35,30 +40,34 @@ type vcVerifier interface { ValidateLinkedDomain(ctx context.Context, signingDID string) error } -type trustRegistry interface { - ValidatePresentation(policyID string, presentationCredentials []*verifiable.Credential) error +type clientAttestationService interface { + ValidateAttestationJWTVP( + ctx context.Context, + jwtVP string, + policyURL string, + payloadBuilder clientattestation.TrustRegistryPayloadBuilder) error } type Config struct { - VDR vdrapi.Registry - DocumentLoader ld.DocumentLoader - VcVerifier vcVerifier - TrustRegistry trustRegistry + VDR vdrapi.Registry + DocumentLoader ld.DocumentLoader + VcVerifier vcVerifier + ClientAttestationService clientAttestationService } type Service struct { - vdr vdrapi.Registry - documentLoader ld.DocumentLoader - vcVerifier vcVerifier - trustRegistry trustRegistry + vdr vdrapi.Registry + documentLoader ld.DocumentLoader + vcVerifier vcVerifier + clientAttestationService clientAttestationService } func New(config *Config) *Service { return &Service{ - vdr: config.VDR, - documentLoader: config.DocumentLoader, - vcVerifier: config.VcVerifier, - trustRegistry: config.TrustRegistry, + vdr: config.VDR, + documentLoader: config.DocumentLoader, + vcVerifier: config.VcVerifier, + clientAttestationService: config.ClientAttestationService, } } @@ -85,6 +94,24 @@ func (s *Service) VerifyPresentation( //nolint:funlen,gocognit var targetPresentation interface{} targetPresentation = presentation + trustRegistryValidationEnabled := profile.Policy.URL != "" + + if trustRegistryValidationEnabled { + st := time.Now() + + // Attestation VC validation. + err := s.clientAttestationService.ValidateAttestationJWTVP( + ctx, presentation.JWT, profile.Policy.URL, clientattestation.VerifierInteractionTrustRegistryPayloadBuilder) + if err != nil { + result = append(result, PresentationVerificationCheckResult{ + Check: "clientAttestation", + Error: err.Error(), + }) + } + + logger.Debugc(ctx, "Checks.Policy", log.WithDuration(time.Since(st))) + } + if profile.Checks.Presentation.Proof { st := time.Now() @@ -112,8 +139,9 @@ func (s *Service) VerifyPresentation( //nolint:funlen,gocognit }) } } + if profile.Checks.Credential.CredentialExpiry { - err := s.checkCredentialExpiry(ctx, credentials) + err := s.checkCredentialExpiry(ctx, credentials, trustRegistryValidationEnabled) if err != nil { result = append(result, PresentationVerificationCheckResult{ Check: "credentialExpiry", @@ -125,7 +153,7 @@ func (s *Service) VerifyPresentation( //nolint:funlen,gocognit if profile.Checks.Credential.Proof { st := time.Now() - err := s.validateCredentialsProof(ctx, presentation.JWT, credentials) + err := s.validateCredentialsProof(ctx, presentation.JWT, credentials, trustRegistryValidationEnabled) if err != nil { result = append(result, PresentationVerificationCheckResult{ Check: "credentialProof", @@ -138,7 +166,8 @@ func (s *Service) VerifyPresentation( //nolint:funlen,gocognit if profile.Checks.Credential.Status { st := time.Now() - err := s.validateCredentialsStatus(ctx, credentials) + + err := s.validateCredentialsStatus(ctx, credentials, trustRegistryValidationEnabled) if err != nil { result = append(result, PresentationVerificationCheckResult{ Check: "credentialStatus", @@ -231,9 +260,17 @@ func (s *Service) checkCredentialStrict( return claimKeysDict, nil } -func (s *Service) checkCredentialExpiry(_ context.Context, credentials []*verifiable.Credential) error { +func (s *Service) checkCredentialExpiry( + _ context.Context, + credentials []*verifiable.Credential, + trustRegistryValidationEnabled bool) error { for _, credential := range credentials { vcc := credential.Contents() + + if trustRegistryValidationEnabled && lo.Contains(vcc.Types, walletAttestationVCType) { + continue + } + if vcc.Expired != nil && time.Now().UTC().After(vcc.Expired.Time) { return errors.New("credential expired") } @@ -353,8 +390,13 @@ func (s *Service) validateCredentialsProof( ctx context.Context, vpJWT string, credentials []*verifiable.Credential, + trustRegistryValidationEnabled bool, ) error { for _, cred := range credentials { + if trustRegistryValidationEnabled && lo.Contains(cred.Contents().Types, walletAttestationVCType) { + continue + } + err := s.vcVerifier.ValidateCredentialProof(ctx, cred, "", "", true, vpJWT == "") if err != nil { return err @@ -367,12 +409,17 @@ func (s *Service) validateCredentialsProof( func (s *Service) validateCredentialsStatus( ctx context.Context, credentials []*verifiable.Credential, + trustRegistryValidationEnabled bool, ) error { for _, cred := range credentials { - extractedType, issuer := s.extractCredentialStatus(cred) + if trustRegistryValidationEnabled && lo.Contains(cred.Contents().Types, walletAttestationVCType) { + continue + } + + typedID, issuer := s.extractCredentialStatus(cred) - if extractedType != nil { - err := s.vcVerifier.ValidateVCStatus(ctx, extractedType, issuer) + if typedID != nil { + err := s.vcVerifier.ValidateVCStatus(ctx, typedID, issuer) if err != nil { return err } diff --git a/pkg/service/verifypresentation/verifypresentation_service_test.go b/pkg/service/verifypresentation/verifypresentation_service_test.go index f6dca76ab..51bd76af0 100644 --- a/pkg/service/verifypresentation/verifypresentation_service_test.go +++ b/pkg/service/verifypresentation/verifypresentation_service_test.go @@ -10,11 +10,14 @@ import ( "context" _ "embed" "errors" + "fmt" "reflect" "testing" + "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + timeutil "github.com/trustbloc/did-go/doc/util/time" vdrapi "github.com/trustbloc/did-go/vdr/api" mockvdr "github.com/trustbloc/did-go/vdr/mock" "github.com/trustbloc/vc-go/sdjwt/common" @@ -27,11 +30,15 @@ import ( ) var ( - //go:embed testdata/valid_vp.jsonld - sampleVPJsonLD string + //go:embed testdata/requested_credentials_vp.jsonld + requestedCredentialsVP []byte + //go:embed testdata/client_attestation_vp.jsonld + clientAttestationVP []byte ) func TestNew(t *testing.T) { + ctrl := gomock.NewController(t) + type args struct { config *Config } @@ -44,15 +51,17 @@ func TestNew(t *testing.T) { name: "OK", args: args{ config: &Config{ - VDR: &mockvdr.VDRegistry{}, - DocumentLoader: testutil.DocumentLoader(t), - VcVerifier: NewMockVcVerifier(gomock.NewController(t)), + VDR: &mockvdr.VDRegistry{}, + DocumentLoader: testutil.DocumentLoader(t), + VcVerifier: NewMockVcVerifier(ctrl), + ClientAttestationService: NewMockClientAttestationService(ctrl), }, }, want: &Service{ - vdr: &mockvdr.VDRegistry{}, - documentLoader: testutil.DocumentLoader(t), - vcVerifier: NewMockVcVerifier(gomock.NewController(t)), + vdr: &mockvdr.VDRegistry{}, + documentLoader: testutil.DocumentLoader(t), + vcVerifier: NewMockVcVerifier(ctrl), + clientAttestationService: NewMockClientAttestationService(ctrl), }, }, } @@ -67,14 +76,16 @@ func TestNew(t *testing.T) { func TestService_VerifyPresentation(t *testing.T) { loader := testutil.DocumentLoader(t) - signedVPResult := testutil.SignedVP(t, []byte(sampleVPJsonLD), vcs.Ldp) + signedClientAttestationVP := testutil.SignedVP(t, clientAttestationVP, vcs.Ldp) + signedRequestedCredentialsVP := testutil.SignedVP(t, requestedCredentialsVP, vcs.Ldp) type fields struct { - getVDR func() vdrapi.Registry - getVcVerifier func() vcVerifier + getVDR func() vdrapi.Registry + getVcVerifier func(t *testing.T) vcVerifier + getClientAttestationSrv func(t *testing.T) clientAttestationService } type args struct { - getPresentation func() *verifiable.Presentation + getPresentation func(t *testing.T) *verifiable.Presentation profile *profileapi.Verifier opts *Options } @@ -87,12 +98,12 @@ func TestService_VerifyPresentation(t *testing.T) { wantErr bool }{ { - name: "OK", + name: "OK with Trust registry validation enabled and client attestation VC included in VP", fields: fields{ getVDR: func() vdrapi.Registry { - return signedVPResult.VDR + return signedClientAttestationVP.VDR }, - getVcVerifier: func() vcVerifier { + getVcVerifier: func(t *testing.T) vcVerifier { mockVerifier := NewMockVcVerifier(gomock.NewController(t)) mockVerifier.EXPECT().ValidateCredentialProof( gomock.Any(), @@ -110,12 +121,25 @@ func TestService_VerifyPresentation(t *testing.T) { gomock.Any()).Times(1).Return(nil) return mockVerifier }, + getClientAttestationSrv: func(t *testing.T) clientAttestationService { + tr := NewMockClientAttestationService(gomock.NewController(t)) + + tr.EXPECT().ValidateAttestationJWTVP( + context.Background(), + gomock.Any(), + "https://trustregistry.example.com", + gomock.Any(), + ).Return(nil) + + return tr + }, }, args: args{ - getPresentation: func() *verifiable.Presentation { - return signedVPResult.Presentation + getPresentation: func(t *testing.T) *verifiable.Presentation { + return signedClientAttestationVP.Presentation }, profile: &profileapi.Verifier{ + Policy: profileapi.Policy{URL: "https://trustregistry.example.com"}, SigningDID: &profileapi.SigningDID{DID: "did:key:abc"}, Checks: &profileapi.VerificationChecks{ Presentation: &profileapi.PresentationChecks{ @@ -144,12 +168,12 @@ func TestService_VerifyPresentation(t *testing.T) { wantErr: false, }, { - name: "OK with credential type", + name: "OK with Trust registry disabled enabled and client attestation VC included in VP", fields: fields{ getVDR: func() vdrapi.Registry { - return signedVPResult.VDR + return signedClientAttestationVP.VDR }, - getVcVerifier: func() vcVerifier { + getVcVerifier: func(t *testing.T) vcVerifier { mockVerifier := NewMockVcVerifier(gomock.NewController(t)) mockVerifier.EXPECT().ValidateCredentialProof( gomock.Any(), @@ -157,20 +181,23 @@ func TestService_VerifyPresentation(t *testing.T) { gomock.Any(), gomock.Any(), gomock.Any(), - gomock.Any()).Times(1).Return(nil) + gomock.Any()).Times(2).Return(nil) mockVerifier.EXPECT().ValidateVCStatus( context.Background(), gomock.Any(), - gomock.Any()).Times(1).Return(nil) + gomock.Any()).Times(2).Return(nil) mockVerifier.EXPECT().ValidateLinkedDomain( context.Background(), gomock.Any()).Times(1).Return(nil) return mockVerifier }, + getClientAttestationSrv: func(t *testing.T) clientAttestationService { + return NewMockClientAttestationService(gomock.NewController(t)) + }, }, args: args{ - getPresentation: func() *verifiable.Presentation { - return signedVPResult.Presentation + getPresentation: func(t *testing.T) *verifiable.Presentation { + return signedClientAttestationVP.Presentation }, profile: &profileapi.Verifier{ SigningDID: &profileapi.SigningDID{DID: "did:key:abc"}, @@ -187,11 +214,7 @@ func TestService_VerifyPresentation(t *testing.T) { CredentialExpiry: true, Strict: true, IssuerTrustList: map[string]profileapi.TrustList{ - "https://example.edu/issuers/14": { - CredentialTypes: []string{ - "UniversityDegreeCredential", - }, - }, + "https://example.edu/issuers/14": {}, }, }, }, @@ -208,9 +231,9 @@ func TestService_VerifyPresentation(t *testing.T) { name: "Err credential type not in trust list", fields: fields{ getVDR: func() vdrapi.Registry { - return signedVPResult.VDR + return signedRequestedCredentialsVP.VDR }, - getVcVerifier: func() vcVerifier { + getVcVerifier: func(t *testing.T) vcVerifier { mockVerifier := NewMockVcVerifier(gomock.NewController(t)) mockVerifier.EXPECT().ValidateCredentialProof( gomock.Any(), @@ -228,10 +251,13 @@ func TestService_VerifyPresentation(t *testing.T) { gomock.Any()).Times(1).Return(nil) return mockVerifier }, + getClientAttestationSrv: func(t *testing.T) clientAttestationService { + return nil + }, }, args: args{ - getPresentation: func() *verifiable.Presentation { - return signedVPResult.Presentation + getPresentation: func(t *testing.T) *verifiable.Presentation { + return signedRequestedCredentialsVP.Presentation }, profile: &profileapi.Verifier{ SigningDID: &profileapi.SigningDID{DID: "did:key:abc"}, @@ -276,12 +302,15 @@ func TestService_VerifyPresentation(t *testing.T) { getVDR: func() vdrapi.Registry { return nil }, - getVcVerifier: func() vcVerifier { + getVcVerifier: func(t *testing.T) vcVerifier { + return nil + }, + getClientAttestationSrv: func(t *testing.T) clientAttestationService { return nil }, }, args: args{ - getPresentation: func() *verifiable.Presentation { + getPresentation: func(t *testing.T) *verifiable.Presentation { return nil }, profile: &profileapi.Verifier{ @@ -303,12 +332,12 @@ func TestService_VerifyPresentation(t *testing.T) { wantErr: false, }, { - name: "Error credentials", + name: "Error all checks", fields: fields{ getVDR: func() vdrapi.Registry { - return signedVPResult.VDR + return signedClientAttestationVP.VDR }, - getVcVerifier: func() vcVerifier { + getVcVerifier: func(t *testing.T) vcVerifier { mockVerifier := NewMockVcVerifier(gomock.NewController(t)) mockVerifier.EXPECT().ValidateCredentialProof( gomock.Any(), @@ -326,12 +355,25 @@ func TestService_VerifyPresentation(t *testing.T) { gomock.Any()).Times(1).Return(errors.New("some error")) return mockVerifier }, + getClientAttestationSrv: func(t *testing.T) clientAttestationService { + ca := NewMockClientAttestationService(gomock.NewController(t)) + + ca.EXPECT().ValidateAttestationJWTVP( + context.Background(), + gomock.Any(), + "https://trustregistry.example.com", + gomock.Any(), + ).Return(errors.New("some error")) + + return ca + }, }, args: args{ - getPresentation: func() *verifiable.Presentation { - return signedVPResult.Presentation + getPresentation: func(t *testing.T) *verifiable.Presentation { + return signedClientAttestationVP.Presentation }, profile: &profileapi.Verifier{ + Policy: profileapi.Policy{URL: "https://trustregistry.example.com"}, SigningDID: &profileapi.SigningDID{DID: "did:key:abc"}, Checks: &profileapi.VerificationChecks{ Presentation: &profileapi.PresentationChecks{ @@ -355,6 +397,10 @@ func TestService_VerifyPresentation(t *testing.T) { }, }, want: []PresentationVerificationCheckResult{ + { + Check: "clientAttestation", + Error: "some error", + }, { Check: "issuerTrustList", Error: "issuer with id: https://example.edu/issuers/14 is not a member of trustlist", @@ -378,12 +424,13 @@ func TestService_VerifyPresentation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &Service{ - vdr: tt.fields.getVDR(), - documentLoader: loader, - vcVerifier: tt.fields.getVcVerifier(), + vdr: tt.fields.getVDR(), + documentLoader: loader, + vcVerifier: tt.fields.getVcVerifier(t), + clientAttestationService: tt.fields.getClientAttestationSrv(t), } - got, _, err := s.VerifyPresentation(context.Background(), tt.args.getPresentation(), tt.args.opts, tt.args.profile) + got, _, err := s.VerifyPresentation(context.Background(), tt.args.getPresentation(t), tt.args.opts, tt.args.profile) if (err != nil) != tt.wantErr { t.Errorf("VerifyPresentation() error = %v, wantErr %v", err, tt.wantErr) return @@ -397,7 +444,7 @@ func TestService_VerifyPresentation(t *testing.T) { func TestService_validatePresentationProof(t *testing.T) { loader := testutil.DocumentLoader(t) - signedVPResult := testutil.SignedVP(t, []byte(sampleVPJsonLD), vcs.Ldp) + signedVPResult := testutil.SignedVP(t, requestedCredentialsVP, vcs.Ldp) type fields struct { getVDR func() vdrapi.Registry @@ -489,7 +536,7 @@ func TestService_validatePresentationProof(t *testing.T) { } func TestService_validateProofData(t *testing.T) { - signedVPResult := testutil.SignedVP(t, []byte(sampleVPJsonLD), vcs.Ldp) + signedVPResult := testutil.SignedVP(t, requestedCredentialsVP, vcs.Ldp) type fields struct { vdr vdrapi.Registry } @@ -707,15 +754,12 @@ func TestService_validateProofData(t *testing.T) { } func TestService_validateCredentialsProof(t *testing.T) { - loader := testutil.DocumentLoader(t) - signedVPResult := testutil.SignedVP(t, []byte(sampleVPJsonLD), vcs.Jwt) - type fields struct { - getVDR func() vdrapi.Registry - getVcVerifier func() vcVerifier + getVcVerifier func(t *testing.T) vcVerifier } type args struct { - getVp func() *verifiable.Presentation + trustRegistryValidationEnabled bool + getCredentials func(t *testing.T) []*verifiable.Credential } tests := []struct { name string @@ -724,12 +768,9 @@ func TestService_validateCredentialsProof(t *testing.T) { wantErr bool }{ { - name: "OK", + name: "OK with trustRegistryValidationEnabled == true and Wallet Attestation VC included", fields: fields{ - getVDR: func() vdrapi.Registry { - return signedVPResult.VDR - }, - getVcVerifier: func() vcVerifier { + getVcVerifier: func(t *testing.T) vcVerifier { mockVerifier := NewMockVcVerifier(gomock.NewController(t)) mockVerifier.EXPECT().ValidateCredentialProof( gomock.Any(), @@ -742,19 +783,80 @@ func TestService_validateCredentialsProof(t *testing.T) { }, }, args: args{ - getVp: func() *verifiable.Presentation { - return signedVPResult.Presentation + trustRegistryValidationEnabled: true, + getCredentials: func(t *testing.T) []*verifiable.Credential { + credContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "UniversityDegreeCredential", + }, + } + + credential, err := verifiable.CreateCredential(credContent, nil) + assert.NoError(t, err) + + attestationVCContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "WalletAttestationCredential", + }, + } + + attestationVC, err := verifiable.CreateCredential(attestationVCContent, nil) + assert.NoError(t, err) + + return []*verifiable.Credential{credential, attestationVC} }, }, wantErr: false, }, { - name: "Error ValidateCredentialProof", + name: "OK with trustRegistryValidationEnabled == false and Wallet Attestation VC included", fields: fields{ - getVDR: func() vdrapi.Registry { - return signedVPResult.VDR + getVcVerifier: func(t *testing.T) vcVerifier { + mockVerifier := NewMockVcVerifier(gomock.NewController(t)) + mockVerifier.EXPECT().ValidateCredentialProof( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any()).Times(2).Return(nil) + return mockVerifier + }, + }, + args: args{ + trustRegistryValidationEnabled: false, + getCredentials: func(t *testing.T) []*verifiable.Credential { + credContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "UniversityDegreeCredential", + }, + } + + credential, err := verifiable.CreateCredential(credContent, nil) + assert.NoError(t, err) + + attestationVCContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "WalletAttestationCredential", + }, + } + + attestationVC, err := verifiable.CreateCredential(attestationVCContent, nil) + assert.NoError(t, err) + + return []*verifiable.Credential{credential, attestationVC} }, - getVcVerifier: func() vcVerifier { + }, + wantErr: false, + }, + { + name: "Error", + fields: fields{ + getVcVerifier: func(t *testing.T) vcVerifier { mockVerifier := NewMockVcVerifier(gomock.NewController(t)) mockVerifier.EXPECT().ValidateCredentialProof( gomock.Any(), @@ -767,8 +869,19 @@ func TestService_validateCredentialsProof(t *testing.T) { }, }, args: args{ - getVp: func() *verifiable.Presentation { - return signedVPResult.Presentation + trustRegistryValidationEnabled: false, + getCredentials: func(t *testing.T) []*verifiable.Credential { + credContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "UniversityDegreeCredential", + }, + } + + credential, err := verifiable.CreateCredential(credContent, nil) + assert.NoError(t, err) + + return []*verifiable.Credential{credential} }, }, wantErr: true, @@ -777,15 +890,14 @@ func TestService_validateCredentialsProof(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &Service{ - vdr: tt.fields.getVDR(), - documentLoader: loader, - vcVerifier: tt.fields.getVcVerifier(), + vcVerifier: tt.fields.getVcVerifier(t), } if err := s.validateCredentialsProof( context.Background(), - tt.args.getVp().JWT, - tt.args.getVp().Credentials(), + "", + tt.args.getCredentials(t), + tt.args.trustRegistryValidationEnabled, ); (err != nil) != tt.wantErr { t.Errorf("validateCredentialsProof() error = %v, wantErr %v", err, tt.wantErr) } @@ -794,15 +906,12 @@ func TestService_validateCredentialsProof(t *testing.T) { } func TestService_validateCredentialsStatus(t *testing.T) { - loader := testutil.DocumentLoader(t) - signedVPResult := testutil.SignedVP(t, []byte(sampleVPJsonLD), vcs.Jwt) - type fields struct { - getVDR func() vdrapi.Registry - getVcVerifier func() vcVerifier + getVcVerifier func(t *testing.T) vcVerifier } type args struct { - getVp func() *verifiable.Presentation + getCredentials func(t *testing.T) []*verifiable.Credential + trustRegistryValidationEnabled bool } tests := []struct { name string @@ -811,45 +920,137 @@ func TestService_validateCredentialsStatus(t *testing.T) { wantErr bool }{ { - name: "OK", + name: "OK with trustRegistryValidationEnabled == true and Wallet Attestation VC", fields: fields{ - getVDR: func() vdrapi.Registry { - return signedVPResult.VDR + getVcVerifier: func(t *testing.T) vcVerifier { + mockVerifier := NewMockVcVerifier(gomock.NewController(t)) + mockVerifier.EXPECT().ValidateVCStatus( + context.Background(), + &verifiable.TypedID{ID: "TypedID"}, + &verifiable.Issuer{ID: "IssuerID"}, + ).Times(1).Return(nil) + return mockVerifier + }, + }, + args: args{ + trustRegistryValidationEnabled: true, + getCredentials: func(t *testing.T) []*verifiable.Credential { + credContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "UniversityDegreeCredential", + }, + Status: &verifiable.TypedID{ID: "TypedID"}, + Issuer: &verifiable.Issuer{ID: "IssuerID"}, + } + + cred1, err := verifiable.CreateCredential(credContent, nil) + assert.NoError(t, err) + + attestationVCContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "WalletAttestationCredential", + }, + Status: &verifiable.TypedID{ID: "TypedID"}, + Issuer: &verifiable.Issuer{ID: "IssuerID"}, + } + + attestationVC, err := verifiable.CreateCredential(attestationVCContent, nil) + assert.NoError(t, err) + + return []*verifiable.Credential{cred1, attestationVC} }, - getVcVerifier: func() vcVerifier { + }, + wantErr: false, + }, + { + name: "OK with trustRegistryValidationEnabled == false and Wallet Attestation VC", + fields: fields{ + getVcVerifier: func(t *testing.T) vcVerifier { mockVerifier := NewMockVcVerifier(gomock.NewController(t)) mockVerifier.EXPECT().ValidateVCStatus( context.Background(), - gomock.Any(), - gomock.Any()).Times(1).Return(nil) + &verifiable.TypedID{ID: "TypedID"}, + &verifiable.Issuer{ID: "IssuerID"}, + ).Times(1).Return(nil) return mockVerifier }, }, args: args{ - getVp: func() *verifiable.Presentation { - return signedVPResult.Presentation + trustRegistryValidationEnabled: false, + getCredentials: func(t *testing.T) []*verifiable.Credential { + attestationVCContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "WalletAttestationCredential", + }, + Status: &verifiable.TypedID{ID: "TypedID"}, + Issuer: &verifiable.Issuer{ID: "IssuerID"}, + } + + attestationVC, err := verifiable.CreateCredential(attestationVCContent, nil) + assert.NoError(t, err) + + return []*verifiable.Credential{attestationVC} }, }, wantErr: false, }, { - name: "Error ValidateVCStatus", + name: "OK with empty typedID", fields: fields{ - getVDR: func() vdrapi.Registry { - return signedVPResult.VDR + getVcVerifier: func(t *testing.T) vcVerifier { + return NewMockVcVerifier(gomock.NewController(t)) + }, + }, + args: args{ + trustRegistryValidationEnabled: false, + getCredentials: func(t *testing.T) []*verifiable.Credential { + credContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "UniversityDegreeCredential", + }, + Issuer: &verifiable.Issuer{ID: "IssuerID"}, + } + + cred1, err := verifiable.CreateCredential(credContent, nil) + assert.NoError(t, err) + + return []*verifiable.Credential{cred1} }, - getVcVerifier: func() vcVerifier { + }, + wantErr: false, + }, + { + name: "Error ValidateVCStatus", + fields: fields{ + getVcVerifier: func(t *testing.T) vcVerifier { mockVerifier := NewMockVcVerifier(gomock.NewController(t)) mockVerifier.EXPECT().ValidateVCStatus( context.Background(), - gomock.Any(), - gomock.Any()).Times(1).Return(errors.New("some error")) + &verifiable.TypedID{ID: "TypedID"}, + &verifiable.Issuer{ID: "IssuerID"}, + ).Times(1).Return(errors.New("some error")) return mockVerifier }, }, args: args{ - getVp: func() *verifiable.Presentation { - return signedVPResult.Presentation + getCredentials: func(t *testing.T) []*verifiable.Credential { + credContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "UniversityDegreeCredential", + }, + Status: &verifiable.TypedID{ID: "TypedID"}, + Issuer: &verifiable.Issuer{ID: "IssuerID"}, + } + + cred1, err := verifiable.CreateCredential(credContent, nil) + assert.NoError(t, err) + + return []*verifiable.Credential{cred1} }, }, wantErr: true, @@ -858,12 +1059,12 @@ func TestService_validateCredentialsStatus(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &Service{ - vdr: tt.fields.getVDR(), - documentLoader: loader, - vcVerifier: tt.fields.getVcVerifier(), + vcVerifier: tt.fields.getVcVerifier(t), } - if err := s.validateCredentialsStatus(context.Background(), - tt.args.getVp().Credentials()); (err != nil) != tt.wantErr { + if err := s.validateCredentialsStatus( + context.Background(), + tt.args.getCredentials(t), + tt.args.trustRegistryValidationEnabled); (err != nil) != tt.wantErr { t.Errorf("validateCredentialsStatus() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -924,6 +1125,42 @@ func TestCredentialStrict(t *testing.T) { func TestCheckTrustList(t *testing.T) { s := New(&Config{}) + t.Run("Success - attestation credential with invalid issuer ignored", func(t *testing.T) { + credContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "UniversityDegreeCredential", + }, + Issuer: &verifiable.Issuer{ + ID: "a", + }} + + attestationVCContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "WalletAttestationCredential", + }, + Issuer: &verifiable.Issuer{ + ID: "123432123", + }} + + cred1, err := verifiable.CreateCredential(credContent, nil) + assert.NoError(t, err) + + attestationVC, err := verifiable.CreateCredential(attestationVCContent, nil) + assert.NoError(t, err) + + err = s.checkIssuerTrustList( + context.TODO(), + []*verifiable.Credential{cred1, attestationVC}, + map[string]profileapi.TrustList{ + "a": {}, + }, + ) + + assert.ErrorContains(t, err, "issuer with id: 123432123 is not a member of trustlist") + }) + t.Run("from credentials v1 trust list", func(t *testing.T) { credContent := verifiable.CredentialContents{ Types: []string{ @@ -948,3 +1185,74 @@ func TestCheckTrustList(t *testing.T) { assert.ErrorContains(t, err, "issuer with id: 123432123 is not a member of trustlist") }) } + +func TestService_checkCredentialExpiry(t *testing.T) { + tests := []struct { + name string + getCredentials func() []*verifiable.Credential + wantErr assert.ErrorAssertionFunc + trustRegistryValidationEnabled bool + }{ + { + name: "Success with trustRegistryValidationEnabled == true and expired Wallet Attestation VC", + getCredentials: func() []*verifiable.Credential { + credContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "UniversityDegreeCredential", + }, + Expired: timeutil.NewTime(time.Now().Add(time.Hour)), + } + + attestationVCContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "WalletAttestationCredential", + }, + Expired: timeutil.NewTime(time.Now().Add(-time.Hour)), + } + + cred1, err := verifiable.CreateCredential(credContent, nil) + assert.NoError(t, err) + + attestationVC, err := verifiable.CreateCredential(attestationVCContent, nil) + assert.NoError(t, err) + + return []*verifiable.Credential{cred1, attestationVC} + }, + trustRegistryValidationEnabled: true, + wantErr: assert.NoError, + }, + { + name: "Error with expired VC", + getCredentials: func() []*verifiable.Credential { + credContent := verifiable.CredentialContents{ + Types: []string{ + "VerifiableCredential", + "UniversityDegreeCredential", + }, + Expired: timeutil.NewTime(time.Now().Add(-time.Hour)), + } + + cred1, err := verifiable.CreateCredential(credContent, nil) + assert.NoError(t, err) + + return []*verifiable.Credential{cred1} + }, + trustRegistryValidationEnabled: true, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return assert.ErrorContains(t, err, "credential expired") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + credentials := tt.getCredentials() + tt.wantErr(t, + (&Service{}).checkCredentialExpiry(ctx, credentials, tt.trustRegistryValidationEnabled), + fmt.Sprintf("checkCredentialExpiry(%v, %v)", ctx, credentials), + ) + }) + } +} diff --git a/test/bdd/fixtures/profile/profiles.json b/test/bdd/fixtures/profile/profiles.json index d157a8804..bc581b450 100644 --- a/test/bdd/fixtures/profile/profiles.json +++ b/test/bdd/fixtures/profile/profiles.json @@ -1829,6 +1829,9 @@ "url": "https://test-verifier.com", "active": true, "webHook": "http://vcs.webhook.example.com:8180", + "policy": { + "url": "https://mock-trustregistry.trustbloc.local:8098/policies/evaluate" + }, "checks": { "credential": { "format": [