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); }
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); }
private Task StoreAccountKeyAsync( IAcmeOptions options, IKey existingKey, CancellationToken cancellationToken) { var filename = GetAccountKeyFilename(options); var content = existingKey.ToPem(); return(_storageProvider.SetAsync(filename, content, 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); }
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)); }
/// <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)); }
/// <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); }
private string GetAccountKeyFilename(IAcmeOptions options) => "account/" + _storageProvider.Escape(string.Format(AccountKeyFilenamePattern, options.CertificateAuthorityUri.Host, options.Email));