github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/sink/kafka/v2/gssapi.go (about) 1 // Copyright 2023 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package v2 15 16 import ( 17 "context" 18 "encoding/asn1" 19 "encoding/binary" 20 21 "github.com/jcmturner/gokrb5/v8/client" 22 "github.com/jcmturner/gokrb5/v8/credentials" 23 "github.com/jcmturner/gokrb5/v8/crypto" 24 "github.com/jcmturner/gokrb5/v8/gssapi" 25 "github.com/jcmturner/gokrb5/v8/iana/chksumtype" 26 "github.com/jcmturner/gokrb5/v8/iana/keyusage" 27 "github.com/jcmturner/gokrb5/v8/messages" 28 "github.com/jcmturner/gokrb5/v8/types" 29 "github.com/pingcap/errors" 30 "github.com/segmentio/kafka-go/sasl" 31 ) 32 33 const ( 34 // TokIDKrbApReq https://tools.ietf.org/html/rfc4121#section-4.1 35 TokIDKrbApReq = "\x01\x00" 36 ) 37 38 // Gokrb5v8Client is the client for gokrbv8 39 type Gokrb5v8Client interface { 40 // GetServiceTicket get a ticker form server 41 GetServiceTicket(spn string) (messages.Ticket, types.EncryptionKey, error) 42 // Destroy stops the auto-renewal of all sessions and removes 43 // the sessions and cache entries from the client. 44 Destroy() 45 // Credentials returns the client credentials 46 Credentials() *credentials.Credentials 47 } 48 49 type gokrb5v8ClientImpl struct { 50 client *client.Client 51 } 52 53 func (c *gokrb5v8ClientImpl) GetServiceTicket(spn string) ( 54 messages.Ticket, types.EncryptionKey, error, 55 ) { 56 return c.client.GetServiceTicket(spn) 57 } 58 59 func (c *gokrb5v8ClientImpl) Credentials() *credentials.Credentials { 60 return c.client.Credentials 61 } 62 63 func (c *gokrb5v8ClientImpl) Destroy() { 64 c.client.Destroy() 65 } 66 67 type mechanism struct { 68 client Gokrb5v8Client 69 serviceName string 70 host string 71 } 72 73 func (m mechanism) Name() string { 74 return "GSSAPI" 75 } 76 77 // Gokrb5v8 uses gokrb5/v8 to implement the GSSAPI mechanism. 78 // 79 // client is a github.com/gokrb5/v8/client *Client instance. 80 // kafkaServiceName is the name of the Kafka service in your Kerberos. 81 func Gokrb5v8(client Gokrb5v8Client, kafkaServiceName string) sasl.Mechanism { 82 return mechanism{client, kafkaServiceName, ""} 83 } 84 85 // StartWithoutHostError is the error type for when Start is called on 86 // the GSSAPI mechanism without the host having been set by WithHost. 87 // 88 // Unless you are calling the GSSAPI SASL mechanim's Start method 89 // yourself for some reason, this error will never be returned. 90 type StartWithoutHostError struct{} 91 92 func (e StartWithoutHostError) Error() string { 93 return "GSSAPI SASL handshake needs a host" 94 } 95 96 func (m mechanism) Start(ctx context.Context) (sasl.StateMachine, []byte, error) { 97 metaData := sasl.MetadataFromContext(ctx) 98 m.host = metaData.Host 99 if m.host == "" { 100 return nil, nil, StartWithoutHostError{} 101 } 102 103 servicePrincipalName := m.serviceName + "/" + m.host 104 ticket, key, err := m.client.GetServiceTicket( 105 servicePrincipalName, 106 ) 107 if err != nil { 108 return nil, nil, errors.Trace(err) 109 } 110 111 authenticator, err := types.NewAuthenticator( 112 m.client.Credentials().Realm(), 113 m.client.Credentials().CName(), 114 ) 115 if err != nil { 116 return nil, nil, errors.Trace(err) 117 } 118 119 encryptionType, err := crypto.GetEtype(key.KeyType) 120 if err != nil { 121 return nil, nil, errors.Trace(err) 122 } 123 124 keySize := encryptionType.GetKeyByteSize() 125 err = authenticator.GenerateSeqNumberAndSubKey(key.KeyType, keySize) 126 if err != nil { 127 return nil, nil, errors.Trace(err) 128 } 129 130 authenticator.Cksum = types.Checksum{ 131 CksumType: chksumtype.GSSAPI, 132 Checksum: authenticatorPseudoChecksum(), 133 } 134 apReq, err := messages.NewAPReq(ticket, key, authenticator) 135 if err != nil { 136 return nil, nil, errors.Trace(err) 137 } 138 139 bytes, err := apReq.Marshal() 140 if err != nil { 141 return nil, nil, errors.Trace(err) 142 } 143 gssapiToken, err := getGssAPIToken(bytes) 144 if err != nil { 145 return nil, nil, errors.Trace(err) 146 } 147 return &gokrb5v8Session{authenticator.SubKey, false}, gssapiToken, nil 148 } 149 150 func getGssAPIToken(bytes []byte) ([]byte, error) { 151 bytesWithPrefix := make([]byte, 0, len(TokIDKrbApReq)+len(bytes)) 152 bytesWithPrefix = append(bytesWithPrefix, TokIDKrbApReq...) 153 bytesWithPrefix = append(bytesWithPrefix, bytes...) 154 155 return prependGSSAPITokenTag(bytesWithPrefix) 156 } 157 158 func authenticatorPseudoChecksum() []byte { 159 // Not actually a checksum, but it goes in the checksum field. 160 // https://tools.ietf.org/html/rfc4121#section-4.1.1 161 checksum := make([]byte, 24) 162 163 flags := gssapi.ContextFlagInteg 164 // Reasons for each flag being on or off: 165 // Delegation: Off. We are not using delegated credentials. 166 // Mutual: Off. Mutual authentication is already provided 167 // as a result of how Kerberos works. 168 // Replay: Off. We don’t need replay protection because each 169 // packet is secured by a per-session key and is unique 170 // within its session. 171 // Sequence: Off. Out-of-order messages cannot happen in our 172 // case, and if it somehow happened anyway it would 173 // necessarily trigger other appropriate errors. 174 // Confidentiality: Off. Our authentication itself does not 175 // seem to be requesting or using any “security layers” 176 // in the GSSAPI sense, and this is just one of the 177 // security layer features. Also, if we were requesting 178 // a GSSAPI security layer, we would be required to 179 // set the mutual flag to on. 180 // https://tools.ietf.org/html/rfc4752#section-3.1 181 // Integrity: On. Must be on when calling the standard API, 182 // so it probably must be set in the raw packet itself. 183 // https://tools.ietf.org/html/rfc4752#section-3.1 184 // https://tools.ietf.org/html/rfc4752#section-7 185 // Anonymous: Off. We are not using an anonymous ticket. 186 // https://tools.ietf.org/html/rfc6112#section-3 187 188 binary.LittleEndian.PutUint32(checksum[0:4], 16) 189 // checksum[4:20] is unused/blank channel binding settings. 190 binary.LittleEndian.PutUint32(checksum[20:24], uint32(flags)) 191 return checksum 192 } 193 194 type gssapiToken struct { 195 OID asn1.ObjectIdentifier 196 Object asn1.RawValue 197 } 198 199 func prependGSSAPITokenTag(payload []byte) ([]byte, error) { 200 // The GSSAPI "token" is almost an ASN.1 encoded object, except 201 // that the "token object" is raw bytes, not necessarily ASN.1. 202 // https://tools.ietf.org/html/rfc2743#page-81 (section 3.1) 203 token := gssapiToken{ 204 OID: asn1.ObjectIdentifier(gssapi.OIDKRB5.OID()), 205 Object: asn1.RawValue{FullBytes: payload}, 206 } 207 return asn1.MarshalWithParams(token, "application") 208 } 209 210 type gokrb5v8Session struct { 211 key types.EncryptionKey 212 done bool 213 } 214 215 func (s *gokrb5v8Session) Next(ctx context.Context, challenge []byte) (bool, []byte, error) { 216 if s.done { 217 return true, nil, nil 218 } 219 const tokenIsFromGSSAcceptor = true 220 challengeToken := gssapi.WrapToken{} 221 err := challengeToken.Unmarshal(challenge, tokenIsFromGSSAcceptor) 222 if err != nil { 223 return false, nil, errors.Trace(err) 224 } 225 226 valid, err := challengeToken.Verify( 227 s.key, 228 keyusage.GSSAPI_ACCEPTOR_SEAL, 229 ) 230 if !valid { 231 return false, nil, errors.Trace(err) 232 } 233 234 responseToken, err := gssapi.NewInitiatorWrapToken( 235 challengeToken.Payload, 236 s.key, 237 ) 238 if err != nil { 239 return false, nil, errors.Trace(err) 240 } 241 242 response, err := responseToken.Marshal() 243 if err != nil { 244 return false, nil, errors.Trace(err) 245 } 246 247 // We are done, but we can't return `true` yet because 248 // the SASL loop calling this needs the first return to be 249 // `false` any time there are response bytes to send. 250 s.done = true 251 return false, response, nil 252 }