瀏覽代碼

add client diagnostics method

add client diagnostics method
Jonathan Turner 6 年之前
父節點
當前提交
f1c93a8558

+ 7 - 0
v8/USAGE.md

@@ -145,6 +145,13 @@ REALM.COM = {
 ```
 See https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html#realms for more information.
 
+#### Client Diagnostics
+In the event of issues the configuration of a client can be investigated with its ``Diagnostics`` method.
+This will check that the required enctypes defined in the client's krb5 config are available in its keytab.
+It will also check that KDCs can be resolved for the client's REALM.
+The error returned will contain details of any failed checks.
+The configuration details of the client will be written to the ``io.Writer`` provided.
+
 ---
 
 ### Kerberised Service

+ 26 - 2
v8/client/cache.go

@@ -1,7 +1,9 @@
 package client
 
 import (
+	"encoding/json"
 	"errors"
+	"sort"
 	"sync"
 	"time"
 
@@ -17,12 +19,13 @@ type Cache struct {
 
 // CacheEntry holds details for a cache entry.
 type CacheEntry struct {
-	Ticket     messages.Ticket
+	SPN        string
+	Ticket     messages.Ticket `json:"-"`
 	AuthTime   time.Time
 	StartTime  time.Time
 	EndTime    time.Time
 	RenewTill  time.Time
-	SessionKey types.EncryptionKey
+	SessionKey types.EncryptionKey `json:"-"`
 }
 
 // NewCache creates a new client ticket cache instance.
@@ -40,12 +43,33 @@ func (c *Cache) getEntry(spn string) (CacheEntry, bool) {
 	return e, ok
 }
 
+// JSON returns information about the cached service tickets in a JSON format.
+func (c *Cache) JSON() (string, error) {
+	c.mux.RLock()
+	defer c.mux.RUnlock()
+	var es []CacheEntry
+	keys := make([]string, 0, len(c.Entries))
+	for k := range c.Entries {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	for _, k := range keys {
+		es = append(es, c.Entries[k])
+	}
+	b, err := json.MarshalIndent(&es, "", "  ")
+	if err != nil {
+		return "", err
+	}
+	return string(b), nil
+}
+
 // addEntry adds a ticket to the cache.
 func (c *Cache) addEntry(tkt messages.Ticket, authTime, startTime, endTime, renewTill time.Time, sessionKey types.EncryptionKey) CacheEntry {
 	spn := tkt.SName.PrincipalNameString()
 	c.mux.Lock()
 	defer c.mux.Unlock()
 	(*c).Entries[spn] = CacheEntry{
+		SPN:        spn,
 		Ticket:     tkt,
 		AuthTime:   authTime,
 		StartTime:  startTime,

+ 146 - 0
v8/client/cache_test.go

@@ -0,0 +1,146 @@
+package client
+
+import (
+	"fmt"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/jcmturner/gokrb5/v8/messages"
+	"github.com/jcmturner/gokrb5/v8/types"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCache_addEntry_getEntry_remove_clear(t *testing.T) {
+	t.Parallel()
+	c := NewCache()
+	cnt := 10
+	var wg sync.WaitGroup
+	for i := 0; i < cnt; i++ {
+		wg.Add(1)
+		tkt := messages.Ticket{
+			SName: types.PrincipalName{
+				NameType:   1,
+				NameString: []string{fmt.Sprintf("%d", i), "test.cache"},
+			},
+		}
+		key := types.EncryptionKey{
+			KeyType:  1,
+			KeyValue: []byte{byte(i)},
+		}
+		go func(i int) {
+			e := c.addEntry(tkt, time.Unix(int64(0+i), 0).UTC(), time.Unix(int64(10+i), 0).UTC(), time.Unix(int64(20+i), 0).UTC(), time.Unix(int64(30+i), 0).UTC(), key)
+			assert.Equal(t, fmt.Sprintf("%d/test.cache", i), e.SPN, "SPN cache key not as expected")
+			wg.Done()
+		}(i)
+	}
+	wg.Wait()
+	for i := 0; i < cnt; i++ {
+		wg.Add(1)
+		go func(i int) {
+			e, ok := c.getEntry(fmt.Sprintf("%d/test.cache", i))
+			assert.True(t, ok, "cache entry %d was not found", i)
+			assert.Equal(t, time.Unix(int64(0+i), 0).UTC(), e.AuthTime, "auth time not as expected")
+			assert.Equal(t, time.Unix(int64(10+i), 0).UTC(), e.StartTime, "start time not as expected")
+			assert.Equal(t, time.Unix(int64(20+i), 0).UTC(), e.EndTime, "end time not as expected")
+			assert.Equal(t, time.Unix(int64(30+i), 0).UTC(), e.RenewTill, "renew time not as expected")
+			assert.Equal(t, []string{fmt.Sprintf("%d", i), "test.cache"}, e.Ticket.SName.NameString, "ticket not correct")
+			assert.Equal(t, []byte{byte(i)}, e.SessionKey.KeyValue, "session key not correct")
+			wg.Done()
+		}(i)
+	}
+	wg.Wait()
+	_, ok := c.getEntry(fmt.Sprintf("%d/test.cache", cnt+1))
+	assert.False(t, ok, "entry found in cache when it shouldn't have been")
+
+	// Remove just the even entries
+	for i := 0; i < cnt; i += 2 {
+		wg.Add(1)
+		go func(i int) {
+			c.RemoveEntry(fmt.Sprintf("%d/test.cache", i))
+			wg.Done()
+		}(i)
+	}
+	wg.Wait()
+
+	for i := 0; i < cnt; i++ {
+		wg.Add(1)
+		go func(i int) {
+			if i%2 == 0 {
+				_, ok := c.getEntry(fmt.Sprintf("%d/test.cache", cnt+1))
+				assert.False(t, ok, "entry %d found in cache when it shouldn't have been", i)
+			} else {
+				e, ok := c.getEntry(fmt.Sprintf("%d/test.cache", i))
+				assert.True(t, ok, "cache entry %d was not found", i)
+				assert.Equal(t, time.Unix(int64(0+i), 0).UTC(), e.AuthTime, "auth time not as expected")
+				assert.Equal(t, time.Unix(int64(10+i), 0).UTC(), e.StartTime, "start time not as expected")
+				assert.Equal(t, time.Unix(int64(20+i), 0).UTC(), e.EndTime, "end time not as expected")
+				assert.Equal(t, time.Unix(int64(30+i), 0).UTC(), e.RenewTill, "renew time not as expected")
+				assert.Equal(t, []string{fmt.Sprintf("%d", i), "test.cache"}, e.Ticket.SName.NameString, "ticket not correct")
+				assert.Equal(t, []byte{byte(i)}, e.SessionKey.KeyValue, "session key not correct")
+			}
+			wg.Done()
+		}(i)
+	}
+	wg.Wait()
+
+	// Clear the cache
+	c.clear()
+	for i := 0; i < cnt; i++ {
+		wg.Add(1)
+		go func(i int) {
+			_, ok := c.getEntry(fmt.Sprintf("%d/test.cache", cnt+1))
+			assert.False(t, ok, "entry %d found in cache when it shouldn't have been", i)
+			wg.Done()
+		}(i)
+	}
+	wg.Wait()
+}
+
+func TestCache_JSON(t *testing.T) {
+	t.Parallel()
+	c := NewCache()
+	cnt := 3
+	for i := 0; i < cnt; i++ {
+		tkt := messages.Ticket{
+			SName: types.PrincipalName{
+				NameType:   1,
+				NameString: []string{fmt.Sprintf("%d", i), "test.cache"},
+			},
+		}
+		key := types.EncryptionKey{
+			KeyType:  1,
+			KeyValue: []byte{byte(i)},
+		}
+		e := c.addEntry(tkt, time.Unix(int64(0+i), 0).UTC(), time.Unix(int64(10+i), 0).UTC(), time.Unix(int64(20+i), 0).UTC(), time.Unix(int64(30+i), 0).UTC(), key)
+		assert.Equal(t, fmt.Sprintf("%d/test.cache", i), e.SPN, "SPN cache key not as expected")
+	}
+	expected := `[
+  {
+    "SPN": "0/test.cache",
+    "AuthTime": "1970-01-01T00:00:00Z",
+    "StartTime": "1970-01-01T00:00:10Z",
+    "EndTime": "1970-01-01T00:00:20Z",
+    "RenewTill": "1970-01-01T00:00:30Z"
+  },
+  {
+    "SPN": "1/test.cache",
+    "AuthTime": "1970-01-01T00:00:01Z",
+    "StartTime": "1970-01-01T00:00:11Z",
+    "EndTime": "1970-01-01T00:00:21Z",
+    "RenewTill": "1970-01-01T00:00:31Z"
+  },
+  {
+    "SPN": "2/test.cache",
+    "AuthTime": "1970-01-01T00:00:02Z",
+    "StartTime": "1970-01-01T00:00:12Z",
+    "EndTime": "1970-01-01T00:00:22Z",
+    "RenewTill": "1970-01-01T00:00:32Z"
+  }
+]`
+	j, err := c.JSON()
+	if err != nil {
+		t.Errorf("error getting json output of cache: %v", err)
+	}
+	assert.Equal(t, expected, j, "json output not as expected")
+}

+ 88 - 0
v8/client/client.go

@@ -2,8 +2,11 @@
 package client
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
+	"strings"
 	"time"
 
 	"github.com/jcmturner/gokrb5/v8/config"
@@ -239,3 +242,88 @@ func (cl *Client) Destroy() {
 	cl.Credentials = creds
 	cl.Log("client destroyed")
 }
+
+// Diagnostics runs a set of checks that the client is properly configured and writes details to the io.Writer provided.
+func (cl *Client) Diagnostics(w io.Writer) error {
+	cl.Print(w)
+	var errs []string
+	if cl.Credentials.HasKeytab() {
+		var loginRealmEncTypes []int32
+		for _, e := range cl.Credentials.Keytab().Entries {
+			if e.Principal.Realm == cl.Credentials.Realm() {
+				loginRealmEncTypes = append(loginRealmEncTypes, e.Key.KeyType)
+			}
+		}
+		for _, et := range cl.Config.LibDefaults.DefaultTktEnctypeIDs {
+			var etInKt bool
+			for _, val := range loginRealmEncTypes {
+				if val == et {
+					etInKt = true
+					break
+				}
+			}
+			if !etInKt {
+				errs = append(errs, fmt.Sprintf("default_tkt_enctypes specifies %d but this enctype is not available in the client's keytab", et))
+			}
+		}
+		for _, et := range cl.Config.LibDefaults.PreferredPreauthTypes {
+			var etInKt bool
+			for _, val := range loginRealmEncTypes {
+				if int(val) == et {
+					etInKt = true
+					break
+				}
+			}
+			if !etInKt {
+				errs = append(errs, fmt.Sprintf("preferred_preauth_types specifies %d but this enctype is not available in the client's keytab", et))
+			}
+		}
+	}
+	udpCnt, udpKDC, err := cl.Config.GetKDCs(cl.Credentials.Realm(), false)
+	if err != nil {
+		errs = append(errs, fmt.Sprintf("error when resolving KDCs for UDP communication: %v", err))
+	}
+	if udpCnt < 1 {
+		errs = append(errs, "no KDCs resolved for communication via UDP.")
+	} else {
+		b, _ := json.MarshalIndent(&udpKDC, "", "  ")
+		fmt.Fprintf(w, "UDP KDCs: %s\n", string(b))
+	}
+	tcpCnt, tcpKDC, err := cl.Config.GetKDCs(cl.Credentials.Realm(), false)
+	if err != nil {
+		errs = append(errs, fmt.Sprintf("error when resolving KDCs for TCP communication: %v", err))
+	}
+	if tcpCnt < 1 {
+		errs = append(errs, "no KDCs resolved for communication via TCP.")
+	} else {
+		b, _ := json.MarshalIndent(&tcpKDC, "", "  ")
+		fmt.Fprintf(w, "TCP KDCs: %s\n", string(b))
+	}
+
+	if errs == nil || len(errs) < 1 {
+		return nil
+	}
+	err = fmt.Errorf(strings.Join(errs, "\n"))
+	return err
+}
+
+// Print writes the details of the client to the io.Writer provided.
+func (cl *Client) Print(w io.Writer) {
+	c, _ := cl.Credentials.JSON()
+	fmt.Fprintf(w, "Credentials:\n%s\n", c)
+
+	s, _ := cl.sessions.JSON()
+	fmt.Fprintf(w, "TGT Sessions:\n%s\n", s)
+
+	c, _ = cl.cache.JSON()
+	fmt.Fprintf(w, "Service ticket cache:\n%s\n", c)
+
+	s, _ = cl.settings.JSON()
+	fmt.Fprintf(w, "Settings:\n%s\n", s)
+
+	j, _ := cl.Config.JSON()
+	fmt.Fprintf(w, "Krb5 config:\n%s\n", j)
+
+	k, _ := cl.Credentials.Keytab().JSON()
+	fmt.Fprintf(w, "Keytab:\n%s\n", k)
+}

+ 1 - 2
v8/client/client_ad_integration_test.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding/hex"
 	"log"
+	"testing"
 
 	"github.com/jcmturner/gokrb5/v8/config"
 	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
@@ -13,8 +14,6 @@ import (
 	"github.com/jcmturner/gokrb5/v8/test/testdata"
 	"github.com/jcmturner/gokrb5/v8/types"
 	"github.com/stretchr/testify/assert"
-
-	"testing"
 )
 
 func TestClient_SuccessfulLogin_AD(t *testing.T) {

+ 40 - 0
v8/client/session.go

@@ -1,7 +1,9 @@
 package client
 
 import (
+	"encoding/json"
 	"fmt"
+	"sort"
 	"strings"
 	"sync"
 	"time"
@@ -69,6 +71,15 @@ type session struct {
 	mux                  sync.RWMutex
 }
 
+// jsonSession is used to enable marshaling some information of a session in a JSON format
+type jsonSession struct {
+	Realm                string
+	AuthTime             time.Time
+	EndTime              time.Time
+	RenewTill            time.Time
+	SessionKeyExpiration time.Time
+}
+
 // AddSession adds a session for a realm with a TGT to the client's session cache.
 // A goroutine is started to automatically renew the TGT before expiry.
 func (cl *Client) addSession(tgt messages.Ticket, dep messages.EncKDCRepPart) {
@@ -140,6 +151,34 @@ func (s *session) timeDetails() (string, time.Time, time.Time, time.Time, time.T
 	return s.realm, s.authTime, s.endTime, s.renewTill, s.sessionKeyExpiration
 }
 
+// JSON return information about the held sessions in a JSON format.
+func (s *sessions) JSON() (string, error) {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+	var js []jsonSession
+	keys := make([]string, 0, len(s.Entries))
+	for k := range s.Entries {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	for _, k := range keys {
+		r, at, et, rt, kt := s.Entries[k].timeDetails()
+		j := jsonSession{
+			Realm:                r,
+			AuthTime:             at,
+			EndTime:              et,
+			RenewTill:            rt,
+			SessionKeyExpiration: kt,
+		}
+		js = append(js, j)
+	}
+	b, err := json.MarshalIndent(js, "", "  ")
+	if err != nil {
+		return "", err
+	}
+	return string(b), nil
+}
+
 // enableAutoSessionRenewal turns on the automatic renewal for the client's TGT session.
 func (cl *Client) enableAutoSessionRenewal(s *session) {
 	var timer *time.Timer
@@ -239,6 +278,7 @@ func (cl *Client) sessionTGT(realm string) (tgt messages.Ticket, sessionKey type
 	return
 }
 
+// sessionTimes provides the timing information with regards to a session for the realm specified.
 func (cl *Client) sessionTimes(realm string) (authTime, endTime, renewTime, sessionExp time.Time, err error) {
 	s, ok := cl.sessions.get(realm)
 	if !ok {

+ 45 - 0
v8/client/session_test.go

@@ -113,3 +113,48 @@ func TestClient_AutoRenew_Goroutine(t *testing.T) {
 		}
 	}
 }
+
+func TestSessions_JSON(t *testing.T) {
+	s := &sessions{
+		Entries: make(map[string]*session),
+	}
+	for i := 0; i < 3; i++ {
+		realm := fmt.Sprintf("test%d", i)
+		e := &session{
+			realm:                realm,
+			authTime:             time.Unix(int64(0+i), 0).UTC(),
+			endTime:              time.Unix(int64(10+i), 0).UTC(),
+			renewTill:            time.Unix(int64(20+i), 0).UTC(),
+			sessionKeyExpiration: time.Unix(int64(30+i), 0).UTC(),
+		}
+		s.Entries[realm] = e
+	}
+	j, err := s.JSON()
+	if err != nil {
+		t.Errorf("error getting json: %v", err)
+	}
+	expected := `[
+  {
+    "Realm": "test0",
+    "AuthTime": "1970-01-01T00:00:00Z",
+    "EndTime": "1970-01-01T00:00:10Z",
+    "RenewTill": "1970-01-01T00:00:20Z",
+    "SessionKeyExpiration": "1970-01-01T00:00:30Z"
+  },
+  {
+    "Realm": "test1",
+    "AuthTime": "1970-01-01T00:00:01Z",
+    "EndTime": "1970-01-01T00:00:11Z",
+    "RenewTill": "1970-01-01T00:00:21Z",
+    "SessionKeyExpiration": "1970-01-01T00:00:31Z"
+  },
+  {
+    "Realm": "test2",
+    "AuthTime": "1970-01-01T00:00:02Z",
+    "EndTime": "1970-01-01T00:00:12Z",
+    "RenewTill": "1970-01-01T00:00:22Z",
+    "SessionKeyExpiration": "1970-01-01T00:00:32Z"
+  }
+]`
+	assert.Equal(t, expected, j, "json output not as expected")
+}

+ 21 - 0
v8/client/settings.go

@@ -1,6 +1,7 @@
 package client
 
 import (
+	"encoding/json"
 	"fmt"
 	"log"
 )
@@ -13,6 +14,12 @@ type Settings struct {
 	logger                  *log.Logger
 }
 
+// jsonSettings is used when marshaling the Settings details to JSON format.
+type jsonSettings struct {
+	DisablePAFXFast         bool
+	AssumePreAuthentication bool
+}
+
 // NewSettings creates a new client settings struct.
 func NewSettings(settings ...func(*Settings)) *Settings {
 	s := new(Settings)
@@ -70,3 +77,17 @@ func (cl *Client) Log(format string, v ...interface{}) {
 		cl.settings.Logger().Output(2, fmt.Sprintf(format, v...))
 	}
 }
+
+// JSON returns a JSON representation of the settings.
+func (s *Settings) JSON() (string, error) {
+	js := jsonSettings{
+		DisablePAFXFast:         s.disablePAFXFast,
+		AssumePreAuthentication: s.assumePreAuthentication,
+	}
+	b, err := json.MarshalIndent(js, "", "  ")
+	if err != nil {
+		return "", err
+	}
+	return string(b), nil
+
+}

+ 10 - 0
v8/config/krb5conf.go

@@ -4,6 +4,7 @@ package config
 import (
 	"bufio"
 	"encoding/hex"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -716,3 +717,12 @@ func appendUntilFinal(s *[]string, value string, final *bool) {
 	}
 	*s = append(*s, value)
 }
+
+// JSON return details of the config in a JSON format.
+func (c *Config) JSON() (string, error) {
+	b, err := json.MarshalIndent(c, "", "  ")
+	if err != nil {
+		return "", err
+	}
+	return string(b), nil
+}

+ 151 - 0
v8/config/krb5conf_test.go

@@ -79,6 +79,141 @@ const (
    krb4_convert = false
  }
 `
+	krb5ConfJson = `{
+  "LibDefaults": {
+    "AllowWeakCrypto": false,
+    "Canonicalize": false,
+    "CCacheType": 4,
+    "Clockskew": 300000000000,
+    "DefaultClientKeytabName": "FILE:/home/gokrb5/client.keytab",
+    "DefaultKeytabName": "FILE:/etc/krb5.keytab",
+    "DefaultRealm": "TEST.GOKRB5",
+    "DefaultTGSEnctypes": [
+      "aes256-cts-hmac-sha1-96",
+      "aes128-cts-hmac-sha1-96",
+      "des3-cbc-sha1",
+      "arcfour-hmac-md5",
+      "camellia256-cts-cmac",
+      "camellia128-cts-cmac",
+      "des-cbc-crc",
+      "des-cbc-md5",
+      "des-cbc-md4"
+    ],
+    "DefaultTktEnctypes": [
+      "aes256-cts-hmac-sha1-96",
+      "aes128-cts-hmac-sha1-96"
+    ],
+    "DefaultTGSEnctypeIDs": [
+      18,
+      17,
+      23
+    ],
+    "DefaultTktEnctypeIDs": [
+      18,
+      17
+    ],
+    "DNSCanonicalizeHostname": true,
+    "DNSLookupKDC": false,
+    "DNSLookupRealm": false,
+    "ExtraAddresses": null,
+    "Forwardable": true,
+    "IgnoreAcceptorHostname": false,
+    "K5LoginAuthoritative": false,
+    "K5LoginDirectory": "/home/test",
+    "KDCDefaultOptions": {
+      "Bytes": "AAAAEA==",
+      "BitLength": 32
+    },
+    "KDCTimeSync": 1,
+    "NoAddresses": true,
+    "PermittedEnctypes": [
+      "aes256-cts-hmac-sha1-96",
+      "aes128-cts-hmac-sha1-96",
+      "des3-cbc-sha1",
+      "arcfour-hmac-md5",
+      "camellia256-cts-cmac",
+      "camellia128-cts-cmac",
+      "des-cbc-crc",
+      "des-cbc-md5",
+      "des-cbc-md4"
+    ],
+    "PermittedEnctypeIDs": [
+      18,
+      17,
+      23
+    ],
+    "PreferredPreauthTypes": [
+      17,
+      16,
+      15,
+      14
+    ],
+    "Proxiable": false,
+    "RDNS": true,
+    "RealmTryDomains": -1,
+    "RenewLifetime": 0,
+    "SafeChecksumType": 8,
+    "TicketLifetime": 36000000000000,
+    "UDPPreferenceLimit": 1465,
+    "VerifyAPReqNofail": false
+  },
+  "Realms": [
+    {
+      "Realm": "TEST.GOKRB5",
+      "AdminServer": [
+        "10.80.88.88:749"
+      ],
+      "DefaultDomain": "test.gokrb5",
+      "KDC": [
+        "10.80.88.88:88",
+        "assume.port.num:88",
+        "some.other.port:1234",
+        "10.80.88.88:88"
+      ],
+      "KPasswdServer": [
+        "10.80.88.88:464"
+      ],
+      "MasterKDC": null
+    },
+    {
+      "Realm": "EXAMPLE.COM",
+      "AdminServer": [
+        "kerberos.example.com"
+      ],
+      "DefaultDomain": "",
+      "KDC": [
+        "kerberos.example.com:88",
+        "kerberos-1.example.com:88"
+      ],
+      "KPasswdServer": [
+        "kerberos.example.com:464"
+      ],
+      "MasterKDC": null
+    },
+    {
+      "Realm": "lowercase.org",
+      "AdminServer": [
+        "kerberos.lowercase.org"
+      ],
+      "DefaultDomain": "",
+      "KDC": [
+        "kerberos.lowercase.org:88"
+      ],
+      "KPasswdServer": [
+        "kerberos.lowercase.org:464"
+      ],
+      "MasterKDC": null
+    }
+  ],
+  "DomainRealm": {
+    ".example.com": "EXAMPLE.COM",
+    ".test.gokrb5": "TEST.GOKRB5",
+    ".testlowercase.org": "lowercase.org",
+    "hostname1.example.com": "EXAMPLE.COM",
+    "hostname2.example.com": "TEST.GOKRB5",
+    "test.gokrb5": "TEST.GOKRB5"
+  }
+}`
 	krb5Conf2 = `
 [logging]
  default = FILE:/var/log/kerberos/krb5libs.log
@@ -528,3 +663,19 @@ func TestResolveRealm(t *testing.T) {
 		})
 	}
 }
+
+func TestJSON(t *testing.T) {
+	t.Parallel()
+	c, err := NewFromString(krb5Conf)
+	if err != nil {
+		t.Fatalf("Error loading config: %v", err)
+	}
+	c.LibDefaults.K5LoginDirectory = "/home/test"
+	j, err := c.JSON()
+	if err != nil {
+		t.Errorf("error marshaling krb config to JSON: %v", err)
+	}
+	assert.Equal(t, krb5ConfJson, j, "krb config marshaled json not as expected")
+
+	t.Log(j)
+}

+ 30 - 9
v8/credentials/credentials.go

@@ -4,6 +4,7 @@ package credentials
 import (
 	"bytes"
 	"encoding/gob"
+	"encoding/json"
 	"time"
 
 	"github.com/hashicorp/go-uuid"
@@ -42,15 +43,15 @@ type marshalCredentials struct {
 	Username        string
 	DisplayName     string
 	Realm           string
-	CName           types.PrincipalName
-	Keytab          *keytab.Keytab
-	Password        string
-	Attributes      map[string]interface{}
+	CName           types.PrincipalName `json:"-"`
+	Keytab          bool
+	Password        bool
+	Attributes      map[string]interface{} `json:"-"`
 	ValidUntil      time.Time
 	Authenticated   bool
 	Human           bool
 	AuthTime        time.Time
-	GroupMembership map[string]bool
+	GroupMembership map[string]bool `json:"-"`
 	SessionID       string
 }
 
@@ -339,8 +340,8 @@ func (c *Credentials) Marshal() ([]byte, error) {
 		DisplayName:     c.displayName,
 		Realm:           c.realm,
 		CName:           c.cname,
-		Keytab:          c.keytab,
-		Password:        c.password,
+		Keytab:          c.HasKeytab(),
+		Password:        c.HasPassword(),
 		Attributes:      c.attributes,
 		ValidUntil:      c.validUntil,
 		Authenticated:   c.authenticated,
@@ -371,8 +372,6 @@ func (c *Credentials) Unmarshal(b []byte) error {
 	c.displayName = mc.DisplayName
 	c.realm = mc.Realm
 	c.cname = mc.CName
-	c.keytab = mc.Keytab
-	c.password = mc.Password
 	c.attributes = mc.Attributes
 	c.validUntil = mc.ValidUntil
 	c.authenticated = mc.Authenticated
@@ -382,3 +381,25 @@ func (c *Credentials) Unmarshal(b []byte) error {
 	c.sessionID = mc.SessionID
 	return nil
 }
+
+// JSON return details of the Credentials in a JSON format.
+func (c *Credentials) JSON() (string, error) {
+	mc := marshalCredentials{
+		Username:      c.username,
+		DisplayName:   c.displayName,
+		Realm:         c.realm,
+		CName:         c.cname,
+		Keytab:        c.HasKeytab(),
+		Password:      c.HasPassword(),
+		ValidUntil:    c.validUntil,
+		Authenticated: c.authenticated,
+		Human:         c.human,
+		AuthTime:      c.authTime,
+		SessionID:     c.sessionID,
+	}
+	b, err := json.MarshalIndent(mc, "", "  ")
+	if err != nil {
+		return "", err
+	}
+	return string(b), nil
+}

+ 11 - 1
v8/keytab/keytab.go

@@ -4,6 +4,7 @@ package keytab
 import (
 	"bytes"
 	"encoding/binary"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -35,7 +36,7 @@ type entry struct {
 
 // Keytab entry principal struct.
 type principal struct {
-	NumComponents int16
+	NumComponents int16 `json:"-"`
 	Realm         string
 	Components    []string
 	NameType      int32
@@ -465,3 +466,12 @@ func isNativeEndianLittle() bool {
 	}
 	return endian
 }
+
+// JSON return information about the keys held in the keytab in a JSON format.
+func (k *Keytab) JSON() (string, error) {
+	b, err := json.MarshalIndent(k, "", "  ")
+	if err != nil {
+		return "", err
+	}
+	return string(b), nil
+}

+ 1 - 1
v8/types/Cryptosystem.go

@@ -18,7 +18,7 @@ type EncryptedData struct {
 // AKA KeyBlock
 type EncryptionKey struct {
 	KeyType  int32  `asn1:"explicit,tag:0"`
-	KeyValue []byte `asn1:"explicit,tag:1"`
+	KeyValue []byte `asn1:"explicit,tag:1" json:"-"`
 }
 
 // Checksum implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.9