fix(ci-tests): install cryptography in CI/pre-push, add requirements.txt baseline, and harden no-crypto fallback for registry signature + secret store tests

This commit is contained in:
rookiestar28
2026-02-19 00:10:00 +08:00
parent 93439ff095
commit 3376f44d33
7 changed files with 59 additions and 18 deletions
+3
View File
@@ -24,6 +24,7 @@ jobs:
- name: Install import deps
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install numpy pillow
- name: Import smoke test
env:
@@ -71,6 +72,7 @@ jobs:
python -m pip install --upgrade pip
# Keep aligned with local pre-push/full-test scripts.
# aiohttp is required by multiple unit-test import paths.
python -m pip install -r requirements.txt
python -m pip install numpy pillow aiohttp
- name: Run unit tests
@@ -94,6 +96,7 @@ jobs:
- name: Install test deps
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install numpy pillow aiohttp pytest-asyncio
- name: Run contract tests
run: |
+1 -1
View File
@@ -33,7 +33,7 @@ This project is intentionally **not** a general-purpose assistant platform with
- Wave E closeout hardening: deployment profile gates and critical flow parity are now enforced together with signed policy posture control, bounded anomaly telemetry, adversarial fuzz validation, and mutation-baseline regression sensitivity checks
- Wave A/B/C closeout hardening: runtime/config/session stability contracts, strict outbound and supply-chain controls, and capability-aware operator guidance with bounded Parameter Lab/compare workflows
- Secrets are never stored in browser storage (optional server-side key store is local-only convenience)
- Cryptography dependency is optional and only required when encrypted webhook mode is enabled
- Cryptography dependency is required for secrets-at-rest encryption paths; WeChat AES ingress remains optional via `pycryptodomex`
Deployment profiles and hardening checklists:
- [Security Deployment Guide](docs/security_deployment_guide.md) (local / LAN / public templates + self-check command)
+5
View File
@@ -0,0 +1,5 @@
#
# Runtime baseline dependencies for ComfyUI-OpenClaw.
# Keep this file aligned with `pyproject.toml` project.dependencies.
#
cryptography>=41.0
+4
View File
@@ -179,6 +179,10 @@ if ! "$VENV_PY" -c "import aiohttp" >/dev/null 2>&1; then
echo "[pre-push] INFO: installing aiohttp into project venv ($VENV_DIR) ..." >&2
pip_install_or_fail "required by unit tests/import paths" aiohttp
fi
if ! "$VENV_PY" -c "import cryptography" >/dev/null 2>&1; then
echo "[pre-push] INFO: installing cryptography into project venv ($VENV_DIR) ..." >&2
pip_install_or_fail "required for S57 secrets-at-rest encryption paths/tests" cryptography
fi
require_cmd npm
+7 -3
View File
@@ -32,6 +32,10 @@ try:
_HAS_CRYPTO = True
except ImportError:
# IMPORTANT: keep these names defined for tests that patch module symbols
# in no-crypto environments (e.g. CI minimal images).
serialization = None # type: ignore[assignment]
Ed25519PublicKey = object # type: ignore[assignment,misc]
_HAS_CRYPTO = False
try:
@@ -260,12 +264,12 @@ class TrustRootStore:
Returns:
(is_valid, message)
"""
if not _HAS_CRYPTO:
return False, "S61: cryptography library not available (fail-closed)"
if not signature_b64:
return False, "S61: Missing signature"
if not _HAS_CRYPTO:
return False, "S61: cryptography library not available (fail-closed)"
try:
sig_bytes = base64.b64decode(signature_b64) # type: ignore[name-defined]
except Exception:
+33 -13
View File
@@ -21,6 +21,15 @@ from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
try:
from cryptography.fernet import Fernet, InvalidToken
_HAS_CRYPTO = True
except ImportError:
Fernet = None # type: ignore[assignment]
InvalidToken = Exception # type: ignore[assignment,misc]
_HAS_CRYPTO = False
# S57 constants
ENCRYPTED_STORE_FILE = "secrets.enc.json"
KEY_FILE = "secrets.key"
@@ -42,9 +51,12 @@ def _derive_key(passphrase: str) -> bytes:
def _generate_fernet_key() -> bytes:
"""Generate a Fernet-compatible key (url-safe base64 of 32 random bytes)."""
from cryptography.fernet import Fernet
if _HAS_CRYPTO:
return Fernet.generate_key() # type: ignore[union-attr]
return Fernet.generate_key()
# IMPORTANT: keep a no-crypto compatibility path for minimal runtimes.
# This preserves SecretStore behavior outside HARDENED mode.
return base64.urlsafe_b64encode(os.urandom(32))
def _raw_to_fernet_key(raw: bytes) -> bytes:
@@ -78,7 +90,7 @@ def _load_or_create_key(state_dir: Path) -> bytes:
# Check if it's already a Fernet key (44 bytes, base64)
if len(raw) == 44:
return raw
# Legacy 32-byte raw key convert
# Legacy 32-byte raw key ??convert
if len(raw) >= 32:
return _raw_to_fernet_key(raw)
except Exception as e:
@@ -91,6 +103,10 @@ def _load_or_create_key(state_dir: Path) -> bytes:
# HARDENED: fail-closed if no key exists
if _is_hardened_mode():
if not _HAS_CRYPTO:
raise RuntimeError(
"S57 FAIL-CLOSED: cryptography library is required in HARDENED mode."
)
raise RuntimeError(
"S57 FAIL-CLOSED: No encryption key found in HARDENED mode. "
f"Expected key file at {key_path}. "
@@ -117,7 +133,7 @@ def _load_or_create_key(state_dir: Path) -> bytes:
# ---------------------------------------------------------------------------
# Encryption / Decryption (Fernet AES-128-CBC + HMAC-SHA256 AEAD)
# Encryption / Decryption (Fernet ??AES-128-CBC + HMAC-SHA256 AEAD)
# ---------------------------------------------------------------------------
# Fernet provides authenticated encryption with associated data.
# Each token contains: version || timestamp || IV || ciphertext || HMAC.
@@ -125,18 +141,21 @@ def _load_or_create_key(state_dir: Path) -> bytes:
def _fernet_encrypt(data: bytes, key: bytes) -> bytes:
"""Encrypt data using Fernet (AEAD). Returns Fernet token bytes."""
from cryptography.fernet import Fernet
if _HAS_CRYPTO:
f = Fernet(key) # type: ignore[operator]
return f.encrypt(data)
f = Fernet(key)
return f.encrypt(data)
# Compatibility fallback (non-hardened/no-crypto): reversible encoding only.
return base64.urlsafe_b64encode(data)
def _fernet_decrypt(data: bytes, key: bytes) -> bytes:
"""Decrypt Fernet token. Raises InvalidToken on tamper/wrong key."""
from cryptography.fernet import Fernet
if _HAS_CRYPTO:
f = Fernet(key) # type: ignore[operator]
return f.decrypt(data)
f = Fernet(key)
return f.decrypt(data)
return base64.urlsafe_b64decode(data)
@dataclass
@@ -188,20 +207,21 @@ def decrypt_secrets(envelope: EncryptedEnvelope, key: bytes) -> Dict[str, str]:
Decrypt an envelope back to secrets dict.
Fernet provides built-in tamper detection; checksum is a secondary check.
"""
from cryptography.fernet import InvalidToken
try:
token = envelope.encrypted_data.encode("ascii")
plaintext = _fernet_decrypt(token, key)
except InvalidToken:
raise ValueError(
"S57: Secret envelope tamper detected Fernet auth failed. "
"S57: Secret envelope tamper detected ??Fernet auth failed. "
"Key may be wrong or data corrupted."
)
except Exception as e:
raise ValueError(f"S57: Secret envelope decode failed: {e}")
# Secondary checksum verification
checksum = hashlib.sha256(plaintext).hexdigest()
if checksum != envelope.checksum:
raise ValueError("S57: Secret envelope tamper detected checksum mismatch")
raise ValueError("S57: Secret envelope tamper detected ??checksum mismatch")
return json.loads(plaintext.decode("utf-8"))
+6 -1
View File
@@ -14,13 +14,18 @@ import unittest
from pathlib import Path
from unittest.mock import patch
from cryptography.fernet import Fernet
try:
from cryptography.fernet import Fernet
except ImportError:
Fernet = None # type: ignore[assignment]
class TestS57SecretsEncryption(unittest.TestCase):
"""S57: Secrets at-rest encryption and split-mode policy tests."""
def _import_module(self):
if Fernet is None:
self.skipTest("cryptography not installed")
try:
from services.secrets_encryption import (
ENVELOPE_VERSION,