Certificate information can now be stored in database. Second attempt at key generation under Android still no luck.

This commit is contained in:
Andrey Shabarshov 2022-02-19 15:13:56 +00:00
parent 6da55bc4a5
commit e07b45d43f
14 changed files with 165 additions and 45 deletions

View File

@ -6,6 +6,14 @@
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>

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>
ReadableCertificateRequest 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>
ReadableCertificateRequest CreateCertificateRequest(); ReadableCertificateRequest CreateCertificateRequest(string subjectName);
/// <summary> /// <summary>
/// Attach CA-generated public key certificate to the private key /// Attach CA-generated public key certificate to the private key

View File

@ -38,6 +38,7 @@
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot> <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies> <BundleAssemblies>false</BundleAssemblies>
<MandroidI18n /> <MandroidI18n />
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>

View File

@ -13,49 +13,55 @@ namespace QRBee.Droid.Services
/// </summary> /// </summary>
public class AndroidPrivateKeyHandler : IPrivateKeyHandler public class AndroidPrivateKeyHandler : IPrivateKeyHandler
{ {
private X509Certificate2? _certificate; private X509Certificate2 _certificate;
private readonly object _syncObject = new object(); private readonly object _syncObject = new object();
private const string FileName = "private_key.p12"; private const string RawRsaKeyFileName = "rsa.key";
protected string CommonName { get; set; } private const string SignedCertificateFileName = "private_key.p12";
private const string VeryBadNeverUsePrivateKeyPassword = "’³¶¾]Ô<N◄¾♪¢ :6TyŽ÷ç♦Mô¶²ùPÎJj";
private const int EncryptionIterationCount = 534;
private const int RSABits = 2048; private const int RSABits = 2048;
private const int CertificateValidityDays = 3650; private const int CertificateValidityDays = 3650;
protected string CertificatePassword { get; set; } private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{SignedCertificateFileName}";
private string PrivateRsaKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{RawRsaKeyFileName}";
private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}";
/// <inheritdoc/> /// <inheritdoc/>
public bool Exists() public bool Exists()
=> File.Exists(PrivateKeyFileName); => File.Exists(PrivateKeyFileName);
/// <inheritdoc/> /// <inheritdoc/>
public ReadableCertificateRequest GeneratePrivateKey(string? subjectName) public ReadableCertificateRequest GeneratePrivateKey(string subjectName)
{ {
// locking used to make sure that only one thread generating a private key // locking used to make sure that only one thread generating a private key
lock (_syncObject) lock (_syncObject)
{ {
var pk = CreateSelfSignedClientCertificate(subjectName ?? CommonName); if ( File.Exists(PrivateRsaKeyFileName) )
var pkcs12data = pk.Export(X509ContentType.Pfx, CertificatePassword); File.Delete(PrivateRsaKeyFileName);
File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
_certificate?.Dispose(); using var rsa = RSA.Create(RSABits);
_certificate = new X509Certificate2(pkcs12data, CertificatePassword); var bytes = rsa.ExportEncryptedPkcs8PrivateKey(VeryBadNeverUsePrivateKeyPassword, new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, EncryptionIterationCount));
File.WriteAllBytes(PrivateRsaKeyFileName, bytes);
} }
return CreateCertificateRequest(); return CreateCertificateRequest(subjectName);
} }
/// <inheritdoc/> /// <inheritdoc/>
public ReadableCertificateRequest CreateCertificateRequest() public ReadableCertificateRequest CreateCertificateRequest(string subjectName)
{ {
var pk = LoadPrivateKey(); if (File.Exists(PrivateRsaKeyFileName))
var rsa = pk.GetRSAPublicKey(); throw new ApplicationException("Private key does not exist");
var bytes = File.ReadAllBytes(PrivateRsaKeyFileName);
using var rsa = RSA.Create(RSABits);
rsa.ImportEncryptedPkcs8PrivateKey(VeryBadNeverUsePrivateKeyPassword, bytes, out _);
var request = new ReadableCertificateRequest var request = new ReadableCertificateRequest
{ {
RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()), RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()),
SubjectName = pk.SubjectName.Name SubjectName = subjectName
}; };
var data = Encoding.UTF8.GetBytes(request.AsDataForSignature()); var data = Encoding.UTF8.GetBytes(request.AsDataForSignature());
@ -128,7 +134,7 @@ namespace QRBee.Droid.Services
if (!Exists()) if (!Exists())
throw new CryptographicException("PrivateKey does not exist"); throw new CryptographicException("PrivateKey does not exist");
_certificate = new X509Certificate2(PrivateKeyFileName, CertificatePassword); _certificate = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUsePrivateKeyPassword);
return _certificate; return _certificate;
} }
} }
@ -140,14 +146,14 @@ namespace QRBee.Droid.Services
// https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net // 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 // we can't use LoadPrivateKey here as it creating non-exportable key
var pk = new X509Certificate2(PrivateKeyFileName, CertificatePassword, X509KeyStorageFlags.Exportable); var pk = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUsePrivateKeyPassword, X509KeyStorageFlags.Exportable);
using var rsa = pk.GetRSAPrivateKey(); using var rsa = pk.GetRSAPrivateKey();
if (rsa == null) if (rsa == null)
throw new CryptographicException("Can't get PrivateKey"); throw new CryptographicException("Can't get PrivateKey");
var newPk = cert.CopyWithPrivateKey(rsa); var newPk = cert.CopyWithPrivateKey(rsa);
var pkcs12data = newPk.Export(X509ContentType.Pfx, CertificatePassword); var pkcs12data = newPk.Export(X509ContentType.Pfx, VeryBadNeverUsePrivateKeyPassword);
File.WriteAllBytes(PrivateKeyFileName, pkcs12data); File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
lock ( _syncObject ) lock ( _syncObject )

