Skip to content

Commit 4fd5c54

Browse files
committed
drpc: add TLS certificate handling and metadata infra for auth interceptors
This commit adds infrastructure needed for authentication interceptors: 1. New drpcctx/tlscert.go: Functions to store/retrieve TLS peer certificates in context 2. Server-side TLS certificate extraction in drpcserver 3. Improved metadata API with ClearContext, ClearContextExcept, and GetValue functions 4. Client-side per-RPC metadata support via WithPerRPCMetadata option
1 parent 7e672f3 commit 4fd5c54

File tree

5 files changed

+101
-1
lines changed

5 files changed

+101
-1
lines changed

drpcclient/clientconn.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"storj.io/drpc"
7+
"storj.io/drpc/drpcmetadata"
78
)
89

910
// ClientConn represents a DRPC client connection, with support for configuring the
@@ -33,6 +34,9 @@ func finalInvoker(ctx context.Context, rpc string, enc drpc.Encoding, in, out dr
3334
}
3435

3536
func (c *ClientConn) Invoke(ctx context.Context, rpc string, enc drpc.Encoding, in, out drpc.Message) error {
37+
if c.dopts.perRPCMetadata != nil {
38+
ctx = drpcmetadata.AddPairs(ctx, c.dopts.perRPCMetadata)
39+
}
3640
if c.dopts.unaryInt != nil {
3741
return c.dopts.unaryInt(ctx, rpc, enc, in, out, c, finalInvoker)
3842
}
@@ -45,6 +49,9 @@ func finalStreamer(ctx context.Context, rpc string, enc drpc.Encoding, cc *Clien
4549
}
4650

4751
func (c *ClientConn) NewStream(ctx context.Context, rpc string, enc drpc.Encoding) (drpc.Stream, error) {
52+
if c.dopts.perRPCMetadata != nil {
53+
ctx = drpcmetadata.AddPairs(ctx, c.dopts.perRPCMetadata)
54+
}
4855
if c.dopts.streamInt != nil {
4956
return c.dopts.streamInt(ctx, rpc, enc, c, finalStreamer)
5057
}

drpcclient/dialoptions.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ type dialOptions struct {
88

99
unaryInts []UnaryClientInterceptor
1010
streamInts []StreamClientInterceptor
11+
12+
perRPCMetadata map[string]string
1113
}
1214

1315
// DialOption configures how we set up the client connection.
@@ -32,3 +34,9 @@ func WithChainStreamInterceptor(ints ...StreamClientInterceptor) DialOption {
3234
opt.streamInts = append(opt.streamInts, ints...)
3335
}
3436
}
37+
38+
func WithPerRPCMetadata(metadata map[string]string) DialOption {
39+
return func(opt *dialOptions) {
40+
opt.perRPCMetadata = metadata
41+
}
42+
}

drpcctx/tlscert.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package drpcctx
2+
3+
import (
4+
"context"
5+
"crypto/x509"
6+
)
7+
8+
// PeerConnectionInfo contains TLS peer connection information.
9+
type PeerConnectionInfo struct {
10+
Certificates []*x509.Certificate
11+
}
12+
13+
// TLSPeerCertKey is used to store TLS peer connection info in the context.
14+
type TLSPeerCertKey struct{}
15+
16+
// WithPeerConnectionInfo associates the peer connection information of the TLS connection
17+
// with the context.
18+
func WithPeerConnectionInfo(ctx context.Context, info PeerConnectionInfo) context.Context {
19+
return context.WithValue(ctx, TLSPeerCertKey{}, info)
20+
}
21+
22+
// GetPeerConnectionInfo returns the TLS peer connection information associated with the
23+
// context and a bool indicating if it existed.
24+
func GetPeerConnectionInfo(ctx context.Context) (PeerConnectionInfo, bool) {
25+
peerConnectionInfo, ok := ctx.Value(TLSPeerCertKey{}).(PeerConnectionInfo)
26+
return peerConnectionInfo, ok
27+
}

drpcmetadata/metadata.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,28 @@ func Decode(buf []byte) (map[string]string, error) {
5050

5151
type metadataKey struct{}
5252

53+
// ClearContext removes all metadata from the context and returns a new context
54+
// with no metadata attached.
55+
func ClearContext(ctx context.Context) context.Context {
56+
return context.WithValue(ctx, metadataKey{}, nil)
57+
}
58+
59+
// ClearContextExcept removes all metadata from the context except for the
60+
// specified key. If the specified key doesn't exist in the metadata, it clears
61+
// all metadata. Returns a new context with only the specified key-value pair
62+
// preserved.
63+
func ClearContextExcept(ctx context.Context, key string) context.Context {
64+
md, ok := Get(ctx)
65+
if !ok {
66+
return ClearContext(ctx)
67+
}
68+
value, ok := md[key]
69+
if !ok {
70+
return ClearContext(ctx)
71+
}
72+
return context.WithValue(ctx, metadataKey{}, map[string]string{key: value})
73+
}
74+
5375
// Add associates a key/value pair on the context.
5476
func Add(ctx context.Context, key, value string) context.Context {
5577
metadata, ok := Get(ctx)
@@ -66,3 +88,13 @@ func Get(ctx context.Context) (map[string]string, bool) {
6688
metadata, ok := ctx.Value(metadataKey{}).(map[string]string)
6789
return metadata, ok
6890
}
91+
92+
// GetValue retrieves a specific value by key from the context's metadata.
93+
func GetValue(ctx context.Context, key string) (string, bool) {
94+
metadata, ok := Get(ctx)
95+
if !ok {
96+
return "", false
97+
}
98+
val, ok := metadata[key]
99+
return val, ok
100+
}

drpcserver/server.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ package drpcserver
55

66
import (
77
"context"
8+
"crypto/tls"
89
"net"
910
"sync"
1011
"time"
1112

1213
"github.com/zeebo/errs"
13-
1414
"storj.io/drpc"
1515
"storj.io/drpc/drpccache"
1616
"storj.io/drpc/drpcctx"
@@ -92,6 +92,32 @@ func (s *Server) getStats(rpc string) *drpcstats.Stats {
9292

9393
// ServeOne serves a single set of rpcs on the provided transport.
9494
func (s *Server) ServeOne(ctx context.Context, tr drpc.Transport) (err error) {
95+
// Check if the transport is a TLS connection
96+
if tlsConn, ok := tr.(*tls.Conn); ok {
97+
// Manually perform the TLS handshake to access peer certificate
98+
// information. In Go's TLS implementation, the handshake is normally
99+
// performed lazily on the first read/write operation. However, the
100+
// transport received by ServeOne hasn't performed any I/O yet, so
101+
// ConnectionState() would be empty. Only after the handshake completes
102+
// is ConnectionState populated with peer certificates and other
103+
// connection details that we need for authentication context.
104+
//
105+
// This explicit Handshake() call is safe and appropriate here. The
106+
// connection hasn't started processing requests yet, so we're not
107+
// interrupting any ongoing communication. Even if we didn't call it
108+
// explicitly, the first read/write operation would call it internally
109+
// anyway.
110+
err := tlsConn.Handshake()
111+
if err != nil {
112+
return err
113+
}
114+
state := tlsConn.ConnectionState()
115+
if len(state.PeerCertificates) > 0 {
116+
ctx = drpcctx.WithPeerConnectionInfo(
117+
ctx, drpcctx.PeerConnectionInfo{Certificates: state.PeerCertificates})
118+
}
119+
}
120+
95121
man := drpcmanager.NewWithOptions(tr, s.opts.Manager)
96122
defer func() { err = errs.Combine(err, man.Close()) }()
97123

0 commit comments

Comments
 (0)