Use asym key to sign webhooks (#916)

* use async key pair for webhooks

* fix tests

* fix linter

* improve code

* add key pair to database

* undo some changes

* more undo

* improve docs

* add api-endpoint

* add signaturne api endpoint

* fix error

* fix linting and test

* fix lint

* add test

* migration 006

* no need for migration

* replace httsign lib

* fix lint

Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
Anbraten 2022-06-01 20:06:27 +02:00 committed by GitHub
parent a2ca657631
commit cc30db44ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1763 additions and 633 deletions

View file

@ -169,12 +169,6 @@ var flags = []cli.Flag{
Name: "config-service-endpoint",
Usage: "url used for calling configuration service endpoint",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CONFIG_SERVICE_SECRET"},
Name: "config-service-secret",
Usage: "secret to sign requests send to configuration service",
FilePath: os.Getenv("WOODPECKER_CONFIG_SERVICE_SECRET_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_DATABASE_DRIVER"},
Name: "driver",

View file

@ -42,7 +42,7 @@ import (
woodpeckerGrpcServer "github.com/woodpecker-ci/woodpecker/server/grpc"
"github.com/woodpecker-ci/woodpecker/server/logging"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/configuration"
"github.com/woodpecker-ci/woodpecker/server/plugins/config"
"github.com/woodpecker-ci/woodpecker/server/pubsub"
"github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/router"
@ -267,13 +267,10 @@ func setupEvilGlobals(c *cli.Context, v store.Store, r remote.Remote) {
server.Config.Services.Secrets = setupSecretService(c, v)
server.Config.Services.Environ = setupEnvironService(c, v)
server.Config.Services.SignaturePrivateKey, server.Config.Services.SignaturePublicKey = setupSignatureKeys(v)
if endpoint := c.String("config-service-endpoint"); endpoint != "" {
secret := c.String("config-service-secret")
if secret == "" {
log.Error().Msg("could not configure configuration service, missing secret")
} else {
server.Config.Services.ConfigService = configuration.NewAPI(endpoint, secret)
}
server.Config.Services.ConfigService = config.NewHTTP(endpoint, server.Config.Services.SignaturePrivateKey)
}
// authentication

View file

@ -16,6 +16,10 @@ package main
import (
"context"
"crypto"
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"fmt"
"net/url"
"os"
@ -352,3 +356,35 @@ func setupMetrics(g *errgroup.Group, _store store.Store) {
}
})
}
// generate or load key pair to sign webhooks requests (i.e. used for extensions)
func setupSignatureKeys(_store store.Store) (crypto.PrivateKey, crypto.PublicKey) {
privKeyID := "signature-private-key"
privKey, err := _store.ServerConfigGet(privKeyID)
if err != nil && err == datastore.RecordNotExist {
_, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to generate private key")
return nil, nil
}
err = _store.ServerConfigSet(privKeyID, hex.EncodeToString(privKey))
if err != nil {
log.Fatal().Err(err).Msgf("Failed to generate private key")
return nil, nil
}
log.Info().Msg("Created private key")
return privKey, privKey.Public()
} else if err != nil {
log.Fatal().Err(err).Msgf("Failed to load private key")
return nil, nil
} else {
privKeyStr, err := hex.DecodeString(privKey)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to decode private key")
return nil, nil
}
privKey := ed25519.PrivateKey(privKeyStr)
return privKey, privKey.Public()
}
}

View file

@ -356,16 +356,6 @@ Example: `WOODPECKER_LIMIT_CPU_SET=1,2`
Specify a configuration service endpoint, see [Configuration Extension](/docs/administration/external-configuration-api)
### `WOODPECKER_CONFIG_SERVICE_SECRET`
> Default: ``
Specify a signing secret for the configuration service endpoint, see [Configuration Extension](/docs/administration/external-configuration-api)
### `WOODPECKER_CONFIG_SERVICE_SECRET_FILE`
> Default: ``
Read the value for `WOODPECKER_CONFIG_SERVICE_SECRET` from the specified filepath
---
### `WOODPECKER_GITHUB_...`

View file

