Hoppa till innehåll

Freja eID v1.0 signatur, validering och autentisering i ASP.NET Core

Jag skapar en tjänst för signaturer, validering av signaturer och autentisering med hjälp av Freja eID v1.0 i denna handledning. Freja eID är en global tjänst för elektronisk identifiering (eID) som kan användas för e-autentisering och e-signaturer. Användare av Freja eID använder en smarttelefonapplikation för att logga in och skapa signaturer, varje användare kan kontrollera hur deras eID får användas och de kan se historik i en webbportal.

Denna kod har testats och fungerar med Google Chrome (75.0.3770.100), Mozilla Firefox (75.0) och Microsoft Edge (81.0.416.62), detta utan polyfills. Koden fungerar i Internet Explorer (11.829.17134.0) med polyfills för Array.from, Promise, String.prototype.padStart, TextEncoder, WebCrypto, XMLHttpRequest, Array.prototype.includes, CustomEvent, Array.prototype.closest, Array.prototype.remove, String.prototype.endsWith and String.prototype.includes och transpilering. Om du vill stödja äldre webbläsare kan du läsa vårt inlägg om transpilering och komplettering av JavaScript. Den här koden har beroenden till annytab.effects, Font Awesome, annytab.notifier och js-spark-md5.

Förberedelser och inställningar

Denna tjänst implementeras med HTML, JavaScript och C-sharp i ASP.NET Core. Du måste hämta ett klienttestcertifikat genom att skicka ett e-postmeddelande till Freja eID. Klienttestcertifikatet används för att ansluta till REST API:et. Du måste också hämta JWS-certifikat för att kunna validera signaturer.

Du måste ladda ner Freja eID-appen till din smarttelefon och starta applikationen i testläge, instruktioner hittar du här. När du har skapat ett testkonto kan du uppgradera ditt konto från BASIC till EXTENDED och sedan till PLUS via Test Vetting Portal. Jag använder en application.json-fil för att spara inställningar avseende min freja-klient och en FrejaOptions-klass för att komma åt dessa inställningar.

  1. {
  2. "Logging": {
  3. "LogLevel": {
  4. "Default": "Debug",
  5. "System": "Information",
  6. "Microsoft": "Information"
  7. }
  8. },
  9. "AllowedHosts": "*",
  10. "FrejaOptions": {
  11. "BaseAddress": "https://services.test.frejaeid.com",
  12. "JwsCertificate": "MIIEETCCAvmgAwIBAgIUTeCJ0hz3mbtyONBEiap7su74LZwwDQYJKoZIhvcNAQELBQAwgYMxCzAJBgNVBAYTAlNFMRIwEAYDVQQHEwlTdG9ja2hvbG0xFDASBgNVBGETCzU1OTExMC00ODA2MR0wGwYDVQQKExRWZXJpc2VjIEZyZWphIGVJRCBBQjENMAsGA1UECxMEVGVzdDEcMBoGA1UEAxMTUlNBIFRFU1QgSXNzdWluZyBDQTAeFw0xNzA3MTIxNTIwMTNaFw0yMDA3MTIxNTIwMTNaMIGKMQswCQYDVQQGEwJTRTESMBAGA1UEBxMJU3RvY2tob2xtMRQwEgYDVQRhEws1NTkxMTAtNDgwNjEdMBsGA1UEChMUVmVyaXNlYyBGcmVqYSBlSUQgQUIxDTALBgNVBAsTBFRlc3QxIzAhBgNVBAMTGkZyZWphIGVJRCBURVNUIE9yZyBTaWduaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgMINs87TiouDPSSmpn05kZv9TN8XdopcHnElp6ElJLpQh3oYGIL4B71oIgF3r8zRWq8kQoJlYMugmhsld0r0EsUJbsrcjBJ5CJ1WYZg1Vu8FpYLKoaFRI/qxT6xCMvd238Q99Sdl6G6O9sQQoFq10EaYBa970Tl3nDziQQ6bbSNkZoOYIZoicx4+1XFsrGiru8o8QIyc3g0eSgrd3esbUkuk0eH65SeaaOCrsaCOpJUqEziD+el4R6d40dTz/uxWmNpGKF4BmsNWeQi9b4gDYuFqNYhs7bnahvkK6LvtDThV79395px/oUz5BEDdVwjxPJzgaAuUHE+6A1dMapkjsQIDAQABo3QwcjAOBgNVHQ8BAf8EBAMCBsAwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRqfIoPnXAOHNpfLaA8Jl+I6BW/nDASBgNVHSAECzAJMAcGBSoDBAUKMB0GA1UdDgQWBBT7j90x8xG2Sg2p7dCiEpsq3mo5PTANBgkqhkiG9w0BAQsFAAOCAQEAaKEIpRJvhXcN3MvP7MIMzzuKh2O8kRVRQAoKCj0K0R9tTUFS5Ang1fEGMxIfLBohOlRhXgKtqJuB33IKzjyA/1IBuRUg2bEyecBf45IohG+vn4fAHWTJcwVChHWcOUH+Uv1g7NX593nugv0fFdPqt0JCnsFx2c/r9oym+VPP7p04BbXzYUk+17qmFBP/yNlltjzfeVnIOk4HauR9i94FrfynuZLuItB6ySCVmOlfA0r1pHv5sofBEirhwceIw1EtFqEDstI+7XZMXgDwSRYFc1pTjrWMaua2UktmJyWZPfIY69pi/z4u+uAnlPuQZnksaGdZiIcAyrt5IXpNCU5wyg==",
  13. "TimeoutInMilliseconds" : 90000
  14. }
  15. }
  1. using System;
  2. namespace Annytab.Scripts
  3. {
  4. public class FrejaOptions
  5. {
  6. #region Variables
  7. public string BaseAddress { get; set; }
  8. public string JwsCertificate { get; set; }
  9. public Int32? TimeoutInMilliseconds { get; set; }
  10. #endregion
  11. #region Constructors
  12. public FrejaOptions()
  13. {
  14. // Set values for instance variables
  15. this.BaseAddress = null;
  16. this.JwsCertificate = null;
  17. this.TimeoutInMilliseconds = null;
  18. } // End of the constructor
  19. #endregion
  20. } // End of the class
  21. } // End of the namespace

