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:
parent
6f634d9f02
commit
2016ce9fd6
4
Makefile
4
Makefile
@ -55,6 +55,10 @@ check-copyrights: ## Check source files for copyright headers
|
|||||||
goimports-fix: ## Applies goimports to every go file (excluding vendored files)
|
goimports-fix: ## Applies goimports to every go file (excluding vendored files)
|
||||||
goimports -w -local storj.io $$(find . -type f -name '*.go' -not -path "*/vendor/*")
|
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
|
.PHONY: proto
|
||||||
proto: ## Rebuild protobuf files
|
proto: ## Rebuild protobuf files
|
||||||
@echo "Running ${@}"
|
@echo "Running ${@}"
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdSetup(cmd *cobra.Command, args []string) (err error) {
|
func cmdSetup(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
redisURL := regexp.MustCompile("^redis://")
|
||||||
setupCfg.BasePath, err = filepath.Abs(setupCfg.BasePath)
|
setupCfg.BasePath, err = filepath.Abs(setupCfg.BasePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -110,6 +112,9 @@ func cmdSetup(cmd *cobra.Command, args []string) (err error) {
|
|||||||
setupCfg.UplinkCA.KeyPath = filepath.Join(uplinkPath, "ca.key")
|
setupCfg.UplinkCA.KeyPath = filepath.Join(uplinkPath, "ca.key")
|
||||||
setupCfg.UplinkIdentity.CertPath = filepath.Join(uplinkPath, "identity.cert")
|
setupCfg.UplinkIdentity.CertPath = filepath.Join(uplinkPath, "identity.cert")
|
||||||
setupCfg.UplinkIdentity.KeyPath = filepath.Join(uplinkPath, "identity.key")
|
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")
|
fmt.Printf("creating identity for uplink\n")
|
||||||
err = provider.SetupIdentity(process.Ctx(cmd), setupCfg.UplinkCA, setupCfg.UplinkIdentity)
|
err = provider.SetupIdentity(process.Ctx(cmd), setupCfg.UplinkCA, setupCfg.UplinkIdentity)
|
||||||
if err != nil {
|
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.key-path": setupCfg.SatelliteIdentity.KeyPath,
|
||||||
"satellite.identity.server.address": joinHostPort(
|
"satellite.identity.server.address": joinHostPort(
|
||||||
setupCfg.ListenHost, startingPort+1),
|
setupCfg.ListenHost, startingPort+1),
|
||||||
|
"satellite.identity.server.revocation-dburl": setupCfg.SatelliteIdentity.Server.RevocationDBURL,
|
||||||
"satellite.kademlia.bootstrap-addr": joinHostPort(
|
"satellite.kademlia.bootstrap-addr": joinHostPort(
|
||||||
setupCfg.ListenHost, startingPort+1),
|
setupCfg.ListenHost, startingPort+1),
|
||||||
"satellite.pointer-db.database-url": "bolt://" + filepath.Join(
|
"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.key-path": setupCfg.UplinkIdentity.KeyPath,
|
||||||
"uplink.identity.server.address": joinHostPort(
|
"uplink.identity.server.address": joinHostPort(
|
||||||
setupCfg.ListenHost, startingPort),
|
setupCfg.ListenHost, startingPort),
|
||||||
|
"uplink.identity.server.revocation-dburl": setupCfg.SatelliteIdentity.Server.RevocationDBURL,
|
||||||
"uplink.client.overlay-addr": joinHostPort(
|
"uplink.client.overlay-addr": joinHostPort(
|
||||||
setupCfg.ListenHost, startingPort+1),
|
setupCfg.ListenHost, startingPort+1),
|
||||||
"uplink.client.pointer-db-addr": joinHostPort(
|
"uplink.client.pointer-db-addr": joinHostPort(
|
||||||
@ -189,6 +196,9 @@ func cmdSetup(cmd *cobra.Command, args []string) (err error) {
|
|||||||
storagenodePath, "identity.key")
|
storagenodePath, "identity.key")
|
||||||
overrides[storagenode+"identity.server.address"] = joinHostPort(
|
overrides[storagenode+"identity.server.address"] = joinHostPort(
|
||||||
setupCfg.ListenHost, startingPort+i*2+3)
|
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(
|
overrides[storagenode+"kademlia.bootstrap-addr"] = joinHostPort(
|
||||||
setupCfg.ListenHost, startingPort+1)
|
setupCfg.ListenHost, startingPort+1)
|
||||||
overrides[storagenode+"storage.path"] = filepath.Join(storagenodePath, "data")
|
overrides[storagenode+"storage.path"] = filepath.Join(storagenodePath, "data")
|
||||||
|
1
go.mod
1
go.mod
@ -94,6 +94,7 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.3
|
github.com/spf13/pflag v1.0.3
|
||||||
github.com/spf13/viper v1.2.1
|
github.com/spf13/viper v1.2.1
|
||||||
github.com/streadway/amqp v0.0.0-20180806233856-70e15c650864 // indirect
|
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/stretchr/testify v1.2.2
|
||||||
github.com/tidwall/gjson v1.1.3 // indirect
|
github.com/tidwall/gjson v1.1.3 // indirect
|
||||||
github.com/tidwall/match v0.0.0-20171002075945-1731857f09b1 // indirect
|
github.com/tidwall/match v0.0.0-20171002075945-1731857f09b1 // indirect
|
||||||
|
6
go.sum
6
go.sum
@ -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/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 h1:CMRCnhHx4xVxJy+wPsS67xmi9RHGNctLMoVn9Q1Kit8=
|
||||||
github.com/cheggaaa/pb v1.0.5-0.20160713104425-73ae1d68fe0b/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
|
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/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 h1:V/dVCzhKOdIU3WRB5inQU20s4yIgL9Dxx/Mhi0SF8eM=
|
||||||
github.com/cloudfoundry/gosigar v1.1.0/go.mod h1:3qLfc2GlfmwOx2+ZDaRGH3Y9fwQ0sQeaAleo2GV5pH0=
|
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 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
|
||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
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/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 h1:c8VQNu/587ErbVKJSz6kKVdrf3kS18Sn50UShPyJ7Wc=
|
||||||
github.com/gogo/protobuf v1.1.2-0.20181116123445-07eab6a8298c/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
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=
|
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/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 h1:Oj3PUEs+OUSYUpn35O+BE/ivHGirKixA3+vqA0Atu9A=
|
||||||
github.com/streadway/amqp v0.0.0-20180806233856-70e15c650864/go.mod h1:1WNBiOZtZQLpVAyu0iTduoJL9hEsMloAK5XWrtW0xdY=
|
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 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/tidwall/gjson v1.1.3 h1:u4mspaByxY+Qk4U1QYYVzGFI8qxN/3jtEV0ZDb2vRic=
|
github.com/tidwall/gjson v1.1.3 h1:u4mspaByxY+Qk4U1QYYVzGFI8qxN/3jtEV0ZDb2vRic=
|
||||||
|
105
internal/testpeertls/certificates.go
Normal file
105
internal/testpeertls/certificates.go
Normal 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("")
|
||||||
|
}
|
@ -4,13 +4,62 @@
|
|||||||
package peertls
|
package peertls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/gob"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/zeebo/errs"
|
"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
|
// 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"`
|
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`)
|
// ExtensionHandlers is a collection of `extensionHandler`s for convenience (see `VerifyFunc`)
|
||||||
type Extensions []extension
|
type ExtensionHandlers []extensionHandler
|
||||||
|
|
||||||
type extension struct {
|
type extensionVerificationFunc func(pkix.Extension, [][]*x509.Certificate) error
|
||||||
|
|
||||||
|
type extensionHandler struct {
|
||||||
id asn1.ObjectIdentifier
|
id asn1.ObjectIdentifier
|
||||||
f func(pkix.Extension, [][]*x509.Certificate) (bool, error)
|
verify extensionVerificationFunc
|
||||||
err error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseExtensions an extension config into a slice of extensions with their
|
// ParseExtOptions holds options for calling `ParseExtensions`
|
||||||
// respective ids (`asn1.ObjectIdentifier`) and a function (`f`) which can be
|
type ParseExtOptions struct {
|
||||||
// used in the context of peer certificate verification.
|
CAWhitelist []*x509.Certificate
|
||||||
func ParseExtensions(c TLSExtConfig, caWhitelist []*x509.Certificate) (exts Extensions) {
|
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 {
|
if c.WhitelistSignedLeaf {
|
||||||
exts = append(exts, extension{
|
handlers = append(handlers, extensionHandler{
|
||||||
id: ExtensionIDs[SignedCertExtID],
|
id: ExtensionIDs[SignedCertExtID],
|
||||||
f: func(certExt pkix.Extension, chains [][]*x509.Certificate) (bool, error) {
|
verify: verifyCAWhitelistSignedLeafFunc(opts.CAWhitelist),
|
||||||
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"),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// VerifyFunc returns a peer certificate verification function which iterates
|
||||||
// over all the leaf cert's extensions and receiver extensions and calls
|
// over all the leaf cert's extensions and receiver extensions and calls
|
||||||
// `extension#f` when it finds a match by id (`asn1.ObjectIdentifier`)
|
// `extensionHandler#verify` when it finds a match by id (`asn1.ObjectIdentifier`)
|
||||||
func (e Extensions) VerifyFunc() PeerCertVerificationFunc {
|
func (e ExtensionHandlers) VerifyFunc() PeerCertVerificationFunc {
|
||||||
return func(_ [][]byte, parsedChains [][]*x509.Certificate) error {
|
if len(e) == 0 {
|
||||||
for _, ext := range parsedChains[0][0].Extensions {
|
return nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
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
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,13 +4,12 @@
|
|||||||
package peertls
|
package peertls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/asn1"
|
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
@ -27,23 +26,7 @@ const (
|
|||||||
BlockTypeIDOptions = "ID OPTIONS"
|
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 (
|
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 is used when a file or directory doesn't exist.
|
||||||
ErrNotExist = errs.Class("file or directory not found error")
|
ErrNotExist = errs.Class("file or directory not found error")
|
||||||
// ErrGenerate is used when an error occurred during cert/key generation.
|
// 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
|
// 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).
|
// (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")
|
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")
|
ErrVerifyCAWhitelist = errs.Class("not signed by any CA in the whitelist")
|
||||||
// ErrSign is used when something goes wrong while generating a signature.
|
// ErrSign is used when something goes wrong while generating a signature.
|
||||||
ErrSign = errs.Class("unable to generate signature")
|
ErrSign = errs.Class("unable to generate signature")
|
||||||
@ -107,20 +90,20 @@ func VerifyPeerCertChains(_ [][]byte, parsedChains [][]*x509.Certificate) error
|
|||||||
return verifyChainSignatures(parsedChains[0])
|
return verifyChainSignatures(parsedChains[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyCAWhitelist verifies that the peer identity's CA and leaf-extension was signed
|
// VerifyCAWhitelist verifies that the peer identity's CA was signed by any one
|
||||||
// by any one of the (certificate authority) certificates in the provided whitelist.
|
// of the (certificate authority) certificates in the provided whitelist.
|
||||||
func VerifyCAWhitelist(cas []*x509.Certificate) PeerCertVerificationFunc {
|
func VerifyCAWhitelist(cas []*x509.Certificate) PeerCertVerificationFunc {
|
||||||
if cas == nil {
|
if cas == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return func(_ [][]byte, parsedChains [][]*x509.Certificate) error {
|
return func(_ [][]byte, parsedChains [][]*x509.Certificate) error {
|
||||||
for _, ca := range cas {
|
for _, ca := range cas {
|
||||||
err := verifyCertSignature(ca, parsedChains[0][1])
|
err := verifyCertSignature(ca, parsedChains[0][CAIndex])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return 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
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSignedLeafExt adds a "signed certificate extension" to the passed cert,
|
// VerifyUnrevokedChainFunc returns a peer certificate verification function which
|
||||||
// using the passed private key.
|
// returns an error if the incoming cert chain contains a revoked CA or leaf.
|
||||||
func AddSignedLeafExt(key crypto.PrivateKey, cert *x509.Certificate) error {
|
func VerifyUnrevokedChainFunc(revDB *RevocationDB) PeerCertVerificationFunc {
|
||||||
ecKey, ok := key.(*ecdsa.PrivateKey)
|
return func(_ [][]byte, chains [][]*x509.Certificate) error {
|
||||||
if !ok {
|
leaf := chains[0][LeafIndex]
|
||||||
return ErrUnsupportedKey.New("%T", key)
|
ca := chains[0][CAIndex]
|
||||||
|
lastRev, lastRevErr := revDB.Get(chains[0])
|
||||||
|
if lastRevErr != nil {
|
||||||
|
return ErrExtension.Wrap(lastRevErr)
|
||||||
}
|
}
|
||||||
|
if lastRev == 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
signature, err := asn1.Marshal(ECDSASignature{R: r, S: s})
|
|
||||||
if err != nil {
|
|
||||||
return ErrSign.Wrap(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
|
||||||
Id: ExtensionIDs[SignedCertExtID],
|
|
||||||
Value: signature,
|
|
||||||
})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,11 +5,21 @@ package peertls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"encoding/gob"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/zeebo/errs"
|
"github.com/zeebo/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -61,23 +71,11 @@ func TestNewCert_Leaf(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestVerifyPeerFunc(t *testing.T) {
|
func TestVerifyPeerFunc(t *testing.T) {
|
||||||
caKey, err := NewKey()
|
_, chain, err := newCertChain(2)
|
||||||
assert.NoError(t, err)
|
if !assert.NoError(t, err) {
|
||||||
|
t.FailNow()
|
||||||
caTemplate, err := CATemplate()
|
}
|
||||||
assert.NoError(t, err)
|
leafCert, caCert := chain[0], chain[1]
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
testFunc := func(chain [][]byte, parsedChains [][]*x509.Certificate) error {
|
testFunc := func(chain [][]byte, parsedChains [][]*x509.Certificate) error {
|
||||||
switch {
|
switch {
|
||||||
@ -104,23 +102,11 @@ func TestVerifyPeerFunc(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestVerifyPeerCertChains(t *testing.T) {
|
func TestVerifyPeerCertChains(t *testing.T) {
|
||||||
caKey, err := NewKey()
|
keys, chain, err := newCertChain(2)
|
||||||
assert.NoError(t, err)
|
if !assert.NoError(t, err) {
|
||||||
|
t.FailNow()
|
||||||
caTemplate, err := CATemplate()
|
}
|
||||||
assert.NoError(t, err)
|
leafKey, leafCert, caCert := keys[1], chain[0], chain[1]
|
||||||
|
|
||||||
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 = VerifyPeerFunc(VerifyPeerCertChains)([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
err = VerifyPeerFunc(VerifyPeerCertChains)([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -128,7 +114,7 @@ func TestVerifyPeerCertChains(t *testing.T) {
|
|||||||
wrongKey, err := NewKey()
|
wrongKey, err := NewKey()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
leafCert, err = NewCert(leafKey, wrongKey, leafTemplate, caCert)
|
leafCert, err = NewCert(leafKey, wrongKey, leafCert, caCert)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = VerifyPeerFunc(VerifyPeerCertChains)([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
err = VerifyPeerFunc(VerifyPeerCertChains)([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
||||||
@ -137,88 +123,119 @@ func TestVerifyPeerCertChains(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestVerifyCAWhitelist(t *testing.T) {
|
func TestVerifyCAWhitelist(t *testing.T) {
|
||||||
caKey, err := NewKey()
|
_, chain2, err := newCertChain(2)
|
||||||
assert.NoError(t, err)
|
if !assert.NoError(t, err) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
leafCert, caCert := chain2[0], chain2[1]
|
||||||
|
|
||||||
caTemplate, err := CATemplate()
|
t.Run("empty whitelist", func(t *testing.T) {
|
||||||
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)
|
|
||||||
|
|
||||||
// empty whitelist
|
|
||||||
err = VerifyPeerFunc(VerifyCAWhitelist(nil))([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
err = VerifyPeerFunc(VerifyCAWhitelist(nil))([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
// whitelist contains ca
|
t.Run("whitelist contains ca", func(t *testing.T) {
|
||||||
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{caCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{caCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
rootKey, err := NewKey()
|
_, unrelatedChain, err := newCertChain(1)
|
||||||
assert.NoError(t, err)
|
if !assert.NoError(t, err) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
unrelatedCert := unrelatedChain[0]
|
||||||
|
|
||||||
rootTemplate, err := CATemplate()
|
t.Run("no valid signed extension, non-empty whitelist", func(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{unrelatedCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
||||||
|
|
||||||
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))
|
assert.True(t, ErrVerifyCAWhitelist.Has(err))
|
||||||
|
})
|
||||||
|
|
||||||
// last cert in whitelist is signer
|
t.Run("last cert in whitelist is signer", func(t *testing.T) {
|
||||||
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{rootCert, caCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// first cert in whitelist is signer
|
assert.Len(t, chain[0].ExtraExtensions, 1)
|
||||||
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{caCert, rootCert}))([][]byte{leafCert.Raw, caCert.Raw}, nil)
|
assert.Equal(t, ExtensionIDs[SignedCertExtID], chain[0].ExtraExtensions[0].Id)
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
ca2Cert, err := NewCert(caKey, rootKey, caTemplate, rootCert)
|
ecKey, ok := keys[0].(*ecdsa.PrivateKey)
|
||||||
assert.NoError(t, err)
|
if !assert.True(t, ok) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
leaf2Cert, err := NewCert(leafKey, caKey, leafTemplate, ca2Cert)
|
err = VerifySignature(
|
||||||
assert.NoError(t, err)
|
chain[0].ExtraExtensions[0].Value,
|
||||||
|
chain[0].RawTBSCertificate,
|
||||||
// length 3 chain; first cert in whitelist is signer
|
&ecKey.PublicKey,
|
||||||
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)
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSignLeafExt(t *testing.T) {
|
func TestSignLeafExt(t *testing.T) {
|
||||||
caKey, err := NewKey()
|
keys, chain, err := newCertChain(2)
|
||||||
assert.NoError(t, err)
|
if !assert.NoError(t, err) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
caKey, leafCert := keys[0], chain[0]
|
||||||
|
|
||||||
caTemplate, err := CATemplate()
|
err = AddSignedCertExt(caKey, leafCert)
|
||||||
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)
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 1, len(leafCert.ExtraExtensions))
|
assert.Equal(t, 1, len(leafCert.ExtraExtensions))
|
||||||
assert.True(t, ExtensionIDs[SignedCertExtID].Equal(leafCert.ExtraExtensions[0].Id))
|
assert.True(t, ExtensionIDs[SignedCertExtID].Equal(leafCert.ExtraExtensions[0].Id))
|
||||||
@ -232,64 +249,525 @@ func TestSignLeafExt(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseExtensions(t *testing.T) {
|
func TestRevocation_Sign(t *testing.T) {
|
||||||
type result struct {
|
keys, chain, err := newCertChain(2)
|
||||||
ok bool
|
assert.NoError(t, err)
|
||||||
err error
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
rootKey, err := NewKey()
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
caKey, err := NewKey()
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
caTemplate, err := CATemplate()
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
rootCert, err := NewCert(rootKey, nil, caTemplate, nil)
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
caCert, err := NewCert(caKey, rootKey, caTemplate, rootCert)
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
leafKey, err := NewKey()
|
ext, err := NewRevocationExt(keys[0], chain[0])
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
leafTemplate, err := LeafTemplate()
|
var rev Revocation
|
||||||
|
err = rev.Unmarshal(ext.Value)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
leafCert, err := NewCert(leafKey, rootKey, leafTemplate, caCert)
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = AddSignedLeafExt(rootKey, leafCert)
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
whitelist := []*x509.Certificate{rootCert}
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
whitelistSignedKeys, whitelistSignedChain, err := newCertChain(3)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = AddSignedCertExt(whitelistSignedKeys[0], whitelistSignedChain[0])
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, unrelatedChain, err := newCertChain(1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
tmp, err := ioutil.TempDir("", os.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = os.RemoveAll(tmp) }()
|
||||||
|
revDB, err := NewRevocationDBBolt(filepath.Join(tmp, "revocations.db"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
testID string
|
testID string
|
||||||
config TLSExtConfig
|
config TLSExtConfig
|
||||||
|
extLen int
|
||||||
|
certChain []*x509.Certificate
|
||||||
whitelist []*x509.Certificate
|
whitelist []*x509.Certificate
|
||||||
expected []result
|
errClass *errs.Class
|
||||||
|
err error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"leaf whitelist signature",
|
"leaf whitelist signature - success",
|
||||||
TLSExtConfig{WhitelistSignedLeaf: true},
|
TLSExtConfig{WhitelistSignedLeaf: true},
|
||||||
whitelist,
|
1,
|
||||||
[]result{{true, nil}},
|
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 {
|
for _, c := range cases {
|
||||||
t.Run(c.testID, func(t *testing.T) {
|
t.Run(c.testID, func(t *testing.T) {
|
||||||
exts := ParseExtensions(c.config, c.whitelist)
|
opts := ParseExtOptions{
|
||||||
assert.Equal(t, 1, len(exts))
|
CAWhitelist: c.whitelist,
|
||||||
for i, e := range exts {
|
RevDB: revDB,
|
||||||
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)
|
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)
|
||||||
|
}
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
@ -86,17 +87,14 @@ func VerifySignature(signedData []byte, data []byte, pubKey crypto.PublicKey) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
signature := new(ECDSASignature)
|
signature := new(ECDSASignature)
|
||||||
|
|
||||||
if _, err := asn1.Unmarshal(signedData, signature); err != nil {
|
if _, err := asn1.Unmarshal(signedData, signature); err != nil {
|
||||||
return ErrVerifySignature.New("unable to unmarshal ecdsa signature: %v", err)
|
return ErrVerifySignature.New("unable to unmarshal ecdsa signature: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
h := crypto.SHA256.New()
|
digest, err := hashBytes(data)
|
||||||
_, err := h.Write(data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrVerifySignature.Wrap(err)
|
return ErrVerifySignature.Wrap(err)
|
||||||
}
|
}
|
||||||
digest := h.Sum(nil)
|
|
||||||
|
|
||||||
if !ecdsa.Verify(key, digest, signature.R, signature.S) {
|
if !ecdsa.Verify(key, digest, signature.R, signature.S) {
|
||||||
return ErrVerifySignature.New("signature is not valid")
|
return ErrVerifySignature.New("signature is not valid")
|
||||||
@ -113,3 +111,47 @@ func newSerialNumber() (*big.Int, error) {
|
|||||||
|
|
||||||
return serialNumber, nil
|
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
|
||||||
|
}
|
||||||
|
@ -247,7 +247,7 @@ func (ca FullCertificateAuthority) NewIdentity() (*FullIdentity, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ca.RestChain != nil && len(ca.RestChain) > 0 {
|
if ca.RestChain != nil && len(ca.RestChain) > 0 {
|
||||||
err := peertls.AddSignedLeafExt(ca.Key, leafCert)
|
err := peertls.AddSignedCertExt(ca.Key, leafCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -25,11 +25,6 @@ import (
|
|||||||
"storj.io/storj/pkg/utils"
|
"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.
|
// PeerIdentity represents another peer on the network.
|
||||||
type PeerIdentity struct {
|
type PeerIdentity struct {
|
||||||
RestChain []*x509.Certificate
|
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"`
|
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"`
|
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"`
|
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
|
// 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
|
// ServerConfig holds server specific configuration parameters
|
||||||
type ServerConfig struct {
|
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)"`
|
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"`
|
Address string `help:"address to listen on" default:":7777"`
|
||||||
Extensions peertls.TLSExtConfig
|
Extensions peertls.TLSExtConfig
|
||||||
@ -193,6 +190,33 @@ func PeerIdentityFromContext(ctx context.Context) (*PeerIdentity, error) {
|
|||||||
return PeerIdentityFromPeer(p)
|
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
|
// Stat returns the status of the identity cert/key files for the config
|
||||||
func (is IdentitySetupConfig) Stat() TLSFilesStatus {
|
func (is IdentitySetupConfig) Stat() TLSFilesStatus {
|
||||||
return statTLSFiles(is.CertPath, is.KeyPath)
|
return statTLSFiles(is.CertPath, is.KeyPath)
|
||||||
@ -318,7 +342,6 @@ func (fi *FullIdentity) ServerOption(pcvFuncs ...peertls.PeerCertVerificationFun
|
|||||||
// to the node with this peer identity
|
// to the node with this peer identity
|
||||||
// id is an optional id of the node we are dialing
|
// id is an optional id of the node we are dialing
|
||||||
func (fi *FullIdentity) DialOption(id storj.NodeID) (grpc.DialOption, error) {
|
func (fi *FullIdentity) DialOption(id storj.NodeID) (grpc.DialOption, error) {
|
||||||
// TODO(coyle): add ID
|
|
||||||
ch := [][]byte{fi.Leaf.Raw, fi.CA.Raw}
|
ch := [][]byte{fi.Leaf.Raw, fi.CA.Raw}
|
||||||
ch = append(ch, fi.RestChainRaw()...)
|
ch = append(ch, fi.RestChainRaw()...)
|
||||||
c, err := peertls.TLSCert(ch, fi.Leaf, fi.Key)
|
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
|
return grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeIDFromKey hashes a publc key and creates a node ID from it
|
// NewRevDB returns a new revocation database given the config
|
||||||
func NodeIDFromKey(k crypto.PublicKey) (storj.NodeID, error) {
|
func (c ServerConfig) NewRevDB() (*peertls.RevocationDB, error) {
|
||||||
kb, err := x509.MarshalPKIXPublicKey(k)
|
driver, source, err := utils.SplitDBURL(c.RevocationDBURL)
|
||||||
if err != nil {
|
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
|
var db *peertls.RevocationDB
|
||||||
func NewFullIdentity(ctx context.Context, difficulty uint16, concurrency uint) (*FullIdentity, error) {
|
switch driver {
|
||||||
ca, err := NewCA(ctx, NewCAOptions{
|
case "bolt":
|
||||||
Difficulty: difficulty,
|
db, err = peertls.NewRevocationDBBolt(source)
|
||||||
Concurrency: concurrency,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, peertls.ErrRevocationDB.Wrap(err)
|
||||||
}
|
}
|
||||||
identity, err := ca.NewIdentity()
|
zap.S().Info("Starting overlay cache with BoltDB")
|
||||||
|
case "redis":
|
||||||
|
db, err = peertls.NewRevocationDBRedis(c.RevocationDBURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, peertls.ErrRevocationDB.Wrap(err)
|
||||||
}
|
}
|
||||||
return identity, err
|
zap.S().Info("Starting overlay cache with Redis")
|
||||||
|
default:
|
||||||
|
return nil, peertls.ErrRevocationDB.New("database scheme not supported: %s", driver)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PCVFuncs returns a slice of peer certificate verification functions based on the config.
|
// PCVFuncs returns a slice of peer certificate verification functions based on the config.
|
||||||
func (c ServerConfig) PCVFuncs() (pcvs []peertls.PeerCertVerificationFunc, err error) {
|
func (c ServerConfig) PCVFuncs() (pcvs []peertls.PeerCertVerificationFunc, err error) {
|
||||||
var caWhitelist []*x509.Certificate
|
parseOpts := peertls.ParseExtOptions{}
|
||||||
|
|
||||||
if c.PeerCAWhitelistPath != "" {
|
if c.PeerCAWhitelistPath != "" {
|
||||||
caWhitelist, err = loadWhitelist(c.PeerCAWhitelistPath)
|
caWhitelist, err := loadWhitelist(c.PeerCAWhitelistPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
parseOpts.CAWhitelist = caWhitelist
|
||||||
pcvs = append(pcvs, peertls.VerifyCAWhitelist(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())
|
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
|
return pcvs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -133,6 +134,148 @@ func TestIdentityConfig_SaveIdentity(t *testing.T) {
|
|||||||
assert.Equal(t, keyPEM.Bytes(), savedKeyPEM)
|
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) {
|
func tempIdentityConfig() (*IdentityConfig, func(), error) {
|
||||||
tmpDir, err := ioutil.TempDir("", "storj-identity")
|
tmpDir, err := ioutil.TempDir("", "storj-identity")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -184,53 +327,3 @@ AwEHoUQDQgAEoLy/0hs5deTXZunRumsMkiHpF0g8wAc58aXANmr7Mxx9tzoIYFnx
|
|||||||
|
|
||||||
return cleanup, ic, fi, difficulty
|
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)
|
|
||||||
}
|
|
||||||
|
@ -6,6 +6,7 @@ package satelliteql
|
|||||||
import (
|
import (
|
||||||
"github.com/graphql-go/graphql"
|
"github.com/graphql-go/graphql"
|
||||||
"github.com/skyrings/skyring-common/tools/uuid"
|
"github.com/skyrings/skyring-common/tools/uuid"
|
||||||
|
|
||||||
"storj.io/storj/pkg/satellite"
|
"storj.io/storj/pkg/satellite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
set -ueo pipefail
|
set -ueo pipefail
|
||||||
go install -v storj.io/storj/cmd/captplanet
|
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`
|
unamestr=`uname`
|
||||||
if [[ "$unamestr" == 'Darwin' ]]; then
|
if [[ "$unamestr" == 'Darwin' ]]; then
|
||||||
@ -84,7 +84,7 @@ fi
|
|||||||
|
|
||||||
kill -9 $CAPT_PID
|
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
|
if [[ "$unamestr" == 'Darwin' ]]; then
|
||||||
sed -i~ 's/interval:.*/interval: 1s/g' $HOME/Library/Application\ Support/Storj/Capt/config.yaml
|
sed -i~ 's/interval:.*/interval: 1s/g' $HOME/Library/Application\ Support/Storj/Capt/config.yaml
|
||||||
else
|
else
|
||||||
|
Loading…
Reference in New Issue
Block a user