Aller au contenu

krb5 — accepter un ticket Kerberos

go-authn/krb5 est la moitié de Kerberos qui accepte : un keytab, un AP-REQ vérifié, l'AP-REP dont l'authentification mutuelle a besoin, et des messages signés et scellés sous la clé de session.

Ce ticket Kerberos est-il bon pour ce service ? La moitié accepteur de Kerberos, et seulement cette moitié : il détient un keytab, vérifie l’AP-REQ qu’un client présente, répond par l’AP-REP qui prouve que le service est authentique, puis signe, vérifie, scelle et ouvre des messages sous la clé que cet échange a établie.

go get github.com/go-authn/krb5@v0.3.1

Il ne parle jamais à un KDC et ne délivre jamais de ticket, donc de qui il accepte les tickets est la décision de quelqu’un d’autre. La moitié qui délivre est kdc, et ce module n’en dépend pas — voir pourquoi ce sont des modules distincts. Le déchiffrement des tickets est celui de gokrb5.

Pourquoi il existe

gokrb5 est une bibliothèque cliente. Elle vérifie un AP-REQ et ne sait pas construire l’AP-REP qui y répond. Tout client GSS-API demande l’authentification mutuelle — le propre gss-client du MIT positionne GSS_C_MUTUAL_FLAG sans moyen de le désactiver — donc un service qui ne sait pas produire d’AP-REP n’établit aucun contexte. Le construire est l’essentiel de ce que ce module ajoute.

Exemple

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 directory

Il a besoin de github.com/jcmturner/gokrb5/v8 pour keytab.New, que krb5.New prend ; krb5.Load lit plutôt un keytab depuis un chemin.

Après Accept, un *Context dit qui : Principal() (alice@EXAMPLE.ORG), ou User() et Realm() séparément. Faire correspondre un principal à un compte local est la politique de l’appelant : alice@EXAMPLE.ORG et alice@PARTNER.ORG répondent tous deux à « alice ». Expires() est le moment où le ticket qui le sous-tend se termine, au-delà duquel un serveur qui maintient une connexion de longue durée devrait cesser de l’honorer. MIC/VerifyMIC et Wrap/Unwrap signent ; Seal/Unseal ajoutent la confidentialité (sec=krb5p). Un Context n’est pas sûr en usage concurrent ; un Acceptor l’est.

Ce qu’il refuse, et pourquoi

Un ticket que ce service n’acceptera pas — mauvais service, mauvaise clé, expiré, rejoué, une horloge au-delà de ClockSkew (cinq minutes par défaut, comme chez le MIT) — est ErrRejected. Des octets qui ne sont pas un jeton GSS-API krb5 sont ErrNotAToken, et un jeton qui n’est pas un AP-REQ est ErrNotAPReq. Les erreurs sont délibérément grossières : un client apprend que son ticket a été refusé, jamais quelle vérification l’a refusé. Une erreur signifie aucun contexte, et le jeton renvoyé est vide.

Un principal pour lequel l’accepteur ne répond pas. Avec krb5.Principal("nfs/files.example.org"), un keytab qui détient plusieurs services ne répond que pour celui-là.

Un message dont la signature ne correspond pas est ErrBadMIC, quel que soit, de la longueur, de l’en-tête, de la séquence ou de la somme de contrôle, l’élément en désaccord. L’accepteur signe avec l’usage de clé 23 et vérifie avec 25, donc un jeton que ce serveur a produit ne peut pas lui être rejoué comme si le client l’avait signé.

Un jeton scellé remis à Unwrap est ErrSealed, distinct d’une mauvaise signature : un appelant qui lit un jeton à intégrité seule ne doit pas recevoir un chiffré qui se lit comme un message. Un jeton non scellé remis à Unseal est ErrNotSealed : un protocole qui a promis la confidentialité ne reçoit pas quelque chose qui a traversé le réseau en clair.

Il n’impose aucune sous-clé d’accepteur : la clé du contexte est la sous-clé que l’initiateur a mise dans son authentificateur, ou la clé de session du ticket quand il n’en a envoyé aucune (RFC 4121 §4.3).

Un keytab est un secret au repos

Son chemin peut venir d’un fichier de configuration ou de KRB5_KTNAME ; la clé qu’il contient ne doit jamais être journalisée, affichée, ni transportée dans une variable d’environnement.

Ce contre quoi il est vérifié

Un vrai KDC du MIT et le propre gss-client du MIT, contre ce paquet qui tient le rôle de gss-server. test/kdc.sh construit un royaume jetable sur l’interface de bouclage, sans privilèges ; la CI obtient MIT Kerberos via pkgx (les paquets d’Ubuntu ne livrent pas gss-client). La voie de test Linux, le détecteur de concurrence et la voie de couverture positionnent KRB5_REQUIRE_JUDGE=1, donc un jeu d’essai manquant fait échouer. Une étape supplémentaire exécute TestMITClientAgainstThisAcceptor et échoue à moins que le journal ne dise authenticated as alice@ — une voie verte dont le juge a silencieusement cessé de s’exécuter n’est pas acceptée. TestMITClientSealed fait de même avec la confidentialité activée.

Les voies macOS, Windows et inter-architectures n’ont pas de royaume, et là le juge est sauté. Le plancher de la voie de couverture est de 90 %.

Référence

pkg.go.dev/github.com/go-authn/krb5@v0.3.1

Créée le par tannevaled

Modifiée le par tannevaled