feat: pin client GPG trust and publish signed Git/Gist key releases

This commit is contained in:
2026-10-13 10:50:56 +08:00
parent 2f2f6db4d6
commit d72aa6eda7
28 changed files with 3187 additions and 427 deletions
-19
View File
@@ -8,7 +8,6 @@
const dialog = document.getElementById("copy-fallback");
const fallbackText = document.getElementById("fallback-text");
const closeFallback = document.getElementById("close-copy-fallback");
const allKeysButton = document.getElementById("copy-all-keys");
const validThemes = new Set(["system", "light", "dark"]);
const resetTimers = new WeakMap();
let fallbackTrigger = null;
@@ -115,23 +114,5 @@
button.addEventListener("click", () => copy(target.textContent, button));
});
let keys = [];
try {
const release = JSON.parse(document.getElementById("release-data")?.textContent || "{}");
if (Array.isArray(release.keys) && release.keys.every((key) => typeof key.text === "string" && key.text.length > 0)) {
keys = release.keys.map((key) => key.text);
}
} catch {
// Prerendered content remains usable if release metadata is unavailable.
}
if (keys.length === 0) {
keys = Array.from(document.querySelectorAll(".key-card pre code"), (code) => code.textContent).filter(Boolean);
}
if (allKeysButton) {
allKeysButton.disabled = keys.length === 0;
if (keys.length === 0) allKeysButton.setAttribute("title", "暂无可复制的公钥");
allKeysButton.addEventListener("click", () => copy(`${keys.join("\n")}\n`, allKeysButton));
}
root.classList.add("enhanced");
})();
+8 -12
View File
@@ -103,19 +103,16 @@ h1 { margin: 0; font-size: clamp(30px, 3.4vw, 42px); font-weight: 650; letter-sp
.section-index { color: var(--subtle); font-size: 10px; margin: 0 0 8px; }
.section-heading h2 { font-size: 23px; font-weight: 650; letter-spacing: -0.45px; line-height: 1.4; margin: 0; }
.section-heading p:not(.section-index) { color: var(--muted); font-size: 12px; margin: 7px 0 0; max-width: 690px; }
.section-actions { display: flex; align-items: center; gap: 18px; flex-shrink: 0; padding-bottom: 2px; }
.section-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 18px; flex-shrink: 0; padding-bottom: 2px; }
.text-link { white-space: nowrap; color: var(--muted); font-size: 12px; }
.text-link span { margin-left: 4px; }
.keys-notice { margin: 0 0 15px; padding: 12px 15px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-soft); color: var(--muted); font-size: 12px; }
.keys-notice:empty { display: none; }
.key-list, .command-list { display: grid; gap: 14px; min-width: 0; }
.command-list { display: grid; gap: 14px; min-width: 0; }
.command-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.key-card, .command-card { min-width: 0; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); overflow: hidden; }
.key-card { padding: 18px; }
.command-card { min-width: 0; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); overflow: hidden; }
.card-heading { display: flex; align-items: flex-start; gap: 12px; justify-content: space-between; min-width: 0; }
.card-heading h3 { min-width: 0; overflow-wrap: anywhere; font-size: 13px; font-weight: 600; margin: 0; line-height: 1.65; }
.key-type { flex-shrink: 0; max-width: 40%; overflow-wrap: anywhere; border: 1px solid var(--line); background: var(--surface-soft); border-radius: 4px; padding: 1px 7px; color: var(--muted); font: 10px/1.6 ui-monospace, SFMono-Regular, Consolas, monospace; }
.fingerprint { color: var(--subtle); font: 11px/1.65 ui-monospace, SFMono-Regular, Consolas, monospace; margin: 6px 0 12px; overflow-wrap: anywhere; }
.code-block { position: relative; min-width: 0; border: 1px solid var(--line); border-radius: 7px; background: var(--code); }
pre { overflow-x: auto; margin: 0; padding: 14px 62px 14px 14px; white-space: pre; scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 11px; line-height: 1.8; }
@@ -129,8 +126,6 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Libe
.copy-button[data-state="success"] { color: var(--accent); background: var(--accent-soft); border-color: var(--accent-line); }
.copy-button[data-state="success"]::before { width: 7px; height: 13px; left: calc(var(--icon-center) - 4px); top: 8px; border: solid currentColor; border-width: 0 2px 2px 0; border-radius: 0; transform: rotate(45deg); }
.copy-button[data-state="success"]::after { display: none; }
.copy-all { --icon-center: 19px; width: auto; gap: 8px; padding: 0 12px 0 38px; color: var(--accent); font-size: 12px; font-weight: 600; }
.copy-all:disabled { opacity: 0.55; }
.public-notice { display: flex; align-items: flex-start; gap: 9px; margin-top: 15px; color: var(--subtle); font-size: 11px; }
.public-notice p { margin: 0; }
.notice-icon { display: grid; place-items: center; flex: 0 0 14px; height: 14px; margin-top: 2px; border: 1px solid var(--subtle); border-radius: 50%; font: 10px/1 serif; }
@@ -143,6 +138,9 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Libe
.advanced-commands summary::marker { color: var(--subtle); }
.advanced-commands[open] summary { border-bottom: 1px solid var(--line); }
.advanced-commands > .command-list { padding: 15px; }
.trust-steps { margin: 0; padding-left: 22px; color: var(--muted); font-size: 12px; }
.trust-steps li { padding-left: 4px; margin-block: 10px; }
.trust-steps a { color: var(--accent); }
.command-note { font-size: 11px; color: var(--subtle); margin: 14px 0 0; }
.empty-state { padding: 26px; border: 1px dashed var(--line); border-radius: 10px; color: var(--muted); text-align: center; }
.preview-notice { margin: 24px 0 0; padding: 12px 16px; border: 1px solid var(--accent-line); border-radius: 8px; background: var(--accent-soft); color: var(--accent); font-size: 12px; }
@@ -204,16 +202,14 @@ dialog::backdrop { background: rgb(10 22 15 / 52%); }
.section-heading h2 { font-size: 21px; }
.section-actions { width: 100%; justify-content: space-between; }
.commands-section > .section-heading > .text-link { margin-top: -3px; }
.key-card, .command-card { padding: 14px; }
.command-card { padding: 14px; }
.command-list { grid-template-columns: minmax(0, 1fr); gap: 12px; }
.card-heading { gap: 8px; }
.fingerprint { font-size: 10px; }
pre { padding-left: 12px; padding-block: 16px; }
.copy-button { width: 40px; height: 40px; }
.copy-button::before { top: 11px; }
.copy-button::after { top: 15px; }
.copy-button[data-state="success"]::before { top: 10px; }
.copy-all { width: auto; }
.public-notice { font-size: 11px; }
.site-footer { flex-direction: column; align-items: flex-start; gap: 12px; padding: 22px 0 26px; }
dialog { padding: 20px; }
@@ -224,5 +220,5 @@ dialog::backdrop { background: rgb(10 22 15 / 52%); }
.page-shell { width: 100%; }
.hero { padding-block: 24px; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
.key-card, .command-card { break-inside: avoid; }
.command-card { break-inside: avoid; }
}
+24 -15
View File
@@ -4,13 +4,13 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="description" content="查看当前已发布的 SSH 公钥,复制服务器配置命令。Quickly Configuring SSH Server。">
<title>SSH 公钥与快速部署 · Quickly Configuring SSH Server</title>
<meta name="description" content="查看 SSH 公钥发布状态,复制服务器配置命令。Quickly Configuring SSH Server。">
<title>SSH 发布状态与快速部署 · Quickly Configuring SSH Server</title>
<link rel="stylesheet" href="/assets/style.css">
<script src="/assets/app.js" defer></script>
</head>
<body>
<a class="skip-link" href="#public-keys">跳到 SSH 公钥</a>
<a class="skip-link" href="#key-status">跳到发布状态</a>
<div class="page-shell">
<header class="site-header">
<a class="brand" href="/" aria-label="Quickly Configuring SSH Server 首页">
@@ -36,8 +36,8 @@
<div class="hero-copy">
<p class="eyebrow"><span class="status-dot" aria-hidden="true"></span> SSH KEY DISTRIBUTION</p>
<h1 id="page-title">公钥集中管理,<br>服务器快速就绪。</h1>
<p class="hero-description">核对公钥,选择命令,配置 SSH。<br class="wide-only">查看当前公钥来源与内容版本,按需部署。</p>
<div class="hero-links"><a href="#public-keys">查看公钥 <span aria-hidden="true">↓</span></a><a href="#quick-start">快速开始 <span aria-hidden="true">↓</span></a></div>
<p class="hero-description">查看发布状态,选择命令,配置 SSH。<br class="wide-only">查看当前公钥来源与内容版本,按需部署。</p>
<div class="hero-links"><a href="#key-status">发布状态 <span aria-hidden="true">↓</span></a><a href="#quick-start">快速开始 <span aria-hidden="true">↓</span></a></div>
</div>
<aside class="release-card" aria-labelledby="release-title">
<div class="release-heading"><h2 id="release-title">版本与公钥来源</h2><span class="release-badge">@@RELEASE_LABEL@@</span></div>
@@ -46,32 +46,41 @@
<div><dt>公钥版本</dt><dd><code class="release-revision">@@CONTENT_REVISION@@</code></dd></div>
<div><dt>公钥来源</dt><dd class="source-label">@@SOURCE_LABEL@@</dd></div>
<div><dt>来源状态</dt><dd class="source-status">@@SOURCE_STATUS@@</dd></div>
<div><dt>签名状态</dt><dd class="signature-status">@@SIGNATURE_STATUS@@</dd></div>
<div><dt>证书状态</dt><dd class="certificate-status">@@CERTIFICATE_STATUS@@</dd></div>
<div><dt>发布时间</dt><dd><time datetime="@@PUBLISHED_AT@@">@@PUBLISHED_AT@@</time></dd></div>
<div><dt>公开密钥</dt><dd><strong class="key-count">@@KEY_COUNT@@</strong><span class="count-unit"> 个</span></dd></div>
<div><dt>公钥数量</dt><dd><strong class="key-count">@@KEY_COUNT@@</strong><span class="count-unit"> 个</span></dd></div>
</dl>
<p class="release-note">此处展示代码发布与当前公钥来源,不代表所有服务器均已同步。</p>
</aside>
</section>
<section id="public-keys" class="content-section" aria-labelledby="keys-title">
<section id="key-status" class="content-section" aria-labelledby="keys-title">
<div class="section-heading">
<div><p class="section-index">01 / PUBLIC KEYS</p><h2 id="keys-title">SSH 公钥</h2><p>部署前,请核对指纹及密钥归属。</p></div>
<div class="section-actions"><a class="text-link" href="/ssh.keys">原始文件 <span aria-hidden="true">↗</span></a><button id="copy-all-keys" class="copy-button copy-all" type="button" aria-label="复制全部公钥" title="复制全部公钥" data-enhance><span>复制全部</span></button></div>
<div><p class="section-index">01 / RELEASE STATUS</p><h2 id="keys-title">公钥发布状态</h2><p>首页仅展示来源、状态、数量与内容摘要,不展示公钥列表、指纹或注释。</p></div>
<div class="section-actions"><a class="text-link" href="/ssh.keys.sig">独立签名接口 <span aria-hidden="true">↗</span></a><a class="text-link" href="/ssh.keys">公钥原始接口 <span aria-hidden="true">↗</span></a>@@CERTIFICATE_LINK@@</div>
</div>
<p class="keys-notice" role="note">@@KEYS_NOTICE@@</p>
<div class="key-list">@@KEYS_HTML@@</div>
<div class="public-notice"><span class="notice-icon" aria-hidden="true">i</span><p>这里展示的是公开密钥。私钥应始终由你保管,本页面不会要求上传或输入私钥。</p></div>
<div class="public-notice"><span class="notice-icon" aria-hidden="true">i</span><p>公钥不是秘密:原始 API 与公开 Git / Gist 仍可能提供公钥内容。私钥应留在离线环境,本页面不会要求上传或输入私钥。</p></div>
</section>
<section id="quick-start" class="content-section commands-section" aria-labelledby="commands-title">
<div class="section-heading"><div><p class="section-index">02 / QUICK START</p><h2 id="commands-title">按需选择,复制即用</h2><p>以下命令会在你执行后修改服务器配置。请先检查脚本,并保留当前 SSH 会话以验证新连接。</p></div><a class="text-link" href="/script.sh">查看脚本 <span aria-hidden="true">↗</span></a></div>
<div class="section-heading"><div><p class="section-index">02 / QUICK START</p><h2 id="commands-title">按需选择,复制即用</h2><p>首次安装的快捷命令会立即执行下载脚本。推荐先检查并独立核验安装脚本;高信任安装可按下方指引操作。修改 SSH 配置时保留当前会话以验证新连接。</p></div><a class="text-link" href="/script.sh">查看脚本 <span aria-hidden="true">↗</span></a></div>
<div class="command-list">@@COMMANDS_HTML@@</div>
<p class="command-note">每条命令独立使用。高级选项涉及 SSH 登录策略或卸载,请阅读说明后操作。</p>
<p class="command-note">每条命令独立使用。同步、预览及高级命令须先安装。脚本一旦落盘,日常同步不会替换可执行代码或本地信任指纹;更新脚本须在服务器重新安装。</p>
</section>
<noscript><p class="noscript-notice">JavaScript 已关闭,公钥与命令仍可查看并手动选择复制。启用 JavaScript 后可使用复制按钮与外观设置。</p></noscript>
<section id="trust-guidance" class="content-section" aria-labelledby="trust-title">
<div class="section-heading"><div><p class="section-index">03 / LOCAL TRUST</p><h2 id="trust-title">先核验,再建立本地信任</h2><p>签名文件存在,不代表 Worker 已验证签名。客户端验签默认关闭,须在服务器本地明确启用。</p></div><a class="text-link" href="@@REPOSITORY_URL@@#readme" target="_blank" rel="noopener noreferrer">完整安装说明 <span aria-hidden="true">↗</span></a></div>
<ol class="trust-steps">
<li>推荐从独立可信渠道取得安装脚本预期 SHA-256,下载、检查脚本并比对摘要后再执行。首次安装或明确重装都会信任你执行的脚本;与脚本同源取得的摘要无法建立独立信任。签名证书 pin 仍须独立核验。</li>
<li>可使用安装参数 --install --gpg-verification on,以 --gpg-fingerprint 固定独立核验的完整指纹,并指定 --list-id。--gpg-pin-mode 默认 primary,信任主钥及其有效绑定的签名子钥,也可选 subkey 固定签名子钥;安装或更换信任时须在服务器本地终端确认完整指纹,不能跳过确认;远端发布内容不能关闭本地验签。</li>
<li>关闭验签时,客户端仅获取 <a class="text-link" href="/ssh.keys">/ssh.keys</a>,不请求签名或证书;启用后还须取得 <a class="text-link" href="/ssh.keys.sig">/ssh.keys.sig</a>,两者均返回 200,客户端使用已验证并保存的本地证书核验公钥文档原始字节,全部通过才接受。证书仅在本地安装流程中发现并固定,日常同步不联网刷新证书;获取或验证失败保留已有公钥。签名私钥始终离线保管。</li>
</ol>
</section>
<noscript><p class="noscript-notice">JavaScript 已关闭,发布状态与命令仍可查看并手动选择复制。启用 JavaScript 后可使用复制按钮与外观设置。</p></noscript>
</main>
<footer class="site-footer"><span>Quickly Configuring SSH Server</span><div><a href="/ssh.keys">ssh.keys</a><a href="/script.sh">script.sh</a><a href="@@REPOSITORY_URL@@" target="_blank" rel="noopener noreferrer">源代码<span class="sr-only">(新窗口打开)</span></a></div></footer>
<footer class="site-footer"><span>Quickly Configuring SSH Server</span><div><a href="/ssh.keys.sig">ssh.keys.sig</a><a href="/ssh.keys">ssh.keys</a><a href="/script.sh">script.sh</a><a href="@@REPOSITORY_URL@@" target="_blank" rel="noopener noreferrer">源代码<span class="sr-only">(新窗口打开)</span></a></div></footer>
</div>
<p id="copy-status" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></p>