mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-04-15 01:41:56 +00:00
Migrate to maintained httpsign library (#3839)
Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
@@ -27,9 +27,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-ap/httpsig"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/yaronf/httpsign"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks"
|
||||
forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
|
||||
@@ -120,25 +120,19 @@ func TestFetchFromConfigService(t *testing.T) {
|
||||
|
||||
fixtureHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
// check signature
|
||||
pubKeyID := "woodpecker-ci-plugins"
|
||||
pubKeyID := "woodpecker-ci-extensions"
|
||||
|
||||
keystore := httpsig.NewMemoryKeyStore()
|
||||
keystore.SetKey(pubKeyID, pubEd25519Key)
|
||||
verifier, err := httpsign.NewEd25519Verifier(pubEd25519Key,
|
||||
httpsign.NewVerifyConfig(),
|
||||
httpsign.Headers("@request-target", "content-digest")) // The Content-Digest header will be auto-generated
|
||||
assert.NoError(t, err)
|
||||
|
||||
verifier := httpsig.NewVerifier(keystore)
|
||||
verifier.SetRequiredHeaders([]string{"(request-target)", "date"})
|
||||
|
||||
keyID, err := verifier.Verify(r)
|
||||
err = httpsign.VerifyRequest(pubKeyID, *verifier, r)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid signature", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if keyID != pubKeyID {
|
||||
http.Error(w, "Used wrong key", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Name string `json:"name"`
|
||||
Data string `json:"data"`
|
||||
@@ -163,12 +157,12 @@ func TestFetchFromConfigService(t *testing.T) {
|
||||
}
|
||||
|
||||
if req.Repo.Name == "Fetch empty" {
|
||||
w.WriteHeader(404)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Repo.Name == "Use old config" {
|
||||
w.WriteHeader(204)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -192,7 +186,7 @@ func TestFetchFromConfigService(t *testing.T) {
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(fixtureHandler))
|
||||
defer ts.Close()
|
||||
httpFetcher := config.NewHTTP(ts.URL, privEd25519Key)
|
||||
httpFetcher := config.NewHTTP(ts.URL+"/", privEd25519Key)
|
||||
|
||||
for _, tt := range testTable {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -16,7 +16,7 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
net_http "net/http"
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
|
||||
type http struct {
|
||||
endpoint string
|
||||
privateKey crypto.PrivateKey
|
||||
privateKey ed25519.PrivateKey
|
||||
}
|
||||
|
||||
// configData same as forge.FileMeta but with json tags and string data.
|
||||
@@ -48,7 +48,7 @@ type responseStructure struct {
|
||||
Configs []*configData `json:"configs"`
|
||||
}
|
||||
|
||||
func NewHTTP(endpoint string, privateKey crypto.PrivateKey) Service {
|
||||
func NewHTTP(endpoint string, privateKey ed25519.PrivateKey) Service {
|
||||
return &http{endpoint, privateKey}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ func setupSecretService(store store.Store) secret.Service {
|
||||
return secret.NewDB(store)
|
||||
}
|
||||
|
||||
func setupConfigService(c *cli.Command, privateSignatureKey crypto.PrivateKey) (config.Service, error) {
|
||||
func setupConfigService(c *cli.Command, privateSignatureKey ed25519.PrivateKey) (config.Service, error) {
|
||||
timeout := c.Duration("forge-timeout")
|
||||
retries := c.Uint("forge-retry")
|
||||
if retries == 0 {
|
||||
@@ -74,7 +74,7 @@ func setupConfigService(c *cli.Command, privateSignatureKey crypto.PrivateKey) (
|
||||
}
|
||||
|
||||
// setupSignatureKeys generate or load key pair to sign webhooks requests (i.e. used for service extensions).
|
||||
func setupSignatureKeys(_store store.Store) (crypto.PrivateKey, crypto.PublicKey, error) {
|
||||
func setupSignatureKeys(_store store.Store) (ed25519.PrivateKey, crypto.PublicKey, error) {
|
||||
privKeyID := "signature-private-key"
|
||||
|
||||
privKey, err := _store.ServerConfigGet(privKeyID)
|
||||
|
||||
@@ -17,19 +17,19 @@ package utils
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-ap/httpsig"
|
||||
"github.com/yaronf/httpsign"
|
||||
)
|
||||
|
||||
// Send makes an http request to the given endpoint, writing the input
|
||||
// to the request body and un-marshaling the output from the response body.
|
||||
func Send(ctx context.Context, method, path string, privateKey crypto.PrivateKey, in, out any) (int, error) {
|
||||
func Send(ctx context.Context, method, path string, privateKey ed25519.PrivateKey, in, out any) (int, error) {
|
||||
uri, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -54,12 +54,12 @@ func Send(ctx context.Context, method, path string, privateKey crypto.PrivateKey
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
err = SignHTTPRequest(privateKey, req)
|
||||
client, err := signClient(privateKey)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -79,10 +79,14 @@ func Send(ctx context.Context, method, path string, privateKey crypto.PrivateKey
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
func SignHTTPRequest(privateKey crypto.PrivateKey, req *http.Request) error {
|
||||
pubKeyID := "woodpecker-ci-plugins"
|
||||
func signClient(privateKey ed25519.PrivateKey) (*httpsign.Client, error) {
|
||||
pubKeyID := "woodpecker-ci-extensions"
|
||||
|
||||
signer := httpsig.NewEd25519Signer(pubKeyID, privateKey, nil)
|
||||
|
||||
return signer.Sign(req)
|
||||
signer, err := httpsign.NewEd25519Signer(privateKey,
|
||||
httpsign.NewSignConfig(),
|
||||
httpsign.Headers("@request-target", "content-digest")) // The Content-Digest header will be auto-generated
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return httpsign.NewDefaultClient(httpsign.NewClientConfig().SetSignatureName(pubKeyID).SetSigner(signer)), nil // sign requests, don't verify responses
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package utils_test
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -21,15 +21,14 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-ap/httpsig"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/services/utils"
|
||||
"github.com/yaronf/httpsign"
|
||||
)
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
pubKeyID := "woodpecker-ci-plugins"
|
||||
func TestSignClient(t *testing.T) {
|
||||
pubKeyID := "woodpecker-ci-extensions"
|
||||
|
||||
pubEd25519Key, privEd25519Key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if !assert.NoError(t, err) {
|
||||
@@ -38,36 +37,36 @@ func TestSign(t *testing.T) {
|
||||
|
||||
body := []byte("{\"foo\":\"bar\"}")
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
err = utils.SignHTTPRequest(privEd25519Key, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
verifyHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
keystore := httpsig.NewMemoryKeyStore()
|
||||
keystore.SetKey(pubKeyID, pubEd25519Key)
|
||||
|
||||
verifier := httpsig.NewVerifier(keystore)
|
||||
verifier.SetRequiredHeaders([]string{"(request-target)", "date"})
|
||||
|
||||
keyID, err := verifier.Verify(r)
|
||||
verifier, err := httpsign.NewEd25519Verifier(pubEd25519Key,
|
||||
httpsign.NewVerifyConfig(),
|
||||
httpsign.Headers("@request-target", "content-digest")) // The Content-Digest header will be auto-generated
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = httpsign.VerifyRequest(pubKeyID, *verifier, r)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, pubKeyID, keyID)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(verifyHandler)
|
||||
server := httptest.NewServer(http.HandlerFunc(verifyHandler))
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
req, err := http.NewRequest("GET", server.URL+"/", bytes.NewBuffer(body))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
req.Header.Set("Date", time.Now().Format(time.RFC3339))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client, err := signClient(privEd25519Key)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
rr, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer rr.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.StatusCode)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user