Skip to content

Commit 6e83d58

Browse files
authored
Merge pull request #200 from SenseUnit/covert_iff
TLS ticket auth
2 parents 871d35d + 46ab1b7 commit 6e83d58

6 files changed

Lines changed: 345 additions & 5 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@ Authentication parameters are passed as URI via `-auth` parameter. Scheme of URI
302302
* `code` - optional parameter specifying HTTP response code. Default is 403.
303303
* `body` - optional parameter specifying file with response body.
304304
* `headers` - optional parameter specifying file with response headers. It uses format identical to request header file format used by `curl` program.
305+
* `tlscookie` - (EXPERIMENTAL) auth provider which grants access to whitelisted TLS session IDs. Whitelist is checked by query of another auth provider (provided as URL in `lookup` query parameter) with session ID as username and empty password. Example of auth parameter: `-auth tlscookie://?lookup=basicfile%3A%2F%2F%3Fpath%3D%2Fetc%2Fdumbproxy%2Fsessions`. Parameters of this scheme are:
306+
* `next` - optional URL specifying the next auth provider to chain to, if authentication succeeded.
307+
* `else` - optional URL specifying the next auth provider to chain to, if authentication failed.
308+
* `lookup` - mandatory URL specifying another auth provider queried for session validity (typically `basicfile` or some Redis-backed password auth). Queries to this lookup provider ask for validity of session providing hexadecimal session ID as username and empty string as password.
305309
306310
## Scripting
307311
@@ -640,6 +644,8 @@ Usage of /home/user/go/bin/dumbproxy:
640644
grace period during server shutdown (default 1s)
641645
-tls-alpn-enabled
642646
enable application protocol negotiation with TLS ALPN extension (default true)
647+
-tls-cookies
648+
mark TLS sessions with cookie-like unique session IDs (default true)
643649
-tls-session-key value
644650
override TLS server session keys. Key must be provided as hex-encoded 32-byte string. This option can be repeated multiple times, first key will be used to create session tickets. Empty value resets the list.
645651
-trusttunnel

auth/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ func NewAuth(paramstr string, logger *clog.CondLogger) (Auth, error) {
4141
return NewRejectHTTPAuth(url, logger)
4242
case "reject-static":
4343
return NewStaticRejectAuth(url, logger)
44+
case "tlscookie":
45+
return NewTLSCookieAuth(url, logger)
4446
default:
4547
return nil, errors.New("Unknown auth scheme")
4648
}

