Initial Go SDK for Keysat licensing service
Pure-Go, stdlib-only implementation of the LIC1 wire format: - ParseKey + Verify + ParseAndVerify for offline verification - HashFingerprint helper (SHA-256, matching the daemon's contract) - LoadPublicKeyPEM for the standard PKIX-encoded Ed25519 public keys the daemon emits - Client.Validate / Client.PublicKey for online checks against a running Keysat daemon - LicensePayload struct with idiomatic Go getters (IsTrial, IsFingerprintBound, IsExpiredAt, HasEntitlement) Wire-format crosscheck against the shared tests/crosscheck/vector.json (the same file the Rust, TypeScript, Python SDKs and the daemon itself test against). All four fixtures pass — v1 legacy fingerprint-bound, v2 trial with entitlements, v2 perpetual unbound, plus end-to-end PEM-load → ParseAndVerify signature roundtrip. Confirms byte-for-byte agreement across five independent implementations. No third-party dependencies. Module path: github.com/keysat-xyz/keysat-client-go go 1.21
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# keysat-client-go
|
||||
|
||||
Go SDK for [Keysat](https://keysat.xyz) — a self-hosted, Bitcoin-paid software licensing service.
|
||||
|
||||
Verifies LIC1-format license keys offline against an Ed25519 public key, and optionally validates them online against a running Keysat daemon.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/keysat-xyz/keysat-client-go
|
||||
```
|
||||
|
||||
Stdlib only — no third-party dependencies.
|
||||
|
||||
## Offline verification
|
||||
|
||||
```go
|
||||
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")
|
||||
}
|
||||
```
|
||||
|
||||
## Online validation (revocation, fingerprint binding, machine cap)
|
||||
|
||||
```go
|
||||
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.).
|
||||
|
||||
## Fingerprint binding
|
||||
|
||||
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):
|
||||
|
||||
```go
|
||||
h := keysat.HashFingerprint(machineUUID)
|
||||
if h != payload.FingerprintHash {
|
||||
log.Fatal("license does not belong to this machine")
|
||||
}
|
||||
```
|
||||
|
||||
## Wire format compatibility
|
||||
|
||||
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.
|
||||
|
||||
## API stability
|
||||
|
||||
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.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see `LICENSE`.
|
||||
Reference in New Issue
Block a user