using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using CertificateManager; using CertificateManager.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Infrastructure.Common; public class CertificateService { private const X509KeyUsageFlags ROOT_CA_X509_KEY_USAGE_FLAGS = X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature; private const X509KeyStorageFlags X509_KEY_STORAGE_FLAGS_MACHINE = X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable; private const X509KeyUsageFlags CERT_X509_KEY_USAGE_FLAGS = X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature; private const string ROOT_CA_CN = "Taito Arcade Machine CA"; private const string CERT_CN = "GC local server"; private const string CERT_DIR = "Certificates"; private const string CERT_FILE_NAME = "cert.pfx"; private const string ROOT_CERT_FILE_NAME = "root.pfx"; private static readonly string CERT_PATH = Path.Combine(CERT_DIR, CERT_FILE_NAME); private static readonly string ROOT_CERT_PATH = Path.Combine(CERT_DIR, ROOT_CERT_FILE_NAME); private ILogger logger; private static readonly DistinguishedName ROOT_CA_DISTINGUISHED_NAME = new() { CommonName = ROOT_CA_CN }; private static readonly DistinguishedName CERT_DISTINGUISHED_NAME = new() { CommonName = CERT_CN }; private static readonly BasicConstraints ROOT_CA_BASIC_CONSTRAINTS = new() { CertificateAuthority = true, HasPathLengthConstraint = true, PathLengthConstraint = 3, Critical = true }; public static readonly BasicConstraints CERT_BASIC_CONSTRAINTS = new() { CertificateAuthority = false, HasPathLengthConstraint = false, PathLengthConstraint = 0, Critical = true, }; private readonly SubjectAlternativeName subjectAlternativeName; private static readonly ValidityPeriod VALIDITY_PERIOD = new() { ValidFrom = DateTime.UtcNow, ValidTo = DateTime.UtcNow.AddYears(3) }; private static readonly OidCollection OID_COLLECTION = new() { OidLookup.ServerAuthentication, OidLookup.AnyPurpose }; public CertificateService(string serverIp, ILogger logger) { this.logger = logger; subjectAlternativeName = new SubjectAlternativeName { DnsName = new List { "localhost", "cert.nesys.jp", "nesys.taito.co.jp", "fjm170920zero.nesica.net" }, IpAddress = System.Net.IPAddress.Parse(serverIp) }; } public X509Certificate2 InitializeCertificate() { return Environment.OSVersion.Platform == PlatformID.Win32NT ? InitializeCertificateWindows() : InitializeCertificateOthers(); } private X509Certificate2 InitializeCertificateOthers() { if (CertificateExists()) { return new X509Certificate2(CERT_PATH); } logger.LogInformation("Existing certs not found! Removing old certificates and genrate new ones..."); File.Delete(CERT_PATH); File.Delete(ROOT_CERT_PATH); return GenerateCertificate(); } private X509Certificate2 InitializeCertificateWindows() { if (CertificateExists()) { var existingCert = GetCertificate(StoreName.My, StoreLocation.LocalMachine, CERT_CN); if (existingCert != null) { return existingCert; } logger.LogInformation("Existing CN not found! Removing old certificates and genrate new ones..."); } RemovePreviousCert(StoreName.My, StoreLocation.LocalMachine); RemovePreviousCert(StoreName.Root, StoreLocation.LocalMachine); return GenerateCertificate(); } private X509Certificate2 GenerateCertificate() { var serviceProvider = new ServiceCollection() .AddCertificateManager().BuildServiceProvider(); var createCertificates = serviceProvider.GetService(); if (createCertificates == null) { logger.LogError("Cannot initialize CreateCertificates service!"); throw new Exception(); } var rootCa = createCertificates.NewRsaSelfSignedCertificate( ROOT_CA_DISTINGUISHED_NAME, ROOT_CA_BASIC_CONSTRAINTS, VALIDITY_PERIOD, new SubjectAlternativeName(), OID_COLLECTION, ROOT_CA_X509_KEY_USAGE_FLAGS, new RsaConfiguration() ); var cert = createCertificates.NewRsaChainedCertificate( CERT_DISTINGUISHED_NAME, CERT_BASIC_CONSTRAINTS, VALIDITY_PERIOD, subjectAlternativeName, rootCa, OID_COLLECTION, CERT_X509_KEY_USAGE_FLAGS, new RsaConfiguration() ); var exportService = serviceProvider.GetService(); if (exportService == null) { logger.LogError("Cannot initialize ImportExportCertificate service!"); throw new Exception(); } var rootCaPfxBytes = exportService.ExportRootPfx(null, rootCa); var certPfxBytes = exportService.ExportChainedCertificatePfx(null, cert, rootCa); var rootCaWithPrivateKey = new X509Certificate2(rootCaPfxBytes, (string)null!, X509_KEY_STORAGE_FLAGS_MACHINE); var certWithPrivateKey = new X509Certificate2(certPfxBytes, (string)null!, X509_KEY_STORAGE_FLAGS_MACHINE); if (Environment.OSVersion.Platform == PlatformID.Win32NT) { AddCertToStore(rootCaWithPrivateKey, StoreName.My, StoreLocation.LocalMachine); AddCertToStore(rootCaWithPrivateKey, StoreName.Root, StoreLocation.LocalMachine); AddCertToStore(certWithPrivateKey, StoreName.My, StoreLocation.LocalMachine); } Directory.CreateDirectory(CERT_DIR); File.WriteAllBytes(ROOT_CERT_PATH, rootCaWithPrivateKey.Export(X509ContentType.Pfx)); File.WriteAllBytes(CERT_PATH, certWithPrivateKey.Export(X509ContentType.Pfx)); return certWithPrivateKey; } private void AddCertToStore(X509Certificate2 cert, StoreName storeName, StoreLocation storeLocation) { try { var store = new X509Store(storeName, storeLocation); store.Open(OpenFlags.ReadWrite); store.Add(cert); store.Close(); } catch (Exception e) { logger.LogError(e, "An exception occurs when adding certificate"); } } private void RemovePreviousCert(StoreName storeName, StoreLocation storeLocation) { try { var store = new X509Store(storeName, storeLocation); store.Open(OpenFlags.ReadWrite); var result = store.Certificates.Find(X509FindType.FindByIssuerName, ROOT_CA_CN, true); if (result.Any()) { store.RemoveRange(result); logger.LogInformation("Removed previous certificates!"); } store.Close(); } catch (Exception e) { logger.LogError(e, "An exception occurs when removing previous certificates"); } } private bool CertificateExists() { bool certificateExists; if (Environment.OSVersion.Platform != PlatformID.Win32NT) { // Non windows just use generated certificate file certificateExists = Path.Exists(Path.Combine(CERT_DIR, "cert.pfx")) && Path.Exists(Path.Combine(CERT_DIR, "root.pfx")); } else { try { var store = new X509Store(StoreName.My, StoreLocation.LocalMachine); store.Open(OpenFlags.ReadOnly); var result = store.Certificates.Find(X509FindType.FindByIssuerName, ROOT_CA_CN, true); certificateExists = result.Count == 2; store.Close(); } catch (Exception e) { logger.LogError(e, "An exception occurs when checking certificates"); return false; } } if (certificateExists) { logger.LogInformation("Certificate exists!"); } else { logger.LogInformation("Certificate not found! Will generate new certs..."); } return certificateExists; } private X509Certificate2? GetCertificate(StoreName storeName, StoreLocation storeLocation, string commonName) { try { var store = new X509Store(storeName, storeLocation); store.Open(OpenFlags.ReadWrite); var result = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, $"CN={commonName}", true); if (result.Any()) { logger.LogInformation("Certificate CN={CommonName} found!", commonName); var cert = result.First(); var extensions = cert.Extensions; if (extensions.Select(extension => extension.Oid).Any(oid => oid?.Value == OidLookup.ServerAuthentication.Value)) { return cert; } logger.LogInformation("Certificate CN={CommonName} does not include server authentication!", commonName); return null; } store.Close(); return null; } catch (Exception e) { logger.LogError(e, "An exception occurs when getting certificate {CommonName}", commonName); return null; } } }