From d0fbbd12c0699e77c6dfa4f28379462c8d0e844d Mon Sep 17 00:00:00 2001 From: yuqei Date: Tue, 6 Oct 2026 18:35:42 +0800 Subject: [PATCH] fix(kms): modernize audited Alibaba SDK closure --- .github/workflows/build-test-quality-gate.yml | 69 ++- .../services/kms-gateway/README.md | 21 +- .../services/kms-gateway/pyproject.toml | 16 +- .../tests/test_sdk_compatibility.py | 148 +++++ .../vendor/alibaba-kms-modern/LICENSE | 201 +++++++ .../vendor/alibaba-kms-modern/README.md | 50 ++ .../vendor/alibaba-kms-modern/UPSTREAM.json | 89 +++ .../vendor/alibaba-kms-modern/build_forks.py | 517 ++++++++++++++++++ .../vendor/verify_dependency_closure.py | 111 ++++ yuqei-infra/docker/kms-gateway.Dockerfile | 53 +- ...gateway-audit-tools-linux-x86_64-py312.txt | 32 ++ .../kms-gateway-build-tools-py312.txt | 13 + ...kms-gateway-runtime-linux-x86_64-py312.txt | 131 +++++ .../requirements/kms-gateway-test-py312.txt | 32 ++ 14 files changed, 1457 insertions(+), 26 deletions(-) create mode 100644 yuqei-contract-platform/services/kms-gateway/tests/test_sdk_compatibility.py create mode 100644 yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/LICENSE create mode 100644 yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/README.md create mode 100644 yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/UPSTREAM.json create mode 100644 yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/build_forks.py create mode 100644 yuqei-contract-platform/services/kms-gateway/vendor/verify_dependency_closure.py create mode 100644 yuqei-infra/requirements/kms-gateway-audit-tools-linux-x86_64-py312.txt create mode 100644 yuqei-infra/requirements/kms-gateway-build-tools-py312.txt create mode 100644 yuqei-infra/requirements/kms-gateway-runtime-linux-x86_64-py312.txt create mode 100644 yuqei-infra/requirements/kms-gateway-test-py312.txt diff --git a/.github/workflows/build-test-quality-gate.yml b/.github/workflows/build-test-quality-gate.yml index f5f94852..cbd72c1c 100644 --- a/.github/workflows/build-test-quality-gate.yml +++ b/.github/workflows/build-test-quality-gate.yml @@ -43,10 +43,11 @@ jobs: kms-gateway: name: Isolated Alibaba and Tencent KMS gateway runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 20 env: - PIP_INDEX_URL: https://pypi.tuna.tsinghua.edu.cn/simple + PIP_INDEX_URL: https://pypi.org/simple PIP_DISABLE_PIP_VERSION_CHECK: "1" + PYTHONPATH: ${{ github.workspace }}/yuqei-contract-platform/services/kms-gateway/src steps: - name: Checkout uses: actions/checkout@v4 @@ -57,19 +58,73 @@ jobs: python-version: "3.12" cache: pip - - name: Install isolated KMS gateway - working-directory: yuqei-contract-platform/services/kms-gateway + - name: Install pinned KMS build toolchain run: | - python -m pip install --upgrade pip - python -m pip install -e ".[test]" + python -m pip install --only-binary=:all: --require-hashes \ + --requirement yuqei-infra/requirements/kms-gateway-build-tools-py312.txt + + - name: Build reproducible Alibaba compatibility wheels + run: | + mkdir -p "$RUNNER_TEMP/kms-fork-build" "$RUNNER_TEMP/kms-fork-wheels" + python yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/build_forks.py \ + --work-dir "$RUNNER_TEMP/kms-fork-build" \ + --wheel-dir "$RUNNER_TEMP/kms-fork-wheels" \ + > "$RUNNER_TEMP/kms-fork-manifest.json" + python -m json.tool "$RUNNER_TEMP/kms-fork-manifest.json" > /dev/null + + - name: Install hash-locked KMS dependency closure + run: | + mkdir -p "$RUNNER_TEMP/kms-runtime-inputs" + python -m pip download --no-build-isolation --no-deps --require-hashes \ + --dest "$RUNNER_TEMP/kms-runtime-inputs" \ + --requirement yuqei-infra/requirements/kms-gateway-runtime-linux-x86_64-py312.txt + python -m pip install --no-build-isolation --require-hashes \ + --no-index --find-links "$RUNNER_TEMP/kms-runtime-inputs" \ + --requirement yuqei-infra/requirements/kms-gateway-runtime-linux-x86_64-py312.txt + python -m pip install --no-deps "$RUNNER_TEMP/kms-fork-wheels"/*.whl + python -m pip install --only-binary=:all: --require-hashes \ + --requirement yuqei-infra/requirements/kms-gateway-test-py312.txt - name: Verify both official cloud SDKs and gateway contracts working-directory: yuqei-contract-platform/services/kms-gateway run: | - python -c "from alibabacloud_kms_kms20160120.client import Client as AliClient; from tencentcloud.kms.v20190118.kms_client import KmsClient as TencentClient; print('cloud KMS SDK imports passed')" + KMS_SITE_PACKAGES="$(python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')" + python vendor/verify_dependency_closure.py \ + --project pyproject.toml \ + --path "$KMS_SITE_PACKAGES" \ + --forbid pyOpenSSL + python -c "import importlib.metadata as m, importlib.util; from alibabacloud_kms_kms20160120.client import Client as AliClient; from openapi_util.protobuf import api_pb2; from tencentcloud.kms.v20190118.kms_client import KmsClient as TencentClient; assert AliClient and TencentClient; assert m.version('alibabacloud-kms-python-sdk') == '1.1.3+yuqei.1'; assert m.version('alibabacloud-dkms-gcs') == '1.0.2+yuqei.2'; assert m.version('protobuf') == '6.33.6'; assert m.version('cryptography') == '50.0.2'; assert importlib.util.find_spec('OpenSSL') is None; request = api_pb2.EncryptRequest(KeyId='ci', Plaintext=b'payload'); assert api_pb2.EncryptRequest.FromString(request.SerializeToString()) == request" python -m pip check python -m pytest -q + - name: Audit the installed KMS environment + run: | + python -m venv "$RUNNER_TEMP/kms-audit-tool" + "$RUNNER_TEMP/kms-audit-tool/bin/python" -m pip install \ + --only-binary=:all: --require-hashes \ + --requirement yuqei-infra/requirements/kms-gateway-audit-tools-linux-x86_64-py312.txt + KMS_SITE_PACKAGES="$(python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')" + KMS_AUDIT_SITE_PACKAGES="$("$RUNNER_TEMP/kms-audit-tool/bin/python" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')" + "$RUNNER_TEMP/kms-audit-tool/bin/python" -m pip_audit \ + --strict --vulnerability-service osv --progress-spinner off \ + --path "$KMS_SITE_PACKAGES" \ + --format json --output "$RUNNER_TEMP/kms-pip-audit.json" + "$RUNNER_TEMP/kms-audit-tool/bin/python" -m pip_audit \ + --strict --vulnerability-service osv --progress-spinner off \ + --path "$KMS_AUDIT_SITE_PACKAGES" \ + --format json --output "$RUNNER_TEMP/kms-audit-tool-pip-audit.json" + + - name: Upload KMS dependency evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: kms-gateway-dependency-evidence + path: | + ${{ runner.temp }}/kms-fork-manifest.json + ${{ runner.temp }}/kms-pip-audit.json + ${{ runner.temp }}/kms-audit-tool-pip-audit.json + if-no-files-found: warn + python: name: Python tests and Contract API migration runs-on: ubuntu-latest diff --git a/yuqei-contract-platform/services/kms-gateway/README.md b/yuqei-contract-platform/services/kms-gateway/README.md index 0b6bd921..973fea3e 100644 --- a/yuqei-contract-platform/services/kms-gateway/README.md +++ b/yuqei-contract-platform/services/kms-gateway/README.md @@ -1,8 +1,8 @@ # Yuqei KMS gateway This service isolates cloud-vendor SDKs from the contract API. The Alibaba Cloud -SDK currently pins an older cryptography stack, so it must run in its own image or -virtual environment. +KMS Instance SDK is supplied by the reviewed compatibility build under +`vendor/alibaba-kms-modern`; it must run in its own image or virtual environment. The contract API calls `POST /v1/encrypt`, `POST /v1/decrypt`, `POST /v1/sign`, and `POST /v1/public-key` with a service bearer token loaded from a file. @@ -10,14 +10,21 @@ Envelope-encryption keys and asymmetric signing keys use separate allowlists. Cloud credentials, client keys and passwords are file-backed and are never accepted in request bodies; private signing material never leaves KMS. -Create an isolated environment and start the gateway: +The canonical build is the repository Dockerfile. It downloads hash-locked +runtime inputs, builds the two Alibaba compatibility wheels from hash-verified +upstream sources, and verifies the dependency closure without consulting global +site-packages: -```powershell -python -m venv .venv -.\.venv\Scripts\python.exe -m pip install -e ".[test]" -.\.venv\Scripts\python.exe -m uvicorn yuqei_kms_gateway.main:app --host 127.0.0.1 --port 8102 +```bash +docker build -f yuqei-infra/docker/kms-gateway.Dockerfile . ``` +Do not use `pip install -e ".[test]"` directly: the `+yuqei` distributions are +intentionally absent from public package indexes. For a local Linux x86_64 +Python 3.12 test environment, replay the `kms-gateway` job in +`.github/workflows/build-test-quality-gate.yml`, including all three hash locks, +then run `PYTHONPATH=src python -m pytest -q`. + `GET /health` is a non-secret liveness/configuration summary. Provider calls are not made by the health endpoint. Production readiness still requires a real round-trip recovery drill from the contract administration console. diff --git a/yuqei-contract-platform/services/kms-gateway/pyproject.toml b/yuqei-contract-platform/services/kms-gateway/pyproject.toml index f933b4a3..9087cc26 100644 --- a/yuqei-contract-platform/services/kms-gateway/pyproject.toml +++ b/yuqei-contract-platform/services/kms-gateway/pyproject.toml @@ -4,17 +4,19 @@ version = "0.1.0" description = "Isolated Alibaba Cloud and Tencent Cloud KMS data-plane gateway" requires-python = ">=3.12" dependencies = [ - "alibabacloud-kms-python-sdk==1.1.3", - "cryptography>=44", - "fastapi>=0.115", - "pydantic>=2.10", - "pydantic-settings>=2.7", + "alibabacloud-dkms-gcs==1.0.2+yuqei.2", + "alibabacloud-kms-python-sdk==1.1.3+yuqei.1", + "cryptography==50.0.2", + "fastapi==0.142.2", + "protobuf==6.33.6", + "pydantic==2.13.5", + "pydantic-settings==2.15.0", "tencentcloud-sdk-python-kms==3.1.164", - "uvicorn>=0.34", + "uvicorn==0.54.0", ] [project.optional-dependencies] -test = ["pytest>=8.3", "httpx>=0.28"] +test = ["pytest==9.1.1", "httpx==0.28.1"] [build-system] requires = ["hatchling"] diff --git a/yuqei-contract-platform/services/kms-gateway/tests/test_sdk_compatibility.py b/yuqei-contract-platform/services/kms-gateway/tests/test_sdk_compatibility.py new file mode 100644 index 00000000..63cf1ae2 --- /dev/null +++ b/yuqei-contract-platform/services/kms-gateway/tests/test_sdk_compatibility.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +import base64 +import datetime as dt +import importlib.metadata +import importlib.util +import json +from types import SimpleNamespace + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.hazmat.primitives.serialization import pkcs12 +from cryptography.x509.oid import NameOID + + +def test_both_cloud_sdk_imports_use_controlled_alibaba_forks() -> None: + from alibabacloud_kms_kms20160120.client import Client as AliyunClient + from openapi_util.protobuf import api_pb2 + from tencentcloud.kms.v20190118.kms_client import KmsClient as TencentClient + + assert AliyunClient is not None + assert TencentClient is not None + assert importlib.metadata.version("alibabacloud-kms-python-sdk") == "1.1.3+yuqei.1" + assert importlib.metadata.version("alibabacloud-dkms-gcs") == "1.0.2+yuqei.2" + assert importlib.metadata.version("protobuf") == "6.33.6" + assert importlib.util.find_spec("OpenSSL") is None + + request = api_pb2.EncryptRequest(KeyId="synthetic-key", Plaintext=b"payload") + assert api_pb2.EncryptRequest.FromString(request.SerializeToString()) == request + + for distribution in ("alibabacloud-kms-python-sdk", "alibabacloud-dkms-gcs"): + requirements = importlib.metadata.requires(distribution) or [] + assert all("pyopenssl" not in requirement.lower() for requirement in requirements) + + +def test_controlled_fork_runs_aap_client_key_transfer_and_signature_path(tmp_path, monkeypatch) -> None: + from alibabacloud_kms_kms20160120.client import Client as KmsSdkClient + from openapi.models import Config + from openapi_util.client import Client as OpenApiUtilClient + from openapi_util.protobuf import api_pb2 + from sdk import models as sdk_models + from Tea.core import TeaCore + + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "kms-compat.test")]) + now = dt.datetime.now(dt.timezone.utc) + certificate = ( + x509.CertificateBuilder() + .subject_name(name) + .issuer_name(name) + .public_key(private_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now - dt.timedelta(minutes=1)) + .not_valid_after(now + dt.timedelta(days=1)) + .sign(private_key, hashes.SHA256()) + ) + password = "synthetic-client-key-password" + client_key = pkcs12.serialize_key_and_certificates( + b"synthetic-client-key", + private_key, + certificate, + None, + serialization.BestAvailableEncryption(password.encode("utf-8")), + ) + + key_id = "synthetic-client-key-id" + client_key_path = tmp_path / "ClientKey_synthetic.json" + client_key_path.write_text( + json.dumps( + { + "KeyId": key_id, + "PrivateKeyData": base64.b64encode(client_key).decode("ascii"), + } + ), + encoding="utf-8", + ) + ca_path = tmp_path / "ca.pem" + ca_path.write_bytes(certificate.public_bytes(serialization.Encoding.PEM)) + + client = KmsSdkClient( + kms_instance_config=Config( + client_key_file=str(client_key_path), + password=password, + endpoint="kms-instance.example.test", + protocol="https", + ca_file_path=str(ca_path), + read_timeout=1000, + connect_timeout=1000, + ignore_ssl=False, + ) + ) + + response_bytes = api_pb2.EncryptResponse( + KeyId="synthetic-data-key-id", + CiphertextBlob=b"synthetic-ciphertext", + Iv=b"synthetic-iv", + RequestId="synthetic-request-id", + Algorithm="AES_GCM", + PaddingMode="NO_PADDING", + ).SerializeToString() + captured = {} + + def fake_do_action(request, runtime): + captured["request"] = request + captured["runtime"] = runtime + return SimpleNamespace(status_code=200, headers={}, body=response_bytes) + + monkeypatch.setattr(TeaCore, "do_action", staticmethod(fake_do_action)) + response = client.encrypt( + sdk_models.EncryptRequest( + key_id="synthetic-data-key-id", + plaintext=b"synthetic-plaintext", + algorithm="AES_GCM", + aad=b"synthetic-aad", + iv=b"synthetic-iv", + padding_mode="NO_PADDING", + ) + ) + + assert response.key_id == "synthetic-data-key-id" + assert response.ciphertext_blob == b"synthetic-ciphertext" + outbound = captured["request"] + assert outbound.protocol == "https" + assert outbound.headers["host"] == "kms-instance.example.test" + assert outbound.headers["x-kms-acccesskeyid"] == key_id + assert outbound.headers["authorization"].startswith("Bearer ") + assert captured["runtime"]["ca"] == str(ca_path) + assert captured["runtime"]["ignoreSSL"] is False + + protobuf_request = api_pb2.EncryptRequest.FromString(outbound.body) + assert protobuf_request.KeyId == "synthetic-data-key-id" + assert protobuf_request.Plaintext == b"synthetic-plaintext" + assert protobuf_request.Aad == b"synthetic-aad" + + string_to_sign = OpenApiUtilClient.get_string_to_sign( + outbound.method, + outbound.pathname, + outbound.headers, + outbound.query, + ) + signature = base64.b64decode(outbound.headers["authorization"].removeprefix("Bearer ")) + private_key.public_key().verify( + signature, + string_to_sign.encode("utf-8"), + padding.PKCS1v15(), + hashes.SHA256(), + ) diff --git a/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/LICENSE b/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/README.md b/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/README.md new file mode 100644 index 00000000..05210326 --- /dev/null +++ b/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/README.md @@ -0,0 +1,50 @@ +# Controlled Alibaba KMS SDK forks + +The gateway retains Alibaba KMS Instance SDK behavior because its deployed +configuration uses AAP ClientKey authentication: an encrypted PKCS#12 client +key, password, private endpoint, and explicit CA. Alibaba's maintained +`alibabacloud_kms20160120` client supports the required business operations, +but its generated Python client marks them as `auth_type = "AK"`. Replacing +the client would therefore change authentication material and the deployment +contract instead of being a dependency-only security fix. + +`build_forks.py` creates two narrow, reviewable forks from upstream PyPI source +archives whose SHA-256 digests and sensitive source-file hashes are pinned: + +- `alibabacloud-kms-python-sdk` `1.1.3+yuqei.1` replaces only the DKMS and + protobuf dependency constraints. +- `alibabacloud-dkms-gcs` `1.0.2+yuqei.2` replaces the removed pyOpenSSL + PKCS#12 API with `cryptography`, removes the obsolete pyOpenSSL dependency, + raises protobuf to the audited 6.33 line, and reconstructs the generated + message classes from the exact upstream serialized descriptor using the + current protobuf generated-code descriptor-pool/builder pattern. + +No provider endpoint, request/response model, transfer-handler, or gateway +source is patched. The compatibility tests exercise both real SDK imports, an +exact protobuf serialize/parse round trip, synthetic ClientKey JSON parsing, +real `KmsSdkClient` initialization, AAP protobuf transfer serialization, CA/TLS +runtime settings, response parsing, and public-key verification of the emitted +authorization signature. The build fails closed if an +upstream archive, reviewed source file, patched output, protobuf descriptor, or +final wheel differs from the recorded bytes. Wheels carry local version +suffixes so they cannot be confused with upstream releases. + +Builds set a fixed `SOURCE_DATE_EPOCH` and `PYTHONHASHSEED`; the pinned Python +3.12 builder requires the recorded wheel SHA-256 digests. Two clean Windows +x86_64 CPython 3.12.14 builds were byte-identical. Linux x86_64 was not executed +in that local verification environment, so the Linux CI/container build remains +a required fail-closed gate rather than a claimed local result. The emitted +JSON manifest records every patched file digest plus the resulting wheel +digests. + +The upstream packages are Apache-2.0 licensed. Both reviewed repository trees +contain the same `LICENSE` and no `NOTICE` file. Their READMEs contain the same +Alibaba Cloud copyright line; setuptools retains that line in each wheel's +`METADATA`, and the builder verifies it. The license is hash-verified before +each build and copied into both wheels. Modified source files receive an +explicit modification notice. `UPSTREAM.json` records source, per-file patch, +copyright, and license provenance. + +This is a compatibility fork, not evidence of a live Alibaba Cloud KMS call. +The compatibility test uses a synthetic encrypted PKCS#12 key and verifies a +local RSA signature only. diff --git a/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/UPSTREAM.json b/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/UPSTREAM.json new file mode 100644 index 00000000..35106556 --- /dev/null +++ b/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/UPSTREAM.json @@ -0,0 +1,89 @@ +{ + "schema_version": 1, + "license": { + "spdx": "Apache-2.0", + "source_urls": [ + "https://raw.githubusercontent.com/aliyun/alibabacloud-kms-python-sdk/3b7c890ee754370937dd4831e1b8709a56f5baae/LICENSE", + "https://raw.githubusercontent.com/aliyun/alibabacloud-dkms-gcs-python-sdk/48aa96f6e9daf2021c2767d2d2ab8720d90f847c/LICENSE" + ], + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + }, + "attribution": { + "notice_file_present": false, + "copyright": "Copyright (c) 2009-present, Alibaba Cloud All rights reserved.", + "retained_in_wheel_metadata": true + }, + "reproducible_build": { + "python": "3.12", + "pip": "26.2.1", + "setuptools": "84.0.0", + "wheel": "0.48.0", + "source_date_epoch": "1704067200", + "locally_verified_platforms": [ + "windows-x86_64-cpython-3.12.14" + ], + "linux_x86_64_ci_required": true, + "wheel_sha256": { + "alibabacloud_dkms_gcs-1.0.2+yuqei.2-py3-none-any.whl": "44d9832a06147f2476ec1aa3bbe7033c212e7155533475e0cb470d780789bebc", + "alibabacloud_kms_python_sdk-1.1.3+yuqei.1-py3-none-any.whl": "44013dbb5215fb6406b4fa26653b38c81c529e130b4086cd09a74efa625bd542" + } + }, + "sources": [ + { + "distribution": "alibabacloud-kms-python-sdk", + "upstream_version": "1.1.3", + "fork_version": "1.1.3+yuqei.1", + "repository": "https://github.com/aliyun/alibabacloud-kms-python-sdk", + "upstream_commit": "3b7c890ee754370937dd4831e1b8709a56f5baae", + "source_url": "https://files.pythonhosted.org/packages/33/68/0a01975d4f9638d66aea5d95147ceab3d1fde0ef8374df3890586b12fd16/alibabacloud-kms-python-sdk-1.1.3.tar.gz", + "source_sha256": "5feb0375ea21e3a103b120ca84a77f4544fdc2318fc26761cbb974593e5e527d", + "readme_sha256": "42f0eed02c96a13d1746894b3b64fa2a8d84ef969113dd99b2f69c3578bcdc7a", + "modified_files": { + "setup.py": { + "upstream_sha256": "765ce8c4683a66560015ba05108fbebbb756fb8f39b2a63334b40a4c476fbc35", + "patched_sha256": "e83f26383e0f73e263ffcb7c69765b9d4b602bc9dc489c82c73bf5abd80a3154", + "patch": "local version; controlled DKMS dependency; protobuf >=6.33.6,<7" + } + } + }, + { + "distribution": "alibabacloud-dkms-gcs", + "upstream_version": "1.0.2", + "fork_version": "1.0.2+yuqei.2", + "repository": "https://github.com/aliyun/alibabacloud-dkms-gcs-python-sdk", + "upstream_tag": "v1.0.2", + "upstream_commit": "48aa96f6e9daf2021c2767d2d2ab8720d90f847c", + "source_url": "https://files.pythonhosted.org/packages/48/e3/3c677b024a4b58f27ff4060be0f27a6d7cf9926c1283505d45d5eb499b77/alibabacloud-dkms-gcs-1.0.2.tar.gz", + "source_sha256": "bae1098ec5f4e087f787bae689481bb029d41e006ecbe8368e3ab37ab6b885ec", + "readme_sha256": "b607773cd6f0d2d90bc4bf899016e44b37cc5acab91c6c49a8470f432671d8f0", + "protobuf_descriptor_sha256": "87faea6229dfa0174823542b62ac86e37c3b772145ec9148c7ba6a7e15308b76", + "modified_files": { + "setup.py": { + "upstream_sha256": "9d7bf8d8ed9a9343a47e6eeb7f8f0d6dd52ea52b61ab1d208ba86489aab7b47e", + "patched_sha256": "037776b7b6c9b9b201aba700bdf3f2ada2c64a91b123cb1524671dbfa8f7bf31", + "patch": "local version; protobuf >=6.33.6,<7; cryptography >=50.0.2,<51; remove pyOpenSSL" + }, + "openapi_util/client.py": { + "upstream_sha256": "91ac249db91777b52cda3aaf777fb92721e401c965f5bb7009ea661aad82853e", + "patched_sha256": "52535dbb10dc2943cc1b21f6392b70db99877bf46523b87cea089bd8e9034d1a", + "patch": "replace removed pyOpenSSL PKCS12 API with cryptography; preserve RSA signing key format" + }, + "openapi_util/protobuf/api_pb2.py": { + "upstream_sha256": "cee7f781bc3f413161b8aba2661f0e33239afb0f4ac64db72b146d351ddf2bc0", + "patched_sha256": "560232dfc5fd094258c2b6868b20a6930b0fc0f8eea6222dd098f0768ef95746", + "patch": "rebuild messages from the exact reviewed serialized descriptor with the modern protobuf builder API" + }, + "openapi_util/__init__.py": { + "upstream_sha256": "6372d27d18a84a5db1721ef4a6afd42e5bf21025f212d37792b55a59e1b26b19", + "patched_sha256": "54be5ed28cbb6ae730f075b0b01ea717793ba044c25403a27ce2e65b4857d55c", + "patch": "local version" + }, + "sdk/__init__.py": { + "upstream_sha256": "6372d27d18a84a5db1721ef4a6afd42e5bf21025f212d37792b55a59e1b26b19", + "patched_sha256": "54be5ed28cbb6ae730f075b0b01ea717793ba044c25403a27ce2e65b4857d55c", + "patch": "local version" + } + } + } + ] +} diff --git a/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/build_forks.py b/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/build_forks.py new file mode 100644 index 00000000..2f3cfe3c --- /dev/null +++ b/yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern/build_forks.py @@ -0,0 +1,517 @@ +from __future__ import annotations + +import argparse +import ast +import hashlib +import importlib.metadata +import json +import os +import shutil +import subprocess +import sys +import tarfile +import urllib.request +import zipfile +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class UpstreamSource: + distribution: str + upstream_version: str + fork_version: str + root: str + url: str + sha256: str + expected_files: dict[str, str] + modified_files: tuple[str, ...] + + +SOURCES = ( + UpstreamSource( + distribution="alibabacloud-kms-python-sdk", + upstream_version="1.1.3", + fork_version="1.1.3+yuqei.1", + root="alibabacloud-kms-python-sdk-1.1.3", + url=( + "https://files.pythonhosted.org/packages/33/68/" + "0a01975d4f9638d66aea5d95147ceab3d1fde0ef8374df3890586b12fd16/" + "alibabacloud-kms-python-sdk-1.1.3.tar.gz" + ), + sha256="5feb0375ea21e3a103b120ca84a77f4544fdc2318fc26761cbb974593e5e527d", + expected_files={ + "setup.py": "765ce8c4683a66560015ba05108fbebbb756fb8f39b2a63334b40a4c476fbc35", + "README.rst": "42f0eed02c96a13d1746894b3b64fa2a8d84ef969113dd99b2f69c3578bcdc7a", + }, + modified_files=("setup.py",), + ), + UpstreamSource( + distribution="alibabacloud-dkms-gcs", + upstream_version="1.0.2", + fork_version="1.0.2+yuqei.2", + root="alibabacloud-dkms-gcs-1.0.2", + url=( + "https://files.pythonhosted.org/packages/48/e3/" + "3c677b024a4b58f27ff4060be0f27a6d7cf9926c1283505d45d5eb499b77/" + "alibabacloud-dkms-gcs-1.0.2.tar.gz" + ), + sha256="bae1098ec5f4e087f787bae689481bb029d41e006ecbe8368e3ab37ab6b885ec", + expected_files={ + "setup.py": "9d7bf8d8ed9a9343a47e6eeb7f8f0d6dd52ea52b61ab1d208ba86489aab7b47e", + "openapi_util/client.py": ( + "91ac249db91777b52cda3aaf777fb92721e401c965f5bb7009ea661aad82853e" + ), + "openapi_util/protobuf/api_pb2.py": ( + "cee7f781bc3f413161b8aba2661f0e33239afb0f4ac64db72b146d351ddf2bc0" + ), + "openapi_util/__init__.py": ( + "6372d27d18a84a5db1721ef4a6afd42e5bf21025f212d37792b55a59e1b26b19" + ), + "sdk/__init__.py": ( + "6372d27d18a84a5db1721ef4a6afd42e5bf21025f212d37792b55a59e1b26b19" + ), + "README.md": "b607773cd6f0d2d90bc4bf899016e44b37cc5acab91c6c49a8470f432671d8f0", + }, + modified_files=( + "setup.py", + "openapi_util/client.py", + "openapi_util/protobuf/api_pb2.py", + "openapi_util/__init__.py", + "sdk/__init__.py", + ), + ), +) + +PROTO_DESCRIPTOR_SHA256 = "87faea6229dfa0174823542b62ac86e37c3b772145ec9148c7ba6a7e15308b76" +LICENSE_SHA256 = "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" +SOURCE_DATE_EPOCH = "1704067200" +COPYRIGHT_NOTICE = "Copyright (c) 2009-present, Alibaba Cloud All rights reserved." +BUILD_TOOL_VERSIONS = { + "pip": "26.2.1", + "setuptools": "84.0.0", + "wheel": "0.48.0", +} +EXPECTED_PATCHED_FILE_SHA256 = { + "alibabacloud-kms-python-sdk": { + "setup.py": "e83f26383e0f73e263ffcb7c69765b9d4b602bc9dc489c82c73bf5abd80a3154", + }, + "alibabacloud-dkms-gcs": { + "setup.py": "037776b7b6c9b9b201aba700bdf3f2ada2c64a91b123cb1524671dbfa8f7bf31", + "openapi_util/client.py": "52535dbb10dc2943cc1b21f6392b70db99877bf46523b87cea089bd8e9034d1a", + "openapi_util/protobuf/api_pb2.py": "560232dfc5fd094258c2b6868b20a6930b0fc0f8eea6222dd098f0768ef95746", + "openapi_util/__init__.py": "54be5ed28cbb6ae730f075b0b01ea717793ba044c25403a27ce2e65b4857d55c", + "sdk/__init__.py": "54be5ed28cbb6ae730f075b0b01ea717793ba044c25403a27ce2e65b4857d55c", + }, +} +EXPECTED_WHEEL_SHA256 = { + "alibabacloud_dkms_gcs-1.0.2+yuqei.2-py3-none-any.whl": ( + "44d9832a06147f2476ec1aa3bbe7033c212e7155533475e0cb470d780789bebc" + ), + "alibabacloud_kms_python_sdk-1.1.3+yuqei.1-py3-none-any.whl": ( + "44013dbb5215fb6406b4fa26653b38c81c529e130b4086cd09a74efa625bd542" + ), +} + + +def _verify_provenance() -> None: + provenance_path = Path(__file__).with_name("UPSTREAM.json") + provenance = json.loads(provenance_path.read_text(encoding="utf-8")) + if provenance.get("schema_version") != 1: + raise RuntimeError("unsupported UPSTREAM.json schema") + if provenance.get("license", {}).get("sha256") != LICENSE_SHA256: + raise RuntimeError("UPSTREAM.json license SHA-256 does not match the builder") + attribution = provenance.get("attribution", {}) + if attribution.get("notice_file_present") is not False: + raise RuntimeError("UPSTREAM.json must record the reviewed absence of NOTICE files") + if attribution.get("copyright") != COPYRIGHT_NOTICE: + raise RuntimeError("UPSTREAM.json copyright notice does not match the builder") + + reproducible = provenance.get("reproducible_build", {}) + recorded_tools = {name: reproducible.get(name) for name in BUILD_TOOL_VERSIONS} + if recorded_tools != BUILD_TOOL_VERSIONS: + raise RuntimeError("UPSTREAM.json build tools do not match the builder") + if reproducible.get("source_date_epoch") != SOURCE_DATE_EPOCH: + raise RuntimeError("UPSTREAM.json SOURCE_DATE_EPOCH does not match the builder") + if reproducible.get("linux_x86_64_ci_required") is not True: + raise RuntimeError("UPSTREAM.json must retain the Linux CI verification boundary") + if reproducible.get("wheel_sha256") != EXPECTED_WHEEL_SHA256: + raise RuntimeError("UPSTREAM.json wheel hashes do not match the builder") + + records = { + record.get("distribution"): record + for record in provenance.get("sources", []) + } + if set(records) != {source.distribution for source in SOURCES}: + raise RuntimeError("UPSTREAM.json source set does not match the builder") + for source in SOURCES: + record = records[source.distribution] + expected_identity = { + "upstream_version": source.upstream_version, + "fork_version": source.fork_version, + "source_url": source.url, + "source_sha256": source.sha256, + } + for field, expected in expected_identity.items(): + if record.get(field) != expected: + raise RuntimeError( + f"UPSTREAM.json {source.distribution} {field} does not match the builder" + ) + recorded_files = record.get("modified_files", {}) + if set(recorded_files) != set(source.modified_files): + raise RuntimeError( + f"UPSTREAM.json {source.distribution} modified file set does not match the builder" + ) + for relative in source.modified_files: + file_record = recorded_files[relative] + if file_record.get("upstream_sha256") != source.expected_files[relative]: + raise RuntimeError( + f"UPSTREAM.json {source.distribution}/{relative} upstream hash mismatch" + ) + expected_patched = EXPECTED_PATCHED_FILE_SHA256[source.distribution][relative] + if file_record.get("patched_sha256") != expected_patched: + raise RuntimeError( + f"UPSTREAM.json {source.distribution}/{relative} patched hash mismatch" + ) + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _reviewed_license_bytes(path: Path) -> bytes: + content = path.read_bytes().replace(b"\r\n", b"\n") + if b"\r" in content or hashlib.sha256(content).hexdigest() != LICENSE_SHA256: + raise RuntimeError("bundled Apache-2.0 license SHA-256 mismatch") + return content + + +def _replace_once(source: str, old: str, new: str, label: str) -> str: + if source.count(old) != 1: + raise RuntimeError(f"unexpected upstream content for {label}") + return source.replace(old, new, 1) + + +def _download(source: UpstreamSource, destination: Path) -> Path: + destination.mkdir(parents=True, exist_ok=True) + archive_path = destination / f"{source.root}.tar.gz" + if not archive_path.exists(): + temporary_path = archive_path.with_suffix(".download") + with urllib.request.urlopen(source.url, timeout=60) as response, temporary_path.open("wb") as output: + shutil.copyfileobj(response, output) + temporary_path.replace(archive_path) + if _sha256(archive_path) != source.sha256: + raise RuntimeError(f"{source.distribution} upstream source SHA-256 mismatch") + return archive_path + + +def _extract(source: UpstreamSource, archive_path: Path, source_root: Path) -> Path: + destination = source_root / source.distribution + if destination.exists(): + raise RuntimeError(f"build source destination already exists: {destination}") + destination.mkdir(parents=True) + with tarfile.open(archive_path, "r:gz") as archive: + archive.extractall(destination, filter="data") + root = destination / source.root + if not root.is_dir(): + raise RuntimeError(f"{source.distribution} upstream root is missing") + for relative, expected in source.expected_files.items(): + if _sha256(root / relative) != expected: + raise RuntimeError(f"unexpected {source.distribution} source revision: {relative}") + return root + + +def _write_modified(path: Path, source: str) -> None: + path.write_text(source, encoding="utf-8", newline="\n") + + +def _verify_build_environment() -> None: + if sys.version_info[:2] != (3, 12): + raise RuntimeError("controlled fork wheels must be built with Python 3.12") + for distribution, expected in BUILD_TOOL_VERSIONS.items(): + actual = importlib.metadata.version(distribution) + if actual != expected: + raise RuntimeError( + f"controlled fork build requires {distribution}=={expected}, found {actual}" + ) + + +def _patch_top_level(root: Path) -> None: + setup_path = root / "setup.py" + source = setup_path.read_text(encoding="utf-8") + source = _replace_once( + source, + 'VERSION = "1.1.3"', + '# Modified by Yuqei: modern dependency closure; see bundled LICENSE and provenance.\n' + 'VERSION = "1.1.3+yuqei.1"', + "top-level fork version", + ) + source = _replace_once( + source, + " 'alibabacloud_dkms_gcs==1.0.2',", + " 'alibabacloud-dkms-gcs==1.0.2+yuqei.2',", + "controlled DKMS dependency", + ) + source = _replace_once( + source, + " 'protobuf>=3.12.0,<3.20.0'", + " 'protobuf>=6.33.6,<7.0.0'", + "modern protobuf dependency", + ) + _write_modified(setup_path, source) + + +def _extract_serialized_descriptor(source: str) -> bytes: + tree = ast.parse(source) + values = [ + ast.literal_eval(node.value) + for node in ast.walk(tree) + if isinstance(node, ast.keyword) and node.arg == "serialized_pb" + ] + if len(values) != 1 or not isinstance(values[0], bytes): + raise RuntimeError("upstream protobuf descriptor could not be extracted") + descriptor = values[0] + if hashlib.sha256(descriptor).hexdigest() != PROTO_DESCRIPTOR_SHA256: + raise RuntimeError("upstream protobuf descriptor SHA-256 mismatch") + return descriptor + + +def _modern_protobuf_source(descriptor: bytes) -> str: + return ( + "# -*- coding: utf-8 -*-\n" + "# Generated from the upstream api.proto descriptor for modern protobuf runtimes.\n" + "# Modified by Yuqei to replace deprecated direct descriptor construction.\n" + "from google.protobuf import descriptor_pool as _descriptor_pool\n" + "from google.protobuf import symbol_database as _symbol_database\n" + "from google.protobuf.internal import builder as _builder\n\n" + "_sym_db = _symbol_database.Default()\n" + f"DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile({descriptor!r})\n\n" + "_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n" + "_builder.BuildTopDescriptorsAndMessages(\n" + " DESCRIPTOR, \"openapi_util.protobuf.api_pb2\", globals()\n" + ")\n" + "# @@protoc_insertion_point(module_scope)\n" + ) + + +def _patch_dkms(root: Path) -> None: + setup_path = root / "setup.py" + setup_source = setup_path.read_text(encoding="utf-8") + setup_source = _replace_once( + setup_source, + 'VERSION = "1.0.2"', + '# Modified by Yuqei: modern crypto/protobuf closure; see bundled LICENSE and provenance.\n' + 'VERSION = "1.0.2+yuqei.2"', + "DKMS fork version", + ) + setup_source = _replace_once( + setup_source, + ' "protobuf>=3.12.0 ,<4.0.0",', + ' "protobuf>=6.33.6,<7.0.0",\n' + ' "cryptography>=50.0.2,<51.0.0",', + "DKMS modern runtime dependencies", + ) + setup_source = _replace_once( + setup_source, + ' "pyopenssl>=16.2.0,<23.3.0",\n', + "", + "obsolete pyOpenSSL dependency", + ) + _write_modified(setup_path, setup_source) + + client_path = root / "openapi_util" / "client.py" + client_source = client_path.read_text(encoding="utf-8") + client_source = _replace_once( + client_source, + "from OpenSSL import crypto\n", + "# Modified by Yuqei: pyOpenSSL removed in favor of cryptography's maintained PKCS#12 API.\n" + "from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey\n" + "from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat\n" + "from cryptography.hazmat.primitives.serialization.pkcs12 import load_key_and_certificates\n", + "PKCS#12 imports", + ) + client_source = _replace_once( + client_source, + " pk12 = crypto.load_pkcs12(private_key_data, password)\n" + " private_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, pk12.get_privatekey()).decode()\n" + " return Client.trim_private_key_pem(private_key)\n", + " password_bytes = password.encode('utf-8') if password else None\n" + " private_key, _certificate, _chain = load_key_and_certificates(private_key_data, password_bytes)\n" + " if not isinstance(private_key, RSAPrivateKey):\n" + " raise ValueError('Alibaba Cloud ClientKey must contain an RSA private key')\n" + " private_key_pem = private_key.private_bytes(\n" + " Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()\n" + " ).decode('ascii')\n" + " return Client.trim_private_key_pem(private_key_pem)\n", + "PKCS#12 implementation", + ) + client_source = _replace_once( + client_source, + ' prefix = "-----BEGIN PRIVATE KEY-----"\n' + ' newline = "\\n"\n' + ' suffix = "-----END PRIVATE KEY-----"\n' + ' private_key = private_key.replace(prefix, "")\n' + ' private_key = private_key.replace(suffix, "")\n' + ' return private_key.replace(newline, "")\n', + ' prefixes = ("-----BEGIN PRIVATE KEY-----", "-----BEGIN RSA PRIVATE KEY-----")\n' + ' suffixes = ("-----END PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----")\n' + " for marker in (*prefixes, *suffixes):\n" + ' private_key = private_key.replace(marker, "")\n' + ' return private_key.replace("\\r", "").replace("\\n", "")\n', + "private-key PEM normalization", + ) + _write_modified(client_path, client_source) + + protobuf_path = root / "openapi_util" / "protobuf" / "api_pb2.py" + descriptor = _extract_serialized_descriptor(protobuf_path.read_text(encoding="utf-8")) + _write_modified(protobuf_path, _modern_protobuf_source(descriptor)) + + for relative in ("openapi_util/__init__.py", "sdk/__init__.py"): + version_path = root / relative + version_source = _replace_once( + version_path.read_text(encoding="utf-8"), + '__version__ = "1.0.2"', + '# Modified by Yuqei; controlled fork version.\n__version__ = "1.0.2+yuqei.2"', + f"runtime version in {relative}", + ) + _write_modified(version_path, version_source) + + +def _normalize_wheel(wheel: Path) -> None: + normalized = wheel.with_suffix(".normalized") + with zipfile.ZipFile(wheel, "r") as source, zipfile.ZipFile( + normalized, + "w", + compression=zipfile.ZIP_DEFLATED, + compresslevel=9, + strict_timestamps=True, + ) as destination: + for source_info in sorted(source.infolist(), key=lambda item: item.filename): + content = source.read(source_info.filename) + target_info = zipfile.ZipInfo(source_info.filename, (2024, 1, 1, 0, 0, 0)) + target_info.compress_type = zipfile.ZIP_DEFLATED + target_info.create_system = 3 + mode = 0o40755 if source_info.is_dir() else 0o100644 + target_info.external_attr = mode << 16 + destination.writestr(target_info, content, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9) + normalized.replace(wheel) + + +def _verify_wheel_attribution(wheel: Path) -> None: + with zipfile.ZipFile(wheel) as archive: + licenses = [name for name in archive.namelist() if name.endswith(".dist-info/licenses/LICENSE")] + metadata = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")] + if len(licenses) != 1 or hashlib.sha256(archive.read(licenses[0])).hexdigest() != LICENSE_SHA256: + raise RuntimeError(f"{wheel.name} does not contain the reviewed Apache-2.0 license") + if len(metadata) != 1 or COPYRIGHT_NOTICE not in archive.read(metadata[0]).decode("utf-8"): + raise RuntimeError(f"{wheel.name} does not retain the upstream copyright notice") + + +def _build_wheel(root: Path, wheel_dir: Path) -> Path: + before = set(wheel_dir.glob("*.whl")) + environment = os.environ.copy() + environment["PYTHONHASHSEED"] = "0" + environment["SOURCE_DATE_EPOCH"] = SOURCE_DATE_EPOCH + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "wheel", + "--quiet", + "--disable-pip-version-check", + "--no-cache-dir", + "--no-build-isolation", + "--no-deps", + "--wheel-dir", + str(wheel_dir), + str(root), + ], + check=True, + env=environment, + ) + created = set(wheel_dir.glob("*.whl")) - before + if len(created) != 1: + raise RuntimeError(f"expected exactly one wheel from {root}, got {len(created)}") + wheel = created.pop() + _normalize_wheel(wheel) + _verify_wheel_attribution(wheel) + return wheel + + +def main() -> int: + parser = argparse.ArgumentParser(description="Build the controlled Alibaba KMS Python SDK forks") + parser.add_argument("--work-dir", type=Path, required=True) + parser.add_argument("--wheel-dir", type=Path, required=True) + args = parser.parse_args() + _verify_build_environment() + _verify_provenance() + + work_dir = args.work_dir.resolve() + wheel_dir = args.wheel_dir.resolve() + if work_dir.exists() and any(work_dir.iterdir()): + raise RuntimeError(f"work directory must be empty: {work_dir}") + if wheel_dir.exists() and any(wheel_dir.iterdir()): + raise RuntimeError(f"wheel directory must be empty: {wheel_dir}") + download_dir = work_dir / "downloads" + source_root = work_dir / "sources" + wheel_dir.mkdir(parents=True, exist_ok=True) + + roots: dict[str, Path] = {} + for upstream in SOURCES: + archive_path = _download(upstream, download_dir) + roots[upstream.distribution] = _extract(upstream, archive_path, source_root) + + _patch_dkms(roots["alibabacloud-dkms-gcs"]) + _patch_top_level(roots["alibabacloud-kms-python-sdk"]) + for distribution, expected_files in EXPECTED_PATCHED_FILE_SHA256.items(): + for relative, expected_hash in expected_files.items(): + if _sha256(roots[distribution] / relative) != expected_hash: + raise RuntimeError(f"unexpected patched output: {distribution}/{relative}") + + license_path = Path(__file__).with_name("LICENSE") + license_content = _reviewed_license_bytes(license_path) + for root in roots.values(): + (root / "LICENSE").write_bytes(license_content) + + wheels = [_build_wheel(roots[source.distribution], wheel_dir) for source in SOURCES] + actual_wheel_hashes = {wheel.name: _sha256(wheel) for wheel in wheels} + if actual_wheel_hashes != EXPECTED_WHEEL_SHA256: + raise RuntimeError( + "controlled fork wheel hash mismatch: " + + json.dumps(actual_wheel_hashes, sort_keys=True) + ) + manifest = { + "schema_version": 1, + "sources": [ + { + "distribution": source.distribution, + "upstream_version": source.upstream_version, + "fork_version": source.fork_version, + "source_url": source.url, + "source_sha256": source.sha256, + "patched_files": { + relative: _sha256(roots[source.distribution] / relative) + for relative in source.modified_files + }, + } + for source in SOURCES + ], + "license_sha256": LICENSE_SHA256, + "source_date_epoch": SOURCE_DATE_EPOCH, + "build_tools": BUILD_TOOL_VERSIONS, + "wheels": [ + {"filename": wheel.name, "sha256": _sha256(wheel)} + for wheel in sorted(wheels) + ], + } + print(json.dumps(manifest, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/yuqei-contract-platform/services/kms-gateway/vendor/verify_dependency_closure.py b/yuqei-contract-platform/services/kms-gateway/vendor/verify_dependency_closure.py new file mode 100644 index 00000000..d893dd45 --- /dev/null +++ b/yuqei-contract-platform/services/kms-gateway/vendor/verify_dependency_closure.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import argparse +import importlib.metadata +import json +import sys +import tomllib +from pathlib import Path + +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name + + +def _installed_distributions(path: Path) -> dict[str, importlib.metadata.Distribution]: + installed: dict[str, importlib.metadata.Distribution] = {} + for distribution in importlib.metadata.distributions(path=[str(path)]): + raw_name = distribution.metadata.get("Name") + if not raw_name: + raise RuntimeError(f"distribution without Name metadata under {path}") + name = canonicalize_name(raw_name) + if name in installed: + raise RuntimeError(f"duplicate distribution under {path}: {raw_name}") + installed[name] = distribution + return installed + + +def _active_requirement(raw: str) -> Requirement | None: + requirement = Requirement(raw) + if requirement.marker is not None and not requirement.marker.evaluate({"extra": ""}): + return None + return requirement + + +def verify(project: Path, target: Path, forbidden: list[str]) -> dict[str, object]: + project_data = tomllib.loads(project.read_text(encoding="utf-8")) + project_table = project_data["project"] + installed = _installed_distributions(target) + errors: list[str] = [] + + for raw_name in forbidden: + name = canonicalize_name(raw_name) + if name in installed: + errors.append(f"forbidden distribution is installed: {raw_name}") + + requirements: list[tuple[str, str]] = [ + (str(project_table["name"]), raw) + for raw in project_table.get("dependencies", []) + ] + for name, distribution in installed.items(): + requirements.extend( + (f"{name}=={distribution.version}", raw) + for raw in (distribution.requires or []) + ) + + checked = 0 + for owner, raw in requirements: + requirement = _active_requirement(raw) + if requirement is None: + continue + checked += 1 + dependency_name = canonicalize_name(requirement.name) + dependency = installed.get(dependency_name) + if dependency is None: + errors.append(f"{owner} requires missing {requirement}") + continue + if requirement.specifier and not requirement.specifier.contains( + dependency.version, + prereleases=True, + ): + errors.append( + f"{owner} requires {requirement}, found {dependency.metadata['Name']}=={dependency.version}" + ) + + result: dict[str, object] = { + "schema_version": 1, + "project": str(project), + "target": str(target), + "distribution_count": len(installed), + "active_requirement_count": checked, + "forbidden": sorted(canonicalize_name(name) for name in forbidden), + "errors": errors, + } + if errors: + raise RuntimeError(json.dumps(result, indent=2, sort_keys=True)) + return result + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Verify a KMS target directory without consulting global site-packages" + ) + parser.add_argument("--project", type=Path, required=True) + parser.add_argument("--path", type=Path, required=True) + parser.add_argument("--forbid", action="append", default=[]) + args = parser.parse_args() + print( + json.dumps( + verify(args.project.resolve(), args.path.resolve(), args.forbid), + indent=2, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except RuntimeError as exc: + print(exc, file=sys.stderr) + raise SystemExit(1) from exc diff --git a/yuqei-infra/docker/kms-gateway.Dockerfile b/yuqei-infra/docker/kms-gateway.Dockerfile index 311dbb99..13255a63 100644 --- a/yuqei-infra/docker/kms-gateway.Dockerfile +++ b/yuqei-infra/docker/kms-gateway.Dockerfile @@ -1,14 +1,57 @@ -FROM python:3.12-slim +ARG PYTHON_BASE_IMAGE=python:3.12-slim@sha256:44ff437bba879d4941b710a369a8f19266aea34b29002807f0c487fabc9eec9b + +FROM ${PYTHON_BASE_IMAGE} AS builder + +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + YUQEI_MIN_GLIBC=2.28 + +ARG PIP_INDEX_URL=https://pypi.org/simple +WORKDIR /build + +COPY yuqei-contract-platform/services/kms-gateway/vendor/alibaba-kms-modern /build/alibaba-kms-modern +COPY yuqei-contract-platform/services/kms-gateway/vendor/verify_dependency_closure.py /build/verify_dependency_closure.py +COPY yuqei-contract-platform/services/kms-gateway/pyproject.toml /build/kms-gateway-pyproject.toml +COPY yuqei-infra/requirements/kms-gateway-build-tools-py312.txt /build/build-tools.lock +COPY yuqei-infra/requirements/kms-gateway-runtime-linux-x86_64-py312.txt /build/runtime.lock + +RUN python -c "import os, platform; name, version = platform.libc_ver(); minimum = tuple(map(int, os.environ['YUQEI_MIN_GLIBC'].split('.'))); actual = tuple(map(int, version.split('.')[:2])); assert name == 'glibc' and actual >= minimum, f'KMS gateway requires glibc >= {minimum[0]}.{minimum[1]}, found {name} {version}'" +RUN python -m pip install --no-cache-dir --only-binary=:all: --require-hashes \ + --index-url "$PIP_INDEX_URL" \ + --requirement /build/build-tools.lock +RUN python /build/alibaba-kms-modern/build_forks.py \ + --work-dir /tmp/alibaba-kms-build \ + --wheel-dir /build/alibaba-kms-wheels +RUN python -m pip download --no-cache-dir --no-build-isolation --no-deps --require-hashes \ + --index-url "$PIP_INDEX_URL" \ + --dest /build/runtime-inputs \ + --requirement /build/runtime.lock +RUN python -m pip install --no-cache-dir --no-build-isolation --require-hashes \ + --no-index --find-links /build/runtime-inputs \ + --target /install \ + --requirement /build/runtime.lock +RUN python -m pip install --no-cache-dir --no-deps --target /install \ + /build/alibaba-kms-wheels/*.whl +RUN python /build/verify_dependency_closure.py \ + --project /build/kms-gateway-pyproject.toml \ + --path /install \ + --forbid pyOpenSSL \ + && PYTHONPATH=/install python -m pip check \ + && PYTHONPATH=/install python -c "import importlib.metadata as m, importlib.util; from alibabacloud_kms_kms20160120.client import Client as AliClient; from openapi_util.protobuf import api_pb2; from tencentcloud.kms.v20190118.kms_client import KmsClient as TencentClient; assert AliClient and TencentClient; assert m.version('alibabacloud-kms-python-sdk') == '1.1.3+yuqei.1'; assert m.version('alibabacloud-dkms-gcs') == '1.0.2+yuqei.2'; assert m.version('protobuf') == '6.33.6'; assert m.version('cryptography') == '50.0.2'; assert importlib.util.find_spec('OpenSSL') is None; request = api_pb2.EncryptRequest(KeyId='docker-build', Plaintext=b'payload'); assert api_pb2.EncryptRequest.FromString(request.SerializeToString()) == request" + +FROM ${PYTHON_BASE_IMAGE} ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ - PYTHONPATH=/app/yuqei-contract-platform/services/kms-gateway/src + PYTHONPATH=/opt/kms-python:/app/yuqei-contract-platform/services/kms-gateway/src WORKDIR /app -ARG PIP_INDEX_URL=https://pypi.org/simple -COPY yuqei-contract-platform/services/kms-gateway /app/yuqei-contract-platform/services/kms-gateway -RUN pip install --no-cache-dir --index-url "$PIP_INDEX_URL" /app/yuqei-contract-platform/services/kms-gateway \ +COPY --from=builder /install /opt/kms-python +COPY yuqei-contract-platform/services/kms-gateway/src /app/yuqei-contract-platform/services/kms-gateway/src + +RUN python -c "from alibabacloud_kms_kms20160120.client import Client as AliClient; from tencentcloud.kms.v20190118.kms_client import KmsClient as TencentClient; from openapi_util.protobuf import api_pb2; assert AliClient and TencentClient and api_pb2.EncryptRequest" \ && addgroup --system --gid 10001 yuqei \ && adduser --system --uid 10001 --ingroup yuqei yuqei diff --git a/yuqei-infra/requirements/kms-gateway-audit-tools-linux-x86_64-py312.txt b/yuqei-infra/requirements/kms-gateway-audit-tools-linux-x86_64-py312.txt new file mode 100644 index 00000000..cc362e6b --- /dev/null +++ b/yuqei-infra/requirements/kms-gateway-audit-tools-linux-x86_64-py312.txt @@ -0,0 +1,32 @@ +# Hash-locked pip-audit 2.10.1 closure for CPython 3.12 Linux x86_64. +# Selected from https://pypi.org/simple; binary wheels only. + +boolean.py==5.0 --hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9 +CacheControl==0.14.4 --hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b +certifi==2026.7.22 --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 +charset-normalizer==3.5.2 --hash=sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649 +cyclonedx-python-lib==11.12.0 --hash=sha256:0e807521a921a5c3cb8ce1153f8a61d29eedfe76a46aac2796b7c6b573391a54 +defusedxml==0.7.1 --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 +filelock==4.0.12 --hash=sha256:5f17ee83ecee8a6f3e389c75822fb70a1c2f0506b99438a6dffa1d56793588c8 +idna==3.20 --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c +license-expression==30.4.4 --hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 +markdown-it-py==4.2.0 --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a +mdurl==0.1.2 --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 +msgpack==1.2.3 --hash=sha256:ede33b2892ceb976283e009ad12fa1834cfdf1f9c43ee9c97849fc588d00a618 +packageurl-python==0.17.6 --hash=sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9 +packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +pip==26.2.1 --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e +pip-api==0.0.35 --hash=sha256:66990d0da25378555cbb538db79384bab92d4617c6350dfd7a2126ba2a3a9894 +pip-audit==2.10.1 --hash=sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a +pip-requirements-parser==32.0.1 --hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 +platformdirs==4.12.3 --hash=sha256:080f3b39423b5abfca9a23d84c4e9795f54d395cd8459867a8ded44084fcd5f8 +py-serializable==2.1.0 --hash=sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304 +Pygments==2.21.0 --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 +pyparsing==3.3.3 --hash=sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4 +requests==2.34.2 --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 +rich==15.0.0 --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb +sortedcontainers==2.4.0 --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 +tomli==2.4.1 --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 +tomli-w==1.2.0 --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 +typing-extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 +urllib3==2.8.0 --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 diff --git a/yuqei-infra/requirements/kms-gateway-build-tools-py312.txt b/yuqei-infra/requirements/kms-gateway-build-tools-py312.txt new file mode 100644 index 00000000..e769498c --- /dev/null +++ b/yuqei-infra/requirements/kms-gateway-build-tools-py312.txt @@ -0,0 +1,13 @@ +# Generated from pip's official resolver report; do not edit by hand. +# Target: CPython 3.12 controlled fork builder; universal wheels only. +# Resolver: pip 26.2.1 against https://pypi.org/simple. +# Resolver report sha256: 295f4d52f6dc64e9cf8ae1ba3bcfef12fc2fbe1e6a6ad2ccb23252241598e609 + +# Artifact: packaging-26.3-py3-none-any.whl +packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +# Artifact: pip-26.2.1-py3-none-any.whl +pip==26.2.1 --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e +# Artifact: setuptools-84.0.0-py3-none-any.whl +setuptools==84.0.0 --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 +# Artifact: wheel-0.48.0-py3-none-any.whl +wheel==0.48.0 --hash=sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab diff --git a/yuqei-infra/requirements/kms-gateway-runtime-linux-x86_64-py312.txt b/yuqei-infra/requirements/kms-gateway-runtime-linux-x86_64-py312.txt new file mode 100644 index 00000000..3da9be5e --- /dev/null +++ b/yuqei-infra/requirements/kms-gateway-runtime-linux-x86_64-py312.txt @@ -0,0 +1,131 @@ +# Generated from pip's official resolver report; do not edit by hand. +# Target: CPython 3.12 / Linux x86_64 / glibc >= 2.28. +# Wheel policy: prefer manylinux_2_28_x86_64, allow manylinux2014_x86_64 fallback. +# Resolver: pip 26.2.1 against https://pypi.org/simple. +# Inputs: KMS gateway runtime plus controlled Alibaba fork metadata. +# Resolver report sha256: 2a4db22588a8715224131bf7fdaaceb88f2fd7f7dcd928c7a65a0b01d9f40af7 +# The controlled forks are built separately and fail closed unless their wheel hashes are: +# alibabacloud_dkms_gcs-1.0.2+yuqei.2-py3-none-any.whl +# sha256:44d9832a06147f2476ec1aa3bbe7033c212e7155533475e0cb470d780789bebc +# alibabacloud_kms_python_sdk-1.1.3+yuqei.1-py3-none-any.whl +# sha256:44013dbb5215fb6406b4fa26653b38c81c529e130b4086cd09a74efa625bd542 +# Nine legacy Alibaba transitive packages publish source archives only; the reviewed +# source hashes below are built without isolation by the pinned builder toolchain. +# A lock for another Python, OS, architecture, or index must be regenerated and reviewed. + +# Artifact: aiofiles-24.1.0-py3-none-any.whl +aiofiles==24.1.0 --hash=sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5 +# Artifact: aiohappyeyeballs-2.7.1-py3-none-any.whl +aiohappyeyeballs==2.7.1 --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 +# Artifact: aiohttp-3.14.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +aiohttp==3.14.4 --hash=sha256:c5e93ab6e6330dce40f5b43996f1fc5f2543296d263a5e4b59ff09e73870b7d8 +# Artifact: aiosignal-1.4.0-py3-none-any.whl +aiosignal==1.4.0 --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e +# Artifact: alibabacloud_darabonba_array-0.1.0.tar.gz +alibabacloud_darabonba_array==0.1.0 --hash=sha256:7f9a7c632518ff4f0cebb0d4e825a48c12e7cf0b9016ea25054dd73732e155aa +# Artifact: alibabacloud_darabonba_map-0.0.1.tar.gz +alibabacloud_darabonba_map==0.0.1 --hash=sha256:adb17384658a1a8f72418f1838d4b6a5fd2566bfd392a3ef06d9dbb0a595a23f +# Artifact: alibabacloud_darabonba_number-0.0.4.tar.gz +alibabacloud_darabonba_number==0.0.4 --hash=sha256:c017fd5fb20bffb91a12a330be050524ae99add164662cb5466e85981faf3f34 +# Artifact: alibabacloud_darabonba_signature_util-0.0.4.tar.gz +alibabacloud_darabonba_signature_util==0.0.4 --hash=sha256:71d79b2ae65957bcfbf699ced894fda782b32f9635f1616635533e5a90d5feb0 +# Artifact: alibabacloud_darabonba_stream-0.0.2.tar.gz +alibabacloud_darabonba_stream==0.0.2 --hash=sha256:df807b4547a5519da5191d99e8c15cae2912aa68d577f8179de6fe825d5c5ab7 +# Artifact: alibabacloud_endpoint_util-0.0.4.tar.gz +alibabacloud_endpoint_util==0.0.4 --hash=sha256:a593eb8ddd8168d5dc2216cd33111b144f9189fcd6e9ca20e48f358a739bbf90 +# Artifact: alibabacloud_tea_xml-0.0.3.tar.gz +alibabacloud_tea_xml==0.0.3 --hash=sha256:979cb51fadf43de77f41c69fc69c12529728919f849723eb0cd24eb7b048a90c +# Artifact: alibabacloud_credentials-1.0.12-py3-none-any.whl +alibabacloud-credentials==1.0.12 --hash=sha256:c09afcf579663374df056c20482fc1397240d20b4ccb143d23c287f3f8a91109 +# Artifact: alibabacloud_credentials_api-1.0.1-py3-none-any.whl +alibabacloud-credentials-api==1.0.1 --hash=sha256:5f27889113214fc53493b3e918eb26d73dfab2022e22bdaac262849b8e58cbc0 +# Artifact: alibabacloud_darabonba_encode_util-0.0.3-py3-none-any.whl +alibabacloud-darabonba-encode-util==0.0.3 --hash=sha256:6a91c82f6cc4227091be39b65d1f6921d7330119bac7b911e5a26daf895cc9dc +# Artifact: alibabacloud-darabonba-string-0.0.4.tar.gz +alibabacloud-darabonba-string==0.0.4 --hash=sha256:ec6614c0448dadcbc5e466485838a1f8cfdd911135bea739e20b14511270c6f7 +# Artifact: alibabacloud_gateway_pop-0.1.3-py3-none-any.whl +alibabacloud-gateway-pop==0.1.3 --hash=sha256:6314f92885d5ddc449716952beabbe9704e3469655a270703397955283676769 +# Artifact: alibabacloud_gateway_spi-0.0.4-py3-none-any.whl +alibabacloud-gateway-spi==0.0.4 --hash=sha256:0d5256e95d8719da8ec9611b7ffbb12c2d26fdf7ce52c8f64e4e06350731e893 +# Artifact: alibabacloud_kms20160120-2.4.0-py3-none-any.whl +alibabacloud-kms20160120==2.4.0 --hash=sha256:dc1c214f8bed8f25dfbeb5443d1a1fb7fc0362d2a0f7ea4ec103bcd7d8c8b47e +# Artifact: alibabacloud_openapi_util-0.2.4-py3-none-any.whl +alibabacloud-openapi-util==0.2.4 --hash=sha256:a2474f230b5965ae9a8c286e0dc86132a887928d02d20b8182656cf6b1b6c5bd +# Artifact: alibabacloud-tea-0.4.3.tar.gz +alibabacloud-tea==0.4.3 --hash=sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a +# Artifact: alibabacloud_tea_openapi-0.4.6-py3-none-any.whl +alibabacloud-tea-openapi==0.4.6 --hash=sha256:c9e1727b9fb2936f487d050fc3590c99f9f2065256dc3a927e5b61f414674ed6 +# Artifact: alibabacloud_tea_util-0.3.15-py3-none-any.whl +alibabacloud-tea-util==0.3.15 --hash=sha256:afb3dd8ad5cd2f93804258fbbac4360c050462100499269795ea055c1644e193 +# Artifact: annotated_doc-0.0.5-py3-none-any.whl +annotated-doc==0.0.5 --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 +# Artifact: annotated_types-0.8.0-py3-none-any.whl +annotated-types==0.8.0 --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 +# Artifact: anyio-4.15.1-py3-none-any.whl +anyio==4.15.1 --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 +# Artifact: apscheduler-3.11.3-py3-none-any.whl +APScheduler==3.11.3 --hash=sha256:bbeb2ec02d23d3c06a6c07ed7f0f3939ada6680eb121fae809a69bb42c537a30 +# Artifact: attrs-26.1.0-py3-none-any.whl +attrs==26.1.0 --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 +# Artifact: certifi-2026.7.22-py3-none-any.whl +certifi==2026.7.22 --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 +# Artifact: cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl +cffi==2.1.1 --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf +# Artifact: charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +charset-normalizer==3.5.2 --hash=sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649 +# Artifact: click-8.5.0-py3-none-any.whl +click==8.5.0 --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 +# Artifact: cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl +cryptography==50.0.2 --hash=sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856 +# Artifact: darabonba_core-1.0.9-py3-none-any.whl +darabonba-core==1.0.9 --hash=sha256:dafe9bc93639ddd9954322df72382b27f51078dbb41c045274a70e59dce57d80 +# Artifact: fastapi-0.142.2-py3-none-any.whl +fastapi==0.142.2 --hash=sha256:bd5f4d81f1e93a88bcd77caf4dfe3c2dbffc3805407a0007e9a114c18b3a670b +# Artifact: frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl +frozenlist==1.8.0 --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 +# Artifact: h11-0.16.0-py3-none-any.whl +h11==0.16.0 --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 +# Artifact: idna-3.20-py3-none-any.whl +idna==3.20 --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c +# Artifact: multidict-6.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +multidict==6.9.1 --hash=sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d +# Artifact: opentelemetry_api-1.45.0-py3-none-any.whl +opentelemetry-api==1.45.0 --hash=sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3 +# Artifact: propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +propcache==0.5.4 --hash=sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b +# Artifact: protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl +protobuf==6.33.6 --hash=sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593 +# Artifact: pycparser-3.0-py3-none-any.whl +pycparser==3.0 --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 +# Artifact: pydantic-2.13.5-py3-none-any.whl +pydantic==2.13.5 --hash=sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73 +# Artifact: pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +pydantic-core==2.46.5 --hash=sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2 +# Artifact: pydantic_settings-2.15.0-py3-none-any.whl +pydantic-settings==2.15.0 --hash=sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42 +# Artifact: python_dotenv-1.2.4-py3-none-any.whl +python-dotenv==1.2.4 --hash=sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc +# Artifact: requests-2.34.2-py3-none-any.whl +requests==2.34.2 --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 +# Artifact: starlette-1.7.0-py3-none-any.whl +starlette==1.7.0 --hash=sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e +# Artifact: tencentcloud_sdk_python_common-3.1.186-py2.py3-none-any.whl +tencentcloud-sdk-python-common==3.1.186 --hash=sha256:b657b34bcb5856ec68b09ec31ebbef2a0f7cce27af9ea9b265669e993179d25d +# Artifact: tencentcloud_sdk_python_kms-3.1.164-py2.py3-none-any.whl +tencentcloud-sdk-python-kms==3.1.164 --hash=sha256:e7543b49c970654275e99b1430f17c6d7b087a14eb2a814982be37c4f2b0fe2d +# Artifact: typing_extensions-4.16.0-py3-none-any.whl +typing-extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 +# Artifact: typing_inspection-0.4.4-py3-none-any.whl +typing-inspection==0.4.4 --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147 +# Artifact: tzdata-2026.5-py2.py3-none-any.whl +tzdata==2026.5 --hash=sha256:b683bd1b6659ddcd810ff02ad09ba821d4bf1065072805063eb35c49617905ac +# Artifact: tzlocal-5.4.4-py3-none-any.whl +tzlocal==5.4.4 --hash=sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15 +# Artifact: urllib3-2.8.0-py3-none-any.whl +urllib3==2.8.0 --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 +# Artifact: uvicorn-0.54.0-py3-none-any.whl +uvicorn==0.54.0 --hash=sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf +# Artifact: websocket_client-1.9.2-py3-none-any.whl +websocket-client==1.9.2 --hash=sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce +# Artifact: yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +yarl==1.25.1 --hash=sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d diff --git a/yuqei-infra/requirements/kms-gateway-test-py312.txt b/yuqei-infra/requirements/kms-gateway-test-py312.txt new file mode 100644 index 00000000..2fb22f14 --- /dev/null +++ b/yuqei-infra/requirements/kms-gateway-test-py312.txt @@ -0,0 +1,32 @@ +# Generated from pip's official resolver report; do not edit by hand. +# Target: CPython 3.12 test dependencies; all selected artifacts are universal wheels. +# Resolver: pip 26.2.1 against https://pypi.org/simple. +# Inputs: pytest==9.1.1 and httpx==0.28.1. +# Resolver report sha256: a889839623322b82b13df85c38a85c95eddbd9a11a135e14a2b8b2ab200d8c45 + +# Artifact: anyio-4.15.1-py3-none-any.whl +anyio==4.15.1 --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 +# Artifact: certifi-2026.7.22-py3-none-any.whl +certifi==2026.7.22 --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 +# Artifact: colorama-0.4.6-py2.py3-none-any.whl +colorama==0.4.6 --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 +# Artifact: h11-0.16.0-py3-none-any.whl +h11==0.16.0 --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 +# Artifact: httpcore-1.0.9-py3-none-any.whl +httpcore==1.0.9 --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 +# Artifact: httpx-0.28.1-py3-none-any.whl +httpx==0.28.1 --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad +# Artifact: idna-3.20-py3-none-any.whl +idna==3.20 --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c +# Artifact: iniconfig-2.3.0-py3-none-any.whl +iniconfig==2.3.0 --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 +# Artifact: packaging-26.3-py3-none-any.whl +packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +# Artifact: pluggy-1.6.0-py3-none-any.whl +pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 +# Artifact: pygments-2.21.0-py3-none-any.whl +Pygments==2.21.0 --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 +# Artifact: pytest-9.1.1-py3-none-any.whl +pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c +# Artifact: typing_extensions-4.16.0-py3-none-any.whl +typing-extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8