From abeb7951bd61a9a43825b5da2f88a6487b6c9539 Mon Sep 17 00:00:00 2001 From: he_sk Date: Sun, 27 Sep 2026 04:01:33 +0800 Subject: [PATCH] feat: support PBES2 encrypted PKCS8 SSL client keys --- MANIFEST.in | 2 +- README.md | 2 +- THIRD_PARTY_NOTICES.md | 63 +++++++++++++++++++ docs/README_zh.md | 2 +- docs/api-reference.md | 2 +- .../test-results/2026-09-27-ssl-connection.md | 10 ++- dpi_bridge/go.mod | 4 +- dpi_bridge/go.sum | 51 +++++++++++++-- .../third_party/chunanyong_dm/PATCHES.md | 10 +-- dpi_bridge/third_party/chunanyong_dm/go.mod | 3 +- dpi_bridge/third_party/chunanyong_dm/go.sum | 47 +++++++++++++- .../third_party/chunanyong_dm/security/zzi.go | 30 ++++++++- .../chunanyong_dm/security/zzi_test.go | 33 ++++++++++ scripts/check_sdist_contents.py | 1 + setup.py | 1 + tests/ssl/test_ssl_connection.py | 30 +++++++++ 16 files changed, 269 insertions(+), 22 deletions(-) create mode 100644 THIRD_PARTY_NOTICES.md diff --git a/MANIFEST.in b/MANIFEST.in index 61ddec3..240befd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include LICENSE README.md setup.py pyproject.toml docs/README_zh.md +include LICENSE THIRD_PARTY_NOTICES.md README.md setup.py pyproject.toml docs/README_zh.md recursive-include src/native *.c *.h recursive-include dpi_bridge *.go recursive-include dpi_bridge *.mod *.sum diff --git a/README.md b/README.md index 2aa3995..0c61673 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ For production environments, prefer the official [DamengDB/dmPython](https://git - **Supported build targets**: macOS 14+ ARM64 with CPython 3.9–3.13. Database behavior is supported only where integration tests have evidence. - **Best-effort**: Extended scenarios not currently covered by CI. - **Not guaranteed**: Production SLA commitments, vendor-certified compatibility guarantees, and closed-source component support contracts. -- **Connection security**: `ssl_path` supports encrypted DM8 connections with client certificate and key. The directory must contain `ca-cert.pem`, `client-cert.pem`, and `client-key.pem`; bundled legacy server certificates without a SAN also require an exact `server-cert.pem` pin. A plain server is rejected when `ssl_path` is set. Non-empty `ssl_pwd`, `ukey_name`, and `ukey_pin` remain unsupported. +- **Connection security**: `ssl_path` supports encrypted DM8 connections with client certificate and key. The directory must contain `ca-cert.pem`, `client-cert.pem`, and `client-key.pem`; bundled legacy server certificates without a SAN also require an exact `server-cert.pem` pin. A plain server is rejected when `ssl_path` is set. `ssl_pwd` supports encrypted traditional PEM and PBES2-encrypted PKCS#8 client keys. Non-empty `ukey_name` and `ukey_pin` remain unsupported. - **Primary/standby routing**: Read/write separation modes 1 and 4 were verified against a local DM8 primary/standby pair with autocommit enabled. MPP cluster routing still needs a cluster regression environment. ## Roadmap & Status diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..ecd525c --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,63 @@ +# Additional Go dependency notices + +These notices cover dependencies added for encrypted PKCS#8 client keys. They +do not establish the redistribution rights of the separately vendored DM Go +driver; see `docs/plans/2026-09-24-open-source-stage-0.md` before publishing. + +## github.com/youmark/pkcs8 + +Version: `v0.0.0-20240726163527-a2c0da244d78` — MIT License + +The MIT License (MIT) + +Copyright (c) 2014 youmark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## golang.org/x/crypto + +Version: `v0.22.0` — BSD 3-Clause License + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/README_zh.md b/docs/README_zh.md index df6da6b..fb48b9d 100644 --- a/docs/README_zh.md +++ b/docs/README_zh.md @@ -22,7 +22,7 @@ dmPython 是达梦数据库(DM8)的原生 Python 驱动程序,遵循 [Pyth - **构建支持范围**:macOS 14+ ARM64、CPython 3.9–3.13。数据库行为仅以已有集成测试证据为准。 - **Best-effort(尽力支持)**:尚未纳入 CI 覆盖的扩展使用场景。 - **Not guaranteed(不保证)**:生产 SLA 承诺、厂商认证兼容性与闭源组件支持协议。 -- **连接安全**:`ssl_path` 支持使用客户端证书与私钥连接启用加密的 DM8。目录需包含 `ca-cert.pem`、`client-cert.pem`、`client-key.pem`;服务端证书没有 SAN 的旧版本还需提供与服务端完全一致的 `server-cert.pem`。指定 `ssl_path` 时,未协商加密的连接会报错。`ssl_pwd` 支持传统 PEM 加密私钥;加密 PKCS#8 私钥和 UKey 仍不支持。 +- **连接安全**:`ssl_path` 支持使用客户端证书与私钥连接启用加密的 DM8。目录需包含 `ca-cert.pem`、`client-cert.pem`、`client-key.pem`;服务端证书没有 SAN 的旧版本还需提供与服务端完全一致的 `server-cert.pem`。指定 `ssl_path` 时,未协商加密的连接会报错。`ssl_pwd` 支持传统 PEM 和 PBES2 加密的 PKCS#8 私钥;UKey 仍不支持。 - **主备与 MPP**:读写分离模式 1 和 4 已在本机 DM8 主备环境、自动提交模式下验证;通过双端点服务名建立的新连接也通过了自动接管后的回归。MPP 全局和本地登录已在本机两节点集群验证分布式读写。 ## 路线图与状态 diff --git a/docs/api-reference.md b/docs/api-reference.md index 0adc712..12a96ef 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -62,7 +62,7 @@ dmPython.connect( - IPv6 地址使用方括号,例如 `server="[::1]"`;`dsn` 可写为 `"[::1]:5236"`。 - `dmsvc_path` 指向包含 `dm_svc.conf` 的目录;连接时可把 `server` 设为配置文件中的服务名。双端点服务名配置 `LOGIN_MODE=1` 后,已在本机 DM8 主备环境验证故障自动接管后的**新连接**会选择晋升的新主库;已有连接的自动恢复尚未验证。 - `mpp_login` 接受 `DSQL_MPP_LOGIN_GLOBAL` 或 `DSQL_MPP_LOGIN_LOCAL`;`rwseparate` 接受 `DSQL_RWSEPARATE_OFF`、`DSQL_RWSEPARATE_ON` 或 `DSQL_RWSEPARATE_ON2`,`rwseparate_percent` 范围为 0–100。这些选项在建连时传给底层驱动。读写分离模式 1 和 4 已在本机 DM8 主备环境中验证自动提交模式下的查询路由;MPP 全局和本地登录已在本机两节点集群验证分布式读写。 -- `ssl_path` 指向含 `ca-cert.pem`、`client-cert.pem`、`client-key.pem` 的目录。服务端证书没有 SAN 时还需提供准确的 `server-cert.pem`,用于证书固定校验;有 SAN 的证书按 CA 链和主机名校验。设置后若服务端未协商加密,连接失败。`ssl_pwd` 可解密传统 PEM 格式的加密客户端私钥,必须与 `ssl_path` 一起使用;加密 PKCS#8 私钥尚不支持。非空的 `ukey_name`、`ukey_pin` 暂不支持。 +- `ssl_path` 指向含 `ca-cert.pem`、`client-cert.pem`、`client-key.pem` 的目录。服务端证书没有 SAN 时还需提供准确的 `server-cert.pem`,用于证书固定校验;有 SAN 的证书按 CA 链和主机名校验。设置后若服务端未协商加密,连接失败。`ssl_pwd` 可解密传统 PEM 或 PBES2 加密的 PKCS#8 客户端私钥,必须与 `ssl_path` 一起使用。非空的 `ukey_name`、`ukey_pin` 暂不支持。 - `user` 支持 `user/password@server:port[/schema][?catalog=...]` 形式。 - `login_timeout` 以毫秒为单位,默认 5000,限制首次建连握手;设为 0 表示不限制。`connection_timeout` 以秒为单位,默认 0 不限制,限制 SQL 执行时间。 - 常量参数建议使用模块常量(如 `DSQL_AUTOCOMMIT_ON`、`ISO_LEVEL_READ_COMMITTED`)。 diff --git a/docs/test-results/2026-09-27-ssl-connection.md b/docs/test-results/2026-09-27-ssl-connection.md index d394ea1..51f2d21 100644 --- a/docs/test-results/2026-09-27-ssl-connection.md +++ b/docs/test-results/2026-09-27-ssl-connection.md @@ -31,6 +31,10 @@ instance. The ARM macOS Python 3.10 extension also connected to the SSL-enabled DM8 instance with the bundled RSA client key re-encrypted in traditional PEM format. `ssl_pwd="test+ssl&pwd 123"` succeeded and executed a query; a missing -or wrong password failed. All five SSL tests passed locally. The password -without `ssl_path` was rejected on the ordinary DM8 instance. Encrypted -PKCS#8 keys and UKey login remain unsupported. +or wrong password failed. An OpenSSL-generated PBES2/AES-256-CBC PKCS#8 RSA +client key also connected and queried with `ssl_pwd="test+pkcs8&pwd 123"`; +missing and wrong passwords failed. All six SSL tests passed against the local +ARM DM8 instance. The password without `ssl_path` was rejected on the ordinary +DM8 instance. Because the PKCS#8 dependency updates `golang.org/x/text`, the +type matrix and Unicode CLOB regressions were rerun against local UTF8 and +GB18030 DM8 instances: 68 passed on each. UKey login remains unsupported. diff --git a/dpi_bridge/go.mod b/dpi_bridge/go.mod index 6e42122..8158065 100644 --- a/dpi_bridge/go.mod +++ b/dpi_bridge/go.mod @@ -8,5 +8,7 @@ replace gitee.com/chunanyong/dm => ./third_party/chunanyong_dm require ( github.com/golang/snappy v0.0.1 // indirect - golang.org/x/text v0.3.2 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/dpi_bridge/go.sum b/dpi_bridge/go.sum index 612ce0d..1393090 100644 --- a/dpi_bridge/go.sum +++ b/dpi_bridge/go.sum @@ -1,9 +1,48 @@ -gitee.com/chunanyong/dm v1.8.15 h1:GIyry2BAlSO7xug95QYbehHdx73md2ZYsSvMZ1Uab0g= -gitee.com/chunanyong/dm v1.8.15/go.mod h1:EPRJnuPFgbyOFgJ0TRYCTGzhq+ZT4wdyaj/GW/LLcNg= -gitee.com/chunanyong/dm v1.8.22 h1:H7fsrnUIvEA0jlDWew7vwELry1ff+tLMIu2Fk2cIBSg= -gitee.com/chunanyong/dm v1.8.22/go.mod h1:EPRJnuPFgbyOFgJ0TRYCTGzhq+ZT4wdyaj/GW/LLcNg= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/dpi_bridge/third_party/chunanyong_dm/PATCHES.md b/dpi_bridge/third_party/chunanyong_dm/PATCHES.md index 64c6bf2..f4b2c00 100644 --- a/dpi_bridge/third_party/chunanyong_dm/PATCHES.md +++ b/dpi_bridge/third_party/chunanyong_dm/PATCHES.md @@ -64,15 +64,17 @@ - Regression: `tests/ssl/test_ssl_connection.py` uses a real SSL-enabled DM8 instance, including wrong and missing pins. -## Patch: encrypted traditional PEM client keys +## Patch: encrypted traditional PEM and PKCS#8 client keys - Files: `a.go`, `n.go`, `security/zzi.go` - Pass `ssl_pwd` through the DPI bridge and connector, preserving special characters in the password. Decrypt traditional encrypted PEM private keys before the TLS handshake; reject a missing or wrong password and identify - encrypted PKCS#8 keys as unsupported. -- Regression: `security/zzi_test.go` checks local TLS handshakes; the real DM8 - `tests/ssl/test_ssl_connection.py` case uses an encrypted RSA client key. + encrypted PKCS#8 keys with the pinned MIT-licensed `github.com/youmark/pkcs8` + library. Reject missing or wrong passwords for both formats. +- Regression: `security/zzi_test.go` checks local TLS handshakes with encrypted + EC keys; real DM8 `tests/ssl/test_ssl_connection.py` cases use an RSA client + key encrypted in both formats, including an OpenSSL-generated PKCS#8 key. ## Patch: initial connection timeout through endpoint groups diff --git a/dpi_bridge/third_party/chunanyong_dm/go.mod b/dpi_bridge/third_party/chunanyong_dm/go.mod index f3d5b57..60801ce 100644 --- a/dpi_bridge/third_party/chunanyong_dm/go.mod +++ b/dpi_bridge/third_party/chunanyong_dm/go.mod @@ -4,5 +4,6 @@ go 1.13 require ( github.com/golang/snappy v0.0.1 - golang.org/x/text v0.3.2 + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 + golang.org/x/text v0.14.0 ) diff --git a/dpi_bridge/third_party/chunanyong_dm/go.sum b/dpi_bridge/third_party/chunanyong_dm/go.sum index bbde4ab..1393090 100644 --- a/dpi_bridge/third_party/chunanyong_dm/go.sum +++ b/dpi_bridge/third_party/chunanyong_dm/go.sum @@ -1,5 +1,48 @@ github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/dpi_bridge/third_party/chunanyong_dm/security/zzi.go b/dpi_bridge/third_party/chunanyong_dm/security/zzi.go index f22019a..9ce88c1 100644 --- a/dpi_bridge/third_party/chunanyong_dm/security/zzi.go +++ b/dpi_bridge/third_party/chunanyong_dm/security/zzi.go @@ -17,6 +17,8 @@ import ( "path/filepath" "sync" "time" + + "github.com/youmark/pkcs8" ) // var dmHome = flag.String("DM_HOME", "", "Where DMDB installed") @@ -98,7 +100,33 @@ func loadClientKeyPair(certPath, keyPath, password string) (tls.Certificate, err return tls.Certificate{}, errors.New("SSL private key is not PEM encoded") } if block.Type == "ENCRYPTED PRIVATE KEY" { - return tls.Certificate{}, errors.New("encrypted PKCS#8 SSL private keys are not supported") + if password == "" { + return tls.Certificate{}, errors.New("encrypted SSL private key requires ssl_pwd") + } + privateKey, err := pkcs8.ParsePKCS8PrivateKey(block.Bytes, []byte(password)) + if err != nil { + return tls.Certificate{}, fmt.Errorf("failed to decrypt PKCS#8 SSL private key: %w", err) + } + der, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return tls.Certificate{}, err + } + defer func() { + for i := range der { + der[i] = 0 + } + }() + certPEM, err := os.ReadFile(certPath) + if err != nil { + return tls.Certificate{}, err + } + plainKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}) + defer func() { + for i := range plainKeyPEM { + plainKeyPEM[i] = 0 + } + }() + return tls.X509KeyPair(certPEM, plainKeyPEM) } if !x509.IsEncryptedPEMBlock(block) { return tls.LoadX509KeyPair(certPath, keyPath) diff --git a/dpi_bridge/third_party/chunanyong_dm/security/zzi_test.go b/dpi_bridge/third_party/chunanyong_dm/security/zzi_test.go index a744244..b205c41 100644 --- a/dpi_bridge/third_party/chunanyong_dm/security/zzi_test.go +++ b/dpi_bridge/third_party/chunanyong_dm/security/zzi_test.go @@ -14,6 +14,8 @@ import ( "strings" "testing" "time" + + "github.com/youmark/pkcs8" ) func testCertificate(t *testing.T, template, parent *x509.Certificate, signer *ecdsa.PrivateKey) ([]byte, *ecdsa.PrivateKey) { @@ -162,3 +164,34 @@ func TestEncryptedClientKey(t *testing.T) { } } } + +func TestEncryptedPKCS8ClientKey(t *testing.T) { + dir, serverCert := testTLSFiles(t) + keyPath := filepath.Join(dir, "client-key.pem") + keyPEM, err := os.ReadFile(keyPath) + if err != nil { + t.Fatal(err) + } + block, _ := pem.Decode(keyPEM) + if block == nil { + t.Fatal("missing private key") + } + privateKey, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + t.Fatal(err) + } + password := "test+pkcs8&pwd 123" + encrypted, err := pkcs8.MarshalPrivateKey(privateKey, []byte(password), nil) + if err != nil { + t.Fatal(err) + } + writeTestFile(t, keyPath, pem.EncodeToMemory(&pem.Block{Type: "ENCRYPTED PRIVATE KEY", Bytes: encrypted})) + if err := testTLSHandshake(t, dir, serverCert, "localhost", password); err != nil { + t.Fatalf("correct PKCS#8 ssl_pwd: %v", err) + } + for _, wrong := range []string{"", "wrong-password"} { + if err := testTLSHandshake(t, dir, serverCert, "localhost", wrong); err == nil { + t.Fatalf("PKCS#8 ssl_pwd %q should fail", wrong) + } + } +} diff --git a/scripts/check_sdist_contents.py b/scripts/check_sdist_contents.py index f3b5ea2..f9162b3 100644 --- a/scripts/check_sdist_contents.py +++ b/scripts/check_sdist_contents.py @@ -9,6 +9,7 @@ REQUIRED = { + "THIRD_PARTY_NOTICES.md", "dpi_bridge/go.mod", "dpi_bridge/go.sum", "dpi_bridge/third_party/chunanyong_dm/go.mod", diff --git a/setup.py b/setup.py index 2d94b4b..dafe167 100644 --- a/setup.py +++ b/setup.py @@ -173,6 +173,7 @@ def _build_go_bridge(self): cmdclass={"build_ext": build_ext}, keywords="Dameng DM8 database DB-API", license="MulanPSL-2.0", + license_files=["LICENSE", "THIRD_PARTY_NOTICES.md"], python_requires=">=3.9,<3.14", classifiers=[ "Development Status :: 4 - Beta", diff --git a/tests/ssl/test_ssl_connection.py b/tests/ssl/test_ssl_connection.py index 65f429d..e365173 100644 --- a/tests/ssl/test_ssl_connection.py +++ b/tests/ssl/test_ssl_connection.py @@ -105,3 +105,33 @@ def test_encrypted_client_key_password(ssl_params, tmp_path): with conn.cursor() as cur: cur.execute("SELECT 1") assert cur.fetchone() == (1,) + + +def test_encrypted_pkcs8_client_key_password(ssl_params, tmp_path): + cert_dir = tmp_path / "encrypted-pkcs8-client-key" + shutil.copytree(ssl_params["ssl_path"], cert_dir) + key_path = cert_dir / "client-key.pem" + encrypted_path = cert_dir / "encrypted-key.pem" + password = "test+pkcs8&pwd 123" + subprocess.run( + [ + "openssl", "pkcs8", "-topk8", "-v2", "aes-256-cbc", "-iter", "10000", + "-in", str(key_path), "-out", str(encrypted_path), + "-passout", "env:DM_SSL_KEY_PWD", + ], + env={**os.environ, "DM_SSL_KEY_PWD": password}, + check=True, + capture_output=True, + text=True, + ) + encrypted_path.replace(key_path) + + options = {**ssl_params, "ssl_path": str(cert_dir)} + with pytest.raises(dmPython.Error, match="requires ssl_pwd"): + dmPython.connect(**options) + with pytest.raises(dmPython.Error, match="decrypt PKCS#8 SSL private key"): + dmPython.connect(**options, ssl_pwd="wrong-password") + with dmPython.connect(**options, ssl_pwd=password) as conn: + with conn.cursor() as cur: + cur.execute("SELECT 1") + assert cur.fetchone() == (1,)