843ff0e5d7
Glue files not covered by subproject repos: top-level docs, logo, keysat-design-system, and crosscheck tests. Subproject folders are gitignored (each has its own Gitea remote).
222 lines
8.9 KiB
Markdown
222 lines
8.9 KiB
Markdown
# Keysat master keypair — generation, storage, and import
|
|
|
|
This document covers the keypair that signs every Keysat self-license — the
|
|
"Keysat-licenses-Keysat" trust root. It is the single most security-critical
|
|
piece of cryptographic material in the entire project. Anyone with the
|
|
private key can mint Keysat licenses indefinitely. Treat it accordingly.
|
|
|
|
## What this keypair is
|
|
|
|
An Ed25519 keypair generated by Grant on his laptop. The **public** half is
|
|
embedded in the Keysat daemon source code at `licensing-service/src/license_self.rs`
|
|
in the `TRUST_ROOT_PUBKEY_PEM` constant. Any Keysat install with that source
|
|
embedded will accept license keys signed by the corresponding private half,
|
|
and reject everything else.
|
|
|
|
The **private** half is held offline and only used to mint Keysat licenses
|
|
(eventually via a "master Keysat" instance that imports the keypair).
|
|
|
|
## How it was generated
|
|
|
|
```
|
|
openssl genpkey -algorithm Ed25519 -out keysat-master-private.pem
|
|
openssl pkey -in keysat-master-private.pem -pubout -out keysat-master-public.pem
|
|
```
|
|
|
|
These are the same commands that ran. They produce two files:
|
|
|
|
- `keysat-master-private.pem` — the private key, ~120 bytes
|
|
- `keysat-master-public.pem` — the public key, ~120 bytes
|
|
|
|
## What's already done
|
|
|
|
✅ Public key embedded in `licensing-service/src/license_self.rs` as `TRUST_ROOT_PUBKEY_PEM`.
|
|
The current value is:
|
|
|
|
```
|
|
-----BEGIN PUBLIC KEY-----
|
|
MCowBQYDK2VwAyEAgsromMy4osMJplX1rY0fd4ouS6wfkm/vfeY2gXEQHkA=
|
|
-----END PUBLIC KEY-----
|
|
```
|
|
|
|
If that block ever needs to change (e.g. if the private key leaks and you have
|
|
to rotate), edit `TRUST_ROOT_PUBKEY_PEM` and ship a new package version. Note
|
|
that all previously-issued licenses become unverifiable on the new package —
|
|
plan rotations carefully.
|
|
|
|
## What you need to do
|
|
|
|
### Right now: secure the private key
|
|
|
|
The private-key file is sitting in your home folder (`~/keysat-master-private.pem`).
|
|
Do all of the following before anything else:
|
|
|
|
1. **Make a paper backup.** Open the file in TextEdit, print it on paper (one
|
|
sheet, three lines of base64). Store the printout in a fireproof safe or
|
|
safe-deposit box. Test that the printed copy is legible — small fonts and
|
|
inkjet bleed are common gotchas.
|
|
|
|
2. **Make a digital backup in something secure.** Recommended in rough order:
|
|
|
|
- 1Password / Bitwarden secure note (attach the .pem file). Synced across
|
|
your devices, encrypted at rest.
|
|
- Encrypted USB drive (VeraCrypt or APFS-encrypted disk image), kept offline.
|
|
- GPG-encrypted backup uploaded to a personal cloud bucket.
|
|
|
|
3. **Delete the plaintext file from your laptop's home folder** once backed up.
|
|
|
|
```
|
|
srm ~/keysat-master-private.pem # macOS
|
|
shred -u ~/keysat-master-private.pem # Linux
|
|
```
|
|
|
|
Don't just `rm` it — that leaves the bytes recoverable on most filesystems.
|
|
|
|
### When you're ready: stand up a master Keysat instance
|
|
|
|
The master Keysat is a separate Keysat install whose only job is to mint
|
|
licenses for the Keysat package itself. It runs on its own Start9 (or wherever)
|
|
and uses the private key you generated as its issuer key.
|
|
|
|
The keypair is stored in the daemon's SQLite database (under the `server_keys`
|
|
table), not as a file on disk. To bootstrap the master instance, use the
|
|
admin-only `POST /v1/admin/import-issuer-key` endpoint, added in v0.1.0:15
|
|
specifically for this scenario.
|
|
|
|
Steps:
|
|
|
|
1. Install the Keysat package on a fresh Start9 (or a separate StartOS account
|
|
on the same Start9, if possible). Standard install flow. The daemon will
|
|
generate a throwaway issuer keypair on first boot — that's expected; you'll
|
|
replace it in step 3.
|
|
|
|
2. Grab the admin API key for this fresh instance: StartOS dashboard → Keysat
|
|
service → **Actions → Show admin API key**. Copy the 64-hex-character key.
|
|
|
|
3. From your laptop (the one with `keysat-master-private.pem`), POST the PEM
|
|
to the master Keysat's import-issuer-key endpoint:
|
|
|
|
```
|
|
ADMIN_KEY="paste-the-admin-api-key-here"
|
|
MASTER_KEYSAT="https://your-master-keysat.example" # or LAN URL like https://your-mdns.local:port
|
|
|
|
curl -X POST \
|
|
-H "Authorization: Bearer $ADMIN_KEY" \
|
|
-H "Content-Type: application/x-pem-file" \
|
|
--data-binary @/path/to/keysat-master-private.pem \
|
|
"$MASTER_KEYSAT/v1/admin/import-issuer-key"
|
|
```
|
|
|
|
Expected response:
|
|
|
|
```json
|
|
{
|
|
"ok": true,
|
|
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
|
|
"restart_required": true,
|
|
"message": "Issuer key imported. Restart the Keysat service for the new key to take effect..."
|
|
}
|
|
```
|
|
|
|
The endpoint refuses if any licenses have already been issued by this
|
|
Keysat — a safety guard against accidentally invalidating customer
|
|
keys. Since this is a freshly-installed master instance, it should
|
|
succeed.
|
|
|
|
4. **Stop and start the Keysat service** from the StartOS dashboard. The
|
|
in-memory keypair gets re-loaded from the DB on startup, picking up
|
|
the imported key. Confirm in the logs that the public key now matches
|
|
your `keysat-master-public.pem`.
|
|
|
|
5. In the master Keysat's admin UI, define a product called "Keysat" with your
|
|
intended pricing (e.g. 50,000 sats standard, 250,000 sats patron).
|
|
|
|
6. Define a `default` policy on the Keysat product. Perpetual single-seat is a
|
|
reasonable default. Optionally add entitlements: `["patron"]` for a Patron
|
|
tier.
|
|
|
|
7. The master instance is now ready. Self-redeem a free license for your own
|
|
non-master Keysat installs (use `free_license` discount codes for
|
|
bootstrapping).
|
|
|
|
#### Alternative: SSH if you prefer file-system access
|
|
|
|
If you'd rather not use the import endpoint, you can SSH into the Start9 host
|
|
and edit the SQLite database directly. Less recommended (more error-prone), but
|
|
documented for completeness:
|
|
|
|
```
|
|
# Enable SSH in StartOS Settings → SSH, add your laptop's pubkey, then:
|
|
ssh start9@<your-mdns>.local
|
|
# find the keysat data volume
|
|
sudo find / -name "*.db" -path "*keysat*" 2>/dev/null
|
|
# edit server_keys row 1 with sqlite3
|
|
```
|
|
|
|
This route requires you to UPSERT the right PEM strings into the
|
|
`server_keys` table manually. Stick with the curl-based import endpoint
|
|
unless you specifically need SSH access for some other reason.
|
|
|
|
#### Why this isn't a StartOS button
|
|
|
|
Putting an "Import master signing key" button in every Keysat operator's
|
|
StartOS Actions tab would clutter the UX for the 95% of operators who don't
|
|
need it (their auto-generated issuer key is exactly what they want). The
|
|
import-issuer-key endpoint is admin-only, invisible by default, and called
|
|
exactly once during master setup.
|
|
|
|
### When 100 free first-user licenses are needed
|
|
|
|
In the master instance's admin UI:
|
|
|
|
1. Go to **Discount codes → Create new code**.
|
|
2. Set kind `free_license`, max uses 100, no expiry.
|
|
3. Pick a memorable code: `EARLY100`, `FOUNDERS100`, etc.
|
|
4. Distribute the code via your launch announcement / Twitter / Nostr / mailing
|
|
list. First 100 users redeem at `https://registry.keysat.xyz` (or wherever
|
|
the buy page is hosted by the master instance).
|
|
|
|
## Threat model and rotation plan
|
|
|
|
**If the private key leaks:**
|
|
|
|
- An attacker can mint Keysat licenses indefinitely.
|
|
- Existing customers' licenses are unaffected — the leak doesn't retroactively
|
|
invalidate signatures.
|
|
- Mitigation: rotate. Generate a fresh keypair, edit `TRUST_ROOT_PUBKEY_PEM`,
|
|
ship a new package version, re-issue licenses to existing customers, deprecate
|
|
the leaked key in your customer comms.
|
|
|
|
**If the private key is destroyed (and no backup):**
|
|
|
|
- You can't issue any new licenses ever.
|
|
- Existing customer licenses keep working.
|
|
- Mitigation: rotation as above. Painful but recoverable.
|
|
|
|
This is why the paper backup matters more than anything. Cloud sync is great
|
|
for daily use, but a paper copy in a safe is the resilience floor.
|
|
|
|
## Future: rolling rotation (v0.3+)
|
|
|
|
The current model is hard-cut: one public key, one private key, one rotation
|
|
event invalidates everything. v0.3 will land a rolling-rotation flow:
|
|
|
|
- Daemon embeds *two* trust-root pubkeys — primary and standby.
|
|
- During rotation, the daemon accepts licenses signed by either.
|
|
- After all customers have re-licensed under the new key, the old pubkey is
|
|
retired in the next package version.
|
|
|
|
This makes rotation graceful instead of catastrophic. Until that ships,
|
|
treat the private key as if losing it would mean re-licensing every customer
|
|
(because it would).
|
|
|
|
## Quick reference
|
|
|
|
| Action | Command |
|
|
|---|---|
|
|
| Generate keypair (already done) | `openssl genpkey -algorithm Ed25519 -out keysat-master-private.pem && openssl pkey -in keysat-master-private.pem -pubout -out keysat-master-public.pem` |
|
|
| Inspect public key | `openssl pkey -in keysat-master-public.pem -pubin -text -noout` |
|
|
| Inspect private key (don't normally do this) | `openssl pkey -in keysat-master-private.pem -text -noout` |
|
|
| Securely delete plaintext | macOS: `srm <path>` · Linux: `shred -u <path>` |
|
|
| Verify which key is embedded in daemon | `grep -A4 TRUST_ROOT_PUBKEY_PEM licensing-service-startos/licensing-service/src/license_self.rs` |
|