Modeller

  1. using System.Security.Cryptography.X509Certificates;
  2. namespace Annytab.Scripts.Models
  3. {
  4. public class ResponseData
  5. {
  6. #region variables
  7. public bool success { get; set; }
  8. public string id { get; set; }
  9. public string message { get; set; }
  10. public string url { get; set; }
  11. #endregion
  12. #region Constructors
  13. public ResponseData()
  14. {
  15. // Set values for instance variables
  16. this.success = false;
  17. this.id = "";
  18. this.message = "";
  19. this.url = "";
  20. } // End of the constructor
  21. public ResponseData(bool success, string id, string message, string url = "")
  22. {
  23. // Set values for instance variables
  24. this.success = success;
  25. this.id = id;
  26. this.message = message;
  27. this.url = url;
  28. } // End of the constructor
  29. #endregion
  30. } // End of the class
  31. public class Signature
  32. {
  33. #region Variables
  34. public string validation_type { get; set; }
  35. public string algorithm { get; set; }
  36. public string padding { get; set; }
  37. public string data { get; set; }
  38. public string value { get; set; }
  39. public string certificate { get; set; }
  40. #endregion
  41. #region Constructors
  42. public Signature()
  43. {
  44. // Set values for instance variables
  45. this.validation_type = null;
  46. this.algorithm = null;
  47. this.padding = null;
  48. this.data = null;
  49. this.value = null;
  50. this.certificate = null;
  51. } // End of the constructor
  52. #endregion
  53. } // End of the class
  54. public class SignatureValidationResult
  55. {
  56. #region Variables
  57. public bool valid { get; set; }
  58. public string signature_data { get; set; }
  59. public string signatory { get; set; }
  60. public X509Certificate2 certificate { get; set; }
  61. #endregion
  62. #region Constructors
  63. public SignatureValidationResult()
  64. {
  65. // Set values for instance variables
  66. this.valid = false;
  67. this.signature_data = null;
  68. this.signatory = null;
  69. this.certificate = null;
  70. } // End of the constructor
  71. #endregion
  72. } // End of the class
  73. } // End of the namespace
  1. using System;
  2. using System.Collections.Generic;
  3. namespace Annytab.Scripts
  4. {
  5. public class DataToSign
  6. {
  7. public string text { get; set; }
  8. public string binaryData { get; set; }
  9. } // End of the class
  10. public class PushNotification
  11. {
  12. public string title { get; set; }
  13. public string text { get; set; }
  14. } // End of the class
  15. public class AttributesToReturnItem
  16. {
  17. public string attribute { get; set; }
  18. } // End of the class
  19. public class FrejaRequest
  20. {
  21. public string userInfoType { get; set; }
  22. public string userInfo { get; set; }
  23. public string minRegistrationLevel { get; set; }
  24. public string title { get; set; }
  25. public PushNotification pushNotification { get; set; }
  26. public Int64? expiry { get; set; }
  27. public string dataToSignType { get; set; }
  28. public DataToSign dataToSign { get; set; }
  29. public string signatureType { get; set; }
  30. public IList<AttributesToReturnItem> attributesToReturn { get; set; }
  31. } // End of the class
  32. public class BasicUserInfo
  33. {
  34. public string name { get; set; }
  35. public string surname { get; set; }
  36. } // End of the class
  37. public class AddressesItem
  38. {
  39. public string country { get; set; }
  40. public string city { get; set; }
  41. public string postCode { get; set; }
  42. public string address1 { get; set; }
  43. public string address2 { get; set; }
  44. public string address3 { get; set; }
  45. public string validFrom { get; set; }
  46. public string type { get; set; }
  47. public string sourceType { get; set; }
  48. } // End of the class
  49. public class Ssn
  50. {
  51. public string ssn { get; set; }
  52. public string country { get; set; }
  53. } // End of the class
  54. public class RequestedAttributes
  55. {
  56. public BasicUserInfo basicUserInfo { get; set; }
  57. public string emailAddress { get; set; }
  58. public string dateOfBirth { get; set; }
  59. public List<AddressesItem> addresses { get; set; }
  60. public Ssn ssn { get; set; }
  61. public string relyingPartyUserId { get; set; }
  62. public string integratorSpecificUserId { get; set; }
  63. public string customIdentifier { get; set; }
  64. } // End of the class
  65. public class FrejaStatusResponse
  66. {
  67. public string authRef { get; set; }
  68. public string signRef { get; set; }
  69. public string status { get; set; }
  70. public string details { get; set; }
  71. public RequestedAttributes requestedAttributes { get; set; }
  72. } // End of the class
  73. public class FrejaResponseHeader
  74. {
  75. public string x5t { get; set; }
  76. public string alg { get; set; }
  77. } // End of the class
  78. public class FrejaPayload
  79. {
  80. public string authRef { get; set; }
  81. public string signRef { get; set; }
  82. public string status { get; set; }
  83. public string userInfoType { get; set; }
  84. public string userInfo { get; set; }
  85. public string minRegistrationLevel { get; set; }
  86. public RequestedAttributes requestedAttributes { get; set; }
  87. public string signatureType { get; set; }
  88. public SignatureData signatureData { get; set; }
  89. public Int64? timestamp { get; set; }
  90. } // End of the class
  91. public class SignatureData
  92. {
  93. public string userSignature { get; set; }
  94. public string certificateStatus { get; set; }
  95. } // End of the class
  96. } // End of the namespace

