#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