diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj new file mode 100644 index 0000000..5e15896 --- /dev/null +++ b/src/Core/Core.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + MaksIT.$(MSBuildProjectName.Replace(" ", "_")) + + + + + + + diff --git a/src/Core/Extensions/ObjectExtensions.cs b/src/Core/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..d604210 --- /dev/null +++ b/src/Core/Extensions/ObjectExtensions.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; +using System.Text.Json; + +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, List? converters) { + if (obj == null) + return "{}"; + + var options = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true + }; + + converters?.ForEach(x => options.Converters.Add(x)); + + return JsonSerializer.Serialize(obj, options); + } + } +} diff --git a/src/Core/Extensions/StringExtensions.cs b/src/Core/Extensions/StringExtensions.cs new file mode 100644 index 0000000..a8d5126 --- /dev/null +++ b/src/Core/Extensions/StringExtensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +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); + + /// + /// + /// + /// + /// + /// + /// + 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 + }; + + converters?.ForEach(x => options.Converters.Add(x)); + + return s != null + ? JsonSerializer.Deserialize(s, options) + : default; + } + } +} diff --git a/src/Core/OperatingSystem.cs b/src/Core/OperatingSystem.cs new file mode 100644 index 0000000..162e2c1 --- /dev/null +++ b/src/Core/OperatingSystem.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +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 IsLinux() => + RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + } +} diff --git a/src/LetsEncrypt.sln b/src/LetsEncrypt.sln new file mode 100644 index 0000000..9f1c543 --- /dev/null +++ b/src/LetsEncrypt.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt\LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LetsEncryptConsole", "LetsEncryptConsole\LetsEncryptConsole.csproj", "{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{27A58A5F-B52A-44F2-9639-84C6F02EA75D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.Build.0 = Release|Any CPU + {2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Release|Any CPU.Build.0 = Release|Any CPU + {27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351} + EndGlobalSection +EndGlobal diff --git a/v2.0/LetsEncrypt/.vscode/launch.json b/src/LetsEncrypt/.vscode/launch.json similarity index 100% rename from v2.0/LetsEncrypt/.vscode/launch.json rename to src/LetsEncrypt/.vscode/launch.json diff --git a/v1.0/LetsEncrypt/.vscode/tasks.json b/src/LetsEncrypt/.vscode/tasks.json similarity index 100% rename from v1.0/LetsEncrypt/.vscode/tasks.json rename to src/LetsEncrypt/.vscode/tasks.json diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/Jwk.cs b/src/LetsEncrypt/Entities/Jws/Jwk.cs similarity index 73% rename from v2.0/LetsEncrypt/Entities/LetsEncrypt/Jwk.cs rename to src/LetsEncrypt/Entities/Jws/Jwk.cs index c941659..8e3ce9f 100644 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/Jwk.cs +++ b/src/LetsEncrypt/Entities/Jws/Jwk.cs @@ -1,10 +1,8 @@ // https://tools.ietf.org/html/rfc7517 -using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; - -namespace LetsEncrypt.Entities +namespace MaksIT.LetsEncrypt.Entities.Jws { public class Jwk { @@ -15,8 +13,8 @@ namespace LetsEncrypt.Entities /// family used with the key, such as "RSA" or "EC". /// /// - [JsonProperty("kty")] - public string KeyType { get; set; } + [JsonPropertyName("kty")] + public string? KeyType { get; set; } /// /// "kid" (Key ID) Parameter @@ -27,8 +25,8 @@ namespace LetsEncrypt.Entities /// unspecified. /// /// - [JsonProperty("kid")] - public string KeyId { get; set; } + [JsonPropertyName("kid")] + public string? KeyId { get; set; } /// /// "use" (Public Key Use) Parameter @@ -39,62 +37,62 @@ namespace LetsEncrypt.Entities /// on data. /// /// - [JsonProperty("use")] - public string Use { get; set; } + [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. /// - [JsonProperty("n")] - public string Modulus { get; set; } + [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. /// - [JsonProperty("e")] - public string Exponent { get; set; } + [JsonPropertyName("e")] + public string? Exponent { get; set; } /// /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation. /// - [JsonProperty("d")] - public string D { get; set; } + [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. /// - [JsonProperty("p")] - public string P { get; set; } + [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. /// - [JsonProperty("q")] - public string Q { get; set; } + [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. /// - [JsonProperty("dp")] - public string DP { get; set; } + [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. /// - [JsonProperty("dq")] - public string DQ { get; set; } + [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. /// - [JsonProperty("qi")] - public string InverseQ { get; set; } + [JsonPropertyName("qi")] + public string? InverseQ { get; set; } /// /// The other primes information, should they exist, null or an empty list if not specified. /// - [JsonProperty("oth")] - public string OthInf { get; set; } + [JsonPropertyName("oth")] + public string? OthInf { get; set; } /// /// "alg" (Algorithm) Parameter @@ -103,7 +101,7 @@ namespace LetsEncrypt.Entities /// use with the key. /// /// - [JsonProperty("alg")] - public string Algorithm { get; set; } + [JsonPropertyName("alg")] + public string? Algorithm { get; set; } } } \ No newline at end of file diff --git a/src/LetsEncrypt/Entities/Jws/JwsMessage.cs b/src/LetsEncrypt/Entities/Jws/JwsMessage.cs new file mode 100644 index 0000000..afd3d25 --- /dev/null +++ b/src/LetsEncrypt/Entities/Jws/JwsMessage.cs @@ -0,0 +1,40 @@ +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; } + } + + + 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 new file mode 100644 index 0000000..decc2ed --- /dev/null +++ b/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs @@ -0,0 +1,13 @@ +using System; + +namespace MaksIT.LetsEncrypt.Entities { + public class AuthorizationChallenge { + public Uri? Url { get; set; } + + public string? Type { get; set; } + + public string? Status { get; set; } + + public string? Token { get; set; } + } +} diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs b/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs new file mode 100644 index 0000000..abb7436 --- /dev/null +++ b/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs @@ -0,0 +1,11 @@ +using System.Security.Cryptography; + +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 new file mode 100644 index 0000000..62b348a --- /dev/null +++ b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using MaksIT.LetsEncrypt.Entities.Jws; + +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; } + } +} diff --git a/src/LetsEncrypt/Exceptions/LetsEncrytException.cs b/src/LetsEncrypt/Exceptions/LetsEncrytException.cs new file mode 100644 index 0000000..615f312 --- /dev/null +++ b/src/LetsEncrypt/Exceptions/LetsEncrytException.cs @@ -0,0 +1,25 @@ +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; } + } + + + 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 new file mode 100644 index 0000000..efa02f9 --- /dev/null +++ b/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +using MaksIT.LetsEncrypt.Services; + +namespace MaksIT.LetsEncrypt.Extensions { + public static class ServiceCollectionExtensions { + public static void RegisterLetsEncrypt(this IServiceCollection services) { + + services.AddHttpClient(); + services.AddSingleton(); + } + } +} diff --git a/src/LetsEncrypt/LetsEncrypt.csproj b/src/LetsEncrypt/LetsEncrypt.csproj new file mode 100644 index 0000000..ae5e10f --- /dev/null +++ b/src/LetsEncrypt/LetsEncrypt.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + enable + MaksIT.$(MSBuildProjectName.Replace(" ", "_")) + + + + + + + + + + + + + + diff --git a/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs b/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs new file mode 100644 index 0000000..0fcaa1f --- /dev/null +++ b/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..6ffeb01 --- /dev/null +++ b/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..8594c90 --- /dev/null +++ b/src/LetsEncrypt/Models/Responses/Account.cs @@ -0,0 +1,38 @@ +using MaksIT.LetsEncrypt.Entities.Jws; +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 { + + public bool TermsOfServiceAgreed { 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[]? Contacts { get; set; } + + public string? Status { get; set; } + + public string? Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public Jwk? Key { get; set; } + + public string? InitialIp { get; set; } + + public Uri? Orders { get; set; } + + public Uri? Location { get; set; } + } +} diff --git a/v1.0/LetsEncrypt/ACMEv2/AcmeDirectory.cs b/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs similarity index 61% rename from v1.0/LetsEncrypt/ACMEv2/AcmeDirectory.cs rename to src/LetsEncrypt/Models/Responses/AcmeDirectory.cs index b060fdd..065e063 100644 --- a/v1.0/LetsEncrypt/ACMEv2/AcmeDirectory.cs +++ b/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs @@ -1,43 +1,29 @@ using System; -using Newtonsoft.Json; -namespace ACMEv2 +namespace MaksIT.LetsEncrypt.Models.Responses { public class AcmeDirectory { - //New nonce - [JsonProperty("newNonce")] public Uri NewNonce { get; set; } - //New account - [JsonProperty("newAccount")] + public Uri NewAccount { get; set; } - //New order - [JsonProperty("newOrder")] 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; } - - //Revoke certificate - [JsonProperty("revokeCert")] public Uri RevokeCertificate { get; set; } - //Key change - [JsonProperty("keyChange")] public Uri KeyChange { get; set; } - //Metadata object - [JsonProperty("meta")] public AcmeDirectoryMeta Meta { get; set; } } public class AcmeDirectoryMeta { - [JsonProperty("termsOfService")] public string TermsOfService { get; set; } } } diff --git a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs new file mode 100644 index 0000000..211ae81 --- /dev/null +++ b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs @@ -0,0 +1,20 @@ + +using MaksIT.LetsEncrypt.Entities; + +namespace MaksIT.LetsEncrypt.Models.Responses { + public class AuthorizationChallengeResponse { + public OrderIdentifier? Identifier { get; set; } + + public string? Status { get; set; } + + public DateTime? Expires { get; set; } + + public bool Wildcard { 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 new file mode 100644 index 0000000..12d316a --- /dev/null +++ b/src/LetsEncrypt/Models/Responses/Order.cs @@ -0,0 +1,34 @@ +using MaksIT.LetsEncrypt.Exceptions; +using MaksIT.LetsEncrypt.Models.Interfaces; + +namespace MaksIT.LetsEncrypt.Models.Responses { + + public class OrderIdentifier { + public string? Type { 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; } + } +} diff --git a/src/LetsEncrypt/Services/JwsService.cs b/src/LetsEncrypt/Services/JwsService.cs new file mode 100644 index 0000000..b66bac6 --- /dev/null +++ b/src/LetsEncrypt/Services/JwsService.cs @@ -0,0 +1,114 @@ +/** +* https://tools.ietf.org/html/rfc4648 +* https://tools.ietf.org/html/rfc4648#section-5 +*/ + + +using System.Text; +using System.Security.Cryptography; + +using MaksIT.LetsEncrypt.Entities.Jws; + +using MaksIT.Core.Extensions; + +namespace MaksIT.LetsEncrypt.Services { + public interface IJwsService { + + void Init(RSA rsa, string? keyId); + + JwsMessage Encode(JwsHeader protectedHeader); + + JwsMessage Encode(TPayload payload, JwsHeader protectedHeader); + + string GetKeyAuthorization(string token); + + string Base64UrlEncoded(byte[] arg); + + void SetKeyId(string location); + } + + + public class JwsService : IJwsService { + + public Jwk? _jwk; + private RSA? _rsa; + + public void Init(RSA rsa, string? keyId) { + _rsa = rsa ?? throw new ArgumentNullException(nameof(rsa)); + + var publicParameters = rsa.ExportParameters(false); + + _jwk = new Jwk() { + KeyType = "RSA", + Exponent = Base64UrlEncoded(publicParameters.Exponent), + Modulus = Base64UrlEncoded(publicParameters.Modulus), + KeyId = keyId + }; + } + + public JwsMessage Encode(JwsHeader protectedHeader) => + Encode(null, protectedHeader); + + public JwsMessage Encode(TPayload? 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) { + string value = payload.ToString(); + message.Payload = Base64UrlEncoded(value); + } + else { + message.Payload = Base64UrlEncoded(payload.ToJson()); + } + } + + + message.Signature = Base64UrlEncoded( + _rsa.SignData(Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}"), + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1)); + + return message; + } + + private string GetSha256Thumbprint() { + var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}"; + + using (var sha256 = SHA256.Create()) { + return Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(json))); + } + } + + public string GetKeyAuthorization(string token) => $"{token}.{GetSha256Thumbprint()}"; + + + + public string Base64UrlEncoded(string s) => Base64UrlEncoded(Encoding.UTF8.GetBytes(s)); + + + // https://tools.ietf.org/html/rfc4648#section-5 + public string Base64UrlEncoded(byte[] arg) { + var s = Convert.ToBase64String(arg); // Regular base64 encoder + s = s.Split('=')[0]; // Remove any trailing '='s + s = s.Replace('+', '-'); // 62nd char of encoding + s = s.Replace('/', '_'); // 63rd char of encoding + return s; + } + + public void SetKeyId(string location) { + _jwk.KeyId = location; + } + } +} diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs new file mode 100644 index 0000000..07f6154 --- /dev/null +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -0,0 +1,478 @@ +/** +* https://community.letsencrypt.org/t/trying-to-do-post-as-get-but-getting-post-jws-not-signed/108371 +* https://tools.ietf.org/html/rfc8555#section-6.2 +* +*/ +using System.Text; + +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +using Microsoft.Extensions.Logging; + +using MaksIT.LetsEncrypt.Entities; +using MaksIT.LetsEncrypt.Exceptions; +using MaksIT.Core.Extensions; + +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 { + + public interface ILetsEncryptService { + + Task ConfigureClient(string url, string[] contacts); + + Task Init(RegistrationCache? registrationCache); + + string GetTermsOfServiceUri(); + + + Task> NewOrder(string[] hostnames, string challengeType); + Task CompleteChallenges(); + Task GetOrder(string[] hostnames); + Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject); + + RegistrationCache? GetRegistrationCache(); + } + + + + + public class LetsEncryptService : ILetsEncryptService { + + //private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings { + // NullValueHandling = NullValueHandling.Ignore, + // Formatting = Formatting.Indented + //}; + + private readonly ILogger _logger; + private readonly IJwsService _jwsService; + private HttpClient _httpClient; + private string[]? _contacts; + + private AcmeDirectory? _directory; + private RegistrationCache? _cache; + + private string? _nonce; + + private List _challenges = new List(); + private Order? _currentOrder; + + public LetsEncryptService( + ILogger logger, + IJwsService jwsService, + HttpClient httpClient + ) { + _logger = logger; + _jwsService = jwsService; + _httpClient = httpClient; + } + + + /// + /// + /// + /// + /// + /// + public async Task ConfigureClient(string url, string[] contacts) { + + _httpClient.BaseAddress ??= new Uri(url); + + _contacts = contacts; + + (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null); + } + + /// + /// Account creation or Initialization from cache + /// + /// + /// + /// + public async Task Init(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 = cache; + accountKey.ImportCspBlob(_cache.AccountKey); + } + + // New Account request + _jwsService.Init(accountKey, null); + + + 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 != "pending") + throw new InvalidOperationException($"Created new order and expected status 'pending or ready', 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(); + } + } + + 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.LogTrace($"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(); + } + } +} \ No newline at end of file diff --git a/src/LetsEncryptConsole/App.cs b/src/LetsEncryptConsole/App.cs new file mode 100644 index 0000000..8b7decd --- /dev/null +++ b/src/LetsEncryptConsole/App.cs @@ -0,0 +1,287 @@ +using System.Text; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using MaksIT.LetsEncrypt.Services; +using MaksIT.LetsEncrypt.Entities; +using MaksIT.LetsEncryptConsole.Services; + +using MaksIT.Core.Extensions; +using System.Text.Json; + +namespace MaksIT.LetsEncryptConsole { + + 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 IJwsService _jwsService; + private readonly IKeyService _keyService; + private readonly ITerminalService _terminalService; + + public App( + ILogger logger, + IOptions appSettings, + ILetsEncryptService letsEncryptService, + IJwsService jwsService, + IKeyService keyService, + ITerminalService terminalService + ) { + _logger = logger; + _appSettings = appSettings.Value; + _letsEncryptService = letsEncryptService; + _jwsService = jwsService; + _keyService = keyService; + _terminalService = terminalService; + } + + public async Task Run(string[] args) { + + foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List()) { + try { + _logger.LogTrace($"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.LogTrace($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}"); + + //loop each customer website + foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List()) { + _logger.LogTrace($"Managing site: {site.Name}"); + + try { + //define cache folder + string cacheFolder = Path.Combine(_appPath, env.Cache, customer.Id); + if (!Directory.Exists(cacheFolder)) { + Directory.CreateDirectory(cacheFolder); + } + + //1. Client initialization + _logger.LogTrace("1. Client Initialization..."); + + #region LetsEncrypt client configuration + await _letsEncryptService.ConfigureClient(env.Url, customer.Contacts); + #endregion + + #region LetsEncrypt local registration cache initialization + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(site.Name)); + var cacheFileName = _jwsService.Base64UrlEncoded(hash) + ".lets-encrypt.cache.json"; + var cachePath = Path.Combine(cacheFolder, cacheFileName); + + var cacheFile = File.Exists(cachePath) + ? File.ReadAllText(cachePath) + : null; + + var registrationCache = cacheFile.ToObject(); + await _letsEncryptService.Init(registrationCache); + registrationCache = _letsEncryptService.GetRegistrationCache(); + #endregion + + #region LetsEncrypt terms of service + _logger.LogTrace($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}"); + #endregion + + //create folder for ssl + string ssl = Path.Combine(env.GetSSL(), site.Name); + if (!Directory.Exists(ssl)) { + Directory.CreateDirectory(ssl); + } + + // 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(ssl, $"{site.Name}.crt"); + //if(!File.Exists(cert)) + File.WriteAllText(cert, certRes.Certificate); + + string key = Path.Combine(ssl, $"{site.Name}.key"); + //if(!File.Exists(key)) { + using (StreamWriter writer = File.CreateText(key)) + _keyService.ExportPrivateKey(certRes.PrivateKey, writer); + //} + + _logger.LogTrace("Certificate and Key exists and valid. Restored from cache."); + } + else { + + //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 + + switch (site.Challenge) { + case "http-01": { + //ensure to enable static file discovery on server in .well-known/acme-challenge + //and listen on 80 port + + //check acme directory + var acme = env.GetACME(); + + if (!Directory.Exists(acme)) { + Directory.CreateDirectory(acme); + } + + foreach (FileInfo file in new DirectoryInfo(acme).GetFiles()) { + if (file.LastWriteTimeUtc < DateTime.UtcNow.AddMonths(-3)) + file.Delete(); + } + + + foreach (var result in orders) { + Console.WriteLine($"Key: {result.Key}, Value: {result.Value}"); + string[] splitToken = result.Value.Split('.'); + + File.WriteAllText(Path.Combine(env.GetACME(), splitToken[0]), result.Value); + } + + if (OperatingSystem.IsLinux()) { + _terminalService.Exec($"chgrp -R nginx {env.GetACME()}"); + _terminalService.Exec($"chmod -R g+rwx {env.GetACME()}"); + } + + break; + } + + case "dns-01": { + //Manage DNS server MX record, depends from provider + throw new NotImplementedException(); + } + + default: { + throw new NotImplementedException(); + } + } + + #region LetsEncrypt complete challenges + _logger.LogTrace("3. Client Complete Challange..."); + await _letsEncryptService.CompleteChallenges(); + _logger.LogTrace("Challanges comleted."); + #endregion + + await Task.Delay(1000); + + // Download new certificate + _logger.LogTrace("4. Download certificate..."); + var (cert, key) = await _letsEncryptService.GetCertificate(site.Name); + + #region Persist cache + registrationCache = _letsEncryptService.GetRegistrationCache(); + File.WriteAllText(cachePath, registrationCache.ToJson()); + #endregion + + #region Save cert and key to filesystem + certRes = new CachedCertificateResult(); + if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) { + string certPath = Path.Combine(ssl, site.Name + ".crt"); + File.WriteAllText(certPath, certRes.Certificate); + + string keyPath = Path.Combine(ssl, site.Name + ".key"); + using (var writer = File.CreateText(keyPath)) + _keyService.ExportPrivateKey(certRes.PrivateKey, writer); + + _logger.LogTrace("Certificate saved."); + } + 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, "Environment unhandled error"); + } + } + + if (env.Name == "ProductionV2") { + _terminalService.Exec("systemctl restart nginx"); + } + } + catch (Exception ex) { + _logger.LogError(ex.Message.ToString()); + break; + } + } + } + + + + + /// + /// + /// + /// + /// + /// + 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; + } + + 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 != null) + foreach (var host in hostsToRemove) + registrationCache.CachedCerts.Remove(host); + + return registrationCache; + } + } +} diff --git a/src/LetsEncryptConsole/Configuration.cs b/src/LetsEncryptConsole/Configuration.cs new file mode 100644 index 0000000..59872d7 --- /dev/null +++ b/src/LetsEncryptConsole/Configuration.cs @@ -0,0 +1,74 @@ +using System.Runtime.InteropServices; + +namespace MaksIT.LetsEncryptConsole { + public class Configuration { + public LetsEncryptEnvironment[]? Environments { get; set; } + public Customer[]? Customers { get; set; } + } + + public class OsDependant { + public string? Windows { get; set; } + public string? Linux { get; set; } + } + + + + public class LetsEncryptEnvironment { + public bool Active { get; set; } + public string? Name { get; set; } + public string? Url { get; set; } + + private string? _cache; + public string Cache { + get => _cache ?? ""; + set => _cache = value; + } + + public OsDependant? ACME { get; set; } + public OsDependant? SSL { get; set; } + + + public string? GetACME() { + + if (OperatingSystem.IsWindows()) + return ACME?.Windows; + + if (OperatingSystem.IsLinux()) + return ACME?.Linux; + + return default; + } + + public string? GetSSL() { + + if (OperatingSystem.IsWindows()) + return SSL?.Windows; + + if (OperatingSystem.IsLinux()) + return SSL?.Linux; + + return default; + } + } + + public class Customer { + private string? _id; + public string Id { + get => _id ?? string.Empty; + set => _id = value; + } + + public bool Active { get; set; } + public string[]? Contacts { get; set; } + public string? Name { get; set; } + public string? LastName { get; set; } + public Site[]? Sites { get; set; } + } + + public class Site { + public bool Active { get; set; } + public string? Name { get; set; } + public string[]? Hosts { get; set; } + public string? Challenge { get; set; } + } +} diff --git a/src/LetsEncryptConsole/LetsEncryptConsole.csproj b/src/LetsEncryptConsole/LetsEncryptConsole.csproj new file mode 100644 index 0000000..15dacd2 --- /dev/null +++ b/src/LetsEncryptConsole/LetsEncryptConsole.csproj @@ -0,0 +1,43 @@ + + + + Exe + net7.0 + enable + enable + MaksIT.$(MSBuildProjectName.Replace(" ", "_")) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/src/LetsEncryptConsole/Program.cs b/src/LetsEncryptConsole/Program.cs new file mode 100644 index 0000000..2b205e2 --- /dev/null +++ b/src/LetsEncryptConsole/Program.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using Serilog; + +using MaksIT.LetsEncryptConsole.Services; +using MaksIT.LetsEncrypt.Extensions; + +namespace MaksIT.LetsEncryptConsole { + class Program { + private static readonly IConfiguration _configuration = InitConfig(); + + static void Main(string[] args) { + // create service collection + var services = new ServiceCollection(); + ConfigureServices(services); + + // 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(); +#pragma warning restore CS8602 // Dereference of a possibly null reference. + } + + public static void ConfigureServices(IServiceCollection services) { + + 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 Services + services.RegisterLetsEncrypt(); + + services.AddSingleton(); + services.AddSingleton(); + #endregion + + // add app + services.AddSingleton(); + } + + private static IConfiguration InitConfig() { + var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + + 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); + + return configuration.Build(); + } + } +} diff --git a/v2.0/LetsEncrypt/README.md b/src/LetsEncryptConsole/README.md similarity index 60% rename from v2.0/LetsEncrypt/README.md rename to src/LetsEncryptConsole/README.md index ac48803..2b63b82 100644 --- a/v2.0/LetsEncrypt/README.md +++ b/src/LetsEncryptConsole/README.md @@ -81,3 +81,42 @@ Lines with labels in quotes indicate HTTP link relations. | | V V valid invalid + + + + + https://community.letsencrypt.org/t/acme-client-finalized-order-stuck-on-ready-state/165196 + + The following table illustrates a typical sequence of requests + required to establish a new account with the server, prove control of + an identifier, issue a certificate, and fetch an updated certificate + some time after issuance. The "->" is a mnemonic for a Location + header field pointing to a created resource. + + +-------------------+--------------------------------+--------------+ + | Action | Request | Response | + +-------------------+--------------------------------+--------------+ + | Get directory | GET directory | 200 | + | | | | + | Get nonce | HEAD newNonce | 200 | + | | | | + | Create account | POST newAccount | 201 -> | + | | | account | + | | | | + | Submit order | POST newOrder | 201 -> order | + | | | | + | Fetch challenges | POST-as-GET order's | 200 | + | | authorization urls | | + | | | | + | Respond to | POST authorization challenge | 200 | + | challenges | urls | | + | | | | + | Poll for status | POST-as-GET order | 200 | + | | | | + | Finalize order | POST order's finalize url | 200 | + | | | | + | Poll for status | POST-as-GET order | 200 | + | | | | + | Download | POST-as-GET order's | 200 | + | certificate | certificate url | | + +-------------------+--------------------------------+--------------+ \ No newline at end of file diff --git a/src/LetsEncryptConsole/Services/KeyService.cs b/src/LetsEncryptConsole/Services/KeyService.cs new file mode 100644 index 0000000..26743fc --- /dev/null +++ b/src/LetsEncryptConsole/Services/KeyService.cs @@ -0,0 +1,151 @@ +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]); + } + } + } + } +} \ No newline at end of file diff --git a/v2.0/LetsEncrypt/Services/TerminalService.cs b/src/LetsEncryptConsole/Services/TerminalService.cs similarity index 94% rename from v2.0/LetsEncrypt/Services/TerminalService.cs rename to src/LetsEncryptConsole/Services/TerminalService.cs index 20f8934..f65a119 100644 --- a/v2.0/LetsEncrypt/Services/TerminalService.cs +++ b/src/LetsEncryptConsole/Services/TerminalService.cs @@ -1,7 +1,6 @@ -using System; using System.Diagnostics; -namespace LetsEncrypt { +namespace MaksIT.LetsEncryptConsole.Services { public interface ITerminalService { void Exec(string cmd); diff --git a/src/LetsEncryptConsole/appsettings.json b/src/LetsEncryptConsole/appsettings.json new file mode 100644 index 0000000..da791d6 --- /dev/null +++ b/src/LetsEncryptConsole/appsettings.json @@ -0,0 +1,94 @@ +{ + "Serilog": { + "Using": [ "Serilog.Settings.Configuration", "Serilog.Expressions", "Serilog.Sinks.Console" ], + "MinimumLevel": "Information", + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ], + "WriteTo": [ + { + "Name": "Console", + "Args": { + "restrictedToMinimumLevel": "Information", + "formatter": "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact" + } + } + ] + }, + "Configuration": { + "Environments": [ + { + "Active": false, + "Name": "StagingV2", + "Url": "https://acme-staging-v02.api.letsencrypt.org/directory", + + "Cache": "staging_cache", + "ACME": { + "Linux": "/var/www/html/.well-known/acme-challenge", + "Windows": "C:\\Windows\\Temp\\www\\html\\.well-known\\acme-challenge" + }, + "SSL": { + "Linux": "/var/www/ssl", + "Windows": "C:\\Windows\\Temp\\www\\ssl" + } + }, + { + "Active": true, + "Name": "ProductionV2", + "Url": "https://acme-v02.api.letsencrypt.org/directory", + + "Cache": "production_cache", + "ACME": { + "Linux": "/var/www/html/.well-known/acme-challenge", + "Windows": "C:\\Windows\\Temp\\www\\html\\.well-known\\acme-challenge" + }, + "SSL": { + "Linux": "/var/www/ssl", + "Windows": "C:\\Windows\\Temp\\www\\ssl" + } + } + ], + + "Customers": [ + { + "Id": "9b4c8584-dc83-4388-b45f-2942e34dca9d", + "Active": true, + "Contacts": [ "maksym.sadovnychyy@gmail.com" ], + "Name": "Maksym", + "LastName": "Sadovnychyy", + + "Sites": [ + { + "Active": false, + "Name": "maks-it.com", + "Hosts": [ + "maks-it.com", + "www.maks-it.com" + ], + "Challenge": "http-01" + } + ] + }, + { + "Id": "46337ef5-d69b-4332-b6ef-67959dfb3c2c", + "Active": true, + "Contacts": [ + "maksym.sadovnychyy@gmail.com", + "anastasiia.pavlovskaia@gmail.com" + ], + "Name": "Anastasiia", + "LastName": "Pavlovskaia", + + "Sites": [ + { + "Active": true, + "Name": "nastyarey.com", + "Hosts": [ + "nastyarey.com", + "www.nastyarey.com" + ], + "Challenge": "http-01" + } + ] + } + ] + } +} diff --git a/v1.0/LetsEncrypt.sln b/v1.0/LetsEncrypt.sln deleted file mode 100644 index bffcf60..0000000 --- a/v1.0/LetsEncrypt.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.572 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt/LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351} - EndGlobalSection -EndGlobal diff --git a/v1.0/LetsEncrypt/.vscode/launch.json b/v1.0/LetsEncrypt/.vscode/launch.json deleted file mode 100644 index 125ba92..0000000 --- a/v1.0/LetsEncrypt/.vscode/launch.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "version": "0.2.0", - "configurations": [ - { - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/bin/Debug/netcoreapp2.2/LetsEncrypt.dll", - "args": [], - "cwd": "${workspaceFolder}", - // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" - } - ] -} \ No newline at end of file diff --git a/v1.0/LetsEncrypt/ACMEv2/Account.cs b/v1.0/LetsEncrypt/ACMEv2/Account.cs deleted file mode 100644 index 091947c..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/Account.cs +++ /dev/null @@ -1,48 +0,0 @@ -/* - * https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.3 - */ - -using System; -using Newtonsoft.Json; - -namespace ACMEv2 -{ - public class Account : IHasLocation - { - [JsonProperty("termsOfServiceAgreed")] - public bool TermsOfServiceAgreed { 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 - */ - [JsonProperty("onlyReturnExisting")] - public bool OnlyReturnExisting { get; set; } - - [JsonProperty("contact")] - public string[] Contacts { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("createdAt")] - public DateTime CreatedAt { get; set; } - - [JsonProperty("key")] - public Jwk Key { get; set; } - - [JsonProperty("initialIp")] - public string InitialIp { get; set; } - - [JsonProperty("orders")] - public Uri Orders { get; set; } - - public Uri Location { get; set; } - } - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/AuthorizationChallange.cs b/v1.0/LetsEncrypt/ACMEv2/AuthorizationChallange.cs deleted file mode 100644 index 618d52a..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/AuthorizationChallange.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace ACMEv2 -{ - public class AuthorizationChallenge - { - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("token")] - public string Token { get; set; } - - } - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs b/v1.0/LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs deleted file mode 100644 index 9345e0e..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace ACMEv2 -{ - public class AuthorizationChallengeResponse - { - [JsonProperty("identifier")] - public OrderIdentifier Identifier { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("expires")] - public DateTime? Expires { get; set; } - - [JsonProperty("wildcard")] - public bool Wildcard { get; set; } - - [JsonProperty("challenges")] - public AuthorizationChallenge[] Challenges { get; set; } - } - - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/AuthorizeChallenge.cs b/v1.0/LetsEncrypt/ACMEv2/AuthorizeChallenge.cs deleted file mode 100644 index e81a133..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/AuthorizeChallenge.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace ACMEv2 -{ - - public class AuthorizeChallenge - { - [JsonProperty("keyAuthorization")] - public string KeyAuthorization { get; set; } - - } - - - - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/CachedCertificateResult.cs b/v1.0/LetsEncrypt/ACMEv2/CachedCertificateResult.cs deleted file mode 100644 index a71a079..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/CachedCertificateResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Security.Cryptography; - -namespace ACMEv2 -{ - public class CachedCertificateResult - { - public RSACryptoServiceProvider PrivateKey; - public string Certificate; - } - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/CertificateCache.cs b/v1.0/LetsEncrypt/ACMEv2/CertificateCache.cs deleted file mode 100644 index 77df951..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/CertificateCache.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ACMEv2 -{ - public class CertificateCache - { - public string Cert; - public byte[] Private; - } - - - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/FinalizeRequest.cs b/v1.0/LetsEncrypt/ACMEv2/FinalizeRequest.cs deleted file mode 100644 index e9175b1..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/FinalizeRequest.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json; - -namespace ACMEv2 -{ - public class FinalizeRequest - { - [JsonProperty("csr")] - public string CSR { get; set; } - } - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/IHashLocation.cs b/v1.0/LetsEncrypt/ACMEv2/IHashLocation.cs deleted file mode 100644 index 8927a70..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/IHashLocation.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace ACMEv2 -{ - interface IHasLocation - { - Uri Location { get; set; } - } - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/Jwk.cs b/v1.0/LetsEncrypt/ACMEv2/Jwk.cs deleted file mode 100644 index 0453ae7..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/Jwk.cs +++ /dev/null @@ -1,114 +0,0 @@ -/* - * JSON Web Key (JWK) - * https://tools.ietf.org/html/rfc7517 - * https://www.gnupg.org/documentation/manuals/gcrypt-devel/RSA-key-parameters.html - * https://static.javadoc.io/com.nimbusds/nimbus-jose-jwt/2.15.2/com/nimbusds/jose/jwk/RSAKey.html - * -*/ - -using Newtonsoft.Json; - -namespace ACMEv2 -{ - 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". - /// - /// - [JsonProperty("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. - /// - /// - [JsonProperty("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. - /// - /// - [JsonProperty("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. - /// - [JsonProperty("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. - /// - [JsonProperty("e")] - public string Exponent { get; set; } - - /// - /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonProperty("d")] - public string D { get; set; } - - /// - /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonProperty("p")] - public string P { get; set; } - - /// - /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonProperty("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. - /// - [JsonProperty("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. - /// - [JsonProperty("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. - /// - [JsonProperty("qi")] - public string InverseQ { get; set; } - - /// - /// The other primes information, should they exist, null or an empty list if not specified. - /// - [JsonProperty("oth")] - public string OthInf { get; set; } - - /// - /// "alg" (Algorithm) Parameter - /// - /// The "alg" (algorithm) parameter identifies the algorithm intended for - /// use with the key. - /// - /// - [JsonProperty("alg")] - public string Algorithm { get; set; } - } - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/Jws.cs b/v1.0/LetsEncrypt/ACMEv2/Jws.cs deleted file mode 100644 index 8f6e365..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/Jws.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Text; -using Newtonsoft.Json; - -namespace ACMEv2 -{ - public class Jws - { - public readonly Jwk _jwk; - private readonly RSA _rsa; - - public Jws(RSA rsa, string keyId) - { - _rsa = rsa ?? throw new ArgumentNullException(nameof(rsa)); - - var publicParameters = rsa.ExportParameters(false); - - _jwk = new Jwk - { - KeyType = "RSA", - Exponent = Base64UrlEncoded(publicParameters.Exponent), - Modulus = Base64UrlEncoded(publicParameters.Modulus), - KeyId = keyId - }; - } - - public JwsMessage Encode(TPayload payload, JwsHeader protectedHeader) - { - - protectedHeader.Algorithm = "RS256"; - if (_jwk.KeyId != null) - { - protectedHeader.KeyId = _jwk.KeyId; - } - else - { - protectedHeader.Key = _jwk; - } - - var message = new JwsMessage - { - Payload = Base64UrlEncoded(JsonConvert.SerializeObject(payload)), - Protected = Base64UrlEncoded(JsonConvert.SerializeObject(protectedHeader)) - }; - - message.Signature = Base64UrlEncoded( - _rsa.SignData(Encoding.ASCII.GetBytes(message.Protected + "." + message.Payload), - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1)); - - return message; - } - - private string GetSha256Thumbprint() - { - var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}"; - - using (var sha256 = SHA256.Create()) - { - return Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(json))); - } - } - - public string GetKeyAuthorization(string token) - { - return token + "." + GetSha256Thumbprint(); - } - - public static string Base64UrlEncoded(string s) - { - return Base64UrlEncoded(Encoding.UTF8.GetBytes(s)); - } - - public static string Base64UrlEncoded(byte[] arg) - { - var s = Convert.ToBase64String(arg); // Regular base64 encoder - s = s.Split('=')[0]; // Remove any trailing '='s - s = s.Replace('+', '-'); // 62nd char of encoding - s = s.Replace('/', '_'); // 63rd char of encoding - return s; - } - - internal void SetKeyId(Account account) - { - _jwk.KeyId = account.Id; - } - } -} diff --git a/v1.0/LetsEncrypt/ACMEv2/JwsHeader.cs b/v1.0/LetsEncrypt/ACMEv2/JwsHeader.cs deleted file mode 100644 index 3429e0a..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/JwsHeader.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace ACMEv2 -{ - public class JwsHeader - { - //public JwsHeader() - //{ - //} - - //public JwsHeader(string algorithm, Jwk key) - //{ - // Algorithm = algorithm; - // Key = key; - //} - - [JsonProperty("alg")] - public string Algorithm { get; set; } - - [JsonProperty("jwk")] - public Jwk Key { get; set; } - - - [JsonProperty("kid")] - public string KeyId { get; set; } - - - [JsonProperty("nonce")] - public string Nonce { get; set; } - - - [JsonProperty("url")] - public Uri Url { get; set; } - - - [JsonProperty("Host")] - public string Host { get; set; } - } -} diff --git a/v1.0/LetsEncrypt/ACMEv2/JwsMessage.cs b/v1.0/LetsEncrypt/ACMEv2/JwsMessage.cs deleted file mode 100644 index 5eae70b..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/JwsMessage.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json; - - -namespace ACMEv2 -{ - - public class JwsMessage - { - [JsonProperty("header")] - public JwsHeader Header { get; set; } - - [JsonProperty("protected")] - public string Protected { get; set; } - - [JsonProperty("payload")] - public string Payload { get; set; } - - [JsonProperty("signature")] - public string Signature { get; set; } - } - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/LetsEncryptClient.cs b/v1.0/LetsEncrypt/ACMEv2/LetsEncryptClient.cs deleted file mode 100644 index e567a1a..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/LetsEncryptClient.cs +++ /dev/null @@ -1,545 +0,0 @@ -/* - * Author: Maksym Sadovnychyy - * Updated according https://tools.ietf.org/html/draft-ietf-acme-acme-18 - */ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - - -namespace ACMEv2 -{ - public class LetsEncryptClient - { - public const string StagingV2 = "https://acme-staging-v02.api.letsencrypt.org/directory"; - public const string ProductionV2 = "https://acme-v02.api.letsencrypt.org/directory"; - - private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.Indented - }; - - private static Dictionary _cachedClients = new Dictionary(StringComparer.OrdinalIgnoreCase); - - private static HttpClient GetCachedClient(string url) - { - if (_cachedClients.TryGetValue(url, out var value)) - { - return value; - } - - lock (Locker) - { - if (_cachedClients.TryGetValue(url, out value)) - { - return value; - } - - value = new HttpClient - { - BaseAddress = new Uri(url) - }; - - _cachedClients = new Dictionary(_cachedClients, StringComparer.OrdinalIgnoreCase) - { - [url] = value - }; - return value; - } - } - - /// - /// In our scenario, we assume a single single wizard progressing - /// and the locking is basic to the wizard progress. Adding explicit - /// locking to be sure that we are not corrupting disk state if user - /// is explicitly calling stuff concurrently (running the setup wizard - /// from two tabs?) - /// - private static readonly object Locker = new object(); - - private Jws _jws; - private readonly string _path; - private readonly string _url; - private readonly string _home; - private string _nonce; - private RSACryptoServiceProvider _accountKey; - private RegistrationCache _cache; - private HttpClient _client; - private AcmeDirectory _directory; - private List _challenges = new List(); - private Order _currentOrder; - - /// - /// Let's encrypt client object - /// - /// - /// - public LetsEncryptClient(string url, string home, string siteName) - { - _url = url ?? throw new ArgumentNullException(nameof(url)); - var hash = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(siteName)); - - _home = home ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, - Environment.SpecialFolderOption.Create); - - var file = Jws.Base64UrlEncoded(hash) + ".lets-encrypt.cache.json"; - _path = Path.Combine(_home, file); - } - - /// - /// Account creation or Initialization from cache - /// - /// - /// - /// - public async Task Init(string[] contacts, CancellationToken token = default(CancellationToken)) - { - _accountKey = new RSACryptoServiceProvider(4096); - _client = GetCachedClient(_url); - - // 1 - Get directory - (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), null, token); - - - if (File.Exists(_path)) - { - bool success; - try - { - lock (Locker) - { - _cache = JsonConvert.DeserializeObject(File.ReadAllText(_path)); - } - - _accountKey.ImportCspBlob(_cache.AccountKey); - //_jws = new Jws(_accountKey, _cache.Id); - success = true; - } - catch - { - success = false; - // if we failed for any reason, we'll just - // generate a new registration - } - - if (success) - { - return; - } - } - - await NewNonce(); - - //New Account request - _jws = new Jws(_accountKey, null); - var (account, response) = await SendAsync(HttpMethod.Post, _directory.NewAccount, new Account - { - // we validate this in the UI before we get here, so that is fine - TermsOfServiceAgreed = true, - Contacts = contacts.Select(contact => - string.Format("mailto:{0}", contact) - ).ToArray() - - }, token); - _jws.SetKeyId(account); - - if (account.Status != "valid") - throw new InvalidOperationException("Account status is not valid, was: " + account.Status + Environment.NewLine + response); - - lock (Locker) - { - _cache = new RegistrationCache - { - Location = account.Location, - AccountKey = _accountKey.ExportCspBlob(true), - Id = account.Id, - Key = account.Key - }; - File.WriteAllText(_path, JsonConvert.SerializeObject(_cache, Formatting.Indented)); - } - } - - /// - /// Just retrive terms of service - /// - /// - /// - public string GetTermsOfServiceUri(CancellationToken token = default(CancellationToken)) - { - return _directory.Meta.TermsOfService; - } - - /// - /// Request New Nonce to be able to start POST requests - /// - /// - /// - public async Task NewNonce(CancellationToken token = default(CancellationToken)) - { - _nonce = (await _client.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce)).ConfigureAwait(false)).Headers.GetValues("Replay-Nonce").First(); - } - - - /// - /// 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, CancellationToken token = default(CancellationToken)) - { - _challenges.Clear(); - - //update jws with account url - _jws = new Jws(_accountKey, _cache.Location.ToString()); - - var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, new Order - { - Expires = DateTime.UtcNow.AddDays(2), - Identifiers = hostnames.Select(hostname => new OrderIdentifier - { - Type = "dns", - Value = hostname - }).ToArray() - }, token); - - if (order.Status != "pending") - throw new InvalidOperationException("Created new order and expected status 'pending', but got: " + order.Status + Environment.NewLine + - response); - _currentOrder = order; - - var results = new Dictionary(); - foreach (var item in order.Authorizations) - { - var (challengeResponse, responseText) = await SendAsync(HttpMethod.Get, item, null, token); - if (challengeResponse.Status == "valid") - continue; - - if (challengeResponse.Status != "pending") - throw new InvalidOperationException("Expected autorization status 'pending', but got: " + order.Status + - Environment.NewLine + responseText); - - var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType); - _challenges.Add(challenge); - - var keyToken = _jws.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 = Jws.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] = challenge.Token + "~" + keyToken; - break; - } - - } - - - - } - - return results; - } - - public async Task CompleteChallenges(CancellationToken token = default(CancellationToken)) - { - _jws = new Jws(_accountKey, _cache.Location.ToString()); - - 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 = _jws.GetKeyAuthorization(challenge.Token); - break; - } - - case "http-01": { - break; - } - } - - var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, authorizeChallenge, token); - - if (result.Status == "valid") - break; - if (result.Status != "pending") - throw new InvalidOperationException("Failed autorization of " + _currentOrder.Identifiers[index].Value + Environment.NewLine + responseText); - - - await Task.Delay(500); - } - } - } - - - - public async Task GetOrder(string[] hostnames, CancellationToken token = default(CancellationToken)) - { - //update jws - _jws = new Jws(_accountKey, _cache.Location.ToString()); - - var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, new Order - { - Expires = DateTime.UtcNow.AddDays(2), - Identifiers = hostnames.Select(hostname => new OrderIdentifier - { - Type = "dns", - Value = hostname - }).ToArray() - }, token); - - _currentOrder = order; - } - - /// - /// - /// - /// - /// - public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject, CancellationToken token = default(CancellationToken)) - { - 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 (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, new FinalizeRequest - { - CSR = Jws.Base64UrlEncoded(csr.CreateSigningRequest()) - }, token); - - while (response.Status != "valid") - { - (response, responseText) = await SendAsync(HttpMethod.Get, response.Location, null, token); - - if(response.Status == "processing") - { - await Task.Delay(500); - continue; - } - throw new InvalidOperationException("Invalid order status: " + response.Status + Environment.NewLine + - responseText); - } - var (pem, _) = await SendAsync(HttpMethod.Get, response.Certificate, null, token); - - var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem)); - - _cache.CachedCerts[subject] = new CertificateCache - { - Cert = pem, - Private = key.ExportCspBlob(true) - }; - - lock (Locker) - { - File.WriteAllText(_path, - JsonConvert.SerializeObject(_cache, Formatting.Indented)); - } - - return (cert, key); - } - - - - - - - - - - - - - /// - /// - /// - /// - /// - public async Task KeyChange(CancellationToken token = default(CancellationToken)) - { - - } - - - /// - /// - /// - /// - /// - public async Task RevokeCertificate(CancellationToken token = default(CancellationToken)) - { - - } - - - - - - /// - /// Main method used to send data to LetsEncrypt - /// - /// - /// - /// - /// - /// - /// - private async Task<(TResult Result, string Response)> SendAsync(HttpMethod method, Uri uri, object message, CancellationToken token) where TResult : class - { - var request = new HttpRequestMessage(method, uri); - - if (message != null) - { - JwsMessage encodedMessage = _jws.Encode(message, new JwsHeader - { - Nonce = _nonce, - Url = uri, - }); - - var json = JsonConvert.SerializeObject(encodedMessage, jsonSettings); - - 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 _client.SendAsync(request, token).ConfigureAwait(false); - - 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().ConfigureAwait(false); - var problem = JsonConvert.DeserializeObject(problemJson); - problem.RawJson = problemJson; - throw new LetsEncrytException(problem, response); - } - - var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") - { - return ((TResult)(object)responseText, null); - } - - var responseContent = JObject.Parse(responseText).ToObject(); - - if (responseContent is IHasLocation ihl) - { - if (response.Headers.Location != null) - ihl.Location = response.Headers.Location; - } - - return (responseContent, responseText); - } - - /// - /// - /// - /// - /// - /// - public bool TryGetCachedCertificate(string subject, out CachedCertificateResult value) - { - value = null; - if (_cache.CachedCerts.TryGetValue(subject, out var cache) == false) - { - 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 void ResetCachedCertificate(IEnumerable hostsToRemove) - { - foreach (var host in hostsToRemove) - { - _cache.CachedCerts.Remove(host); - } - } - } -} \ No newline at end of file diff --git a/v1.0/LetsEncrypt/ACMEv2/LetsEncrytException.cs b/v1.0/LetsEncrypt/ACMEv2/LetsEncrytException.cs deleted file mode 100644 index ed8c440..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/LetsEncrytException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Net.Http; - -namespace ACMEv2 -{ - 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; } - } -} diff --git a/v1.0/LetsEncrypt/ACMEv2/Order.cs b/v1.0/LetsEncrypt/ACMEv2/Order.cs deleted file mode 100644 index 41e6070..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/Order.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Newtonsoft.Json; - - -namespace ACMEv2 -{ - public class Order : IHasLocation - { - public Uri Location { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("expires")] - public DateTime? Expires { get; set; } - - [JsonProperty("identifiers")] - public OrderIdentifier[] Identifiers { get; set; } - - [JsonProperty("notBefore")] - public DateTime? NotBefore { get; set; } - - [JsonProperty("notAfter")] - public DateTime? NotAfter { get; set; } - - [JsonProperty("error")] - public Problem Error { get; set; } - - [JsonProperty("authorizations")] - public Uri[] Authorizations { get; set; } - - [JsonProperty("finalize")] - public Uri Finalize { get; set; } - - [JsonProperty("certificate")] - public Uri Certificate { get; set; } - } - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/OrderIdentifier.cs b/v1.0/LetsEncrypt/ACMEv2/OrderIdentifier.cs deleted file mode 100644 index b0015fd..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/OrderIdentifier.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - - -namespace ACMEv2 -{ - public class OrderIdentifier - { - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("value")] - public string Value { get; set; } - - } - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/Problem.cs b/v1.0/LetsEncrypt/ACMEv2/Problem.cs deleted file mode 100644 index 512d2b7..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/Problem.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace ACMEv2 -{ - public class Problem - { - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("detail")] - public string Detail { get; set; } - - public string RawJson { get; set; } - } - -} diff --git a/v1.0/LetsEncrypt/ACMEv2/RegistrationCache.cs b/v1.0/LetsEncrypt/ACMEv2/RegistrationCache.cs deleted file mode 100644 index 4057ad8..0000000 --- a/v1.0/LetsEncrypt/ACMEv2/RegistrationCache.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace ACMEv2 -{ - public class RegistrationCache - { - public readonly Dictionary CachedCerts = new Dictionary(StringComparer.OrdinalIgnoreCase); - public byte[] AccountKey; - public string Id; - public Jwk Key; - public Uri Location; - } - - - -} diff --git a/v1.0/LetsEncrypt/LetsEncrypt.csproj b/v1.0/LetsEncrypt/LetsEncrypt.csproj deleted file mode 100644 index 493f066..0000000 --- a/v1.0/LetsEncrypt/LetsEncrypt.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - netcoreapp2.2 - - - - - - - - - PreserveNewest - - - - diff --git a/v1.0/LetsEncrypt/Library.cs b/v1.0/LetsEncrypt/Library.cs deleted file mode 100644 index 3ab6a94..0000000 --- a/v1.0/LetsEncrypt/Library.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; - - -namespace LetsEncrypt -{ - class Library - { - /// - /// 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 static 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 static 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 static 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 static 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/v1.0/LetsEncrypt/Program.cs b/v1.0/LetsEncrypt/Program.cs deleted file mode 100644 index 6c9b135..0000000 --- a/v1.0/LetsEncrypt/Program.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using ACMEv2; - -namespace LetsEncrypt -{ - class Program - { - private static readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory; - - - - static void Main(string[] args) - { - try - { - Console.WriteLine("Let's Encrypt C# .Net Core Client"); - - Settings settings = (new SettingsProvider(null)).settings; - - //loop all customers - foreach(Customer customer in settings.customers) { - try { - Console.WriteLine(string.Format("Managing customer: {0} - {1} {2}", customer.id, customer.name, customer.lastname)); - - //loop each customer website - foreach(Site site in customer.sites) { - Console.WriteLine(string.Format("Managing site: {0}", site.name)); - - try { - //define cache folder - string cache = Path.Combine(AppPath, "cache", customer.id); - if(!Directory.Exists(cache)) { - Directory.CreateDirectory(cache); - } - - LetsEncryptClient client = new LetsEncryptClient(settings.url, cache, site.name); - - //1. Client initialization - Console.WriteLine("1. Client Initialization..."); - client.Init(customer.contacts).Wait(); - Console.WriteLine(string.Format("Terms of service: {0}", client.GetTermsOfServiceUri())); - - //create folder for ssl - string ssl = Path.Combine(settings.ssl, site.name); - if(!Directory.Exists(ssl)) { - Directory.CreateDirectory(ssl); - } - - // 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 - CachedCertificateResult certRes = new CachedCertificateResult(); - if (client.TryGetCachedCertificate(site.name, out certRes)) - { - string cert = Path.Combine(ssl, site.name + ".crt"); - if(!File.Exists(cert)) - File.WriteAllText(cert, certRes.Certificate); - - string key = Path.Combine(ssl, site.name + ".key"); - if(!File.Exists(key)) { - using (StreamWriter writer = File.CreateText(key)) - Library.ExportPrivateKey(certRes.PrivateKey, writer); - } - - Console.WriteLine("Certificate and Key exists and valid."); - } - else { - //new nonce - client.NewNonce().Wait(); - - //try to make new order - try - { - //create new orders - Console.WriteLine("2. Client New Order..."); - Task> orders = client.NewOrder(site.hosts, site.challenge); - orders.Wait(); - - switch(site.challenge) { - case "http-01": { - //ensure to enable static file discovery on server in .well-known/acme-challenge - //and listen on 80 port - - //check acme directory - string acme = Path.Combine(settings.www, settings.acme); - if(!Directory.Exists(acme)) { - throw new DirectoryNotFoundException(string.Format("Directory {0} wasn't created", acme)); - } - - foreach (FileInfo file in new DirectoryInfo(acme).GetFiles()) - file.Delete(); - - foreach (var result in orders.Result) - { - Console.WriteLine("Key: " + result.Key + Environment.NewLine + "Value: " + result.Value); - string[] splitToken = result.Value.Split('~'); - - string token = Path.Combine(acme, splitToken[0]); - File.WriteAllText(token, splitToken[1]); - } - - break; - } - - case "dns-01": { - //Manage DNS server MX record, depends from provider - - break; - } - - default: { - - break; - } - } - - //complete challanges - Console.WriteLine("3. Client Complete Challange..."); - client.CompleteChallenges().Wait(); - Console.WriteLine("Challanges comleted."); - } - catch (Exception ex) - { - Console.WriteLine(ex.Message.ToString()); - client.GetOrder(site.hosts).Wait(); - } - - - // Download new certificate - Console.WriteLine("4. Download certificate..."); - client.GetCertificate(site.name).Wait(); - - // Write to filesystem - certRes = new CachedCertificateResult(); - if (client.TryGetCachedCertificate(site.name, out certRes)) { - string cert = Path.Combine(ssl, site.name + ".crt"); - File.WriteAllText(cert, certRes.Certificate); - - string key = Path.Combine(ssl, site.name + ".key"); - using (StreamWriter writer = File.CreateText(key)) - Library.ExportPrivateKey(certRes.PrivateKey, writer); - - Console.WriteLine("Certificate saved."); - } - else { - Console.WriteLine("Unable to get new cached certificate."); - } - - - } - } - catch (Exception ex) { - Console.WriteLine(ex.Message.ToString()); - } - } - } - catch (Exception ex) { - Console.WriteLine(ex.Message.ToString()); - } - } - } - catch (Exception ex) { - Console.WriteLine(ex.Message.ToString()); - } - } - } -} diff --git a/v1.0/LetsEncrypt/README.md b/v1.0/LetsEncrypt/README.md deleted file mode 100644 index ac48803..0000000 --- a/v1.0/LetsEncrypt/README.md +++ /dev/null @@ -1,83 +0,0 @@ -#ACMEv2 Client library - -https://tools.ietf.org/html/draft-ietf-acme-acme-18 - -The following diagram illustrates the relations between resources on -an ACME server. For the most part, these relations are expressed by -URLs provided as strings in the resources' JSON representations. -Lines with labels in quotes indicate HTTP link relations. - - directory - | - +--> new-nonce - | - +----------+----------+-----+-----+------------+ - | | | | | - | | | | | - V V V V V - newAccount newAuthz newOrder revokeCert keyChange - | | | - | | | - V | V - account | order -----> cert - | | - | | - | V - +------> authz - | ^ - | | "up" - V | - challenge - - - - +-------------------+--------------------------------+--------------+ - | Action | Request | Response | - +-------------------+--------------------------------+--------------+ - | Get directory | GET directory | 200 | - | | | | - | Get nonce | HEAD newNonce | 200 | - | | | | - | Create account | POST newAccount | 201 -> | - | | | account | - | | | | - | Submit order | POST newOrder | 201 -> order | - | | | | - | Fetch challenges | POST-as-GET order's | 200 | - | | authorization urls | | - | | | | - | Respond to | POST authorization challenge | 200 | - | challenges | urls | | - | | | | - | Poll for status | POST-as-GET order | 200 | - | | | | - | Finalize order | POST order's finalize url | 200 | - | | | | - | Poll for status | POST-as-GET order | 200 | - | | | | - | Download | POST-as-GET order's | 200 | - | certificate | certificate url | | - +-------------------+--------------------------------+--------------+ - - - - - - - pending - | - | Receive - | response - V - processing <-+ - | | | Server retry or - | | | client retry request - | +----+ - | - | - Successful | Failed - validation | validation - +---------+---------+ - | | - V V - valid invalid diff --git a/v1.0/LetsEncrypt/SettingsProvider.cs b/v1.0/LetsEncrypt/SettingsProvider.cs deleted file mode 100644 index dfd6f97..0000000 --- a/v1.0/LetsEncrypt/SettingsProvider.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json; - -namespace LetsEncrypt -{ - - - public class SettingsProvider - { - private readonly string _path; - public Settings settings; - public SettingsProvider(string path) { - _path = path ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json"); - - if(!File.Exists(_path)) - throw new FileNotFoundException(string.Format("Settings file \"{0}\" not found."), _path); - - settings = JsonConvert.DeserializeObject(File.ReadAllText(_path)); - } - } - - public class Settings { - public string url { get; set; } - public string www { get; set; } - public string acme { get; set; } - public string ssl { get; set; } - - public Customer [] customers { get; set;} - } - - public class Customer { - public string id { get; set; } - public string [] contacts { get; set; } - public string name { get; set; } - public string lastname { get; set; } - public Site [] sites { get; set; } - } - - public class Site { - public string root { get; set; } - public string name { get; set; } - public string [] hosts { get; set; } - public string challenge { get; set; } - } - - - -} \ No newline at end of file diff --git a/v1.0/LetsEncrypt/settings.json b/v1.0/LetsEncrypt/settings.json deleted file mode 100644 index b1d8fa4..0000000 --- a/v1.0/LetsEncrypt/settings.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "_StagingV2": "https://acme-staging-v02.api.letsencrypt.org/directory", - "_ProductionV2": "https://acme-v02.api.letsencrypt.org/directory", - - "url": "https://acme-staging-v02.api.letsencrypt.org/directory", - - "www": "/var/www", - "acme": ".well-known/acme-challenge", - "ssl": "/etc/nginx/ssl", - - "customers": [ - { - "id": "9b4c8584-dc83-4388-b45f-2942e34dca9d", - "contacts": [ "maksym.sadovnychyy@gmail.com" ], - "name": "Maksym", - "lastname": "Sadovnychyy", - - "sites": [ - { - "name": "maks-it.com", - "hosts": [ - "maks-it.com", - "www.maks-it.com", - "it.maks-it.com", - "www.it.maks-it.com", - "ru.maks-it.com", - "www.ru.maks-it.com", - "api.maks-it.com", - "www.api.maks-it.com" - ], - "challenge": "http-01" - } - ] - }, - { - "id": "d6be989c-3b68-480d-9f4f-b7317674847a", - "contacts": [ "anastasiia.pavlovskaia@gmail.com" ], - - "name": "Anastasiia", - "lastname": "Pavlovskaia", - - "sites": [ - { - - "name": "nastyarey.com", - "hosts": [ - "nastyarey.com", - "www.nastyarey.com", - "it.nastyarey.com", - "www.it.nastyarey.com", - "ru.nastyarey.com", - "www.ru.nastyarey.com" - ], - "challenge": "http-01" - } - ] - } - ] -} diff --git a/v2.0/.vscode/launch.json b/v2.0/.vscode/launch.json deleted file mode 100644 index 0714eae..0000000 --- a/v2.0/.vscode/launch.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceFolder}/LetsEncrypt/bin/Debug/netcoreapp3.1/LetsEncrypt.dll", - "args": [], - "cwd": "${workspaceFolder}/LetsEncrypt", - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" - } - ] -} \ No newline at end of file diff --git a/v2.0/.vscode/tasks.json b/v2.0/.vscode/tasks.json deleted file mode 100644 index c231259..0000000 --- a/v2.0/.vscode/tasks.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/LetsEncrypt/LetsEncrypt.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/LetsEncrypt/LetsEncrypt.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "${workspaceFolder}/LetsEncrypt/LetsEncrypt.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - } - ] -} \ No newline at end of file diff --git a/v2.0/LetsEncrypt.sln b/v2.0/LetsEncrypt.sln deleted file mode 100644 index bffcf60..0000000 --- a/v2.0/LetsEncrypt.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.572 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt/LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351} - EndGlobalSection -EndGlobal diff --git a/v2.0/LetsEncrypt/.vscode/tasks.json b/v2.0/LetsEncrypt/.vscode/tasks.json deleted file mode 100644 index 723a349..0000000 --- a/v2.0/LetsEncrypt/.vscode/tasks.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/LetsEncrypt.csproj" - ], - "problemMatcher": "$tsc" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/LetsEncrypt.csproj" - ], - "problemMatcher": "$tsc" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "${workspaceFolder}/LetsEncrypt.csproj" - ], - "problemMatcher": "$tsc" - } - ] -} \ No newline at end of file diff --git a/v2.0/LetsEncrypt/App.cs b/v2.0/LetsEncrypt/App.cs deleted file mode 100644 index 30cdf27..0000000 --- a/v2.0/LetsEncrypt/App.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System; -using System.IO; -using System.Linq; - -using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; - - - -using Microsoft.Extensions.Options; - - - -using LetsEncrypt.Services; -using LetsEncrypt.Helpers; -using LetsEncrypt.Entities; - -namespace LetsEncrypt -{ - public class App { - - private readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory; - - private readonly AppSettings _appSettings; - private readonly ILetsEncryptService _letsEncryptService; - private readonly IKeyService _keyService; - private readonly ITerminalService _terminalService; - - public App(IOptions appSettings, ILetsEncryptService letsEncryptService, IKeyService keyService, ITerminalService terminalService) { - _appSettings = appSettings.Value; - _letsEncryptService = letsEncryptService; - _keyService = keyService; - _terminalService = terminalService; - } - - public void Run() { - foreach(var env in _appSettings.environments.Where(env => env.active)) { - try { - Console.WriteLine(string.Format("Let's Encrypt C# .Net Core Client, environment: {0}", env.name)); - - //loop all customers - foreach(Customer customer in _appSettings.customers) { - try { - Console.WriteLine(string.Format("Managing customer: {0} - {1} {2}", customer.id, customer.name, customer.lastname)); - - //loop each customer website - foreach(Site site in customer.sites.Where(s => s.active)) { - Console.WriteLine(string.Format("Managing site: {0}", site.name)); - - try { - //define cache folder - string cache = Path.Combine(AppPath, env.cache, customer.id); - if(!Directory.Exists(cache)) { - Directory.CreateDirectory(cache); - } - - //1. Client initialization - Console.WriteLine("1. Client Initialization..."); - _letsEncryptService.Init(env.url, cache, site.name, customer.contacts).Wait(); - - - Console.WriteLine(string.Format("Terms of service: {0}", _letsEncryptService.GetTermsOfServiceUri())); - - //create folder for ssl - string ssl = Path.Combine(env.ssl, site.name); - if(!Directory.Exists(ssl)) { - Directory.CreateDirectory(ssl); - } - - // 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 - CachedCertificateResult certRes = new CachedCertificateResult(); - if (_letsEncryptService.TryGetCachedCertificate(site.name, out certRes)) { - string cert = Path.Combine(ssl, site.name + ".crt"); - //if(!File.Exists(cert)) - File.WriteAllText(cert, certRes.Certificate); - - string key = Path.Combine(ssl, site.name + ".key"); - //if(!File.Exists(key)) { - using (StreamWriter writer = File.CreateText(key)) - _keyService.ExportPrivateKey(certRes.PrivateKey, writer); - //} - - Console.WriteLine("Certificate and Key exists and valid. Restored from cache."); - } - else { - //new nonce - _letsEncryptService.NewNonce().Wait(); - - //try to make new order - try { - //create new orders - Console.WriteLine("2. Client New Order..."); - Task> orders = _letsEncryptService.NewOrder(site.hosts, site.challenge); - orders.Wait(); - - switch(site.challenge) { - case "http-01": { - //ensure to enable static file discovery on server in .well-known/acme-challenge - //and listen on 80 port - - //check acme directory - string acme = Path.Combine(env.www, env.acme); - if(!Directory.Exists(acme)) { - throw new DirectoryNotFoundException(string.Format("Directory {0} wasn't created", acme)); - } - - foreach (FileInfo file in new DirectoryInfo(acme).GetFiles()) { - if(file.LastWriteTimeUtc < DateTime.UtcNow.AddMonths(-3)) - file.Delete(); - } - - - foreach (var result in orders.Result) - { - Console.WriteLine("Key: " + result.Key + System.Environment.NewLine + "Value: " + result.Value); - string[] splitToken = result.Value.Split('~'); - - string token = Path.Combine(acme, splitToken[0]); - File.WriteAllText(token, splitToken[1]); - } - - _terminalService.Exec("chgrp -R nginx /var/www"); - _terminalService.Exec("chmod -R g+rwx /var/www"); - - break; - } - - case "dns-01": { - //Manage DNS server MX record, depends from provider - - break; - } - - default: { - - break; - } - } - - //complete challanges - Console.WriteLine("3. Client Complete Challange..."); - _letsEncryptService.CompleteChallenges().Wait(); - Console.WriteLine("Challanges comleted."); - } - catch (Exception ex) { - Console.WriteLine(ex.Message.ToString()); - _letsEncryptService.GetOrder(site.hosts).Wait(); - } - - - // Download new certificate - Console.WriteLine("4. Download certificate..."); - _letsEncryptService.GetCertificate(site.name).Wait(); - - // Write to filesystem - certRes = new CachedCertificateResult(); - if (_letsEncryptService.TryGetCachedCertificate(site.name, out certRes)) { - string cert = Path.Combine(ssl, site.name + ".crt"); - File.WriteAllText(cert, certRes.Certificate); - - string key = Path.Combine(ssl, site.name + ".key"); - using (StreamWriter writer = File.CreateText(key)) - _keyService.ExportPrivateKey(certRes.PrivateKey, writer); - - Console.WriteLine("Certificate saved."); - } - else { - Console.WriteLine("Unable to get new cached certificate."); - } - - - } - - - } - catch (Exception ex) { - Console.WriteLine(ex.Message.ToString()); - } - } - } - catch (Exception ex) { - Console.WriteLine(ex.Message.ToString()); - } - } - - if(env.name == "ProductionV2") { - _terminalService.Exec("systemctl restart nginx"); - } - } - catch (Exception ex) { - Console.WriteLine(ex.Message.ToString()); - break; - } - } - } - } -} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/Account.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/Account.cs deleted file mode 100644 index 5e785bd..0000000 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/Account.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.3 - */ - -using System; -using Newtonsoft.Json; -using LetsEncrypt.Exceptions; - -namespace LetsEncrypt.Entities -{ - interface IHasLocation - { - Uri Location { get; set; } - } - - public class Account : IHasLocation - { - [JsonProperty("termsOfServiceAgreed")] - public bool TermsOfServiceAgreed { 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 - */ - [JsonProperty("onlyReturnExisting")] - public bool OnlyReturnExisting { get; set; } - - [JsonProperty("contact")] - public string[] Contacts { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("createdAt")] - public DateTime CreatedAt { get; set; } - - [JsonProperty("key")] - public Jwk Key { get; set; } - - [JsonProperty("initialIp")] - public string InitialIp { get; set; } - - [JsonProperty("orders")] - public Uri Orders { get; set; } - - public Uri Location { get; set; } - } - - - public class Order : IHasLocation - { - public Uri Location { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("expires")] - public DateTime? Expires { get; set; } - - [JsonProperty("identifiers")] - public OrderIdentifier[] Identifiers { get; set; } - - [JsonProperty("notBefore")] - public DateTime? NotBefore { get; set; } - - [JsonProperty("notAfter")] - public DateTime? NotAfter { get; set; } - - [JsonProperty("error")] - public Problem Error { get; set; } - - [JsonProperty("authorizations")] - public Uri[] Authorizations { get; set; } - - [JsonProperty("finalize")] - public Uri Finalize { get; set; } - - [JsonProperty("certificate")] - public Uri Certificate { get; set; } - } - - -} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/AcmeDirectory.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/AcmeDirectory.cs deleted file mode 100644 index ae70c7c..0000000 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/AcmeDirectory.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace LetsEncrypt.Entities -{ - public class AcmeDirectory - { - //New nonce - [JsonProperty("newNonce")] - public Uri NewNonce { get; set; } - - //New account - [JsonProperty("newAccount")] - public Uri NewAccount { get; set; } - - //New order - [JsonProperty("newOrder")] - 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; } - - //Revoke certificate - [JsonProperty("revokeCert")] - public Uri RevokeCertificate { get; set; } - - //Key change - [JsonProperty("keyChange")] - public Uri KeyChange { get; set; } - - //Metadata object - [JsonProperty("meta")] - public AcmeDirectoryMeta Meta { get; set; } - } - - public class AcmeDirectoryMeta - { - [JsonProperty("termsOfService")] - public string TermsOfService { get; set; } - } -} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs deleted file mode 100644 index 98dcb1a..0000000 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace LetsEncrypt.Entities -{ - public class AuthorizationChallenge - { - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("token")] - public string Token { get; set; } - - } - -} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallengeResponse.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallengeResponse.cs deleted file mode 100644 index b17fb8d..0000000 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallengeResponse.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace LetsEncrypt.Entities -{ - public class AuthorizationChallengeResponse - { - [JsonProperty("identifier")] - public OrderIdentifier Identifier { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("expires")] - public DateTime? Expires { get; set; } - - [JsonProperty("wildcard")] - public bool Wildcard { get; set; } - - [JsonProperty("challenges")] - public AuthorizationChallenge[] Challenges { get; set; } - } - - public class AuthorizeChallenge - { - [JsonProperty("keyAuthorization")] - public string KeyAuthorization { get; set; } - - } - - -} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs deleted file mode 100644 index 0b4595a..0000000 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Security.Cryptography; - -namespace LetsEncrypt.Entities -{ - public class CachedCertificateResult - { - public RSACryptoServiceProvider PrivateKey; - public string Certificate; - } - -} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/CertificateCache.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/CertificateCache.cs deleted file mode 100644 index 29e5008..0000000 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/CertificateCache.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace LetsEncrypt.Entities -{ - public class CertificateCache - { - public string Cert; - public byte[] Private; - } - - - -} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/FinalizeRequest.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/FinalizeRequest.cs deleted file mode 100644 index 01f4c69..0000000 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/FinalizeRequest.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json; - -namespace LetsEncrypt.Entities -{ - public class FinalizeRequest - { - [JsonProperty("csr")] - public string CSR { get; set; } - } - -} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/JwsMessage.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/JwsMessage.cs deleted file mode 100644 index 668eae6..0000000 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/JwsMessage.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Newtonsoft.Json; - - -namespace LetsEncrypt.Entities -{ - - public class JwsMessage - { - //[JsonProperty("header")] - //public JwsHeader Header { get; set; } - - [JsonProperty("protected")] - public string Protected { get; set; } - - [JsonProperty("payload")] - public string Payload { get; set; } - - [JsonProperty("signature")] - public string Signature { get; set; } - } - - - public class JwsHeader - { - //public JwsHeader() - //{ - //} - - //public JwsHeader(string algorithm, Jwk key) - //{ - // Algorithm = algorithm; - // Key = key; - //} - - [JsonProperty("alg")] - public string Algorithm { get; set; } - - [JsonProperty("jwk")] - public Jwk Key { get; set; } - - - [JsonProperty("kid")] - public string KeyId { get; set; } - - - [JsonProperty("nonce")] - public string Nonce { get; set; } - - - [JsonProperty("url")] - public Uri Url { get; set; } - - - [JsonProperty("Host")] - public string Host { get; set; } - } - - - -} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/OrderIdentifier.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/OrderIdentifier.cs deleted file mode 100644 index a8efd8b..0000000 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/OrderIdentifier.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - - -namespace LetsEncrypt.Entities -{ - public class OrderIdentifier - { - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("value")] - public string Value { get; set; } - - } - -} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs deleted file mode 100644 index 409a6cc..0000000 --- a/v2.0/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace LetsEncrypt.Entities -{ - public class RegistrationCache - { - public readonly Dictionary CachedCerts = new Dictionary(StringComparer.OrdinalIgnoreCase); - public byte[] AccountKey; - public string Id; - public Jwk Key; - public Uri Location; - } - - - -} diff --git a/v2.0/LetsEncrypt/Exceptions/LetsEncrytException.cs b/v2.0/LetsEncrypt/Exceptions/LetsEncrytException.cs deleted file mode 100644 index e55599d..0000000 --- a/v2.0/LetsEncrypt/Exceptions/LetsEncrytException.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Net.Http; - -using Newtonsoft.Json; - -namespace 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; } - } - - - public class Problem - { - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("detail")] - public string Detail { get; set; } - - public string RawJson { get; set; } - } -} diff --git a/v2.0/LetsEncrypt/Helpers/AppSettings.cs b/v2.0/LetsEncrypt/Helpers/AppSettings.cs deleted file mode 100644 index 7a03167..0000000 --- a/v2.0/LetsEncrypt/Helpers/AppSettings.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace LetsEncrypt.Helpers -{ - public class AppSettings { - public Environment [] environments { get; set; } - public Customer [] customers { get; set;} - } - - public class Environment { - public bool active { get; set; } - public string name { get; set; } - public string url { get; set; } - public string cache { get; set; } - public string www { get; set; } - public string acme { get; set; } - public string ssl { get; set; } - } - - public class Customer { - public string id { get; set; } - public string [] contacts { get; set; } - public string name { get; set; } - public string lastname { get; set; } - public Site [] sites { get; set; } - } - - public class Site { - public bool active { get; set; } - public string name { get; set; } - public string [] hosts { get; set; } - public string challenge { get; set; } - } -} diff --git a/v2.0/LetsEncrypt/LetsEncrypt.csproj b/v2.0/LetsEncrypt/LetsEncrypt.csproj deleted file mode 100644 index fe32ad2..0000000 --- a/v2.0/LetsEncrypt/LetsEncrypt.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - Exe - net5.0 - - - - - - - - - - - - - - - diff --git a/v2.0/LetsEncrypt/Program.cs b/v2.0/LetsEncrypt/Program.cs deleted file mode 100644 index ebb0616..0000000 --- a/v2.0/LetsEncrypt/Program.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; - -using Microsoft.Extensions.Configuration; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -using System.IO; - -using LetsEncrypt.Helpers; -using LetsEncrypt.Services; - -namespace LetsEncrypt -{ - class Program - { - public IConfiguration Configuration { get; } - - static void Main(string[] args) { - // create service collection - var services = new ServiceCollection(); - ConfigureServices(services); - - // create service provider - var serviceProvider = services.BuildServiceProvider(); - - // entry to run app - serviceProvider.GetService().Run(); - } - - public static void ConfigureServices(IServiceCollection services) { - // build configuration - IConfiguration Configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", false) - .Build(); - - - // configure strongly typed settings objects - var appSettingsSection = Configuration.GetSection("AppSettings"); - services.Configure(appSettingsSection); - - // Dependency Injection - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - // add app - services.AddTransient(); - } - } - - -} diff --git a/v2.0/LetsEncrypt/Services/JwsService.cs b/v2.0/LetsEncrypt/Services/JwsService.cs deleted file mode 100644 index b7d23e6..0000000 --- a/v2.0/LetsEncrypt/Services/JwsService.cs +++ /dev/null @@ -1,136 +0,0 @@ -/** -* https://tools.ietf.org/html/rfc4648 -* https://tools.ietf.org/html/rfc4648#section-5 -*/ - -using System; -using System.Security.Cryptography; -using System.Text; -using Newtonsoft.Json; - -using LetsEncrypt.Entities; - -namespace LetsEncrypt.Services -{ - public interface IJwsService { - void Init(RSA rsa, string keyId); - JwsMessage Encode(TPayload payload, JwsHeader protectedHeader); - string GetKeyAuthorization(string token); - string Base64UrlEncoded(byte[] arg); - - void SetKeyId(Account account); - } - - - public class JwsService : IJwsService { - - public Jwk _jwk; - private RSA _rsa; - - public JwsService() { - - } - - public void Init(RSA rsa, string keyId) { - _rsa = rsa ?? throw new ArgumentNullException(nameof(rsa)); - - var publicParameters = rsa.ExportParameters(false); - - _jwk = new Jwk () { - KeyType = "RSA", - Exponent = Base64UrlEncoded(publicParameters.Exponent), - Modulus = Base64UrlEncoded(publicParameters.Modulus), - KeyId = keyId - }; - } - - - - public JwsMessage Encode(TPayload 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(JsonConvert.SerializeObject(protectedHeader)) - }; - - if(payload != null) { - if(payload is String) { - string value = payload.ToString(); - switch(value) { - case "POST-as-GET": - message.Payload = string.Empty; - break; - - default: - message.Payload = Base64UrlEncoded(value); - break; - } - - - } else { - message.Payload = Base64UrlEncoded(JsonConvert.SerializeObject(payload)); - } - - } - - - message.Signature = Base64UrlEncoded( - _rsa.SignData(Encoding.ASCII.GetBytes(message.Protected + "." + message.Payload), - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1)); - - return message; - } - - private string GetSha256Thumbprint() - { - var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}"; - - using (var sha256 = SHA256.Create()) - { - return Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(json))); - } - } - - public string GetKeyAuthorization(string token) - { - return token + "." + GetSha256Thumbprint(); - } - - - - public string Base64UrlEncoded(string s) - { - return Base64UrlEncoded(Encoding.UTF8.GetBytes(s)); - } - - // https://tools.ietf.org/html/rfc4648#section-5 - public string Base64UrlEncoded(byte[] arg) - { - var s = Convert.ToBase64String(arg); // Regular base64 encoder - s = s.Split('=')[0]; // Remove any trailing '='s - s = s.Replace('+', '-'); // 62nd char of encoding - s = s.Replace('/', '_'); // 63rd char of encoding - return s; - } - - public void SetKeyId(Account account) - { - _jwk.KeyId = account.Id; - } - - - } -} diff --git a/v2.0/LetsEncrypt/Services/KeyService.cs b/v2.0/LetsEncrypt/Services/KeyService.cs deleted file mode 100644 index 3f6bc1c..0000000 --- a/v2.0/LetsEncrypt/Services/KeyService.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; - -namespace LetsEncrypt.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]); - } - } - } - } -} \ No newline at end of file diff --git a/v2.0/LetsEncrypt/Services/LetsEncryptService.cs b/v2.0/LetsEncrypt/Services/LetsEncryptService.cs deleted file mode 100644 index 979f674..0000000 --- a/v2.0/LetsEncrypt/Services/LetsEncryptService.cs +++ /dev/null @@ -1,588 +0,0 @@ -/** -* https://community.letsencrypt.org/t/trying-to-do-post-as-get-but-getting-post-jws-not-signed/108371 -* https://tools.ietf.org/html/rfc8555#section-6.2 -* -*/ - -using System; - -using System.Threading; -using System.Threading.Tasks; - -using System.Collections.Generic; - -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; - -using System.Net.Http; - -using System.IO; -using System.Text; -using System.Linq; - -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -using LetsEncrypt.Entities; -using LetsEncrypt.Exceptions; - - - -namespace LetsEncrypt.Services { - - public interface ILetsEncryptService { - Task Init(string url, string home, string siteName, string[] contacts, CancellationToken token = default(CancellationToken)); - string GetTermsOfServiceUri(CancellationToken token = default(CancellationToken)); - bool TryGetCachedCertificate(string subject, out CachedCertificateResult value); - Task NewNonce(CancellationToken token = default(CancellationToken)); - Task> NewOrder(string[] hostnames, string challengeType, CancellationToken token = default(CancellationToken)); - Task CompleteChallenges(CancellationToken token = default(CancellationToken)); - Task GetOrder(string[] hostnames, CancellationToken token = default(CancellationToken)); - Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject, CancellationToken token = default(CancellationToken)); - } - - - - - public class LetsEncryptService: ILetsEncryptService { - - private readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory; - - private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings { - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.Indented - }; - - private readonly IJwsService _jwsService; - - - - private string _path; - private string _url; - private string _home; - private string _nonce; - - private RSACryptoServiceProvider _accountKey; - - private RegistrationCache _cache; - private HttpClient _client; - private AcmeDirectory _directory; - private List _challenges = new List(); - private Order _currentOrder; - - - public LetsEncryptService(IJwsService jwsService) { - _jwsService = jwsService; - } - - /// - /// Account creation or Initialization from cache - /// - /// - /// - /// - public async Task Init(string url, string home, string siteName, string[] contacts, CancellationToken token = default(CancellationToken)) { - // old Letsencrypt constructor - _url = url ?? throw new ArgumentNullException(nameof(url)); - var hash = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(siteName)); - - _home = home ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create); - - var file = _jwsService.Base64UrlEncoded(hash) + ".lets-encrypt.cache.json"; - _path = Path.Combine(_home, file); - - - // originally Init part was here - _accountKey = new RSACryptoServiceProvider(4096); - _client = GetCachedClient(_url); - - // 1 - Get directory - (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), null, token); - - if (File.Exists(_path)) - { - bool success; - try - { - lock (Locker) - { - _cache = JsonConvert.DeserializeObject(File.ReadAllText(_path)); - } - - _accountKey.ImportCspBlob(_cache.AccountKey); - //_jws = new Jws(_accountKey, _cache.Id); - success = true; - } - catch - { - success = false; - // if we failed for any reason, we'll just - // generate a new registration - } - - if (success) - { - return; - } - } - - await NewNonce(); - - //New Account request - _jwsService.Init(_accountKey, null); - - var letsEncryptOrder = new Account - { - // we validate this in the UI before we get here, so that is fine - TermsOfServiceAgreed = true, - Contacts = contacts.Select(contact => - string.Format("mailto:{0}", contact) - ).ToArray() - }; - - var (account, response) = await SendAsync(HttpMethod.Post, _directory.NewAccount, letsEncryptOrder, token); - _jwsService.SetKeyId(account); - - if (account.Status != "valid") - throw new InvalidOperationException("Account status is not valid, was: " + account.Status + Environment.NewLine + response); - - lock (Locker) - { - _cache = new RegistrationCache - { - Location = account.Location, - AccountKey = _accountKey.ExportCspBlob(true), - Id = account.Id, - Key = account.Key - }; - File.WriteAllText(_path, JsonConvert.SerializeObject(_cache, Formatting.Indented)); - } - } - - - /// - /// Just retrive terms of service - /// - /// - /// - public string GetTermsOfServiceUri(CancellationToken token = default(CancellationToken)) - { - return _directory.Meta.TermsOfService; - } - - /// - /// Request New Nonce to be able to start POST requests - /// - /// - /// - public async Task NewNonce(CancellationToken token = default(CancellationToken)) - { - var result = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce)).ConfigureAwait(false); - _nonce = result.Headers.GetValues("Replay-Nonce").First(); - } - - /// - /// 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, CancellationToken token = default(CancellationToken)) { - _challenges.Clear(); - - //update jws with account url - _jwsService.Init(_accountKey, _cache.Location.ToString()); - - 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, letsEncryptOrder, token); - - if (order.Status != "pending") - throw new InvalidOperationException("Created new order and expected status 'pending', but got: " + order.Status + Environment.NewLine + - response); - _currentOrder = order; - - var results = new Dictionary(); - foreach (var item in order.Authorizations) - { - var (challengeResponse, responseText) = await SendAsync(HttpMethod.Post, item, "POST-as-GET", token); - - if (challengeResponse.Status == "valid") - continue; - - if (challengeResponse.Status != "pending") - throw new InvalidOperationException("Expected autorization status 'pending', but got: " + order.Status + - Environment.NewLine + 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] = challenge.Token + "~" + keyToken; - break; - } - - } - - - - } - - return results; - } - - - - - public async Task CompleteChallenges(CancellationToken token = default(CancellationToken)) - { - _jwsService.Init(_accountKey, _cache.Location.ToString()); - - - - - 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, "{}", token); - - if (result.Status == "valid") - break; - if (result.Status != "pending") - throw new InvalidOperationException("Failed autorization of " + _currentOrder.Identifiers[index].Value + Environment.NewLine + responseText); - - - - await Task.Delay(1000); - } - - } - } - - - - - public async Task GetOrder(string[] hostnames, CancellationToken token = default(CancellationToken)) - { - //update jws - _jwsService.Init(_accountKey, _cache.Location.ToString()); - - 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, letsEncryptOrder, token); - - _currentOrder = order; - } - - - - - - /// - /// - /// - /// - /// - public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject, CancellationToken token = default(CancellationToken)) - { - 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()) - }; - - var (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, letsEncryptOrder, token); - - while (response.Status != "valid") - { - (response, responseText) = await SendAsync(HttpMethod.Post, response.Location, "POST-as-GET", token); - - if(response.Status == "processing") - { - await Task.Delay(500); - continue; - } - throw new InvalidOperationException("Invalid order status: " + response.Status + Environment.NewLine + - responseText); - } - var (pem, _) = await SendAsync(HttpMethod.Post, response.Certificate, "POST-as-GET", token); - - var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem)); - - _cache.CachedCerts[subject] = new CertificateCache - { - Cert = pem, - Private = key.ExportCspBlob(true) - }; - - lock (Locker) - { - File.WriteAllText(_path, - JsonConvert.SerializeObject(_cache, Formatting.Indented)); - } - - return (cert, key); - } - - - - - - - /// - /// - /// - /// - /// - public async Task KeyChange(CancellationToken token = default(CancellationToken)) { - - } - - - /// - /// - /// - /// - /// - public async Task RevokeCertificate(CancellationToken token = default(CancellationToken)) { - - } - - - - - - /// - /// Main method used to send data to LetsEncrypt - /// - /// - /// - /// - /// - /// - /// - private async Task<(TResult Result, string Response)> SendAsync(HttpMethod method, Uri uri, object message, CancellationToken token) where TResult : class - { - var request = new HttpRequestMessage(method, uri); - - if (message != null) - { - JwsMessage encodedMessage = _jwsService.Encode(message, new JwsHeader - { - Nonce = _nonce, - Url = uri, - }); - - var json = JsonConvert.SerializeObject(encodedMessage, jsonSettings); - - 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 _client.SendAsync(request, token).ConfigureAwait(false); - - 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().ConfigureAwait(false); - var problem = JsonConvert.DeserializeObject(problemJson); - problem.RawJson = problemJson; - throw new LetsEncrytException(problem, response); - } - - var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") - { - return ((TResult)(object)responseText, null); - } - - var responseContent = JObject.Parse(responseText).ToObject(); - - if (responseContent is IHasLocation ihl) - { - if (response.Headers.Location != null) - ihl.Location = response.Headers.Location; - } - - return (responseContent, responseText); - } - - - /// - /// - /// - /// - /// - /// - public bool TryGetCachedCertificate(string subject, out CachedCertificateResult value) - { - value = null; - if (_cache.CachedCerts.TryGetValue(subject, out var cache) == false) - { - 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 void ResetCachedCertificate(IEnumerable hostsToRemove) - { - foreach (var host in hostsToRemove) - { - _cache.CachedCerts.Remove(host); - } - } - - - - private Dictionary _cachedClients = new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// In our scenario, we assume a single single wizard progressing - /// and the locking is basic to the wizard progress. Adding explicit - /// locking to be sure that we are not corrupting disk state if user - /// is explicitly calling stuff concurrently (running the setup wizard - /// from two tabs?) - /// - private readonly object Locker = new object(); - private HttpClient GetCachedClient(string url) { - - if (_cachedClients.TryGetValue(url, out var value)) { - return value; - } - - lock (Locker) { - if (_cachedClients.TryGetValue(url, out value)) { - return value; - } - - value = new HttpClient { - BaseAddress = new Uri(url) - }; - - _cachedClients = new Dictionary(_cachedClients, StringComparer.OrdinalIgnoreCase) { - [url] = value - }; - return value; - } - } - - } - - -} \ No newline at end of file diff --git a/v2.0/LetsEncrypt/appsettings copy.json b/v2.0/LetsEncrypt/appsettings copy.json deleted file mode 100644 index 8d4fa58..0000000 --- a/v2.0/LetsEncrypt/appsettings copy.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "AppSettings": { - - "active": "StagingV2", - - "environments": [ - { - "name": "StagingV2", - "url": "https://acme-staging-v02.api.letsencrypt.org/directory", - - "www": "/var/www", - "acme": ".well-known/acme-challenge", - "ssl": "/home/maksym/source/temp" - }, - { - "name": "ProductionV2", - "url": "https://acme-v02.api.letsencrypt.org/directory", - - "www": "/var/www", - "acme": ".well-known/acme-challenge", - "ssl": "/etc/nginx/ssl" - } - ], - - "customers": [ - { - "id": "9b4c8584-dc83-4388-b45f-2942e34dca9d", - "contacts": [ "maksym.sadovnychyy@gmail.com" ], - "name": "Maksym", - "lastname": "Sadovnychyy", - - "sites": [ - { - "name": "maks-it.com", - "hosts": [ - "maks-it.com", - "www.maks-it.com", - "it.maks-it.com", - "www.it.maks-it.com", - "ru.maks-it.com", - "www.ru.maks-it.com", - "api.maks-it.com", - "www.api.maks-it.com" - ], - "challenge": "http-01" - } - ] - }, - { - "id": "d6be989c-3b68-480d-9f4f-b7317674847a", - "contacts": [ "anastasiia.pavlovskaia@gmail.com" ], - - "name": "Anastasiia", - "lastname": "Pavlovskaia", - - "sites": [ - { - - "name": "nastyarey.com", - "hosts": [ - "nastyarey.com", - "www.nastyarey.com", - "it.nastyarey.com", - "www.it.nastyarey.com", - "ru.nastyarey.com", - "www.ru.nastyarey.com" - ], - "challenge": "http-01" - } - ] - } - ] - } -} diff --git a/v2.0/LetsEncrypt/appsettings.json b/v2.0/LetsEncrypt/appsettings.json deleted file mode 100644 index 076e377..0000000 --- a/v2.0/LetsEncrypt/appsettings.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "AppSettings": { - "environments": [ - { - "active": true, - "name": "StagingV2", - "url": "https://acme-staging-v02.api.letsencrypt.org/directory", - - "cache": "staging_cache", - "www": "/var/www", - "acme": ".well-known/acme-challenge", - "ssl": "/home/maksym/source/temp" - }, - { - "active": true, - "name": "ProductionV2", - "url": "https://acme-v02.api.letsencrypt.org/directory", - - "cache": "production_cache", - "www": "/var/www", - "acme": ".well-known/acme-challenge", - "ssl": "/etc/nginx/ssl/" - } - ], - - "customers": [ - { - "id": "9b4c8584-dc83-4388-b45f-2942e34dca9d", - "contacts": [ "maksym.sadovnychyy@gmail.com" ], - "name": "Maksym", - "lastname": "Sadovnychyy", - - "sites": [ - { - "active": true, - "name": "maks-it.com", - "hosts": [ - "maks-it.com", - "www.maks-it.com", - "it.maks-it.com", - "www.it.maks-it.com", - "ru.maks-it.com", - "www.ru.maks-it.com", - "api.maks-it.com", - "www.api.maks-it.com", - - "git.maks-it.com", - - "demo.maks-it.com" - ], - "challenge": "http-01" - } - ] - }, - { - "id": "46337ef5-d69b-4332-b6ef-67959dfb3c2c", - "contacts": [ "maksym.sadovnychyy@gmail.com" ], - "name": "Anastasiia", - "lastname": "Pavlovskaia", - - "sites": [ - { - "active": true, - "name": "nastyarey.com", - "hosts": [ - "nastyarey.com", - "www.nastyarey.com", - "api.nastyarey.com", - "www.api.nastyarey.com" - ], - "challenge": "http-01" - } - ] - }, - { - "id": "341ebe34-e2b3-4645-9f54-aa4fe8eb0250", - "contacts": [ "maksym.sadovnychyy@gmail.com" ], - "name": "Antonio", - "lastname": "Di Franco", - - "sites": [ - { - "active": false, - "name": "aerusitalia.it", - "hosts": [ - "aerusitalia.it", - "www.aerusitalia.it", - "api.aerusitalia.it", - "www.api.aerusitalia.it" - ], - "challenge": "http-01" - } - ] - } - ], - - "_customers": [ - - ] - } -}