public async Task Run(IAcmeDnsRequest acmeDnsRequest, int renewXNumberOfDaysBeforeExpiration) { try { CertificateInstallModel model = null; string hostsPlusSeparated = AcmeClient.GetHostsPlusSeparated(acmeDnsRequest.Hosts); var certname = $"{hostsPlusSeparated}-{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() { CertificateInfo = cert, Hosts = acmeDnsRequest.Hosts }; } await certificateConsumer.Install(model); logger.LogInformation("Removing expired certificates"); var expired = await certificateConsumer.CleanUp(); logger.LogInformation("The following certificates was removed {Thumbprints}", string.Join(", ", expired.ToArray())); } catch (Exception e) { logger.LogError(e, "Failed"); throw; } }
/// <summary> /// 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. /// </summary> /// <param name="azureDnsEnvironment"></param> /// <param name="acmeConfig"></param> /// <returns></returns> public async Task <CertificateInstallModel> RequestDnsChallengeCertificate(IAcmeDnsRequest acmeConfig) { logger.LogInformation("Starting request DNS Challenge certificate for {AcmeEnvironment} and {Email}", acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.RegistrationEmail); var acmeContext = await GetOrCreateAcmeContext(acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.RegistrationEmail); var idn = new IdnMapping(); var order = await acmeContext.NewOrder(new[] { "*." + idn.GetAscii(acmeConfig.Host.Substring(2)) }); var a = await order.Authorizations(); var authz = a.First(); var challenge = await authz.Dns(); var dnsTxt = acmeContext.AccountKey.DnsTxt(challenge.Token); logger.LogInformation("Got DNS challenge token {Token}", dnsTxt); ///add dns entry await this.dnsProvider.PersistChallenge(String.Concat("_acme-challenge.", DnsLookupService.GetNoneWildcardSubdomain(acmeConfig.Host)), dnsTxt); if (!(await this.dnsLookupService.Exists(acmeConfig.Host, dnsTxt, this.dnsProvider.MinimumTtl))) { throw new TimeoutException($"Unable to validate that _acme-challenge was stored in txt _acme-challenge record after {this.dnsProvider.MinimumTtl} seconds"); } Challenge chalResp = await challenge.Validate(); while (chalResp.Status == ChallengeStatus.Pending || chalResp.Status == ChallengeStatus.Processing) { logger.LogInformation("Dns challenge response status {ChallengeStatus} more info at {ChallengeStatusUrl} retrying in 5 sec", chalResp.Status, chalResp.Url.ToString()); await Task.Delay(5000); chalResp = await challenge.Resource(); } logger.LogInformation("Finished validating dns challenge token, response was {ChallengeStatus} more info at {ChallengeStatusUrl}", chalResp.Status, chalResp.Url); var privateKey = await GetOrCreateKey(acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.Host); var cert = await order.Generate(new Certes.CsrInfo { CountryName = acmeConfig.CsrInfo.CountryName, State = acmeConfig.CsrInfo.State, Locality = acmeConfig.CsrInfo.Locality, Organization = acmeConfig.CsrInfo.Organization, OrganizationUnit = acmeConfig.CsrInfo.OrganizationUnit, CommonName = acmeConfig.CsrInfo.CommonName, }, privateKey); var certPem = cert.ToPem(); var pfxBuilder = cert.ToPfx(privateKey); var pfx = pfxBuilder.Build(acmeConfig.Host, acmeConfig.PFXPassword); await this.dnsProvider.Cleanup(dnsTxt); return(new CertificateInstallModel() { CertificateInfo = new CertificateInfo() { Certificate = new X509Certificate2(pfx, acmeConfig.PFXPassword, X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable), Name = $"{acmeConfig.Host} {DateTime.Now}", Password = acmeConfig.PFXPassword, PfxCertificate = pfx }, Host = acmeConfig.Host }); }
/// <summary> /// 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. /// </summary> /// <param name="azureDnsEnvironment"></param> /// <param name="acmeConfig"></param> /// <returns></returns> public async Task <CertificateInstallModel> RequestDnsChallengeCertificate(IAcmeDnsRequest acmeConfig) { logger.LogInformation("Starting request DNS Challenge certificate for {AcmeEnvironment} and {Email}", acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.RegistrationEmail); var acmeContext = await GetOrCreateAcmeContext(acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.RegistrationEmail); var idn = new IdnMapping(); var orderHosts = (from host in acmeConfig.Hosts let asciiHost = idn.GetAscii(host) let asciiDomain = asciiHost.StartsWith("*.") ? asciiHost.Substring(2) : asciiHost select(Host: asciiHost, Domain: asciiDomain)) .ToImmutableArray(); var order = await acmeContext.NewOrder(orderHosts.Select(h => h.Host).ToImmutableArray()); var authorizations = (await order.Authorizations()).ToImmutableArray(); var tasks = new List <Task>(authorizations.Length); var dnsTxts = new List <string>(authorizations.Length); // TODO: Consider parallelizing for (var i = 0; i < authorizations.Length; i++) /*tasks.Add(Task.Factory.StartNew(async state =>*/ { //var (authorization, host) = (Tuple<IAuthorizationContext, string>)state; var(authorization, zoneName) = (authorizations[i], orderHosts[i].Domain); var challenge = await authorization.Dns(); var dnsTxt = acmeContext.AccountKey.DnsTxt(challenge.Token); logger.LogInformation("Got DNS challenge token {Token}", dnsTxt); ///add dns entry await this.dnsProvider.PersistChallenge(zoneName, "_acme-challenge", dnsTxt); await Task.Delay(500); if (!(await this.dnsLookupService.Exists(zoneName, dnsTxt, this.dnsProvider.MinimumTtl))) { throw new TimeoutException($"Unable to validate that _acme-challenge was stored in txt _acme-challenge record after {this.dnsProvider.MinimumTtl} seconds"); } Challenge chalResp = await challenge.Validate(); while (chalResp.Status == ChallengeStatus.Pending || chalResp.Status == ChallengeStatus.Processing) { logger.LogInformation("Dns challenge response status {ChallengeStatus} more info at {ChallengeStatusUrl} retrying in 5 sec", chalResp.Status, chalResp.Url.ToString()); await Task.Delay(5000); chalResp = await challenge.Resource(); } logger.LogInformation("Finished validating dns challenge token, response was {ChallengeStatus} more info at {ChallengeStatusUrl}", chalResp.Status, chalResp.Url); }/*, Tuple.Create(authorizations[i], orderHosts[i].BaseHost), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default).Unwrap()); * await Task.WhenAll(tasks); * tasks.Clear()*/ ; var privateKey = await GetOrCreateKey(acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.Hosts); var cert = await order.Generate(new Certes.CsrInfo { CountryName = acmeConfig.CsrInfo.CountryName, State = acmeConfig.CsrInfo.State, Locality = acmeConfig.CsrInfo.Locality, Organization = acmeConfig.CsrInfo.Organization, OrganizationUnit = acmeConfig.CsrInfo.OrganizationUnit, CommonName = acmeConfig.CsrInfo.CommonName, }, privateKey); var certPem = cert.ToPem(); string hostsPlusSeparated = GetHostsPlusSeparated(acmeConfig.Hosts); var pfxBuilder = cert.ToPfx(privateKey); var pfx = pfxBuilder.Build(hostsPlusSeparated, acmeConfig.PFXPassword); for (var i = 0; i < dnsTxts.Count; i++) { tasks.Add(this.dnsProvider.Cleanup(orderHosts[i].Domain, dnsTxts[i])); } await Task.WhenAll(tasks); tasks.Clear(); return(new CertificateInstallModel() { CertificateInfo = new CertificateInfo() { #pragma warning disable DF0100 // Marks return values that hides the IDisposable implementation of return value. Certificate = new X509Certificate2(pfx, acmeConfig.PFXPassword, X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable), #pragma warning restore DF0100 // Marks return values that hides the IDisposable implementation of return value. Name = $"{acmeConfig.Hosts} {DateTime.Now}", Password = acmeConfig.PFXPassword, PfxCertificate = pfx }, Hosts = acmeConfig.Hosts }); }