From de8fcd7a186c4b0b1f0e0ada157f2d24ec81cbdc Mon Sep 17 00:00:00 2001 From: kurone-kito Date: Fri, 14 Aug 2026 03:31:48 +0900 Subject: [PATCH 1/2] fix(pwsh): replace utf8NoBOM Set-Content in 3 unexecuted .tmpl scripts Set-Content -Encoding utf8NoBOM rejects on PowerShell 5.1 (PS6+-only encoding literal). PR #212 fixed every call site exercised by a test; these four sites across three chezmoi templates were left out since their bodies are only text-diffed, never executed, by the current suite -- but chezmoi apply under real PS5.1 still hits them. Replaces each with [System.IO.File]::WriteAllText(..., [System.Text.UTF8Encoding]::new($false)), the pattern already used in run_once_before_10-import-gpg-keys.ps1.tmpl. Verified byte-for-byte equivalence against the prior Set-Content behavior empirically (pwsh 7.6.4), including platform-newline restoration for the one site (generate-git-profiles.ps1.tmpl) that does not use -NoNewline -- correcting the issue's own premise that all three sites use -NoNewline; only the other two actually do. Adds a new deploy-ssh-keys fixture + Pester suite (synthetic key material only) covering the two SSH-key-writing sites, which previously had zero test coverage -- run_once_before_20-deploy-ssh-keys writes SSH private and public key material and needed careful review rather than a mechanical pass alone. Fixes #214 Co-Authored-By: Claude Sonnet 5 --- ...un_once_before_20-deploy-ssh-keys.ps1.tmpl | 18 ++- ...ge_after_generate-authorized-keys.ps1.tmpl | 7 +- ...hange_after_generate-git-profiles.ps1.tmpl | 10 +- tests/powershell/deploy-ssh-keys.Tests.ps1 | 129 ++++++++++++++++++ tests/powershell/fixtures/deploy-ssh-keys.ps1 | 78 +++++++++++ tests/powershell/signing-resolve.Tests.ps1 | 7 +- 6 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 tests/powershell/deploy-ssh-keys.Tests.ps1 create mode 100644 tests/powershell/fixtures/deploy-ssh-keys.ps1 diff --git a/home/run_once_before_20-deploy-ssh-keys.ps1.tmpl b/home/run_once_before_20-deploy-ssh-keys.ps1.tmpl index 5b2075ed..52826c46 100644 --- a/home/run_once_before_20-deploy-ssh-keys.ps1.tmpl +++ b/home/run_once_before_20-deploy-ssh-keys.ps1.tmpl @@ -31,9 +31,14 @@ $privateKey = Join-Path $sshDir '{{ $sshKey.filename }}' $publicKey = Join-Path $sshDir '{{ $sshKey.filename }}.pub' if (-not (Test-Path $privateKey)) { - @' + $privateKeyContent = @' {{ template "get-ssh-key" dict "item" $sshKey.item "type" "private" "ctx" $ }} -'@ | Set-Content -Path $privateKey -Encoding utf8NoBOM -NoNewline +'@ + # PS5.1 rejects the utf8NoBOM encoding literal (PS6+ only); write the + # BOM-less UTF-8 bytes directly instead. No trailing newline is added, + # matching the prior -NoNewline call. + [System.IO.File]::WriteAllText($privateKey, $privateKeyContent, + [System.Text.UTF8Encoding]::new($false)) # Restrict to current user only (icacls works without elevation) icacls $privateKey /inheritance:r /grant:r "${env:USERNAME}:(F)" 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { @@ -46,9 +51,14 @@ if (-not (Test-Path $privateKey)) { } if (-not (Test-Path $publicKey)) { - @' + $publicKeyContent = @' {{ template "get-ssh-key" dict "item" $sshKey.item "type" "public" "ctx" $ }} -'@ | Set-Content -Path $publicKey -Encoding utf8NoBOM -NoNewline +'@ + # PS5.1 rejects the utf8NoBOM encoding literal (PS6+ only); write the + # BOM-less UTF-8 bytes directly instead. No trailing newline is added, + # matching the prior -NoNewline call. + [System.IO.File]::WriteAllText($publicKey, $publicKeyContent, + [System.Text.UTF8Encoding]::new($false)) Write-Host ' Public key written.' Record-State -Category 'sshKey' -Name '{{ $key }}.pub' -Path $publicKey } else { diff --git a/home/run_onchange_after_generate-authorized-keys.ps1.tmpl b/home/run_onchange_after_generate-authorized-keys.ps1.tmpl index fa2ecdc6..09f810e0 100644 --- a/home/run_onchange_after_generate-authorized-keys.ps1.tmpl +++ b/home/run_onchange_after_generate-authorized-keys.ps1.tmpl @@ -88,7 +88,12 @@ if ($hasValidBlock) { $outLines += $endMarker } -($outLines -join "`n") + "`n" | Set-Content -Path $authorized -Encoding utf8NoBOM -NoNewline +# PS5.1 rejects the utf8NoBOM encoding literal (PS6+ only); write the +# BOM-less UTF-8 bytes directly instead. The joined content already +# carries its own trailing newline, matching the prior -NoNewline call. +$authorizedContent = ($outLines -join "`n") + "`n" +[System.IO.File]::WriteAllText($authorized, $authorizedContent, + [System.Text.UTF8Encoding]::new($false)) # Keep the file private, but still readable by the LocalSystem sshd # service when Windows OpenSSH is configured to use per-user keys. diff --git a/home/run_onchange_after_generate-git-profiles.ps1.tmpl b/home/run_onchange_after_generate-git-profiles.ps1.tmpl index ab52ac90..9cef8428 100644 --- a/home/run_onchange_after_generate-git-profiles.ps1.tmpl +++ b/home/run_onchange_after_generate-git-profiles.ps1.tmpl @@ -24,7 +24,7 @@ New-Item -ItemType Directory -Path $profilesDir -Force | Out-Null {{- $sshAbsPath := "" -}} {{- if $sshFile -}}{{ $sshAbsPath = printf "%s/.ssh/%s.pub" $homeDir $sshFile -}}{{- end }} $profilePath = Join-Path $profilesDir '{{ $key }}' -@' +$profileContent = @' {{- if eq $format "ssh" }} [gpg] format = ssh @@ -53,7 +53,13 @@ $profilePath = Join-Path $profilesDir '{{ $key }}' tag-ssh = "!f() { git -c 'gpg.format=ssh' -c 'user.signingkey={{ $sshAbsPath }}' -c 'tag.gpgsign=true' tag \"$@\"; }; f" rebase-ssh = "!f() { git -c 'gpg.format=ssh' -c 'user.signingkey={{ $sshAbsPath }}' -c 'commit.gpgsign=true' rebase \"$@\"; }; f" {{- end }} -'@ | Set-Content -Path $profilePath -Encoding utf8NoBOM +'@ +# PS5.1 rejects the utf8NoBOM encoding literal (PS6+ only); write the +# BOM-less UTF-8 bytes directly instead. Set-Content's default (no +# -NoNewline) behavior appends one platform newline after the content, +# so replicate that explicitly to preserve prior byte-for-byte output. +[System.IO.File]::WriteAllText($profilePath, $profileContent + [System.Environment]::NewLine, + [System.Text.UTF8Encoding]::new($false)) {{- end }} $validProfiles = @( diff --git a/tests/powershell/deploy-ssh-keys.Tests.ps1 b/tests/powershell/deploy-ssh-keys.Tests.ps1 new file mode 100644 index 00000000..37ec9207 --- /dev/null +++ b/tests/powershell/deploy-ssh-keys.Tests.ps1 @@ -0,0 +1,129 @@ +# Tests for the PowerShell SSH key deployment script. +# Exercises: BOM-less UTF-8 encoding (PS5.1-compatible), exact byte +# preservation (no added trailing newline), skip-if-exists behavior, +# and the Windows ACL restriction applied to private keys only. + +BeforeAll { + $script:Fixture = Join-Path $PSScriptRoot 'fixtures/deploy-ssh-keys.ps1' +} + +Describe 'deploy-ssh-keys' { + + BeforeEach { + $script:OriginalHome = $HOME + $script:OriginalUserName = $env:USERNAME + $script:HomeDir = 'TestDrive:\home' + if (Test-Path $script:HomeDir) { + Remove-Item $script:HomeDir -Recurse -Force + } + New-Item -ItemType Directory -Path $script:HomeDir -Force | Out-Null + Set-Variable -Name HOME -Value $script:HomeDir -Scope Global -Force + $env:USERNAME = 'sample-user' + $global:DotfilesTestIcaclsCalls = @() + + function global:icacls { + $global:DotfilesTestIcaclsCalls += , @($args) + $global:LASTEXITCODE = 0 + } + + $script:SshDirReal = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( + (Join-Path $script:HomeDir '.ssh')) + } + + AfterEach { + Set-Variable -Name HOME -Value $script:OriginalHome -Scope Global -Force + $env:USERNAME = $script:OriginalUserName + Remove-Item Function:\icacls -ErrorAction SilentlyContinue + } + + It 'writes both key files with no BOM' { + & $script:Fixture | Out-Null + + $privateBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_secondary')) + $publicBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_secondary.pub')) + + # A UTF-8 BOM is the 3-byte sequence EF BB BF; neither file should + # start with it. + ($privateBytes.Length -ge 3 -and $privateBytes[0] -eq 0xEF -and + $privateBytes[1] -eq 0xBB -and $privateBytes[2] -eq 0xBF) | Should -BeFalse + ($publicBytes.Length -ge 3 -and $publicBytes[0] -eq 0xEF -and + $publicBytes[1] -eq 0xBB -and $publicBytes[2] -eq 0xBF) | Should -BeFalse + } + + It 'writes exact byte-for-byte content with no added trailing newline' { + & $script:Fixture | Out-Null + + $expectedPrivate = 'TEST-ONLY-PRIVATE-KEY-CONTENT-secondary' + $expectedPublic = 'ssh-ed25519 AAAASECONDARY secondary@test' + $expectedPrivateBytes = [System.Text.UTF8Encoding]::new($false).GetBytes($expectedPrivate) + $expectedPublicBytes = [System.Text.UTF8Encoding]::new($false).GetBytes($expectedPublic) + + $actualPrivateBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_secondary')) + $actualPublicBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_secondary.pub')) + + [System.Linq.Enumerable]::SequenceEqual($actualPrivateBytes, $expectedPrivateBytes) | Should -BeTrue + [System.Linq.Enumerable]::SequenceEqual($actualPublicBytes, $expectedPublicBytes) | Should -BeTrue + } + + It 'preserves embedded newlines in multi-line key content exactly' { + & $script:Fixture | Out-Null + + $expected = "TEST-ONLY-PRIVATE-KEY-CONTENT-primary`nline two" + $expectedBytes = [System.Text.UTF8Encoding]::new($false).GetBytes($expected) + $actualBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_primary')) + + [System.Linq.Enumerable]::SequenceEqual($actualBytes, $expectedBytes) | Should -BeTrue + } + + It 'writes both key pairs for multiple configured keys' { + & $script:Fixture | Out-Null + + Join-Path $script:SshDirReal 'id_primary' | Should -Exist + Join-Path $script:SshDirReal 'id_primary.pub' | Should -Exist + Join-Path $script:SshDirReal 'id_secondary' | Should -Exist + Join-Path $script:SshDirReal 'id_secondary.pub' | Should -Exist + } + + It 'restricts private key permissions to the current user via icacls' { + & $script:Fixture | Out-Null + + # @(...) forces array semantics: Where-Object unwraps a single + # matching element when that element is itself an array, which + # would otherwise make .Count report the inner args count instead + # of the number of matching icacls calls. + $privateCalls = @($global:DotfilesTestIcaclsCalls | + Where-Object { $_ -contains (Join-Path $script:SshDirReal 'id_primary') }) + $privateCalls.Count | Should -Be 1 + $privateCalls[0] | Should -Contain 'sample-user:(F)' + } + + It 'does not run icacls against the public key' { + & $script:Fixture | Out-Null + + $publicCalls = @($global:DotfilesTestIcaclsCalls | + Where-Object { $_ -contains (Join-Path $script:SshDirReal 'id_primary.pub') }) + $publicCalls.Count | Should -Be 0 + } + + It 'skips writing a key that already exists' { + New-Item -ItemType Directory -Path $script:SshDirReal -Force | Out-Null + $existingPath = Join-Path $script:SshDirReal 'id_primary' + [System.IO.File]::WriteAllText($existingPath, 'PRE-EXISTING-CONTENT', + [System.Text.UTF8Encoding]::new($false)) + + & $script:Fixture | Out-Null + + $content = [System.IO.File]::ReadAllText($existingPath) + $content | Should -Be 'PRE-EXISTING-CONTENT' + } + + It 'produces no diff when re-run with keys already deployed' { + & $script:Fixture | Out-Null + $before = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_primary')) + + & $script:Fixture | Out-Null + $after = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_primary')) + + [System.Linq.Enumerable]::SequenceEqual($before, $after) | Should -BeTrue + } +} diff --git a/tests/powershell/fixtures/deploy-ssh-keys.ps1 b/tests/powershell/fixtures/deploy-ssh-keys.ps1 new file mode 100644 index 00000000..4086de1c --- /dev/null +++ b/tests/powershell/fixtures/deploy-ssh-keys.ps1 @@ -0,0 +1,78 @@ +#!/usr/bin/env pwsh +# Pre-rendered test fixture for run_once_before_20-deploy-ssh-keys.ps1.tmpl. +# Contains two hardcoded SSH key entries: +# primary - filename 'id_primary' +# secondary - filename 'id_secondary' +# +# This script is intentionally NOT a chezmoi template, and the key +# content below is synthetic placeholder text (not a real key pair) -- +# it exists only to exercise the encoding/newline/permission handling +# this script performs, not real SSH functionality. +$ErrorActionPreference = 'Stop' + +$sshDir = Join-Path $HOME '.ssh' +New-Item -ItemType Directory -Path $sshDir -Force | Out-Null + +# [System.IO.File] does not understand PS provider paths (e.g. TestDrive:\...), +# so resolve to a real filesystem path before using it with .NET I/O below. +$sshDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($sshDir) + +$deployState = Join-Path $HOME '.local/bin/secret-deploy-state.ps1' +function Record-State { + param([string]$Category, [string]$Name, [string]$Path) + if (-not (Test-Path $deployState)) { return } + try { & $deployState record $Category $Name $Path | Out-Null } catch { } +} + +$sshKeys = [ordered]@{ + primary = @{ + filename = 'id_primary' + private = "TEST-ONLY-PRIVATE-KEY-CONTENT-primary`nline two" + public = 'ssh-ed25519 AAAAPRIMARY primary@test' + } + secondary = @{ + filename = 'id_secondary' + private = 'TEST-ONLY-PRIVATE-KEY-CONTENT-secondary' + public = 'ssh-ed25519 AAAASECONDARY secondary@test' + } +} + +foreach ($key in $sshKeys.Keys) { + $sshKey = $sshKeys[$key] + Write-Host "Deploying SSH key: $key ($($sshKey.filename))..." + $privateKey = Join-Path $sshDir $sshKey.filename + $publicKey = Join-Path $sshDir "$($sshKey.filename).pub" + + if (-not (Test-Path $privateKey)) { + $privateKeyContent = $sshKey.private + # PS5.1 rejects the utf8NoBOM encoding literal (PS6+ only); write the + # BOM-less UTF-8 bytes directly instead. No trailing newline is added, + # matching the prior -NoNewline call. + [System.IO.File]::WriteAllText($privateKey, $privateKeyContent, + [System.Text.UTF8Encoding]::new($false)) + # Restrict to current user only (icacls works without elevation) + icacls $privateKey /inheritance:r /grant:r "${env:USERNAME}:(F)" 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to restrict permissions on ${privateKey} (icacls exit code: $LASTEXITCODE)" + } + Write-Host ' Private key written.' + Record-State -Category 'sshKey' -Name $key -Path $privateKey + } else { + Write-Host ' Private key already exists; skipping.' + } + + if (-not (Test-Path $publicKey)) { + $publicKeyContent = $sshKey.public + # PS5.1 rejects the utf8NoBOM encoding literal (PS6+ only); write the + # BOM-less UTF-8 bytes directly instead. No trailing newline is added, + # matching the prior -NoNewline call. + [System.IO.File]::WriteAllText($publicKey, $publicKeyContent, + [System.Text.UTF8Encoding]::new($false)) + Write-Host ' Public key written.' + Record-State -Category 'sshKey' -Name "$key.pub" -Path $publicKey + } else { + Write-Host ' Public key already exists; skipping.' + } +} + +Write-Host 'SSH key deployment complete.' diff --git a/tests/powershell/signing-resolve.Tests.ps1 b/tests/powershell/signing-resolve.Tests.ps1 index 5bd7b6b9..d4759b3d 100644 --- a/tests/powershell/signing-resolve.Tests.ps1 +++ b/tests/powershell/signing-resolve.Tests.ps1 @@ -131,7 +131,12 @@ Describe 'signing-resolve' -Skip:(-not $script:HasChezmoi) { # The rendered output is the generator *script* source, which # writes the profile gitconfig via a here-string; extract the # here-string body so it can be read as a real gitconfig file. - if ($r.Output -notmatch "(?ms)^@'\r?\n(.*?)\r?\n^'@") { + # `@'` may be preceded on the same line by a variable assignment + # (e.g. `$profileContent = @'`) now that the generator captures + # the here-string into a variable before writing it via + # [System.IO.File]::WriteAllText (PS5.1-compatible BOM-less + # UTF-8; see run_onchange_after_generate-git-profiles.ps1.tmpl). + if ($r.Output -notmatch "(?ms)^.*@'\r?\n(.*?)\r?\n^'@") { throw 'Could not locate the profile here-string in rendered output' } $renderedProfile = Join-Path ([IO.Path]::GetTempPath()) ("signing-{0}-profile" -f [guid]::NewGuid()) From b3d5984fed1101926172aa566354e5c8ddf743e7 Mon Sep 17 00:00:00 2001 From: kurone-kito Date: Fri, 14 Aug 2026 15:21:51 +0900 Subject: [PATCH 2/2] fix(test): address CodeRabbit findings on PR #253's fixture/regex - deploy-ssh-keys fixture: model a secret ending in a trailing newline (the conventional shape for a real SSH key secret from a file or secret manager), matching this repo's fixture-fidelity guideline; add a dedicated test asserting that trailing newline survives untouched with no second newline appended, and split the no-newline-secret assertion onto the public key alone (the multi-line-content test already covers the private key's no-newline shape). - signing-resolve.Tests.ps1: fix a real bug in the prior commit's own regex edit -- `^.*@'` is greedy under (?ms) dot-matches-newline and spans every rendered profile, capturing the LAST here-string instead of the first when more than one profile renders. Confirmed empirically with a two-profile simulation before and after. Constrained to `^[^\r\n]*@'` (same-line only) per CodeRabbit's suggested fix. The single-profile test in this file doesn't surface the bug (only one here-string exists to match), but it is a latent correctness issue for any future multi-profile test. Co-Authored-By: Claude Sonnet 5 --- tests/powershell/deploy-ssh-keys.Tests.ps1 | 24 ++++++++++++++++--- tests/powershell/fixtures/deploy-ssh-keys.ps1 | 8 +++++-- tests/powershell/signing-resolve.Tests.ps1 | 6 ++++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/tests/powershell/deploy-ssh-keys.Tests.ps1 b/tests/powershell/deploy-ssh-keys.Tests.ps1 index 37ec9207..8f52520c 100644 --- a/tests/powershell/deploy-ssh-keys.Tests.ps1 +++ b/tests/powershell/deploy-ssh-keys.Tests.ps1 @@ -50,11 +50,14 @@ Describe 'deploy-ssh-keys' { $publicBytes[1] -eq 0xBB -and $publicBytes[2] -eq 0xBF) | Should -BeFalse } - It 'writes exact byte-for-byte content with no added trailing newline' { + It 'preserves a secret''s own trailing newline exactly, with no additional newline added' { & $script:Fixture | Out-Null - $expectedPrivate = 'TEST-ONLY-PRIVATE-KEY-CONTENT-secondary' - $expectedPublic = 'ssh-ed25519 AAAASECONDARY secondary@test' + # secondary's fixture content already ends with "`n" (mirroring a + # real SSH key secret's conventional trailing newline) -- this + # must survive untouched, and no *second* newline may be appended. + $expectedPrivate = "TEST-ONLY-PRIVATE-KEY-CONTENT-secondary`n" + $expectedPublic = "ssh-ed25519 AAAASECONDARY secondary@test`n" $expectedPrivateBytes = [System.Text.UTF8Encoding]::new($false).GetBytes($expectedPrivate) $expectedPublicBytes = [System.Text.UTF8Encoding]::new($false).GetBytes($expectedPublic) @@ -65,6 +68,21 @@ Describe 'deploy-ssh-keys' { [System.Linq.Enumerable]::SequenceEqual($actualPublicBytes, $expectedPublicBytes) | Should -BeTrue } + It 'writes exact byte-for-byte content with no added trailing newline when the secret has none' { + & $script:Fixture | Out-Null + + # primary's fixture content has no trailing newline at all -- the + # multi-line-content case below covers this shape; here we assert + # the same no-addition guarantee on the simpler single-line public + # key, which also has no trailing newline in the fixture. + $expectedPublic = 'ssh-ed25519 AAAAPRIMARY primary@test' + $expectedPublicBytes = [System.Text.UTF8Encoding]::new($false).GetBytes($expectedPublic) + + $actualPublicBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_primary.pub')) + + [System.Linq.Enumerable]::SequenceEqual($actualPublicBytes, $expectedPublicBytes) | Should -BeTrue + } + It 'preserves embedded newlines in multi-line key content exactly' { & $script:Fixture | Out-Null diff --git a/tests/powershell/fixtures/deploy-ssh-keys.ps1 b/tests/powershell/fixtures/deploy-ssh-keys.ps1 index 4086de1c..c9d044c2 100644 --- a/tests/powershell/fixtures/deploy-ssh-keys.ps1 +++ b/tests/powershell/fixtures/deploy-ssh-keys.ps1 @@ -32,8 +32,12 @@ $sshKeys = [ordered]@{ } secondary = @{ filename = 'id_secondary' - private = 'TEST-ONLY-PRIVATE-KEY-CONTENT-secondary' - public = 'ssh-ed25519 AAAASECONDARY secondary@test' + # A real SSH key secret, as commonly stored by a secret manager or + # read from a PEM/OpenSSH-format file, conventionally ends with a + # trailing newline. Model that shape here to verify it survives + # the write untouched -- with no *additional* newline appended. + private = "TEST-ONLY-PRIVATE-KEY-CONTENT-secondary`n" + public = "ssh-ed25519 AAAASECONDARY secondary@test`n" } } diff --git a/tests/powershell/signing-resolve.Tests.ps1 b/tests/powershell/signing-resolve.Tests.ps1 index d4759b3d..8f923bef 100644 --- a/tests/powershell/signing-resolve.Tests.ps1 +++ b/tests/powershell/signing-resolve.Tests.ps1 @@ -136,7 +136,11 @@ Describe 'signing-resolve' -Skip:(-not $script:HasChezmoi) { # the here-string into a variable before writing it via # [System.IO.File]::WriteAllText (PS5.1-compatible BOM-less # UTF-8; see run_onchange_after_generate-git-profiles.ps1.tmpl). - if ($r.Output -notmatch "(?ms)^.*@'\r?\n(.*?)\r?\n^'@") { + # The opener prefix uses [^\r\n]* (not .*) so it stays confined + # to one line under (?s) dot-matches-newline: a greedy .* would + # span every profile in a multi-profile render and capture the + # LAST here-string instead of the first. + if ($r.Output -notmatch "(?ms)^[^\r\n]*@'\r?\n(.*?)\r?\n^'@") { throw 'Could not locate the profile here-string in rendered output' } $renderedProfile = Join-Path ([IO.Path]::GetTempPath()) ("signing-{0}-profile" -f [guid]::NewGuid())