diff --git a/src/WalletFramework.Core/Base64Url/Base64UrlString.cs b/src/WalletFramework.Core/Base64Url/Base64UrlString.cs index 1d288cd..ea6669e 100644 --- a/src/WalletFramework.Core/Base64Url/Base64UrlString.cs +++ b/src/WalletFramework.Core/Base64Url/Base64UrlString.cs @@ -5,9 +5,11 @@ namespace WalletFramework.Core.Base64Url; public readonly struct Base64UrlString { private string Value { get; } - + public byte[] AsByteArray => Base64UrlEncoder.DecodeBytes(Value); + public string AsString => Value; + private Base64UrlString(string value) { Value = value; diff --git a/src/WalletFramework.Core/WalletFramework.Core.csproj b/src/WalletFramework.Core/WalletFramework.Core.csproj index 3b7367b..a9bb151 100644 --- a/src/WalletFramework.Core/WalletFramework.Core.csproj +++ b/src/WalletFramework.Core/WalletFramework.Core.csproj @@ -5,6 +5,7 @@ enable + diff --git a/src/WalletFramework.MdocLib/Mdoc.cs b/src/WalletFramework.MdocLib/Mdoc.cs index 5b8e0f1..f0d55b6 100644 --- a/src/WalletFramework.MdocLib/Mdoc.cs +++ b/src/WalletFramework.MdocLib/Mdoc.cs @@ -104,11 +104,10 @@ public static Validation FromIssuerSigned(string base64UrlencodedCborByteS }); var validateIntegrity = new List> - { - MdocFun.DocTypeMatches, - MdocFun.DigestsMatch - } - .AggregateValidators(); + { + MdocFun.DocTypeMatches, + MdocFun.DigestsMatch + }.AggregateValidators(); return from bytes in decodeBase64Url(base64UrlencodedCborByteString) diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs index a86ed70..a2c63da 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs @@ -136,8 +136,7 @@ from issState in code.IssuerState null, null); - var authServerMetadata = - await FetchAuthorizationServerMetadataAsync(issuerMetadata); + var authServerMetadata = await FetchAuthorizationServerMetadataAsync(issuerMetadata); _httpClient.DefaultRequestHeaders.Clear(); var response = await _httpClient.PostAsync( diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/AuthResponse/Encryption/EncryptedAuthorizationResponse.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/AuthResponse/Encryption/EncryptedAuthorizationResponse.cs new file mode 100644 index 0000000..1553e6b --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/AuthResponse/Encryption/EncryptedAuthorizationResponse.cs @@ -0,0 +1,50 @@ +using Hyperledger.Aries.Extensions; +using Jose; +using LanguageExt; +using WalletFramework.Oid4Vc.Oid4Vp.Jwk; +using WalletFramework.Oid4Vc.Oid4Vp.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vp.AuthResponse.Encryption; + +public record EncryptedAuthorizationResponse(string Jwe) +{ + public override string ToString() => Jwe; +} + +public static class EncryptedAuthorizationResponseFun +{ + public static FormUrlEncodedContent ToFormUrl(this EncryptedAuthorizationResponse response) + { + var content = new Dictionary + { + { "response", response.ToString() }, + }; + + return new FormUrlEncodedContent(content); + } + + public static EncryptedAuthorizationResponse Encrypt( + this AuthorizationResponse response, + AuthorizationRequest authorizationRequest, + Option mdocNonce) + { + var verifierPubKey = authorizationRequest.ClientMetadata!.Jwks.First(); + + var headers = new Dictionary + { + { "apv", authorizationRequest.Nonce }, + { "kid", verifierPubKey.Kid } + }; + + mdocNonce.IfSome(nonce => headers.Add("apu", nonce.AsBase64Url.ToString())); + + var jwe = JWE.EncryptBytes( + response.ToJson().GetUTF8Bytes(), + new [] { new JweRecipient(JweAlgorithm.ECDH_ES, verifierPubKey.ToEcdh()) }, + JweEncryption.A256GCM, + mode: SerializationMode.Compact, + extraProtectedHeaders: headers); + + return new EncryptedAuthorizationResponse(jwe); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Jwk/JwkFun.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Jwk/JwkFun.cs new file mode 100644 index 0000000..b2c841b --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Jwk/JwkFun.cs @@ -0,0 +1,23 @@ +using System.Security.Cryptography; +using Microsoft.IdentityModel.Tokens; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Jwk; + +public static class JwkFun +{ + public static ECDiffieHellman ToEcdh(this JsonWebKey jwk) + { + var ecParameters = new ECParameters + { + Q = + { + X = Base64UrlEncoder.DecodeBytes(jwk.X), + Y = Base64UrlEncoder.DecodeBytes(jwk.Y) + }, + // TODO: Map curve from jwk + Curve = ECCurve.NamedCurves.nistP256 + }; + + return ECDiffieHellman.Create(ecParameters); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequest.cs index af71fb4..c6f5964 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequest.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequest.cs @@ -11,8 +11,8 @@ namespace WalletFramework.Oid4Vc.Oid4Vp.Models; /// public record AuthorizationRequest { - private const string DirectPost = "direct_post"; - private const string DirectPostJwt = "direct_post.jwt"; + public const string DirectPost = "direct_post"; + public const string DirectPostJwt = "direct_post.jwt"; private const string VpToken = "vp_token"; @@ -45,6 +45,9 @@ public record AuthorizationRequest /// [JsonProperty("response_uri")] public string ResponseUri { get; } + + [JsonProperty("response_mode")] + public string ResponseMode { get; } /// /// Gets the client metadata. Contains the Verifier metadata. @@ -89,6 +92,7 @@ private AuthorizationRequest( string clientId, string nonce, string responseUri, + string responseMode, ClientMetadata? clientMetadata, string? clientMetadataUri, string? scope, @@ -101,6 +105,7 @@ private AuthorizationRequest( Nonce = nonce; PresentationDefinition = presentationDefinition; ResponseUri = responseUri; + ResponseMode = responseMode; Scope = scope; State = state; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationResponse.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationResponse.cs index 901ac78..039f1f1 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationResponse.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationResponse.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models; namespace WalletFramework.Oid4Vc.Oid4Vp.Models; @@ -10,18 +11,30 @@ public class AuthorizationResponse /// /// Gets or sets the VP Token. /// - [JsonProperty ("vp_token")] + [JsonProperty("vp_token")] public string VpToken { get; set; } = null!; /// /// Gets or sets the Presentation Submission. /// - [JsonProperty ("presentation_submission")] - public string PresentationSubmission { get; set; } = null!; + [JsonProperty("presentation_submission")] + public PresentationSubmission PresentationSubmission { get; set; } = null!; /// /// Gets or sets the State. /// - [JsonProperty ("state", NullValueHandling = NullValueHandling.Ignore)] - public string? State { get; set; } = null!; + [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] + public string? State { get; set; } +} + +public static class AuthorizationResponseFun +{ + public static FormUrlEncodedContent ToFormUrl(this AuthorizationResponse authorizationResponse) + { + var json = JsonConvert.SerializeObject(authorizationResponse); + var nameValueCollection = + JsonConvert.DeserializeObject>(json)!.ToList(); + + return new FormUrlEncodedContent(nameValueCollection); + } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/ClientJwksConverter.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/ClientJwksConverter.cs new file mode 100644 index 0000000..f716e6f --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/ClientJwksConverter.cs @@ -0,0 +1,40 @@ +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Models; + +public class ClientJwksConverter : JsonConverter> +{ + public override bool CanRead => true; + + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, List? value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override List ReadJson( + JsonReader reader, + Type objectType, + List? existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + var json = JObject.Load(reader); + var validKeysArray = + from keys in json.GetByKey("keys") + from keysArray in keys.ToJArray() + select keysArray; + + var result = validKeysArray.OnSuccess(keysArray => + { + return keysArray.Select(keyToken => JsonWebKey.Create(keyToken.ToString())).ToList(); + }).UnwrapOrThrow(); + + return result; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/ClientMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/ClientMetadata.cs index 635972a..828fd3e 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/ClientMetadata.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/ClientMetadata.cs @@ -1,3 +1,4 @@ +using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; namespace WalletFramework.Oid4Vc.Oid4Vp.Models; @@ -5,7 +6,6 @@ namespace WalletFramework.Oid4Vc.Oid4Vp.Models; /// /// Represents the metadata of a client (verifier). /// -// TODO: Rename this to VerifierMetadata public record ClientMetadata { /// @@ -38,8 +38,9 @@ public record ClientMetadata [JsonProperty("logo_uri")] public string? LogoUri { get; } - // TODO: JWKs of the verifier - // public List PublicKeys { get; } + [JsonProperty("jwks")] + [JsonConverter(typeof(ClientJwksConverter))] + public List Jwks { get; } /// /// The URI to a human-readable privacy policy document for the client (verifier). @@ -54,7 +55,15 @@ public record ClientMetadata public string? TosUri { get; } [JsonConstructor] - private ClientMetadata(string? clientName, string? clientUri, string[]? contacts, string? logoUri, string? policyUri, string? tosUri, string[] redirectUris) + private ClientMetadata( + string? clientName, + string? clientUri, + string[]? contacts, + string? logoUri, + string? policyUri, + string? tosUri, + string[] redirectUris, + List jwks) { ClientName = clientName; ClientUri = clientUri; @@ -63,5 +72,6 @@ private ClientMetadata(string? clientName, string? clientUri, string[]? contacts PolicyUri = policyUri; TosUri = tosUri; RedirectUris = redirectUris; + Jwks = jwks; } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs index 3696823..aab8247 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs @@ -12,6 +12,7 @@ using WalletFramework.MdocLib.Security; using WalletFramework.MdocVc; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; +using WalletFramework.Oid4Vc.Oid4Vp.AuthResponse.Encryption; using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Services; using WalletFramework.SdJwtVc.Models.Records; @@ -126,6 +127,7 @@ from path in field.Path.Select(path => path.TrimStart('$', '.')) var toDisclose = claims.Select(claim => { + // TODO: This is needed because in mdoc the requested attributes look like this: $[Namespace][ElementId]. Refactor this more clean var keys = claim.Split(new[] { "[", "]" }, StringSplitOptions.RemoveEmptyEntries); var nameSpace = NameSpace.ValidNameSpace(keys[0]).UnwrapOrThrow(); @@ -170,22 +172,12 @@ from path in field.Path.Select(path => path.TrimStart('$', '.')) presentationMaps.Select(tuple => (tuple.InputDescriptorId, tuple.Presentation, tuple.Format)).ToArray() ); - var httpClient = _httpClientFactory.CreateClient(); - httpClient.DefaultRequestHeaders.Clear(); - if (clientAttestation is not null) - httpClient.AddClientAttestationPopHeader(clientAttestation); - - var json = SerializeObject(authorizationResponse); - var nameValueCollection = DeserializeObject>(json)!.ToList(); - - // TODO: This is only a hack until the encryption response is implemented - mdocNonce.IfSome(nonce => + var content = authorizationRequest.ResponseMode switch { - var pair = new KeyValuePair("apu", nonce.AsBase64Url.ToString()); - nameValueCollection.Add(pair); - }); - - var content = new FormUrlEncodedContent(nameValueCollection); + AuthorizationRequest.DirectPost => authorizationResponse.ToFormUrl(), + AuthorizationRequest.DirectPostJwt => authorizationResponse.Encrypt(authorizationRequest, mdocNonce).ToFormUrl(), + _ => throw new ArgumentOutOfRangeException(nameof(authorizationRequest.ResponseMode)) + }; var message = new HttpRequestMessage { @@ -193,13 +185,15 @@ from path in field.Path.Select(path => path.TrimStart('$', '.')) Method = HttpMethod.Post, Content = content }; - + // TODO: Introduce timeout + var httpClient = _httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.Clear(); var responseMessage = await httpClient.SendAsync(message); if (!responseMessage.IsSuccessStatusCode) { var str = await responseMessage.Content.ReadAsStringAsync(); - throw new InvalidOperationException($"Authorization Response could not be sent with message {str}"); + throw new InvalidOperationException($"Authorization Response failed with message {str}"); } var presentedCredentials = presentationMaps.Select(presentationMap => diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpHaipClient.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpHaipClient.cs index f61baf6..0d47eff 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpHaipClient.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpHaipClient.cs @@ -56,7 +56,7 @@ public async Task CreateAuthorizationResponseAsync( return new AuthorizationResponse { - PresentationSubmission = SerializeObject(presentationSubmission), + PresentationSubmission = presentationSubmission, VpToken = vpToken.Count > 1 ? SerializeObject(vpToken) : vpToken[0], State = authorizationRequest.State }; diff --git a/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj b/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj index b83cad8..801ff21 100644 --- a/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj +++ b/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj @@ -1,5 +1,4 @@ - netstandard2.1 enable