auth/tlscookie.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"net/url"
10+
"sync"
11+
12+
"github.com/hashicorp/go-multierror"
13+
14+
clog "github.com/SenseUnit/dumbproxy/log"
15+
"github.com/SenseUnit/dumbproxy/tlsutil"
16+
)
17+
18+
type sessionValidator interface {
19+
Valid(sessionID, _, userAddr string) bool
20+
}
21+
22+
type TLSCookieAuth struct {
23+
logger *clog.CondLogger
24+
stopOnce sync.Once
25+
next Auth
26+
reject Auth
27+
lookup sessionValidator
28+
}
29+
30+
func NewTLSCookieAuth(param_url *url.URL, logger *clog.CondLogger) (*TLSCookieAuth, error) {
31+
values, err := url.ParseQuery(param_url.RawQuery)
32+
if err != nil {
33+
return nil, err
34+
}
35+
auth := &TLSCookieAuth{
36+
logger: logger,
37+
}
38+
if lookupURL := values.Get("lookup"); lookupURL == "" {
39+
return nil, errors.New("\"lookup\" parameter is mandatory for TLS cookie auth provider")
40+
} else {
41+
lookupAuth, err := NewAuth(lookupURL, logger)
42+
if err != nil {
43+
return nil, fmt.Errorf("unable to construct lookup provider for TLS cookie auth provider: %w", err)
44+
}
45+
lookup, ok := lookupAuth.(sessionValidator)
46+
if !ok {
47+
return nil, fmt.Errorf("unable to construct TLS cookie auth provider: provided lookup provider %q is not suitable for session validation", lookupURL)
48+
}
49+
auth.lookup = lookup
50+
}
51+
if nextAuth := values.Get("next"); nextAuth != "" {
52+
nap, err := NewAuth(nextAuth, logger)
53+
if err != nil {
54+
return nil, fmt.Errorf("chained auth provider construction failed: %w", err)
55+
}
56+
auth.next = nap
57+
}
58+
if nextAuth := values.Get("else"); nextAuth != "" {
59+
nap, err := NewAuth(nextAuth, logger)
60+
if err != nil {
61+
return nil, fmt.Errorf("chained auth provider construction failed: %w", err)
62+
}
63+
auth.reject = nap
64+
}
65+
return auth, nil
66+
}
67+
68+
func (auth *TLSCookieAuth) Validate(ctx context.Context, wr http.ResponseWriter, req *http.Request) (string, bool) {
69+
sessionID, ok := tlsutil.TLSSessionIDFromContext(ctx)
70+
if !ok {
71+
auth.logger.Debug("tlscookie: no session extracted for %s", req.RemoteAddr)
72+
return auth.handleReject(ctx, wr, req)
73+
}
74+
if !auth.lookup.Valid(hex.EncodeToString(sessionID[:]), "", req.RemoteAddr) {
75+
auth.logger.Info("tlscookie: session ID %x from %s is not permitted", sessionID, req.RemoteAddr)
76+
return auth.handleReject(ctx, wr, req)
77+
}
78+
if auth.next != nil {
79+
return auth.next.Validate(ctx, wr, req)
80+
}
81+
return fmt.Sprintf("tlscookie:%x", sessionID), true
82+
}
83+
84+
func (auth *TLSCookieAuth) handleReject(ctx context.Context, wr http.ResponseWriter, req *http.Request) (string, bool) {
85+
if auth.reject != nil {
86+
return auth.reject.Validate(ctx, wr, req)
87+
}
88+
http.Error(wr, BAD_REQ_MSG, http.StatusBadRequest)
89+
return "", false
90+
}
91+
92+
func (auth *TLSCookieAuth) Close() error {
93+
var err error
94+
auth.stopOnce.Do(func() {
95+
if auth.next != nil {
96+
if closeErr := auth.next.Close(); closeErr != nil {
97+
err = multierror.Append(err, closeErr)
98+
}
99+
}
100+
if auth.reject != nil {
101+
if closeErr := auth.reject.Close(); closeErr != nil {
102+
err = multierror.Append(err, closeErr)
103+
}
104+
}
105+
})
106+
return err
107+
}

