krb5

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: BSD-3-Clause Imports: 18 Imported by: 2

README

krb5

Go Reference License CI

Accept Kerberos tickets in Go, with no cgo. Pure Go, CGO_ENABLED=0, over gokrb5 for the ticket decryption.

a, err := krb5.Load("/etc/krb5.keytab")
ctx, apRep, err := a.Accept(token)   // apRep goes back to the client
fmt.Println(ctx.Principal())         // alice@EXAMPLE.ORG — an authenticated answer

This is the acceptor half of Kerberos and only that half. It holds a keytab, verifies the AP-REQ a client presents, answers with the AP-REP that proves the service is genuine, and then signs and verifies messages under the session key. It never talks to a KDC and never issues a ticket, so whose tickets it accepts is somebody else's decision: MIT, Heimdal, Active Directory and FreeIPA all produce tickets it verifies.

krb5.Load(path) reads a keytab from disk and krb5.New(kt) takes one already loaded. Principal("nfs/host.example.org") restricts an acceptor to one service of a keytab that holds several, and ClockSkew(seconds) changes the five minutes gokrb5 and MIT allow. A Context says who: Principal(), or User() and Realm() apart, since mapping a principal to a local account is the caller's policy (alice@EXAMPLE.ORG and alice@PARTNER.ORG both answer to "alice"), and Expires(), past which a server holding long-lived connections should stop honouring it. Messages are signed with MIC/VerifyMIC and Wrap/Unwrap, and sealed with Seal/Unseal.

Why it exists

gokrb5 is a client library. It verifies an AP-REQ and then cannot answer one:

case TOK_ID_KRB_AP_REP:
    return []byte{}, errors.New("marshal of AP_REP GSSAPI MechToken not supported by gokrb5")

Every GSS-API client asks for mutual authentication — MIT's own gss-client sets GSS_C_MUTUAL_FLAG and offers no way to turn it off — so a service that cannot produce an AP-REP completes no context at all. Building it is most of what this package adds; gokrb5 does the ticket decryption underneath.

What it does not do

Nothing a KDC does. No AS-REQ, no TGS-REQ, no principal database.

Unwrap still refuses a SEALED token, with a distinct error: a caller reading an integrity-only token must not be handed ciphertext that reads like a message. Sealed tokens go through Seal and Unseal, which is sec=krb5p.

Acceptor subkeys. It asserts none, so the context key is the subkey the initiator put in its authenticator — RFC 4121 §4.3 — which keeps the key usage numbers unambiguous on both sides.

The judge

The tests are not run against a stand-in. A real MIT KDC issues a ticket and MIT's own gss-client presents it, against this package standing in for gss-server. A fake built from one's own reading of RFC 4121 can only confirm that reading.

test/kdc.sh /tmp/krbtest
set -a; . /tmp/krbtest/env; set +a
go test ./...

test/kdc.sh builds a throwaway realm on loopback, unprivileged, and touches nothing the machine already has. Setting KRB5_REQUIRE_JUDGE=1 turns "the fixture is missing" from a skip into a failure — every lane that has MIT to judge with sets it (the Linux test lane, the race detector and coverage), because a differential test that can quietly not run is not a control. The macOS, Windows and cross-architecture lanes have no realm, and there the judge skips.

The sample protocol's framing was measured by putting a recording proxy between the real client and the real server, not read out of its source.

Licence

BSD-3-Clause.

Documentation

Overview

Package krb5 accepts Kerberos tickets.

It is the ACCEPTOR half of Kerberos, and only that half: it holds a keytab, verifies the AP-REQ a client presents, and then signs and verifies messages under the session key that exchange established. It never talks to a KDC and never issues a ticket. Whose tickets it accepts is therefore somebody else's decision — MIT, Heimdal, Active Directory and FreeIPA all produce tickets this package verifies, because verification only needs the service key, which the keytab already holds.

Why this exists

github.com/jcmturner/gokrb5 is a CLIENT library. It verifies an AP-REQ (service.VerifyAPREQ) but it cannot build the AP-REP that answers one, and says so itself:

case TOK_ID_KRB_AP_REP:
	return []byte{}, errors.New("marshal of AP_REP GSSAPI MechToken not supported by gokrb5")

Every GSS-API client asks for mutual authentication — MIT's own gss-client sets GSS_C_MUTUAL_FLAG with no option to turn it off — so a service that cannot produce an AP-REP cannot complete a single context. Building it is most of what this package adds.

What it does not do

Anything a KDC does: no AS-REQ, no TGS-REQ, no principal database.

Context.Unwrap still refuses a SEALED token, with a distinct error. A caller reading an integrity-only token must not be handed ciphertext that reads like a message; confidentiality goes through Context.Seal and Context.Unseal, which together are sec=krb5p.

It also asserts no acceptor subkey. The context key is the subkey the initiator put in its authenticator, or the ticket's session key when it sent none, which is what RFC 4121 §4.3 allows and what keeps the key usage numbers on both sides unambiguous.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotAToken reports bytes that are not a GSS-API krb5 token at all.
	ErrNotAToken = errors.New("krb5: not a GSS-API krb5 token")
	// ErrNotAPReq reports a well-formed token that is not an AP-REQ — a
	// client sending its own AP-REP, or a KRB-ERROR.
	ErrNotAPReq = errors.New("krb5: token is not an AP-REQ")
	// ErrRejected reports a ticket this service will not accept: wrong
	// service, wrong key, expired, replayed, or a clock too far out.
	ErrRejected = errors.New("krb5: ticket rejected")
)

Errors an acceptor returns. They are deliberately coarse: a client learns that its ticket was refused, never which of the many checks refused it.