@ -3,7 +3,7 @@
To provide additional management and preprocessing capabilities for pipeline configurations Woodpecker supports an HTTP api which can be enabled to call an external config service.
Before the run or restart of any pipeline Woodpecker will make a POST request to an external HTTP api sending the current repository, build information and all current config files retrieved from the repository. The external api can then send back new pipeline configurations that will be used immediately or respond with `HTTP 204` to tell the system to use the existing configuration.
Every request sent by Woodpecker is signed using a http-signature using the provided secret from `WOODPECKER_CONFIG_SERVICE_SECRET`. This way the external api can verify the authenticity request from the Woodpecker instance.
Every request sent by Woodpecker is signed using a [http-signature](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) by a private key (ed25519) generated on the first start of the Woodpecker server. You can get the public key for the verification of the http-signature from `http(s)://your-woodpecker-server/api/signature/public-key`.
A simplistic example configuration service can be found here: [https://github.com/woodpecker-ci/example-config-service](https://github.com/woodpecker-ci/example-config-service)
@ -13,8 +13,6 @@ A simplistic example configuration service can be found here: [https://github.co
# Server
# ...
WOODPECKER_CONFIG_SERVICE_ENDPOINT=https://example.com/ciconfig
WOODPECKER_CONFIG_SERVICE_SECRET=mysecretsigningkey
```
### Example request made by Woodpecker

View file

@ -4,6 +4,7 @@ Some versions need some changes to the server configuration or the pipeline conf
## 1.0.0
- The signature used to verify extensions calls (like those used for the [config-extension](/docs/administration/external-configuration-api)) done by the Woodpecker server switched from using a shared-secret HMac to an ed25519 key-pair. Read more about it at the [config-extensions](/docs/administration/external-configuration-api) documentation.
- Refactored support of old agent filter labels and expression. Learn how to use the new [filter](/docs/usage/pipeline-syntax#labels).
- Renamed step environment variable `CI_SYSTEM_ARCH` to `CI_SYSTEM_PLATFORM`. Same applies for the cli exec variable.

2
go.mod
View file

@ -4,7 +4,6 @@ go 1.16
require (
code.gitea.io/sdk/gitea v0.15.1-0.20220501190934-319a978c6c71
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/containerd/containerd v1.5.9 // indirect
@ -19,6 +18,7 @@ require (
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf
github.com/gin-gonic/gin v1.7.7
github.com/go-ap/httpsig v0.0.0-20210714162115-62a09257db51
github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/go-sql-driver/mysql v1.6.0
github.com/goccy/go-json v0.9.7 // indirect

4
go.sum
View file

@ -60,8 +60,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc=
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY=
github.com/Antonboom/errname v0.1.5 h1:IM+A/gz0pDhKmlt5KSNTVAvfLMb+65RxavBXpRtCUEg=
github.com/Antonboom/errname v0.1.5/go.mod h1:DugbBstvPFQbv/5uLcRRzfrNqKE9tVdVCqWCLp6Cifo=
github.com/Antonboom/nilnil v0.1.0 h1:DLDavmg0a6G/F4Lt9t7Enrbgb3Oph6LnDE6YVsmTt74=
@ -448,6 +446,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-ap/httpsig v0.0.0-20210714162115-62a09257db51 h1:cytjZGyqtAu9JspfDt9ThCJ2KKCT/kPGDPCKWIZv8dw=
github.com/go-ap/httpsig v0.0.0-20210714162115-62a09257db51/go.mod h1:+4SUDMvPlRMUPW5PlMTbxj3U5a4fWasBIbakUw7Kp6c=
github.com/go-critic/go-critic v0.6.2 h1:L5SDut1N4ZfsWZY0sH4DCrsHLHnhuuWak2wa165t9gs=
github.com/go-critic/go-critic v0.6.2/go.mod h1:td1s27kfmLpe5G/DPjlnFI7o1UCzePptwU7Az0V5iCM=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=

View file

@ -482,7 +482,7 @@ func PostBuild(c *gin.Context) {
currentFileMeta[i] = &remote.FileMeta{Name: cfg.Name, Data: cfg.Data}
}
newConfig, useOld, err := server.Config.Services.ConfigService.FetchExternalConfig(c, repo, build, currentFileMeta)
newConfig, useOld, err := server.Config.Services.ConfigService.FetchConfig(c, repo, build, currentFileMeta)
if err != nil {
msg := fmt.Sprintf("On fetching external build config: %s", err)
c.String(http.StatusBadRequest, msg)

View file

@ -0,0 +1,41 @@
// Copyright 2021 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"crypto/x509"
"encoding/pem"
"net/http"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/server"
)
func GetSignaturePublicKey(c *gin.Context) {
b, err := x509.MarshalPKIXPublicKey(server.Config.Services.SignaturePublicKey)
if err != nil {
log.Error().Err(err).Msg("can't marshal public key")
c.AbortWithStatus(http.StatusInternalServerError)
return
}
block := &pem.Block{
Type: "PUBLIC KEY",
Bytes: b,
}
c.String(200, "%s", pem.EncodeToMemory(block))
}

View file

@ -18,11 +18,12 @@
package server
import (
"crypto"
"time"
"github.com/woodpecker-ci/woodpecker/server/logging"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/configuration"
"github.com/woodpecker-ci/woodpecker/server/plugins/config"
"github.com/woodpecker-ci/woodpecker/server/pubsub"
"github.com/woodpecker-ci/woodpecker/server/queue"
"github.com/woodpecker-ci/woodpecker/server/remote"
@ -30,14 +31,16 @@ import (
var Config = struct {
Services struct {
Pubsub pubsub.Publisher
Queue queue.Queue
Logs logging.Log
Secrets model.SecretService
Registries model.RegistryService
Environ model.EnvironService
Remote remote.Remote
ConfigService configuration.ConfigService
Pubsub pubsub.Publisher
Queue queue.Queue
Logs logging.Log
Secrets model.SecretService
Registries model.RegistryService
Environ model.EnvironService
Remote remote.Remote
ConfigService config.Extension
SignaturePrivateKey crypto.PrivateKey
SignaturePublicKey crypto.PublicKey
}
Storage struct {
// Users model.UserStore

View file

@ -0,0 +1,27 @@
// Copyright 2022 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
// ServerConfigStore persists key-value pairs for storing server configurations.
type ServerConfigStore interface {
ServerConfigGet(key string) (string, error)
ServerConfigSet(key int64, value string) error
}
// ServerConfig represents a key-value pair for storing server configurations.
type ServerConfig struct {
Key string `json:"key" xorm:"pk"`
Value string `json:"value" xorm:""`
}

View file

@ -0,0 +1,13 @@
package config
import (
"context"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote"
)
type Extension interface {
IsConfigured() bool
FetchConfig(ctx context.Context, repo *model.Repo, build *model.Build, currentFileMeta []*remote.FileMeta) (configData []*remote.FileMeta, useOld bool, err error)
}

View file

@ -0,0 +1,66 @@
package config
import (
"context"
"crypto"
"fmt"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/utils"
"github.com/woodpecker-ci/woodpecker/server/remote"
)
type http struct {
endpoint string
privateKey crypto.PrivateKey
}
// Same as remote.FileMeta but with json tags and string data
type config struct {
Name string `json:"name"`
Data string `json:"data"`
}
type requestStructure struct {
Repo *model.Repo `json:"repo"`
Build *model.Build `json:"build"`
Configuration []*config `json:"configs"`
}
type responseStructure struct {
Configs []config `json:"configs"`
}
func NewHTTP(endpoint string, privateKey crypto.PrivateKey) Extension {
return &http{endpoint, privateKey}
}
func (cp *http) IsConfigured() bool {
return cp.endpoint != ""
}
func (cp *http) FetchConfig(ctx context.Context, repo *model.Repo, build *model.Build, currentFileMeta []*remote.FileMeta) (configData []*remote.FileMeta, useOld bool, err error) {
currentConfigs := make([]*config, len(currentFileMeta))
for i, pipe := range currentFileMeta {
currentConfigs[i] = &config{Name: pipe.Name, Data: string(pipe.Data)}
}
response := new(responseStructure)
body := requestStructure{Repo: repo, Build: build, Configuration: currentConfigs}
status, err := utils.Send(ctx, "POST", cp.endpoint, cp.privateKey, body, response)
if err != nil && status != 204 {
return nil, false, fmt.Errorf("Failed to fetch config via http (%d) %w", status, err)
}
var newFileMeta []*remote.FileMeta
if status != 200 {
newFileMeta = make([]*remote.FileMeta, 0)
} else {
newFileMeta = make([]*remote.FileMeta, len(response.Configs))
for i, pipe := range response.Configs {
newFileMeta[i] = &remote.FileMeta{Name: pipe.Name, Data: []byte(pipe.Data)}
}
}
return newFileMeta, status == 204, nil
}

View file

@ -1,127 +0,0 @@
package configuration
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"github.com/99designs/httpsignatures-go"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote"
)
type ConfigService struct {
endpoint string
secret string
}
// Same as remote.FileMeta but with json tags and string data
type config struct {
Name string `json:"name"`
Data string `json:"data"`
}
type requestStructure struct {
Repo *model.Repo `json:"repo"`
Build *model.Build `json:"build"`
Configuration []*config `json:"configs"`
}
type responseStructure struct {
Configs []config `json:"configs"`
}
func NewAPI(endpoint, secret string) ConfigService {
return ConfigService{endpoint: endpoint, secret: secret}
}
func (cp *ConfigService) IsConfigured() bool {
return cp.endpoint != ""
}
func (cp *ConfigService) FetchExternalConfig(ctx context.Context, repo *model.Repo, build *model.Build, currentFileMeta []*remote.FileMeta) (configData []*remote.FileMeta, useOld bool, err error) {
currentConfigs := make([]*config, len(currentFileMeta))
for i, pipe := range currentFileMeta {
currentConfigs[i] = &config{Name: pipe.Name, Data: string(pipe.Data)}
}
response, status, err := sendRequest(ctx, "POST", cp.endpoint, cp.secret, requestStructure{Repo: repo, Build: build, Configuration: currentConfigs})
if err != nil {
return nil, false, fmt.Errorf("Failed to fetch config via http (%d) %w", status, err)
}
var newFileMeta []*remote.FileMeta
if response != nil {
newFileMeta = make([]*remote.FileMeta, len(response.Configs))
for i, pipe := range response.Configs {
newFileMeta[i] = &remote.FileMeta{Name: pipe.Name, Data: []byte(pipe.Data)}
}
} else {
newFileMeta = make([]*remote.FileMeta, 0)
}
return newFileMeta, status == 204, nil
}
func sendRequest(ctx context.Context, method, path, signkey string, in interface{}) (response *responseStructure, statuscode int, err error) {
uri, err := url.Parse(path)
if err != nil {
return nil, 0, err
}
// if we are posting or putting data, we need to
// write it to the body of the request.
var buf io.ReadWriter
if in != nil {
buf = new(bytes.Buffer)
jsonerr := json.NewEncoder(buf).Encode(in)
if jsonerr != nil {
return nil, 0, jsonerr
}
}
// creates a new http request to bitbucket.
req, err := http.NewRequestWithContext(ctx, method, uri.String(), buf)
if err != nil {
return nil, 0, err
}
if in != nil {
req.Header.Set("Content-Type", "application/json")
}
// Sign using the 'Signature' header
err = httpsignatures.DefaultSha256Signer.SignRequest("hmac-key", signkey, req)
if err != nil {
return nil, 0, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 && resp.StatusCode != 204 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, resp.StatusCode, err
}
return nil, resp.StatusCode, fmt.Errorf("Response: %s", string(body))
}
if resp.StatusCode == 204 {
return nil, resp.StatusCode, nil
}
// if no other errors parse and return the json response.
decodedResponse := new(responseStructure)
err = json.NewDecoder(resp.Body).Decode(decodedResponse)
return decodedResponse, resp.StatusCode, err
}

View file

@ -1,83 +0,0 @@
package internal
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
)
// Send makes an http request to the given endpoint, writing the input
// to the request body and unmarshaling the output from the response body.
func Send(ctx context.Context, method, path string, in, out interface{}) error {
uri, err := url.Parse(path)
if err != nil {
return err
}
// if we are posting or putting data, we need to
// write it to the body of the request.
var buf io.ReadWriter
if in != nil {
buf = new(bytes.Buffer)
jsonerr := json.NewEncoder(buf).Encode(in)
if jsonerr != nil {
return jsonerr
}
}
// creates a new http request to bitbucket.
req, err := http.NewRequestWithContext(ctx, method, uri.String(), buf)
if err != nil {
return err
}
if in != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// if an error is encountered, parse and return the
// error response.
if resp.StatusCode > http.StatusPartialContent {
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return &Error{
code: resp.StatusCode,
text: string(out),
}
}
// if a json response is expected, parse and return
// the json response.
if out != nil {
return json.NewDecoder(resp.Body).Decode(out)
}
return nil
}
// Error represents a http error.
type Error struct {
code int
text string
}
// Code returns the http error code.
func (e *Error) Code() int {
return e.code
}
// Error returns the error message in string format.
func (e *Error) Error() string {
return e.text
}

View file

@ -0,0 +1,75 @@
package utils
import (
"bytes"
"context"
"crypto"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"github.com/go-ap/httpsig"
)
// 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 interface{}) (int, error) {
uri, err := url.Parse(path)
if err != nil {
return 0, err
}
// if we are posting or putting data, we need to write it to the body of the request.
var buf io.ReadWriter
if in != nil {
buf = new(bytes.Buffer)
jsonerr := json.NewEncoder(buf).Encode(in)
if jsonerr != nil {
return 0, jsonerr
}
}
// creates a new http request to the endpoint.
req, err := http.NewRequestWithContext(ctx, method, uri.String(), buf)
if err != nil {
return 0, err
}
if in != nil {
req.Header.Set("Content-Type", "application/json")
}
err = SignHTTPRequest(privateKey, req)
if err != nil {
return 0, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return resp.StatusCode, err
}
return resp.StatusCode, fmt.Errorf("Response: %s", string(body))
}
// if no other errors parse and return the json response.
err = json.NewDecoder(resp.Body).Decode(out)
return resp.StatusCode, err
}
func SignHTTPRequest(privateKey crypto.PrivateKey, req *http.Request) error {
pubKeyID := "woodpecker-ci-plugins"
signer := httpsig.NewEd25519Signer(pubKeyID, privateKey, nil)
return signer.Sign(req)
}

View file

@ -0,0 +1,65 @@
package utils_test
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-ap/httpsig"
"github.com/woodpecker-ci/woodpecker/server/plugins/utils"
)
func TestSign(t *testing.T) {
pubKeyID := "woodpecker-ci-plugins"
pubEd25519Key, privEd25519Key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
body := []byte("{\"foo\":\"bar\"}")
req, err := http.NewRequest("GET", "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)
if err != nil {
t.Fatal(err)
}
if keyID != pubKeyID {
t.Fatalf("expected key ID %q, got %q", pubKeyID, keyID)
}
w.WriteHeader(http.StatusOK)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(VerifyHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
}

View file

@ -145,6 +145,8 @@ func apiRoutes(e *gin.Engine) {
logLevel.POST("", api.SetLogLevel)
}
e.GET("/api/signature/public-key", session.MustUser(), api.GetSignaturePublicKey)
// TODO: remove /hook in favor of /api/hook
e.POST("/hook", api.PostHook)
e.POST("/api/hook", api.PostHook)

View file

@ -8,7 +8,7 @@ import (
"time"
"github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/server/plugins/configuration"
"github.com/woodpecker-ci/woodpecker/server/plugins/config"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote"
@ -19,20 +19,20 @@ type ConfigFetcher interface {
}
type configFetcher struct {
remote remote.Remote
user *model.User
repo *model.Repo
build *model.Build
configService configuration.ConfigService
remote remote.Remote
user *model.User
repo *model.Repo
build *model.Build
configExtension config.Extension
}
func NewConfigFetcher(remote remote.Remote, configurationService configuration.ConfigService, user *model.User, repo *model.Repo, build *model.Build) ConfigFetcher {
func NewConfigFetcher(remote remote.Remote, configExtension config.Extension, user *model.User, repo *model.Repo, build *model.Build) ConfigFetcher {
return &configFetcher{
remote: remote,
user: user,
repo: repo,
build: build,
configService: configurationService,
remote: remote,
user: user,
repo: repo,
build: build,
configExtension: configExtension,
}
}
@ -53,12 +53,12 @@ func (cf *configFetcher) Fetch(ctx context.Context) (files []*remote.FileMeta, e
continue
}
if cf.configService.IsConfigured() {
if cf.configExtension.IsConfigured() {
fetchCtx, cancel := context.WithTimeout(ctx, configFetchTimeout)
defer cancel() // ok here as we only try http fetching once, returning on fail and success
log.Trace().Msgf("ConfigFetch[%s]: getting config from external http service", cf.repo.FullName)
newConfigs, useOld, err := cf.configService.FetchExternalConfig(fetchCtx, cf.repo, cf.build, files)
newConfigs, useOld, err := cf.configExtension.FetchConfig(fetchCtx, cf.repo, cf.build, files)
if err != nil {
log.Error().Msg("Got error " + err.Error())
return nil, fmt.Errorf("On Fetching config via http : %s", err)

View file

@ -2,21 +2,22 @@ package shared_test
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/99designs/httpsignatures-go"
"github.com/go-ap/httpsig"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/configuration"
"github.com/woodpecker-ci/woodpecker/server/plugins/config"
"github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/mocks"
"github.com/woodpecker-ci/woodpecker/server/shared"
@ -244,7 +245,7 @@ func TestFetch(t *testing.T) {
configFetcher := shared.NewConfigFetcher(
r,
configuration.NewAPI("", ""),
config.NewHTTP("", ""),
&model.User{Token: "xxx"},
repo,
&model.Build{Commit: "89ab7b2d6bfb347144ac7c557e638ab402848fee"},
@ -341,17 +342,29 @@ func TestFetchFromConfigService(t *testing.T) {
},
}
httpSigSecret := "wykf9frJbGXwSHcJ7AQF4tlfXUo0Tkixh57WPEXMyWVgkxIsAarYa2Hb8UTwPpbqO0N3NueKwjv4DVhPgvQjGur3LuCbiGHbBoaL1X5gZ9oyxD2lBHndoNxifDyNH7tNPw3Lh5lX2MSrWP1yuqHp8Sgm7fX8pLTjaKKFgFIKlODd"
pubEd25519Key, privEd25519Key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal("can't generate ed25519 key pair")
}
fixtureHandler := func(w http.ResponseWriter, r *http.Request) {
// check signature
signature, err := httpsignatures.FromRequest(r)
pubKeyID := "woodpecker-ci-plugins"
keystore := httpsig.NewMemoryKeyStore()
keystore.SetKey(pubKeyID, pubEd25519Key)
verifier := httpsig.NewVerifier(keystore)
verifier.SetRequiredHeaders([]string{"(request-target)", "date"})
keyID, err := verifier.Verify(r)
if err != nil {
http.Error(w, "Invalid or Missing Signature", http.StatusBadRequest)
http.Error(w, "Invalid signature", http.StatusBadRequest)
return
}
if !signature.IsValid(httpSigSecret, r) {
http.Error(w, "Invalid Signature", http.StatusBadRequest)
if keyID != pubKeyID {
http.Error(w, "Used wrong key", http.StatusBadRequest)
return
}
@ -369,7 +382,6 @@ func TestFetchFromConfigService(t *testing.T) {
var req incoming
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
http.Error(w, "can't read body", http.StatusBadRequest)
return
}
@ -409,7 +421,7 @@ func TestFetchFromConfigService(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(fixtureHandler))
defer ts.Close()
configAPI := configuration.NewAPI(ts.URL, httpSigSecret)
configAPI := config.NewHTTP(ts.URL, privEd25519Key)
for _, tt := range testTable {
t.Run(tt.name, func(t *testing.T) {

View file

@ -49,6 +49,7 @@ var allBeans = []interface{}{
new(model.Secret),
new(model.Task),
new(model.User),
new(model.ServerConfig),
}
type migrations struct {

View file

@ -0,0 +1,36 @@
package datastore
import "github.com/woodpecker-ci/woodpecker/server/model"
func (s storage) ServerConfigGet(key string) (string, error) {
config := &model.ServerConfig{
Key: key,
}
err := wrapGet(s.engine.Get(config))
if err != nil {
return "", err
}
return config.Value, nil
}
func (s storage) ServerConfigSet(key, value string) error {
config := &model.ServerConfig{
Key: key,
Value: value,
}
count, err := s.engine.Count(config)
if err != nil {
return err
}
if count == 0 {
_, err := s.engine.Insert(config)
return err
}
_, err = s.engine.Where("key = ?", config.Key).AllCols().Update(config)
return err
}

View file

@ -0,0 +1,32 @@
package datastore
import (
"testing"
"github.com/woodpecker-ci/woodpecker/server/model"
)
func TestServerConfigGetSet(t *testing.T) {
store, closer := newTestStore(t, new(model.ServerConfig))
defer closer()
serverConfig := &model.ServerConfig{
Key: "test",
Value: "wonderland",
}
if err := store.ServerConfigSet(serverConfig.Key, serverConfig.Value); err != nil {
t.Errorf("Unexpected error: insert secret: %s", err)
return
}
value, err := store.ServerConfigGet(serverConfig.Key)
if err != nil {
t.Errorf("Unexpected error: delete secret: %s", err)
return
}
if value != serverConfig.Value {
t.Errorf("Want server-config value %s, got %s", serverConfig.Value, value)
return
}
}

View file

@ -145,6 +145,10 @@ type Store interface {
TaskInsert(*model.Task) error
TaskDelete(string) error
// ServerConfig
ServerConfigGet(string) (string, error)
ServerConfigSet(string, string) error
// Store operations
Ping() error
Close() error

View file

@ -1,13 +0,0 @@
language: go
go:
- 1.2
- 1.3
- 1.4
- 1.5
- 1.6
- 1.7
- 1.8
install:
- go get github.com/stretchr/testify/assert

View file

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 99designs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,9 +0,0 @@
httpsignatures-go
=================
[![GoDoc](https://godoc.org/github.com/99designs/httpsignatures-go?status.svg)](https://godoc.org/github.com/99designs/httpsignatures-go)
[![Build Status](https://travis-ci.org/99designs/httpsignatures-go.svg)](https://travis-ci.org/99designs/httpsignatures-go)
Golang library for the [http-signatures spec](https://tools.ietf.org/html/draft-cavage-http-signatures).
See https://godoc.org/github.com/99designs/httpsignatures-go for documentation and examples

View file

@ -1,31 +0,0 @@
package httpsignatures
import (
"crypto/sha1"
"crypto/sha256"
"errors"
"hash"
)
var (
AlgorithmHmacSha256 = &Algorithm{"hmac-sha256", sha256.New}
AlgorithmHmacSha1 = &Algorithm{"hmac-sha1", sha1.New}
ErrorUnknownAlgorithm = errors.New("Unknown Algorithm")
)
type Algorithm struct {
name string
hash func() hash.Hash
}
func algorithmFromString(name string) (*Algorithm, error) {
switch name {
case AlgorithmHmacSha1.name:
return AlgorithmHmacSha1, nil
case AlgorithmHmacSha256.name:
return AlgorithmHmacSha256, nil
}
return nil, ErrorUnknownAlgorithm
}

View file

@ -1,202 +0,0 @@
// httpsignatures is a golang implementation of the http-signatures spec
// found at https://tools.ietf.org/html/draft-cavage-http-signatures
package httpsignatures
import (
"crypto/hmac"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"net/http"
"regexp"
"strings"
)
const (
headerSignature = "Signature"
headerAuthorization = "Authorization"
RequestTarget = "(request-target)"
authScheme = "Signature "
)
var (
ErrorNoSignatureHeader = errors.New("No Signature header found in request")
signatureRegex = regexp.MustCompile(`(\w+)="([^"]*)"`)
)
// Signature is the hashed key + headers, either from a request or a signer
type Signature struct {
KeyID string
Algorithm *Algorithm
Headers HeaderList
Signature string
}
// FromRequest creates a new Signature from the Request
// both Signature and Authorization http headers are supported.
func FromRequest(r *http.Request) (*Signature, error) {
if s, ok := r.Header[headerSignature]; ok {
return FromString(s[0])
}
if a, ok := r.Header[headerAuthorization]; ok {
return FromString(strings.TrimPrefix(a[0], authScheme))
}
return nil, ErrorNoSignatureHeader
}
// FromString creates a new Signature from its encoded form,
// eg `keyId="a",algorithm="b",headers="c",signature="d"`
func FromString(in string) (*Signature, error) {
var res Signature = Signature{}
var key string
var value string
for _, m := range signatureRegex.FindAllStringSubmatch(in, -1) {
key = m[1]
value = m[2]
if key == "keyId" {
res.KeyID = value
} else if key == "algorithm" {
alg, err := algorithmFromString(value)
if err != nil {
return nil, err
}
res.Algorithm = alg
} else if key == "headers" {
res.Headers = headerListFromString(value)
} else if key == "signature" {
res.Signature = value
} else {
return nil, errors.New(fmt.Sprintf("Unexpected key in signature '%s'", key))
}
}
if len(res.Signature) == 0 {
return nil, errors.New("Missing signature")
}
if len(res.KeyID) == 0 {
return nil, errors.New("Missing keyId")
}
if res.Algorithm == nil {
return nil, errors.New("Missing algorithm")
}
return &res, nil
}
// String returns the encoded form of the Signature
func (s Signature) String() string {
str := fmt.Sprintf(
`keyId="%s",algorithm="%s",signature="%s"`,
s.KeyID,
s.Algorithm.name,
s.Signature,
)
if len(s.Headers) > 0 {
str += fmt.Sprintf(`,headers="%s"`, s.Headers.String())
}
return str
}
func (s Signature) calculateSignature(key string, r *http.Request) (string, error) {
hash := hmac.New(s.Algorithm.hash, []byte(key))
signingString, err := s.Headers.signingString(r)
if err != nil {
return "", err
}
hash.Write([]byte(signingString))
return base64.StdEncoding.EncodeToString(hash.Sum(nil)), nil
}
// Sign this signature using the given key
func (s *Signature) sign(key string, r *http.Request) error {
sig, err := s.calculateSignature(key, r)
if err != nil {
return err
}
s.Signature = sig
return nil
}
// IsValid validates this signature for the given key
func (s Signature) IsValid(key string, r *http.Request) bool {
if !s.Headers.hasDate() {
return false
}
sig, err := s.calculateSignature(key, r)
if err != nil {
return false
}
return subtle.ConstantTimeCompare([]byte(s.Signature), []byte(sig)) == 1
}
type HeaderList []string
func headerListFromString(list string) HeaderList {
return strings.Split(strings.ToLower(string(list)), " ")
}
func (h HeaderList) String() string {
return strings.ToLower(strings.Join(h, " "))
}
func (h HeaderList) hasDate() bool {
for _, header := range h {
if header == "date" {
return true
}
}
return false
}
func (h HeaderList) signingString(req *http.Request) (string, error) {
lines := []string{}
for _, header := range h {
if header == RequestTarget {
lines = append(lines, requestTargetLine(req))
} else {
line, err := headerLine(req, header)
if err != nil {
return "", err
}
lines = append(lines, line)
}
}
return strings.Join(lines, "\n"), nil
}
func requestTargetLine(req *http.Request) string {
var url string = ""
if req.URL != nil {
url = req.URL.RequestURI()
}
return fmt.Sprintf("%s: %s %s", RequestTarget, strings.ToLower(req.Method), url)
}
func headerLine(req *http.Request, header string) (string, error) {
if value := req.Header.Get(header); value != "" {
return fmt.Sprintf("%s: %s", header, value), nil
}
return "", errors.New(fmt.Sprintf("Missing required header '%s'", header))
}

