2018-05-23 21:43:40 +00:00
|
|
|
package ca
|
2018-04-20 08:30:34 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/x509"
|
|
|
|
"crypto/x509/pkix"
|
|
|
|
"encoding/pem"
|
|
|
|
"fmt"
|
|
|
|
"math/big"
|
|
|
|
"net/url"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/hashicorp/consul/agent/connect"
|
2018-05-03 19:50:45 +00:00
|
|
|
"github.com/hashicorp/consul/agent/consul/state"
|
2018-04-20 08:30:34 +00:00
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
|
|
)
|
|
|
|
|
2018-05-09 22:12:31 +00:00
|
|
|
type ConsulProvider struct {
|
2018-05-03 19:50:45 +00:00
|
|
|
config *structs.ConsulCAProviderConfig
|
|
|
|
id string
|
2018-05-09 22:12:31 +00:00
|
|
|
delegate ConsulProviderStateDelegate
|
2018-04-20 08:30:34 +00:00
|
|
|
sync.RWMutex
|
|
|
|
}
|
|
|
|
|
2018-05-09 22:12:31 +00:00
|
|
|
type ConsulProviderStateDelegate interface {
|
2018-05-03 19:50:45 +00:00
|
|
|
State() *state.Store
|
|
|
|
ApplyCARequest(*structs.CARequest) error
|
|
|
|
}
|
|
|
|
|
2018-05-09 22:12:31 +00:00
|
|
|
// NewConsulProvider returns a new instance of the Consul CA provider,
|
2018-04-21 01:46:02 +00:00
|
|
|
// bootstrapping its state in the state store necessary
|
2018-05-09 22:12:31 +00:00
|
|
|
func NewConsulProvider(rawConfig map[string]interface{}, delegate ConsulProviderStateDelegate) (*ConsulProvider, error) {
|
2018-04-30 03:44:40 +00:00
|
|
|
conf, err := ParseConsulCAConfig(rawConfig)
|
2018-04-20 08:30:34 +00:00
|
|
|
if err != nil {
|
2018-04-21 01:46:02 +00:00
|
|
|
return nil, err
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
2018-05-09 22:12:31 +00:00
|
|
|
provider := &ConsulProvider{
|
2018-05-03 19:50:45 +00:00
|
|
|
config: conf,
|
|
|
|
delegate: delegate,
|
|
|
|
id: fmt.Sprintf("%s,%s", conf.PrivateKey, conf.RootCert),
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-04-21 01:46:02 +00:00
|
|
|
// Check if this configuration of the provider has already been
|
|
|
|
// initialized in the state store.
|
2018-05-03 19:50:45 +00:00
|
|
|
state := delegate.State()
|
2018-04-21 01:46:02 +00:00
|
|
|
_, providerState, err := state.CAProviderState(provider.id)
|
2018-04-20 08:30:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-04-21 01:46:02 +00:00
|
|
|
// Exit early if the state store has already been populated for this config.
|
2018-04-20 08:30:34 +00:00
|
|
|
if providerState != nil {
|
2018-04-21 01:46:02 +00:00
|
|
|
return provider, nil
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-04-21 01:46:02 +00:00
|
|
|
newState := structs.CAConsulProviderState{
|
|
|
|
ID: provider.id,
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-04-21 01:46:02 +00:00
|
|
|
// Write the initial provider state to get the index to use for the
|
|
|
|
// CA serial number.
|
|
|
|
{
|
2018-04-20 08:30:34 +00:00
|
|
|
args := &structs.CARequest{
|
|
|
|
Op: structs.CAOpSetProviderState,
|
|
|
|
ProviderState: &newState,
|
|
|
|
}
|
2018-05-03 19:50:45 +00:00
|
|
|
if err := delegate.ApplyCARequest(args); err != nil {
|
2018-04-20 08:30:34 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2018-04-21 01:46:02 +00:00
|
|
|
|
|
|
|
idx, _, err := state.CAProviderState(provider.id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a private key if needed
|
|
|
|
if conf.PrivateKey == "" {
|
2018-05-09 16:15:29 +00:00
|
|
|
_, pk, err := connect.GeneratePrivateKey()
|
2018-04-21 01:46:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
newState.PrivateKey = pk
|
|
|
|
} else {
|
|
|
|
newState.PrivateKey = conf.PrivateKey
|
|
|
|
}
|
|
|
|
|
2018-04-24 23:16:37 +00:00
|
|
|
// Generate the root CA if necessary
|
|
|
|
if conf.RootCert == "" {
|
|
|
|
ca, err := provider.generateCA(newState.PrivateKey, idx+1)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error generating CA: %v", err)
|
|
|
|
}
|
|
|
|
newState.RootCert = ca
|
|
|
|
} else {
|
|
|
|
newState.RootCert = conf.RootCert
|
2018-04-21 01:46:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Write the provider state
|
|
|
|
args := &structs.CARequest{
|
|
|
|
Op: structs.CAOpSetProviderState,
|
|
|
|
ProviderState: &newState,
|
|
|
|
}
|
2018-05-03 19:50:45 +00:00
|
|
|
if err := delegate.ApplyCARequest(args); err != nil {
|
2018-04-21 01:46:02 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return provider, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the active root CA and generate a new one if needed
|
2018-05-09 22:12:31 +00:00
|
|
|
func (c *ConsulProvider) ActiveRoot() (string, error) {
|
2018-05-03 19:50:45 +00:00
|
|
|
state := c.delegate.State()
|
2018-04-21 01:46:02 +00:00
|
|
|
_, providerState, err := state.CAProviderState(c.id)
|
|
|
|
if err != nil {
|
2018-04-24 23:16:37 +00:00
|
|
|
return "", err
|
2018-04-21 01:46:02 +00:00
|
|
|
}
|
|
|
|
|
2018-04-24 23:16:37 +00:00
|
|
|
return providerState.RootCert, nil
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-04-21 03:39:51 +00:00
|
|
|
// We aren't maintaining separate root/intermediate CAs for the builtin
|
|
|
|
// provider, so just return the root.
|
2018-05-09 22:12:31 +00:00
|
|
|
func (c *ConsulProvider) ActiveIntermediate() (string, error) {
|
2018-04-20 08:30:34 +00:00
|
|
|
return c.ActiveRoot()
|
|
|
|
}
|
|
|
|
|
2018-04-21 03:39:51 +00:00
|
|
|
// We aren't maintaining separate root/intermediate CAs for the builtin
|
2018-06-13 08:40:03 +00:00
|
|
|
// provider, so just return the root.
|
2018-05-09 22:12:31 +00:00
|
|
|
func (c *ConsulProvider) GenerateIntermediate() (string, error) {
|
2018-06-13 08:40:03 +00:00
|
|
|
return c.ActiveIntermediate()
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-04-21 01:46:02 +00:00
|
|
|
// Remove the state store entry for this provider instance.
|
2018-05-09 22:12:31 +00:00
|
|
|
func (c *ConsulProvider) Cleanup() error {
|
2018-04-21 01:46:02 +00:00
|
|
|
args := &structs.CARequest{
|
|
|
|
Op: structs.CAOpDeleteProviderState,
|
|
|
|
ProviderState: &structs.CAConsulProviderState{ID: c.id},
|
|
|
|
}
|
2018-05-03 19:50:45 +00:00
|
|
|
if err := c.delegate.ApplyCARequest(args); err != nil {
|
2018-04-21 01:46:02 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-04-20 08:30:34 +00:00
|
|
|
// Sign returns a new certificate valid for the given SpiffeIDService
|
|
|
|
// using the current CA.
|
2018-05-09 22:12:31 +00:00
|
|
|
func (c *ConsulProvider) Sign(csr *x509.CertificateRequest) (string, error) {
|
2018-04-21 01:46:02 +00:00
|
|
|
// Lock during the signing so we don't use the same index twice
|
|
|
|
// for different cert serial numbers.
|
|
|
|
c.Lock()
|
|
|
|
defer c.Unlock()
|
|
|
|
|
2018-04-20 08:30:34 +00:00
|
|
|
// Get the provider state
|
2018-05-03 19:50:45 +00:00
|
|
|
state := c.delegate.State()
|
2018-05-04 23:01:38 +00:00
|
|
|
idx, providerState, err := state.CAProviderState(c.id)
|
2018-04-20 08:30:34 +00:00
|
|
|
if err != nil {
|
2018-04-24 23:31:42 +00:00
|
|
|
return "", err
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-04-21 03:39:51 +00:00
|
|
|
// Create the keyId for the cert from the signing private key.
|
2018-04-20 08:30:34 +00:00
|
|
|
signer, err := connect.ParseSigner(providerState.PrivateKey)
|
|
|
|
if err != nil {
|
2018-04-24 23:31:42 +00:00
|
|
|
return "", err
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
if signer == nil {
|
2018-04-24 23:31:42 +00:00
|
|
|
return "", fmt.Errorf("error signing cert: Consul CA not initialized yet")
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
keyId, err := connect.KeyId(signer.Public())
|
|
|
|
if err != nil {
|
2018-04-24 23:31:42 +00:00
|
|
|
return "", err
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-04-24 23:16:37 +00:00
|
|
|
// Parse the SPIFFE ID
|
|
|
|
spiffeId, err := connect.ParseCertURI(csr.URIs[0])
|
|
|
|
if err != nil {
|
2018-04-24 23:31:42 +00:00
|
|
|
return "", err
|
2018-04-24 23:16:37 +00:00
|
|
|
}
|
|
|
|
serviceId, ok := spiffeId.(*connect.SpiffeIDService)
|
|
|
|
if !ok {
|
2018-04-24 23:31:42 +00:00
|
|
|
return "", fmt.Errorf("SPIFFE ID in CSR must be a service ID")
|
2018-04-24 23:16:37 +00:00
|
|
|
}
|
|
|
|
|
2018-04-20 08:30:34 +00:00
|
|
|
// Parse the CA cert
|
2018-04-24 23:16:37 +00:00
|
|
|
caCert, err := connect.ParseCert(providerState.RootCert)
|
2018-04-20 08:30:34 +00:00
|
|
|
if err != nil {
|
2018-04-24 23:31:42 +00:00
|
|
|
return "", fmt.Errorf("error parsing CA cert: %s", err)
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Cert template for generation
|
|
|
|
sn := &big.Int{}
|
2018-05-04 23:01:38 +00:00
|
|
|
sn.SetUint64(idx + 1)
|
2018-06-20 19:28:54 +00:00
|
|
|
// Sign the certificate valid from 1 minute in the past, this helps it be
|
|
|
|
// accepted right away even when nodes are not in close time sync accross the
|
|
|
|
// cluster. A minute is more than enough for typical DC clock drift.
|
|
|
|
effectiveNow := time.Now().Add(-1 * time.Minute)
|
2018-04-20 08:30:34 +00:00
|
|
|
template := x509.Certificate{
|
|
|
|
SerialNumber: sn,
|
|
|
|
Subject: pkix.Name{CommonName: serviceId.Service},
|
|
|
|
URIs: csr.URIs,
|
|
|
|
Signature: csr.Signature,
|
|
|
|
SignatureAlgorithm: csr.SignatureAlgorithm,
|
|
|
|
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
|
|
|
|
PublicKey: csr.PublicKey,
|
|
|
|
BasicConstraintsValid: true,
|
|
|
|
KeyUsage: x509.KeyUsageDataEncipherment |
|
|
|
|
x509.KeyUsageKeyAgreement |
|
|
|
|
x509.KeyUsageDigitalSignature |
|
|
|
|
x509.KeyUsageKeyEncipherment,
|
|
|
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
|
|
x509.ExtKeyUsageClientAuth,
|
|
|
|
x509.ExtKeyUsageServerAuth,
|
|
|
|
},
|
2018-04-27 03:14:37 +00:00
|
|
|
// todo(kyhavlov): add a way to set the cert lifetime here from the CA config
|
2018-06-20 19:28:54 +00:00
|
|
|
NotAfter: effectiveNow.Add(3 * 24 * time.Hour),
|
|
|
|
NotBefore: effectiveNow,
|
2018-04-20 08:30:34 +00:00
|
|
|
AuthorityKeyId: keyId,
|
|
|
|
SubjectKeyId: keyId,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the certificate, PEM encode it and return that value.
|
|
|
|
var buf bytes.Buffer
|
|
|
|
bs, err := x509.CreateCertificate(
|
2018-04-30 21:23:49 +00:00
|
|
|
rand.Reader, &template, caCert, csr.PublicKey, signer)
|
2018-04-20 08:30:34 +00:00
|
|
|
if err != nil {
|
2018-04-24 23:31:42 +00:00
|
|
|
return "", fmt.Errorf("error generating certificate: %s", err)
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
|
|
|
if err != nil {
|
2018-05-09 16:15:29 +00:00
|
|
|
return "", fmt.Errorf("error encoding certificate: %s", err)
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-05-04 23:01:38 +00:00
|
|
|
err = c.incrementProviderIndex(providerState)
|
2018-04-30 21:23:49 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2018-04-20 08:30:34 +00:00
|
|
|
|
|
|
|
// Set the response
|
2018-04-24 23:31:42 +00:00
|
|
|
return buf.String(), nil
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-06-19 23:46:18 +00:00
|
|
|
// CrossSignCA returns the given CA cert signed by the current active root.
|
|
|
|
func (c *ConsulProvider) CrossSignCA(cert *x509.Certificate) (string, error) {
|
2018-04-21 03:39:51 +00:00
|
|
|
c.Lock()
|
|
|
|
defer c.Unlock()
|
|
|
|
|
|
|
|
// Get the provider state
|
2018-05-03 19:50:45 +00:00
|
|
|
state := c.delegate.State()
|
2018-05-04 23:01:38 +00:00
|
|
|
idx, providerState, err := state.CAProviderState(c.id)
|
2018-04-21 03:39:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
privKey, err := connect.ParseSigner(providerState.PrivateKey)
|
|
|
|
if err != nil {
|
2018-04-25 18:34:08 +00:00
|
|
|
return "", fmt.Errorf("error parsing private key %q: %s", providerState.PrivateKey, err)
|
2018-04-21 03:39:51 +00:00
|
|
|
}
|
|
|
|
|
2018-04-24 23:16:37 +00:00
|
|
|
rootCA, err := connect.ParseCert(providerState.RootCert)
|
2018-04-21 03:39:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2018-04-24 23:16:37 +00:00
|
|
|
|
2018-06-19 23:46:18 +00:00
|
|
|
keyId, err := connect.KeyId(privKey.Public())
|
2018-04-21 03:39:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2018-04-24 23:16:37 +00:00
|
|
|
// Create the cross-signing template from the existing root CA
|
2018-04-21 03:39:51 +00:00
|
|
|
serialNum := &big.Int{}
|
2018-05-04 23:01:38 +00:00
|
|
|
serialNum.SetUint64(idx + 1)
|
2018-06-19 23:46:18 +00:00
|
|
|
template := *cert
|
|
|
|
template.SerialNumber = serialNum
|
|
|
|
template.SignatureAlgorithm = rootCA.SignatureAlgorithm
|
|
|
|
template.AuthorityKeyId = keyId
|
2018-04-21 03:39:51 +00:00
|
|
|
|
2018-06-20 19:28:54 +00:00
|
|
|
// Sign the certificate valid from 1 minute in the past, this helps it be
|
|
|
|
// accepted right away even when nodes are not in close time sync accross the
|
|
|
|
// cluster. A minute is more than enough for typical DC clock drift.
|
|
|
|
effectiveNow := time.Now().Add(-1 * time.Minute)
|
|
|
|
template.NotBefore = effectiveNow
|
|
|
|
// This cross-signed cert is only needed during rotation, and only while old
|
|
|
|
// leaf certs are still in use. They expire within 3 days currently so 7 is
|
|
|
|
// safe. TODO(banks): make this be based on leaf expiry time when that is
|
|
|
|
// configurable.
|
|
|
|
template.NotAfter = effectiveNow.Add(7 * 24 * time.Hour)
|
|
|
|
|
2018-04-21 03:39:51 +00:00
|
|
|
bs, err := x509.CreateCertificate(
|
2018-06-19 23:46:18 +00:00
|
|
|
rand.Reader, &template, rootCA, cert.PublicKey, privKey)
|
2018-04-21 03:39:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error generating CA certificate: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error encoding private key: %s", err)
|
|
|
|
}
|
|
|
|
|
2018-05-04 23:01:38 +00:00
|
|
|
err = c.incrementProviderIndex(providerState)
|
2018-04-30 21:23:49 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2018-04-27 03:14:37 +00:00
|
|
|
|
2018-04-21 03:39:51 +00:00
|
|
|
return buf.String(), nil
|
|
|
|
}
|
|
|
|
|
2018-05-04 23:01:38 +00:00
|
|
|
// incrementProviderIndex does a write to increment the provider state store table index
|
|
|
|
// used for serial numbers when generating certificates.
|
2018-05-09 22:12:31 +00:00
|
|
|
func (c *ConsulProvider) incrementProviderIndex(providerState *structs.CAConsulProviderState) error {
|
2018-04-27 03:14:37 +00:00
|
|
|
newState := *providerState
|
|
|
|
args := &structs.CARequest{
|
|
|
|
Op: structs.CAOpSetProviderState,
|
|
|
|
ProviderState: &newState,
|
|
|
|
}
|
2018-05-03 19:50:45 +00:00
|
|
|
if err := c.delegate.ApplyCARequest(args); err != nil {
|
2018-04-27 03:14:37 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-04-20 08:30:34 +00:00
|
|
|
// generateCA makes a new root CA using the current private key
|
2018-05-09 22:12:31 +00:00
|
|
|
func (c *ConsulProvider) generateCA(privateKey string, sn uint64) (string, error) {
|
2018-05-03 19:50:45 +00:00
|
|
|
state := c.delegate.State()
|
2018-04-20 08:30:34 +00:00
|
|
|
_, config, err := state.CAConfig()
|
|
|
|
if err != nil {
|
2018-04-24 23:16:37 +00:00
|
|
|
return "", err
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
privKey, err := connect.ParseSigner(privateKey)
|
|
|
|
if err != nil {
|
2018-04-25 18:34:08 +00:00
|
|
|
return "", fmt.Errorf("error parsing private key %q: %s", privateKey, err)
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
name := fmt.Sprintf("Consul CA %d", sn)
|
|
|
|
|
2018-04-24 23:16:37 +00:00
|
|
|
// The URI (SPIFFE compatible) for the cert
|
2018-05-08 13:23:44 +00:00
|
|
|
id := connect.SpiffeIDSigningForCluster(config)
|
2018-04-24 23:16:37 +00:00
|
|
|
keyId, err := connect.KeyId(privKey.Public())
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2018-04-21 01:46:02 +00:00
|
|
|
|
2018-04-24 23:16:37 +00:00
|
|
|
// Create the CA cert
|
|
|
|
serialNum := &big.Int{}
|
|
|
|
serialNum.SetUint64(sn)
|
|
|
|
template := x509.Certificate{
|
|
|
|
SerialNumber: serialNum,
|
|
|
|
Subject: pkix.Name{CommonName: name},
|
|
|
|
URIs: []*url.URL{id.URI()},
|
|
|
|
PermittedDNSDomainsCritical: true,
|
|
|
|
PermittedDNSDomains: []string{id.URI().Hostname()},
|
|
|
|
BasicConstraintsValid: true,
|
|
|
|
KeyUsage: x509.KeyUsageCertSign |
|
|
|
|
x509.KeyUsageCRLSign |
|
|
|
|
x509.KeyUsageDigitalSignature,
|
|
|
|
IsCA: true,
|
|
|
|
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
|
|
|
|
NotBefore: time.Now(),
|
|
|
|
AuthorityKeyId: keyId,
|
|
|
|
SubjectKeyId: keyId,
|
|
|
|
}
|
2018-04-21 01:46:02 +00:00
|
|
|
|
2018-04-24 23:16:37 +00:00
|
|
|
bs, err := x509.CreateCertificate(
|
|
|
|
rand.Reader, &template, &template, privKey.Public(), privKey)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error generating CA certificate: %s", err)
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-04-24 23:16:37 +00:00
|
|
|
var buf bytes.Buffer
|
|
|
|
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
2018-04-20 08:30:34 +00:00
|
|
|
if err != nil {
|
2018-04-24 23:16:37 +00:00
|
|
|
return "", fmt.Errorf("error encoding private key: %s", err)
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|
|
|
|
|
2018-04-24 23:16:37 +00:00
|
|
|
return buf.String(), nil
|
2018-04-20 08:30:34 +00:00
|
|
|
}
|