mirror of
https://github.com/NecroticBamboo/QRBee.git
synced 2025-12-21 12:11:53 +00:00
SecurityService implemented. Not working due to Android limitations (CertificateRequest).
This commit is contained in:
parent
551a44c38d
commit
6da55bc4a5
@ -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;
|
||||
|
||||
@ -18,13 +18,13 @@ namespace QRBee.Core.Security
|
||||
/// </summary>
|
||||
/// <param name="subjectName"></param>
|
||||
/// <returns>Certificate request to be sent to CA in PEM format</returns>
|
||||
string GeneratePrivateKey(string? subjectName = null);
|
||||
ReadableCertificateRequest GeneratePrivateKey(string? subjectName = null);
|
||||
|
||||
/// <summary>
|
||||
/// Re-create certificate request if CA response was not received in time.
|
||||
/// </summary>
|
||||
/// <returns>Certificate request to be sent to CA in PEM format</returns>
|
||||
string CreateCertificateRequest();
|
||||
ReadableCertificateRequest CreateCertificateRequest();
|
||||
|
||||
/// <summary>
|
||||
/// Attach CA-generated public key certificate to the private key
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
QRBee.Core/Security/ReadableCertificateRequest.cs
Normal file
29
QRBee.Core/Security/ReadableCertificateRequest.cs
Normal 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}";
|
||||
|
||||
}
|
||||
}
|
||||
@ -55,6 +55,7 @@ namespace QRBee.Droid
|
||||
.AddSingleton<ISecurityService,AndroidSecurityService>()
|
||||
.AddSingleton<ILocalSettings, LocalSettings>()
|
||||
.AddSingleton<IQRScanner, QRScannerService>()
|
||||
.AddSingleton<IPrivateKeyHandler, AndroidPrivateKeyHandler>()
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@ -82,6 +82,7 @@
|
||||
<Compile Include="Resources\Resource.designer.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Services\LocalSettings.cs" />
|
||||
<Compile Include="Services\AndroidPrivateKeyHandler.cs" />
|
||||
<Compile Include="Services\QRScannerService.cs" />
|
||||
<Compile Include="Services\AndroidSecurityService.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
162
QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs
Normal file
162
QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -30,9 +30,7 @@ namespace QRBee
|
||||
addPlatformServices?.Invoke(services);
|
||||
|
||||
// TODO: Add core services here
|
||||
services
|
||||
.AddSingleton<IPrivateKeyHandler, ClientPrivateKeyHandler>()
|
||||
;
|
||||
|
||||
|
||||
// Add ViewModels
|
||||
services
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -119,11 +123,17 @@ namespace QRBee.ViewModels
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@ -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<IQRBeeAPI,QRBeeAPI>()
|
||||
.AddSingleton<IQRBeeAPI,QRBeeAPIService>()
|
||||
.AddSingleton<IStorage, Storage>()
|
||||
.Configure<DatabaseSettings>(builder.Configuration.GetSection("QRBeeDatabase"))
|
||||
.AddSingleton<IMongoClient>( cfg => new MongoClient(cfg.GetRequiredService<IOptions<DatabaseSettings>>().Value.ToMongoDbSettings()))
|
||||
.AddSingleton<IPrivateKeyHandler, ServerPrivateKeyHandler>()
|
||||
.AddSingleton<ISecurityService, SecurityService>()
|
||||
;
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@ -5,7 +5,7 @@ using QRBee.Core.Data;
|
||||
namespace QRBee.Api.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// QRBeeAPI interface
|
||||
/// QRBeeAPIService interface
|
||||
/// </summary>
|
||||
public interface IQRBeeAPI
|
||||
{
|
||||
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of <see href="IQRBeeAPI"/>
|
||||
/// </summary>
|
||||
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<RegistrationResponse> 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)
|
||||
@ -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
|
||||
/// <inheritdoc/>
|
||||
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>
|
||||
@ -27,7 +44,7 @@ namespace QRBee.Api.Services
|
||||
/// <param name="distinguishedName"></param>
|
||||
/// <param name="rsa"></param>
|
||||
/// <returns></returns>
|
||||
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-----";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override X509Certificate2 Deserialize(string pemData)
|
||||
{
|
||||
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Private key handler for API server
|
||||
/// </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";
|
||||
private const string FileName = "private_key.p12";
|
||||
protected string CommonName { get; set; }
|
||||
private const int RSABits = 2048;
|
||||
private const int CertificateValidityDays = 3650;
|
||||
|
||||
public ServerPrivateKeyHandler()
|
||||
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;
|
||||
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();
|
||||
}
|
||||
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user