From 75de1897d86f4c032feb0bf9f7a0c888d69dee6f Mon Sep 17 00:00:00 2001 From: maks-it Date: Sat, 29 Jun 2019 18:07:02 +0200 Subject: [PATCH] Add project files. --- LetsEncrypt.sln | 25 + LetsEncrypt/.vscode/launch.json | 27 + LetsEncrypt/.vscode/tasks.json | 36 ++ LetsEncrypt/ACMEv2/Account.cs | 58 ++ LetsEncrypt/ACMEv2/AuthorizationChallange.cs | 32 ++ .../ACMEv2/AuthorizationChallengeResponse.cs | 35 ++ LetsEncrypt/ACMEv2/AuthorizeChallenge.cs | 27 + LetsEncrypt/ACMEv2/CachedCertificateResult.cs | 22 + LetsEncrypt/ACMEv2/CertificateCache.cs | 24 + LetsEncrypt/ACMEv2/Directory.cs | 37 ++ LetsEncrypt/ACMEv2/DirectoryMeta.cs | 23 + LetsEncrypt/ACMEv2/FinalizeRequest.cs | 22 + LetsEncrypt/ACMEv2/IHashLocation.cs | 20 + LetsEncrypt/ACMEv2/Jwk.cs | 98 ++++ LetsEncrypt/ACMEv2/Jws.cs | 89 +++ LetsEncrypt/ACMEv2/JwsHeader.cs | 50 ++ LetsEncrypt/ACMEv2/JwsMessage.cs | 33 ++ LetsEncrypt/ACMEv2/LetsEncryptClient.cs | 542 ++++++++++++++++++ LetsEncrypt/ACMEv2/LetsEncrytException.cs | 29 + LetsEncrypt/ACMEv2/Order.cs | 49 ++ LetsEncrypt/ACMEv2/OrderIdentifier.cs | 26 + LetsEncrypt/ACMEv2/Problem.cs | 27 + LetsEncrypt/ACMEv2/RegistrationCache.cs | 27 + LetsEncrypt/LetsEncrypt.csproj | 12 + LetsEncrypt/Library.cs | 26 + LetsEncrypt/Program.cs | 110 ++++ LetsEncrypt/README.md | 83 +++ 27 files changed, 1589 insertions(+) create mode 100644 LetsEncrypt.sln create mode 100644 LetsEncrypt/.vscode/launch.json create mode 100644 LetsEncrypt/.vscode/tasks.json create mode 100644 LetsEncrypt/ACMEv2/Account.cs create mode 100644 LetsEncrypt/ACMEv2/AuthorizationChallange.cs create mode 100644 LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs create mode 100644 LetsEncrypt/ACMEv2/AuthorizeChallenge.cs create mode 100644 LetsEncrypt/ACMEv2/CachedCertificateResult.cs create mode 100644 LetsEncrypt/ACMEv2/CertificateCache.cs create mode 100644 LetsEncrypt/ACMEv2/Directory.cs create mode 100644 LetsEncrypt/ACMEv2/DirectoryMeta.cs create mode 100644 LetsEncrypt/ACMEv2/FinalizeRequest.cs create mode 100644 LetsEncrypt/ACMEv2/IHashLocation.cs create mode 100644 LetsEncrypt/ACMEv2/Jwk.cs create mode 100644 LetsEncrypt/ACMEv2/Jws.cs create mode 100644 LetsEncrypt/ACMEv2/JwsHeader.cs create mode 100644 LetsEncrypt/ACMEv2/JwsMessage.cs create mode 100644 LetsEncrypt/ACMEv2/LetsEncryptClient.cs create mode 100644 LetsEncrypt/ACMEv2/LetsEncrytException.cs create mode 100644 LetsEncrypt/ACMEv2/Order.cs create mode 100644 LetsEncrypt/ACMEv2/OrderIdentifier.cs create mode 100644 LetsEncrypt/ACMEv2/Problem.cs create mode 100644 LetsEncrypt/ACMEv2/RegistrationCache.cs create mode 100644 LetsEncrypt/LetsEncrypt.csproj create mode 100644 LetsEncrypt/Library.cs create mode 100644 LetsEncrypt/Program.cs create mode 100644 LetsEncrypt/README.md 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