mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
Add project files.
This commit is contained in:
parent
5c31b7f5dc
commit
75de1897d8
25
LetsEncrypt.sln
Normal file
25
LetsEncrypt.sln
Normal file
@ -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
|
||||||
27
LetsEncrypt/.vscode/launch.json
vendored
Normal file
27
LetsEncrypt/.vscode/launch.json
vendored
Normal file
@ -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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
36
LetsEncrypt/.vscode/tasks.json
vendored
Normal file
36
LetsEncrypt/.vscode/tasks.json
vendored
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
58
LetsEncrypt/ACMEv2/Account.cs
Normal file
58
LetsEncrypt/ACMEv2/Account.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
32
LetsEncrypt/ACMEv2/AuthorizationChallange.cs
Normal file
32
LetsEncrypt/ACMEv2/AuthorizationChallange.cs
Normal file
@ -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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
35
LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs
Normal file
35
LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
27
LetsEncrypt/ACMEv2/AuthorizeChallenge.cs
Normal file
27
LetsEncrypt/ACMEv2/AuthorizeChallenge.cs
Normal file
@ -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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
22
LetsEncrypt/ACMEv2/CachedCertificateResult.cs
Normal file
22
LetsEncrypt/ACMEv2/CachedCertificateResult.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
LetsEncrypt/ACMEv2/CertificateCache.cs
Normal file
24
LetsEncrypt/ACMEv2/CertificateCache.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
37
LetsEncrypt/ACMEv2/Directory.cs
Normal file
37
LetsEncrypt/ACMEv2/Directory.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
23
LetsEncrypt/ACMEv2/DirectoryMeta.cs
Normal file
23
LetsEncrypt/ACMEv2/DirectoryMeta.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
22
LetsEncrypt/ACMEv2/FinalizeRequest.cs
Normal file
22
LetsEncrypt/ACMEv2/FinalizeRequest.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
LetsEncrypt/ACMEv2/IHashLocation.cs
Normal file
20
LetsEncrypt/ACMEv2/IHashLocation.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
98
LetsEncrypt/ACMEv2/Jwk.cs
Normal file
98
LetsEncrypt/ACMEv2/Jwk.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// "kty" (Key Type) Parameter
|
||||||
|
/// <para>
|
||||||
|
/// The "kty" (key type) parameter identifies the cryptographic algorithm
|
||||||
|
/// family used with the key, such as "RSA" or "EC".
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("kty")]
|
||||||
|
public string KeyType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "kid" (Key ID) Parameter
|
||||||
|
/// <para>
|
||||||
|
/// 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.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("kid")]
|
||||||
|
public string KeyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "use" (Public Key Use) Parameter
|
||||||
|
/// <para>
|
||||||
|
/// 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.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("use")]
|
||||||
|
public string Use { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RSA public modulus n.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("n")]
|
||||||
|
public string Modulus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RSA public exponent e.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("e")]
|
||||||
|
public string Exponent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RSA secret exponent d = e^-1 \bmod (p-1)(q-1).
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("d")]
|
||||||
|
public string D { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RSA secret prime p.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("p")]
|
||||||
|
public string P { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RSA secret prime q with p < q.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "alg" (Algorithm) Parameter
|
||||||
|
/// <para>
|
||||||
|
/// The "alg" (algorithm) parameter identifies the algorithm intended for
|
||||||
|
/// use with the key.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("alg")]
|
||||||
|
public string Algorithm { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
89
LetsEncrypt/ACMEv2/Jws.cs
Normal file
89
LetsEncrypt/ACMEv2/Jws.cs
Normal file
@ -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>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
LetsEncrypt/ACMEv2/JwsHeader.cs
Normal file
50
LetsEncrypt/ACMEv2/JwsHeader.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
33
LetsEncrypt/ACMEv2/JwsMessage.cs
Normal file
33
LetsEncrypt/ACMEv2/JwsMessage.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
542
LetsEncrypt/ACMEv2/LetsEncryptClient.cs
Normal file
542
LetsEncrypt/ACMEv2/LetsEncryptClient.cs
Normal file
@ -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<string, HttpClient> _cachedClients = new Dictionary<string, HttpClient>(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<string, HttpClient>(_cachedClients, StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
[url] = value
|
||||||
|
};
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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?)
|
||||||
|
/// </summary>
|
||||||
|
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<AuthorizationChallenge> _challenges = new List<AuthorizationChallenge>();
|
||||||
|
private Order _currentOrder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Let's encrypt client object
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url"></param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Account creation or Initialization from cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contacts"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task Init(string[] contacts, CancellationToken token = default(CancellationToken))
|
||||||
|
{
|
||||||
|
_accountKey = new RSACryptoServiceProvider(4096);
|
||||||
|
_client = GetCachedClient(_url);
|
||||||
|
|
||||||
|
// 1 - Get directory
|
||||||
|
(_directory, _) = await SendAsync<Directory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), null, token);
|
||||||
|
|
||||||
|
|
||||||
|
if (File.Exists(_path))
|
||||||
|
{
|
||||||
|
bool success;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock (Locker)
|
||||||
|
{
|
||||||
|
_cache = JsonConvert.DeserializeObject<RegistrationCache>(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<Account>(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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Just retrive terms of service
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string GetTermsOfServiceUri(CancellationToken token = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return _directory.Meta.TermsOfService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request New Nonce to be able to start POST requests
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange.
|
||||||
|
/// <para>
|
||||||
|
/// Available challange types:
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>dns-01</item>
|
||||||
|
/// <item>http-01</item>
|
||||||
|
/// <item>tls-alpn-01</item>
|
||||||
|
/// </list>
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hostnames"></param>
|
||||||
|
/// <param name="challengeType"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Dictionary<string, string>> 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<Order>(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<string, string>();
|
||||||
|
foreach (var item in order.Authorizations)
|
||||||
|
{
|
||||||
|
var (challengeResponse, responseText) = await SendAsync<AuthorizationChallengeResponse>(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<AuthorizationChallengeResponse>(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<Order>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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<Order>(HttpMethod.Post, _currentOrder.Finalize, new FinalizeRequest
|
||||||
|
{
|
||||||
|
CSR = Jws.Base64UrlEncoded(csr.CreateSigningRequest())
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
while (response.Status != "valid")
|
||||||
|
{
|
||||||
|
(response, responseText) = await SendAsync<Order>(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<string>(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task KeyChange(CancellationToken token = default(CancellationToken))
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task RevokeCertificate(CancellationToken token = default(CancellationToken))
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main method used to send data to LetsEncrypt
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResult"></typeparam>
|
||||||
|
/// <param name="method"></param>
|
||||||
|
/// <param name="uri"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task<(TResult Result, string Response)> SendAsync<TResult>(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<Problem>(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<TResult>();
|
||||||
|
|
||||||
|
if (responseContent is IHasLocation ihl)
|
||||||
|
{
|
||||||
|
if (response.Headers.Location != null)
|
||||||
|
ihl.Location = response.Headers.Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (responseContent, responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hosts"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool TryGetCachedCertificate(List<string> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hostsToRemove"></param>
|
||||||
|
public void ResetCachedCertificate(IEnumerable<string> hostsToRemove)
|
||||||
|
{
|
||||||
|
foreach (var host in hostsToRemove)
|
||||||
|
{
|
||||||
|
_cache.CachedCerts.Remove(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
LetsEncrypt/ACMEv2/LetsEncrytException.cs
Normal file
29
LetsEncrypt/ACMEv2/LetsEncrytException.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
49
LetsEncrypt/ACMEv2/Order.cs
Normal file
49
LetsEncrypt/ACMEv2/Order.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
LetsEncrypt/ACMEv2/OrderIdentifier.cs
Normal file
26
LetsEncrypt/ACMEv2/OrderIdentifier.cs
Normal file
@ -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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
LetsEncrypt/ACMEv2/Problem.cs
Normal file
27
LetsEncrypt/ACMEv2/Problem.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
LetsEncrypt/ACMEv2/RegistrationCache.cs
Normal file
27
LetsEncrypt/ACMEv2/RegistrationCache.cs
Normal file
@ -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<string, CertificateCache> CachedCerts = new Dictionary<string, CertificateCache>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public byte[] AccountKey;
|
||||||
|
public string Id;
|
||||||
|
public Jwk Key;
|
||||||
|
public Uri Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
12
LetsEncrypt/LetsEncrypt.csproj
Normal file
12
LetsEncrypt/LetsEncrypt.csproj
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
26
LetsEncrypt/Library.cs
Normal file
26
LetsEncrypt/Library.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
|
||||||
|
namespace LetsEncrypt
|
||||||
|
{
|
||||||
|
class Library
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Export a certificate to a PEM format string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cert">The certificate to export</param>
|
||||||
|
/// <returns>A PEM encoded string</returns>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
LetsEncrypt/Program.cs
Normal file
110
LetsEncrypt/Program.cs
Normal file
@ -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://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>
|
||||||
|
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<string> contacts = new List<string>();
|
||||||
|
contacts.Add("maksym.sadovnychyy@gmail.com");
|
||||||
|
|
||||||
|
List<string> hosts = new List<string>();
|
||||||
|
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<Dictionary<string, string>> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
83
LetsEncrypt/README.md
Normal file
83
LetsEncrypt/README.md
Normal file
@ -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
|
||||||
Loading…
Reference in New Issue
Block a user