SecurityService implemented. Not working due to Android limitations (CertificateRequest).

This commit is contained in:
Andrey Shabarshov 2022-02-18 14:24:58 +00:00
parent 551a44c38d
commit 6da55bc4a5
15 changed files with 448 additions and 96 deletions

View File

@ -1,4 +1,6 @@
namespace QRBee.Core.Data using QRBee.Core.Security;
namespace QRBee.Core.Data
{ {
public record RegistrationRequest public record RegistrationRequest
{ {
@ -20,7 +22,7 @@
set; set;
} }
public string CertificateRequest public ReadableCertificateRequest CertificateRequest
{ {
get; get;
set; set;

View File

@ -18,13 +18,13 @@ namespace QRBee.Core.Security
/// </summary> /// </summary>
/// <param name="subjectName"></param> /// <param name="subjectName"></param>
/// <returns>Certificate request to be sent to CA in PEM format</returns> /// <returns>Certificate request to be sent to CA in PEM format</returns>
string GeneratePrivateKey(string? subjectName = null); ReadableCertificateRequest GeneratePrivateKey(string? subjectName = null);
/// <summary> /// <summary>
/// Re-create certificate request if CA response was not received in time. /// Re-create certificate request if CA response was not received in time.
/// </summary> /// </summary>
/// <returns>Certificate request to be sent to CA in PEM format</returns> /// <returns>Certificate request to be sent to CA in PEM format</returns>
string CreateCertificateRequest(); ReadableCertificateRequest CreateCertificateRequest();
/// <summary> /// <summary>
/// Attach CA-generated public key certificate to the private key /// Attach CA-generated public key certificate to the private key

View File

@ -1,39 +0,0 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace QRBee.Core.Security
{
/// <summary>
/// Private key handler for API server
/// </summary>
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();
}
}
}

View File

@ -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}";
}
}

View File

@ -55,6 +55,7 @@ namespace QRBee.Droid
.AddSingleton<ISecurityService,AndroidSecurityService>() .AddSingleton<ISecurityService,AndroidSecurityService>()
.AddSingleton<ILocalSettings, LocalSettings>() .AddSingleton<ILocalSettings, LocalSettings>()
.AddSingleton<IQRScanner, QRScannerService>() .AddSingleton<IQRScanner, QRScannerService>()
.AddSingleton<IPrivateKeyHandler, AndroidPrivateKeyHandler>()
; ;
} }

View File

@ -82,6 +82,7 @@
<Compile Include="Resources\Resource.designer.cs" /> <Compile Include="Resources\Resource.designer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\LocalSettings.cs" /> <Compile Include="Services\LocalSettings.cs" />
<Compile Include="Services\AndroidPrivateKeyHandler.cs" />
<Compile Include="Services\QRScannerService.cs" /> <Compile Include="Services\QRScannerService.cs" />
<Compile Include="Services\AndroidSecurityService.cs" /> <Compile Include="Services\AndroidSecurityService.cs" />
</ItemGroup> </ItemGroup>

View File

@ -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
{
/// <summary>
/// Private key handler for API server
/// </summary>
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}";
/// <inheritdoc/>
public bool Exists()
=> File.Exists(PrivateKeyFileName);
/// <inheritdoc/>
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();
}
/// <inheritdoc/>
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;
}
/// <summary>
/// Generate EXPORTABLE certificate
/// </summary>
/// <param name="subjectName"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Generate CA certificate request (i.e. with KeyCertSign usage extension)
/// </summary>
/// <param name="distinguishedName"></param>
/// <param name="rsa"></param>
/// <returns></returns>
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;
}
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/>
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
}
}
}
}

View File

@ -30,9 +30,7 @@ namespace QRBee
addPlatformServices?.Invoke(services); addPlatformServices?.Invoke(services);
// TODO: Add core services here // TODO: Add core services here
services
.AddSingleton<IPrivateKeyHandler, ClientPrivateKeyHandler>()
;
// Add ViewModels // Add ViewModels
services services

View File

@ -1,23 +0,0 @@
using QRBee.Core.Security;
namespace QRBee.Services
{
/// <summary>
/// Private key handler for Client side
/// </summary>
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;
}
}
}

View File

