mirror of
https://github.com/ncarlier/webhookd.git
synced 2025-04-07 18:16:12 +00:00
feat(): finalize HTTP signature support
This commit is contained in:
parent
c16ec83a5a
commit
43204677d3
|
@ -311,6 +311,8 @@ $ curl -X POST \
|
||||||
"http://loclahost:8080/echo?msg=hello"
|
"http://loclahost:8080/echo?msg=hello"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can find a small HTTP client in the ["tooling" directory](./tooling/httpsig/README.md) that is capable of forging HTTP signatures.
|
||||||
|
|
||||||
### TLS
|
### TLS
|
||||||
|
|
||||||
You can activate TLS to secure communications:
|
You can activate TLS to secure communications:
|
||||||
|
|
|
@ -12,8 +12,8 @@ func HTTPSignature(inner http.Handler, keyStore pubkey.KeyStore) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
verifier, err := httpsig.NewVerifier(r)
|
verifier, err := httpsig.NewVerifier(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(400)
|
||||||
w.Write([]byte("unable to initialize HTTP signature verifier: " + err.Error()))
|
w.Write([]byte("invalid HTTP signature: " + err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pubKeyID := verifier.KeyId()
|
pubKeyID := verifier.KeyId()
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/go-fed/httpsig"
|
"github.com/go-fed/httpsig"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultAlgorithm = httpsig.RSA_SHA256
|
const defaultAlgorithm = httpsig.RSA_SHA256
|
||||||
|
@ -28,17 +29,17 @@ func (ks *directoryKeyStore) Get(keyID string) (crypto.PublicKey, httpsig.Algori
|
||||||
return key, defaultAlgorithm, nil
|
return key, defaultAlgorithm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDirectoryKeyStore(root string) (store *directoryKeyStore, err error) {
|
func newDirectoryKeyStore(root string) (*directoryKeyStore, error) {
|
||||||
store = &directoryKeyStore{
|
store := &directoryKeyStore{
|
||||||
algorithm: "",
|
algorithm: "",
|
||||||
keys: make(map[string]crypto.PublicKey),
|
keys: make(map[string]crypto.PublicKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
walkErr := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if filepath.Ext(path) == "pem" {
|
if filepath.Ext(path) == ".pem" {
|
||||||
data, err := ioutil.ReadFile(path)
|
data, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -51,7 +52,7 @@ func newDirectoryKeyStore(root string) (store *directoryKeyStore, err error) {
|
||||||
|
|
||||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
rsaPublicKey, ok := pub.(*rsa.PublicKey)
|
rsaPublicKey, ok := pub.(*rsa.PublicKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -61,9 +62,10 @@ func newDirectoryKeyStore(root string) (store *directoryKeyStore, err error) {
|
||||||
keyID, ok := block.Headers["key_id"]
|
keyID, ok := block.Headers["key_id"]
|
||||||
if ok {
|
if ok {
|
||||||
store.keys[keyID] = rsaPublicKey
|
store.keys[keyID] = rsaPublicKey
|
||||||
|
logger.Debug.Println("HTTP signature public key loaded: ", path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return
|
return store, walkErr
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,10 @@ package pubkey
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-fed/httpsig"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-fed/httpsig"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyStore is a generic interface to retrieve a public key
|
// KeyStore is a generic interface to retrieve a public key
|
||||||
|
@ -19,14 +21,14 @@ func NewKeyStore(uri string) (store KeyStore, err error) {
|
||||||
}
|
}
|
||||||
u, err := url.Parse(uri)
|
u, err := url.Parse(uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid KeyStore URL: %s", uri)
|
return nil, fmt.Errorf("invalid KeyStore URI: %s", uri)
|
||||||
}
|
}
|
||||||
switch u.Scheme {
|
switch u.Scheme {
|
||||||
case "file":
|
case "file":
|
||||||
store, err = newDirectoryKeyStore(u.RawPath)
|
store, err = newDirectoryKeyStore(strings.TrimPrefix(uri, "file://"))
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("non supported KeyStore URL: %s", uri)
|
err = fmt.Errorf("non supported KeyStore URI: %s", uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
return store, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
26
pkg/pubkey/test/keystore_test.go
Normal file
26
pkg/pubkey/test/keystore_test.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-fed/httpsig"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/assert"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/logger"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/pubkey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeyStore(t *testing.T) {
|
||||||
|
logger.Init("warn")
|
||||||
|
|
||||||
|
ks, err := pubkey.NewKeyStore("file://.")
|
||||||
|
assert.Nil(t, err, "")
|
||||||
|
assert.NotNil(t, ks, "")
|
||||||
|
|
||||||
|
pk, algo, err := ks.Get("test")
|
||||||
|
assert.Nil(t, err, "")
|
||||||
|
assert.NotNil(t, pk, "")
|
||||||
|
assert.Equal(t, httpsig.RSA_SHA256, algo, "")
|
||||||
|
|
||||||
|
_, _, err = ks.Get("notfound")
|
||||||
|
assert.NotNil(t, err, "")
|
||||||
|
}
|
11
pkg/pubkey/test/test-key.pem
Normal file
11
pkg/pubkey/test/test-key.pem
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
key_id: test
|
||||||
|
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwdCB5DZD0cFeJYUu1W3I
|
||||||
|
lNN9y+NZC/Jqktdkn8/WHlXec07nesyDyicveduuaaDBeUoR3imRUtTS+eFKvDR/
|
||||||
|
HMke8HmoleZvADJ0ppqbvj0YZOhWp2SqZvAKJbce8D+OSKuLGpxqq6FLl0cq+Fv+
|
||||||
|
mFpZyDcqPZtrwAz+AbJp6sxvVvNb0r0Z1LxEIVb0JHHLhsJkxJ06PcbT2tnvaPHT
|
||||||
|
8S80cvFO57zFpiX/M8gQNaRCqwD1/sHEGJc6Av+WUBhrE7pz0XGMLjU4n7W6Ooyz
|
||||||
|
g2QdBiLE3tW8HYL9iJ7EuLG5apYbFVVHJHl8HNWpmD0q8sVExvx5+AKQ3kEDp68m
|
||||||
|
ywIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
40
tooling/httpsig/Makefile
Normal file
40
tooling/httpsig/Makefile
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
.SILENT :
|
||||||
|
|
||||||
|
export GO111MODULE=on
|
||||||
|
|
||||||
|
# App name
|
||||||
|
APPNAME=httpsig
|
||||||
|
|
||||||
|
# Go configuration
|
||||||
|
GOOS?=linux
|
||||||
|
GOARCH?=amd64
|
||||||
|
|
||||||
|
# Add exe extension if windows target
|
||||||
|
is_windows:=$(filter windows,$(GOOS))
|
||||||
|
EXT:=$(if $(is_windows),".exe","")
|
||||||
|
|
||||||
|
# Archive name
|
||||||
|
ARCHIVE=$(APPNAME)-$(GOOS)-$(GOARCH).tgz
|
||||||
|
|
||||||
|
# Executable name
|
||||||
|
EXECUTABLE=$(APPNAME)$(EXT)
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm -rf release
|
||||||
|
.PHONY: clean
|
||||||
|
|
||||||
|
## Build executable
|
||||||
|
build:
|
||||||
|
-mkdir -p release
|
||||||
|
echo "Building: $(EXECUTABLE) $(VERSION) for $(GOOS)-$(GOARCH) ..."
|
||||||
|
GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(LDFLAGS) -o release/$(EXECUTABLE)
|
||||||
|
.PHONY: build
|
||||||
|
|
||||||
|
release/$(EXECUTABLE): build
|
||||||
|
|
||||||
|
key:
|
||||||
|
openssl genrsa -out key.pem 2048
|
||||||
|
openssl rsa -in key.pem -outform PEM -pubout -out key-pub.pem
|
||||||
|
.PHONY: key
|
33
tooling/httpsig/README.md
Normal file
33
tooling/httpsig/README.md
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# httpsig
|
||||||
|
|
||||||
|
A simple HTTP client with HTTP signature support.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
- Generate an RSA key: `make key`
|
||||||
|
|
||||||
|
- Add `key_id` header to public key:
|
||||||
|
|
||||||
|
```pem
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
key_id: my-key
|
||||||
|
|
||||||
|
MIIEowIBAAKCAQEAwdCB5DZD0cFeJYUu1W3IlNN9y+NZC/Jqktdkn8/WHlXec07n
|
||||||
|
...
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
```
|
||||||
|
|
||||||
|
- Start Webhookd with HTTP signature support:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ webhookd -key-store-uri file://.
|
||||||
|
```
|
||||||
|
|
||||||
|
- Make HTTP signed request:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./release/httpsig \
|
||||||
|
--key-id my-key \
|
||||||
|
--key-file ./key.pem \
|
||||||
|
http://localhost:8080/echo`
|
||||||
|
```
|
8
tooling/httpsig/go.mod
Normal file
8
tooling/httpsig/go.mod
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module github.com/ncarlier/webhookd/tooling/httpsig
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-fed/httpsig v0.1.0
|
||||||
|
github.com/ncarlier/webhookd v1.7.0
|
||||||
|
)
|
19
tooling/httpsig/go.sum
Normal file
19
tooling/httpsig/go.sum
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
github.com/go-fed/httpsig v0.1.0 h1:6F2OxRVnNTN4OPN+Mc2jxs2WEay9/qiHT/jphlvAwIY=
|
||||||
|
github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||||
|
github.com/ncarlier/webhookd v1.7.0 h1:9oJs7Ihe8T2jG04OTyNJv9pbaKRi1nk0k8qRdaoFYZY=
|
||||||
|
github.com/ncarlier/webhookd v1.7.0/go.mod h1:Y+KgOmrNoNpjWc3VazWaWQMvJGEm9dY+waVdhP/rxbg=
|
||||||
|
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
|
||||||
|
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi7zUfwPyXo23HNjMnXPz7w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43 h1:PvnWIWTbA7gsEBkKjt0HV9hckYfcqYv8s/ju7ArZ0do=
|
||||||
|
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
115
tooling/httpsig/main.go
Normal file
115
tooling/httpsig/main.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-fed/httpsig"
|
||||||
|
configflag "github.com/ncarlier/webhookd/pkg/config/flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
KeyID string `flag:"key-id" desc:"Signature key ID"`
|
||||||
|
KeyFile string `flag:"key-file" desc:"Public key file (PEM format)" default:"./key.pem"`
|
||||||
|
JSON string `flag:"json" desc:"JSON payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
conf := &config{}
|
||||||
|
configflag.Bind(conf, "HTTP_SIG")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if conf.KeyID == "" {
|
||||||
|
log.Fatal("missing key ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := flag.Args()
|
||||||
|
if len(args) <= 0 {
|
||||||
|
log.Fatal("missing target URL")
|
||||||
|
}
|
||||||
|
targetURL := args[0]
|
||||||
|
if _, err := url.Parse(targetURL); err != nil {
|
||||||
|
log.Fatal("invalid target URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes, err := ioutil.ReadFile(conf.KeyFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBlock, _ := pem.Decode(keyBytes)
|
||||||
|
if pemBlock == nil {
|
||||||
|
log.Fatal("invalid PEM format")
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload io.Reader
|
||||||
|
if conf.JSON != "" {
|
||||||
|
jsonBytes, err := ioutil.ReadFile(conf.JSON)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
payload = bytes.NewReader(jsonBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||||
|
headers := []string{httpsig.RequestTarget, "date"}
|
||||||
|
signer, _, err := httpsig.NewSigner(prefs, headers, httpsig.Signature)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", targetURL, payload)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
if payload != nil {
|
||||||
|
req.Header.Add("content-type", "application/json")
|
||||||
|
}
|
||||||
|
req.Header.Add("date", time.Now().UTC().Format(http.TimeFormat))
|
||||||
|
|
||||||
|
if err = signer.SignRequest(privateKey, conf.KeyID, req); err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
dump, err := httputil.DumpRequest(req, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(string(dump)))
|
||||||
|
for scanner.Scan() {
|
||||||
|
fmt.Println(">", scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
dump, err = httputil.DumpResponse(res, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
scanner = bufio.NewScanner(strings.NewReader(string(dump)))
|
||||||
|
for scanner.Scan() {
|
||||||
|
fmt.Println("<", scanner.Text())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user