mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
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:
@@ -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: |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#
|
||||
# Runtime baseline dependencies for ComfyUI-OpenClaw.
|
||||
# Keep this file aligned with `pyproject.toml` project.dependencies.
|
||||
#
|
||||
cryptography>=41.0
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user