@ -4,6 +4,7 @@ using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using QRBee.Core; using QRBee.Core;
using QRBee.Core.Data; using QRBee.Core.Data;
using QRBee.Core.Security;
using QRBee.Services; using QRBee.Services;
using QRBee.Views; using QRBee.Views;
using Xamarin.Forms; using Xamarin.Forms;
@ -13,15 +14,18 @@ namespace QRBee.ViewModels
internal class RegisterViewModel: BaseViewModel internal class RegisterViewModel: BaseViewModel
{ {
private readonly ILocalSettings _settings; private readonly ILocalSettings _settings;
private readonly IPrivateKeyHandler _privateKeyHandler;
private readonly ISecurityService _securityService;
private string _password1; private string _password1;
private string _password2; private string _password2;
public RegisterViewModel(ILocalSettings localSettings) public RegisterViewModel(ILocalSettings localSettings, IPrivateKeyHandler privateKeyHandler, ISecurityService securityService)
{ {
_settings = localSettings; _settings = localSettings;
_privateKeyHandler = privateKeyHandler;
_securityService = securityService;
RegisterCommand = new Command(OnRegisterClicked); RegisterCommand = new Command(OnRegisterClicked);
var settings = localSettings.LoadSettings(); var settings = localSettings.LoadSettings();
Name = settings.Name; Name = settings.Name;
Email = settings.Email; Email = settings.Email;
DateOfBirth = settings.DateOfBirth; DateOfBirth = settings.DateOfBirth;
@ -115,15 +119,21 @@ namespace QRBee.ViewModels
settings.IssueNo = IssueNo; settings.IssueNo = IssueNo;
settings.ValidFrom = ValidFrom; settings.ValidFrom = ValidFrom;
settings.Name = Name; settings.Name = Name;
settings.PIN = Pin; settings.PIN = Pin;
await _settings.SaveSettings(settings); await _settings.SaveSettings(settings);
if (!_privateKeyHandler.Exists())
{
_privateKeyHandler.GeneratePrivateKey(settings.Name);
}
var request = new RegistrationRequest var request = new RegistrationRequest
{ {
DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"), DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"),
Email = Email, Email = Email,
Name = Name, Name = Name,
CertificateRequest = _privateKeyHandler.CreateCertificateRequest(),
RegisterAsMerchant = false RegisterAsMerchant = false
}; };
@ -136,6 +146,9 @@ namespace QRBee.ViewModels
settings.ClientId = response.ClientId; settings.ClientId = response.ClientId;
await _settings.SaveSettings(settings); 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(); var page = Application.Current.MainPage.Navigation.NavigationStack.LastOrDefault();
await page.DisplayAlert("Success", "You have been registered successfully", "Ok"); await page.DisplayAlert("Success", "You have been registered successfully", "Ok");
} }

View File

@ -4,6 +4,7 @@ using MongoDB.Driver;
using QRBee.Api; using QRBee.Api;
using QRBee.Api.Services; using QRBee.Api.Services;
using QRBee.Api.Services.Database; using QRBee.Api.Services.Database;
using QRBee.Core.Security;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -24,10 +25,12 @@ builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
builder.Services builder.Services
.AddSingleton<IQRBeeAPI,QRBeeAPI>() .AddSingleton<IQRBeeAPI,QRBeeAPIService>()
.AddSingleton<IStorage, Storage>() .AddSingleton<IStorage, Storage>()
.Configure<DatabaseSettings>(builder.Configuration.GetSection("QRBeeDatabase")) .Configure<DatabaseSettings>(builder.Configuration.GetSection("QRBeeDatabase"))
.AddSingleton<IMongoClient>( cfg => new MongoClient(cfg.GetRequiredService<IOptions<DatabaseSettings>>().Value.ToMongoDbSettings())) .AddSingleton<IMongoClient>( cfg => new MongoClient(cfg.GetRequiredService<IOptions<DatabaseSettings>>().Value.ToMongoDbSettings()))
.AddSingleton<IPrivateKeyHandler, ServerPrivateKeyHandler>()
.AddSingleton<ISecurityService, SecurityService>()
; ;
var app = builder.Build(); var app = builder.Build();

View File

@ -5,7 +5,7 @@ using QRBee.Core.Data;
namespace QRBee.Api.Services namespace QRBee.Api.Services
{ {
/// <summary> /// <summary>
/// QRBeeAPI interface /// QRBeeAPIService interface
/// </summary> /// </summary>
public interface IQRBeeAPI public interface IQRBeeAPI
{ {

View File

@ -1,23 +1,43 @@
using System.Globalization; using System.Globalization;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using QRBee.Api.Services.Database; using QRBee.Api.Services.Database;
using QRBee.Core; using QRBee.Core;
using QRBee.Core.Data; using QRBee.Core.Data;
using QRBee.Core.Security;
namespace QRBee.Api.Services namespace QRBee.Api.Services
{ {
/// <summary> /// <summary>
/// Implementation of <see href="IQRBeeAPI"/> /// Implementation of <see href="IQRBeeAPI"/>
/// </summary> /// </summary>
public class QRBeeAPI: IQRBeeAPI public class QRBeeAPIService: IQRBeeAPI
{ {
private readonly IStorage _storage; 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 MaxNameLength = 512;
private const int MaxEmailLength = 512; private const int MaxEmailLength = 512;
public QRBeeAPI(IStorage storage) public QRBeeAPIService(IStorage storage, ISecurityService securityService, IPrivateKeyHandler privateKeyHandler)
{ {
_storage = storage; _storage = storage;
_securityService = securityService;
_privateKeyHandler = privateKeyHandler;
Init(_privateKeyHandler);
}
private static void Init(IPrivateKeyHandler privateKeyHandler)
{
lock (_lock)
{
if (!privateKeyHandler.Exists())
{
privateKeyHandler.GeneratePrivateKey();
}
}
} }
public async Task<RegistrationResponse> Register(RegistrationRequest request) public async Task<RegistrationResponse> Register(RegistrationRequest request)
@ -28,8 +48,14 @@ namespace QRBee.Api.Services
var info = Convert(request); var info = Convert(request);
var clientId = await _storage.PutUserInfo(info); 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) public Task Update(string clientId, RegistrationRequest request)
@ -45,7 +71,7 @@ namespace QRBee.Api.Services
return _storage.PutTransactionInfo(info); return _storage.PutTransactionInfo(info);
} }
private static void Validate(RegistrationRequest request) private void Validate(RegistrationRequest request)
{ {
if (request == null) if (request == null)
{ {
@ -55,6 +81,7 @@ namespace QRBee.Api.Services
var name = request.Name; var name = request.Name;
var email = request.Email; var email = request.Email;
var dateOfBirth = request.DateOfBirth; var dateOfBirth = request.DateOfBirth;
var certificateRequest = request.CertificateRequest;
if (string.IsNullOrEmpty(name) || name.All(char.IsLetter)==false || name.Length>=MaxNameLength) 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"); 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) private static UserInfo Convert(RegistrationRequest request)

View File

@ -10,6 +10,10 @@ namespace QRBee.Api.Services
internal class SecurityService : SecurityServiceBase 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) public SecurityService(IPrivateKeyHandler privateKeyHandler)
: base(privateKeyHandler) : base(privateKeyHandler)
{ {
@ -18,7 +22,20 @@ namespace QRBee.Api.Services
/// <inheritdoc/> /// <inheritdoc/>
public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey) 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;
} }
/// <summary> /// <summary>
@ -27,7 +44,7 @@ namespace QRBee.Api.Services
/// <param name="distinguishedName"></param> /// <param name="distinguishedName"></param>
/// <param name="rsa"></param> /// <param name="rsa"></param>
/// <returns></returns> /// <returns></returns>
private static CertificateRequest CreateRequest(X500DistinguishedName distinguishedName, RSA rsa) private static CertificateRequest CreateClientCertRequest(X500DistinguishedName distinguishedName, RSA rsa)
{ {
var request = new CertificateRequest( var request = new CertificateRequest(
distinguishedName, distinguishedName,
@ -41,19 +58,11 @@ namespace QRBee.Api.Services
X509KeyUsageFlags.DataEncipherment X509KeyUsageFlags.DataEncipherment
| X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.KeyEncipherment
| X509KeyUsageFlags.DigitalSignature, | X509KeyUsageFlags.DigitalSignature,
//| X509KeyUsageFlags.KeyCertSign,
false)); false));
// request.CertificateExtensions.Add(
// new X509EnhancedKeyUsageExtension(
// new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
return request; return request;
} }
private const string CertHeader = "-----BEGIN CERTIFICATE-----";
private const string CertFooter = "-----END CERTIFICATE-----";
/// <inheritdoc/> /// <inheritdoc/>
public override X509Certificate2 Deserialize(string pemData) public override X509Certificate2 Deserialize(string pemData)
{ {

View File

@ -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 namespace QRBee.Api.Services
{ {
/// <summary> /// <summary>
/// Private key handler for API server /// Private key handler for API server
/// </summary> /// </summary>
public class ServerPrivateKeyHandler : PrivateKeyHandlerBase public class ServerPrivateKeyHandler : IPrivateKeyHandler
{ {
private const string CACommonName = "QRBee-CA"; private X509Certificate2? _certificate;
private readonly object _syncObject = new object();
private const string VeryBadAndInsecureCertificatePassword = "U…Š)+œ¶€=ø‘ c¬Í↨ð´áY/ÿ☼æX";
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}";
/// <inheritdoc/>
public bool Exists()
=> File.Exists(PrivateKeyFileName);
/// <inheritdoc/>
public ReadableCertificateRequest GeneratePrivateKey(string? subjectName)
{ {
CertificatePassword = VeryBadAndInsecureCertificatePassword; // locking used to make sure that only one thread generating a private key
CommonName = CACommonName; 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();
}
/// <inheritdoc/>
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();
// }
//}
/// <summary>
/// Generate EXPORTABLE certificate
/// </summary>
/// <param name="subjectName"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Generate CA certificate request (i.e. with KeyCertSign usage extension)
/// </summary>
/// <param name="distinguishedName"></param>
/// <param name="rsa"></param>
/// <returns></returns>
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;
}
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/>
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
}
} }
} }