From e15be6091de8b85a58124f0cfb5900e7237d62e7 Mon Sep 17 00:00:00 2001 From: "Simon J.K. Pedersen" Date: Thu, 3 Oct 2019 23:31:16 +0200 Subject: [PATCH 1/4] WIP letsencrypt v2 api --- LetsEncrypt-SiteExtension/Web.config | 4 + .../CertificateManager.cs | 2 +- .../LetsEncrypt.Azure.Core.csproj | 10 ++ .../Services/ACMEService.cs | 7 +- .../Services/AcmeServiceV2.cs | 134 +++++++++++++++++ .../BaseDnsAuthorizationChallengeProvider.cs | 13 +- .../BaseHttpAuthorizationChallengeProvider.cs | 139 +++++++++++++----- ...obStorageAuthorizationChallengeProvider.cs | 13 +- .../IAuthorizationChallengeProvider.cs | 9 +- ...ileSystemAuthorizationChallengeProvider.cs | 11 +- ...ileSystemAuthorizationChallengeProvider.cs | 25 +--- .../packages.config | 3 + LetsEncrypt.SiteExtension.Test/App.config | 4 + .../BlobStorageAuthorizationChallengeTest.cs | 22 +-- .../CertificateManagerTest.cs | 24 +++ 15 files changed, 334 insertions(+), 86 deletions(-) create mode 100644 LetsEncrypt.SiteExtension.Core/Services/AcmeServiceV2.cs diff --git a/LetsEncrypt-SiteExtension/Web.config b/LetsEncrypt-SiteExtension/Web.config index 05f10f5..09dd77e 100644 --- a/LetsEncrypt-SiteExtension/Web.config +++ b/LetsEncrypt-SiteExtension/Web.config @@ -133,6 +133,10 @@ + + + + diff --git a/LetsEncrypt.SiteExtension.Core/CertificateManager.cs b/LetsEncrypt.SiteExtension.Core/CertificateManager.cs index cee4c53..7ec58ae 100644 --- a/LetsEncrypt.SiteExtension.Core/CertificateManager.cs +++ b/LetsEncrypt.SiteExtension.Core/CertificateManager.cs @@ -226,7 +226,7 @@ internal CertificateInstallModel RequestAndInstallInternal(IAcmeConfig config) internal async Task RequestInternalAsync(IAcmeConfig config) { - var service = new AcmeService(config, this.challengeProvider); + var service = new AcmeServiceV2(config, this.challengeProvider); var cert = await service.RequestCertificate(); var model = new CertificateInstallModel() diff --git a/LetsEncrypt.SiteExtension.Core/LetsEncrypt.Azure.Core.csproj b/LetsEncrypt.SiteExtension.Core/LetsEncrypt.Azure.Core.csproj index 464f7cd..820844b 100644 --- a/LetsEncrypt.SiteExtension.Core/LetsEncrypt.Azure.Core.csproj +++ b/LetsEncrypt.SiteExtension.Core/LetsEncrypt.Azure.Core.csproj @@ -69,6 +69,12 @@ MinimumRecommendedRules.ruleset + + ..\packages\Portable.BouncyCastle.1.8.1.4\lib\net40\BouncyCastle.Crypto.dll + + + ..\packages\Certes.2.3.3\lib\net45\Certes.dll + ..\packages\Microsoft.Azure.KeyVault.Core.3.0.1\lib\net452\Microsoft.Azure.KeyVault.Core.dll @@ -164,6 +170,9 @@ ..\packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net46\System.Security.Cryptography.X509Certificates.dll + + ..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll + @@ -192,6 +201,7 @@ + diff --git a/LetsEncrypt.SiteExtension.Core/Services/ACMEService.cs b/LetsEncrypt.SiteExtension.Core/Services/ACMEService.cs index 74d8c13..11f25f2 100644 --- a/LetsEncrypt.SiteExtension.Core/Services/ACMEService.cs +++ b/LetsEncrypt.SiteExtension.Core/Services/ACMEService.cs @@ -41,10 +41,11 @@ public async Task RequestCertificate() using (var client = Register(signer)) { IdnMapping idn = new IdnMapping(); - var auth = await this.authorizeChallengeProvider.Authorize(client, config.Hostnames.Select(s => idn.GetAscii(s)).ToList()); + authorizeChallengeProvider.RegisterClient(client); + var auth = await this.authorizeChallengeProvider.Authorize( config.Hostnames.Select(s => idn.GetAscii(s)).ToList()); //GetCertificate - if (auth.Status == "valid") + if (auth == "valid") { var pfxFilename = GetCertificate(client); @@ -63,7 +64,7 @@ public async Task RequestCertificate() }; } - throw new Exception("Unable to complete challenge with Lets Encrypt servers error was: " + auth.Status); + throw new Exception("Unable to complete challenge with Lets Encrypt servers error was: " + auth); } } diff --git a/LetsEncrypt.SiteExtension.Core/Services/AcmeServiceV2.cs b/LetsEncrypt.SiteExtension.Core/Services/AcmeServiceV2.cs new file mode 100644 index 0000000..b3be25f --- /dev/null +++ b/LetsEncrypt.SiteExtension.Core/Services/AcmeServiceV2.cs @@ -0,0 +1,134 @@ +using Certes; +using Certes.Acme; +using LetsEncrypt.Azure.Core.Models; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using System.Web.Hosting; + +namespace LetsEncrypt.Azure.Core.Services +{ + public class AcmeServiceV2 + { + private readonly Uri acmeenvironment; + private readonly string configPath; + private readonly IAcmeConfig config; + private readonly IAuthorizationChallengeProvider authorizeChallengeProvider; + + public AcmeServiceV2(IAcmeConfig config, IAuthorizationChallengeProvider authorizeChallengeProvider) + { + if (string.IsNullOrEmpty(config.BaseUri)) + { + this.acmeenvironment = (config.UseProduction ? WellKnownServers.LetsEncryptV2 : WellKnownServers.LetsEncryptStagingV2); + } + else + { + this.acmeenvironment = new Uri(config.BaseUri); + } + + this.configPath = ConfigPath(this.acmeenvironment.AbsoluteUri); + this.config = config; + this.authorizeChallengeProvider = authorizeChallengeProvider; + } + + public async Task RequestCertificate() + { + var context = await GetOrCreateAcmeContext(this.acmeenvironment, config.RegistrationEmail); + + IdnMapping idn = new IdnMapping(); + var domains = config.Hostnames.Select(s => idn.GetAscii(s)).ToList(); + var order = await context.NewOrder(domains); + + authorizeChallengeProvider.RegisterClient(order); + + + + var response = await authorizeChallengeProvider.Authorize(domains); + + if (response == "valid") + { + + var privateKey = KeyFactory.NewKey(KeyAlgorithm.RS256); + var cert = await order.Generate(new Certes.CsrInfo + { + CountryName = "", + State = "", + Locality = "", + Organization = "", + OrganizationUnit = "", + CommonName = config.Host, + }, privateKey); + + var certPem = cert.ToPem(); + + var pfxBuilder = cert.ToPfx(privateKey); + var pfx = pfxBuilder.Build(config.Host, config.PFXPassword); + + + return new CertificateInfo() + { + Certificate = new X509Certificate2(pfx, config.PFXPassword, X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable), + Name = $"{config.Host} {DateTime.Now}", + Password = config.PFXPassword, + PfxCertificate = pfx + }; + + } + throw new Exception("Unable to complete challenge with Lets Encrypt servers error was: " + response); + } + + private async Task GetOrCreateAcmeContext(Uri acmeDirectoryUri, string email) + { + if (!Directory.Exists(configPath)) + { + Directory.CreateDirectory(configPath); + } + + AcmeContext acme = null; + string filename = $"account{email}--{acmeDirectoryUri.Host}"; + var filePath = Path.Combine(configPath, filename); + if (!File.Exists(filePath) || string.IsNullOrEmpty(File.ReadAllText(filePath))) + { + + acme = new AcmeContext(acmeDirectoryUri); + var account = acme.NewAccount(email, true); + + // Save the account key for later use + var pemKey = acme.AccountKey.ToPem(); + File.WriteAllText(filePath, 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 secret = File.ReadAllText(filePath); + var accountKey = KeyFactory.FromPem(secret); + acme = new AcmeContext(acmeDirectoryUri, accountKey, new AcmeHttpClient(acmeDirectoryUri, new HttpClient())); + } + + return acme; + } + + static string CleanFileName(string fileName) => Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c.ToString(), string.Empty)); + + private static string ConfigPath(string baseUri) + { + if (Util.IsAzure) + { + return Path.Combine(Environment.ExpandEnvironmentVariables("%HOME%"), "siteextensions", "letsencrypt", "config", CleanFileName(baseUri)); + } + else + { + var folder = HostingEnvironment.MapPath("~/App_Data") ?? Path.Combine(Directory.GetCurrentDirectory(), "App_Data"); + + return Path.Combine(folder, "siteextensions", "letsencrypt", "config", CleanFileName(baseUri)); + } + } + } +} diff --git a/LetsEncrypt.SiteExtension.Core/Services/BaseDnsAuthorizationChallengeProvider.cs b/LetsEncrypt.SiteExtension.Core/Services/BaseDnsAuthorizationChallengeProvider.cs index 2103e62..8772125 100644 --- a/LetsEncrypt.SiteExtension.Core/Services/BaseDnsAuthorizationChallengeProvider.cs +++ b/LetsEncrypt.SiteExtension.Core/Services/BaseDnsAuthorizationChallengeProvider.cs @@ -16,7 +16,9 @@ namespace LetsEncrypt.Azure.Core.Services { public abstract class BaseDnsAuthorizationChallengeProvider : IAuthorizationChallengeProvider { - public async Task Authorize(AcmeClient client, List allDnsIdentifiers) + private AcmeClient client; + + public async Task Authorize(List allDnsIdentifiers) { List authStatus = new List(); @@ -74,14 +76,19 @@ public async Task Authorize(AcmeClient client, List { if (authState.Status != "valid") { - return authState; + return authState.Status; } } - return new AuthorizationState { Status = "valid" }; + return "valid"; } public abstract Task CleanupChallenge(DnsChallenge httpChallenge); public abstract Task PersistsChallenge(DnsChallenge httpChallenge); + + public void RegisterClient(object client) + { + this.client = client as AcmeClient; + } } } diff --git a/LetsEncrypt.SiteExtension.Core/Services/BaseHttpAuthorizationChallengeProvider.cs b/LetsEncrypt.SiteExtension.Core/Services/BaseHttpAuthorizationChallengeProvider.cs index 659d242..1539b04 100644 --- a/LetsEncrypt.SiteExtension.Core/Services/BaseHttpAuthorizationChallengeProvider.cs +++ b/LetsEncrypt.SiteExtension.Core/Services/BaseHttpAuthorizationChallengeProvider.cs @@ -1,5 +1,8 @@ using ACMESharp; using ACMESharp.ACME; +using Certes; +using Certes.Acme; +using Certes.Acme.Resource; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -33,6 +36,8 @@ public abstract class BaseHttpAuthorizationChallengeProvider : IAuthorizationCha "; + private ACMESharp.AcmeClient client; + private IOrderContext order; public virtual Task EnsureDirectory() { return Task.CompletedTask; @@ -43,11 +48,72 @@ public virtual Task EnsureWebConfig() return Task.CompletedTask; } - public abstract Task PersistsChallengeFile(HttpChallenge challenge); + public abstract Task PersistsChallengeFile(string path, string context); - public abstract Task CleanupChallengeFile(HttpChallenge challenge); + public abstract Task CleanupChallengeFile(string path); - public async Task Authorize(AcmeClient client, List allDnsIdentifiers) + public void RegisterClient(object client) + { + if (client is ACMESharp.AcmeClient) + this.client = client as ACMESharp.AcmeClient; + else if (client as IOrderContext != null) + this.order = client as IOrderContext; + else + throw new InvalidProgramException($"Unable to determine type of AcmeClient {client.GetType()}"); + } + + public async Task Authorize(List allDnsIdentifiers) + { + if (client != null) + { + return await this.AuthorizeOld(allDnsIdentifiers); + } + if (order != null) + { + return await this.AuthorizeNew(allDnsIdentifiers); + } + + throw new Exception("order and client are both null"); + } + + private async Task AuthorizeNew(List allDnsIdentifiers) + { + await EnsureDirectory(); + await EnsureWebConfig(); + + var auths = await order.Authorizations(); + foreach (var auth in auths) + { + + var authz = await auth.Http(); + var tokenRelativeUri = $".well-known/acme-challenge/{authz.Token}"; + await PersistsChallengeFile(tokenRelativeUri, authz.KeyAuthz); + var challengeUri = new Uri($"http://{allDnsIdentifiers.First()}/{tokenRelativeUri}"); + var retry = await ValidateChallengeFile(challengeUri); + if (retry == 0) + throw new Exception($"Unable to validate presence of http challenge at {challengeUri} ensure that it is browsable"); + + var response = await authz.Validate(); + retry = 10; + while ((response.Status == ChallengeStatus.Pending || response.Status == ChallengeStatus.Processing) && retry-- > 0) + { + Trace.TraceInformation($"Dns challenge response status {response.Status} more info at {response.Url.ToString()} retrying in 5 sec"); + await Task.Delay(5000); + response = await authz.Resource(); + } + + Console.WriteLine($" Authorization Result: {response.Status}"); + Trace.TraceInformation("Auth Result {0}", response.Status); + + if (response.Status != ChallengeStatus.Valid) + { + return response.Status.ToString(); + } + } + return "valid"; + } + + public async Task AuthorizeOld(List allDnsIdentifiers) { List authStatus = new List(); @@ -63,34 +129,11 @@ public async Task Authorize(AcmeClient client, List var challenge = client.DecodeChallenge(authzState, AcmeProtocol.CHALLENGE_TYPE_HTTP); var httpChallenge = challenge.Challenge as HttpChallenge; - await PersistsChallengeFile(httpChallenge); - - var answerUri = new Uri(httpChallenge.FileUrl); - Console.WriteLine($" Answer should now be browsable at {answerUri}"); - Trace.TraceInformation("Answer should now be browsable at {0}", answerUri); - + await PersistsChallengeFile(httpChallenge.FilePath, httpChallenge.FileContent); try { - var retry = 10; - var handler = new WebRequestHandler(); - handler.ServerCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true; - - var httpclient = new HttpClient(handler); - while (true) - { - - //Allow self-signed certs otherwise staging wont work - - - await Task.Delay(1000); - var x = await httpclient.GetAsync(answerUri); - Trace.TraceInformation("Checking status {0}", x.StatusCode); - if (x.StatusCode == HttpStatusCode.OK) - break; - if (retry-- == 0) - break; - Trace.TraceInformation("Retrying {0}", retry); - } + var answerUri = new Uri(httpChallenge.FileUrl); + int retry = await ValidateChallengeFile(answerUri); Console.WriteLine(" Submitting answer"); Trace.TraceInformation("Submitting answer"); authzState.Challenges = new AuthorizeChallenge[] { challenge }; @@ -103,7 +146,7 @@ public async Task Authorize(AcmeClient client, List retry++; Console.WriteLine(" Refreshing authorization attempt " + retry); Trace.TraceInformation("Refreshing authorization attempt " + retry); - await Task.Delay(2000*retry); // this has to be here to give ACME server a chance to think + await Task.Delay(2000 * retry); // this has to be here to give ACME server a chance to think var newAuthzState = client.RefreshIdentifierAuthorization(authzState); if (newAuthzState.Status != "pending") authzState = newAuthzState; @@ -128,7 +171,7 @@ public async Task Authorize(AcmeClient client, List { Console.WriteLine(" Deleting answer"); Trace.TraceInformation("Deleting answer"); - await CleanupChallengeFile(httpChallenge); + await CleanupChallengeFile(httpChallenge.FilePath); } } } @@ -136,10 +179,40 @@ public async Task Authorize(AcmeClient client, List { if (authState.Status != "valid") { - return authState; + return authState.Status; } } - return new AuthorizationState { Status = "valid" }; + return "valid"; + } + + private static async Task ValidateChallengeFile(Uri answerUri) + { + Console.WriteLine($" Answer should now be browsable at {answerUri}"); + Trace.TraceInformation("Answer should now be browsable at {0}", answerUri); + + + var retry = 10; + var handler = new WebRequestHandler(); + handler.ServerCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true; + + var httpclient = new HttpClient(handler); + while (true) + { + + //Allow self-signed certs otherwise staging wont work + + + await Task.Delay(1000); + var x = await httpclient.GetAsync(answerUri); + Trace.TraceInformation("Checking status {0}", x.StatusCode); + if (x.StatusCode == HttpStatusCode.OK) + break; + if (retry-- == 0) + break; + Trace.TraceInformation("Retrying {0}", retry); + } + + return retry; } } } diff --git a/LetsEncrypt.SiteExtension.Core/Services/BlobStorageAuthorizationChallengeProvider.cs b/LetsEncrypt.SiteExtension.Core/Services/BlobStorageAuthorizationChallengeProvider.cs index ceb5934..97fb397 100644 --- a/LetsEncrypt.SiteExtension.Core/Services/BlobStorageAuthorizationChallengeProvider.cs +++ b/LetsEncrypt.SiteExtension.Core/Services/BlobStorageAuthorizationChallengeProvider.cs @@ -20,22 +20,22 @@ public BlobStorageAuthorizationChallengeProvider(string storageConnectionString, this.containerName = string.IsNullOrEmpty(container) ? "letsencrypt-siteextension" : container; } - public override async Task CleanupChallengeFile(HttpChallenge challenge) + public override async Task CleanupChallengeFile(string filePath) { - var blob = await GetBlob(challenge); + var blob = await GetBlob(filePath); await blob.DeleteIfExistsAsync(); } - public override async Task PersistsChallengeFile(HttpChallenge challenge) + public override async Task PersistsChallengeFile(string filePath, string fileContent) { - CloudBlockBlob blob = await GetBlob(challenge); + CloudBlockBlob blob = await GetBlob(filePath); blob.Properties.ContentType = "text/plain"; - await blob.UploadTextAsync(challenge.FileContent); + await blob.UploadTextAsync(fileContent); await blob.SetPropertiesAsync(); } - private async Task GetBlob(HttpChallenge challenge) + private async Task GetBlob(string filePath) { var client = storageAccount.CreateCloudBlobClient(); var container = client.GetContainerReference(containerName); @@ -48,7 +48,6 @@ await container.SetPermissionsAsync(new BlobContainerPermissions() }); } // We need to strip off any leading '/' in the path - var filePath = challenge.FilePath; if (filePath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) filePath = filePath.Substring(1); var blob = container.GetBlockBlobReference(filePath); diff --git a/LetsEncrypt.SiteExtension.Core/Services/IAuthorizationChallengeProvider.cs b/LetsEncrypt.SiteExtension.Core/Services/IAuthorizationChallengeProvider.cs index 1d5b60c..7b08079 100644 --- a/LetsEncrypt.SiteExtension.Core/Services/IAuthorizationChallengeProvider.cs +++ b/LetsEncrypt.SiteExtension.Core/Services/IAuthorizationChallengeProvider.cs @@ -1,4 +1,5 @@ using ACMESharp; +using Certes; using LetsEncrypt.Azure.Core.Models; using System; using System.Collections.Generic; @@ -10,6 +11,12 @@ namespace LetsEncrypt.Azure.Core.Services { public interface IAuthorizationChallengeProvider { - Task Authorize(AcmeClient client, List dnsIdentifiers); + /// + /// Returns the authorization status from lets encrypt. + /// + /// + /// + Task Authorize(List dnsIdentifiers); + void RegisterClient(object client); } } diff --git a/LetsEncrypt.SiteExtension.Core/Services/KuduFileSystemAuthorizationChallengeProvider.cs b/LetsEncrypt.SiteExtension.Core/Services/KuduFileSystemAuthorizationChallengeProvider.cs index 033399b..cd4ada7 100644 --- a/LetsEncrypt.SiteExtension.Core/Services/KuduFileSystemAuthorizationChallengeProvider.cs +++ b/LetsEncrypt.SiteExtension.Core/Services/KuduFileSystemAuthorizationChallengeProvider.cs @@ -27,7 +27,7 @@ public KuduFileSystemAuthorizationChallengeProvider(IAzureWebAppEnvironment azur this.pathProvider = new PathProvider(azureEnvironment); } - public override Task CleanupChallengeFile(HttpChallenge challenge) + public override Task CleanupChallengeFile(string filePath) { return Task.CompletedTask; } @@ -43,10 +43,10 @@ public override async Task EnsureWebConfig() await WriteFile(dir + "/web.config", webConfig); } - public override async Task PersistsChallengeFile(HttpChallenge challenge) + public override async Task PersistsChallengeFile(string filePath, string fileContent) { - var answerPath = await GetAnswerPath(challenge); - await WriteFile(answerPath, challenge.FileContent); + var answerPath = await GetAnswerPath(filePath); + await WriteFile(answerPath, fileContent); } private async Task WriteFile(string answerPath, string content) @@ -60,11 +60,10 @@ private async Task WriteFile(string answerPath, string content) } } - private async Task GetAnswerPath(HttpChallenge httpChallenge) + private async Task GetAnswerPath(string filePath) { var root = await this.pathProvider.WebRootPath(true); // We need to strip off any leading '/' in the path - var filePath = httpChallenge.FilePath; if (filePath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) filePath = filePath.Substring(1); var answerPath = root + "/" +filePath; diff --git a/LetsEncrypt.SiteExtension.Core/Services/LocalFileSystemAuthorizationChallengeProvider.cs b/LetsEncrypt.SiteExtension.Core/Services/LocalFileSystemAuthorizationChallengeProvider.cs index 216c74c..ef98f2d 100644 --- a/LetsEncrypt.SiteExtension.Core/Services/LocalFileSystemAuthorizationChallengeProvider.cs +++ b/LetsEncrypt.SiteExtension.Core/Services/LocalFileSystemAuthorizationChallengeProvider.cs @@ -1,17 +1,7 @@ -using ACMESharp; -using ACMESharp.ACME; -using LetsEncrypt.Azure.Core.Models; -using Newtonsoft.Json; +using LetsEncrypt.Azure.Core.Models; using System; -using System.Collections.Generic; -using System.Configuration; using System.Diagnostics; using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading; using System.Threading.Tasks; namespace LetsEncrypt.Azure.Core.Services @@ -59,30 +49,29 @@ public override async Task EnsureWebConfig() } } - public override async Task PersistsChallengeFile(HttpChallenge httpChallenge) + public override async Task PersistsChallengeFile(string filePath, string fileContent) { - string answerPath = await GetAnswerPath(httpChallenge); + string answerPath = await GetAnswerPath(filePath); Console.WriteLine($" Writing challenge answer to {answerPath}"); Trace.TraceInformation("Writing challenge answer to {0}", answerPath); - File.WriteAllText(answerPath, httpChallenge.FileContent); + File.WriteAllText(answerPath, fileContent); } - private async Task GetAnswerPath(HttpChallenge httpChallenge) + private async Task GetAnswerPath(string filePath) { var rootDir = await this.pathProvider.WebRootPath(false); // We need to strip off any leading '/' in the path - var filePath = httpChallenge.FilePath; if (filePath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) filePath = filePath.Substring(1); var answerPath = Environment.ExpandEnvironmentVariables(Path.Combine(rootDir, filePath)); return answerPath; } - public override async Task CleanupChallengeFile(HttpChallenge challenge) + public override async Task CleanupChallengeFile(string filePath) { - File.Delete(await GetAnswerPath(challenge)); + File.Delete(await GetAnswerPath(filePath)); } } } diff --git a/LetsEncrypt.SiteExtension.Core/packages.config b/LetsEncrypt.SiteExtension.Core/packages.config index 8e63676..440d0c5 100644 --- a/LetsEncrypt.SiteExtension.Core/packages.config +++ b/LetsEncrypt.SiteExtension.Core/packages.config @@ -1,5 +1,6 @@  + @@ -14,6 +15,7 @@ + @@ -55,6 +57,7 @@ + \ No newline at end of file diff --git a/LetsEncrypt.SiteExtension.Test/App.config b/LetsEncrypt.SiteExtension.Test/App.config index 642ed8d..2dff46b 100644 --- a/LetsEncrypt.SiteExtension.Test/App.config +++ b/LetsEncrypt.SiteExtension.Test/App.config @@ -70,6 +70,10 @@ + + + + diff --git a/LetsEncrypt.SiteExtension.Test/BlobStorageAuthorizationChallengeTest.cs b/LetsEncrypt.SiteExtension.Test/BlobStorageAuthorizationChallengeTest.cs index 99e6fa9..f74ae48 100644 --- a/LetsEncrypt.SiteExtension.Test/BlobStorageAuthorizationChallengeTest.cs +++ b/LetsEncrypt.SiteExtension.Test/BlobStorageAuthorizationChallengeTest.cs @@ -14,18 +14,16 @@ namespace LetsEncrypt.SiteExtension.Test [TestClass] public class BlobStorageAuthorizationChallengeTest { + const string FileContent = "test"; + const string FilePath = "/.well-known/acme-challenge/aBAasda234"; [TestMethod] public async Task TestPersistDelete() { var testObj = new BlobStorageAuthorizationChallengeProvider(ConfigurationManager.AppSettings[AppSettingsAuthConfig.authorizationChallengeBlobStorageAccount]); - ACMESharp.ACME.HttpChallenge challenge = new ACMESharp.ACME.HttpChallenge("http", new HttpChallengeAnswer()) - { - FileContent = "test", - FilePath = "/.well-known/acme-challenge/aBAasda234" - }; - await testObj.PersistsChallengeFile(challenge); - await testObj.CleanupChallengeFile(challenge); + + await testObj.PersistsChallengeFile(FilePath, FileContent); + await testObj.CleanupChallengeFile(FilePath); } [TestMethod] @@ -33,13 +31,9 @@ public async Task TestWebPersistDelete() { var testObj = new BlobStorageAuthorizationChallengeProvider(ConfigurationManager.AppSettings[AppSettingsAuthConfig.authorizationChallengeBlobStorageAccount], "$web"); - ACMESharp.ACME.HttpChallenge challenge = new ACMESharp.ACME.HttpChallenge("http", new HttpChallengeAnswer()) - { - FileContent = "test", - FilePath = "/.well-known/acme-challenge/aBAasda234" - }; - await testObj.PersistsChallengeFile(challenge); - await testObj.CleanupChallengeFile(challenge); + + await testObj.PersistsChallengeFile(FilePath,FileContent); + await testObj.CleanupChallengeFile(FilePath); } } } diff --git a/LetsEncrypt.SiteExtension.Test/CertificateManagerTest.cs b/LetsEncrypt.SiteExtension.Test/CertificateManagerTest.cs index f05af36..4be9b08 100644 --- a/LetsEncrypt.SiteExtension.Test/CertificateManagerTest.cs +++ b/LetsEncrypt.SiteExtension.Test/CertificateManagerTest.cs @@ -57,6 +57,30 @@ public async Task RenewCertificateDnsChallengeTest() Assert.AreNotEqual(0, result.Count()); } + [TestCategory("Integration")] + [TestMethod] + public async Task AddCertificateHttpChallengeTest() + { + var config = new AppSettingsAuthConfig(); + + + var dnsEnvironment = new AzureDnsEnvironment(config.Tenant, new Guid("14fe4c66-c75a-4323-881b-ea53c1d86a9d"), config.ClientId, config.ClientSecret, "dns", "ai4bots.com", "letsencrypt"); + var mgr = new CertificateManager(config, new AcmeConfig() + { + Host = "letsencrypt.sjkp.dk", + PFXPassword = "Simon123", + RegistrationEmail = "mail@sjkp.dk", + RSAKeyLength = 2048 + }, new WebAppCertificateService(config, new CertificateServiceSettings() + { + UseIPBasedSSL = config.UseIPBasedSSL + }), new KuduFileSystemAuthorizationChallengeProvider(config, new AuthorizationChallengeProviderConfig())); + var result = await mgr.AddCertificate(); + + Assert.IsNotNull(result); + ValidateCertificate(new[] { result }, "https://letsencrypt.sjkp.dk"); + } + [TestCategory("Integration")] [TestMethod] public async Task AddCertificateDnsChallengeTest() From f910a1dea5016d0d18c63e1235a98b2105bf9966 Mon Sep 17 00:00:00 2001 From: "Simon J.K. Pedersen" Date: Sun, 6 Oct 2019 09:46:39 +0200 Subject: [PATCH 2/4] Clean up of all dns related functionality. Removal of ACMESharp. --- LetsEncrypt-SiteExtension.sln | 32 -- .../Controllers/Api/CertificateController.cs | 64 ---- .../Controllers/HomeController.cs | 7 +- .../CertificateManager.cs | 31 +- .../LetsEncrypt.Azure.Core.csproj | 18 +- .../Services/ACMEService.cs | 358 ++++-------------- .../Services/AcmeServiceV2.cs | 134 ------- .../AzureDnsAuthorizationChallengeProvider.cs | 82 ---- .../BaseDnsAuthorizationChallengeProvider.cs | 94 ----- .../BaseHttpAuthorizationChallengeProvider.cs | 111 +----- ...obStorageAuthorizationChallengeProvider.cs | 4 - .../IAuthorizationChallengeProvider.cs | 11 +- ...ileSystemAuthorizationChallengeProvider.cs | 6 - ...reDnsAuthorizationChallengeProviderTest.cs | 47 --- .../BlobStorageAuthorizationChallengeTest.cs | 7 +- .../CertificateManagerTest.cs | 61 +-- .../LetsEncrypt.SiteExtension.Test.csproj | 9 - 17 files changed, 89 insertions(+), 987 deletions(-) delete mode 100644 LetsEncrypt.SiteExtension.Core/Services/AcmeServiceV2.cs delete mode 100644 LetsEncrypt.SiteExtension.Core/Services/AzureDnsAuthorizationChallengeProvider.cs delete mode 100644 LetsEncrypt.SiteExtension.Core/Services/BaseDnsAuthorizationChallengeProvider.cs delete mode 100644 LetsEncrypt.SiteExtension.Test/AzureDnsAuthorizationChallengeProviderTest.cs diff --git a/LetsEncrypt-SiteExtension.sln b/LetsEncrypt-SiteExtension.sln index e5deb4e..54b897d 100644 --- a/LetsEncrypt-SiteExtension.sln +++ b/LetsEncrypt-SiteExtension.sln @@ -30,10 +30,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ACMESharp", "ACMESharp", "{E4B09348-2E98-4A58-8D5A-B55231D6A2E3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ACMESharp", "ACMESharp\ACMESharp\ACMESharp\ACMESharp.csproj", "{D551234B-0A8D-4DEE-8178-A81998DF0EDB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ACMESharp.PKI.Providers.BouncyCastle", "ACMESharp\ACMESharp\ACMESharp.PKI.Providers.BouncyCastle\ACMESharp.PKI.Providers.BouncyCastle.csproj", "{473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -104,38 +100,10 @@ Global {284F0226-F481-4C10-A408-4146FDBB71CC}.Release|x64.Build.0 = Release|x64 {284F0226-F481-4C10-A408-4146FDBB71CC}.Release|x86.ActiveCfg = Release|x86 {284F0226-F481-4C10-A408-4146FDBB71CC}.Release|x86.Build.0 = Release|x86 - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Debug|x64.ActiveCfg = Debug|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Debug|x64.Build.0 = Debug|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Debug|x86.ActiveCfg = Debug|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Debug|x86.Build.0 = Debug|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Release|Any CPU.Build.0 = Release|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Release|x64.ActiveCfg = Release|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Release|x64.Build.0 = Release|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Release|x86.ActiveCfg = Release|Any CPU - {D551234B-0A8D-4DEE-8178-A81998DF0EDB}.Release|x86.Build.0 = Release|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Debug|x64.ActiveCfg = Debug|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Debug|x64.Build.0 = Debug|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Debug|x86.ActiveCfg = Debug|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Debug|x86.Build.0 = Debug|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Release|Any CPU.Build.0 = Release|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Release|x64.ActiveCfg = Release|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Release|x64.Build.0 = Release|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Release|x86.ActiveCfg = Release|Any CPU - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {D551234B-0A8D-4DEE-8178-A81998DF0EDB} = {E4B09348-2E98-4A58-8D5A-B55231D6A2E3} - {473BFF7D-C7F0-471D-B7A3-19AD9ADFDBA9} = {E4B09348-2E98-4A58-8D5A-B55231D6A2E3} - EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CE278D6B-F8FA-4F40-A67B-A8367F40FCA4} EndGlobalSection diff --git a/LetsEncrypt-SiteExtension/Controllers/Api/CertificateController.cs b/LetsEncrypt-SiteExtension/Controllers/Api/CertificateController.cs index 0da8d02..1eb05d7 100644 --- a/LetsEncrypt-SiteExtension/Controllers/Api/CertificateController.cs +++ b/LetsEncrypt-SiteExtension/Controllers/Api/CertificateController.cs @@ -83,69 +83,5 @@ public async Task GenerateAndInstallBlob(HttpKuduInstallModel return Ok(await mgr.AddCertificate()); } - - /// - /// Requests a Let's Encrypt certificate using the DNS challenge, using Azure DNS. - /// - /// - /// - /// - [HttpPost] - [Route("api/certificates/challengeprovider/dns/azure")] - [ResponseType(typeof(CertificateInstallModel))] - public async Task Generate(DnsAzureModel model, [FromUri(Name = "api-version")]string apiversion = null) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var res = await CertificateManager.RequestDnsChallengeCertificate(model.AzureDnsEnvironment, model.AcmeConfig); - - return Ok(res); - } - - /// - /// Requests a Let's Encrypt certificate using the DNS challenge, using Azure DNS. - /// - /// - /// - /// - [HttpPost] - [Route("api/certificates/challengeprovider/dns-v2/azure")] - [ResponseType(typeof(CertificateInstallModel))] - public async Task Generate_v2(DnsAzureModel model, [FromUri(Name = "api-version")]string apiversion = null) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var res = await CertificateManager.RequestDnsChallengeCertificate(model.AzureDnsEnvironment, model.AcmeConfig); - - return Ok(res); - } - - /// - /// Requests a Let's Encrypt certificate using the DNS challenge, using Azure DNS. The - /// certificate is installed to the web app. - /// - /// - /// - /// - [HttpPost] - [Route("api/certificates/challengeprovider/dns/azure/certificateinstall/azurewebapp")] - [ResponseType(typeof(CertificateInstallModel))] - public async Task GenerateAndInstall(DnsAzureInstallModel model, [FromUri(Name = "api-version")]string apiversion = null) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var mgr = CertificateManager.CreateAzureDnsWebAppCertificateManager(model.AzureWebAppEnvironment, model.AcmeConfig, model.CertificateSettings, model); - - return Ok(await mgr.AddCertificate()); - } } } \ No newline at end of file diff --git a/LetsEncrypt-SiteExtension/Controllers/HomeController.cs b/LetsEncrypt-SiteExtension/Controllers/HomeController.cs index 38be016..70097e8 100644 --- a/LetsEncrypt-SiteExtension/Controllers/HomeController.cs +++ b/LetsEncrypt-SiteExtension/Controllers/HomeController.cs @@ -230,11 +230,10 @@ public async Task Install(RequestAndInstallModel model) Name = "email", Value = model.Email }); - var baseUri = model.UseStaging == false ? "https://acme-v01.api.letsencrypt.org/" : "https://acme-staging.api.letsencrypt.org/"; s.Add(new SettingEntry() { - Name = "baseUri", - Value = baseUri + Name = "useStaging", + Value = model.UseStaging.ToString() }); SettingsStore.Instance.Save(s); var settings = new AppSettingsAuthConfig(); @@ -242,7 +241,7 @@ public async Task Install(RequestAndInstallModel model) { RegistrationEmail = model.Email, Host = model.Hostnames.First(), - BaseUri = baseUri, + UseProduction = !model.UseStaging, AlternateNames = model.Hostnames.Skip(1).ToList(), PFXPassword = settings.PFXPassword, RSAKeyLength = settings.RSAKeyLength, diff --git a/LetsEncrypt.SiteExtension.Core/CertificateManager.cs b/LetsEncrypt.SiteExtension.Core/CertificateManager.cs index 7ec58ae..b236924 100644 --- a/LetsEncrypt.SiteExtension.Core/CertificateManager.cs +++ b/LetsEncrypt.SiteExtension.Core/CertificateManager.cs @@ -93,32 +93,6 @@ public static CertificateManager CreateKuduWebAppCertificateManager(IAzureWebApp return new CertificateManager(settings, acmeConfig, new WebAppCertificateService(settings, certSettings), new KuduFileSystemAuthorizationChallengeProvider(settings, authProviderConfig)); } - /// - /// Returns a configured to use DNS Challenge, placing the challenge record in Azure DNS, - /// and assigning the obtained certificate directly to the web app service. - /// - /// - /// - /// - /// - /// - public static CertificateManager CreateAzureDnsWebAppCertificateManager(IAzureWebAppEnvironment settings, IAcmeConfig acmeConfig, IWebAppCertificateSettings certSettings, IAzureDnsEnvironment dnsEnvironment) - { - return new CertificateManager(settings, acmeConfig, new WebAppCertificateService(settings, certSettings), new AzureDnsAuthorizationChallengeProvider(dnsEnvironment)); - } - - /// - /// Request a certificate from lets encrypt using the DNS challenge, placing the challenge record in Azure DNS. - /// The certifiacte is not assigned, but just returned. - /// - /// - /// - /// - public static async Task RequestDnsChallengeCertificate(IAzureDnsEnvironment azureDnsEnvironment, IAcmeConfig acmeConfig) - { - return await new CertificateManager(null, acmeConfig, null, new AzureDnsAuthorizationChallengeProvider(azureDnsEnvironment)).RequestInternalAsync(acmeConfig); - } - /// /// Used for automatic installation of letsencrypt certificate @@ -186,7 +160,8 @@ public async Task> RenewCertificate(bool skipInsta RegistrationEmail = this.acmeConfig.RegistrationEmail ?? ss.FirstOrDefault(s => s.Name == "email").Value, Host = sslStates.First().Name, - BaseUri = this.acmeConfig.BaseUri ?? ss.FirstOrDefault(s => s.Name == "baseUri").Value, + BaseUri = this.acmeConfig.BaseUri, + UseProduction = !bool.Parse(ss.FirstOrDefault(s => s.Name == "useStaging")?.Value ?? false.ToString()), AlternateNames = sslStates.Skip(1).Select(s => s.Name).ToList(), PFXPassword = this.acmeConfig.PFXPassword, RSAKeyLength = this.acmeConfig.RSAKeyLength @@ -226,7 +201,7 @@ internal CertificateInstallModel RequestAndInstallInternal(IAcmeConfig config) internal async Task RequestInternalAsync(IAcmeConfig config) { - var service = new AcmeServiceV2(config, this.challengeProvider); + var service = new AcmeService(config, this.challengeProvider); var cert = await service.RequestCertificate(); var model = new CertificateInstallModel() diff --git a/LetsEncrypt.SiteExtension.Core/LetsEncrypt.Azure.Core.csproj b/LetsEncrypt.SiteExtension.Core/LetsEncrypt.Azure.Core.csproj index 820844b..c6d5877 100644 --- a/LetsEncrypt.SiteExtension.Core/LetsEncrypt.Azure.Core.csproj +++ b/LetsEncrypt.SiteExtension.Core/LetsEncrypt.Azure.Core.csproj @@ -69,9 +69,6 @@ MinimumRecommendedRules.ruleset - - ..\packages\Portable.BouncyCastle.1.8.1.4\lib\net40\BouncyCastle.Crypto.dll - ..\packages\Certes.2.3.3\lib\net45\Certes.dll @@ -201,12 +198,9 @@ - - + - - @@ -232,16 +226,6 @@ Designer - - - {473bff7d-c7f0-471d-b7a3-19ad9adfdba9} - ACMESharp.PKI.Providers.BouncyCastle - - - {d551234b-0a8d-4dee-8178-a81998df0edb} - ACMESharp - -