Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ClientAttesatiton #128

Merged
merged 10 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 0 additions & 39 deletions src/WalletFramework.Core/Cryptography/Abstractions/IKeyStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,6 @@ public interface IKeyStore
/// <returns>A <see cref="Task{TResult}" /> representing the generated key's identifier as a string.</returns>
Task<KeyId> GenerateKey(string alg = "ES256");

/// <summary>
/// Asynchronously creates a proof of possession for a specific key, based on the provided audience and nonce.
/// </summary>
/// <param name="keyId">The identifier of the key to be used in creating the proof of possession.</param>
/// <param name="audience">The intended recipient of the proof. Typically represents the entity that will verify it.</param>
/// <param name="nonce">
/// A unique token, typically used to prevent replay attacks by ensuring that the proof is only used once.
/// </param>
/// <param name="type">The type of the proof. (For example "openid4vci-proof+jwt")</param>
/// <param name="sdHash">Base64url-encoded hash digest over the Issuer-signed JWT and the selected Disclosures for integrity protection</param>
/// <returns>
/// A <see cref="Task{TResult}" /> representing the asynchronous operation. When evaluated, the task's result contains
/// the proof.
/// </returns>
Task<string> GenerateKbProofOfPossessionAsync(
KeyId keyId,
string audience,
string nonce,
string type,
string? sdHash = null,
string? clientId = null);

/// <summary>
/// Asynchronously creates a DPoP Proof JWT for a specific key, based on the provided audience, nonce and access token.
/// </summary>
/// <param name="keyId">The identifier of the key to be used in creating the proof of possession.</param>
/// <param name="audience">The intended recipient of the proof. Typically represents the entity that will verify it.</param>
/// <param name="nonce">A unique token, typically used to prevent replay attacks by ensuring that the proof is only used once.</param>
/// <param name="accessToken">The access token, that the DPoP Proof JWT is bound to</param>
/// <returns>
/// A <see cref="Task{TResult}" /> representing the asynchronous operation. When evaluated, the task's result contains
/// the DPoP Proof JWT.
/// </returns>
Task<string> GenerateDPopProofOfPossessionAsync(
KeyId keyId,
string audience,
string? nonce,
string? accessToken);

/// <summary>
/// Asynchronously loads a key by its identifier and returns it as a JSON Web Key (JWK) containing the public key
/// information.
Expand Down
11 changes: 11 additions & 0 deletions src/WalletFramework.Core/Json/JsonSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Newtonsoft.Json;

namespace WalletFramework.Core.Json;

