Files
browser-cli/tests/test_auth.py
T
daniel156161 0d5c49c19a
Testing / test (push) Failing after 10m21s
refactor: split compat into package, harden serve proxy (v0.9.3)
- compat.py → compat/ package: auth.py (auth-field normalizers),
  commands.py (command-format shims), __init__.py (re-exports)
- Add _auth_0_9_3 transformer: normalizes pubkey to lowercase before auth
  so clients < 0.9.3 sending uppercase hex are accepted
- adapt_auth() now called before auth check in serve.py; command extracted
  after adapt_auth so future transformers can rename commands safely
- serve.py: deduplicate _recv_exact (import from client), unify
  resp/resp_payload across Windows/Unix branches, require lowercase hex
  pubkey (re.fullmatch), reorganize imports, drop unused os import
- client.py: move payload/framed construction inside branches (remote path
  no longer serializes JSON it never uses); fix _is_valid_key_spec
  operator precedence; import MAX_MSG_BYTES from version_manager
- auth.py: narrow except clause (ValueError instead of bare Exception)
- Bump version 0.9.2 → 0.9.3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 10:12:55 +02:00

161 lines
5.3 KiB
Python

import json
import tempfile
from pathlib import Path
import pytest
from browser_cli.auth import (
add_authorized_key,
canonical_payload,
generate_keypair,
load_authorized_keys,
load_authorized_keys_with_names,
load_private_key,
new_nonce,
sign,
verify,
)
from browser_cli.client import _is_valid_key_spec
class TestGenerateKeypair:
def test_returns_pem_and_hex(self):
pem, pub_hex = generate_keypair()
assert pem.startswith(b"-----BEGIN PRIVATE KEY-----")
assert len(pub_hex) == 64
def test_each_call_unique(self):
_, pub1 = generate_keypair()
_, pub2 = generate_keypair()
assert pub1 != pub2
class TestCanonicalPayload:
def test_strips_pubkey_and_sig(self):
msg = {"command": "tabs.list", "id": "x", "pubkey": "abc", "sig": "def"}
data = json.loads(canonical_payload(msg))
assert "pubkey" not in data
assert "sig" not in data
def test_keys_sorted(self):
msg = {"z": 1, "a": 2, "m": 3}
payload = canonical_payload(msg).decode()
assert payload.index('"a"') < payload.index('"m"') < payload.index('"z"')
def test_deterministic(self):
msg = {"b": 2, "a": 1}
assert canonical_payload(msg) == canonical_payload(msg)
@pytest.fixture()
def keypair(tmp_path):
pem, pub_hex = generate_keypair()
key_path = tmp_path / "client.key.pem"
key_path.write_bytes(pem)
priv = load_private_key(key_path)
return priv, pub_hex
class TestSignVerify:
def test_valid_signature_verifies(self, keypair):
priv, pub_hex = keypair
nonce = bytes.fromhex(new_nonce())
msg = {"command": "tabs.list", "id": "uuid-1", "args": {}}
sig = sign(priv, nonce, msg).hex()
assert verify(pub_hex, nonce, msg, sig) is True
def test_tampered_sig_fails(self, keypair):
priv, pub_hex = keypair
nonce = bytes.fromhex(new_nonce())
msg = {"command": "tabs.list", "id": "x"}
sign(priv, nonce, msg)
assert verify(pub_hex, nonce, msg, "00" * 64) is False
def test_wrong_pubkey_fails(self, keypair):
priv, _ = keypair
_, other_pub = generate_keypair()
nonce = bytes.fromhex(new_nonce())
msg = {"command": "tabs.list"}
sig = sign(priv, nonce, msg).hex()
assert verify(other_pub, nonce, msg, sig) is False
def test_wrong_nonce_fails(self, keypair):
priv, pub_hex = keypair
nonce = bytes.fromhex(new_nonce())
msg = {"command": "tabs.list"}
sig = sign(priv, nonce, msg).hex()
other_nonce = bytes.fromhex(new_nonce())
assert verify(pub_hex, other_nonce, msg, sig) is False
def test_garbage_pub_hex_returns_false_not_exception(self):
assert verify("not-hex!!!!", b"nonce", {}, "00" * 64) is False
def test_truncated_sig_hex_returns_false_not_exception(self, keypair):
_, pub_hex = keypair
assert verify(pub_hex, b"nonce", {}, "aabb") is False
def test_wrong_length_pubkey_returns_false_not_exception(self):
assert verify("aabbcc", b"nonce", {}, "00" * 64) is False
class TestAuthorizedKeys:
def test_add_and_load(self, tmp_path):
path = tmp_path / "authorized_keys"
_, pub = generate_keypair()
assert add_authorized_key(path, pub, "alice") is True
assert pub in load_authorized_keys(path)
def test_add_duplicate_returns_false(self, tmp_path):
path = tmp_path / "authorized_keys"
_, pub = generate_keypair()
add_authorized_key(path, pub)
assert add_authorized_key(path, pub) is False
def test_load_with_names(self, tmp_path):
path = tmp_path / "authorized_keys"
_, pub1 = generate_keypair()
_, pub2 = generate_keypair()
add_authorized_key(path, pub1, "alice")
add_authorized_key(path, pub2)
entries = load_authorized_keys_with_names(path)
assert (pub1, "alice") in entries
assert (pub2, "") in entries
def test_ignores_comment_lines(self, tmp_path):
path = tmp_path / "authorized_keys"
path.write_text("# this is a comment\n")
assert load_authorized_keys(path) == []
def test_returns_empty_for_missing_file(self, tmp_path):
assert load_authorized_keys(tmp_path / "nofile") == []
class TestIsValidKeySpec:
def test_agent_bare(self):
assert _is_valid_key_spec("agent") is True
def test_agent_with_selector(self):
assert _is_valid_key_spec("agent:cardno:000012345678") is True
def test_absolute_pem_path(self):
assert _is_valid_key_spec("/home/user/.config/browser-cli/client.key.pem") is True
def test_dot_key_extension(self):
assert _is_valid_key_spec("/tmp/mykey.key") is True
def test_angled_bracket_pem_rejected(self):
# regression: operator precedence bug allowed "<garbage>.pem" to pass
assert _is_valid_key_spec("<garbage>.pem") is False
def test_angled_bracket_key_rejected(self):
assert _is_valid_key_spec("<garbage>.key") is False
def test_serialized_object_rejected(self):
assert _is_valid_key_spec("<AgentKey(blob=b'...', comment='test')>.pem") is False
def test_empty_string_rejected(self):
assert _is_valid_key_spec("") is False
def test_bare_filename_no_slash_no_ext_rejected(self):
assert _is_valid_key_spec("mykey") is False