View file

@ -1,79 +0,0 @@
package httpsignatures
import (
"net/http"
"strings"
"time"
)
// Signer is used to create a signature for a given request.
type Signer struct {
algorithm *Algorithm
headers HeaderList
}
var (
// DefaultSha1Signer will sign requests with the url and date using the SHA1 algorithm.
// Users are encouraged to create their own signer with the headers they require.
DefaultSha1Signer = NewSigner(AlgorithmHmacSha1, RequestTarget, "date")
// DefaultSha256Signer will sign requests with the url and date using the SHA256 algorithm.
// Users are encouraged to create their own signer with the headers they require.
DefaultSha256Signer = NewSigner(AlgorithmHmacSha256, RequestTarget, "date")
)
func NewSigner(algorithm *Algorithm, headers ...string) *Signer {
hl := HeaderList{}
for _, header := range headers {
hl = append(hl, strings.ToLower(header))
}
return &Signer{
algorithm: algorithm,
headers: hl,
}
}
// SignRequest adds a http signature to the Signature: HTTP Header
func (s Signer) SignRequest(id, key string, r *http.Request) error {
sig, err := s.buildSignature(id, key, r)
if err != nil {
return err
}
r.Header.Add(headerSignature, sig.String())
return nil
}
// AuthRequest adds a http signature to the Authorization: HTTP Header
func (s Signer) AuthRequest(id, key string, r *http.Request) error {
sig, err := s.buildSignature(id, key, r)
if err != nil {
return err
}
r.Header.Add(headerAuthorization, authScheme+sig.String())
return nil
}
func (s Signer) buildSignature(id, key string, r *http.Request) (*Signature, error) {
if r.Header.Get("date") == "" {
r.Header.Set("date", time.Now().Format(time.RFC1123))
}
sig := &Signature{
KeyID: id,
Algorithm: s.algorithm,
Headers: s.headers,
}
err := sig.sign(key, r)
if err != nil {
return nil, err
}
return sig, nil
}

