6using System.Reflection;
7using System.Security.Authentication;
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 = Crypto.SecureTls
54 catch (PlatformNotSupportedException)
56 this.httpClient =
new HttpClient(
new HttpClientHandler()
58 AllowAutoRedirect =
true
63 Version Version = T.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)
168 ContentResponse Temp = await Content.Getters.WebGetter.ProcessResponse(Response, Request.RequestUri);
172 if (Response.Headers.TryGetValues(
"Replay-Nonce", out IEnumerable<string> Values))
174 foreach (
string s
in Values)
178 throw new Exception(
"No nonce returned from server.");
188 if (this.directory is
null)
195 internal class AcmeResponse
197 public IEnumerable<KeyValuePair<string, object>> Payload;
198 public HttpResponseMessage ResponseMessage;
203 private async Task<HttpResponseMessage> HttpPost(Uri URL, Uri KeyID,
string Accept, params KeyValuePair<string, object>[] Payload)
206 string PayloadString;
211 this.jws.
Sign(
new KeyValuePair<string, object>[]
213 new KeyValuePair<string, object>(
"nonce", await this.NextNonce()),
214 new KeyValuePair<string, object>(
"url", URL.ToString())
215 }, Payload, out HeaderString, out PayloadString, out Signature);
219 this.jws.
Sign(
new KeyValuePair<string, object>[]
221 new KeyValuePair<string, object>(
"kid", KeyID.ToString()),
222 new KeyValuePair<string, object>(
"nonce", await this.NextNonce()),
223 new KeyValuePair<string, object>(
"url", URL.ToString())
224 }, Payload, out HeaderString, out PayloadString, out Signature);
227 string Json =
JSON.
Encode(
new KeyValuePair<string, object>[]
229 new KeyValuePair<string, object>(
"protected", HeaderString),
230 new KeyValuePair<string, object>(
"payload", PayloadString),
231 new KeyValuePair<string, object>(
"signature", Signature)
234 HttpContent Content =
new ByteArrayContent(Encoding.ASCII.GetBytes(Json));
237 if (!
string.IsNullOrEmpty(Accept))
238 Content.Headers.TryAddWithoutValidation(
"Accept", Accept);
240 HttpResponseMessage Response = await this.httpClient.PostAsync(URL, Content);
242 this.GetNextNonce(Response);
247 internal async Task<AcmeResponse> POST(Uri URL, Uri KeyID, params KeyValuePair<string, object>[] Payload)
249 HttpResponseMessage Response = await this.HttpPost(URL, KeyID,
null, Payload);
250 byte[] Bin = await Response.Content.ReadAsByteArrayAsync();
251 string CharSet = Response.Content.Headers.ContentType?.CharSet;
254 if (
string.IsNullOrEmpty(CharSet))
255 Encoding = Encoding.UTF8;
259 AcmeResponse AcmeResponse =
new AcmeResponse()
261 Json = Encoding.GetString(Bin),
263 ResponseMessage = Response,
267 if (Response.Headers.TryGetValues(
"Location", out IEnumerable<string> Values))
269 foreach (
string s
in Values)
271 AcmeResponse.Location =
new Uri(s);
276 if (
string.IsNullOrEmpty(AcmeResponse.Json))
277 AcmeResponse.Payload =
null;
278 else if ((AcmeResponse.Payload =
JSON.
Parse(AcmeResponse.Json) as IEnumerable<KeyValuePair<string, object>>) is
null)
279 throw new Exception(
"Unexpected response returned.");
281 if (Response.IsSuccessStatusCode)
284 throw CreateException(AcmeResponse.Payload, Response);
287 internal static AcmeException CreateException(IEnumerable<KeyValuePair<string, object>> Obj, HttpResponseMessage Response)
289 AcmeException[] Subproblems =
null;
291 string Detail =
null;
292 string instance =
null;
295 foreach (KeyValuePair<string, object> P
in Obj)
300 Type = P.Value as string;
304 Detail = P.Value as string;
308 instance = P.Value as string;
312 if (
int.TryParse(P.Value as
string, out
int i))
317 if (P.Value is Array A)
319 List<AcmeException> Subproblems2 =
new List<AcmeException>();
321 foreach (
object Obj2
in A)
323 if (Obj2 is IEnumerable<KeyValuePair<string, object>> Obj3)
324 Subproblems2.Add(CreateException(Obj3, Response));
327 Subproblems = Subproblems2.ToArray();
333 if (Type.StartsWith(
"urn:ietf:params:acme:error:"))
335 switch (Type.Substring(27))
337 case "accountDoesNotExist":
return new AcmeAccountDoesNotExistException(Type, Detail, Status, Subproblems);
338 case "badCSR":
return new AcmeBadCsrException(Type, Detail, Status, Subproblems);
339 case "badNonce":
return new AcmeBadNonceException(Type, Detail, Status, Subproblems);
340 case "badRevocationReason":
return new AcmeBadRevocationReasonException(Type, Detail, Status, Subproblems);
341 case "badSignatureAlgorithm":
return new AcmeBadSignatureAlgorithmException(Type, Detail, Status, Subproblems);
342 case "caa":
return new AcmeCaaException(Type, Detail, Status, Subproblems);
343 case "compound":
return new AcmeCompoundException(Type, Detail, Status, Subproblems);
344 case "connection":
return new AcmeConnectionException(Type, Detail, Status, Subproblems);
345 case "dns":
return new AcmeDnsException(Type, Detail, Status, Subproblems);
346 case "externalAccountRequired":
return new AcmeExternalAccountRequiredException(Type, Detail, Status, Subproblems);
347 case "incorrectResponse":
return new AcmeIncorrectResponseException(Type, Detail, Status, Subproblems);
348 case "invalidContact":
return new AcmeInvalidContactException(Type, Detail, Status, Subproblems);
349 case "malformed":
return new AcmeMalformedException(Type, Detail, Status, Subproblems);
350 case "rateLimited":
return new AcmeRateLimitedException(Type, Detail, Status, Subproblems);
351 case "rejectedIdentifier":
return new AcmeRejectedIdentifierException(Type, Detail, Status, Subproblems);
352 case "serverInternal":
return new AcmeServerInternalException(Type, Detail, Status, Subproblems);
353 case "tls":
return new AcmeTlsException(Type, Detail, Status, Subproblems);
354 case "unauthorized":
return new AcmeUnauthorizedException(Type, Detail, Status, Subproblems);
355 case "unsupportedContact":
return new AcmeUnsupportedContactException(Type, Detail, Status, Subproblems);
356 case "unsupportedIdentifier":
return new AcmeUnsupportedIdentifierException(Type, Detail, Status, Subproblems);
357 case "userActionRequired":
return new AcmeUserActionRequiredException(Type, Detail, Status, Subproblems,
new Uri(instance), GetLink(Response,
"terms-of-service"));
358 default:
return new AcmeException(Type, Detail, Status, Subproblems);
362 return new AcmeException(Type, Detail, Status, Subproblems);
365 private static readonly Regex nextUrl =
new Regex(
"^\\s*[<](?'URL'[^>]+)[>]\\s*;\\s*rel\\s*=\\s*['\"](?'Rel'.*)['\"]\\s*$", RegexOptions.Singleline | RegexOptions.Compiled);
367 internal static Uri GetLink(HttpResponseMessage Response,
string Rel)
369 if (Response.Headers.TryGetValues(
"Link", out IEnumerable<string> Values))
371 foreach (
string s
in Values)
373 Match M = nextUrl.Match(s);
376 if (M.Groups[
"Rel"].Value == Rel)
377 return new Uri(M.Groups[
"URL"].Value);
385 private void GetNextNonce(HttpResponseMessage Response)
387 if (Response.Headers.TryGetValues(
"Replay-Nonce", out IEnumerable<string> Values))
389 foreach (
string s
in Values)
403 public async Task<AcmeAccount>
CreateAccount(
string[] ContactURLs,
bool TermsOfServiceAgreed)
405 if (this.directory is
null)
408 AcmeResponse Response = await this.POST(this.directory.
NewAccount,
null,
409 new KeyValuePair<string, object>(
"termsOfServiceAgreed", TermsOfServiceAgreed),
410 new KeyValuePair<string, object>(
"contact", ContactURLs));
414 if (Response.Payload is
null)
416 Response = await this.POST(Response.Location, Response.Location);
417 Account =
new AcmeAccount(
this, Response.Location, Response.Payload);
419 bool ContactsDifferent =
false;
420 int i, c = ContactURLs.Length;
422 if (c != Account.
Contact.Length)
423 ContactsDifferent =
true;
426 for (i = 0; i < c; i++)
428 if (ContactURLs[i] != Account.
Contact[i])
430 ContactsDifferent =
true;
436 if (ContactsDifferent)
440 Account =
new AcmeAccount(
this, Response.Location, Response.Payload);
451 if (this.directory is
null)
454 AcmeResponse Response = await this.POST(this.directory.
NewAccount,
null,
455 new KeyValuePair<string, object>(
"onlyReturnExisting",
true));
457 if (Response.Payload is
null)
458 Response = await this.POST(Response.Location, Response.Location);
460 return new AcmeAccount(
this, Response.Location, Response.Payload);
469 public async Task<AcmeAccount>
UpdateAccount(Uri AccountLocation,
string[] Contact)
471 if (this.directory is
null)
474 AcmeResponse Response = await this.POST(AccountLocation, AccountLocation,
475 new KeyValuePair<string, object>(
"contact", Contact));
477 return new AcmeAccount(
this, Response.Location, Response.Payload);
487 if (this.directory is
null)
490 AcmeResponse Response = await this.POST(AccountLocation, AccountLocation,
491 new KeyValuePair<string, object>(
"status",
"deactivated"));
493 return new AcmeAccount(
this, Response.Location, Response.Payload);
500 public async Task<AcmeAccount>
NewKey(Uri AccountLocation)
502 if (this.directory is
null)
504 RSA
NewKey = RSA.Create();
505 NewKey.KeySize = KeySize;
507 if (
NewKey.KeySize != KeySize)
509 Type T = Runtime.Inventory.Types.GetType(
"System.Security.Cryptography.RSACryptoServiceProvider")
510 ??
throw new Exception(
"Unable to set RSA key size to anything but default (" +
NewKey.KeySize.ToString() +
" bits).");
512 NewKey = Runtime.Inventory.Types.Instantiate(T, KeySize) as RSA;
519 Jws2.
Sign(
new KeyValuePair<string, object>[]
521 new KeyValuePair<string, object>(
"url", this.directory.
KeyChange.ToString())
522 },
new KeyValuePair<string, object>[]
524 new KeyValuePair<string, object>(
"account", AccountLocation.ToString()),
525 new KeyValuePair<string, object>(
"oldkey", this.jws.
PublicWebKey),
526 }, out
string Header, out
string Payload, out
string Signature);
528 AcmeResponse Response = await this.POST(this.directory.
KeyChange, AccountLocation,
529 new KeyValuePair<string, object>(
"protected", Header),
530 new KeyValuePair<string, object>(
"payload", Payload),
531 new KeyValuePair<string, object>(
"signature", Signature));
533 this.jwkThumbprint =
null;
536 return new AcmeAccount(
this, Response.Location, Response.Payload);
553 DateTime? NotBefore, DateTime? NotAfter)
555 if (this.directory is
null)
558 int i, c = Identifiers.Length;
559 IEnumerable<KeyValuePair<string, object>>[] Identifiers2 =
new IEnumerable<KeyValuePair<string, object>>[c];
561 for (i = 0; i < c; i++)
563 Identifiers2[i] =
new KeyValuePair<string, object>[]
565 new KeyValuePair<string, object>(
"type", Identifiers[i].Type),
566 new KeyValuePair<string, object>(
"value", Identifiers[i].Value)
570 List<KeyValuePair<string, object>> Payload =
new List<KeyValuePair<string, object>>()
572 new KeyValuePair<string, object>(
"identifiers", Identifiers2)
575 if (NotBefore.HasValue)
576 Payload.Add(
new KeyValuePair<string, object>(
"notBefore", NotBefore.Value));
578 if (NotAfter.HasValue)
579 Payload.Add(
new KeyValuePair<string, object>(
"notAfter", NotAfter.Value));
581 AcmeResponse Response = await this.POST(this.directory.
NewOrder, AccountLocation, Payload.ToArray());
583 return new AcmeOrder(
this, AccountLocation, Response.Location, Response.Payload, Response.ResponseMessage);
592 public async Task<AcmeOrder>
GetOrder(Uri AccountLocation, Uri OrderLocation)
594 AcmeResponse Response = await this.POST_as_GET(OrderLocation, AccountLocation);
595 return new AcmeOrder(
this, AccountLocation, OrderLocation, Response.Payload, Response.ResponseMessage);
604 public async Task<AcmeOrder[]>
GetOrders(Uri AccountLocation, Uri OrdersLocation)
606 AcmeResponse
_ = await this.GET(OrdersLocation);
607 throw new NotImplementedException(
"Method not implemented.");
616 public async Task<AcmeAuthorization>
GetAuthorization(Uri AccountLocation, Uri AuthorizationLocation)
618 AcmeResponse Response = await this.POST_as_GET(AuthorizationLocation, AccountLocation);
619 return new AcmeAuthorization(
this, AccountLocation, AuthorizationLocation, Response.Payload);
630 AcmeResponse Response = await this.POST(AuthorizationLocation, AccountLocation,
631 new KeyValuePair<string, object>(
"status",
"deactivated"));
633 return new AcmeAuthorization(
this, AccountLocation, Response.Location, Response.Payload);
644 AcmeResponse Response = await this.POST(ChallengeLocation, AccountLocation);
645 return this.CreateChallenge(AccountLocation, Response.Payload);
648 internal AcmeChallenge CreateChallenge(Uri AccountLocation, IEnumerable<KeyValuePair<string, object>> Obj)
650 string Type =
string.Empty;
652 foreach (KeyValuePair<string, object> P2
in Obj)
654 if (P2.Key ==
"type" && P2.Value is
string s)
663 case "http-01":
return new AcmeHttpChallenge(
this, AccountLocation, Obj);
664 case "dns-01":
return new AcmeDnsChallenge(
this, AccountLocation, Obj);
665 default:
return new AcmeChallenge(
this, AccountLocation, Obj);
673 internal string JwkThumbprint
677 if (this.jwkThumbprint is
null)
679 SortedDictionary<string, object> Sorted =
new SortedDictionary<string, object>();
681 foreach (KeyValuePair<string, object> P
in this.jws.
PublicWebKey)
688 Sorted[P.Key] = P.Value;
694 byte[] Bin = Encoding.UTF8.GetBytes(Json);
700 return this.jwkThumbprint;
714 AcmeResponse Response = await this.POST(FinalizeLocation, AccountLocation,
717 return new AcmeOrder(
this, AccountLocation, Response.Location, Response.Payload, Response.ResponseMessage);
729 HttpResponseMessage Response = await this.HttpPost(CertificateLocation, AccountLocation, ContentType,
null);
731 if (!Response.IsSuccessStatusCode)
737 byte[] Bin = await Response.Content.ReadAsByteArrayAsync();
739 if (Response.Headers.TryGetValues(
"Content-Type", out IEnumerable<string> Values))
741 foreach (
string s
in Values)
751 if (!(Content.
Decoded is X509Certificate2[] Certificates))
752 throw new Exception(
"Unexpected response returned. Content-Type: " + ContentType);
764 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.
Contains information about a response to a content request.
object Decoded
Decoded object.
void AssertOk()
Asserts response is OK.
Gets resources from the Web (i.e. using HTTP or HTTPS).
static async Task< ContentResponse > ProcessResponse(HttpResponseMessage Response, Uri Uri)
Decodes a response from the web. If the response is a success, the decoded response is returned....
Static class managing encoding and decoding of internet content.
static Task< ContentResponse > 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