private async Task <IKey> LoadExistingAccountKey(
            IAcmeOptions options,
            CancellationToken cancellationToken)
        {
            _logger.LogInformation($"starting -> LoadExistingAccountKey var fileName = GetAccountKeyFilename(options);");
            var fileName = GetAccountKeyFilename(options);

            _logger.LogInformation($"done -> var fileName = GetAccountKeyFilename(options);");

            _logger.LogInformation($"starting -> if (!await _storageProvider.ExistsAsync(fileName, cancellationToken))");
            if (!await _storageProvider.ExistsAsync(fileName, cancellationToken))
            {
                return(null);
            }
            _logger.LogInformation($"done -> if (!await _storageProvider.ExistsAsync(fileName, cancellationToken))");

            _logger.LogInformation($"starting -> await _storageProvider.GetAsync(fileName, cancellationToken);");
            var content = await _storageProvider.GetAsync(fileName, cancellationToken);

            _logger.LogInformation($"done -> await _storageProvider.GetAsync(fileName, cancellationToken);");

            _logger.LogInformation($"starting -> awaitvar result = _keyFactory.FromPem(content);");
            var result = _keyFactory.FromPem(content);

            _logger.LogInformation($"done -> awaitvar result = _keyFactory.FromPem(content);");
            return(result);
        }
        /// <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);
        }
Esempio n. 3
0
        public async Task <AuthenticationContext> AuthenticateAsync(
            IAcmeOptions options,
            CancellationToken cancellationToken)
        {
            if (options == null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            IAcmeContext acme;
            var          existingKey = await LoadExistingAccountKey(options, cancellationToken);

            if (existingKey == null)
            {
                acme = _contextFactory.GetContext(options.CertificateAuthorityUri);
                // as far as I understand there is a penalty for calling NewAccount too often
                // thus storing the key is encouraged
                // however a keyloss is "non critical" as NewAccount can be called on any existing account without problems
                await acme.NewAccount(options.Email, true);

                existingKey = acme.AccountKey;
                await StoreAccountKeyAsync(options, existingKey, cancellationToken);
            }
            else
            {
                acme = _contextFactory.GetContext(options.CertificateAuthorityUri, existingKey);
            }
            return(new AuthenticationContext(acme, options));
        }
        private string GetAccountKeyFilename(IAcmeOptions options)
        {
            _logger.LogInformation($"starting -> AuthenticationService.GetAccountKeyFilename");
            var result = "account/" + _storageProvider.Escape(string.Format(AccountKeyFilenamePattern, options.CertificateAuthorityUri.Host, options.Email));

            _logger.LogInformation($"starting -> AuthenticationService.GetAccountKeyFilename {result}");
            return(result);
        }
        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);
        }
Esempio n. 6
0
        private Task StoreAccountKeyAsync(
            IAcmeOptions options,
            IKey existingKey,
            CancellationToken cancellationToken)
        {
            var filename = GetAccountKeyFilename(options);
            var content  = existingKey.ToPem();

            return(_storageProvider.SetAsync(filename, content, cancellationToken));
        }
Esempio n. 7
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);
        }
Esempio n. 8
0
        private async Task <IKey> LoadExistingAccountKey(
            IAcmeOptions options,
            CancellationToken cancellationToken)
        {
            var fileName = GetAccountKeyFilename(options);

            if (!await _storageProvider.ExistsAsync(fileName, cancellationToken))
            {
                return(null);
            }

            var content = await _storageProvider.GetAsync(fileName, cancellationToken);

            return(_keyFactory.FromPem(content));
        }
Esempio n. 9
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);
        }
        private Task StoreAccountKeyAsync(
            IAcmeOptions options,
            IKey existingKey,
            CancellationToken cancellationToken)
        {
            _logger.LogInformation($"starting -> StoreAccountKeyAsync var filename = GetAccountKeyFilename(options);");
            var filename = GetAccountKeyFilename(options);

            _logger.LogInformation($"done -> StoreAccountKeyAsync var filename = GetAccountKeyFilename(options);");

            _logger.LogInformation($"starting -> var content = existingKey.ToPem();");
            var content = existingKey.ToPem();

            _logger.LogInformation($"done -> var content = existingKey.ToPem();");

            _logger.LogInformation($"starting -> _storageProvider.SetAsync(filename, content, cancellationToken);");
            var result = _storageProvider.SetAsync(filename, content, cancellationToken);

            _logger.LogInformation($"done -> _storageProvider.SetAsync(filename, content, cancellationToken);");
            return(result);
        }
 public AuthenticationContext(IAcmeContext acme, IAcmeOptions options)
 {
     AcmeContext = acme ?? throw new ArgumentNullException(nameof(acme));
     Options     = options ?? throw new ArgumentNullException(nameof(options));
 }
Esempio n. 12
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.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);
        }
Esempio n. 14
0
 private string GetAccountKeyFilename(IAcmeOptions options)
 => "account/" + _storageProvider.Escape(string.Format(AccountKeyFilenamePattern, options.CertificateAuthorityUri.Host, options.Email));