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>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

View File

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

View File

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

View File

@ -13,49 +13,55 @@ namespace QRBee.Droid.Services
/// </summary>
public class AndroidPrivateKeyHandler : IPrivateKeyHandler
{
private X509Certificate2? _certificate;
private X509Certificate2 _certificate;
private readonly object _syncObject = new object();
private const string FileName = "private_key.p12";
protected string CommonName { get; set; }
private const string RawRsaKeyFileName = "rsa.key";
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 CertificateValidityDays = 3650;
protected string CertificatePassword { get; set; }
private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}";
private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{SignedCertificateFileName}";
private string PrivateRsaKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{RawRsaKeyFileName}";
/// <inheritdoc/>
public bool Exists()
=> File.Exists(PrivateKeyFileName);
/// <inheritdoc/>
public ReadableCertificateRequest GeneratePrivateKey(string? subjectName)
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);
if ( File.Exists(PrivateRsaKeyFileName) )
File.Delete(PrivateRsaKeyFileName);
_certificate?.Dispose();
_certificate = new X509Certificate2(pkcs12data, CertificatePassword);
using var rsa = RSA.Create(RSABits);
var bytes = rsa.ExportEncryptedPkcs8PrivateKey(VeryBadNeverUsePrivateKeyPassword, new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, EncryptionIterationCount));
File.WriteAllBytes(PrivateRsaKeyFileName, bytes);
}
return CreateCertificateRequest();
return CreateCertificateRequest(subjectName);
}
/// <inheritdoc/>
public ReadableCertificateRequest CreateCertificateRequest()
public ReadableCertificateRequest CreateCertificateRequest(string subjectName)
{
var pk = LoadPrivateKey();
var rsa = pk.GetRSAPublicKey();
if (File.Exists(PrivateRsaKeyFileName))
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
{
RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()),
SubjectName = pk.SubjectName.Name
SubjectName = subjectName
};
var data = Encoding.UTF8.GetBytes(request.AsDataForSignature());
@ -128,7 +134,7 @@ namespace QRBee.Droid.Services
if (!Exists())
throw new CryptographicException("PrivateKey does not exist");
_certificate = new X509Certificate2(PrivateKeyFileName, CertificatePassword);
_certificate = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUsePrivateKeyPassword);
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
// 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();
if (rsa == null)
throw new CryptographicException("Can't get PrivateKey");
var newPk = cert.CopyWithPrivateKey(rsa);
var pkcs12data = newPk.Export(X509ContentType.Pfx, CertificatePassword);
var pkcs12data = newPk.Export(X509ContentType.Pfx, VeryBadNeverUsePrivateKeyPassword);
File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
lock ( _syncObject )

View File

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

View File

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

View File

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

View File

@ -7,6 +7,14 @@
<UserSecretsId>3b7dc7f1-0b82-4746-b99b-73c43c8826e0</UserSecretsId>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Log4Net.AspNetCore" Version="6.1.0" />
<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>
/// <param name="info">Information to be inserted</param>
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 QRBee.Api.Controllers;
@ -100,5 +101,50 @@ namespace QRBee.Api.Services.Database
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.Data;

View File

@ -1,4 +1,5 @@
using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using QRBee.Api.Services.Database;
@ -49,7 +50,9 @@ namespace QRBee.Api.Services
var clientId = await _storage.PutUserInfo(info);
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
{
@ -124,5 +127,17 @@ namespace QRBee.Api.Services
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 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 const string VeryBadNeverUseCertificatePassword = "+ñèbòFëc׎ßRúß¿ãçPN";
private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}";
@ -27,33 +26,38 @@ namespace QRBee.Api.Services
=> File.Exists(PrivateKeyFileName);
/// <inheritdoc/>
public ReadableCertificateRequest GeneratePrivateKey(string? subjectName)
public ReadableCertificateRequest GeneratePrivateKey(string subjectName)
{
// 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);
var pk = CreateSelfSignedServerCertificate(subjectName);
var pkcs12data = pk.Export(X509ContentType.Pfx, VeryBadNeverUseCertificatePassword);
File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
_certificate?.Dispose();
_certificate = new X509Certificate2(pkcs12data, CertificatePassword);
_certificate = new X509Certificate2(pkcs12data, VeryBadNeverUseCertificatePassword);
}
return CreateCertificateRequest();
return CreateCertificateRequest(subjectName);
}
/// <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.
var pk = LoadPrivateKey();
var rsa = pk.GetRSAPublicKey();
if (rsa == null)
{
throw new ApplicationException("Object missing public key.");
}
var request = new ReadableCertificateRequest
{
RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()),
SubjectName = pk.SubjectName.Name
SubjectName = subjectName
};
var data = Encoding.UTF8.GetBytes(request.AsDataForSignature());
@ -147,7 +151,7 @@ namespace QRBee.Api.Services
if (!Exists())
throw new CryptographicException("PrivateKey does not exist");
_certificate = new X509Certificate2(PrivateKeyFileName, CertificatePassword);
_certificate = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUseCertificatePassword);
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
// 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();
if (rsa == null)
throw new CryptographicException("Can't get PrivateKey");
var newPk = cert.CopyWithPrivateKey(rsa);
var pkcs12data = newPk.Export(X509ContentType.Pfx, CertificatePassword);
var pkcs12data = newPk.Export(X509ContentType.Pfx, VeryBadNeverUseCertificatePassword);
File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
lock ( _syncObject )