// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT // The Kerberos V5 GSS-API mechanism of RFC 4121 over the raw krb5 // mechanism of RFC 1964 framing: the context establishment through // AP-REQ and AP-REP (RFC 4120), and the per-message MIC and Wrap // tokens (RFC 4121 sections 4.2.4 to 4.2.6). package krb5 import ( "crypto/hmac" "crypto/rand" "encoding/binary" "errors" "strings" "sync" "time" ) // GSS token identifiers and flag bits of RFC 4121. const ( TokenMIC = 0x0404 TokenWrap = 0x0504 FlagSentByAcceptor = 0x01 FlagSealed = 0x02 FlagAcceptorSubkey = 0x04 ) // clockSkew is the window the acceptor tolerates between the client // clock and its own, five minutes, the customary maximum of RFC 4120 // section 10.1. const clockSkew = 5 * time.Minute // ErrToken marks a malformed or unusable GSS token. var ErrToken = errors.New("krb5: bad gss token") // ErrContext marks a mismatch against the established context. var ErrContext = errors.New("krb5: context mismatch") // ErrReplay marks an authenticator the acceptor has already seen. var ErrReplay = errors.New("krb5: replayed token") // ErrExpired marks a ticket whose endtime has passed. var ErrExpired = errors.New("krb5: ticket expired") // ErrSkew marks an authenticator outside the clock skew window. var ErrSkew = errors.New("krb5: clock skew too great") // A Context is one established security context: the session key, the // etype, the per-peer sequence counters and the acceptor role. The // counters and the token operations are guarded by the internal mutex, so // one context serves concurrent callers. type Context struct { Key []byte Etype uint32 Accepting bool SendSeq uint64 RecvSeq uint64 mu sync.Mutex } // appTag wraps a payload in the DER application tag of RFC 4120. func appTag(number byte, payload []byte) []byte { return derTLV(0x60|number, payload) } // principalName encodes a PrincipalName of name type one, the name of // a single component service. func principalName(name string) []byte { names := derTLV(tagSequence, derGeneralString(name)) return derTLV(tagSequence, append(derInt(1), names...)) } // encryptedData encodes an EncryptedData with a version number. func encryptedData(etype uint32, cipher []byte, kvno uint64) []byte { body := append(derInt(uint64(etype)), derInt(kvno)...) body = append(body, derTLV(tagOctet, cipher)...) return derTLV(tagSequence, body) } // checksum encodes a Checksum of type 0x8003, the GSS-API checksum. func gssChecksum(sum []byte) []byte { return derTLV(tagSequence, append(derInt(0x8003), derTLV(tagOctet, sum)...)) } // bitFlags encodes the BIT STRING form of the Kerberos flags. func bitFlags(flags uint32) []byte { var bits [6]byte binary.BigEndian.PutUint32(bits[2:], flags) bits[0] = 5 // construction bits[1] = 0 // unused bit count; the leading octets of a 32 bit field bits[2] = 0 bits[3] = byte(flags >> 24) bits[4] = byte(flags >> 16) bits[5] = byte(flags >> 8) bits[5] = byte(flags) return derTLV(tagBitString, append([]byte{0}, bits[2:6]...)) } // kerberosTime encodes a GeneralizedTime of whole seconds. func kerberosTime(t time.Time) []byte { return derTLV(0x18, []byte(t.UTC().Format("20060102150405Z"))) } // parseKerberosTime reads the GeneralizedTime of whole seconds. func parseKerberosTime(payload []byte) (time.Time, error) { t, err := time.Parse("20060102150405Z", string(payload)) if err != nil { return time.Time{}, ErrToken } return t, nil } // buildTicket produces the ticket part of an AP-REQ: the encrypted part // carries the session key under the service long term key, valid until // the given endtime. func buildTicket(realm, service string, session, serviceKey []byte, etype uint32, endtime time.Time) ([]byte, error) { flags := bitFlags(0) key := derTLV(tagSequence, append(derInt(uint64(etype)), derTLV(tagOctet, session)...)) inner := append(append(append(append([]byte{}, flags...), key...), derGeneralString(realm)...), principalName(service)...) inner = append(inner, derTLV(tagSequence, nil)...) // transited: empty inner = append(inner, kerberosTime(time.Unix(0, 0))...) // authtime inner = append(inner, kerberosTime(endtime)...) // endtime inner = append(inner, derGeneralString(realm)...) // srealm inner = append(inner, principalName(service)...) // sname cipher, cerr := Encrypt(etype, serviceKey, UsageTicket, inner) if cerr != nil { return nil, cerr } ticket := appTag(1, derTLV(tagSequence, append(append( derInt(5), derGeneralString(realm)...), append(principalName(service), encryptedData(etype, cipher, 1)...)...))) return ticket, nil } // buildAuthenticator produces the encrypted authenticator of an AP-REQ // for the given client time, with the 0x8003 checksum of RFC 4121 // section 4.1.1: the little endian length of Bnd, the sixteen Bnd octets // of an unused channel binding, and the little endian Flags, so a // conformant acceptor can read it. func buildAuthenticator(realm, client string, session []byte, etype uint32, seq uint64, ctime time.Time) ([]byte, error) { inner := append(derInt(5), derGeneralString(realm)...) inner = append(inner, principalName(client)...) sum := make([]byte, 0, 24) var word [4]byte binary.LittleEndian.PutUint32(word[:], 16) sum = append(sum, word[:]...) // length of Bnd sum = append(sum, make([]byte, 16)...) // Bnd: no channel binding binary.LittleEndian.PutUint32(word[:], 0) sum = append(sum, word[:]...) // Flags: none inner = append(inner, gssChecksum(sum)...) inner = append(inner, derInt(uint64(ctime.Nanosecond())/1000)...) inner = append(inner, kerberosTime(ctime)...) inner = append(inner, derInt(seq)...) cipher, err := Encrypt(etype, session, UsageAPReqAuth, inner) if err != nil { return nil, err } return encryptedData(etype, cipher, 0), nil } // ClientInit builds the context establishment token the initiator sends // in RPCSEC_GSS_INIT: the GSS framed AP-REQ. The service key is the // long term key the ticket is sealed with, as a KDC would have it. func ClientInit(etype uint32, serviceKey []byte, realm, service, client string) (*Context, []byte, error) { session := make([]byte, len(serviceKey)) if _, err := rand.Read(session); err != nil { return nil, nil, err } ctx := &Context{Key: session, Etype: etype} ticket, err := buildTicket(realm, service, session, serviceKey, etype, time.Now().Add(time.Hour)) if err != nil { return nil, nil, err } auth, err := buildAuthenticator(realm, client, session, etype, 0, time.Now()) if err != nil { return nil, nil, err } apreq := appTag(14, derTLV(tagSequence, append(append( derInt(5), derInt(14)...), append(bitFlags(0), append(ticket, auth...)...)...))) inner := append(append([]byte{}, derOID...), apreq...) return ctx, appTag(0, inner), nil } // checkGSSChecksum validates the 0x8003 checksum body of RFC 4121 // section 4.1.1: the little endian length of Bnd, the sixteen Bnd octets // and the little endian Flags. func checkGSSChecksum(payload []byte) error { r := newDERReader(payload) typeRaw, err := r.expect(tagInteger) if err != nil { return err } ctype, err := derUint(typeRaw) if err != nil || ctype != 0x8003 { return ErrToken } body, err := r.expect(tagOctet) if err != nil { return err } if len(body) != 24 || binary.LittleEndian.Uint32(body[0:4]) != 16 { return ErrToken } return nil } // parsePrincipal reads a PrincipalName and answers its components joined // with a slash. func parsePrincipal(payload []byte) (string, error) { r := newDERReader(payload) if _, err := r.expect(tagInteger); err != nil { // name type return "", err } strs, err := r.expect(tagSequence) if err != nil { return "", err } sr := newDERReader(strs) var parts []string for { tag, raw, err := sr.next() if err != nil { break } if tag != 0x1b { return "", ErrDER } parts = append(parts, string(raw)) } return strings.Join(parts, "/"), nil } // parseAuthenticator reads the fields of a decrypted authenticator the // acceptor checks: the client principal, the timestamp, the microseconds // and the sequence number when one is present. func parseAuthenticator(plain []byte) (client string, ctime time.Time, cusec uint64, seq int64, err error) { r := newDERReader(plain) vnoRaw, err := r.expect(tagInteger) if err != nil { return } vno, err := derUint(vnoRaw) if err != nil || vno != 5 { err = ErrToken return } if _, err = r.expect(0x1b); err != nil { // crealm return } cnameRaw, err := r.expect(tagSequence) if err != nil { return } if client, err = parsePrincipal(cnameRaw); err != nil { return } cksumRaw, err := r.expect(tagSequence) // the GSS checksum, required if err != nil { return } if err = checkGSSChecksum(cksumRaw); err != nil { return } cusecRaw, err := r.expect(tagInteger) if err != nil { return } if cusec, err = derUint(cusecRaw); err != nil { return } ctimeRaw, err := r.expect(0x18) if err != nil { return } if ctime, err = parseKerberosTime(ctimeRaw); err != nil { return } // Past the timestamp this profile writes at most the sequence number, // an integer; anything else ends the fields it reads. if tag, payload, terr := r.next(); terr == nil && tag == tagInteger { v, verr := derUint(payload) if verr != nil { err = ErrToken return } seq = int64(v) } return } // replayKey identifies one authenticator of one client: the principal // plus the timestamp, microseconds and sequence number it carried. type replayKey struct { client string ctime int64 cusec int64 seq int64 } // replays is the acceptor wide replay cache. Entries live for the clock // skew window, beyond which a token is refused for its age anyway. var replays = struct { mu sync.Mutex seen map[replayKey]time.Time }{seen: make(map[replayKey]time.Time)} // seenReplay records the authenticator and reports whether one with the // same key was already accepted. func seenReplay(k replayKey) bool { now := time.Now() replays.mu.Lock() defer replays.mu.Unlock() for key, until := range replays.seen { if now.After(until) { delete(replays.seen, key) } } if _, ok := replays.seen[k]; ok { return true } replays.seen[k] = now.Add(clockSkew) return false } // AcceptInit verifies the initiator token and answers the AP-REP and // the established context of the acceptor side. The ticket must not have // expired, the authenticator must sit inside the clock skew window and // must never have been accepted before. func AcceptInit(token []byte, serviceKey []byte) (*Context, []byte, error) { r := newDERReader(token) outer, err := r.expect(0x60 | 0) if err != nil { return nil, nil, err } ir := newDERReader(outer) if _, err = ir.expect(tagOID); err != nil { return nil, nil, err } apreq, err := ir.expect(0x60 | 14) if err != nil { return nil, nil, err } sr := newDERReader(apreq) seqPayload, err := sr.expect(tagSequence) if err != nil { return nil, nil, err } br := newDERReader(seqPayload) if _, err = br.expect(tagInteger); err != nil { // pvno return nil, nil, err } if _, err = br.expect(tagInteger); err != nil { // msg-type return nil, nil, err } if _, err = br.expect(tagBitString); err != nil { // ap-options return nil, nil, err } ticketRaw, err := br.expect(0x60 | 1) if err != nil { return nil, nil, err } authRaw, err := br.expect(tagSequence) if err != nil { return nil, nil, err } // The ticket: decrypt the encrypted part with the service key. tr := newDERReader(ticketRaw) ticketSeq, terr := tr.expect(tagSequence) if terr != nil { return nil, nil, terr } t := newDERReader(ticketSeq) if _, err = t.expect(tagInteger); err != nil { return nil, nil, err } if _, err = t.expect(0x1b); err != nil { // realm return nil, nil, err } if _, err = t.expect(tagSequence); err != nil { // sname return nil, nil, err } encTag, encPayload, err := t.next() if err != nil || encTag != tagSequence { return nil, nil, ErrDER } er := newDERReader(encPayload) _, epayload, eerr := er.next() if eerr != nil { return nil, nil, eerr } etype, eerr2 := derUint(epayload) if eerr2 != nil { return nil, nil, eerr2 } if _, err = er.expect(tagInteger); err != nil { // kvno return nil, nil, err } cipherPayload, err := er.expect(tagOctet) if err != nil { return nil, nil, err } inner, err := Decrypt(uint32(etype), serviceKey, UsageTicket, cipherPayload) if err != nil { return nil, nil, err } // The decrypted ticket part: flags, key, crealm, cname, transited, // authtime, endtime, srealm, sname. The key and the endtime matter // here; this profile writes no starttime, so the second time is the // endtime. ir2 := newDERReader(inner) if _, err = ir2.expect(tagBitString); err != nil { return nil, nil, err } _, keyPayload, err := ir2.next() if err != nil { return nil, nil, err } kr := newDERReader(keyPayload) if _, err = kr.expect(tagInteger); err != nil { return nil, nil, err } keyValue, err := kr.expect(tagOctet) if err != nil { return nil, nil, err } if _, err = ir2.expect(0x1b); err != nil { // crealm return nil, nil, err } if _, err = ir2.expect(tagSequence); err != nil { // cname return nil, nil, err } if _, err = ir2.expect(tagSequence); err != nil { // transited return nil, nil, err } if _, err = ir2.expect(0x18); err != nil { // authtime return nil, nil, err } endRaw, err := ir2.expect(0x18) // endtime if err != nil { return nil, nil, err } endtime, err := parseKerberosTime(endRaw) if err != nil { return nil, nil, err } if time.Now().After(endtime) { return nil, nil, ErrExpired } // The authenticator: decrypt with the session key. ar := newDERReader(authRaw) atag, apayload, aerr := ar.next() if aerr != nil { return nil, nil, aerr } if atag != tagInteger { return nil, nil, ErrDER } authEtype, aerr2 := derUint(apayload) if aerr2 != nil { return nil, nil, aerr2 } if _, err = ar.expect(tagInteger); err != nil { // kvno return nil, nil, err } authCipher, err := ar.expect(tagOctet) if err != nil { return nil, nil, err } plain, err := Decrypt(uint32(authEtype), keyValue, UsageAPReqAuth, authCipher) if err != nil { return nil, nil, err } client, ctime, cusec, seq, perr := parseAuthenticator(plain) if perr != nil { return nil, nil, perr } authTime := ctime.Add(time.Duration(cusec) * time.Microsecond) if d := time.Since(authTime); d > clockSkew || d < -clockSkew { return nil, nil, ErrSkew } if seenReplay(replayKey{client: client, ctime: ctime.Unix(), cusec: int64(cusec), seq: seq}) { return nil, nil, ErrReplay } ctx := &Context{Key: keyValue, Etype: uint32(authEtype), Accepting: true, RecvSeq: 0} // The AP-REP: pvno, msg-type, the encrypted part under usage twelve // with the echo of the timestamp and the sequence number zero. enc, err := Encrypt(uint32(authEtype), keyValue, UsageAPRepAuth, append(append(derInt(0), derInt(0)...), derInt(0)...)) if err != nil { return nil, nil, err } aprep := appTag(15, derTLV(tagSequence, append(append( derInt(5), derInt(15)...), encryptedData(uint32(authEtype), enc, 0)...))) return ctx, aprep, nil } // ClientAcceptRep verifies the AP-REP the acceptor answers the // establishment with and completes the client half of the context: the // encrypted part must decrypt under the session key with the AP-REP // usage and carry the message type and version of RFC 4120. func (c *Context) ClientAcceptRep(token []byte) error { if c.Key == nil { return ErrContext } r := newDERReader(token) aprep, err := r.expect(0x60 | 15) if err != nil { return err } pr := newDERReader(aprep) seqPayload, err := pr.expect(tagSequence) if err != nil { return err } sr := newDERReader(seqPayload) pvnoRaw, err := sr.expect(tagInteger) if err != nil { return err } pvno, err := derUint(pvnoRaw) if err != nil || pvno != 5 { return ErrToken } msgTypeRaw, err := sr.expect(tagInteger) if err != nil { return err } msgType, err := derUint(msgTypeRaw) if err != nil || msgType != 15 { return ErrToken } encTag, encPayload, err := sr.next() if err != nil || encTag != tagSequence { return ErrDER } er := newDERReader(encPayload) _, epayload, eerr := er.next() if eerr != nil { return eerr } etype, eerr2 := derUint(epayload) if eerr2 != nil { return eerr2 } if _, err = er.expect(tagInteger); err != nil { // kvno return err } cipher, err := er.expect(tagOctet) if err != nil { return err } if uint32(etype) != c.Etype { return ErrContext } plain, err := Decrypt(uint32(etype), c.Key, UsageAPRepAuth, cipher) if err != nil { return err } // The decrypted part answers with the client time and usec echo; // three integers in this profile. tr := newDERReader(plain) for range 3 { if _, err = tr.expect(tagInteger); err != nil { return ErrToken } } return nil } // acceptMIC computes the MIC checksum body of RFC 4121 section 4.2.6.1: // the checksum operation over the signed data and the token header. func (c *Context) micSum(header, data []byte) ([]byte, error) { sum, err := Checksum(c.Etype, c.Key, micUsage(c.Accepting), append(append([]byte{}, data...), header...)) if err != nil { return nil, err } return sum, nil } // micUsage answers the key usage of the given side: the initiator // signs with seven, the acceptor with eight; a verifier always uses the // usage of the peer who produced the token. func micUsage(accepting bool) uint32 { if accepting { return UsageAcceptorSign } return UsageInitiatorSign } // sealUsage answers the seal key usage of a side. func sealUsage(accepting bool) uint32 { if accepting { return UsageAcceptorSeal } return UsageInitiatorSeal } // GetMIC produces the per-message MIC token: the fixed header with the // sequence number and the checksum over data and header. func (c *Context) GetMIC(data []byte) ([]byte, error) { c.mu.Lock() defer c.mu.Unlock() header := make([]byte, 16) binary.BigEndian.PutUint16(header[0:], TokenMIC) flags := byte(0) if c.Accepting { flags = FlagSentByAcceptor } header[2] = flags for i := 3; i < 8; i++ { header[i] = 0xff } binary.BigEndian.PutUint64(header[8:], c.SendSeq) c.SendSeq++ sum, err := c.micSum(header, data) if err != nil { return nil, err } return append(header, sum...), nil } // VerifyMIC checks a peer MIC token against the data. func (c *Context) VerifyMIC(data, token []byte) error { c.mu.Lock() defer c.mu.Unlock() if len(token) < 28 || binary.BigEndian.Uint16(token[0:]) != TokenMIC { return ErrToken } peerFlags := byte(0) if !c.Accepting { peerFlags = FlagSentByAcceptor } if token[2] != peerFlags { return ErrContext } seq := binary.BigEndian.Uint64(token[8:]) if seq != c.RecvSeq { return ErrContext } c.RecvSeq++ // The token was signed by the peer, whose signing usage differs. sum, err := Checksum(c.Etype, c.Key, micUsage(!c.Accepting), append(append([]byte{}, data...), token[:16]...)) if err != nil { return err } if !hmac.Equal(sum, token[16:28]) { return ErrIntegrity } return nil } // Wrap produces the confidential Wrap token: the header, then the // plaintext with the header appended encrypted by the profile. No // filler octets are needed with AES, whose cipher keeps the length. func (c *Context) Wrap(data []byte) ([]byte, error) { c.mu.Lock() defer c.mu.Unlock() header := make([]byte, 16) binary.BigEndian.PutUint16(header[0:], TokenWrap) flags := byte(FlagSealed) if c.Accepting { flags |= FlagSentByAcceptor } header[2] = flags header[3] = 0xff binary.BigEndian.PutUint64(header[8:], c.SendSeq) c.SendSeq++ sealed, err := Encrypt(c.Etype, c.Key, sealUsage(c.Accepting), append(append([]byte{}, data...), header...)) if err != nil { return nil, err } return append(header, sealed...), nil } // Unwrap reverses Wrap and returns the plaintext. func (c *Context) Unwrap(token []byte) ([]byte, error) { c.mu.Lock() defer c.mu.Unlock() if len(token) < 16+16+12 || binary.BigEndian.Uint16(token[0:]) != TokenWrap { return nil, ErrToken } peerFlags := byte(FlagSealed) if !c.Accepting { peerFlags |= FlagSentByAcceptor } if token[2]&0x03 != peerFlags&0x03 { return nil, ErrContext } // The Wrap token carries its own sequence number, which advances the // receive counter like a MIC does. if binary.BigEndian.Uint64(token[8:]) != c.RecvSeq { return nil, ErrContext } c.RecvSeq++ // The token was sealed by the peer, whose seal usage differs. data, err := Decrypt(c.Etype, c.Key, sealUsage(!c.Accepting), token[16:]) if err != nil { return nil, err } if string(data[len(data)-16:]) != string(token[:16]) { return nil, ErrContext } return data[:len(data)-16], nil }