diff --git a/docker-compose.yml b/docker-compose.yml index 6137e84e..c2a79bc1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -148,7 +148,7 @@ services: - pg_password - pg_dbname entrypoint: /run-migration.sh - command: "" + command: ["up"] depends_on: api-db: condition: service_healthy diff --git a/pkg/auth/schema_test.go b/pkg/auth/schema_test.go new file mode 100644 index 00000000..6b459b00 --- /dev/null +++ b/pkg/auth/schema_test.go @@ -0,0 +1,29 @@ +package auth + +import ( + "reflect" + "testing" +) + +func TestSchemaConstants(t *testing.T) { + tests := []struct { + name string + got any + want any + }{ + {"PublicKeyPrefix", PublicKeyPrefix, "pk_"}, + {"SecretKeyPrefix", SecretKeyPrefix, "sk_"}, + {"TokenMinLength", TokenMinLength, 16}, + {"AccountNameMinLength", AccountNameMinLength, 5}, + {"EncryptionKeyLength", EncryptionKeyLength, 32}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if !reflect.DeepEqual(tt.got, tt.want) { + t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.want) + } + }) + } +} diff --git a/pkg/cli/colour_test.go b/pkg/cli/colour_test.go new file mode 100644 index 00000000..edf9717b --- /dev/null +++ b/pkg/cli/colour_test.go @@ -0,0 +1,33 @@ +package cli + +import ( + "reflect" + "testing" +) + +func TestColourConstants(t *testing.T) { + tests := []struct { + name string + got any + want any + }{ + {"Reset", Reset, "\033[0m"}, + {"RedColour", RedColour, "\033[31m"}, + {"GreenColour", GreenColour, "\033[32m"}, + {"YellowColour", YellowColour, "\033[33m"}, + {"BlueColour", BlueColour, "\033[34m"}, + {"MagentaColour", MagentaColour, "\033[35m"}, + {"CyanColour", CyanColour, "\033[36m"}, + {"GrayColour", GrayColour, "\033[37m"}, + {"WhiteColour", WhiteColour, "\033[97m"}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if !reflect.DeepEqual(tt.got, tt.want) { + t.Errorf("%s = %q, want %q", tt.name, tt.got, tt.want) + } + }) + } +} diff --git a/pkg/middleware/token_middleware.go b/pkg/middleware/token_middleware.go index 99c23575..14fced79 100644 --- a/pkg/middleware/token_middleware.go +++ b/pkg/middleware/token_middleware.go @@ -256,8 +256,9 @@ func (t TokenCheckMiddleware) shallReject(logger *slog.Logger, accountName, publ return t.getUnauthenticatedError() } - // Compute local signature over canonical request and compare in constant time (hash to fixed-length first) - localSignature := auth.CreateSignatureFrom(canonical, token.PublicKey) //@todo Change! + // Compute local signature over canonical request using the account's secret key + // and compare in constant time (hash to fixed-length first) + localSignature := auth.CreateSignatureFrom(canonical, token.SecretKey) hSig := sha256.Sum256([]byte(strings.TrimSpace(signature))) hLocal := sha256.Sum256([]byte(localSignature)) diff --git a/pkg/middleware/token_middleware_additional_test.go b/pkg/middleware/token_middleware_additional_test.go index 19535a3c..ac694a3b 100644 --- a/pkg/middleware/token_middleware_additional_test.go +++ b/pkg/middleware/token_middleware_additional_test.go @@ -97,7 +97,7 @@ func TestTokenMiddleware_PublicTokenMismatch(t *testing.T) { next := func(w http.ResponseWriter, r *http.Request) *pkgHttp.ApiError { return nil } handler := tm.Handle(next) - req := makeSignedRequest(t, http.MethodGet, "https://api.test.local/v1/x", "", seed.AccountName, "wrong-"+seed.PublicKey, seed.SecretKey, time.Now(), "nonce-mm", "req-mm") + req := makeSignedRequest(t, http.MethodGet, "https://api.test.local/v1/x", "", seed.AccountName, "wrong-"+seed.PublicKey, seed.SecretKey, time.Now(), "nonce-mm", "req-mm") req.Header.Set("X-Forwarded-For", "1.1.1.1") rec := httptest.NewRecorder() if err := handler(rec, req); err == nil || err.Status != http.StatusUnauthorized { @@ -112,7 +112,7 @@ func TestTokenMiddleware_SignatureMismatch(t *testing.T) { next := func(w http.ResponseWriter, r *http.Request) *pkgHttp.ApiError { return nil } handler := tm.Handle(next) - req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "body", seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), "nonce-sig", "req-sig") + req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "body", seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), "nonce-sig", "req-sig") req.Header.Set("X-Forwarded-For", "1.1.1.1") req.Header.Set("X-API-Signature", req.Header.Get("X-API-Signature")+"tamper") rec := httptest.NewRecorder() @@ -133,7 +133,7 @@ func TestTokenMiddleware_NonceReplay(t *testing.T) { } handler := tm.Handle(next) - req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "{}", seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), "nonce-rp", "req-rp") + req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "{}", seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), "nonce-rp", "req-rp") req.Header.Set("X-Forwarded-For", "1.1.1.1") rec := httptest.NewRecorder() if err := handler(rec, req); err != nil { @@ -161,22 +161,22 @@ func TestTokenMiddleware_RateLimiter(t *testing.T) { // Pre-warm limiter by sending invalid signature requests up to the limit for i := 0; i < tm.maxFailPerScope; i++ { - req := makeSignedRequest( - t, http.MethodGet, "https://api.test.local/v1/rl", "", - seed.AccountName, seed.PublicKey, "wrong-secret", time.Now(), - fmt.Sprintf("nonce-rl-%d", i), fmt.Sprintf("req-rl-%d", i), - ) + req := makeSignedRequest( + t, http.MethodGet, "https://api.test.local/v1/rl", "", + seed.AccountName, seed.PublicKey, "wrong-secret", time.Now(), + fmt.Sprintf("nonce-rl-%d", i), fmt.Sprintf("req-rl-%d", i), + ) req.Header.Set("X-Forwarded-For", "9.9.9.9") rec := httptest.NewRecorder() _ = handler(rec, req) // ignore errors while warming } // Next request with valid signature should be rate limited - req := makeSignedRequest( - t, http.MethodGet, "https://api.test.local/v1/rl", "", - seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), - "nonce-rl-final", "req-rl-final", - ) + req := makeSignedRequest( + t, http.MethodGet, "https://api.test.local/v1/rl", "", + seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), + "nonce-rl-final", "req-rl-final", + ) req.Header.Set("X-Forwarded-For", "9.9.9.9") rec := httptest.NewRecorder() err := handler(rec, req) diff --git a/pkg/middleware/token_middleware_test.go b/pkg/middleware/token_middleware_test.go index 18b00b57..9fa3e13a 100644 --- a/pkg/middleware/token_middleware_test.go +++ b/pkg/middleware/token_middleware_test.go @@ -209,7 +209,8 @@ func generate32(t *testing.T) []byte { return buf } -// makeSignedRequest builds a request with required headers and a valid HMAC signature over the canonical string. +// makeSignedRequest builds a request with required headers and a valid HMAC +// signature over the canonical string using the account's secret key. func makeSignedRequest(t *testing.T, method, rawURL, body, account, public, secret string, ts time.Time, nonce, reqID string) *http.Request { t.Helper() var bodyBuf *bytes.Buffer @@ -226,8 +227,8 @@ func makeSignedRequest(t *testing.T, method, rawURL, body, account, public, secr req.Header.Set("X-API-Nonce", nonce) bodyHash := portal.Sha256Hex([]byte(body)) - canonical := portal.BuildCanonical(method, req.URL, account, public, req.Header.Get("X-API-Timestamp"), nonce, bodyHash) - sig := auth.CreateSignatureFrom(canonical, secret) + canonical := portal.BuildCanonical(method, req.URL, account, public, req.Header.Get("X-API-Timestamp"), nonce, bodyHash) + sig := auth.CreateSignatureFrom(canonical, secret) req.Header.Set("X-API-Signature", sig) return req } @@ -273,12 +274,12 @@ func TestTokenMiddleware_DB_Integration(t *testing.T) { http.MethodPost, "https://api.test.local/v1/posts?z=9&a=1", "{\"title\":\"ok\"}", - seed.AccountName, - seed.PublicKey, - seed.SecretKey, - now, - "nonce-1", - "req-001", + seed.AccountName, + seed.PublicKey, + seed.SecretKey, + now, + "nonce-1", + "req-001", ) rec := httptest.NewRecorder() if err := handler(rec, req); err != nil { @@ -294,12 +295,12 @@ func TestTokenMiddleware_DB_Integration(t *testing.T) { http.MethodGet, "https://api.test.local/v1/ping", "", - "no-such-user", - seed.PublicKey, - seed.SecretKey, - now, - "nonce-2", - "req-002", + "no-such-user", + seed.PublicKey, + seed.SecretKey, + now, + "nonce-2", + "req-002", ) rec = httptest.NewRecorder() if err := handler(rec, reqUnknown); err == nil || err.Status != http.StatusUnauthorized { @@ -350,12 +351,12 @@ func TestTokenMiddleware_DB_Integration_HappyPath(t *testing.T) { http.MethodPost, "https://api.test.local/v1/resource?b=2&a=1", "{\"x\":123}", - seed.AccountName, - seed.PublicKey, - seed.SecretKey, - time.Now(), - "n-happy-1", - "rid-happy-1", + seed.AccountName, + seed.PublicKey, + seed.SecretKey, + time.Now(), + "n-happy-1", + "rid-happy-1", ) rec := httptest.NewRecorder() if err := handler(rec, req); err != nil { @@ -416,12 +417,12 @@ func TestTokenMiddleware_RejectsFutureTimestamps(t *testing.T) { http.MethodGet, "https://api.test.local/v1/test", "", - seed.AccountName, - seed.PublicKey, - seed.SecretKey, - futureTime, - "n-future-1", - "rid-future-1", + seed.AccountName, + seed.PublicKey, + seed.SecretKey, + futureTime, + "n-future-1", + "rid-future-1", ) rec := httptest.NewRecorder()