public static async Task Authorization([ActivityTrigger] DurableActivityContext context, ILogger log) { var(site, authzUrl) = context.GetInput <(Site, string)>(); var acme = await CreateAcmeClientAsync(); var authz = await acme.GetAuthorizationDetailsAsync(authzUrl); // http-01 Challenge のみ対応 var challenge = authz.Challenges.First(x => x.Type == "http-01"); var challengeValidationDetails = AuthorizationDecoder.ResolveChallengeForHttp01(authz, challenge, acme.Signer); var websiteClient = await CreateManagementClientAsync(); var credentials = await websiteClient.WebApps.ListPublishingCredentialsAsync(site.ResourceGroup, site.Name); // Kudu API を使い、Answer 用のファイルを作成 var kuduClient = new KuduApiClient(site.Name, credentials.PublishingUserName, credentials.PublishingPassword); await kuduClient.WriteFileAsync(DefaultWebConfigPath, DefaultWebConfig); await kuduClient.WriteFileAsync(challengeValidationDetails.HttpResourcePath, challengeValidationDetails.HttpResourceValue); // Answer の準備が出来たことを通知 await acme.AnswerChallengeAsync(challenge.Url); }
public void Test_Decode_OrderChallengeForDns01_ForMultiDns() { var testCtx = SetTestContext(); var oldOrder = testCtx.GroupLoadObject <OrderDetails>("order.json"); var oldAuthz = testCtx.GroupLoadObject <Authorization[]>("order-authz.json"); var authzIndex = 0; foreach (var authz in oldAuthz) { var chlngIndex = 0; foreach (var chlng in authz.Challenges.Where( x => x.Type == Dns01ChallengeValidationDetails.Dns01ChallengeType)) { Log.LogInformation("Decoding Authorization {0} Challenge {1}", authzIndex, chlngIndex); var chlngDetails = AuthorizationDecoder.ResolveChallengeForDns01( authz, chlng, Clients.Acme.Signer); Assert.Equal(Dns01ChallengeValidationDetails.Dns01ChallengeType, chlngDetails.ChallengeType, ignoreCase: true); Assert.NotNull(chlngDetails.DnsRecordName); Assert.NotNull(chlngDetails.DnsRecordValue); Assert.Equal("TXT", chlngDetails.DnsRecordType, ignoreCase: true); testCtx.GroupSaveObject($"order-authz_{authzIndex}-chlng_{chlngIndex}.json", chlngDetails); ++chlngIndex; } ++authzIndex; } }
private Task DecodeOrderAuthorizationChallenges(ACMESharp.Crypto.JOSE.IJwsTool signer) { foreach (var authz in _lastOrder.Authorizations) { var miscList = new List <Challenge>(); foreach (var ch in authz.Details.Challenges) { switch (ch.Type) { case Dns01ChallengeValidationDetails.Dns01ChallengeType: authz.DnsChallenge = AuthorizationDecoder.ResolveChallengeForDns01( authz.Details, ch, signer); miscList.Add(ch); break; case Http01ChallengeValidationDetails.Http01ChallengeType: authz.HttpChallenge = AuthorizationDecoder.ResolveChallengeForHttp01( authz.Details, ch, signer); miscList.Add(ch); break; default: miscList.Add(ch); break; } } authz.MiscChallenges = miscList.ToArray(); } return(Task.CompletedTask); }
private async Task PerformChallengesAsync(OrderDetails order) { var exceptions = new List <Exception>(); // Could be multiple authorizations if there are multiple distinct domains specified in the cert foreach (var authUrl in order.Payload.Authorizations) { var auth = await client.GetAuthorizationDetailsAsync(authUrl); var dnsChallenge = auth.Challenges.FirstOrDefault(c => c.Type == Dns01ChallengeValidationDetails.Dns01ChallengeType); if (dnsChallenge == null) { exceptions.Add(new Exception("No DNS challenges for this authorization: " + authUrl)); continue; } var cd = (Dns01ChallengeValidationDetails)AuthorizationDecoder.DecodeChallengeValidation( auth, dnsChallenge.Type, client.Signer); // Create requested DNS record, keep reference to remove after challenge passes logger.LogInformation("[{0}]@{1}ms Creating DNS record '{2}'", auth.Identifier, stopwatch.ElapsedMilliseconds, cd.DnsRecordName); var createdRecord = await this.dnsHandler.HandleAsync(cd.DnsRecordType, cd.DnsRecordName, cd.DnsRecordValue); logger.LogInformation("[{0}]@{1}ms Created DNS record successfully", auth.Identifier, stopwatch.ElapsedMilliseconds); var retries = 0; do { Thread.Sleep(1000 * retries); logger.LogInformation("[{0}]@{1}ms Answering challenge", auth.Identifier, stopwatch.ElapsedMilliseconds); dnsChallenge = await client.AnswerChallengeAsync(dnsChallenge.Url); auth = await client.GetAuthorizationDetailsAsync(authUrl); retries++; } while (auth.Status != "valid" && retries < 5); if (auth.Status != "valid") { exceptions.Add(new Exception($"Challenge validation was unsuccessful: [{dnsChallenge.Status}]{dnsChallenge.Error}\r\nAuthStatus: [{auth.Status}]")); } logger.LogInformation("[{0}]@{1}ms Removing DNS record '{2}'", auth.Identifier, stopwatch.ElapsedMilliseconds, cd.DnsRecordName); await createdRecord.CleanAsync(); logger.LogInformation("[{0}]@{1}ms Removed DNS record successfully", auth.Identifier, stopwatch.ElapsedMilliseconds); } if (exceptions.Any()) { throw new AggregateException("One or more exceptions occured while authorizing the request", exceptions); } }
protected async Task <bool> ResolveChallenges(AcmeProtocolClient acme) { if (AcmeState.PendingStatus == _state.Order?.Payload?.Status) { _logger.LogInformation("Order is pending, resolving Authorizations"); if (_state.Authorizations == null) { _state.Authorizations = new Dictionary <string, Authorization>(); } foreach (var authzUrl in _state.Order.Payload.Authorizations) { var authz = await acme.GetAuthorizationDetailsAsync(authzUrl); _state.Authorizations[authzUrl] = authz; if (AcmeState.PendingStatus == authz.Status) { foreach (var chlng in authz.Challenges) { if (string.IsNullOrEmpty(_options.ChallengeType) || _options.ChallengeType == chlng.Type) { var chlngValidation = AuthorizationDecoder.DecodeChallengeValidation( authz, chlng.Type, acme.Signer); if (_options.ChallengeHandler(_services, chlngValidation)) { _logger.LogInformation("Challenge Handler has handled challenge:"); _logger.LogInformation(JsonConvert.SerializeObject(chlngValidation, Formatting.Indented)); var chlngUpdated = await acme.AnswerChallengeAsync(chlng.Url); if (chlngUpdated.Error != null) { _logger.LogError("Submitting Challenge Answer reported an error:"); _logger.LogError(JsonConvert.SerializeObject(chlngUpdated.Error)); } } _logger.LogInformation("Refreshing Authorization status"); authz = await acme.GetAuthorizationDetailsAsync(authzUrl); if (AcmeState.PendingStatus != authz.Status) { break; } } } } } Save(_state.AuthorizationsFile, _state.Authorizations); _logger.LogInformation("Refreshing Order status"); _state.Order = await acme.GetOrderDetailsAsync(_state.Order.OrderUrl, _state.Order); Save(_state.OrderFile, _state.Order); } return(true); }
public async Task <IList <AcmeChallengeResult> > Dns01Authorization([ActivityTrigger] string[] authorizationUrls) { var acmeProtocolClient = await _acmeProtocolClientFactory.CreateClientAsync(); var challengeResults = new List <AcmeChallengeResult>(); foreach (var authorizationUrl in authorizationUrls) { // Authorization の詳細を取得 var authorization = await acmeProtocolClient.GetAuthorizationDetailsAsync(authorizationUrl); // DNS-01 Challenge の情報を拾う var challenge = authorization.Challenges.First(x => x.Type == "dns-01"); var challengeValidationDetails = AuthorizationDecoder.ResolveChallengeForDns01(authorization, challenge, acmeProtocolClient.Signer); // Challenge の情報を保存する challengeResults.Add(new AcmeChallengeResult { Url = challenge.Url, DnsRecordName = challengeValidationDetails.DnsRecordName, DnsRecordValue = challengeValidationDetails.DnsRecordValue }); } // Azure DNS zone の一覧を取得する var zones = await _dnsManagementClient.Zones.ListAllAsync(); // DNS-01 の検証レコード名毎に Azure DNS に TXT レコードを作成 foreach (var lookup in challengeResults.ToLookup(x => x.DnsRecordName)) { var dnsRecordName = lookup.Key; var zone = zones.Where(x => dnsRecordName.EndsWith($".{x.Name}", StringComparison.OrdinalIgnoreCase)) .OrderByDescending(x => x.Name.Length) .First(); var resourceGroup = ExtractResourceGroup(zone.Id); // Challenge の詳細から Azure DNS 向けにレコード名を作成 var acmeDnsRecordName = dnsRecordName.Replace($".{zone.Name}", "", StringComparison.OrdinalIgnoreCase); // 既存の TXT レコードがあれば取得する var recordSet = await _dnsManagementClient.RecordSets.GetOrDefaultAsync(resourceGroup, zone.Name, acmeDnsRecordName, RecordType.TXT) ?? new RecordSet(); // TXT レコードに TTL と値をセットする recordSet.TTL = 60; recordSet.TxtRecords = lookup.Select(x => new TxtRecord(new[] { x.DnsRecordValue })).ToArray(); await _dnsManagementClient.RecordSets.CreateOrUpdateAsync(resourceGroup, zone.Name, acmeDnsRecordName, RecordType.TXT, recordSet); } return(challengeResults); }
public async Task <(IReadOnlyList <AcmeChallengeResult>, int)> Dns01Authorization([ActivityTrigger] IReadOnlyList <string> authorizationUrls) { var acmeProtocolClient = await _acmeProtocolClientFactory.CreateClientAsync(); var challengeResults = new List <AcmeChallengeResult>(); foreach (var authorizationUrl in authorizationUrls) { // Authorization の詳細を取得 var authorization = await acmeProtocolClient.GetAuthorizationDetailsAsync(authorizationUrl); // DNS-01 Challenge の情報を拾う var challenge = authorization.Challenges.First(x => x.Type == "dns-01"); var challengeValidationDetails = AuthorizationDecoder.ResolveChallengeForDns01(authorization, challenge, acmeProtocolClient.Signer); // Challenge の情報を保存する challengeResults.Add(new AcmeChallengeResult { Url = challenge.Url, DnsRecordName = challengeValidationDetails.DnsRecordName, DnsRecordValue = challengeValidationDetails.DnsRecordValue }); } // DNS zone の一覧を取得する var zones = await _dnsProvider.ListZonesAsync(); // DNS-01 の検証レコード名毎に DNS に TXT レコードを作成 foreach (var lookup in challengeResults.ToLookup(x => x.DnsRecordName)) { var dnsRecordName = lookup.Key; var zone = zones.Where(x => dnsRecordName.EndsWith($".{x.Name}", StringComparison.OrdinalIgnoreCase)) .OrderByDescending(x => x.Name.Length) .First(); // Challenge の詳細から DNS 向けにレコード名を作成 var acmeDnsRecordName = dnsRecordName.Replace($".{zone.Name}", "", StringComparison.OrdinalIgnoreCase); await _dnsProvider.DeleteTxtRecordAsync(zone, acmeDnsRecordName); await _dnsProvider.CreateTxtRecordAsync(zone, acmeDnsRecordName, lookup.Select(x => x.DnsRecordValue)); } return(challengeResults, _dnsProvider.PropagationSeconds); }
internal async Task <IChallengeValidationDetails> DecodeChallengeValidation(acme.Authorization auth, acme.Challenge challenge) { var client = await GetClient(); return(AuthorizationDecoder.DecodeChallengeValidation(auth, challenge.Type, client.Signer)); }
internal IChallengeValidationDetails DecodeChallengeValidation(Authorization auth, Challenge challenge) { return(AuthorizationDecoder.DecodeChallengeValidation(auth, challenge.Type, _client.Signer)); }
public static async Task <ChallengeResult> Dns01Authorization([ActivityTrigger] DurableActivityContext context, ILogger log) { var authzUrl = context.GetInput <string>(); var acme = await CreateAcmeClientAsync(); var authz = await acme.GetAuthorizationDetailsAsync(authzUrl); // DNS-01 Challenge の情報を拾う var challenge = authz.Challenges.First(x => x.Type == "dns-01"); var challengeValidationDetails = AuthorizationDecoder.ResolveChallengeForDns01(authz, challenge, acme.Signer); // Azure DNS の TXT レコードを書き換え var dnsClient = await CreateDnsManagementClientAsync(); var zone = (await dnsClient.Zones.ListAsync()).First(x => challengeValidationDetails.DnsRecordName.EndsWith(x.Name)); var resourceId = ParseResourceId(zone.Id); // Challenge の詳細から Azure DNS 向けにレコード名を作成 var acmeDnsRecordName = challengeValidationDetails.DnsRecordName.Replace("." + zone.Name, ""); RecordSet recordSet; try { recordSet = await dnsClient.RecordSets.GetAsync(resourceId["resourceGroups"], zone.Name, acmeDnsRecordName, RecordType.TXT); } catch { recordSet = null; } if (recordSet != null) { if (recordSet.Metadata == null || !recordSet.Metadata.TryGetValue(nameof(context.InstanceId), out var instanceId) || instanceId != context.InstanceId) { recordSet.Metadata = new Dictionary <string, string> { { nameof(context.InstanceId), context.InstanceId } }; recordSet.TxtRecords.Clear(); } recordSet.TTL = 60; // 既存の TXT レコードに値を追加する recordSet.TxtRecords.Add(new TxtRecord(new[] { challengeValidationDetails.DnsRecordValue })); } else { // 新しく TXT レコードを作成する recordSet = new RecordSet { TTL = 60, Metadata = new Dictionary <string, string> { { nameof(context.InstanceId), context.InstanceId } }, TxtRecords = new[] { new TxtRecord(new[] { challengeValidationDetails.DnsRecordValue }) } }; } await dnsClient.RecordSets.CreateOrUpdateAsync(resourceId["resourceGroups"], zone.Name, acmeDnsRecordName, RecordType.TXT, recordSet); return(new ChallengeResult { Url = challenge.Url, DnsRecordName = challengeValidationDetails.DnsRecordName, DnsRecordValue = challengeValidationDetails.DnsRecordValue }); }
private async Task <ChallengeResult> DnsAuthorizationAsync(string authorizationUrl, string instanceId) { var acmeProtocolClient = await CreateAcmeClientAsync(); var authorizationDetails = await acmeProtocolClient.GetAuthorizationDetailsAsync(authorizationUrl); var challenge = authorizationDetails.Challenges.First(x => x.Type == "dns-01"); var challengeValidationDetails = AuthorizationDecoder.ResolveChallengeForDns01(authorizationDetails, challenge, acmeProtocolClient.Signer); var dnsManagementClient = await CreateDnsManagementClientAsync(); var zone = (await dnsManagementClient.Zones.ListAsync()).First(x => challengeValidationDetails.DnsRecordName.EndsWith(x.Name)); var resourceId = ParseResourceId(zone.Id); var acmeDnsRecordName = challengeValidationDetails.DnsRecordName.Replace("." + zone.Name, ""); RecordSetInner recordSet; try { recordSet = await dnsManagementClient.RecordSets.GetAsync(resourceId["resourceGroups"], zone.Name, acmeDnsRecordName, RecordType.TXT); } catch { recordSet = null; } if (recordSet != null) { if (recordSet.Metadata == null || !recordSet.Metadata.TryGetValue("InstanceId", out var dnsInstanceId) || dnsInstanceId != instanceId) { recordSet.Metadata = new Dictionary <string, string> { { "InstanceId", instanceId } }; recordSet.TxtRecords.Clear(); } recordSet.TTL = 60; recordSet.TxtRecords.Add(new TxtRecord(new[] { challengeValidationDetails.DnsRecordValue })); } else { recordSet = new RecordSetInner() { TTL = 60, Metadata = new Dictionary <string, string> { { "InstanceId", instanceId } }, TxtRecords = new[] { new TxtRecord(new[] { challengeValidationDetails.DnsRecordValue }) } }; } await dnsManagementClient.RecordSets.CreateOrUpdateAsync(resourceId["resourceGroups"], zone.Name, acmeDnsRecordName, RecordType.TXT, recordSet); return(new ChallengeResult() { Url = challenge.Url, DnsRecordName = challengeValidationDetails.DnsRecordName, DnsRecordValue = challengeValidationDetails.DnsRecordValue }); }
public async Task <IActionResult> Post([FromBody] DomainRequest request) { if (!IsValidDomain(request.Domain)) { return(BadRequest(new DomainResponse { Error = "Invalid domain" })); } var domain = string.Join(".", request.Domain.Split(".").TakeLast(2)); var subDomain = string.Join(".", request.Domain.Split(".").SkipLast(2)); var credentials = new AzureCredentialsFactory() .FromServicePrincipal( _configuration["Azure:ClientId"], _configuration["Azure:ClientSecret"], _configuration["Azure:TenantId"], AzureEnvironment.AzureGlobalCloud ); var azure = Azure .Configure() .WithRetryPolicy(new RetryPolicy(new TransientErrorIgnoreStrategy(), 0)) .Authenticate(credentials) .WithSubscription(_configuration["Azure:SubscriptionId"]); var webApp = await azure.AppServices.WebApps.GetByIdAsync( _configuration["Azure:AppId"]); try { webApp.Update() .DefineHostnameBinding() .WithThirdPartyDomain(domain) .WithSubDomain(subDomain) .WithDnsRecordType(CustomHostNameDnsRecordType.CName) .Attach() .Apply(); } catch (Exception e) { return(BadRequest(new DomainResponse { Error = "Unable to validate domain ownership" })); } _ = Task.Run(async() => { using var airtableBase = new AirtableBase(_configuration["Airtable:Key"], _configuration["Airtable:Base"]); try { HttpClient httpClient = new HttpClient { BaseAddress = new Uri(_configuration["Acme:Endpoint"]) }; AcmeProtocolClient acme = new AcmeProtocolClient(httpClient, usePostAsGet: true); var acmeDir = await acme.GetDirectoryAsync(); acme.Directory = acmeDir; await acme.GetNonceAsync(); var account = await acme.CreateAccountAsync(new[] { "mailto:" + _configuration["Acme:Email"] }, true); acme.Account = account; var order = await acme.CreateOrderAsync(new[] { request.Domain }); if (order.Payload.Status == "invalid") { return; } var authorizationUrl = order.Payload.Authorizations.FirstOrDefault(); if (string.IsNullOrEmpty(authorizationUrl)) { return; } var authorization = await acme.GetAuthorizationDetailsAsync(authorizationUrl); foreach (var challenge in authorization.Challenges.Where(x => x.Type == "http-01").ToList()) { var challengeValidationDetails = (Http01ChallengeValidationDetails) AuthorizationDecoder.DecodeChallengeValidation(authorization, challenge.Type, acme.Signer); var path = challengeValidationDetails.HttpResourcePath; var token = path.Split("/", StringSplitOptions.RemoveEmptyEntries).Last(); var value = challengeValidationDetails.HttpResourceValue; var contentType = challengeValidationDetails.HttpResourceContentType; await airtableBase.CreateRecord("Acme", new Fields { FieldsCollection = new Dictionary <string, object> { ["Token"] = token, ["Value"] = value, ["ContentType"] = contentType } }); await Task.Delay(10 * 1000); var challengeUpdated = await acme.AnswerChallengeAsync(challenge.Url); } //Wait for challenge to be resolved var waitUntil = DateTime.Now.AddSeconds(300); Authorization authorizationUpdated; do { await Task.Delay(10 * 1000); authorizationUpdated = await acme.GetAuthorizationDetailsAsync(authorizationUrl); } while (authorizationUpdated.Status != "valid" && DateTime.Now < waitUntil); if (authorizationUpdated.Status != "valid") { return; } //Generate certificate private key and CSR (Certificate signing request) var keyPair = PkiKeyPair.GenerateEcdsaKeyPair(256); var csr = new PkiCertificateSigningRequest($"CN={request.Domain}", keyPair, PkiHashAlgorithm.Sha256); var certCsr = csr.ExportSigningRequest(PkiEncodingFormat.Der); order = await acme.FinalizeOrderAsync(order.Payload.Finalize, certCsr); if (order.Payload.Status != "valid") { return; } if (string.IsNullOrEmpty(order.Payload.Certificate)) { //Wait for certificate var waitUntil2 = DateTime.Now.AddSeconds(300); while (DateTime.Now < waitUntil2) { await Task.Delay(10 * 1000); order = await acme.GetOrderDetailsAsync(order.OrderUrl, existing: order); if (!string.IsNullOrEmpty(order.Payload.Certificate)) { break; } } } if (string.IsNullOrEmpty(order.Payload.Certificate)) { return; } var certResp = await acme.GetAsync(order.Payload.Certificate); if (!certResp.IsSuccessStatusCode) { return; } var certByteArray = await certResp.Content.ReadAsByteArrayAsync(); //Export PFX file var pfxPassword = _configuration["Acme:PfxPassword"]; var privateKey = keyPair.PrivateKey; using var cert = new X509Certificate2(certByteArray); X509Chain chain = new X509Chain(); chain.Build(cert); List <PkiCertificate> chainList = new List <PkiCertificate>(); foreach (var e in chain.ChainElements) { chainList.Add(PkiCertificate.From(e.Certificate)); } var pfx = chainList[0].Export(PkiArchiveFormat.Pkcs12, chain: chainList.Skip(1), privateKey: privateKey, password: pfxPassword?.ToCharArray()); webApp.Update() .DefineSslBinding() .ForHostname(request.Domain) .WithPfxByteArrayToUpload(pfx, pfxPassword) .WithSniBasedSsl() .Attach() .Apply(); } catch (Exception e) { await airtableBase.CreateRecord("Logs", new Fields { FieldsCollection = new Dictionary <string, object> { ["Hostname"] = request.Domain, ["Event"] = "exception-thrown", ["Data"] = JsonConvert.SerializeObject(e) } }); } }); return(Ok(new DomainResponse { IsSuccessful = true })); }