View File

@ -51,6 +51,7 @@
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements> <CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
<MtouchLink>None</MtouchLink> <MtouchLink>None</MtouchLink>
<MtouchInterpreter>-all</MtouchInterpreter> <MtouchInterpreter>-all</MtouchInterpreter>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' ">
<DebugType>none</DebugType> <DebugType>none</DebugType>
@ -133,10 +134,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Plugin.Fingerprint"> <PackageReference Include="Plugin.Fingerprint">
<Version>2.1.4</Version> <Version>2.1.5</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2244" /> <PackageReference Include="Xamarin.Forms" Version="5.0.0.2337" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" /> <PackageReference Include="Xamarin.Essentials" Version="1.7.1" />
<PackageReference Include="ZXing.Net.Mobile"> <PackageReference Include="ZXing.Net.Mobile">
<Version>2.4.1</Version> <Version>2.4.1</Version>
</PackageReference> </PackageReference>

View File

@ -7,17 +7,19 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Plugin.Fingerprint" Version="2.1.4" /> <PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2244" /> <PackageReference Include="Xamarin.Forms" Version="5.0.0.2337" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" /> <PackageReference Include="Xamarin.Essentials" Version="1.7.1" />
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" /> <PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" /> <PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
</ItemGroup> </ItemGroup>

View File

@ -133,7 +133,7 @@ namespace QRBee.ViewModels
DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"), DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"),
Email = Email, Email = Email,
Name = Name, Name = Name,
CertificateRequest = _privateKeyHandler.CreateCertificateRequest(), CertificateRequest = _privateKeyHandler.CreateCertificateRequest(Email),
RegisterAsMerchant = false RegisterAsMerchant = false
}; };

View File

@ -7,6 +7,14 @@
<UserSecretsId>3b7dc7f1-0b82-4746-b99b-73c43c8826e0</UserSecretsId> <UserSecretsId>3b7dc7f1-0b82-4746-b99b-73c43c8826e0</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Log4Net.AspNetCore" Version="6.1.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Log4Net.AspNetCore" Version="6.1.0" />
<PackageReference Include="MongoDB.Driver" Version="2.14.1" /> <PackageReference Include="MongoDB.Driver" Version="2.14.1" />

View File

@ -0,0 +1,14 @@
using System.Security.Cryptography.X509Certificates;
using MongoDB.Bson.Serialization.Attributes;
namespace QRBee.Api.Services.Database
{
public class CertificateInfo
{
[BsonId] public string? Id { get; set; }
public string? ClientId { get; set; }
public string? Certificate { get; set; }
public DateTime ServerTimeStamp { get; set; }
}
}

