summaryrefslogtreecommitdiffstats
path: root/src/main.lib/Plugins/StorePlugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.lib/Plugins/StorePlugins')
-rw-r--r--src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs116
-rw-r--r--src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslArguments.cs9
-rw-r--r--src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslArgumentsProvider.cs30
-rw-r--r--src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptions.cs38
-rw-r--r--src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptionsFactory.cs92
-rw-r--r--src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStore.cs294
-rw-r--r--src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreArguments.cs8
-rw-r--r--src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreArgumentsProvider.cs28
-rw-r--r--src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptions.cs28
-rw-r--r--src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptionsFactory.cs30
-rw-r--r--src/main.lib/Plugins/StorePlugins/PemFiles/PemFiles.cs118
-rw-r--r--src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesArguments.cs7
-rw-r--r--src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesArgumentsProvider.cs24
-rw-r--r--src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptions.cs28
-rw-r--r--src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptionsFactory.cs65
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