Skip to content

Commit 9e344a5

Browse files
authored
Fix the DCQL missing credential filtering (#349)
* use ValidMdoc in MDoc case Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> * filter missing creds by tracking creds that are only alternatives Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> * ensure filtering ONLY alternative credentials Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> --------- Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>
1 parent dcca3f8 commit 9e344a5

File tree

4 files changed

+121
-3
lines changed

4 files changed

+121
-3
lines changed

src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQuery.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public static Option<OneOf<Vct, DocType>> GetRequestedCredentialType(this Creden
138138
: Option<OneOf<Vct, DocType>>.None;
139139
case Constants.MdocFormat:
140140
return credentialQuery.Meta?.Doctype?.Any() == true
141-
? Option<OneOf<Vct, DocType>>.Some(Vct.ValidVct(credentialQuery.Meta!.Doctype).UnwrapOrThrow())
141+
? Option<OneOf<Vct, DocType>>.Some(DocType.ValidDoctype(credentialQuery.Meta!.Doctype).UnwrapOrThrow())
142142
: Option<OneOf<Vct, DocType>>.None;
143143
default:
144144
return Option<OneOf<Vct, DocType>>.None;

src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/DcqlFun.cs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ private static CandidateQueryResult BuildPresentationCandidateSets(
5252
List<CredentialRequirement> missing)
5353
{
5454
var sets = new List<PresentationCandidateSet>();
55+
var alternativeCredentialIds = new List<string>();
56+
var satisfiedCredentialSets = new List<CredentialSetQuery>();
57+
58+
// First pass: identify satisfied credential sets and collect alternative credential IDs
5559
foreach (var setQuery in credentialSetQueries)
5660
{
5761
var firstMatchingOption = setQuery.Options
@@ -69,11 +73,62 @@ private static CandidateQueryResult BuildPresentationCandidateSets(
6973
if (firstMatchingOption != default)
7074
{
7175
sets.Add(new PresentationCandidateSet(firstMatchingOption.SetCandidates, setQuery.Required));
76+
satisfiedCredentialSets.Add(setQuery);
77+
78+
// Mark credential IDs from alternative options in this set as alternatives
79+
foreach (var option in setQuery.Options)
80+
{
81+
if (option.Ids.Select(id => id.AsString()).SequenceEqual(firstMatchingOption.Option.Ids.Select(id => id.AsString())))
82+
continue;
83+
84+
foreach (var id in option.Ids)
85+
{
86+
alternativeCredentialIds.Add(id.AsString());
87+
}
88+
}
7289
}
7390
}
91+
92+
// Second pass: identify credentials that are ONLY alternatives (not needed for any unsatisfied credential sets)
93+
var onlyAlternativeCredentialIds = new List<string>();
94+
foreach (var alternativeId in alternativeCredentialIds)
95+
{
96+
var isOnlyAlternative = true;
97+
98+
// Check if this credential is needed for any unsatisfied credential sets
99+
foreach (var setQuery in credentialSetQueries)
100+
{
101+
if (satisfiedCredentialSets.Contains(setQuery))
102+
continue; // Skip satisfied sets
103+
104+
// Check if this credential is needed in any option of this unsatisfied set
105+
foreach (var option in setQuery.Options)
106+
{
107+
if (option.Ids.Any(id => id.AsString() == alternativeId))
108+
{
109+
isOnlyAlternative = false;
110+
break;
111+
}
112+
}
113+
114+
if (!isOnlyAlternative)
115+
break;
116+
}
117+
118+
if (isOnlyAlternative)
119+
{
120+
onlyAlternativeCredentialIds.Add(alternativeId);
121+
}
122+
}
123+
124+
// Filter out missing credentials that are ONLY alternatives
125+
var filteredMissing = missing
126+
.Where(requirement => !onlyAlternativeCredentialIds.Contains(requirement.GetIdentifier()))
127+
.ToList();
128+
74129
return new CandidateQueryResult(
75130
sets.Count > 0 ? sets : Option<List<PresentationCandidateSet>>.None,
76-
missing.Count > 0 ? missing : Option<List<CredentialRequirement>>.None
131+
filteredMissing.Count > 0 ? filteredMissing : Option<List<CredentialRequirement>>.None
77132
);
78133
}
79134
}

test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialSetTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,29 @@ public void Candidate_Query_Result_Can_Be_Built_Correctly_From_Dcql_With_One_Cre
6464
candidatesList.Count.Should().Be(2, "should have a candidate for each credential query in the set");
6565
candidateSet.IsRequired.Should().BeTrue();
6666
}
67+
68+
[Fact]
69+
public void Candidate_Query_Result_Can_Be_Built_Correctly_From_Dcql_With_One_Credential_Set_And_Filter_Missing_Credentials_That_Are_Only_Alternatives()
70+
{
71+
// Arrange
72+
var query = DcqlSamples.GetDcqlQueryWithOneCredentialSetAndMultipleSingleOptions;
73+
var idCard = SdJwtSamples.GetIdCardCredential();
74+
var credentials = new List<ICredential> { idCard };
75+
76+
// Act
77+
var result = query.ProcessWith(credentials);
78+
79+
// Assert
80+
result.MissingCredentials.IsNone.Should().BeTrue();
81+
result.Candidates.IsSome.Should().BeTrue();
82+
var sets = result.Candidates.IfNone([]);
83+
var setsList = sets.ToList();
84+
setsList.Count.Should().Be(1, "only one set should be satisfied by the provided credentials");
85+
var candidateSet = setsList[0];
86+
var candidatesList = candidateSet.Candidates.ToList();
87+
candidatesList.Count.Should().Be(1, "should have a candidate for each credential query in the set");
88+
candidateSet.IsRequired.Should().BeTrue();
89+
}
6790

6891
[Fact]
6992
public void Candidate_Set_Wont_Be_Built_When_A_Credential_is_Missing()

test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/Samples/DcqlSamples.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,43 @@ public static class DcqlSamples
106106
}
107107
]
108108
}";
109+
110+
public const string DcqlQueryWithOneCredentialSetAndMultipleSingleOptionsJson = @"{
111+
""credentials"": [
112+
{
113+
""id"": ""idcard"",
114+
""format"": ""dc+sd-jwt"",
115+
""meta"": {
116+
""vct_values"": [""ID-Card""]
117+
},
118+
""claims"": [
119+
{""path"": [""first_name""]},
120+
{""path"": [""last_name""]},
121+
{""path"": [""address"", ""street_address""]}
122+
]
123+
},
124+
{
125+
""id"": ""idcard2"",
126+
""format"": ""dc+sd-jwt"",
127+
""meta"": {
128+
""vct_values"": [""ID-Card-2""]
129+
},
130+
""claims"": [
131+
{""path"": [""first_name""]},
132+
{""path"": [""last_name""]},
133+
{""path"": [""address"", ""street_address""]}
134+
]
135+
}
136+
],
137+
""credential_sets"": [
138+
{
139+
""options"": [
140+
[ ""idcard""],
141+
[ ""idcard2""]
142+
]
143+
}
144+
]
145+
}";
109146

110147
public const string IdCardAndIdCard2NationalitiesSecondIndexQueryJson = @"{
111148
""credentials"": [
@@ -194,7 +231,10 @@ public static class DcqlSamples
194231

195232
public static DcqlQuery GetDcqlQueryWithNoClaims =>
196233
JsonConvert.DeserializeObject<DcqlQuery>(QueryStrWithNoClaims)!;
197-
234+
235+
public static DcqlQuery GetDcqlQueryWithOneCredentialSetAndMultipleSingleOptions =>
236+
JsonConvert.DeserializeObject<DcqlQuery>(DcqlQueryWithOneCredentialSetAndMultipleSingleOptionsJson)!;
237+
198238
public static string GetDcqlQueryAsJsonStr() => GetJsonForTestCase("DcqlQuerySample");
199239

200240
public static DcqlQuery GetIdCardAndIdCard2NationalitiesSecondIndexQuery() =>

0 commit comments

Comments
 (0)