Skip to content
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
4 changes: 2 additions & 2 deletions database/migration_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
84 changes: 76 additions & 8 deletions database/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
}
}
179 changes: 179 additions & 0 deletions database/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package database
import (
"testing"

"github.com/fiware/VCVerifier/config"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -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)
}
Loading