diff --git a/go.mod b/go.mod index 11a7c36fc249b635ddf55c2171df4cc7f03ddbd8..69ae3ebaa2cb95c64334b6575a9bcb66da8035f3 100644 --- a/go.mod +++ b/go.mod @@ -3,35 +3,16 @@ go 1.25.0 require ( - github.com/alecthomas/assert/v2 v2.11.0 + code.superseriousbusiness.org/httpsig v1.5.0 github.com/go-chi/chi/v5 v5.2.3 github.com/google/go-jsonnet v0.21.0 github.com/peterbourgon/ff/v3 v3.4.0 - github.com/yaronf/httpsign v0.4.1 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/alecthomas/repr v0.4.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/dunglas/httpsfv v1.0.2 // indirect - github.com/goccy/go-json v0.10.3 // indirect - github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/lestrrat-go/blackmagic v1.0.4 // indirect - github.com/lestrrat-go/dsig v1.0.0 // indirect - github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.6 // indirect - github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/jwx/v2 v2.1.2 // indirect - github.com/lestrrat-go/jwx/v3 v3.0.12 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect - github.com/lestrrat-go/option/v2 v2.0.0 // indirect - github.com/segmentio/asm v1.2.1 // indirect - github.com/valyala/fastjson v1.6.4 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/sys v0.37.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/sys v0.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index f80a6722765844e1ae2fae61c8dd18d1ff69e2b6..23b4fbf01508d9e5f7d348d3c0fe0e14c2a4bad0 100644 --- a/go.sum +++ b/go.sum @@ -1,74 +1,25 @@ -github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= -github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= -github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +code.superseriousbusiness.org/httpsig v1.5.0 h1:jw/qc//yYWSoOYytTZXHvW7yh8kceCipNIBfUeXQghA= +code.superseriousbusiness.org/httpsig v1.5.0/go.mod h1:i2AKpj/WbA/o/UTvia9TAREzt0jP1AH3T1Uxjyhdzlw= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-jsonnet v0.21.0 h1:43Bk3K4zMRP/aAZm9Po2uSEjY6ALCkYUVIcz9HLGMvA= github.com/google/go-jsonnet v0.21.0/go.mod h1:tCGAu8cpUpEZcdGMmdOu37nh8bGgqubhI5v2iSk3KJQ= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= -github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= -github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= -github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo= -github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= -github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= -github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI= -github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.2 h1:6poete4MPsO8+LAEVhpdrNI4Xp2xdiafgl2RD89moBc= -github.com/lestrrat-go/jwx/v2 v2.1.2/go.mod h1:pO+Gz9whn7MPdbsqSJzG8TlEpMZCwQDXnFJ+zsUVh8Y= -github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg= -github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= -github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= -github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= -github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= -github.com/yaronf/httpsign v0.4.1 h1:q2yLLkL4+8G9VmOBR6SIsD4+00ncjt+GyadGA3weY0Y= -github.com/yaronf/httpsign v0.4.1/go.mod h1:euOXi3++HLtx5YlsJEWcIzF3ztK4TL2M2F0Wg3KL+V0= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/main.go b/main.go index 897822f4e2e5ec3f75a4e0465c16a1e3fcf17fc0..a29b1b1cc4def4c8ec11d12fc856311ff5637f2e 100644 --- a/main.go +++ b/main.go @@ -14,12 +14,12 @@ "net/http" "os" "strings" + "code.superseriousbusiness.org/httpsig" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/google/go-jsonnet" "github.com/peterbourgon/ff/v3" "github.com/peterbourgon/ff/v3/ffyaml" - "github.com/yaronf/httpsign" "gopkg.in/yaml.v3" ) @@ -31,8 +31,6 @@ logLevel slog.Level logJSON bool } - -const magicKey = "woodpcecker-ci-extensions" func maine() error { var args args @@ -85,12 +83,6 @@ if err != nil { return err } - addr := fmt.Sprintf(":%d", args.port) - fmt.Printf("Listening at http://localhost%s\n", addr) - return http.ListenAndServe(addr, mux(pubKey)) -} - -func mux(pubKey ed25519.PublicKey) http.Handler { mux := chi.NewMux() mux.Use(middleware.Logger) @@ -133,14 +125,13 @@ } json.NewEncoder(w).Encode(Request{Configs: configs}) }) - return mux + addr := fmt.Sprintf(":%d", args.port) + fmt.Printf("Listening at http://localhost%s\n", addr) + return http.ListenAndServe(addr, mux) } func publicKey(raw string) (ed25519.PublicKey, error) { pemblock, _ := pem.Decode([]byte(raw)) - if pemblock == nil { - return nil, errors.New("failed to decode PEM block") - } b, err := x509.ParsePKIXPublicKey(pemblock.Bytes) if err != nil { @@ -156,17 +147,11 @@ return pubKey, nil } func verify(pubKey ed25519.PublicKey, r *http.Request) error { - verifier, err := httpsign.NewEd25519Verifier(pubKey, httpsign.NewVerifyConfig(), httpsign.Headers("@request-target", "content-digest")) - if err != nil { - return fmt.Errorf("could not create new http signing verifier: %w", err) - } - - err = httpsign.VerifyRequest(magicKey, *verifier, r) + verifier, err := httpsig.NewVerifier(r) if err != nil { - return fmt.Errorf("could not verify request: %w", err) + return err } - - return nil + return verifier.Verify(pubKey, httpsig.ED25519) } func verifyMiddleware(pubKey ed25519.PublicKey) func(http.Handler) http.Handler { diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 1e563ffd4089ee6081746c6b315e3f52ec568214..0000000000000000000000000000000000000000 --- a/main_test.go +++ /dev/null @@ -1,398 +0,0 @@ -package main - -import ( - "bytes" - "crypto/ed25519" - "crypto/x509" - "encoding/json" - "encoding/pem" - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/alecthomas/assert/v2" - "github.com/yaronf/httpsign" -) - -func generateTestKeys(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) { - t.Helper() - - pub, priv, err := ed25519.GenerateKey(nil) - assert.NoError(t, err) - - return pub, priv -} - -func encodePEMPublicKey(t *testing.T, pub ed25519.PublicKey) string { - t.Helper() - - b, err := x509.MarshalPKIXPublicKey(pub) - assert.NoError(t, err) - - pemBlock := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: b, - } - - return string(pem.EncodeToMemory(pemBlock)) -} - -func signRequest(t *testing.T, req *http.Request, priv ed25519.PrivateKey, body []byte) { - t.Helper() - - signer, err := httpsign.NewEd25519Signer(priv, httpsign.NewSignConfig(), httpsign.Headers("@request-target", "content-digest")) - assert.NoError(t, err) - - nop := io.NopCloser(bytes.NewReader(body)) - digest, err := httpsign.GenerateContentDigestHeader(&nop, []string{httpsign.DigestSha256}) - assert.NoError(t, err) - req.Header.Set("Content-Digest", digest) - - signatureInput, signature, err := httpsign.SignRequest(magicKey, *signer, req) - assert.NoError(t, err) - req.Header.Set("Signature-Input", signatureInput) - req.Header.Set("Signature", signature) -} - -func TestPublicKey(t *testing.T) { - tests := []struct { - name string - input string - wantErr bool - }{ - { - name: "valid ed25519 public key", - input: func() string { - pub, _ := generateTestKeys(t) - return encodePEMPublicKey(t, pub) - }(), - wantErr: false, - }, - { - name: "empty string", - input: "", - wantErr: true, - }, - { - name: "invalid PEM", - input: "not a valid pem", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - key, err := publicKey(tt.input) - if tt.wantErr { - assert.Error(t, err) - assert.Zero(t, key) - } else { - assert.NoError(t, err) - assert.NotZero(t, key) - } - }) - } -} - -func TestGetRoot(t *testing.T) { - pub, _ := generateTestKeys(t) - handler := mux(pub) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - w := httptest.NewRecorder() - - handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "send me some jsonnet", w.Body.String()) -} - -func TestPostRootWithoutSignature(t *testing.T) { - pub, _ := generateTestKeys(t) - handler := mux(pub) - - reqBody := Request{ - Configs: []Config{ - { - Name: "test.yaml", - Data: "key: value", - }, - }, - } - body, err := json.Marshal(reqBody) - assert.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusUnauthorized, w.Code) - - var resp map[string]string - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(t, err) - assert.Equal(t, "Failed to verify request", resp["error"]) -} - -func TestPostRootWithInvalidJSON(t *testing.T) { - pub, priv := generateTestKeys(t) - handler := mux(pub) - - body := []byte(`{invalid json}`) - - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - signRequest(t, req, priv, body) - - w := httptest.NewRecorder() - handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusBadRequest, w.Code) - - var resp map[string]string - err := json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(t, err) - assert.Equal(t, "could not decode JSON", resp["error"]) -} - -func TestPostYAMLPassthrough(t *testing.T) { - pub, priv := generateTestKeys(t) - handler := mux(pub) - - reqBody := Request{ - Configs: []Config{ - { - Name: "test.yaml", - Data: "key: value\nnumber: 42", - }, - }, - } - body, err := json.Marshal(reqBody) - assert.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - signRequest(t, req, priv, body) - - w := httptest.NewRecorder() - handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var resp Request - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(t, err) - assert.Equal(t, 1, len(resp.Configs)) - assert.Equal(t, "test.yaml", resp.Configs[0].Name) - assert.Equal(t, "key: value\nnumber: 42", resp.Configs[0].Data) -} - -func TestPostJsonnetProcessing(t *testing.T) { - pub, priv := generateTestKeys(t) - handler := mux(pub) - - reqBody := Request{ - Configs: []Config{ - { - Name: "test.jsonnet", - Data: `{"key": "value", "number": 42}`, - }, - }, - } - body, err := json.Marshal(reqBody) - assert.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - signRequest(t, req, priv, body) - - w := httptest.NewRecorder() - handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var resp Request - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(t, err) - assert.Equal(t, 1, len(resp.Configs)) - assert.Equal(t, "test.jsonnet", resp.Configs[0].Name) - assert.Contains(t, resp.Configs[0].Data, "key: value") - assert.Contains(t, resp.Configs[0].Data, "number: 42") -} - -func TestPostMixedConfigs(t *testing.T) { - pub, priv := generateTestKeys(t) - handler := mux(pub) - - reqBody := Request{ - Configs: []Config{ - { - Name: "first.yaml", - Data: "plain: yaml", - }, - { - Name: "second.jsonnet", - Data: `{"from": "jsonnet"}`, - }, - { - Name: "third.yaml", - Data: "another: yaml", - }, - }, - } - body, err := json.Marshal(reqBody) - assert.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - signRequest(t, req, priv, body) - - w := httptest.NewRecorder() - handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var resp Request - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(t, err) - assert.Equal(t, 3, len(resp.Configs)) - - assert.Equal(t, "first.yaml", resp.Configs[0].Name) - assert.Equal(t, "plain: yaml", resp.Configs[0].Data) - - assert.Equal(t, "second.jsonnet", resp.Configs[1].Name) - assert.Contains(t, resp.Configs[1].Data, "from: jsonnet") - - assert.Equal(t, "third.yaml", resp.Configs[2].Name) - assert.Equal(t, "another: yaml", resp.Configs[2].Data) -} - -func TestPostInvalidJsonnet(t *testing.T) { - pub, priv := generateTestKeys(t) - handler := mux(pub) - - reqBody := Request{ - Configs: []Config{ - { - Name: "good.yaml", - Data: "valid: yaml", - }, - { - Name: "bad.jsonnet", - Data: `{this is not valid jsonnet`, - }, - { - Name: "also-good.yaml", - Data: "also: valid", - }, - }, - } - body, err := json.Marshal(reqBody) - assert.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - signRequest(t, req, priv, body) - - w := httptest.NewRecorder() - handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var resp Request - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(t, err) - assert.Equal(t, 2, len(resp.Configs)) - assert.Equal(t, "good.yaml", resp.Configs[0].Name) - assert.Equal(t, "also-good.yaml", resp.Configs[1].Name) -} - -func TestVerify(t *testing.T) { - pub, priv := generateTestKeys(t) - - tests := []struct { - name string - signReq bool - wantErr bool - }{ - { - name: "valid signature", - signReq: true, - wantErr: false, - }, - { - name: "no signature", - signReq: false, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - body := []byte(`{"test": "data"}`) - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) - - if tt.signReq { - signRequest(t, req, priv, body) - } - - err := verify(pub, req) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestVerifyMiddleware(t *testing.T) { - pub, priv := generateTestKeys(t) - - okHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("success")) - }) - - wrappedHandler := verifyMiddleware(pub)(okHandler) - - tests := []struct { - name string - signRequest bool - expectedStatus int - expectedBody string - }{ - { - name: "valid signature allows request through", - signRequest: true, - expectedStatus: http.StatusOK, - expectedBody: "success", - }, - { - name: "missing signature blocks request", - signRequest: false, - expectedStatus: http.StatusUnauthorized, - expectedBody: `{"error":"Failed to verify request"}`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - body := []byte(`{"test": "data"}`) - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) - - if tt.signRequest { - signRequest(t, req, priv, body) - } - - w := httptest.NewRecorder() - wrappedHandler.ServeHTTP(w, req) - - assert.Equal(t, tt.expectedStatus, w.Code) - assert.Contains(t, w.Body.String(), tt.expectedBody) - }) - } -}