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  }