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