Certificate revocation (#836)

* wip certificate revocation

* refactor tests

* wip testing

* testing

* review fixes

* integration fix attempt #1

* review fixes

* integration fix attempt #2

* linter fixes

* add copywrite

* integration fix attemp #3

* more testing

* more tests

* go mod tidy

* review fixes

* linter fixes
This commit is contained in:
Bryan White 2018-12-13 21:01:43 +01:00 committed by GitHub
parent 6f634d9f02
commit 2016ce9fd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1387 additions and 315 deletions

View File

@ -55,6 +55,10 @@ check-copyrights: ## Check source files for copyright headers
goimports-fix: ## Applies goimports to every go file (excluding vendored files)
goimports -w -local storj.io $$(find . -type f -name '*.go' -not -path "*/vendor/*")
.PHONY: goimports-st
goimports-st: ## Applies goimports to every go file in `git status` (ignores untracked files)
git status --porcelain -uno|grep .go|sed -E 's,\w+\s+,,g'|xargs -I {} goimports -w -local storj.io {}
.PHONY: proto
proto: ## Rebuild protobuf files
@echo "Running ${@}"

View File

@ -8,6 +8,7 @@ import (
"net"
"os"
"path/filepath"
"regexp"
"github.com/spf13/cobra"
@ -50,6 +51,7 @@ func init() {
}
func cmdSetup(cmd *cobra.Command, args []string) (err error) {
redisURL := regexp.MustCompile("^redis://")
setupCfg.BasePath, err = filepath.Abs(setupCfg.BasePath)
if err != nil {
return err
@ -110,6 +112,9 @@ func cmdSetup(cmd *cobra.Command, args []string) (err error) {
setupCfg.UplinkCA.KeyPath = filepath.Join(uplinkPath, "ca.key")
setupCfg.UplinkIdentity.CertPath = filepath.Join(uplinkPath, "identity.cert")
setupCfg.UplinkIdentity.KeyPath = filepath.Join(uplinkPath, "identity.key")
if redisURL.MatchString(setupCfg.SatelliteIdentity.Server.RevocationDBURL) {
setupCfg.UplinkIdentity.Server.RevocationDBURL = setupCfg.SatelliteIdentity.Server.RevocationDBURL
}
fmt.Printf("creating identity for uplink\n")
err = provider.SetupIdentity(process.Ctx(cmd), setupCfg.UplinkCA, setupCfg.UplinkIdentity)
if err != nil {
@ -138,6 +143,7 @@ func cmdSetup(cmd *cobra.Command, args []string) (err error) {
"satellite.identity.key-path": setupCfg.SatelliteIdentity.KeyPath,
"satellite.identity.server.address": joinHostPort(
setupCfg.ListenHost, startingPort+1),
"satellite.identity.server.revocation-dburl": setupCfg.SatelliteIdentity.Server.RevocationDBURL,
"satellite.kademlia.bootstrap-addr": joinHostPort(
setupCfg.ListenHost, startingPort+1),
"satellite.pointer-db.database-url": "bolt://" + filepath.Join(
@ -154,6 +160,7 @@ func cmdSetup(cmd *cobra.Command, args []string) (err error) {
"uplink.identity.key-path": setupCfg.UplinkIdentity.KeyPath,
"uplink.identity.server.address": joinHostPort(
setupCfg.ListenHost, startingPort),
"uplink.identity.server.revocation-dburl": setupCfg.SatelliteIdentity.Server.RevocationDBURL,
"uplink.client.overlay-addr": joinHostPort(
setupCfg.ListenHost, startingPort+1),
"uplink.client.pointer-db-addr": joinHostPort(
@ -189,6 +196,9 @@ func cmdSetup(cmd *cobra.Command, args []string) (err error) {
storagenodePath, "identity.key")
overrides[storagenode+"identity.server.address"] = joinHostPort(
setupCfg.ListenHost, startingPort+i*2+3)
if redisURL.MatchString(setupCfg.SatelliteIdentity.Server.RevocationDBURL) {
overrides[storagenode+"identity.server.revocation-dburl"] = setupCfg.SatelliteIdentity.Server.RevocationDBURL
}
overrides[storagenode+"kademlia.bootstrap-addr"] = joinHostPort(
setupCfg.ListenHost, startingPort+1)
overrides[storagenode+"storage.path"] = filepath.Join(storagenodePath, "data")

1
go.mod
View File

@ -94,6 +94,7 @@ require (
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.2.1
github.com/streadway/amqp v0.0.0-20180806233856-70e15c650864 // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2
github.com/tidwall/gjson v1.1.3 // indirect
github.com/tidwall/match v0.0.0-20171002075945-1731857f09b1 // indirect

6
go.sum
View File

@ -32,8 +32,6 @@ github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoA
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/cheggaaa/pb v1.0.5-0.20160713104425-73ae1d68fe0b h1:CMRCnhHx4xVxJy+wPsS67xmi9RHGNctLMoVn9Q1Kit8=
github.com/cheggaaa/pb v1.0.5-0.20160713104425-73ae1d68fe0b/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
github.com/ckaznocha/protoc-gen-lint v0.2.1 h1:wP+SgbHat4ovpPQayCroxK/1pXtnBR4HIo9G+2gTnHU=
github.com/ckaznocha/protoc-gen-lint v0.2.1/go.mod h1:EveTCMo4KBPAmWqVxMXUDrI/iV6v93ydJyZVdEYyFIg=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry/gosigar v1.1.0 h1:V/dVCzhKOdIU3WRB5inQU20s4yIgL9Dxx/Mhi0SF8eM=
github.com/cloudfoundry/gosigar v1.1.0/go.mod h1:3qLfc2GlfmwOx2+ZDaRGH3Y9fwQ0sQeaAleo2GV5pH0=
@ -98,8 +96,6 @@ github.com/go-redis/redis v6.14.1+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/gocql/gocql v0.0.0-20180913072538-864d5908455a/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.2-0.20181116123445-07eab6a8298c h1:c8VQNu/587ErbVKJSz6kKVdrf3kS18Sn50UShPyJ7Wc=
github.com/gogo/protobuf v1.1.2-0.20181116123445-07eab6a8298c/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-migrate/migrate/v3 v3.5.2 h1:SUWSv6PD8Lr2TGx1lmVW7W2lRoQiVny3stM4He6jczQ=
@ -312,6 +308,8 @@ github.com/spf13/viper v1.2.1 h1:bIcUwXqLseLF3BDAZduuNfekWG87ibtFxi59Bq+oI9M=
github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
github.com/streadway/amqp v0.0.0-20180806233856-70e15c650864 h1:Oj3PUEs+OUSYUpn35O+BE/ivHGirKixA3+vqA0Atu9A=
github.com/streadway/amqp v0.0.0-20180806233856-70e15c650864/go.mod h1:1WNBiOZtZQLpVAyu0iTduoJL9hEsMloAK5XWrtW0xdY=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tidwall/gjson v1.1.3 h1:u4mspaByxY+Qk4U1QYYVzGFI8qxN/3jtEV0ZDb2vRic=

View File

@ -0,0 +1,105 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package testpeertls
import (
"bytes"
"crypto/ecdsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"fmt"
"math/big"
)
// DebugCert is a subset of the most relevant fields from an x509.Certificate for debugging
type DebugCert struct {
Raw []byte
RawTBSCertificate []byte
Signature []byte
PublicKeyX *big.Int
PublicKeyY *big.Int
Extensions []pkix.Extension
}
// NewCertDebug converts an *x509.Certificate into a DebugCert
func NewCertDebug(cert x509.Certificate) DebugCert {
pubKey := cert.PublicKey.(*ecdsa.PublicKey)
c := DebugCert{
Raw: make([]byte, len(cert.Raw)),
RawTBSCertificate: make([]byte, len(cert.RawTBSCertificate)),
Signature: make([]byte, len(cert.Signature)),
PublicKeyX: pubKey.X,
PublicKeyY: pubKey.Y,
Extensions: []pkix.Extension{},
}
copy(c.Raw, cert.Raw)
copy(c.RawTBSCertificate, cert.RawTBSCertificate)
copy(c.Signature, cert.Signature)
for _, e := range cert.ExtraExtensions {
ext := pkix.Extension{Id: e.Id, Value: make([]byte, len(e.Value))}
copy(ext.Value, e.Value)
c.Extensions = append(c.Extensions, ext)
}
return c
}
// Cmp is used to compare 2 DebugCerts against each other and print the diff
func (c DebugCert) Cmp(c2 DebugCert, label string) {
fmt.Println("diff " + label + " ---================================================================---")
cmpBytes := func(a, b []byte) {
PrintJSON(bytes.Compare(a, b), "")
}
cmpBytes(c.Raw, c2.Raw)
cmpBytes(c.RawTBSCertificate, c2.RawTBSCertificate)
cmpBytes(c.Signature, c2.Signature)
c.PublicKeyX.Cmp(c2.PublicKeyX)
c.PublicKeyY.Cmp(c2.PublicKeyY)
}
// PrintJSON uses a json marshaler to pretty-print arbitrary data for debugging
// with special considerations for certain, specific types
func PrintJSON(data interface{}, label string) {
var (
jsonBytes []byte
err error
)
switch d := data.(type) {
case x509.Certificate:
data = NewCertDebug(d)
case *x509.Certificate:
data = NewCertDebug(*d)
case ecdsa.PublicKey:
data = struct {
X *big.Int
Y *big.Int
}{
d.X, d.Y,
}
case *ecdsa.PrivateKey:
data = struct {
X *big.Int
Y *big.Int
D *big.Int
}{
d.X, d.Y, d.D,
}
}
jsonBytes, err = json.MarshalIndent(data, "", "\t\t")
if label != "" {
fmt.Println(label + ": ---================================================================---")
}
if err != nil {
fmt.Printf("ERROR: %s", err.Error())
}
fmt.Println(string(jsonBytes))
fmt.Println("")
}

View File

@ -4,13 +4,62 @@
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/utils"
"storj.io/storj/storage"
"storj.io/storj/storage/boltdb"
"storj.io/storj/storage/redis"
)
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 is revoked and not expected to be
ErrRevokedCert = ErrRevocation.New("leaf certificate 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
@ -20,59 +69,330 @@ type TLSExtConfig struct {
WhitelistSignedLeaf bool `help:"if true, client leafs 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"`
}
// Extensions is a collection of `extension`s for convenience (see `VerifyFunc`)
type Extensions []extension
// ExtensionHandlers is a collection of `extensionHandler`s for convenience (see `VerifyFunc`)
type ExtensionHandlers []extensionHandler
type extension struct {
id asn1.ObjectIdentifier
f func(pkix.Extension, [][]*x509.Certificate) (bool, error)
err error
type extensionVerificationFunc func(pkix.Extension, [][]*x509.Certificate) error
type extensionHandler struct {
id asn1.ObjectIdentifier
verify extensionVerificationFunc
}
// ParseExtensions an extension config into a slice of extensions with their
// respective ids (`asn1.ObjectIdentifier`) and a function (`f`) which can be
// used in the context of peer certificate verification.
func ParseExtensions(c TLSExtConfig, caWhitelist []*x509.Certificate) (exts Extensions) {
// 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 the most recently seen revocation for each nodeID
// (i.e. nodeID [CA certificate hash] is the key, value is the most
// recently seen revocation).
type RevocationDB struct {
DB storage.KeyValueStore
}
// 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 {
exts = append(exts, extension{
id: ExtensionIDs[SignedCertExtID],
f: func(certExt pkix.Extension, chains [][]*x509.Certificate) (bool, error) {
if caWhitelist == nil {
return false, errs.New("whitelist required for leaf whitelist signature verification")
}
leaf := chains[0][0]
for _, ca := range caWhitelist {
err := VerifySignature(certExt.Value, leaf.RawTBSCertificate, ca.PublicKey)
if err == nil {
return true, nil
}
}
return false, nil
},
err: ErrVerifyCAWhitelist.New("leaf whitelist signature extension verification error"),
handlers = append(handlers, extensionHandler{
id: ExtensionIDs[SignedCertExtID],
verify: verifyCAWhitelistSignedLeafFunc(opts.CAWhitelist),
})
}
return exts
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
}
// NewRevocationDBBolt creates a bolt-backed RevocationDB
func NewRevocationDBBolt(path string) (*RevocationDB, error) {
client, err := boltdb.New(path, RevocationBucket)
if err != nil {
return nil, err
}
return &RevocationDB{
DB: client,
}, nil
}
// NewRevocationDBRedis creates a redis-backed RevocationDB.
func NewRevocationDBRedis(address string) (*RevocationDB, error) {
client, err := redis.NewClientFrom(address)
if err != nil {
return nil, err
}
return &RevocationDB{
DB: client,
}, nil
}
// NewRevocationExt generates a revocation extension for a certificate.
func NewRevocationExt(key crypto.PrivateKey, revokedCert *x509.Certificate) (pkix.Extension, error) {
nowUnix := time.Now().Unix()
hash, err := hashBytes(revokedCert.Raw)
if err != nil {
return pkix.Extension{}, err
}
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 := signHashOf(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
// `extension#f` when it finds a match by id (`asn1.ObjectIdentifier`)
func (e Extensions) VerifyFunc() PeerCertVerificationFunc {
// `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 {
for _, ext := range parsedChains[0][0].Extensions {
for _, v := range e {
if v.id.Equal(ext.Id) {
ok, err := v.f(ext, parsedChains)
if err != nil {
return ErrExtension.Wrap(utils.CombineErrors(v.err, err))
} else if !ok {
return v.err
}
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
}
}
// Get attempts to retrieve the most recent revocation for the given cert chain
// (the key used in the underlying database is the hash of the CA cert bytes).
func (r RevocationDB) Get(chain []*x509.Certificate) (*Revocation, error) {
hash, err := hashBytes(chain[CAIndex].Raw)
if err != nil {
return nil, ErrRevocation.Wrap(err)
}
revBytes, err := r.DB.Get(hash)
if err != nil && !storage.ErrKeyNotFound.Has(err) {
return nil, ErrRevocationDB.Wrap(err)
}
if revBytes == nil {
return nil, nil
}
rev := new(Revocation)
if err = rev.Unmarshal(revBytes); err != nil {
return rev, ErrRevocationDB.Wrap(err)
}
return rev, nil
}
// Put stores the most recent revocation for the given cert chain IF the timestamp
// is newer than the current value (the key used in the underlying database is
// the hash of the CA cert bytes).
func (r RevocationDB) Put(chain []*x509.Certificate, revExt pkix.Extension) error {
ca := chain[CAIndex]
var rev Revocation
if err := rev.Unmarshal(revExt.Value); err != nil {
return err
}
// TODO: what happens if cert/timestamp/sig is empty/garbage?
if err := rev.Verify(ca); err != nil {
return err
}
lastRev, err := r.Get(chain)
if err != nil {
return err
} else if lastRev != nil && lastRev.Timestamp >= rev.Timestamp {
return ErrRevocationTimestamp
}
hash, err := hashBytes(ca.Raw)
if err != nil {
return err
}
if err := r.DB.Put(hash, revExt.Value); err != nil {
return 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 ErrUnsupportedKey.New("%T", signingCert.PublicKey)
}
data, err := r.TBSBytes()
if err != nil {
return err
}
if err := VerifySignature(r.Signature, data, pubKey); 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, error) {
toHash := new(bytes.Buffer)
_, err := toHash.Write(r.CertHash)
if err != nil {
return nil, ErrExtension.Wrap(err)
}
// NB: append timestamp to revoked cert bytes
binary.PutVarint(toHash.Bytes(), r.Timestamp)
return hashBytes(toHash.Bytes())
}
// Sign generates a signature using the passed key and attaches it to the revocation.
func (r *Revocation) Sign(key crypto.PrivateKey) error {
data, err := r.TBSBytes()
if err != nil {
return err
}
r.Signature, err = signHashOf(key, data)
if err != nil {
return err
}
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 := VerifySignature(certExt.Value, leaf.RawTBSCertificate, ca.PublicKey)
if err == nil {
return nil
}
}
return ErrVerifyCAWhitelist.New("leaf extension")
}
}

View File

@ -4,13 +4,12 @@
package peertls
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"io"
@ -27,23 +26,7 @@ const (
BlockTypeIDOptions = "ID OPTIONS"
)
const (
// SignedCertExtID is the asn1 object ID for a pkix extension holding a signature of the cert it's extending, signed by some CA (e.g. the root cert chain).
// This extension allows for an additional signature per certificate.
SignedCertExtID = iota
// RevocationExtID is the asn1 object ID for a pkix extension containing the most recent certificate revocation data
// for the current TLS cert chain.
RevocationExtID
)
var (
// ExtensionIDs is a map from an enum to extension object identifiers.
ExtensionIDs = map[int]asn1.ObjectIdentifier{
SignedCertExtID: {2, 999, 1, 1},
RevocationExtID: {2, 999, 2, 1},
}
// ErrExtension is used when an error occurs while processing an extension.
ErrExtension = errs.Class("extension error")
// ErrNotExist is used when a file or directory doesn't exist.
ErrNotExist = errs.Class("file or directory not found error")
// ErrGenerate is used when an error occurred during cert/key generation.
@ -61,7 +44,7 @@ var (
// ErrVerifyCertificateChain is used when a certificate chain can't be verified from leaf to root
// (i.e.: each cert in the chain should be signed by the preceding cert and the root should be self-signed).
ErrVerifyCertificateChain = errs.Class("certificate chain signature verification failed")
// ErrVerifyCAWhitelist is used when the leaf of a peer certificate isn't signed by any CA in the whitelist.
// ErrVerifyCAWhitelist is used when a signature wasn't produced by any CA in the whitelist.
ErrVerifyCAWhitelist = errs.Class("not signed by any CA in the whitelist")
// ErrSign is used when something goes wrong while generating a signature.
ErrSign = errs.Class("unable to generate signature")
@ -107,20 +90,20 @@ func VerifyPeerCertChains(_ [][]byte, parsedChains [][]*x509.Certificate) error
return verifyChainSignatures(parsedChains[0])
}
// VerifyCAWhitelist verifies that the peer identity's CA and leaf-extension was signed
// by any one of the (certificate authority) certificates in the provided whitelist.
// VerifyCAWhitelist verifies that the peer identity's CA was signed by any one
// of the (certificate authority) certificates in the provided whitelist.
func VerifyCAWhitelist(cas []*x509.Certificate) PeerCertVerificationFunc {
if cas == nil {
return nil
}
return func(_ [][]byte, parsedChains [][]*x509.Certificate) error {
for _, ca := range cas {
err := verifyCertSignature(ca, parsedChains[0][1])
err := verifyCertSignature(ca, parsedChains[0][CAIndex])
if err == nil {
return nil
}
}
return ErrVerifyCAWhitelist.New("extension signature doesn't match any CA in the whitelist")
return ErrVerifyCAWhitelist.New("CA cert")
}
}
@ -227,32 +210,28 @@ func NewCert(key, parentKey crypto.PrivateKey, template, parent *x509.Certificat
return cert, nil
}
// AddSignedLeafExt adds a "signed certificate extension" to the passed cert,
// using the passed private key.
func AddSignedLeafExt(key crypto.PrivateKey, cert *x509.Certificate) error {
ecKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
return ErrUnsupportedKey.New("%T", key)
}
// VerifyUnrevokedChainFunc returns a peer certificate verification function which
// returns an error if the incoming cert chain contains a revoked CA or leaf.
func VerifyUnrevokedChainFunc(revDB *RevocationDB) PeerCertVerificationFunc {
return func(_ [][]byte, chains [][]*x509.Certificate) error {
leaf := chains[0][LeafIndex]
ca := chains[0][CAIndex]
lastRev, lastRevErr := revDB.Get(chains[0])
if lastRevErr != nil {
return ErrExtension.Wrap(lastRevErr)
}
if lastRev == nil {
return nil
}
hash := crypto.SHA256.New()
_, err := hash.Write(cert.RawTBSCertificate)
if err != nil {
return ErrSign.Wrap(err)
}
r, s, err := ecdsa.Sign(rand.Reader, ecKey, hash.Sum(nil))
if err != nil {
return ErrSign.Wrap(err)
}
if bytes.Equal(lastRev.CertHash, ca.Raw) || bytes.Equal(lastRev.CertHash, leaf.Raw) {
lastRevErr := lastRev.Verify(ca)
if lastRevErr != nil {
return ErrExtension.Wrap(lastRevErr)
}
return ErrRevokedCert
}
signature, err := asn1.Marshal(ECDSASignature{R: r, S: s})
if err != nil {
return ErrSign.Wrap(err)
return nil
}
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
Id: ExtensionIDs[SignedCertExtID],
Value: signature,
})
return nil
}

View File

@ -5,11 +5,21 @@ package peertls
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/gob"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/zeebo/errs"
)
@ -61,23 +71,11 @@ func TestNewCert_Leaf(t *testing.T) {
}
func TestVerifyPeerFunc(t *testing.T) {
caKey, err := NewKey()
assert.NoError(t, err)
caTemplate, err := CATemplate()
assert.NoError(t, err)
caCert, err := NewCert(caKey, nil, caTemplate, nil)
assert.NoError(t, err)
leafKey, err := NewKey()
assert.NoError(t, err)
leafTemplate, err := LeafTemplate()
assert.NoError(t, err)
leafCert, err := NewCert(leafKey, caKey, leafTemplate, caCert)
assert.NoError(t, err)
_, chain, err := newCertChain(2)
if !assert.NoError(t, err) {
t.FailNow()
}
leafCert, caCert := chain[0], chain[1]
testFunc := func(chain [][]byte, parsedChains [][]*x509.Certificate) error {
switch {
@ -104,23 +102,11 @@ func TestVerifyPeerFunc(t *testing.T) {
}
func TestVerifyPeerCertChains(t *testing.T) {
caKey, err := NewKey()
assert.NoError(t, err)
caTemplate, err := CATemplate()
assert.NoError(t, err)
caCert, err := NewCert(caKey, nil, caTemplate, nil)
assert.NoError(t, err)
leafKey, err := NewKey()
assert.NoError(t, err)
leafTemplate, err := LeafTemplate()
assert.NoError(t, err)
leafCert, err := NewCert(leafKey, caKey, leafTemplate, caCert)
assert.NoError(t, err)
keys, chain, err := newCertChain(2)
if !assert.NoError(t, err) {
t.FailNow()
}
leafKey, leafCert, caCert := keys[1], chain[0], chain[1]
err = VerifyPeerFunc(VerifyPeerCertChains)([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.NoError(t, err)
@ -128,7 +114,7 @@ func TestVerifyPeerCertChains(t *testing.T) {
wrongKey, err := NewKey()
assert.NoError(t, err)
leafCert, err = NewCert(leafKey, wrongKey, leafTemplate, caCert)
leafCert, err = NewCert(leafKey, wrongKey, leafCert, caCert)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyPeerCertChains)([][]byte{leafCert.Raw, caCert.Raw}, nil)
@ -137,88 +123,119 @@ func TestVerifyPeerCertChains(t *testing.T) {
}
func TestVerifyCAWhitelist(t *testing.T) {
caKey, err := NewKey()
_, chain2, err := newCertChain(2)
if !assert.NoError(t, err) {
t.FailNow()
}
leafCert, caCert := chain2[0], chain2[1]
t.Run("empty whitelist", func(t *testing.T) {
err = VerifyPeerFunc(VerifyCAWhitelist(nil))([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.NoError(t, err)
})
t.Run("whitelist contains ca", func(t *testing.T) {
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{caCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.NoError(t, err)
})
_, unrelatedChain, err := newCertChain(1)
if !assert.NoError(t, err) {
t.FailNow()
}
unrelatedCert := unrelatedChain[0]
t.Run("no valid signed extension, non-empty whitelist", func(t *testing.T) {
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{unrelatedCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.True(t, ErrVerifyCAWhitelist.Has(err))
})
t.Run("last cert in whitelist is signer", func(t *testing.T) {
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{unrelatedCert, caCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.NoError(t, err)
})
t.Run("first cert in whitelist is signer", func(t *testing.T) {
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{caCert, unrelatedCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.NoError(t, err)
})
_, chain3, err := newCertChain(3)
if !assert.NoError(t, err) {
t.FailNow()
}
leaf2Cert, ca2Cert, rootCert := chain3[0], chain3[1], chain3[2]
t.Run("length 3 chain - first cert in whitelist is signer", func(t *testing.T) {
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{rootCert, unrelatedCert}))([][]byte{leaf2Cert.Raw, ca2Cert.Raw, unrelatedCert.Raw}, nil)
assert.NoError(t, err)
})
t.Run("length 3 chain - last cert in whitelist is signer", func(t *testing.T) {
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{unrelatedCert, rootCert}))([][]byte{leaf2Cert.Raw, ca2Cert.Raw, unrelatedCert.Raw}, nil)
assert.NoError(t, err)
})
}
func TestAddExtension(t *testing.T) {
_, chain, err := newCertChain(1)
if !assert.NoError(t, err) {
t.FailNow()
}
// NB: there's nothing special about length 32
randBytes := make([]byte, 32)
exampleID := asn1.ObjectIdentifier{2, 999}
i, err := rand.Read(randBytes)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, 32, i)
ext := pkix.Extension{
Id: exampleID,
Value: randBytes,
}
err = AddExtension(chain[0], ext)
assert.NoError(t, err)
assert.Len(t, chain[0].ExtraExtensions, 1)
assert.Equal(t, ext, chain[0].ExtraExtensions[0])
}
func TestAddSignedCertExt(t *testing.T) {
keys, chain, err := newCertChain(1)
if !assert.NoError(t, err) {
t.FailNow()
}
err = AddSignedCertExt(keys[0], chain[0])
assert.NoError(t, err)
caTemplate, err := CATemplate()
assert.NoError(t, err)
assert.Len(t, chain[0].ExtraExtensions, 1)
assert.Equal(t, ExtensionIDs[SignedCertExtID], chain[0].ExtraExtensions[0].Id)
caCert, err := NewCert(caKey, nil, caTemplate, nil)
assert.NoError(t, err)
ecKey, ok := keys[0].(*ecdsa.PrivateKey)
if !assert.True(t, ok) {
t.FailNow()
}
leafKey, err := NewKey()
assert.NoError(t, err)
leafTemplate, err := LeafTemplate()
assert.NoError(t, err)
leafCert, err := NewCert(leafKey, caKey, leafTemplate, caCert)
assert.NoError(t, err)
// empty whitelist
err = VerifyPeerFunc(VerifyCAWhitelist(nil))([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.NoError(t, err)
// whitelist contains ca
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{caCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.NoError(t, err)
rootKey, err := NewKey()
assert.NoError(t, err)
rootTemplate, err := CATemplate()
assert.NoError(t, err)
rootCert, err := NewCert(rootKey, nil, rootTemplate, nil)
assert.NoError(t, err)
// no valid signed extension, non-empty whitelist
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{rootCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.True(t, ErrVerifyCAWhitelist.Has(err))
// last cert in whitelist is signer
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{rootCert, caCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.NoError(t, err)
// first cert in whitelist is signer
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{caCert, rootCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
assert.NoError(t, err)
ca2Cert, err := NewCert(caKey, rootKey, caTemplate, rootCert)
assert.NoError(t, err)
leaf2Cert, err := NewCert(leafKey, caKey, leafTemplate, ca2Cert)
assert.NoError(t, err)
// length 3 chain; first cert in whitelist is signer
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{rootCert, caCert}))([][]byte{leaf2Cert.Raw, ca2Cert.Raw, rootCert.Raw}, nil)
assert.NoError(t, err)
// length 3 chain; last cert in whitelist is signer
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{caCert, rootCert}))([][]byte{leaf2Cert.Raw, ca2Cert.Raw, rootCert.Raw}, nil)
err = VerifySignature(
chain[0].ExtraExtensions[0].Value,
chain[0].RawTBSCertificate,
&ecKey.PublicKey,
)
assert.NoError(t, err)
}
func TestSignLeafExt(t *testing.T) {
caKey, err := NewKey()
assert.NoError(t, err)
keys, chain, err := newCertChain(2)
if !assert.NoError(t, err) {
t.FailNow()
}
caKey, leafCert := keys[0], chain[0]
caTemplate, err := CATemplate()
assert.NoError(t, err)
caCert, err := NewCert(caKey, nil, caTemplate, nil)
assert.NoError(t, err)
leafKey, err := NewKey()
assert.NoError(t, err)
leafTemplate, err := LeafTemplate()
assert.NoError(t, err)
leafCert, err := NewCert(leafKey, caKey, leafTemplate, caCert)
assert.NoError(t, err)
err = AddSignedLeafExt(caKey, leafCert)
err = AddSignedCertExt(caKey, leafCert)
assert.NoError(t, err)
assert.Equal(t, 1, len(leafCert.ExtraExtensions))
assert.True(t, ExtensionIDs[SignedCertExtID].Equal(leafCert.ExtraExtensions[0].Id))
@ -232,64 +249,525 @@ func TestSignLeafExt(t *testing.T) {
assert.NoError(t, err)
}
func TestParseExtensions(t *testing.T) {
type result struct {
ok bool
err error
func TestRevocation_Sign(t *testing.T) {
keys, chain, err := newCertChain(2)
assert.NoError(t, err)
leafCert, caKey := chain[0], keys[0]
leafHash, err := hashBytes(leafCert.Raw)
assert.NoError(t, err)
rev := Revocation{
Timestamp: time.Now().Unix(),
CertHash: make([]byte, len(leafHash)),
}
copy(rev.CertHash, leafHash)
err = rev.Sign(caKey)
assert.NoError(t, err)
assert.NotEmpty(t, rev.Signature)
}
func TestRevocation_Verify(t *testing.T) {
keys, chain, err := newCertChain(2)
assert.NoError(t, err)
leafCert, caCert, caKey := chain[0], chain[1], keys[0]
leafHash, err := hashBytes(leafCert.Raw)
assert.NoError(t, err)
rev := Revocation{
Timestamp: time.Now().Unix(),
CertHash: make([]byte, len(leafHash)),
}
copy(rev.CertHash, leafHash)
err = rev.Sign(caKey)
assert.NoError(t, err)
assert.NotEmpty(t, rev.Signature)
err = rev.Verify(caCert)
assert.NoError(t, err)
}
func TestRevocation_Marshal(t *testing.T) {
keys, chain, err := newCertChain(2)
assert.NoError(t, err)
leafCert, caKey := chain[0], keys[0]
leafHash, err := hashBytes(leafCert.Raw)
assert.NoError(t, err)
rev := Revocation{
Timestamp: time.Now().Unix(),
CertHash: make([]byte, len(leafHash)),
}
copy(rev.CertHash, leafHash)
err = rev.Sign(caKey)
assert.NoError(t, err)
assert.NotEmpty(t, rev.Signature)
revBytes, err := rev.Marshal()
assert.NoError(t, err)
assert.NotEmpty(t, revBytes)
decodedRev := new(Revocation)
decoder := gob.NewDecoder(bytes.NewBuffer(revBytes))
err = decoder.Decode(decodedRev)
assert.NoError(t, err)
assert.Equal(t, rev, *decodedRev)
}
func TestRevocation_Unmarshal(t *testing.T) {
keys, chain, err := newCertChain(2)
assert.NoError(t, err)
leafCert, caKey := chain[0], keys[0]
leafHash, err := hashBytes(leafCert.Raw)
assert.NoError(t, err)
rev := Revocation{
Timestamp: time.Now().Unix(),
CertHash: make([]byte, len(leafHash)),
}
copy(rev.CertHash, leafHash)
err = rev.Sign(caKey)
assert.NoError(t, err)
assert.NotEmpty(t, rev.Signature)
encodedRev := new(bytes.Buffer)
encoder := gob.NewEncoder(encodedRev)
err = encoder.Encode(rev)
assert.NoError(t, err)
unmarshaledRev := new(Revocation)
err = unmarshaledRev.Unmarshal(encodedRev.Bytes())
assert.NoError(t, err)
assert.NotNil(t, rev)
assert.Equal(t, rev, *unmarshaledRev)
}
func TestNewRevocationExt(t *testing.T) {
keys, chain, err := newCertChain(2)
assert.NoError(t, err)
ext, err := NewRevocationExt(keys[0], chain[0])
assert.NoError(t, err)
var rev Revocation
err = rev.Unmarshal(ext.Value)
assert.NoError(t, err)
err = rev.Verify(chain[1])
assert.NoError(t, err)
}
func TestRevocationDB_Get(t *testing.T) {
tmp, err := ioutil.TempDir("", os.TempDir())
defer func() { _ = os.RemoveAll(tmp) }()
keys, chain, err := newCertChain(2)
if !assert.NoError(t, err) {
t.FailNow()
}
rootKey, err := NewKey()
ext, err := NewRevocationExt(keys[0], chain[0])
if !assert.NoError(t, err) {
t.FailNow()
}
revDB, err := NewRevocationDBBolt(filepath.Join(tmp, "revocations.db"))
if !assert.NoError(t, err) {
t.FailNow()
}
var rev *Revocation
t.Run("missing key", func(t *testing.T) {
rev, err = revDB.Get(chain)
assert.NoError(t, err)
assert.Nil(t, rev)
})
caHash, err := hashBytes(chain[1].Raw)
if !assert.NoError(t, err) {
t.FailNow()
}
err = revDB.DB.Put(caHash, ext.Value)
if !assert.NoError(t, err) {
t.FailNow()
}
t.Run("existing key", func(t *testing.T) {
rev, err = revDB.Get(chain)
assert.NoError(t, err)
revBytes, err := rev.Marshal()
assert.NoError(t, err)
assert.True(t, bytes.Equal(ext.Value, revBytes))
})
}
func TestRevocationDB_Put(t *testing.T) {
tmp, err := ioutil.TempDir("", os.TempDir())
defer func() { _ = os.RemoveAll(tmp) }()
keys, chain, err := newCertChain(2)
if !assert.NoError(t, err) {
t.FailNow()
}
olderExt, err := NewRevocationExt(keys[0], chain[0])
assert.NoError(t, err)
caKey, err := NewKey()
time.Sleep(1 * time.Second)
ext, err := NewRevocationExt(keys[0], chain[0])
if !assert.NoError(t, err) {
t.FailNow()
}
time.Sleep(1 * time.Second)
newerExt, err := NewRevocationExt(keys[0], chain[0])
assert.NoError(t, err)
caTemplate, err := CATemplate()
revDB, err := NewRevocationDBBolt(filepath.Join(tmp, "revocations.db"))
if !assert.NoError(t, err) {
t.FailNow()
}
cases := []struct {
testID string
ext pkix.Extension
errClass *errs.Class
err error
}{
{
"new key",
ext,
nil,
nil,
},
{
"existing key - older timestamp",
olderExt,
&ErrExtension,
ErrRevocationTimestamp,
},
{
"existing key - newer timestamp",
newerExt,
nil,
nil,
},
}
for _, c := range cases {
t.Run(c.testID, func(t2 *testing.T) {
if !assert.NotNil(t, c.ext) {
t2.Fail()
t.FailNow()
}
err = revDB.Put(chain, c.ext)
if c.errClass != nil {
assert.True(t, c.errClass.Has(err))
}
if c.err != nil {
assert.Equal(t, c.err, err)
}
if c.err == nil && c.errClass == nil {
if !assert.NoError(t2, err) {
t2.Fail()
t.FailNow()
}
func(t2 *testing.T, ext pkix.Extension) {
caHash, err := hashBytes(chain[1].Raw)
if !assert.NoError(t2, err) {
t2.FailNow()
}
revBytes, err := revDB.DB.Get(caHash)
if !assert.NoError(t2, err) {
t2.FailNow()
}
rev := new(Revocation)
err = rev.Unmarshal(revBytes)
assert.NoError(t2, err)
assert.True(t2, bytes.Equal(ext.Value, revBytes))
}(t2, c.ext)
}
})
}
}
type extensionHandlerMock struct {
mock.Mock
}
func (m *extensionHandlerMock) verify(ext pkix.Extension, chain [][]*x509.Certificate) error {
args := m.Called(ext, chain)
return args.Error(0)
}
func TestExtensionHandlers_VerifyFunc(t *testing.T) {
keys, chain, err := newRevokedLeafChain()
chains := [][]*x509.Certificate{chain}
if !assert.NoError(t, err) {
t.FailNow()
}
err = AddSignedCertExt(keys[0], chain[0])
if !assert.NoError(t, err) {
t.FailNow()
}
extMock := new(extensionHandlerMock)
verify := func(ext pkix.Extension, chain [][]*x509.Certificate) error {
return extMock.verify(ext, chain)
}
handlers := ExtensionHandlers{
{
id: ExtensionIDs[RevocationExtID],
verify: verify,
},
{
id: ExtensionIDs[SignedCertExtID],
verify: verify,
},
}
extMock.On("verify", chains[0][LeafIndex].ExtraExtensions[0], chains).Return(nil)
extMock.On("verify", chains[0][LeafIndex].ExtraExtensions[1], chains).Return(nil)
err = handlers.VerifyFunc()(nil, chains)
assert.NoError(t, err)
extMock.AssertCalled(t, "verify", chains[0][LeafIndex].ExtraExtensions[0], chains)
extMock.AssertCalled(t, "verify", chains[0][LeafIndex].ExtraExtensions[1], chains)
extMock.AssertExpectations(t)
// TODO: test error scenario(s)
}
func TestParseExtensions(t *testing.T) {
revokedLeafKeys, revokedLeafChain, err := newRevokedLeafChain()
assert.NoError(t, err)
rootCert, err := NewCert(rootKey, nil, caTemplate, nil)
whitelistSignedKeys, whitelistSignedChain, err := newCertChain(3)
assert.NoError(t, err)
caCert, err := NewCert(caKey, rootKey, caTemplate, rootCert)
err = AddSignedCertExt(whitelistSignedKeys[0], whitelistSignedChain[0])
assert.NoError(t, err)
leafKey, err := NewKey()
_, unrelatedChain, err := newCertChain(1)
assert.NoError(t, err)
leafTemplate, err := LeafTemplate()
assert.NoError(t, err)
tmp, err := ioutil.TempDir("", os.TempDir())
if err != nil {
t.FailNow()
}
leafCert, err := NewCert(leafKey, rootKey, leafTemplate, caCert)
defer func() { _ = os.RemoveAll(tmp) }()
revDB, err := NewRevocationDBBolt(filepath.Join(tmp, "revocations.db"))
assert.NoError(t, err)
err = AddSignedLeafExt(rootKey, leafCert)
assert.NoError(t, err)
whitelist := []*x509.Certificate{rootCert}
cases := []struct {
testID string
config TLSExtConfig
extLen int
certChain []*x509.Certificate
whitelist []*x509.Certificate
expected []result
errClass *errs.Class
err error
}{
{
"leaf whitelist signature",
"leaf whitelist signature - success",
TLSExtConfig{WhitelistSignedLeaf: true},
whitelist,
[]result{{true, nil}},
1,
whitelistSignedChain,
[]*x509.Certificate{whitelistSignedChain[2]},
nil,
nil,
},
{
"leaf whitelist signature - failure (empty whitelist)",
TLSExtConfig{WhitelistSignedLeaf: true},
1,
whitelistSignedChain,
nil,
&ErrVerifyCAWhitelist,
nil,
},
{
"leaf whitelist signature - failure",
TLSExtConfig{WhitelistSignedLeaf: true},
1,
whitelistSignedChain,
unrelatedChain,
&ErrVerifyCAWhitelist,
nil,
},
{
"certificate revocation - single revocation ",
TLSExtConfig{Revocation: true},
1,
revokedLeafChain,
nil,
nil,
nil,
},
{
"certificate revocation - serial revocations",
TLSExtConfig{Revocation: true},
1,
func() []*x509.Certificate {
rev := new(Revocation)
time.Sleep(1 * time.Second)
_, chain, err := revokeLeaf(revokedLeafKeys, revokedLeafChain)
assert.NoError(t, err)
err = rev.Unmarshal(chain[0].ExtraExtensions[0].Value)
assert.NoError(t, err)
return chain
}(),
nil,
nil,
nil,
},
{
"certificate revocation - serial revocations error (older timestamp)",
TLSExtConfig{Revocation: true},
1,
func() []*x509.Certificate {
keys, chain, err := newRevokedLeafChain()
assert.NoError(t, err)
rev := new(Revocation)
err = rev.Unmarshal(chain[0].ExtraExtensions[0].Value)
assert.NoError(t, err)
rev.Timestamp = rev.Timestamp + 300
err = rev.Sign(keys[0])
assert.NoError(t, err)
revBytes, err := rev.Marshal()
assert.NoError(t, err)
err = revDB.Put(chain, pkix.Extension{
Id: ExtensionIDs[RevocationExtID],
Value: revBytes,
})
assert.NoError(t, err)
return chain
}(),
nil,
&ErrExtension,
ErrRevocationTimestamp,
},
{
"certificate revocation and leaf whitelist signature",
TLSExtConfig{Revocation: true, WhitelistSignedLeaf: true},
2,
func() []*x509.Certificate {
_, chain, err := newRevokedLeafChain()
assert.NoError(t, err)
err = AddSignedCertExt(whitelistSignedKeys[0], chain[0])
assert.NoError(t, err)
return chain
}(),
[]*x509.Certificate{whitelistSignedChain[2]},
nil,
nil,
},
}
for _, c := range cases {
t.Run(c.testID, func(t *testing.T) {
exts := ParseExtensions(c.config, c.whitelist)
assert.Equal(t, 1, len(exts))
for i, e := range exts {
ok, err := e.f(leafCert.ExtraExtensions[0], [][]*x509.Certificate{{leafCert, caCert, rootCert}})
assert.Equal(t, c.expected[i].err, err)
assert.Equal(t, c.expected[i].ok, ok)
opts := ParseExtOptions{
CAWhitelist: c.whitelist,
RevDB: revDB,
}
handlers := ParseExtensions(c.config, opts)
assert.Equal(t, c.extLen, len(handlers))
err := handlers.VerifyFunc()(nil, [][]*x509.Certificate{c.certChain})
if c.errClass != nil {
assert.True(t, c.errClass.Has(err))
}
if c.err != nil {
assert.NotNil(t, err)
}
if c.errClass == nil && c.err == nil {
assert.NoError(t, err)
}
})
}
}
// NB: keys are in the reverse order compared to certs (i.e. first key belongs to last cert)!
func newCertChain(length int) (keys []crypto.PrivateKey, certs []*x509.Certificate, _ error) {
for i := 0; i < length; i++ {
key, err := NewKey()
if err != nil {
return nil, nil, err
}
keys = append(keys, key)
var template *x509.Certificate
if i == length-1 {
template, err = CATemplate()
} else {
template, err = LeafTemplate()
}
if err != nil {
return nil, nil, err
}
var cert *x509.Certificate
if i == 0 {
cert, err = NewCert(key, nil, template, nil)
} else {
cert, err = NewCert(key, keys[i-1], template, certs[i-1:][0])
}
if err != nil {
return nil, nil, err
}
certs = append([]*x509.Certificate{cert}, certs...)
}
return keys, certs, nil
}
func revokeLeaf(keys []crypto.PrivateKey, chain []*x509.Certificate) ([]crypto.PrivateKey, []*x509.Certificate, error) {
revokingKey, err := NewKey()
if err != nil {
return nil, nil, err
}
revokingTemplate, err := LeafTemplate()
if err != nil {
return nil, nil, err
}
revokingCert, err := NewCert(revokingKey, keys[0], revokingTemplate, chain[1])
if err != nil {
return nil, nil, err
}
err = AddRevocationExt(keys[0], chain[0], revokingCert)
if err != nil {
return nil, nil, err
}
return keys, append([]*x509.Certificate{revokingCert}, chain[1:]...), nil
}
func newRevokedLeafChain() ([]crypto.PrivateKey, []*x509.Certificate, error) {
keys2, certs2, err := newCertChain(2)
if err != nil {
return nil, nil, err
}
return revokeLeaf(keys2, certs2)
}

View File

@ -17,6 +17,7 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"math/big"
@ -86,17 +87,14 @@ func VerifySignature(signedData []byte, data []byte, pubKey crypto.PublicKey) er
}
signature := new(ECDSASignature)
if _, err := asn1.Unmarshal(signedData, signature); err != nil {
return ErrVerifySignature.New("unable to unmarshal ecdsa signature: %v", err)
}
h := crypto.SHA256.New()
_, err := h.Write(data)
digest, err := hashBytes(data)
if err != nil {
return ErrVerifySignature.Wrap(err)
}
digest := h.Sum(nil)
if !ecdsa.Verify(key, digest, signature.R, signature.S) {
return ErrVerifySignature.New("signature is not valid")
@ -113,3 +111,47 @@ func newSerialNumber() (*big.Int, error) {
return serialNumber, nil
}
func uniqueExts(exts []pkix.Extension) bool {
seen := make(map[string]struct{}, len(exts))
for _, e := range exts {
s := e.Id.String()
if _, ok := seen[s]; ok {
return false
}
seen[s] = struct{}{}
}
return true
}
func signHashOf(key crypto.PrivateKey, data []byte) ([]byte, error) {
hash, err := hashBytes(data)
if err != nil {
return nil, ErrSign.Wrap(err)
}
signature, err := signBytes(key, hash)
if err != nil {
return nil, ErrSign.Wrap(err)
}
return signature, nil
}
func signBytes(key crypto.PrivateKey, data []byte) ([]byte, error) {
ecKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, ErrUnsupportedKey.New("%T", key)
}
r, s, err := ecdsa.Sign(rand.Reader, ecKey, data)
if err != nil {
return nil, ErrSign.Wrap(err)
}
return asn1.Marshal(ECDSASignature{R: r, S: s})
}
func hashBytes(data []byte) ([]byte, error) {
hash := crypto.SHA256.New()
if _, err := hash.Write(data); err != nil {
return nil, err
}
return hash.Sum(nil), nil
}

View File

@ -247,7 +247,7 @@ func (ca FullCertificateAuthority) NewIdentity() (*FullIdentity, error) {
}
if ca.RestChain != nil && len(ca.RestChain) > 0 {
err := peertls.AddSignedLeafExt(ca.Key, leafCert)
err := peertls.AddSignedCertExt(ca.Key, leafCert)
if err != nil {
return nil, err
}

View File

@ -25,11 +25,6 @@ import (
"storj.io/storj/pkg/utils"
)
const (
// IdentityLength is the number of bytes required to represent node id
IdentityLength = uint16(256 / 8) // 256 bits
)
// PeerIdentity represents another peer on the network.
type PeerIdentity struct {
RestChain []*x509.Certificate
@ -64,6 +59,7 @@ type IdentitySetupConfig struct {
KeyPath string `help:"path to the private key for this identity" default:"$CONFDIR/identity.key"`
Overwrite bool `help:"if true, existing identity certs AND keys will overwritten for" default:"false"`
Version string `help:"semantic version of identity storage format" default:"0"`
Server ServerConfig
}
// IdentityConfig allows you to run a set of Responsibilities with the given
@ -76,6 +72,7 @@ type IdentityConfig struct {
// ServerConfig holds server specific configuration parameters
type ServerConfig struct {
RevocationDBURL string `help:"url for revocation database (e.g. bolt://some.db OR redis://127.0.0.1:6378?db=2&password=abc123)" default:"bolt://$CONFDIR/revocations.db"`
PeerCAWhitelistPath string `help:"path to the CA cert whitelist (peer identities must be signed by one these to be verified)"`
Address string `help:"address to listen on" default:":7777"`
Extensions peertls.TLSExtConfig
@ -193,6 +190,33 @@ func PeerIdentityFromContext(ctx context.Context) (*PeerIdentity, error) {
return PeerIdentityFromPeer(p)
}
// NodeIDFromKey hashes a publc key and creates a node ID from it
func NodeIDFromKey(k crypto.PublicKey) (storj.NodeID, error) {
kb, err := x509.MarshalPKIXPublicKey(k)
if err != nil {
return storj.NodeID{}, storj.ErrNodeID.Wrap(err)
}
hash := make([]byte, len(storj.NodeID{}))
sha3.ShakeSum256(hash, kb)
return storj.NodeIDFromBytes(hash)
}
// NewFullIdentity creates a new ID for nodes with difficulty and concurrency params
func NewFullIdentity(ctx context.Context, difficulty uint16, concurrency uint) (*FullIdentity, error) {
ca, err := NewCA(ctx, NewCAOptions{
Difficulty: difficulty,
Concurrency: concurrency,
})
if err != nil {
return nil, err
}
identity, err := ca.NewIdentity()
if err != nil {
return nil, err
}
return identity, err
}
// Stat returns the status of the identity cert/key files for the config
func (is IdentitySetupConfig) Stat() TLSFilesStatus {
return statTLSFiles(is.CertPath, is.KeyPath)
@ -318,7 +342,6 @@ func (fi *FullIdentity) ServerOption(pcvFuncs ...peertls.PeerCertVerificationFun
// to the node with this peer identity
// id is an optional id of the node we are dialing
func (fi *FullIdentity) DialOption(id storj.NodeID) (grpc.DialOption, error) {
// TODO(coyle): add ID
ch := [][]byte{fi.Leaf.Raw, fi.CA.Raw}
ch = append(ch, fi.RestChainRaw()...)
c, err := peertls.TLSCert(ch, fi.Leaf, fi.Key)
@ -338,47 +361,65 @@ func (fi *FullIdentity) DialOption(id storj.NodeID) (grpc.DialOption, error) {
return grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), nil
}
// NodeIDFromKey hashes a publc key and creates a node ID from it
func NodeIDFromKey(k crypto.PublicKey) (storj.NodeID, error) {
kb, err := x509.MarshalPKIXPublicKey(k)
// NewRevDB returns a new revocation database given the config
func (c ServerConfig) NewRevDB() (*peertls.RevocationDB, error) {
driver, source, err := utils.SplitDBURL(c.RevocationDBURL)
if err != nil {
return storj.NodeID{}, storj.ErrNodeID.Wrap(err)
return nil, peertls.ErrRevocationDB.Wrap(err)
}
hash := make([]byte, len(storj.NodeID{}))
sha3.ShakeSum256(hash, kb)
return storj.NodeIDFromBytes(hash)
}
// NewFullIdentity creates a new ID for nodes with difficulty and concurrency params
func NewFullIdentity(ctx context.Context, difficulty uint16, concurrency uint) (*FullIdentity, error) {
ca, err := NewCA(ctx, NewCAOptions{
Difficulty: difficulty,
Concurrency: concurrency,
})
if err != nil {
return nil, err
var db *peertls.RevocationDB
switch driver {
case "bolt":
db, err = peertls.NewRevocationDBBolt(source)
if err != nil {
return nil, peertls.ErrRevocationDB.Wrap(err)
}
zap.S().Info("Starting overlay cache with BoltDB")
case "redis":
db, err = peertls.NewRevocationDBRedis(c.RevocationDBURL)
if err != nil {
return nil, peertls.ErrRevocationDB.Wrap(err)
}
zap.S().Info("Starting overlay cache with Redis")
default:
return nil, peertls.ErrRevocationDB.New("database scheme not supported: %s", driver)
}
identity, err := ca.NewIdentity()
if err != nil {
return nil, err
}
return identity, err
return db, nil
}
// PCVFuncs returns a slice of peer certificate verification functions based on the config.
func (c ServerConfig) PCVFuncs() (pcvs []peertls.PeerCertVerificationFunc, err error) {
var caWhitelist []*x509.Certificate
parseOpts := peertls.ParseExtOptions{}
if c.PeerCAWhitelistPath != "" {
caWhitelist, err = loadWhitelist(c.PeerCAWhitelistPath)
caWhitelist, err := loadWhitelist(c.PeerCAWhitelistPath)
if err != nil {
return nil, err
}
parseOpts.CAWhitelist = caWhitelist
pcvs = append(pcvs, peertls.VerifyCAWhitelist(caWhitelist))
}
exts := peertls.ParseExtensions(c.Extensions, caWhitelist)
if c.Extensions.Revocation {
revDB, err := c.NewRevDB()
if err != nil {
return nil, err
}
pcvs = append(pcvs, peertls.VerifyUnrevokedChainFunc(revDB))
}
exts := peertls.ParseExtensions(c.Extensions, parseOpts)
pcvs = append(pcvs, exts.VerifyFunc())
// NB: remove nil elements
for i, f := range pcvs {
if f == nil {
copy(pcvs[i:], pcvs[i+1:])
pcvs = pcvs[:len(pcvs)-1]
}
}
return pcvs, nil
}

View File

@ -12,6 +12,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"runtime"
"testing"
@ -113,7 +114,7 @@ func TestIdentityConfig_SaveIdentity(t *testing.T) {
assert.NoError(t, err)
if runtime.GOOS != "windows" {
//TODO (windows): ignoring for windows due to different default permissions
// TODO (windows): ignoring for windows due to different default permissions
certInfo, err := os.Stat(ic.CertPath)
assert.NoError(t, err)
assert.Equal(t, os.FileMode(0644), certInfo.Mode())
@ -133,6 +134,148 @@ func TestIdentityConfig_SaveIdentity(t *testing.T) {
assert.Equal(t, keyPEM.Bytes(), savedKeyPEM)
}
func TestIdentityConfig_LoadIdentity(t *testing.T) {
done, ic, expectedFI, _ := tempIdentity(t)
defer done()
err := ic.Save(expectedFI)
assert.NoError(t, err)
fi, err := ic.Load()
assert.NoError(t, err)
assert.NotEmpty(t, fi)
assert.NotEmpty(t, fi.Key)
assert.NotEmpty(t, fi.Leaf)
assert.NotEmpty(t, fi.CA)
assert.NotEmpty(t, fi.ID.Bytes())
assert.Equal(t, expectedFI.Key, fi.Key)
assert.Equal(t, expectedFI.Leaf, fi.Leaf)
assert.Equal(t, expectedFI.CA, fi.CA)
assert.Equal(t, expectedFI.ID.Bytes(), fi.ID.Bytes())
}
func TestNodeID_Difficulty(t *testing.T) {
done, _, fi, knownDifficulty := tempIdentity(t)
defer done()
difficulty, err := fi.ID.Difficulty()
assert.NoError(t, err)
assert.True(t, difficulty >= knownDifficulty)
}
func TestVerifyPeer(t *testing.T) {
check := func(e error) {
if !assert.NoError(t, e) {
t.Fail()
}
}
ca, err := newTestCA(context.Background())
check(err)
fi, err := ca.NewIdentity()
check(err)
err = peertls.VerifyPeerFunc(peertls.VerifyPeerCertChains)([][]byte{fi.Leaf.Raw, fi.CA.Raw}, nil)
assert.NoError(t, err)
}
func TestNewServerOptions(t *testing.T) {
done, _, fi, _ := tempIdentity(t)
defer done()
tmp, err := ioutil.TempDir(os.TempDir(), "")
if !assert.NoError(t, err) {
t.FailNow()
}
defer func() { _ = os.RemoveAll(tmp) }()
whitelistPath := filepath.Join(tmp, "whitelist.pem")
w, err := os.Create(whitelistPath)
if !assert.NoError(t, err) {
t.FailNow()
}
err = peertls.WriteChain(w, fi.CA)
if !assert.NoError(t, err) {
t.FailNow()
}
cases := []struct {
testID string
config ServerConfig
pcvFuncsLen int
}{
{
"default",
ServerConfig{},
0,
},
{
"revocation processing",
ServerConfig{
RevocationDBURL: "bolt://" + filepath.Join(tmp, "revocation1.db"),
Extensions: peertls.TLSExtConfig{
Revocation: true,
},
},
2,
},
{
"ca whitelist verification",
ServerConfig{
PeerCAWhitelistPath: whitelistPath,
},
1,
},
{
"ca whitelist verification and whitelist signed leaf verification",
ServerConfig{
// NB: file doesn't actually exist
PeerCAWhitelistPath: filepath.Join(tmp, "whitelist.pem"),
Extensions: peertls.TLSExtConfig{
WhitelistSignedLeaf: true,
},
},
2,
},
{
"revocation processing and whitelist verification",
ServerConfig{
// NB: file doesn't actually exist
PeerCAWhitelistPath: whitelistPath,
RevocationDBURL: "bolt://" + filepath.Join(tmp, "revocation2.db"),
Extensions: peertls.TLSExtConfig{
Revocation: true,
},
},
3,
},
{
"revocation processing, whitelist, and signed leaf verification",
ServerConfig{
// NB: file doesn't actually exist
PeerCAWhitelistPath: whitelistPath,
RevocationDBURL: "bolt://" + filepath.Join(tmp, "revocation3.db"),
Extensions: peertls.TLSExtConfig{
Revocation: true,
WhitelistSignedLeaf: true,
},
},
3,
},
}
for _, c := range cases {
t.Run(c.testID, func(t *testing.T) {
opts, err := NewServerOptions(fi, c.config)
assert.NoError(t, err)
assert.True(t, reflect.DeepEqual(fi, opts.Ident))
assert.Equal(t, c.config, opts.Config)
assert.Len(t, opts.PCVFuncs, c.pcvFuncsLen)
})
}
}
func tempIdentityConfig() (*IdentityConfig, func(), error) {
tmpDir, err := ioutil.TempDir("", "storj-identity")
if err != nil {
@ -184,53 +327,3 @@ AwEHoUQDQgAEoLy/0hs5deTXZunRumsMkiHpF0g8wAc58aXANmr7Mxx9tzoIYFnx
return cleanup, ic, fi, difficulty
}
func TestIdentityConfig_LoadIdentity(t *testing.T) {
done, ic, expectedFI, _ := tempIdentity(t)
defer done()
err := ic.Save(expectedFI)
assert.NoError(t, err)
fi, err := ic.Load()
assert.NoError(t, err)
assert.NotEmpty(t, fi)
assert.NotEmpty(t, fi.Key)
assert.NotEmpty(t, fi.Leaf)
assert.NotEmpty(t, fi.CA)
assert.NotEmpty(t, fi.ID.Bytes())
assert.Equal(t, expectedFI.Key, fi.Key)
assert.Equal(t, expectedFI.Leaf, fi.Leaf)
assert.Equal(t, expectedFI.CA, fi.CA)
assert.Equal(t, expectedFI.ID.Bytes(), fi.ID.Bytes())
}
func TestNewI(t *testing.T) {
}
func TestNodeID_Difficulty(t *testing.T) {
done, _, fi, knownDifficulty := tempIdentity(t)
defer done()
difficulty, err := fi.ID.Difficulty()
assert.NoError(t, err)
assert.True(t, difficulty >= knownDifficulty)
}
func TestVerifyPeer(t *testing.T) {
check := func(e error) {
if !assert.NoError(t, e) {
t.Fail()
}
}
ca, err := newTestCA(context.Background())
check(err)
fi, err := ca.NewIdentity()
check(err)
err = peertls.VerifyPeerFunc(peertls.VerifyPeerCertChains)([][]byte{fi.Leaf.Raw, fi.CA.Raw}, nil)
assert.NoError(t, err)
}

View File

@ -6,6 +6,7 @@ package satelliteql
import (
"github.com/graphql-go/graphql"
"github.com/skyrings/skyring-common/tools/uuid"
"storj.io/storj/pkg/satellite"
)

View File

@ -2,7 +2,7 @@
set -ueo pipefail
go install -v storj.io/storj/cmd/captplanet
captplanet setup --overwrite
captplanet setup --overwrite --satellite-identity.server.revocation-dburl="redis://127.0.0.1:6378?db=2&password=abc123"
unamestr=`uname`
if [[ "$unamestr" == 'Darwin' ]]; then
@ -84,7 +84,7 @@ fi
kill -9 $CAPT_PID
captplanet setup --listen-host ::1 --overwrite
captplanet setup --listen-host ::1 --overwrite --satellite-identity.server.revocation-dburl="redis://127.0.0.1:6378?db=2&password=abc123"
if [[ "$unamestr" == 'Darwin' ]]; then
sed -i~ 's/interval:.*/interval: 1s/g' $HOME/Library/Application\ Support/Storj/Capt/config.yaml
else