2026-07-17: join URLs + cert pinning (remote-issuance step 1)
Implemented step 1 of /plans/remote-token-issuance.md on branch join-string: the no-sidecar prefix (join-string grammar + TOFU cert pinning), after the Option C sidecar decision landed in the plan (v3) and roadmap (v58).
What shipped
client/joinurl(new package): Build/Parse ofmark://host#token=...&fp=sha256:<hex>join URLs, cert fingerprint helpers (DER, PEM),VerifyPinTLS verifier. Fragment carries the credentials; it never appears in any protocol request.client/fetch:Options.Pins(host to fingerprint map). A pinned host getsInsecureSkipVerifyplusVerifyPinat dial time, per host, so a multi-host crawler client pins only what it should. Wire tests: right pin, wrong pin (handshake failure with re-join hint), bare-hostname fallback, unpinned hosts still CA-verify.client/internal/tokens: entry gainspinfield (tokens.toml, additive),SetPin/Pins/ResolvePin(flag >DEMARKUS_PIN> stored).- All three clients wire stored pins;
demarkus-mcpgains-pin; newdemarkus join '<join-url>'stores token + pin in one paste. demarkus-token joinsubcommand: composes with generate via pipe,-certembeds the fingerprint.install.sh: summary now prints a ready-to-paste/soul-join <join-url>anddemarkus joinline.- Plugin:
registry soul-joinaccepts a join URL (fragment supplies token + pin), souls catalog gains a 5th pin column (all readers were length-guarded),mcp-servepasses-pininstead of-insecurefor pinned souls.commands/soul-join.mdupdated; plugin 0.12.16 to 0.13.0. - Docs:
docs/site/tools/index.mdjoin-URL section.
Decisions and gotchas
- Never pin Let's Encrypt certs: leaves rotate on renewal (install.sh runs certbot twice daily) and would break every pinned client. install.sh embeds
fp=only for custom certs; LE domains pass CA verification anyway. The dev self-signed cert has no stable file, so that path falls back to--insecurewith a note. - Go url fragment gotcha:
u.Fragmentis pre-decoded; parsing it withParseQuerycorrupts+and%in tokens. Useu.EscapedFragment(). Converselyurl.Values.Encodeescapes:to%3A, uglifying the paste string; the fragment is hand-built (pin literal, token QueryEscaped). - Pin resolution mirrors token resolution exactly (explicit > env > stored), per the shared-behavior guideline; pins ride tokens.toml instead of a new file.
- mcp-serve precedence: pin wins over insecure (pin verifies exactly one cert; insecure verifies nothing).
Verified live
Self-signed server on :6399: demarkus-token generate | join -cert produced the URL, demarkus join stored both credentials, publish + fetch ran with no --insecure, and a corrupted pin failed at the QUIC handshake with the actionable mismatch error. Plugin path: registry soul-join <join-url> wrote the 0600 token file, pinned=1, catalog row carries the pin.
Full make test + bash pre-commit.sh clean. Branch ready for PR; binary pin bumps ride the release automation after merge.
Follow-up: self-signed to CA migration
Question surfaced the scenario: server moves from a pinned self-signed cert to a CA cert. Pinned clients hard-fail (strict pin, by design); recovery is re-joining with a pinless join URL. The plugin path already handled it (catalog row upsert clears the pin), but demarkus join only wrote a pin when the URL carried one, so a stale pin survived re-join and kept failing. Fixed in the same branch: re-join re-states the trust material, a token-bearing URL without fp= clears the stored pin (with a stderr notice when one existed). Verified live against the e2e server. Lesson: when a credential store is updated by re-stating, absence of a field must overwrite, not preserve; the two join paths (upsert-row vs set-field) diverged on exactly that.