Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: perform strict validation #1725

Merged
merged 8 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
383 changes: 192 additions & 191 deletions api/spec/openapi.gen.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions cmd/vc-rest/startcmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ func buildEchoHandler(
TrustRegistry: trustRegistryService,
AckService: ackService,
Composer: oidc4ci.NewCredentialComposer(),
DocumentLoader: documentLoader,
})
if err != nil {
return nil, fmt.Errorf("failed to instantiate new oidc4ci service: %w", err)
Expand Down
8 changes: 8 additions & 0 deletions docs/v1/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,10 @@ components:
type: boolean
description: Override credential subject did.
nullable: true
perform_strict_validation:
type: boolean
description: Perform strict validation.
nullable: true
credential:
type: object
description: Raw Complete credential for sign and customization
Expand Down Expand Up @@ -2250,6 +2254,10 @@ components:
type: boolean
description: Override credential subject did.
nullable: true
credential_perform_strict_validation:
type: boolean
description: Perform strict validation.
nullable: true
credential:
type: object
description: Raw Complete credential for sign and customization
Expand Down
18 changes: 10 additions & 8 deletions pkg/restapi/v1/issuer/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,10 +425,11 @@ func (c *Controller) InitiateCredentialComposeIssuance(e echo.Context, profileID
for _, compose := range lo.FromPtr(body.Compose) {
configs = append(configs, InitiateIssuanceCredentialConfiguration{
Compose: &DeprecatedComposeOIDC4CICredential{
Credential: compose.Credential,
IdTemplate: compose.CredentialOverrideId,
OverrideIssuer: compose.CredentialOverrideIssuer,
OverrideSubjectDid: compose.CredentialOverrideSubjectDid,
Credential: compose.Credential,
IdTemplate: compose.CredentialOverrideId,
OverrideIssuer: compose.CredentialOverrideIssuer,
OverrideSubjectDid: compose.CredentialOverrideSubjectDid,
PerformStrictValidation: compose.CredentialPerformStrictValidation,
},
CredentialExpiresAt: compose.CredentialExpiresAt,
})
Expand Down Expand Up @@ -524,10 +525,11 @@ func (c *Controller) initiateIssuance(

if multiCredentialIssuance.Compose != nil {
credConfig.ComposeCredential = &oidc4ci.InitiateIssuanceComposeCredential{
Credential: multiCredentialIssuance.Compose.Credential,
IDTemplate: lo.FromPtr(multiCredentialIssuance.Compose.IdTemplate),
OverrideIssuer: lo.FromPtr(multiCredentialIssuance.Compose.OverrideIssuer),
OverrideSubjectDID: lo.FromPtr(multiCredentialIssuance.Compose.OverrideSubjectDid),
Credential: multiCredentialIssuance.Compose.Credential,
IDTemplate: lo.FromPtr(multiCredentialIssuance.Compose.IdTemplate),
OverrideIssuer: lo.FromPtr(multiCredentialIssuance.Compose.OverrideIssuer),
OverrideSubjectDID: lo.FromPtr(multiCredentialIssuance.Compose.OverrideSubjectDid),
PerformStrictValidation: lo.FromPtr(multiCredentialIssuance.Compose.PerformStrictValidation),
}
}

Expand Down
73 changes: 73 additions & 0 deletions pkg/restapi/v1/issuer/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,79 @@ func TestController_initiateCredentialIssuance_CompatibilityV1(t *testing.T) {
})
}

func TestController_ComposeIssuance(t *testing.T) {
issuerProfile := &profileapi.Issuer{
OrganizationID: orgID,
ID: profileID,
Version: profileVersion,
Active: true,
OIDCConfig: &profileapi.OIDCConfig{},
CredentialTemplates: []*profileapi.CredentialTemplate{
{
ID: "templateID",
},
},
}

var (
mockProfileSvc = NewMockProfileService(gomock.NewController(t))
mockOIDC4CISvc = NewMockOIDC4CIService(gomock.NewController(t))
mockEventSvc = NewMockEventService(gomock.NewController(t))
c echo.Context
)

t.Run("Success", func(t *testing.T) {
expectedCred := map[string]interface{}{
"a": "b",
}
req, err := json.Marshal(&InitiateOIDC4CIComposeRequest{
ClientInitiateIssuanceUrl: lo.ToPtr("https://wallet.example.com/initiate_issuance"),
ClientWellknown: lo.ToPtr("https://wallet.example.com/.well-known/openid-configuration"),
Compose: lo.ToPtr([]InitiateIssuanceCredentialConfigurationCompose{
{
CredentialOverrideId: lo.ToPtr("abc"),
Credential: &expectedCred,
},
}),
})

require.NoError(t, err)

resp := &oidc4ci.InitiateIssuanceResponse{
InitiateIssuanceURL: "https://wallet.example.com/initiate_issuance",
TxID: "txID",
}

mockProfileSvc.EXPECT().GetProfile(profileID, profileVersion).Times(1).Return(issuerProfile, nil)
mockOIDC4CISvc.EXPECT().InitiateIssuance(gomock.Any(), gomock.Any(), issuerProfile).
DoAndReturn(func(
ctx context.Context,
request *oidc4ci.InitiateIssuanceRequest,
issuer *profileapi.Issuer,
) (*oidc4ci.InitiateIssuanceResponse, error) {
require.Len(t, request.CredentialConfiguration, 1)
require.EqualValues(t, expectedCred,
*request.CredentialConfiguration[0].ComposeCredential.Credential)

return resp, nil
})
mockEventSvc.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()).Times(0)

controller := NewController(&Config{
ProfileSvc: mockProfileSvc,
OIDC4CIService: mockOIDC4CISvc,
EventSvc: mockEventSvc,
EventTopic: spi.IssuerEventTopic,
Tracer: trace.NewNoopTracerProvider().Tracer(""),
})

c = echoContext(withRequestBody(req))

err = controller.InitiateCredentialComposeIssuance(c, profileID, profileVersion)
require.NoError(t, err)
})
}

func TestController_InitiateCredentialIssuance(t *testing.T) {
issuerProfile := &profileapi.Issuer{
OrganizationID: orgID,
Expand Down
6 changes: 6 additions & 0 deletions pkg/restapi/v1/issuer/openapi.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions pkg/service/oidc4ci/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,11 @@ type InitiateIssuanceCredentialConfiguration struct {
}

type InitiateIssuanceComposeCredential struct {
Credential *map[string]interface{} `json:"credential,omitempty"`
IDTemplate string `json:"id_template"`
OverrideIssuer bool `json:"override_issuer"`
OverrideSubjectDID bool `json:"override_subject_did"`
Credential *map[string]interface{} `json:"credential,omitempty"`
IDTemplate string `json:"id_template"`
OverrideIssuer bool `json:"override_issuer"`
OverrideSubjectDID bool `json:"override_subject_did"`
PerformStrictValidation bool `json:"perform_strict_validation,omitempty"`
}

// InitiateIssuanceResponse is the response from the Issuer to the Wallet with initiate issuance URL.
Expand Down
11 changes: 10 additions & 1 deletion pkg/service/oidc4ci/oidc4ci_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

//go:generate mockgen -destination oidc4ci_service_mocks_test.go -self_package mocks -package oidc4ci_test -source=oidc4ci_service.go -mock_names transactionStore=MockTransactionStore,wellKnownService=MockWellKnownService,eventService=MockEventService,pinGenerator=MockPinGenerator,credentialOfferReferenceStore=MockCredentialOfferReferenceStore,claimDataStore=MockClaimDataStore,profileService=MockProfileService,dataProtector=MockDataProtector,kmsRegistry=MockKMSRegistry,cryptoJWTSigner=MockCryptoJWTSigner,jsonSchemaValidator=MockJSONSchemaValidator,trustRegistry=MockTrustRegistry,ackStore=MockAckStore,ackService=MockAckService,composer=MockComposer
//go:generate mockgen -destination oidc4ci_service_mocks_test.go -self_package mocks -package oidc4ci_test -source=oidc4ci_service.go -mock_names transactionStore=MockTransactionStore,wellKnownService=MockWellKnownService,eventService=MockEventService,pinGenerator=MockPinGenerator,credentialOfferReferenceStore=MockCredentialOfferReferenceStore,claimDataStore=MockClaimDataStore,profileService=MockProfileService,dataProtector=MockDataProtector,kmsRegistry=MockKMSRegistry,cryptoJWTSigner=MockCryptoJWTSigner,jsonSchemaValidator=MockJSONSchemaValidator,trustRegistry=MockTrustRegistry,ackStore=MockAckStore,ackService=MockAckService,composer=MockComposer,documentLoader=MockDocumentLoader

package oidc4ci

Expand All @@ -19,6 +19,7 @@ import (
"time"

"github.com/google/uuid"
"github.com/piprate/json-gold/ld"
"github.com/samber/lo"
util "github.com/trustbloc/did-go/doc/util/time"
"github.com/trustbloc/logutil-go/pkg/log"
Expand Down Expand Up @@ -153,6 +154,11 @@ type composer interface {
) (*verifiable.Credential, error)
}

// DocumentLoader knows how to load remote documents.
type documentLoader interface {
LoadDocument(u string) (*ld.RemoteDocument, error)
}

// Config holds configuration options and dependencies for Service.
type Config struct {
TransactionStore transactionStore
Expand All @@ -173,6 +179,7 @@ type Config struct {
TrustRegistry trustRegistry
AckService ackService
Composer composer
DocumentLoader documentLoader
}

// Service implements VCS credential interaction API for OIDC credential issuance.
Expand All @@ -195,6 +202,7 @@ type Service struct {
trustRegistry trustRegistry
ackService ackService
composer composer
documentLoader documentLoader
}

// NewService returns a new Service instance.
Expand All @@ -218,6 +226,7 @@ func NewService(config *Config) (*Service, error) {
trustRegistry: config.TrustRegistry,
ackService: config.AckService,
composer: config.Composer,
documentLoader: config.DocumentLoader,
}, nil
}

Expand Down
48 changes: 45 additions & 3 deletions pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"github.com/samber/lo"
"github.com/trustbloc/logutil-go/pkg/log"
"github.com/trustbloc/vc-go/jwt"
verifiable2 "github.com/trustbloc/vc-go/verifiable"

"github.com/trustbloc/vcs/internal/logfields"
"github.com/trustbloc/vcs/pkg/doc/vc"
Expand Down Expand Up @@ -183,10 +184,17 @@

var targetCredentialTemplate *profileapi.CredentialTemplate

if credentialConfiguration.CredentialTemplateID == "" &&
credentialConfiguration.ComposeCredential != nil &&
credentialConfiguration.ComposeCredential.Credential != nil {
isCompose := credentialConfiguration.ComposeCredential != nil &&
credentialConfiguration.ComposeCredential.Credential != nil

if credentialConfiguration.CredentialTemplateID == "" && isCompose { //nolint:nestif
targetCredentialTemplate = s.buildVirtualTemplate(&credentialConfiguration)

if targetCredentialTemplate.Checks.Strict { //nolint:nestif
if err = s.validateComposeCredential(*credentialConfiguration.ComposeCredential.Credential); err != nil {
return nil, err
}
}
} else {
targetCredentialTemplate, err = findCredentialTemplate(credentialConfiguration.CredentialTemplateID, profile)
if err != nil {
Expand Down Expand Up @@ -235,9 +243,43 @@
return txCredentialConfiguration, nil
}

func (s *Service) validateComposeCredential(credential map[string]interface{}) error {
requiredFields := map[string]string{
"issuer": "did:orb:anything",
"issuanceDate": "2021-01-01T00:00:00Z",
}

var missingFieldsAdded []string

for key, value := range requiredFields {
if _, ok := credential[key]; !ok {
credential[key] = value
missingFieldsAdded = append(missingFieldsAdded, key)

Check warning on line 257 in pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go#L256-L257

Added lines #L256 - L257 were not covered by tests
}
}

if _, credCheckErr := verifiable2.ParseCredentialJSON(credential,
verifiable2.WithJSONLDDocumentLoader(s.documentLoader),
verifiable2.WithDisabledProofCheck(),
verifiable2.WithStrictValidation(),
); credCheckErr != nil {
return resterr.NewValidationError(resterr.InvalidValue, "credential",
fmt.Errorf("parse credential: %w", credCheckErr))
}

for _, key := range missingFieldsAdded {
delete(credential, key)

Check warning on line 271 in pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go#L270-L271

Added lines #L270 - L271 were not covered by tests
}

return nil

Check warning on line 274 in pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go#L274

Added line #L274 was not covered by tests
}

func (s *Service) buildVirtualTemplate(req *InitiateIssuanceCredentialConfiguration) *profileapi.CredentialTemplate {
result := &profileapi.CredentialTemplate{
ID: fmt.Sprintf("virtual_%s", uuid.NewString()),
Checks: profileapi.CredentialTemplateChecks{
Strict: req.ComposeCredential.PerformStrictValidation,
},
}

if req.ComposeCredential.Credential != nil {
Expand Down
Loading
Loading