fix(kms): modernize audited Alibaba SDK closure
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
+201
@@ -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.
|
||||
+50
@@ -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.
|
||||
+89
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+517
@@ -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())
|
||||
+111
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user