Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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