Issue and verify credentials using the Swiss Digital identity public beta, ASP.NET Core and .NET Aspire

This post shows how to issue and verify identities (verifiable credentials) using the Swiss Digital identity and trust infrastructure, (swiyu), ASP.NET Core and .NET Aspire. The swiyu infrastructure is implemented using the provided generic containers which implement the OpenID for Verifiable Credential Issuance and the OpenID for Verifiable Presentations standards as well as many other standards for implementing verifiable credentials. This infrastructure can be used to implement Swiss digital identity use cases.

Code: https://github.com/swiss-ssi-group/swiyu-aspire-aspnetcore

Demo: https://swiyuaspiremgmt.delightfulsky-453308fc.switzerlandnorth.azurecontainerapps.io/

Blogs in this series

Setup

The basic solution requires different components. A Postgres database is required which can be used by all four of the swiyu provided generic containers, a digital wallet installed on a mobile device for the end user identity credentials, two public containers which implement the issuance and verification of the credentials and the interaction with the wallet and the management applications. Two private generic containers are used for management flows and an ASP.NET Core application is used to implement the specific logic for the specific issuance and verification.

In a productive setup, the ASP.NET Core application is most likely implemented in two separate solutions, one for issuing credentials and one for verifying credentials.

Development setup

To test and debug in a development environment, the digital wallet requires a public endpoint for the issuing and verifying containers. This can be implemented using ngrok or by deploying the applications to public endpoints and using these in the development setup. I deployed the two containers to public endpoints and set the container configuration to match. The swiyu management APIs should be protected with network and application security. At present the APIs do not support OAuth and so only network security can be implemented. The APIs must be deployed in a private network.

Issuing credentials

To setup and issuer credentials, the APIs and the configuration needs to be set up as described here:

https://swiyu-admin-ch.github.io/cookbooks/onboarding-generic-issuer/

A new credential type was created and is described in the configuration file: https://raw.githubusercontent.com/swiss-ssi-group/swiyu-config-files/refs/heads/main/issuer_metadataconfigfile.json

The configuration file for the damienbod VC is defined here:

https://raw.githubusercontent.com/swiss-ssi-group/swiyu-config-files/refs/heads/main/issuer_metadataconfigfile.json

To issue a credential, a POST request can be sent to generic API management APIs. Credentials for this issuer can be created by using the API and so this API must be well protected, otherwise anyone with access could potentially issue new credentials which would mean that all credentials issued from this source cannot be trusted. Calling the API and issuing credentials can be implemented as follows:

public async Task<string> IssuerCredentialAsync(PayloadCredentialData payloadCredentialData)
{
    _logger.LogInformation("Issuer credential for data");

    var statusRegistryUrl = 
      "https://status-reg.trust-infra.swiyu-int.admin.ch/api/v1/statuslist/8cddcd3c-d0c3-49db-a62f-83a5299214d4.jwt";
    var vcType = "damienbod-vc";

    var json = GetBody(statusRegistryUrl, vcType, payloadCredentialData);

    //  curl - X POST http://localhost:8084/api/v1/credentials \
    // -H "accept: */*" \
    // -H "Content-Type: application/json" \
    // -d '

    var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");

    using HttpResponseMessage response = await _httpClient.PostAsync(
        $"{_swiyuIssuerMgmtUrl}/api/v1/credentials", jsonContent);

    if(response.IsSuccessStatusCode)
    {
        var jsonResponse = await response.Content.ReadAsStringAsync();

        return jsonResponse;
    }

    var error = await response.Content.ReadAsStringAsync();
    _logger.LogError("Could not create issue credential {issuer}", error);

    throw new Exception(error);
}

The body of the payload can be set using the supported structure from the APIs. The credential_subject_data and the metadata_credential_supported_id must match the supported credentials in the configuration.

private static string GetBody(string statusRegistryUrl, 
	string vcType, 
	PayloadCredentialData payloadCredentialData)
{
    var json = $$"""
         {
           "metadata_credential_supported_id": [
             "{{vcType}}"
           ],
           "credential_subject_data": {
             "firstName": "{{payloadCredentialData.FirstName}}",
             "lastName": "{{payloadCredentialData.LastName}}",
             "birthDate": "{{payloadCredentialData.BirthDate}}"
           },
           "offer_validity_seconds": 86400,
           "credential_valid_until": "2030-01-01T19:23:24Z",
           "credential_valid_from": "2025-01-01T18:23:24Z",
           "status_lists": [
             "{{statusRegistryUrl}}"
           ]
         }
         """;

    return json;
}

