Skip to content

Commit b47bb75

Browse files
authored
Dc api validate origin (#473)
* DC-API: Validate origin Signed-off-by: Kevin <[email protected]> * DC-API: refactor Signed-off-by: Kevin <[email protected]> --------- Signed-off-by: Kevin <[email protected]>
1 parent daa1991 commit b47bb75

File tree

7 files changed

+142
-41
lines changed

7 files changed

+142
-41
lines changed

src/WalletFramework.Oid4Vc/Oid4Vp/DcApi/Models/DcApiRequestBatch.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using LanguageExt;
12
using Newtonsoft.Json.Linq;
23
using WalletFramework.Core.Functional;
34
using WalletFramework.Core.Functional.Errors;
@@ -24,7 +25,7 @@ private DcApiRequestBatch(DcApiRequestItem[] requests)
2425

2526
private static DcApiRequestBatch Create(DcApiRequestItem[] requests) => new(requests);
2627

27-
public static Validation<DcApiRequestBatch> From(string requestBatchJson)
28+
public static Validation<DcApiRequestBatch> From(string requestBatchJson, Option<Origin> origin)
2829
{
2930
if (string.IsNullOrWhiteSpace(requestBatchJson))
3031
{
@@ -41,10 +42,10 @@ public static Validation<DcApiRequestBatch> From(string requestBatchJson)
4142
return new InvalidJsonError(requestBatchJson, e).ToInvalid<DcApiRequestBatch>();
4243
}
4344

44-
return From(jObject);
45+
return From(jObject, origin);
4546
}
4647

47-
public static Validation<DcApiRequestBatch> From(JObject requestBatchJson)
48+
public static Validation<DcApiRequestBatch> From(JObject requestBatchJson, Option<Origin> origin)
4849
{
4950
var requestsValidation =
5051
from jToken in requestBatchJson.GetByKey("requests")
@@ -53,7 +54,7 @@ from items in jArray.TraverseAll(token =>
5354
{
5455
return
5556
from jObject in token.ToJObject()
56-
from item in DcApiRequestItem.ValidDcApiRequestItem(jObject)
57+
from item in DcApiRequestItem.ValidDcApiRequestItem(jObject, origin)
5758
select item;
5859
})
5960
select items.ToArray();

src/WalletFramework.Oid4Vc/Oid4Vp/DcApi/Models/DcApiRequestItem.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,26 @@ public record DcApiRequestItem
1717
/// <summary>
1818
/// Gets the data. Contains the actual DcApiRequest.
1919
/// </summary>
20-
public AuthorizationRequest Data { get; }
20+
public AuthorizationRequest Data { get; init; }
2121

2222
/// <summary>
2323
/// Gets the protocol. Specifies the protocol used for this request.
2424
/// </summary>
2525
public string Protocol { get; }
2626

27-
public Option<Origin> Origin { get; init; } = Option<Origin>.None;
27+
public Option<Origin> Origin { get; } = Option<Origin>.None;
2828

2929
private DcApiRequestItem(
3030
AuthorizationRequest data,
31-
string protocol)
31+
string protocol,
32+
Option<Origin> origin)
3233
{
3334
Data = data;
3435
Protocol = protocol;
36+
Origin = origin;
3537
}
3638

37-
public static Validation<DcApiRequestItem> ValidDcApiRequestItem(JObject requestItemJson)
39+
public static Validation<DcApiRequestItem> ValidDcApiRequestItem(JObject requestItemJson, Option<Origin> origin)
3840
{
3941
var protocolValidation = requestItemJson
4042
.GetByKey("protocol")
@@ -53,17 +55,19 @@ public static Validation<DcApiRequestItem> ValidDcApiRequestItem(JObject request
5355
.OnSuccess(jObject =>
5456
{
5557
var protocol = protocolValidation.Fallback(DcApiConstants.UnsignedProtocol);
56-
return ProcessAuthRequest(jObject, protocol);
58+
return ProcessAuthRequest(jObject, protocol, origin);
5759
});
5860

5961
return Valid(Create)
6062
.Apply(dataValidation)
61-
.Apply(protocolValidation);
63+
.Apply(protocolValidation)
64+
.Apply(origin);
6265
}
6366

6467
private static DcApiRequestItem Create(
6568
AuthorizationRequest data,
66-
string protocol) => new(data, protocol);
69+
string protocol,
70+
Option<Origin> origin) => new(data, protocol, origin);
6771

6872
private static Validation<AuthorizationRequest> LiftRequest(
6973
Validation<AuthorizationRequestCancellation, AuthorizationRequest> validation)
@@ -78,7 +82,10 @@ private static Validation<AuthorizationRequest> LiftRequest(
7882
);
7983
}
8084

