Skip to content

Commit 262279f

Browse files
authored
DCQL - Make the meta property mandatory (#367)
* Makes the meta property in CredentialQuery mandatory Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> * Fix CredentialQuery ID nullability Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> * Rewrite the negated xor Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> --------- Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>
1 parent 729088b commit 262279f

File tree

7 files changed

+160
-35
lines changed

7 files changed

+160
-35
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace WalletFramework.Core.Functional.Errors;
2+
3+
public record ObjectRequirementsAreNotMetError<T>(string errorMessage) : Error($"Requirements of Type `{typeof(T).Name}` were not met: {errorMessage}");

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

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public class CredentialQuery
3030
/// An object defining additional properties requested by the Verifier.
3131
/// </summary>
3232
[JsonProperty(MetaJsonKey)]
33-
public CredentialMetaQuery? Meta { get; set; }
33+
[JsonConverter(typeof(CredentialMetaQueryJsonConverter))]
34+
public CredentialMetaQuery Meta { get; set; } = null!;
3435

3536
/// <summary>
3637
/// This MUST be a string that specifies the format of the requested Verifiable Credential.
@@ -57,8 +58,15 @@ public static Validation<CredentialQuery> FromJObject(JObject json)
5758
{
5859
var id = json.GetByKey(IdJsonKey)
5960
.OnSuccess(token => token.ToJValue())
60-
.OnSuccess(value => CredentialQueryId.Create(value.Value?.ToString() ?? string.Empty))
61-
.ToOption();
61+
.OnSuccess(value =>
62+
{
63+
if (string.IsNullOrWhiteSpace(value.Value?.ToString()))
64+
{
65+
return new StringIsNullOrWhitespaceError<CredentialQueryId>();
66+
}
67+
68+
return CredentialQueryId.Create(value.Value.ToString());
69+
});
6270

6371
var format = json.GetByKey(FormatJsonKey)
6472
.OnSuccess(token => token.ToJValue())
@@ -98,13 +106,13 @@ public static Validation<CredentialQuery> FromJObject(JObject json)
98106
}
99107

100108
private static CredentialQuery Create(
101-
Option<CredentialQueryId> id,
109+
CredentialQueryId id,
102110
string format,
103111
CredentialMetaQuery meta,
104112
Option<IEnumerable<ClaimQuery>> claims,
105113
Option<IEnumerable<ClaimSet>> claimSets) => new()
106114
{
107-
Id = id.ToNullable(),
115+
Id = id,
108116
Format = format,
109117
Meta = meta,
110118
Claims = claims.ToNullable()?.ToArray(),
@@ -127,40 +135,33 @@ public static class CredentialQueryConstants
127135

128136
public static class CredentialQueryFun
129137
{
130-
public static Option<OneOf<Vct, DocType>> GetRequestedCredentialType(this CredentialQuery credentialQuery)
131-
{
132-
switch (credentialQuery.Format)
138+
public static OneOf<Vct, DocType> GetRequestedCredentialType(this CredentialQuery credentialQuery) =>
139+
credentialQuery.Format switch
133140
{
134-
case Constants.SdJwtVcFormat:
135-
case Constants.SdJwtDcFormat:
136-
return credentialQuery.Meta?.Vcts?.Any() == true
137-
? Option<OneOf<Vct, DocType>>.Some(
138-
Vct.ValidVct(credentialQuery.Meta!.Vcts!.First()).UnwrapOrThrow())
139-
: Option<OneOf<Vct, DocType>>.None;
140-
case Constants.MdocFormat:
141-
return credentialQuery.Meta?.Doctype?.Any() == true
142-
? Option<OneOf<Vct, DocType>>.Some(DocType.ValidDoctype(credentialQuery.Meta!.Doctype).UnwrapOrThrow())
143-
: Option<OneOf<Vct, DocType>>.None;
144-
default:
145-
return Option<OneOf<Vct, DocType>>.None;
146-
}
147-
}
141+
Constants.SdJwtVcFormat or Constants.SdJwtDcFormat
142+
=> Vct.ValidVct(credentialQuery.Meta.Vcts!.First()).UnwrapOrThrow(),
143+
Constants.MdocFormat
144+
=> DocType.ValidDoctype(credentialQuery.Meta.Doctype).UnwrapOrThrow(),
145+
_ => throw new InvalidOperationException("Only sd-jwt-dc and mdoc formats are supported.")
146+
};
148147

149148
public static Option<PresentationCandidate> FindMatchingCandidate(
150149
this CredentialQuery credentialQuery,
151150
IEnumerable<ICredential> credentials)
152151
{
153152
// Determine the credential types requested by the query
154-
var requestedTypes = credentialQuery.Meta?.Vcts?.Concat([credentialQuery.Meta.Doctype]).ToArray();
153+
var requestedTypes = credentialQuery.Format switch
154+
{
155+
Constants.SdJwtVcFormat or Constants.SdJwtDcFormat
156+
=> credentialQuery.Meta.Vcts!.ToArray(),
157+
Constants.MdocFormat
158+
=> [credentialQuery.Meta.Doctype!],
159+
_ => []
160+
};
155161

156162
// Filter credentials by requested types (if any)
157163
var credentialsWhereTypeMatches = credentials
158-
.Where(credential =>
159-
{
160-
return requestedTypes == null
161-
|| !requestedTypes.Any()
162-
|| requestedTypes.Contains(credential.GetCredentialTypeAsString());
163-
})
164+
.Where(credential => requestedTypes.Contains(credential.GetCredentialTypeAsString()))
164165
.ToArray();
165166

166167
// Get the claims and claim sets to be disclosed

src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/CredentialMetaQuery.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@ namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;
1212
/// <summary>
1313
/// The credential query meta.
1414
/// </summary>
15+
1516
public class CredentialMetaQuery
1617
{
1718
/// <summary>
1819
/// Specifies allowed values for the type of the requested Verifiable credential.
1920
/// </summary>
20-
[JsonProperty("vct_values")]
21+
[JsonProperty(VctValuesJsonKey)]
2122
public IEnumerable<string>? Vcts { get; set; }
2223

2324
/// <summary>
2425
/// Specifies an allowed value for the doctype of the requested Verifiable credential.
2526
/// </summary>
26-
[JsonProperty("doctype_value")]
27+
[JsonProperty(DoctypeValueJsonKey)]
2728
public string? Doctype { get; set; }
2829

2930
public static Validation<CredentialMetaQuery> FromJObject(JObject json)
@@ -54,6 +55,12 @@ public static Validation<CredentialMetaQuery> FromJObject(JObject json)
5455
return ValidationFun.Valid(value.Value.ToString());
5556
})
5657
.ToOption();
58+
59+
if (vcts.IsSome == doctype.IsSome)
60+
{
61+
return new ObjectRequirementsAreNotMetError<CredentialMetaQuery>(
62+
"In the CredentialMetaQuery the 'vct_values' and 'doctype_value' must be mutually exclusive.");
63+
}
5764

5865
return ValidationFun.Valid(Create)
5966
.Apply(vcts)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Newtonsoft.Json;
2+
using Newtonsoft.Json.Linq;
3+
using WalletFramework.Core.Functional;
4+
5+
namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;
6+
7+
public class CredentialMetaQueryJsonConverter : JsonConverter<CredentialMetaQuery>
8+
{
9+
public override CredentialMetaQuery ReadJson(JsonReader reader, Type objectType, CredentialMetaQuery? existingValue, bool hasExistingValue, JsonSerializer serializer)
10+
{
11+
var jObject = JObject.Load(reader);
12+
13+
return CredentialMetaQuery.FromJObject(jObject).Match(
14+
metadataQuery => metadataQuery,
15+
errors => throw new JsonSerializationException(
16+
$"Failed to deserialize CredentialMetaQuery: {string.Join(", ", errors.Select(e => e.Message))}")
17+
);
18+
}
19+
20+
public override void WriteJson(JsonWriter writer, CredentialMetaQuery? value, JsonSerializer serializer)
21+
{
22+
var jObject = new JObject();
23+
24+
if (value!.Vcts != null)
25+
{
26+
jObject[CredentialMetaFun.VctValuesJsonKey] = JToken.FromObject(value.Vcts, serializer);
27+
}
28+
29+
if (value!.Doctype != null)
30+
{
31+
jObject[CredentialMetaFun.DoctypeValueJsonKey] = JToken.FromObject(value.Doctype, serializer);
32+
}
33+
34+
jObject.WriteTo(writer);
35+
}
36+
}

test/WalletFramework.Oid4Vc.Tests/Oid4Vp/DcApi/DcApiRequestBatchTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public void Unsigned_Request_Can_Be_Processed()
3636
var credentialQuery = dcApiRequest.DcqlQuery.CredentialQueries[0];
3737
credentialQuery.Id.AsString().Should().Be("cred1");
3838
credentialQuery.Format.Should().Be("mso_mdoc");
39-
credentialQuery.Meta!.Doctype.Should().Be("org.iso.18013.5.1.mDL");
39+
credentialQuery.Meta.Doctype.Should().Be("org.iso.18013.5.1.mDL");
4040
credentialQuery.Claims.Should().HaveCount(2);
4141

4242
var firstClaim = credentialQuery.Claims![0];
@@ -77,7 +77,7 @@ public void Signed_Request_Can_Be_Processed()
7777
var credentialQuery = dcApiRequest.DcqlQuery.CredentialQueries[0];
7878
credentialQuery.Id.AsString().Should().Be("cred1");
7979
credentialQuery.Format.Should().Be("mso_mdoc");
80-
credentialQuery.Meta!.Doctype.Should().Be("org.iso.18013.5.1.mDL");
80+
credentialQuery.Meta.Doctype.Should().Be("org.iso.18013.5.1.mDL");
8181
credentialQuery.Claims.Should().HaveCount(2);
8282

8383
var firstClaim = credentialQuery.Claims![0];
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using Newtonsoft.Json;
2+
using Newtonsoft.Json.Linq;
3+
using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;
4+
5+
namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql;
6+
7+
public class CredentialMetaQueryJsonConverterTests
8+
{
9+
private static readonly JsonSerializerSettings Settings = new()
10+
{
11+
Converters =
12+
{
13+
new CredentialMetaQueryJsonConverter()
14+
}
15+
};
16+
17+
[Fact]
18+
public void CanSerializeCredentialMetaQuery()
19+
{
20+
// Arrange
21+
var credentialMetaQuery = new CredentialMetaQuery()
22+
{
23+
Doctype = "DocumentType1"
24+
};
25+
26+
// Act
27+
var json = JsonConvert.SerializeObject(credentialMetaQuery, Settings);
28+
29+
// Assert
30+
Assert.NotNull(json);
31+
}
32+
33+
[Fact]
34+
public void CanDeserializeCredentialMetaQuery()
35+
{
36+
//Arrange
37+
var jsonString = new JObject()
38+
{
39+
["vct_values"] = new JArray(){"VerifiableCredentialType1", "VerifiableCredentialType2"}
40+
}.ToString();
41+
42+
//Act
43+
var credentialMetaQuery = JsonConvert.DeserializeObject<CredentialMetaQuery>(jsonString, Settings);
44+
45+
//Assert
46+
Assert.Equal(["VerifiableCredentialType1", "VerifiableCredentialType2"], credentialMetaQuery!.Vcts);
47+
}
48+
49+
[Fact]
50+
public void ThrowsOnInvalidCredentialMetaQuery_VctValuesAndDoctypeValueIsNotMutuallyExclusive()
51+
{
52+
// Arrange
53+
var invalidJson = new JObject()
54+
{
55+
["vct_values"] = new JArray(){"VerifiableCredentialType1", "VerifiableCredentialType2"},
56+
["doctype_value"] = "DocumentType1"
57+
};
58+
59+
// Act & Assert
60+
Assert.Throws<JsonSerializationException>(() =>
61+
JsonConvert.DeserializeObject<CredentialMetaQuery>(invalidJson.ToString(), Settings));
62+
}
63+
64+
[Fact]
65+
public void ThrowsOnInvalidCredentialMetaQuery()
66+
{
67+
// Arrange
68+
var invalidJson = new JObject()
69+
{
70+
["some"] = new JArray(){1, 2},
71+
["another"] = 1
72+
};
73+
74+
// Act & Assert
75+
Assert.Throws<JsonSerializationException>(() =>
76+
JsonConvert.DeserializeObject<CredentialMetaQuery>(invalidJson.ToString(), Settings));
77+
}
78+
}

test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlParsingTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public void Can_Parse_Dcql_Query()
1717

1818
dcqlQuery.CredentialQueries[0].Id.AsString().Should().Be("pid");
1919
dcqlQuery.CredentialQueries[0].Format.Should().Be("dc+sd-jwt");
20-
dcqlQuery.CredentialQueries[0].Meta!.Vcts!
20+
dcqlQuery.CredentialQueries[0].Meta.Vcts!
2121
.First()
2222
.Should()
2323
.Be("https://credentials.example.com/identity_credential");
@@ -48,7 +48,7 @@ public void Can_Parse_Query_With_Claim_Sets()
4848
var cred = sut.CredentialQueries[0];
4949
cred.Id.AsString().Should().Be("idcard");
5050
cred.Format.Should().Be("dc+sd-jwt");
51-
cred.Meta!.Vcts!.Should().ContainSingle().Which.Should()
51+
cred.Meta.Vcts!.Should().ContainSingle().Which.Should()
5252
.Be("ID-Card");
5353
cred.Claims!.Length.Should().Be(4);
5454
cred.Claims[0].Id!.AsString().Should().Be("a");

0 commit comments

Comments
 (0)