2using System.Collections.Generic;
6using System.Reflection;
7using System.Security.Authentication;
8using System.Security.Cryptography;
9using System.Security.Cryptography.X509Certificates;
11using System.Text.RegularExpressions;
12using System.Threading.Tasks;
25 private const int KeySize = 4096;
27 private readonly Uri directoryEndpoint;
28 private HttpClient httpClient;
31 private string nonce =
null;
32 private string jwkThumbprint =
null;
39 public AcmeClient(Uri DirectoryEndpoint, RSAParameters Parameters)
41 this.directoryEndpoint = DirectoryEndpoint;
46 this.httpClient =
new HttpClient(
new HttpClientHandler()
48 AllowAutoRedirect =
true,
49 AutomaticDecompression = (DecompressionMethods)(-1),
50 CheckCertificateRevocationList =
true,
51 SslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12
54 catch (PlatformNotSupportedException)
56 this.httpClient =
new HttpClient(
new HttpClientHandler()
58 AllowAutoRedirect =
true
63 Version Version = T.GetTypeInfo().Assembly.GetName().Version;
64 StringBuilder UserAgent =
new StringBuilder();
66 UserAgent.Append(T.Namespace);
67 UserAgent.Append(
'/');
68 UserAgent.Append(Version.Major.ToString());
69 UserAgent.Append(
'.');
70 UserAgent.Append(Version.Minor.ToString());
71 UserAgent.Append(
'.');
72 UserAgent.Append(Version.Build.ToString());
74 this.httpClient.DefaultRequestHeaders.Add(
"User-Agent", UserAgent.ToString());
76 this.httpClient.DefaultRequestHeaders.Add(
"Accept-Language",
"en");
84 if (!(this.httpClient is
null))
86 this.httpClient.Dispose();
87 this.httpClient =
null;
90 if (!(this.jws is
null))
103 if (this.directory is
null)
104 this.directory =
new AcmeDirectory(
this, (await this.GET(this.directoryEndpoint)).Payload);
106 return this.directory;
109 internal Task<AcmeResponse> POST_as_GET(Uri URL, Uri AccountLocation)
111 return this.POST(URL, AccountLocation,
null);
114 internal async Task<AcmeResponse> GET(Uri URL)
116 HttpResponseMessage Response = await this.httpClient.GetAsync(URL);
118 byte[] Bin = await Response.Content.ReadAsByteArrayAsync();
119 string CharSet = Response.Content.Headers.ContentType?.CharSet;
122 if (
string.IsNullOrEmpty(CharSet))
123 Encoding = Encoding.UTF8;
127 string JsonResponse = Encoding.GetString(Bin);
129 if (!(
JSON.
Parse(JsonResponse) is IEnumerable<KeyValuePair<string, object>> Obj))
130 throw new Exception(
"Unexpected response returned.");
132 if (Response.Content.Headers.TryGetValues(
"Retry-After", out IEnumerable<string> _))
137 if (Response.IsSuccessStatusCode)
139 return new AcmeResponse()
144 ResponseMessage = Response
148 throw CreateException(Obj, Response);
151 internal async Task<string> NextNonce()
153 if (!
string.IsNullOrEmpty(this.nonce))
155 string s = this.nonce;
160 if (this.directory is
null)
163 HttpRequestMessage Request =
new HttpRequestMessage(HttpMethod.Head,
this.directory.NewNonce);
164 HttpResponseMessage Response = await this.httpClient.SendAsync(Request);
166 if (!Response.IsSuccessStatusCode)
167 await Content.Getters.WebGetter.ProcessResponse(Response, Request.RequestUri);
169 if (Response.Headers.TryGetValues(
"Replay-Nonce", out IEnumerable<string> Values))
171 foreach (
string s
in Values)
175 throw new Exception(
"No nonce returned from server.");
185 if (this.directory is
null)
192 internal class AcmeResponse
194 public IEnumerable<KeyValuePair<string, object>> Payload;
195 public HttpResponseMessage ResponseMessage;
200 private async Task<HttpResponseMessage> HttpPost(Uri URL, Uri KeyID,
string Accept, params KeyValuePair<string, object>[] Payload)
203 string PayloadString;
208 this.jws.
Sign(
new KeyValuePair<string, object>[]
210 new KeyValuePair<string, object>(
"nonce", await this.NextNonce()),
211 new KeyValuePair<string, object>(
"url", URL.ToString())
212 }, Payload, out HeaderString, out PayloadString, out Signature);
216 this.jws.
Sign(
new KeyValuePair<string, object>[]
218 new KeyValuePair<string, object>(
"kid", KeyID.ToString()),
219 new KeyValuePair<string, object>(
"nonce", await this.NextNonce()),
220 new KeyValuePair<string, object>(
"url", URL.ToString())
221 }, Payload, out HeaderString, out PayloadString, out Signature);
224 string Json =
JSON.
Encode(
new KeyValuePair<string, object>[]
226 new KeyValuePair<string, object>(
"protected", HeaderString),
227 new KeyValuePair<string, object>(
"payload", PayloadString),
228 new KeyValuePair<string, object>(
"signature", Signature)
231 HttpContent Content =
new ByteArrayContent(Encoding.ASCII.GetBytes(Json));
234 if (!
string.IsNullOrEmpty(Accept))
235 Content.Headers.TryAddWithoutValidation(
"Accept", Accept);
237 HttpResponseMessage Response = await this.httpClient.PostAsync(URL, Content);
239 this.GetNextNonce(Response);
244 internal async Task<AcmeResponse> POST(Uri URL, Uri KeyID, params KeyValuePair<string, object>[] Payload)
246 HttpResponseMessage Response = await this.HttpPost(URL, KeyID,
null, Payload);
247 byte[] Bin = await Response.Content.ReadAsByteArrayAsync();
248 string CharSet = Response.Content.Headers.ContentType?.CharSet;
251 if (
string.IsNullOrEmpty(CharSet))
252 Encoding = Encoding.UTF8;
256 AcmeResponse AcmeResponse =
new AcmeResponse()
258 Json = Encoding.GetString(Bin),
260 ResponseMessage = Response,
264 if (Response.Headers.TryGetValues(
"Location", out IEnumerable<string> Values))
266 foreach (
string s
in Values)
268 AcmeResponse.Location =
new Uri(s);
273 if (
string.IsNullOrEmpty(AcmeResponse.Json))
274 AcmeResponse.Payload =
null;
275 else if ((AcmeResponse.Payload =
JSON.
Parse(AcmeResponse.Json) as IEnumerable<KeyValuePair<string, object>>) is
null)
276 throw new Exception(
"Unexpected response returned.");
278 if (Response.IsSuccessStatusCode)
281 throw CreateException(AcmeResponse.Payload, Response);
284 internal static AcmeException CreateException(IEnumerable<KeyValuePair<string, object>> Obj, HttpResponseMessage Response)
286 AcmeException[] Subproblems =
null;
288 string Detail =
null;
289 string instance =
null;
292 foreach (KeyValuePair<string, object> P
in Obj)
297 Type = P.Value as string;
301 Detail = P.Value as string;
305 instance = P.Value as string;
309 if (
int.TryParse(P.Value as
string, out
int i))
314 if (P.Value is Array A)
316 List<AcmeException> Subproblems2 =
new List<AcmeException>();
318 foreach (
object Obj2
in A)
320 if (Obj2 is IEnumerable<KeyValuePair<string, object>> Obj3)
321 Subproblems2.Add(CreateException(Obj3, Response));
324 Subproblems = Subproblems2.ToArray();
330 if (Type.StartsWith(
"urn:ietf:params:acme:error:"))
332 switch (Type.Substring(27))
334 case "accountDoesNotExist":
return new AcmeAccountDoesNotExistException(Type, Detail, Status, Subproblems);
335 case "badCSR":
return new AcmeBadCsrException(Type, Detail, Status, Subproblems);
336 case "badNonce":
return new AcmeBadNonceException(Type, Detail, Status, Subproblems);
337 case "badRevocationReason":
return new AcmeBadRevocationReasonException(Type, Detail, Status, Subproblems);
338 case "badSignatureAlgorithm":
return new AcmeBadSignatureAlgorithmException(Type, Detail, Status, Subproblems);
339 case "caa":
return new AcmeCaaException(Type, Detail, Status, Subproblems);
340 case "compound":
return new AcmeCompoundException(Type, Detail, Status, Subproblems);
341 case "connection":
return new AcmeConnectionException(Type, Detail, Status, Subproblems);
342 case "dns":
return new AcmeDnsException(Type, Detail, Status, Subproblems);
343 case "externalAccountRequired":
return new AcmeExternalAccountRequiredException(Type, Detail, Status, Subproblems);
344 case "incorrectResponse":
return new AcmeIncorrectResponseException(Type, Detail, Status, Subproblems);
345 case "invalidContact":
return new AcmeInvalidContactException(Type, Detail, Status, Subproblems);
346 case "malformed":
return new AcmeMalformedException(Type, Detail, Status, Subproblems);
347 case "rateLimited":
return new AcmeRateLimitedException(Type, Detail, Status, Subproblems);
348 case "rejectedIdentifier":
return new AcmeRejectedIdentifierException(Type, Detail, Status, Subproblems);
349 case "serverInternal":
return new AcmeServerInternalException(Type, Detail, Status, Subproblems);
350 case "tls":
return new AcmeTlsException(Type, Detail, Status, Subproblems);
351 case "unauthorized":
return new AcmeUnauthorizedException(Type, Detail, Status, Subproblems);
352 case "unsupportedContact":
return new AcmeUnsupportedContactException(Type, Detail, Status, Subproblems);
353 case "unsupportedIdentifier":
return new AcmeUnsupportedIdentifierException(Type, Detail, Status, Subproblems);
354 case "userActionRequired":
return new AcmeUserActionRequiredException(Type, Detail, Status, Subproblems,
new Uri(instance), GetLink(Response,
"terms-of-service"));
355 default:
return new AcmeException(Type, Detail, Status, Subproblems);
359 return new AcmeException(Type, Detail, Status, Subproblems);
362 private static readonly Regex nextUrl =
new Regex(
"^\\s*[<](?'URL'[^>]+)[>]\\s*;\\s*rel\\s*=\\s*['\"](?'Rel'.*)['\"]\\s*$", RegexOptions.Singleline | RegexOptions.Compiled);
364 internal static Uri GetLink(HttpResponseMessage Response,
string Rel)
366 if (Response.Headers.TryGetValues(
"Link", out IEnumerable<string> Values))
368 foreach (
string s
in Values)
370 Match M = nextUrl.Match(s);
373 if (M.Groups[
"Rel"].Value == Rel)
374 return new Uri(M.Groups[
"URL"].Value);
382 private void GetNextNonce(HttpResponseMessage Response)
384 if (Response.Headers.TryGetValues(
"Replay-Nonce", out IEnumerable<string> Values))
386 foreach (
string s
in Values)
400 public async Task<AcmeAccount>
CreateAccount(
string[] ContactURLs,
bool TermsOfServiceAgreed)
402 if (this.directory is
null)
405 AcmeResponse Response = await this.POST(this.directory.
NewAccount,
null,
406 new KeyValuePair<string, object>(
"termsOfServiceAgreed", TermsOfServiceAgreed),
407 new KeyValuePair<string, object>(
"contact", ContactURLs));
411 if (Response.Payload is
null)
413 Response = await this.POST(Response.Location, Response.Location);
414 Account =
new AcmeAccount(
this, Response.Location, Response.Payload);
416 bool ContactsDifferent =
false;
417 int i, c = ContactURLs.Length;
419 if (c != Account.
Contact.Length)
420 ContactsDifferent =
true;
423 for (i = 0; i < c; i++)
425 if (ContactURLs[i] != Account.
Contact[i])
427 ContactsDifferent =
true;
433 if (ContactsDifferent)
437 Account =
new AcmeAccount(
this, Response.Location, Response.Payload);
448 if (this.directory is
null)
451 AcmeResponse Response = await this.POST(this.directory.
NewAccount,
null,
452 new KeyValuePair<string, object>(
"onlyReturnExisting",
true));
454 if (Response.Payload is
null)
455 Response = await this.POST(Response.Location, Response.Location);
457 return new AcmeAccount(
this, Response.Location, Response.Payload);
466 public async Task<AcmeAccount>
UpdateAccount(Uri AccountLocation,
string[] Contact)
468 if (this.directory is
null)
471 AcmeResponse Response = await this.POST(AccountLocation, AccountLocation,
472 new KeyValuePair<string, object>(
"contact", Contact));
474 return new AcmeAccount(
this, Response.Location, Response.Payload);
484 if (this.directory is
null)
487 AcmeResponse Response = await this.POST(AccountLocation, AccountLocation,
488 new KeyValuePair<string, object>(
"status",
"deactivated"));
490 return new AcmeAccount(
this, Response.Location, Response.Payload);
497 public async Task<AcmeAccount>
NewKey(Uri AccountLocation)
499 if (this.directory is
null)
501 RSA
NewKey = RSA.Create();
502 NewKey.KeySize = KeySize;
504 if (
NewKey.KeySize != KeySize)
506 Type T = Runtime.Inventory.Types.GetType(
"System.Security.Cryptography.RSACryptoServiceProvider")
507 ??
throw new Exception(
"Unable to set RSA key size to anything but default (" +
NewKey.KeySize.ToString() +
" bits).");
509 NewKey = Runtime.Inventory.Types.Instantiate(T, KeySize) as RSA;
516 Jws2.
Sign(
new KeyValuePair<string, object>[]
518 new KeyValuePair<string, object>(
"url", this.directory.
KeyChange.ToString())
519 },
new KeyValuePair<string, object>[]
521 new KeyValuePair<string, object>(
"account", AccountLocation.ToString()),
522 new KeyValuePair<string, object>(
"oldkey", this.jws.
PublicWebKey),
523 }, out
string Header, out
string Payload, out
string Signature);
525 AcmeResponse Response = await this.POST(this.directory.
KeyChange, AccountLocation,
526 new KeyValuePair<string, object>(
"protected", Header),
527 new KeyValuePair<string, object>(
"payload", Payload),
528 new KeyValuePair<string, object>(
"signature", Signature));
530 this.jwkThumbprint =
null;
533 return new AcmeAccount(
this, Response.Location, Response.Payload);
550 DateTime? NotBefore, DateTime? NotAfter)
552 if (this.directory is
null)
555 int i, c = Identifiers.Length;
556 IEnumerable<KeyValuePair<string, object>>[] Identifiers2 =
new IEnumerable<KeyValuePair<string, object>>[c];
558 for (i = 0; i < c; i++)
560 Identifiers2[i] =
new KeyValuePair<string, object>[]
562 new KeyValuePair<string, object>(
"type", Identifiers[i].Type),
563 new KeyValuePair<string, object>(
"value", Identifiers[i].Value)
567 List<KeyValuePair<string, object>> Payload =
new List<KeyValuePair<string, object>>()
569 new KeyValuePair<string, object>(
"identifiers", Identifiers2)
572 if (NotBefore.HasValue)
573 Payload.Add(
new KeyValuePair<string, object>(
"notBefore", NotBefore.Value));
575 if (NotAfter.HasValue)
576 Payload.Add(
new KeyValuePair<string, object>(
"notAfter", NotAfter.Value));
578 AcmeResponse Response = await this.POST(this.directory.
NewOrder, AccountLocation, Payload.ToArray());
580 return new AcmeOrder(
this, AccountLocation, Response.Location, Response.Payload, Response.ResponseMessage);
589 public async Task<AcmeOrder>
GetOrder(Uri AccountLocation, Uri OrderLocation)
591 AcmeResponse Response = await this.POST_as_GET(OrderLocation, AccountLocation);
592 return new AcmeOrder(
this, AccountLocation, OrderLocation, Response.Payload, Response.ResponseMessage);
601 public async Task<AcmeOrder[]>
GetOrders(Uri AccountLocation, Uri OrdersLocation)
603 AcmeResponse _ = await this.GET(OrdersLocation);
604 throw new NotImplementedException(
"Method not implemented.");
613 public async Task<AcmeAuthorization>
GetAuthorization(Uri AccountLocation, Uri AuthorizationLocation)
615 AcmeResponse Response = await this.POST_as_GET(AuthorizationLocation, AccountLocation);
616 return new AcmeAuthorization(
this, AccountLocation, AuthorizationLocation, Response.Payload);
627 AcmeResponse Response = await this.POST(AuthorizationLocation, AccountLocation,
628 new KeyValuePair<string, object>(
"status",
"deactivated"));
630 return new AcmeAuthorization(
this, AccountLocation, Response.Location, Response.Payload);
641 AcmeResponse Response = await this.POST(ChallengeLocation, AccountLocation);
642 return this.CreateChallenge(AccountLocation, Response.Payload);
645 internal AcmeChallenge CreateChallenge(Uri AccountLocation, IEnumerable<KeyValuePair<string, object>> Obj)
647 string Type =
string.Empty;
649 foreach (KeyValuePair<string, object> P2
in Obj)
651 if (P2.Key ==
"type" && P2.Value is
string s)
660 case "http-01":
return new AcmeHttpChallenge(
this, AccountLocation, Obj);
661 case "dns-01":
return new AcmeDnsChallenge(
this, AccountLocation, Obj);
662 default:
return new AcmeChallenge(
this, AccountLocation, Obj);
670 internal string JwkThumbprint
674 if (this.jwkThumbprint is
null)
676 SortedDictionary<string, object> Sorted =
new SortedDictionary<string, object>();
678 foreach (KeyValuePair<string, object> P
in this.jws.
PublicWebKey)
685 Sorted[P.Key] = P.Value;
691 byte[] Bin = Encoding.UTF8.GetBytes(Json);
697 return this.jwkThumbprint;
711 AcmeResponse Response = await this.POST(FinalizeLocation, AccountLocation,
714 return new AcmeOrder(
this, AccountLocation, Response.Location, Response.Payload, Response.ResponseMessage);
726 HttpResponseMessage Response = await this.HttpPost(CertificateLocation, AccountLocation, ContentType,
null);
728 if (!Response.IsSuccessStatusCode)
729 await Content.Getters.WebGetter.ProcessResponse(Response, AccountLocation);
731 byte[] Bin = await Response.Content.ReadAsByteArrayAsync();
733 if (Response.Headers.TryGetValues(
"Content-Type", out IEnumerable<string> Values))
735 foreach (
string s
in Values)
743 if (!(Decoded is X509Certificate2[] Certificates))
744 throw new Exception(
"Unexpected response returned. Content-Type: " + ContentType);
756 return this.jws.
RSA.ExportParameters(IncludePrivateParameters);
Static class that does BASE64URL encoding (using URL and filename safe alphabet), as defined in RFC46...
static string Encode(byte[] Data)
Converts a binary block of data to a Base64URL-encoded string.
Static class managing encoding and decoding of internet content.
static Task< object > DecodeAsync(string ContentType, byte[] Data, Encoding Encoding, KeyValuePair< string, string >[] Fields, Uri BaseUri)
Decodes an object.
static Encoding GetEncoding(string CharacterSet)
Gets a character encoding from its name.
Helps with common JSON-related tasks.
static object Parse(string Json)
Parses a JSON string.
static string Encode(string s)
Encodes a string for inclusion in JSON.
Represents an ACME account.
string[] Contact
Optional array of URLs that the server can use to contact the client for issues related to this accou...
Represents an ACME authorization.
Base class of all ACME challenges.
Implements an ACME client for the generation of certificates using ACME-compliant certificate servers...
async Task< AcmeAuthorization > DeactivateAuthorization(Uri AccountLocation, Uri AuthorizationLocation)
Deactivates an authorization.
async Task< AcmeAccount > NewKey(Uri AccountLocation)
Generates a new key for the account. (Account keys are managed by the CSP.)
async Task< AcmeOrder > GetOrder(Uri AccountLocation, Uri OrderLocation)
Gets the state of an order.
RSAParameters ExportAccountKey(bool IncludePrivateParameters)
Exports the account key.
async Task< AcmeAccount > GetAccount()
Gets the account object from the ACME server.
async Task< AcmeAccount > CreateAccount(string[] ContactURLs, bool TermsOfServiceAgreed)
Creates an account on the ACME server.
async Task< AcmeOrder > OrderCertificate(Uri AccountLocation, AcmeIdentifier[] Identifiers, DateTime? NotBefore, DateTime? NotAfter)
Orders certificate.
void Dispose()
IDisposable.Dispose
async Task< AcmeChallenge > AcknowledgeChallenge(Uri AccountLocation, Uri ChallengeLocation)
Acknowledges a challenge from the server.
async Task< AcmeAccount > UpdateAccount(Uri AccountLocation, string[] Contact)
Updates an account.
async Task< AcmeOrder[]> GetOrders(Uri AccountLocation, Uri OrdersLocation)
Gets the list of current orders for an account.
async Task< AcmeDirectory > GetDirectory()
Gets the ACME directory.
AcmeClient(Uri DirectoryEndpoint, RSAParameters Parameters)
Implements an ACME client for the generation of certificates using ACME-compliant certificate servers...
async Task< AcmeAuthorization > GetAuthorization(Uri AccountLocation, Uri AuthorizationLocation)
Gets the state of an authorization.
async Task< AcmeOrder > FinalizeOrder(Uri AccountLocation, Uri FinalizeLocation, CertificateRequest CertificateRequest)
Finalize order.
Task< AcmeDirectory > Directory
Directory object.
async Task< X509Certificate2[]> DownloadCertificate(Uri AccountLocation, Uri CertificateLocation)
Downloads a certificate.
async Task< AcmeAccount > DeactivateAccount(Uri AccountLocation)
Deactivates an account.
Represents an ACME directory.
Uri KeyChange
URL for keyChange method.
Uri NewAccount
URL for newAccount method.
Uri NewOrder
URL for newOrder method.
Represents an ACME identifier.
Represents an ACME order.
Uri Location
Location of resource.
Contains methods for simple hash calculations.
static byte[] ComputeSHA256Hash(byte[] Data)
Computes the SHA-256 hash of a block of binary data.
Abstract base class for JWS algorithm.
const string JwsContentType
application/jose+json
RSASSA-PKCS1-v1_5 SHA-256 algorithm. https://tools.ietf.org/html/rfc3447#page-32
RSA RSA
RSA Cryptographic service provider.
void ImportKey(RSA RSA)
Imports a new key from an external RSA Cryptographic service provider.
override IEnumerable< KeyValuePair< string, object > > PublicWebKey
The public JSON web key, if supported.
override void Dispose()
IDisposable.Dispose
override string Sign(string HeaderEncoded, string PayloadEncoded)
Signs data.
Contains information about a Certificate Signing Request (CSR).
byte[] BuildCSR()
Building a Certificate Signing Request (CSR) in accordance with RFC 2986
Decodes certificates encoded using the application/pem-certificate-chain content type.
const string ContentType
application/pem-certificate-chain