@trillboards/edge-sdk 0.2.26 → 0.2.36
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.
|
@@ -57,6 +57,12 @@
|
|
|
57
57
|
signed (by Azure Trusted Signing) and are proved
|
|
58
58
|
as-is, with the unsigned control derived by stripping
|
|
59
59
|
the signature block from a copy.
|
|
60
|
+
.PARAMETER RetainSelfSignedFixturePath
|
|
61
|
+
In hermetic mode, retain the exact proven root until
|
|
62
|
+
a downstream proof cleans it and write only its
|
|
63
|
+
subject/thumbprint metadata to this path. The owning
|
|
64
|
+
harness emits this directly; callers must not scan
|
|
65
|
+
the certificate provider after the proof.
|
|
60
66
|
|
|
61
67
|
.NOTES
|
|
62
68
|
.cmd/.bat have no Authenticode SIP and cmd.exe has no execution policy, so a
|
|
@@ -71,7 +77,8 @@ param(
|
|
|
71
77
|
[string]$CanaryPs1 = "",
|
|
72
78
|
[string]$CanaryVbs = "",
|
|
73
79
|
[switch]$SelfSign,
|
|
74
|
-
[switch]$AlreadySigned
|
|
80
|
+
[switch]$AlreadySigned,
|
|
81
|
+
[string]$RetainSelfSignedFixturePath = ""
|
|
75
82
|
)
|
|
76
83
|
|
|
77
84
|
$ErrorActionPreference = 'Stop'
|
|
@@ -81,12 +88,17 @@ if (-not ($SelfSign -xor $AlreadySigned)) {
|
|
|
81
88
|
Write-Error 'Choose exactly one of -SelfSign or -AlreadySigned.'
|
|
82
89
|
exit 2
|
|
83
90
|
}
|
|
91
|
+
if ($RetainSelfSignedFixturePath -and -not $SelfSign) {
|
|
92
|
+
Write-Error '-RetainSelfSignedFixturePath is valid only with -SelfSign.'
|
|
93
|
+
exit 2
|
|
94
|
+
}
|
|
84
95
|
|
|
85
96
|
$PolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell'
|
|
86
97
|
$WshKey = 'HKLM:\SOFTWARE\Microsoft\Windows Script Host\Settings'
|
|
87
98
|
$Work = Join-Path ([IO.Path]::GetTempPath()) ("tb-allsigned-" + [Guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
88
99
|
$failures = New-Object System.Collections.Generic.List[string]
|
|
89
100
|
$addedThumbprints = New-Object System.Collections.Generic.List[string]
|
|
101
|
+
$retainedRootThumbprint = $null
|
|
90
102
|
|
|
91
103
|
function Say($m) { Write-Host "[allsigned] $m" }
|
|
92
104
|
function Fail($m) { $script:failures.Add($m); Write-Host "[allsigned] FAIL: $m" -ForegroundColor Red }
|
|
@@ -288,6 +300,7 @@ try {
|
|
|
288
300
|
# reason.
|
|
289
301
|
Add-CertToStore -Cert $pub -StoreName 'Root'
|
|
290
302
|
$addedThumbprints.Add('Root|' + $pub.Thumbprint)
|
|
303
|
+
if ($RetainSelfSignedFixturePath) { $retainedRootThumbprint = $pub.Thumbprint }
|
|
291
304
|
foreach ($f in @(Get-ChildItem -LiteralPath $signedDir -File)) {
|
|
292
305
|
$r = Set-AuthenticodeSignature -FilePath $f.FullName -Certificate $cert -HashAlgorithm SHA256
|
|
293
306
|
if ($r.Status -ne 'Valid') { throw "self-signing produced $($r.Status) for $($f.Name)" }
|
|
@@ -589,6 +602,7 @@ try {
|
|
|
589
602
|
elseif (Test-Path $WshKey) { Remove-ItemProperty -Path $WshKey -Name 'UseWINSAFER' -ErrorAction SilentlyContinue }
|
|
590
603
|
foreach ($entry in $addedThumbprints) {
|
|
591
604
|
$parts = $entry.Split('|')
|
|
605
|
+
if ($retainedRootThumbprint -and $parts[0] -eq 'Root' -and $parts[1] -eq $retainedRootThumbprint) { continue }
|
|
592
606
|
Remove-CertFromStore -Thumbprint $parts[1] -StoreName $parts[0]
|
|
593
607
|
}
|
|
594
608
|
} catch {
|
|
@@ -604,5 +618,14 @@ if ($failures.Count -gt 0) {
|
|
|
604
618
|
exit 1
|
|
605
619
|
}
|
|
606
620
|
|
|
621
|
+
if ($RetainSelfSignedFixturePath) {
|
|
622
|
+
if (-not $cert -or -not $cert.HasPrivateKey -or $cert.Thumbprint -ne $retainedRootThumbprint) {
|
|
623
|
+
Write-Error 'the retained self-signed fixture is not the exact proven certificate'
|
|
624
|
+
exit 1
|
|
625
|
+
}
|
|
626
|
+
@([pscustomobject]@{ subject = $cert.Subject; thumbprint = $cert.Thumbprint }) |
|
|
627
|
+
ConvertTo-Json -Depth 3 | Set-Content -LiteralPath $RetainSelfSignedFixturePath -Encoding UTF8
|
|
628
|
+
}
|
|
629
|
+
|
|
607
630
|
Say 'PASSED - unsigned is blocked, untrusted-publisher is blocked, tampered is blocked, and every shipped script is admitted.'
|
|
608
631
|
exit 0
|
|
@@ -51,7 +51,25 @@
|
|
|
51
51
|
[CmdletBinding()]
|
|
52
52
|
param(
|
|
53
53
|
# Arm K needs to create a local account. Off-switch for a host that forbids it.
|
|
54
|
-
[switch]$SkipUnelevatedArm
|
|
54
|
+
[switch]$SkipUnelevatedArm,
|
|
55
|
+
# The production proof owns eight minutes. Seconds are injectable only so the
|
|
56
|
+
# Windows deadline probe can prove a blocked certificate child is killed.
|
|
57
|
+
[ValidateRange(1, 480)]
|
|
58
|
+
[int]$DeadlineSeconds = 480,
|
|
59
|
+
[ValidateRange(0, 60)]
|
|
60
|
+
[int]$CertificateMintDelaySeconds = 0,
|
|
61
|
+
[switch]$CertificateDeadlineProbe,
|
|
62
|
+
# Forces Process.WaitForExit to throw on an unstarted process. CI uses this
|
|
63
|
+
# to prove the wait observer fails closed rather than calling a thrown wait a
|
|
64
|
+
# successful exit and falling into a later unbounded wait.
|
|
65
|
+
[switch]$BoundedWaitFailureProbe,
|
|
66
|
+
# The full CI proof receives the one real certificate already minted, signed,
|
|
67
|
+
# and proven by the preceding AllSigned harness. `powershell.exe` children
|
|
68
|
+
# repeatedly wedged while minting on windows-latest even though the same
|
|
69
|
+
# cmdlet succeeds in the workflow host. Keep the killable child below only for
|
|
70
|
+
# the deliberate deadline mutant; do not use that unreliable process shape
|
|
71
|
+
# as the happy-path fixture factory.
|
|
72
|
+
[string]$CertificateFixturePath
|
|
55
73
|
)
|
|
56
74
|
|
|
57
75
|
$ErrorActionPreference = 'Stop'
|
|
@@ -65,6 +83,7 @@ $Root = Join-Path $env:RUNNER_TEMP 'tb-update-applier'
|
|
|
65
83
|
if (-not $env:RUNNER_TEMP) { $Root = Join-Path $env:TEMP 'tb-update-applier' }
|
|
66
84
|
$Www = Join-Path $Root 'www'
|
|
67
85
|
$Certs = New-Object System.Collections.Generic.List[string]
|
|
86
|
+
$CertSubjects = New-Object System.Collections.Generic.List[string]
|
|
68
87
|
$LocalUser = 'tbupd'
|
|
69
88
|
$Failures = 0
|
|
70
89
|
$StartedAt = Get-Date
|
|
@@ -81,7 +100,7 @@ $StartedAt = Get-Date
|
|
|
81
100
|
time, because the reason that first failure was undiagnosable was that there
|
|
82
101
|
was no output between "origin listening" and the first arm.
|
|
83
102
|
#>
|
|
84
|
-
$
|
|
103
|
+
$script:DeadlineSeconds = $DeadlineSeconds
|
|
85
104
|
|
|
86
105
|
function Elapsed { ((Get-Date) - $script:StartedAt).TotalSeconds }
|
|
87
106
|
function Say([string]$m) { Write-Host ("[update-applier {0,6:N1}s] {1}" -f (Elapsed), $m) }
|
|
@@ -89,7 +108,7 @@ function Say([string]$m) { Write-Host ("[update-applier {0,6:N1}s] {1}" -f (Elap
|
|
|
89
108
|
$script:CurrentPhase = 'startup'
|
|
90
109
|
|
|
91
110
|
function Fail-Deadline([string]$Phase, [string]$When) {
|
|
92
|
-
Write-Host "FAIL - the harness exceeded its $($script:
|
|
111
|
+
Write-Host "FAIL - the harness exceeded its $($script:DeadlineSeconds)-second deadline $When`: $Phase"
|
|
93
112
|
Write-Host ' HARNESS BUDGET failure, not a product failure. The arms above ran;'
|
|
94
113
|
Write-Host ' the ones below never started. Read the per-phase timings to find'
|
|
95
114
|
Write-Host ' what got slow before assuming a defect in apply-update.ps1.'
|
|
@@ -99,12 +118,12 @@ function Fail-Deadline([string]$Phase, [string]$When) {
|
|
|
99
118
|
|
|
100
119
|
function Assert-WithinDeadline([string]$Phase) {
|
|
101
120
|
$script:CurrentPhase = $Phase
|
|
102
|
-
if ((
|
|
121
|
+
if ((Elapsed) -lt $script:DeadlineSeconds) { return }
|
|
103
122
|
Fail-Deadline $Phase 'before starting'
|
|
104
123
|
}
|
|
105
124
|
|
|
106
125
|
function Remaining-DeadlineSeconds {
|
|
107
|
-
[Math]::Max(0,
|
|
126
|
+
[Math]::Max(0, $script:DeadlineSeconds - (Elapsed))
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
<#
|
|
@@ -132,14 +151,25 @@ function Wait-Bounded {
|
|
|
132
151
|
try { Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue } catch {}
|
|
133
152
|
Fail-Deadline $Phase 'before starting'
|
|
134
153
|
}
|
|
154
|
+
$timeoutMilliseconds = [int]([Math]::Min(2147483, $budget) * 1000)
|
|
135
155
|
$exited = $false
|
|
136
|
-
try {
|
|
156
|
+
try {
|
|
157
|
+
$exited = $Process.WaitForExit($timeoutMilliseconds)
|
|
158
|
+
} catch {
|
|
159
|
+
$waitException = $_.Exception
|
|
160
|
+
if ($null -ne $waitException.InnerException) { $waitException = $waitException.InnerException }
|
|
161
|
+
$waitErrorType = $waitException.GetType().FullName
|
|
162
|
+
$waitErrorMessage = $waitException.Message
|
|
163
|
+
try { Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue } catch {}
|
|
164
|
+
try { $Process.WaitForExit(5000) | Out-Null } catch {}
|
|
165
|
+
try { $Process.Close() } catch {}
|
|
166
|
+
Die "$Phase bounded process wait failed ($waitErrorType): $waitErrorMessage"
|
|
167
|
+
}
|
|
137
168
|
if (-not $exited) {
|
|
138
169
|
try { Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue } catch {}
|
|
139
170
|
try { $Process.WaitForExit(5000) | Out-Null } catch {}
|
|
140
171
|
Fail-Deadline $Phase 'while running'
|
|
141
172
|
}
|
|
142
|
-
try { $Process.Refresh() } catch {}
|
|
143
173
|
return $Process
|
|
144
174
|
}
|
|
145
175
|
function Pass([string]$m) { Write-Host "PASS - $m" }
|
|
@@ -199,17 +229,111 @@ function Stop-Origin {
|
|
|
199
229
|
# Certificates
|
|
200
230
|
# ---------------------------------------------------------------------------
|
|
201
231
|
function New-HarnessCert([string]$Subject) {
|
|
202
|
-
|
|
203
|
-
|
|
232
|
+
# New-SelfSignedCertificate blocked for longer than the GitHub step budget on
|
|
233
|
+
# 2026-08-15. A synchronous cmdlet cannot be interrupted by Wait-Bounded, so
|
|
234
|
+
# mint in a disposable child process. The child receives the SAME remaining
|
|
235
|
+
# whole-harness budget as every applier invocation and is forcibly killed on
|
|
236
|
+
# expiry; a phase-specific failure is emitted before the job timeout can turn
|
|
237
|
+
# the result into an unattributable cancellation.
|
|
238
|
+
$phase = "minting code-signing certificate '$Subject'"
|
|
239
|
+
$tag = [guid]::NewGuid().ToString('N')
|
|
240
|
+
$thumbprintPath = Join-Path $Root "cert-$tag.thumbprint"
|
|
241
|
+
$stderrPath = Join-Path $Root "cert-$tag.stderr"
|
|
242
|
+
$subjectLiteral = $Subject.Replace("'", "''")
|
|
243
|
+
$thumbprintLiteral = $thumbprintPath.Replace("'", "''")
|
|
244
|
+
$stderrLiteral = $stderrPath.Replace("'", "''")
|
|
245
|
+
$script:CertSubjects.Add("CN=$Subject")
|
|
246
|
+
$mintSource = @"
|
|
247
|
+
`$ErrorActionPreference = 'Stop'
|
|
248
|
+
`$ProgressPreference = 'SilentlyContinue'
|
|
249
|
+
try {
|
|
250
|
+
if ($CertificateMintDelaySeconds -gt 0) { Start-Sleep -Seconds $CertificateMintDelaySeconds }
|
|
251
|
+
`$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject 'CN=$subjectLiteral' -CertStoreLocation 'Cert:\CurrentUser\My' -NotAfter (Get-Date).AddDays(2)
|
|
252
|
+
[System.IO.File]::WriteAllText('$thumbprintLiteral', `$cert.Thumbprint)
|
|
253
|
+
} catch {
|
|
254
|
+
[System.IO.File]::WriteAllText('$stderrLiteral', (`$_ | Out-String))
|
|
255
|
+
exit 1
|
|
256
|
+
}
|
|
257
|
+
"@
|
|
258
|
+
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($mintSource))
|
|
259
|
+
$powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
260
|
+
$startInfo = [Diagnostics.ProcessStartInfo]::new()
|
|
261
|
+
$startInfo.FileName = $powershell
|
|
262
|
+
$startInfo.Arguments = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded"
|
|
263
|
+
$startInfo.UseShellExecute = $false
|
|
264
|
+
$startInfo.CreateNoWindow = $true
|
|
265
|
+
$p = [Diagnostics.Process]::new()
|
|
266
|
+
$p.StartInfo = $startInfo
|
|
267
|
+
$null = $p.Start()
|
|
268
|
+
$null = Wait-Bounded -Process $p -Phase $phase
|
|
269
|
+
$exitCode = $p.ExitCode
|
|
270
|
+
$p.Close()
|
|
271
|
+
|
|
272
|
+
$childError = ''
|
|
273
|
+
if (Test-Path -LiteralPath $stderrPath) { $childError = (Get-Content -LiteralPath $stderrPath -Raw).Trim() }
|
|
274
|
+
if ($exitCode -ne 0) { Die "$phase failed (exit $exitCode): $childError" }
|
|
275
|
+
if (-not (Test-Path -LiteralPath $thumbprintPath)) { Die "$phase returned no thumbprint" }
|
|
276
|
+
$thumbprint = (Get-Content -LiteralPath $thumbprintPath -Raw).Trim().ToUpperInvariant()
|
|
277
|
+
if ($thumbprint -notmatch '^[0-9A-F]{40,128}$') { Die "$phase returned malformed thumbprint '$thumbprint'" }
|
|
278
|
+
$script:Certs.Add($thumbprint)
|
|
279
|
+
$cert = Get-Item -LiteralPath "Cert:\CurrentUser\My\$thumbprint" -ErrorAction Stop
|
|
280
|
+
if ((-not $cert.HasPrivateKey) -or ($cert.Subject -ne "CN=$Subject")) {
|
|
281
|
+
Die "$phase returned a certificate without the expected subject and private key"
|
|
282
|
+
}
|
|
283
|
+
Remove-Item -LiteralPath $thumbprintPath, $stderrPath -Force -ErrorAction SilentlyContinue
|
|
204
284
|
# Into Root, or the chain does not build and Status is UnknownError - which
|
|
205
285
|
# would make arm E fire for the wrong reason entirely.
|
|
206
286
|
$pub = New-Object Security.Cryptography.X509Certificates.X509Certificate2(, $cert.RawData)
|
|
207
287
|
$store = New-Object Security.Cryptography.X509Certificates.X509Store('Root', 'CurrentUser')
|
|
208
288
|
$store.Open('ReadWrite'); $store.Add($pub); $store.Close()
|
|
209
|
-
$Certs.Add($cert.Thumbprint)
|
|
210
289
|
return $cert
|
|
211
290
|
}
|
|
212
291
|
|
|
292
|
+
function Read-HarnessCertificateFixtures([string]$Path) {
|
|
293
|
+
if (-not $Path) { Die 'CertificateFixturePath is required for the full harness' }
|
|
294
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
295
|
+
Die "CertificateFixturePath does not exist: $Path"
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
$rows = @(Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json)
|
|
299
|
+
} catch {
|
|
300
|
+
Die "CertificateFixturePath is not valid JSON: $($_.Exception.Message)"
|
|
301
|
+
}
|
|
302
|
+
if ($rows.Count -ne 1) { Die "CertificateFixturePath must contain exactly one certificate (got $($rows.Count))" }
|
|
303
|
+
|
|
304
|
+
$expectedSubjects = @('CN=Trillboards CI AllSigned Harness')
|
|
305
|
+
$bySubject = @{}
|
|
306
|
+
foreach ($row in $rows) {
|
|
307
|
+
$keys = @($row.PSObject.Properties.Name | Sort-Object)
|
|
308
|
+
if (($keys.Count -ne 2) -or (($keys -join ',') -ne 'subject,thumbprint')) {
|
|
309
|
+
Die 'CertificateFixturePath rows must contain exactly subject and thumbprint'
|
|
310
|
+
}
|
|
311
|
+
$subject = "$($row.subject)"
|
|
312
|
+
$thumbprint = "$($row.thumbprint)".Trim().ToUpperInvariant()
|
|
313
|
+
if ($expectedSubjects -notcontains $subject) { Die "CertificateFixturePath contains unexpected subject '$subject'" }
|
|
314
|
+
if ($bySubject.ContainsKey($subject)) { Die "CertificateFixturePath repeats subject '$subject'" }
|
|
315
|
+
if ($thumbprint -notmatch '^[0-9A-F]{40,128}$') { Die "CertificateFixturePath contains malformed thumbprint '$thumbprint'" }
|
|
316
|
+
|
|
317
|
+
$cert = Get-Item -LiteralPath "Cert:\CurrentUser\My\$thumbprint" -ErrorAction SilentlyContinue
|
|
318
|
+
if ((-not $cert) -or (-not $cert.HasPrivateKey) -or ($cert.Subject -ne $subject)) {
|
|
319
|
+
Die "CertificateFixturePath certificate '$subject' is absent or lacks its expected private key"
|
|
320
|
+
}
|
|
321
|
+
$root = Get-Item -LiteralPath "Cert:\LocalMachine\Root\$thumbprint" -ErrorAction SilentlyContinue
|
|
322
|
+
if ((-not $root) -or ($root.Subject -ne $subject) -or ($root.Thumbprint -ne $cert.Thumbprint)) {
|
|
323
|
+
Die "CertificateFixturePath certificate '$subject' is not bound to the matching trusted root"
|
|
324
|
+
}
|
|
325
|
+
$script:Certs.Add($thumbprint)
|
|
326
|
+
$script:CertSubjects.Add($subject)
|
|
327
|
+
$bySubject[$subject] = $cert
|
|
328
|
+
}
|
|
329
|
+
foreach ($subject in $expectedSubjects) {
|
|
330
|
+
if (-not $bySubject.ContainsKey($subject)) { Die "CertificateFixturePath is missing subject '$subject'" }
|
|
331
|
+
}
|
|
332
|
+
return [pscustomobject]@{
|
|
333
|
+
Good = $bySubject['CN=Trillboards CI AllSigned Harness']
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
213
337
|
function Remove-HarnessCerts {
|
|
214
338
|
foreach ($tp in $Certs) {
|
|
215
339
|
foreach ($name in @('My', 'Root')) {
|
|
@@ -221,6 +345,18 @@ function Remove-HarnessCerts {
|
|
|
221
345
|
} catch {}
|
|
222
346
|
}
|
|
223
347
|
}
|
|
348
|
+
# If the child was killed after committing the store write but before writing
|
|
349
|
+
# its thumbprint marker, the parent never saw the thumbprint. Exact harness
|
|
350
|
+
# subjects are therefore a second cleanup key; they are intentionally unique
|
|
351
|
+
# to this ephemeral CI proof.
|
|
352
|
+
foreach ($name in @('My', 'Root')) {
|
|
353
|
+
try {
|
|
354
|
+
$store = New-Object Security.Cryptography.X509Certificates.X509Store($name, 'CurrentUser')
|
|
355
|
+
$store.Open('ReadWrite')
|
|
356
|
+
foreach ($c in @($store.Certificates | Where-Object { $script:CertSubjects.Contains($_.Subject) })) { $store.Remove($c) }
|
|
357
|
+
$store.Close()
|
|
358
|
+
} catch {}
|
|
359
|
+
}
|
|
224
360
|
}
|
|
225
361
|
|
|
226
362
|
function Sign-File([string]$Path, $Cert) {
|
|
@@ -361,11 +497,49 @@ function New-Catalog([string]$Path, [string]$Version, [string]$Sha, [long]$Size)
|
|
|
361
497
|
plus whether staged.json exists, which together are the only observable the
|
|
362
498
|
connector ever acts on.
|
|
363
499
|
#>
|
|
364
|
-
function Invoke-Applier([string]$InstallDir, [
|
|
500
|
+
function Invoke-Applier([string]$InstallDir, [System.Collections.IDictionary]$ApplierParameters) {
|
|
365
501
|
$ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
366
|
-
|
|
367
|
-
|
|
502
|
+
# Start-Process flattens -ArgumentList into one command-line string without
|
|
503
|
+
# preserving element boundaries. A value such as the real signer
|
|
504
|
+
# "Trillboards CI AllSigned Harness" therefore arrives at apply-update.ps1
|
|
505
|
+
# as four positional arguments. An array splat is also positional, so it
|
|
506
|
+
# cannot reconstruct names such as -ExpectedVersion after deserialization.
|
|
507
|
+
# Carry a named parameter map as JSON inside an encoded child program instead:
|
|
508
|
+
# the only native-process argument is base64, and the child rebuilds a
|
|
509
|
+
# hashtable that PowerShell binds by parameter name without reparsing values.
|
|
510
|
+
$namedParameters = [ordered]@{ InstallDir = [string]$InstallDir }
|
|
511
|
+
foreach ($name in $ApplierParameters.Keys) {
|
|
512
|
+
if ([string]$name -eq 'InstallDir') { Die 'Invoke-Applier parameters must not override InstallDir' }
|
|
513
|
+
$namedParameters[[string]$name] = $ApplierParameters[$name]
|
|
514
|
+
}
|
|
515
|
+
$invocationJson = [ordered]@{
|
|
516
|
+
applier = [string]$Applier
|
|
517
|
+
parameters = $namedParameters
|
|
518
|
+
} | ConvertTo-Json -Compress
|
|
519
|
+
$invocationBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($invocationJson))
|
|
520
|
+
$childSource = @"
|
|
521
|
+
`$ErrorActionPreference = 'Stop'
|
|
522
|
+
`$decodedInvocation = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('$invocationBase64'))
|
|
523
|
+
`$invocation = `$decodedInvocation | ConvertFrom-Json
|
|
524
|
+
`$childApplier = [string]`$invocation.applier
|
|
525
|
+
`$childParameters = @{}
|
|
526
|
+
foreach (`$property in @(`$invocation.parameters.PSObject.Properties)) {
|
|
527
|
+
`$childParameters[`$property.Name] = `$property.Value
|
|
528
|
+
}
|
|
529
|
+
& `$childApplier @childParameters
|
|
530
|
+
exit `$LASTEXITCODE
|
|
531
|
+
"@
|
|
532
|
+
$encodedChild = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($childSource))
|
|
533
|
+
$startInfo = [Diagnostics.ProcessStartInfo]::new()
|
|
534
|
+
$startInfo.FileName = $ps
|
|
535
|
+
$startInfo.Arguments = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedChild"
|
|
536
|
+
$startInfo.UseShellExecute = $false
|
|
537
|
+
$p = [Diagnostics.Process]::new()
|
|
538
|
+
$p.StartInfo = $startInfo
|
|
539
|
+
$null = $p.Start()
|
|
368
540
|
$null = Wait-Bounded -Process $p
|
|
541
|
+
$exitCode = $p.ExitCode
|
|
542
|
+
$p.Close()
|
|
369
543
|
$updateDir = Join-Path $InstallDir '.auto-update'
|
|
370
544
|
$outcome = $null
|
|
371
545
|
$outcomePath = Join-Path $updateDir 'outcome.json'
|
|
@@ -382,7 +556,7 @@ function Invoke-Applier([string]$InstallDir, [string[]]$Args) {
|
|
|
382
556
|
if ($null -ne $outcome.detail) { $detail = [string]$outcome.detail }
|
|
383
557
|
}
|
|
384
558
|
return [pscustomobject]@{
|
|
385
|
-
ExitCode = $
|
|
559
|
+
ExitCode = $exitCode
|
|
386
560
|
Outcome = $outcome
|
|
387
561
|
Result = $result
|
|
388
562
|
Detail = $detail
|
|
@@ -442,15 +616,31 @@ try {
|
|
|
442
616
|
if (-not (Test-Path -LiteralPath $Applier)) { Die "apply-update.ps1 not found at $Applier" }
|
|
443
617
|
if (Test-Path -LiteralPath $Root) { Remove-Item -LiteralPath $Root -Recurse -Force }
|
|
444
618
|
New-Item -ItemType Directory -Force -Path $Root, $Www | Out-Null
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
$
|
|
449
|
-
|
|
450
|
-
|
|
619
|
+
if (($CertificateMintDelaySeconds -gt 0) -and (-not $CertificateDeadlineProbe)) {
|
|
620
|
+
Die 'CertificateMintDelaySeconds is a deadline-probe input, not a full-harness option'
|
|
621
|
+
}
|
|
622
|
+
if ($BoundedWaitFailureProbe) {
|
|
623
|
+
$unstarted = [Diagnostics.Process]::new()
|
|
624
|
+
$null = Wait-Bounded -Process $unstarted -Phase 'bounded wait observer failure probe'
|
|
625
|
+
Die 'bounded wait observer failure probe was reclassified as a process exit'
|
|
626
|
+
}
|
|
627
|
+
if ($CertificateDeadlineProbe) {
|
|
628
|
+
if ($CertificateMintDelaySeconds -le $DeadlineSeconds) {
|
|
629
|
+
Die 'certificate deadline probe requires a mint delay longer than its deadline'
|
|
630
|
+
}
|
|
631
|
+
Say 'deadline probe: starting a deliberately blocked certificate mint'
|
|
632
|
+
$null = New-HarnessCert 'Trillboards CI Deadline Probe'
|
|
633
|
+
Die 'certificate deadline probe was not stopped by the harness deadline'
|
|
634
|
+
}
|
|
635
|
+
Say 'loading the real certificate retained from the AllSigned proof'
|
|
636
|
+
$certificateFixtures = Read-HarnessCertificateFixtures $CertificateFixturePath
|
|
637
|
+
$goodCert = $certificateFixtures.Good
|
|
638
|
+
$signer = 'Trillboards CI AllSigned Harness'
|
|
451
639
|
$version = '9.9.9'
|
|
452
640
|
Say 'certificates ready'
|
|
453
641
|
|
|
642
|
+
Start-Origin
|
|
643
|
+
|
|
454
644
|
$payloadTree = Join-Path $Root 'payload-src'
|
|
455
645
|
New-PayloadTree -Dest $payloadTree -SignCert $goodCert
|
|
456
646
|
$good = Publish-Payload $payloadTree 'payload.zip'
|
|
@@ -468,8 +658,10 @@ try {
|
|
|
468
658
|
Assert-WithinDeadline 'arm A: happy path (the shared control)'
|
|
469
659
|
$installA = Join-Path $Root 'install-a'
|
|
470
660
|
New-InstallTree $installA
|
|
471
|
-
$a = Invoke-Applier $installA @
|
|
472
|
-
|
|
661
|
+
$a = Invoke-Applier $installA ([ordered]@{
|
|
662
|
+
Stage = $true; UpdateUrl = $urlZip; CatalogUrl = "$BaseUrl/catalog-good.ps1"
|
|
663
|
+
ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
|
|
664
|
+
})
|
|
473
665
|
if (-not $a.Staged) {
|
|
474
666
|
Die "arm A did not stage (exit $($a.ExitCode), result '$($a.Result)', detail '$($a.Detail)') - every refusal below would be VACUOUS"
|
|
475
667
|
}
|
|
@@ -487,8 +679,10 @@ try {
|
|
|
487
679
|
[System.IO.File]::WriteAllBytes($tampered, $bytes)
|
|
488
680
|
$installB = Join-Path $Root 'install-b'
|
|
489
681
|
New-InstallTree $installB
|
|
490
|
-
$b = Invoke-Applier $installB @
|
|
491
|
-
|
|
682
|
+
$b = Invoke-Applier $installB ([ordered]@{
|
|
683
|
+
Stage = $true; UpdateUrl = "$BaseUrl/payload-tampered.zip"; CatalogUrl = "$BaseUrl/catalog-good.ps1"
|
|
684
|
+
ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
|
|
685
|
+
})
|
|
492
686
|
if ($b.Staged) { Bad 'arm B staged a payload whose SHA-256 did not match the signed catalog' }
|
|
493
687
|
elseif ($b.Result -ne 'rejected') { Bad "arm B recorded '$($b.Result)', expected 'rejected'" }
|
|
494
688
|
elseif ($b.Detail -notmatch 'SHA-256 mismatch') { Bad "arm B rejected for the wrong reason: $($b.Detail)" }
|
|
@@ -508,8 +702,10 @@ try {
|
|
|
508
702
|
[System.IO.File]::WriteAllBytes($short, $half)
|
|
509
703
|
$installC = Join-Path $Root 'install-c'
|
|
510
704
|
New-InstallTree $installC
|
|
511
|
-
$c = Invoke-Applier $installC @
|
|
512
|
-
|
|
705
|
+
$c = Invoke-Applier $installC ([ordered]@{
|
|
706
|
+
Stage = $true; UpdateUrl = "$BaseUrl/payload-short.zip"; CatalogUrl = "$BaseUrl/catalog-good.ps1"
|
|
707
|
+
ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
|
|
708
|
+
})
|
|
513
709
|
if ($c.Staged) { Bad 'arm C staged a truncated payload' }
|
|
514
710
|
elseif ($c.Detail -notmatch 'size mismatch') { Bad "arm C rejected for the wrong reason: $($c.Detail)" }
|
|
515
711
|
elseif ($c.Archive) { Bad 'arm C left the truncated archive on disk' }
|
|
@@ -525,8 +721,10 @@ try {
|
|
|
525
721
|
}
|
|
526
722
|
$installD = Join-Path $Root 'install-d'
|
|
527
723
|
New-InstallTree $installD
|
|
528
|
-
$d = Invoke-Applier $installD @
|
|
529
|
-
|
|
724
|
+
$d = Invoke-Applier $installD ([ordered]@{
|
|
725
|
+
Stage = $true; UpdateUrl = $urlZip; CatalogUrl = "$BaseUrl/catalog-unsigned.ps1"
|
|
726
|
+
ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
|
|
727
|
+
})
|
|
530
728
|
if ($d.Staged) { Bad 'arm D staged against an unsigned catalog' }
|
|
531
729
|
elseif ($d.Detail -notmatch 'not Valid') { Bad "arm D rejected for the wrong reason: $($d.Detail)" }
|
|
532
730
|
else { Pass 'an unsigned catalog is rejected' }
|
|
@@ -536,15 +734,17 @@ try {
|
|
|
536
734
|
Assert-WithinDeadline 'arm E: the catalog is VALIDLY signed, by somebody else'
|
|
537
735
|
$catalogOther = Join-Path $Www 'catalog-other.ps1'
|
|
538
736
|
New-Catalog $catalogOther $version $good.Sha $good.Bytes
|
|
539
|
-
Sign-File $catalogOther $
|
|
737
|
+
Sign-File $catalogOther $goodCert
|
|
540
738
|
$otherStatus = (Get-AuthenticodeSignature -LiteralPath $catalogOther).Status
|
|
541
739
|
if ($otherStatus -ne 'Valid') {
|
|
542
740
|
Die "arm E is VACUOUS: the other publisher's signature is '$otherStatus', not Valid - a status check alone would reject it and prove nothing about the subject check"
|
|
543
741
|
}
|
|
544
742
|
$installE = Join-Path $Root 'install-e'
|
|
545
743
|
New-InstallTree $installE
|
|
546
|
-
$e = Invoke-Applier $installE @
|
|
547
|
-
|
|
744
|
+
$e = Invoke-Applier $installE ([ordered]@{
|
|
745
|
+
Stage = $true; UpdateUrl = $urlZip; CatalogUrl = "$BaseUrl/catalog-other.ps1"
|
|
746
|
+
ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = 'Definitely Not Trillboards'
|
|
747
|
+
})
|
|
548
748
|
if ($e.Staged) { Bad 'arm E staged a catalog signed by an unexpected publisher' }
|
|
549
749
|
elseif ($e.Detail -notmatch 'unexpected publisher') { Bad "arm E rejected for the wrong reason: $($e.Detail)" }
|
|
550
750
|
else { Pass 'a VALID signature from the wrong publisher is rejected on the subject' }
|
|
@@ -559,8 +759,10 @@ try {
|
|
|
559
759
|
if ($editedStatus -eq 'Valid') { Die 'arm F is VACUOUS: the edited catalog still reports Valid' }
|
|
560
760
|
$installF = Join-Path $Root 'install-f'
|
|
561
761
|
New-InstallTree $installF
|
|
562
|
-
$f = Invoke-Applier $installF @
|
|
563
|
-
|
|
762
|
+
$f = Invoke-Applier $installF ([ordered]@{
|
|
763
|
+
Stage = $true; UpdateUrl = $urlZip; CatalogUrl = "$BaseUrl/catalog-edited.ps1"
|
|
764
|
+
ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
|
|
765
|
+
})
|
|
564
766
|
if ($f.Staged) { Bad 'arm F staged a catalog that was edited after signing' }
|
|
565
767
|
else { Pass "a catalog edited after signing is rejected ($editedStatus)" }
|
|
566
768
|
|
|
@@ -572,8 +774,10 @@ try {
|
|
|
572
774
|
Sign-File $huge $goodCert
|
|
573
775
|
$installG = Join-Path $Root 'install-g'
|
|
574
776
|
New-InstallTree $installG
|
|
575
|
-
$g = Invoke-Applier $installG @
|
|
576
|
-
|
|
777
|
+
$g = Invoke-Applier $installG ([ordered]@{
|
|
778
|
+
Stage = $true; UpdateUrl = $urlZip; CatalogUrl = "$BaseUrl/catalog-huge.ps1"
|
|
779
|
+
ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
|
|
780
|
+
})
|
|
577
781
|
if ($g.Staged) { Bad 'arm G staged a payload that cannot fit' }
|
|
578
782
|
elseif ($g.Detail -notmatch 'free disk space') { Bad "arm G rejected for the wrong reason: $($g.Detail)" }
|
|
579
783
|
elseif ($g.Archive) { Bad 'arm G downloaded before checking that the payload would fit' }
|
|
@@ -590,8 +794,10 @@ try {
|
|
|
590
794
|
Sign-File $catalogRogue $goodCert
|
|
591
795
|
$installH = Join-Path $Root 'install-h'
|
|
592
796
|
New-InstallTree $installH
|
|
593
|
-
$h = Invoke-Applier $installH @
|
|
594
|
-
|
|
797
|
+
$h = Invoke-Applier $installH ([ordered]@{
|
|
798
|
+
Stage = $true; UpdateUrl = "$BaseUrl/payload-rogue.zip"; CatalogUrl = "$BaseUrl/catalog-rogue.ps1"
|
|
799
|
+
ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
|
|
800
|
+
})
|
|
595
801
|
if ($h.Staged) { Bad 'arm H staged a payload containing an unsigned script' }
|
|
596
802
|
elseif ($h.Detail -notmatch 'unsigned or invalid') { Bad "arm H rejected for the wrong reason: $($h.Detail)" }
|
|
597
803
|
else { Pass 'a payload carrying an unsigned script is rejected' }
|
|
@@ -613,9 +819,11 @@ try {
|
|
|
613
819
|
New-Catalog $catDown $version $down.Sha $down.Bytes
|
|
614
820
|
Sign-File $catDown $goodCert
|
|
615
821
|
|
|
616
|
-
$staged = Invoke-Applier $installJ @
|
|
617
|
-
|
|
618
|
-
|
|
822
|
+
$staged = Invoke-Applier $installJ ([ordered]@{
|
|
823
|
+
Stage = $true; UpdateUrl = "$BaseUrl/payload-down-$($arm.Name).zip"
|
|
824
|
+
CatalogUrl = "$BaseUrl/catalog-down-$($arm.Name).ps1"; ExpectedVersion = $version
|
|
825
|
+
CurrentVersion = '9.9.8'; ExpectedSigner = $signer
|
|
826
|
+
})
|
|
619
827
|
if (-not $staged.Staged) { Die "arm $($arm.Name) could not stage: $($staged.Detail)" }
|
|
620
828
|
|
|
621
829
|
Register-HeadlessTask $installJ
|
|
@@ -633,9 +841,9 @@ try {
|
|
|
633
841
|
Die "arm $($arm.Name) is VACUOUS: node.exe was not locked while the connector was running, so a successful swap proves nothing"
|
|
634
842
|
}
|
|
635
843
|
|
|
636
|
-
$
|
|
637
|
-
if ($arm.Skip) { $
|
|
638
|
-
$j = Invoke-Applier $installJ $
|
|
844
|
+
$commitParameters = [ordered]@{ Commit = $true; ExpectedVersion = $version; CurrentVersion = '9.9.8' }
|
|
845
|
+
if ($arm.Skip) { $commitParameters['SkipTaskShapeRestore'] = $true }
|
|
846
|
+
$j = Invoke-Applier $installJ $commitParameters
|
|
639
847
|
try { Stop-Process -Id $running.Id -Force -ErrorAction SilentlyContinue } catch {}
|
|
640
848
|
|
|
641
849
|
$after = Get-Shape $TaskName
|
|
@@ -670,8 +878,10 @@ try {
|
|
|
670
878
|
$catK = Join-Path $Www 'catalog-k.ps1'
|
|
671
879
|
New-Catalog $catK $version $down.Sha $down.Bytes
|
|
672
880
|
Sign-File $catK $goodCert
|
|
673
|
-
$stagedK = Invoke-Applier $installK @
|
|
674
|
-
|
|
881
|
+
$stagedK = Invoke-Applier $installK ([ordered]@{
|
|
882
|
+
Stage = $true; UpdateUrl = "$BaseUrl/payload-k.zip"; CatalogUrl = "$BaseUrl/catalog-k.ps1"
|
|
883
|
+
ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
|
|
884
|
+
})
|
|
675
885
|
if (-not $stagedK.Staged) { Die "arm K could not stage: $($stagedK.Detail)" }
|
|
676
886
|
Register-HeadlessTask $installK
|
|
677
887
|
$beforeK = Get-Shape $TaskName
|
package/dist/EdgeAgent.js
CHANGED
|
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
|
|
|
44
44
|
const CommandRouter_1 = require("./CommandRouter");
|
|
45
45
|
const config_1 = require("./config");
|
|
46
46
|
const dataDir_1 = require("./dataDir");
|
|
47
|
-
const SDK_VERSION = '0.2.
|
|
47
|
+
const SDK_VERSION = '0.2.36';
|
|
48
48
|
class EdgeAgent extends events_1.EventEmitter {
|
|
49
49
|
constructor(config, deps) {
|
|
50
50
|
super();
|
package/dist/diagnose.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface DiagnosticReport {
|
|
|
31
31
|
skipped: number;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
declare const SDK_VERSION = "0.2.
|
|
34
|
+
declare const SDK_VERSION = "0.2.36";
|
|
35
35
|
declare function checkNodeVersion(): CheckResult;
|
|
36
36
|
declare function checkPlatform(): CheckResult;
|
|
37
37
|
declare function checkMemory(): CheckResult;
|
package/dist/diagnose.js
CHANGED
|
@@ -63,7 +63,7 @@ const localNetworkAccess_1 = require("./localNetworkAccess");
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
// SDK version — mirrors package.json; kept in sync by CI
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
|
-
const SDK_VERSION = '0.2.
|
|
66
|
+
const SDK_VERSION = '0.2.36';
|
|
67
67
|
exports._SDK_VERSION = SDK_VERSION;
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
69
69
|
// ANSI helpers (no external deps)
|