54 lines
3.6 KiB
PowerShell
54 lines
3.6 KiB
PowerShell
#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
|
|
}
|