Skip to content

Commit

Permalink
add status list support (#235)
Browse files Browse the repository at this point in the history
* add status list support

Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>

* trigger pipe

Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>

* only allow https scheme for vct metadata retreival

Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>

* trigger pipe

Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>

* replace zlib with c# implementation

Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>

* remove virtual

Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>

---------

Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>
  • Loading branch information
JoTiTu authored Dec 9, 2024
1 parent d6f58df commit f23104f
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 10 deletions.
19 changes: 14 additions & 5 deletions src/WalletFramework.Oid4Vc/CredentialSet/CredentialSetService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using WalletFramework.Oid4Vc.CredentialSet.Models;
using WalletFramework.Oid4Vc.Oid4Vci.Abstractions;
using WalletFramework.SdJwtVc.Models.Records;
using WalletFramework.SdJwtVc.Services;
using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService;

namespace WalletFramework.Oid4Vc.CredentialSet;
Expand All @@ -16,6 +17,7 @@ public class CredentialSetService(
IAgentProvider agentProvider,
ISdJwtVcHolderService sdJwtVcHolderService,
IMdocStorage mDocStorage,
IStatusListService statusListService,
IWalletRecordService walletRecordService)
: ICredentialSetService
{
Expand Down Expand Up @@ -101,7 +103,7 @@ public async Task<Option<IEnumerable<CredentialSetRecord>>> ListAsync(
return records;
}

public virtual async Task<Option<CredentialSetRecord>> GetAsync(CredentialSetId credentialSetId)
public async Task<Option<CredentialSetRecord>> GetAsync(CredentialSetId credentialSetId)
{
var context = await agentProvider.GetContextAsync();
var record = await walletRecordService.GetAsync<CredentialSetRecord>(context.Wallet, credentialSetId);
Expand All @@ -127,10 +129,17 @@ public async Task<CredentialSetRecord> RefreshCredentialSetState(CredentialSetRe
if (expiresAt < DateTime.UtcNow)
credentialSetRecord.State = CredentialState.Expired;
});

//TODO: Implement revocation check (status List) -> Currently IsRevoked always returns false
if (credentialSetRecord.IsRevoked())
credentialSetRecord.State = CredentialState.Revoked;

await credentialSetRecord.StatusList.IfSomeAsync(
async statusList =>
{
await statusListService.GetState(statusList).IfSomeAsync(
state =>
{
if (state == CredentialState.Revoked)
credentialSetRecord.State = CredentialState.Revoked;
});
});

if (oldState != credentialSetRecord.State)
await UpdateAsync(credentialSetRecord);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using WalletFramework.MdocVc;
using WalletFramework.SdJwtVc.Models;
using WalletFramework.SdJwtVc.Models.Records;
using WalletFramework.SdJwtVc.Models.StatusList;
using CredentialState = WalletFramework.Core.Credentials.CredentialState;

namespace WalletFramework.Oid4Vc.CredentialSet.Models;
Expand All @@ -31,8 +32,7 @@ public sealed class CredentialSetRecord : RecordBase

public Option<DateTime> ExpiresAt { get; set; }

//TODO: Add Status List
// public Option<StatusList> StatusList { get; }
public Option<Status> StatusList { get; set; }

public Option<DateTime> RevokedAt { get; set; }

Expand All @@ -53,6 +53,7 @@ public CredentialSetRecord(
Option<DocType> mDocCredentialType,
Dictionary<string, string> credentialAttributes,
CredentialState credentialState,
Option<Status> statusList,
Option<DateTime> expiresAt,
Option<DateTime> issuedAt,
Option<DateTime> notBefore,
Expand All @@ -66,6 +67,7 @@ public CredentialSetRecord(
MDocCredentialType = mDocCredentialType;
CredentialAttributes = credentialAttributes;
State = credentialState;
StatusList = statusList;
ExpiresAt = expiresAt;
IssuedAt = issuedAt;
NotBefore = notBefore;
Expand Down Expand Up @@ -119,6 +121,7 @@ public static class CredentialSetRecordExtensions
private const string IssuerIdJsonKey = "issuer_id";
private const string CreatedAtJsonKey = "created_at";
private const string UpdatedAtJsonKey = "updated_at";
private const string StatusListJsonKey = "status_list";

public static void AddSdJwtData(
this CredentialSetRecord credentialSetRecord,
Expand All @@ -131,6 +134,7 @@ public static void AddSdJwtData(
credentialSetRecord.IssuedAt = sdJwtRecord.IssuedAt.ToOption();
credentialSetRecord.NotBefore = sdJwtRecord.NotBefore.ToOption();
credentialSetRecord.IssuerId = sdJwtRecord.IssuerId;
credentialSetRecord.StatusList = sdJwtRecord.Status;
}

public static void AddMDocData(
Expand Down Expand Up @@ -200,6 +204,7 @@ public static JObject EncodeToJson(this CredentialSetRecord credentialSetRecord)
credentialSetRecord.DeletedAt.IfSome(deletedAt => result.Add(DeletedAtJsonKey, deletedAt));
credentialSetRecord.CreatedAtUtc.IfSome(createdAtUtc => result.Add(CreatedAtJsonKey, createdAtUtc));
credentialSetRecord.UpdatedAtUtc.IfSome(updatedAtUtc => result.Add(UpdatedAtJsonKey, updatedAtUtc));
credentialSetRecord.StatusList.IfSome(statusList => result.Add(StatusListJsonKey, JObject.FromObject(statusList)));
result.Add(IssuerIdJsonKey, credentialSetRecord.IssuerId);

return result;
Expand All @@ -223,6 +228,10 @@ from docType in DocType.ValidDoctype(jToken).ToOption()

var stateType = Enum.Parse<CredentialState>(json[StateJsonKey]!.ToString());

var statusListType =
from jToken in json.GetByKey(StatusListJsonKey).ToOption()
select jToken.ToObject<Status>();

var expiresAtType =
from jToken in json.GetByKey(ExpiresAtJsonKey).ToOption()
select jToken.ToObject<DateTime>();
Expand Down Expand Up @@ -258,6 +267,7 @@ from jToken in json.GetByKey(UpdatedAtJsonKey).ToOption()
mDocCredentialType,
credentialAttributesType,
stateType,
statusListType,
expiresAtType,
issuedAtType,
notBeforeType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class PresentationDefinition
/// This MUST be a string. The string SHOULD provide a unique ID for the desired context.
/// </summary>
[JsonProperty("id", Required = Required.Always)]
public string Id { get; }
public string Id { get; }

/// <summary>
/// This SHOULD be a human-friendly string intended to constitute a distinctive designation of the Presentation
Expand Down
1 change: 1 addition & 0 deletions src/WalletFramework.Oid4Vc/SeviceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static IServiceCollection AddOpenIdServices(this IServiceCollection build
builder.AddSingleton<ICredentialSetService, CredentialSetService>();
builder.AddSingleton<IVctMetadataService, VctMetadataService>();
builder.AddSingleton<IAuthorizationRequestService, AuthorizationRequestService>();
builder.AddSingleton<IStatusListService, StatusListService>();

builder.AddSdJwtVcServices();

Expand Down
15 changes: 15 additions & 0 deletions src/WalletFramework.SdJwtVc/Models/Records/SdJwtRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using WalletFramework.Core.Functional;
using WalletFramework.SdJwtVc.Models.Credential;
using WalletFramework.SdJwtVc.Models.Credential.Attributes;
using WalletFramework.SdJwtVc.Models.StatusList;

namespace WalletFramework.SdJwtVc.Models.Records;

Expand Down Expand Up @@ -69,6 +70,11 @@ public sealed class SdJwtRecord : RecordBase, ICredential
/// </summary>
public DateTime? IssuedAt { get; set; }

/// <summary>
/// Tracks when the Sd-JWT was issued
/// </summary>
public Status? Status { get; set; }

/// <summary>
/// Tracks when the Sd-JWT is valid from
/// </summary>
Expand Down Expand Up @@ -143,6 +149,7 @@ public SdJwtRecord()
/// <param name="issuerId">The Id of the issuer</param>
/// <param name="encodedIssuerSignedJwt">The Issuer-signed JWT part of the SD-JWT.</param>
/// <param name="credentialSetId">The CredentialSetId.</param>
/// <param name="status">The status list.</param>
/// <param name="expiresAt">The Expiration Date.</param>
/// <param name="issuedAt">The Issued at date.</param>
/// <param name="notBefore">The valid after date.</param>
Expand All @@ -156,6 +163,7 @@ public SdJwtRecord(
string issuerId,
string encodedIssuerSignedJwt,
string credentialSetId,
Status status,
DateTime? expiresAt,
DateTime? issuedAt,
DateTime? notBefore,
Expand All @@ -175,6 +183,7 @@ public SdJwtRecord(
IssuerId = issuerId;
CredentialSetId = credentialSetId;
OneTimeUse = isOneTimeUse;
Status = status;
}

public SdJwtRecord(
Expand All @@ -193,6 +202,9 @@ public SdJwtRecord(
Claims = sdJwtDoc.GetAllSubjectClaims();
Display = display;
DisplayedAttributes = displayedAttributes;
Status = sdJwtDoc.UnsecuredPayload.SelectToken("status")?.ToObject<Status>() is not null
? sdJwtDoc.UnsecuredPayload.SelectToken("status")?.ToObject<Status>()
: null;

CredentialSetId = credentialSetId;
CredentialState = CredentialState.Active;
Expand Down Expand Up @@ -229,6 +241,9 @@ public SdJwtRecord(
Claims = sdJwtDoc.GetAllSubjectClaims();
Display = display;
DisplayedAttributes = displayedAttributes;
Status = sdJwtDoc.UnsecuredPayload.SelectToken("status")?.ToObject<Status>() is not null
? sdJwtDoc.UnsecuredPayload.SelectToken("status")?.ToObject<Status>()
: null;

CredentialSetId = credentialSetId;
CredentialState = CredentialState.Active;
Expand Down
3 changes: 3 additions & 0 deletions src/WalletFramework.SdJwtVc/Models/StatusList/Status.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace WalletFramework.SdJwtVc.Models.StatusList;

public record Status(int Idx, string Uri);
10 changes: 10 additions & 0 deletions src/WalletFramework.SdJwtVc/Services/IStatusListService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using LanguageExt;
using WalletFramework.Core.Credentials;
using WalletFramework.SdJwtVc.Models.StatusList;

namespace WalletFramework.SdJwtVc.Services;

public interface IStatusListService
{
Task<Option<CredentialState>> GetState(Status status);
}
114 changes: 114 additions & 0 deletions src/WalletFramework.SdJwtVc/Services/StatusListService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System.Collections;
using System.IdentityModel.Tokens.Jwt;
using System.IO.Compression;
using LanguageExt;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
using WalletFramework.Core.Credentials;
using WalletFramework.Core.Functional;
using WalletFramework.Core.Json;
using WalletFramework.SdJwtVc.Models.StatusList;

namespace WalletFramework.SdJwtVc.Services;

public class StatusListService(IHttpClientFactory httpClientFactory) : IStatusListService
{
public async Task<Option<CredentialState>> GetState(Status status)
{
var client = httpClientFactory.CreateClient();
var response = await client.GetAsync(status.Uri);

if (!response.IsSuccessStatusCode)
return Option<CredentialState>.None;

var content = await response.Content.ReadAsStringAsync();

var jwt = new JwtSecurityTokenHandler().ReadJwtToken(content);

var statusListClaim = jwt.Claims.Find(claim => claim.Type == "status_list");
return statusListClaim.Match(
Some: claim =>
{
var json = JObject.Parse(claim.Value);

var bits = from bitsJson in json.GetByKey("bits").ToOption()
select bitsJson.ToObject<int>();

var list = from listJson in json.GetByKey("lst").ToOption()
select Base64UrlEncoder.DecodeBytes(listJson.ToObject<string>());

return list.Match(bytes =>
{
return bits.Match(
Some: bitSize =>
{
var decompressedBytes = DecompressBytes(bytes);

var statusPerByte = 8 / bitSize;
var relevantByteLocation = status.Idx / statusPerByte;
var relevantByte = decompressedBytes[relevantByteLocation];

var startPointByteIndex = status.Idx % statusPerByte;
var startPointBitIndex = startPointByteIndex * bitSize;

var sum = 0;
for (int i = startPointBitIndex; i < startPointBitIndex + bitSize; i++)
{
var bit = new BitArray(new byte[]{relevantByte}).Get(i);
if (bit)
sum += 1 << (i % bitSize);
}

return sum switch
{
0x00 => CredentialState.Active,
0x01 => CredentialState.Revoked,
_ => Option<CredentialState>.None
};
},
None: () => Option<CredentialState>.None
);
},
None: () => Option<CredentialState>.None);
},
None: () => Option<CredentialState>.None);
}

private static byte[] DecompressBytes(byte[] compressedData)
{
if (compressedData == null || compressedData.Length == 0)
throw new ArgumentException("Compressed data cannot be null or empty");

// Check the zlib header (2 bytes)
if (compressedData.Length < 6)
throw new InvalidDataException("Compressed data is too short.");

var cmf = compressedData[0];
var flg = compressedData[1];

// Validate compression method (CM) and compression info (CINFO)
if ((cmf & 0x0F) != 8 || (cmf >> 4) > 7)
throw new InvalidDataException("Unsupported zlib compression method or info.");

// Validate the header checksum
if ((cmf * 256 + flg) % 31 != 0)
throw new InvalidDataException("Invalid zlib header checksum.");

// Remove the zlib header (first 2 bytes) and Adler-32 checksum (last 4 bytes)
var deflateDataLength = compressedData.Length - 6;
if (deflateDataLength <= 0)
throw new InvalidDataException("No deflate-compressed data found.");

var deflateData = new byte[deflateDataLength];
Array.Copy(compressedData, 2, deflateData, 0, deflateDataLength);

// Decompress using DeflateStream
using (var inputStream = new MemoryStream(deflateData))
using (var deflateStream = new DeflateStream(inputStream, CompressionMode.Decompress))
using (var outputStream = new MemoryStream())
{
deflateStream.CopyTo(outputStream);
return outputStream.ToArray();
}
}
}
2 changes: 1 addition & 1 deletion src/WalletFramework.SdJwtVc/Services/VctMetadataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public VctMetadataService(

public async Task<Option<VctMetadata>> ProcessMetadata(Vct vct)
{
if(!Uri.TryCreate(vct, UriKind.Absolute, out Uri vctUri))
if(!Uri.TryCreate(vct, UriKind.Absolute, out Uri vctUri) || vctUri.Scheme != Uri.UriSchemeHttps)
return Option<VctMetadata>.None;

var baseEndpoint = new Uri(vctUri.GetLeftPart(UriPartial.Authority));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace WalletFramework.SdJwtVc.Tests;

public class SdJwtVcHolderServiceTests
{
private readonly SdJwtVcHolderService _service;
private readonly SdJwtVcHolderService _service;

public SdJwtVcHolderServiceTests()
{
Expand Down
Loading

0 comments on commit f23104f

Please sign in to comment.