Skip to content

Commit bd9be50

Browse files
Begin Managed (DNS) Challenge implementation
1 parent 7fe2bbd commit bd9be50

File tree

13 files changed

+1156
-17
lines changed

13 files changed

+1156
-17
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using Certify.Core.Management.Challenges;
7+
using Certify.Models;
8+
using Certify.Models.Config;
9+
using Certify.Models.Hub;
10+
using Serilog;
11+
12+
namespace Certify.Management
13+
{
14+
public partial class CertifyManager
15+
{
16+
public async Task<ICollection<ManagedChallenge>> GetManagedChallenges()
17+
{
18+
return await _configStore.GetItems<ManagedChallenge>(nameof(ManagedChallenge));
19+
}
20+
21+
public async Task<ActionResult> UpdateManagedChallenge(ManagedChallenge update)
22+
{
23+
if (string.IsNullOrEmpty(update.Id))
24+
{
25+
update.Id = Guid.NewGuid().ToString();
26+
}
27+
28+
await _configStore.Update<ManagedChallenge>(nameof(ManagedChallenge), update);
29+
return new ActionResult { IsSuccess = true };
30+
}
31+
32+
public async Task<ActionResult> DeleteManagedChallenge(string id)
33+
{
34+
var deleted = await _configStore.Delete<ManagedChallenge>(nameof(ManagedChallenge), id);
35+
36+
return new ActionResult { IsSuccess = deleted };
37+
}
38+
39+
private ManagedChallenge ManagedChallengeFindBestMatch(ManagedChallengeRequest request, ICollection<ManagedChallenge> managedChallenges)
40+
{
41+
// find most specific matching challenge for the request - based on ManagedCertificate.GetChallengeConfig
42+
//TODO: filter based on access
43+
var matchedConfig = managedChallenges.FirstOrDefault(c => string.IsNullOrEmpty(c.ChallengeConfig.DomainMatch));
44+
45+
if (request.Identifier != null && !string.IsNullOrEmpty(request.Identifier))
46+
{
47+
// expand configs into per identifier list
48+
var configsPerDomain = new Dictionary<string, ManagedChallenge>();
49+
foreach (var managedChallenge in managedChallenges.Where(c => !string.IsNullOrEmpty(c.ChallengeConfig.DomainMatch)))
50+
{
51+
var c = managedChallenge.ChallengeConfig;
52+
if (c != null)
53+
{
54+
if (c.DomainMatch != null && !string.IsNullOrEmpty(c.DomainMatch))
55+
{
56+
c.DomainMatch = c.DomainMatch.Replace(",", ";"); // if user has entered comma separators instead of semicolons, convert now.
57+
58+
if (!c.DomainMatch.Contains(';'))
59+
{
60+
var domainMatchKey = c.DomainMatch.Trim();
61+
62+
// if identifier key is test.com for example we only support one matching config
63+
if (!configsPerDomain.ContainsKey(domainMatchKey))
64+
{
65+
configsPerDomain.Add(domainMatchKey, managedChallenge);
66+
}
67+
}
68+
else
69+
{
70+
var domains = c.DomainMatch.Split(';');
71+
foreach (var d in domains)
72+
{
73+
if (!string.IsNullOrWhiteSpace(d))
74+
{
75+
var domainMatchKey = d.Trim().ToLowerInvariant();
76+
if (!configsPerDomain.ContainsKey(domainMatchKey))
77+
{
78+
configsPerDomain.Add(domainMatchKey, managedChallenge);
79+
}
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}
86+
87+
// if exact match exists, use that
88+
var identifierKey = request.Identifier.ToLowerInvariant() ?? "";
89+
if (configsPerDomain.TryGetValue(identifierKey, out var value))
90+
{
91+
return value;
92+
}
93+
94+
// if explicit wildcard match exists, use that
95+
if (configsPerDomain.TryGetValue("*." + identifierKey, out var wildValue))
96+
{
97+
return wildValue;
98+
}
99+
100+
//if a more specific config matches the identifier, use that, in order of longest identifier name match first
101+
var allMatchingConfigKeys = configsPerDomain.Keys.OrderByDescending(l => l.Length);
102+
103+
foreach (var wildcard in allMatchingConfigKeys.Where(k => k.StartsWith("*.", StringComparison.CurrentCultureIgnoreCase)))
104+
{
105+
if (ManagedCertificate.IsDomainOrWildcardMatch(new List<string> { wildcard }, request.Identifier))
106+
{
107+
return configsPerDomain[wildcard];
108+
}
109+
}
110+
111+
foreach (var configDomain in allMatchingConfigKeys)
112+
{
113+
if (configDomain.EndsWith(request.Identifier.ToLowerInvariant(), StringComparison.CurrentCultureIgnoreCase))
114+
{
115+
// use longest matching identifier (so subdomain.test.com takes priority
116+
// over test.com, )
117+
return configsPerDomain[configDomain];
118+
}
119+
}
120+
}
121+
122+
// no other matches, just use first
123+
if (matchedConfig != null)
124+
{
125+
return matchedConfig;
126+
}
127+
else
128+
{
129+
// no match, return null
130+
return default;
131+
}
132+
}
133+
public async Task<ActionResult> PerformManagedChallengeRequest(ManagedChallengeRequest request)
134+
{
135+
var log = _serviceLog;
136+
137+
var managedChallenges = await GetManagedChallenges();
138+
139+
var matchingChallenge = ManagedChallengeFindBestMatch(request, managedChallenges);
140+
141+
if (matchingChallenge == null)
142+
{
143+
return new ActionResult { IsSuccess = false, Message = "No matching challenge found" };
144+
}
145+
else
146+
{
147+
// perform challenge
148+
var _dnsHelper = new DnsChallengeHelper(_credentialsManager);
149+
150+
DnsChallengeHelperResult dnsResult;
151+
var managedCertificate = new ManagedCertificate
152+
{
153+
RequestConfig = new CertRequestConfig
154+
{
155+
Challenges = new ObservableCollection<CertRequestChallengeConfig>(
156+
new List<CertRequestChallengeConfig>
157+
{
158+
matchingChallenge.ChallengeConfig
159+
})
160+
}
161+
};
162+
163+
var domain = new CertIdentifierItem { IdentifierType = CertIdentifierType.Dns, Value = request.Identifier };
164+
165+
dnsResult = await _dnsHelper.CompleteDNSChallenge(log, managedCertificate, domain, request.ResponseKey, request.ResponseValue, isTestMode: false);
166+
167+
if (!dnsResult.Result.IsSuccess)
168+
{
169+
if (dnsResult.IsAwaitingUser)
170+
{
171+
log?.Error($"Action Required: {dnsResult.Result.Message}");
172+
}
173+
else
174+
{
175+
log?.Error($"DNS update failed: {dnsResult.Result.Message}");
176+
}
177+
178+
return dnsResult.Result;
179+
}
180+
else
181+
{
182+
log.Information($"DNS: {dnsResult.Result.Message}");
183+
}
184+
185+
var cleanupQueue = new List<Action> { };
186+
187+
// configure cleanup actions for use after challenge completes
188+
/* pendingAuth.Cleanup = async () =>
189+
{
190+
_ = await _dnsHelper.DeleteDNSChallenge(log, managedCertificate, domain, dnsChallenge.Key, dnsChallenge.Value);
191+
};
192+
*/
193+
194+
return new ActionResult { IsSuccess = true, Message = $"Challenge response {request.ChallengeType} completed {request.ResponseKey} : {request.ResponseValue}" };
195+
196+
}
197+
}
198+
199+
public async Task<ActionResult> CleanupManagedChallengeRequest(ManagedChallengeRequest request)
200+
{
201+
var log = _serviceLog;
202+
203+
var managedChallenges = await GetManagedChallenges();
204+
205+
var matchingChallenge = ManagedChallengeFindBestMatch(request, managedChallenges);
206+
207+
if (matchingChallenge == null)
208+
{
209+
return new ActionResult { IsSuccess = false, Message = "No matching challenge found" };
210+
}
211+
else
212+
{
213+
// perform challenge
214+
var _dnsHelper = new DnsChallengeHelper(_credentialsManager);
215+
216+
var managedCertificate = new ManagedCertificate
217+
{
218+
RequestConfig = new CertRequestConfig
219+
{
220+
Challenges = new ObservableCollection<CertRequestChallengeConfig>(
221+
new List<CertRequestChallengeConfig>
222+
{
223+
matchingChallenge.ChallengeConfig
224+
})
225+
}
226+
};
227+
228+
var domain = new CertIdentifierItem { IdentifierType = CertIdentifierType.Dns, Value = request.Identifier };
229+
230+
var dnsResult = await _dnsHelper.DeleteDNSChallenge(log, managedCertificate, domain, request.ResponseKey, request.ResponseValue);
231+
232+
if (!dnsResult.Result.IsSuccess)
233+
{
234+
if (dnsResult.IsAwaitingUser)
235+
{
236+
log?.Error($"Action Required: {dnsResult.Result.Message}");
237+
}
238+
else
239+
{
240+
log?.Error($"DNS cleanup failed: {dnsResult.Result.Message}");
241+
}
242+
243+
return dnsResult.Result;
244+
}
245+
else
246+
{
247+
log.Information($"DNS: {dnsResult.Result.Message}");
248+
}
249+
250+
return new ActionResult { IsSuccess = true, Message = $"Challenge cleanup {request.ChallengeType} completed {request.ResponseKey} : {request.ResponseValue}" };
251+
252+
}
253+
}
254+
}
255+
}

src/Certify.Core/Management/CertifyManager/ICertifyManager.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,14 @@ public interface ICertifyManager
108108
Task<List<ActionStep>> UpdateDataStoreConnection(DataStoreConnection dataStore);
109109
Task<List<ActionStep>> RemoveDataStoreConnection(string dataStoreId);
110110
Task<List<ActionStep>> TestDataStoreConnection(DataStoreConnection connection);
111+
111112
Task<ActionResult> TestCredentials(string storageKey);
112113
Task<Core.Management.Access.IAccessControl> GetCurrentAccessControl();
114+
115+
Task<ICollection<ManagedChallenge>> GetManagedChallenges();
116+
Task<ActionResult> UpdateManagedChallenge(ManagedChallenge update);
117+
Task<ActionResult> DeleteManagedChallenge(string id);
118+
Task<ActionResult> PerformManagedChallengeRequest(ManagedChallengeRequest request);
119+
Task<ActionResult> CleanupManagedChallengeRequest(ManagedChallengeRequest request);
113120
}
114121
}

src/Certify.Core/Management/Challenges/ChallengeResponseService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
@@ -621,7 +621,7 @@ private Func<bool> PrepareChallengeResponse_TlsSni01(ILog log, ITargetWebServer
621621

622622
private DnsChallengeHelper _dnsHelper = null;
623623

624-
private async Task<DnsChallengeHelperResult> PerformChallengeResponse_Dns01(ILog log, CertIdentifierItem domain, ManagedCertificate managedCertificate, PendingAuthorization pendingAuth, bool isTestMode, bool isCleanupOnly, ICredentialsManager credentialsManager)
624+
internal async Task<DnsChallengeHelperResult> PerformChallengeResponse_Dns01(ILog log, CertIdentifierItem domain, ManagedCertificate managedCertificate, PendingAuthorization pendingAuth, bool isTestMode, bool isCleanupOnly, ICredentialsManager credentialsManager)
625625
{
626626
var dnsChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_DNS);
627627

src/Certify.Core/Management/SettingsManager.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ public static CoreAppSettings Current
183183
/// </summary>
184184
public bool PerformChallengeCleanupsLast { get; set; }
185185
public string CurrentServiceVersion { get; set; }
186+
187+
/// <summary>
188+
/// if true, additional management hub features and data stores may be enabled
189+
/// </summary>
190+
public bool IsManagementHub { get; set; }
186191
}
187192

188193
public class SettingsManager
Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Text;
4-
using Certify.Models;
52

63
namespace Certify.Models.Hub
74
{
85
/// <summary>
96
/// Configuration for a managed challenge, such as a DNS challenge for a specific domain/zone
107
/// A managed challenge is one the management hub can complete on behalf of another ACME client
118
/// </summary>
12-
public class ManagedChallenge
9+
public class ManagedChallenge : ConfigurationStoreItem
1310
{
14-
public ManagedChallenge()
15-
{
16-
Id = Guid.NewGuid().ToString();
17-
}
18-
public string Id { get; set; }
19-
public string Title { get; set; } = string.Empty;
20-
public string Description { get; set; } = string.Empty;
21-
public string ItemType { get; set; } = string.Empty;
11+
public CertRequestChallengeConfig? ChallengeConfig { get; set; }
12+
}
13+
14+
public class ManagedChallengeRequest
15+
{
16+
/// <summary>
17+
/// The type of challenge to perform (e.g. dns-01)
18+
/// </summary>
19+
public string ChallengeType { get; set; } = string.Empty;
2220

23-
public CertRequestChallengeConfig ChallengeConfig { get; set; }
21+
/// <summary>
22+
/// domain etc challenge is being performed for
23+
/// </summary>
24+
public string Identifier { get; set; } = string.Empty;
25+
public string ResponseKey { get; set; } = string.Empty;
26+
public string ResponseValue { get; set; } = string.Empty;
27+
public string AuthKey { get; set; } = string.Empty;
28+
public string AuthSecret { get; set; } = string.Empty;
2429
}
2530
}

0 commit comments

Comments
 (0)