mirror of
https://github.com/openziti/ziti.git
synced 2026-09-11 13:29:03 +00:00
362 lines
12 KiB
Go
362 lines
12 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 webapis
|
||
|
||
import (
|
||
"crypto"
|
||
"crypto/ecdsa"
|
||
"crypto/elliptic"
|
||
"crypto/rand"
|
||
"crypto/x509"
|
||
"crypto/x509/pkix"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"math/big"
|
||
"net"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/openziti/identity"
|
||
"github.com/openziti/xweb/v3"
|
||
"github.com/openziti/ziti/v2/common/bindpoints"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func Test_getPossibleIssuers(t *testing.T) {
|
||
req := require.New(t)
|
||
|
||
caKey, caCertTemplate := mkCaCert("Parent CA")
|
||
parentDer, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, caKey.Public(), caKey)
|
||
req.NoError(err)
|
||
|
||
childKey1, childCert1 := mkClientCert("Test Child 1")
|
||
childCert1Der, err := x509.CreateCertificate(rand.Reader, childCert1, caCertTemplate, childKey1.Public(), caKey)
|
||
req.NoError(err)
|
||
|
||
childKey2, childCert2 := mkServerCert("Test Child 2", []string{"client2.netfoundry.io"}, []net.IP{net.ParseIP("127.0.0.1")})
|
||
childCert2Der, err := x509.CreateCertificate(rand.Reader, childCert2, caCertTemplate, childKey2.Public(), caKey)
|
||
req.NoError(err)
|
||
|
||
childKey3, childCert3 := mkServerCert("Test Child 3", []string{"client3.netfoundry.io"}, []net.IP{net.ParseIP("10.8.0.1")})
|
||
childCert3Der, err := x509.CreateCertificate(rand.Reader, childCert3, caCertTemplate, childKey3.Public(), caKey)
|
||
req.NoError(err)
|
||
|
||
childKey4, childCert4 := mkServerCert("Test Child 4", []string{"*.wildcard.io"}, []net.IP{net.ParseIP("192.168.0.1")})
|
||
childCert4Der, err := x509.CreateCertificate(rand.Reader, childCert4, caCertTemplate, childKey4.Public(), caKey)
|
||
req.NoError(err)
|
||
|
||
childKey1Der, _ := x509.MarshalECPrivateKey(childKey1.(*ecdsa.PrivateKey))
|
||
childKey1Pem := &pem.Block{
|
||
Type: "EC PRIVATE KEY",
|
||
Bytes: childKey1Der,
|
||
}
|
||
|
||
childKey2Der, _ := x509.MarshalECPrivateKey(childKey2.(*ecdsa.PrivateKey))
|
||
childKey2Pem := &pem.Block{
|
||
Type: "EC PRIVATE KEY",
|
||
Bytes: childKey2Der,
|
||
}
|
||
|
||
childKey3Der, _ := x509.MarshalECPrivateKey(childKey3.(*ecdsa.PrivateKey))
|
||
childKey3Pem := &pem.Block{
|
||
Type: "EC PRIVATE KEY",
|
||
Bytes: childKey3Der,
|
||
}
|
||
|
||
childKey4Der, _ := x509.MarshalECPrivateKey(childKey4.(*ecdsa.PrivateKey))
|
||
childKey4Pem := &pem.Block{
|
||
Type: "EC PRIVATE KEY",
|
||
Bytes: childKey4Der,
|
||
}
|
||
|
||
parentPem := &pem.Block{
|
||
Type: "CERTIFICATE",
|
||
Bytes: parentDer,
|
||
}
|
||
|
||
childCert1Pem := &pem.Block{
|
||
Type: "CERTIFICATE",
|
||
Bytes: childCert1Der,
|
||
}
|
||
|
||
childCert2Pem := &pem.Block{
|
||
Type: "CERTIFICATE",
|
||
Bytes: childCert2Der,
|
||
}
|
||
|
||
childCert3Pem := &pem.Block{
|
||
Type: "CERTIFICATE",
|
||
Bytes: childCert3Der,
|
||
}
|
||
|
||
childCert4Pem := &pem.Block{
|
||
Type: "CERTIFICATE",
|
||
Bytes: childCert4Der,
|
||
}
|
||
|
||
cfg := identity.Config{
|
||
Key: "pem:" + string(pem.EncodeToMemory(childKey1Pem)),
|
||
Cert: "pem:" + string(pem.EncodeToMemory(childCert1Pem)) + string(pem.EncodeToMemory(parentPem)),
|
||
ServerKey: "pem:" + string(pem.EncodeToMemory(childKey2Pem)),
|
||
ServerCert: "pem:" + string(pem.EncodeToMemory(childCert2Pem)) + string(pem.EncodeToMemory(parentPem)),
|
||
AltServerCerts: []identity.ServerPair{
|
||
{
|
||
ServerKey: "pem:" + string(pem.EncodeToMemory(childKey3Pem)),
|
||
ServerCert: "pem:" + string(pem.EncodeToMemory(childCert3Pem)) + string(pem.EncodeToMemory(parentPem)),
|
||
},
|
||
{
|
||
ServerKey: "pem:" + string(pem.EncodeToMemory(childKey4Pem)),
|
||
ServerCert: "pem:" + string(pem.EncodeToMemory(childCert4Pem)) + string(pem.EncodeToMemory(parentPem)),
|
||
},
|
||
},
|
||
}
|
||
|
||
id, err := identity.LoadIdentity(cfg)
|
||
req.NoError(err)
|
||
|
||
t.Run("receives the proper issuers", func(t *testing.T) {
|
||
req := require.New(t)
|
||
const (
|
||
bindPoint1Address = "test1.example.com:1234"
|
||
bindPoint2Address = "test2.example.com:443"
|
||
)
|
||
|
||
bindPoints := []xweb.BindPoint{
|
||
&bindpoints.UnderlayBindPoint{
|
||
Address: bindPoint1Address,
|
||
},
|
||
&bindpoints.UnderlayBindPoint{
|
||
Address: bindPoint2Address,
|
||
},
|
||
}
|
||
|
||
// star.wildcard.io is the only allowed hostname; it is covered by the *.wildcard.io SAN and so
|
||
// becomes a valid issuer. Other hosts under the wildcard are not allow-listed and must not.
|
||
issuers := getPossibleIssuers(id, bindPoints, []string{"star.wildcard.io"})
|
||
|
||
req.Len(issuers, 21) // base 18 + star.wildcard.io:{1234,443} + bare star.wildcard.io
|
||
|
||
isValidIssuer := func(address string) error {
|
||
for _, issuer := range issuers {
|
||
if err := issuer.ValidFor(address); err == nil {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
return fmt.Errorf("invalid address, no issuer supports: %s", address)
|
||
}
|
||
|
||
req.NoError(isValidIssuer("test1.example.com:1234"))
|
||
req.NoError(isValidIssuer("test2.example.com:443"))
|
||
req.NoError(isValidIssuer("test2.example.com"))
|
||
|
||
req.NoError(isValidIssuer("client2.netfoundry.io:1234"))
|
||
req.NoError(isValidIssuer("client2.netfoundry.io:443"))
|
||
req.NoError(isValidIssuer("client2.netfoundry.io"))
|
||
|
||
req.NoError(isValidIssuer("client3.netfoundry.io:1234"))
|
||
req.NoError(isValidIssuer("client3.netfoundry.io:443"))
|
||
req.NoError(isValidIssuer("client3.netfoundry.io"))
|
||
|
||
// the allow-listed host under the wildcard SAN is a valid issuer
|
||
req.NoError(isValidIssuer("star.wildcard.io:1234"))
|
||
req.NoError(isValidIssuer("star.wildcard.io:443"))
|
||
req.NoError(isValidIssuer("star.wildcard.io"))
|
||
|
||
req.NoError(isValidIssuer("127.0.0.1:1234"))
|
||
req.NoError(isValidIssuer("127.0.0.1:443"))
|
||
req.NoError(isValidIssuer("127.0.0.1"))
|
||
|
||
req.NoError(isValidIssuer("10.8.0.1:1234"))
|
||
req.NoError(isValidIssuer("10.8.0.1:443"))
|
||
req.NoError(isValidIssuer("10.8.0.1"))
|
||
|
||
req.NoError(isValidIssuer("192.168.0.1:1234"))
|
||
req.NoError(isValidIssuer("192.168.0.1:443"))
|
||
req.NoError(isValidIssuer("192.168.0.1"))
|
||
|
||
req.Error(isValidIssuer("10.123.123.1"))
|
||
req.Error(isValidIssuer("star.wildcard.io:555"))
|
||
req.Error(isValidIssuer("10.8.0.1:555"))
|
||
req.Error(isValidIssuer("google.com"))
|
||
|
||
// a host covered by the wildcard SAN but NOT in allowedHostnames must not become an issuer
|
||
req.Error(isValidIssuer("other.wildcard.io:1234"))
|
||
req.Error(isValidIssuer("other.wildcard.io:443"))
|
||
req.Error(isValidIssuer("other.wildcard.io"))
|
||
|
||
})
|
||
|
||
bindPoints := []xweb.BindPoint{
|
||
&bindpoints.UnderlayBindPoint{Address: "test1.example.com:1234"},
|
||
&bindpoints.UnderlayBindPoint{Address: "test2.example.com:443"},
|
||
}
|
||
|
||
t.Run("wildcard SAN with no allowedHostnames yields no wildcard issuers", func(t *testing.T) {
|
||
req := require.New(t)
|
||
issuers := getPossibleIssuers(id, bindPoints, nil)
|
||
for _, issuer := range issuers {
|
||
req.Error(issuer.ValidFor("star.wildcard.io:1234"), "wildcard host must not be a valid issuer without an allowlist")
|
||
}
|
||
})
|
||
|
||
t.Run("allowedHostnames entry not covered by any SAN is ignored", func(t *testing.T) {
|
||
req := require.New(t)
|
||
// nocover.example.org is matched by neither *.wildcard.io nor any concrete SAN
|
||
issuers := getPossibleIssuers(id, bindPoints, []string{"nocover.example.org"})
|
||
for _, issuer := range issuers {
|
||
req.Error(issuer.ValidFor("nocover.example.org:1234"), "uncovered allowlist entry must not become an issuer")
|
||
}
|
||
})
|
||
}
|
||
|
||
// Test_NewOidcApiHandler_AllowedHostnames verifies that structurally invalid edge-oidc allowedHostnames
|
||
// configuration is a hard error that fails controller startup (NewOidcApiHandler returns an error), rather
|
||
// than being silently ignored.
|
||
func Test_NewOidcApiHandler_AllowedHostnames(t *testing.T) {
|
||
id := newMinimalServerIdentity(t)
|
||
serverConfig := &xweb.ServerConfig{
|
||
Identity: id,
|
||
BindPoints: []xweb.BindPoint{&bindpoints.UnderlayBindPoint{Address: "localhost:1280"}},
|
||
}
|
||
|
||
t.Run("non-list value is rejected", func(t *testing.T) {
|
||
_, err := NewOidcApiHandler(serverConfig, nil, map[interface{}]interface{}{"allowedHostnames": "ctrl.wildcard.io"})
|
||
require.Error(t, err)
|
||
})
|
||
|
||
t.Run("non-string entry is rejected", func(t *testing.T) {
|
||
_, err := NewOidcApiHandler(serverConfig, nil, map[interface{}]interface{}{"allowedHostnames": []interface{}{123}})
|
||
require.Error(t, err)
|
||
})
|
||
|
||
t.Run("wildcard entry is rejected", func(t *testing.T) {
|
||
_, err := NewOidcApiHandler(serverConfig, nil, map[interface{}]interface{}{"allowedHostnames": []interface{}{"*.wildcard.io"}})
|
||
require.Error(t, err)
|
||
})
|
||
}
|
||
|
||
// helpers
|
||
|
||
var testSerial = int64(0)
|
||
|
||
// newMinimalServerIdentity builds an identity with a single concrete server cert, enough for
|
||
// NewOidcApiHandler to reach allowedHostnames parsing.
|
||
func newMinimalServerIdentity(t *testing.T) identity.Identity {
|
||
req := require.New(t)
|
||
|
||
caKey, caTmpl := mkCaCert("Parent CA")
|
||
caDer, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, caKey.Public(), caKey)
|
||
req.NoError(err)
|
||
|
||
clientKey, clientCert := mkClientCert("client")
|
||
clientDer, err := x509.CreateCertificate(rand.Reader, clientCert, caTmpl, clientKey.Public(), caKey)
|
||
req.NoError(err)
|
||
|
||
serverKey, serverCert := mkServerCert("server", []string{"localhost"}, []net.IP{net.ParseIP("127.0.0.1")})
|
||
serverDer, err := x509.CreateCertificate(rand.Reader, serverCert, caTmpl, serverKey.Public(), caKey)
|
||
req.NoError(err)
|
||
|
||
certPem := func(der []byte) string {
|
||
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
|
||
}
|
||
keyPem := func(k crypto.Signer) string {
|
||
der, mErr := x509.MarshalECPrivateKey(k.(*ecdsa.PrivateKey))
|
||
req.NoError(mErr)
|
||
return string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}))
|
||
}
|
||
|
||
cfg := identity.Config{
|
||
Key: "pem:" + keyPem(clientKey),
|
||
Cert: "pem:" + certPem(clientDer) + certPem(caDer),
|
||
ServerKey: "pem:" + keyPem(serverKey),
|
||
ServerCert: "pem:" + certPem(serverDer) + certPem(caDer),
|
||
}
|
||
|
||
id, err := identity.LoadIdentity(cfg)
|
||
req.NoError(err)
|
||
return id
|
||
}
|
||
|
||
func mkCaCert(cn string) (crypto.Signer, *x509.Certificate) {
|
||
testSerial++
|
||
|
||
key, _ := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
|
||
|
||
cert := &x509.Certificate{
|
||
SerialNumber: big.NewInt(testSerial),
|
||
Subject: pkix.Name{
|
||
Organization: []string{"OpenZiti Identity Tests"},
|
||
OrganizationalUnit: []string{"CA Certs"},
|
||
CommonName: cn,
|
||
},
|
||
NotBefore: time.Now(),
|
||
NotAfter: time.Now().Add(1 * time.Hour),
|
||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||
ExtKeyUsage: []x509.ExtKeyUsage{}, // CAs typically don’t need ExtKeyUsage
|
||
IsCA: true,
|
||
BasicConstraintsValid: true,
|
||
MaxPathLen: 2,
|
||
MaxPathLenZero: false,
|
||
}
|
||
|
||
return key, cert
|
||
}
|
||
|
||
func mkServerCert(cn string, dns []string, ips []net.IP) (crypto.Signer, *x509.Certificate) {
|
||
testSerial++
|
||
|
||
key, _ := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
|
||
|
||
cert := &x509.Certificate{
|
||
SerialNumber: big.NewInt(testSerial),
|
||
Subject: pkix.Name{
|
||
Organization: []string{"OpenZiti Identity Tests"},
|
||
OrganizationalUnit: []string{"Server Certs"},
|
||
CommonName: cn,
|
||
},
|
||
DNSNames: dns,
|
||
IPAddresses: ips,
|
||
NotBefore: time.Now(),
|
||
NotAfter: time.Now().Add(1 * time.Hour),
|
||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||
BasicConstraintsValid: true,
|
||
}
|
||
|
||
return key, cert
|
||
}
|
||
|
||
func mkClientCert(cn string) (crypto.Signer, *x509.Certificate) {
|
||
key, _ := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
|
||
|
||
cert := &x509.Certificate{
|
||
SerialNumber: big.NewInt(testSerial),
|
||
Subject: pkix.Name{
|
||
Organization: []string{"OpenZiti Identity Tests"},
|
||
OrganizationalUnit: []string{"Client Certs"},
|
||
CommonName: cn,
|
||
},
|
||
NotBefore: time.Now(),
|
||
NotAfter: time.Now().Add(1 * time.Hour),
|
||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||
BasicConstraintsValid: true,
|
||
}
|
||
|
||
return key, cert
|
||
}
|