Skip to content

Commit

Permalink
Azure certificate retrieval fix (#12)
Browse files Browse the repository at this point in the history
* * Fixed not being able to retrieve site certificate PFX bytes from Azure
* Fixed error in formatting of domains array in logging
* Added conditions to reduce unnecessary updates of hostname bindings

* Changes to use 'var' for consistency

* Removed use of $ in logging for consistency

* Added constant AzureCertThumbprintsAppSettingName "WEBSITE_LOAD_CERTIFICATES"

* Reverted explicit serialization of domains array in logging

* Added DoesBindingNeedUpdating
  • Loading branch information
killswtch authored and ffMathy committed Oct 30, 2019
1 parent 60183db commit 52462c6
Showing 1 changed file with 123 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ namespace FluffySpoon.LetsEncrypt.Azure

public class AzureAppServiceSslBindingCertificatePersistenceStrategy : ICertificatePersistenceStrategy, IAzureAppServiceSslBindingCertificatePersistenceStrategy
{
private const string AzureCertThumbprintsAppSettingName = "WEBSITE_LOAD_CERTIFICATES";

private readonly AzureOptions azureOptions;
private readonly LetsEncryptOptions letsEncryptOptions;

Expand Down Expand Up @@ -105,9 +107,9 @@ public async Task PersistAsync(PersistenceType persistenceType, byte[] bytes)
}

if (regionName == null)
throw new InvalidOperationException("Could not find an app that has a hostname created for domains " + domains + ".");
throw new InvalidOperationException("Could not find an app that has a hostname created for domains " + String.Join(", ", domains) + ".");

var azureCertificate = await GetExistingCertificateAsync(persistenceType);
var azureCertificate = await GetExistingAzureCertificateAsync(persistenceType);
if (azureCertificate != null)
{
logger.LogInformation("Updating existing Azure certificate for key {0}.", persistenceType);
Expand All @@ -123,7 +125,7 @@ await client.WebApps.Manager
HostNames = domains,
PfxBlob = bytes
});
azureCertificate = await GetExistingCertificateAsync(persistenceType);
azureCertificate = await GetExistingAzureCertificateAsync(persistenceType);
}
else
{
Expand Down Expand Up @@ -164,7 +166,7 @@ await client.WebApps.Manager
string[] domainsToUpgrade;
if (azureOptions.Slot == null)
{
logger.LogInformation("Updating host name bindings for app {0}", appTuple.App.Name);
logger.LogInformation("Checking host name bindings for app {0}", appTuple.App.Name);
domainsToUpgrade = appTuple
.App
.HostNames
Expand All @@ -173,7 +175,7 @@ await client.WebApps.Manager
}
else
{
logger.LogInformation("Updating host name bindings for app {0}/{1}", appTuple.App.Name, appTuple.Slot.Name);
logger.LogInformation("Checking host name bindings for app {0}/{1}", appTuple.App.Name, appTuple.Slot.Name);
domainsToUpgrade = appTuple
.Slot
.HostNames
Expand All @@ -183,24 +185,42 @@ await client.WebApps.Manager

foreach (var domain in domainsToUpgrade)
{
logger.LogDebug("Updating host name bindings for domain {0}", domain);
logger.LogDebug("Checking host name binding for domain {0}", domain);

if (azureOptions.Slot == null)
{
await client.WebApps.Inner.CreateOrUpdateHostNameBindingWithHttpMessagesAsync(
azureOptions.ResourceGroupName,
var existingBinding = await client.WebApps.Inner.GetHostNameBindingAsync(azureOptions.ResourceGroupName,
appTuple.App.Name,
domain,
new HostNameBindingInner(
azureResourceType: AzureResourceType.Website,
hostNameType: HostNameType.Verified,
customHostNameDnsRecordType: CustomHostNameDnsRecordType.CName,
sslState: SslState.SniEnabled,
thumbprint: azureCertificate.Thumbprint));
domain);

if (DoesBindingNeedUpdating(existingBinding, azureCertificate.Thumbprint))
{
logger.LogDebug("Updating host name binding for domain {0}", domain);

await client.WebApps.Inner.CreateOrUpdateHostNameBindingWithHttpMessagesAsync(
azureOptions.ResourceGroupName,
appTuple.App.Name,
domain,
new HostNameBindingInner(
azureResourceType: AzureResourceType.Website,
hostNameType: HostNameType.Verified,
customHostNameDnsRecordType: CustomHostNameDnsRecordType.CName,
sslState: SslState.SniEnabled,
thumbprint: azureCertificate.Thumbprint));
}
}
else
{
await client.WebApps.Inner.CreateOrUpdateHostNameBindingSlotWithHttpMessagesAsync(
var existingBinding = await client.WebApps.Inner.GetHostNameBindingSlotAsync(azureOptions.ResourceGroupName,
appTuple.App.Name,
appTuple.Slot.Name,
domain);

if (DoesBindingNeedUpdating(existingBinding, azureCertificate.Thumbprint))
{
logger.LogDebug("Updating host name binding for domain {0}", domain);

await client.WebApps.Inner.CreateOrUpdateHostNameBindingSlotWithHttpMessagesAsync(
azureOptions.ResourceGroupName,
appTuple.App.Name,
domain,
Expand All @@ -211,18 +231,71 @@ await client.WebApps.Inner.CreateOrUpdateHostNameBindingSlotWithHttpMessagesAsyn
sslState: SslState.SniEnabled,
thumbprint: azureCertificate.Thumbprint),
appTuple.Slot.Name);
}
}
}

logger.LogDebug($"Getting app settings");

var appSettings = await client.WebApps.Manager
.WebApps
.GetByResourceGroup(appTuple.App.ResourceGroupName, appTuple.App.Name)
.GetAppSettingsAsync();

var loadCertificatesSetting = appSettings.ContainsKey(AzureCertThumbprintsAppSettingName) ? appSettings[AzureCertThumbprintsAppSettingName].Value : String.Empty;
var certThumbprintsToLoad = loadCertificatesSetting.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
if (!certThumbprintsToLoad.Contains(azureCertificate.Thumbprint))
{
logger.LogInformation("Adding certificate thumbprint {0} to {1} app setting", azureCertificate.Thumbprint, AzureCertThumbprintsAppSettingName);

certThumbprintsToLoad.Add(azureCertificate.Thumbprint);

loadCertificatesSetting = String.Join(",", certThumbprintsToLoad);

try
{
await client.WebApps.Manager
.WebApps
.GetByResourceGroup(appTuple.App.ResourceGroupName, appTuple.App.Name)
.Update()
.WithAppSetting(AzureCertThumbprintsAppSettingName, loadCertificatesSetting)
.ApplyAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Error updating app settings for {0}", appTuple.App.Name);
}
}
}
}

