diff options
Diffstat (limited to 'src')
39 files changed, 631 insertions, 382 deletions
diff --git a/src/main.lib/Clients/Acme/AcmeClient.cs b/src/main.lib/Clients/Acme/AcmeClient.cs index 5e0385a..2eae016 100644 --- a/src/main.lib/Clients/Acme/AcmeClient.cs +++ b/src/main.lib/Clients/Acme/AcmeClient.cs @@ -38,6 +38,8 @@ namespace PKISharp.WACS.Clients.Acme public const string AuthorizationPending = "pending"; public const string AuthorizationProcessing = "processing"; + public const string ChallengeValid = "valid"; + private readonly ILogService _log; private readonly IInputService _input; private readonly ISettingsService _settings; diff --git a/src/main.lib/Clients/Acme/OrderManager.cs b/src/main.lib/Clients/Acme/OrderManager.cs index a0f9c2b..774927c 100644 --- a/src/main.lib/Clients/Acme/OrderManager.cs +++ b/src/main.lib/Clients/Acme/OrderManager.cs @@ -39,9 +39,9 @@ namespace PKISharp.WACS.Clients.Acme /// <param name="renewal"></param> /// <param name="target"></param> /// <returns></returns> - public async Task<OrderDetails?> GetOrCreate(Renewal renewal, Target target, RunLevel runLevel) + public async Task<OrderDetails?> GetOrCreate(Order order, RunLevel runLevel) { - var cacheKey = _certificateService.CacheKey(renewal, target); + var cacheKey = _certificateService.CacheKey(order); var existingOrder = FindRecentOrder(cacheKey); if (existingOrder != null) { @@ -74,7 +74,7 @@ namespace PKISharp.WACS.Clients.Acme _log.Warning("Unable to refresh cached order: {ex}", ex.Message); } } - var identifiers = target.GetHosts(false); + var identifiers = order.Target.GetHosts(false); return await CreateOrder(identifiers, cacheKey); } diff --git a/src/main.lib/Configuration/MainArguments.cs b/src/main.lib/Configuration/MainArguments.cs index ba6ae22..e446d8a 100644 --- a/src/main.lib/Configuration/MainArguments.cs +++ b/src/main.lib/Configuration/MainArguments.cs @@ -26,6 +26,7 @@ namespace PKISharp.WACS.Configuration public string? Target { get; set; } public string? Validation { get; set; } public string? ValidationMode { get; set; } + public string? Order { get; set; } public string? Csr { get; set; } public string? Store { get; set; } public string? Installation { get; set; } diff --git a/src/main.lib/Configuration/MainArgumentsProvider.cs b/src/main.lib/Configuration/MainArgumentsProvider.cs index 2f02081..6fd17fa 100644 --- a/src/main.lib/Configuration/MainArgumentsProvider.cs +++ b/src/main.lib/Configuration/MainArgumentsProvider.cs @@ -15,6 +15,7 @@ namespace PKISharp.WACS.Configuration !string.IsNullOrEmpty(current.FriendlyName) || !string.IsNullOrEmpty(current.Installation) || !string.IsNullOrEmpty(current.Store) || + !string.IsNullOrEmpty(current.Order) || !string.IsNullOrEmpty(current.Csr) || !string.IsNullOrEmpty(current.Target) || !string.IsNullOrEmpty(current.Validation); @@ -98,10 +99,14 @@ namespace PKISharp.WACS.Configuration .As("validationmode") .SetDefault(Constants.Http01ChallengeType) .WithDescription("Specify which validation mode to use. HTTP-01 is the default."); + + parser.Setup(o => o.Order) + .As("order") + .WithDescription("Specify which order plugin to use. Single is the default."); parser.Setup(o => o.Csr) .As("csr") - .WithDescription("Specify which csr plugin to use. RSA is the default."); + .WithDescription("Specify which CSR plugin to use. RSA is the default."); parser.Setup(o => o.Store) .As("store") diff --git a/src/main.lib/DomainObjects/Order.cs b/src/main.lib/DomainObjects/Order.cs new file mode 100644 index 0000000..cb7cdc5 --- /dev/null +++ b/src/main.lib/DomainObjects/Order.cs @@ -0,0 +1,25 @@ +using acme = ACMESharp.Protocol; + +namespace PKISharp.WACS.DomainObjects +{ + public class Order + { + public string? CacheKeyPart { get; set; } + public string? FriendlyNamePart { get; set; } + public Target Target { get; set; } + public Renewal Renewal { get; set; } + public acme.OrderDetails? Details { get; set; } = null; + + public Order( + Renewal renewal, + Target target, + string? cacheKeyPart = null, + string? friendlyNamePart = null) + { + Target = target; + Renewal = renewal; + CacheKeyPart = cacheKeyPart; + FriendlyNamePart = friendlyNamePart; + } + } +} diff --git a/src/main.lib/DomainObjects/RenewResult.cs b/src/main.lib/DomainObjects/RenewResult.cs index 8ab75eb..4f1bc72 100644 --- a/src/main.lib/DomainObjects/RenewResult.cs +++ b/src/main.lib/DomainObjects/RenewResult.cs @@ -1,32 +1,69 @@ using PKISharp.WACS.Extensions; using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; namespace PKISharp.WACS.DomainObjects { public class RenewResult { public DateTime Date { get; set; } + + [JsonIgnore] + public bool Abort { get; set; } + public bool Success { get; set; } - public string? ErrorMessage { get; set; } - public string? Thumbprint { get; set; } - private RenewResult() => Date = DateTime.UtcNow; + public string? ErrorMessage { set => AddErrorMessage(value); } + + public string? Thumbprint { set => AddThumbprint(value); } + + public RenewResult AddErrorMessage(string? value, bool fatal = true) + { + if (value != null) + { + if (!ErrorMessages.Contains(value)) + { + ErrorMessages.Add(value); + } + } + if (fatal) + { + Success = false; + } + return this; + } + + public void AddThumbprint(string? value) + { + if (value != null) + { + if (!Thumbprints.Contains(value)) + { + Thumbprints.Add(value); + } + } + } + + public List<string> Thumbprints { get; set; } = new List<string>(); + public List<string> ErrorMessages { get; set; } = new List<string>(); - public RenewResult(CertificateInfo certificate) : this() + public RenewResult() { Success = true; - Thumbprint = certificate.Certificate.Thumbprint; + Date = DateTime.UtcNow; } public RenewResult(string error) : this() { Success = false; - ErrorMessage = error; + AddErrorMessage(error); } public override string ToString() => $"{Date} " + $"- {(Success ? "Success" : "Error")}" + - $"{(string.IsNullOrEmpty(Thumbprint) ? "" : $" - Thumbprint {Thumbprint}")}" + - $"{(string.IsNullOrEmpty(ErrorMessage) ? "" : $" - {ErrorMessage.ReplaceNewLines()}")}"; + $"{(Thumbprints.Count == 0 ? "" : $" - Thumbprint {string.Join(", ", Thumbprints)}")}" + + $"{(ErrorMessages.Count == 0 ? "" : $" - {string.Join(", ", ErrorMessages.Select(x => x.ReplaceNewLines()))}")}"; } } diff --git a/src/main.lib/DomainObjects/Renewal.cs b/src/main.lib/DomainObjects/Renewal.cs index 3cd8062..cb8b32f 100644 --- a/src/main.lib/DomainObjects/Renewal.cs +++ b/src/main.lib/DomainObjects/Renewal.cs @@ -120,6 +120,11 @@ namespace PKISharp.WACS.DomainObjects public CsrPluginOptions? CsrPluginOptions { get; set; } /// <summary> + /// Store information about OrderPlugin + /// </summary> + public OrderPluginOptions? OrderPluginOptions { get; set; } + + /// <summary> /// Store information about StorePlugin /// </summary> public List<StorePluginOptions> StorePluginOptions { get; set; } = new List<StorePluginOptions>(); @@ -162,7 +167,8 @@ namespace PKISharp.WACS.DomainObjects if (errors.Count() > 0) { - ret += $", {errors.Count()} error{(errors.Count() != 1 ? "s" : "")} like \"{errors.First().ErrorMessage}\""; + var messages = errors.SelectMany(x => x.ErrorMessages).Where(x => !string.IsNullOrEmpty(x)); + ret += $", {errors.Count()} error{(errors.Count() != 1 ? "s" : "")} like \"{messages.FirstOrDefault() ?? "[null]"}\""; } return ret; } diff --git a/src/main.lib/Extensions/RenewalExtensions.cs b/src/main.lib/Extensions/RenewalExtensions.cs deleted file mode 100644 index ed28419..0000000 --- a/src/main.lib/Extensions/RenewalExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using PKISharp.WACS.DomainObjects; -using System.Linq; - -namespace PKISharp.WACS.Extensions -{ - public static class RenewalExtensions - { - /// <summary> - /// Get the most recent thumbprint - /// </summary> - /// <returns></returns> - public static string? Thumbprint(this Renewal renewal) - { - return renewal. - History?. - OrderByDescending(x => x.Date). - Where(x => x.Success). - Select(x => x.Thumbprint). - FirstOrDefault(); - } - } -} diff --git a/src/main.lib/Plugins/Base/Options/OrderPluginOptions.cs b/src/main.lib/Plugins/Base/Options/OrderPluginOptions.cs new file mode 100644 index 0000000..b50dfc6 --- /dev/null +++ b/src/main.lib/Plugins/Base/Options/OrderPluginOptions.cs @@ -0,0 +1,28 @@ +using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Services; +using PKISharp.WACS.Services.Serialization; +using System; + +namespace PKISharp.WACS.Plugins.Base.Options +{ + public class OrderPluginOptions : PluginOptions + { + public override string Name => throw new NotImplementedException(); + public override string Description => throw new NotImplementedException(); + public override Type Instance => throw new NotImplementedException(); + } + + public abstract class OrderPluginOptions<T> : OrderPluginOptions where T : IOrderPlugin + { + public abstract override string Name { get; } + public abstract override string Description { get; } + + public override void Show(IInputService input) + { + input.Show("Order"); + input.Show("Plugin", $"{Name} - ({Description})", level: 1); + } + + public override Type Instance => typeof(T); + } +} diff --git a/src/main.lib/Plugins/Base/OptionsFactories/CsrPluginOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/CsrPluginOptionsFactory.cs index 1aa2e0b..224da47 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/CsrPluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/CsrPluginOptionsFactory.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.Base.Factories { /// <summary> - /// StorePluginFactory base implementation + /// CsrPluginFactory base implementation /// </summary> /// <typeparam name="TPlugin"></typeparam> public abstract class CsrPluginOptionsFactory<TPlugin, TOptions> : @@ -17,7 +17,7 @@ namespace PKISharp.WACS.Plugins.Base.Factories { public abstract Task<TOptions> Aquire(IInputService inputService, RunLevel runLevel); public abstract Task<TOptions> Default(); - async Task<CsrPluginOptions?> ICsrPluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); - async Task<CsrPluginOptions?> ICsrPluginOptionsFactory.Default() => await Default(); + async Task<CsrPluginOptions?> IPluginOptionsFactory<CsrPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); + async Task<CsrPluginOptions?> IPluginOptionsFactory<CsrPluginOptions>.Default() => await Default(); } }
\ No newline at end of file diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullCsrOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullCsrOptionsFactory.cs index 3b7caf1..7d36375 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullCsrOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullCsrOptionsFactory.cs @@ -18,7 +18,7 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null int IPluginOptionsFactory.Order => int.MaxValue; (bool, string?) IPluginOptionsFactory.Disabled => (false, null); bool IPluginOptionsFactory.Match(string name) => false; - Task<CsrPluginOptions?> ICsrPluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<CsrPluginOptions?>(null); - Task<CsrPluginOptions?> ICsrPluginOptionsFactory.Default() => Task.FromResult<CsrPluginOptions?>(null); + Task<CsrPluginOptions?> IPluginOptionsFactory<CsrPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<CsrPluginOptions?>(null); + Task<CsrPluginOptions?> IPluginOptionsFactory<CsrPluginOptions>.Default() => Task.FromResult<CsrPluginOptions?>(null); } } diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullOrderPluginOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullOrderPluginOptionsFactory.cs new file mode 100644 index 0000000..0da5c3b --- /dev/null +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullOrderPluginOptionsFactory.cs @@ -0,0 +1,24 @@ +using PKISharp.WACS.Plugins.Base.Options; +using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Services; +using System; +using System.Threading.Tasks; + +namespace PKISharp.WACS.Plugins.Base.Factories.Null +{ + /// <summary> + /// Null implementation + /// </summary> + internal class NullOrderOptionsFactory : IOrderPluginOptionsFactory, INull + { + Type IPluginOptionsFactory.InstanceType => typeof(object); + Type IPluginOptionsFactory.OptionsType => typeof(object); + string IPluginOptionsFactory.Name => "None"; + string? IPluginOptionsFactory.Description => null; + int IPluginOptionsFactory.Order => int.MaxValue; + (bool, string?) IPluginOptionsFactory.Disabled => (false, null); + bool IPluginOptionsFactory.Match(string name) => false; + Task<OrderPluginOptions?> IPluginOptionsFactory<OrderPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<OrderPluginOptions?>(null); + Task<OrderPluginOptions?> IPluginOptionsFactory<OrderPluginOptions>.Default() => Task.FromResult<OrderPluginOptions?>(null); + } +} diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs index 75d3439..e98cd78 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs @@ -15,8 +15,8 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null Type IPluginOptionsFactory.InstanceType => typeof(NullStore); Type IPluginOptionsFactory.OptionsType => typeof(NullStoreOptions); Task<StorePluginOptions?> Generate() => Task.FromResult<StorePluginOptions?>(new NullStoreOptions()); - Task<StorePluginOptions?> IStorePluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => Generate(); - Task<StorePluginOptions?> IStorePluginOptionsFactory.Default() => Generate(); + Task<StorePluginOptions?> IPluginOptionsFactory<StorePluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => Generate(); + Task<StorePluginOptions?> IPluginOptionsFactory<StorePluginOptions>.Default() => Generate(); (bool, string?) IPluginOptionsFactory.Disabled => (false, null); string IPluginOptionsFactory.Name => NullStoreOptions.PluginName; string IPluginOptionsFactory.Description => new NullStoreOptions().Description; diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullTargetOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullTargetOptionsFactory.cs index 8598f1d..0ea7499 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullTargetOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullTargetOptionsFactory.cs @@ -16,8 +16,8 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null bool ITargetPluginOptionsFactory.Hidden => true; (bool, string?) IPluginOptionsFactory.Disabled => (false, null); bool IPluginOptionsFactory.Match(string name) => false; - Task<TargetPluginOptions?> ITargetPluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<TargetPluginOptions?>(default); - Task<TargetPluginOptions?> ITargetPluginOptionsFactory.Default() => Task.FromResult<TargetPluginOptions?>(default); + Task<TargetPluginOptions?> IPluginOptionsFactory<TargetPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<TargetPluginOptions?>(default); + Task<TargetPluginOptions?> IPluginOptionsFactory<TargetPluginOptions>.Default() => Task.FromResult<TargetPluginOptions?>(default); string IPluginOptionsFactory.Name => "None"; string? IPluginOptionsFactory.Description => null; int IPluginOptionsFactory.Order => int.MaxValue; diff --git a/src/main.lib/Plugins/Base/OptionsFactories/OrderPluginOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/OrderPluginOptionsFactory.cs new file mode 100644 index 0000000..cd1cc4a --- /dev/null +++ b/src/main.lib/Plugins/Base/OptionsFactories/OrderPluginOptionsFactory.cs @@ -0,0 +1,23 @@ +using PKISharp.WACS.Plugins.Base.Options; +using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Services; +using System.Threading.Tasks; + +namespace PKISharp.WACS.Plugins.Base.Factories +{ + /// <summary> + /// OrderPluginFactory base implementation + /// </summary> + /// <typeparam name="TPlugin"></typeparam> + public abstract class OrderPluginOptionsFactory<TPlugin, TOptions> : + PluginOptionsFactory<TPlugin, TOptions>, + IOrderPluginOptionsFactory + where TPlugin : IOrderPlugin + where TOptions : OrderPluginOptions, new() + { + public abstract Task<TOptions> Aquire(IInputService inputService, RunLevel runLevel); + public abstract Task<TOptions> Default(); + async Task<OrderPluginOptions?> IPluginOptionsFactory<OrderPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); + async Task<OrderPluginOptions?> IPluginOptionsFactory<OrderPluginOptions>.Default() => await Default(); + } +}
\ No newline at end of file diff --git a/src/main.lib/Plugins/Base/OptionsFactories/StorePluginOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/StorePluginOptionsFactory.cs index 6fcfa88..f1664b9 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/StorePluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/StorePluginOptionsFactory.cs @@ -17,8 +17,8 @@ namespace PKISharp.WACS.Plugins.Base.Factories { public abstract Task<TOptions?> Aquire(IInputService inputService, RunLevel runLevel); public abstract Task<TOptions?> Default(); - async Task<StorePluginOptions?> IStorePluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); - async Task<StorePluginOptions?> IStorePluginOptionsFactory.Default() => await Default(); + async Task<StorePluginOptions?> IPluginOptionsFactory<StorePluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); + async Task<StorePluginOptions?> IPluginOptionsFactory<StorePluginOptions>.Default() => await Default(); } diff --git a/src/main.lib/Plugins/Base/OptionsFactories/TargetPluginOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/TargetPluginOptionsFactory.cs index 1358cde..d943db1 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/TargetPluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/TargetPluginOptionsFactory.cs @@ -24,7 +24,7 @@ namespace PKISharp.WACS.Plugins.Base.Factories /// </summary> public virtual bool Hidden { get; protected set; } = false; - async Task<TargetPluginOptions?> ITargetPluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); - async Task<TargetPluginOptions?> ITargetPluginOptionsFactory.Default() => await Default(); + async Task<TargetPluginOptions?> IPluginOptionsFactory<TargetPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); + async Task<TargetPluginOptions?> IPluginOptionsFactory<TargetPluginOptions>.Default() => await Default(); } } diff --git a/src/main.lib/Plugins/Interfaces/ICore.cs b/src/main.lib/Plugins/Interfaces/ICore.cs index 3b90ff0..a3a700b 100644 --- a/src/main.lib/Plugins/Interfaces/ICore.cs +++ b/src/main.lib/Plugins/Interfaces/ICore.cs @@ -1,4 +1,7 @@ -using System; +using PKISharp.WACS.Services; +using PKISharp.WACS.Services.Serialization; +using System; +using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.Interfaces { @@ -43,6 +46,21 @@ namespace PKISharp.WACS.Plugins.Interfaces (bool, string?) Disabled { get; } } + public interface IPluginOptionsFactory<T>: IPluginOptionsFactory where T: PluginOptions + { + /// <summary> + /// Check or get configuration information needed (interactive) + /// </summary> + /// <param name="target"></param> + Task<T?> Aquire(IInputService inputService, RunLevel runLevel); + + /// <summary> + /// Check information needed (unattended) + /// </summary> + /// <param name="target"></param> + Task<T?> Default(); + } + public interface INull { } public interface IIgnore { } diff --git a/src/main.lib/Plugins/Interfaces/ICsrPluginOptionsFactory.cs b/src/main.lib/Plugins/Interfaces/ICsrPluginOptionsFactory.cs index af45be2..39005c1 100644 --- a/src/main.lib/Plugins/Interfaces/ICsrPluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Interfaces/ICsrPluginOptionsFactory.cs @@ -1,21 +1,6 @@ using PKISharp.WACS.Plugins.Base.Options; -using PKISharp.WACS.Services; -using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.Interfaces { - public interface ICsrPluginOptionsFactory : IPluginOptionsFactory - { - /// <summary> - /// Check or get information needed for store (interactive) - /// </summary> - /// <param name="target"></param> - Task<CsrPluginOptions?> Aquire(IInputService inputService, RunLevel runLevel); - - /// <summary> - /// Check information needed for store (unattended) - /// </summary> - /// <param name="target"></param> - Task<CsrPluginOptions?> Default(); - } + public interface ICsrPluginOptionsFactory : IPluginOptionsFactory<CsrPluginOptions> {} } diff --git a/src/main.lib/Plugins/Interfaces/IOrderPlugin.cs b/src/main.lib/Plugins/Interfaces/IOrderPlugin.cs new file mode 100644 index 0000000..4b59586 --- /dev/null +++ b/src/main.lib/Plugins/Interfaces/IOrderPlugin.cs @@ -0,0 +1,10 @@ +using PKISharp.WACS.DomainObjects; +using System.Collections.Generic; + +namespace PKISharp.WACS.Plugins.Interfaces +{ + public interface IOrderPlugin + { + IEnumerable<Order> Split(Renewal renewal, Target target); + } +} diff --git a/src/main.lib/Plugins/Interfaces/IOrderPluginOptionsFactory.cs b/src/main.lib/Plugins/Interfaces/IOrderPluginOptionsFactory.cs new file mode 100644 index 0000000..9f49084 --- /dev/null +++ b/src/main.lib/Plugins/Interfaces/IOrderPluginOptionsFactory.cs @@ -0,0 +1,6 @@ +using PKISharp.WACS.Plugins.Base.Options; + +namespace PKISharp.WACS.Plugins.Interfaces +{ + public interface IOrderPluginOptionsFactory : IPluginOptionsFactory<OrderPluginOptions> { } +} diff --git a/src/main.lib/Plugins/Interfaces/IResolver.cs b/src/main.lib/Plugins/Interfaces/IResolver.cs index 85315ab..b22d6ec 100644 --- a/src/main.lib/Plugins/Interfaces/IResolver.cs +++ b/src/main.lib/Plugins/Interfaces/IResolver.cs @@ -8,18 +8,22 @@ namespace PKISharp.WACS.Plugins.Interfaces { public interface IResolver { + Task<ITargetPluginOptionsFactory> GetTargetPlugin(ILifetimeScope scope); + + Task<IValidationPluginOptionsFactory> GetValidationPlugin(ILifetimeScope scope, Target target); + + Task<IOrderPluginOptionsFactory> GetOrderPlugin(ILifetimeScope scope); + + Task<ICsrPluginOptionsFactory> GetCsrPlugin(ILifetimeScope scope); + + Task<IStorePluginOptionsFactory?> GetStorePlugin(ILifetimeScope scope, IEnumerable<IStorePluginOptionsFactory> chosen); + Task<IInstallationPluginOptionsFactory?> GetInstallationPlugin( ILifetimeScope scope, IEnumerable<Type> storeType, IEnumerable<IInstallationPluginOptionsFactory> chosen); - Task<IStorePluginOptionsFactory?> GetStorePlugin(ILifetimeScope scope, - IEnumerable<IStorePluginOptionsFactory> chosen); - Task<ITargetPluginOptionsFactory> GetTargetPlugin(ILifetimeScope scope); - Task<ICsrPluginOptionsFactory> GetCsrPlugin(ILifetimeScope scope); - - Task<IValidationPluginOptionsFactory> GetValidationPlugin(ILifetimeScope scope, Target target); } }
\ No newline at end of file diff --git a/src/main.lib/Plugins/Interfaces/IStorePluginOptionsFactory.cs b/src/main.lib/Plugins/Interfaces/IStorePluginOptionsFactory.cs index 3becd42..26c93c7 100644 --- a/src/main.lib/Plugins/Interfaces/IStorePluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Interfaces/IStorePluginOptionsFactory.cs @@ -1,24 +1,9 @@ using PKISharp.WACS.Plugins.Base.Options; -using PKISharp.WACS.Services; -using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.Interfaces { /// <summary> /// StorePluginFactory interface /// </summary> - public interface IStorePluginOptionsFactory : IPluginOptionsFactory - { - /// <summary> - /// Check or get information needed for store (interactive) - /// </summary> - /// <param name="target"></param> - Task<StorePluginOptions?> Aquire(IInputService inputService, RunLevel runLevel); - - /// <summary> - /// Check information needed for store (unattended) - /// </summary> - /// <param name="target"></param> - Task<StorePluginOptions?> Default(); - } + public interface IStorePluginOptionsFactory : IPluginOptionsFactory<StorePluginOptions> { } } diff --git a/src/main.lib/Plugins/Interfaces/ITargetPluginOptionsFactory.cs b/src/main.lib/Plugins/Interfaces/ITargetPluginOptionsFactory.cs index d9b13d5..6b7564e 100644 --- a/src/main.lib/Plugins/Interfaces/ITargetPluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Interfaces/ITargetPluginOptionsFactory.cs @@ -7,22 +7,11 @@ namespace PKISharp.WACS.Plugins.Interfaces /// <summary> /// TargetPluginFactory interface /// </summary> - public interface ITargetPluginOptionsFactory : IPluginOptionsFactory + public interface ITargetPluginOptionsFactory : IPluginOptionsFactory<TargetPluginOptions> { /// <summary> /// Hide when it cannot be chosen /// </summary> bool Hidden { get; } - /// <summary> - /// Check or get information needed for target (interactive) - /// </summary> - /// <param name="target"></param> - Task<TargetPluginOptions?> Aquire(IInputService inputService, RunLevel runLevel); - - /// <summary> - /// Check information needed for target (unattended) - /// </summary> - /// <param name="target"></param> - Task<TargetPluginOptions?> Default(); } } diff --git a/src/main.lib/Plugins/OrderPlugins/Single/Single.cs b/src/main.lib/Plugins/OrderPlugins/Single/Single.cs new file mode 100644 index 0000000..7c911e7 --- /dev/null +++ b/src/main.lib/Plugins/OrderPlugins/Single/Single.cs @@ -0,0 +1,11 @@ +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Plugins.Interfaces; +using System.Collections.Generic; + +namespace PKISharp.WACS.Plugins.OrderPlugins +{ + class Single : IOrderPlugin + { + public IEnumerable<Order> Split(Renewal renewal, Target target) => new List<Order>() { new Order(renewal, target) }; + } +} diff --git a/src/main.lib/Plugins/OrderPlugins/Single/SingleOptions.cs b/src/main.lib/Plugins/OrderPlugins/Single/SingleOptions.cs new file mode 100644 index 0000000..a4fcacf --- /dev/null +++ b/src/main.lib/Plugins/OrderPlugins/Single/SingleOptions.cs @@ -0,0 +1,12 @@ +using PKISharp.WACS.Plugins.Base; +using PKISharp.WACS.Plugins.Base.Options; + +namespace PKISharp.WACS.Plugins.OrderPlugins +{ + [Plugin("b705fa7c-1152-4436-8913-e433d7f84c82")] + internal class SingleOptions : OrderPluginOptions<Single> + { + public override string Name => "Single"; + public override string Description => "Single certificate"; + } +} diff --git a/src/main.lib/Plugins/OrderPlugins/Single/SingleOptionsFactory.cs b/src/main.lib/Plugins/OrderPlugins/Single/SingleOptionsFactory.cs new file mode 100644 index 0000000..76d574a --- /dev/null +++ b/src/main.lib/Plugins/OrderPlugins/Single/SingleOptionsFactory.cs @@ -0,0 +1,12 @@ +using PKISharp.WACS.Plugins.Base.Factories; +using PKISharp.WACS.Services; +using System.Threading.Tasks; + +namespace PKISharp.WACS.Plugins.OrderPlugins +{ + class SingleOptionsFactory : OrderPluginOptionsFactory<Single, SingleOptions> + { + public override Task<SingleOptions> Aquire(IInputService inputService, RunLevel runLevel) => Default(); + public override Task<SingleOptions> Default() => Task.FromResult(new SingleOptions()); + } +} diff --git a/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs b/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs index f898d87..0572bcf 100644 --- a/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs +++ b/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs @@ -4,6 +4,7 @@ using PKISharp.WACS.Extensions; using PKISharp.WACS.Plugins.Base.Factories.Null; using PKISharp.WACS.Plugins.CsrPlugins; using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Plugins.OrderPlugins; using PKISharp.WACS.Plugins.StorePlugins; using PKISharp.WACS.Plugins.ValidationPlugins.Http; using PKISharp.WACS.Services; @@ -202,5 +203,21 @@ namespace PKISharp.WACS.Plugins.Resolvers } return factory; } + + public virtual async Task<IOrderPluginOptionsFactory> GetOrderPlugin(ILifetimeScope scope) + { + var pluginName = _options.MainArguments.Order; + if (string.IsNullOrEmpty(pluginName)) + { + return scope.Resolve<SingleOptionsFactory>(); + } + var factory = _plugins.OrderPluginFactory(scope, pluginName); + if (factory == null) + { + _log.Error("Unable to find order plugin {PluginName}", pluginName); + return new NullOrderOptionsFactory(); + } + return factory; + } } } diff --git a/src/main.lib/RenewalCreator.cs b/src/main.lib/RenewalCreator.cs index 69131e5..5f3e1c7 100644 --- a/src/main.lib/RenewalCreator.cs +++ b/src/main.lib/RenewalCreator.cs @@ -324,8 +324,8 @@ namespace PKISharp.WACS // Try to run for the first time var renewal = await CreateRenewal(tempRenewal, runLevel); retry: - var result = await _renewalExecution.Execute(renewal, runLevel); - if (result == null) + var result = await _renewalExecution.HandleRenewal(renewal, runLevel); + if (result.Abort) { _exceptionHandler.HandleException(message: $"Create certificate cancelled"); } @@ -336,7 +336,7 @@ namespace PKISharp.WACS { goto retry; } - _exceptionHandler.HandleException(message: $"Create certificate failed: {result?.ErrorMessage}"); + _exceptionHandler.HandleException(message: $"Create certificate failed: {string.Join(", ", result.ErrorMessages)}"); } else { diff --git a/src/main.lib/RenewalExecutor.cs b/src/main.lib/RenewalExecutor.cs index a4a8eb3..e4ff7a9 100644 --- a/src/main.lib/RenewalExecutor.cs +++ b/src/main.lib/RenewalExecutor.cs @@ -1,6 +1,4 @@ -using ACMESharp.Protocol; -using ACMESharp.Protocol.Resources; -using Autofac; +using Autofac; using PKISharp.WACS.Clients.Acme; using PKISharp.WACS.Configuration; using PKISharp.WACS.DomainObjects; @@ -12,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using acme = ACMESharp.Protocol.Resources; namespace PKISharp.WACS { @@ -27,6 +26,27 @@ namespace PKISharp.WACS private readonly IInputService _input; private readonly ExceptionHandler _exceptionHandler; + /// <summary> + /// Common objects used throughout the renewal process + /// </summary> + private class ExecutionContext + { + public ILifetimeScope Scope { get; private set; } + public Order Order { get; private set; } + public RunLevel RunLevel { get; private set; } + public RenewResult Result { get; private set; } + public Target Target => Order.Target; + public Renewal Renewal => Order.Renewal; + + public ExecutionContext(ILifetimeScope scope, Order order, RunLevel runLevel, RenewResult result) + { + Scope = scope; + Order = order; + RunLevel = runLevel; + Result = result; + } + } + public RenewalExecutor( MainArguments args, IAutofacBuilder scopeBuilder, ILogService log, IInputService input, @@ -40,7 +60,13 @@ namespace PKISharp.WACS _container = container; } - public async Task<RenewResult?> Execute(Renewal renewal, RunLevel runLevel) + /// <summary> + /// Determine if the renewal should be executes + /// </summary> + /// <param name="renewal"></param> + /// <param name="runLevel"></param> + /// <returns></returns> + public async Task<RenewResult> HandleRenewal(Renewal renewal, RunLevel runLevel) { using var ts = _scopeBuilder.Target(_container, renewal, runLevel); using var es = _scopeBuilder.Execution(ts, renewal, runLevel); @@ -68,6 +94,11 @@ namespace PKISharp.WACS throw new Exception($"Validation plugin is unable to validate the target. A wildcard host was introduced into a HTTP validated renewal."); } + // Create one or more orders based on the target + var orderPlugin = es.Resolve<IOrderPlugin>(); + var orders = orderPlugin.Split(renewal, target); + _log.Verbose("Target convert into {n} order(s)", orders.Count()); + // Check if renewal is needed if (!runLevel.HasFlag(RunLevel.ForceRenew) && !renewal.Updated) { @@ -75,15 +106,21 @@ namespace PKISharp.WACS if (!renewal.IsDue()) { var cs = es.Resolve<ICertificateService>(); - var cache = cs.CachedInfo(renewal, target); - if (cache != null) + var abort = true; + foreach (var order in orders) { - _log.Information("Renewal for {renewal} is due after {date}", renewal.LastFriendlyName, renewal.GetDueDate()); - return null; + var cache = cs.CachedInfo(order); + if (cache == null && !renewal.New) + { + _log.Information(LogType.All, "Renewal for {renewal} running prematurely due to detected target change", renewal.LastFriendlyName); + abort = false; + break; + } } - else if (!renewal.New) + if (abort) { - _log.Information(LogType.All, "Renewal for {renewal} running prematurely due to detected target change", renewal.LastFriendlyName); + _log.Information("Renewal for {renewal} is due after {date}", renewal.LastFriendlyName, renewal.GetDueDate()); + return new RenewResult() { Abort = true }; } } else if (!renewal.New) @@ -96,103 +133,132 @@ namespace PKISharp.WACS _log.Information(LogType.All, "Force renewing certificate for {renewal}", renewal.LastFriendlyName); } - // Create the order - var orderManager = es.Resolve<OrderManager>(); - var order = await orderManager.GetOrCreate(renewal, target, runLevel); - if (order == null) - { - return OnRenewFail(new Challenge() { Error = "Unable to create order" }); - } + // If at this point we haven't retured already with an error/abort + // actually execute the renewal + return await ExecuteRenewal(es, orders.ToList(), runLevel); + } - // Answer the challenges - var client = es.Resolve<AcmeClient>(); - foreach (var authUrl in order.Payload.Authorizations) + /// <summary> + /// Run the renewal + /// </summary> + /// <param name="execute"></param> + /// <param name="orders"></param> + /// <param name="runLevel"></param> + /// <returns></returns> + private async Task<RenewResult> ExecuteRenewal(ILifetimeScope execute, List<Order> orders, RunLevel runLevel) + { + var result = new RenewResult(); + foreach (var order in orders) { - // Get authorization details - _log.Verbose("Handle authorization {n}/{m}", - order.Payload.Authorizations.ToList().IndexOf(authUrl) + 1, - order.Payload.Authorizations.Length + 1); + _log.Verbose("Handle order {n}/{m} ({friendly})", + orders.IndexOf(order) + 1, + orders.Count, + order.FriendlyNamePart ?? "main"); - var authorization = await client.GetAuthorizationDetails(authUrl); + // Create the order details + var orderManager = execute.Resolve<OrderManager>(); + order.Details = await orderManager.GetOrCreate(order, runLevel); - // Find a targetPart that matches the challenge - var targetPart = target.Parts. - FirstOrDefault(tp => tp.GetHosts(false). - Any(h => authorization.Identifier.Value == h.Replace("*.", ""))); - if (targetPart == null) - { - return OnRenewFail(new Challenge() - { - Error = "Unable to match challenge to target" - }); - } + // Create the execution context + var context = new ExecutionContext(execute, order, runLevel, result); - // Run the validation plugin - var challenge = await Authorize(es, runLevel, renewal.ValidationPluginOptions, targetPart, authorization); - if (challenge.Status != AcmeClient.AuthorizationValid) + // Authorize the order (validation) + await AuthorizeOrder(context); + if (context.Result.Success) { - return OnRenewFail(challenge); + // Execute final steps (CSR, store, install) + await ExecuteOrder(context); } } - return await OnValidationSuccess(es, renewal, target, order, runLevel); + return result; } /// <summary> - /// Steps to take on authorization failed + /// Answer all the challenges in the order /// </summary> - /// <param name="auth"></param> + /// <param name="execute"></param> + /// <param name="order"></param> + /// <param name="result"></param> + /// <param name="runLevel"></param> /// <returns></returns> - private RenewResult OnRenewFail(Challenge challenge) + private async Task AuthorizeOrder(ExecutionContext context) { - var errors = challenge?.Error; - if (errors != null) + // Sanity check + if (context.Order.Details == null) { - return new RenewResult($"Authorization failed: {errors.ToString()}"); - } - else + context.Result.AddErrorMessage($"Unable to create order"); + return; + } + + // Answer the challenges + var client = context.Scope.Resolve<AcmeClient>(); + var authorizations = context.Order.Details.Payload.Authorizations.ToList(); + foreach (var authorizationUri in authorizations) { - return new RenewResult($"Authorization failed"); + _log.Verbose("Handle authorization {n}/{m}", + authorizations.IndexOf(authorizationUri) + 1, + authorizations.Count); + + // Get authorization challenge details from server + var authorization = await client.GetAuthorizationDetails(authorizationUri); + + // Find a targetPart that matches the challenge + var targetPart = context.Target.Parts. + FirstOrDefault(tp => tp.GetHosts(false). + Any(h => authorization.Identifier.Value == h.Replace("*.", ""))); + if (targetPart == null) + { + context.Result.AddErrorMessage("Unable to match challenge to target"); + return; + } + + // Run the validation plugin + await HandleChallenge(context, targetPart, authorization); } } /// <summary> /// Steps to take on succesful (re)authorization /// </summary> - /// <param name="target"></param> - private async Task<RenewResult?> OnValidationSuccess(ILifetimeScope renewalScope, Renewal renewal, Target target, OrderDetails order, RunLevel runLevel) + /// <param name="partialTarget"></param> + private async Task ExecuteOrder(ExecutionContext context) { - RenewResult? result = null; try { - var certificateService = renewalScope.Resolve<ICertificateService>(); - var csrPlugin = target.CsrBytes == null ? renewalScope.Resolve<ICsrPlugin>() : null; + var certificateService = context.Scope.Resolve<ICertificateService>(); + var csrPlugin = context.Target.CsrBytes == null ? + context.Scope.Resolve<ICsrPlugin>() : + null; if (csrPlugin != null) { var (disabled, disabledReason) = csrPlugin.Disabled; if (disabled) { - return new RenewResult($"CSR plugin is not available. {disabledReason}"); + context.Result.AddErrorMessage($"CSR plugin is not available. {disabledReason}"); + return; } } - var oldCertificate = certificateService.CachedInfo(renewal); - var newCertificate = await certificateService.RequestCertificate(csrPlugin, runLevel, renewal, target, order); + var oldCertificate = certificateService.CachedInfo(context.Order); + var newCertificate = await certificateService.RequestCertificate(csrPlugin, context.RunLevel, context.Order); // Test if a new certificate has been generated if (newCertificate == null) { - return new RenewResult("No certificate generated"); + context.Result.AddErrorMessage("No certificate generated"); + return; } else { - result = new RenewResult(newCertificate); + context.Result.AddThumbprint(newCertificate.Certificate.Thumbprint); } // Early escape for testing validation only - if (renewal.New && - runLevel.HasFlag(RunLevel.Test) && + if (context.Renewal.New && + context.RunLevel.HasFlag(RunLevel.Test) && !await _input.PromptYesNo($"[--test] Do you want to install the certificate?", true)) { - return null; + context.Result.Abort = true; + return; } // Run store plugin(s) @@ -200,11 +266,11 @@ namespace PKISharp.WACS var storePlugins = new List<IStorePlugin>(); try { - var steps = renewal.StorePluginOptions.Count(); + var steps = context.Renewal.StorePluginOptions.Count(); for (var i = 0; i < steps; i++) { - var storeOptions = renewal.StorePluginOptions[i]; - var storePlugin = (IStorePlugin)renewalScope.Resolve(storeOptions.Instance); + var storeOptions = context.Renewal.StorePluginOptions[i]; + var storePlugin = (IStorePlugin)context.Scope.Resolve(storeOptions.Instance); if (!(storePlugin is INull)) { if (steps > 1) @@ -218,7 +284,8 @@ namespace PKISharp.WACS var (disabled, disabledReason) = storePlugin.Disabled; if (disabled) { - return new RenewResult($"Store plugin is not available. {disabledReason}"); + context.Result.AddErrorMessage($"Store plugin is not available. {disabledReason}"); + return; } await storePlugin.Save(newCertificate); storePlugins.Add(storePlugin); @@ -229,19 +296,18 @@ namespace PKISharp.WACS catch (Exception ex) { var reason = _exceptionHandler.HandleException(ex, "Unable to store certificate"); - result.ErrorMessage = $"Store failed: {reason}"; - result.Success = false; - return result; + context.Result.AddErrorMessage($"Store failed: {reason}"); + return; } // Run installation plugin(s) try { - var steps = renewal.InstallationPluginOptions.Count(); + var steps = context.Renewal.InstallationPluginOptions.Count(); for (var i = 0; i < steps; i++) { - var installOptions = renewal.InstallationPluginOptions[i]; - var installPlugin = (IInstallationPlugin)renewalScope.Resolve( + var installOptions = context.Renewal.InstallationPluginOptions[i]; + var installPlugin = (IInstallationPlugin)context.Scope.Resolve( installOptions.Instance, new TypedParameter(installOptions.GetType(), installOptions)); @@ -258,7 +324,8 @@ namespace PKISharp.WACS var (disabled, disabledReason) = installPlugin.Disabled; if (disabled) { - return new RenewResult($"Installation plugin is not available. {disabledReason}"); + context.Result.AddErrorMessage($"Installation plugin is not available. {disabledReason}"); + return; } await installPlugin.Install(storePlugins, newCertificate, oldCertificate); } @@ -267,8 +334,8 @@ namespace PKISharp.WACS catch (Exception ex) { var reason = _exceptionHandler.HandleException(ex, "Unable to install certificate"); - result.Success = false; - result.ErrorMessage = $"Install failed: {reason}"; + context.Result.AddErrorMessage($"Install failed: {reason}"); + return; } // Delete the old certificate if not forbidden, found and not re-used @@ -285,52 +352,33 @@ namespace PKISharp.WACS catch (Exception ex) { _log.Error(ex, "Unable to delete previous certificate"); - //result.Success = false; // not a show-stopper, consider the renewal a success - result.ErrorMessage = $"Delete failed: {ex.Message}"; + // not a show-stopper, consider the renewal a success + context.Result.AddErrorMessage($"Delete failed: {ex.Message}", false); } } } - if ((renewal.New || renewal.Updated) && !_args.NoTaskScheduler) + if ((context.Renewal.New || context.Renewal.Updated) && !_args.NoTaskScheduler) { - if (runLevel.HasFlag(RunLevel.Test) && + if (context.RunLevel.HasFlag(RunLevel.Test) && !await _input.PromptYesNo($"[--test] Do you want to automatically renew this certificate?", true)) { // Early out for test runs - return null; + context.Result.Abort = true; + return; } else { // Make sure the Task Scheduler is configured - await renewalScope.Resolve<TaskSchedulerService>().EnsureTaskScheduler(runLevel, false); + await context.Scope.Resolve<TaskSchedulerService>().EnsureTaskScheduler(context.RunLevel, false); } } - - return result; } catch (Exception ex) { - _exceptionHandler.HandleException(ex); - while (ex.InnerException != null) - { - ex = ex.InnerException; - } - - // Result might still contain the Thumbprint of the certificate - // that was requested and (partially? installed, which might help - // with debugging - if (result == null) - { - result = new RenewResult(ex.Message); - } - else - { - result.Success = false; - result.ErrorMessage = ex.Message; - } + var message = _exceptionHandler.HandleException(ex); + context.Result.AddErrorMessage(message); } - - return result; } /// <summary> @@ -338,37 +386,26 @@ namespace PKISharp.WACS /// </summary> /// <param name="target"></param> /// <returns></returns> - private async Task<Challenge> Authorize( - ILifetimeScope execute, RunLevel runLevel, - ValidationPluginOptions options, TargetPart targetPart, - Authorization authorization) + private async Task HandleChallenge(ExecutionContext context, TargetPart targetPart, acme.Authorization authorization) { - var invalid = new Challenge { Status = AcmeClient.AuthorizationInvalid }; - var valid = new Challenge { Status = AcmeClient.AuthorizationValid }; - var client = execute.Resolve<AcmeClient>(); + var valid = false; + var client = context.Scope.Resolve<AcmeClient>(); var identifier = authorization.Identifier.Value; + var options = context.Renewal.ValidationPluginOptions; IValidationPlugin? validationPlugin = null; - using var validation = _scopeBuilder.Validation(execute, options, targetPart, identifier); + using var validation = _scopeBuilder.Validation(context.Scope, options, targetPart, identifier); try { if (authorization.Status == AcmeClient.AuthorizationValid) { - if (!runLevel.HasFlag(RunLevel.Test) && - !runLevel.HasFlag(RunLevel.IgnoreCache)) + if (!context.RunLevel.HasFlag(RunLevel.Test) && + !context.RunLevel.HasFlag(RunLevel.IgnoreCache)) { _log.Information("Cached authorization result for {identifier}: {Status}", identifier, authorization.Status); - return valid; - } - - if (runLevel.HasFlag(RunLevel.IgnoreCache)) - { - // Due to the IgnoreCache flag (--force switch) - // we are going to attempt to re-authorize the - // domain even though its already autorized. - // On failure, we can still use the cached result. - // This helps for migration scenarios. - invalid = valid; + return; } + // Used to make --force validation errors non-fatal + valid = true; } _log.Information("Authorize identifier {identifier}", identifier); @@ -377,48 +414,39 @@ namespace PKISharp.WACS var challenge = authorization.Challenges.FirstOrDefault(c => string.Equals(c.Type, options.ChallengeType, StringComparison.CurrentCultureIgnoreCase)); if (challenge == null) { - if (authorization.Status == AcmeClient.AuthorizationValid) + if (valid) { var usedType = authorization.Challenges. - Where(x => x.Status == AcmeClient.AuthorizationValid). + Where(x => x.Status == AcmeClient.ChallengeValid). FirstOrDefault(); _log.Warning("Expected challenge type {type} not available for {identifier}, already validated using {valided}.", options.ChallengeType, authorization.Identifier.Value, usedType?.Type ?? "[unknown]"); - return valid; + return; } else { _log.Error("Expected challenge type {type} not available for {identifier}.", options.ChallengeType, authorization.Identifier.Value); - invalid.Error = "Expected challenge type not available"; - return invalid; + context.Result.AddErrorMessage("Expected challenge type not available", !valid); + return; } } else { _log.Verbose("Initial challenge status: {status}", challenge.Status); - if (challenge.Status == AcmeClient.AuthorizationValid) + if (challenge.Status == AcmeClient.ChallengeValid) { // We actually should not get here because if one of the // challenges is valid, the authorization itself should also // be valid. - if (!runLevel.HasFlag(RunLevel.Test) && - !runLevel.HasFlag(RunLevel.IgnoreCache)) - { - _log.Information("Cached authorization result: {Status}", authorization.Status); - return valid; - } - if (runLevel.HasFlag(RunLevel.IgnoreCache)) + if (!context.RunLevel.HasFlag(RunLevel.Test) && + !context.RunLevel.HasFlag(RunLevel.IgnoreCache)) { - // Due to the IgnoreCache flag (--force switch) - // we are going to attempt to re-authorize the - // domain even though its already autorized. - // On failure, we can still use the cached result. - // This helps for migration scenarios. - invalid = valid; + _log.Information("Cached challenge result: {Status}", authorization.Status); + return; } } } @@ -434,16 +462,16 @@ namespace PKISharp.WACS } if (validationPlugin == null) { - _log.Error("Validation plugin not found or not created."); - invalid.Error = "Validation plugin not found or not created."; - return invalid; + _log.Error("Validation plugin not found or not created"); + context.Result.AddErrorMessage("Validation plugin not found or not created", !valid); + return; } var (disabled, disabledReason) = validationPlugin.Disabled; if (disabled) { _log.Error($"Validation plugin is not available. {disabledReason}"); - invalid.Error = "Validation plugin is not available."; - return invalid; + context.Result.AddErrorMessage("Validation plugin is not available", !valid); + return; } _log.Information("Authorizing {dnsIdentifier} using {challengeType} validation ({name})", identifier, @@ -457,34 +485,33 @@ namespace PKISharp.WACS catch (Exception ex) { _log.Error(ex, "Error preparing for challenge answer"); - invalid.Error = "Error preparing for challenge answer"; - return invalid; + context.Result.AddErrorMessage("Error preparing for challenge answer", !valid); + return; } _log.Debug("Submitting challenge answer"); challenge = await client.AnswerChallenge(challenge); - if (challenge.Status != AcmeClient.AuthorizationValid) + if (challenge.Status != AcmeClient.ChallengeValid) { if (challenge.Error != null) { _log.Error(challenge.Error.ToString()); } _log.Error("Authorization result: {Status}", challenge.Status); - invalid.Error = challenge.Error; - return invalid; + context.Result.AddErrorMessage(challenge.Error?.ToString() ?? "Unspecified error", !valid); + return; } else { _log.Information("Authorization result: {Status}", challenge.Status); - return valid; + return; } } catch (Exception ex) { _log.Error("Error authorizing {renewal}", targetPart); - _exceptionHandler.HandleException(ex); - invalid.Error = ex.Message; - return invalid; + var message = _exceptionHandler.HandleException(ex); + context.Result.AddErrorMessage(message, !valid); } finally { diff --git a/src/main.lib/RenewalManager.cs b/src/main.lib/RenewalManager.cs index 0410339..424e1fe 100644 --- a/src/main.lib/RenewalManager.cs +++ b/src/main.lib/RenewalManager.cs @@ -198,20 +198,7 @@ namespace PKISharp.WACS var confirm = await _input.PromptYesNo($"Are you sure you want to revoke the most recently issued certificate for {selectedRenewals.Count()} currently selected {renewalSelectedLabel}? This should only be done in case of a (suspected) security breach. Cancel the {renewalSelectedLabel} if you simply don't need the certificates anymore.", false); if (confirm) { - foreach (var renewal in selectedRenewals) - { - using var scope = _scopeBuilder.Execution(_container, renewal, RunLevel.Interactive); - var cs = scope.Resolve<ICertificateService>(); - try - { - await cs.RevokeCertificate(renewal); - renewal.History.Add(new RenewResult("Certificate revoked")); - } - catch (Exception ex) - { - _exceptionHandler.HandleException(ex); - } - }; + await RevokeCertificates(selectedRenewals); } }, $"Revoke certificate for {selectionLabel}", "V", @@ -502,8 +489,8 @@ namespace PKISharp.WACS var notification = _container.Resolve<NotificationService>(); try { - var result = await _renewalExecutor.Execute(renewal, runLevel); - if (result != null) + var result = await _renewalExecutor.HandleRenewal(renewal, runLevel); + if (!result.Abort) { _renewalStore.Save(renewal, result); if (result.Success) @@ -512,14 +499,14 @@ namespace PKISharp.WACS } else { - notification.NotifyFailure(runLevel, renewal, result.ErrorMessage); + notification.NotifyFailure(runLevel, renewal, result.ErrorMessages); } } } catch (Exception ex) { _exceptionHandler.HandleException(ex); - notification.NotifyFailure(runLevel, renewal, ex.Message); + notification.NotifyFailure(runLevel, renewal, new List<string> { ex.Message }); } } @@ -553,6 +540,10 @@ namespace PKISharp.WACS _input.Show("Renewed", $"{renewal.History.Where(x => x.Success).Count()} times"); renewal.TargetPluginOptions.Show(_input); renewal.ValidationPluginOptions.Show(_input); + if (renewal.OrderPluginOptions != null) + { + renewal.OrderPluginOptions.Show(_input); + } if (renewal.CsrPluginOptions != null) { renewal.CsrPluginOptions.Show(_input); @@ -613,6 +604,16 @@ namespace PKISharp.WACS { _log.Warning($"Certificates should only be revoked in case of a (suspected) security breach. Cancel the renewal if you simply don't need the certificate anymore."); var renewals = await FilterRenewalsByCommandLine("revoke"); + await RevokeCertificates(renewals); + } + + /// <summary> + /// Shared code for command line and renewal manager + /// </summary> + /// <param name="renewals"></param> + /// <returns></returns> + internal async Task RevokeCertificates(IEnumerable<Renewal> renewals) + { foreach (var renewal in renewals) { using var scope = _scopeBuilder.Execution(_container, renewal, RunLevel.Unattended); diff --git a/src/main.lib/Services/AutofacBuilder.cs b/src/main.lib/Services/AutofacBuilder.cs index b94f599..d3a3149 100644 --- a/src/main.lib/Services/AutofacBuilder.cs +++ b/src/main.lib/Services/AutofacBuilder.cs @@ -152,6 +152,10 @@ namespace PKISharp.WACS.Services { builder.RegisterInstance(renewal.CsrPluginOptions).As(renewal.CsrPluginOptions.GetType()); } + if (renewal.OrderPluginOptions != null) + { + builder.RegisterInstance(renewal.OrderPluginOptions).As(renewal.OrderPluginOptions.GetType()); + } builder.RegisterInstance(renewal.ValidationPluginOptions).As(renewal.ValidationPluginOptions.GetType()); builder.RegisterInstance(renewal.TargetPluginOptions).As(renewal.TargetPluginOptions.GetType()); @@ -167,6 +171,14 @@ namespace PKISharp.WACS.Services { builder.RegisterType(renewal.CsrPluginOptions.Instance).As<ICsrPlugin>().SingleInstance(); } + if (renewal.OrderPluginOptions != null) + { + builder.RegisterType(renewal.OrderPluginOptions.Instance).As<IOrderPlugin>().SingleInstance(); + } + else + { + builder.RegisterType<Plugins.OrderPlugins.Single>().As<IOrderPlugin>().SingleInstance(); + } builder.RegisterType(renewal.ValidationPluginOptions.Instance).As<IValidationPlugin>().SingleInstance(); builder.RegisterType(renewal.TargetPluginOptions.Instance).As<ITargetPlugin>().SingleInstance(); foreach (var i in renewal.InstallationPluginOptions) diff --git a/src/main.lib/Services/CertificateService.cs b/src/main.lib/Services/CertificateService.cs index a0fabbe..3e73ff3 100644 --- a/src/main.lib/Services/CertificateService.cs +++ b/src/main.lib/Services/CertificateService.cs @@ -11,7 +11,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; @@ -130,58 +129,49 @@ namespace PKISharp.WACS.Services /// </summary> /// <param name="renewal"></param> /// <returns></returns> - public CertificateInfo? CachedInfo(Renewal renewal, Target? target = null) + public CertificateInfo? CachedInfo(Order order) { - var nameAll = GetPath(renewal, ""); - var directory = new DirectoryInfo(Path.GetDirectoryName(nameAll)); - var allPattern = Path.GetFileName(nameAll); - var allFiles = directory.EnumerateFiles(allPattern + "*"); - if (!allFiles.Any()) + var cachedInfos = CachedInfos(order.Renewal); + if (!cachedInfos.Any()) { return null; } - FileInfo? fileCache = null; - if (target != null) + var keyName = GetPath(order.Renewal, $"-{CacheKey(order)}{PfxPostFix}"); + var fileCache = cachedInfos.Where(x => x.CacheFile?.FullName == keyName).FirstOrDefault(); + if (fileCache == null) { - var key = CacheKey(renewal, target); - var keyName = Path.GetFileName(GetPath(renewal, $"-{key}{PfxPostFix}")); - var keyFile = allFiles.Where(x => x.Name == keyName).FirstOrDefault(); - if (keyFile != null) - { - fileCache = keyFile; - } - else + var legacyFile = GetPath(order.Renewal, PfxPostFixLegacy); + var candidate = cachedInfos.Where(x => x.CacheFile?.FullName == legacyFile).FirstOrDefault(); + if (Match(candidate, order.Target)) { - var legacyFile = new FileInfo(GetPath(renewal, PfxPostFixLegacy)); - if (legacyFile.Exists) - { - var legacyInfo = FromCache(legacyFile, renewal.PfxPassword?.Value); - if (Match(legacyInfo, target)) - { - fileCache = legacyFile; - } - } + fileCache = candidate; } } - else - { - fileCache = allFiles.OrderByDescending(x => x.LastWriteTime).FirstOrDefault(); - } + return fileCache; + } - if (fileCache != null) + public IEnumerable<CertificateInfo> CachedInfos(Renewal renewal) + { + var ret = new List<CertificateInfo>(); + var nameAll = GetPath(renewal, "*.pfx"); + var directory = new DirectoryInfo(Path.GetDirectoryName(nameAll)); + var allPattern = Path.GetFileName(nameAll); + var allFiles = directory.EnumerateFiles(allPattern + "*"); + var fileCache = allFiles.OrderByDescending(x => x.LastWriteTime); + foreach (var file in fileCache) { try { - return FromCache(fileCache, renewal.PfxPassword?.Value); + ret.Add(FromCache(file, renewal.PfxPassword?.Value)); } catch { // File corrupt or invalid password? - _log.Warning("Unable to read from certificate cache"); + _log.Warning("Unable to read {i} from certificate cache", file.Name); } } - return null; + return ret; } /// <summary> @@ -209,19 +199,20 @@ namespace PKISharp.WACS.Services /// <param name="renewal"></param> /// <param name="target"></param> /// <returns></returns> - public string CacheKey(Renewal renewal, Target target) + public string CacheKey(Order order) { // Check if we can reuse a cached certificate and/or order // based on currently active set of parameters and shape of // the target. var cacheKeyBuilder = new StringBuilder(); - cacheKeyBuilder.Append(target.CommonName); - cacheKeyBuilder.Append(string.Join(',', target.GetHosts(true).OrderBy(x => x).Select(x => x.ToLower()))); - _ = target.CsrBytes != null ? - cacheKeyBuilder.Append(Convert.ToBase64String(target.CsrBytes)) : + cacheKeyBuilder.Append(order.CacheKeyPart); + cacheKeyBuilder.Append(order.Target.CommonName); + cacheKeyBuilder.Append(string.Join(',', order.Target.GetHosts(true).OrderBy(x => x).Select(x => x.ToLower()))); + _ = order.Target.CsrBytes != null ? + cacheKeyBuilder.Append(Convert.ToBase64String(order.Target.CsrBytes)) : cacheKeyBuilder.Append("-"); - _ = renewal.CsrPluginOptions != null ? - cacheKeyBuilder.Append(JsonConvert.SerializeObject(renewal.CsrPluginOptions)) : + _ = order.Renewal.CsrPluginOptions != null ? + cacheKeyBuilder.Append(JsonConvert.SerializeObject(order.Renewal.CsrPluginOptions)) : cacheKeyBuilder.Append("-"); return cacheKeyBuilder.ToString().SHA1(); } @@ -235,20 +226,20 @@ namespace PKISharp.WACS.Services /// <param name="target"></param> /// <param name="order"></param> /// <returns></returns> - public async Task<CertificateInfo> RequestCertificate( - ICsrPlugin? csrPlugin, - RunLevel runLevel, - Renewal renewal, - Target target, - OrderDetails order) + public async Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Order order) { + if (order.Details == null) + { + throw new InvalidOperationException(); + } + // What are we going to get? - var cacheKey = CacheKey(renewal, target); - var pfxFileInfo = new FileInfo(GetPath(renewal, $"-{cacheKey}{PfxPostFix}")); + var cacheKey = CacheKey(order); + var pfxFileInfo = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}{PfxPostFix}")); // Determine/check the common name - var identifiers = target.GetHosts(false); - var commonNameUni = target.CommonName; + var identifiers = order.Target.GetHosts(false); + var commonNameUni = order.Target.CommonName; var commonNameAscii = string.Empty; if (!string.IsNullOrWhiteSpace(commonNameUni)) { @@ -263,22 +254,26 @@ namespace PKISharp.WACS.Services } // Determine the friendly name - var friendlyNameBase = renewal.FriendlyName; + var friendlyNameBase = order.Renewal.FriendlyName; if (string.IsNullOrEmpty(friendlyNameBase)) { - friendlyNameBase = target.FriendlyName; + friendlyNameBase = order.Target.FriendlyName; } if (string.IsNullOrEmpty(friendlyNameBase)) { friendlyNameBase = commonNameUni; } + if (!string.IsNullOrEmpty(order.FriendlyNamePart)) + { + friendlyNameBase += $" [{order.FriendlyNamePart}]"; + } var friendyName = $"{friendlyNameBase} @ {_inputService.FormatDate(DateTime.Now)}"; // Try using cached certificate first to avoid rate limiting during // (initial?) deployment troubleshooting. Real certificate requests // will only be done once per day maximum unless the --force parameter // is used. - var cache = CachedInfo(renewal, target); + var cache = CachedInfo(order); if (cache != null && cache.CacheFile != null) { if (cache.CacheFile.LastWriteTime > DateTime.Now.AddDays(_settings.Cache.ReuseDays * -1)) @@ -300,39 +295,39 @@ namespace PKISharp.WACS.Services } } - if (order.Payload.Status != AcmeClient.OrderValid) + if (order.Details.Payload.Status != AcmeClient.OrderValid) { // Clear cache and write new cert - ClearCache(renewal, postfix: CsrPostFix); + ClearCache(order.Renewal, postfix: CsrPostFix); - if (target.CsrBytes == null) + if (order.Target.CsrBytes == null) { if (csrPlugin == null) { throw new InvalidOperationException("Missing csrPlugin"); } - var keyFile = GetPath(renewal, ".keys"); + var keyFile = GetPath(order.Renewal, ".keys"); var csr = await csrPlugin.GenerateCsr(keyFile, commonNameAscii, identifiers); var keySet = await csrPlugin.GetKeys(); - target.CsrBytes = csr.GetDerEncoded(); - target.PrivateKey = keySet.Private; - var csrPath = GetPath(renewal, CsrPostFix); - File.WriteAllText(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", target.CsrBytes)); + order.Target.CsrBytes = csr.GetDerEncoded(); + order.Target.PrivateKey = keySet.Private; + var csrPath = GetPath(order.Renewal, CsrPostFix); + File.WriteAllText(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", order.Target.CsrBytes)); _log.Debug("CSR stored at {path} in certificate cache folder {folder}", Path.GetFileName(csrPath), Path.GetDirectoryName(csrPath)); } _log.Verbose("Submitting CSR"); - order = await _client.SubmitCsr(order, target.CsrBytes); - if (order.Payload.Status != AcmeClient.OrderValid) + order.Details = await _client.SubmitCsr(order.Details, order.Target.CsrBytes); + if (order.Details.Payload.Status != AcmeClient.OrderValid) { - _log.Error("Unexpected order status {status}", order.Payload.Status); + _log.Error("Unexpected order status {status}", order.Details.Payload.Status); throw new Exception($"Unable to complete order"); } } _log.Information("Requesting certificate {friendlyName}", friendlyNameBase); - var rawCertificate = await _client.GetCertificate(order); + var rawCertificate = await _client.GetCertificate(order.Details); if (rawCertificate == null) { throw new Exception($"Unable to get certificate"); @@ -371,9 +366,9 @@ namespace PKISharp.WACS.Services // Assume that the first certificate in the reponse is the main one // so we associate the private key with that one. Other certificates // are intermediates - if (startIndex == 0 && target.PrivateKey != null) + if (startIndex == 0 && order.Target.PrivateKey != null) { - var bcPrivateKeyEntry = new bc.Pkcs.AsymmetricKeyEntry(target.PrivateKey); + var bcPrivateKeyEntry = new bc.Pkcs.AsymmetricKeyEntry(order.Target.PrivateKey); pfx.SetKeyEntry(bcCertificateAlias, bcPrivateKeyEntry, new[] { bcCertificateEntry }); } } @@ -398,9 +393,9 @@ namespace PKISharp.WACS.Services X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); - ClearCache(renewal, postfix: $"*{PfxPostFix}"); - ClearCache(renewal, postfix: $"*{PfxPostFixLegacy}"); - File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, renewal.PfxPassword?.Value)); + ClearCache(order.Renewal, postfix: $"*{PfxPostFix}"); + ClearCache(order.Renewal, postfix: $"*{PfxPostFixLegacy}"); + File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value)); _log.Debug("Certificate written to cache file {path} in certificate cache folder {folder}. It will be " + "reused when renewing within {x} day(s) as long as the Target and Csr parameters remain the same and " + "the --force switch is not used.", @@ -424,7 +419,7 @@ namespace PKISharp.WACS.Services { newVersion.FriendlyName = friendyName; tempPfx[certIndex] = newVersion; - File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, renewal.PfxPassword?.Value)); + File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value)); newVersion.Dispose(); } } @@ -440,10 +435,10 @@ namespace PKISharp.WACS.Services // Update LastFriendlyName so that the user sees // the most recently issued friendlyName in // the WACS GUI - renewal.LastFriendlyName = friendlyNameBase; + order.Renewal.LastFriendlyName = friendlyNameBase; // Recreate X509Certificate2 with correct flags for Store/Install - return FromCache(pfxFileInfo, renewal.PfxPassword?.Value); + return FromCache(pfxFileInfo, order.Renewal.PfxPassword?.Value); } private CertificateInfo FromCache(FileInfo pfxFileInfo, string? password) @@ -501,14 +496,21 @@ namespace PKISharp.WACS.Services public async Task RevokeCertificate(Renewal renewal) { // Delete cached files - var info = CachedInfo(renewal); - if (info != null) + var infos = CachedInfos(renewal); + foreach (var info in infos) { - var certificateDer = info.Certificate.Export(X509ContentType.Cert); - await _client.RevokeCertificate(certificateDer); + try + { + var certificateDer = info.Certificate.Export(X509ContentType.Cert); + await _client.RevokeCertificate(certificateDer); + info.CacheFile?.Delete(); + _log.Warning($"Revoked certificate {info.Certificate.FriendlyName}"); + } + catch (Exception ex) + { + _log.Error(ex, $"Error revoking certificate {info.Certificate.FriendlyName}, you may retry"); + } } - ClearCache(renewal); - _log.Warning("Certificate for {target} revoked, you should renew immediately", renewal); } /// <summary> @@ -517,6 +519,5 @@ namespace PKISharp.WACS.Services /// <param name="friendlyName"></param> /// <returns></returns> public static Func<X509Certificate2, bool> ThumbprintFilter(string thumbprint) => new Func<X509Certificate2, bool>(x => string.Equals(x.Thumbprint, thumbprint)); - } } diff --git a/src/main.lib/Services/Interfaces/ICertificateService.cs b/src/main.lib/Services/Interfaces/ICertificateService.cs index 27e8cb8..1d7f4f3 100644 --- a/src/main.lib/Services/Interfaces/ICertificateService.cs +++ b/src/main.lib/Services/Interfaces/ICertificateService.cs @@ -1,15 +1,17 @@ using ACMESharp.Protocol; using PKISharp.WACS.DomainObjects; using PKISharp.WACS.Plugins.Interfaces; +using System.Collections.Generic; using System.Threading.Tasks; namespace PKISharp.WACS.Services { internal interface ICertificateService { - string CacheKey(Renewal renewal, Target target); - CertificateInfo? CachedInfo(Renewal renewal, Target? target = null); - Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Renewal renewal, Target target, OrderDetails order); + string CacheKey(Order order); + CertificateInfo? CachedInfo(Order order); + IEnumerable<CertificateInfo> CachedInfos(Renewal renewal); + Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Order order); Task RevokeCertificate(Renewal renewal); void Encrypt(); void Delete(Renewal renewal); diff --git a/src/main.lib/Services/Interfaces/IPluginService.cs b/src/main.lib/Services/Interfaces/IPluginService.cs index 790c644..02a4793 100644 --- a/src/main.lib/Services/Interfaces/IPluginService.cs +++ b/src/main.lib/Services/Interfaces/IPluginService.cs @@ -8,17 +8,21 @@ namespace PKISharp.WACS.Services { public interface IPluginService { - ICsrPluginOptionsFactory CsrPluginFactory(ILifetimeScope scope, string name); + List<ITargetPluginOptionsFactory> TargetPluginFactories(ILifetimeScope scope); + List<IValidationPluginOptionsFactory> ValidationPluginFactories(ILifetimeScope scope); + List<IOrderPluginOptionsFactory> OrderPluginFactories(ILifetimeScope scope); List<ICsrPluginOptionsFactory> CsrPluginOptionsFactories(ILifetimeScope scope); + List<IStorePluginOptionsFactory> StorePluginFactories(ILifetimeScope scope); List<IInstallationPluginOptionsFactory> InstallationPluginFactories(ILifetimeScope scope); + + ITargetPluginOptionsFactory TargetPluginFactory(ILifetimeScope scope, string name); + IValidationPluginOptionsFactory ValidationPluginFactory(ILifetimeScope scope, string type, string name); + IOrderPluginOptionsFactory OrderPluginFactory(ILifetimeScope scope, string name); + ICsrPluginOptionsFactory CsrPluginFactory(ILifetimeScope scope, string name); + IStorePluginOptionsFactory StorePluginFactory(ILifetimeScope scope, string name); IInstallationPluginOptionsFactory InstallationPluginFactory(ILifetimeScope scope, string name); + List<IArgumentsProvider> OptionProviders(); List<Type> PluginOptionTypes<T>() where T : PluginOptions; - List<IStorePluginOptionsFactory> StorePluginFactories(ILifetimeScope scope); - IStorePluginOptionsFactory StorePluginFactory(ILifetimeScope scope, string name); - List<ITargetPluginOptionsFactory> TargetPluginFactories(ILifetimeScope scope); - ITargetPluginOptionsFactory TargetPluginFactory(ILifetimeScope scope, string name); - List<IValidationPluginOptionsFactory> ValidationPluginFactories(ILifetimeScope scope); - IValidationPluginOptionsFactory ValidationPluginFactory(ILifetimeScope scope, string type, string name); } } diff --git a/src/main.lib/Services/NotificationService.cs b/src/main.lib/Services/NotificationService.cs index 282e8aa..62e344e 100644 --- a/src/main.lib/Services/NotificationService.cs +++ b/src/main.lib/Services/NotificationService.cs @@ -2,6 +2,7 @@ using PKISharp.WACS.Clients; using PKISharp.WACS.DomainObjects; using System; +using System.Collections.Generic; using System.Linq; using System.Net.Mail; @@ -49,14 +50,20 @@ namespace PKISharp.WACS.Services /// </summary> /// <param name="runLevel"></param> /// <param name="renewal"></param> - internal void NotifyFailure(RunLevel runLevel, Renewal renewal, string? errorMessage) + internal void NotifyFailure(RunLevel runLevel, Renewal renewal, List<string> errorMessage) { // Do not send emails when running interactively _log.Error("Renewal for {friendlyName} failed, will retry on next run", renewal.LastFriendlyName); + if (errorMessage.Count == 0) + { + errorMessage.Add("No specific error reason provided."); + } if (runLevel.HasFlag(RunLevel.Unattended)) { _email.Send("Error processing certificate renewal", - $"<p>Renewal for <b>{renewal.LastFriendlyName}</b> failed with error <b>{errorMessage ?? "(null)"}</b>, will retry on next run.</p> {NotificationInformation(renewal)}", + @$"<p>Renewal for <b>{renewal.LastFriendlyName}</b> failed with error(s) + <ul><li>{string.Join("</li><li>", errorMessage)}</li></ul> will retry + on next run.</p> {NotificationInformation(renewal)}", MessagePriority.Urgent); } } @@ -70,6 +77,10 @@ namespace PKISharp.WACS.Services extraMessage += "<p><table><tr><td>Plugins</td><td></td></tr>"; extraMessage += $"<tr><td>Target: </td><td> {renewal.TargetPluginOptions.Name}</td></tr>"; extraMessage += $"<tr><td>Validation: </td><td> {renewal.ValidationPluginOptions.Name}</td></tr>"; + if (renewal.OrderPluginOptions != null) + { + extraMessage += $"<tr><td>Order: </td><td> {renewal.OrderPluginOptions.Name}</td></tr>"; + } if (renewal.CsrPluginOptions != null) { extraMessage += $"<tr><td>CSR: </td><td> {renewal.CsrPluginOptions.Name}</td></tr>"; @@ -90,14 +101,14 @@ namespace PKISharp.WACS.Services { try { - var cache = _certificateService.CachedInfo(renewal); - if (cache == null) + var infos = _certificateService.CachedInfos(renewal); + if (infos == null || infos.Count() == 0) { return "Unknown"; } else { - return string.Join(", ", cache.SanNames); + return string.Join(", ", infos.SelectMany(i => i.SanNames).Distinct()); } } catch diff --git a/src/main.lib/Services/PluginService.cs b/src/main.lib/Services/PluginService.cs index fff7fec..b2c4a57 100644 --- a/src/main.lib/Services/PluginService.cs +++ b/src/main.lib/Services/PluginService.cs @@ -19,12 +19,14 @@ namespace PKISharp.WACS.Services private readonly List<Type> _targetOptionFactories; private readonly List<Type> _validationOptionFactories; + private readonly List<Type> _orderOptionFactories; private readonly List<Type> _csrOptionFactories; private readonly List<Type> _storeOptionFactories; private readonly List<Type> _installationOptionFactories; private readonly List<Type> _target; private readonly List<Type> _validation; + private readonly List<Type> _order; private readonly List<Type> _csr; private readonly List<Type> _store; private readonly List<Type> _installation; @@ -43,6 +45,8 @@ namespace PKISharp.WACS.Services public List<ITargetPluginOptionsFactory> TargetPluginFactories(ILifetimeScope scope) => GetFactories<ITargetPluginOptionsFactory>(_targetOptionFactories, scope); public List<IValidationPluginOptionsFactory> ValidationPluginFactories(ILifetimeScope scope) => GetFactories<IValidationPluginOptionsFactory>(_validationOptionFactories, scope); + + public List<IOrderPluginOptionsFactory> OrderPluginFactories(ILifetimeScope scope) => GetFactories<IOrderPluginOptionsFactory>(_orderOptionFactories, scope); public List<ICsrPluginOptionsFactory> CsrPluginOptionsFactories(ILifetimeScope scope) => GetFactories<ICsrPluginOptionsFactory>(_csrOptionFactories, scope); @@ -60,6 +64,8 @@ namespace PKISharp.WACS.Services FirstOrDefault(x => x.Match(name) && string.Equals(type, x.ChallengeType, StringComparison.InvariantCultureIgnoreCase)); } + public IOrderPluginOptionsFactory OrderPluginFactory(ILifetimeScope scope, string name) => GetByName<IOrderPluginOptionsFactory>(_orderOptionFactories, name, scope); + public ICsrPluginOptionsFactory CsrPluginFactory(ILifetimeScope scope, string name) => GetByName<ICsrPluginOptionsFactory>(_csrOptionFactories, name, scope); public IStorePluginOptionsFactory StorePluginFactory(ILifetimeScope scope, string name) => GetByName<IStorePluginOptionsFactory>(_storeOptionFactories, name, scope); @@ -72,12 +78,14 @@ namespace PKISharp.WACS.Services { _targetOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); _validationOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); + _orderOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); _csrOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); _storeOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); _installationOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); _target.ForEach(ip => builder.RegisterType(ip)); _validation.ForEach(ip => builder.RegisterType(ip)); + _order.ForEach(ip => builder.RegisterType(ip)); _csr.ForEach(ip => builder.RegisterType(ip)); _store.ForEach(ip => builder.RegisterType(ip)); _installation.ForEach(ip => builder.RegisterType(ip)); @@ -96,18 +104,21 @@ namespace PKISharp.WACS.Services _targetOptionFactories = GetResolvable<ITargetPluginOptionsFactory>(); _validationOptionFactories = GetResolvable<IValidationPluginOptionsFactory>(); + _orderOptionFactories = GetResolvable<IOrderPluginOptionsFactory>(); _csrOptionFactories = GetResolvable<ICsrPluginOptionsFactory>(); _storeOptionFactories = GetResolvable<IStorePluginOptionsFactory>(true); _installationOptionFactories = GetResolvable<IInstallationPluginOptionsFactory>(true); _target = GetResolvable<ITargetPlugin>(); _validation = GetResolvable<IValidationPlugin>(); + _order = GetResolvable<IOrderPlugin>(); _csr = GetResolvable<ICsrPlugin>(); _store = GetResolvable<IStorePlugin>(); _installation = GetResolvable<IInstallationPlugin>(); ListPlugins(_target, "target"); ListPlugins(_validation, "validation"); + ListPlugins(_order, "order"); ListPlugins(_csr, "csr"); ListPlugins(_store, "store"); ListPlugins(_installation, "installation"); diff --git a/src/main.test/Mock/Services/CertificateService.cs b/src/main.test/Mock/Services/CertificateService.cs index aafb218..690c4dc 100644 --- a/src/main.test/Mock/Services/CertificateService.cs +++ b/src/main.test/Mock/Services/CertificateService.cs @@ -3,6 +3,7 @@ using PKISharp.WACS.DomainObjects; using PKISharp.WACS.Plugins.Interfaces; using PKISharp.WACS.Services; using System; +using System.Collections.Generic; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; @@ -11,18 +12,18 @@ namespace PKISharp.WACS.UnitTests.Mock.Services { internal class CertificateService : ICertificateService { - public CertificateInfo? CachedInfo(Renewal renewal, Target? target = null) => null; - public string CacheKey(Renewal renewal, Target target) => ""; - + public CertificateInfo? CachedInfo(Order order) => null; + public IEnumerable<CertificateInfo> CachedInfos(Renewal renewal) => new List<CertificateInfo>(); + public string CacheKey(Order order) => ""; public void Delete(Renewal renewal) {} public void Encrypt() { } - public Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Renewal renewal, Target target, OrderDetails order) + public Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Order order) { // Create self-signed certificate var ecdsa = ECDsa.Create(); // generate asymmetric key pair - var req = new CertificateRequest($"CN={target.CommonName}", ecdsa, HashAlgorithmName.SHA256); + var req = new CertificateRequest($"CN={order.Target.CommonName}", ecdsa, HashAlgorithmName.SHA256); var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5)); return Task.FromResult(new CertificateInfo(cert) { @@ -32,6 +33,6 @@ namespace PKISharp.WACS.UnitTests.Mock.Services }); } - public Task RevokeCertificate(Renewal renewal) => Task.CompletedTask; + public Task RevokeCertificate(Renewal renewal) => Task.CompletedTask; } } diff --git a/src/main.test/Tests/InstallationPluginTests/ScriptPluginTests.cs b/src/main.test/Tests/InstallationPluginTests/ScriptPluginTests.cs index 7a6746b..ddbb419 100644 --- a/src/main.test/Tests/InstallationPluginTests/ScriptPluginTests.cs +++ b/src/main.test/Tests/InstallationPluginTests/ScriptPluginTests.cs @@ -66,8 +66,9 @@ namespace PKISharp.WACS.UnitTests.Tests.InstallationPluginTests var settings = new MockSettingsService(); var userRoleService = new Mock.Services.UserRoleService(); var store = new CertificateStore(log, iis, settings, userRoleService, new FindPrivateKey(log), storeOptions); - var oldCert = cs.RequestCertificate(null, RunLevel.Unattended, renewal, new Target("", "test.local", new List<TargetPart>()), new ACMESharp.Protocol.OrderDetails()).Result; - var newCert = cs.RequestCertificate(null, RunLevel.Unattended, renewal, new Target("", "test.local", new List<TargetPart>()), new ACMESharp.Protocol.OrderDetails()).Result; + var targetOrder = new Order(renewal, new Target("", "test.local", new List<TargetPart>())); + var oldCert = cs.RequestCertificate(null, RunLevel.Unattended, targetOrder).Result; + var newCert = cs.RequestCertificate(null, RunLevel.Unattended, targetOrder).Result; newCert.StoreInfo.Add(typeof(CertificateStore), new StoreInfo() { }); var options = new ScriptOptions { |