diff --git a/pkg/assert/assert.go b/pkg/assert/assert.go index 2c3d647..04aad15 100644 --- a/pkg/assert/assert.go +++ b/pkg/assert/assert.go @@ -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) + } +} diff --git a/pkg/middleware/signature.go b/pkg/middleware/signature.go index 0982e0c..6f65d8a 100644 --- a/pkg/middleware/signature.go +++ b/pkg/middleware/signature.go @@ -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())) diff --git a/pkg/pubkey/pem_truststore.go b/pkg/pubkey/pem_truststore.go index 15cee8c..634fb3f 100644 --- a/pkg/pubkey/pem_truststore.go +++ b/pkg/pubkey/pem_truststore.go @@ -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 } diff --git a/pkg/pubkey/test/pem_truststore_test.go b/pkg/pubkey/test/pem_truststore_test.go new file mode 100644 index 0000000..a7943fd --- /dev/null +++ b/pkg/pubkey/test/pem_truststore_test.go @@ -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, "") +} diff --git a/pkg/pubkey/test/test-key.pem b/pkg/pubkey/test/test-key.pem index 934e124..01e9bf0 100644 --- a/pkg/pubkey/test/test-key.pem +++ b/pkg/pubkey/test/test-key.pem @@ -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----- diff --git a/pkg/pubkey/test/truststore_test.go b/pkg/pubkey/test/truststore_test.go deleted file mode 100644 index e10503b..0000000 --- a/pkg/pubkey/test/truststore_test.go +++ /dev/null @@ -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, "") -} diff --git a/pkg/pubkey/truststore.go b/pkg/pubkey/truststore.go index 5214d79..f8193b6 100644 --- a/pkg/pubkey/truststore.go +++ b/pkg/pubkey/truststore.go @@ -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