Skip to content

Commit

Permalink
feature: create abstraction for SNI certificates in Kestrel
Browse files Browse the repository at this point in the history
  • Loading branch information
natemcmaster committed May 31, 2020
1 parent 1d8502c commit 239c503
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 6 deletions.
15 changes: 15 additions & 0 deletions LettuceEncrypt.sln
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LettuceEncrypt.Azure.UnitTe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeyVault", "samples\KeyVault\KeyVault.csproj", "{6143D097-6983-4A29-9DAE-1DD0EBC900D2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McMaster.AspNetCore.Kestrel.Certificates", "src\Kestrel.Certificates\McMaster.AspNetCore.Kestrel.Certificates.csproj", "{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -115,6 +117,18 @@ Global
{6143D097-6983-4A29-9DAE-1DD0EBC900D2}.Release|x64.Build.0 = Release|Any CPU
{6143D097-6983-4A29-9DAE-1DD0EBC900D2}.Release|x86.ActiveCfg = Release|Any CPU
{6143D097-6983-4A29-9DAE-1DD0EBC900D2}.Release|x86.Build.0 = Release|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Debug|x64.ActiveCfg = Debug|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Debug|x64.Build.0 = Debug|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Debug|x86.ActiveCfg = Debug|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Debug|x86.Build.0 = Debug|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Release|Any CPU.Build.0 = Release|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Release|x64.ActiveCfg = Release|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Release|x64.Build.0 = Release|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Release|x86.ActiveCfg = Release|Any CPU
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -126,6 +140,7 @@ Global
{3E8A37BC-D59C-497D-A3BF-2A9740F351C7} = {CC50E6BF-ECAC-4B32-9DDF-68E0C09DEA4A}
{5D2D6ACA-3EE3-42C8-84D1-D9B6ABE38343} = {F832E417-FA9F-4DB4-8E82-3D56054E47B1}
{6143D097-6983-4A29-9DAE-1DD0EBC900D2} = {C1D11C9E-C330-450D-9FD4-13A679A3D707}
{8131AFC9-ED23-4A3A-9D38-5200E4D8B168} = {CC50E6BF-ECAC-4B32-9DDF-68E0C09DEA4A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {EA50A4AB-48E4-4D43-8EFD-2BC849EA7AF8}
Expand Down
25 changes: 25 additions & 0 deletions src/Kestrel.Certificates/IServerCertificateSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Connections;

namespace McMaster.AspNetCore.Kestrel.Certificates
{
/// <summary>
/// Selects a certificate for incoming TLS connections.
/// </summary>
public interface IServerCertificateSelector
{
/// <summary>
/// <para>
/// A callback that will be invoked to dynamically select a server certificate.
/// If SNI is not available, then the domainName parameter will be null.
/// </para>
/// <para>
/// If the server certificate has an Extended Key Usage extension, the usages must include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
/// </para>
/// </summary>
X509Certificate2? Select(ConnectionContext context, string? domainName);
}
}
29 changes: 29 additions & 0 deletions src/Kestrel.Certificates/KestrelHttpsOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using McMaster.AspNetCore.Kestrel.Certificates;
using Microsoft.AspNetCore.Server.Kestrel.Https;

// ReSharper disable once CheckNamespace
namespace Microsoft.AspNetCore.Hosting
{
/// <summary>
/// API for configuring Kestrel certificiate options
/// </summary>
public static class KestrelHttpsOptionsExtensions
{
/// <summary>
/// Configure HTTPS certificates dynamically with an implementation of <see cref="IServerCertificateSelector"/>.
/// </summary>
/// <param name="httpsOptions">The HTTPS configuration</param>
/// <param name="certificateSelector">The server certificate selector.</param>
/// <returns>The HTTPS configuration</returns>
public static HttpsConnectionAdapterOptions UseServerCertificateSelector(
this HttpsConnectionAdapterOptions httpsOptions,
IServerCertificateSelector certificateSelector)
{
httpsOptions.ServerCertificateSelector = certificateSelector.Select;
return httpsOptions;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.0;netstandard2.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>
<Description>A class library for managing HTTPS certificates with ASP.NET Core.</Description>
<VersionPrefix>1.0.0</VersionPrefix>
<PackageVersion>$(VersionPrefix)</PackageVersion>
<PackageVersion Condition="'$(IncludePreReleaseLabelInPackageVersion)' == 'true'">$(PackageVersion)-$(VersionSuffix)</PackageVersion>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.1.2" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions src/Kestrel.Certificates/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
5 changes: 5 additions & 0 deletions src/Kestrel.Certificates/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#nullable enable
McMaster.AspNetCore.Kestrel.Certificates.IServerCertificateSelector
McMaster.AspNetCore.Kestrel.Certificates.IServerCertificateSelector.Select(Microsoft.AspNetCore.Connections.ConnectionContext! context, string? domainName) -> System.Security.Cryptography.X509Certificates.X509Certificate2?
Microsoft.AspNetCore.Hosting.KestrelHttpsOptionsExtensions
static Microsoft.AspNetCore.Hosting.KestrelHttpsOptionsExtensions.UseServerCertificateSelector(this Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions! httpsOptions, McMaster.AspNetCore.Kestrel.Certificates.IServerCertificateSelector! certificateSelector) -> Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions!
3 changes: 2 additions & 1 deletion src/LettuceEncrypt/Internal/CertificateSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using McMaster.AspNetCore.Kestrel.Certificates;
using Microsoft.AspNetCore.Connections;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace LettuceEncrypt.Internal
{
internal class CertificateSelector
internal class CertificateSelector : IServerCertificateSelector
{
private readonly ConcurrentDictionary<string, X509Certificate2> _certs =
new ConcurrentDictionary<string, X509Certificate2>(StringComparer.OrdinalIgnoreCase);
Expand Down
8 changes: 5 additions & 3 deletions src/LettuceEncrypt/Internal/KestrelOptionsSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using McMaster.AspNetCore.Kestrel.Certificates;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Options;

namespace LettuceEncrypt.Internal
{
internal class KestrelOptionsSetup : IConfigureOptions<KestrelServerOptions>
{
private readonly CertificateSelector _certificateSelector;
private readonly IServerCertificateSelector _certificateSelector;
private readonly TlsAlpnChallengeResponder _tlsAlpnChallengeResponder;

public KestrelOptionsSetup(CertificateSelector certificateSelector, TlsAlpnChallengeResponder tlsAlpnChallengeResponder)
public KestrelOptionsSetup(IServerCertificateSelector certificateSelector, TlsAlpnChallengeResponder tlsAlpnChallengeResponder)
{
_certificateSelector = certificateSelector ?? throw new ArgumentNullException(nameof(certificateSelector));
_tlsAlpnChallengeResponder = tlsAlpnChallengeResponder ?? throw new ArgumentNullException(nameof(tlsAlpnChallengeResponder));
Expand All @@ -28,7 +30,7 @@ public void Configure(KestrelServerOptions options)
#else
#error Update TFMs
#endif
o.ServerCertificateSelector = _certificateSelector.Select;
o.UseServerCertificateSelector(_certificateSelector);
});
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/LettuceEncrypt/LettuceEncrypt.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ This only works with Kestrel, which is the default server configuration for ASP.
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.1.1" />
<PackageReference Include="System.Text.Json" Version="4.7.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Kestrel.Certificates\McMaster.AspNetCore.Kestrel.Certificates.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using LettuceEncrypt.Acme;
using LettuceEncrypt.Internal;
using LettuceEncrypt.Internal.IO;
using McMaster.AspNetCore.Kestrel.Certificates;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -44,7 +45,9 @@ public static ILettuceEncryptServiceBuilder AddLettuceEncrypt(this IServiceColle

services.TryAddSingleton<ICertificateAuthorityConfiguration, DefaultCertificateAuthorityConfiguration>();

services.AddSingleton<CertificateSelector>()
services
.AddSingleton<CertificateSelector>()
.AddSingleton<IServerCertificateSelector>(s => s.GetRequiredService<CertificateSelector>())
.AddSingleton<IConsole>(PhysicalConsole.Singleton)
.AddSingleton<IClock, SystemClock>()
.AddSingleton<TermsOfServiceChecker>()
Expand Down
39 changes: 39 additions & 0 deletions test/LettuceEncrypt.UnitTests/KestrelOptionsSetupTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;

namespace LettuceEncrypt.UnitTests
{
public class KestrelOptionsSetupTests
{
[Fact]
public void ItSetsCertificateSelector()
{
var services = new ServiceCollection()
.AddLogging()
.AddLettuceEncrypt()
.Services
.BuildServiceProvider(validateScopes: true);

var kestrelOptions = services.GetRequiredService<IOptions<KestrelServerOptions>>().Value;
// reflection is gross, but there is no public API for this so (shrug)
var httpsDefaultsProp =
typeof(KestrelServerOptions).GetProperty("HttpsDefaults",
BindingFlags.Instance | BindingFlags.NonPublic);
var httpsDefaultsFunc =
(Action<HttpsConnectionAdapterOptions>) httpsDefaultsProp.GetMethod.Invoke(kestrelOptions,
Array.Empty<object>());
var httpsDefaults = new HttpsConnectionAdapterOptions();

Assert.Null(httpsDefaults.ServerCertificateSelector);

httpsDefaultsFunc(httpsDefaults);

Assert.NotNull(httpsDefaults.ServerCertificateSelector);
}
}
}

0 comments on commit 239c503

Please sign in to comment.