authorize: do not rely on Envoy client cert validation (#4438)

Partially revert #4374: do not record the peerCertificateValidated()
result as reported by Envoy, as this does not work correctly for resumed
TLS sessions. Instead always record the certificate chain as presented
by the client. Remove the corresponding ClientCertificateInfo Validated
field, and update affected code accordingly. Skip the CRL integration
test case for now.
This commit is contained in:
Kenneth Jenkins 2023-08-03 10:45:55 -07:00 committed by GitHub
parent 465de43e67
commit e91600c158
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 12 additions and 69 deletions

View file

@ -63,22 +63,14 @@ func NewRequestHTTP(
// ClientCertificateInfo contains information about the certificate presented // ClientCertificateInfo contains information about the certificate presented
// by the client (if any). // by the client (if any).
type ClientCertificateInfo struct { type ClientCertificateInfo struct {
// Presented is true if the client presented any certificate at all. // Presented is true if the client presented a certificate.
Presented bool `json:"presented"` Presented bool `json:"presented"`
// Validated is true if the client presented a valid certificate with a // Leaf contains the leaf client certificate (unvalidated).
// trust chain rooted at any of the CAs configured within the Envoy
// listener. If any routes define a tls_downstream_client_ca, additional
// validation is required (for all routes).
Validated bool `json:"validated"`
// Leaf contains the leaf client certificate, provided that the certificate
// validated successfully.
Leaf string `json:"leaf,omitempty"` Leaf string `json:"leaf,omitempty"`
// Intermediates contains the remainder of the client certificate chain as // Intermediates contains the remainder of the client certificate chain as
// it was originally presented by the client, provided that the client // it was originally presented by the client (unvalidated).
// certificate validated successfully.
Intermediates string `json:"intermediates,omitempty"` Intermediates string `json:"intermediates,omitempty"`
} }

View file

@ -123,7 +123,6 @@ func TestEvaluator(t *testing.T) {
validCertInfo := ClientCertificateInfo{ validCertInfo := ClientCertificateInfo{
Presented: true, Presented: true,
Validated: true,
Leaf: testValidCert, Leaf: testValidCert,
} }
@ -167,23 +166,12 @@ func TestEvaluator(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, NewRuleResult(true, criteria.ReasonClientCertificateRequired), res.Deny) assert.Equal(t, NewRuleResult(true, criteria.ReasonClientCertificateRequired), res.Deny)
}) })
t.Run("invalid (Envoy)", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
res, err := eval(t, options, nil, &Request{
Policy: &policies[10],
HTTP: RequestHTTP{
ClientCertificate: ClientCertificateInfo{Presented: true},
},
})
require.NoError(t, err)
assert.Equal(t, NewRuleResult(true, criteria.ReasonInvalidClientCertificate), res.Deny)
})
t.Run("invalid (authorize)", func(t *testing.T) {
res, err := eval(t, options, nil, &Request{ res, err := eval(t, options, nil, &Request{
Policy: &policies[10], Policy: &policies[10],
HTTP: RequestHTTP{ HTTP: RequestHTTP{
ClientCertificate: ClientCertificateInfo{ ClientCertificate: ClientCertificateInfo{
Presented: true, Presented: true,
Validated: true,
Leaf: testUnsignedCert, Leaf: testUnsignedCert,
}, },
}, },

View file

@ -21,7 +21,7 @@ func isValidClientCertificate(ca string, certInfo ClientCertificateInfo) (bool,
cert := certInfo.Leaf cert := certInfo.Leaf
if !certInfo.Validated || cert == "" { if cert == "" {
return false, nil return false, nil
} }

View file

@ -107,25 +107,14 @@ func Test_isValidClientCertificate(t *testing.T) {
t.Run("valid cert", func(t *testing.T) { t.Run("valid cert", func(t *testing.T) {
valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{ valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{
Presented: true, Presented: true,
Validated: true,
Leaf: testValidCert, Leaf: testValidCert,
}) })
assert.NoError(t, err, "should not return an error") assert.NoError(t, err, "should not return an error")
assert.True(t, valid, "should return true") assert.True(t, valid, "should return true")
}) })
t.Run("cert not externally validated", func(t *testing.T) {
valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{
Presented: true,
Validated: false,
Leaf: testValidCert,
})
assert.NoError(t, err, "should not return an error")
assert.False(t, valid, "should return false")
})
t.Run("unsigned cert", func(t *testing.T) { t.Run("unsigned cert", func(t *testing.T) {
valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{ valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{
Presented: true, Presented: true,
Validated: true,
Leaf: testUnsignedCert, Leaf: testUnsignedCert,
}) })
assert.NoError(t, err, "should not return an error") assert.NoError(t, err, "should not return an error")
@ -134,7 +123,6 @@ func Test_isValidClientCertificate(t *testing.T) {
t.Run("not a cert", func(t *testing.T) { t.Run("not a cert", func(t *testing.T) {
valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{ valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{
Presented: true, Presented: true,
Validated: true,
Leaf: "WHATEVER!", Leaf: "WHATEVER!",
}) })
assert.Error(t, err, "should return an error") assert.Error(t, err, "should return an error")

View file

@ -186,7 +186,6 @@ func getClientCertificateInfo(
return c return c
} }
c.Presented = metadata.Fields["presented"].GetBoolValue() c.Presented = metadata.Fields["presented"].GetBoolValue()
c.Validated = metadata.Fields["validated"].GetBoolValue()
escapedChain := metadata.Fields["chain"].GetStringValue() escapedChain := metadata.Fields["chain"].GetStringValue()
if escapedChain == "" { if escapedChain == "" {
// No validated client certificate. // No validated client certificate.

View file

@ -80,7 +80,6 @@ func Test_getEvaluatorRequest(t *testing.T) {
"com.pomerium.client-certificate-info": { "com.pomerium.client-certificate-info": {
Fields: map[string]*structpb.Value{ Fields: map[string]*structpb.Value{
"presented": structpb.NewBoolValue(true), "presented": structpb.NewBoolValue(true),
"validated": structpb.NewBoolValue(true),
"chain": structpb.NewStringValue(url.QueryEscape(certPEM)), "chain": structpb.NewStringValue(url.QueryEscape(certPEM)),
}, },
}, },
@ -107,7 +106,6 @@ func Test_getEvaluatorRequest(t *testing.T) {
}, },
evaluator.ClientCertificateInfo{ evaluator.ClientCertificateInfo{
Presented: true, Presented: true,
Validated: true,
Leaf: certPEM[1:] + "\n", Leaf: certPEM[1:] + "\n",
Intermediates: "", Intermediates: "",
}, },
@ -202,7 +200,6 @@ fYCZHo3CID0gRSemaQ/jYMgyeBFrHIr6icZh
cases := []struct { cases := []struct {
label string label string
presented bool presented bool
validated bool
chain string chain string
expected evaluator.ClientCertificateInfo expected evaluator.ClientCertificateInfo
expectedLog string expectedLog string
@ -210,41 +207,26 @@ fYCZHo3CID0gRSemaQ/jYMgyeBFrHIr6icZh
{ {
"not presented", "not presented",
false, false,
false,
"", "",
evaluator.ClientCertificateInfo{}, evaluator.ClientCertificateInfo{},
"", "",
}, },
{ {
"presented but invalid", "presented",
true,
false,
"",
evaluator.ClientCertificateInfo{
Presented: true,
},
"",
},
{
"validated",
true,
true, true,
url.QueryEscape(leafPEM), url.QueryEscape(leafPEM),
evaluator.ClientCertificateInfo{ evaluator.ClientCertificateInfo{
Presented: true, Presented: true,
Validated: true,
Leaf: leafPEM, Leaf: leafPEM,
}, },
"", "",
}, },
{ {
"validated with intermediates", "presented with intermediates",
true,
true, true,
url.QueryEscape(leafPEM + intermediatePEM + rootPEM), url.QueryEscape(leafPEM + intermediatePEM + rootPEM),
evaluator.ClientCertificateInfo{ evaluator.ClientCertificateInfo{
Presented: true, Presented: true,
Validated: true,
Leaf: leafPEM, Leaf: leafPEM,
Intermediates: intermediatePEM + rootPEM, Intermediates: intermediatePEM + rootPEM,
}, },
@ -253,7 +235,6 @@ fYCZHo3CID0gRSemaQ/jYMgyeBFrHIr6icZh
{ {
"invalid chain URL encoding", "invalid chain URL encoding",
false, false,
false,
"invalid%URL%encoding", "invalid%URL%encoding",
evaluator.ClientCertificateInfo{}, evaluator.ClientCertificateInfo{},
`{"level":"warn","chain":"invalid%URL%encoding","error":"invalid URL escape \"%UR\"","message":"received unexpected client certificate \"chain\" value"} `{"level":"warn","chain":"invalid%URL%encoding","error":"invalid URL escape \"%UR\"","message":"received unexpected client certificate \"chain\" value"}
@ -262,11 +243,9 @@ fYCZHo3CID0gRSemaQ/jYMgyeBFrHIr6icZh
{ {
"invalid chain PEM encoding", "invalid chain PEM encoding",
true, true,
true,
"not valid PEM data", "not valid PEM data",
evaluator.ClientCertificateInfo{ evaluator.ClientCertificateInfo{
Presented: true, Presented: true,
Validated: true,
}, },
`{"level":"warn","chain":"not valid PEM data","message":"received unexpected client certificate \"chain\" value (no PEM block found)"} `{"level":"warn","chain":"not valid PEM data","message":"received unexpected client certificate \"chain\" value (no PEM block found)"}
`, `,
@ -285,7 +264,6 @@ fYCZHo3CID0gRSemaQ/jYMgyeBFrHIr6icZh
metadata := &structpb.Struct{ metadata := &structpb.Struct{
Fields: map[string]*structpb.Value{ Fields: map[string]*structpb.Value{
"presented": structpb.NewBoolValue(c.presented), "presented": structpb.NewBoolValue(c.presented),
"validated": structpb.NewBoolValue(c.validated),
"chain": structpb.NewStringValue(c.chain), "chain": structpb.NewStringValue(c.chain),
}, },
} }

View file

@ -3,12 +3,8 @@ function envoy_on_request(request_handle)
local ssl = request_handle:streamInfo():downstreamSslConnection() local ssl = request_handle:streamInfo():downstreamSslConnection()
metadata:set("com.pomerium.client-certificate-info", "presented", metadata:set("com.pomerium.client-certificate-info", "presented",
ssl:peerCertificatePresented()) ssl:peerCertificatePresented())
local validated = ssl:peerCertificateValidated() metadata:set("com.pomerium.client-certificate-info", "chain",
metadata:set("com.pomerium.client-certificate-info", "validated", validated) ssl:urlEncodedPemEncodedPeerCertificateChain())
if validated then
metadata:set("com.pomerium.client-certificate-info", "chain",
ssl:urlEncodedPemEncodedPeerCertificateChain())
end
end end
function envoy_on_response(response_handle) end function envoy_on_response(response_handle) end

View file

@ -38,7 +38,7 @@
"typedConfig": { "typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua", "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua",
"defaultSourceCode": { "defaultSourceCode": {
"inlineString": "function envoy_on_request(request_handle)\n local metadata = request_handle:streamInfo():dynamicMetadata()\n local ssl = request_handle:streamInfo():downstreamSslConnection()\n metadata:set(\"com.pomerium.client-certificate-info\", \"presented\",\n ssl:peerCertificatePresented())\n local validated = ssl:peerCertificateValidated()\n metadata:set(\"com.pomerium.client-certificate-info\", \"validated\", validated)\n if validated then\n metadata:set(\"com.pomerium.client-certificate-info\", \"chain\",\n ssl:urlEncodedPemEncodedPeerCertificateChain())\n end\nend\n\nfunction envoy_on_response(response_handle) end\n" "inlineString": "function envoy_on_request(request_handle)\n local metadata = request_handle:streamInfo():dynamicMetadata()\n local ssl = request_handle:streamInfo():downstreamSslConnection()\n metadata:set(\"com.pomerium.client-certificate-info\", \"presented\",\n ssl:peerCertificatePresented())\n metadata:set(\"com.pomerium.client-certificate-info\", \"chain\",\n ssl:urlEncodedPemEncodedPeerCertificateChain())\nend\n\nfunction envoy_on_response(response_handle) end\n"
} }
} }
}, },

View file

@ -393,6 +393,8 @@ func TestDownstreamClientCA(t *testing.T) {
assert.Equal(t, "/", result.Path) assert.Equal(t, "/", result.Path)
}) })
t.Run("revoked client cert", func(t *testing.T) { t.Run("revoked client cert", func(t *testing.T) {
t.Skip("CRL support must be reimplemented first")
// Configure an http.Client with a revoked client certificate. // Configure an http.Client with a revoked client certificate.
cert := loadCertificate(t, "downstream-1-client-revoked") cert := loadCertificate(t, "downstream-1-client-revoked")
client, transport := getClientWithTransport(t) client, transport := getClientWithTransport(t)