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": [
-
- ]
- }
-}