diff --git a/docs/adr/0001-package-registry.md b/docs/adr/0001-package-registry.md index 40d1750..ae11066 100644 --- a/docs/adr/0001-package-registry.md +++ b/docs/adr/0001-package-registry.md @@ -55,7 +55,10 @@ The `shortarrow.jp` zone is on Cloudflare. the static assets of a Cloudflare Worker that has no script. They are small and change on every release and every re-signing (ยง5). Requests for static assets are free and unlimited, and a request no asset or - rule matches gets a 404. + rule matches gets a 404. The root of the host serves one page, built + from `keyring/` on every deploy, that names the repository, its + maintainer and the fingerprint of every key, so a reader can check the + keyring they downloaded without the page ever disagreeing with it. - The packages are GitHub Release assets of the project that built them and nothing else. The repository's `pool///` path answers with a 302 to diff --git a/scripts/Keyring.psm1 b/scripts/Keyring.psm1 index 4aca56e..3c5cb35 100644 --- a/scripts/Keyring.psm1 +++ b/scripts/Keyring.psm1 @@ -141,6 +141,32 @@ function Get-KeyFingerprint([string[]] $Path) { } } +function Get-KeyringKey { + <# + .SYNOPSIS + Returns the Fingerprint and UserId of the primary key in each *.asc file under KeyringDirectory, in file name order. + #> + param([string] $KeyringDirectory) + + $work = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) + New-Item -ItemType Directory -Path $work | Out-Null + try { + foreach ($asc in Get-ChildItem $KeyringDirectory -Filter '*.asc' | Sort-Object Name) { + $lines = & gpg --homedir $work --batch --with-colons --show-keys $asc.FullName 2>$null + $fingerprint = ($lines | Where-Object { $_ -like 'fpr:*' } | Select-Object -First 1).Split(':')[9] + $userId = ($lines | Where-Object { $_ -like 'uid:*' } | Select-Object -First 1).Split(':')[9] + [pscustomobject]@{ + Fingerprint = $fingerprint + UserId = [regex]::Replace($userId, '\\x([0-9a-fA-F]{2})', { [string] [char] [Convert]::ToInt32($args[0].Groups[1].Value, 16) }) + } + } + } + finally { + & gpgconf --homedir $work --kill gpg-agent 2>$null + Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue + } +} + function Test-KeyringPackageCurrent { <# .SYNOPSIS @@ -165,4 +191,4 @@ function Test-KeyringPackageCurrent { } } -Export-ModuleMember -Function Get-NextKeyringVersion, Write-BinaryKeyring, New-KeyringPackage, Select-NewestKeyringEntry, Test-KeyringPackageCurrent +Export-ModuleMember -Function Get-NextKeyringVersion, Write-BinaryKeyring, New-KeyringPackage, Select-NewestKeyringEntry, Get-KeyringKey, Test-KeyringPackageCurrent diff --git a/scripts/PublishedSite.psm1 b/scripts/PublishedSite.psm1 index cba8b16..d0f29b5 100644 --- a/scripts/PublishedSite.psm1 +++ b/scripts/PublishedSite.psm1 @@ -112,6 +112,41 @@ function Write-Gzip([string] $Path, [byte[]] $Bytes) { try { $stream.Write($Bytes, 0, $Bytes.Length) } finally { $stream.Dispose() } } +function New-FrontPage([string] $KeyringDirectory) { + $keys = foreach ($key in Get-KeyringKey -KeyringDirectory $KeyringDirectory) { + $fingerprint = $key.Fingerprint -replace '(.{4})(?!$)', '$1 ' + "
  • $fingerprint
    $([System.Net.WebUtility]::HtmlEncode($key.UserId))
  • " + } + @" + + + + + +pkg.shortarrow.jp + + + +

    pkg.shortarrow.jp

    +

    The APT repository of ShortArrow's command-line tools, at https://pkg.shortarrow.jp/apt. +It is maintained by ShortArrow and built from +ShortArrow/pkg, which gives the install steps.

    +

    Keys

    +

    shortarrow-archive-keyring.gpg holds these public keys. +The signing key signs the repository; the standby key takes over when the signing key has to be replaced.

    +
      +$($keys -join "`n") +
    + + + +"@ +} + function Write-Site { <# .SYNOPSIS @@ -149,6 +184,7 @@ function Write-Site { $suite = Join-Path $apt 'dists' 'stable' New-Item -ItemType Directory -Force -Path $suite | Out-Null Copy-Item $Redirects (Join-Path $Destination '_redirects') + [System.IO.File]::WriteAllText((Join-Path $Destination 'index.html'), ((New-FrontPage $KeyringDirectory) -replace "`r`n", "`n")) Write-BinaryKeyring -KeyringDirectory $KeyringDirectory -Path (Join-Path $apt 'shortarrow-archive-keyring.gpg') foreach ($entry in $Index | Where-Object { $_.Filename -notlike 'pool/*' }) { diff --git a/tests/WriteSite.Tests.ps1 b/tests/WriteSite.Tests.ps1 index fab66c7..47f8270 100644 --- a/tests/WriteSite.Tests.ps1 +++ b/tests/WriteSite.Tests.ps1 @@ -31,6 +31,10 @@ Describe 'Write-Site' -Tag 'gpg' { $lines = & gpg --homedir $gpgHome --batch --with-colons --list-keys '=signing' $signing = ($lines | Where-Object { $_ -like 'fpr:*' } | Select-Object -First 1).Split(':')[9] & gpg --homedir $gpgHome --batch --yes --armor --output (Join-Path $keyring "$signing.asc") --export $signing + & gpg --homedir $gpgHome --batch --pinentry-mode loopback --passphrase '' --quick-generate-key 'standby ' ed25519 sign never 2>$null + $lines = & gpg --homedir $gpgHome --batch --with-colons --list-keys '=standby ' + $standby = ($lines | Where-Object { $_ -like 'fpr:*' } | Select-Object -First 1).Split(':')[9] + & gpg --homedir $gpgHome --batch --yes --armor --output (Join-Path $keyring "$standby.asc") --export $standby $source = New-TempDirectory $kept = Join-Path $source 'apt' 'keyring' 'shortarrow-archive-keyring_2026.10.21.1_all.deb' @@ -60,7 +64,7 @@ Describe 'Write-Site' -Tag 'gpg' { Remove-Item -Recurse -Force $gpgHome, $keyring, $source, (Split-Path $site) -ErrorAction SilentlyContinue } - It 'writes the redirects, the keyring, the files the index names off pool/, and the signed index, and nothing else' { + It 'writes the redirects, the front page, the keyring, the files the index names off pool/, and the signed index, and nothing else' { Get-RelativeFile $site | Should -Be @( '_redirects' 'apt/dists/stable/InRelease' @@ -71,7 +75,17 @@ Describe 'Write-Site' -Tag 'gpg' { 'apt/dists/stable/Release' 'apt/keyring/shortarrow-archive-keyring_2026.10.21.1_all.deb' 'apt/shortarrow-archive-keyring.gpg' - ) | Sort-Object + 'index.html' + ) + } + + It 'names on the front page every key under keyring/, with its fingerprint and its user ID escaped' { + $page = [System.IO.File]::ReadAllText((Join-Path $site 'index.html')) + $page | Should -Match ([regex]::Escape(($signing -replace '(.{4})(?!$)', '$1 '))) + $page | Should -Match ([regex]::Escape(($standby -replace '(.{4})(?!$)', '$1 '))) + $page | Should -Match 'standby <b>' + $page | Should -Not -Match 'standby ' + $page | Should -Match 'href="/apt/shortarrow-archive-keyring.gpg"' } It 'puts each architecture''s own entries and the architecture-independent ones in its Packages, and the same text in Packages.gz' {