diff --git a/Directory.Build.props b/Directory.Build.props
index fc6d385..9b4b9a7 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,7 +8,7 @@
-->
512
- 11.0
+ 12.0
@@ -16,7 +16,7 @@
Ken Christensen
Kenc.ACMELib
Ken Christensen
- © 2022 Ken Christensen. All rights reserved.
+ © 2024 Ken Christensen. All rights reserved.
diff --git a/global.json b/global.json
index f7f16fd..2841819 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "7.0.0",
+ "version": "8.0.0",
"rollForward": "latestFeature",
"allowPrerelease": false
}
diff --git a/src/ACME.sln b/src/ACME.sln
index a453e63..a6bc513 100644
--- a/src/ACME.sln
+++ b/src/ACME.sln
@@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kenc.ACMELibCore.Tests", "L
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudflareIntegration", "Examples\CloudflareIntegration\CloudflareIntegration.csproj", "{79B0EA59-9B39-4E97-8080-012D91BCA16B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Examples\Shared\Shared.csproj", "{887A6A88-2865-498E-BF8C-52B194295553}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -35,6 +37,10 @@ Global
{79B0EA59-9B39-4E97-8080-012D91BCA16B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{79B0EA59-9B39-4E97-8080-012D91BCA16B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{79B0EA59-9B39-4E97-8080-012D91BCA16B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {887A6A88-2865-498E-BF8C-52B194295553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {887A6A88-2865-498E-BF8C-52B194295553}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {887A6A88-2865-498E-BF8C-52B194295553}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {887A6A88-2865-498E-BF8C-52B194295553}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -42,6 +48,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{BD4872DB-73DF-497D-915D-C20EB4283167} = {C9A9AB54-D122-48CC-9CA7-F34BC5DB3293}
{79B0EA59-9B39-4E97-8080-012D91BCA16B} = {C9A9AB54-D122-48CC-9CA7-F34BC5DB3293}
+ {887A6A88-2865-498E-BF8C-52B194295553} = {C9A9AB54-D122-48CC-9CA7-F34BC5DB3293}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A7972CCF-1FB8-4419-B115-B60882F8FCF0}
diff --git a/src/Examples/ACMEClient/ACMEClient.csproj b/src/Examples/ACMEClient/ACMEClient.csproj
index 79800d6..d4761fb 100644
--- a/src/Examples/ACMEClient/ACMEClient.csproj
+++ b/src/Examples/ACMEClient/ACMEClient.csproj
@@ -1,5 +1,4 @@
-
Exe
net7.0
@@ -8,6 +7,7 @@
+
\ No newline at end of file
diff --git a/src/Examples/ACMEClient/Program.cs b/src/Examples/ACMEClient/Program.cs
index 556da29..ce351fa 100644
--- a/src/Examples/ACMEClient/Program.cs
+++ b/src/Examples/ACMEClient/Program.cs
@@ -13,6 +13,7 @@
using Kenc.ACMELib;
using Kenc.ACMELib.ACMEObjects;
using Kenc.ACMELib.ACMEResponses;
+ using Kenc.ACMELib.Examples.Shared;
using Kenc.ACMELib.Exceptions;
using Kenc.ACMELib.Exceptions.API;
@@ -87,7 +88,7 @@ private static async Task Main()
var userContact = Console.ReadLine();
try
{
- account = await acmeClient.RegisterAsync(new[] { "mailto:" + userContact });
+ account = await acmeClient.RegisterAsync(["mailto:" + userContact]);
}
catch (Exception ex)
{
@@ -112,8 +113,8 @@ private static async Task Main()
var certificateFiles = Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.crt").ToList();
Console.WriteLine($"Revoking certificates: {string.Join(',', certificateFiles)}");
- var foo = HandleConsoleInput("Continue?", new[] { "y", "yes", "n", "no" }, false).ToLower();
- if (foo == "y" || foo == "yes")
+ var foo = HandleConsoleInput("Continue?", ["y", "yes", "n", "no"], false).ToLower();
+ if (foo is "y" or "yes")
{
IEnumerable certificates = certificateFiles.Select(path => X509Certificate2.CreateFromCertFile(path));
foreach (X509Certificate certificate in certificates)
@@ -199,8 +200,8 @@ private static async Task OrderDomains(ACMEClient acmeClient, params string[] do
Console.WriteLine($"Unknown challenge type encountered '{challenge.Type}'. Please handle accourdingly.");
}
- var result = HandleConsoleInput("Challenge completed? [y/n]", new[] { "y", "yes", "n", "no" });
- if (result == "y" || result == "yes")
+ var result = HandleConsoleInput("Challenge completed? [y/n]", ["y", "yes", "n", "no"]);
+ if (result is "y" or "yes")
{
Console.WriteLine("Validating challenge");
var validation = await ValidateChallengeCompletion(challenge, item.Identifier.Value);
@@ -362,12 +363,8 @@ private static string HandleConsoleInput(string prompt, string[] acceptedRespons
{
Console.WriteLine(prompt);
var input = Console.ReadLine();
- if (!caseSensitive)
- {
- input = input.ToLowerInvariant();
- }
- if (acceptedResponses.Contains(input))
+ if (acceptedResponses.Contains(input, caseSensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal))
{
return input;
}
diff --git a/src/Examples/CloudflareIntegration/CloudflareIntegration.csproj b/src/Examples/CloudflareIntegration/CloudflareIntegration.csproj
index 3c8465b..996e62e 100644
--- a/src/Examples/CloudflareIntegration/CloudflareIntegration.csproj
+++ b/src/Examples/CloudflareIntegration/CloudflareIntegration.csproj
@@ -1,9 +1,9 @@
-
+
Exe
net7.0
- true
+
@@ -15,6 +15,6 @@
+
-
diff --git a/src/Examples/CloudflareIntegration/OrderDomains.cs b/src/Examples/CloudflareIntegration/OrderDomains.cs
index 810b2df..830e980 100644
--- a/src/Examples/CloudflareIntegration/OrderDomains.cs
+++ b/src/Examples/CloudflareIntegration/OrderDomains.cs
@@ -16,6 +16,7 @@
using Kenc.ACMELib;
using Kenc.ACMELib.ACMEObjects;
using Kenc.ACMELib.ACMEResponses;
+ using Kenc.ACMELib.Examples.Shared;
using Kenc.ACMELib.Exceptions.API;
using Kenc.Cloudflare.Core;
using Kenc.Cloudflare.Core.Clients;
@@ -63,6 +64,17 @@ public OrderDomains(Options options)
var exportKey = rsaCryptoServiceProvider.ExportCspBlob(true);
var strKey = Convert.ToBase64String(exportKey);
+ if (File.Exists("acmekey.key"))
+ {
+
+ var result = ConsoleInput.Prompt("WARNING - acmekey.key already exists - do you wish to overwrite?", ["y", "yes", "n", "no"]);
+ if (!ConsoleInput.PositivePromptAnswers.Contains(result, StringComparer.OrdinalIgnoreCase))
+ {
+ // user doesn't wish to overwrite the key. Abort
+ throw new Exception("User aborted");
+ }
+ }
+
File.WriteAllText("acmekey.key", strKey);
}
else
@@ -139,7 +151,7 @@ public async Task ValidateACMEConnection()
var userContact = Console.ReadLine();
try
{
- account = await acmeClient.RegisterAsync(new[] { "mailto:" + userContact });
+ account = await acmeClient.RegisterAsync(["mailto:" + userContact]);
}
catch (Exception ex)
{
@@ -161,23 +173,26 @@ public async Task ValidateDomains()
AuthorizationChallengeResponse[] challengeResponses = await Task.WhenAll(preAuthorizationChallenges);
}
- IEnumerable domains = options.Domains.Select(domain => new OrderIdentifier { Type = ChallengeType.DNSChallenge, Value = domain });
+ var domains = options.Domains.Select(domain => new OrderIdentifier { Type = ChallengeType.DNSChallenge, Value = domain }).ToList();
Order order = await NewOrderAsync(acmeClient, domains);
// todo: save order identifier
+ Uri location = order.Location;
Program.LogLine($"Order location: {order.Location}");
+
+ // get challenge results.
Uri[] validations = order.Authorizations;
var dnsRecords = new List(order.Authorizations.Length);
- IEnumerable auths = await RetrieveAuthz(acmeClient, validations);
- foreach (AuthorizationChallengeResponse item in auths)
- {
- Program.LogLine($"Processing validations for {item.Identifier.Value}");
+ List auths = await RetrieveAuthz(acmeClient, validations);
+ // now that we have all the authz responses - add challenges for those that needs it.
+ Task<(AuthorizationChallenge dnsChallenge, string Value)>[] dnsChallenges = auths.Select(async item =>
+ {
if (item.Status == ACMEStatus.Valid)
{
- Program.LogLine("Domain already validated succesfully.");
- continue;
+ Program.LogLine($"{item.Identifier} already validated succesfully.");
+ return (null, null);
}
AuthorizationChallenge validChallenge = item.Challenges.Where(challenge => challenge.Status == ACMEStatus.Valid).FirstOrDefault();
@@ -185,52 +200,64 @@ public async Task ValidateDomains()
{
Program.LogLine("Found a valid challenge, skipping domain.", true);
Program.LogLine(validChallenge.Type, true);
- continue;
+ return (null, null);
}
// limit to DNS challenges, as we can handle them with Cloudflare.
AuthorizationChallenge dnsChallenge = item.Challenges.FirstOrDefault(x => x.Type == "dns-01");
- Program.LogLine($"Got challenge for {item.Identifier.Value}");
+ Program.LogLine($"Adding DNS entry for {item.Identifier.Value}");
var recordId = await AddDNSEntry(item.Identifier.Value, dnsChallenge.AuthorizationToken);
dnsRecords.Add(recordId);
- // validate the DNS record is accessible.
- await ValidateChallengeCompletion(dnsChallenge, item.Identifier.Value);
- AuthorizationChallengeResponse c = await CompleteChallenge(acmeClient, dnsChallenge);
+ return (dnsChallenge, item.Identifier.Value);
+ }).ToArray();
- while (c.Status == ACMEStatus.Pending)
- {
- await Task.Delay(5000);
- c = await acmeClient.GetAuthorizationChallengeAsync(dnsChallenge.Url);
- }
+ (AuthorizationChallenge dnsChallenge, string Domain)[] challenges = (await Task.WhenAll(dnsChallenges))
+ .Where(x => x.dnsChallenge is not null).ToArray();
- if (c.Status == ACMEStatus.Valid)
- {
- // no reason to keep going, we have one succesfull challenge!
- continue;
- }
- }
+ // foreach of the challenges - let's check if we can resolve them with DNS.
+ Task[] completionTasks = challenges.Select(async x =>
+ {
+ await ValidateChallengeCompletion(x.dnsChallenge, x.Domain);
+ await CompleteChallenge(acmeClient, x.dnsChallenge);
+ }).ToArray();
+ await Task.WhenAll(completionTasks);
- var failedAuthorizations = new List();
- foreach (Uri challenge in order.Authorizations)
+ // now that we have validated that all DNS records exists AND completed the challenges - let's check if they completed succesfully
+ Task[] authorizationTasks = order.Authorizations.Select(async challenge =>
{
AuthorizationChallengeResponse c;
do
{
- await Task.Delay(5000);
c = await acmeClient.GetAuthorizationChallengeAsync(challenge);
+
}
while (c == null || c.Status == ACMEStatus.Pending);
- if (c.Status == ACMEStatus.Invalid)
+ return c;
+ }).ToArray();
+
+ AuthorizationChallengeResponse[] authorizationResults = await Task.WhenAll(authorizationTasks);
+ IEnumerable> groupedByStatus = authorizationResults.GroupBy(x => x.Status);
+ foreach (IGrouping group in groupedByStatus)
+ {
+ Console.WriteLine(group.Key);
+ foreach (AuthorizationChallengeResponse item in group)
{
- failedAuthorizations.Add(c.Identifier.Value);
+ Console.WriteLine(item.Identifier.Value);
}
}
+ IEnumerable> failedAuthorizations = groupedByStatus.Where(x => x.Key == ACMEStatus.Invalid);
if (failedAuthorizations.Any())
{
throw new Exception($"Failed to authorize the following domains {string.Join(',', failedAuthorizations)}.");
+ };
+
+ if (order.Location == null)
+ {
+ // sometimes, ACME doesn't respond with the location -.-
+ order.Location = location;
}
return order;
@@ -366,22 +393,10 @@ private static async Task NewAuthorizationAsync(
return await acmeClient.NewAuthorizationAsync(domain);
}
- private static async Task> RetrieveAuthz(ACMEClient acmeClient, Uri[] uris)
+ private static async Task> RetrieveAuthz(ACMEClient acmeClient, Uri[] uris)
{
- var challenges = new List();
- foreach (Uri uri in uris)
- {
- try
- {
- AuthorizationChallengeResponse result = await acmeClient.GetAuthorizationChallengeAsync(uri);
- challenges.Add(result);
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex.Message);
- }
- }
- return challenges;
+ var tasks = uris.Select(x => acmeClient.GetAuthorizationChallengeAsync(x)).ToList();
+ return [.. (await Task.WhenAll(tasks))];
}
private static async Task CompleteChallenge(ACMEClient acmeClient, AuthorizationChallenge challenge)
@@ -408,6 +423,11 @@ private async Task AddDNSEntry(string domain, string value)
Program.LogLine($"The DNS entry already exists. Ignoring {domain}/{value}");
return string.Empty;
}
+ catch (CloudflareException existsAlreadyException) when (existsAlreadyException.Errors[0].Code == "81058")
+ {
+ // record already exists.
+ return string.Empty;
+ }
}
private static async Task ValidateChallengeCompletion(AuthorizationChallenge challenge, string domainName)
@@ -423,7 +443,18 @@ private static async Task ValidateChallengeCompletion(AuthorizationChallenge cha
for (var i = 0; i < maxRetries; i++)
{
var lookup = new LookupClient(IPAddress.Parse("1.1.1.1"));
- IDnsQueryResponse result = await lookup.QueryAsync($"_acme-challenge.{domainName}", QueryType.TXT);
+ IDnsQueryResponse result;
+ try
+ {
+ result = await lookup.QueryAsync($"_acme-challenge.{domainName}", QueryType.TXT);
+
+ }
+ catch (DnsResponseException dnsResponseException)
+ {
+ Program.LogLine($"Exception while querying DNS: {dnsResponseException.Message}");
+ continue;
+ }
+
TxtRecord record = result.Answers.TxtRecords().Where(txt => txt.Text.Contains(challenge.AuthorizationToken)).FirstOrDefault();
if (record != null)
{
diff --git a/src/Examples/CloudflareIntegration/PasswordInput.cs b/src/Examples/CloudflareIntegration/PasswordInput.cs
deleted file mode 100644
index 8d3edad..0000000
--- a/src/Examples/CloudflareIntegration/PasswordInput.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-namespace CloudflareIntegration
-{
- using System;
- using System.ComponentModel;
- using System.Linq;
- using System.Runtime.InteropServices;
- using System.Security;
-
- ///
- /// Based on https://stackoverflow.com/questions/3404421/password-masking-console-application
- ///
- internal partial class PasswordInput
- {
- private enum StdHandle
- {
- Input = -10,
- Output = -11,
- Error = -12,
- }
-
- private enum ConsoleMode
- {
- ENABLE_ECHO_INPUT = 4
- }
-
- private const int ENTER = 13, BACKSP = 8, CTRLBACKSP = 127;
- private static readonly int[] Filtered = { 0, 27 /* escape */, 9 /*tab*/, 10 /* line feed */ };
-
- [LibraryImport("kernel32.dll", SetLastError = true)]
- private static partial IntPtr GetStdHandle(StdHandle nStdHandle);
-
- [LibraryImport("kernel32.dll", SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- private static partial bool GetConsoleMode(IntPtr hConsoleHandle, out int lpMode);
-
- [LibraryImport("kernel32.dll", SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- private static partial bool SetConsoleMode(IntPtr hConsoleHandle, int dwMode);
-
- public static SecureString ReadPassword()
- {
- IntPtr stdInputHandle = GetStdHandle(StdHandle.Input);
- if (stdInputHandle == IntPtr.Zero)
- {
- throw new InvalidOperationException("No console input");
- }
-
- if (!GetConsoleMode(stdInputHandle, out var previousConsoleMode))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not get console mode.");
- }
-
- // disable console input echo
- if (!SetConsoleMode(stdInputHandle, dwMode: previousConsoleMode & ~(int)ConsoleMode.ENABLE_ECHO_INPUT))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not disable console input echo.");
- }
-
- var secureString = new SecureString();
-
- char character;
- while ((character = Console.ReadKey(true).KeyChar) != ENTER)
- {
- if (((character == BACKSP) || (character == CTRLBACKSP))
- && (secureString.Length > 0))
- {
- secureString.RemoveAt(secureString.Length - 1);
- }
- else if (((character == BACKSP) || (character == CTRLBACKSP)) && (secureString.Length == 0))
- {
- }
- else if (Filtered.Contains(character))
- {
- }
- else
- {
- Console.Write('*');
- secureString.AppendChar(character);
- }
- }
-
- // reset console mode to previous
- if (!SetConsoleMode(stdInputHandle, previousConsoleMode))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not reset console mode.");
- }
-
- return secureString;
- }
- }
-}
diff --git a/src/Examples/Shared/ConsoleInput.cs b/src/Examples/Shared/ConsoleInput.cs
new file mode 100644
index 0000000..f2257bb
--- /dev/null
+++ b/src/Examples/Shared/ConsoleInput.cs
@@ -0,0 +1,30 @@
+namespace Kenc.ACMELib.Examples.Shared
+{
+ using System;
+ using System.Linq;
+
+ public static class ConsoleInput
+ {
+
+ public static readonly string[] NegativePromptAnswers = ["n", "no"];
+ public static readonly string[] PositivePromptAnswers = ["y", "yes"];
+ public static readonly string[] CombinedPromptAnswers = [.. NegativePromptAnswers, .. PositivePromptAnswers];
+
+ public static string Prompt(string text, string[] options)
+ {
+ Console.WriteLine(text);
+ Console.Write($"[{string.Join('/', options)}]");
+
+ while (true)
+ {
+ var res = Console.ReadLine();
+ if (options.Contains(res, StringComparer.OrdinalIgnoreCase))
+ {
+ return res;
+ }
+
+ Console.WriteLine("Invalid options. Try again or hit ctrl+c to abort.");
+ }
+ }
+ }
+}
diff --git a/src/Examples/ACMEClient/PasswordInput.cs b/src/Examples/Shared/PasswordInput.cs
similarity index 85%
rename from src/Examples/ACMEClient/PasswordInput.cs
rename to src/Examples/Shared/PasswordInput.cs
index aee3c5a..054382a 100644
--- a/src/Examples/ACMEClient/PasswordInput.cs
+++ b/src/Examples/Shared/PasswordInput.cs
@@ -1,4 +1,4 @@
-namespace ACMEClient
+namespace Kenc.ACMELib.Examples.Shared
{
using System;
using System.ComponentModel;
@@ -9,7 +9,7 @@
///
/// Based on https://stackoverflow.com/questions/3404421/password-masking-console-application
///
- internal partial class PasswordInput
+ public partial class PasswordInput
{
private enum StdHandle
{
@@ -24,7 +24,7 @@ private enum ConsoleMode
}
private const int ENTER = 13, BACKSP = 8, CTRLBACKSP = 127;
- private static readonly int[] Filtered = { 0, 27 /* escape */, 9 /*tab*/, 10 /* line feed */ };
+ private static readonly int[] Filtered = [0, 27 /* escape */, 9 /*tab*/, 10 /* line feed */];
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial IntPtr GetStdHandle(StdHandle nStdHandle);
@@ -39,7 +39,7 @@ private enum ConsoleMode
public static SecureString ReadPassword()
{
- IntPtr stdInputHandle = GetStdHandle(StdHandle.Input);
+ var stdInputHandle = GetStdHandle(StdHandle.Input);
if (stdInputHandle == IntPtr.Zero)
{
throw new InvalidOperationException("No console input");
@@ -80,12 +80,9 @@ public static SecureString ReadPassword()
}
// reset console mode to previous
- if (!SetConsoleMode(stdInputHandle, previousConsoleMode))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not reset console mode.");
- }
-
- return secureString;
+ return !SetConsoleMode(stdInputHandle, previousConsoleMode)
+ ? throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not reset console mode.")
+ : secureString;
}
}
}
diff --git a/src/Examples/Shared/Shared.csproj b/src/Examples/Shared/Shared.csproj
new file mode 100644
index 0000000..e8e070c
--- /dev/null
+++ b/src/Examples/Shared/Shared.csproj
@@ -0,0 +1,10 @@
+
+
+
+ net7.0
+ enable
+ enable
+ true
+
+
+
diff --git a/src/Libraries/ACMELib.Tests/AcmeClientTests.cs b/src/Libraries/ACMELib.Tests/AcmeClientTests.cs
index 87e6e35..0922030 100644
--- a/src/Libraries/ACMELib.Tests/AcmeClientTests.cs
+++ b/src/Libraries/ACMELib.Tests/AcmeClientTests.cs
@@ -36,7 +36,7 @@ public async Task PostAddsProperHeader()
var rsaKey = RSA.Create();
var acmeClient = new ACMEClient(TestHelpers.BaseUri, rsaKey, httpClient);
- await acmeClient.RegisterAsync(new[] { "test@test.test" });
+ await acmeClient.RegisterAsync(["test@test.test"]);
// assert
httpRequestMessage.Content.Headers.ContentType.MediaType.Should().Be("application/jose+json");
@@ -59,7 +59,7 @@ public async Task GetsANonceIfNoneIsPresent()
var rsaKey = RSA.Create();
var acmeClient = new ACMEClient(TestHelpers.BaseUri, rsaKey, httpClient);
- await acmeClient.RegisterAsync(new[] { "test@test.test" });
+ await acmeClient.RegisterAsync(["test@test.test"]);
messageHandlerMock.VerifyAll();
}
diff --git a/src/Libraries/ACMELib.Tests/Kenc.ACMELibCore.Tests.csproj b/src/Libraries/ACMELib.Tests/Kenc.ACMELibCore.Tests.csproj
index 5238e73..583be58 100644
--- a/src/Libraries/ACMELib.Tests/Kenc.ACMELibCore.Tests.csproj
+++ b/src/Libraries/ACMELib.Tests/Kenc.ACMELibCore.Tests.csproj
@@ -16,5 +16,4 @@
-
\ No newline at end of file
diff --git a/src/Libraries/ACMELib.Tests/Mocks/CertificateMock.cs b/src/Libraries/ACMELib.Tests/Mocks/CertificateMock.cs
index 6c00e1f..ab7d356 100644
--- a/src/Libraries/ACMELib.Tests/Mocks/CertificateMock.cs
+++ b/src/Libraries/ACMELib.Tests/Mocks/CertificateMock.cs
@@ -25,10 +25,10 @@ public override string GetRawCertDataString()
public override byte[] GetRawCertData()
{
- return new byte[]
- {
+ return
+ [
1,2,3,4,5,6,7,8,9,10
- };
+ ];
}
}
}
diff --git a/src/Libraries/ACMELib.Tests/Mocks/TestHttpMessageHandler.cs b/src/Libraries/ACMELib.Tests/Mocks/TestHttpMessageHandler.cs
index c1d570c..915ef65 100644
--- a/src/Libraries/ACMELib.Tests/Mocks/TestHttpMessageHandler.cs
+++ b/src/Libraries/ACMELib.Tests/Mocks/TestHttpMessageHandler.cs
@@ -8,7 +8,7 @@
public class TestHttpMessageHandler : HttpMessageHandler
{
- private readonly Dictionary> responses = new();
+ private readonly Dictionary> responses = [];
public virtual HttpResponseMessage Send(HttpRequestMessage request)
{
diff --git a/src/Libraries/ACMELib/ACMEClient.cs b/src/Libraries/ACMELib/ACMEClient.cs
index 3966fc3..e556ebf 100644
--- a/src/Libraries/ACMELib/ACMEClient.cs
+++ b/src/Libraries/ACMELib/ACMEClient.cs
@@ -113,14 +113,7 @@ public async Task GetAuthorizationChallengeAsync
{
foreach (AuthorizationChallenge challenge in result.Challenges)
{
- if (challenge.Type == "dns-01")
- {
- challenge.AuthorizationToken = jws.GetDNSKeyAuthorization(challenge.Token);
- }
- else
- {
- challenge.AuthorizationToken = jws.GetKeyAuthorization(challenge.Token);
- }
+ challenge.AuthorizationToken = challenge.Type == "dns-01" ? jws.GetDNSKeyAuthorization(challenge.Token) : jws.GetKeyAuthorization(challenge.Token);
}
}
@@ -139,7 +132,7 @@ public async Task GetCertificateAsync(Order order, Cancellatio
if (order.Status != ACMEStatus.Valid)
{
- throw new ArgumentOutOfRangeException(nameof(order.Status), "Order status is not in valid range.");
+ throw new ArgumentOutOfRangeException($"{nameof(order)}.{nameof(order.Status)}", "Order status is not in valid range.");
}
var result = await GetAsync(order.Certificate, cancellationToken);
@@ -271,7 +264,10 @@ public async Task UpdateOrderAsync(Order order, CancellationToken cancell
throw new ArgumentNullException(nameof(order));
}
- return await GetAsync(order.Location, cancellationToken);
+ // check order.location
+ return order.Location == null || !order.Location.IsAbsoluteUri
+ ? throw new InvalidOperationException($"order location {order.Location} is either null or not an absolute uri")
+ : await GetAsync(order.Location, cancellationToken);
}
private async Task GetAsync(Uri uri, CancellationToken cancellationToken) where TResult : class
@@ -293,8 +289,14 @@ private async Task NewNonceAsync(CancellationToken cancellationToken = d
private async Task PostAsync(Uri uri, object message, CancellationToken cancellationToken) where TResult : class
{
- if (!nonces.TryDequeue(out var nonce))
+ string nonce;
+ while (true)
{
+ if (nonces.TryDequeue(out nonce))
+ {
+ break;
+ }
+
await NewNonceAsync(cancellationToken);
}
@@ -352,7 +354,7 @@ private async Task SendRequest(HttpRequestMessage httpRequestM
if (httpResponseMessage.Content.Headers.ContentType.MediaType.Equals(ApplicationPemCertChainMime, StringComparison.OrdinalIgnoreCase))
{
var responseStr = await httpResponseMessage.Content.ReadAsStringAsync();
- responseContent = (TResult)(object)(responseStr);
+ responseContent = (TResult)(object)responseStr;
}
else
{
@@ -382,7 +384,7 @@ private async Task SendRequest(HttpRequestMessage httpRequestM
}
}
- if (responseContent != null && responseContent is Account account)
+ if (responseContent is not null and Account account)
{
jws.SetKeyId(account);
}
diff --git a/src/Libraries/ACMELib/Exceptions/ExceptionHelper.cs b/src/Libraries/ACMELib/Exceptions/ExceptionHelper.cs
index 50ea7b8..2f4bea4 100644
--- a/src/Libraries/ACMELib/Exceptions/ExceptionHelper.cs
+++ b/src/Libraries/ACMELib/Exceptions/ExceptionHelper.cs
@@ -12,8 +12,8 @@
///
public static class ExceptionHelper
{
- private static readonly List KnownExceptions = new()
- {
+ private static readonly List KnownExceptions =
+ [
typeof(AccountDoesNotExistException),
typeof(BadCSRException),
typeof(BadNonceException),
@@ -36,7 +36,7 @@ public static class ExceptionHelper
typeof(UserActionRequiredException),
typeof(BadPublicKeyException),
typeof(OrderNotReadyException)
- };
+ ];
///
/// Throw an exception based on the
@@ -57,7 +57,7 @@ public static void ThrowException(Problem problem)
if (typeWhereAttributeMatches != null)
{
- throw (ACMEException)Activator.CreateInstance(typeWhereAttributeMatches, new object[] { problem.Status, problem.Detail });
+ throw (ACMEException)Activator.CreateInstance(typeWhereAttributeMatches, [problem.Status, problem.Detail]);
}
throw new ACMEException(problem.Status, problem.Detail, problem.Type);
diff --git a/src/Libraries/ACMELib/JWS/JwsHeader.cs b/src/Libraries/ACMELib/JWS/JwsHeader.cs
index 1d43e7f..3c31010 100644
--- a/src/Libraries/ACMELib/JWS/JwsHeader.cs
+++ b/src/Libraries/ACMELib/JWS/JwsHeader.cs
@@ -20,6 +20,11 @@ public JwsHeader(string algorithm, Jwk key)
public JwsHeader(string nonce, Uri url)
{
+ if (string.IsNullOrEmpty(nonce))
+ {
+ throw new ArgumentNullException(nameof(nonce));
+ }
+
Url = url;
Nonce = nonce;
}
diff --git a/src/Libraries/ACMELib/Kenc.ACMELib.csproj b/src/Libraries/ACMELib/Kenc.ACMELib.csproj
index b3d525a..cf98f1c 100644
--- a/src/Libraries/ACMELib/Kenc.ACMELib.csproj
+++ b/src/Libraries/ACMELib/Kenc.ACMELib.csproj
@@ -2,20 +2,17 @@
netstandard2.1
-
+
.net core library for communicating with Let’s Encrypt ACME servers for certificate management.
true
-
Kenc.ACMELib
- Kenc.ACMELib
ACME LetsEncrypt Certificate
ACME v2 compliant client implementation
MIT
true
README.md
-
Github
true
diff --git a/src/Packages.props b/src/Packages.props
index 0b64632..8988e94 100644
--- a/src/Packages.props
+++ b/src/Packages.props
@@ -1,34 +1,34 @@
-
-
-
-
+
+
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
+
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers