#Requires -Version 7.2 param([Parameter(Mandatory)][string]$Verifier) $ErrorActionPreference = 'Stop' $folder = Join-Path ([IO.Path]::GetTempPath()) ('jobjakt-signature-test-' + [Guid]::NewGuid().ToString('N')) [IO.Directory]::CreateDirectory($folder) | Out-Null try { $rsa = [Security.Cryptography.RSA]::Create(2048) $request = [Security.Cryptography.X509Certificates.CertificateRequest]::new('CN=Synthetic only', $rsa, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1) $certificate = $request.CreateSelfSigned([DateTimeOffset]::UtcNow.AddDays(-1), [DateTimeOffset]::UtcNow.AddDays(1)) $certPath = Join-Path $folder 'owner.cer' [IO.File]::WriteAllBytes($certPath, $certificate.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert)) $requestPath = Join-Path $folder 'request.json' $approvalPath = Join-Path $folder 'approval.json' $original = '{"schemaVersion":1,"kind":"design-selection","manifestSha256":"manifest","policySha256":"policy","candidateCommit":"synthetic"}' [IO.File]::WriteAllText($requestPath, $original, [Text.UTF8Encoding]::new($false)) $requestHash = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([IO.File]::ReadAllBytes($requestPath))).ToLowerInvariant() function Write-Envelope([string]$Owner='SYNTHETIC-OWNER', [int]$Hours=1, [bool]$Forge=$false) { $payload = @{ requestSha256=$requestHash; approved=$true; ownerSid=$Owner; decision='Select concept B'; issuedUtc=[DateTimeOffset]::UtcNow.ToString('o'); expiresUtc=[DateTimeOffset]::UtcNow.AddHours($Hours).ToString('o') } | ConvertTo-Json -Compress $bytes = [Text.Encoding]::UTF8.GetBytes($payload) $signature = $rsa.SignData($bytes,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1) if ($Forge) { $signature[0] = $signature[0] -bxor 255 } @{ payloadBase64=[Convert]::ToBase64String($bytes); signatureBase64=[Convert]::ToBase64String($signature) } | ConvertTo-Json -Compress | Set-Content -LiteralPath $approvalPath -Encoding utf8NoBOM } function Assert-Verify([bool]$Expected, [string]$Case) { $messages = & (Get-Process -Id $PID).Path -NoProfile -File $Verifier -Action Verify -RequestPath $requestPath ` -ApprovalPath $approvalPath -CertificatePath $certPath -ExpectedOwnerSid 'SYNTHETIC-OWNER' 2>&1 if (($LASTEXITCODE -eq 0) -ne $Expected) { throw "Signature assertion failed: $Case $messages" } Write-Output "PASS $Case" } Write-Envelope Assert-Verify $true 'valid signature and concrete decision' [IO.File]::WriteAllText($requestPath, $original.Replace('synthetic','changed'), [Text.UTF8Encoding]::new($false)) Assert-Verify $false 'replayed approval on changed commit' [IO.File]::WriteAllText($requestPath, $original, [Text.UTF8Encoding]::new($false)) Write-Envelope -Forge $true Assert-Verify $false 'forged signature' Write-Envelope -Hours -1 Assert-Verify $false 'expired approval' Write-Envelope -Owner 'OTHER-OWNER' Assert-Verify $false 'wrong signing owner' '{"approved":true}' | Set-Content -LiteralPath $approvalPath Assert-Verify $false 'model-written approval JSON' $rsa.Dispose() $certificate.Dispose() } finally { $resolved = [IO.Path]::GetFullPath($folder) $tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()) if (-not $resolved.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'Cleanup escaped temp directory' } Remove-Item -LiteralPath $resolved -Recurse -Force }