使用瑞士数字身份公测版、ASP.NET Core和.NET Aspire颁发与验证凭证

本文详细介绍了如何利用瑞士数字身份基础设施、ASP.NET Core和.NET Aspire实现可验证凭证的颁发与验证,包含完整的技术架构、代码示例和实现细节。

使用瑞士数字身份公测版、ASP.NET Core和.NET Aspire颁发与验证凭证

本文展示了如何使用瑞士数字身份和信任基础设施(swiyu)、ASP.NET Core和.NET Aspire来颁发和验证身份(可验证凭证)。swiyu基础设施使用提供的通用容器实现,这些容器实现了OpenID for Verifiable Credential Issuance和OpenID for Verifiable Presentations标准,以及许多其他实现可验证凭证的标准。该基础设施可用于实现瑞士数字身份用例。

代码:https://github.com/swiss-ssi-group/swiyu-aspire-aspnetcore
演示:https://swiyuaspiremgmt.delightfulsky-453308fc.switzerlandnorth.azurecontainerapps.io/

设置

基本解决方案需要不同的组件。需要一个Postgres数据库,可供所有四个swiyu提供的通用容器使用;一个安装在移动设备上的数字钱包,用于终端用户身份凭证;两个公共容器,用于实现凭证的颁发和验证以及与钱包和管理应用程序的交互;两个私有通用容器用于管理流程;一个ASP.NET Core应用程序用于实现特定颁发和验证的逻辑。

在生产设置中,ASP.NET Core应用程序很可能在两个独立的解决方案中实现,一个用于颁发凭证,另一个用于验证凭证。

开发设置

为了在开发环境中测试和调试,数字钱包需要用于颁发和验证容器的公共端点。这可以使用ngrok实现,或者通过将应用程序部署到公共端点并在开发设置中使用这些端点。作者将两个容器部署到公共端点,并设置容器配置以匹配。swiyu管理API应受到网络和应用程序安全保护。目前这些API不支持OAuth,因此只能实现网络安全。API必须部署在私有网络中。

颁发凭证

要设置和颁发凭证,需要按照以下说明设置API和配置:

创建了一个新的凭证类型,并在配置文件中描述:

damienbod VC的配置文件在此定义:

要颁发凭证,可以向通用API管理API发送POST请求。可以通过使用API为此颁发者创建凭证,因此必须很好地保护此API,否则任何有权访问的人都有可能颁发新凭证,这意味着从此来源颁发的所有凭证都不可信。调用API和颁发凭证可以实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

{

    
    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);
    
    var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
    
    using HttpResponseMessage response = await _httpClient.PostAsync(

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

    
    throw new Exception(error);
}

有效负载的主体可以使用API支持的结构设置。credential_subject_datametadata_credential_supported_id必须与配置中支持的凭证匹配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}

实现了一个Razor页面UI来调用此方法,并为终端用户返回一个QR码,以扫描并将凭证添加到他们的数字钱包中。凭证被添加到钱包中,可以被任何有权访问钱包的人或事物使用。在大多数用例中,颁发凭证需要身份验证和授权。访问钱包也需要身份验证和授权。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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;


{

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

    {

    }
    
    public void OnGet()
    {
    }
    
    public async Task OnPostAsync()
    {

            new PayloadCredentialData
            {
                FirstName = "damienbod",
                LastName = "cool apps",
                BirthDate = DateTime.UtcNow.ToShortDateString()
            });
        

        
        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;
    }
}

UI显示QR码。

扫描后,使用Javascript检查凭证的状态并用状态更新UI。代码调用状态API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public async Task<StatusModel?> GetIssuanceStatus(string id)
{
    using HttpResponseMessage response = await _httpClient.GetAsync(

    
    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();

    
    throw new Exception(error);
}

验证凭证

凭证的验证方式与颁发凭证类似。swiyu公测版有设置此功能的文档: https://swiyu-admin-ch.github.io/cookbooks/onboarding-generic-verifier/

验证服务类用于调用API:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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 HttpClient _httpClient;
    
    public VerificationService(IHttpClientFactory httpClientFactory,
        ILoggerFactory loggerFactory, IConfiguration configuration)
    {
        _swiyuVerifierMgmtUrl = configuration["SwiyuVerifierMgmtUrl"];

        _httpClient = httpClientFactory.CreateClient();
        _logger = loggerFactory.CreateLogger<VerificationService>();
    }
    
    public async Task<string> CreateBetaIdVerificationPresentationAsync()
    {
        _logger.LogInformation("Creating verification presentation");
        

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

        
        return await SendCreateVerificationPostRequest(json);
    }
    
    public async Task<string> CreateDamienbodVerificationPresentationAsync()
    {
        _logger.LogInformation("Creating verification presentation");
        
        var inputDescriptorsId = Guid.NewGuid().ToString();
        var presentationDefinitionId = "00000000-0000-0000-0000-000000000000";
        
        var json = GetDataForLocalCredential(inputDescriptorsId,

        
        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;
            }
            
            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 vcType)
    {
        var json = $$"""
            {

                 "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 vcType)
    {
        var json = $$"""
            {

                 "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;
    }
}

使用Razor页面实现UI。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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);
        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;
    }
}

UI可用于启动验证过程。

验证公测版凭证

swiyu公测版颁发的任何凭证都可以使用您自己的基础设施进行验证。您只需要知道颁发者DID和可验证凭证类型。还需要主题详细信息来请求数据(input_descriptors和path)。

说明与结论

该解决方案正在进行中,作者计划基于此设置实现一些特定用例。作者欢迎改进和建议。计划将其作为参考实现进行维护。请在相关的Github存储库中创建问题或PR。

待解决问题

  • 通用容器API应支持OAuth,目前应用的安全标头较弱
  • 解决方案应使用自动基础设施部署,通常使用terraform
  • 可以使用API网关保护容器API以及加固API端点
  • 公共部署应实现某种DDoS保护,Cloudflare有很好的解决方案
  • 需要在UI解决方案中实现深度链接

链接

标准

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计