Skip to content

Commit

Permalink
Updates to v2
Browse files Browse the repository at this point in the history
  • Loading branch information
sjkp committed Apr 19, 2019
1 parent 6f1a648 commit c753c31
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 134 deletions.
17 changes: 9 additions & 8 deletions LetsEncrypt.Azure.Core.V2/AcmeClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Certes;
using Certes.Acme;
using Certes.Acme.Resource;
using LetsEncrypt.Azure.Core.V2.CertificateStores;
using LetsEncrypt.Azure.Core.V2.DnsProviders;
using LetsEncrypt.Azure.Core.V2.Models;
using Microsoft.Extensions.Logging;
Expand All @@ -18,15 +19,15 @@ public class AcmeClient
{
private readonly IDnsProvider dnsProvider;
private readonly DnsLookupService dnsLookupService;
private readonly IFileSystem fileSystem;
private readonly ICertificateStore certificateStore;

private readonly ILogger<AcmeClient> logger;

public AcmeClient(IDnsProvider dnsProvider, DnsLookupService dnsLookupService, IFileSystem fileSystem = null, ILogger<AcmeClient> logger = null)
public AcmeClient(IDnsProvider dnsProvider, DnsLookupService dnsLookupService, ICertificateStore certifcateStore, ILogger<AcmeClient> logger = null)
{
this.dnsProvider = dnsProvider;
this.dnsLookupService = dnsLookupService;
this.fileSystem = fileSystem ?? new FileSystem();
this.certificateStore = certifcateStore;
this.logger = logger ?? NullLogger<AcmeClient>.Instance;

}
Expand Down Expand Up @@ -103,22 +104,22 @@ public async Task<CertificateInstallModel> RequestDnsChallengeCertificate(IAcmeD
private async Task<AcmeContext> GetOrCreateAcmeContext(Uri acmeDirectoryUri, string email)
{
AcmeContext acme = null;
string filename = $"account{email}--{acmeDirectoryUri.Host}.pem";
if (! await fileSystem.Exists(filename))
string filename = $"account{email}--{acmeDirectoryUri.Host}";
var secret = await this.certificateStore.GetSecret(filename);
if (string.IsNullOrEmpty(secret))
{
acme = new AcmeContext(acmeDirectoryUri);
var account = acme.NewAccount(email, true);

// Save the account key for later use
var pemKey = acme.AccountKey.ToPem();
await fileSystem.WriteAllText(filename, pemKey);
await certificateStore.SaveSecret(filename, pemKey);
await Task.Delay(10000); //Wait a little before using the new account.
acme = new AcmeContext(acmeDirectoryUri, acme.AccountKey, new AcmeHttpClient(acmeDirectoryUri, new HttpClient()));
}
else
{
var pemKey = await fileSystem.ReadAllText(filename);
var accountKey = KeyFactory.FromPem(pemKey);
var accountKey = KeyFactory.FromPem(secret);
acme = new AcmeContext(acmeDirectoryUri, accountKey, new AcmeHttpClient(acmeDirectoryUri, new HttpClient()));
}

Expand Down
7 changes: 5 additions & 2 deletions LetsEncrypt.Azure.Core.V2/AzureHelper.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using LetsEncrypt.Azure.Core.V2.Models;
using Microsoft.Azure.Management.ResourceManager.Fluent.Authentication;
using System;
using System.Collections.Generic;
using System.Text;

namespace LetsEncrypt.Azure.Core.V2
{
Expand All @@ -20,6 +18,11 @@ public static AzureCredentials GetAzureCredentials(AzureServicePrincipal service
throw new ArgumentNullException(nameof(azureSubscription));
}

if (servicePrincipal.UseManagendIdentity)
{
return new AzureCredentials(new MSILoginInformation(MSIResourceType.AppService), Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment.FromName(azureSubscription.AzureRegion));
}

return new AzureCredentials(servicePrincipal.ServicePrincipalLoginInformation,
azureSubscription.Tenant, Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment.FromName(azureSubscription.AzureRegion));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using LetsEncrypt.Azure.Core.V2.Models;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.KeyVault.Models;

namespace LetsEncrypt.Azure.Core.V2.CertificateStores
{
Expand Down Expand Up @@ -34,12 +33,17 @@ public AzureKeyVaultCertificateStore(IKeyVaultClient keyVaultClient, string vaul

public async Task<CertificateInfo> GetCertificate(string name, string password)
{
// This retrieves the secret/certificate with the private key
var secret = await this.keyVaultClient.GetSecretAsync(this.vaultBaseUrl, name);
X509Certificate2 certificate = new X509Certificate2(Convert.FromBase64String(secret.Value), password);
var secretName = CleanName(name);
var secret = await GetSecret(name);
if (secret == null)
{
return null;
}

X509Certificate2 certificate = new X509Certificate2(Convert.FromBase64String(secret), password);

// This retrieves the secret/certificate without the private key
var certBundle = await this.keyVaultClient.GetCertificateAsync(this.vaultBaseUrl, name);
var certBundle = await this.keyVaultClient.GetCertificateAsync(this.vaultBaseUrl, secretName);
var cert = new X509Certificate2(certBundle.Cer, password);

return new CertificateInfo()
Expand All @@ -56,7 +60,38 @@ public async Task<CertificateInfo> GetCertificate(string name, string password)
/// <returns>An asynchronous result.</returns>
public Task SaveCertificate(CertificateInfo certificate)
{
return this.keyVaultClient.ImportCertificateAsync(this.vaultBaseUrl, certificate.Name, certificate.PfxCertificate.ToString(), certificate.Password);
return this.keyVaultClient.ImportCertificateAsync(this.vaultBaseUrl, CleanName(certificate.Name), certificate.PfxCertificate.ToString(), certificate.Password);
}

private string CleanName(string name)
{
Regex regex = new Regex("[^a-zA-Z0-9-]");
return regex.Replace(name, "");
}

public async Task<string> GetSecret(string name)
{
var secretName = CleanName(name);
// This retrieves the secret/certificate with the private key
SecretBundle secret = null;
try
{
secret = await this.keyVaultClient.GetSecretAsync(this.vaultBaseUrl, secretName);
}
catch (KeyVaultErrorException kvex)
{
if (kvex.Body.Error.Code == "SecretNotFound")
{
return null;
}
throw;
}
return secret.Value;
}

public Task SaveSecret(string name, string secret)
{
return this.keyVaultClient.SetSecretAsync(this.vaultBaseUrl, CleanName(name), secret);
}
}
}
23 changes: 19 additions & 4 deletions LetsEncrypt.Azure.Core.V2/CertificateStores/FileSystemBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace LetsEncrypt.Azure.Core.V2.CertificateStores
public abstract class FileSystemBase : ICertificateStore
{
private readonly IFileSystem fileSystem;

private const string fileExtension = ".pfx";

public FileSystemBase(IFileSystem fileSystem)
{
Expand All @@ -20,9 +20,10 @@ public FileSystemBase(IFileSystem fileSystem)

public async Task<CertificateInfo> GetCertificate(string name, string password)
{
if (! await this.fileSystem.Exists(name))
var filename = name + fileExtension;
if (! await this.fileSystem.Exists(filename))
return null;
var pfx = await this.fileSystem.Read(name);
var pfx = await this.fileSystem.Read(filename);
return new CertificateInfo()
{
PfxCertificate = pfx,
Expand All @@ -34,7 +35,21 @@ public async Task<CertificateInfo> GetCertificate(string name, string password)

public Task SaveCertificate(CertificateInfo certificate)
{
this.fileSystem.Write(certificate.Name, certificate.PfxCertificate);
this.fileSystem.Write(certificate.Name+fileExtension, certificate.PfxCertificate);
return Task.CompletedTask;
}

public async Task<string> GetSecret(string name)
{
var filename = name + fileExtension;
if (!await this.fileSystem.Exists(filename))
return null;
return System.Text.Encoding.UTF8.GetString(await this.fileSystem.Read(filename));
}

public Task SaveSecret(string name, string secret)
{
this.fileSystem.Write(name + fileExtension, Encoding.UTF8.GetBytes(secret));
return Task.CompletedTask;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ namespace LetsEncrypt.Azure.Core.V2.CertificateStores
{
public interface ICertificateStore
{
Task<string> GetSecret(string name);
Task SaveSecret(string name, string secret);

Task<CertificateInfo> GetCertificate(string name, string password);
Task SaveCertificate(CertificateInfo certificate);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Threading.Tasks;
using LetsEncrypt.Azure.Core.V2.Models;

namespace LetsEncrypt.Azure.Core.V2.CertificateStores
{
internal class NullCertificateStore : ICertificateStore
{
public Task<CertificateInfo> GetCertificate(string name, string password)
{
return Task.FromResult<CertificateInfo>(null);
}

public Task<string> GetSecret(string name)
{
return Task.FromResult<string>(null);
}

public Task SaveCertificate(CertificateInfo certificate)
{
return Task.CompletedTask;
}

public Task SaveSecret(string name, string secret)
{
return Task.CompletedTask;
}
}
}
90 changes: 46 additions & 44 deletions LetsEncrypt.Azure.Core.V2/LetsencryptService.cs
Original file line number Diff line number Diff line change
@@ -1,60 +1,62 @@
using LetsEncrypt.Azure.Core.V2.CertificateStores;
using LetsEncrypt.Azure.Core.V2.DnsProviders;
using LetsEncrypt.Azure.Core.V2.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Threading.Tasks;

namespace LetsEncrypt.Azure.Core.V2
{
public static class LetsencryptService
public class LetsencryptService
{
public static IServiceCollection AddAcmeClient<TDnsProvider>(this IServiceCollection serviceCollection, object dnsProviderConfig, string azureStorageConnectionString = null) where TDnsProvider : class, IDnsProvider
{
if (serviceCollection == null)
{
throw new ArgumentNullException(nameof(serviceCollection));
}
private readonly AcmeClient acmeClient;
private readonly ICertificateStore certificateStore;
private readonly AzureWebAppService azureWebAppService;
private readonly ILogger<LetsencryptService> logger;

if (dnsProviderConfig == null)
{
throw new ArgumentNullException(nameof(dnsProviderConfig));
}
if (string.IsNullOrEmpty(azureStorageConnectionString))
{
serviceCollection
.AddTransient<IFileSystem, FileSystem>()
.AddTransient<ICertificateStore, FileSystemCertificateStore>();
}
else
public LetsencryptService(AcmeClient acmeClient, ICertificateStore certificateStore, AzureWebAppService azureWebAppService, ILogger<LetsencryptService> logger = null)
{
this.acmeClient = acmeClient;
this.certificateStore = certificateStore;
this.azureWebAppService = azureWebAppService;
this.logger = logger ?? NullLogger<LetsencryptService>.Instance;
}
public async Task Run(AcmeDnsRequest acmeDnsRequest, int renewXNumberOfDaysBeforeExpiration)
{
try
{
serviceCollection
.AddTransient<IFileSystem, AzureBlobStorage>(s =>
CertificateInstallModel model = null;

var certname = acmeDnsRequest.Host.Substring(2) + "-" + acmeDnsRequest.AcmeEnvironment.Name;
var cert = await certificateStore.GetCertificate(certname, acmeDnsRequest.PFXPassword);
if (cert == null || cert.Certificate.NotAfter < DateTime.UtcNow.AddDays(renewXNumberOfDaysBeforeExpiration)) //Cert doesnt exist or expires in less than renewXNumberOfDaysBeforeExpiration days, lets renew.
{
logger.LogInformation("Certificate store didn't contain certificate or certificate was expired starting renewing");
model = await acmeClient.RequestDnsChallengeCertificate(acmeDnsRequest);
model.CertificateInfo.Name = certname;
await certificateStore.SaveCertificate(model.CertificateInfo);
}
else
{
logger.LogInformation("Certificate expires in more than {renewXNumberOfDaysBeforeExpiration} days, reusing certificate from certificate store", renewXNumberOfDaysBeforeExpiration);
model = new CertificateInstallModel()
{
return new AzureBlobStorage(azureStorageConnectionString);
})
.AddTransient<AzureBlobStorage, AzureBlobStorage>(s =>
{
return new AzureBlobStorage(azureStorageConnectionString);
})
.AddTransient<ICertificateStore, AzureBlobCertificateStore>();
}
return serviceCollection
.AddTransient<AcmeClient>()
.AddTransient<DnsLookupService>()
.AddSingleton(dnsProviderConfig.GetType(), dnsProviderConfig)
.AddTransient<IDnsProvider, TDnsProvider>();
}
CertificateInfo = cert,
Host = acmeDnsRequest.Host
};
}
await azureWebAppService.Install(model);

public static IServiceCollection AddAzureAppService(this IServiceCollection serviceCollection, params AzureWebAppSettings[] settings)
{
if (settings == null || settings.Length == 0)
logger.LogInformation("Removing expired certificates");
var expired = azureWebAppService.RemoveExpired();
logger.LogInformation("The following certificates was removed {Thumbprints}", string.Join(", ", expired.ToArray()));

}
catch (Exception e)
{
throw new ArgumentNullException(nameof(settings));
logger.LogError(e, "Failed");
throw;
}

return serviceCollection
.AddSingleton(settings)
.AddTransient<AzureWebAppService>();
}
}
}
Loading

0 comments on commit c753c31

Please sign in to comment.