/// <summary> /// Runs the LetsEncrypt challenge and verifies that it was completed successfully. /// </summary> /// <param name="options"></param> /// <param name="cfg"></param> /// <param name="cancellationToken"></param> /// <returns>Returns the context, ready to generate certificate from.</returns> private async Task <IOrderContext> ValidateOrderAsync( IAcmeOptions options, CertificateRenewalOptions cfg, CancellationToken cancellationToken) { var authenticationContext = await _authenticationService.AuthenticateAsync(options, cancellationToken); var order = await authenticationContext.AcmeContext.NewOrder(cfg.HostNames); var challenge = await _renewalOptionParser.ParseChallengeResponderAsync(cfg, cancellationToken); var challengeContexts = await challenge.InitiateChallengesAsync(order, cancellationToken); if (challengeContexts.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(challengeContexts)); } try { // validate domain ownership await ValidateDomainOwnershipAsync(challengeContexts, cancellationToken); } finally { await challenge.CleanupAsync(challengeContexts, cancellationToken); } return(order); }
public async Task <RenewalResult> RenewCertificateAsync( IAcmeOptions options, CertificateRenewalOptions cfg, CancellationToken cancellationToken) { if (options == null) { throw new ArgumentNullException(nameof(options)); } if (cfg == null) { throw new ArgumentNullException(nameof(cfg)); } var hostNames = string.Join(";", cfg.HostNames); _logger.LogInformation($"Working on certificate for: {hostNames}"); // 1. check if valid cert exists var cert = await GetExistingCertificateAsync(options, cfg, cancellationToken); bool updateResource = false; if (cert == null) { // 2. run Let's Encrypt challenge as cert either doesn't exist or is expired _logger.LogInformation($"Issuing a new certificate for {hostNames}"); var order = await ValidateOrderAsync(options, cfg, cancellationToken); // 3. save certificate cert = await GenerateAndStoreCertificateAsync(order, cfg, cancellationToken); updateResource = true; } var resource = _renewalOptionParser.ParseTargetResource(cfg); // if no update is required still check with target resource // and only skip if latest cert is already used // this helps if cert issuance worked but resource updated failed // suggestion from https://github.com/MarcStan/lets-encrypt-azure/issues/6 if (!updateResource && (!resource.SupportsCertificateCheck || await resource.IsUsingCertificateAsync(cert, cancellationToken))) { _logger.LogWarning(resource.SupportsCertificateCheck ? $"Resource {resource.Name} ({resource.Type}) is already using latest certificate. Skipping resource update!" : $"Cannot check resource {resource.Name} ({resource.Type}). Assuming it is already using latest certificate. Skipping resource update!"); return(RenewalResult.NoChange); } // 5. update Azure resource _logger.LogInformation($"Updating {resource.Name} ({resource.Type}) with certificates for {hostNames}"); await resource.UpdateAsync(cert, cancellationToken); return(RenewalResult.Success); }
/// <summary> /// Creates a valid certificate from the order and uploads it to the store. /// </summary> /// <param name="order"></param> /// <param name="cfg"></param> /// <param name="cancellationToken"></param> /// <returns>Returns metadata about the certificate.</returns> private async Task <ICertificate> GenerateAndStoreCertificateAsync( IOrderContext order, CertificateRenewalOptions cfg, CancellationToken cancellationToken) { var store = _renewalOptionParser.ParseCertificateStore(cfg); _logger.LogInformation($"Storing certificate in {store.Type} {store.Name}"); // request certificate (byte[] pfxBytes, string password) = await _certificateBuilder.BuildCertificateAsync(order, cfg, cancellationToken); return(await store.UploadAsync(pfxBytes, password, cfg.HostNames, cancellationToken)); }
public async Task <RenewalResult> RenewCertificateAsync( IAcmeOptions options, CertificateRenewalOptions cfg, CancellationToken cancellationToken) { if (options == null) { throw new ArgumentNullException(nameof(options)); } if (cfg == null) { throw new ArgumentNullException(nameof(cfg)); } var hostNames = string.Join(";", cfg.HostNames); _log.LogInformation($"Working on certificate for: {hostNames}"); // 1. skip if not outdated yet var cert = await GetExistingCertificateAsync(options, cfg, cancellationToken); if (cert != null) { // can usually skip rest, except if override is used if (!cfg.Overrides.UpdateResource) { return(RenewalResult.NoChange); } _log.LogWarning($"Override '{nameof(cfg.Overrides.UpdateResource)}' is enabled. Forcing resource update."); } else { // 2. run Let's Encrypt challenge as cert either doesn't exist or is expired _log.LogInformation($"Issuing a new certificate for {hostNames}"); var order = await ValidateOrderAsync(options, cfg, cancellationToken); // 3. save certificate cert = await GenerateAndStoreCertificateAsync(order, cfg, cancellationToken); } // 4. update Azure resource var resource = _renewalOptionParser.ParseTargetResource(cfg); _log.LogInformation($"Updating {resource.Name} ({resource.Type}) with certificates for {hostNames}"); await resource.UpdateAsync(cert, cancellationToken); return(RenewalResult.Success); }
/// <summary> /// Checks if we have to renew the specific certificate just yet. /// </summary> /// <param name="options"></param> /// <param name="cfg"></param> /// <param name="cancellationToken"></param> /// <returns>The cert if it is still valid according to rules in config. False otherwise.</returns> private async Task <ICertificate> GetExistingCertificateAsync( IAcmeOptions options, CertificateRenewalOptions cfg, CancellationToken cancellationToken) { if (cfg.Overrides.NewCertificate) { // ignore existing certificate _log.LogWarning($"Override '{nameof(cfg.Overrides.NewCertificate)}' is enabled, forcing certificate renewal."); return(null); } var certStore = _renewalOptionParser.ParseCertificateStore(cfg); // determine if renewal is needed based on existing cert var existingCert = await certStore.GetCertificateAsync(cancellationToken); // check if cert exists and that it is valid right now if (existingCert != null) { // handle cases of manually uploaded certificates if (!existingCert.Expires.HasValue) { throw new NotSupportedException($"Missing expiration value on certificate {existingCert.Name} (provider: {certStore.Type}). " + "Must be set to expiration date of the certificate."); } var now = DateTime.UtcNow; // must be valid now and some day in the future based on config expiration rule var isValidAlready = !existingCert.NotBefore.HasValue || existingCert.NotBefore.Value < now; var isStillValid = existingCert.Expires.Value.Date.AddDays(-options.RenewXDaysBeforeExpiry) >= now; if (isValidAlready && isStillValid) { _log.LogInformation($"Certificate {existingCert.Name} (from source: {certStore.Name}) is still valid until {existingCert.Expires.Value}. " + $"Will be renewed in {(int)(existingCert.Expires.Value - now).TotalDays - options.RenewXDaysBeforeExpiry} days. Skipping renewal."); return(existingCert); } var reason = !isValidAlready ? $"certificate won't be valid until {existingCert.NotBefore}" : $"renewal is demanded {options.RenewXDaysBeforeExpiry} days before expiry and it is currently {(int)(existingCert.Expires.Value - now).TotalDays} days before expiry"; _log.LogInformation($"Certificate {existingCert.Name} (from source: {certStore.Name}) is no longer up to date ({reason})."); } // either no cert or expired return(null); }
public async Task <(byte[] pfxBytes, string password)> BuildCertificateAsync( IOrderContext order, CertificateRenewalOptions cfg, CancellationToken cancellationToken) { var key = KeyFactory.NewKey(KeyAlgorithm.RS256); await order.Finalize(new CsrInfo(), key); var certChain = await order.Download(); var builder = certChain.ToPfx(key); builder.FullChain = true; var bytes = new byte[32]; _randomGenerator.GetNonZeroBytes(bytes); var password = Convert.ToBase64String(bytes); var pfxBytes = builder.Build(string.Join(";", cfg.HostNames), password); return(pfxBytes, password); }
/// <summary> /// Checks if we have to renew the specific certificate just yet. /// </summary> /// <param name="options"></param> /// <param name="cfg"></param> /// <param name="cancellationToken"></param> /// <returns>The cert if it is still valid according to rules in config. False otherwise.</returns> private async Task <ICertificate> GetExistingCertificateAsync( IAcmeOptions options, CertificateRenewalOptions cfg, CancellationToken cancellationToken) { if (cfg.Overrides.ForceNewCertificates) { // if overrides contain domain whitelist then only ignore existing certificate if it is not matched if (cfg.Overrides.DomainsToUpdate.Any()) { if (cfg.Overrides.DomainsToUpdate.Any(domain => cfg.HostNames.Contains(domain, StringComparison.OrdinalIgnoreCase))) { _logger.LogWarning($"Override '{nameof(cfg.Overrides.ForceNewCertificates)}' is enabled, forcing certificate renewal."); return(null); } _logger.LogWarning($"Override '{nameof(cfg.Overrides.ForceNewCertificates)}' is enabled but certificate does not match any of the hostnames -> force renewal is not applied to this certificate."); } else { // ignore existing certificate _logger.LogWarning($"Override '{nameof(cfg.Overrides.ForceNewCertificates)}' is enabled, forcing certificate renewal."); return(null); } } var certStore = _renewalOptionParser.ParseCertificateStore(cfg); // determine if renewal is needed based on existing cert var existingCert = await certStore.GetCertificateAsync(cancellationToken); // check if cert exists and that it is valid right now if (existingCert != null) { // handle cases of manually uploaded certificates if (!existingCert.Expires.HasValue) { throw new NotSupportedException($"Missing expiration value on certificate {existingCert.Name} (provider: {certStore.Type}). " + "Must be set to expiration date of the certificate."); } var now = DateTime.UtcNow; // must be valid now and some day in the future based on config expiration rule var isValidAlready = !existingCert.NotBefore.HasValue || existingCert.NotBefore.Value < now; var isStillValid = existingCert.Expires.Value.Date.AddDays(-options.RenewXDaysBeforeExpiry) >= now; if (isValidAlready && isStillValid) { _logger.LogInformation($"Certificate {existingCert.Name} (from source: {certStore.Name}) is still valid until {existingCert.Expires.Value}. " + $"Will be renewed in {(int)(existingCert.Expires.Value - now).TotalDays - options.RenewXDaysBeforeExpiry} days. Skipping renewal."); // ensure cert covers all requested domains exactly (order doesn't matter, but one cert more or less does) var requestedDomains = cfg.HostNames .Select(s => s.ToLowerInvariant()) .OrderBy(s => s) .ToArray(); var certDomains = existingCert.HostNames .Select(s => s.ToLowerInvariant()) .OrderBy(s => s) .ToArray(); if (!requestedDomains.SequenceEqual(certDomains)) { // if not exact domains as requested consider invalid and issue a new cert return(null); } return(existingCert); } var reason = !isValidAlready ? $"certificate won't be valid until {existingCert.NotBefore}" : $"renewal is demanded {options.RenewXDaysBeforeExpiry} days before expiry and it is currently {(int)(existingCert.Expires.Value - now).TotalDays} days before expiry"; _logger.LogInformation($"Certificate {existingCert.Name} (from source: {certStore.Name}) is no longer up to date ({reason})."); } // either no cert or expired return(null); }
/// <summary> /// Runs the LetsEncrypt challenge and verifies that it was completed successfully. /// </summary> /// <param name="options"></param> /// <param name="cfg"></param> /// <param name="cancellationToken"></param> /// <returns>Returns the context, ready to generate certificate from.</returns> private async Task <IOrderContext> ValidateOrderAsync( IAcmeOptions options, CertificateRenewalOptions cfg, CancellationToken cancellationToken) { _logger.LogInformation($"starting -> _authenticationService.AuthenticateAsync"); var authenticationContext = await _authenticationService.AuthenticateAsync(options, cancellationToken); _logger.LogInformation($"done -> _authenticationService.AuthenticateAsync"); _logger.LogInformation($"starting -> authenticationContext.AcmeContext.NewOrder"); var order = await authenticationContext.AcmeContext.NewOrder(cfg.HostNames); _logger.LogInformation($"done -> authenticationContext.AcmeContext.NewOrder"); _logger.LogInformation($"starting -> renewalOptionParser.ParseChallengeResponderAsync"); var challenge = await _renewalOptionParser.ParseChallengeResponderAsync(cfg, cancellationToken); _logger.LogInformation($"done -> renewalOptionParser.ParseChallengeResponderAsync"); _logger.LogInformation($"starting -> challenge.InitiateChallengesAsync {order.Location}"); var challengeContexts = await challenge.InitiateChallengesAsync(order, cancellationToken); _logger.LogInformation($"challengeContextCount {challengeContexts.Count()}"); foreach (var item in challengeContexts) { _logger.LogInformation($"starting -> itemContext validating"); var result = await item.Validate(); if (result != null) { _logger.LogInformation($"result.value = {result?.Status.Value}"); //_logger.LogInformation($"result.token = {result?.Token}"); //_logger.LogInformation($"result.type = {result?.Type}"); //_logger.LogInformation($"result.Url = {result?.Url?.AbsoluteUri}"); //_logger.LogInformation($"result.Error.Detail = {result?.Error?.Detail}"); //_logger.LogInformation($"result.Error.Identifier.Value = {result?.Error?.Identifier?.Value}"); //_logger.LogInformation($"result.Error.Identifier.Status= {result?.Error?.Status}"); //foreach (var err in result?.Error?.Subproblems) //{ // _logger.LogInformation($"result.Error.Identifier.Subproblems= {err.Detail}"); //} //_logger.LogInformation($"result.Error.Identifier.Type= {result?.Error?.Type}"); } } _logger.LogInformation($"done -> challenge.InitiateChallengesAsync"); if (challengeContexts.IsNullOrEmpty()) { _logger.LogError($"starting -> challenge.InitiateChallengesAsync empty challengecontext"); throw new ArgumentNullException(nameof(challengeContexts)); } try { // validate domain ownership _logger.LogInformation($"starting -> ValidateDomainOwnershipAsync"); await ValidateDomainOwnershipAsync(challengeContexts, cancellationToken); _logger.LogInformation($"done -> ValidateDomainOwnershipAsync"); } finally { _logger.LogInformation($"starting -> challenge.CleanupAsync"); await challenge.CleanupAsync(challengeContexts, cancellationToken); _logger.LogInformation($"done -> challenge.CleanupAsync"); } return(order); }