mirror of
https://github.com/ncarlier/webhookd.git
synced 2025-04-05 18:03:41 +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"
|
||||
```
|
||||
|
||||
You can find a small HTTP client in the ["tooling" directory](./tooling/httpsig/README.md) that is capable of forging HTTP signatures.
|
||||
|
||||
### TLS
|
||||
|
||||
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) {
|
||||
verifier, err := httpsig.NewVerifier(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("unable to initialize HTTP signature verifier: " + err.Error()))
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte("invalid HTTP signature: " + err.Error()))
|
||||
return
|
||||
}
|
||||
pubKeyID := verifier.KeyId()
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/ncarlier/webhookd/pkg/logger"
|
||||
)
|
||||
|
||||
const defaultAlgorithm = httpsig.RSA_SHA256
|
||||
|
@ -28,17 +29,17 @@ func (ks *directoryKeyStore) Get(keyID string) (crypto.PublicKey, httpsig.Algori
|
|||
return key, defaultAlgorithm, nil
|
||||
}
|
||||
|
||||
func newDirectoryKeyStore(root string) (store *directoryKeyStore, err error) {
|
||||
store = &directoryKeyStore{
|
||||
func newDirectoryKeyStore(root string) (*directoryKeyStore, error) {
|
||||
store := &directoryKeyStore{
|
||||
algorithm: "",
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if filepath.Ext(path) == "pem" {
|
||||
if filepath.Ext(path) == ".pem" {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -51,7 +52,7 @@ func newDirectoryKeyStore(root string) (store *directoryKeyStore, err error) {
|
|||
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
rsaPublicKey, ok := pub.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
|
@ -61,9 +62,10 @@ func newDirectoryKeyStore(root string) (store *directoryKeyStore, err error) {
|
|||
keyID, ok := block.Headers["key_id"]
|
||||
if ok {
|
||||
store.keys[keyID] = rsaPublicKey
|
||||
logger.Debug.Println("HTTP signature public key loaded: ", path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
return store, walkErr
|
||||
}
|
||||
|
|
|
@ -3,8 +3,10 @@ package pubkey
|
|||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
"github.com/go-fed/httpsig"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/go-fed/httpsig"
|
||||
)
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid KeyStore URL: %s", uri)
|
||||
return nil, fmt.Errorf("invalid KeyStore URI: %s", uri)
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "file":
|
||||
store, err = newDirectoryKeyStore(u.RawPath)
|
||||
store, err = newDirectoryKeyStore(strings.TrimPrefix(uri, "file://"))
|
||||
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