A Razor page UI is implemented to call this method and return a QR code for the end user to scan and to add the credential to their digital wallet. The credential is added to the wallet and can be used by anyone or anything with access to the wallet. Issuing credentials require authentication and authorization in most use cases. Access to the wallet also requires authentication and authorization.

using Swiyu.Aspire.Mgmt.Services;
using ImageMagick;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Net.Codecrete.QrCodeGenerator;
using System.Text.Json;

namespace Swiyu.Aspire.Mgmt.Pages;

public class CreateCredentialIssuerModel : PageModel
{
    private readonly IssuerService _issuerService;

    [BindProperty]
    public string? QrCodeUrl { get; set; } = null;

    [BindProperty]
    public byte[] QrCodePng { get; set; } = [];

    [BindProperty]
    public string? ManagementId { get; set; } = null;

    public CreateCredentialIssuerModel(IssuerService issuerService)
    {
        _issuerService = issuerService;
    }

    public void OnGet()
    {
    }

    /// <summary>
    /// QrCode.Ecc.Low, QrCode.Ecc.Medium, QrCode.Ecc.Quartile, QrCode.Ecc.High
    /// </summary>
    /// <returns></returns>
    public async Task OnPostAsync()
    {
        var vci = await _issuerService.IssuerCredentialAsync(
            new PayloadCredentialData
            {
                FirstName = "damienbod",
                LastName = "cool apps",
                BirthDate = DateTime.UtcNow.ToShortDateString()
            });

        var data = JsonSerializer.Deserialize<CredentialIssuerModel>(vci);

        var qrCode = QrCode.EncodeText(data!.offer_deeplink, QrCode.Ecc.Quartile);
        QrCodePng = qrCode.ToPng(20, 4, MagickColors.Black, MagickColors.White);

        QrCodeUrl = data!.offer_deeplink;
        ManagementId = data!.management_id;
    }
}

The UI displays the QR Code.

Once scanned, Javascript is used to check the status of the credential and update the UI with the status. The code calls the status API:

    public async Task<StatusModel?> GetIssuanceStatus(string id)
    {
        using HttpResponseMessage response = await _httpClient.GetAsync(
            $"{_swiyuIssuerMgmtUrl}/api/v1/credentials/{id}/status");

        if (response.IsSuccessStatusCode)
        {
            var jsonResponse = await response.Content.ReadAsStringAsync();

            if(jsonResponse == null)
            {
                _logger.LogError("GetIssuanceStatus no data returned from Swiyu");
                return new StatusModel { id="none", status="ERROR"};
            }

            return JsonSerializer.Deserialize<StatusModel>(jsonResponse);
        }

        var error = await response.Content.ReadAsStringAsync();
        _logger.LogError("Could not create issue credential {issuer}", error);

        throw new Exception(error);
    }
}

Verifying credentials

The credentials can be verified in a similar way to issuing credentials. The swiyu public beta has documentation for setting this up:

https://swiyu-admin-ch.github.io/cookbooks/onboarding-generic-verifier/

The verification service class is used to call the APIs:

using System.Text;
using System.Text.Json;

namespace Swiyu.Aspire.Mgmt.Services;

public class VerificationService
{
    private readonly ILogger<VerificationService> _logger;
    private readonly string? _swiyuVerifierMgmtUrl;
    private readonly string? _issuerId;
    private readonly HttpClient _httpClient;

    public VerificationService(IHttpClientFactory httpClientFactory,
        ILoggerFactory loggerFactory, IConfiguration configuration)
    {
        _swiyuVerifierMgmtUrl = configuration["SwiyuVerifierMgmtUrl"];
        _issuerId = configuration["ISSUER_ID"];
        _httpClient = httpClientFactory.CreateClient();
        _logger = loggerFactory.CreateLogger<VerificationService>();
    }

    /// <summary>
    /// curl - X POST http://localhost:8082/api/v1/verifications \
    ///       -H "accept: application/json" \
    ///       -H "Content-Type: application/json" \
    ///       -d '
    /// </summary>
    public async Task<string> CreateBetaIdVerificationPresentationAsync()
    {
        _logger.LogInformation("Creating verification presentation");

        // from "betaid-sdjwt"
        var acceptedIssuerDid = "did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527";

        var inputDescriptorsId = Guid.NewGuid().ToString();
        var presentationDefinitionId = "00000000-0000-0000-0000-000000000000"; // Guid.NewGuid().ToString();

        var json = GetBetaIdVerificationPresentationBody(inputDescriptorsId,
            presentationDefinitionId, acceptedIssuerDid, "betaid-sdjwt");

        return await SendCreateVerificationPostRequest(json);
    }

