|
| 1 | +// Copyright (c) SimpleIdServer. All rights reserved. |
| 2 | +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. |
| 3 | +using Microsoft.IdentityModel.JsonWebTokens; |
| 4 | +using Microsoft.IdentityModel.Tokens; |
| 5 | +using SimpleIdServer.CredentialIssuer.Api.CredentialIssuer; |
| 6 | +using SimpleIdServer.Did.Crypto; |
| 7 | +using System.Security.Cryptography; |
| 8 | +using System.Text; |
| 9 | +using System.Text.Json; |
| 10 | +using System.Text.Json.Nodes; |
| 11 | +using System.Web; |
| 12 | + |
| 13 | +namespace SimpleIdServer.CredentialIssuer.Console; |
| 14 | + |
| 15 | +public class EsbiWallet |
| 16 | +{ |
| 17 | + private const string url = "https://api-conformance.ebsi.eu/conformance/v3/issuer-mock/.well-known/openid-credential-issuer"; |
| 18 | + private const string publicKey = "z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbpMAoXtZtunruYnM4gCV65AKAUX2AwEReRhEaf3BRQNJArZPwQdmf9ENZcF8VT13a58WsHeVjJtvAKKPYEibaEfdUxvU7sgxEUTJpjEkq6BJKrRV1JQ1CqhYvGbmJ1WyoUQ"; |
| 19 | + private const string did = $"did:key:{publicKey}"; |
| 20 | + private const string refDid = $"{did}#{publicKey}"; |
| 21 | + |
| 22 | + public static async Task RegisterEsbiWalletForConformance() |
| 23 | + { |
| 24 | + using (var httpClient = new HttpClient()) |
| 25 | + { |
| 26 | + var (challenge, verifier) = PkceGenerate(); |
| 27 | + var openidCredentialIssuer = GetOpenidCredentialIssuer(httpClient).Result; |
| 28 | + var configuration = GetAuthorizationEndpoint(httpClient, openidCredentialIssuer.AuthorizationServer).Result; |
| 29 | + var parameters = ExecuteAuthorizationRequest(httpClient, configuration.authorizationEndpoint, challenge, openidCredentialIssuer.CredentialIssuer).Result; |
| 30 | + var redirectUri = parameters["redirect_uri"]; |
| 31 | + var nonce = parameters["nonce"]; |
| 32 | + var state = parameters["state"]; |
| 33 | + var postAuthResult = ExecutePostAuthorizationRequest(httpClient, redirectUri, openidCredentialIssuer.CredentialIssuer, nonce, state).Result; |
| 34 | + var tokenResult = GetToken(httpClient, configuration.tokenEndpoint, postAuthResult["code"], verifier).Result; |
| 35 | + GetCredential(httpClient, openidCredentialIssuer.CredentialEndpoint, openidCredentialIssuer.CredentialIssuer, tokenResult.cNonce, tokenResult.accessToken).Wait(); |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + private static async Task<ESBICredentialIssuerResult> GetOpenidCredentialIssuer(HttpClient httpClient) |
| 40 | + { |
| 41 | + var url = "https://api-conformance.ebsi.eu/conformance/v3/issuer-mock/.well-known/openid-credential-issuer"; |
| 42 | + var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); |
| 43 | + var httpResult = await httpClient.SendAsync(requestMessage); |
| 44 | + var json = await httpResult.Content.ReadAsStringAsync(); |
| 45 | + var openidCredentialIssuer = JsonSerializer.Deserialize<ESBICredentialIssuerResult>(json); |
| 46 | + return openidCredentialIssuer; |
| 47 | + } |
| 48 | + |
| 49 | + private static async Task<(string authorizationEndpoint, string issuer, string tokenEndpoint)> GetAuthorizationEndpoint(HttpClient httpClient, string authUrl) |
| 50 | + { |
| 51 | + var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{authUrl}/.well-known/openid-configuration"); |
| 52 | + var httpResult = await httpClient.SendAsync(requestMessage); |
| 53 | + var json = await httpResult.Content.ReadAsStringAsync(); |
| 54 | + var jsonObj = JsonObject.Parse(json); |
| 55 | + return (jsonObj["authorization_endpoint"].ToString(), jsonObj["issuer"].ToString(), jsonObj["token_endpoint"].ToString()); |
| 56 | + } |
| 57 | + |
| 58 | + private static async Task<Dictionary<string, string>> ExecuteAuthorizationRequest(HttpClient httpClient, string url, string challenge, string aud) |
| 59 | + { |
| 60 | + var uriBuilder = new UriBuilder(url); |
| 61 | + var authorizationDetails = new JsonArray |
| 62 | +{ |
| 63 | + new JsonObject |
| 64 | + { |
| 65 | + { "type", "openid_credential" }, |
| 66 | + { "format", "jwt_vc" }, |
| 67 | + { "types", new JsonArray |
| 68 | + { |
| 69 | + "VerifiableCredential", |
| 70 | + "VerifiableAttestation", |
| 71 | + "CTIssueQualificationCredential" |
| 72 | + } }, |
| 73 | + { "locations", new JsonArray |
| 74 | + { |
| 75 | + aud |
| 76 | + } } |
| 77 | + } |
| 78 | +}; |
| 79 | + var clientMetadata = new JsonObject |
| 80 | +{ |
| 81 | + { "response_types_supported", |
| 82 | + new JsonArray |
| 83 | + { |
| 84 | + "vp_token", "id_token" |
| 85 | + } |
| 86 | + }, |
| 87 | + { "authorization_endpoint", "openid://"} |
| 88 | +}; |
| 89 | + var dic = new Dictionary<string, string> |
| 90 | +{ |
| 91 | + { "response_type", "code" }, |
| 92 | + { "scope", "openid" }, |
| 93 | + // { "issuer_state", "issuer-state" }, |
| 94 | + { "state", "client-state" }, |
| 95 | + { "client_id", did }, |
| 96 | + { "authorization_details", HttpUtility.UrlEncode(authorizationDetails.ToJsonString()) }, |
| 97 | + { "redirect_uri", "openid://" }, |
| 98 | + { "nonce", "nonce" }, |
| 99 | + { "code_challenge", challenge }, |
| 100 | + { "code_challenge_method", "S256" }, |
| 101 | + { "client_metadata", HttpUtility.UrlEncode(clientMetadata.ToJsonString()) } |
| 102 | +}; |
| 103 | + uriBuilder.Query = string.Join("&", dic.Select(kvp => $"{kvp.Key}={kvp.Value}")); |
| 104 | + var requestMessage = new HttpRequestMessage |
| 105 | + { |
| 106 | + RequestUri = uriBuilder.Uri |
| 107 | + }; |
| 108 | + var httpResult = await httpClient.SendAsync(requestMessage); |
| 109 | + var result = httpResult.Headers.Location.AbsoluteUri; |
| 110 | + uriBuilder = new UriBuilder(result); |
| 111 | + var queryParameters = uriBuilder.Query.Trim('?').Split('&').Select(s => s.Split('=')).ToDictionary(arr => arr[0], arr => arr[1]); |
| 112 | + return queryParameters; |
| 113 | + } |
| 114 | + |
| 115 | + private static async Task<Dictionary<string, string>> ExecutePostAuthorizationRequest(HttpClient httpClient, string url, string aud, string nonce, string state) |
| 116 | + { |
| 117 | + var serializedPrivateKey = System.IO.File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "privatekey.json")); |
| 118 | + var signatureKey = SignatureKeySerializer.Deserialize(serializedPrivateKey); |
| 119 | + var signingCredentials = signatureKey.BuildSigningCredentials(refDid); |
| 120 | + var handler = new JsonWebTokenHandler(); |
| 121 | + var securityTokenDescriptor = new SecurityTokenDescriptor |
| 122 | + { |
| 123 | + IssuedAt = DateTime.UtcNow, |
| 124 | + SigningCredentials = signingCredentials, |
| 125 | + Audience = aud |
| 126 | + }; |
| 127 | + var claims = new Dictionary<string, object> |
| 128 | +{ |
| 129 | + { "nonce", nonce }, |
| 130 | + { "iss", did }, |
| 131 | + { "sub", did } |
| 132 | +}; |
| 133 | + securityTokenDescriptor.Claims = claims; |
| 134 | + var token = handler.CreateToken(securityTokenDescriptor); |
| 135 | + var requestMessage = new HttpRequestMessage |
| 136 | + { |
| 137 | + RequestUri = new Uri(HttpUtility.UrlDecode(url)), |
| 138 | + Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>> |
| 139 | + { |
| 140 | + new KeyValuePair<string, string>("id_token", token), |
| 141 | + new KeyValuePair<string, string>("state", state) |
| 142 | + }), |
| 143 | + Method = HttpMethod.Post |
| 144 | + }; |
| 145 | + var httpResult = await httpClient.SendAsync(requestMessage); |
| 146 | + var result = httpResult.Headers.Location.AbsoluteUri; |
| 147 | + var uriBuilder = new UriBuilder(result); |
| 148 | + var queryParameters = uriBuilder.Query.Trim('?').Split('&').Select(s => s.Split('=')).ToDictionary(arr => arr[0], arr => arr[1]); |
| 149 | + return queryParameters; |
| 150 | + } |
| 151 | + |
| 152 | + private static async Task GetCredential(HttpClient httpClient, string credentialEndpoint, string aud, string nonce, string accessToken) |
| 153 | + { |
| 154 | + var serializedPrivateKey = System.IO.File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "privatekey.json")); |
| 155 | + var signatureKey = SignatureKeySerializer.Deserialize(serializedPrivateKey); |
| 156 | + var signingCredentials = signatureKey.BuildSigningCredentials(refDid); |
| 157 | + var handler = new JsonWebTokenHandler(); |
| 158 | + var securityTokenDescriptor = new SecurityTokenDescriptor |
| 159 | + { |
| 160 | + IssuedAt = DateTime.UtcNow, |
| 161 | + SigningCredentials = signingCredentials, |
| 162 | + Audience = aud, |
| 163 | + TokenType = "openid4vci-proof+jwt" |
| 164 | + }; |
| 165 | + var claims = new Dictionary<string, object> |
| 166 | +{ |
| 167 | + { "iss", did }, |
| 168 | + { "nonce", nonce } |
| 169 | +}; |
| 170 | + securityTokenDescriptor.Claims = claims; |
| 171 | + var proofJwt = handler.CreateToken(securityTokenDescriptor); |
| 172 | + var proofRequest = new JsonObject(); |
| 173 | + proofRequest.Add("proof_type", "jwt"); |
| 174 | + proofRequest.Add("jwt", proofJwt); |
| 175 | + var request = new JsonObject |
| 176 | +{ |
| 177 | + { "types", new JsonArray |
| 178 | + { |
| 179 | + "VerifiableCredential", |
| 180 | + "VerifiableAttestation", |
| 181 | + "CTIssueQualificationCredential" |
| 182 | + } }, |
| 183 | + { "format", "jwt_vc" } |
| 184 | +}; |
| 185 | + request.Add("proof", proofRequest); |
| 186 | + var requestMessage = new HttpRequestMessage |
| 187 | + { |
| 188 | + RequestUri = new Uri(HttpUtility.UrlDecode(credentialEndpoint)), |
| 189 | + Content = new StringContent(request.ToJsonString(), Encoding.UTF8, "application/json"), |
| 190 | + Method = HttpMethod.Post |
| 191 | + }; |
| 192 | + requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); |
| 193 | + await httpClient.SendAsync(requestMessage); |
| 194 | + } |
| 195 | + |
| 196 | + private static async Task<(string accessToken, string idToken, string cNonce)> GetToken(HttpClient httpClient, string url, string authorizationCode, string codeVerifier) |
| 197 | + { |
| 198 | + var requestMessage = new HttpRequestMessage |
| 199 | + { |
| 200 | + RequestUri = new Uri(url), |
| 201 | + Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>> |
| 202 | + { |
| 203 | + new KeyValuePair<string, string>("grant_type", "authorization_code"), |
| 204 | + new KeyValuePair<string, string>("client_id", did), |
| 205 | + new KeyValuePair<string, string>("code", authorizationCode), |
| 206 | + new KeyValuePair<string, string>("code_verifier", codeVerifier) |
| 207 | + }), |
| 208 | + Method = HttpMethod.Post |
| 209 | + }; |
| 210 | + var httpResult = await httpClient.SendAsync(requestMessage); |
| 211 | + var content = await httpResult.Content.ReadAsStringAsync(); |
| 212 | + var jObj = JsonObject.Parse(content); |
| 213 | + return (jObj["access_token"].ToString(), jObj["id_token"].ToString(), jObj["c_nonce"].ToString()); |
| 214 | + } |
| 215 | + |
| 216 | + private static (string codeChallenge, string verifier) PkceGenerate(int size = 32) |
| 217 | + { |
| 218 | + using var rng = RandomNumberGenerator.Create(); |
| 219 | + var randomBytes = new byte[size]; |
| 220 | + rng.GetBytes(randomBytes); |
| 221 | + var verifier = Base64UrlEncode(randomBytes); |
| 222 | + |
| 223 | + var buffer = Encoding.UTF8.GetBytes(verifier); |
| 224 | + var hash = SHA256.Create().ComputeHash(buffer); |
| 225 | + var challenge = Base64UrlEncode(hash); |
| 226 | + |
| 227 | + return (challenge, verifier); |
| 228 | + } |
| 229 | + private static string Base64UrlEncode(byte[] data) => |
| 230 | + Convert.ToBase64String(data) |
| 231 | + .Replace("+", "-") |
| 232 | + .Replace("/", "_") |
| 233 | + .TrimEnd('='); |
| 234 | +} |
0 commit comments