44c3b736bf
* Allow tidy to backup legacy CA bundles With the new tidy_move_legacy_ca_bundle option, we'll use tidy to move the legacy CA bundle from /config/ca_bundle to /config/ca_bundle.bak. This does two things: 1. Removes ca_bundle from the hot-path of initialization after initial migration has completed. Because this entry is seal wrapped, this may result in performance improvements. 2. Allows recovery of this value in the event of some other failure with migration. Notably, this cannot occur during migration in the unlikely (and largely unsupported) case that the operator immediately downgrades to Vault <1.11.x. Thus, we reuse issuer_safety_buffer; while potentially long, tidy can always be run manually with a shorter buffer (and only this flag) to manually move the bundle if necessary. In the event of needing to recover or undo this operation, it is sufficient to use sys/raw to read the backed up value and subsequently write it to its old path (/config/ca_bundle). The new entry remains seal wrapped, but otherwise isn't used within the code and so has better performance characteristics. Performing a fat deletion (DELETE /root) will again remove the backup like the old legacy bundle, preserving its wipe characteristics. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add changelog Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add documentation about new tidy parameter Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add tests for migration scenarios Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Clean up time comparisons Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
884 lines
33 KiB
Go
884 lines
33 KiB
Go
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) {
|
|
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 {
|
|
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-----
|
|
`
|
|
)
|