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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace WalletFramework.Core.Functional.Errors;

public record ObjectRequirementsAreNotMetError<T>(string errorMessage) : Error($"Requirements of Type `{typeof(T).Name}` were not met: {errorMessage}");
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public class CredentialQuery
/// An object defining additional properties requested by the Verifier.
/// </summary>
[JsonProperty(MetaJsonKey)]
public CredentialMetaQuery? Meta { get; set; }
[JsonConverter(typeof(CredentialMetaQueryJsonConverter))]
public CredentialMetaQuery Meta { get; set; } = null!;

/// <summary>
/// This MUST be a string that specifies the format of the requested Verifiable Credential.
Expand All @@ -57,8 +58,15 @@ public static Validation<CredentialQuery> FromJObject(JObject json)
{
var id = json.GetByKey(IdJsonKey)
.OnSuccess(token => token.ToJValue())
.OnSuccess(value => CredentialQueryId.Create(value.Value?.ToString() ?? string.Empty))
.ToOption();
.OnSuccess(value =>
{
if (string.IsNullOrWhiteSpace(value.Value?.ToString()))
{
return new StringIsNullOrWhitespaceError<CredentialQueryId>();
}

return CredentialQueryId.Create(value.Value.ToString());
});

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

private static CredentialQuery Create(
Option<CredentialQueryId> id,
CredentialQueryId id,
string format,
CredentialMetaQuery meta,
Option<IEnumerable<ClaimQuery>> claims,
Option<IEnumerable<ClaimSet>> claimSets) => new()
{
Id = id.ToNullable(),
Id = id,
Format = format,
Meta = meta,
Claims = claims.ToNullable()?.ToArray(),
Expand All @@ -127,40 +135,33 @@ public static class CredentialQueryConstants

public static class CredentialQueryFun
{
public static Option<OneOf<Vct, DocType>> GetRequestedCredentialType(this CredentialQuery credentialQuery)
{
switch (credentialQuery.Format)
public static OneOf<Vct, DocType> GetRequestedCredentialType(this CredentialQuery credentialQuery) =>
credentialQuery.Format switch
{
case Constants.SdJwtVcFormat:
case Constants.SdJwtDcFormat:
return credentialQuery.Meta?.Vcts?.Any() == true
? Option<OneOf<Vct, DocType>>.Some(
Vct.ValidVct(credentialQuery.Meta!.Vcts!.First()).UnwrapOrThrow())
: Option<OneOf<Vct, DocType>>.None;
case Constants.MdocFormat:
return credentialQuery.Meta?.Doctype?.Any() == true
? Option<OneOf<Vct, DocType>>.Some(DocType.ValidDoctype(credentialQuery.Meta!.Doctype).UnwrapOrThrow())
: Option<OneOf<Vct, DocType>>.None;
default:
return Option<OneOf<Vct, DocType>>.None;
}
}
Constants.SdJwtVcFormat or Constants.SdJwtDcFormat
=> Vct.ValidVct(credentialQuery.Meta.Vcts!.First()).UnwrapOrThrow(),
Constants.MdocFormat
=> DocType.ValidDoctype(credentialQuery.Meta.Doctype).UnwrapOrThrow(),
_ => throw new InvalidOperationException("Only sd-jwt-dc and mdoc formats are supported.")
};

public static Option<PresentationCandidate> FindMatchingCandidate(
this CredentialQuery credentialQuery,
IEnumerable<ICredential> credentials)
{
// Determine the credential types requested by the query
var requestedTypes = credentialQuery.Meta?.Vcts?.Concat([credentialQuery.Meta.Doctype]).ToArray();
var requestedTypes = credentialQuery.Format switch
{
Constants.SdJwtVcFormat or Constants.SdJwtDcFormat
=> credentialQuery.Meta.Vcts!.ToArray(),
Constants.MdocFormat
=> [credentialQuery.Meta.Doctype!],
_ => []
};

// Filter credentials by requested types (if any)
var credentialsWhereTypeMatches = credentials
.Where(credential =>
{
return requestedTypes == null
|| !requestedTypes.Any()
|| requestedTypes.Contains(credential.GetCredentialTypeAsString());
})
.Where(credential => requestedTypes.Contains(credential.GetCredentialTypeAsString()))
.ToArray();

// Get the claims and claim sets to be disclosed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;
/// <summary>
/// The credential query meta.
/// </summary>

public class CredentialMetaQuery
{
/// <summary>
/// Specifies allowed values for the type of the requested Verifiable credential.
/// </summary>
[JsonProperty("vct_values")]
[JsonProperty(VctValuesJsonKey)]
public IEnumerable<string>? Vcts { get; set; }

/// <summary>
/// Specifies an allowed value for the doctype of the requested Verifiable credential.
/// </summary>
[JsonProperty("doctype_value")]
[JsonProperty(DoctypeValueJsonKey)]
public string? Doctype { get; set; }

public static Validation<CredentialMetaQuery> FromJObject(JObject json)
Expand Down Expand Up @@ -54,6 +55,12 @@ public static Validation<CredentialMetaQuery> FromJObject(JObject json)
return ValidationFun.Valid(value.Value.ToString());
})
.ToOption();

if (vcts.IsSome == doctype.IsSome)
{
return new ObjectRequirementsAreNotMetError<CredentialMetaQuery>(
"In the CredentialMetaQuery the 'vct_values' and 'doctype_value' must be mutually exclusive.");
}

return ValidationFun.Valid(Create)
.Apply(vcts)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WalletFramework.Core.Functional;

namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;

public class CredentialMetaQueryJsonConverter : JsonConverter<CredentialMetaQuery>
{
public override CredentialMetaQuery ReadJson(JsonReader reader, Type objectType, CredentialMetaQuery? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var jObject = JObject.Load(reader);

return CredentialMetaQuery.FromJObject(jObject).Match(
metadataQuery => metadataQuery,
errors => throw new JsonSerializationException(
$"Failed to deserialize CredentialMetaQuery: {string.Join(", ", errors.Select(e => e.Message))}")
);
}

public override void WriteJson(JsonWriter writer, CredentialMetaQuery? value, JsonSerializer serializer)
{
var jObject = new JObject();

if (value!.Vcts != null)
{
jObject[CredentialMetaFun.VctValuesJsonKey] = JToken.FromObject(value.Vcts, serializer);
}

if (value!.Doctype != null)
{
jObject[CredentialMetaFun.DoctypeValueJsonKey] = JToken.FromObject(value.Doctype, serializer);
}

jObject.WriteTo(writer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void Unsigned_Request_Can_Be_Processed()
var credentialQuery = dcApiRequest.DcqlQuery.CredentialQueries[0];
credentialQuery.Id.AsString().Should().Be("cred1");
credentialQuery.Format.Should().Be("mso_mdoc");
credentialQuery.Meta!.Doctype.Should().Be("org.iso.18013.5.1.mDL");
credentialQuery.Meta.Doctype.Should().Be("org.iso.18013.5.1.mDL");
credentialQuery.Claims.Should().HaveCount(2);

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

var firstClaim = credentialQuery.Claims![0];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;

namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql;

public class CredentialMetaQueryJsonConverterTests
{
private static readonly JsonSerializerSettings Settings = new()
{
Converters =
{
new CredentialMetaQueryJsonConverter()
}
};

[Fact]
public void CanSerializeCredentialMetaQuery()
{
// Arrange
var credentialMetaQuery = new CredentialMetaQuery()
{
Doctype = "DocumentType1"
};

// Act
var json = JsonConvert.SerializeObject(credentialMetaQuery, Settings);

// Assert
Assert.NotNull(json);
}

[Fact]
public void CanDeserializeCredentialMetaQuery()
{
//Arrange
var jsonString = new JObject()
{
["vct_values"] = new JArray(){"VerifiableCredentialType1", "VerifiableCredentialType2"}
}.ToString();

//Act
var credentialMetaQuery = JsonConvert.DeserializeObject<CredentialMetaQuery>(jsonString, Settings);

//Assert
Assert.Equal(["VerifiableCredentialType1", "VerifiableCredentialType2"], credentialMetaQuery!.Vcts);
}

[Fact]
public void ThrowsOnInvalidCredentialMetaQuery_VctValuesAndDoctypeValueIsNotMutuallyExclusive()
{
// Arrange
var invalidJson = new JObject()
{
["vct_values"] = new JArray(){"VerifiableCredentialType1", "VerifiableCredentialType2"},
["doctype_value"] = "DocumentType1"
};

// Act & Assert
Assert.Throws<JsonSerializationException>(() =>
JsonConvert.DeserializeObject<CredentialMetaQuery>(invalidJson.ToString(), Settings));
}

[Fact]
public void ThrowsOnInvalidCredentialMetaQuery()
{
// Arrange
var invalidJson = new JObject()
{
["some"] = new JArray(){1, 2},
["another"] = 1
};

// Act & Assert
Assert.Throws<JsonSerializationException>(() =>
JsonConvert.DeserializeObject<CredentialMetaQuery>(invalidJson.ToString(), Settings));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void Can_Parse_Dcql_Query()

dcqlQuery.CredentialQueries[0].Id.AsString().Should().Be("pid");
dcqlQuery.CredentialQueries[0].Format.Should().Be("dc+sd-jwt");
dcqlQuery.CredentialQueries[0].Meta!.Vcts!
dcqlQuery.CredentialQueries[0].Meta.Vcts!
.First()
.Should()
.Be("https://credentials.example.com/identity_credential");
Expand Down Expand Up @@ -48,7 +48,7 @@ public void Can_Parse_Query_With_Claim_Sets()
var cred = sut.CredentialQueries[0];
cred.Id.AsString().Should().Be("idcard");
cred.Format.Should().Be("dc+sd-jwt");
cred.Meta!.Vcts!.Should().ContainSingle().Which.Should()
cred.Meta.Vcts!.Should().ContainSingle().Which.Should()
.Be("ID-Card");
cred.Claims!.Length.Should().Be(4);
cred.Claims[0].Id!.AsString().Should().Be("a");
Expand Down
Loading