diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/AuthRequest/AuthRequestFun.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/AuthRequest/AuthRequestFun.cs index f5b59e7d..a7c0fb68 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/AuthRequest/AuthRequestFun.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/AuthRequest/AuthRequestFun.cs @@ -1,5 +1,6 @@ using System.Text; using OneOf; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models; diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/CredentialQuery.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQuery.cs similarity index 91% rename from src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/CredentialQuery.cs rename to src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQuery.cs index c5238785..d635d1f7 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/CredentialQuery.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQuery.cs @@ -4,15 +4,16 @@ using OneOf; using WalletFramework.Core.Credentials.Abstractions; using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; using WalletFramework.Core.Json; using WalletFramework.MdocLib; -using WalletFramework.SdJwtVc.Models; -using WalletFramework.Oid4Vc.Oid4Vp.Models; -using WalletFramework.Core.Functional.Errors; using WalletFramework.Oid4Vc.Credential; -using static WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models.CredentialQueryConstants; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; +using WalletFramework.Oid4Vc.Oid4Vp.Models; +using WalletFramework.SdJwtVc.Models; +using static WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries.CredentialQueryConstants; -namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; +namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; /// /// The credential query. @@ -38,10 +39,11 @@ public class CredentialQuery public string Format { get; set; } = null!; /// - /// This MUST be a string identifying the Credential in the response. + /// This MUST be a CredentialQueryId identifying the Credential in the response. /// [JsonProperty(IdJsonKey)] - public string? Id { get; set; } = null!; + [JsonConverter(typeof(CredentialQueryIdJsonConverter))] + public CredentialQueryId Id { get; set; } = null!; /// /// Represents a collection, where each value contains a collection of identifiers for elements in claims that @@ -54,15 +56,8 @@ public static Validation FromJObject(JObject json) { var id = json.GetByKey(IdJsonKey) .OnSuccess(token => token.ToJValue()) - .OnSuccess(value => - { - if (string.IsNullOrWhiteSpace(value.Value?.ToString())) - { - return new StringIsNullOrWhitespaceError(); - } - - return ValidationFun.Valid(value.Value.ToString()); - }).ToOption(); + .OnSuccess(value => CredentialQueryId.Create(value.Value?.ToString() ?? string.Empty)) + .ToOption(); var format = json.GetByKey(FormatJsonKey) .OnSuccess(token => token.ToJValue()) @@ -102,7 +97,7 @@ public static Validation FromJObject(JObject json) } private static CredentialQuery Create( - Option id, + Option id, string format, CredentialMetaQuery meta, Option> claims, @@ -188,9 +183,10 @@ public static Option FindMatchingCandidate( if (groupedCandidates.Any()) { return new PresentationCandidate( - credentialQuery.Id, + credentialQuery.Id.AsString(), groupedCandidates, - disclosures.ToList()); + disclosures.ToList() + ); } } @@ -206,7 +202,7 @@ public static Option FindMatchingCandidate( .ToArray(); return credentialsWhereTypeMatches.Any() - ? new PresentationCandidate(credentialQuery.Id, allCandidates, Option>.None) + ? new PresentationCandidate(credentialQuery.Id.AsString(), allCandidates, Option>.None) : Option.None; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQueryId.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQueryId.cs new file mode 100644 index 00000000..da4491a6 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQueryId.cs @@ -0,0 +1,20 @@ +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; + +public record CredentialQueryId +{ + private readonly string _value; + + public string AsString() => _value; + + private CredentialQueryId(string value) => _value = value; + + public static implicit operator string(CredentialQueryId credentialQueryId) => credentialQueryId._value; + + public static Validation Create(string value) => + string.IsNullOrWhiteSpace(value) + ? new StringIsNullOrWhitespaceError() + : new CredentialQueryId(value); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQueryIdJsonConverter.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQueryIdJsonConverter.cs new file mode 100644 index 00000000..dab50e47 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQueryIdJsonConverter.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; + +public class CredentialQueryIdJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, CredentialQueryId? value, JsonSerializer serializer) + { + writer.WriteValue(value?.AsString()); + } + + public override CredentialQueryId ReadJson(JsonReader reader, Type objectType, CredentialQueryId? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.Value is string str) + { + var result = CredentialQueryId.Create(str); + return result.Match( + success => success, + errors => throw new JsonSerializationException( + $"Failed to deserialize CredentialQueryId: {string.Join(", ", errors.Select(e => e.Message))}") + ); + } + else + { + throw new JsonSerializationException($"Expected string but got {reader.TokenType}"); + } + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQueryIdListJsonConverter.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQueryIdListJsonConverter.cs new file mode 100644 index 00000000..0e3423df --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialQueries/CredentialQueryIdListJsonConverter.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; + +public class CredentialQueryIdListJsonConverter : JsonConverter> +{ + public override void WriteJson(JsonWriter writer, IReadOnlyList? value, JsonSerializer serializer) + { + writer.WriteStartArray(); + foreach (var id in value!) + { + writer.WriteValue(id.AsString()); + } + writer.WriteEndArray(); + } + + public override IReadOnlyList ReadJson(JsonReader reader, Type objectType, IReadOnlyList? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var array = JArray.Load(reader); + var result = array.TraverseAll(token => CredentialQueryId.Create(token.ToString())); + + return result.Match( + list => list.ToArray(), + errors => throw new JsonSerializationException( + $"Failed to deserialize CredentialQueryId list: {string.Join(", ", errors.SelectMany(e => e.Message))}") + ); + } +} \ No newline at end of file diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialSets/CredentialSetJsonConverter.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialSets/CredentialSetJsonConverter.cs new file mode 100644 index 00000000..a29e8072 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialSets/CredentialSetJsonConverter.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets; + +public class CredentialSetJsonConverter : JsonConverter> +{ + public override void WriteJson(JsonWriter writer, IReadOnlyList? value, JsonSerializer serializer) + { + writer.WriteStartArray(); + foreach (var option in value!) + { + writer.WriteStartArray(); + foreach (var id in option.Ids) + { + writer.WriteValue(id.AsString()); + } + writer.WriteEndArray(); + } + writer.WriteEndArray(); + } + + public override IReadOnlyList ReadJson(JsonReader reader, Type objectType, IReadOnlyList? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var array = JArray.Load(reader); + var result = array + .Select(inner => inner.ToObject>() ?? []) + .TraverseAll(CredentialSetOption.FromStrings); + + return result.Match( + list => list.ToList(), + errors => throw new JsonSerializationException( + $"Failed to deserialize CredentialSetOption list: {string.Join(", ", errors.SelectMany(e => e.Message))}") + ); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialSets/CredentialSetOption.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialSets/CredentialSetOption.cs new file mode 100644 index 00000000..8e9dd870 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialSets/CredentialSetOption.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets; + +public record CredentialSetOption( + [property: JsonConverter(typeof(CredentialQueryIdListJsonConverter))] + IReadOnlyList Ids) +{ + public static Validation FromJArray(JArray array) => + from jValues in array.TraverseAll(token => token.ToJValue()) + from queryIds in FromStrings(jValues.Select(value => value.ToString(CultureInfo.InvariantCulture))) + select queryIds; + + public static Validation FromStrings(IEnumerable strings) => + from queryIds in strings.TraverseAll(CredentialQueryId.Create) + select new CredentialSetOption(queryIds.ToArray()); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/CredentialSetQuery.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialSets/CredentialSetQuery.cs similarity index 66% rename from src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/CredentialSetQuery.cs rename to src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialSets/CredentialSetQuery.cs index 3ca09276..e4a20601 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/CredentialSetQuery.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/CredentialSets/CredentialSetQuery.cs @@ -5,34 +5,35 @@ using WalletFramework.Core.Functional.Errors; using WalletFramework.Core.Json; using WalletFramework.Oid4Vc.RelyingPartyAuthentication.RegistrationCertificate; -using static WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models.CredentialSetQueryFun; +using static WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets.CredentialSetQueryConstants; -namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; +namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets; /// /// The credential set query. /// -public class CredentialSetQuery +public record CredentialSetQuery { /// /// Specifies the purpose of the query. /// [JsonProperty(PurposeJsonKey)] [JsonConverter(typeof(PurposeConverter))] - public Purpose[]? Purpose { get; set; } - + public Purpose[]? Purpose { get; init; } + /// /// Indicates whether this set of Credentials is required to satisfy the particular use case at the Verifier. /// [JsonProperty("required")] - public bool Required { get; set; } + public bool Required { get; init; } = true; /// /// Represents a collection, where each value is a list of Credential query identifiers representing one set Credentials that satisfies the use case. /// [JsonProperty(OptionsJsonKey)] - public string[][]? Options { get; set; } - + [JsonConverter(typeof(CredentialSetJsonConverter))] + public List Options { get; private init; } = null!; + public static Validation FromJObject(JObject json) { var purpose = json.GetByKey(PurposeJsonKey) @@ -67,43 +68,39 @@ public static Validation FromJObject(JObject json) }) .ToOption(); - var options = json.GetByKey(OptionsJsonKey) - .OnSuccess(token => token.ToJArray()) - .OnSuccess(array => array.TraverseAll(jToken => jToken.ToJArray())) - .OnSuccess(array => array.TraverseAll(innerArray => + var optionsValidation = + from jToken in json.GetByKey(OptionsJsonKey) + from jArray in jToken.ToJArray() + from options in jArray.TraverseAll(token => { - return innerArray.TraverseAll(x => x.ToJValue()) - .OnSuccess(values => values.Select(value => - { - if (string.IsNullOrWhiteSpace(value.Value?.ToString())) - { - return new StringIsNullOrWhitespaceError(); - } - - return ValidationFun.Valid(value.Value.ToString()); - })) - .OnSuccess(values => values.TraverseAll(x => x)); - })) - .ToOption(); + return + from array in token.ToJArray() + from option in CredentialSetOption.FromJArray(array) + select option; + }) + select options; return ValidationFun.Valid(Create) .Apply(purpose) .Apply(required) - .Apply(options); + .Apply(optionsValidation); } - + private static CredentialSetQuery Create( Option> purpose, Option required, - Option>> options) => new() + IEnumerable options) { - Purpose = purpose.ToNullable()?.ToArray(), - Required = required.ToNullable() ?? false, - Options = options.ToNullable()?.Select(x => x.ToArray()).ToArray() - }; + return new CredentialSetQuery + { + Purpose = purpose.ToNullable()?.ToArray(), + Required = required.ToNullable() ?? false, + Options = options.ToList() + }; + } } -public static class CredentialSetQueryFun +public static class CredentialSetQueryConstants { public const string PurposeJsonKey = "purpose"; public const string RequiredJsonKey = "required"; diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/DcqlFun.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/DcqlFun.cs index a5d8d40b..04c4df67 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/DcqlFun.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/DcqlFun.cs @@ -1,22 +1,79 @@ using LanguageExt; -using WalletFramework.Core.Functional; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Core.Credentials.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; +using WalletFramework.Oid4Vc.Oid4Vp.Query; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets; namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql; internal static class DcqlFun { - internal static Option> FindMatchingCandidates( + internal static CandidateQueryResult ProcessWith( this DcqlQuery query, IEnumerable credentials) { - if (query.CredentialQueries.Length == 0) - return Option>.None; + var pairs = query.CredentialQueries + .Select(q => (Query: q, Candidate: q.FindMatchingCandidate(credentials))) + .ToList(); - return query.CredentialQueries - .TraverseAll(credentialQuery => credentialQuery.FindMatchingCandidate(credentials)) - .ToOption(); + var candidates = pairs + .Choose(x => x.Candidate) + .ToList(); + + var missing = pairs + .Where(x => x.Candidate.IsNone) + .Select(x => new CredentialRequirement(x.Query)) + .ToList(); + + var setQueriesOption = query.CredentialSetQueries == null || query.CredentialSetQueries.Length == 0 + ? Option.None + : query.CredentialSetQueries; + + return setQueriesOption.Match( + setQueries => BuildPresentationCandidateSets(setQueries, candidates, missing), + () => { + var singleSets = candidates.Count > 0 + ? candidates.Select(c => new PresentationCandidateSet([c])).ToList() + : Option>.None; + + return new CandidateQueryResult( + singleSets, + missing.Count > 0 ? missing : Option>.None + ); + } + ); + } + + private static CandidateQueryResult BuildPresentationCandidateSets( + CredentialSetQuery[] credentialSetQueries, + IReadOnlyList candidates, + List missing) + { + var sets = new List(); + foreach (var setQuery in credentialSetQueries) + { + var firstMatchingOption = setQuery.Options + .Select(option => + { + return ( + Option: option, + SetCandidates: candidates + .Where(c => option.Ids.Select(id => id.AsString()).Contains(c.Identifier)) + .ToList() + ); + }) + .FirstOrDefault(x => x.SetCandidates.Count == x.Option.Ids.Count); + + if (firstMatchingOption != default) + { + sets.Add(new PresentationCandidateSet(firstMatchingOption.SetCandidates, setQuery.Required)); + } + } + return new CandidateQueryResult( + sets.Count > 0 ? sets : Option>.None, + missing.Count > 0 ? missing : Option>.None + ); } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/ClaimQuery.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/ClaimQuery.cs index 0a5a88fc..a0668d47 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/ClaimQuery.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/ClaimQuery.cs @@ -11,6 +11,7 @@ using WalletFramework.MdocVc; using WalletFramework.Oid4Vc.Oid4Vci.Implementations; using WalletFramework.Oid4Vc.Oid4Vp.ClaimPaths; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; using WalletFramework.Oid4Vc.RelyingPartyAuthentication.RegistrationCertificate; using WalletFramework.SdJwtVc.Models.Records; using static WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models.ClaimQueryConstants; diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/DcqlQuery.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/DcqlQuery.cs index 9c60e90b..995e4e8f 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/DcqlQuery.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/DcqlQuery.cs @@ -1,11 +1,13 @@ using Newtonsoft.Json; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets; namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; /// /// Represents constraints on the combinations of credentials and claims that articulate what Verifier requires /// -public class DcqlQuery +public record DcqlQuery { /// /// Represents a collection of Credential queries diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Services/DcqlService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Services/DcqlService.cs index 0bb590fd..a0f383f1 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Services/DcqlService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Services/DcqlService.cs @@ -4,6 +4,7 @@ using WalletFramework.Core.Functional; using WalletFramework.Core.Credentials.Abstractions; using WalletFramework.Oid4Vc.Oid4Vci.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; @@ -16,7 +17,7 @@ public class DcqlService( IMdocStorage mdocStorage, ISdJwtVcHolderService sdJwtVcHolderService) : IDcqlService { - public async Task>> Query(DcqlQuery query) + public async Task Query(DcqlQuery query) { var context = await agentProvider.GetContextAsync(); var sdJwtRecords = await sdJwtVcHolderService.ListAsync(context); @@ -24,7 +25,7 @@ public async Task>> Query(DcqlQuery qu var mdocs = mdocsOption.ToNullable() ?? []; var credentials = sdJwtRecords.Cast().Concat(mdocs); - return query.FindMatchingCandidates(credentials); + return query.ProcessWith(credentials); } public async Task> QuerySingle(CredentialQuery query) diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Services/IDcqlService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Services/IDcqlService.cs index 780871a9..068f7e7d 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Services/IDcqlService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Services/IDcqlService.cs @@ -1,4 +1,5 @@ using LanguageExt; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; using WalletFramework.Oid4Vc.Oid4Vp.Models; @@ -10,7 +11,7 @@ public interface IDcqlService /// Finds the presentation candidates based on the provided credentials and DCQL query. /// /// An array of presentation candidates, each containing a list of credentials that match the DCQL query. - Task>> Query(DcqlQuery query); + Task Query(DcqlQuery query); /// /// Finds a presentation candidate based on the provided credentials and credential query. diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/CandidateQueryResult.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/CandidateQueryResult.cs new file mode 100644 index 00000000..3ecfc453 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/CandidateQueryResult.cs @@ -0,0 +1,14 @@ +using LanguageExt; +using WalletFramework.Oid4Vc.Oid4Vp.Query; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Models; + +public record CandidateQueryResult( + Option> Candidates, + Option> MissingCredentials); + +public static class CandidateQueryResultFun +{ + public static Option> FlattenCandidates(this CandidateQueryResult result) => + result.Candidates.Map(candidates => candidates.SelectMany(c => c.Candidates).ToList()); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationCandidate.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationCandidate.cs index c211511c..56939dad 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationCandidate.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationCandidate.cs @@ -69,9 +69,13 @@ public static PresentationCandidate AddTransactionDatas( this PresentationCandidate candidate, IEnumerable transactionDatas) { + var td = candidate.TransactionData.Match( + list => list.Append(transactionDatas), + transactionDatas.ToList); + return candidate with { - TransactionData = transactionDatas.ToList() + TransactionData = td.ToList() }; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationCandidateSet.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationCandidateSet.cs new file mode 100644 index 00000000..ad5ad2be --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationCandidateSet.cs @@ -0,0 +1,11 @@ +namespace WalletFramework.Oid4Vc.Oid4Vp.Models; + +public record PresentationCandidateSet(List Candidates, bool IsRequired = true); + +public static class PresentationCandidateSetFun +{ + public static PresentationCandidateSet ToSet(this IEnumerable candidates) + { + return new PresentationCandidateSet([.. candidates]); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationRequest.cs index 11865f9e..4912a3a6 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationRequest.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/PresentationRequest.cs @@ -5,7 +5,7 @@ namespace WalletFramework.Oid4Vc.Oid4Vp.Models; public record PresentationRequest( AuthorizationRequest AuthorizationRequest, - Option> Candidates) + CandidateQueryResult CandidateQueryResult) { public Option RpAuthResult => AuthorizationRequest.RpAuthResult; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/SelectedCredential.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/SelectedCredential.cs index bb03e5e0..731b53a5 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/SelectedCredential.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/SelectedCredential.cs @@ -1,5 +1,6 @@ using LanguageExt; using WalletFramework.Core.Credentials.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; using WalletFramework.Oid4Vc.Oid4Vp.TransactionDatas; using WalletFramework.Oid4Vc.Qes.Authorization; diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/IPexService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/IPexService.cs index 9e02222f..8a418820 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/IPexService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/IPexService.cs @@ -13,7 +13,7 @@ public interface IPexService /// Finds the presentation candidates based on the provided credentials and input descriptors. /// /// An array of credential candidates, each containing a list of credentials that match the input descriptors. - Task>> FindPresentationCandidatesAsync(PresentationDefinition presentationDefinition, Option supportedFormatSigningAlgorithms); + Task FindPresentationCandidatesAsync(PresentationDefinition presentationDefinition, Option supportedFormatSigningAlgorithms); /// /// Finds a presentation candidate based on the provided input descriptor. diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/PexService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/PexService.cs index 983740a6..546adc89 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/PexService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/PexService.cs @@ -11,6 +11,7 @@ using WalletFramework.Oid4Vc.Oid4Vci.Implementations; using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models; +using WalletFramework.Oid4Vc.Oid4Vp.Query; using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; namespace WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Services; @@ -21,17 +22,28 @@ public class PexService( IMdocStorage mdocStorage, ISdJwtVcHolderService sdJwtVcHolderService) : IPexService { - public async Task>> FindPresentationCandidatesAsync(PresentationDefinition presentationDefinition, Option supportedFormatSigningAlgorithms) + public async Task FindPresentationCandidatesAsync(PresentationDefinition presentationDefinition, Option supportedFormatSigningAlgorithms) { var candidates = await FindCandidates( presentationDefinition.InputDescriptors, supportedFormatSigningAlgorithms); - var list = candidates.ToList(); + var candidateList = candidates.ToList(); + var candidatesOption = candidateList.Count == 0 + ? Option>.None + : new List { candidateList.ToSet() }; - return list.Count == 0 - ? Option>.None - : list; + // Find missing credentials: input descriptors with no candidates + var missing = presentationDefinition.InputDescriptors + .Where(inputDescriptor => candidateList.All(c => c.Identifier != inputDescriptor.Id)) + .Select(inputDescriptor => new CredentialRequirement(inputDescriptor)) + .ToList(); + var missingOption = missing.Count > 0 ? missing : Option>.None; + + return new CandidateQueryResult( + candidatesOption, + missingOption + ); } public async Task> FindPresentationCandidateAsync(InputDescriptor inputDescriptor) diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Query/CredentialRequirement.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Query/CredentialRequirement.cs index df59e227..45e462ed 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Query/CredentialRequirement.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Query/CredentialRequirement.cs @@ -2,8 +2,8 @@ using LanguageExt; using OneOf; using WalletFramework.MdocLib; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; -using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models; using WalletFramework.SdJwtVc.Models; @@ -66,7 +66,7 @@ public static string FormatForLog(this CredentialRequirement requirement) => public static string GetIdentifier(this CredentialRequirement requirement) => requirement.GetQuery().Match( - credentialQuery => credentialQuery.Id, + credentialQuery => credentialQuery.Id.AsString(), inputDescriptor => inputDescriptor.Id); public static IEnumerable GetRequestedAttributes(this CredentialRequirement requirement, Option> claimsToDisclose) => diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/CandidateQueryService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/CandidateQueryService.cs index 3daca56a..728f5108 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/CandidateQueryService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/CandidateQueryService.cs @@ -1,6 +1,6 @@ using LanguageExt; using OneOf; -using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Services; using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models; @@ -12,13 +12,18 @@ public class CandidateQueryService( IPexService pexService, IDcqlService dcqlService) : ICandidateQueryService { - public async Task>> Query(AuthorizationRequest authRequest) - { - return await authRequest.Requirements.Match( + public async Task Query(AuthorizationRequest authRequest) => + await authRequest.Requirements.Match( dcqlService.Query, - presentationDefinition => pexService.FindPresentationCandidatesAsync(presentationDefinition, authRequest.ClientMetadata?.Formats)); - } - + presentationDefinition => + { + return pexService.FindPresentationCandidatesAsync( + presentationDefinition, + authRequest.ClientMetadata?.Formats + ); + } + ); + public async Task> Query(OneOf credentialRequirement) { return await credentialRequirement.Match( diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/ICandidateQueryService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/ICandidateQueryService.cs index 25fc8853..fdacf93d 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/ICandidateQueryService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/ICandidateQueryService.cs @@ -1,6 +1,6 @@ using LanguageExt; using OneOf; -using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models; @@ -8,7 +8,7 @@ namespace WalletFramework.Oid4Vc.Oid4Vp.Services; public interface ICandidateQueryService { - Task>> Query(AuthorizationRequest authRequest); + Task Query(AuthorizationRequest authRequest); Task> Query(OneOf credentialRequirement); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs index 0ed00381..884a2b14 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs @@ -30,12 +30,11 @@ using WalletFramework.Oid4Vc.Oid4Vci.Implementations; using WalletFramework.Oid4Vc.Oid4Vp.AuthResponse.Encryption; using WalletFramework.Oid4Vc.Oid4Vp.AuthResponse.Encryption.Abstractions; -using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; using WalletFramework.Oid4Vc.Oid4Vp.Errors; using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models; using WalletFramework.Oid4Vc.Oid4Vp.TransactionDatas; -using WalletFramework.Oid4Vc.Oid4Vp.TransactionDatas.Errors; using WalletFramework.Oid4Vc.Qes.Authorization; using WalletFramework.SdJwtVc.Models; using WalletFramework.SdJwtVc.Models.Records; @@ -514,8 +513,10 @@ from keyAuths in mdoc.IssuerSigned.IssuerAuth.Payload.DeviceKeyInfo.KeyAuthoriza var sigStructureHash = sha256.ComputeHash(sigStructureByteString.EncodeToBytes()); - var mDocPostContent = new JObject(); - mDocPostContent.Add("hash_bytes", Base64UrlEncoder.Encode(sigStructureHash)); + var mDocPostContent = new JObject + { + { "hash_bytes", Base64UrlEncoder.Encode(sigStructureHash) } + }; var mDocHttpContent = new StringContent @@ -674,8 +675,8 @@ public async Task { - var candidates = (await _candidateQueryService.Query(authRequest)).OnSome(enumerable => enumerable.ToList()); - var presentationCandidates = new PresentationRequest(authRequest, candidates); + var queryResult = await _candidateQueryService.Query(authRequest); + var presentationCandidates = new PresentationRequest(authRequest, queryResult); var vpTxDataOption = presentationCandidates.AuthorizationRequest.TransactionData; @@ -684,8 +685,12 @@ public async Task Option>.None, presentationDefinition => presentationDefinition.InputDescriptors.TraverseAny(descriptor => - descriptor.TransactionData.OnSome(list => - new InputDescriptorTransactionData(descriptor.Id, list)))); + { + return + from list in descriptor.TransactionData + select new InputDescriptorTransactionData(descriptor.Id, list); + }) + ); switch (vpTxDataOption.IsSome, uc5TxDataOption.IsSome) { @@ -693,12 +698,12 @@ public async Task candidates)).Flatten(); } - - private static Validation ProcessVpTransactionData( - PresentationRequest presentationRequest, - IEnumerable vpTransactionDatas) - { - var result = presentationRequest.Candidates.Match( - candidates => - { - var transactionDatas = vpTransactionDatas.ToList(); - var candidatesValidation = transactionDatas - .TraverseAll(candidates.FindCandidateForTransactionData) - .OnSuccess(matches => - { - return matches - .GroupBy(match => match.GetIdentifier()) - .Select(group => - { - var txData = group.Select(match => match.TransactionData).ToList(); - return group.First().Candidate.AddTransactionDatas(txData); - }) - .ToList(); - }); - - return - from presentationCandidates in candidatesValidation - select presentationRequest with { Candidates = presentationCandidates }; - }, - () => new InvalidTransactionDataError( - "No credentials found that satisfy the authorization request with transaction data", - presentationRequest).ToInvalid() - ); - - return result.Value.MapFail(error => - { - var responseUriOption = presentationRequest.AuthorizationRequest.GetResponseUriMaybe(); - var vpError = error as VpError ?? new InvalidRequestError("Could not parse the Authorization Request"); - return new AuthorizationRequestCancellation(responseUriOption, [vpError]); - }); - } - - private static Validation ProcessUc5TransactionData( - PresentationRequest presentationRequest, - IEnumerable txData) - { - var result = presentationRequest.Candidates.Match( - candidates => - { - var candidatesValidation = txData.TraverseAll(inputDescriptorTxData => - { - Option candidateOption = candidates.FirstOrDefault( - candidate => string.Equals(candidate.Identifier, inputDescriptorTxData.InputDescriptorId)); - - return candidateOption.Match( - candidate => candidate.AddUc5TransactionData(inputDescriptorTxData.TransactionData), - () => (Validation)new InvalidTransactionDataError( - "No credentials found that satisfy the authorization request with transaction data", - presentationRequest) - ); - }); - - return candidatesValidation.OnSuccess(enumerable => presentationRequest with - { - Candidates = enumerable.ToList() - }); - }, - () => presentationRequest); - - return result.Value.MapFail(error => - { - var responseUriOption = presentationRequest.AuthorizationRequest.GetResponseUriMaybe(); - var vpError = error as VpError ?? new InvalidRequestError("Could not parse the Authorization Request"); - return new AuthorizationRequestCancellation(responseUriOption, [vpError]); - }); - } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/TransactionDatas/TransactionData.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/TransactionDatas/TransactionData.cs index 6c39d005..9d8aa85a 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/TransactionDatas/TransactionData.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/TransactionDatas/TransactionData.cs @@ -1,11 +1,7 @@ -using LanguageExt; using OneOf; using WalletFramework.Core.Base64Url; -using WalletFramework.Core.Credentials; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Oid4Vc.Oid4Vp.Models; -using WalletFramework.Oid4Vc.Oid4Vp.TransactionDatas.Errors; using WalletFramework.Oid4Vc.Payment; using WalletFramework.Oid4Vc.Qes.Authorization; using WalletFramework.Oid4Vc.Qes.CertCreation; @@ -43,45 +39,3 @@ public static TransactionData WithQCertCreationTransactionData(QCertCreationTran return new TransactionData(input); } } - -public static class TransactionDataFun -{ - public static TransactionDataType GetTransactionDataType(this TransactionData transactionData) => - transactionData.GetTransactionDataProperties().Type; - - public static IEnumerable GetHashesAlg(this TransactionData transactionData) => - transactionData.GetTransactionDataProperties().TransactionDataHashesAlg; - - public static Base64UrlString GetEncoded(this TransactionData transactionData) => - transactionData.GetTransactionDataProperties().Encoded; - - public static Validation FindCandidateForTransactionData( - this IEnumerable candidates, - TransactionData transactionData) - { - var result = candidates.FirstOrDefault(candidate => - { - return transactionData - .GetTransactionDataProperties() - .CredentialIds - .Select(id => id.AsString) - .Contains(candidate.Identifier); - }); - - if (result is null) - { - var error = new InvalidTransactionDataError("Not enough credentials found to satisfy the authorization request with transaction data"); - return error.ToInvalid(); - } - else - { - return new CandidateTxDataMatch(result, transactionData); - } - } - - private static TransactionDataProperties GetTransactionDataProperties(this TransactionData transactionData) => - transactionData.Match( - payment => payment.TransactionDataProperties, - qes => qes.TransactionDataProperties, - qcert => qcert.TransactionDataProperties); -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/TransactionDatas/TransactionDataFun.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/TransactionDatas/TransactionDataFun.cs new file mode 100644 index 00000000..55e40b62 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/TransactionDatas/TransactionDataFun.cs @@ -0,0 +1,175 @@ +using LanguageExt; +using WalletFramework.Core.Base64Url; +using WalletFramework.Core.Functional; +using WalletFramework.Oid4Vc.Oid4Vp.Errors; +using WalletFramework.Oid4Vc.Oid4Vp.Models; +using WalletFramework.Oid4Vc.Oid4Vp.TransactionDatas.Errors; +using WalletFramework.Oid4Vc.Qes.Authorization; + +namespace WalletFramework.Oid4Vc.Oid4Vp.TransactionDatas; + +internal static class TransactionDataFun +{ + public static Validation FindCandidateForTransactionData( + this IEnumerable candidates, + TransactionData transactionData) + { + var result = candidates.FirstOrDefault(candidate => + { + return transactionData + .GetTransactionDataProperties() + .CredentialIds + .Select(id => id.AsString) + .Contains(candidate.Identifier); + }); + + if (result is null) + { + var error = new InvalidTransactionDataError("Not enough credentials found to satisfy the authorization request with transaction data"); + return error.ToInvalid(); + } + else + { + return new CandidateTxDataMatch(result, transactionData); + } + } + + public static Base64UrlString GetEncoded(this TransactionData transactionData) => + transactionData.GetTransactionDataProperties().Encoded; + + public static IEnumerable GetHashesAlg(this TransactionData transactionData) => + transactionData.GetTransactionDataProperties().TransactionDataHashesAlg; + + public static TransactionDataType GetTransactionDataType(this TransactionData transactionData) => + transactionData.GetTransactionDataProperties().Type; + + internal static Validation ProcessUc5TransactionData( + PresentationRequest presentationRequest, + IEnumerable txData) + { + var result = presentationRequest.CandidateQueryResult.Candidates.Match( + candidateSets => + { + // Flatten to (setIndex, candidateIndex, candidate) + var indexedCandidates = candidateSets + .SelectMany((set, setIdx) => + set.Candidates.Select((candidate, candIdx) => (setIdx, candIdx, candidate))) + .ToList(); + + var updatedCandidates = indexedCandidates.ToDictionary(x => (x.setIdx, x.candIdx), x => x.candidate); + + foreach (var inputDescriptorTxData in txData) + { + var found = indexedCandidates.FirstOrDefault(x => + x.candidate.Identifier == inputDescriptorTxData.InputDescriptorId); + + if (found == default) + { + return new InvalidTransactionDataError( + $"No credentials found that satisfy the authorization request for input descriptor {inputDescriptorTxData.InputDescriptorId}", + presentationRequest).ToInvalid(); + } + + // Add the UC5 transaction data to the candidate + var updated = found.candidate.AddUc5TransactionData(inputDescriptorTxData.TransactionData); + updatedCandidates[(found.setIdx, found.candIdx)] = updated; + } + + // Reconstruct sets with updated candidates + var newSets = candidateSets.Select((set, setIdx) => + set with + { + Candidates = [.. set.Candidates.Select((_, candIdx) => updatedCandidates[(setIdx, candIdx)])] + }).ToList(); + + var newResult = presentationRequest.CandidateQueryResult with + { + Candidates = newSets + }; + + return presentationRequest with { CandidateQueryResult = newResult }; + }, + () => presentationRequest + ); + + return result.Value.MapFail(error => + { + var responseUriOption = presentationRequest.AuthorizationRequest.GetResponseUriMaybe(); + var vpError = error as VpError ?? new InvalidRequestError("Could not parse the Authorization Request"); + return new AuthorizationRequestCancellation(responseUriOption, [vpError]); + }); + } + + internal static Validation ProcessVpTransactionData( + PresentationRequest presentationRequest, + IEnumerable transactionDatas) + { + var result = presentationRequest.CandidateQueryResult.Candidates.Match( + candidateSets => + { + // Flatten to (setIndex, candidateIndex, candidate) + var indexedCandidates = candidateSets + .SelectMany((set, setIdx) => + { + return set.Candidates.Select((candidate, candIdx) => (setIdx, candIdx, candidate)); + }) + .ToList(); + + var updatedCandidates = indexedCandidates.ToDictionary( + tuple => (tuple.setIdx, tuple.candIdx), + tuple => tuple.candidate + ); + + foreach (var txData in transactionDatas) + { + var found = indexedCandidates.FirstOrDefault(tuple => + { + return new[] { tuple.candidate }.FindCandidateForTransactionData(txData).IsSuccess; + }); + + if (found == default) + { + return new InvalidTransactionDataError( + $"No credentials found that satisfy the transaction data with type {txData.GetTransactionDataType().AsString()}", + presentationRequest).ToInvalid(); + } + + // Update the candidate with the transaction data + var updated = found.candidate.AddTransactionDatas([txData]); + updatedCandidates[(found.setIdx, found.candIdx)] = updated; + } + + // Reconstruct sets with updated candidates + var newSets = candidateSets.Select((set, setIdx) => + set with + { + Candidates = [.. set.Candidates.Select((_, candIdx) => updatedCandidates[(setIdx, candIdx)])] + }).ToList(); + + var newResult = presentationRequest.CandidateQueryResult with + { + Candidates = newSets + }; + + return presentationRequest with { CandidateQueryResult = newResult }; + }, + () => new InvalidTransactionDataError( + "No credentials found that satisfy the authorization request with transaction data", + presentationRequest) + .ToInvalid() + ); + + return result.Value.MapFail(error => + { + var responseUriOption = presentationRequest.AuthorizationRequest.GetResponseUriMaybe(); + var vpError = error as VpError ?? new InvalidRequestError("Could not parse the Authorization Request"); + return new AuthorizationRequestCancellation(responseUriOption, [vpError]); + }); + } + + private static TransactionDataProperties GetTransactionDataProperties(this TransactionData transactionData) => + transactionData.Match( + payment => payment.TransactionDataProperties, + qes => qes.TransactionDataProperties, + qcert => qcert.TransactionDataProperties); +} diff --git a/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/OverAskingValidationResult.cs b/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/OverAskingValidationResult.cs index 8bd4ced4..c8b584e8 100644 --- a/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/OverAskingValidationResult.cs +++ b/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/OverAskingValidationResult.cs @@ -1,5 +1,6 @@ using LanguageExt; using WalletFramework.Core.X509; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; using WalletFramework.Oid4Vc.Oid4Vp.Models; diff --git a/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/Purpose.cs b/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/Purpose.cs index b7dca086..847111a4 100644 --- a/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/Purpose.cs +++ b/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/Purpose.cs @@ -79,8 +79,7 @@ public override Purpose[] ReadJson( { JArray => token.ToJArray() .OnSuccess(array => array.TraverseAll(jToken => jToken.ToJObject())) - .OnSuccess(array => - array.Select(Purpose.FromJObject)) + .OnSuccess(array =>array.Select(Purpose.FromJObject)) .OnSuccess(array => array.TraverseAll(x => x)), JValue => token.ToJValue() .OnSuccess(Purpose.FromJValue) diff --git a/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/RegistrationCertificate.cs b/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/RegistrationCertificate.cs index c0631427..78597689 100644 --- a/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/RegistrationCertificate.cs +++ b/src/WalletFramework.Oid4Vc/RelyingPartyAuthentication/RegistrationCertificate/RegistrationCertificate.cs @@ -6,6 +6,8 @@ using WalletFramework.Core.Functional; using WalletFramework.Core.Json; using WalletFramework.Core.StatusList; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; using static WalletFramework.Oid4Vc.RelyingPartyAuthentication.RegistrationCertificate.RegistrationCertificateFun; diff --git a/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Dcql/DcqlServiceTests.cs b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Dcql/DcqlServiceTests.cs index 6e5a5dab..8645e392 100644 --- a/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Dcql/DcqlServiceTests.cs +++ b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Dcql/DcqlServiceTests.cs @@ -11,6 +11,8 @@ using WalletFramework.Core.Functional; using WalletFramework.Oid4Vc; using WalletFramework.Oid4Vc.Oid4Vci.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Services; using WalletFramework.Oid4Vc.Oid4Vp.Models; @@ -261,7 +263,7 @@ private static CredentialQuery CreateCredentialQuery(string id, string format, C { var credentialQuery = new CredentialQuery { - Id = id, + Id = CredentialQueryId.Create(id).UnwrapOrThrow(), Format = format, Claims = credentialQueryClaims }; diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialQueryIdJsonConverterTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialQueryIdJsonConverterTests.cs new file mode 100644 index 00000000..55756d85 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialQueryIdJsonConverterTests.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets; +using static WalletFramework.Core.Functional.ValidationFun; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.CredentialSets; + +public class CredentialQueryIdJsonConverterTests +{ + private static readonly JsonSerializerSettings Settings = new() + { + Converters = + { + new CredentialQueryIdJsonConverter() + } + }; + + [Fact] + public void CanSerializeCredentialQueryId() + { + var id = CredentialQueryId.Create("id1").UnwrapOrThrow(); + var json = JsonConvert.SerializeObject(id, Settings); + Assert.Equal("\"id1\"", json); + } + + [Fact] + public void CanDeserializeCredentialQueryId() + { + const string json = "\"id1\""; + var id = JsonConvert.DeserializeObject(json, Settings); + Assert.NotNull(id); + Assert.Equal("id1", id.AsString()); + } + + [Fact] + public void ThrowsOnInvalidCredentialQueryId() + { + const string invalidJson = "null"; + Assert.Throws(() => + JsonConvert.DeserializeObject(invalidJson, Settings)); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialSetJsonConverterTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialSetJsonConverterTests.cs new file mode 100644 index 00000000..220eb9c2 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialSetJsonConverterTests.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialQueries; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets; +using static WalletFramework.Core.Functional.ValidationFun; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.CredentialSets; + +public class CredentialSetJsonConverterTests +{ + private static readonly JsonSerializerSettings Settings = new() + { + Converters = { new CredentialSetJsonConverter() } + }; + + [Fact] + public void CanSerializeCredentialSetOptionList() + { + var list = new List + { + new([ + CredentialQueryId.Create("id1").UnwrapOrThrow(), + CredentialQueryId.Create("id2").UnwrapOrThrow() + ]), + new([ + CredentialQueryId.Create("id3").UnwrapOrThrow() + ]) + }; + var json = JsonConvert.SerializeObject(list, Settings); + Assert.Equal("[[\"id1\",\"id2\"],[\"id3\"]]", json); + } + + [Fact] + public void CanDeserializeCredentialSetOptionList() + { + const string json = "[[\"id1\",\"id2\"],[\"id3\"]]"; + var deserialized = JsonConvert.DeserializeObject>(json, Settings); + Assert.NotNull(deserialized); + Assert.Equal(2, deserialized.Count); + Assert.Equal(["id1", "id2"], deserialized[0].Ids.Select(id => id.AsString())); + Assert.Equal(["id3"], deserialized[1].Ids.Select(id => id.AsString())); + } + + [Fact] + public void ThrowsOnInvalidCredentialSetOptionList() + { + const string invalidJson = "[[123, null], [\"id4\"]]"; + Assert.Throws(() => + JsonConvert.DeserializeObject>(invalidJson, Settings)); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialSetOptionTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialSetOptionTests.cs new file mode 100644 index 00000000..b7c1e841 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialSetOptionTests.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json.Linq; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.CredentialSets; +using static WalletFramework.Core.Functional.ValidationFun; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.CredentialSets; + +public class CredentialSetOptionTests +{ + private const string ValidJson = "[\"id1\", \"id2\", \"id3\"]"; + private const string InvalidJson = "[\"id1\", null, \"\"]"; + + [Fact] + public void Can_Parse_From_Json() + { + var array = JArray.Parse(ValidJson); + + var result = CredentialSetOption.FromJArray(array); + + result.Match( + option => Assert.Equal(["id1", "id2", "id3"], option.Ids.Select(id => id.AsString())), + errors => Assert.Fail($"Expected success but got errors: {string.Join(", ", errors.Select(e => e.Message))}") + ); + } + + [Fact] + public void Invalid_Json_Wont_Be_Parsed() + { + var array = JArray.Parse(InvalidJson); + + var result = CredentialSetOption.FromJArray(array); + + Assert.True(result.IsFail); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialSetTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialSetTests.cs new file mode 100644 index 00000000..ccda0d00 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/CredentialSets/CredentialSetTests.cs @@ -0,0 +1,84 @@ +using FluentAssertions; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql; +using WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.Samples; +using WalletFramework.Oid4Vc.Tests.Samples; +using WalletFramework.Core.Credentials.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vp.Query; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.CredentialSets; + +public class CredentialSetTests +{ + [Fact] + public void Candidate_Query_Result_Can_Be_Built_Correctly_From_Dcql_Query_With_One_Credential_Set() + { + // Arrange + var query = DcqlSamples.GetDcqlQueryWithOneCredentialSet; + var idCard1 = SdJwtSamples.GetIdCardCredential(); + var idCard2 = SdJwtSamples.GetIdCard2Credential(); + var credentials = new List { idCard1, idCard2 }; + + // Act + var result = query.ProcessWith(credentials); + + // Assert + result.Candidates.IsSome.Should().BeTrue(); + var sets = result.Candidates.IfNone([]); + var setsList = sets.ToList(); + setsList.Count.Should().Be(1, "only the required set with idcard and idcard2 should be satisfied"); + var candidateSet = setsList[0]; + var candidatesList = candidateSet.Candidates.ToList(); + candidatesList.Count.Should().Be(2, "should have a candidate for each credential query in the set"); + candidateSet.IsRequired.Should().BeTrue(); + result.MissingCredentials.Match( + list => + { + list.Count.Should().Be(1); + var first = list.First(); + first.GetIdentifier().Should().Be("idcard3"); + }, + () => Assert.Fail("Expected optional credential to be missing") + ); + } + + [Fact] + public void Candidate_Query_Result_Can_Be_Built_Correctly_From_Dcql_With_One_Credential_Set_And_Multiple_Options() + { + // Arrange + var query = DcqlSamples.GetDcqlQueryWithOneCredentialSetAndMultipleOptions; + var idCard = SdJwtSamples.GetIdCardCredential(); + var idCard2 = SdJwtSamples.GetIdCard2Credential(); + var idCard3 = SdJwtSamples.GetIdCard3Credential(); + var credentials = new List { idCard, idCard2, idCard3 }; + + // Act + var result = query.ProcessWith(credentials); + + // Assert + result.Candidates.IsSome.Should().BeTrue(); + var sets = result.Candidates.IfNone([]); + var setsList = sets.ToList(); + setsList.Count.Should().Be(1, "only one set should be satisfied by the provided credentials"); + var candidateSet = setsList[0]; + var candidatesList = candidateSet.Candidates.ToList(); + candidatesList.Count.Should().Be(2, "should have a candidate for each credential query in the set"); + candidateSet.IsRequired.Should().BeTrue(); + } + + [Fact] + public void Candidate_Set_Wont_Be_Built_When_A_Credential_is_Missing() + { + // Arrange + var query = DcqlSamples.GetDcqlQueryWithOneCredentialSetAndMultipleOptions; + var idCard = SdJwtSamples.GetIdCardCredential(); + // idCard2 is missing + var idCard3 = SdJwtSamples.GetIdCard3Credential(); + var credentials = new List { idCard, idCard3 }; + + // Act + var result = query.ProcessWith(credentials); + + // Assert + result.Candidates.IsNone.Should().BeTrue("no set should be satisfied if a required credential is missing"); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlClaimSetsTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlClaimSetsTests.cs new file mode 100644 index 00000000..e4b00bff --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlClaimSetsTests.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.Samples; +using WalletFramework.Oid4Vc.Tests.Samples; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql; +using WalletFramework.Oid4Vc.Oid4Vp.Models; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql; + +public class DcqlClaimSetsTests +{ + [Fact] + public void The_First_Matching_Claim_Set_Is_Disclosed() + { + // Arrange + var dcqlQuery = DcqlSamples.GetDcqlQueryWithClaimsets; + var credentialQuery = dcqlQuery.CredentialQueries[0]; + var expectedClaimIds = credentialQuery.ClaimSets![0].Claims.Select(id => id.AsString()).ToArray(); + var credential = SdJwtSamples.GetIdCardCredential(); + + // Act + var sut = dcqlQuery.ProcessWith([credential]); + + // Assert + sut.FlattenCandidates().Match( + candidates => + { + var presentationCandidates = candidates.ToList(); + presentationCandidates.Should().HaveCount(1); + foreach (var candidate in presentationCandidates) + { + candidate.Identifier.Should().Be(credentialQuery.Id); + } + var claimsToDisclose = presentationCandidates[0].ClaimsToDisclose; + claimsToDisclose.Match( + claims => + { + var claimIds = claims.Select(c => c.Id!.AsString()).ToArray(); + claimIds.Should().BeEquivalentTo(expectedClaimIds); + }, + () => Assert.Fail("Expected claims to be returned, but got none.") + ); + }, + () => Assert.Fail("Expected candidates to be returned, but got none.") + ); + } + + [Fact] + public void No_Claims_Are_Disclosed_When_Claims_in_Dcql_Query_Are_Absent() + { + // Arrange + var dcqlQuery = DcqlSamples.GetDcqlQueryWithNoClaims; + var credential = SdJwtSamples.GetIdCardCredential(); + + // Act + var sut = dcqlQuery.ProcessWith([credential]); + + // Assert + sut.FlattenCandidates().Match( + candidates => + { + var presentationCandidates = candidates.ToList(); + presentationCandidates.Should().HaveCount(1); + foreach (var candidate in presentationCandidates) + { + candidate.Identifier.Should().Be(dcqlQuery.CredentialQueries[0].Id); + } + var claimsToDisclose = presentationCandidates[0].ClaimsToDisclose; + claimsToDisclose.IsNone.Should().BeTrue(); + }, + () => Assert.Fail("Expected candidates to be returned, but got none.") + ); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlFindingCandidatesTests.cs similarity index 64% rename from test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlTests.cs rename to test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlFindingCandidatesTests.cs index a6933fb5..a9507a91 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlFindingCandidatesTests.cs @@ -1,76 +1,22 @@ using FluentAssertions; -using Newtonsoft.Json; -using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; +using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.Samples; using WalletFramework.Oid4Vc.Tests.Samples; using static WalletFramework.Oid4Vc.Oid4Vp.Dcql.DcqlFun; namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql; -public class DcqlTests +public class DcqlFindingCandidatesTests { - [Fact] - public void Can_Parse_Dcql_Query() - { - var json = DcqlSamples.GetDcqlQueryAsJsonStr(); - var dcqlQuery = JsonConvert.DeserializeObject(json)!; - - dcqlQuery.CredentialQueries.Length.Should().Be(5); - - dcqlQuery.CredentialQueries[0].Id.Should().Be("pid"); - dcqlQuery.CredentialQueries[0].Format.Should().Be("dc+sd-jwt"); - dcqlQuery.CredentialQueries[0].Meta!.Vcts! - .First() - .Should() - .Be("https://credentials.example.com/identity_credential"); - - dcqlQuery.CredentialQueries[0].Claims![0].Path.GetPathComponents().Length().Should().Be(1); - dcqlQuery.CredentialQueries[0].Claims![1].Path.GetPathComponents().Length().Should().Be(1); - dcqlQuery.CredentialQueries[0].Claims![2].Path.GetPathComponents().Length().Should().Be(2); - - dcqlQuery.CredentialSetQueries!.Length.Should().Be(2); - dcqlQuery.CredentialSetQueries[0].Purpose.Should().Contain(x => x.Name == "Identification"); - dcqlQuery.CredentialSetQueries[0].Options![0][0].Should().Be("pid"); - dcqlQuery.CredentialSetQueries[0].Options![1][0].Should().Be("other_pid"); - dcqlQuery.CredentialSetQueries[0].Options![2][0].Should().Be("pid_reduced_cred_1"); - dcqlQuery.CredentialSetQueries[0].Options![2][1].Should().Be("pid_reduced_cred_2"); - } - - [Fact] - public void Can_Parse_Query_With_Claim_Sets() - { - // Arrange - const string query = DcqlSamples.QueryStrWithClaimSets; - - // Act - var sut = JsonConvert.DeserializeObject(query)!; - - // Assert - sut.CredentialQueries.Length.Should().Be(1); - var cred = sut.CredentialQueries[0]; - cred.Id.Should().Be("pid"); - cred.Format.Should().Be("dc+sd-jwt"); - cred.Meta!.Vcts!.Should().ContainSingle().Which.Should() - .Be("ID-Card"); - cred.Claims!.Length.Should().Be(4); - cred.Claims[0].Id!.AsString().Should().Be("a"); - cred.Claims[1].Id!.AsString().Should().Be("b"); - cred.Claims[2].Id!.AsString().Should().Be("c"); - cred.Claims[3].Id!.AsString().Should().Be("d"); - cred.ClaimSets!.Count.Should().Be(2); - cred.ClaimSets![0].Claims.Select(c => c.AsString()).Should().BeEquivalentTo("a", "b", "d"); - cred.ClaimSets![1].Claims.Select(c => c.AsString()).Should().BeEquivalentTo("a", "c"); - } - [Fact] public void Can_Process_Mdoc_Credential_Query() { var mdoc = MdocSamples.MdocRecord; var query = DcqlSamples.GetMdocGivenNameQuery(); - var sut = query.FindMatchingCandidates([mdoc]); + var sut = query.ProcessWith([mdoc]); - sut.Match( + sut.FlattenCandidates().Match( candidates => { var presentationCandidates = candidates.ToList(); @@ -104,9 +50,9 @@ public void Can_Process_Mdoc_Credential_Query_With_Multiple_Claims() var mdoc = MdocSamples.MdocRecord; var query = DcqlSamples.GetMdocGivenNameAndFamilyNameQuery(); - var sut = query.FindMatchingCandidates([mdoc]); + var sut = query.ProcessWith([mdoc]); - sut.Match( + sut.FlattenCandidates().Match( candidates => { var presentationCandidates = candidates.ToList(); @@ -142,9 +88,9 @@ public void Can_Process_Mdoc_Credential_Query_With_Multiple_Credentials_In_One_C var mdoc2 = MdocSamples.MdocRecord; var query = DcqlSamples.GetMdocGivenNameQuery(); - var sut = query.FindMatchingCandidates([mdoc1, mdoc2]); + var sut = query.ProcessWith([mdoc1, mdoc2]); - sut.Match( + sut.FlattenCandidates().Match( candidates => { var presentationCandidates = candidates.ToList(); @@ -179,16 +125,16 @@ public void Can_Process_Query_That_Asks_For_SdJwt_And_Mdoc_At_The_Same_Time() var sdJwt = SdJwtSamples.GetIdCardCredential(); var query = DcqlSamples.GetMdocAndSdJwtFamilyNameQuery(); - var sut = query.FindMatchingCandidates([mdoc, sdJwt]); + var sut = query.ProcessWith([mdoc, sdJwt]); - sut.Match( + sut.FlattenCandidates().Match( candidates => { var presentationCandidates = candidates.ToList(); presentationCandidates.Should().HaveCount(2); foreach (var candidate in presentationCandidates) { - query.CredentialQueries.Select(q => q.Id).Should().Contain(candidate.Identifier); + query.CredentialQueries.Select(q => q.Id.AsString()).Should().Contain(candidate.Identifier); var expectedClaims = query.CredentialQueries.First(q => q.Id == candidate.Identifier).Claims!; candidate.ClaimsToDisclose.Match( claims => @@ -215,9 +161,9 @@ public void Can_Process_SdJwt_Credential_Query() var sdJwt = SdJwtSamples.GetIdCardCredential(); var query = DcqlSamples.GetIdCardNationalitiesSecondIndexQuery(); - var sut = query.FindMatchingCandidates([sdJwt]); + var sut = query.ProcessWith([sdJwt]); - sut.Match( + sut.FlattenCandidates().Match( candidates => { var presentationCandidates = candidates.ToList(); @@ -253,16 +199,16 @@ public void Can_Process_SdJwt_Credential_Query_With_Multiple_Candidates() var idCard2 = SdJwtSamples.GetIdCard2Credential(); var query = DcqlSamples.GetIdCardAndIdCard2NationalitiesSecondIndexQuery(); - var sut = query.FindMatchingCandidates([idCard, idCard2]); + var sut = query.ProcessWith([idCard, idCard2]); - sut.Match( + sut.FlattenCandidates().Match( candidates => { var presentationCandidates = candidates.ToList(); presentationCandidates.Should().HaveCount(2); foreach (var candidate in presentationCandidates) { - query.CredentialQueries.Select(q => q.Id).Should().Contain(candidate.Identifier); + query.CredentialQueries.Select(q => q.Id.AsString()).Should().Contain(candidate.Identifier); var expectedClaims = query.CredentialQueries.First(q => q.Id == candidate.Identifier).Claims!; candidate.ClaimsToDisclose.Match( claims => @@ -290,9 +236,9 @@ public void Can_Process_SdJwt_Credential_Query_With_Multiple_Credentials_In_One_ var idCard2 = SdJwtSamples.GetIdCardCredential(); var query = DcqlSamples.GetIdCardNationalitiesSecondIndexQuery(); - var sut = query.FindMatchingCandidates([idCard, idCard2]); + var sut = query.ProcessWith([idCard, idCard2]); - sut.Match( + sut.FlattenCandidates().Match( candidates => { var presentationCandidates = candidates.ToList(); @@ -326,9 +272,9 @@ public void No_Match_Returns_None() var sdJwt = SdJwtSamples.GetIdCardCredential(); var query = DcqlSamples.GetNoMatchErrorClaimPathQuery(); - var sut = query.FindMatchingCandidates([sdJwt]); + var sut = query.ProcessWith([sdJwt]); - sut.Match( + sut.FlattenCandidates().Match( _ => Assert.Fail(), () => { } ); @@ -340,74 +286,11 @@ public void No_Match_Returns_None_For_Mdoc() var mdoc = MdocSamples.MdocRecord; var query = DcqlSamples.GetNoMatchErrorClaimPathQuery(); - var sut = query.FindMatchingCandidates([mdoc]); + var sut = query.ProcessWith([mdoc]); - sut.Match( + sut.FlattenCandidates().Match( _ => Assert.Fail(), () => { } ); } - - [Fact] - public void The_First_Matching_Claim_Set_Is_Disclosed() - { - // Arrange - var dcqlQuery = DcqlSamples.GetDcqlQueryWithClaimsets; - var credentialQuery = dcqlQuery.CredentialQueries[0]; - var expectedClaimIds = credentialQuery.ClaimSets![0].Claims.Select(id => id.AsString()).ToArray(); - var credential = SdJwtSamples.GetIdCardCredential(); - - // Act - var sut = dcqlQuery.FindMatchingCandidates([credential]); - - // Assert - sut.Match( - candidates => - { - var presentationCandidates = candidates.ToList(); - presentationCandidates.Should().HaveCount(1); - foreach (var candidate in presentationCandidates) - { - candidate.Identifier.Should().Be(credentialQuery.Id); - } - var claimsToDisclose = presentationCandidates[0].ClaimsToDisclose; - claimsToDisclose.Match( - claims => - { - var claimIds = claims.Select(c => c.Id!.AsString()).ToArray(); - claimIds.Should().BeEquivalentTo(expectedClaimIds); - }, - () => Assert.Fail("Expected claims to be returned, but got none.") - ); - }, - () => Assert.Fail("Expected candidates to be returned, but got none.") - ); - } - - [Fact] - public void No_Claims_Are_Disclosed_When_Claims_in_Dcql_Query_Are_Absent() - { - // Arrange - var dcqlQuery = DcqlSamples.GetDcqlQueryWithNoClaims; - var credential = SdJwtSamples.GetIdCardCredential(); - - // Act - var sut = dcqlQuery.FindMatchingCandidates([credential]); - - // Assert - sut.Match( - candidates => - { - var presentationCandidates = candidates.ToList(); - presentationCandidates.Should().HaveCount(1); - foreach (var candidate in presentationCandidates) - { - candidate.Identifier.Should().Be(dcqlQuery.CredentialQueries[0].Id); - } - var claimsToDisclose = presentationCandidates[0].ClaimsToDisclose; - claimsToDisclose.IsNone.Should().BeTrue(); - }, - () => Assert.Fail("Expected candidates to be returned, but got none.") - ); - } -} +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlParsingTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlParsingTests.cs new file mode 100644 index 00000000..9be2867d --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlParsingTests.cs @@ -0,0 +1,62 @@ +using FluentAssertions; +using Newtonsoft.Json; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models; +using WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.Samples; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql; + +public class DcqlParsingTests +{ + [Fact] + public void Can_Parse_Dcql_Query() + { + var json = DcqlSamples.GetDcqlQueryAsJsonStr(); + var dcqlQuery = JsonConvert.DeserializeObject(json)!; + + dcqlQuery.CredentialQueries.Length.Should().Be(5); + + dcqlQuery.CredentialQueries[0].Id.AsString().Should().Be("pid"); + dcqlQuery.CredentialQueries[0].Format.Should().Be("dc+sd-jwt"); + dcqlQuery.CredentialQueries[0].Meta!.Vcts! + .First() + .Should() + .Be("https://credentials.example.com/identity_credential"); + + dcqlQuery.CredentialQueries[0].Claims![0].Path.GetPathComponents().Length().Should().Be(1); + dcqlQuery.CredentialQueries[0].Claims![1].Path.GetPathComponents().Length().Should().Be(1); + dcqlQuery.CredentialQueries[0].Claims![2].Path.GetPathComponents().Length().Should().Be(2); + + dcqlQuery.CredentialSetQueries!.Length.Should().Be(2); + dcqlQuery.CredentialSetQueries[0].Purpose.Should().Contain(x => x.Name == "Identification"); + dcqlQuery.CredentialSetQueries[0].Options[0].Ids[0].AsString().Should().Be("pid"); + dcqlQuery.CredentialSetQueries[0].Options[1].Ids[0].AsString().Should().Be("other_pid"); + dcqlQuery.CredentialSetQueries[0].Options[2].Ids[0].AsString().Should().Be("pid_reduced_cred_1"); + dcqlQuery.CredentialSetQueries[0].Options[2].Ids[1].AsString().Should().Be("pid_reduced_cred_2"); + } + + [Fact] + public void Can_Parse_Query_With_Claim_Sets() + { + // Arrange + const string query = DcqlSamples.QueryStrWithClaimSets; + + // Act + var sut = JsonConvert.DeserializeObject(query)!; + + // Assert + sut.CredentialQueries.Length.Should().Be(1); + 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() + .Be("ID-Card"); + cred.Claims!.Length.Should().Be(4); + cred.Claims[0].Id!.AsString().Should().Be("a"); + cred.Claims[1].Id!.AsString().Should().Be("b"); + cred.Claims[2].Id!.AsString().Should().Be("c"); + cred.Claims[3].Id!.AsString().Should().Be("d"); + cred.ClaimSets!.Count.Should().Be(2); + cred.ClaimSets![0].Claims.Select(c => c.AsString()).Should().BeEquivalentTo("a", "b", "d"); + cred.ClaimSets![1].Claims.Select(c => c.AsString()).Should().BeEquivalentTo("a", "c"); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlResultTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlResultTests.cs new file mode 100644 index 00000000..64caa536 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/DcqlResultTests.cs @@ -0,0 +1,33 @@ +using FluentAssertions; +using WalletFramework.Core.Credentials.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql; +using WalletFramework.Oid4Vc.Oid4Vp.Models; +using WalletFramework.Oid4Vc.Oid4Vp.Query; +using WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.Samples; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql; + +public class DcqlResultTests +{ + [Fact] + public void Result_Can_Show_Missing_Credentials() + { + // Arrange + var query = DcqlSamples.GetNoMatchErrorClaimPathQuery(); + var credentials = Array.Empty(); + + // Act + var result = query.ProcessWith(credentials); + + // Assert + result.FlattenCandidates().IsNone.Should().BeTrue(); + result.MissingCredentials.Match( + missing => + { + missing.Should().HaveCount(1); + missing[0].GetIdentifier().Should().Be(query.CredentialQueries[0].Id.AsString()); + }, + () => Assert.Fail("Expected missing credentials, but got none.") + ); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/Samples/DcqlSamples.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/Samples/DcqlSamples.cs index 944cf054..f97c72c4 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/Samples/DcqlSamples.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/Samples/DcqlSamples.cs @@ -8,12 +8,151 @@ public static class DcqlSamples { public const string ClaimSetSampleJson = "[\"a\", \"b\", \"c\"]"; + public const string DcqlQueryWithOneCredentialSetJson = @"{ + ""credentials"": [ + { + ""id"": ""idcard"", + ""format"": ""dc+sd-jwt"", + ""meta"": { + ""vct_values"": [""ID-Card""] + }, + ""claims"": [ + {""path"": [""first_name""]}, + {""path"": [""last_name""]}, + {""path"": [""address"", ""street_address""]} + ] + }, + { + ""id"": ""idcard2"", + ""format"": ""dc+sd-jwt"", + ""meta"": { + ""vct_values"": [""ID-Card-2""] + }, + ""claims"": [ + {""path"": [""first_name""]}, + {""path"": [""last_name""]}, + {""path"": [""address"", ""street_address""]} + ] + }, + { + ""id"": ""idcard3"", + ""format"": ""dc+sd-jwt"", + ""meta"": { + ""vct_values"": [""ID-Card-3""] + }, + ""claims"": [ + {""path"": [""rewards_number""]} + ] + } + ], + ""credential_sets"": [ + { + ""options"": [ + [ ""idcard"", ""idcard2"" ] + ] + }, + { + ""required"": false, + ""options"": [ + [ ""idcard3"" ] + ] + } + ] + }"; + + public const string DcqlQueryWithOneCredentialSetAndMultipleOptionsJson = @"{ + ""credentials"": [ + { + ""id"": ""idcard"", + ""format"": ""dc+sd-jwt"", + ""meta"": { + ""vct_values"": [""ID-Card""] + }, + ""claims"": [ + {""path"": [""first_name""]}, + {""path"": [""last_name""]}, + {""path"": [""address"", ""street_address""]} + ] + }, + { + ""id"": ""idcard2"", + ""format"": ""dc+sd-jwt"", + ""meta"": { + ""vct_values"": [""ID-Card-2""] + }, + ""claims"": [ + {""path"": [""first_name""]}, + {""path"": [""last_name""]}, + {""path"": [""address"", ""street_address""]} + ] + }, + { + ""id"": ""idcard3"", + ""format"": ""dc+sd-jwt"", + ""meta"": { + ""vct_values"": [""ID-Card-3""] + }, + ""claims"": [ + {""path"": [""rewards_number""]} + ] + } + ], + ""credential_sets"": [ + { + ""options"": [ + [ ""idcard"", ""idcard2""], + [ ""idcard2""] + ] + } + ] +}"; + + public const string IdCardAndIdCard2NationalitiesSecondIndexQueryJson = @"{ + ""credentials"": [ + { + ""id"": ""idcard"", + ""format"": ""dc+sd-jwt"", + ""meta"": { + ""vct_values"": [""ID-Card""] + }, + ""claims"": [ + { ""path"": [""nationalities"", 1] } + ] + }, + { + ""id"": ""idcard2"", + ""format"": ""dc+sd-jwt"", + ""meta"": { + ""vct_values"": [""ID-Card-2""] + }, + ""claims"": [ + { ""path"": [""nationalities"", 1] } + ] + } + ] + }"; + + public const string IdCardNationalitiesSecondIndexQueryJson = @"{ + ""credentials"": [ + { + ""id"": ""idcard"", + ""format"": ""dc+sd-jwt"", + ""meta"": { + ""vct_values"": [""ID-Card""] + }, + ""claims"": [ + { ""path"": [""nationalities"", 1] } + ] + } + ] + }"; + public const string MultipleClaimSetsSampleJson = "[[\"a\", \"b\"], [\"c\", \"d\"]]"; public const string QueryStrWithClaimSets = @"{ ""credentials"": [ { - ""id"": ""pid"", + ""id"": ""idcard"", ""format"": ""dc+sd-jwt"", ""meta"": { ""vct_values"": [ ""ID-Card"" ] @@ -31,46 +170,53 @@ public static class DcqlSamples } ] }"; - - public static DcqlQuery GetDcqlQueryWithClaimsets => + + public const string QueryStrWithNoClaims = @"{ + ""credentials"": [ + { + ""id"": ""pid"", + ""format"": ""dc+sd-jwt"", + ""meta"": { + ""vct_values"": [ ""ID-Card"" ] + } + } + ] + }"; + + public static DcqlQuery GetDcqlQueryWithClaimsets => JsonConvert.DeserializeObject(QueryStrWithClaimSets)!; + public static DcqlQuery GetDcqlQueryWithCredentialSets => + JsonConvert.DeserializeObject(DcqlQueryWithOneCredentialSetJson)!; + + public static DcqlQuery GetDcqlQueryWithOneCredentialSet => + JsonConvert.DeserializeObject(DcqlQueryWithOneCredentialSetJson)!; + + public static DcqlQuery GetDcqlQueryWithNoClaims => + JsonConvert.DeserializeObject(QueryStrWithNoClaims)!; + public static string GetDcqlQueryAsJsonStr() => GetJsonForTestCase("DcqlQuerySample"); - public static DcqlQuery GetIdCardAndIdCard2NationalitiesSecondIndexQuery() + public static DcqlQuery GetIdCardAndIdCard2NationalitiesSecondIndexQuery() => + JsonConvert.DeserializeObject(IdCardAndIdCard2NationalitiesSecondIndexQueryJson)!; + + public static DcqlQuery GetIdCardNationalitiesSecondIndexQuery() => + JsonConvert.DeserializeObject(IdCardNationalitiesSecondIndexQueryJson)!; + + public static DcqlQuery GetMdocAndSdJwtFamilyNameQuery() { var json = @"{ ""credentials"": [ { - ""id"": ""idcard1"", - ""format"": ""dc+sd-jwt"", + ""id"": ""mdoc"", + ""format"": ""mso_mdoc"", ""meta"": { - ""vct_values"": [""ID-Card""] + ""doctype_value"": ""org.iso.18013.5.1.mDL"" }, ""claims"": [ - { ""path"": [""nationalities"", 1] } + { ""path"": [""org.iso.18013.5.1"", ""family_name""] } ] }, - { - ""id"": ""idcard2"", - ""format"": ""dc+sd-jwt"", - ""meta"": { - ""vct_values"": [""ID-Card-2""] - }, - ""claims"": [ - { ""path"": [""nationalities"", 1] } - ] - } - ] - }"; - - return JsonConvert.DeserializeObject(json)!; - } - - public static DcqlQuery GetIdCardNationalitiesSecondIndexQuery() - { - var json = @"{ - ""credentials"": [ { ""id"": ""idcard"", ""format"": ""dc+sd-jwt"", @@ -78,12 +224,11 @@ public static DcqlQuery GetIdCardNationalitiesSecondIndexQuery() ""vct_values"": [""ID-Card""] }, ""claims"": [ - { ""path"": [""nationalities"", 1] } + { ""path"": [""last_name""] } ] } ] }"; - return JsonConvert.DeserializeObject(json)!; } @@ -148,49 +293,8 @@ public static DcqlQuery GetNoMatchErrorClaimPathQuery() return JsonConvert.DeserializeObject(json)!; } - public static DcqlQuery GetMdocAndSdJwtFamilyNameQuery() - { - var json = @"{ - ""credentials"": [ - { - ""id"": ""mdoc"", - ""format"": ""mso_mdoc"", - ""meta"": { - ""doctype_value"": ""org.iso.18013.5.1.mDL"" - }, - ""claims"": [ - { ""path"": [""org.iso.18013.5.1"", ""family_name""] } - ] - }, - { - ""id"": ""idcard"", - ""format"": ""dc+sd-jwt"", - ""meta"": { - ""vct_values"": [""ID-Card""] - }, - ""claims"": [ - { ""path"": [""last_name""] } - ] - } - ] - }"; - return JsonConvert.DeserializeObject(json)!; - } - - public const string QueryStrWithNoClaims = @"{ - ""credentials"": [ - { - ""id"": ""pid"", - ""format"": ""dc+sd-jwt"", - ""meta"": { - ""vct_values"": [ ""ID-Card"" ] - } - } - ] - }"; - - public static DcqlQuery GetDcqlQueryWithNoClaims => - JsonConvert.DeserializeObject(QueryStrWithNoClaims)!; + public static DcqlQuery GetDcqlQueryWithOneCredentialSetAndMultipleOptions => + JsonConvert.DeserializeObject(DcqlQueryWithOneCredentialSetAndMultipleOptionsJson)!; private static string GetJsonForTestCase(string name = "") { diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/Samples/TransactionDataSamples.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/Samples/TransactionDataSamples.cs new file mode 100644 index 00000000..27eb741c --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Dcql/Samples/TransactionDataSamples.cs @@ -0,0 +1,75 @@ +using WalletFramework.Oid4Vc.Oid4Vp.Models; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.Samples; + +public static class TransactionDataSamples +{ + public const string PaymentTransactionDataForPid = + "eyJwYXltZW50X2RhdGEiOnsicGF5ZWUiOiJBQkMgQmFuayIsImN1cnJlbmN5X2Ftb3VudCI6eyJjdXJyZW5jeSI6IkVVUiIsInZhbHVlIjoiODAwIn19LCJ0cmFuc2FjdGlvbl9kYXRhX2hhc2hlc19hbGciOlsic2hhLTI1NiJdLCJjcmVkZW50aWFsX2lkcyI6WyJwaWQiXSwidHlwZSI6InBheW1lbnRfZGF0YSJ9"; + + private const string AuthRequestWithTransactionDataTemplate = @"{ + ""response_uri"": ""https://test.test.test.io/openid4vp/authorization-response"", + ""transaction_data"": [ + ""eyJwYXltZW50X2RhdGEiOnsicGF5ZWUiOiJBQkMgQmFuayIsImN1cnJlbmN5X2Ftb3VudCI6eyJjdXJyZW5jeSI6IkVVUiIsInZhbHVlIjoiODAwIn19LCJ0cmFuc2FjdGlvbl9kYXRhX2hhc2hlc19hbGciOlsic2hhLTI1NiJdLCJjcmVkZW50aWFsX2lkcyI6WyJpZGNhcmQiXSwidHlwZSI6InBheW1lbnRfZGF0YSJ9"" + ], + ""client_id_scheme"": ""x509_san_dns"", + ""iss"": ""https://test.test.test.io"", + ""response_type"": ""vp_token"", + ""nonce"": ""bRlAPdfKK2rSyn8RKoYDkr"", + ""client_id"": ""test.test.test.io"", + ""response_mode"": ""direct_post"", + ""aud"": ""https://self-issued.me/v2"", + ""dcql_query"": {0}, + ""state"": ""73ec8b46-2289-4a31-856c-06ef56cdf165"", + ""exp"": 1747316703, + ""iat"": 1747313103, + ""client_metadata"": { + ""client_name"": ""default-test-updated"", + ""logo_uri"": ""https://www.defaultTestLogo.com/updated-logo.png"", + ""redirect_uris"": [""https://test.id""], + ""tos_uri"": ""https://www.example.com/tos"", + ""policy_uri"": ""https://www.example.com/policy"", + ""client_uri"": ""https://www.example.com"", + ""contacts"": [""admin@admin.it""], + ""vp_formats"": { + ""vc+sd-jwt"": { + ""sd-jwt_alg_values"": [""ES256""], + ""kb-jwt_alg_values"": [""ES256""] + }, + ""dc+sd-jwt"": { + ""sd-jwt_alg_values"": [""ES256""], + ""kb-jwt_alg_values"": [""ES256""] + } + } + } +}"; + + public static AuthorizationRequest GetAuthRequestWithSingleCredentialTransactionData() + { + var authRequestJson = GetAuthRequestWithSingleCredentialTransactionDataStr(); + return AuthorizationRequest.CreateAuthorizationRequest(authRequestJson).UnwrapOrThrow(); + } + + public static string GetAuthRequestWithSingleCredentialTransactionDataStr() + { + return AuthRequestWithTransactionDataTemplate.Replace("{0}", DcqlSamples.IdCardNationalitiesSecondIndexQueryJson); + } + + public static AuthorizationRequest GetAuthRequestWithTwoCredentialsTransactionData() + { + var authRequestJson = GetAuthRequestWithTwoCredentialsTransactionDataStr(); + return AuthorizationRequest.CreateAuthorizationRequest(authRequestJson).UnwrapOrThrow(); + } + + public static string GetAuthRequestWithTwoCredentialsTransactionDataStr() + { + return AuthRequestWithTransactionDataTemplate.Replace("{0}", DcqlSamples.IdCardAndIdCard2NationalitiesSecondIndexQueryJson); + } + + public static AuthorizationRequest GetAuthRequestWithMultipleCandidatesInOneSetTransactionData() + { + var authRequestJson = AuthRequestWithTransactionDataTemplate.Replace("{0}", DcqlSamples.DcqlQueryWithOneCredentialSetJson); + return AuthorizationRequest.CreateAuthorizationRequest(authRequestJson).UnwrapOrThrow(); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/TransactionDatas/TransactionDataTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/TransactionDatas/TransactionDataTests.cs new file mode 100644 index 00000000..c4e9c676 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/TransactionDatas/TransactionDataTests.cs @@ -0,0 +1,182 @@ +using FluentAssertions; +using WalletFramework.Core.Credentials.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vp.Dcql; +using WalletFramework.Oid4Vc.Oid4Vp.Models; +using WalletFramework.Oid4Vc.Oid4Vp.TransactionDatas; +using WalletFramework.Oid4Vc.Tests.Oid4Vp.Dcql.Samples; +using WalletFramework.Oid4Vc.Tests.Samples; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.TransactionDatas; + +public class TransactionDataTests +{ + [Fact] + public void Transaction_Data_Are_Matched_To_Candidate_Correctly() + { + // Arrange + var authRequest = TransactionDataSamples.GetAuthRequestWithSingleCredentialTransactionData(); + var sdJwtRecord = SdJwtSamples.GetIdCardCredential(); + + var credentials = new List { sdJwtRecord }; + var candidateQueryResult = authRequest.Requirements.Match( + dcql => dcql.ProcessWith(credentials), + _ => throw new NotSupportedException("Only DCQL flow supported in this test") + ); + + var presentationRequest = new PresentationRequest(authRequest, candidateQueryResult); + var transactionDatas = authRequest.TransactionData.IfNone([]); + + // Act + var result = TransactionDataFun.ProcessVpTransactionData(presentationRequest, transactionDatas); + + // Assert + result.Match( + updatedRequest => + { + updatedRequest.CandidateQueryResult.Candidates.Match( + sets => + { + sets.Should().HaveCount(1); + sets[0].Candidates[0].TransactionData.Match( + data => data.Should().Contain(transactionDatas[0]), + () => Assert.Fail("Expected transaction data to be present") + ); + }, + () => Assert.Fail("Expected candidate sets to be present") + ); + }, + error => Assert.Fail($"Expected success but got error: {error}") + ); + } + + [Fact] + public void Transaction_Data_Are_Matched_To_Multiple_Credentials_Sets_Correctly() + { + // Arrange + var authRequest = TransactionDataSamples.GetAuthRequestWithTwoCredentialsTransactionData(); + + var idCardCredential = SdJwtSamples.GetIdCardCredential(); + var idCard2Credential = SdJwtSamples.GetIdCard2Credential(); + + var credentials = new List { idCardCredential, idCard2Credential }; + var candidateQueryResult = authRequest.Requirements.Match( + dcql => dcql.ProcessWith(credentials), + _ => throw new NotSupportedException("Only DCQL flow supported in this test") + ); + + var presentationRequest = new PresentationRequest(authRequest, candidateQueryResult); + var transactionDatas = authRequest.TransactionData.IfNone([]); + + // Act + var result = TransactionDataFun.ProcessVpTransactionData(presentationRequest, transactionDatas); + + // Assert + result.Match( + updatedRequest => + { + updatedRequest.CandidateQueryResult.Candidates.Match( + sets => + { + var idCardCandidate = sets[0].Candidates.FirstOrDefault(c => c.Identifier == "idcard"); + var idCard2Candidate = sets[1].Candidates.FirstOrDefault(c => c.Identifier == "idcard2"); + idCardCandidate!.TransactionData.Match( + data => data.Should().Contain(transactionDatas[0]), + () => Assert.Fail("Expected transaction data to be present for idcard candidate") + ); + idCard2Candidate!.TransactionData.Match( + _ => Assert.Fail("Expected no transaction data for idcard2 candidate"), + () => { } + ); + }, + () => Assert.Fail("Expected candidate sets to be present") + ); + }, + error => Assert.Fail($"Expected success but got error: {error}") + ); + } + + [Fact] + public void Transaction_Data_Are_Matched_To_Multiple_Candidates_In_One_Set_Correctly() + { + // Arrange + var authRequest = TransactionDataSamples.GetAuthRequestWithMultipleCandidatesInOneSetTransactionData(); + var idCardCredential1 = SdJwtSamples.GetIdCardCredential(); + var idCardCredential2 = SdJwtSamples.GetIdCard2Credential(); + var credentials = new List { idCardCredential1, idCardCredential2 }; + var candidateQueryResult = authRequest.Requirements.Match( + dcql => dcql.ProcessWith(credentials), + _ => throw new NotSupportedException("Only DCQL flow supported in this test") + ); + + var presentationRequest = new PresentationRequest(authRequest, candidateQueryResult); + var transactionDatas = authRequest.TransactionData.IfNone([]); + + // Act + var result = TransactionDataFun.ProcessVpTransactionData(presentationRequest, transactionDatas); + + // Assert + result.Match( + updatedRequest => + { + updatedRequest.CandidateQueryResult.Candidates.Match( + sets => + { + sets.Should().HaveCount(1); + var candidateSet = sets[0]; + candidateSet.Candidates.Should().HaveCount(2); + candidateSet.Candidates[0].TransactionData.Match( + data => data.Should().Contain(transactionDatas[0]), + () => Assert.Fail("Expected transaction data to be present for candidate") + ); + candidateSet.Candidates[1].TransactionData.IsNone.Should().BeTrue(); + }, + () => Assert.Fail("Expected candidate sets to be present") + ); + }, + error => Assert.Fail($"Expected success but got error: {error}") + ); + } + + [Fact] + public void Only_One_Candidate_Fulfills_Transaction_Data_When_Multiple_Are_Possible() + { + // Arrange + var authRequest = TransactionDataSamples.GetAuthRequestWithMultipleCandidatesInOneSetTransactionData(); + var idCardCredential1 = SdJwtSamples.GetIdCardCredential(); + var idCardCredential1Clone = SdJwtSamples.GetIdCardCredential(); + var idCardCredential2 = SdJwtSamples.GetIdCard2Credential(); + var credentials = new List { idCardCredential1, idCardCredential1Clone, idCardCredential2 }; + var candidateQueryResult = authRequest.Requirements.Match( + dcql => dcql.ProcessWith(credentials), + _ => throw new NotSupportedException("Only DCQL flow supported in this test") + ); + + var presentationRequest = new PresentationRequest(authRequest, candidateQueryResult); + var transactionDatas = authRequest.TransactionData.IfNone([]); + + // Act + var result = TransactionDataFun.ProcessVpTransactionData(presentationRequest, transactionDatas); + + // Assert + result.Match( + updatedRequest => + { + updatedRequest.CandidateQueryResult.Candidates.Match( + sets => + { + sets.Should().HaveCount(1); + var candidateSet = sets[0]; + candidateSet.Candidates.Should().HaveCount(2); + candidateSet.Candidates[0].TransactionData.Match( + data => data.Should().Contain(transactionDatas[0]), + () => Assert.Fail("Expected transaction data to be present for candidate") + ); + candidateSet.Candidates[1].TransactionData.IsNone.Should().BeTrue(); + }, + () => Assert.Fail("Expected candidate sets to be present") + ); + }, + error => Assert.Fail($"Expected success but got error: {error}") + ); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Samples/SdJwtSamples.cs b/test/WalletFramework.Oid4Vc.Tests/Samples/SdJwtSamples.cs index 2d78260d..21f70cce 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Samples/SdJwtSamples.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Samples/SdJwtSamples.cs @@ -38,4 +38,20 @@ public static SdJwtRecord GetIdCard2Credential() return record; } + + public static SdJwtRecord GetIdCard3Credential() + { + const string encodedSdJwt = "eyJraWQiOiJiZmFmYjkzMy1iNzQ4LTQ3ODYtODc1Ny0zYzg0ZWFlNmUzZGUiLCJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiUVBEUFFCbEEzdk9QaU9qR0lRRXBOc1l5S2Zjd2M1T3dDUlV5eWY2QTlRbyIsIk9LMWJpZXUwR0RIZWVRc2lzRkxOcUdmX0Z4eW5HT0dTNHl5Q2dZeFVhTkEiLCJUSkJ4ajBGSmdTQlUxMzVDSDRacFJieTRfVG4tNWR4TFJBX0paRnNscXhjIiwiaFBjV0phVkRJdDlDZ1E3bWxzNmFSVFR6bHZ0NmlMYzlUWFRJZ2VuZDFWayIsIkNhZm9TdzRiMWdsV196ckdyN3lodFFyQ3RIYW51NG15MVBxTGtXQkx5aFkiXSwibmJmIjoxNzA2NTQyNjgxLCJ2Y3QiOiJJRC1DYXJkLTMiLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vZTgwYy0yMTctMTExLTEwOC0xNzQubmdyb2stZnJlZS5hcHAvIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Img2VUtiVXQ1SW4yTzVwUzUxYXRWaERuTDl0SGR4S3lkMTZXTG94R2dFQzQiLCJ5IjoiMDdIX05RcmlxRmxSb0JjVk5ZVW5aS2wwQ1A0U0NiN3RxU0NWWFNDTWh0ayJ9fSwiZXhwIjoxNzM4MjUxNDgxLCJpYXQiOjE3MTY5OTA0MDAsInN0YXR1cyI6eyJpZHgiOjYsInVyaSI6Imh0dHBzOi8vZTgwYy0yMTctMTExLTEwOC0xNzQubmdyb2stZnJlZS5hcHAvc3RhdHVzLWxpc3RzP3JlZ2lzdHJ5SWQ9YmQ1MDllMzYtNTQzNy00Zjg4LTkzYTUtNDEzNDA3ZjZiZDhmIn19.-3GEPOjEn4bopEGyy8ho_kFSfQVmkkZiFKMebtiZE6EsyRnunJtA46M_SwHQjmSm-73zIeRX7L7Rpszm8dkFhQ~WyJfSU1WWFVtc052bm9YTDR3NVRPSFpnIiwiYWRkcmVzcyIseyJzdHJlZXRfYWRkcmVzcyI6IjQyIE1hcmtldCBTdHJlZXQiLCJwb3N0YWxfY29kZSI6IjEyMzQ1In1d~WyJ3RzkzbExRRFBDUVgxTUtCYW5mVkVRIiwibGFzdF9uYW1lIiwiRG9lIl0~WyJFa1h0a0JHZXd2dkthRXlzTWhyVGJnIiwibmF0aW9uYWxpdGllcyIsWyJCcml0aXNoIiwiQmV0ZWxnZXVzaWFuIl1d~WyJzQlh2dVQxRHhaN0NrMTdJUXQzWWd3IiwiZmlyc3RfbmFtZSIsIkpvaG4iXQ~WyJoRWphWTA2WmFsNUZTS0pXSm9kUjZnIiwiZGVncmVlcyIsW3sidW5pdmVyc2l0eSI6IlVuaXZlcnNpdHkgb2YgQmV0ZWxnZXVzZSIsInR5cGUiOiJCYWNoZWxvciBvZiBTY2llbmNlIn0seyJ1bml2ZXJzaXR5IjoiVW5pdmVyc2l0eSBvZiBCZXRlbGdldXNlIiwidHlwZSI6Ik1hc3RlciBvZiBTY2llbmNlIn1dXQ~"; + var keyId = KeyId.CreateKeyId(); + + var record = new SdJwtRecord( + encodedSdJwt, + new Dictionary(), + [], + keyId, + CredentialSetId.CreateCredentialSetId() + ); + + return record; + } }