    /// <summary>
    /// curl - X POST http://localhost:8082/api/v1/verifications \
    ///       -H "accept: application/json" \
    ///       -H "Content-Type: application/json" \
    ///       -d '
    /// </summary>
    public async Task<string> CreateDamienbodVerificationPresentationAsync()
    {
        _logger.LogInformation("Creating verification presentation");

        var inputDescriptorsId = Guid.NewGuid().ToString();
        var presentationDefinitionId = "00000000-0000-0000-0000-000000000000"; // Guid.NewGuid().ToString();

        var json = GetDataForLocalCredential(inputDescriptorsId,
           presentationDefinitionId, _issuerId!, "damienbod-vc");

        return await SendCreateVerificationPostRequest(json);
    }

    public async Task<VerificationManagementModel?> GetVerificationStatus(string verificationId)
    {
        using HttpResponseMessage response = await _httpClient.GetAsync(
            $"{_swiyuVerifierMgmtUrl}/api/v1/verifications/{verificationId}");

        if (response.IsSuccessStatusCode)
        {
            var jsonResponse = await response.Content.ReadAsStringAsync();

            if (jsonResponse == null)
            {
                _logger.LogError("GetVerificationStatus no data returned from Swiyu");
                return null;
            }

            //  state: PENDING, SUCCESS, FAILED
            return JsonSerializer.Deserialize<VerificationManagementModel>(jsonResponse);
        }

        var error = await response.Content.ReadAsStringAsync();
        _logger.LogError("Could not create verification presentation {vp}", error);

        throw new Exception(error);
    }

    private async Task<string> SendCreateVerificationPostRequest(string json)
    {
        var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync(
                    $"{_swiyuVerifierMgmtUrl}/api/v1/verifications", jsonContent);
        if (response.IsSuccessStatusCode)
        {
            var jsonResponse = await response.Content.ReadAsStringAsync();

            return jsonResponse;
        }

        var error = await response.Content.ReadAsStringAsync();
        _logger.LogError("Could not create verification presentation {vp}", error);

        throw new Exception(error);
    }

    private string GetDataForLocalCredential(string inputDescriptorsId, 
           string presentationDefinitionId, 
           string issuer, 
           string vcType)
    {
        // jwt_secured_authorization_request disabled, need docs for this
        var json = $$"""
             {
                 "accepted_issuer_dids": [ "{{issuer}}" ],
                 "jwt_secured_authorization_request": true,
                 "presentation_definition": {
                     "id": "{{presentationDefinitionId}}",
                     "name": "Verification",
                     "purpose": "Verify damienbod VC",
                     "input_descriptors": [
                         {
                             "id": "{{inputDescriptorsId}}",
                             "format": {
                                 "vc+sd-jwt": {
                                     "sd-jwt_alg_values": [
                                         "ES256"
                                     ],
                                     "kb-jwt_alg_values": [
                                         "ES256"
                                     ]
                                 }
                             },
                             "constraints": {
             	                "fields": [
             		                {
             			                "path": [ "$.vct" ],
             			                "filter": {
             				                "type": "string",
             				                "const": "{{vcType}}"
             			                }
             		                },
                                    {
                                        "path": [ "$.firstName" ]
                                    },
                                    {
                                        "path": [ "$.lastName" ]
                                    },
             		                {
             			                "path": [ "$.birthDate" ]
             		                }
             	                ]
                             }
                         }
                     ]
                 }
             }
             """;

        return json;
    }

    private string GetBetaIdVerificationPresentationBody(string inputDescriptorsId, 
               string presentationDefinitionId, 
               string acceptedIssuerDid, 
               string vcType)
    {
        var json = $$"""
             {
                 "accepted_issuer_dids": [ "{{acceptedIssuerDid}}" ],
                 "jwt_secured_authorization_request": true,
                 "presentation_definition": {
                     "id": "{{presentationDefinitionId}}",
                     "name": "Verification",
                     "purpose": "Verify using Beta ID",
                     "input_descriptors": [
                         {
                             "id": "{{inputDescriptorsId}}",
                             "format": {
                                 "vc+sd-jwt": {
                                     "sd-jwt_alg_values": [
                                         "ES256"
                                     ],
                                     "kb-jwt_alg_values": [
                                         "ES256"
                                     ]
                                 }
                             },
                             "constraints": {
             	                "fields": [
             		                {
             			                "path": [
             				                "$.vct"
             			                ],
             			                "filter": {
             				                "type": "string",
             				                "const": "{{vcType}}"
             			                }
             		                },
             		                {
             			                "path": [
             				                "$.birth_date"
             			                ]
             		                }
             	                ]
                             }
                         }
                     ]
                 }
             }
             """;

        return json;
    }
}

