Repository: honeydipper/honeydipper
Pull request #372 (2021-01-25 21:38:48)

chore(release): release
 * 8391b01 fix(drivers): redis drivers avoid printing password
 * 48c979c feat(drivers): redis drivers support TLS
 * 78dc32a feat(dipper): helper function to check truthy value
 * 37619a6 feat(dipper): support deferred decryption in drivers
 * a6a2615 fix(services): api service discovery using staged dataset (#369)



Patch:
diff --git a/drivers/cmd/redislock/main.go b/drivers/cmd/redislock/main.go
index 03d024a..a9e7706 100644
--- a/drivers/cmd/redislock/main.go
+++ b/drivers/cmd/redislock/main.go
@@ -44,7 +44,7 @@ func (l *Locker) start(msg *dipper.Message) {
 }
 
 func (l *Locker) loadOptions(msg *dipper.Message) {
-	l.redisOptions = redisclient.GetRedisOps(l.driver)
+	l.redisOptions = redisclient.GetRedisOpts(l.driver)
 	var ok bool
 	if l.prefix, ok = dipper.GetMapDataStr(l.driver.Options, "data.prefix"); !ok {
 		l.prefix = DefaultPrefix
diff --git a/drivers/cmd/redispubsub/main.go b/drivers/cmd/redispubsub/main.go
index db17a5e..75f02e0 100644
--- a/drivers/cmd/redispubsub/main.go
+++ b/drivers/cmd/redispubsub/main.go
@@ -52,6 +52,7 @@ func main() {
 
 func loadOptions() {
 	log = driver.GetLogger()
+	redisOptions = redisclient.GetRedisOpts(driver)
 	log.Infof("[%s] receiving driver data %+v", driver.Service, driver.Options)
 
 	broadcastTopic, ok = driver.GetOptionStr("data.topic")
@@ -62,8 +63,6 @@ func loadOptions() {
 	if !ok {
 		broadcastChannel = "broadcast"
 	}
-
-	redisOptions = redisclient.GetRedisOps(driver)
 }
 
 func start(msg *dipper.Message) {
diff --git a/drivers/cmd/redisqueue/main.go b/drivers/cmd/redisqueue/main.go
index cebcaf3..29c293c 100644
--- a/drivers/cmd/redisqueue/main.go
+++ b/drivers/cmd/redisqueue/main.go
@@ -68,6 +68,7 @@ func main() {
 
 func loadOptions() {
 	log = driver.GetLogger()
+	redisOptions = redisclient.GetRedisOpts(driver)
 	log.Infof("[%s] receiving driver data %+v", driver.Service, driver.Options)
 
 	eb := &EventBusOptions{
@@ -89,8 +90,6 @@ func loadOptions() {
 		eb.APITopic = apiTopic
 	}
 	eventbus = eb
-
-	redisOptions = redisclient.GetRedisOps(driver)
 }
 
 func start(msg *dipper.Message) {
diff --git a/drivers/pkg/redisclient/redisclient.go b/drivers/pkg/redisclient/redisclient.go
index f001d4c..542ba93 100644
--- a/drivers/pkg/redisclient/redisclient.go
+++ b/drivers/pkg/redisclient/redisclient.go
@@ -1,31 +1,116 @@
+// Copyright 2021 Honey Science Corporation
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package redisclient is shared by various redis drivers.
 package redisclient
 
 import (
+	"crypto/tls"
+	"crypto/x509"
 	"os"
 	"strconv"
+	"strings"
+	"time"
 
 	"github.com/go-redis/redis/v8"
 	"github.com/honeydipper/honeydipper/pkg/dipper"
 )
 
-// GetRedisOps configures driver to talk to Redis.
-func GetRedisOps(driver *dipper.Driver) *redis.Options {
+func verifyPeerCertificate(config *tls.Config, rawCerts [][]byte, _ [][]*x509.Certificate) error {
+	// the function does the samething as the part this is skipped due to
+	// InsecureSkipVerify in the verifyServerCertificate function from tls
+	// handshake_client.go except that it doesn't do the name and SANs checking.
+
+	var err error
+	certs := make([]*x509.Certificate, len(rawCerts))
+	for i, asn1Data := range rawCerts {
+		certs[i], err = x509.ParseCertificate(asn1Data)
+		if err != nil {
+			//nolint:wrapcheck
+			return err
+		}
+	}
+	vOpts := x509.VerifyOptions{
+		Roots:         config.RootCAs,
+		CurrentTime:   time.Now(),
+		DNSName:       "", // this has be blank to skip verifying names against CN or SANs
+		Intermediates: x509.NewCertPool(),
+	}
+	for _, cert := range certs[1:] {
+		vOpts.Intermediates.AddCert(cert)
+	}
+	_, err = certs[0].Verify(vOpts)
+
+	//nolint:wrapcheck
+	return err
+}
+
+func setupTLSConfig(driver *dipper.Driver) *tls.Config {
+	config := &tls.Config{
+		MinVersion: tls.VersionTLS12,
+	}
+	if driver.CheckOption("data.connection.TLS.InsecureSkipVerify") {
+		config.InsecureSkipVerify = true
+	} else {
+		serverName, ok := driver.GetOptionStr("data.connection.TLS.VerifyServerName")
+		if ok && strings.TrimSpace(serverName) != "*" {
+			config.ServerName = serverName
+		} else if ok {
+			// golang tls doesn't support verify certificate without any SANs, using
+			// InsecureSkipVerify and a custom VerifyPeerCertificate to support this,
+			// so we can use google memorystore redis with TLS.
+
+			config.InsecureSkipVerify = true
+			config.VerifyPeerCertificate = func(rawCerts [][]byte, chains [][]*x509.Certificate) error {
+				return verifyPeerCertificate(config, rawCerts, chains)
+			}
+		}
+
+		var pemblocks []interface{}
+		if caCerts, ok := driver.GetOption("data.connection.TLS.CACerts"); ok {
+			pemblocks = caCerts.([]interface{})
+		}
+		config.RootCAs = dipper.LoadCACerts(pemblocks, true)
+	}
+
+	return config
+}
+
+// GetRedisOpts configures driver to talk to Redis.
+func GetRedisOpts(driver *dipper.Driver) *redis.Options {
+	if conn, ok := dipper.GetMapData(driver.Options, "data.connection"); ok {
+		defer delete(conn.(map[string]interface{}), "Password")
+	}
+	if tls, ok := dipper.GetMapData(driver.Options, "data.connection.TLS"); ok {
+		defer delete(tls.(map[string]interface{}), "CACerts")
+	}
+
 	if localRedis, ok := os.LookupEnv("LOCALREDIS"); ok && localRedis != "" {
 		return &redis.Options{
 			Addr: "127.0.0.1:6379",
 			DB:   0,
 		}
 	}
+
 	opts := &redis.Options{}
 	if value, ok := driver.GetOptionStr("data.connection.Addr"); ok {
 		opts.Addr = value
 	}
+	if value, ok := driver.GetOptionStr("data.connection.Username"); ok {
+		opts.Username = value
+	}
 	if value, ok := driver.GetOptionStr("data.connection.Password"); ok {
 		opts.Password = value
 	}
 	if DB, ok := driver.GetOptionStr("data.connection.DB"); ok {
 		opts.DB = dipper.Must(strconv.Atoi(DB)).(int)
 	}
+	if driver.CheckOption("data.connection.TLS.Enabled") {
+		opts.TLSConfig = setupTLSConfig(driver)
+	}
 
 	return opts
 }
diff --git a/internal/service/api.go b/internal/service/api.go
index 95ac272..c80e68c 100644
--- a/internal/service/api.go
+++ b/internal/service/api.go
@@ -124,7 +124,7 @@ func handleAPIMessage(d *driver.Runtime, m *dipper.Message) {
 // APIFeatures figures out what features need to be loaded for API services.
 func APIFeatures(s *config.DataSet) map[string]interface{} {
 	features := map[string]interface{}{}
-	if providers, ok := dipper.GetMapData(APICfg, "auth-providers"); ok && providers != nil {
+	if providers, ok := dipper.GetMapData(s.Drivers, "daemon.services.api.auth-providers"); ok && providers != nil {
 		if len(providers.([]interface{})) > 0 {
 			for _, p := range providers.([]interface{}) {
 				parts := strings.Split(p.(string), ".")
diff --git a/internal/service/service.go b/internal/service/service.go
index a1232dc..d31efd5 100644
--- a/internal/service/service.go
+++ b/internal/service/service.go
@@ -138,23 +138,26 @@ func (s *Service) GetStream(feature string) io.Writer {
 }
 
 func (s *Service) decryptDriverData(key string, val interface{}) (ret interface{}, replace bool) {
-	if str, ok := val.(string); ok {
-		if strings.HasPrefix(str, "ENC[") {
-			dipper.Logger.Debugf("[%s] decrypting %s", s.name, key)
-			parts := strings.SplitN(str[4:len(str)-1], ",", 2)
-			encDriver := parts[0]
-			data := []byte(parts[1])
-			decoded, err := base64.StdEncoding.DecodeString(string(data))
-			if err != nil {
-				dipper.Logger.Panicf("encrypted data shoud be base64 encoded")
-			}
-			decrypted, _ := s.CallRaw("driver:"+encDriver, "decrypt", decoded)
+	str, ok := val.(string)
+	if !ok || !strings.HasPrefix(str, "ENC[") {
+		return nil, false
+	}
 
-			return string(decrypted), true
-		}
+	dipper.Logger.Debugf("[%s] decrypting %s", s.name, key)
+	parts := strings.SplitN(str[4:len(str)-1], ",", 2)
+	encDriver := parts[0]
+	if encDriver == "deferred" {
+		return "ENC[" + parts[1] + "]", true
+	}
+
+	data := []byte(parts[1])
+	decoded, err := base64.StdEncoding.DecodeString(string(data))
+	if err != nil {
+		dipper.Logger.Panicf("encrypted data shoud be base64 encoded")
 	}
+	decrypted, _ := s.CallRaw("driver:"+encDriver, "decrypt", decoded)
 
-	return nil, false
+	return string(decrypted), true
 }
 
 func (s *Service) loadFeature(feature string) (affected bool, driverName string, rerr error) {
diff --git a/pkg/dipper/certs.go b/pkg/dipper/certs.go
new file mode 100644
index 0000000..f4d46c3
--- /dev/null
+++ b/pkg/dipper/certs.go
@@ -0,0 +1,57 @@
+// Copyright 2021 Honey Science Corporation
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package dipper is a library used for developing drivers for Honeydipper.
+package dipper
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"strings"
+)
+
+// LoadCACerts loads CA certs on top of the system certs from SSL_CERT_FILE or SSL_CERT_DIR.
+func LoadCACerts(pemblocks []interface{}, includeSystemCerts bool) *x509.CertPool {
+	var (
+		cp  *x509.CertPool
+		err error
+	)
+
+	if includeSystemCerts {
+		cp, err = x509.SystemCertPool()
+		if err != nil {
+			Logger.Warningf("unable to load system cert pool: %+v", err)
+		}
+	}
+
+	if cp == nil {
+		cp = x509.NewCertPool()
+	}
+
+	for _, pemblock := range pemblocks {
+		rest := []byte(strings.TrimSpace(pemblock.(string)))
+		for len(rest) > 0 {
+			var block *pem.Block
+			block, rest = pem.Decode(rest)
+
+			switch {
+			case block == nil:
+				Logger.Warningf("skipping non cert block")
+			case block.Type != "CERTIFICATE":
+				Logger.Warningf("skipping non cert block: %s", block.Type)
+			default:
+				cert, err := x509.ParseCertificate(block.Bytes)
+				if err != nil {
+					Logger.Warningf("skipping invalid CA cert due to: %s", err)
+				} else {
+					cp.AddCert(cert)
+				}
+			}
+		}
+	}
+
+	return cp
+}
diff --git a/pkg/dipper/condition.go b/pkg/dipper/condition.go
index 07cbde1..9823505 100644
--- a/pkg/dipper/condition.go
+++ b/pkg/dipper/condition.go
@@ -143,3 +143,20 @@ func RegexParser(key string, val interface{}) (ret interface{}, replace bool) {
 
 	return nil, false
 }
+
+// IsTruthy is a helper function to check if a value represents Truthy value, and only boolean true or
+// string "true" (ignore case and trailing leading white spaces) will yield truthy value.
+func IsTruthy(v interface{}) bool {
+	switch c := v.(type) {
+	case string:
+		c = strings.ToLower(strings.TrimSpace(c))
+
+		return c == "true"
+	case nil:
+		return false
+	case bool:
+		return c
+	}
+
+	return false
+}
diff --git a/pkg/dipper/driver.go b/pkg/dipper/driver.go
index 158097d..618502b 100644
--- a/pkg/dipper/driver.go
+++ b/pkg/dipper/driver.go
@@ -111,6 +111,7 @@ func (d *Driver) Ping(msg *Message) {
 func (d *Driver) ReceiveOptions(msg *Message) {
 	msg = DeserializePayload(msg)
 	Recursive(msg.Payload, RegexParser)
+	DecryptAll(d, msg.Payload)
 	d.Options = msg.Payload
 	Logger = nil
 	d.GetLogger()
@@ -163,6 +164,11 @@ func (d *Driver) SendMessage(m *Message) {
 	SendMessage(d.Out, m)
 }
 
+// CheckOption : get the data from options and check if it is truthy.
+func (d *Driver) CheckOption(path string) bool {
+	return CheckMapData(d.Options, path)
+}
+
 // GetOption : get the data from options map with the key.
 func (d *Driver) GetOption(path string) (interface{}, bool) {
 	return GetMapData(d.Options, path)
diff --git a/pkg/dipper/encryption.go b/pkg/dipper/encryption.go
index 83fd2e0..e804c86 100644
--- a/pkg/dipper/encryption.go
+++ b/pkg/dipper/encryption.go
@@ -15,21 +15,25 @@ import (
 func DecryptAll(rpc RPCCaller, from interface{}) {
 	Recursive(from, func(key string, val interface{}) (interface{}, bool) {
 		Logger.Debugf("[%s] decrypting %s", rpc.GetName(), key)
-		if str, ok := val.(string); ok {
-			if strings.HasPrefix(str, "ENC[") {
-				parts := strings.SplitN(str[4:len(str)-1], ",", 2)
-				encDriver := parts[0]
-				data := []byte(parts[1])
-				decoded, err := base64.StdEncoding.DecodeString(string(data))
-				if err != nil {
-					Logger.Panicf("encrypted data shoud be base64 encoded")
-				}
-				decrypted, _ := rpc.CallRaw("driver:"+encDriver, "decrypt", decoded)
+		str, ok := val.(string)
+		if !ok || !strings.HasPrefix(str, "ENC[") {
+			return nil, false
+		}
+
+		parts := strings.SplitN(str[4:len(str)-1], ",", 2)
+		encDriver := parts[0]
+		if encDriver == "deferred" {
+			return "ENC[" + parts[1] + "]", true
+		}
 
-				return string(decrypted), true
-			}
+		data := []byte(parts[1])
+		decoded, err := base64.StdEncoding.DecodeString(string(data))
+		if err != nil {
+			Logger.Panicf("encrypted data shoud be base64 encoded")
 		}
 
-		return nil, false
+		decrypted, _ := rpc.CallRaw("driver:"+encDriver, "decrypt", decoded)
+
+		return string(decrypted), true
 	})
 }
diff --git a/pkg/dipper/mapUtils.go b/pkg/dipper/mapUtils.go
index 85ae472..27580fa 100644
--- a/pkg/dipper/mapUtils.go
+++ b/pkg/dipper/mapUtils.go
@@ -128,6 +128,13 @@ func MustGetMapDataBool(from interface{}, path string) bool {
 	panic(fmt.Errorf("%w: not a bool: %s", ErrMapError, path))
 }
 
+// CheckMapData check if data exists and is truthy.
+func CheckMapData(from interface{}, path string) bool {
+	v, ok := GetMapData(from, path)
+
+	return ok && IsTruthy(v)
+}
+
 // ItemProcessor is a function processing one of the items in a data structure.
 type ItemProcessor func(key string, val interface{}) (newval interface{}, ok bool)
 


Test patch:
diff --git a/cmd/honeydipper/integration_test.go b/cmd/honeydipper/integration_test.go
index 14ec492..8028560 100644
--- a/cmd/honeydipper/integration_test.go
+++ b/cmd/honeydipper/integration_test.go
@@ -11,6 +11,7 @@ package main
 import (
 	"context"
 	"fmt"
+	"net/http"
 	"os"
 	"os/exec"
 	"runtime"
@@ -35,6 +36,7 @@ func TestIntegrationStart(t *testing.T) {
 	defer t.Run("shutting down daemon", intTestDaemonShutdown)
 	t.Run("checking services", intTestServices)
 	t.Run("checking processes", intTestProcesses)
+	t.Run("checking processes", intTestMakingAPICall)
 	t.Run("checking crashed driver", intTestDriverCrash)
 }
 
@@ -60,7 +62,7 @@ func intTestDaemonStartup(t *testing.T) {
 		workingBranch = ref[len(ref)-1]
 	}
 	cfg := config.Config{
-		Services: []string{"engine", "receiver", "operator"},
+		Services: []string{"engine", "receiver", "operator", "api"},
 		InitRepo: config.RepoInfo{
 			Repo:   "../..",
 			Branch: workingBranch,
@@ -72,6 +74,7 @@ func intTestDaemonStartup(t *testing.T) {
 			service.StartEngine(&cfg)
 			service.StartReceiver(&cfg)
 			service.StartOperator(&cfg)
+			service.StartAPI(&cfg)
 		}
 		daemon.Run(&cfg)
 	}()
@@ -87,7 +90,9 @@ func intTestServices(t *testing.T) {
 	assert.True(t, ok, "engine service should be running")
 	_, ok = service.Services["operator"]
 	assert.True(t, ok, "operator service should be running")
-	assert.True(t, len(service.Services) == 3, "there should be 3 services running")
+	_, ok = service.Services["api"]
+	assert.True(t, ok, "api service should be running")
+	assert.True(t, len(service.Services) == 4, "there should be 4 services running")
 }
 
 func intTestProcesses(t *testing.T) {
@@ -109,7 +114,28 @@ func intTestProcesses(t *testing.T) {
 	pidstr, err = exec.CommandContext(ctx, "/usr/bin/pgrep", "-P", ppid).Output()
 	assert.Nil(t, err, "should be able to run pgrep to find all child processes")
 	pids := strings.Split(string(pidstr), "\n")
-	assert.Lenf(t, pids, 11, "expecting 10 child processes for honeydipper process")
+	assert.Lenf(t, pids, 18, "expecting 17 child processes for honeydipper process")
+}
+
+func intTestMakingAPICall(t *testing.T) {
+	// making an api call with wrong credentials
+	client := &http.Client{}
+	req, err := http.NewRequest("GET", "http://localhost:9000/api/events", nil)
+	assert.NoErrorf(t, err, "creating http request should not receive error")
+	req.Header.Add("Authorization", "bearer wrongcredentials")
+	resp, err := client.Do(req)
+	defer resp.Body.Close()
+	assert.NoErrorf(t, err, "api call should not receive error")
+	assert.Equalf(t, 401, resp.StatusCode, "api call should fail with bad creds")
+
+	// making an api call with correct credentials
+	req, err = http.NewRequest("GET", "http://localhost:9000/api/events", nil)
+	assert.NoErrorf(t, err, "creating http request should not receive error")
+	req.Header.Add("Authorization", "bearer abcdefg")
+	resp, err = client.Do(req)
+	defer resp.Body.Close()
+	assert.NoErrorf(t, err, "api call should not receive error")
+	assert.Equalf(t, 200, resp.StatusCode, "api call should succeed with correct creds")
 }
 
 func intTestDaemonShutdown(t *testing.T) {
diff --git a/cmd/honeydipper/test_fixtures/bootstrap/daemon.yaml b/cmd/honeydipper/test_fixtures/bootstrap/daemon.yaml
index 1e7d2d6..515f37e 100644
--- a/cmd/honeydipper/test_fixtures/bootstrap/daemon.yaml
+++ b/cmd/honeydipper/test_fixtures/bootstrap/daemon.yaml
@@ -5,12 +5,14 @@ drivers:
     featureMap:
       global:
         eventbus: redisqueue
+        api-broadcast: api-broadcast
     features:
       global:
         - name: "driver:gcloud-kms"
           required: true
         - name: "eventbus"
           required: true
+        - name: api-broadcast
       operator:
         - name: "driver:gcloud-gke"
         - name: "driver:gcloud-dataflow"
@@ -46,4 +48,38 @@ drivers:
         type: builtin
         handlerData:
           shortName: kubernetes
+      auth-simple:
+        name: auth-simple
+        type: builtin
+        handlerData:
+          shortName: auth-simple
+      api-broadcast:
+        name: api-broadcast
+        type: builtin
+        handlerData:
+          shortName: redispubsub
+
+    services:
+      api:
+        auth-providers:
+          - auth-simple
+        auth:
+          casbin:
+            models:
+              - |
+                [request_definition]
+                r = sub, obj, act
+
+                [policy_definition]
+                p = sub, obj, act
+
+                [policy_effect]
+                e = some(where (p.eft == allow))
+
+                [matchers]
+                m = r.sub == p.sub && (r.obj == p.obj || p.obj == "*") && (r.act == p.act || p.act == "*")
 
+            policies:
+              - |
+                # define basic policy effects
+                p, admin, *, *
diff --git a/cmd/honeydipper/test_fixtures/bootstrap/drivers.yaml b/cmd/honeydipper/test_fixtures/bootstrap/drivers.yaml
index 8ee57b1..0b33d32 100644
--- a/cmd/honeydipper/test_fixtures/bootstrap/drivers.yaml
+++ b/cmd/honeydipper/test_fixtures/bootstrap/drivers.yaml
@@ -3,7 +3,18 @@ drivers:
   redisqueue:
     connection:
       Addr: localhost:6379
+  api-broadcast:
+    topic: honeydipper:api-broadcast
+    channel: api
+    connection:
+      Addr: 127.0.0.1:6379
   webhook:
     Addr: "127.0.0.1:8081"
   gcloud-kms:
     keyname: projects/test-project/locations/us-central1/keyRings/secure_config/cryptoKeys/kube_secrets
+  auth-simple:
+    schemes:
+      - token
+    tokens:
+      - token: '$2y$05$3XXyVmdtvdLS1i2gV4qPZO0EPDwEL8BiVlTWQM4fYXEz4UysQaixu'
+        subject: admin
diff --git a/pkg/dipper/certs_test.go b/pkg/dipper/certs_test.go
new file mode 100644
index 0000000..8e24545
--- /dev/null
+++ b/pkg/dipper/certs_test.go
@@ -0,0 +1,63 @@
+// Copyright 2021 Honey Science Corporation
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+// +build !integration
+
+package dipper
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLoadCACerts(t *testing.T) {
+	certs := []interface{}{
+		`
+-----BEGIN CERTIFICATE-----
+MIIBuDCCAV4CCQDX6zn1LwUpWDAKBggqhkjOPQQDAjBkMQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCQ0ExFDASBgNVBAcMC0xvcyBBbmdlbGVzMRAwDgYDVQQKDAdDb21w
+YW55MRAwDgYDVQQLDAdTZWN0aW9uMQ4wDAYDVQQDDAVUZXN0MTAeFw0yMTAxMjQw
+MTIwNDFaFw0yMjAxMjQwMTIwNDFaMGQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD
+QTEUMBIGA1UEBwwLTG9zIEFuZ2VsZXMxEDAOBgNVBAoMB0NvbXBhbnkxEDAOBgNV
+BAsMB1NlY3Rpb24xDjAMBgNVBAMMBVRlc3QxMFkwEwYHKoZIzj0CAQYIKoZIzj0D
+AQcDQgAELv64EsaVD0o3JJG/UMuVPmo570VN3hxtKB3nkJ3tNXYaurSgpe1AohMX
+/0avIuqnzlxM2JKkevYgW/HJyY+oVzAKBggqhkjOPQQDAgNIADBFAiBcDGOA7sy8
+MPZ+cqSWEXI4VAanhKCFxpN3urEb+mLtbAIhANjpn1Hjfbx5XSwWrHPfoCAoRzPd
+KiCFvgTSZQojAG8t
+-----END CERTIFICATE-----
+		`,
+		`
+-----BEGIN CERTIFICATE-----
+MIIBszCCAVgCCQCb9f3CUhy1wzAKBggqhkjOPQQDAjBhMQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCQ0ExETAPBgNVBAcMCFBhc2FkZW5hMRAwDgYDVQQKDAdDb21wYW55
+MRAwDgYDVQQLDAdTZWN0aW9uMQ4wDAYDVQQDDAVUZXN0MjAeFw0yMTAxMjQwMTIx
+NDBaFw0yMjAxMjQwMTIxNDBaMGExCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTER
+MA8GA1UEBwwIUGFzYWRlbmExEDAOBgNVBAoMB0NvbXBhbnkxEDAOBgNVBAsMB1Nl
+Y3Rpb24xDjAMBgNVBAMMBVRlc3QyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
+Lv64EsaVD0o3JJG/UMuVPmo570VN3hxtKB3nkJ3tNXYaurSgpe1AohMX/0avIuqn
+zlxM2JKkevYgW/HJyY+oVzAKBggqhkjOPQQDAgNJADBGAiEAwXclv5JcBBOAFcJ1
+mTh7GDlF5m4UBEQUAx4Sm4nVEAoCIQDoenOGwxdEvw5xbeZizct50bOZp1dU4u2c
+YYaBtD/3UQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBrTCCAVICCQC6ufl87XwWCjAKBggqhkjOPQQDAjBeMQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCQ0ExDjAMBgNVBAcMBUF6dXNhMRAwDgYDVQQKDAdDb21wYW55MRAw
+DgYDVQQLDAdTZWN0aW9uMQ4wDAYDVQQDDAVUZXN0MzAeFw0yMTAxMjQwMTIyMTha
+Fw0yMjAxMjQwMTIyMThaMF4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEOMAwG
+A1UEBwwFQXp1c2ExEDAOBgNVBAoMB0NvbXBhbnkxEDAOBgNVBAsMB1NlY3Rpb24x
+DjAMBgNVBAMMBVRlc3QzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELv64EsaV
+D0o3JJG/UMuVPmo570VN3hxtKB3nkJ3tNXYaurSgpe1AohMX/0avIuqnzlxM2JKk
+evYgW/HJyY+oVzAKBggqhkjOPQQDAgNJADBGAiEA3wxNoM814xT/QVpUoQrmhurE
+s99PXe7pSqqKPGBq5Y0CIQCK367N+dWphmnMV4z3RO2loTPDlJaGh2Jz0VhQ4AE/
+Ow==
+-----END CERTIFICATE-----
+		`,
+	}
+	assert.NotEmpty(t, LoadCACerts(nil, true).Subjects(), "system cert pool should not be empty")
+	assert.Empty(t, LoadCACerts(nil, false).Subjects(), "pool should be empty")
+	assert.Equal(t, 3, len(LoadCACerts(certs, false).Subjects()), "should load 3 CA certs")
+}
diff --git a/pkg/dipper/condition_test.go b/pkg/dipper/condition_test.go
index a49a3aa..c6f27da 100644
--- a/pkg/dipper/condition_test.go
+++ b/pkg/dipper/condition_test.go
@@ -70,3 +70,20 @@ func TestCompareAllMap(t *testing.T) {
 	assert.False(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2", "key3": "val3"}, "invalid condition"), "fail with invalid condition for map value")
 	assert.False(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2", "key3": "val3"}, map[string]interface{}{"key4": "111"}), "fail with condition that missing value")
 }
+
+func TestIsTruthy(t *testing.T) {
+	assert.False(t, IsTruthy(nil), "nil should NOT be truthy")
+	assert.False(t, IsTruthy(" 	"), "whitespace only string should NOT be truthy")
+	assert.False(t, IsTruthy("some random string"), "non-empty string should NOT be truthy")
+	assert.False(t, IsTruthy("false"), `"false" string should NOT be truthy`)
+	assert.False(t, IsTruthy(map[string]interface{}{}), "empty map should NOT be truthy")
+	assert.False(t, IsTruthy(map[string]interface{}{"k": "v"}), "non-empty map should NOT be truthy")
+	assert.False(t, IsTruthy([]interface{}{}), "empty list should NOT be truthy")
+	assert.False(t, IsTruthy([]interface{}{"k"}), "non-empty list should NOT be truthy")
+	assert.False(t, IsTruthy(0), "0 should NOT be truthy")
+	assert.False(t, IsTruthy(-3), "non-zero number should NOT be truthy")
+
+	assert.True(t, IsTruthy("True"), `"True" string should be truthy`)
+	assert.True(t, IsTruthy("true"), `"true" string should be truthy`)
+	assert.True(t, IsTruthy(true), `boolean "true" should be truthy`)
+}
diff --git a/pkg/dipper/encryption_test.go b/pkg/dipper/encryption_test.go
new file mode 100644
index 0000000..8a105e3
--- /dev/null
+++ b/pkg/dipper/encryption_test.go
@@ -0,0 +1,111 @@
+// Copyright 2021 Honey Science Corporation
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+// +build !integration
+
+package dipper
+
+import (
+	"io"
+	"testing"
+
+	"github.com/ghodss/yaml"
+	"github.com/golang/mock/gomock"
+	"github.com/honeydipper/honeydipper/pkg/dipper/mock_dipper"
+	"github.com/stretchr/testify/assert"
+)
+
+func wrapDecryptAll(t *testing.T, doc string, expect func(io.Reader, *RPCCallerBase)) map[string]interface{} {
+	var data map[string]interface{}
+	assert.NoError(t, yaml.Unmarshal([]byte(doc), &data))
+
+	O, I := io.Pipe()
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	mockStub := mock_dipper.NewMockRPCCallerStub(ctrl)
+	c := &RPCCallerBase{}
+	mockStub.EXPECT().GetName().AnyTimes().Return("mockCaller")
+	mockStub.EXPECT().GetStream(gomock.Any()).AnyTimes().Return(I)
+	c.Init(mockStub, "rpc", "call")
+
+	done := make(chan struct{})
+	go func() {
+		assert.NotPanics(t, func() { expect(O, c) }, "should not panic when sending and receiving messages")
+		assert.PanicsWithError(t, "invalid message envelope: io: read/write on closed pipe", func() { FetchRawMessage(O) }, "should contain no more messages")
+		close(done)
+	}()
+
+	DecryptAll(c, data)
+
+	O.Close()
+	I.Close()
+	<-done
+
+	return data
+}
+
+func TestDecryptAll(t *testing.T) {
+	doc := `
+data:
+  item1: not encrypted
+  item2: ENC[noexist,YWFiYmNjZGQ=]
+`
+
+	expect := func(O io.Reader, c *RPCCallerBase) {
+		assert.Equalf(t,
+			Message{
+				Channel: "rpc",
+				Subject: "call",
+				IsRaw:   true,
+				Size:    8,
+				Payload: []byte("aabbccdd"),
+				Labels: map[string]string{
+					"caller":  "-",
+					"feature": "driver:noexist",
+					"method":  "decrypt",
+					"rpcID":   "0",
+				},
+			},
+			*FetchRawMessage(O),
+			"should make call to decrypt item2",
+		)
+		c.HandleReturn(&Message{
+			Channel: "rpc",
+			Subject: "return",
+			IsRaw:   true,
+			Size:    18,
+			Payload: []byte("decrypted aabbccdd"),
+			Labels: map[string]string{
+				"caller":  "-",
+				"feature": "driver:noexist",
+				"method":  "decrypt",
+				"rpcID":   "0",
+				"status":  "success",
+			},
+		})
+	}
+
+	data := wrapDecryptAll(t, doc, expect)
+	assert.Equal(t, "decrypted aabbccdd", MustGetMapDataStr(data, "data.item2"), "data.item2 should container decrypted data")
+	assert.Equal(t, "not encrypted", MustGetMapDataStr(data, "data.item1"), "data.item1 should remain unchanged")
+}
+
+func TestDecryptAllWithDeferred(t *testing.T) {
+	doc := `
+data:
+  item1: not encrypted
+  item3:
+    item4: ENC[deferred,driver1,YWFiYmNjZGQ=]
+`
+
+	expect := func(O io.Reader, c *RPCCallerBase) {
+	}
+
+	data := wrapDecryptAll(t, doc, expect)
+	assert.Equal(t, "ENC[driver1,YWFiYmNjZGQ=]", MustGetMapDataStr(data, "data.item3.item4"), "item4 should be stripped off one deferred flag")
+	assert.Equal(t, "not encrypted", MustGetMapDataStr(data, "data.item1"), "data.item1 should remain unchanged")
+}
diff --git a/pkg/dipper/mapUtils_test.go b/pkg/dipper/mapUtils_test.go
index 46c0a5e..5ac5f36 100644
--- a/pkg/dipper/mapUtils_test.go
+++ b/pkg/dipper/mapUtils_test.go
@@ -181,3 +181,28 @@ func TestMerge(t *testing.T) {
 		assert.Equal(t, e, d, n)
 	}
 }
+
+func TestCheckMapData(t *testing.T) {
+	data := map[string]interface{}{
+		"1": map[string]interface{}{
+			"a": "there",
+			"b": "false",
+			"c": 0,
+		},
+		"2": []interface{}{
+			false,
+			"true",
+			true,
+		},
+	}
+
+	assert.False(t, CheckMapData(data, "1.a"), `"1.a" value "there" should be false.`)
+	assert.False(t, CheckMapData(data, "1.b"), `"1.b" value "false" should be false.`)
+	assert.False(t, CheckMapData(data, "1.c"), `"1.c" value 0 should be false.`)
+	assert.False(t, CheckMapData(data, "1.d"), `"1.d" not present should be false.`)
+	assert.False(t, CheckMapData(data, "2.0"), `"2.0" value false should be false.`)
+	assert.False(t, CheckMapData(data, "2.3"), `"2.3" value not present should be false.`)
+
+	assert.True(t, CheckMapData(data, "2.1"), `"2.1" value "true" should be true.`)
+	assert.True(t, CheckMapData(data, "2.2"), `"2.2" value boolean "true" should be true.`)
+}