diff options
Diffstat (limited to 'src/main.lib/Plugins/StorePlugins')
15 files changed, 915 insertions, 0 deletions
diff --git a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs new file mode 100644 index 0000000..e42ea79 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs @@ -0,0 +1,116 @@ +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Extensions; +using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Services; +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + internal class CentralSsl : IStorePlugin + { + private ILogService _log; + private readonly string _path; + private readonly string _password; + + public CentralSsl(ILogService log, ISettingsService settings, CentralSslOptions options) + { + _log = log; + + if (!string.IsNullOrWhiteSpace(options.PfxPassword?.Value)) + { + _password = options.PfxPassword.Value; + } + else + { + _password = settings.DefaultCentralSslPfxPassword; + } + + if (!string.IsNullOrWhiteSpace(options.Path)) + { + _path = options.Path; + } + else + { + _path = settings.DefaultCentralSslStore; + } + if (_path.ValidPath(log)) + { + _log.Debug("Using Centralized SSL path: {_path}", _path); + } + else + { + throw new Exception($"Specified CentralSsl path {_path} is not valid."); + } + } + + public void Save(CertificateInfo input) + { + _log.Information("Copying certificate to the Central SSL store"); + var source = input.CacheFile; + IEnumerable<string> targets = input.HostNames; + foreach (var identifier in targets) + { + var dest = Path.Combine(_path, $"{identifier.Replace("*", "_")}.pfx"); + _log.Information("Saving certificate to Central SSL location {dest}", dest); + try + { + File.WriteAllBytes(dest, input.Certificate.Export(X509ContentType.Pfx, _password)); + } + catch (Exception ex) + { + _log.Error(ex, "Error copying certificate to Central SSL store"); + } + } + input.StoreInfo.Add(GetType(), + new StoreInfo() + { + Name = CentralSslOptions.PluginName, + Path = _path + }); + } + + public void Delete(CertificateInfo input) + { + _log.Information("Removing certificate from the Central SSL store"); + var di = new DirectoryInfo(_path); + foreach (var fi in di.GetFiles("*.pfx")) + { + var cert = LoadCertificate(fi); + if (cert != null && string.Equals(cert.Thumbprint, input.Certificate.Thumbprint, StringComparison.InvariantCultureIgnoreCase)) + { + fi.Delete(); + } + } + } + + /// <summary> + /// Load certificate from disk + /// </summary> + /// <param name="fi"></param> + /// <returns></returns> + private X509Certificate2 LoadCertificate(FileInfo fi) + { + X509Certificate2 cert = null; + try + { + cert = new X509Certificate2(fi.FullName, _password); + } + catch (CryptographicException) + { + try + { + cert = new X509Certificate2(fi.FullName, ""); + } + catch + { + _log.Warning("Unable to scan certificate {name}", fi.FullName); + } + } + return cert; + } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslArguments.cs b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslArguments.cs new file mode 100644 index 0000000..95ebe7f --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslArguments.cs @@ -0,0 +1,9 @@ +namespace PKISharp.WACS.Plugins.StorePlugins +{ + class CentralSslArguments + { + public bool KeepExisting { get; set; } + public string CentralSslStore { get; set; } + public string PfxPassword { get; set; } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslArgumentsProvider.cs b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslArgumentsProvider.cs new file mode 100644 index 0000000..87ef5b0 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslArgumentsProvider.cs @@ -0,0 +1,30 @@ +using Fclp; +using PKISharp.WACS.Configuration; +using PKISharp.WACS.Services; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + class CentralSslArgumentsProvider : BaseArgumentsProvider<CentralSslArguments> + { + public override string Name => "Central Certificate Store plugin"; + public override string Group => "Store"; + public override string Condition => "--store centralssl"; + + public override void Configure(FluentCommandLineParser<CentralSslArguments> parser) + { + parser.Setup(o => o.CentralSslStore) + .As("centralsslstore") + .WithDescription("When using this setting, certificate files are stored to the CCS and IIS bindings are configured to reflect that."); + parser.Setup(o => o.PfxPassword) + .As("pfxpassword") + .WithDescription("Password to set for .pfx files exported to the IIS CSS."); + } + + public override bool Active(CentralSslArguments current) + { + return !string.IsNullOrEmpty(current.CentralSslStore) || + !string.IsNullOrEmpty(current.PfxPassword); + } + + } +} diff --git a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptions.cs b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptions.cs new file mode 100644 index 0000000..064c386 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptions.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using PKISharp.WACS.Plugins.Base; +using PKISharp.WACS.Plugins.Base.Options; +using PKISharp.WACS.Services; +using PKISharp.WACS.Services.Serialization; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + [Plugin("af1f77b6-4e7b-4f96-bba5-c2eeb4d0dd42")] + internal class CentralSslOptions : StorePluginOptions<CentralSsl> + { + /// <summary> + /// Path to the Central Ssl store + /// </summary> + public string Path { get; set; } + + /// <summary> + /// PfxFile password + /// </summary> + [JsonProperty(propertyName: "PfxPasswordProtected")] + public ProtectedString PfxPassword { get; set; } + + internal const string PluginName = "CentralSsl"; + public override string Name { get => PluginName; } + public override string Description { get => "IIS Central Certificate Store (.pfx per domain)"; } + + /// <summary> + /// Show details to the user + /// </summary> + /// <param name="input"></param> + public override void Show(IInputService input) + { + base.Show(input); + input.Show("Path", string.IsNullOrEmpty(Path) ? "[Default from settings.config]" : Path, level:2); + input.Show("Password", string.IsNullOrEmpty(PfxPassword?.Value) ? "[Default from settings.config]" : new string('*', PfxPassword.Value.Length), level: 2); + } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptionsFactory.cs b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptionsFactory.cs new file mode 100644 index 0000000..7315d3f --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptionsFactory.cs @@ -0,0 +1,92 @@ +using PKISharp.WACS.Extensions; +using PKISharp.WACS.Plugins.Base.Factories; +using PKISharp.WACS.Services; +using PKISharp.WACS.Services.Serialization; +using System; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + internal class CentralSslOptionsFactory : StorePluginOptionsFactory<CentralSsl, CentralSslOptions> + { + private ILogService _log; + private IArgumentsService _arguments; + private ISettingsService _settings; + + public CentralSslOptionsFactory(ILogService log, ISettingsService settings, IArgumentsService arguments) + { + _log = log; + _arguments = arguments; + _settings = settings; + } + + public override CentralSslOptions Aquire(IInputService input, RunLevel runLevel) + { + var args = _arguments.GetArguments<CentralSslArguments>(); + + // Get path from command line, default setting or user input + var path = args.CentralSslStore; + if (string.IsNullOrWhiteSpace(path)) + { + path = _settings.DefaultCentralSslStore; + } + while (string.IsNullOrWhiteSpace(path) || !path.ValidPath(_log)) + { + path = input.RequestString("Path to Central Certificate Store"); + } + + // Get password from command line, default setting or user input + var password = args.PfxPassword; + if (string.IsNullOrWhiteSpace(password)) + { + password = _settings.DefaultCentralSslPfxPassword; + } + if (string.IsNullOrEmpty(password)) + { + password = input.ReadPassword("Password to use for the PFX files, or enter for none"); + } + return Create(path, password, args.KeepExisting); + } + + public override CentralSslOptions Default() + { + var args = _arguments.GetArguments<CentralSslArguments>(); + var path = _settings.DefaultCentralSslStore; + if (string.IsNullOrWhiteSpace(path)) + { + path = _arguments.TryGetRequiredArgument(nameof(args.CentralSslStore), args.CentralSslStore); + } + + var password = _settings.DefaultCentralSslPfxPassword; + if (!string.IsNullOrWhiteSpace(args.PfxPassword)) + { + password = args.PfxPassword; + } + + if (path.ValidPath(_log)) + { + return Create(path, password, args.KeepExisting); + } + else + { + throw new Exception("Invalid path specified"); + } + } + + private CentralSslOptions Create(string path, string password, bool keepExisting) + { + var ret = new CentralSslOptions + { + KeepExisting = keepExisting + }; + if (!string.IsNullOrWhiteSpace(password) && !string.Equals(password, _settings.DefaultCentralSslPfxPassword)) + { + ret.PfxPassword = new ProtectedString(password); + } + if (!string.Equals(path, _settings.DefaultCentralSslStore, StringComparison.CurrentCultureIgnoreCase)) + { + ret.Path = path; + } + return ret; + } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStore.cs b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStore.cs new file mode 100644 index 0000000..3cf9503 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStore.cs @@ -0,0 +1,294 @@ +using PKISharp.WACS.Clients.IIS; +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + internal class CertificateStore : IStorePlugin, IDisposable + { + private ILogService _log; + private ISettingsService _settings; + private const string _defaultStoreName = nameof(StoreName.My); + private string _storeName; + private X509Store _store; + private IIISClient _iisClient; + private CertificateStoreOptions _options; + + public CertificateStore( + ILogService log, IIISClient iisClient, + ISettingsService settings, + CertificateStoreOptions options) + { + _log = log; + _iisClient = iisClient; + _options = options; + _settings = settings; + ParseCertificateStore(); + _store = new X509Store(_storeName, StoreLocation.LocalMachine); + } + + private void ParseCertificateStore() + { + try + { + // First priority: specified in the parameters + _storeName = _options.StoreName; + + // Second priority: specified in the .config + if (string.IsNullOrEmpty(_storeName)) + { + _storeName = _settings.DefaultCertificateStore; + } + + // Third priority: defaults + if (string.IsNullOrEmpty(_storeName)) + { + // Default store should be WebHosting on IIS8+, and My (Personal) for IIS7.x + _storeName = _iisClient.Version.Major < 8 ? nameof(StoreName.My) : "WebHosting"; + } + + // Rewrite + if (string.Equals(_storeName, "Personal", StringComparison.InvariantCultureIgnoreCase)) + { + // Users trying to use the "My" store might have set "Personal" in their + // config files, because that's what the store is called in mmc + _storeName = nameof(StoreName.My); + } + + _log.Debug("Certificate store: {_certificateStore}", _storeName); + } + catch (Exception ex) + { + _log.Warning("Error reading CertificateStore from config, defaulting to {_certificateStore} Error: {@ex}", _defaultStoreName, ex); + } + } + + public void Save(CertificateInfo input) + { + var existing = FindByThumbprint(input.Certificate.Thumbprint); + if (existing != null) + { + _log.Warning("Certificate with thumbprint {thumbprint} is already in the store", input.Certificate.Thumbprint); + } + else + { + var certificate = input.Certificate; + if (!_settings.PrivateKeyExportable) + { + certificate = new X509Certificate2( + input.CacheFile.FullName, + input.CacheFilePassword, + X509KeyStorageFlags.MachineKeySet | + X509KeyStorageFlags.PersistKeySet); + } + _log.Information("Installing certificate in the certificate store"); + InstallCertificate(certificate); + } + input.StoreInfo.Add( + GetType(), + new StoreInfo() + { + Name = CertificateStoreOptions.PluginName, + Path = _store.Name + }); + } + + public void Delete(CertificateInfo input) + { + _log.Information("Uninstalling certificate from the certificate store"); + UninstallCertificate(input.Certificate.Thumbprint); + } + + public CertificateInfo FindByThumbprint(string thumbprint) + { + return ToInfo(GetCertificate(CertificateService.ThumbprintFilter(thumbprint))); + } + + private CertificateInfo ToInfo(X509Certificate2 cert) + { + if (cert != null) + { + var ret = new CertificateInfo() + { + Certificate = cert + }; + ret.StoreInfo.Add( + GetType(), + new StoreInfo() { + Path = _store.Name + }); + return ret; + } + else + { + return null; + } + } + + private void InstallCertificate(X509Certificate2 certificate) + { + X509Store rootStore = null; + try + { + rootStore = new X509Store(StoreName.AuthRoot, StoreLocation.LocalMachine); + rootStore.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite); + } + catch + { + _log.Warning("Error encountered while opening root store"); + rootStore = null; + } + + X509Store imStore = null; + try + { + imStore = new X509Store(StoreName.CertificateAuthority, StoreLocation.LocalMachine); + imStore.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite); + } + catch + { + _log.Warning("Error encountered while opening intermediate certificate store"); + imStore = null; + } + + try + { + _store.Open(OpenFlags.ReadWrite); + _log.Debug("Opened certificate store {Name}", _store.Name); + } + catch (Exception ex) + { + _log.Error(ex, "Error encountered while opening certificate store {name}", _store.Name); + throw; + } + + try + { + _log.Information(LogType.All, "Adding certificate {FriendlyName} to store {name}", certificate.FriendlyName, _store.Name); + var chain = new X509Chain(); + chain.Build(certificate); + foreach (var chainElement in chain.ChainElements) + { + var cert = chainElement.Certificate; + if (cert.Subject == certificate.Subject) + { + _log.Verbose("{sub} - {iss} ({thumb})", cert.Subject, cert.Issuer, cert.Thumbprint); + _store.Add(cert); + } + else if (cert.Subject != cert.Issuer && imStore != null) + { + _log.Verbose("{sub} - {iss} ({thumb}) to CA store", cert.Subject, cert.Issuer, cert.Thumbprint); + imStore.Add(cert); + } + else if (cert.Subject == cert.Issuer && rootStore != null) + { + _log.Verbose("{sub} - {iss} ({thumb}) to AuthRoot store", cert.Subject, cert.Issuer, cert.Thumbprint); + rootStore.Add(cert); + } + } + } + catch (Exception ex) + { + _log.Error(ex, "Error saving certificate"); + } + _log.Debug("Closing certificate stores"); + _store.Close(); + imStore.Close(); + rootStore.Close(); + } + + private void UninstallCertificate(string thumbprint) + { + try + { + _store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite); + } + catch (Exception ex) + { + _log.Error(ex, "Error encountered while opening certificate store"); + throw; + } + + _log.Debug("Opened certificate store {Name}", _store.Name); + try + { + var col = _store.Certificates; + foreach (var cert in col) + { + if (string.Equals(cert.Thumbprint, thumbprint, StringComparison.InvariantCultureIgnoreCase)) + { + _log.Information(LogType.All, "Removing certificate {cert} from store {name}", cert.FriendlyName, _store.Name); + _store.Remove(cert); + } + } + _log.Debug("Closing certificate store"); + } + catch (Exception ex) + { + _log.Error(ex, "Error removing certificate"); + throw; + } + _store.Close(); + } + + private X509Certificate2 GetCertificate(Func<X509Certificate2, bool> filter) + { + var possibles = new List<X509Certificate2>(); + try + { + _store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly); + } + catch (Exception ex) + { + _log.Error(ex, "Error encountered while opening certificate store"); + return null; + } + try + { + var col = _store.Certificates; + foreach (var cert in col) + { + if (filter(cert)) + { + possibles.Add(cert); + } + } + } + catch (Exception ex) + { + _log.Error(ex, "Error finding certificate in certificate store"); + return null; + } + _store.Close(); + return possibles.OrderByDescending(x => x.NotBefore).FirstOrDefault(); + } + + #region IDisposable + + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _store.Dispose(); + } + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + + #endregion + } +}
\ No newline at end of file diff --git a/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreArguments.cs b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreArguments.cs new file mode 100644 index 0000000..2cbaae7 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreArguments.cs @@ -0,0 +1,8 @@ +namespace PKISharp.WACS.Plugins.StorePlugins +{ + class CertificateStoreArguments + { + public bool KeepExisting { get; set; } + public string CertificateStore { get; set; } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreArgumentsProvider.cs b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreArgumentsProvider.cs new file mode 100644 index 0000000..2834b47 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreArgumentsProvider.cs @@ -0,0 +1,28 @@ +using Fclp; +using PKISharp.WACS.Configuration; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + class CertificateStoreArgumentsProvider : BaseArgumentsProvider<CertificateStoreArguments> + { + public override string Name => "Certificate Store plugin"; + public override string Group => "Store"; + public override string Condition => "--store certificatestore"; + public override bool Default => true; + + public override void Configure(FluentCommandLineParser<CertificateStoreArguments> parser) + { + parser.Setup(o => o.CertificateStore) + .As("certificatestore") + .WithDescription("This setting can be used to save the certificate in a specific store. By default it will go to 'WebHosting' store on modern versions of Windows."); + parser.Setup(o => o.KeepExisting) + .As("keepexisting") + .WithDescription("While renewing, do not remove the previous certificate."); + } + + public override bool Active(CertificateStoreArguments current) + { + return !string.IsNullOrEmpty(current.CertificateStore) || current.KeepExisting; + } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptions.cs b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptions.cs new file mode 100644 index 0000000..52ff113 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptions.cs @@ -0,0 +1,28 @@ +using PKISharp.WACS.Plugins.Base; +using PKISharp.WACS.Plugins.Base.Options; +using PKISharp.WACS.Services; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + [Plugin("e30adc8e-d756-4e16-a6f2-450f784b1a97")] + internal class CertificateStoreOptions : StorePluginOptions<CertificateStore> + { + internal const string PluginName = "CertificateStore"; + public override string Name { get => PluginName; } + public override string Description { get => "Windows Certificate Store"; } + + /// <summary> + /// Name of the certificate store to use + /// </summary> + public string StoreName { get; set; } + + public override void Show(IInputService input) + { + base.Show(input); + if (!string.IsNullOrEmpty(StoreName)) + { + input.Show("Store", StoreName, level: 2); + } + } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptionsFactory.cs b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptionsFactory.cs new file mode 100644 index 0000000..d2bfed9 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptionsFactory.cs @@ -0,0 +1,30 @@ +using PKISharp.WACS.Plugins.Base.Factories; +using PKISharp.WACS.Services; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + internal class CertificateStoreOptionsFactory : StorePluginOptionsFactory<CertificateStore, CertificateStoreOptions> + { + private IArgumentsService _arguments; + + public CertificateStoreOptionsFactory(IArgumentsService arguments) + { + _arguments = arguments; + } + + public override CertificateStoreOptions Aquire(IInputService inputService, RunLevel runLevel) + { + return Default(); + } + + public override CertificateStoreOptions Default() + { + var args = _arguments.GetArguments<CertificateStoreArguments>(); + return new CertificateStoreOptions + { + StoreName = args.CertificateStore, + KeepExisting = args.KeepExisting + }; + } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/PemFiles/PemFiles.cs b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFiles.cs new file mode 100644 index 0000000..ea7e79a --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFiles.cs @@ -0,0 +1,118 @@ +using Org.BouncyCastle.Pkcs; +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Extensions; +using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Services; +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + internal class PemFiles : IStorePlugin + { + private readonly ILogService _log; + private readonly PemService _pemService; + + private readonly string _path; + + public PemFiles( + ILogService log, ISettingsService settings, + PemService pemService, PemFilesOptions options) + { + _log = log; + _pemService = pemService; + if (!string.IsNullOrWhiteSpace(options.Path)) + { + _path = options.Path; + } + else + { + _path = settings.DefaultPemFilesPath; + } + if (_path.ValidPath(log)) + { + _log.Debug("Using .pem certificate path: {_path}", _path); + } + else + { + throw new Exception($"Specified PemFiles path {_path} is not valid."); + } + } + + public void Save(CertificateInfo input) + { + _log.Information("Exporting .pem files to {folder}", _path); + try + { + // Determine name + var name = input.SubjectName.Replace("*", "_"); + + // Base certificate + var certificateExport = input.Certificate.Export(X509ContentType.Cert); + var exportString = _pemService.GetPem("CERTIFICATE", certificateExport); + File.WriteAllText(Path.Combine(_path, $"{name}-crt.pem"), exportString); + + // Rest of the chain + var chain = new X509Chain(); + chain.Build(input.Certificate); + for (var i = 1; i < chain.ChainElements.Count; i++) + { + var chainCertificate = chain.ChainElements[i].Certificate; + // Do not include self-signed certificates, root certificates + // are supposed to be known already by the client. + if (chainCertificate.Subject != chainCertificate.Issuer) + { + var chainCertificateExport = chainCertificate.Export(X509ContentType.Cert); + exportString += _pemService.GetPem("CERTIFICATE", chainCertificateExport); + } + } + + // Save complete chain + File.WriteAllText(Path.Combine(_path, $"{name}-chain.pem"), exportString); + + // Private key + var pkPem = ""; + var store = new Pkcs12Store(input.CacheFile.OpenRead(), input.CacheFilePassword.ToCharArray()); + var alias = store.Aliases.OfType<string>().FirstOrDefault(p => store.IsKeyEntry(p)); + var entry = store.GetKey(alias); + var key = entry.Key; + if (key.IsPrivate) + { + pkPem = _pemService.GetPem(entry.Key); + } + if (!string.IsNullOrEmpty(pkPem)) + { + File.WriteAllText(Path.Combine(_path, $"{name}-key.pem"), pkPem); + } + else + { + _log.Warning("No private key found"); + } + + input.StoreInfo.Add(GetType(), + new StoreInfo() + { + Name = PemFilesOptions.PluginName, + Path = _path + }); + } + catch (Exception ex) + { + _log.Error(ex, "Error exporting .pem files to folder"); + } + + } + + public void Delete(CertificateInfo input) + { + // Not supported + } + + public CertificateInfo FindByThumbprint(string thumbprint) + { + return null; + } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesArguments.cs b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesArguments.cs new file mode 100644 index 0000000..23370d6 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesArguments.cs @@ -0,0 +1,7 @@ +namespace PKISharp.WACS.Plugins.StorePlugins +{ + class PemFilesArguments + { + public string PemFilesPath { get; set; } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesArgumentsProvider.cs b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesArgumentsProvider.cs new file mode 100644 index 0000000..52368eb --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesArgumentsProvider.cs @@ -0,0 +1,24 @@ +using Fclp; +using PKISharp.WACS.Configuration; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + class PemFilesArgumentsProvider : BaseArgumentsProvider<PemFilesArguments> + { + public override string Name => "PEM files plugin"; + public override string Group => "Store"; + public override string Condition => "--store pemfiles"; + + public override void Configure(FluentCommandLineParser<PemFilesArguments> parser) + { + parser.Setup(o => o.PemFilesPath) + .As("pemfilespath") + .WithDescription(".pem files are exported to this folder"); + } + + public override bool Active(PemFilesArguments current) + { + return !string.IsNullOrEmpty(current.PemFilesPath); + } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptions.cs b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptions.cs new file mode 100644 index 0000000..47e8ca3 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptions.cs @@ -0,0 +1,28 @@ +using PKISharp.WACS.Plugins.Base; +using PKISharp.WACS.Plugins.Base.Options; +using PKISharp.WACS.Services; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + [Plugin("e57c70e4-cd60-4ba6-80f6-a41703e21031")] + internal class PemFilesOptions : StorePluginOptions<PemFiles> + { + /// <summary> + /// Path to the .pem directory + /// </summary> + public string Path { get; set; } + internal const string PluginName = "PemFiles"; + public override string Name { get => PluginName; } + public override string Description { get => "PEM encoded files (Apache, nginx, etc.)"; } + + /// <summary> + /// Show details to the user + /// </summary> + /// <param name="input"></param> + public override void Show(IInputService input) + { + base.Show(input); + input.Show("Path", Path, level:1); + } + } +} diff --git a/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptionsFactory.cs b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptionsFactory.cs new file mode 100644 index 0000000..2dab221 --- /dev/null +++ b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptionsFactory.cs @@ -0,0 +1,65 @@ +using PKISharp.WACS.Extensions; +using PKISharp.WACS.Plugins.Base.Factories; +using PKISharp.WACS.Services; +using System; + +namespace PKISharp.WACS.Plugins.StorePlugins +{ + internal class PemFilesOptionsFactory : StorePluginOptionsFactory<PemFiles, PemFilesOptions> + { + private ILogService _log; + private IArgumentsService _arguments; + private ISettingsService _settings; + + public PemFilesOptionsFactory(ILogService log, ISettingsService settings, IArgumentsService arguments) + { + _log = log; + _arguments = arguments; + _settings = settings; + } + + public override PemFilesOptions Aquire(IInputService input, RunLevel runLevel) + { + var args = _arguments.GetArguments<PemFilesArguments>(); + var path = args.PemFilesPath; + if (string.IsNullOrWhiteSpace(path)) + { + path = _settings.DefaultPemFilesPath; + } + while (string.IsNullOrWhiteSpace(path) || !path.ValidPath(_log)) + { + path = input.RequestString("Path to folder where .pem files are stored"); + } + return Create(path); + } + + public override PemFilesOptions Default() + { + var args = _arguments.GetArguments<PemFilesArguments>(); + var path = _settings.DefaultPemFilesPath; + if (string.IsNullOrWhiteSpace(path)) + { + path = _arguments.TryGetRequiredArgument(nameof(args.PemFilesPath), args.PemFilesPath); + } + if (path.ValidPath(_log)) + { + return Create(path); + } + else + { + throw new Exception("Invalid path specified"); + } + } + + private PemFilesOptions Create(string path) + { + var ret = new PemFilesOptions(); + if (!string.Equals(path, _settings.DefaultPemFilesPath, StringComparison.CurrentCultureIgnoreCase)) + { + ret.Path = path; + } + return ret; + } + } + +}
\ No newline at end of file |