76 lines
4.8 KiB
PowerShell
76 lines
4.8 KiB
PowerShell
#Requires -Version 7.2
|
|
param([ValidateSet('CreateKey','RemoveKey','Probe','TestOwnerKey')][string]$Action,[string]$Tag,[string]$Thumbprint,[string]$PlanPath)
|
|
$ErrorActionPreference = 'Stop'
|
|
if ($Action -eq 'CreateKey') {
|
|
$cert = New-SelfSignedCertificate -Subject "CN=Jobjakt-SYNTHETIC-$Tag" -CertStoreLocation Cert:\CurrentUser\My `
|
|
-KeyAlgorithm RSA -KeyLength 2048 -KeyExportPolicy NonExportable -KeyUsage DigitalSignature -NotAfter (Get-Date).AddHours(2)
|
|
try {
|
|
$rsa = [Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
|
|
$null = $rsa.SignData([Text.Encoding]::UTF8.GetBytes('synthetic-positive-control'),[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)
|
|
@{thumbprint=$cert.Thumbprint;keyName=$rsa.Key.KeyName;tag=$Tag} | ConvertTo-Json -Compress
|
|
} catch {
|
|
Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($cert.Thumbprint)" -DeleteKey
|
|
throw
|
|
} finally { if ($rsa) { $rsa.Dispose() } }
|
|
exit 0
|
|
}
|
|
if ($Action -eq 'TestOwnerKey') {
|
|
$cert=Get-Item -LiteralPath "Cert:\CurrentUser\My\$Thumbprint"
|
|
$rsa=[Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
|
|
try {
|
|
$null=$rsa.SignData([Text.Encoding]::UTF8.GetBytes('positive-control-no-approval'),[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)
|
|
@{keyName=$rsa.Key.KeyName;certificateSha256=[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($cert.RawData)).ToLowerInvariant()} | ConvertTo-Json -Compress
|
|
} finally { $rsa.Dispose() }
|
|
exit 0
|
|
}
|
|
if ($Action -eq 'RemoveKey') {
|
|
$cert = Get-Item -LiteralPath "Cert:\CurrentUser\My\$Thumbprint"
|
|
if ($cert.Subject -cne "CN=Jobjakt-SYNTHETIC-$Tag") { throw 'Refusing to remove any non-canary certificate' }
|
|
Remove-Item -LiteralPath "Cert:\CurrentUser\My\$Thumbprint" -DeleteKey
|
|
exit 0
|
|
}
|
|
$plan = Get-Content -LiteralPath $PlanPath -Raw | ConvertFrom-Json
|
|
$records = @()
|
|
foreach ($item in $plan.files) {
|
|
$readDenied=$false; $writeDenied=$false
|
|
try { $f=[IO.File]::Open($item.path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::ReadWrite); $f.Dispose() } catch { $readDenied=$true }
|
|
try { $f=[IO.File]::Open($item.path,[IO.FileMode]::Open,[IO.FileAccess]::Write,[IO.FileShare]::ReadWrite); $f.Dispose() } catch { $writeDenied=$true }
|
|
$records += @{category=$item.category;readDenied=$readDenied;writeDenied=$writeDenied}
|
|
}
|
|
function Can-SignCertificate([string]$CertificateThumbprint) {
|
|
if (-not $CertificateThumbprint) { return $false }
|
|
try {
|
|
$cert=Get-Item -LiteralPath "Cert:\CurrentUser\My\$CertificateThumbprint"
|
|
$rsa=[Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
|
|
try { $null=$rsa.SignData([Text.Encoding]::UTF8.GetBytes('sandbox-canary-only-no-approval'),[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1); return $true }
|
|
finally { if ($rsa) { $rsa.Dispose() } }
|
|
} catch { return $false }
|
|
}
|
|
$canSign = Can-SignCertificate $plan.key.thumbprint
|
|
try {
|
|
$key=[Security.Cryptography.CngKey]::Open($plan.key.keyName)
|
|
$rsa=[Security.Cryptography.RSACng]::new($key)
|
|
try { $null=$rsa.SignData([Text.Encoding]::UTF8.GetBytes('sandbox-canary-only-no-approval'),[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1); $canSign=$true }
|
|
finally { $rsa.Dispose(); $key.Dispose() }
|
|
} catch {}
|
|
$ownerCanSign = Can-SignCertificate $plan.ownerThumbprint
|
|
if ($plan.ownerKeyName) {
|
|
try {
|
|
$key=[Security.Cryptography.CngKey]::Open($plan.ownerKeyName)
|
|
$rsa=[Security.Cryptography.RSACng]::new($key)
|
|
try { $null=$rsa.SignData([Text.Encoding]::UTF8.GetBytes('sandbox-canary-only-no-approval'),[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1); $ownerCanSign=$true }
|
|
finally { $rsa.Dispose(); $key.Dispose() }
|
|
} catch {}
|
|
}
|
|
$networkDenied=$false
|
|
$client=[Net.Sockets.TcpClient]::new()
|
|
try { $connect=$client.ConnectAsync('127.0.0.1',[int]$plan.port); if (-not $connect.Wait(2000)) { $networkDenied=$true } else { $networkDenied=-not $client.Connected } }
|
|
catch { $networkDenied=$true }
|
|
finally { $client.Dispose() }
|
|
$positive=([IO.File]::ReadAllText($PlanPath).Length -gt 0)
|
|
$positivePath=Join-Path (Split-Path $PlanPath) 'positive.txt'
|
|
try { [IO.File]::WriteAllText($positivePath,'synthetic'); $positive=$positive -and [IO.File]::ReadAllText($positivePath).Equals('synthetic') }
|
|
catch { $positive=$false }
|
|
@{positiveControl=$positive;files=$records;networkDenied=$networkDenied;signingKeyDenied=(-not $canSign);
|
|
ownerKeyDenied=([bool]$plan.ownerThumbprint -and -not $ownerCanSign)} | ConvertTo-Json -Depth 8 -Compress
|