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.
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"AllowedHosts": "*",
"FrejaOptions": {
"BaseAddress": "https://services.test.frejaeid.com",
"JwsCertificate": "MIIEETCCAvmgAwIBAgIUTeCJ0hz3mbtyONBEiap7su74LZwwDQYJKoZIhvcNAQELBQAwgYMxCzAJBgNVBAYTAlNFMRIwEAYDVQQHEwlTdG9ja2hvbG0xFDASBgNVBGETCzU1OTExMC00ODA2MR0wGwYDVQQKExRWZXJpc2VjIEZyZWphIGVJRCBBQjENMAsGA1UECxMEVGVzdDEcMBoGA1UEAxMTUlNBIFRFU1QgSXNzdWluZyBDQTAeFw0xNzA3MTIxNTIwMTNaFw0yMDA3MTIxNTIwMTNaMIGKMQswCQYDVQQGEwJTRTESMBAGA1UEBxMJU3RvY2tob2xtMRQwEgYDVQRhEws1NTkxMTAtNDgwNjEdMBsGA1UEChMUVmVyaXNlYyBGcmVqYSBlSUQgQUIxDTALBgNVBAsTBFRlc3QxIzAhBgNVBAMTGkZyZWphIGVJRCBURVNUIE9yZyBTaWduaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgMINs87TiouDPSSmpn05kZv9TN8XdopcHnElp6ElJLpQh3oYGIL4B71oIgF3r8zRWq8kQoJlYMugmhsld0r0EsUJbsrcjBJ5CJ1WYZg1Vu8FpYLKoaFRI/qxT6xCMvd238Q99Sdl6G6O9sQQoFq10EaYBa970Tl3nDziQQ6bbSNkZoOYIZoicx4+1XFsrGiru8o8QIyc3g0eSgrd3esbUkuk0eH65SeaaOCrsaCOpJUqEziD+el4R6d40dTz/uxWmNpGKF4BmsNWeQi9b4gDYuFqNYhs7bnahvkK6LvtDThV79395px/oUz5BEDdVwjxPJzgaAuUHE+6A1dMapkjsQIDAQABo3QwcjAOBgNVHQ8BAf8EBAMCBsAwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRqfIoPnXAOHNpfLaA8Jl+I6BW/nDASBgNVHSAECzAJMAcGBSoDBAUKMB0GA1UdDgQWBBT7j90x8xG2Sg2p7dCiEpsq3mo5PTANBgkqhkiG9w0BAQsFAAOCAQEAaKEIpRJvhXcN3MvP7MIMzzuKh2O8kRVRQAoKCj0K0R9tTUFS5Ang1fEGMxIfLBohOlRhXgKtqJuB33IKzjyA/1IBuRUg2bEyecBf45IohG+vn4fAHWTJcwVChHWcOUH+Uv1g7NX593nugv0fFdPqt0JCnsFx2c/r9oym+VPP7p04BbXzYUk+17qmFBP/yNlltjzfeVnIOk4HauR9i94FrfynuZLuItB6ySCVmOlfA0r1pHv5sofBEirhwceIw1EtFqEDstI+7XZMXgDwSRYFc1pTjrWMaua2UktmJyWZPfIY69pi/z4u+uAnlPuQZnksaGdZiIcAyrt5IXpNCU5wyg==",
"TimeoutInMilliseconds" : 90000
}
}
using System;
namespace Annytab.Scripts
{
public class FrejaOptions
{
#region Variables
public string BaseAddress { get; set; }
public string JwsCertificate { get; set; }
public Int32? TimeoutInMilliseconds { get; set; }
#endregion
#region Constructors
public FrejaOptions()
{
// Set values for instance variables
this.BaseAddress = null;
this.JwsCertificate = null;
this.TimeoutInMilliseconds = null;
} // End of the constructor
#endregion
} // End of the class
} // End of the namespace
Modeller
using System.Security.Cryptography.X509Certificates;
namespace Annytab.Scripts.Models
{
public class ResponseData
{
#region variables
public bool success { get; set; }
public string id { get; set; }
public string message { get; set; }
public string url { get; set; }
#endregion
#region Constructors
public ResponseData()
{
// Set values for instance variables
this.success = false;
this.id = "";
this.message = "";
this.url = "";
} // End of the constructor
public ResponseData(bool success, string id, string message, string url = "")
{
// Set values for instance variables
this.success = success;
this.id = id;
this.message = message;
this.url = url;
} // End of the constructor
#endregion
} // End of the class
public class Signature
{
#region Variables
public string validation_type { get; set; }
public string algorithm { get; set; }
public string padding { get; set; }
public string data { get; set; }
public string value { get; set; }
public string certificate { get; set; }
#endregion
#region Constructors
public Signature()
{
// Set values for instance variables
this.validation_type = null;
this.algorithm = null;
this.padding = null;
this.data = null;
this.value = null;
this.certificate = null;
} // End of the constructor
#endregion
} // End of the class
public class SignatureValidationResult
{
#region Variables
public bool valid { get; set; }
public string signature_data { get; set; }
public string signatory { get; set; }
public X509Certificate2 certificate { get; set; }
#endregion
#region Constructors
public SignatureValidationResult()
{
// Set values for instance variables
this.valid = false;
this.signature_data = null;
this.signatory = null;
this.certificate = null;
} // End of the constructor
#endregion
} // End of the class
} // End of the namespace
using System;
using System.Collections.Generic;
namespace Annytab.Scripts
{
public class DataToSign
{
public string text { get; set; }
public string binaryData { get; set; }
} // End of the class
public class PushNotification
{
public string title { get; set; }
public string text { get; set; }
} // End of the class
public class AttributesToReturnItem
{
public string attribute { get; set; }
} // End of the class
public class FrejaRequest
{
public string userInfoType { get; set; }
public string userInfo { get; set; }
public string minRegistrationLevel { get; set; }
public string title { get; set; }
public PushNotification pushNotification { get; set; }
public Int64? expiry { get; set; }
public string dataToSignType { get; set; }
public DataToSign dataToSign { get; set; }
public string signatureType { get; set; }
public IList<AttributesToReturnItem> attributesToReturn { get; set; }
} // End of the class
public class BasicUserInfo
{
public string name { get; set; }
public string surname { get; set; }
} // End of the class
public class AddressesItem
{
public string country { get; set; }
public string city { get; set; }
public string postCode { get; set; }
public string address1 { get; set; }
public string address2 { get; set; }
public string address3 { get; set; }
public string validFrom { get; set; }
public string type { get; set; }
public string sourceType { get; set; }
} // End of the class
public class Ssn
{
public string ssn { get; set; }
public string country { get; set; }
} // End of the class
public class RequestedAttributes
{
public BasicUserInfo basicUserInfo { get; set; }
public string emailAddress { get; set; }
public string dateOfBirth { get; set; }
public List<AddressesItem> addresses { get; set; }
public Ssn ssn { get; set; }
public string relyingPartyUserId { get; set; }
public string integratorSpecificUserId { get; set; }
public string customIdentifier { get; set; }
} // End of the class
public class FrejaStatusResponse
{
public string authRef { get; set; }
public string signRef { get; set; }
public string status { get; set; }
public string details { get; set; }
public RequestedAttributes requestedAttributes { get; set; }
} // End of the class
public class FrejaResponseHeader
{
public string x5t { get; set; }
public string alg { get; set; }
} // End of the class
public class FrejaPayload
{
public string authRef { get; set; }
public string signRef { get; set; }
public string status { get; set; }
public string userInfoType { get; set; }
public string userInfo { get; set; }
public string minRegistrationLevel { get; set; }
public RequestedAttributes requestedAttributes { get; set; }
public string signatureType { get; set; }
public SignatureData signatureData { get; set; }
public Int64? timestamp { get; set; }
} // End of the class
public class SignatureData
{
public string userSignature { get; set; }
public string certificateStatus { get; set; }
} // End of the class
} // End of the namespace
Freja-klient
using System.Threading.Tasks;
using Annytab.Scripts.Models;
namespace Annytab.Scripts
{
public interface IFrejaClient
{
Task<bool> Authenticate(string userInfoType, string userInfo);
Task<bool> Sign(string userInfoType, string userInfo, Annytab.Scripts.Models.Signature signature);
SignatureValidationResult Validate(Signature signature);
} // End of the interface
} // End of the namespace
using System;
using System.Text;
using System.Text.Json;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
using Annytab.Scripts.Models;
namespace Annytab.Scripts
{
public class FrejaClient : IFrejaClient
{
#region Variables
private readonly HttpClient client;
private readonly FrejaOptions options;
private readonly ILogger logger;
#endregion
#region Constructors
public FrejaClient(HttpClient http_client, IOptions<FrejaOptions> options, ILogger<IFrejaClient> logger)
{
// Set values for instance variables
this.client = http_client;
this.options = options.Value;
this.logger = logger;
// Set values for the client
this.client.BaseAddress = new Uri(this.options.BaseAddress);
this.client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
} // End of the constructor
#endregion
#region Authentication
public async Task<bool> Authenticate(string userInfoType, string userInfo)
{
// Variables
StringContent content = null;
FrejaStatusResponse status_response = null;
try
{
// Create a request
FrejaRequest request = new FrejaRequest
{
userInfoType = userInfoType,
userInfo = userInfo,
minRegistrationLevel = "PLUS", // BASIC, EXTENDED or PLUS
attributesToReturn = new List<AttributesToReturnItem>
{
new AttributesToReturnItem
{
attribute = "BASIC_USER_INFO",
},
new AttributesToReturnItem
{
attribute = "EMAIL_ADDRESS",
},
new AttributesToReturnItem
{
attribute = "DATE_OF_BIRTH",
},
new AttributesToReturnItem
{
attribute = "ADDRESSES",
},
new AttributesToReturnItem
{
attribute = "SSN",
}
}
};
// Set serializer options
var json_options = new JsonSerializerOptions
{
IgnoreNullValues = true,
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
// Convert request to json
string json = JsonSerializer.Serialize(request, json_options);
// Create string content
content = new StringContent("initAuthRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
content.Headers.ContentType.MediaType = "application/json";
content.Headers.ContentType.CharSet = "utf-8";
// Get the response
HttpResponseMessage response = await client.PostAsync("/authentication/1.0/initAuthentication", content);
// Check the status code for the response
if (response.IsSuccessStatusCode == true)
{
// Get string data
json = await response.Content.ReadAsStringAsync();
// Add content
content = new StringContent("getOneAuthResultRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
content.Headers.ContentType.MediaType = "application/json";
content.Headers.ContentType.CharSet = "utf-8";
// Wait for authentication
Int32 timeout = this.options.TimeoutInMilliseconds.GetValueOrDefault();
while (true)
{
// Check for a timeout
if (timeout <= 0)
{
// Cancel the order and return false
content = new StringContent("cancelAuthRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
content.Headers.ContentType.MediaType = "application/json";
content.Headers.ContentType.CharSet = "utf-8";
response = await client.PostAsync("/authentication/1.0/cancel", content);
return false;
}
// Sleep for 2 seconds
await Task.Delay(2000);
timeout -= 2000;
// Collect a signature
response = await client.PostAsync("/authentication/1.0/getOneResult", content);
// Check the status code for the response
if (response.IsSuccessStatusCode == true)
{
// Get string data
string data = await response.Content.ReadAsStringAsync();
// Convert data to a bankid response
status_response = JsonSerializer.Deserialize<FrejaStatusResponse>(data);
if (status_response.status == "APPROVED")
{
// Break out from the loop
break;
}
else if (status_response.status == "STARTED" || status_response.status == "DELIVERED_TO_MOBILE"
|| status_response.status == "OPENED" || status_response.status == "OPENED")
{
// Continue to loop
continue;
}
else
{
// CANCELED, RP_CANCELED, EXPIRED or REJECTED
return false;
}
}
else
{
// Get string data
string data = await response.Content.ReadAsStringAsync();
// Return false
return false;
}
}
}
else
{
// Get string data
string data = await response.Content.ReadAsStringAsync();
// Log the error
this.logger.LogError($"Authenticate: {data}");
// Return false
return false;
}
}
catch (Exception ex)
{
// Log the exception
this.logger.LogInformation(ex, $"Authenticate: {status_response.details}", null);
// Return false
return false;
}
finally
{
if (content != null)
{
content.Dispose();
}
}
// Return success
return true;
} // End of the Authenticate method
#endregion
#region Signatures
public async Task<bool> Sign(string userInfoType, string userInfo, Annytab.Scripts.Models.Signature signature)
{
// Variables
StringContent content = null;
FrejaStatusResponse status_response = null;
try
{
// Create a request
FrejaRequest request = new FrejaRequest
{
userInfoType = userInfoType,
userInfo = userInfo,
minRegistrationLevel = "BASIC", // BASIC, EXTENDED or PLUS
title = "Sign File",
pushNotification = new PushNotification // Can not include swedish characters å,ä,ö
{
title = "Hello - Hallå",
text = "Please sign this file - Signera denna fil"
},
expiry = (Int64)DateTime.UtcNow.AddMinutes(5).Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds,
//expiry = DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeMilliseconds(),
dataToSignType = "SIMPLE_UTF8_TEXT",
dataToSign = new DataToSign { text = Convert.ToBase64String(Encoding.UTF8.GetBytes(signature.data)) },
signatureType = "SIMPLE",
attributesToReturn = new List<AttributesToReturnItem>
{
//new AttributesToReturnItem
//{
// attribute = "BASIC_USER_INFO",
//},
new AttributesToReturnItem
{
attribute = "EMAIL_ADDRESS",
},
//new AttributesToReturnItem
//{
// attribute = "DATE_OF_BIRTH",
//},
//new AttributesToReturnItem
//{
// attribute = "ADDRESSES",
//},
//new AttributesToReturnItem
//{
// attribute = "SSN",
//}
}
};
// Set serializer options
var json_options = new JsonSerializerOptions
{
IgnoreNullValues = true,
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
// Convert request to json
string json = JsonSerializer.Serialize(request, json_options);
// Create string content
content = new StringContent("initSignRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
content.Headers.ContentType.MediaType = "application/json";
content.Headers.ContentType.CharSet = "utf-8";
// Get the response
HttpResponseMessage response = await client.PostAsync("/sign/1.0/initSignature", content);
// Check the status code for the response
if (response.IsSuccessStatusCode == true)
{
// Get string data
json = await response.Content.ReadAsStringAsync();
// Add content
content = new StringContent("getOneSignResultRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
content.Headers.ContentType.MediaType = "application/json";
content.Headers.ContentType.CharSet = "utf-8";
// Collect the signature
Int32 timeout = this.options.TimeoutInMilliseconds.GetValueOrDefault();
while (true)
{
// Check for a timeout
if (timeout <= 0)
{
// Cancel the order and return false
content = new StringContent("cancelSignRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
content.Headers.ContentType.MediaType = "application/json";
content.Headers.ContentType.CharSet = "utf-8";
response = await client.PostAsync("/sign/1.0/cancel", content);
return false;
}
// Sleep for 2 seconds
await Task.Delay(2000);
timeout -= 2000;
// Collect a signature
response = await client.PostAsync("/sign/1.0/getOneResult", content);
// Check the status code for the response
if (response.IsSuccessStatusCode == true)
{
// Get string data
string data = await response.Content.ReadAsStringAsync();
// Convert data to a bankid response
status_response = JsonSerializer.Deserialize<FrejaStatusResponse>(data);
if (status_response.status == "APPROVED")
{
// Break out from the loop
break;
}
else if (status_response.status == "STARTED" || status_response.status == "DELIVERED_TO_MOBILE"
|| status_response.status == "OPENED" || status_response.status == "OPENED")
{
// Continue to loop
continue;
}
else
{
// CANCELED, RP_CANCELED or EXPIRED
return false;
}
}
else
{
// Get string data
string data = await response.Content.ReadAsStringAsync();
// Return false
return false;
}
}
}
else
{
// Get string data
string data = await response.Content.ReadAsStringAsync();
// Log the error
this.logger.LogError($"Sign: {data}");
// Return false
return false;
}
// Update the signature
signature.algorithm = "SHA-256";
signature.padding = "Pkcs1";
signature.value = status_response.details;
signature.certificate = this.options.JwsCertificate;
}
catch (Exception ex)
{
// Log the exception
this.logger.LogInformation(ex, $"Sign: {signature.value}", null);
// Return false
return false;
}
finally
{
if (content != null)
{
content.Dispose();
}
}
// Return success
return true;
} // End of the Sign method
public SignatureValidationResult Validate(Annytab.Scripts.Models.Signature signature)
{
// Create the result to return
SignatureValidationResult result = new SignatureValidationResult();
result.signature_data = signature.data;
// Get JWS data (signed by Freja)
string[] jws = signature.value.Split('.');
byte[] data = Encoding.UTF8.GetBytes(jws[0] + "." + jws[1]);
byte[] digest = WebEncoders.Base64UrlDecode(jws[2]);
// Get payload data
FrejaPayload payload = JsonSerializer.Deserialize<FrejaPayload>(Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jws[1])));
result.signatory = payload.userInfoType + ": " + payload.userInfo;
string[] user_signature = payload.signatureData.userSignature.Split('.');
string signed_data = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(user_signature[1]));
try
{
// Get the certificate
result.certificate = new X509Certificate2(Convert.FromBase64String(signature.certificate));
// Get the public key
using (RSA rsa = result.certificate.GetRSAPublicKey())
{
// Check if the signature is valid
result.valid = rsa.VerifyData(data, digest, GetHashAlgorithmName(signature.algorithm), GetRSASignaturePadding(signature.padding));
}
}
catch (Exception ex)
{
// Log the exception
this.logger.LogInformation(ex, $"Validate: {signature.value}", null);
}
// Make sure that signature data conforms
if(signature.data != signed_data)
{
result.valid = false;
}
// Return the validation result
return result;
} // End of the Validate method
#endregion
#region Helpers
public static HashAlgorithmName GetHashAlgorithmName(string signature_algorithm)
{
if (signature_algorithm == "SHA-256")
{
return HashAlgorithmName.SHA256;
}
else if (signature_algorithm == "SHA-384")
{
return HashAlgorithmName.SHA384;
}
else if (signature_algorithm == "SHA-512")
{
return HashAlgorithmName.SHA512;
}
else
{
return HashAlgorithmName.SHA1;
}
} // End of the GetHashAlgorithmName method
public static RSASignaturePadding GetRSASignaturePadding(string signature_padding)
{
if (signature_padding == "Pss")
{
return RSASignaturePadding.Pss;
}
else
{
return RSASignaturePadding.Pkcs1;
}
} // End of the GetRSASignaturePadding method
#endregion
} // End of the class
} // End of the namespace
Konfiguration
using System;
using System.Net;
using System.Net.Http;
using System.Globalization;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Annytab.Scripts
{
/// <summary>
/// This class handles application startup
/// </summary>
public class Startup
{
/// <summary>
/// Variables
/// </summary>
public IConfiguration configuration { get; set; }
/// <summary>
/// Create a new startup object
/// </summary>
public Startup(IConfiguration configuration)
{
this.configuration = configuration;
} // End of the constructor method
/// <summary>
/// Configure services
/// </summary>
public void ConfigureServices(IServiceCollection services)
{
// Add the mvc framework
services.AddRazorPages();
// Set limits for form options
services.Configure<FormOptions>(x =>
{
x.BufferBody = false;
x.KeyLengthLimit = 2048; // 2 KiB
x.ValueLengthLimit = 4194304; // 32 MiB
x.ValueCountLimit = 2048;// 1024
x.MultipartHeadersCountLimit = 32; // 16
x.MultipartHeadersLengthLimit = 32768; // 16384
x.MultipartBoundaryLengthLimit = 256; // 128
x.MultipartBodyLengthLimit = 134217728; // 128 MiB
});
// Create api options
services.Configure<FrejaOptions>(configuration.GetSection("FrejaOptions"));
// Create clients
services.AddHttpClient<IFrejaClient, FrejaClient>()
.ConfigurePrimaryHttpMessageHandler(() =>
{
HttpClientHandler handler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11,
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }
};
handler.ClientCertificates.Add(new X509Certificate2("C:\\DATA\\home\\Freja\\Certificates\\ANameNotYetTakenAB_1.pfx", "5iTCTp"));
return handler;
});
} // End of the ConfigureServices method
/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Use error handling
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseStatusCodePagesWithReExecute("/home/error/{0}");
}
// To get client ip address
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
// Use static files
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Cache static files for 30 days
ctx.Context.Response.Headers.Add("Cache-Control", "public,max-age=2592000");
ctx.Context.Response.Headers.Add("Expires", DateTime.UtcNow.AddDays(30).ToString("R", CultureInfo.InvariantCulture));
}
});
// For most apps, calls to UseAuthentication, UseAuthorization, and UseCors must
// appear between the calls to UseRouting and UseEndpoints to be effective.
app.UseRouting();
// Use authentication and authorization middlewares
app.UseAuthentication();
app.UseAuthorization();
// Routing endpoints
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
"default",
"{controller=home}/{action=index}/{id?}");
});
} // End of the Configure method
} // End of the class
} // End of the namespace
Controller
using System;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Annytab.Scripts.Models;
namespace Annytab.Scripts.Controllers
{
public class frejaController : Controller
{
#region Variables
private readonly ILogger logger;
private readonly IFrejaClient freja_client;
#endregion
#region Constructors
public frejaController(ILogger<frejaController> logger, IFrejaClient freja_client)
{
// Set values for instance variables
this.logger = logger;
this.freja_client = freja_client;
} // End of the constructor
#endregion
#region Post methods
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> authentication(IFormCollection collection)
{
// Get form data
string userInfoType = collection["userInfoType"];
string userInfo = collection["txtUserInfo"];
// Check if email is personal id
if(userInfoType == "SSN")
{
userInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"ssn\":" + Convert.ToInt64(userInfo) + ", \"country\":\"" + "SE" + "\"}"));
}
// Authenticate with freja eID v1.0
bool success = await this.freja_client.Authenticate(userInfoType, userInfo);
if (success == false)
{
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."));
}
// Return a response
return Json(data: new ResponseData(success, "You were successfully authenticated!", null));
} // End of the authentication method
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> sign(IFormCollection collection)
{
// Create a signature
Annytab.Scripts.Models.Signature signature = new Annytab.Scripts.Models.Signature();
signature.validation_type = "Freja eID v1.0";
// Get form data
signature.data = collection["txtSignatureData"];
string userInfoType = collection["userInfoType"];
string userInfo = collection["txtUserInfo"];
// Check if email is personal id
if (userInfoType == "SSN")
{
userInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"ssn\":" + Convert.ToInt64(userInfo) + ", \"country\":\"" + "SE" + "\"}"));
}
// Sign with freja eID
bool success = await this.freja_client.Sign(userInfoType, userInfo, signature);
if (success == false)
{
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."));
}
// Return a response
return Json(data: new ResponseData(success, "Signature was successfully created!", signature.value, signature.certificate));
} // End of the sign method
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult validate(IFormCollection collection)
{
// Create a signature
Annytab.Scripts.Models.Signature signature = new Annytab.Scripts.Models.Signature();
signature.validation_type = "Freja eID v1.0";
signature.algorithm = "SHA-256";
signature.padding = "Pkcs1";
signature.data = collection["txtSignatureData"];
signature.value = collection["txtSignatureValue"];
signature.certificate = collection["txtSignatureCertificate"];
// Validate the signature
SignatureValidationResult result = this.freja_client.Validate(signature);
// Set a title and a message
string title = result.valid == false ? "Invalid Signature" : "Valid Signature";
string message = "<b>" + title + "</b><br />" + signature.data + "<br />" + result.signatory + "<br />";
message += result.certificate != null ? result.certificate.GetNameInfo(X509NameType.SimpleName, false) + ", " + result.certificate.GetNameInfo(X509NameType.SimpleName, true)
+ ", " + result.certificate.NotBefore.ToString("yyyy-MM-dd") + " to "
+ result.certificate.NotAfter.ToString("yyyy-MM-dd") : "";
// Return a response
return Json(data: new ResponseData(result.valid, title, message));
} // End of the validate method
#endregion
} // End of the class
} // 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.
<!DOCTYPE html>
<html>
<head>
<title>Freja eID v1.0 Signature</title>
<style>
.annytab-textarea{width:300px;height:100px;}
.annytab-textbox {width:300px;}
.annytab-form-loading-container {display: none;width: 300px;padding: 20px 0px 20px 0px;text-align: center;}
.annytab-basic-loading-text {margin: 20px 0px 0px 0px;font-size: 16px;line-height: 24px;}
.annytab-cancel-link {color: #ff0000;cursor: pointer;}
</style>
</head>
<body style="width:100%;font-family:Arial, Helvetica, sans-serif;">
<!-- Container -->
<div style="display:block;padding:10px;">
<!-- Input form -->
<form id="inputForm">
<!-- Hidden data -->
@Html.AntiForgeryToken()
<div>Select file to sign <span id="loading"></span></div>
<input id="fuFile" name="fuFile" type="file" onchange="calculateMd5();" class="annytab-textbox" /><br /><br />
<div>Signature data</div>
<textarea id="txtSignatureData" name="txtSignatureData" class="annytab-textarea"></textarea><br /><br />
<div>User information type</div>
<input type="radio" name="userInfoType" value="EMAIL" checked>Email
<input type="radio" name="userInfoType" value="PHONE">Phone
<input type="radio" name="userInfoType" value="SSN">SSN<br /><br />
<div>User information</div>
<input name="txtUserInfo" type="text" class="annytab-textbox" placeholder="Email, SSN or Phone" value="" /><br /><br />
<div>Certificate</div>
<textarea id="txtSignatureCertificate" name="txtSignatureCertificate" class="annytab-textarea"></textarea><br /><br />
<div>Signature value</div>
<textarea id="txtSignatureValue" name="txtSignatureValue" class="annytab-textarea"></textarea><br /><br />
<div class="annytab-form-loading-container">
<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>
<div class="annytab-basic-loading-text annytab-cancel-link" onclick="cancelSignature()">Cancel</div>
</div>
<input type="button" value="Authenticate" class="btn-disablable" onclick="authenticate()" disabled />
<input type="button" value="Sign file" class="btn-disablable" onclick="createSignature()" disabled />
<input type="button" value="Validate signature" class="btn-disablable" onclick="validateSignature()" disabled />
</form>
</div>
<!-- Style and scripts -->
<link href="/css/annytab.notifier.css" rel="stylesheet" />
<script src="/js/font-awesome/all.min.js"></script>
<script src="/js/annytab.effects.js"></script>
<script src="/js/annytab.notifier.js"></script>
<script src="/js/crypto/spark-md5.js"></script>
<script>
// Set default focus
document.querySelector('#fuFile').focus();
// Authenticate
function authenticate()
{
// Make sure that the request is secure (SSL)
if (location.protocol !== 'https:') {
annytab.notifier.show('error', 'You need a secure connection (SSL)!');
return;
}
// Show loading animation
annytab.effects.fadeIn(document.querySelector('.annytab-form-loading-container'), 500);
// Disable buttons
disableButtons();
// Create form data
var fd = new FormData(document.querySelector('#inputForm'));
// Post form data
postFormData('/freja/authentication', fd, function (data) {
if (data.success === true) {
annytab.notifier.show('success', data.id);
cancelSignature();
}
else {
annytab.notifier.show('error', data.message);
cancelSignature();
}
}, function (data) {
annytab.notifier.show('error', data.message);
cancelSignature();
});
} // End of the authenticate method
// Create a signature
function createSignature()
{
// Make sure that the request is secure (SSL)
if (location.protocol !== 'https:') {
annytab.notifier.show('error', 'You need a secure connection (SSL)!');
return;
}
// Show loading animation
annytab.effects.fadeIn(document.querySelector('.annytab-form-loading-container'), 500);
// Disable buttons
disableButtons();
// Create form data
var fd = new FormData(document.querySelector('#inputForm'));
// Post form data
postFormData('/freja/sign', fd, function (data) {
if (data.success === true) {
annytab.notifier.show('success', data.id);
document.querySelector('#txtSignatureValue').value = data.message;
document.querySelector('#txtSignatureCertificate').value = data.url;
cancelSignature();
}
else
{
annytab.notifier.show('error', data.message);
cancelSignature();
}
}, function (data) {
annytab.notifier.show('error', data.message);
cancelSignature();
});
} // End of the createSignature method
// Cancel a signature
function cancelSignature()
{
// Hide loading container
annytab.effects.fadeOut(document.querySelector('.annytab-form-loading-container'), 500);
// Enable buttons
enableButtons();
} // End of the cancelSignature method
// Validate signature
function validateSignature() {
// Disable buttons
disableButtons();
// Create form data
var fd = new FormData(document.querySelector('#inputForm'));
// Post form data
postFormData('/freja/validate', fd, function (data) {
if (data.success === true) {
annytab.notifier.show('success', data.message);
}
else {
annytab.notifier.show('error', data.message);
}
// Enable buttons
enableButtons();
}, function (data) {
annytab.notifier.show('error', data.message);
// Enable buttons
enableButtons();
});
} // End of the validateSignature method
// Get a hash of a message
async function getHash(data, algorithm)
{
// Hash data
var hashBuffer = await crypto.subtle.digest(algorithm, new TextEncoder().encode(data));
// Convert buffer to byte array
var hashArray = Array.from(new Uint8Array(hashBuffer));
// Convert bytes to hex string
var hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// Return hash as hex string
return hashHex;
} // End of the getHash method
// #region MD5
// Convert Md5 to C# version
function convertMd5(str) {
return btoa(String.fromCharCode.apply(null,
str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" "))
);
} // End of the convertMd5 method
// Calculate a MD5 value of a file
async function calculateMd5() {
// Get the controls
var data = document.querySelector("#txtSignatureData");
var loading = document.querySelector("#loading");
// Get the file
var file = document.querySelector("#fuFile").files[0];
// Make sure that a file is selected
if (typeof file === 'undefined' || file === null) {
return;
}
// Add a loading animation
loading.innerHTML = '- 0 %';
// Variables
var block_size = 4 * 1024 * 1024; // 4 MiB
var offset = 0;
// Create a spark object
var spark = new SparkMD5.ArrayBuffer();
var reader = new FileReader();
// Create blocks
while (offset < file.size) {
// Get the start and end indexes
var start = offset;
var end = Math.min(offset + block_size, file.size);
await loadToMd5(spark, reader, file.slice(start, end));
loading.innerHTML = '- ' + Math.round((offset / file.size) * 100) + ' %';
// Modify the offset and increment the index
offset = end;
}
// Get todays date
var today = new Date();
var dd = String(today.getDate()).padStart(2, '0');
var mm = String(today.getMonth() + 1).padStart(2, '0');
var yyyy = today.getFullYear();
// Output signature data
data.value = yyyy + '-' + mm + '-' + dd + ',' + convertMd5(spark.end());
loading.innerHTML = '- 100 %';
// Enable buttons
enableButtons();
} // End of the calculateMd5 method
// Load to md5
async function loadToMd5(spark, reader, chunk) {
return new Promise((resolve, reject) => {
reader.readAsArrayBuffer(chunk);
reader.onload = function (e) {
resolve(spark.append(e.target.result));
};
reader.onerror = function () {
reject(reader.abort());
};
});
} // End of the loadToMd5 method
// #endregion
// #region form methods
// Post form data
function postFormData(url, fd, successCallback, errorCallback) {
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.onload = function () {
if (xhr.status === 200) {
// Get response
var data = JSON.parse(xhr.response);
// Check success status
if (data.success === true) {
// Callback success
if (successCallback !== null) { successCallback(data); }
}
else {
// Callback error
if (errorCallback !== null) { errorCallback(data); }
}
}
else {
// Callback error information
data = { success: false, id: '', message: xhr.status + " - " + xhr.statusText };
if (errorCallback !== null) { errorCallback(data); }
}
};
xhr.onerror = function () {
// Callback error information
data = { success: false, id: '', message: xhr.status + " - " + xhr.statusText };
if (errorCallback !== null) { errorCallback(data); }
};
xhr.send(fd);
} // End of the postFormData method
// Disable buttons
function disableButtons() {
var buttons = document.getElementsByClassName('btn-disablable');
for (var i = 0; i < buttons.length; i++) {
buttons[i].setAttribute('disabled', true);
}
} // End of the disableButtons method
// Enable buttons
function enableButtons() {
var buttons = document.getElementsByClassName('btn-disablable');
for (var i = 0; i < buttons.length; i++) {
setTimeout(function (button) { button.removeAttribute('disabled'); }, 1000, buttons[i]);
}
} // End of the enableButtons method
// #endregion
</script>
</body>
</html>