storj/pkg/identity/identity.go
Jeff Wendling 098cbc9c67 all: use pkg/rpc instead of pkg/transport
all of the packages and tests work with both grpc and
drpc. we'll probably need to do some jenkins pipelines
to run the tests with drpc as well.

most of the changes are really due to a bit of cleanup
of the pkg/transport.Client api into an rpc.Dialer in
the spirit of a net.Dialer. now that we don't need
observers, we can pass around stateless configuration
to everything rather than stateful things that issue
observations. it also adds a DialAddressID for the
case where we don't have a pb.Node, but we do have an
address and want to assert some ID. this happened
pretty frequently, and now there's no more weird
contortions creating custom tls options, etc.

a lot of the other changes are being consistent/using
the abstractions in the rpc package to do rpc style
things like finding peer information, or checking
status codes.

Change-Id: Ief62875e21d80a21b3c56a5a37f45887679f9412
2019-09-25 15:37:06 -06:00

547 lines
16 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package identity
import (
"bytes"
"context"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"io/ioutil"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/zeebo/errs"
"storj.io/storj/pkg/peertls"
"storj.io/storj/pkg/peertls/extensions"
"storj.io/storj/pkg/pkcrypto"
"storj.io/storj/pkg/rpc/rpcpeer"
"storj.io/storj/pkg/storj"
)
// PeerIdentity represents another peer on the network.
type PeerIdentity struct {
RestChain []*x509.Certificate
// CA represents the peer's self-signed CA.
CA *x509.Certificate
// Leaf represents the leaf they're currently using. The leaf should be
// signed by the CA. The leaf is what is used for communication.
Leaf *x509.Certificate
// The ID taken from the CA public key.
ID storj.NodeID
}
// FullIdentity represents you on the network. In addition to a PeerIdentity,
// a FullIdentity also has a Key, which a PeerIdentity doesn't have.
type FullIdentity struct {
RestChain []*x509.Certificate
// CA represents the peer's self-signed CA. The ID is taken from this cert.
CA *x509.Certificate
// Leaf represents the leaf they're currently using. The leaf should be
// signed by the CA. The leaf is what is used for communication.
Leaf *x509.Certificate
// The ID taken from the CA public key.
ID storj.NodeID
// Key is the key this identity uses with the leaf for communication.
Key crypto.PrivateKey
}
// ManageablePeerIdentity is a `PeerIdentity` and its corresponding `FullCertificateAuthority`
// in a single struct. It is used for making changes to the identity that require CA
// authorization; e.g. adding extensions.
type ManageablePeerIdentity struct {
*PeerIdentity
CA *FullCertificateAuthority
}
// ManageableFullIdentity is a `FullIdentity` and its corresponding `FullCertificateAuthority`
// in a single struct. It is used for making changes to the identity that require CA
// authorization and the leaf private key; e.g. revoking a leaf cert (private key changes).
type ManageableFullIdentity struct {
*FullIdentity
CA *FullCertificateAuthority
}
// SetupConfig allows you to run a set of Responsibilities with the given
// identity. You can also just load an Identity from disk.
type SetupConfig struct {
CertPath string `help:"path to the certificate chain for this identity" default:"$IDENTITYDIR/identity.cert"`
KeyPath string `help:"path to the private key for this identity" default:"$IDENTITYDIR/identity.key"`
Overwrite bool `help:"if true, existing identity certs AND keys will overwritten for" default:"false" setup:"true"`
Version string `help:"semantic version of identity storage format" default:"0"`
}
// Config allows you to run a set of Responsibilities with the given
// identity. You can also just load an Identity from disk.
type Config struct {
CertPath string `help:"path to the certificate chain for this identity" default:"$IDENTITYDIR/identity.cert" user:"true"`
KeyPath string `help:"path to the private key for this identity" default:"$IDENTITYDIR/identity.key" user:"true"`
}
// PeerConfig allows you to interact with a peer identity (cert, no key) on disk.
type PeerConfig struct {
CertPath string `help:"path to the certificate chain for this identity" default:"$IDENTITYDIR/identity.cert" user:"true"`
}
// FullCertificateAuthorityFromPEM loads a FullIdentity from a certificate chain and
// private key PEM-encoded bytes.
func FullCertificateAuthorityFromPEM(chainPEM, keyPEM []byte) (*FullCertificateAuthority, error) {
peerCA, err := PeerCertificateAuthorityFromPEM(chainPEM)
if err != nil {
return nil, err
}
// NB: there shouldn't be multiple keys in the key file but if there
// are, this uses the first one
key, err := pkcrypto.PrivateKeyFromPEM(keyPEM)
if err != nil {
return nil, err
}
return &FullCertificateAuthority{
RestChain: peerCA.RestChain,
Cert: peerCA.Cert,
Key: key,
ID: peerCA.ID,
}, nil
}
// PeerCertificateAuthorityFromPEM loads a FullIdentity from a certificate chain and
// private key PEM-encoded bytes.
func PeerCertificateAuthorityFromPEM(chainPEM []byte) (*PeerCertificateAuthority, error) {
chain, err := pkcrypto.CertsFromPEM(chainPEM)
if err != nil {
return nil, errs.Wrap(err)
}
// NB: the "leaf" cert in a CA chain is the "CA" cert in an identity chain
nodeID, err := NodeIDFromCert(chain[peertls.LeafIndex])
if err != nil {
return nil, err
}
return &PeerCertificateAuthority{
RestChain: chain[peertls.CAIndex:],
Cert: chain[peertls.LeafIndex],
ID: nodeID,
}, nil
}
// FullIdentityFromPEM loads a FullIdentity from a certificate chain and
// private key PEM-encoded bytes.
func FullIdentityFromPEM(chainPEM, keyPEM []byte) (*FullIdentity, error) {
peerIdent, err := PeerIdentityFromPEM(chainPEM)
if err != nil {
return nil, err
}
// NB: there shouldn't be multiple keys in the key file but if there
// are, this uses the first one
key, err := pkcrypto.PrivateKeyFromPEM(keyPEM)
if err != nil {
return nil, err
}
return &FullIdentity{
RestChain: peerIdent.RestChain,
CA: peerIdent.CA,
Leaf: peerIdent.Leaf,
Key: key,
ID: peerIdent.ID,
}, nil
}
// PeerIdentityFromPEM loads a PeerIdentity from a certificate chain and
// private key PEM-encoded bytes.
func PeerIdentityFromPEM(chainPEM []byte) (*PeerIdentity, error) {
chain, err := pkcrypto.CertsFromPEM(chainPEM)
if err != nil {
return nil, errs.Wrap(err)
}
if len(chain) < peertls.CAIndex+1 {
return nil, pkcrypto.ErrChainLength.New("identity chain does not contain a CA certificate")
}
nodeID, err := NodeIDFromCert(chain[peertls.CAIndex])
if err != nil {
return nil, err
}
return &PeerIdentity{
RestChain: chain[peertls.CAIndex+1:],
CA: chain[peertls.CAIndex],
Leaf: chain[peertls.LeafIndex],
ID: nodeID,
}, nil
}
// PeerIdentityFromChain loads a PeerIdentity from an identity certificate chain.
func PeerIdentityFromChain(chain []*x509.Certificate) (*PeerIdentity, error) {
nodeID, err := NodeIDFromCert(chain[peertls.CAIndex])
if err != nil {
return nil, err
}
return &PeerIdentity{
RestChain: chain[peertls.CAIndex+1:],
CA: chain[peertls.CAIndex],
ID: nodeID,
Leaf: chain[peertls.LeafIndex],
}, nil
}
// PeerIdentityFromPeer loads a PeerIdentity from a peer connection.
func PeerIdentityFromPeer(peer *rpcpeer.Peer) (*PeerIdentity, error) {
chain := peer.State.PeerCertificates
if len(chain)-1 < peertls.CAIndex {
return nil, Error.New("invalid certificate chain")
}
pi, err := PeerIdentityFromChain(chain)
if err != nil {
return nil, err
}
return pi, nil
}
// PeerIdentityFromContext loads a PeerIdentity from a ctx TLS credentials.
func PeerIdentityFromContext(ctx context.Context) (*PeerIdentity, error) {
peer, err := rpcpeer.FromContext(ctx)
if err != nil {
return nil, err
}
return PeerIdentityFromPeer(peer)
}
// NodeIDFromCertPath loads a node ID from a certificate file path.
func NodeIDFromCertPath(certPath string) (storj.NodeID, error) {
certBytes, err := ioutil.ReadFile(certPath)
if err != nil {
return storj.NodeID{}, err
}
return NodeIDFromPEM(certBytes)
}
// NodeIDFromPEM loads a node ID from certificate bytes.
func NodeIDFromPEM(pemBytes []byte) (storj.NodeID, error) {
chain, err := pkcrypto.CertsFromPEM(pemBytes)
if err != nil {
return storj.NodeID{}, Error.New("invalid identity certificate")
}
if len(chain)-1 < peertls.CAIndex {
return storj.NodeID{}, Error.New("no CA in identity certificate")
}
return NodeIDFromCert(chain[peertls.CAIndex])
}
// NodeIDFromCert looks for a version in an ID version extension in the passed
// cert and then calculates a versioned node ID using the certificate public key.
// NB: `cert` would typically be an identity's certificate authority certificate.
func NodeIDFromCert(cert *x509.Certificate) (id storj.NodeID, err error) {
version, err := storj.IDVersionFromCert(cert)
if err != nil {
return id, Error.Wrap(err)
}
return NodeIDFromKey(cert.PublicKey, version)
}
// NodeIDFromKey calculates the node ID for a given public key with the passed version.
func NodeIDFromKey(k crypto.PublicKey, version storj.IDVersion) (storj.NodeID, error) {
idBytes, err := peertls.DoubleSHA256PublicKey(k)
if err != nil {
return storj.NodeID{}, storj.ErrNodeID.Wrap(err)
}
return storj.NewVersionedID(idBytes, version), nil
}
// NewFullIdentity creates a new ID for nodes with difficulty and concurrency params.
func NewFullIdentity(ctx context.Context, opts NewCAOptions) (*FullIdentity, error) {
ca, err := NewCA(ctx, opts)
if err != nil {
return nil, err
}
identity, err := ca.NewIdentity()
if err != nil {
return nil, err
}
return identity, err
}
// ToChains takes a number of certificate chains and returns them as a 2d slice of chains of certificates.
func ToChains(chains ...[]*x509.Certificate) [][]*x509.Certificate {
combinedChains := make([][]*x509.Certificate, len(chains))
copy(combinedChains, chains)
return combinedChains
}
// NewManageablePeerIdentity returns a manageable identity given a full identity and a full certificate authority.
func NewManageablePeerIdentity(ident *PeerIdentity, ca *FullCertificateAuthority) *ManageablePeerIdentity {
return &ManageablePeerIdentity{
PeerIdentity: ident,
CA: ca,
}
}
// NewManageableFullIdentity returns a manageable identity given a full identity and a full certificate authority.
func NewManageableFullIdentity(ident *FullIdentity, ca *FullCertificateAuthority) *ManageableFullIdentity {
return &ManageableFullIdentity{
FullIdentity: ident,
CA: ca,
}
}
// Status returns the status of the identity cert/key files for the config
func (is SetupConfig) Status() (TLSFilesStatus, error) {
return statTLSFiles(is.CertPath, is.KeyPath)
}
// Create generates and saves a CA using the config
func (is SetupConfig) Create(ca *FullCertificateAuthority) (*FullIdentity, error) {
fi, err := ca.NewIdentity()
if err != nil {
return nil, err
}
fi.CA = ca.Cert
ic := Config{
CertPath: is.CertPath,
KeyPath: is.KeyPath,
}
return fi, ic.Save(fi)
}
// FullConfig converts a `SetupConfig` to `Config`
func (is SetupConfig) FullConfig() Config {
return Config{
CertPath: is.CertPath,
KeyPath: is.KeyPath,
}
}
// Load loads a FullIdentity from the config
func (ic Config) Load() (*FullIdentity, error) {
c, err := ioutil.ReadFile(ic.CertPath)
if err != nil {
return nil, peertls.ErrNotExist.Wrap(err)
}
k, err := ioutil.ReadFile(ic.KeyPath)
if err != nil {
return nil, peertls.ErrNotExist.Wrap(err)
}
fi, err := FullIdentityFromPEM(c, k)
if err != nil {
return nil, errs.New("failed to load identity %#v, %#v: %v",
ic.CertPath, ic.KeyPath, err)
}
return fi, nil
}
// Save saves a FullIdentity according to the config
func (ic Config) Save(fi *FullIdentity) error {
var (
certData, keyData bytes.Buffer
writeChainErr, writeChainDataErr, writeKeyErr, writeKeyDataErr error
)
chain := []*x509.Certificate{fi.Leaf, fi.CA}
chain = append(chain, fi.RestChain...)
if ic.CertPath != "" {
writeChainErr = peertls.WriteChain(&certData, chain...)
writeChainDataErr = writeChainData(ic.CertPath, certData.Bytes())
}
if ic.KeyPath != "" {
writeKeyErr = pkcrypto.WritePrivateKeyPEM(&keyData, fi.Key)
writeKeyDataErr = writeKeyData(ic.KeyPath, keyData.Bytes())
}
writeErr := errs.Combine(writeChainErr, writeKeyErr)
if writeErr != nil {
return writeErr
}
return errs.Combine(
writeChainDataErr,
writeKeyDataErr,
)
}
// SaveBackup saves the certificate of the config with a timestamped filename
func (ic Config) SaveBackup(fi *FullIdentity) error {
return Config{
CertPath: backupPath(ic.CertPath),
KeyPath: backupPath(ic.KeyPath),
}.Save(fi)
}
// PeerConfig converts a Config to a PeerConfig
func (ic Config) PeerConfig() *PeerConfig {
return &PeerConfig{
CertPath: ic.CertPath,
}
}
// Load loads a PeerIdentity from the config
func (ic PeerConfig) Load() (*PeerIdentity, error) {
c, err := ioutil.ReadFile(ic.CertPath)
if err != nil {
return nil, peertls.ErrNotExist.Wrap(err)
}
pi, err := PeerIdentityFromPEM(c)
if err != nil {
return nil, errs.New("failed to load identity %#v: %v", ic.CertPath, err)
}
return pi, nil
}
// Save saves a PeerIdentity according to the config
func (ic PeerConfig) Save(peerIdent *PeerIdentity) error {
chain := []*x509.Certificate{peerIdent.Leaf, peerIdent.CA}
chain = append(chain, peerIdent.RestChain...)
if ic.CertPath != "" {
var certData bytes.Buffer
err := peertls.WriteChain(&certData, chain...)
if err != nil {
return err
}
return writeChainData(ic.CertPath, certData.Bytes())
}
return nil
}
// SaveBackup saves the certificate of the config with a timestamped filename
func (ic PeerConfig) SaveBackup(pi *PeerIdentity) error {
return PeerConfig{
CertPath: backupPath(ic.CertPath),
}.Save(pi)
}
// Chain returns the Identity's certificate chain
func (fi *FullIdentity) Chain() []*x509.Certificate {
return append([]*x509.Certificate{fi.Leaf, fi.CA}, fi.RestChain...)
}
// RawChain returns all of the certificate chain as a 2d byte slice
func (fi *FullIdentity) RawChain() [][]byte {
chain := fi.Chain()
rawChain := make([][]byte, len(chain))
for i, cert := range chain {
rawChain[i] = cert.Raw
}
return rawChain
}
// RawRestChain returns the rest (excluding leaf and CA) of the certificate chain as a 2d byte slice
func (fi *FullIdentity) RawRestChain() [][]byte {
rawChain := make([][]byte, len(fi.RestChain))
for _, cert := range fi.RestChain {
rawChain = append(rawChain, cert.Raw)
}
return rawChain
}
// PeerIdentity converts a FullIdentity into a PeerIdentity
func (fi *FullIdentity) PeerIdentity() *PeerIdentity {
return &PeerIdentity{
CA: fi.CA,
Leaf: fi.Leaf,
ID: fi.ID,
RestChain: fi.RestChain,
}
}
// Version looks up the version based on the certificate's ID version extension.
func (fi *FullIdentity) Version() (storj.IDVersion, error) {
return storj.IDVersionFromCert(fi.CA)
}
// AddExtension adds extensions to the leaf cert of an identity. Extensions
// are serialized into the certificate's raw bytes and is re-signed by it's
// certificate authority.
func (manageableIdent *ManageablePeerIdentity) AddExtension(ext ...pkix.Extension) error {
if err := extensions.AddExtraExtension(manageableIdent.Leaf, ext...); err != nil {
return err
}
updatedCert, err := peertls.CreateCertificate(manageableIdent.Leaf.PublicKey, manageableIdent.CA.Key, manageableIdent.Leaf, manageableIdent.CA.Cert)
if err != nil {
return err
}
manageableIdent.Leaf = updatedCert
return nil
}
// Revoke extends the CA certificate with a certificate revocation extension.
func (manageableIdent *ManageableFullIdentity) Revoke() error {
ext, err := extensions.NewRevocationExt(manageableIdent.CA.Key, manageableIdent.Leaf)
if err != nil {
return err
}
revokingIdent, err := manageableIdent.CA.NewIdentity(ext)
if err != nil {
return err
}
manageableIdent.Leaf = revokingIdent.Leaf
return nil
}
func backupPath(path string) string {
pathExt := filepath.Ext(path)
base := strings.TrimSuffix(path, pathExt)
return fmt.Sprintf(
"%s.%s%s",
base,
strconv.Itoa(int(time.Now().Unix())),
pathExt,
)
}
// EncodePeerIdentity encodes the complete identity chain to bytes
func EncodePeerIdentity(pi *PeerIdentity) []byte {
var chain []byte
chain = append(chain, pi.Leaf.Raw...)
chain = append(chain, pi.CA.Raw...)
for _, cert := range pi.RestChain {
chain = append(chain, cert.Raw...)
}
return chain
}
// DecodePeerIdentity Decodes the bytes into complete identity chain
func DecodePeerIdentity(ctx context.Context, chain []byte) (_ *PeerIdentity, err error) {
defer mon.Task()(&ctx)(&err)
var certs []*x509.Certificate
for len(chain) > 0 {
var raw asn1.RawValue
var err error
chain, err = asn1.Unmarshal(chain, &raw)
if err != nil {
return nil, Error.Wrap(err)
}
cert, err := pkcrypto.CertFromDER(raw.FullBytes)
if err != nil {
return nil, Error.Wrap(err)
}
certs = append(certs, cert)
}
if len(certs) < 2 {
return nil, Error.New("not enough certificates")
}
return PeerIdentityFromChain(certs)
}