Skip to content

Commit

Permalink
[wip] support tls-alpn-01 (#124)
Browse files Browse the repository at this point in the history
* add `tls-alpn-01` challenge type
* add helper to generate validation certificate for `tls-alpn-01` challenge
  • Loading branch information
fszlin authored Jun 12, 2018
1 parent 45285d8 commit b5171d7
Show file tree
Hide file tree
Showing 17 changed files with 447 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 2.2.3.{build}
version: 2.3.0.{build}
build:
verbosity: minimal
project: Certes.sln
Expand Down
8 changes: 8 additions & 0 deletions docs/APIv2.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Retrieve challenges of the authorzation.
var challenges = await authz.Challenges();
var dnsChallenge = await authz.Dns();
var httpChallenge = await authz.Http();
var tlsAlpnChallenge = await authz.TlsAlpn();
```

Create the respone file for provisioning to `/.well-know/acme-challenge/`.
Expand All @@ -186,6 +187,13 @@ Compute the value for DNS TXT record.
var dnsTxt = context.AccountKey.DnsTxt(challenge.Token);
```

Generate certificate with X509 ACME validation extension.

```C#
var alpnCertKey = KeyFactory.NewKey(KeyAlgorithm.ES256);
var alpnCert = context.AccountKey.TlsAlpnCertificate(challenge.Token, "www.my-domain.com", alpnCertKey);
```

Let the ACME server to validate the challenge once it is ready.

```C#
Expand Down
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
All notable changes to this project will be documented in this file.

## [Unreleased]
### Added
- Support `tls-alpn-01` challenge ([#125][i125])

## [2.2.2] - 2018-05-31
### Changed
Expand Down Expand Up @@ -93,3 +95,4 @@ All notable changes to this project will be documented in this file.
[i100]: https://github.com/fszlin/certes/issues/100
[i106]: https://github.com/fszlin/certes/issues/106
[i112]: https://github.com/fszlin/certes/issues/112
[i125]: https://github.com/fszlin/certes/issues/125
8 changes: 8 additions & 0 deletions src/Certes/Acme/Resource/ChallengeTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,13 @@ public static class ChallengeTypes
/// The dns-01 challenge.
/// </summary>
public const string Dns01 = "dns-01";

/// <summary>
/// Gets the tls-alpn-01 challenge name.
/// </summary>
/// <value>
/// The tls-alpn-01 challenge name.
/// </value>
public static string TlsAlpn01 { get; } = "tls-alpn-01";
}
}
2 changes: 1 addition & 1 deletion src/Certes/Certes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard1.3;net45;net47</TargetFrameworks>
<AssemblyVersion>2.2.3</AssemblyVersion>
<AssemblyVersion>2.3.0</AssemblyVersion>
<Version>$(AssemblyVersion)$(CertesPackageVersionSuffix)</Version>
<FileVersion>$(AssemblyVersion)$(CertesFileVersionSuffix)</FileVersion>
<InformationalVersion>$(AssemblyVersion)$(CertesInformationalVersionSuffix)</InformationalVersion>
Expand Down
16 changes: 12 additions & 4 deletions src/Certes/Extensions/IAuthorizationContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,24 @@ public static class IAuthorizationContextExtensions
/// </summary>
/// <param name="authorizationContext">The authorization context.</param>
/// <returns>The HTTP challenge, <c>null</c> if no HTTP challenge available.</returns>
public static Task<IChallengeContext> Http(this IAuthorizationContext authorizationContext)
=> authorizationContext.Challenge(ChallengeTypes.Http01);
public static Task<IChallengeContext> Http(this IAuthorizationContext authorizationContext) =>
authorizationContext.Challenge(ChallengeTypes.Http01);

/// <summary>
/// Gets the DNS challenge.
/// </summary>
/// <param name="authorizationContext">The authorization context.</param>
/// <returns>The DNS challenge, <c>null</c> if no DNS challenge available.</returns>
public static Task<IChallengeContext> Dns(this IAuthorizationContext authorizationContext)
=> authorizationContext.Challenge(ChallengeTypes.Dns01);
public static Task<IChallengeContext> Dns(this IAuthorizationContext authorizationContext) =>
authorizationContext.Challenge(ChallengeTypes.Dns01);

/// <summary>
/// Gets the TLS ALPN challenge.
/// </summary>
/// <param name="authorizationContext">The authorization context.</param>
/// <returns>The TLS ALPN challenge, <c>null</c> if no TLS ALPN challenge available.</returns>
public static Task<IChallengeContext> TlsAlpn(this IAuthorizationContext authorizationContext) =>
authorizationContext.Challenge(ChallengeTypes.TlsAlpn01);

/// <summary>
/// Gets a challenge by type.
Expand Down
67 changes: 63 additions & 4 deletions src/Certes/IKey.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
using System.Text;
using System;
using System.IO;
using System.Text;
using Certes.Acme.Resource;
using Certes.Crypto;
using Certes.Json;
using Certes.Jws;
using Newtonsoft.Json;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;

namespace Certes
{
Expand All @@ -17,7 +27,7 @@ public interface IKey : IEncodable
/// <value>
/// The algorithm.
/// </value>
KeyAlgorithm Algorithm { get;}
KeyAlgorithm Algorithm { get; }

/// <summary>
/// Gets the json web key.
Expand All @@ -33,6 +43,8 @@ public interface IKey : IEncodable
/// </summary>
public static class ISignatureKeyExtensions
{
private static readonly DerObjectIdentifier acmeValidationV1Id = new DerObjectIdentifier("1.3.6.1.5.5.7.1.30.1");
private static readonly KeyAlgorithmProvider signatureAlgorithmProvider = new KeyAlgorithmProvider();
private static readonly JsonSerializerSettings thumbprintSettings = JsonUtil.CreateSettings();

/// <summary>
Expand Down Expand Up @@ -66,7 +78,7 @@ public static string Thumbprint(this IKey key)
/// </summary>
/// <param name="key">The key.</param>
/// <param name="token">The challenge token.</param>
/// <returns></returns>
/// <returns>The key authorization string.</returns>
public static string KeyAuthorization(this IKey key, string token)
{
var jwkThumbprintEncoded = key.Thumbprint();
Expand All @@ -78,12 +90,59 @@ public static string KeyAuthorization(this IKey key, string token)
/// </summary>
/// <param name="key">The key.</param>
/// <param name="token">The challenge token.</param>
/// <returns></returns>
/// <returns>The DNS text value for dns-01 validation.</returns>
public static string DnsTxt(this IKey key, string token)
{
var keyAuthz = key.KeyAuthorization(token);
var hashed = DigestUtilities.CalculateDigest("SHA256", Encoding.UTF8.GetBytes(keyAuthz));
return JwsConvert.ToBase64String(hashed);
}

/// <summary>
/// Generates the certificate for <see cref="ChallengeTypes.TlsAlpn01" /> validation.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="token">The <see cref="ChallengeTypes.TlsAlpn01" /> token.</param>
/// <param name="subjectName">Name of the subject.</param>
/// <param name="certificateKey">The certificate key pair.</param>
/// <returns>The tls-alpn-01 certificate in PEM.</returns>
public static string TlsAlpnCertificate(this IKey key, string token, string subjectName, IKey certificateKey)
{
var keyAuthz = key.KeyAuthorization(token);
var hashed = DigestUtilities.CalculateDigest("SHA256", Encoding.UTF8.GetBytes(keyAuthz));

var (_, keyPair) = signatureAlgorithmProvider.GetKeyPair(certificateKey.ToDer());

var signatureFactory = new Asn1SignatureFactory(certificateKey.Algorithm.ToPkcsObjectId(), keyPair.Private, new SecureRandom());
var gen = new X509V3CertificateGenerator();
var certName = new X509Name($"CN={subjectName}");
var serialNo = BigInteger.ProbablePrime(120, new SecureRandom());

gen.SetSerialNumber(serialNo);
gen.SetSubjectDN(certName);
gen.SetIssuerDN(certName);
gen.SetNotBefore(DateTime.UtcNow);
gen.SetNotAfter(DateTime.UtcNow.AddDays(7));
gen.SetPublicKey(keyPair.Public);

// SAN for validation
var gns = new[] { new GeneralName(GeneralName.DnsName, subjectName) };
gen.AddExtension(X509Extensions.SubjectAlternativeName.Id, false, new GeneralNames(gns));

// ACME-TLS/1
gen.AddExtension(
acmeValidationV1Id,
true,
hashed);

var newCert = gen.Generate(signatureFactory);

using (var sr = new StringWriter())
{
var pemWriter = new PemWriter(sr);
pemWriter.WriteObject(newCert);
return sr.ToString();
}
}
}
}
2 changes: 1 addition & 1 deletion src/Certes/Pkcs/PfxBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public byte[] Build(string friendlyName, string password)
var entry = new X509CertificateEntry(certificate);
store.SetCertificateEntry(friendlyName, entry);

if (FullChain)
if (FullChain && !certificate.IssuerDN.Equivalent(certificate.SubjectDN))
{
var certChain = FindIssuers();
var certChainEntries = certChain.Select(c => new X509CertificateEntry(c)).ToList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public async Task CanRunAccountFlows()

var ctx = new AcmeContext(dirUri, http: GetAcmeHttpClient(dirUri));
var accountCtx = await ctx.NewAccount(
new[] { $"mailto:certes-{DateTime.UtcNow.Ticks}@example.com" }, true);
new[] { $"mailto:certes-{DateTime.UtcNow.Ticks}@certes.app" }, true);
var account = await accountCtx.Resource();
var location = accountCtx.Location;

Assert.NotNull(account);
Assert.Equal(AccountStatus.Valid, account.Status);

await accountCtx.Update(agreeTermsOfService: true);
await accountCtx.Update(contact: new[] { $"mailto:certes-{DateTime.UtcNow.Ticks}@example.com" });
await accountCtx.Update(contact: new[] { $"mailto:certes-{DateTime.UtcNow.Ticks}@certes.app" });

account = await accountCtx.Deactivate();
Assert.NotNull(account);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public async Task CanGenerateCertificateHttp()
var order = await orderCtx.Resource();
Assert.NotNull(order);
Assert.Equal(hosts.Length, order.Authorizations?.Count);
Assert.True(OrderStatus.Pending == order.Status || OrderStatus.Processing == order.Status);
Assert.True(OrderStatus.Pending == order.Status || OrderStatus.Ready == order.Status || OrderStatus.Processing == order.Status);

var authrizations = await orderCtx.Authorizations();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Certes.Acme;
using Certes.Acme.Resource;
using Certes.Json;
using Newtonsoft.Json;
using Org.BouncyCastle.X509;
using Xunit;
using Xunit.Abstractions;

using static Certes.Helper;
using static Certes.IntegrationHelper;

namespace Certes
{
public partial class AcmeContextIntegration
{
public class CertificateByTlsAlpnTests : AcmeContextIntegration
{
public CertificateByTlsAlpnTests(ITestOutputHelper output)
: base(output)
{
}

[Fact]
public async Task CanGenerateCertificateTlsAlpn()
{
var dirUri = await GetAcmeUriV2();
var hosts = new[] { $"{DomainSuffix}.tls-alpn.certes-ci.dymetis.com" };
var ctx = new AcmeContext(dirUri, GetKeyV2(), http: GetAcmeHttpClient(dirUri));
var orderCtx = await ctx.NewOrder(hosts);
var order = await orderCtx.Resource();
Assert.NotNull(order);
Assert.Equal(hosts.Length, order.Authorizations?.Count);
Assert.True(
OrderStatus.Ready == order.Status || OrderStatus.Pending == order.Status || OrderStatus.Processing == order.Status,
$"Invalid order status: {order.Status}");

var authrizations = await orderCtx.Authorizations();

foreach (var authzCtx in authrizations)
{
var authz = await authzCtx.Resource();

var tlsAlpnChallenge = await authzCtx.TlsAlpn();
var alpnCertKey = KeyFactory.NewKey(KeyAlgorithm.ES256);
var alpnCert = ctx.AccountKey.TlsAlpnCertificate(tlsAlpnChallenge.Token, authz.Identifier.Value, alpnCertKey);

await SetupValidationResponder(authz, alpnCert, alpnCertKey);
await tlsAlpnChallenge.Validate();
}

while (true)
{
await Task.Delay(100);

var statuses = new List<AuthorizationStatus>();
foreach (var authz in authrizations)
{
var a = await authz.Resource();
statuses.Add(a.Status ?? AuthorizationStatus.Pending);
}

if (statuses.All(s => s == AuthorizationStatus.Valid || s == AuthorizationStatus.Invalid))
{
break;
}
}

var certKey = KeyFactory.NewKey(KeyAlgorithm.RS256);
var finalizedOrder = await orderCtx.Finalize(new CsrInfo
{
CountryName = "CA",
State = "Ontario",
Locality = "Toronto",
Organization = "Certes",
OrganizationUnit = "Dev",
CommonName = hosts[0],
}, certKey);
var certChain = await orderCtx.Download();

var pfxBuilder = certChain.ToPfx(certKey);
pfxBuilder.AddIssuers(TestCertificates);

var pfx = pfxBuilder.Build("my-pfx", "abcd1234");

// revoke certificate
var certParser = new X509CertificateParser();
var certificate = certParser.ReadCertificate(certChain.Certificate.ToDer());
var der = certificate.GetEncoded();

await ctx.RevokeCertificate(der, RevocationReason.Unspecified, null);

// deactivate authz so the subsequence can trigger challenge validation
foreach (var authz in authrizations)
{
var authzRes = await authz.Deactivate();
Assert.Equal(AuthorizationStatus.Deactivated, authzRes.Status);
}
}

private static async Task SetupValidationResponder(Authorization authz, string alpnCert, IKey certKey)
{
// setup validation certificate
var certC = new CertificateChain(alpnCert);
var json = JsonConvert.SerializeObject(new
{
Cert = certC.Certificate.ToDer(),
Key = certKey.ToDer(),
}, JsonUtil.CreateSettings());

using (var resp = await http.Value.PostAsync(
$"https://{authz.Identifier.Value}/tls-alpn-01/",
new StringContent(json, Encoding.UTF8, "application/json")))
{
Assert.Equal(authz.Identifier.Value, await resp.Content.ReadAsStringAsync());
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public async Task CanChangeAccountKey()

var ctx = new AcmeContext(dirUri, http: GetAcmeHttpClient(dirUri));
var account = await ctx.NewAccount(
new[] { $"mailto:certes-{DateTime.UtcNow.Ticks}@example.com" }, true);
new[] { $"mailto:certes-{DateTime.UtcNow.Ticks}@certes.app" }, true);
var location = await ctx.Account().Location();

var newKey = KeyFactory.NewKey(KeyAlgorithm.ES256);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Microsoft.CodeCoverage" Version="1.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="Moq" Version="4.8.2" />
<PackageReference Include="Moq" Version="4.8.3" />
<PackageReference Include="NLog" Version="4.5.6" />
<PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="System.CommandLine" Version="0.1.0-*" Condition="'$(IncludeCli)' == 'True'" />
Expand Down
Loading

0 comments on commit b5171d7

Please sign in to comment.