summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.lib/Clients/Acme/AcmeClient.cs2
-rw-r--r--src/main.lib/Clients/Acme/OrderManager.cs6
-rw-r--r--src/main.lib/Configuration/MainArguments.cs1
-rw-r--r--src/main.lib/Configuration/MainArgumentsProvider.cs7
-rw-r--r--src/main.lib/DomainObjects/Order.cs25
-rw-r--r--src/main.lib/DomainObjects/RenewResult.cs53
-rw-r--r--src/main.lib/DomainObjects/Renewal.cs8
-rw-r--r--src/main.lib/Extensions/RenewalExtensions.cs22
-rw-r--r--src/main.lib/Plugins/Base/Options/OrderPluginOptions.cs28
-rw-r--r--src/main.lib/Plugins/Base/OptionsFactories/CsrPluginOptionsFactory.cs6
-rw-r--r--src/main.lib/Plugins/Base/OptionsFactories/Null/NullCsrOptionsFactory.cs4
-rw-r--r--src/main.lib/Plugins/Base/OptionsFactories/Null/NullOrderPluginOptionsFactory.cs24
-rw-r--r--src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs4
-rw-r--r--src/main.lib/Plugins/Base/OptionsFactories/Null/NullTargetOptionsFactory.cs4
-rw-r--r--src/main.lib/Plugins/Base/OptionsFactories/OrderPluginOptionsFactory.cs23
-rw-r--r--src/main.lib/Plugins/Base/OptionsFactories/StorePluginOptionsFactory.cs4
-rw-r--r--src/main.lib/Plugins/Base/OptionsFactories/TargetPluginOptionsFactory.cs4
-rw-r--r--src/main.lib/Plugins/Interfaces/ICore.cs20
-rw-r--r--src/main.lib/Plugins/Interfaces/ICsrPluginOptionsFactory.cs17
-rw-r--r--src/main.lib/Plugins/Interfaces/IOrderPlugin.cs10
-rw-r--r--src/main.lib/Plugins/Interfaces/IOrderPluginOptionsFactory.cs6
-rw-r--r--src/main.lib/Plugins/Interfaces/IResolver.cs16
-rw-r--r--src/main.lib/Plugins/Interfaces/IStorePluginOptionsFactory.cs17
-rw-r--r--src/main.lib/Plugins/Interfaces/ITargetPluginOptionsFactory.cs13
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Single/Single.cs11
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Single/SingleOptions.cs12
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Single/SingleOptionsFactory.cs12
-rw-r--r--src/main.lib/Plugins/Resolvers/UnattendedResolver.cs17
-rw-r--r--src/main.lib/RenewalCreator.cs6
-rw-r--r--src/main.lib/RenewalExecutor.cs343
-rw-r--r--src/main.lib/RenewalManager.cs37
-rw-r--r--src/main.lib/Services/AutofacBuilder.cs12
-rw-r--r--src/main.lib/Services/CertificateService.cs163
-rw-r--r--src/main.lib/Services/Interfaces/ICertificateService.cs8
-rw-r--r--src/main.lib/Services/Interfaces/IPluginService.cs18
-rw-r--r--src/main.lib/Services/NotificationService.cs21
-rw-r--r--src/main.lib/Services/PluginService.cs11
-rw-r--r--src/main.test/Mock/Services/CertificateService.cs13
-rw-r--r--src/main.test/Tests/InstallationPluginTests/ScriptPluginTests.cs5
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
{