Freja-klient

  1. using System.Threading.Tasks;
  2. using Annytab.Scripts.Models;
  3. namespace Annytab.Scripts
  4. {
  5. public interface IFrejaClient
  6. {
  7. Task<bool> Authenticate(string userInfoType, string userInfo);
  8. Task<bool> Sign(string userInfoType, string userInfo, Annytab.Scripts.Models.Signature signature);
  9. SignatureValidationResult Validate(Signature signature);
  10. } // End of the interface
  11. } // End of the namespace
  1. using System;
  2. using System.Text;
  3. using System.Text.Json;
  4. using System.Net.Http;
  5. using System.Net.Http.Headers;
  6. using System.Collections.Generic;
  7. using System.Threading.Tasks;
  8. using System.Security.Cryptography;
  9. using System.Security.Cryptography.X509Certificates;
  10. using Microsoft.AspNetCore.WebUtilities;
  11. using Microsoft.Extensions.Options;
  12. using Microsoft.Extensions.Logging;
  13. using Annytab.Scripts.Models;
  14. namespace Annytab.Scripts
  15. {
  16. public class FrejaClient : IFrejaClient
  17. {
  18. #region Variables
  19. private readonly HttpClient client;
  20. private readonly FrejaOptions options;
  21. private readonly ILogger logger;
  22. #endregion
  23. #region Constructors
  24. public FrejaClient(HttpClient http_client, IOptions<FrejaOptions> options, ILogger<IFrejaClient> logger)
  25. {
  26. // Set values for instance variables
  27. this.client = http_client;
  28. this.options = options.Value;
  29. this.logger = logger;
  30. // Set values for the client
  31. this.client.BaseAddress = new Uri(this.options.BaseAddress);
  32. this.client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
  33. } // End of the constructor
  34. #endregion
  35. #region Authentication
  36. public async Task<bool> Authenticate(string userInfoType, string userInfo)
  37. {
  38. // Variables
  39. StringContent content = null;
  40. FrejaStatusResponse status_response = null;
  41. try
  42. {
  43. // Create a request
  44. FrejaRequest request = new FrejaRequest
  45. {
  46. userInfoType = userInfoType,
  47. userInfo = userInfo,
  48. minRegistrationLevel = "PLUS", // BASIC, EXTENDED or PLUS
  49. attributesToReturn = new List<AttributesToReturnItem>
  50. {
  51. new AttributesToReturnItem
  52. {
  53. attribute = "BASIC_USER_INFO",
  54. },
  55. new AttributesToReturnItem
  56. {
  57. attribute = "EMAIL_ADDRESS",
  58. },
  59. new AttributesToReturnItem
  60. {
  61. attribute = "DATE_OF_BIRTH",
  62. },
  63. new AttributesToReturnItem
  64. {
  65. attribute = "ADDRESSES",
  66. },
  67. new AttributesToReturnItem
  68. {
  69. attribute = "SSN",
  70. }
  71. }
  72. };
  73. // Set serializer options
  74. var json_options = new JsonSerializerOptions
  75. {
  76. IgnoreNullValues = true,
  77. WriteIndented = true,
  78. Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
  79. };
  80. // Convert request to json
  81. string json = JsonSerializer.Serialize(request, json_options);
  82. // Create string content
  83. content = new StringContent("initAuthRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
  84. content.Headers.ContentType.MediaType = "application/json";
  85. content.Headers.ContentType.CharSet = "utf-8";
  86. // Get the response
  87. HttpResponseMessage response = await client.PostAsync("/authentication/1.0/initAuthentication", content);
  88. // Check the status code for the response
  89. if (response.IsSuccessStatusCode == true)
  90. {
  91. // Get string data
  92. json = await response.Content.ReadAsStringAsync();
  93. // Add content
  94. content = new StringContent("getOneAuthResultRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
  95. content.Headers.ContentType.MediaType = "application/json";
  96. content.Headers.ContentType.CharSet = "utf-8";
  97. // Wait for authentication
  98. Int32 timeout = this.options.TimeoutInMilliseconds.GetValueOrDefault();
  99. while (true)
  100. {
  101. // Check for a timeout
  102. if (timeout <= 0)
  103. {
  104. // Cancel the order and return false
  105. content = new StringContent("cancelAuthRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
  106. content.Headers.ContentType.MediaType = "application/json";
  107. content.Headers.ContentType.CharSet = "utf-8";
  108. response = await client.PostAsync("/authentication/1.0/cancel", content);
  109. return false;
  110. }
  111. // Sleep for 2 seconds
  112. await Task.Delay(2000);
  113. timeout -= 2000;
  114. // Collect a signature
  115. response = await client.PostAsync("/authentication/1.0/getOneResult", content);
  116. // Check the status code for the response
  117. if (response.IsSuccessStatusCode == true)
  118. {
  119. // Get string data
  120. string data = await response.Content.ReadAsStringAsync();
  121. // Convert data to a bankid response
  122. status_response = JsonSerializer.Deserialize<FrejaStatusResponse>(data);
  123. if (status_response.status == "APPROVED")
  124. {
  125. // Break out from the loop
  126. break;
  127. }
  128. else if (status_response.status == "STARTED" || status_response.status == "DELIVERED_TO_MOBILE"
  129. || status_response.status == "OPENED" || status_response.status == "OPENED")
  130. {
  131. // Continue to loop
  132. continue;
  133. }
  134. else
  135. {
  136. // CANCELED, RP_CANCELED, EXPIRED or REJECTED
  137. return false;
  138. }
  139. }
  140. else
  141. {
  142. // Get string data
  143. string data = await response.Content.ReadAsStringAsync();
  144. // Return false
  145. return false;
  146. }
  147. }
  148. }
  149. else
  150. {
  151. // Get string data
  152. string data = await response.Content.ReadAsStringAsync();
  153. // Log the error
  154. this.logger.LogError($"Authenticate: {data}");
  155. // Return false
  156. return false;
  157. }
  158. }
  159. catch (Exception ex)
  160. {
  161. // Log the exception
  162. this.logger.LogInformation(ex, $"Authenticate: {status_response.details}", null);
  163. // Return false
  164. return false;
  165. }
  166. finally
  167. {
  168. if (content != null)
  169. {
  170. content.Dispose();
  171. }
  172. }
  173. // Return success
  174. return true;
  175. } // End of the Authenticate method
  176. #endregion
  177. #region Signatures
  178. public async Task<bool> Sign(string userInfoType, string userInfo, Annytab.Scripts.Models.Signature signature)
  179. {
  180. // Variables
  181. StringContent content = null;
  182. FrejaStatusResponse status_response = null;
  183. try
  184. {
  185. // Create a request
  186. FrejaRequest request = new FrejaRequest
  187. {
  188. userInfoType = userInfoType,
  189. userInfo = userInfo,
  190. minRegistrationLevel = "BASIC", // BASIC, EXTENDED or PLUS
  191. title = "Sign File",
  192. pushNotification = new PushNotification // Can not include swedish characters å,ä,ö
  193. {
  194. title = "Hello - Hallå",
  195. text = "Please sign this file - Signera denna fil"
  196. },
  197. expiry = (Int64)DateTime.UtcNow.AddMinutes(5).Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds,
  198. //expiry = DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeMilliseconds(),
  199. dataToSignType = "SIMPLE_UTF8_TEXT",
  200. dataToSign = new DataToSign { text = Convert.ToBase64String(Encoding.UTF8.GetBytes(signature.data)) },
  201. signatureType = "SIMPLE",
  202. attributesToReturn = new List<AttributesToReturnItem>
  203. {
  204. //new AttributesToReturnItem
  205. //{
  206. // attribute = "BASIC_USER_INFO",
  207. //},
  208. new AttributesToReturnItem
  209. {
  210. attribute = "EMAIL_ADDRESS",
  211. },
  212. //new AttributesToReturnItem
  213. //{
  214. // attribute = "DATE_OF_BIRTH",
  215. //},
  216. //new AttributesToReturnItem
  217. //{
  218. // attribute = "ADDRESSES",
  219. //},
  220. //new AttributesToReturnItem
  221. //{
  222. // attribute = "SSN",
  223. //}
  224. }
  225. };
  226. // Set serializer options
  227. var json_options = new JsonSerializerOptions
  228. {
  229. IgnoreNullValues = true,
  230. WriteIndented = true,
  231. Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
  232. };
  233. // Convert request to json
  234. string json = JsonSerializer.Serialize(request, json_options);
  235. // Create string content
  236. content = new StringContent("initSignRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
  237. content.Headers.ContentType.MediaType = "application/json";
  238. content.Headers.ContentType.CharSet = "utf-8";
  239. // Get the response
  240. HttpResponseMessage response = await client.PostAsync("/sign/1.0/initSignature", content);
  241. // Check the status code for the response
  242. if (response.IsSuccessStatusCode == true)
  243. {
  244. // Get string data
  245. json = await response.Content.ReadAsStringAsync();
  246. // Add content
  247. content = new StringContent("getOneSignResultRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
  248. content.Headers.ContentType.MediaType = "application/json";
  249. content.Headers.ContentType.CharSet = "utf-8";
  250. // Collect the signature
  251. Int32 timeout = this.options.TimeoutInMilliseconds.GetValueOrDefault();
  252. while (true)
  253. {
  254. // Check for a timeout
  255. if (timeout <= 0)
  256. {
  257. // Cancel the order and return false
  258. content = new StringContent("cancelSignRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
  259. content.Headers.ContentType.MediaType = "application/json";
  260. content.Headers.ContentType.CharSet = "utf-8";
  261. response = await client.PostAsync("/sign/1.0/cancel", content);
  262. return false;
  263. }
  264. // Sleep for 2 seconds
  265. await Task.Delay(2000);
  266. timeout -= 2000;
  267. // Collect a signature
  268. response = await client.PostAsync("/sign/1.0/getOneResult", content);
  269. // Check the status code for the response
  270. if (response.IsSuccessStatusCode == true)
  271. {
  272. // Get string data
  273. string data = await response.Content.ReadAsStringAsync();
  274. // Convert data to a bankid response
  275. status_response = JsonSerializer.Deserialize<FrejaStatusResponse>(data);
  276. if (status_response.status == "APPROVED")
  277. {
  278. // Break out from the loop
  279. break;
  280. }
  281. else if (status_response.status == "STARTED" || status_response.status == "DELIVERED_TO_MOBILE"
  282. || status_response.status == "OPENED" || status_response.status == "OPENED")
  283. {
  284. // Continue to loop
  285. continue;
  286. }
  287. else
  288. {
  289. // CANCELED, RP_CANCELED or EXPIRED
  290. return false;
  291. }
  292. }
  293. else
  294. {
  295. // Get string data
  296. string data = await response.Content.ReadAsStringAsync();
  297. // Return false
  298. return false;
  299. }
  300. }
  301. }
  302. else
  303. {
  304. // Get string data
  305. string data = await response.Content.ReadAsStringAsync();
  306. // Log the error
  307. this.logger.LogError($"Sign: {data}");
  308. // Return false
  309. return false;
  310. }
  311. // Update the signature
  312. signature.algorithm = "SHA-256";
  313. signature.padding = "Pkcs1";
  314. signature.value = status_response.details;
  315. signature.certificate = this.options.JwsCertificate;
  316. }
  317. catch (Exception ex)
  318. {
  319. // Log the exception
  320. this.logger.LogInformation(ex, $"Sign: {signature.value}", null);
  321. // Return false
  322. return false;
  323. }
  324. finally
  325. {
  326. if (content != null)
  327. {
  328. content.Dispose();
  329. }
  330. }
  331. // Return success
  332. return true;
  333. } // End of the Sign method
  334. public SignatureValidationResult Validate(Annytab.Scripts.Models.Signature signature)
  335. {
  336. // Create the result to return
  337. SignatureValidationResult result = new SignatureValidationResult();
  338. result.signature_data = signature.data;
  339. // Get JWS data (signed by Freja)
  340. string[] jws = signature.value.Split('.');
  341. byte[] data = Encoding.UTF8.GetBytes(jws[0] + "." + jws[1]);
  342. byte[] digest = WebEncoders.Base64UrlDecode(jws[2]);
  343. // Get payload data
  344. FrejaPayload payload = JsonSerializer.Deserialize<FrejaPayload>(Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jws[1])));
  345. result.signatory = payload.userInfoType + ": " + payload.userInfo;
  346. string[] user_signature = payload.signatureData.userSignature.Split('.');
  347. string signed_data = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(user_signature[1]));
  348. try
  349. {
  350. // Get the certificate
  351. result.certificate = new X509Certificate2(Convert.FromBase64String(signature.certificate));
  352. // Get the public key
  353. using (RSA rsa = result.certificate.GetRSAPublicKey())
  354. {
  355. // Check if the signature is valid
  356. result.valid = rsa.VerifyData(data, digest, GetHashAlgorithmName(signature.algorithm), GetRSASignaturePadding(signature.padding));
  357. }
  358. }
  359. catch (Exception ex)
  360. {
  361. // Log the exception
  362. this.logger.LogInformation(ex, $"Validate: {signature.value}", null);
  363. }
  364. // Make sure that signature data conforms
  365. if(signature.data != signed_data)
  366. {
  367. result.valid = false;
  368. }
  369. // Return the validation result
  370. return result;
  371. } // End of the Validate method
  372. #endregion
  373. #region Helpers
  374. public static HashAlgorithmName GetHashAlgorithmName(string signature_algorithm)
  375. {
  376. if (signature_algorithm == "SHA-256")
  377. {
  378. return HashAlgorithmName.SHA256;
  379. }
  380. else if (signature_algorithm == "SHA-384")
  381. {
  382. return HashAlgorithmName.SHA384;
  383. }
  384. else if (signature_algorithm == "SHA-512")
  385. {
  386. return HashAlgorithmName.SHA512;
  387. }
  388. else
  389. {
  390. return HashAlgorithmName.SHA1;
  391. }
  392. } // End of the GetHashAlgorithmName method
  393. public static RSASignaturePadding GetRSASignaturePadding(string signature_padding)
  394. {
  395. if (signature_padding == "Pss")
  396. {
  397. return RSASignaturePadding.Pss;
  398. }
  399. else
  400. {
  401. return RSASignaturePadding.Pkcs1;
  402. }
  403. } // End of the GetRSASignaturePadding method
  404. #endregion
  405. } // End of the class
  406. } // End of the namespace

Konfiguration

  1. using System;
  2. using System.Net;
  3. using System.Net.Http;
  4. using System.Globalization;
  5. using System.Security.Authentication;
  6. using System.Security.Cryptography.X509Certificates;
  7. using Microsoft.AspNetCore.Builder;
  8. using Microsoft.AspNetCore.Hosting;
  9. using Microsoft.AspNetCore.Http.Features;
  10. using Microsoft.AspNetCore.HttpOverrides;
  11. using Microsoft.Extensions.Configuration;
  12. using Microsoft.Extensions.DependencyInjection;
  13. using Microsoft.Extensions.Hosting;
  14. namespace Annytab.Scripts
  15. {
  16. /// <summary>
  17. /// This class handles application startup
  18. /// </summary>
  19. public class Startup
  20. {
  21. /// <summary>
  22. /// Variables
  23. /// </summary>
  24. public IConfiguration configuration { get; set; }
  25. /// <summary>
  26. /// Create a new startup object
  27. /// </summary>
  28. public Startup(IConfiguration configuration)
  29. {
  30. this.configuration = configuration;
  31. } // End of the constructor method
  32. /// <summary>
  33. /// Configure services
  34. /// </summary>
  35. public void ConfigureServices(IServiceCollection services)
  36. {
  37. // Add the mvc framework
  38. services.AddRazorPages();
  39. // Set limits for form options
  40. services.Configure<FormOptions>(x =>
  41. {
  42. x.BufferBody = false;
  43. x.KeyLengthLimit = 2048; // 2 KiB
  44. x.ValueLengthLimit = 4194304; // 32 MiB
  45. x.ValueCountLimit = 2048;// 1024
  46. x.MultipartHeadersCountLimit = 32; // 16
  47. x.MultipartHeadersLengthLimit = 32768; // 16384
  48. x.MultipartBoundaryLengthLimit = 256; // 128
  49. x.MultipartBodyLengthLimit = 134217728; // 128 MiB
  50. });
  51. // Create api options
  52. services.Configure<FrejaOptions>(configuration.GetSection("FrejaOptions"));
  53. // Create clients
  54. services.AddHttpClient<IFrejaClient, FrejaClient>()
  55. .ConfigurePrimaryHttpMessageHandler(() =>
  56. {
  57. HttpClientHandler handler = new HttpClientHandler
  58. {
  59. AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
  60. ClientCertificateOptions = ClientCertificateOption.Manual,
  61. SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11,
  62. ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }
  63. };
  64. handler.ClientCertificates.Add(new X509Certificate2("C:\\DATA\\home\\Freja\\Certificates\\ANameNotYetTakenAB_1.pfx", "5iTCTp"));
  65. return handler;
  66. });
  67. } // End of the ConfigureServices method
  68. /// <summary>
  69. /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
  70. /// </summary>
  71. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  72. {
  73. // Use error handling
  74. if (env.IsDevelopment())
  75. {
  76. app.UseDeveloperExceptionPage();
  77. }
  78. else
  79. {
  80. app.UseStatusCodePagesWithReExecute("/home/error/{0}");
  81. }
  82. // To get client ip address
  83. app.UseForwardedHeaders(new ForwardedHeadersOptions
  84. {
  85. ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
  86. });
  87. // Use static files
  88. app.UseStaticFiles(new StaticFileOptions
  89. {
  90. OnPrepareResponse = ctx =>
  91. {
  92. // Cache static files for 30 days
  93. ctx.Context.Response.Headers.Add("Cache-Control", "public,max-age=2592000");
  94. ctx.Context.Response.Headers.Add("Expires", DateTime.UtcNow.AddDays(30).ToString("R", CultureInfo.InvariantCulture));
  95. }
  96. });
  97. // For most apps, calls to UseAuthentication, UseAuthorization, and UseCors must
  98. // appear between the calls to UseRouting and UseEndpoints to be effective.
  99. app.UseRouting();
  100. // Use authentication and authorization middlewares
  101. app.UseAuthentication();
  102. app.UseAuthorization();
  103. // Routing endpoints
  104. app.UseEndpoints(endpoints =>
  105. {
  106. endpoints.MapControllerRoute(
  107. "default",
  108. "{controller=home}/{action=index}/{id?}");
  109. });
  110. } // End of the Configure method
  111. } // End of the class
  112. } // End of the namespace

Controller

  1. using System;
  2. using System.Text;
  3. using System.Threading.Tasks;
  4. using System.Security.Cryptography.X509Certificates;
  5. using Microsoft.AspNetCore.Http;
  6. using Microsoft.AspNetCore.Mvc;
  7. using Microsoft.Extensions.Logging;
  8. using Annytab.Scripts.Models;
  9. namespace Annytab.Scripts.Controllers
  10. {
  11. public class frejaController : Controller
  12. {
  13. #region Variables
  14. private readonly ILogger logger;
  15. private readonly IFrejaClient freja_client;
  16. #endregion
  17. #region Constructors
  18. public frejaController(ILogger<frejaController> logger, IFrejaClient freja_client)
  19. {
  20. // Set values for instance variables
  21. this.logger = logger;
  22. this.freja_client = freja_client;
  23. } // End of the constructor
  24. #endregion
  25. #region Post methods
  26. [HttpPost]
  27. [ValidateAntiForgeryToken]
  28. public async Task<IActionResult> authentication(IFormCollection collection)
  29. {
  30. // Get form data
  31. string userInfoType = collection["userInfoType"];
  32. string userInfo = collection["txtUserInfo"];
  33. // Check if email is personal id
  34. if(userInfoType == "SSN")
  35. {
  36. userInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"ssn\":" + Convert.ToInt64(userInfo) + ", \"country\":\"" + "SE" + "\"}"));
  37. }
  38. // Authenticate with freja eID v1.0
  39. bool success = await this.freja_client.Authenticate(userInfoType, userInfo);
  40. if (success == false)
  41. {
  42. return Json(data: new ResponseData(false, "", "Was not able to authenticate you with Freja eID. If you have a Freja eID app with a valid certificate, try again."));
  43. }
  44. // Return a response
  45. return Json(data: new ResponseData(success, "You were successfully authenticated!", null));
  46. } // End of the authentication method
  47. [HttpPost]
  48. [ValidateAntiForgeryToken]
  49. public async Task<IActionResult> sign(IFormCollection collection)
  50. {
  51. // Create a signature
  52. Annytab.Scripts.Models.Signature signature = new Annytab.Scripts.Models.Signature();
  53. signature.validation_type = "Freja eID v1.0";
  54. // Get form data
  55. signature.data = collection["txtSignatureData"];
  56. string userInfoType = collection["userInfoType"];
  57. string userInfo = collection["txtUserInfo"];
  58. // Check if email is personal id
  59. if (userInfoType == "SSN")
  60. {
  61. userInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"ssn\":" + Convert.ToInt64(userInfo) + ", \"country\":\"" + "SE" + "\"}"));
  62. }
  63. // Sign with freja eID
  64. bool success = await this.freja_client.Sign(userInfoType, userInfo, signature);
  65. if (success == false)
  66. {
  67. return Json(data: new ResponseData(false, "", "The file could not be signed with Freja eID. If you have a Freja eID app with a valid certificate, try again."));
  68. }
  69. // Return a response
  70. return Json(data: new ResponseData(success, "Signature was successfully created!", signature.value, signature.certificate));
  71. } // End of the sign method
  72. [HttpPost]
  73. [ValidateAntiForgeryToken]
  74. public IActionResult validate(IFormCollection collection)
  75. {
  76. // Create a signature
  77. Annytab.Scripts.Models.Signature signature = new Annytab.Scripts.Models.Signature();
  78. signature.validation_type = "Freja eID v1.0";
  79. signature.algorithm = "SHA-256";
  80. signature.padding = "Pkcs1";
  81. signature.data = collection["txtSignatureData"];
  82. signature.value = collection["txtSignatureValue"];
  83. signature.certificate = collection["txtSignatureCertificate"];
  84. // Validate the signature
  85. SignatureValidationResult result = this.freja_client.Validate(signature);
  86. // Set a title and a message
  87. string title = result.valid == false ? "Invalid Signature" : "Valid Signature";
  88. string message = "<b>" + title + "</b><br />" + signature.data + "<br />" + result.signatory + "<br />";
  89. message += result.certificate != null ? result.certificate.GetNameInfo(X509NameType.SimpleName, false) + ", " + result.certificate.GetNameInfo(X509NameType.SimpleName, true)
  90. + ", " + result.certificate.NotBefore.ToString("yyyy-MM-dd") + " to "
  91. + result.certificate.NotAfter.ToString("yyyy-MM-dd") : "";
  92. // Return a response
  93. return Json(data: new ResponseData(result.valid, title, message));
  94. } // End of the validate method
  95. #endregion
  96. } // End of the class
  97. } // End of the namespace

