diff --git a/api/spec/openapi.gen.go b/api/spec/openapi.gen.go index 3dd54a8fb..55623ab17 100644 --- a/api/spec/openapi.gen.go +++ b/api/spec/openapi.gen.go @@ -19,176 +19,183 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "AAD//5zSy+Vx+gAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/cmd/vc-rest/go.mod b/cmd/vc-rest/go.mod index 02d6407aa..0d1bc7fed 100644 --- a/cmd/vc-rest/go.mod +++ b/cmd/vc-rest/go.mod @@ -11,6 +11,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.0 github.com/deepmap/oapi-codegen v1.11.0 github.com/dgraph-io/ristretto v0.1.1 + github.com/go-jose/go-jose/v3 v3.0.1-0.20221117193127-916db76e8214 github.com/google/uuid v1.3.0 github.com/labstack/echo/v4 v4.10.2 github.com/ory/dockertest/v3 v3.9.1 @@ -97,7 +98,6 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/getkin/kin-openapi v0.94.0 // indirect github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect - github.com/go-jose/go-jose/v3 v3.0.1-0.20221117193127-916db76e8214 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect diff --git a/cmd/vc-rest/startcmd/start.go b/cmd/vc-rest/startcmd/start.go index 75f70b67d..8f16e2fdc 100644 --- a/cmd/vc-rest/startcmd/start.go +++ b/cmd/vc-rest/startcmd/start.go @@ -27,24 +27,24 @@ import ( oapimw "github.com/deepmap/oapi-codegen/pkg/middleware" "github.com/deepmap/oapi-codegen/pkg/securityprovider" "github.com/dgraph-io/ristretto" - "github.com/trustbloc/did-go/doc/ld/documentloader" - "github.com/trustbloc/vc-go/proof/defaults" - "github.com/trustbloc/vc-go/vermethod" - "go.mongodb.org/mongo-driver/mongo" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" - + "github.com/go-jose/go-jose/v3" "github.com/labstack/echo/v4" echomw "github.com/labstack/echo/v4/middleware" jsonld "github.com/piprate/json-gold/ld" echopprof "github.com/sevenNt/echo-pprof" "github.com/spf13/cobra" "github.com/trustbloc/did-go/doc/ld/context/remote" + "github.com/trustbloc/did-go/doc/ld/documentloader" "github.com/trustbloc/logutil-go/pkg/log" + "github.com/trustbloc/vc-go/proof/defaults" + "github.com/trustbloc/vc-go/vermethod" + "go.mongodb.org/mongo-driver/mongo" "go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws" "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" "github.com/trustbloc/vcs/api/spec" "github.com/trustbloc/vcs/component/credentialstatus" @@ -702,6 +702,7 @@ func buildEchoHandler( AckStore: ackStore, ProfileSvc: issuerProfileSvc, }) + oidc4ciService, err = oidc4ci.NewService(&oidc4ci.Config{ TransactionStore: oidc4ciTransactionStore, ClaimDataStore: oidc4ciClaimDataStore, @@ -811,6 +812,17 @@ func buildEchoHandler( TransactionStore: oidc4ciTransactionStore, }) + jweEncrypterCreator := func(jwk jose.JSONWebKey, alg jose.KeyAlgorithm, enc jose.ContentEncryption) (jose.Encrypter, error) { //nolint:lll + return jose.NewEncrypter( + enc, + jose.Recipient{ + Algorithm: alg, + Key: jwk, + }, + nil, + ) + } + oidc4civ1.RegisterHandlers(e, oidc4civ1.NewController(&oidc4civ1.Config{ OAuth2Provider: oauthProvider, StateStore: oidc4ciStateStore, @@ -824,6 +836,7 @@ func buildEchoHandler( ClientIDSchemeService: clientIDSchemeSvc, Tracer: conf.Tracer, AckService: ackService, + JWEEncrypterCreator: jweEncrypterCreator, })) oidc4vpv1.RegisterHandlers(e, oidc4vpv1.NewController(&oidc4vpv1.Config{ diff --git a/docs/v1/openapi.yaml b/docs/v1/openapi.yaml index 93de682c9..c42432582 100644 --- a/docs/v1/openapi.yaml +++ b/docs/v1/openapi.yaml @@ -797,6 +797,9 @@ paths: application/json: schema: $ref: '#/components/schemas/CredentialResponse' + application/jwt: + schema: + type: string operationId: oidc-credential description: Issues credentials in exchange for an authorization token. requestBody: @@ -812,7 +815,7 @@ paths: - oidc4ci responses: '204': - description: Ok + description: Ok '400': description: Error content: @@ -826,7 +829,7 @@ paths: application/json: schema: $ref: '#/components/schemas/AckRequest' - parameters: [ ] + parameters: [] components: schemas: HealthCheckResponse: @@ -914,18 +917,18 @@ components: description: 'URL of the Credential Issuer''s Batch Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. If omitted, the Credential Issuer does not support the Batch Credential Endpoint.' deferred_credential_endpoint: type: string - description: "URL of the Credential Issuer's Deferred Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path, and query parameter components. If omitted, the Credential Issuer does not support the Deferred Credential Endpoint." + description: 'URL of the Credential Issuer''s Deferred Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path, and query parameter components. If omitted, the Credential Issuer does not support the Deferred Credential Endpoint.' notification_endpoint: type: string - description: "URL of the Credential Issuer's Notification Endpoint. This URL MUST use the https scheme and MAY contain port, path, and query parameter components. If omitted, the Credential Issuer does not support the Notification Endpoint." + description: 'URL of the Credential Issuer''s Notification Endpoint. This URL MUST use the https scheme and MAY contain port, path, and query parameter components. If omitted, the Credential Issuer does not support the Notification Endpoint.' credential_response_encryption: - $ref: '#/components/schemas/CredentialResponseEncryption' + $ref: '#/components/schemas/CredentialResponseEncryptionSupported' credential_identifiers_supported: type: boolean - description: "Boolean value specifying whether the Credential Issuer supports returning credential_identifiers parameter in the authorization_details Token Response parameter, with true indicating support. If omitted, the default value is false." + description: 'Boolean value specifying whether the Credential Issuer supports returning credential_identifiers parameter in the authorization_details Token Response parameter, with true indicating support. If omitted, the default value is false.' signed_metadata: type: string - description: "String that is a signed JWT. This JWT contains Credential Issuer metadata parameters as claims." + description: String that is a signed JWT. This JWT contains Credential Issuer metadata parameters as claims. display: type: array description: 'An array of objects, where each object contains display properties of a Credential Issuer for a certain language.' @@ -935,8 +938,7 @@ components: type: object additionalProperties: $ref: '#/components/schemas/CredentialConfigurationsSupported' - description: "An object that describes specifics of the Credential that the Credential Issuer supports issuance of. This object contains a list of name/value pairs, where each name is a unique identifier of the supported credential being described." - + description: 'An object that describes specifics of the Credential that the Credential Issuer supports issuance of. This object contains a list of name/value pairs, where each name is a unique identifier of the supported credential being described.' credential_ack_endpoint: type: string description: URL of the acknowledgement endpoint. @@ -1781,9 +1783,9 @@ components: description: Transaction ID. types: type: array + description: Array of types of the credential being issued. items: type: string - description: Array of types of the credential being issued. format: type: string description: Format of the credential being issued. @@ -1794,13 +1796,31 @@ components: type: string description: The "aud" claim received from the client. hashed_token: - type: string - description: Hashed token received from the client. + type: string + description: Hashed token received from the client. + requested_credential_response_encryption: + $ref: '#/components/schemas/RequestedCredentialResponseEncryption' required: - tx_id - types - audienceClaim - hashed_token + RequestedCredentialResponseEncryption: + title: RequestedCredentialResponseEncryption + x-tags: + - issuer + type: object + description: Object containing requested information for encrypting the Credential Response. + properties: + alg: + type: string + description: JWE alg algorithm for encrypting the Credential Response. + enc: + type: string + description: JWE enc algorithm for encrypting the Credential Response. + required: + - alg + - enc PrepareCredentialResult: title: PrepareCredentialResult x-tags: @@ -1819,8 +1839,8 @@ components: type: string description: OIDC credential format ack_id: - type: string - description: String identifying an issued Credential that the Wallet includes in the acknowledgement request. + type: string + description: String identifying an issued Credential that the Wallet includes in the acknowledgement request. retry: type: boolean description: TRUE if claim data is not yet available in the issuer OP server. This will indicate VCS OIDC to issue acceptance_token instead of credential response (Deferred Credential flow). @@ -1838,16 +1858,38 @@ components: properties: types: type: array + description: Array of types of the credential being issued. items: type: string - description: Array of types of the credential being issued. format: type: string description: Format of the credential being issued. proof: $ref: '#/components/schemas/JWTProof' + credential_response_encryption: + $ref: '#/components/schemas/CredentialResponseEncryption' required: - types + CredentialResponseEncryption: + title: CredentialResponseEncryption + x-tags: + - oidc4ci + type: object + description: Object containing information for encrypting the Credential Response. + properties: + jwk: + type: string + description: Object containing a single public key as a JWK used for encrypting the Credential Response. + alg: + type: string + description: JWE alg algorithm for encrypting the Credential Response. + enc: + type: string + description: JWE enc algorithm for encrypting the Credential Response. + required: + - jwk + - alg + - enc JWTProof: title: JWTProof x-tags: @@ -1873,7 +1915,7 @@ components: credentials: type: array items: - $ref: '#/components/schemas/AcpRequestItem' + $ref: '#/components/schemas/AcpRequestItem' required: - credentials AcpRequestItem: @@ -1945,7 +1987,7 @@ components: properties: format: type: string - description: A JSON string identifying the format of this credential, i.e., jwt_vc_json or ldp_vc. Depending on the format value, the object contains further elements defining the type and (optionally) particular claims the credential MAY contain and information about how to display the credential. + description: 'A JSON string identifying the format of this credential, i.e., jwt_vc_json or ldp_vc. Depending on the format value, the object contains further elements defining the type and (optionally) particular claims the credential MAY contain and information about how to display the credential.' scope: type: string description: A JSON string identifying the scope value that this Credential Issuer supports for this particular credential. @@ -1955,12 +1997,12 @@ components: type: string description: Array of case sensitive strings that identify how the Credential is bound to the identifier of the End-User who possesses the Credential. cryptographic_suites_supported: - type: array - items: - type: string - description: Array of case sensitive strings that identify the cryptographic suites that are supported for the cryptographic_binding_methods_supported. + type: array + items: + type: string + description: Array of case sensitive strings that identify the cryptographic suites that are supported for the cryptographic_binding_methods_supported. credential_definition: - $ref: '#/components/schemas/CredentialConfigurationsSupportedDefinition' + $ref: '#/components/schemas/CredentialConfigurationsSupportedDefinition' order: type: array items: @@ -1968,27 +2010,27 @@ components: description: Array of the claim name values that lists them in the order they should be displayed by the Wallet. doctype: type: string - description: For mso_mdoc vc only. String identifying the Credential type, as defined in [ISO.18013-5]. + description: 'For mso_mdoc vc only. String identifying the Credential type, as defined in [ISO.18013-5].' vct: type: string - description: For vc+sd-jwt vc only. String designating the type of a Credential, as defined in https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-01 + description: 'For vc+sd-jwt vc only. String designating the type of a Credential, as defined in https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-01' claims: type: object - description: For mso_mdoc and vc+sd-jwt vc only. Object containing a list of name/value pairs, where each name identifies a claim about the subject offered in the Credential. The value can be another such object (nested data structures), or an array of such objects. + description: 'For mso_mdoc and vc+sd-jwt vc only. Object containing a list of name/value pairs, where each name identifies a claim about the subject offered in the Credential. The value can be another such object (nested data structures), or an array of such objects.' proof_types: - type: array - items: - type: string - description: A JSON array of case sensitive strings, each representing proof_type that the Credential Issuer supports. If omitted, the default value is jwt. + type: array + items: + type: string + description: 'A JSON array of case sensitive strings, each representing proof_type that the Credential Issuer supports. If omitted, the default value is jwt.' display: type: array - description: An array of objects, where each object contains the display properties of the supported credential for a certain language. + description: 'An array of objects, where each object contains the display properties of the supported credential for a certain language.' items: $ref: '#/components/schemas/CredentialDisplay' required: - format CredentialConfigurationsSupportedDefinition: - title: CredentialSupported.CredentialDefinition object definition. + title: CredentialConfigurationsSupportedDefinition object definition. x-tags: - issuer type: object @@ -1998,18 +2040,18 @@ components: type: array items: type: string - description: For ldp_vc only. Array as defined in https://www.w3.org/TR/vc-data-model/#contexts. + description: 'For ldp_vc only. Array as defined in https://www.w3.org/TR/vc-data-model/#contexts.' type: type: array items: type: string description: Array designating the types a certain credential type supports credentialSubject: - type: object - description: An object containing a list of name/value pairs, where each name identifies a claim offered in the Credential. The value can be another such object (nested data structures), or an array of such objects. + type: object + description: 'An object containing a list of name/value pairs, where each name identifies a claim offered in the Credential. The value can be another such object (nested data structures), or an array of such objects.' required: - type - CredentialResponseEncryption: + CredentialResponseEncryptionSupported: title: CredentialResponseEncryption object definition. x-tags: - issuer @@ -2020,15 +2062,15 @@ components: type: array items: type: string - description: Array containing a list of the JWE [RFC7516] encryption algorithms (alg values) [RFC7518] supported by the Credential and Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519]. + description: 'Array containing a list of the JWE [RFC7516] encryption algorithms (alg values) [RFC7518] supported by the Credential and Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519].' enc_values_supported: type: array items: type: string - description: Array containing a list of the JWE [RFC7516] encryption algorithms (enc values) [RFC7518] supported by the Credential and Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519]. + description: 'Array containing a list of the JWE [RFC7516] encryption algorithms (enc values) [RFC7518] supported by the Credential and Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519].' encryption_required: type: boolean - description: Boolean value specifying whether the Credential Issuer requires the additional encryption on top of TLS for the Credential Response. If the value is true, the Credential Issuer requires encryption for every Credential Response and therefore the Wallet MUST provide encryption keys in the Credential Request. If the value is false, the Wallet MAY chose whether it provides encryption keys or not. + description: 'Boolean value specifying whether the Credential Issuer requires the additional encryption on top of TLS for the Credential Response. If the value is true, the Credential Issuer requires encryption for every Credential Response and therefore the Wallet MUST provide encryption keys in the Credential Request. If the value is false, the Wallet MAY chose whether it provides encryption keys or not.' required: - alg_values_supported - enc_values_supported diff --git a/pkg/profile/api.go b/pkg/profile/api.go index f596c05da..8f5558554 100644 --- a/pkg/profile/api.go +++ b/pkg/profile/api.go @@ -167,6 +167,9 @@ type OIDCConfig struct { WalletInitiatedAuthFlowSupported bool `json:"wallet_initiated_auth_flow_supported"` SignedCredentialOfferSupported bool `json:"signed_credential_offer_supported"` SignedIssuerMetadataSupported bool `json:"signed_issuer_metadata_supported"` + CredentialResponseAlgValuesSupported []string `json:"credential_response_alg_values_supported"` + CredentialResponseEncValuesSupported []string `json:"credential_response_enc_values_supported"` + CredentialResponseEncryptionRequired bool `json:"credential_response_encryption_required"` ClaimsEndpoint string `json:"claims_endpoint"` } diff --git a/pkg/restapi/v1/issuer/controller.go b/pkg/restapi/v1/issuer/controller.go index 0d262a0e4..1ff470075 100644 --- a/pkg/restapi/v1/issuer/controller.go +++ b/pkg/restapi/v1/issuer/controller.go @@ -755,6 +755,10 @@ func (c *Controller) PrepareCredential(e echo.Context) error { return resterr.NewCustomError(resterr.ClaimsValidationErr, err) } + if err = validateCredentialResponseEncryption(profile, body.RequestedCredentialResponseEncryption); err != nil { + return err + } + signedCredential, err := c.signCredential( ctx, result.Credential, profile, issuecredential.WithTransactionID(body.TxId)) if err != nil { @@ -906,6 +910,44 @@ func getCredentialSubjects(subject interface{}) ([]verifiable.Subject, error) { return nil, fmt.Errorf("invalid type for credential subject: %T", subject) } +func validateCredentialResponseEncryption( + profile *profileapi.Issuer, + requested *RequestedCredentialResponseEncryption, +) error { + if profile.OIDCConfig == nil { + return nil + } + + if profile.OIDCConfig.CredentialResponseEncryptionRequired && requested == nil { + return resterr.NewValidationError(resterr.InvalidValue, "credential_response_encryption", + errors.New("credential response encryption is required")) + } + + alg := "" + if requested != nil { + alg = requested.Alg + } + + if len(profile.OIDCConfig.CredentialResponseAlgValuesSupported) > 0 && + !lo.Contains(profile.OIDCConfig.CredentialResponseAlgValuesSupported, alg) { + return resterr.NewValidationError(resterr.InvalidValue, "credential_response_encryption.alg", + fmt.Errorf("alg %s not supported", requested.Alg)) + } + + enc := "" + if requested != nil { + enc = requested.Enc + } + + if len(profile.OIDCConfig.CredentialResponseEncValuesSupported) > 0 && + !lo.Contains(profile.OIDCConfig.CredentialResponseEncValuesSupported, enc) { + return resterr.NewValidationError(resterr.InvalidValue, "credential_response_encryption.enc", + fmt.Errorf("enc %s not supported", requested.Enc)) + } + + return nil +} + // OpenidCredentialIssuerConfig request VCS IDP OIDC Configuration. // GET /issuer/{profileID}/{profileVersion}/.well-known/openid-credential-issuer. func (c *Controller) OpenidCredentialIssuerConfig(ctx echo.Context, profileID, profileVersion string) error { diff --git a/pkg/restapi/v1/issuer/controller_test.go b/pkg/restapi/v1/issuer/controller_test.go index c0b1a2f1e..f3c51054b 100644 --- a/pkg/restapi/v1/issuer/controller_test.go +++ b/pkg/restapi/v1/issuer/controller_test.go @@ -1312,6 +1312,7 @@ func TestController_ValidatePreAuthorizedCodeRequest(t *testing.T) { }) } +// nolint:lll func TestController_PrepareCredential(t *testing.T) { var universityDegreeSchemaDoc map[string]interface{} require.NoError(t, json.Unmarshal(universityDegreeSchema, &universityDegreeSchemaDoc)) @@ -1332,6 +1333,7 @@ func TestController_PrepareCredential(t *testing.T) { VCConfig: &profileapi.VCConfig{ Format: vcsverifiable.Ldp, }, + OIDCConfig: &profileapi.OIDCConfig{}, }, nil) mockIssueCredentialSvc := NewMockIssueCredentialService(gomock.NewController(t)) @@ -1380,6 +1382,67 @@ func TestController_PrepareCredential(t *testing.T) { assert.NoError(t, c.PrepareCredential(ctx)) }) + t.Run("success with requested credential response encryption", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().GetProfile(profileID, profileVersion).Times(1).Return( + &profileapi.Issuer{ + OrganizationID: orgID, + ID: profileID, + VCConfig: &profileapi.VCConfig{ + Format: vcsverifiable.Ldp, + }, + OIDCConfig: &profileapi.OIDCConfig{ + CredentialResponseAlgValuesSupported: []string{"ECDH-ES"}, + CredentialResponseEncValuesSupported: []string{"A128CBC-HS256"}, + }, + }, nil) + + mockIssueCredentialSvc := NewMockIssueCredentialService(gomock.NewController(t)) + mockIssueCredentialSvc.EXPECT().IssueCredential( + context.Background(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil) + + mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) + mockOIDC4CIService.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).DoAndReturn( + func( + ctx context.Context, + req *oidc4ci.PrepareCredential, + ) (*oidc4ci.PrepareCredentialResult, error) { + assert.Equal(t, oidc4ci.TxID("123"), req.TxID) + + return &oidc4ci.PrepareCredentialResult{ + ProfileID: profileID, + ProfileVersion: profileVersion, + Credential: sampleVC, + Format: vcsverifiable.Ldp, + Retry: false, + EnforceStrictValidation: true, + CredentialTemplate: &profileapi.CredentialTemplate{ + JSONSchema: string(universityDegreeSchema), + JSONSchemaID: "https://trustbloc.com/universitydegree.schema.json", + Checks: profileapi.CredentialTemplateChecks{ + Strict: true, + }, + }, + }, nil + }, + ) + + mockJSONSchemaValidator := NewMockJSONSchemaValidator(gomock.NewController(t)) + mockJSONSchemaValidator.EXPECT().Validate(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + c := NewController(&Config{ + ProfileSvc: mockProfileSvc, + IssueCredentialService: mockIssueCredentialSvc, + OIDC4CIService: mockOIDC4CIService, + DocumentLoader: testutil.DocumentLoader(t), + JSONSchemaValidator: mockJSONSchemaValidator, + }) + + req := `{"tx_id":"123","type":"UniversityDegreeCredential","format":"ldp_vc","requested_credential_response_encryption":{"alg":"ECDH-ES","enc":"A128CBC-HS256"}}` + ctx := echoContext(withRequestBody([]byte(req))) + assert.NoError(t, c.PrepareCredential(ctx)) + }) + t.Run("fail to access profile", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().GetProfile(profileID, profileVersion).Return(nil, errors.New("get profile error")) @@ -1567,6 +1630,190 @@ func TestController_PrepareCredential(t *testing.T) { ctx := echoContext(withRequestBody([]byte(req))) assert.EqualError(t, c.PrepareCredential(ctx), "invalid-claims: validation error") }) + + t.Run("credential response encryption is required error", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().GetProfile(profileID, profileVersion).Times(1).Return( + &profileapi.Issuer{ + OrganizationID: orgID, + ID: profileID, + VCConfig: &profileapi.VCConfig{ + Format: vcsverifiable.Ldp, + }, + OIDCConfig: &profileapi.OIDCConfig{ + CredentialResponseEncryptionRequired: true, + CredentialResponseAlgValuesSupported: []string{"ECDH-ES"}, + CredentialResponseEncValuesSupported: []string{"A128CBC-HS256"}, + }, + }, nil) + + mockIssueCredentialSvc := NewMockIssueCredentialService(gomock.NewController(t)) + mockIssueCredentialSvc.EXPECT().IssueCredential( + context.Background(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + + mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) + mockOIDC4CIService.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).DoAndReturn( + func( + ctx context.Context, + req *oidc4ci.PrepareCredential, + ) (*oidc4ci.PrepareCredentialResult, error) { + assert.Equal(t, oidc4ci.TxID("123"), req.TxID) + + return &oidc4ci.PrepareCredentialResult{ + ProfileID: profileID, + ProfileVersion: profileVersion, + Credential: sampleVC, + Format: vcsverifiable.Ldp, + Retry: false, + EnforceStrictValidation: true, + CredentialTemplate: &profileapi.CredentialTemplate{ + JSONSchema: string(universityDegreeSchema), + JSONSchemaID: "https://trustbloc.com/universitydegree.schema.json", + Checks: profileapi.CredentialTemplateChecks{ + Strict: true, + }, + }, + }, nil + }, + ) + + mockJSONSchemaValidator := NewMockJSONSchemaValidator(gomock.NewController(t)) + mockJSONSchemaValidator.EXPECT().Validate(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + c := NewController(&Config{ + ProfileSvc: mockProfileSvc, + IssueCredentialService: mockIssueCredentialSvc, + OIDC4CIService: mockOIDC4CIService, + DocumentLoader: testutil.DocumentLoader(t), + JSONSchemaValidator: mockJSONSchemaValidator, + }) + + req := `{"tx_id":"123","type":"UniversityDegreeCredential","format":"ldp_vc"}` + ctx := echoContext(withRequestBody([]byte(req))) + assert.ErrorContains(t, c.PrepareCredential(ctx), "credential response encryption is required") + }) + + t.Run("alg not supported error", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().GetProfile(profileID, profileVersion).Times(1).Return( + &profileapi.Issuer{ + OrganizationID: orgID, + ID: profileID, + VCConfig: &profileapi.VCConfig{ + Format: vcsverifiable.Ldp, + }, + OIDCConfig: &profileapi.OIDCConfig{ + CredentialResponseAlgValuesSupported: []string{"ECDH-ES"}, + CredentialResponseEncValuesSupported: []string{"A128CBC-HS256"}, + }, + }, nil) + + mockIssueCredentialSvc := NewMockIssueCredentialService(gomock.NewController(t)) + mockIssueCredentialSvc.EXPECT().IssueCredential( + context.Background(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + + mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) + mockOIDC4CIService.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).DoAndReturn( + func( + ctx context.Context, + req *oidc4ci.PrepareCredential, + ) (*oidc4ci.PrepareCredentialResult, error) { + assert.Equal(t, oidc4ci.TxID("123"), req.TxID) + + return &oidc4ci.PrepareCredentialResult{ + ProfileID: profileID, + ProfileVersion: profileVersion, + Credential: sampleVC, + Format: vcsverifiable.Ldp, + Retry: false, + EnforceStrictValidation: true, + CredentialTemplate: &profileapi.CredentialTemplate{ + JSONSchema: string(universityDegreeSchema), + JSONSchemaID: "https://trustbloc.com/universitydegree.schema.json", + Checks: profileapi.CredentialTemplateChecks{ + Strict: true, + }, + }, + }, nil + }, + ) + + mockJSONSchemaValidator := NewMockJSONSchemaValidator(gomock.NewController(t)) + mockJSONSchemaValidator.EXPECT().Validate(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + c := NewController(&Config{ + ProfileSvc: mockProfileSvc, + IssueCredentialService: mockIssueCredentialSvc, + OIDC4CIService: mockOIDC4CIService, + DocumentLoader: testutil.DocumentLoader(t), + JSONSchemaValidator: mockJSONSchemaValidator, + }) + + req := `{"tx_id":"123","type":"UniversityDegreeCredential","format":"ldp_vc","requested_credential_response_encryption":{"alg":"RSA-OAEP-256","enc":"A128CBC-HS256"}}` + ctx := echoContext(withRequestBody([]byte(req))) + assert.ErrorContains(t, c.PrepareCredential(ctx), "alg RSA-OAEP-256 not supported") + }) + + t.Run("enc not supported error", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().GetProfile(profileID, profileVersion).Times(1).Return( + &profileapi.Issuer{ + OrganizationID: orgID, + ID: profileID, + VCConfig: &profileapi.VCConfig{ + Format: vcsverifiable.Ldp, + }, + OIDCConfig: &profileapi.OIDCConfig{ + CredentialResponseAlgValuesSupported: []string{"ECDH-ES"}, + CredentialResponseEncValuesSupported: []string{"A128CBC-HS256"}, + }, + }, nil) + + mockIssueCredentialSvc := NewMockIssueCredentialService(gomock.NewController(t)) + mockIssueCredentialSvc.EXPECT().IssueCredential( + context.Background(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + + mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) + mockOIDC4CIService.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).DoAndReturn( + func( + ctx context.Context, + req *oidc4ci.PrepareCredential, + ) (*oidc4ci.PrepareCredentialResult, error) { + assert.Equal(t, oidc4ci.TxID("123"), req.TxID) + + return &oidc4ci.PrepareCredentialResult{ + ProfileID: profileID, + ProfileVersion: profileVersion, + Credential: sampleVC, + Format: vcsverifiable.Ldp, + Retry: false, + EnforceStrictValidation: true, + CredentialTemplate: &profileapi.CredentialTemplate{ + JSONSchema: string(universityDegreeSchema), + JSONSchemaID: "https://trustbloc.com/universitydegree.schema.json", + Checks: profileapi.CredentialTemplateChecks{ + Strict: true, + }, + }, + }, nil + }, + ) + + mockJSONSchemaValidator := NewMockJSONSchemaValidator(gomock.NewController(t)) + mockJSONSchemaValidator.EXPECT().Validate(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + c := NewController(&Config{ + ProfileSvc: mockProfileSvc, + IssueCredentialService: mockIssueCredentialSvc, + OIDC4CIService: mockOIDC4CIService, + DocumentLoader: testutil.DocumentLoader(t), + JSONSchemaValidator: mockJSONSchemaValidator, + }) + + req := `{"tx_id":"123","type":"UniversityDegreeCredential","format":"ldp_vc","requested_credential_response_encryption":{"alg":"ECDH-ES","enc":"A192CBC-HS384"}}` + ctx := echoContext(withRequestBody([]byte(req))) + assert.ErrorContains(t, c.PrepareCredential(ctx), "enc A192CBC-HS384 not supported") + }) } func TestOpenIdCredentialIssuerConfiguration(t *testing.T) { diff --git a/pkg/restapi/v1/issuer/openapi.gen.go b/pkg/restapi/v1/issuer/openapi.gen.go index d494adcf1..155b8cd9b 100644 --- a/pkg/restapi/v1/issuer/openapi.gen.go +++ b/pkg/restapi/v1/issuer/openapi.gen.go @@ -100,7 +100,7 @@ type CredentialIssuanceHistoryData struct { } // Object containing information about whether the Credential Issuer supports encryption of the Credential and Batch Credential Response on top of TLS -type CredentialResponseEncryption struct { +type CredentialResponseEncryptionSupported struct { // Array containing a list of the JWE [RFC7516] encryption algorithms (alg values) [RFC7518] supported by the Credential and Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519]. AlgValuesSupported []string `json:"alg_values_supported"` @@ -296,6 +296,9 @@ type PrepareCredential struct { // Hashed token received from the client. HashedToken string `json:"hashed_token"` + // Object containing requested information for encrypting the Credential Response. + RequestedCredentialResponseEncryption *RequestedCredentialResponseEncryption `json:"requested_credential_response_encryption,omitempty"` + // Transaction ID. TxId string `json:"tx_id"` @@ -326,6 +329,15 @@ type PushAuthorizationDetailsRequest struct { OpState string `json:"op_state"` } +// Object containing requested information for encrypting the Credential Response. +type RequestedCredentialResponseEncryption struct { + // JWE alg algorithm for encrypting the Credential Response. + Alg string `json:"alg"` + + // JWE enc algorithm for encrypting the Credential Response. + Enc string `json:"enc"` +} + // Model for storing auth code from issuer oauth type StoreAuthorizationCodeRequest struct { Code string `json:"code"` @@ -432,7 +444,7 @@ type WellKnownOpenIDIssuerConfiguration struct { CredentialIssuer *string `json:"credential_issuer,omitempty"` // Object containing information about whether the Credential Issuer supports encryption of the Credential and Batch Credential Response on top of TLS - CredentialResponseEncryption *CredentialResponseEncryption `json:"credential_response_encryption,omitempty"` + CredentialResponseEncryption *CredentialResponseEncryptionSupported `json:"credential_response_encryption,omitempty"` // URL of the Credential Issuer's Deferred Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path, and query parameter components. If omitted, the Credential Issuer does not support the Deferred Credential Endpoint. DeferredCredentialEndpoint *string `json:"deferred_credential_endpoint,omitempty"` diff --git a/pkg/restapi/v1/oidc4ci/controller.go b/pkg/restapi/v1/oidc4ci/controller.go index 90e678697..d56540396 100644 --- a/pkg/restapi/v1/oidc4ci/controller.go +++ b/pkg/restapi/v1/oidc4ci/controller.go @@ -108,6 +108,9 @@ type AckService interface { ) error } +// JWEEncrypterCreator creates JWE encrypter for given JWK, alg and enc. +type JWEEncrypterCreator func(jwk gojose.JSONWebKey, alg gojose.KeyAlgorithm, enc gojose.ContentEncryption) (gojose.Encrypter, error) //nolint:lll + // Config holds configuration options for Controller. type Config struct { OAuth2Provider OAuth2Provider @@ -122,6 +125,7 @@ type Config struct { IssuerVCSPublicHost string ExternalHostURL string AckService AckService + JWEEncrypterCreator JWEEncrypterCreator } // Controller for OIDC credential issuance API. @@ -138,6 +142,7 @@ type Controller struct { issuerVCSPublicHost string internalHostURL string ackService AckService + jweEncrypterCreator JWEEncrypterCreator } // NewController creates a new Controller instance. @@ -155,6 +160,7 @@ func NewController(config *Config) *Controller { issuerVCSPublicHost: config.IssuerVCSPublicHost, internalHostURL: config.ExternalHostURL, ackService: config.AckService, + jweEncrypterCreator: config.JWEEncrypterCreator, } } @@ -622,19 +628,19 @@ func (c *Controller) OidcAcknowledgement(e echo.Context) error { } // OidcCredential handles OIDC credential request (POST /oidc/credential). -func (c *Controller) OidcCredential(e echo.Context) error { +func (c *Controller) OidcCredential(e echo.Context) error { //nolint:funlen req := e.Request() ctx, span := c.tracer.Start(req.Context(), "OidcCredential") defer span.End() - var credentialRequest CredentialRequest + var credentialReq CredentialRequest - if err := validateCredentialRequest(e, &credentialRequest); err != nil { + if err := validateCredentialRequest(e, &credentialReq); err != nil { return err } - span.SetAttributes(attributeutil.JSON("oidc_credential_request", credentialRequest)) + span.SetAttributes(attributeutil.JSON("oidc_credential_request", credentialReq)) token := fosite.AccessTokenFromRequest(req) if token == "" { @@ -648,7 +654,7 @@ func (c *Controller) OidcCredential(e echo.Context) error { session := ar.GetSession().(*fosite.DefaultSession) //nolint:errcheck - jws, rawClaims, err := jwt.ParseAndCheckProof(credentialRequest.Proof.Jwt, + jws, rawClaims, err := jwt.ParseAndCheckProof(credentialReq.Proof.Jwt, c.jwtVerifier, false, jwt.WithIgnoreClaimsMapDecoding(true), ) @@ -666,16 +672,23 @@ func (c *Controller) OidcCredential(e echo.Context) error { return err } - resp, err := c.issuerInteractionClient.PrepareCredential(ctx, - issuer.PrepareCredentialJSONRequestBody{ - TxId: session.Extra[txIDKey].(string), //nolint:errcheck - Did: lo.ToPtr(did), - Types: credentialRequest.Types, - Format: credentialRequest.Format, - AudienceClaim: claims.Audience, - HashedToken: hashToken(token), - }, - ) + prepareCredentialReq := issuer.PrepareCredentialJSONRequestBody{ + TxId: session.Extra[txIDKey].(string), //nolint:errcheck + Did: lo.ToPtr(did), + Types: credentialReq.Types, + Format: credentialReq.Format, + AudienceClaim: claims.Audience, + HashedToken: hashToken(token), + } + + if credentialReq.CredentialResponseEncryption != nil { + prepareCredentialReq.RequestedCredentialResponseEncryption = &issuer.RequestedCredentialResponseEncryption{ + Alg: credentialReq.CredentialResponseEncryption.Alg, + Enc: credentialReq.CredentialResponseEncryption.Enc, + } + } + + resp, err := c.issuerInteractionClient.PrepareCredential(ctx, prepareCredentialReq) if err != nil { return fmt.Errorf("prepare credential: %w", err) } @@ -715,13 +728,35 @@ func (c *Controller) OidcCredential(e echo.Context) error { session.Extra[cNonceKey] = nonce session.Extra[cNonceExpiresAtKey] = time.Now().Add(cNonceTTL).Unix() - return apiUtil.WriteOutput(e)(CredentialResponse{ + credentialResp := &CredentialResponse{ Credential: result.Credential, Format: result.OidcFormat, CNonce: lo.ToPtr(nonce), CNonceExpiresIn: lo.ToPtr(int(cNonceTTL.Seconds())), AckId: result.AckId, - }, nil) + } + + if credentialReq.CredentialResponseEncryption != nil { + var encryptedResponse string + + if encryptedResponse, err = c.encryptCredentialResponse( + credentialResp, + credentialReq.CredentialResponseEncryption, + ); err != nil { + return fmt.Errorf("encrypt credential response: %w", err) + } + + e.Response().Header().Set("Content-Type", "application/jwt") + e.Response().WriteHeader(http.StatusOK) + + if _, err = e.Response().Write([]byte(encryptedResponse)); err != nil { + return err + } + + return nil + } + + return apiUtil.WriteOutput(e)(credentialResp, nil) } func validateCredentialRequest(e echo.Context, req *CredentialRequest) error { @@ -745,6 +780,39 @@ func validateCredentialRequest(e echo.Context, req *CredentialRequest) error { return nil } +func (c *Controller) encryptCredentialResponse( + resp *CredentialResponse, + enc *CredentialResponseEncryption, +) (string, error) { + var jwk gojose.JSONWebKey + + if err := json.Unmarshal([]byte(enc.Jwk), &jwk); err != nil { + return "", fmt.Errorf("unmarshal jwk: %w", err) + } + + encrypter, err := c.jweEncrypterCreator(jwk, gojose.KeyAlgorithm(enc.Alg), gojose.ContentEncryption(enc.Enc)) + if err != nil { + return "", fmt.Errorf("create encrypter: %w", err) + } + + b, err := json.Marshal(resp) + if err != nil { + return "", fmt.Errorf("marshal credential response: %w", err) + } + + encrypted, err := encrypter.Encrypt(b) + if err != nil { + return "", fmt.Errorf("encrypt credential response: %w", err) + } + + jwe, err := encrypted.CompactSerialize() + if err != nil { + return "", fmt.Errorf("serialize credential response: %w", err) + } + + return jwe, nil +} + //nolint:gocognit func (c *Controller) validateProofClaims( clientID string, diff --git a/pkg/restapi/v1/oidc4ci/controller_test.go b/pkg/restapi/v1/oidc4ci/controller_test.go index 7659b82cf..66c0f82a6 100644 --- a/pkg/restapi/v1/oidc4ci/controller_test.go +++ b/pkg/restapi/v1/oidc4ci/controller_test.go @@ -10,7 +10,9 @@ package oidc4ci_test import ( "bytes" "context" + "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" "crypto/rand" "crypto/sha256" "encoding/hex" @@ -961,6 +963,7 @@ func TestController_OidcCredential(t *testing.T) { var ( mockOAuthProvider = NewMockOAuth2Provider(gomock.NewController(t)) mockInteractionClient = NewMockIssuerInteractionClient(gomock.NewController(t)) + jweEncrypterCreator func(jwk gojose.JSONWebKey, alg gojose.KeyAlgorithm, enc gojose.ContentEncryption) (gojose.Encrypter, error) //nolint:lll accessToken string requestBody []byte ) @@ -997,6 +1000,20 @@ func TestController_OidcCredential(t *testing.T) { Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, } + ecdsaPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + defaultJWEEncrypterCreator := func(jwk gojose.JSONWebKey, alg gojose.KeyAlgorithm, enc gojose.ContentEncryption) (gojose.Encrypter, error) { //nolint:lll + return gojose.NewEncrypter( + enc, + gojose.Recipient{ + Algorithm: alg, + Key: jwk, + }, + nil, + ) + } + tests := []struct { name string setup func() @@ -1032,6 +1049,8 @@ func TestController_OidcCredential(t *testing.T) { Body: io.NopCloser(bytes.NewBuffer(b)), }, nil) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1074,6 +1093,8 @@ func TestController_OidcCredential(t *testing.T) { Body: io.NopCloser(bytes.NewBuffer(b)), }, nil) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1084,11 +1105,84 @@ func TestController_OidcCredential(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) }, }, + { + name: "success with encrypted credential response", + setup: func() { + mockOAuthProvider.EXPECT().IntrospectToken(gomock.Any(), gomock.Any(), fosite.AccessToken, gomock.Any()). + Return( + fosite.AccessToken, + fosite.NewAccessRequest( + &fosite.DefaultSession{ + Extra: map[string]interface{}{ + "txID": "tx_id", + "cNonce": "c_nonce", + "preAuth": true, + "cNonceExpiresAt": time.Now().Add(time.Minute).Unix(), + }, + }, + ), nil) + + b, marshalErr := json.Marshal(issuer.PrepareCredentialResult{ + Credential: "credential in jwt format", + Format: string(verifiable.Jwt), + }) + require.NoError(t, marshalErr) + + mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()). + Return( + &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(b)), + }, nil) + + jweEncrypterCreator = defaultJWEEncrypterCreator + + accessToken = "access-token" + + jwk := gojose.JSONWebKey{ + Key: &ecdsaPrivateKey.PublicKey, + } + + b, err = jwk.MarshalJSON() + require.NoError(t, err) + + requestBody, err = json.Marshal(oidc4ci.CredentialRequest{ + Format: lo.ToPtr(string(common.JwtVcJsonLd)), + Proof: &oidc4ci.JWTProof{ProofType: "jwt", Jwt: jws}, + Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialResponseEncryption: &oidc4ci.CredentialResponseEncryption{ + Alg: string(gojose.ECDH_ES), + Enc: string(gojose.A128CBC_HS256), + Jwk: string(b), + }, + }) + require.NoError(t, err) + }, + check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { + require.NoError(t, err) + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "application/jwt", rec.Header().Get("Content-Type")) + + jwe, err := gojose.ParseEncrypted(rec.Body.String()) + require.NoError(t, err) + + decrypted, err := jwe.Decrypt(ecdsaPrivateKey) + require.NoError(t, err) + + var resp *oidc4ci.CredentialResponse + require.NoError(t, json.Unmarshal(decrypted, &resp)) + + require.Equal(t, "credential in jwt format", resp.Credential) + require.NotEmpty(t, resp.CNonce) + require.NotEmpty(t, resp.CNonceExpiresIn) + }, + }, { name: "invalid credential format", setup: func() { mockOAuthProvider.EXPECT().IntrospectToken(gomock.Any(), gomock.Any(), fosite.AccessToken, gomock.Any()).Times(0) mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator accessToken = "access-token" @@ -1108,6 +1202,7 @@ func TestController_OidcCredential(t *testing.T) { setup: func() { mockOAuthProvider.EXPECT().IntrospectToken(gomock.Any(), gomock.Any(), fosite.AccessToken, gomock.Any()).Times(0) mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator accessToken = "access-token" @@ -1127,6 +1222,7 @@ func TestController_OidcCredential(t *testing.T) { setup: func() { mockOAuthProvider.EXPECT().IntrospectToken(gomock.Any(), gomock.Any(), fosite.AccessToken, gomock.Any()).Times(0) mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator accessToken = "access-token" @@ -1146,6 +1242,7 @@ func TestController_OidcCredential(t *testing.T) { setup: func() { mockOAuthProvider.EXPECT().IntrospectToken(gomock.Any(), gomock.Any(), fosite.AccessToken, gomock.Any()).Times(0) mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator accessToken = "" @@ -1164,6 +1261,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1191,6 +1290,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(oidc4ci.CredentialRequest{ @@ -1223,6 +1324,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1251,6 +1354,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" var signedJWTInvalid *jwt.JSONWebToken @@ -1292,6 +1397,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1322,6 +1429,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1359,6 +1468,8 @@ func TestController_OidcCredential(t *testing.T) { Body: io.NopCloser(bytes.NewBufferString(responseBody)), }, nil) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1387,6 +1498,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" var signedJWTInvalid *jwt.JSONWebToken @@ -1433,6 +1546,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" invalidNonceJWT, jwtErr := jwt.NewSigned(&oidc4ci.JWTProofClaims{ @@ -1476,6 +1591,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" invalidNonceJWT, jwtErr := jwt.NewSigned(&oidc4ci.JWTProofClaims{ @@ -1527,6 +1644,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" invalidHeaders := map[string]interface{}{ @@ -1576,6 +1695,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()).Times(0) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" currentTime := time.Now().Unix() @@ -1625,6 +1746,8 @@ func TestController_OidcCredential(t *testing.T) { mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()). Return(nil, errors.New("prepare credential error")) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1658,6 +1781,8 @@ func TestController_OidcCredential(t *testing.T) { Body: io.NopCloser(strings.NewReader(`{"code" : "oidc-credential-format-not-supported"}`)), }, nil) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1693,6 +1818,8 @@ func TestController_OidcCredential(t *testing.T) { Body: io.NopCloser(strings.NewReader(`{`)), }, nil) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1726,6 +1853,8 @@ func TestController_OidcCredential(t *testing.T) { Body: io.NopCloser(strings.NewReader(`{"code" : "oidc-credential-type-not-supported"}`)), }, nil) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1761,6 +1890,8 @@ func TestController_OidcCredential(t *testing.T) { Body: io.NopCloser(strings.NewReader(`{"code" : "random", "message": "awesome"}`)), }, nil) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1795,6 +1926,8 @@ func TestController_OidcCredential(t *testing.T) { Body: io.NopCloser(bytes.NewBufferString("invalid json")), }, nil) + jweEncrypterCreator = defaultJWEEncrypterCreator + accessToken = "access-token" requestBody, err = json.Marshal(credentialReq) @@ -1804,6 +1937,235 @@ func TestController_OidcCredential(t *testing.T) { require.ErrorContains(t, err, "decode prepare credential result") }, }, + { + name: "fail to unmarshal jwk for encrypting credential response", + setup: func() { + mockOAuthProvider.EXPECT().IntrospectToken(gomock.Any(), gomock.Any(), fosite.AccessToken, gomock.Any()). + Return( + fosite.AccessToken, + fosite.NewAccessRequest( + &fosite.DefaultSession{ + Extra: map[string]interface{}{ + "txID": "tx_id", + "cNonce": "c_nonce", + "preAuth": true, + "cNonceExpiresAt": time.Now().Add(time.Minute).Unix(), + }, + }, + ), nil) + + b, marshalErr := json.Marshal(issuer.PrepareCredentialResult{ + Credential: "credential in jwt format", + Format: string(verifiable.Jwt), + }) + require.NoError(t, marshalErr) + + mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()). + Return( + &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(b)), + }, nil) + + jweEncrypterCreator = defaultJWEEncrypterCreator + + accessToken = "access-token" + + requestBody, err = json.Marshal(oidc4ci.CredentialRequest{ + Format: lo.ToPtr(string(common.JwtVcJsonLd)), + Proof: &oidc4ci.JWTProof{ProofType: "jwt", Jwt: jws}, + Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialResponseEncryption: &oidc4ci.CredentialResponseEncryption{ + Alg: string(gojose.ECDH_ES), + Enc: string(gojose.A128CBC_HS256), + Jwk: "invalid jwk", + }, + }) + require.NoError(t, err) + }, + check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { + require.ErrorContains(t, err, "unmarshal jwk") + }, + }, + { + name: "fail to create encrypter for encrypting credential response", + setup: func() { + mockOAuthProvider.EXPECT().IntrospectToken(gomock.Any(), gomock.Any(), fosite.AccessToken, gomock.Any()). + Return( + fosite.AccessToken, + fosite.NewAccessRequest( + &fosite.DefaultSession{ + Extra: map[string]interface{}{ + "txID": "tx_id", + "cNonce": "c_nonce", + "preAuth": true, + "cNonceExpiresAt": time.Now().Add(time.Minute).Unix(), + }, + }, + ), nil) + + b, marshalErr := json.Marshal(issuer.PrepareCredentialResult{ + Credential: "credential in jwt format", + Format: string(verifiable.Jwt), + }) + require.NoError(t, marshalErr) + + mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()). + Return( + &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(b)), + }, nil) + + jweEncrypterCreator = defaultJWEEncrypterCreator + + accessToken = "access-token" + + jwk := gojose.JSONWebKey{ + Key: &ecdsaPrivateKey.PublicKey, + } + + b, err = jwk.MarshalJSON() + require.NoError(t, err) + + requestBody, err = json.Marshal(oidc4ci.CredentialRequest{ + Format: lo.ToPtr(string(common.JwtVcJsonLd)), + Proof: &oidc4ci.JWTProof{ProofType: "jwt", Jwt: jws}, + Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialResponseEncryption: &oidc4ci.CredentialResponseEncryption{ + Alg: string(gojose.ED25519), + Enc: string(gojose.A128CBC_HS256), + Jwk: string(b), + }, + }) + require.NoError(t, err) + }, + check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { + require.ErrorContains(t, err, "create encrypter") + }, + }, + { + name: "fail to encrypt credential response", + setup: func() { + mockOAuthProvider.EXPECT().IntrospectToken(gomock.Any(), gomock.Any(), fosite.AccessToken, gomock.Any()). + Return( + fosite.AccessToken, + fosite.NewAccessRequest( + &fosite.DefaultSession{ + Extra: map[string]interface{}{ + "txID": "tx_id", + "cNonce": "c_nonce", + "preAuth": true, + "cNonceExpiresAt": time.Now().Add(time.Minute).Unix(), + }, + }, + ), nil) + + b, marshalErr := json.Marshal(issuer.PrepareCredentialResult{ + Credential: "credential in jwt format", + Format: string(verifiable.Jwt), + }) + require.NoError(t, marshalErr) + + mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()). + Return( + &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(b)), + }, nil) + + jweEncrypterCreator = func(gojose.JSONWebKey, gojose.KeyAlgorithm, gojose.ContentEncryption) (gojose.Encrypter, error) { //nolint:lll,unparam + return &mockJWEEncrypter{ + Err: errors.New("encrypt error"), + }, nil + } + + accessToken = "access-token" + + jwk := gojose.JSONWebKey{ + Key: &ecdsaPrivateKey.PublicKey, + } + + b, err = jwk.MarshalJSON() + require.NoError(t, err) + + requestBody, err = json.Marshal(oidc4ci.CredentialRequest{ + Format: lo.ToPtr(string(common.JwtVcJsonLd)), + Proof: &oidc4ci.JWTProof{ProofType: "jwt", Jwt: jws}, + Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialResponseEncryption: &oidc4ci.CredentialResponseEncryption{ + Alg: string(gojose.ECDH_ES), + Enc: string(gojose.A128CBC_HS256), + Jwk: string(b), + }, + }) + require.NoError(t, err) + }, + check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { + require.ErrorContains(t, err, "encrypt credential response") + }, + }, + { + name: "fail to serialize encrypted credential response", + setup: func() { + mockOAuthProvider.EXPECT().IntrospectToken(gomock.Any(), gomock.Any(), fosite.AccessToken, gomock.Any()). + Return( + fosite.AccessToken, + fosite.NewAccessRequest( + &fosite.DefaultSession{ + Extra: map[string]interface{}{ + "txID": "tx_id", + "cNonce": "c_nonce", + "preAuth": true, + "cNonceExpiresAt": time.Now().Add(time.Minute).Unix(), + }, + }, + ), nil) + + b, marshalErr := json.Marshal(issuer.PrepareCredentialResult{ + Credential: "credential in jwt format", + Format: string(verifiable.Jwt), + }) + require.NoError(t, marshalErr) + + mockInteractionClient.EXPECT().PrepareCredential(gomock.Any(), gomock.Any()). + Return( + &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(b)), + }, nil) + + jweEncrypterCreator = func(gojose.JSONWebKey, gojose.KeyAlgorithm, gojose.ContentEncryption) (gojose.Encrypter, error) { //nolint:lll,unparam + return &mockJWEEncrypter{ + JWE: &gojose.JSONWebEncryption{}, + }, nil + } + + accessToken = "access-token" + + jwk := gojose.JSONWebKey{ + Key: &ecdsaPrivateKey.PublicKey, + } + + b, err = jwk.MarshalJSON() + require.NoError(t, err) + + requestBody, err = json.Marshal(oidc4ci.CredentialRequest{ + Format: lo.ToPtr(string(common.JwtVcJsonLd)), + Proof: &oidc4ci.JWTProof{ProofType: "jwt", Jwt: jws}, + Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialResponseEncryption: &oidc4ci.CredentialResponseEncryption{ + Alg: string(gojose.ECDH_ES), + Enc: string(gojose.A128CBC_HS256), + Jwk: string(b), + }, + }) + require.NoError(t, err) + }, + check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { + require.ErrorContains(t, err, "serialize credential response") + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1815,6 +2177,7 @@ func TestController_OidcCredential(t *testing.T) { JWTVerifier: proofChecker, Tracer: trace.NewNoopTracerProvider().Tracer(""), IssuerVCSPublicHost: aud, + JWEEncrypterCreator: jweEncrypterCreator, }) req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(requestBody)) @@ -1826,7 +2189,7 @@ func TestController_OidcCredential(t *testing.T) { rec := httptest.NewRecorder() - err := controller.OidcCredential(echo.New().NewContext(req, rec)) + err = controller.OidcCredential(echo.New().NewContext(req, rec)) tt.check(t, rec, err) }) } @@ -2423,3 +2786,20 @@ func TestController_OidcRegisterClient(t *testing.T) { }) } } + +type mockJWEEncrypter struct { + JWE *gojose.JSONWebEncryption + Err error +} + +func (m *mockJWEEncrypter) Encrypt([]byte) (*gojose.JSONWebEncryption, error) { + return m.JWE, m.Err +} + +func (m *mockJWEEncrypter) EncryptWithAuthData([]byte, []byte) (*gojose.JSONWebEncryption, error) { + return &gojose.JSONWebEncryption{}, nil +} + +func (m *mockJWEEncrypter) Options() gojose.EncrypterOptions { + return gojose.EncrypterOptions{} +} diff --git a/pkg/restapi/v1/oidc4ci/openapi.gen.go b/pkg/restapi/v1/oidc4ci/openapi.gen.go index 7715bb662..c5741da34 100644 --- a/pkg/restapi/v1/oidc4ci/openapi.gen.go +++ b/pkg/restapi/v1/oidc4ci/openapi.gen.go @@ -63,6 +63,9 @@ type AcpRequestItem struct { // Model for OIDC Credential request. type CredentialRequest struct { + // Object containing information for encrypting the Credential Response. + CredentialResponseEncryption *CredentialResponseEncryption `json:"credential_response_encryption,omitempty"` + // Format of the credential being issued. Format *string `json:"format,omitempty"` Proof *JWTProof `json:"proof,omitempty"` @@ -90,6 +93,18 @@ type CredentialResponse struct { Format string `json:"format"` } +// Object containing information for encrypting the Credential Response. +type CredentialResponseEncryption struct { + // JWE alg algorithm for encrypting the Credential Response. + Alg string `json:"alg"` + + // JWE enc algorithm for encrypting the Credential Response. + Enc string `json:"enc"` + + // Object containing a single public key as a JWK used for encrypting the Credential Response. + Jwk string `json:"jwk"` +} + // JWTProof defines model for JWTProof. type JWTProof struct { // REQUIRED. Signed JWT as proof of key possession.