From 767b4f2fc6d6dd4d1245177f7a9692ea853f1000 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Tue, 1 Aug 2023 12:31:24 +0200 Subject: [PATCH 1/3] (refactor): codebase review, small syntax fixes --- src/Core/Core.csproj | 4 +- src/Core/Extensions/ObjectExtensions.cs | 53 +- src/Core/Extensions/StringExtensions.cs | 51 +- src/Core/OperatingSystem.cs | 17 +- src/LetsEncrypt/Entities/Jws/Jwk.cs | 177 ++-- src/LetsEncrypt/Entities/Jws/JwsMessage.cs | 58 +- .../LetsEncrypt/AuthorizationChallange.cs | 13 +- .../LetsEncrypt/CachedCertificateResult.cs | 11 +- .../Entities/LetsEncrypt/RegistrationCache.cs | 64 +- .../Exceptions/LetsEncrytException.cs | 40 +- .../Extensions/ServiceCollectionExtensions.cs | 9 +- .../RequestModelWithLocationBase.cs | 7 +- .../Models/Requests/FinalizeRequest.cs | 10 +- src/LetsEncrypt/Models/Responses/Account.cs | 39 +- .../Models/Responses/AcmeDirectory.cs | 36 +- .../AuthorizationChallengeResponse.cs | 22 +- src/LetsEncrypt/Models/Responses/Order.cs | 53 +- src/LetsEncrypt/Services/JwsService.cs | 168 ++-- .../Services/LetsEncryptService.cs | 839 +++++++++--------- src/LetsEncryptConsole/App.cs | 472 +++++----- src/LetsEncryptConsole/Configuration.cs | 51 +- src/LetsEncryptConsole/Program.cs | 91 +- src/LetsEncryptConsole/Services/KeyService.cs | 265 +++--- .../Services/TerminalService.cs | 42 +- src/SSHProvider/Configuration.cs | 11 +- src/SSHProvider/SSHService.cs | 6 +- .../Abstractions/ConfigurationBase.cs | 73 +- .../Abstractions/ServiceBase.cs | 33 +- src/Tests/SSHSerivceTests/UnitTest1.cs | 63 +- 29 files changed, 1368 insertions(+), 1410 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 5e15896..fcac937 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -8,7 +8,9 @@ - + + + diff --git a/src/Core/Extensions/ObjectExtensions.cs b/src/Core/Extensions/ObjectExtensions.cs index d604210..0c5f2b1 100644 --- a/src/Core/Extensions/ObjectExtensions.cs +++ b/src/Core/Extensions/ObjectExtensions.cs @@ -1,37 +1,36 @@ using System.Text.Json.Serialization; using System.Text.Json; -namespace MaksIT.Core.Extensions { - public static class ObjectExtensions { +namespace MaksIT.Core.Extensions; +public static class ObjectExtensions { - /// - /// - /// - /// - /// - /// - public static string ToJson(this T? obj) => obj.ToJson(null); + /// + /// + /// + /// + /// + /// + public static string ToJson(this T? obj) => obj.ToJson(null); - /// - /// - /// - /// - /// - /// - /// - public static string ToJson(this T? obj, List? converters) { - if (obj == null) - return "{}"; + /// + /// + /// + /// + /// + /// + /// + public static string ToJson(this T? obj, List? converters) { + if (obj == null) + return "{}"; - var options = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = true - }; + var options = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true + }; - converters?.ForEach(x => options.Converters.Add(x)); + converters?.ForEach(x => options.Converters.Add(x)); - return JsonSerializer.Serialize(obj, options); - } + return JsonSerializer.Serialize(obj, options); } } diff --git a/src/Core/Extensions/StringExtensions.cs b/src/Core/Extensions/StringExtensions.cs index a8d5126..fb178e3 100644 --- a/src/Core/Extensions/StringExtensions.cs +++ b/src/Core/Extensions/StringExtensions.cs @@ -6,35 +6,34 @@ using System.Text.Json.Serialization; using System.Text.Json; using System.Threading.Tasks; -namespace MaksIT.Core.Extensions { - public static class StringExtensions { - /// - /// Converts JSON string to object - /// - /// - /// - /// - public static T? ToObject(this string? s) => ToObjectCore(s, null); +namespace MaksIT.Core.Extensions; +public static class StringExtensions { + /// + /// Converts JSON string to object + /// + /// + /// + /// + public static T? ToObject(this string? s) => ToObjectCore(s, null); - /// - /// - /// - /// - /// - /// - /// - public static T? ToObject(this string? s, List converters) => ToObjectCore(s, converters); + /// + /// + /// + /// + /// + /// + /// + public static T? ToObject(this string? s, List converters) => ToObjectCore(s, converters); - private static T? ToObjectCore(string? s, List? converters) { - var options = new JsonSerializerOptions { - PropertyNameCaseInsensitive = true - }; + private static T? ToObjectCore(string? s, List? converters) { + var options = new JsonSerializerOptions { + PropertyNameCaseInsensitive = true + }; - converters?.ForEach(x => options.Converters.Add(x)); + converters?.ForEach(x => options.Converters.Add(x)); - return s != null - ? JsonSerializer.Deserialize(s, options) - : default; - } + return s != null + ? JsonSerializer.Deserialize(s, options) + : default; } } diff --git a/src/Core/OperatingSystem.cs b/src/Core/OperatingSystem.cs index 162e2c1..fb45bec 100644 --- a/src/Core/OperatingSystem.cs +++ b/src/Core/OperatingSystem.cs @@ -1,14 +1,13 @@ using System.Runtime.InteropServices; -namespace MaksIT.Core { - public static class OperatingSystem { - public static bool IsWindows() => - RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +namespace MaksIT.Core; +public static class OperatingSystem { + public static bool IsWindows() => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - public static bool IsMacOS() => - RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + public static bool IsMacOS() => + RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - public static bool IsLinux() => - RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - } + public static bool IsLinux() => + RuntimeInformation.IsOSPlatform(OSPlatform.Linux); } diff --git a/src/LetsEncrypt/Entities/Jws/Jwk.cs b/src/LetsEncrypt/Entities/Jws/Jwk.cs index 2b4d807..0907727 100644 --- a/src/LetsEncrypt/Entities/Jws/Jwk.cs +++ b/src/LetsEncrypt/Entities/Jws/Jwk.cs @@ -2,104 +2,103 @@ using System.Text.Json.Serialization; -namespace MaksIT.LetsEncrypt.Entities.Jws { - public class Jwk { - /// - /// "kty" (Key Type) Parameter - /// - /// The "kty" (key type) parameter identifies the cryptographic algorithm - /// family used with the key, such as "RSA" or "EC". - /// - /// - [JsonPropertyName("kty")] - public string? KeyType { get; set; } +namespace MaksIT.LetsEncrypt.Entities.Jws; +public class Jwk { + /// + /// "kty" (Key Type) Parameter + /// + /// The "kty" (key type) parameter identifies the cryptographic algorithm + /// family used with the key, such as "RSA" or "EC". + /// + /// + [JsonPropertyName("kty")] + public string? KeyType { get; set; } - /// - /// "kid" (Key ID) Parameter - /// - /// The "kid" (key ID) parameter is used to match a specific key. This - /// is used, for instance, to choose among a set of keys within a JWK Set - /// during key rollover. The structure of the "kid" value is - /// unspecified. - /// - /// - [JsonPropertyName("kid")] - public string? KeyId { get; set; } + /// + /// "kid" (Key ID) Parameter + /// + /// The "kid" (key ID) parameter is used to match a specific key. This + /// is used, for instance, to choose among a set of keys within a JWK Set + /// during key rollover. The structure of the "kid" value is + /// unspecified. + /// + /// + [JsonPropertyName("kid")] + public string? KeyId { get; set; } - /// - /// "use" (Public Key Use) Parameter - /// - /// The "use" (public key use) parameter identifies the intended use of - /// the public key. The "use" parameter is employed to indicate whether - /// a public key is used for encrypting data or verifying the signature - /// on data. - /// - /// - [JsonPropertyName("use")] - public string? Use { get; set; } + /// + /// "use" (Public Key Use) Parameter + /// + /// The "use" (public key use) parameter identifies the intended use of + /// the public key. The "use" parameter is employed to indicate whether + /// a public key is used for encrypting data or verifying the signature + /// on data. + /// + /// + [JsonPropertyName("use")] + public string? Use { get; set; } - /// - /// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. - /// - [JsonPropertyName("n")] - public string? Modulus { get; set; } + /// + /// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. + /// + [JsonPropertyName("n")] + public string? Modulus { get; set; } - /// - /// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. - /// - [JsonPropertyName("e")] - public string? Exponent { get; set; } + /// + /// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. + /// + [JsonPropertyName("e")] + public string? Exponent { get; set; } - /// - /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("d")] - public string? D { get; set; } + /// + /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("d")] + public string? D { get; set; } - /// - /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("p")] - public string? P { get; set; } + /// + /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("p")] + public string? P { get; set; } - /// - /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("q")] - public string? Q { get; set; } + /// + /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("q")] + public string? Q { get; set; } - /// - /// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("dp")] - public string? DP { get; set; } + /// + /// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("dp")] + public string? DP { get; set; } - /// - /// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("dq")] - public string? DQ { get; set; } + /// + /// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("dq")] + public string? DQ { get; set; } - /// - /// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("qi")] - public string? InverseQ { get; set; } + /// + /// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("qi")] + public string? InverseQ { get; set; } - /// - /// The other primes information, should they exist, null or an empty list if not specified. - /// - [JsonPropertyName("oth")] - public string? OthInf { get; set; } + /// + /// The other primes information, should they exist, null or an empty list if not specified. + /// + [JsonPropertyName("oth")] + public string? OthInf { get; set; } - /// - /// "alg" (Algorithm) Parameter - /// - /// The "alg" (algorithm) parameter identifies the algorithm intended for - /// use with the key. - /// - /// - [JsonPropertyName("alg")] - public string? Algorithm { get; set; } - } -} \ No newline at end of file + /// + /// "alg" (Algorithm) Parameter + /// + /// The "alg" (algorithm) parameter identifies the algorithm intended for + /// use with the key. + /// + /// + [JsonPropertyName("alg")] + public string? Algorithm { get; set; } +} diff --git a/src/LetsEncrypt/Entities/Jws/JwsMessage.cs b/src/LetsEncrypt/Entities/Jws/JwsMessage.cs index afd3d25..4726d5e 100644 --- a/src/LetsEncrypt/Entities/Jws/JwsMessage.cs +++ b/src/LetsEncrypt/Entities/Jws/JwsMessage.cs @@ -1,40 +1,36 @@ using System; using System.Text.Json.Serialization; -namespace MaksIT.LetsEncrypt.Entities.Jws -{ - - public class JwsMessage { - - public string? Protected { get; set; } - - public string? Payload { get; set; } - - public string? Signature { get; set; } - } +namespace MaksIT.LetsEncrypt.Entities.Jws; - public class JwsHeader { - - [JsonPropertyName("alg")] - public string? Algorithm { get; set; } - - [JsonPropertyName("jwk")] - public Jwk? Key { get; set; } - - - [JsonPropertyName("kid")] - public string? KeyId { get; set; } - - public string? Nonce { get; set; } - - public Uri? Url { get; set; } - - - [JsonPropertyName("Host")] - public string? Host { get; set; } - } +public class JwsMessage { + public string? Protected { get; set; } + public string? Payload { get; set; } + public string? Signature { get; set; } +} + + +public class JwsHeader { + + [JsonPropertyName("alg")] + public string? Algorithm { get; set; } + + [JsonPropertyName("jwk")] + public Jwk? Key { get; set; } + + + [JsonPropertyName("kid")] + public string? KeyId { get; set; } + + public string? Nonce { get; set; } + + public Uri? Url { get; set; } + + + [JsonPropertyName("Host")] + public string? Host { get; set; } } diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs b/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs index decc2ed..c937703 100644 --- a/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs +++ b/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs @@ -1,13 +1,12 @@ using System; -namespace MaksIT.LetsEncrypt.Entities { - public class AuthorizationChallenge { - public Uri? Url { get; set; } +namespace MaksIT.LetsEncrypt.Entities; +public class AuthorizationChallenge { + public Uri? Url { get; set; } - public string? Type { get; set; } + public string? Type { get; set; } - public string? Status { get; set; } + public string? Status { get; set; } - public string? Token { get; set; } - } + public string? Token { get; set; } } diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs b/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs index abb7436..5b8fbe2 100644 --- a/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs +++ b/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs @@ -1,11 +1,8 @@ using System.Security.Cryptography; -namespace MaksIT.LetsEncrypt.Entities -{ - public class CachedCertificateResult - { - public RSACryptoServiceProvider? PrivateKey { get; set; } - public string? Certificate { get; set; } - } +namespace MaksIT.LetsEncrypt.Entities; +public class CachedCertificateResult { + public RSACryptoServiceProvider? PrivateKey { get; set; } + public string? Certificate { get; set; } } diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs index 62b348a..6aee7b4 100644 --- a/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs +++ b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs @@ -1,18 +1,62 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography; +using System.Text; using MaksIT.LetsEncrypt.Entities.Jws; -namespace MaksIT.LetsEncrypt.Entities { - public class CertificateCache { - public string? Cert { get; set; } - public byte[]? Private { get; set; } +namespace MaksIT.LetsEncrypt.Entities; +public class CertificateCache { + public string? Cert { get; set; } + public byte[]? Private { get; set; } +} + +public class RegistrationCache { + public Dictionary? CachedCerts { get; set; } + public byte[]? AccountKey { get; set; } + public string? Id { get; set; } + public Jwk? Key { get; set; } + public Uri? Location { get; set; } + + /// + /// + /// + /// + /// + /// + public bool TryGetCachedCertificate(string subject, out CachedCertificateResult? value) { + value = null; + + if (CachedCerts == null) + return false; + + if (!CachedCerts.TryGetValue(subject, out var cache)) { + return false; + } + + var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cache.Cert)); + + // if it is about to expire, we need to refresh + if ((cert.NotAfter - DateTime.UtcNow).TotalDays < 30) + return false; + + var rsa = new RSACryptoServiceProvider(4096); + rsa.ImportCspBlob(cache.Private); + + value = new CachedCertificateResult { + Certificate = cache.Cert, + PrivateKey = rsa + }; + return true; } - public class RegistrationCache { - public Dictionary? CachedCerts { get; set; } - public byte[]? AccountKey { get; set; } - public string? Id { get; set; } - public Jwk? Key { get; set; } - public Uri? Location { get; set; } + /// + /// + /// + /// + public void ResetCachedCertificate(IEnumerable hostsToRemove) { + if (CachedCerts != null) + foreach (var host in hostsToRemove) + CachedCerts.Remove(host); } } diff --git a/src/LetsEncrypt/Exceptions/LetsEncrytException.cs b/src/LetsEncrypt/Exceptions/LetsEncrytException.cs index 615f312..78198d7 100644 --- a/src/LetsEncrypt/Exceptions/LetsEncrytException.cs +++ b/src/LetsEncrypt/Exceptions/LetsEncrytException.cs @@ -1,25 +1,23 @@ -using System; -using System.Net.Http; - -namespace MaksIT.LetsEncrypt.Exceptions { - public class LetsEncrytException : Exception { - public LetsEncrytException(Problem problem, HttpResponseMessage response) - : base($"{problem.Type}: {problem.Detail}") { - Problem = problem; - Response = response; - } - - public Problem Problem { get; } - - public HttpResponseMessage Response { get; } + +namespace MaksIT.LetsEncrypt.Exceptions; +public class LetsEncrytException : Exception { + public LetsEncrytException(Problem problem, HttpResponseMessage response) + : base($"{problem.Type}: {problem.Detail}") { + Problem = problem; + Response = response; } + public Problem Problem { get; } - public class Problem { - public string Type { get; set; } - - public string Detail { get; set; } - - public string RawJson { get; set; } - } + public HttpResponseMessage Response { get; } } + + +public class Problem { + public string Type { get; set; } + + public string Detail { get; set; } + + public string RawJson { get; set; } +} + diff --git a/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs b/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs index 9bcc721..99f3de4 100644 --- a/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs +++ b/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs @@ -2,11 +2,10 @@ using MaksIT.LetsEncrypt.Services; -namespace MaksIT.LetsEncrypt.Extensions { - public static class ServiceCollectionExtensions { - public static void RegisterLetsEncrypt(this IServiceCollection services) { +namespace MaksIT.LetsEncrypt.Extensions; +public static class ServiceCollectionExtensions { + public static void RegisterLetsEncrypt(this IServiceCollection services) { - services.AddHttpClient(); - } + services.AddHttpClient(); } } diff --git a/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs b/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs index 0fcaa1f..b06d292 100644 --- a/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs +++ b/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs @@ -1,5 +1,4 @@ -namespace MaksIT.LetsEncrypt.Models.Interfaces { - public interface IHasLocation { - Uri? Location { get; set; } - } +namespace MaksIT.LetsEncrypt.Models.Interfaces; +public interface IHasLocation { + Uri? Location { get; set; } } diff --git a/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs b/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs index 6ffeb01..de09b97 100644 --- a/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs +++ b/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs @@ -1,7 +1,5 @@ -namespace MaksIT.LetsEncrypt.Models.Requests -{ - public class FinalizeRequest - { - public string? Csr { get; set; } - } +namespace MaksIT.LetsEncrypt.Models.Requests; + +public class FinalizeRequest { + public string? Csr { get; set; } } diff --git a/src/LetsEncrypt/Models/Responses/Account.cs b/src/LetsEncrypt/Models/Responses/Account.cs index 8594c90..b00fae4 100644 --- a/src/LetsEncrypt/Models/Responses/Account.cs +++ b/src/LetsEncrypt/Models/Responses/Account.cs @@ -5,34 +5,33 @@ using MaksIT.LetsEncrypt.Models.Interfaces; * https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.3 */ -namespace MaksIT.LetsEncrypt.Models.Responses -{ - public class Account : IHasLocation { +namespace MaksIT.LetsEncrypt.Models.Responses; - public bool TermsOfServiceAgreed { get; set; } +public class Account : IHasLocation { - /* - onlyReturnExisting (optional, boolean): If this field is present - with the value "true", then the server MUST NOT create a new - account if one does not already exist. This allows a client to - look up an account URL based on an account key - */ - public bool OnlyReturnExisting { get; set; } + public bool TermsOfServiceAgreed { get; set; } - public string[]? Contacts { get; set; } + /* + onlyReturnExisting (optional, boolean): If this field is present + with the value "true", then the server MUST NOT create a new + account if one does not already exist. This allows a client to + look up an account URL based on an account key + */ + public bool OnlyReturnExisting { get; set; } - public string? Status { get; set; } + public string[]? Contacts { get; set; } - public string? Id { get; set; } + public string? Status { get; set; } - public DateTime CreatedAt { get; set; } + public string? Id { get; set; } - public Jwk? Key { get; set; } + public DateTime CreatedAt { get; set; } - public string? InitialIp { get; set; } + public Jwk? Key { get; set; } - public Uri? Orders { get; set; } + public string? InitialIp { get; set; } - public Uri? Location { get; set; } - } + public Uri? Orders { get; set; } + + public Uri? Location { get; set; } } diff --git a/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs b/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs index 065e063..fc35900 100644 --- a/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs +++ b/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs @@ -1,29 +1,25 @@ using System; -namespace MaksIT.LetsEncrypt.Models.Responses -{ - public class AcmeDirectory - { - public Uri NewNonce { get; set; } +namespace MaksIT.LetsEncrypt.Models.Responses; +public class AcmeDirectory { + public Uri NewNonce { get; set; } - public Uri NewAccount { get; set; } + public Uri NewAccount { get; set; } - public Uri NewOrder { get; set; } + public Uri NewOrder { get; set; } - // New authorization If the ACME server does not implement pre-authorization - // (Section 7.4.1) it MUST omit the "newAuthz" field of the directory. - // [JsonProperty("newAuthz")] - // public Uri NewAuthz { get; set; } - public Uri RevokeCertificate { get; set; } + // New authorization If the ACME server does not implement pre-authorization + // (Section 7.4.1) it MUST omit the "newAuthz" field of the directory. + // [JsonProperty("newAuthz")] + // public Uri NewAuthz { get; set; } + public Uri RevokeCertificate { get; set; } - public Uri KeyChange { get; set; } + public Uri KeyChange { get; set; } - public AcmeDirectoryMeta Meta { get; set; } - } - - public class AcmeDirectoryMeta - { - public string TermsOfService { get; set; } - } + public AcmeDirectoryMeta Meta { get; set; } } + +public class AcmeDirectoryMeta { + public string TermsOfService { get; set; } +} \ No newline at end of file diff --git a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs index 211ae81..c6dba8b 100644 --- a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs +++ b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs @@ -1,20 +1,20 @@  using MaksIT.LetsEncrypt.Entities; -namespace MaksIT.LetsEncrypt.Models.Responses { - public class AuthorizationChallengeResponse { - public OrderIdentifier? Identifier { get; set; } +namespace MaksIT.LetsEncrypt.Models.Responses; - public string? Status { get; set; } +public class AuthorizationChallengeResponse { + public OrderIdentifier? Identifier { get; set; } - public DateTime? Expires { get; set; } + public string? Status { get; set; } - public bool Wildcard { get; set; } + public DateTime? Expires { get; set; } - public AuthorizationChallenge[]? Challenges { get; set; } - } + public bool Wildcard { get; set; } - public class AuthorizeChallenge { - public string? KeyAuthorization { get; set; } - } + public AuthorizationChallenge[]? Challenges { get; set; } +} + +public class AuthorizeChallenge { + public string? KeyAuthorization { get; set; } } diff --git a/src/LetsEncrypt/Models/Responses/Order.cs b/src/LetsEncrypt/Models/Responses/Order.cs index 12d316a..ea2236a 100644 --- a/src/LetsEncrypt/Models/Responses/Order.cs +++ b/src/LetsEncrypt/Models/Responses/Order.cs @@ -1,34 +1,33 @@ using MaksIT.LetsEncrypt.Exceptions; using MaksIT.LetsEncrypt.Models.Interfaces; -namespace MaksIT.LetsEncrypt.Models.Responses { +namespace MaksIT.LetsEncrypt.Models.Responses; - public class OrderIdentifier { - public string? Type { get; set; } +public class OrderIdentifier { + public string? Type { get; set; } - public string? Value { get; set; } + public string? Value { get; set; } - } - - public class Order : IHasLocation { - public Uri? Location { get; set; } - - public string? Status { get; set; } - - public DateTime? Expires { get; set; } - - public OrderIdentifier[]? Identifiers { get; set; } - - public DateTime? NotBefore { get; set; } - - public DateTime? NotAfter { get; set; } - - public Problem? Error { get; set; } - - public Uri[]? Authorizations { get; set; } - - public Uri? Finalize { get; set; } - - public Uri? Certificate { get; set; } - } +} + +public class Order : IHasLocation { + public Uri? Location { get; set; } + + public string? Status { get; set; } + + public DateTime? Expires { get; set; } + + public OrderIdentifier[]? Identifiers { get; set; } + + public DateTime? NotBefore { get; set; } + + public DateTime? NotAfter { get; set; } + + public Problem? Error { get; set; } + + public Uri[]? Authorizations { get; set; } + + public Uri? Finalize { get; set; } + + public Uri? Certificate { get; set; } } diff --git a/src/LetsEncrypt/Services/JwsService.cs b/src/LetsEncrypt/Services/JwsService.cs index b1d775c..2258666 100644 --- a/src/LetsEncrypt/Services/JwsService.cs +++ b/src/LetsEncrypt/Services/JwsService.cs @@ -11,102 +11,102 @@ using MaksIT.LetsEncrypt.Entities.Jws; using MaksIT.Core.Extensions; -namespace MaksIT.LetsEncrypt.Services { - public interface IJwsService { - void SetKeyId(string location); +namespace MaksIT.LetsEncrypt.Services; - JwsMessage Encode(JwsHeader protectedHeader); +public interface IJwsService { + void SetKeyId(string location); - JwsMessage Encode(TPayload payload, JwsHeader protectedHeader); + JwsMessage Encode(JwsHeader protectedHeader); - string GetKeyAuthorization(string token); + JwsMessage Encode(TPayload payload, JwsHeader protectedHeader); + + string GetKeyAuthorization(string token); - string Base64UrlEncoded(string s); + string Base64UrlEncoded(string s); - string Base64UrlEncoded(byte[] arg); + string Base64UrlEncoded(byte[] arg); +} + + +public class JwsService : IJwsService { + + public Jwk _jwk; + private RSA _rsa; + + public JwsService(RSA rsa) { + _rsa = rsa ?? throw new ArgumentNullException(nameof(rsa)); + + var publicParameters = rsa.ExportParameters(false); + + var exp = publicParameters.Exponent ?? throw new ArgumentNullException(nameof(publicParameters.Exponent)); + var mod = publicParameters.Modulus ?? throw new ArgumentNullException(nameof(publicParameters.Modulus)); + + _jwk = new Jwk() { + KeyType = "RSA", + Exponent = Base64UrlEncoded(exp), + Modulus = Base64UrlEncoded(mod), + }; + } + + public void SetKeyId(string location) { + _jwk.KeyId = location; + } + + public JwsMessage Encode(JwsHeader protectedHeader) => + Encode(null, protectedHeader); + + public JwsMessage Encode(T? payload, JwsHeader protectedHeader) { + + protectedHeader.Algorithm = "RS256"; + if (_jwk.KeyId != null) { + protectedHeader.KeyId = _jwk.KeyId; + } + else { + protectedHeader.Key = _jwk; + } + + var message = new JwsMessage { + Payload = "", + Protected = Base64UrlEncoded(protectedHeader.ToJson()) + }; + + if (payload != null) { + if (payload is string stringPayload) + message.Payload = Base64UrlEncoded(stringPayload); + else + message.Payload = Base64UrlEncoded(payload.ToJson()); + } + + + message.Signature = Base64UrlEncoded( + _rsa.SignData(Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}"), + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1)); + + return message; + } + + public string GetKeyAuthorization(string token) => + $"{token}.{GetSha256Thumbprint()}"; + + private string GetSha256Thumbprint() { + var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}"; + return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json))); } - public class JwsService : IJwsService { - public Jwk _jwk; - private RSA _rsa; + public string Base64UrlEncoded(string s) => + Base64UrlEncoded(Encoding.UTF8.GetBytes(s)); - public JwsService(RSA rsa) { - _rsa = rsa ?? throw new ArgumentNullException(nameof(rsa)); - - var publicParameters = rsa.ExportParameters(false); - - var exp = publicParameters.Exponent ?? throw new ArgumentNullException(nameof(publicParameters.Exponent)); - var mod = publicParameters.Modulus ?? throw new ArgumentNullException(nameof(publicParameters.Modulus)); - - _jwk = new Jwk() { - KeyType = "RSA", - Exponent = Base64UrlEncoded(exp), - Modulus = Base64UrlEncoded(mod), - }; - } - - public void SetKeyId(string location) { - _jwk.KeyId = location; - } - - public JwsMessage Encode(JwsHeader protectedHeader) => - Encode(null, protectedHeader); - - public JwsMessage Encode(T? payload, JwsHeader protectedHeader) { - - protectedHeader.Algorithm = "RS256"; - if (_jwk.KeyId != null) { - protectedHeader.KeyId = _jwk.KeyId; - } - else { - protectedHeader.Key = _jwk; - } - - var message = new JwsMessage { - Payload = "", - Protected = Base64UrlEncoded(protectedHeader.ToJson()) - }; - - if (payload != null) { - if (payload is string stringPayload) - message.Payload = Base64UrlEncoded(stringPayload); - else - message.Payload = Base64UrlEncoded(payload.ToJson()); - } - - - message.Signature = Base64UrlEncoded( - _rsa.SignData(Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}"), - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1)); - - return message; - } - - public string GetKeyAuthorization(string token) => - $"{token}.{GetSha256Thumbprint()}"; - - private string GetSha256Thumbprint() { - var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}"; - return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json))); - } - - - - public string Base64UrlEncoded(string s) => - Base64UrlEncoded(Encoding.UTF8.GetBytes(s)); - - // https://tools.ietf.org/html/rfc4648#section-5 - public string Base64UrlEncoded(byte[] bytes) => - Convert.ToBase64String(bytes) // Regular base64 encoder - .Split('=').First() // Remove any trailing '='s - .Replace('+', '-') // 62nd char of encoding - .Replace('/', '_'); // 63rd char of encoding + // https://tools.ietf.org/html/rfc4648#section-5 + public string Base64UrlEncoded(byte[] bytes) => + Convert.ToBase64String(bytes) // Regular base64 encoder + .Split('=').First() // Remove any trailing '='s + .Replace('+', '-') // 62nd char of encoding + .Replace('/', '_'); // 63rd char of encoding - } } diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs index 24c58e1..c409dcb 100644 --- a/src/LetsEncrypt/Services/LetsEncryptService.cs +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -18,460 +18,457 @@ using MaksIT.LetsEncrypt.Models.Responses; using MaksIT.LetsEncrypt.Models.Interfaces; using MaksIT.LetsEncrypt.Models.Requests; using MaksIT.LetsEncrypt.Entities.Jws; -using System.Xml; -using System.Diagnostics; -namespace MaksIT.LetsEncrypt.Services { +namespace MaksIT.LetsEncrypt.Services; - public interface ILetsEncryptService { +public interface ILetsEncryptService { - Task ConfigureClient(string url); + Task ConfigureClient(string url); - Task Init(string[] contacts, RegistrationCache? registrationCache); + Task Init(string[] contacts, RegistrationCache? registrationCache); - string GetTermsOfServiceUri(); + string GetTermsOfServiceUri(); - Task> NewOrder(string[] hostnames, string challengeType); - Task CompleteChallenges(); - Task GetOrder(string[] hostnames); - Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject); + Task> NewOrder(string[] hostnames, string challengeType); + Task CompleteChallenges(); + Task GetOrder(string[] hostnames); + Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject); - RegistrationCache? GetRegistrationCache(); + RegistrationCache? GetRegistrationCache(); +} + + + + +public class LetsEncryptService : ILetsEncryptService { + + //private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings { + // NullValueHandling = NullValueHandling.Ignore, + // Formatting = Formatting.Indented + //}; + + private readonly ILogger _logger; + + private HttpClient _httpClient; + + private IJwsService _jwsService; + private AcmeDirectory? _directory; + private RegistrationCache? _cache; + + private string? _nonce; + + private List _challenges = new List(); + private Order? _currentOrder; + + public LetsEncryptService( + ILogger logger, + HttpClient httpClient + ) { + _logger = logger; + _httpClient = httpClient; + } + + + /// + /// + /// + /// + /// + /// + public async Task ConfigureClient(string url) { + + _httpClient.BaseAddress ??= new Uri(url); + + (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null); + } + + /// + /// Account creation or Initialization from cache + /// + /// + /// + /// + public async Task Init(string? [] contacts, RegistrationCache? cache) { + + if (contacts == null || contacts.Length == 0) + throw new ArgumentNullException(); + + if (_directory == null) + throw new ArgumentNullException(); + + var accountKey = new RSACryptoServiceProvider(4096); + + if (cache != null && cache.AccountKey != null) { + _cache = cache; + accountKey.ImportCspBlob(cache.AccountKey); + } + + // New Account request + _jwsService = new JwsService(accountKey); + + + var letsEncryptOrder = new Account { + TermsOfServiceAgreed = true, + Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray() + }; + + var (account, response) = await SendAsync(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder); + _jwsService.SetKeyId(account.Location.ToString()); + + if (account.Status != "valid") + throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}"); + + _cache = new RegistrationCache { + Location = account.Location, + AccountKey = accountKey.ExportCspBlob(true), + Id = account.Id, + Key = account.Key + }; + } + + /// + /// + /// + /// + public RegistrationCache? GetRegistrationCache() => + _cache; + + /// + /// Just retrive terms of service + /// + /// + /// + public string GetTermsOfServiceUri() { + + if (_directory == null) + throw new NullReferenceException(); + + return _directory.Meta.TermsOfService; } + /// + /// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange. + /// + /// Available challange types: + /// + /// dns-01 + /// http-01 + /// tls-alpn-01 + /// + /// + /// + /// + /// + /// + /// + public async Task> NewOrder(string[] hostnames, string challengeType) { + _challenges.Clear(); - public class LetsEncryptService : ILetsEncryptService { + var letsEncryptOrder = new Order { + Expires = DateTime.UtcNow.AddDays(2), + Identifiers = hostnames.Select(hostname => new OrderIdentifier { + Type = "dns", + Value = hostname + }).ToArray() + }; - //private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings { - // NullValueHandling = NullValueHandling.Ignore, - // Formatting = Formatting.Indented - //}; + var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); - private readonly ILogger _logger; - - private HttpClient _httpClient; + if (order.Status == "ready") + return new Dictionary(); - private IJwsService _jwsService; - private AcmeDirectory? _directory; - private RegistrationCache? _cache; - - private string? _nonce; - - private List _challenges = new List(); - private Order? _currentOrder; - - public LetsEncryptService( - ILogger logger, - HttpClient httpClient - ) { - _logger = logger; - _httpClient = httpClient; - } - - - /// - /// - /// - /// - /// - /// - public async Task ConfigureClient(string url) { - - _httpClient.BaseAddress ??= new Uri(url); - - (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null); - } - - /// - /// Account creation or Initialization from cache - /// - /// - /// - /// - public async Task Init(string? [] contacts, RegistrationCache? cache) { - - if (contacts == null || contacts.Length == 0) - throw new ArgumentNullException(); - - if (_directory == null) - throw new ArgumentNullException(); - - var accountKey = new RSACryptoServiceProvider(4096); - - if (cache != null && cache.AccountKey != null) { - _cache = cache; - accountKey.ImportCspBlob(cache.AccountKey); - } - - // New Account request - _jwsService = new JwsService(accountKey); - - - var letsEncryptOrder = new Account { - TermsOfServiceAgreed = true, - Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray() - }; - - var (account, response) = await SendAsync(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder); - _jwsService.SetKeyId(account.Location.ToString()); - - if (account.Status != "valid") - throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}"); - - _cache = new RegistrationCache { - Location = account.Location, - AccountKey = accountKey.ExportCspBlob(true), - Id = account.Id, - Key = account.Key - }; - } - - /// - /// - /// - /// - public RegistrationCache? GetRegistrationCache() => - _cache; - - /// - /// Just retrive terms of service - /// - /// - /// - public string GetTermsOfServiceUri() { - - if (_directory == null) - throw new NullReferenceException(); - - return _directory.Meta.TermsOfService; - } - - - - /// - /// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange. - /// - /// Available challange types: - /// - /// dns-01 - /// http-01 - /// tls-alpn-01 - /// - /// - /// - /// - /// - /// - /// - public async Task> NewOrder(string[] hostnames, string challengeType) { - _challenges.Clear(); - - var letsEncryptOrder = new Order { - Expires = DateTime.UtcNow.AddDays(2), - Identifiers = hostnames.Select(hostname => new OrderIdentifier { - Type = "dns", - Value = hostname - }).ToArray() - }; - - var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); - - if (order.Status == "ready") - return new Dictionary(); - - if (order.Status != "pending") - throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}"); + if (order.Status != "pending") + throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}"); - _currentOrder = order; + _currentOrder = order; - var results = new Dictionary(); - foreach (var item in order.Authorizations) { - var (challengeResponse, responseText) = await SendAsync(HttpMethod.Post, item, true, null); + var results = new Dictionary(); + foreach (var item in order.Authorizations) { + var (challengeResponse, responseText) = await SendAsync(HttpMethod.Post, item, true, null); - if (challengeResponse.Status == "valid") - continue; - - if (challengeResponse.Status != "pending") - throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {responseText}"); - - var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType); - _challenges.Add(challenge); - - var keyToken = _jwsService.GetKeyAuthorization(challenge.Token); - - switch (challengeType) { - - // A client fulfills this challenge by constructing a key authorization - // from the "token" value provided in the challenge and the client's - // account key. The client then computes the SHA-256 digest [FIPS180-4] - // of the key authorization. - // - // The record provisioned to the DNS contains the base64url encoding of - // this digest. - - case "dns-01": { - using (var sha256 = SHA256.Create()) { - var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken))); - results[challengeResponse.Identifier.Value] = dnsToken; - } - break; - } - - - // A client fulfills this challenge by constructing a key authorization - // from the "token" value provided in the challenge and the client's - // account key. The client then provisions the key authorization as a - // resource on the HTTP server for the domain in question. - // - // The path at which the resource is provisioned is comprised of the - // fixed prefix "/.well-known/acme-challenge/", followed by the "token" - // value in the challenge. The value of the resource MUST be the ASCII - // representation of the key authorization. - - case "http-01": { - results[challengeResponse.Identifier.Value] = keyToken; - break; - } - - default: - throw new NotImplementedException(); - } - } - - return results; - } - - /// - /// - /// - /// - /// - public async Task CompleteChallenges() { - - for (var index = 0; index < _challenges.Count; index++) { - - var challenge = _challenges[index]; - - while (true) { - AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge(); - - switch (challenge.Type) { - case "dns-01": { - authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token); - //var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, authorizeChallenge, token); - break; - } - - case "http-01": { - break; - } - } - - var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, false, "{}"); - - if (result.Status == "valid") - break; - if (result.Status != "pending") - throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}"); - - await Task.Delay(1000); - } - } - } - - /// - /// - /// - /// - /// - public async Task GetOrder(string[] hostnames) { - - var letsEncryptOrder = new Order { - Expires = DateTime.UtcNow.AddDays(2), - Identifiers = hostnames.Select(hostname => new OrderIdentifier { - Type = "dns", - Value = hostname - }).ToArray() - }; - - var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); - - _currentOrder = order; - } - - /// - /// - /// - /// - /// Cert and Private key - /// - public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) { - - _logger.LogInformation($"Invoked: {nameof(GetCertificate)}"); - - - if (_currentOrder == null) - throw new ArgumentNullException(); - - var key = new RSACryptoServiceProvider(4096); - var csr = new CertificateRequest("CN=" + subject, - key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - var san = new SubjectAlternativeNameBuilder(); - foreach (var host in _currentOrder.Identifiers) - san.AddDnsName(host.Value); - - csr.CertificateExtensions.Add(san.Build()); - - var letsEncryptOrder = new FinalizeRequest { - Csr = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest()) - }; - - Uri? certificateUrl = default; - - - var start = DateTime.UtcNow; - - while (certificateUrl == null) { - // https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882 - await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray()); - - if (_currentOrder.Status == "ready") { - var (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder); - - if (response.Status == "processing") - (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Location, true, null); - - if (response.Status == "valid") { - certificateUrl = response.Certificate; - } - } - - if ((start - DateTime.UtcNow).Seconds > 120) - throw new TimeoutException(); - - await Task.Delay(1000); + if (challengeResponse.Status == "valid") continue; - // throw new InvalidOperationException(/*$"Invalid order status: "*/); + if (challengeResponse.Status != "pending") + throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {responseText}"); + + var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType); + _challenges.Add(challenge); + + var keyToken = _jwsService.GetKeyAuthorization(challenge.Token); + + switch (challengeType) { + + // A client fulfills this challenge by constructing a key authorization + // from the "token" value provided in the challenge and the client's + // account key. The client then computes the SHA-256 digest [FIPS180-4] + // of the key authorization. + // + // The record provisioned to the DNS contains the base64url encoding of + // this digest. + + case "dns-01": { + using (var sha256 = SHA256.Create()) { + var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken))); + results[challengeResponse.Identifier.Value] = dnsToken; + } + break; + } + + + // A client fulfills this challenge by constructing a key authorization + // from the "token" value provided in the challenge and the client's + // account key. The client then provisions the key authorization as a + // resource on the HTTP server for the domain in question. + // + // The path at which the resource is provisioned is comprised of the + // fixed prefix "/.well-known/acme-challenge/", followed by the "token" + // value in the challenge. The value of the resource MUST be the ASCII + // representation of the key authorization. + + case "http-01": { + results[challengeResponse.Identifier.Value] = keyToken; + break; + } + + default: + throw new NotImplementedException(); } - - var (pem, _) = await SendAsync(HttpMethod.Post, certificateUrl, true, null); - - if (_cache == null) - throw new NullReferenceException(); - - _cache.CachedCerts ??= new Dictionary(); - _cache.CachedCerts[subject] = new CertificateCache { - Cert = pem, - Private = key.ExportCspBlob(true) - }; - - var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem)); - - return (cert, key); } - /// - /// - /// - /// - /// - public Task KeyChange() { - throw new NotImplementedException(); - } + return results; + } - /// - /// - /// - /// - /// - public Task RevokeCertificate() { - throw new NotImplementedException(); - } + /// + /// + /// + /// + /// + public async Task CompleteChallenges() { - /// - /// Main method used to send data to LetsEncrypt - /// - /// - /// - /// - /// - /// - /// - private async Task<(TResult, string)> SendAsync(HttpMethod method, Uri uri, bool isPostAsGet, object? message) where TResult : class { - var request = new HttpRequestMessage(method, uri); + for (var index = 0; index < _challenges.Count; index++) { - _nonce = uri.OriginalString != "directory" - ? await NewNonce() - : default; + var challenge = _challenges[index]; - if (message != null || isPostAsGet) { - var jwsHeader = new JwsHeader { - Url = uri, - }; + while (true) { + AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge(); - if (_nonce != null) - jwsHeader.Nonce = _nonce; + switch (challenge.Type) { + case "dns-01": { + authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token); + //var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, authorizeChallenge, token); + break; + } - var encodedMessage = isPostAsGet - ? _jwsService.Encode(jwsHeader) - : _jwsService.Encode(message, jwsHeader); + case "http-01": { + break; + } + } - var json = encodedMessage.ToJson(); + var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, false, "{}"); - request.Content = new StringContent(json); + if (result.Status == "valid") + break; + if (result.Status != "pending") + throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}"); - var requestType = "application/json"; - if (method == HttpMethod.Post) - requestType = "application/jose+json"; - - request.Content.Headers.Remove("Content-Type"); - request.Content.Headers.Add("Content-Type", requestType); + await Task.Delay(1000); } - - var response = await _httpClient.SendAsync(request); - - if (method == HttpMethod.Post) - _nonce = response.Headers.GetValues("Replay-Nonce").First(); - - if (response.Content.Headers.ContentType.MediaType == "application/problem+json") { - var problemJson = await response.Content.ReadAsStringAsync(); - var problem = problemJson.ToObject(); - problem.RawJson = problemJson; - throw new LetsEncrytException(problem, response); - } - - var responseText = await response.Content.ReadAsStringAsync(); - - if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") { - return ((TResult)(object)responseText, null); - } - - var responseContent = responseText.ToObject(); - - if (responseContent is IHasLocation ihl) { - if (response.Headers.Location != null) - ihl.Location = response.Headers.Location; - } - - return (responseContent, responseText); - } - - /// - /// Request New Nonce to be able to start POST requests - /// - /// - /// - private async Task NewNonce() { - if (_directory == null) - throw new NotImplementedException(); - - var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce)); - return result.Headers.GetValues("Replay-Nonce").First(); } } -} \ No newline at end of file + + /// + /// + /// + /// + /// + public async Task GetOrder(string[] hostnames) { + + var letsEncryptOrder = new Order { + Expires = DateTime.UtcNow.AddDays(2), + Identifiers = hostnames.Select(hostname => new OrderIdentifier { + Type = "dns", + Value = hostname + }).ToArray() + }; + + var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + + _currentOrder = order; + } + + /// + /// + /// + /// + /// Cert and Private key + /// + public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) { + + _logger.LogInformation($"Invoked: {nameof(GetCertificate)}"); + + + if (_currentOrder == null) + throw new ArgumentNullException(); + + var key = new RSACryptoServiceProvider(4096); + var csr = new CertificateRequest("CN=" + subject, + key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + var san = new SubjectAlternativeNameBuilder(); + foreach (var host in _currentOrder.Identifiers) + san.AddDnsName(host.Value); + + csr.CertificateExtensions.Add(san.Build()); + + var letsEncryptOrder = new FinalizeRequest { + Csr = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest()) + }; + + Uri? certificateUrl = default; + + + var start = DateTime.UtcNow; + + while (certificateUrl == null) { + // https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882 + await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray()); + + if (_currentOrder.Status == "ready") { + var (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder); + + if (response.Status == "processing") + (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Location, true, null); + + if (response.Status == "valid") { + certificateUrl = response.Certificate; + } + } + + if ((start - DateTime.UtcNow).Seconds > 120) + throw new TimeoutException(); + + await Task.Delay(1000); + continue; + + // throw new InvalidOperationException(/*$"Invalid order status: "*/); + } + + var (pem, _) = await SendAsync(HttpMethod.Post, certificateUrl, true, null); + + if (_cache == null) + throw new NullReferenceException(); + + _cache.CachedCerts ??= new Dictionary(); + _cache.CachedCerts[subject] = new CertificateCache { + Cert = pem, + Private = key.ExportCspBlob(true) + }; + + var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem)); + + return (cert, key); + } + + /// + /// + /// + /// + /// + public Task KeyChange() { + throw new NotImplementedException(); + } + + /// + /// + /// + /// + /// + public Task RevokeCertificate() { + throw new NotImplementedException(); + } + + /// + /// Main method used to send data to LetsEncrypt + /// + /// + /// + /// + /// + /// + /// + private async Task<(TResult, string)> SendAsync(HttpMethod method, Uri uri, bool isPostAsGet, object? message) where TResult : class { + var request = new HttpRequestMessage(method, uri); + + _nonce = uri.OriginalString != "directory" + ? await NewNonce() + : default; + + if (message != null || isPostAsGet) { + var jwsHeader = new JwsHeader { + Url = uri, + }; + + if (_nonce != null) + jwsHeader.Nonce = _nonce; + + var encodedMessage = isPostAsGet + ? _jwsService.Encode(jwsHeader) + : _jwsService.Encode(message, jwsHeader); + + var json = encodedMessage.ToJson(); + + request.Content = new StringContent(json); + + var requestType = "application/json"; + if (method == HttpMethod.Post) + requestType = "application/jose+json"; + + request.Content.Headers.Remove("Content-Type"); + request.Content.Headers.Add("Content-Type", requestType); + } + + var response = await _httpClient.SendAsync(request); + + if (method == HttpMethod.Post) + _nonce = response.Headers.GetValues("Replay-Nonce").First(); + + if (response.Content.Headers.ContentType.MediaType == "application/problem+json") { + var problemJson = await response.Content.ReadAsStringAsync(); + var problem = problemJson.ToObject(); + problem.RawJson = problemJson; + throw new LetsEncrytException(problem, response); + } + + var responseText = await response.Content.ReadAsStringAsync(); + + if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") { + return ((TResult)(object)responseText, null); + } + + var responseContent = responseText.ToObject(); + + if (responseContent is IHasLocation ihl) { + if (response.Headers.Location != null) + ihl.Location = response.Headers.Location; + } + + return (responseContent, responseText); + } + + /// + /// Request New Nonce to be able to start POST requests + /// + /// + /// + private async Task NewNonce() { + if (_directory == null) + throw new NotImplementedException(); + + var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce)); + return result.Headers.GetValues("Replay-Nonce").First(); + } +} diff --git a/src/LetsEncryptConsole/App.cs b/src/LetsEncryptConsole/App.cs index a2a021e..0bf1031 100644 --- a/src/LetsEncryptConsole/App.cs +++ b/src/LetsEncryptConsole/App.cs @@ -1,7 +1,3 @@ -using System.Text; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; - using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -10,314 +6,268 @@ using MaksIT.Core.Extensions; using MaksIT.LetsEncrypt.Services; using MaksIT.LetsEncrypt.Entities; using MaksIT.LetsEncryptConsole.Services; -using SSHProvider; -using Mono.Unix.Native; -using Serilog.Core; -namespace MaksIT.LetsEncryptConsole { +using MaksIT.SSHProvider; - public interface IApp { +namespace MaksIT.LetsEncryptConsole; - Task Run(string[] args); +public interface IApp { + + Task Run(string[] args); +} + +public class App : IApp { + + private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; + + private readonly ILogger _logger; + private readonly Configuration _appSettings; + private readonly ILetsEncryptService _letsEncryptService; + private readonly IKeyService _keyService; + private readonly ITerminalService _terminalService; + + public App( + ILogger logger, + IOptions appSettings, + ILetsEncryptService letsEncryptService, + IKeyService keyService, + ITerminalService terminalService + ) { + _logger = logger; + _appSettings = appSettings.Value; + _letsEncryptService = letsEncryptService; + _keyService = keyService; + _terminalService = terminalService; } - public class App : IApp { + public async Task Run(string[] args) { - private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; + _logger.LogInformation("Letsencrypt client estarted..."); - private readonly ILogger _logger; - private readonly Configuration _appSettings; - private readonly ILetsEncryptService _letsEncryptService; - private readonly IKeyService _keyService; - private readonly ITerminalService _terminalService; + foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List()) { + try { + _logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}"); - public App( - ILogger logger, - IOptions appSettings, - ILetsEncryptService letsEncryptService, - IKeyService keyService, - ITerminalService terminalService - ) { - _logger = logger; - _appSettings = appSettings.Value; - _letsEncryptService = letsEncryptService; - _keyService = keyService; - _terminalService = terminalService; - } + //loop all customers + foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List()) { + try { + _logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}"); - public async Task Run(string[] args) { + //define cache folder + string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache"); + if (!Directory.Exists(cachePath)) { + Directory.CreateDirectory(cachePath); + } - _logger.LogInformation("Letsencrypt client estarted..."); + //check acme directory + var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme"); + if (!Directory.Exists(acmePath)) { + Directory.CreateDirectory(acmePath); + } - foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List()) { - try { - _logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}"); + //loop each customer website + foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List()) { + _logger.LogInformation($"Managing site: {site.Name}"); - //loop all customers - foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List()) { - try { - _logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}"); + try { + //create folder for ssl + string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name); + if (!Directory.Exists(sslPath)) { + Directory.CreateDirectory(sslPath); + } - //define cache folder - string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache"); - if (!Directory.Exists(cachePath)) { - Directory.CreateDirectory(cachePath); - } + var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json"); - //check acme directory - var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme"); - if (!Directory.Exists(acmePath)) { - Directory.CreateDirectory(acmePath); - } + //1. Client initialization + _logger.LogInformation("1. Client Initialization..."); - //loop each customer website - foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List()) { - _logger.LogInformation($"Managing site: {site.Name}"); + #region LetsEncrypt client configuration + await _letsEncryptService.ConfigureClient(env.Url); + #endregion - try { - //create folder for ssl - string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name); - if (!Directory.Exists(sslPath)) { - Directory.CreateDirectory(sslPath); - } + #region LetsEncrypt local registration cache initialization + var registrationCache = (File.Exists(cacheFile) + ? File.ReadAllText(cacheFile) + : null) + .ToObject(); - var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json"); + await _letsEncryptService.Init(customer.Contacts, registrationCache); + #endregion - //1. Client initialization - _logger.LogInformation("1. Client Initialization..."); + #region LetsEncrypt terms of service + _logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}"); + #endregion - #region LetsEncrypt client configuration - await _letsEncryptService.ConfigureClient(env.Url); - #endregion + // get cached certificate and check if it's valid + // if valid check if cert and key exists otherwise recreate + // else continue with new certificate request + var certRes = new CachedCertificateResult(); + if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { + string cert = Path.Combine(sslPath, $"{site.Name}.crt"); + //if(!File.Exists(cert)) + File.WriteAllText(cert, certRes.Certificate); - #region LetsEncrypt local registration cache initialization - var registrationCache = (File.Exists(cacheFile) - ? File.ReadAllText(cacheFile) - : null) - .ToObject(); + string key = Path.Combine(sslPath, $"{site.Name}.key"); + //if(!File.Exists(key)) { + using (StreamWriter writer = File.CreateText(key)) + _keyService.ExportPrivateKey(certRes.PrivateKey, writer); + //} - await _letsEncryptService.Init(customer.Contacts, registrationCache); - #endregion + _logger.LogInformation("Certificate and Key exists and valid. Restored from cache."); + } + else { - #region LetsEncrypt terms of service - _logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}"); - #endregion + //try to make new order + try { + //create new orders + Console.WriteLine("2. Client New Order..."); - // get cached certificate and check if it's valid - // if valid check if cert and key exists otherwise recreate - // else continue with new certificate request - var certRes = new CachedCertificateResult(); - if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) { - string cert = Path.Combine(sslPath, $"{site.Name}.crt"); - //if(!File.Exists(cert)) - File.WriteAllText(cert, certRes.Certificate); + #region LetsEncrypt new order + var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge); + #endregion - string key = Path.Combine(sslPath, $"{site.Name}.key"); - //if(!File.Exists(key)) { - using (StreamWriter writer = File.CreateText(key)) - _keyService.ExportPrivateKey(certRes.PrivateKey, writer); - //} + if (orders.Count > 0) { + switch (site.Challenge) { + case "http-01": { + //ensure to enable static file discovery on server in .well-known/acme-challenge + //and listen on 80 port - _logger.LogInformation("Certificate and Key exists and valid. Restored from cache."); - } - else { + foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) + file.Delete(); - //try to make new order - try { - //create new orders - Console.WriteLine("2. Client New Order..."); + foreach (var result in orders) { + Console.WriteLine($"Key: {result.Key}, Value: {result.Value}"); + string[] splitToken = result.Value.Split('.'); - #region LetsEncrypt new order - var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge); - #endregion + File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value); + } - if (orders.Count > 0) { - switch (site.Challenge) { - case "http-01": { - //ensure to enable static file discovery on server in .well-known/acme-challenge - //and listen on 80 port - - foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) - file.Delete(); - - foreach (var result in orders) { - Console.WriteLine($"Key: {result.Key}, Value: {result.Value}"); - string[] splitToken = result.Value.Split('.'); - - File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value); + foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) { + if (env?.SSH?.Active ?? false) { + UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode); } - - foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) { - if (env?.SSH?.Active ?? false) { - UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode); - } - else { - throw new NotImplementedException(); - } + else { + throw new NotImplementedException(); } - - - - - - break; } - case "dns-01": { - //Manage DNS server MX record, depends from provider - throw new NotImplementedException(); - } - - default: { - throw new NotImplementedException(); - } - } - - - #region LetsEncrypt complete challenges - _logger.LogInformation("3. Client Complete Challange..."); - await _letsEncryptService.CompleteChallenges(); - _logger.LogInformation("Challanges comleted."); - #endregion - - await Task.Delay(1000); - - // Download new certificate - _logger.LogInformation("4. Download certificate..."); - var (cert, key) = await _letsEncryptService.GetCertificate(site.Name); - - #region Persist cache - registrationCache = _letsEncryptService.GetRegistrationCache(); - File.WriteAllText(cacheFile, registrationCache.ToJson()); - #endregion - } - - #region Save cert and key to filesystem - certRes = new CachedCertificateResult(); - if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) { - - File.WriteAllText(Path.Combine(sslPath, site.Name + ".crt"), certRes.Certificate); - - using (var writer = File.CreateText(Path.Combine(sslPath, site.Name + ".key"))) { - _keyService.ExportPrivateKey(certRes.PrivateKey, writer); - } - - _logger.LogInformation("Certificate saved."); - - foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) { - - if (env?.SSH?.Active ?? false) { - UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode); + break; } - else { + + case "dns-01": { + //Manage DNS server MX record, depends from provider throw new NotImplementedException(); } - } + default: { + throw new NotImplementedException(); + } } - else { - _logger.LogError("Unable to get new cached certificate."); - } + + + #region LetsEncrypt complete challenges + _logger.LogInformation("3. Client Complete Challange..."); + await _letsEncryptService.CompleteChallenges(); + _logger.LogInformation("Challanges comleted."); + #endregion + + await Task.Delay(1000); + + #region Download new certificate + _logger.LogInformation("4. Download certificate..."); + var (cert, key) = await _letsEncryptService.GetCertificate(site.Name); + #endregion + + #region Persist cache + registrationCache = _letsEncryptService.GetRegistrationCache(); + File.WriteAllText(cacheFile, registrationCache.ToJson()); #endregion } - catch (Exception ex) { - _logger.LogError(ex, ""); - await _letsEncryptService.GetOrder(site.Hosts); - } + #region Save cert and key to filesystem + certRes = new CachedCertificateResult(); + if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { + + File.WriteAllText(Path.Combine(sslPath, site.Name + ".crt"), certRes.Certificate); + + using (var writer = File.CreateText(Path.Combine(sslPath, site.Name + ".key"))) { + _keyService.ExportPrivateKey(certRes.PrivateKey, writer); + } + + _logger.LogInformation("Certificate saved."); + + foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) { + + if (env?.SSH?.Active ?? false) { + UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode); + } + else { + throw new NotImplementedException(); + } + } + + } + else { + _logger.LogError("Unable to get new cached certificate."); + } + #endregion + } + catch (Exception ex) { + _logger.LogError(ex, ""); + await _letsEncryptService.GetOrder(site.Hosts); } - - - - } - catch (Exception ex) { - _logger.LogError(ex, "Customer unhandled error"); } + + + + + } + catch (Exception ex) { + _logger.LogError(ex, "Customer unhandled error"); } } - catch (Exception ex) { - _logger.LogError(ex, "Environment unhandled error"); - } } - - if (env.Name == "ProductionV2") { - _terminalService.Exec("systemctl restart nginx"); + catch (Exception ex) { + _logger.LogError(ex, "Environment unhandled error"); } } - catch (Exception ex) { - _logger.LogError(ex.Message.ToString()); - break; + + if (env.Name == "ProductionV2") { + _terminalService.Exec("systemctl restart nginx"); } } - } - - /// - /// - /// - /// - /// - /// - private bool TryGetCachedCertificate(RegistrationCache? registrationCache, string subject, out CachedCertificateResult? value) { - value = null; - - if (registrationCache?.CachedCerts == null) - return false; - - if (!registrationCache.CachedCerts.TryGetValue(subject, out var cache)) { - return false; + catch (Exception ex) { + _logger.LogError(ex.Message.ToString()); + break; } - - var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cache.Cert)); - - // if it is about to expire, we need to refresh - if ((cert.NotAfter - DateTime.UtcNow).TotalDays < 30) - return false; - - var rsa = new RSACryptoServiceProvider(4096); - rsa.ImportCspBlob(cache.Private); - - value = new CachedCertificateResult { - Certificate = cache.Cert, - PrivateKey = rsa - }; - return true; - } - - - /// - /// - /// - /// - public RegistrationCache? ResetCachedCertificate(RegistrationCache? registrationCache, IEnumerable hostsToRemove) { - if (registrationCache?.CachedCerts != null) - foreach (var host in hostsToRemove) - registrationCache.CachedCerts.Remove(host); - - return registrationCache; - } - private void UploadFiles( - ILogger logger, - SSHClientSettings sshSettings, - string workDir, - string fileName, - byte [] bytes, - string owner, - string changeMode - ) { - - using var sshService = new SSHService(logger, sshSettings.Host, sshSettings.Port, sshSettings.Username, sshSettings.Password); - sshService.Connect(); - - sshService.RunSudoCommand(sshSettings.Password, $"mkdir {workDir}"); - - sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R"); - sshService.RunSudoCommand(sshSettings.Password, $"chmod 777 {workDir} -R"); - - sshService.Upload($"{workDir}", fileName, bytes); - - sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R"); - sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R"); } } + + + + + private void UploadFiles( + ILogger logger, + SSHClientSettings sshSettings, + string workDir, + string fileName, + byte [] bytes, + string owner, + string changeMode + ) { + + using var sshService = new SSHService(logger, sshSettings.Host, sshSettings.Port, sshSettings.Username, sshSettings.Password); + sshService.Connect(); + + sshService.RunSudoCommand(sshSettings.Password, $"mkdir {workDir}"); + + sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R"); + sshService.RunSudoCommand(sshSettings.Password, $"chmod 777 {workDir} -R"); + + sshService.Upload($"{workDir}", fileName, bytes); + + sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R"); + sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R"); + } } diff --git a/src/LetsEncryptConsole/Configuration.cs b/src/LetsEncryptConsole/Configuration.cs index afec942..58d0cb1 100644 --- a/src/LetsEncryptConsole/Configuration.cs +++ b/src/LetsEncryptConsole/Configuration.cs @@ -1,40 +1,40 @@ -using System.Runtime.InteropServices; -namespace MaksIT.LetsEncryptConsole { - public class Configuration { - public LetsEncryptEnvironment[]? Environments { get; set; } - public Customer[]? Customers { get; set; } - } +namespace MaksIT.LetsEncryptConsole; - public class OsWindows { - public string? Path { get; set; } - } +public class Configuration { + public LetsEncryptEnvironment[]? Environments { get; set; } + public Customer[]? Customers { get; set; } +} - public class OsLinux { - public string? Path { get; set; } +public class OsWindows { + public string? Path { get; set; } +} - public string? Owner { get; set; } +public class OsLinux { + public string? Path { get; set; } - public string? ChangeMode { get; set; } + public string? Owner { get; set; } - } + public string? ChangeMode { get; set; } - public class OsDependant { - public OsWindows? Windows { get; set; } - public OsLinux? Linux { get; set; } - } +} - public class SSHClientSettings { - public bool Active { get; set; } +public class OsDependant { + public OsWindows? Windows { get; set; } + public OsLinux? Linux { get; set; } +} - public string? Host { get; set; } +public class SSHClientSettings { + public bool Active { get; set; } - public int Port { get; set; } + public string? Host { get; set; } - public string? Username { get; set; } + public int Port { get; set; } - public string? Password { get; set; } - } + public string? Username { get; set; } + + public string? Password { get; set; } +} @@ -69,4 +69,3 @@ namespace MaksIT.LetsEncryptConsole { public string[]? Hosts { get; set; } public string? Challenge { get; set; } } -} diff --git a/src/LetsEncryptConsole/Program.cs b/src/LetsEncryptConsole/Program.cs index 2b205e2..e9d7efe 100644 --- a/src/LetsEncryptConsole/Program.cs +++ b/src/LetsEncryptConsole/Program.cs @@ -6,65 +6,66 @@ using Serilog; using MaksIT.LetsEncryptConsole.Services; using MaksIT.LetsEncrypt.Extensions; -namespace MaksIT.LetsEncryptConsole { - class Program { - private static readonly IConfiguration _configuration = InitConfig(); +namespace MaksIT.LetsEncryptConsole; - static void Main(string[] args) { - // create service collection - var services = new ServiceCollection(); - ConfigureServices(services); +class Program { + private static readonly IConfiguration _configuration = InitConfig(); - // create service provider - var serviceProvider = services.BuildServiceProvider(); + static void Main(string[] args) { + // create service collection + var services = new ServiceCollection(); + ConfigureServices(services); - // entry to run app + // create service provider + var serviceProvider = services.BuildServiceProvider(); + + // entry to run app #pragma warning disable CS8602 // Dereference of a possibly null reference. - var app = serviceProvider.GetService(); - app.Run(args).Wait(); + var app = serviceProvider.GetService(); + app.Run(args).Wait(); #pragma warning restore CS8602 // Dereference of a possibly null reference. - } + } - public static void ConfigureServices(IServiceCollection services) { + public static void ConfigureServices(IServiceCollection services) { - var configurationSection = _configuration.GetSection("Configuration"); - services.Configure(configurationSection); - var appSettings = configurationSection.Get(); + var configurationSection = _configuration.GetSection("Configuration"); + services.Configure(configurationSection); + var appSettings = configurationSection.Get(); - #region Configure logging - services.AddLogging(configure => { - configure.AddSerilog(new LoggerConfiguration() - .ReadFrom.Configuration(_configuration) - .CreateLogger()); - }); - #endregion + #region Configure logging + services.AddLogging(configure => { + configure.AddSerilog(new LoggerConfiguration() + .ReadFrom.Configuration(_configuration) + .CreateLogger()); + }); + #endregion - #region Services - services.RegisterLetsEncrypt(); + #region Services + services.RegisterLetsEncrypt(); - services.AddSingleton(); - services.AddSingleton(); - #endregion + services.AddSingleton(); + services.AddSingleton(); + #endregion - // add app - services.AddSingleton(); - } + // add app + services.AddSingleton(); + } - private static IConfiguration InitConfig() { - var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + private static IConfiguration InitConfig() { + var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddEnvironmentVariables(); + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddEnvironmentVariables(); - if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) - && new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), $"appsettings.{aspNetCoreEnvironment}.json")).Exists - ) - configuration.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true); - else - configuration.AddJsonFile($"appsettings.json", true, true); + if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) + && new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), $"appsettings.{aspNetCoreEnvironment}.json")).Exists + ) + configuration.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true); + else + configuration.AddJsonFile($"appsettings.json", true, true); - return configuration.Build(); - } + return configuration.Build(); } } + diff --git a/src/LetsEncryptConsole/Services/KeyService.cs b/src/LetsEncryptConsole/Services/KeyService.cs index 26743fc..5718db1 100644 --- a/src/LetsEncryptConsole/Services/KeyService.cs +++ b/src/LetsEncryptConsole/Services/KeyService.cs @@ -1,151 +1,150 @@ using System.Security.Cryptography; -namespace MaksIT.LetsEncryptConsole.Services { +namespace MaksIT.LetsEncryptConsole.Services; - public interface IKeyService { - void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream); - void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream); +public interface IKeyService { + void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream); + void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream); +} + +public class KeyService : IKeyService { + /// + /// Export a certificate to a PEM format string + /// + /// The certificate to export + /// A PEM encoded string + //public static string ExportToPEM(X509Certificate2 cert) + //{ + // StringBuilder builder = new StringBuilder(); + + // builder.AppendLine("-----BEGIN CERTIFICATE-----"); + // builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); + // builder.AppendLine("-----END CERTIFICATE-----"); + + // return builder.ToString(); + //} + public void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream) { + var parameters = csp.ExportParameters(false); + using (var stream = new MemoryStream()) { + var writer = new BinaryWriter(stream); + writer.Write((byte)0x30); // SEQUENCE + using (var innerStream = new MemoryStream()) { + var innerWriter = new BinaryWriter(innerStream); + innerWriter.Write((byte)0x30); // SEQUENCE + EncodeLength(innerWriter, 13); + innerWriter.Write((byte)0x06); // OBJECT IDENTIFIER + var rsaEncryptionOid = new byte[] { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01 }; + EncodeLength(innerWriter, rsaEncryptionOid.Length); + innerWriter.Write(rsaEncryptionOid); + innerWriter.Write((byte)0x05); // NULL + EncodeLength(innerWriter, 0); + innerWriter.Write((byte)0x03); // BIT STRING + using (var bitStringStream = new MemoryStream()) { + var bitStringWriter = new BinaryWriter(bitStringStream); + bitStringWriter.Write((byte)0x00); // # of unused bits + bitStringWriter.Write((byte)0x30); // SEQUENCE + using (var paramsStream = new MemoryStream()) { + var paramsWriter = new BinaryWriter(paramsStream); + EncodeIntegerBigEndian(paramsWriter, parameters.Modulus); // Modulus + EncodeIntegerBigEndian(paramsWriter, parameters.Exponent); // Exponent + var paramsLength = (int)paramsStream.Length; + EncodeLength(bitStringWriter, paramsLength); + bitStringWriter.Write(paramsStream.GetBuffer(), 0, paramsLength); + } + var bitStringLength = (int)bitStringStream.Length; + EncodeLength(innerWriter, bitStringLength); + innerWriter.Write(bitStringStream.GetBuffer(), 0, bitStringLength); + } + var length = (int)innerStream.Length; + EncodeLength(writer, length); + writer.Write(innerStream.GetBuffer(), 0, length); + } + + var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); + outputStream.WriteLine("-----BEGIN PUBLIC KEY-----"); + for (var i = 0; i < base64.Length; i += 64) { + outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i)); + } + outputStream.WriteLine("-----END PUBLIC KEY-----"); + } } - public class KeyService : IKeyService { - /// - /// Export a certificate to a PEM format string - /// - /// The certificate to export - /// A PEM encoded string - //public static string ExportToPEM(X509Certificate2 cert) - //{ - // StringBuilder builder = new StringBuilder(); + public void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream) { + if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp"); + var parameters = csp.ExportParameters(true); + using (var stream = new MemoryStream()) { + var writer = new BinaryWriter(stream); + writer.Write((byte)0x30); // SEQUENCE + using (var innerStream = new MemoryStream()) { + var innerWriter = new BinaryWriter(innerStream); + EncodeIntegerBigEndian(innerWriter, new byte[] { 0x00 }); // Version + EncodeIntegerBigEndian(innerWriter, parameters.Modulus); + EncodeIntegerBigEndian(innerWriter, parameters.Exponent); + EncodeIntegerBigEndian(innerWriter, parameters.D); + EncodeIntegerBigEndian(innerWriter, parameters.P); + EncodeIntegerBigEndian(innerWriter, parameters.Q); + EncodeIntegerBigEndian(innerWriter, parameters.DP); + EncodeIntegerBigEndian(innerWriter, parameters.DQ); + EncodeIntegerBigEndian(innerWriter, parameters.InverseQ); + var length = (int)innerStream.Length; + EncodeLength(writer, length); + writer.Write(innerStream.GetBuffer(), 0, length); + } - // builder.AppendLine("-----BEGIN CERTIFICATE-----"); - // builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); - // builder.AppendLine("-----END CERTIFICATE-----"); + var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); + outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----"); + // Output as Base64 with lines chopped at 64 characters + for (var i = 0; i < base64.Length; i += 64) { + outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i)); + } + outputStream.WriteLine("-----END RSA PRIVATE KEY-----"); + } + } - // return builder.ToString(); - //} - public void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream) { - var parameters = csp.ExportParameters(false); - using (var stream = new MemoryStream()) { - var writer = new BinaryWriter(stream); - writer.Write((byte)0x30); // SEQUENCE - using (var innerStream = new MemoryStream()) { - var innerWriter = new BinaryWriter(innerStream); - innerWriter.Write((byte)0x30); // SEQUENCE - EncodeLength(innerWriter, 13); - innerWriter.Write((byte)0x06); // OBJECT IDENTIFIER - var rsaEncryptionOid = new byte[] { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01 }; - EncodeLength(innerWriter, rsaEncryptionOid.Length); - innerWriter.Write(rsaEncryptionOid); - innerWriter.Write((byte)0x05); // NULL - EncodeLength(innerWriter, 0); - innerWriter.Write((byte)0x03); // BIT STRING - using (var bitStringStream = new MemoryStream()) { - var bitStringWriter = new BinaryWriter(bitStringStream); - bitStringWriter.Write((byte)0x00); // # of unused bits - bitStringWriter.Write((byte)0x30); // SEQUENCE - using (var paramsStream = new MemoryStream()) { - var paramsWriter = new BinaryWriter(paramsStream); - EncodeIntegerBigEndian(paramsWriter, parameters.Modulus); // Modulus - EncodeIntegerBigEndian(paramsWriter, parameters.Exponent); // Exponent - var paramsLength = (int)paramsStream.Length; - EncodeLength(bitStringWriter, paramsLength); - bitStringWriter.Write(paramsStream.GetBuffer(), 0, paramsLength); - } - var bitStringLength = (int)bitStringStream.Length; - EncodeLength(innerWriter, bitStringLength); - innerWriter.Write(bitStringStream.GetBuffer(), 0, bitStringLength); - } - var length = (int)innerStream.Length; - EncodeLength(writer, length); - writer.Write(innerStream.GetBuffer(), 0, length); - } - - var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); - outputStream.WriteLine("-----BEGIN PUBLIC KEY-----"); - for (var i = 0; i < base64.Length; i += 64) { - outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i)); - } - outputStream.WriteLine("-----END PUBLIC KEY-----"); + private void EncodeLength(BinaryWriter stream, int length) { + if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative"); + if (length < 0x80) { + // Short form + stream.Write((byte)length); + } + else { + // Long form + var temp = length; + var bytesRequired = 0; + while (temp > 0) { + temp >>= 8; + bytesRequired++; + } + stream.Write((byte)(bytesRequired | 0x80)); + for (var i = bytesRequired - 1; i >= 0; i--) { + stream.Write((byte)(length >> (8 * i) & 0xff)); } } + } - public void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream) { - if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp"); - var parameters = csp.ExportParameters(true); - using (var stream = new MemoryStream()) { - var writer = new BinaryWriter(stream); - writer.Write((byte)0x30); // SEQUENCE - using (var innerStream = new MemoryStream()) { - var innerWriter = new BinaryWriter(innerStream); - EncodeIntegerBigEndian(innerWriter, new byte[] { 0x00 }); // Version - EncodeIntegerBigEndian(innerWriter, parameters.Modulus); - EncodeIntegerBigEndian(innerWriter, parameters.Exponent); - EncodeIntegerBigEndian(innerWriter, parameters.D); - EncodeIntegerBigEndian(innerWriter, parameters.P); - EncodeIntegerBigEndian(innerWriter, parameters.Q); - EncodeIntegerBigEndian(innerWriter, parameters.DP); - EncodeIntegerBigEndian(innerWriter, parameters.DQ); - EncodeIntegerBigEndian(innerWriter, parameters.InverseQ); - var length = (int)innerStream.Length; - EncodeLength(writer, length); - writer.Write(innerStream.GetBuffer(), 0, length); - } - - var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); - outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----"); - // Output as Base64 with lines chopped at 64 characters - for (var i = 0; i < base64.Length; i += 64) { - outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i)); - } - outputStream.WriteLine("-----END RSA PRIVATE KEY-----"); - } + private void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true) { + stream.Write((byte)0x02); // INTEGER + var prefixZeros = 0; + for (var i = 0; i < value.Length; i++) { + if (value[i] != 0) break; + prefixZeros++; } - - private void EncodeLength(BinaryWriter stream, int length) { - if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative"); - if (length < 0x80) { - // Short form - stream.Write((byte)length); - } - else { - // Long form - var temp = length; - var bytesRequired = 0; - while (temp > 0) { - temp >>= 8; - bytesRequired++; - } - stream.Write((byte)(bytesRequired | 0x80)); - for (var i = bytesRequired - 1; i >= 0; i--) { - stream.Write((byte)(length >> (8 * i) & 0xff)); - } - } + if (value.Length - prefixZeros == 0) { + EncodeLength(stream, 1); + stream.Write((byte)0); } - - private void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true) { - stream.Write((byte)0x02); // INTEGER - var prefixZeros = 0; - for (var i = 0; i < value.Length; i++) { - if (value[i] != 0) break; - prefixZeros++; - } - if (value.Length - prefixZeros == 0) { - EncodeLength(stream, 1); + else { + if (forceUnsigned && value[prefixZeros] > 0x7f) { + // Add a prefix zero to force unsigned if the MSB is 1 + EncodeLength(stream, value.Length - prefixZeros + 1); stream.Write((byte)0); } else { - if (forceUnsigned && value[prefixZeros] > 0x7f) { - // Add a prefix zero to force unsigned if the MSB is 1 - EncodeLength(stream, value.Length - prefixZeros + 1); - stream.Write((byte)0); - } - else { - EncodeLength(stream, value.Length - prefixZeros); - } - for (var i = prefixZeros; i < value.Length; i++) { - stream.Write(value[i]); - } + EncodeLength(stream, value.Length - prefixZeros); + } + for (var i = prefixZeros; i < value.Length; i++) { + stream.Write(value[i]); } } } -} \ No newline at end of file +} diff --git a/src/LetsEncryptConsole/Services/TerminalService.cs b/src/LetsEncryptConsole/Services/TerminalService.cs index f65a119..7607531 100644 --- a/src/LetsEncryptConsole/Services/TerminalService.cs +++ b/src/LetsEncryptConsole/Services/TerminalService.cs @@ -1,30 +1,28 @@ using System.Diagnostics; -namespace MaksIT.LetsEncryptConsole.Services { +namespace MaksIT.LetsEncryptConsole.Services; - public interface ITerminalService { - void Exec(string cmd); - } +public interface ITerminalService { + void Exec(string cmd); +} - public class TerminalService : ITerminalService { +public class TerminalService : ITerminalService { - public void Exec(string cmd) { - var escapedArgs = cmd.Replace("\"", "\\\""); + public void Exec(string cmd) { + var escapedArgs = cmd.Replace("\"", "\\\""); - var pc = new Process { - StartInfo = new ProcessStartInfo { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - FileName = "/bin/bash", - Arguments = $"-c \"{escapedArgs}\"" - } - }; + var pc = new Process { + StartInfo = new ProcessStartInfo { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + FileName = "/bin/bash", + Arguments = $"-c \"{escapedArgs}\"" + } + }; - pc.Start(); - pc.WaitForExit(); - } + pc.Start(); + pc.WaitForExit(); } - -} \ No newline at end of file +} diff --git a/src/SSHProvider/Configuration.cs b/src/SSHProvider/Configuration.cs index e562e50..7f082ab 100644 --- a/src/SSHProvider/Configuration.cs +++ b/src/SSHProvider/Configuration.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; + +namespace MaksIT.SSHProvider; -namespace SSHProvider { - public class Configuration { - } +public class Configuration { } diff --git a/src/SSHProvider/SSHService.cs b/src/SSHProvider/SSHService.cs index 89b61e0..90ef7e2 100644 --- a/src/SSHProvider/SSHService.cs +++ b/src/SSHProvider/SSHService.cs @@ -5,7 +5,7 @@ using Renci.SshNet; using Renci.SshNet.Common; using System.Text.RegularExpressions; -namespace SSHProvider { +namespace MaksIT.SSHProvider { public interface ISSHService : IDisposable { IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes); @@ -77,8 +77,8 @@ namespace SSHProvider { _logger.LogInformation($"Listing directory:"); - foreach (var fi in listDirectory) { - _logger.LogInformation($" - " + fi.Name); + foreach (var file in listDirectory) { + _logger.LogInformation($" - " + file.Name); } return IDomainResult.Success(); diff --git a/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs b/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs index b67b6ca..147f91e 100644 --- a/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs +++ b/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs @@ -7,57 +7,56 @@ using Xunit; //using PecMgr.VaultProvider; //using PecMgr.Core.Abstractions; -namespace MaksIT.Tests.SSHProviderTests.Abstractions { - //[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] - public abstract class ConfigurationBase { +namespace MaksIT.Tests.SSHProviderTests.Abstractions; +//[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] +public abstract class ConfigurationBase { - protected IConfiguration Configuration; + protected IConfiguration Configuration; - protected ServiceCollection ServiceCollection = new ServiceCollection(); + protected ServiceCollection ServiceCollection = new ServiceCollection(); - protected ServiceProvider ServiceProvider { get => ServiceCollection.BuildServiceProvider(); } + protected ServiceProvider ServiceProvider { get => ServiceCollection.BuildServiceProvider(); } - public ConfigurationBase() { - Configuration = InitConfig(); - ConfigureServices(ServiceCollection); - } + public ConfigurationBase() { + Configuration = InitConfig(); + ConfigureServices(ServiceCollection); + } - protected abstract void ConfigureServices(IServiceCollection services); + protected abstract void ConfigureServices(IServiceCollection services); - private IConfiguration InitConfig() { - var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - var currentDirectory = Directory.GetCurrentDirectory(); + private IConfiguration InitConfig() { + var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + var currentDirectory = Directory.GetCurrentDirectory(); - var configurationBuilder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddEnvironmentVariables(); + var configurationBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddEnvironmentVariables(); - if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists) - configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true); - else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists) - configurationBuilder.AddJsonFile("appsettings.json", true, true); - else - throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}"); + if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists) + configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true); + else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists) + configurationBuilder.AddJsonFile("appsettings.json", true, true); + else + throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}"); - //var builtConfig = configurationBuilder.Build(); - //var vaultOptions = builtConfig.GetSection("Vault"); + //var builtConfig = configurationBuilder.Build(); + //var vaultOptions = builtConfig.GetSection("Vault"); - //configurationBuilder.AddVault(options => { - // options.Address = vaultOptions["Address"]; + //configurationBuilder.AddVault(options => { + // options.Address = vaultOptions["Address"]; - // options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get>(); + // options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get>(); - // options.AuthMethod = EnumerationStringId.FromValue(vaultOptions["AuthMethod"]); - // options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get(); - // options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get(); + // options.AuthMethod = EnumerationStringId.FromValue(vaultOptions["AuthMethod"]); + // options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get(); + // options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get(); - // options.MountPath = vaultOptions["MountPath"]; - // options.SecretType = vaultOptions["SecretType"]; + // options.MountPath = vaultOptions["MountPath"]; + // options.SecretType = vaultOptions["SecretType"]; - // options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get>(); - //}); + // options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get>(); + //}); - return configurationBuilder.Build(); - } + return configurationBuilder.Build(); } } diff --git a/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs b/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs index 7d52b27..b608520 100644 --- a/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs +++ b/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs @@ -4,27 +4,26 @@ using Serilog; using Microsoft.Extensions.Configuration; -using SSHProvider; +using MaksIT.SSHProvider; -namespace MaksIT.Tests.SSHProviderTests.Abstractions { - public abstract class ServicesBase : ConfigurationBase { +namespace MaksIT.Tests.SSHProviderTests.Abstractions; +public abstract class ServicesBase : ConfigurationBase { - public ServicesBase() : base() { } + public ServicesBase() : base() { } - protected override void ConfigureServices(IServiceCollection services) { - // configure strongly typed settings objects - var appSettingsSection = Configuration.GetSection("Configuration"); - services.Configure(appSettingsSection); - var appSettings = appSettingsSection.Get(); + protected override void ConfigureServices(IServiceCollection services) { + // configure strongly typed settings objects + var appSettingsSection = Configuration.GetSection("Configuration"); + services.Configure(appSettingsSection); + var appSettings = appSettingsSection.Get(); - #region configurazione logging - services.AddLogging(configure => { - configure.AddSerilog(new LoggerConfiguration() - //.ReadFrom.Configuration(_configuration) - .CreateLogger()); - }); - #endregion + #region configurazione logging + services.AddLogging(configure => { + configure.AddSerilog(new LoggerConfiguration() + //.ReadFrom.Configuration(_configuration) + .CreateLogger()); + }); + #endregion - } } } \ No newline at end of file diff --git a/src/Tests/SSHSerivceTests/UnitTest1.cs b/src/Tests/SSHSerivceTests/UnitTest1.cs index 6d87802..b4cfa22 100644 --- a/src/Tests/SSHSerivceTests/UnitTest1.cs +++ b/src/Tests/SSHSerivceTests/UnitTest1.cs @@ -3,54 +3,53 @@ using System.Security.Cryptography; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using SSHProvider; +using MaksIT.SSHProvider; using MaksIT.Tests.SSHProviderTests.Abstractions; -namespace SSHSerivceTests { - public class UnitTest1 : ServicesBase { +namespace MaksIT.SSHSerivceTests; +public class UnitTest1 : ServicesBase { - public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; + public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; - [Fact] - public void UploadFile() { + [Fact] + public void UploadFile() { - var username = ""; - var password = ""; - var filePath = Path.Combine(_appPath, "randomfile.txt"); - CreateRandomFile(filePath, 1); + var username = ""; + var password = ""; + var filePath = Path.Combine(_appPath, "randomfile.txt"); + CreateRandomFile(filePath, 1); - var logger = ServiceProvider.GetService>(); + var logger = ServiceProvider.GetService>(); - using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password); - sshService.Connect(); + using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password); + sshService.Connect(); - var bytes = File.ReadAllBytes(filePath); + var bytes = File.ReadAllBytes(filePath); - logger.LogInformation($"Uploading {filePath} ({bytes.Length:N0} bytes)"); + logger.LogInformation($"Uploading {filePath} ({bytes.Length:N0} bytes)"); - sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R"); - sshService.RunSudoCommand(password, "chmod 777 /var/www/ssl -R"); + sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R"); + sshService.RunSudoCommand(password, "chmod 777 /var/www/ssl -R"); - sshService.Upload("/var/www/ssl", Path.GetFileName(filePath), bytes); + sshService.Upload("/var/www/ssl", Path.GetFileName(filePath), bytes); - sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R"); - sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R"); - } + sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R"); + sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R"); + } - private void CreateRandomFile(string filePath, int sizeInMb) { - // Note: block size must be a factor of 1MB to avoid rounding errors - const int blockSize = 1024 * 8; - const int blocksPerMb = (1024 * 1024) / blockSize; + private void CreateRandomFile(string filePath, int sizeInMb) { + // Note: block size must be a factor of 1MB to avoid rounding errors + const int blockSize = 1024 * 8; + const int blocksPerMb = (1024 * 1024) / blockSize; - byte[] data = new byte[blockSize]; + byte[] data = new byte[blockSize]; - using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) { - using (FileStream stream = File.OpenWrite(filePath)) { - for (int i = 0; i < sizeInMb * blocksPerMb; i++) { - crypto.GetBytes(data); - stream.Write(data, 0, data.Length); - } + using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) { + using (FileStream stream = File.OpenWrite(filePath)) { + for (int i = 0; i < sizeInMb * blocksPerMb; i++) { + crypto.GetBytes(data); + stream.Write(data, 0, data.Length); } } } From f7411a4e3d07ac1b1b7cda9d13b037b0a78989c7 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Fri, 4 Aug 2023 21:29:36 +0200 Subject: [PATCH 2/3] (refactor): code cleanup and bugfixing --- src/LetsEncrypt/LetsEncrypt.csproj | 3 +- src/LetsEncrypt/Services/JwsService.cs | 3 - .../Services/LetsEncryptService.cs | 2 +- src/LetsEncryptConsole/App.cs | 25 +-- src/LetsEncryptConsole/Program.cs | 2 - src/LetsEncryptConsole/Services/KeyService.cs | 150 ------------------ src/SSHProvider/SSHProvider.csproj | 2 +- src/SSHProvider/SSHService.cs | 38 ++++- .../SSHSerivceTests/SSHProviderTests.csproj | 8 +- 9 files changed, 50 insertions(+), 183 deletions(-) delete mode 100644 src/LetsEncryptConsole/Services/KeyService.cs diff --git a/src/LetsEncrypt/LetsEncrypt.csproj b/src/LetsEncrypt/LetsEncrypt.csproj index ae5e10f..54bddde 100644 --- a/src/LetsEncrypt/LetsEncrypt.csproj +++ b/src/LetsEncrypt/LetsEncrypt.csproj @@ -8,8 +8,9 @@ + - + diff --git a/src/LetsEncrypt/Services/JwsService.cs b/src/LetsEncrypt/Services/JwsService.cs index 2258666..73720f5 100644 --- a/src/LetsEncrypt/Services/JwsService.cs +++ b/src/LetsEncrypt/Services/JwsService.cs @@ -106,7 +106,4 @@ public class JwsService : IJwsService { .Split('=').First() // Remove any trailing '='s .Replace('+', '-') // 62nd char of encoding .Replace('/', '_'); // 63rd char of encoding - - - } diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs index c409dcb..ff78184 100644 --- a/src/LetsEncrypt/Services/LetsEncryptService.cs +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -52,7 +52,7 @@ public class LetsEncryptService : ILetsEncryptService { private HttpClient _httpClient; - private IJwsService _jwsService; + private IJwsService? _jwsService; private AcmeDirectory? _directory; private RegistrationCache? _cache; diff --git a/src/LetsEncryptConsole/App.cs b/src/LetsEncryptConsole/App.cs index 0bf1031..d5866a2 100644 --- a/src/LetsEncryptConsole/App.cs +++ b/src/LetsEncryptConsole/App.cs @@ -23,20 +23,17 @@ public class App : IApp { private readonly ILogger _logger; private readonly Configuration _appSettings; private readonly ILetsEncryptService _letsEncryptService; - private readonly IKeyService _keyService; private readonly ITerminalService _terminalService; public App( ILogger logger, IOptions appSettings, ILetsEncryptService letsEncryptService, - IKeyService keyService, ITerminalService terminalService ) { _logger = logger; _appSettings = appSettings.Value; _letsEncryptService = letsEncryptService; - _keyService = keyService; _terminalService = terminalService; } @@ -102,16 +99,12 @@ public class App : IApp { // if valid check if cert and key exists otherwise recreate // else continue with new certificate request var certRes = new CachedCertificateResult(); - if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { - string cert = Path.Combine(sslPath, $"{site.Name}.crt"); - //if(!File.Exists(cert)) - File.WriteAllText(cert, certRes.Certificate); + if (registrationCache != null && registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { - string key = Path.Combine(sslPath, $"{site.Name}.key"); - //if(!File.Exists(key)) { - using (StreamWriter writer = File.CreateText(key)) - _keyService.ExportPrivateKey(certRes.PrivateKey, writer); - //} + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate); + + if (certRes.PrivateKey != null) + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem()); _logger.LogInformation("Certificate and Key exists and valid. Restored from cache."); } @@ -188,11 +181,10 @@ public class App : IApp { certRes = new CachedCertificateResult(); if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { - File.WriteAllText(Path.Combine(sslPath, site.Name + ".crt"), certRes.Certificate); + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate); - using (var writer = File.CreateText(Path.Combine(sslPath, site.Name + ".key"))) { - _keyService.ExportPrivateKey(certRes.PrivateKey, writer); - } + if(certRes.PrivateKey != null) + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem()); _logger.LogInformation("Certificate saved."); @@ -256,7 +248,6 @@ public class App : IApp { string owner, string changeMode ) { - using var sshService = new SSHService(logger, sshSettings.Host, sshSettings.Port, sshSettings.Username, sshSettings.Password); sshService.Connect(); diff --git a/src/LetsEncryptConsole/Program.cs b/src/LetsEncryptConsole/Program.cs index e9d7efe..7edba4d 100644 --- a/src/LetsEncryptConsole/Program.cs +++ b/src/LetsEncryptConsole/Program.cs @@ -42,8 +42,6 @@ class Program { #region Services services.RegisterLetsEncrypt(); - - services.AddSingleton(); services.AddSingleton(); #endregion diff --git a/src/LetsEncryptConsole/Services/KeyService.cs b/src/LetsEncryptConsole/Services/KeyService.cs deleted file mode 100644 index 5718db1..0000000 --- a/src/LetsEncryptConsole/Services/KeyService.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System.Security.Cryptography; - -namespace MaksIT.LetsEncryptConsole.Services; - -public interface IKeyService { - void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream); - void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream); -} - -public class KeyService : IKeyService { - /// - /// Export a certificate to a PEM format string - /// - /// The certificate to export - /// A PEM encoded string - //public static string ExportToPEM(X509Certificate2 cert) - //{ - // StringBuilder builder = new StringBuilder(); - - // builder.AppendLine("-----BEGIN CERTIFICATE-----"); - // builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); - // builder.AppendLine("-----END CERTIFICATE-----"); - - // return builder.ToString(); - //} - public void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream) { - var parameters = csp.ExportParameters(false); - using (var stream = new MemoryStream()) { - var writer = new BinaryWriter(stream); - writer.Write((byte)0x30); // SEQUENCE - using (var innerStream = new MemoryStream()) { - var innerWriter = new BinaryWriter(innerStream); - innerWriter.Write((byte)0x30); // SEQUENCE - EncodeLength(innerWriter, 13); - innerWriter.Write((byte)0x06); // OBJECT IDENTIFIER - var rsaEncryptionOid = new byte[] { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01 }; - EncodeLength(innerWriter, rsaEncryptionOid.Length); - innerWriter.Write(rsaEncryptionOid); - innerWriter.Write((byte)0x05); // NULL - EncodeLength(innerWriter, 0); - innerWriter.Write((byte)0x03); // BIT STRING - using (var bitStringStream = new MemoryStream()) { - var bitStringWriter = new BinaryWriter(bitStringStream); - bitStringWriter.Write((byte)0x00); // # of unused bits - bitStringWriter.Write((byte)0x30); // SEQUENCE - using (var paramsStream = new MemoryStream()) { - var paramsWriter = new BinaryWriter(paramsStream); - EncodeIntegerBigEndian(paramsWriter, parameters.Modulus); // Modulus - EncodeIntegerBigEndian(paramsWriter, parameters.Exponent); // Exponent - var paramsLength = (int)paramsStream.Length; - EncodeLength(bitStringWriter, paramsLength); - bitStringWriter.Write(paramsStream.GetBuffer(), 0, paramsLength); - } - var bitStringLength = (int)bitStringStream.Length; - EncodeLength(innerWriter, bitStringLength); - innerWriter.Write(bitStringStream.GetBuffer(), 0, bitStringLength); - } - var length = (int)innerStream.Length; - EncodeLength(writer, length); - writer.Write(innerStream.GetBuffer(), 0, length); - } - - var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); - outputStream.WriteLine("-----BEGIN PUBLIC KEY-----"); - for (var i = 0; i < base64.Length; i += 64) { - outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i)); - } - outputStream.WriteLine("-----END PUBLIC KEY-----"); - } - } - - public void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream) { - if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp"); - var parameters = csp.ExportParameters(true); - using (var stream = new MemoryStream()) { - var writer = new BinaryWriter(stream); - writer.Write((byte)0x30); // SEQUENCE - using (var innerStream = new MemoryStream()) { - var innerWriter = new BinaryWriter(innerStream); - EncodeIntegerBigEndian(innerWriter, new byte[] { 0x00 }); // Version - EncodeIntegerBigEndian(innerWriter, parameters.Modulus); - EncodeIntegerBigEndian(innerWriter, parameters.Exponent); - EncodeIntegerBigEndian(innerWriter, parameters.D); - EncodeIntegerBigEndian(innerWriter, parameters.P); - EncodeIntegerBigEndian(innerWriter, parameters.Q); - EncodeIntegerBigEndian(innerWriter, parameters.DP); - EncodeIntegerBigEndian(innerWriter, parameters.DQ); - EncodeIntegerBigEndian(innerWriter, parameters.InverseQ); - var length = (int)innerStream.Length; - EncodeLength(writer, length); - writer.Write(innerStream.GetBuffer(), 0, length); - } - - var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); - outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----"); - // Output as Base64 with lines chopped at 64 characters - for (var i = 0; i < base64.Length; i += 64) { - outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i)); - } - outputStream.WriteLine("-----END RSA PRIVATE KEY-----"); - } - } - - private void EncodeLength(BinaryWriter stream, int length) { - if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative"); - if (length < 0x80) { - // Short form - stream.Write((byte)length); - } - else { - // Long form - var temp = length; - var bytesRequired = 0; - while (temp > 0) { - temp >>= 8; - bytesRequired++; - } - stream.Write((byte)(bytesRequired | 0x80)); - for (var i = bytesRequired - 1; i >= 0; i--) { - stream.Write((byte)(length >> (8 * i) & 0xff)); - } - } - } - - private void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true) { - stream.Write((byte)0x02); // INTEGER - var prefixZeros = 0; - for (var i = 0; i < value.Length; i++) { - if (value[i] != 0) break; - prefixZeros++; - } - if (value.Length - prefixZeros == 0) { - EncodeLength(stream, 1); - stream.Write((byte)0); - } - else { - if (forceUnsigned && value[prefixZeros] > 0x7f) { - // Add a prefix zero to force unsigned if the MSB is 1 - EncodeLength(stream, value.Length - prefixZeros + 1); - stream.Write((byte)0); - } - else { - EncodeLength(stream, value.Length - prefixZeros); - } - for (var i = prefixZeros; i < value.Length; i++) { - stream.Write(value[i]); - } - } - } -} diff --git a/src/SSHProvider/SSHProvider.csproj b/src/SSHProvider/SSHProvider.csproj index aef8d01..7a8377a 100644 --- a/src/SSHProvider/SSHProvider.csproj +++ b/src/SSHProvider/SSHProvider.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/SSHProvider/SSHService.cs b/src/SSHProvider/SSHService.cs index 90ef7e2..419f4b0 100644 --- a/src/SSHProvider/SSHService.cs +++ b/src/SSHProvider/SSHService.cs @@ -1,9 +1,12 @@ -using DomainResults.Common; +using System.Text; +using System.Text.RegularExpressions; + using Microsoft.Extensions.Logging; +using DomainResults.Common; + using Renci.SshNet; using Renci.SshNet.Common; -using System.Text.RegularExpressions; namespace MaksIT.SSHProvider { @@ -22,8 +25,6 @@ namespace MaksIT.SSHProvider { public readonly SshClient _sshClient; public readonly SftpClient _sftpClient; - - public SSHService( ILogger logger, string host, @@ -31,11 +32,40 @@ namespace MaksIT.SSHProvider { string username, string password ) { + + if(string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + throw new ArgumentNullException($"{nameof(username)} or {nameof(password)} is null, empty or white space"); + _logger = logger; _sshClient = new SshClient(host, port, username, password); _sftpClient = new SftpClient(host, port, username, password); } + + public SSHService( + ILogger logger, + string host, + int port, + string username, + string [] privateKeys + ) { + + if (string.IsNullOrWhiteSpace(username) || privateKeys.Any(x => string.IsNullOrWhiteSpace(x))) + throw new ArgumentNullException($"{nameof(username)} or {nameof(privateKeys)} contains key which is null, empty or white space"); + + _logger = logger; + + var privateKeyFiles = new List(); + foreach (var privateKey in privateKeys) { + using (var ms = new MemoryStream(Encoding.ASCII.GetBytes(privateKey))) { + privateKeyFiles.Add(new PrivateKeyFile(ms)); + } + } + + _sshClient = new SshClient(host, port, username, privateKeyFiles.ToArray()); + _sftpClient = new SftpClient(host, port, username, privateKeyFiles.ToArray()); + } + public IDomainResult Connect() { try { _sshClient.Connect(); diff --git a/src/Tests/SSHSerivceTests/SSHProviderTests.csproj b/src/Tests/SSHSerivceTests/SSHProviderTests.csproj index 49415f7..cb4a20a 100644 --- a/src/Tests/SSHSerivceTests/SSHProviderTests.csproj +++ b/src/Tests/SSHSerivceTests/SSHProviderTests.csproj @@ -10,13 +10,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 327f3be2c76d16b1c6b2f87a051485ea96543e3d Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Sun, 6 Aug 2023 22:35:13 +0200 Subject: [PATCH 3/3] (refactor): domain results --- .../Entities/LetsEncrypt/SendResult.cs | 10 + .../Exceptions/LetsEncrytException.cs | 30 +- src/LetsEncrypt/Models/Responses/Problem.cs | 15 + .../Services/LetsEncryptService.cs | 704 +++++++++++------- src/LetsEncryptConsole/App.cs | 304 ++++---- 5 files changed, 619 insertions(+), 444 deletions(-) create mode 100644 src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs create mode 100644 src/LetsEncrypt/Models/Responses/Problem.cs diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs b/src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs new file mode 100644 index 0000000..dd55fd1 --- /dev/null +++ b/src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs @@ -0,0 +1,10 @@ +namespace MaksIT.LetsEncrypt.Entities { + public class SendResult { + + public TResult? Result { get; set; } + + public string? ResponseText { get; set; } + + + } +} diff --git a/src/LetsEncrypt/Exceptions/LetsEncrytException.cs b/src/LetsEncrypt/Exceptions/LetsEncrytException.cs index 78198d7..184dbb9 100644 --- a/src/LetsEncrypt/Exceptions/LetsEncrytException.cs +++ b/src/LetsEncrypt/Exceptions/LetsEncrytException.cs @@ -1,23 +1,19 @@ - +using MaksIT.Core.Extensions; +using MaksIT.LetsEncrypt.Models.Responses; + namespace MaksIT.LetsEncrypt.Exceptions; public class LetsEncrytException : Exception { - public LetsEncrytException(Problem problem, HttpResponseMessage response) - : base($"{problem.Type}: {problem.Detail}") { + + public Problem? Problem { get; } + + public HttpResponseMessage Response { get; } + + public LetsEncrytException( + Problem? problem, + HttpResponseMessage response + ) : base(problem != null ? $"{problem.Type}: {problem.Detail}" : "") { + Problem = problem; Response = response; } - - public Problem Problem { get; } - - public HttpResponseMessage Response { get; } } - - -public class Problem { - public string Type { get; set; } - - public string Detail { get; set; } - - public string RawJson { get; set; } -} - diff --git a/src/LetsEncrypt/Models/Responses/Problem.cs b/src/LetsEncrypt/Models/Responses/Problem.cs new file mode 100644 index 0000000..fa7811b --- /dev/null +++ b/src/LetsEncrypt/Models/Responses/Problem.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MaksIT.LetsEncrypt.Models.Responses { + public class Problem { + public string Type { get; set; } + + public string Detail { get; set; } + + public string RawJson { get; set; } + } +} diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs index ff78184..ac27dc2 100644 --- a/src/LetsEncrypt/Services/LetsEncryptService.cs +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -18,24 +18,25 @@ using MaksIT.LetsEncrypt.Models.Responses; using MaksIT.LetsEncrypt.Models.Interfaces; using MaksIT.LetsEncrypt.Models.Requests; using MaksIT.LetsEncrypt.Entities.Jws; +using DomainResults.Common; namespace MaksIT.LetsEncrypt.Services; public interface ILetsEncryptService { - Task ConfigureClient(string url); + Task ConfigureClient(string url); - Task Init(string[] contacts, RegistrationCache? registrationCache); - - string GetTermsOfServiceUri(); - - - Task> NewOrder(string[] hostnames, string challengeType); - Task CompleteChallenges(); - Task GetOrder(string[] hostnames); - Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject); + Task Init(string[] contacts, RegistrationCache? registrationCache); RegistrationCache? GetRegistrationCache(); + + (string?, IDomainResult) GetTermsOfServiceUri(); + + + Task<(Dictionary?, IDomainResult)> NewOrder(string[] hostnames, string challengeType); + Task CompleteChallenges(); + Task GetOrder(string[] hostnames); + Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject); } @@ -76,11 +77,22 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public async Task ConfigureClient(string url) { + public async Task ConfigureClient(string url) { + try { + _httpClient.BaseAddress ??= new Uri(url); - _httpClient.BaseAddress ??= new Uri(url); + var (directory, getAcmeDirectoryResult) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null); + if (!getAcmeDirectoryResult.IsSuccess) + return getAcmeDirectoryResult; - (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null); + _directory = directory.Result; + + return IDomainResult.Success(); + } + catch (Exception ex) { + _logger.LogError(ex, "Let's Encrypt client unhandled exception"); + return IDomainResult.CriticalDependencyError(); + } } /// @@ -89,42 +101,57 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public async Task Init(string? [] contacts, RegistrationCache? cache) { + public async Task Init(string? [] contacts, RegistrationCache? cache) { - if (contacts == null || contacts.Length == 0) - throw new ArgumentNullException(); + try { - if (_directory == null) - throw new ArgumentNullException(); + _logger.LogInformation($"Executing {nameof(Init)}..."); - var accountKey = new RSACryptoServiceProvider(4096); + if (contacts == null || contacts.Length == 0) + return IDomainResult.Failed(); - if (cache != null && cache.AccountKey != null) { - _cache = cache; - accountKey.ImportCspBlob(cache.AccountKey); + if (_directory == null) + return IDomainResult.Failed(); + + var accountKey = new RSACryptoServiceProvider(4096); + + if (cache != null && cache.AccountKey != null) { + _cache = cache; + accountKey.ImportCspBlob(cache.AccountKey); + } + + // New Account request + _jwsService = new JwsService(accountKey); + + + var letsEncryptOrder = new Account { + TermsOfServiceAgreed = true, + Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray() + }; + + var (account, postAccuntResult) = await SendAsync(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder); + _jwsService.SetKeyId(account.Result.Location.ToString()); + + if (account.Result.Status != "valid") { + _logger.LogError($"Account status is not valid, was: {account.Result.Status} \r\n {account.ResponseText}"); + return IDomainResult.Failed(); + } + + _cache = new RegistrationCache { + Location = account.Result.Location, + AccountKey = accountKey.ExportCspBlob(true), + Id = account.Result.Id, + Key = account.Result.Key + }; + + return IDomainResult.Success(); } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; - // New Account request - _jwsService = new JwsService(accountKey); - - - var letsEncryptOrder = new Account { - TermsOfServiceAgreed = true, - Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray() - }; - - var (account, response) = await SendAsync(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder); - _jwsService.SetKeyId(account.Location.ToString()); - - if (account.Status != "valid") - throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}"); - - _cache = new RegistrationCache { - Location = account.Location, - AccountKey = accountKey.ExportCspBlob(true), - Id = account.Id, - Key = account.Key - }; + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError(message); + } } /// @@ -139,16 +166,25 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public string GetTermsOfServiceUri() { + public (string?, IDomainResult) GetTermsOfServiceUri() { + try { - if (_directory == null) - throw new NullReferenceException(); + _logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}..."); - return _directory.Meta.TermsOfService; + if (_directory == null) { + return IDomainResult.Failed(); + } + + return IDomainResult.Success(_directory.Meta.TermsOfService); + } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; + + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError(message); + } } - - /// /// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange. /// @@ -164,82 +200,104 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public async Task> NewOrder(string[] hostnames, string challengeType) { - _challenges.Clear(); + public async Task<(Dictionary?, IDomainResult)> NewOrder(string[] hostnames, string challengeType) { + try { - var letsEncryptOrder = new Order { - Expires = DateTime.UtcNow.AddDays(2), - Identifiers = hostnames.Select(hostname => new OrderIdentifier { - Type = "dns", - Value = hostname - }).ToArray() - }; + _logger.LogInformation($"Executing {nameof(NewOrder)}..."); - var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + _challenges.Clear(); - if (order.Status == "ready") - return new Dictionary(); + var letsEncryptOrder = new Order { + Expires = DateTime.UtcNow.AddDays(2), + Identifiers = hostnames.Select(hostname => new OrderIdentifier { + Type = "dns", + Value = hostname + }).ToArray() + }; - if (order.Status != "pending") - throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}"); - - _currentOrder = order; - - var results = new Dictionary(); - foreach (var item in order.Authorizations) { - var (challengeResponse, responseText) = await SendAsync(HttpMethod.Post, item, true, null); - - if (challengeResponse.Status == "valid") - continue; - - if (challengeResponse.Status != "pending") - throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {responseText}"); - - var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType); - _challenges.Add(challenge); - - var keyToken = _jwsService.GetKeyAuthorization(challenge.Token); - - switch (challengeType) { - - // A client fulfills this challenge by constructing a key authorization - // from the "token" value provided in the challenge and the client's - // account key. The client then computes the SHA-256 digest [FIPS180-4] - // of the key authorization. - // - // The record provisioned to the DNS contains the base64url encoding of - // this digest. - - case "dns-01": { - using (var sha256 = SHA256.Create()) { - var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken))); - results[challengeResponse.Identifier.Value] = dnsToken; - } - break; - } - - - // A client fulfills this challenge by constructing a key authorization - // from the "token" value provided in the challenge and the client's - // account key. The client then provisions the key authorization as a - // resource on the HTTP server for the domain in question. - // - // The path at which the resource is provisioned is comprised of the - // fixed prefix "/.well-known/acme-challenge/", followed by the "token" - // value in the challenge. The value of the resource MUST be the ASCII - // representation of the key authorization. - - case "http-01": { - results[challengeResponse.Identifier.Value] = keyToken; - break; - } - - default: - throw new NotImplementedException(); + var (order, postNewOrderResult) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + if (!postNewOrderResult.IsSuccess) { + return (null, postNewOrderResult); } - } - return results; + if (order.Result.Status == "ready") + return IDomainResult.Success(new Dictionary()); + + if (order.Result.Status != "pending") { + _logger.LogError($"Created new order and expected status 'pending', but got: {order.Result.Status} \r\n {order.Result}"); + return IDomainResult.Failed?>(); + } + + _currentOrder = order.Result; + + var results = new Dictionary(); + foreach (var item in order.Result.Authorizations) { + + var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync(HttpMethod.Post, item, true, null); + if (!postAuthorisationChallengeResult.IsSuccess) { + return (null, postAuthorisationChallengeResult); + } + + if (challengeResponse.Result.Status == "valid") + continue; + + if (challengeResponse.Result.Status != "pending") { + _logger.LogError($"Expected autorization status 'pending', but got: {order.Result.Status} \r\n {challengeResponse.ResponseText}"); + return IDomainResult.Failed?>(); + } + + var challenge = challengeResponse.Result.Challenges.First(x => x.Type == challengeType); + _challenges.Add(challenge); + + var keyToken = _jwsService.GetKeyAuthorization(challenge.Token); + + switch (challengeType) { + + // A client fulfills this challenge by constructing a key authorization + // from the "token" value provided in the challenge and the client's + // account key. The client then computes the SHA-256 digest [FIPS180-4] + // of the key authorization. + // + // The record provisioned to the DNS contains the base64url encoding of + // this digest. + + case "dns-01": { + using (var sha256 = SHA256.Create()) { + var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken))); + results[challengeResponse.Result.Identifier.Value] = dnsToken; + } + break; + } + + + // A client fulfills this challenge by constructing a key authorization + // from the "token" value provided in the challenge and the client's + // account key. The client then provisions the key authorization as a + // resource on the HTTP server for the domain in question. + // + // The path at which the resource is provisioned is comprised of the + // fixed prefix "/.well-known/acme-challenge/", followed by the "token" + // value in the challenge. The value of the resource MUST be the ASCII + // representation of the key authorization. + + case "http-01": { + results[challengeResponse.Result.Identifier.Value] = keyToken; + break; + } + + default: + throw new NotImplementedException(); + } + } + + return IDomainResult.Success(results); + } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; + + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError?>(message); + } } /// @@ -247,36 +305,63 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public async Task CompleteChallenges() { + public async Task CompleteChallenges() { + try { - for (var index = 0; index < _challenges.Count; index++) { + _logger.LogInformation($"Executing {nameof(CompleteChallenges)}..."); - var challenge = _challenges[index]; - - while (true) { - AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge(); - - switch (challenge.Type) { - case "dns-01": { - authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token); - //var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, authorizeChallenge, token); - break; - } - - case "http-01": { - break; - } - } - - var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, false, "{}"); - - if (result.Status == "valid") - break; - if (result.Status != "pending") - throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}"); - - await Task.Delay(1000); + if (_currentOrder?.Identifiers == null) { + return IDomainResult.Failed(); } + + for (var index = 0; index < _challenges.Count; index++) { + + var challenge = _challenges[index]; + + var start = DateTime.UtcNow; + + while (true) { + var authorizeChallenge = new AuthorizeChallenge(); + + switch (challenge.Type) { + case "dns-01": { + authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token); + //var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, authorizeChallenge, token); + break; + } + + case "http-01": { + break; + } + } + + var (authChallenge, postAuthChallengeResult) = await SendAsync(HttpMethod.Post, challenge.Url, false, "{}"); + if (!postAuthChallengeResult.IsSuccess) { + return postAuthChallengeResult; + } + + if (authChallenge.Result.Status == "valid") + break; + + if (authChallenge.Result.Status != "pending") { + _logger.LogError($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {authChallenge.ResponseText}"); + return IDomainResult.Failed(); + } + + await Task.Delay(1000); + + if ((DateTime.UtcNow - start).Seconds > 120) + throw new TimeoutException(); + } + } + + return IDomainResult.Success(); + } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; + + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError(message); } } @@ -285,19 +370,34 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public async Task GetOrder(string[] hostnames) { + public async Task GetOrder(string[] hostnames) { - var letsEncryptOrder = new Order { - Expires = DateTime.UtcNow.AddDays(2), - Identifiers = hostnames.Select(hostname => new OrderIdentifier { - Type = "dns", - Value = hostname - }).ToArray() - }; + try { - var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + _logger.LogInformation($"Executing {nameof(GetOrder)}"); - _currentOrder = order; + var letsEncryptOrder = new Order { + Expires = DateTime.UtcNow.AddDays(2), + Identifiers = hostnames.Select(hostname => new OrderIdentifier { + Type = "dns", + Value = hostname + }).ToArray() + }; + + var (order, postOrderResult) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + if (!postOrderResult.IsSuccess) + return postOrderResult; + + _currentOrder = order.Result; + + return IDomainResult.Success(); + } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; + + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError(message); + } } /// @@ -306,71 +406,87 @@ public class LetsEncryptService : ILetsEncryptService { /// /// Cert and Private key /// - public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) { + public async Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject) { - _logger.LogInformation($"Invoked: {nameof(GetCertificate)}"); + try { + _logger.LogInformation($"Executing {nameof(GetCertificate)}..."); - - if (_currentOrder == null) - throw new ArgumentNullException(); - - var key = new RSACryptoServiceProvider(4096); - var csr = new CertificateRequest("CN=" + subject, - key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - var san = new SubjectAlternativeNameBuilder(); - foreach (var host in _currentOrder.Identifiers) - san.AddDnsName(host.Value); - - csr.CertificateExtensions.Add(san.Build()); - - var letsEncryptOrder = new FinalizeRequest { - Csr = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest()) - }; - - Uri? certificateUrl = default; - - - var start = DateTime.UtcNow; - - while (certificateUrl == null) { - // https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882 - await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray()); - - if (_currentOrder.Status == "ready") { - var (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder); - - if (response.Status == "processing") - (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Location, true, null); - - if (response.Status == "valid") { - certificateUrl = response.Certificate; - } + if (_currentOrder == null) { + return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>(); } - if ((start - DateTime.UtcNow).Seconds > 120) - throw new TimeoutException(); + var key = new RSACryptoServiceProvider(4096); + var csr = new CertificateRequest("CN=" + subject, + key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - await Task.Delay(1000); - continue; + var san = new SubjectAlternativeNameBuilder(); + foreach (var host in _currentOrder.Identifiers) + san.AddDnsName(host.Value); - // throw new InvalidOperationException(/*$"Invalid order status: "*/); + csr.CertificateExtensions.Add(san.Build()); + + var letsEncryptOrder = new FinalizeRequest { + Csr = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest()) + }; + + Uri? certificateUrl = default; + + + var start = DateTime.UtcNow; + + while (certificateUrl == null) { + // https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882 + await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray()); + + if (_currentOrder.Status == "ready") { + var (order, postOrderResult) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder); + if (!postOrderResult.IsSuccess || order?.Result == null) + return (null, postOrderResult); + + + if (order.Result.Status == "processing") { + (order, postOrderResult) = await SendAsync(HttpMethod.Post, _currentOrder.Location, true, null); + if (!postOrderResult.IsSuccess || order?.Result == null) + return (null, postOrderResult); + } + + if (order.Result.Status == "valid") { + certificateUrl = order.Result.Certificate; + } + } + + if ((DateTime.UtcNow - start).Seconds > 120) + throw new TimeoutException(); + + await Task.Delay(1000); + } + + var (pem, postPemResult) = await SendAsync(HttpMethod.Post, certificateUrl, true, null); + if (!postPemResult.IsSuccess || pem?.Result == null) + return (null, postPemResult); + + + if (_cache == null) { + _logger.LogError($"{nameof(_cache)} is null"); + return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>(); + } + + _cache.CachedCerts ??= new Dictionary(); + _cache.CachedCerts[subject] = new CertificateCache { + Cert = pem.Result, + Private = key.ExportCspBlob(true) + }; + + var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result)); + + return IDomainResult.Success((cert, key)); } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; - var (pem, _) = await SendAsync(HttpMethod.Post, certificateUrl, true, null); - - if (_cache == null) - throw new NullReferenceException(); - - _cache.CachedCerts ??= new Dictionary(); - _cache.CachedCerts[subject] = new CertificateCache { - Cert = pem, - Private = key.ExportCspBlob(true) - }; - - var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem)); - - return (cert, key); + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError< (X509Certificate2 Cert, RSA PrivateKey)?>(message); + } } /// @@ -378,7 +494,7 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public Task KeyChange() { + public Task KeyChange() { throw new NotImplementedException(); } @@ -387,88 +503,128 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public Task RevokeCertificate() { + public Task RevokeCertificate() { throw new NotImplementedException(); } + + /// + /// Request New Nonce to be able to start POST requests + /// + /// + /// + private async Task<(string?, IDomainResult)> NewNonce() { + + try { + + _logger.LogInformation($"Executing {nameof(NewNonce)}..."); + + if (_directory == null) + IDomainResult.Failed(); + + var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce)); + return IDomainResult.Success(result.Headers.GetValues("Replay-Nonce").First()); + + } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; + + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError(message); + } + } + /// /// Main method used to send data to LetsEncrypt /// /// /// /// - /// + /// /// /// - private async Task<(TResult, string)> SendAsync(HttpMethod method, Uri uri, bool isPostAsGet, object? message) where TResult : class { - var request = new HttpRequestMessage(method, uri); + private async Task<(SendResult?, IDomainResult)> SendAsync(HttpMethod method, Uri uri, bool isPostAsGet, object? requestModel) { + try { - _nonce = uri.OriginalString != "directory" - ? await NewNonce() - : default; + _logger.LogInformation($"Executing {nameof(SendAsync)}..."); - if (message != null || isPostAsGet) { - var jwsHeader = new JwsHeader { - Url = uri, - }; + //if (_jwsService == null) { + // _logger.LogError($"{nameof(_jwsService)} is null"); + // return IDomainResult.Failed?>(); + //} - if (_nonce != null) - jwsHeader.Nonce = _nonce; + var request = new HttpRequestMessage(method, uri); - var encodedMessage = isPostAsGet - ? _jwsService.Encode(jwsHeader) - : _jwsService.Encode(message, jwsHeader); + if (uri.OriginalString != "directory") { + var (nonce, newNonceResult) = await NewNonce(); + if (!newNonceResult.IsSuccess || nonce == null) { + return (null, newNonceResult); + } - var json = encodedMessage.ToJson(); + _nonce = nonce; + } + else { + _nonce = default; + } - request.Content = new StringContent(json); + if (requestModel != null || isPostAsGet) { + var jwsHeader = new JwsHeader { + Url = uri, + }; + + if (_nonce != null) + jwsHeader.Nonce = _nonce; + + var encodedMessage = isPostAsGet + ? _jwsService.Encode(jwsHeader) + : _jwsService.Encode(requestModel, jwsHeader); + + var json = encodedMessage.ToJson(); + + request.Content = new StringContent(json); + + var requestType = "application/json"; + if (method == HttpMethod.Post) + requestType = "application/jose+json"; + + request.Content.Headers.Remove("Content-Type"); + request.Content.Headers.Add("Content-Type", requestType); + } + + var response = await _httpClient.SendAsync(request); - var requestType = "application/json"; if (method == HttpMethod.Post) - requestType = "application/jose+json"; + _nonce = response.Headers.GetValues("Replay-Nonce").First(); + + var responseText = await response.Content.ReadAsStringAsync(); + + if (response.Content.Headers.ContentType?.MediaType == "application/problem+json") + throw new LetsEncrytException(responseText.ToObject(), response); + + if (response.Content.Headers.ContentType?.MediaType == "application/pem-certificate-chain" && typeof(TResult) == typeof(string)) { + return IDomainResult.Success(new SendResult { + Result = (TResult)(object)responseText + }); + } + + var responseContent = responseText.ToObject(); + + if (responseContent is IHasLocation ihl) { + if (response.Headers.Location != null) + ihl.Location = response.Headers.Location; + } + + return IDomainResult.Success(new SendResult { + Result = responseContent, + ResponseText = responseText + }); - request.Content.Headers.Remove("Content-Type"); - request.Content.Headers.Add("Content-Type", requestType); } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; - var response = await _httpClient.SendAsync(request); - - if (method == HttpMethod.Post) - _nonce = response.Headers.GetValues("Replay-Nonce").First(); - - if (response.Content.Headers.ContentType.MediaType == "application/problem+json") { - var problemJson = await response.Content.ReadAsStringAsync(); - var problem = problemJson.ToObject(); - problem.RawJson = problemJson; - throw new LetsEncrytException(problem, response); + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError?>(message); } - - var responseText = await response.Content.ReadAsStringAsync(); - - if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") { - return ((TResult)(object)responseText, null); - } - - var responseContent = responseText.ToObject(); - - if (responseContent is IHasLocation ihl) { - if (response.Headers.Location != null) - ihl.Location = response.Headers.Location; - } - - return (responseContent, responseText); - } - - /// - /// Request New Nonce to be able to start POST requests - /// - /// - /// - private async Task NewNonce() { - if (_directory == null) - throw new NotImplementedException(); - - var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce)); - return result.Headers.GetValues("Replay-Nonce").First(); } } diff --git a/src/LetsEncryptConsole/App.cs b/src/LetsEncryptConsole/App.cs index d5866a2..9d12d13 100644 --- a/src/LetsEncryptConsole/App.cs +++ b/src/LetsEncryptConsole/App.cs @@ -39,204 +39,200 @@ public class App : IApp { public async Task Run(string[] args) { - _logger.LogInformation("Letsencrypt client estarted..."); + try { + _logger.LogInformation("Let's Encrypt client. Started..."); + + foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List()) { - foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List()) { - try { _logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}"); //loop all customers foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List()) { - try { - _logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}"); - //define cache folder - string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache"); - if (!Directory.Exists(cachePath)) { - Directory.CreateDirectory(cachePath); + _logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}"); + + //define cache folder + string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache"); + if (!Directory.Exists(cachePath)) { + Directory.CreateDirectory(cachePath); + } + + //check acme directory + var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme"); + if (!Directory.Exists(acmePath)) { + Directory.CreateDirectory(acmePath); + } + + //loop each customer website + foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List()) { + _logger.LogInformation($"Managing site: {site.Name}"); + + + //create folder for ssl + string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name); + if (!Directory.Exists(sslPath)) { + Directory.CreateDirectory(sslPath); } - //check acme directory - var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme"); - if (!Directory.Exists(acmePath)) { - Directory.CreateDirectory(acmePath); + var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json"); + + + + #region LetsEncrypt client configuration and local registration cache initialization + _logger.LogInformation("1. Client Initialization..."); + + await _letsEncryptService.ConfigureClient(env.Url); + + var registrationCache = (File.Exists(cacheFile) + ? File.ReadAllText(cacheFile) + : null) + .ToObject(); + + var initResult = await _letsEncryptService.Init(customer.Contacts, registrationCache); + if (!initResult.IsSuccess) { + continue; } + #endregion - //loop each customer website - foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List()) { - _logger.LogInformation($"Managing site: {site.Name}"); + #region LetsEncrypt terms of service + _logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}"); + #endregion - try { - //create folder for ssl - string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name); - if (!Directory.Exists(sslPath)) { - Directory.CreateDirectory(sslPath); - } + // get cached certificate and check if it's valid + // if valid check if cert and key exists otherwise recreate + // else continue with new certificate request + var certRes = new CachedCertificateResult(); + if (registrationCache != null && registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { - var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json"); + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate); - //1. Client initialization - _logger.LogInformation("1. Client Initialization..."); + if (certRes.PrivateKey != null) + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem()); - #region LetsEncrypt client configuration - await _letsEncryptService.ConfigureClient(env.Url); - #endregion + _logger.LogInformation("Certificate and Key exists and valid. Restored from cache."); + } + else { - #region LetsEncrypt local registration cache initialization - var registrationCache = (File.Exists(cacheFile) - ? File.ReadAllText(cacheFile) - : null) - .ToObject(); - await _letsEncryptService.Init(customer.Contacts, registrationCache); - #endregion + //create new orders + #region LetsEncrypt new order + _logger.LogInformation("2. Client New Order..."); - #region LetsEncrypt terms of service - _logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}"); - #endregion + var (orders, newOrderResult) = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge); + if (!newOrderResult.IsSuccess || orders == null) { + continue; + } + #endregion - // get cached certificate and check if it's valid - // if valid check if cert and key exists otherwise recreate - // else continue with new certificate request - var certRes = new CachedCertificateResult(); - if (registrationCache != null && registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { + if (orders.Count > 0) { + switch (site.Challenge) { + case "http-01": { + //ensure to enable static file discovery on server in .well-known/acme-challenge + //and listen on 80 port - File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate); - - if (certRes.PrivateKey != null) - File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem()); + foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) + file.Delete(); - _logger.LogInformation("Certificate and Key exists and valid. Restored from cache."); - } - else { + foreach (var result in orders) { + Console.WriteLine($"Key: {result.Key}, Value: {result.Value}"); + string[] splitToken = result.Value.Split('.'); - //try to make new order - try { - //create new orders - Console.WriteLine("2. Client New Order..."); - - #region LetsEncrypt new order - var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge); - #endregion - - if (orders.Count > 0) { - switch (site.Challenge) { - case "http-01": { - //ensure to enable static file discovery on server in .well-known/acme-challenge - //and listen on 80 port - - foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) - file.Delete(); - - foreach (var result in orders) { - Console.WriteLine($"Key: {result.Key}, Value: {result.Value}"); - string[] splitToken = result.Value.Split('.'); - - File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value); - } - - foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) { - if (env?.SSH?.Active ?? false) { - UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode); - } - else { - throw new NotImplementedException(); - } - } - - break; - } - - case "dns-01": { - //Manage DNS server MX record, depends from provider - throw new NotImplementedException(); - } - - default: { - throw new NotImplementedException(); - } + File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value); } - - #region LetsEncrypt complete challenges - _logger.LogInformation("3. Client Complete Challange..."); - await _letsEncryptService.CompleteChallenges(); - _logger.LogInformation("Challanges comleted."); - #endregion - - await Task.Delay(1000); - - #region Download new certificate - _logger.LogInformation("4. Download certificate..."); - var (cert, key) = await _letsEncryptService.GetCertificate(site.Name); - #endregion - - #region Persist cache - registrationCache = _letsEncryptService.GetRegistrationCache(); - File.WriteAllText(cacheFile, registrationCache.ToJson()); - #endregion - } - - #region Save cert and key to filesystem - certRes = new CachedCertificateResult(); - if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { - - File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate); - - if(certRes.PrivateKey != null) - File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem()); - - _logger.LogInformation("Certificate saved."); - - foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) { - + foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) { if (env?.SSH?.Active ?? false) { - UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode); + UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode); } else { throw new NotImplementedException(); } } + break; } - else { - _logger.LogError("Unable to get new cached certificate."); - } - #endregion - } - catch (Exception ex) { - _logger.LogError(ex, ""); - await _letsEncryptService.GetOrder(site.Hosts); - } + case "dns-01": { + //Manage DNS server MX record, depends from provider + throw new NotImplementedException(); + } + + default: { + throw new NotImplementedException(); + } } + #region LetsEncrypt complete challenges + _logger.LogInformation("3. Client Complete Challange..."); + var completeChallengesResult = await _letsEncryptService.CompleteChallenges(); + if (!completeChallengesResult.IsSuccess) { + continue; + } + _logger.LogInformation("Challanges comleted."); + #endregion + await Task.Delay(1000); + + #region Download new certificate + _logger.LogInformation("4. Download certificate..."); + var (certData, getCertResult) = await _letsEncryptService.GetCertificate(site.Name); + if (!getCertResult.IsSuccess || certData == null) { + continue; + } + + // not used in this scenario + // var (cert, key) = certData.Value; + #endregion + + #region Persist cache + registrationCache = _letsEncryptService.GetRegistrationCache(); + File.WriteAllText(cacheFile, registrationCache.ToJson()); + #endregion + } + + #region Save cert and key to filesystem + certRes = new CachedCertificateResult(); + if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { + + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate); + + if (certRes.PrivateKey != null) + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem()); + + _logger.LogInformation("Certificate saved."); + + foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) { + + if (env?.SSH?.Active ?? false) { + UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode); + } + else { + throw new NotImplementedException(); + } + } } - catch (Exception ex) { - _logger.LogError(ex, "Customer unhandled error"); + else { + _logger.LogError("Unable to get new cached certificate."); } + #endregion + } } - catch (Exception ex) { - _logger.LogError(ex, "Environment unhandled error"); - } } + } - if (env.Name == "ProductionV2") { - _terminalService.Exec("systemctl restart nginx"); - } - } - catch (Exception ex) { - _logger.LogError(ex.Message.ToString()); - break; - } + _logger.LogInformation($"Let's Encrypt client. Execution complete."); } + catch (Exception ex) { + _logger.LogError(ex, $"Let's Encrypt client. Unhandled exception."); + } + + } - + private void UploadFiles( @@ -244,7 +240,7 @@ public class App : IApp { SSHClientSettings sshSettings, string workDir, string fileName, - byte [] bytes, + byte[] bytes, string owner, string changeMode ) { @@ -260,5 +256,7 @@ public class App : IApp { sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R"); sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R"); + + //sshService.RunSudoCommand(sshSettings.Password, $"systemctl restart nginx"); } }