krb5 — accepting a Kerberos ticket
Is this Kerberos ticket good for this service? 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, verifies, seals and opens messages under the key that exchange established.
go get github.com/go-authn/krb5@v0.3.1It never talks to a KDC and never issues a ticket, so whose tickets it accepts is somebody else’s decision. The issuing half is kdc, and this module does not depend on it — see why they are separate. Ticket decryption is gokrb5’s.
Why it exists
gokrb5 is a client library. It verifies an AP-REQ and cannot build the AP-REP
that answers one. Every GSS-API client asks for mutual authentication — MIT’s
own gss-client sets GSS_C_MUTUAL_FLAG with 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 module adds.
Example
package main
import (
"errors"
"fmt"
"log"
"github.com/go-authn/krb5"
"github.com/jcmturner/gokrb5/v8/keytab"
)
// establish is one GSS-API exchange: the client's AP-REQ in, the AP-REP out.
func establish(a *krb5.Acceptor, token []byte) (*krb5.Context, []byte, error) {
ctx, apRep, err := a.Accept(token)
if err != nil {
return nil, nil, err // no context: refuse, never fall back to something weaker
}
// The client will not consider the context open until it has verified
// apRep, so sending it back is not optional.
log.Printf("%s (user %q, realm %q) until %s", ctx.Principal(), ctx.User(), ctx.Realm(), ctx.Expires())
return ctx, apRep, nil
}
func main() {
// In a service: krb5.Load("/etc/krb5.keytab", krb5.Principal("nfs/files.example.org")).
// Here, an empty keytab, so the refusals can be seen.
a := krb5.New(keytab.New(), krb5.Principal("nfs/files.example.org"))
_, _, err := establish(a, []byte("not a token"))
fmt.Println(errors.Is(err, krb5.ErrNotAToken), err)
_, err = krb5.Load("/nonexistent/krb5.keytab")
fmt.Println(errors.Is(err, krb5.ErrNoKeytab), err)
}true krb5: not a GSS-API krb5 token: asn1: structure error: explicitly tagged member didn't match
true krb5: keytab "/nonexistent/krb5.keytab": open /nonexistent/krb5.keytab: no such file or directoryIt needs github.com/jcmturner/gokrb5/v8 for keytab.New, which krb5.New
takes; krb5.Load reads a keytab from a path instead.
After Accept, a *Context says who: Principal() (alice@EXAMPLE.ORG), or
User() and Realm() apart. Mapping a principal to a local account is the
caller’s policy: alice@EXAMPLE.ORG and alice@PARTNER.ORG both answer to
“alice”. Expires() is when the ticket behind it ends, past which a server
holding a long-lived connection should stop honouring it. MIC/VerifyMIC
and Wrap/Unwrap sign; Seal/Unseal add confidentiality (sec=krb5p).
A Context is not safe for concurrent use; an Acceptor is.
What it refuses, and why
A ticket this service will not accept — wrong service, wrong key,
expired, replayed, a clock beyond ClockSkew (five minutes by default, MIT’s
too) — is ErrRejected. Bytes that are not a GSS-API krb5 token are
ErrNotAToken, and a token that is not an AP-REQ is ErrNotAPReq. The errors
are deliberately coarse: a client learns that its ticket was refused, never
which check refused it. An error means no context, and the returned token is
empty.
A principal the acceptor does not answer for. With
krb5.Principal("nfs/files.example.org"), a keytab holding several services
answers for that one only.
A message whose signature does not match is ErrBadMIC, whichever of the
length, the header, the sequence or the checksum disagreed. The acceptor signs
with key usage 23 and verifies with 25, so a token this server produced cannot
be replayed back at it as though the client had signed it.
A sealed token given to Unwrap is ErrSealed, distinct from a bad
signature: a caller reading an integrity-only token must not be handed
ciphertext that reads like a message. An unsealed token given to Unseal
is ErrNotSealed: a protocol that promised confidentiality is not handed
something that crossed the wire in the clear.
It 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 (RFC 4121 §4.3).
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.
What it is checked against
A real MIT KDC and MIT’s own gss-client, against this package standing
in for gss-server. test/kdc.sh builds a throwaway realm on loopback,
unprivileged; the CI gets MIT Kerberos through pkgx (Ubuntu’s packages do not
ship gss-client). The Linux test lane, the race detector and the coverage
lane set KRB5_REQUIRE_JUDGE=1, so a missing fixture fails. A further step
runs TestMITClientAgainstThisAcceptor and fails unless the log says
authenticated as alice@ — a green lane whose judge silently stopped running
is not accepted. TestMITClientSealed does the same with confidentiality on.
The macOS, Windows and cross-architecture lanes have no realm, and there the judge skips. The coverage lane’s floor is 90%.
Reference
Created on by tannevaled
Modified on by tannevaled