/// <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));
        }
示例#4
0
        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);
        }
示例#5
0
        /// <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);
        }