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.
@@ -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