mirror of
https://github.com/ncarlier/webhookd.git
synced 2025-04-19 20:50:11 +00:00
feat(signature): refactore the trust store system
This commit is contained in:
parent
a5fe96d2e8
commit
d91e84d1be
|
@ -293,16 +293,15 @@ $ curl -u api:test -XPOST "http://localhost:8080/echo?msg=hello"
|
||||||
|
|
||||||
You can ensure message integrity (and authenticity) with [HTTP Signatures](https://www.ietf.org/archive/id/draft-cavage-http-signatures-12.txt).
|
You can ensure message integrity (and authenticity) with [HTTP Signatures](https://www.ietf.org/archive/id/draft-cavage-http-signatures-12.txt).
|
||||||
|
|
||||||
To activate HTTP signature verification, you have to configure the key store:
|
To activate HTTP signature verification, you have to configure the trust store:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ export WHD_KEY_STORE_URI=file:///etc/webhookd/keys
|
$ export WHD_TRUST_STORE_FILE=/etc/webhookd/pubkey.pem
|
||||||
$ # or
|
$ # or
|
||||||
$ webhookd --key-store-uri file:///etc/webhookd/keys
|
$ webhookd --trust-store-file /etc/webhookd/pubkey.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that only `file://` URI s currently supported.
|
Public key is stored in PEM format.
|
||||||
All public keys stored in PEM format in the targeted directory will be loaded.
|
|
||||||
|
|
||||||
Once configured, you must call webhooks using a valid HTTP signature:
|
Once configured, you must call webhooks using a valid HTTP signature:
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,8 @@
|
||||||
# Output debug logs, default is false
|
# Output debug logs, default is false
|
||||||
#WHD_DEBUG=false
|
#WHD_DEBUG=false
|
||||||
|
|
||||||
# Key store URI, disabled by default
|
# Maximum hook execution time in second, default is 10
|
||||||
# Enable HTTP signature verification if set.
|
#WHD_HOOK_TIMEOUT=10
|
||||||
# Example: `file:///etc/webhookd/keys`
|
|
||||||
#KEY_STORE_URI=
|
|
||||||
|
|
||||||
# HTTP listen address, default is ":8080"
|
# HTTP listen address, default is ":8080"
|
||||||
# Example: `localhost:8080` or `:8080` for all interfaces
|
# Example: `localhost:8080` or `:8080` for all interfaces
|
||||||
|
@ -30,8 +28,10 @@
|
||||||
# Scripts location, default is "scripts"
|
# Scripts location, default is "scripts"
|
||||||
#WHD_SCRIPTS="scripts"
|
#WHD_SCRIPTS="scripts"
|
||||||
|
|
||||||
# Maximum hook execution time in second, default is 10
|
# Trust store URI, disabled by default
|
||||||
#WHD_HOOK_TIMEOUT=10
|
# Enable HTTP signature verification if set.
|
||||||
|
# Example: `/etc/webhookd/pubkey.pem`
|
||||||
|
#WHD_TRUST_STORE_FILE=
|
||||||
|
|
||||||
# TLS listend address, disabled by default
|
# TLS listend address, disabled by default
|
||||||
# Example: `localhost:8443` or `:8443` for all interfaces
|
# Example: `localhost:8443` or `:8443` for all interfaces
|
||||||
|
|
|
@ -26,9 +26,9 @@ func NewRouter(conf *config.Config) *http.ServeMux {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load key store...
|
// Load key store...
|
||||||
keystore, err := pubkey.NewKeyStore(conf.KeyStoreURI)
|
keystore, err := pubkey.NewTrustStore(conf.TrustStoreFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning.Printf("unable to load key store (\"%s\"): %s\n", conf.KeyStoreURI, err)
|
logger.Warning.Printf("unable to load trust store (\"%s\"): %s\n", conf.TrustStoreFile, err)
|
||||||
}
|
}
|
||||||
if keystore != nil {
|
if keystore != nil {
|
||||||
middlewares = append(middlewares, middleware.HTTPSignature(keystore))
|
middlewares = append(middlewares, middleware.HTTPSignature(keystore))
|
||||||
|
|
|
@ -14,5 +14,5 @@ type Config struct {
|
||||||
PasswdFile string `flag:"passwd-file" desc:"Password file for basic HTTP authentication" default:".htpasswd"`
|
PasswdFile string `flag:"passwd-file" desc:"Password file for basic HTTP authentication" default:".htpasswd"`
|
||||||
LogDir string `flag:"log-dir" desc:"Hook execution logs location" default:""`
|
LogDir string `flag:"log-dir" desc:"Hook execution logs location" default:""`
|
||||||
NotificationURI string `flag:"notification-uri" desc:"Notification URI"`
|
NotificationURI string `flag:"notification-uri" desc:"Notification URI"`
|
||||||
KeyStoreURI string `flag:"key-store-uri" desc:"Key store URI used by HTTP signature verifier"`
|
TrustStoreFile string `flag:"trust-store-file" desc:"Trust store used by HTTP signature verifier (.pem or .p12)"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPSignature is a middleware to checks HTTP request signature
|
// HTTPSignature is a middleware to checks HTTP request signature
|
||||||
func HTTPSignature(keyStore pubkey.KeyStore) Middleware {
|
func HTTPSignature(trustStore pubkey.TrustStore) Middleware {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) 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)
|
||||||
|
@ -18,7 +18,7 @@ func HTTPSignature(keyStore pubkey.KeyStore) Middleware {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pubKeyID := verifier.KeyId()
|
pubKeyID := verifier.KeyId()
|
||||||
pubKey, algo, err := keyStore.Get(pubKeyID)
|
pubKey, algo, err := trustStore.Get(pubKeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(400)
|
w.WriteHeader(400)
|
||||||
w.Write([]byte("invalid HTTP signature: " + err.Error()))
|
w.Write([]byte("invalid HTTP signature: " + err.Error()))
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
package pubkey
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/go-fed/httpsig"
|
|
||||||
"github.com/ncarlier/webhookd/pkg/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultAlgorithm = httpsig.RSA_SHA256
|
|
||||||
|
|
||||||
type directoryKeyStore struct {
|
|
||||||
algorithm string
|
|
||||||
keys map[string]crypto.PublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ks *directoryKeyStore) Get(keyID string) (crypto.PublicKey, httpsig.Algorithm, error) {
|
|
||||||
key, ok := ks.keys[keyID]
|
|
||||||
if !ok {
|
|
||||||
return nil, defaultAlgorithm, fmt.Errorf("public key not found: %s", keyID)
|
|
||||||
}
|
|
||||||
return key, defaultAlgorithm, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDirectoryKeyStore(root string) (*directoryKeyStore, error) {
|
|
||||||
store := &directoryKeyStore{
|
|
||||||
algorithm: "",
|
|
||||||
keys: make(map[string]crypto.PublicKey),
|
|
||||||
}
|
|
||||||
|
|
||||||
walkErr := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if filepath.Ext(path) == ".pem" {
|
|
||||||
data, err := ioutil.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
block, _ := pem.Decode(data)
|
|
||||||
if block == nil {
|
|
||||||
return fmt.Errorf("invalid PEM file: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
rsaPublicKey, ok := pub.(*rsa.PublicKey)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unable to cast public key to RSA public key")
|
|
||||||
}
|
|
||||||
|
|
||||||
keyID, ok := block.Headers["key_id"]
|
|
||||||
if ok {
|
|
||||||
store.keys[keyID] = rsaPublicKey
|
|
||||||
logger.Debug.Println("HTTP signature public key loaded: ", path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return store, walkErr
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
package pubkey
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-fed/httpsig"
|
|
||||||
)
|
|
||||||
|
|
||||||
// KeyStore is a generic interface to retrieve a public key
|
|
||||||
type KeyStore interface {
|
|
||||||
Get(keyID string) (crypto.PublicKey, httpsig.Algorithm, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewKeyStore creates new Key Store from URI
|
|
||||||
func NewKeyStore(uri string) (store KeyStore, err error) {
|
|
||||||
if uri == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
u, err := url.Parse(uri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid KeyStore URI: %s", uri)
|
|
||||||
}
|
|
||||||
switch u.Scheme {
|
|
||||||
case "file":
|
|
||||||
store, err = newDirectoryKeyStore(strings.TrimPrefix(uri, "file://"))
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("non supported KeyStore URI: %s", uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
55
pkg/pubkey/pem_truststore.go
Normal file
55
pkg/pubkey/pem_truststore.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package pubkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/go-fed/httpsig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pemTrustStore struct {
|
||||||
|
key crypto.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *pemTrustStore) Get(keyID string) (crypto.PublicKey, httpsig.Algorithm, error) {
|
||||||
|
return ts.key, defaultAlgorithm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPEMTrustStore(filename string) (*pemTrustStore, error) {
|
||||||
|
data, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(data)
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("invalid PEM file: %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rsaPublicKey *rsa.PublicKey
|
||||||
|
switch block.Type {
|
||||||
|
case "PUBLIC KEY":
|
||||||
|
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rsaPublicKey, _ = pub.(*rsa.PublicKey)
|
||||||
|
case "CERTIFICATE":
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rsaPublicKey, _ = cert.PublicKey.(*rsa.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsaPublicKey == nil {
|
||||||
|
return nil, fmt.Errorf("no RSA public key found: %s", filename)
|
||||||
|
}
|
||||||
|
return &pemTrustStore{
|
||||||
|
key: rsaPublicKey,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import (
|
||||||
func TestKeyStore(t *testing.T) {
|
func TestKeyStore(t *testing.T) {
|
||||||
logger.Init("warn")
|
logger.Init("warn")
|
||||||
|
|
||||||
ks, err := pubkey.NewKeyStore("file://.")
|
ks, err := pubkey.NewTrustStore("test-key.pem")
|
||||||
assert.Nil(t, err, "")
|
assert.Nil(t, err, "")
|
||||||
assert.NotNil(t, ks, "")
|
assert.NotNil(t, ks, "")
|
||||||
|
|
||||||
|
@ -20,7 +20,4 @@ func TestKeyStore(t *testing.T) {
|
||||||
assert.Nil(t, err, "")
|
assert.Nil(t, err, "")
|
||||||
assert.NotNil(t, pk, "")
|
assert.NotNil(t, pk, "")
|
||||||
assert.Equal(t, httpsig.RSA_SHA256, algo, "")
|
assert.Equal(t, httpsig.RSA_SHA256, algo, "")
|
||||||
|
|
||||||
_, _, err = ks.Get("notfound")
|
|
||||||
assert.NotNil(t, err, "")
|
|
||||||
}
|
}
|
32
pkg/pubkey/truststore.go
Normal file
32
pkg/pubkey/truststore.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package pubkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/go-fed/httpsig"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultAlgorithm = httpsig.RSA_SHA256
|
||||||
|
|
||||||
|
// TrustStore is a generic interface to retrieve a public key
|
||||||
|
type TrustStore interface {
|
||||||
|
Get(keyID string) (crypto.PublicKey, httpsig.Algorithm, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTrustStore creates new Key Store from URI
|
||||||
|
func NewTrustStore(filename string) (store TrustStore, err error) {
|
||||||
|
if filename == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch filepath.Ext(filename) {
|
||||||
|
case ".pem":
|
||||||
|
store, err = newPEMTrustStore(filename)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported TrustStore file format: %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ MIIEowIBAAKCAQEAwdCB5DZD0cFeJYUu1W3IlNN9y+NZC/Jqktdkn8/WHlXec07n
|
||||||
- Start Webhookd with HTTP signature support:
|
- Start Webhookd with HTTP signature support:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ webhookd -key-store-uri file://.
|
$ webhookd --trust-store-file ./key-pub.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
- Make HTTP signed request:
|
- Make HTTP signed request:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user