No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-16 12:17:46 +02:00
appindex updated platform/arch checks to be not required for some packages 2026-09-16 12:17:46 +02:00
cmd updated platform/arch checks to be not required for some packages 2026-09-16 12:17:46 +02:00
upload added srepo put 2026-09-07 23:07:41 +02:00
go.mod Updated repo spec & tor/socks5 for package validation 2026-07-04 22:45:03 +02:00
go.sum Updated repo spec & tor/socks5 for package validation 2026-07-04 22:45:03 +02:00
README.md added srepo put 2026-09-07 23:07:41 +02:00

Logo

Spitfire Repository Manager (or SRepo)

A Go library and CLI tool for managing APPINDEX based repositories.

This project currently handles:

  • artifact compression (directory -> tar.xz)
  • artifact uploads to S3
  • APPINDEX generation/updates
  • Ed25519 signing with an offline ROOT / online index-key split

And It's primary usage is to manage Spitfire Browser's repositaries.

Prerequisites

  • Go version 1.25 or newer

CMD Install

go install weforge.xyz/Spitfire/Repo/cmd@latest

Commands

srepo keygen

Generates a new Ed25519 index keypair (the online key that signs APPINDEX). Store the private key at the signer as ED25519_PRIVATE_KEY. Clients never pin this key: it is valid only while listed as a k: key in the signed ROOT (see srepo root and Key distribution).

srepo keygen
# Private key (set as ED25519_PRIVATE_KEY):   <base64>
# Public key (list as k: via srepo root ...): <base64>

srepo pubkey

Prints the public key derived from private ED25519_PRIVATE_KEY.

srepo pubkey
# Public key: <base64>

srepo appindex

Update a local APPINDEX entry, sign it, and write APPINDEX and APPINDEX.sig to disk. Pass --upload to also push both files to S3.

# generates files locally
srepo appindex \
  --appid xyz.example.MyApp \
  --name "My App" \
  --version 2025.05.06 \
  --arch amd64 \
  --platform linux \
  --compressed-file myapp.tar.xz \
  --uncompressed-file myapp.tar \
  --download-path amd64/2025.05.05/myapp.tar.xz \
  --description "Nightly build of My App" \
  --website https://example.xyz \
  --license MPL-2.0

# with upload
srepo appindex --upload \
  --appid xyz.example.MyApp \
  ...

srepo compress

Compress a directory to tar.xz locally.

srepo compress \
  --dir ./build \
  --output ./myapp-amd64-linux.tar.xz

srepo upload

Compress a directory, upload the artifact to S3, and update APPINDEX in one step.

# full pipeline (compress + upload + APPINDEX)
srepo upload \
  --dir ./build \
  --object xyz.example.MyApp/amd64/2026.05.07/myapp-amd64-linux.tar.xz \
  --appid xyz.example.MyApp \
  --name "My App" \
  --arch amd64 \
  --platform linux \
  --license MPL-2.0

# pre-compressed artifact
srepo upload --no-compress \
  --file ./myapp-amd64-linux.tar.xz \
  --object xyz.example.MyApp/amd64/2026.05.07/myapp-amd64-linux.tar.xz \
  --appid xyz.example.MyApp --name "My App" --arch amd64 --platform linux

PS: uncompressed checksum and size will be omitted from the APPINDEX entry.

srepo put

Upload one file as is, not signed, not compressed, not in APPINDEX.

srepo put --file ./spitfire-launcher.exe --object installer.exe
# https://<bucket host>/installer.exe

srepo resign

Re-sign an unchanged APPINDEX with a fresh v:/E: header, run it on a schedule (by default valid for 21 days) so the index never expires. Refuses an index whose current signature doesn't verify, and the serial only ever increases.

# scheduled on the signer: read live index from S3, re-sign, push back
srepo resign --remote --upload          # --valid-days 21 to change the E: window

# first resign after an index-key rotation
srepo resign --remote --upload --trust-pubkey "<old index pubkey b64>"

srepo root

Manage the repo's ROOT trust metadata, its reccomended to run these offline/separate machine. Each run writes ROOT, ROOT.sig and an immutable root/<version>.ROOT{,.sig} upload all of them to repositary, never overwrite a versioned copy.

srepo root keygen --out root-a          # writes root-a.key (keep safe and offline!) + root-a.pub
srepo root init --key-file root-a.key \
  --expiry-days 365 \
  --index-keys "<index pubkey b64>" \
  --root-pubs "<standby root pubkey b64>"   # optional backup key

# annual re-sign (bumps version + expiry, --expiry-days to change it)
srepo root update --key-file root-a.key

# revoke a stolen index key (replaces the k: list, clients pick it up on next check)
srepo root update --key-file root-a.key --index-keys "<new index pubkey b64>"

# check and automatically publish (online machine, public files only, needs S3_* env)
srepo root upload --dir ./ceremony-output

