Skip to content

Commit

Permalink
feat: support dynamic wellknown (#1792)
Browse files Browse the repository at this point in the history
* feat: basic loggic for dynamic wellknown

* feat: update vc

* feat: cleanup

* fix: lint

* feat: add wellknown

* feat: add more unit tests

* fix: profile

* fix: lint
  • Loading branch information
skynet2 authored Nov 6, 2024
1 parent 96d3bf3 commit 75a15ee
Show file tree
Hide file tree
Showing 21 changed files with 571 additions and 24 deletions.
1 change: 1 addition & 0 deletions cmd/vc-rest/startcmd/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ const (
defaultOIDC4CITransactionDataTTL = 15 * time.Minute
defaultOIDC4CIAckDataTTL = 24 * time.Hour
defaultOIDC4CIAuthStateTTL = 15 * time.Minute
defaultDynamicWellKnownTTL = 1 * time.Hour
defaultDataEncryptionKeyLength = 256
)

Expand Down
20 changes: 17 additions & 3 deletions cmd/vc-rest/startcmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ import (
"github.com/trustbloc/vcs/pkg/storage/redis"
redisclient "github.com/trustbloc/vcs/pkg/storage/redis"
"github.com/trustbloc/vcs/pkg/storage/redis/ackstore"
"github.com/trustbloc/vcs/pkg/storage/redis/dynamicwellknown"
oidc4ciclaimdatastoreredis "github.com/trustbloc/vcs/pkg/storage/redis/oidc4ciclaimdatastore"
oidc4cinoncestoreredis "github.com/trustbloc/vcs/pkg/storage/redis/oidc4cinoncestore"
oidc4cistatestoreredis "github.com/trustbloc/vcs/pkg/storage/redis/oidc4cistatestore"
Expand Down Expand Up @@ -553,10 +554,16 @@ func buildEchoHandler(
return newHTTPClient(tlsConfig, conf.StartupParameters, metrics, id)
}

dynamicWellKnownStore, err := getDynamicWellKnownStore(redisClient)
if err != nil {
return nil, fmt.Errorf("failed to get dynamic well-known store: %w", err)
}

openidCredentialIssuerConfigProviderSvc := wellknownprovider.NewService(&wellknownprovider.Config{
ExternalHostURL: conf.StartupParameters.apiGatewayURL,
KMSRegistry: kmsRegistry,
CryptoJWTSigner: vcCrypto,
ExternalHostURL: conf.StartupParameters.apiGatewayURL,
KMSRegistry: kmsRegistry,
CryptoJWTSigner: vcCrypto,
DynamicWellKnownStore: dynamicWellKnownStore,
})

// Issuer Profile Management API
Expand Down Expand Up @@ -735,6 +742,7 @@ func buildEchoHandler(
AckService: ackService,
DocumentLoader: documentLoader,
PrepareCredential: prepareCredentialSvc,
WellKnownProvider: openidCredentialIssuerConfigProviderSvc,
})
if err != nil {
return nil, fmt.Errorf("failed to instantiate new oidc4ci service: %w", err)
Expand Down Expand Up @@ -1213,6 +1221,12 @@ func getOIDC4CITransactionStore(
return store, nil
}

func getDynamicWellKnownStore(
redisClient *redis.Client,
) (wellknownprovider.DynamicWellKnownStore, error) {
return dynamicwellknown.New(redisClient, defaultDynamicWellKnownTTL), nil
}

func getAckStore(
redisClient *redis.Client,
oidc4ciAckDataTTL int32,
Expand Down
3 changes: 2 additions & 1 deletion component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"encoding/json"
"errors"
"fmt"
"go.uber.org/zap"
"io"
"log/slog"
"net"
Expand All @@ -24,6 +23,8 @@ import (
"strings"
"time"

"go.uber.org/zap"

"github.com/cli/browser"
"github.com/google/uuid"
"github.com/piprate/json-gold/ld"
Expand Down
1 change: 1 addition & 0 deletions pkg/profile/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ type OIDCConfig struct {
CredentialResponseAlgValuesSupported []string `json:"credential_response_alg_values_supported"`
CredentialResponseEncValuesSupported []string `json:"credential_response_enc_values_supported"`
CredentialResponseEncryptionRequired bool `json:"credential_response_encryption_required"`
DynamicWellKnownSupported bool `json:"dynamic_well_known_supported"`
ClaimsEndpoint string `json:"claims_endpoint"`
}

Expand Down
13 changes: 13 additions & 0 deletions pkg/restapi/v1/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ func ValidateVPFormat(format VPFormat) (vcsverifiable.Format, error) {
return "", fmt.Errorf("unsupported vp format %s, use one of next [%s, %s]", format, JwtVcJsonLd, LdpVc)
}

func MapToVCFormat(format vcsverifiable.Format) (vcsverifiable.OIDCFormat, error) {
switch format {
case vcsverifiable.Jwt:
return vcsverifiable.JwtVCJson, nil
case vcsverifiable.Ldp:
return vcsverifiable.JwtVCJsonLD, nil
case vcsverifiable.Cwt:
return vcsverifiable.CwtVcLD, nil
}

return "", fmt.Errorf("vc format missmatch %s, rest api supports only [%s, %s]", format, JwtVcJsonLd, LdpVc)
}

func MapToVPFormat(format vcsverifiable.Format) (VPFormat, error) {
switch format {
case vcsverifiable.Jwt:
Expand Down
17 changes: 17 additions & 0 deletions pkg/restapi/v1/common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,23 @@ func TestValidateVPFormat(t *testing.T) {
require.Error(t, err)
}

func TestMapVcFormat(t *testing.T) {
got, err := MapToVCFormat(vcsverifiable.Jwt)
require.NoError(t, err)
require.EqualValues(t, JwtVcJson, got)

got, err = MapToVCFormat(vcsverifiable.Ldp)
require.NoError(t, err)
require.EqualValues(t, JwtVcJsonLd, got)

got, err = MapToVCFormat(vcsverifiable.Cwt)
require.NoError(t, err)
require.EqualValues(t, CwtVcLd, got)

_, err = MapToVCFormat("invalid")
require.Error(t, err)
}

func TestValidateDIDMethod(t *testing.T) {
got, err := ValidateDIDMethod(DIDMethodKey)
require.NoError(t, err)
Expand Down
10 changes: 10 additions & 0 deletions pkg/service/oidc4ci/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/trustbloc/vc-go/verifiable"

profileapi "github.com/trustbloc/vcs/pkg/profile"
"github.com/trustbloc/vcs/pkg/service/issuecredential"
)

Expand All @@ -24,3 +25,12 @@ type composer interface { // nolint:unused
req *issuecredential.PrepareCredentialsRequest,
) (*verifiable.Credential, error)
}

type wellKnownProvider interface {
AddDynamicConfiguration(
ctx context.Context,
profileID string,
id string,
credSupported *profileapi.CredentialsConfigurationSupported,
) error
}
3 changes: 3 additions & 0 deletions pkg/service/oidc4ci/oidc4ci_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ type Config struct {
AckService ackService
DocumentLoader documentLoader
PrepareCredential credentialIssuer
WellKnownProvider wellKnownProvider
}

// Service implements VCS credential interaction API for OIDC credential issuance.
Expand All @@ -198,6 +199,7 @@ type Service struct {
ackService ackService
documentLoader documentLoader
credentialIssuer credentialIssuer
wellKnownProvider wellKnownProvider
}

// NewService returns a new Service instance.
Expand All @@ -222,6 +224,7 @@ func NewService(config *Config) (*Service, error) {
ackService: config.AckService,
documentLoader: config.DocumentLoader,
credentialIssuer: config.PrepareCredential,
wellKnownProvider: config.WellKnownProvider,
}, nil
}

Expand Down
39 changes: 32 additions & 7 deletions pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/trustbloc/vcs/pkg/doc/verifiable"
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/issuecredential"
)

Expand Down Expand Up @@ -208,16 +209,17 @@ func (s *Service) newTxCredentialConf(
}
}

credentialConfigurationID, _, err := findCredentialConfigurationID(
targetCredentialTemplate.ID, targetCredentialTemplate.Type, profile)
credentialConfigurationID, metaCredentialConfiguration, err := s.findCredentialConfigurationID(
ctx,
targetCredentialTemplate.ID,
targetCredentialTemplate.Type,
profile,
)

if err != nil {
return nil, err
}

profileMeta := profile.CredentialMetaData

metaCredentialConfiguration := profileMeta.CredentialsConfigurationSupported[credentialConfigurationID]

txCredentialConfiguration := &issuecredential.TxCredentialConfiguration{
ID: uuid.NewString(),
CredentialTemplate: targetCredentialTemplate,
Expand Down Expand Up @@ -489,7 +491,8 @@ func findCredentialTemplate(
return profile.CredentialTemplates[0], nil
}

func findCredentialConfigurationID(
func (s *Service) findCredentialConfigurationID(
ctx context.Context,
requestedTemplateID string,
credentialType string,
profile *profileapi.Issuer,
Expand All @@ -500,6 +503,28 @@ func findCredentialConfigurationID(
}
}

if profile.OIDCConfig.DynamicWellKnownSupported {
id := uuid.NewString()

vcFormat, err := common.MapToVCFormat(profile.VCConfig.Format)
if err != nil {
return "", nil, err
}

cfg := &profileapi.CredentialsConfigurationSupported{
Format: vcFormat,
CredentialDefinition: &profileapi.CredentialDefinition{
Type: []string{credentialType},
},
}

if err = s.wellKnownProvider.AddDynamicConfiguration(ctx, profile.ID, id, cfg); err != nil {
return "", nil, err
}

return id, cfg, nil
}

return "", nil, resterr.NewValidationError(resterr.InvalidValue, "credential_template_id",
fmt.Errorf("credential configuration not found for requested template id %s", requestedTemplateID))
}
Expand Down
43 changes: 43 additions & 0 deletions pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type mocks struct {
ackService *MockAckService
documentLoader *jsonld.DefaultDocumentLoader
composer *Mockcomposer
wellKnown *MockwellKnownProvider
}

func TestService_InitiateIssuance(t *testing.T) {
Expand Down Expand Up @@ -1335,6 +1336,45 @@ func TestService_InitiateIssuance(t *testing.T) {
require.Nil(t, resp)
},
},
{
name: "Success with Dynamic WellKnown",
setup: func(mocks *mocks) {
initialOpState := ""
claimData := degreeClaims

b, err := json.Marshal(testProfile) //nolint
assert.NoError(t, err)

assert.NoError(t, json.Unmarshal(b, &profile))
delete(profile.CredentialMetaData.CredentialsConfigurationSupported, "UniversityDegreeCredentialIdentifier")
profile.OIDCConfig.DynamicWellKnownSupported = true

mocks.wellKnown.EXPECT().
AddDynamicConfiguration(gomock.Any(), profile.ID, gomock.Any(), gomock.Any()).
Return(nil)

mocks.jsonSchemaValidator.EXPECT().Validate(gomock.Any(), gomock.Any(), gomock.Any()).
Return(errors.New("schema validation err"))

issuanceReq = &oidc4ci.InitiateIssuanceRequest{
ClientWellKnownURL: walletWellKnownURL,
OpState: initialOpState,
UserPinRequired: false,
GrantType: oidc4ci.GrantTypePreAuthorizedCode,
Scope: []string{"openid", "profile"},
CredentialConfiguration: []oidc4ci.InitiateIssuanceCredentialConfiguration{
{
CredentialTemplateID: "templateID2",
ClaimData: claimData,
},
},
}
},
check: func(t *testing.T, resp *oidc4ci.InitiateIssuanceResponse, err error) {
require.ErrorContains(t, err, "schema validation err")
require.Nil(t, resp)
},
},
{
name: "Error because of event publishing",
setup: func(mocks *mocks) {
Expand Down Expand Up @@ -1533,6 +1573,7 @@ func TestService_InitiateIssuance(t *testing.T) {
}

profile = &testProfile
profile.OIDCConfig.DynamicWellKnownSupported = false
},
check: func(t *testing.T, resp *oidc4ci.InitiateIssuanceResponse, err error) {
require.Nil(t, resp)
Expand Down Expand Up @@ -1866,6 +1907,7 @@ func TestService_InitiateIssuance(t *testing.T) {
crypto: NewMockDataProtector(gomock.NewController(t)),
jsonSchemaValidator: NewMockJSONSchemaValidator(gomock.NewController(t)),
documentLoader: jsonld.NewDefaultDocumentLoader(http.DefaultClient),
wellKnown: NewMockwellKnownProvider(gomock.NewController(t)),
}

tt.setup(m)
Expand All @@ -1881,6 +1923,7 @@ func TestService_InitiateIssuance(t *testing.T) {
DataProtector: m.crypto,
JSONSchemaValidator: m.jsonSchemaValidator,
DocumentLoader: m.documentLoader,
WellKnownProvider: m.wellKnown,
})
require.NoError(t, err)

Expand Down
1 change: 1 addition & 0 deletions pkg/service/verifycredential/verifycredential_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ func (s *Service) verifyVC(
return fmt.Errorf("get data integrity verifier: %w", err)
}

logger.Info(fmt.Sprintf("verifying VC with challenge[%s] and domain[%s]", challenge, domain))
opts := []verifiable.CredentialOpt{
verifiable.WithProofChecker(
defaults.NewDefaultProofChecker(vermethod.NewVDRResolver(s.vdr)),
Expand Down
9 changes: 9 additions & 0 deletions pkg/service/wellknown/provider/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Copyright Gen Digital Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package provider

type DynamicWellKnownStore dynamicWellKnownStore
24 changes: 24 additions & 0 deletions pkg/service/wellknown/provider/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
Copyright Gen Digital Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package provider

import (
"context"

profileapi "github.com/trustbloc/vcs/pkg/profile"
)

//go:generate mockgen -destination interfaces_mocks_test.go -package provider -source=interfaces.go

type dynamicWellKnownStore interface {
Upsert(
ctx context.Context,
profileID string,
item map[string]*profileapi.CredentialsConfigurationSupported,
) error
Get(ctx context.Context, profileID string) (map[string]*profileapi.CredentialsConfigurationSupported, error)
}
Loading

0 comments on commit 75a15ee

Please sign in to comment.