Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor DNS-01 Challenge processing #71

Merged
merged 2 commits into from
Mar 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions KeyVault.Acmebot/Contracts/ISharedFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public interface ISharedFunctions

Task Dns01Precondition(string[] hostNames);

Task<AcmeChallengeResult> Dns01Authorization((string, string) input);
Task<IList<AcmeChallengeResult>> Dns01Authorization(string[] authorizationUrls);

[RetryOptions("00:00:10", 6, HandlerType = typeof(RetryStrategy), HandlerMethodName = nameof(RetryStrategy.RetriableException))]
Task CheckDnsChallenge(AcmeChallengeResult challenge);
Task CheckDnsChallenge(IList<AcmeChallengeResult> challengeResults);

[RetryOptions("00:00:05", 12, HandlerType = typeof(RetryStrategy), HandlerMethodName = nameof(RetryStrategy.RetriableException))]
Task CheckIsReady(OrderDetails orderDetails);

Task AnswerChallenges(IList<AcmeChallengeResult> challenges);
Task AnswerChallenges(IList<AcmeChallengeResult> challengeResults);

Task FinalizeOrder((string[], OrderDetails) input);
}
Expand Down
12 changes: 12 additions & 0 deletions KeyVault.Acmebot/Internal/AzureSdkExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,17 @@ public static async Task<IList<CertificateItem>> GetAllCertificatesAsync(this IK

return certificates;
}

public static async Task<RecordSet> GetOrDefaultAsync(this IRecordSetsOperations operations, string resourceGroupName, string zoneName, string relativeRecordSetName, RecordType recordType)
{
try
{
return await operations.GetAsync(resourceGroupName, zoneName, relativeRecordSetName, RecordType.TXT);
}
catch
{
return null;
}
}
}
}
154 changes: 61 additions & 93 deletions KeyVault.Acmebot/SharedFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ public SharedFunctions(IHttpClientFactory httpClientFactory, LookupClient lookup
_dnsManagementClient = dnsManagementClient;
}

private const string InstanceIdKey = "InstanceId";

private readonly IHttpClientFactory _httpClientFactory;
private readonly LookupClient _lookupClient;
private readonly IAcmeProtocolClientFactory _acmeProtocolClientFactory;
Expand All @@ -63,22 +61,14 @@ public async Task IssueCertificate([OrchestrationTrigger] IDurableOrchestrationC
// 新しく ACME Order を作成する
var orderDetails = await activity.Order(dnsNames);

// 複数の Authorizations を処理する
var challenges = new List<AcmeChallengeResult>();

foreach (var authorization in orderDetails.Payload.Authorizations)
{
// ACME Challenge を実行
var result = await activity.Dns01Authorization((authorization, context.ParentInstanceId ?? context.InstanceId));
// ACME Challenge を実行
var challengeResults = await activity.Dns01Authorization(orderDetails.Payload.Authorizations);

// Azure DNS で正しくレコードが引けるか確認
await activity.CheckDnsChallenge(result);

challenges.Add(result);
}
// Azure DNS で正しくレコードが引けるか確認
await activity.CheckDnsChallenge(challengeResults);

// ACME Answer を実行
await activity.AnswerChallenges(challenges);
await activity.AnswerChallenges(challengeResults);

// Order のステータスが ready になるまで 60 秒待機
await activity.CheckIsReady(orderDetails);
Expand Down Expand Up @@ -135,106 +125,84 @@ public async Task Dns01Precondition([ActivityTrigger] string[] hostNames)
}

