Update "temporary" OpenSSL wrapper to support 1.1, and add HTTP/2 sample with docker (#2149)

This commit is contained in:
Andrew Stanton-Nurse 2017-11-13 15:54:16 -08:00 committed by GitHub
parent 73a37363e1
commit 065e9bb57a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 290 additions and 29 deletions

View File

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.16
VisualStudioVersion = 15.0.27101.0
MinimumVisualStudioVersion = 15.0.26730.03
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7972A5D6-3385-4127-9277-428506DD44FF}"
ProjectSection(SolutionItems) = preProject
@ -121,6 +121,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kestrel.Transport.Libuv.Fun
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kestrel.Transport.Sockets.FunctionalTests", "test\Kestrel.Transport.Sockets.FunctionalTests\Kestrel.Transport.Sockets.FunctionalTests.csproj", "{9C7B6B5F-088A-436E-834B-6373EA36DEEE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Http2SampleApp", "samples\Http2SampleApp\Http2SampleApp.csproj", "{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -335,6 +337,18 @@ Global
{9C7B6B5F-088A-436E-834B-6373EA36DEEE}.Release|x64.Build.0 = Release|Any CPU
{9C7B6B5F-088A-436E-834B-6373EA36DEEE}.Release|x86.ActiveCfg = Release|Any CPU
{9C7B6B5F-088A-436E-834B-6373EA36DEEE}.Release|x86.Build.0 = Release|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Debug|x64.ActiveCfg = Debug|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Debug|x64.Build.0 = Debug|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Debug|x86.ActiveCfg = Debug|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Debug|x86.Build.0 = Debug|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Release|Any CPU.Build.0 = Release|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Release|x64.ActiveCfg = Release|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Release|x64.Build.0 = Release|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Release|x86.ActiveCfg = Release|Any CPU
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -359,6 +373,7 @@ Global
{B7B0EA74-528F-46B8-9BC4-909D9A67C194} = {D3273454-EA07-41D2-BF0B-FCC3675C2483}
{74032D79-8EA7-4483-BD82-C38370420FFF} = {D3273454-EA07-41D2-BF0B-FCC3675C2483}
{9C7B6B5F-088A-436E-834B-6373EA36DEEE} = {D3273454-EA07-41D2-BF0B-FCC3675C2483}
{7BC22A4A-15D2-44C2-AB45-049F0FB562FA} = {8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2D10D020-6770-47CA-BB8D-2C23FE3AE071}

View File

@ -0,0 +1,14 @@
FROM microsoft/aspnetcore:2.0.0-stretch
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libssl-dev && \
rm -rf /var/lib/apt/lists/*
ARG CONFIGURATION=Debug
WORKDIR /app
COPY ./bin/${CONFIGURATION}/netcoreapp2.0/publish/ /app
ENTRYPOINT [ "/usr/bin/dotnet", "/app/Http2SampleApp.dll" ]

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Kestrel\Kestrel.csproj" />
<ProjectReference Include="..\..\src\Kestrel.Tls\Kestrel.Tls.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
</ItemGroup>
<ItemGroup>
<Content Include="../../test/shared/TestCertificates/testCert.pfx" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,50 @@
using System.Globalization;
using System.IO;
using System.Net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Http2SampleApp
{
public class Program
{
public static void Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
if (!ushort.TryParse(configuration["BASE_PORT"], NumberStyles.None, CultureInfo.InvariantCulture, out var basePort))
{
basePort = 5000;
}
var hostBuilder = new WebHostBuilder()
.ConfigureLogging((_, factory) =>
{
// Set logging to the MAX.
factory.SetMinimumLevel(LogLevel.Trace);
factory.AddConsole();
})
.UseKestrel(options =>
{
// Run callbacks on the transport thread
options.ApplicationSchedulingMode = SchedulingMode.Inline;
options.Listen(IPAddress.Any, basePort, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
listenOptions.UseTls("testCert.pfx", "testPassword");
listenOptions.UseConnectionLogging();
});
})
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>();
hostBuilder.Build().Run();
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Http2SampleApp
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
}
}

View File

@ -0,0 +1,3 @@
dotnet publish --framework netcoreapp2.0 "$PSScriptRoot/../Http2SampleApp.csproj"
docker build -t kestrel-http2-sample (Convert-Path "$PSScriptRoot/..")

View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
dotnet publish --framework netcoreapp2.0 "$DIR/../Http2SampleApp.csproj"
docker build -t kestrel-http2-sample "$DIR/.."

View File

@ -0,0 +1 @@
docker run -p 5000:5000 -it --rm kestrel-http2-sample

View File

@ -0,0 +1,2 @@
#!/usr/bin/env bash
docker run -it -p 5000:5000 --rm kestrel-http2-sample

View File

@ -6,10 +6,6 @@
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
</PropertyGroup>
<ItemGroup>
<Content Include="testCert.pfx" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Kestrel\Kestrel.csproj" />
<ProjectReference Include="..\..\src\Kestrel.Https\Kestrel.Https.csproj" />
@ -19,4 +15,8 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
</ItemGroup>
<ItemGroup>
<Content Include="../../test/shared/TestCertificates/testCert.pfx" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

Binary file not shown.

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
@ -40,5 +41,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Payload.Slice(Length - padLength.Value).Fill(0);
}
}
private void DataTraceFrame(ILogger logger)
{
logger.LogTrace("'DATA' Frame. Flags = {DataFlags}, PadLength = {PadLength}, PayloadLength = {PayloadLength}", DataFlags, DataPadLength, DataPayload.Count);
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{

View File

@ -10,12 +10,12 @@ namespace Microsoft.AspNetCore.Hosting
{
public static class ListenOptionsTlsExtensions
{
public static ListenOptions UseTls(this ListenOptions listenOptions, string certificatePath, string privateKeyPath)
public static ListenOptions UseTls(this ListenOptions listenOptions, string certificatePath, string password)
{
return listenOptions.UseTls(new TlsConnectionAdapterOptions
{
CertificatePath = certificatePath,
PrivateKeyPath = privateKeyPath,
Password = password,
Protocols = listenOptions.Protocols
});
}

View File

@ -15,23 +15,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls
public const int OPENSSL_NPN_NEGOTIATED = 1;
public const int SSL_TLSEXT_ERR_OK = 0;
public const int SSL_TLSEXT_ERR_NOACK = 3;
public const int SSL_CTRL_CHAIN = 88;
private const int BIO_C_SET_BUF_MEM_EOF_RETURN = 130;
private const int SSL_CTRL_SET_ECDH_AUTO = 94;
public static int SSL_library_init()
public static void SSL_library_init()
{
return NativeMethods.SSL_library_init();
try
{
// Try OpenSSL 1.0.2
NativeMethods.SSL_library_init();
}
catch (EntryPointNotFoundException)
{
// It's fine, OpenSSL 1.1 doesn't need initialization
}
}
public static void SSL_load_error_strings()
{
NativeMethods.SSL_load_error_strings();
try
{
NativeMethods.SSL_load_error_strings();
}
catch (EntryPointNotFoundException)
{
// Also fine, OpenSSL 1.1 doesn't need it.
}
}
public static void OpenSSL_add_all_algorithms()
{
NativeMethods.OPENSSL_add_all_algorithms_noconf();
try
{
NativeMethods.OPENSSL_add_all_algorithms_noconf();
}
catch (EntryPointNotFoundException)
{
// Also fine, OpenSSL 1.1 doesn't need it.
}
}
public static IntPtr TLSv1_2_method()
@ -49,6 +72,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls
NativeMethods.SSL_CTX_free(ctx);
}
public unsafe static int SSL_CTX_Set_Pfx(IntPtr ctx, string path, string password)
{
var pass = Marshal.StringToHGlobalAnsi(password);
var key = IntPtr.Zero;
var cert = IntPtr.Zero;
var ca = IntPtr.Zero;
try
{
var file = System.IO.File.ReadAllBytes(path);
fixed (void* f = file)
{
var buffer = (IntPtr)f;
var pkcs = NativeMethods.d2i_PKCS12(IntPtr.Zero, ref buffer, file.Length);
var result = NativeMethods.PKCS12_parse(pkcs, pass, ref key, ref cert, ref ca);
if (result != 1)
{
return -1;
}
if (NativeMethods.SSL_CTX_use_certificate(ctx, cert) != 1) return -1;
if (NativeMethods.SSL_CTX_use_PrivateKey(ctx, key) != 1) return -1;
if (NativeMethods.SSL_CTX_ctrl(ctx, SSL_CTRL_CHAIN, 1, ca) != 1) return -1;
return 1;
}
}
finally
{
Marshal.FreeHGlobal(pass);
if (key != IntPtr.Zero) NativeMethods.EVP_PKEY_free(key);
if (cert != IntPtr.Zero) NativeMethods.X509_free(cert);
if (ca != IntPtr.Zero) NativeMethods.sk_X509_pop_free(ca);
}
}
public static int SSL_CTX_set_ecdh_auto(IntPtr ctx, int onoff)
{
return (int)NativeMethods.SSL_CTX_ctrl(ctx, SSL_CTRL_SET_ECDH_AUTO, onoff, IntPtr.Zero);
@ -109,6 +167,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls
return NativeMethods.SSL_get_error(ssl, ret);
}
public static int ERR_get_error()
{
return NativeMethods.ERR_get_error();
}
public static string ERR_error_string(int error)
{
var buf = NativeMethods.ERR_error_string(error, IntPtr.Zero);
// Don't free the buffer! It's a static buffer
return Marshal.PtrToStringAnsi(buf);
}
public static void SSL_set_accept_state(IntPtr ssl)
{
NativeMethods.SSL_set_accept_state(ssl);
@ -263,6 +334,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern void ERR_load_BIO_strings();
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern int ERR_get_error();
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr ERR_error_string(int error, IntPtr buf);
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr d2i_PKCS12(IntPtr unsused, ref IntPtr bufferPointer, long length);
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern int PKCS12_parse(IntPtr p12, IntPtr pass, ref IntPtr pkey, ref IntPtr cert, ref IntPtr ca);
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern void PKCS12_free(IntPtr p12);
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern void EVP_PKEY_free(IntPtr pkey);
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern void X509_free(IntPtr a);
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern void sk_X509_pop_free(IntPtr ca);
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern int SSL_CTX_ctrl(IntPtr ctx, int cmd, int larg, IntPtr parg);
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern int SSL_CTX_set1_chain(IntPtr ctx, IntPtr sk);
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern int SSL_CTX_use_certificate(IntPtr ctx, IntPtr x509);
[DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
public static extern int SSL_CTX_use_PrivateKey(IntPtr ctx, IntPtr pkey);
}
}
}

View File

@ -40,9 +40,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls
throw new ArgumentException("Certificate path must be non-null.", nameof(options));
}
if (options.PrivateKeyPath == null)
if (options.Password == null)
{
throw new ArgumentException("Private key path must be non-null.", nameof(options));
throw new ArgumentException("Password must be non-null.", nameof(options));
}
_options = options;
@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls
private async Task<IAdaptedConnection> InnerOnConnectionAsync(ConnectionAdapterContext context)
{
var tlsStream = new TlsStream(context.ConnectionStream, _options.CertificatePath, _options.PrivateKeyPath, _serverProtocols);
var tlsStream = new TlsStream(context.ConnectionStream, _options.CertificatePath, _options.Password, _serverProtocols);
try
{

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls
{
public string CertificatePath { get; set; } = string.Empty;
public string PrivateKeyPath { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public HttpProtocols Protocols { get; set; }
}

View File

@ -9,11 +9,15 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Tls
{
public class TlsStream : Stream
{
// Error code that indicates that a handshake failed because unencrypted HTTP was sent
private const int SSL_ERROR_HTTP_REQUEST = 336130204;
private static unsafe OpenSsl.alpn_select_cb_t _alpnSelectCallback = AlpnSelectCallback;
private readonly Stream _innerStream;
@ -36,7 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls
OpenSsl.OpenSSL_add_all_algorithms();
}
public TlsStream(Stream innerStream, string certificatePath, string privateKeyPath, IEnumerable<string> protocols)
public TlsStream(Stream innerStream, string certificatePath, string password, IEnumerable<string> protocols)
{
_innerStream = innerStream;
_protocols = ToWireFormat(protocols);
@ -49,18 +53,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls
throw new Exception("Unable to create SSL context.");
}
if(OpenSsl.SSL_CTX_Set_Pfx(_ctx, certificatePath, password) != 1)
{
throw new InvalidOperationException("Unable to load PFX");
}
OpenSsl.SSL_CTX_set_ecdh_auto(_ctx, 1);
if (OpenSsl.SSL_CTX_use_certificate_file(_ctx, certificatePath, 1) != 1)
{
throw new Exception("Unable to load certificate file.");
}
if (OpenSsl.SSL_CTX_use_PrivateKey_file(_ctx, privateKeyPath, 1) != 1)
{
throw new Exception("Unable to load private key file.");
}
OpenSsl.SSL_CTX_set_alpn_select_cb(_ctx, _alpnSelectCallback, GCHandle.ToIntPtr(_protocolsHandle));
_ssl = OpenSsl.SSL_new(_ctx);
@ -181,9 +180,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls
{
var error = OpenSsl.SSL_get_error(_ssl, ret);
if (error != 2)
if (error == 1)
{
throw new IOException($"TLS handshake failed: {nameof(OpenSsl.SSL_do_handshake)} error {error}.");
// SSL error, get it from the OpenSSL error queue
error = OpenSsl.ERR_get_error();
if (error == SSL_ERROR_HTTP_REQUEST)
{
throw new InvalidOperationException("Unencrypted HTTP traffic was sent to an HTTPS endpoint");
}
var errorString = OpenSsl.ERR_error_string(error);
throw new IOException($"TLS handshake failed: {nameof(OpenSsl.SSL_do_handshake)} error {error}. {errorString}");
}
}