From bde2f53f5922b0cf1f5b37efba8d6816bb2c4c02 Mon Sep 17 00:00:00 2001 From: Andrey Shabarshov Date: Tue, 1 Feb 2022 17:18:13 +0000 Subject: [PATCH] Started working on Security service. --- QRBee.Core/Data/RegistrationRequest.cs | 2 +- QRBee.Core/Security/IPrivateKeyHandler.cs | 43 ++++++++ QRBee.Core/Security/ISecurityService.cs | 89 +++++++++++++++ QRBee.Core/Security/SecurityServiceBase.cs | 104 ++++++++++++++++++ QRBee/QRBee.Android/QRBee.Android.csproj | 5 + .../QRBee.Android/Services/SecurityService.cs | 40 +++++++ QRBeeApi/Services/SecurityService.cs | 62 +++++++++++ 7 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 QRBee.Core/Security/IPrivateKeyHandler.cs create mode 100644 QRBee.Core/Security/ISecurityService.cs create mode 100644 QRBee.Core/Security/SecurityServiceBase.cs create mode 100644 QRBee/QRBee.Android/Services/SecurityService.cs create mode 100644 QRBeeApi/Services/SecurityService.cs diff --git a/QRBee.Core/Data/RegistrationRequest.cs b/QRBee.Core/Data/RegistrationRequest.cs index 9286ae6..97397e0 100644 --- a/QRBee.Core/Data/RegistrationRequest.cs +++ b/QRBee.Core/Data/RegistrationRequest.cs @@ -1,4 +1,4 @@ -namespace QRBee.Core +namespace QRBee.Core.Data { public record RegistrationRequest { diff --git a/QRBee.Core/Security/IPrivateKeyHandler.cs b/QRBee.Core/Security/IPrivateKeyHandler.cs new file mode 100644 index 0000000..873f67e --- /dev/null +++ b/QRBee.Core/Security/IPrivateKeyHandler.cs @@ -0,0 +1,43 @@ +using System.Security.Cryptography.X509Certificates; + +namespace QRBee.Core.Security +{ + /// + /// Private key manipulation methods + /// + public interface IPrivateKeyHandler + { + /// + /// Check if private key exists on this machine + /// + /// + bool Exists(); + + /// + /// Generate new private key and store it + /// + /// + /// Certificate request to be sent to CA + byte [] GeneratePrivateKey(string? subjectName = null); + + /// + /// Re-create certificate request if CA response was not received in time. + /// + /// + byte[] CreateCertificateRequest(); + + /// + /// Attach CA-generated public key certificate to the private key + /// and store it + /// + /// + void AttachCertificate(X509Certificate2 cert); + + /// + /// Load private key. Note that public key certificate part can be + /// self-signed until CA issues a proper certificate + /// + /// + X509Certificate2 LoadPrivateKey(); + } +} diff --git a/QRBee.Core/Security/ISecurityService.cs b/QRBee.Core/Security/ISecurityService.cs new file mode 100644 index 0000000..e151c1c --- /dev/null +++ b/QRBee.Core/Security/ISecurityService.cs @@ -0,0 +1,89 @@ +using System.Security.Cryptography.X509Certificates; + +namespace QRBee.Core.Security +{ + /// + /// All cryptographic primitives are here. + /// + public interface ISecurityService + { + // -------------------------- encryption -------------------------- + + /// + /// Sign block of data + /// + /// Data to sign + /// Signature + /// + byte[] Sign(byte [] data); + + /// + /// Verify digital signature + /// + /// Source data + /// Signature to check + /// Public key certificate to use + /// + /// + bool Verify(byte [] data, byte [] signature, X509Certificate2 signedBy); + + /// + /// Encrypt data for the selected client identified by X.509 certificate. + /// + /// Clear data to encrypt + /// Certificate of the destination client + /// Encrypted data + /// + byte[] Encrypt(byte[] data, X509Certificate2 destCert); + + /// + /// Decrypt data encrypted for this service + /// + /// Binary encrypted data + /// Decrypted data + /// + byte[] Decrypt(byte[] data); + + + // -------------------------- certificate services -------------------------- + + /// + /// Convert binary block to X509Certificate2. + /// + /// + /// PEM-encoded certificate + /// + X509Certificate2 Deserialize(string pemData); + + /// + /// Convert certificate to PEM-encoded string. + /// + /// + /// + string Serialize(X509Certificate2 cert); + + /// + /// Get certificate serial number. + /// + /// + /// + string GetSerialNumber(X509Certificate2 cert); + + /// + /// 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. + /// + /// Certificate to check + /// True is certificate is valid for this service use + bool IsValid(X509Certificate2 destCert); + + /// + /// Issue client certificate + /// + /// Client name (goes to CN=) + /// Client's RSA public key + /// Certificate + X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey); + } +} diff --git a/QRBee.Core/Security/SecurityServiceBase.cs b/QRBee.Core/Security/SecurityServiceBase.cs new file mode 100644 index 0000000..c16d2ac --- /dev/null +++ b/QRBee.Core/Security/SecurityServiceBase.cs @@ -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; + } + + + /// + public abstract X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey); + /// + public abstract X509Certificate2 Deserialize(string pemData); + /// + public abstract string Serialize(X509Certificate2 cert); + + /// + /// Subject may only contain letters, numbers, -, . and spaces. + /// Subject should not be an empty string. + /// International letters are supported, but not tested. + /// + /// + /// True for valid subject name + protected static bool IsValidSubjectName(string subjectName) + { + if (string.IsNullOrWhiteSpace(subjectName)) + return false; + return Regex.IsMatch(@"[\w\s[0-9]\-\.]+", subjectName); + } + + /// + 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; + } + + /// + 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; + } + + /// + public bool IsValid(X509Certificate2 destCert) + { + // check if certificate issued by this service + return true; + } + + /// + 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; + } + + /// + 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; + } + + /// + 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; + } + + } +} diff --git a/QRBee/QRBee.Android/QRBee.Android.csproj b/QRBee/QRBee.Android/QRBee.Android.csproj index 8d89eaa..2673c98 100644 --- a/QRBee/QRBee.Android/QRBee.Android.csproj +++ b/QRBee/QRBee.Android/QRBee.Android.csproj @@ -80,6 +80,7 @@ + @@ -106,6 +107,10 @@ + + {7c461562-66ef-4894-8ad8-f27f0b94053f} + QRBee.Core + {C651AD58-D087-4261-8C8E-EBA6140F3E72} QRBee diff --git a/QRBee/QRBee.Android/Services/SecurityService.cs b/QRBee/QRBee.Android/Services/SecurityService.cs new file mode 100644 index 0000000..9a2c525 --- /dev/null +++ b/QRBee/QRBee.Android/Services/SecurityService.cs @@ -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) + { + } + + /// + public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey) + { + throw new ApplicationException("Client never issues certificates"); + } + + /// + public override X509Certificate2 Deserialize(string pemData) + { + throw new NotImplementedException(); + //return X509Certificate2.CreateFromPem(pemData); + } + + /// + 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); + } + } + +} diff --git a/QRBeeApi/Services/SecurityService.cs b/QRBeeApi/Services/SecurityService.cs new file mode 100644 index 0000000..d009d2a --- /dev/null +++ b/QRBeeApi/Services/SecurityService.cs @@ -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) + { + } + + /// + 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; + + } + + /// + public override X509Certificate2 Deserialize(string pemData) + { + return X509Certificate2.CreateFromPem(pemData); + } + + /// + 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); + } + } + +}