feat(signature): multi entries for a PEM file

This commit is contained in:
Nicolas Carlier 2020-03-15 23:10:51 +00:00
parent 8a393cc0c3
commit 0e2f58012d
7 changed files with 185 additions and 68 deletions

View File

@ -56,3 +56,13 @@ func ContainsStr(t *testing.T, expected string, array []string, message string)
}
t.Fatalf("%s - array: %v, expected value: %s", message, array, expected)
}
// True assert that an expression is true
func True(t *testing.T, expression bool, message string) {
if message == "" {
message = "Expression is not true"
}
if !expression {
t.Fatalf("%s : %v", message, expression)
}
}

View File

@ -18,13 +18,13 @@ func HTTPSignature(trustStore pubkey.TrustStore) Middleware {
return
}
pubKeyID := verifier.KeyId()
pubKey, algo, err := trustStore.Get(pubKeyID)
if err != nil {
entry := trustStore.Get(pubKeyID)
if entry == nil {
w.WriteHeader(400)
w.Write([]byte("invalid HTTP signature: " + err.Error()))
return
}
err = verifier.Verify(pubKey, algo)
err = verifier.Verify(entry.Pubkey, entry.Algorythm)
if err != nil {
w.WriteHeader(400)
w.Write([]byte("invalid HTTP signature: " + err.Error()))

View File

@ -1,55 +1,75 @@
package pubkey
import (
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"github.com/go-fed/httpsig"
"github.com/ncarlier/webhookd/pkg/logger"
)
type pemTrustStore struct {
key crypto.PublicKey
keys map[string]TrustStoreEntry
}
func (ts *pemTrustStore) Get(keyID string) (crypto.PublicKey, httpsig.Algorithm, error) {
return ts.key, defaultAlgorithm, nil
func (ts *pemTrustStore) Get(keyID string) *TrustStoreEntry {
key, ok := ts.keys[keyID]
if ok {
return &key
}
return nil
}
func newPEMTrustStore(filename string) (*pemTrustStore, error) {
data, err := ioutil.ReadFile(filename)
raw, 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)
result := pemTrustStore{
keys: make(map[string]TrustStoreEntry),
}
for {
block, rest := pem.Decode(raw)
if block == nil {
break
}
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{
Algorythm: defaultAlgorithm,
Pubkey: rsaPublicKey,
}
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{
Algorythm: defaultAlgorithm,
Pubkey: rsaPublicKey,
}
logger.Debug.Printf("certificate \"%s\" loaded into the trustore", keyID)
}
raw = rest
}
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 {
if len(result.keys) == 0 {
return nil, fmt.Errorf("no RSA public key found: %s", filename)
}
return &pemTrustStore{
key: rsaPublicKey,
}, nil
return &result, nil
}

View File

@ -0,0 +1,59 @@
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.Algorythm, "")
}
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.Algorythm, "")
}
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.Algorythm, "")
}
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.Algorythm, "")
entry = ts.Get("foo")
assert.NotNil(t, entry, "")
assert.Equal(t, httpsig.RSA_SHA256, entry.Algorythm, "")
}

View File

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

View File

@ -1,23 +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 TestKeyStore(t *testing.T) {
logger.Init("warn")
ks, err := pubkey.NewTrustStore("test-key.pem")
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, "")
}

View File

@ -6,13 +6,20 @@ import (
"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
Algorythm httpsig.Algorithm
}
// TrustStore is a generic interface to retrieve a public key
type TrustStore interface {
Get(keyID string) (crypto.PublicKey, httpsig.Algorithm, error)
Get(keyID string) *TrustStoreEntry
}
// NewTrustStore creates new Key Store from URI
@ -21,11 +28,12 @@ func NewTrustStore(filename string) (store TrustStore, err error) {
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 TrustStore file format: %s", filename)
err = fmt.Errorf("unsupported trust store file format: %s", filename)
}
return