feat: refuse to learn a first-contact key from an unpinned server

Adds an opt-in "require a pinned server" setting that gates the one
RESOLVE path that currently trusts whatever key a server hands back
for a brand-new, hand-typed address — self-certifying addresses and
already-known contacts are structurally exempt. A refusal shows an
inline recovery card in compose to fetch, compare, and pin the key
without losing the draft.

Also closes two adjacent gaps: a self-certifying address could
silently overwrite an already-trusted contact's key (now asks,
showing both fingerprints), and a contact's detail view now names its
server and pin state.
This commit is contained in:
randogoth 2026-10-08 22:12:36 +03:00
parent 84397a3fdd
commit 80aca8a29f
4 changed files with 192 additions and 10 deletions

View file

@ -143,6 +143,21 @@
<input name="to" required spellcheck="false" placeholder="to: alice@example.org or smol://…">
<input name="subject" spellcheck="false" placeholder="subject">
<textarea name="body" rows="12" placeholder="message"></textarea>
<div id="compose-unpinned" class="seed-card" hidden>
<h4>unpinned server</h4>
<p class="hint">
<strong id="compose-unpinned-host"></strong> is not pinned, and "require a pinned
server" refuses to trust a key fetched from it for a new recipient. Fetch its key,
compare the fingerprint against what the operator published through a trusted
channel, then pin it to send.
</p>
<div id="compose-unpinned-key-area" hidden></div>
<div class="actions">
<button type="button" id="compose-unpinned-fetch">Fetch their server's key</button>
<button type="button" id="compose-unpinned-pin" class="filled" hidden>Pin and send</button>
</div>
<span id="compose-unpinned-error" class="error"></span>
</div>
<footer>
<label class="anon"><input type="checkbox" name="anonymous"> anonymous — omit my address</label>
<span id="compose-error" class="error"></span>
@ -211,6 +226,17 @@
</section>
<section class="panel" data-panel="advanced" hidden>
<h3>trust</h3>
<label class="check-row">
<input type="checkbox" id="require-pinned-server">
<span>
<span class="check-title">Require a pinned server</span>
<span class="hint">Refuse to look up a new recipient's key from a server you haven't
pinned. An address that already carries a key, or a recipient you already know, is
unaffected — this only applies to a new, hand-typed address.</span>
</span>
</label>
<h3>rotations</h3>
<p id="rotations-description" class="hint">Issues a fresh key from your secret with a signed
certificate proving it replaces this one. Contacts accept the change from that chain, and