summaryrefslogtreecommitdiffstats
path: root/src/main.lib/Services/CertificateService.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.lib/Services/CertificateService.cs')
-rw-r--r--src/main.lib/Services/CertificateService.cs179
1 files changed, 93 insertions, 86 deletions
diff --git a/src/main.lib/Services/CertificateService.cs b/src/main.lib/Services/CertificateService.cs
index a0fabbe..eca6861 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;
@@ -110,7 +109,7 @@ namespace PKISharp.WACS.Services
{
foreach (var f in _cache.EnumerateFiles($"*.keys"))
{
- var x = new ProtectedString(File.ReadAllText(f.FullName));
+ var x = new ProtectedString(File.ReadAllText(f.FullName), _log);
_log.Information("Rewriting {x}", f.Name);
File.WriteAllText(f.FullName, x.DiskValue(_settings.Security.EncryptConfig));
}
@@ -130,58 +129,52 @@ 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 (candidate != null)
{
- var legacyFile = new FileInfo(GetPath(renewal, PfxPostFixLegacy));
- if (legacyFile.Exists)
+ if (Match(candidate, order.Target))
{
- 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 +202,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 +229,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))
{
@@ -262,23 +256,30 @@ namespace PKISharp.WACS.Services
}
}
- // Determine the friendly name
- var friendlyNameBase = renewal.FriendlyName;
+ // Determine the friendly name base (for the renewal)
+ var friendlyNameBase = order.Renewal.FriendlyName;
if (string.IsNullOrEmpty(friendlyNameBase))
{
- friendlyNameBase = target.FriendlyName;
+ friendlyNameBase = order.Target.FriendlyName;
}
if (string.IsNullOrEmpty(friendlyNameBase))
{
friendlyNameBase = commonNameUni;
}
- var friendyName = $"{friendlyNameBase} @ {_inputService.FormatDate(DateTime.Now)}";
+
+ // Determine the friendly name for this specific certificate
+ var friendlyNameIntermediate = friendlyNameBase;
+ if (!string.IsNullOrEmpty(order.FriendlyNamePart))
+ {
+ friendlyNameIntermediate += $" [{order.FriendlyNamePart}]";
+ }
+ var friendlyName = $"{friendlyNameIntermediate} @ {_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))
@@ -292,7 +293,7 @@ namespace PKISharp.WACS.Services
{
_log.Warning("Using cached certificate for {friendlyName}. To force a new request of the " +
"certificate within {days} days, run with the --{switch} switch.",
- friendlyNameBase,
+ friendlyNameIntermediate,
_settings.Cache.ReuseDays,
nameof(MainArguments.Force).ToLower());
return cache;
@@ -300,39 +301,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);
+ _log.Information("Requesting certificate {friendlyName}", friendlyNameIntermediate);
+ var rawCertificate = await _client.GetCertificate(order.Details);
if (rawCertificate == null)
{
throw new Exception($"Unable to get certificate");
@@ -364,16 +365,16 @@ namespace PKISharp.WACS.Services
{
var bcCertificateEntry = new bc.Pkcs.X509CertificateEntry(bcCertificate);
var bcCertificateAlias = startIndex == 0 ?
- friendyName :
+ friendlyName :
bcCertificate.SubjectDN.ToString();
pfx.SetCertificateEntry(bcCertificateAlias, bcCertificateEntry);
// 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 +399,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.",
@@ -422,9 +423,9 @@ namespace PKISharp.WACS.Services
var newVersion = await csrPlugin.PostProcess(cert);
if (newVersion != cert)
{
- newVersion.FriendlyName = friendyName;
+ newVersion.FriendlyName = friendlyName;
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 +441,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 +502,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 +525,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));
-
}
}