HTML and JavaScript

Detta formulär har en filöverföringskontroll som startar signeringsprocessen, dagens datum och filens md5-hash är den data som blir signerad. Personen som vill verifiera eller underteckna en fil måste ange sin e-postadress, telefonnummer eller personnummer (SSN). Signaturen kan också valideras.

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Freja eID v1.0 Signature</title>
  5. <style>
  6. .annytab-textarea{width:300px;height:100px;}
  7. .annytab-textbox {width:300px;}
  8. .annytab-form-loading-container {display: none;width: 300px;padding: 20px 0px 20px 0px;text-align: center;}
  9. .annytab-basic-loading-text {margin: 20px 0px 0px 0px;font-size: 16px;line-height: 24px;}
  10. .annytab-cancel-link {color: #ff0000;cursor: pointer;}
  11. </style>
  12. </head>
  13. <body style="width:100%;font-family:Arial, Helvetica, sans-serif;">
  14. <!-- Container -->
  15. <div style="display:block;padding:10px;">
  16. <!-- Input form -->
  17. <form id="inputForm">
  18. <!-- Hidden data -->
  19. @Html.AntiForgeryToken()
  20. <div>Select file to sign <span id="loading"></span></div>
  21. <input id="fuFile" name="fuFile" type="file" onchange="calculateMd5();" class="annytab-textbox" /><br /><br />
  22. <div>Signature data</div>
  23. <textarea id="txtSignatureData" name="txtSignatureData" class="annytab-textarea"></textarea><br /><br />
  24. <div>User information type</div>
  25. <input type="radio" name="userInfoType" value="EMAIL" checked>Email
  26. <input type="radio" name="userInfoType" value="PHONE">Phone
  27. <input type="radio" name="userInfoType" value="SSN">SSN<br /><br />
  28. <div>User information</div>
  29. <input name="txtUserInfo" type="text" class="annytab-textbox" placeholder="Email, SSN or Phone" value="" /><br /><br />
  30. <div>Certificate</div>
  31. <textarea id="txtSignatureCertificate" name="txtSignatureCertificate" class="annytab-textarea"></textarea><br /><br />
  32. <div>Signature value</div>
  33. <textarea id="txtSignatureValue" name="txtSignatureValue" class="annytab-textarea"></textarea><br /><br />
  34. <div class="annytab-form-loading-container">
  35. <i class="fas fa-spinner fa-pulse fa-4x fa-fw"></i><div class="annytab-basic-loading-text">Start your Freja eID app on your smartphone or tablet.</div>
  36. <div class="annytab-basic-loading-text annytab-cancel-link" onclick="cancelSignature()">Cancel</div>
  37. </div>
  38. <input type="button" value="Authenticate" class="btn-disablable" onclick="authenticate()" disabled />
  39. <input type="button" value="Sign file" class="btn-disablable" onclick="createSignature()" disabled />
  40. <input type="button" value="Validate signature" class="btn-disablable" onclick="validateSignature()" disabled />
  41. </form>
  42. </div>
  43. <!-- Style and scripts -->
  44. <link href="/css/annytab.notifier.css" rel="stylesheet" />
  45. <script src="/js/font-awesome/all.min.js"></script>
  46. <script src="/js/annytab.effects.js"></script>
  47. <script src="/js/annytab.notifier.js"></script>
  48. <script src="/js/crypto/spark-md5.js"></script>
  49. <script>
  50. // Set default focus
  51. document.querySelector('#fuFile').focus();
  52. // Authenticate
  53. function authenticate()
  54. {
  55. // Make sure that the request is secure (SSL)
  56. if (location.protocol !== 'https:') {
  57. annytab.notifier.show('error', 'You need a secure connection (SSL)!');
  58. return;
  59. }
  60. // Show loading animation
  61. annytab.effects.fadeIn(document.querySelector('.annytab-form-loading-container'), 500);
  62. // Disable buttons
  63. disableButtons();
  64. // Create form data
  65. var fd = new FormData(document.querySelector('#inputForm'));
  66. // Post form data
  67. postFormData('/freja/authentication', fd, function (data) {
  68. if (data.success === true) {
  69. annytab.notifier.show('success', data.id);
  70. cancelSignature();
  71. }
  72. else {
  73. annytab.notifier.show('error', data.message);
  74. cancelSignature();
  75. }
  76. }, function (data) {
  77. annytab.notifier.show('error', data.message);
  78. cancelSignature();
  79. });
  80. } // End of the authenticate method
  81. // Create a signature
  82. function createSignature()
  83. {
  84. // Make sure that the request is secure (SSL)
  85. if (location.protocol !== 'https:') {
  86. annytab.notifier.show('error', 'You need a secure connection (SSL)!');
  87. return;
  88. }
  89. // Show loading animation
  90. annytab.effects.fadeIn(document.querySelector('.annytab-form-loading-container'), 500);
  91. // Disable buttons
  92. disableButtons();
  93. // Create form data
  94. var fd = new FormData(document.querySelector('#inputForm'));
  95. // Post form data
  96. postFormData('/freja/sign', fd, function (data) {
  97. if (data.success === true) {
  98. annytab.notifier.show('success', data.id);
  99. document.querySelector('#txtSignatureValue').value = data.message;
  100. document.querySelector('#txtSignatureCertificate').value = data.url;
  101. cancelSignature();
  102. }
  103. else
  104. {
  105. annytab.notifier.show('error', data.message);
  106. cancelSignature();
  107. }
  108. }, function (data) {
  109. annytab.notifier.show('error', data.message);
  110. cancelSignature();
  111. });
  112. } // End of the createSignature method
  113. // Cancel a signature
  114. function cancelSignature()
  115. {
  116. // Hide loading container
  117. annytab.effects.fadeOut(document.querySelector('.annytab-form-loading-container'), 500);
  118. // Enable buttons
  119. enableButtons();
  120. } // End of the cancelSignature method
  121. // Validate signature
  122. function validateSignature() {
  123. // Disable buttons
  124. disableButtons();
  125. // Create form data
  126. var fd = new FormData(document.querySelector('#inputForm'));
  127. // Post form data
  128. postFormData('/freja/validate', fd, function (data) {
  129. if (data.success === true) {
  130. annytab.notifier.show('success', data.message);
  131. }
  132. else {
  133. annytab.notifier.show('error', data.message);
  134. }
  135. // Enable buttons
  136. enableButtons();
  137. }, function (data) {
  138. annytab.notifier.show('error', data.message);
  139. // Enable buttons
  140. enableButtons();
  141. });
  142. } // End of the validateSignature method
  143. // Get a hash of a message
  144. async function getHash(data, algorithm)
  145. {
  146. // Hash data
  147. var hashBuffer = await crypto.subtle.digest(algorithm, new TextEncoder().encode(data));
  148. // Convert buffer to byte array
  149. var hashArray = Array.from(new Uint8Array(hashBuffer));
  150. // Convert bytes to hex string
  151. var hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  152. // Return hash as hex string
  153. return hashHex;
  154. } // End of the getHash method
  155. // #region MD5
  156. // Convert Md5 to C# version
  157. function convertMd5(str) {
  158. return btoa(String.fromCharCode.apply(null,
  159. str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" "))
  160. );
  161. } // End of the convertMd5 method
  162. // Calculate a MD5 value of a file
  163. async function calculateMd5() {
  164. // Get the controls
  165. var data = document.querySelector("#txtSignatureData");
  166. var loading = document.querySelector("#loading");
  167. // Get the file
  168. var file = document.querySelector("#fuFile").files[0];
  169. // Make sure that a file is selected
  170. if (typeof file === 'undefined' || file === null) {
  171. return;
  172. }
  173. // Add a loading animation
  174. loading.innerHTML = '- 0 %';
  175. // Variables
  176. var block_size = 4 * 1024 * 1024; // 4 MiB
  177. var offset = 0;
  178. // Create a spark object
  179. var spark = new SparkMD5.ArrayBuffer();
  180. var reader = new FileReader();
  181. // Create blocks
  182. while (offset < file.size) {
  183. // Get the start and end indexes
  184. var start = offset;
  185. var end = Math.min(offset + block_size, file.size);
  186. await loadToMd5(spark, reader, file.slice(start, end));
  187. loading.innerHTML = '- ' + Math.round((offset / file.size) * 100) + ' %';
  188. // Modify the offset and increment the index
  189. offset = end;
  190. }
  191. // Get todays date
  192. var today = new Date();
  193. var dd = String(today.getDate()).padStart(2, '0');
  194. var mm = String(today.getMonth() + 1).padStart(2, '0');
  195. var yyyy = today.getFullYear();
  196. // Output signature data
  197. data.value = yyyy + '-' + mm + '-' + dd + ',' + convertMd5(spark.end());
  198. loading.innerHTML = '- 100 %';
  199. // Enable buttons
  200. enableButtons();
  201. } // End of the calculateMd5 method
  202. // Load to md5
  203. async function loadToMd5(spark, reader, chunk) {
  204. return new Promise((resolve, reject) => {
  205. reader.readAsArrayBuffer(chunk);
  206. reader.onload = function (e) {
  207. resolve(spark.append(e.target.result));
  208. };
  209. reader.onerror = function () {
  210. reject(reader.abort());
  211. };
  212. });
  213. } // End of the loadToMd5 method
  214. // #endregion
  215. // #region form methods
  216. // Post form data
  217. function postFormData(url, fd, successCallback, errorCallback) {
  218. var xhr = new XMLHttpRequest();
  219. xhr.open('POST', url, true);
  220. xhr.onload = function () {
  221. if (xhr.status === 200) {
  222. // Get response
  223. var data = JSON.parse(xhr.response);
  224. // Check success status
  225. if (data.success === true) {
  226. // Callback success
  227. if (successCallback !== null) { successCallback(data); }
  228. }
  229. else {
  230. // Callback error
  231. if (errorCallback !== null) { errorCallback(data); }
  232. }
  233. }
  234. else {
  235. // Callback error information
  236. data = { success: false, id: '', message: xhr.status + " - " + xhr.statusText };
  237. if (errorCallback !== null) { errorCallback(data); }
  238. }
  239. };
  240. xhr.onerror = function () {
  241. // Callback error information
  242. data = { success: false, id: '', message: xhr.status + " - " + xhr.statusText };
  243. if (errorCallback !== null) { errorCallback(data); }
  244. };
  245. xhr.send(fd);
  246. } // End of the postFormData method
  247. // Disable buttons
  248. function disableButtons() {
  249. var buttons = document.getElementsByClassName('btn-disablable');
  250. for (var i = 0; i < buttons.length; i++) {
  251. buttons[i].setAttribute('disabled', true);
  252. }
  253. } // End of the disableButtons method
  254. // Enable buttons
  255. function enableButtons() {
  256. var buttons = document.getElementsByClassName('btn-disablable');
  257. for (var i = 0; i < buttons.length; i++) {
  258. setTimeout(function (button) { button.removeAttribute('disabled'); }, 1000, buttons[i]);
  259. }
  260. } // End of the enableButtons method
  261. // #endregion
  262. </script>
  263. </body>
  264. </html>

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *