Browse Source

pac example

Jonathan Turner 9 years ago
parent
commit
5b3589f9e7

+ 20 - 0
client/client_integration_test.go

@@ -167,6 +167,26 @@ func TestClient_GetServiceTicket_OlderKDC(t *testing.T) {
 	assert.Equal(t, 18, key.KeyType)
 }
 
+func TestClient_GetServiceTicket_AD(t *testing.T) {
+	b, err := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt, _ := keytab.Parse(b)
+	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF_AD)
+	cl := NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt)
+	cl.WithConfig(c)
+
+	err = cl.Login()
+	if err != nil {
+		t.Fatalf("Error on login: %v\n", err)
+	}
+	spn := "HTTP/host.test.gokrb5"
+	tkt, key, err := cl.GetServiceTicket(spn)
+	if err != nil {
+		t.Fatalf("Error getting service ticket: %v\n", err)
+	}
+	assert.Equal(t, spn, tkt.SName.GetPrincipalNameString())
+	assert.Equal(t, 18, key.KeyType)
+}
+
 func TestClient_SetSPNEGOHeader(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt, _ := keytab.Parse(b)

+ 7 - 5
credentials/credentials.go

@@ -28,16 +28,18 @@ func NewCredentials(username string, realm string) Credentials {
 			NameType:   nametype.KRB_NT_PRINCIPAL,
 			NameString: []string{username},
 		},
-		Keytab: keytab.NewKeytab(),
+		Keytab:     keytab.NewKeytab(),
+		Attributes: make(map[string]interface{}),
 	}
 }
 
 func NewCredentialsFromPrincipal(cname types.PrincipalName, realm string) Credentials {
 	return Credentials{
-		Username: cname.GetPrincipalNameString(),
-		Realm:    realm,
-		CName:    cname,
-		Keytab:   keytab.NewKeytab(),
+		Username:   cname.GetPrincipalNameString(),
+		Realm:      realm,
+		CName:      cname,
+		Keytab:     keytab.NewKeytab(),
+		Attributes: make(map[string]interface{}),
 	}
 }
 

+ 94 - 0
examples/example-AD.go

