tigrcorn-certification 0.3.18.dev1__tar.gz → 0.3.18.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tigrcorn_certification-0.3.18.dev1/src/tigrcorn_certification.egg-info → tigrcorn_certification-0.3.18.dev3}/PKG-INFO +5 -4
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/README.md +1 -1
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/pyproject.toml +4 -3
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/__init__.py +26 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/certification_env.py +1 -1
- tigrcorn_certification-0.3.18.dev3/src/tigrcorn_certification/certify/__init__.py +5 -0
- tigrcorn_certification-0.3.18.dev3/src/tigrcorn_certification/certify/static.py +118 -0
- tigrcorn_certification-0.3.18.dev3/src/tigrcorn_certification/hardening_suites.py +103 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/__init__.py +1 -0
- tigrcorn_certification-0.3.18.dev3/src/tigrcorn_certification/interop_runner/impairment.py +55 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/proxies.py +20 -9
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3/src/tigrcorn_certification.egg-info}/PKG-INFO +5 -4
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification.egg-info/SOURCES.txt +4 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification.egg-info/requires.txt +3 -2
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/LICENSE +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/setup.cfg +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/aioquic_preflight/__init__.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/aioquic_preflight/bundle.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/aioquic_preflight/core.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/aioquic_preflight/helpers.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/aioquic_preflight/imports.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/aioquic_preflight/records.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/aioquic_preflight/status_docs.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/artifacts.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/conformance.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/explicit_surfaces.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/adapters.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/assertions.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/core.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/environment.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/helpers.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/imports.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/matrix.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/models.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/ports.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/process.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/qlog.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/runner.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/interop_runner/scenario.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/perf_runner/__init__.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/perf_runner/artifacts.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/perf_runner/environment.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/perf_runner/imports.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/perf_runner/matrix.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/perf_runner/metrics.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/perf_runner/models.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/perf_runner/runner.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/perf_runner/stats.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/perf_runner/validation.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/py.typed +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/__init__.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/artifact_gate.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/contract_registry.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/core.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/docs.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/imports.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/independent.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/loaders.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/models.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/promotion.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/promotion_sections.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/release_gates/supply_chain_gate.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification/supply_chain.py +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification.egg-info/dependency_links.txt +0 -0
- {tigrcorn_certification-0.3.18.dev1 → tigrcorn_certification-0.3.18.dev3}/src/tigrcorn_certification.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrcorn-certification
|
|
3
|
-
Version: 0.3.18.
|
|
3
|
+
Version: 0.3.18.dev3
|
|
4
4
|
Summary: Certification and release-gate tooling for Tigrcorn HTTP/3, QUIC, WebSocket, WebTransport, and ASGI conformance evidence.
|
|
5
5
|
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
6
6
|
License: Apache License
|
|
@@ -187,8 +187,9 @@ Classifier: Typing :: Typed
|
|
|
187
187
|
Requires-Python: <3.15,>=3.10
|
|
188
188
|
Description-Content-Type: text/markdown
|
|
189
189
|
License-File: LICENSE
|
|
190
|
-
Requires-Dist: tigrcorn-compat==0.3.18.
|
|
191
|
-
Requires-Dist: tigrcorn-runtime==0.3.18.
|
|
190
|
+
Requires-Dist: tigrcorn-compat==0.3.18.dev3
|
|
191
|
+
Requires-Dist: tigrcorn-runtime==0.3.18.dev3
|
|
192
|
+
Requires-Dist: tigrcorn-static==0.3.18.dev3
|
|
192
193
|
Requires-Dist: tomli>=2.0.0; python_version < "3.11"
|
|
193
194
|
Requires-Dist: cryptography>=46.0.0
|
|
194
195
|
Requires-Dist: aioquic>=1.3.0
|
|
@@ -286,7 +287,7 @@ The package exposes its supported public surface through the <code>tigrcorn_cert
|
|
|
286
287
|
|
|
287
288
|
## Package Graph
|
|
288
289
|
|
|
289
|
-
[tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
|
|
290
|
+
[tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
|
|
290
291
|
|
|
291
292
|
## Best Practices
|
|
292
293
|
|
|
@@ -87,7 +87,7 @@ The package exposes its supported public surface through the <code>tigrcorn_cert
|
|
|
87
87
|
|
|
88
88
|
## Package Graph
|
|
89
89
|
|
|
90
|
-
[tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
|
|
90
|
+
[tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
|
|
91
91
|
|
|
92
92
|
## Best Practices
|
|
93
93
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tigrcorn-certification"
|
|
7
|
-
version = "0.3.18.
|
|
7
|
+
version = "0.3.18.dev3"
|
|
8
8
|
description = "Certification and release-gate tooling for Tigrcorn HTTP/3, QUIC, WebSocket, WebTransport, and ASGI conformance evidence."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10,<3.15"
|
|
@@ -30,8 +30,9 @@ classifiers = [
|
|
|
30
30
|
"Typing :: Typed",
|
|
31
31
|
]
|
|
32
32
|
dependencies = [
|
|
33
|
-
"tigrcorn-compat==0.3.18.
|
|
34
|
-
"tigrcorn-runtime==0.3.18.
|
|
33
|
+
"tigrcorn-compat==0.3.18.dev3",
|
|
34
|
+
"tigrcorn-runtime==0.3.18.dev3",
|
|
35
|
+
"tigrcorn-static==0.3.18.dev3",
|
|
35
36
|
"tomli>=2.0.0; python_version < '3.11'",
|
|
36
37
|
"cryptography>=46.0.0",
|
|
37
38
|
"aioquic>=1.3.0",
|
|
@@ -44,10 +44,36 @@ __all__ = [
|
|
|
44
44
|
"validate_package_manifest_integrity",
|
|
45
45
|
"validate_release_evidence",
|
|
46
46
|
"write_supply_chain_release_bundle",
|
|
47
|
+
"certify_static",
|
|
48
|
+
"certify_static_main",
|
|
49
|
+
"export_hardening_suite_catalog",
|
|
50
|
+
"validate_hardening_suite_catalog",
|
|
47
51
|
]
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
def __getattr__(name: str):
|
|
55
|
+
if name in {
|
|
56
|
+
"certify_static",
|
|
57
|
+
"certify_static_main",
|
|
58
|
+
}:
|
|
59
|
+
from .certify.static import certify_static, certify_static_main
|
|
60
|
+
|
|
61
|
+
mapping = {
|
|
62
|
+
"certify_static": certify_static,
|
|
63
|
+
"certify_static_main": certify_static_main,
|
|
64
|
+
}
|
|
65
|
+
return mapping[name]
|
|
66
|
+
if name in {
|
|
67
|
+
"export_hardening_suite_catalog",
|
|
68
|
+
"validate_hardening_suite_catalog",
|
|
69
|
+
}:
|
|
70
|
+
from .hardening_suites import export_hardening_suite_catalog, validate_hardening_suite_catalog
|
|
71
|
+
|
|
72
|
+
mapping = {
|
|
73
|
+
"export_hardening_suite_catalog": export_hardening_suite_catalog,
|
|
74
|
+
"validate_hardening_suite_catalog": validate_hardening_suite_catalog,
|
|
75
|
+
}
|
|
76
|
+
return mapping[name]
|
|
51
77
|
if name in {
|
|
52
78
|
"evaluate_release_gates",
|
|
53
79
|
"assert_release_ready",
|
|
@@ -13,7 +13,7 @@ try:
|
|
|
13
13
|
import tomllib
|
|
14
14
|
except ModuleNotFoundError: # pragma: no cover - Python 3.10 fallback
|
|
15
15
|
import tomli as tomllib
|
|
16
|
-
SUPPORTED_PYTHON_VERSIONS: tuple[str, ...] = ('3.11', '3.12')
|
|
16
|
+
SUPPORTED_PYTHON_VERSIONS: tuple[str, ...] = ('3.10', '3.11', '3.12', '3.13', '3.14')
|
|
17
17
|
REQUIRED_IMPORTS: tuple[str, ...] = ('aioquic', 'h2', 'websockets', 'wsproto')
|
|
18
18
|
REQUIRED_EXTRAS: tuple[str, ...] = ('certification', 'dev')
|
|
19
19
|
SAFE_ENV_KEYS: tuple[str, ...] = (
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Callable, Iterable
|
|
8
|
+
|
|
9
|
+
from tigrcorn_static.static import (
|
|
10
|
+
StaticSecurityCertificationError,
|
|
11
|
+
build_static_certification_evidence,
|
|
12
|
+
certify_static_delivery_security,
|
|
13
|
+
static_delivery_certification_artifact,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ..artifacts import canonical_json
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StaticCertificationCommandError(ValueError):
|
|
20
|
+
"""Raised when static certification cannot be completed."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_static_certification_parser() -> argparse.ArgumentParser:
|
|
24
|
+
parser = argparse.ArgumentParser(prog="tigrcorn certify static")
|
|
25
|
+
parser.add_argument("--mount", required=True, help="Static mount directory to certify")
|
|
26
|
+
parser.add_argument("--profile", default="default", help="Blessed deployment profile")
|
|
27
|
+
parser.add_argument("--output", required=True, help="Directory where certification artifacts are written")
|
|
28
|
+
return parser
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def certify_static(
|
|
32
|
+
*,
|
|
33
|
+
mount: str | Path,
|
|
34
|
+
profile: str = "default",
|
|
35
|
+
output: str | Path,
|
|
36
|
+
require_supported: Callable[[Iterable[str]], None] | None = None,
|
|
37
|
+
) -> dict[str, object]:
|
|
38
|
+
mount_path = Path(mount)
|
|
39
|
+
if not mount_path.exists() or not mount_path.is_dir():
|
|
40
|
+
raise StaticCertificationCommandError(f"static mount does not exist or is not a directory: {mount_path}")
|
|
41
|
+
|
|
42
|
+
if require_supported is not None:
|
|
43
|
+
require_supported(["delivery.static"])
|
|
44
|
+
|
|
45
|
+
evidence = build_static_certification_evidence(mount_path, profile=profile)
|
|
46
|
+
certification = certify_static_delivery_security(evidence)
|
|
47
|
+
static_artifact = {
|
|
48
|
+
"artifact": static_delivery_certification_artifact(),
|
|
49
|
+
"certification": certification,
|
|
50
|
+
"evidence": evidence,
|
|
51
|
+
"mount_name": mount_path.name,
|
|
52
|
+
"profile": profile,
|
|
53
|
+
"schema_version": 1,
|
|
54
|
+
}
|
|
55
|
+
manifest = {
|
|
56
|
+
"artifacts": [
|
|
57
|
+
{
|
|
58
|
+
"digest": _digest(static_artifact),
|
|
59
|
+
"name": "static.json",
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"certification": "static",
|
|
63
|
+
"digest_algorithm": "sha256",
|
|
64
|
+
"manifest_version": 1,
|
|
65
|
+
"profile": profile,
|
|
66
|
+
}
|
|
67
|
+
output_path = Path(output)
|
|
68
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
static_path = output_path / "static.json"
|
|
70
|
+
manifest_path = output_path / "manifest.json"
|
|
71
|
+
static_path.write_text(canonical_json(static_artifact), encoding="utf-8", newline="\n")
|
|
72
|
+
manifest_path.write_text(canonical_json(manifest), encoding="utf-8", newline="\n")
|
|
73
|
+
return {
|
|
74
|
+
"artifact_paths": [str(static_path), str(manifest_path)],
|
|
75
|
+
"mount": str(mount_path),
|
|
76
|
+
"output": str(output_path),
|
|
77
|
+
"passed": True,
|
|
78
|
+
"profile": profile,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def certify_static_main(
|
|
83
|
+
argv: list[str] | None = None,
|
|
84
|
+
*,
|
|
85
|
+
require_supported: Callable[[Iterable[str]], None] | None = None,
|
|
86
|
+
) -> int:
|
|
87
|
+
parser = build_static_certification_parser()
|
|
88
|
+
ns = parser.parse_args(list(argv or ()))
|
|
89
|
+
|
|
90
|
+
capability_gate = None
|
|
91
|
+
if require_supported is not None:
|
|
92
|
+
capability_gate = lambda ids: require_supported(ids, profile=ns.profile) # noqa: E731
|
|
93
|
+
try:
|
|
94
|
+
result = certify_static(
|
|
95
|
+
mount=ns.mount,
|
|
96
|
+
profile=ns.profile,
|
|
97
|
+
output=ns.output,
|
|
98
|
+
require_supported=capability_gate,
|
|
99
|
+
)
|
|
100
|
+
except (StaticCertificationCommandError, StaticSecurityCertificationError, ValueError) as exc:
|
|
101
|
+
print(str(exc), file=sys.stderr)
|
|
102
|
+
return 2
|
|
103
|
+
print(json.dumps(result, sort_keys=True, separators=(",", ":")))
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _digest(payload: dict[str, object]) -> str:
|
|
108
|
+
import hashlib
|
|
109
|
+
|
|
110
|
+
return hashlib.sha256(canonical_json(payload).encode("utf-8")).hexdigest()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = [
|
|
114
|
+
"StaticCertificationCommandError",
|
|
115
|
+
"build_static_certification_parser",
|
|
116
|
+
"certify_static",
|
|
117
|
+
"certify_static_main",
|
|
118
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
HARDENING_SUITE_CATALOG: dict[str, dict[str, Any]] = {
|
|
8
|
+
"http1": {
|
|
9
|
+
"owner_package": "tigrcorn-protocols",
|
|
10
|
+
"package_tests": ["tests/test_http1_hardening_pass.py"],
|
|
11
|
+
"cross_cutting_tests": ["tests/test_adversarial_protocol_hardening_suites.py"],
|
|
12
|
+
"feature_ids": ["feat:adversarial-protocol-hardening-suites"],
|
|
13
|
+
"spec_ids": ["spc:2080"],
|
|
14
|
+
"cases": ["slowloris", "malformed_chunking", "request_smuggling", "oversized_headers", "premature_eof"],
|
|
15
|
+
},
|
|
16
|
+
"http2": {
|
|
17
|
+
"owner_package": "tigrcorn-protocols",
|
|
18
|
+
"package_tests": ["tests/test_http2_state_machine_completion.py"],
|
|
19
|
+
"cross_cutting_tests": ["tests/test_adversarial_protocol_hardening_suites.py"],
|
|
20
|
+
"feature_ids": ["feat:adversarial-protocol-hardening-suites"],
|
|
21
|
+
"spec_ids": ["spc:2080"],
|
|
22
|
+
"cases": ["rapid_reset", "continuation_flood", "stream_exhaustion", "settings_churn", "flow_control_deadlock"],
|
|
23
|
+
},
|
|
24
|
+
"http3_quic": {
|
|
25
|
+
"owner_package": "tigrcorn-transports",
|
|
26
|
+
"package_tests": ["tests/test_quic_operational_security_certification.py", "tests/test_quic_impairment_policy.py"],
|
|
27
|
+
"cross_cutting_tests": ["tests/test_quic_operational_security_integration.py", "tests/test_quic_recovery_live_runtime_integration.py"],
|
|
28
|
+
"feature_ids": ["feat:quic-operational-security-certification", "feat:adversarial-protocol-hardening-suites"],
|
|
29
|
+
"spec_ids": ["spc:2060", "spc:2080"],
|
|
30
|
+
"cases": ["packet_loss", "packet_reordering", "packet_duplication", "controlled_delay", "reliable_stream_recovery", "datagram_abandonment", "amplification_limit", "connection_migration", "cid_rotation"],
|
|
31
|
+
},
|
|
32
|
+
"websocket": {
|
|
33
|
+
"owner_package": "tigrcorn-protocols",
|
|
34
|
+
"package_tests": ["tests/test_server_websocket.py"],
|
|
35
|
+
"cross_cutting_tests": ["tests/test_adversarial_protocol_hardening_suites.py"],
|
|
36
|
+
"feature_ids": ["feat:adversarial-protocol-hardening-suites"],
|
|
37
|
+
"spec_ids": ["spc:2080"],
|
|
38
|
+
"cases": ["fragmented_control_frames", "masking_rules", "close_races", "compression_bombs"],
|
|
39
|
+
},
|
|
40
|
+
"webtransport": {
|
|
41
|
+
"owner_package": "tigrcorn-protocols",
|
|
42
|
+
"package_tests": ["tests/test_webtransport_resource_governance.py"],
|
|
43
|
+
"cross_cutting_tests": ["tests/test_webtransport_resource_governance_integration.py"],
|
|
44
|
+
"feature_ids": ["feat:webtransport-resource-governance", "feat:adversarial-protocol-hardening-suites"],
|
|
45
|
+
"spec_ids": ["spc:2061", "spc:2080"],
|
|
46
|
+
"cases": ["session_budget", "stream_budget", "datagram_budget", "drain", "reset_stop_sending"],
|
|
47
|
+
},
|
|
48
|
+
"static": {
|
|
49
|
+
"owner_package": "tigrcorn-static",
|
|
50
|
+
"package_tests": ["tests/test_static_delivery_security_certification.py"],
|
|
51
|
+
"cross_cutting_tests": ["tests/test_certification_command_namespace.py"],
|
|
52
|
+
"feature_ids": ["feat:static-delivery-security-certification", "feat:adversarial-protocol-hardening-suites"],
|
|
53
|
+
"spec_ids": ["spc:2062", "spc:2080"],
|
|
54
|
+
"cases": ["traversal", "sidecar_mismatch", "range_amplification", "content_type_ambiguity"],
|
|
55
|
+
},
|
|
56
|
+
"observability": {
|
|
57
|
+
"owner_package": "tigrcorn-observability",
|
|
58
|
+
"package_tests": ["tests/test_protocol_aware_observability.py"],
|
|
59
|
+
"cross_cutting_tests": ["tests/test_protocol_aware_observability_integration.py"],
|
|
60
|
+
"feature_ids": ["feat:protocol-aware-observability"],
|
|
61
|
+
"spec_ids": ["spc:2064"],
|
|
62
|
+
"cases": ["metric_names", "event_names", "cardinality_limits"],
|
|
63
|
+
},
|
|
64
|
+
"certification": {
|
|
65
|
+
"owner_package": "tigrcorn-certification",
|
|
66
|
+
"package_tests": ["tests/test_certification_command_namespace.py"],
|
|
67
|
+
"cross_cutting_tests": ["tests/test_adversarial_protocol_hardening_suites.py"],
|
|
68
|
+
"feature_ids": ["feat:certification-command-namespace"],
|
|
69
|
+
"spec_ids": ["spc:2079"],
|
|
70
|
+
"cases": ["command_dispatch", "artifact_shape", "leaf_import_boundary", "fail_closed"],
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def export_hardening_suite_catalog() -> dict[str, Any]:
|
|
76
|
+
return {
|
|
77
|
+
"catalog": deepcopy(HARDENING_SUITE_CATALOG),
|
|
78
|
+
"schema_version": 1,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def validate_hardening_suite_catalog(catalog: dict[str, dict[str, Any]] | None = None) -> dict[str, Any]:
|
|
83
|
+
selected = catalog or HARDENING_SUITE_CATALOG
|
|
84
|
+
failures: list[str] = []
|
|
85
|
+
for suite_id, suite in sorted(selected.items()):
|
|
86
|
+
owner = suite.get("owner_package")
|
|
87
|
+
if not isinstance(owner, str) or not owner.startswith("tigrcorn-"):
|
|
88
|
+
failures.append(f"{suite_id}: owner_package must be a tigrcorn distribution")
|
|
89
|
+
for field in ("package_tests", "cross_cutting_tests", "feature_ids", "spec_ids", "cases"):
|
|
90
|
+
values = suite.get(field)
|
|
91
|
+
if not isinstance(values, list) or not values:
|
|
92
|
+
failures.append(f"{suite_id}: {field} must be a non-empty list")
|
|
93
|
+
for test_path in suite.get("package_tests", []) + suite.get("cross_cutting_tests", []):
|
|
94
|
+
if not isinstance(test_path, str) or not test_path.startswith("tests/"):
|
|
95
|
+
failures.append(f"{suite_id}: test path must be under tests/: {test_path!r}")
|
|
96
|
+
return {"failures": failures, "passed": not failures, "suite_count": len(selected)}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
__all__ = [
|
|
100
|
+
"HARDENING_SUITE_CATALOG",
|
|
101
|
+
"export_hardening_suite_catalog",
|
|
102
|
+
"validate_hardening_suite_catalog",
|
|
103
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class UDPImpairmentProfile:
|
|
8
|
+
"""Deterministic UDP impairment settings for repeatable QUIC proofs."""
|
|
9
|
+
|
|
10
|
+
drop_every: int = 0
|
|
11
|
+
duplicate_every: int = 0
|
|
12
|
+
reorder_every: int = 0
|
|
13
|
+
delay_seconds: float = 0.0
|
|
14
|
+
|
|
15
|
+
def __post_init__(self) -> None:
|
|
16
|
+
for name in ("drop_every", "duplicate_every", "reorder_every"):
|
|
17
|
+
if int(getattr(self, name)) < 0:
|
|
18
|
+
raise ValueError(f"{name} must be non-negative")
|
|
19
|
+
if self.delay_seconds < 0:
|
|
20
|
+
raise ValueError("delay_seconds must be non-negative")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class UDPImpairmentPolicy:
|
|
25
|
+
profile: UDPImpairmentProfile
|
|
26
|
+
_counts: dict[str, int] = field(default_factory=dict)
|
|
27
|
+
_held: dict[str, bytes] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
def apply(self, direction: str, payload: bytes) -> tuple[bytes, ...]:
|
|
30
|
+
count = self._counts.get(direction, 0) + 1
|
|
31
|
+
self._counts[direction] = count
|
|
32
|
+
if self.profile.drop_every and count % self.profile.drop_every == 0:
|
|
33
|
+
return ()
|
|
34
|
+
|
|
35
|
+
emitted: list[bytes] = []
|
|
36
|
+
held = self._held.pop(direction, None)
|
|
37
|
+
if self.profile.reorder_every and count % self.profile.reorder_every == 0:
|
|
38
|
+
self._held[direction] = bytes(payload)
|
|
39
|
+
if held is not None:
|
|
40
|
+
emitted.append(held)
|
|
41
|
+
return tuple(emitted)
|
|
42
|
+
|
|
43
|
+
emitted.append(bytes(payload))
|
|
44
|
+
if held is not None:
|
|
45
|
+
emitted.append(held)
|
|
46
|
+
if self.profile.duplicate_every and count % self.profile.duplicate_every == 0:
|
|
47
|
+
emitted.append(bytes(payload))
|
|
48
|
+
return tuple(emitted)
|
|
49
|
+
|
|
50
|
+
def flush(self, direction: str) -> tuple[bytes, ...]:
|
|
51
|
+
held = self._held.pop(direction, None)
|
|
52
|
+
return () if held is None else (held,)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["UDPImpairmentPolicy", "UDPImpairmentProfile"]
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from .imports import *
|
|
4
4
|
from .helpers import *
|
|
5
5
|
from .ports import _normalize_sockaddr
|
|
6
|
+
from .impairment import UDPImpairmentPolicy
|
|
6
7
|
|
|
7
8
|
class _PacketTraceWriter:
|
|
8
9
|
def __init__(self, path: Path) -> None:
|
|
@@ -124,7 +125,7 @@ class TCPRecordProxy:
|
|
|
124
125
|
|
|
125
126
|
|
|
126
127
|
class UDPRecordProxy:
|
|
127
|
-
def __init__(self, *, listen_host: str, listen_port: int, target_host: str, target_port: int, packet_trace_path: Path, ip_family: str = 'ipv4') -> None:
|
|
128
|
+
def __init__(self, *, listen_host: str, listen_port: int, target_host: str, target_port: int, packet_trace_path: Path, ip_family: str = 'ipv4', impairment: UDPImpairmentPolicy | None = None) -> None:
|
|
128
129
|
family = socket.AF_INET6 if ip_family == 'ipv6' else socket.AF_INET
|
|
129
130
|
self.listen_host = listen_host
|
|
130
131
|
self.listen_port = listen_port
|
|
@@ -144,6 +145,14 @@ class UDPRecordProxy:
|
|
|
144
145
|
self._stop = threading.Event()
|
|
145
146
|
self._thread = threading.Thread(target=self._loop, daemon=True)
|
|
146
147
|
self._last_client: tuple[str, int] | None = None
|
|
148
|
+
self._impairment = impairment
|
|
149
|
+
|
|
150
|
+
def _forward_payloads(self, direction: str, payload: bytes) -> tuple[bytes, ...]:
|
|
151
|
+
if self._impairment is None:
|
|
152
|
+
return (payload,)
|
|
153
|
+
if self._impairment.profile.delay_seconds:
|
|
154
|
+
time.sleep(self._impairment.profile.delay_seconds)
|
|
155
|
+
return self._impairment.apply(direction, payload)
|
|
147
156
|
|
|
148
157
|
def start(self) -> None:
|
|
149
158
|
self._thread.start()
|
|
@@ -170,10 +179,11 @@ class UDPRecordProxy:
|
|
|
170
179
|
remote=(self.target_host, self.target_port),
|
|
171
180
|
payload=payload,
|
|
172
181
|
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
182
|
+
for forwarded in self._forward_payloads('client_to_server', payload):
|
|
183
|
+
try:
|
|
184
|
+
self._upstream.sendto(forwarded, target)
|
|
185
|
+
except OSError:
|
|
186
|
+
continue
|
|
177
187
|
elif key.fileobj is self._upstream:
|
|
178
188
|
try:
|
|
179
189
|
payload, _addr = self._upstream.recvfrom(65535)
|
|
@@ -188,10 +198,11 @@ class UDPRecordProxy:
|
|
|
188
198
|
remote=self._last_client,
|
|
189
199
|
payload=payload,
|
|
190
200
|
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
for forwarded in self._forward_payloads('server_to_client', payload):
|
|
202
|
+
try:
|
|
203
|
+
self._downstream.sendto(forwarded, self._last_client)
|
|
204
|
+
except OSError:
|
|
205
|
+
continue
|
|
195
206
|
finally:
|
|
196
207
|
selector.close()
|
|
197
208
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrcorn-certification
|
|
3
|
-
Version: 0.3.18.
|
|
3
|
+
Version: 0.3.18.dev3
|
|
4
4
|
Summary: Certification and release-gate tooling for Tigrcorn HTTP/3, QUIC, WebSocket, WebTransport, and ASGI conformance evidence.
|
|
5
5
|
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
6
6
|
License: Apache License
|
|
@@ -187,8 +187,9 @@ Classifier: Typing :: Typed
|
|
|
187
187
|
Requires-Python: <3.15,>=3.10
|
|
188
188
|
Description-Content-Type: text/markdown
|
|
189
189
|
License-File: LICENSE
|
|
190
|
-
Requires-Dist: tigrcorn-compat==0.3.18.
|
|
191
|
-
Requires-Dist: tigrcorn-runtime==0.3.18.
|
|
190
|
+
Requires-Dist: tigrcorn-compat==0.3.18.dev3
|
|
191
|
+
Requires-Dist: tigrcorn-runtime==0.3.18.dev3
|
|
192
|
+
Requires-Dist: tigrcorn-static==0.3.18.dev3
|
|
192
193
|
Requires-Dist: tomli>=2.0.0; python_version < "3.11"
|
|
193
194
|
Requires-Dist: cryptography>=46.0.0
|
|
194
195
|
Requires-Dist: aioquic>=1.3.0
|
|
@@ -286,7 +287,7 @@ The package exposes its supported public surface through the <code>tigrcorn_cert
|
|
|
286
287
|
|
|
287
288
|
## Package Graph
|
|
288
289
|
|
|
289
|
-
[tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
|
|
290
|
+
[tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
|
|
290
291
|
|
|
291
292
|
## Best Practices
|
|
292
293
|
|
|
@@ -6,6 +6,7 @@ src/tigrcorn_certification/artifacts.py
|
|
|
6
6
|
src/tigrcorn_certification/certification_env.py
|
|
7
7
|
src/tigrcorn_certification/conformance.py
|
|
8
8
|
src/tigrcorn_certification/explicit_surfaces.py
|
|
9
|
+
src/tigrcorn_certification/hardening_suites.py
|
|
9
10
|
src/tigrcorn_certification/py.typed
|
|
10
11
|
src/tigrcorn_certification/supply_chain.py
|
|
11
12
|
src/tigrcorn_certification.egg-info/PKG-INFO
|
|
@@ -20,12 +21,15 @@ src/tigrcorn_certification/aioquic_preflight/helpers.py
|
|
|
20
21
|
src/tigrcorn_certification/aioquic_preflight/imports.py
|
|
21
22
|
src/tigrcorn_certification/aioquic_preflight/records.py
|
|
22
23
|
src/tigrcorn_certification/aioquic_preflight/status_docs.py
|
|
24
|
+
src/tigrcorn_certification/certify/__init__.py
|
|
25
|
+
src/tigrcorn_certification/certify/static.py
|
|
23
26
|
src/tigrcorn_certification/interop_runner/__init__.py
|
|
24
27
|
src/tigrcorn_certification/interop_runner/adapters.py
|
|
25
28
|
src/tigrcorn_certification/interop_runner/assertions.py
|
|
26
29
|
src/tigrcorn_certification/interop_runner/core.py
|
|
27
30
|
src/tigrcorn_certification/interop_runner/environment.py
|
|
28
31
|
src/tigrcorn_certification/interop_runner/helpers.py
|
|
32
|
+
src/tigrcorn_certification/interop_runner/impairment.py
|
|
29
33
|
src/tigrcorn_certification/interop_runner/imports.py
|
|
30
34
|
src/tigrcorn_certification/interop_runner/matrix.py
|
|
31
35
|
src/tigrcorn_certification/interop_runner/models.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|