b3af482e09
* Support Vault Namespaces explicitly in CA config If there is a Namespace entry included in the Vault CA configuration, set it as the Vault Namespace on the Vault client Currently the only way to support Vault namespaces in the Consul CA config is by doing one of the following: 1) Set the VAULT_NAMESPACE environment variable which will be picked up by the Vault API client 2) Prefix all Vault paths with the namespace Neither of these are super pleasant. The first requires direct access and modification to the Consul runtime environment. It's possible and expected, not super pleasant. The second requires more indepth knowledge of Vault and how it uses Namespaces and could be confusing for anyone without that context. It also infers that it is not supported * Add changelog * Remove fmt.Fprint calls * Make comment clearer * Add next consul version to website docs * Add new test for default configuration * go mod tidy * Add skip if vault not present * Tweak changelog text
585 lines
17 KiB
Go
585 lines
17 KiB
Go
package ca
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
vaultapi "github.com/hashicorp/vault/api"
|
|
vaultconst "github.com/hashicorp/vault/sdk/helper/consts"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hashicorp/consul/agent/connect"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
|
)
|
|
|
|
func TestVaultCAProvider_VaultTLSConfig(t *testing.T) {
|
|
config := &structs.VaultCAProviderConfig{
|
|
CAFile: "/capath/ca.pem",
|
|
CAPath: "/capath/",
|
|
CertFile: "/certpath/cert.pem",
|
|
KeyFile: "/certpath/key.pem",
|
|
TLSServerName: "server.name",
|
|
TLSSkipVerify: true,
|
|
}
|
|
tlsConfig := vaultTLSConfig(config)
|
|
require := require.New(t)
|
|
require.Equal(config.CAFile, tlsConfig.CACert)
|
|
require.Equal(config.CAPath, tlsConfig.CAPath)
|
|
require.Equal(config.CertFile, tlsConfig.ClientCert)
|
|
require.Equal(config.KeyFile, tlsConfig.ClientKey)
|
|
require.Equal(config.TLSServerName, tlsConfig.TLSServerName)
|
|
require.Equal(config.TLSSkipVerify, tlsConfig.Insecure)
|
|
}
|
|
|
|
func TestVaultCAProvider_Configure(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
testcases := []struct {
|
|
name string
|
|
rawConfig map[string]interface{}
|
|
expectedValue func(t *testing.T, v *VaultProvider)
|
|
}{
|
|
{
|
|
name: "DefaultConfig",
|
|
rawConfig: map[string]interface{}{},
|
|
expectedValue: func(t *testing.T, v *VaultProvider) {
|
|
headers := v.client.Headers()
|
|
require.Equal(t, "", headers.Get(vaultconst.NamespaceHeaderName))
|
|
require.Equal(t, "pki-root/", v.config.RootPKIPath)
|
|
require.Equal(t, "pki-intermediate/", v.config.IntermediatePKIPath)
|
|
},
|
|
},
|
|
{
|
|
name: "TestConfigWithNamespace",
|
|
rawConfig: map[string]interface{}{"namespace": "ns1"},
|
|
expectedValue: func(t *testing.T, v *VaultProvider) {
|
|
|
|
h := v.client.Headers()
|
|
require.Equal(t, "ns1", h.Get(vaultconst.NamespaceHeaderName))
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testcase := range testcases {
|
|
t.Run(testcase.name, func(t *testing.T) {
|
|
provider, _ := testVaultProviderWithConfig(t, true, testcase.rawConfig)
|
|
|
|
testcase.expectedValue(t, provider)
|
|
})
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func TestVaultCAProvider_SecondaryActiveIntermediate(t *testing.T) {
|
|
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
provider, testVault := testVaultProviderWithConfig(t, false, nil)
|
|
defer testVault.Stop()
|
|
require := require.New(t)
|
|
|
|
cert, err := provider.ActiveIntermediate()
|
|
require.Empty(cert)
|
|
require.NoError(err)
|
|
}
|
|
|
|
func TestVaultCAProvider_RenewToken(t *testing.T) {
|
|
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
testVault, err := runTestVault(t)
|
|
require.NoError(t, err)
|
|
testVault.WaitUntilReady(t)
|
|
|
|
// Create a token with a short TTL to be renewed by the provider.
|
|
ttl := 1 * time.Second
|
|
tcr := &vaultapi.TokenCreateRequest{
|
|
TTL: ttl.String(),
|
|
}
|
|
secret, err := testVault.client.Auth().Token().Create(tcr)
|
|
require.NoError(t, err)
|
|
providerToken := secret.Auth.ClientToken
|
|
|
|
_, err = createVaultProvider(t, true, testVault.Addr, providerToken, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Check the last renewal time.
|
|
secret, err = testVault.client.Auth().Token().Lookup(providerToken)
|
|
require.NoError(t, err)
|
|
firstRenewal, err := secret.Data["last_renewal_time"].(json.Number).Int64()
|
|
require.NoError(t, err)
|
|
|
|
// Wait past the TTL and make sure the token has been renewed.
|
|
retry.Run(t, func(r *retry.R) {
|
|
secret, err = testVault.client.Auth().Token().Lookup(providerToken)
|
|
require.NoError(r, err)
|
|
lastRenewal, err := secret.Data["last_renewal_time"].(json.Number).Int64()
|
|
require.NoError(r, err)
|
|
require.Greater(r, lastRenewal, firstRenewal)
|
|
})
|
|
}
|
|
|
|
func TestVaultCAProvider_Bootstrap(t *testing.T) {
|
|
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
providerWDefaultRootCertTtl, testvault1 := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
|
"LeafCertTTL": "1h",
|
|
})
|
|
defer testvault1.Stop()
|
|
client1 := testvault1.client
|
|
|
|
providerCustomRootCertTtl, testvault2 := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
|
"LeafCertTTL": "1h",
|
|
"RootCertTTL": "8761h",
|
|
})
|
|
defer testvault2.Stop()
|
|
client2 := testvault2.client
|
|
|
|
require := require.New(t)
|
|
|
|
cases := []struct {
|
|
certFunc func() (string, error)
|
|
backendPath string
|
|
rootCaCreation bool
|
|
provider *VaultProvider
|
|
client *vaultapi.Client
|
|
expectedRootCertTTL string
|
|
}{
|
|
{
|
|
certFunc: providerWDefaultRootCertTtl.ActiveRoot,
|
|
backendPath: "pki-root/",
|
|
rootCaCreation: true,
|
|
client: client1,
|
|
provider: providerWDefaultRootCertTtl,
|
|
expectedRootCertTTL: structs.DefaultRootCertTTL,
|
|
},
|
|
{
|
|
certFunc: providerCustomRootCertTtl.ActiveIntermediate,
|
|
backendPath: "pki-intermediate/",
|
|
rootCaCreation: false,
|
|
provider: providerCustomRootCertTtl,
|
|
client: client2,
|
|
expectedRootCertTTL: "8761h",
|
|
},
|
|
}
|
|
|
|
// Verify the root and intermediate certs match the ones in the vault backends
|
|
for _, tc := range cases {
|
|
provider := tc.provider
|
|
client := tc.client
|
|
cert, err := tc.certFunc()
|
|
require.NoError(err)
|
|
req := client.NewRequest("GET", "/v1/"+tc.backendPath+"ca/pem")
|
|
resp, err := client.RawRequest(req)
|
|
require.NoError(err)
|
|
bytes, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(err)
|
|
require.Equal(cert, string(bytes)+"\n")
|
|
|
|
// Should be a valid CA cert
|
|
parsed, err := connect.ParseCert(cert)
|
|
require.NoError(err)
|
|
require.True(parsed.IsCA)
|
|
require.Len(parsed.URIs, 1)
|
|
require.Equal(fmt.Sprintf("spiffe://%s.consul", provider.clusterID), parsed.URIs[0].String())
|
|
|
|
// test that the root cert ttl as applied
|
|
if tc.rootCaCreation {
|
|
rootCertTTL, err := time.ParseDuration(tc.expectedRootCertTTL)
|
|
require.NoError(err)
|
|
expectedNotAfter := time.Now().Add(rootCertTTL).UTC()
|
|
|
|
require.WithinDuration(expectedNotAfter, parsed.NotAfter, 10*time.Minute, "expected parsed cert ttl to be the same as the value configured")
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertCorrectKeyType(t *testing.T, want, certPEM string) {
|
|
t.Helper()
|
|
|
|
cert, err := connect.ParseCert(certPEM)
|
|
require.NoError(t, err)
|
|
|
|
switch want {
|
|
case "ec":
|
|
require.Equal(t, x509.ECDSA, cert.PublicKeyAlgorithm)
|
|
case "rsa":
|
|
require.Equal(t, x509.RSA, cert.PublicKeyAlgorithm)
|
|
default:
|
|
t.Fatal("test doesn't support key type")
|
|
}
|
|
}
|
|
|
|
func TestVaultCAProvider_SignLeaf(t *testing.T) {
|
|
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
for _, tc := range KeyTestCases {
|
|
tc := tc
|
|
t.Run(tc.Desc, func(t *testing.T) {
|
|
require := require.New(t)
|
|
provider, testVault := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
|
"LeafCertTTL": "1h",
|
|
"PrivateKeyType": tc.KeyType,
|
|
"PrivateKeyBits": tc.KeyBits,
|
|
})
|
|
defer testVault.Stop()
|
|
|
|
spiffeService := &connect.SpiffeIDService{
|
|
Host: "node1",
|
|
Namespace: "default",
|
|
Datacenter: "dc1",
|
|
Service: "foo",
|
|
}
|
|
|
|
rootPEM, err := provider.ActiveRoot()
|
|
require.NoError(err)
|
|
assertCorrectKeyType(t, tc.KeyType, rootPEM)
|
|
|
|
intPEM, err := provider.ActiveIntermediate()
|
|
require.NoError(err)
|
|
assertCorrectKeyType(t, tc.KeyType, intPEM)
|
|
|
|
// Generate a leaf cert for the service.
|
|
var firstSerial uint64
|
|
{
|
|
raw, _ := connect.TestCSR(t, spiffeService)
|
|
|
|
csr, err := connect.ParseCSR(raw)
|
|
require.NoError(err)
|
|
|
|
cert, err := provider.Sign(csr)
|
|
require.NoError(err)
|
|
|
|
parsed, err := connect.ParseCert(cert)
|
|
require.NoError(err)
|
|
require.Equal(parsed.URIs[0], spiffeService.URI())
|
|
firstSerial = parsed.SerialNumber.Uint64()
|
|
|
|
// Ensure the cert is valid now and expires within the correct limit.
|
|
now := time.Now()
|
|
require.True(parsed.NotAfter.Sub(now) < time.Hour)
|
|
require.True(parsed.NotBefore.Before(now))
|
|
|
|
// Make sure we can validate the cert as expected.
|
|
require.NoError(connect.ValidateLeaf(rootPEM, cert, []string{intPEM}))
|
|
requireTrailingNewline(t, cert)
|
|
}
|
|
|
|
// Generate a new cert for another service and make sure
|
|
// the serial number is unique.
|
|
spiffeService.Service = "bar"
|
|
{
|
|
raw, _ := connect.TestCSR(t, spiffeService)
|
|
|
|
csr, err := connect.ParseCSR(raw)
|
|
require.NoError(err)
|
|
|
|
cert, err := provider.Sign(csr)
|
|
require.NoError(err)
|
|
|
|
parsed, err := connect.ParseCert(cert)
|
|
require.NoError(err)
|
|
require.Equal(parsed.URIs[0], spiffeService.URI())
|
|
require.NotEqual(firstSerial, parsed.SerialNumber.Uint64())
|
|
|
|
// Ensure the cert is valid now and expires within the correct limit.
|
|
require.True(time.Until(parsed.NotAfter) < time.Hour)
|
|
require.True(parsed.NotBefore.Before(time.Now()))
|
|
|
|
// Make sure we can validate the cert as expected.
|
|
require.NoError(connect.ValidateLeaf(rootPEM, cert, []string{intPEM}))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultCAProvider_CrossSignCA(t *testing.T) {
|
|
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
tests := CASigningKeyTypeCases()
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.Desc, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
if tc.SigningKeyType != tc.CSRKeyType {
|
|
// See https://github.com/hashicorp/vault/issues/7709
|
|
t.Skip("Vault doesn't support cross-signing different key types yet.")
|
|
}
|
|
provider1, testVault1 := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
|
"LeafCertTTL": "1h",
|
|
"PrivateKeyType": tc.SigningKeyType,
|
|
"PrivateKeyBits": tc.SigningKeyBits,
|
|
})
|
|
defer testVault1.Stop()
|
|
|
|
{
|
|
rootPEM, err := provider1.ActiveRoot()
|
|
require.NoError(err)
|
|
assertCorrectKeyType(t, tc.SigningKeyType, rootPEM)
|
|
|
|
intPEM, err := provider1.ActiveIntermediate()
|
|
require.NoError(err)
|
|
assertCorrectKeyType(t, tc.SigningKeyType, intPEM)
|
|
}
|
|
|
|
provider2, testVault2 := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
|
"LeafCertTTL": "1h",
|
|
"PrivateKeyType": tc.CSRKeyType,
|
|
"PrivateKeyBits": tc.CSRKeyBits,
|
|
})
|
|
defer testVault2.Stop()
|
|
|
|
{
|
|
rootPEM, err := provider2.ActiveRoot()
|
|
require.NoError(err)
|
|
assertCorrectKeyType(t, tc.CSRKeyType, rootPEM)
|
|
|
|
intPEM, err := provider2.ActiveIntermediate()
|
|
require.NoError(err)
|
|
assertCorrectKeyType(t, tc.CSRKeyType, intPEM)
|
|
}
|
|
|
|
testCrossSignProviders(t, provider1, provider2)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultProvider_SignIntermediate(t *testing.T) {
|
|
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
tests := CASigningKeyTypeCases()
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.Desc, func(t *testing.T) {
|
|
provider1, testVault1 := testVaultProviderWithConfig(t, true, map[string]interface{}{
|
|
"LeafCertTTL": "1h",
|
|
"PrivateKeyType": tc.SigningKeyType,
|
|
"PrivateKeyBits": tc.SigningKeyBits,
|
|
})
|
|
defer testVault1.Stop()
|
|
|
|
provider2, testVault2 := testVaultProviderWithConfig(t, false, map[string]interface{}{
|
|
"LeafCertTTL": "1h",
|
|
"PrivateKeyType": tc.CSRKeyType,
|
|
"PrivateKeyBits": tc.CSRKeyBits,
|
|
})
|
|
defer testVault2.Stop()
|
|
|
|
testSignIntermediateCrossDC(t, provider1, provider2)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultProvider_SignIntermediateConsul(t *testing.T) {
|
|
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
// primary = Vault, secondary = Consul
|
|
t.Run("pri=vault,sec=consul", func(t *testing.T) {
|
|
provider1, testVault1 := testVaultProviderWithConfig(t, true, nil)
|
|
defer testVault1.Stop()
|
|
|
|
conf := testConsulCAConfig()
|
|
delegate := newMockDelegate(t, conf)
|
|
provider2 := TestConsulProvider(t, delegate)
|
|
cfg := testProviderConfig(conf)
|
|
cfg.IsPrimary = false
|
|
cfg.Datacenter = "dc2"
|
|
require.NoError(t, provider2.Configure(cfg))
|
|
|
|
testSignIntermediateCrossDC(t, provider1, provider2)
|
|
})
|
|
|
|
// primary = Consul, secondary = Vault
|
|
t.Run("pri=consul,sec=vault", func(t *testing.T) {
|
|
conf := testConsulCAConfig()
|
|
delegate := newMockDelegate(t, conf)
|
|
provider1 := TestConsulProvider(t, delegate)
|
|
require.NoError(t, provider1.Configure(testProviderConfig(conf)))
|
|
require.NoError(t, provider1.GenerateRoot())
|
|
|
|
// Ensure that we don't configure vault to try and mint leafs that
|
|
// outlive their CA during the test (which hard fails in vault).
|
|
intermediateCertTTL := getIntermediateCertTTL(t, conf)
|
|
leafCertTTL := intermediateCertTTL - 4*time.Hour
|
|
|
|
overrideConf := map[string]interface{}{
|
|
"LeafCertTTL": []uint8(leafCertTTL.String()),
|
|
}
|
|
|
|
provider2, testVault2 := testVaultProviderWithConfig(t, false, overrideConf)
|
|
defer testVault2.Stop()
|
|
|
|
testSignIntermediateCrossDC(t, provider1, provider2)
|
|
})
|
|
}
|
|
|
|
func TestVaultProvider_Cleanup(t *testing.T) {
|
|
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
testVault, err := runTestVault(t)
|
|
require.NoError(t, err)
|
|
|
|
testVault.WaitUntilReady(t)
|
|
|
|
t.Run("provider-change", func(t *testing.T) {
|
|
provider, err := createVaultProvider(t, true, testVault.Addr, testVault.RootToken, nil)
|
|
require.NoError(t, err)
|
|
|
|
// ensure that the intermediate PKI mount exists
|
|
mounts, err := provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.Contains(t, mounts, provider.config.IntermediatePKIPath)
|
|
|
|
// call cleanup with a provider change - this should cause removal of the mount
|
|
require.NoError(t, provider.Cleanup(true, nil))
|
|
|
|
// verify the mount was removed
|
|
mounts, err = provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.NotContains(t, mounts, provider.config.IntermediatePKIPath)
|
|
})
|
|
|
|
t.Run("pki-path-change", func(t *testing.T) {
|
|
provider, err := createVaultProvider(t, true, testVault.Addr, testVault.RootToken, nil)
|
|
require.NoError(t, err)
|
|
|
|
// ensure that the intermediate PKI mount exists
|
|
mounts, err := provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.Contains(t, mounts, provider.config.IntermediatePKIPath)
|
|
|
|
// call cleanup with an intermediate pki path change - this should cause removal of the mount
|
|
require.NoError(t, provider.Cleanup(false, map[string]interface{}{
|
|
"Address": testVault.Addr,
|
|
"Token": testVault.RootToken,
|
|
"RootPKIPath": "pki-root/",
|
|
//
|
|
"IntermediatePKIPath": "pki-intermediate2/",
|
|
// Tests duration parsing after msgpack type mangling during raft apply.
|
|
"LeafCertTTL": []uint8("72h"),
|
|
}))
|
|
|
|
// verify the mount was removed
|
|
mounts, err = provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.NotContains(t, mounts, provider.config.IntermediatePKIPath)
|
|
})
|
|
|
|
t.Run("pki-path-unchanged", func(t *testing.T) {
|
|
provider, err := createVaultProvider(t, true, testVault.Addr, testVault.RootToken, nil)
|
|
require.NoError(t, err)
|
|
|
|
// ensure that the intermediate PKI mount exists
|
|
mounts, err := provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.Contains(t, mounts, provider.config.IntermediatePKIPath)
|
|
|
|
// call cleanup with no config changes - this should not cause removal of the intermediate pki path
|
|
require.NoError(t, provider.Cleanup(false, map[string]interface{}{
|
|
"Address": testVault.Addr,
|
|
"Token": testVault.RootToken,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
// Tests duration parsing after msgpack type mangling during raft apply.
|
|
"LeafCertTTL": []uint8("72h"),
|
|
}))
|
|
|
|
// verify the mount was NOT removed
|
|
mounts, err = provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.Contains(t, mounts, provider.config.IntermediatePKIPath)
|
|
})
|
|
}
|
|
|
|
func getIntermediateCertTTL(t *testing.T, caConf *structs.CAConfiguration) time.Duration {
|
|
t.Helper()
|
|
|
|
require.NotNil(t, caConf)
|
|
require.NotNil(t, caConf.Config)
|
|
|
|
iface, ok := caConf.Config["IntermediateCertTTL"]
|
|
require.True(t, ok)
|
|
|
|
ttlBytes, ok := iface.([]uint8)
|
|
require.True(t, ok)
|
|
|
|
ttlString := string(ttlBytes)
|
|
|
|
dur, err := time.ParseDuration(ttlString)
|
|
require.NoError(t, err)
|
|
return dur
|
|
}
|
|
|
|
func testVaultProvider(t *testing.T) (*VaultProvider, *TestVaultServer) {
|
|
return testVaultProviderWithConfig(t, true, nil)
|
|
}
|
|
|
|
func testVaultProviderWithConfig(t *testing.T, isPrimary bool, rawConf map[string]interface{}) (*VaultProvider, *TestVaultServer) {
|
|
testVault, err := runTestVault(t)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
testVault.WaitUntilReady(t)
|
|
|
|
provider, err := createVaultProvider(t, isPrimary, testVault.Addr, testVault.RootToken, rawConf)
|
|
if err != nil {
|
|
testVault.Stop()
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
return provider, testVault
|
|
}
|
|
|
|
func createVaultProvider(t *testing.T, isPrimary bool, addr, token string, rawConf map[string]interface{}) (*VaultProvider, error) {
|
|
conf := map[string]interface{}{
|
|
"Address": addr,
|
|
"Token": token,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
// Tests duration parsing after msgpack type mangling during raft apply.
|
|
"LeafCertTTL": []uint8("72h"),
|
|
}
|
|
for k, v := range rawConf {
|
|
conf[k] = v
|
|
}
|
|
|
|
provider := NewVaultProvider(hclog.New(nil))
|
|
|
|
cfg := ProviderConfig{
|
|
ClusterID: connect.TestClusterID,
|
|
Datacenter: "dc1",
|
|
IsPrimary: true,
|
|
RawConfig: conf,
|
|
}
|
|
|
|
if !isPrimary {
|
|
cfg.IsPrimary = false
|
|
cfg.Datacenter = "dc2"
|
|
}
|
|
|
|
require.NoError(t, provider.Configure(cfg))
|
|
if isPrimary {
|
|
require.NoError(t, provider.GenerateRoot())
|
|
_, err := provider.GenerateIntermediate()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
return provider, nil
|
|
}
|