81-
private static Validation<AuthorizationRequest> ProcessAuthRequest(JObject jObject, string protocol)
85+
private static Validation<AuthorizationRequest> ProcessAuthRequest(
86+
JObject jObject,
87+
string protocol,
88+
Option<Origin> origin)
8289
{
8390
switch (protocol)
8491
{
@@ -90,6 +97,7 @@ private static Validation<AuthorizationRequest> ProcessAuthRequest(JObject jObje
9097
var jToken = jObject.GetByKey("request").UnwrapOrThrow();
9198
var result =
9299
from requestObject in RequestObject.FromStr(jToken.ToString(), Option<string>.None)
100+
from _ in requestObject.ValidateOrigin(origin)
93101
select requestObject.ToAuthorizationRequest();
94102
return LiftRequest(result);
95103
default:
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace WalletFramework.Oid4Vc.Oid4Vp.Errors;
2+
3+
public record OriginMismatchError : InvalidRequestError
4+
{
5+
public OriginMismatchError(string message) : base(message)
6+
{
7+
}
8+
9+
public OriginMismatchError(string Message, Exception Exception)
10+
: base(Message, Exception)
11+
{
12+
}
13+
}
14+

src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequest.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using OneOf;
66
using WalletFramework.Core.Functional;
77
using WalletFramework.Core.Json;
8+
using WalletFramework.Oid4Vc.Oid4Vp.DcApi.Models;
89
using WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;
910
using WalletFramework.Oid4Vc.Oid4Vp.Errors;
1011
using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models;
@@ -101,6 +102,10 @@ public record AuthorizationRequest
101102
[JsonProperty("verifier_attestations")]
102103
[JsonConverter(typeof(VerifierAttestationsConverter))]
103104
public VerifierAttestation[]? VerifierAttestations { get; }
105+
106+
[JsonProperty("expected_origins")]
107+
[JsonConverter(typeof(ExpectedOriginsConverter))]
108+
public Origin[]? ExpectedOrigins { get; }
104109

105110
/// <summary>
106111
/// The X509 certificate of the verifier, this property is only set when ClientIDScheme is X509SanDNS.
@@ -134,7 +139,8 @@ private AuthorizationRequest(
134139
string? clientMetadataUri,
135140
string? scope,
136141
string? state,
137-
VerifierAttestation[] verifierAttestations)
142+
VerifierAttestation[] verifierAttestations,
143+
Origin[]? expectedOrigins)
138144
{
139145
if (clientId is not null)
140146
{
@@ -161,6 +167,7 @@ private AuthorizationRequest(
161167
Scope = scope;
162168
State = state;
163169
VerifierAttestations = verifierAttestations;
170+
ExpectedOrigins = expectedOrigins;
164171
}
165172

166173
/// <summary>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using Newtonsoft.Json;
2+
using Newtonsoft.Json.Linq;
3+
using WalletFramework.Oid4Vc.Oid4Vp.DcApi.Models;
4+
5+
namespace WalletFramework.Oid4Vc.Oid4Vp.Models;
6+
7+
public class ExpectedOriginsConverter : JsonConverter<Origin[]>
8+
{
9+
public override bool CanRead => true;
10+
11+
public override bool CanWrite => false;
12+
13+
public override Origin[]? ReadJson(JsonReader reader, Type objectType,
14+
Origin[]? existingValue,
15+
bool hasExistingValue, JsonSerializer serializer)
16+
{
17+
try
18+
{
19+
var jArray = JArray.Load(reader);
20+
var origins = jArray
21+
.Select(token => token.ToString())
22+
.Where(origin => !string.IsNullOrEmpty(origin))
23+
.Select(origin => new Origin(origin))
24+
.ToArray();
25+
return origins;
26+
}
27+
catch (Exception)
28+
{
29+
return null;
30+
}
31+
}
32+
33+
public override void WriteJson(JsonWriter writer, Origin[]? value, JsonSerializer serializer) =>
34+
throw new NotImplementedException();
35+
}

src/WalletFramework.Oid4Vc/Oid4Vp/Models/RequestObject.cs

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
using System.Collections;
12
using System.IdentityModel.Tokens.Jwt;
23
using System.Security.Cryptography;
34
using System.Security.Cryptography.X509Certificates;
45
using LanguageExt;
6+
using Newtonsoft.Json.Linq;
57
using Org.BouncyCastle.X509;
68
using WalletFramework.Core.Functional;
79
using WalletFramework.Core.X509;
10+
using WalletFramework.Oid4Vc.Oid4Vp.DcApi.Models;
811
using WalletFramework.Oid4Vc.Oid4Vp.Errors;
912
using WalletFramework.Oid4Vc.Oid4Vp.Extensions;
13+
using static WalletFramework.Core.Functional.ValidationFun;
1014
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
1115
using static WalletFramework.Oid4Vc.Oid4Vp.Models.AuthorizationRequest;
1216
using static Newtonsoft.Json.Linq.JArray;
@@ -24,12 +28,12 @@ public readonly struct RequestObject
2428
/// <summary>
2529
/// The client ID scheme used to obtain and validate metadata of the verifier.
2630
/// </summary>
27-
public ClientIdScheme ClientIdScheme => AuthorizationRequest.ClientIdScheme;
31+
public ClientIdScheme ClientIdScheme => AuthorizationRequest.ClientIdScheme!;
2832

2933
/// <summary>
3034
/// The client ID of the verifier.
3135
/// </summary>
32-
public string ClientId => AuthorizationRequest.ClientId;
36+
public string ClientId => AuthorizationRequest.ClientId!;
3337

3438
private AuthorizationRequest AuthorizationRequest { get; init; }
3539

@@ -66,19 +70,20 @@ public static Validation<AuthorizationRequestCancellation, RequestObject> FromSt
6670
return new AuthorizationRequestCancellation(Option<Uri>.None, [error]);
6771
}
6872

69-
walletNonce.IfSome(nonce =>
73+
walletNonce.IfSome(nonce =>
7074
{
7175
if (jwt.Payload.TryGetValue("wallet_nonce", out var nonceValue))
7276
{
7377
if (nonceValue.ToString() != nonce)
74-
throw new InvalidOperationException("wallet_nonce in request object does not match the provided wallet_nonce");
78+
throw new InvalidOperationException(
79+
"wallet_nonce in request object does not match the provided wallet_nonce");
7580
}
7681
else
7782
{
7883
throw new InvalidOperationException("wallet_nonce is required but not present in the Request Object");
7984
}
8085
});
81-
86+
8287
var json = jwt.Payload.SerializeToJson();
8388

8489
return
@@ -90,7 +95,7 @@ from authRequest in CreateAuthorizationRequest(json)
9095
/// Gets the authorization request from the request object.
9196
/// </summary>
9297
public AuthorizationRequest ToAuthorizationRequest() => AuthorizationRequest;
93-
98+
9499
internal RequestObject WithX509()
95100
{
96101
var encodedCertificate = this.GetLeafCertificate().GetEncoded();
@@ -118,7 +123,7 @@ internal RequestObject WithX509()
118123
AuthorizationRequest = authRequest
119124
};
120125
}
121-
126+
122127
internal RequestObject WithClientMetadata(Option<ClientMetadata> clientMetadata)
123128
{
124129
var authRequest = AuthorizationRequest with
@@ -138,6 +143,13 @@ internal RequestObject WithClientMetadata(Option<ClientMetadata> clientMetadata)
138143
/// </summary>
139144
public static class RequestObjectExtensions
140145
{
146+
public static RequestObject ValidateClientIdPrefix(this RequestObject requestObject) =>
147+
requestObject.ClientIdScheme.Value == ClientIdScheme.ClientIdSchemeValue.RedirectUri
148+
&& requestObject.ToAuthorizationRequest().ResponseUri != requestObject.ClientId
149+
? throw new InvalidOperationException(
150+
"When client_id_prefix is 'redirect_uri', the response_uri must match the client_id")
151+
: requestObject;
152+
141153
/// <summary>
142154
/// Validates the JWT signature.
143155
/// </summary>
@@ -182,22 +194,15 @@ public static RequestObject ValidateTrustChain(this RequestObject requestObject)
182194
else
183195
throw new InvalidOperationException("Validation of trust chain failed");
184196
}
185-
186-
public static RequestObject ValidateClientIdPrefix(this RequestObject requestObject) =>
187-
requestObject.ClientIdScheme.Value == ClientIdScheme.ClientIdSchemeValue.RedirectUri
188-
&& requestObject.ToAuthorizationRequest().ResponseUri != requestObject.ClientId
189-
? throw new InvalidOperationException("When client_id_prefix is 'redirect_uri', the response_uri must match the client_id")
190-
: requestObject;
191-
197+
192198
internal static List<X509Certificate> GetCertificates(this RequestObject requestObject)
193199
{
194200
var x5C = ((JwtSecurityToken)requestObject).Header.X5c;
195-
var result = Parse(x5C).Select(
196-
certAsJToken =>
197-
{
198-
var certBytes = FromBase64String(certAsJToken.ToString());
199-
return new X509CertificateParser().ReadCertificate(certBytes);
200-
}).ToList();
201+
var result = Parse(x5C).Select(certAsJToken =>
202+
{
203+
var certBytes = FromBase64String(certAsJToken.ToString());
204+
return new X509CertificateParser().ReadCertificate(certBytes);
205+
}).ToList();
201206

202207
if (result.Count == 0)
203208
{
@@ -210,20 +215,49 @@ internal static List<X509Certificate> GetCertificates(this RequestObject request
210215
internal static X509Certificate GetLeafCertificate(this RequestObject requestObject) =>
211216
GetCertificates(requestObject).First();
212217

218+
internal static Validation<AuthorizationRequestCancellation, RequestObject> ValidateOrigin(this RequestObject requestObject, Option<Origin> origin)
219+
{
220+
var authRequest = requestObject.ToAuthorizationRequest();
221+
var responseUriOption = authRequest.GetResponseUriMaybe();
222+
223+
return origin.Match(
224+
value =>
225+
{
226+
var expectedOrigins = authRequest.ExpectedOrigins;
227+
if (expectedOrigins?.Contains(value) ?? false)
228+
{
229+
return Prelude.Success<AuthorizationRequestCancellation, RequestObject>(requestObject);
230+
}
231+
else
232+
{
233+
var expectedOriginsList = string.Join(", ", expectedOrigins?.Select(o => o.Value) ?? []);
234+
var error = new OriginMismatchError(
235+
$"Origin {value} is not present in expected origins: [{expectedOriginsList}]");
236+
return new AuthorizationRequestCancellation(responseUriOption, [error]);
237+
}
238+
},
239+
() =>
240+
{
241+
var error = new OriginMismatchError("Origin is required but not provided");
242+
return new AuthorizationRequestCancellation(responseUriOption, [error]);
243+
}
244+
);
245+
}
246+
213247
private static IEnumerable<string> GetSanDnsNames(X509Certificate2 certificate)
214248
{
215249
const string sanOid = "2.5.29.17";
216250
var sanNames = new List<string>();
217251

218252
foreach (var extension in certificate.Extensions)
219253
{
220-
if (extension.Oid.Value != sanOid)
254+
if (extension.Oid!.Value != sanOid)
221255
continue;
222256

223-
var sanExtension = (AsnEncodedData)extension;
257+
AsnEncodedData sanExtension = extension;
224258
var sanData = sanExtension.Format(true);
225259

226-
foreach (var line in sanData.Split(new[] { "\r\n", "\n", "," }, StringSplitOptions.RemoveEmptyEntries))
260+
foreach (var line in sanData.Split(["\r\n", "\n", ","], StringSplitOptions.RemoveEmptyEntries))
227261
{
228262
sanNames.Add(line.Split(':', '=').Last().Trim());
229263
}

0 commit comments

Comments
 (0)