A Razor page is used to implement the UI.

using Swiyu.Aspire.Mgmt.Services;
using ImageMagick;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Net.Codecrete.QrCodeGenerator;
using System.Text.Json;

namespace Swiyu.Aspire.Mgmt.Pages;

public class VerifyDamienbodCredentialModel : PageModel
{
    private readonly VerificationService _verificationService;
    private readonly string? _swiyuOid4vpUrl;

    [BindProperty]
    public string? VerificationId { get; set; }

    [BindProperty]
    public string? QrCodeUrl { get; set; } = string.Empty;

    [BindProperty]
    public byte[] QrCodePng { get; set; } = [];

    public VerifyDamienbodCredentialModel(VerificationService verificationService,
        IConfiguration configuration)
    {
        _verificationService = verificationService;
        _swiyuOid4vpUrl = configuration["SwiyuOid4vpUrl"];
        QrCodeUrl = QrCodeUrl.Replace("{OID4VP_URL}", _swiyuOid4vpUrl);
    }

    public void OnGet()
    {
    }

    public async Task OnPostAsync()
    {
        var presentation = await _verificationService
            .CreateDamienbodVerificationPresentationAsync();

        var verificationResponse = JsonSerializer.Deserialize<CreateVerificationPresentationModel>(presentation);
        // verification_url
        QrCodeUrl = verificationResponse!.verification_url;

        var qrCode = QrCode.EncodeText(verificationResponse!.verification_url, QrCode.Ecc.Quartile);
        QrCodePng = qrCode.ToPng(20, 4, MagickColors.Black, MagickColors.White);

        VerificationId = verificationResponse.id;
    }
}

The UI can be used to start the verification process.

Verify public beta credentials

Any credentials issued by the swiyu public beta can be verified using your own infrastructure. You only need to know the issuer DID and the verifiable credential type. The subject details are also required to request the data (input_descriptors and path).

Notes and conclusions

The solution is a work-in-progress and I plan to implement some specific use cases based on this setup. I am open to improvements and recommendations. I plan to maintain this as my reference implementation. Please create issues or PRs in the associated Github repository.

Open issues:

The generic container APIs should support OAuth and at present have weak security headers applied. The solution should use an automatic infrastructure deployment, I normally use terraform. An API gateway can be used to protect the container APIs as well as hardening the API endpoints. The public deployments should implement some sort of DDoS protection. Cloudflare has a good solution for this. Deep links need to be implemented in the UI solution.

Links

https://swiyu-admin-ch.github.io/

https://www.eid.admin.ch/en/public-beta-e

https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview

https://www.npmjs.com/package/ngrok

https://swiyu-admin-ch.github.io/specifications/interoperability-profile/

https://andrewlock.net/converting-a-docker-compose-file-to-aspire/

https://swiyu-admin-ch.github.io/cookbooks/onboarding-generic-verifier/

https://github.com/orgs/swiyu-admin-ch/projects/2/views/2

Standards

https://identity.foundation/trustdidweb/

https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html

https://openid.net/specs/openid-4-verifiable-presentations-1_0.html

https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/

https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/

https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/

https://www.w3.org/TR/vc-data-model-2.0/

6 comments

  1. […] Issuer and verify credentials using the Swiss Digital identity public beta, ASP.NET Core and .NET As… (Damien Bowden) […]

  2. Unknown's avatar

    […] Issue and verify credentials using the Swiss Digital identity public beta, ASP.NET Core and .NET Asp… […]

  3. Unknown's avatar

    […] Issue and verify credentials using the Swiss Digital identity public beta, ASPi.NET Core and .NET As… withdrawal address is provided after out sponsors […]

  4. Unknown's avatar

    […] Issue and verify credentials using the Swiss Digital identity public beta, ASP.NET Core and .NET Asp… […]

  5. Unknown's avatar

    […] Issue and verify credentials using the Swiss Digital identity public beta, ASP.NET Core and .NET Asp… […]

  6. Unknown's avatar

    […] Issue and verify credentials using the Swiss Digital identity public beta, ASP.NET Core and .NET Asp… […]

Leave a reply to Dew Drop – August 4, 2025 (#4472) – Morning Dew by Alvin Ashcraft Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.