# 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>` &middot; 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` |
