private async Task ValidateOrderAsync(IOrderContext order, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var allAuthorizations = await order.Authorizations(); var challengeContexts = await Task.WhenAll(allAuthorizations.Select(x => x.Http())); _logger.LogInformation("Validating all pending order authorizations."); var acmeChallengeResponses = challengeContexts.Select(x => new AcmeChallengeResponse(x.Token, x.KeyAuthz)).ToArray(); foreach (var acmeChallengeResponse in acmeChallengeResponses) { await _challengeStore.SaveChallengesAsync(acmeChallengeResponse, cancellationToken); } var challenges = await ValidateChallengesAsync(challengeContexts, cancellationToken); var challengeExceptions = challenges .Where(x => x.Status == ChallengeStatus.Invalid) .Select(x => new Exception(x.Error.Type + ": " + x.Error.Detail)) .ToArray(); if (challengeExceptions.Length > 0) { throw new AcmeRequestException( "One or more LetsEncrypt orders were invalid. Make sure that LetsEncrypt can contact the domain you are trying to request an SSL certificate for, in order to verify it.", new AggregateException(challengeExceptions)); } }
public async Task <byte[]> GetCertificateAsync(Action <string> setDnsTxt) { IOrderContext order = await AcmeContext.NewOrder(new[] { Configuration.CertificateIdentifier }); var authz = (await order.Authorizations()).First(); var dnsChallenge = await authz.Dns(); var dnsTxt = AcmeContext.AccountKey.DnsTxt(dnsChallenge.Token); var privateKey = KeyFactory.NewKey(KeyAlgorithm.ES256); setDnsTxt(dnsTxt); Challenge challenge = null; for (int i = 0; challenge?.Status != ChallengeStatus.Valid && i < 10; i++) { challenge = await dnsChallenge.Validate(); if (challenge.Status == ChallengeStatus.Valid) { break; } Thread.Sleep(1000); } var cert = await order.Generate(Configuration.CsrInfo, privateKey); var pfxBuilder = cert.ToPfx(privateKey); return(pfxBuilder.Build(Configuration.CertificateName, Configuration.CertificatePassword)); }
private static async Task ValidateOrder(IOrderContext order) { Log($" 4. Validating domain {_configuration.DomainName}..."); var authz = (await order.Authorizations()).First(); var httpChallenge = await authz.Http(); var keyAuthz = httpChallenge.KeyAuthz; Log(" 5. Writing challenge file"); var tokens = keyAuthz.Split('.'); await File.WriteAllTextAsync(Path.Combine(_configuration.ChallengePath, tokens[0]), keyAuthz); var chall = await httpChallenge.Validate(); while (chall.Status == ChallengeStatus.Pending) { await Task.Delay(10000); chall = await httpChallenge.Validate(); } if (chall.Status == ChallengeStatus.Valid) { Log($" 6. Domain {_configuration.DomainName} is valid!"); } if (chall.Status == ChallengeStatus.Invalid) { Log($" 6. Domain {_configuration.DomainName} is NOT valid! {chall.Error.Detail}"); } }
/// <summary> /// Register a new order on the ACME service, for the specified domains. Challenges will be automatically verified. /// This method manages automatically the creation of necessary directory and files. /// </summary> /// <remarks> /// When using HTTP Validation, the ACME directory will access to http://__domain__/.well-known/acme-challenge/token, that should be served /// by a local web server when not using built-in, and translated into local path {challengeVerifyPath}\.well-known\acme-challenge\token. /// Important Note: currently WinCertes supports only http-01 validation mode, and dns-01 validation mode with limitations. /// </remarks> /// <param name="domains">The list of domains to be registered and validated</param> /// <param name="httpChallengeValidator">The object used for challenge validation</param> /// <returns>True if successful</returns> public async Task <bool> RegisterNewOrderAndVerify(IList <string> domains, IHTTPChallengeValidator httpChallengeValidator, IDNSChallengeValidator dnsChallengeValidator) { try { // Re-init to be sure to get a fresh Nonce InitCertes(); // Creating the order _orderCtx = await _acme.NewOrder(domains); if (_orderCtx == null) { throw new Exception("Could not create certificate order."); } // And fetching authorizations var orderAuthz = await _orderCtx.Authorizations(); // Looping through authorizations foreach (IAuthorizationContext authz in orderAuthz) { InitCertes(); await ValidateAuthz(authz, httpChallengeValidator, dnsChallengeValidator); } _options.CertificateChallenged = true; // If we are here, it means order was properly created, and authorizations & challenges were properly verified. logger.Info($"Generated orders and validated challenges for domains: {String.Join(",", domains)}"); return(true); } catch (Exception exp) { logger.Debug(exp, "Error while trying to register and validate order"); logger.Error($"Failed to register and validate order with CA: {ProcessCertesException(exp)}"); _options.CertificateChallenged = false; return(false); } }
protected async Task ProcessOrderAuthorizations(IOrderContext orderCtx, IAcmeContext acmeCtx, CertificateInfo certInfo) { // Пройти по всем авторизациям и создать необходимые DNS записи (DNS challenge) // или текстовые файлы (HTTP challenge) var authorizations = (await orderCtx.Authorizations()).ToList(); foreach (IAuthorizationContext authCtx in authorizations) { Challenge challenge = null; foreach (var handler in challengeHandlers.OrderBy(x => x.Priority)) { challenge = await handler.DoChallengeAsync(authCtx, acmeCtx, orderCtx, certInfo); if (challenge != null) { break; } } //Если не нашлось ни одного handler'а для обработки авторизации, то выкинуть исключение это означает, //что мы не можем завершить подтвержденрие владения доменом if (challenge == null) { throw new InvalidOperationException($"Can't complete authorizations for certificate {certInfo.Name}"); } } }
public async Task <string> GetDnsAuthorizationTextAsync() { var authorization = (await orderContext.Authorizations()).First(); challengeContext = await authorization.Dns(); return(acmeContext.AccountKey.DnsTxt(challengeContext.Token)); }
private async Task ValidateOrderAsync(string[] domains, IOrderContext order) { var allAuthorizations = await order.Authorizations(); var challengeContextTasks = new List <Task <IChallengeContext> >(); challengeContextTasks.AddRange(allAuthorizations.Select(x => x.Http())); var challengeContexts = await Task.WhenAll(challengeContextTasks); var nonNullChallengeContexts = challengeContexts.Where(x => x != null); _logger.LogInformation("Validating all pending order authorizations."); var challengeDtos = nonNullChallengeContexts.Select(x => new ChallengeDto() { Token = x.Type == ChallengeTypes.Dns01 ? acme.AccountKey.DnsTxt(x.Token) : x.Token, Response = x.KeyAuthz, Domains = domains }).ToArray(); await _persistenceService.PersistChallengesAsync(challengeDtos); try { var challengeValidationResponses = await ValidateChallengesAsync(nonNullChallengeContexts); var nonNullChallengeValidationResponses = challengeValidationResponses.Where(x => x != null).ToArray(); if (challengeValidationResponses.Length > nonNullChallengeValidationResponses.Length) { _logger.LogWarning("Some challenge responses were null."); } await _persistenceService.DeleteChallengesAsync(challengeDtos); var challengeExceptions = nonNullChallengeValidationResponses .Where(x => x.Status == ChallengeStatus.Invalid) .Select(x => new Exception($"{x.Error?.Type ?? "errortype null"}: {x.Error?.Detail ?? "null errordetails"} (challenge type {x.Type ?? "null"})")) .ToArray(); if (challengeExceptions.Length > 0) { throw new OrderInvalidException( "One or more LetsEncrypt orders were invalid. Make sure that LetsEncrypt can contact the domain you are trying to request an SSL certificate for, in order to verify it.", new AggregateException(challengeExceptions)); } } finally { await _persistenceService.DeleteChallengesAsync(challengeDtos); } }
/// <summary> /// Register a new order on the ACME service, for the specified domains. Challenges will be automatically verified. /// This method manages automatically the creation of necessary directory and files. /// </summary> /// <remarks> /// The ACME directory will access to http://__domain__/.well-known/acme-challenge/token, that should be served by a local web server /// when not using built-in, and translated into local path {challengeVerifyPath}\.well-known\acme-challenge\token. /// Important Note: currently WinCertes supports only http-01 validation mode. /// </remarks> /// <param name="domains">The list of domains to be registered and validated</param> /// <param name="challengeValidator">The object used for challenge validation</param> /// <returns></returns> public async Task <bool> RegisterNewOrderAndVerify(IList <string> domains, IHTTPChallengeValidator challengeValidator) { try { // Re-init to be sure to get a fresh Nonce InitCertes(); // Creating the order _orderCtx = await _acme.NewOrder(domains); if (_orderCtx == null) { throw new Exception("Could not create certificate order."); } // And fetching authorizations var orderAuthz = await _orderCtx.Authorizations(); // Looping through authorizations foreach (IAuthorizationContext authz in orderAuthz) { InitCertes(); // For each authorization, get the challenges var allChallenges = await authz.Challenges(); // Not sure if it's useful... var res = await authz.Resource(); // Get the HTTP challenge var httpChallenge = await authz.Http(); if (httpChallenge != null) { var resValidation = await ValidateChallenge(httpChallenge, challengeValidator); if (!resValidation) { throw new Exception($"Could not validate challenge {httpChallenge.Location.ToString()}"); } } else { throw new Exception("Only HTTP challenges are supported for now"); } } // If we are here, it means order was properly created, and authorizations & challenges were properly verified. logger.Info($"Generated orders and validated challenges for domains: {String.Join(",", domains)}"); return(true); } catch (Exception exp) { logger.Error($"Failed to register and validate order with CA: {ProcessCertesException(exp)}"); return(false); } }
} // proc SaveAsync #endregion #region -- Core Helper ---------------------------------------------------- private async Task <ChallengeStatus> GetChallengeStateAsync(IOrderContext order) { var http = await(await order.Authorizations()).First().Http(); var httpRes = await http.Resource(); var status = httpRes.Status ?? ChallengeStatus.Invalid; if (status == ChallengeStatus.Pending) // start validating { await http.Validate(); } return(status); } // func GetCurrentStateAsync
public async Task <IChallengeContext[]> InitiateChallengesAsync(IOrderContext order, CancellationToken cancellationToken) { var auth = await order.Authorizations(); var challengeContexts = await Task.WhenAll(auth.Select(a => a.Http())); var wellKnownChallengeFiles = challengeContexts .Select(c => (c.Token, c.KeyAuthz)) .ToArray(); await PersistFileChallengeAsync(wellKnownChallengeFiles, cancellationToken); return(challengeContexts); }
private async Task <byte[]> RequestNewCertificate() { await AcmeContext.NewAccount(Email, termsOfServiceAgreed : true); IOrderContext order = await AcmeContext.NewOrder(new List <string> { Hostname }); IEnumerable <IAuthorizationContext> authorizations = await order.Authorizations(); await Task.WhenAll(authorizations.Select(Validate)); byte[] certificate = await CreateCertificate(order); return(certificate); }
public async Task <(bool success, string errorMsg)> Authorize(IOrderContext order, List <string> allDnsIdentifiers) { await EnsureDirectory(); await EnsureWebConfig(); var auths = await order.Authorizations(); var i = 0; foreach (var auth in auths) { var authz = await auth.Http(); var tokenRelativeUri = $".well-known/acme-challenge/{authz.Token}"; await PersistsChallengeFile(tokenRelativeUri, authz.KeyAuthz); var challengeUri = new Uri($"http://{allDnsIdentifiers.ElementAt(i)}/{tokenRelativeUri}"); var retry = await ValidateChallengeFile(challengeUri); if (retry == 0) { throw new Exception($"Unable to validate presence of http challenge at {challengeUri} ensure that it is browsable"); } var response = await authz.Validate(); retry = 10; while ((response.Status == ChallengeStatus.Pending || response.Status == ChallengeStatus.Processing) && retry-- > 0) { Trace.TraceInformation($"Http challenge response status {response.Status} more info at {response.Url.ToString()} retrying in 5 sec"); await Task.Delay(5000); response = await authz.Resource(); } Console.WriteLine($"Authorization Result: {response.Status}, more info at {response.Url}"); Trace.TraceInformation($"Auth Result {response.Status}, more info at {response.Url}"); if (response.Status != ChallengeStatus.Valid) { return(false, JsonConvert.SerializeObject(response)); } i++; } return(true, string.Empty); }
protected async Task WaitValidationAsync(IOrderContext orderCtx) { var authorizations = (await orderCtx.Authorizations()).ToList(); Authorization[] authzList = null; //Ждем пока все проверки будут закончены так или иначе (не останется статусов Pending) while (true) { authzList = await Task.WhenAll(authorizations.Select(x => x.Resource())); if (authzList.FirstOrDefault(x => x.Status == AuthorizationStatus.Pending) == null) { break; } await Task.Delay(TimeSpan.FromSeconds(5)); } //Если все проверки закончились успешно, то возвращаемся if (authzList.All(x => x.Status == AuthorizationStatus.Valid)) { return; } //Если часть закончилась с ошибками, то собираем все ощибки в большую строку и кидаем исключение с ней var errors = authzList.Where(x => x.Status != AuthorizationStatus.Valid) .Select(x => { var errorsDetails = x.Challenges .Where(c => c.Status == ChallengeStatus.Invalid) .Select(c => c.Error?.Detail ?? ""); return(x.Identifier.Value + ": " + string.Join(",", errorsDetails)); }); throw new InvalidOperationException($"Error in domain validation: {string.Join(";",errors)}"); }
/// <summary> /// Gets the authorization by identifier. /// </summary> /// <param name="context">The order context.</param> /// <param name="value">The identifier value.</param> /// <param name="type">The identifier type.</param> /// <returns>The authorization found.</returns> public static async Task <IAuthorizationContext> Authorization(this IOrderContext context, string value, IdentifierType type = IdentifierType.Dns) { var wildcard = value.StartsWith("*."); if (wildcard) { value = value.Substring(2); } foreach (var authzCtx in await context.Authorizations()) { var authz = await authzCtx.Resource(); if (string.Equals(authz.Identifier.Value, value, StringComparison.OrdinalIgnoreCase) && wildcard == authz.Wildcard.GetValueOrDefault() && authz.Identifier.Type == type) { return(authzCtx); } } return(null); }
private async Task ValidateOrderAsync(IOrderContext order) { var allAuthorizations = await order.Authorizations(); var challengeContexts = await Task.WhenAll( allAuthorizations.Select(x => x.Http())); _logger.LogInformation("Validating all pending order authorizations."); var challengeDtos = challengeContexts.Select(x => new ChallengeDto() { Token = x.Token, Response = x.KeyAuthz }).ToArray(); await _persistenceService.PersistChallengesAsync(challengeDtos); try { var challenges = await ValidateChallengesAsync(challengeContexts); var challengeExceptions = challenges .Where(x => x.Status == ChallengeStatus.Invalid) .Select(x => new Exception(x.Error.Type + ": " + x.Error.Detail)) .ToArray(); if (challengeExceptions.Length > 0) { throw new OrderInvalidException( "One or more LetsEncrypt orders were invalid. Make sure that LetsEncrypt can contact the domain you are trying to request an SSL certificate for, in order to verify it.", new AggregateException(challengeExceptions)); } } finally { await _persistenceService.PersistChallengesAsync(null); } }
public static void GetFirstCert(bool staging = true) { ContextAccountBundle bundle; if (staging) { bundle = GetStagingParameters(); } else { bundle = GetNonStagingParameters(); } var account = bundle.Account; var ctx = bundle.Ctx; IOrderListContext orders = account.Orders().Result; List <IOrderContext> orderContexts = new List <IOrderContext>(orders.Orders().Result); IOrderContext currentOrder = null; if (orderContexts.Count == 0) { currentOrder = ctx.NewOrder(new[] { "jcf-ai.com" }).Result; } else { foreach (IOrderContext orderCtx in orderContexts) { if (orderCtx.Resource().Result.Status != OrderStatus.Valid) { currentOrder = orderCtx; break; } } if (currentOrder == null) { currentOrder = ctx.NewOrder(new[] { "jcf-ai.com" }).Result; } } Order order = currentOrder.Resource().Result; var authorizationList = currentOrder.Authorizations().Result; IAuthorizationContext currentAuthContext; IEnumerator <IAuthorizationContext> enumerator = authorizationList.GetEnumerator(); while (enumerator.Current == null) { enumerator.MoveNext(); } currentAuthContext = enumerator.Current; Authorization authResource = currentAuthContext.Resource().Result; IChallengeContext httpContext = currentAuthContext.Http().Result; if (httpContext.Resource().Result.Status != ChallengeStatus.Valid) { if (!System.IO.Directory.Exists("tokens")) { System.IO.Directory.CreateDirectory("tokens"); } StreamWriter writer = new StreamWriter("tokens/" + httpContext.Token); writer.Write(httpContext.KeyAuthz); writer.Close(); Challenge httpChallenge = httpContext.Validate().Result; } //Everything should be good to go on the whole validate everything front. IKey privateKey = KeyFactory.NewKey(KeyAlgorithm.ES384); var cert = currentOrder.Generate( new CsrInfo() { CountryName = "US", State = "Washington", Locality = "Tacoma", CommonName = "jcf-ai.com" }, privateKey).Result; byte[] pfx = cert.ToPfx(privateKey).Build("jcf-ai.com", "pass"); FileStream pfxOutStream = new FileStream("jcf-ai.pfx", FileMode.Create, FileAccess.Write); pfxOutStream.Write(pfx, 0, pfx.Length); pfxOutStream.Close(); ProcessStartInfo psi = new ProcessStartInfo("powershell", "-Command ./bindcert.ps1 jcf-ai.pfx pass"); psi.CreateNoWindow = true; Process.Start(psi); }
public async Task <IEnumerable <IAuthorizationContext> > GetOrderAuthorizations(IOrderContext orderContext) { _logger.LogAcmeAction("FetchAuthorizations", orderContext); return(await orderContext.Authorizations()); }
/// <summary> /// Begin order for new certificate for one or more domains, fetching the required challenges /// to complete /// </summary> /// <param name="log"> </param> /// <param name="config"> </param> /// <param name="orderUri"> Uri of existing order to resume </param> /// <returns> </returns> public async Task <PendingOrder> BeginCertificateOrder(ILog log, CertRequestConfig config, string orderUri = null) { if (DateTime.Now.Subtract(_lastInitDateTime).TotalMinutes > 30) { // our acme context nonce may have expired (which returns "JWS has an invalid // anti-replay nonce") so start a new one await InitProvider(null); } var pendingOrder = new PendingOrder { IsPendingAuthorizations = true }; // prepare a list of all pending authorization we need to complete, or those we have // already satisfied var authzList = new List <PendingAuthorization>(); //if no alternative domain specified, use the primary domain as the subject var domainOrders = new List <string>(); // order all of the distinct domains in the config (primary + SAN). domainOrders.Add(_idnMapping.GetAscii(config.PrimaryDomain)); if (config.SubjectAlternativeNames != null) { foreach (var s in config.SubjectAlternativeNames) { if (!domainOrders.Contains(s)) { domainOrders.Add(_idnMapping.GetAscii(s)); } } } try { IOrderContext order = null; int remainingAttempts = 3; bool orderCreated = false; try { while (!orderCreated && remainingAttempts > 0) { try { remainingAttempts--; log.Error($"BeginCertificateOrder: creating/retrieving order. Retries remaining:{remainingAttempts} "); if (orderUri != null) { order = _acme.Order(new Uri(orderUri)); } else { order = await _acme.NewOrder(domainOrders); } if (order != null) { orderCreated = true; } } catch (Exception exp) { remainingAttempts--; log.Error($"BeginCertificateOrder: error creating order. Retries remaining:{remainingAttempts} {exp.ToString()} "); if (remainingAttempts == 0) { // all attempts to create order failed throw; } else { await Task.Delay(1000); } } } } catch (NullReferenceException exp) { var msg = $"Failed to begin certificate order (account problem or API is not currently available): {exp}"; log.Error(msg); pendingOrder.Authorizations = new List <PendingAuthorization> { new PendingAuthorization { AuthorizationError = msg, IsFailure = true } }; return(pendingOrder); } if (order == null) { throw new Exception("Failed to begin certificate order."); } orderUri = order.Location.ToString(); pendingOrder.OrderUri = orderUri; log.Information($"Created ACME Order: {orderUri}"); // track order in memory, keyed on order Uri if (_currentOrders.Keys.Contains(orderUri)) { _currentOrders.Remove(orderUri); } _currentOrders.Add(orderUri, order); // handle order status 'Ready' if all authorizations are already valid var orderDetails = await order.Resource(); if (orderDetails.Status == OrderStatus.Ready) { pendingOrder.IsPendingAuthorizations = false; } // get all required pending (or already valid) authorizations for this order log.Information($"Fetching Authorizations."); var orderAuthorizations = await order.Authorizations(); // get the challenges for each authorization foreach (IAuthorizationContext authz in orderAuthorizations) { log.Debug($"Fetching Authz Challenges."); var allChallenges = await authz.Challenges(); var res = await authz.Resource(); string authzDomain = res.Identifier.Value; if (res.Wildcard == true) { authzDomain = "*." + authzDomain; } var challenges = new List <AuthorizationChallengeItem>(); // add http challenge (if any) var httpChallenge = await authz.Http(); if (httpChallenge != null) { var httpChallengeStatus = await httpChallenge.Resource(); log.Information($"Got http-01 challenge {httpChallengeStatus.Url}"); if (httpChallengeStatus.Status == ChallengeStatus.Invalid) { log.Error($"HTTP challenge has an invalid status"); } challenges.Add(new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP, Key = httpChallenge.Token, Value = httpChallenge.KeyAuthz, ChallengeData = httpChallenge, ResourceUri = $"http://{authzDomain.Replace("*.", "")}/.well-known/acme-challenge/{httpChallenge.Token}", ResourcePath = $".well-known\\acme-challenge\\{httpChallenge.Token}", IsValidated = (httpChallengeStatus.Status == ChallengeStatus.Valid) }); } // add dns challenge (if any) var dnsChallenge = await authz.Dns(); if (dnsChallenge != null) { var dnsChallengeStatus = await dnsChallenge.Resource(); log.Information($"Got dns-01 challenge {dnsChallengeStatus.Url}"); if (dnsChallengeStatus.Status == ChallengeStatus.Invalid) { log.Error($"DNS challenge has an invalid status"); } var dnsValue = _acme.AccountKey.DnsTxt(dnsChallenge.Token); //ComputeDnsValue(dnsChallenge, _acme.AccountKey); var dnsKey = $"_acme-challenge.{authzDomain}".Replace("*.", ""); challenges.Add(new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS, Key = dnsKey, Value = dnsValue, ChallengeData = dnsChallenge, IsValidated = (dnsChallengeStatus.Status == ChallengeStatus.Valid) }); } // add tls-sni-01 challenge (if any) /* var tls01Challenge = await authz.Challenges(; * if (dnsChallenge != null) * { * var dnsChallengeStatus = await dnsChallenge.Resource(); * * challenges.Add(new AuthorizationChallengeItem * { * ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS, * Key = dnsChallenge.Token, * Value = dnsChallenge.KeyAuthz, * ChallengeData = dnsChallenge, * IsValidated = (dnsChallengeStatus.Status == ChallengeStatus.Valid) * }); * }*/ // report back on the challenges we now may need to attempt authzList.Add( new PendingAuthorization { Challenges = challenges, Identifier = new IdentifierItem { Dns = authzDomain, IsAuthorizationPending = !challenges.Any(c => c.IsValidated) //auth is pending if we have no challenges already validated }, AuthorizationContext = authz, IsValidated = challenges.Any(c => c.IsValidated), OrderUri = orderUri }); } pendingOrder.Authorizations = authzList; return(pendingOrder); } catch (AcmeRequestException exp) { // failed to register one or more domain identifier with LE (invalid, rate limit or // CAA fail?) var msg = $"Failed to begin certificate order: {exp.Error?.Detail}"; log.Error(msg); pendingOrder.Authorizations = new List <PendingAuthorization> { new PendingAuthorization { AuthorizationError = msg, IsFailure = true } }; return(pendingOrder); } }
static async Task Main(string[] args) { try { ConsoleHelper.WriteLineByColor("Acme .NET Certes类库简单控制台应用程序演示", ConsoleColor.Green); ConsoleHelper.WriteLineByColor("详见:https://github.com/fszlin/certes", ConsoleColor.Green); ConsoleHelper.DrawHalfSplitLine(); AcmeV2Config acmeConfig = AcmeV2Helper.GetAcmeConfig(_acmeConfigFilePath); ShowAcmeConfigInfo(acmeConfig); ShowCertInfo(acmeConfig); if (!Directory.Exists(acmeConfig.ChallengeTokenSavePath)) { ConsoleHelper.WriteLineByColor($"Error: 验证令牌路径不存在 ---{acmeConfig.ChallengeTokenSavePath}", ConsoleColor.Red); return; } if (!Directory.Exists(acmeConfig.CertSavePath)) { ConsoleHelper.WriteLineByColor($"Error: 证书文件存放路径不存在 ---{acmeConfig.CertSavePath}", ConsoleColor.Red); return; } #region 步骤一,创建或使用已有的ACME帐户 ConsoleHelper.DrawHalfSplitLine(); Console.WriteLine("步骤一,创建或使用已有的ACME帐户"); /* * 注意!!! * 目前调用的API使用的是测试环境, * WellKnownServers.LetsEncryptStagingV2=https://acme-staging-v02.api.letsencrypt.org * 生产环境应当使用:WellKnownServers.LetsEncryptV2 */ // AcmeContext acmeCtx = AcmeV2Helper.GetAcmeContext(acmeConfig.AccountPemKey, WellKnownServers.LetsEncryptStagingV2, out bool isExist); if (acmeCtx == null) { ConsoleHelper.WriteLineByColor($"Error: 无法获取正确的ACME上下文信息,检查Api地址和AccountPemKey是否正确", ConsoleColor.Red); return; } IAccountContext accountCtx; //根据Account Pem Key判断是否需要创建新帐号,或者用已有帐户 if (isExist) { Console.WriteLine($"已存在Account Pem Key,当前帐户:[{acmeConfig.AccountName}],获取帐户操作上下文..."); accountCtx = await acmeCtx.Account(); } else { Console.WriteLine($"未有Account Pem Key,创建新帐户[{acmeConfig.AccountName}]以获取帐户操作上下文..."); accountCtx = await AcmeV2Helper.GetAccountContextByNewAsync(acmeCtx, acmeConfig.AccountName); } if (accountCtx == null) { ConsoleHelper.WriteLineByColor($"Error: 无法获取正确的ACME帐户操作上下文信息,请检查相关帐户密钥是否正确,{acmeConfig.AccountName}", ConsoleColor.Red); return; } //保存ACME帐户密钥(pem key)到本地 acmeConfig.AccountPemKey = acmeCtx.AccountKey.ToPem(); AcmeV2Helper.SaveAcmeConfigJson(acmeConfig, _acmeConfigFilePath); Console.WriteLine("步骤一完成"); #endregion #region 步骤二,通过订单上下文向ACME服务器发送请求以验证域名所有权 //步骤二,域名认证,通过订单上下文向ACME服务器发送请求以验证域名所有权 //此处使用HTTP-01验证方式轮询所有域名,适合非通配符证书。Ps:如果需要使用通配符域名则需要使用DNS-01验证方式 ConsoleHelper.DrawHalfSplitLine(); Console.WriteLine("步骤二,通过订单上下文向ACME服务器发送请求以验证域名所有权"); IOrderContext orderCtx = await acmeCtx.NewOrder(acmeConfig.Identifiers.ToArray()); IList <IAuthorizationContext> authzList = (await orderCtx.Authorizations()).ToList(); bool isValidate = await AcmeV2Helper.HttpChallengeValidateByListAsync(authzList, acmeConfig.ChallengeTokenSavePath); if (!isValidate) { ConsoleHelper.WriteLineByColor($"Error: HTTP-01验证未通过!!!", ConsoleColor.Red); return; } Console.WriteLine("步骤二完成"); #endregion #region 步骤三,验证通过后下载证书 //步骤三,验证通过后下载证书 ConsoleHelper.DrawHalfSplitLine(); Console.WriteLine("步骤三,验证通过后下载证书"); AcmeV2Helper.SaveAcmeConfigJson(acmeConfig, _acmeConfigFilePath); await AcmeV2Helper.ExportCertificateAsync(orderCtx, acmeConfig); Console.WriteLine("步骤三完成"); #endregion } catch (Exception ex) { ConsoleHelper.WriteLineByColor(ex.Message, ConsoleColor.Red); ConsoleHelper.DrawHalfSplitLine(); ConsoleHelper.WriteLineByColor(ex.StackTrace, ConsoleColor.Yellow); } Console.ReadKey(); }
/// <summary> /// HTTP-01 challenge. /// </summary> /// <exception cref="LetsEncryptMikroTikException"/> public async Task <LetsEncryptCert> GetCertAsync(string?accountPemKey = null) { AcmeContext acme; IAccountContext account; //Uri knownServer = _letsEncryptAddress; if (accountPemKey != null) { // Load the saved account key var accountKey = KeyFactory.FromPem(accountPemKey); acme = new AcmeContext(_letsEncryptAddress, accountKey); Log.Information("Авторизация в Let's Encrypt."); account = await acme.Account().ConfigureAwait(false); } else { acme = new AcmeContext(_letsEncryptAddress); Log.Information("Авторизация в Let's Encrypt."); account = await acme.NewAccount(_config.Email, termsOfServiceAgreed : true).ConfigureAwait(false); // Save the account key for later use //string pemKey = acme.AccountKey.ToPem(); } Log.Information("Заказываем новый сертификат."); IOrderContext order = await acme.NewOrder(new[] { _config.DomainName }).ConfigureAwait(false); // Get the token and key authorization string. Log.Information("Получаем способы валидации заказа."); var authz = (await order.Authorizations().ConfigureAwait(false)).First(); if (_config.UseAlpn) { Log.Information("Выбираем TLS-ALPN-01 способ валидации."); IChallengeContext challenge = await authz.TlsAlpn().ConfigureAwait(false); string keyAuthz = challenge.KeyAuthz; string token = challenge.Token; await ChallengeAlpnAsync(challenge, keyAuthz).ConfigureAwait(false); } else { Log.Information("Выбираем HTTP-01 способ валидации."); IChallengeContext challenge = await authz.Http().ConfigureAwait(false); string keyAuthz = challenge.KeyAuthz; await HttpChallengeAsync(challenge, keyAuthz).ConfigureAwait(false); } Log.Information("Загружаем сертификат."); // Download the certificate once validation is done IKey privateKey = KeyFactory.NewKey(KeyAlgorithm.RS256); CertificateChain cert = await order.Generate(new CsrInfo { CommonName = _config.DomainName, CountryName = "CA", State = "Ontario", Locality = "Toronto", Organization = "Certes", OrganizationUnit = "Dev", }, privateKey) .ConfigureAwait(false); // Export full chain certification. string certPem = cert.ToPem(); string keyPem = privateKey.ToPem(); // Export PFX PfxBuilder pfxBuilder = cert.ToPfx(privateKey); byte[] pfx = pfxBuilder.Build(friendlyName: _config.DomainName, password: ""); //await acme.RevokeCertificate(pfx, RevocationReason.Superseded, privateKey); using (var cert2 = new X509Certificate2(pfx)) { return(new LetsEncryptCert(cert2.NotAfter, certPem, keyPem, cert2.GetCommonName(), cert2.GetSha2Thumbprint())); } }
public async Task <AcmeOrder> BeginOrder() { var domains = new List <string> { _acmeCertificate.Subject }; if (!string.IsNullOrWhiteSpace(_acmeCertificate.SANs)) { var sans = _acmeCertificate.SANs .Split(new[] { "\r\n", "\r", "\n", ",", ";" }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim()) .ToList(); domains.AddRange(sans); } _acmeOrder = new AcmeOrder { AcmeCertificate = _acmeCertificate, DateCreated = DateTime.UtcNow, Status = AcmeOrderStatus.Created }; _acmeCertificate.AcmeOrders.Add(_acmeOrder); try { _order = await _acmeContext.NewOrder(domains); _logger.LogDebug($"Order created: {_order.Location}"); // Get authorizations for the new order which we'll then place var authz = await _order.Authorizations(); // Track all auth requests to the corresponding validation and // subsequent completion and certificate response _authChallengeContainers = authz.Select(x => new AuthChallengeContainer { AuthorizationContext = x, // TODO: Once dns-01 is implemented, check and specify the type below ChallengeContextTask = x.Http() }).ToList(); await Task.WhenAll(_authChallengeContainers.Select(x => x.ChallengeContextTask).ToList()); _acmeOrder.Status = AcmeOrderStatus.Challenging; foreach (var cc in _authChallengeContainers) { cc.ChallengeContext = cc.ChallengeContextTask.Result; var acmeReq = new AcmeRequest { KeyAuthorization = cc.ChallengeContext.KeyAuthz, Token = cc.ChallengeContext.Token, DateCreated = DateTime.UtcNow, AcmeOrder = _acmeOrder }; _acmeOrder.AcmeRequests.Add(acmeReq); cc.AcmeRequest = acmeReq; } } catch (AcmeRequestException e) { _logger.LogError(e, "Error requesting order"); _acmeOrder.Status = AcmeOrderStatus.Error; _acmeOrder.Errors = e.Message; } catch (Exception e) { _logger.LogError(e, "Error creating order"); _acmeOrder.Status = AcmeOrderStatus.Error; } return(_acmeOrder); }
/// <summary> /// Begin order for new certificate for one or more domains, fetching the required challenges /// to complete /// </summary> /// <param name="log"> </param> /// <param name="config"> </param> /// <param name="orderUri"> Uri of existing order to resume </param> /// <returns> </returns> public async Task <PendingOrder> BeginCertificateOrder(ILog log, CertRequestConfig config, string orderUri = null) { if (DateTime.Now.Subtract(_lastInitDateTime).TotalMinutes > 30) { // our acme context nonce may have expired (which returns "JWS has an invalid // anti-replay nonce") so start a new one await InitProvider(_log); } var pendingOrder = new PendingOrder { IsPendingAuthorizations = true }; // prepare a list of all pending authorization we need to complete, or those we have // already satisfied var authzList = new List <PendingAuthorization>(); //if no alternative domain specified, use the primary domain as the subject var domainOrders = new List <string> { // order all of the distinct domains in the config (primary + SAN). _idnMapping.GetAscii(config.PrimaryDomain) }; if (config.SubjectAlternativeNames != null) { foreach (var s in config.SubjectAlternativeNames) { if (!domainOrders.Contains(s)) { domainOrders.Add(_idnMapping.GetAscii(s)); } } } try { IOrderContext order = null; var remainingAttempts = 3; var orderCreated = false; object lastException = null; var orderErrorMsg = ""; try { while (!orderCreated && remainingAttempts > 0) { try { remainingAttempts--; log.Information($"BeginCertificateOrder: creating/retrieving order. Retries remaining:{remainingAttempts} "); if (orderUri != null) { order = _acme.Order(new Uri(orderUri)); } else { order = await _acme.NewOrder(domainOrders); } if (order != null) { orderCreated = true; } } catch (Exception exp) { log.Error(exp.ToString()); orderErrorMsg = exp.Message; if (exp is TaskCanceledException) { log.Warning($"BeginCertificateOrder: timeout while communicating with the ACME API"); } if (exp is AcmeRequestException) { var err = (exp as AcmeRequestException).Error; // e.g. urn:ietf:params:acme:error:userActionRequired orderErrorMsg = err?.Detail ?? orderErrorMsg; if ((int)err.Status == 429) { // hit an ACME API rate limit log.Warning($"BeginCertificateOrder: encountered a rate limit while communicating with the ACME API"); return(new PendingOrder(orderErrorMsg)); } if (err.Type?.EndsWith("accountDoesNotExist") == true) { // wrong account details, probably used staging for prod or vice versa log.Warning($"BeginCertificateOrder: attempted to use invalid account details with the ACME API"); return(new PendingOrder(orderErrorMsg)); } } else if (exp.InnerException != null && exp.InnerException is AcmeRequestException) { orderErrorMsg = (exp.InnerException as AcmeRequestException).Error?.Detail ?? orderErrorMsg; } remainingAttempts--; log.Error($"BeginCertificateOrder: error creating order. Retries remaining:{remainingAttempts} :: {orderErrorMsg} "); lastException = exp; if (remainingAttempts == 0) { // all attempts to create order failed throw; } else { await Task.Delay(1000); } } } } catch (NullReferenceException exp) { var msg = $"Failed to begin certificate order (account problem or API is not currently available): {exp.Message}"; log.Error(msg); return(new PendingOrder(msg)); } if (order == null) { var msg = "Failed to begin certificate order."; if (lastException is AcmeRequestException) { var err = (lastException as AcmeRequestException).Error; msg = err?.Detail ?? msg; if (lastException != null && (lastException as Exception).InnerException is AcmeRequestException) { msg = ((lastException as Exception).InnerException as AcmeRequestException).Error?.Detail ?? msg; } } else { if (lastException is Exception) { msg += "::" + (lastException as Exception).ToString(); } } return(new PendingOrder("Error creating Order with Certificate Authority: " + msg)); } orderUri = order.Location.ToString(); pendingOrder.OrderUri = orderUri; log.Information($"Created ACME Order: {orderUri}"); // track order in memory, keyed on order Uri if (_currentOrders.Keys.Contains(orderUri)) { _currentOrders.Remove(orderUri); } _currentOrders.Add(orderUri, order); // handle order status 'Ready' if all authorizations are already valid var orderDetails = await order.Resource(); if (orderDetails.Status == OrderStatus.Ready) { pendingOrder.IsPendingAuthorizations = false; } // get all required pending (or already valid) authorizations for this order log.Information($"Fetching Authorizations."); var orderAuthorizations = await order.Authorizations(); // get the challenges for each authorization foreach (var authz in orderAuthorizations) { log.Debug($"Fetching Authz Challenges."); var allChallenges = await authz.Challenges(); var res = await authz.Resource(); var authzDomain = res.Identifier.Value; if (res.Wildcard == true) { authzDomain = "*." + authzDomain; } var challenges = new List <AuthorizationChallengeItem>(); // add http challenge (if any) var httpChallenge = await authz.Http(); if (httpChallenge != null) { var httpChallengeStatus = await httpChallenge.Resource(); log.Information($"Got http-01 challenge {httpChallengeStatus.Url}"); if (httpChallengeStatus.Status == ChallengeStatus.Invalid) { log.Error($"HTTP challenge has an invalid status"); } challenges.Add(new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP, Key = httpChallenge.Token, Value = httpChallenge.KeyAuthz, ChallengeData = httpChallenge, ResourceUri = $"http://{authzDomain.Replace("*.", "")}/.well-known/acme-challenge/{httpChallenge.Token}", ResourcePath = $".well-known\\acme-challenge\\{httpChallenge.Token}", IsValidated = (httpChallengeStatus.Status == ChallengeStatus.Valid) }); } // add dns challenge (if any) var dnsChallenge = await authz.Dns(); if (dnsChallenge != null) { var dnsChallengeStatus = await dnsChallenge.Resource(); log.Information($"Got dns-01 challenge {dnsChallengeStatus.Url}"); if (dnsChallengeStatus.Status == ChallengeStatus.Invalid) { log.Error($"DNS challenge has an invalid status"); } var dnsValue = _acme.AccountKey.DnsTxt(dnsChallenge.Token); //ComputeDnsValue(dnsChallenge, _acme.AccountKey); var dnsKey = $"_acme-challenge.{authzDomain}".Replace("*.", ""); challenges.Add(new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS, Key = dnsKey, Value = dnsValue, ChallengeData = dnsChallenge, IsValidated = (dnsChallengeStatus.Status == ChallengeStatus.Valid) }); } // report back on the challenges we now may need to attempt authzList.Add( new PendingAuthorization { Challenges = challenges, Identifier = new IdentifierItem { Dns = authzDomain, IsAuthorizationPending = !challenges.Any(c => c.IsValidated) //auth is pending if we have no challenges already validated }, AuthorizationContext = authz, IsValidated = challenges.Any(c => c.IsValidated), OrderUri = orderUri }); } pendingOrder.Authorizations = authzList; return(pendingOrder); } catch (AcmeRequestException exp) { // failed to register one or more domain identifier with LE (invalid, rate limit or // CAA fail?) var msg = $"Could not begin certificate order: {exp.Error?.Detail}"; log.Error(msg); return(new PendingOrder(msg)); } }
public async Task <AcmeOrder> BeginOrder() { var domains = new List <string> { _acmeCertificate.Subject }; if (!string.IsNullOrWhiteSpace(_acmeCertificate.SANs)) { var sans = _acmeCertificate.SANs .Split(new[] { "\r\n", "\r", "\n", ",", ";" }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim()) .ToList(); domains.AddRange(sans); } domains = domains.Distinct().ToList(); _acmeOrder = new AcmeOrder { AcmeCertificate = _acmeCertificate, DateCreated = DateTime.UtcNow, Status = AcmeOrderStatus.Created }; _acmeCertificate.AcmeOrders.Add(_acmeOrder); try { _order = await _acmeContext.NewOrder(domains); _logger.LogDebug($"Order created: {_order.Location}"); // Get authorizations for the new order which we'll then place var authz = await _order.Authorizations(); // Track all auth requests to the corresponding validation and // subsequent completion and certificate response _authChallengeContainers = new List <AuthChallengeContainer>(); foreach (var auth in authz) { var resource = await auth.Resource(); var domain = resource.Identifier.Value; _authChallengeContainers.Add(new AuthChallengeContainer { AuthorizationContext = auth, Domain = domain, ChallengeContextTask = _acmeCertificate.IsDnsChallengeType() ? auth.Dns() : auth.Http(), }); } await Task.WhenAll(_authChallengeContainers.Select(x => x.ChallengeContextTask).ToList()); _acmeOrder.Status = AcmeOrderStatus.Challenging; var newRequests = new List <AcmeRequest>(); foreach (var cc in _authChallengeContainers) { cc.ChallengeContext = cc.ChallengeContextTask.Result; var acmeReq = new AcmeRequest { KeyAuthorization = cc.ChallengeContext.KeyAuthz, Token = cc.ChallengeContext.Token, DateCreated = DateTime.UtcNow, Domain = cc.Domain, DnsTxtValue = _acmeContext.AccountKey.DnsTxt(cc.ChallengeContext.Token), AcmeOrder = _acmeOrder }; _acmeOrder.AcmeRequests.Add(acmeReq); newRequests.Add(acmeReq); cc.AcmeRequest = acmeReq; } } catch (AcmeRequestException e) { _logger.LogError(e, "Error requesting order"); _acmeOrder.Status = AcmeOrderStatus.Error; _acmeOrder.Errors = e.Message; } catch (Exception e) { _logger.LogError(e, "Error creating order"); _acmeOrder.Status = AcmeOrderStatus.Error; } return(_acmeOrder); }