Skip to content

Commit c753c31

Browse files
committed
Updates to v2
1 parent 6f1a648 commit c753c31

13 files changed

+315
-134
lines changed

LetsEncrypt.Azure.Core.V2/AcmeClient.cs

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Certes;
22
using Certes.Acme;
33
using Certes.Acme.Resource;
4+
using LetsEncrypt.Azure.Core.V2.CertificateStores;
45
using LetsEncrypt.Azure.Core.V2.DnsProviders;
56
using LetsEncrypt.Azure.Core.V2.Models;
67
using Microsoft.Extensions.Logging;
@@ -18,15 +19,15 @@ public class AcmeClient
1819
{
1920
private readonly IDnsProvider dnsProvider;
2021
private readonly DnsLookupService dnsLookupService;
21-
private readonly IFileSystem fileSystem;
22+
private readonly ICertificateStore certificateStore;
2223

2324
private readonly ILogger<AcmeClient> logger;
2425

25-
public AcmeClient(IDnsProvider dnsProvider, DnsLookupService dnsLookupService, IFileSystem fileSystem = null, ILogger<AcmeClient> logger = null)
26+
public AcmeClient(IDnsProvider dnsProvider, DnsLookupService dnsLookupService, ICertificateStore certifcateStore, ILogger<AcmeClient> logger = null)
2627
{
2728
this.dnsProvider = dnsProvider;
2829
this.dnsLookupService = dnsLookupService;
29-
this.fileSystem = fileSystem ?? new FileSystem();
30+
this.certificateStore = certifcateStore;
3031
this.logger = logger ?? NullLogger<AcmeClient>.Instance;
3132

3233
}
@@ -103,22 +104,22 @@ public async Task<CertificateInstallModel> RequestDnsChallengeCertificate(IAcmeD
103104
private async Task<AcmeContext> GetOrCreateAcmeContext(Uri acmeDirectoryUri, string email)
104105
{
105106
AcmeContext acme = null;
106-
string filename = $"account{email}--{acmeDirectoryUri.Host}.pem";
107-
if (! await fileSystem.Exists(filename))
107+
string filename = $"account{email}--{acmeDirectoryUri.Host}";
108+
var secret = await this.certificateStore.GetSecret(filename);
109+
if (string.IsNullOrEmpty(secret))
108110
{
109111
acme = new AcmeContext(acmeDirectoryUri);
110112
var account = acme.NewAccount(email, true);
111113

112114
// Save the account key for later use
113115
var pemKey = acme.AccountKey.ToPem();
114-
await fileSystem.WriteAllText(filename, pemKey);
116+
await certificateStore.SaveSecret(filename, pemKey);
115117
await Task.Delay(10000); //Wait a little before using the new account.
116118
acme = new AcmeContext(acmeDirectoryUri, acme.AccountKey, new AcmeHttpClient(acmeDirectoryUri, new HttpClient()));
117119
}
118120
else
119121
{
120-
var pemKey = await fileSystem.ReadAllText(filename);
121-
var accountKey = KeyFactory.FromPem(pemKey);
122+
var accountKey = KeyFactory.FromPem(secret);
122123
acme = new AcmeContext(acmeDirectoryUri, accountKey, new AcmeHttpClient(acmeDirectoryUri, new HttpClient()));
123124
}
124125

LetsEncrypt.Azure.Core.V2/AzureHelper.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using LetsEncrypt.Azure.Core.V2.Models;
22
using Microsoft.Azure.Management.ResourceManager.Fluent.Authentication;
33
using System;
4-
using System.Collections.Generic;
5-
using System.Text;
64

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

21+
if (servicePrincipal.UseManagendIdentity)
22+
{
23+
return new AzureCredentials(new MSILoginInformation(MSIResourceType.AppService), Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment.FromName(azureSubscription.AzureRegion));
24+
}
25+
2326
return new AzureCredentials(servicePrincipal.ServicePrincipalLoginInformation,
2427
azureSubscription.Tenant, Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment.FromName(azureSubscription.AzureRegion));
2528
}

LetsEncrypt.Azure.Core.V2/CertificateStores/AzureKeyVaultCertificateStore.cs

+43-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Security.Cryptography.X509Certificates;
4-
using System.Text;
3+
using System.Text.RegularExpressions;
54
using System.Threading.Tasks;
65
using LetsEncrypt.Azure.Core.V2.Models;
76
using Microsoft.Azure.KeyVault;
8-
using Microsoft.Azure.Services.AppAuthentication;
7+
using Microsoft.Azure.KeyVault.Models;
98

109
namespace LetsEncrypt.Azure.Core.V2.CertificateStores
1110
{
@@ -34,12 +33,17 @@ public AzureKeyVaultCertificateStore(IKeyVaultClient keyVaultClient, string vaul
3433

3534
public async Task<CertificateInfo> GetCertificate(string name, string password)
3635
{
37-
// This retrieves the secret/certificate with the private key
38-
var secret = await this.keyVaultClient.GetSecretAsync(this.vaultBaseUrl, name);
39-
X509Certificate2 certificate = new X509Certificate2(Convert.FromBase64String(secret.Value), password);
36+
var secretName = CleanName(name);
37+
var secret = await GetSecret(name);
38+
if (secret == null)
39+
{
40+
return null;
41+
}
42+
43+
X509Certificate2 certificate = new X509Certificate2(Convert.FromBase64String(secret), password);
4044

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

4549
return new CertificateInfo()
@@ -56,7 +60,38 @@ public async Task<CertificateInfo> GetCertificate(string name, string password)
5660
/// <returns>An asynchronous result.</returns>
5761
public Task SaveCertificate(CertificateInfo certificate)
5862
{
59-
return this.keyVaultClient.ImportCertificateAsync(this.vaultBaseUrl, certificate.Name, certificate.PfxCertificate.ToString(), certificate.Password);
63+
return this.keyVaultClient.ImportCertificateAsync(this.vaultBaseUrl, CleanName(certificate.Name), certificate.PfxCertificate.ToString(), certificate.Password);
64+
}
65+
66+
private string CleanName(string name)
67+
{
68+
Regex regex = new Regex("[^a-zA-Z0-9-]");
69+
return regex.Replace(name, "");
70+
}
71+
72+
public async Task<string> GetSecret(string name)
73+
{
74+
var secretName = CleanName(name);
75+
// This retrieves the secret/certificate with the private key
76+
SecretBundle secret = null;
77+
try
78+
{
79+
secret = await this.keyVaultClient.GetSecretAsync(this.vaultBaseUrl, secretName);
80+
}
81+
catch (KeyVaultErrorException kvex)
82+
{
83+
if (kvex.Body.Error.Code == "SecretNotFound")
84+
{
85+
return null;
86+
}
87+
throw;
88+
}
89+
return secret.Value;
90+
}
91+
92+
public Task SaveSecret(string name, string secret)
93+
{
94+
return this.keyVaultClient.SetSecretAsync(this.vaultBaseUrl, CleanName(name), secret);
6095
}
6196
}
6297
}

LetsEncrypt.Azure.Core.V2/CertificateStores/FileSystemBase.cs

+19-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace LetsEncrypt.Azure.Core.V2.CertificateStores
1010
public abstract class FileSystemBase : ICertificateStore
1111
{
1212
private readonly IFileSystem fileSystem;
13-
13+
private const string fileExtension = ".pfx";
1414

1515
public FileSystemBase(IFileSystem fileSystem)
1616
{
@@ -20,9 +20,10 @@ public FileSystemBase(IFileSystem fileSystem)
2020

2121
public async Task<CertificateInfo> GetCertificate(string name, string password)
2222
{
23-
if (! await this.fileSystem.Exists(name))
23+
var filename = name + fileExtension;
24+
if (! await this.fileSystem.Exists(filename))
2425
return null;
25-
var pfx = await this.fileSystem.Read(name);
26+
var pfx = await this.fileSystem.Read(filename);
2627
return new CertificateInfo()
2728
{
2829
PfxCertificate = pfx,
@@ -34,7 +35,21 @@ public async Task<CertificateInfo> GetCertificate(string name, string password)
3435

3536
public Task SaveCertificate(CertificateInfo certificate)
3637
{
37-
this.fileSystem.Write(certificate.Name, certificate.PfxCertificate);
38+
this.fileSystem.Write(certificate.Name+fileExtension, certificate.PfxCertificate);
39+
return Task.CompletedTask;
40+
}
41+
42+
public async Task<string> GetSecret(string name)
43+
{
44+
var filename = name + fileExtension;
45+
if (!await this.fileSystem.Exists(filename))
46+
return null;
47+
return System.Text.Encoding.UTF8.GetString(await this.fileSystem.Read(filename));
48+
}
49+
50+
public Task SaveSecret(string name, string secret)
51+
{
52+
this.fileSystem.Write(name + fileExtension, Encoding.UTF8.GetBytes(secret));
3853
return Task.CompletedTask;
3954
}
4055
}

LetsEncrypt.Azure.Core.V2/CertificateStores/ICertificateStore.cs

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ namespace LetsEncrypt.Azure.Core.V2.CertificateStores
88
{
99
public interface ICertificateStore
1010
{
11+
Task<string> GetSecret(string name);
12+
Task SaveSecret(string name, string secret);
13+
1114
Task<CertificateInfo> GetCertificate(string name, string password);
1215
Task SaveCertificate(CertificateInfo certificate);
1316
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Threading.Tasks;
2+
using LetsEncrypt.Azure.Core.V2.Models;
3+
4+
namespace LetsEncrypt.Azure.Core.V2.CertificateStores
5+
{
6+
internal class NullCertificateStore : ICertificateStore
7+
{
8+
public Task<CertificateInfo> GetCertificate(string name, string password)
9+
{
10+
return Task.FromResult<CertificateInfo>(null);
11+
}
12+
13+
public Task<string> GetSecret(string name)
14+
{
15+
return Task.FromResult<string>(null);
16+
}
17+
18+
public Task SaveCertificate(CertificateInfo certificate)
19+
{
20+
return Task.CompletedTask;
21+
}
22+
23+
public Task SaveSecret(string name, string secret)
24+
{
25+
return Task.CompletedTask;
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,62 @@
11
using LetsEncrypt.Azure.Core.V2.CertificateStores;
2-
using LetsEncrypt.Azure.Core.V2.DnsProviders;
32
using LetsEncrypt.Azure.Core.V2.Models;
4-
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Logging.Abstractions;
55
using System;
6+
using System.Threading.Tasks;
67

78
namespace LetsEncrypt.Azure.Core.V2
89
{
9-
public static class LetsencryptService
10+
public class LetsencryptService
1011
{
11-
public static IServiceCollection AddAcmeClient<TDnsProvider>(this IServiceCollection serviceCollection, object dnsProviderConfig, string azureStorageConnectionString = null) where TDnsProvider : class, IDnsProvider
12-
{
13-
if (serviceCollection == null)
14-
{
15-
throw new ArgumentNullException(nameof(serviceCollection));
16-
}
12+
private readonly AcmeClient acmeClient;
13+
private readonly ICertificateStore certificateStore;
14+
private readonly AzureWebAppService azureWebAppService;
15+
private readonly ILogger<LetsencryptService> logger;
1716

18-
if (dnsProviderConfig == null)
19-
{
20-
throw new ArgumentNullException(nameof(dnsProviderConfig));
21-
}
22-
if (string.IsNullOrEmpty(azureStorageConnectionString))
23-
{
24-
serviceCollection
25-
.AddTransient<IFileSystem, FileSystem>()
26-
.AddTransient<ICertificateStore, FileSystemCertificateStore>();
27-
}
28-
else
17+
public LetsencryptService(AcmeClient acmeClient, ICertificateStore certificateStore, AzureWebAppService azureWebAppService, ILogger<LetsencryptService> logger = null)
18+
{
19+
this.acmeClient = acmeClient;
20+
this.certificateStore = certificateStore;
21+
this.azureWebAppService = azureWebAppService;
22+
this.logger = logger ?? NullLogger<LetsencryptService>.Instance;
23+
}
24+
public async Task Run(AcmeDnsRequest acmeDnsRequest, int renewXNumberOfDaysBeforeExpiration)
25+
{
26+
try
2927
{
30-
serviceCollection
31-
.AddTransient<IFileSystem, AzureBlobStorage>(s =>
28+
CertificateInstallModel model = null;
29+
30+
var certname = acmeDnsRequest.Host.Substring(2) + "-" + acmeDnsRequest.AcmeEnvironment.Name;
31+
var cert = await certificateStore.GetCertificate(certname, acmeDnsRequest.PFXPassword);
32+
if (cert == null || cert.Certificate.NotAfter < DateTime.UtcNow.AddDays(renewXNumberOfDaysBeforeExpiration)) //Cert doesnt exist or expires in less than renewXNumberOfDaysBeforeExpiration days, lets renew.
33+
{
34+
logger.LogInformation("Certificate store didn't contain certificate or certificate was expired starting renewing");
35+
model = await acmeClient.RequestDnsChallengeCertificate(acmeDnsRequest);
36+
model.CertificateInfo.Name = certname;
37+
await certificateStore.SaveCertificate(model.CertificateInfo);
38+
}
39+
else
40+
{
41+
logger.LogInformation("Certificate expires in more than {renewXNumberOfDaysBeforeExpiration} days, reusing certificate from certificate store", renewXNumberOfDaysBeforeExpiration);
42+
model = new CertificateInstallModel()
3243
{
33-
return new AzureBlobStorage(azureStorageConnectionString);
34-
})
35-
.AddTransient<AzureBlobStorage, AzureBlobStorage>(s =>
36-
{
37-
return new AzureBlobStorage(azureStorageConnectionString);
38-
})
39-
.AddTransient<ICertificateStore, AzureBlobCertificateStore>();
40-
}
41-
return serviceCollection
42-
.AddTransient<AcmeClient>()
43-
.AddTransient<DnsLookupService>()
44-
.AddSingleton(dnsProviderConfig.GetType(), dnsProviderConfig)
45-
.AddTransient<IDnsProvider, TDnsProvider>();
46-
}
44+
CertificateInfo = cert,
45+
Host = acmeDnsRequest.Host
46+
};
47+
}
48+
await azureWebAppService.Install(model);
4749

48-
public static IServiceCollection AddAzureAppService(this IServiceCollection serviceCollection, params AzureWebAppSettings[] settings)
49-
{
50-
if (settings == null || settings.Length == 0)
50+
logger.LogInformation("Removing expired certificates");
51+
var expired = azureWebAppService.RemoveExpired();
52+
logger.LogInformation("The following certificates was removed {Thumbprints}", string.Join(", ", expired.ToArray()));
53+
54+
}
55+
catch (Exception e)
5156
{
52-
throw new ArgumentNullException(nameof(settings));
57+
logger.LogError(e, "Failed");
58+
throw;
5359
}
54-
55-
return serviceCollection
56-
.AddSingleton(settings)
57-
.AddTransient<AzureWebAppService>();
5860
}
5961
}
6062
}

0 commit comments

Comments
 (0)