mirror of
https://github.com/openziti/ziti.git
synced 2026-10-26 13:39:58 +00:00
6b342c9515
- verifies the control-channel peer leaf against the controller's full trusted-CA pool (identity.CA()) instead of only self-signed roots, honoring intermediate trust anchors and multi-root bundles - drops the client-auth extended-key-usage requirement so an externally managed PKI with arbitrary or absent EKUs is not rejected
157 lines
6.3 KiB
Go
157 lines
6.3 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package handler_ctrl
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/openziti/channel/v4"
|
|
"github.com/openziti/identity"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// meshChannelType mirrors controller/raft/mesh.ChannelTypeMesh without importing that package.
|
|
const meshChannelType = "ctrl.mesh"
|
|
|
|
func helloWithType(chType string) *channel.Hello {
|
|
h := channel.Headers{}
|
|
if chType != "" {
|
|
h.PutStringHeader(channel.TypeHeader, chType)
|
|
}
|
|
return &channel.Hello{Headers: h}
|
|
}
|
|
|
|
// Test_ConnectHandler_isSeparatelyValidated covers the rule that only channel types dispatched to a
|
|
// dedicated, self-validating acceptor are skipped; every other type - including unrecognized types and
|
|
// the mesh type on a non-clustered controller - routes to the router control acceptor and must be
|
|
// validated here.
|
|
func Test_ConnectHandler_isSeparatelyValidated(t *testing.T) {
|
|
// Clustered controller: only the mesh type has a dedicated acceptor.
|
|
clustered := &ConnectHandler{separatelyValidatedTypes: map[string]struct{}{meshChannelType: {}}}
|
|
require.True(t, clustered.isSeparatelyValidated(helloWithType(meshChannelType)), "mesh is validated by its own acceptor")
|
|
require.False(t, clustered.isSeparatelyValidated(helloWithType("bogus")), "unrecognized types route to the router acceptor and must be validated")
|
|
require.False(t, clustered.isSeparatelyValidated(helloWithType("")), "legacy (no type) connections are validated here")
|
|
|
|
// Non-clustered controller: no dedicated acceptors, so even a mesh-typed connection routes to the
|
|
// router acceptor and must be validated.
|
|
standalone := &ConnectHandler{separatelyValidatedTypes: map[string]struct{}{}}
|
|
require.False(t, standalone.isSeparatelyValidated(helloWithType(meshChannelType)), "mesh on a non-clustered controller must be validated")
|
|
require.False(t, standalone.isSeparatelyValidated(helloWithType("")))
|
|
}
|
|
|
|
// caPoolIdentity is a minimal identity.Identity whose only useful method is CA. The certificate
|
|
// verification path of HandleConnection consults nothing else, so the remaining interface methods are
|
|
// left to the embedded nil interface (never called on the paths exercised here).
|
|
type caPoolIdentity struct {
|
|
identity.Identity
|
|
roots *x509.CertPool
|
|
}
|
|
|
|
func (f *caPoolIdentity) CA() *x509.CertPool { return f.roots }
|
|
|
|
type ctCertAndKey struct {
|
|
cert *x509.Certificate
|
|
key *ecdsa.PrivateKey
|
|
}
|
|
|
|
var ctSerial int64
|
|
|
|
func ctMkCert(t *testing.T, cn string, isCA bool, signer *ctCertAndKey) *ctCertAndKey {
|
|
t.Helper()
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
ctSerial++
|
|
tmpl := &x509.Certificate{
|
|
SerialNumber: big.NewInt(ctSerial),
|
|
Subject: pkix.Name{CommonName: cn},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
BasicConstraintsValid: true,
|
|
}
|
|
if isCA {
|
|
tmpl.IsCA = true
|
|
tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign
|
|
} else {
|
|
tmpl.KeyUsage = x509.KeyUsageDigitalSignature
|
|
tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
|
|
}
|
|
signParent, signKey := tmpl, key
|
|
if signer != nil {
|
|
signParent, signKey = signer.cert, signer.key
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey)
|
|
require.NoError(t, err)
|
|
c, err := x509.ParseCertificate(der)
|
|
require.NoError(t, err)
|
|
return &ctCertAndKey{cert: c, key: key}
|
|
}
|
|
|
|
// Test_ConnectHandler_HandleConnection_RejectsUntrustedLeaf covers the router control-channel
|
|
// verification: a connection dispatched to the router acceptor must present a leaf that chains to the
|
|
// controller CA. In particular, presenting a self-signed leaf followed by a legitimate router's public
|
|
// certificate as filler must be rejected - the check is bound to the leaf, not to "some presented cert
|
|
// chains". These paths fail during certificate verification, before any network state is consulted.
|
|
func Test_ConnectHandler_HandleConnection_RejectsUntrustedLeaf(t *testing.T) {
|
|
req := require.New(t)
|
|
|
|
root := ctMkCert(t, "root", true, nil)
|
|
inter := ctMkCert(t, "int", true, root)
|
|
roots := x509.NewCertPool()
|
|
roots.AddCert(root.cert)
|
|
roots.AddCert(inter.cert)
|
|
|
|
handler := &ConnectHandler{
|
|
identity: &caPoolIdentity{roots: roots},
|
|
separatelyValidatedTypes: map[string]struct{}{},
|
|
}
|
|
|
|
// A legitimate, CA-chained certificate an attacker could scrape off the wire and present as filler.
|
|
legit := ctMkCert(t, "legit-router", false, inter)
|
|
// The attacker's own self-signed leaf - it does not chain to the CA.
|
|
forged := ctMkCert(t, "forged", false, nil)
|
|
|
|
hello := &channel.Hello{IdToken: "router1", Headers: channel.Headers{}}
|
|
|
|
req.Error(handler.HandleConnection(hello, nil), "no certificates must be rejected")
|
|
req.Error(handler.HandleConnection(hello, []*x509.Certificate{forged.cert}),
|
|
"self-signed leaf that does not chain to the CA must be rejected")
|
|
req.Error(handler.HandleConnection(hello, []*x509.Certificate{forged.cert, legit.cert}),
|
|
"self-signed leaf backed by a scraped CA-chained filler cert must be rejected")
|
|
}
|
|
|
|
// Test_ConnectHandler_HandleConnection_SkipsSeparatelyValidated verifies that connections whose type is
|
|
// handled by a separate acceptor (e.g. the raft mesh on a clustered controller) are not validated here,
|
|
// even when the presented certificate would fail the router control-channel check.
|
|
func Test_ConnectHandler_HandleConnection_SkipsSeparatelyValidated(t *testing.T) {
|
|
req := require.New(t)
|
|
|
|
forged := ctMkCert(t, "forged", false, nil)
|
|
handler := &ConnectHandler{
|
|
separatelyValidatedTypes: map[string]struct{}{meshChannelType: {}},
|
|
}
|
|
|
|
req.NoError(handler.HandleConnection(helloWithType(meshChannelType), []*x509.Certificate{forged.cert}),
|
|
"mesh-typed connection is validated by its own acceptor and must be skipped here")
|
|
}
|