@@ -0,0 +1,94 @@
+// +build examples
+
+package main
+
+import (
+	"encoding/hex"
+	"fmt"
+	"github.com/jcmturner/gokrb5/client"
+	"github.com/jcmturner/gokrb5/config"
+	"github.com/jcmturner/gokrb5/credentials"
+	"github.com/jcmturner/gokrb5/keytab"
+	"github.com/jcmturner/gokrb5/service"
+	"github.com/jcmturner/gokrb5/testdata"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"time"
+)
+
+func main() {
+	s := httpServer()
+	defer s.Close()
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt, _ := keytab.Parse(b)
+	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF_AD)
+	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt)
+	cl.WithConfig(c)
+	httpRequest(s.URL, cl)
+
+	b, _ = hex.DecodeString(testdata.TESTUSER2_KEYTAB)
+	kt, _ = keytab.Parse(b)
+	c, _ = config.NewConfigFromString(testdata.TEST_KRB5CONF_AD)
+	cl = client.NewClientWithKeytab("testuser2", "TEST.GOKRB5", kt)
+	cl.WithConfig(c)
+	httpRequest(s.URL, cl)
+
+	//httpRequest("http://host.test.gokrb5/index.html")
+}
+
+func httpRequest(url string, cl client.Client) {
+	l := log.New(os.Stderr, "GOKRB5 Client: ", log.Ldate|log.Ltime|log.Lshortfile)
+
+	err := cl.Login()
+	if err != nil {
+		l.Printf("Error on AS_REQ: %v\n", err)
+	}
+	cl.EnableAutoSessionRenewal()
+	r, _ := http.NewRequest("GET", url, nil)
+	err = cl.SetSPNEGOHeader(r, "HTTP/host.test.gokrb5")
+	if err != nil {
+		l.Printf("Error setting client SPNEGO header: %v", err)
+	}
+	httpResp, err := http.DefaultClient.Do(r)
+	if err != nil {
+		l.Printf("Request error: %v\n", err)
+	}
+	fmt.Fprintf(os.Stdout, "Response Code: %v\n", httpResp.StatusCode)
+	content, _ := ioutil.ReadAll(httpResp.Body)
+	fmt.Fprintf(os.Stdout, "Response Body:\n%s\n", content)
+}
+
+func httpServer() *httptest.Server {
+	l := log.New(os.Stderr, "GOKRB5 Service: ", log.Ldate|log.Ltime|log.Lshortfile)
+	b, _ := hex.DecodeString(testdata.SYSHTTP_KEYTAB)
+	kt, _ := keytab.Parse(b)
+	th := http.HandlerFunc(testAppHandler)
+	s := httptest.NewServer(service.SPNEGOKRB5Authenticate(th, kt, "sysHTTP", l))
+	return s
+}
+
+func testAppHandler(w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(http.StatusOK)
+	ctx := r.Context()
+	if ctx.Value("credentials") != nil {
+		// Note that you should really check each attribute is not nil before doing the type assertion.
+		fmt.Fprintf(w, "<html>\nTEST.GOKRB5 Handler\n<ul><li>Authenticed user: %s</li>\n<li>User's realm: %s</li>\n", ctx.Value("credentials").(credentials.Credentials).Username, ctx.Value("credentials").(credentials.Credentials).Realm)
+		fmt.Fprintf(w, "<li>EffectiveName: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["effectiveName"].(string))
+		fmt.Fprintf(w, "<li>FullName: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["fullName"].(string))
+		fmt.Fprintf(w, "<li>UserID: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["userID"].(int))
+		fmt.Fprintf(w, "<li>PrimaryGroupID: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["primaryGroupID"].(int))
+		fmt.Fprintf(w, "<li>Group SIDs: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["groupMembershipSIDs"].([]string))
+		fmt.Fprintf(w, "<li>LogOnTime: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["logOnTime"].(time.Time))
+		fmt.Fprintf(w, "<li>LogOffTime: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["logOffTime"].(time.Time))
+		fmt.Fprintf(w, "<li>PasswordLastSet: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["passwordLastSet"].(time.Time))
+		fmt.Fprintf(w, "<li>LogonServer: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["logonServer"].(string))
+		fmt.Fprintf(w, "<li>LogonDomainName: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["logonDomainName"].(string))
+		fmt.Fprintf(w, "<li>LogonDomainID: %v</li>\n", ctx.Value("credentials").(credentials.Credentials).Attributes["logonDomainID"].(string))
+		fmt.Fprintf(w, "</ul></html>")
+	}
+	return
+}

+ 1 - 1
examples/example.go

@@ -79,7 +79,7 @@ func httpServer() *httptest.Server {
 	b, _ := hex.DecodeString(testdata.HTTP_KEYTAB)
 	kt, _ := keytab.Parse(b)
 	th := http.HandlerFunc(testAppHandler)
-	s := httptest.NewServer(service.SPNEGOKRB5Authenticate(th, kt, l))
+	s := httptest.NewServer(service.SPNEGOKRB5Authenticate(th, kt, "", l))
 	return s
 }
 

+ 5 - 1
messages/KRBError.go

@@ -58,7 +58,11 @@ func (k *KRBError) Unmarshal(b []byte) error {
 
 // Error method implementing error interface on KRBError struct.
 func (k KRBError) Error() string {
-	return fmt.Sprintf("KRB Error: %s - %s", errorcode.ErrorCodeLookup(k.ErrorCode), k.EText)
+	etxt := fmt.Sprintf("KRB Error: %s", errorcode.ErrorCodeLookup(k.ErrorCode))
+	if k.EText != "" {
+		etxt = fmt.Sprintf("%s - %s", etxt, k.EText)
+	}
+	return etxt
 }
 
 func processReplyError(b []byte, err error) error {

+ 22 - 9
messages/Ticket.go

@@ -2,7 +2,6 @@ package messages
 
 import (
 	"crypto/rand"
-	"errors"
 	"fmt"
 	"github.com/jcmturner/asn1"
 	"github.com/jcmturner/gokrb5/asn1tools"
@@ -167,8 +166,14 @@ func MarshalTicketSequence(tkts []Ticket) (asn1.RawValue, error) {
 	return raw, nil
 }
 
-func (t *Ticket) DecryptEncPart(keytab keytab.Keytab) error {
-	key, err := keytab.GetEncryptionKey(t.SName.NameString, t.Realm, t.EncPart.KVNO, t.EncPart.EType)
+func (t *Ticket) DecryptEncPart(keytab keytab.Keytab, sa string) error {
+	var upn []string
+	if sa != "" {
+		upn = []string{sa}
+	} else {
+		upn = t.SName.NameString
+	}
+	key, err := keytab.GetEncryptionKey(upn, t.Realm, t.EncPart.KVNO, t.EncPart.EType)
 	if err != nil {
 		return NewKRBError(t.SName, t.Realm, errorcode.KRB_AP_ERR_NOKEY, fmt.Sprintf("Could not get key from keytab: %v", err))
 	}
@@ -185,7 +190,8 @@ func (t *Ticket) DecryptEncPart(keytab keytab.Keytab) error {
 	return nil
 }
 
-func (t *Ticket) GetPACType(keytab keytab.Keytab) (pac.PACType, error) {
+func (t *Ticket) GetPACType(keytab keytab.Keytab, sa string) (bool, pac.PACType, error) {
+	var isPAC bool
 	for _, ad := range t.DecryptedEncPart.AuthorizationData {
 		if ad.ADType == adtype.AD_IF_RELEVANT {
 			var ad2 types.AuthorizationData
@@ -195,19 +201,26 @@ func (t *Ticket) GetPACType(keytab keytab.Keytab) (pac.PACType, error) {
 			}
 			// TODO note does the entry contain and AuthorizationData or AuthorizationDataEntry. Assuming the former atm.
 			if ad2[0].ADType == adtype.AD_WIN2K_PAC {
+				isPAC = true
 				var pac pac.PACType
 				err = pac.Unmarshal(ad2[0].ADData)
 				if err != nil {
-					return pac, fmt.Errorf("Error unmarshaling PAC: %v", err)
+					return isPAC, pac, fmt.Errorf("Error unmarshaling PAC: %v", err)
+				}
+				var upn []string
+				if sa != "" {
+					upn = []string{sa}
+				} else {
+					upn = t.SName.NameString
 				}
-				key, err := keytab.GetEncryptionKey(t.SName.NameString, t.Realm, t.EncPart.KVNO, t.EncPart.EType)
+				key, err := keytab.GetEncryptionKey(upn, t.Realm, t.EncPart.KVNO, t.EncPart.EType)
 				if err != nil {
-					return pac, NewKRBError(t.SName, t.Realm, errorcode.KRB_AP_ERR_NOKEY, fmt.Sprintf("Could not get key from keytab: %v", err))
+					return isPAC, pac, NewKRBError(t.SName, t.Realm, errorcode.KRB_AP_ERR_NOKEY, fmt.Sprintf("Could not get key from keytab: %v", err))
 				}
 				err = pac.ProcessPACInfoBuffers(key)
-				return pac, err
+				return isPAC, pac, err
 			}
 		}
 	}
-	return pac.PACType{}, errors.New("AuthorizationData within the ticket does not contain PAC data.")
+	return isPAC, pac.PACType{}, nil
 }

+ 0 - 0
pac/pac_client_claims.go → pac/client_claims.go


+ 0 - 0
pac/pac_client_info.go → pac/client_info.go


+ 0 - 0
pac/pac_credentials_info.go → pac/credentials_info.go


+ 0 - 0
pac/pac_device_claims.go → pac/device_claims.go


+ 0 - 0
pac/pac_device_info.go → pac/device_info.go


+ 0 - 0
pac/pac_signature_data.go → pac/signature_data.go


+ 34 - 15
pac/upn_dns_info.go

@@ -1,8 +1,9 @@
 package pac
 
 import (
-	"fmt"
+	"encoding/binary"
 	"github.com/jcmturner/gokrb5/ndr"
+	"sort"
 )
 
 // https://msdn.microsoft.com/en-us/library/dd240468.aspx
@@ -12,6 +13,8 @@ type UPN_DNSInfo struct {
 	DNSDomainNameLength uint16
 	DNSDomainNameOffset uint16
 	Flags               uint32
+	UPN                 string
+	DNSDomain           string
 }
 
 const (
@@ -19,25 +22,41 @@ const (
 )
 
 func (k *UPN_DNSInfo) Unmarshal(b []byte) error {
-	ch, _, p, err := ndr.ReadHeaders(&b)
-	if err != nil {
-		return fmt.Errorf("Error parsing byte stream headers: %v", err)
-	}
-	e := &ch.Endianness
+	//The UPN_DNS_INFO structure is a simple structure that is not NDR-encoded.
+	var p int
+	var e binary.ByteOrder = binary.LittleEndian
 
-	//The next 4 bytes are an RPC unique pointer referent. We just skip these
-	p += 4
+	k.UPNLength = ndr.Read_uint16(&b, &p, &e)
+	k.UPNOffset = ndr.Read_uint16(&b, &p, &e)
+	k.DNSDomainNameLength = ndr.Read_uint16(&b, &p, &e)
+	k.DNSDomainNameOffset = ndr.Read_uint16(&b, &p, &e)
+	k.Flags = ndr.Read_uint32(&b, &p, &e)
+	ub := b[k.UPNOffset : k.UPNOffset+k.UPNLength]
+	db := b[k.DNSDomainNameOffset : k.DNSDomainNameOffset+k.DNSDomainNameLength]
 
-	k.UPNLength = ndr.Read_uint16(&b, &p, e)
-	k.UPNOffset = ndr.Read_uint16(&b, &p, e)
-	k.DNSDomainNameLength = ndr.Read_uint16(&b, &p, e)
-	k.DNSDomainNameOffset = ndr.Read_uint16(&b, &p, e)
-	k.Flags = ndr.Read_uint32(&b, &p, e)
+	u := make([]rune, k.UPNLength/2, k.UPNLength/2)
+	for i := 0; i < len(u); i++ {
+		q := i * 2
+		u[i] = rune(ndr.Read_uint16(&ub, &q, &e))
+	}
+	k.UPN = string(u)
+	d := make([]rune, k.DNSDomainNameLength/2, k.DNSDomainNameLength/2)
+	for i := 0; i < len(d); i++ {
+		q := i * 2
+		d[i] = rune(ndr.Read_uint16(&db, &q, &e))
+	}
+	k.UPN = string(d)
 
+	l := []int{
+		p,
+		int(k.UPNOffset + k.UPNLength),
+		int(k.DNSDomainNameOffset + k.DNSDomainNameLength),
+	}
+	sort.Ints(l)
 	//Check that there is only zero padding left
-	for _, v := range b[p:] {
+	for _, v := range b[l[2]:] {
 		if v != 0 {
-			return ndr.NDRMalformed{EText: "Non-zero padding left over at end of data stream"}
+			return ndr.NDRMalformed{EText: "Non-zero padding left over at end of data stream."}
 		}
 	}
 

+ 8 - 5
service/APExchange.go

@@ -14,9 +14,9 @@ import (
 )
 
 // Validates an AP_REQ sent to the service. Returns a boolean for if the AP_REQ is valid and the client's principal name and realm.
-func ValidateAPREQ(APReq messages.APReq, kt keytab.Keytab, cAddr string) (bool, credentials.Credentials, error) {
+func ValidateAPREQ(APReq messages.APReq, kt keytab.Keytab, sa string, cAddr string) (bool, credentials.Credentials, error) {
 	var creds credentials.Credentials
-	err := APReq.Ticket.DecryptEncPart(kt)
+	err := APReq.Ticket.DecryptEncPart(kt, sa)
 	if err != nil {
 		return false, creds, fmt.Errorf("Error decrypting encpart of service ticket provided: %v", err)
 	}
@@ -81,9 +81,12 @@ func ValidateAPREQ(APReq messages.APReq, kt keytab.Keytab, cAddr string) (bool,
 		return false, creds, err
 	}
 	creds = credentials.NewCredentialsFromPrincipal(a.CName, a.CRealm)
-	pac, err := APReq.Ticket.GetPACType(kt)
-	if err == nil {
-		// There is a PAC. Adding attributes to creds
+	isPAC, pac, err := APReq.Ticket.GetPACType(kt, sa)
+	if isPAC && err != nil {
+		return false, creds, err
+	}
+	if isPAC {
+		// There is a valid PAC. Adding attributes to creds
 		creds.Attributes["groupMembershipSIDs"] = pac.KerbValidationInfo.GetGroupMembershipSIDs()
 		creds.Attributes["logOnTime"] = pac.KerbValidationInfo.LogOnTime.Time()
 		creds.Attributes["logOffTime"] = pac.KerbValidationInfo.LogOffTime.Time()

+ 8 - 2
service/http.go

@@ -19,7 +19,13 @@ const (
 )
 
 // Kerberos SPNEGO authentication HTTP handler wrapper.
-func SPNEGOKRB5Authenticate(f http.Handler, kt keytab.Keytab, l *log.Logger) http.Handler {
+//
+// kt - keytab for the service user
+//
+// sa - service account name.
+// If Active Directory is used for the KDC this is the account name you have set the SPN against (setspn.exe -a "HTTP/<fqdn>" <account name>)
+// If the SPN was added to the KDC without associating it with an account pass and empty string "". This is the case if you create the SPN in MIT KDC with: /usr/sbin/kadmin.local -q "add_principal HTTP/<fqdn>"
+func SPNEGOKRB5Authenticate(f http.Handler, kt keytab.Keytab, sa string, l *log.Logger) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
 		if len(s) != 2 || s[0] != "Negotiate" {
@@ -54,7 +60,7 @@ func SPNEGOKRB5Authenticate(f http.Handler, kt keytab.Keytab, l *log.Logger) htt
 			return
 		}
 
-		if ok, creds, err := ValidateAPREQ(mt.APReq, kt, r.RemoteAddr); ok {
+		if ok, creds, err := ValidateAPREQ(mt.APReq, kt, sa, r.RemoteAddr); ok {
 			ctx := r.Context()
 			ctx = context.WithValue(ctx, "credentials", creds)
 			ctx = context.WithValue(ctx, "authenticated", true)

File diff suppressed because it is too large
+ 0 - 0
testdata/test_vectors.go


Some files were not shown because too many files changed in this diff