public static class JsonSettings
{
public static JsonSerializerSettings SerializerSettings => new()
{
NullValueHandling = NullValueHandling.Ignore
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using LanguageExt;
using static System.String;

namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models;

public record ClientAttestationPopDetails
{
public string Audience { get; }

public string Issuer { get; }

public Option<string> Nonce { get; }

private ClientAttestationPopDetails(string audience, string issuer, string? nonce)
{
Audience = audience;
Issuer = issuer;
Nonce = nonce;
}

public static ClientAttestationPopDetails CreateClientAttestationPopOptions(string audience, string issuer, string? nonce)
{
if (IsNullOrWhiteSpace(audience) || IsNullOrWhiteSpace(issuer))
{
throw new ArgumentException("Audience and Issuer must be provided.");
}

return new ClientAttestationPopDetails(audience, issuer, nonce);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models;

public record CombinedWalletAttestation
{
public WalletInstanceAttestationJwt WalletInstanceAttestationJwt { get; }

public WalletInstanceAttestationPopJwt WalletInstanceAttestationPopJwt { get; }

private CombinedWalletAttestation(
WalletInstanceAttestationJwt walletInstanceAttestationJwt,
WalletInstanceAttestationPopJwt walletInstanceAttestationPopJwt)
{
WalletInstanceAttestationJwt = walletInstanceAttestationJwt;
WalletInstanceAttestationPopJwt = walletInstanceAttestationPopJwt;
}

public static CombinedWalletAttestation Create(
WalletInstanceAttestationJwt walletInstanceAttestationJwt,
WalletInstanceAttestationPopJwt walletInstanceAttestationPopJwt)
=> new(walletInstanceAttestationJwt, walletInstanceAttestationPopJwt);
}

public static class CombinedWalletAttestationExtensions
{
public static string ToStringRepresentation(this CombinedWalletAttestation combinedWalletAttestation)
=> combinedWalletAttestation.WalletInstanceAttestationJwt + "~" +
combinedWalletAttestation.WalletInstanceAttestationPopJwt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using WalletFramework.Core.Functional;
using WalletFramework.Core.Functional.Errors;

namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models;

public struct WalletInstanceAttestationJwt
{
public string Value { get; }

public static implicit operator string(WalletInstanceAttestationJwt keyId) => keyId.Value;

private WalletInstanceAttestationJwt(string value) => Value = value;

public static Validation<WalletInstanceAttestationJwt> ValidWalletInstanceAttestationJwt(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return new StringIsNullOrWhitespaceError<WalletInstanceAttestationJwt>();
}

return new WalletInstanceAttestationJwt(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models;

public struct WalletInstanceAttestationPopJwt
{
public string Value { get; }

public static implicit operator string(WalletInstanceAttestationPopJwt keyId) => keyId.Value;

private WalletInstanceAttestationPopJwt(string value) => Value = value;

public static WalletInstanceAttestationPopJwt CreateWalletInstanceAttestationPopJwt(string value) => new (value);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WalletFramework.Core.Cryptography.Abstractions;
using WalletFramework.Core.Cryptography.Models;
using WalletFramework.Core.Functional;
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Abstractions;
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models;
using WalletFramework.Oid4Vc.Oid4Vci.Exceptions;
using WalletFramework.Oid4Vc.Oid4Vci.Extensions;
using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService;

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

Expand All @@ -18,14 +24,17 @@ public class DPopHttpClient : IDPopHttpClient
public DPopHttpClient(
IHttpClientFactory httpClientFactory,
IKeyStore keyStore,
ISdJwtSignerService sdJwtSignerService,
ILogger<DPopHttpClient> logger)
{
_keyStore = keyStore;
_sdJwtSignerService = sdJwtSignerService;
_httpClient = httpClientFactory.CreateClient();
_logger = logger;
}

private readonly IKeyStore _keyStore;
private readonly ISdJwtSignerService _sdJwtSignerService;
private readonly ILogger<DPopHttpClient> _logger;
private readonly HttpClient _httpClient;

Expand All @@ -34,7 +43,7 @@ public async Task<DPopHttpResponse> Post(
DPopConfig config,
Func<HttpContent> getContent)
{
var dPop = await _keyStore.GenerateDPopProofOfPossessionAsync(
var dPop = await GenerateDPopHeaderAsync(
config.KeyId,
config.Audience,
config.Nonce.ToNullable(),
Expand All @@ -53,7 +62,7 @@ public async Task<DPopHttpResponse> Post(
{
config = config with { Nonce = new DPopNonce(nonceStr) };

var newDpop = await _keyStore.GenerateDPopProofOfPossessionAsync(
var newDpop = await GenerateDPopHeaderAsync(
config.KeyId,
config.Audience,
config.Nonce.ToNullable(),
Expand Down Expand Up @@ -106,4 +115,39 @@ or System.Net.HttpStatusCode.Unauthorized

return null;
}

private async Task<string> GenerateDPopHeaderAsync(KeyId keyId, string audience, string? nonce, string? accessToken)
{
var header = new Dictionary<string, object>
{
{ "alg", "ES256" },
{ "typ", "dpop+jwt" }
};

var jwkSerialized = await _keyStore.LoadKey(keyId);
var jwkDeserialized = JsonConvert.DeserializeObject(jwkSerialized);
if (jwkDeserialized != null)
{
header["jwk"] = jwkDeserialized;
}

string? ath = null;
if (!string.IsNullOrEmpty(accessToken))
{
var sha256 = SHA256.Create();
ath = Base64UrlEncoder.Encode(sha256.ComputeHash(Encoding.UTF8.GetBytes(accessToken)));
}

var dPopPayload = new
{
jti = Guid.NewGuid().ToString(),
htm = "POST",
iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
htu = new Uri(audience).GetLeftPart(UriPartial.Path),
nonce,
ath
};

return await _sdJwtSignerService.CreateSignedJwt(header, dPopPayload, keyId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models;
using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models.SdJwt;
using WalletFramework.Oid4Vc.Oid4Vci.CredResponse;
using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService;

namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Implementations;

Expand All @@ -28,15 +29,18 @@ public class CredentialRequestService : ICredentialRequestService
public CredentialRequestService(
HttpClient httpClient,
IDPopHttpClient dPopHttpClient,
ISdJwtSignerService sdJwtSignerService,
IKeyStore keyStore)
{
_dPopHttpClient = dPopHttpClient;
_httpClient = httpClient;
_sdJwtSignerService = sdJwtSignerService;
_keyStore = keyStore;
_httpClient = httpClient;
}

private readonly HttpClient _httpClient;
private readonly IDPopHttpClient _dPopHttpClient;
private readonly ISdJwtSignerService _sdJwtSignerService;
private readonly IKeyStore _keyStore;

private async Task<CredentialRequest> CreateCredentialRequest(
Expand All @@ -50,7 +54,7 @@ private async Task<CredentialRequest> CreateCredentialRequest(
oauthToken => oauthToken.CNonce,
dPopToken => dPopToken.Token.CNonce);

var keyBindingJwt = await _keyStore.GenerateKbProofOfPossessionAsync(
var keyBindingJwt = await _sdJwtSignerService.GenerateKbProofOfPossessionAsync(
keyId,
issuerMetadata.CredentialIssuer.ToString(),
cNonce,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,4 @@ private static SdJwtDoc _toSdJwtDoc(SdJwtRecord record)
{
return new SdJwtDoc(record.EncodedIssuerSignedJwt + "~" + string.Join("~", record.Disclosures) + "~");
}
}
}
1 change: 1 addition & 0 deletions src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static IServiceCollection AddSdJwtVcServices(this IServiceCollection buil
{
builder.AddSingleton<IHolder, Holder>();
builder.AddSingleton<ISdJwtVcHolderService, SdJwtVcHolderService>();
builder.AddSingleton<ISdJwtSignerService, SdJwtSignerService>();
return builder;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using WalletFramework.Core.Cryptography.Models;

namespace WalletFramework.SdJwtVc.Services.SdJwtVcHolderService;

public interface ISdJwtSignerService
{
Task<string> CreateSignedJwt(object header, object payload, KeyId keyId);

Task<string> GenerateKbProofOfPossessionAsync(KeyId keyId, string audience, string nonce, string type, string? sdHash, string? clientId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Org.BouncyCastle.Asn1;
using WalletFramework.Core.Cryptography.Abstractions;
using WalletFramework.Core.Cryptography.Models;

namespace WalletFramework.SdJwtVc.Services.SdJwtVcHolderService;

public class SdJwtSignerService : ISdJwtSignerService
{
private readonly IKeyStore _keyStore;

public SdJwtSignerService(IKeyStore keyStore) => _keyStore = keyStore;

public async Task<string> GenerateKbProofOfPossessionAsync(KeyId keyId, string audience, string nonce, string type, string? sdHash, string? clientId)
{
var header = new Dictionary<string, object>
{
{ "alg", "ES256" },
{ "typ", type }
};

if (string.Equals(type, "openid4vci-proof+jwt", StringComparison.OrdinalIgnoreCase))
{
var jwkSerialized = await _keyStore.LoadKey(keyId);
var jwkDeserialized = JsonConvert.DeserializeObject(jwkSerialized);
if (jwkDeserialized != null)
{
header["jwk"] = jwkDeserialized;
}
}

var payload = new
{
aud = audience,
nonce,
iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
sd_hash = sdHash,
iss = clientId
};

return await CreateSignedJwt(header, payload, keyId);
}

public async Task<string> CreateSignedJwt(object header, object payload, KeyId keyId)
{
var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header));
var encodedPayload = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(payload));

var dataToSign = encodedHeader + "." + encodedPayload;
var signedData = await _keyStore.Sign(keyId, Encoding.UTF8.GetBytes(dataToSign));
var rawSignature = ConvertDerToRawFormat(signedData);

var encodedSignature = Base64UrlEncoder.Encode(rawSignature);
return encodedHeader + "." + encodedPayload + "." + encodedSignature;
}

private static byte[] ConvertDerToRawFormat(byte[]? derSignature)
{
var seq = (Asn1Sequence)Asn1Object.FromByteArray(derSignature);
var r = ((DerInteger)seq[0]).Value;
var s = ((DerInteger)seq[1]).Value;
var rawSignature = new byte[64];
var rBytes = r.ToByteArrayUnsigned();
var sBytes = s.ToByteArrayUnsigned();
Array.Copy(rBytes, Math.Max(0, rBytes.Length - 32), rawSignature, 0, Math.Min(32, rBytes.Length));
Array.Copy(sBytes, Math.Max(0, sBytes.Length - 32), rawSignature, 32, Math.Min(32, sBytes.Length));
return rawSignature;
}
}
Loading
Loading