From dca6e884ce8f9363029f9dfd1a68f02882c5394b Mon Sep 17 00:00:00 2001 From: Jan Tattermusch <jtattermusch@google.com> Date: Wed, 22 Apr 2015 16:56:27 -0700 Subject: [PATCH] First attempt to add service account creds interop test --- src/csharp/Grpc.Auth/GoogleCredential.cs | 50 +++++++++++-------- src/csharp/Grpc.Auth/Grpc.Auth.csproj | 48 +++++++++++++++++- .../Grpc.Auth/OAuth2InterceptorFactory.cs | 6 +-- src/csharp/Grpc.Auth/app.config | 15 ++++++ src/csharp/Grpc.Auth/packages.config | 12 +++++ src/csharp/Grpc.Core/Grpc.Core.csproj | 3 +- .../Grpc.IntegrationTesting.Client/app.config | 4 ++ .../Grpc.IntegrationTesting.Server/app.config | 4 ++ .../Grpc.IntegrationTesting.csproj | 21 ++++---- .../Grpc.IntegrationTesting/InteropClient.cs | 47 ++++++++++++++++- src/csharp/Grpc.IntegrationTesting/app.config | 4 ++ 11 files changed, 173 insertions(+), 41 deletions(-) create mode 100644 src/csharp/Grpc.Auth/app.config create mode 100644 src/csharp/Grpc.Auth/packages.config diff --git a/src/csharp/Grpc.Auth/GoogleCredential.cs b/src/csharp/Grpc.Auth/GoogleCredential.cs index d66952a901..7385a26485 100644 --- a/src/csharp/Grpc.Auth/GoogleCredential.cs +++ b/src/csharp/Grpc.Auth/GoogleCredential.cs @@ -41,6 +41,11 @@ using Grpc.Core.Utils; using Google.Apis.Auth.OAuth2; using System.Security.Cryptography.X509Certificates; +using Newtonsoft.Json.Linq; +using Mono.Security.Cryptography; +using Org.BouncyCastle.Crypto.Parameters; +using System.Security.Cryptography; +using Org.BouncyCastle.Security; namespace Grpc.Auth { @@ -53,6 +58,8 @@ namespace Grpc.Auth public class GoogleCredential { private const string GoogleApplicationCredentialsEnvName = "GOOGLE_APPLICATION_CREDENTIALS"; + private const string ClientEmailFieldName = "client_email"; + private const string PrivateKeyFieldName = "private_key"; private ServiceCredential credential; @@ -76,31 +83,20 @@ namespace Grpc.Auth public GoogleCredential CreateScoped(IEnumerable<string> scopes) { + // TODO(jtattermusch): also support compute credential. + var credsPath = Environment.GetEnvironmentVariable(GoogleApplicationCredentialsEnvName); - // TODO: also support compute credential. - - //var credsPath = Environment.GetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS"); - //var credsPath = "/usr/local/google/home/jtattermusch/certs/service_account/stubbyCloudTestingTest-7dd63462c60c.json"; - - //JObject o1 = JObject.Parse(File.ReadAllText(credsPath)); - //string privateKey = o1.GetValue("private_key").Value<string>(); - //Console.WriteLine(privateKey); - - //var certificate = new X509Certificate2(System.Text.Encoding.UTF8.GetBytes(privateKey), "notasecret", X509KeyStorageFlags.Exportable); - - // TODO: support JSON key file. - - // TODO: get file location from GoogleApplicationCredential env var - var certificate = new X509Certificate2("/usr/local/google/home/jtattermusch/certs/stubbyCloudTestingTest-090796e783f3.p12", "notasecret", X509KeyStorageFlags.Exportable); - - // TODO: auth user will be read from the JSON key - string authUser = "155450119199-3psnrh1sdr3d8cpj1v46naggf81mhdnk@developer.gserviceaccount.com"; + JObject o1 = JObject.Parse(File.ReadAllText(credsPath)); + string clientEmail = o1.GetValue(ClientEmailFieldName).Value<string>(); + string privateKeyString = o1.GetValue(PrivateKeyFieldName).Value<string>(); + var privateKey = ParsePrivateKeyFromString(privateKeyString); var serviceCredential = new ServiceAccountCredential( - new ServiceAccountCredential.Initializer(authUser) + new ServiceAccountCredential.Initializer(clientEmail) { - Scopes = scopes - }.FromCertificate(certificate)); + Scopes = scopes, + Key = privateKey + }); return new GoogleCredential(serviceCredential); } @@ -111,5 +107,17 @@ namespace Grpc.Auth return credential; } } + + private RSACryptoServiceProvider ParsePrivateKeyFromString(string base64PrivateKey) + { + // TODO(jtattermusch): temporary code to create RSACryptoServiceProvider. + base64PrivateKey = base64PrivateKey.Replace("-----BEGIN PRIVATE KEY-----", "").Replace("\n", "").Replace("-----END PRIVATE KEY-----", ""); + PKCS8.PrivateKeyInfo PKI = new PKCS8.PrivateKeyInfo(Convert.FromBase64String(base64PrivateKey)); + RsaPrivateCrtKeyParameters key = (RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(PKI.GetBytes()); + RSAParameters rsaParameters = DotNetUtilities.ToRSAParameters(key); + RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(); + rsa.ImportParameters(rsaParameters); + return rsa; + } } } diff --git a/src/csharp/Grpc.Auth/Grpc.Auth.csproj b/src/csharp/Grpc.Auth/Grpc.Auth.csproj index dbbee780a8..1931db5fd8 100644 --- a/src/csharp/Grpc.Auth/Grpc.Auth.csproj +++ b/src/csharp/Grpc.Auth/Grpc.Auth.csproj @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -30,7 +30,44 @@ <ConsolePause>false</ConsolePause> </PropertyGroup> <ItemGroup> + <Reference Include="BouncyCastle.Crypto"> + <HintPath>..\packages\BouncyCastle.1.7.0\lib\Net40-Client\BouncyCastle.Crypto.dll</HintPath> + </Reference> + <Reference Include="Google.Apis.Auth"> + <HintPath>..\packages\Google.Apis.Auth.1.9.1\lib\net40\Google.Apis.Auth.dll</HintPath> + </Reference> + <Reference Include="Google.Apis.Auth.PlatformServices"> + <HintPath>..\packages\Google.Apis.Auth.1.9.1\lib\net40\Google.Apis.Auth.PlatformServices.dll</HintPath> + </Reference> + <Reference Include="Google.Apis.Core"> + <HintPath>..\packages\Google.Apis.Core.1.9.1\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Threading.Tasks"> + <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Threading.Tasks.Extensions"> + <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop"> + <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath> + </Reference> + <Reference Include="Mono.Security"> + <HintPath>..\packages\Mono.Security.3.2.3.0\lib\net45\Mono.Security.dll</HintPath> + </Reference> + <Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll</HintPath> + </Reference> <Reference Include="System" /> + <Reference Include="System.Net" /> + <Reference Include="System.Net.Http" /> + <Reference Include="System.Net.Http.Extensions"> + <HintPath>..\packages\Microsoft.Net.Http.2.2.28\lib\net45\System.Net.Http.Extensions.dll</HintPath> + </Reference> + <Reference Include="System.Net.Http.Primitives"> + <HintPath>..\packages\Microsoft.Net.Http.2.2.28\lib\net45\System.Net.Http.Primitives.dll</HintPath> + </Reference> + <Reference Include="System.Net.Http.WebRequest" /> </ItemGroup> <ItemGroup> <Compile Include="Properties\AssemblyInfo.cs" /> @@ -44,4 +81,13 @@ <Name>Grpc.Core</Name> </ProjectReference> </ItemGroup> + <ItemGroup> + <None Include="app.config" /> + <None Include="packages.config" /> + </ItemGroup> + <Import Project="..\packages\Microsoft.Bcl.Build.1.0.14\tools\Microsoft.Bcl.Build.targets" Condition="Exists('..\packages\Microsoft.Bcl.Build.1.0.14\tools\Microsoft.Bcl.Build.targets')" /> + <Target Name="EnsureBclBuildImported" BeforeTargets="BeforeBuild" Condition="'$(BclBuildImported)' == ''"> + <Error Condition="!Exists('..\packages\Microsoft.Bcl.Build.1.0.14\tools\Microsoft.Bcl.Build.targets')" Text="This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=317567." HelpKeyword="BCLBUILD2001" /> + <Error Condition="Exists('..\packages\Microsoft.Bcl.Build.1.0.14\tools\Microsoft.Bcl.Build.targets')" Text="The build restored NuGet packages. Build the project again to include these packages in the build. For more information, see http://go.microsoft.com/fwlink/?LinkID=317568." HelpKeyword="BCLBUILD2002" /> + </Target> </Project> \ No newline at end of file diff --git a/src/csharp/Grpc.Auth/OAuth2InterceptorFactory.cs b/src/csharp/Grpc.Auth/OAuth2InterceptorFactory.cs index ae9d70deb8..8a1e87ad6f 100644 --- a/src/csharp/Grpc.Auth/OAuth2InterceptorFactory.cs +++ b/src/csharp/Grpc.Auth/OAuth2InterceptorFactory.cs @@ -33,17 +33,13 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Google.ProtocolBuffers; -using grpc.testing; using Grpc.Core; using Grpc.Core.Utils; -using NUnit.Framework; using Google.Apis.Auth.OAuth2; using System.Security.Cryptography.X509Certificates; @@ -59,7 +55,7 @@ namespace Grpc.Auth string accessToken = credential.Token.AccessToken; // TODO: token refresh logic!! - return new HeaderInterceptorDelegate((b)=> { b.Add(new Metadata.MetadataEntry("Authorization", "Bearer " + accessToken)); }); + return new HeaderInterceptorDelegate((b) => { b.Add(new Metadata.MetadataEntry("Authorization", "Bearer " + accessToken)); }); } } diff --git a/src/csharp/Grpc.Auth/app.config b/src/csharp/Grpc.Auth/app.config new file mode 100644 index 0000000000..966b777192 --- /dev/null +++ b/src/csharp/Grpc.Auth/app.config @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <runtime> + <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> + <dependentAssembly> + <assemblyIdentity name="System.Net.Http.Primitives" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.2.28.0" /> + </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.0.0.0" /> + </dependentAssembly> + </assemblyBinding> + </runtime> +</configuration> \ No newline at end of file diff --git a/src/csharp/Grpc.Auth/packages.config b/src/csharp/Grpc.Auth/packages.config new file mode 100644 index 0000000000..0816bdbad1 --- /dev/null +++ b/src/csharp/Grpc.Auth/packages.config @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="BouncyCastle" version="1.7.0" targetFramework="net45" /> + <package id="Google.Apis.Auth" version="1.9.1" targetFramework="net45" /> + <package id="Google.Apis.Core" version="1.9.1" targetFramework="net45" /> + <package id="Microsoft.Bcl" version="1.1.9" targetFramework="net45" /> + <package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net45" /> + <package id="Microsoft.Bcl.Build" version="1.0.14" targetFramework="net45" /> + <package id="Microsoft.Net.Http" version="2.2.28" targetFramework="net45" /> + <package id="Mono.Security" version="3.2.3.0" targetFramework="net45" /> + <package id="Newtonsoft.Json" version="6.0.6" targetFramework="net45" /> +</packages> \ No newline at end of file diff --git a/src/csharp/Grpc.Core/Grpc.Core.csproj b/src/csharp/Grpc.Core/Grpc.Core.csproj index b612512b03..0b85392e15 100644 --- a/src/csharp/Grpc.Core/Grpc.Core.csproj +++ b/src/csharp/Grpc.Core/Grpc.Core.csproj @@ -34,8 +34,7 @@ </PropertyGroup> <ItemGroup> <Reference Include="System" /> - <Reference Include="System.Collections.Immutable, Version=1.0.34.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> + <Reference Include="System.Collections.Immutable"> <HintPath>..\packages\Microsoft.Bcl.Immutable.1.0.34\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll</HintPath> </Reference> </ItemGroup> diff --git a/src/csharp/Grpc.IntegrationTesting.Client/app.config b/src/csharp/Grpc.IntegrationTesting.Client/app.config index 4e4d248a19..966b777192 100644 --- a/src/csharp/Grpc.IntegrationTesting.Client/app.config +++ b/src/csharp/Grpc.IntegrationTesting.Client/app.config @@ -6,6 +6,10 @@ <assemblyIdentity name="System.Net.Http.Primitives" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.2.28.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.0.0.0" /> + </dependentAssembly> </assemblyBinding> </runtime> </configuration> \ No newline at end of file diff --git a/src/csharp/Grpc.IntegrationTesting.Server/app.config b/src/csharp/Grpc.IntegrationTesting.Server/app.config index 4e4d248a19..966b777192 100644 --- a/src/csharp/Grpc.IntegrationTesting.Server/app.config +++ b/src/csharp/Grpc.IntegrationTesting.Server/app.config @@ -6,6 +6,10 @@ <assemblyIdentity name="System.Net.Http.Primitives" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.2.28.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.0.0.0" /> + </dependentAssembly> </assemblyBinding> </runtime> </configuration> \ No newline at end of file diff --git a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj index ce4a18c192..13bbb5363f 100644 --- a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj +++ b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj @@ -32,9 +32,6 @@ <PlatformTarget>x86</PlatformTarget> </PropertyGroup> <ItemGroup> - <Reference Include="Google.Apis.Auth"> - <HintPath>..\packages\Google.Apis.Auth.1.9.1\lib\net40\Google.Apis.Auth.dll</HintPath> - </Reference> <Reference Include="Google.Apis.Auth.PlatformServices"> <HintPath>..\packages\Google.Apis.Auth.1.9.1\lib\net40\Google.Apis.Auth.PlatformServices.dll</HintPath> </Reference> @@ -50,10 +47,6 @@ <Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop"> <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath> </Reference> - <Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll</HintPath> - </Reference> <Reference Include="nunit.framework"> <HintPath>..\packages\NUnit.2.6.4\lib\nunit.framework.dll</HintPath> </Reference> @@ -61,10 +54,6 @@ <Reference Include="Google.ProtocolBuffers"> <HintPath>..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.dll</HintPath> </Reference> - <Reference Include="System.Collections.Immutable, Version=1.0.34.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Microsoft.Bcl.Immutable.1.0.34\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll</HintPath> - </Reference> <Reference Include="System.Net" /> <Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http.Extensions"> @@ -74,6 +63,12 @@ <HintPath>..\packages\Microsoft.Net.Http.2.2.28\lib\net45\System.Net.Http.Primitives.dll</HintPath> </Reference> <Reference Include="System.Net.Http.WebRequest" /> + <Reference Include="Newtonsoft.Json"> + <HintPath>..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll</HintPath> + </Reference> + <Reference Include="System.Collections.Immutable"> + <HintPath>..\packages\Microsoft.Bcl.Immutable.1.0.34\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll</HintPath> + </Reference> </ItemGroup> <ItemGroup> <Compile Include="Properties\AssemblyInfo.cs" /> @@ -92,6 +87,10 @@ <Project>{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}</Project> <Name>Grpc.Core</Name> </ProjectReference> + <ProjectReference Include="..\Grpc.Auth\Grpc.Auth.csproj"> + <Project>{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}</Project> + <Name>Grpc.Auth</Name> + </ProjectReference> </ItemGroup> <ItemGroup> <None Include="app.config" /> diff --git a/src/csharp/Grpc.IntegrationTesting/InteropClient.cs b/src/csharp/Grpc.IntegrationTesting/InteropClient.cs index 6b92d3c660..20ecd60c30 100644 --- a/src/csharp/Grpc.IntegrationTesting/InteropClient.cs +++ b/src/csharp/Grpc.IntegrationTesting/InteropClient.cs @@ -39,14 +39,25 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Google.ProtocolBuffers; using grpc.testing; +using Grpc.Auth; using Grpc.Core; using Grpc.Core.Utils; using NUnit.Framework; +using Newtonsoft.Json.Linq; +using System.Threading; + +using Google.Apis.Auth.OAuth2; +using System.Security.Cryptography.X509Certificates; namespace Grpc.IntegrationTesting { public class InteropClient { + private const string ServiceAccountUser = "155450119199-3psnrh1sdr3d8cpj1v46naggf81mhdnk@developer.gserviceaccount.com"; + private const string ComputeEngineUser = "155450119199-r5aaqa2vqoa9g5mv2m6s3m1l293rlmel@developer.gserviceaccount.com"; + private const string AuthScope = "https://www.googleapis.com/auth/xapi.zoo"; + private const string AuthScopeResponse = "xapi.zoo"; + private class ClientOptions { public bool help; @@ -115,7 +126,18 @@ namespace Grpc.IntegrationTesting using (Channel channel = new Channel(addr, credentials, channelArgs)) { - TestServiceGrpc.ITestServiceClient client = new TestServiceGrpc.TestServiceClientStub(channel); + var stubConfig = StubConfiguration.Default; + if (options.testCase == "service_account_creds") + { + var credential = GoogleCredential.GetApplicationDefault(); + if (credential.IsCreateScopedRequired) + { + credential = credential.CreateScoped(new[] { AuthScope }); + } + stubConfig = new StubConfiguration(OAuth2InterceptorFactory.Create(credential)); + } + + TestServiceGrpc.ITestServiceClient client = new TestServiceGrpc.TestServiceClientStub(channel, stubConfig); RunTestCase(options.testCase, client); } @@ -144,6 +166,9 @@ namespace Grpc.IntegrationTesting case "empty_stream": RunEmptyStream(client); break; + case "service_account_creds": + RunServiceAccountCreds(client); + break; case "benchmark_empty_unary": RunBenchmarkEmptyUnary(client); break; @@ -287,6 +312,26 @@ namespace Grpc.IntegrationTesting Console.WriteLine("Passed!"); } + public static void RunServiceAccountCreds(TestServiceGrpc.ITestServiceClient client) + { + Console.WriteLine("running service_account_creds"); + var request = SimpleRequest.CreateBuilder() + .SetResponseType(PayloadType.COMPRESSABLE) + .SetResponseSize(314159) + .SetPayload(CreateZerosPayload(271828)) + .SetFillUsername(true) + .SetFillOauthScope(true) + .Build(); + + var response = client.UnaryCall(request); + + Assert.AreEqual(PayloadType.COMPRESSABLE, response.Payload.Type); + Assert.AreEqual(314159, response.Payload.Body.Length); + Assert.AreEqual(AuthScopeResponse, response.OauthScope); + Assert.AreEqual(ServiceAccountUser, response.Username); + Console.WriteLine("Passed!"); + } + // This is not an official interop test, but it's useful. public static void RunBenchmarkEmptyUnary(TestServiceGrpc.ITestServiceClient client) { diff --git a/src/csharp/Grpc.IntegrationTesting/app.config b/src/csharp/Grpc.IntegrationTesting/app.config index 4e4d248a19..966b777192 100644 --- a/src/csharp/Grpc.IntegrationTesting/app.config +++ b/src/csharp/Grpc.IntegrationTesting/app.config @@ -6,6 +6,10 @@ <assemblyIdentity name="System.Net.Http.Primitives" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.2.28.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.0.0.0" /> + </dependentAssembly> </assemblyBinding> </runtime> </configuration> \ No newline at end of file -- GitLab