diff --git a/QRBee.Core/Data/RegistrationRequest.cs b/QRBee.Core/Data/RegistrationRequest.cs
index 97397e0..3f77c5d 100644
--- a/QRBee.Core/Data/RegistrationRequest.cs
+++ b/QRBee.Core/Data/RegistrationRequest.cs
@@ -1,4 +1,6 @@
-namespace QRBee.Core.Data
+using QRBee.Core.Security;
+
+namespace QRBee.Core.Data
{
public record RegistrationRequest
{
@@ -20,7 +22,7 @@
set;
}
- public string CertificateRequest
+ public ReadableCertificateRequest CertificateRequest
{
get;
set;
diff --git a/QRBee.Core/Security/IPrivateKeyHandler.cs b/QRBee.Core/Security/IPrivateKeyHandler.cs
index c0c38d0..2c8c0b7 100644
--- a/QRBee.Core/Security/IPrivateKeyHandler.cs
+++ b/QRBee.Core/Security/IPrivateKeyHandler.cs
@@ -18,13 +18,13 @@ namespace QRBee.Core.Security
///
///
/// Certificate request to be sent to CA in PEM format
- string GeneratePrivateKey(string? subjectName = null);
+ ReadableCertificateRequest GeneratePrivateKey(string? subjectName = null);
///
/// Re-create certificate request if CA response was not received in time.
///
/// Certificate request to be sent to CA in PEM format
- string CreateCertificateRequest();
+ ReadableCertificateRequest CreateCertificateRequest();
///
/// Attach CA-generated public key certificate to the private key
diff --git a/QRBee.Core/Security/PrivateKeyHandlerBase.cs b/QRBee.Core/Security/PrivateKeyHandlerBase.cs
deleted file mode 100644
index 19acba8..0000000
--- a/QRBee.Core/Security/PrivateKeyHandlerBase.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Security.Cryptography;
-using System.Security.Cryptography.X509Certificates;
-
-namespace QRBee.Core.Security
-{
- ///
- /// Private key handler for API server
- ///
- public class PrivateKeyHandlerBase : IPrivateKeyHandler
- {
-
- protected string CommonName { get; set; }
- protected string CertificatePassword { get; set; }
- public bool Exists()
- {
- throw new NotImplementedException();
- }
-
- public string GeneratePrivateKey(string? subjectName = null)
- {
- throw new NotImplementedException();
- }
-
- public string CreateCertificateRequest()
- {
- throw new NotImplementedException();
- }
-
- public void AttachCertificate(X509Certificate2 cert)
- {
- throw new NotImplementedException();
- }
-
- public X509Certificate2 LoadPrivateKey()
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/QRBee.Core/Security/ReadableCertificateRequest.cs b/QRBee.Core/Security/ReadableCertificateRequest.cs
new file mode 100644
index 0000000..2643fe5
--- /dev/null
+++ b/QRBee.Core/Security/ReadableCertificateRequest.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QRBee.Core.Security
+{
+ public class ReadableCertificateRequest
+ {
+ private byte[] _rsaPublicKey;
+ private byte[] _signature;
+
+ public string SubjectName { get; set; }
+
+ public string RsaPublicKey
+ {
+ get => _rsaPublicKey != null ? Convert.ToBase64String(_rsaPublicKey): null;
+ set => _rsaPublicKey = value != null ? Convert.FromBase64String(value) : null;
+ }
+
+ public string Signature
+ {
+ get => _signature != null ? Convert.ToBase64String(_signature): null;
+ set => _signature = value != null ? Convert.FromBase64String(value) : null;
+ }
+
+ public string AsDataForSignature() => $"{SubjectName}|{RsaPublicKey}";
+
+ }
+}
diff --git a/QRBee/QRBee.Android/MainActivity.cs b/QRBee/QRBee.Android/MainActivity.cs
index 5ec7a1f..8005002 100644
--- a/QRBee/QRBee.Android/MainActivity.cs
+++ b/QRBee/QRBee.Android/MainActivity.cs
@@ -55,6 +55,7 @@ namespace QRBee.Droid
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
;
}
diff --git a/QRBee/QRBee.Android/QRBee.Android.csproj b/QRBee/QRBee.Android/QRBee.Android.csproj
index d4ca956..136e5b2 100644
--- a/QRBee/QRBee.Android/QRBee.Android.csproj
+++ b/QRBee/QRBee.Android/QRBee.Android.csproj
@@ -82,6 +82,7 @@
+
diff --git a/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs b/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs
new file mode 100644
index 0000000..9197da5
--- /dev/null
+++ b/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs
@@ -0,0 +1,162 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using Javax.Xml.Transform;
+using QRBee.Core.Security;
+
+namespace QRBee.Droid.Services
+{
+ ///
+ /// Private key handler for API server
+ ///
+ public class AndroidPrivateKeyHandler : IPrivateKeyHandler
+ {
+ private X509Certificate2? _certificate;
+ private readonly object _syncObject = new object();
+
+ private const string FileName = "private_key.p12";
+ protected string CommonName { get; set; }
+ private const int RSABits = 2048;
+ private const int CertificateValidityDays = 3650;
+
+ protected string CertificatePassword { get; set; }
+
+ private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}";
+
+ ///
+ public bool Exists()
+ => File.Exists(PrivateKeyFileName);
+
+ ///
+ public ReadableCertificateRequest GeneratePrivateKey(string? subjectName)
+ {
+ // locking used to make sure that only one thread generating a private key
+ lock (_syncObject)
+ {
+ var pk = CreateSelfSignedClientCertificate(subjectName ?? CommonName);
+ var pkcs12data = pk.Export(X509ContentType.Pfx, CertificatePassword);
+ File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
+
+ _certificate?.Dispose();
+ _certificate = new X509Certificate2(pkcs12data, CertificatePassword);
+ }
+
+ return CreateCertificateRequest();
+ }
+
+ ///
+ public ReadableCertificateRequest CreateCertificateRequest()
+ {
+ var pk = LoadPrivateKey();
+ var rsa = pk.GetRSAPublicKey();
+
+ var request = new ReadableCertificateRequest
+ {
+ RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()),
+ SubjectName = pk.SubjectName.Name
+ };
+ var data = Encoding.UTF8.GetBytes(request.AsDataForSignature());
+
+ //We can't use SecurityService here because it uses this class. This creates cyclic dependency.
+ var signature = rsa?.SignData(data, 0, data.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No private key found");
+ request.Signature = Convert.ToBase64String(signature);
+ return request;
+ }
+
+ ///
+ /// Generate EXPORTABLE certificate
+ ///
+ ///
+ ///
+ private X509Certificate2 CreateSelfSignedClientCertificate(string subjectName)
+ {
+ // https://stackoverflow.com/questions/42786986/how-to-create-a-valid-self-signed-x509certificate2-programmatically-not-loadin
+
+ var distinguishedName = new X500DistinguishedName($"CN={subjectName}");
+
+ using var rsa = RSA.Create(RSABits);
+ var request = CreateRequest(distinguishedName, rsa);
+
+ var certificate = request.CreateSelfSigned(
+ new DateTimeOffset(DateTime.UtcNow.AddDays(-1)),
+ new DateTimeOffset(DateTime.UtcNow.AddDays(CertificateValidityDays))
+ );
+
+ return certificate;
+ }
+
+ ///
+ /// Generate CA certificate request (i.e. with KeyCertSign usage extension)
+ ///
+ ///
+ ///
+ ///
+ private static CertificateRequest CreateRequest(X500DistinguishedName distinguishedName, RSA rsa)
+ {
+ //TODO not supported on Android
+ var request = new CertificateRequest(
+ distinguishedName,
+ rsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1
+ );
+
+ request.CertificateExtensions.Add(
+ new X509KeyUsageExtension(
+ X509KeyUsageFlags.DataEncipherment
+ | X509KeyUsageFlags.KeyEncipherment
+ | X509KeyUsageFlags.DigitalSignature,
+ false));
+
+ return request;
+ }
+
+ ///
+ public X509Certificate2 LoadPrivateKey()
+ {
+ if (_certificate != null)
+ return _certificate;
+
+ // double locking
+ lock ( _syncObject )
+ {
+ if (_certificate != null)
+ return _certificate;
+
+ if (!Exists())
+ throw new CryptographicException("PrivateKey does not exist");
+
+ _certificate = new X509Certificate2(PrivateKeyFileName, CertificatePassword);
+ return _certificate;
+ }
+ }
+
+ ///
+ public void AttachCertificate(X509Certificate2 cert)
+ {
+ // heavily modified version of:
+ // https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net
+
+ // we can't use LoadPrivateKey here as it creating non-exportable key
+ var pk = new X509Certificate2(PrivateKeyFileName, CertificatePassword, X509KeyStorageFlags.Exportable);
+ using var rsa = pk.GetRSAPrivateKey();
+ if (rsa == null)
+ throw new CryptographicException("Can't get PrivateKey");
+
+ var newPk = cert.CopyWithPrivateKey(rsa);
+
+ var pkcs12data = newPk.Export(X509ContentType.Pfx, CertificatePassword);
+ File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
+
+ lock ( _syncObject )
+ {
+ _certificate?.Dispose();
+ _certificate = null;
+ // it will be loaded on the next access
+ }
+ }
+
+ }
+}
diff --git a/QRBee/QRBee/App.xaml.cs b/QRBee/QRBee/App.xaml.cs
index ed3ce3c..b3c2eb8 100644
--- a/QRBee/QRBee/App.xaml.cs
+++ b/QRBee/QRBee/App.xaml.cs
@@ -30,9 +30,7 @@ namespace QRBee
addPlatformServices?.Invoke(services);
// TODO: Add core services here
- services
- .AddSingleton()
- ;
+
// Add ViewModels
services
diff --git a/QRBee/QRBee/Services/ClientPrivateKeyHandler.cs b/QRBee/QRBee/Services/ClientPrivateKeyHandler.cs
deleted file mode 100644
index 035c4f9..0000000
--- a/QRBee/QRBee/Services/ClientPrivateKeyHandler.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using QRBee.Core.Security;
-
-namespace QRBee.Services
-{
- ///
- /// Private key handler for Client side
- ///
- public class ClientPrivateKeyHandler : PrivateKeyHandlerBase
- {
-
- private const string VeryBadAndInsecureCertificatePassword = "Rî‹T=›'ÄζgÚrʯю™pudF";
-
- public ClientPrivateKeyHandler(ILocalSettings settings)
- {
- CertificatePassword = VeryBadAndInsecureCertificatePassword;
-
- var clientSettings = settings.LoadSettings();
- CommonName = clientSettings?.ClientId;
- }
-
- }
-}
-
diff --git a/QRBee/QRBee/ViewModels/RegisterViewModel.cs b/QRBee/QRBee/ViewModels/RegisterViewModel.cs
index 1a8bfb3..48f4284 100644
--- a/QRBee/QRBee/ViewModels/RegisterViewModel.cs
+++ b/QRBee/QRBee/ViewModels/RegisterViewModel.cs
@@ -4,6 +4,7 @@ using System.Net.Http;
using System.Text.RegularExpressions;
using QRBee.Core;
using QRBee.Core.Data;
+using QRBee.Core.Security;
using QRBee.Services;
using QRBee.Views;
using Xamarin.Forms;
@@ -13,15 +14,18 @@ namespace QRBee.ViewModels
internal class RegisterViewModel: BaseViewModel
{
private readonly ILocalSettings _settings;
+ private readonly IPrivateKeyHandler _privateKeyHandler;
+ private readonly ISecurityService _securityService;
private string _password1;
private string _password2;
- public RegisterViewModel(ILocalSettings localSettings)
+ public RegisterViewModel(ILocalSettings localSettings, IPrivateKeyHandler privateKeyHandler, ISecurityService securityService)
{
_settings = localSettings;
+ _privateKeyHandler = privateKeyHandler;
+ _securityService = securityService;
RegisterCommand = new Command(OnRegisterClicked);
var settings = localSettings.LoadSettings();
-
Name = settings.Name;
Email = settings.Email;
DateOfBirth = settings.DateOfBirth;
@@ -115,15 +119,21 @@ namespace QRBee.ViewModels
settings.IssueNo = IssueNo;
settings.ValidFrom = ValidFrom;
settings.Name = Name;
- settings.PIN = Pin;
+ settings.PIN = Pin;
await _settings.SaveSettings(settings);
+ if (!_privateKeyHandler.Exists())
+ {
+ _privateKeyHandler.GeneratePrivateKey(settings.Name);
+ }
+
var request = new RegistrationRequest
{
DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"),
Email = Email,
Name = Name,
+ CertificateRequest = _privateKeyHandler.CreateCertificateRequest(),
RegisterAsMerchant = false
};
@@ -136,6 +146,9 @@ namespace QRBee.ViewModels
settings.ClientId = response.ClientId;
await _settings.SaveSettings(settings);
+ // Attach certificate to privateKey (replace self-sighed with server issued certificate)
+ _privateKeyHandler.AttachCertificate(_securityService.Deserialize(response.Certificate));
+
var page = Application.Current.MainPage.Navigation.NavigationStack.LastOrDefault();
await page.DisplayAlert("Success", "You have been registered successfully", "Ok");
}
diff --git a/QRBeeApi/Program.cs b/QRBeeApi/Program.cs
index 65ccb31..8bd8199 100644
--- a/QRBeeApi/Program.cs
+++ b/QRBeeApi/Program.cs
@@ -4,6 +4,7 @@ using MongoDB.Driver;
using QRBee.Api;
using QRBee.Api.Services;
using QRBee.Api.Services.Database;
+using QRBee.Core.Security;
var builder = WebApplication.CreateBuilder(args);
@@ -24,10 +25,12 @@ builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services
- .AddSingleton()
+ .AddSingleton()
.AddSingleton()
.Configure(builder.Configuration.GetSection("QRBeeDatabase"))
.AddSingleton( cfg => new MongoClient(cfg.GetRequiredService>().Value.ToMongoDbSettings()))
+ .AddSingleton()
+ .AddSingleton()
;
var app = builder.Build();
diff --git a/QRBeeApi/Services/IQRBeeAPI.cs b/QRBeeApi/Services/IQRBeeAPI.cs
index 8442b5b..0d9f0ff 100644
--- a/QRBeeApi/Services/IQRBeeAPI.cs
+++ b/QRBeeApi/Services/IQRBeeAPI.cs
@@ -5,7 +5,7 @@ using QRBee.Core.Data;
namespace QRBee.Api.Services
{
///
- /// QRBeeAPI interface
+ /// QRBeeAPIService interface
///
public interface IQRBeeAPI
{
diff --git a/QRBeeApi/Services/QRBeeAPI.cs b/QRBeeApi/Services/QRBeeAPIService.cs
similarity index 59%
rename from QRBeeApi/Services/QRBeeAPI.cs
rename to QRBeeApi/Services/QRBeeAPIService.cs
index 064a254..e6fc023 100644
--- a/QRBeeApi/Services/QRBeeAPI.cs
+++ b/QRBeeApi/Services/QRBeeAPIService.cs
@@ -1,23 +1,43 @@
using System.Globalization;
+using System.Text;
using System.Text.RegularExpressions;
using QRBee.Api.Services.Database;
using QRBee.Core;
using QRBee.Core.Data;
+using QRBee.Core.Security;
namespace QRBee.Api.Services
{
///
/// Implementation of
///
- public class QRBeeAPI: IQRBeeAPI
+ public class QRBeeAPIService: IQRBeeAPI
{
private readonly IStorage _storage;
+ private readonly ISecurityService _securityService;
+ private readonly IPrivateKeyHandler _privateKeyHandler;
+ private static readonly object _lock = new ();
+
private const int MaxNameLength = 512;
private const int MaxEmailLength = 512;
- public QRBeeAPI(IStorage storage)
+ public QRBeeAPIService(IStorage storage, ISecurityService securityService, IPrivateKeyHandler privateKeyHandler)
{
_storage = storage;
+ _securityService = securityService;
+ _privateKeyHandler = privateKeyHandler;
+ Init(_privateKeyHandler);
+ }
+
+ private static void Init(IPrivateKeyHandler privateKeyHandler)
+ {
+ lock (_lock)
+ {
+ if (!privateKeyHandler.Exists())
+ {
+ privateKeyHandler.GeneratePrivateKey();
+ }
+ }
}
public async Task Register(RegistrationRequest request)
@@ -28,8 +48,14 @@ namespace QRBee.Api.Services
var info = Convert(request);
var clientId = await _storage.PutUserInfo(info);
+ var clientCertificate = _securityService.CreateCertificate(clientId,System.Convert.FromBase64String(request.CertificateRequest.RsaPublicKey));
+ //TODO save certificate to certificate mongoDB collection
- return new RegistrationResponse{ClientId = clientId};
+ return new RegistrationResponse
+ {
+ ClientId = clientId,
+ Certificate = _securityService.Serialize(clientCertificate)
+ };
}
public Task Update(string clientId, RegistrationRequest request)
@@ -45,7 +71,7 @@ namespace QRBee.Api.Services
return _storage.PutTransactionInfo(info);
}
- private static void Validate(RegistrationRequest request)
+ private void Validate(RegistrationRequest request)
{
if (request == null)
{
@@ -55,6 +81,7 @@ namespace QRBee.Api.Services
var name = request.Name;
var email = request.Email;
var dateOfBirth = request.DateOfBirth;
+ var certificateRequest = request.CertificateRequest;
if (string.IsNullOrEmpty(name) || name.All(char.IsLetter)==false || name.Length>=MaxNameLength)
{
@@ -76,6 +103,15 @@ namespace QRBee.Api.Services
throw new ApplicationException($"DateOfBirth \"{dateOfBirth}\" isn't valid");
}
+ //Check digital signature
+ var verified = _securityService.Verify(
+ Encoding.UTF8.GetBytes(certificateRequest.AsDataForSignature()),
+ Encoding.UTF8.GetBytes(certificateRequest.Signature),
+ _privateKeyHandler.LoadPrivateKey());
+ if (!verified)
+ {
+ throw new ApplicationException($"Digital signature is not valid.");
+ }
}
private static UserInfo Convert(RegistrationRequest request)
diff --git a/QRBeeApi/Services/SecurityService.cs b/QRBeeApi/Services/SecurityService.cs
index 3b66bd4..72f2250 100644
--- a/QRBeeApi/Services/SecurityService.cs
+++ b/QRBeeApi/Services/SecurityService.cs
@@ -10,6 +10,10 @@ namespace QRBee.Api.Services
internal class SecurityService : SecurityServiceBase
{
+ private const int CertificateValidityPeriodDays = 365;
+ private const string CertHeader = "-----BEGIN CERTIFICATE-----";
+ private const string CertFooter = "-----END CERTIFICATE-----";
+
public SecurityService(IPrivateKeyHandler privateKeyHandler)
: base(privateKeyHandler)
{
@@ -18,7 +22,20 @@ namespace QRBee.Api.Services
///
public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey)
{
- throw new ApplicationException("Client never issues certificates");
+ using var rsa = new RSACryptoServiceProvider();
+ rsa.ImportRSAPublicKey(rsaPublicKey, out _);
+
+ var distinguishedName = new X500DistinguishedName($"CN={subjectName}");
+ var req = CreateClientCertRequest(distinguishedName, rsa);
+
+ var pk = PrivateKeyHandler.LoadPrivateKey();
+ var clientCert = req.Create(pk,
+ DateTimeOffset.UtcNow - TimeSpan.FromDays(1),
+ DateTimeOffset.UtcNow + TimeSpan.FromDays(CertificateValidityPeriodDays),
+ Guid.NewGuid()
+ .ToByteArray());
+
+ return clientCert;
}
///
@@ -27,7 +44,7 @@ namespace QRBee.Api.Services
///
///
///
- private static CertificateRequest CreateRequest(X500DistinguishedName distinguishedName, RSA rsa)
+ private static CertificateRequest CreateClientCertRequest(X500DistinguishedName distinguishedName, RSA rsa)
{
var request = new CertificateRequest(
distinguishedName,
@@ -41,19 +58,11 @@ namespace QRBee.Api.Services
X509KeyUsageFlags.DataEncipherment
| X509KeyUsageFlags.KeyEncipherment
| X509KeyUsageFlags.DigitalSignature,
- //| X509KeyUsageFlags.KeyCertSign,
false));
-
- // request.CertificateExtensions.Add(
- // new X509EnhancedKeyUsageExtension(
- // new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
return request;
}
- private const string CertHeader = "-----BEGIN CERTIFICATE-----";
- private const string CertFooter = "-----END CERTIFICATE-----";
-
///
public override X509Certificate2 Deserialize(string pemData)
{
diff --git a/QRBeeApi/Services/ServerPrivateKeyHandler.cs b/QRBeeApi/Services/ServerPrivateKeyHandler.cs
index 632e516..c5c9372 100644
--- a/QRBeeApi/Services/ServerPrivateKeyHandler.cs
+++ b/QRBeeApi/Services/ServerPrivateKeyHandler.cs
@@ -1,20 +1,180 @@
-using QRBee.Core.Security;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using QRBee.Core.Security;
namespace QRBee.Api.Services
{
///
/// Private key handler for API server
///
- public class ServerPrivateKeyHandler : PrivateKeyHandlerBase
+ public class ServerPrivateKeyHandler : IPrivateKeyHandler
{
- private const string CACommonName = "QRBee-CA";
-
- private const string VeryBadAndInsecureCertificatePassword = "U…Š)+œ¶€=ø‘ c¬Í↨ð´áY/ÿ☼æX";
+ private X509Certificate2? _certificate;
+ private readonly object _syncObject = new object();
- public ServerPrivateKeyHandler()
+ private const string FileName = "private_key.p12";
+ protected string CommonName { get; set; }
+ private const int RSABits = 2048;
+ private const int CertificateValidityDays = 3650;
+
+ protected string CertificatePassword { get; set; }
+
+ private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}";
+
+ ///
+ public bool Exists()
+ => File.Exists(PrivateKeyFileName);
+
+ ///
+ public ReadableCertificateRequest GeneratePrivateKey(string? subjectName)
{
- CertificatePassword = VeryBadAndInsecureCertificatePassword;
- CommonName = CACommonName;
+ // locking used to make sure that only one thread generating a private key
+ lock (_syncObject)
+ {
+ var pk = CreateSelfSignedServerCertificate(subjectName ?? CommonName);
+ var pkcs12data = pk.Export(X509ContentType.Pfx, CertificatePassword);
+ File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
+
+ _certificate?.Dispose();
+ _certificate = new X509Certificate2(pkcs12data, CertificatePassword);
+ }
+
+ return CreateCertificateRequest();
+ }
+
+ ///
+ public ReadableCertificateRequest CreateCertificateRequest()
+ {
+ //TODO in fact server should create certificate request in standard format if we ever want to get externally sighed certificate.
+ var pk = LoadPrivateKey();
+ var rsa = pk.GetRSAPublicKey();
+
+ var request = new ReadableCertificateRequest
+ {
+ RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()),
+ SubjectName = pk.SubjectName.Name
+ };
+ var data = Encoding.UTF8.GetBytes(request.AsDataForSignature());
+
+ //We can't use SecurityService here because it uses this class. This creates cyclic dependency.
+ var signature = rsa?.SignData(data, 0, data.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No private key found");
+ request.Signature = Convert.ToBase64String(signature);
+ return request;
+ }
+
+ //private static string AsCsr(CertificateRequest request)
+ //{
+ // // https://stackoverflow.com/questions/65943968/how-to-convert-a-csr-text-file-into-net-core-standard-certificaterequest-for-s
+
+ // var encoded = request.CreateSigningRequest();
+ // var payload = Convert.ToBase64String(encoded, Base64FormattingOptions.InsertLineBreaks);
+ // using var stream = new MemoryStream();
+ // using (var writer = new StreamWriter(stream, System.Text.Encoding.UTF8, 512, true))
+ // {
+ // writer.WriteLine("-----BEGIN CERTIFICATE REQUEST-----");
+ // writer.WriteLine(payload);
+ // writer.WriteLine("-----END CERTIFICATE REQUEST-----");
+ // writer.Flush();
+ // }
+
+ // stream.Position = 0;
+ // using (var reader = new StreamReader(stream))
+ // {
+ // return reader.ReadToEnd();
+ // }
+ //}
+
+ ///
+ /// Generate EXPORTABLE certificate
+ ///
+ ///
+ ///
+ private X509Certificate2 CreateSelfSignedServerCertificate(string subjectName)
+ {
+ // https://stackoverflow.com/questions/42786986/how-to-create-a-valid-self-signed-x509certificate2-programmatically-not-loadin
+
+ var distinguishedName = new X500DistinguishedName($"CN={subjectName}");
+
+ using RSA rsa = RSA.Create(RSABits);
+ var request = CreateClientCertificateRequest(distinguishedName, rsa);
+
+ var certificate = request.CreateSelfSigned(
+ new DateTimeOffset(DateTime.UtcNow.AddDays(-1)),
+ new DateTimeOffset(DateTime.UtcNow.AddDays(CertificateValidityDays))
+ );
+
+ return certificate;
+ }
+
+ ///
+ /// Generate CA certificate request (i.e. with KeyCertSign usage extension)
+ ///
+ ///
+ ///
+ ///
+ private static CertificateRequest CreateClientCertificateRequest(X500DistinguishedName distinguishedName, RSA rsa)
+ {
+ var request = new CertificateRequest(
+ distinguishedName,
+ rsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1
+ );
+
+ request.CertificateExtensions.Add(
+ new X509KeyUsageExtension(
+ X509KeyUsageFlags.DataEncipherment
+ | X509KeyUsageFlags.KeyEncipherment
+ | X509KeyUsageFlags.DigitalSignature,
+ false));
+
+ return request;
+ }
+
+ ///
+ public X509Certificate2 LoadPrivateKey()
+ {
+ if (_certificate != null)
+ return _certificate;
+
+ // double locking
+ lock ( _syncObject )
+ {
+ if (_certificate != null)
+ return _certificate;
+
+ if (!Exists())
+ throw new CryptographicException("PrivateKey does not exist");
+
+ _certificate = new X509Certificate2(PrivateKeyFileName, CertificatePassword);
+ return _certificate;
+ }
+ }
+
+ ///
+ public void AttachCertificate(X509Certificate2 cert)
+ {
+ // heavily modified version of:
+ // https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net
+
+ // we can't use LoadPrivateKey here as it creating non-exportable key
+ var pk = new X509Certificate2(PrivateKeyFileName, CertificatePassword, X509KeyStorageFlags.Exportable);
+ using var rsa = pk.GetRSAPrivateKey();
+ if (rsa == null)
+ throw new CryptographicException("Can't get PrivateKey");
+
+ var newPk = cert.CopyWithPrivateKey(rsa);
+
+ var pkcs12data = newPk.Export(X509ContentType.Pfx, CertificatePassword);
+ File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
+
+ lock ( _syncObject )
+ {
+ _certificate?.Dispose();
+ _certificate = null;
+ // it will be loaded on the next access
+ }
}
}