Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/adr/0001-package-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<repo>/<tag>/<file>` path
answers with a 302 to
Expand Down
28 changes: 27 additions & 1 deletion scripts/Keyring.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
36 changes: 36 additions & 0 deletions scripts/PublishedSite.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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 '
"<li><code>$fingerprint</code><br>$([System.Net.WebUtility]::HtmlEncode($key.UserId))</li>"
}
@"
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>pkg.shortarrow.jp</title>
<style>
body { font-family: system-ui, sans-serif; line-height: 1.5; max-width: 46rem; margin: 2rem auto; padding: 0 1rem; }
code { font-family: ui-monospace, monospace; }
@media (prefers-color-scheme: dark) { body { background: #111; color: #ddd; } a { color: #8ab4f8; } }
</style>
</head>
<body>
<h1>pkg.shortarrow.jp</h1>
<p>The APT repository of ShortArrow's command-line tools, at <code>https://pkg.shortarrow.jp/apt</code>.
It is maintained by <a href="https://github.com/ShortArrow">ShortArrow</a> and built from
<a href="https://github.com/ShortArrow/pkg">ShortArrow/pkg</a>, which gives the install steps.</p>
<h2>Keys</h2>
<p><a href="/apt/shortarrow-archive-keyring.gpg">shortarrow-archive-keyring.gpg</a> holds these public keys.
The signing key signs the repository; the standby key takes over when the signing key has to be replaced.</p>
<ul>
$($keys -join "`n")
</ul>
</body>
</html>

"@
}

function Write-Site {
<#
.SYNOPSIS
Expand Down Expand Up @@ -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/*' }) {
Expand Down
18 changes: 16 additions & 2 deletions tests/WriteSite.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 <b>' ed25519 sign never 2>$null
$lines = & gpg --homedir $gpgHome --batch --with-colons --list-keys '=standby <b>'
$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'
Expand Down Expand Up @@ -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'
Expand All @@ -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 &lt;b&gt;'
$page | Should -Not -Match 'standby <b>'
$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' {
Expand Down
Loading