Initial public commit
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
<!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="index.html" class="brand"><img src="assets/keysat-mark.svg" alt=""><span>Keysat</span></a>
|
||||
<span class="docs-tag">Docs</span>
|
||||
<nav>
|
||||
<a href="install.html">Install</a>
|
||||
<a href="integrate.html">Integrate</a>
|
||||
<a href="wire-format.html" class="active">Wire format</a>
|
||||
<a href="operate.html">Operate</a>
|
||||
<a href="https://keysat.xyz">Marketing</a>
|
||||
</nav>
|
||||
</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>
|
||||
</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>
|
||||
Reference in New Issue
Block a user