diff --git a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Extensions/JwtSecurityTokenExtensions.cs b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Extensions/JwtSecurityTokenExtensions.cs new file mode 100644 index 00000000..c566a2d2 --- /dev/null +++ b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Extensions/JwtSecurityTokenExtensions.cs @@ -0,0 +1,57 @@ +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System; +using static Microsoft.IdentityModel.Tokens.Base64UrlEncoder; +using static Org.BouncyCastle.Security.SignerUtilities; +using static System.Text.Encoding; + +namespace Hyperledger.Aries.Features.OpenID4VC.Vp.Extensions +{ + /// + /// Extension methods for . + /// + public static class JwtSecurityTokenExtensions + { + /// + /// Validates the signature of the JWT token using the provided public key. + /// + /// The JWT token to validate. + /// The public key to use for validation. + /// True if the signature is valid, otherwise false. + public static bool IsSignatureValid(this JwtSecurityToken token, ECPublicKeyParameters publicKey) + { + var signer = GetSigner("SHA-256withECDSA"); + + signer.Init(false, publicKey); + + var encodedHeaderAndPayload = UTF8.GetBytes(token.EncodedHeader + "." + token.EncodedPayload); + + signer.BlockUpdate(encodedHeaderAndPayload, 0, encodedHeaderAndPayload.Length); + + return signer.VerifySignature( + ConvertRawToDerFormat( + DecodeBytes(token.RawSignature) + ) + ); + } + + private static byte[] ConvertRawToDerFormat(byte[] rawSignature) + { + if (rawSignature.Length != 64) + throw new ArgumentException("Raw signature should be 64 bytes long", nameof(rawSignature)); + + var r = new BigInteger(1, rawSignature.Take(32).ToArray()); + var s = new BigInteger(1, rawSignature.Skip(32).ToArray()); + + var derSignature = new DerSequence( + new DerInteger(r), + new DerInteger(s) + ).GetDerEncoded(); + + return derSignature; + } + } +} diff --git a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Extensions/X509CertificateExtensions.cs b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Extensions/X509CertificateExtensions.cs new file mode 100644 index 00000000..cf5596a0 --- /dev/null +++ b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Extensions/X509CertificateExtensions.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Pkix; +using Org.BouncyCastle.Utilities.Collections; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.X509.Store; + +namespace Hyperledger.Aries.Features.OpenID4VC.Vp.Extensions +{ + /// + /// Extension methods for . + /// + public static class X509CertificateExtensions + { + /// + /// Validates the trust chain of the certificate. + /// + /// The trust chain to validate. + /// True if the trust chain is valid, otherwise false. + public static bool IsTrustChainValid(this List trustChain) + { + var leafCert = trustChain.First(); + + var rootCerts = + new HashSet( + trustChain + .Skip(1) + .Where(cert => cert.IssuerDN.Equivalent(cert.SubjectDN)) + .Select(cert => new TrustAnchor(cert, null)) + .ToList() + ); + + var intermediateCerts = + new HashSet( + trustChain + .Skip(1) + .Where(cert => !cert.IssuerDN.Equivalent(cert.SubjectDN)) + .Append(leafCert) + ); + + var builderParams = + new PkixBuilderParameters( + rootCerts, + new X509CertStoreSelector + { + Certificate = leafCert + } + ) + { + IsRevocationEnabled = + leafCert.GetExtensionValue(X509Extensions.CrlDistributionPoints) is not null + }; + + builderParams.AddStore( + X509StoreFactory.Create( + "Certificate/Collection", + new X509CollectionStoreParameters(intermediateCerts) + ) + ); + + // This throws if validation fails + new PkixCertPathValidator() + .Validate( + new PkixCertPathBuilder() + .Build(builderParams) + .CertPath, + builderParams + ); + + return true; + } + } +} diff --git a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Models/AuthorizationRequest.cs b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Models/AuthorizationRequest.cs index c85c8c69..fc7c41f5 100644 --- a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Models/AuthorizationRequest.cs +++ b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Models/AuthorizationRequest.cs @@ -1,9 +1,8 @@ using System; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Web; using Hyperledger.Aries.Features.Pex.Models; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using static Hyperledger.Aries.Features.OpenId4Vc.Vp.Models.ClientIdScheme; namespace Hyperledger.Aries.Features.OpenId4Vc.Vp.Models { @@ -12,156 +11,118 @@ namespace Hyperledger.Aries.Features.OpenId4Vc.Vp.Models /// public class AuthorizationRequest { - /// - /// Gets or sets the response type. Determines what the Authorization Response should contain. - /// - [JsonProperty("response_type")] - public string ResponseType { get; set; } = null!; - - /// - /// Gets or sets the client id. The Identifier of the Verifier - /// - [JsonProperty("client_id")] - public string ClientId { get; set; } = null!; + private const string DirectPost = "direct_post"; - /// - /// Gets or sets the client id scheme. - /// - [JsonProperty("client_id_scheme")] - public string? ClientIdScheme { get; set; } - - /// - /// Gets or sets the redirect uri. - /// - [JsonProperty("redirect_uri")] - public string? RedirectUri { get; set; } - - /// - /// Gets or sets the client metadata. Contains the Verifier metadata - /// - [JsonProperty("client_metadata")] - public string? ClientMetadata { get; set; } + private const string VpToken = "vp_token"; /// - /// Gets or sets the client metadata uri. Can be used to retrieve the verifier metadata. + /// Gets the client id scheme. /// - [JsonProperty("client_metadata_uri")] - public string? ClientMetadataUri { get; set; } + [JsonProperty("client_id_scheme")] + public ClientIdScheme ClientIdScheme { get; } /// - /// The scope of the request. + /// Gets the presentation definition. Contains the claims that the Verifier wants to receive. /// - [JsonProperty("scope")] - public string? Scope { get; set; } + [JsonProperty("presentation_definition")] + public PresentationDefinition PresentationDefinition { get; } /// - /// Gets or sets the nonce. Random string for session binding. + /// Gets the client id. The Identifier of the Verifier. /// - [JsonProperty("nonce")] - public string Nonce { get; set; } = null!; + [JsonProperty("client_id")] + public string ClientId { get; } /// - /// Gets or sets the response mode. Determines how to send the Authorization Response. + /// Gets the nonce. Random string for session binding. /// - [JsonProperty("response_mode")] - public string? ResponseMode { get; set; } + [JsonProperty("nonce")] + public string Nonce { get; } /// - /// Gets or sets the response mode. Determines where to send the Authorization Response to. + /// Gets the response mode. Determines where to send the Authorization Response to. /// [JsonProperty("response_uri")] - public string ResponseUri { get; set; } = null!; + public string ResponseUri { get; } /// - /// Gets or sets the state. + /// Gets the client metadata. Contains the Verifier metadata. /// - [JsonProperty("state")] - public string? State { get; set; } + [JsonProperty("client_metadata")] + public string? ClientMetadata { get; } /// - /// Gets or sets the presentation definition. Contains the claims that the Verifier wants to receive. + /// Gets the client metadata uri. Can be used to retrieve the verifier metadata. /// - [JsonIgnore] - public PresentationDefinition PresentationDefinition { get; set; } = null!; + [JsonProperty("client_metadata_uri")] + public string? ClientMetadataUri { get; } /// - /// Parses an Authorization Request from a Jwt. + /// The scope of the request. /// - /// - /// The AuthorizationRequest - public static AuthorizationRequest? ParseFromJwt(string jwt) - { - var jwtHandler = new JwtSecurityTokenHandler(); - var token = jwtHandler.ReadToken(jwt) as JwtSecurityToken; - if (token == null) return null; - - var presentationDefinition = JsonConvert.DeserializeObject(token.Payload["presentation_definition"].ToString()); - var authorizationRequest = JsonConvert.DeserializeObject(token.Payload.SerializeToJson()); - - if (!(authorizationRequest != null && presentationDefinition != null)) - return null; - - authorizationRequest.PresentationDefinition = presentationDefinition; - - return authorizationRequest; - } + [JsonProperty("scope")] + public string? Scope { get; } /// - /// Parses an Authorization Request from a Uri. + /// Gets the state. /// - /// - /// The AuthorizationRequest - public static AuthorizationRequest? ParseFromUri(Uri uri) + [JsonProperty("state")] + public string? State { get; } + + [JsonConstructor] + private AuthorizationRequest( + ClientIdScheme clientIdScheme, + PresentationDefinition presentationDefinition, + string clientId, + string nonce, + string responseUri, + string? clientMetadata, + string? clientMetadataUri, + string? scope, + string? state) { - var query = HttpUtility.ParseQueryString(uri.Query); - var dict = query.AllKeys.ToDictionary(key => key, key => query[key]); - var json = JsonConvert.SerializeObject(dict); - - var presentationDefinition = JsonConvert.DeserializeObject(query["presentation_definition"]); - var authorizationRequest = JsonConvert.DeserializeObject(json); - - if (!(authorizationRequest != null && presentationDefinition != null)) - return null; - - authorizationRequest.PresentationDefinition = presentationDefinition; - - return authorizationRequest; + ClientId = clientId; + ClientIdScheme = clientIdScheme; + ClientMetadata = clientMetadata; + ClientMetadataUri = clientMetadataUri; + Nonce = nonce; + PresentationDefinition = presentationDefinition; + ResponseUri = responseUri; + Scope = scope; + State = state; } - } - /// - /// Extension methods for the class. - /// - public static class AuthorizationRequestExtension - { /// - /// Checks if the Authorization Request is HAIP conform. + /// Creates a new instance of the class. /// - /// - /// Returns bool indicating whether the AuthorizationRequest is haip conform - public static bool IsHaipConform(this AuthorizationRequest authorizationRequest) + /// The json representation of the authorization request. + /// A new instance of the class. + /// Thrown when the request does not match the HAIP. + public static AuthorizationRequest CreateAuthorizationRequest(string authorizationRequestJson) + => CreateAuthorizationRequest(JObject.Parse(authorizationRequestJson)); + + private static AuthorizationRequest CreateAuthorizationRequest(JObject authorizationRequestJson) => + IsHaipConform(authorizationRequestJson) + ? authorizationRequestJson.ToObject() + ?? throw new InvalidOperationException("Could not parse the Authorization Request") + : throw new InvalidOperationException( + "Invalid Authorization Request. The request does not match the HAIP" + ); + + private static bool IsHaipConform(JObject authorizationRequestJson) { - if (authorizationRequest.ResponseType != "vp_token") - return false; - - if (authorizationRequest.ResponseMode != "direct_post") - return false; - - if (String.IsNullOrEmpty(authorizationRequest.ResponseUri)) - return false; - - if (!String.IsNullOrEmpty(authorizationRequest.RedirectUri)) - return false; - - if (authorizationRequest.ClientIdScheme == "redirect_uri" - & authorizationRequest.ClientId != authorizationRequest.ResponseUri) - return false; - - //TODO: Not supported yet - //if (!(authorizationRequest.ClientIdScheme == "x509_san_dns" || authorizationRequest.ClientIdScheme == "verifier_attestation")) - //return false; - - return true; + var responseType = authorizationRequestJson["response_type"]!.ToString(); + var responseUri = authorizationRequestJson["response_uri"]!.ToString(); + var responseMode = authorizationRequestJson["response_mode"]!.ToString(); + var redirectUri = authorizationRequestJson["redirect_uri"]; + var clientIdScheme = authorizationRequestJson["client_id_scheme"]; + + return + responseType == VpToken + && responseMode == DirectPost + && !string.IsNullOrEmpty(responseUri) + && redirectUri is null + && clientIdScheme!.ToString() is X509SanDnsScheme or VerifierAttestationScheme; } } } diff --git a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Models/ClientIdScheme.cs b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Models/ClientIdScheme.cs new file mode 100644 index 00000000..4a9aef0c --- /dev/null +++ b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Models/ClientIdScheme.cs @@ -0,0 +1,67 @@ +using System; +using static Hyperledger.Aries.Features.OpenId4Vc.Vp.Models.ClientIdScheme.ClientIdSchemeValue; + +namespace Hyperledger.Aries.Features.OpenId4Vc.Vp.Models +{ + /// + /// The client ID scheme used to obtain and validate metadata of the verifier. + /// + public record ClientIdScheme + { + /// + /// The client ID scheme value. + /// + public enum ClientIdSchemeValue + { + /// + /// The X509 SAN DNS client ID scheme. + /// + X509SanDns, + + /// + /// The verifier attestation client ID scheme. + /// + VerifierAttestation + } + + /// + /// The Verifier Attestation scheme. + /// + public const string VerifierAttestationScheme = "verifier_attestation"; + + /// + /// The X509 SAN DNS scheme. + /// + public const string X509SanDnsScheme = "x509_san_dns"; + + /// + /// The client ID scheme value. + /// + public ClientIdSchemeValue Value { get; } + + /// + /// Initializes a new instance of the class. + /// + private ClientIdScheme(ClientIdSchemeValue value) => Value = value; + + /// + /// Creates a client ID scheme from the specified input. + /// + /// The input to create the client ID scheme from. + /// The client ID scheme created from the input. + /// The client ID scheme is not supported. + public static ClientIdScheme CreateClientIdScheme(string input) => + input switch + { + X509SanDnsScheme => new ClientIdScheme(X509SanDns), + VerifierAttestationScheme => + throw new NotImplementedException("Verifier Attestation not yet implemented"), + _ => throw new InvalidOperationException($"Client ID Scheme {input} is not supported") + }; + + /// + /// Implicitly converts the input to a client ID scheme. + /// + public static implicit operator ClientIdScheme(string input) => CreateClientIdScheme(input); + } +} diff --git a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Models/RequestObject.cs b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Models/RequestObject.cs new file mode 100644 index 00000000..dec60ffe --- /dev/null +++ b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Models/RequestObject.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Hyperledger.Aries.Features.OpenID4VC.Vp.Extensions; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.X509; +using X509Certificate = Org.BouncyCastle.X509.X509Certificate; +using static Hyperledger.Aries.Features.OpenId4Vc.Vp.Models.AuthorizationRequest; +using static Newtonsoft.Json.Linq.JArray; +using static System.Convert; + +namespace Hyperledger.Aries.Features.OpenId4Vc.Vp.Models +{ + /// + /// Represents a request object as defined in the context of OpenID4VP. + /// + public readonly struct RequestObject + { + private JwtSecurityToken Value { get; } + + /// + /// The client ID scheme used to obtain and validate metadata of the verifier. + /// + public ClientIdScheme ClientIdScheme => AuthorizationRequest.ClientIdScheme; + + /// + /// The client ID of the verifier. + /// + public string ClientId => AuthorizationRequest.ClientId; + + private AuthorizationRequest AuthorizationRequest { get; } + + /// + /// Creates a new instance of the class. + /// + public static implicit operator RequestObject(JwtSecurityToken token) => new(token); + + /// + /// Creates a new instance of the class. + /// + public static implicit operator JwtSecurityToken(RequestObject requestObject) => requestObject.Value; + + private RequestObject(JwtSecurityToken token) + { + AuthorizationRequest = CreateAuthorizationRequest(token.Payload.SerializeToJson()); + Value = token; + } + + /// + /// Creates a new instance of the class. + /// + public static RequestObject CreateRequestObject(string requestObjectJson) + { + var tokenHandler = new JwtSecurityTokenHandler(); + + var jwt = tokenHandler.ReadJwtToken(requestObjectJson); + + return new RequestObject(jwt); + } + + /// + /// Gets the authorization request from the request object. + /// + public AuthorizationRequest ToAuthorizationRequest() => AuthorizationRequest; + } + + /// + /// Extension methods for . + /// + public static class RequestObjectExtensions + { + /// + /// Validates the JWT signature. + /// + /// The validated request object. + /// Throws when validation fails + public static RequestObject ValidateJwt(this RequestObject requestObject) => + ((JwtSecurityToken)requestObject).IsSignatureValid( + (ECPublicKeyParameters)requestObject.GetLeafCertificate().GetPublicKey() + ) + ? requestObject + : throw new InvalidOperationException("Invalid JWT Signature"); + + /// + /// Validates the SAN name of the leaf certificate + /// + /// The validated request object + /// Throws when validation fails + public static RequestObject ValidateSanName(this RequestObject requestObject) => + new X509Certificate2( + requestObject.GetLeafCertificate().GetEncoded() + ) + .GetNameInfo(X509NameType.DnsName, false) == requestObject.ClientId + ? requestObject + : throw new InvalidOperationException("SAN does not match Client ID"); + + /// + /// Validates the trust chain of the leaf certificate + /// + /// The validated request object + /// Throws when validation fails + public static RequestObject ValidateTrustChain(this RequestObject requestObject) => + requestObject + .GetCertificates() + .IsTrustChainValid() + ? requestObject + : throw new InvalidOperationException("Validation of trust chain failed"); + + private static List GetCertificates(this RequestObject requestObject) => + Parse(((JwtSecurityToken)requestObject).Header.X5c) + .Select( + certAsJToken => + new X509CertificateParser() + .ReadCertificate( + FromBase64String(certAsJToken.ToString()) + ) + ) + .ToList(); + + private static X509Certificate GetLeafCertificate(this RequestObject requestObject) => + GetCertificates(requestObject).First(); + } +} diff --git a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/IOid4VpClientService.cs b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/IOid4VpClientService.cs index c67d8fdd..a23f9d52 100644 --- a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/IOid4VpClientService.cs +++ b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/IOid4VpClientService.cs @@ -30,6 +30,6 @@ public interface IOid4VpClientService /// /// A task representing the asynchronous operation. The task result contains the Callback Url of the Authorization Response if present. /// - Task PrepareAndSendAuthorizationResponseAsync(IAgentContext agentContext, AuthorizationRequest authorizationRequest, SelectedCredential[] selectedCredentials); + Task SendAuthorizationResponseAsync(IAgentContext agentContext, AuthorizationRequest authorizationRequest, SelectedCredential[] selectedCredentials); } } diff --git a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/Oid4VpClientService.cs b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/Oid4VpClientService.cs index 9f96b1d3..a83b1d8e 100644 --- a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/Oid4VpClientService.cs +++ b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/Oid4VpClientService.cs @@ -2,27 +2,20 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Reactive.Linq; using System.Threading.Tasks; using Hyperledger.Aries.Agents; using Hyperledger.Aries.Features.OpenId4Vc.Vp.Models; using Hyperledger.Aries.Features.OpenId4Vc.Vp.Services; -using Hyperledger.Aries.Features.Pex.Models; using Hyperledger.Aries.Features.SdJwt.Models.Records; using Hyperledger.Aries.Features.SdJwt.Services.SdJwtVcHolderService; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using static Newtonsoft.Json.JsonConvert; +using static Newtonsoft.Json.Linq.JObject; namespace Hyperledger.Aries.Features.OpenID4VC.Vp.Services { /// internal class Oid4VpClientService : IOid4VpClientService { - private readonly IHttpClientFactory _httpClientFactory; - private readonly IOid4VpHaipClient _oid4VpHaipClient; - private readonly IOid4VpRecordService _oid4VpRecordService; - private readonly ISdJwtVcHolderService _sdJwtVcHolderService; - /// /// Initializes a new instance of the class. /// @@ -42,118 +35,137 @@ public Oid4VpClientService( _oid4VpRecordService = oid4VpRecordService; } + private readonly IHttpClientFactory _httpClientFactory; + private readonly IOid4VpHaipClient _oid4VpHaipClient; + private readonly IOid4VpRecordService _oid4VpRecordService; + private readonly ISdJwtVcHolderService _sdJwtVcHolderService; + /// public async Task<(AuthorizationRequest, CredentialCandidates[])> ProcessAuthorizationRequestAsync( IAgentContext agentContext, Uri authorizationRequestUri) { var haipAuthorizationRequestUri = HaipAuthorizationRequestUri.FromUri(authorizationRequestUri); - - var authorizationRequest = await _oid4VpHaipClient.ProcessAuthorizationRequestAsync(haipAuthorizationRequestUri); + + var authorizationRequest = + await _oid4VpHaipClient.ProcessAuthorizationRequestAsync(haipAuthorizationRequestUri); var credentials = await _sdJwtVcHolderService.ListAsync(agentContext); - var credentialCandidates = await _sdJwtVcHolderService.FindCredentialCandidates(credentials.ToArray(), - authorizationRequest.PresentationDefinition.InputDescriptors); + + var credentialCandidates = await _sdJwtVcHolderService.FindCredentialCandidates( + credentials.ToArray(), + authorizationRequest.PresentationDefinition.InputDescriptors + ); return (authorizationRequest, credentialCandidates); } /// - public async Task PrepareAndSendAuthorizationResponseAsync( + public async Task SendAuthorizationResponseAsync( IAgentContext agentContext, AuthorizationRequest authorizationRequest, SelectedCredential[] selectedCredentials) { - var authorizationResponse = await PrepareAuthorizationResponse( - authorizationRequest, - selectedCredentials); - - var redirectUri = await SendAuthorizationResponse( - authorizationResponse, - new Uri(authorizationRequest.ResponseUri)); - - var presentedCredentials = - from credential in selectedCredentials - let inputD = authorizationRequest - .PresentationDefinition - .InputDescriptors - .FirstOrDefault(x => x.Id == credential.InputDescriptorId) - select new PresentedCredential - { - CredentialId = ((SdJwtRecord)credential.Credential).Id, - PresentedClaims = GetPresentedClaimsForCredential(inputD, (SdJwtRecord)credential.Credential) - }; - - await _oid4VpRecordService.StoreAsync( - agentContext, - authorizationRequest.ClientId, - authorizationRequest.ClientMetadata, - authorizationRequest.PresentationDefinition.Name, - presentedCredentials.ToArray()); - - return redirectUri; - } - - private async Task PrepareAuthorizationResponse(AuthorizationRequest authorizationRequest, SelectedCredential[] selectedCredentials) - { - var presentationMaps = - from credential in selectedCredentials.ToObservable() + var createPresentationMaps = + from credential in selectedCredentials from inputDescriptor in authorizationRequest.PresentationDefinition.InputDescriptors where credential.InputDescriptorId == inputDescriptor.Id - from presentation in Observable.FromAsync(async () => await CreatePresentation( - inputDescriptor, + let disclosureNames = inputDescriptor + .Constraints + .Fields? + .SelectMany(field => field + .Path + .Select(path => path.Split(".").Last()) + ) + let createPresentation = _sdJwtVcHolderService.CreatePresentation( (SdJwtRecord)credential.Credential, + disclosureNames.ToArray(), authorizationRequest.ClientId, - authorizationRequest.Nonce)) - select (inputDescriptor.Id, presentation); - - return await _oid4VpHaipClient.CreateAuthorizationResponseAsync(authorizationRequest, - await presentationMaps.ToArray()); - } - - private async Task SendAuthorizationResponse(AuthorizationResponse authorizationResponse, Uri responseUri) - { - var authorizationResponseJson = JsonConvert.SerializeObject(authorizationResponse); - var authorizationResponseDict = JsonConvert.DeserializeObject>(authorizationResponseJson); + authorizationRequest.Nonce + ) + select (inputDescriptor.Id, createPresentation); - var request = new HttpRequestMessage + var presentationMaps = new List<(string, string)>(); + foreach (var (inputDescriptorId, createPresentation) in createPresentationMaps) { - RequestUri = responseUri, - Method = HttpMethod.Post, - Content = new FormUrlEncodedContent(authorizationResponseDict) - }; + presentationMaps.Add((inputDescriptorId, await createPresentation)); + } + + var authorizationResponse = await _oid4VpHaipClient.CreateAuthorizationResponseAsync( + authorizationRequest, + presentationMaps.ToArray() + ); var httpClient = _httpClientFactory.CreateClient(); httpClient.DefaultRequestHeaders.Clear(); - var responseMessage = await httpClient.SendAsync(request); + var responseMessage = + await httpClient.SendAsync( + new HttpRequestMessage + { + RequestUri = new Uri(authorizationRequest.ResponseUri), + Method = HttpMethod.Post, + Content = new FormUrlEncodedContent( + DeserializeObject>( + SerializeObject(authorizationResponse) + )? + .ToList() + ?? throw new InvalidOperationException("Authorization Response could not be parsed") + ) + } + ); if (!responseMessage.IsSuccessStatusCode) throw new InvalidOperationException("Authorization Response could not be sent"); - - var content = await responseMessage.Content.ReadAsStringAsync(); - var redirectUri = string.IsNullOrEmpty(content) ? null : JObject.Parse(content)["redirect_uri"]?.ToString(); - return redirectUri == null ? null : new Uri(redirectUri); - } - private async Task CreatePresentation(InputDescriptor inputDescriptor, SdJwtRecord credential, - string clientId, string nonce) - { - var claimsToDisclose = GetDisclosureNamesFromInputDescriptor(inputDescriptor); - return await _sdJwtVcHolderService.CreatePresentation(credential, claimsToDisclose, clientId, nonce); - } + var responseContent = await responseMessage.Content.ReadAsStringAsync(); - private static string[]? GetDisclosureNamesFromInputDescriptor(InputDescriptor inputDescriptor) - => inputDescriptor.Constraints.Fields == null + var redirectUri = string.IsNullOrEmpty(responseContent) ? null - : (from field in inputDescriptor.Constraints.Fields - from path in field.Path - select path.Split(".").Last()).ToArray(); - - private static Dictionary GetPresentedClaimsForCredential(InputDescriptor inputDescriptor, - SdJwtRecord sdJwtRecord) - => (from field in inputDescriptor.Constraints.Fields - from path in field.Path - select sdJwtRecord.Claims.FirstOrDefault(x => x.Key == path.Split(".").Last())) - .ToDictionary(claim => claim.Key, claim => new PresentedClaim(){Value = claim.Value}); + : new Uri(Parse(responseContent)["redirect_uri"]?.ToString()!); + + var presentedCredentials = selectedCredentials + .Select(credential => + { + var inputDescriptor = + authorizationRequest + .PresentationDefinition + .InputDescriptors + .Single(descriptor => descriptor.Id == credential.InputDescriptorId); + + return new PresentedCredential + { + CredentialId = ((SdJwtRecord)credential.Credential).Id, + PresentedClaims = + inputDescriptor + .Constraints + .Fields? + .SelectMany( + field => field.Path.Select( + path => + ((SdJwtRecord)credential.Credential) + .Claims + .First(x => + x.Key == path.Split(".").Last() + ) + ) + ) + .ToDictionary( + claim => claim.Key, + claim => new PresentedClaim { Value = claim.Value } + )! + }; + } + ); + + await _oid4VpRecordService.StoreAsync( + agentContext, + authorizationRequest.ClientId, + authorizationRequest.ClientMetadata, + authorizationRequest.PresentationDefinition.Name, + presentedCredentials.ToArray() + ); + + return redirectUri; + } } } diff --git a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/Oid4VpHaipClient.cs b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/Oid4VpHaipClient.cs index 4bfdd371..65d06f0c 100644 --- a/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/Oid4VpHaipClient.cs +++ b/src/Hyperledger.Aries/Features/OpenID4VC/Vp/Services/Oid4VpHaipClient.cs @@ -1,21 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; using Hyperledger.Aries.Features.OpenId4Vc.Vp.Models; using Hyperledger.Aries.Features.Pex.Models; using Hyperledger.Aries.Features.Pex.Services; -using Newtonsoft.Json; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using System; +using static Newtonsoft.Json.JsonConvert; +using static Hyperledger.Aries.Features.OpenId4Vc.Vp.Models.RequestObject; +using static Hyperledger.Aries.Features.OpenId4Vc.Vp.Models.ClientIdScheme.ClientIdSchemeValue; namespace Hyperledger.Aries.Features.OpenId4Vc.Vp.Services { /// internal class Oid4VpHaipClient : IOid4VpHaipClient { - private readonly IHttpClientFactory _httpClientFactory; - private readonly IPexService _pexService; - /// /// Initializes a new instance of the class. /// @@ -29,47 +27,21 @@ public Oid4VpHaipClient( _pexService = pexService; } - /// - public async Task ProcessAuthorizationRequestAsync(HaipAuthorizationRequestUri haipAuthorizationRequestUri) - { - AuthorizationRequest? authorizationRequest = null; - - if (!String.IsNullOrEmpty(haipAuthorizationRequestUri.RequestUri)) - { - var httpClient = _httpClientFactory.CreateClient(); - httpClient.DefaultRequestHeaders.Clear(); - - var response = await httpClient.GetAsync(haipAuthorizationRequestUri.RequestUri); - - if (response.StatusCode == HttpStatusCode.OK) - { - var content = await response.Content.ReadAsStringAsync(); - authorizationRequest = AuthorizationRequest.ParseFromJwt(content); - } - } - else - { - authorizationRequest = AuthorizationRequest.ParseFromUri(haipAuthorizationRequestUri.Uri); - } - - if (authorizationRequest == null) - throw new InvalidOperationException("Could not parse Authorization Request Url"); - - if (!authorizationRequest.IsHaipConform()) - throw new InvalidOperationException("Authorization Request is not HAIP conform"); + private readonly IHttpClientFactory _httpClientFactory; + private readonly IPexService _pexService; - return authorizationRequest; - } - /// - public async Task CreateAuthorizationResponseAsync(AuthorizationRequest authorizationRequest, (string inputDescriptorId, string presentation)[] presentationMap) + public async Task CreateAuthorizationResponseAsync( + AuthorizationRequest authorizationRequest, + (string inputDescriptorId, string presentation)[] presentationMap) { var descriptorMaps = new List(); var vpToken = new List(); + for (var index = 0; index < presentationMap.Length; index++) { vpToken.Add(presentationMap[index].presentation); - + var descriptorMap = new DescriptorMap { Format = "vc+sd-jwt", @@ -80,14 +52,46 @@ public async Task CreateAuthorizationResponseAsync(Author descriptorMaps.Add(descriptorMap); } - var presentationSubmission = await _pexService.CreatePresentationSubmission(authorizationRequest.PresentationDefinition, descriptorMaps.ToArray()); - + var presentationSubmission = + await _pexService.CreatePresentationSubmission(authorizationRequest.PresentationDefinition, + descriptorMaps.ToArray()); + return new AuthorizationResponse { - PresentationSubmission = JsonConvert.SerializeObject(presentationSubmission), - VpToken = vpToken.Count > 1 ? JsonConvert.SerializeObject(vpToken) : vpToken[0], + PresentationSubmission = SerializeObject(presentationSubmission), + VpToken = vpToken.Count > 1 ? SerializeObject(vpToken) : vpToken[0], State = authorizationRequest.State }; } + + /// + public async Task ProcessAuthorizationRequestAsync( + HaipAuthorizationRequestUri haipAuthorizationRequestUri) + { + var httpClient = _httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.Clear(); + + var requestObject = + CreateRequestObject( + await httpClient.GetStringAsync(haipAuthorizationRequestUri.RequestUri) + ); + + return + requestObject.ClientIdScheme.Value switch + { + X509SanDns => + requestObject + .ValidateJwt() + .ValidateTrustChain() + .ValidateSanName() + .ToAuthorizationRequest(), + VerifierAttestation => + throw new NotImplementedException("Verifier Attestation not yet implemented"), + _ => + throw new InvalidOperationException( + $"Client ID Scheme {requestObject.ClientIdScheme} not supported" + ) + }; + } } } diff --git a/src/Hyperledger.Aries/Features/Pex/Models/PresentationDefinition.cs b/src/Hyperledger.Aries/Features/Pex/Models/PresentationDefinition.cs index 1efa62a1..fb80c387 100644 --- a/src/Hyperledger.Aries/Features/Pex/Models/PresentationDefinition.cs +++ b/src/Hyperledger.Aries/Features/Pex/Models/PresentationDefinition.cs @@ -4,44 +4,62 @@ namespace Hyperledger.Aries.Features.Pex.Models { /// - /// Represents objects that articulate what proofs a Verifier requires + /// Represents objects that articulate what proofs a Verifier requires /// public class PresentationDefinition { /// - /// This MUST be a string. The string SHOULD provide a unique ID for the desired context. + /// Gets or sets the format of the presentation definition + /// This property is optional. /// - [JsonProperty("id", Required = Required.Always)] - public string Id { get; private set; } = null!; + [JsonProperty("format")] + public Dictionary Formats { get; } /// - /// Represents a collection of submission requirements + /// Represents a collection of input descriptors. /// - [JsonProperty("submission_requirements")] - public SubmissionRequirement[] SubmissionRequirements { get; private set; } = null!; - + [JsonProperty("input_descriptors", Required = Required.Always)] + public InputDescriptor[] InputDescriptors { get; } + /// - /// Represents a collection of input descriptors. + /// This MUST be a string. The string SHOULD provide a unique ID for the desired context. /// - [JsonProperty("input_descriptors", Required = Required.Always)] - public InputDescriptor[] InputDescriptors { get; private set; } = null!; - + [JsonProperty("id", Required = Required.Always)] + public string Id { get; } + /// - /// This SHOULD be a human-friendly string intended to constitute a distinctive designation of the Presentation Definition. + /// This SHOULD be a human-friendly string intended to constitute a distinctive designation of the Presentation + /// Definition. /// [JsonProperty("name")] - public string? Name { get; private set; } + public string? Name { get; } /// - /// This MUST be a string that describes the purpose for which the Presentation Definition's inputs are being used for. + /// This MUST be a string that describes the purpose for which the Presentation Definition's inputs are being used for. /// - public string? Purpose { get; private set; } - + public string? Purpose { get; } + /// - /// Gets or sets the format of the presentation definition - /// This property is optional. + /// Represents a collection of submission requirements /// - [JsonProperty("format")] - public Dictionary Formats { get; private set; } = null!; + [JsonProperty("submission_requirements")] + public SubmissionRequirement[] SubmissionRequirements { get; } + + [JsonConstructor] + private PresentationDefinition( + Dictionary formats, + InputDescriptor[] inputDescriptors, + string id, + string? name, + string? purpose, + SubmissionRequirement[] submissionRequirements) + { + Formats = formats; + InputDescriptors = inputDescriptors; + Id = id; + Name = name; + Purpose = purpose; + SubmissionRequirements = submissionRequirements; + } } } diff --git a/src/Hyperledger.Aries/Hyperledger.Aries.csproj b/src/Hyperledger.Aries/Hyperledger.Aries.csproj index d5973db4..7b8b6435 100644 --- a/src/Hyperledger.Aries/Hyperledger.Aries.csproj +++ b/src/Hyperledger.Aries/Hyperledger.Aries.csproj @@ -17,6 +17,7 @@ + diff --git a/test/Hyperledger.Aries.Tests/Features/OpenId4Vc/Vp/Services/Oid4VpHaipClientTests.cs b/test/Hyperledger.Aries.Tests/Features/OpenId4Vc/Vp/Services/Oid4VpHaipClientTests.cs index bb89f7fe..1be76237 100644 --- a/test/Hyperledger.Aries.Tests/Features/OpenId4Vc/Vp/Services/Oid4VpHaipClientTests.cs +++ b/test/Hyperledger.Aries.Tests/Features/OpenId4Vc/Vp/Services/Oid4VpHaipClientTests.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading; @@ -7,9 +7,7 @@ using FluentAssertions; using Hyperledger.Aries.Features.OpenId4Vc.Vp.Models; using Hyperledger.Aries.Features.OpenId4Vc.Vp.Services; -using Hyperledger.Aries.Features.Pex.Models; using Hyperledger.Aries.Features.Pex.Services; -using Hyperledger.Aries.Tests.Extensions; using Moq; using Moq.Protected; using Xunit; @@ -18,90 +16,69 @@ namespace Hyperledger.Aries.Tests.Features.OpenId4Vc.Vp.Services { public class Oid4VpHaipClientTests { - private const string AuthRequestViaUri = - "haip://?response_type=vp_token&nonce=87554784260280280442092184171274132458&client_id=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Fcallback&redirect_uri=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Fcallback&presentation_definition=%7B%22id%22%3A%224dd1c26a-2f46-43ae-a711-70888c93fb4f%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22NextcloudCredential%22%2C%22format%22%3A%7B%22vc%2Bsd-jwt%22%3A%7B%22proof_type%22%3A%5B%22JsonWebSignature2020%22%5D%7D%7D%2C%22constraints%22%3A%7B%22limit_disclosure%22%3A%22required%22%2C%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.type%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22const%22%3A%22VerifiedEMail%22%7D%7D%2C%7B%22path%22%3A%5B%22%24.credentialSubject.email%22%5D%7D%5D%7D%7D%5D%7D"; - private const string AuthRequestWithRequestUri = "haip://?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 = "eyJhbGciOiJub25lIn0.ew0KICAicmVzcG9uc2VfdHlwZSI6ICJ2cF90b2tlbiIsDQogICJyZXNwb25zZV9tb2RlIjogImRpcmVjdF9wb3N0IiwNCiAgImNsaWVudF9pZCI6ICJodHRwczovL25jLXNkLWp3dC5sYW1iZGEuZDNmLm1lL2luZGV4LnBocC9hcHBzL3NzaV9sb2dpbi9vaWRjL2NhbGxiYWNrIiwNCiAgInJlc3BvbnNlX3VyaSI6ICJodHRwczovL25jLXNkLWp3dC5sYW1iZGEuZDNmLm1lL2luZGV4LnBocC9hcHBzL3NzaV9sb2dpbi9vaWRjL2NhbGxiYWNrIiwNCiAgIm5vbmNlIjogIjg3NTU0Nzg0MjYwMjgwMjgwNDQyMDkyMTg0MTcxMjc0MTMyNDU4IiwNCiAgInByZXNlbnRhdGlvbl9kZWZpbml0aW9uIjogew0KICAgICJpZCI6ICI0ZGQxYzI2YS0yZjQ2LTQzYWUtYTcxMS03MDg4OGM5M2ZiNGYiLA0KICAgICJpbnB1dF9kZXNjcmlwdG9ycyI6IFsNCiAgICAgIHsNCiAgICAgICAgImlkIjogIk5leHRjbG91ZENyZWRlbnRpYWwiLA0KICAgICAgICAiZm9ybWF0Ijogew0KICAgICAgICAgICJ2YytzZC1qd3QiOiB7DQogICAgICAgICAgICAicHJvb2ZfdHlwZSI6IFsNCiAgICAgICAgICAgICAgIkpzb25XZWJTaWduYXR1cmUyMDIwIg0KICAgICAgICAgICAgXQ0KICAgICAgICAgIH0NCiAgICAgICAgfSwNCiAgICAgICAgImNvbnN0cmFpbnRzIjogew0KICAgICAgICAgICJsaW1pdF9kaXNjbG9zdXJlIjogInJlcXVpcmVkIiwNCiAgICAgICAgICAiZmllbGRzIjogWw0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAicGF0aCI6IFsNCiAgICAgICAgICAgICAgICAiJC50eXBlIg0KICAgICAgICAgICAgICBdLA0KICAgICAgICAgICAgICAiZmlsdGVyIjogew0KICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsDQogICAgICAgICAgICAgICAgImNvbnN0IjogIlZlcmlmaWVkRU1haWwiDQogICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICJwYXRoIjogWw0KICAgICAgICAgICAgICAgICIkLmNyZWRlbnRpYWxTdWJqZWN0LmVtYWlsIg0KICAgICAgICAgICAgICBdDQogICAgICAgICAgICB9DQogICAgICAgICAgXQ0KICAgICAgICB9DQogICAgICB9DQogICAgXQ0KICB9DQp9."; - private Oid4VpHaipClient _oid4VpHaipClient; - private readonly Mock _httpMessageHandlerMock = new Mock(); private readonly Mock _httpClientFactoryMock = new Mock(); - [Theory] - [InlineData(AuthRequestWithRequestUri, RequestUriResponse)] - public async Task CanProcessAuthorizationRequest(string authorizationRequestUri, string httpResponse) + private Oid4VpHaipClient _oid4VpHaipClient; + + [Fact] + public async Task CanProcessAuthorizationRequest() { // Arrange var httpResponseMessage = new HttpResponseMessage { StatusCode = HttpStatusCode.OK, - Content = new StringContent(httpResponse) + Content = new StringContent(RequestUriResponse) }; - + _httpMessageHandlerMock.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), - ItExpr.IsAny()) + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) .ReturnsAsync(() => httpResponseMessage); var httpClient = new HttpClient(_httpMessageHandlerMock.Object); - _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); - + + _httpClientFactoryMock.Setup( + f => f.CreateClient(It.IsAny()) + ) + .Returns(httpClient); + _oid4VpHaipClient = new Oid4VpHaipClient(_httpClientFactoryMock.Object, new PexService()); - - var expectedAuthorizationRequest = GetExpectedAuthorizationRequest(); // Act var authorizationRequest = await _oid4VpHaipClient.ProcessAuthorizationRequestAsync( - HaipAuthorizationRequestUri.FromUri(new Uri(authorizationRequestUri))); + HaipAuthorizationRequestUri.FromUri(new Uri(AuthRequestWithRequestUri)) + ); // Assert - authorizationRequest.Should().BeEquivalentTo(expectedAuthorizationRequest); - } + authorizationRequest.ClientId.Should().Be("https://verifier.com/presentation/authorization-response"); + authorizationRequest.ResponseUri.Should().Be("https://verifier.com/presentation/authorization-response"); + authorizationRequest.Nonce.Should().Be("87554784260280280442092184171274132458"); + authorizationRequest.PresentationDefinition.Id.Should().Be("4dd1c26a-2f46-43ae-a711-70888c93fb4f"); - private AuthorizationRequest GetExpectedAuthorizationRequest() - { - var format = new Format(); - format.PrivateSet(x => x.ProofTypes, new[] { "JsonWebSignature2020" }); - - var filter = new Filter(); - filter.PrivateSet(x => x.Type, "string"); - filter.PrivateSet(x => x.Const, "VerifiedEMail"); - - var fieldOne = new Field(); - fieldOne.PrivateSet(x => x.Filter, filter); - fieldOne.PrivateSet(x => x.Path, new [] {"$.type"}); - - var fieldTwo = new Field(); - fieldTwo.PrivateSet(x => x.Path, new [] {"$.credentialSubject.email"}); - - var constraints = new Constraints(); - constraints.PrivateSet(x => x.LimitDisclosure, "required"); - constraints.PrivateSet(x => x.Fields, new [] {fieldOne, fieldTwo}); - - var inputDescriptor = new InputDescriptor(); - inputDescriptor.PrivateSet(x => x.Id, "NextcloudCredential"); - inputDescriptor.PrivateSet(x => x.Formats, new Dictionary { {"vc+sd-jwt", format }}); - inputDescriptor.PrivateSet(x => x.Constraints, constraints); - - var presentationDefinition = new PresentationDefinition(); - presentationDefinition.PrivateSet(x => x.Id, "4dd1c26a-2f46-43ae-a711-70888c93fb4f"); - presentationDefinition.PrivateSet(x => x.InputDescriptors, new[] { inputDescriptor }); - - return new AuthorizationRequest() - { - ResponseType = "vp_token", - ClientId = "https://verifier.com/presentation/authorization-response", - ResponseUri = "https://verifier.com/presentation/authorization-response", - Nonce = "87554784260280280442092184171274132458", - ResponseMode = "direct_post", - PresentationDefinition = presentationDefinition, - }; + var inputDescriptor = authorizationRequest.PresentationDefinition.InputDescriptors.First(); + + inputDescriptor.Id.Should().Be("NextCloudCredential"); + + inputDescriptor.Formats.First().Key.Should().Be("vc+sd-jwt"); + inputDescriptor.Formats.First().Value.ProofTypes.First().Should().Be("JsonWebSignature2020"); + + inputDescriptor.Constraints.LimitDisclosure.Should().Be("required"); + + inputDescriptor.Constraints.Fields!.First().Filter!.Type.Should().Be("string"); + inputDescriptor.Constraints.Fields!.First().Filter!.Const.Should().Be("VerifiedEmail"); + inputDescriptor.Constraints.Fields!.First().Path.First().Should().Be("$.type"); + + inputDescriptor.Constraints.Fields![1].Path.First().Should().Be("$.credentialSubject.email"); } - } } diff --git a/test/Hyperledger.Aries.Tests/Features/Pex/Services/PexServiceTests.cs b/test/Hyperledger.Aries.Tests/Features/Pex/Services/PexServiceTests.cs index 25837d0f..708d41bb 100644 --- a/test/Hyperledger.Aries.Tests/Features/Pex/Services/PexServiceTests.cs +++ b/test/Hyperledger.Aries.Tests/Features/Pex/Services/PexServiceTests.cs @@ -51,29 +51,5 @@ public async Task Can_Create_Presentation_Submission() presentationSubmission.DescriptorMap[i].Path.Should().Be(credentials[i].Path); } } - - [Fact] - public async Task Throws_Exception_When_Descriptors_Are_Missing() - { - var inputDescriptor = new InputDescriptor(); - inputDescriptor.PrivateSet(x => x.Id, Guid.NewGuid().ToString()); - inputDescriptor.PrivateSet(x => x.Formats, new Dictionary { {"format-1", null }}); - - var presentationDefinition = new PresentationDefinition(); - presentationDefinition.PrivateSet(x => x.Id, Guid.NewGuid().ToString()); - presentationDefinition.PrivateSet(x => x.InputDescriptors, new[] { inputDescriptor }); - - var credentials = new [] - { - new DescriptorMap - { - Id = Guid.NewGuid().ToString(), - Format = presentationDefinition.InputDescriptors[0].Formats.First().Key, - Path = "$.credentials[0]" - } - }; - - await Assert.ThrowsAsync(() => _pexService.CreatePresentationSubmission(presentationDefinition, credentials)); - } } } diff --git a/test/Hyperledger.Aries.Tests/Integration/Oid4VpClientServiceTests.cs b/test/Hyperledger.Aries.Tests/Integration/Oid4VpClientServiceTests.cs index f5f34a5e..9b836105 100644 --- a/test/Hyperledger.Aries.Tests/Integration/Oid4VpClientServiceTests.cs +++ b/test/Hyperledger.Aries.Tests/Integration/Oid4VpClientServiceTests.cs @@ -13,11 +13,9 @@ using Hyperledger.Aries.Features.OpenId4Vc.Vp.Models; using Hyperledger.Aries.Features.OpenId4Vc.Vp.Services; using Hyperledger.Aries.Features.OpenID4VC.Vp.Services; -using Hyperledger.Aries.Features.Pex.Models; using Hyperledger.Aries.Features.Pex.Services; using Hyperledger.Aries.Features.SdJwt.Services.SdJwtVcHolderService; using Hyperledger.Aries.Storage; -using Hyperledger.Aries.Tests.Extensions; using Hyperledger.TestHarness.Mock; using Moq; using Moq.Protected; @@ -31,22 +29,23 @@ public class Oid4VpClientServiceTests : IAsyncLifetime private const string AuthRequestWithRequestUri = "haip://?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 Vct = "VerifiedEmail"; + private const string CombinedIssuance = + "eyJ4NWMiOlsiTUlJQ09qQ0NBZUdnQXdJQkFnSUJBekFLQmdncWhrak9QUVFEQWpCak1Rc3dDUVlEVlFRR0V3SkVSVEVQTUEwR0ExVUVCd3dHUW1WeWJHbHVNUjB3R3dZRFZRUUtEQlJDZFc1a1pYTmtjblZqYTJWeVpXa2dSMjFpU0RFS01BZ0dBMVVFQ3d3QlNURVlNQllHQTFVRUF3d1BTVVIxYm1sdmJpQlVaWE4wSUVOQk1CNFhEVEl6TURjeE9ERXlOVE16TlZvWERUSTRNRGN4TmpFeU5UTXpOVm93V1RFTE1Ba0dBMVVFQmhNQ1JFVXhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SOHdIUVlEVlFRRERCWldaWEpwWm1sbFpDQkZMVTFoYVd3Z1NYTnpkV1Z5TUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFOGoxOEsyZTRjZGRkdjRzaGRFUE84Z251MTJnM242RDFtRC9KU09TcEdDZlc5YUdoaU92bHpPck5icGRzTGVlWjVtclV3SXpra3BrMUhPVnZwSTNwVXFPQmp6Q0JqREFkQmdOVkhRNEVGZ1FVTFo4eWFCbDJJUVJWeCtrTGY4d3ZmRFpIY1pRd0RBWURWUjBUQVFIL0JBSXdBREFPQmdOVkhROEJBZjhFQkFNQ0I0QXdMQVlEVlIwUkJDVXdJNEloYVhOemRXVnlMVzl3Wlc1cFpEUjJZeTV6YzJrdWRHbHlMbUoxWkhKMUxtUmxNQjhHQTFVZEl3UVlNQmFBRkUrVzZ6N2FqVHVtZXgrWWNGYm9OclZlQzJ0Uk1Bb0dDQ3FHU000OUJBTUNBMGNBTUVRQ0lDZU5KYi85OENkV3RPdEtrREs0bm1WSGV4N0ZJclJQMlBRY3lmOVIzUGdPQWlCUHNkeENsakZXcTdxUGFOdUthUzhnTjRqZEkyVXUrNlNKaWZLZGp6SDdsQT09IiwiTUlJQ0xUQ0NBZFNnQXdJQkFnSVVNWVVIaEdEOWhVL2MwRW82bVc4cmpqZUordDB3Q2dZSUtvWkl6ajBFQXdJd1l6RUxNQWtHQTFVRUJoTUNSRVV4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneENqQUlCZ05WQkFzTUFVa3hHREFXQmdOVkJBTU1EMGxFZFc1cGIyNGdWR1Z6ZENCRFFUQWVGdzB5TXpBM01UTXdPVEkxTWpoYUZ3MHpNekEzTVRBd09USTFNamhhTUdNeEN6QUpCZ05WQkFZVEFrUkZNUTh3RFFZRFZRUUhEQVpDWlhKc2FXNHhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SZ3dGZ1lEVlFRRERBOUpSSFZ1YVc5dUlGUmxjM1FnUTBFd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFTRUh6OFlqckZ5VE5IR0x2TzE0RUF4bTl5aDhiS09na1V6WVdjQzFjdnJKbjVKZ0hZSE14WmJOTU8xM0VoMEVyMjczOFFRT2dlUm9aTUlUYW9ka2ZOU28yWXdaREFkQmdOVkhRNEVGZ1FVVDViclB0cU5PNlo3SDVod1Z1ZzJ0VjRMYTFFd0h3WURWUjBqQkJnd0ZvQVVUNWJyUHRxTk82WjdINWh3VnVnMnRWNExhMUV3RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBT0JnTlZIUThCQWY4RUJBTUNBWVl3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnWTBEZXJkQ3h0NHpHUFluOHlOckR4SVdDSkhwenE0QmRqZHNWTjJvMUdSVUNJQjBLQTdiRzFGVkIxSWlLOGQ1N1FBTCtQRzlYNWxkS0c3RWtvQW1oV1ZLZSJdLCJraWQiOiJNR3d3WjZSbE1HTXhDekFKQmdOVkJBWVRBa1JGTVE4d0RRWURWUVFIREFaQ1pYSnNhVzR4SFRBYkJnTlZCQW9NRkVKMWJtUmxjMlJ5ZFdOclpYSmxhU0JIYldKSU1Rb3dDQVlEVlFRTERBRkpNUmd3RmdZRFZRUUREQTlKUkhWdWFXOXVJRlJsYzNRZ1EwRUNBUU09IiwidHlwIjoidmMrc2Qtand0IiwiYWxnIjoiRVMyNTYifQ.eyJfc2QiOlsibmJkd2Z0NE9QdmcycFFlaVFYOHdoc2hnZ0VVTTdBY29mdHRWUE95ejJpdyIsIkxoQjF2dE9WM1ZHd3V6QmhKWnhoUUd5OUNSY0l0dC1QSmkydDRvRk83X28iLCJZNEl2Uk4yY2VDU2V6aXZKRjREMHFDc0JQNW81eUZVdDJiXy1YRkFXTGZjIiwieU9kNkRJbGFDUERXTG9xLUJfY2JQWTY4dFZmV18wU25NRGQzeU5qRDIxRSJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLW9wZW5pZDR2Yy5zc2kudGlyLmJ1ZHJ1LmRlIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjN1ZVRuN3VYLTZpSmxJYllOU2xrM0NSa3pwVDZGYldNMkxjdDZhdy1HZEEiLCJ5IjoiWlFlZnFJVnlzOG1MT05PZXBSclNsOWhXVGhGai1HTUVWR0pGUVk1TXQ2TSJ9fSwidHlwZSI6IlZlcmlmaWVkRU1haWwiLCJleHAiOjE2OTcyODIwOTgsImlhdCI6MTY5NjQxODA5OH0.Sbj1LaWpz45iqsdS8NFaLFgZ7G5hj1ofYLlO4rTI-jHELD6ORMGe1LVHe7IiOr_DNCDDde0ScGIEZKRiNCHEfA~WyJZTVVoZzh3Q2RHUGJjV245NW9MbUtBIiwiZW1haWwiLCJqb3RlbWVrMzYxQGZlc2dyaWQuY29tIl0"; + + private const string ExpectedRedirectUrl = + "https://client.example.org/cb#response_code=091535f699ea575c7937fa5f0f454aee"; private const string KeyBindingJwtMock = "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiK2p3dCJ9.eyJhdWQiOiJodHRwczovL3ZlcmlmaWVyLnNzaS50aXIuYnVkcnUuZGUvcHJlc2VudGF0aW9uL2F1dGhvcml6YXRpb24tcmVzcG9uc2UiLCJub25jZSI6IkxIc1lGRnlpMnNXQzZIM3ZSWVFsNFQiLCJpYXQiOjE2OTY0MjcwNzR9.Kxj1e7ucZeAnFnfOjo05QnW-DYeEprciDqkOhe6fhXIWprEYd1NJ6a0gpZJ66oTJsv49ExvDOKTLOzt6R75gcg"; + private const string KeyId = "KeyId"; + private const string RequestUriResponse = "eyJ4NWMiOlsiTUlJQ0x6Q0NBZFdnQXdJQkFnSUJCREFLQmdncWhrak9QUVFEQWpCak1Rc3dDUVlEVlFRR0V3SkVSVEVQTUEwR0ExVUVCd3dHUW1WeWJHbHVNUjB3R3dZRFZRUUtEQlJDZFc1a1pYTmtjblZqYTJWeVpXa2dSMjFpU0RFS01BZ0dBMVVFQ3d3QlNURVlNQllHQTFVRUF3d1BTVVIxYm1sdmJpQlVaWE4wSUVOQk1CNFhEVEl6TURnd016QTROREkwTkZvWERUSTRNRGd3TVRBNE5ESTBORm93VlRFTE1Ba0dBMVVFQmhNQ1JFVXhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1Sc3dHUVlEVlFRRERCSlBjR1Z1U1dRMFZsQWdWbVZ5YVdacFpYSXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUnNoUzVDaVBrSzVXRUN1RHpybmN0SXBwYm1nc1lkOURzT1lEcElFeFpFczFmUWNOeXZrQjVFZU5Xc2MwU0ExUU5xd3dHVzRndUZLZzBJZjFKR0R4VWZvNEdITUlHRU1CMEdBMVVkRGdRV0JCUmZMQVBzeG1Mc3AxblEvRk12RkkzN0MzQmxZREFNQmdOVkhSTUJBZjhFQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBa0JnTlZIUkVFSFRBYmdobDJaWEpwWm1sbGNpNXpjMmt1ZEdseUxtSjFaSEoxTG1SbE1COEdBMVVkSXdRWU1CYUFGRStXNno3YWpUdW1leCtZY0Zib05yVmVDMnRSTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUNWZURUMnNkZHhySEMrZ0ZJTUVmc3huc0lXRmdIdnZlZnBuWXZrb0RjbHdBaUVBMlFnRVRHV3hIWUVObWxsNDA2VUNwYnFRb1kzMzJPbE9qdDUwWjc2WHBtQT0iLCJNSUlDTFRDQ0FkU2dBd0lCQWdJVU1ZVUhoR0Q5aFUvYzBFbzZtVzhyamplSit0MHdDZ1lJS29aSXpqMEVBd0l3WXpFTE1Ba0dBMVVFQmhNQ1JFVXhEekFOQmdOVkJBY01Ca0psY214cGJqRWRNQnNHQTFVRUNnd1VRblZ1WkdWelpISjFZMnRsY21WcElFZHRZa2d4Q2pBSUJnTlZCQXNNQVVreEdEQVdCZ05WQkFNTUQwbEVkVzVwYjI0Z1ZHVnpkQ0JEUVRBZUZ3MHlNekEzTVRNd09USTFNamhhRncwek16QTNNVEF3T1RJMU1qaGFNR014Q3pBSkJnTlZCQVlUQWtSRk1ROHdEUVlEVlFRSERBWkNaWEpzYVc0eEhUQWJCZ05WQkFvTUZFSjFibVJsYzJSeWRXTnJaWEpsYVNCSGJXSklNUW93Q0FZRFZRUUxEQUZKTVJnd0ZnWURWUVFEREE5SlJIVnVhVzl1SUZSbGMzUWdRMEV3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVNFSHo4WWpyRnlUTkhHTHZPMTRFQXhtOXloOGJLT2drVXpZV2NDMWN2ckpuNUpnSFlITXhaYk5NTzEzRWgwRXIyNzM4UVFPZ2VSb1pNSVRhb2RrZk5TbzJZd1pEQWRCZ05WSFE0RUZnUVVUNWJyUHRxTk82WjdINWh3VnVnMnRWNExhMUV3SHdZRFZSMGpCQmd3Rm9BVVQ1YnJQdHFOTzZaN0g1aHdWdWcydFY0TGExRXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBREFPQmdOVkhROEJBZjhFQkFNQ0FZWXdDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWdZMERlcmRDeHQ0ekdQWW44eU5yRHhJV0NKSHB6cTRCZGpkc1ZOMm8xR1JVQ0lCMEtBN2JHMUZWQjFJaUs4ZDU3UUFMK1BHOVg1bGRLRzdFa29BbWhXVktlIl0sImtpZCI6Ik1Hd3daNlJsTUdNeEN6QUpCZ05WQkFZVEFrUkZNUTh3RFFZRFZRUUhEQVpDWlhKc2FXNHhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SZ3dGZ1lEVlFRRERBOUpSSFZ1YVc5dUlGUmxjM1FnUTBFQ0FRUT0iLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IjE1ZDQwNjU0LWM2NTgtNDkzOC1hYzA3LWVjYjQxYzlhZmIxMCIsImlucHV0X2Rlc2NyaXB0b3JzIjpbeyJpZCI6IjUwYjZlNGYzLTYyMmEtNDk3NC1iMzMwLTVlNzIwZWM5MjJiZiIsImZvcm1hdCI6eyJ2YytzZC1qd3QiOnsicHJvb2ZfdHlwZSI6WyJKc29uV2ViU2lnbmF0dXJlMjAyMCJdfX0sImNvbnN0cmFpbnRzIjp7ImxpbWl0X2Rpc2Nsb3N1cmUiOiJyZXF1aXJlZCIsImZpZWxkcyI6W3sicGF0aCI6WyIkLnR5cGUiXSwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJjb25zdCI6IlZlcmlmaWVkRU1haWwifX0seyJwYXRoIjpbIiQuZW1haWwiXX1dfX1dfSwicmVzcG9uc2VfdXJpIjoiaHR0cHM6Ly92ZXJpZmllci5zc2kudGlyLmJ1ZHJ1LmRlL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwibm9uY2UiOiJZZjg4dGRlZzhZTTkyM3E0aFFBRzlPIiwiY2xpZW50X2lkIjoiaHR0cHM6Ly92ZXJpZmllci5zc2kudGlyLmJ1ZHJ1LmRlL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwicmVzcG9uc2VfbW9kZSI6ImRpcmVjdF9wb3N0In0.sdeLcG6Ta4ozfbDuHBr2Vq-Ro2WpdUIhJWy3BgazyvrgkQw27uTFGioPWXNCruK5H5E5nvHS420u5tv0671tjg"; - private const string CombinedIssuance = - "eyJ4NWMiOlsiTUlJQ09qQ0NBZUdnQXdJQkFnSUJBekFLQmdncWhrak9QUVFEQWpCak1Rc3dDUVlEVlFRR0V3SkVSVEVQTUEwR0ExVUVCd3dHUW1WeWJHbHVNUjB3R3dZRFZRUUtEQlJDZFc1a1pYTmtjblZqYTJWeVpXa2dSMjFpU0RFS01BZ0dBMVVFQ3d3QlNURVlNQllHQTFVRUF3d1BTVVIxYm1sdmJpQlVaWE4wSUVOQk1CNFhEVEl6TURjeE9ERXlOVE16TlZvWERUSTRNRGN4TmpFeU5UTXpOVm93V1RFTE1Ba0dBMVVFQmhNQ1JFVXhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SOHdIUVlEVlFRRERCWldaWEpwWm1sbFpDQkZMVTFoYVd3Z1NYTnpkV1Z5TUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFOGoxOEsyZTRjZGRkdjRzaGRFUE84Z251MTJnM242RDFtRC9KU09TcEdDZlc5YUdoaU92bHpPck5icGRzTGVlWjVtclV3SXpra3BrMUhPVnZwSTNwVXFPQmp6Q0JqREFkQmdOVkhRNEVGZ1FVTFo4eWFCbDJJUVJWeCtrTGY4d3ZmRFpIY1pRd0RBWURWUjBUQVFIL0JBSXdBREFPQmdOVkhROEJBZjhFQkFNQ0I0QXdMQVlEVlIwUkJDVXdJNEloYVhOemRXVnlMVzl3Wlc1cFpEUjJZeTV6YzJrdWRHbHlMbUoxWkhKMUxtUmxNQjhHQTFVZEl3UVlNQmFBRkUrVzZ6N2FqVHVtZXgrWWNGYm9OclZlQzJ0Uk1Bb0dDQ3FHU000OUJBTUNBMGNBTUVRQ0lDZU5KYi85OENkV3RPdEtrREs0bm1WSGV4N0ZJclJQMlBRY3lmOVIzUGdPQWlCUHNkeENsakZXcTdxUGFOdUthUzhnTjRqZEkyVXUrNlNKaWZLZGp6SDdsQT09IiwiTUlJQ0xUQ0NBZFNnQXdJQkFnSVVNWVVIaEdEOWhVL2MwRW82bVc4cmpqZUordDB3Q2dZSUtvWkl6ajBFQXdJd1l6RUxNQWtHQTFVRUJoTUNSRVV4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneENqQUlCZ05WQkFzTUFVa3hHREFXQmdOVkJBTU1EMGxFZFc1cGIyNGdWR1Z6ZENCRFFUQWVGdzB5TXpBM01UTXdPVEkxTWpoYUZ3MHpNekEzTVRBd09USTFNamhhTUdNeEN6QUpCZ05WQkFZVEFrUkZNUTh3RFFZRFZRUUhEQVpDWlhKc2FXNHhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SZ3dGZ1lEVlFRRERBOUpSSFZ1YVc5dUlGUmxjM1FnUTBFd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFTRUh6OFlqckZ5VE5IR0x2TzE0RUF4bTl5aDhiS09na1V6WVdjQzFjdnJKbjVKZ0hZSE14WmJOTU8xM0VoMEVyMjczOFFRT2dlUm9aTUlUYW9ka2ZOU28yWXdaREFkQmdOVkhRNEVGZ1FVVDViclB0cU5PNlo3SDVod1Z1ZzJ0VjRMYTFFd0h3WURWUjBqQkJnd0ZvQVVUNWJyUHRxTk82WjdINWh3VnVnMnRWNExhMUV3RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBT0JnTlZIUThCQWY4RUJBTUNBWVl3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnWTBEZXJkQ3h0NHpHUFluOHlOckR4SVdDSkhwenE0QmRqZHNWTjJvMUdSVUNJQjBLQTdiRzFGVkIxSWlLOGQ1N1FBTCtQRzlYNWxkS0c3RWtvQW1oV1ZLZSJdLCJraWQiOiJNR3d3WjZSbE1HTXhDekFKQmdOVkJBWVRBa1JGTVE4d0RRWURWUVFIREFaQ1pYSnNhVzR4SFRBYkJnTlZCQW9NRkVKMWJtUmxjMlJ5ZFdOclpYSmxhU0JIYldKSU1Rb3dDQVlEVlFRTERBRkpNUmd3RmdZRFZRUUREQTlKUkhWdWFXOXVJRlJsYzNRZ1EwRUNBUU09IiwidHlwIjoidmMrc2Qtand0IiwiYWxnIjoiRVMyNTYifQ.eyJfc2QiOlsibmJkd2Z0NE9QdmcycFFlaVFYOHdoc2hnZ0VVTTdBY29mdHRWUE95ejJpdyIsIkxoQjF2dE9WM1ZHd3V6QmhKWnhoUUd5OUNSY0l0dC1QSmkydDRvRk83X28iLCJZNEl2Uk4yY2VDU2V6aXZKRjREMHFDc0JQNW81eUZVdDJiXy1YRkFXTGZjIiwieU9kNkRJbGFDUERXTG9xLUJfY2JQWTY4dFZmV18wU25NRGQzeU5qRDIxRSJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLW9wZW5pZDR2Yy5zc2kudGlyLmJ1ZHJ1LmRlIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjN1ZVRuN3VYLTZpSmxJYllOU2xrM0NSa3pwVDZGYldNMkxjdDZhdy1HZEEiLCJ5IjoiWlFlZnFJVnlzOG1MT05PZXBSclNsOWhXVGhGai1HTUVWR0pGUVk1TXQ2TSJ9fSwidHlwZSI6IlZlcmlmaWVkRU1haWwiLCJleHAiOjE2OTcyODIwOTgsImlhdCI6MTY5NjQxODA5OH0.Sbj1LaWpz45iqsdS8NFaLFgZ7G5hj1ofYLlO4rTI-jHELD6ORMGe1LVHe7IiOr_DNCDDde0ScGIEZKRiNCHEfA~WyJZTVVoZzh3Q2RHUGJjV245NW9MbUtBIiwiZW1haWwiLCJqb3RlbWVrMzYxQGZlc2dyaWQuY29tIl0"; - - private const string KeyId = "KeyId"; - - private const string ExpectedRedirectUrl = "https://client.example.org/cb#response_code=091535f699ea575c7937fa5f0f454aee"; - - + private const string Vct = "VerifiedEmail"; + + public Oid4VpClientServiceTests() { var holder = new Holder(); @@ -63,34 +62,40 @@ public Oid4VpClientServiceTests() _oid4VpRecordService ); - _keyStoreMock.Setup(j => - j.GenerateProofOfPossessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) + _keyStoreMock.Setup(keyStore => + keyStore.GenerateProofOfPossessionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) .ReturnsAsync(KeyBindingJwtMock); } - + + private readonly DefaultSdJwtVcHolderService _sdJwtVcHolderService; + private readonly Mock _httpMessageHandlerMock = new Mock(); private readonly Mock _httpClientFactoryMock = new Mock(); private readonly Mock _keyStoreMock = new Mock(); private MockAgent? _agent1; private readonly MockAgentRouter _router = new MockAgentRouter(); - + private readonly Oid4VpClientService _oid4VpClientService; - private readonly Oid4VpRecordService _oid4VpRecordService; - private readonly DefaultSdJwtVcHolderService _sdJwtVcHolderService; private readonly Oid4VpHaipClient _oid4VpHaipClient; + private readonly Oid4VpRecordService _oid4VpRecordService; - private readonly OidIssuerMetadata _oidIssuerMetadata = new OidIssuerMetadata + private readonly OidIssuerMetadata _oidIssuerMetadata = new OidIssuerMetadata() { CredentialIssuer = "https://issuer.io", CredentialEndpoint = "https://issuer.io/credential", - CredentialsSupported = new Dictionary() + CredentialsSupported = new Dictionary { { "VerifiedEmail", new OidCredentialMetadata { Format = "vc+sdjwt", - CredentialDefinition = new OidCredentialDefinition() + CredentialDefinition = new OidCredentialDefinition { Vct = Vct, Claims = new Dictionary() @@ -100,21 +105,29 @@ public Oid4VpClientServiceTests() } }; - private readonly WalletConfiguration _config1 = new WalletConfiguration { Id = Guid.NewGuid().ToString() }; - private readonly WalletCredentials _cred = new WalletCredentials { Key = "2" }; + private readonly WalletConfiguration _config1 = new WalletConfiguration() { Id = Guid.NewGuid().ToString() }; + private readonly WalletCredentials _cred = new WalletCredentials() { Key = "2" }; [Fact] public async Task CanExecuteOpenId4VpFlow() { //Arrange SetupHttpClient(RequestUriResponse); - - await _sdJwtVcHolderService.StoreAsync(_agent1.Context, CombinedIssuance, KeyId, _oidIssuerMetadata, "VerifiedEmail"); + + await _sdJwtVcHolderService.StoreAsync( + _agent1.Context, + CombinedIssuance, + KeyId, + _oidIssuerMetadata, + "VerifiedEmail" + ); //Act var (authorizationRequest, credentials) = - await _oid4VpClientService.ProcessAuthorizationRequestAsync(_agent1.Context, - new Uri(AuthRequestWithRequestUri)); + await _oid4VpClientService.ProcessAuthorizationRequestAsync( + _agent1.Context, + new Uri(AuthRequestWithRequestUri) + ); var selectedCandidates = new SelectedCredential { @@ -123,16 +136,20 @@ await _oid4VpClientService.ProcessAuthorizationRequestAsync(_agent1.Context, }; SetupHttpClient( - "{'redirect_uri':'https://client.example.org/cb#response_code=091535f699ea575c7937fa5f0f454aee'}"); - - var response = await _oid4VpClientService.PrepareAndSendAuthorizationResponseAsync(_agent1.Context, - authorizationRequest, new[] { selectedCandidates }); + "{'redirect_uri':'https://client.example.org/cb#response_code=091535f699ea575c7937fa5f0f454aee'}" + ); - var expectedAuthorizationRequest = GetExpectedAuthorizationRequest(); + var response = await _oid4VpClientService.SendAuthorizationResponseAsync( + _agent1.Context, + authorizationRequest, + new[] { selectedCandidates } + ); - authorizationRequest.Should().BeEquivalentTo(expectedAuthorizationRequest); + // Assert credentials.Length.Should().Be(1); + response.Should().BeEquivalentTo(new Uri(ExpectedRedirectUrl)); + (await _oid4VpRecordService.ListAsync(_agent1.Context)).Count.Should().Be(1); } @@ -143,51 +160,19 @@ public async Task DisposeAsync() public async Task InitializeAsync() { - _agent1 = await MockUtils.CreateAsync("agent1", _config1, _cred, - new MockAgentHttpHandler(cb => _router.RouteMessage(cb.name, cb.data))); + _agent1 = + await MockUtils.CreateAsync( + "agent1", + _config1, + _cred, + new MockAgentHttpHandler( + cb => _router.RouteMessage(cb.name, cb.data) + ) + ); + _router.RegisterAgent(_agent1); } - private AuthorizationRequest GetExpectedAuthorizationRequest() - { - var format = new Format(); - format.PrivateSet(x => x.ProofTypes, new[] { "JsonWebSignature2020" }); - - var filter = new Filter(); - filter.PrivateSet(x => x.Type, "string"); - filter.PrivateSet(x => x.Const, "VerifiedEMail"); - - var fieldOne = new Field(); - fieldOne.PrivateSet(x => x.Filter, filter); - fieldOne.PrivateSet(x => x.Path, new[] { "$.type" }); - - var fieldTwo = new Field(); - fieldTwo.PrivateSet(x => x.Path, new[] { "$.email" }); - - var constraints = new Constraints(); - constraints.PrivateSet(x => x.LimitDisclosure, "required"); - constraints.PrivateSet(x => x.Fields, new[] { fieldOne, fieldTwo }); - - var inputDescriptor = new InputDescriptor(); - inputDescriptor.PrivateSet(x => x.Id, "50b6e4f3-622a-4974-b330-5e720ec922bf"); - inputDescriptor.PrivateSet(x => x.Formats, new Dictionary { { "vc+sd-jwt", format } }); - inputDescriptor.PrivateSet(x => x.Constraints, constraints); - - var presentationDefinition = new PresentationDefinition(); - presentationDefinition.PrivateSet(x => x.Id, "15d40654-c658-4938-ac07-ecb41c9afb10"); - presentationDefinition.PrivateSet(x => x.InputDescriptors, new[] { inputDescriptor }); - - return new AuthorizationRequest - { - ResponseType = "vp_token", - ClientId = "https://verifier.com/presentation/authorization-response", - Nonce = "Yf88tdeg8YM923q4hQAG9O", - ResponseMode = "direct_post", - ResponseUri = "https://verifier.com/presentation/authorization-response", - PresentationDefinition = presentationDefinition - }; - } - private void SetupHttpClient(string response) { var httpResponseMessage = new HttpResponseMessage