using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using QRBee.Core.Security; namespace QRBee.Api.Services { /// /// Private key handler for API server /// public class ServerPrivateKeyHandler : IPrivateKeyHandler { private readonly ILogger _logger; private X509Certificate2? _certificate; private readonly object _syncObject = new object(); private const string FileName = "private_key.p12"; private const int RSABits = 2048; private const int CertificateValidityDays = 3650; private const string VeryBadNeverUseCertificatePassword = "+ñèbòFëc׎ßRúß¿ãçPN"; private string PrivateKeyFileName => $"{Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData)}/{FileName}"; public ServerPrivateKeyHandler(ILogger logger) { _logger = logger; } /// public bool Exists() => File.Exists(PrivateKeyFileName); /// public ReadableCertificateRequest GeneratePrivateKey(string subjectName) { // locking used to make sure that only one thread generating a private key lock (_syncObject) { _logger.LogDebug("Generating private key"); var pk = CreateSelfSignedServerCertificate(subjectName); var pkcs12data = pk.Export(X509ContentType.Pfx, VeryBadNeverUseCertificatePassword); File.WriteAllBytes(PrivateKeyFileName, pkcs12data); _certificate?.Dispose(); _certificate = new X509Certificate2(pkcs12data, VeryBadNeverUseCertificatePassword); _logger.LogInformation($"Private key generated: {PrivateKeyFileName}"); } return CreateCertificateRequest(subjectName); } /// 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 = Load(); var rsa = pk.GetRSAPrivateKey(); if (rsa == null) { throw new ApplicationException("Object missing public key."); } var request = new ReadableCertificateRequest { RsaPublicKey = ExportKey(rsa,false), SubjectName = subjectName }; 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 StringRSAParameters ExportKey(RSA rsa, bool includePrivateKey) { var rsaParameters = rsa.ExportParameters(includePrivateKey); var stringParameters = new StringRSAParameters { StringExponent = SafeConvertToBase64(rsaParameters.Exponent), StringModulus = SafeConvertToBase64(rsaParameters.Modulus), StringP = SafeConvertToBase64(rsaParameters.P), StringQ = SafeConvertToBase64(rsaParameters.Q), StringDP = SafeConvertToBase64(rsaParameters.DP), StringDQ = SafeConvertToBase64(rsaParameters.DQ), StringInverseQ = SafeConvertToBase64(rsaParameters.InverseQ), StringD = SafeConvertToBase64(rsaParameters.D) }; return stringParameters; } private static string SafeConvertToBase64(byte[]? bytes) => bytes == null ? "" : Convert.ToBase64String(bytes); //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(); // } //} /// /// Generate EXPORTABLE certificate /// /// /// private X509Certificate2 CreateSelfSignedServerCertificate(string subjectName) { _logger.LogDebug("Creating self-signed certificate"); // 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 = CreateServerCertificateRequest(distinguishedName, rsa); var certificate = request.CreateSelfSigned( new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(CertificateValidityDays)) ); _logger.LogInformation("Self-signed certificate created"); return certificate; } /// /// Generate CA certificate request (i.e. with KeyCertSign usage extension) /// /// /// /// private static CertificateRequest CreateServerCertificateRequest(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 | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign, false)); request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true,false,0,true)); return request; } /// private X509Certificate2 Load() { if (_certificate != null) return _certificate; // double locking lock ( _syncObject ) { if (_certificate != null) return _certificate; if (!Exists()) GeneratePrivateKey("QRBeeCA"); _certificate = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUseCertificatePassword); return _certificate; } } public RSA LoadPrivateKey() { var pk = Load(); return pk.GetRSAPrivateKey()?? throw new ApplicationException("Private key not found"); } public X509Certificate2 GetCertificate() { var pk = Load(); var bytes = pk.Export(X509ContentType.Cert); var cert = new X509Certificate2(bytes); return cert; } /// 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, 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, VeryBadNeverUseCertificatePassword); File.WriteAllBytes(PrivateKeyFileName, pkcs12data); lock ( _syncObject ) { _certificate?.Dispose(); _certificate = null; // it will be loaded on the next access } } } }