Skip to content

Commit 080f956

Browse files
credentials, transport, grpc : add a call option to override the :authority header on a per-RPC basis (#8068)
1 parent 6821606 commit 080f956

File tree

9 files changed

+425
-0
lines changed

9 files changed

+425
-0
lines changed

credentials/credentials.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ type AuthInfo interface {
120120
AuthType() string
121121
}
122122

123+
// AuthorityValidator validates the authority used to override the `:authority`
124+
// header. This is an optional interface that implementations of AuthInfo can
125+
// implement if they support per-RPC authority overrides. It is invoked when the
126+
// application attempts to override the HTTP/2 `:authority` header using the
127+
// CallAuthority call option.
128+
type AuthorityValidator interface {
129+
// ValidateAuthority checks the authority value used to override the
130+
// `:authority` header. The authority parameter is the override value
131+
// provided by the application via the CallAuthority option. This value
132+
// typically corresponds to the server hostname or endpoint the RPC is
133+
// targeting. It returns non-nil error if the validation fails.
134+
ValidateAuthority(authority string) error
135+
}
136+
123137
// ErrConnDispatched indicates that rawConn has been dispatched out of gRPC
124138
// and the caller should not close rawConn.
125139
var ErrConnDispatched = errors.New("credentials: rawConn is dispatched out of gRPC")
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
/*
2+
*
3+
* Copyright 2025 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package credentials_test
20+
21+
import (
22+
"context"
23+
"crypto/tls"
24+
"fmt"
25+
"net"
26+
"testing"
27+
"time"
28+
29+
"google.golang.org/grpc"
30+
"google.golang.org/grpc/codes"
31+
"google.golang.org/grpc/credentials"
32+
"google.golang.org/grpc/credentials/insecure"
33+
"google.golang.org/grpc/internal/stubserver"
34+
"google.golang.org/grpc/metadata"
35+
"google.golang.org/grpc/status"
36+
"google.golang.org/grpc/testdata"
37+
38+
testgrpc "google.golang.org/grpc/interop/grpc_testing"
39+
testpb "google.golang.org/grpc/interop/grpc_testing"
40+
)
41+
42+
func authorityChecker(ctx context.Context, wantAuthority string) error {
43+
md, ok := metadata.FromIncomingContext(ctx)
44+
if !ok {
45+
return status.Error(codes.InvalidArgument, "failed to parse metadata")
46+
}
47+
auths, ok := md[":authority"]
48+
if !ok {
49+
return status.Error(codes.InvalidArgument, "no authority header")
50+
}
51+
if len(auths) != 1 {
52+
return status.Errorf(codes.InvalidArgument, "expected exactly one authority header, got %v", auths)
53+
}
54+
if auths[0] != wantAuthority {
55+
return status.Errorf(codes.InvalidArgument, "invalid authority header %q, want %q", auths[0], wantAuthority)
56+
}
57+
return nil
58+
}
59+
60+
// Tests the `grpc.CallAuthority` option with TLS credentials. This test verifies
61+
// that the provided authority is correctly propagated to the server when a
62+
// correct authority is used.
63+
func (s) TestCorrectAuthorityWithTLSCreds(t *testing.T) {
64+
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
65+
if err != nil {
66+
t.Fatalf("Failed to load key pair: %s", err)
67+
}
68+
creds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "x.test.example.com")
69+
if err != nil {
70+
t.Fatalf("Failed to create credentials %v", err)
71+
}
72+
const authority = "auth.test.example.com"
73+
ss := &stubserver.StubServer{
74+
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
75+
if err := authorityChecker(ctx, authority); err != nil {
76+
return nil, err
77+
}
78+
return &testpb.Empty{}, nil
79+
},
80+
}
81+
if err := ss.StartServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert))); err != nil {
82+
t.Fatalf("Error starting endpoint server: %v", err)
83+
}
84+
defer ss.Stop()
85+
86+
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(creds))
87+
if err != nil {
88+
t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err)
89+
}
90+
defer cc.Close()
91+
92+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
93+
defer cancel()
94+
95+
if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(authority)); status.Code(err) != codes.OK {
96+
t.Fatalf("EmptyCall() returned status %v, want %v", status.Code(err), codes.OK)
97+
}
98+
99+
}
100+
101+
// Tests the `grpc.CallAuthority` option with TLS credentials. This test verifies
102+
// that the RPC fails with `UNAVAILABLE` status code and doesn't reach the server
103+
// when an incorrect authority is used.
104+
func (s) TestIncorrectAuthorityWithTLS(t *testing.T) {
105+
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
106+
if err != nil {
107+
t.Fatalf("Failed to load key pair: %s", err)
108+
}
109+
creds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "x.test.example.com")
110+
if err != nil {
111+
t.Fatalf("Failed to create credentials %v", err)
112+
}
113+
114+
serverCalled := make(chan struct{})
115+
ss := &stubserver.StubServer{
116+
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
117+
close(serverCalled)
118+
return nil, nil
119+
},
120+
}
121+
if err := ss.StartServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert))); err != nil {
122+
t.Fatalf("Error starting endpoint server: %v", err)
123+
}
124+
defer ss.Stop()
125+
126+
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(creds))
127+
if err != nil {
128+
t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err)
129+
}
130+
defer cc.Close()
131+
132+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
133+
defer cancel()
134+
135+
const authority = "auth.example.com"
136+
if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(authority)); status.Code(err) != codes.Unavailable {
137+
t.Fatalf("EmptyCall() returned status %v, want %v", status.Code(err), codes.Unavailable)
138+
}
139+
select {
140+
case <-serverCalled:
141+
t.Fatalf("Server handler should not have been called")
142+
case <-time.After(defaultTestShortTimeout):
143+
}
144+
}
145+
146+
// Tests the scenario where the `grpc.CallAuthority` call option is used with
147+
// insecure transport credentials. The test verifies that the specified
148+
// authority is correctly propagated to the server.
149+
func (s) TestAuthorityCallOptionWithInsecureCreds(t *testing.T) {
150+
const authority = "test.server.name"
151+
152+
ss := &stubserver.StubServer{
153+
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
154+
if err := authorityChecker(ctx, authority); err != nil {
155+
return nil, err
156+
}
157+
return &testpb.Empty{}, nil
158+
},
159+
}
160+
if err := ss.Start(nil); err != nil {
161+
t.Fatalf("Error starting endpoint server: %v", err)
162+
}
163+
defer ss.Stop()
164+
165+
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
166+
if err != nil {
167+
t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err)
168+
}
169+
defer cc.Close()
170+
171+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
172+
defer cancel()
173+
if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(authority)); err != nil {
174+
t.Fatalf("EmptyCall() rpc failed: %v", err)
175+
}
176+
}
177+
178+
// testAuthInfoNoValidator implements only credentials.AuthInfo and not
179+
// credentials.AuthorityValidator.
180+
type testAuthInfoNoValidator struct{}
181+
182+
// AuthType returns the authentication type.
183+
func (testAuthInfoNoValidator) AuthType() string {
184+
return "test"
185+
}
186+
187+
// testAuthInfoWithValidator implements both credentials.AuthInfo and
188+
// credentials.AuthorityValidator.
189+
type testAuthInfoWithValidator struct {
190+
validAuthority string
191+
}
192+
193+
// AuthType returns the authentication type.
194+
func (testAuthInfoWithValidator) AuthType() string {
195+
return "test"
196+
}
197+
198+
// ValidateAuthority implements credentials.AuthorityValidator.
199+
func (v testAuthInfoWithValidator) ValidateAuthority(authority string) error {
200+
if authority == v.validAuthority {
201+
return nil
202+
}
203+
return fmt.Errorf("invalid authority %q, want %q", authority, v.validAuthority)
204+
}
205+
206+
// testCreds is a test TransportCredentials that can optionally support
207+
// authority validation.
208+
type testCreds struct {
209+
authority string
210+
}
211+
212+
// ClientHandshake performs the client-side handshake.
213+
func (c *testCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
214+
if c.authority != "" {
215+
return rawConn, testAuthInfoWithValidator{validAuthority: c.authority}, nil
216+
}
217+
return rawConn, testAuthInfoNoValidator{}, nil
218+
}
219+
220+
// ServerHandshake performs the server-side handshake.
221+
func (c *testCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
222+
if c.authority != "" {
223+
return rawConn, testAuthInfoWithValidator{validAuthority: c.authority}, nil
224+
}
225+
return rawConn, testAuthInfoNoValidator{}, nil
226+
}
227+
228+
// Clone creates a copy of testCreds.
229+
func (c *testCreds) Clone() credentials.TransportCredentials {
230+
return &testCreds{authority: c.authority}
231+
}
232+
233+
// Info provides protocol information.
234+
func (c *testCreds) Info() credentials.ProtocolInfo {
235+
return credentials.ProtocolInfo{}
236+
}
237+
238+
// OverrideServerName overrides the server name used for verification.
239+
func (c *testCreds) OverrideServerName(serverName string) error {
240+
return nil
241+
}
242+
243+
// TestAuthorityValidationFailureWithCustomCreds tests the `grpc.CallAuthority`
244+
// call option using custom credentials. It covers two failure scenarios:
245+
// - The credentials implement AuthorityValidator but authority used to override
246+
// is not valid.
247+
// - The credentials do not implement AuthorityValidator, but an authority
248+
// override is specified.
249+
// In both cases, the RPC is expected to fail with an `UNAVAILABLE` status code.
250+
func (s) TestAuthorityValidationFailureWithCustomCreds(t *testing.T) {
251+
tests := []struct {
252+
name string
253+
creds credentials.TransportCredentials
254+
authority string
255+
}{
256+
{
257+
name: "IncorrectAuthorityWithFakeCreds",
258+
authority: "auth.example.com",
259+
creds: &testCreds{authority: "auth.test.example.com"},
260+
},
261+
{
262+
name: "FakeCredsWithNoAuthValidator",
263+
creds: &testCreds{},
264+
authority: "auth.test.example.com",
265+
},
266+
}
267+
for _, tt := range tests {
268+
t.Run(tt.name, func(t *testing.T) {
269+
serverCalled := make(chan struct{})
270+
ss := stubserver.StubServer{
271+
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
272+
close(serverCalled)
273+
return nil, nil
274+
},
275+
}
276+
if err := ss.StartServer(); err != nil {
277+
t.Fatalf("Failed to start stub server: %v", err)
278+
}
279+
defer ss.Stop()
280+
281+
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(tt.creds))
282+
if err != nil {
283+
t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err)
284+
}
285+
defer cc.Close()
286+
287+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
288+
defer cancel()
289+
if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(tt.authority)); status.Code(err) != codes.Unavailable {
290+
t.Fatalf("EmptyCall() returned status %v, want %v", status.Code(err), codes.Unavailable)
291+
}
292+
select {
293+
case <-serverCalled:
294+
t.Fatalf("Server should not have been called")
295+
case <-time.After(defaultTestShortTimeout):
296+
}
297+
})
298+
}
299+
300+
}
301+
302+
// TestCorrectAuthorityWithCustomCreds tests the `grpc.CallAuthority` call
303+
// option using custom credentials. It verifies that the provided authority is
304+
// correctly propagated to the server when a correct authority is used.
305+
func (s) TestCorrectAuthorityWithCustomCreds(t *testing.T) {
306+
const authority = "auth.test.example.com"
307+
creds := &testCreds{authority: "auth.test.example.com"}
308+
ss := stubserver.StubServer{
309+
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
310+
if err := authorityChecker(ctx, authority); err != nil {
311+
return nil, err
312+
}
313+
return &testpb.Empty{}, nil
314+
},
315+
}
316+
if err := ss.StartServer(); err != nil {
317+
t.Fatalf("Failed to start stub server: %v", err)
318+
}
319+
defer ss.Stop()
320+
321+
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(creds))
322+
if err != nil {
323+
t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err)
324+
}
325+
defer cc.Close()
326+
327+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
328+
defer cancel()
329+
if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(authority)); status.Code(err) != codes.OK {
330+
t.Fatalf("EmptyCall() returned status %v, want %v", status.Code(err), codes.OK)
331+
}
332+
}

credentials/insecure/insecure.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ func (info) AuthType() string {
7171
return "insecure"
7272
}
7373

74+
// ValidateAuthority allows any value to be overridden for the :authority
75+
// header.
76+
func (info) ValidateAuthority(string) error {
77+
return nil
78+
}
79+
7480
// insecureBundle implements an insecure bundle.
7581
// An insecure bundle provides a thin wrapper around insecureTC to support
7682
// the credentials.Bundle interface.

credentials/tls.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"context"
2323
"crypto/tls"
2424
"crypto/x509"
25+
"errors"
2526
"fmt"
2627
"net"
2728
"net/url"
@@ -50,6 +51,21 @@ func (t TLSInfo) AuthType() string {
5051
return "tls"
5152
}
5253

54+
// ValidateAuthority validates the provided authority being used to override the
55+
// :authority header by verifying it against the peer certificates. It returns a
56+
// non-nil error if the validation fails.
57+
func (t TLSInfo) ValidateAuthority(authority string) error {
58+
var errs []error
59+
for _, cert := range t.State.PeerCertificates {
60+
var err error
61+
if err = cert.VerifyHostname(authority); err == nil {
62+
return nil
63+
}
64+
errs = append(errs, err)
65+
}
66+
return fmt.Errorf("credentials: invalid authority %q: %v", authority, errors.Join(errs...))
67+
}
68+
5369
// cipherSuiteLookup returns the string version of a TLS cipher suite ID.
5470
func cipherSuiteLookup(cipherSuiteID uint16) string {
5571
for _, s := range tls.CipherSuites() {

0 commit comments

Comments
 (0)