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

Credential Manifest - Update to Credential Response #174

Merged
merged 3 commits into from
Aug 25, 2022
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
87 changes: 56 additions & 31 deletions credential/manifest/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

const (
BuilderEmptyError string = "builder cannot be empty"
SpecVersion string = "https://identity.foundation/credential-manifest/spec/v1.0.0/"
)

type CredentialManifestBuilder struct {
Expand All @@ -21,7 +22,8 @@ type CredentialManifestBuilder struct {
func NewCredentialManifestBuilder() CredentialManifestBuilder {
return CredentialManifestBuilder{
CredentialManifest: &CredentialManifest{
ID: uuid.NewString(),
ID: uuid.NewString(),
SpecVersion: SpecVersion,
},
}
}
Expand Down Expand Up @@ -119,10 +121,9 @@ type CredentialApplicationBuilder struct {
func NewCredentialApplicationBuilder(manifestID string) CredentialApplicationBuilder {
return CredentialApplicationBuilder{
CredentialApplication: &CredentialApplication{
Application: Application{
ID: uuid.NewString(),
ManifestID: manifestID,
},
ID: uuid.NewString(),
SpecVersion: SpecVersion,
ManifestID: manifestID,
},
}
}
Expand Down Expand Up @@ -151,7 +152,7 @@ func (cab *CredentialApplicationBuilder) SetApplicationManifestID(manifestID str
return errors.New(BuilderEmptyError)
}

cab.Application.ManifestID = manifestID
cab.ManifestID = manifestID
return nil
}

Expand All @@ -168,7 +169,7 @@ func (cab *CredentialApplicationBuilder) SetApplicationClaimFormat(format exchan
return errors.Wrapf(err, "cannot set invalid claim format: %+v", format)
}

cab.Application.Format = &format
cab.Format = &format
return nil
}

Expand All @@ -185,58 +186,59 @@ func (cab *CredentialApplicationBuilder) SetPresentationSubmission(submission ex
return nil
}

type CredentialFulfillmentBuilder struct {
*CredentialFulfillment
type CredentialResponseBuilder struct {
*CredentialResponse
}

func NewCredentialFulfillmentBuilder(manifestID string) CredentialFulfillmentBuilder {
return CredentialFulfillmentBuilder{
CredentialFulfillment: &CredentialFulfillment{
ID: uuid.NewString(),
ManifestID: manifestID,
func NewCredentialResponseBuilder(manifestID string) CredentialResponseBuilder {
return CredentialResponseBuilder{
CredentialResponse: &CredentialResponse{
ID: uuid.NewString(),
SpecVersion: SpecVersion,
ManifestID: manifestID,
},
}
}

func (cfb *CredentialFulfillmentBuilder) Build() (*CredentialFulfillment, error) {
if cfb.IsEmpty() {
func (crb *CredentialResponseBuilder) Build() (*CredentialResponse, error) {
if crb.IsEmpty() {
return nil, errors.New(BuilderEmptyError)
}

if err := cfb.CredentialFulfillment.IsValid(); err != nil {
return nil, util.LoggingErrorMsg(err, "credential fulfillment not ready to be built")
if err := crb.CredentialResponse.IsValid(); err != nil {
return nil, util.LoggingErrorMsg(err, "credential response not ready to be built")
}

return cfb.CredentialFulfillment, nil
return crb.CredentialResponse, nil
}

func (cfb *CredentialFulfillmentBuilder) IsEmpty() bool {
if cfb == nil || cfb.CredentialFulfillment.IsEmpty() {
func (crb *CredentialResponseBuilder) IsEmpty() bool {
if crb == nil || crb.CredentialResponse.IsEmpty() {
return true
}
return reflect.DeepEqual(cfb, &CredentialFulfillmentBuilder{})
return reflect.DeepEqual(crb, &CredentialResponseBuilder{})
}

func (cfb *CredentialFulfillmentBuilder) SetManifestID(manifestID string) error {
if cfb.IsEmpty() {
func (crb *CredentialResponseBuilder) SetManifestID(manifestID string) error {
if crb.IsEmpty() {
return errors.New(BuilderEmptyError)
}

cfb.ManifestID = manifestID
crb.ManifestID = manifestID
return nil
}

func (cfb *CredentialFulfillmentBuilder) SetApplicationID(applicationID string) error {
if cfb.IsEmpty() {
func (crb *CredentialResponseBuilder) SetApplicationID(applicationID string) error {
if crb.IsEmpty() {
return errors.New(BuilderEmptyError)
}

cfb.ApplicationID = applicationID
crb.ApplicationID = applicationID
return nil
}

func (cfb *CredentialFulfillmentBuilder) SetDescriptorMap(descriptors []exchange.SubmissionDescriptor) error {
if cfb.IsEmpty() {
func (crb *CredentialResponseBuilder) SetFulfillment(descriptors []exchange.SubmissionDescriptor) error {
if crb.IsEmpty() {
return errors.New(BuilderEmptyError)
}

Expand All @@ -251,6 +253,29 @@ func (cfb *CredentialFulfillmentBuilder) SetDescriptorMap(descriptors []exchange
}
}

cfb.DescriptorMap = descriptors
crb.Fulfillment = &struct {
DescriptorMap []exchange.SubmissionDescriptor `json:"descriptor_map" validate:"required"`
}{
DescriptorMap: descriptors,
}
return nil
}

func (crb *CredentialResponseBuilder) SetDenial(reason string, inputDescriptors []string) error {
if crb.IsEmpty() {
return errors.New(BuilderEmptyError)
}

if len(reason) == 0 {
return errors.New("cannot set empty reason")
}

crb.Denial = &struct {
Reason string `json:"reason" validate:"required"`
InputDescriptors []string `json:"input_descriptors"`
}{
Reason: reason,
InputDescriptors: inputDescriptors,
}
return nil
}
12 changes: 6 additions & 6 deletions credential/manifest/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,11 @@ func TestCredentialApplicationBuilder(t *testing.T) {
assert.NotEmpty(t, application)
}

func TestCredentialFulfillmentBuilder(t *testing.T) {
builder := NewCredentialFulfillmentBuilder("manifest-id")
func TestCredentialResponseBuilder(t *testing.T) {
builder := NewCredentialResponseBuilder("manifest-id")
_, err := builder.Build()
assert.Error(t, err)
notReadyErr := "credential fulfillment not ready to be built"
notReadyErr := "credential response not ready to be built"
assert.Contains(t, err.Error(), notReadyErr)

assert.False(t, builder.IsEmpty())
Expand All @@ -173,12 +173,12 @@ func TestCredentialFulfillmentBuilder(t *testing.T) {
assert.NoError(t, err)

// bad map
err = builder.SetDescriptorMap(nil)
err = builder.SetFulfillment(nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot set no submission descriptors")

// another bad map
err = builder.SetDescriptorMap([]exchange.SubmissionDescriptor{
err = builder.SetFulfillment([]exchange.SubmissionDescriptor{
{
ID: "bad",
Path: "bad",
Expand All @@ -188,7 +188,7 @@ func TestCredentialFulfillmentBuilder(t *testing.T) {
assert.Contains(t, err.Error(), "cannot set descriptor map; invalid descriptor")

// good map
err = builder.SetDescriptorMap([]exchange.SubmissionDescriptor{
err = builder.SetFulfillment([]exchange.SubmissionDescriptor{
{
ID: "descriptor-id",
Format: "jwt",
Expand Down
47 changes: 26 additions & 21 deletions credential/manifest/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
// CredentialManifest https://identity.foundation/credential-manifest/#general-composition
type CredentialManifest struct {
ID string `json:"id" validate:"required"`
SpecVersion string `json:"spec_version" validate:"required"`
Issuer Issuer `json:"issuer" validate:"required,dive"`
OutputDescriptors []OutputDescriptor `json:"output_descriptors" validate:"required,dive"`
Format *exchange.ClaimFormat `json:"format,omitempty" validate:"omitempty,dive"`
Expand Down Expand Up @@ -78,7 +79,10 @@ func (od *OutputDescriptor) IsValid() error {

// CredentialApplication https://identity.foundation/credential-manifest/#credential-application
type CredentialApplication struct {
Application Application `json:"credential_application" validate:"required"`
ID string `json:"id" validate:"required"`
SpecVersion string `json:"spec_version" validate:"required"`
ManifestID string `json:"manifest_id" validate:"required"`
Format *exchange.ClaimFormat `json:"format" validate:"required,dive"`
// Must be present if the corresponding manifest contains a presentation_definition
PresentationSubmission *exchange.PresentationSubmission `json:"presentation_submission,omitempty" validate:"omitempty,dive"`
}
Expand All @@ -97,41 +101,42 @@ func (ca *CredentialApplication) IsValid() error {
if err := IsValidCredentialApplication(*ca); err != nil {
return errors.Wrap(err, "application failed json schema validation")
}
if ca.Application.Format != nil {
if err := exchange.IsValidDefinitionClaimFormatDesignation(*ca.Application.Format); err != nil {
if ca.Format != nil {
if err := exchange.IsValidDefinitionClaimFormatDesignation(*ca.Format); err != nil {
return errors.Wrap(err, "application's claim format failed json schema validation")
}
}
return util.NewValidator().Struct(ca)
}

type Application struct {
ID string `json:"id" validate:"required"`
ManifestID string `json:"manifest_id" validate:"required"`
Format *exchange.ClaimFormat `json:"format" validate:"required,dive"`
// CredentialResponse https://identity.foundation/credential-manifest/#credential-response
type CredentialResponse struct {
ID string `json:"id" validate:"required"`
SpecVersion string `json:"spec_version" validate:"required"`
ManifestID string `json:"manifest_id" validate:"required"`
ApplicationID string `json:"application_id"`
Fulfillment *struct {
DescriptorMap []exchange.SubmissionDescriptor `json:"descriptor_map" validate:"required"`
} `json:"fulfillment,omitempty" validate:"omitempty,dive"`
Denial *struct {
Reason string `json:"reason" validate:"required"`
InputDescriptors []string `json:"input_descriptors"`
} `json:"denial,omitempty" validate:"omitempty,dive"`
}

// CredentialFulfillment https://identity.foundation/credential-manifest/#credential-fulfillment
type CredentialFulfillment struct {
ID string `json:"id" validate:"required"`
ManifestID string `json:"manifest_id" validate:"required"`
ApplicationID string `json:"application_id"`
DescriptorMap []exchange.SubmissionDescriptor `json:"descriptor_map" validate:"required"`
}

func (cf *CredentialFulfillment) IsEmpty() bool {
func (cf *CredentialResponse) IsEmpty() bool {
if cf == nil {
return true
}
return reflect.DeepEqual(cf, &CredentialFulfillment{})
return reflect.DeepEqual(cf, &CredentialResponse{})
}

func (cf *CredentialFulfillment) IsValid() error {
func (cf *CredentialResponse) IsValid() error {
if cf.IsEmpty() {
return errors.New("fulfillment is empty")
return errors.New("response is empty")
}
if err := IsValidCredentialFulfillment(*cf); err != nil {
return errors.Wrap(err, "fulfillment failed json schema validation")
if err := IsValidCredentialResponse(*cf); err != nil {
return errors.Wrap(err, "response failed json schema validation")
}
return util.NewValidator().Struct(cf)
}
Expand Down
45 changes: 31 additions & 14 deletions credential/manifest/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (
)

const (
ManifestVector1 string = "cm-manifest-example-1.json"
ManifestVector2 string = "cm-manifest-example-2.json"
ApplicationVector1 string = "cm-application-example-1.json"
FulfillmentExample1 string = "cm-fulfillment-example-1.json"
ManifestVector1 string = "cm-manifest-example-1.json"
ManifestVector2 string = "cm-manifest-example-2.json"
ApplicationVector1 string = "cm-application-example-1.json"
ResponseVector1 string = "cm-response-example-1.json"
ResponseVector2 string = "cm-response-example-2.json"
)

var (
Expand All @@ -23,7 +24,7 @@ var (
// Round trip de/serialize to test our object models, and check validity

func TestCredentialManifest(t *testing.T) {
// example here https://identity.foundation/credential-manifest/#credential-manifest---all-features-exercised
// examples here https://identity.foundation/credential-manifest/#credential-response

t.Run("Credential Manifest Vector 1", func(tt *testing.T) {
vector, err := getTestVector(ManifestVector1)
Expand Down Expand Up @@ -84,21 +85,37 @@ func TestCredentialApplication(t *testing.T) {
})
}

func TestCredentialFulfillment(t *testing.T) {
// example here https://identity.foundation/credential-manifest/#credential-fulfillment---simple-example
func TestCredentialResponse(t *testing.T) {
// example here https://identity.foundation/credential-manifest/#credential-response

t.Run("Credential Fulfillment Vector 1", func(tt *testing.T) {
vector, err := getTestVector(FulfillmentExample1)
t.Run("Credential Response - Fulfillment Vector 1", func(tt *testing.T) {
vector, err := getTestVector(ResponseVector1)
assert.NoError(tt, err)

var fulfillment CredentialFulfillment
err = json.Unmarshal([]byte(vector), &fulfillment)
var response CredentialResponse
err = json.Unmarshal([]byte(vector), &response)
assert.NoError(tt, err)
assert.NotEmpty(tt, fulfillment)
assert.NotEmpty(tt, response)

assert.NoError(tt, fulfillment.IsValid())
assert.NoError(tt, response.IsValid())

roundTripBytes, err := json.Marshal(fulfillment)
roundTripBytes, err := json.Marshal(response)
assert.NoError(tt, err)
assert.JSONEq(tt, vector, string(roundTripBytes))
})

t.Run("Credential Response - Denial Vector 1", func(tt *testing.T) {
vector, err := getTestVector(ResponseVector2)
assert.NoError(tt, err)

var response CredentialResponse
err = json.Unmarshal([]byte(vector), &response)
assert.NoError(tt, err)
assert.NotEmpty(tt, response)

assert.NoError(tt, response.IsValid())

roundTripBytes, err := json.Marshal(response)
assert.NoError(tt, err)
assert.JSONEq(tt, vector, string(roundTripBytes))
})
Expand Down
21 changes: 8 additions & 13 deletions credential/manifest/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
const (
credentialManifestSchema string = "cm-credential-manifest.json"
credentialApplicationSchema string = "cm-credential-application.json"
credentialFulfillmentSchema string = "cm-credential-fulfillment.json"
credentialResponseSchema string = "cm-credential-response.json"
outputDescriptorsSchema string = "cm-output-descriptors.json"
)

Expand Down Expand Up @@ -49,23 +49,18 @@ func IsValidCredentialApplication(application CredentialApplication) error {
return nil
}

// IsValidCredentialFulfillment validates a given credential fulfillment object against its known JSON schema
func IsValidCredentialFulfillment(fulfillment CredentialFulfillment) error {
fulfillmentWrapper := struct {
CredentialFulfillment `json:"credential_fulfillment"`
}{
CredentialFulfillment: fulfillment,
}
jsonBytes, err := json.Marshal(fulfillmentWrapper)
// IsValidCredentialResponse validates a given credential response object against its known JSON schema
func IsValidCredentialResponse(response CredentialResponse) error {
jsonBytes, err := json.Marshal(response)
if err != nil {
return errors.Wrap(err, "could not marshal fulfillment to JSON")
return errors.Wrap(err, "could not marshal response to JSON")
}
s, err := schema.GetKnownSchema(credentialFulfillmentSchema)
s, err := schema.GetKnownSchema(credentialResponseSchema)
if err != nil {
return errors.Wrap(err, "could not get credential fulfillment schema")
return errors.Wrap(err, "could not get credential response schema")
}
if err = schema.IsJSONValidAgainstSchema(string(jsonBytes), s); err != nil {
logrus.WithError(err).Error("credential fulfillment not valid against schema")
logrus.WithError(err).Error("credential response not valid against schema")
return err
}
return nil
Expand Down
Loading