blob: ccb571e275d596b73cec6ce9ac91396e4b998593 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cache
import (
"bytes"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"sync"
"testing"
"time"
)
import (
"istio.io/pkg/log"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/file"
"github.com/apache/dubbo-go-pixiu/pkg/security"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/retry"
"github.com/apache/dubbo-go-pixiu/pkg/testcerts"
"github.com/apache/dubbo-go-pixiu/security/pkg/nodeagent/caclient/providers/mock"
"github.com/apache/dubbo-go-pixiu/security/pkg/nodeagent/cafile"
pkiutil "github.com/apache/dubbo-go-pixiu/security/pkg/pki/util"
)
func TestWorkloadAgentGenerateSecret(t *testing.T) {
t.Run("plugin", func(t *testing.T) {
testWorkloadAgentGenerateSecret(t, true)
})
t.Run("no plugin", func(t *testing.T) {
testWorkloadAgentGenerateSecret(t, false)
})
}
func createCache(t *testing.T, caClient security.Client, notifyCb func(resourceName string), options security.Options) *SecretManagerClient {
t.Helper()
sc, err := NewSecretManagerClient(caClient, &options)
if err != nil {
t.Fatal(err)
}
sc.RegisterSecretHandler(notifyCb)
t.Cleanup(sc.Close)
return sc
}
func testWorkloadAgentGenerateSecret(t *testing.T, isUsingPluginProvider bool) {
fakeCACli, err := mock.NewMockCAClient(time.Hour, true)
var got, want []byte
if err != nil {
t.Fatalf("Error creating Mock CA client: %v", err)
}
opt := &security.Options{}
if isUsingPluginProvider {
fakePlugin := mock.NewMockTokenExchangeServer(nil)
opt.TokenExchanger = fakePlugin
}
sc := createCache(t, fakeCACli, func(resourceName string) {}, security.Options{})
gotSecret, err := sc.GenerateSecret(security.WorkloadKeyCertResourceName)
if err != nil {
t.Fatalf("Failed to get secrets: %v", err)
}
if got, want := gotSecret.CertificateChain, []byte(strings.Join(fakeCACli.GeneratedCerts[0], "")); !bytes.Equal(got, want) {
t.Errorf("Got unexpected certificate chain #1. Got: %v, want: %v", string(got), string(want))
}
gotSecretRoot, err := sc.GenerateSecret(security.RootCertReqResourceName)
if err != nil {
t.Fatalf("Failed to get secrets: %v", err)
}
// Root cert is the last element in the generated certs.
got, want = gotSecretRoot.RootCert, []byte(strings.TrimSuffix(fakeCACli.GeneratedCerts[0][2], "\n"))
if !bytes.Equal(got, want) {
t.Errorf("Got unexpected root certificate. Got: %v\n want: %v", string(got), string(want))
}
// Try to get secret again, verify secret is not generated.
gotSecret, err = sc.GenerateSecret(security.WorkloadKeyCertResourceName)
if err != nil {
t.Fatalf("Failed to get secrets: %v", err)
}
if got, want := gotSecret.CertificateChain, []byte(strings.Join(fakeCACli.GeneratedCerts[0], "")); !bytes.Equal(got, want) {
t.Errorf("Got unexpected certificate chain #1. Got: %v, want: %v", string(got), string(want))
}
// Root cert is the last element in the generated certs.
want = []byte(fakeCACli.GeneratedCerts[0][2])
if got := sc.cache.GetRoot(); !bytes.Equal(got, want) {
t.Errorf("Got unexpected root certificate. Got: %v\n want: %v", string(got), string(want))
}
}
type UpdateTracker struct {
t *testing.T
hits map[string]int
mu sync.Mutex
}
func NewUpdateTracker(t *testing.T) *UpdateTracker {
return &UpdateTracker{
t: t,
hits: map[string]int{},
mu: sync.Mutex{},
}
}
func (u *UpdateTracker) Callback(name string) {
u.mu.Lock()
defer u.mu.Unlock()
u.hits[name]++
}
func (u *UpdateTracker) Expect(want map[string]int) {
u.t.Helper()
retry.UntilSuccessOrFail(u.t, func() error {
u.mu.Lock()
defer u.mu.Unlock()
if !reflect.DeepEqual(u.hits, want) {
return fmt.Errorf("wanted %+v got %+v", want, u.hits)
}
return nil
}, retry.Timeout(time.Second*5))
}
func (u *UpdateTracker) Reset() {
u.mu.Lock()
defer u.mu.Unlock()
u.hits = map[string]int{}
}
func TestWorkloadAgentRefreshSecret(t *testing.T) {
cacheLog.SetOutputLevel(log.DebugLevel)
fakeCACli, err := mock.NewMockCAClient(time.Millisecond*200, false)
if err != nil {
t.Fatalf("Error creating Mock CA client: %v", err)
}
u := NewUpdateTracker(t)
sc := createCache(t, fakeCACli, u.Callback, security.Options{})
_, err = sc.GenerateSecret(security.WorkloadKeyCertResourceName)
if err != nil {
t.Fatalf("failed to get secrets: %v", err)
}
// First update will trigger root cert immediately, then workload cert once it expires in 200ms
u.Expect(map[string]int{security.WorkloadKeyCertResourceName: 1, security.RootCertReqResourceName: 1})
_, err = sc.GenerateSecret(security.WorkloadKeyCertResourceName)
if err != nil {
t.Fatalf("failed to get secrets: %v", err)
}
u.Expect(map[string]int{security.WorkloadKeyCertResourceName: 2, security.RootCertReqResourceName: 1})
}
// Compare times, with 5s error allowance
func almostEqual(t1, t2 time.Duration) bool {
diff := t1 - t2
if diff < 0 {
diff *= -1
}
return diff < time.Second*5
}
func TestRotateTime(t *testing.T) {
now := time.Now()
cases := []struct {
name string
created time.Time
expire time.Time
gracePeriod float64
expected time.Duration
}{
{
name: "already expired",
created: now.Add(-time.Second * 2),
expire: now.Add(-time.Second),
gracePeriod: 0.5,
expected: 0,
},
{
name: "grace period .50",
created: now,
expire: now.Add(time.Hour),
gracePeriod: 0.5,
expected: time.Minute * 30,
},
{
name: "grace period .25",
created: now,
expire: now.Add(time.Hour),
gracePeriod: 0.25,
expected: time.Minute * 45,
},
{
name: "grace period .75",
created: now,
expire: now.Add(time.Hour),
gracePeriod: 0.75,
expected: time.Minute * 15,
},
{
name: "grace period 1",
created: now,
expire: now.Add(time.Hour),
gracePeriod: 1,
expected: 0,
},
{
name: "grace period 0",
created: now,
expire: now.Add(time.Hour),
gracePeriod: 0,
expected: time.Hour,
},
{
name: "grace period .25 shifted",
created: now.Add(time.Minute * 30),
expire: now.Add(time.Minute * 90),
gracePeriod: 0.25,
expected: time.Minute * 75,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
sc := &SecretManagerClient{configOptions: &security.Options{SecretRotationGracePeriodRatio: tt.gracePeriod}}
got := sc.rotateTime(security.SecretItem{CreatedTime: tt.created, ExpireTime: tt.expire})
if !almostEqual(got, tt.expected) {
t.Fatalf("expected %v got %v", tt.expected, got)
}
})
}
}
func TestRootCertificateExists(t *testing.T) {
testCases := map[string]struct {
certPath string
expectResult bool
}{
"cert not exist": {
certPath: "./invalid-path/invalid-file",
expectResult: false,
},
"cert valid": {
certPath: "./testdata/cert-chain.pem",
expectResult: true,
},
}
sc := createCache(t, nil, func(resourceName string) {}, security.Options{})
for _, tc := range testCases {
ret := sc.rootCertificateExist(tc.certPath)
if tc.expectResult != ret {
t.Errorf("unexpected result is returned!")
}
}
}
func TestKeyCertificateExist(t *testing.T) {
testCases := map[string]struct {
certPath string
keyPath string
expectResult bool
}{
"cert not exist": {
certPath: "./invalid-path/invalid-file",
keyPath: "./testdata/cert-chain.pem",
expectResult: false,
},
"key not exist": {
certPath: "./testdata/cert-chain.pem",
keyPath: "./invalid-path/invalid-file",
expectResult: false,
},
"key and cert valid": {
certPath: "./testdata/cert-chain.pem",
keyPath: "./testdata/cert-chain.pem",
expectResult: true,
},
}
sc := createCache(t, nil, func(resourceName string) {}, security.Options{})
for _, tc := range testCases {
ret := sc.keyCertificateExist(tc.certPath, tc.keyPath)
if tc.expectResult != ret {
t.Errorf("unexpected result is returned!")
}
}
}
func setupTestDir(t *testing.T, sc *SecretManagerClient) {
dir := t.TempDir()
for _, f := range []string{"root-cert.pem", "key.pem", "cert-chain.pem"} {
if err := file.AtomicCopy(filepath.Join("./testdata", f), dir, f); err != nil {
t.Fatal(err)
}
}
sc.existingCertificateFile = security.SdsCertificateConfig{
CertificatePath: filepath.Join(dir, "cert-chain.pem"),
PrivateKeyPath: filepath.Join(dir, "key.pem"),
CaCertificatePath: filepath.Join(dir, "root-cert.pem"),
}
}
// TestWorkloadAgentGenerateSecretFromFile tests generating secrets from existing files on a
// secretcache instance.
func TestFileSecrets(t *testing.T) {
t.Run("file", func(t *testing.T) {
runFileAgentTest(t, false)
})
t.Run("file sds", func(t *testing.T) {
runFileAgentTest(t, true)
})
}
func runFileAgentTest(t *testing.T, sds bool) {
fakeCACli, err := mock.NewMockCAClient(time.Hour, false)
if err != nil {
t.Fatalf("Error creating Mock CA client: %v", err)
}
opt := security.Options{}
u := NewUpdateTracker(t)
sc := createCache(t, fakeCACli, u.Callback, opt)
setupTestDir(t, sc)
workloadResource := security.WorkloadKeyCertResourceName
rootResource := security.RootCertReqResourceName
if sds {
workloadResource = sc.existingCertificateFile.GetResourceName()
rootResource = sc.existingCertificateFile.GetRootResourceName()
}
certchain, err := os.ReadFile(sc.existingCertificateFile.CertificatePath)
if err != nil {
t.Fatalf("Error reading the cert chain file: %v", err)
}
privateKey, err := os.ReadFile(sc.existingCertificateFile.PrivateKeyPath)
if err != nil {
t.Fatalf("Error reading the private key file: %v", err)
}
rootCert, err := os.ReadFile(sc.existingCertificateFile.CaCertificatePath)
if err != nil {
t.Fatalf("Error reading the root cert file: %v", err)
}
// Check we can load key, cert, and root
checkSecret(t, sc, workloadResource, security.SecretItem{
ResourceName: workloadResource,
CertificateChain: certchain,
PrivateKey: privateKey,
})
checkSecret(t, sc, rootResource, security.SecretItem{
ResourceName: rootResource,
RootCert: rootCert,
})
// We shouldn't get an pushes; these only happen on changes
u.Expect(map[string]int{})
u.Reset()
if err := file.AtomicWrite(sc.existingCertificateFile.CertificatePath, testcerts.RotatedCert, os.FileMode(0o644)); err != nil {
t.Fatal(err)
}
if err := file.AtomicWrite(sc.existingCertificateFile.PrivateKeyPath, testcerts.RotatedKey, os.FileMode(0o644)); err != nil {
t.Fatal(err)
}
// Expect update callback
u.Expect(map[string]int{workloadResource: 1})
// On the next generate call, we should get the new cert
checkSecret(t, sc, workloadResource, security.SecretItem{
ResourceName: workloadResource,
CertificateChain: testcerts.RotatedCert,
PrivateKey: testcerts.RotatedKey,
})
if err := file.AtomicWrite(sc.existingCertificateFile.PrivateKeyPath, testcerts.RotatedKey, os.FileMode(0o644)); err != nil {
t.Fatal(err)
}
// We do NOT expect update callback. We only watch the cert file, since the key and cert must be updated
// in tandem.
u.Expect(map[string]int{workloadResource: 1})
u.Reset()
checkSecret(t, sc, workloadResource, security.SecretItem{
ResourceName: workloadResource,
CertificateChain: testcerts.RotatedCert,
PrivateKey: testcerts.RotatedKey,
})
if err := file.AtomicWrite(sc.existingCertificateFile.CaCertificatePath, testcerts.CACert, os.FileMode(0o644)); err != nil {
t.Fatal(err)
}
// We expect to get an update notification, and the new root cert to be read
u.Expect(map[string]int{rootResource: 1})
u.Reset()
checkSecret(t, sc, rootResource, security.SecretItem{
ResourceName: rootResource,
RootCert: testcerts.CACert,
})
// Remove the file and add it again and validate that proxy is updated with new cert.
if err := os.Remove(sc.existingCertificateFile.CaCertificatePath); err != nil {
t.Fatal(err)
}
if err := file.AtomicWrite(sc.existingCertificateFile.CaCertificatePath, testcerts.CACert, os.FileMode(0o644)); err != nil {
t.Fatal(err)
}
// We expect to get an update notification, and the new root cert to be read
// We do not expect update callback for REMOVE events.
u.Expect(map[string]int{rootResource: 1})
checkSecret(t, sc, rootResource, security.SecretItem{
ResourceName: rootResource,
RootCert: testcerts.CACert,
})
// Double check workload cert is untouched
checkSecret(t, sc, workloadResource, security.SecretItem{
ResourceName: workloadResource,
CertificateChain: testcerts.RotatedCert,
PrivateKey: testcerts.RotatedKey,
})
}
func checkSecret(t *testing.T, sc *SecretManagerClient, name string, expected security.SecretItem) {
t.Helper()
got, err := sc.GenerateSecret(name)
if err != nil {
t.Fatalf("Failed to get secrets: %v", err)
}
verifySecret(t, got, &expected)
}
func TestWorkloadAgentGenerateSecretFromFileOverSdsWithBogusFiles(t *testing.T) {
originalTimeout := totalTimeout
totalTimeout = time.Millisecond * 1
defer func() {
totalTimeout = originalTimeout
}()
u := NewUpdateTracker(t)
sc := createCache(t, nil, u.Callback, security.Options{})
rootCertPath, _ := filepath.Abs("./testdata/root-cert-bogus.pem")
keyPath, _ := filepath.Abs("./testdata/key-bogus.pem")
certChainPath, _ := filepath.Abs("./testdata/cert-chain-bogus.pem")
resource := fmt.Sprintf("file-cert:%s~%s", certChainPath, keyPath)
gotSecret, err := sc.GenerateSecret(resource)
if err == nil {
t.Fatalf("expected to get error")
}
if gotSecret != nil {
t.Fatalf("Expected to get nil secret but got %v", gotSecret)
}
rootResource := "file-root:" + rootCertPath
gotSecretRoot, err := sc.GenerateSecret(rootResource)
if err == nil {
t.Fatalf("Expected to get error, but did not get")
}
if !strings.Contains(err.Error(), "no such file or directory") {
t.Fatalf("Expected file not found error, but got %v", err)
}
if gotSecretRoot != nil {
t.Fatalf("Expected to get nil secret but got %v", gotSecret)
}
u.Expect(map[string]int{})
}
func verifySecret(t *testing.T, gotSecret *security.SecretItem, expectedSecret *security.SecretItem) {
if expectedSecret.ResourceName != gotSecret.ResourceName {
t.Fatalf("resource name:: expected %s but got %s", expectedSecret.ResourceName,
gotSecret.ResourceName)
}
cfg, ok := security.SdsCertificateConfigFromResourceName(expectedSecret.ResourceName)
if expectedSecret.ResourceName == security.RootCertReqResourceName || (ok && cfg.IsRootCertificate()) {
if !bytes.Equal(expectedSecret.RootCert, gotSecret.RootCert) {
t.Fatalf("root cert: expected %v but got %v", expectedSecret.RootCert,
gotSecret.RootCert)
}
} else {
if !bytes.Equal(expectedSecret.CertificateChain, gotSecret.CertificateChain) {
t.Fatalf("cert chain: expected %s but got %s", string(expectedSecret.CertificateChain),
string(gotSecret.CertificateChain))
}
if !bytes.Equal(expectedSecret.PrivateKey, gotSecret.PrivateKey) {
t.Fatalf("private key: expected %s but got %s", string(expectedSecret.PrivateKey), string(gotSecret.PrivateKey))
}
}
if !expectedSecret.ExpireTime.IsZero() && expectedSecret.ExpireTime != gotSecret.ExpireTime {
t.Fatalf("expiration: expected %v but got %v",
expectedSecret.ExpireTime, gotSecret.ExpireTime)
}
}
func TestConcatCerts(t *testing.T) {
cases := []struct {
name string
certs []string
expected string
}{
{
name: "no certs",
certs: []string{},
expected: "",
},
{
name: "single cert",
certs: []string{"a"},
expected: "a",
},
{
name: "multiple certs",
certs: []string{"a", "b"},
expected: "a\nb",
},
{
name: "existing newline",
certs: []string{"a\n", "b"},
expected: "a\nb",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
result := string(concatCerts(c.certs))
if result != c.expected {
t.Fatalf("expected %q, got %q", c.expected, result)
}
})
}
}
func TestProxyConfigAnchors(t *testing.T) {
fakeCACli, err := mock.NewMockCAClient(time.Hour, false)
if err != nil {
t.Fatalf("Error creating Mock CA client: %v", err)
}
u := NewUpdateTracker(t)
sc := createCache(t, fakeCACli, u.Callback, security.Options{})
_, err = sc.GenerateSecret(security.WorkloadKeyCertResourceName)
if err != nil {
t.Errorf("failed to generate certificate for trustAnchor test case")
}
// Ensure Root cert call back gets invoked once
u.Expect(map[string]int{security.RootCertReqResourceName: 1})
u.Reset()
caClientRootCert := []byte(strings.TrimRight(fakeCACli.GeneratedCerts[0][2], "\n"))
// Ensure that contents of the rootCert are correct.
checkSecret(t, sc, security.RootCertReqResourceName, security.SecretItem{
ResourceName: security.RootCertReqResourceName,
RootCert: caClientRootCert,
})
rootCert, err := os.ReadFile(filepath.Join("./testdata", "root-cert.pem"))
if err != nil {
t.Fatalf("Error reading the root cert file: %v", err)
}
// Update the proxyConfig with certs
sc.UpdateConfigTrustBundle(rootCert)
// Ensure Callback gets invoked when updating proxyConfig trust bundle
u.Expect(map[string]int{security.RootCertReqResourceName: 1})
u.Reset()
concatCerts := func(certs ...string) []byte {
expectedRootBytes := []byte{}
sort.Strings(certs)
for _, cert := range certs {
expectedRootBytes = pkiutil.AppendCertByte(expectedRootBytes, []byte(cert))
}
return expectedRootBytes
}
expectedCerts := concatCerts(string(rootCert), string(caClientRootCert))
// Ensure that contents of the rootCert are correct.
checkSecret(t, sc, security.RootCertReqResourceName, security.SecretItem{
ResourceName: security.RootCertReqResourceName,
RootCert: expectedCerts,
})
// Add Duplicates
sc.UpdateConfigTrustBundle(expectedCerts)
// Ensure that contents of the rootCert are correct without the duplicate caClientRootCert
checkSecret(t, sc, security.RootCertReqResourceName, security.SecretItem{
ResourceName: security.RootCertReqResourceName,
RootCert: expectedCerts,
})
if !bytes.Equal(sc.mergeConfigTrustBundle([]string{string(caClientRootCert), string(rootCert)}), expectedCerts) {
t.Fatalf("deduplicate test failed!")
}
// Update the proxyConfig with fakeCaClient certs
sc.UpdateConfigTrustBundle(caClientRootCert)
setupTestDir(t, sc)
rootCert, err = os.ReadFile(sc.existingCertificateFile.CaCertificatePath)
if err != nil {
t.Fatalf("Error reading the root cert file: %v", err)
}
// Check request for workload root-certs merges configuration with ProxyConfig TrustAnchor
checkSecret(t, sc, security.RootCertReqResourceName, security.SecretItem{
ResourceName: security.RootCertReqResourceName,
RootCert: concatCerts(string(rootCert), string(caClientRootCert)),
})
// Check request for non-workload root-certs doesn't configuration with ProxyConfig TrustAnchor
checkSecret(t, sc, sc.existingCertificateFile.GetRootResourceName(), security.SecretItem{
ResourceName: sc.existingCertificateFile.GetRootResourceName(),
RootCert: rootCert,
})
}
func TestOSCACertGenerateSecret(t *testing.T) {
fakeCACli, err := mock.NewMockCAClient(time.Hour, false)
if err != nil {
t.Fatalf("Error creating Mock CA client: %v", err)
}
opt := &security.Options{}
fakePlugin := mock.NewMockTokenExchangeServer(nil)
opt.TokenExchanger = fakePlugin
sc := createCache(t, fakeCACli, func(resourceName string) {}, security.Options{CARootPath: cafile.CACertFilePath})
certPath := security.GetOSRootFilePath()
expected, err := sc.GenerateSecret("file-root:" + certPath)
if err != nil {
t.Fatalf("Could not get OS Cert: %v", err)
}
gotSecret, err := sc.GenerateSecret(security.FileRootSystemCACert)
if err != nil {
t.Fatalf("Error using %s: %v", security.FileRootSystemCACert, err)
}
if !bytes.Equal(gotSecret.RootCert, expected.RootCert) {
t.Fatal("Certs did not match")
}
}
func TestOSCACertGenerateSecretEmpty(t *testing.T) {
fakeCACli, err := mock.NewMockCAClient(time.Hour, false)
if err != nil {
t.Fatalf("Error creating Mock CA client: %v", err)
}
opt := &security.Options{}
fakePlugin := mock.NewMockTokenExchangeServer(nil)
opt.TokenExchanger = fakePlugin
sc := createCache(t, fakeCACli, func(resourceName string) {}, security.Options{})
certPath := security.GetOSRootFilePath()
expected, err := sc.GenerateSecret("file-root:" + certPath)
if err != nil {
t.Fatalf(": %v", err)
}
gotSecret, err := sc.GenerateSecret(security.FileRootSystemCACert)
if err != nil && len(gotSecret.RootCert) != 0 {
t.Fatalf("Error using %s: %v", security.FileRootSystemCACert, err)
}
if bytes.Equal(gotSecret.RootCert, expected.RootCert) {
t.Fatal("Certs did match")
}
}