Skip to content

Commit

Permalink
User/kenchr/variousfixes (#22)
Browse files Browse the repository at this point in the history
Rename InvalidServerResponse to InvalidServerResponseException
Ensure all Exception classes are marked Serializable
Rename namespace for JsonWebSiganture
Change ACMEEnvironment strings to be const
Add ConfigureAwait(false) to async calls
  • Loading branch information
Kencdk authored Nov 17, 2019
1 parent 55bb91d commit cbe59e5
Show file tree
Hide file tree
Showing 39 changed files with 188 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace ACMELibCore.Test.RequestMethodTests
using System.Threading.Tasks;
using Kenc.ACMELib;
using Kenc.ACMELib.ACMEResponses;
using Kenc.ACMELib.JWS;
using Kenc.ACMELib.JsonWebSignature;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public async Task ValidateRegisterAsyncPositiveFlow()
}

[TestMethod]
[ExpectedException(typeof(InvalidServerResponse))]
[ExpectedException(typeof(InvalidServerResponseException))]
public async Task ValidateRegisterAsyncThrowsInvalidServerResponseForBadResponse()
{
var testSystem = new TestSystem().WithDirectoryResponse();
Expand Down
2 changes: 1 addition & 1 deletion src/Libraries/ACMELib.Tests/TestSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Threading.Tasks;
using Kenc.ACMELib;
using Kenc.ACMELib.ACMEResponses;
using Kenc.ACMELib.JWS;
using Kenc.ACMELib.JsonWebSignature;
using Moq;

class TestSystem
Expand Down
111 changes: 75 additions & 36 deletions src/Libraries/ACMELib/ACMEClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@
using Kenc.ACMELib.ACMEEntities;
using Kenc.ACMELib.ACMEResponses;
using Kenc.ACMELib.Exceptions;
using Kenc.ACMELib.JWS;
using Kenc.ACMELib.JsonWebSignature;

/// <summary>
/// Implementation of an ACME client.
/// Following https://tools.ietf.org/html/draft-ietf-acme-acme-16
/// </summary>
public class ACMEClient : IACMEClient
{
private Jws jws;
private readonly Jws jws;
private readonly RSA rsaKey;
private readonly Uri endpoint;
public ACMEDirectory Directory;

private string nonce;
public ACMEDirectory Directory { get; private set; }

private readonly IRestClient client;

/// <summary>
Expand All @@ -35,11 +35,16 @@ public class ACMEClient : IACMEClient
/// <param name="restClientFactory">An instance of a <see cref="IRESTClient"/> for API requests.</param>
public ACMEClient(string endpoint, RSA rsaKey, IRestClientFactory restClientFactory)
{
if (endpoint == null)
if (string.IsNullOrEmpty(endpoint))
{
throw new ArgumentNullException(nameof(endpoint));
}

if (restClientFactory == null)
{
throw new ArgumentNullException(nameof(restClientFactory));
}

this.endpoint = new Uri(endpoint);
this.rsaKey = rsaKey ?? throw new ArgumentNullException(nameof(rsaKey));

Expand All @@ -49,8 +54,10 @@ public ACMEClient(string endpoint, RSA rsaKey, IRestClientFactory restClientFact

public async Task<ACMEDirectory> InitializeAsync()
{
await GetDirectoryAsync();
await NewNonceAsync();
await GetDirectoryAsync()
.ConfigureAwait(false);
await NewNonceAsync()
.ConfigureAwait(false);
return Directory;
}

Expand All @@ -61,12 +68,13 @@ public async Task<ACMEDirectory> InitializeAsync()
/// <param name="cancellationToken">Cancellation token for the async call.</param>
/// <returns><see cref="Account"/></returns>
/// <exception cref="ACMEException">Thrown for all errors from ACME servers.</exception>
/// <exception cref="InvalidServerResponse">Thrown when the response from the server wasn't expected.</exception>
/// <exception cref="InvalidServerResponseException">Thrown when the response from the server wasn't expected.</exception>
public async Task<Account> RegisterAsync(string[] contacts, CancellationToken cancellationToken = default)
{
if (Directory == null)
{
await GetDirectoryAsync();
await GetDirectoryAsync()
.ConfigureAwait(false);
}

var message = new Account
Expand All @@ -75,13 +83,14 @@ public async Task<Account> RegisterAsync(string[] contacts, CancellationToken ca
Contacts = contacts,
};

var (result, response) = await client.PostAsync<Account>(Directory.NewAccount, message, cancellationToken);
var (result, response) = await client.PostAsync<Account>(Directory.NewAccount, message, cancellationToken)
.ConfigureAwait(false);
if (result is Account acmeAccount)
{
return acmeAccount;
}

throw new InvalidServerResponse("Invalid response from server during registration.", response, Directory.NewAccount);
throw new InvalidServerResponseException("registration.", response, Directory.NewAccount);
}

/// <summary>
Expand All @@ -93,21 +102,23 @@ public async Task<Account> GetAccountAsync(CancellationToken cancellationToken =
{
if (Directory == null)
{
await GetDirectoryAsync();
await GetDirectoryAsync()
.ConfigureAwait(false);
}

var message = new Account
{
OnlyReturnExisting = true
};

var (result, response) = await client.PostAsync<Account>(Directory.NewAccount, message, cancellationToken);
var (result, response) = await client.PostAsync<Account>(Directory.NewAccount, message, cancellationToken)
.ConfigureAwait(false);
if (result is Account acmeAccount)
{
return acmeAccount;
}

throw new InvalidServerResponse("Invalid response from server during account retrieval.", response, Directory.NewAccount);
throw new InvalidServerResponseException("account retrieval.", response, Directory.NewAccount);
}

/// <summary>
Expand All @@ -118,20 +129,20 @@ public async Task<Account> GetAccountAsync(CancellationToken cancellationToken =
/// <returns>An <see cref="Order"/> object with details for the requested identifiers.</returns>
public async Task<Order> OrderAsync(IEnumerable<OrderIdentifier> identifiers, CancellationToken cancellationToken = default)
{

var message = new Order
{
Expires = DateTime.UtcNow.AddDays(2),
Identifiers = identifiers.ToArray()
};

var (result, response) = await client.PostAsync<Order>(Directory.NewOrder, message, cancellationToken);
var (result, response) = await client.PostAsync<Order>(Directory.NewOrder, message, cancellationToken)
.ConfigureAwait(false);
if (result is Order acmeOrder)
{
return acmeOrder;
}

throw new InvalidServerResponse("Invalid response from server during order.", response, Directory.NewOrder);
throw new InvalidServerResponseException("order.", response, Directory.NewOrder);
}

/// <summary>
Expand All @@ -142,7 +153,8 @@ public async Task<Order> OrderAsync(IEnumerable<OrderIdentifier> identifiers, Ca
/// <returns><see cref="AuthorizationChallengeResponse"/></returns>
public async Task<AuthorizationChallengeResponse> GetAuthorizationChallengeAsync(Uri uri, CancellationToken cancellationToken = default)
{
var (result, response) = await client.GetAsync<AuthorizationChallengeResponse>(uri, cancellationToken);
var (result, response) = await client.GetAsync<AuthorizationChallengeResponse>(uri, cancellationToken)
.ConfigureAwait(false);
if (result is AuthorizationChallengeResponse acmeOrder)
{
if (result.Challenges != null)
Expand All @@ -163,7 +175,7 @@ public async Task<AuthorizationChallengeResponse> GetAuthorizationChallengeAsync
return acmeOrder;
}

throw new InvalidServerResponse("Invalid response from server during GetAuthorizationChallenge.", response, Directory.NewAccount);
throw new InvalidServerResponseException("GetAuthorizationChallenge.", response, Directory.NewAccount);
}

/// <summary>
Expand All @@ -181,13 +193,14 @@ public async Task<AuthorizationChallengeResponse> CompleteChallengeAsync(Uri uri
KeyAuthorization = authorization
};

var (result, response) = await client.PostAsync<AuthorizationChallengeResponse>(uri, message, cancellationToken);
var (result, response) = await client.PostAsync<AuthorizationChallengeResponse>(uri, message, cancellationToken)
.ConfigureAwait(false);
if (result is AuthorizationChallengeResponse acmeOrder)
{
return acmeOrder;
}

throw new InvalidServerResponse("Invalid response from server during CompleteChallenge.", response, Directory.NewAccount);
throw new InvalidServerResponseException("CompleteChallenge.", response, Directory.NewAccount);
}

/// <summary>
Expand All @@ -204,13 +217,14 @@ public async Task<AuthorizationChallengeResponse> UpdateChallengeAsync(Uri uri,
KeyAuthorization = jws.GetKeyAuthorization(token)
};

var (result, response) = await client.PostAsync<AuthorizationChallengeResponse>(uri, message, cancellationToken);
var (result, response) = await client.PostAsync<AuthorizationChallengeResponse>(uri, message, cancellationToken)
.ConfigureAwait(false);
if (result is AuthorizationChallengeResponse acmeOrder)
{
return acmeOrder;
}

throw new InvalidServerResponse("Invalid response from server during UpdateChallenge.", response, Directory.NewAccount);
throw new InvalidServerResponseException("UpdateChallenge.", response, Directory.NewAccount);
}

/// <summary>
Expand All @@ -223,6 +237,16 @@ public async Task<AuthorizationChallengeResponse> UpdateChallengeAsync(Uri uri,
/// <remarks>The subjectname for the request is the first identifier in <paramref name="order"/>. Subsequent identifiers are added as alternative names.</remarks>
public async Task<Order> RequestCertificateAsync(Order order, RSACryptoServiceProvider key, CancellationToken cancellationToken = default)
{
if (order == null)
{
throw new ArgumentNullException(nameof(order));
}

if (key == null)
{
throw new ArgumentNullException(nameof(key));
}

var identifiers = order.Identifiers.Select(item => item.Value).ToList();
if (identifiers.Any(x => x[0] == '*'))
{
Expand All @@ -244,13 +268,14 @@ public async Task<Order> RequestCertificateAsync(Order order, RSACryptoServicePr
CSR = Utilities.Base64UrlEncoded(csr.CreateSigningRequest())
};

var (result, responseText) = await client.PostAsync<Order>(order.Finalize, message, cancellationToken);
var (result, responseText) = await client.PostAsync<Order>(order.Finalize, message, cancellationToken)
.ConfigureAwait(false);
if (result is Order acmeOrder)
{
return acmeOrder;
}

throw new InvalidServerResponse("Invalid response from server during RequestCertificate.", responseText, order.Finalize.ToString());
throw new InvalidServerResponseException("RequestCertificate.", responseText, order.Finalize);
}

/// <summary>
Expand All @@ -261,13 +286,19 @@ public async Task<Order> RequestCertificateAsync(Order order, RSACryptoServicePr
/// <returns>An updated <see cref="Order"/> object.</returns>
public async Task<Order> UpdateOrderAsync(Order order, CancellationToken cancellationToken = default)
{
var (result, responseText) = await client.GetAsync<Order>(order.Location, cancellationToken);
if (order == null)
{
throw new ArgumentNullException(nameof(order));
}

var (result, responseText) = await client.GetAsync<Order>(order.Location, cancellationToken)
.ConfigureAwait(false);
if (result is Order acmeOrder)
{
return acmeOrder;
}

throw new InvalidServerResponse("Invalid response from server during UpdateOrder.", responseText, order.Location.ToString());
throw new InvalidServerResponseException("UpdateOrder.", responseText, order.Location);
}

/// <summary>
Expand All @@ -279,13 +310,18 @@ public async Task<Order> UpdateOrderAsync(Order order, CancellationToken cancell
/// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="order"/>.Status isn't <see cref="Order.Valid"/></exception>
public async Task<X509Certificate2> GetCertificateAsync(Order order, CancellationToken cancellationToken = default)
{
if (order == null)
{
throw new ArgumentNullException(nameof(order));
}

if (order.Status != ACMEStatus.Valid)
{
var exception = new ArgumentOutOfRangeException(nameof(order.Status), "Order status is not in valid range.");
throw exception;
throw new ArgumentOutOfRangeException(nameof(order.Status), "Order status is not in valid range.");
}

var (result, responseText) = await client.GetAsync<string>(order.Certificate, cancellationToken);
var (result, _) = await client.GetAsync<string>(order.Certificate, cancellationToken)
.ConfigureAwait(false);
var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(result));
return certificate;
}
Expand All @@ -297,7 +333,8 @@ public async Task<X509Certificate2> GetCertificateAsync(Order order, Cancellatio
/// <returns><see cref="ACMEDirectory"/></returns>
public async Task<ACMEDirectory> GetDirectoryAsync(CancellationToken token = default)
{
Directory = await RequestDirectoryAsync(token).ConfigureAwait(false);
Directory = await RequestDirectoryAsync(token)
.ConfigureAwait(false);
return Directory;
}

Expand All @@ -323,27 +360,29 @@ public async Task RevokeCertificateAsync(X509Certificate certificate, Revocation
Certificate = Utilities.Base64UrlEncoded(certificate.GetRawCertData())
};

await client.PostAsync<string>(Directory.RevokeCertificate, revocationRequest, cancellationToken);
await client.PostAsync<string>(Directory.RevokeCertificate, revocationRequest, cancellationToken)
.ConfigureAwait(false);
}

private async Task<string> NewNonceAsync(CancellationToken token = default)
{
var response = await client.HeadAsync<string>(Directory.NewNonce, token);
nonce = response.response;
var response = await client.HeadAsync<string>(Directory.NewNonce, token)
.ConfigureAwait(false);
return response.response;
}

private async Task<ACMEDirectory> RequestDirectoryAsync(CancellationToken cancellationToken = default)
{
var uri = new Uri(endpoint, "directory");

var (result, text) = await client.GetAsync<ACMEDirectory>(uri, cancellationToken);
var (result, text) = await client.GetAsync<ACMEDirectory>(uri, cancellationToken)
.ConfigureAwait(false);
if (result is ACMEDirectory)
{
return result;
}

throw new InvalidServerResponse("Invalid response from server when requesting directory.", text, uri);
throw new InvalidServerResponseException("Invalid response from server when requesting directory.", text, uri);
}
}
}
2 changes: 1 addition & 1 deletion src/Libraries/ACMELib/ACMEEntities/Account.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
using System;
using Kenc.ACMELib.ACMEResponses;
using Kenc.ACMELib.JWS;
using Kenc.ACMELib.JsonWebSignature;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

Expand Down
6 changes: 3 additions & 3 deletions src/Libraries/ACMELib/ACMEEntities/ChallengeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
/// </summary>
public static class ChallengeType
{
public static string DNSChallenge = "dns";
public static string HttpChallenge = "http";
public static string TLSSNIChallenge = "tls-sni";
public static readonly string DNSChallenge = "dns";
public static readonly string HttpChallenge = "http";
public static readonly string TLSSNIChallenge = "tls-sni";
}
}
8 changes: 4 additions & 4 deletions src/Libraries/ACMELib/ACMEEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
/// </summary>
public static class ACMEEnvironment
{
public static string StagingV1 = "https://acme-staging.api.letsencrypt.org/directory";
public static string ProductionV1 = "https://acme-v01.api.letsencrypt.org/directory";
public const string StagingV1 = "https://acme-staging.api.letsencrypt.org/directory";
public const string ProductionV1 = "https://acme-v01.api.letsencrypt.org/directory";

public static string StagingV2 = "https://acme-staging-v02.api.letsencrypt.org/directory";
public static string ProductionV2 = "https://acme-v02.api.letsencrypt.org/directory";
public const string StagingV2 = "https://acme-staging-v02.api.letsencrypt.org/directory";
public const string ProductionV2 = "https://acme-v02.api.letsencrypt.org/directory";
}
}
Loading

0 comments on commit cbe59e5

Please sign in to comment.