[FunctionName(nameof(Dns01Authorization))]
public async Task<AcmeChallengeResult> Dns01Authorization([ActivityTrigger] (string, string) input)
public async Task<IList<AcmeChallengeResult>> Dns01Authorization([ActivityTrigger] string[] authorizationUrls)
{
var (authzUrl, instanceId) = input;

var acmeProtocolClient = await _acmeProtocolClientFactory.CreateClientAsync();

var authz = await acmeProtocolClient.GetAuthorizationDetailsAsync(authzUrl);
var challengeResults = new List<AcmeChallengeResult>();

// DNS-01 Challenge の情報を拾う
var challenge = authz.Challenges.First(x => x.Type == "dns-01");
foreach (var authorizationUrl in authorizationUrls)
{
// Authorization の詳細を取得
var authorization = await acmeProtocolClient.GetAuthorizationDetailsAsync(authorizationUrl);

var challengeValidationDetails = AuthorizationDecoder.ResolveChallengeForDns01(authz, challenge, acmeProtocolClient.Signer);
// DNS-01 Challenge の情報を拾う
var challenge = authorization.Challenges.First(x => x.Type == "dns-01");

// Azure DNS の TXT レコードを書き換え
var zones = await _dnsManagementClient.Zones.ListAllAsync();
var challengeValidationDetails = AuthorizationDecoder.ResolveChallengeForDns01(authorization, challenge, acmeProtocolClient.Signer);

var zone = zones.Where(x => challengeValidationDetails.DnsRecordName.EndsWith($".{x.Name}", StringComparison.OrdinalIgnoreCase))
.OrderByDescending(x => x.Name.Length)
.First();
// Challenge の情報を保存する
challengeResults.Add(new AcmeChallengeResult
{
Url = challenge.Url,
DnsRecordName = challengeValidationDetails.DnsRecordName,
DnsRecordValue = challengeValidationDetails.DnsRecordValue
});
}

var resourceGroup = ExtractResourceGroup(zone.Id);
// Azure DNS zone の一覧を取得する
var zones = await _dnsManagementClient.Zones.ListAllAsync();

// Challenge の詳細から Azure DNS 向けにレコード名を作成
var acmeDnsRecordName = challengeValidationDetails.DnsRecordName.Replace($".{zone.Name}", "", StringComparison.OrdinalIgnoreCase);
// DNS-01 の検証レコード名毎に Azure DNS に TXT レコードを作成
foreach (var lookup in challengeResults.ToLookup(x => x.DnsRecordName))
{
var dnsRecordName = lookup.Key;

RecordSet recordSet;
var zone = zones.Where(x => dnsRecordName.EndsWith($".{x.Name}", StringComparison.OrdinalIgnoreCase))
.OrderByDescending(x => x.Name.Length)
.First();

try
{
recordSet = await _dnsManagementClient.RecordSets.GetAsync(resourceGroup, zone.Name, acmeDnsRecordName, RecordType.TXT);
}
catch
{
recordSet = null;
}
var resourceGroup = ExtractResourceGroup(zone.Id);

if (recordSet != null)
{
if (recordSet.Metadata == null || !recordSet.Metadata.TryGetValue(InstanceIdKey, out var dnsInstanceId) || dnsInstanceId != instanceId)
{
recordSet.Metadata = new Dictionary<string, string>
{
{ InstanceIdKey, instanceId }
};
// Challenge の詳細から Azure DNS 向けにレコード名を作成
var acmeDnsRecordName = dnsRecordName.Replace($".{zone.Name}", "", StringComparison.OrdinalIgnoreCase);

recordSet.TxtRecords.Clear();
}
// 既存の 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();

// 既存の TXT レコードに値を追加する
recordSet.TxtRecords.Add(new TxtRecord(new[] { challengeValidationDetails.DnsRecordValue }));
}
else
{
// 新しく TXT レコードを作成する
recordSet = new RecordSet
{
TTL = 60,
Metadata = new Dictionary<string, string>
{
{ InstanceIdKey, instanceId }
},
TxtRecords = new[]
{
new TxtRecord(new[] { challengeValidationDetails.DnsRecordValue })
}
};
await _dnsManagementClient.RecordSets.CreateOrUpdateAsync(resourceGroup, zone.Name, acmeDnsRecordName, RecordType.TXT, recordSet);
}

await _dnsManagementClient.RecordSets.CreateOrUpdateAsync(resourceGroup, zone.Name, acmeDnsRecordName, RecordType.TXT, recordSet);

return new AcmeChallengeResult
{
Url = challenge.Url,
DnsRecordName = challengeValidationDetails.DnsRecordName,
DnsRecordValue = challengeValidationDetails.DnsRecordValue
};
return challengeResults;
}

[FunctionName(nameof(CheckDnsChallenge))]
public async Task CheckDnsChallenge([ActivityTrigger] AcmeChallengeResult challenge)
public async Task CheckDnsChallenge([ActivityTrigger] IList<AcmeChallengeResult> challengeResults)
{
// 実際に ACME の TXT レコードを引いて確認する
var queryResult = await _lookupClient.QueryAsync(challenge.DnsRecordName, QueryType.TXT);
foreach (var challengeResult in challengeResults)
{
// 実際に ACME の TXT レコードを引いて確認する
var queryResult = await _lookupClient.QueryAsync(challengeResult.DnsRecordName, QueryType.TXT);

var txtRecords = queryResult.Answers
.OfType<DnsClient.Protocol.TxtRecord>()
.ToArray();
var txtRecords = queryResult.Answers
.OfType<DnsClient.Protocol.TxtRecord>()
.ToArray();

// レコードが存在しなかった場合はエラー
if (txtRecords.Length == 0)
{
throw new RetriableActivityException($"{challenge.DnsRecordName} did not resolve.");
}
// レコードが存在しなかった場合はエラー
if (txtRecords.Length == 0)
{
throw new RetriableActivityException($"{challengeResult.DnsRecordName} did not resolve.");
}

// レコードに今回のチャレンジが含まれていない場合もエラー
if (!txtRecords.Any(x => x.Text.Contains(challenge.DnsRecordValue)))
{
throw new RetriableActivityException($"{challenge.DnsRecordName} value is not correct.");
// レコードに今回のチャレンジが含まれていない場合もエラー
if (!txtRecords.Any(x => x.Text.Contains(challengeResult.DnsRecordValue)))
{
throw new RetriableActivityException($"{challengeResult.DnsRecordName} value is not correct.");
}
}
}

Expand All @@ -259,14 +227,14 @@ public async Task CheckIsReady([ActivityTrigger] OrderDetails orderDetails)
}

[FunctionName(nameof(AnswerChallenges))]
public async Task AnswerChallenges([ActivityTrigger] IList<AcmeChallengeResult> challenges)
public async Task AnswerChallenges([ActivityTrigger] IList<AcmeChallengeResult> challengeResults)
{
var acmeProtocolClient = await _acmeProtocolClientFactory.CreateClientAsync();

// Answer の準備が出来たことを通知
foreach (var challenge in challenges)
foreach (var challengeResult in challengeResults)
{
await acmeProtocolClient.AnswerChallengeAsync(challenge.Url);
await acmeProtocolClient.AnswerChallengeAsync(challengeResult.Url);
}
}

Expand Down