srepo verify

Verify an APPINDEX signature against the repo's published public key.

srepo verify --pubkey "$(cat pubkey.b64)" --appindex ./APPINDEX --sig ./APPINDEX.sig

Domain ownership (y:)

appindex and upload check at build time whether the domain derived from app ID lists that ID in https://<domain>/.well-known/srepo.txt and record a hit in the signed entry as y:1.

--no-verify-domain skips it,

--tor / --socks5 host:port route the fetch through a SOCKS5 proxy.

The domain is derived from the app ID by reversing all but the last component:

  • xyz.weforge.Spitfire -> weforge.xyz
  • io.github.username.App -> username.github.io

To publish ownership, host a plain text file at https://<domain>/.well-known/srepo.txt listing your app IDs one per line:

xyz.weforge.Spitfire
xyz.weforge.Luncher
xyz.weforge.Installer

Key distribution

Trust is a two-level chain (a minimal TUF-style split):

  1. Root keys (r:) - kept separate (srepo root keygen). The root public key is the only key built into the client - everything else is trusted only because a root key signed it. Root keys sign only the ROOT file.
  2. Index keys (k:) - the online keys that sign APPINDEX, not pinned by clients, the valid list comes from the signed ROOT. Rotating or revoking one = publish a new ROOT, no client update.

ROOT file

Published at the repo root next to APPINDEX, signed into ROOT.sig (raw 64-byte Ed25519 by a root key). Every version is also kept at root/<version>.ROOT{,.sig}.

v:1                   monotonic version, starts at 1
E:1788115200          expiry (unix seconds)
r:<base64 pubkey>     root public key (repeatable)
k:<base64 pubkey>     valid APPINDEX signing key (repeatable)

Rotation

  • Index key (routine, or compromise recovery): srepo keygen, swap ED25519_PRIVATE_KEY at the signer, offline srepo root update --key-file root-a.key --index-keys <new pubkey>, upload, then srepo resign --remote --upload --trust-pubkey <old pubkey>. Clients revoke the old key the moment they see the new ROOT — no client release.
  • Root key (rare): srepo root update ... --root-pubs <new set> signed by the current root key; clients walk the chain. Also update the root keys built into SPM/PackageStore so fresh installs start from the new key.
  • Root re-sign (annual): srepo root update --key-file root-a.key before the ROOT E: passes, or every client starts refusing the repo.
  • Root key stolen or lost with no backup: last resort — ship new client binaries with new built-in root keys.

Environment variables

Variable Description
S3_ENDPOINT S3-compatible endpoint URL
S3_BUCKET Bucket name
S3_ACCESS_KEY_ID Access key ID
S3_SECRET_ACCESS_KEY Secret access key
ED25519_PRIVATE_KEY Base64-encoded Ed25519 private key (for signing)

APPINDEX format

Each entry describes one "app" for one arch+platform. Entries are separated by a blank line, always starting with C: and ending with c:. The whole file is signed - see APPINDEX.sig.

Header

The first two lines of APPINDEX, written by srepo at signing time and covered by APPINDEX.sig:

v:1756425600    serial - unix signing time, never decreases
E:1758240000    expiry - unix seconds, default 21 days (--valid-days)

C:...           first entry

Clients refuse an expired index or a serial lower than the last accepted one, so an unchanged index must still be re-signed before E: (srepo resign). srepo derives the serial as max(now, previous + 1).

Path convention

All path fields (d:, n:, G:, i:, s:) are relative. The client reconstructs the full URL as:

{repoURL}/{AppID}/{path field value}

Example - APPINDEX at https://repo.example.com/, app xyz.weforge.Spitfire:

  • d:amd64/2025.05.06/xyz.weforge.Spitfire-amd64-linux.tar.xz resolves to:
  • https://repo.example.com/xyz.weforge.Spitfire/amd64/2025.05.06/xyz.weforge.Spitfire-amd64-linux.tar.xz

  • i:assets/icon.svg resolves to:
  • https://repo.example.com/xyz.weforge.Spitfire/assets/icon.svg

Field spec

Field Description Required
C: SHA-256 hex of the compressed artifact - marks entry start yes
P: App ID - reverse-domain, e.g. xyz.weforge.Spitfire yes
N: Display name - e.g. Spitfire Browser yes
V: Version - YYYY.MM.DD or vX.Y.Z semver yes
A: Architecture - amd64, arm64, … yes
p: Platform - linux, windows, … yes
k: Kind - see Package kinds yes
S: Compressed size in bytes yes
I: Uncompressed / installed size in bytes yes
d: Download path (relative) yes
t: Unix timestamp - publish time yes
c: SHA-256 hex of the uncompressed artifact - marks entry end yes
n: Release notes .md path (relative) no
X: Short description - inline one-liner no
G: Long description .md path (relative) no
U: Project website URL no
B: Bug tracker / issue reports URL no
x: Donation / support URL no
L: License - SPDX identifier no
m: Maintainer no
D: Dependencies - comma-separated app IDs, omit if none no
T: Tags - comma-separated free-form search keywords no
i: Icon path (relative) no
s: Screenshot paths - comma-separated (relative) no
y: Verified - 1 when the app ID's domain ownership at build time. Set automatically by appindex/upload no

