Skip to content

Commit 21b8639

Browse files
JoTiTuDindexx
andauthored
Add ClientAttesatiton (#128)
* add ClientAttesatiton to KeyStore interface Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> * mdoc oid4vci Signed-off-by: Kevin <kevin.dinh@lissi.id> * separate integration tests Signed-off-by: Kevin <kevin.dinh@lissi.id> * move away from json converters Signed-off-by: Kevin <kevin.dinh@lissi.id> * fix credential request json Signed-off-by: Kevin <kevin.dinh@lissi.id> * refactor key store Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> * move structs to own file Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> * add singelton Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> --------- Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> Signed-off-by: Kevin <kevin.dinh@lissi.id> Co-authored-by: Kevin <kevin.dinh@lissi.id>
1 parent 7bdbaa1 commit 21b8639

File tree

14 files changed

+250
-58
lines changed

14 files changed

+250
-58
lines changed

src/WalletFramework.Core/Cryptography/Abstractions/IKeyStore.cs

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,45 +16,6 @@ public interface IKeyStore
1616
/// <returns>A <see cref="Task{TResult}" /> representing the generated key's identifier as a string.</returns>
1717
Task<KeyId> GenerateKey(string alg = "ES256");
1818

19-
/// <summary>
20-
/// Asynchronously creates a proof of possession for a specific key, based on the provided audience and nonce.
21-
/// </summary>
22-
/// <param name="keyId">The identifier of the key to be used in creating the proof of possession.</param>
23-
/// <param name="audience">The intended recipient of the proof. Typically represents the entity that will verify it.</param>
24-
/// <param name="nonce">
25-
/// A unique token, typically used to prevent replay attacks by ensuring that the proof is only used once.
26-
/// </param>
27-
/// <param name="type">The type of the proof. (For example "openid4vci-proof+jwt")</param>
28-
/// <param name="sdHash">Base64url-encoded hash digest over the Issuer-signed JWT and the selected Disclosures for integrity protection</param>
29-
/// <returns>
30-
/// A <see cref="Task{TResult}" /> representing the asynchronous operation. When evaluated, the task's result contains
31-
/// the proof.
32-
/// </returns>
33-
Task<string> GenerateKbProofOfPossessionAsync(
34-
KeyId keyId,
35-
string audience,
36-
string nonce,
37-
string type,
38-
string? sdHash = null,
39-
string? clientId = null);
40-
41-
/// <summary>
42-
/// Asynchronously creates a DPoP Proof JWT for a specific key, based on the provided audience, nonce and access token.
43-
/// </summary>
44-
/// <param name="keyId">The identifier of the key to be used in creating the proof of possession.</param>
45-
/// <param name="audience">The intended recipient of the proof. Typically represents the entity that will verify it.</param>
46-
/// <param name="nonce">A unique token, typically used to prevent replay attacks by ensuring that the proof is only used once.</param>
47-
/// <param name="accessToken">The access token, that the DPoP Proof JWT is bound to</param>
48-
/// <returns>
49-
/// A <see cref="Task{TResult}" /> representing the asynchronous operation. When evaluated, the task's result contains
50-
/// the DPoP Proof JWT.
51-
/// </returns>
52-
Task<string> GenerateDPopProofOfPossessionAsync(
53-
KeyId keyId,
54-
string audience,
55-
string? nonce,
56-
string? accessToken);
57-
5819
/// <summary>
5920
/// Asynchronously loads a key by its identifier and returns it as a JSON Web Key (JWK) containing the public key
6021
/// information.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Newtonsoft.Json;
2+
3+
namespace WalletFramework.Core.Json;
4+
5+
public static class JsonSettings
6+
{
7+
public static JsonSerializerSettings SerializerSettings => new()
8+
{
9+
NullValueHandling = NullValueHandling.Ignore
10+
};
11+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using LanguageExt;
2+
using static System.String;
3+
4+
namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models;
5+
6+
public record ClientAttestationPopDetails
7+
{
8+
public string Audience { get; }
9+
10+
public string Issuer { get; }
11+
12+
public Option<string> Nonce { get; }
13+
14+
private ClientAttestationPopDetails(string audience, string issuer, string? nonce)
15+
{
16+
Audience = audience;
17+
Issuer = issuer;
18+
Nonce = nonce;
19+
}
20+
21+
public static ClientAttestationPopDetails CreateClientAttestationPopOptions(string audience, string issuer, string? nonce)
22+
{
23+
if (IsNullOrWhiteSpace(audience) || IsNullOrWhiteSpace(issuer))
24+
{
25+
throw new ArgumentException("Audience and Issuer must be provided.");
26+
}
27+
28+
return new ClientAttestationPopDetails(audience, issuer, nonce);
29+
}
30+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models;
2+
3+
public record CombinedWalletAttestation
4+
{
5+
public WalletInstanceAttestationJwt WalletInstanceAttestationJwt { get; }
6+
7+
public WalletInstanceAttestationPopJwt WalletInstanceAttestationPopJwt { get; }
8+
9+
private CombinedWalletAttestation(
10+
WalletInstanceAttestationJwt walletInstanceAttestationJwt,
11+
WalletInstanceAttestationPopJwt walletInstanceAttestationPopJwt)
12+
{
13+
WalletInstanceAttestationJwt = walletInstanceAttestationJwt;
14+
WalletInstanceAttestationPopJwt = walletInstanceAttestationPopJwt;
15+
}
16+
17+
public static CombinedWalletAttestation Create(
18+
WalletInstanceAttestationJwt walletInstanceAttestationJwt,
19+
WalletInstanceAttestationPopJwt walletInstanceAttestationPopJwt)
20+
=> new(walletInstanceAttestationJwt, walletInstanceAttestationPopJwt);
21+
}
22+
23+
public static class CombinedWalletAttestationExtensions
24+
{
25+
public static string ToStringRepresentation(this CombinedWalletAttestation combinedWalletAttestation)
26+
=> combinedWalletAttestation.WalletInstanceAttestationJwt + "~" +
27+
combinedWalletAttestation.WalletInstanceAttestationPopJwt;
28+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using WalletFramework.Core.Functional;
2+
using WalletFramework.Core.Functional.Errors;
3+
4+
namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models;
5+
6+
public struct WalletInstanceAttestationJwt
7+
{
8+
public string Value { get; }
9+
10+
public static implicit operator string(WalletInstanceAttestationJwt keyId) => keyId.Value;
11+
12+
private WalletInstanceAttestationJwt(string value) => Value = value;
13+
14+
public static Validation<WalletInstanceAttestationJwt> ValidWalletInstanceAttestationJwt(string value)
15+
{
16+
if (string.IsNullOrWhiteSpace(value))
17+
{
18+
return new StringIsNullOrWhitespaceError<WalletInstanceAttestationJwt>();
19+
}
20+
21+
return new WalletInstanceAttestationJwt(value);
22+
}
23+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models;
2+
3+
public struct WalletInstanceAttestationPopJwt
4+
{
5+
public string Value { get; }
6+
7+
public static implicit operator string(WalletInstanceAttestationPopJwt keyId) => keyId.Value;
8+
9+
private WalletInstanceAttestationPopJwt(string value) => Value = value;
10+
11+
public static WalletInstanceAttestationPopJwt CreateWalletInstanceAttestationPopJwt(string value) => new (value);
12+
}

src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
using System.Security.Cryptography;
2+
using System.Text;
13
using Microsoft.Extensions.Logging;
4+
using Microsoft.IdentityModel.Tokens;
5+
using Newtonsoft.Json;
26
using Newtonsoft.Json.Linq;
37
using WalletFramework.Core.Cryptography.Abstractions;
8+
using WalletFramework.Core.Cryptography.Models;
49
using WalletFramework.Core.Functional;
510
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Abstractions;
611
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models;
712
using WalletFramework.Oid4Vc.Oid4Vci.Exceptions;
813
using WalletFramework.Oid4Vc.Oid4Vci.Extensions;
14+
using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService;
915

1016
namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Implementations;
1117

@@ -18,14 +24,17 @@ public class DPopHttpClient : IDPopHttpClient
1824
public DPopHttpClient(
1925
IHttpClientFactory httpClientFactory,
2026
IKeyStore keyStore,
27+
ISdJwtSignerService sdJwtSignerService,
2128
ILogger<DPopHttpClient> logger)
2229
{
2330
_keyStore = keyStore;
31+
_sdJwtSignerService = sdJwtSignerService;
2432
_httpClient = httpClientFactory.CreateClient();
2533
_logger = logger;
2634
}
2735

2836
private readonly IKeyStore _keyStore;
37+
private readonly ISdJwtSignerService _sdJwtSignerService;
2938
private readonly ILogger<DPopHttpClient> _logger;
3039
private readonly HttpClient _httpClient;
3140

@@ -34,7 +43,7 @@ public async Task<DPopHttpResponse> Post(
3443
DPopConfig config,
3544
Func<HttpContent> getContent)
3645
{
37-
var dPop = await _keyStore.GenerateDPopProofOfPossessionAsync(
46+
var dPop = await GenerateDPopHeaderAsync(
3847
config.KeyId,
3948
config.Audience,
4049
config.Nonce.ToNullable(),
@@ -53,7 +62,7 @@ public async Task<DPopHttpResponse> Post(
5362
{
5463
config = config with { Nonce = new DPopNonce(nonceStr) };
5564

56-
var newDpop = await _keyStore.GenerateDPopProofOfPossessionAsync(
65+
var newDpop = await GenerateDPopHeaderAsync(
5766
config.KeyId,
5867
config.Audience,
5968
config.Nonce.ToNullable(),
@@ -106,4 +115,39 @@ or System.Net.HttpStatusCode.Unauthorized
106115

107116
return null;
108117
}
118+
119+
private async Task<string> GenerateDPopHeaderAsync(KeyId keyId, string audience, string? nonce, string? accessToken)
120+
{
121+
var header = new Dictionary<string, object>
122+
{
123+
{ "alg", "ES256" },
124+
{ "typ", "dpop+jwt" }
125+
};
126+
127+
var jwkSerialized = await _keyStore.LoadKey(keyId);
128+
var jwkDeserialized = JsonConvert.DeserializeObject(jwkSerialized);
129+
if (jwkDeserialized != null)
130+
{
131+
header["jwk"] = jwkDeserialized;
132+
}
133+
134+
string? ath = null;
135+
if (!string.IsNullOrEmpty(accessToken))
136+
{
137+
var sha256 = SHA256.Create();
138+
ath = Base64UrlEncoder.Encode(sha256.ComputeHash(Encoding.UTF8.GetBytes(accessToken)));
139+
}
140+
141+
var dPopPayload = new
142+
{
143+
jti = Guid.NewGuid().ToString(),
144+
htm = "POST",
145+
iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
146+
htu = new Uri(audience).GetLeftPart(UriPartial.Path),
147+
nonce,
148+
ath
149+
};
150+
151+
return await _sdJwtSignerService.CreateSignedJwt(header, dPopPayload, keyId);
152+
}
109153
}

src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models;
2121
using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models.SdJwt;
2222
using WalletFramework.Oid4Vc.Oid4Vci.CredResponse;
23+
using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService;
2324

2425
namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Implementations;
2526

@@ -28,15 +29,18 @@ public class CredentialRequestService : ICredentialRequestService
2829
public CredentialRequestService(
2930
HttpClient httpClient,
3031
IDPopHttpClient dPopHttpClient,
32+
ISdJwtSignerService sdJwtSignerService,
3133
IKeyStore keyStore)
3234
{
3335
_dPopHttpClient = dPopHttpClient;
34-
_httpClient = httpClient;
36+
_sdJwtSignerService = sdJwtSignerService;
3537
_keyStore = keyStore;
38+
_httpClient = httpClient;
3639
}
3740

3841
private readonly HttpClient _httpClient;
3942
private readonly IDPopHttpClient _dPopHttpClient;
43+
private readonly ISdJwtSignerService _sdJwtSignerService;
4044
private readonly IKeyStore _keyStore;
4145

4246
private async Task<CredentialRequest> CreateCredentialRequest(
@@ -50,7 +54,7 @@ private async Task<CredentialRequest> CreateCredentialRequest(
5054
oauthToken => oauthToken.CNonce,
5155
dPopToken => dPopToken.Token.CNonce);
5256

53-
var keyBindingJwt = await _keyStore.GenerateKbProofOfPossessionAsync(
57+
var keyBindingJwt = await _sdJwtSignerService.GenerateKbProofOfPossessionAsync(
5458
keyId,
5559
issuerMetadata.CredentialIssuer.ToString(),
5660
cNonce,

src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public static IServiceCollection AddSdJwtVcServices(this IServiceCollection buil
1111
{
1212
builder.AddSingleton<IHolder, Holder>();
1313
builder.AddSingleton<ISdJwtVcHolderService, SdJwtVcHolderService>();
14+
builder.AddSingleton<ISdJwtSignerService, SdJwtSignerService>();
1415
return builder;
1516
}
1617

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using WalletFramework.Core.Cryptography.Models;
2+
3+
namespace WalletFramework.SdJwtVc.Services.SdJwtVcHolderService;
4+
5+
public interface ISdJwtSignerService
6+
{
7+
Task<string> CreateSignedJwt(object header, object payload, KeyId keyId);
8+
9+
Task<string> GenerateKbProofOfPossessionAsync(KeyId keyId, string audience, string nonce, string type, string? sdHash, string? clientId);
10+
}

0 commit comments

Comments
 (0)