191
vendor/github.com/go-ap/httpsig/LICENSE generated vendored Normal file
View file

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

54
vendor/github.com/go-ap/httpsig/Makefile generated vendored Normal file
View file

@ -0,0 +1,54 @@
GOPATH := $(shell go env GOPATH)
all: build
getdeps:
@echo "Installing golint" && go get -u golang.org/x/lint/golint
@echo "Installing gocyclo" && go get -u github.com/fzipp/gocyclo
@echo "Installing deadcode" && go get -u github.com/remyoudompheng/go-misc/deadcode
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
@echo "Installing ineffassign" && go get -u github.com/gordonklaus/ineffassign
verifiers: vet fmt lint cyclo spelling static deadcode
vet:
@echo "Running $@"
@go vet -atomic -bool -copylocks -nilfunc -printf -rangeloops -unreachable -unsafeptr -unusedresult ./...
fmt:
@echo "Running $@"
@gofmt -d .
lint:
@echo "Running $@"
@${GOPATH}/bin/golint -set_exit_status $(shell go list ./...)
ineffassign:
@echo "Running $@"
@${GOPATH}/bin/ineffassign .
cyclo:
@echo "Running $@"
@${GOPATH}/bin/gocyclo -over 100 .
deadcode:
@echo "Running $@"
@${GOPATH}/bin/deadcode -test $(shell go list ./...) || true
spelling:
@echo "Running $@"
@${GOPATH}/bin/misspell -i monitord -error `find .`
static:
@echo "Running $@"
go run honnef.co/go/tools/cmd/staticcheck -- ./...
check: test
test: verifiers build
go test -v ./...
testrace: verifiers build
go test -v -race ./...
build:
go build -v ./...

