From 85f10a7729dc0eb1e5370250486b5804216f7066 Mon Sep 17 00:00:00 2001 From: Miguel Ortega Date: Tue, 26 May 2026 14:00:46 +0200 Subject: [PATCH] fix(database): update dcql models to match previous implementation --- database/migration_compat_test.go | 4 +- database/models.go | 84 ++++++++++++-- database/models_test.go | 179 ++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+), 10 deletions(-) diff --git a/database/migration_compat_test.go b/database/migration_compat_test.go index 1a21077..c1c7052 100644 --- a/database/migration_compat_test.go +++ b/database/migration_compat_test.go @@ -26,8 +26,8 @@ const ccsCredentialJSON = `[{"credentialType":"PacketDeliveryService","verifyHol // struct JSON tags: camelCase inputDescriptors, format as array of FormatObject). const ccsPresentationDefinitionJSON = `{"id":"pd-1","inputDescriptors":[{"id":"desc-1","constraints":{"fields":[{"id":"f-1","path":["$.credentialSubject.type"],"optional":false,"filter":{"type":"string","pattern":"PacketDeliveryService"}}]},"format":[{"formatKey":"jwt_vp","alg":["ES256"]}]}],"format":[{"formatKey":"jwt_vp","alg":["ES256"]}]}` -// ccsDcqlJSON matches the CCS Java DCQL Jackson serialization format. -const ccsDcqlJSON = `{"credentials":[{"id":"cred-q-1","format":"dc+sd-jwt","multiple":false,"claims":[{"id":"claim-1","path":["$.credentialSubject.email"]}],"meta":{"vct_values":["PacketDeliveryService"]}}],"credential_sets":[{"options":[["cred-q-1"]],"required":true}]}` +// ccsDcqlJSON matches the Go-internal DCQL serialization format used by the database layer (DCQLDB struct). +const ccsDcqlJSON = `{"credentials":[{"id":"cred-q-1","format":"dc+sd-jwt","multiple":false,"claims":[{"id":"claim-1","path":["$.credentialSubject.email"]}],"meta":{"vct_values":["PacketDeliveryService"]}}],"credentialSets":[{"options":[["cred-q-1"]],"required":true}]}` // newTestSQLiteDB creates a fresh in-memory SQLite database with the schema // initialized. Returns the raw *sql.DB and a cleanup function. diff --git a/database/models.go b/database/models.go index 7938971..e710636 100644 --- a/database/models.go +++ b/database/models.go @@ -8,6 +8,21 @@ import ( "github.com/fiware/VCVerifier/config" ) +var DB_FORMAT_MAP = map[string]string{ + "dc+sd-jwt": "DC_SD_JWT", + "vc+sd-jwt": "VC_SD_JWT", + "mso_mdoc": "MSO_MDOC", + "ldp_vc": "LDP_VC", + "jwt_vc_json": "JWT_VC_JSON", +} +var FORMAT_MAP = map[string]string{ + "DC_SD_JWT": "dc+sd-jwt", + "VC_SD_JWT": "vc+sd-jwt", + "MSO_MDOC": "mso_mdoc", + "LDP_VC": "ldp_vc", + "JWT_VC_JSON": "jwt_vc_json", +} + // ServiceRow represents a row in the service table. type ServiceRow struct { // ID is the unique service identifier (primary key). @@ -405,7 +420,7 @@ type DCQLDB struct { // A non-empty array of Credential Queries that specify the requested Credentials. Credentials []CredentialQueryDB `json:"credentials" mapstructure:"credentials"` // A non-empty array of Credential Set Queries that specifies additional constraints on which of the requested Credentials to return. - CredentialSets []config.CredentialSetQuery `json:"credential_sets,omitempty" mapstructure:"credential_sets,omitempty"` + CredentialSets []config.CredentialSetQuery `json:"credentialSets,omitempty" mapstructure:"credentialSets,omitempty"` } func (dcql DCQLDB) VO() config.DCQL { @@ -438,23 +453,31 @@ type CredentialQueryDB struct { // A boolean which indicates whether multiple Credentials can be returned for this Credential Query. If omitted, the default value is false. Multiple bool `json:"multiple" mapstructure:"multiple" default:"false"` // A non-empty array of objects that specifies claims in the requested Credential. Verifiers MUST NOT point to the same claim more than once in a single query. Wallets SHOULD ignore such duplicate claim queries. - Claims []config.ClaimsQuery `json:"claims" mapstructure:"claims"` + Claims []ClaimsQueryDB `json:"claims" mapstructure:"claims"` // Defines additional properties requested by the Verifier that apply to the metadata and validity data of the Credential. The properties of this object are defined per Credential Format. If empty, no specific constraints are placed on the metadata or validity of the requested Credential. Meta *config.MetaDataQuery `json:"meta,omitempty" mapstructure:"meta,omitempty"` // A boolean which indicates whether the Verifier requires a Cryptographic Holder Binding proof. The default value is true, i.e., a Verifiable Presentation with Cryptographic Holder Binding is required. If set to false, the Verifier accepts a Credential without Cryptographic Holder Binding proof. RequireCryptographicHolderBinding bool `json:"requireCryptographicHolderBinding,omitempty" mapstructure:"requireCryptographicHolderBinding,omitempty" default:"false"` // A non-empty array containing arrays of identifiers for elements in claims that specifies which combinations of claims for the Credential are requested. - ClaimSets [][]string `json:"claim_sets,omitempty" mapstructure:"claim_sets,omitempty"` + ClaimSets [][]string `json:"claimSets,omitempty" mapstructure:"claimSets,omitempty"` // A non-empty array of objects that specifies expected authorities or trust frameworks that certify Issuers, that the Verifier will accept. Every Credential returned by the Wallet SHOULD match at least one of the conditions present in the corresponding trusted_authorities array if present. - TrustedAuthorities []config.TrustedAuthorityQuery `json:"trusted_authorities" mapstructure:"trusted_authorities" default:"[]"` + TrustedAuthorities []config.TrustedAuthorityQuery `json:"trustedAuthorities" mapstructure:"trustedAuthorities" default:"[]"` } func (cq CredentialQueryDB) VO() config.CredentialQuery { + format, ok := FORMAT_MAP[cq.Format] + if !ok { + format = strings.ToLower(cq.Format) + } + claims := make([]config.ClaimsQuery, 0, len(cq.Claims)) + for _, claim := range cq.Claims { + claims = append(claims, claim.VO()) + } vo := config.CredentialQuery{ Id: cq.Id, - Format: strings.ToLower(cq.Format), + Format: format, Multiple: cq.Multiple, - Claims: cq.Claims, + Claims: claims, Meta: cq.Meta, RequireCryptographicHolderBinding: &cq.RequireCryptographicHolderBinding, ClaimSets: cq.ClaimSets, @@ -470,14 +493,59 @@ func (cq CredentialQueryDB) VO() config.CredentialQuery { } func (cq CredentialQueryDB) FromVO(cqVO config.CredentialQuery) CredentialQueryDB { + format, ok := DB_FORMAT_MAP[cqVO.Format] + if !ok { + format = strings.ToUpper(cqVO.Format) + } + claims := make([]ClaimsQueryDB, 0, len(cqVO.Claims)) + for _, claimVO := range cqVO.Claims { + claims = append(claims, ClaimsQueryDB{}.FromVO(claimVO)) + } return CredentialQueryDB{ Id: cqVO.Id, - Format: strings.ToUpper(cqVO.Format), + Format: format, Multiple: cqVO.Multiple, - Claims: cqVO.Claims, + Claims: claims, Meta: cqVO.Meta, RequireCryptographicHolderBinding: cqVO.RequiresCryptographicHolderBinding(), ClaimSets: cqVO.ClaimSets, TrustedAuthorities: cqVO.TrustedAuthorities, } } + +type ClaimsQueryDB struct { + // REQUIRED if claim_sets is present in the Credential Query; OPTIONAL otherwise. A string identifying the particular claim. The value MUST be a non-empty string consisting of alphanumeric, underscore (_), or hyphen (-) characters. Within the particular claims array, the same id MUST NOT be present more than once. + Id string `json:"id,omitempty" mapstructure:"id,omitempty"` + // The value MUST be a non-empty array representing a claims path pointer that specifies the path to a claim within the Credential. See https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-claims-path-pointer + Path []interface{} `json:"path,omitempty" mapstructure:"path,omitempty"` + // A non-empty array of strings, integers or boolean values that specifies the expected values of the claim. If the values property is present, the Wallet SHOULD return the claim only if the type and value of the claim both match exactly for at least one of the elements in the array. + Values []interface{} `json:"values,omitempty" mapstructure:"values,omitempty"` + // MDoc specific parameter, ignored for all other types. The flag can be set to inform that the reader wishes to keep(store) the data. In case of false, its data is only used to be dispalyed and verified. + IntentToRetain bool `json:"intent_to_retain,omitempty" mapstructure:"intent_to_retain,omitempty"` + // MDoc specific parameter, ignored for all other types. Refers to a namespace inside an mdoc. + Namespace string `json:"namespace,omitempty" mapstructure:"namespace,omitempty"` + // MDoc specific parameter, ignored for all other types. Identifier for the data-element in the namespace. + ClaimName string `json:"claimName,omitempty" mapstructure:"claimName,omitempty"` +} + +func (cq ClaimsQueryDB) VO() config.ClaimsQuery { + return config.ClaimsQuery{ + Id: cq.Id, + Path: cq.Path, + Values: cq.Values, + IntentToRetain: cq.IntentToRetain, + Namespace: cq.Namespace, + ClaimName: cq.ClaimName, + } +} + +func (cq ClaimsQueryDB) FromVO(cqVO config.ClaimsQuery) ClaimsQueryDB { + return ClaimsQueryDB{ + Id: cqVO.Id, + Path: cqVO.Path, + Values: cqVO.Values, + IntentToRetain: cqVO.IntentToRetain, + Namespace: cqVO.Namespace, + ClaimName: cqVO.ClaimName, + } +} \ No newline at end of file diff --git a/database/models_test.go b/database/models_test.go index cdf25a6..7fdbb14 100644 --- a/database/models_test.go +++ b/database/models_test.go @@ -3,6 +3,7 @@ package database import ( "testing" + "github.com/fiware/VCVerifier/config" "github.com/stretchr/testify/assert" ) @@ -61,3 +62,181 @@ func TestRefreshTokenRow_Fields(t *testing.T) { }) } } + +// --------------------------------------------------------------------------- +// ClaimsQueryDB — VO / FromVO round-trip +// --------------------------------------------------------------------------- + +func TestClaimsQueryDB_VO(t *testing.T) { + db := ClaimsQueryDB{ + Id: "claim-1", + Path: []interface{}{"credentialSubject", "familyName"}, + Values: []interface{}{"Smith"}, + IntentToRetain: true, + Namespace: "eu.europa.ec.eudi.pid.1", + ClaimName: "family_name", + } + vo := db.VO() + assert.Equal(t, "claim-1", vo.Id) + assert.Equal(t, []interface{}{"credentialSubject", "familyName"}, vo.Path) + assert.Equal(t, []interface{}{"Smith"}, vo.Values) + assert.True(t, vo.IntentToRetain) + assert.Equal(t, "eu.europa.ec.eudi.pid.1", vo.Namespace) + assert.Equal(t, "family_name", vo.ClaimName) +} + +func TestClaimsQueryDB_FromVO(t *testing.T) { + vo := config.ClaimsQuery{ + Id: "claim-2", + Path: []interface{}{"credentialSubject", "givenName"}, + Values: []interface{}{"John"}, + IntentToRetain: false, + Namespace: "ns", + ClaimName: "given_name", + } + db := ClaimsQueryDB{}.FromVO(vo) + assert.Equal(t, "claim-2", db.Id) + assert.Equal(t, []interface{}{"credentialSubject", "givenName"}, db.Path) + assert.Equal(t, []interface{}{"John"}, db.Values) + assert.False(t, db.IntentToRetain) + assert.Equal(t, "ns", db.Namespace) + assert.Equal(t, "given_name", db.ClaimName) +} + +// --------------------------------------------------------------------------- +// CredentialQueryDB — format mapping in VO / FromVO +// --------------------------------------------------------------------------- + +func TestCredentialQueryDB_VO_FormatMapping(t *testing.T) { + tests := []struct { + dbFormat string + wantFormat string + }{ + {"DC_SD_JWT", "dc+sd-jwt"}, + {"VC_SD_JWT", "vc+sd-jwt"}, + {"MSO_MDOC", "mso_mdoc"}, + {"LDP_VC", "ldp_vc"}, + {"JWT_VC_JSON", "jwt_vc_json"}, + // unknown format falls back to lower-case + {"CUSTOM_FORMAT", "custom_format"}, + } + + for _, tc := range tests { + t.Run(tc.dbFormat, func(t *testing.T) { + db := CredentialQueryDB{Format: tc.dbFormat} + vo := db.VO() + assert.Equal(t, tc.wantFormat, vo.Format) + }) + } +} + +func TestCredentialQueryDB_FromVO_FormatMapping(t *testing.T) { + tests := []struct { + voFormat string + wantFormat string + }{ + {"dc+sd-jwt", "DC_SD_JWT"}, + {"vc+sd-jwt", "VC_SD_JWT"}, + {"mso_mdoc", "MSO_MDOC"}, + {"ldp_vc", "LDP_VC"}, + {"jwt_vc_json", "JWT_VC_JSON"}, + // unknown format falls back to upper-case + {"custom_format", "CUSTOM_FORMAT"}, + } + + for _, tc := range tests { + t.Run(tc.voFormat, func(t *testing.T) { + vo := config.CredentialQuery{Format: tc.voFormat} + db := CredentialQueryDB{}.FromVO(vo) + assert.Equal(t, tc.wantFormat, db.Format) + }) + } +} + +func TestCredentialQueryDB_VO_ClaimsConverted(t *testing.T) { + db := CredentialQueryDB{ + Id: "cred-1", + Format: "DC_SD_JWT", + Claims: []ClaimsQueryDB{ + {Id: "c1", Path: []interface{}{"sub"}, Values: []interface{}{"alice"}}, + {Id: "c2", Namespace: "ns", ClaimName: "age"}, + }, + ClaimSets: [][]string{{"c1", "c2"}}, + } + vo := db.VO() + assert.Len(t, vo.Claims, 2) + assert.Equal(t, "c1", vo.Claims[0].Id) + assert.Equal(t, []interface{}{"sub"}, vo.Claims[0].Path) + assert.Equal(t, "c2", vo.Claims[1].Id) + assert.Equal(t, "age", vo.Claims[1].ClaimName) + assert.Equal(t, [][]string{{"c1", "c2"}}, vo.ClaimSets) +} + +func TestCredentialQueryDB_FromVO_ClaimsConverted(t *testing.T) { + reqBinding := false + vo := config.CredentialQuery{ + Id: "cred-2", + Format: "dc+sd-jwt", + Claims: []config.ClaimsQuery{ + {Id: "c1", Path: []interface{}{"given_name"}}, + }, + RequireCryptographicHolderBinding: &reqBinding, + ClaimSets: [][]string{{"c1"}}, + } + db := CredentialQueryDB{}.FromVO(vo) + assert.Equal(t, "DC_SD_JWT", db.Format) + assert.Len(t, db.Claims, 1) + assert.Equal(t, "c1", db.Claims[0].Id) + assert.Equal(t, []interface{}{"given_name"}, db.Claims[0].Path) + assert.False(t, db.RequireCryptographicHolderBinding) + assert.Equal(t, [][]string{{"c1"}}, db.ClaimSets) +} + +// --------------------------------------------------------------------------- +// DCQLDB — full VO / FromVO conversion +// --------------------------------------------------------------------------- + +func TestDCQLDB_VO(t *testing.T) { + db := DCQLDB{ + Credentials: []CredentialQueryDB{ + {Id: "pid", Format: "DC_SD_JWT", Claims: []ClaimsQueryDB{{Id: "fn", Path: []interface{}{"family_name"}}}}, + }, + CredentialSets: []config.CredentialSetQuery{ + {Options: [][]string{{"pid"}}, Required: true}, + }, + } + vo := db.VO() + assert.Len(t, vo.Credentials, 1) + assert.Equal(t, "pid", vo.Credentials[0].Id) + assert.Equal(t, "dc+sd-jwt", vo.Credentials[0].Format) + assert.Len(t, vo.Credentials[0].Claims, 1) + assert.Equal(t, "fn", vo.Credentials[0].Claims[0].Id) + assert.Len(t, vo.CredentialSets, 1) + assert.Equal(t, [][]string{{"pid"}}, vo.CredentialSets[0].Options) +} + +func TestDCQLDB_FromVO(t *testing.T) { + reqBinding := true + vo := config.DCQL{ + Credentials: []config.CredentialQuery{ + { + Id: "mdl", + Format: "mso_mdoc", + Claims: []config.ClaimsQuery{{Id: "age", Namespace: "org.iso.18013.5.1", ClaimName: "age_over_18"}}, + RequireCryptographicHolderBinding: &reqBinding, + }, + }, + CredentialSets: []config.CredentialSetQuery{ + {Options: [][]string{{"mdl"}}, Required: false}, + }, + } + db := DCQLDB{}.FromVO(vo) + assert.Len(t, db.Credentials, 1) + assert.Equal(t, "MSO_MDOC", db.Credentials[0].Format) + assert.Equal(t, "mdl", db.Credentials[0].Id) + assert.Len(t, db.Credentials[0].Claims, 1) + assert.Equal(t, "age", db.Credentials[0].Claims[0].Id) + assert.Equal(t, "org.iso.18013.5.1", db.Credentials[0].Claims[0].Namespace) + assert.True(t, db.Credentials[0].RequireCryptographicHolderBinding) + assert.Len(t, db.CredentialSets, 1) +}