diff --git a/CHANGELOG.md b/CHANGELOG.md index 827d91eb..38306f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed bug: public key presented not for certificate signature. - Fixed: YdbDataReader does not throw YdbException when CloseAsync is called for UPDATE/INSERT statements with no result. diff --git a/examples/src/YC/CmdOptions.cs b/examples/src/YC/CmdOptions.cs new file mode 100644 index 00000000..0c6f3b11 --- /dev/null +++ b/examples/src/YC/CmdOptions.cs @@ -0,0 +1,15 @@ +using CommandLine; + +namespace YcCloud; + +internal class CmdOptions +{ + [Option('h', "host", Required = true, HelpText = "Database host")] + public string Host { get; set; } = null!; + + [Option('d', "database", Required = true, HelpText = "Database name")] + public string Database { get; set; } = null!; + + [Option("saFilePath", Required = true, HelpText = "Sa Key")] + public string SaFilePath { get; set; } = null!; +} \ No newline at end of file diff --git a/examples/src/YC/Program.cs b/examples/src/YC/Program.cs new file mode 100644 index 00000000..6a163335 --- /dev/null +++ b/examples/src/YC/Program.cs @@ -0,0 +1,30 @@ +using CommandLine; +using Microsoft.Extensions.Logging; +using YcCloud; +using Ydb.Sdk.Ado; +using Ydb.Sdk.Yc; + +await Parser.Default.ParseArguments(args).WithParsedAsync(async cmd => +{ + var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information)); + + var saProvider = new ServiceAccountProvider(saFilePath: cmd.SaFilePath, loggerFactory: loggerFactory); + await saProvider.Initialize(); + + var builder = new YdbConnectionStringBuilder + { + UseTls = true, + Host = cmd.Host, + Port = 2135, + Database = cmd.Database, + CredentialsProvider = saProvider, + LoggerFactory = loggerFactory, + ServerCertificates = YcCerts.GetYcServerCertificates() + }; + + await using var ydbConnection = new YdbConnection(builder); + await ydbConnection.OpenAsync(); + + Console.WriteLine(await new YdbCommand(ydbConnection) { CommandText = "SELECT 'Hello Dedicated YDB!'u" } + .ExecuteScalarAsync()); +}); \ No newline at end of file diff --git a/examples/src/YC/YC.csproj b/examples/src/YC/YC.csproj new file mode 100644 index 00000000..5e705f72 --- /dev/null +++ b/examples/src/YC/YC.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + YcCloud + + + + + + + + + + + + + diff --git a/examples/src/YdbExamples.sln b/examples/src/YdbExamples.sln index f51a56bf..331e0962 100644 --- a/examples/src/YdbExamples.sln +++ b/examples/src/YdbExamples.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdoNet", "AdoNet\AdoNet.csp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DapperExample", "DapperExample\DapperExample.csproj", "{AC8F1B10-31EB-4A29-BF35-7F49B06E1FA8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YC", "YC\YC.csproj", "{753E4F33-CB08-47B9-864F-4CC037B278C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {AC8F1B10-31EB-4A29-BF35-7F49B06E1FA8}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC8F1B10-31EB-4A29-BF35-7F49B06E1FA8}.Release|Any CPU.ActiveCfg = Release|Any CPU {AC8F1B10-31EB-4A29-BF35-7F49B06E1FA8}.Release|Any CPU.Build.0 = Release|Any CPU + {753E4F33-CB08-47B9-864F-4CC037B278C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {753E4F33-CB08-47B9-864F-4CC037B278C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {753E4F33-CB08-47B9-864F-4CC037B278C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {753E4F33-CB08-47B9-864F-4CC037B278C4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs index 50ed8d8b..b4eb9651 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs @@ -137,9 +137,11 @@ public string? RootCertificate private string? _rootCertificate; - public ILoggerFactory? LoggerFactory { get; set; } + public ILoggerFactory? LoggerFactory { get; init; } - public ICredentialsProvider? CredentialsProvider { get; set; } + public ICredentialsProvider? CredentialsProvider { get; init; } + + public X509Certificate2Collection? ServerCertificates { get; init; } private void SaveValue(string propertyName, object? value) { @@ -189,7 +191,8 @@ internal Task BuildDriver() endpoint: Endpoint, database: Database, credentials: credentialsProvider, - customServerCertificate: cert + customServerCertificate: cert, + customServerCertificates: ServerCertificates ), LoggerFactory); } diff --git a/src/Ydb.Sdk/src/DriverConfig.cs b/src/Ydb.Sdk/src/DriverConfig.cs index 62dea659..66bca0c6 100644 --- a/src/Ydb.Sdk/src/DriverConfig.cs +++ b/src/Ydb.Sdk/src/DriverConfig.cs @@ -8,8 +8,8 @@ public class DriverConfig public string Endpoint { get; } public string Database { get; } public ICredentialsProvider Credentials { get; } - public X509Certificate? CustomServerCertificate { get; } + internal X509Certificate2Collection CustomServerCertificates { get; } = new(); internal TimeSpan EndpointDiscoveryInterval = TimeSpan.FromMinutes(1); internal TimeSpan EndpointDiscoveryTimeout = TimeSpan.FromSeconds(10); @@ -17,12 +17,22 @@ public DriverConfig( string endpoint, string database, ICredentialsProvider? credentials = null, - X509Certificate? customServerCertificate = null) + X509Certificate? customServerCertificate = null, + X509Certificate2Collection? customServerCertificates = null) { Endpoint = FormatEndpoint(endpoint); Database = database; Credentials = credentials ?? new AnonymousProvider(); - CustomServerCertificate = customServerCertificate; + + if (customServerCertificate != null) + { + CustomServerCertificates.Add(new X509Certificate2(customServerCertificate)); + } + + if (customServerCertificates != null) + { + CustomServerCertificates.AddRange(customServerCertificates); + } } private static string FormatEndpoint(string endpoint) diff --git a/src/Ydb.Sdk/src/Pool/ChannelPool.cs b/src/Ydb.Sdk/src/Pool/ChannelPool.cs index fc736786..d9d93d98 100644 --- a/src/Ydb.Sdk/src/Pool/ChannelPool.cs +++ b/src/Ydb.Sdk/src/Pool/ChannelPool.cs @@ -5,7 +5,6 @@ using Grpc.Core; using Grpc.Net.Client; using Microsoft.Extensions.Logging; -using Org.BouncyCastle.Security; namespace Ydb.Sdk.Pool; @@ -79,14 +78,14 @@ public interface IChannelFactory where T : ChannelBase, IDisposable internal class GrpcChannelFactory : IChannelFactory { private readonly ILoggerFactory _loggerFactory; - private readonly X509Certificate? _x509Certificate; private readonly ILogger _logger; + private readonly X509Certificate2Collection _x509Certificate2Collection; internal GrpcChannelFactory(ILoggerFactory loggerFactory, DriverConfig config) { _loggerFactory = loggerFactory; - _x509Certificate = config.CustomServerCertificate; _logger = loggerFactory.CreateLogger(); + _x509Certificate2Collection = config.CustomServerCertificates; } public GrpcChannel CreateChannel(string endpoint) @@ -98,32 +97,40 @@ public GrpcChannel CreateChannel(string endpoint) LoggerFactory = _loggerFactory }; - if (_x509Certificate == null) + var httpHandler = new SocketsHttpHandler(); + + // https://github.com/grpc/grpc-dotnet/issues/2312#issuecomment-1790661801 + httpHandler.Properties["__GrpcLoadBalancingDisabled"] = true; + + channelOptions.HttpHandler = httpHandler; + channelOptions.DisposeHttpClient = true; + + if (_x509Certificate2Collection.Count == 0) { return GrpcChannel.ForAddress(endpoint, channelOptions); } - var httpHandler = new SocketsHttpHandler(); - - var customCertificate = DotNetUtilities.FromX509Certificate(_x509Certificate); - - httpHandler.SslOptions.RemoteCertificateValidationCallback = - (_, certificate, _, sslPolicyErrors) => + httpHandler.SslOptions.RemoteCertificateValidationCallback += + (_, certificate, chain, sslPolicyErrors) => { if (sslPolicyErrors == SslPolicyErrors.None) { return true; } - if (certificate is null) + if (certificate is null || chain is null) { return false; } try { - var cert = DotNetUtilities.FromX509Certificate(certificate); - cert.Verify(customCertificate.GetPublicKey()); + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + chain.ChainPolicy.ExtraStore.AddRange(_x509Certificate2Collection); + + return chain.Build(new X509Certificate2(certificate)) && chain.ChainElements.Any(chainElement => + _x509Certificate2Collection.Any(trustedCert => + chainElement.Certificate.Thumbprint == trustedCert.Thumbprint)); } catch (Exception e) { @@ -131,14 +138,7 @@ public GrpcChannel CreateChannel(string endpoint) return false; } - - return true; }; - // https://github.com/grpc/grpc-dotnet/issues/2312#issuecomment-1790661801 - httpHandler.Properties["__GrpcLoadBalancingDisabled"] = true; - - channelOptions.HttpHandler = httpHandler; - channelOptions.DisposeHttpClient = true; return GrpcChannel.ForAddress(endpoint, channelOptions); } diff --git a/src/Ydb.Sdk/src/Transport/AuthGrpcChannelDriver.cs b/src/Ydb.Sdk/src/Transport/AuthGrpcChannelDriver.cs index f76274bd..bdb95e2f 100644 --- a/src/Ydb.Sdk/src/Transport/AuthGrpcChannelDriver.cs +++ b/src/Ydb.Sdk/src/Transport/AuthGrpcChannelDriver.cs @@ -17,7 +17,7 @@ ILoggerFactory loggerFactory new DriverConfig( endpoint: driverConfig.Endpoint, database: driverConfig.Database, - customServerCertificate: driverConfig.CustomServerCertificate + customServerCertificates: driverConfig.CustomServerCertificates ), loggerFactory, loggerFactory.CreateLogger()) { _channel = grpcChannelFactory.CreateChannel(Config.Endpoint);