Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

init vp_format support #252

Merged
merged 7 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading