Zion Boggan
repos/Oversight/tests/test_gui_hardening_unit.py
zionboggan.com ↗
161 lines · python
History for this file →
1
"""
2
test_gui_hardening_unit
3
=======================
4
 
5
Focused checks for GUI/CLI filesystem safety and container parser hardening.
6
"""
7
 
8
from __future__ import annotations
9
 
10
import json
11
import sys
12
import tempfile
13
from pathlib import Path
14
 
15
ROOT = Path(__file__).resolve().parent.parent
16
sys.path.insert(0, str(ROOT))
17
 
18
try:
19
    import tkinter
20
except ImportError:
21
    tkinter = None
22
 
23
import pytest
24
 
25
pytestmark = pytest.mark.skipif(
26
    tkinter is None, reason="python3-tk not installed; GUI tests skipped"
27
)
28
 
29
if tkinter is not None:
30
    from cli import gui
31
 
32
from oversight_core import ClassicIdentity, Manifest, Recipient, content_hash, seal
33
from oversight_core.container import SealedFile
34
from oversight_core.safe_io import is_private_key_file, validate_output_path
35
 
36
 
37
def _identity_dict(identity_id: str = "alice") -> dict:
38
    ident = ClassicIdentity.generate()
39
    return {
40
        "id": identity_id,
41
        "x25519_priv": ident.x25519_priv.hex(),
42
        "x25519_pub": ident.x25519_pub.hex(),
43
        "ed25519_priv": ident.ed25519_priv.hex(),
44
        "ed25519_pub": ident.ed25519_pub.hex(),
45
    }
46
 
47
 
48
def _sealed_blob() -> bytes:
49
    issuer = ClassicIdentity.generate()
50
    recipient = ClassicIdentity.generate()
51
    plaintext = b"hello oversight"
52
    manifest = Manifest.new(
53
        "hello.txt",
54
        content_hash(plaintext),
55
        len(plaintext),
56
        "issuer",
57
        issuer.ed25519_pub.hex(),
58
        Recipient("alice", recipient.x25519_pub.hex(), recipient.ed25519_pub.hex()),
59
        "https://registry.oversightprotocol.dev",
60
        "text/plain",
61
    )
62
    return seal(plaintext, manifest, issuer.ed25519_priv, recipient.x25519_pub)
63
 
64
 
65
def test_private_key_outputs_are_blocked():
66
    with tempfile.TemporaryDirectory() as td:
67
        key_path = Path(td) / "alice.priv.json"
68
        key_path.write_text(json.dumps(_identity_dict()), encoding="utf-8")
69
        assert is_private_key_file(key_path), "fixture should parse as private key"
70
        try:
71
            validate_output_path(key_path)
72
        except ValueError as exc:
73
            assert "private key" in str(exc)
74
        else:
75
            raise AssertionError("private key overwrite was not blocked")
76
    print("  [PASS] private key output targets are hard-blocked")
77
 
78
 
79
def test_same_path_outputs_are_blocked():
80
    with tempfile.TemporaryDirectory() as td:
81
        input_path = Path(td) / "source.txt"
82
        input_path.write_text("source", encoding="utf-8")
83
        try:
84
            validate_output_path(input_path, input_paths=[input_path])
85
        except ValueError as exc:
86
            assert "different" in str(exc)
87
        else:
88
            raise AssertionError("same-path output was not blocked")
89
    print("  [PASS] output paths cannot equal input paths")
90
 
91
 
92
def test_windows_reserved_names_are_rejected():
93
    try:
94
        validate_output_path(Path("NUL.priv.json"))
95
    except ValueError as exc:
96
        assert "reserved" in str(exc)
97
    else:
98
        raise AssertionError("Windows reserved output name was not blocked")
99
    print("  [PASS] Windows reserved output names are rejected")
100
 
101
 
102
def test_gui_key_shape_errors_are_friendly():
103
    with tempfile.TemporaryDirectory() as td:
104
        pub_path = Path(td) / "alice.pub.json"
105
        pub_path.write_text(json.dumps({"id": "alice", "x25519_pub": "00" * 32}), encoding="utf-8")
106
        try:
107
            gui._read_private_identity(pub_path, "Issuer file")
108
        except ValueError as exc:
109
            assert "public key" in str(exc) and "x25519_priv" in str(exc)
110
        else:
111
            raise AssertionError("public key accepted as private identity")
112
    print("  [PASS] key-shape mistakes get actionable GUI errors")
113
 
114
 
115
def test_gui_registry_domain_uses_user_url():
116
    assert gui._registry_domain("https://registry.example.test:8443/api") == "registry.example.test:8443"
117
    print("  [PASS] GUI beacon domain derives from the configured registry URL")
118
 
119
 
120
def test_container_rejects_suite_id_tamper():
121
    blob = bytearray(_sealed_blob())
122
    blob[7] ^= 0x01
123
    try:
124
        SealedFile.from_bytes(bytes(blob))
125
    except ValueError as exc:
126
        assert "suite" in str(exc).lower()
127
    else:
128
        raise AssertionError("suite_id tamper was accepted")
129
    print("  [PASS] unauthenticated suite_id tamper is rejected")
130
 
131
 
132
def test_container_rejects_trailing_bytes():
133
    try:
134
        SealedFile.from_bytes(_sealed_blob() + b"junk")
135
    except ValueError as exc:
136
        assert "Trailing bytes" in str(exc)
137
    else:
138
        raise AssertionError("trailing bytes were accepted")
139
    print("  [PASS] trailing bytes after ciphertext are rejected")
140
 
141
 
142
def main():
143
    print("=" * 60)
144
    print("  GUI/CLI hardening - focused unit tests")
145
    print("=" * 60)
146
    test_private_key_outputs_are_blocked()
147
    test_same_path_outputs_are_blocked()
148
    test_windows_reserved_names_are_rejected()
149
    test_gui_key_shape_errors_are_friendly()
150
    test_gui_registry_domain_uses_user_url()
151
    test_container_rejects_suite_id_tamper()
152
    test_container_rejects_trailing_bytes()
153
    print()
154
    print("  ALL TESTS PASSED - 7/7")
155
 
156
 
157
if __name__ == "__main__":
158
    if tkinter is None:
159
        print("python3-tk not installed; GUI tests skipped")
160
    else:
161
        main()