Package kinds

k: tells the client how to install a package.

Kind Payload Installed as
extension .xpi AddonManager
statictheme .xpi AddonManager
layout .tar.xz userChrome.css layout
config .tar.xz with a user.js managed pref block
bundle none - groups its D: members installs each member
browser, launcher, installer .tar.xz full applications
wallpaper .tar.xz new tab background, image or video
soundpack .tar.xz UI event sounds
keysounds .tar.xz typing sounds
music .tar.xz looping ambient layers
webstyle .tar.xz per-site CSS
accent .tar.xz UI accent colors

Example entry

C:a6af7ebb5c1382084704be0b5714ab026c819d63c2d3e4f5a6b7c8d9e0f1a2b3
P:xyz.weforge.Spitfire
N:Spitfire Browser
V:2025.05.06
A:amd64
p:linux
k:browser
S:838594187
I:3595780372
d:amd64/2025.05.06/xyz.weforge.Spitfire-amd64-linux.tar.xz
X:Fast, private Firefox fork
G:description.md
U:https://spitfirebrowser.xyz/
B:https://weforge.xyz/Spitfire/Browser/issues
x:https://spitfirebrowser.xyz/donate
L:MPL-2.0
m:Internet Addict
T:browser,experimental,testing
t:1746518400
i:assets/icon.svg
s:assets/screenshots/1.png,assets/screenshots/2.png
c:b7c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1

Use as a Go module

Add the dependency:

go get weforge.xyz/Spitfire/Repo@latest

APPINDEX - generate and sign

import (
    "os"
    "time"
    "weforge.xyz/Spitfire/Repo/appindex"
)

// Read existing index (nil = start fresh)
existing, _ := os.ReadFile("APPINDEX")

entry := appindex.Build(appindex.Entry{
    AppID:          "xyz.example.MyApp",
    DisplayName:    "My App",
    Version:        "2025.05.06",
    Arch:           "amd64",
    Platform:       "linux",
    CompSize:       1234567,
    UncompSize:     4567890,
    DownloadPath:   "amd64/2025.05.05/myapp.tar.xz",
    CompChecksum:   "sha256hex...",
    UncompChecksum: "sha256hex...",
    Description:    "My build",
    Website:        "https://example.xyz",
    License:        "MPL-2.0",
})

updated := appindex.Finalize(
    appindex.Update(existing, "xyz.example.MyApp", "amd64", "linux", entry),
    21*24*time.Hour,
    appindex.ParseHeader(existing).Serial)

sig, err := appindex.Sign(updated, os.Getenv("ED25519_PRIVATE_KEY"))

os.WriteFile("APPINDEX", updated, 0o644)
os.WriteFile("APPINDEX.sig", sig, 0o644)

APPINDEX - verify a signature

import "weforge.xyz/Spitfire/Repo/appindex"

data, _ := os.ReadFile("APPINDEX")
sig, _  := os.ReadFile("APPINDEX.sig")

ok, err := appindex.Verify(data, sig, publicKeyB64)

APPINDEX - checksum a file

checksum, err := appindex.SHA256File("myapp.tar.xz")

Keys - generate a keypair

privateKeyB64, publicKeyB64, err := appindex.GenerateKey()

Keys - derive public key from private key

publicKeyB64, err := appindex.PublicKey(privateKeyB64)

Upload - upload files

import (
    "context"
    "weforge.xyz/Spitfire/Repo/upload"
)

// reads S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY from env
client, err := upload.NewFromEnv()

ctx := context.Background()

// upload a local file
client.Upload(ctx, "xyz.example.MyApp/amd64/2025.05.05/myapp.tar.xz", "./myapp.tar.xz")

// upload bytes directly (e.g. a freshly signed APPINDEX)
client.UploadBytes(ctx, "APPINDEX", updated)
client.UploadBytes(ctx, "APPINDEX.sig", sig)

// download to memory (returns nil, nil if key not found)
data, err := client.Download(ctx, "APPINDEX")

Repository structure

example.com/
├── APPINDEX
├── APPINDEX.sig
├── ROOT
├── ROOT.sig
├── root/
│   ├── 1.ROOT
│   ├── 1.ROOT.sig
│   ├── 2.ROOT
│   └── 2.ROOT.sig
└── xyz.example.MyApp/
    └── amd64/
        └── 2025.05.05/
            └── xyz.example.MyApp-amd64-linux.tar.xz

License

Logo