storj/pkg/identity/identity.go
Egon Elbre 00b2e1a7d7 all: enable staticcheck (#2849)
* by having megacheck in disable it also disabled staticcheck

* fix closing body

* keep interfacer disabled

* hide bodies

* don't use deprecated func

* fix dead code

* fix potential overrun

* keep stylecheck disabled

* don't pass nil as context

* fix infinite recursion

* remove extraneous return

* fix data race

* use correct func

* ignore unused var

* remove unused consts
2019-08-22 13:40:15 +02:00

567 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"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/peer"
"storj.io/storj/pkg/peertls"
"storj.io/storj/pkg/peertls/extensions"
"storj.io/storj/pkg/pkcrypto"
"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 *peer.Peer) (*PeerIdentity, error) {
if peer.AuthInfo == nil {
return nil, Error.New("peer AuthInfo is nil")
}
tlsInfo, ok := peer.AuthInfo.(credentials.TLSInfo)
if !ok {
return nil, Error.New("peer AuthInfo is not credentials.TLSInfo")
}
chain := tlsInfo.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) {
p, ok := peer.FromContext(ctx)
if !ok {
return nil, Error.New("unable to get grpc peer from contex")
}
return PeerIdentityFromPeer(p)
}
// 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 {
var (
certData bytes.Buffer
writeChainErr, writeChainDataErr error
)
chain := []*x509.Certificate{peerIdent.Leaf, peerIdent.CA}
chain = append(chain, peerIdent.RestChain...)
if ic.CertPath != "" {
writeChainErr = peertls.WriteChain(&certData, chain...)
writeChainDataErr = writeChainData(ic.CertPath, certData.Bytes())
}
writeErr := errs.Combine(writeChainErr)
if writeErr != nil {
return writeErr
}
return errs.Combine(
writeChainDataErr,
)
}
// 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)
}