2016-04-04 14:44:22 +00:00
|
|
|
package vault
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2018-01-19 06:44:44 +00:00
|
|
|
"context"
|
2018-05-20 21:49:37 +00:00
|
|
|
"crypto/subtle"
|
2016-04-04 14:44:22 +00:00
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2018-02-23 22:18:48 +00:00
|
|
|
"sync/atomic"
|
2016-04-04 14:44:22 +00:00
|
|
|
|
2018-04-05 15:49:21 +00:00
|
|
|
"github.com/hashicorp/errwrap"
|
2019-04-12 21:54:35 +00:00
|
|
|
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
|
|
|
"github.com/hashicorp/vault/sdk/physical"
|
2018-09-18 03:03:00 +00:00
|
|
|
"github.com/hashicorp/vault/vault/seal"
|
2016-04-04 14:44:22 +00:00
|
|
|
|
2016-08-11 12:31:43 +00:00
|
|
|
"github.com/keybase/go-crypto/openpgp"
|
|
|
|
"github.com/keybase/go-crypto/openpgp/packet"
|
2016-04-04 14:44:22 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// barrierSealConfigPath is the path used to store our seal configuration.
|
|
|
|
// This value is stored in plaintext, since we must be able to read it even
|
|
|
|
// with the Vault sealed. This is required so that we know how many secret
|
|
|
|
// parts must be used to reconstruct the master key.
|
|
|
|
barrierSealConfigPath = "core/seal-config"
|
|
|
|
|
|
|
|
// recoverySealConfigPath is the path to the recovery key seal
|
2017-10-23 18:59:37 +00:00
|
|
|
// configuration. It lives inside the barrier.
|
|
|
|
// DEPRECATED: Use recoverySealConfigPlaintextPath instead.
|
2016-04-04 14:44:22 +00:00
|
|
|
recoverySealConfigPath = "core/recovery-seal-config"
|
|
|
|
|
2017-10-23 18:59:37 +00:00
|
|
|
// recoverySealConfigPlaintextPath is the path to the recovery key seal
|
|
|
|
// configuration. This is stored in plaintext so that we can perform
|
|
|
|
// auto-unseal.
|
|
|
|
recoverySealConfigPlaintextPath = "core/recovery-config"
|
|
|
|
|
2016-04-04 14:44:22 +00:00
|
|
|
// recoveryKeyPath is the path to the recovery key
|
|
|
|
recoveryKeyPath = "core/recovery-key"
|
2017-10-23 18:59:37 +00:00
|
|
|
|
2018-11-05 19:06:39 +00:00
|
|
|
// StoredBarrierKeysPath is the path used for storing HSM-encrypted unseal keys
|
|
|
|
StoredBarrierKeysPath = "core/hsm/barrier-unseal-keys"
|
2017-10-23 18:59:37 +00:00
|
|
|
|
|
|
|
// hsmStoredIVPath is the path to the initialization vector for stored keys
|
|
|
|
hsmStoredIVPath = "core/hsm/iv"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
RecoveryTypeUnsupported = "unsupported"
|
|
|
|
RecoveryTypeShamir = "shamir"
|
2016-04-04 14:44:22 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Seal interface {
|
|
|
|
SetCore(*Core)
|
2018-01-19 06:44:44 +00:00
|
|
|
Init(context.Context) error
|
|
|
|
Finalize(context.Context) error
|
|
|
|
|
2018-01-19 08:44:06 +00:00
|
|
|
StoredKeysSupported() bool
|
2018-01-19 06:44:44 +00:00
|
|
|
SetStoredKeys(context.Context, [][]byte) error
|
|
|
|
GetStoredKeys(context.Context) ([][]byte, error)
|
|
|
|
|
2018-01-19 08:17:36 +00:00
|
|
|
BarrierType() string
|
2018-01-19 06:44:44 +00:00
|
|
|
BarrierConfig(context.Context) (*SealConfig, error)
|
|
|
|
SetBarrierConfig(context.Context, *SealConfig) error
|
2018-10-23 06:34:02 +00:00
|
|
|
SetCachedBarrierConfig(*SealConfig)
|
2018-01-19 06:44:44 +00:00
|
|
|
|
2018-01-19 08:44:06 +00:00
|
|
|
RecoveryKeySupported() bool
|
2018-01-19 08:17:36 +00:00
|
|
|
RecoveryType() string
|
2018-01-19 06:44:44 +00:00
|
|
|
RecoveryConfig(context.Context) (*SealConfig, error)
|
2019-02-01 19:29:55 +00:00
|
|
|
RecoveryKey(context.Context) ([]byte, error)
|
2018-01-19 06:44:44 +00:00
|
|
|
SetRecoveryConfig(context.Context, *SealConfig) error
|
2018-10-23 06:34:02 +00:00
|
|
|
SetCachedRecoveryConfig(*SealConfig)
|
2018-01-19 06:44:44 +00:00
|
|
|
SetRecoveryKey(context.Context, []byte) error
|
|
|
|
VerifyRecoveryKey(context.Context, []byte) error
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
type defaultSeal struct {
|
2018-05-20 21:49:37 +00:00
|
|
|
config atomic.Value
|
|
|
|
core *Core
|
|
|
|
PretendToAllowStoredShares bool
|
|
|
|
PretendToAllowRecoveryKeys bool
|
|
|
|
PretendRecoveryKey []byte
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func NewDefaultSeal() Seal {
|
|
|
|
ret := &defaultSeal{}
|
|
|
|
ret.config.Store((*SealConfig)(nil))
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *defaultSeal) checkCore() error {
|
2016-04-04 14:44:22 +00:00
|
|
|
if d.core == nil {
|
|
|
|
return fmt.Errorf("seal does not have a core set")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) SetCore(core *Core) {
|
2016-04-04 14:44:22 +00:00
|
|
|
d.core = core
|
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) Init(ctx context.Context) error {
|
2016-04-04 14:44:22 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) Finalize(ctx context.Context) error {
|
2016-04-14 20:36:20 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) BarrierType() string {
|
2018-09-18 03:03:00 +00:00
|
|
|
return seal.Shamir
|
2016-04-15 22:16:48 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) StoredKeysSupported() bool {
|
2018-05-20 21:49:37 +00:00
|
|
|
return d.PretendToAllowStoredShares
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) RecoveryKeySupported() bool {
|
2018-05-20 21:49:37 +00:00
|
|
|
return d.PretendToAllowRecoveryKeys
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) SetStoredKeys(ctx context.Context, keys [][]byte) error {
|
2018-04-05 15:49:21 +00:00
|
|
|
return fmt.Errorf("stored keys are not supported")
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) GetStoredKeys(ctx context.Context) ([][]byte, error) {
|
2018-04-05 15:49:21 +00:00
|
|
|
return nil, fmt.Errorf("stored keys are not supported")
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) BarrierConfig(ctx context.Context) (*SealConfig, error) {
|
|
|
|
if d.config.Load().(*SealConfig) != nil {
|
|
|
|
return d.config.Load().(*SealConfig).Clone(), nil
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := d.checkCore(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch the core configuration
|
2018-01-19 06:44:44 +00:00
|
|
|
pe, err := d.core.physical.Get(ctx, barrierSealConfigPath)
|
2016-04-04 14:44:22 +00:00
|
|
|
if err != nil {
|
2018-04-03 00:46:59 +00:00
|
|
|
d.core.logger.Error("failed to read seal configuration", "error", err)
|
2018-04-05 15:49:21 +00:00
|
|
|
return nil, errwrap.Wrapf("failed to check seal configuration: {{err}}", err)
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the seal configuration is missing, we are not initialized
|
|
|
|
if pe == nil {
|
2018-04-03 00:46:59 +00:00
|
|
|
d.core.logger.Info("seal configuration missing, not initialized")
|
2016-04-04 14:44:22 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var conf SealConfig
|
|
|
|
|
|
|
|
// Decode the barrier entry
|
2016-07-06 16:25:40 +00:00
|
|
|
if err := jsonutil.DecodeJSON(pe.Value, &conf); err != nil {
|
2018-04-03 00:46:59 +00:00
|
|
|
d.core.logger.Error("failed to decode seal configuration", "error", err)
|
2018-04-05 15:49:21 +00:00
|
|
|
return nil, errwrap.Wrapf("failed to decode seal configuration: {{err}}", err)
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
2016-04-15 22:16:48 +00:00
|
|
|
switch conf.Type {
|
|
|
|
// This case should not be valid for other types as only this is the default
|
|
|
|
case "":
|
2018-01-19 08:17:36 +00:00
|
|
|
conf.Type = d.BarrierType()
|
|
|
|
case d.BarrierType():
|
2016-04-15 22:16:48 +00:00
|
|
|
default:
|
2018-10-23 06:34:02 +00:00
|
|
|
d.core.logger.Error("barrier seal type does not match expected type", "barrier_seal_type", conf.Type, "loaded_seal_type", d.BarrierType())
|
|
|
|
return nil, fmt.Errorf("barrier seal type of %q does not match expected type of %q", conf.Type, d.BarrierType())
|
2016-04-15 22:16:48 +00:00
|
|
|
}
|
|
|
|
|
2016-04-04 14:44:22 +00:00
|
|
|
// Check for a valid seal configuration
|
|
|
|
if err := conf.Validate(); err != nil {
|
2018-04-03 00:46:59 +00:00
|
|
|
d.core.logger.Error("invalid seal configuration", "error", err)
|
2018-04-05 15:49:21 +00:00
|
|
|
return nil, errwrap.Wrapf("seal validation failed: {{err}}", err)
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
d.config.Store(&conf)
|
|
|
|
return conf.Clone(), nil
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) SetBarrierConfig(ctx context.Context, config *SealConfig) error {
|
2016-04-04 14:44:22 +00:00
|
|
|
if err := d.checkCore(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-01-17 20:43:10 +00:00
|
|
|
// Provide a way to wipe out the cached value (also prevents actually
|
|
|
|
// saving a nil config)
|
|
|
|
if config == nil {
|
2018-02-23 22:18:48 +00:00
|
|
|
d.config.Store((*SealConfig)(nil))
|
2017-01-17 20:43:10 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-01-19 08:17:36 +00:00
|
|
|
config.Type = d.BarrierType()
|
2016-04-15 22:16:48 +00:00
|
|
|
|
2016-04-04 14:44:22 +00:00
|
|
|
// Encode the seal configuration
|
|
|
|
buf, err := json.Marshal(config)
|
|
|
|
if err != nil {
|
2018-04-05 15:49:21 +00:00
|
|
|
return errwrap.Wrapf("failed to encode seal configuration: {{err}}", err)
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Store the seal configuration
|
|
|
|
pe := &physical.Entry{
|
|
|
|
Key: barrierSealConfigPath,
|
|
|
|
Value: buf,
|
|
|
|
}
|
|
|
|
|
2018-01-19 06:44:44 +00:00
|
|
|
if err := d.core.physical.Put(ctx, pe); err != nil {
|
2018-04-03 00:46:59 +00:00
|
|
|
d.core.logger.Error("failed to write seal configuration", "error", err)
|
2018-04-05 15:49:21 +00:00
|
|
|
return errwrap.Wrapf("failed to write seal configuration: {{err}}", err)
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
d.config.Store(config.Clone())
|
2016-04-04 14:44:22 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-10-23 06:34:02 +00:00
|
|
|
func (d *defaultSeal) SetCachedBarrierConfig(config *SealConfig) {
|
|
|
|
d.config.Store(config)
|
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) RecoveryType() string {
|
2018-05-20 21:49:37 +00:00
|
|
|
if d.PretendToAllowRecoveryKeys {
|
2018-05-20 04:02:45 +00:00
|
|
|
return RecoveryTypeShamir
|
|
|
|
}
|
2017-10-23 18:59:37 +00:00
|
|
|
return RecoveryTypeUnsupported
|
2016-04-15 22:16:48 +00:00
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) RecoveryConfig(ctx context.Context) (*SealConfig, error) {
|
2018-05-20 21:49:37 +00:00
|
|
|
if d.PretendToAllowRecoveryKeys {
|
|
|
|
return &SealConfig{
|
|
|
|
SecretShares: 5,
|
|
|
|
SecretThreshold: 3,
|
|
|
|
}, nil
|
2018-05-20 04:02:45 +00:00
|
|
|
}
|
2016-04-04 14:44:22 +00:00
|
|
|
return nil, fmt.Errorf("recovery not supported")
|
|
|
|
}
|
|
|
|
|
2019-02-01 19:29:55 +00:00
|
|
|
func (d *defaultSeal) RecoveryKey(ctx context.Context) ([]byte, error) {
|
|
|
|
if d.PretendToAllowRecoveryKeys {
|
|
|
|
return d.PretendRecoveryKey, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("recovery not supported")
|
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) SetRecoveryConfig(ctx context.Context, config *SealConfig) error {
|
2018-05-20 21:49:37 +00:00
|
|
|
if d.PretendToAllowRecoveryKeys {
|
|
|
|
return nil
|
|
|
|
}
|
2016-04-04 14:44:22 +00:00
|
|
|
return fmt.Errorf("recovery not supported")
|
|
|
|
}
|
|
|
|
|
2018-10-23 06:34:02 +00:00
|
|
|
func (d *defaultSeal) SetCachedRecoveryConfig(config *SealConfig) {
|
|
|
|
}
|
|
|
|
|
2018-05-20 21:49:37 +00:00
|
|
|
func (d *defaultSeal) VerifyRecoveryKey(ctx context.Context, key []byte) error {
|
|
|
|
if d.PretendToAllowRecoveryKeys {
|
|
|
|
if subtle.ConstantTimeCompare(key, d.PretendRecoveryKey) == 1 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return fmt.Errorf("mismatch")
|
|
|
|
}
|
2016-04-04 14:44:22 +00:00
|
|
|
return fmt.Errorf("recovery not supported")
|
|
|
|
}
|
|
|
|
|
2018-02-23 22:18:48 +00:00
|
|
|
func (d *defaultSeal) SetRecoveryKey(ctx context.Context, key []byte) error {
|
2018-05-20 21:49:37 +00:00
|
|
|
if d.PretendToAllowRecoveryKeys {
|
|
|
|
d.PretendRecoveryKey = key
|
|
|
|
return nil
|
|
|
|
}
|
2016-04-04 14:44:22 +00:00
|
|
|
return fmt.Errorf("recovery not supported")
|
|
|
|
}
|
|
|
|
|
|
|
|
// SealConfig is used to describe the seal configuration
|
|
|
|
type SealConfig struct {
|
2016-04-15 22:16:48 +00:00
|
|
|
// The type, for sanity checking
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
2016-04-04 14:44:22 +00:00
|
|
|
// SecretShares is the number of shares the secret is split into. This is
|
|
|
|
// the N value of Shamir.
|
|
|
|
SecretShares int `json:"secret_shares"`
|
|
|
|
|
|
|
|
// SecretThreshold is the number of parts required to open the vault. This
|
|
|
|
// is the T value of Shamir.
|
|
|
|
SecretThreshold int `json:"secret_threshold"`
|
|
|
|
|
|
|
|
// PGPKeys is the array of public PGP keys used, if requested, to encrypt
|
|
|
|
// the output unseal tokens. If provided, it sets the value of
|
|
|
|
// SecretShares. Ordering is important.
|
|
|
|
PGPKeys []string `json:"pgp_keys"`
|
|
|
|
|
|
|
|
// Nonce is a nonce generated by Vault used to ensure that when unseal keys
|
|
|
|
// are submitted for a rekey operation, the rekey operation itself is the
|
|
|
|
// one intended. This prevents hijacking of the rekey operation, since it
|
|
|
|
// is unauthenticated.
|
|
|
|
Nonce string `json:"nonce"`
|
|
|
|
|
|
|
|
// Backup indicates whether or not a backup of PGP-encrypted unseal keys
|
|
|
|
// should be stored at coreUnsealKeysBackupPath after successful rekeying.
|
|
|
|
Backup bool `json:"backup"`
|
|
|
|
|
|
|
|
// How many keys to store, for seals that support storage.
|
|
|
|
StoredShares int `json:"stored_shares"`
|
2018-05-18 01:17:52 +00:00
|
|
|
|
2018-05-21 21:46:32 +00:00
|
|
|
// Stores the progress of the rekey operation (key shares)
|
|
|
|
RekeyProgress [][]byte `json:"-"`
|
|
|
|
|
2018-05-18 01:17:52 +00:00
|
|
|
// VerificationRequired indicates that after a rekey validation must be
|
|
|
|
// performed (via providing shares from the new key) before the new key is
|
2018-05-21 20:13:38 +00:00
|
|
|
// actually installed. This is omitted from JSON as we don't persist the
|
2018-05-18 01:17:52 +00:00
|
|
|
// new key, it lives only in memory.
|
|
|
|
VerificationRequired bool `json:"-"`
|
|
|
|
|
|
|
|
// VerificationKey is the new key that we will roll to after successful
|
|
|
|
// validation
|
|
|
|
VerificationKey []byte `json:"-"`
|
|
|
|
|
|
|
|
// VerificationNonce stores the current operation nonce for verification
|
|
|
|
VerificationNonce string `json:"-"`
|
2018-05-21 21:46:32 +00:00
|
|
|
|
|
|
|
// Stores the progress of the verification operation (key shares)
|
|
|
|
VerificationProgress [][]byte `json:"-"`
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate is used to sanity check the seal configuration
|
|
|
|
func (s *SealConfig) Validate() error {
|
|
|
|
if s.SecretShares < 1 {
|
|
|
|
return fmt.Errorf("shares must be at least one")
|
|
|
|
}
|
|
|
|
if s.SecretThreshold < 1 {
|
|
|
|
return fmt.Errorf("threshold must be at least one")
|
|
|
|
}
|
|
|
|
if s.SecretShares > 1 && s.SecretThreshold == 1 {
|
|
|
|
return fmt.Errorf("threshold must be greater than one for multiple shares")
|
|
|
|
}
|
|
|
|
if s.SecretShares > 255 {
|
|
|
|
return fmt.Errorf("shares must be less than 256")
|
|
|
|
}
|
|
|
|
if s.SecretThreshold > 255 {
|
|
|
|
return fmt.Errorf("threshold must be less than 256")
|
|
|
|
}
|
|
|
|
if s.SecretThreshold > s.SecretShares {
|
|
|
|
return fmt.Errorf("threshold cannot be larger than shares")
|
|
|
|
}
|
|
|
|
if s.StoredShares > s.SecretShares {
|
|
|
|
return fmt.Errorf("stored keys cannot be larger than shares")
|
|
|
|
}
|
|
|
|
if len(s.PGPKeys) > 0 && len(s.PGPKeys) != s.SecretShares-s.StoredShares {
|
|
|
|
return fmt.Errorf("count mismatch between number of provided PGP keys and number of shares")
|
|
|
|
}
|
|
|
|
if len(s.PGPKeys) > 0 {
|
|
|
|
for _, keystring := range s.PGPKeys {
|
|
|
|
data, err := base64.StdEncoding.DecodeString(keystring)
|
|
|
|
if err != nil {
|
2018-04-05 15:49:21 +00:00
|
|
|
return errwrap.Wrapf("error decoding given PGP key: {{err}}", err)
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
_, err = openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(data)))
|
|
|
|
if err != nil {
|
2018-04-05 15:49:21 +00:00
|
|
|
return errwrap.Wrapf("error parsing given PGP key: {{err}}", err)
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SealConfig) Clone() *SealConfig {
|
|
|
|
ret := &SealConfig{
|
2018-05-18 01:17:52 +00:00
|
|
|
Type: s.Type,
|
|
|
|
SecretShares: s.SecretShares,
|
|
|
|
SecretThreshold: s.SecretThreshold,
|
|
|
|
Nonce: s.Nonce,
|
|
|
|
Backup: s.Backup,
|
|
|
|
StoredShares: s.StoredShares,
|
|
|
|
VerificationRequired: s.VerificationRequired,
|
2018-05-20 06:42:15 +00:00
|
|
|
VerificationNonce: s.VerificationNonce,
|
2016-04-04 14:44:22 +00:00
|
|
|
}
|
|
|
|
if len(s.PGPKeys) > 0 {
|
|
|
|
ret.PGPKeys = make([]string, len(s.PGPKeys))
|
|
|
|
copy(ret.PGPKeys, s.PGPKeys)
|
|
|
|
}
|
2018-05-18 01:17:52 +00:00
|
|
|
if len(s.VerificationKey) > 0 {
|
|
|
|
ret.VerificationKey = make([]byte, len(s.VerificationKey))
|
|
|
|
copy(ret.VerificationKey, s.VerificationKey)
|
|
|
|
}
|
2016-04-04 14:44:22 +00:00
|
|
|
return ret
|
|
|
|
}
|