Scaffolds the server module and everything Phase 0 of PLAN.md calls for: a binary that boots, serves its discovery endpoints and nothing else yet. - config: one YAML file plus per-field WARP_* environment overrides, unknown fields rejected, validation that reports every problem at once - logging and metrics: slog, a Prometheus registry, per-route HTTP instrumentation labelled by pattern rather than path - httputil: the Matrix error envelope, error-returning handlers, CORS, request logging, panic recovery, and a router over http.ServeMux - endpoints: /_matrix/client/versions, /.well-known/matrix/client, /.well-known/matrix/server (gated), /healthz, /metrics, and M_UNRECOGNIZED for everything else - storage: driver registry, shared database/sql base, checksummed migration runner, sqlite and postgres drivers, pocketbase stub - settings: server_settings table behind a cached service, so the federation and registration switches change without a restart - federation: the gate that makes federation a runtime mode - cmd/warp: serve, migrate, generate-keys, version - Makefile, golangci-lint v2, GitHub Actions CI (Linux and Windows tests, race on Linux, storage tests against PostgreSQL, lint, boot smoke test), Dockerfile, example config and docs Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
3.7 KiB
Go
148 lines
3.7 KiB
Go
package signing
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestGenerateSaveLoadRoundTrip(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "keys", "signing.key")
|
|
|
|
key, err := Generate("")
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
if !strings.HasPrefix(key.ID, "a_") {
|
|
t.Errorf("generated key ID %q should look like a_xxxxx", key.ID)
|
|
}
|
|
if got, want := key.KeyID(), "ed25519:"+key.ID; got != want {
|
|
t.Errorf("KeyID() = %q, want %q", got, want)
|
|
}
|
|
if err := key.Save(path); err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
|
|
loaded, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if loaded.ID != key.ID {
|
|
t.Errorf("key ID = %q, want %q", loaded.ID, key.ID)
|
|
}
|
|
if !loaded.Private.Equal(key.Private) {
|
|
t.Error("loaded private key differs from the generated one")
|
|
}
|
|
if loaded.PublicBase64() != key.PublicBase64() {
|
|
t.Error("loaded public key differs from the generated one")
|
|
}
|
|
|
|
msg := []byte("the quick brown fox")
|
|
if !ed25519.Verify(loaded.Public, msg, ed25519.Sign(loaded.Private, msg)) {
|
|
t.Error("loaded key does not sign and verify")
|
|
}
|
|
}
|
|
|
|
func TestSaveRefusesToOverwrite(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "signing.key")
|
|
key, err := Generate("test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := key.Save(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = key.Save(path)
|
|
if err == nil {
|
|
t.Fatal("expected the second Save to fail")
|
|
}
|
|
if !strings.Contains(err.Error(), "refusing to overwrite") {
|
|
t.Errorf("error should explain itself, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSaveFormat(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "signing.key")
|
|
key, err := Generate("abc")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := key.Save(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fields := strings.Fields(string(data))
|
|
if len(fields) != 3 || fields[0] != "ed25519" || fields[1] != "abc" {
|
|
t.Fatalf("unexpected key file contents: %q", string(data))
|
|
}
|
|
if strings.Contains(fields[2], "=") {
|
|
t.Error("the seed should be unpadded base64")
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsGarbage(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cases := map[string]string{
|
|
"empty": "",
|
|
"only a comment": "# nothing here\n",
|
|
"wrong field count": "ed25519 abc\n",
|
|
"wrong algorithm": "rsa abc AAAA\n",
|
|
"bad key id": "ed25519 a:b AAAA\n",
|
|
"short seed": "ed25519 abc AAAA\n",
|
|
}
|
|
for name, body := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
path := filepath.Join(dir, strings.ReplaceAll(name, " ", "_")+".key")
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Load(path); err == nil {
|
|
t.Fatalf("expected Load to reject %q", body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateKeyID(t *testing.T) {
|
|
for _, ok := range []string{"a_abc", "ABC123", "x_1"} {
|
|
if err := ValidateKeyID(ok); err != nil {
|
|
t.Errorf("ValidateKeyID(%q) = %v, want nil", ok, err)
|
|
}
|
|
}
|
|
for _, bad := range []string{"", "ed25519:abc", "a-b", "a b", "ä"} {
|
|
if err := ValidateKeyID(bad); err == nil {
|
|
t.Errorf("ValidateKeyID(%q) = nil, want an error", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadIgnoresCommentsAndBlankLines(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "signing.key")
|
|
key, err := Generate("real")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := key.Save(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, append([]byte("# warp signing key\n\n"), body...), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if loaded.ID != "real" {
|
|
t.Errorf("key ID = %q, want %q", loaded.ID, "real")
|
|
}
|
|
}
|