Skip to content

Commit

Permalink
init vp_format support (#252)
Browse files Browse the repository at this point in the history
* vp_formats support

Signed-off-by: Johannes Tuerk <[email protected]>

---------

Signed-off-by: Johannes Tuerk <[email protected]>
Signed-off-by: kenkosmowski <[email protected]>
Co-authored-by: kenkosmowski <[email protected]>
  • Loading branch information
JoTiTu and kenkosmowski authored Jan 17, 2025
1 parent 2b5bc2f commit 474107c
Show file tree
Hide file tree
Showing 14 changed files with 149 additions and 96 deletions.
13 changes: 4 additions & 9 deletions .github/workflows/publish-nuget.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ env:
jobs:
build:

runs-on: ubuntu-latest
runs-on: ubuntu-22.04

steps:
- uses: actions/checkout@v2
Expand All @@ -37,14 +37,6 @@ jobs:
fi
echo "APP_VERSION=$VERSION$SUFFIX" >> $GITHUB_ENV
- name: Setup NuGet
uses: NuGet/setup-nuget@v2
with:
nuget-version: 6.10.2

- name: Restore dependencies
run: nuget restore $SOLUTION

- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
Expand All @@ -57,6 +49,9 @@ jobs:
# sudo apt-get update \
# apt-get install -y libindy

- name: Restore dependencies
run: dotnet restore $SOLUTION

- name: Build
run: dotnet build $SOLUTION --configuration $BUILD_CONFIG -p:Version=$APP_VERSION

Expand Down
10 changes: 9 additions & 1 deletion src/WalletFramework.Oid4Vc/Oid4Vp/Models/ClientMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public record ClientMetadata
/// </summary>
[JsonProperty("tos_uri")]
public string? TosUri { get; }

/// <summary>
/// The URI to a human-readable terms of service document for the client (verifier).
/// </summary>
[JsonProperty("vp_formats")]
public Formats Formats { get; }

public ClientMetadata(
string? clientName,
Expand All @@ -63,7 +69,8 @@ public ClientMetadata(
string? policyUri,
string? tosUri,
string[] redirectUris,
List<JsonWebKey> jwks)
List<JsonWebKey> jwks,
Formats formats)
{
ClientName = clientName;
ClientUri = clientUri;
Expand All @@ -73,5 +80,6 @@ public ClientMetadata(
TosUri = tosUri;
RedirectUris = redirectUris;
Jwks = jwks;
Formats = formats;
}
}
12 changes: 12 additions & 0 deletions src/WalletFramework.Oid4Vc/Oid4Vp/Models/Formats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Newtonsoft.Json;

namespace WalletFramework.Oid4Vc.Oid4Vp.Models;

public record Formats
{
[JsonProperty("vc+sd-jwt")]
public SdJwtFormat? SdJwtFormat { get; init; }

[JsonProperty("mso_mdoc")]
public MDocFormat? MDocFormat { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using Newtonsoft.Json;

namespace WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models;
namespace WalletFramework.Oid4Vc.Oid4Vp.Models;

/// <summary>
/// Represents the claim format, encapsulating supported algorithms.
/// </summary>
public class Format
public class MDocFormat
{
/// <summary>
/// Gets the names of supported algorithms.
Expand All @@ -18,4 +18,4 @@ public class Format
/// </summary>
[JsonProperty("proof_type")]
public string[] ProofTypes { get; private set; } = null!;
}
}
12 changes: 12 additions & 0 deletions src/WalletFramework.Oid4Vc/Oid4Vp/Models/SdJwtFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Newtonsoft.Json;

namespace WalletFramework.Oid4Vc.Oid4Vp.Models;

public record SdJwtFormat
{
[JsonProperty("sd-jwt_alg_values")]
public List<string>? IssuerSignedJwtAlgValues { get; init; }

[JsonProperty("kb-jwt_alg_values")]
public List<string>? KeyBindingJwtAlgValues { get; init; }
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using WalletFramework.Oid4Vc.Oid4Vp.Models;

namespace WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models;

Expand All @@ -20,7 +21,7 @@ public class InputDescriptor
/// This property is optional.
/// </summary>
[JsonProperty("format")]
public Dictionary<string, Format> Formats { get; private set; } = null!;
public Formats? Formats { get; private set; }

/// <summary>
/// Gets or sets the unique identifier for the input descriptor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,6 @@ namespace WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models;
/// </summary>
public class PresentationDefinition
{
/// <summary>
/// Gets or sets the format of the presentation definition
/// This property is optional.
/// </summary>
[JsonProperty("format")]
public Dictionary<string, Format> Formats { get; }

/// <summary>
/// Represents a collection of input descriptors.
/// </summary>
Expand All @@ -24,7 +17,7 @@ public class PresentationDefinition
/// This MUST be a string. The string SHOULD provide a unique ID for the desired context.
/// </summary>
[JsonProperty("id", Required = Required.Always)]
public string Id { get; }
public string Id { get; }

/// <summary>
/// This SHOULD be a human-friendly string intended to constitute a distinctive designation of the Presentation
Expand All @@ -47,14 +40,12 @@ public class PresentationDefinition

[JsonConstructor]
private PresentationDefinition(
Dictionary<string, Format> formats,
InputDescriptor[] inputDescriptors,
string id,
string? name,
string? purpose,
SubmissionRequirement[] submissionRequirements)
{
Formats = formats;
InputDescriptors = inputDescriptors;
Id = id;
Name = name;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using LanguageExt;
using WalletFramework.Oid4Vc.Oid4Vp.Models;
using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models;

Expand All @@ -17,11 +18,12 @@ public interface IPexService
/// A task representing the asynchronous operation. The task result contains the Presentation Submission.
/// </returns>
Task<PresentationSubmission> CreatePresentationSubmission(PresentationDefinition presentationDefinition, DescriptorMap[] descriptorMaps);

/// <summary>
/// Finds the credential candidates based on the provided credentials and input descriptors.
/// </summary>
/// <param name="inputDescriptors">An array of input descriptors to be satisfied.</param>
/// <param name="supportedFormatSigningAlgorithms">An array of input descriptors to be satisfied.</param>
/// <returns>An array of credential candidates, each containing a list of credentials that match the input descriptors.</returns>
Task<PresentationCandidates[]> FindCredentialCandidates(IEnumerable<InputDescriptor> inputDescriptors);
Task<PresentationCandidates[]> FindCredentialCandidates(IEnumerable<InputDescriptor> inputDescriptors, Option<Formats> supportedFormatSigningAlgorithms);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System.IdentityModel.Tokens.Jwt;
using Hyperledger.Aries.Agents;
using LanguageExt;
using Newtonsoft.Json.Linq;
using SD_JWT.Models;
using WalletFramework.Core.Credentials.Abstractions;
using WalletFramework.Core.Functional;
using WalletFramework.MdocLib.Issuer;
using WalletFramework.MdocLib.Security.Cose;
using WalletFramework.Oid4Vc.Oid4Vci.Abstractions;
using WalletFramework.Oid4Vc.Oid4Vp.Models;
using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models;
Expand Down Expand Up @@ -40,23 +43,19 @@ public Task<PresentationSubmission> CreatePresentationSubmission(
}

/// <inheritdoc />
public virtual async Task<PresentationCandidates[]> FindCredentialCandidates(IEnumerable<InputDescriptor> inputDescriptors)
public virtual async Task<PresentationCandidates[]> FindCredentialCandidates(
IEnumerable<InputDescriptor> inputDescriptors, Option<Formats> supportedFormatSigningAlgorithms)
{
var result = new List<PresentationCandidates>();

foreach (var inputDescriptor in inputDescriptors)
{
if (!(inputDescriptor.Formats.Keys.Contains("vc+sd-jwt") || inputDescriptor.Formats.Keys.Contains("mso_mdoc")))
{
throw new NotSupportedException("Only vc+sd-jwt or mso_mdoc format are supported");
}

if (inputDescriptor.Constraints.Fields == null || inputDescriptor.Constraints.Fields.Length == 0)
{
throw new InvalidOperationException("Fields cannot be null or empty");
}

var matchingCredentials = await GetMatchingCredentials(inputDescriptor);
var matchingCredentials = await GetMatchingCredentials(inputDescriptor, supportedFormatSigningAlgorithms);

if (matchingCredentials.Count == 0)
continue;
Expand All @@ -83,7 +82,7 @@ public virtual async Task<PresentationCandidates[]> FindCredentialCandidates(IEn
return result.ToArray();
}

private async Task<List<ICredential>> GetMatchingCredentials(InputDescriptor inputDescriptor)
private async Task<List<ICredential>> GetMatchingCredentials(InputDescriptor inputDescriptor, Option<Formats> supportedFormatSigningAlgorithms)
{
var context = await agentProvider.GetContextAsync();

Expand All @@ -93,7 +92,15 @@ private async Task<List<ICredential>> GetMatchingCredentials(InputDescriptor inp
var filteredSdJwtRecords = sdJwtRecords.Where(record =>
{
var doc = _toSdJwtDoc(record);
return inputDescriptor.Formats.ContainsKey("vc+sd-jwt") && inputDescriptor.Constraints.Fields!.All(field =>

var handler = new JwtSecurityTokenHandler();
var issuerSignedJwt = handler.ReadJwtToken(doc.IssuerSignedJwt);

return issuerSignedJwt.Header.TryGetValue("alg", out var alg)
&& supportedFormatSigningAlgorithms.Match(
formats => formats.SdJwtFormat?.IssuerSignedJwtAlgValues?.Contains(alg.ToString()) ?? true,
() => inputDescriptor.Formats?.SdJwtFormat?.IssuerSignedJwtAlgValues?.Contains(alg.ToString()) ?? true)
&& inputDescriptor.Constraints.Fields!.All(field =>
{
try
{
Expand All @@ -115,27 +122,34 @@ private async Task<List<ICredential>> GetMatchingCredentials(InputDescriptor inp
}).Cast<ICredential>().AsOption();

var filteredMdocRecords = mdocRecords.OnSome(records => records
.Where(record => inputDescriptor.Formats.ContainsKey("mso_mdoc") && inputDescriptor.Constraints.Fields!.All(field =>
.Where(record =>
{
try
return record.Mdoc.IssuerSigned.IssuerAuth.ProtectedHeaders.Value.TryGetValue(new CoseLabel(1), out var alg)
&& supportedFormatSigningAlgorithms.Match(
formats => formats.MDocFormat?.Alg.Contains(alg.ToString()) ?? true,
() => inputDescriptor.Formats?.MDocFormat?.Alg.Contains(alg.ToString()) ?? true)
&& inputDescriptor.Constraints.Fields!.All(field =>
{
var jObj = record.Mdoc.IssuerSigned.IssuerNameSpaces.ToJObject();

if (jObj.SelectToken(field.Path.First(), true) is not JValue value)
return false;

if (field.Filter?.Const != null)
try
{
return field.Filter?.Const == value.Value?.ToString();
}
var jObj = record.Mdoc.IssuerSigned.IssuerNameSpaces.ToJObject();

return true;
}
catch (Exception)
{
return false;
}
}))
if (jObj.SelectToken(field.Path.First(), true) is not JValue value)
return false;

if (field.Filter?.Const != null)
{
return field.Filter?.Const == value.Value?.ToString();
}

return true;
}
catch (Exception)
{
return false;
}
});
})
.Cast<ICredential>()
.AsOption());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models;
using WalletFramework.Oid4Vc.Oid4Vp.Models;
using Format = WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Format;

namespace WalletFramework.Oid4Vc.Oid4Vp.Services;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ public Oid4VpClientService(
);

var credentialCandidates = await _pexService.FindCredentialCandidates(
authorizationRequest.PresentationDefinition.InputDescriptors);

authorizationRequest.PresentationDefinition.InputDescriptors, authorizationRequest.ClientMetadata?.Formats);
return (authorizationRequest, credentialCandidates);
}

/// <inheritdoc />
public async Task<Option<PresentationCandidates>> FindCredentialCandidateForInputDescriptorAsync(InputDescriptor inputDescriptor)
{
var candidates = await _pexService.FindCredentialCandidates([inputDescriptor]);
var candidates = await _pexService.FindCredentialCandidates([inputDescriptor], Option<Formats>.None);
return candidates.Any() ? candidates.First() : Option<PresentationCandidates>.None;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class Oid4VpHaipClientTests
"openid4vp://?client_id=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Fcallback&request_uri=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Frequestobject%2F4ba20ad0cb08545830aa549ab4305c03";

private const string RequestUriResponse =
"eyJhbGciOiJFUzI1NiJ9.eyJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJjbGllbnRfaWRfc2NoZW1lIjoicmVkaXJlY3RfdXJpIiwiY2xpZW50X2lkIjoiaHR0cHM6Ly92ZXJpZmllci5jb20vcHJlc2VudGF0aW9uL2F1dGhvcml6YXRpb24tcmVzcG9uc2UiLCJjbGllbnRfbWV0YWRhdGFfdXJpIjoiaHR0cHM6Ly92ZXJpZmllci5jb20vbWV0YWRhdGEvMTIzNCIsInJlc3BvbnNlX3VyaSI6Imh0dHBzOi8vdmVyaWZpZXIuY29tL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwibm9uY2UiOiI4NzU1NDc4NDI2MDI4MDI4MDQ0MjA5MjE4NDE3MTI3NDEzMjQ1OCIsInByZXNlbnRhdGlvbl9kZWZpbml0aW9uIjp7ImlkIjoiNGRkMWMyNmEtMmY0Ni00M2FlLWE3MTEtNzA4ODhjOTNmYjRmIiwiaW5wdXRfZGVzY3JpcHRvcnMiOlt7ImlkIjoiTmV4dGNsb3VkQ3JlZGVudGlhbCIsImZvcm1hdCI6eyJ2YytzZC1qd3QiOnsicHJvb2ZfdHlwZSI6WyJKc29uV2ViU2lnbmF0dXJlMjAyMCJdfX0sImNvbnN0cmFpbnRzIjp7ImxpbWl0X2Rpc2Nsb3N1cmUiOiJyZXF1aXJlZCIsImZpZWxkcyI6W3sicGF0aCI6WyIkLnR5cGUiXSwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJjb25zdCI6IlZlcmlmaWVkRU1haWwifX0seyJwYXRoIjpbIiQuY3JlZGVudGlhbFN1YmplY3QuZW1haWwiXX1dfX1dfX0.";
"eyJhbGciOiJFUzI1NiJ9.eyJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJjbGllbnRfaWRfc2NoZW1lIjoicmVkaXJlY3RfdXJpIiwiY2xpZW50X2lkIjoiaHR0cHM6Ly92ZXJpZmllci5jb20vcHJlc2VudGF0aW9uL2F1dGhvcml6YXRpb24tcmVzcG9uc2UiLCJjbGllbnRfbWV0YWRhdGFfdXJpIjoiaHR0cHM6Ly92ZXJpZmllci5jb20vbWV0YWRhdGEvMTIzNCIsInJlc3BvbnNlX3VyaSI6Imh0dHBzOi8vdmVyaWZpZXIuY29tL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwibm9uY2UiOiI4NzU1NDc4NDI2MDI4MDI4MDQ0MjA5MjE4NDE3MTI3NDEzMjQ1OCIsInByZXNlbnRhdGlvbl9kZWZpbml0aW9uIjp7ImlkIjoiNGRkMWMyNmEtMmY0Ni00M2FlLWE3MTEtNzA4ODhjOTNmYjRmIiwiaW5wdXRfZGVzY3JpcHRvcnMiOlt7ImlkIjoiTmV4dGNsb3VkQ3JlZGVudGlhbCIsImZvcm1hdCI6eyJ2YytzZC1qd3QiOnsic2Qtand0X2FsZ192YWx1ZXMiOlsiRVMyNTYiXSwia2Itand0X2FsZ192YWx1ZXMiOlsiSnNvbldlYlNpZ25hdHVyZTIwMjAiXX19LCJjb25zdHJhaW50cyI6eyJsaW1pdF9kaXNjbG9zdXJlIjoicmVxdWlyZWQiLCJmaWVsZHMiOlt7InBhdGgiOlsiJC50eXBlIl0sImZpbHRlciI6eyJ0eXBlIjoic3RyaW5nIiwiY29uc3QiOiJWZXJpZmllZEVNYWlsIn19LHsicGF0aCI6WyIkLmNyZWRlbnRpYWxTdWJqZWN0LmVtYWlsIl19XX19XX19.";

private const string AuthRequestByValueWithPresentationDefinitionUri =
"openid4vp:///?client_id=https%3A%2F%2Fsome.de%2Fissuer%2Fdirect_post_vci&response_type=vp_token&response_mode=direct_post&response_uri=https%3A%2F%2Fsome.de%2Fissuer%2Fdirect_post_vci&presentation_definition_uri=https%3A%2F%2Fsome.de%2Fissuer%2Fpresentation-definition&client_id_scheme=redirect_uri&client_metadata_uri=https%3A%2F%2Fsome.de%2Fissuer%2Fclient-metadata&nonce=n0S6_WzA2Mj&state=af0ifjsldkj";
Expand All @@ -37,9 +37,14 @@ public class Oid4VpHaipClientTests
["id"] = "NextcloudCredential",
["format"] = new JObject()
{
["vc+sd-jwt"] = new JObject()
["vc+sd-jwt"] = new JObject
{
["proof_type"] = new JArray("JsonWebSignature2020")
["sd-jwt_alg_values"] = new JArray("ES256"),
["kb-jwt_alg_values"] = new JArray("JsonWebSignature2020")
},
["dc+jwt"] = new JObject
{
["alg"] = new JArray("ES256")
}
},
["constraints"] = new JObject()
Expand Down Expand Up @@ -77,6 +82,18 @@ public class Oid4VpHaipClientTests
["redirect_uris"] = new JArray("https://verifier.com/redirect-uri"),
["policy_uri"] = "https://some.de/policy",
["tos_uri"] = "https://some.de/tos",
["vp_formats"] = new JObject
{
["vc+sd-jwt"] = new JObject
{
["sd-jwt_alg_values"] = new JArray("ES256"),
["kb-jwt_alg_values"] = new JArray("JsonWebSignature2020")
},
["dc+jwt"] = new JObject
{
["alg"] = new JArray("ES256")
}
}
}
.ToString();

Expand Down Expand Up @@ -133,8 +150,8 @@ public async Task CanProcessAuthorizationRequestByReference()

inputDescriptor.Id.Should().Be("NextcloudCredential");

inputDescriptor.Formats.First().Key.Should().Be("vc+sd-jwt");
inputDescriptor.Formats.First().Value.ProofTypes.First().Should().Be("JsonWebSignature2020");
inputDescriptor.Formats.SdJwtFormat.IssuerSignedJwtAlgValues.First().Should().Be("ES256");
inputDescriptor.Formats.SdJwtFormat.KeyBindingJwtAlgValues.First().Should().Be("JsonWebSignature2020");

inputDescriptor.Constraints.LimitDisclosure.Should().Be("required");

Expand Down Expand Up @@ -185,8 +202,8 @@ public async Task CanProcessAuthorizationRequestByValueWithPresentationDefinitio

inputDescriptor.Id.Should().Be("NextcloudCredential");

inputDescriptor.Formats.First().Key.Should().Be("vc+sd-jwt");
inputDescriptor.Formats.First().Value.ProofTypes.First().Should().Be("JsonWebSignature2020");
inputDescriptor.Formats.SdJwtFormat.IssuerSignedJwtAlgValues.First().Should().Be("ES256");
inputDescriptor.Formats.SdJwtFormat.KeyBindingJwtAlgValues.First().Should().Be("JsonWebSignature2020");

inputDescriptor.Constraints.LimitDisclosure.Should().Be("required");

Expand Down
Loading

0 comments on commit 474107c

Please sign in to comment.