Skip to content

Commit

Permalink
Fix #1330 (#1331)
Browse files Browse the repository at this point in the history
* Fix renewals being due when upgrading to version 2.1.2 - #1330
  • Loading branch information
WouterTinus committed Dec 19, 2019
1 parent d9e9786 commit f0e0bc4
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 34 deletions.
1 change: 1 addition & 0 deletions src/main.lib/Clients/IIS/IISHelper.cs
Expand Up @@ -212,6 +212,7 @@ private List<string> GetHosts(IIISSite site)
{
return site.Bindings.Select(x => x.Host.ToLower()).
Where(x => !string.IsNullOrWhiteSpace(x)).
OrderBy(x => x).
Distinct().
ToList();
}
Expand Down
106 changes: 74 additions & 32 deletions src/main.lib/Services/CertificateService.cs
Expand Up @@ -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;
Expand Down Expand Up @@ -82,11 +85,11 @@ private void CheckStaleFiles()
/// 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();
}
}
Expand Down Expand Up @@ -121,32 +124,49 @@ public void Encrypt()
/// <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));
pfxFileInfo = allFiles.Where(x => x.Name == fileName).FirstOrDefault();
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 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
{
Expand All @@ -157,6 +177,22 @@ public void Encrypt()
return null;
}

/// <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
Expand Down Expand Up @@ -193,7 +229,7 @@ public async Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, Run
{
// 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);
Expand Down Expand Up @@ -251,10 +287,11 @@ public async Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, Run
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)
Expand All @@ -263,9 +300,13 @@ public async Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, Run
}
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");
Expand Down Expand Up @@ -342,7 +383,15 @@ public async Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, Run
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)
{
Expand Down Expand Up @@ -447,13 +496,6 @@ public async Task RevokeCertificate(Renewal renewal)
_log.Warning("Certificate for {target} revoked, you should renew immediately", renewal);
}

/// <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>
Expand Down
4 changes: 2 additions & 2 deletions src/main.test/Tests/TargetPluginTests/IISSitesTests.cs
Expand Up @@ -134,8 +134,8 @@ public void CommonNameExcludedAfter()
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]
Expand Down

0 comments on commit f0e0bc4

Please sign in to comment.