Skip to content

Commit

Permalink
introduce batch issuance (#213)
Browse files Browse the repository at this point in the history
* introduce batch issuance

Signed-off-by: kenkosmowski <[email protected]>

* process credential response with multiple credentials

Signed-off-by: kenkosmowski <[email protected]>

---------

Signed-off-by: kenkosmowski <[email protected]>
  • Loading branch information
kenkosmowski authored Nov 11, 2024
1 parent 68a2657 commit 5560133
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Implementations;

public class CredentialRequestService : ICredentialRequestService
{
private const int MaxBatchSize = 10;

public CredentialRequestService(
HttpClient httpClient,
IDPopHttpClient dPopHttpClient,
Expand Down Expand Up @@ -58,33 +60,69 @@ private async Task<CredentialRequest> CreateCredentialRequest(
dPopToken => dPopToken.Token.CNonce);

var proof = Option<ProofOfPossession>.None;
var proofs = Option<ProofsOfPossession>.None;
var sessionTranscript = Option<SessionTranscript>.None;

await authorizationRequest.Match(
Some: _ =>
{
if (format == "mso_mdoc")
sessionTranscript = authorizationRequest.UnwrapOrThrow(new Exception()).ToVpHandover().ToSessionTranscript();
sessionTranscript = authorizationRequest.UnwrapOrThrow(new Exception()).ToVpHandover()
.ToSessionTranscript();
return Task.CompletedTask;
},
None: async () =>
{
var keyBindingJwt = await _sdJwtSigner.GenerateKbProofOfPossessionAsync(
keyId,
issuerMetadata.CredentialIssuer.ToString(),
cNonce,
"openid4vci-proof+jwt",
null,
clientOptions.ToNullable()?.ClientId);

proof = new ProofOfPossession
{
ProofType = "jwt",
Jwt = keyBindingJwt
};
await issuerMetadata.BatchCredentialIssuance.Match(
Some: async batchCredentialIssuance =>
{
await batchCredentialIssuance.BatchSize.Match(
Some: async batchSize =>
{
proofs = await GetProofsOfPossessionAsync(Math.Max(MaxBatchSize, batchSize), keyId,
issuerMetadata, cNonce, clientOptions);
},
None: async () =>
{
proof = await GetProofOfPossessionAsync(keyId, issuerMetadata, cNonce, clientOptions);
});
},
None: async () =>
proof = await GetProofOfPossessionAsync(keyId, issuerMetadata, cNonce, clientOptions));
});

return new CredentialRequest(format, proof, sessionTranscript);
return new CredentialRequest(format, proof, proofs, sessionTranscript);
}

private async Task<ProofOfPossession> GetProofOfPossessionAsync(KeyId keyId, IssuerMetadata issuerMetadata, string cNonce, Option<ClientOptions> clientOptions)
{
return new ProofOfPossession
{
ProofType = "jwt",
Jwt = await GenerateKbProofOfPossession(keyId, issuerMetadata, cNonce, clientOptions)
};
}

private async Task<ProofsOfPossession> GetProofsOfPossessionAsync(int batchSize, KeyId keyId, IssuerMetadata issuerMetadata, string cNonce, Option<ClientOptions> clientOptions)
{
var jwts = new List<string>();
for(var i = 0; i < batchSize; i++)
{
jwts.Add(await GenerateKbProofOfPossession(keyId, issuerMetadata, cNonce, clientOptions));
}

return new ProofsOfPossession("jwt", jwts.ToArray());
}

private async Task<string> GenerateKbProofOfPossession(KeyId keyId, IssuerMetadata issuerMetadata, string cNonce, Option<ClientOptions> clientOptions)
{
return await _sdJwtSigner.GenerateKbProofOfPossessionAsync(
keyId,
issuerMetadata.CredentialIssuer.ToString(),
cNonce,
"openid4vci-proof+jwt",
null,
clientOptions.ToNullable()?.ClientId);
}

async Task<Validation<CredentialResponse>> ICredentialRequestService.RequestCredentials(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models;
/// This request contains the format of the credential, the type of credential,
/// and a proof of possession of the key material the issued credential shall be bound to.
/// </summary>
public record CredentialRequest(Format Format, Option<ProofOfPossession> Proof, Option<SessionTranscript> SessionTranscript)
public record CredentialRequest(Format Format, Option<ProofOfPossession> Proof, Option<ProofsOfPossession> Proofs, Option<SessionTranscript> SessionTranscript)
{
/// <summary>
/// Gets the proof of possession of the key material the issued credential shall be bound to.
/// </summary>
public Option<ProofOfPossession> Proof { get; } = Proof;

/// <summary>
/// Gets one or more proof of possessions of the key material the issued credential shall be bound to.
/// </summary>
public Option<ProofsOfPossession> Proofs { get; } = Proofs;

/// <summary>
/// Gets the format of the credential to be issued.
Expand All @@ -29,6 +34,7 @@ public record CredentialRequest(Format Format, Option<ProofOfPossession> Proof,
public static class CredentialRequestFun
{
private const string ProofJsonKey = "proof";
private const string ProofsJsonKey = "proofs";
private const string FormatJsonKey = "format";
private const string SessionTranscriptKey = "session_transcript";

Expand All @@ -41,6 +47,11 @@ public static JObject EncodeToJson(this CredentialRequest request)
result.Add(ProofJsonKey, JObject.FromObject(proof));
});

request.Proofs.IfSome(proofs =>
{
result.Add(ProofsJsonKey, proofs.EncodeToJson());
});

request.SessionTranscript.IfSome(sessionTranscript =>
{
result.Add(SessionTranscriptKey, Base64UrlString.CreateBase64UrlString(sessionTranscript.ToCbor().ToJSONBytes()).ToString());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models;

/// <summary>
/// Represents one or more proof of possessions of the key material that the issued credential is bound to.
/// This contains the jwts that acts as the proof of possessions.
/// </summary>
public record ProofsOfPossession(string ProofType, string[] Jwt);

public static class ProofsOfPossessionFun
{
public static JObject EncodeToJson(this ProofsOfPossession proofsOfPossession)
{
return new JObject
{
[proofsOfPossession.ProofType] = JArray.FromObject(proofsOfPossession.Jwt)
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public record CredentialResponse
{
/// <summary>
/// <para>
/// Credential: Contains issued Credential. It MUST be present when transaction_id is not returned. It MAY be a
/// Credentials: Contains issued Credentials. It MUST be present when transaction_id is not returned. It MAY be a
/// string or an object, depending on the Credential format
/// </para>
/// <para>
Expand All @@ -32,7 +32,7 @@ public record CredentialResponse
/// has been obtained by the Wallet
/// </para>
/// </summary>
public OneOf<Credential, TransactionId> CredentialOrTransactionId { get; }
public OneOf<List<Credential>, TransactionId> CredentialsOrTransactionId { get; }

/// <summary>
/// OPTIONAL. JSON string containing a nonce to be used to create a proof of possession of key material
Expand All @@ -51,32 +51,47 @@ public record CredentialResponse
public KeyId KeyId { get; }

private CredentialResponse(
OneOf<Credential, TransactionId> credentialOrTransactionId,
OneOf<List<Credential>, TransactionId> credentialsOrTransactionId,
Option<string> cNonce,
Option<int> cNonceExpiresIn,
KeyId keyId)
{
CNonceExpiresIn = cNonceExpiresIn;
CredentialOrTransactionId = credentialOrTransactionId;
CredentialsOrTransactionId = credentialsOrTransactionId;
CNonce = cNonce;
KeyId = keyId;
}

private static CredentialResponse Create(
OneOf<Credential, TransactionId> credentialOrTransactionId,
OneOf<List<Credential>, TransactionId> credentialsOrTransactionId,
Option<string> cNonce,
Option<int> cNonceExpiresIn,
KeyId keyId) =>
new(credentialOrTransactionId, cNonce, cNonceExpiresIn, keyId);
new(credentialsOrTransactionId, cNonce, cNonceExpiresIn, keyId);

public static Validation<CredentialResponse> ValidCredentialResponse(JObject response, KeyId keyId)
{
// TODO: Implement transactionID
var credential =
from jToken in response.GetByKey("credential")
from jValue in jToken.ToJValue()
from cred in Credential.ValidCredential(jValue)
select (OneOf<Credential, TransactionId>)cred;
from jToken in response.GetByKey("credential").ToOption()
from jValue in jToken.ToJValue().ToOption()
from cred in Credential.ValidCredential(jValue).ToOption()
select cred;

var batchCredentials =
from jToken in response.GetByKey("credentials").ToOption()
from jArray in jToken.ToJArray().ToOption()
from all in jArray.TraverseAll(jToken =>
from jValue in jToken.ToJValue().ToOption()
from cred in Credential.ValidCredential(jValue).ToOption()
select cred)
select all;

var credentials = batchCredentials.Match(
Some: bc => (OneOf<List<Credential>, TransactionId>)bc.ToList(),
None: () => credential.Match(
Some: c => (OneOf<List<Credential>, TransactionId>)new List<Credential> { c },
None: () => throw new InvalidOperationException("Credential response contains no credentials")));

var cNonce = response
.GetByKey("c_nonce")
Expand Down Expand Up @@ -109,7 +124,7 @@ from cred in Credential.ValidCredential(jValue)
.ToOption();

return ValidationFun.Valid(Create)
.Apply(credential)
.Apply(credentials)
.Apply(cNonce)
.Apply(cNonceExpiresIn)
.Apply(keyId);
Expand Down
Loading

0 comments on commit 5560133

Please sign in to comment.