chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$PSNativeCommandUseErrorActionPreference = $true
|
||||
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $projectRoot
|
||||
|
||||
$apiBaseUrl = 'http://localhost:58080'
|
||||
$deviceId = '10000000-0000-0000-0000-000000000511'
|
||||
$oldSecret = 'local-simulated-device-secret-32-bytes-minimum'
|
||||
$drillContainer = 'voice-key-rotation-drill'
|
||||
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('kms-rotation-conformance-' + [Guid]::NewGuid().ToString('N'))
|
||||
$capturePath = Join-Path $temporaryRoot 'rotation-capture.json'
|
||||
$reportPath = Join-Path $temporaryRoot 'rotation-report.json'
|
||||
$newSecret = [Convert]::ToHexString([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)).ToLowerInvariant()
|
||||
$originalImage = ((docker compose images -q api | Select-Object -First 1).Trim() -replace '^sha256:', '')
|
||||
$rotationStartedAt = [DateTime]::UtcNow
|
||||
|
||||
function Wait-Ready([int]$Seconds = 60) {
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds($Seconds)
|
||||
do {
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri "$apiBaseUrl/health/ready" -TimeoutSec 2
|
||||
if ($response.StatusCode -eq 200) { return }
|
||||
} catch {}
|
||||
Start-Sleep -Milliseconds 500
|
||||
} while ([DateTime]::UtcNow -lt $deadline)
|
||||
throw 'API未在限定时间内恢复就绪。'
|
||||
}
|
||||
|
||||
function Invoke-SignedConfig([string]$Secret) {
|
||||
$path = '/device/v2/config'
|
||||
$timestamp = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||
$nonce = [Guid]::NewGuid().ToString('N')
|
||||
$digest = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([byte[]]::new(0))).ToLowerInvariant()
|
||||
$canonical = "GET`n$path`n$timestamp`n$nonce`n$digest"
|
||||
$hmac = [Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($Secret))
|
||||
try {
|
||||
$signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical))).TrimEnd('=').Replace('+', '-').Replace('/', '_')
|
||||
} finally {
|
||||
$hmac.Dispose()
|
||||
}
|
||||
return Invoke-WebRequest -UseBasicParsing -Uri ($apiBaseUrl + $path) -Headers @{
|
||||
'X-Device-Id' = $deviceId
|
||||
'X-Device-Timestamp' = $timestamp
|
||||
'X-Device-Nonce' = $nonce
|
||||
'X-Device-Content-SHA256' = $digest
|
||||
'X-Device-Signature' = $signature
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $temporaryRoot | Out-Null
|
||||
Wait-Ready
|
||||
if ((Invoke-SignedConfig $oldSecret).StatusCode -ne 200) { throw '轮换前基线签名验证失败。' }
|
||||
|
||||
docker compose stop api | Out-Null
|
||||
$env:DEVICE_LOCAL_SECRET = $newSecret
|
||||
docker compose run -d --name $drillContainer -p 58080:8080 -e DEVICE_LOCAL_SECRET api | Out-Null
|
||||
Wait-Ready
|
||||
|
||||
if ((Invoke-SignedConfig $newSecret).StatusCode -ne 200) { throw '新密钥注入后签名验证失败。' }
|
||||
try {
|
||||
Invoke-SignedConfig $oldSecret | Out-Null
|
||||
throw '旧密钥在轮换后不应继续通过。'
|
||||
} catch {
|
||||
if ($null -eq $_.Exception.Response -or [int]$_.Exception.Response.StatusCode -ne 401) { throw }
|
||||
}
|
||||
$drillImage = ((docker inspect --format '{{.Image}}' $drillContainer).Trim() -replace '^sha256:', '')
|
||||
if ($drillImage -ne $originalImage) { throw '轮换演练意外改变了容器镜像。' }
|
||||
$now = [DateTime]::UtcNow
|
||||
$capture = @{
|
||||
schema_version = 'kms-rotation-capture-v1'; evidence_source = 'LOCAL'; candidate_version = '0.63.0-rc.local'
|
||||
build_digest = ('sha256:' + $originalImage); environment = 'LOCAL'; provider_ref = 'docker-compose:runtime-environment'
|
||||
workload_identity_ref = ''; provider_audit_ref = ''; attested_by_ref = 'local-smoke:automated'
|
||||
attested_at = $now.ToString('o'); collected_at = $now.ToString('o')
|
||||
rotations = @(@{
|
||||
secret_ref = 'device-local-signing-secret'; consumer_ref = 'api'; consumption_mode = 'ROLLING_RESTART'
|
||||
old_version_ref = 'local-version:baseline'; new_version_ref = 'local-version:drill'
|
||||
started_at = $rotationStartedAt.ToString('o'); completed_at = $now.ToString('o')
|
||||
new_credential_verified = $true; old_credential_rejected = $true; business_continuity_verified = $true; secret_value_not_exported = $true
|
||||
pre_rotation_probe_ref = 'http-probe:baseline-200'; new_credential_probe_ref = 'http-probe:new-key-200'
|
||||
old_rejection_probe_ref = 'http-probe:old-key-401'; continuity_probe_ref = 'health-ready:200'; access_audit_ref = 'local-container-log:masked'
|
||||
})
|
||||
}
|
||||
[System.IO.File]::WriteAllText($capturePath, ($capture | ConvertTo-Json -Depth 10), [System.Text.UTF8Encoding]::new($false))
|
||||
go run ./cmd/kms-rotation-conformance -capture $capturePath -candidate-version '0.63.0-rc.local' -build-digest ('sha256:' + $originalImage) -output $reportPath
|
||||
$report = Get-Content -Raw -LiteralPath $reportPath | ConvertFrom-Json
|
||||
if ($report.result -ne 'PASS' -or $report.evidence_scope -ne 'LOCAL_SECRET' -or -not $report.external_validation_required -or $report.rotation_count -ne 1) { throw '本地轮换报告错误地改变了云KMS/HSM验收边界。' }
|
||||
Write-Host '长期密钥本地演练通过:新密钥生效、旧密钥失效、镜像未变化;报告范围为LOCAL_SECRET,真实KMS/HSM仍待验。'
|
||||
} finally {
|
||||
Remove-Item Env:DEVICE_LOCAL_SECRET -ErrorAction SilentlyContinue
|
||||
docker rm -f $drillContainer 2>$null | Out-Null
|
||||
docker compose up -d api | Out-Null
|
||||
Wait-Ready
|
||||
if ((Invoke-SignedConfig $oldSecret).StatusCode -ne 200) { throw '演练回退后原本地密钥未恢复。' }
|
||||
if (Test-Path -LiteralPath $temporaryRoot) { Remove-Item -LiteralPath $temporaryRoot -Recurse -Force }
|
||||
Write-Host '演练容器已删除,Compose API已恢复并通过原密钥验证。'
|
||||
}
|
||||
Reference in New Issue
Block a user