Keysat Docs
Reference · Wire format

Wire format reference.

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.

Overview

A Keysat license key looks like this on a receipt:

KS-9F2A-7C41-XK22-6D8E-LM77-PQ91

Strip the KS- prefix and the dashes, and you have a Crockford base32-encoded blob. Base32-decode that blob, and you get the binary license envelope: a fixed-layout struct followed by an Ed25519 signature.

Binary layout

All multi-byte integers are big-endian.

OffsetLengthFieldNotes
04MagicASCII KSAT (0x4B 0x53 0x41 0x54).
41VersionCurrently 0x01. Decoders MUST reject unknown versions.
51FlagsBit 0: TRIAL. Bit 1: PERPETUAL. Bits 2–7 reserved.
616License IDUUIDv4 binary form.
2216Issuer fingerprintSHA-256 of the issuer public key, truncated to 16 bytes.
388Issued-atUnix seconds, signed.
468Expires-atUnix seconds, signed. 0 if PERPETUAL flag is set.
542SeatsMax machines. 0 = unlimited.
562Payload lengthLength L of the variable-size payload that follows.
58LPayloadUTF-8 JSON: { "product": "...", "policy": "...", "entitlements": [...] }.
58 + L64SignatureEd25519 signature over bytes 0 .. (58 + L).

Crockford base32

Keysat uses Crockford’s base32 alphabet (0123456789ABCDEFGHJKMNPQRSTVWXYZ) without checksum, without padding, and case-insensitive on decode.

The reason for Crockford over standard base32: human-friendly. I, L, O, U are excluded from the alphabet to avoid ambiguity when typing keys off a printed receipt.

Dash grouping & prefix

For display, keys are upper-cased, then grouped into 4-character chunks separated by dashes, and prefixed with KS-:

// raw base32, length depends on payload size
9F2A7C41XK226D8ELM77PQ91RR54VV01

// grouped + prefixed for display
KS-9F2A-7C41-XK22-6D8E-LM77-PQ91-RR54-VV01

Decoders MUST strip the KS- prefix (case-insensitive), strip whitespace and dashes, and case-fold to upper before base32-decoding.

Signature

The signature covers the entire envelope from offset 0 through the end of the payload — that is, all bytes before the 64-byte signature itself.

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".

Worked example

Test vector for the Python SDK’s cross-check tests (issuer fingerprint 0xfeed face cafe babe..., single-seat perpetual license):

# Hex dump of the binary envelope
00000000  4B 53 41 54 01 02 9F 2A  7C 41 XK 22 6D 8E LM 77   |KSAT...*|A.."m..w|
00000010  PQ 91 RR 54 VV 01 FE ED  FA CE CA FE BA BE 00 00   |...T....|........|
00000020  00 00 00 00 65 4F 12 34  00 00 00 00 00 00 00 00   |....eO.4|........|
00000030  00 01 00 24 7B 22 70 72  6F 64 75 63 74 22 3A 22   |...${"product":"|
00000040  73 75 6E 64 69 61 6C 22  2C 22 70 6F 6C 69 63 79   |sundial","policy|
00000050  22 3A 22 64 65 66 61 75  6C 74 22 7D ...sig...     |":"default"}.....|

# As displayed
KS-9F2A-7C41-XK22-6D8E-LM77-PQ91-…

The full vector lives in licensing-client-python/tests/fixtures/canonical.json and is what every official SDK is tested against.

Issuer public key format

Public keys are exchanged in PEM format, SubjectPublicKeyInfo encoded:

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAmz7q8r4t1v…h3k2pXq9wL
-----END PUBLIC KEY-----

This is the same encoding that openssl pkey -pubout produces. Keysat exposes it at GET /v1/issuer/public-key:

{
  "public_key_pem": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----\n",
  "public_key_b64": "mz7q8r4t1v…h3k2pXq9wL",
  "fingerprint_hex": "feed face cafe babe …"
}

Porting to a new language

The wire format is small enough to port in an afternoon. The order is:

  1. Copy the test vectors from licensing-client-python/tests/fixtures/canonical.json.
  2. Implement Crockford base32 decode (~30 lines).
  3. Implement the binary unmarshal (~40 lines, mostly offset arithmetic).
  4. Wire it up to your language’s Ed25519 verifier from a vetted crypto library.
  5. Run the cross-check tests — if they pass, you’re wire-compatible.

See PORTING_SDK_TO_NEW_LANGUAGES.md in the repo for the full contributor guide.

Versioning policy

The version byte at offset 4 is a hard gate. Decoders MUST reject any version they don’t implement. We commit to: