Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
3 changes: 3 additions & 0 deletions src/WalletFramework.Core/Functional/OptionFun.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ public static Option<IEnumerable<T>> AsOption<T>(this IEnumerable<T> enumerable)
// ReSharper disable once PossibleMultipleEnumeration
: Some(enumerable);

public static Option<T> AsOption<T>(this T? value) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    var list = enumerable.ToList();
    return list.IsEmpty()
        ? Option<IEnumerable<T>>.None
        : list;

value != null ? Option<T>.Some(value) : Option<T>.None;

public static T UnwrapOrThrow<T>(this Option<T> option, Exception e) =>
option.Match(
t => t,
Expand Down
2 changes: 2 additions & 0 deletions src/WalletFramework.Oid4Vc/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public static class Constants
public const string MdocFormat = "mso_mdoc";

public const string RegistrationCertificateFormat = "jwt";

public const string DefaultResponseEncryptionEncAlgorithm = "A256GCM";
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,30 @@
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using WalletFramework.Core.Base64Url;
using WalletFramework.Core.Functional;
using WalletFramework.Oid4Vc.Oid4Vp.Jwk;
using WalletFramework.Oid4Vc.Oid4Vp.Models;
using static WalletFramework.Oid4Vc.Constants;

namespace WalletFramework.Oid4Vc.Oid4Vp.AuthResponse.Encryption;

public record EncryptedAuthorizationResponse(string Jwe, Option<string> State)
public record EncryptedAuthorizationResponse(string Jwe)
{
public override string ToString() => Jwe;
}

public static class EncryptedAuthorizationResponseFun
{
private static readonly IReadOnlyDictionary<string, JweEncryption> SupportedEncAlgorithmsMap = new Dictionary<string, JweEncryption>
{
["A256GCM"] = JweEncryption.A256GCM,
["A128CBC-HS256"] = JweEncryption.A128CBC_HS256
};

public static EncryptedAuthorizationResponse Encrypt(
this AuthorizationResponse response,
JsonWebKey verifierPubKey,
string apv,
Option<string> authorizationEncryptedResponseEnc,
Option<string[]> encryptedResponseEncAlgorithms,
Option<Nonce> mdocNonce)
{
var apvBase64 = Base64UrlString.CreateBase64UrlString(apv.GetUTF8Bytes());
Expand All @@ -44,20 +50,20 @@ public static EncryptedAuthorizationResponse Encrypt(
var settings = new JwtSettings();
settings.RegisterJwe(JweEncryption.A256GCM, new AesGcmEncryption());

var selectedEncAlgorithm = encryptedResponseEncAlgorithms.Match(
encAlgs => encAlgs.FirstOrDefault(encAlg => SupportedEncAlgorithmsMap.ContainsKey(encAlg))
?? throw new NotSupportedException("Unsupported response encryption algorithms requested by verifier."),
() => DefaultResponseEncryptionEncAlgorithm);

var jwe = JWE.EncryptBytes(
response.ToJson().GetUTF8Bytes(),
[new JweRecipient(JweAlgorithm.ECDH_ES, verifierPubKey.ToEcdh())],
authorizationEncryptedResponseEnc.ToNullable() switch {
"A256GCM" => JweEncryption.A256GCM,
"A128CBC-HS256" => JweEncryption.A128CBC_HS256,
null => JweEncryption.A256GCM,
_ => throw new NotSupportedException("Unsupported response encryption algorithm requested by verifier.")
},
SupportedEncAlgorithmsMap[selectedEncAlgorithm],
mode: SerializationMode.Compact,
extraProtectedHeaders: headers,
settings: settings);

return new EncryptedAuthorizationResponse(jwe, response.State);
return new EncryptedAuthorizationResponse(jwe);
}

public static FormUrlEncodedContent ToFormUrl(this EncryptedAuthorizationResponse response)
Expand All @@ -67,8 +73,6 @@ public static FormUrlEncodedContent ToFormUrl(this EncryptedAuthorizationRespons
{ "response", response.ToString() }
};

response.State.IfSome(state => content["state"] = state);

return new FormUrlEncodedContent(content);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using LanguageExt;
using WalletFramework.Core.Functional;
using WalletFramework.Oid4Vc.Oid4Vp.AuthResponse.Encryption.Abstractions;
using WalletFramework.Oid4Vc.Oid4Vp.Models;

Expand All @@ -14,10 +15,15 @@ public async Task<EncryptedAuthorizationResponse> Encrypt(
{
var verifierPubKey = await verifierKeyService.GetPublicKey(request);

var supportedAlgorithms = (request.ClientMetadata?.EncryptedResponseEncValuesSupported).AsOption().Match(
encValues => encValues,
() => (request.ClientMetadata?.AuthorizationEncryptedResponseEnc).AsOption().OnSome(encValue => new[] { encValue })
);

return response.Encrypt(
verifierPubKey,
request.Nonce,
request.ClientMetadata?.AuthorizationEncryptedResponseEnc,
supportedAlgorithms,
mdocNonce);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace WalletFramework.Oid4Vc.Oid4Vp.Errors;

public record VpFormatsNotSupportedError : VpError
{
private const string Code = "vp_formats_not_supported";

public VpFormatsNotSupportedError(string message) : base(Code, message)
{
}
};
118 changes: 115 additions & 3 deletions src/WalletFramework.Oid4Vc/Oid4Vp/Models/ClientMetadata.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
using LanguageExt;
using Newtonsoft.Json;
using OneOf;
using WalletFramework.Core.Functional;
using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;
using WalletFramework.Oid4Vc.Oid4Vp.Errors;
using WalletFramework.Oid4Vc.Oid4Vp.Jwk;
using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models;

namespace WalletFramework.Oid4Vc.Oid4Vp.Models;

Expand All @@ -12,6 +17,7 @@ public record ClientMetadata
// Needed for Newtonsoft Json Serialization
public ClientMetadata(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add a unit test for parsing ClientMetadata

string? authorizationEncryptedResponseEnc,
string[]? encryptedResponseEncValuesSupported,
string[] redirectUris,
string? clientName,
string? clientUri,
Expand All @@ -21,9 +27,11 @@ public ClientMetadata(
string? jwksUri,
string? policyUri,
string? tosUri,
Formats formats)
Formats vpFormats,
Formats vpFormatsSupported)
{
AuthorizationEncryptedResponseEnc = authorizationEncryptedResponseEnc;
EncryptedResponseEncValuesSupported = encryptedResponseEncValuesSupported;
RedirectUris = redirectUris;
ClientName = clientName;
ClientUri = clientUri;
Expand All @@ -33,15 +41,24 @@ public ClientMetadata(
JwksUri = jwksUri;
PolicyUri = policyUri;
TosUri = tosUri;
Formats = formats;
VpFormats = vpFormats;
VpFormatsSupported = vpFormatsSupported;
}

/// <summary>
/// Defined the encoding that should be used when an encrypted Auth Response is requested by the verifier.
/// Replaced by encrypted_response_enc_values_supported but kept for now for backwards compatibility.
/// </summary>
[Obsolete("This property is obsolete.")]
[JsonProperty("authorization_encrypted_response_enc")]
public string? AuthorizationEncryptedResponseEnc { get; init; }

/// <summary>
/// Defined the encoding that should be used when an encrypted Auth Response is requested by the verifier.
/// </summary>
[JsonProperty("encrypted_response_enc_values_supported")]
public string[]? EncryptedResponseEncValuesSupported { get; init; }

/// <summary>
/// The redirect URIs of the client (verifier).
/// </summary>
Expand Down Expand Up @@ -94,7 +111,102 @@ public ClientMetadata(

/// <summary>
/// The URI to a human-readable terms of service document for the client (verifier).
/// This is deprecated and replaced by vp_formats_supported but kept for now for backwards compatibility.
/// </summary>
[Obsolete("This property is obsolete.")]
[JsonProperty("vp_formats")]
public Formats Formats { get; init; }
public Formats? VpFormats { get; init; }

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

public static class ClientMetadataExtensions
{
public static Validation<AuthorizationRequestCancellation, Option<ClientMetadata>> VpFormatsSupportedValidation(this ClientMetadata clientMetadata, OneOf<DcqlQuery, PresentationDefinition> requirements, Option<Uri> responseUri)
{
return requirements.Match(
dcql =>
{
var walletMetadata = WalletMetadata.CreateDefault();

var (sdJwtRequested, mdocRequested) =
(dcql.CredentialQueries.Any(query => query.Format == Constants.SdJwtDcFormat || query.Format == Constants.SdJwtVcFormat),
dcql.CredentialQueries.Any(query => query.Format == Constants.MdocFormat));

return (sdJwtRequested, mdocRequested) switch
{
(true, false) => IsSdJwtVpFormatSupported(clientMetadata, walletMetadata)
? clientMetadata.AsOption()
: (Validation<AuthorizationRequestCancellation, Option<ClientMetadata>>) GetVpFormatsNotSupportedCancellation(responseUri),
(false, true) => IsMdocVpFormatSupported(clientMetadata, walletMetadata)
? clientMetadata.AsOption()
: (Validation<AuthorizationRequestCancellation, Option<ClientMetadata>>) GetVpFormatsNotSupportedCancellation(responseUri),
(true, true) => IsSdJwtVpFormatSupported(clientMetadata, walletMetadata) && IsMdocVpFormatSupported(clientMetadata, walletMetadata)
? clientMetadata.AsOption()
: (Validation<AuthorizationRequestCancellation, Option<ClientMetadata>>) GetVpFormatsNotSupportedCancellation(responseUri),
_ => clientMetadata.AsOption()
};
},
_ => clientMetadata.AsOption());
}

private static bool IsMdocVpFormatSupported(ClientMetadata clientMetadata, WalletMetadata walletMetadata)
{
var rpSupportedVpFormats = clientMetadata.VpFormatsSupported ?? clientMetadata.VpFormats;
var walletMetadataSupportedVpFormats = walletMetadata.VpFormatsSupported;

if (rpSupportedVpFormats?.MDocFormat == null)
return true;

if (rpSupportedVpFormats.MDocFormat.IssuerAuthAlgValues != null &&
!rpSupportedVpFormats.MDocFormat.IssuerAuthAlgValues.Any(clientAlg => walletMetadataSupportedVpFormats.MDocFormat!.IssuerAuthAlgValues!.Contains(clientAlg)))
return false;

if (rpSupportedVpFormats.MDocFormat.DeviceAuthAlgValues != null &&
!rpSupportedVpFormats.MDocFormat.DeviceAuthAlgValues.Any(clientAlg => walletMetadataSupportedVpFormats.MDocFormat!.DeviceAuthAlgValues!.Contains(clientAlg)))
return false;

return true;
}

private static bool IsSdJwtVpFormatSupported(ClientMetadata clientMetadata, WalletMetadata walletMetadata)
{
var rpSupportedVpFormats = clientMetadata.VpFormatsSupported ?? clientMetadata.VpFormats;
var walletMetadataSupportedVpFormats = walletMetadata.VpFormatsSupported;

if (rpSupportedVpFormats?.SdJwtDcFormat != null)
{
if (rpSupportedVpFormats.SdJwtDcFormat.IssuerSignedJwtAlgValues != null &&
!rpSupportedVpFormats.SdJwtDcFormat.IssuerSignedJwtAlgValues.Any(clientAlg => walletMetadataSupportedVpFormats.SdJwtDcFormat!.IssuerSignedJwtAlgValues!.Contains(clientAlg)))
return false;

if (rpSupportedVpFormats.SdJwtDcFormat.KeyBindingJwtAlgValues != null &&
!rpSupportedVpFormats.SdJwtDcFormat.KeyBindingJwtAlgValues.Any(clientAlg => walletMetadataSupportedVpFormats.SdJwtDcFormat!.KeyBindingJwtAlgValues!.Contains(clientAlg)))
return false;
}

//TODO: Remove SdJwtVcFormat in the future as it is deprecated (kept for now for backwards compatibility)
if (rpSupportedVpFormats?.SdJwtVcFormat != null)
{
if (rpSupportedVpFormats.SdJwtVcFormat.IssuerSignedJwtAlgValues != null &&
!rpSupportedVpFormats.SdJwtVcFormat.IssuerSignedJwtAlgValues.Any(clientAlg => walletMetadataSupportedVpFormats.SdJwtVcFormat!.IssuerSignedJwtAlgValues!.Contains(clientAlg)))
return false;

if (rpSupportedVpFormats.SdJwtVcFormat.KeyBindingJwtAlgValues != null &&
!rpSupportedVpFormats.SdJwtVcFormat.KeyBindingJwtAlgValues.Any(clientAlg => walletMetadataSupportedVpFormats.SdJwtVcFormat!.KeyBindingJwtAlgValues!.Contains(clientAlg)))
return false;
}

return true;
}

private static AuthorizationRequestCancellation GetVpFormatsNotSupportedCancellation(Option<Uri> responseUri)
{
var error = new VpFormatsNotSupportedError("The provided vp_formats_supported values are not supported");
return new AuthorizationRequestCancellation(responseUri, [error]);
}
}
8 changes: 4 additions & 4 deletions src/WalletFramework.Oid4Vc/Oid4Vp/Models/MDocFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ public class MDocFormat
/// <summary>
/// Gets the names of supported algorithms.
/// </summary>
[JsonProperty("alg")]
public string[]? Alg { get; private set; }
[JsonProperty("issuerauth_alg_values")]
public string[]? IssuerAuthAlgValues { get; init; }

/// <summary>
/// Gets the names of supported proof types.
/// </summary>
[JsonProperty("proof_type")]
public string[]? ProofTypes { get; private set; }
[JsonProperty("deviceauth_alg_values")]
public string[]? DeviceAuthAlgValues { get; init; }
}
57 changes: 57 additions & 0 deletions src/WalletFramework.Oid4Vc/Oid4Vp/Models/WalletMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Newtonsoft.Json.Linq;

namespace WalletFramework.Oid4Vc.Oid4Vp.Models;

public record WalletMetadata
{
private const string VpFormatsSupportedIdentifier = "vp_formats_supported";
private const string ClientIdPrefixesSupportedIdentifier = "client_id_prefixes_supported";
//TODO: Remove the following identifier in the future, it is deprecated but kept for backwards compatibility for now.
private const string ClientIdSchemesSupportedIdentifier = "client_id_schemes_supported";

public Formats VpFormatsSupported { get; }

public ClientIdScheme[] ClientIdPrefixesSupported { get; }

private WalletMetadata(Formats vpFormatsSupported, ClientIdScheme[] clientIdPrefixesSupported)
{
VpFormatsSupported = vpFormatsSupported;
ClientIdPrefixesSupported = clientIdPrefixesSupported;
}

public static WalletMetadata CreateDefault()
{
var vpFormatsSupported = new Formats
{
SdJwtVcFormat = new SdJwtFormat
{
IssuerSignedJwtAlgValues = ["ES256", "ES384", "ES512", "RS256"],
KeyBindingJwtAlgValues = ["ES256"]
},
SdJwtDcFormat = new SdJwtFormat
{
IssuerSignedJwtAlgValues = ["ES256", "ES384", "ES512", "RS256"],
KeyBindingJwtAlgValues = ["ES256"]
},
MDocFormat = new MDocFormat
{
IssuerAuthAlgValues = ["-7", "-35", "-36", "-8"],
DeviceAuthAlgValues = ["-7"]
}
};

var clientIdPrefixesSupported = new []{(ClientIdScheme)ClientIdScheme.RedirectUriScheme, (ClientIdScheme)ClientIdScheme.X509SanDnsScheme};

return new WalletMetadata(vpFormatsSupported, clientIdPrefixesSupported);
}

public string ToJsonString()
{
return new JObject
{
[VpFormatsSupportedIdentifier] = JObject.FromObject(VpFormatsSupported),
[ClientIdPrefixesSupportedIdentifier] = new JArray {ClientIdPrefixesSupported.Select(x => x.AsString())},
[ClientIdSchemesSupportedIdentifier] = new JArray {ClientIdPrefixesSupported.Select(x => x.AsString())},
}.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,6 @@ private async Task<Option<IEnumerable<ICredential>>> GetMatchingCredentials(
{
return record.DocType == inputDescriptor.Id
&& 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 =>
{
try
Expand Down
Loading
Loading