138
vendor/github.com/go-ap/httpsig/README.md generated vendored Normal file
View file

@ -0,0 +1,138 @@
# HTTPSIG for Go
This library implements HTTP request signature generation and verification based on
the RFC draft specification https://tools.ietf.org/html/draft-cavage-http-signatures-12.
The library strives be compatible with the popular python library of the same
name: https://github.com/ahknight/httpsig
## Installing
```
go get gopkg.in/spacemonkeygo/httpsig.v0
```
## Signing Requests
Signing requests is done by constructing a new `Signer`. The key id, key,
algorithm, and what headers to sign are required.
For example to construct a `Signer` with key id `"foo"`, using an RSA private
key, for the rsa-sha256 algorithm, with the default header set, you can do:
```go
var key *rsa.PrivateKey = ...
signer := httpsig.NewSigner("foo", key, httpsig.RSASHA256, nil)
```
There are helper functions for specific algorithms that are less verbose and
provide more type safety (the key paramater need not be of type `interface{}`
because the type required for the algorithm is known).
```go
var key *rsa.PrivateKey = ...
signer := httpsig.NewRSASHA256Signer("foo", key, nil)
```
By default, if no headers are passed to `NewSigner` (or the helpers), the
`(request-target)` pseudo-header and `Date` header are signed.
To sign requests, call the `Sign()` method. The method signs the request and
adds an `Authorization` header containing the signature parameters.
```go
err = signer.Sign(req)
if err != nil {
...
}
fmt.Println("AUTHORIZATION:", req.Header.Get('Authorization'))
...
AUTHORIZATION: Signature: keyId="foo",algorithm="sha-256",signature="..."
```
## Verifying Requests
Verifying requests is done by constructing a new `Verifier`. The verifier
requires a KeyGetter implementation to look up keys based on `keyId`'s
retrieved from signature parameters.
```go
var getter httpsig.KeyGetter = ....
verifier := httpsig.NewVerifier(getter)
```
A request can be verified by calling the `Verify()` method:
```go
err = verifier.Verify(req)
```
By default, the verifier only requires the `Date` header to be included
in the signature. The set of required headers be changed using the
`SetRequiredHeaders()` method to enforce stricter requirements.
```go
verifier.SetRequiredHeaders([]string{"(request-target)", "host", "date"})
```
Requests that don't include the full set of required headers in the `headers`
signature parameter (either implicitly or explicitly) will fail verification.
**Note that required headers are simply a specification for which headers must
be included in the signature, and does not enforce header presence in requests.
It is up to callers to validate header contents (or the lack thereof).**
A simple in-memory key store is provided by the library and can be constructed
with the `NewMemoryKeyStore()` function. Keys can be added using the SetKey
method:
```go
keystore := NewMemoryKeyStore()
var rsa_key *rsa.PublicKey = ...
keystore.SetKey("foo", rsa_key)
var hmac_key []byte = ...
keystore.SetKey("foo", hmac_key)
```
## Handler
A convenience function is provided that wraps an `http.Handler` and verifies
incoming request signatures before passing them down to the wrapped handler.
If requires a verifier and optionally a realm (for constructing the
`WWW-Authenticate` header).
```go
var handler http.Handler = ...
var verifier *httpsig.Verifier = ...
wrapped := httpsig.RequireSignature(handler, verifier, "example.com")
```
If signature validation fails, a `401` is returned along with a
`WWW-Authenticate` header containing a `Signature` challenge with optional
`realm` and `headers` parameters.
## Supported algorithms
- rsa-sha1 (using PKCS1v15)
- rsa-sha256 (using PKCS1v15)
- hmac-sha256
- ed25519
### License
Copyright (C) 2017 Space Monkey, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

140
vendor/github.com/go-ap/httpsig/common.go generated vendored Normal file
View file