View Source
var ErrBadMIC = errors.New("krb5: bad message signature")

ErrBadMIC reports a message whose signature does not match. It is the only answer a verifier gives: which of the length, the header, the sequence or the checksum disagreed is not a client's business.

View Source
var ErrNoKeytab = errors.New("krb5: keytab")

ErrNoKeytab reports a keytab that could not be read.

View Source
var (
	// ErrNotSealed reports a wrap token without confidentiality handed to a
	// function that requires it.
	ErrNotSealed = errors.New("krb5: wrap token is not sealed")
)
View Source
var ErrSealed = errors.New("krb5: wrap token is sealed (sec=krb5p): read it with Unseal, not Unwrap")

ErrSealed reports that Context.Unwrap was given a wrap token carrying confidentiality: its payload is ciphertext, which Context.Unseal reads. It is a DISTINCT error from ErrBadMIC on purpose: a sealed token is well-formed and correctly signed, and returning it as a bad signature would send an operator looking at keys and clocks for the wrong call.

Functions

This section is empty.

Types

type Acceptor

type Acceptor struct {
	// contains filtered or unexported fields
}

Acceptor verifies tickets presented for one service.

It is safe for concurrent use: the keytab is read-only after loading, and each accepted ticket produces its own Context.

func Load

func Load(path string, opts ...Option) (*Acceptor, error)

Load reads a keytab from disk and returns an acceptor for it.

A keytab is a secret at rest. Its path may come from a configuration file or from KRB5_KTNAME; the KEY inside it must never be logged, printed, or carried in an environment variable.

func New

func New(kt *keytab.Keytab, opts ...Option) *Acceptor

New returns an acceptor over an already-loaded keytab.

func (*Acceptor) Accept

func (a *Acceptor) Accept(token []byte) (*Context, []byte, error)

Accept verifies one AP-REQ and returns the context it establishes.

The second result is the token to send back to the client. It is non-empty whenever the client asked for mutual authentication, which in practice is always; sending it is not optional, because the client will not consider the context open until it has verified the AP-REP.

An error means no context: the returned token is then empty, and the caller should refuse the operation rather than fall back to something weaker.

type Context

type Context struct {
	// contains filtered or unexported fields
}

Context is one authenticated conversation with one client.

It is created by Acceptor.Accept and holds the key that exchange established. Everything it signs or verifies is bound to that key, so a context is exactly as long-lived as the ticket behind it — see Context.Expires.

A Context is NOT safe for concurrent use.

func (*Context) Expires

func (c *Context) Expires() time.Time

Expires is when the ticket behind this context stops being valid. A server holding long-lived connections should stop honouring a context past it.

func (*Context) MIC

func (c *Context) MIC(msg []byte) ([]byte, error)

MIC signs msg as the acceptor and returns the token to send back.

The key usage differs from the one Context.VerifyMIC checks — 23 here, 25 there. That asymmetry is what stops a token this server produced from being replayed back at it as though the client had signed it.

func (*Context) PeerSequence

func (c *Context) PeerSequence() uint64

PeerSequence is the sequence number the client started at, from its authenticator. Protocols that carry their own sequence — RPCSEC_GSS does — check messages against it.

func (*Context) Principal

func (c *Context) Principal() string

Principal is who the client proved itself to be, in full Kerberos spelling ("alice@EXAMPLE.ORG").

This is an authenticated answer, which is the whole point of the exchange: unlike an AUTH_UNIX uid or an SMB username, the client could not have produced it without holding the key.

func (*Context) Realm

func (c *Context) Realm() string

func (*Context) Seal added in v0.2.0

func (c *Context) Seal(msg []byte) ([]byte, error)

Seal produces a wrap token carrying confidentiality, as the acceptor.

EC and RRC are zero, which is what MIT emits and what every implementation accepts; a non-zero rotation buys nothing here and would only be code no test reaches.

func (*Context) Unseal added in v0.2.0

func (c *Context) Unseal(token []byte) ([]byte, error)

Unseal opens a wrap token that carries confidentiality.

It refuses an unsealed one with ErrNotSealed rather than returning its payload: a caller that asked to unseal is a caller whose protocol promised confidentiality, and handing back something that crossed the wire in the clear would keep that promise in name only.

func (*Context) Unwrap

func (c *Context) Unwrap(token []byte) ([]byte, error)

Unwrap reads a wrap token the client sent and returns its payload.

func (*Context) User

func (c *Context) User() string

User is the principal without its realm, and Realm is the realm alone.

Mapping a principal onto a local account is the CALLER's decision and a policy question, not a string operation: alice@EXAMPLE.ORG and alice@PARTNER.ORG are different people who both answer to "alice".

func (*Context) VerifyMIC

func (c *Context) VerifyMIC(msg, token []byte) error

VerifyMIC checks that msg was signed by the client on this context.

The signature is over msg and the token header together, so a token lifted from one message will not verify against another.

func (*Context) Wrap

func (c *Context) Wrap(msg []byte) ([]byte, error)

Wrap packages msg as the acceptor, signed but not encrypted.

type Option

type Option func(*Acceptor)

Option configures an Acceptor.

func ClockSkew

func ClockSkew(seconds int) Option

ClockSkew is how far the client's clock may be from this one, in seconds. Zero means gokrb5's default of five minutes, which is also MIT's.

func Principal

func Principal(name string) Option

Principal restricts the acceptor to one service principal, named without its realm ("nfs/host.example.org").

Without it, any principal in the keytab may be addressed. A keytab that holds exactly one service does not need this; one that holds several, and wants a port to answer for only one of them, does.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL