Started working on Security service.

This commit is contained in:
Andrey Shabarshov 2022-02-01 17:18:13 +00:00
parent 1ae205298c
commit bde2f53f59
7 changed files with 344 additions and 1 deletions

View File

@ -1,4 +1,4 @@
namespace QRBee.Core
namespace QRBee.Core.Data
{
public record RegistrationRequest
{

View File

@ -0,0 +1,43 @@
using System.Security.Cryptography.X509Certificates;
namespace QRBee.Core.Security
{
/// <summary>
/// Private key manipulation methods
/// </summary>
public interface IPrivateKeyHandler
{
/// <summary>
/// Check if private key exists on this machine
/// </summary>
/// <returns></returns>
bool Exists();
/// <summary>
/// Generate new private key and store it
/// </summary>
/// <param name="subjectName"></param>
/// <returns>Certificate request to be sent to CA</returns>
byte [] GeneratePrivateKey(string? subjectName = null);
/// <summary>
/// Re-create certificate request if CA response was not received in time.
/// </summary>
/// <returns></returns>
byte[] CreateCertificateRequest();
/// <summary>
/// Attach CA-generated public key certificate to the private key
/// and store it
/// </summary>
/// <param name="cert"></param>
void AttachCertificate(X509Certificate2 cert);
/// <summary>
/// Load private key. Note that public key certificate part can be
/// self-signed until CA issues a proper certificate
/// </summary>
/// <returns></returns>
X509Certificate2 LoadPrivateKey();
}
}

View File

@ -0,0 +1,89 @@
using System.Security.Cryptography.X509Certificates;
namespace QRBee.Core.Security
{
/// <summary>
/// All cryptographic primitives are here.
/// </summary>
public interface ISecurityService
{
// -------------------------- encryption --------------------------
/// <summary>
/// Sign block of data
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns>Signature</returns>
/// <exception cref="CryptographicException"></exception>
byte[] Sign(byte [] data);
/// <summary>
/// Verify digital signature
/// </summary>
/// <param name="data">Source data</param>
/// <param name="signature">Signature to check</param>
/// <param name="signedBy">Public key certificate to use</param>
/// <returns></returns>
/// <exception cref="CryptographicException"></exception>
bool Verify(byte [] data, byte [] signature, X509Certificate2 signedBy);
/// <summary>
/// Encrypt data for the selected client identified by X.509 certificate.
/// </summary>
/// <param name="data">Clear data to encrypt</param>
/// <param name="destCert">Certificate of the destination client</param>
/// <returns>Encrypted data</returns>
/// <exception cref="CryptographicException"></exception>
byte[] Encrypt(byte[] data, X509Certificate2 destCert);
/// <summary>
/// Decrypt data encrypted for this service
/// </summary>
/// <param name="data">Binary encrypted data</param>
/// <returns>Decrypted data</returns>
/// <exception cref="CryptographicException"></exception>
byte[] Decrypt(byte[] data);
// -------------------------- certificate services --------------------------
/// <summary>
/// Convert binary block to X509Certificate2.
/// <see cref="X509Certificate2.CreateFromPem"/>
/// </summary>
/// <param name="pemData">PEM-encoded certificate</param>
/// <returns></returns>
X509Certificate2 Deserialize(string pemData);
/// <summary>
/// Convert certificate to PEM-encoded string.
/// </summary>
/// <param name="cert"></param>
/// <returns></returns>
string Serialize(X509Certificate2 cert);
/// <summary>
/// Get certificate serial number.
/// </summary>
/// <param name="cert"></param>
/// <returns></returns>
string GetSerialNumber(X509Certificate2 cert);
/// <summary>
/// Check if certificate is valid for this particular service.
/// Note that such certificates will (and should) fail normal cert chain check.
/// Valid certificates issued by different authority will fail the test.
/// </summary>
/// <param name="destCert">Certificate to check</param>
/// <returns>True is certificate is valid for this service use</returns>
bool IsValid(X509Certificate2 destCert);
/// <summary>
/// Issue client certificate
/// </summary>
/// <param name="subjectName">Client name (goes to CN=)</param>
/// <param name="rsaPublicKey">Client's RSA public key</param>
/// <returns>Certificate</returns>
X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey);
}
}

View File

@ -0,0 +1,104 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
namespace QRBee.Core.Security
{
public abstract class SecurityServiceBase : ISecurityService
{
protected IPrivateKeyHandler PrivateKeyHandler { get; }
protected SecurityServiceBase(IPrivateKeyHandler privateKeyHandler)
{
PrivateKeyHandler = privateKeyHandler;
}
/// <inheritdoc/>
public abstract X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey);
/// <inheritdoc/>
public abstract X509Certificate2 Deserialize(string pemData);
/// <inheritdoc/>
public abstract string Serialize(X509Certificate2 cert);
/// <summary>
/// Subject may only contain letters, numbers, -, . and spaces.
/// Subject should not be an empty string.
/// International letters are supported, but not tested.
/// </summary>
/// <param name="subjectName"></param>
/// <returns>True for valid subject name</returns>
protected static bool IsValidSubjectName(string subjectName)
{
if (string.IsNullOrWhiteSpace(subjectName))
return false;
return Regex.IsMatch(@"[\w\s[0-9]\-\.]+", subjectName);
}
/// <inheritdoc/>
public byte[] Decrypt(byte[] data)
{
using var myCert = LoadPrivateKey();
using var rsa = myCert.GetRSAPrivateKey();
var res = rsa?.Decrypt(data, RSAEncryptionPadding.Pkcs1) ?? throw new CryptographicException("No private key found");
return res;
}
/// <inheritdoc/>
public byte [] Encrypt(byte[] data, X509Certificate2 destCert)
{
using var rsa = destCert.GetRSAPublicKey();
var res = rsa?.Encrypt(data, RSAEncryptionPadding.Pkcs1) ?? throw new CryptographicException("No destination public key found");
return res;
}
/// <inheritdoc/>
public bool IsValid(X509Certificate2 destCert)
{
// check if certificate issued by this service
return true;
}
/// <inheritdoc/>
public byte[] Sign(byte[] data)
{
using var myCert = LoadPrivateKey();
using var rsa = myCert.GetRSAPrivateKey();
var res = rsa?.SignData(data, 0, data.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No private key found");
return res;
}
/// <inheritdoc/>
public bool Verify(byte[] data, byte[] signature, X509Certificate2 signedBy)
{
if ( !IsValid(signedBy))
throw new CryptographicException("Signer's certificate is not valid");
using var rsa = signedBy.GetRSAPublicKey();
var res = rsa?.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No signer public key found");
return res;
}
/// <inheritdoc/>
public string GetSerialNumber(X509Certificate2 cert)
{
var serNo = cert.SerialNumber;
return serNo;
}
private X509Certificate2 LoadPrivateKey()
{
if (!PrivateKeyHandler.Exists())
PrivateKeyHandler.GeneratePrivateKey(); //TODO: subject name
var pk = PrivateKeyHandler.LoadPrivateKey();
if (!IsValid(pk) )
{
throw new CryptographicException("CA private key is not valid");
}
return pk;
}
}
}

View File

@ -80,6 +80,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\LocalSettings.cs" />
<Compile Include="Services\QRScannerService.cs" />
<Compile Include="Services\SecurityService.cs" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\AboutResources.txt" />
@ -106,6 +107,10 @@
<AndroidResource Include="Resources\drawable\icon_feed.png" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\QRBee.Core\QRBee.Core.csproj">
<Project>{7c461562-66ef-4894-8ad8-f27f0b94053f}</Project>
<Name>QRBee.Core</Name>
</ProjectReference>
<ProjectReference Include="..\QRBee\QRBee.csproj">
<Project>{C651AD58-D087-4261-8C8E-EBA6140F3E72}</Project>
<Name>QRBee</Name>

View File

@ -0,0 +1,40 @@
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using QRBee.Core.Security;
namespace QRBee.Api.Services
{
internal class SecurityService : SecurityServiceBase
{
public SecurityService(IPrivateKeyHandler privateKeyHandler)
: base(privateKeyHandler)
{
}
/// <inheritdoc/>
public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey)
{
throw new ApplicationException("Client never issues certificates");
}
/// <inheritdoc/>
public override X509Certificate2 Deserialize(string pemData)
{
throw new NotImplementedException();
//return X509Certificate2.CreateFromPem(pemData);
}
/// <inheritdoc/>
public override string Serialize(X509Certificate2 cert)
{
throw new NotImplementedException();
// https://stackoverflow.com/questions/43928064/export-private-public-keys-from-x509-certificate-to-pem
//var pem = PemEncoding.Write("CERTIFICATE", cert.RawData);
//return new string(pem);
}
}
}

View File

@ -0,0 +1,62 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using QRBee.Core.Security;
namespace QRBee.Api.Services
{
internal class SecurityService : SecurityServiceBase
{
public SecurityService(IPrivateKeyHandler privateKeyHandler)
: base(privateKeyHandler)
{
}
/// <inheritdoc/>
public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey)
{
if (!IsValidSubjectName(subjectName))
throw new CryptographicException("Invalid subject name");
// https://stackoverflow.com/questions/60930065/generate-and-sign-certificate-in-different-machines-c-sharp
using var publicKey = RSA.Create();
publicKey.ImportSubjectPublicKeyInfo(rsaPublicKey, out var nBytes);
//TODO: check that nBytes is within allowed range
var request = new CertificateRequest("CN=" + subjectName, publicKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.NonRepudiation, false));
request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
// create a new certificate
using var caPrivateKey = PrivateKeyHandler.LoadPrivateKey();
var certificate = request.Create(
caPrivateKey,
DateTimeOffset.UtcNow.AddSeconds(-1), // user can use it now
DateTimeOffset.UtcNow.AddDays(30), // user need to login every 30 days
Guid.NewGuid().ToByteArray());
return certificate;
}
/// <inheritdoc/>
public override X509Certificate2 Deserialize(string pemData)
{
return X509Certificate2.CreateFromPem(pemData);
}
/// <inheritdoc/>
public override string Serialize(X509Certificate2 cert)
{
// https://stackoverflow.com/questions/43928064/export-private-public-keys-from-x509-certificate-to-pem
var pem = PemEncoding.Write("CERTIFICATE", cert.RawData);
return new string(pem);
}
}
}