storj/pkg/provider/certificate_authority.go

264 lines
6.9 KiB
Go

// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package provider
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"github.com/zeebo/errs"
"storj.io/storj/pkg/peertls"
"storj.io/storj/pkg/storj"
"storj.io/storj/pkg/utils"
)
// PeerCertificateAuthority represents the CA which is used to validate peer identities
type PeerCertificateAuthority struct {
RestChain []*x509.Certificate
// Cert is the x509 certificate of the CA
Cert *x509.Certificate
// The ID is calculated from the CA public key.
ID storj.NodeID
}
// FullCertificateAuthority represents the CA which is used to author and validate full identities
type FullCertificateAuthority struct {
RestChain []*x509.Certificate
// Cert is the x509 certificate of the CA
Cert *x509.Certificate
// The ID is calculated from the CA public key.
ID storj.NodeID
// Key is the private key of the CA
Key crypto.PrivateKey
}
// CASetupConfig is for creating a CA
type CASetupConfig struct {
ParentCertPath string `help:"path to the parent authority's certificate chain"`
ParentKeyPath string `help:"path to the parent authority's private key"`
CertPath string `help:"path to the certificate chain for this identity" default:"$CONFDIR/ca.cert"`
KeyPath string `help:"path to the private key for this identity" default:"$CONFDIR/ca.key"`
Difficulty uint64 `help:"minimum difficulty for identity generation" default:"15"`
Timeout string `help:"timeout for CA generation; golang duration string (0 no timeout)" default:"5m"`
Overwrite bool `help:"if true, existing CA certs AND keys will overwritten" default:"false"`
Concurrency uint `help:"number of concurrent workers for certificate authority generation" default:"4"`
}
// NewCAOptions is used to pass parameters to `NewCA`
type NewCAOptions struct {
// Difficulty is the number of trailing zero-bits the nodeID must have
Difficulty uint16
// Concurrency is the number of go routines used to generate a CA of sufficient difficulty
Concurrency uint
// ParentCert, if provided will be prepended to the certificate chain
ParentCert *x509.Certificate
// ParentKey ()
ParentKey crypto.PrivateKey
}
// PeerCAConfig is for locating a CA certificate without a private key
type PeerCAConfig struct {
CertPath string `help:"path to the certificate chain for this identity" default:"$CONFDIR/ca.cert"`
}
// FullCAConfig is for locating a CA certificate and it's private key
type FullCAConfig struct {
CertPath string `help:"path to the certificate chain for this identity" default:"$CONFDIR/ca.cert"`
KeyPath string `help:"path to the private key for this identity" default:"$CONFDIR/ca.key"`
}
// Status returns the status of the CA cert/key files for the config
func (caS CASetupConfig) Status() TLSFilesStatus {
return statTLSFiles(caS.CertPath, caS.KeyPath)
}
// Create generates and saves a CA using the config
func (caS CASetupConfig) Create(ctx context.Context) (*FullCertificateAuthority, error) {
var (
err error
parent *FullCertificateAuthority
)
if caS.ParentCertPath != "" && caS.ParentKeyPath != "" {
parent, err = FullCAConfig{
CertPath: caS.ParentCertPath,
KeyPath: caS.ParentKeyPath,
}.Load()
}
if err != nil {
return nil, err
}
if parent == nil {
parent = &FullCertificateAuthority{}
}
ca, err := NewCA(ctx, NewCAOptions{
Difficulty: uint16(caS.Difficulty),
Concurrency: caS.Concurrency,
ParentCert: parent.Cert,
ParentKey: parent.Key,
})
if err != nil {
return nil, err
}
caC := FullCAConfig{
CertPath: caS.CertPath,
KeyPath: caS.KeyPath,
}
return ca, caC.Save(ca)
}
// Load loads a CA from the given configuration
func (fc FullCAConfig) Load() (*FullCertificateAuthority, error) {
p, err := fc.PeerConfig().Load()
if err != nil {
return nil, err
}
kb, err := ioutil.ReadFile(fc.KeyPath)
if err != nil {
return nil, peertls.ErrNotExist.Wrap(err)
}
kp, _ := pem.Decode(kb)
k, err := x509.ParseECPrivateKey(kp.Bytes)
if err != nil {
return nil, errs.New("unable to parse EC private key: %v", err)
}
return &FullCertificateAuthority{
RestChain: p.RestChain,
Cert: p.Cert,
Key: k,
ID: p.ID,
}, nil
}
// PeerConfig converts a full ca config to a peer ca config
func (fc FullCAConfig) PeerConfig() PeerCAConfig {
return PeerCAConfig{
CertPath: fc.CertPath,
}
}
// Load loads a CA from the given configuration
func (pc PeerCAConfig) Load() (*PeerCertificateAuthority, error) {
cd, err := ioutil.ReadFile(pc.CertPath)
if err != nil {
return nil, peertls.ErrNotExist.Wrap(err)
}
var cb [][]byte
for {
var cp *pem.Block
cp, cd = pem.Decode(cd)
if cp == nil {
break
}
cb = append(cb, cp.Bytes)
}
c, err := ParseCertChain(cb)
if err != nil {
return nil, errs.New("failed to load identity %#v: %v",
pc.CertPath, err)
}
i, err := NodeIDFromKey(c[0].PublicKey)
if err != nil {
return nil, err
}
return &PeerCertificateAuthority{
RestChain: c[1:],
Cert: c[0],
ID: i,
}, nil
}
// NewCA creates a new full identity with the given difficulty
func NewCA(ctx context.Context, opts NewCAOptions) (*FullCertificateAuthority, error) {
if opts.Concurrency < 1 {
opts.Concurrency = 1
}
ctx, cancel := context.WithCancel(ctx)
eC := make(chan error)
caC := make(chan FullCertificateAuthority, 1)
for i := 0; i < int(opts.Concurrency); i++ {
go newCAWorker(ctx, opts.Difficulty, opts.ParentCert, opts.ParentKey, caC, eC)
}
select {
case ca := <-caC:
cancel()
return &ca, nil
case err := <-eC:
cancel()
return nil, err
}
}
// Save saves a CA with the given configuration
func (fc FullCAConfig) Save(ca *FullCertificateAuthority) error {
var certData, keyData bytes.Buffer
chain := []*x509.Certificate{ca.Cert}
chain = append(chain, ca.RestChain...)
writeErr := utils.CombineErrors(
peertls.WriteChain(&certData, chain...),
peertls.WriteKey(&keyData, ca.Key),
)
if writeErr != nil {
return writeErr
}
return utils.CombineErrors(
writeCertData(fc.CertPath, certData.Bytes()),
writeKeyData(fc.KeyPath, keyData.Bytes()),
)
}
// NewIdentity generates a new `FullIdentity` based on the CA. The CA
// cert is included in the identity's cert chain and the identity's leaf cert
// is signed by the CA.
func (ca FullCertificateAuthority) NewIdentity() (*FullIdentity, error) {
leafTemplate, err := peertls.LeafTemplate()
if err != nil {
return nil, err
}
leafKey, err := peertls.NewKey()
if err != nil {
return nil, err
}
pk, ok := leafKey.(*ecdsa.PrivateKey)
if !ok {
return nil, peertls.ErrUnsupportedKey.New("%T", leafKey)
}
leafCert, err := peertls.NewCert(pk, ca.Key, leafTemplate, ca.Cert)
if err != nil {
return nil, err
}
if ca.RestChain != nil && len(ca.RestChain) > 0 {
err := peertls.AddSignedLeafExt(ca.Key, leafCert)
if err != nil {
return nil, err
}
}
return &FullIdentity{
RestChain: ca.RestChain,
CA: ca.Cert,
Leaf: leafCert,
Key: leafKey,
ID: ca.ID,
}, nil
}