mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-02 16:30:17 +02:00
config: add support for max_verify_depth
Add a new max_verify_depth option to the downstream_mtls settings group, with a default value of 1 (to match the behavior of current Pomerium releases). Populate the corresponding setting within Envoy, and also implement a depth check within isValidClientCertificate() in the authorize service.
This commit is contained in:
parent
0fcc3f16de
commit
e8509c50b4
9 changed files with 147 additions and 25 deletions
|
@ -9,6 +9,7 @@ type evaluatorConfig struct {
|
|||
clientCA []byte
|
||||
clientCRL []byte
|
||||
addDefaultClientCertificateRule bool
|
||||
clientCertConstraints ClientCertConstraints
|
||||
signingKey []byte
|
||||
authenticateURL string
|
||||
googleCloudServerlessAuthenticationServiceAccount string
|
||||
|
@ -55,6 +56,13 @@ func WithAddDefaultClientCertificateRule(addDefaultClientCertificateRule bool) O
|
|||
}
|
||||
}
|
||||
|
||||
// WithClientCertConstraints sets addition client certificate constraints.
|
||||
func WithClientCertConstraints(constraints ClientCertConstraints) Option {
|
||||
return func(cfg *evaluatorConfig) {
|
||||
cfg.clientCertConstraints = constraints
|
||||
}
|
||||
}
|
||||
|
||||
// WithSigningKey sets the signing key and algorithm in the config.
|
||||
func WithSigningKey(signingKey []byte) Option {
|
||||
return func(cfg *evaluatorConfig) {
|
||||
|
|
|
@ -89,11 +89,12 @@ type Result struct {
|
|||
|
||||
// An Evaluator evaluates policies.
|
||||
type Evaluator struct {
|
||||
store *store.Store
|
||||
policyEvaluators map[uint64]*PolicyEvaluator
|
||||
headersEvaluators *HeadersEvaluator
|
||||
clientCA []byte
|
||||
clientCRL []byte
|
||||
store *store.Store
|
||||
policyEvaluators map[uint64]*PolicyEvaluator
|
||||
headersEvaluators *HeadersEvaluator
|
||||
clientCA []byte
|
||||
clientCRL []byte
|
||||
clientCertConstraints ClientCertConstraints
|
||||
}
|
||||
|
||||
// New creates a new Evaluator.
|
||||
|
@ -114,6 +115,7 @@ func New(ctx context.Context, store *store.Store, options ...Option) (*Evaluator
|
|||
|
||||
e.clientCA = cfg.clientCA
|
||||
e.clientCRL = cfg.clientCRL
|
||||
e.clientCertConstraints = cfg.clientCertConstraints
|
||||
|
||||
e.policyEvaluators = make(map[uint64]*PolicyEvaluator)
|
||||
for i := range cfg.policies {
|
||||
|
@ -211,8 +213,8 @@ func (e *Evaluator) evaluatePolicy(ctx context.Context, req *Request) (*PolicyRe
|
|||
return nil, err
|
||||
}
|
||||
|
||||
isValidClientCertificate, err :=
|
||||
isValidClientCertificate(clientCA, string(e.clientCRL), req.HTTP.ClientCertificate)
|
||||
isValidClientCertificate, err := isValidClientCertificate(
|
||||
clientCA, string(e.clientCRL), req.HTTP.ClientCertificate, e.clientCertConstraints)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authorize: error validating client certificate: %w", err)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package evaluator
|
|||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -12,9 +13,19 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/log"
|
||||
)
|
||||
|
||||
var isValidClientCertificateCache, _ = lru.New2Q[[4]string, bool](100)
|
||||
// ClientCertConstraints contains additional constraints to validate when
|
||||
// verifying a client certificate.
|
||||
type ClientCertConstraints struct {
|
||||
// MaxVerifyDepth is the maximum allowed certificate chain depth (not
|
||||
// counting the leaf certificate). A value of 0 indicates no maximum.
|
||||
MaxVerifyDepth uint32
|
||||
}
|
||||
|
||||
func isValidClientCertificate(ca, crl string, certInfo ClientCertificateInfo) (bool, error) {
|
||||
var isValidClientCertificateCache, _ = lru.New2Q[[5]string, bool](100)
|
||||
|
||||
func isValidClientCertificate(
|
||||
ca, crl string, certInfo ClientCertificateInfo, constraints ClientCertConstraints,
|
||||
) (bool, error) {
|
||||
// when ca is the empty string, client certificates are not required
|
||||
if ca == "" {
|
||||
return true, nil
|
||||
|
@ -27,7 +38,12 @@ func isValidClientCertificate(ca, crl string, certInfo ClientCertificateInfo) (b
|
|||
return false, nil
|
||||
}
|
||||
|
||||
cacheKey := [4]string{ca, crl, cert, intermediates}
|
||||
constraintsJSON, err := json.Marshal(constraints)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("internal error: failed to serialize constraints: %w", err)
|
||||
}
|
||||
|
||||
cacheKey := [5]string{ca, crl, cert, intermediates, string(constraintsJSON)}
|
||||
|
||||
value, ok := isValidClientCertificateCache.Get(cacheKey)
|
||||
if ok {
|
||||
|
@ -50,7 +66,7 @@ func isValidClientCertificate(ca, crl string, certInfo ClientCertificateInfo) (b
|
|||
return false, err
|
||||
}
|
||||
|
||||
verifyErr := verifyClientCertificate(xcert, roots, intermediatesPool, crls)
|
||||
verifyErr := verifyClientCertificate(xcert, roots, intermediatesPool, crls, constraints)
|
||||
valid := verifyErr == nil
|
||||
|
||||
if verifyErr != nil {
|
||||
|
@ -67,6 +83,7 @@ func verifyClientCertificate(
|
|||
roots *x509.CertPool,
|
||||
intermediates *x509.CertPool,
|
||||
crls map[string]*x509.RevocationList,
|
||||
constraints ClientCertConstraints,
|
||||
) error {
|
||||
chains, err := cert.Verify(x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
|
@ -77,10 +94,11 @@ func verifyClientCertificate(
|
|||
return err
|
||||
}
|
||||
|
||||
// At least one of the verified chains must also pass revocation checking.
|
||||
// At least one of the verified chains must also pass revocation checking
|
||||
// and satisfy any additional constraints.
|
||||
err = errors.New("internal error: no verified chains")
|
||||
for _, chain := range chains {
|
||||
err = validateClientCertificateChain(chain, crls)
|
||||
err = validateClientCertificateChain(chain, crls, constraints)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -94,7 +112,15 @@ func verifyClientCertificate(
|
|||
func validateClientCertificateChain(
|
||||
chain []*x509.Certificate,
|
||||
crls map[string]*x509.RevocationList,
|
||||
constraints ClientCertConstraints,
|
||||
) error {
|
||||
if constraints.MaxVerifyDepth > 0 {
|
||||
if d := uint32(len(chain) - 1); d > constraints.MaxVerifyDepth {
|
||||
return fmt.Errorf("chain depth %d exceeds max_verify_depth %d",
|
||||
d, constraints.MaxVerifyDepth)
|
||||
}
|
||||
}
|
||||
|
||||
// Consult CRLs for all CAs in the chain (that is, all certificates except
|
||||
// for the first one). To match Envoy's behavior, if a CRL is provided for
|
||||
// any CA in the chain, CRLs must be provided for all CAs in the chain (see
|
||||
|
|
|
@ -97,13 +97,15 @@ BVAnH/e8AiEAjy8cP1msG62BeDaAVU5NcU9RAXDw1Oz4HkpELXQWqK8=
|
|||
)
|
||||
|
||||
func Test_isValidClientCertificate(t *testing.T) {
|
||||
var noConstraints ClientCertConstraints
|
||||
t.Run("no ca", func(t *testing.T) {
|
||||
valid, err := isValidClientCertificate("", "", ClientCertificateInfo{Leaf: "WHATEVER!"})
|
||||
valid, err := isValidClientCertificate(
|
||||
"", "", ClientCertificateInfo{Leaf: "WHATEVER!"}, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.True(t, valid, "should return true")
|
||||
})
|
||||
t.Run("no cert", func(t *testing.T) {
|
||||
valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{})
|
||||
valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{}, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.False(t, valid, "should return false")
|
||||
})
|
||||
|
@ -111,7 +113,7 @@ func Test_isValidClientCertificate(t *testing.T) {
|
|||
valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{
|
||||
Presented: true,
|
||||
Leaf: testValidCert,
|
||||
})
|
||||
}, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.True(t, valid, "should return true")
|
||||
})
|
||||
|
@ -120,7 +122,7 @@ func Test_isValidClientCertificate(t *testing.T) {
|
|||
Presented: true,
|
||||
Leaf: testValidIntermediateCert,
|
||||
Intermediates: testIntermediateCA,
|
||||
})
|
||||
}, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.True(t, valid, "should return true")
|
||||
})
|
||||
|
@ -129,7 +131,7 @@ func Test_isValidClientCertificate(t *testing.T) {
|
|||
Presented: true,
|
||||
Leaf: testValidIntermediateCert,
|
||||
Intermediates: "",
|
||||
})
|
||||
}, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.False(t, valid, "should return false")
|
||||
})
|
||||
|
@ -137,7 +139,7 @@ func Test_isValidClientCertificate(t *testing.T) {
|
|||
valid, err := isValidClientCertificate(testIntermediateCA, "", ClientCertificateInfo{
|
||||
Presented: true,
|
||||
Leaf: testValidIntermediateCert,
|
||||
})
|
||||
}, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.True(t, valid, "should return true")
|
||||
})
|
||||
|
@ -145,7 +147,7 @@ func Test_isValidClientCertificate(t *testing.T) {
|
|||
valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{
|
||||
Presented: true,
|
||||
Leaf: testUntrustedCert,
|
||||
})
|
||||
}, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.False(t, valid, "should return false")
|
||||
})
|
||||
|
@ -153,7 +155,7 @@ func Test_isValidClientCertificate(t *testing.T) {
|
|||
valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{
|
||||
Presented: true,
|
||||
Leaf: "WHATEVER!",
|
||||
})
|
||||
}, noConstraints)
|
||||
assert.Error(t, err, "should return an error")
|
||||
assert.False(t, valid, "should return false")
|
||||
})
|
||||
|
@ -164,11 +166,11 @@ func Test_isValidClientCertificate(t *testing.T) {
|
|||
}
|
||||
|
||||
// The "revoked cert" should otherwise be valid (when no CRL is specified).
|
||||
valid, err := isValidClientCertificate(testCA, "", revokedCertInfo)
|
||||
valid, err := isValidClientCertificate(testCA, "", revokedCertInfo, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.True(t, valid, "should return true")
|
||||
|
||||
valid, err = isValidClientCertificate(testCA, testCRL, revokedCertInfo)
|
||||
valid, err = isValidClientCertificate(testCA, testCRL, revokedCertInfo, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.False(t, valid, "should return false")
|
||||
|
||||
|
@ -176,7 +178,7 @@ func Test_isValidClientCertificate(t *testing.T) {
|
|||
valid, err = isValidClientCertificate(testCA, testCRL, ClientCertificateInfo{
|
||||
Presented: true,
|
||||
Leaf: testValidCert,
|
||||
})
|
||||
}, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.True(t, valid, "should return true")
|
||||
})
|
||||
|
@ -186,7 +188,16 @@ func Test_isValidClientCertificate(t *testing.T) {
|
|||
Presented: true,
|
||||
Leaf: testValidIntermediateCert,
|
||||
Intermediates: testIntermediateCA,
|
||||
})
|
||||
}, noConstraints)
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.False(t, valid, "should return false")
|
||||
})
|
||||
t.Run("chain too deep", func(t *testing.T) {
|
||||
valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{
|
||||
Presented: true,
|
||||
Leaf: testValidIntermediateCert,
|
||||
Intermediates: testIntermediateCA,
|
||||
}, ClientCertConstraints{MaxVerifyDepth: 1})
|
||||
assert.NoError(t, err, "should not return an error")
|
||||
assert.False(t, valid, "should return false")
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue