feat(signature): signature refactoring

- add ed5519 HTTP signature support
- refactor truststore package
- add P12 trust store support

close #72
This commit is contained in:
Nicolas Carlier 2022-12-29 16:57:16 +00:00
parent 9020473d14
commit f2054d2dc4
20 changed files with 406 additions and 306 deletions

View File

@ -311,9 +311,14 @@ $ curl -u api:test -XPOST "http://localhost:8080/echo?msg=hello"
### Signature
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) by signing HTTP requests.
To activate HTTP signature verification, you have to configure the trust store:
Webhookd supports 2 signature methods:
- [HTTP Signatures](https://www.ietf.org/archive/id/draft-cavage-http-signatures-12.txt)
- [Ed25519 Signature](https://ed25519.cr.yp.to/) (used by [Discord](https://discord.com/developers/docs/interactions/receiving-and-responding#security-and-authorization))
To activate request signature verification, you have to configure the trust store:
```bash
$ export WHD_TRUST_STORE_FILE=/etc/webhookd/pubkey.pem
@ -323,17 +328,24 @@ $ webhookd --trust-store-file /etc/webhookd/pubkey.pem
Public key is stored in PEM format.
Once configured, you must call webhooks using a valid HTTP signature:
Once configured, you must call webhooks using a valid signature:
```bash
# Using HTTP Signature:
$ curl -X POST \
-H 'Date: <req-date>' \
-H 'Signature: keyId=<key-id>,algorithm="rsa-sha256",headers="(request-target) date",signature=<signature-string>' \
-H 'Accept: application/json' \
"http://localhost:8080/echo?msg=hello"
# or using Ed25519 Signature:
$ curl -X POST \
-H 'X-Signature-Timestamp: <timestamp>' \
-H 'X-Signature-Ed25519: <signature-string>' \
-H 'Accept: application/json' \
"http://localhost: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.
You can find a small HTTP client in the ["tooling" directory](./tooling/httpsig/README.md) that is capable of forging `HTTP signatures`.
### TLS

View File

@ -5,7 +5,7 @@ import (
"github.com/ncarlier/webhookd/pkg/config"
"github.com/ncarlier/webhookd/pkg/logger"
"github.com/ncarlier/webhookd/pkg/middleware"
"github.com/ncarlier/webhookd/pkg/pubkey"
"github.com/ncarlier/webhookd/pkg/truststore"
)
var commonMiddlewares = middleware.Middlewares{
@ -21,12 +21,12 @@ func buildMiddlewares(conf *config.Config) middleware.Middlewares {
}
// Load trust store...
trustStore, err := pubkey.NewTrustStore(conf.TrustStoreFile)
ts, err := truststore.New(conf.TrustStoreFile)
if err != nil {
logger.Warning.Printf("unable to load trust store (\"%s\"): %s\n", conf.TrustStoreFile, err)
}
if trustStore != nil {
middlewares = middlewares.UseAfter(middleware.HTTPSignature(trustStore))
if ts != nil {
middlewares = middlewares.UseAfter(middleware.Signature(ts))
}
// Load authenticator...

View File

@ -3,31 +3,21 @@ package middleware
import (
"net/http"
"github.com/go-fed/httpsig"
"github.com/ncarlier/webhookd/pkg/pubkey"
"github.com/ncarlier/webhookd/pkg/middleware/signature"
"github.com/ncarlier/webhookd/pkg/truststore"
)
// HTTPSignature is a middleware to checks HTTP request signature
func HTTPSignature(trustStore pubkey.TrustStore) Middleware {
// Signature is a middleware to checks HTTP request signature
func Signature(ts truststore.TrustStore) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
verifier, err := httpsig.NewVerifier(r)
if err != nil {
w.WriteHeader(400)
w.Write([]byte("invalid HTTP signature: " + err.Error()))
return
handler := signature.HTTPSignatureHandler
if signature.IsEd25519SignatureRequest(r.Header) {
handler = signature.Ed25519SignatureHandler
}
pubKeyID := verifier.KeyId()
entry := trustStore.Get(pubKeyID)
if entry == nil {
w.WriteHeader(400)
w.Write([]byte("invalid HTTP signature: public key not found: " + pubKeyID))
return
}
err = verifier.Verify(entry.Pubkey, entry.Algorithm)
if err != nil {
w.WriteHeader(400)
w.Write([]byte("invalid HTTP signature: " + err.Error()))
if err := handler(r, ts); err != nil {
w.WriteHeader(401)
w.Write([]byte("401 Unauthorized: " + err.Error()))
return
}
next.ServeHTTP(w, r)

View File

@ -0,0 +1,70 @@
package signature
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"github.com/ncarlier/webhookd/pkg/truststore"
)
const (
defaultKeyId = "default"
xSignatureEd25519 = "X-Signature-Ed25519"
xSignatureTimestamp = "X-Signature-Timestamp"
)
// IsEd25519SignatureRequest test if HTTP headers contains Ed25519 Signature
func IsEd25519SignatureRequest(headers http.Header) bool {
return headers.Get(xSignatureEd25519) != ""
}
// Ed25519SignatureHandler validate request HTTP signature
func Ed25519SignatureHandler(r *http.Request, ts truststore.TrustStore) error {
pubkey := ts.GetPublicKey(defaultKeyId)
if pubkey == nil {
return fmt.Errorf("public key not found: %s", defaultKeyId)
}
key, ok := pubkey.(ed25519.PublicKey)
if !ok {
return errors.New("invalid public key: verify the algorithm")
}
value := r.Header.Get(xSignatureEd25519)
timestamp := r.Header.Get(xSignatureTimestamp)
if value == "" || timestamp == "" {
return errors.New("missing signature header")
}
sig, err := hex.DecodeString(value)
if err != nil || len(sig) != ed25519.SignatureSize || sig[63]&224 != 0 {
return fmt.Errorf("invalid signature format: %s", sig)
}
var msg bytes.Buffer
msg.WriteString(timestamp)
defer r.Body.Close()
var body bytes.Buffer
// Copy the original body back into the request after finishing.
defer func() {
r.Body = io.NopCloser(&body)
}()
// Copy body into buffers
_, err = io.Copy(&msg, io.TeeReader(r.Body, &body))
if err != nil {
return err
}
if !ed25519.Verify(key, msg.Bytes(), sig) {
return errors.New("invalid signature")
}
return nil
}

View File

@ -0,0 +1,28 @@
package signature
import (
"fmt"
"net/http"
"github.com/go-fed/httpsig"
"github.com/ncarlier/webhookd/pkg/truststore"
)
// HTTPSignatureHandler validate request HTTP signature
func HTTPSignatureHandler(r *http.Request, ts truststore.TrustStore) error {
verifier, err := httpsig.NewVerifier(r)
if err != nil {
return err
}
pubkeyID := verifier.KeyId()
pubkey := ts.GetPublicKey(pubkeyID)
if pubkey == nil {
return fmt.Errorf("public key not found: %s", pubkeyID)
}
// TODO dynamic algo
err = verifier.Verify(pubkey, httpsig.RSA_SHA256)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,50 @@
package test
import (
"bytes"
"crypto"
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"net/http"
"strconv"
"testing"
"time"
"github.com/ncarlier/webhookd/pkg/assert"
"github.com/ncarlier/webhookd/pkg/logger"
"github.com/ncarlier/webhookd/pkg/middleware/signature"
"github.com/ncarlier/webhookd/pkg/truststore"
)
func TestEd5519Signature(t *testing.T) {
logger.Init("warn")
pubkey, privkey, err := ed25519.GenerateKey(rand.Reader)
assert.Nil(t, err, "")
ts := &truststore.InMemoryTrustStore{
Keys: map[string]crypto.PublicKey{
"default": pubkey,
},
}
body := "this is a test"
req, err := http.NewRequest("POST", "/", bytes.NewBufferString(body))
assert.Nil(t, err, "")
now := time.Now()
timestamp := strconv.FormatInt(now.Unix(), 10)
var msg bytes.Buffer
msg.WriteString(timestamp)
msg.WriteString(body)
s := ed25519.Sign(privkey, msg.Bytes())
req.Header.Set("X-Signature-Ed25519", hex.EncodeToString(s[:ed25519.SignatureSize]))
req.Header.Set("X-Signature-Timestamp", timestamp)
req.Header.Add("date", now.UTC().Format(http.TimeFormat))
req.Header.Set("Content-Type", "text/plain")
err = signature.Ed25519SignatureHandler(req, ts)
assert.Nil(t, err, "")
}

View File

@ -0,0 +1,52 @@
package test
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"net/http"
"testing"
"time"
"github.com/go-fed/httpsig"
"github.com/ncarlier/webhookd/pkg/assert"
"github.com/ncarlier/webhookd/pkg/logger"
"github.com/ncarlier/webhookd/pkg/middleware/signature"
"github.com/ncarlier/webhookd/pkg/truststore"
)
func assertSigner(t *testing.T) httpsig.Signer {
prefs := []httpsig.Algorithm{httpsig.RSA_SHA256}
digestAlgorithm := httpsig.DigestSha256
headers := []string{httpsig.RequestTarget, "date"}
signer, _, err := httpsig.NewSigner(prefs, digestAlgorithm, headers, httpsig.Signature, 0)
assert.Nil(t, err, "")
return signer
}
func TestHTTPSignature(t *testing.T) {
logger.Init("warn")
privkey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.Nil(t, err, "")
pubkey := &privkey.PublicKey
ts := &truststore.InMemoryTrustStore{
Keys: map[string]crypto.PublicKey{
"default": pubkey,
},
}
//pk := assertPrivateKey(t)
signer := assertSigner(t)
var body []byte
req, err := http.NewRequest("GET", "/", nil)
assert.Nil(t, err, "")
req.Header.Add("date", time.Now().UTC().Format(http.TimeFormat))
err = signer.SignRequest(privkey, "default", req, body)
assert.Nil(t, err, "")
// ts := assertTrustStore(t)
err = signature.HTTPSignatureHandler(req, ts)
assert.Nil(t, err, "")
}

View File

@ -1,59 +0,0 @@
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 TestTrustStoreWithNoKeyID(t *testing.T) {
logger.Init("warn")
ts, err := pubkey.NewTrustStore("test-key-01.pem")
assert.Nil(t, err, "")
assert.NotNil(t, ts, "")
entry := ts.Get("test")
assert.True(t, entry == nil, "")
entry = ts.Get("default")
assert.NotNil(t, entry, "")
assert.Equal(t, httpsig.RSA_SHA256, entry.Algorithm, "")
}
func TestTrustStoreWithKeyID(t *testing.T) {
logger.Init("warn")
ts, err := pubkey.NewTrustStore("test-key-02.pem")
assert.Nil(t, err, "")
assert.NotNil(t, ts, "")
entry := ts.Get("test")
assert.NotNil(t, entry, "")
assert.Equal(t, httpsig.RSA_SHA256, entry.Algorithm, "")
}
func TestTrustStoreWithCertificate(t *testing.T) {
logger.Init("warn")
ts, err := pubkey.NewTrustStore("test-cert.pem")
assert.Nil(t, err, "")
assert.NotNil(t, ts, "")
entry := ts.Get("test.localnet")
assert.NotNil(t, entry, "")
assert.Equal(t, httpsig.RSA_SHA256, entry.Algorithm, "")
}
func TestTrustStoreWithMultipleEntries(t *testing.T) {
logger.Init("warn")
ts, err := pubkey.NewTrustStore("test-multi.pem")
assert.Nil(t, err, "")
assert.NotNil(t, ts, "")
entry := ts.Get("test.localnet")
assert.NotNil(t, entry, "")
assert.Equal(t, httpsig.RSA_SHA256, entry.Algorithm, "")
entry = ts.Get("foo")
assert.NotNil(t, entry, "")
assert.Equal(t, httpsig.RSA_SHA256, entry.Algorithm, "")
}

View File

@ -1,32 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFkDCCA3igAwIBAgIJAI76c8w4edCdMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxFjAUBgNVBAMMDXRlc3QubG9jYWxuZXQwHhcNMjAwMzE1
MjIxMTQzWhcNMjEwMzE1MjIxMTQzWjBdMQswCQYDVQQGEwJBVTETMBEGA1UECAwK
U29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRYw
FAYDVQQDDA10ZXN0LmxvY2FsbmV0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEA8WYogCpb9isAmX/5crbCvEj7yA3yHFNfNb+NQXnVCp/ONZVEXSjhGEp5
Z2NQ1hHb9xeU1CjyDmAaIKSjBWHEfGwi95+qBa/5UzacAfMVaV1UHwVVwZU/yiTF
AHFOVndtPKNbIK2XJIOcmeL1Ejp7LmV2NEKVE/HGAEE10J8X9CzgPZgcU0D8zePq
Fg5NFGHEN0AyP07e+UooWtxyjOfPu1U88xr8Qxcg8Y9ffQNqA2XIQDf+zwR0K3tH
LE1Wfbws088xF2cq3qp2nA1373t74wKOJu3cJKqMeyEsYIJ3aVwPbS5qP6jqakIB
m9sqZqqWmdqm/S8V9O03AJv9w1/49maVxy6O0wABmiNpDaXU3iLqE4MLBOFJ56Ul
lEJuUKHIgFZ6CxZIUMv1E4jKDSAYUgzFt2ON9WwVnweR0lbv+AiF26KOIx51XsZg
vrTIMkKQU0lfnAWkgUxGyGAkJmVfXsrZUZvaBWF1H8Nt+nsZDwrp3ffJvSK/MIw7
9VPYdCNhnyrBWQMI5ZTFQCrTEXokXLAugX7W1qjSptWu3+H+7klq1yvip/YgpZtf
OgE95uJV9KTjcX4F3eAsnAfF7urEArV0t0qlRrb+rFWjNKJYnQejjXYuu83L+/ue
J+5wiqJZKen2HnD+2T3OyhY7FkiP8PwVxKT8mJt4qvhJHeZHc6cCAwEAAaNTMFEw
HQYDVR0OBBYEFJoUuP6T91NBZXCRcJU3w8mEny9iMB8GA1UdIwQYMBaAFJoUuP6T
91NBZXCRcJU3w8mEny9iMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD
ggIBABQ3zGtBz4KIgT5jz2CiyQVuwk+gfAlIj3UdcOS55764jG7Wj8kcWL+Lp3NJ
h2lXBEmT5PaGqrhVUIPf5j0JQeC9A6+1msT3NPd0ZDnNXi5G2/KYDsLgh5OWB0gh
qXMyKAME65k1MXQuOnM23JewfVn0+zNAAvzZl9ofWm9ZRHLOGMgDYMItxHE+Q+eQ
AI7Mi/TewVZX8S39u4LfUKGrokFlhaaqecrNVAsGuFrgJyOK1/x3pUSn8PvsxRGT
jtnkQ/nlQskCobof7e+CM55QWzcePS547X+kspyzqOoKPqvmnacipXuy2/Zxc4bS
NV3J5cOdt+LxYrVru81qc7xM72ZirZepc4YvDMRJ3gMhCLciGsyiqoDkSBamGxcQ
QVzbHdWXwlNWp6cJGgtPWSjP1baeLlplbLWiy+hK9fOsoBV8t2anE6WfQgZoZ4/t
WSFKW5JS0g0eZq1KyGre9ynOSL4WCcHoDF5eY3pZwTCgPhu1SEqV1wnHo9YXiWZL
LHuVpX+HI+zpChciV+XyD3OQ3p0eXbo/Czd2XWVkvUA24v5EZKkZWHzvHfqlC0pT
yE9fmqizZlX9nZwyh4+dp/V3IUDIml6CmpZaPCC6zDbuFPpi0geYomgRFAGgePsf
CJhfs0VHYlLZCvziqLfXMhC3Seg2NDNqoLR8icuUceXTc839
-----END CERTIFICATE-----

View File

@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwdCB5DZD0cFeJYUu1W3I
lNN9y+NZC/Jqktdkn8/WHlXec07nesyDyicveduuaaDBeUoR3imRUtTS+eFKvDR/
HMke8HmoleZvADJ0ppqbvj0YZOhWp2SqZvAKJbce8D+OSKuLGpxqq6FLl0cq+Fv+
mFpZyDcqPZtrwAz+AbJp6sxvVvNb0r0Z1LxEIVb0JHHLhsJkxJ06PcbT2tnvaPHT
8S80cvFO57zFpiX/M8gQNaRCqwD1/sHEGJc6Av+WUBhrE7pz0XGMLjU4n7W6Ooyz
g2QdBiLE3tW8HYL9iJ7EuLG5apYbFVVHJHl8HNWpmD0q8sVExvx5+AKQ3kEDp68m
ywIDAQAB
-----END PUBLIC KEY-----

View File

@ -1,11 +0,0 @@
-----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-----

View File

@ -1,54 +0,0 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
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
DUyBMrtuX98+CQnimJx/Rg==
-----END ENCRYPTED PRIVATE KEY-----

View File

@ -1,42 +0,0 @@
-----BEGIN PUBLIC KEY-----
key_id: foo
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwdCB5DZD0cFeJYUu1W3I
lNN9y+NZC/Jqktdkn8/WHlXec07nesyDyicveduuaaDBeUoR3imRUtTS+eFKvDR/
HMke8HmoleZvADJ0ppqbvj0YZOhWp2SqZvAKJbce8D+OSKuLGpxqq6FLl0cq+Fv+
mFpZyDcqPZtrwAz+AbJp6sxvVvNb0r0Z1LxEIVb0JHHLhsJkxJ06PcbT2tnvaPHT
8S80cvFO57zFpiX/M8gQNaRCqwD1/sHEGJc6Av+WUBhrE7pz0XGMLjU4n7W6Ooyz
g2QdBiLE3tW8HYL9iJ7EuLG5apYbFVVHJHl8HNWpmD0q8sVExvx5+AKQ3kEDp68m
ywIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIFkDCCA3igAwIBAgIJAI76c8w4edCdMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxFjAUBgNVBAMMDXRlc3QubG9jYWxuZXQwHhcNMjAwMzE1
MjIxMTQzWhcNMjEwMzE1MjIxMTQzWjBdMQswCQYDVQQGEwJBVTETMBEGA1UECAwK
U29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRYw
FAYDVQQDDA10ZXN0LmxvY2FsbmV0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEA8WYogCpb9isAmX/5crbCvEj7yA3yHFNfNb+NQXnVCp/ONZVEXSjhGEp5
Z2NQ1hHb9xeU1CjyDmAaIKSjBWHEfGwi95+qBa/5UzacAfMVaV1UHwVVwZU/yiTF
AHFOVndtPKNbIK2XJIOcmeL1Ejp7LmV2NEKVE/HGAEE10J8X9CzgPZgcU0D8zePq
Fg5NFGHEN0AyP07e+UooWtxyjOfPu1U88xr8Qxcg8Y9ffQNqA2XIQDf+zwR0K3tH
LE1Wfbws088xF2cq3qp2nA1373t74wKOJu3cJKqMeyEsYIJ3aVwPbS5qP6jqakIB
m9sqZqqWmdqm/S8V9O03AJv9w1/49maVxy6O0wABmiNpDaXU3iLqE4MLBOFJ56Ul
lEJuUKHIgFZ6CxZIUMv1E4jKDSAYUgzFt2ON9WwVnweR0lbv+AiF26KOIx51XsZg
vrTIMkKQU0lfnAWkgUxGyGAkJmVfXsrZUZvaBWF1H8Nt+nsZDwrp3ffJvSK/MIw7
9VPYdCNhnyrBWQMI5ZTFQCrTEXokXLAugX7W1qjSptWu3+H+7klq1yvip/YgpZtf
OgE95uJV9KTjcX4F3eAsnAfF7urEArV0t0qlRrb+rFWjNKJYnQejjXYuu83L+/ue
J+5wiqJZKen2HnD+2T3OyhY7FkiP8PwVxKT8mJt4qvhJHeZHc6cCAwEAAaNTMFEw
HQYDVR0OBBYEFJoUuP6T91NBZXCRcJU3w8mEny9iMB8GA1UdIwQYMBaAFJoUuP6T
91NBZXCRcJU3w8mEny9iMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD
ggIBABQ3zGtBz4KIgT5jz2CiyQVuwk+gfAlIj3UdcOS55764jG7Wj8kcWL+Lp3NJ
h2lXBEmT5PaGqrhVUIPf5j0JQeC9A6+1msT3NPd0ZDnNXi5G2/KYDsLgh5OWB0gh
qXMyKAME65k1MXQuOnM23JewfVn0+zNAAvzZl9ofWm9ZRHLOGMgDYMItxHE+Q+eQ
AI7Mi/TewVZX8S39u4LfUKGrokFlhaaqecrNVAsGuFrgJyOK1/x3pUSn8PvsxRGT
jtnkQ/nlQskCobof7e+CM55QWzcePS547X+kspyzqOoKPqvmnacipXuy2/Zxc4bS
NV3J5cOdt+LxYrVru81qc7xM72ZirZepc4YvDMRJ3gMhCLciGsyiqoDkSBamGxcQ
QVzbHdWXwlNWp6cJGgtPWSjP1baeLlplbLWiy+hK9fOsoBV8t2anE6WfQgZoZ4/t
WSFKW5JS0g0eZq1KyGre9ynOSL4WCcHoDF5eY3pZwTCgPhu1SEqV1wnHo9YXiWZL
LHuVpX+HI+zpChciV+XyD3OQ3p0eXbo/Czd2XWVkvUA24v5EZKkZWHzvHfqlC0pT
yE9fmqizZlX9nZwyh4+dp/V3IUDIml6CmpZaPCC6zDbuFPpi0geYomgRFAGgePsf
CJhfs0VHYlLZCvziqLfXMhC3Seg2NDNqoLR8icuUceXTc839
-----END CERTIFICATE-----

View File

@ -1,40 +0,0 @@
package pubkey
import (
"crypto"
"fmt"
"path/filepath"
"github.com/go-fed/httpsig"
"github.com/ncarlier/webhookd/pkg/logger"
)
const defaultAlgorithm = httpsig.RSA_SHA256
// TrustStoreEntry is a trust store entry
type TrustStoreEntry struct {
Pubkey crypto.PublicKey
Algorithm httpsig.Algorithm
}
// TrustStore is a generic interface to retrieve a public key
type TrustStore interface {
Get(keyID string) *TrustStoreEntry
}
// NewTrustStore creates new Key Store from URI
func NewTrustStore(filename string) (store TrustStore, err error) {
if filename == "" {
return nil, nil
}
logger.Debug.Printf("loading trust store: %s", filename)
switch filepath.Ext(filename) {
case ".pem":
store, err = newPEMTrustStore(filename)
default:
err = fmt.Errorf("unsupported trust store file format: %s", filename)
}
return
}

View File

@ -0,0 +1,31 @@
package truststore
import (
"crypto"
"io/ioutil"
"github.com/ncarlier/webhookd/pkg/logger"
"golang.org/x/crypto/pkcs12"
)
func newP12TrustStore(filename string) (TrustStore, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
_, cert, err := pkcs12.Decode(data, "test")
if err != nil {
return nil, err
}
result := &InMemoryTrustStore{
Keys: make(map[string]crypto.PublicKey),
}
keyID := string(cert.Subject.CommonName)
result.Keys[keyID] = cert.PublicKey
logger.Debug.Printf("certificate \"%s\" loaded into the trustore", keyID)
return result, nil
}

View File

@ -1,7 +1,7 @@
package pubkey
package truststore
import (
"crypto/rsa"
"crypto"
"crypto/x509"
"encoding/pem"
"fmt"
@ -10,26 +10,14 @@ import (
"github.com/ncarlier/webhookd/pkg/logger"
)
type pemTrustStore struct {
keys map[string]TrustStoreEntry
}
func (ts *pemTrustStore) Get(keyID string) *TrustStoreEntry {
key, ok := ts.keys[keyID]
if ok {
return &key
}
return nil
}
func newPEMTrustStore(filename string) (TrustStore, error) {
raw, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
result := pemTrustStore{
keys: make(map[string]TrustStoreEntry),
result := &InMemoryTrustStore{
Keys: make(map[string]crypto.PublicKey),
}
for {
block, rest := pem.Decode(raw)
@ -38,38 +26,32 @@ func newPEMTrustStore(filename string) (TrustStore, error) {
}
switch block.Type {
case "PUBLIC KEY":
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
rsaPublicKey, _ := pub.(*rsa.PublicKey)
keyID, ok := block.Headers["key_id"]
if !ok {
keyID = "default"
}
result.keys[keyID] = TrustStoreEntry{
Algorithm: defaultAlgorithm,
Pubkey: rsaPublicKey,
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
result.Keys[keyID] = key
logger.Debug.Printf("public key \"%s\" loaded into the trustore", keyID)
case "CERTIFICATE":
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
rsaPublicKey, _ := cert.PublicKey.(*rsa.PublicKey)
keyID := string(cert.Subject.CommonName)
result.keys[keyID] = TrustStoreEntry{
Algorithm: defaultAlgorithm,
Pubkey: rsaPublicKey,
}
result.Keys[keyID] = cert.PublicKey
logger.Debug.Printf("certificate \"%s\" loaded into the trustore", keyID)
}
raw = rest
}
if len(result.keys) == 0 {
if len(result.Keys) == 0 {
return nil, fmt.Errorf("no RSA public key found: %s", filename)
}
return &result, nil
return result, nil
}

View File

@ -0,0 +1,23 @@
package test
import (
"crypto/rsa"
"testing"
"github.com/ncarlier/webhookd/pkg/assert"
"github.com/ncarlier/webhookd/pkg/logger"
"github.com/ncarlier/webhookd/pkg/truststore"
)
func TestTrustStoreWithP12(t *testing.T) {
t.Skip()
logger.Init("warn")
ts, err := truststore.New("test.p12")
assert.Nil(t, err, "")
assert.NotNil(t, ts, "")
pubkey := ts.GetPublicKey("test.localnet")
assert.NotNil(t, pubkey, "")
_, ok := pubkey.(*rsa.PublicKey)
assert.True(t, ok, "")
}

View File

@ -0,0 +1,64 @@
package test
import (
"crypto/rsa"
"testing"
"github.com/ncarlier/webhookd/pkg/assert"
"github.com/ncarlier/webhookd/pkg/logger"
"github.com/ncarlier/webhookd/pkg/truststore"
)
func TestTrustStoreWithNoKeyID(t *testing.T) {
logger.Init("warn")
ts, err := truststore.New("test-key-01.pem")
assert.Nil(t, err, "")
assert.NotNil(t, ts, "")
pubkey := ts.GetPublicKey("test")
assert.True(t, pubkey == nil, "")
pubkey = ts.GetPublicKey("default")
assert.NotNil(t, pubkey, "")
_, ok := pubkey.(*rsa.PublicKey)
assert.True(t, ok, "")
}
func TestTrustStoreWithKeyID(t *testing.T) {
logger.Init("warn")
ts, err := truststore.New("test-key-02.pem")
assert.Nil(t, err, "")
assert.NotNil(t, ts, "")
pubkey := ts.GetPublicKey("test")
assert.NotNil(t, pubkey, "")
_, ok := pubkey.(*rsa.PublicKey)
assert.True(t, ok, "")
}
func TestTrustStoreWithCertificate(t *testing.T) {
logger.Init("warn")
ts, err := truststore.New("test-cert.pem")
assert.Nil(t, err, "")
assert.NotNil(t, ts, "")
pubkey := ts.GetPublicKey("test.localnet")
assert.NotNil(t, pubkey, "")
_, ok := pubkey.(*rsa.PublicKey)
assert.True(t, ok, "")
}
func TestTrustStoreWithMultipleEntries(t *testing.T) {
logger.Init("warn")
ts, err := truststore.New("test-multi.pem")
assert.Nil(t, err, "")
assert.NotNil(t, ts, "")
pubkey := ts.GetPublicKey("test.localnet")
assert.NotNil(t, pubkey, "")
_, ok := pubkey.(*rsa.PublicKey)
assert.True(t, ok, "")
pubkey = ts.GetPublicKey("foo")
assert.NotNil(t, pubkey, "")
_, ok = pubkey.(*rsa.PublicKey)
assert.True(t, ok, "")
}

Binary file not shown.

View File

@ -0,0 +1,45 @@
package truststore
import (
"crypto"
"fmt"
"path/filepath"
"github.com/ncarlier/webhookd/pkg/logger"
)
// TrustStore is a generic interface to retrieve a public key
type TrustStore interface {
GetPublicKey(keyID string) crypto.PublicKey
}
// InMemoryTrustStore is a in memory storage for public keys
type InMemoryTrustStore struct {
Keys map[string]crypto.PublicKey
}
func (ts *InMemoryTrustStore) GetPublicKey(keyID string) crypto.PublicKey {
if key, ok := ts.Keys[keyID]; ok {
return key
}
return nil
}
// New creates new Trust Store from URI
func New(filename string) (store TrustStore, err error) {
if filename == "" {
return nil, nil
}
logger.Debug.Printf("loading trust store: %s", filename)
switch filepath.Ext(filename) {
case ".pem":
store, err = newPEMTrustStore(filename)
case ".p12":
store, err = newP12TrustStore(filename)
default:
err = fmt.Errorf("unsupported trust store file format: %s", filename)
}
return
}