@ -0,0 +1,140 @@
// Copyright (C) 2017 Space Monkey, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpsig
import (
ed "crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"fmt"
"io"
"net/http"
"strings"
"time"
)
var (
// Rand is a hookable reader used as a random byte source.
Rand io.Reader = rand.Reader
)
// requestPath returns the :path pseudo header according to the HTTP/2 spec.
func requestPath(req *http.Request) string {
path := req.URL.Path
if path == "" {
path = "/"
}
if req.URL.RawQuery != "" {
path += "?" + req.URL.RawQuery
}
return path
}
// BuildSignatureString constructs a signature string following section 2.3
func BuildSignatureString(req *http.Request, headers []string, created, expires time.Time) (string, error) {
if len(headers) == 0 {
headers = []string{"(created)"}
}
values := make([]string, 0, len(headers))
for _, h := range headers {
switch h {
case "(request-target)":
values = append(values, fmt.Sprintf("%s: %s %s",
h, strings.ToLower(req.Method), requestPath(req)))
case "(created)":
values = append(values, fmt.Sprintf("%s: %d", h, created.Unix()))
case "(expires)":
values = append(values, fmt.Sprintf("%s: %d", h, expires.Unix()))
case "host":
values = append(values, fmt.Sprintf("%s: %s", h, req.Host))
case "date":
if req.Header.Get(h) == "" {
req.Header.Set(h, time.Now().UTC().Format(http.TimeFormat))
}
values = append(values, fmt.Sprintf("%s: %s", h, req.Header.Get(h)))
default:
vs, found := req.Header[http.CanonicalHeaderKey(h)]
if !found {
return "", fmt.Errorf("expected %s to exists", h)
}
for _, v := range vs {
values = append(values, fmt.Sprintf("%s: %s", h, strings.TrimSpace(v)))
}
}
}
return strings.Join(values, "\n"), nil
}
// BuildSignatureData is a convenience wrapper around BuildSignatureString that
// returns []byte instead of a string.
func BuildSignatureData(req *http.Request, headers []string, created, expires time.Time) ([]byte, error) {
s, err := BuildSignatureString(req, headers, created, expires)
return []byte(s), err
}
func toRSAPrivateKey(key interface{}) *rsa.PrivateKey {
switch k := key.(type) {
case *rsa.PrivateKey:
return k
default:
return nil
}
}
func toRSAPublicKey(key interface{}) *rsa.PublicKey {
switch k := key.(type) {
case *rsa.PublicKey:
return k
case *rsa.PrivateKey:
return &k.PublicKey
default:
return nil
}
}
func toHMACKey(key interface{}) []byte {
switch k := key.(type) {
case []byte:
return k
default:
return nil
}
}
func toEd25519PrivateKey(key interface{}) ed.PrivateKey {
switch k := key.(type) {
case ed.PrivateKey:
return k
default:
return nil
}
}
func toEd25519PublicKey(key interface{}) ed.PublicKey {
switch k := key.(type) {
case ed.PublicKey:
return k
case ed.PrivateKey:
return k.Public().(ed.PublicKey)
default:
return nil
}
}
func unsupportedAlgorithm(a Algorithm) error {
return fmt.Errorf("key does not support algorithm %q", a.Name())
}

58
vendor/github.com/go-ap/httpsig/ed25519.go generated vendored Normal file
View file

@ -0,0 +1,58 @@
package httpsig
import (
ed "crypto/ed25519"
"fmt"
)
// Ed25519 implements Ed25519 Algorithm
var Ed25519 Algorithm = ed25519{}
type ed25519 struct{}
func (ed25519) Name() string {
return "ed25519"
}
func (a ed25519) Sign(key interface{}, data []byte) ([]byte, error) {
k := toEd25519PrivateKey(key)
if k == nil {
return nil, unsupportedAlgorithm(a)
}
return Ed25519Sign(k, data)
}
func (a ed25519) Verify(key interface{}, data, sig []byte) error {
k := toHMACKey(key)
if k == nil {
return unsupportedAlgorithm(a)
}
return Ed25519Verify(k, data, sig)
}
// Ed25519Verify reports whether sig is a valid signature of message by publicKey.
func Ed25519Verify(key interface{}, message, sig []byte) error {
k, ok := key.(ed.PublicKey)
if !ok {
return fmt.Errorf("key must be an instance of crypto/ed25519.PublicKey")
}
if len(k) != ed.PublicKeySize {
return fmt.Errorf("public key has the wrong size")
}
if !ed.Verify(k, message, sig) {
return fmt.Errorf("signature verification failed")
}
return nil
}
// Ed25519Sign signs the message with privateKey and returns a signature.
func Ed25519Sign(key interface{}, message []byte) ([]byte, error) {
k, ok := key.(ed.PrivateKey)
if !ok {
return nil, fmt.Errorf("key must be an instance of crypto/ed25519.PrivateKey")
}
if len(k) != ed.PrivateKeySize {
return nil, fmt.Errorf("private key has the wrong size")
}
return ed.Sign(k, message), nil
}

9
vendor/github.com/go-ap/httpsig/go.mod generated vendored Normal file
View file

@ -0,0 +1,9 @@
module github.com/go-ap/httpsig
go 1.14
require (
github.com/kr/pretty v0.1.0 // indirect
github.com/stretchr/testify v1.5.1
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

18
vendor/github.com/go-ap/httpsig/go.sum generated vendored Normal file
View file

@ -0,0 +1,18 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

76
vendor/github.com/go-ap/httpsig/handler.go generated vendored Normal file
View file

@ -0,0 +1,76 @@
// Copyright (C) 2017 Space Monkey, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpsig
import (
"context"
"fmt"
"net/http"
"strings"
)
// ctxKeyIDType is the type used to retrieve the KeyId parametes extracted from the HTTP headers
// and set into the request.Context during call of verifier.Verify
type ctxKeyIDType struct{}
var ctxKeyIDKey = &ctxKeyIDType{}
// WithKeyID retrieves the KeyId parameter from the requests
func WithKeyID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, ctxKeyIDKey, id)
}
// KeyIDFromContext returns the request ID from the context.
// A zero ID is returned if there are no identifers in the
// current context.
func KeyIDFromContext(ctx context.Context) string {
v := ctx.Value(ctxKeyIDKey)
if v == nil {
return ""
}
return v.(string)
}
// RequireSignature is a http middleware that ensure the incoming request have
// the required signature using verifier v
func RequireSignature(h http.Handler, v *Verifier, realm string) (
out http.Handler) {
var challengeParams []string
if realm != "" {
challengeParams = append(challengeParams,
fmt.Sprintf("realm=%q", realm))
}
if headers := v.RequiredHeaders(); len(headers) > 0 {
challengeParams = append(challengeParams,
fmt.Sprintf("headers=%q", strings.Join(headers, " ")))
}
challenge := "Signature"
if len(challengeParams) > 0 {
challenge += fmt.Sprintf(" %s", strings.Join(challengeParams, ", "))
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
keyID, err := v.Verify(req)
if err != nil {
w.Header()["WWW-Authenticate"] = []string{challenge}
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, err.Error())
return
}
h.ServeHTTP(w, req.WithContext(WithKeyID(req.Context(), keyID)))
})
}

68
vendor/github.com/go-ap/httpsig/hmac.go generated vendored Normal file
View file

