Skip to content

Commit b446105

Browse files
committed
Move X.509 client certificate validation out of pkg/grpc
Right now we only perform client certificate validation within the context of gRPC request authentication. Within Bonanza I'm interested in also using X.509 client certificates outside of TLS, namely to perform ECDH to encrypt traffic between workers and clients. Let's decompose our bb_grpc.TLSClientCertificateAuthenticator, moving all certificate validation logic into bb_x509.ClientCertificateVerifier. This is similar to how JWT validation lives in pkg/jwt.
1 parent b484eba commit b446105

15 files changed

Lines changed: 494 additions & 224 deletions

pkg/grpc/BUILD.bazel

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ go_library(
4242
"//pkg/proto/auth",
4343
"//pkg/proto/configuration/grpc",
4444
"//pkg/util",
45+
"//pkg/x509",
4546
"@bazel_remote_apis//build/bazel/remote/execution/v2:remote_execution_go_proto",
4647
"@com_github_grpc_ecosystem_go_grpc_middleware//:go-grpc-middleware",
4748
"@com_github_grpc_ecosystem_go_grpc_prometheus//:go-grpc-prometheus",
@@ -103,7 +104,6 @@ go_test(
103104
"proto_trace_attributes_extractor_test.go",
104105
"request_headers_authenticator_test.go",
105106
"request_metadata_tracing_interceptor_test.go",
106-
"tls_client_certificate_authenticator_test.go",
107107
] + select({
108108
"@rules_go//go/platform:android": [
109109
"peer_transport_credentials_test.go",
@@ -138,7 +138,6 @@ go_test(
138138
"@io_opentelemetry_go_proto_otlp//common/v1:common",
139139
"@org_golang_google_grpc//:grpc",
140140
"@org_golang_google_grpc//codes",
141-
"@org_golang_google_grpc//credentials",
142141
"@org_golang_google_grpc//metadata",
143142
"@org_golang_google_grpc//peer",
144143
"@org_golang_google_grpc//status",

pkg/grpc/authenticator.go

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package grpc
22

33
import (
44
"context"
5-
"crypto/x509"
65

76
"github.com/buildbarn/bb-storage/pkg/auth"
87
"github.com/buildbarn/bb-storage/pkg/clock"
@@ -11,6 +10,7 @@ import (
1110
"github.com/buildbarn/bb-storage/pkg/program"
1211
configuration "github.com/buildbarn/bb-storage/pkg/proto/configuration/grpc"
1312
"github.com/buildbarn/bb-storage/pkg/util"
13+
"github.com/buildbarn/bb-storage/pkg/x509"
1414
"github.com/jmespath/go-jmespath"
1515

1616
"google.golang.org/grpc/codes"
@@ -68,24 +68,11 @@ func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolic
6868
case *configuration.AuthenticationPolicy_Deny:
6969
return NewDenyAuthenticator(policyKind.Deny), false, false, nil
7070
case *configuration.AuthenticationPolicy_TlsClientCertificate:
71-
clientCAs := x509.NewCertPool()
72-
if !clientCAs.AppendCertsFromPEM([]byte(policyKind.TlsClientCertificate.ClientCertificateAuthorities)) {
73-
return nil, false, false, status.Error(codes.InvalidArgument, "Failed to parse client certificate authorities")
74-
}
75-
validator, err := jmespath.Compile(policyKind.TlsClientCertificate.ValidationJmespathExpression)
76-
if err != nil {
77-
return nil, false, false, util.StatusWrap(err, "Failed to compile validation JMESPath expression")
78-
}
79-
metadataExtractor, err := jmespath.Compile(policyKind.TlsClientCertificate.MetadataExtractionJmespathExpression)
71+
clientCertificateVerifier, err := x509.NewClientCertificateVerifierFromConfiguration(policyKind.TlsClientCertificate)
8072
if err != nil {
81-
return nil, false, false, util.StatusWrap(err, "Failed to compile metadata extraction JMESPath expression")
73+
return nil, false, false, err
8274
}
83-
return NewTLSClientCertificateAuthenticator(
84-
clientCAs,
85-
clock.SystemClock,
86-
validator,
87-
metadataExtractor,
88-
), false, true, nil
75+
return NewTLSClientCertificateAuthenticator(clientCertificateVerifier), false, true, nil
8976
case *configuration.AuthenticationPolicy_Jwt:
9077
authorizationHeaderParser, err := jwt.NewAuthorizationHeaderParserFromConfiguration(policyKind.Jwt, group)
9178
if err != nil {

pkg/grpc/tls_client_certificate_authenticator.go

Lines changed: 5 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@ package grpc
22

33
import (
44
"context"
5-
"crypto/x509"
65

76
"github.com/buildbarn/bb-storage/pkg/auth"
8-
"github.com/buildbarn/bb-storage/pkg/clock"
9-
"github.com/buildbarn/bb-storage/pkg/util"
10-
"github.com/jmespath/go-jmespath"
7+
"github.com/buildbarn/bb-storage/pkg/x509"
118

129
"google.golang.org/grpc/codes"
1310
"google.golang.org/grpc/credentials"
@@ -16,22 +13,16 @@ import (
1613
)
1714

1815
type tlsClientCertificateAuthenticator struct {
19-
clientCAs *x509.CertPool
20-
clock clock.Clock
21-
validator *jmespath.JMESPath
22-
metadataExtractor *jmespath.JMESPath
16+
verifier *x509.ClientCertificateVerifier
2317
}
2418

2519
// NewTLSClientCertificateAuthenticator creates an Authenticator that
2620
// only grants access in case the client connected to the gRPC server
2721
// using a TLS client certificate that can be validated against the
2822
// chain of CAs used by the server.
29-
func NewTLSClientCertificateAuthenticator(clientCAs *x509.CertPool, clock clock.Clock, validator, metadataExtractor *jmespath.JMESPath) Authenticator {
23+
func NewTLSClientCertificateAuthenticator(verifier *x509.ClientCertificateVerifier) Authenticator {
3024
return &tlsClientCertificateAuthenticator{
31-
clientCAs: clientCAs,
32-
clock: clock,
33-
validator: validator,
34-
metadataExtractor: metadataExtractor,
25+
verifier: verifier,
3526
}
3627
}
3728

@@ -45,71 +36,5 @@ func (a *tlsClientCertificateAuthenticator) Authenticate(ctx context.Context) (*
4536
if !ok {
4637
return nil, status.Error(codes.Unauthenticated, "Connection was not established using TLS")
4738
}
48-
certs := tlsInfo.State.PeerCertificates
49-
if len(certs) == 0 {
50-
return nil, status.Error(codes.Unauthenticated, "Client provided no TLS client certificate")
51-
}
52-
53-
// Perform certificate verification.
54-
// TODO: Should this be memoized?
55-
opts := x509.VerifyOptions{
56-
Roots: a.clientCAs,
57-
CurrentTime: a.clock.Now(),
58-
Intermediates: x509.NewCertPool(),
59-
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
60-
}
61-
for _, cert := range certs[1:] {
62-
opts.Intermediates.AddCert(cert)
63-
}
64-
if _, err := certs[0].Verify(opts); err != nil {
65-
return nil, util.StatusWrapWithCode(err, codes.Unauthenticated, "Cannot validate TLS client certificate")
66-
}
67-
68-
searchContext := getClientCertificateJMESPathSearchContext(certs[0])
69-
70-
// Validate the client cert matches our expectations.
71-
validationResult, err := a.validator.Search(searchContext)
72-
if err != nil {
73-
return nil, util.StatusWrapWithCode(err, codes.Unauthenticated, "Cannot validate TLS client certificate claims")
74-
}
75-
if validationResult != true {
76-
return nil, status.Error(codes.Unauthenticated, "Rejected TLS client certificate claims")
77-
}
78-
79-
// Extract metadata from the client cert.
80-
metadataRaw, err := a.metadataExtractor.Search(searchContext)
81-
if err != nil {
82-
return nil, util.StatusWrapWithCode(err, codes.Unauthenticated, "Cannot extract metadata from TLS client certificate")
83-
}
84-
85-
return auth.NewAuthenticationMetadataFromRaw(metadataRaw)
86-
}
87-
88-
func getClientCertificateJMESPathSearchContext(cert *x509.Certificate) map[string]any {
89-
// We have to go through this copying and json dance in order to
90-
// ensure that we don't replace [] with null, and that we have the proper
91-
// types needed for JMESPath to search over without typing failures.
92-
93-
dnsNames := make([]any, 0, len(cert.DNSNames))
94-
for _, d := range cert.DNSNames {
95-
dnsNames = append(dnsNames, d)
96-
}
97-
emailAddresses := make([]any, 0, len(cert.EmailAddresses))
98-
for _, e := range cert.EmailAddresses {
99-
emailAddresses = append(emailAddresses, e)
100-
}
101-
102-
uris := make([]any, 0, len(cert.URIs))
103-
for _, e := range cert.URIs {
104-
uris = append(uris, e.String())
105-
}
106-
107-
// The data structure that users can search over
108-
searchContext := map[string]any{
109-
"dnsNames": dnsNames,
110-
"emailAddresses": emailAddresses,
111-
"uris": uris,
112-
}
113-
114-
return searchContext
39+
return a.verifier.VerifyClientCertificate(tlsInfo.State.PeerCertificates)
11540
}

pkg/proto/configuration/grpc/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ proto_library(
1111
"//pkg/proto/configuration/eviction:eviction_proto",
1212
"//pkg/proto/configuration/jwt:jwt_proto",
1313
"//pkg/proto/configuration/tls:tls_proto",
14+
"//pkg/proto/configuration/x509:x509_proto",
1415
"@protobuf//:duration_proto",
1516
"@protobuf//:empty_proto",
1617
"@protobuf//:struct_proto",
@@ -27,6 +28,7 @@ go_proto_library(
2728
"//pkg/proto/configuration/eviction",
2829
"//pkg/proto/configuration/jwt",
2930
"//pkg/proto/configuration/tls",
31+
"//pkg/proto/configuration/x509",
3032
],
3133
)
3234

pkg/proto/configuration/grpc/grpc.pb.go

Lines changed: 36 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)