27bb03bbc0
* adding copyright header * fix fmt and a test
891 lines
33 KiB
Go
891 lines
33 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package pki
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func Test_migrateStorageEmptyStorage(t *testing.T) {
|
|
t.Parallel()
|
|
startTime := time.Now()
|
|
ctx := context.Background()
|
|
b, s := CreateBackendWithStorage(t)
|
|
sc := b.makeStorageContext(ctx, s)
|
|
|
|
// Reset the version the helper above set to 1.
|
|
b.pkiStorageVersion.Store(0)
|
|
require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.")
|
|
|
|
request := &logical.InitializationRequest{Storage: s}
|
|
err := b.initialize(ctx, request)
|
|
require.NoError(t, err)
|
|
|
|
issuerIds, err := sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.Empty(t, issuerIds)
|
|
|
|
keyIds, err := sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.Empty(t, keyIds)
|
|
|
|
logEntry, err := getLegacyBundleMigrationLog(ctx, s)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, logEntry)
|
|
require.Equal(t, latestMigrationVersion, logEntry.MigrationVersion)
|
|
require.True(t, len(strings.TrimSpace(logEntry.Hash)) > 0,
|
|
"Hash value (%s) should not have been empty", logEntry.Hash)
|
|
require.True(t, startTime.Before(logEntry.Created),
|
|
"created log entry time (%v) was before our start time(%v)?", logEntry.Created, startTime)
|
|
require.Empty(t, logEntry.CreatedIssuer)
|
|
require.Empty(t, logEntry.CreatedKey)
|
|
|
|
require.False(t, b.useLegacyBundleCaStorage(), "post migration we are still told to use legacy storage")
|
|
|
|
// Make sure we can re-run the migration without issues
|
|
request = &logical.InitializationRequest{Storage: s}
|
|
err = b.initialize(ctx, request)
|
|
require.NoError(t, err)
|
|
logEntry2, err := getLegacyBundleMigrationLog(ctx, s)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, logEntry2)
|
|
|
|
// Make sure the hash and created times have not changed.
|
|
require.Equal(t, logEntry.Created, logEntry2.Created)
|
|
require.Equal(t, logEntry.Hash, logEntry2.Hash)
|
|
}
|
|
|
|
func Test_migrateStorageOnlyKey(t *testing.T) {
|
|
t.Parallel()
|
|
startTime := time.Now()
|
|
ctx := context.Background()
|
|
b, s := CreateBackendWithStorage(t)
|
|
sc := b.makeStorageContext(ctx, s)
|
|
|
|
// Reset the version the helper above set to 1.
|
|
b.pkiStorageVersion.Store(0)
|
|
require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.")
|
|
|
|
bundle := genCertBundle(t, b, s)
|
|
// Clear everything except for the key
|
|
bundle.SerialNumber = ""
|
|
bundle.CAChain = []string{}
|
|
bundle.Certificate = ""
|
|
bundle.IssuingCA = ""
|
|
|
|
json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle)
|
|
require.NoError(t, err)
|
|
err = s.Put(ctx, json)
|
|
require.NoError(t, err)
|
|
|
|
request := &logical.InitializationRequest{Storage: s}
|
|
err = b.initialize(ctx, request)
|
|
require.NoError(t, err)
|
|
|
|
issuerIds, err := sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, len(issuerIds))
|
|
|
|
keyIds, err := sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(keyIds))
|
|
|
|
logEntry, err := getLegacyBundleMigrationLog(ctx, s)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, logEntry)
|
|
require.Equal(t, latestMigrationVersion, logEntry.MigrationVersion)
|
|
require.True(t, len(strings.TrimSpace(logEntry.Hash)) > 0,
|
|
"Hash value (%s) should not have been empty", logEntry.Hash)
|
|
require.True(t, startTime.Before(logEntry.Created),
|
|
"created log entry time (%v) was before our start time(%v)?", logEntry.Created, startTime)
|
|
require.Equal(t, logEntry.CreatedIssuer, issuerID(""))
|
|
require.Equal(t, logEntry.CreatedKey, keyIds[0])
|
|
|
|
keyId := keyIds[0]
|
|
key, err := sc.fetchKeyById(keyId)
|
|
require.NoError(t, err)
|
|
require.True(t, strings.HasPrefix(key.Name, "current-"),
|
|
"expected key name to start with current- was %s", key.Name)
|
|
require.Equal(t, keyId, key.ID)
|
|
require.Equal(t, strings.TrimSpace(bundle.PrivateKey), strings.TrimSpace(key.PrivateKey))
|
|
require.Equal(t, bundle.PrivateKeyType, key.PrivateKeyType)
|
|
|
|
// Make sure we kept the old bundle
|
|
_, certBundle, err := getLegacyCertBundle(ctx, s)
|
|
require.NoError(t, err)
|
|
require.Equal(t, bundle, certBundle)
|
|
|
|
// Make sure we setup the default values
|
|
keysConfig, err := sc.getKeysConfig()
|
|
require.NoError(t, err)
|
|
require.Equal(t, &keyConfigEntry{DefaultKeyId: keyId}, keysConfig)
|
|
|
|
issuersConfig, err := sc.getIssuersConfig()
|
|
require.NoError(t, err)
|
|
require.Equal(t, issuerID(""), issuersConfig.DefaultIssuerId)
|
|
|
|
// Make sure if we attempt to re-run the migration nothing happens...
|
|
err = migrateStorage(ctx, b, s)
|
|
require.NoError(t, err)
|
|
logEntry2, err := getLegacyBundleMigrationLog(ctx, s)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, logEntry2)
|
|
|
|
require.Equal(t, logEntry.Created, logEntry2.Created)
|
|
require.Equal(t, logEntry.Hash, logEntry2.Hash)
|
|
|
|
require.False(t, b.useLegacyBundleCaStorage(), "post migration we are still told to use legacy storage")
|
|
}
|
|
|
|
func Test_migrateStorageSimpleBundle(t *testing.T) {
|
|
t.Parallel()
|
|
startTime := time.Now()
|
|
ctx := context.Background()
|
|
b, s := CreateBackendWithStorage(t)
|
|
sc := b.makeStorageContext(ctx, s)
|
|
|
|
// Reset the version the helper above set to 1.
|
|
b.pkiStorageVersion.Store(0)
|
|
require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.")
|
|
|
|
bundle := genCertBundle(t, b, s)
|
|
json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle)
|
|
require.NoError(t, err)
|
|
err = s.Put(ctx, json)
|
|
require.NoError(t, err)
|
|
|
|
request := &logical.InitializationRequest{Storage: s}
|
|
err = b.initialize(ctx, request)
|
|
require.NoError(t, err)
|
|
|
|
issuerIds, err := sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(issuerIds))
|
|
|
|
keyIds, err := sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(keyIds))
|
|
|
|
logEntry, err := getLegacyBundleMigrationLog(ctx, s)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, logEntry)
|
|
require.Equal(t, latestMigrationVersion, logEntry.MigrationVersion)
|
|
require.True(t, len(strings.TrimSpace(logEntry.Hash)) > 0,
|
|
"Hash value (%s) should not have been empty", logEntry.Hash)
|
|
require.True(t, startTime.Before(logEntry.Created),
|
|
"created log entry time (%v) was before our start time(%v)?", logEntry.Created, startTime)
|
|
require.Equal(t, logEntry.CreatedIssuer, issuerIds[0])
|
|
require.Equal(t, logEntry.CreatedKey, keyIds[0])
|
|
|
|
issuerId := issuerIds[0]
|
|
keyId := keyIds[0]
|
|
issuer, err := sc.fetchIssuerById(issuerId)
|
|
require.NoError(t, err)
|
|
require.True(t, strings.HasPrefix(issuer.Name, "current-"),
|
|
"expected issuer name to start with current- was %s", issuer.Name)
|
|
require.Equal(t, certutil.ErrNotAfterBehavior, issuer.LeafNotAfterBehavior)
|
|
|
|
key, err := sc.fetchKeyById(keyId)
|
|
require.NoError(t, err)
|
|
require.True(t, strings.HasPrefix(key.Name, "current-"),
|
|
"expected key name to start with current- was %s", key.Name)
|
|
|
|
require.Equal(t, issuerId, issuer.ID)
|
|
require.Equal(t, bundle.SerialNumber, issuer.SerialNumber)
|
|
require.Equal(t, strings.TrimSpace(bundle.Certificate), strings.TrimSpace(issuer.Certificate))
|
|
require.Equal(t, keyId, issuer.KeyID)
|
|
require.Empty(t, issuer.ManualChain)
|
|
require.Equal(t, []string{bundle.Certificate + "\n"}, issuer.CAChain)
|
|
require.Equal(t, AllIssuerUsages, issuer.Usage)
|
|
require.Equal(t, certutil.ErrNotAfterBehavior, issuer.LeafNotAfterBehavior)
|
|
|
|
require.Equal(t, keyId, key.ID)
|
|
require.Equal(t, strings.TrimSpace(bundle.PrivateKey), strings.TrimSpace(key.PrivateKey))
|
|
require.Equal(t, bundle.PrivateKeyType, key.PrivateKeyType)
|
|
|
|
// Make sure we kept the old bundle
|
|
_, certBundle, err := getLegacyCertBundle(ctx, s)
|
|
require.NoError(t, err)
|
|
require.Equal(t, bundle, certBundle)
|
|
|
|
// Make sure we setup the default values
|
|
keysConfig, err := sc.getKeysConfig()
|
|
require.NoError(t, err)
|
|
require.Equal(t, &keyConfigEntry{DefaultKeyId: keyId}, keysConfig)
|
|
|
|
issuersConfig, err := sc.getIssuersConfig()
|
|
require.NoError(t, err)
|
|
require.Equal(t, issuerId, issuersConfig.DefaultIssuerId)
|
|
|
|
// Make sure if we attempt to re-run the migration nothing happens...
|
|
err = migrateStorage(ctx, b, s)
|
|
require.NoError(t, err)
|
|
logEntry2, err := getLegacyBundleMigrationLog(ctx, s)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, logEntry2)
|
|
|
|
require.Equal(t, logEntry.Created, logEntry2.Created)
|
|
require.Equal(t, logEntry.Hash, logEntry2.Hash)
|
|
|
|
require.False(t, b.useLegacyBundleCaStorage(), "post migration we are still told to use legacy storage")
|
|
|
|
// Make sure we can re-process a migration from scratch for whatever reason
|
|
err = s.Delete(ctx, legacyMigrationBundleLogKey)
|
|
require.NoError(t, err)
|
|
|
|
err = migrateStorage(ctx, b, s)
|
|
require.NoError(t, err)
|
|
|
|
logEntry3, err := getLegacyBundleMigrationLog(ctx, s)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, logEntry3)
|
|
|
|
require.NotEqual(t, logEntry.Created, logEntry3.Created)
|
|
require.Equal(t, logEntry.Hash, logEntry3.Hash)
|
|
}
|
|
|
|
func TestMigration_OnceChainRebuild(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
b, s := CreateBackendWithStorage(t)
|
|
sc := b.makeStorageContext(ctx, s)
|
|
|
|
// Create a legacy CA bundle that we'll migrate to the new layout. We call
|
|
// ToParsedCertBundle just to make sure it works and to populate
|
|
// bundle.SerialNumber for us.
|
|
bundle := &certutil.CertBundle{
|
|
PrivateKeyType: certutil.RSAPrivateKey,
|
|
Certificate: migIntCA,
|
|
IssuingCA: migRootCA,
|
|
CAChain: []string{migRootCA},
|
|
PrivateKey: migIntPrivKey,
|
|
}
|
|
_, err := bundle.ToParsedCertBundle()
|
|
require.NoError(t, err)
|
|
writeLegacyBundle(t, b, s, bundle)
|
|
|
|
// Do an initial migration. Ensure we end up at least on version 2.
|
|
request := &logical.InitializationRequest{Storage: s}
|
|
err = b.initialize(ctx, request)
|
|
require.NoError(t, err)
|
|
|
|
issuerIds, err := sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(issuerIds))
|
|
|
|
keyIds, err := sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(keyIds))
|
|
|
|
logEntry, err := getLegacyBundleMigrationLog(ctx, s)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, logEntry)
|
|
require.GreaterOrEqual(t, logEntry.MigrationVersion, 2)
|
|
require.GreaterOrEqual(t, latestMigrationVersion, 2)
|
|
|
|
// Verify the chain built correctly: current should have a CA chain of
|
|
// length two.
|
|
//
|
|
// Afterwards, we mutate these issuers to only point at themselves and
|
|
// write back out.
|
|
var rootIssuerId issuerID
|
|
var intIssuerId issuerID
|
|
for _, issuerId := range issuerIds {
|
|
issuer, err := sc.fetchIssuerById(issuerId)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, issuer)
|
|
|
|
if strings.HasPrefix(issuer.Name, "current-") {
|
|
require.Equal(t, 2, len(issuer.CAChain))
|
|
require.Equal(t, migIntCA, issuer.CAChain[0])
|
|
require.Equal(t, migRootCA, issuer.CAChain[1])
|
|
intIssuerId = issuerId
|
|
|
|
issuer.CAChain = []string{migIntCA}
|
|
err = sc.writeIssuer(issuer)
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Equal(t, 1, len(issuer.CAChain))
|
|
require.Equal(t, migRootCA, issuer.CAChain[0])
|
|
rootIssuerId = issuerId
|
|
}
|
|
}
|
|
|
|
// Reset our migration version back to one, as if this never
|
|
// happened...
|
|
logEntry.MigrationVersion = 1
|
|
err = setLegacyBundleMigrationLog(ctx, s, logEntry)
|
|
require.NoError(t, err)
|
|
b.pkiStorageVersion.Store(1)
|
|
|
|
// Re-attempt the migration by reinitializing the mount.
|
|
err = b.initialize(ctx, request)
|
|
require.NoError(t, err)
|
|
|
|
newIssuerIds, err := sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(newIssuerIds))
|
|
require.Equal(t, issuerIds, newIssuerIds)
|
|
|
|
newKeyIds, err := sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(newKeyIds))
|
|
require.Equal(t, keyIds, newKeyIds)
|
|
|
|
logEntry, err = getLegacyBundleMigrationLog(ctx, s)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, logEntry)
|
|
require.Equal(t, logEntry.MigrationVersion, latestMigrationVersion)
|
|
|
|
// Ensure the chains are correct on the intermediate. By using the
|
|
// issuerId saved above, this ensures we didn't change any issuerIds,
|
|
// we merely updated the existing issuers.
|
|
intIssuer, err := sc.fetchIssuerById(intIssuerId)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, intIssuer)
|
|
require.Equal(t, 2, len(intIssuer.CAChain))
|
|
require.Equal(t, migIntCA, intIssuer.CAChain[0])
|
|
require.Equal(t, migRootCA, intIssuer.CAChain[1])
|
|
|
|
rootIssuer, err := sc.fetchIssuerById(rootIssuerId)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, rootIssuer)
|
|
require.Equal(t, 1, len(rootIssuer.CAChain))
|
|
require.Equal(t, migRootCA, rootIssuer.CAChain[0])
|
|
}
|
|
|
|
func TestExpectedOpsWork_PreMigration(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
b, s := CreateBackendWithStorage(t)
|
|
// Reset the version the helper above set to 1.
|
|
b.pkiStorageVersion.Store(0)
|
|
require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.")
|
|
|
|
bundle := genCertBundle(t, b, s)
|
|
json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle)
|
|
require.NoError(t, err)
|
|
err = s.Put(ctx, json)
|
|
require.NoError(t, err)
|
|
|
|
// generate role
|
|
resp, err := b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "roles/allow-all",
|
|
Storage: s,
|
|
Data: map[string]interface{}{
|
|
"allow_any_name": "true",
|
|
"no_store": "false",
|
|
},
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error from creating role")
|
|
require.NotNil(t, resp, "got nil response object from creating role")
|
|
|
|
// List roles
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.ListOperation,
|
|
Path: "roles",
|
|
Storage: s,
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error from listing roles")
|
|
require.NotNil(t, resp, "got nil response object from listing roles")
|
|
require.False(t, resp.IsError(), "got error response from listing roles: %#v", resp)
|
|
require.Contains(t, resp.Data["keys"], "allow-all", "failed to list our roles")
|
|
|
|
// Read roles
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Path: "roles/allow-all",
|
|
Storage: s,
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error from reading role")
|
|
require.NotNil(t, resp, "got nil response object from reading role")
|
|
require.False(t, resp.IsError(), "got error response from reading role: %#v", resp)
|
|
require.NotEmpty(t, resp.Data, "data map should not have been empty of reading role")
|
|
|
|
// Issue a cert from our legacy bundle.
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "issue/allow-all",
|
|
Storage: s,
|
|
Data: map[string]interface{}{
|
|
"common_name": "test.com",
|
|
"ttl": "60s",
|
|
},
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error issue on allow-all")
|
|
require.NotNil(t, resp, "got nil response object from issue allow-all")
|
|
require.False(t, resp.IsError(), "got error response from issue on allow-all: %#v", resp)
|
|
serialNum := resp.Data["serial_number"].(string)
|
|
require.NotEmpty(t, serialNum)
|
|
|
|
// Make sure we can list
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.ListOperation,
|
|
Path: "certs",
|
|
Storage: s,
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error listing certs")
|
|
require.NotNil(t, resp, "got nil response object from listing certs")
|
|
require.False(t, resp.IsError(), "got error response from listing certs: %#v", resp)
|
|
require.Contains(t, resp.Data["keys"], serialNum, "failed to list our cert")
|
|
|
|
// Revoke the cert now.
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "revoke",
|
|
Storage: s,
|
|
Data: map[string]interface{}{
|
|
"serial_number": serialNum,
|
|
},
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error revoking cert")
|
|
require.NotNil(t, resp, "got nil response object from revoke cert")
|
|
require.False(t, resp.IsError(), "got error response from revoke cert: %#v", resp)
|
|
|
|
// Check our CRL includes the revoked cert.
|
|
resp = requestCrlFromBackend(t, s, b)
|
|
crl := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte))
|
|
requireSerialNumberInCRL(t, crl, serialNum)
|
|
|
|
// Set CRL config
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config/crl",
|
|
Storage: s,
|
|
Data: map[string]interface{}{
|
|
"expiry": "72h",
|
|
"disable": "false",
|
|
},
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error setting CRL config")
|
|
require.NotNil(t, resp, "got nil response setting CRL config")
|
|
|
|
// Set URL config
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config/urls",
|
|
Storage: s,
|
|
Data: map[string]interface{}{
|
|
"ocsp_servers": []string{"https://localhost:8080"},
|
|
},
|
|
MountPoint: "pki/",
|
|
})
|
|
requireSuccessNonNilResponse(t, resp, err)
|
|
|
|
// Make sure we can fetch the old values...
|
|
for _, path := range []string{"ca/pem", "ca_chain", "cert/" + serialNum, "cert/ca", "cert/crl", "cert/ca_chain", "config/crl", "config/urls"} {
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Path: path,
|
|
Storage: s,
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error reading cert %s", path)
|
|
require.NotNil(t, resp, "got nil response object from reading cert %s", path)
|
|
require.False(t, resp.IsError(), "got error response from reading cert %s: %#v", path, resp)
|
|
}
|
|
|
|
// Sign CSR
|
|
_, csr := generateTestCsr(t, certutil.ECPrivateKey, 224)
|
|
for _, path := range []string{"sign/allow-all", "root/sign-intermediate", "sign-verbatim"} {
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: path,
|
|
Storage: s,
|
|
Data: map[string]interface{}{
|
|
"csr": csr,
|
|
},
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error signing csr from path %s", path)
|
|
require.NotNil(t, resp, "got nil response object from path %s", path)
|
|
require.NotEmpty(t, resp.Data, "data map response was empty from path %s", path)
|
|
}
|
|
|
|
// Sign self-issued
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "root/sign-self-issued",
|
|
Storage: s,
|
|
Data: map[string]interface{}{
|
|
"certificate": csr,
|
|
},
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error signing csr from path root/sign-self-issued")
|
|
require.NotNil(t, resp, "got nil response object from path root/sign-self-issued")
|
|
require.NotEmpty(t, resp.Data, "data map response was empty from path root/sign-self-issued")
|
|
|
|
// Delete Role
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.DeleteOperation,
|
|
Path: "roles/allow-all",
|
|
Storage: s,
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error deleting role")
|
|
require.Nil(t, resp, "got non-nil response object from deleting role")
|
|
|
|
// Delete Root
|
|
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: logical.DeleteOperation,
|
|
Path: "root",
|
|
Storage: s,
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error deleting root")
|
|
require.NotNil(t, resp, "got nil response object from deleting root")
|
|
require.NotEmpty(t, resp.Warnings, "expected warnings set on delete root")
|
|
|
|
///////////////////////////////
|
|
// Legacy calls we expect to fail when in migration mode
|
|
///////////////////////////////
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "config/ca")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "intermediate/generate/internal")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "intermediate/set-signed")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "root/generate/internal")
|
|
|
|
///////////////////////////////
|
|
// New apis should be unavailable
|
|
///////////////////////////////
|
|
requireFailInMigration(t, b, s, logical.ListOperation, "issuers")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "issuers/generate/root/internal")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "issuers/generate/intermediate/internal")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "issuers/import/cert")
|
|
requireFailInMigration(t, b, s, logical.ReadOperation, "issuer/default/json")
|
|
requireFailInMigration(t, b, s, logical.ReadOperation, "issuer/default/crl/pem")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/test-role")
|
|
|
|
// The following calls work as they are shared handlers with existing paths.
|
|
// requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/issue/test-role")
|
|
// requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/sign/test-role")
|
|
// requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/sign-verbatim")
|
|
// requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/sign-self-issued")
|
|
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "root/replace")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "root/rotate/internal")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "intermediate/cross-sign")
|
|
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "config/issuers")
|
|
requireFailInMigration(t, b, s, logical.ReadOperation, "config/issuers")
|
|
|
|
requireFailInMigration(t, b, s, logical.ListOperation, "keys")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "keys/generate/internal")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "keys/import")
|
|
requireFailInMigration(t, b, s, logical.ReadOperation, "key/default")
|
|
requireFailInMigration(t, b, s, logical.UpdateOperation, "config/keys")
|
|
requireFailInMigration(t, b, s, logical.ReadOperation, "config/keys")
|
|
}
|
|
|
|
func TestBackupBundle(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
b, s := CreateBackendWithStorage(t)
|
|
sc := b.makeStorageContext(ctx, s)
|
|
|
|
// Reset the version the helper above set to 1.
|
|
b.pkiStorageVersion.Store(0)
|
|
require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.")
|
|
|
|
// Create an empty request and tidy configuration for us.
|
|
req := &logical.Request{
|
|
Storage: s,
|
|
MountPoint: "pki/",
|
|
}
|
|
cfg := &tidyConfig{
|
|
BackupBundle: true,
|
|
IssuerSafetyBuffer: 120 * time.Second,
|
|
}
|
|
|
|
// Migration should do nothing if we're on an empty mount.
|
|
err := b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
|
|
require.NoError(t, err)
|
|
requireFileNotExists(t, sc, legacyCertBundlePath)
|
|
requireFileNotExists(t, sc, legacyCertBundleBackupPath)
|
|
issuerIds, err := sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.Empty(t, issuerIds)
|
|
keyIds, err := sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.Empty(t, keyIds)
|
|
|
|
// Create a legacy CA bundle and write it out.
|
|
bundle := genCertBundle(t, b, s)
|
|
json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle)
|
|
require.NoError(t, err)
|
|
err = s.Put(ctx, json)
|
|
require.NoError(t, err)
|
|
legacyContents := requireFileExists(t, sc, legacyCertBundlePath, nil)
|
|
|
|
// Doing another tidy should maintain the status quo since we've
|
|
// still not done our migration.
|
|
err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
|
|
require.NoError(t, err)
|
|
requireFileExists(t, sc, legacyCertBundlePath, legacyContents)
|
|
requireFileNotExists(t, sc, legacyCertBundleBackupPath)
|
|
issuerIds, err = sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.Empty(t, issuerIds)
|
|
keyIds, err = sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.Empty(t, keyIds)
|
|
|
|
// Do a migration; this should provision an issuer and key.
|
|
initReq := &logical.InitializationRequest{Storage: s}
|
|
err = b.initialize(ctx, initReq)
|
|
require.NoError(t, err)
|
|
requireFileExists(t, sc, legacyCertBundlePath, legacyContents)
|
|
issuerIds, err = sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, issuerIds)
|
|
keyIds, err = sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, keyIds)
|
|
|
|
// Doing another tidy should maintain the status quo since we've
|
|
// done our migration too recently relative to the safety buffer.
|
|
err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
|
|
require.NoError(t, err)
|
|
requireFileExists(t, sc, legacyCertBundlePath, legacyContents)
|
|
requireFileNotExists(t, sc, legacyCertBundleBackupPath)
|
|
issuerIds, err = sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, issuerIds)
|
|
keyIds, err = sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, keyIds)
|
|
|
|
// Shortening our buffer should ensure the migration occurs, removing
|
|
// the legacy bundle but creating the backup one.
|
|
time.Sleep(2 * time.Second)
|
|
cfg.IssuerSafetyBuffer = 1 * time.Second
|
|
err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
|
|
require.NoError(t, err)
|
|
requireFileNotExists(t, sc, legacyCertBundlePath)
|
|
requireFileExists(t, sc, legacyCertBundleBackupPath, legacyContents)
|
|
issuerIds, err = sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, issuerIds)
|
|
keyIds, err = sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, keyIds)
|
|
|
|
// A new initialization should do nothing.
|
|
err = b.initialize(ctx, initReq)
|
|
require.NoError(t, err)
|
|
requireFileNotExists(t, sc, legacyCertBundlePath)
|
|
requireFileExists(t, sc, legacyCertBundleBackupPath, legacyContents)
|
|
issuerIds, err = sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, issuerIds)
|
|
require.Equal(t, len(issuerIds), 1)
|
|
keyIds, err = sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, keyIds)
|
|
require.Equal(t, len(keyIds), 1)
|
|
|
|
// Restoring the legacy bundles with new issuers should redo the
|
|
// migration.
|
|
newBundle := genCertBundle(t, b, s)
|
|
json, err = logical.StorageEntryJSON(legacyCertBundlePath, newBundle)
|
|
require.NoError(t, err)
|
|
err = s.Put(ctx, json)
|
|
require.NoError(t, err)
|
|
newLegacyContents := requireFileExists(t, sc, legacyCertBundlePath, nil)
|
|
|
|
// -> reinit
|
|
err = b.initialize(ctx, initReq)
|
|
require.NoError(t, err)
|
|
requireFileExists(t, sc, legacyCertBundlePath, newLegacyContents)
|
|
requireFileExists(t, sc, legacyCertBundleBackupPath, legacyContents)
|
|
issuerIds, err = sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, issuerIds)
|
|
require.Equal(t, len(issuerIds), 2)
|
|
keyIds, err = sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, keyIds)
|
|
require.Equal(t, len(keyIds), 2)
|
|
|
|
// -> when we tidy again, we'll overwrite the old backup with the new
|
|
// one.
|
|
time.Sleep(2 * time.Second)
|
|
err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
|
|
require.NoError(t, err)
|
|
requireFileNotExists(t, sc, legacyCertBundlePath)
|
|
requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents)
|
|
issuerIds, err = sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, issuerIds)
|
|
keyIds, err = sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, keyIds)
|
|
|
|
// Finally, restoring the legacy bundle and re-migrating should redo
|
|
// the migration.
|
|
err = s.Put(ctx, json)
|
|
require.NoError(t, err)
|
|
requireFileExists(t, sc, legacyCertBundlePath, newLegacyContents)
|
|
requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents)
|
|
|
|
// -> overwrite the version and re-migrate
|
|
logEntry, err := getLegacyBundleMigrationLog(ctx, s)
|
|
require.NoError(t, err)
|
|
logEntry.MigrationVersion = 0
|
|
err = setLegacyBundleMigrationLog(ctx, s, logEntry)
|
|
require.NoError(t, err)
|
|
err = b.initialize(ctx, initReq)
|
|
require.NoError(t, err)
|
|
requireFileExists(t, sc, legacyCertBundlePath, newLegacyContents)
|
|
requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents)
|
|
issuerIds, err = sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, issuerIds)
|
|
require.Equal(t, len(issuerIds), 2)
|
|
keyIds, err = sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, keyIds)
|
|
require.Equal(t, len(keyIds), 2)
|
|
|
|
// -> Re-tidy should remove the legacy one.
|
|
time.Sleep(2 * time.Second)
|
|
err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
|
|
require.NoError(t, err)
|
|
requireFileNotExists(t, sc, legacyCertBundlePath)
|
|
requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents)
|
|
issuerIds, err = sc.listIssuers()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, issuerIds)
|
|
keyIds, err = sc.listKeys()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, keyIds)
|
|
}
|
|
|
|
// requireFailInMigration validate that we fail the operation with the appropriate error message to the end-user
|
|
func requireFailInMigration(t *testing.T, b *backend, s logical.Storage, operation logical.Operation, path string) {
|
|
resp, err := b.HandleRequest(context.Background(), &logical.Request{
|
|
Operation: operation,
|
|
Path: path,
|
|
Storage: s,
|
|
MountPoint: "pki/",
|
|
})
|
|
require.NoError(t, err, "error from op:%s path:%s", operation, path)
|
|
require.NotNil(t, resp, "got nil response from op:%s path:%s", operation, path)
|
|
require.True(t, resp.IsError(), "error flag was not set from op:%s path:%s resp: %#v", operation, path, resp)
|
|
require.Contains(t, resp.Error().Error(), "migration has completed",
|
|
"error message did not contain migration test for op:%s path:%s resp: %#v", operation, path, resp)
|
|
}
|
|
|
|
func requireFileNotExists(t *testing.T, sc *storageContext, path string) {
|
|
t.Helper()
|
|
|
|
entry, err := sc.Storage.Get(sc.Context, path)
|
|
require.NoError(t, err)
|
|
if entry != nil {
|
|
require.Empty(t, entry.Value)
|
|
} else {
|
|
require.Empty(t, entry)
|
|
}
|
|
}
|
|
|
|
func requireFileExists(t *testing.T, sc *storageContext, path string, contents []byte) []byte {
|
|
t.Helper()
|
|
|
|
entry, err := sc.Storage.Get(sc.Context, path)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, entry)
|
|
require.NotEmpty(t, entry.Value)
|
|
if contents != nil {
|
|
require.Equal(t, entry.Value, contents)
|
|
}
|
|
return entry.Value
|
|
}
|
|
|
|
// Keys to simulate an intermediate CA mount with also-imported root (parent).
|
|
const (
|
|
migIntPrivKey = `-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEpAIBAAKCAQEAqu88Jcct/EyT8gDF+jdWuAwFplvanQ7KXAO5at58G6Y39UUz
|
|
fwnMS3P3VRBUoV5BDX+13wI2ldskbTKITsl6IXBPXUz0sKrdEKzXRVY4D6P2JR7W
|
|
YO1IUytfTgR+3F4sotFNQB++3ivT66AYLW7lOkoa+5lxsPM/oJ82DOlD2uGtDVTU
|
|
gQy1zugMBgPDlj+8tB562J9MTIdcKe9JpYrN0eO+aHzhbfvaSpScU4aZBgkS0kDv
|
|
8G4FxVfrBSDeD/JjCWaC48rLdgei1YrY0NFuw/8p/nPfA9vf2AtHMsWZRSwukQfq
|
|
I5HhQu+0OHQy3NWqXaBPzJNu3HnpKLykPHW7sQIDAQABAoIBAHNJy/2G66MhWx98
|
|
Ggt7S4fyw9TCWx5XHXEWKfbEfFyBrXhF5kemqh2x5319+DamRaX/HwF8kqhcF6N2
|
|
06ygAzmOcFjzUI3fkB5xFPh1AHa8FYZP2DOjloZR2IPcUFv9QInINRwszSU31kUz
|
|
w1rRUtYPqUdM5Pt99Mo219O5eMSlGtPKXm09uDAR8ZPuUx4jwGw90pSgeRB1Bg7X
|
|
Dt3YXx3X+OOs3Hbir1VDLSqCuy825l6Kn79h3eB8LAi+FUwCBvnTqyOEWyH2XjgP
|
|
z+tbz7lwnhGeKtxUl6Jb3m3SHtXpylot/4fwPisRV/9vaEDhVjKTmySH1WM+TRNR
|
|
CQLCJekCgYEA3b67DBhAYsFFdUd/4xh4QhHBanOcepV1CwaRln+UUjw1618ZEsTS
|
|
DKb9IS72C+ukUusGhQqxjFJlhOdXeMXpEbnEUY3PlREevWwm3bVAxtoAVRcmkQyK
|
|
PM4Oj9ODi2z8Cds0NvEXdX69uVutcbvm/JRZr/dsERWcLsfwdV/QqYcCgYEAxVce
|
|
d4ylsqORLm0/gcLnEyB9zhEPwmiJe1Yj5sH7LhGZ6JtLCqbOJO4jXmIzCrkbGyuf
|
|
BA/U7klc6jSprkBMgYhgOIuaULuFJvtKzJUzoATGFqX4r8WJm2ZycXgooAwZq6SZ
|
|
ySXOuQe9V7hlpI0fJfNhw+/HIjivL1jrnjBoXwcCgYEAtTv6LLx1g0Frv5scj0Ok
|
|
pntUlei/8ADPlJ9dxp+nXj8P4rvrBkgPVX/2S3TSbJO/znWA8qP20TVW+/UIrRE0
|
|
mOQ37F/3VWKUuUT3zyUhOGVc+C7fupWBNolDpZG+ZepBZNzgJDeQcNuRvTmM3PQy
|
|
qiWl2AhlLuF2sVWA1q3lIWkCgYEAnuHWgNA3dE1nDWceE351hxvIzklEU/TQhAHF
|
|
o/uYHO5E6VdmoqvMG0W0KkCL8d046rZDMAUDHdrpOROvbcENF9lSBxS26LshqFH4
|
|
ViDmULanOgLk57f2Y6ynBZ6Frt4vKNe8jYuoFacale67vzFz251JoHSD8pSKz2cb
|
|
ROCal68CgYA51hKqvki4r5rmS7W/Yvc3x3Wc0wpDEHTgLMoH+EV7AffJ8dy0/+po
|
|
AHK0nnRU63++1JmhQczBR0yTI6PUyeegEBk/d5CgFlY7UJQMTFPsMsiuM0Xw5nAv
|
|
KMPykK01D28UAkUxhwF7CqFrwwEv9GislgjewbdF5Za176+EuMEwIw==
|
|
-----END RSA PRIVATE KEY-----
|
|
`
|
|
migIntCA = `-----BEGIN CERTIFICATE-----
|
|
MIIDHTCCAgWgAwIBAgIUfxlNBmrI7jsgH2Sdle1nVTqn5YQwDQYJKoZIhvcNAQEL
|
|
BQAwEjEQMA4GA1UEAxMHUm9vdCBYMTAeFw0yMjExMDIxMjI2MjhaFw0yMjEyMDQx
|
|
MjI2NThaMBoxGDAWBgNVBAMTD0ludGVybWVkaWF0ZSBSMTCCASIwDQYJKoZIhvcN
|
|
AQEBBQADggEPADCCAQoCggEBAKrvPCXHLfxMk/IAxfo3VrgMBaZb2p0OylwDuWre
|
|
fBumN/VFM38JzEtz91UQVKFeQQ1/td8CNpXbJG0yiE7JeiFwT11M9LCq3RCs10VW
|
|
OA+j9iUe1mDtSFMrX04EftxeLKLRTUAfvt4r0+ugGC1u5TpKGvuZcbDzP6CfNgzp
|
|
Q9rhrQ1U1IEMtc7oDAYDw5Y/vLQeetifTEyHXCnvSaWKzdHjvmh84W372kqUnFOG
|
|
mQYJEtJA7/BuBcVX6wUg3g/yYwlmguPKy3YHotWK2NDRbsP/Kf5z3wPb39gLRzLF
|
|
mUUsLpEH6iOR4ULvtDh0MtzVql2gT8yTbtx56Si8pDx1u7ECAwEAAaNjMGEwDgYD
|
|
VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFusWj3piAiY
|
|
CR7tszR6uNYSMLe2MB8GA1UdIwQYMBaAFMNRNkLozstIhNhXCefi+WnaQApbMA0G
|
|
CSqGSIb3DQEBCwUAA4IBAQCmH852E/pDGBhf2VI1JAPZy9VYaRkKoqn4+5R1Gnoq
|
|
b90zhdCGueIm/usC1wAa0OOn7+xdQXFNfeI8UUB9w10q0QnM/A/G2v8UkdlLPPQP
|
|
zPjIYLalOOIOHf8hU2O5lwj0IA4JwjwDQ4xj69eX/N+x2LEI7SHyVVUZWAx0Y67a
|
|
QdyubpIJZlW/PI7kMwGyTx3tdkZxk1nTNtf/0nKvNuXKKcVzBCEMfvXyx4LFEM+U
|
|
nc2vdWN7PAoXcjUbxD3ZNGinr7mSBpQg82+nur/8yuSwu6iHomnfGxjUsEHic2GC
|
|
ja9siTbR+ONvVb4xUjugN/XmMSSaZnxig2vM9xcV8OMG
|
|
-----END CERTIFICATE-----
|
|
`
|
|
migRootCA = `-----BEGIN CERTIFICATE-----
|
|
MIIDFTCCAf2gAwIBAgIURDTnXp8u78jWMe770Jj6Ac1paxkwDQYJKoZIhvcNAQEL
|
|
BQAwEjEQMA4GA1UEAxMHUm9vdCBYMTAeFw0yMjExMDIxMjI0NTVaFw0yMjEyMDQx
|
|
MjI1MjRaMBIxEDAOBgNVBAMTB1Jvb3QgWDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
|
DwAwggEKAoIBAQC/+dh/o1qKTOua/OkHRMIvHiyBxjjoqrLqFSBYhjYKs+alA0qS
|
|
lLVzNqIKU8jm3fT73orx7yk/6acWaEYv/6owMaUn51xwS3gQhTHdFR/fLJwXnu2O
|
|
PZNqAs6tjAM3Q08aqR0qfxnjDvcgO7TOWSyOvVT2cTRK+uKYzxJEY52BDMUbp+iC
|
|
WJdXca9UwKRzi2wFqGliDycYsBBt/tr8tHSbTSZ5Qx6UpFrKpjZn+sT5KhKUlsdd
|
|
BYFmRegc0wXq4/kRjum0oEUigUMlHADIEhRasyXPEKa19sGP8nAZfo/hNOusGhj7
|
|
z7UPA0Cbe2uclpYPxsKgvcqQmgKugqKLL305AgMBAAGjYzBhMA4GA1UdDwEB/wQE
|
|
AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTDUTZC6M7LSITYVwnn4vlp
|
|
2kAKWzAfBgNVHSMEGDAWgBTDUTZC6M7LSITYVwnn4vlp2kAKWzANBgkqhkiG9w0B
|
|
AQsFAAOCAQEAu7qdM1Li6V6iDCPpLg5zZReRtcxhUdwb5Xn4sDa8GJCy35f1voew
|
|
n0TQgM3Uph5x/djCR/Sj91MyAJ1/Q1PQQTyKGyUjSHvkcOBg628IAnLthn8Ua1fL
|
|
oQC/F/mlT1Yv+/W8eNPtD453/P0z8E0xMT5K3kpEDW/6K9RdHZlDJMW/z3UJ+4LN
|
|
6ONjIBmgffmLz9sVMpgCFyL7+w3W01bGP7w5AfKj2duoVG/Ekf2yUwmm6r9NgTQ1
|
|
oke0ShbZuMocwO8anq7k0R42FoluH3ipv9Qzzhsy+KdK5/fW5oqy1tKFaZsc67Q6
|
|
0UmD9DiDpCtn2Wod3nwxn0zW5HvDAWuDwg==
|
|
-----END CERTIFICATE-----
|
|
`
|
|
)
|