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