storj/pkg/peertls/extensions.go
paul cannon c35b93766d
Unite all cryptographic signing and verifying (#1244)
this change removes the cryptopasta dependency.

a couple possible sources of problem with this change:

 * the encoding used for ECDSA signatures on SignedMessage has changed.
   the encoding employed by cryptopasta was workable, but not the same
   as the encoding used for such signatures in the rest of the world
   (most particularly, on ECDSA signatures in X.509 certificates). I
   think we'll be best served by using one ECDSA signature encoding from
   here on, but if we need to use the old encoding for backwards
   compatibility with existing nodes, that can be arranged.

 * since there's already a breaking change in SignedMessage, I changed
   it to send and receive public keys in raw PKIX format, instead of
   PEM. PEM just adds unhelpful overhead for this case.
2019-02-07 14:39:20 -06:00

307 lines
9.4 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package peertls
import (
"bytes"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/binary"
"encoding/gob"
"time"
"github.com/zeebo/errs"
"storj.io/storj/pkg/pkcrypto"
)
const (
// SignedCertExtID is the asn1 object ID for a pkix extensionHandler holding a
// signature of the cert it's extending, signed by some CA (e.g. the root cert chain).
// This extensionHandler allows for an additional signature per certificate.
SignedCertExtID = iota
// RevocationExtID is the asn1 object ID for a pkix extensionHandler containing the
// most recent certificate revocation data
// for the current TLS cert chain.
RevocationExtID
// RevocationBucket is the bolt bucket to store revocation data in
RevocationBucket = "revocations"
)
const (
// LeafIndex is the index of the leaf certificate in a cert chain (0)
LeafIndex = iota
// CAIndex is the index of the CA certificate in a cert chain (1)
CAIndex
)
var (
// ExtensionIDs is a map from an enum to object identifiers used in peertls extensions.
ExtensionIDs = map[int]asn1.ObjectIdentifier{
// NB: 2.999.X is reserved for "example" OIDs
// (see http://oid-info.com/get/2.999)
SignedCertExtID: {2, 999, 1, 1},
RevocationExtID: {2, 999, 1, 2},
}
// ErrExtension is used when an error occurs while processing an extensionHandler.
ErrExtension = errs.Class("extension error")
// ErrRevocation is used when an error occurs involving a certificate revocation
ErrRevocation = errs.Class("revocation processing error")
// ErrRevocationDB is used when an error occurs involving the revocations database
ErrRevocationDB = errs.Class("revocation database error")
// ErrRevokedCert is used when a certificate in the chain is revoked and not expected to be
ErrRevokedCert = ErrRevocation.New("a certificate in the chain is revoked")
// ErrUniqueExtensions is used when multiple extensions have the same Id
ErrUniqueExtensions = ErrExtension.New("extensions are not unique")
// ErrRevocationTimestamp is used when a revocation's timestamp is older than the last recorded revocation
ErrRevocationTimestamp = ErrExtension.New("revocation timestamp is older than last known revocation")
)
// TLSExtConfig is used to bind cli flags for determining which extensions will
// be used by the server
type TLSExtConfig struct {
Revocation bool `help:"if true, client leaves may contain the most recent certificate revocation for the current certificate" default:"true"`
WhitelistSignedLeaf bool `help:"if true, client leaves must contain a valid \"signed certificate extension\" (NB: verified against certs in the peer ca whitelist; i.e. if true, a whitelist must be provided)" default:"false"`
}
// ExtensionHandlers is a collection of `extensionHandler`s for convenience (see `VerifyFunc`)
type ExtensionHandlers []ExtensionHandler
type extensionVerificationFunc func(pkix.Extension, [][]*x509.Certificate) error
// ExtensionHandler represents a verify function for handling an extension
// with the given ID
type ExtensionHandler struct {
ID asn1.ObjectIdentifier
Verify extensionVerificationFunc
}
// ParseExtOptions holds options for calling `ParseExtensions`
type ParseExtOptions struct {
CAWhitelist []*x509.Certificate
RevDB RevocationDB
}
// Revocation represents a certificate revocation for storage in the revocation
// database and for use in a TLS extension
type Revocation struct {
Timestamp int64
CertHash []byte
Signature []byte
}
// RevocationDB stores certificate revocation data.
type RevocationDB interface {
Get(chain []*x509.Certificate) (*Revocation, error)
Put(chain []*x509.Certificate, ext pkix.Extension) error
List() ([]*Revocation, error)
Close() error
}
// ParseExtensions parses an extension config into a slice of extension handlers
// with their respective ids (`asn1.ObjectIdentifier`) and a "verify" function
// to be used in the context of peer certificate verification.
func ParseExtensions(c TLSExtConfig, opts ParseExtOptions) (handlers ExtensionHandlers) {
if c.WhitelistSignedLeaf {
handlers = append(handlers, ExtensionHandler{
ID: ExtensionIDs[SignedCertExtID],
Verify: verifyCAWhitelistSignedLeafFunc(opts.CAWhitelist),
})
}
if c.Revocation {
handlers = append(handlers, ExtensionHandler{
ID: ExtensionIDs[RevocationExtID],
Verify: func(certExt pkix.Extension, chains [][]*x509.Certificate) error {
if err := opts.RevDB.Put(chains[0], certExt); err != nil {
return err
}
return nil
},
})
}
return handlers
}
// NewRevocationExt generates a revocation extension for a certificate.
func NewRevocationExt(key crypto.PrivateKey, revokedCert *x509.Certificate) (pkix.Extension, error) {
nowUnix := time.Now().Unix()
hash := pkcrypto.SHA256Hash(revokedCert.Raw)
rev := Revocation{
Timestamp: nowUnix,
CertHash: make([]byte, len(hash)),
}
copy(rev.CertHash, hash)
if err := rev.Sign(key); err != nil {
return pkix.Extension{}, err
}
revBytes, err := rev.Marshal()
if err != nil {
return pkix.Extension{}, err
}
ext := pkix.Extension{
Id: ExtensionIDs[RevocationExtID],
Value: make([]byte, len(revBytes)),
}
copy(ext.Value, revBytes)
return ext, nil
}
// AddRevocationExt generates a revocation extension for a cert and attaches it
// to the cert which will replace the revoked cert.
func AddRevocationExt(key crypto.PrivateKey, revokedCert, newCert *x509.Certificate) error {
ext, err := NewRevocationExt(key, revokedCert)
if err != nil {
return err
}
err = AddExtension(newCert, ext)
if err != nil {
return err
}
return nil
}
// AddSignedCertExt generates a signed certificate extension for a cert and attaches
// it to that cert.
func AddSignedCertExt(key crypto.PrivateKey, cert *x509.Certificate) error {
signature, err := pkcrypto.HashAndSign(key, cert.RawTBSCertificate)
if err != nil {
return err
}
err = AddExtension(cert, pkix.Extension{
Id: ExtensionIDs[SignedCertExtID],
Value: signature,
})
if err != nil {
return err
}
return nil
}
// AddExtension adds one or more extensions to a certificate
func AddExtension(cert *x509.Certificate, exts ...pkix.Extension) (err error) {
if len(exts) == 0 {
return nil
}
if !uniqueExts(append(cert.ExtraExtensions, exts...)) {
return ErrUniqueExtensions
}
for _, ext := range exts {
e := pkix.Extension{Id: ext.Id, Value: make([]byte, len(ext.Value))}
copy(e.Value, ext.Value)
cert.ExtraExtensions = append(cert.ExtraExtensions, e)
}
return nil
}
// VerifyFunc returns a peer certificate verification function which iterates
// over all the leaf cert's extensions and receiver extensions and calls
// `extensionHandler#verify` when it finds a match by id (`asn1.ObjectIdentifier`)
func (e ExtensionHandlers) VerifyFunc() PeerCertVerificationFunc {
if len(e) == 0 {
return nil
}
return func(_ [][]byte, parsedChains [][]*x509.Certificate) error {
leafExts := make(map[string]pkix.Extension)
for _, ext := range parsedChains[0][LeafIndex].ExtraExtensions {
leafExts[ext.Id.String()] = ext
}
for _, handler := range e {
if ext, ok := leafExts[handler.ID.String()]; ok {
err := handler.Verify(ext, parsedChains)
if err != nil {
return ErrExtension.Wrap(err)
}
}
}
return nil
}
}
// Verify checks if the signature of the revocation was produced by the passed cert's public key.
func (r Revocation) Verify(signingCert *x509.Certificate) error {
pubKey, ok := signingCert.PublicKey.(crypto.PublicKey)
if !ok {
return pkcrypto.ErrUnsupportedKey.New("%T", signingCert.PublicKey)
}
data := r.TBSBytes()
if err := pkcrypto.HashAndVerifySignature(pubKey, data, r.Signature); err != nil {
return err
}
return nil
}
// TBSBytes (ToBeSigned) returns the hash of the revoked certificate hash and
// the timestamp (i.e. hash(hash(cert bytes) + timestamp)).
func (r *Revocation) TBSBytes() []byte {
var tsBytes [binary.MaxVarintLen64]byte
binary.PutVarint(tsBytes[:], r.Timestamp)
toHash := append(append([]byte{}, r.CertHash...), tsBytes[:]...)
return pkcrypto.SHA256Hash(toHash)
}
// Sign generates a signature using the passed key and attaches it to the revocation.
func (r *Revocation) Sign(key crypto.PrivateKey) error {
data := r.TBSBytes()
sig, err := pkcrypto.HashAndSign(key, data)
if err != nil {
return err
}
r.Signature = sig
return nil
}
// Marshal serializes a revocation to bytes
func (r Revocation) Marshal() ([]byte, error) {
data := new(bytes.Buffer)
// NB: using gob instead of asn1 because we plan to leave tls and asn1 is meh
encoder := gob.NewEncoder(data)
err := encoder.Encode(r)
if err != nil {
return nil, err
}
return data.Bytes(), nil
}
// Unmarshal deserializes a revocation from bytes
func (r *Revocation) Unmarshal(data []byte) error {
// NB: using gob instead of asn1 because we plan to leave tls and asn1 is meh
decoder := gob.NewDecoder(bytes.NewBuffer(data))
if err := decoder.Decode(r); err != nil {
return err
}
return nil
}
func verifyCAWhitelistSignedLeafFunc(caWhitelist []*x509.Certificate) extensionVerificationFunc {
return func(certExt pkix.Extension, chains [][]*x509.Certificate) error {
if caWhitelist == nil {
return ErrVerifyCAWhitelist.New("no whitelist provided")
}
leaf := chains[0][LeafIndex]
for _, ca := range caWhitelist {
err := pkcrypto.HashAndVerifySignature(ca.PublicKey, leaf.RawTBSCertificate, certExt.Value)
if err == nil {
return nil
}
}
return ErrVerifyCAWhitelist.New("leaf extension")
}
}