Go SDK for Keysat — a Bitcoin-native self-hosted software licensing service.
Verifies LIC1-format license keys offline against an Ed25519 public key, and optionally validates them online against a running Keysat daemon.
go get github.com/keysat-xyz/keysat-client-goStdlib only — no third-party dependencies.
package main
import (
"fmt"
"log"
"time"
"github.com/keysat-xyz/keysat-client-go"
)
// Embed the daemon's PEM public key at build time. Get it from your
// Keysat admin UI or `curl https://your-keysat.example/v1/pubkey`.
const publicKeyPEM = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA...
-----END PUBLIC KEY-----`
func main() {
pub, err := keysat.LoadPublicKeyPEM(publicKeyPEM)
if err != nil { log.Fatal(err) }
licenseKey := readKeyFromUserConfig() // however your app stores it
payload, err := keysat.ParseAndVerify(licenseKey, pub)
if err != nil { log.Fatalf("license invalid: %v", err) }
if payload.IsExpiredAt(time.Now().Unix()) {
log.Fatal("license expired")
}
if !payload.HasEntitlement("pro") {
log.Fatal("license does not include 'pro' tier")
}
fmt.Println("license OK")
}ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
c := keysat.NewClient("https://licensing.example.com", nil)
resp, err := c.Validate(ctx, keysat.ValidateRequest{
Key: licenseKey,
ProductSlug: "myapp",
Fingerprint: machineUUID,
})
if err != nil { log.Fatalf("daemon unreachable: %v", err) }
if !resp.OK {
log.Fatalf("license rejected: %s", resp.Reason)
}Validate returns HTTP 200 in all cases; license failures are conveyed via resp.OK + resp.Reason (bad_signature, revoked, expired, too_many_machines, etc.).
When a key is fingerprint-bound, the daemon's first successful online validation pins the machine's fingerprint hash to the license row. Subsequent validations from a different machine fail with fingerprint_mismatch.
The SDK exposes keysat.HashFingerprint if you need to compute the hash yourself (e.g., to compare against a key's FingerprintHash field offline):
h := keysat.HashFingerprint(machineUUID)
if h != payload.FingerprintHash {
log.Fatal("license does not belong to this machine")
}Every SDK + the daemon agree on the LIC1 wire format. Crosscheck tests in this package run against the shared tests/crosscheck/vector.json (alongside the daemon repo) — three independently-signed fixtures (v1 legacy, v2 trial with entitlements, v2 perpetual unbound) parse to the same field values across Rust, TypeScript, Python, and Go.
When fetched standalone via go get, the crosscheck test skips gracefully (the vector file isn't bundled into the Go module). The crosscheck only runs from the parent licensing/ workspace.
This SDK is alpha alongside Keysat v0.1.0. The wire format itself is stable and won't break compatibility — license keys issued by any v0.1 daemon will keep parsing in any future SDK. The Go API surface (function names, struct fields) may settle further before v1.0; nothing here is wildly out of line with idiomatic Go but expect minor tweaks.
MIT — see LICENSE.