Files
ziti/controller/webapis/oidc-api_test.go
2026-06-03 13:15:49 -04:00

362 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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 dont 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
}