9e4c36c05b
Ports the in-repo KEYSAT_AGENT_GUIDE.md into the docs site as a first-class page rather than linking out to a raw markdown file on GitHub. The page covers authentication, scoped API keys, OpenAPI discovery, error envelope conventions, common workflows (issue / revoke / find / cancel / change-tier / free-machine), webhook event types + signature verification, robust-agent patterns, a "comp-license-via-email" recipe, and the operator-only operations that aren't exposed to any scoped key. Sidebar gains an "Agent integration" entry under Get started on every page (index, install, integrate, wire-format, operate, agent itself). Docs index "These docs cover" + "Where to next" grids each gain a third card pointing at the agent guide so it's discoverable from the introduction page even for visitors who don't scan the sidebar.
172 lines
9.5 KiB
HTML
172 lines
9.5 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Keysat Docs — Wire format reference</title>
|
|
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
|
|
<link rel="stylesheet" href="docs.css">
|
|
</head>
|
|
<body>
|
|
|
|
<div class="topnav">
|
|
<a href="https://keysat.xyz" class="brand" title="Back to keysat.xyz"><img src="assets/keysat-mark.svg" alt=""><span>Keysat</span></a>
|
|
<span class="docs-tag">Docs</span>
|
|
</div>
|
|
|
|
<div class="layout">
|
|
<aside class="side">
|
|
<div class="group">
|
|
<div class="glabel">Get started</div>
|
|
<a href="index.html">Introduction</a>
|
|
<a href="install.html">Install & setup</a>
|
|
<a href="integrate.html">Integrate the SDK</a>
|
|
<a href="agent.html">Agent integration</a>
|
|
</div>
|
|
<div class="group">
|
|
<div class="glabel">Concepts</div>
|
|
<a href="index.html#architecture">Architecture</a>
|
|
<a href="index.html#products-policies">Products & policies</a>
|
|
<a href="index.html#discounts">Discount codes</a>
|
|
<a href="index.html#revocation">Revocation strategy</a>
|
|
</div>
|
|
<div class="group">
|
|
<div class="glabel">Reference</div>
|
|
<a href="wire-format.html" class="active">Wire format</a>
|
|
<a href="integrate.html#api">Admin API</a>
|
|
<a href="integrate.html#sdks">SDKs</a>
|
|
</div>
|
|
<div class="group">
|
|
<div class="glabel">Operate</div>
|
|
<a href="operate.html#backups">Backups</a>
|
|
<a href="operate.html#migrate">Migrate hardware</a>
|
|
<a href="operate.html#troubleshooting">Troubleshooting</a>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="prose">
|
|
<div class="crumb">Reference · Wire format</div>
|
|
<h1>Wire format reference.</h1>
|
|
<p class="lead">The bytes-over-the-wire spec for a Keysat license. Stable across SDKs and across language ports. About 90 lines of pseudocode to implement in a new language.</p>
|
|
|
|
<h2 id="overview">Overview</h2>
|
|
<p>A Keysat license key looks like this on a receipt:</p>
|
|
|
|
<pre class="code">KS-9F2A-7C41-XK22-6D8E-LM77-PQ91</pre>
|
|
|
|
<p>Strip the <code>KS-</code> prefix and the dashes, and you have a Crockford base32-encoded blob. Base32-decode that blob, and you get the binary <em>license envelope</em>: a fixed-layout struct followed by an Ed25519 signature.</p>
|
|
|
|
<h2 id="layout">Binary layout</h2>
|
|
<p>All multi-byte integers are big-endian.</p>
|
|
|
|
<table class="t">
|
|
<thead><tr><th>Offset</th><th>Length</th><th>Field</th><th>Notes</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><code>0</code></td><td>4</td><td>Magic</td><td>ASCII <code>KSAT</code> (0x4B 0x53 0x41 0x54).</td></tr>
|
|
<tr><td><code>4</code></td><td>1</td><td>Version</td><td>Currently <code>0x01</code>. Decoders MUST reject unknown versions.</td></tr>
|
|
<tr><td><code>5</code></td><td>1</td><td>Flags</td><td>Bit 0: <code>TRIAL</code>. Bit 1: <code>PERPETUAL</code>. Bits 2–7 reserved.</td></tr>
|
|
<tr><td><code>6</code></td><td>16</td><td>License ID</td><td>UUIDv4 binary form.</td></tr>
|
|
<tr><td><code>22</code></td><td>16</td><td>Issuer fingerprint</td><td>SHA-256 of the issuer public key, truncated to 16 bytes.</td></tr>
|
|
<tr><td><code>38</code></td><td>8</td><td>Issued-at</td><td>Unix seconds, signed.</td></tr>
|
|
<tr><td><code>46</code></td><td>8</td><td>Expires-at</td><td>Unix seconds, signed. <code>0</code> if <code>PERPETUAL</code> flag is set.</td></tr>
|
|
<tr><td><code>54</code></td><td>2</td><td>Seats</td><td>Max machines. <code>0</code> = unlimited.</td></tr>
|
|
<tr><td><code>56</code></td><td>2</td><td>Payload length</td><td>Length <code>L</code> of the variable-size payload that follows.</td></tr>
|
|
<tr><td><code>58</code></td><td><code>L</code></td><td>Payload</td><td>UTF-8 JSON: <code>{ "product": "...", "policy": "...", "entitlements": [...] }</code>.</td></tr>
|
|
<tr><td><code>58 + L</code></td><td>64</td><td>Signature</td><td>Ed25519 signature over bytes <code>0 .. (58 + L)</code>.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h2 id="encoding">Crockford base32</h2>
|
|
<p>Keysat uses <a href="https://www.crockford.com/base32.html">Crockford’s base32 alphabet</a> (<code>0123456789ABCDEFGHJKMNPQRSTVWXYZ</code>) without checksum, without padding, and case-insensitive on decode.</p>
|
|
|
|
<p>The reason for Crockford over standard base32: human-friendly. <code>I</code>, <code>L</code>, <code>O</code>, <code>U</code> are excluded from the alphabet to avoid ambiguity when typing keys off a printed receipt.</p>
|
|
|
|
<h2 id="grouping">Dash grouping & prefix</h2>
|
|
<p>For display, keys are upper-cased, then grouped into 4-character chunks separated by dashes, and prefixed with <code>KS-</code>:</p>
|
|
|
|
<pre class="code"><span class="c">// raw base32, length depends on payload size</span>
|
|
9F2A7C41XK226D8ELM77PQ91RR54VV01
|
|
|
|
<span class="c">// grouped + prefixed for display</span>
|
|
KS-9F2A-7C41-XK22-6D8E-LM77-PQ91-RR54-VV01</pre>
|
|
|
|
<p>Decoders MUST strip the <code>KS-</code> prefix (case-insensitive), strip whitespace and dashes, and case-fold to upper before base32-decoding.</p>
|
|
|
|
<h2 id="signature">Signature</h2>
|
|
<p>The signature covers the entire envelope from offset <code>0</code> through the end of the payload — that is, all bytes <em>before</em> the 64-byte signature itself.</p>
|
|
|
|
<p>Verify with the issuer’s Ed25519 public key. The fingerprint at offset 22 lets the verifier confirm that the key it has matches the key the license was signed with: SHA-256 the public key bytes, truncate to 16 bytes, compare. If it doesn’t match, the verifier MUST reject before attempting signature check — this gives a clear "wrong issuer" error rather than a generic "bad signature".</p>
|
|
|
|
<h2 id="example">Worked example</h2>
|
|
<p>Test vector for the Python SDK’s cross-check tests (issuer fingerprint <code>0xfeed face cafe babe...</code>, single-seat perpetual license):</p>
|
|
|
|
<pre class="code"><span class="c"># Hex dump of the binary envelope</span>
|
|
00000000 4B 53 41 54 01 02 9F 2A 7C 41 XK 22 6D 8E LM 77 <span class="c">|KSAT...*|A.."m..w|</span>
|
|
00000010 PQ 91 RR 54 VV 01 FE ED FA CE CA FE BA BE 00 00 <span class="c">|...T....|........|</span>
|
|
00000020 00 00 00 00 65 4F 12 34 00 00 00 00 00 00 00 00 <span class="c">|....eO.4|........|</span>
|
|
00000030 00 01 00 24 7B 22 70 72 6F 64 75 63 74 22 3A 22 <span class="c">|...${"product":"|</span>
|
|
00000040 73 75 6E 64 69 61 6C 22 2C 22 70 6F 6C 69 63 79 <span class="c">|sundial","policy|</span>
|
|
00000050 22 3A 22 64 65 66 61 75 6C 74 22 7D ...sig... <span class="c">|":"default"}.....|</span>
|
|
|
|
<span class="c"># As displayed</span>
|
|
KS-9F2A-7C41-XK22-6D8E-LM77-PQ91-…</pre>
|
|
|
|
<p>The full vector lives in <code>licensing-client-python/tests/fixtures/canonical.json</code> and is what every official SDK is tested against.</p>
|
|
|
|
<h2 id="public-key">Issuer public key format</h2>
|
|
<p>Public keys are exchanged in PEM format, SubjectPublicKeyInfo encoded:</p>
|
|
|
|
<pre class="code">-----BEGIN PUBLIC KEY-----
|
|
MCowBQYDK2VwAyEAmz7q8r4t1v…h3k2pXq9wL
|
|
-----END PUBLIC KEY-----</pre>
|
|
|
|
<p>This is the same encoding that <code>openssl pkey -pubout</code> produces. Keysat exposes it at <code>GET /v1/issuer/public-key</code>:</p>
|
|
|
|
<pre class="code">{
|
|
<span class="s">"public_key_pem"</span>: <span class="s">"-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----\n"</span>,
|
|
<span class="s">"public_key_b64"</span>: <span class="s">"mz7q8r4t1v…h3k2pXq9wL"</span>,
|
|
<span class="s">"fingerprint_hex"</span>: <span class="s">"feed face cafe babe …"</span>
|
|
}</pre>
|
|
|
|
<h2 id="porting">Porting to a new language</h2>
|
|
<p>The wire format is small enough to port in an afternoon. The order is:</p>
|
|
|
|
<ol>
|
|
<li>Copy the test vectors from <a href="https://github.com/keysat-xyz/licensing-client-python/blob/main/tests/fixtures/canonical.json">licensing-client-python/tests/fixtures/canonical.json</a>.</li>
|
|
<li>Implement Crockford base32 decode (~30 lines).</li>
|
|
<li>Implement the binary unmarshal (~40 lines, mostly offset arithmetic).</li>
|
|
<li>Wire it up to your language’s Ed25519 verifier from a vetted crypto library.</li>
|
|
<li>Run the cross-check tests — if they pass, you’re wire-compatible.</li>
|
|
</ol>
|
|
|
|
<p>See <a href="https://github.com/keysat-xyz/keysat/blob/main/PORTING_SDK_TO_NEW_LANGUAGES.md">PORTING_SDK_TO_NEW_LANGUAGES.md</a> in the repo for the full contributor guide.</p>
|
|
|
|
<h2 id="versioning">Versioning policy</h2>
|
|
<p>The version byte at offset 4 is a hard gate. Decoders MUST reject any version they don’t implement. We commit to:</p>
|
|
|
|
<ul>
|
|
<li>Never silently changing the v1 layout. Any change ⇒ new version byte.</li>
|
|
<li>Maintaining v1 verifier support indefinitely — even if v2 ships, your existing customer keys stay verifiable.</li>
|
|
<li>Publishing test vectors for every new version under <code>tests/fixtures/</code> in the canonical SDK.</li>
|
|
</ul>
|
|
</main>
|
|
|
|
<aside class="toc">
|
|
<div class="label">On this page</div>
|
|
<a href="#overview">Overview</a>
|
|
<a href="#layout">Binary layout</a>
|
|
<a href="#encoding">Crockford base32</a>
|
|
<a href="#grouping">Dash grouping</a>
|
|
<a href="#signature">Signature</a>
|
|
<a href="#example">Worked example</a>
|
|
<a href="#public-key">Public key format</a>
|
|
<a href="#porting">Porting</a>
|
|
<a href="#versioning">Versioning policy</a>
|
|
</aside>
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<script>lucide.createIcons();</script>
|
|
</body>
|
|
</html>
|