diff options
-rw-r--r-- | src/main.lib/Clients/IIS/IISHelper.cs | 2 | ||||
-rw-r--r-- | src/main.lib/Services/CertificateService.cs | 106 | ||||
-rw-r--r-- | src/main.test/Tests/TargetPluginTests/IISSitesTests.cs | 4 |
3 files changed, 75 insertions, 37 deletions
diff --git a/src/main.lib/Clients/IIS/IISHelper.cs b/src/main.lib/Clients/IIS/IISHelper.cs index 9e8771a..93d08ce 100644 --- a/src/main.lib/Clients/IIS/IISHelper.cs +++ b/src/main.lib/Clients/IIS/IISHelper.cs @@ -102,7 +102,6 @@ namespace PKISharp.WACS.Clients.IIS Https = sbi.https }). DistinctBy(t => t.HostUnicode + "@" + t.SiteId). - OrderBy(t => t.HostUnicode). ToList(); return targets; @@ -213,6 +212,7 @@ namespace PKISharp.WACS.Clients.IIS { return site.Bindings.Select(x => x.Host.ToLower()). Where(x => !string.IsNullOrWhiteSpace(x)). + OrderBy(x => x). Distinct(). ToList(); } diff --git a/src/main.lib/Services/CertificateService.cs b/src/main.lib/Services/CertificateService.cs index 5156ed9..70d5b50 100644 --- a/src/main.lib/Services/CertificateService.cs +++ b/src/main.lib/Services/CertificateService.cs @@ -21,6 +21,9 @@ namespace PKISharp.WACS.Services { internal class CertificateService : ICertificateService { + private const string CsrPostFix = "-csr.pem"; + private const string PfxPostFix = "-temp.pfx"; + private readonly IInputService _inputService; private readonly ILogService _log; private readonly ISettingsService _settings; @@ -82,11 +85,11 @@ namespace PKISharp.WACS.Services /// Delete cached files related to a specific renewal /// </summary> /// <param name="renewal"></param> - private void ClearCache(Renewal renewal) + private void ClearCache(Renewal renewal, string prefix = "*", string postfix = "*") { - foreach (var f in _cache.GetFiles($"{renewal.Id}*")) + foreach (var f in _cache.GetFiles($"{prefix}{renewal.Id}{postfix}")) { - _log.Verbose("Deleting {file} from cache", f.Name); + _log.Verbose("Deleting {file} from certificate cache @ {folder}", f.Name, _cache.FullName); f.Delete(); } } @@ -121,36 +124,49 @@ namespace PKISharp.WACS.Services /// <returns></returns> public CertificateInfo? CachedInfo(Renewal renewal, Target? target = null) { - var fullPattern = PfxFilePattern(renewal, "*"); - var directory = new DirectoryInfo(Path.GetDirectoryName(fullPattern)); - var filePattern = Path.GetFileName(fullPattern); - var allFiles = directory.GetFiles(filePattern); - var pfxFileInfo = allFiles. - OrderByDescending(x => x.LastWriteTime). - FirstOrDefault(); + var nameAll = GetPath(renewal, $"*{PfxPostFix}"); + var directory = new DirectoryInfo(Path.GetDirectoryName(nameAll)); + var allPattern = Path.GetFileName(nameAll); + var allFiles = directory.GetFiles(allPattern); + if (!allFiles.Any()) + { + return null; + } + FileInfo? fileCache = null; if (target != null) { - var cacheKey = CacheKey(renewal, target); - var fileName = Path.GetFileName(PfxFilePattern(renewal, $"-{cacheKey}-")); - var specificFile = allFiles.Where(x => x.Name == fileName).FirstOrDefault(); - if (specificFile != 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) { - pfxFileInfo = specificFile; + fileCache = keyFile; + } + else + { + var legacyName = Path.GetFileName(GetPath(renewal, PfxPostFix)); + var legacyFile = allFiles.Where(x => x.Name == legacyName).FirstOrDefault(); + if (legacyFile != null) + { + var legacyInfo = FromCache(legacyFile, renewal.PfxPassword?.Value); + if (Match(legacyInfo, target)) + { + fileCache = legacyFile; + } + } } } - - // Delete other (older) cache files - foreach (var other in allFiles.Except(new[] { pfxFileInfo })) + else { - other.Delete(); + fileCache = allFiles.OrderByDescending(x => x.LastWriteTime).FirstOrDefault(); } - - if (pfxFileInfo != null) + + if (fileCache != null) { try { - return FromCache(pfxFileInfo, renewal.PfxPassword?.Value); + return FromCache(fileCache, renewal.PfxPassword?.Value); } catch { @@ -162,6 +178,22 @@ namespace PKISharp.WACS.Services } /// <summary> + /// See if the information in the certificate matches + /// that of the specified target. Used to figure out whether + /// or not the cache is out of date. + /// </summary> + /// <param name="target"></param> + /// <returns></returns> + private bool Match(CertificateInfo info, Target target) + { + var identifiers = target.GetHosts(false); + var idn = new IdnMapping(); + return info.SubjectName == idn.GetAscii(target.CommonName) && + info.HostNames.Count == identifiers.Count() && + info.HostNames.All(h => identifiers.Contains(idn.GetAscii(h))); + } + + /// <summary> /// To check if it's possible to reuse a previously retrieved /// certificate we create a hash of its key properties and included /// that hash in the file name. If we get the same hash on a @@ -197,7 +229,7 @@ namespace PKISharp.WACS.Services { // What are we going to get? var cacheKey = CacheKey(renewal, target); - var pfxFileInfo = new FileInfo(PfxFilePattern(renewal, cacheKey)); + var pfxFileInfo = new FileInfo(GetPath(renewal, $"-{cacheKey}{PfxPostFix}")); // Determine/check the common name var identifiers = target.GetHosts(false); @@ -255,10 +287,11 @@ namespace PKISharp.WACS.Services return cache; } } - // Cache is present but not used anymore - cache.CacheFile.Delete(); } + // Clear cache and write new cert + ClearCache(renewal, postfix: CsrPostFix); + if (target.CsrBytes == null) { if (csrPlugin == null) @@ -267,9 +300,13 @@ namespace PKISharp.WACS.Services } var keyFile = GetPath(renewal, ".keys"); var csr = await csrPlugin.GenerateCsr(keyFile, commonNameAscii, identifiers); + var keySet = await csrPlugin.GetKeys(); target.CsrBytes = csr.GetDerEncoded(); - target.PrivateKey = (await csrPlugin.GetKeys()).Private; - File.WriteAllText(GetPath(renewal, "-csr.pem"), _pemService.GetPem("CERTIFICATE REQUEST", target.CsrBytes)); + target.PrivateKey = keySet.Private; + var csrPath = GetPath(renewal, CsrPostFix); + File.WriteAllText(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", target.CsrBytes)); + _log.Debug("CSR stored at {path} in certificate cache folder {folder}", Path.GetFileName(csrPath), Path.GetDirectoryName(csrPath)); + } _log.Verbose("Submitting CSR"); @@ -346,7 +383,15 @@ namespace PKISharp.WACS.Services X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); + + ClearCache(renewal, postfix: $"*{PfxPostFix}"); File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, 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.", + pfxFileInfo.Name, + pfxFileInfo.Directory.FullName, + _settings.Cache.ReuseDays); if (csrPlugin != null) { @@ -452,13 +497,6 @@ namespace PKISharp.WACS.Services } /// <summary> - /// Path to the cached PFX file - /// </summary> - /// <param name="renewal"></param> - /// <returns></returns> - private string PfxFilePattern(Renewal renewal, string cacheKey) => GetPath(renewal, $"{cacheKey}temp.pfx"); - - /// <summary> /// Common filter for different store plugins /// </summary> /// <param name="friendlyName"></param> diff --git a/src/main.test/Tests/TargetPluginTests/IISSitesTests.cs b/src/main.test/Tests/TargetPluginTests/IISSitesTests.cs index d20eb61..028a3e9 100644 --- a/src/main.test/Tests/TargetPluginTests/IISSitesTests.cs +++ b/src/main.test/Tests/TargetPluginTests/IISSitesTests.cs @@ -134,8 +134,8 @@ namespace PKISharp.WACS.UnitTests.Tests.TargetPluginTests var site = iis.GetWebSite(siteId); var options = new IISSitesOptions() { SiteIds = new List<long>() { 1, 2 }, CommonName = "missing.example.com" }; var target = Target(options); - Assert.AreEqual(target.IsValid(log), true); - Assert.AreEqual(target.CommonName, site.Bindings.First().Host); + Assert.AreEqual(true, target.IsValid(log)); + Assert.AreEqual(site.Bindings.First().Host, target.CommonName); } [TestMethod] |