VirtualDesktop 1.0.1__py3-none-any.whl
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.
- VirtualDesktop/__init__.py +121 -0
- VirtualDesktop/__main__.py +10 -0
- VirtualDesktop/_bundle.py +369 -0
- VirtualDesktop/_compat.py +576 -0
- VirtualDesktop/_devices.py +335 -0
- VirtualDesktop/_download.py +462 -0
- VirtualDesktop/_helper.py +31 -0
- VirtualDesktop/_log.py +85 -0
- VirtualDesktop/_process.py +912 -0
- VirtualDesktop/_sigcheck.ps1 +32 -0
- VirtualDesktop/_sigcheck.py +321 -0
- VirtualDesktop/_winapi.py +714 -0
- VirtualDesktop/capture.py +671 -0
- VirtualDesktop/cli.py +673 -0
- VirtualDesktop/config.py +255 -0
- VirtualDesktop/desktop.py +231 -0
- VirtualDesktop/desktop_capture.py +159 -0
- VirtualDesktop/desktop_session.py +533 -0
- VirtualDesktop/display_setup.py +790 -0
- VirtualDesktop/driver.py +831 -0
- VirtualDesktop/driver_manifest.json +118 -0
- VirtualDesktop/errors.py +211 -0
- VirtualDesktop/manifest.py +310 -0
- VirtualDesktop/session.py +514 -0
- VirtualDesktop/window.py +869 -0
- virtualdesktop-1.0.1.dist-info/METADATA +307 -0
- virtualdesktop-1.0.1.dist-info/RECORD +31 -0
- virtualdesktop-1.0.1.dist-info/WHEEL +5 -0
- virtualdesktop-1.0.1.dist-info/entry_points.txt +2 -0
- virtualdesktop-1.0.1.dist-info/licenses/LICENSE +21 -0
- virtualdesktop-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
# Read the file list from stdin. Using stdin (rather than TLS arguments) keeps paths with
|
|
3
|
+
# spaces, quotes or non-ASCII characters safe and avoids relying on `$args` binding rules,
|
|
4
|
+
# which differ between PowerShell 5.1 and PowerShell 7.
|
|
5
|
+
$paths = @($input)
|
|
6
|
+
$out = New-Object System.Collections.ArrayList
|
|
7
|
+
foreach ($p in $paths) {
|
|
8
|
+
$line = "$p".Trim()
|
|
9
|
+
if (-not $line) { continue }
|
|
10
|
+
$entry = [ordered]@{ path = $line; ok = $false; status = 'unknown'; subject = ''; issuer = ''; thumbprint = ''; kind = 'none'; message = ''; error = '' }
|
|
11
|
+
try {
|
|
12
|
+
$sig = Get-AuthenticodeSignature -LiteralPath $line -ErrorAction Stop
|
|
13
|
+
$entry.status = "$($sig.Status)"
|
|
14
|
+
$entry.message = "$($sig.StatusMessage)"
|
|
15
|
+
if ($sig.SignerCertificate) {
|
|
16
|
+
$entry.subject = "$($sig.SignerCertificate.Subject)"
|
|
17
|
+
$entry.issuer = "$($sig.SignerCertificate.Issuer)"
|
|
18
|
+
$entry.thumbprint = "$($sig.SignerCertificate.Thumbprint)"
|
|
19
|
+
}
|
|
20
|
+
$props = @($sig.PSObject.Properties.Name)
|
|
21
|
+
if ($props -contains 'SignatureType') { $entry.kind = "$($sig.SignatureType)".ToLower() }
|
|
22
|
+
elseif ($sig.IsOSBinary) { $entry.kind = 'os' }
|
|
23
|
+
else { $entry.kind = 'embedded' }
|
|
24
|
+
$entry.ok = ($entry.status -eq 'Valid')
|
|
25
|
+
} catch {
|
|
26
|
+
$entry.status = 'unknown-error'
|
|
27
|
+
$entry.error = "$($_.Exception.Message)"
|
|
28
|
+
}
|
|
29
|
+
[void]$out.Add([pscustomobject]$entry)
|
|
30
|
+
}
|
|
31
|
+
$json = ConvertTo-Json -InputObject @($out) -Compress -Depth 4
|
|
32
|
+
[Console]::Out.WriteLine($json)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Authenticode verification for driver packages.
|
|
2
|
+
|
|
3
|
+
The manifest pins SHA-256 digests, which already protects against a swapped mirror. This
|
|
4
|
+
module adds *defence in depth*: after extraction (and before the driver is handed to the
|
|
5
|
+
Windows installer) the signature is verified and the signer certificate thumbprint is compared
|
|
6
|
+
against the manifest allow-list. A revoked or unexpected signer is reported instead of being
|
|
7
|
+
installed silently.
|
|
8
|
+
|
|
9
|
+
Why verification is delegated to PowerShell's ``Get-AuthenticodeSignature``
|
|
10
|
+
--------------------------------------------------------------------------
|
|
11
|
+
A direct ``WinVerifyTrust`` implementation through ``ctypes`` was written and **removed again**:
|
|
12
|
+
on this machine every call — including for known-good system binaries such as
|
|
13
|
+
``notepad.exe`` — returned ``TRUST_E_PROVIDER_UNKNOWN`` (``0x800B0001``) regardless of the
|
|
14
|
+
``WINTRUST_DATA`` layout or the ``hwnd`` value used. A verifier that answers "cannot verify"
|
|
15
|
+
for everything is worse than no verifier, because it looks like a checked result. The PowerShell
|
|
16
|
+
cmdlet uses the same WinVerifyTrust path ``signtool verify`` uses and returns the real answer
|
|
17
|
+
(measured on the driver package: ``mttvdd.cat`` and ``MttVDD.dll`` both ``Valid``, signer
|
|
18
|
+
``CN=SignPath Foundation``). When no verifier is available the result is ``UNKNOWN``, never
|
|
19
|
+
``VALID``.
|
|
20
|
+
|
|
21
|
+
``SIGNATURE_TARGETS`` deliberately checks the PE files *before* the catalog: a catalog carries no
|
|
22
|
+
signer certificate, so it can never satisfy the thumbprint allow-list, while ``MttVDD.dll`` does.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import shutil
|
|
30
|
+
import subprocess
|
|
31
|
+
import tempfile
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from enum import Enum
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Iterable, List, Optional, Sequence
|
|
36
|
+
|
|
37
|
+
from ._log import get_logger
|
|
38
|
+
from .errors import DriverVerificationError
|
|
39
|
+
|
|
40
|
+
log = get_logger("sigcheck")
|
|
41
|
+
|
|
42
|
+
__all__ = ["SignatureStatus", "SignatureReport", "verify_signer", "evaluate_trust", "POWERSHELL"]
|
|
43
|
+
|
|
44
|
+
POWERSHELL = "powershell.exe"
|
|
45
|
+
_CREATE_NO_WINDOW = 0x08000000
|
|
46
|
+
|
|
47
|
+
#: Files inside a driver package that must carry a trusted signature. The PE files come first so
|
|
48
|
+
#: that a signer thumbprint is available for the allow-list check; the catalog is verified too and
|
|
49
|
+
#: is what Windows itself enforces when the driver is installed.
|
|
50
|
+
SIGNATURE_TARGETS = (".dll", ".sys", ".cat")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SignatureStatus(str, Enum):
|
|
54
|
+
VALID = "valid"
|
|
55
|
+
NOT_SIGNED = "not-signed"
|
|
56
|
+
HASH_MISMATCH = "hash-mismatch"
|
|
57
|
+
NOT_TRUSTED = "not-trusted"
|
|
58
|
+
UNKNOWN_ERROR = "unknown-error"
|
|
59
|
+
UNKNOWN = "unknown"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_TRUSTED_STATUSES = {SignatureStatus.VALID.value}
|
|
63
|
+
_BAD_STATUSES = {
|
|
64
|
+
SignatureStatus.NOT_SIGNED.value,
|
|
65
|
+
SignatureStatus.HASH_MISMATCH.value,
|
|
66
|
+
SignatureStatus.NOT_TRUSTED.value,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class SignatureReport:
|
|
72
|
+
path: Path
|
|
73
|
+
status: SignatureStatus = SignatureStatus.UNKNOWN
|
|
74
|
+
subject: str = ""
|
|
75
|
+
issuer: str = ""
|
|
76
|
+
thumbprint: str = ""
|
|
77
|
+
kind: str = "none" # embedded | catalog | none
|
|
78
|
+
message: str = ""
|
|
79
|
+
error: str = ""
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def ok(self) -> bool:
|
|
83
|
+
return self.status is SignatureStatus.VALID
|
|
84
|
+
|
|
85
|
+
def to_dict(self) -> dict:
|
|
86
|
+
return {
|
|
87
|
+
"path": str(self.path),
|
|
88
|
+
"status": self.status.value,
|
|
89
|
+
"subject": self.subject,
|
|
90
|
+
"issuer": self.issuer,
|
|
91
|
+
"thumbprint": self.thumbprint,
|
|
92
|
+
"kind": self.kind,
|
|
93
|
+
"message": self.message,
|
|
94
|
+
"error": self.error,
|
|
95
|
+
"ok": self.ok,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_SCRIPT_PATH = Path(__file__).with_name("_sigcheck.ps1")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _fallback_script() -> str:
|
|
103
|
+
"""Inline equivalent of ``_sigcheck.ps1`` for the (unlikely) case it is missing."""
|
|
104
|
+
try:
|
|
105
|
+
return _SCRIPT_PATH.read_text(encoding="utf-8")
|
|
106
|
+
except OSError: # pragma: no cover - packaged wheels always ship the file
|
|
107
|
+
return (
|
|
108
|
+
"$paths=@($input);$out=New-Object System.Collections.ArrayList;"
|
|
109
|
+
"foreach($p in $paths){$line=\"$p\".Trim();if(-not $line){continue};"
|
|
110
|
+
"$e=[ordered]@{path=$line;ok=$false;status='unknown';subject='';issuer='';"
|
|
111
|
+
"thumbprint='';kind='none';message='';error=''};"
|
|
112
|
+
"try{$s=Get-AuthenticodeSignature -LiteralPath $line -ErrorAction Stop;"
|
|
113
|
+
"$e.status=\"$($s.Status)\";$e.message=\"$($s.StatusMessage)\";"
|
|
114
|
+
"if($s.SignerCertificate){$e.subject=\"$($s.SignerCertificate.Subject)\";"
|
|
115
|
+
"$e.issuer=\"$($s.SignerCertificate.Issuer)\";"
|
|
116
|
+
"$e.thumbprint=\"$($s.SignerCertificate.Thumbprint)\"};"
|
|
117
|
+
"if(@($s.PSObject.Properties.Name) -contains 'SignatureType'){$e.kind=\"$($s.SignatureType)\".ToLower()}"
|
|
118
|
+
"elseif($s.IsOSBinary){$e.kind='os'}else{$e.kind='embedded'};$e.ok=($e.status -eq 'Valid')}"
|
|
119
|
+
"catch{$e.status='unknown-error';$e.error=\"$($_.Exception.Message)\"};"
|
|
120
|
+
"[void]$out.Add([pscustomobject]$e)};"
|
|
121
|
+
"[Console]::Out.WriteLine((ConvertTo-Json -InputObject @($out) -Compress -Depth 4))"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _powershell_path() -> Optional[str]:
|
|
126
|
+
found = shutil.which(POWERSHELL)
|
|
127
|
+
return found
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def verify_signer(paths: Sequence[os.PathLike], *, timeout: float = 120.0) -> List[SignatureReport]:
|
|
131
|
+
"""Verify Authenticode signatures for one or more files in a single PowerShell call.
|
|
132
|
+
|
|
133
|
+
The file list travels through a UTF-8 scratch file that the verifier reads, rather than over
|
|
134
|
+
stdin or through the command line. Both alternatives were measured and rejected:
|
|
135
|
+
|
|
136
|
+
* piping ``script + "\\n" + paths`` into ``powershell -Command -`` produced **no output at
|
|
137
|
+
all** on Windows PowerShell 5.1 — the verifier answered ``[]`` for a driver package whose
|
|
138
|
+
signatures are both valid, which reads like "nothing is signed";
|
|
139
|
+
* passing the paths as command-line arguments needs quoting rules that differ between
|
|
140
|
+
PowerShell 5.1 and 7 and break on paths containing quotes.
|
|
141
|
+
|
|
142
|
+
A file has neither problem, and ``Get-Content -Encoding UTF8`` keeps non-ASCII paths intact.
|
|
143
|
+
"""
|
|
144
|
+
targets = [Path(p) for p in paths]
|
|
145
|
+
if not targets:
|
|
146
|
+
return []
|
|
147
|
+
unknown = [
|
|
148
|
+
SignatureReport(path=target, status=SignatureStatus.UNKNOWN, error="verifier-unavailable")
|
|
149
|
+
for target in targets
|
|
150
|
+
]
|
|
151
|
+
if os.name != "nt": # pragma: no cover - Windows-only library
|
|
152
|
+
return [
|
|
153
|
+
SignatureReport(path=t, status=SignatureStatus.UNKNOWN, error="not-windows")
|
|
154
|
+
for t in targets
|
|
155
|
+
]
|
|
156
|
+
ps = _powershell_path()
|
|
157
|
+
if not ps:
|
|
158
|
+
log.warning("no powershell.exe available; signature verification reported as unknown")
|
|
159
|
+
return unknown
|
|
160
|
+
|
|
161
|
+
script = _SCRIPT_PATH if _SCRIPT_PATH.is_file() else None
|
|
162
|
+
scratch_dir = Path(tempfile.mkdtemp(prefix="VirtualDesktop-sigcheck-"))
|
|
163
|
+
list_path = scratch_dir / "paths.txt"
|
|
164
|
+
try:
|
|
165
|
+
list_path.write_text("\n".join(str(t) for t in targets) + "\n", encoding="utf-8")
|
|
166
|
+
if script is not None:
|
|
167
|
+
expression = (
|
|
168
|
+
f"Get-Content -LiteralPath '{list_path}' -Encoding UTF8 | "
|
|
169
|
+
f"& '{script}'"
|
|
170
|
+
)
|
|
171
|
+
command = [
|
|
172
|
+
ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
173
|
+
"-Command", expression,
|
|
174
|
+
]
|
|
175
|
+
stdin_payload = None
|
|
176
|
+
else: # pragma: no cover - packaged wheels always ship the script
|
|
177
|
+
command = [
|
|
178
|
+
ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
179
|
+
"-Command", "-",
|
|
180
|
+
]
|
|
181
|
+
stdin_payload = _fallback_script() + "\n"
|
|
182
|
+
try:
|
|
183
|
+
completed = subprocess.run(
|
|
184
|
+
command,
|
|
185
|
+
input=stdin_payload,
|
|
186
|
+
capture_output=True,
|
|
187
|
+
text=True,
|
|
188
|
+
timeout=timeout,
|
|
189
|
+
creationflags=_CREATE_NO_WINDOW,
|
|
190
|
+
)
|
|
191
|
+
except (OSError, subprocess.SubprocessError) as exc: # pragma: no cover - env specific
|
|
192
|
+
log.warning("signature verification failed to run: %s", exc)
|
|
193
|
+
return [
|
|
194
|
+
SignatureReport(path=t, status=SignatureStatus.UNKNOWN, error=str(exc))
|
|
195
|
+
for t in targets
|
|
196
|
+
]
|
|
197
|
+
finally:
|
|
198
|
+
shutil.rmtree(scratch_dir, ignore_errors=True)
|
|
199
|
+
|
|
200
|
+
payload = (completed.stdout or "").strip()
|
|
201
|
+
if not payload:
|
|
202
|
+
return [
|
|
203
|
+
SignatureReport(
|
|
204
|
+
path=t,
|
|
205
|
+
status=SignatureStatus.UNKNOWN,
|
|
206
|
+
error=(completed.stderr or "").strip()[:500] or "empty verifier output",
|
|
207
|
+
)
|
|
208
|
+
for t in targets
|
|
209
|
+
]
|
|
210
|
+
try:
|
|
211
|
+
data = json.loads(payload)
|
|
212
|
+
except json.JSONDecodeError as exc: # pragma: no cover - defensive
|
|
213
|
+
log.warning("could not parse signature report: %s", exc)
|
|
214
|
+
return unknown
|
|
215
|
+
if isinstance(data, dict):
|
|
216
|
+
data = [data]
|
|
217
|
+
by_path: dict = {}
|
|
218
|
+
for item in data:
|
|
219
|
+
if not isinstance(item, dict):
|
|
220
|
+
continue
|
|
221
|
+
raw_status = str(item.get("status", "unknown")).strip().lower().replace(" ", "-")
|
|
222
|
+
try:
|
|
223
|
+
status = SignatureStatus(raw_status)
|
|
224
|
+
except ValueError:
|
|
225
|
+
status = SignatureStatus.UNKNOWN
|
|
226
|
+
report = SignatureReport(
|
|
227
|
+
path=Path(str(item.get("path", ""))),
|
|
228
|
+
status=status,
|
|
229
|
+
subject=str(item.get("subject", "")),
|
|
230
|
+
issuer=str(item.get("issuer", "")),
|
|
231
|
+
thumbprint=str(item.get("thumbprint", "")).replace(" ", "").upper(),
|
|
232
|
+
kind=str(item.get("kind", "none")),
|
|
233
|
+
message=str(item.get("message", "")),
|
|
234
|
+
error=str(item.get("error", "")),
|
|
235
|
+
)
|
|
236
|
+
by_path[str(report.path).lower()] = report
|
|
237
|
+
return [by_path.get(str(t).lower(), unknown[i]) for i, t in enumerate(targets)]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def evaluate_trust(
|
|
241
|
+
reports: Iterable[SignatureReport],
|
|
242
|
+
trusted_thumbprints: Sequence[str],
|
|
243
|
+
*,
|
|
244
|
+
strict: bool = False,
|
|
245
|
+
) -> tuple:
|
|
246
|
+
"""Fold signature reports into ``(trusted, findings)``.
|
|
247
|
+
|
|
248
|
+
``trusted`` is True only when at least one *driver-significant* file has a valid
|
|
249
|
+
signature and no report is outright bad. ``findings`` is a list of human-readable
|
|
250
|
+
lines suitable for a warning banner or a CLI report.
|
|
251
|
+
"""
|
|
252
|
+
trusted_set = {t.replace(" ", "").upper() for t in trusted_thumbprints if t}
|
|
253
|
+
findings: List[str] = []
|
|
254
|
+
any_valid = False
|
|
255
|
+
bad = False
|
|
256
|
+
for report in reports:
|
|
257
|
+
if report.status is SignatureStatus.VALID:
|
|
258
|
+
any_valid = True
|
|
259
|
+
if trusted_set and report.thumbprint not in trusted_set:
|
|
260
|
+
findings.append(
|
|
261
|
+
f"{report.path.name}: signed by an unexpected publisher "
|
|
262
|
+
f"({report.subject or 'unknown'}, thumbprint {report.thumbprint or 'n/a'})"
|
|
263
|
+
)
|
|
264
|
+
if strict:
|
|
265
|
+
bad = True
|
|
266
|
+
else:
|
|
267
|
+
findings.append(f"{report.path.name}: valid signature ({report.subject})")
|
|
268
|
+
elif report.status.value in _BAD_STATUSES:
|
|
269
|
+
findings.append(
|
|
270
|
+
f"{report.path.name}: signature {report.status.value} ({report.message or report.error})"
|
|
271
|
+
)
|
|
272
|
+
bad = True
|
|
273
|
+
else:
|
|
274
|
+
findings.append(
|
|
275
|
+
f"{report.path.name}: signature could not be verified "
|
|
276
|
+
f"({report.error or report.message or report.status.value})"
|
|
277
|
+
)
|
|
278
|
+
if strict:
|
|
279
|
+
bad = True
|
|
280
|
+
if not any_valid:
|
|
281
|
+
findings.append("no file in the driver package carried a verifiable signature")
|
|
282
|
+
bad = True
|
|
283
|
+
return (any_valid and not bad), findings
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def assert_driver_trusted(
|
|
287
|
+
inf_dir: os.PathLike,
|
|
288
|
+
trusted_thumbprints: Sequence[str],
|
|
289
|
+
*,
|
|
290
|
+
strict: bool = False,
|
|
291
|
+
policy: str = "warn",
|
|
292
|
+
) -> List[str]:
|
|
293
|
+
"""Verify the driver package next to ``MttVDD.inf``.
|
|
294
|
+
|
|
295
|
+
``policy`` is ``warn`` (log and continue), ``strict`` (raise on any doubt) or ``off``.
|
|
296
|
+
Returns the findings so the caller can surface them in a report.
|
|
297
|
+
"""
|
|
298
|
+
if policy == "off":
|
|
299
|
+
return ["signature verification disabled by configuration"]
|
|
300
|
+
directory = Path(inf_dir)
|
|
301
|
+
targets = sorted(
|
|
302
|
+
(p for p in directory.iterdir() if p.is_file() and p.suffix.lower() in SIGNATURE_TARGETS),
|
|
303
|
+
# PE files first so a signer certificate is available for the allow-list check.
|
|
304
|
+
key=lambda p: (SIGNATURE_TARGETS.index(p.suffix.lower()), p.name),
|
|
305
|
+
)
|
|
306
|
+
if not targets:
|
|
307
|
+
return ["no signature targets found next to the INF"]
|
|
308
|
+
reports = verify_signer(targets)
|
|
309
|
+
trusted, findings = evaluate_trust(reports, trusted_thumbprints, strict=strict)
|
|
310
|
+
for line in findings:
|
|
311
|
+
log.info("driver signature: %s", line)
|
|
312
|
+
if not trusted:
|
|
313
|
+
message = "driver package signature verification did not succeed: " + "; ".join(findings)
|
|
314
|
+
if policy == "strict" or strict:
|
|
315
|
+
raise DriverVerificationError(
|
|
316
|
+
message,
|
|
317
|
+
code="driver-signature-untrusted",
|
|
318
|
+
hint="Re-download the driver, or pin the expected signer thumbprint in your manifest.",
|
|
319
|
+
)
|
|
320
|
+
log.warning("%s (continuing because signature_policy='warn')", message)
|
|
321
|
+
return findings
|