View File

@ -30,5 +30,19 @@
/// </summary> /// </summary>
/// <param name="info">Information to be inserted</param> /// <param name="info">Information to be inserted</param>
Task PutTransactionInfo(TransactionInfo info); Task PutTransactionInfo(TransactionInfo info);
/// <summary>
/// Inserts CertificateInfo into database
/// </summary>
/// <param name="info">Information to be inserted</param>
/// <returns></returns>
Task InsertCertificate(CertificateInfo info);
/// <summary>
/// Retrieve certificate information from database
/// </summary>
/// <param name="id">Identifier by which certificate information will be retrieved</param>
/// <returns>Certificate information</returns>
Task<CertificateInfo> GetCertificateInfo(string id);
} }
} }

View File

@ -1,4 +1,5 @@
using Microsoft.Extensions.Options; using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Options;
using MongoDB.Driver; using MongoDB.Driver;
using QRBee.Api.Controllers; using QRBee.Api.Controllers;
@ -100,5 +101,50 @@ namespace QRBee.Api.Services.Database
return cursor.Current.FirstOrDefault(); return cursor.Current.FirstOrDefault();
} }
public async Task InsertCertificate(CertificateInfo info)
{
var collection = _database.GetCollection<CertificateInfo>("Certificates");
if (info.Id == null)
{
throw new ApplicationException("Info Id is null.");
}
var certificate = await TryGetCertificateInfo(info.Id);
if (certificate == null)
{
await collection.InsertOneAsync(info);
_logger.LogInformation($"Inserted new certificate with ID: {info.Id}");
return;
}
_logger.LogInformation($"Found certificate with ID: {info.Id}");
}
/// <summary>
/// Try to find if the Certificate already exists in the database
/// </summary>
/// <param name="id">parameter by which to find CertificateInfo</param>
/// <returns>null if certificate doesn't exist or CertificateInfo</returns>
private async Task<CertificateInfo?> TryGetCertificateInfo(string id)
{
var collection = _database.GetCollection<CertificateInfo>("Transactions");
using var cursor = await collection.FindAsync($"{{ Id: \"{id}\" }}");
if (!await cursor.MoveNextAsync())
{
return null;
}
return cursor.Current.FirstOrDefault();
}
public async Task<CertificateInfo> GetCertificateInfo(string id)
{
var certificate = await TryGetCertificateInfo(id);
return certificate ?? throw new ApplicationException($"Certificate with Id: {id} not found.");
}
} }
} }

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Mvc;
using QRBee.Core; using QRBee.Core;
using QRBee.Core.Data; using QRBee.Core.Data;

View File

@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using QRBee.Api.Services.Database; using QRBee.Api.Services.Database;
@ -49,7 +50,9 @@ namespace QRBee.Api.Services
var clientId = await _storage.PutUserInfo(info); var clientId = await _storage.PutUserInfo(info);
var clientCertificate = _securityService.CreateCertificate(clientId,System.Convert.FromBase64String(request.CertificateRequest.RsaPublicKey)); var clientCertificate = _securityService.CreateCertificate(clientId,System.Convert.FromBase64String(request.CertificateRequest.RsaPublicKey));
//TODO save certificate to certificate mongoDB collection
var convertedClientCertificate = Convert(clientCertificate, clientId);
await _storage.InsertCertificate(convertedClientCertificate);
return new RegistrationResponse return new RegistrationResponse
{ {
@ -124,5 +127,17 @@ namespace QRBee.Api.Services
return new TransactionInfo(request, DateTime.UtcNow); return new TransactionInfo(request, DateTime.UtcNow);
} }
private CertificateInfo Convert(X509Certificate2 certificate, string clientId)
{
var convertedCertificate = _securityService.Serialize(certificate);
return new CertificateInfo
{
Id = certificate.SerialNumber,
ClientId = clientId,
Certificate = convertedCertificate,
ServerTimeStamp = DateTime.UtcNow
};
}
} }
} }

View File

