diff --git a/LetsEncrypt.sln b/LetsEncrypt.sln
new file mode 100644
index 0000000..bffcf60
--- /dev/null
+++ b/LetsEncrypt.sln
@@ -0,0 +1,25 @@
+
+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/LetsEncrypt/.vscode/launch.json b/LetsEncrypt/.vscode/launch.json
new file mode 100644
index 0000000..125ba92
--- /dev/null
+++ b/LetsEncrypt/.vscode/launch.json
@@ -0,0 +1,27 @@
+{
+ // 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/LetsEncrypt/.vscode/tasks.json b/LetsEncrypt/.vscode/tasks.json
new file mode 100644
index 0000000..723a349
--- /dev/null
+++ b/LetsEncrypt/.vscode/tasks.json
@@ -0,0 +1,36 @@
+{
+ "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/LetsEncrypt/ACMEv2/Account.cs b/LetsEncrypt/ACMEv2/Account.cs
new file mode 100644
index 0000000..dc9fe45
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/Account.cs
@@ -0,0 +1,58 @@
+/*
+ * https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.3
+ */
+
+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 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/LetsEncrypt/ACMEv2/AuthorizationChallange.cs b/LetsEncrypt/ACMEv2/AuthorizationChallange.cs
new file mode 100644
index 0000000..a1eac08
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/AuthorizationChallange.cs
@@ -0,0 +1,32 @@
+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 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/LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs b/LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs
new file mode 100644
index 0000000..43f9726
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs
@@ -0,0 +1,35 @@
+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 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/LetsEncrypt/ACMEv2/AuthorizeChallenge.cs b/LetsEncrypt/ACMEv2/AuthorizeChallenge.cs
new file mode 100644
index 0000000..54525e0
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/AuthorizeChallenge.cs
@@ -0,0 +1,27 @@
+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 AuthorizeChallenge
+ {
+ [JsonProperty("keyAuthorization")]
+ public string KeyAuthorization { get; set; }
+
+ }
+
+
+
+
+}
diff --git a/LetsEncrypt/ACMEv2/CachedCertificateResult.cs b/LetsEncrypt/ACMEv2/CachedCertificateResult.cs
new file mode 100644
index 0000000..9895663
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/CachedCertificateResult.cs
@@ -0,0 +1,22 @@
+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 CachedCertificateResult
+ {
+ public RSA PrivateKey;
+ public string Certificate;
+ }
+
+}
diff --git a/LetsEncrypt/ACMEv2/CertificateCache.cs b/LetsEncrypt/ACMEv2/CertificateCache.cs
new file mode 100644
index 0000000..ebadc42
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/CertificateCache.cs
@@ -0,0 +1,24 @@
+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 CertificateCache
+ {
+ public string Cert;
+ public byte[] Private;
+ }
+
+
+
+}
diff --git a/LetsEncrypt/ACMEv2/Directory.cs b/LetsEncrypt/ACMEv2/Directory.cs
new file mode 100644
index 0000000..c6863bc
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/Directory.cs
@@ -0,0 +1,37 @@
+using System;
+using Newtonsoft.Json;
+
+namespace ACMEv2
+{
+ public class Directory
+ {
+ //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 DirectoryMeta Meta { get; set; }
+ }
+}
diff --git a/LetsEncrypt/ACMEv2/DirectoryMeta.cs b/LetsEncrypt/ACMEv2/DirectoryMeta.cs
new file mode 100644
index 0000000..0260dd7
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/DirectoryMeta.cs
@@ -0,0 +1,23 @@
+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 DirectoryMeta
+ {
+ [JsonProperty("termsOfService")]
+ public string TermsOfService { get; set; }
+ }
+
+
+}
diff --git a/LetsEncrypt/ACMEv2/FinalizeRequest.cs b/LetsEncrypt/ACMEv2/FinalizeRequest.cs
new file mode 100644
index 0000000..4cc0f80
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/FinalizeRequest.cs
@@ -0,0 +1,22 @@
+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 FinalizeRequest
+ {
+ [JsonProperty("csr")]
+ public string CSR { get; set; }
+ }
+
+}
diff --git a/LetsEncrypt/ACMEv2/IHashLocation.cs b/LetsEncrypt/ACMEv2/IHashLocation.cs
new file mode 100644
index 0000000..a7fee22
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/IHashLocation.cs
@@ -0,0 +1,20 @@
+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
+{
+ interface IHasLocation
+ {
+ Uri Location { get; set; }
+ }
+
+}
diff --git a/LetsEncrypt/ACMEv2/Jwk.cs b/LetsEncrypt/ACMEv2/Jwk.cs
new file mode 100644
index 0000000..f618059
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/Jwk.cs
@@ -0,0 +1,98 @@
+/*
+ * JSON Web Key (JWK)
+ * https://tools.ietf.org/html/rfc7517
+ * https://www.gnupg.org/documentation/manuals/gcrypt-devel/RSA-key-parameters.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; }
+
+ ///
+ /// RSA public modulus n.
+ ///
+ [JsonProperty("n")]
+ public string Modulus { get; set; }
+
+ ///
+ /// RSA public exponent e.
+ ///
+ [JsonProperty("e")]
+ public string Exponent { get; set; }
+
+ ///
+ /// RSA secret exponent d = e^-1 \bmod (p-1)(q-1).
+ ///
+ [JsonProperty("d")]
+ public string D { get; set; }
+
+ ///
+ /// RSA secret prime p.
+ ///
+ [JsonProperty("p")]
+ public string P { get; set; }
+
+ ///
+ /// RSA secret prime q with p < q.
+ ///
+ [JsonProperty("q")]
+ public string Q { get; set; }
+
+ [JsonProperty("dp")]
+ public string DP { get; set; }
+
+ [JsonProperty("dq")]
+ public string DQ { get; set; }
+
+ [JsonProperty("qi")]
+ public string InverseQ { 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/LetsEncrypt/ACMEv2/Jws.cs b/LetsEncrypt/ACMEv2/Jws.cs
new file mode 100644
index 0000000..8f6e365
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/Jws.cs
@@ -0,0 +1,89 @@
+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/LetsEncrypt/ACMEv2/JwsHeader.cs b/LetsEncrypt/ACMEv2/JwsHeader.cs
new file mode 100644
index 0000000..bfbd52e
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/JwsHeader.cs
@@ -0,0 +1,50 @@
+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 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/LetsEncrypt/ACMEv2/JwsMessage.cs b/LetsEncrypt/ACMEv2/JwsMessage.cs
new file mode 100644
index 0000000..0b854c6
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/JwsMessage.cs
@@ -0,0 +1,33 @@
+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 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/LetsEncrypt/ACMEv2/LetsEncryptClient.cs b/LetsEncrypt/ACMEv2/LetsEncryptClient.cs
new file mode 100644
index 0000000..21b978e
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/LetsEncryptClient.cs
@@ -0,0 +1,542 @@
+/*
+ * 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 Directory _directory;
+ private List _challenges = new List();
+ private Order _currentOrder;
+
+ ///
+ /// Let's encrypt client object
+ ///
+ ///
+ public LetsEncryptClient(string url, string home)
+ {
+ _url = url ?? throw new ArgumentNullException(nameof(url));
+ var hash = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(url));
+
+ _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(CancellationToken token = default(CancellationToken))
+ {
+ var key = new RSACryptoServiceProvider(4096);
+ var csr = new CertificateRequest("CN=" + _currentOrder.Identifiers[0].Value,
+ 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[_currentOrder.Identifiers[0].Value] = 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(List hosts, out CachedCertificateResult value)
+ {
+ value = null;
+ if (_cache.CachedCerts.TryGetValue(hosts[0], out var cache) == false)
+ {
+ return false;
+ }
+
+ var cert = new X509Certificate2(cache.Cert);
+
+ // if it is about to expire, we need to refresh
+ if ((cert.NotAfter - DateTime.UtcNow).TotalDays < 14)
+ 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/LetsEncrypt/ACMEv2/LetsEncrytException.cs b/LetsEncrypt/ACMEv2/LetsEncrytException.cs
new file mode 100644
index 0000000..30e7bfa
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/LetsEncrytException.cs
@@ -0,0 +1,29 @@
+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 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/LetsEncrypt/ACMEv2/Order.cs b/LetsEncrypt/ACMEv2/Order.cs
new file mode 100644
index 0000000..602b20e
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/Order.cs
@@ -0,0 +1,49 @@
+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 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/LetsEncrypt/ACMEv2/OrderIdentifier.cs b/LetsEncrypt/ACMEv2/OrderIdentifier.cs
new file mode 100644
index 0000000..4f4e037
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/OrderIdentifier.cs
@@ -0,0 +1,26 @@
+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 OrderIdentifier
+ {
+ [JsonProperty("type")]
+ public string Type { get; set; }
+
+ [JsonProperty("value")]
+ public string Value { get; set; }
+
+ }
+
+}
diff --git a/LetsEncrypt/ACMEv2/Problem.cs b/LetsEncrypt/ACMEv2/Problem.cs
new file mode 100644
index 0000000..cb3a64b
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/Problem.cs
@@ -0,0 +1,27 @@
+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 Problem
+ {
+ [JsonProperty("type")]
+ public string Type { get; set; }
+
+ [JsonProperty("detail")]
+ public string Detail { get; set; }
+
+ public string RawJson { get; set; }
+ }
+
+}
diff --git a/LetsEncrypt/ACMEv2/RegistrationCache.cs b/LetsEncrypt/ACMEv2/RegistrationCache.cs
new file mode 100644
index 0000000..cb5789e
--- /dev/null
+++ b/LetsEncrypt/ACMEv2/RegistrationCache.cs
@@ -0,0 +1,27 @@
+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 RegistrationCache
+ {
+ public readonly Dictionary CachedCerts = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ public byte[] AccountKey;
+ public string Id;
+ public Jwk Key;
+ public Uri Location;
+ }
+
+
+
+}
diff --git a/LetsEncrypt/LetsEncrypt.csproj b/LetsEncrypt/LetsEncrypt.csproj
new file mode 100644
index 0000000..faef3b1
--- /dev/null
+++ b/LetsEncrypt/LetsEncrypt.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ netcoreapp2.2
+
+
+
+
+
+
+
diff --git a/LetsEncrypt/Library.cs b/LetsEncrypt/Library.cs
new file mode 100644
index 0000000..76cc4bd
--- /dev/null
+++ b/LetsEncrypt/Library.cs
@@ -0,0 +1,26 @@
+using System;
+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(X509Certificate 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();
+ }
+ }
+}
diff --git a/LetsEncrypt/Program.cs b/LetsEncrypt/Program.cs
new file mode 100644
index 0000000..988a165
--- /dev/null
+++ b/LetsEncrypt/Program.cs
@@ -0,0 +1,110 @@
+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;
+
+
+using ACMEv2;
+
+
+using FS = System.IO;
+
+
+
+namespace LetsEncrypt
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ // save to http:///.well-known/acme-challenge/
+ var tokensPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".well-known/acme-challenge");
+ if (!FS.Directory.Exists(tokensPath))
+ FS.Directory.CreateDirectory(tokensPath);
+
+ foreach (FileInfo file in new DirectoryInfo(tokensPath).GetFiles())
+ file.Delete();
+
+
+ var certsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certs");
+ if (!FS.Directory.Exists(certsPath))
+ FS.Directory.CreateDirectory(certsPath);
+
+
+ List contacts = new List();
+ contacts.Add("maksym.sadovnychyy@gmail.com");
+
+ List hosts = new List();
+ hosts.Add("maks-it.com");
+ hosts.Add("www.maks-it.com");
+
+ Console.WriteLine("Let's Encrypt C# .Net Core Client");
+
+ try
+ {
+ LetsEncryptClient client = new LetsEncryptClient(LetsEncryptClient.StagingV2, AppDomain.CurrentDomain.BaseDirectory);
+ Console.WriteLine("1. Client Initialization...");
+
+ // 1
+ client.Init(contacts.ToArray()).Wait();
+ Console.WriteLine(string.Format("Terms of service: {0}",client.GetTermsOfServiceUri()));
+ client.NewNonce().Wait();
+
+
+ // 2
+ try
+ {
+ Console.WriteLine("2. Client New Order...");
+ Task> orders = client.NewOrder(hosts.ToArray(), "http-01");
+ orders.Wait();
+
+ foreach (var result in orders.Result)
+ {
+ Console.WriteLine("Key: " + result.Key + Environment.NewLine + "Value: " + result.Value);
+ string[] splitToken = result.Value.Split('~');
+ File.WriteAllText(FS.Path.Combine(tokensPath, splitToken[0]), splitToken[1]);
+ }
+
+ // 3
+ Console.WriteLine("3. Client Complete Challange...");
+ client.CompleteChallenges().Wait();
+ Console.WriteLine("Challanges comleted.");
+ }
+ catch (Exception ex) {
+ Console.WriteLine(ex.Message.ToString());
+ client.GetOrder(hosts.ToArray()).Wait();
+ }
+
+
+ // 4 Download certificate
+ Console.WriteLine("4. Download certificate...");
+ Task<(X509Certificate2 Cert, RSA PrivateKey)> certificate = client.GetCertificate();
+ certificate.Wait();
+
+ File.WriteAllText(Path.Combine(certsPath, "maks-it.com.crt"), Library.ExportToPEM(certificate.Result.Cert));
+ Console.WriteLine("Certificate saved.");
+ }
+ catch (Exception ex) {
+ Console.WriteLine(ex.Message.ToString());
+ }
+
+
+
+
+ Console.Read();
+ }
+
+
+
+ }
+
+
+}
diff --git a/LetsEncrypt/README.md b/LetsEncrypt/README.md
new file mode 100644
index 0000000..ac48803
--- /dev/null
+++ b/LetsEncrypt/README.md
@@ -0,0 +1,83 @@
+#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