@ -0,0 +1,68 @@
// Copyright (C) 2017 Space Monkey, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpsig
import (
"crypto"
"crypto/hmac"
"errors"
)
// HMACSHA256 implements keyed HMAC over SHA256 digests
var HMACSHA256 Algorithm = hmacSha256{}
type hmacSha256 struct{}
func (hmacSha256) Name() string {
return "hmac-sha256"
}
func (a hmacSha256) Sign(key interface{}, data []byte) ([]byte, error) {
k := toHMACKey(key)
if k == nil {
return nil, unsupportedAlgorithm(a)
}
return HMACSign(k, crypto.SHA256, data)
}
func (a hmacSha256) Verify(key interface{}, data, sig []byte) error {
k := toHMACKey(key)
if k == nil {
return unsupportedAlgorithm(a)
}
return HMACVerify(k, crypto.SHA256, data, sig)
}
// HMACSign signs a digest of the data hashed using the provided hash and key.
func HMACSign(key []byte, hash crypto.Hash, data []byte) ([]byte, error) {
h := hmac.New(hash.New, key)
if _, err := h.Write(data); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// HMACVerify verifies a signed digest of the data hashed using the provided
// hash and key.
func HMACVerify(key []byte, hash crypto.Hash, data, sig []byte) error {
actualSig, err := HMACSign(key, hash, data)
if err != nil {
return err
}
if !hmac.Equal(actualSig, sig) {
return errors.New("hmac signature mismatch")
}
return nil
}

44
vendor/github.com/go-ap/httpsig/httpsig.go generated vendored Normal file
View file

@ -0,0 +1,44 @@
// Copyright (C) 2017 Space Monkey, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpsig
// Algorithm provides methods used to sign/verify signatures.
type Algorithm interface {
Name() string
Sign(key interface{}, data []byte) (sig []byte, err error)
Verify(key interface{}, data, sig []byte) error
}
// KeyGetter is an interface used by the verifier to retrieve a key stored
// by key id.
//
// The following types are supported for the specified algorithms:
// []byte - HMAC signatures
// *rsa.PublicKey - RSA signatures
// *rsa.PrivateKey - RSA signatures
//
// Other types will treated as if no key was returned.
type KeyGetter interface {
GetKey(id string) (interface{}, error)
}
// KeyGetterFunc is a convenience type for implementing a KeyGetter with a
// regular function
type KeyGetterFunc func(id string) (interface{}, error)
// GetKey calls fn(id)
func (fn KeyGetterFunc) GetKey(id string) (interface{}, error) {
return fn(id)
}

44
vendor/github.com/go-ap/httpsig/keystore.go generated vendored Normal file
View file

@ -0,0 +1,44 @@
// Copyright (C) 2017 Space Monkey, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpsig
import "fmt"
// MemoryKeyStore is a simple in memory key store that implement the
// KeyGetter interface
type MemoryKeyStore struct {
keys map[string]interface{}
}
// NewMemoryKeyStore creates a new MemoryKeyStore
func NewMemoryKeyStore() *MemoryKeyStore {
return &MemoryKeyStore{
keys: make(map[string]interface{}),
}
}
// GetKey implements KeyGetter interface
func (m *MemoryKeyStore) GetKey(id string) (interface{}, error) {
pk, ok := m.keys[id]
if !ok {
return nil, fmt.Errorf("key not found")
}
return pk, nil
}
// SetKey link id to a key
func (m *MemoryKeyStore) SetKey(id string, key interface{}) {
m.keys[id] = key
}

92
vendor/github.com/go-ap/httpsig/rsa.go generated vendored Normal file
View file

@ -0,0 +1,92 @@
// Copyright (C) 2017 Space Monkey, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpsig
import (
"crypto"
"crypto/rsa"
)
// RSASHA1 implements RSA PKCS1v15 signatures over a SHA1 digest
var RSASHA1 Algorithm = rsaSha1{}
type rsaSha1 struct{}
func (rsaSha1) Name() string {
return "rsa-sha1"
}
func (a rsaSha1) Sign(key interface{}, data []byte) ([]byte, error) {
k := toRSAPrivateKey(key)
if k == nil {
return nil, unsupportedAlgorithm(a)
}
return RSASign(k, crypto.SHA1, data)
}
func (a rsaSha1) Verify(key interface{}, data, sig []byte) error {
k := toRSAPublicKey(key)
if k == nil {
return unsupportedAlgorithm(a)
}
return RSAVerify(k, crypto.SHA1, data, sig)
}
// RSASHA256 implements RSA PKCS1v15 signatures over a SHA256 digest
var RSASHA256 Algorithm = rsaSha256{}
type rsaSha256 struct{}
func (rsaSha256) Name() string {
return "rsa-sha256"
}
func (a rsaSha256) Sign(key interface{}, data []byte) ([]byte, error) {
k := toRSAPrivateKey(key)
if k == nil {
return nil, unsupportedAlgorithm(a)
}
return RSASign(k, crypto.SHA256, data)
}
func (a rsaSha256) Verify(key interface{}, data, sig []byte) error {
k := toRSAPublicKey(key)
if k == nil {
return unsupportedAlgorithm(a)
}
return RSAVerify(k, crypto.SHA256, data, sig)
}
// RSASign signs a digest of the data hashed using the provided hash
func RSASign(key *rsa.PrivateKey, hash crypto.Hash, data []byte) (
signature []byte, err error) {
h := hash.New()
if _, err := h.Write(data); err != nil {
return nil, err
}
return rsa.SignPKCS1v15(Rand, key, hash, h.Sum(nil))
}
// RSAVerify verifies a signed digest of the data hashed using the provided hash
func RSAVerify(key *rsa.PublicKey, hash crypto.Hash, data, sig []byte) (
err error) {
h := hash.New()
if _, err := h.Write(data); err != nil {
return err
}
return rsa.VerifyPKCS1v15(key, hash, h.Sum(nil), sig)
}

122
vendor/github.com/go-ap/httpsig/sign.go generated vendored Normal file
View file

@ -0,0 +1,122 @@
// Copyright (C) 2017 Space Monkey, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpsig
import (
"crypto"
"crypto/rsa"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
)
// Signer is the type used by HTTP clients to sign their request
type Signer struct {
id string
key interface{}
algo Algorithm
headers []string
}
// NewSigner contructs a signer with the specified key id, key, algorithm,
// and headers to sign. By default, if headers is nil or empty, the
// request-target and date headers will be signed.
func NewSigner(id string, key interface{}, algo Algorithm, headers []string) (
signer *Signer) {
s := &Signer{
id: id,
key: key,
algo: algo,
}
// copy the headers slice, lowercasing as necessary
if len(headers) == 0 {
headers = []string{"(request-target)", "date"}
}
s.headers = make([]string, 0, len(headers))
for _, header := range headers {
s.headers = append(s.headers, strings.ToLower(header))
}
return s
}
// NewRSASHA1Signer contructs a signer with the specified key id, rsa private
// key and headers to sign.
func NewRSASHA1Signer(id string, key *rsa.PrivateKey, headers []string) (signer *Signer) {
return NewSigner(id, key, RSASHA1, headers)
}
// NewRSASHA256Signer contructs a signer with the specified key id, rsa private
// key and headers to sign.
func NewRSASHA256Signer(id string, key *rsa.PrivateKey, headers []string) (signer *Signer) {
return NewSigner(id, key, RSASHA256, headers)
}
// NewHMACSHA256Signer contructs a signer with the specified key id, hmac key,
// and headers to sign.
func NewHMACSHA256Signer(id string, key []byte, headers []string) (
signer *Signer) {
return NewSigner(id, key, HMACSHA256, headers)
}
// NewEd25519Signer contructs a signer with the specified key id, Ed25519 key,
// and headers to sign.
func NewEd25519Signer(id string, key crypto.PrivateKey, headers []string) *Signer {
return NewSigner(id, key, Ed25519, headers)
}
// Sign signs an http request and adds the signature to the authorization header
func (r *Signer) Sign(req *http.Request) error {
now := time.Now()
params, err := signRequest(r.id, r.key, r.algo, r.headers, now, now.Add(time.Minute), req)
if err != nil {
return err
}
req.Header.Set("Authorization", "Signature "+params)
return nil
}
// signRequest signs an http request and returns the parameter string.
func signRequest(id string, key interface{}, algo Algorithm, headers []string, created, expires time.Time, req *http.Request) (params string, err error) {
signatureData, err := BuildSignatureData(req, headers, created, expires)
if err != nil {
return "", err
}
signature, err := algo.Sign(key, signatureData)
if err != nil {
return "", err
}
// The headers parameter can be omitted if the only header is "Date". The
// receiving end assumes ["date"] if no headers parameter is present.
var headersParam string
if !(len(headers) == 1 && headers[0] == "date") {
headersParam = fmt.Sprintf("headers=%q,", strings.Join(headers, " "))
}
return fmt.Sprintf(
"keyId=%q,algorithm=%q,created=\"%d\",expires=\"%d\",%ssignature=%q",
id,
algo.Name(),
created.Unix(),
expires.Unix(),
headersParam,
base64.StdEncoding.EncodeToString(signature)), nil
}

249
vendor/github.com/go-ap/httpsig/verify.go generated vendored Normal file
View file

@ -0,0 +1,249 @@
// Copyright (C) 2017 Space Monkey, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpsig
import (
"crypto"
"encoding/base64"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"
)
// Verifier is used by by the HTTP server to verify the incoming HTTP requests
type Verifier struct {
keyGetter KeyGetter
requiredHeaders []string
}
// NewVerifier creates a new Verifier using kg to get the key
// mapped to the ID received in the requests
func NewVerifier(kg KeyGetter) *Verifier {
v := &Verifier{
keyGetter: kg,
}
v.SetRequiredHeaders(nil)
return v
}
// RequiredHeaders returns the required header the client have to include in
// the signature
func (v *Verifier) RequiredHeaders() []string {
return append([]string{}, v.requiredHeaders...)
}
// SetRequiredHeaders set the list of headers to be included by the client to generate the signature
func (v *Verifier) SetRequiredHeaders(headers []string) {
if len(headers) == 0 {
headers = []string{"date"}
}
requiredHeaders := make([]string, 0, len(headers))
for _, h := range headers {
requiredHeaders = append(requiredHeaders, strings.ToLower(h))
}
v.requiredHeaders = requiredHeaders
}
// Verify parses req and verify the signature using the key returned by
// the keyGetter. It returns the KeyId parameter from he signature header
// and a nil error if the signature verifies, an error otherwise
func (v *Verifier) Verify(req *http.Request) (string, error) {
// retrieve and validate params from the request
params := getParamsFromAuthHeader(req)
if params == nil {
return "", fmt.Errorf("no params present")
}
if params.KeyID == "" {
return "", fmt.Errorf("keyId is required")
}
if params.Algorithm == "" {
return "", fmt.Errorf("algorithm is required")
}
if len(params.Signature) == 0 {
return "", fmt.Errorf("signature is required")
}
if len(params.Headers) == 0 {
params.Headers = []string{"date"}
}
header_check:
for _, h := range v.requiredHeaders {
for _, header := range params.Headers {
if strings.EqualFold(h, header) {
continue header_check
}
}
return "", fmt.Errorf("missing required header in signature %q",
h)
}
// calculate signature string for request
sigData, err := BuildSignatureData(req, params.Headers, params.Created, params.Expires)
if err != nil {
return "", err
}
// look up key based on keyId
key, err := v.keyGetter.GetKey(params.KeyID)
if err != nil {
return "", err
}
// we still leave this sanity check
if key == nil {
return "", fmt.Errorf("no key with id %q", params.KeyID)
}
switch params.Algorithm {
case "rsa-sha1":
rsaPubkey := toRSAPublicKey(key)
if rsaPubkey == nil {
return "", fmt.Errorf("algorithm %q is not supported by key %q",
params.Algorithm, params.KeyID)
}
return params.KeyID, RSAVerify(rsaPubkey, crypto.SHA1, sigData, params.Signature)
case "rsa-sha256":
rsaPubkey := toRSAPublicKey(key)
if rsaPubkey == nil {
return "", fmt.Errorf("algorithm %q is not supported by key %q",
params.Algorithm, params.KeyID)
}
return params.KeyID, RSAVerify(rsaPubkey, crypto.SHA256, sigData, params.Signature)
case "hmac-sha256":
hmacKey := toHMACKey(key)
if hmacKey == nil {
return "", fmt.Errorf("algorithm %q is not supported by key %q",
params.Algorithm, params.KeyID)
}
return params.KeyID, HMACVerify(hmacKey, crypto.SHA256, sigData, params.Signature)
case "ed25519":
ed25519Key := toEd25519PublicKey(key)
if ed25519Key == nil {
return "", fmt.Errorf("algorithm %q is not supported by key %q",
params.Algorithm, params.KeyID)
}
return params.KeyID, Ed25519Verify(ed25519Key, sigData, params.Signature)
default:
return "", fmt.Errorf("unsupported algorithm %q", params.Algorithm)
}
}
// paramRE scans out recognized parameter keypairs. accepted values are those
// that are quoted
var paramRE = regexp.MustCompile(`(?U)\s*([a-zA-Z][a-zA-Z0-9_]*)\s*=\s*"(.*)"\s*`)
// Params holds the field requires to build the signature string
type Params struct {
KeyID string
Algorithm string
Headers []string
Signature []byte
Created time.Time
Expires time.Time
}
func getParamsFromAuthHeader(req *http.Request) *Params {
return getParams(req, "Authorization", "Signature ")
}
func getParams(req *http.Request, header, prefix string) *Params {
values := req.Header[http.CanonicalHeaderKey(header)]
// last well-formed parameter wins
for i := len(values) - 1; i >= 0; i-- {
value := values[i]
if prefix != "" {
if trimmed := strings.TrimPrefix(value, prefix); trimmed != value {
value = trimmed
} else {
continue
}
}
matches := paramRE.FindAllStringSubmatch(value, -1)
if matches == nil {
continue
}
params := Params{}
// malformed parameters get ignored.
for _, match := range matches {
switch match[1] {
case "keyId":
params.KeyID = match[2]
case "algorithm":
if algorithm, ok := parseAlgorithm(match[2]); ok {
params.Algorithm = algorithm
}
case "headers":
if headers, ok := parseHeaders(match[2]); ok {
params.Headers = headers
}
case "signature":
if signature, ok := parseSignature(match[2]); ok {
params.Signature = signature
}
case "created":
if created, ok := parseTime(match[2]); ok {
params.Created = created
}
case "expires":
if expires, ok := parseTime(match[2]); ok {
params.Expires = expires
}
}
}
return &params
}
return nil
}
// parseAlgorithm parses recognized algorithm values
func parseAlgorithm(s string) (algorithm string, ok bool) {
s = strings.TrimSpace(s)
switch s {
case "rsa-sha1", "rsa-sha256", "hmac-sha256", "ed25519":
return s, true
}
return "", false
}
// parseHeaders parses a space separated list of header values.
func parseHeaders(s string) (headers []string, ok bool) {
for _, header := range strings.Split(s, " ") {
if header != "" {
headers = append(headers, strings.ToLower(header))
}
}
return headers, true
}
func parseSignature(s string) (signature []byte, ok bool) {
signature, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, false
}
return signature, true
}
func parseTime(s string) (t time.Time, ok bool) {
sec, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return t, false
}
return time.Unix(sec, 0), true
}

6
vendor/modules.txt vendored
View file

@ -3,9 +3,6 @@
# code.gitea.io/sdk/gitea v0.15.1-0.20220501190934-319a978c6c71
## explicit
code.gitea.io/sdk/gitea
# github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e
## explicit
github.com/99designs/httpsignatures-go
# github.com/Antonboom/errname v0.1.5
github.com/Antonboom/errname/pkg/analyzer
# github.com/Antonboom/nilnil v0.1.0
@ -151,6 +148,9 @@ github.com/gin-gonic/gin/binding
github.com/gin-gonic/gin/internal/bytesconv
github.com/gin-gonic/gin/internal/json
github.com/gin-gonic/gin/render
# github.com/go-ap/httpsig v0.0.0-20210714162115-62a09257db51
## explicit
github.com/go-ap/httpsig
# github.com/go-critic/go-critic v0.6.2
github.com/go-critic/go-critic/checkers
github.com/go-critic/go-critic/checkers/internal/astwalk