@ -14,11 +14,10 @@ namespace QRBee.Api.Services
private readonly object _syncObject = new object(); private readonly object _syncObject = new object();
private const string FileName = "private_key.p12"; private const string FileName = "private_key.p12";
protected string CommonName { get; set; }
private const int RSABits = 2048; private const int RSABits = 2048;
private const int CertificateValidityDays = 3650; private const int CertificateValidityDays = 3650;
protected string CertificatePassword { get; set; } private const string VeryBadNeverUseCertificatePassword = "+ñèbòFëc׎ßRúß¿ãçPN";
private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}"; private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}";
@ -27,33 +26,38 @@ namespace QRBee.Api.Services
=> File.Exists(PrivateKeyFileName); => File.Exists(PrivateKeyFileName);
/// <inheritdoc/> /// <inheritdoc/>
public ReadableCertificateRequest GeneratePrivateKey(string? subjectName) public ReadableCertificateRequest GeneratePrivateKey(string subjectName)
{ {
// locking used to make sure that only one thread generating a private key // locking used to make sure that only one thread generating a private key
lock (_syncObject) lock (_syncObject)
{ {
var pk = CreateSelfSignedServerCertificate(subjectName ?? CommonName); var pk = CreateSelfSignedServerCertificate(subjectName);
var pkcs12data = pk.Export(X509ContentType.Pfx, CertificatePassword); var pkcs12data = pk.Export(X509ContentType.Pfx, VeryBadNeverUseCertificatePassword);
File.WriteAllBytes(PrivateKeyFileName, pkcs12data); File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
_certificate?.Dispose(); _certificate?.Dispose();
_certificate = new X509Certificate2(pkcs12data, CertificatePassword); _certificate = new X509Certificate2(pkcs12data, VeryBadNeverUseCertificatePassword);
} }
return CreateCertificateRequest(); return CreateCertificateRequest(subjectName);
} }
/// <inheritdoc/> /// <inheritdoc/>
public ReadableCertificateRequest CreateCertificateRequest() public ReadableCertificateRequest CreateCertificateRequest(string subjectName)
{ {
//TODO in fact server should create certificate request in standard format if we ever want to get externally sighed certificate. //TODO in fact server should create certificate request in standard format if we ever want to get externally sighed certificate.
var pk = LoadPrivateKey(); var pk = LoadPrivateKey();
var rsa = pk.GetRSAPublicKey(); var rsa = pk.GetRSAPublicKey();
if (rsa == null)
{
throw new ApplicationException("Object missing public key.");
}
var request = new ReadableCertificateRequest var request = new ReadableCertificateRequest
{ {
RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()), RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()),
SubjectName = pk.SubjectName.Name SubjectName = subjectName
}; };
var data = Encoding.UTF8.GetBytes(request.AsDataForSignature()); var data = Encoding.UTF8.GetBytes(request.AsDataForSignature());
@ -147,7 +151,7 @@ namespace QRBee.Api.Services
if (!Exists()) if (!Exists())
throw new CryptographicException("PrivateKey does not exist"); throw new CryptographicException("PrivateKey does not exist");
_certificate = new X509Certificate2(PrivateKeyFileName, CertificatePassword); _certificate = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUseCertificatePassword);
return _certificate; return _certificate;
} }
} }
@ -159,14 +163,14 @@ namespace QRBee.Api.Services
// https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net // 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 // we can't use LoadPrivateKey here as it creating non-exportable key
var pk = new X509Certificate2(PrivateKeyFileName, CertificatePassword, X509KeyStorageFlags.Exportable); var pk = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUseCertificatePassword, X509KeyStorageFlags.Exportable);
using var rsa = pk.GetRSAPrivateKey(); using var rsa = pk.GetRSAPrivateKey();
if (rsa == null) if (rsa == null)
throw new CryptographicException("Can't get PrivateKey"); throw new CryptographicException("Can't get PrivateKey");
var newPk = cert.CopyWithPrivateKey(rsa); var newPk = cert.CopyWithPrivateKey(rsa);
var pkcs12data = newPk.Export(X509ContentType.Pfx, CertificatePassword); var pkcs12data = newPk.Export(X509ContentType.Pfx, VeryBadNeverUseCertificatePassword);
File.WriteAllBytes(PrivateKeyFileName, pkcs12data); File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
lock ( _syncObject ) lock ( _syncObject )