|
5 | 5 |
|
6 | 6 | package security |
7 | 7 |
|
8 | | -import "crypto/tls" |
| 8 | +import ( |
| 9 | + "crypto/tls" |
| 10 | + "net" |
| 11 | + |
| 12 | + "github.com/cockroachdb/cockroach/pkg/util/syncutil" |
| 13 | + "github.com/cockroachdb/errors" |
| 14 | + "golang.org/x/exp/slices" |
| 15 | +) |
9 | 16 |
|
10 | 17 | // RecommendedCipherSuites returns a list of enabled TLS 1.2 cipher |
11 | 18 | // suites. The order of the list is ignored; prioritization of cipher |
@@ -69,3 +76,164 @@ func OldCipherSuites() []uint16 { |
69 | 76 | tls.TLS_RSA_WITH_AES_256_CBC_SHA, |
70 | 77 | } |
71 | 78 | } |
| 79 | + |
| 80 | +type tlsRestrictConfiguration struct { |
| 81 | + syncutil.RWMutex |
| 82 | + c []string |
| 83 | + restrictFn func(*tls.Conn) (error net.Error) |
| 84 | +} |
| 85 | + |
| 86 | +var tlsRestrictConfig = tlsRestrictConfiguration{ |
| 87 | + c: []string{}, |
| 88 | + restrictFn: func(tlsConn *tls.Conn) (error net.Error) { return }, |
| 89 | +} |
| 90 | + |
| 91 | +type allowedTLSCiphers struct { |
| 92 | + ciphersMapByName map[string]uint16 |
| 93 | + ciphersMapByID map[uint16]string |
| 94 | +} |
| 95 | + |
| 96 | +// getAllowedCiphersMapByName returns map of allowed cipher names to cipher ID. |
| 97 | +func (ciphers *allowedTLSCiphers) getAllowedCiphersMapByName() map[string]uint16 { |
| 98 | + return ciphers.ciphersMapByName |
| 99 | +} |
| 100 | + |
| 101 | +// getAllowedCiphersMapByID returns map of allowed cipher ID to cipher names. |
| 102 | +func (ciphers *allowedTLSCiphers) getAllowedCiphersMapByID() map[uint16]string { |
| 103 | + return ciphers.ciphersMapByID |
| 104 | +} |
| 105 | + |
| 106 | +// newAllowedTLSCiphers instantiates allowedTLSCiphers with all the ciphers |
| 107 | +// which golang implements and have been allowed for cockroach in |
| 108 | +// RecommendedCipherSuites, OldCipherSuites or as part of TLS 1.3 ciphers in |
| 109 | +// crypto/tls. |
| 110 | +func newAllowedTLSCiphers() (ciphers *allowedTLSCiphers) { |
| 111 | + ciphers = &allowedTLSCiphers{} |
| 112 | + ciphers.ciphersMapByName = map[string]uint16{} |
| 113 | + ciphers.ciphersMapByID = map[uint16]string{} |
| 114 | + cockroachEnabledCiphers := append(RecommendedCipherSuites(), OldCipherSuites()...) |
| 115 | + for _, cipher := range tls.CipherSuites() { |
| 116 | + if slices.Contains(cockroachEnabledCiphers, cipher.ID) || slices.Contains(cipher.SupportedVersions, tls.VersionTLS13) { |
| 117 | + ciphers.ciphersMapByName[cipher.Name] = cipher.ID |
| 118 | + ciphers.ciphersMapByID[cipher.ID] = cipher.Name |
| 119 | + } |
| 120 | + } |
| 121 | + for _, cipher := range tls.InsecureCipherSuites() { |
| 122 | + if slices.Contains(cockroachEnabledCiphers, cipher.ID) || slices.Contains(cipher.SupportedVersions, tls.VersionTLS13) { |
| 123 | + ciphers.ciphersMapByName[cipher.Name] = cipher.ID |
| 124 | + ciphers.ciphersMapByID[cipher.ID] = cipher.Name |
| 125 | + } |
| 126 | + } |
| 127 | + return |
| 128 | +} |
| 129 | + |
| 130 | +var allowedCiphers = newAllowedTLSCiphers() |
| 131 | + |
| 132 | +// getCipherID verifies if provided cipher is implemented by crypto/tls and |
| 133 | +// return the corresponding cipherID |
| 134 | +func getCipherNameFromID(cid uint16) (cipher string, ok bool) { |
| 135 | + allowedCiphersMap := allowedCiphers.getAllowedCiphersMapByID() |
| 136 | + cipher, ok = allowedCiphersMap[cid] |
| 137 | + return |
| 138 | +} |
| 139 | + |
| 140 | +// SetTLSCipherSuitesConfigured sets the global TLS cipher suites for all |
| 141 | +// incoming connections(sql/rpc/http, etc.) of a node. The entries in the list |
| 142 | +// should be a subset of RecommendedCipherSuites or OldCipherSuites in case of |
| 143 | +// TLS 1.2. For TLS 1.3, they should be a subset of ciphers list defined at |
| 144 | +// https://github.com/golang/go/blob/4aa1efed4853ea067d665a952eee77c52faac774/src/crypto/tls/cipher_suites.go#L676-L679 |
| 145 | +// for TLS 1.3. |
| 146 | +func SetTLSCipherSuitesConfigured(ciphers []string) error { |
| 147 | + allowedCiphersMap := allowedCiphers.getAllowedCiphersMapByName() |
| 148 | + for _, cipher := range ciphers { |
| 149 | + if _, ok := allowedCiphersMap[cipher]; !ok { |
| 150 | + return &cipherRestrictError{errors.Errorf("invalid cipher provided in tls cipher suites: %s", cipher)} |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + tlsRestrictConfig.configureTLSRestrict(ciphers) |
| 155 | + return nil |
| 156 | +} |
| 157 | + |
| 158 | +func (*tlsRestrictConfiguration) configureTLSRestrict(ciphers []string) { |
| 159 | + tlsRestrictConfig.Lock() |
| 160 | + defer tlsRestrictConfig.Unlock() |
| 161 | + tlsRestrictConfig.restrictFn = func(tlsConn *tls.Conn) (error net.Error) { return } |
| 162 | + tlsRestrictConfig.c = ciphers |
| 163 | + if len(ciphers) == 0 { |
| 164 | + return |
| 165 | + } |
| 166 | + |
| 167 | + tlsRestrictConfig.restrictFn = func(tlsConn *tls.Conn) (error net.Error) { |
| 168 | + if !tlsConn.ConnectionState().HandshakeComplete { |
| 169 | + // TODO(souravcrl): we need to provide a timebound context for handshake as it |
| 170 | + // ensures client failures are properly handled, issue: #144754 |
| 171 | + if err := tlsConn.Handshake(); err != nil { |
| 172 | + // we don't want to close the connection for handshake errors |
| 173 | + return nil //nolint:returnerrcheck |
| 174 | + } |
| 175 | + } |
| 176 | + selectedCipherID := tlsConn.ConnectionState().CipherSuite |
| 177 | + cName, ok := getCipherNameFromID(selectedCipherID) |
| 178 | + if !ok { |
| 179 | + return &cipherRestrictError{errors.Errorf("cipher id %v does match implemented tls ciphers", selectedCipherID)} |
| 180 | + } |
| 181 | + if !slices.Contains(tlsRestrictConfig.c, cName) { |
| 182 | + return &cipherRestrictError{errors.Newf("presented cipher %s not in allowed cipher suite list", cName)} |
| 183 | + } |
| 184 | + return |
| 185 | + } |
| 186 | +} |
| 187 | + |
| 188 | +// TLSCipherRestrict restricts the cipher suites used for tls connections to |
| 189 | +// ones specified by tls-cipher-suites cli flag. If the flag is not set, we do |
| 190 | +// not check for used ciphers in the connection. It returns an error if the used |
| 191 | +// cipher is not present in the configured ciphers for the node. |
| 192 | +var TLSCipherRestrict = func(conn net.Conn) (err net.Error) { |
| 193 | + var tlsRestrictFn func(*tls.Conn) (error net.Error) |
| 194 | + { |
| 195 | + tlsRestrictConfig.Lock() |
| 196 | + defer tlsRestrictConfig.Unlock() |
| 197 | + tlsRestrictFn = tlsRestrictConfig.restrictFn |
| 198 | + } |
| 199 | + // we always expect a TLS connection here, since this is executed on the |
| 200 | + // tls.Listener or post applying tls.Server on the incoming connection |
| 201 | + tlsConn, _ := conn.(*tls.Conn) |
| 202 | + return tlsRestrictFn(tlsConn) |
| 203 | +} |
| 204 | + |
| 205 | +// cipherRestrictError implements net.Error interface so that we can override |
| 206 | +// the error handling in net/http package as it only considers a net.Error type |
| 207 | +// for error rule matching. The cipher restrict error is overridden by net.Error |
| 208 | +// in TLSCipherRestrict fn. |
| 209 | +type cipherRestrictError struct { |
| 210 | + err error |
| 211 | +} |
| 212 | + |
| 213 | +// Error implements net.Error. |
| 214 | +func (e *cipherRestrictError) Error() string { return e.err.Error() } |
| 215 | + |
| 216 | +// Unwrap implements net.Error. |
| 217 | +func (e *cipherRestrictError) Unwrap() error { return e.err } |
| 218 | + |
| 219 | +// Timeout implements net.Error. |
| 220 | +func (e *cipherRestrictError) Timeout() bool { return false } |
| 221 | + |
| 222 | +// Temporary implements net.Error. We need to set this to true since |
| 223 | +// http/server.go:func Serve(l net.Listener) error |
| 224 | +// https://github.com/golang/go/blob/go1.23.7/src/net/http/server.go#L3329-L3349 |
| 225 | +// uses this value to resume serving on the connection without explicitly |
| 226 | +// registering an error. As mentioned in net/net.go Error interface temporary |
| 227 | +// errors are deprecated and may not be supported in the future: |
| 228 | +// https://github.com/golang/go/blob/go1.23.7/src/net/net.go#L419, hence we need |
| 229 | +// a check that cipherRestrictError always implements the net.Error interface |
| 230 | +// fully. |
| 231 | +// TODO(souravcrl): update the accept handler to just log an error and continue |
| 232 | +// processing new connection requests |
| 233 | +func (e *cipherRestrictError) Temporary() bool { return true } |
| 234 | + |
| 235 | +var _ error = (*cipherRestrictError)(nil) |
| 236 | + |
| 237 | +// We implement net.Error the same way that context.DeadlineExceeded does, so |
| 238 | +// that people looking for net.Error attributes will still find them. |
| 239 | +var _ net.Error = (*cipherRestrictError)(nil) |
0 commit comments