Copilot commented on code in PR #60:
URL:
https://github.com/apache/iotdb-client-csharp/pull/60#discussion_r3498152692
##########
src/Apache.IoTDB/SessionPool.cs:
##########
@@ -524,6 +538,144 @@ private async Task<Client> CreateAndOpen(string host, int
port, bool enableRpcCo
}
}
+ private static X509Certificate2 LoadClientCertificate(string
clientCertificatePath, string clientCertificatePassword)
+ {
+ if (string.IsNullOrWhiteSpace(clientCertificatePath))
+ {
+ return null;
+ }
+
+ return clientCertificatePassword == null
+ ? new X509Certificate2(clientCertificatePath)
+ : new X509Certificate2(clientCertificatePath,
clientCertificatePassword);
+ }
+
+ private static X509Certificate2Collection LoadRootCertificates(string
rootCertificatePath)
+ {
+ if (string.IsNullOrWhiteSpace(rootCertificatePath))
+ {
+ return null;
+ }
+
+ var certificateBytes = File.ReadAllBytes(rootCertificatePath);
+ var certificates = LoadPemCertificates(certificateBytes);
+ if (certificates.Count > 0)
+ {
+ return certificates;
+ }
+
+ certificates.Import(certificateBytes);
+ return certificates;
+ }
+
+ private static X509Certificate2Collection LoadPemCertificates(byte[]
certificateBytes)
+ {
+ const string beginCertificate = "-----BEGIN CERTIFICATE-----";
+ const string endCertificate = "-----END CERTIFICATE-----";
+
+ var certificates = new X509Certificate2Collection();
+ var certificateText = Encoding.ASCII.GetString(certificateBytes);
+ var startIndex = certificateText.IndexOf(beginCertificate,
StringComparison.Ordinal);
+ while (startIndex >= 0)
+ {
+ startIndex += beginCertificate.Length;
+ var endIndex = certificateText.IndexOf(endCertificate,
startIndex, StringComparison.Ordinal);
+ if (endIndex < 0)
+ {
+ break;
+ }
+
+ var base64 = certificateText.Substring(startIndex, endIndex -
startIndex)
+ .Replace("\r", string.Empty)
+ .Replace("\n", string.Empty)
+ .Trim();
+ certificates.Add(new
X509Certificate2(Convert.FromBase64String(base64)));
+ startIndex = certificateText.IndexOf(beginCertificate,
endIndex + endCertificate.Length, StringComparison.Ordinal);
+ }
+
+ return certificates;
+ }
+
+ private static RemoteCertificateValidationCallback
CreateRemoteCertificateValidationCallback(X509Certificate2Collection
rootCertificates)
+ {
+ if (rootCertificates == null || rootCertificates.Count == 0)
+ {
+ return null;
+ }
+
+ return (sender, certificate, chain, sslPolicyErrors) =>
+ {
+ if ((sslPolicyErrors &
SslPolicyErrors.RemoteCertificateNameMismatch) != 0 ||
+ (sslPolicyErrors &
SslPolicyErrors.RemoteCertificateNotAvailable) != 0 ||
+ certificate == null)
+ {
+ return false;
+ }
+
+ var serverCertificate = certificate as X509Certificate2 ?? new
X509Certificate2(certificate);
+ try
+ {
+ using var customChain = new X509Chain();
+ customChain.ChainPolicy.RevocationMode =
X509RevocationMode.NoCheck;
+ customChain.ChainPolicy.VerificationFlags =
X509VerificationFlags.AllowUnknownCertificateAuthority;
Review Comment:
When a custom root CA is provided, the RemoteCertificateValidationCallback
currently rebuilds the chain but does not enforce that the leaf certificate is
valid for TLS server authentication (EKU 1.3.6.1.5.5.7.3.1). Because the
callback ignores SslPolicyErrors.RemoteCertificateChainErrors and X509Chain
does not validate EKU by default, this can unintentionally accept certificates
that are not appropriate for server auth as long as they chain to the provided
root.
##########
src/Apache.IoTDB/SessionPool.cs:
##########
@@ -524,6 +538,144 @@ private async Task<Client> CreateAndOpen(string host, int
port, bool enableRpcCo
}
}
+ private static X509Certificate2 LoadClientCertificate(string
clientCertificatePath, string clientCertificatePassword)
+ {
+ if (string.IsNullOrWhiteSpace(clientCertificatePath))
+ {
+ return null;
+ }
+
+ return clientCertificatePassword == null
+ ? new X509Certificate2(clientCertificatePath)
+ : new X509Certificate2(clientCertificatePath,
clientCertificatePassword);
Review Comment:
LoadClientCertificate treats an empty password differently from a missing
password. If ClientCertificatePassword is provided but empty (e.g., connection
string sets it to an empty value), X509Certificate2(path, "") will be used and
can fail for certs that are unencrypted / expect a null password. Treating
null/empty uniformly avoids surprising load failures.
##########
src/Apache.IoTDB/SessionPool.Builder.cs:
##########
@@ -100,9 +102,21 @@ public Builder SetUseSsl(bool useSsl)
return this;
}
- public Builder SetCertificatePath(string certificatePath)
+ public Builder SetClientCertificatePath(string clientCertificatePath)
{
- _certificatePath = certificatePath;
+ _clientCertificatePath = clientCertificatePath;
+ return this;
+ }
Review Comment:
Renaming/removing SetCertificatePath is a breaking change for existing users
of the fluent builder. If the previous SetCertificatePath was intended for
client certificate auth (as it was passed into the TLS transport), consider
keeping it as an [Obsolete] shim that forwards to SetClientCertificatePath to
preserve backward compatibility.
##########
README.md:
##########
@@ -63,6 +63,50 @@ Users can quickly get started by referring to the use cases
under the Apache-IoT
For those who wish to delve deeper into the client's usage and explore more
advanced features, the samples directory contains additional code samples.
+## TLS and mTLS
+
+Enable TLS by calling `SetUseSsl(true)`. The C# client uses the .NET
certificate model and does not read Java truststores directly. If your
certificates were generated with the JDK 17 Java/keytool workflow,
`client.keystore` is PKCS#12 by default and can be used directly as the client
certificate file; use `ca.crt` directly as the trusted root.
+
+| keytool artifact | C# client usage |
+| --- | --- |
+| `ca.crt` | Pass to `SetRootCertificatePath` / `RootCertificatePath` to trust
the server certificate |
+| `client.keystore` | Contains the client private key and certificate chain;
JDK 17 creates PKCS#12 by default, so pass it directly to
`SetClientCertificatePath` |
+| `client.truststore` | Java client truststore; the C# client uses `ca.crt`
instead |
+| `server.truststore` | Server-side truststore for trusting client
certificates; not a C# client option |
+
+Only convert the keystore first if you are reusing an older JKS file, or if it
was explicitly generated with `-storetype JKS`:
+
+```bash
+$KT -importkeystore \
+ -srckeystore client.keystore \
+ -srcstorepass $PWD \
+ -srcalias client \
+ -destkeystore client.p12 \
+ -deststoretype PKCS12 \
+ -deststorepass $PWD \
+ -destkeypass $PWD \
+ -destalias client
+```
+
+C# builder example:
+
+```csharp
+var sessionPool = new SessionPool.Builder()
+ .SetHost("127.0.0.1")
Review Comment:
The custom RemoteCertificateValidationCallback rejects
SslPolicyErrors.RemoteCertificateNameMismatch. With the example using an IP
address, TLS will fail unless the server certificate contains that IP in the
SAN. Using a hostname that matches the certificate (e.g., localhost) avoids a
confusing “it doesn’t work” experience.
##########
src/Apache.IoTDB/SessionPool.cs:
##########
@@ -448,15 +458,19 @@ public async Task<string> GetTimeZone()
}
}
- private async Task<Client> CreateAndOpen(string host, int port, bool
enableRpcCompression, int timeout, bool useSsl, string cert, string sqlDialect,
string database, CancellationToken cancellationToken = default)
+ private async Task<Client> CreateAndOpen(string host, int port, bool
enableRpcCompression, int timeout, bool useSsl, string clientCertificatePath,
string clientCertificatePassword, string rootCertificatePath, string
sqlDialect, string database, CancellationToken cancellationToken = default)
{
TTransport socket;
if (useSsl)
{
+ var clientCertificate =
LoadClientCertificate(clientCertificatePath, clientCertificatePassword);
+ var rootCertificates =
LoadRootCertificates(rootCertificatePath);
+ var remoteCertificateValidationCallback =
CreateRemoteCertificateValidationCallback(rootCertificates);
+ var localCertificateSelectionCallback =
CreateLocalCertificateSelectionCallback(clientCertificate);
Review Comment:
Client/root certificates are loaded from disk and parsed on every
CreateAndOpen call (including for each pooled client and each reconnect). This
adds repeated file I/O and certificate parsing overhead and can amplify
operational impact under reconnect storms. Consider loading/caching the
certificates once per SessionPool instance and reusing them for all
connections, disposing them when the pool is disposed.
##########
src/Apache.IoTDB/TableSessionPool.Builder.cs:
##########
@@ -103,9 +105,21 @@ public Builder SetUseSsl(bool useSsl)
return this;
}
- public Builder SetCertificatePath(string certificatePath)
+ public Builder SetClientCertificatePath(string clientCertificatePath)
{
- _certificatePath = certificatePath;
+ _clientCertificatePath = clientCertificatePath;
+ return this;
+ }
Review Comment:
Renaming/removing SetCertificatePath is a breaking change for
TableSessionPool.Builder users. Consider keeping SetCertificatePath as an
[Obsolete] shim forwarding to SetClientCertificatePath to preserve source
compatibility.
##########
README_ZH.md:
##########
@@ -61,6 +61,49 @@ dotnet add package Apache.IoTDB
对于希望深入了解客户端用法并探索更高级特性的用户,samples目录包含了额外的代码示例。
+## TLS 和 mTLS
+
+通过 `SetUseSsl(true)` 开启 TLS。C# 客户端使用 .NET 的证书模型,不直接读取 Java truststore;如果证书按
JDK 17 的 Java/keytool 文档生成,`client.keystore` 默认就是 PKCS#12,可以直接作为客户端证书文件使用,并直接使用
`ca.crt` 作为信任根。
+
+| keytool 产物 | C# 客户端用法 |
+| --- | --- |
+| `ca.crt` | 传给 `SetRootCertificatePath` / `RootCertificatePath`,用于信任服务端证书 |
+| `client.keystore` | 包含客户端私钥和证书链;JDK 17 默认是 PKCS#12,直接传给
`SetClientCertificatePath` |
+| `client.truststore` | Java 客户端的 truststore;C# 侧用 `ca.crt`,不需要这个文件 |
+| `server.truststore` | 服务端用于信任客户端证书,不是 C# 客户端参数 |
+
+只有在复用旧版 JDK 生成的 JKS 文件,或显式使用 `-storetype JKS` 生成 keystore 时,才需要先转换为 PKCS#12:
+
+```bash
+$KT -importkeystore \
+ -srckeystore client.keystore \
+ -srcstorepass $PWD \
+ -srcalias client \
+ -destkeystore client.p12 \
+ -deststoretype PKCS12 \
+ -deststorepass $PWD \
+ -destkeypass $PWD \
+ -destalias client
+```
+
+C# builder 示例:
+
+```csharp
+var sessionPool = new SessionPool.Builder()
+ .SetHost("127.0.0.1")
Review Comment:
当 RootCertificatePath 启用自定义 CA 校验时,代码会拒绝 RemoteCertificateNameMismatch。示例里使用
IP(127.0.0.1)会导致 TLS 失败,除非证书 SAN 中包含该 IP。建议示例改为使用与证书匹配的主机名(例如
localhost),避免用户按文档操作却握手失败。
##########
README.md:
##########
@@ -63,6 +63,50 @@ Users can quickly get started by referring to the use cases
under the Apache-IoT
For those who wish to delve deeper into the client's usage and explore more
advanced features, the samples directory contains additional code samples.
+## TLS and mTLS
+
+Enable TLS by calling `SetUseSsl(true)`. The C# client uses the .NET
certificate model and does not read Java truststores directly. If your
certificates were generated with the JDK 17 Java/keytool workflow,
`client.keystore` is PKCS#12 by default and can be used directly as the client
certificate file; use `ca.crt` directly as the trusted root.
+
+| keytool artifact | C# client usage |
+| --- | --- |
+| `ca.crt` | Pass to `SetRootCertificatePath` / `RootCertificatePath` to trust
the server certificate |
+| `client.keystore` | Contains the client private key and certificate chain;
JDK 17 creates PKCS#12 by default, so pass it directly to
`SetClientCertificatePath` |
+| `client.truststore` | Java client truststore; the C# client uses `ca.crt`
instead |
+| `server.truststore` | Server-side truststore for trusting client
certificates; not a C# client option |
+
+Only convert the keystore first if you are reusing an older JKS file, or if it
was explicitly generated with `-storetype JKS`:
+
+```bash
+$KT -importkeystore \
+ -srckeystore client.keystore \
+ -srcstorepass $PWD \
+ -srcalias client \
+ -destkeystore client.p12 \
+ -deststoretype PKCS12 \
+ -deststorepass $PWD \
+ -destkeypass $PWD \
+ -destalias client
+```
+
+C# builder example:
+
+```csharp
+var sessionPool = new SessionPool.Builder()
+ .SetHost("127.0.0.1")
+ .SetPort(6667)
+ .SetUseSsl(true)
+ .SetRootCertificatePath("tls-certs/ca.crt")
+ .SetClientCertificatePath("tls-certs/client.keystore")
+ .SetClientCertificatePassword("IoTDB")
+ .Build();
+```
+
+The ADO.NET connection string supports the same options:
+
+```text
+DataSource=127.0.0.1;Port=6667;UseSsl=True;RootCertificatePath=tls-certs/ca.crt;ClientCertificatePath=tls-certs/client.keystore;ClientCertificatePassword=IoTDB
Review Comment:
This connection string example uses an IP address. When RootCertificatePath
is set, the code enforces host name validation and will fail unless the server
cert SAN includes the IP. Prefer a hostname matching the certificate (e.g.,
localhost) or explicitly document the SAN requirement.
##########
README_ZH.md:
##########
@@ -61,6 +61,49 @@ dotnet add package Apache.IoTDB
对于希望深入了解客户端用法并探索更高级特性的用户,samples目录包含了额外的代码示例。
+## TLS 和 mTLS
+
+通过 `SetUseSsl(true)` 开启 TLS。C# 客户端使用 .NET 的证书模型,不直接读取 Java truststore;如果证书按
JDK 17 的 Java/keytool 文档生成,`client.keystore` 默认就是 PKCS#12,可以直接作为客户端证书文件使用,并直接使用
`ca.crt` 作为信任根。
+
+| keytool 产物 | C# 客户端用法 |
+| --- | --- |
+| `ca.crt` | 传给 `SetRootCertificatePath` / `RootCertificatePath`,用于信任服务端证书 |
+| `client.keystore` | 包含客户端私钥和证书链;JDK 17 默认是 PKCS#12,直接传给
`SetClientCertificatePath` |
+| `client.truststore` | Java 客户端的 truststore;C# 侧用 `ca.crt`,不需要这个文件 |
+| `server.truststore` | 服务端用于信任客户端证书,不是 C# 客户端参数 |
+
+只有在复用旧版 JDK 生成的 JKS 文件,或显式使用 `-storetype JKS` 生成 keystore 时,才需要先转换为 PKCS#12:
+
+```bash
+$KT -importkeystore \
+ -srckeystore client.keystore \
+ -srcstorepass $PWD \
+ -srcalias client \
+ -destkeystore client.p12 \
+ -deststoretype PKCS12 \
+ -deststorepass $PWD \
+ -destkeypass $PWD \
+ -destalias client
+```
+
+C# builder 示例:
+
+```csharp
+var sessionPool = new SessionPool.Builder()
+ .SetHost("127.0.0.1")
+ .SetPort(6667)
+ .SetUseSsl(true)
+ .SetRootCertificatePath("tls-certs/ca.crt")
+ .SetClientCertificatePath("tls-certs/client.keystore")
+ .SetClientCertificatePassword("IoTDB")
+ .Build();
+```
+
+ADO.NET 连接字符串也支持相同配置:
+
+```text
+DataSource=127.0.0.1;Port=6667;UseSsl=True;RootCertificatePath=tls-certs/ca.crt;ClientCertificatePath=tls-certs/client.keystore;ClientCertificatePassword=IoTDB
Review Comment:
这个连接字符串示例使用 IP 地址。当前实现会在提供 RootCertificatePath 时严格校验主机名,除非服务端证书 SAN 包含该
IP,否则会握手失败。建议示例改为 localhost 或补充说明 SAN 要求。
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]