main.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ type CLIArgs struct {
314314
maxTLSVersion TLSVersionArg
315315
tlsALPNEnabled bool
316316
tlsSessionKeys [][32]byte
317+
tlsCookies bool
317318
bwLimit forward.LimitSpec
318319
bwBurst int64
319320
bwSeparate bool
@@ -505,6 +506,7 @@ func parse_args() *CLIArgs {
505506
args.tlsSessionKeys = append(args.tlsSessionKeys, [32]byte(key))
506507
return nil
507508
})
509+
flag.BoolVar(&args.tlsCookies, "tls-cookies", true, "mark TLS sessions with cookie-like unique session IDs")
508510
flag.Func("config", "read configuration from file with space-separated keys and values", readConfig)
509511
flag.Parse()
510512
// pull up remaining parameters from other BW-related arguments
@@ -612,6 +614,9 @@ func run() int {
612614
ttDemuxLogger := clog.NewCondLogger(log.New(logWriter, "TTDEMUX :",
613615
log.LstdFlags|log.Lshortfile),
614616
args.verbosity)
617+
tlsSessionLogger := clog.NewCondLogger(log.New(logWriter, "TLSSESS :",
618+
log.LstdFlags|log.Lshortfile),
619+
args.verbosity)
615620

616621
// setup auth provider
617622
authProvider, err := auth.NewAuth(args.auth, authLogger)
@@ -780,11 +785,12 @@ func run() int {
780785
}
781786

782787
if args.cert != "" {
783-
cfg, err1 := makeServerTLSConfig(args)
788+
cfg, err1 := makeServerTLSConfig(args, tlsSessionLogger)
784789
if err1 != nil {
785790
mainLogger.Critical("TLS config construction failed: %v", err1)
786791
return 3
787792
}
793+
listener = tlsutil.NewTaggedConnListener(listener) // attach DTO container
788794
listener = tls.NewListener(listener, cfg)
789795
} else if args.autocert {
790796
// cert caching chain
@@ -841,7 +847,7 @@ func run() int {
841847
http.ListenAndServe(args.autocertHTTP, m.HTTPHandler(nil)))
842848
}()
843849
}
844-
cfg, err := makeServerTLSConfig(args)
850+
cfg, err := makeServerTLSConfig(args, tlsSessionLogger)
845851
if err != nil {
846852
mainLogger.Critical("TLS config construction failed: %v", err)
847853
return 3
@@ -850,6 +856,7 @@ func run() int {
850856
if len(cfg.NextProtos) > 0 {
851857
cfg.NextProtos = append(cfg.NextProtos, acme.ALPNProto)
852858
}
859+
listener = tlsutil.NewTaggedConnListener(listener) // attach DTO container
853860
listener = tls.NewListener(listener, cfg)
854861
}
855862
defer listener.Close()
@@ -889,6 +896,9 @@ func run() int {
889896
BaseContext: func(_ net.Listener) context.Context {
890897
return stopContext
891898
},
899+
ConnContext: func(ctx context.Context, conn net.Conn) context.Context {
900+
return tlsutil.TLSSessionIDToContext(ctx, conn)
901+
},
892902
}
893903
if args.disableHTTP2 {
894904
server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
@@ -1003,8 +1013,8 @@ func run() int {
10031013
return 2
10041014
}
10051015

1006-
func makeServerTLSConfig(args *CLIArgs) (*tls.Config, error) {
1007-
cfg := tls.Config{
1016+
func makeServerTLSConfig(args *CLIArgs, logger *clog.CondLogger) (*tls.Config, error) {
1017+
cfg := &tls.Config{
10081018
MinVersion: uint16(args.minTLSVersion),
10091019
MaxVersion: uint16(args.maxTLSVersion),
10101020
}
@@ -1041,8 +1051,11 @@ func makeServerTLSConfig(args *CLIArgs) (*tls.Config, error) {
10411051
}
10421052
if len(args.tlsSessionKeys) > 0 {
10431053
cfg.SetSessionTicketKeys(args.tlsSessionKeys)
1054+
if args.tlsCookies {
1055+
cfg = tlsutil.EnableTLSCookies(cfg, logger)
1056+
}
10441057
}
1045-
return &cfg, nil
1058+
return cfg, nil
10461059
}
10471060

10481061
func readConfig(filename string) error {

tlsutil/session.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package tlsutil
2+
3+
import (
4+
"bytes"
5+
"context"
6+
crand "crypto/rand"
7+
"crypto/tls"
8+
"errors"
9+
"net"
10+
11+
clog "github.com/SenseUnit/dumbproxy/log"
12+
)
13+
14+
const tlsCookiePrefix = "dpSessionCookieV1="
15+
16+
type TLSSessionID = [16]byte
17+
18+
func NewTLSSessionID() (res TLSSessionID) {
19+
crand.Read(res[:])
20+
return
21+
}
22+
23+
func TLSSessionIDFromState(ss *tls.SessionState) (TLSSessionID, bool) {
24+
for _, tag := range ss.Extra {
25+
if !bytes.HasPrefix(tag, []byte(tlsCookiePrefix)) {
26+
continue
27+
}
28+
tag = tag[len(tlsCookiePrefix):]
29+
if len(tag) != len(TLSSessionID{}) {
30+
continue
31+
}
32+
return TLSSessionID(tag), true
33+
}
34+
return TLSSessionID{}, false
35+
}
36+
37+
type tlsSessionIDKey struct{}
38+
type connKey struct{}
39+
40+
func getTLSSessionID(conn ConnTagger) (TLSSessionID, bool) {
41+
saved, ok := conn.GetTag(tlsSessionIDKey{})
42+
if !ok {
43+
return TLSSessionID{}, false
44+
}
45+
val, ok := saved.(TLSSessionID)
46+
return val, ok
47+
}
48+
49+
func setTLSSessionID(conn ConnTagger, sessionID TLSSessionID) {
50+
conn.SetTag(tlsSessionIDKey{}, sessionID)
51+
}
52+
53+
func GetTLSSessionID(conn net.Conn) (TLSSessionID, bool) {
54+
tagger, ok := conn.(ConnTagger)
55+
if !ok {
56+
if netconner, ok := conn.(interface {
57+
NetConn() net.Conn
58+
}); ok {
59+
return GetTLSSessionID(netconner.NetConn())
60+
}
61+
return TLSSessionID{}, false
62+
}
63+
return getTLSSessionID(tagger)
64+
}
65+
66+
func TLSSessionIDToContext(ctx context.Context, conn net.Conn) context.Context {
67+
return context.WithValue(ctx, connKey{}, conn)
68+
}
69+
70+
func TLSSessionIDFromContext(ctx context.Context) (TLSSessionID, bool) {
71+
val := ctx.Value(connKey{})
72+
conn, ok := val.(net.Conn)
73+
if !ok {
74+
return TLSSessionID{}, false
75+
}
76+
return GetTLSSessionID(conn)
77+
}
78+
79+
func EnableTLSCookies(cfg *tls.Config, logger *clog.CondLogger) *tls.Config {
80+
getConfig := func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
81+
return cfg.Clone(), nil
82+
}
83+
if cfg.GetConfigForClient != nil {
84+
getConfig = cfg.GetConfigForClient
85+
}
86+
// this one will be returned as updated TLS config to outer function caller
87+
cfg = cfg.Clone()
88+
cfg.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
89+
conn, ok := chi.Conn.(ConnTagger)
90+
remoteAddr := chi.Conn.RemoteAddr().String()
91+
if !ok {
92+
return nil, errors.New("tlsCfg.GetConfigForClient: connection does is not a ConnTagger")
93+
}
94+
// this one holds closures which capture conn
95+
cfg, err := getConfig(chi)
96+
if err != nil {
97+
return nil, err
98+
}
99+
cfg.UnwrapSession = func(identity []byte, cs tls.ConnectionState) (*tls.SessionState, error) {
100+
ss, err := cfg.DecryptTicket(identity, cs)
101+
if err != nil {
102+
logger.Error("got error from TLS session ticket decryption: %v", err)
103+
return nil, err
104+
}
105+
if ss == nil {
106+
// nothing was decrypted, issue a new session
107+
sessionID := NewTLSSessionID()
108+
logger.Debug("assigning NEW session ID %x to connection from %s", sessionID, remoteAddr)
109+
setTLSSessionID(conn, sessionID)
110+
return nil, nil
111+
}
112+
if sessionID, ok := TLSSessionIDFromState(ss); ok {
113+
// valid session ID in ticket
114+
logger.Debug("recovered session ID = %x from %s", sessionID, remoteAddr)
115+
setTLSSessionID(conn, sessionID)
116+
} else {
117+
// no valid session ID in ticket (migrating outdated ticket?)
118+
sessionID = NewTLSSessionID()
119+
logger.Debug("session ID was NOT recovered from ticket from %s. assigning NEW session ID %x", remoteAddr, sessionID)
120+
setTLSSessionID(conn, sessionID)
121+
}
122+
return ss, nil
123+
}
124+
cfg.WrapSession = func(cs tls.ConnectionState, ss *tls.SessionState) ([]byte, error) {
125+
// is there session in TLS session state already?
126+
if sessionID, found := TLSSessionIDFromState(ss); found {
127+
logger.Warning("sessionState from %s already has sessionID %x", remoteAddr, sessionID)
128+
setTLSSessionID(conn, sessionID)
129+
return cfg.EncryptTicket(cs, ss)
130+
}
131+
// did we had a chance to assign a session ID to this connection?
132+
sessionID, ok := getTLSSessionID(conn)
133+
if ok {
134+
logger.Debug("sending new TLS ticket with old session ID %x to remote %s", sessionID, remoteAddr)
135+
} else {
136+
sessionID = NewTLSSessionID()
137+
setTLSSessionID(conn, sessionID)
138+
logger.Debug("sending new TLS ticket with NEW session ID %x to remote %s", sessionID, remoteAddr)
139+
}
140+
cookie := append([]byte(tlsCookiePrefix), sessionID[:]...)
141+
ss.Extra = append(ss.Extra, cookie)
142+
return cfg.EncryptTicket(cs, ss)
143+
}
144+
return cfg, nil
145+
}
146+
return cfg
147+
}

0 commit comments

Comments
 (0)