private bool DoesBindingNeedUpdating(HostNameBindingInner existingBinding, string certificateThumbprint)
{
return existingBinding == null || existingBinding.SslState != SslState.SniEnabled || existingBinding.Thumbprint != certificateThumbprint;
}

public async Task<byte[]> RetrieveAsync(PersistenceType persistenceType)
{
var certificate = await GetExistingCertificateAsync(persistenceType);
return certificate?.PfxBlob;

if (certificate == null)
{
logger.LogInformation("Certificate of type {0} not found.", persistenceType);
return null;
}

var pfxBlob = certificate?.GetRawCertData();

if (pfxBlob == null || pfxBlob.Length == 0)
{
logger.LogError("Certificate was found (thumbprint {0}), but PfxBlob was null or 0 length.", certificate.Thumbprint);
return null;
}

return pfxBlob;
}

private async Task<IAppServiceCertificate> GetExistingCertificateAsync(PersistenceType persistenceType)
private async Task<IAppServiceCertificate> GetExistingAzureCertificateAsync(PersistenceType persistenceType)
{
if (persistenceType != PersistenceType.Site)
{
Expand All @@ -249,5 +322,37 @@ private async Task<IAppServiceCertificate> GetExistingCertificateAsync(Persisten

return null;
}

private async Task<X509Certificate2> GetExistingCertificateAsync(PersistenceType persistenceType)
{
var azureCert = await GetExistingAzureCertificateAsync(persistenceType);

var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);

try
{
var certCollection = certStore.Certificates.Find(
X509FindType.FindByThumbprint,
// Replace below with your certificate's thumbprint
azureCert.Thumbprint,
false);

// Get the first cert with the thumbprint
if (certCollection.Count > 0)
{
var cert = certCollection[0];
return cert;
}
}
finally
{
certStore.Close();
}

logger.LogInformation("Could not find existing Azure certificate.");

return null;
}
}
}

0 comments on commit 52462c6

Please sign in to comment.