Przeglądaj źródła

Version 8

- Introduce v8 using Go modules for dependencies
- Client.Key function takes a kvno for which key to return
- Removed stutter on creating client functions
- Removed stutter on creating config functions
- Removed stutter from krberror New func
- Credentials now has method to get AD additional details
- Credentials can now be marshalled/unmarshalled to support sessions
- Store marshalled credentials bytes in http request context
- Support for server side sessions in SPNEGO
- Return kvno from keytab GetEncryptionKey method
- APREQ verify takes point so calling code can access decrypted parts if needs be
Jonathan Turner 6 lat temu
rodzic
commit
3196640dcd
100 zmienionych plików z 9465 dodań i 474 usunięć
  1. 19 8
      .github/workflows/testing.yml
  2. 85 0
      .github/workflows/testingv8.yml
  3. 8 214
      README.md
  4. 98 0
      USAGE.md
  5. 2 2
      client/ASExchange.go
  6. 26 14
      client/client.go
  7. 10 10
      client/client_ad_integration_test.go
  8. 2 2
      client/client_dns_test.go
  9. 41 41
      client/client_integration_test.go
  10. 1 1
      client/client_test.go
  11. 4 4
      client/session_test.go
  12. 2 2
      config/hosts_test.go
  13. 12 12
      config/krb5conf.go
  14. 3 3
      config/krb5conf_test.go
  15. 94 24
      credentials/credentials.go
  16. 16 2
      credentials/credentials_test.go
  17. 20 18
      examples/example-AD.go
  18. 10 13
      examples/example.go
  19. 2 2
      examples/httpClient.go
  20. 44 4
      examples/httpServer.go
  21. 4 2
      examples/longRunningClient.go
  22. 4 1
      gssapi/gssapi.go
  23. 2 1
      gssapi/gssapi_test.go
  24. 7 6
      keytab/keytab.go
  25. 2 2
      krberror/error.go
  26. 1 1
      messages/KDCRep.go
  27. 3 3
      messages/Ticket.go
  28. 1 1
      pac/pac_type_test.go
  29. 1 2
      service/APExchange.go
  30. 10 10
      service/APExchange_test.go
  31. 3 3
      service/authenticator.go
  32. 1 1
      service/authenticator_test.go
  33. 26 0
      service/settings.go
  34. 98 33
      spnego/http.go
  35. 78 17
      spnego/http_test.go
  36. 5 6
      spnego/krb5Token.go
  37. 1 1
      spnego/krb5Token_test.go
  38. 3 3
      spnego/negotiationToken.go
  39. 5 5
      spnego/spnego.go
  40. 300 0
      v8/README.md
  41. 2 0
      v8/USAGE.md
  42. 86 0
      v8/asn1tools/tools.go
  43. 189 0
      v8/client/ASExchange.go
  44. 103 0
      v8/client/TGSExchange.go
  45. 110 0
      v8/client/cache.go
  46. 241 0
      v8/client/client.go
  47. 190 0
      v8/client/client_ad_integration_test.go
  48. 34 0
      v8/client/client_dns_test.go
  49. 705 0
      v8/client/client_integration_test.go
  50. 20 0
      v8/client/client_test.go
  51. 224 0
      v8/client/network.go
  52. 95 0
      v8/client/passwd.go
  53. 255 0
      v8/client/session.go
  54. 115 0
      v8/client/session_test.go
  55. 69 0
      v8/client/settings.go
  56. 30 0
      v8/config/error.go
  57. 141 0
      v8/config/hosts.go
  58. 91 0
      v8/config/hosts_test.go
  59. 726 0
      v8/config/krb5conf.go
  60. 530 0
      v8/config/krb5conf_test.go
  61. 348 0
      v8/credentials/ccache.go
  62. 181 0
      v8/credentials/ccache_integration_test.go
  63. 131 0
      v8/credentials/ccache_test.go
  64. 384 0
      v8/credentials/credentials.go
  65. 28 0
      v8/credentials/credentials_test.go
  66. 173 0
      v8/crypto/aes128-cts-hmac-sha1-96.go
  67. 45 0
      v8/crypto/aes128-cts-hmac-sha1-96_test.go
  68. 135 0
      v8/crypto/aes128-cts-hmac-sha256-128.go
  69. 148 0
      v8/crypto/aes128-cts-hmac-sha256-128_test.go
  70. 173 0
      v8/crypto/aes256-cts-hmac-sha1-96.go
  71. 45 0
      v8/crypto/aes256-cts-hmac-sha1-96_test.go
  72. 135 0
      v8/crypto/aes256-cts-hmac-sha384-192.go
  73. 147 0
      v8/crypto/aes256-cts-hmac-sha384-192_test.go
  74. 143 0
      v8/crypto/common/common.go
  75. 175 0
      v8/crypto/crypto.go
  76. 174 0
      v8/crypto/des3-cbc-sha1-kd.go
  77. 68 0
      v8/crypto/des3-cbc-sha1-kd_test.go
  78. 29 0
      v8/crypto/etype/etype.go
  79. 135 0
      v8/crypto/rc4-hmac.go
  80. 125 0
      v8/crypto/rfc3961/encryption.go
  81. 178 0
      v8/crypto/rfc3961/keyDerivation.go
  82. 33 0
      v8/crypto/rfc3961/keyDerivation_test.go
  83. 128 0
      v8/crypto/rfc3961/nfold.go
  84. 28 0
      v8/crypto/rfc3961/nfold_test.go
  85. 89 0
      v8/crypto/rfc3962/encryption.go
  86. 58 0
      v8/crypto/rfc3962/keyDerivation.go
  87. 40 0
      v8/crypto/rfc4757/checksum.go
  88. 80 0
      v8/crypto/rfc4757/encryption.go
  89. 55 0
      v8/crypto/rfc4757/keyDerivation.go
  90. 23 0
      v8/crypto/rfc4757/keyDerivation_test.go
  91. 20 0
      v8/crypto/rfc4757/msgtype.go
  92. 128 0
      v8/crypto/rfc8009/encryption.go
  93. 144 0
      v8/crypto/rfc8009/keyDerivation.go
  94. 112 0
      v8/examples/example-AD.go
  95. 88 0
      v8/examples/example.go
  96. 94 0
      v8/examples/httpClient.go
  97. 105 0
      v8/examples/httpServer.go
  98. 78 0
      v8/examples/longRunningClient.go
  99. 15 0
      v8/go.mod
  100. 37 0
      v8/go.sum

+ 19 - 8
.github/workflows/testing.yml

@@ -1,5 +1,11 @@
-name: gokrb5
-on: [push, pull_request]
+name: v7
+on:
+  push:
+    paths-ignore:
+    - 'v[0-9]+/**'
+  pull_request:
+    paths-ignore:
+    - 'v[0-9]+/**'
 
 jobs:
   build:
@@ -27,6 +33,8 @@ jobs:
 
       - name: Test well formatted with gofmt
         run: |
+          # Remove major version sub directories
+          find . -maxdepth 1 -type d -regex '\./v[0-9]+' | xargs -i rm -rf {}
           GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
           test -z $(gofmt -s -d -l -e $GO_FILES | tee /dev/fd/2 | xargs | sed 's/\s//g')
         id: gofmt
@@ -43,7 +51,9 @@ jobs:
         id: goGet
 
       - name: Unit tests
-        run: go test -race ./...
+        run: |
+          cd /home/runner/go/src/gopkg.in/jcmturner/gokrb5.v7
+          go test -race $(go list ./... | grep -E -v '/v[0-9]+' | grep -v /vendor/)
         id: unitTests
 
       - name: Start integration test dependencies
@@ -62,13 +72,13 @@ jobs:
         id: intgTestDeps
 
       - name: Run Examples
-        run: |
-          go run -tags="examples" examples/example.go
+        run: go run -tags="examples" examples/example.go
         id: examples
 
       - name: Tests including integration tests
         run: |
-          go test -race ./...
+          cd /home/runner/go/src/gopkg.in/jcmturner/gokrb5.v7
+          go test -race $(go list ./... | grep -E -v '/v[0-9]+' | grep -v /vendor/)
         env:
           INTEGRATION: 1
           TESTPRIVILEGED: 1
@@ -76,9 +86,10 @@ jobs:
 
       - name: Tests (32bit)
         run: |
-          go test ./...
+          cd /home/runner/go/src/gopkg.in/jcmturner/gokrb5.v7
+          go test $(go list ./... | grep -E -v '/v[0-9]+' | grep -v /vendor/)
         env:
           GOARCH: 386
           INTEGRATION: 1
           TESTPRIVILEGED: 1
-        id: test32
+        id: test32

+ 85 - 0
.github/workflows/testingv8.yml

@@ -0,0 +1,85 @@
+# Name of the workflow needs to match the name of the major version directory
+name: v8
+on:
+  push:
+    paths:
+      - 'v8/**'
+  pull_request:
+    paths:
+      - 'v8/**'
+
+jobs:
+  build:
+    name: Tests
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        go: [ '1.11.x', '1.12.x', '1.13.x' ]
+    env:
+      TEST_KDC_ADDR: 127.0.0.1
+      TEST_HTTP_URL: http://cname.test.gokrb5
+      TEST_HTTP_ADDR: 127.0.0.1
+      DNS_IP: 127.0.88.53
+      DNSUTILS_OVERRIDE_NS: 127.0.88.53:53
+    steps:
+      - name: Set up Go ${{ matrix.go }}
+        uses: actions/setup-go@v1
+        with:
+          go-version: ${{ matrix.go }}
+
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          ref: ${{ github.ref }}
+
+      - name: Test well formatted with gofmt
+        run: |
+          GO_FILES=$(find ${GITHUB_WORKFLOW} -iname '*.go' -type f | grep -v /vendor/)
+          test -z $(gofmt -s -d -l -e $GO_FILES | tee /dev/fd/2 | xargs | sed 's/\s//g')
+        id: gofmt
+
+      - name: Unit tests
+        run: |
+          cd ${GITHUB_WORKFLOW}
+          go test -race ./...
+        id: unitTests
+
+      - name: Start integration test dependencies
+        run: |
+          sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq krb5-user
+          sudo chmod 666 /etc/krb5.conf
+          sudo docker run -d -h ns.test.gokrb5 -v /etc/localtime:/etc/localtime:ro -e "TEST_KDC_ADDR=${TEST_KDC_ADDR}" -e "TEST_HTTP_ADDR=${TEST_HTTP_ADDR}" -p ${DNSUTILS_OVERRIDE_NS}:53 -p ${DNSUTILS_OVERRIDE_NS}:53/udp --name dns jcmturner/gokrb5:dns
+          sudo docker run -d -h kdc.test.gokrb5 -v /etc/localtime:/etc/localtime:ro -p 88:88 -p 88:88/udp -p 464:464 -p 464:464/udp --name krb5kdc jcmturner/gokrb5:kdc-centos-default
+          sudo docker run -d -h kdc.test.gokrb5 -v /etc/localtime:/etc/localtime:ro -p 78:88 -p 78:88/udp --name krb5kdc-old jcmturner/gokrb5:kdc-older
+          sudo docker run -d -h kdc.test.gokrb5 -v /etc/localtime:/etc/localtime:ro -p 98:88 -p 98:88/udp --name krb5kdc-latest jcmturner/gokrb5:kdc-latest
+          sudo docker run -d -h kdc.resdom.gokrb5 -v /etc/localtime:/etc/localtime:ro -p 188:88 -p 188:88/udp --name krb5kdc-resdom jcmturner/gokrb5:kdc-resdom
+          sudo docker run -d -h kdc.test.gokrb5 -v /etc/localtime:/etc/localtime:ro -p 58:88 -p 58:88/udp --name krb5kdc-shorttickets jcmturner/gokrb5:kdc-shorttickets
+          sudo docker run -d --add-host host.test.gokrb5:127.0.0.88 -v /etc/localtime:/etc/localtime:ro -p 80:80 -p 443:443 --name gokrb5-http jcmturner/gokrb5:http
+          sudo sed -i 's/nameserver .*/nameserver '${DNS_IP}'/g' /etc/resolv.conf
+          dig _kerberos._udp.TEST.GOKRB5
+        id: intgTestDeps
+
+      - name: Run Examples
+        run: |
+          cd ${GITHUB_WORKFLOW}
+          go run -tags="examples" examples/example.go
+        id: examples
+
+      - name: Tests including integration tests
+        run: |
+          cd ${GITHUB_WORKFLOW}
+          go test -race ./...
+        env:
+          INTEGRATION: 1
+          TESTPRIVILEGED: 1
+        id: intgTests
+
+      - name: Tests (32bit)
+        run: |
+          cd ${GITHUB_WORKFLOW}
+          go test ./...
+        env:
+          GOARCH: 386
+          INTEGRATION: 1
+          TESTPRIVILEGED: 1
+        id: test32

+ 8 - 214
README.md

@@ -1,9 +1,14 @@
 # gokrb5
-[![Version](https://img.shields.io/github/release/jcmturner/gokrb5.svg)](https://github.com/jcmturner/gokrb5/releases)
 
-[![GoDoc](https://godoc.org/gopkg.in/jcmturner/gokrb5.v7?status.svg)](https://godoc.org/gopkg.in/jcmturner/gokrb5.v7) [![Go Report Card](https://goreportcard.com/badge/gopkg.in/jcmturner/gokrb5.v7)](https://goreportcard.com/report/gopkg.in/jcmturner/gokrb5.v7) 
+It is recommended to use the latest version: [![Version](https://img.shields.io/github/release/jcmturner/gokrb5.svg)](https://github.com/jcmturner/gokrb5/releases)
+
+Development will be focused on the latest major version. New features will only be targeted at this version.
+
+| Versions | Dependency Management | Import Path | Usage | Godoc | Go Report Card |
+|----------|-----------------------|-------------|-------|-------|----------------|
+| [![v8](https://github.com/jcmturner/gokrb5/workflows/v8/badge.svg)](https://github.com/jcmturner/gokrb5/actions?query=workflow%3Av8) | Go modules | import "github.com/jcmturner/gokrb5/v8/{sub-package}" | [![Usage](https://img.shields.io/badge/v8-usage-blue)](https://github.com/jcmturner/gokrb5/blob/master/v8/USAGE.md) | [![GoDoc](https://godoc.org/github.com/jcmturner/gokrb5/v8?status.svg)](https://godoc.org/github.com/jcmturner/gokrb5/v8) | [![Go Report Card](https://goreportcard.com/badge/github.com/jcmturner/gokrb5/v8)](https://goreportcard.com/report/github.com/jcmturner/gokrb5/v8) |
+| [![v7](https://github.com/jcmturner/gokrb5/workflows/v7/badge.svg)](https://github.com/jcmturner/gokrb5/actions?query=workflow%3Av7) | gopkg.in | import "gopkg.in/jcmturner/gokrb5.v7/{sub-package}" | [![Usage](https://img.shields.io/badge/v7-usage-blue)](https://github.com/jcmturner/gokrb5/blob/master/USAGE.md) | [![GoDoc](https://godoc.org/gopkg.in/jcmturner/gokrb5.v7?status.svg)](https://godoc.org/gopkg.in/jcmturner/gokrb5.v7) | [![Go Report Card](https://goreportcard.com/badge/gopkg.in/jcmturner/gokrb5.v7)](https://goreportcard.com/report/gopkg.in/jcmturner/gokrb5.v7) |
 
-[![Build Status](https://github.com/jcmturner/gokrb5/workflows/gokrb5/badge.svg)](https://github.com/jcmturner/gokrb5/actions)
 
 #### Go Version Support
 ![Go version](https://img.shields.io/badge/Go-1.13-brightgreen.svg)
@@ -12,16 +17,6 @@
 
 gokrb5 may work with other versions of Go but they are not tested.
 
-### Go Get
-To get the package, execute:
-```
-go get -d gopkg.in/jcmturner/gokrb5.v7/...
-```
-To import this package, add the following line to your code:
-```go
-import "gopkg.in/jcmturner/gokrb5.v7/<sub package>"
-```
-
 ## Features
 * **Pure Go** - no dependency on external libraries 
 * No platform specific code
@@ -58,207 +53,6 @@ The following is working/tested:
 ## Contributing
 If you are interested in contributing to gokrb5, great! Please read the [contribution guidelines](https://github.com/jcmturner/gokrb5/blob/master/CONTRIBUTING.md).
 
-## Usage
-
----
-
-### Configuration
-The gokrb5 libraries use the same krb5.conf configuration file format as MIT Kerberos, described [here](https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html).
-Config instances can be created by loading from a file path or by passing a string, io.Reader or bufio.Scanner to the relevant method:
-```go
-import "gopkg.in/jcmturner/gokrb5.v7/config"
-cfg, err := config.Load("/path/to/config/file")
-cfg, err := config.NewConfigFromString(krb5Str) //String must have appropriate newline separations
-cfg, err := config.NewConfigFromReader(reader)
-cfg, err := config.NewConfigFromScanner(scanner)
-```
-### Keytab files
-Standard keytab files can be read from a file or from a slice of bytes:
-```go
-import 	"gopkg.in/jcmturner/gokrb5.v7/keytab"
-ktFromFile, err := keytab.Load("/path/to/file.keytab")
-ktFromBytes, err := keytab.Parse(b)
-
-```
-
----
-
-### Kerberos Client
-**Create** a client instance with either a password or a keytab.
-A configuration must also be passed. Additionally optional additional settings can be provided.
-```go
-import 	"gopkg.in/jcmturner/gokrb5.v7/client"
-cl := client.NewClientWithPassword("username", "REALM.COM", "password", cfg)
-cl := client.NewClientWithKeytab("username", "REALM.COM", kt, cfg)
-```
-Optional settings are provided using the functions defined in the ``client/settings.go`` source file.
-
-**Login**:
-```go
-err := cl.Login()
-```
-Kerberos Ticket Granting Tickets (TGT) will be automatically renewed unless the client was created from a CCache.
-
-A client can be **destroyed** with the following method:
-```go
-cl.Destroy()
-```
-
-#### Active Directory KDC and FAST negotiation
-Active Directory does not commonly support FAST negotiation so you will need to disable this on the client.
-If this is the case you will see this error:
-```KDC did not respond appropriately to FAST negotiation```
-To resolve this disable PA-FX-Fast on the client before performing Login().
-This is done with one of the optional client settings as shown below:
-```go
-cl := client.NewClientWithPassword("username", "REALM.COM", "password", cfg, client.DisablePAFXFAST(true))
-```
-
-#### Authenticate to a Service
-
-##### HTTP SPNEGO
-Create the HTTP request object and then create an SPNEGO client and use this to process the request with methods that 
-are the same as on a HTTP client.
-If nil is passed as the HTTP client when creating the SPNEGO client the http.DefaultClient is used.
-When creating the SPNEGO client pass the Service Principal Name (SPN) or auto generate the SPN from the request 
-object by passing a null string "".
-```go
-r, _ := http.NewRequest("GET", "http://host.test.gokrb5/index.html", nil)
-spnegoCl := spnego.NewClient(cl, nil, "")
-resp, err := spnegoCl.Do(r)
-```
-
-##### Generic Kerberos Client
-To authenticate to a service a client will need to request a service ticket for a Service Principal Name (SPN) and form into an AP_REQ message along with an authenticator encrypted with the session key that was delivered from the KDC along with the service ticket.
-
-The steps below outline how to do this.
-* Get the service ticket and session key for the service the client is authenticating to.
-The following method will use the client's cache either returning a valid cached ticket, renewing a cached ticket with the KDC or requesting a new ticket from the KDC.
-Therefore the GetServiceTicket method can be continually used for the most efficient interaction with the KDC.
-```go
-tkt, key, err := cl.GetServiceTicket("HTTP/host.test.gokrb5")
-```
-
-The steps after this will be specific to the application protocol but it will likely involve a client/server Authentication Protocol exchange (AP exchange).
-This will involve these steps:
-
-* Generate a new Authenticator and generate a sequence number and subkey:
-```go
-auth, _ := types.NewAuthenticator(cl.Credentials.Realm, cl.Credentials.CName)
-etype, _ := crypto.GetEtype(key.KeyType)
-auth.GenerateSeqNumberAndSubKey(key.KeyType, etype.GetKeyByteSize())
-```
-* Set the checksum on the authenticator
-The checksum is an application specific value. Set as follows:
-```go
-auth.Cksum = types.Checksum{
-		CksumType: checksumIDint,
-		Checksum:  checksumBytesSlice,
-	}
-```
-* Create the AP_REQ:
-```go
-APReq, err := messages.NewAPReq(tkt, key, auth)
-```
-
-Now send the AP_REQ to the service. How this is done will be specific to the application use case.
-
-#### Changing a Client Password
-This feature uses the Microsoft Kerberos Password Change protocol (RFC 3244). 
-This is implemented in Microsoft Active Directory and in MIT krb5kdc as of version 1.7.
-Typically the kpasswd server listens on port 464.
-
-Below is example code for how to use this feature:
-```go
-cfg, err := config.Load("/path/to/config/file")
-if err != nil {
-	panic(err.Error())
-}
-kt, err := keytab.Load("/path/to/file.keytab")
-if err != nil {
-	panic(err.Error())
-}
-cl := client.NewClientWithKeytab("username", "REALM.COM", kt)
-cl.WithConfig(cfg)
-
-ok, err := cl.ChangePasswd("newpassword")
-if err != nil {
-	panic(err.Error())
-}
-if !ok {
-	panic("failed to change password")
-}
-```
-
-The client kerberos config (krb5.conf) will need to have either the kpassd_server or admin_server defined in the relevant [realms] section.
-For example:
-```
-REALM.COM = {
-  kdc = 127.0.0.1:88
-  kpasswd_server = 127.0.0.1:464
-  default_domain = realm.com
- }
-```
-See https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html#realms for more information.
-
----
-
-### Kerberised Service
-
-#### SPNEGO/Kerberos HTTP Service
-A HTTP handler wrapper can be used to implement Kerberos SPNEGO authentication for web services.
-To configure the wrapper the keytab for the SPN and a Logger are required:
-```go
-kt, err := keytab.Load("/path/to/file.keytab")
-l := log.New(os.Stderr, "GOKRB5 Service: ", log.Ldate|log.Ltime|log.Lshortfile)
-```
-Create a handler function of the application's handling method (apphandler in the example below):
-```go
-h := http.HandlerFunc(apphandler)
-```
-Configure the HTTP handler:
-```go
-http.Handler("/", spnego.SPNEGOKRB5Authenticate(h, &kt, service.Logger(l)))
-```
-The handler to be wrapped and the keytab are required arguments. 
-Additional optional settings can be provided, such as the logger shown above.
-
-Another example of optional settings may be that when using Active Directory where the SPN is mapped to a user account 
-the keytab may contain an entry for this user account. In this case this should be specified as below with the ``KeytabPrincipal``:
-```go
-http.Handler("/", spnego.SPNEGOKRB5Authenticate(h, &kt, service.Logger(l), service.KeytabPrincipal(pn)))
-```
-
-If authentication succeeds then the request's context will have the following values added so they can be accessed within the application's handler:
-* spnego.CTXKeyAuthenticated - Boolean indicating if the user is authenticated. Use of this value should also handle that this value may not be set and should assume "false" in that case.
-* spnego.CTXKeyCredentials - The authenticated user's credentials.
-If Microsoft Active Directory is used as the KDC then additional ADCredentials are available in the credentials.Attributes map under the key credentials.AttributeKeyADCredentials. For example the SIDs of the users group membership are available and can be used by your application for authorization.
-
-Access the credentials within your application:
-```go
-ctx := r.Context()
-if validuser, ok := ctx.Value(spnego.CTXKeyAuthenticated).(bool); ok && validuser {
-        if creds, ok := ctx.Value(spnego.CTXKeyCredentials).(goidentity.Identity); ok {
-                if ADCreds, ok := creds.Attributes()[credentials.AttributeKeyADCredentials].(credentials.ADCredentials); ok {
-                        // Now access the fields of the ADCredentials struct. For example:
-                        groupSids := ADCreds.GroupMembershipSIDs
-                }
-        } 
-
-}
-```
-
-#### Generic Kerberised Service - Validating Client Details
-To validate the AP_REQ sent by the client on the service side call this method:
-```go
-import 	"gopkg.in/jcmturner/gokrb5.v7/service"
-s := service.NewSettings(&kt) // kt is a keytab and optional settings can also be provided.
-if ok, creds, err := service.VerifyAPREQ(APReq, s); ok {
-        // Perform application specific actions
-        // creds object has details about the client identity
-}
-```
-
 ---
 
 ## References

+ 98 - 0
USAGE.md

@@ -0,0 +1,98 @@
+# gokrb5
+
+## Versions
+
+It is recommended to use the latest version.
+
+[![Version](https://img.shields.io/github/release/jcmturner/gokrb5.svg)](https://github.com/jcmturner/gokrb5/releases)
+
+| Versions | Dependency Management | Import Path | Usage | Godoc | Go Report Card |
+|----------|-----------------------|-------------|-------|-------|----------------|
+| v7 | gopkg.in | import "gopkg.in/jcmturner/gokrb5.v7/<sub package>" | https://github.com/jcmturner/gokrb5/blob/master/USAGE.md | [![GoDoc](https://godoc.org/gopkg.in/jcmturner/gokrb5.v7?status.svg)](https://godoc.org/gopkg.in/jcmturner/gokrb5.v7) | [![Go Report Card](https://goreportcard.com/badge/gopkg.in/jcmturner/gokrb5.v7)](https://goreportcard.com/report/gopkg.in/jcmturner/gokrb5.v7) |
+| v8 | Go modules | import "github.com/jcmturner/gokrb5/v8/<sub package>" | https://github.com/jcmturner/gokrb5/blob/master/v8/USAGE.md | [![GoDoc](https://godoc.org/github.com/jcmturner/gokrb5/v8?status.svg)](https://godoc.org/github.com/jcmturner/gokrb5/v8) | [![Go Report Card](https://goreportcard.com/badge/github.com/jcmturner/gokrb5/v8)](https://goreportcard.com/report/github.com/jcmturner/gokrb5/v8) |
+
+
+[![Build Status](https://github.com/jcmturner/gokrb5/workflows/gokrb5/badge.svg)](https://github.com/jcmturner/gokrb5/actions)
+
+#### Go Version Support
+![Go version](https://img.shields.io/badge/Go-1.13-brightgreen.svg)
+![Go version](https://img.shields.io/badge/Go-1.12-brightgreen.svg)
+![Go version](https://img.shields.io/badge/Go-1.11-brightgreen.svg)
+
+gokrb5 may work with other versions of Go but they are not tested.
+
+## Features
+* **Pure Go** - no dependency on external libraries 
+* No platform specific code
+* Server Side
+  * HTTP handler wrapper implements SPNEGO Kerberos authentication
+  * HTTP handler wrapper decodes Microsoft AD PAC authorization data
+* Client Side
+  * Client that can authenticate to an SPNEGO Kerberos authenticated web service
+  * Ability to change client's password
+* General
+  * Kerberos libraries for custom integration
+  * Parsing Keytab files
+  * Parsing krb5.conf files
+  * Parsing client credentials cache files such as `/tmp/krb5cc_$(id -u $(whoami))`
+
+#### Implemented Encryption & Checksum Types
+
+| Implementation | Encryption ID | Checksum ID | RFC |
+|-------|-------------|------------|------|
+| des3-cbc-sha1-kd | 16 | 12 | 3961 |
+| aes128-cts-hmac-sha1-96 | 17 | 15 | 3962 |
+| aes256-cts-hmac-sha1-96 | 18 | 16 | 3962 |
+| aes128-cts-hmac-sha256-128 | 19 | 19 | 8009 |
+| aes256-cts-hmac-sha384-192 | 20 | 20 | 8009 |
+| rc4-hmac | 23 | -138 | 4757 |
+
+
+The following is working/tested:
+* Tested against MIT KDC (1.6.3 is the oldest version tested against) and Microsoft Active Directory (Windows 2008 R2)
+* Tested against a KDC that supports PA-FX-FAST.
+* Tested against users that have pre-authentication required using PA-ENC-TIMESTAMP.
+* Microsoft PAC Authorization Data is processed and exposed in the HTTP request context. Available if Microsoft Active Directory is used as the KDC.
+
+## Contributing
+If you are interested in contributing to gokrb5, great! Please read the [contribution guidelines](https://github.com/jcmturner/gokrb5/blob/master/CONTRIBUTING.md).
+
+---
+
+## References
+* [RFC 3244 Microsoft Windows 2000 Kerberos Change Password and Set Password Protocols](https://tools.ietf.org/html/rfc3244)
+* [RFC 4120 The Kerberos Network Authentication Service (V5)](https://tools.ietf.org/html/rfc4120)
+* [RFC 3961 Encryption and Checksum Specifications for Kerberos 5](https://tools.ietf.org/html/rfc3961)
+* [RFC 3962 Advanced Encryption Standard (AES) Encryption for Kerberos 5](https://tools.ietf.org/html/rfc3962)
+* [RFC 4121 The Kerberos Version 5 GSS-API Mechanism](https://tools.ietf.org/html/rfc4121)
+* [RFC 4178 The Simple and Protected Generic Security Service Application Program Interface (GSS-API) Negotiation Mechanism](https://tools.ietf.org/html/rfc4178.html)
+* [RFC 4559 SPNEGO-based Kerberos and NTLM HTTP Authentication in Microsoft Windows](https://tools.ietf.org/html/rfc4559.html)
+* [RFC 4757 The RC4-HMAC Kerberos Encryption Types Used by Microsoft Windows](https://tools.ietf.org/html/rfc4757)
+* [RFC 6806 Kerberos Principal Name Canonicalization and Cross-Realm Referrals](https://tools.ietf.org/html/rfc6806.html)
+* [RFC 6113 A Generalized Framework for Kerberos Pre-Authentication](https://tools.ietf.org/html/rfc6113.html)
+* [RFC 8009 AES Encryption with HMAC-SHA2 for Kerberos 5](https://tools.ietf.org/html/rfc8009)
+* [IANA Assigned Kerberos Numbers](http://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xhtml)
+* [HTTP-Based Cross-Platform Authentication by Using the Negotiate Protocol - Part 1](https://msdn.microsoft.com/en-us/library/ms995329.aspx)
+* [HTTP-Based Cross-Platform Authentication by Using the Negotiate Protocol - Part 2](https://msdn.microsoft.com/en-us/library/ms995330.aspx)
+* [Microsoft PAC Validation](https://blogs.msdn.microsoft.com/openspecification/2009/04/24/understanding-microsoft-kerberos-pac-validation/)
+* [Microsoft Kerberos Protocol Extensions](https://msdn.microsoft.com/en-us/library/cc233855.aspx)
+* [Windows Data Types](https://msdn.microsoft.com/en-us/library/cc230273.aspx)
+
+### Useful Links
+* https://en.wikipedia.org/wiki/Ciphertext_stealing#CBC_ciphertext_stealing
+
+## Thanks
+* Greg Hudson from the MIT Consortium for Kerberos and Internet Trust for providing useful advice.
+
+## Contributing
+Thank you for your interest in contributing to gokrb5 please read the 
+[contribution guide](https://github.com/jcmturner/gokrb5/blob/master/CONTRIBUTING.md) as it should help you get started.
+
+## Known Issues
+| Issue | Worked around? | References |
+|-------|-------------|------------|
+| The Go standard library's encoding/asn1 package cannot unmarshal into slice of asn1.RawValue | Yes | https://github.com/golang/go/issues/17321 |
+| The Go standard library's encoding/asn1 package cannot marshal into a GeneralString | Yes - using https://github.com/jcmturner/gofork/tree/master/encoding/asn1 | https://github.com/golang/go/issues/18832 |
+| The Go standard library's encoding/asn1 package cannot marshal into slice of strings and pass stringtype parameter tags to members | Yes - using https://github.com/jcmturner/gofork/tree/master/encoding/asn1 | https://github.com/golang/go/issues/18834 |
+| The Go standard library's encoding/asn1 package cannot marshal with application tags | Yes | |
+| The Go standard library's x/crypto/pbkdf2.Key function uses the int type for iteraction count limiting meaning the 4294967296 count specified in https://tools.ietf.org/html/rfc3962 section 4 cannot be met on 32bit systems | Yes - using https://github.com/jcmturner/gofork/tree/master/x/crypto/pbkdf2 | https://go-review.googlesource.com/c/crypto/+/85535 |

+ 2 - 2
client/ASExchange.go

@@ -97,7 +97,7 @@ func setPAData(cl *Client, krberr *messages.KRBError, ASReq *messages.ASReq) err
 			if err != nil {
 				return krberror.Errorf(err, krberror.EncryptingError, "error getting etype for pre-auth encryption")
 			}
-			key, err = cl.Key(et, nil)
+			key, _, err = cl.Key(et, 0, nil)
 			if err != nil {
 				return krberror.Errorf(err, krberror.EncryptingError, "error getting key from credentials")
 			}
@@ -108,7 +108,7 @@ func setPAData(cl *Client, krberr *messages.KRBError, ASReq *messages.ASReq) err
 				return krberror.Errorf(err, krberror.EncryptingError, "error getting etype for pre-auth encryption")
 			}
 			cl.settings.preAuthEType = et.GetETypeID() // Set the etype that has been defined for potential future use
-			key, err = cl.Key(et, krberr)
+			key, _, err = cl.Key(et, 0, krberr)
 			if err != nil {
 				return krberror.Errorf(err, krberror.EncryptingError, "error getting key from credentials")
 			}

+ 26 - 14
client/client.go

@@ -27,9 +27,9 @@ type Client struct {
 	cache       *Cache
 }
 
-// NewClientWithPassword creates a new client from a password credential.
+// NewWithPassword creates a new client from a password credential.
 // Set the realm to empty string to use the default realm from config.
-func NewClientWithPassword(username, realm, password string, krb5conf *config.Config, settings ...func(*Settings)) *Client {
+func NewWithPassword(username, realm, password string, krb5conf *config.Config, settings ...func(*Settings)) *Client {
 	creds := credentials.New(username, realm)
 	return &Client{
 		Credentials: creds.WithPassword(password),
@@ -42,8 +42,8 @@ func NewClientWithPassword(username, realm, password string, krb5conf *config.Co
 	}
 }
 
-// NewClientWithKeytab creates a new client from a keytab credential.
-func NewClientWithKeytab(username, realm string, kt *keytab.Keytab, krb5conf *config.Config, settings ...func(*Settings)) *Client {
+// NewWithKeytab creates a new client from a keytab credential.
+func NewWithKeytab(username, realm string, kt *keytab.Keytab, krb5conf *config.Config, settings ...func(*Settings)) *Client {
 	creds := credentials.New(username, realm)
 	return &Client{
 		Credentials: creds.WithKeytab(kt),
@@ -56,10 +56,10 @@ func NewClientWithKeytab(username, realm string, kt *keytab.Keytab, krb5conf *co
 	}
 }
 
-// NewClientFromCCache create a client from a populated client cache.
+// NewFromCCache create a client from a populated client cache.
 //
 // WARNING: A client created from CCache does not automatically renew TGTs and a failure will occur after the TGT expires.
-func NewClientFromCCache(c *credentials.CCache, krb5conf *config.Config, settings ...func(*Settings)) (*Client, error) {
+func NewFromCCache(c *credentials.CCache, krb5conf *config.Config, settings ...func(*Settings)) (*Client, error) {
 	cl := &Client{
 		Credentials: c.GetClientCredentials(),
 		Config:      krb5conf,
@@ -108,28 +108,28 @@ func NewClientFromCCache(c *credentials.CCache, krb5conf *config.Config, setting
 	return cl, nil
 }
 
-// Key returns the client's encryption key for the specified encryption type.
+// Key returns the client's encryption key for the specified encryption type and its kvno (kvno of zero will find latest).
 // The key can be retrieved either from the keytab or generated from the client's password.
 // If the client has both a keytab and a password defined the keytab is favoured as the source for the key
 // A KRBError can be passed in the event the KDC returns one of type KDC_ERR_PREAUTH_REQUIRED and is required to derive
 // the key for pre-authentication from the client's password. If a KRBError is not available, pass nil to this argument.
-func (cl *Client) Key(etype etype.EType, krberr *messages.KRBError) (types.EncryptionKey, error) {
+func (cl *Client) Key(etype etype.EType, kvno int, krberr *messages.KRBError) (types.EncryptionKey, int, error) {
 	if cl.Credentials.HasKeytab() && etype != nil {
-		return cl.Credentials.Keytab().GetEncryptionKey(cl.Credentials.CName(), cl.Credentials.Domain(), 0, etype.GetETypeID())
+		return cl.Credentials.Keytab().GetEncryptionKey(cl.Credentials.CName(), cl.Credentials.Domain(), kvno, etype.GetETypeID())
 	} else if cl.Credentials.HasPassword() {
 		if krberr != nil && krberr.ErrorCode == errorcode.KDC_ERR_PREAUTH_REQUIRED {
 			var pas types.PADataSequence
 			err := pas.Unmarshal(krberr.EData)
 			if err != nil {
-				return types.EncryptionKey{}, fmt.Errorf("could not get PAData from KRBError to generate key from password: %v", err)
+				return types.EncryptionKey{}, 0, fmt.Errorf("could not get PAData from KRBError to generate key from password: %v", err)
 			}
 			key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), krberr.CName, krberr.CRealm, etype.GetETypeID(), pas)
-			return key, err
+			return key, 0, err
 		}
 		key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), cl.Credentials.CName(), cl.Credentials.Domain(), etype.GetETypeID(), types.PADataSequence{})
-		return key, err
+		return key, 0, err
 	}
-	return types.EncryptionKey{}, errors.New("credential has neither keytab or password to generate key")
+	return types.EncryptionKey{}, 0, errors.New("credential has neither keytab or password to generate key")
 }
 
 // IsConfigured indicates if the client has the values required set.
@@ -171,7 +171,7 @@ func (cl *Client) Login() error {
 			return krberror.Errorf(err, krberror.KRBMsgError, "no user credentials available and error getting any existing session")
 		}
 		if time.Now().UTC().After(endTime) {
-			return krberror.NewKrberror(krberror.KRBMsgError, "cannot login, no user credentials available and no valid existing session")
+			return krberror.New(krberror.KRBMsgError, "cannot login, no user credentials available and no valid existing session")
 		}
 		// no credentials but there is a session with tgt already
 		return nil
@@ -188,6 +188,18 @@ func (cl *Client) Login() error {
 	return nil
 }
 
+// AffirmLogin will only perform an AS exchange with the KDC if the client does not already have a TGT.
+func (cl *Client) AffirmLogin() error {
+	_, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
+	if err != nil || time.Now().UTC().After(endTime) {
+		err := cl.Login()
+		if err != nil {
+			return fmt.Errorf("could not get valid TGT for client's realm: %v", err)
+		}
+	}
+	return nil
+}
+
 // realmLogin obtains or renews a TGT and establishes a session for the realm specified.
 func (cl *Client) realmLogin(realm string) error {
 	if realm == cl.Credentials.Domain() {

+ 10 - 10
client/client_ad_integration_test.go

@@ -23,9 +23,9 @@ func TestClient_SuccessfulLogin_AD(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	c.Realms[0].KDC = []string{testdata.TEST_KDC_AD}
-	cl := NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {
@@ -39,9 +39,9 @@ func TestClient_GetServiceTicket_AD(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	c.Realms[0].KDC = []string{testdata.TEST_KDC_AD}
-	cl := NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {
@@ -80,10 +80,10 @@ func TestClient_SuccessfulLogin_AD_TRUST_USER_DOMAIN(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_USERKRB5_AD_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	c.Realms[0].KDC = []string{testdata.TEST_KDC_AD_TRUST_USER_DOMAIN}
 	c.LibDefaults.DefaultRealm = "USER.GOKRB5"
-	cl := NewClientWithKeytab("testuser1", "USER.GOKRB5", kt, c, DisablePAFXFAST(true))
+	cl := NewWithKeytab("testuser1", "USER.GOKRB5", kt, c, DisablePAFXFAST(true))
 
 	err := cl.Login()
 	if err != nil {
@@ -97,7 +97,7 @@ func TestClient_GetServiceTicket_AD_TRUST_USER_DOMAIN(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_USERKRB5_AD_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	c.Realms[0].KDC = []string{testdata.TEST_KDC_AD_TRUST_USER_DOMAIN}
 	c.LibDefaults.DefaultRealm = "USER.GOKRB5"
 	c.LibDefaults.Canonicalize = true
@@ -105,7 +105,7 @@ func TestClient_GetServiceTicket_AD_TRUST_USER_DOMAIN(t *testing.T) {
 	c.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.ETypesByName["rc4-hmac"]}
 	c.LibDefaults.DefaultTGSEnctypes = []string{"rc4-hmac"}
 	c.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.ETypesByName["rc4-hmac"]}
-	cl := NewClientWithKeytab("testuser1", "USER.GOKRB5", kt, c, DisablePAFXFAST(true))
+	cl := NewWithKeytab("testuser1", "USER.GOKRB5", kt, c, DisablePAFXFAST(true))
 
 	err := cl.Login()
 
@@ -146,7 +146,7 @@ func TestClient_GetServiceTicket_AD_USER_DOMAIN(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_USERKRB5_AD_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	c.Realms[0].KDC = []string{testdata.TEST_KDC_AD_TRUST_USER_DOMAIN}
 	c.LibDefaults.DefaultRealm = "USER.GOKRB5"
 	c.LibDefaults.Canonicalize = true
@@ -154,7 +154,7 @@ func TestClient_GetServiceTicket_AD_USER_DOMAIN(t *testing.T) {
 	c.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.ETypesByName["rc4-hmac"]}
 	c.LibDefaults.DefaultTGSEnctypes = []string{"rc4-hmac"}
 	c.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.ETypesByName["rc4-hmac"]}
-	cl := NewClientWithKeytab("testuser1", "USER.GOKRB5", kt, c, DisablePAFXFAST(true))
+	cl := NewWithKeytab("testuser1", "USER.GOKRB5", kt, c, DisablePAFXFAST(true))
 
 	err := cl.Login()
 

+ 2 - 2
client/client_dns_test.go

@@ -16,7 +16,7 @@ func TestClient_Login_DNSKDCs(t *testing.T) {
 	//if ns == "" {
 	//	os.Setenv("DNSUTILS_OVERRIDE_NS", testdata.TEST_NS)
 	//}
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	// Set to lookup KDCs in DNS
 	c.LibDefaults.DNSLookupKDC = true
 	//Blank out the KDCs to ensure they are not being used
@@ -25,7 +25,7 @@ func TestClient_Login_DNSKDCs(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	cl := NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {

+ 41 - 41
client/client_integration_test.go

@@ -37,7 +37,7 @@ func TestClient_SuccessfulLogin_Keytab(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	var tests = []string{
 		testdata.TEST_KDC,
 		testdata.TEST_KDC_OLD,
@@ -45,7 +45,7 @@ func TestClient_SuccessfulLogin_Keytab(t *testing.T) {
 	}
 	for _, tst := range tests {
 		c.Realms[0].KDC = []string{addr + ":" + tst}
-		cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+		cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 		err := cl.Login()
 		if err != nil {
@@ -61,7 +61,7 @@ func TestClient_SuccessfulLogin_Password(t *testing.T) {
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	var tests = []string{
 		testdata.TEST_KDC,
 		testdata.TEST_KDC_OLD,
@@ -69,7 +69,7 @@ func TestClient_SuccessfulLogin_Password(t *testing.T) {
 	}
 	for _, tst := range tests {
 		c.Realms[0].KDC = []string{addr + ":" + tst}
-		cl := client.NewClientWithPassword("testuser1", "TEST.GOKRB5", "passwordvalue", c)
+		cl := client.NewWithPassword("testuser1", "TEST.GOKRB5", "passwordvalue", c)
 
 		err := cl.Login()
 		if err != nil {
@@ -84,14 +84,14 @@ func TestClient_SuccessfulLogin_TCPOnly(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
 	c.LibDefaults.UDPPreferenceLimit = 1
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {
@@ -105,7 +105,7 @@ func TestClient_ASExchange_TGSExchange_EncTypes_Keytab(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
@@ -124,7 +124,7 @@ func TestClient_ASExchange_TGSExchange_EncTypes_Keytab(t *testing.T) {
 		c.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.ETypesByName[tst]}
 		c.LibDefaults.DefaultTGSEnctypes = []string{tst}
 		c.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.ETypesByName[tst]}
-		cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+		cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 		err := cl.Login()
 		if err != nil {
@@ -142,7 +142,7 @@ func TestClient_ASExchange_TGSExchange_EncTypes_Keytab(t *testing.T) {
 func TestClient_ASExchange_TGSExchange_EncTypes_Password(t *testing.T) {
 	test.Integration(t)
 
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
@@ -161,7 +161,7 @@ func TestClient_ASExchange_TGSExchange_EncTypes_Password(t *testing.T) {
 		c.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.ETypesByName[tst]}
 		c.LibDefaults.DefaultTGSEnctypes = []string{tst}
 		c.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.ETypesByName[tst]}
-		cl := client.NewClientWithPassword("testuser1", "TEST.GOKRB5", "passwordvalue", c)
+		cl := client.NewWithPassword("testuser1", "TEST.GOKRB5", "passwordvalue", c)
 
 		err := cl.Login()
 		if err != nil {
@@ -182,13 +182,13 @@ func TestClient_FailedLogin(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_WRONGPASSWD)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err == nil {
@@ -202,13 +202,13 @@ func TestClient_SuccessfulLogin_UserRequiringPreAuth(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER2_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
-	cl := client.NewClientWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {
@@ -222,14 +222,14 @@ func TestClient_SuccessfulLogin_UserRequiringPreAuth_TCPOnly(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER2_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
 	c.LibDefaults.UDPPreferenceLimit = 1
-	cl := client.NewClientWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {
@@ -243,9 +243,9 @@ func TestClient_NetworkTimeout(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	c.Realms[0].KDC = []string{testdata.TEST_KDC_BADADDR + ":88"}
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err == nil {
@@ -259,13 +259,13 @@ func TestClient_GetServiceTicket(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {
@@ -294,13 +294,13 @@ func TestClient_GetServiceTicket_InvalidSPN(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {
@@ -318,13 +318,13 @@ func TestClient_GetServiceTicket_OlderKDC(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC_OLD}
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {
@@ -345,13 +345,13 @@ func TestMultiThreadedClientUse(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	var wg sync.WaitGroup
 	wg.Add(5)
@@ -407,7 +407,7 @@ func spnegoGet(cl *client.Client) error {
 	return nil
 }
 
-func TestNewClientFromCCache(t *testing.T) {
+func TestNewFromCCache(t *testing.T) {
 	test.Integration(t)
 
 	b, err := hex.DecodeString(testdata.CCACHE_TEST)
@@ -419,13 +419,13 @@ func TestNewClientFromCCache(t *testing.T) {
 	if err != nil {
 		t.Fatal("error getting test CCache")
 	}
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
-	cl, err := client.NewClientFromCCache(cc, c)
+	cl, err := client.NewFromCCache(cc, c)
 	if err != nil {
 		t.Fatalf("error creating client from CCache: %v", err)
 	}
@@ -442,7 +442,7 @@ func TestClient_GetServiceTicket_Trusted_Resource_Domain(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
@@ -458,7 +458,7 @@ func TestClient_GetServiceTicket_Trusted_Resource_Domain(t *testing.T) {
 	}
 
 	c.LibDefaults.DefaultRealm = "TEST.GOKRB5"
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 	c.LibDefaults.DefaultTktEnctypes = []string{"aes256-cts-hmac-sha1-96"}
 	c.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.ETypesByName["aes256-cts-hmac-sha1-96"]}
 	c.LibDefaults.DefaultTGSEnctypes = []string{"aes256-cts-hmac-sha1-96"}
@@ -559,13 +559,13 @@ func TestGetServiceTicketFromCCacheTGT(t *testing.T) {
 	if err != nil {
 		t.Errorf("error loading CCache: %v", err)
 	}
-	cfg, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	cfg, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	cfg.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
-	cl, err := client.NewClientFromCCache(c, cfg)
+	cl, err := client.NewFromCCache(c, cfg)
 	if err != nil {
 		t.Fatalf("error generating client from ccache: %v", err)
 	}
@@ -616,8 +616,8 @@ func TestGetServiceTicketFromCCacheWithoutKDC(t *testing.T) {
 	if err != nil {
 		t.Errorf("error loading CCache: %v", err)
 	}
-	cfg, _ := config.NewConfigFromString("...")
-	cl, err := client.NewClientFromCCache(c, cfg)
+	cfg, _ := config.NewFromString("...")
+	cl, err := client.NewFromCCache(c, cfg)
 	if err != nil {
 		t.Fatalf("error generating client from ccache: %v", err)
 	}
@@ -643,14 +643,14 @@ func TestClient_ChangePasswd(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
 	c.Realms[0].KPasswdServer = []string{addr + ":464"}
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	ok, err := cl.ChangePasswd("newpassword")
 	if err != nil {
@@ -658,14 +658,14 @@ func TestClient_ChangePasswd(t *testing.T) {
 	}
 	assert.True(t, ok, "password was not changed")
 
-	cl = client.NewClientWithPassword("testuser1", "TEST.GOKRB5", "newpassword", c)
+	cl = client.NewWithPassword("testuser1", "TEST.GOKRB5", "newpassword", c)
 	ok, err = cl.ChangePasswd(testdata.TESTUSER1_PASSWORD)
 	if err != nil {
 		t.Fatalf("error changing password: %v", err)
 	}
 	assert.True(t, ok, "password was not changed back")
 
-	cl = client.NewClientWithPassword("testuser1", "TEST.GOKRB5", testdata.TESTUSER1_PASSWORD, c)
+	cl = client.NewWithPassword("testuser1", "TEST.GOKRB5", testdata.TESTUSER1_PASSWORD, c)
 	err = cl.Login()
 	if err != nil {
 		t.Fatalf("Could not log back in after reverting password: %v", err)
@@ -682,9 +682,9 @@ func TestClient_Destroy(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC_SHORTTICKETS}
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {

+ 1 - 1
client/client_test.go

@@ -10,7 +10,7 @@ import (
 func TestAssumePreauthentication(t *testing.T) {
 	t.Parallel()
 
-	cl := NewClientWithKeytab("username", "REALM", &keytab.Keytab{}, &config.Config{}, AssumePreAuthentication(true))
+	cl := NewWithKeytab("username", "REALM", &keytab.Keytab{}, &config.Config{}, AssumePreAuthentication(true))
 	if !cl.settings.assumePreAuthentication {
 		t.Fatal("assumePreAuthentication should be true")
 	}

+ 4 - 4
client/session_test.go

@@ -24,13 +24,13 @@ func TestMultiThreadedClientSession(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
-	cl := NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 	err := cl.Login()
 	if err != nil {
 		t.Fatalf("failed to log in: %v", err)
@@ -78,10 +78,10 @@ func TestClient_AutoRenew_Goroutine(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER2_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC_SHORTTICKETS}
 	c.LibDefaults.PreferredPreauthTypes = []int{int(etypeID.DES3_CBC_SHA1_KD)} // a preauth etype the KDC does not support. Test this does not cause renewal to fail.
-	cl := NewClientWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
+	cl := NewWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
 
 	err := cl.Login()
 	if err != nil {

+ 2 - 2
config/hosts_test.go

@@ -23,7 +23,7 @@ func TestConfig_GetKDCsUsesConfiguredKDC(t *testing.T) {
  }
 `
 
-	c, err := NewConfigFromString(krb5ConfWithKDCAndDNSLookupKDC)
+	c, err := NewFromString(krb5ConfWithKDCAndDNSLookupKDC)
 	if err != nil {
 		t.Fatalf("Error loading config: %v", err)
 	}
@@ -43,7 +43,7 @@ func TestConfig_GetKDCsUsesConfiguredKDC(t *testing.T) {
 func TestResolveKDC(t *testing.T) {
 	test.Privileged(t)
 
-	c, err := NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, err := NewFromString(testdata.TEST_KRB5CONF)
 	if err != nil {
 		t.Fatal(err)
 	}

+ 12 - 12
config/krb5conf.go

@@ -32,8 +32,8 @@ type Config struct {
 // WeakETypeList is a list of encryption types that have been deemed weak.
 const WeakETypeList = "des-cbc-crc des-cbc-md4 des-cbc-md5 des-cbc-raw des3-cbc-raw des-hmac-sha1 arcfour-hmac-exp rc4-hmac-exp arcfour-hmac-md5-exp des"
 
-// NewConfig creates a new config struct instance.
-func NewConfig() *Config {
+// New creates a new config struct instance.
+func New() *Config {
 	d := make(DomainRealm)
 	return &Config{
 		LibDefaults: newLibDefaults(),
@@ -523,24 +523,24 @@ func Load(cfgPath string) (*Config, error) {
 	}
 	defer fh.Close()
 	scanner := bufio.NewScanner(fh)
-	return NewConfigFromScanner(scanner)
+	return NewFromScanner(scanner)
 }
 
-// NewConfigFromString creates a new Config struct from a string.
-func NewConfigFromString(s string) (*Config, error) {
+// NewFromString creates a new Config struct from a string.
+func NewFromString(s string) (*Config, error) {
 	reader := strings.NewReader(s)
-	return NewConfigFromReader(reader)
+	return NewFromReader(reader)
 }
 
-// NewConfigFromReader creates a new Config struct from an io.Reader.
-func NewConfigFromReader(r io.Reader) (*Config, error) {
+// NewFromReader creates a new Config struct from an io.Reader.
+func NewFromReader(r io.Reader) (*Config, error) {
 	scanner := bufio.NewScanner(r)
-	return NewConfigFromScanner(scanner)
+	return NewFromScanner(scanner)
 }
 
-// NewConfigFromScanner creates a new Config struct from a bufio.Scanner.
-func NewConfigFromScanner(scanner *bufio.Scanner) (*Config, error) {
-	c := NewConfig()
+// NewFromScanner creates a new Config struct from a bufio.Scanner.
+func NewFromScanner(scanner *bufio.Scanner) (*Config, error) {
+	c := New()
 	var e error
 	sections := make(map[int]string)
 	var sectionLineNum []int

+ 3 - 3
config/krb5conf_test.go

@@ -381,7 +381,7 @@ func TestLoadWithV4Lines(t *testing.T) {
 
 func TestLoad2(t *testing.T) {
 	t.Parallel()
-	c, err := NewConfigFromString(krb5Conf2)
+	c, err := NewFromString(krb5Conf2)
 	if err != nil {
 		t.Fatalf("Error loading config: %v", err)
 	}
@@ -411,7 +411,7 @@ func TestLoad2(t *testing.T) {
 
 func TestLoadNoBlankLines(t *testing.T) {
 	t.Parallel()
-	c, err := NewConfigFromString(krb5ConfNoBlankLines)
+	c, err := NewFromString(krb5ConfNoBlankLines)
 	if err != nil {
 		t.Fatalf("Error loading config: %v", err)
 	}
@@ -504,7 +504,7 @@ func TestParseDuration(t *testing.T) {
 
 func TestResolveRealm(t *testing.T) {
 	t.Parallel()
-	c, err := NewConfigFromString(krb5Conf)
+	c, err := NewFromString(krb5Conf)
 	if err != nil {
 		t.Fatalf("Error loading config: %v", err)
 	}

+ 94 - 24
credentials/credentials.go

@@ -2,6 +2,8 @@
 package credentials
 
 import (
+	"bytes"
+	"encoding/gob"
 	"time"
 
 	"github.com/hashicorp/go-uuid"
@@ -19,15 +21,14 @@ const (
 // Contains either a keytab, password or both.
 // Keytabs are used over passwords if both are defined.
 type Credentials struct {
-	username    string
-	displayName string
-	realm       string
-	cname       types.PrincipalName
-	keytab      *keytab.Keytab
-	password    string
-	attributes  map[string]interface{}
-	validUntil  time.Time
-
+	username        string
+	displayName     string
+	realm           string
+	cname           types.PrincipalName
+	keytab          *keytab.Keytab
+	password        string
+	attributes      map[string]interface{}
+	validUntil      time.Time
 	authenticated   bool
 	human           bool
 	authTime        time.Time
@@ -35,6 +36,24 @@ type Credentials struct {
 	sessionID       string
 }
 
+// marshalCredentials is used to enable marshaling and unmarshaling of credentials
+// without having exported fields on the Credentials struct
+type marshalCredentials struct {
+	Username        string
+	DisplayName     string
+	Realm           string
+	CName           types.PrincipalName
+	Keytab          *keytab.Keytab
+	Password        string
+	Attributes      map[string]interface{}
+	ValidUntil      time.Time
+	Authenticated   bool
+	Human           bool
+	AuthTime        time.Time
+	GroupMembership map[string]bool
+	SessionID       string
+}
+
 // ADCredentials contains information obtained from the PAC.
 type ADCredentials struct {
 	EffectiveName       string
@@ -71,21 +90,9 @@ func New(username string, realm string) *Credentials {
 
 // NewFromPrincipalName creates a new Credentials instance with the user details provides as a PrincipalName type.
 func NewFromPrincipalName(cname types.PrincipalName, realm string) *Credentials {
-	uid, err := uuid.GenerateUUID()
-	if err != nil {
-		uid = "00unique-sess-ions-uuid-unavailable0"
-	}
-	return &Credentials{
-		username:        cname.PrincipalNameString(),
-		displayName:     cname.PrincipalNameString(),
-		realm:           realm,
-		cname:           cname,
-		keytab:          keytab.New(),
-		attributes:      make(map[string]interface{}),
-		groupMembership: make(map[string]bool),
-		sessionID:       uid,
-		human:           true,
-	}
+	c := New(cname.PrincipalNameString(), realm)
+	c.cname = cname
+	return c
 }
 
 // WithKeytab sets the Keytab in the Credentials struct.
@@ -147,6 +154,14 @@ func (c *Credentials) SetADCredentials(a ADCredentials) {
 	}
 }
 
+// GetADCredentials returns ADCredentials attributes sorted in the credential
+func (c *Credentials) GetADCredentials() ADCredentials {
+	if a, ok := c.attributes[AttributeKeyADCredentials].(ADCredentials); ok {
+		return a
+	}
+	return ADCredentials{}
+}
+
 // Methods to implement goidentity.Identity interface
 
 // UserName returns the credential's username.
@@ -312,3 +327,58 @@ func (c *Credentials) SetAttributes(a map[string]interface{}) {
 func (c *Credentials) RemoveAttribute(k string) {
 	delete(c.attributes, k)
 }
+
+// Marshal the Credentials into a byte slice
+func (c *Credentials) Marshal() ([]byte, error) {
+	gob.Register(map[string]interface{}{})
+	gob.Register(ADCredentials{})
+	buf := new(bytes.Buffer)
+	enc := gob.NewEncoder(buf)
+	mc := marshalCredentials{
+		Username:        c.username,
+		DisplayName:     c.displayName,
+		Realm:           c.realm,
+		CName:           c.cname,
+		Keytab:          c.keytab,
+		Password:        c.password,
+		Attributes:      c.attributes,
+		ValidUntil:      c.validUntil,
+		Authenticated:   c.authenticated,
+		Human:           c.human,
+		AuthTime:        c.authTime,
+		GroupMembership: c.groupMembership,
+		SessionID:       c.sessionID,
+	}
+	err := enc.Encode(&mc)
+	if err != nil {
+		return []byte{}, err
+	}
+	return buf.Bytes(), nil
+}
+
+// Unmarshal a byte slice into Credentials
+func (c *Credentials) Unmarshal(b []byte) error {
+	gob.Register(map[string]interface{}{})
+	gob.Register(ADCredentials{})
+	mc := new(marshalCredentials)
+	buf := bytes.NewBuffer(b)
+	dec := gob.NewDecoder(buf)
+	err := dec.Decode(mc)
+	if err != nil {
+		return err
+	}
+	c.username = mc.Username
+	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
+	c.human = mc.Human
+	c.authTime = mc.AuthTime
+	c.groupMembership = mc.GroupMembership
+	c.sessionID = mc.SessionID
+	return nil
+}

+ 16 - 2
credentials/credentials_test.go

@@ -1,9 +1,10 @@
 package credentials
 
 import (
-	"github.com/stretchr/testify/assert"
-	goidentity "gopkg.in/jcmturner/goidentity.v3"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/jcmturner/goidentity.v5"
 )
 
 func TestImplementsInterface(t *testing.T) {
@@ -12,3 +13,16 @@ func TestImplementsInterface(t *testing.T) {
 	i := new(goidentity.Identity)
 	assert.Implements(t, i, u, "Credentials type does not implement the Identity interface")
 }
+
+func TestCredentials_Marshal(t *testing.T) {
+	var cred Credentials
+	b, err := cred.Marshal()
+	if err != nil {
+		t.Fatalf("could not marshal credetials: %v", err)
+	}
+	var credum Credentials
+	err = credum.Unmarshal(b)
+	if err != nil {
+		t.Fatalf("could not unmarshal credetials: %v", err)
+	}
+}

+ 20 - 18
examples/example-AD.go

@@ -4,8 +4,9 @@ package main
 
 import (
 	"encoding/hex"
+	"encoding/json"
 	"fmt"
-	"gopkg.in/jcmturner/goidentity.v3"
+	"gopkg.in/jcmturner/goidentity.v5"
 	"gopkg.in/jcmturner/gokrb5.v7/client"
 	"gopkg.in/jcmturner/gokrb5.v7/config"
 	"gopkg.in/jcmturner/gokrb5.v7/credentials"
@@ -27,15 +28,15 @@ func main() {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_USERKRB5_AD_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
-	cl := client.NewClientWithKeytab("testuser1", "USER.GOKRB5", kt, c, client.DisablePAFXFAST(true))
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	cl := client.NewWithKeytab("testuser1", "USER.GOKRB5", kt, c, client.DisablePAFXFAST(true))
 	httpRequest(s.URL, cl)
 
 	b, _ = hex.DecodeString(testdata.TESTUSER2_USERKRB5_AD_KEYTAB)
 	kt = keytab.New()
 	kt.Unmarshal(b)
-	c, _ = config.NewConfigFromString(testdata.TEST_KRB5CONF)
-	cl = client.NewClientWithKeytab("testuser2", "USER.GOKRB5", kt, c, client.DisablePAFXFAST(true))
+	c, _ = config.NewFromString(testdata.TEST_KRB5CONF)
+	cl = client.NewWithKeytab("testuser2", "USER.GOKRB5", kt, c, client.DisablePAFXFAST(true))
 	httpRequest(s.URL, cl)
 
 	//httpRequest("http://host.test.gokrb5/index.html")
@@ -73,18 +74,20 @@ func httpServer() *httptest.Server {
 }
 
 func testAppHandler(w http.ResponseWriter, r *http.Request) {
-	ctx := r.Context()
+	creds := goidentity.FromHTTPRequestContext(r)
 	fmt.Fprint(w, "<html>\n<p><h1>TEST.GOKRB5 Handler</h1></p>\n")
-	if validuser, ok := ctx.Value(spnego.CTXKeyAuthenticated).(bool); ok && validuser {
-		if creds, ok := ctx.Value(spnego.CTXKeyCredentials).(goidentity.Identity); ok {
-			fmt.Fprintf(w, "<ul><li>Authenticed user: %s</li>\n", creds.UserName())
-			fmt.Fprintf(w, "<li>User's realm: %s</li>\n", creds.Domain())
-			fmt.Fprint(w, "<li>Authz Attributes (Group Memberships):</li><ul>\n")
-			for _, s := range creds.AuthzAttributes() {
-				fmt.Fprintf(w, "<li>%v</li>\n", s)
-			}
-			fmt.Fprint(w, "</ul>\n")
-			if ADCreds, ok := creds.Attributes()[credentials.AttributeKeyADCredentials].(credentials.ADCredentials); ok {
+	if creds != nil && creds.Authenticated() {
+		fmt.Fprintf(w, "<ul><li>Authenticed user: %s</li>\n", creds.UserName())
+		fmt.Fprintf(w, "<li>User's realm: %s</li>\n", creds.Domain())
+		fmt.Fprint(w, "<li>Authz Attributes (Group Memberships):</li><ul>\n")
+		for _, s := range creds.AuthzAttributes() {
+			fmt.Fprintf(w, "<li>%v</li>\n", s)
+		}
+		fmt.Fprint(w, "</ul>\n")
+		if ADCredsJSON, ok := creds.Attributes()[credentials.AttributeKeyADCredentials]; ok {
+			ADCreds := new(credentials.ADCredentials)
+			err := json.Unmarshal([]byte(ADCredsJSON), ADCreds)
+			if err == nil {
 				// Now access the fields of the ADCredentials struct. For example:
 				fmt.Fprintf(w, "<li>EffectiveName: %v</li>\n", ADCreds.EffectiveName)
 				fmt.Fprintf(w, "<li>FullName: %v</li>\n", ADCreds.FullName)
@@ -98,9 +101,8 @@ func testAppHandler(w http.ResponseWriter, r *http.Request) {
 				fmt.Fprintf(w, "<li>LogonDomainName: %v</li>\n", ADCreds.LogonDomainName)
 				fmt.Fprintf(w, "<li>LogonDomainID: %v</li>\n", ADCreds.LogonDomainID)
 			}
-			fmt.Fprint(w, "</ul>")
 		}
-
+		fmt.Fprint(w, "</ul>")
 	} else {
 		w.WriteHeader(http.StatusUnauthorized)
 		fmt.Fprint(w, "Authentication failed")

+ 10 - 13
examples/example.go

@@ -1,6 +1,6 @@
+// Package examples provides simple examples of gokrb5 use.
 // +build examples
 
-// Package examples provides simple examples of gokrb5 use.
 package main
 
 import (
@@ -12,7 +12,7 @@ import (
 	"net/http/httptest"
 	"os"
 
-	"gopkg.in/jcmturner/goidentity.v3"
+	"gopkg.in/jcmturner/goidentity.v5"
 	"gopkg.in/jcmturner/gokrb5.v7/client"
 	"gopkg.in/jcmturner/gokrb5.v7/config"
 	"gopkg.in/jcmturner/gokrb5.v7/keytab"
@@ -28,17 +28,17 @@ func main() {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	c.LibDefaults.NoAddresses = true
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 	httpRequest(s.URL, cl)
 
 	b, _ = hex.DecodeString(testdata.TESTUSER2_KEYTAB)
 	kt = keytab.New()
 	kt.Unmarshal(b)
-	c, _ = config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ = config.NewFromString(testdata.TEST_KRB5CONF)
 	c.LibDefaults.NoAddresses = true
-	cl = client.NewClientWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
+	cl = client.NewWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
 	httpRequest(s.URL, cl)
 }
 
@@ -74,14 +74,11 @@ func httpServer() *httptest.Server {
 }
 
 func testAppHandler(w http.ResponseWriter, r *http.Request) {
-	ctx := r.Context()
+	creds := goidentity.FromHTTPRequestContext(r)
 	fmt.Fprint(w, "<html>\n<p><h1>TEST.GOKRB5 Handler</h1></p>\n")
-	if validuser, ok := ctx.Value(spnego.CTXKeyAuthenticated).(bool); ok && validuser {
-		if creds, ok := ctx.Value(spnego.CTXKeyCredentials).(goidentity.Identity); ok {
-			fmt.Fprintf(w, "<ul><li>Authenticed user: %s</li>\n", creds.UserName())
-			fmt.Fprintf(w, "<li>User's realm: %s</li></ul>\n", creds.Domain())
-		}
-
+	if creds != nil && creds.Authenticated() {
+		fmt.Fprintf(w, "<ul><li>Authenticed user: %s</li>\n", creds.UserName())
+		fmt.Fprintf(w, "<li>User's realm: %s</li></ul>\n", creds.Domain())
 	} else {
 		w.WriteHeader(http.StatusUnauthorized)
 		fmt.Fprint(w, "Authentication failed")

+ 2 - 2
examples/httpClient.go

@@ -54,7 +54,7 @@ func main() {
 	}
 
 	// Load the client krb5 config
-	conf, err := config.NewConfigFromString(kRB5CONF)
+	conf, err := config.NewFromString(kRB5CONF)
 	if err != nil {
 		l.Fatalf("could not load krb5.conf: %v", err)
 	}
@@ -64,7 +64,7 @@ func main() {
 	}
 
 	// Create the client with the keytab
-	cl := client.NewClientWithKeytab("testuser2", "TEST.GOKRB5", kt, conf, client.Logger(l), client.DisablePAFXFAST(true))
+	cl := client.NewWithKeytab("testuser2", "TEST.GOKRB5", kt, conf, client.Logger(l), client.DisablePAFXFAST(true))
 
 	// Log in the client
 	err = cl.Login()

+ 44 - 4
examples/httpServer.go

@@ -9,7 +9,9 @@ import (
 	"net/http"
 	"os"
 
-	goidentity "gopkg.in/jcmturner/goidentity.v3"
+	"github.com/gorilla/sessions"
+	"github.com/pkg/errors"
+	"gopkg.in/jcmturner/goidentity.v5"
 	"gopkg.in/jcmturner/gokrb5.v7/keytab"
 	"gopkg.in/jcmturner/gokrb5.v7/service"
 	"gopkg.in/jcmturner/gokrb5.v7/spnego"
@@ -35,7 +37,7 @@ func main() {
 
 	// Set up handler mappings wrapping in the SPNEGOKRB5Authenticate handler wrapper
 	mux := http.NewServeMux()
-	mux.Handle("/", spnego.SPNEGOKRB5Authenticate(th, kt, service.Logger(l)))
+	mux.Handle("/", spnego.SPNEGOKRB5Authenticate(th, kt, service.Logger(l), service.SessionManager(NewSessionMgr("gokrb5"))))
 
 	// Start up the web server
 	log.Fatal(http.ListenAndServe(port, mux))
@@ -44,8 +46,7 @@ func main() {
 // Simple application specific handler
 func testAppHandler(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
-	ctx := r.Context()
-	creds := ctx.Value(spnego.CTXKeyCredentials).(goidentity.Identity)
+	creds := goidentity.FromHTTPRequestContext(r)
 	fmt.Fprintf(w,
 		`<html>
 <h1>GOKRB5 Handler</h1>
@@ -63,3 +64,42 @@ func testAppHandler(w http.ResponseWriter, r *http.Request) {
 	)
 	return
 }
+
+type SessionMgr struct {
+	skey       []byte
+	store      sessions.Store
+	cookieName string
+}
+
+func NewSessionMgr(cookieName string) SessionMgr {
+	skey := []byte("thisistestsecret") // Best practice is to load this key from a secure location.
+	return SessionMgr{
+		skey:       skey,
+		store:      sessions.NewCookieStore(skey),
+		cookieName: cookieName,
+	}
+}
+
+func (smgr SessionMgr) Get(r *http.Request, k string) ([]byte, error) {
+	s, err := smgr.store.Get(r, smgr.cookieName)
+	if err != nil {
+		return nil, err
+	}
+	if s == nil {
+		return nil, errors.New("nil session")
+	}
+	b, ok := s.Values[k].([]byte)
+	if !ok {
+		return nil, fmt.Errorf("could not get bytes held in session at %s", k)
+	}
+	return b, nil
+}
+
+func (smgr SessionMgr) New(w http.ResponseWriter, r *http.Request, k string, v []byte) error {
+	s, err := smgr.store.New(r, smgr.cookieName)
+	if err != nil {
+		return fmt.Errorf("could not get new session from session manager: %v", err)
+	}
+	s.Values[k] = v
+	return s.Save(r, w)
+}

+ 4 - 2
examples/longRunningClient.go

@@ -1,3 +1,5 @@
+// +build examples
+
 package main
 
 import (
@@ -48,7 +50,7 @@ func main() {
 	}
 
 	// Load the client krb5 config
-	conf, err := config.NewConfigFromString(kRB5CONF)
+	conf, err := config.NewFromString(kRB5CONF)
 	if err != nil {
 		l.Fatalf("could not load krb5.conf: %v", err)
 	}
@@ -58,7 +60,7 @@ func main() {
 	}
 
 	// Create the client with the keytab
-	cl := client.NewClientWithKeytab("testuser2", "TEST.GOKRB5", kt, conf, client.Logger(l), client.DisablePAFXFAST(true))
+	cl := client.NewWithKeytab("testuser2", "TEST.GOKRB5", kt, conf, client.Logger(l), client.DisablePAFXFAST(true))
 
 	// Log in the client
 	err = cl.Login()

+ 4 - 1
gssapi/gssapi.go

@@ -14,6 +14,7 @@ const (
 	OIDKRB5         OIDName = "KRB5"         // MechType OID for Kerberos 5
 	OIDMSLegacyKRB5 OIDName = "MSLegacyKRB5" // MechType OID for Kerberos 5
 	OIDSPNEGO       OIDName = "SPNEGO"
+	OIDGSSIAKerb    OIDName = "GSSIAKerb" // Indicates the client cannot get a service ticket and asks the server to serve as an intermediate to the target KDC. http://k5wiki.kerberos.org/wiki/Projects/IAKERB#IAKERB_mech
 )
 
 // GSS-API status values
@@ -117,7 +118,7 @@ type Mechanism interface {
 type OIDName string
 
 // OID returns the OID for the provided OID name.
-func OID(o OIDName) asn1.ObjectIdentifier {
+func (o OIDName) OID() asn1.ObjectIdentifier {
 	switch o {
 	case OIDSPNEGO:
 		return asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 2}
@@ -125,6 +126,8 @@ func OID(o OIDName) asn1.ObjectIdentifier {
 		return asn1.ObjectIdentifier{1, 2, 840, 113554, 1, 2, 2}
 	case OIDMSLegacyKRB5:
 		return asn1.ObjectIdentifier{1, 2, 840, 48018, 1, 2, 2}
+	case OIDGSSIAKerb:
+		return asn1.ObjectIdentifier{1, 3, 6, 1, 5, 2, 5}
 	}
 	return asn1.ObjectIdentifier{}
 }

+ 2 - 1
gssapi/gssapi_test.go

@@ -15,10 +15,11 @@ func TestOID(t *testing.T) {
 		{OIDMSLegacyKRB5, []int{1, 2, 840, 48018, 1, 2, 2}},
 		{OIDKRB5, []int{1, 2, 840, 113554, 1, 2, 2}},
 		{OIDSPNEGO, []int{1, 3, 6, 1, 5, 5, 2}},
+		{OIDGSSIAKerb, []int{1, 3, 6, 1, 5, 2, 5}},
 	}
 
 	for _, tst := range tests {
 		oid := asn1.ObjectIdentifier(tst.oid)
-		assert.True(t, oid.Equal(OID(tst.name)), "OID value not as expected for %s", tst.name)
+		assert.True(t, oid.Equal(OIDName(tst.name).OID()), "OID value not as expected for %s", tst.name)
 	}
 }

+ 7 - 6
keytab/keytab.go

@@ -51,8 +51,8 @@ func New() *Keytab {
 }
 
 // GetEncryptionKey returns the EncryptionKey from the Keytab for the newest entry with the required kvno, etype and matching principal.
-func (kt *Keytab) GetEncryptionKey(princName types.PrincipalName, realm string, kvno int, etype int32) (types.EncryptionKey, error) {
-	//TODO (theme: KVNO from keytab) this function should return the kvno too
+// If the kvno is zero then the latest kvno will be returned. The kvno is also returned for
+func (kt *Keytab) GetEncryptionKey(princName types.PrincipalName, realm string, kvno int, etype int32) (types.EncryptionKey, int, error) {
 	var key types.EncryptionKey
 	var t time.Time
 	for _, k := range kt.Entries {
@@ -69,18 +69,19 @@ func (kt *Keytab) GetEncryptionKey(princName types.PrincipalName, realm string,
 			}
 			if p {
 				key = k.Key
+				kvno = int(k.KVNO)
 				t = k.Timestamp
 			}
 		}
 	}
 	if len(key.KeyValue) < 1 {
-		return key, fmt.Errorf("matching key not found in keytab. Looking for %v realm: %v kvno: %v etype: %v", princName.NameString, realm, kvno, etype)
+		return key, 0, fmt.Errorf("matching key not found in keytab. Looking for %v realm: %v kvno: %v etype: %v", princName.NameString, realm, kvno, etype)
 	}
-	return key, nil
+	return key, kvno, nil
 }
 
 // Create a new Keytab entry.
-func newKeytabEntry() entry {
+func newEntry() entry {
 	var b []byte
 	return entry{
 		Principal: newPrincipal(),
@@ -188,7 +189,7 @@ func (kt *Keytab) Unmarshal(b []byte) error {
 			}
 			eb := b[n : n+int(l)]
 			n = n + int(l)
-			ke := newKeytabEntry()
+			ke := newEntry()
 			// p keeps track as to where we are in the byte stream
 			var p int
 			var err error

+ 2 - 2
krberror/error.go

@@ -35,8 +35,8 @@ func (e *Krberror) Add(et string, s string) {
 	e.EText = append([]string{fmt.Sprintf("%s: %s", et, s)}, e.EText...)
 }
 
-// NewKrberror creates a new instance of Krberror.
-func NewKrberror(et, s string) Krberror {
+// New creates a new instance of Krberror.
+func New(et, s string) Krberror {
 	return Krberror{
 		RootCause: et,
 		EText:     []string{s},

+ 1 - 1
messages/KDCRep.go

@@ -154,7 +154,7 @@ func (k *ASRep) DecryptEncPart(c *credentials.Credentials) (types.EncryptionKey,
 	var key types.EncryptionKey
 	var err error
 	if c.HasKeytab() {
-		key, err = c.Keytab().GetEncryptionKey(k.CName, k.CRealm, k.EncPart.KVNO, k.EncPart.EType)
+		key, _, err = c.Keytab().GetEncryptionKey(k.CName, k.CRealm, k.EncPart.KVNO, k.EncPart.EType)
 		if err != nil {
 			return key, krberror.Errorf(err, krberror.DecryptingError, "error decrypting AS_REP encrypted part")
 		}

+ 3 - 3
messages/Ticket.go

@@ -83,7 +83,7 @@ func NewTicket(cname types.PrincipalName, crealm string, sname types.PrincipalNa
 		return Ticket{}, types.EncryptionKey{}, krberror.Errorf(err, krberror.EncodingError, "error marshalling ticket encpart")
 	}
 	b = asn1tools.AddASNAppTag(b, asnAppTag.EncTicketPart)
-	skey, err := sktab.GetEncryptionKey(sname, srealm, kvno, eTypeID)
+	skey, _, err := sktab.GetEncryptionKey(sname, srealm, kvno, eTypeID)
 	if err != nil {
 		return Ticket{}, types.EncryptionKey{}, krberror.Errorf(err, krberror.EncryptingError, "error getting encryption key for new ticket")
 	}
@@ -193,7 +193,7 @@ func (t *Ticket) DecryptEncPart(keytab *keytab.Keytab, sname *types.PrincipalNam
 	if sname == nil {
 		sname = &t.SName
 	}
-	key, err := keytab.GetEncryptionKey(*sname, t.Realm, t.EncPart.KVNO, t.EncPart.EType)
+	key, _, err := keytab.GetEncryptionKey(*sname, 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))
 	}
@@ -236,7 +236,7 @@ func (t *Ticket) GetPACType(keytab *keytab.Keytab, sname *types.PrincipalName, l
 				if sname == nil {
 					sname = &t.SName
 				}
-				key, err := keytab.GetEncryptionKey(*sname, t.Realm, t.EncPart.KVNO, t.EncPart.EType)
+				key, _, err := keytab.GetEncryptionKey(*sname, t.Realm, t.EncPart.KVNO, t.EncPart.EType)
 				if err != nil {
 					return isPAC, p, NewKRBError(t.SName, t.Realm, errorcode.KRB_AP_ERR_NOKEY, fmt.Sprintf("Could not get key from keytab: %v", err))
 				}

+ 1 - 1
pac/pac_type_test.go

@@ -29,7 +29,7 @@ func TestPACTypeVerify(t *testing.T) {
 	kt := keytab.New()
 	kt.Unmarshal(b)
 	pn, _ := types.ParseSPNString("sysHTTP")
-	key, err := kt.GetEncryptionKey(pn, "TEST.GOKRB5", 2, 18)
+	key, _, err := kt.GetEncryptionKey(pn, "TEST.GOKRB5", 2, 18)
 	if err != nil {
 		t.Fatalf("Error getting key: %v", err)
 	}

+ 1 - 2
service/APExchange.go

@@ -9,9 +9,8 @@ import (
 )
 
 // VerifyAPREQ verifies 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 VerifyAPREQ(APReq messages.APReq, s *Settings) (bool, *credentials.Credentials, error) {
+func VerifyAPREQ(APReq *messages.APReq, s *Settings) (bool, *credentials.Credentials, error) {
 	var creds *credentials.Credentials
-
 	ok, err := APReq.Verify(s.Keytab, s.MaxClockSkew(), s.ClientAddress())
 	if err != nil || !ok {
 		return false, creds, err

+ 10 - 10
service/APExchange_test.go

@@ -54,7 +54,7 @@ func TestVerifyAPREQ(t *testing.T) {
 
 	h, _ := types.GetHostAddress("127.0.0.1:1234")
 	s := NewSettings(kt, ClientAddress(h))
-	ok, _, err := VerifyAPREQ(APReq, s)
+	ok, _, err := VerifyAPREQ(&APReq, s)
 	if !ok || err != nil {
 		t.Fatalf("Validation of AP_REQ failed when it should not have: %v", err)
 	}
@@ -100,7 +100,7 @@ func TestVerifyAPREQ_KRB_AP_ERR_BADMATCH(t *testing.T) {
 	}
 	h, _ := types.GetHostAddress("127.0.0.1:1234")
 	s := NewSettings(kt, ClientAddress(h))
-	ok, _, err := VerifyAPREQ(APReq, s)
+	ok, _, err := VerifyAPREQ(&APReq, s)
 	if ok || err == nil {
 		t.Fatal("Validation of AP_REQ passed when it should not have")
 	}
@@ -149,7 +149,7 @@ func TestVerifyAPREQ_LargeClockSkew(t *testing.T) {
 
 	h, _ := types.GetHostAddress("127.0.0.1:1234")
 	s := NewSettings(kt, ClientAddress(h))
-	ok, _, err := VerifyAPREQ(APReq, s)
+	ok, _, err := VerifyAPREQ(&APReq, s)
 	if ok || err == nil {
 		t.Fatal("Validation of AP_REQ passed when it should not have")
 	}
@@ -196,12 +196,12 @@ func TestVerifyAPREQ_Replay(t *testing.T) {
 
 	h, _ := types.GetHostAddress("127.0.0.1:1234")
 	s := NewSettings(kt, ClientAddress(h))
-	ok, _, err := VerifyAPREQ(APReq, s)
+	ok, _, err := VerifyAPREQ(&APReq, s)
 	if !ok || err != nil {
 		t.Fatalf("Validation of AP_REQ failed when it should not have: %v", err)
 	}
 	// Replay
-	ok, _, err = VerifyAPREQ(APReq, s)
+	ok, _, err = VerifyAPREQ(&APReq, s)
 	if ok || err == nil {
 		t.Fatal("Validation of AP_REQ passed when it should not have")
 	}
@@ -246,7 +246,7 @@ func TestVerifyAPREQ_FutureTicket(t *testing.T) {
 
 	h, _ := types.GetHostAddress("127.0.0.1:1234")
 	s := NewSettings(kt, ClientAddress(h))
-	ok, _, err := VerifyAPREQ(APReq, s)
+	ok, _, err := VerifyAPREQ(&APReq, s)
 	if ok || err == nil {
 		t.Fatal("Validation of AP_REQ passed when it should not have")
 	}
@@ -295,7 +295,7 @@ func TestVerifyAPREQ_InvalidTicket(t *testing.T) {
 
 	h, _ := types.GetHostAddress("127.0.0.1:1234")
 	s := NewSettings(kt, ClientAddress(h))
-	ok, _, err := VerifyAPREQ(APReq, s)
+	ok, _, err := VerifyAPREQ(&APReq, s)
 	if ok || err == nil {
 		t.Fatal("Validation of AP_REQ passed when it should not have")
 	}
@@ -343,7 +343,7 @@ func TestVerifyAPREQ_ExpiredTicket(t *testing.T) {
 
 	h, _ := types.GetHostAddress("127.0.0.1:1234")
 	s := NewSettings(kt, ClientAddress(h))
-	ok, _, err := VerifyAPREQ(APReq, s)
+	ok, _, err := VerifyAPREQ(&APReq, s)
 	if ok || err == nil {
 		t.Fatal("Validation of AP_REQ passed when it should not have")
 	}
@@ -368,7 +368,7 @@ func getClient() *client.Client {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 	return cl
 }

+ 3 - 3
service/authenticator.go

@@ -6,7 +6,7 @@ import (
 	"strings"
 	"time"
 
-	goidentity "gopkg.in/jcmturner/goidentity.v3"
+	goidentity "gopkg.in/jcmturner/goidentity.v5"
 	"gopkg.in/jcmturner/gokrb5.v7/client"
 	"gopkg.in/jcmturner/gokrb5.v7/config"
 	"gopkg.in/jcmturner/gokrb5.v7/credentials"
@@ -22,7 +22,7 @@ func NewKRB5BasicAuthenticator(headerVal string, krb5conf *config.Config, servic
 	}
 }
 
-// KRB5BasicAuthenticator implements gopkg.in/jcmturner/goidentity.v3.Authenticator interface.
+// KRB5BasicAuthenticator implements gopkg.in/jcmturner/goidentity.v5.Authenticator interface.
 // It takes username and password so can be used for basic authentication.
 type KRB5BasicAuthenticator struct {
 	BasicHeaderValue string
@@ -41,7 +41,7 @@ func (a KRB5BasicAuthenticator) Authenticate() (i goidentity.Identity, ok bool,
 		err = fmt.Errorf("could not parse basic authentication header: %v", err)
 		return
 	}
-	cl := client.NewClientWithPassword(a.username, a.realm, a.password, a.clientConfig)
+	cl := client.NewWithPassword(a.username, a.realm, a.password, a.clientConfig)
 	err = cl.Login()
 	if err != nil {
 		// Username and/or password could be wrong

+ 1 - 1
service/authenticator_test.go

@@ -4,7 +4,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"gopkg.in/jcmturner/goidentity.v3"
+	"gopkg.in/jcmturner/goidentity.v5"
 )
 
 func TestImplementsInterface(t *testing.T) {

+ 26 - 0
service/settings.go

@@ -2,6 +2,7 @@ package service
 
 import (
 	"log"
+	"net/http"
 	"time"
 
 	"gopkg.in/jcmturner/gokrb5.v7/keytab"
@@ -18,6 +19,7 @@ type Settings struct {
 	cAddr              types.HostAddress
 	maxClockSkew       time.Duration
 	logger             *log.Logger
+	sessionMgr         SessionMgr
 }
 
 // NewSettings creates a new service Settings.
@@ -134,3 +136,27 @@ func SName(sname string) func(*Settings) {
 func (s *Settings) SName() string {
 	return s.sname
 }
+
+// SessionManager configures a session manager to establish sessions with clients to avoid excessive authentication challenges.
+//
+// s := NewSettings(kt, SessionManager(sm))
+func SessionManager(sm SessionMgr) func(*Settings) {
+	return func(s *Settings) {
+		s.sessionMgr = sm
+	}
+}
+
+// SessionManager returns any configured session manager.
+func (s *Settings) SessionManager() SessionMgr {
+	return s.sessionMgr
+}
+
+// SessionMgr must provide a ways to:
+//
+// - Create new sessions and in the process add a value to the session under the key provided.
+//
+// - Get an existing the value in the session under the key provided. Return nil bytes and/or error if there is no session.
+type SessionMgr interface {
+	New(w http.ResponseWriter, r *http.Request, k string, v []byte) error
+	Get(r *http.Request, k string) ([]byte, error)
+}

+ 98 - 33
spnego/http.go

@@ -2,7 +2,6 @@ package spnego
 
 import (
 	"bytes"
-	"context"
 	"encoding/base64"
 	"errors"
 	"fmt"
@@ -14,8 +13,9 @@ import (
 	"net/url"
 	"strings"
 
-	"gopkg.in/jcmturner/goidentity.v3"
+	"gopkg.in/jcmturner/goidentity.v5"
 	"gopkg.in/jcmturner/gokrb5.v7/client"
+	"gopkg.in/jcmturner/gokrb5.v7/credentials"
 	"gopkg.in/jcmturner/gokrb5.v7/gssapi"
 	"gopkg.in/jcmturner/gokrb5.v7/keytab"
 	"gopkg.in/jcmturner/gokrb5.v7/krberror"
@@ -191,8 +191,6 @@ func SetSPNEGOHeader(cl *client.Client, r *http.Request, spn string) error {
 
 // Service side functionality //
 
-type ctxKey string
-
 const (
 	// spnegoNegTokenRespKRBAcceptCompleted - The response on successful authentication always has this header. Capturing as const so we don't have marshaling and encoding overhead.
 	spnegoNegTokenRespKRBAcceptCompleted = "Negotiate oRQwEqADCgEAoQsGCSqGSIb3EgECAg=="
@@ -200,10 +198,10 @@ const (
 	spnegoNegTokenRespReject = "Negotiate oQcwBaADCgEC"
 	// spnegoNegTokenRespIncompleteKRB5 - Response token specifying incomplete context and KRB5 as the supported mechtype.
 	spnegoNegTokenRespIncompleteKRB5 = "Negotiate oRQwEqADCgEBoQsGCSqGSIb3EgECAg=="
-	// CTXKeyAuthenticated is the request context key holding a boolean indicating if the request has been authenticated.
-	CTXKeyAuthenticated ctxKey = "github.com/jcmturner/gokrb5/CTXKeyAuthenticated"
-	// CTXKeyCredentials is the request context key holding the credentials gopkg.in/jcmturner/goidentity.v2/Identity object.
-	CTXKeyCredentials ctxKey = "github.com/jcmturner/gokrb5/CTXKeyCredentials"
+	// sessionCredentials is the session value key holding the credentials jcmturner/goidentity/Identity object.
+	sessionCredentials = "github.com/jcmturner/gokrb5/sessionCredentials"
+	// ctxCredentials is the SPNEGO context key holding the credentials jcmturner/goidentity/Identity object.
+	ctxCredentials = "github.com/jcmturner/gokrb5/ctxCredentials"
 	// HTTPHeaderAuthRequest is the header that will hold authn/z information.
 	HTTPHeaderAuthRequest = "Authorization"
 	// HTTPHeaderAuthResponse is the header that will hold SPNEGO data from the server.
@@ -217,15 +215,6 @@ const (
 // SPNEGOKRB5Authenticate is a Kerberos SPNEGO authentication HTTP handler wrapper.
 func SPNEGOKRB5Authenticate(inner http.Handler, kt *keytab.Keytab, settings ...func(*service.Settings)) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		// Get the auth header
-		s := strings.SplitN(r.Header.Get(HTTPHeaderAuthRequest), " ", 2)
-		if len(s) != 2 || s[0] != HTTPHeaderAuthResponseValueKey {
-			// No Authorization header set so return 401 with WWW-Authenticate Negotiate header
-			w.Header().Set(HTTPHeaderAuthResponse, HTTPHeaderAuthResponseValueKey)
-			http.Error(w, UnauthorizedMsg, http.StatusUnauthorized)
-			return
-		}
-
 		// Set up the SPNEGO GSS-API mechanism
 		var spnego *SPNEGO
 		h, err := types.GetHostAddress(r.RemoteAddr)
@@ -238,21 +227,23 @@ func SPNEGOKRB5Authenticate(inner http.Handler, kt *keytab.Keytab, settings ...f
 			spnego.Log("%s - SPNEGO could not parse client address: %v", r.RemoteAddr, err)
 		}
 
-		// Decode the header into an SPNEGO context token
-		b, err := base64.StdEncoding.DecodeString(s[1])
-		if err != nil {
-			spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO error in base64 decoding negotiation header: %v", r.RemoteAddr, err)
+		// Check if there is a session manager and if there is an already established session for this client
+		id, err := getSessionCredentials(spnego, r)
+		if err == nil && id.Authenticated() {
+			// There is an established session so bypass auth and serve
+			spnego.Log("%s - SPNEGO request served under session %s", r.RemoteAddr, id.SessionID())
+			inner.ServeHTTP(w, goidentity.AddToHTTPRequestContext(&id, r))
 			return
 		}
-		var st SPNEGOToken
-		err = st.Unmarshal(b)
-		if err != nil {
-			spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO error in unmarshaling SPNEGO token: %v", r.RemoteAddr, err)
+
+		st, err := getAuthorizationNegotiationHeaderAsSPNEGOToken(spnego, r, w)
+		if st == nil || err != nil {
+			// response to client and logging handled in function above so just return
 			return
 		}
 
 		// Validate the context token
-		authed, ctx, status := spnego.AcceptSecContext(&st)
+		authed, ctx, status := spnego.AcceptSecContext(st)
 		if status.Code != gssapi.StatusComplete && status.Code != gssapi.StatusContinueNeeded {
 			spnegoResponseReject(spnego, w, "%s - SPNEGO validation error: %v", r.RemoteAddr, status)
 			return
@@ -261,20 +252,89 @@ func SPNEGOKRB5Authenticate(inner http.Handler, kt *keytab.Keytab, settings ...f
 			spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO GSS-API continue needed", r.RemoteAddr)
 			return
 		}
+
 		if authed {
-			id := ctx.Value(CTXKeyCredentials).(goidentity.Identity)
-			requestCtx := r.Context()
-			requestCtx = context.WithValue(requestCtx, CTXKeyCredentials, id)
-			requestCtx = context.WithValue(requestCtx, CTXKeyAuthenticated, ctx.Value(CTXKeyAuthenticated))
+			// Authentication successful; get user's credentials from the context
+			id := ctx.Value(ctxCredentials).(*credentials.Credentials)
+			// Create a new session if a session manager has been configured
+			err = newSession(spnego, r, w, id)
+			if err != nil {
+				return
+			}
 			spnegoResponseAcceptCompleted(spnego, w, "%s %s@%s - SPNEGO authentication succeeded", r.RemoteAddr, id.UserName(), id.Domain())
-			inner.ServeHTTP(w, r.WithContext(requestCtx))
-		} else {
-			spnegoResponseReject(spnego, w, "%s - SPNEGO Kerberos authentication failed", r.RemoteAddr)
+			// Add the identity to the context and serve the inner/wrapped handler
+			inner.ServeHTTP(w, goidentity.AddToHTTPRequestContext(id, r))
 			return
 		}
+		// If we get to here we have not authenticationed so just reject
+		spnegoResponseReject(spnego, w, "%s - SPNEGO Kerberos authentication failed", r.RemoteAddr)
+		return
 	})
 }
 
+func getAuthorizationNegotiationHeaderAsSPNEGOToken(spnego *SPNEGO, r *http.Request, w http.ResponseWriter) (*SPNEGOToken, error) {
+	s := strings.SplitN(r.Header.Get(HTTPHeaderAuthRequest), " ", 2)
+	if len(s) != 2 || s[0] != HTTPHeaderAuthResponseValueKey {
+		// No Authorization header set so return 401 with WWW-Authenticate Negotiate header
+		w.Header().Set(HTTPHeaderAuthResponse, HTTPHeaderAuthResponseValueKey)
+		http.Error(w, UnauthorizedMsg, http.StatusUnauthorized)
+		return nil, errors.New("client did not provide a negotiation authorization header")
+	}
+
+	// Decode the header into an SPNEGO context token
+	b, err := base64.StdEncoding.DecodeString(s[1])
+	if err != nil {
+		err = fmt.Errorf("error in base64 decoding negotiation header: %v", err)
+		spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO %v", r.RemoteAddr, err)
+		return nil, err
+	}
+	var st SPNEGOToken
+	err = st.Unmarshal(b)
+	if err != nil {
+		err = fmt.Errorf("error in unmarshaling SPNEGO token: %v", err)
+		spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO %v", r.RemoteAddr, err)
+		return nil, err
+	}
+	return &st, nil
+}
+
+func getSessionCredentials(spnego *SPNEGO, r *http.Request) (credentials.Credentials, error) {
+	var creds credentials.Credentials
+	// Check if there is a session manager and if there is an already established session for this client
+	if sm := spnego.serviceSettings.SessionManager(); sm != nil {
+		cb, err := sm.Get(r, sessionCredentials)
+		if err != nil || cb == nil || len(cb) < 1 {
+			return creds, fmt.Errorf("%s - SPNEGO error getting session and credentials for request: %v", r.RemoteAddr, err)
+		}
+		err = creds.Unmarshal(cb)
+		if err != nil {
+			return creds, fmt.Errorf("%s - SPNEGO credentials malformed in session: %v", r.RemoteAddr, err)
+		}
+		return creds, nil
+	}
+	return creds, errors.New("no session manager configured")
+}
+
+func newSession(spnego *SPNEGO, r *http.Request, w http.ResponseWriter, id *credentials.Credentials) error {
+	if sm := spnego.serviceSettings.SessionManager(); sm != nil {
+		// create new session
+		idb, err := id.Marshal()
+		if err != nil {
+			spnegoInternalServerError(spnego, w, "SPNEGO could not marshal credentials to add to the session: %v", err)
+			return err
+		}
+		err = sm.New(w, r, sessionCredentials, idb)
+		if err != nil {
+			spnegoInternalServerError(spnego, w, "SPNEGO could not create new session: %v", err)
+			return err
+		}
+		spnego.Log("%s %s@%s - SPNEGO new session (%s) created", r.RemoteAddr, id.UserName(), id.Domain(), id.SessionID())
+	}
+	return nil
+}
+
+// Log and respond to client for error conditions
+
 func spnegoNegotiateKRB5MechType(s *SPNEGO, w http.ResponseWriter, format string, v ...interface{}) {
 	s.Log(format, v...)
 	w.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespIncompleteKRB5)
@@ -291,3 +351,8 @@ func spnegoResponseAcceptCompleted(s *SPNEGO, w http.ResponseWriter, format stri
 	s.Log(format, v...)
 	w.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespKRBAcceptCompleted)
 }
+
+func spnegoInternalServerError(s *SPNEGO, w http.ResponseWriter, format string, v ...interface{}) {
+	s.Log(format, v...)
+	http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+}

+ 78 - 17
spnego/http_test.go

@@ -4,19 +4,22 @@ import (
 	"bytes"
 	"crypto/rand"
 	"encoding/hex"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"log"
 	"mime/multipart"
 	"net/http"
+	"net/http/cookiejar"
 	"net/http/httptest"
 	"os"
 	"sync"
 	"testing"
 
+	"github.com/gorilla/sessions"
 	"github.com/stretchr/testify/assert"
-	"gopkg.in/jcmturner/goidentity.v3"
+	"gopkg.in/jcmturner/goidentity.v5"
 	"gopkg.in/jcmturner/gokrb5.v7/client"
 	"gopkg.in/jcmturner/gokrb5.v7/config"
 	"gopkg.in/jcmturner/gokrb5.v7/keytab"
@@ -30,14 +33,14 @@ func TestClient_SetSPNEGOHeader(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
 	l := log.New(os.Stderr, "SPNEGO Client:", log.LstdFlags)
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c, client.Logger(l))
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c, client.Logger(l))
 
 	err := cl.Login()
 	if err != nil {
@@ -79,14 +82,14 @@ func TestSPNEGOHTTPClient(t *testing.T) {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
 		addr = testdata.TEST_KDC_ADDR
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
 	l := log.New(os.Stderr, "SPNEGO Client:", log.LstdFlags)
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c, client.Logger(l))
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c, client.Logger(l))
 
 	err := cl.Login()
 	if err != nil {
@@ -155,7 +158,7 @@ func TestService_SPNEGOKRB_ValidUser(t *testing.T) {
 func TestService_SPNEGOKRB_Replay(t *testing.T) {
 	test.Integration(t)
 
-	s := httpServer()
+	s := httpServerWithoutSessionManager()
 	defer s.Close()
 	r1, _ := http.NewRequest("GET", s.URL, nil)
 
@@ -212,7 +215,7 @@ func TestService_SPNEGOKRB_Replay(t *testing.T) {
 func TestService_SPNEGOKRB_ReplayCache_Concurrency(t *testing.T) {
 	test.Integration(t)
 
-	s := httpServer()
+	s := httpServerWithoutSessionManager()
 	defer s.Close()
 	r1, _ := http.NewRequest("GET", s.URL, nil)
 
@@ -221,6 +224,7 @@ func TestService_SPNEGOKRB_ReplayCache_Concurrency(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error setting client's SPNEGO header: %v", err)
 	}
+	r1h := r1.Header.Get(HTTPHeaderAuthRequest)
 
 	r2, _ := http.NewRequest("GET", s.URL, nil)
 
@@ -228,6 +232,7 @@ func TestService_SPNEGOKRB_ReplayCache_Concurrency(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error setting client's SPNEGO header: %v", err)
 	}
+	r2h := r2.Header.Get(HTTPHeaderAuthRequest)
 
 	// Concurrent 1st requests should be OK
 	var wg sync.WaitGroup
@@ -241,8 +246,12 @@ func TestService_SPNEGOKRB_ReplayCache_Concurrency(t *testing.T) {
 	noReq := 10
 	wg2.Add(noReq * 2)
 	for i := 0; i < noReq; i++ {
-		go httpGet(r1, &wg2)
-		go httpGet(r2, &wg2)
+		rr1, _ := http.NewRequest("GET", s.URL, nil)
+		rr1.Header.Set(HTTPHeaderAuthRequest, r1h)
+		rr2, _ := http.NewRequest("GET", s.URL, nil)
+		rr2.Header.Set(HTTPHeaderAuthRequest, r2h)
+		go httpGet(rr1, &wg2)
+		go httpGet(rr2, &wg2)
 	}
 	wg2.Wait()
 }
@@ -274,7 +283,10 @@ func TestService_SPNEGOKRB_Upload(t *testing.T) {
 	r.Header.Set("Content-Type", bodyWriter.FormDataContentType())
 
 	cl := getClient()
-	spnegoCl := NewClient(cl, nil, "HTTP/host.test.gokrb5")
+	cookieJar, _ := cookiejar.New(nil)
+	httpCl := http.DefaultClient
+	httpCl.Jar = cookieJar
+	spnegoCl := NewClient(cl, httpCl, "HTTP/host.test.gokrb5")
 	httpResp, err := spnegoCl.Do(r)
 	if err != nil {
 		t.Fatalf("Request error: %v\n", err)
@@ -292,8 +304,8 @@ func httpGet(r *http.Request, wg *sync.WaitGroup) {
 	http.DefaultClient.Do(r)
 }
 
-func httpServer() *httptest.Server {
-	l := log.New(os.Stderr, "GOKRB5 Service Tests: ", log.Ldate|log.Ltime|log.Lshortfile)
+func httpServerWithoutSessionManager() *httptest.Server {
+	l := log.New(os.Stderr, "GOKRB5 Service Tests: ", log.LstdFlags)
 	b, _ := hex.DecodeString(testdata.HTTP_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
@@ -302,6 +314,16 @@ func httpServer() *httptest.Server {
 	return s
 }
 
+func httpServer() *httptest.Server {
+	l := log.New(os.Stderr, "GOKRB5 Service Tests: ", log.LstdFlags)
+	b, _ := hex.DecodeString(testdata.HTTP_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	th := http.HandlerFunc(testAppHandler)
+	s := httptest.NewServer(SPNEGOKRB5Authenticate(th, kt, service.Logger(l), service.SessionManager(NewSessionMgr("gokrb5"))))
+	return s
+}
+
 func testAppHandler(w http.ResponseWriter, r *http.Request) {
 	if r.Method == http.MethodPost {
 		maxUploadSize := int64(11240)
@@ -325,10 +347,10 @@ func testAppHandler(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 	w.WriteHeader(http.StatusOK)
-	ctx := r.Context()
+	id := goidentity.FromHTTPRequestContext(r)
 	fmt.Fprintf(w, "<html>\nTEST.GOKRB5 Handler\nAuthenticed user: %s\nUser's realm: %s\n</html>",
-		ctx.Value(CTXKeyCredentials).(goidentity.Identity).UserName(),
-		ctx.Value(CTXKeyCredentials).(goidentity.Identity).Domain())
+		id.UserName(),
+		id.Domain())
 	return
 }
 
@@ -336,7 +358,7 @@ func getClient() *client.Client {
 	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
 	kt := keytab.New()
 	kt.Unmarshal(b)
-	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
 	c.LibDefaults.NoAddresses = true
 	addr := os.Getenv("TEST_KDC_ADDR")
 	if addr == "" {
@@ -344,6 +366,45 @@ func getClient() *client.Client {
 	}
 	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
 	c.Realms[0].KPasswdServer = []string{addr + ":464"}
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
 	return cl
 }
+
+type SessionMgr struct {
+	skey       []byte
+	store      sessions.Store
+	cookieName string
+}
+
+func NewSessionMgr(cookieName string) SessionMgr {
+	skey := []byte("thisistestsecret") // Best practice is to load this key from a secure location.
+	return SessionMgr{
+		skey:       skey,
+		store:      sessions.NewCookieStore(skey),
+		cookieName: cookieName,
+	}
+}
+
+func (smgr SessionMgr) Get(r *http.Request, k string) ([]byte, error) {
+	s, err := smgr.store.Get(r, smgr.cookieName)
+	if err != nil {
+		return nil, err
+	}
+	if s == nil {
+		return nil, errors.New("nil session")
+	}
+	b, ok := s.Values[k].([]byte)
+	if !ok {
+		return nil, fmt.Errorf("could not get bytes held in session at %s", k)
+	}
+	return b, nil
+}
+
+func (smgr SessionMgr) New(w http.ResponseWriter, r *http.Request, k string, v []byte) error {
+	s, err := smgr.store.New(r, smgr.cookieName)
+	if err != nil {
+		return fmt.Errorf("could not get new session from session manager: %v", err)
+	}
+	s.Values[k] = v
+	return s.Save(r, w)
+}

+ 5 - 6
spnego/krb5Token.go

@@ -70,8 +70,8 @@ func (m *KRB5Token) Unmarshal(b []byte) error {
 	if err != nil {
 		return fmt.Errorf("error unmarshalling KRB5Token OID: %v", err)
 	}
-	if !oid.Equal(gssapi.OID(gssapi.OIDKRB5)) {
-		return fmt.Errorf("error unmarshalling KRB5Token, OID is %s not %s", oid.String(), gssapi.OID(gssapi.OIDKRB5).String())
+	if !oid.Equal(gssapi.OIDKRB5.OID()) {
+		return fmt.Errorf("error unmarshalling KRB5Token, OID is %s not %s", oid.String(), gssapi.OIDKRB5.OID().String())
 	}
 	m.OID = oid
 	if len(r) < 2 {
@@ -108,7 +108,7 @@ func (m *KRB5Token) Unmarshal(b []byte) error {
 func (m *KRB5Token) Verify() (bool, gssapi.Status) {
 	switch hex.EncodeToString(m.tokID) {
 	case TOK_ID_KRB_AP_REQ:
-		ok, creds, err := service.VerifyAPREQ(m.APReq, m.settings)
+		ok, creds, err := service.VerifyAPREQ(&m.APReq, m.settings)
 		if err != nil {
 			return false, gssapi.Status{Code: gssapi.StatusDefectiveToken, Message: err.Error()}
 		}
@@ -116,8 +116,7 @@ func (m *KRB5Token) Verify() (bool, gssapi.Status) {
 			return false, gssapi.Status{Code: gssapi.StatusDefectiveCredential, Message: "KRB5_AP_REQ token not valid"}
 		}
 		m.context = context.Background()
-		m.context = context.WithValue(m.context, CTXKeyCredentials, creds)
-		m.context = context.WithValue(m.context, CTXKeyAuthenticated, ok)
+		m.context = context.WithValue(m.context, ctxCredentials, creds)
 		return true, gssapi.Status{Code: gssapi.StatusComplete}
 	case TOK_ID_KRB_AP_REP:
 		// Client side
@@ -165,7 +164,7 @@ func (m *KRB5Token) Context() context.Context {
 func NewKRB5TokenAPREQ(cl *client.Client, tkt messages.Ticket, sessionKey types.EncryptionKey, GSSAPIFlags []int, APOptions []int) (KRB5Token, error) {
 	// TODO consider providing the SPN rather than the specific tkt and key and get these from the krb client.
 	var m KRB5Token
-	m.OID = gssapi.OID(gssapi.OIDKRB5)
+	m.OID = gssapi.OIDKRB5.OID()
 	tb, _ := hex.DecodeString(TOK_ID_KRB_AP_REQ)
 	m.tokID = tb
 

+ 1 - 1
spnego/krb5Token_test.go

@@ -33,7 +33,7 @@ func TestKRB5Token_Unmarshal(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Error unmarshalling KRB5Token: %v", err)
 	}
-	assert.Equal(t, gssapi.OID(gssapi.OIDKRB5), mt.OID, "KRB5Token OID not as expected.")
+	assert.Equal(t, gssapi.OIDKRB5.OID(), mt.OID, "KRB5Token OID not as expected.")
 	assert.Equal(t, []byte{1, 0}, mt.tokID, "TokID not as expected")
 	assert.Equal(t, msgtype.KRB_AP_REQ, mt.APReq.MsgType, "KRB5Token AP_REQ does not have the right message type.")
 	assert.Equal(t, int32(0), mt.KRBError.ErrorCode, "KRBError in KRB5Token does not indicate no error.")

+ 3 - 3
spnego/negotiationToken.go

@@ -142,7 +142,7 @@ func (n *NegTokenInit) Verify() (bool, gssapi.Status) {
 	// Check if supported mechanisms are in the MechTypeList
 	var mtSupported bool
 	for _, m := range n.MechTypes {
-		if m.Equal(gssapi.OID(gssapi.OIDKRB5)) || m.Equal(gssapi.OID(gssapi.OIDMSLegacyKRB5)) {
+		if m.Equal(gssapi.OIDKRB5.OID()) || m.Equal(gssapi.OIDMSLegacyKRB5.OID()) {
 			if n.mechToken == nil && n.MechTokenBytes == nil {
 				return false, gssapi.Status{Code: gssapi.StatusContinueNeeded}
 			}
@@ -229,7 +229,7 @@ func (n *NegTokenResp) Unmarshal(b []byte) error {
 
 // Verify a Resp/Targ negotiation token
 func (n *NegTokenResp) Verify() (bool, gssapi.Status) {
-	if n.SupportedMech.Equal(gssapi.OID(gssapi.OIDKRB5)) || n.SupportedMech.Equal(gssapi.OID(gssapi.OIDMSLegacyKRB5)) {
+	if n.SupportedMech.Equal(gssapi.OIDKRB5.OID()) || n.SupportedMech.Equal(gssapi.OIDMSLegacyKRB5.OID()) {
 		if n.mechToken == nil && n.ResponseToken == nil {
 			return false, gssapi.Status{Code: gssapi.StatusContinueNeeded}
 		}
@@ -328,7 +328,7 @@ func NewNegTokenInitKRB5(cl *client.Client, tkt messages.Ticket, sessionKey type
 		return NegTokenInit{}, fmt.Errorf("error marshalling KRB5 token; %v", err)
 	}
 	return NegTokenInit{
-		MechTypes:      []asn1.ObjectIdentifier{gssapi.OID(gssapi.OIDKRB5)},
+		MechTypes:      []asn1.ObjectIdentifier{gssapi.OIDKRB5.OID()},
 		MechTokenBytes: mtb,
 	}, nil
 }

+ 5 - 5
spnego/spnego.go

@@ -39,12 +39,12 @@ func SPNEGOService(kt *keytab.Keytab, options ...func(*service.Settings)) *SPNEG
 
 // OID returns the GSS-API assigned OID for SPNEGO.
 func (s *SPNEGO) OID() asn1.ObjectIdentifier {
-	return gssapi.OID(gssapi.OIDSPNEGO)
+	return gssapi.OIDSPNEGO.OID()
 }
 
 // AcquireCred is the GSS-API method to acquire a client credential via Kerberos for SPNEGO.
 func (s *SPNEGO) AcquireCred() error {
-	return s.client.Login()
+	return s.client.AffirmLogin()
 }
 
 // InitSecContext is the GSS-API method for the client to a generate a context token to the service via Kerberos.
@@ -80,7 +80,7 @@ func (s *SPNEGO) AcceptSecContext(ct gssapi.ContextToken) (bool, context.Context
 	if t.Resp {
 		oid = t.NegTokenResp.SupportedMech
 	}
-	if !(oid.Equal(gssapi.OID(gssapi.OIDKRB5)) || oid.Equal(gssapi.OID(gssapi.OIDMSLegacyKRB5))) {
+	if !(oid.Equal(gssapi.OIDKRB5.OID()) || oid.Equal(gssapi.OIDMSLegacyKRB5.OID())) {
 		return false, ctx, gssapi.Status{Code: gssapi.StatusDefectiveToken, Message: "SPNEGO OID of MechToken is not of type KRB5"}
 	}
 	// Flags in the NegInit must be used 	t.NegTokenInit.ReqFlags
@@ -110,7 +110,7 @@ type SPNEGOToken struct {
 func (s *SPNEGOToken) Marshal() ([]byte, error) {
 	var b []byte
 	if s.Init {
-		hb, _ := asn1.Marshal(gssapi.OID(gssapi.OIDSPNEGO))
+		hb, _ := asn1.Marshal(gssapi.OIDSPNEGO.OID())
 		tb, err := s.NegTokenInit.Marshal()
 		if err != nil {
 			return b, fmt.Errorf("could not marshal NegTokenInit: %v", err)
@@ -144,7 +144,7 @@ func (s *SPNEGOToken) Unmarshal(b []byte) error {
 			return fmt.Errorf("not a valid SPNEGO token: %v", err)
 		}
 		// Check the OID is the SPNEGO OID value
-		SPNEGOOID := gssapi.OID(gssapi.OIDSPNEGO)
+		SPNEGOOID := gssapi.OIDSPNEGO.OID()
 		if !oid.Equal(SPNEGOOID) {
 			return fmt.Errorf("OID %s does not match SPNEGO OID %s", oid.String(), SPNEGOOID.String())
 		}

+ 300 - 0
v8/README.md

@@ -0,0 +1,300 @@
+# gokrb5
+[![Version](https://img.shields.io/github/release/jcmturner/gokrb5.svg)](https://github.com/jcmturner/gokrb5/releases)
+
+[![GoDoc](https://godoc.org/github.com/jcmturner/gokrb5?status.svg)](https://godoc.org/github.com/jcmturner/gokrb5) [![Go Report Card](https://goreportcard.com/badge/github.com/jcmturner/gokrb5)](https://goreportcard.com/report/github.com/jcmturner/gokrb5) 
+
+[![Build Status](https://github.com/jcmturner/gokrb5/workflows/gokrb5/badge.svg)](https://github.com/jcmturner/gokrb5/actions)
+
+#### Go Version Support
+![Go version](https://img.shields.io/badge/Go-1.13-brightgreen.svg)
+![Go version](https://img.shields.io/badge/Go-1.12-brightgreen.svg)
+![Go version](https://img.shields.io/badge/Go-1.11-brightgreen.svg)
+
+gokrb5 may work with other versions of Go but they are not tested.
+
+### Go Get
+To get the package, execute:
+```
+go get -d github.com/jcmturner/gokrb5/...
+```
+To import this package, add the following line to your code:
+```go
+import "github.com/jcmturner/gokrb5/v8/<sub package>"
+```
+
+## Features
+* **Pure Go** - no dependency on external libraries 
+* No platform specific code
+* Server Side
+  * HTTP handler wrapper implements SPNEGO Kerberos authentication
+  * HTTP handler wrapper decodes Microsoft AD PAC authorization data
+* Client Side
+  * Client that can authenticate to an SPNEGO Kerberos authenticated web service
+  * Ability to change client's password
+* General
+  * Kerberos libraries for custom integration
+  * Parsing Keytab files
+  * Parsing krb5.conf files
+  * Parsing client credentials cache files such as `/tmp/krb5cc_$(id -u $(whoami))`
+
+#### Implemented Encryption & Checksum Types
+
+| Implementation | Encryption ID | Checksum ID | RFC |
+|-------|-------------|------------|------|
+| des3-cbc-sha1-kd | 16 | 12 | 3961 |
+| aes128-cts-hmac-sha1-96 | 17 | 15 | 3962 |
+| aes256-cts-hmac-sha1-96 | 18 | 16 | 3962 |
+| aes128-cts-hmac-sha256-128 | 19 | 19 | 8009 |
+| aes256-cts-hmac-sha384-192 | 20 | 20 | 8009 |
+| rc4-hmac | 23 | -138 | 4757 |
+
+
+The following is working/tested:
+* Tested against MIT KDC (1.6.3 is the oldest version tested against) and Microsoft Active Directory (Windows 2008 R2)
+* Tested against a KDC that supports PA-FX-FAST.
+* Tested against users that have pre-authentication required using PA-ENC-TIMESTAMP.
+* Microsoft PAC Authorization Data is processed and exposed in the HTTP request context. Available if Microsoft Active Directory is used as the KDC.
+
+## Contributing
+If you are interested in contributing to gokrb5, great! Please read the [contribution guidelines](https://github.com/jcmturner/gokrb5/blob/master/CONTRIBUTING.md).
+
+## Usage
+
+---
+
+### Configuration
+The gokrb5 libraries use the same krb5.conf configuration file format as MIT Kerberos, described [here](https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html).
+Config instances can be created by loading from a file path or by passing a string, io.Reader or bufio.Scanner to the relevant method:
+```go
+import "github.com/jcmturner/gokrb5/v8/config"
+cfg, err := config.Load("/path/to/config/file")
+cfg, err := config.NewFromString(krb5Str) //String must have appropriate newline separations
+cfg, err := config.NewFromReader(reader)
+cfg, err := config.NewFromScanner(scanner)
+```
+### Keytab files
+Standard keytab files can be read from a file or from a slice of bytes:
+```go
+import 	"github.com/jcmturner/gokrb5/v8/keytab"
+ktFromFile, err := keytab.Load("/path/to/file.keytab")
+ktFromBytes, err := keytab.Parse(b)
+
+```
+
+---
+
+### Kerberos Client
+**Create** a client instance with either a password or a keytab.
+A configuration must also be passed. Additionally optional additional settings can be provided.
+```go
+import 	"github.com/jcmturner/gokrb5/v8/client"
+cl := client.NewClientWithPassword("username", "REALM.COM", "password", cfg)
+cl := client.NewWithKeytab("username", "REALM.COM", kt, cfg)
+```
+Optional settings are provided using the functions defined in the ``client/settings.go`` source file.
+
+**Login**:
+```go
+err := cl.Login()
+```
+Kerberos Ticket Granting Tickets (TGT) will be automatically renewed unless the client was created from a CCache.
+
+A client can be **destroyed** with the following method:
+```go
+cl.Destroy()
+```
+
+#### Active Directory KDC and FAST negotiation
+Active Directory does not commonly support FAST negotiation so you will need to disable this on the client.
+If this is the case you will see this error:
+```KDC did not respond appropriately to FAST negotiation```
+To resolve this disable PA-FX-Fast on the client before performing Login().
+This is done with one of the optional client settings as shown below:
+```go
+cl := client.NewClientWithPassword("username", "REALM.COM", "password", cfg, client.DisablePAFXFAST(true))
+```
+
+#### Authenticate to a Service
+
+##### HTTP SPNEGO
+Create the HTTP request object and then create an SPNEGO client and use this to process the request with methods that 
+are the same as on a HTTP client.
+If nil is passed as the HTTP client when creating the SPNEGO client the http.DefaultClient is used.
+When creating the SPNEGO client pass the Service Principal Name (SPN) or auto generate the SPN from the request 
+object by passing a null string "".
+```go
+r, _ := http.NewRequest("GET", "http://host.test.gokrb5/index.html", nil)
+spnegoCl := spnego.NewClient(cl, nil, "")
+resp, err := spnegoCl.Do(r)
+```
+
+##### Generic Kerberos Client
+To authenticate to a service a client will need to request a service ticket for a Service Principal Name (SPN) and form into an AP_REQ message along with an authenticator encrypted with the session key that was delivered from the KDC along with the service ticket.
+
+The steps below outline how to do this.
+* Get the service ticket and session key for the service the client is authenticating to.
+The following method will use the client's cache either returning a valid cached ticket, renewing a cached ticket with the KDC or requesting a new ticket from the KDC.
+Therefore the GetServiceTicket method can be continually used for the most efficient interaction with the KDC.
+```go
+tkt, key, err := cl.GetServiceTicket("HTTP/host.test.gokrb5")
+```
+
+The steps after this will be specific to the application protocol but it will likely involve a client/server Authentication Protocol exchange (AP exchange).
+This will involve these steps:
+
+* Generate a new Authenticator and generate a sequence number and subkey:
+```go
+auth, _ := types.NewAuthenticator(cl.Credentials.Realm, cl.Credentials.CName)
+etype, _ := crypto.GetEtype(key.KeyType)
+auth.GenerateSeqNumberAndSubKey(key.KeyType, etype.GetKeyByteSize())
+```
+* Set the checksum on the authenticator
+The checksum is an application specific value. Set as follows:
+```go
+auth.Cksum = types.Checksum{
+		CksumType: checksumIDint,
+		Checksum:  checksumBytesSlice,
+	}
+```
+* Create the AP_REQ:
+```go
+APReq, err := messages.NewAPReq(tkt, key, auth)
+```
+
+Now send the AP_REQ to the service. How this is done will be specific to the application use case.
+
+#### Changing a Client Password
+This feature uses the Microsoft Kerberos Password Change protocol (RFC 3244). 
+This is implemented in Microsoft Active Directory and in MIT krb5kdc as of version 1.7.
+Typically the kpasswd server listens on port 464.
+
+Below is example code for how to use this feature:
+```go
+cfg, err := config.Load("/path/to/config/file")
+if err != nil {
+	panic(err.Error())
+}
+kt, err := keytab.Load("/path/to/file.keytab")
+if err != nil {
+	panic(err.Error())
+}
+cl := client.NewWithKeytab("username", "REALM.COM", kt)
+cl.WithConfig(cfg)
+
+ok, err := cl.ChangePasswd("newpassword")
+if err != nil {
+	panic(err.Error())
+}
+if !ok {
+	panic("failed to change password")
+}
+```
+
+The client kerberos config (krb5.conf) will need to have either the kpassd_server or admin_server defined in the relevant [realms] section.
+For example:
+```
+REALM.COM = {
+  kdc = 127.0.0.1:88
+  kpasswd_server = 127.0.0.1:464
+  default_domain = realm.com
+ }
+```
+See https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html#realms for more information.
+
+---
+
+### Kerberised Service
+
+#### SPNEGO/Kerberos HTTP Service
+A HTTP handler wrapper can be used to implement Kerberos SPNEGO authentication for web services.
+To configure the wrapper the keytab for the SPN and a Logger are required:
+```go
+kt, err := keytab.Load("/path/to/file.keytab")
+l := log.New(os.Stderr, "GOKRB5 Service: ", log.Ldate|log.Ltime|log.Lshortfile)
+```
+Create a handler function of the application's handling method (apphandler in the example below):
+```go
+h := http.HandlerFunc(apphandler)
+```
+Configure the HTTP handler:
+```go
+http.Handler("/", spnego.SPNEGOKRB5Authenticate(h, &kt, service.Logger(l)))
+```
+The handler to be wrapped and the keytab are required arguments. 
+Additional optional settings can be provided, such as the logger shown above.
+
+Another example of optional settings may be that when using Active Directory where the SPN is mapped to a user account 
+the keytab may contain an entry for this user account. In this case this should be specified as below with the ``KeytabPrincipal``:
+```go
+http.Handler("/", spnego.SPNEGOKRB5Authenticate(h, &kt, service.Logger(l), service.KeytabPrincipal(pn)))
+```
+
+If authentication succeeds then the request's context will have the following values added so they can be accessed within the application's handler:
+* spnego.CTXKeyAuthenticated - Boolean indicating if the user is authenticated. Use of this value should also handle that this value may not be set and should assume "false" in that case.
+* spnego.CTXKeyCredentials - The authenticated user's credentials.
+If Microsoft Active Directory is used as the KDC then additional ADCredentials are available in the credentials.Attributes map under the key credentials.AttributeKeyADCredentials. For example the SIDs of the users group membership are available and can be used by your application for authorization.
+
+Access the credentials within your application:
+```go
+ctx := r.Context()
+if validuser, ok := ctx.Value(spnego.CTXKeyAuthenticated).(bool); ok && validuser {
+        if creds, ok := ctx.Value(spnego.CTXKeyCredentials).(goidentity.Identity); ok {
+                if ADCreds, ok := creds.Attributes()[credentials.AttributeKeyADCredentials].(credentials.ADCredentials); ok {
+                        // Now access the fields of the ADCredentials struct. For example:
+                        groupSids := ADCreds.GroupMembershipSIDs
+                }
+        } 
+
+}
+```
+
+#### Generic Kerberised Service - Validating Client Details
+To validate the AP_REQ sent by the client on the service side call this method:
+```go
+import 	"github.com/jcmturner/gokrb5/v8/service"
+s := service.NewSettings(&kt) // kt is a keytab and optional settings can also be provided.
+if ok, creds, err := service.VerifyAPREQ(APReq, s); ok {
+        // Perform application specific actions
+        // creds object has details about the client identity
+}
+```
+
+---
+
+## References
+* [RFC 3244 Microsoft Windows 2000 Kerberos Change Password and Set Password Protocols](https://tools.ietf.org/html/rfc3244)
+* [RFC 4120 The Kerberos Network Authentication Service (V5)](https://tools.ietf.org/html/rfc4120)
+* [RFC 3961 Encryption and Checksum Specifications for Kerberos 5](https://tools.ietf.org/html/rfc3961)
+* [RFC 3962 Advanced Encryption Standard (AES) Encryption for Kerberos 5](https://tools.ietf.org/html/rfc3962)
+* [RFC 4121 The Kerberos Version 5 GSS-API Mechanism](https://tools.ietf.org/html/rfc4121)
+* [RFC 4178 The Simple and Protected Generic Security Service Application Program Interface (GSS-API) Negotiation Mechanism](https://tools.ietf.org/html/rfc4178.html)
+* [RFC 4559 SPNEGO-based Kerberos and NTLM HTTP Authentication in Microsoft Windows](https://tools.ietf.org/html/rfc4559.html)
+* [RFC 4757 The RC4-HMAC Kerberos Encryption Types Used by Microsoft Windows](https://tools.ietf.org/html/rfc4757)
+* [RFC 6806 Kerberos Principal Name Canonicalization and Cross-Realm Referrals](https://tools.ietf.org/html/rfc6806.html)
+* [RFC 6113 A Generalized Framework for Kerberos Pre-Authentication](https://tools.ietf.org/html/rfc6113.html)
+* [RFC 8009 AES Encryption with HMAC-SHA2 for Kerberos 5](https://tools.ietf.org/html/rfc8009)
+* [IANA Assigned Kerberos Numbers](http://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xhtml)
+* [HTTP-Based Cross-Platform Authentication by Using the Negotiate Protocol - Part 1](https://msdn.microsoft.com/en-us/library/ms995329.aspx)
+* [HTTP-Based Cross-Platform Authentication by Using the Negotiate Protocol - Part 2](https://msdn.microsoft.com/en-us/library/ms995330.aspx)
+* [Microsoft PAC Validation](https://blogs.msdn.microsoft.com/openspecification/2009/04/24/understanding-microsoft-kerberos-pac-validation/)
+* [Microsoft Kerberos Protocol Extensions](https://msdn.microsoft.com/en-us/library/cc233855.aspx)
+* [Windows Data Types](https://msdn.microsoft.com/en-us/library/cc230273.aspx)
+
+### Useful Links
+* https://en.wikipedia.org/wiki/Ciphertext_stealing#CBC_ciphertext_stealing
+
+## Thanks
+* Greg Hudson from the MIT Consortium for Kerberos and Internet Trust for providing useful advice.
+
+## Contributing
+Thank you for your interest in contributing to gokrb5 please read the 
+[contribution guide](https://github.com/jcmturner/gokrb5/blob/master/CONTRIBUTING.md) as it should help you get started.
+
+## Known Issues
+| Issue | Worked around? | References |
+|-------|-------------|------------|
+| The Go standard library's encoding/asn1 package cannot unmarshal into slice of asn1.RawValue | Yes | https://github.com/golang/go/issues/17321 |
+| The Go standard library's encoding/asn1 package cannot marshal into a GeneralString | Yes - using https://github.com/jcmturner/gofork/tree/master/encoding/asn1 | https://github.com/golang/go/issues/18832 |
+| The Go standard library's encoding/asn1 package cannot marshal into slice of strings and pass stringtype parameter tags to members | Yes - using https://github.com/jcmturner/gofork/tree/master/encoding/asn1 | https://github.com/golang/go/issues/18834 |
+| The Go standard library's encoding/asn1 package cannot marshal with application tags | Yes | |
+| The Go standard library's x/crypto/pbkdf2.Key function uses the int type for iteraction count limiting meaning the 4294967296 count specified in https://tools.ietf.org/html/rfc3962 section 4 cannot be met on 32bit systems | Yes - using https://github.com/jcmturner/gofork/tree/master/x/crypto/pbkdf2 | https://go-review.googlesource.com/c/crypto/+/85535 |

+ 2 - 0
v8/USAGE.md

@@ -0,0 +1,2 @@
+# gokrb5 Usage
+

+ 86 - 0
v8/asn1tools/tools.go

@@ -0,0 +1,86 @@
+// Package asn1tools provides tools for managing ASN1 marshaled data.
+package asn1tools
+
+import (
+	"github.com/jcmturner/gofork/encoding/asn1"
+)
+
+// MarshalLengthBytes returns the ASN1 encoded bytes for the length 'l'
+//
+// There are two forms: short (for lengths between 0 and 127), and long definite (for lengths between 0 and 2^1008 -1).
+//
+// Short form: One octet. Bit 8 has value "0" and bits 7-1 give the length.
+//
+// Long form: Two to 127 octets. Bit 8 of first octet has value "1" and bits 7-1 give the number of additional length octets. Second and following octets give the length, base 256, most significant digit first.
+func MarshalLengthBytes(l int) []byte {
+	if l <= 127 {
+		return []byte{byte(l)}
+	}
+	var b []byte
+	p := 1
+	for i := 1; i < 127; {
+		b = append([]byte{byte((l % (p * 256)) / p)}, b...)
+		p = p * 256
+		l = l - l%p
+		if l <= 0 {
+			break
+		}
+	}
+	return append([]byte{byte(128 + len(b))}, b...)
+}
+
+// GetLengthFromASN returns the length of a slice of ASN1 encoded bytes from the ASN1 length header it contains.
+func GetLengthFromASN(b []byte) int {
+	if int(b[1]) <= 127 {
+		return int(b[1])
+	}
+	// The bytes that indicate the length
+	lb := b[2 : 2+int(b[1])-128]
+	base := 1
+	l := 0
+	for i := len(lb) - 1; i >= 0; i-- {
+		l += int(lb[i]) * base
+		base = base * 256
+	}
+	return l
+}
+
+// GetNumberBytesInLengthHeader returns the number of bytes in the ASn1 header that indicate the length.
+func GetNumberBytesInLengthHeader(b []byte) int {
+	if int(b[1]) <= 127 {
+		return 1
+	}
+	// The bytes that indicate the length
+	return 1 + int(b[1]) - 128
+}
+
+// AddASNAppTag adds an ASN1 encoding application tag value to the raw bytes provided.
+func AddASNAppTag(b []byte, tag int) []byte {
+	r := asn1.RawValue{
+		Class:      asn1.ClassApplication,
+		IsCompound: true,
+		Tag:        tag,
+		Bytes:      b,
+	}
+	ab, _ := asn1.Marshal(r)
+	return ab
+}
+
+/*
+// The Marshal method of golang's asn1 package does not enable you to define wrapping the output in an application tag.
+// This method adds that wrapping tag.
+func AddASNAppTag(b []byte, tag int) []byte {
+	// The ASN1 wrapping consists of 2 bytes:
+	// 1st byte -> Identifier Octet - Application Tag
+	// 2nd byte -> The length (this will be the size indicated in the input bytes + 2 for the additional bytes we add here.
+	// Application Tag:
+	//| Bit:        | 8                            | 7                          | 6                                         | 5 | 4 | 3 | 2 | 1             |
+	//| Value:      | 0                            | 1                          | 1                                         | From the RFC spec 4120        |
+	//| Explanation | Defined by the ASN1 encoding rules for an application tag | A value of 1 indicates a constructed type | The ASN Application tag value |
+	// Therefore the value of the byte is an integer = ( Application tag value + 96 )
+	//b = append(MarshalLengthBytes(int(b[1])+2), b...)
+	b = append(MarshalLengthBytes(len(b)), b...)
+	b = append([]byte{byte(96 + tag)}, b...)
+	return b
+}
+*/

+ 189 - 0
v8/client/ASExchange.go

@@ -0,0 +1,189 @@
+package client
+
+import (
+	"github.com/jcmturner/gokrb5/v8/crypto"
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+	"github.com/jcmturner/gokrb5/v8/iana/errorcode"
+	"github.com/jcmturner/gokrb5/v8/iana/keyusage"
+	"github.com/jcmturner/gokrb5/v8/iana/patype"
+	"github.com/jcmturner/gokrb5/v8/krberror"
+	"github.com/jcmturner/gokrb5/v8/messages"
+	"github.com/jcmturner/gokrb5/v8/types"
+)
+
+// ASExchange performs an AS exchange for the client to retrieve a TGT.
+func (cl *Client) ASExchange(realm string, ASReq messages.ASReq, referral int) (messages.ASRep, error) {
+	if ok, err := cl.IsConfigured(); !ok {
+		return messages.ASRep{}, krberror.Errorf(err, krberror.ConfigError, "AS Exchange cannot be performed")
+	}
+
+	// Set PAData if required
+	err := setPAData(cl, nil, &ASReq)
+	if err != nil {
+		return messages.ASRep{}, krberror.Errorf(err, krberror.KRBMsgError, "AS Exchange Error: issue with setting PAData on AS_REQ")
+	}
+
+	b, err := ASReq.Marshal()
+	if err != nil {
+		return messages.ASRep{}, krberror.Errorf(err, krberror.EncodingError, "AS Exchange Error: failed marshaling AS_REQ")
+	}
+	var ASRep messages.ASRep
+
+	rb, err := cl.sendToKDC(b, realm)
+	if err != nil {
+		if e, ok := err.(messages.KRBError); ok {
+			switch e.ErrorCode {
+			case errorcode.KDC_ERR_PREAUTH_REQUIRED, errorcode.KDC_ERR_PREAUTH_FAILED:
+				// From now on assume this client will need to do this pre-auth and set the PAData
+				cl.settings.assumePreAuthentication = true
+				err = setPAData(cl, &e, &ASReq)
+				if err != nil {
+					return messages.ASRep{}, krberror.Errorf(err, krberror.KRBMsgError, "AS Exchange Error: failed setting AS_REQ PAData for pre-authentication required")
+				}
+				b, err := ASReq.Marshal()
+				if err != nil {
+					return messages.ASRep{}, krberror.Errorf(err, krberror.EncodingError, "AS Exchange Error: failed marshaling AS_REQ with PAData")
+				}
+				rb, err = cl.sendToKDC(b, realm)
+				if err != nil {
+					if _, ok := err.(messages.KRBError); ok {
+						return messages.ASRep{}, krberror.Errorf(err, krberror.KDCError, "AS Exchange Error: kerberos error response from KDC")
+					}
+					return messages.ASRep{}, krberror.Errorf(err, krberror.NetworkingError, "AS Exchange Error: failed sending AS_REQ to KDC")
+				}
+			case errorcode.KDC_ERR_WRONG_REALM:
+				// Client referral https://tools.ietf.org/html/rfc6806.html#section-7
+				if referral > 5 {
+					return messages.ASRep{}, krberror.Errorf(err, krberror.KRBMsgError, "maximum number of client referrals exceeded")
+				}
+				referral++
+				return cl.ASExchange(e.CRealm, ASReq, referral)
+			default:
+				return messages.ASRep{}, krberror.Errorf(err, krberror.KDCError, "AS Exchange Error: kerberos error response from KDC")
+			}
+		} else {
+			return messages.ASRep{}, krberror.Errorf(err, krberror.NetworkingError, "AS Exchange Error: failed sending AS_REQ to KDC")
+		}
+	}
+	err = ASRep.Unmarshal(rb)
+	if err != nil {
+		return messages.ASRep{}, krberror.Errorf(err, krberror.EncodingError, "AS Exchange Error: failed to process the AS_REP")
+	}
+	if ok, err := ASRep.Verify(cl.Config, cl.Credentials, ASReq); !ok {
+		return messages.ASRep{}, krberror.Errorf(err, krberror.KRBMsgError, "AS Exchange Error: AS_REP is not valid or client password/keytab incorrect")
+	}
+	return ASRep, nil
+}
+
+// setPAData adds pre-authentication data to the AS_REQ.
+func setPAData(cl *Client, krberr *messages.KRBError, ASReq *messages.ASReq) error {
+	if !cl.settings.DisablePAFXFAST() {
+		pa := types.PAData{PADataType: patype.PA_REQ_ENC_PA_REP}
+		ASReq.PAData = append(ASReq.PAData, pa)
+	}
+	if cl.settings.AssumePreAuthentication() {
+		// Identify the etype to use to encrypt the PA Data
+		var et etype.EType
+		var err error
+		var key types.EncryptionKey
+		kvno := 1
+		if krberr == nil {
+			// This is not in response to an error from the KDC. It is preemptive or renewal
+			// There is no KRB Error that tells us the etype to use
+			etn := cl.settings.preAuthEType // Use the etype that may have previously been negotiated
+			if etn == 0 {
+				etn = int32(cl.Config.LibDefaults.PreferredPreauthTypes[0]) // Resort to config
+			}
+			et, err = crypto.GetEtype(etn)
+			if err != nil {
+				return krberror.Errorf(err, krberror.EncryptingError, "error getting etype for pre-auth encryption")
+			}
+			key, kvno, err = cl.Key(et, 0, nil)
+			if err != nil {
+				return krberror.Errorf(err, krberror.EncryptingError, "error getting key from credentials")
+			}
+		} else {
+			// Get the etype to use from the PA data in the KRBError e-data
+			et, err = preAuthEType(krberr)
+			if err != nil {
+				return krberror.Errorf(err, krberror.EncryptingError, "error getting etype for pre-auth encryption")
+			}
+			cl.settings.preAuthEType = et.GetETypeID() // Set the etype that has been defined for potential future use
+			key, kvno, err = cl.Key(et, 0, krberr)
+			if err != nil {
+				return krberror.Errorf(err, krberror.EncryptingError, "error getting key from credentials")
+			}
+		}
+		// Generate the PA data
+		paTSb, err := types.GetPAEncTSEncAsnMarshalled()
+		if err != nil {
+			return krberror.Errorf(err, krberror.KRBMsgError, "error creating PAEncTSEnc for Pre-Authentication")
+		}
+		paEncTS, err := crypto.GetEncryptedData(paTSb, key, keyusage.AS_REQ_PA_ENC_TIMESTAMP, kvno)
+		if err != nil {
+			return krberror.Errorf(err, krberror.EncryptingError, "error encrypting pre-authentication timestamp")
+		}
+		pb, err := paEncTS.Marshal()
+		if err != nil {
+			return krberror.Errorf(err, krberror.EncodingError, "error marshaling the PAEncTSEnc encrypted data")
+		}
+		pa := types.PAData{
+			PADataType:  patype.PA_ENC_TIMESTAMP,
+			PADataValue: pb,
+		}
+		// Look for and delete any exiting patype.PA_ENC_TIMESTAMP
+		for i, pa := range ASReq.PAData {
+			if pa.PADataType == patype.PA_ENC_TIMESTAMP {
+				ASReq.PAData[i] = ASReq.PAData[len(ASReq.PAData)-1]
+				ASReq.PAData = ASReq.PAData[:len(ASReq.PAData)-1]
+			}
+		}
+		ASReq.PAData = append(ASReq.PAData, pa)
+	}
+	return nil
+}
+
+// preAuthEType establishes what encryption type to use for pre-authentication from the KRBError returned from the KDC.
+func preAuthEType(krberr *messages.KRBError) (etype etype.EType, err error) {
+	//The preferred ordering of the "hint" pre-authentication data that
+	//affect client key selection is: ETYPE-INFO2, followed by ETYPE-INFO,
+	//followed by PW-SALT.
+	//A KDC SHOULD NOT send PA-PW-SALT when issuing a KRB-ERROR message
+	//that requests additional pre-authentication.  Implementation note:
+	//Some KDC implementations issue an erroneous PA-PW-SALT when issuing a
+	//KRB-ERROR message that requests additional pre-authentication.
+	//Therefore, clients SHOULD ignore a PA-PW-SALT accompanying a
+	//KRB-ERROR message that requests additional pre-authentication.
+	var etypeID int32
+	var pas types.PADataSequence
+	e := pas.Unmarshal(krberr.EData)
+	if e != nil {
+		err = krberror.Errorf(e, krberror.EncodingError, "error unmashalling KRBError data")
+		return
+	}
+	for _, pa := range pas {
+		switch pa.PADataType {
+		case patype.PA_ETYPE_INFO2:
+			info, e := pa.GetETypeInfo2()
+			if e != nil {
+				err = krberror.Errorf(e, krberror.EncodingError, "error unmashalling ETYPE-INFO2 data")
+				return
+			}
+			etypeID = info[0].EType
+			break
+		case patype.PA_ETYPE_INFO:
+			info, e := pa.GetETypeInfo()
+			if e != nil {
+				err = krberror.Errorf(e, krberror.EncodingError, "error unmashalling ETYPE-INFO data")
+				return
+			}
+			etypeID = info[0].EType
+		}
+	}
+	etype, e = crypto.GetEtype(etypeID)
+	if e != nil {
+		err = krberror.Errorf(e, krberror.EncryptingError, "error creating etype")
+		return
+	}
+	return etype, nil
+}

+ 103 - 0
v8/client/TGSExchange.go

@@ -0,0 +1,103 @@
+package client
+
+import (
+	"github.com/jcmturner/gokrb5/v8/iana/flags"
+	"github.com/jcmturner/gokrb5/v8/iana/nametype"
+	"github.com/jcmturner/gokrb5/v8/krberror"
+	"github.com/jcmturner/gokrb5/v8/messages"
+	"github.com/jcmturner/gokrb5/v8/types"
+)
+
+// TGSREQGenerateAndExchange generates the TGS_REQ and performs a TGS exchange to retrieve a ticket to the specified SPN.
+func (cl *Client) TGSREQGenerateAndExchange(spn types.PrincipalName, kdcRealm string, tgt messages.Ticket, sessionKey types.EncryptionKey, renewal bool) (tgsReq messages.TGSReq, tgsRep messages.TGSRep, err error) {
+	tgsReq, err = messages.NewTGSReq(cl.Credentials.CName(), kdcRealm, cl.Config, tgt, sessionKey, spn, renewal)
+	if err != nil {
+		return tgsReq, tgsRep, krberror.Errorf(err, krberror.KRBMsgError, "TGS Exchange Error: failed to generate a new TGS_REQ")
+	}
+	return cl.TGSExchange(tgsReq, kdcRealm, tgsRep.Ticket, sessionKey, 0)
+}
+
+// TGSExchange exchanges the provided TGS_REQ with the KDC to retrieve a TGS_REP.
+// Referrals are automatically handled.
+// The client's cache is updated with the ticket received.
+func (cl *Client) TGSExchange(tgsReq messages.TGSReq, kdcRealm string, tgt messages.Ticket, sessionKey types.EncryptionKey, referral int) (messages.TGSReq, messages.TGSRep, error) {
+	var tgsRep messages.TGSRep
+	b, err := tgsReq.Marshal()
+	if err != nil {
+		return tgsReq, tgsRep, krberror.Errorf(err, krberror.EncodingError, "TGS Exchange Error: failed to marshal TGS_REQ")
+	}
+	r, err := cl.sendToKDC(b, kdcRealm)
+	if err != nil {
+		if _, ok := err.(messages.KRBError); ok {
+			return tgsReq, tgsRep, krberror.Errorf(err, krberror.KDCError, "TGS Exchange Error: kerberos error response from KDC when requesting for %s", tgsReq.ReqBody.SName.PrincipalNameString())
+		}
+		return tgsReq, tgsRep, krberror.Errorf(err, krberror.NetworkingError, "TGS Exchange Error: issue sending TGS_REQ to KDC")
+	}
+	err = tgsRep.Unmarshal(r)
+	if err != nil {
+		return tgsReq, tgsRep, krberror.Errorf(err, krberror.EncodingError, "TGS Exchange Error: failed to process the TGS_REP")
+	}
+	err = tgsRep.DecryptEncPart(sessionKey)
+	if err != nil {
+		return tgsReq, tgsRep, krberror.Errorf(err, krberror.EncodingError, "TGS Exchange Error: failed to process the TGS_REP")
+	}
+	if ok, err := tgsRep.Verify(cl.Config, tgsReq); !ok {
+		return tgsReq, tgsRep, krberror.Errorf(err, krberror.EncodingError, "TGS Exchange Error: TGS_REP is not valid")
+	}
+
+	if tgsRep.Ticket.SName.NameString[0] == "krbtgt" && !tgsRep.Ticket.SName.Equal(tgsReq.ReqBody.SName) {
+		if referral > 5 {
+			return tgsReq, tgsRep, krberror.Errorf(err, krberror.KRBMsgError, "TGS Exchange Error: maximum number of referrals exceeded")
+		}
+		// Server referral https://tools.ietf.org/html/rfc6806.html#section-8
+		// The TGS Rep contains a TGT for another domain as the service resides in that domain.
+		cl.addSession(tgsRep.Ticket, tgsRep.DecryptedEncPart)
+		realm := tgsRep.Ticket.SName.NameString[len(tgsRep.Ticket.SName.NameString)-1]
+		referral++
+		if types.IsFlagSet(&tgsReq.ReqBody.KDCOptions, flags.EncTktInSkey) && len(tgsReq.ReqBody.AdditionalTickets) > 0 {
+			tgsReq, err = messages.NewUser2UserTGSReq(cl.Credentials.CName(), kdcRealm, cl.Config, tgt, sessionKey, tgsReq.ReqBody.SName, tgsReq.Renewal, tgsReq.ReqBody.AdditionalTickets[0])
+			if err != nil {
+				return tgsReq, tgsRep, err
+			}
+		}
+		tgsReq, err = messages.NewTGSReq(cl.Credentials.CName(), realm, cl.Config, tgsRep.Ticket, tgsRep.DecryptedEncPart.Key, tgsReq.ReqBody.SName, tgsReq.Renewal)
+		if err != nil {
+			return tgsReq, tgsRep, err
+		}
+		return cl.TGSExchange(tgsReq, realm, tgsRep.Ticket, tgsRep.DecryptedEncPart.Key, referral)
+	}
+	cl.cache.addEntry(
+		tgsRep.Ticket,
+		tgsRep.DecryptedEncPart.AuthTime,
+		tgsRep.DecryptedEncPart.StartTime,
+		tgsRep.DecryptedEncPart.EndTime,
+		tgsRep.DecryptedEncPart.RenewTill,
+		tgsRep.DecryptedEncPart.Key,
+	)
+	cl.Log("ticket added to cache for %s (EndTime: %v)", tgsRep.Ticket.SName.PrincipalNameString(), tgsRep.DecryptedEncPart.EndTime)
+	return tgsReq, tgsRep, err
+}
+
+// GetServiceTicket makes a request to get a service ticket for the SPN specified
+// SPN format: <SERVICE>/<FQDN> Eg. HTTP/www.example.com
+// The ticket will be added to the client's ticket cache
+func (cl *Client) GetServiceTicket(spn string) (messages.Ticket, types.EncryptionKey, error) {
+	var tkt messages.Ticket
+	var skey types.EncryptionKey
+	if tkt, skey, ok := cl.GetCachedTicket(spn); ok {
+		// Already a valid ticket in the cache
+		return tkt, skey, nil
+	}
+	princ := types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, spn)
+	realm := cl.Config.ResolveRealm(princ.NameString[len(princ.NameString)-1])
+
+	tgt, skey, err := cl.sessionTGT(realm)
+	if err != nil {
+		return tkt, skey, err
+	}
+	_, tgsRep, err := cl.TGSREQGenerateAndExchange(princ, realm, tgt, skey, false)
+	if err != nil {
+		return tkt, skey, err
+	}
+	return tgsRep.Ticket, tgsRep.DecryptedEncPart.Key, nil
+}

+ 110 - 0
v8/client/cache.go

@@ -0,0 +1,110 @@
+package client
+
+import (
+	"errors"
+	"sync"
+	"time"
+
+	"github.com/jcmturner/gokrb5/v8/messages"
+	"github.com/jcmturner/gokrb5/v8/types"
+)
+
+// Cache for service tickets held by the client.
+type Cache struct {
+	Entries map[string]CacheEntry
+	mux     sync.RWMutex
+}
+
+// CacheEntry holds details for a cache entry.
+type CacheEntry struct {
+	Ticket     messages.Ticket
+	AuthTime   time.Time
+	StartTime  time.Time
+	EndTime    time.Time
+	RenewTill  time.Time
+	SessionKey types.EncryptionKey
+}
+
+// NewCache creates a new client ticket cache instance.
+func NewCache() *Cache {
+	return &Cache{
+		Entries: map[string]CacheEntry{},
+	}
+}
+
+// getEntry returns a cache entry that matches the SPN.
+func (c *Cache) getEntry(spn string) (CacheEntry, bool) {
+	c.mux.RLock()
+	defer c.mux.RUnlock()
+	e, ok := (*c).Entries[spn]
+	return e, ok
+}
+
+// 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{
+		Ticket:     tkt,
+		AuthTime:   authTime,
+		StartTime:  startTime,
+		EndTime:    endTime,
+		RenewTill:  renewTill,
+		SessionKey: sessionKey,
+	}
+	return c.Entries[spn]
+}
+
+// clear deletes all the cache entries
+func (c *Cache) clear() {
+	c.mux.Lock()
+	defer c.mux.Unlock()
+	for k := range c.Entries {
+		delete(c.Entries, k)
+	}
+}
+
+// RemoveEntry removes the cache entry for the defined SPN.
+func (c *Cache) RemoveEntry(spn string) {
+	c.mux.Lock()
+	defer c.mux.Unlock()
+	delete(c.Entries, spn)
+}
+
+// GetCachedTicket returns a ticket from the cache for the SPN.
+// Only a ticket that is currently valid will be returned.
+func (cl *Client) GetCachedTicket(spn string) (messages.Ticket, types.EncryptionKey, bool) {
+	if e, ok := cl.cache.getEntry(spn); ok {
+		//If within time window of ticket return it
+		if time.Now().UTC().After(e.StartTime) && time.Now().UTC().Before(e.EndTime) {
+			cl.Log("ticket received from cache for %s", spn)
+			return e.Ticket, e.SessionKey, true
+		} else if time.Now().UTC().Before(e.RenewTill) {
+			e, err := cl.renewTicket(e)
+			if err != nil {
+				return e.Ticket, e.SessionKey, false
+			}
+			return e.Ticket, e.SessionKey, true
+		}
+	}
+	var tkt messages.Ticket
+	var key types.EncryptionKey
+	return tkt, key, false
+}
+
+// renewTicket renews a cache entry ticket.
+// To renew from outside the client package use GetCachedTicket
+func (cl *Client) renewTicket(e CacheEntry) (CacheEntry, error) {
+	spn := e.Ticket.SName
+	_, _, err := cl.TGSREQGenerateAndExchange(spn, e.Ticket.Realm, e.Ticket, e.SessionKey, true)
+	if err != nil {
+		return e, err
+	}
+	e, ok := cl.cache.getEntry(e.Ticket.SName.PrincipalNameString())
+	if !ok {
+		return e, errors.New("ticket was not added to cache")
+	}
+	cl.Log("ticket renewed for %s (EndTime: %v)", spn.PrincipalNameString(), e.EndTime)
+	return e, nil
+}

+ 241 - 0
v8/client/client.go

@@ -0,0 +1,241 @@
+// Package client provides a client library and methods for Kerberos 5 authentication.
+package client
+
+import (
+	"errors"
+	"fmt"
+	"time"
+
+	"github.com/jcmturner/gokrb5/v8/config"
+	"github.com/jcmturner/gokrb5/v8/credentials"
+	"github.com/jcmturner/gokrb5/v8/crypto"
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+	"github.com/jcmturner/gokrb5/v8/iana/errorcode"
+	"github.com/jcmturner/gokrb5/v8/iana/nametype"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/krberror"
+	"github.com/jcmturner/gokrb5/v8/messages"
+	"github.com/jcmturner/gokrb5/v8/types"
+)
+
+// Client side configuration and state.
+type Client struct {
+	Credentials *credentials.Credentials
+	Config      *config.Config
+	settings    *Settings
+	sessions    *sessions
+	cache       *Cache
+}
+
+// NewWithPassword creates a new client from a password credential.
+// Set the realm to empty string to use the default realm from config.
+func NewWithPassword(username, realm, password string, krb5conf *config.Config, settings ...func(*Settings)) *Client {
+	creds := credentials.New(username, realm)
+	return &Client{
+		Credentials: creds.WithPassword(password),
+		Config:      krb5conf,
+		settings:    NewSettings(settings...),
+		sessions: &sessions{
+			Entries: make(map[string]*session),
+		},
+		cache: NewCache(),
+	}
+}
+
+// NewWithKeytab creates a new client from a keytab credential.
+func NewWithKeytab(username, realm string, kt *keytab.Keytab, krb5conf *config.Config, settings ...func(*Settings)) *Client {
+	creds := credentials.New(username, realm)
+	return &Client{
+		Credentials: creds.WithKeytab(kt),
+		Config:      krb5conf,
+		settings:    NewSettings(settings...),
+		sessions: &sessions{
+			Entries: make(map[string]*session),
+		},
+		cache: NewCache(),
+	}
+}
+
+// NewFromCCache create a client from a populated client cache.
+//
+// WARNING: A client created from CCache does not automatically renew TGTs and a failure will occur after the TGT expires.
+func NewFromCCache(c *credentials.CCache, krb5conf *config.Config, settings ...func(*Settings)) (*Client, error) {
+	cl := &Client{
+		Credentials: c.GetClientCredentials(),
+		Config:      krb5conf,
+		settings:    NewSettings(settings...),
+		sessions: &sessions{
+			Entries: make(map[string]*session),
+		},
+		cache: NewCache(),
+	}
+	spn := types.PrincipalName{
+		NameType:   nametype.KRB_NT_SRV_INST,
+		NameString: []string{"krbtgt", c.DefaultPrincipal.Realm},
+	}
+	cred, ok := c.GetEntry(spn)
+	if !ok {
+		return cl, errors.New("TGT not found in CCache")
+	}
+	var tgt messages.Ticket
+	err := tgt.Unmarshal(cred.Ticket)
+	if err != nil {
+		return cl, fmt.Errorf("TGT bytes in cache are not valid: %v", err)
+	}
+	cl.sessions.Entries[c.DefaultPrincipal.Realm] = &session{
+		realm:      c.DefaultPrincipal.Realm,
+		authTime:   cred.AuthTime,
+		endTime:    cred.EndTime,
+		renewTill:  cred.RenewTill,
+		tgt:        tgt,
+		sessionKey: cred.Key,
+	}
+	for _, cred := range c.GetEntries() {
+		var tkt messages.Ticket
+		err = tkt.Unmarshal(cred.Ticket)
+		if err != nil {
+			return cl, fmt.Errorf("cache entry ticket bytes are not valid: %v", err)
+		}
+		cl.cache.addEntry(
+			tkt,
+			cred.AuthTime,
+			cred.StartTime,
+			cred.EndTime,
+			cred.RenewTill,
+			cred.Key,
+		)
+	}
+	return cl, nil
+}
+
+// Key returns the client's encryption key for the specified encryption type and its kvno (kvno of zero will find latest).
+// The key can be retrieved either from the keytab or generated from the client's password.
+// If the client has both a keytab and a password defined the keytab is favoured as the source for the key
+// A KRBError can be passed in the event the KDC returns one of type KDC_ERR_PREAUTH_REQUIRED and is required to derive
+// the key for pre-authentication from the client's password. If a KRBError is not available, pass nil to this argument.
+func (cl *Client) Key(etype etype.EType, kvno int, krberr *messages.KRBError) (types.EncryptionKey, int, error) {
+	if cl.Credentials.HasKeytab() && etype != nil {
+		return cl.Credentials.Keytab().GetEncryptionKey(cl.Credentials.CName(), cl.Credentials.Domain(), kvno, etype.GetETypeID())
+	} else if cl.Credentials.HasPassword() {
+		if krberr != nil && krberr.ErrorCode == errorcode.KDC_ERR_PREAUTH_REQUIRED {
+			var pas types.PADataSequence
+			err := pas.Unmarshal(krberr.EData)
+			if err != nil {
+				return types.EncryptionKey{}, 0, fmt.Errorf("could not get PAData from KRBError to generate key from password: %v", err)
+			}
+			key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), krberr.CName, krberr.CRealm, etype.GetETypeID(), pas)
+			return key, 0, err
+		}
+		key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), cl.Credentials.CName(), cl.Credentials.Domain(), etype.GetETypeID(), types.PADataSequence{})
+		return key, 0, err
+	}
+	return types.EncryptionKey{}, 0, errors.New("credential has neither keytab or password to generate key")
+}
+
+// IsConfigured indicates if the client has the values required set.
+func (cl *Client) IsConfigured() (bool, error) {
+	if cl.Credentials.UserName() == "" {
+		return false, errors.New("client does not have a username")
+	}
+	if cl.Credentials.Domain() == "" {
+		return false, errors.New("client does not have a define realm")
+	}
+	// Client needs to have either a password, keytab or a session already (later when loading from CCache)
+	if !cl.Credentials.HasPassword() && !cl.Credentials.HasKeytab() {
+		authTime, _, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
+		if err != nil || authTime.IsZero() {
+			return false, errors.New("client has neither a keytab nor a password set and no session")
+		}
+	}
+	if !cl.Config.LibDefaults.DNSLookupKDC {
+		for _, r := range cl.Config.Realms {
+			if r.Realm == cl.Credentials.Domain() {
+				if len(r.KDC) > 0 {
+					return true, nil
+				}
+				return false, errors.New("client krb5 config does not have any defined KDCs for the default realm")
+			}
+		}
+	}
+	return true, nil
+}
+
+// Login the client with the KDC via an AS exchange.
+func (cl *Client) Login() error {
+	if ok, err := cl.IsConfigured(); !ok {
+		return err
+	}
+	if !cl.Credentials.HasPassword() && !cl.Credentials.HasKeytab() {
+		_, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
+		if err != nil {
+			return krberror.Errorf(err, krberror.KRBMsgError, "no user credentials available and error getting any existing session")
+		}
+		if time.Now().UTC().After(endTime) {
+			return krberror.New(krberror.KRBMsgError, "cannot login, no user credentials available and no valid existing session")
+		}
+		// no credentials but there is a session with tgt already
+		return nil
+	}
+	ASReq, err := messages.NewASReqForTGT(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName())
+	if err != nil {
+		return krberror.Errorf(err, krberror.KRBMsgError, "error generating new AS_REQ")
+	}
+	ASRep, err := cl.ASExchange(cl.Credentials.Domain(), ASReq, 0)
+	if err != nil {
+		return err
+	}
+	cl.addSession(ASRep.Ticket, ASRep.DecryptedEncPart)
+	return nil
+}
+
+// AffirmLogin will only perform an AS exchange with the KDC if the client does not already have a TGT.
+func (cl *Client) AffirmLogin() error {
+	_, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
+	if err != nil || time.Now().UTC().After(endTime) {
+		err := cl.Login()
+		if err != nil {
+			return fmt.Errorf("could not get valid TGT for client's realm: %v", err)
+		}
+	}
+	return nil
+}
+
+// realmLogin obtains or renews a TGT and establishes a session for the realm specified.
+func (cl *Client) realmLogin(realm string) error {
+	if realm == cl.Credentials.Domain() {
+		return cl.Login()
+	}
+	_, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
+	if err != nil || time.Now().UTC().After(endTime) {
+		err := cl.Login()
+		if err != nil {
+			return fmt.Errorf("could not get valid TGT for client's realm: %v", err)
+		}
+	}
+	tgt, skey, err := cl.sessionTGT(cl.Credentials.Domain())
+	if err != nil {
+		return err
+	}
+
+	spn := types.PrincipalName{
+		NameType:   nametype.KRB_NT_SRV_INST,
+		NameString: []string{"krbtgt", realm},
+	}
+
+	_, tgsRep, err := cl.TGSREQGenerateAndExchange(spn, cl.Credentials.Domain(), tgt, skey, false)
+	if err != nil {
+		return err
+	}
+	cl.addSession(tgsRep.Ticket, tgsRep.DecryptedEncPart)
+
+	return nil
+}
+
+// Destroy stops the auto-renewal of all sessions and removes the sessions and cache entries from the client.
+func (cl *Client) Destroy() {
+	creds := credentials.New("", "")
+	cl.sessions.destroy()
+	cl.cache.clear()
+	cl.Credentials = creds
+	cl.Log("client destroyed")
+}

+ 190 - 0
v8/client/client_ad_integration_test.go

@@ -0,0 +1,190 @@
+package client
+
+import (
+	"bytes"
+	"encoding/hex"
+	"log"
+
+	"github.com/jcmturner/gokrb5/v8/config"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+	"github.com/jcmturner/gokrb5/v8/iana/nametype"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/test"
+	"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) {
+	test.AD(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	c.Realms[0].KDC = []string{testdata.TEST_KDC_AD}
+	cl := NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+	err := cl.Login()
+	if err != nil {
+		t.Fatalf("Error on login: %v\n", err)
+	}
+}
+
+func TestClient_GetServiceTicket_AD(t *testing.T) {
+	test.AD(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	c.Realms[0].KDC = []string{testdata.TEST_KDC_AD}
+	cl := NewWithKeytab("testuser1", "TEST.GOKRB5", kt, 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.PrincipalNameString())
+	assert.Equal(t, int32(18), key.KeyType)
+
+	b, _ = hex.DecodeString(testdata.SYSHTTP_KEYTAB)
+	skt := keytab.New()
+	skt.Unmarshal(b)
+	sname := types.PrincipalName{NameType: nametype.KRB_NT_PRINCIPAL, NameString: []string{"sysHTTP"}}
+	err = tkt.DecryptEncPart(skt, &sname)
+	if err != nil {
+		t.Errorf("could not decrypt service ticket: %v", err)
+	}
+	w := bytes.NewBufferString("")
+	l := log.New(w, "", 0)
+	isPAC, pac, err := tkt.GetPACType(skt, &sname, l)
+	if err != nil {
+		t.Log(w.String())
+		t.Errorf("error getting PAC: %v", err)
+	}
+	assert.True(t, isPAC, "should have PAC")
+	assert.Equal(t, "TEST", pac.KerbValidationInfo.LogonDomainName.String(), "domain name in PAC not correct")
+}
+
+func TestClient_SuccessfulLogin_AD_TRUST_USER_DOMAIN(t *testing.T) {
+	test.AD(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_USERKRB5_AD_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	c.Realms[0].KDC = []string{testdata.TEST_KDC_AD_TRUST_USER_DOMAIN}
+	c.LibDefaults.DefaultRealm = "USER.GOKRB5"
+	cl := NewWithKeytab("testuser1", "USER.GOKRB5", kt, c, DisablePAFXFAST(true))
+
+	err := cl.Login()
+	if err != nil {
+		t.Fatalf("Error on login: %v\n", err)
+	}
+}
+
+func TestClient_GetServiceTicket_AD_TRUST_USER_DOMAIN(t *testing.T) {
+	test.AD(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_USERKRB5_AD_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	c.Realms[0].KDC = []string{testdata.TEST_KDC_AD_TRUST_USER_DOMAIN}
+	c.LibDefaults.DefaultRealm = "USER.GOKRB5"
+	c.LibDefaults.Canonicalize = true
+	c.LibDefaults.DefaultTktEnctypes = []string{"rc4-hmac"}
+	c.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.ETypesByName["rc4-hmac"]}
+	c.LibDefaults.DefaultTGSEnctypes = []string{"rc4-hmac"}
+	c.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.ETypesByName["rc4-hmac"]}
+	cl := NewWithKeytab("testuser1", "USER.GOKRB5", kt, c, DisablePAFXFAST(true))
+
+	err := cl.Login()
+
+	if err != nil {
+		t.Fatalf("Error on login: %v\n", err)
+	}
+	spn := "HTTP/host.res.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.PrincipalNameString())
+	assert.Equal(t, etypeID.ETypesByName["rc4-hmac"], key.KeyType)
+
+	b, _ = hex.DecodeString(testdata.SYSHTTP_RESGOKRB5_AD_KEYTAB)
+	skt := keytab.New()
+	skt.Unmarshal(b)
+	sname := types.PrincipalName{NameType: nametype.KRB_NT_PRINCIPAL, NameString: []string{"sysHTTP"}}
+	err = tkt.DecryptEncPart(skt, &sname)
+	if err != nil {
+		t.Errorf("error decrypting ticket with service keytab: %v", err)
+	}
+	w := bytes.NewBufferString("")
+	l := log.New(w, "", 0)
+	isPAC, pac, err := tkt.GetPACType(skt, &sname, l)
+	if err != nil {
+		t.Log(w.String())
+		t.Errorf("error getting PAC: %v", err)
+	}
+	assert.True(t, isPAC, "Did not find PAC in service ticket")
+	assert.Equal(t, "testuser1", pac.KerbValidationInfo.EffectiveName.Value, "PAC value not parsed")
+
+}
+
+func TestClient_GetServiceTicket_AD_USER_DOMAIN(t *testing.T) {
+	test.AD(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_USERKRB5_AD_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	c.Realms[0].KDC = []string{testdata.TEST_KDC_AD_TRUST_USER_DOMAIN}
+	c.LibDefaults.DefaultRealm = "USER.GOKRB5"
+	c.LibDefaults.Canonicalize = true
+	c.LibDefaults.DefaultTktEnctypes = []string{"rc4-hmac"}
+	c.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.ETypesByName["rc4-hmac"]}
+	c.LibDefaults.DefaultTGSEnctypes = []string{"rc4-hmac"}
+	c.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.ETypesByName["rc4-hmac"]}
+	cl := NewWithKeytab("testuser1", "USER.GOKRB5", kt, c, DisablePAFXFAST(true))
+
+	err := cl.Login()
+
+	if err != nil {
+		t.Fatalf("Error on login: %v\n", err)
+	}
+	spn := "HTTP/user2.user.gokrb5"
+	tkt, _, err := cl.GetServiceTicket(spn)
+	if err != nil {
+		t.Fatalf("Error getting service ticket: %v\n", err)
+	}
+	assert.Equal(t, spn, tkt.SName.PrincipalNameString())
+	//assert.Equal(t, etypeID.ETypesByName["rc4-hmac"], key.KeyType)
+
+	b, _ = hex.DecodeString(testdata.TESTUSER2_USERKRB5_AD_KEYTAB)
+	skt := keytab.New()
+	skt.Unmarshal(b)
+	sname := types.PrincipalName{NameType: nametype.KRB_NT_PRINCIPAL, NameString: []string{"testuser2"}}
+	err = tkt.DecryptEncPart(skt, &sname)
+	if err != nil {
+		t.Errorf("error decrypting ticket with service keytab: %v", err)
+	}
+	w := bytes.NewBufferString("")
+	l := log.New(w, "", 0)
+	isPAC, pac, err := tkt.GetPACType(skt, &sname, l)
+	if err != nil {
+		t.Log(w.String())
+		t.Errorf("error getting PAC: %v", err)
+	}
+	assert.True(t, isPAC, "Did not find PAC in service ticket")
+	assert.Equal(t, "testuser1", pac.KerbValidationInfo.EffectiveName.Value, "PAC value not parsed")
+
+}

+ 34 - 0
v8/client/client_dns_test.go

@@ -0,0 +1,34 @@
+package client
+
+import (
+	"encoding/hex"
+	"github.com/jcmturner/gokrb5/v8/config"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/test"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+	"testing"
+)
+
+func TestClient_Login_DNSKDCs(t *testing.T) {
+	test.Privileged(t)
+
+	//ns := os.Getenv("DNSUTILS_OVERRIDE_NS")
+	//if ns == "" {
+	//	os.Setenv("DNSUTILS_OVERRIDE_NS", testdata.TEST_NS)
+	//}
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	// Set to lookup KDCs in DNS
+	c.LibDefaults.DNSLookupKDC = true
+	//Blank out the KDCs to ensure they are not being used
+	c.Realms = []config.Realm{}
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	cl := NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+	err := cl.Login()
+	if err != nil {
+		t.Errorf("error on logging in using DNS lookup of KDCs: %v\n", err)
+	}
+}

+ 705 - 0
v8/client/client_integration_test.go

@@ -0,0 +1,705 @@
+package client_test
+
+import (
+	"bytes"
+	"encoding/hex"
+	"errors"
+	"io"
+	"net/http"
+	"os"
+	"os/exec"
+	"os/user"
+	"runtime"
+	"testing"
+	"time"
+
+	"fmt"
+	"github.com/jcmturner/gokrb5/v8/client"
+	"github.com/jcmturner/gokrb5/v8/config"
+	"github.com/jcmturner/gokrb5/v8/credentials"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/spnego"
+	"github.com/jcmturner/gokrb5/v8/test"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+	"github.com/stretchr/testify/assert"
+	"strings"
+	"sync"
+)
+
+func TestClient_SuccessfulLogin_Keytab(t *testing.T) {
+	test.Integration(t)
+
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	var tests = []string{
+		testdata.TEST_KDC,
+		testdata.TEST_KDC_OLD,
+		testdata.TEST_KDC_LASTEST,
+	}
+	for _, tst := range tests {
+		c.Realms[0].KDC = []string{addr + ":" + tst}
+		cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+		err := cl.Login()
+		if err != nil {
+			t.Errorf("error on logging in with KDC %s: %v\n", tst, err)
+		}
+	}
+}
+
+func TestClient_SuccessfulLogin_Password(t *testing.T) {
+	test.Integration(t)
+
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	var tests = []string{
+		testdata.TEST_KDC,
+		testdata.TEST_KDC_OLD,
+		testdata.TEST_KDC_LASTEST,
+	}
+	for _, tst := range tests {
+		c.Realms[0].KDC = []string{addr + ":" + tst}
+		cl := client.NewWithPassword("testuser1", "TEST.GOKRB5", "passwordvalue", c)
+
+		err := cl.Login()
+		if err != nil {
+			t.Errorf("error on logging in with KDC %s: %v\n", tst, err)
+		}
+	}
+}
+
+func TestClient_SuccessfulLogin_TCPOnly(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	c.LibDefaults.UDPPreferenceLimit = 1
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+	err := cl.Login()
+	if err != nil {
+		t.Fatalf("error on login: %v\n", err)
+	}
+}
+
+func TestClient_ASExchange_TGSExchange_EncTypes_Keytab(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC_LASTEST}
+	var tests = []string{
+		"des3-cbc-sha1-kd",
+		"aes128-cts-hmac-sha1-96",
+		"aes256-cts-hmac-sha1-96",
+		"aes128-cts-hmac-sha256-128",
+		"aes256-cts-hmac-sha384-192",
+		"rc4-hmac",
+	}
+	for _, tst := range tests {
+		c.LibDefaults.DefaultTktEnctypes = []string{tst}
+		c.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.ETypesByName[tst]}
+		c.LibDefaults.DefaultTGSEnctypes = []string{tst}
+		c.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.ETypesByName[tst]}
+		cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+		err := cl.Login()
+		if err != nil {
+			t.Errorf("error on login using enctype %s: %v\n", tst, err)
+		}
+		tkt, key, err := cl.GetServiceTicket("HTTP/host.test.gokrb5")
+		if err != nil {
+			t.Errorf("error in TGS exchange using enctype %s: %v", tst, err)
+		}
+		assert.Equal(t, "TEST.GOKRB5", tkt.Realm, "Realm in ticket not as expected for %s test", tst)
+		assert.Equal(t, etypeID.ETypesByName[tst], key.KeyType, "Key is not for enctype %s", tst)
+	}
+}
+
+func TestClient_ASExchange_TGSExchange_EncTypes_Password(t *testing.T) {
+	test.Integration(t)
+
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC_LASTEST}
+	var tests = []string{
+		"des3-cbc-sha1-kd",
+		"aes128-cts-hmac-sha1-96",
+		"aes256-cts-hmac-sha1-96",
+		"aes128-cts-hmac-sha256-128",
+		"aes256-cts-hmac-sha384-192",
+		"rc4-hmac",
+	}
+	for _, tst := range tests {
+		c.LibDefaults.DefaultTktEnctypes = []string{tst}
+		c.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.ETypesByName[tst]}
+		c.LibDefaults.DefaultTGSEnctypes = []string{tst}
+		c.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.ETypesByName[tst]}
+		cl := client.NewWithPassword("testuser1", "TEST.GOKRB5", "passwordvalue", c)
+
+		err := cl.Login()
+		if err != nil {
+			t.Errorf("error on login using enctype %s: %v\n", tst, err)
+		}
+		tkt, key, err := cl.GetServiceTicket("HTTP/host.test.gokrb5")
+		if err != nil {
+			t.Errorf("error in TGS exchange using enctype %s: %v", tst, err)
+		}
+		assert.Equal(t, "TEST.GOKRB5", tkt.Realm, "Realm in ticket not as expected for %s test", tst)
+		assert.Equal(t, etypeID.ETypesByName[tst], key.KeyType, "Key is not for enctype %s", tst)
+	}
+}
+
+func TestClient_FailedLogin(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_WRONGPASSWD)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+	err := cl.Login()
+	if err == nil {
+		t.Fatal("login with incorrect password did not error")
+	}
+}
+
+func TestClient_SuccessfulLogin_UserRequiringPreAuth(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER2_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	cl := client.NewWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
+
+	err := cl.Login()
+	if err != nil {
+		t.Fatalf("error on login: %v\n", err)
+	}
+}
+
+func TestClient_SuccessfulLogin_UserRequiringPreAuth_TCPOnly(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER2_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	c.LibDefaults.UDPPreferenceLimit = 1
+	cl := client.NewWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
+
+	err := cl.Login()
+	if err != nil {
+		t.Fatalf("error on login: %v\n", err)
+	}
+}
+
+func TestClient_NetworkTimeout(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	c.Realms[0].KDC = []string{testdata.TEST_KDC_BADADDR + ":88"}
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+	err := cl.Login()
+	if err == nil {
+		t.Fatal("login with incorrect KDC address did not error")
+	}
+}
+
+func TestClient_GetServiceTicket(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, 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.PrincipalNameString())
+	assert.Equal(t, int32(18), key.KeyType)
+
+	//Check cache use - should get the same values back again
+	tkt2, key2, err := cl.GetServiceTicket(spn)
+	if err != nil {
+		t.Fatalf("error getting service ticket: %v\n", err)
+	}
+	assert.Equal(t, tkt.EncPart.Cipher, tkt2.EncPart.Cipher)
+	assert.Equal(t, key.KeyValue, key2.KeyValue)
+}
+
+func TestClient_GetServiceTicket_InvalidSPN(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+	err := cl.Login()
+	if err != nil {
+		t.Fatalf("error on login: %v\n", err)
+	}
+	spn := "host.test.gokrb5"
+	_, _, err = cl.GetServiceTicket(spn)
+	assert.NotNil(t, err, "Expected unknown principal error")
+	assert.True(t, strings.Contains(err.Error(), "KDC_ERR_S_PRINCIPAL_UNKNOWN"), "Error text not as expected")
+}
+
+func TestClient_GetServiceTicket_OlderKDC(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC_OLD}
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, 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.PrincipalNameString())
+	assert.Equal(t, int32(18), key.KeyType)
+}
+
+func TestMultiThreadedClientUse(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+	var wg sync.WaitGroup
+	wg.Add(5)
+	for i := 0; i < 5; i++ {
+		go func() {
+			defer wg.Done()
+			err := cl.Login()
+			if err != nil {
+				panic(err)
+			}
+		}()
+	}
+	wg.Wait()
+
+	var wg2 sync.WaitGroup
+	wg2.Add(5)
+	for i := 0; i < 5; i++ {
+		go func() {
+			defer wg2.Done()
+			err := spnegoGet(cl)
+			if err != nil {
+				panic(err)
+			}
+		}()
+	}
+	wg2.Wait()
+}
+
+func spnegoGet(cl *client.Client) error {
+	url := os.Getenv("TEST_HTTP_URL")
+	if url == "" {
+		url = testdata.TEST_HTTP_URL
+	}
+	r, _ := http.NewRequest("GET", url+"/modgssapi/index.html", nil)
+	httpResp, err := http.DefaultClient.Do(r)
+	if err != nil {
+		return fmt.Errorf("request error: %v\n", err)
+	}
+	if httpResp.StatusCode != http.StatusUnauthorized {
+		return errors.New("did not get unauthorized code when no SPNEGO header set")
+	}
+	err = spnego.SetSPNEGOHeader(cl, r, "HTTP/host.test.gokrb5")
+	if err != nil {
+		return fmt.Errorf("error setting client SPNEGO header: %v", err)
+	}
+	httpResp, err = http.DefaultClient.Do(r)
+	if err != nil {
+		return fmt.Errorf("request error: %v\n", err)
+	}
+	if httpResp.StatusCode != http.StatusOK {
+		return errors.New("did not get OK code when SPNEGO header set")
+	}
+	return nil
+}
+
+func TestNewFromCCache(t *testing.T) {
+	test.Integration(t)
+
+	b, err := hex.DecodeString(testdata.CCACHE_TEST)
+	if err != nil {
+		t.Fatalf("error decoding test data")
+	}
+	cc := new(credentials.CCache)
+	err = cc.Unmarshal(b)
+	if err != nil {
+		t.Fatal("error getting test CCache")
+	}
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	cl, err := client.NewFromCCache(cc, c)
+	if err != nil {
+		t.Fatalf("error creating client from CCache: %v", err)
+	}
+	if ok, err := cl.IsConfigured(); !ok {
+		t.Fatalf("client was not configured from CCache: %v", err)
+	}
+}
+
+// Login to the TEST.GOKRB5 domain and request service ticket for resource in the RESDOM.GOKRB5 domain.
+// There is a trust between the two domains.
+func TestClient_GetServiceTicket_Trusted_Resource_Domain(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	for i, r := range c.Realms {
+		if r.Realm == "TEST.GOKRB5" {
+			c.Realms[i].KDC = []string{addr + ":" + testdata.TEST_KDC}
+		}
+		if r.Realm == "RESDOM.GOKRB5" {
+			c.Realms[i].KDC = []string{addr + ":" + testdata.TEST_KDC_RESDOM}
+		}
+	}
+
+	c.LibDefaults.DefaultRealm = "TEST.GOKRB5"
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	c.LibDefaults.DefaultTktEnctypes = []string{"aes256-cts-hmac-sha1-96"}
+	c.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.ETypesByName["aes256-cts-hmac-sha1-96"]}
+	c.LibDefaults.DefaultTGSEnctypes = []string{"aes256-cts-hmac-sha1-96"}
+	c.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.ETypesByName["aes256-cts-hmac-sha1-96"]}
+
+	err := cl.Login()
+
+	if err != nil {
+		t.Fatalf("error on login: %v\n", err)
+	}
+	spn := "HTTP/host.resdom.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.PrincipalNameString())
+	assert.Equal(t, etypeID.ETypesByName["aes256-cts-hmac-sha1-96"], key.KeyType)
+
+	b, _ = hex.DecodeString(testdata.SYSHTTP_RESDOM_KEYTAB)
+	skt := keytab.New()
+	skt.Unmarshal(b)
+	err = tkt.DecryptEncPart(skt, nil)
+	if err != nil {
+		t.Errorf("error decrypting ticket with service keytab: %v", err)
+	}
+}
+
+const (
+	kinitCmd = "kinit"
+	kvnoCmd  = "kvno"
+	spn      = "HTTP/host.test.gokrb5"
+)
+
+func login() error {
+	file, err := os.Create("/etc/krb5.conf")
+	if err != nil {
+		return fmt.Errorf("cannot open krb5.conf: %v", err)
+	}
+	defer file.Close()
+	fmt.Fprintf(file, testdata.TEST_KRB5CONF)
+
+	cmd := exec.Command(kinitCmd, "testuser1@TEST.GOKRB5")
+
+	stdinR, stdinW := io.Pipe()
+	stderrR, stderrW := io.Pipe()
+	cmd.Stdin = stdinR
+	cmd.Stderr = stderrW
+
+	err = cmd.Start()
+	if err != nil {
+		return fmt.Errorf("could not start %s command: %v", kinitCmd, err)
+	}
+
+	go func() {
+		io.WriteString(stdinW, "passwordvalue")
+		stdinW.Close()
+	}()
+	errBuf := new(bytes.Buffer)
+	go func() {
+		io.Copy(errBuf, stderrR)
+		stderrR.Close()
+	}()
+
+	err = cmd.Wait()
+	if err != nil {
+		return fmt.Errorf("%s did not run successfully: %v stderr: %s", kinitCmd, err, string(errBuf.Bytes()))
+	}
+	return nil
+}
+
+func getServiceTkt() error {
+	cmd := exec.Command(kvnoCmd, spn)
+	err := cmd.Start()
+	if err != nil {
+		return fmt.Errorf("could not start %s command: %v", kvnoCmd, err)
+	}
+	err = cmd.Wait()
+	if err != nil {
+		return fmt.Errorf("%s did not run successfully: %v", kvnoCmd, err)
+	}
+	return nil
+}
+
+func loadCCache() (*credentials.CCache, error) {
+	usr, _ := user.Current()
+	cpath := "/tmp/krb5cc_" + usr.Uid
+	return credentials.LoadCCache(cpath)
+}
+
+func TestGetServiceTicketFromCCacheTGT(t *testing.T) {
+	test.Privileged(t)
+
+	err := login()
+	if err != nil {
+		t.Fatalf("error logging in with kinit: %v", err)
+	}
+	c, err := loadCCache()
+	if err != nil {
+		t.Errorf("error loading CCache: %v", err)
+	}
+	cfg, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	cfg.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	cl, err := client.NewFromCCache(c, cfg)
+	if err != nil {
+		t.Fatalf("error generating client from ccache: %v", 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.PrincipalNameString())
+	assert.Equal(t, int32(18), key.KeyType)
+
+	//Check cache use - should get the same values back again
+	tkt2, key2, err := cl.GetServiceTicket(spn)
+	if err != nil {
+		t.Fatalf("error getting service ticket: %v\n", err)
+	}
+	assert.Equal(t, tkt.EncPart.Cipher, tkt2.EncPart.Cipher)
+	assert.Equal(t, key.KeyValue, key2.KeyValue)
+
+	url := os.Getenv("TEST_HTTP_URL")
+	if url == "" {
+		url = testdata.TEST_HTTP_URL
+	}
+	r, _ := http.NewRequest("GET", url+"/modgssapi/index.html", nil)
+	err = spnego.SetSPNEGOHeader(cl, r, "HTTP/host.test.gokrb5")
+	if err != nil {
+		t.Fatalf("error setting client SPNEGO header: %v", err)
+	}
+	httpResp, err := http.DefaultClient.Do(r)
+	if err != nil {
+		t.Fatalf("request error: %v\n", err)
+	}
+	assert.Equal(t, http.StatusOK, httpResp.StatusCode, "status code in response to client SPNEGO request not as expected")
+}
+
+func TestGetServiceTicketFromCCacheWithoutKDC(t *testing.T) {
+	test.Privileged(t)
+
+	err := login()
+	if err != nil {
+		t.Fatalf("error logging in with kinit: %v", err)
+	}
+	err = getServiceTkt()
+	if err != nil {
+		t.Fatalf("error getting service ticket: %v", err)
+	}
+	c, err := loadCCache()
+	if err != nil {
+		t.Errorf("error loading CCache: %v", err)
+	}
+	cfg, _ := config.NewFromString("...")
+	cl, err := client.NewFromCCache(c, cfg)
+	if err != nil {
+		t.Fatalf("error generating client from ccache: %v", err)
+	}
+	url := os.Getenv("TEST_HTTP_URL")
+	if url == "" {
+		url = testdata.TEST_HTTP_URL
+	}
+	r, _ := http.NewRequest("GET", url+"/modgssapi/index.html", nil)
+	err = spnego.SetSPNEGOHeader(cl, r, "HTTP/host.test.gokrb5")
+	if err != nil {
+		t.Fatalf("error setting client SPNEGO header: %v", err)
+	}
+	httpResp, err := http.DefaultClient.Do(r)
+	if err != nil {
+		t.Fatalf("request error: %v\n", err)
+	}
+	assert.Equal(t, http.StatusOK, httpResp.StatusCode, "status code in response to client SPNEGO request not as expected")
+}
+
+func TestClient_ChangePasswd(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	c.Realms[0].KPasswdServer = []string{addr + ":464"}
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+	ok, err := cl.ChangePasswd("newpassword")
+	if err != nil {
+		t.Fatalf("error changing password: %v", err)
+	}
+	assert.True(t, ok, "password was not changed")
+
+	cl = client.NewWithPassword("testuser1", "TEST.GOKRB5", "newpassword", c)
+	ok, err = cl.ChangePasswd(testdata.TESTUSER1_PASSWORD)
+	if err != nil {
+		t.Fatalf("error changing password: %v", err)
+	}
+	assert.True(t, ok, "password was not changed back")
+
+	cl = client.NewWithPassword("testuser1", "TEST.GOKRB5", testdata.TESTUSER1_PASSWORD, c)
+	err = cl.Login()
+	if err != nil {
+		t.Fatalf("Could not log back in after reverting password: %v", err)
+	}
+}
+
+func TestClient_Destroy(t *testing.T) {
+	test.Integration(t)
+
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC_SHORTTICKETS}
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+
+	err := cl.Login()
+	if err != nil {
+		t.Fatalf("error on login: %v\n", err)
+	}
+	spn := "HTTP/host.test.gokrb5"
+	_, _, err = cl.GetServiceTicket(spn)
+	if err != nil {
+		t.Fatalf("error getting service ticket: %v\n", err)
+	}
+	n := runtime.NumGoroutine()
+	time.Sleep(time.Second * 60)
+	cl.Destroy()
+	time.Sleep(time.Second * 5)
+	assert.True(t, runtime.NumGoroutine() < n, "auto-renewal goroutine was not stopped when client destroyed")
+	is, _ := cl.IsConfigured()
+	assert.False(t, is, "client is still configured after it was destroyed")
+}

+ 20 - 0
v8/client/client_test.go

@@ -0,0 +1,20 @@
+package client
+
+import (
+	"testing"
+
+	"github.com/jcmturner/gokrb5/v8/config"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+)
+
+func TestAssumePreauthentication(t *testing.T) {
+	t.Parallel()
+
+	cl := NewWithKeytab("username", "REALM", &keytab.Keytab{}, &config.Config{}, AssumePreAuthentication(true))
+	if !cl.settings.assumePreAuthentication {
+		t.Fatal("assumePreAuthentication should be true")
+	}
+	if !cl.settings.AssumePreAuthentication() {
+		t.Fatal("AssumePreAuthentication() should be true")
+	}
+}

+ 224 - 0
v8/client/network.go

@@ -0,0 +1,224 @@
+package client
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"time"
+
+	"github.com/jcmturner/gokrb5/v8/iana/errorcode"
+	"github.com/jcmturner/gokrb5/v8/messages"
+)
+
+// SendToKDC performs network actions to send data to the KDC.
+func (cl *Client) sendToKDC(b []byte, realm string) ([]byte, error) {
+	var rb []byte
+	if cl.Config.LibDefaults.UDPPreferenceLimit == 1 {
+		//1 means we should always use TCP
+		rb, errtcp := cl.sendKDCTCP(realm, b)
+		if errtcp != nil {
+			if e, ok := errtcp.(messages.KRBError); ok {
+				return rb, e
+			}
+			return rb, fmt.Errorf("communication error with KDC via TCP: %v", errtcp)
+		}
+		return rb, nil
+	}
+	if len(b) <= cl.Config.LibDefaults.UDPPreferenceLimit {
+		//Try UDP first, TCP second
+		rb, errudp := cl.sendKDCUDP(realm, b)
+		if errudp != nil {
+			if e, ok := errudp.(messages.KRBError); ok && e.ErrorCode != errorcode.KRB_ERR_RESPONSE_TOO_BIG {
+				// Got a KRBError from KDC
+				// If this is not a KRB_ERR_RESPONSE_TOO_BIG we will return immediately otherwise will try TCP.
+				return rb, e
+			}
+			// Try TCP
+			r, errtcp := cl.sendKDCTCP(realm, b)
+			if errtcp != nil {
+				if e, ok := errtcp.(messages.KRBError); ok {
+					// Got a KRBError
+					return r, e
+				}
+				return r, fmt.Errorf("failed to communicate with KDC. Attempts made with UDP (%v) and then TCP (%v)", errudp, errtcp)
+			}
+			rb = r
+		}
+		return rb, nil
+	}
+	//Try TCP first, UDP second
+	rb, errtcp := cl.sendKDCTCP(realm, b)
+	if errtcp != nil {
+		if e, ok := errtcp.(messages.KRBError); ok {
+			// Got a KRBError from KDC so returning and not trying UDP.
+			return rb, e
+		}
+		rb, errudp := cl.sendKDCUDP(realm, b)
+		if errudp != nil {
+			if e, ok := errudp.(messages.KRBError); ok {
+				// Got a KRBError
+				return rb, e
+			}
+			return rb, fmt.Errorf("failed to communicate with KDC. Attempts made with TCP (%v) and then UDP (%v)", errtcp, errudp)
+		}
+	}
+	return rb, nil
+}
+
+// dialKDCTCP establishes a UDP connection to a KDC.
+func dialKDCUDP(count int, kdcs map[int]string) (*net.UDPConn, error) {
+	i := 1
+	for i <= count {
+		udpAddr, err := net.ResolveUDPAddr("udp", kdcs[i])
+		if err != nil {
+			return nil, fmt.Errorf("error resolving KDC address: %v", err)
+		}
+
+		conn, err := net.DialTimeout("udp", udpAddr.String(), 5*time.Second)
+		if err == nil {
+			if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
+				return nil, err
+			}
+			// conn is guaranteed to be a UDPConn
+			return conn.(*net.UDPConn), nil
+		}
+		i++
+	}
+	return nil, errors.New("error in getting a UDP connection to any of the KDCs")
+}
+
+// dialKDCTCP establishes a TCP connection to a KDC.
+func dialKDCTCP(count int, kdcs map[int]string) (*net.TCPConn, error) {
+	i := 1
+	for i <= count {
+		tcpAddr, err := net.ResolveTCPAddr("tcp", kdcs[i])
+		if err != nil {
+			return nil, fmt.Errorf("error resolving KDC address: %v", err)
+		}
+
+		conn, err := net.DialTimeout("tcp", tcpAddr.String(), 5*time.Second)
+		if err == nil {
+			if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
+				return nil, err
+			}
+			// conn is guaranteed to be a TCPConn
+			return conn.(*net.TCPConn), nil
+		}
+		i++
+	}
+	return nil, errors.New("error in getting a TCP connection to any of the KDCs")
+}
+
+// sendKDCUDP sends bytes to the KDC via UDP.
+func (cl *Client) sendKDCUDP(realm string, b []byte) ([]byte, error) {
+	var r []byte
+	count, kdcs, err := cl.Config.GetKDCs(realm, false)
+	if err != nil {
+		return r, err
+	}
+	conn, err := dialKDCUDP(count, kdcs)
+	if err != nil {
+		return r, err
+	}
+	r, err = cl.sendUDP(conn, b)
+	if err != nil {
+		return r, err
+	}
+	return checkForKRBError(r)
+}
+
+// sendKDCTCP sends bytes to the KDC via TCP.
+func (cl *Client) sendKDCTCP(realm string, b []byte) ([]byte, error) {
+	var r []byte
+	count, kdcs, err := cl.Config.GetKDCs(realm, true)
+	if err != nil {
+		return r, err
+	}
+	conn, err := dialKDCTCP(count, kdcs)
+	if err != nil {
+		return r, err
+	}
+	rb, err := cl.sendTCP(conn, b)
+	if err != nil {
+		return r, err
+	}
+	return checkForKRBError(rb)
+}
+
+// sendUDP sends bytes to connection over UDP.
+func (cl *Client) sendUDP(conn *net.UDPConn, b []byte) ([]byte, error) {
+	var r []byte
+	defer conn.Close()
+	_, err := conn.Write(b)
+	if err != nil {
+		return r, fmt.Errorf("error sending to (%s): %v", conn.RemoteAddr().String(), err)
+	}
+	udpbuf := make([]byte, 4096)
+	n, _, err := conn.ReadFrom(udpbuf)
+	r = udpbuf[:n]
+	if err != nil {
+		return r, fmt.Errorf("sending over UDP failed to %s: %v", conn.RemoteAddr().String(), err)
+	}
+	if len(r) < 1 {
+		return r, fmt.Errorf("no response data from %s", conn.RemoteAddr().String())
+	}
+	return r, nil
+}
+
+// sendTCP sends bytes to connection over TCP.
+func (cl *Client) sendTCP(conn *net.TCPConn, b []byte) ([]byte, error) {
+	defer conn.Close()
+	var r []byte
+	/*
+		RFC https://tools.ietf.org/html/rfc4120#section-7.2.2
+		Each request (KRB_KDC_REQ) and response (KRB_KDC_REP or KRB_ERROR)
+		sent over the TCP stream is preceded by the length of the request as
+		4 octets in network byte order.  The high bit of the length is
+		reserved for future expansion and MUST currently be set to zero.  If
+		a KDC that does not understand how to interpret a set high bit of the
+		length encoding receives a request with the high order bit of the
+		length set, it MUST return a KRB-ERROR message with the error
+		KRB_ERR_FIELD_TOOLONG and MUST close the TCP stream.
+		NB: network byte order == big endian
+	*/
+	var buf bytes.Buffer
+	err := binary.Write(&buf, binary.BigEndian, uint32(len(b)))
+	if err != nil {
+		return r, err
+	}
+	b = append(buf.Bytes(), b...)
+
+	_, err = conn.Write(b)
+	if err != nil {
+		return r, fmt.Errorf("error sending to KDC (%s): %v", conn.RemoteAddr().String(), err)
+	}
+
+	sh := make([]byte, 4, 4)
+	_, err = conn.Read(sh)
+	if err != nil {
+		return r, fmt.Errorf("error reading response size header: %v", err)
+	}
+	s := binary.BigEndian.Uint32(sh)
+
+	rb := make([]byte, s, s)
+	_, err = io.ReadFull(conn, rb)
+	if err != nil {
+		return r, fmt.Errorf("error reading response: %v", err)
+	}
+	if len(rb) < 1 {
+		return r, fmt.Errorf("no response data from KDC %s", conn.RemoteAddr().String())
+	}
+	return rb, nil
+}
+
+// checkForKRBError checks if the response bytes from the KDC are a KRBError.
+func checkForKRBError(b []byte) ([]byte, error) {
+	var KRBErr messages.KRBError
+	if err := KRBErr.Unmarshal(b); err == nil {
+		return b, KRBErr
+	}
+	return b, nil
+}

+ 95 - 0
v8/client/passwd.go

@@ -0,0 +1,95 @@
+package client
+
+import (
+	"fmt"
+	"net"
+
+	"github.com/jcmturner/gokrb5/v8/kadmin"
+	"github.com/jcmturner/gokrb5/v8/messages"
+)
+
+// Kpasswd server response codes.
+const (
+	KRB5_KPASSWD_SUCCESS             = 0
+	KRB5_KPASSWD_MALFORMED           = 1
+	KRB5_KPASSWD_HARDERROR           = 2
+	KRB5_KPASSWD_AUTHERROR           = 3
+	KRB5_KPASSWD_SOFTERROR           = 4
+	KRB5_KPASSWD_ACCESSDENIED        = 5
+	KRB5_KPASSWD_BAD_VERSION         = 6
+	KRB5_KPASSWD_INITIAL_FLAG_NEEDED = 7
+)
+
+// ChangePasswd changes the password of the client to the value provided.
+func (cl *Client) ChangePasswd(newPasswd string) (bool, error) {
+	ASReq, err := messages.NewASReqForChgPasswd(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName())
+	if err != nil {
+		return false, err
+	}
+	ASRep, err := cl.ASExchange(cl.Credentials.Domain(), ASReq, 0)
+	if err != nil {
+		return false, err
+	}
+
+	msg, key, err := kadmin.ChangePasswdMsg(cl.Credentials.CName(), cl.Credentials.Domain(), newPasswd, ASRep.Ticket, ASRep.DecryptedEncPart.Key)
+	if err != nil {
+		return false, err
+	}
+	r, err := cl.sendToKPasswd(msg)
+	if err != nil {
+		return false, err
+	}
+	err = r.Decrypt(key)
+	if err != nil {
+		return false, err
+	}
+	if r.ResultCode != KRB5_KPASSWD_SUCCESS {
+		return false, fmt.Errorf("error response from kadmin: code: %d; result: %s; krberror: %v", r.ResultCode, r.Result, r.KRBError)
+	}
+	cl.Credentials.WithPassword(newPasswd)
+	return true, nil
+}
+
+func (cl *Client) sendToKPasswd(msg kadmin.Request) (r kadmin.Reply, err error) {
+	_, kps, err := cl.Config.GetKpasswdServers(cl.Credentials.Domain(), true)
+	if err != nil {
+		return
+	}
+	addr := kps[1]
+	b, err := msg.Marshal()
+	if err != nil {
+		return
+	}
+	if len(b) <= cl.Config.LibDefaults.UDPPreferenceLimit {
+		return cl.sendKPasswdUDP(b, addr)
+	}
+	return cl.sendKPasswdTCP(b, addr)
+}
+
+func (cl *Client) sendKPasswdTCP(b []byte, kadmindAddr string) (r kadmin.Reply, err error) {
+	tcpAddr, err := net.ResolveTCPAddr("tcp", kadmindAddr)
+	if err != nil {
+		return
+	}
+	conn, err := net.DialTCP("tcp", nil, tcpAddr)
+	if err != nil {
+		return
+	}
+	rb, err := cl.sendTCP(conn, b)
+	err = r.Unmarshal(rb)
+	return
+}
+
+func (cl *Client) sendKPasswdUDP(b []byte, kadmindAddr string) (r kadmin.Reply, err error) {
+	udpAddr, err := net.ResolveUDPAddr("udp", kadmindAddr)
+	if err != nil {
+		return
+	}
+	conn, err := net.DialUDP("udp", nil, udpAddr)
+	if err != nil {
+		return
+	}
+	rb, err := cl.sendUDP(conn, b)
+	err = r.Unmarshal(rb)
+	return
+}

+ 255 - 0
v8/client/session.go

@@ -0,0 +1,255 @@
+package client
+
+import (
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/jcmturner/gokrb5/v8/iana/nametype"
+	"github.com/jcmturner/gokrb5/v8/krberror"
+	"github.com/jcmturner/gokrb5/v8/messages"
+	"github.com/jcmturner/gokrb5/v8/types"
+)
+
+// sessions hold TGTs and are keyed on the realm name
+type sessions struct {
+	Entries map[string]*session
+	mux     sync.RWMutex
+}
+
+// destroy erases all sessions
+func (s *sessions) destroy() {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+	for k, e := range s.Entries {
+		e.destroy()
+		delete(s.Entries, k)
+	}
+}
+
+// update replaces a session with the one provided or adds it as a new one
+func (s *sessions) update(sess *session) {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+	// if a session already exists for this, cancel its auto renew.
+	if i, ok := s.Entries[sess.realm]; ok {
+		if i != sess {
+			// Session in the sessions cache is not the same as one provided.
+			// Cancel the one in the cache and add this one.
+			i.mux.Lock()
+			defer i.mux.Unlock()
+			i.cancel <- true
+			s.Entries[sess.realm] = sess
+			return
+		}
+	}
+	// No session for this realm was found so just add it
+	s.Entries[sess.realm] = sess
+}
+
+// get returns the session for the realm specified
+func (s *sessions) get(realm string) (*session, bool) {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+	sess, ok := s.Entries[realm]
+	return sess, ok
+}
+
+// session holds the TGT details for a realm
+type session struct {
+	realm                string
+	authTime             time.Time
+	endTime              time.Time
+	renewTill            time.Time
+	tgt                  messages.Ticket
+	sessionKey           types.EncryptionKey
+	sessionKeyExpiration time.Time
+	cancel               chan bool
+	mux                  sync.RWMutex
+}
+
+// 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) {
+	if strings.ToLower(tgt.SName.NameString[0]) != "krbtgt" {
+		// Not a TGT
+		return
+	}
+	realm := tgt.SName.NameString[len(tgt.SName.NameString)-1]
+	s := &session{
+		realm:                realm,
+		authTime:             dep.AuthTime,
+		endTime:              dep.EndTime,
+		renewTill:            dep.RenewTill,
+		tgt:                  tgt,
+		sessionKey:           dep.Key,
+		sessionKeyExpiration: dep.KeyExpiration,
+	}
+	cl.sessions.update(s)
+	cl.enableAutoSessionRenewal(s)
+	cl.Log("TGT session added for %s (EndTime: %v)", realm, dep.EndTime)
+}
+
+// update overwrites the session details with those from the TGT and decrypted encPart
+func (s *session) update(tgt messages.Ticket, dep messages.EncKDCRepPart) {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+	s.authTime = dep.AuthTime
+	s.endTime = dep.EndTime
+	s.renewTill = dep.RenewTill
+	s.tgt = tgt
+	s.sessionKey = dep.Key
+	s.sessionKeyExpiration = dep.KeyExpiration
+}
+
+// destroy will cancel any auto renewal of the session and set the expiration times to the current time
+func (s *session) destroy() {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+	if s.cancel != nil {
+		s.cancel <- true
+	}
+	s.endTime = time.Now().UTC()
+	s.renewTill = s.endTime
+	s.sessionKeyExpiration = s.endTime
+}
+
+// valid informs if the TGT is still within the valid time window
+func (s *session) valid() bool {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+	t := time.Now().UTC()
+	if t.Before(s.endTime) && s.authTime.Before(t) {
+		return true
+	}
+	return false
+}
+
+// tgtDetails is a thread safe way to get the session's realm, TGT and session key values
+func (s *session) tgtDetails() (string, messages.Ticket, types.EncryptionKey) {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+	return s.realm, s.tgt, s.sessionKey
+}
+
+// timeDetails is a thread safe way to get the session's validity time values
+func (s *session) timeDetails() (string, time.Time, time.Time, time.Time, time.Time) {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+	return s.realm, s.authTime, s.endTime, s.renewTill, s.sessionKeyExpiration
+}
+
+// enableAutoSessionRenewal turns on the automatic renewal for the client's TGT session.
+func (cl *Client) enableAutoSessionRenewal(s *session) {
+	var timer *time.Timer
+	s.mux.Lock()
+	s.cancel = make(chan bool, 1)
+	s.mux.Unlock()
+	go func(s *session) {
+		for {
+			s.mux.RLock()
+			w := (s.endTime.Sub(time.Now().UTC()) * 5) / 6
+			s.mux.RUnlock()
+			if w < 0 {
+				return
+			}
+			timer = time.NewTimer(w)
+			select {
+			case <-timer.C:
+				renewal, err := cl.refreshSession(s)
+				if err != nil {
+					cl.Log("error refreshing session: %v", err)
+				}
+				if !renewal && err == nil {
+					// end this goroutine as there will have been a new login and new auto renewal goroutine created.
+					return
+				}
+			case <-s.cancel:
+				// cancel has been called. Stop the timer and exit.
+				timer.Stop()
+				return
+			}
+		}
+	}(s)
+}
+
+// renewTGT renews the client's TGT session.
+func (cl *Client) renewTGT(s *session) error {
+	realm, tgt, skey := s.tgtDetails()
+	spn := types.PrincipalName{
+		NameType:   nametype.KRB_NT_SRV_INST,
+		NameString: []string{"krbtgt", realm},
+	}
+	_, tgsRep, err := cl.TGSREQGenerateAndExchange(spn, cl.Credentials.Domain(), tgt, skey, true)
+	if err != nil {
+		return krberror.Errorf(err, krberror.KRBMsgError, "error renewing TGT for %s", realm)
+	}
+	s.update(tgsRep.Ticket, tgsRep.DecryptedEncPart)
+	cl.sessions.update(s)
+	cl.Log("TGT session renewed for %s (EndTime: %v)", realm, tgsRep.DecryptedEncPart.EndTime)
+	return nil
+}
+
+// refreshSession updates either through renewal or creating a new login.
+// The boolean indicates if the update was a renewal.
+func (cl *Client) refreshSession(s *session) (bool, error) {
+	s.mux.RLock()
+	realm := s.realm
+	renewTill := s.renewTill
+	s.mux.RUnlock()
+	cl.Log("refreshing TGT session for %s", realm)
+	if time.Now().UTC().Before(renewTill) {
+		err := cl.renewTGT(s)
+		return true, err
+	}
+	err := cl.realmLogin(realm)
+	return false, err
+}
+
+// ensureValidSession makes sure there is a valid session for the realm
+func (cl *Client) ensureValidSession(realm string) error {
+	s, ok := cl.sessions.get(realm)
+	if ok {
+		s.mux.RLock()
+		d := s.endTime.Sub(s.authTime) / 6
+		if s.endTime.Sub(time.Now().UTC()) > d {
+			s.mux.RUnlock()
+			return nil
+		}
+		s.mux.RUnlock()
+		_, err := cl.refreshSession(s)
+		return err
+	}
+	return cl.realmLogin(realm)
+}
+
+// sessionTGTDetails is a thread safe way to get the TGT and session key values for a realm
+func (cl *Client) sessionTGT(realm string) (tgt messages.Ticket, sessionKey types.EncryptionKey, err error) {
+	err = cl.ensureValidSession(realm)
+	if err != nil {
+		return
+	}
+	s, ok := cl.sessions.get(realm)
+	if !ok {
+		err = fmt.Errorf("could not find TGT session for %s", realm)
+		return
+	}
+	_, tgt, sessionKey = s.tgtDetails()
+	return
+}
+
+func (cl *Client) sessionTimes(realm string) (authTime, endTime, renewTime, sessionExp time.Time, err error) {
+	s, ok := cl.sessions.get(realm)
+	if !ok {
+		err = fmt.Errorf("could not find TGT session for %s", realm)
+		return
+	}
+	_, authTime, endTime, renewTime, sessionExp = s.timeDetails()
+	return
+}
+
+// spnRealm resolves the realm name of a service principal name
+func (cl *Client) spnRealm(spn types.PrincipalName) string {
+	return cl.Config.ResolveRealm(spn.NameString[len(spn.NameString)-1])
+}

+ 115 - 0
v8/client/session_test.go

@@ -0,0 +1,115 @@
+package client
+
+import (
+	"encoding/hex"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"runtime"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/jcmturner/gokrb5/v8/config"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/test"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMultiThreadedClientSession(t *testing.T) {
+	test.Integration(t)
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC}
+	cl := NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	err := cl.Login()
+	if err != nil {
+		t.Fatalf("failed to log in: %v", err)
+	}
+
+	s, ok := cl.sessions.get("TEST.GOKRB5")
+	if !ok {
+		t.Fatal("error initially getting session")
+	}
+	go func() {
+		for {
+			err := cl.renewTGT(s)
+			if err != nil {
+				t.Logf("error renewing TGT: %v", err)
+			}
+			time.Sleep(time.Millisecond * 100)
+		}
+	}()
+
+	var wg sync.WaitGroup
+	wg.Add(10)
+	for i := 0; i < 10; i++ {
+		go func() {
+			defer wg.Done()
+			tgt, _, err := cl.sessionTGT("TEST.GOKRB5")
+			if err != nil || tgt.Realm != "TEST.GOKRB5" {
+				t.Logf("error getting session: %v", err)
+			}
+			_, _, _, r, _ := cl.sessionTimes("TEST.GOKRB5")
+			fmt.Fprintf(ioutil.Discard, "%v", r)
+		}()
+		time.Sleep(time.Second)
+	}
+	wg.Wait()
+}
+
+func TestClient_AutoRenew_Goroutine(t *testing.T) {
+	test.Integration(t)
+
+	// Tests that the auto renew of client credentials is not spawning goroutines out of control.
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr == "" {
+		addr = testdata.TEST_KDC_ADDR
+	}
+	b, _ := hex.DecodeString(testdata.TESTUSER2_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	c.Realms[0].KDC = []string{addr + ":" + testdata.TEST_KDC_SHORTTICKETS}
+	c.LibDefaults.PreferredPreauthTypes = []int{int(etypeID.DES3_CBC_SHA1_KD)} // a preauth etype the KDC does not support. Test this does not cause renewal to fail.
+	cl := NewWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
+
+	err := cl.Login()
+	if err != nil {
+		t.Errorf("error on logging in: %v\n", err)
+	}
+	n := runtime.NumGoroutine()
+	for i := 0; i < 24; i++ {
+		time.Sleep(time.Second * 5)
+		_, endTime, _, _, err := cl.sessionTimes("TEST.GOKRB5")
+		if err != nil {
+			t.Errorf("could not get client's session: %v", err)
+		}
+		if time.Now().UTC().After(endTime) {
+			t.Fatalf("session auto update failed")
+		}
+		spn := "HTTP/host.test.gokrb5"
+		tkt, key, err := cl.GetServiceTicket(spn)
+		if err != nil {
+			t.Fatalf("error getting service ticket: %v\n", err)
+		}
+		b, _ := hex.DecodeString(testdata.HTTP_KEYTAB)
+		skt := keytab.New()
+		skt.Unmarshal(b)
+		tkt.DecryptEncPart(skt, nil)
+		assert.Equal(t, spn, tkt.SName.PrincipalNameString())
+		assert.Equal(t, int32(18), key.KeyType)
+		if runtime.NumGoroutine() > n {
+			t.Fatalf("number of goroutines is increasing: should not be more than %d, is %d", n, runtime.NumGoroutine())
+		}
+	}
+}

+ 69 - 0
v8/client/settings.go

@@ -0,0 +1,69 @@
+package client
+
+import "log"
+
+// Settings holds optional client settings.
+type Settings struct {
+	disablePAFXFast         bool
+	assumePreAuthentication bool
+	preAuthEType            int32
+	logger                  *log.Logger
+}
+
+// NewSettings creates a new client settings struct.
+func NewSettings(settings ...func(*Settings)) *Settings {
+	s := new(Settings)
+	for _, set := range settings {
+		set(s)
+	}
+	return s
+}
+
+// DisablePAFXFAST used to configure the client to not use PA_FX_FAST.
+//
+// s := NewSettings(DisablePAFXFAST(true))
+func DisablePAFXFAST(b bool) func(*Settings) {
+	return func(s *Settings) {
+		s.disablePAFXFast = b
+	}
+}
+
+// DisablePAFXFAST indicates is the client should disable the use of PA_FX_FAST.
+func (s *Settings) DisablePAFXFAST() bool {
+	return s.disablePAFXFast
+}
+
+// AssumePreAuthentication used to configure the client to assume pre-authentication is required.
+//
+// s := NewSettings(AssumePreAuthentication(true))
+func AssumePreAuthentication(b bool) func(*Settings) {
+	return func(s *Settings) {
+		s.assumePreAuthentication = b
+	}
+}
+
+// AssumePreAuthentication indicates if the client should proactively assume using pre-authentication.
+func (s *Settings) AssumePreAuthentication() bool {
+	return s.assumePreAuthentication
+}
+
+// Logger used to configure client with a logger.
+//
+// s := NewSettings(kt, Logger(l))
+func Logger(l *log.Logger) func(*Settings) {
+	return func(s *Settings) {
+		s.logger = l
+	}
+}
+
+// Logger returns the client logger instance.
+func (s *Settings) Logger() *log.Logger {
+	return s.logger
+}
+
+// Log will write to the service's logger if it is configured.
+func (cl *Client) Log(format string, v ...interface{}) {
+	if cl.settings.Logger() != nil {
+		cl.settings.Logger().Printf(format, v...)
+	}
+}

+ 30 - 0
v8/config/error.go

@@ -0,0 +1,30 @@
+package config
+
+import "fmt"
+
+// UnsupportedDirective error.
+type UnsupportedDirective struct {
+	text string
+}
+
+// Error implements the error interface for unsupported directives.
+func (e UnsupportedDirective) Error() string {
+	return e.text
+}
+
+// Invalid config error.
+type Invalid struct {
+	text string
+}
+
+// Error implements the error interface for invalid config error.
+func (e Invalid) Error() string {
+	return e.text
+}
+
+// InvalidErrorf creates a new Invalid error.
+func InvalidErrorf(format string, a ...interface{}) Invalid {
+	return Invalid{
+		text: fmt.Sprintf("invalid krb5 config "+format, a...),
+	}
+}

+ 141 - 0
v8/config/hosts.go

@@ -0,0 +1,141 @@
+package config
+
+import (
+	"fmt"
+	"math/rand"
+	"net"
+	"strconv"
+	"strings"
+
+	"github.com/jcmturner/dnsutils"
+)
+
+// GetKDCs returns the count of KDCs available and a map of KDC host names keyed on preference order.
+func (c *Config) GetKDCs(realm string, tcp bool) (int, map[int]string, error) {
+	if realm == "" {
+		realm = c.LibDefaults.DefaultRealm
+	}
+	kdcs := make(map[int]string)
+	var count int
+
+	// Get the KDCs from the krb5.conf.
+	var ks []string
+	for _, r := range c.Realms {
+		if r.Realm != realm {
+			continue
+		}
+		ks = r.KDC
+	}
+	count = len(ks)
+
+	if count > 0 {
+		// Order the kdcs randomly for preference.
+		kdcs = randServOrder(ks)
+		return count, kdcs, nil
+	}
+
+	if !c.LibDefaults.DNSLookupKDC {
+		return count, kdcs, fmt.Errorf("no KDCs defined in configuration for realm %s", realm)
+	}
+
+	// Use DNS to resolve kerberos SRV records.
+	proto := "udp"
+	if tcp {
+		proto = "tcp"
+	}
+	index, addrs, err := dnsutils.OrderedSRV("kerberos", proto, realm)
+	if err != nil {
+		return count, kdcs, err
+	}
+	if len(addrs) < 1 {
+		return count, kdcs, fmt.Errorf("no KDC SRV records found for realm %s", realm)
+	}
+	count = index
+	for k, v := range addrs {
+		kdcs[k] = strings.TrimRight(v.Target, ".") + ":" + strconv.Itoa(int(v.Port))
+	}
+	return count, kdcs, nil
+}
+
+// GetKpasswdServers returns the count of kpasswd servers available and a map of kpasswd host names keyed on preference order.
+// https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html#realms - see kpasswd_server section
+func (c *Config) GetKpasswdServers(realm string, tcp bool) (int, map[int]string, error) {
+	kdcs := make(map[int]string)
+	var count int
+
+	// Use DNS to resolve kerberos SRV records if configured to do so in krb5.conf.
+	if c.LibDefaults.DNSLookupKDC {
+		proto := "udp"
+		if tcp {
+			proto = "tcp"
+		}
+		c, addrs, err := dnsutils.OrderedSRV("kpasswd", proto, realm)
+		if err != nil {
+			return count, kdcs, err
+		}
+		if c < 1 {
+			c, addrs, err = dnsutils.OrderedSRV("kerberos-adm", proto, realm)
+			if err != nil {
+				return count, kdcs, err
+			}
+		}
+		if len(addrs) < 1 {
+			return count, kdcs, fmt.Errorf("no kpasswd or kadmin SRV records found for realm %s", realm)
+		}
+		count = c
+		for k, v := range addrs {
+			kdcs[k] = strings.TrimRight(v.Target, ".") + ":" + strconv.Itoa(int(v.Port))
+		}
+	} else {
+		// Get the KDCs from the krb5.conf an order them randomly for preference.
+		var ks []string
+		var ka []string
+		for _, r := range c.Realms {
+			if r.Realm == realm {
+				ks = r.KPasswdServer
+				ka = r.AdminServer
+				break
+			}
+		}
+		if len(ks) < 1 {
+			for _, k := range ka {
+				h, _, err := net.SplitHostPort(k)
+				if err != nil {
+					continue
+				}
+				ks = append(ks, h+":464")
+			}
+		}
+		count = len(ks)
+		if count < 1 {
+			return count, kdcs, fmt.Errorf("no kpasswd or kadmin defined in configuration for realm %s", realm)
+		}
+		kdcs = randServOrder(ks)
+	}
+	return count, kdcs, nil
+}
+
+func randServOrder(ks []string) map[int]string {
+	kdcs := make(map[int]string)
+	count := len(ks)
+	i := 1
+	if count > 1 {
+		l := len(ks)
+		for l > 0 {
+			ri := rand.Intn(l)
+			kdcs[i] = ks[ri]
+			if l > 1 {
+				// Remove the entry from the source slice by swapping with the last entry and truncating
+				ks[len(ks)-1], ks[ri] = ks[ri], ks[len(ks)-1]
+				ks = ks[:len(ks)-1]
+				l = len(ks)
+			} else {
+				l = 0
+			}
+			i++
+		}
+	} else {
+		kdcs[i] = ks[0]
+	}
+	return kdcs
+}

+ 91 - 0
v8/config/hosts_test.go

@@ -0,0 +1,91 @@
+package config
+
+import (
+	"testing"
+
+	"github.com/jcmturner/gokrb5/v8/test"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConfig_GetKDCsUsesConfiguredKDC(t *testing.T) {
+	t.Parallel()
+
+	// This test is meant to cover the fix for
+	// https://github.com/jcmturner/gokrb5/issues/332
+	krb5ConfWithKDCAndDNSLookupKDC := `
+[libdefaults]
+ dns_lookup_kdc = true
+
+[realms]
+ TEST.GOKRB5 = {
+  kdc = kdc2b.test.gokrb5:88
+ }
+`
+
+	c, err := NewFromString(krb5ConfWithKDCAndDNSLookupKDC)
+	if err != nil {
+		t.Fatalf("Error loading config: %v", err)
+	}
+
+	count, kdcs, err := c.GetKDCs("TEST.GOKRB5", false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if count != 1 {
+		t.Fatalf("expected 1 but received %d", count)
+	}
+	if kdcs[1] != "kdc2b.test.gokrb5:88" {
+		t.Fatalf("expected kdc2b.test.gokrb5:88 but received %s", kdcs[1])
+	}
+}
+
+func TestResolveKDC(t *testing.T) {
+	test.Privileged(t)
+
+	c, err := NewFromString(testdata.TEST_KRB5CONF)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Store the original value for realms since we'll use them in our
+	// second test.
+	originalRealms := c.Realms
+
+	// For our first test, let's check that we discover the expected
+	// KDCs when they're not provided and we should be looking them up.
+	c.LibDefaults.DNSLookupKDC = true
+	c.Realms = make([]Realm, 0)
+	count, res, err := c.GetKDCs(c.LibDefaults.DefaultRealm, true)
+	if err != nil {
+		t.Errorf("error resolving KDC via DNS TCP: %v", err)
+	}
+	assert.Equal(t, 5, count, "Number of SRV records not as expected: %v", res)
+	assert.Equal(t, count, len(res), "Map size does not match: %v", res)
+	expected := []string{
+		"kdc.test.gokrb5:88",
+		"kdc1a.test.gokrb5:88",
+		"kdc2a.test.gokrb5:88",
+		"kdc1b.test.gokrb5:88",
+		"kdc2b.test.gokrb5:88",
+	}
+	for _, s := range expected {
+		var found bool
+		for _, v := range res {
+			if s == v {
+				found = true
+				break
+			}
+		}
+		assert.True(t, found, "Record %s not found in results", s)
+	}
+
+	// For our second check, verify that when we shouldn't be looking them up,
+	// we get the expected value.
+	c.LibDefaults.DNSLookupKDC = false
+	c.Realms = originalRealms
+	_, res, err = c.GetKDCs(c.LibDefaults.DefaultRealm, true)
+	if err != nil {
+		t.Errorf("error resolving KDCs from config: %v", err)
+	}
+	assert.Equal(t, "127.0.0.1:88", res[1], "KDC not read from config as expected")
+}

+ 726 - 0
v8/config/krb5conf.go

@@ -0,0 +1,726 @@
+// Package config implements KRB5 client and service configuration as described at https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html
+package config
+
+import (
+	"bufio"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"os"
+	"os/user"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/jcmturner/gofork/encoding/asn1"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+)
+
+// Config represents the KRB5 configuration.
+type Config struct {
+	LibDefaults LibDefaults
+	Realms      []Realm
+	DomainRealm DomainRealm
+	//CaPaths
+	//AppDefaults
+	//Plugins
+}
+
+// WeakETypeList is a list of encryption types that have been deemed weak.
+const WeakETypeList = "des-cbc-crc des-cbc-md4 des-cbc-md5 des-cbc-raw des3-cbc-raw des-hmac-sha1 arcfour-hmac-exp rc4-hmac-exp arcfour-hmac-md5-exp des"
+
+// New creates a new config struct instance.
+func New() *Config {
+	d := make(DomainRealm)
+	return &Config{
+		LibDefaults: newLibDefaults(),
+		DomainRealm: d,
+	}
+}
+
+// LibDefaults represents the [libdefaults] section of the configuration.
+type LibDefaults struct {
+	AllowWeakCrypto bool //default false
+	// ap_req_checksum_type int //unlikely to support this
+	Canonicalize bool          //default false
+	CCacheType   int           //default is 4. unlikely to implement older
+	Clockskew    time.Duration //max allowed skew in seconds, default 300
+	//Default_ccache_name string // default /tmp/krb5cc_%{uid} //Not implementing as will hold in memory
+	DefaultClientKeytabName string //default /usr/local/var/krb5/user/%{euid}/client.keytab
+	DefaultKeytabName       string //default /etc/krb5.keytab
+	DefaultRealm            string
+	DefaultTGSEnctypes      []string //default 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      []string //default 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
+	DefaultTGSEnctypeIDs    []int32  //default 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
+	DefaultTktEnctypeIDs    []int32  //default 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
+	DNSCanonicalizeHostname bool     //default true
+	DNSLookupKDC            bool     //default false
+	DNSLookupRealm          bool
+	ExtraAddresses          []net.IP       //Not implementing yet
+	Forwardable             bool           //default false
+	IgnoreAcceptorHostname  bool           //default false
+	K5LoginAuthoritative    bool           //default false
+	K5LoginDirectory        string         //default user's home directory. Must be owned by the user or root
+	KDCDefaultOptions       asn1.BitString //default 0x00000010 (KDC_OPT_RENEWABLE_OK)
+	KDCTimeSync             int            //default 1
+	//kdc_req_checksum_type int //unlikely to implement as for very old KDCs
+	NoAddresses         bool     //default true
+	PermittedEnctypes   []string //default 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 []int32
+	//plugin_base_dir string //not supporting plugins
+	PreferredPreauthTypes []int         //default “17, 16, 15, 14”, which forces libkrb5 to attempt to use PKINIT if it is supported
+	Proxiable             bool          //default false
+	RDNS                  bool          //default true
+	RealmTryDomains       int           //default -1
+	RenewLifetime         time.Duration //default 0
+	SafeChecksumType      int           //default 8
+	TicketLifetime        time.Duration //default 1 day
+	UDPPreferenceLimit    int           // 1 means to always use tcp. MIT krb5 has a default value of 1465, and it prevents user setting more than 32700.
+	VerifyAPReqNofail     bool          //default false
+}
+
+// Create a new LibDefaults struct.
+func newLibDefaults() LibDefaults {
+	uid := "0"
+	var hdir string
+	usr, _ := user.Current()
+	if usr != nil {
+		uid = usr.Uid
+		hdir = usr.HomeDir
+	}
+	opts := asn1.BitString{}
+	opts.Bytes, _ = hex.DecodeString("00000010")
+	opts.BitLength = len(opts.Bytes) * 8
+	return LibDefaults{
+		CCacheType:              4,
+		Clockskew:               time.Duration(300) * time.Second,
+		DefaultClientKeytabName: fmt.Sprintf("/usr/local/var/krb5/user/%s/client.keytab", uid),
+		DefaultKeytabName:       "/etc/krb5.keytab",
+		DefaultTGSEnctypes:      []string{"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:      []string{"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"},
+		DNSCanonicalizeHostname: true,
+		K5LoginDirectory:        hdir,
+		KDCDefaultOptions:       opts,
+		KDCTimeSync:             1,
+		NoAddresses:             true,
+		PermittedEnctypes:       []string{"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"},
+		RDNS:                    true,
+		RealmTryDomains:         -1,
+		SafeChecksumType:        8,
+		TicketLifetime:          time.Duration(24) * time.Hour,
+		UDPPreferenceLimit:      1465,
+		PreferredPreauthTypes:   []int{17, 16, 15, 14},
+	}
+}
+
+// Parse the lines of the [libdefaults] section of the configuration into the LibDefaults struct.
+func (l *LibDefaults) parseLines(lines []string) error {
+	for _, line := range lines {
+		//Remove comments after the values
+		if idx := strings.IndexAny(line, "#;"); idx != -1 {
+			line = line[:idx]
+		}
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+		if !strings.Contains(line, "=") {
+			return InvalidErrorf("libdefaults section line (%s)", line)
+		}
+
+		p := strings.Split(line, "=")
+		key := strings.TrimSpace(strings.ToLower(p[0]))
+		switch key {
+		case "allow_weak_crypto":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.AllowWeakCrypto = v
+		case "canonicalize":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.Canonicalize = v
+		case "ccache_type":
+			p[1] = strings.TrimSpace(p[1])
+			v, err := strconv.ParseUint(p[1], 10, 32)
+			if err != nil || v < 0 || v > 4 {
+				return InvalidErrorf("libdefaults section line (%s)", line)
+			}
+			l.CCacheType = int(v)
+		case "clockskew":
+			d, err := parseDuration(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.Clockskew = d
+		case "default_client_keytab_name":
+			l.DefaultClientKeytabName = strings.TrimSpace(p[1])
+		case "default_keytab_name":
+			l.DefaultKeytabName = strings.TrimSpace(p[1])
+		case "default_realm":
+			l.DefaultRealm = strings.TrimSpace(p[1])
+		case "default_tgs_enctypes":
+			l.DefaultTGSEnctypes = strings.Fields(p[1])
+		case "default_tkt_enctypes":
+			l.DefaultTktEnctypes = strings.Fields(p[1])
+		case "dns_canonicalize_hostname":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.DNSCanonicalizeHostname = v
+		case "dns_lookup_kdc":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.DNSLookupKDC = v
+		case "dns_lookup_realm":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.DNSLookupRealm = v
+		case "extra_addresses":
+			ipStr := strings.TrimSpace(p[1])
+			for _, ip := range strings.Split(ipStr, ",") {
+				if eip := net.ParseIP(ip); eip != nil {
+					l.ExtraAddresses = append(l.ExtraAddresses, eip)
+				}
+			}
+		case "forwardable":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.Forwardable = v
+		case "ignore_acceptor_hostname":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.IgnoreAcceptorHostname = v
+		case "k5login_authoritative":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.K5LoginAuthoritative = v
+		case "k5login_directory":
+			l.K5LoginDirectory = strings.TrimSpace(p[1])
+		case "kdc_default_options":
+			v := strings.TrimSpace(p[1])
+			v = strings.Replace(v, "0x", "", -1)
+			b, err := hex.DecodeString(v)
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.KDCDefaultOptions.Bytes = b
+			l.KDCDefaultOptions.BitLength = len(b) * 8
+		case "kdc_timesync":
+			p[1] = strings.TrimSpace(p[1])
+			v, err := strconv.ParseInt(p[1], 10, 32)
+			if err != nil || v < 0 {
+				return InvalidErrorf("libdefaults section line (%s)", line)
+			}
+			l.KDCTimeSync = int(v)
+		case "noaddresses":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.NoAddresses = v
+		case "permitted_enctypes":
+			l.PermittedEnctypes = strings.Fields(p[1])
+		case "preferred_preauth_types":
+			p[1] = strings.TrimSpace(p[1])
+			t := strings.Split(p[1], ",")
+			var v []int
+			for _, s := range t {
+				i, err := strconv.ParseInt(s, 10, 32)
+				if err != nil {
+					return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+				}
+				v = append(v, int(i))
+			}
+			l.PreferredPreauthTypes = v
+		case "proxiable":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.Proxiable = v
+		case "rdns":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.RDNS = v
+		case "realm_try_domains":
+			p[1] = strings.TrimSpace(p[1])
+			v, err := strconv.ParseInt(p[1], 10, 32)
+			if err != nil || v < -1 {
+				return InvalidErrorf("libdefaults section line (%s)", line)
+			}
+			l.RealmTryDomains = int(v)
+		case "renew_lifetime":
+			d, err := parseDuration(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.RenewLifetime = d
+		case "safe_checksum_type":
+			p[1] = strings.TrimSpace(p[1])
+			v, err := strconv.ParseInt(p[1], 10, 32)
+			if err != nil || v < 0 {
+				return InvalidErrorf("libdefaults section line (%s)", line)
+			}
+			l.SafeChecksumType = int(v)
+		case "ticket_lifetime":
+			d, err := parseDuration(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.TicketLifetime = d
+		case "udp_preference_limit":
+			p[1] = strings.TrimSpace(p[1])
+			v, err := strconv.ParseUint(p[1], 10, 32)
+			if err != nil || v > 32700 {
+				return InvalidErrorf("libdefaults section line (%s)", line)
+			}
+			l.UDPPreferenceLimit = int(v)
+		case "verify_ap_req_nofail":
+			v, err := parseBoolean(p[1])
+			if err != nil {
+				return InvalidErrorf("libdefaults section line (%s): %v", line, err)
+			}
+			l.VerifyAPReqNofail = v
+		default:
+			//Ignore the line
+			continue
+		}
+	}
+	l.DefaultTGSEnctypeIDs = parseETypes(l.DefaultTGSEnctypes, l.AllowWeakCrypto)
+	l.DefaultTktEnctypeIDs = parseETypes(l.DefaultTktEnctypes, l.AllowWeakCrypto)
+	l.PermittedEnctypeIDs = parseETypes(l.PermittedEnctypes, l.AllowWeakCrypto)
+	return nil
+}
+
+// Realm represents an entry in the [realms] section of the configuration.
+type Realm struct {
+	Realm       string
+	AdminServer []string
+	//auth_to_local //Not implementing for now
+	//auth_to_local_names //Not implementing for now
+	DefaultDomain string
+	KDC           []string
+	KPasswdServer []string //default admin_server:464
+	MasterKDC     []string
+}
+
+// Parse the lines of a [realms] entry into the Realm struct.
+func (r *Realm) parseLines(name string, lines []string) (err error) {
+	r.Realm = name
+	var adminServerFinal bool
+	var KDCFinal bool
+	var kpasswdServerFinal bool
+	var masterKDCFinal bool
+	var ignore bool
+	var c int // counts the depth of blocks within brackets { }
+	for _, line := range lines {
+		if ignore && c > 0 && !strings.Contains(line, "{") && !strings.Contains(line, "}") {
+			continue
+		}
+		//Remove comments after the values
+		if idx := strings.IndexAny(line, "#;"); idx != -1 {
+			line = line[:idx]
+		}
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+		if !strings.Contains(line, "=") && !strings.Contains(line, "}") {
+			return InvalidErrorf("realms section line (%s)", line)
+		}
+		if strings.Contains(line, "v4_") {
+			ignore = true
+			err = UnsupportedDirective{"v4 configurations are not supported"}
+		}
+		if strings.Contains(line, "{") {
+			c++
+			if ignore {
+				continue
+			}
+		}
+		if strings.Contains(line, "}") {
+			c--
+			if c < 0 {
+				return InvalidErrorf("unpaired curly brackets")
+			}
+			if ignore {
+				if c < 1 {
+					c = 0
+					ignore = false
+				}
+				continue
+			}
+		}
+
+		p := strings.Split(line, "=")
+		key := strings.TrimSpace(strings.ToLower(p[0]))
+		v := strings.TrimSpace(p[1])
+		switch key {
+		case "admin_server":
+			appendUntilFinal(&r.AdminServer, v, &adminServerFinal)
+		case "default_domain":
+			r.DefaultDomain = v
+		case "kdc":
+			if !strings.Contains(v, ":") {
+				// No port number specified default to 88
+				if strings.HasSuffix(v, `*`) {
+					v = strings.TrimSpace(strings.TrimSuffix(v, `*`)) + ":88*"
+				} else {
+					v = strings.TrimSpace(v) + ":88"
+				}
+			}
+			appendUntilFinal(&r.KDC, v, &KDCFinal)
+		case "kpasswd_server":
+			appendUntilFinal(&r.KPasswdServer, v, &kpasswdServerFinal)
+		case "master_kdc":
+			appendUntilFinal(&r.MasterKDC, v, &masterKDCFinal)
+		default:
+			//Ignore the line
+			continue
+		}
+	}
+	//default for Kpasswd_server = admin_server:464
+	if len(r.KPasswdServer) < 1 {
+		for _, a := range r.AdminServer {
+			s := strings.Split(a, ":")
+			r.KPasswdServer = append(r.KPasswdServer, s[0]+":464")
+		}
+	}
+	return
+}
+
+// Parse the lines of the [realms] section of the configuration into an slice of Realm structs.
+func parseRealms(lines []string) (realms []Realm, err error) {
+	var name string
+	var start int
+	var c int
+	for i, l := range lines {
+		//Remove comments after the values
+		if idx := strings.IndexAny(l, "#;"); idx != -1 {
+			l = l[:idx]
+		}
+		l = strings.TrimSpace(l)
+		if l == "" {
+			continue
+		}
+		//if strings.Contains(l, "v4_") {
+		//	return nil, errors.New("v4 configurations are not supported in Realms section")
+		//}
+		if strings.Contains(l, "{") {
+			c++
+			if !strings.Contains(l, "=") {
+				return nil, fmt.Errorf("realm configuration line invalid: %s", l)
+			}
+			if c == 1 {
+				start = i
+				p := strings.Split(l, "=")
+				name = strings.TrimSpace(p[0])
+			}
+		}
+		if strings.Contains(l, "}") {
+			if c < 1 {
+				// but not started a block!!!
+				return nil, errors.New("invalid Realms section in configuration")
+			}
+			c--
+			if c == 0 {
+				var r Realm
+				e := r.parseLines(name, lines[start+1:i])
+				if e != nil {
+					if _, ok := e.(UnsupportedDirective); !ok {
+						err = e
+						return
+					}
+					err = e
+				}
+				realms = append(realms, r)
+			}
+		}
+	}
+	return
+}
+
+// DomainRealm maps the domains to realms representing the [domain_realm] section of the configuration.
+type DomainRealm map[string]string
+
+// Parse the lines of the [domain_realm] section of the configuration and add to the mapping.
+func (d *DomainRealm) parseLines(lines []string) error {
+	for _, line := range lines {
+		//Remove comments after the values
+		if idx := strings.IndexAny(line, "#;"); idx != -1 {
+			line = line[:idx]
+		}
+		if strings.TrimSpace(line) == "" {
+			continue
+		}
+		if !strings.Contains(line, "=") {
+			return InvalidErrorf("realm line (%s)", line)
+		}
+		p := strings.Split(line, "=")
+		domain := strings.TrimSpace(strings.ToLower(p[0]))
+		realm := strings.TrimSpace(p[1])
+		d.addMapping(domain, realm)
+	}
+	return nil
+}
+
+// Add a domain to realm mapping.
+func (d *DomainRealm) addMapping(domain, realm string) {
+	(*d)[domain] = realm
+}
+
+// Delete a domain to realm mapping.
+func (d *DomainRealm) deleteMapping(domain, realm string) {
+	delete(*d, domain)
+}
+
+// ResolveRealm resolves the kerberos realm for the specified domain name from the domain to realm mapping.
+// The most specific mapping is returned.
+func (c *Config) ResolveRealm(domainName string) string {
+	domainName = strings.TrimSuffix(domainName, ".")
+
+	// Try to match the entire hostname first
+	if r, ok := c.DomainRealm[domainName]; ok {
+		return r
+	}
+
+	// Try to match all DNS domain parts
+	periods := strings.Count(domainName, ".") + 1
+	for i := 2; i <= periods; i++ {
+		z := strings.SplitN(domainName, ".", i)
+		if r, ok := c.DomainRealm["."+z[len(z)-1]]; ok {
+			return r
+		}
+	}
+	return c.LibDefaults.DefaultRealm
+}
+
+// Load the KRB5 configuration from the specified file path.
+func Load(cfgPath string) (*Config, error) {
+	fh, err := os.Open(cfgPath)
+	if err != nil {
+		return nil, errors.New("configuration file could not be opened: " + cfgPath + " " + err.Error())
+	}
+	defer fh.Close()
+	scanner := bufio.NewScanner(fh)
+	return NewFromScanner(scanner)
+}
+
+// NewFromString creates a new Config struct from a string.
+func NewFromString(s string) (*Config, error) {
+	reader := strings.NewReader(s)
+	return NewFromReader(reader)
+}
+
+// NewFromReader creates a new Config struct from an io.Reader.
+func NewFromReader(r io.Reader) (*Config, error) {
+	scanner := bufio.NewScanner(r)
+	return NewFromScanner(scanner)
+}
+
+// NewFromScanner creates a new Config struct from a bufio.Scanner.
+func NewFromScanner(scanner *bufio.Scanner) (*Config, error) {
+	c := New()
+	var e error
+	sections := make(map[int]string)
+	var sectionLineNum []int
+	var lines []string
+	for scanner.Scan() {
+		// Skip comments and blank lines
+		if matched, _ := regexp.MatchString(`^\s*(#|;|\n)`, scanner.Text()); matched {
+			continue
+		}
+		if matched, _ := regexp.MatchString(`^\s*\[libdefaults\]\s*`, scanner.Text()); matched {
+			sections[len(lines)] = "libdefaults"
+			sectionLineNum = append(sectionLineNum, len(lines))
+			continue
+		}
+		if matched, _ := regexp.MatchString(`^\s*\[realms\]\s*`, scanner.Text()); matched {
+			sections[len(lines)] = "realms"
+			sectionLineNum = append(sectionLineNum, len(lines))
+			continue
+		}
+		if matched, _ := regexp.MatchString(`^\s*\[domain_realm\]\s*`, scanner.Text()); matched {
+			sections[len(lines)] = "domain_realm"
+			sectionLineNum = append(sectionLineNum, len(lines))
+			continue
+		}
+		if matched, _ := regexp.MatchString(`^\s*\[.*\]\s*`, scanner.Text()); matched {
+			sections[len(lines)] = "unknown_section"
+			sectionLineNum = append(sectionLineNum, len(lines))
+			continue
+		}
+		lines = append(lines, scanner.Text())
+	}
+	for i, start := range sectionLineNum {
+		var end int
+		if i+1 >= len(sectionLineNum) {
+			end = len(lines)
+		} else {
+			end = sectionLineNum[i+1]
+		}
+		switch section := sections[start]; section {
+		case "libdefaults":
+			err := c.LibDefaults.parseLines(lines[start:end])
+			if err != nil {
+				if _, ok := err.(UnsupportedDirective); !ok {
+					return nil, fmt.Errorf("error processing libdefaults section: %v", err)
+				}
+				e = err
+			}
+		case "realms":
+			realms, err := parseRealms(lines[start:end])
+			if err != nil {
+				if _, ok := err.(UnsupportedDirective); !ok {
+					return nil, fmt.Errorf("error processing realms section: %v", err)
+				}
+				e = err
+			}
+			c.Realms = realms
+		case "domain_realm":
+			err := c.DomainRealm.parseLines(lines[start:end])
+			if err != nil {
+				if _, ok := err.(UnsupportedDirective); !ok {
+					return nil, fmt.Errorf("error processing domaain_realm section: %v", err)
+				}
+				e = err
+			}
+		default:
+			continue
+		}
+	}
+	return c, e
+}
+
+// Parse a space delimited list of ETypes into a list of EType numbers optionally filtering out weak ETypes.
+func parseETypes(s []string, w bool) []int32 {
+	var eti []int32
+	for _, et := range s {
+		if !w {
+			var weak bool
+			for _, wet := range strings.Fields(WeakETypeList) {
+				if et == wet {
+					weak = true
+					break
+				}
+			}
+			if weak {
+				continue
+			}
+		}
+		i := etypeID.EtypeSupported(et)
+		if i != 0 {
+			eti = append(eti, i)
+		}
+	}
+	return eti
+}
+
+// Parse a time duration string in the configuration to a golang time.Duration.
+func parseDuration(s string) (time.Duration, error) {
+	s = strings.Replace(strings.TrimSpace(s), " ", "", -1)
+
+	// handle Nd[NmNs]
+	if strings.Contains(s, "d") {
+		ds := strings.SplitN(s, "d", 2)
+		dn, err := strconv.ParseUint(ds[0], 10, 32)
+		if err != nil {
+			return time.Duration(0), errors.New("invalid time duration")
+		}
+		d := time.Duration(dn*24) * time.Hour
+		if ds[1] != "" {
+			dp, err := time.ParseDuration(ds[1])
+			if err != nil {
+				return time.Duration(0), errors.New("invalid time duration")
+			}
+			d = d + dp
+		}
+		return d, nil
+	}
+
+	// handle Nm[Ns]
+	d, err := time.ParseDuration(s)
+	if err == nil {
+		return d, nil
+	}
+
+	// handle N
+	v, err := strconv.ParseUint(s, 10, 32)
+	if err == nil && v > 0 {
+		return time.Duration(v) * time.Second, nil
+	}
+
+	// handle h:m[:s]
+	if strings.Contains(s, ":") {
+		t := strings.Split(s, ":")
+		if 2 > len(t) || len(t) > 3 {
+			return time.Duration(0), errors.New("invalid time duration value")
+		}
+		var i []int
+		for _, n := range t {
+			j, err := strconv.ParseInt(n, 10, 16)
+			if err != nil {
+				return time.Duration(0), errors.New("invalid time duration value")
+			}
+			i = append(i, int(j))
+		}
+		d := time.Duration(i[0])*time.Hour + time.Duration(i[1])*time.Minute
+		if len(i) == 3 {
+			d = d + time.Duration(i[2])*time.Second
+		}
+		return d, nil
+	}
+	return time.Duration(0), errors.New("invalid time duration value")
+}
+
+// Parse possible boolean values to golang bool.
+func parseBoolean(s string) (bool, error) {
+	s = strings.TrimSpace(s)
+	v, err := strconv.ParseBool(s)
+	if err == nil {
+		return v, nil
+	}
+	switch strings.ToLower(s) {
+	case "yes":
+		return true, nil
+	case "y":
+		return true, nil
+	case "no":
+		return false, nil
+	case "n":
+		return false, nil
+	}
+	return false, errors.New("invalid boolean value")
+}
+
+// Parse array of strings but stop if an asterisk is placed at the end of a line.
+func appendUntilFinal(s *[]string, value string, final *bool) {
+	if *final {
+		return
+	}
+	if last := len(value) - 1; last >= 0 && value[last] == '*' {
+		*final = true
+		value = value[:len(value)-1]
+	}
+	*s = append(*s, value)
+}

+ 530 - 0
v8/config/krb5conf_test.go

@@ -0,0 +1,530 @@
+package config
+
+import (
+	"io/ioutil"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	krb5Conf = `
+[logging]
+ default = FILE:/var/log/kerberos/krb5libs.log
+ kdc = FILE:/var/log/kerberos/krb5kdc.log
+ admin_server = FILE:/var/log/kerberos/kadmind.log
+
+[libdefaults]
+ default_realm = TEST.GOKRB5 ; comment to be ignored
+ dns_lookup_realm = false
+
+ dns_lookup_kdc = false
+ #dns_lookup_kdc = true
+ ;dns_lookup_kdc = true
+#dns_lookup_kdc = true
+;dns_lookup_kdc = true
+ ticket_lifetime = 10h ;comment to be ignored
+ forwardable = yes #comment to be ignored
+ default_keytab_name = FILE:/etc/krb5.keytab
+
+ default_client_keytab_name = FILE:/home/gokrb5/client.keytab
+ default_tkt_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 # comment to be ignored
+
+
+[realms]
+ TEST.GOKRB5 = {
+  kdc = 10.80.88.88:88 #comment to be ignored
+  kdc = assume.port.num ;comment to be ignored
+  kdc = some.other.port:1234 # comment to be ignored
+
+  kdc = 10.80.88.88*
+  kdc = 10.1.2.3.4:88
+
+  admin_server = 10.80.88.88:749 ; comment to be ignored
+  default_domain = test.gokrb5
+ }
+ EXAMPLE.COM = {
+        kdc = kerberos.example.com
+        kdc = kerberos-1.example.com
+        admin_server = kerberos.example.com
+        auth_to_local = RULE:[1:$1@$0](.*@EXAMPLE.COM)s/.*//
+ }
+ lowercase.org = {
+  kdc = kerberos.lowercase.org
+  admin_server = kerberos.lowercase.org
+ }
+
+
+[domain_realm]
+ .test.gokrb5 = TEST.GOKRB5 #comment to be ignored
+
+ test.gokrb5 = TEST.GOKRB5 ;comment to be ignored
+ 
+  .example.com = EXAMPLE.COM # comment to be ignored
+ hostname1.example.com = EXAMPLE.COM ; comment to be ignored
+ hostname2.example.com = TEST.GOKRB5
+ .testlowercase.org = lowercase.org
+
+
+[appdefaults]
+ pam = {
+   debug = false
+
+   ticket_lifetime = 36000
+
+   renew_lifetime = 36000
+   forwardable = true
+   krb4_convert = false
+ }
+`
+	krb5Conf2 = `
+[logging]
+ default = FILE:/var/log/kerberos/krb5libs.log
+ kdc = FILE:/var/log/kerberos/krb5kdc.log
+ admin_server = FILE:/var/log/kerberos/kadmind.log
+
+[libdefaults]
+ noaddresses = true
+ default_realm = TEST.GOKRB5
+ dns_lookup_realm = false
+
+ dns_lookup_kdc = false
+ #dns_lookup_kdc = true
+ ;dns_lookup_kdc = true
+#dns_lookup_kdc = true
+;dns_lookup_kdc = true
+ ticket_lifetime = 10h
+ forwardable = yes
+ default_keytab_name = FILE:/etc/krb5.keytab
+
+ default_client_keytab_name = FILE:/home/gokrb5/client.keytab
+ default_tkt_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96
+
+[domain_realm]
+ .test.gokrb5 = TEST.GOKRB5
+
+ test.gokrb5 = TEST.GOKRB5
+
+[appdefaults]
+ pam = {
+   debug = false
+
+   ticket_lifetime = 36000
+
+   renew_lifetime = 36000
+   forwardable = true
+   krb4_convert = false
+ }
+ [realms]
+ TEST.GOKRB5 = {
+  kdc = 10.80.88.88:88
+  kdc = assume.port.num
+  kdc = some.other.port:1234
+
+  kdc = 10.80.88.88*
+  kdc = 10.1.2.3.4:88
+
+  admin_server = 10.80.88.88:749
+  default_domain = test.gokrb5
+ }
+ EXAMPLE.COM = {
+        kdc = kerberos.example.com
+        kdc = kerberos-1.example.com
+        admin_server = kerberos.example.com
+ }
+`
+	krb5ConfNoBlankLines = `
+[logging]
+ default = FILE:/var/log/kerberos/krb5libs.log
+ kdc = FILE:/var/log/kerberos/krb5kdc.log
+ admin_server = FILE:/var/log/kerberos/kadmind.log
+[libdefaults]
+ default_realm = TEST.GOKRB5
+ dns_lookup_realm = false
+ dns_lookup_kdc = false
+ #dns_lookup_kdc = true
+ ;dns_lookup_kdc = true
+#dns_lookup_kdc = true
+;dns_lookup_kdc = true
+ ticket_lifetime = 10h
+ forwardable = yes
+ default_keytab_name = FILE:/etc/krb5.keytab
+ default_client_keytab_name = FILE:/home/gokrb5/client.keytab
+ default_tkt_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96
+[realms]
+ TEST.GOKRB5 = {
+  kdc = 10.80.88.88:88
+  kdc = assume.port.num
+  kdc = some.other.port:1234
+  kdc = 10.80.88.88*
+  kdc = 10.1.2.3.4:88
+  admin_server = 10.80.88.88:749
+  default_domain = test.gokrb5
+ }
+ EXAMPLE.COM = {
+        kdc = kerberos.example.com
+        kdc = kerberos-1.example.com
+        admin_server = kerberos.example.com
+        auth_to_local = RULE:[1:$1@$0](.*@EXAMPLE.COM)s/.*//
+ }
+[domain_realm]
+ .test.gokrb5 = TEST.GOKRB5
+ test.gokrb5 = TEST.GOKRB5
+`
+	krb5ConfTabs = `
+[logging]
+	default = FILE:/var/log/kerberos/krb5libs.log
+	kdc = FILE:/var/log/kerberos/krb5kdc.log
+	admin_server = FILE:/var/log/kerberos/kadmind.log
+
+[libdefaults]
+	default_realm = TEST.GOKRB5
+	dns_lookup_realm = false
+
+	dns_lookup_kdc = false
+	#dns_lookup_kdc = true
+	;dns_lookup_kdc = true
+	#dns_lookup_kdc = true
+	;dns_lookup_kdc = true
+	ticket_lifetime = 10h
+	forwardable = yes
+	default_keytab_name = FILE:/etc/krb5.keytab
+
+	default_client_keytab_name = FILE:/home/gokrb5/client.keytab
+	default_tkt_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96
+
+
+[realms]
+	TEST.GOKRB5 = {
+		kdc = 10.80.88.88:88
+		kdc = assume.port.num
+		kdc = some.other.port:1234
+
+		kdc = 10.80.88.88*
+		kdc = 10.1.2.3.4:88
+
+		admin_server = 10.80.88.88:749
+		default_domain = test.gokrb5
+	}
+	EXAMPLE.COM = {
+		kdc = kerberos.example.com
+		kdc = kerberos-1.example.com
+		admin_server = kerberos.example.com
+		auth_to_local = RULE:[1:$1@$0](.*@EXAMPLE.COM)s/.*//
+	}
+
+
+[domain_realm]
+	.test.gokrb5 = TEST.GOKRB5
+
+	test.gokrb5 = TEST.GOKRB5
+ 
+	.example.com = EXAMPLE.COM
+	hostname1.example.com = EXAMPLE.COM
+	hostname2.example.com = TEST.GOKRB5
+
+
+[appdefaults]
+	pam = {
+	debug = false
+
+	ticket_lifetime = 36000
+
+	renew_lifetime = 36000
+	forwardable = true
+	krb4_convert = false
+}`
+
+	krb5ConfV4Lines = `
+[logging]
+ default = FILE:/var/log/kerberos/krb5libs.log
+ kdc = FILE:/var/log/kerberos/krb5kdc.log
+ admin_server = FILE:/var/log/kerberos/kadmind.log
+
+[libdefaults]
+ default_realm = TEST.GOKRB5
+ dns_lookup_realm = false
+
+ dns_lookup_kdc = false
+ #dns_lookup_kdc = true
+ ;dns_lookup_kdc = true
+#dns_lookup_kdc = true
+;dns_lookup_kdc = true
+ ticket_lifetime = 10h
+ forwardable = yes
+ default_keytab_name = FILE:/etc/krb5.keytab
+
+ default_client_keytab_name = FILE:/home/gokrb5/client.keytab
+ default_tkt_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96
+
+
+[realms]
+ TEST.GOKRB5 = {
+  kdc = 10.80.88.88:88
+  kdc = assume.port.num
+  kdc = some.other.port:1234
+
+  kdc = 10.80.88.88*
+  kdc = 10.1.2.3.4:88
+
+  admin_server = 10.80.88.88:749
+  default_domain = test.gokrb5
+    v4_name_convert = {
+     host = {
+        rcmd = host
+     }
+   }
+ }
+ EXAMPLE.COM = {
+        kdc = kerberos.example.com
+        kdc = kerberos-1.example.com
+        admin_server = kerberos.example.com
+        auth_to_local = RULE:[1:$1@$0](.*@EXAMPLE.COM)s/.*//
+ }
+
+
+[domain_realm]
+ .test.gokrb5 = TEST.GOKRB5
+
+ test.gokrb5 = TEST.GOKRB5
+ 
+  .example.com = EXAMPLE.COM
+ hostname1.example.com = EXAMPLE.COM
+ hostname2.example.com = TEST.GOKRB5
+
+
+[appdefaults]
+ pam = {
+   debug = false
+
+   ticket_lifetime = 36000
+
+   renew_lifetime = 36000
+   forwardable = true
+   krb4_convert = false
+ }
+`
+)
+
+func TestLoad(t *testing.T) {
+	t.Parallel()
+	cf, _ := ioutil.TempFile(os.TempDir(), "TEST-gokrb5-krb5.conf")
+	defer os.Remove(cf.Name())
+	cf.WriteString(krb5Conf)
+
+	c, err := Load(cf.Name())
+	if err != nil {
+		t.Fatalf("Error loading config: %v", err)
+	}
+
+	assert.Equal(t, "TEST.GOKRB5", c.LibDefaults.DefaultRealm, "[libdefaults] default_realm not as expected")
+	assert.Equal(t, false, c.LibDefaults.DNSLookupRealm, "[libdefaults] dns_lookup_realm not as expected")
+	assert.Equal(t, false, c.LibDefaults.DNSLookupKDC, "[libdefaults] dns_lookup_kdc not as expected")
+	assert.Equal(t, time.Duration(10)*time.Hour, c.LibDefaults.TicketLifetime, "[libdefaults] Ticket lifetime not as expected")
+	assert.Equal(t, true, c.LibDefaults.Forwardable, "[libdefaults] forwardable not as expected")
+	assert.Equal(t, "FILE:/etc/krb5.keytab", c.LibDefaults.DefaultKeytabName, "[libdefaults] default_keytab_name not as expected")
+	assert.Equal(t, "FILE:/home/gokrb5/client.keytab", c.LibDefaults.DefaultClientKeytabName, "[libdefaults] default_client_keytab_name not as expected")
+	assert.Equal(t, []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96"}, c.LibDefaults.DefaultTktEnctypes, "[libdefaults] default_tkt_enctypes not as expected")
+
+	assert.Equal(t, 3, len(c.Realms), "Number of realms not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.Realms[0].Realm, "[realm] realm name not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:749"}, c.Realms[0].AdminServer, "[realm] Admin_server not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:464"}, c.Realms[0].KPasswdServer, "[realm] Kpasswd_server not as expectd")
+	assert.Equal(t, "test.gokrb5", c.Realms[0].DefaultDomain, "[realm] Default_domain not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:88", "assume.port.num:88", "some.other.port:1234", "10.80.88.88:88"}, c.Realms[0].KDC, "[realm] Kdc not as expectd")
+	assert.Equal(t, []string{"kerberos.example.com:88", "kerberos-1.example.com:88"}, c.Realms[1].KDC, "[realm] Kdc not as expectd")
+	assert.Equal(t, []string{"kerberos.example.com"}, c.Realms[1].AdminServer, "[realm] Admin_server not as expectd")
+
+	assert.Equal(t, "TEST.GOKRB5", c.DomainRealm[".test.gokrb5"], "Domain to realm mapping not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.DomainRealm["test.gokrb5"], "Domain to realm mapping not as expected")
+
+}
+
+func TestLoadWithV4Lines(t *testing.T) {
+	t.Parallel()
+	cf, _ := ioutil.TempFile(os.TempDir(), "TEST-gokrb5-krb5.conf")
+	defer os.Remove(cf.Name())
+	cf.WriteString(krb5ConfV4Lines)
+
+	c, err := Load(cf.Name())
+	if err == nil {
+		t.Fatalf("error should not be nil for config that includes v4 lines")
+	}
+	if _, ok := err.(UnsupportedDirective); !ok {
+		t.Fatalf("error should be of type UnsupportedDirective: %v", err)
+	}
+
+	assert.Equal(t, "TEST.GOKRB5", c.LibDefaults.DefaultRealm, "[libdefaults] default_realm not as expected")
+	assert.Equal(t, false, c.LibDefaults.DNSLookupRealm, "[libdefaults] dns_lookup_realm not as expected")
+	assert.Equal(t, false, c.LibDefaults.DNSLookupKDC, "[libdefaults] dns_lookup_kdc not as expected")
+	assert.Equal(t, time.Duration(10)*time.Hour, c.LibDefaults.TicketLifetime, "[libdefaults] Ticket lifetime not as expected")
+	assert.Equal(t, true, c.LibDefaults.Forwardable, "[libdefaults] forwardable not as expected")
+	assert.Equal(t, "FILE:/etc/krb5.keytab", c.LibDefaults.DefaultKeytabName, "[libdefaults] default_keytab_name not as expected")
+	assert.Equal(t, "FILE:/home/gokrb5/client.keytab", c.LibDefaults.DefaultClientKeytabName, "[libdefaults] default_client_keytab_name not as expected")
+	assert.Equal(t, []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96"}, c.LibDefaults.DefaultTktEnctypes, "[libdefaults] default_tkt_enctypes not as expected")
+
+	assert.Equal(t, 2, len(c.Realms), "Number of realms not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.Realms[0].Realm, "[realm] realm name not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:749"}, c.Realms[0].AdminServer, "[realm] Admin_server not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:464"}, c.Realms[0].KPasswdServer, "[realm] Kpasswd_server not as expectd")
+	assert.Equal(t, "test.gokrb5", c.Realms[0].DefaultDomain, "[realm] Default_domain not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:88", "assume.port.num:88", "some.other.port:1234", "10.80.88.88:88"}, c.Realms[0].KDC, "[realm] Kdc not as expectd")
+	assert.Equal(t, []string{"kerberos.example.com:88", "kerberos-1.example.com:88"}, c.Realms[1].KDC, "[realm] Kdc not as expectd")
+	assert.Equal(t, []string{"kerberos.example.com"}, c.Realms[1].AdminServer, "[realm] Admin_server not as expectd")
+
+	assert.Equal(t, "TEST.GOKRB5", c.DomainRealm[".test.gokrb5"], "Domain to realm mapping not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.DomainRealm["test.gokrb5"], "Domain to realm mapping not as expected")
+
+}
+
+func TestLoad2(t *testing.T) {
+	t.Parallel()
+	c, err := NewFromString(krb5Conf2)
+	if err != nil {
+		t.Fatalf("Error loading config: %v", err)
+	}
+
+	assert.Equal(t, "TEST.GOKRB5", c.LibDefaults.DefaultRealm, "[libdefaults] default_realm not as expected")
+	assert.Equal(t, false, c.LibDefaults.DNSLookupRealm, "[libdefaults] dns_lookup_realm not as expected")
+	assert.Equal(t, false, c.LibDefaults.DNSLookupKDC, "[libdefaults] dns_lookup_kdc not as expected")
+	assert.Equal(t, time.Duration(10)*time.Hour, c.LibDefaults.TicketLifetime, "[libdefaults] Ticket lifetime not as expected")
+	assert.Equal(t, true, c.LibDefaults.Forwardable, "[libdefaults] forwardable not as expected")
+	assert.Equal(t, "FILE:/etc/krb5.keytab", c.LibDefaults.DefaultKeytabName, "[libdefaults] default_keytab_name not as expected")
+	assert.Equal(t, "FILE:/home/gokrb5/client.keytab", c.LibDefaults.DefaultClientKeytabName, "[libdefaults] default_client_keytab_name not as expected")
+	assert.Equal(t, []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96"}, c.LibDefaults.DefaultTktEnctypes, "[libdefaults] default_tkt_enctypes not as expected")
+
+	assert.Equal(t, 2, len(c.Realms), "Number of realms not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.Realms[0].Realm, "[realm] realm name not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:749"}, c.Realms[0].AdminServer, "[realm] Admin_server not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:464"}, c.Realms[0].KPasswdServer, "[realm] Kpasswd_server not as expectd")
+	assert.Equal(t, "test.gokrb5", c.Realms[0].DefaultDomain, "[realm] Default_domain not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:88", "assume.port.num:88", "some.other.port:1234", "10.80.88.88:88"}, c.Realms[0].KDC, "[realm] Kdc not as expectd")
+	assert.Equal(t, []string{"kerberos.example.com:88", "kerberos-1.example.com:88"}, c.Realms[1].KDC, "[realm] Kdc not as expectd")
+	assert.Equal(t, []string{"kerberos.example.com"}, c.Realms[1].AdminServer, "[realm] Admin_server not as expectd")
+
+	assert.Equal(t, "TEST.GOKRB5", c.DomainRealm[".test.gokrb5"], "Domain to realm mapping not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.DomainRealm["test.gokrb5"], "Domain to realm mapping not as expected")
+	assert.True(t, c.LibDefaults.NoAddresses, "No address not set as true")
+}
+
+func TestLoadNoBlankLines(t *testing.T) {
+	t.Parallel()
+	c, err := NewFromString(krb5ConfNoBlankLines)
+	if err != nil {
+		t.Fatalf("Error loading config: %v", err)
+	}
+
+	assert.Equal(t, "TEST.GOKRB5", c.LibDefaults.DefaultRealm, "[libdefaults] default_realm not as expected")
+	assert.Equal(t, false, c.LibDefaults.DNSLookupRealm, "[libdefaults] dns_lookup_realm not as expected")
+	assert.Equal(t, false, c.LibDefaults.DNSLookupKDC, "[libdefaults] dns_lookup_kdc not as expected")
+	assert.Equal(t, time.Duration(10)*time.Hour, c.LibDefaults.TicketLifetime, "[libdefaults] Ticket lifetime not as expected")
+	assert.Equal(t, true, c.LibDefaults.Forwardable, "[libdefaults] forwardable not as expected")
+	assert.Equal(t, "FILE:/etc/krb5.keytab", c.LibDefaults.DefaultKeytabName, "[libdefaults] default_keytab_name not as expected")
+	assert.Equal(t, "FILE:/home/gokrb5/client.keytab", c.LibDefaults.DefaultClientKeytabName, "[libdefaults] default_client_keytab_name not as expected")
+	assert.Equal(t, []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96"}, c.LibDefaults.DefaultTktEnctypes, "[libdefaults] default_tkt_enctypes not as expected")
+
+	assert.Equal(t, 2, len(c.Realms), "Number of realms not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.Realms[0].Realm, "[realm] realm name not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:749"}, c.Realms[0].AdminServer, "[realm] Admin_server not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:464"}, c.Realms[0].KPasswdServer, "[realm] Kpasswd_server not as expectd")
+	assert.Equal(t, "test.gokrb5", c.Realms[0].DefaultDomain, "[realm] Default_domain not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:88", "assume.port.num:88", "some.other.port:1234", "10.80.88.88:88"}, c.Realms[0].KDC, "[realm] Kdc not as expectd")
+	assert.Equal(t, []string{"kerberos.example.com:88", "kerberos-1.example.com:88"}, c.Realms[1].KDC, "[realm] Kdc not as expectd")
+	assert.Equal(t, []string{"kerberos.example.com"}, c.Realms[1].AdminServer, "[realm] Admin_server not as expectd")
+
+	assert.Equal(t, "TEST.GOKRB5", c.DomainRealm[".test.gokrb5"], "Domain to realm mapping not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.DomainRealm["test.gokrb5"], "Domain to realm mapping not as expected")
+
+}
+
+func TestLoadTabs(t *testing.T) {
+	t.Parallel()
+	cf, _ := ioutil.TempFile(os.TempDir(), "TEST-gokrb5-krb5.conf")
+	defer os.Remove(cf.Name())
+	cf.WriteString(krb5ConfTabs)
+
+	c, err := Load(cf.Name())
+	if err != nil {
+		t.Fatalf("Error loading config: %v", err)
+	}
+
+	assert.Equal(t, "TEST.GOKRB5", c.LibDefaults.DefaultRealm, "[libdefaults] default_realm not as expected")
+	assert.Equal(t, false, c.LibDefaults.DNSLookupRealm, "[libdefaults] dns_lookup_realm not as expected")
+	assert.Equal(t, false, c.LibDefaults.DNSLookupKDC, "[libdefaults] dns_lookup_kdc not as expected")
+	assert.Equal(t, time.Duration(10)*time.Hour, c.LibDefaults.TicketLifetime, "[libdefaults] Ticket lifetime not as expected")
+	assert.Equal(t, true, c.LibDefaults.Forwardable, "[libdefaults] forwardable not as expected")
+	assert.Equal(t, "FILE:/etc/krb5.keytab", c.LibDefaults.DefaultKeytabName, "[libdefaults] default_keytab_name not as expected")
+	assert.Equal(t, "FILE:/home/gokrb5/client.keytab", c.LibDefaults.DefaultClientKeytabName, "[libdefaults] default_client_keytab_name not as expected")
+	assert.Equal(t, []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96"}, c.LibDefaults.DefaultTktEnctypes, "[libdefaults] default_tkt_enctypes not as expected")
+
+	assert.Equal(t, 2, len(c.Realms), "Number of realms not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.Realms[0].Realm, "[realm] realm name not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:749"}, c.Realms[0].AdminServer, "[realm] Admin_server not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:464"}, c.Realms[0].KPasswdServer, "[realm] Kpasswd_server not as expectd")
+	assert.Equal(t, "test.gokrb5", c.Realms[0].DefaultDomain, "[realm] Default_domain not as expectd")
+	assert.Equal(t, []string{"10.80.88.88:88", "assume.port.num:88", "some.other.port:1234", "10.80.88.88:88"}, c.Realms[0].KDC, "[realm] Kdc not as expectd")
+	assert.Equal(t, []string{"kerberos.example.com:88", "kerberos-1.example.com:88"}, c.Realms[1].KDC, "[realm] Kdc not as expectd")
+	assert.Equal(t, []string{"kerberos.example.com"}, c.Realms[1].AdminServer, "[realm] Admin_server not as expectd")
+
+	assert.Equal(t, "TEST.GOKRB5", c.DomainRealm[".test.gokrb5"], "Domain to realm mapping not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.DomainRealm["test.gokrb5"], "Domain to realm mapping not as expected")
+
+}
+
+func TestParseDuration(t *testing.T) {
+	t.Parallel()
+	// https://web.mit.edu/kerberos/krb5-1.12/doc/basic/date_format.html#duration
+	hms, _ := time.ParseDuration("12h30m15s")
+	hm, _ := time.ParseDuration("12h30m")
+	h, _ := time.ParseDuration("12h")
+	var tests = []struct {
+		timeStr  string
+		duration time.Duration
+	}{
+		{"100", time.Duration(100) * time.Second},
+		{"12:30", hm},
+		{"12:30:15", hms},
+		{"1d12h30m15s", time.Duration(24)*time.Hour + hms},
+		{"1d12h30m", time.Duration(24)*time.Hour + hm},
+		{"1d12h", time.Duration(24)*time.Hour + h},
+		{"1d", time.Duration(24) * time.Hour},
+	}
+	for _, test := range tests {
+		d, err := parseDuration(test.timeStr)
+		if err != nil {
+			t.Errorf("error parsing %s: %v", test.timeStr, err)
+		}
+		assert.Equal(t, test.duration, d, "Duration not as expected for: "+test.timeStr)
+
+	}
+
+}
+
+func TestResolveRealm(t *testing.T) {
+	t.Parallel()
+	c, err := NewFromString(krb5Conf)
+	if err != nil {
+		t.Fatalf("Error loading config: %v", err)
+	}
+
+	tests := []struct {
+		domainName string
+		want       string
+	}{
+		{"unknown.com", "TEST.GOKRB5"},
+		{"hostname1.example.com", "EXAMPLE.COM"},
+		{"hostname2.example.com", "TEST.GOKRB5"},
+		{"one.two.three.example.com", "EXAMPLE.COM"},
+		{".test.gokrb5", "TEST.GOKRB5"},
+		{"foo.testlowercase.org", "lowercase.org"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.domainName, func(t *testing.T) {
+			if got := c.ResolveRealm(tt.domainName); got != tt.want {
+				t.Errorf("config.ResolveRealm() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 348 - 0
v8/credentials/ccache.go

@@ -0,0 +1,348 @@
+package credentials
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"io/ioutil"
+	"strings"
+	"time"
+	"unsafe"
+
+	"github.com/jcmturner/gofork/encoding/asn1"
+	"github.com/jcmturner/gokrb5/v8/types"
+)
+
+const (
+	headerFieldTagKDCOffset = 1
+)
+
+// The first byte of the file always has the value 5.
+// The value of the second byte contains the version number (1 through 4)
+// Versions 1 and 2 of the file format use native byte order for integer representations.
+// Versions 3 and 4 always use big-endian byte order
+// After the two-byte version indicator, the file has three parts:
+//   1) the header (in version 4 only)
+//   2) the default principal name
+//   3) a sequence of credentials
+
+// CCache is the file credentials cache as define here: https://web.mit.edu/kerberos/krb5-latest/doc/formats/ccache_file_format.html
+type CCache struct {
+	Version          uint8
+	Header           header
+	DefaultPrincipal principal
+	Credentials      []*Credential
+	Path             string
+}
+
+type header struct {
+	length uint16
+	fields []headerField
+}
+
+type headerField struct {
+	tag    uint16
+	length uint16
+	value  []byte
+}
+
+// Credential cache entry principal struct.
+type principal struct {
+	Realm         string
+	PrincipalName types.PrincipalName
+}
+
+// Credential holds a Kerberos client's ccache credential information.
+type Credential struct {
+	Client       principal
+	Server       principal
+	Key          types.EncryptionKey
+	AuthTime     time.Time
+	StartTime    time.Time
+	EndTime      time.Time
+	RenewTill    time.Time
+	IsSKey       bool
+	TicketFlags  asn1.BitString
+	Addresses    []types.HostAddress
+	AuthData     []types.AuthorizationDataEntry
+	Ticket       []byte
+	SecondTicket []byte
+}
+
+// LoadCCache loads a credential cache file into a CCache type.
+func LoadCCache(cpath string) (*CCache, error) {
+	c := new(CCache)
+	b, err := ioutil.ReadFile(cpath)
+	if err != nil {
+		return c, err
+	}
+	err = c.Unmarshal(b)
+	return c, err
+}
+
+// Unmarshal a byte slice of credential cache data into CCache type.
+func (c *CCache) Unmarshal(b []byte) error {
+	p := 0
+	//The first byte of the file always has the value 5
+	if int8(b[p]) != 5 {
+		return errors.New("Invalid credential cache data. First byte does not equal 5")
+	}
+	p++
+	//Get credential cache version
+	//The second byte contains the version number (1 to 4)
+	c.Version = b[p]
+	if c.Version < 1 || c.Version > 4 {
+		return errors.New("Invalid credential cache data. Keytab version is not within 1 to 4")
+	}
+	p++
+	//Version 1 or 2 of the file format uses native byte order for integer representations. Versions 3 & 4 always uses big-endian byte order
+	var endian binary.ByteOrder
+	endian = binary.BigEndian
+	if (c.Version == 1 || c.Version == 2) && isNativeEndianLittle() {
+		endian = binary.LittleEndian
+	}
+	if c.Version == 4 {
+		err := parseHeader(b, &p, c, &endian)
+		if err != nil {
+			return err
+		}
+	}
+	c.DefaultPrincipal = parsePrincipal(b, &p, c, &endian)
+	for p < len(b) {
+		cred, err := parseCredential(b, &p, c, &endian)
+		if err != nil {
+			return err
+		}
+		c.Credentials = append(c.Credentials, cred)
+	}
+	return nil
+}
+
+func parseHeader(b []byte, p *int, c *CCache, e *binary.ByteOrder) error {
+	if c.Version != 4 {
+		return errors.New("Credentials cache version is not 4 so there is no header to parse.")
+	}
+	h := header{}
+	h.length = uint16(readInt16(b, p, e))
+	for *p <= int(h.length) {
+		f := headerField{}
+		f.tag = uint16(readInt16(b, p, e))
+		f.length = uint16(readInt16(b, p, e))
+		f.value = b[*p : *p+int(f.length)]
+		*p += int(f.length)
+		if !f.valid() {
+			return errors.New("Invalid credential cache header found")
+		}
+		h.fields = append(h.fields, f)
+	}
+	c.Header = h
+	return nil
+}
+
+// Parse the Keytab bytes of a principal into a Keytab entry's principal.
+func parsePrincipal(b []byte, p *int, c *CCache, e *binary.ByteOrder) (princ principal) {
+	if c.Version != 1 {
+		//Name Type is omitted in version 1
+		princ.PrincipalName.NameType = readInt32(b, p, e)
+	}
+	nc := int(readInt32(b, p, e))
+	if c.Version == 1 {
+		//In version 1 the number of components includes the realm. Minus 1 to make consistent with version 2
+		nc--
+	}
+	lenRealm := readInt32(b, p, e)
+	princ.Realm = string(readBytes(b, p, int(lenRealm), e))
+	for i := 0; i < nc; i++ {
+		l := readInt32(b, p, e)
+		princ.PrincipalName.NameString = append(princ.PrincipalName.NameString, string(readBytes(b, p, int(l), e)))
+	}
+	return princ
+}
+
+func parseCredential(b []byte, p *int, c *CCache, e *binary.ByteOrder) (cred *Credential, err error) {
+	cred = new(Credential)
+	cred.Client = parsePrincipal(b, p, c, e)
+	cred.Server = parsePrincipal(b, p, c, e)
+	key := types.EncryptionKey{}
+	key.KeyType = int32(readInt16(b, p, e))
+	if c.Version == 3 {
+		//repeated twice in version 3
+		key.KeyType = int32(readInt16(b, p, e))
+	}
+	key.KeyValue = readData(b, p, e)
+	cred.Key = key
+	cred.AuthTime = readTimestamp(b, p, e)
+	cred.StartTime = readTimestamp(b, p, e)
+	cred.EndTime = readTimestamp(b, p, e)
+	cred.RenewTill = readTimestamp(b, p, e)
+	if ik := readInt8(b, p, e); ik == 0 {
+		cred.IsSKey = false
+	} else {
+		cred.IsSKey = true
+	}
+	cred.TicketFlags = types.NewKrbFlags()
+	cred.TicketFlags.Bytes = readBytes(b, p, 4, e)
+	l := int(readInt32(b, p, e))
+	cred.Addresses = make([]types.HostAddress, l, l)
+	for i := range cred.Addresses {
+		cred.Addresses[i] = readAddress(b, p, e)
+	}
+	l = int(readInt32(b, p, e))
+	cred.AuthData = make([]types.AuthorizationDataEntry, l, l)
+	for i := range cred.AuthData {
+		cred.AuthData[i] = readAuthDataEntry(b, p, e)
+	}
+	cred.Ticket = readData(b, p, e)
+	cred.SecondTicket = readData(b, p, e)
+	return
+}
+
+// GetClientPrincipalName returns a PrincipalName type for the client the credentials cache is for.
+func (c *CCache) GetClientPrincipalName() types.PrincipalName {
+	return c.DefaultPrincipal.PrincipalName
+}
+
+// GetClientRealm returns the reals of the client the credentials cache is for.
+func (c *CCache) GetClientRealm() string {
+	return c.DefaultPrincipal.Realm
+}
+
+// GetClientCredentials returns a Credentials object representing the client of the credentials cache.
+func (c *CCache) GetClientCredentials() *Credentials {
+	return &Credentials{
+		username: c.DefaultPrincipal.PrincipalName.PrincipalNameString(),
+		realm:    c.GetClientRealm(),
+		cname:    c.DefaultPrincipal.PrincipalName,
+	}
+}
+
+// Contains tests if the cache contains a credential for the provided server PrincipalName
+func (c *CCache) Contains(p types.PrincipalName) bool {
+	for _, cred := range c.Credentials {
+		if cred.Server.PrincipalName.Equal(p) {
+			return true
+		}
+	}
+	return false
+}
+
+// GetEntry returns a specific credential for the PrincipalName provided.
+func (c *CCache) GetEntry(p types.PrincipalName) (*Credential, bool) {
+	cred := new(Credential)
+	var found bool
+	for i := range c.Credentials {
+		if c.Credentials[i].Server.PrincipalName.Equal(p) {
+			cred = c.Credentials[i]
+			found = true
+			break
+		}
+	}
+	if !found {
+		return cred, false
+	}
+	return cred, true
+}
+
+// GetEntries filters out configuration entries an returns a slice of credentials.
+func (c *CCache) GetEntries() []*Credential {
+	creds := make([]*Credential, 0)
+	for _, cred := range c.Credentials {
+		// Filter out configuration entries
+		if strings.HasPrefix(cred.Server.Realm, "X-CACHECONF") {
+			continue
+		}
+		creds = append(creds, cred)
+	}
+	return creds
+}
+
+func (h *headerField) valid() bool {
+	// At this time there is only one defined header field.
+	// Its tag value is 1, its length is always 8.
+	// Its contents are two 32-bit integers giving the seconds and microseconds
+	// of the time offset of the KDC relative to the client.
+	// Adding this offset to the current time on the client should give the current time on the KDC, if that offset has not changed since the initial authentication.
+
+	// Done as a switch in case other tag values are added in the future.
+	switch h.tag {
+	case headerFieldTagKDCOffset:
+		if h.length != 8 || len(h.value) != 8 {
+			return false
+		}
+		return true
+	}
+	return false
+}
+
+func readData(b []byte, p *int, e *binary.ByteOrder) []byte {
+	l := readInt32(b, p, e)
+	return readBytes(b, p, int(l), e)
+}
+
+func readAddress(b []byte, p *int, e *binary.ByteOrder) types.HostAddress {
+	a := types.HostAddress{}
+	a.AddrType = int32(readInt16(b, p, e))
+	a.Address = readData(b, p, e)
+	return a
+}
+
+func readAuthDataEntry(b []byte, p *int, e *binary.ByteOrder) types.AuthorizationDataEntry {
+	a := types.AuthorizationDataEntry{}
+	a.ADType = int32(readInt16(b, p, e))
+	a.ADData = readData(b, p, e)
+	return a
+}
+
+// Read bytes representing a timestamp.
+func readTimestamp(b []byte, p *int, e *binary.ByteOrder) time.Time {
+	return time.Unix(int64(readInt32(b, p, e)), 0)
+}
+
+// Read bytes representing an eight bit integer.
+func readInt8(b []byte, p *int, e *binary.ByteOrder) (i int8) {
+	buf := bytes.NewBuffer(b[*p : *p+1])
+	binary.Read(buf, *e, &i)
+	*p++
+	return
+}
+
+// Read bytes representing a sixteen bit integer.
+func readInt16(b []byte, p *int, e *binary.ByteOrder) (i int16) {
+	buf := bytes.NewBuffer(b[*p : *p+2])
+	binary.Read(buf, *e, &i)
+	*p += 2
+	return
+}
+
+// Read bytes representing a thirty two bit integer.
+func readInt32(b []byte, p *int, e *binary.ByteOrder) (i int32) {
+	buf := bytes.NewBuffer(b[*p : *p+4])
+	binary.Read(buf, *e, &i)
+	*p += 4
+	return
+}
+
+func readBytes(b []byte, p *int, s int, e *binary.ByteOrder) []byte {
+	buf := bytes.NewBuffer(b[*p : *p+s])
+	r := make([]byte, s)
+	binary.Read(buf, *e, &r)
+	*p += s
+	return r
+}
+
+func isNativeEndianLittle() bool {
+	var x = 0x012345678
+	var p = unsafe.Pointer(&x)
+	var bp = (*[4]byte)(p)
+
+	var endian bool
+	if 0x01 == bp[0] {
+		endian = false
+	} else if (0x78 & 0xff) == (bp[0] & 0xff) {
+		endian = true
+	} else {
+		// Default to big endian
+		endian = false
+	}
+	return endian
+}

+ 181 - 0
v8/credentials/ccache_integration_test.go

@@ -0,0 +1,181 @@
+package credentials
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"os/user"
+	"sync"
+	"testing"
+
+	"github.com/jcmturner/gokrb5/v8/iana/nametype"
+	"github.com/jcmturner/gokrb5/v8/test"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+	"github.com/jcmturner/gokrb5/v8/types"
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	kinitCmd = "kinit"
+	kvnoCmd  = "kvno"
+	klistCmd = "klist"
+	spn      = "HTTP/host.test.gokrb5"
+)
+
+type output struct {
+	buf   *bytes.Buffer
+	lines []string
+	*sync.Mutex
+}
+
+func newOutput() *output {
+	return &output{
+		buf:   &bytes.Buffer{},
+		lines: []string{},
+		Mutex: &sync.Mutex{},
+	}
+}
+
+func (rw *output) Write(p []byte) (int, error) {
+	rw.Lock()
+	defer rw.Unlock()
+	return rw.buf.Write(p)
+}
+
+func (rw *output) Lines() []string {
+	rw.Lock()
+	defer rw.Unlock()
+	s := bufio.NewScanner(rw.buf)
+	for s.Scan() {
+		rw.lines = append(rw.lines, s.Text())
+	}
+	return rw.lines
+}
+
+func login() error {
+	file, err := os.Create("/etc/krb5.conf")
+	if err != nil {
+		return fmt.Errorf("cannot open krb5.conf: %v", err)
+	}
+	defer file.Close()
+	fmt.Fprintf(file, testdata.TEST_KRB5CONF)
+
+	cmd := exec.Command(kinitCmd, "testuser1@TEST.GOKRB5")
+
+	stdinR, stdinW := io.Pipe()
+	stderrR, stderrW := io.Pipe()
+	cmd.Stdin = stdinR
+	cmd.Stderr = stderrW
+
+	err = cmd.Start()
+	if err != nil {
+		return fmt.Errorf("could not start %s command: %v", kinitCmd, err)
+	}
+
+	go func() {
+		io.WriteString(stdinW, "passwordvalue")
+		stdinW.Close()
+	}()
+	errBuf := new(bytes.Buffer)
+	go func() {
+		io.Copy(errBuf, stderrR)
+		stderrR.Close()
+	}()
+
+	err = cmd.Wait()
+	if err != nil {
+		return fmt.Errorf("%s did not run successfully: %v stderr: %s", kinitCmd, err, string(errBuf.Bytes()))
+	}
+	return nil
+}
+
+func getServiceTkt() error {
+	cmd := exec.Command(kvnoCmd, spn)
+	err := cmd.Start()
+	if err != nil {
+		return fmt.Errorf("could not start %s command: %v", kvnoCmd, err)
+	}
+	err = cmd.Wait()
+	if err != nil {
+		return fmt.Errorf("%s did not run successfully: %v", kvnoCmd, err)
+	}
+	return nil
+}
+
+func klist() ([]string, error) {
+	cmd := exec.Command(klistCmd, "-Aef")
+
+	stdout := newOutput()
+	cmd.Stdout = stdout
+
+	err := cmd.Start()
+	if err != nil {
+		return []string{}, fmt.Errorf("could not start %s command: %v", klistCmd, err)
+	}
+
+	err = cmd.Wait()
+	if err != nil {
+		return []string{}, fmt.Errorf("%s did not run successfully: %v", klistCmd, err)
+	}
+
+	return stdout.Lines(), nil
+}
+
+func loadCCache() (*CCache, error) {
+	usr, _ := user.Current()
+	cpath := "/tmp/krb5cc_" + usr.Uid
+	return LoadCCache(cpath)
+}
+
+func TestLoadCCache(t *testing.T) {
+	test.Privileged(t)
+
+	err := login()
+	if err != nil {
+		t.Fatalf("error logging in with kinit: %v", err)
+	}
+	c, err := loadCCache()
+	if err != nil {
+		t.Errorf("error loading CCache: %v", err)
+	}
+	pn := c.GetClientPrincipalName()
+	assert.Equal(t, "testuser1", pn.PrincipalNameString(), "principal not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.GetClientRealm(), "realm not as expected")
+}
+
+func TestCCacheEntries(t *testing.T) {
+	test.Privileged(t)
+
+	err := login()
+	if err != nil {
+		t.Fatalf("error logging in with kinit: %v", err)
+	}
+	err = getServiceTkt()
+	if err != nil {
+		t.Fatalf("error getting service ticket: %v", err)
+	}
+	clist, _ := klist()
+	t.Log("OS Creds Cache contents:")
+	for _, l := range clist {
+		t.Log(l)
+	}
+	c, err := loadCCache()
+	if err != nil {
+		t.Errorf("error loading CCache: %v", err)
+	}
+	creds := c.GetEntries()
+	var found bool
+	n := types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, spn)
+	for _, cred := range creds {
+		if cred.Server.PrincipalName.Equal(n) {
+			found = true
+			break
+		}
+	}
+	if !found {
+		t.Errorf("Entry for %s not found in CCache", spn)
+	}
+}

+ 131 - 0
v8/credentials/ccache_test.go

@@ -0,0 +1,131 @@
+package credentials
+
+import (
+	"encoding/hex"
+	"testing"
+
+	"github.com/jcmturner/gokrb5/v8/iana/nametype"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+	"github.com/jcmturner/gokrb5/v8/types"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParse(t *testing.T) {
+	t.Parallel()
+	b, err := hex.DecodeString(testdata.CCACHE_TEST)
+	if err != nil {
+		t.Fatal("Error decoding test data")
+	}
+	c := new(CCache)
+	err = c.Unmarshal(b)
+	if err != nil {
+		t.Fatalf("Error parsing cache: %v", err)
+	}
+	assert.Equal(t, uint8(4), c.Version, "Version not as expected")
+	assert.Equal(t, 1, len(c.Header.fields), "Number of header fields not as expected")
+	assert.Equal(t, uint16(1), c.Header.fields[0].tag, "Header tag not as expected")
+	assert.Equal(t, uint16(8), c.Header.fields[0].length, "Length of header not as expected")
+	assert.Equal(t, "TEST.GOKRB5", c.DefaultPrincipal.Realm, "Default client principal realm not as expected")
+	assert.Equal(t, "testuser1", c.DefaultPrincipal.PrincipalName.PrincipalNameString(), "Default client principaal name not as expected")
+	assert.Equal(t, 3, len(c.Credentials), "Number of credentials not as expected")
+	tgtpn := types.PrincipalName{
+		NameType:   nametype.KRB_NT_SRV_INST,
+		NameString: []string{"krbtgt", "TEST.GOKRB5"},
+	}
+	assert.True(t, c.Contains(tgtpn), "Cache does not contain TGT credential")
+	httppn := types.PrincipalName{
+		NameType:   nametype.KRB_NT_PRINCIPAL,
+		NameString: []string{"HTTP", "host.test.gokrb5"},
+	}
+	assert.True(t, c.Contains(httppn), "Cache does not contain HTTP SPN credential")
+}
+
+func TestCCache_GetClientPrincipalName(t *testing.T) {
+	t.Parallel()
+	b, err := hex.DecodeString(testdata.CCACHE_TEST)
+	if err != nil {
+		t.Fatal("Error decoding test data")
+	}
+	c := new(CCache)
+	err = c.Unmarshal(b)
+	if err != nil {
+		t.Fatalf("Error parsing cache: %v", err)
+	}
+	pn := types.PrincipalName{
+		NameType:   nametype.KRB_NT_PRINCIPAL,
+		NameString: []string{"testuser1"},
+	}
+	assert.Equal(t, pn, c.GetClientPrincipalName(), "Client PrincipalName not as expected")
+}
+
+func TestCCache_GetClientCredentials(t *testing.T) {
+	t.Parallel()
+	b, err := hex.DecodeString(testdata.CCACHE_TEST)
+	if err != nil {
+		t.Fatal("Error decoding test data")
+	}
+	c := new(CCache)
+	err = c.Unmarshal(b)
+	if err != nil {
+		t.Fatalf("Error parsing cache: %v", err)
+	}
+	pn := types.PrincipalName{
+		NameType:   nametype.KRB_NT_PRINCIPAL,
+		NameString: []string{"testuser1"},
+	}
+	cred := c.GetClientCredentials()
+	assert.Equal(t, "TEST.GOKRB5", cred.Domain(), "Client realm in credential not as expected")
+	assert.Equal(t, pn, cred.CName(), "Client Principal Name not as expected")
+	assert.Equal(t, "testuser1", cred.UserName(), "Username not as expected")
+}
+
+func TestCCache_GetClientRealm(t *testing.T) {
+	t.Parallel()
+	b, err := hex.DecodeString(testdata.CCACHE_TEST)
+	if err != nil {
+		t.Fatal("Error decoding test data")
+	}
+	c := new(CCache)
+	err = c.Unmarshal(b)
+	if err != nil {
+		t.Fatalf("Error parsing cache: %v", err)
+	}
+	assert.Equal(t, "TEST.GOKRB5", c.GetClientRealm(), "Client realm not as expected")
+}
+
+func TestCCache_GetEntry(t *testing.T) {
+	t.Parallel()
+	b, err := hex.DecodeString(testdata.CCACHE_TEST)
+	if err != nil {
+		t.Fatal("Error decoding test data")
+	}
+	c := new(CCache)
+	err = c.Unmarshal(b)
+	if err != nil {
+		t.Fatalf("Error parsing cache: %v", err)
+	}
+	httppn := types.PrincipalName{
+		NameType:   nametype.KRB_NT_PRINCIPAL,
+		NameString: []string{"HTTP", "host.test.gokrb5"},
+	}
+	cred, ok := c.GetEntry(httppn)
+	if !ok {
+		t.Fatal("Could not get entry from CCache as not found")
+	}
+	assert.Equal(t, httppn, cred.Server.PrincipalName, "Credential does not have the right server principal name")
+}
+
+func TestCCache_GetEntries(t *testing.T) {
+	t.Parallel()
+	b, err := hex.DecodeString(testdata.CCACHE_TEST)
+	if err != nil {
+		t.Fatal("Error decoding test data")
+	}
+	c := new(CCache)
+	err = c.Unmarshal(b)
+	if err != nil {
+		t.Fatalf("Error parsing cache: %v", err)
+	}
+	creds := c.GetEntries()
+	assert.Equal(t, 2, len(creds), "Number of credentials entries not as expected")
+}

+ 384 - 0
v8/credentials/credentials.go

@@ -0,0 +1,384 @@
+// Package credentials provides credentials management for Kerberos 5 authentication.
+package credentials
+
+import (
+	"bytes"
+	"encoding/gob"
+	"time"
+
+	"github.com/hashicorp/go-uuid"
+	"github.com/jcmturner/gokrb5/v8/iana/nametype"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/types"
+)
+
+const (
+	// AttributeKeyADCredentials assigned number for AD credentials.
+	AttributeKeyADCredentials = "gokrb5AttributeKeyADCredentials"
+)
+
+// Credentials struct for a user.
+// Contains either a keytab, password or both.
+// Keytabs are used over passwords if both are defined.
+type Credentials struct {
+	username        string
+	displayName     string
+	realm           string
+	cname           types.PrincipalName
+	keytab          *keytab.Keytab
+	password        string
+	attributes      map[string]interface{}
+	validUntil      time.Time
+	authenticated   bool
+	human           bool
+	authTime        time.Time
+	groupMembership map[string]bool
+	sessionID       string
+}
+
+// marshalCredentials is used to enable marshaling and unmarshaling of credentials
+// without having exported fields on the Credentials struct
+type marshalCredentials struct {
+	Username        string
+	DisplayName     string
+	Realm           string
+	CName           types.PrincipalName
+	Keytab          *keytab.Keytab
+	Password        string
+	Attributes      map[string]interface{}
+	ValidUntil      time.Time
+	Authenticated   bool
+	Human           bool
+	AuthTime        time.Time
+	GroupMembership map[string]bool
+	SessionID       string
+}
+
+// ADCredentials contains information obtained from the PAC.
+type ADCredentials struct {
+	EffectiveName       string
+	FullName            string
+	UserID              int
+	PrimaryGroupID      int
+	LogOnTime           time.Time
+	LogOffTime          time.Time
+	PasswordLastSet     time.Time
+	GroupMembershipSIDs []string
+	LogonDomainName     string
+	LogonDomainID       string
+	LogonServer         string
+}
+
+// New creates a new Credentials instance.
+func New(username string, realm string) *Credentials {
+	uid, err := uuid.GenerateUUID()
+	if err != nil {
+		uid = "00unique-sess-ions-uuid-unavailable0"
+	}
+	return &Credentials{
+		username:        username,
+		displayName:     username,
+		realm:           realm,
+		cname:           types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, username),
+		keytab:          keytab.New(),
+		attributes:      make(map[string]interface{}),
+		groupMembership: make(map[string]bool),
+		sessionID:       uid,
+		human:           true,
+	}
+}
+
+// NewFromPrincipalName creates a new Credentials instance with the user details provides as a PrincipalName type.
+func NewFromPrincipalName(cname types.PrincipalName, realm string) *Credentials {
+	c := New(cname.PrincipalNameString(), realm)
+	c.cname = cname
+	return c
+}
+
+// WithKeytab sets the Keytab in the Credentials struct.
+func (c *Credentials) WithKeytab(kt *keytab.Keytab) *Credentials {
+	c.keytab = kt
+	c.password = ""
+	return c
+}
+
+// Keytab returns the credential's Keytab.
+func (c *Credentials) Keytab() *keytab.Keytab {
+	return c.keytab
+}
+
+// HasKeytab queries if the Credentials has a keytab defined.
+func (c *Credentials) HasKeytab() bool {
+	if c.keytab != nil && len(c.keytab.Entries) > 0 {
+		return true
+	}
+	return false
+}
+
+// WithPassword sets the password in the Credentials struct.
+func (c *Credentials) WithPassword(password string) *Credentials {
+	c.password = password
+	c.keytab = keytab.New() // clear any keytab
+	return c
+}
+
+// Password returns the credential's password.
+func (c *Credentials) Password() string {
+	return c.password
+}
+
+// HasPassword queries if the Credentials has a password defined.
+func (c *Credentials) HasPassword() bool {
+	if c.password != "" {
+		return true
+	}
+	return false
+}
+
+// SetValidUntil sets the expiry time of the credentials
+func (c *Credentials) SetValidUntil(t time.Time) {
+	c.validUntil = t
+}
+
+// SetADCredentials adds ADCredentials attributes to the credentials
+func (c *Credentials) SetADCredentials(a ADCredentials) {
+	c.SetAttribute(AttributeKeyADCredentials, a)
+	if a.FullName != "" {
+		c.SetDisplayName(a.FullName)
+	}
+	if a.EffectiveName != "" {
+		c.SetUserName(a.EffectiveName)
+	}
+	for i := range a.GroupMembershipSIDs {
+		c.AddAuthzAttribute(a.GroupMembershipSIDs[i])
+	}
+}
+
+// GetADCredentials returns ADCredentials attributes sorted in the credential
+func (c *Credentials) GetADCredentials() ADCredentials {
+	if a, ok := c.attributes[AttributeKeyADCredentials].(ADCredentials); ok {
+		return a
+	}
+	return ADCredentials{}
+}
+
+// Methods to implement goidentity.Identity interface
+
+// UserName returns the credential's username.
+func (c *Credentials) UserName() string {
+	return c.username
+}
+
+// SetUserName sets the username value on the credential.
+func (c *Credentials) SetUserName(s string) {
+	c.username = s
+}
+
+// CName returns the credential's client principal name.
+func (c *Credentials) CName() types.PrincipalName {
+	return c.cname
+}
+
+// SetCName sets the client principal name on the credential.
+func (c *Credentials) SetCName(pn types.PrincipalName) {
+	c.cname = pn
+}
+
+// Domain returns the credential's domain.
+func (c *Credentials) Domain() string {
+	return c.realm
+}
+
+// SetDomain sets the domain value on the credential.
+func (c *Credentials) SetDomain(s string) {
+	c.realm = s
+}
+
+// Realm returns the credential's realm. Same as the domain.
+func (c *Credentials) Realm() string {
+	return c.Domain()
+}
+
+// SetRealm sets the realm value on the credential. Same as the domain
+func (c *Credentials) SetRealm(s string) {
+	c.SetDomain(s)
+}
+
+// DisplayName returns the credential's display name.
+func (c *Credentials) DisplayName() string {
+	return c.displayName
+}
+
+// SetDisplayName sets the display name value on the credential.
+func (c *Credentials) SetDisplayName(s string) {
+	c.displayName = s
+}
+
+// Human returns if the  credential represents a human or not.
+func (c *Credentials) Human() bool {
+	return c.human
+}
+
+// SetHuman sets the credential as human.
+func (c *Credentials) SetHuman(b bool) {
+	c.human = b
+}
+
+// AuthTime returns the time the credential was authenticated.
+func (c *Credentials) AuthTime() time.Time {
+	return c.authTime
+}
+
+// SetAuthTime sets the time the credential was authenticated.
+func (c *Credentials) SetAuthTime(t time.Time) {
+	c.authTime = t
+}
+
+// AuthzAttributes returns the credentials authorizing attributes.
+func (c *Credentials) AuthzAttributes() []string {
+	s := make([]string, len(c.groupMembership))
+	i := 0
+	for a := range c.groupMembership {
+		s[i] = a
+		i++
+	}
+	return s
+}
+
+// Authenticated indicates if the credential has been successfully authenticated or not.
+func (c *Credentials) Authenticated() bool {
+	return c.authenticated
+}
+
+// SetAuthenticated sets the credential as having been successfully authenticated.
+func (c *Credentials) SetAuthenticated(b bool) {
+	c.authenticated = b
+}
+
+// AddAuthzAttribute adds an authorization attribute to the credential.
+func (c *Credentials) AddAuthzAttribute(a string) {
+	c.groupMembership[a] = true
+}
+
+// RemoveAuthzAttribute removes an authorization attribute from the credential.
+func (c *Credentials) RemoveAuthzAttribute(a string) {
+	if _, ok := c.groupMembership[a]; !ok {
+		return
+	}
+	delete(c.groupMembership, a)
+}
+
+// EnableAuthzAttribute toggles an authorization attribute to an enabled state on the credential.
+func (c *Credentials) EnableAuthzAttribute(a string) {
+	if enabled, ok := c.groupMembership[a]; ok && !enabled {
+		c.groupMembership[a] = true
+	}
+}
+
+// DisableAuthzAttribute toggles an authorization attribute to a disabled state on the credential.
+func (c *Credentials) DisableAuthzAttribute(a string) {
+	if enabled, ok := c.groupMembership[a]; ok && enabled {
+		c.groupMembership[a] = false
+	}
+}
+
+// Authorized indicates if the credential has the specified authorizing attribute.
+func (c *Credentials) Authorized(a string) bool {
+	if enabled, ok := c.groupMembership[a]; ok && enabled {
+		return true
+	}
+	return false
+}
+
+// SessionID returns the credential's session ID.
+func (c *Credentials) SessionID() string {
+	return c.sessionID
+}
+
+// Expired indicates if the credential has expired.
+func (c *Credentials) Expired() bool {
+	if !c.validUntil.IsZero() && time.Now().UTC().After(c.validUntil) {
+		return true
+	}
+	return false
+}
+
+// ValidUntil returns the credential's valid until date
+func (c *Credentials) ValidUntil() time.Time {
+	return c.validUntil
+}
+
+// Attributes returns the Credentials' attributes map.
+func (c *Credentials) Attributes() map[string]interface{} {
+	return c.attributes
+}
+
+// SetAttribute sets the value of an attribute.
+func (c *Credentials) SetAttribute(k string, v interface{}) {
+	c.attributes[k] = v
+}
+
+// SetAttributes replaces the attributes map with the one provided.
+func (c *Credentials) SetAttributes(a map[string]interface{}) {
+	c.attributes = a
+}
+
+// RemoveAttribute deletes an attribute from the attribute map that has the key provided.
+func (c *Credentials) RemoveAttribute(k string) {
+	delete(c.attributes, k)
+}
+
+// Marshal the Credentials into a byte slice
+func (c *Credentials) Marshal() ([]byte, error) {
+	gob.Register(map[string]interface{}{})
+	gob.Register(ADCredentials{})
+	buf := new(bytes.Buffer)
+	enc := gob.NewEncoder(buf)
+	mc := marshalCredentials{
+		Username:        c.username,
+		DisplayName:     c.displayName,
+		Realm:           c.realm,
+		CName:           c.cname,
+		Keytab:          c.keytab,
+		Password:        c.password,
+		Attributes:      c.attributes,
+		ValidUntil:      c.validUntil,
+		Authenticated:   c.authenticated,
+		Human:           c.human,
+		AuthTime:        c.authTime,
+		GroupMembership: c.groupMembership,
+		SessionID:       c.sessionID,
+	}
+	err := enc.Encode(&mc)
+	if err != nil {
+		return []byte{}, err
+	}
+	return buf.Bytes(), nil
+}
+
+// Unmarshal a byte slice into Credentials
+func (c *Credentials) Unmarshal(b []byte) error {
+	gob.Register(map[string]interface{}{})
+	gob.Register(ADCredentials{})
+	mc := new(marshalCredentials)
+	buf := bytes.NewBuffer(b)
+	dec := gob.NewDecoder(buf)
+	err := dec.Decode(mc)
+	if err != nil {
+		return err
+	}
+	c.username = mc.Username
+	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
+	c.human = mc.Human
+	c.authTime = mc.AuthTime
+	c.groupMembership = mc.GroupMembership
+	c.sessionID = mc.SessionID
+	return nil
+}

+ 28 - 0
v8/credentials/credentials_test.go

@@ -0,0 +1,28 @@
+package credentials
+
+import (
+	"testing"
+
+	"github.com/jcmturner/goidentity/v6"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestImplementsInterface(t *testing.T) {
+	t.Parallel()
+	u := new(Credentials)
+	i := new(goidentity.Identity)
+	assert.Implements(t, i, u, "Credentials type does not implement the Identity interface")
+}
+
+func TestCredentials_Marshal(t *testing.T) {
+	var cred Credentials
+	b, err := cred.Marshal()
+	if err != nil {
+		t.Fatalf("could not marshal credetials: %v", err)
+	}
+	var credum Credentials
+	err = credum.Unmarshal(b)
+	if err != nil {
+		t.Fatalf("could not unmarshal credetials: %v", err)
+	}
+}

+ 173 - 0
v8/crypto/aes128-cts-hmac-sha1-96.go

@@ -0,0 +1,173 @@
+package crypto
+
+import (
+	"crypto/aes"
+	"crypto/hmac"
+	"crypto/sha1"
+	"hash"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc3961"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc3962"
+	"github.com/jcmturner/gokrb5/v8/iana/chksumtype"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+)
+
+// RFC 3962
+//+--------------------------------------------------------------------+
+//|               protocol key format        128- or 256-bit string    |
+//|                                                                    |
+//|            string-to-key function        PBKDF2+DK with variable   |
+//|                                          iteration count (see      |
+//|                                          above)                    |
+//|                                                                    |
+//|  default string-to-key parameters        00 00 10 00               |
+//|                                                                    |
+//|        key-generation seed length        key size                  |
+//|                                                                    |
+//|            random-to-key function        identity function         |
+//|                                                                    |
+//|                  hash function, H        SHA-1                     |
+//|                                                                    |
+//|               HMAC output size, h        12 octets (96 bits)       |
+//|                                                                    |
+//|             message block size, m        1 octet                   |
+//|                                                                    |
+//|  encryption/decryption functions,        AES in CBC-CTS mode       |
+//|  E and D                                 (cipher block size 16     |
+//|                                          octets), with next-to-    |
+//|                                          last block (last block    |
+//|                                          if only one) as CBC-style |
+//|                                          ivec                      |
+//+--------------------------------------------------------------------+
+//
+//+--------------------------------------------------------------------+
+//|                         encryption types                           |
+//+--------------------------------------------------------------------+
+//|         type name                  etype value          key size   |
+//+--------------------------------------------------------------------+
+//|   aes128-cts-hmac-sha1-96              17                 128      |
+//|   aes256-cts-hmac-sha1-96              18                 256      |
+//+--------------------------------------------------------------------+
+//
+//+--------------------------------------------------------------------+
+//|                          checksum types                            |
+//+--------------------------------------------------------------------+
+//|        type name                 sumtype value           length    |
+//+--------------------------------------------------------------------+
+//|    hmac-sha1-96-aes128                15                   96      |
+//|    hmac-sha1-96-aes256                16                   96      |
+//+--------------------------------------------------------------------+
+
+// Aes128CtsHmacSha96 implements Kerberos encryption type aes128-cts-hmac-sha1-96
+type Aes128CtsHmacSha96 struct {
+}
+
+// GetETypeID returns the EType ID number.
+func (e Aes128CtsHmacSha96) GetETypeID() int32 {
+	return etypeID.AES128_CTS_HMAC_SHA1_96
+}
+
+// GetHashID returns the checksum type ID number.
+func (e Aes128CtsHmacSha96) GetHashID() int32 {
+	return chksumtype.HMAC_SHA1_96_AES128
+}
+
+// GetKeyByteSize returns the number of bytes for key of this etype.
+func (e Aes128CtsHmacSha96) GetKeyByteSize() int {
+	return 128 / 8
+}
+
+// GetKeySeedBitLength returns the number of bits for the seed for key generation.
+func (e Aes128CtsHmacSha96) GetKeySeedBitLength() int {
+	return e.GetKeyByteSize() * 8
+}
+
+// GetHashFunc returns the hash function for this etype.
+func (e Aes128CtsHmacSha96) GetHashFunc() func() hash.Hash {
+	return sha1.New
+}
+
+// GetMessageBlockByteSize returns the block size for the etype's messages.
+func (e Aes128CtsHmacSha96) GetMessageBlockByteSize() int {
+	return 1
+}
+
+// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
+func (e Aes128CtsHmacSha96) GetDefaultStringToKeyParams() string {
+	return "00001000"
+}
+
+// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
+func (e Aes128CtsHmacSha96) GetConfounderByteSize() int {
+	return aes.BlockSize
+}
+
+// GetHMACBitLength returns the bit count size of the integrity hash.
+func (e Aes128CtsHmacSha96) GetHMACBitLength() int {
+	return 96
+}
+
+// GetCypherBlockBitLength returns the bit count size of the cypher block.
+func (e Aes128CtsHmacSha96) GetCypherBlockBitLength() int {
+	return aes.BlockSize * 8
+}
+
+// StringToKey returns a key derived from the string provided.
+func (e Aes128CtsHmacSha96) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
+	return rfc3962.StringToKey(secret, salt, s2kparams, e)
+}
+
+// RandomToKey returns a key from the bytes provided.
+func (e Aes128CtsHmacSha96) RandomToKey(b []byte) []byte {
+	return rfc3961.RandomToKey(b)
+}
+
+// EncryptData encrypts the data provided.
+func (e Aes128CtsHmacSha96) EncryptData(key, data []byte) ([]byte, []byte, error) {
+	return rfc3962.EncryptData(key, data, e)
+}
+
+// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
+func (e Aes128CtsHmacSha96) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
+	return rfc3962.EncryptMessage(key, message, usage, e)
+}
+
+// DecryptData decrypts the data provided.
+func (e Aes128CtsHmacSha96) DecryptData(key, data []byte) ([]byte, error) {
+	return rfc3962.DecryptData(key, data, e)
+}
+
+// DecryptMessage decrypts the message provided and verifies the integrity of the message.
+func (e Aes128CtsHmacSha96) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
+	return rfc3962.DecryptMessage(key, ciphertext, usage, e)
+}
+
+// DeriveKey derives a key from the protocol key based on the usage value.
+func (e Aes128CtsHmacSha96) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
+	return rfc3961.DeriveKey(protocolKey, usage, e)
+}
+
+// DeriveRandom generates data needed for key generation.
+func (e Aes128CtsHmacSha96) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
+	return rfc3961.DeriveRandom(protocolKey, usage, e)
+}
+
+// VerifyIntegrity checks the integrity of the plaintext message.
+func (e Aes128CtsHmacSha96) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
+	return rfc3961.VerifyIntegrity(protocolKey, ct, pt, usage, e)
+}
+
+// GetChecksumHash returns a keyed checksum hash of the bytes provided.
+func (e Aes128CtsHmacSha96) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
+	return common.GetHash(data, protocolKey, common.GetUsageKc(usage), e)
+}
+
+// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
+func (e Aes128CtsHmacSha96) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
+	c, err := e.GetChecksumHash(protocolKey, data, usage)
+	if err != nil {
+		return false
+	}
+	return hmac.Equal(chksum, c)
+}

+ 45 - 0
v8/crypto/aes128-cts-hmac-sha1-96_test.go

@@ -0,0 +1,45 @@
+package crypto
+
+import (
+	"encoding/hex"
+	"testing"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc3962"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAes128CtsHmacSha196_StringToKey(t *testing.T) {
+	t.Parallel()
+	// Test vectors from RFC 3962 Appendix B
+	b, _ := hex.DecodeString("1234567878563412")
+	s := string(b)
+	b, _ = hex.DecodeString("f09d849e")
+	s2 := string(b)
+	var tests = []struct {
+		iterations int64
+		phrase     string
+		salt       string
+		pbkdf2     string
+		key        string
+	}{
+		{1, "password", "ATHENA.MIT.EDUraeburn", "cdedb5281bb2f801565a1122b2563515", "42263c6e89f4fc28b8df68ee09799f15"},
+		{2, "password", "ATHENA.MIT.EDUraeburn", "01dbee7f4a9e243e988b62c73cda935d", "c651bf29e2300ac27fa469d693bdda13"},
+		{1200, "password", "ATHENA.MIT.EDUraeburn", "5c08eb61fdf71e4e4ec3cf6ba1f5512b", "4c01cd46d632d01e6dbe230a01ed642a"},
+		{5, "password", s, "d1daa78615f287e6a1c8b120d7062a49", "e9b23d52273747dd5c35cb55be619d8e"},
+		{1200, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "pass phrase equals block size", "139c30c0966bc32ba55fdbf212530ac9", "59d1bb789a828b1aa54ef9c2883f69ed"},
+		{1200, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "pass phrase exceeds block size", "9ccad6d468770cd51b10e6a68721be61", "cb8005dc5f90179a7f02104c0018751d"},
+		{50, s2, "EXAMPLE.COMpianist", "6b9cf26d45455a43a5b8bb276a403b39", "f149c1f2e154a73452d43e7fe62a56e5"},
+	}
+	var e Aes128CtsHmacSha96
+	for i, test := range tests {
+
+		assert.Equal(t, test.pbkdf2, hex.EncodeToString(rfc3962.StringToPBKDF2(test.phrase, test.salt, test.iterations, e)), "PBKDF2 not as expected")
+		k, err := e.StringToKey(test.phrase, test.salt, common.IterationsToS2Kparams(uint32(test.iterations)))
+		if err != nil {
+			t.Errorf("error in processing string to key for test %d: %v", i, err)
+		}
+		assert.Equal(t, test.key, hex.EncodeToString(k), "String to Key not as expected")
+
+	}
+}

+ 135 - 0
v8/crypto/aes128-cts-hmac-sha256-128.go

@@ -0,0 +1,135 @@
+package crypto
+
+import (
+	"crypto/aes"
+	"crypto/hmac"
+	"crypto/sha256"
+	"hash"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc8009"
+	"github.com/jcmturner/gokrb5/v8/iana/chksumtype"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+)
+
+// RFC https://tools.ietf.org/html/rfc8009
+
+// Aes128CtsHmacSha256128 implements Kerberos encryption type aes128-cts-hmac-sha256-128
+type Aes128CtsHmacSha256128 struct {
+}
+
+// GetETypeID returns the EType ID number.
+func (e Aes128CtsHmacSha256128) GetETypeID() int32 {
+	return etypeID.AES128_CTS_HMAC_SHA256_128
+}
+
+// GetHashID returns the checksum type ID number.
+func (e Aes128CtsHmacSha256128) GetHashID() int32 {
+	return chksumtype.HMAC_SHA256_128_AES128
+}
+
+// GetKeyByteSize returns the number of bytes for key of this etype.
+func (e Aes128CtsHmacSha256128) GetKeyByteSize() int {
+	return 128 / 8
+}
+
+// GetKeySeedBitLength returns the number of bits for the seed for key generation.
+func (e Aes128CtsHmacSha256128) GetKeySeedBitLength() int {
+	return e.GetKeyByteSize() * 8
+}
+
+// GetHashFunc returns the hash function for this etype.
+func (e Aes128CtsHmacSha256128) GetHashFunc() func() hash.Hash {
+	return sha256.New
+}
+
+// GetMessageBlockByteSize returns the block size for the etype's messages.
+func (e Aes128CtsHmacSha256128) GetMessageBlockByteSize() int {
+	return 1
+}
+
+// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
+func (e Aes128CtsHmacSha256128) GetDefaultStringToKeyParams() string {
+	return "00008000"
+}
+
+// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
+func (e Aes128CtsHmacSha256128) GetConfounderByteSize() int {
+	return aes.BlockSize
+}
+
+// GetHMACBitLength returns the bit count size of the integrity hash.
+func (e Aes128CtsHmacSha256128) GetHMACBitLength() int {
+	return 128
+}
+
+// GetCypherBlockBitLength returns the bit count size of the cypher block.
+func (e Aes128CtsHmacSha256128) GetCypherBlockBitLength() int {
+	return aes.BlockSize * 8
+}
+
+// StringToKey returns a key derived from the string provided.
+func (e Aes128CtsHmacSha256128) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
+	saltp := rfc8009.GetSaltP(salt, "aes128-cts-hmac-sha256-128")
+	return rfc8009.StringToKey(secret, saltp, s2kparams, e)
+}
+
+// RandomToKey returns a key from the bytes provided.
+func (e Aes128CtsHmacSha256128) RandomToKey(b []byte) []byte {
+	return rfc8009.RandomToKey(b)
+}
+
+// EncryptData encrypts the data provided.
+func (e Aes128CtsHmacSha256128) EncryptData(key, data []byte) ([]byte, []byte, error) {
+	return rfc8009.EncryptData(key, data, e)
+}
+
+// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
+func (e Aes128CtsHmacSha256128) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
+	return rfc8009.EncryptMessage(key, message, usage, e)
+}
+
+// DecryptData decrypts the data provided.
+func (e Aes128CtsHmacSha256128) DecryptData(key, data []byte) ([]byte, error) {
+	return rfc8009.DecryptData(key, data, e)
+}
+
+// DecryptMessage decrypts the message provided and verifies the integrity of the message.
+func (e Aes128CtsHmacSha256128) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
+	return rfc8009.DecryptMessage(key, ciphertext, usage, e)
+}
+
+// DeriveKey derives a key from the protocol key based on the usage value.
+func (e Aes128CtsHmacSha256128) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
+	return rfc8009.DeriveKey(protocolKey, usage, e), nil
+}
+
+// DeriveRandom generates data needed for key generation.
+func (e Aes128CtsHmacSha256128) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
+	return rfc8009.DeriveRandom(protocolKey, usage, e)
+}
+
+// VerifyIntegrity checks the integrity of the ciphertext message.
+// The HMAC is calculated over the cipher state concatenated with the
+// AES output, instead of being calculated over the confounder and
+// plaintext.  This allows the message receiver to verify the
+// integrity of the message before decrypting the message.
+// Therefore the pt value to this interface method is not use. Pass any []byte.
+func (e Aes128CtsHmacSha256128) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
+	// We don't need ib just there for the interface
+	return rfc8009.VerifyIntegrity(protocolKey, ct, usage, e)
+}
+
+// GetChecksumHash returns a keyed checksum hash of the bytes provided.
+func (e Aes128CtsHmacSha256128) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
+	return common.GetHash(data, protocolKey, common.GetUsageKc(usage), e)
+}
+
+// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
+func (e Aes128CtsHmacSha256128) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
+	c, err := e.GetChecksumHash(protocolKey, data, usage)
+	if err != nil {
+		return false
+	}
+	return hmac.Equal(chksum, c)
+}

+ 148 - 0
v8/crypto/aes128-cts-hmac-sha256-128_test.go

@@ -0,0 +1,148 @@
+package crypto
+
+import (
+	"encoding/hex"
+	"testing"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc8009"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAes128CtsHmacSha256128_StringToKey(t *testing.T) {
+	t.Parallel()
+	// Test vectors from RFC 8009 Appendix A
+	// Random 16bytes in test vector as string
+	r, _ := hex.DecodeString("10DF9DD783E5BC8ACEA1730E74355F61")
+	s := string(r)
+	var tests = []struct {
+		iterations uint32
+		phrase     string
+		salt       string
+		saltp      string
+		key        string
+	}{
+		{32768, "password", s + "ATHENA.MIT.EDUraeburn", "6165733132382d6374732d686d61632d7368613235362d3132380010df9dd783e5bc8acea1730e74355f61415448454e412e4d49542e4544557261656275726e", "089bca48b105ea6ea77ca5d2f39dc5e7"},
+	}
+	var e Aes128CtsHmacSha256128
+	for _, test := range tests {
+		saltp := rfc8009.GetSaltP(test.salt, "aes128-cts-hmac-sha256-128")
+		assert.Equal(t, test.saltp, hex.EncodeToString([]byte(saltp)), "SaltP not as expected")
+
+		k, _ := e.StringToKey(test.phrase, test.salt, common.IterationsToS2Kparams(test.iterations))
+		assert.Equal(t, test.key, hex.EncodeToString(k), "String to Key not as expected")
+
+	}
+}
+
+func TestAes128CtsHmacSha256128_DeriveKey(t *testing.T) {
+	t.Parallel()
+	// Test vectors from RFC 8009 Appendix A
+	protocolBaseKey, _ := hex.DecodeString("3705d96080c17728a0e800eab6e0d23c")
+	testUsage := uint32(2)
+	var e Aes128CtsHmacSha256128
+	k, err := e.DeriveKey(protocolBaseKey, common.GetUsageKc(testUsage))
+	if err != nil {
+		t.Fatalf("Error deriving checksum key: %v", err)
+	}
+	assert.Equal(t, "b31a018a48f54776f403e9a396325dc3", hex.EncodeToString(k), "Checksum derived key not as epxected")
+	k, err = e.DeriveKey(protocolBaseKey, common.GetUsageKe(testUsage))
+	if err != nil {
+		t.Fatalf("Error deriving encryption key: %v", err)
+	}
+	assert.Equal(t, "9b197dd1e8c5609d6e67c3e37c62c72e", hex.EncodeToString(k), "Encryption derived key not as epxected")
+	k, err = e.DeriveKey(protocolBaseKey, common.GetUsageKi(testUsage))
+	if err != nil {
+		t.Fatalf("Error deriving integrity key: %v", err)
+	}
+	assert.Equal(t, "9fda0e56ab2d85e1569a688696c26a6c", hex.EncodeToString(k), "Integrity derived key not as epxected")
+}
+
+func TestAes128CtsHmacSha256128_VerifyIntegrity(t *testing.T) {
+	t.Parallel()
+	// Test vectors from RFC 8009
+	protocolBaseKey, _ := hex.DecodeString("3705d96080c17728a0e800eab6e0d23c")
+	testUsage := uint32(2)
+	var e Aes128CtsHmacSha256128
+	var tests = []struct {
+		kc     string
+		pt     string
+		chksum string
+	}{
+		{"b31a018a48f54776f403e9a396325dc3", "000102030405060708090a0b0c0d0e0f1011121314", "d78367186643d67b411cba9139fc1dee"},
+	}
+	for _, test := range tests {
+		p, _ := hex.DecodeString(test.pt)
+		b, err := e.GetChecksumHash(protocolBaseKey, p, testUsage)
+		if err != nil {
+			t.Errorf("error generating checksum: %v", err)
+		}
+		assert.Equal(t, test.chksum, hex.EncodeToString(b), "Checksum not as expected")
+	}
+}
+
+func TestAes128CtsHmacSha256128_Cypto(t *testing.T) {
+	t.Parallel()
+	protocolBaseKey, _ := hex.DecodeString("3705d96080c17728a0e800eab6e0d23c")
+	testUsage := uint32(2)
+	var tests = []struct {
+		plain      string
+		confounder string
+		ke         string
+		ki         string
+		encrypted  string // AESOutput
+		hash       string // TruncatedHMACOutput
+		cipher     string // Ciphertext(AESOutput|HMACOutput)
+	}{
+		// Test vectors from RFC 8009 Appendix A
+		{"", "7e5895eaf2672435bad817f545a37148", "9b197dd1e8c5609d6e67c3e37c62c72e", "9fda0e56ab2d85e1569a688696c26a6c", "ef85fb890bb8472f4dab20394dca781d", "ad877eda39d50c870c0d5a0a8e48c718", "ef85fb890bb8472f4dab20394dca781dad877eda39d50c870c0d5a0a8e48c718"},
+		{"000102030405", "7bca285e2fd4130fb55b1a5c83bc5b24", "9b197dd1e8c5609d6e67c3e37c62c72e", "9fda0e56ab2d85e1569a688696c26a6c", "84d7f30754ed987bab0bf3506beb09cfb55402cef7e6", "877ce99e247e52d16ed4421dfdf8976c", "84d7f30754ed987bab0bf3506beb09cfb55402cef7e6877ce99e247e52d16ed4421dfdf8976c"},
+		{"000102030405060708090a0b0c0d0e0f", "56ab21713ff62c0a1457200f6fa9948f", "9b197dd1e8c5609d6e67c3e37c62c72e", "9fda0e56ab2d85e1569a688696c26a6c", "3517d640f50ddc8ad3628722b3569d2ae07493fa8263254080ea65c1008e8fc2", "95fb4852e7d83e1e7c48c37eebe6b0d3", "3517d640f50ddc8ad3628722b3569d2ae07493fa8263254080ea65c1008e8fc295fb4852e7d83e1e7c48c37eebe6b0d3"},
+		{"000102030405060708090a0b0c0d0e0f1011121314", "a7a4e29a4728ce10664fb64e49ad3fac", "9b197dd1e8c5609d6e67c3e37c62c72e", "9fda0e56ab2d85e1569a688696c26a6c", "720f73b18d9859cd6ccb4346115cd336c70f58edc0c4437c5573544c31c813bce1e6d072c1", "86b39a413c2f92ca9b8334a287ffcbfc", "720f73b18d9859cd6ccb4346115cd336c70f58edc0c4437c5573544c31c813bce1e6d072c186b39a413c2f92ca9b8334a287ffcbfc"},
+	}
+	var e Aes128CtsHmacSha256128
+	for i, test := range tests {
+		m, _ := hex.DecodeString(test.plain)
+		b, _ := hex.DecodeString(test.encrypted)
+		ke, _ := hex.DecodeString(test.ke)
+		cf, _ := hex.DecodeString(test.confounder)
+		ct, _ := hex.DecodeString(test.cipher)
+		cfm := append(cf, m...)
+
+		// Test encryption to raw encrypted bytes
+		_, c, err := e.EncryptData(ke, cfm)
+		if err != nil {
+			t.Errorf("encryption failed for test %v: %v", i+1, err)
+		}
+		assert.Equal(t, test.encrypted, hex.EncodeToString(c), "Encrypted result not as expected - test %v", i)
+
+		// Test decryption of raw encrypted bytes
+		p, err := e.DecryptData(ke, b)
+		//Remove the confounder bytes
+		p = p[e.GetConfounderByteSize():]
+		if err != nil {
+			t.Errorf("decryption failed for test %v: %v", i+1, err)
+		}
+		assert.Equal(t, test.plain, hex.EncodeToString(p), "Decrypted result not as expected - test %v", i)
+
+		// Test integrity check of complete ciphertext message
+		assert.True(t, e.VerifyIntegrity(protocolBaseKey, ct, ct, testUsage), "Integrity check of cipher text failed")
+
+		// Test encrypting and decrypting a complete cipertext message (with confounder, integrity hash)
+		_, cm, err := e.EncryptMessage(protocolBaseKey, m, testUsage)
+		if err != nil {
+			t.Errorf("encryption to message failed for test %v: %v", i+1, err)
+		}
+		dm, err := e.DecryptMessage(protocolBaseKey, cm, testUsage)
+		if err != nil {
+			t.Errorf("decrypting complete encrypted message failed for test %v: %v", i+1, err)
+		}
+		assert.Equal(t, m, dm, "Message not as expected after encrypting and decrypting for test %v: %v", i+1, err)
+
+		// Test the integrity hash
+		ivz := make([]byte, e.GetConfounderByteSize())
+		hm := append(ivz, b...)
+		mac, _ := common.GetIntegrityHash(hm, protocolBaseKey, testUsage, e)
+		assert.Equal(t, test.hash, hex.EncodeToString(mac), "HMAC result not as expected - test %v", i)
+	}
+}

+ 173 - 0
v8/crypto/aes256-cts-hmac-sha1-96.go

@@ -0,0 +1,173 @@
+package crypto
+
+import (
+	"crypto/aes"
+	"crypto/hmac"
+	"crypto/sha1"
+	"hash"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc3961"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc3962"
+	"github.com/jcmturner/gokrb5/v8/iana/chksumtype"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+)
+
+// RFC 3962
+//+--------------------------------------------------------------------+
+//|               protocol key format        128- or 256-bit string    |
+//|                                                                    |
+//|            string-to-key function        PBKDF2+DK with variable   |
+//|                                          iteration count (see      |
+//|                                          above)                    |
+//|                                                                    |
+//|  default string-to-key parameters        00 00 10 00               |
+//|                                                                    |
+//|        key-generation seed length        key size                  |
+//|                                                                    |
+//|            random-to-key function        identity function         |
+//|                                                                    |
+//|                  hash function, H        SHA-1                     |
+//|                                                                    |
+//|               HMAC output size, h        12 octets (96 bits)       |
+//|                                                                    |
+//|             message block size, m        1 octet                   |
+//|                                                                    |
+//|  encryption/decryption functions,        AES in CBC-CTS mode       |
+//|  E and D                                 (cipher block size 16     |
+//|                                          octets), with next-to-    |
+//|                                          last block (last block    |
+//|                                          if only one) as CBC-style |
+//|                                          ivec                      |
+//+--------------------------------------------------------------------+
+//
+//+--------------------------------------------------------------------+
+//|                         encryption types                           |
+//+--------------------------------------------------------------------+
+//|         type name                  etype value          key size   |
+//+--------------------------------------------------------------------+
+//|   aes128-cts-hmac-sha1-96              17                 128      |
+//|   aes256-cts-hmac-sha1-96              18                 256      |
+//+--------------------------------------------------------------------+
+//
+//+--------------------------------------------------------------------+
+//|                          checksum types                            |
+//+--------------------------------------------------------------------+
+//|        type name                 sumtype value           length    |
+//+--------------------------------------------------------------------+
+//|    hmac-sha1-96-aes128                15                   96      |
+//|    hmac-sha1-96-aes256                16                   96      |
+//+--------------------------------------------------------------------+
+
+// Aes256CtsHmacSha96 implements Kerberos encryption type aes256-cts-hmac-sha1-96
+type Aes256CtsHmacSha96 struct {
+}
+
+// GetETypeID returns the EType ID number.
+func (e Aes256CtsHmacSha96) GetETypeID() int32 {
+	return etypeID.AES256_CTS_HMAC_SHA1_96
+}
+
+// GetHashID returns the checksum type ID number.
+func (e Aes256CtsHmacSha96) GetHashID() int32 {
+	return chksumtype.HMAC_SHA1_96_AES256
+}
+
+// GetKeyByteSize returns the number of bytes for key of this etype.
+func (e Aes256CtsHmacSha96) GetKeyByteSize() int {
+	return 256 / 8
+}
+
+// GetKeySeedBitLength returns the number of bits for the seed for key generation.
+func (e Aes256CtsHmacSha96) GetKeySeedBitLength() int {
+	return e.GetKeyByteSize() * 8
+}
+
+// GetHashFunc returns the hash function for this etype.
+func (e Aes256CtsHmacSha96) GetHashFunc() func() hash.Hash {
+	return sha1.New
+}
+
+// GetMessageBlockByteSize returns the block size for the etype's messages.
+func (e Aes256CtsHmacSha96) GetMessageBlockByteSize() int {
+	return 1
+}
+
+// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
+func (e Aes256CtsHmacSha96) GetDefaultStringToKeyParams() string {
+	return "00001000"
+}
+
+// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
+func (e Aes256CtsHmacSha96) GetConfounderByteSize() int {
+	return aes.BlockSize
+}
+
+// GetHMACBitLength returns the bit count size of the integrity hash.
+func (e Aes256CtsHmacSha96) GetHMACBitLength() int {
+	return 96
+}
+
+// GetCypherBlockBitLength returns the bit count size of the cypher block.
+func (e Aes256CtsHmacSha96) GetCypherBlockBitLength() int {
+	return aes.BlockSize * 8
+}
+
+// StringToKey returns a key derived from the string provided.
+func (e Aes256CtsHmacSha96) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
+	return rfc3962.StringToKey(secret, salt, s2kparams, e)
+}
+
+// RandomToKey returns a key from the bytes provided.
+func (e Aes256CtsHmacSha96) RandomToKey(b []byte) []byte {
+	return rfc3961.RandomToKey(b)
+}
+
+// EncryptData encrypts the data provided.
+func (e Aes256CtsHmacSha96) EncryptData(key, data []byte) ([]byte, []byte, error) {
+	return rfc3962.EncryptData(key, data, e)
+}
+
+// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
+func (e Aes256CtsHmacSha96) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
+	return rfc3962.EncryptMessage(key, message, usage, e)
+}
+
+// DecryptData decrypts the data provided.
+func (e Aes256CtsHmacSha96) DecryptData(key, data []byte) ([]byte, error) {
+	return rfc3962.DecryptData(key, data, e)
+}
+
+// DecryptMessage decrypts the message provided and verifies the integrity of the message.
+func (e Aes256CtsHmacSha96) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
+	return rfc3962.DecryptMessage(key, ciphertext, usage, e)
+}
+
+// DeriveKey derives a key from the protocol key based on the usage value.
+func (e Aes256CtsHmacSha96) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
+	return rfc3961.DeriveKey(protocolKey, usage, e)
+}
+
+// DeriveRandom generates data needed for key generation.
+func (e Aes256CtsHmacSha96) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
+	return rfc3961.DeriveRandom(protocolKey, usage, e)
+}
+
+// VerifyIntegrity checks the integrity of the plaintext message.
+func (e Aes256CtsHmacSha96) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
+	return rfc3961.VerifyIntegrity(protocolKey, ct, pt, usage, e)
+}
+
+// GetChecksumHash returns a keyed checksum hash of the bytes provided.
+func (e Aes256CtsHmacSha96) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
+	return common.GetHash(data, protocolKey, common.GetUsageKc(usage), e)
+}
+
+// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
+func (e Aes256CtsHmacSha96) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
+	c, err := e.GetChecksumHash(protocolKey, data, usage)
+	if err != nil {
+		return false
+	}
+	return hmac.Equal(chksum, c)
+}

+ 45 - 0
v8/crypto/aes256-cts-hmac-sha1-96_test.go

@@ -0,0 +1,45 @@
+package crypto
+
+import (
+	"encoding/hex"
+	"testing"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc3962"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAes256CtsHmacSha196_StringToKey(t *testing.T) {
+	t.Parallel()
+	// Test vectors from RFC 3962 Appendix B
+	b, _ := hex.DecodeString("1234567878563412")
+	s := string(b)
+	b, _ = hex.DecodeString("f09d849e")
+	s2 := string(b)
+	var tests = []struct {
+		iterations int64
+		phrase     string
+		salt       string
+		pbkdf2     string
+		key        string
+	}{
+		{1, "password", "ATHENA.MIT.EDUraeburn", "cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837", "fe697b52bc0d3ce14432ba036a92e65bbb52280990a2fa27883998d72af30161"},
+		{2, "password", "ATHENA.MIT.EDUraeburn", "01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86", "a2e16d16b36069c135d5e9d2e25f896102685618b95914b467c67622225824ff"},
+		{1200, "password", "ATHENA.MIT.EDUraeburn", "5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13", "55a6ac740ad17b4846941051e1e8b0a7548d93b0ab30a8bc3ff16280382b8c2a"},
+		{5, "password", s, "d1daa78615f287e6a1c8b120d7062a493f98d203e6be49a6adf4fa574b6e64ee", "97a4e786be20d81a382d5ebc96d5909cabcdadc87ca48f574504159f16c36e31"},
+		{1200, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "pass phrase equals block size", "139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1", "89adee3608db8bc71f1bfbfe459486b05618b70cbae22092534e56c553ba4b34"},
+		{1200, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "pass phrase exceeds block size", "9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a", "d78c5c9cb872a8c9dad4697f0bb5b2d21496c82beb2caeda2112fceea057401b"},
+		{50, s2, "EXAMPLE.COMpianist", "6b9cf26d45455a43a5b8bb276a403b39e7fe37a0c41e02c281ff3069e1e94f52", "4b6d9839f84406df1f09cc166db4b83c571848b784a3d6bdc346589a3e393f9e"},
+	}
+	var e Aes256CtsHmacSha96
+	for i, test := range tests {
+
+		assert.Equal(t, test.pbkdf2, hex.EncodeToString(rfc3962.StringToPBKDF2(test.phrase, test.salt, test.iterations, e)), "PBKDF2 not as expected")
+		k, err := e.StringToKey(test.phrase, test.salt, common.IterationsToS2Kparams(uint32(test.iterations)))
+		if err != nil {
+			t.Errorf("error in processing string to key for test %d: %v", i, err)
+		}
+		assert.Equal(t, test.key, hex.EncodeToString(k), "String to Key not as expected")
+
+	}
+}

+ 135 - 0
v8/crypto/aes256-cts-hmac-sha384-192.go

@@ -0,0 +1,135 @@
+package crypto
+
+import (
+	"crypto/aes"
+	"crypto/hmac"
+	"crypto/sha512"
+	"hash"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc8009"
+	"github.com/jcmturner/gokrb5/v8/iana/chksumtype"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+)
+
+// RFC https://tools.ietf.org/html/rfc8009
+
+// Aes256CtsHmacSha384192 implements Kerberos encryption type aes256-cts-hmac-sha384-192
+type Aes256CtsHmacSha384192 struct {
+}
+
+// GetETypeID returns the EType ID number.
+func (e Aes256CtsHmacSha384192) GetETypeID() int32 {
+	return etypeID.AES256_CTS_HMAC_SHA384_192
+}
+
+// GetHashID returns the checksum type ID number.
+func (e Aes256CtsHmacSha384192) GetHashID() int32 {
+	return chksumtype.HMAC_SHA384_192_AES256
+}
+
+// GetKeyByteSize returns the number of bytes for key of this etype.
+func (e Aes256CtsHmacSha384192) GetKeyByteSize() int {
+	return 192 / 8
+}
+
+// GetKeySeedBitLength returns the number of bits for the seed for key generation.
+func (e Aes256CtsHmacSha384192) GetKeySeedBitLength() int {
+	return e.GetKeyByteSize() * 8
+}
+
+// GetHashFunc returns the hash function for this etype.
+func (e Aes256CtsHmacSha384192) GetHashFunc() func() hash.Hash {
+	return sha512.New384
+}
+
+// GetMessageBlockByteSize returns the block size for the etype's messages.
+func (e Aes256CtsHmacSha384192) GetMessageBlockByteSize() int {
+	return 1
+}
+
+// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
+func (e Aes256CtsHmacSha384192) GetDefaultStringToKeyParams() string {
+	return "00008000"
+}
+
+// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
+func (e Aes256CtsHmacSha384192) GetConfounderByteSize() int {
+	return aes.BlockSize
+}
+
+// GetHMACBitLength returns the bit count size of the integrity hash.
+func (e Aes256CtsHmacSha384192) GetHMACBitLength() int {
+	return 192
+}
+
+// GetCypherBlockBitLength returns the bit count size of the cypher block.
+func (e Aes256CtsHmacSha384192) GetCypherBlockBitLength() int {
+	return aes.BlockSize * 8
+}
+
+// StringToKey returns a key derived from the string provided.
+func (e Aes256CtsHmacSha384192) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
+	saltp := rfc8009.GetSaltP(salt, "aes256-cts-hmac-sha384-192")
+	return rfc8009.StringToKey(secret, saltp, s2kparams, e)
+}
+
+// RandomToKey returns a key from the bytes provided.
+func (e Aes256CtsHmacSha384192) RandomToKey(b []byte) []byte {
+	return rfc8009.RandomToKey(b)
+}
+
+// EncryptData encrypts the data provided.
+func (e Aes256CtsHmacSha384192) EncryptData(key, data []byte) ([]byte, []byte, error) {
+	return rfc8009.EncryptData(key, data, e)
+}
+
+// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
+func (e Aes256CtsHmacSha384192) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
+	return rfc8009.EncryptMessage(key, message, usage, e)
+}
+
+// DecryptData decrypts the data provided.
+func (e Aes256CtsHmacSha384192) DecryptData(key, data []byte) ([]byte, error) {
+	return rfc8009.DecryptData(key, data, e)
+}
+
+// DecryptMessage decrypts the message provided and verifies the integrity of the message.
+func (e Aes256CtsHmacSha384192) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
+	return rfc8009.DecryptMessage(key, ciphertext, usage, e)
+}
+
+// DeriveKey derives a key from the protocol key based on the usage value.
+func (e Aes256CtsHmacSha384192) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
+	return rfc8009.DeriveKey(protocolKey, usage, e), nil
+}
+
+// DeriveRandom generates data needed for key generation.
+func (e Aes256CtsHmacSha384192) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
+	return rfc8009.DeriveRandom(protocolKey, usage, e)
+}
+
+// VerifyIntegrity checks the integrity of the ciphertext message.
+// The HMAC is calculated over the cipher state concatenated with the
+// AES output, instead of being calculated over the confounder and
+// plaintext.  This allows the message receiver to verify the
+// integrity of the message before decrypting the message.
+// Therefore the pt value to this interface method is not use. Pass any []byte.
+func (e Aes256CtsHmacSha384192) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
+	// We don't need ib just there for the interface
+	return rfc8009.VerifyIntegrity(protocolKey, ct, usage, e)
+}
+
+// GetChecksumHash returns a keyed checksum hash of the bytes provided.
+func (e Aes256CtsHmacSha384192) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
+	return common.GetHash(data, protocolKey, common.GetUsageKc(usage), e)
+}
+
+// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
+func (e Aes256CtsHmacSha384192) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
+	c, err := e.GetChecksumHash(protocolKey, data, usage)
+	if err != nil {
+		return false
+	}
+	return hmac.Equal(chksum, c)
+}

+ 147 - 0
v8/crypto/aes256-cts-hmac-sha384-192_test.go

@@ -0,0 +1,147 @@
+package crypto
+
+import (
+	"encoding/hex"
+	"testing"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc8009"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAes256CtsHmacSha384192_StringToKey(t *testing.T) {
+	t.Parallel()
+	// Test vectors from RFC 8009 Appendix A
+	// Random 16bytes in test vector as string
+	r, _ := hex.DecodeString("10DF9DD783E5BC8ACEA1730E74355F61")
+	s := string(r)
+	var tests = []struct {
+		iterations uint32
+		phrase     string
+		salt       string
+		saltp      string
+		key        string
+	}{
+		{32768, "password", s + "ATHENA.MIT.EDUraeburn", "6165733235362d6374732d686d61632d7368613338342d3139320010df9dd783e5bc8acea1730e74355f61415448454e412e4d49542e4544557261656275726e", "45bd806dbf6a833a9cffc1c94589a222367a79bc21c413718906e9f578a78467"},
+	}
+	var e Aes256CtsHmacSha384192
+	for _, test := range tests {
+		saltp := rfc8009.GetSaltP(test.salt, "aes256-cts-hmac-sha384-192")
+		assert.Equal(t, test.saltp, hex.EncodeToString([]byte(saltp)), "SaltP not as expected")
+
+		k, _ := e.StringToKey(test.phrase, test.salt, common.IterationsToS2Kparams(test.iterations))
+		assert.Equal(t, test.key, hex.EncodeToString(k), "String to Key not as expected")
+	}
+}
+
+func TestAes256CtsHmacSha384192_DeriveKey(t *testing.T) {
+	t.Parallel()
+	// Test vectors from RFC 8009 Appendix A
+	protocolBaseKey, _ := hex.DecodeString("6d404d37faf79f9df0d33568d320669800eb4836472ea8a026d16b7182460c52")
+	testUsage := uint32(2)
+	var e Aes256CtsHmacSha384192
+	k, err := e.DeriveKey(protocolBaseKey, common.GetUsageKc(testUsage))
+	if err != nil {
+		t.Fatalf("Error deriving checksum key: %v", err)
+	}
+	assert.Equal(t, "ef5718be86cc84963d8bbb5031e9f5c4ba41f28faf69e73d", hex.EncodeToString(k), "Checksum derived key not as epxected")
+	k, err = e.DeriveKey(protocolBaseKey, common.GetUsageKe(testUsage))
+	if err != nil {
+		t.Fatalf("Error deriving encryption key: %v", err)
+	}
+	assert.Equal(t, "56ab22bee63d82d7bc5227f6773f8ea7a5eb1c825160c38312980c442e5c7e49", hex.EncodeToString(k), "Encryption derived key not as epxected")
+	k, err = e.DeriveKey(protocolBaseKey, common.GetUsageKi(testUsage))
+	if err != nil {
+		t.Fatalf("Error deriving integrity key: %v", err)
+	}
+	assert.Equal(t, "69b16514e3cd8e56b82010d5c73012b622c4d00ffc23ed1f", hex.EncodeToString(k), "Integrity derived key not as epxected")
+}
+
+func TestAes256CtsHmacSha384192_Cypto(t *testing.T) {
+	t.Parallel()
+	protocolBaseKey, _ := hex.DecodeString("6d404d37faf79f9df0d33568d320669800eb4836472ea8a026d16b7182460c52")
+	testUsage := uint32(2)
+	var tests = []struct {
+		plain      string
+		confounder string
+		ke         string
+		ki         string
+		encrypted  string // AESOutput
+		hash       string // TruncatedHMACOutput
+		cipher     string // Ciphertext(AESOutput|HMACOutput)
+	}{
+		// Test vectors from RFC 8009 Appendix A
+		{"", "f764e9fa15c276478b2c7d0c4e5f58e4", "56ab22bee63d82d7bc5227f6773f8ea7a5eb1c825160c38312980c442e5c7e49", "69b16514e3cd8e56b82010d5c73012b622c4d00ffc23ed1f", "41f53fa5bfe7026d91faf9be959195a0", "58707273a96a40f0a01960621ac612748b9bbfbe7eb4ce3c", "41f53fa5bfe7026d91faf9be959195a058707273a96a40f0a01960621ac612748b9bbfbe7eb4ce3c"},
+		{"000102030405", "b80d3251c1f6471494256ffe712d0b9a", "56ab22bee63d82d7bc5227f6773f8ea7a5eb1c825160c38312980c442e5c7e49", "69b16514e3cd8e56b82010d5c73012b622c4d00ffc23ed1f", "4ed7b37c2bcac8f74f23c1cf07e62bc7b75fb3f637b9", "f559c7f664f69eab7b6092237526ea0d1f61cb20d69d10f2", "4ed7b37c2bcac8f74f23c1cf07e62bc7b75fb3f637b9f559c7f664f69eab7b6092237526ea0d1f61cb20d69d10f2"},
+		{"000102030405060708090a0b0c0d0e0f", "53bf8a0d105265d4e276428624ce5e63", "56ab22bee63d82d7bc5227f6773f8ea7a5eb1c825160c38312980c442e5c7e49", "69b16514e3cd8e56b82010d5c73012b622c4d00ffc23ed1f", "bc47ffec7998eb91e8115cf8d19dac4bbbe2e163e87dd37f49beca92027764f6", "8cf51f14d798c2273f35df574d1f932e40c4ff255b36a266", "bc47ffec7998eb91e8115cf8d19dac4bbbe2e163e87dd37f49beca92027764f68cf51f14d798c2273f35df574d1f932e40c4ff255b36a266"},
+		{"000102030405060708090a0b0c0d0e0f1011121314", "763e65367e864f02f55153c7e3b58af1", "56ab22bee63d82d7bc5227f6773f8ea7a5eb1c825160c38312980c442e5c7e49", "69b16514e3cd8e56b82010d5c73012b622c4d00ffc23ed1f", "40013e2df58e8751957d2878bcd2d6fe101ccfd556cb1eae79db3c3ee86429f2b2a602ac86", "fef6ecb647d6295fae077a1feb517508d2c16b4192e01f62", "40013e2df58e8751957d2878bcd2d6fe101ccfd556cb1eae79db3c3ee86429f2b2a602ac86fef6ecb647d6295fae077a1feb517508d2c16b4192e01f62"},
+	}
+	var e Aes256CtsHmacSha384192
+	for i, test := range tests {
+		m, _ := hex.DecodeString(test.plain)
+		b, _ := hex.DecodeString(test.encrypted)
+		ke, _ := hex.DecodeString(test.ke)
+		cf, _ := hex.DecodeString(test.confounder)
+		ct, _ := hex.DecodeString(test.cipher)
+		cfm := append(cf, m...)
+
+		// Test encryption to raw encrypted bytes
+		_, c, err := e.EncryptData(ke, cfm)
+		if err != nil {
+			t.Errorf("encryption failed for test %v: %v", i+1, err)
+		}
+		assert.Equal(t, test.encrypted, hex.EncodeToString(c), "Encrypted result not as expected - test %v", i)
+
+		// Test decryption of raw encrypted bytes
+		p, err := e.DecryptData(ke, b)
+		//Remove the confounder bytes
+		p = p[e.GetConfounderByteSize():]
+		if err != nil {
+			t.Errorf("decryption failed for test %v: %v", i+1, err)
+		}
+		assert.Equal(t, test.plain, hex.EncodeToString(p), "Decrypted result not as expected - test %v", i)
+
+		// Test integrity check of complete ciphertext message
+		assert.True(t, e.VerifyIntegrity(protocolBaseKey, ct, ct, testUsage), "Integrity check of cipher text failed")
+
+		// Test encrypting and decrypting a complete cipertext message (with confounder, integrity hash)
+		_, cm, err := e.EncryptMessage(protocolBaseKey, m, testUsage)
+		if err != nil {
+			t.Errorf("encryption to message failed for test %v: %v", i+1, err)
+		}
+		dm, err := e.DecryptMessage(protocolBaseKey, cm, testUsage)
+		if err != nil {
+			t.Errorf("decrypting complete encrypted message failed for test %v: %v", i+1, err)
+		}
+		assert.Equal(t, m, dm, "Message not as expected after encrypting and decrypting for test %v: %v", i+1, err)
+
+		// Test the integrity hash
+		ivz := make([]byte, e.GetConfounderByteSize())
+		hm := append(ivz, b...)
+		mac, _ := common.GetIntegrityHash(hm, protocolBaseKey, testUsage, e)
+		assert.Equal(t, test.hash, hex.EncodeToString(mac), "HMAC result not as expected - test %v", i)
+	}
+}
+
+func TestAes256CtsHmacSha384192_VerifyIntegrity(t *testing.T) {
+	t.Parallel()
+	// Test vectors from RFC 8009
+	protocolBaseKey, _ := hex.DecodeString("6d404d37faf79f9df0d33568d320669800eb4836472ea8a026d16b7182460c52")
+	testUsage := uint32(2)
+	var e Aes256CtsHmacSha384192
+	var tests = []struct {
+		kc     string
+		pt     string
+		chksum string
+	}{
+		{"ef5718be86cc84963d8bbb5031e9f5c4ba41f28faf69e73d", "000102030405060708090a0b0c0d0e0f1011121314", "45ee791567eefca37f4ac1e0222de80d43c3bfa06699672a"},
+	}
+	for _, test := range tests {
+		p, _ := hex.DecodeString(test.pt)
+		b, err := e.GetChecksumHash(protocolBaseKey, p, testUsage)
+		if err != nil {
+			t.Errorf("error generating checksum: %v", err)
+		}
+		assert.Equal(t, test.chksum, hex.EncodeToString(b), "Checksum not as expected")
+	}
+}

+ 143 - 0
v8/crypto/common/common.go

@@ -0,0 +1,143 @@
+// Package common provides encryption methods common across encryption types
+package common
+
+import (
+	"bytes"
+	"crypto/hmac"
+	"encoding/binary"
+	"encoding/hex"
+	"errors"
+	"fmt"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+)
+
+// ZeroPad pads bytes with zeros to nearest multiple of message size m.
+func ZeroPad(b []byte, m int) ([]byte, error) {
+	if m <= 0 {
+		return nil, errors.New("Invalid message block size when padding")
+	}
+	if b == nil || len(b) == 0 {
+		return nil, errors.New("Data not valid to pad: Zero size")
+	}
+	if l := len(b) % m; l != 0 {
+		n := m - l
+		z := make([]byte, n)
+		b = append(b, z...)
+	}
+	return b, nil
+}
+
+// PKCS7Pad pads bytes according to RFC 2315 to nearest multiple of message size m.
+func PKCS7Pad(b []byte, m int) ([]byte, error) {
+	if m <= 0 {
+		return nil, errors.New("Invalid message block size when padding")
+	}
+	if b == nil || len(b) == 0 {
+		return nil, errors.New("Data not valid to pad: Zero size")
+	}
+	n := m - (len(b) % m)
+	pb := make([]byte, len(b)+n)
+	copy(pb, b)
+	copy(pb[len(b):], bytes.Repeat([]byte{byte(n)}, n))
+	return pb, nil
+}
+
+// PKCS7Unpad removes RFC 2315 padding from byes where message size is m.
+func PKCS7Unpad(b []byte, m int) ([]byte, error) {
+	if m <= 0 {
+		return nil, errors.New("invalid message block size when unpadding")
+	}
+	if b == nil || len(b) == 0 {
+		return nil, errors.New("padded data not valid: Zero size")
+	}
+	if len(b)%m != 0 {
+		return nil, errors.New("padded data not valid: Not multiple of message block size")
+	}
+	c := b[len(b)-1]
+	n := int(c)
+	if n == 0 || n > len(b) {
+		return nil, errors.New("padded data not valid: Data may not have been padded")
+	}
+	for i := 0; i < n; i++ {
+		if b[len(b)-n+i] != c {
+			return nil, errors.New("padded data not valid")
+		}
+	}
+	return b[:len(b)-n], nil
+}
+
+// GetHash generates the keyed hash value according to the etype's hash function.
+func GetHash(pt, key []byte, usage []byte, etype etype.EType) ([]byte, error) {
+	k, err := etype.DeriveKey(key, usage)
+	if err != nil {
+		return nil, fmt.Errorf("unable to derive key for checksum: %v", err)
+	}
+	mac := hmac.New(etype.GetHashFunc(), k)
+	p := make([]byte, len(pt))
+	copy(p, pt)
+	mac.Write(p)
+	return mac.Sum(nil)[:etype.GetHMACBitLength()/8], nil
+}
+
+// GetChecksumHash returns a keyed checksum hash of the bytes provided.
+func GetChecksumHash(b, key []byte, usage uint32, etype etype.EType) ([]byte, error) {
+	return GetHash(b, key, GetUsageKc(usage), etype)
+}
+
+// GetIntegrityHash returns a keyed integrity hash of the bytes provided.
+func GetIntegrityHash(b, key []byte, usage uint32, etype etype.EType) ([]byte, error) {
+	return GetHash(b, key, GetUsageKi(usage), etype)
+}
+
+// VerifyChecksum compares the checksum of the msg bytes is the same as the checksum provided.
+func VerifyChecksum(key, chksum, msg []byte, usage uint32, etype etype.EType) bool {
+	//The ciphertext output is the concatenation of the output of the basic
+	//encryption function E and a (possibly truncated) HMAC using the
+	//specified hash function H, both applied to the plaintext with a
+	//random confounder prefix and sufficient padding to bring it to a
+	//multiple of the message block size.  When the HMAC is computed, the
+	//key is used in the protocol key form.
+	expectedMAC, _ := GetChecksumHash(msg, key, usage, etype)
+	return hmac.Equal(chksum, expectedMAC)
+}
+
+// GetUsageKc returns the checksum key usage value for the usage number un.
+//
+// RFC 3961: The "well-known constant" used for the DK function is the key usage number, expressed as four octets in big-endian order, followed by one octet indicated below.
+//
+// Kc = DK(base-key, usage | 0x99);
+func GetUsageKc(un uint32) []byte {
+	return getUsage(un, 0x99)
+}
+
+// GetUsageKe returns the encryption key usage value for the usage number un
+//
+// RFC 3961: The "well-known constant" used for the DK function is the key usage number, expressed as four octets in big-endian order, followed by one octet indicated below.
+//
+// Ke = DK(base-key, usage | 0xAA);
+func GetUsageKe(un uint32) []byte {
+	return getUsage(un, 0xAA)
+}
+
+// GetUsageKi returns the integrity key usage value for the usage number un
+//
+// RFC 3961: The "well-known constant" used for the DK function is the key usage number, expressed as four octets in big-endian order, followed by one octet indicated below.
+//
+// Ki = DK(base-key, usage | 0x55);
+func GetUsageKi(un uint32) []byte {
+	return getUsage(un, 0x55)
+}
+
+func getUsage(un uint32, o byte) []byte {
+	var buf bytes.Buffer
+	binary.Write(&buf, binary.BigEndian, un)
+	return append(buf.Bytes(), o)
+}
+
+// IterationsToS2Kparams converts the number of iterations as an integer to a string representation.
+func IterationsToS2Kparams(i uint32) string {
+	b := make([]byte, 4, 4)
+	binary.BigEndian.PutUint32(b, i)
+	return hex.EncodeToString(b)
+}

+ 175 - 0
v8/crypto/crypto.go

@@ -0,0 +1,175 @@
+// Package crypto implements cryptographic functions for Kerberos 5 implementation.
+package crypto
+
+import (
+	"encoding/hex"
+	"fmt"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+	"github.com/jcmturner/gokrb5/v8/iana/chksumtype"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+	"github.com/jcmturner/gokrb5/v8/iana/patype"
+	"github.com/jcmturner/gokrb5/v8/types"
+)
+
+// GetEtype returns an instances of the required etype struct for the etype ID.
+func GetEtype(id int32) (etype.EType, error) {
+	switch id {
+	case etypeID.AES128_CTS_HMAC_SHA1_96:
+		var et Aes128CtsHmacSha96
+		return et, nil
+	case etypeID.AES256_CTS_HMAC_SHA1_96:
+		var et Aes256CtsHmacSha96
+		return et, nil
+	case etypeID.AES128_CTS_HMAC_SHA256_128:
+		var et Aes128CtsHmacSha256128
+		return et, nil
+	case etypeID.AES256_CTS_HMAC_SHA384_192:
+		var et Aes256CtsHmacSha384192
+		return et, nil
+	case etypeID.DES3_CBC_SHA1_KD:
+		var et Des3CbcSha1Kd
+		return et, nil
+	case etypeID.RC4_HMAC:
+		var et RC4HMAC
+		return et, nil
+	default:
+		return nil, fmt.Errorf("unknown or unsupported EType: %d", id)
+	}
+}
+
+// GetChksumEtype returns an instances of the required etype struct for the checksum ID.
+func GetChksumEtype(id int32) (etype.EType, error) {
+	switch id {
+	case chksumtype.HMAC_SHA1_96_AES128:
+		var et Aes128CtsHmacSha96
+		return et, nil
+	case chksumtype.HMAC_SHA1_96_AES256:
+		var et Aes256CtsHmacSha96
+		return et, nil
+	case chksumtype.HMAC_SHA256_128_AES128:
+		var et Aes128CtsHmacSha256128
+		return et, nil
+	case chksumtype.HMAC_SHA384_192_AES256:
+		var et Aes256CtsHmacSha384192
+		return et, nil
+	case chksumtype.HMAC_SHA1_DES3_KD:
+		var et Des3CbcSha1Kd
+		return et, nil
+	case chksumtype.KERB_CHECKSUM_HMAC_MD5:
+		var et RC4HMAC
+		return et, nil
+	//case chksumtype.KERB_CHECKSUM_HMAC_MD5_UNSIGNED:
+	//	var et RC4HMAC
+	//	return et, nil
+	default:
+		return nil, fmt.Errorf("unknown or unsupported checksum type: %d", id)
+	}
+}
+
+// GetKeyFromPassword generates an encryption key from the principal's password.
+func GetKeyFromPassword(passwd string, cname types.PrincipalName, realm string, etypeID int32, pas types.PADataSequence) (types.EncryptionKey, etype.EType, error) {
+	var key types.EncryptionKey
+	et, err := GetEtype(etypeID)
+	if err != nil {
+		return key, et, fmt.Errorf("error getting encryption type: %v", err)
+	}
+	sk2p := et.GetDefaultStringToKeyParams()
+	var salt string
+	var paID int32
+	for _, pa := range pas {
+		switch pa.PADataType {
+		case patype.PA_PW_SALT:
+			if paID > pa.PADataType {
+				continue
+			}
+			salt = string(pa.PADataValue)
+		case patype.PA_ETYPE_INFO:
+			if paID > pa.PADataType {
+				continue
+			}
+			var eti types.ETypeInfo
+			err := eti.Unmarshal(pa.PADataValue)
+			if err != nil {
+				return key, et, fmt.Errorf("error unmashaling PA Data to PA-ETYPE-INFO2: %v", err)
+			}
+			if etypeID != eti[0].EType {
+				et, err = GetEtype(eti[0].EType)
+				if err != nil {
+					return key, et, fmt.Errorf("error getting encryption type: %v", err)
+				}
+			}
+			salt = string(eti[0].Salt)
+		case patype.PA_ETYPE_INFO2:
+			if paID > pa.PADataType {
+				continue
+			}
+			var et2 types.ETypeInfo2
+			err := et2.Unmarshal(pa.PADataValue)
+			if err != nil {
+				return key, et, fmt.Errorf("error unmashalling PA Data to PA-ETYPE-INFO2: %v", err)
+			}
+			if etypeID != et2[0].EType {
+				et, err = GetEtype(et2[0].EType)
+				if err != nil {
+					return key, et, fmt.Errorf("error getting encryption type: %v", err)
+				}
+			}
+			if len(et2[0].S2KParams) == 4 {
+				sk2p = hex.EncodeToString(et2[0].S2KParams)
+			}
+			salt = et2[0].Salt
+		}
+	}
+	if salt == "" {
+		salt = cname.GetSalt(realm)
+	}
+	k, err := et.StringToKey(passwd, salt, sk2p)
+	if err != nil {
+		return key, et, fmt.Errorf("error deriving key from string: %+v", err)
+	}
+	key = types.EncryptionKey{
+		KeyType:  etypeID,
+		KeyValue: k,
+	}
+	return key, et, nil
+}
+
+// GetEncryptedData encrypts the data provided and returns and EncryptedData type.
+// Pass a usage value of zero to use the key provided directly rather than deriving one.
+func GetEncryptedData(plainBytes []byte, key types.EncryptionKey, usage uint32, kvno int) (types.EncryptedData, error) {
+	var ed types.EncryptedData
+	et, err := GetEtype(key.KeyType)
+	if err != nil {
+		return ed, fmt.Errorf("error getting etype: %v", err)
+	}
+	_, b, err := et.EncryptMessage(key.KeyValue, plainBytes, usage)
+	if err != nil {
+		return ed, err
+	}
+
+	ed = types.EncryptedData{
+		EType:  key.KeyType,
+		Cipher: b,
+		KVNO:   kvno,
+	}
+	return ed, nil
+}
+
+// DecryptEncPart decrypts the EncryptedData.
+func DecryptEncPart(ed types.EncryptedData, key types.EncryptionKey, usage uint32) ([]byte, error) {
+	return DecryptMessage(ed.Cipher, key, usage)
+}
+
+// DecryptMessage decrypts the ciphertext and verifies the integrity.
+func DecryptMessage(ciphertext []byte, key types.EncryptionKey, usage uint32) ([]byte, error) {
+	et, err := GetEtype(key.KeyType)
+	if err != nil {
+		return []byte{}, fmt.Errorf("error decrypting: %v", err)
+	}
+	b, err := et.DecryptMessage(key.KeyValue, ciphertext, usage)
+	if err != nil {
+		return nil, fmt.Errorf("error decrypting: %v", err)
+	}
+	return b, nil
+}

+ 174 - 0
v8/crypto/des3-cbc-sha1-kd.go

@@ -0,0 +1,174 @@
+package crypto
+
+import (
+	"crypto/des"
+	"crypto/hmac"
+	"crypto/sha1"
+	"errors"
+	"hash"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc3961"
+	"github.com/jcmturner/gokrb5/v8/iana/chksumtype"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+)
+
+//RFC: 3961 Section 6.3
+
+/*
+                 des3-cbc-hmac-sha1-kd, hmac-sha1-des3-kd
+              ------------------------------------------------
+              protocol key format     24 bytes, parity in low
+                                      bit of each
+
+              key-generation seed     21 bytes
+              length
+
+              hash function           SHA-1
+
+              HMAC output size        160 bits
+
+              message block size      8 bytes
+
+              default string-to-key   empty string
+              params
+
+              encryption and          triple-DES encrypt and
+              decryption functions    decrypt, in outer-CBC
+                                      mode (cipher block size
+                                      8 octets)
+
+              key generation functions:
+
+              random-to-key           DES3random-to-key (see
+                                      below)
+
+              string-to-key           DES3string-to-key (see
+                                      below)
+
+   The des3-cbc-hmac-sha1-kd encryption type is assigned the value
+   sixteen (16).  The hmac-sha1-des3-kd checksum algorithm is assigned a
+   checksum type number of twelve (12)*/
+
+// Des3CbcSha1Kd implements Kerberos encryption type des3-cbc-hmac-sha1-kd
+type Des3CbcSha1Kd struct {
+}
+
+// GetETypeID returns the EType ID number.
+func (e Des3CbcSha1Kd) GetETypeID() int32 {
+	return etypeID.DES3_CBC_SHA1_KD
+}
+
+// GetHashID returns the checksum type ID number.
+func (e Des3CbcSha1Kd) GetHashID() int32 {
+	return chksumtype.HMAC_SHA1_DES3_KD
+}
+
+// GetKeyByteSize returns the number of bytes for key of this etype.
+func (e Des3CbcSha1Kd) GetKeyByteSize() int {
+	return 24
+}
+
+// GetKeySeedBitLength returns the number of bits for the seed for key generation.
+func (e Des3CbcSha1Kd) GetKeySeedBitLength() int {
+	return 21 * 8
+}
+
+// GetHashFunc returns the hash function for this etype.
+func (e Des3CbcSha1Kd) GetHashFunc() func() hash.Hash {
+	return sha1.New
+}
+
+// GetMessageBlockByteSize returns the block size for the etype's messages.
+func (e Des3CbcSha1Kd) GetMessageBlockByteSize() int {
+	//For traditional CBC mode with padding, it would be the underlying cipher's block size
+	return des.BlockSize
+}
+
+// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
+func (e Des3CbcSha1Kd) GetDefaultStringToKeyParams() string {
+	var s string
+	return s
+}
+
+// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
+func (e Des3CbcSha1Kd) GetConfounderByteSize() int {
+	return des.BlockSize
+}
+
+// GetHMACBitLength returns the bit count size of the integrity hash.
+func (e Des3CbcSha1Kd) GetHMACBitLength() int {
+	return e.GetHashFunc()().Size() * 8
+}
+
+// GetCypherBlockBitLength returns the bit count size of the cypher block.
+func (e Des3CbcSha1Kd) GetCypherBlockBitLength() int {
+	return des.BlockSize * 8
+}
+
+// StringToKey returns a key derived from the string provided.
+func (e Des3CbcSha1Kd) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
+	if s2kparams != "" {
+		return []byte{}, errors.New("s2kparams must be an empty string")
+	}
+	return rfc3961.DES3StringToKey(secret, salt, e)
+}
+
+// RandomToKey returns a key from the bytes provided.
+func (e Des3CbcSha1Kd) RandomToKey(b []byte) []byte {
+	return rfc3961.DES3RandomToKey(b)
+}
+
+// DeriveRandom generates data needed for key generation.
+func (e Des3CbcSha1Kd) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
+	r, err := rfc3961.DeriveRandom(protocolKey, usage, e)
+	return r, err
+}
+
+// DeriveKey derives a key from the protocol key based on the usage value.
+func (e Des3CbcSha1Kd) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
+	r, err := e.DeriveRandom(protocolKey, usage)
+	if err != nil {
+		return nil, err
+	}
+	return e.RandomToKey(r), nil
+}
+
+// EncryptData encrypts the data provided.
+func (e Des3CbcSha1Kd) EncryptData(key, data []byte) ([]byte, []byte, error) {
+	return rfc3961.DES3EncryptData(key, data, e)
+}
+
+// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
+func (e Des3CbcSha1Kd) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
+	return rfc3961.DES3EncryptMessage(key, message, usage, e)
+}
+
+// DecryptData decrypts the data provided.
+func (e Des3CbcSha1Kd) DecryptData(key, data []byte) ([]byte, error) {
+	return rfc3961.DES3DecryptData(key, data, e)
+}
+
+// DecryptMessage decrypts the message provided and verifies the integrity of the message.
+func (e Des3CbcSha1Kd) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
+	return rfc3961.DES3DecryptMessage(key, ciphertext, usage, e)
+}
+
+// VerifyIntegrity checks the integrity of the plaintext message.
+func (e Des3CbcSha1Kd) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
+	return rfc3961.VerifyIntegrity(protocolKey, ct, pt, usage, e)
+}
+
+// GetChecksumHash returns a keyed checksum hash of the bytes provided.
+func (e Des3CbcSha1Kd) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
+	return common.GetHash(data, protocolKey, common.GetUsageKc(usage), e)
+}
+
+// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
+func (e Des3CbcSha1Kd) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
+	c, err := e.GetChecksumHash(protocolKey, data, usage)
+	if err != nil {
+		return false
+	}
+	return hmac.Equal(chksum, c)
+}

+ 68 - 0
v8/crypto/des3-cbc-sha1-kd_test.go

@@ -0,0 +1,68 @@
+package crypto
+
+import (
+	"encoding/hex"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDes3CbcSha1Kd_DR_DK(t *testing.T) {
+	t.Parallel()
+	// Test vectors from RFC 3961 Appendix A3
+	var tests = []struct {
+		key   string
+		usage string
+		dr    string
+		dk    string
+	}{
+		{"dce06b1f64c857a11c3db57c51899b2cc1791008ce973b92", "0000000155", "935079d14490a75c3093c4a6e8c3b049c71e6ee705", "925179d04591a79b5d3192c4a7e9c289b049c71f6ee604cd"},
+		{"5e13d31c70ef765746578531cb51c15bf11ca82c97cee9f2", "00000001aa", "9f58e5a047d894101c469845d67ae3c5249ed812f2", "9e58e5a146d9942a101c469845d67a20e3c4259ed913f207"},
+		{"98e6fd8a04a4b6859b75a176540b9752bad3ecd610a252bc", "0000000155", "12fff90c773f956d13fc2ca0d0840349dbd39908eb", "13fef80d763e94ec6d13fd2ca1d085070249dad39808eabf"},
+		{"622aec25a2fe2cad7094680b7c64940280084c1a7cec92b5", "00000001aa", "f8debf05b097e7dc0603686aca35d91fd9a5516a70", "f8dfbf04b097e6d9dc0702686bcb3489d91fd9a4516b703e"},
+		{"d3f8298ccb166438dcb9b93ee5a7629286a491f838f802fb", "6b65726265726f73", "2270db565d2a3d64cfbfdc5305d4f778a6de42d9da", "2370da575d2a3da864cebfdc5204d56df779a7df43d9da43"},
+		{"c1081649ada74362e6a1459d01dfd30d67c2234c940704da", "0000000155", "348056ec98fcc517171d2b4d7a9493af482d999175", "348057ec98fdc48016161c2a4c7a943e92ae492c989175f7"},
+		{"5d154af238f46713155719d55e2f1f790dd661f279a7917c", "00000001aa", "a8818bc367dadacbe9a6c84627fb60c294b01215e5", "a8808ac267dada3dcbe9a7c84626fbc761c294b01315e5c1"},
+		{"798562e049852f57dc8c343ba17f2ca1d97394efc8adc443", "0000000155", "c813f88b3be2b2f75424ce9175fbc8483b88c8713a", "c813f88a3be3b334f75425ce9175fbe3c8493b89c8703b49"},
+		{"26dce334b545292f2feab9a8701a89a4b99eb9942cecd016", "00000001aa", "f58efc6f83f93e55e695fd252cf8fe59f7d5ba37ec", "f48ffd6e83f83e7354e694fd252cf83bfe58f7d5ba37ec5d"},
+	}
+	for _, test := range tests {
+		var e Des3CbcSha1Kd
+		key, _ := hex.DecodeString(test.key)
+		usage, _ := hex.DecodeString(test.usage)
+		derivedRandom, err := e.DeriveRandom(key, usage)
+		if err != nil {
+			t.Fatal(fmt.Sprintf("Error in deriveRandom: %v", err))
+		}
+		assert.Equal(t, test.dr, hex.EncodeToString(derivedRandom), "DR not as expected")
+		derivedKey, err := e.DeriveKey(key, usage)
+		if err != nil {
+			t.Fatal(fmt.Sprintf("Error in deriveKey: %v", err))
+		}
+		assert.Equal(t, test.dk, hex.EncodeToString(derivedKey), "DK not as expected")
+	}
+}
+
+func TestDes3CbcSha1Kd_StringToKey(t *testing.T) {
+	t.Parallel()
+	var tests = []struct {
+		salt   string
+		secret string
+		key    string
+	}{
+		{"ATHENA.MIT.EDUraeburn", "password", "850bb51358548cd05e86768c313e3bfef7511937dcf72c3e"},
+		{"WHITEHOUSE.GOVdanny", "potatoe", "dfcd233dd0a43204ea6dc437fb15e061b02979c1f74f377a"},
+		{"EXAMPLE.COMbuckaroo", "penny", "6d2fcdf2d6fbbc3ddcadb5da5710a23489b0d3b69d5d9d4a"},
+		{"ATHENA.MIT.EDUJuri" + "\u0161" + "i" + "\u0107", "\u00DF", "16d5a40e1ce3bacb61b9dce00470324c831973a7b952feb0"},
+		{"EXAMPLE.COMpianist", "𝄞", "85763726585dbc1cce6ec43e1f751f07f1c4cbb098f40b19"},
+	}
+	var e Des3CbcSha1Kd
+	for _, test := range tests {
+		key, err := e.StringToKey(test.secret, test.salt, "")
+		if err != nil {
+			t.Errorf("error in StringToKey: %v", err)
+		}
+		assert.Equal(t, test.key, hex.EncodeToString(key), "StringToKey not as expected")
+	}
+}

+ 29 - 0
v8/crypto/etype/etype.go

@@ -0,0 +1,29 @@
+// Package etype provides the Kerberos Encryption Type interface
+package etype
+
+import "hash"
+
+// EType is the interface defining the Encryption Type.
+type EType interface {
+	GetETypeID() int32
+	GetHashID() int32
+	GetKeyByteSize() int
+	GetKeySeedBitLength() int                                   // key-generation seed length, k
+	GetDefaultStringToKeyParams() string                        // default string-to-key parameters (s2kparams)
+	StringToKey(string, salt, s2kparams string) ([]byte, error) // string-to-key (UTF-8 string, UTF-8 string, opaque)->(protocol-key)
+	RandomToKey(b []byte) []byte                                // random-to-key (bitstring[K])->(protocol-key)
+	GetHMACBitLength() int                                      // HMAC output size, h
+	GetMessageBlockByteSize() int                               // message block size, m
+	EncryptData(key, data []byte) ([]byte, []byte, error)       // E function - encrypt (specific-key, state, octet string)->(state, octet string)
+	EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error)
+	DecryptData(key, data []byte) ([]byte, error) // D function
+	DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error)
+	GetCypherBlockBitLength() int                           // cipher block size, c
+	GetConfounderByteSize() int                             // This is the same as the cipher block size but in bytes.
+	DeriveKey(protocolKey, usage []byte) ([]byte, error)    // DK key-derivation (protocol-key, integer)->(specific-key)
+	DeriveRandom(protocolKey, usage []byte) ([]byte, error) // DR pseudo-random (protocol-key, octet-string)->(octet-string)
+	VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool
+	GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error)
+	VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool
+	GetHashFunc() func() hash.Hash
+}

+ 135 - 0
v8/crypto/rc4-hmac.go

@@ -0,0 +1,135 @@
+package crypto
+
+import (
+	"bytes"
+	"crypto/hmac"
+	"crypto/md5"
+	"hash"
+	"io"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc3961"
+	"github.com/jcmturner/gokrb5/v8/crypto/rfc4757"
+	"github.com/jcmturner/gokrb5/v8/iana/chksumtype"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+	"golang.org/x/crypto/md4"
+)
+
+//http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/sun/security/krb5/internal/crypto/dk/ArcFourCrypto.java#ArcFourCrypto.encrypt%28byte%5B%5D%2Cint%2Cbyte%5B%5D%2Cbyte%5B%5D%2Cbyte%5B%5D%2Cint%2Cint%29
+
+// RC4HMAC implements Kerberos encryption type aes256-cts-hmac-sha1-96
+type RC4HMAC struct {
+}
+
+// GetETypeID returns the EType ID number.
+func (e RC4HMAC) GetETypeID() int32 {
+	return etypeID.RC4_HMAC
+}
+
+// GetHashID returns the checksum type ID number.
+func (e RC4HMAC) GetHashID() int32 {
+	return chksumtype.KERB_CHECKSUM_HMAC_MD5
+}
+
+// GetKeyByteSize returns the number of bytes for key of this etype.
+func (e RC4HMAC) GetKeyByteSize() int {
+	return 16
+}
+
+// GetKeySeedBitLength returns the number of bits for the seed for key generation.
+func (e RC4HMAC) GetKeySeedBitLength() int {
+	return e.GetKeyByteSize() * 8
+}
+
+// GetHashFunc returns the hash function for this etype.
+func (e RC4HMAC) GetHashFunc() func() hash.Hash {
+	return md5.New
+}
+
+// GetMessageBlockByteSize returns the block size for the etype's messages.
+func (e RC4HMAC) GetMessageBlockByteSize() int {
+	return 1
+}
+
+// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
+func (e RC4HMAC) GetDefaultStringToKeyParams() string {
+	return ""
+}
+
+// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
+func (e RC4HMAC) GetConfounderByteSize() int {
+	return 8
+}
+
+// GetHMACBitLength returns the bit count size of the integrity hash.
+func (e RC4HMAC) GetHMACBitLength() int {
+	return md5.Size * 8
+}
+
+// GetCypherBlockBitLength returns the bit count size of the cypher block.
+func (e RC4HMAC) GetCypherBlockBitLength() int {
+	return 8 // doesn't really apply
+}
+
+// StringToKey returns a key derived from the string provided.
+func (e RC4HMAC) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
+	return rfc4757.StringToKey(secret)
+}
+
+// RandomToKey returns a key from the bytes provided.
+func (e RC4HMAC) RandomToKey(b []byte) []byte {
+	r := bytes.NewReader(b)
+	h := md4.New()
+	io.Copy(h, r)
+	return h.Sum(nil)
+}
+
+// EncryptData encrypts the data provided.
+func (e RC4HMAC) EncryptData(key, data []byte) ([]byte, []byte, error) {
+	b, err := rfc4757.EncryptData(key, data, e)
+	return []byte{}, b, err
+}
+
+// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
+func (e RC4HMAC) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
+	b, err := rfc4757.EncryptMessage(key, message, usage, false, e)
+	return []byte{}, b, err
+}
+
+// DecryptData decrypts the data provided.
+func (e RC4HMAC) DecryptData(key, data []byte) ([]byte, error) {
+	return rfc4757.DecryptData(key, data, e)
+}
+
+// DecryptMessage decrypts the message provided and verifies the integrity of the message.
+func (e RC4HMAC) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
+	return rfc4757.DecryptMessage(key, ciphertext, usage, false, e)
+}
+
+// DeriveKey derives a key from the protocol key based on the usage value.
+func (e RC4HMAC) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
+	return rfc4757.HMAC(protocolKey, usage), nil
+}
+
+// DeriveRandom generates data needed for key generation.
+func (e RC4HMAC) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
+	return rfc3961.DeriveRandom(protocolKey, usage, e)
+}
+
+// VerifyIntegrity checks the integrity of the plaintext message.
+func (e RC4HMAC) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
+	return rfc4757.VerifyIntegrity(protocolKey, pt, ct, e)
+}
+
+// GetChecksumHash returns a keyed checksum hash of the bytes provided.
+func (e RC4HMAC) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
+	return rfc4757.Checksum(protocolKey, usage, data)
+}
+
+// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
+func (e RC4HMAC) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
+	checksum, err := rfc4757.Checksum(protocolKey, usage, data)
+	if err != nil {
+		return false
+	}
+	return hmac.Equal(checksum, chksum)
+}

+ 125 - 0
v8/crypto/rfc3961/encryption.go

@@ -0,0 +1,125 @@
+// Package rfc3961 provides encryption and checksum methods as specified in RFC 3961
+package rfc3961
+
+import (
+	"crypto/cipher"
+	"crypto/des"
+	"crypto/hmac"
+	"crypto/rand"
+	"errors"
+	"fmt"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+)
+
+// DES3EncryptData encrypts the data provided using DES3 and methods specific to the etype provided.
+func DES3EncryptData(key, data []byte, e etype.EType) ([]byte, []byte, error) {
+	if len(key) != e.GetKeyByteSize() {
+		return nil, nil, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
+	}
+	data, _ = common.ZeroPad(data, e.GetMessageBlockByteSize())
+
+	block, err := des.NewTripleDESCipher(key)
+	if err != nil {
+		return nil, nil, fmt.Errorf("error creating cipher: %v", err)
+	}
+
+	//RFC 3961: initial cipher state      All bits zero
+	ivz := make([]byte, des.BlockSize)
+
+	ct := make([]byte, len(data))
+	mode := cipher.NewCBCEncrypter(block, ivz)
+	mode.CryptBlocks(ct, data)
+	return ct[len(ct)-e.GetMessageBlockByteSize():], ct, nil
+}
+
+// DES3EncryptMessage encrypts the message provided using DES3 and methods specific to the etype provided.
+// The encrypted data is concatenated with its integrity hash to create an encrypted message.
+func DES3EncryptMessage(key, message []byte, usage uint32, e etype.EType) ([]byte, []byte, error) {
+	//confounder
+	c := make([]byte, e.GetConfounderByteSize())
+	_, err := rand.Read(c)
+	if err != nil {
+		return []byte{}, []byte{}, fmt.Errorf("could not generate random confounder: %v", err)
+	}
+	plainBytes := append(c, message...)
+	plainBytes, _ = common.ZeroPad(plainBytes, e.GetMessageBlockByteSize())
+
+	// Derive key for encryption from usage
+	var k []byte
+	if usage != 0 {
+		k, err = e.DeriveKey(key, common.GetUsageKe(usage))
+		if err != nil {
+			return []byte{}, []byte{}, fmt.Errorf("error deriving key for encryption: %v", err)
+		}
+	}
+
+	iv, b, err := e.EncryptData(k, plainBytes)
+	if err != nil {
+		return iv, b, fmt.Errorf("error encrypting data: %v", err)
+	}
+
+	// Generate and append integrity hash
+	ih, err := common.GetIntegrityHash(plainBytes, key, usage, e)
+	if err != nil {
+		return iv, b, fmt.Errorf("error encrypting data: %v", err)
+	}
+	b = append(b, ih...)
+	return iv, b, nil
+}
+
+// DES3DecryptData decrypts the data provided using DES3 and methods specific to the etype provided.
+func DES3DecryptData(key, data []byte, e etype.EType) ([]byte, error) {
+	if len(key) != e.GetKeyByteSize() {
+		return []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
+	}
+
+	if len(data) < des.BlockSize || len(data)%des.BlockSize != 0 {
+		return []byte{}, errors.New("ciphertext is not a multiple of the block size")
+	}
+	block, err := des.NewTripleDESCipher(key)
+	if err != nil {
+		return []byte{}, fmt.Errorf("error creating cipher: %v", err)
+	}
+	pt := make([]byte, len(data))
+	ivz := make([]byte, des.BlockSize)
+	mode := cipher.NewCBCDecrypter(block, ivz)
+	mode.CryptBlocks(pt, data)
+	return pt, nil
+}
+
+// DES3DecryptMessage decrypts the message provided using DES3 and methods specific to the etype provided.
+// The integrity of the message is also verified.
+func DES3DecryptMessage(key, ciphertext []byte, usage uint32, e etype.EType) ([]byte, error) {
+	//Derive the key
+	k, err := e.DeriveKey(key, common.GetUsageKe(usage))
+	if err != nil {
+		return nil, fmt.Errorf("error deriving key: %v", err)
+	}
+	// Strip off the checksum from the end
+	b, err := e.DecryptData(k, ciphertext[:len(ciphertext)-e.GetHMACBitLength()/8])
+	if err != nil {
+		return nil, fmt.Errorf("error decrypting: %v", err)
+	}
+	//Verify checksum
+	if !e.VerifyIntegrity(key, ciphertext, b, usage) {
+		return nil, errors.New("error decrypting: integrity verification failed")
+	}
+	//Remove the confounder bytes
+	return b[e.GetConfounderByteSize():], nil
+}
+
+// VerifyIntegrity verifies the integrity of cipertext bytes ct.
+func VerifyIntegrity(key, ct, pt []byte, usage uint32, etype etype.EType) bool {
+	//The ciphertext output is the concatenation of the output of the basic
+	//encryption function E and a (possibly truncated) HMAC using the
+	//specified hash function H, both applied to the plaintext with a
+	//random confounder prefix and sufficient padding to bring it to a
+	//multiple of the message block size.  When the HMAC is computed, the
+	//key is used in the protocol key form.
+	h := make([]byte, etype.GetHMACBitLength()/8)
+	copy(h, ct[len(ct)-etype.GetHMACBitLength()/8:])
+	expectedMAC, _ := common.GetIntegrityHash(pt, key, usage, etype)
+	return hmac.Equal(h, expectedMAC)
+}

+ 178 - 0
v8/crypto/rfc3961/keyDerivation.go

@@ -0,0 +1,178 @@
+package rfc3961
+
+import (
+	"bytes"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+)
+
+const (
+	prfconstant = "prf"
+)
+
+// DeriveRandom implements the RFC 3961 defined function: DR(Key, Constant) = k-truncate(E(Key, Constant, initial-cipher-state)).
+//
+// key: base key or protocol key. Likely to be a key from a keytab file.
+//
+// usage: a constant.
+//
+// n: block size in bits (not bytes) - note if you use something like aes.BlockSize this is in bytes.
+//
+// k: key length / key seed length in bits. Eg. for AES256 this value is 256.
+//
+// e: the encryption etype function to use.
+func DeriveRandom(key, usage []byte, e etype.EType) ([]byte, error) {
+	n := e.GetCypherBlockBitLength()
+	k := e.GetKeySeedBitLength()
+	//Ensure the usage constant is at least the size of the cypher block size. Pass it through the nfold algorithm that will "stretch" it if needs be.
+	nFoldUsage := Nfold(usage, n)
+	//k-truncate implemented by creating a byte array the size of k (k is in bits hence /8)
+	out := make([]byte, k/8)
+
+	/*If the output	of E is shorter than k bits, it is fed back into the encryption as many times as necessary.
+	The construct is as follows (where | indicates concatenation):
+
+	K1 = E(Key, n-fold(Constant), initial-cipher-state)
+	K2 = E(Key, K1, initial-cipher-state)
+	K3 = E(Key, K2, initial-cipher-state)
+	K4 = ...
+
+	DR(Key, Constant) = k-truncate(K1 | K2 | K3 | K4 ...)*/
+	_, K, err := e.EncryptData(key, nFoldUsage)
+	if err != nil {
+		return out, err
+	}
+	for i := copy(out, K); i < len(out); {
+		_, K, _ = e.EncryptData(key, K)
+		i = i + copy(out[i:], K)
+	}
+	return out, nil
+}
+
+// DeriveKey derives a key from the protocol key based on the usage and the etype's specific methods.
+func DeriveKey(protocolKey, usage []byte, e etype.EType) ([]byte, error) {
+	r, err := e.DeriveRandom(protocolKey, usage)
+	if err != nil {
+		return nil, err
+	}
+	return e.RandomToKey(r), nil
+}
+
+// RandomToKey returns a key from the bytes provided according to the definition in RFC 3961.
+func RandomToKey(b []byte) []byte {
+	return b
+}
+
+// DES3RandomToKey returns a key from the bytes provided according to the definition in RFC 3961 for DES3 etypes.
+func DES3RandomToKey(b []byte) []byte {
+	r := fixWeakKey(stretch56Bits(b[:7]))
+	r2 := fixWeakKey(stretch56Bits(b[7:14]))
+	r = append(r, r2...)
+	r3 := fixWeakKey(stretch56Bits(b[14:21]))
+	r = append(r, r3...)
+	return r
+}
+
+// DES3StringToKey returns a key derived from the string provided according to the definition in RFC 3961 for DES3 etypes.
+func DES3StringToKey(secret, salt string, e etype.EType) ([]byte, error) {
+	s := secret + salt
+	tkey := e.RandomToKey(Nfold([]byte(s), e.GetKeySeedBitLength()))
+	return e.DeriveKey(tkey, []byte("kerberos"))
+}
+
+// PseudoRandom function as defined in RFC 3961
+func PseudoRandom(key, b []byte, e etype.EType) ([]byte, error) {
+	h := e.GetHashFunc()()
+	h.Write(b)
+	tmp := h.Sum(nil)[:e.GetMessageBlockByteSize()]
+	k, err := e.DeriveKey(key, []byte(prfconstant))
+	if err != nil {
+		return []byte{}, err
+	}
+	_, prf, err := e.EncryptData(k, tmp)
+	if err != nil {
+		return []byte{}, err
+	}
+	return prf, nil
+}
+
+func stretch56Bits(b []byte) []byte {
+	d := make([]byte, len(b), len(b))
+	copy(d, b)
+	var lb byte
+	for i, v := range d {
+		bv, nb := calcEvenParity(v)
+		d[i] = nb
+		if bv != 0 {
+			lb = lb | (1 << uint(i+1))
+		} else {
+			lb = lb &^ (1 << uint(i+1))
+		}
+	}
+	_, lb = calcEvenParity(lb)
+	d = append(d, lb)
+	return d
+}
+
+func calcEvenParity(b byte) (uint8, uint8) {
+	lowestbit := b & 0x01
+	// c counter of 1s in the first 7 bits of the byte
+	var c int
+	// Iterate over the highest 7 bits (hence p starts at 1 not zero) and count the 1s.
+	for p := 1; p < 8; p++ {
+		v := b & (1 << uint(p))
+		if v != 0 {
+			c++
+		}
+	}
+	if c%2 == 0 {
+		//Even number of 1s so set parity to 1
+		b = b | 1
+	} else {
+		//Odd number of 1s so set parity to 0
+		b = b &^ 1
+	}
+	return lowestbit, b
+}
+
+func fixWeakKey(b []byte) []byte {
+	if weak(b) {
+		b[7] ^= 0xF0
+	}
+	return b
+}
+
+func weak(b []byte) bool {
+	// weak keys from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-67r1.pdf
+	weakKeys := [4][]byte{
+		{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01},
+		{0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE},
+		{0xE0, 0xE0, 0xE0, 0xE0, 0xF1, 0xF1, 0xF1, 0xF1},
+		{0x1F, 0x1F, 0x1F, 0x1F, 0x0E, 0x0E, 0x0E, 0x0E},
+	}
+	semiWeakKeys := [12][]byte{
+		{0x01, 0x1F, 0x01, 0x1F, 0x01, 0x0E, 0x01, 0x0E},
+		{0x1F, 0x01, 0x1F, 0x01, 0x0E, 0x01, 0x0E, 0x01},
+		{0x01, 0xE0, 0x01, 0xE0, 0x01, 0xF1, 0x01, 0xF1},
+		{0xE0, 0x01, 0xE0, 0x01, 0xF1, 0x01, 0xF1, 0x01},
+		{0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE},
+		{0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01},
+		{0x1F, 0xE0, 0x1F, 0xE0, 0x0E, 0xF1, 0x0E, 0xF1},
+		{0xE0, 0x1F, 0xE0, 0x1F, 0xF1, 0x0E, 0xF1, 0x0E},
+		{0x1F, 0xFE, 0x1F, 0xFE, 0x0E, 0xFE, 0x0E, 0xFE},
+		{0xFE, 0x1F, 0xFE, 0x1F, 0xFE, 0x0E, 0xFE, 0x0E},
+		{0xE0, 0xFE, 0xE0, 0xFE, 0xF1, 0xFE, 0xF1, 0xFE},
+		{0xFE, 0xE0, 0xFE, 0xE0, 0xFE, 0xF1, 0xFE, 0xF1},
+	}
+	for _, k := range weakKeys {
+		if bytes.Equal(b, k) {
+			return true
+		}
+	}
+	for _, k := range semiWeakKeys {
+		if bytes.Equal(b, k) {
+			return true
+		}
+	}
+	return false
+}

+ 33 - 0
v8/crypto/rfc3961/keyDerivation_test.go

@@ -0,0 +1,33 @@
+package rfc3961
+
+import "testing"
+
+func TestFixWeakKey(t *testing.T) {
+	var weakKeys = []struct {
+		key      []byte
+		lastbyte byte
+	}{
+		{[]byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}, byte(0xF1)},
+		{[]byte{0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE}, byte(0x0E)},
+		{[]byte{0xE0, 0xE0, 0xE0, 0xE0, 0xF1, 0xF1, 0xF1, 0xF1}, byte(0x01)},
+		{[]byte{0x1F, 0x1F, 0x1F, 0x1F, 0x0E, 0x0E, 0x0E, 0x0E}, byte(0xFE)},
+		{[]byte{0x01, 0x1F, 0x01, 0x1F, 0x01, 0x0E, 0x01, 0x0E}, byte(0xFE)},
+		{[]byte{0x1F, 0x01, 0x1F, 0x01, 0x0E, 0x01, 0x0E, 0x01}, byte(0xF1)},
+		{[]byte{0x01, 0xE0, 0x01, 0xE0, 0x01, 0xF1, 0x01, 0xF1}, byte(0x01)},
+		{[]byte{0xE0, 0x01, 0xE0, 0x01, 0xF1, 0x01, 0xF1, 0x01}, byte(0xF1)},
+		{[]byte{0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE}, byte(0x0E)},
+		{[]byte{0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01}, byte(0xF1)},
+		{[]byte{0x1F, 0xE0, 0x1F, 0xE0, 0x0E, 0xF1, 0x0E, 0xF1}, byte(0x01)},
+		{[]byte{0xE0, 0x1F, 0xE0, 0x1F, 0xF1, 0x0E, 0xF1, 0x0E}, byte(0xFE)},
+		{[]byte{0x1F, 0xFE, 0x1F, 0xFE, 0x0E, 0xFE, 0x0E, 0xFE}, byte(0x0E)},
+		{[]byte{0xFE, 0x1F, 0xFE, 0x1F, 0xFE, 0x0E, 0xFE, 0x0E}, byte(0xFE)},
+		{[]byte{0xE0, 0xFE, 0xE0, 0xFE, 0xF1, 0xFE, 0xF1, 0xFE}, byte(0x0E)},
+		{[]byte{0xFE, 0x2F, 0xFE, 0xE0, 0xFE, 0xF1, 0xFE, 0xF1}, byte(0xF1)}, // Non weak key
+	}
+	for i, k := range weakKeys {
+		b := fixWeakKey(k.key)
+		if b[7] != weakKeys[i].lastbyte {
+			t.Errorf("key not fixed correctly %X - %X", b, weakKeys[i].lastbyte)
+		}
+	}
+}

+ 128 - 0
v8/crypto/rfc3961/nfold.go

@@ -0,0 +1,128 @@
+package rfc3961
+
+/*
+Implementation of the n-fold algorithm as defined in RFC 3961.
+
+n-fold is an algorithm that takes m input bits and "stretches" them
+to form n output bits with equal contribution from each input bit to
+the output, as described in [Blumenthal96]:
+
+We first define a primitive called n-folding, which takes a
+variable-length input block and produces a fixed-length output
+sequence.  The intent is to give each input bit approximately
+equal weight in determining the value of each output bit.  Note
+that whenever we need to treat a string of octets as a number, the
+assumed representation is Big-Endian -- Most Significant Byte
+first.
+
+To n-fold a number X, replicate the input value to a length that
+is the least common multiple of n and the length of X.  Before
+each repetition, the input is rotated to the right by 13 bit
+positions.  The successive n-bit chunks are added together using
+1's-complement addition (that is, with end-around carry) to yield
+a n-bit result....
+*/
+
+/* Credits
+This golang implementation of nfold used the following project for help with implementation detail.
+Although their source is in java it was helpful as a reference implementation of the RFC.
+You can find the source code of their open source project along with license information below.
+We acknowledge and are grateful to these developers for their contributions to open source
+
+Project: Apache Directory (http://http://directory.apache.org/)
+https://svn.apache.org/repos/asf/directory/apacheds/tags/1.5.1/kerberos-shared/src/main/java/org/apache/directory/server/kerberos/shared/crypto/encryption/NFold.java
+License: http://www.apache.org/licenses/LICENSE-2.0
+*/
+
+// Nfold expands the key to ensure it is not smaller than one cipher block.
+// Defined in RFC 3961.
+//
+// m input bytes that will be "stretched" to the least common multiple of n bits and the bit length of m.
+func Nfold(m []byte, n int) []byte {
+	k := len(m) * 8
+
+	//Get the lowest common multiple of the two bit sizes
+	lcm := lcm(n, k)
+	relicate := lcm / k
+	var sumBytes []byte
+
+	for i := 0; i < relicate; i++ {
+		rotation := 13 * i
+		sumBytes = append(sumBytes, rotateRight(m, rotation)...)
+	}
+
+	nfold := make([]byte, n/8)
+	sum := make([]byte, n/8)
+	for i := 0; i < lcm/n; i++ {
+		for j := 0; j < n/8; j++ {
+			sum[j] = sumBytes[j+(i*len(sum))]
+		}
+		nfold = onesComplementAddition(nfold, sum)
+	}
+	return nfold
+}
+
+func onesComplementAddition(n1, n2 []byte) []byte {
+	numBits := len(n1) * 8
+	out := make([]byte, numBits/8)
+	carry := 0
+	for i := numBits - 1; i > -1; i-- {
+		n1b := getBit(&n1, i)
+		n2b := getBit(&n2, i)
+		s := n1b + n2b + carry
+
+		if s == 0 || s == 1 {
+			setBit(&out, i, s)
+			carry = 0
+		} else if s == 2 {
+			carry = 1
+		} else if s == 3 {
+			setBit(&out, i, 1)
+			carry = 1
+		}
+	}
+	if carry == 1 {
+		carryArray := make([]byte, len(n1))
+		carryArray[len(carryArray)-1] = 1
+		out = onesComplementAddition(out, carryArray)
+	}
+	return out
+}
+
+func rotateRight(b []byte, step int) []byte {
+	out := make([]byte, len(b))
+	bitLen := len(b) * 8
+	for i := 0; i < bitLen; i++ {
+		v := getBit(&b, i)
+		setBit(&out, (i+step)%bitLen, v)
+	}
+	return out
+}
+
+func lcm(x, y int) int {
+	return (x * y) / gcd(x, y)
+}
+
+func gcd(x, y int) int {
+	for y != 0 {
+		x, y = y, x%y
+	}
+	return x
+}
+
+func getBit(b *[]byte, p int) int {
+	pByte := p / 8
+	pBit := uint(p % 8)
+	vByte := (*b)[pByte]
+	vInt := int(vByte >> (8 - (pBit + 1)) & 0x0001)
+	return vInt
+}
+
+func setBit(b *[]byte, p, v int) {
+	pByte := p / 8
+	pBit := uint(p % 8)
+	oldByte := (*b)[pByte]
+	var newByte byte
+	newByte = byte(v<<(8-(pBit+1))) | oldByte
+	(*b)[pByte] = newByte
+}

+ 28 - 0
v8/crypto/rfc3961/nfold_test.go

@@ -0,0 +1,28 @@
+package rfc3961
+
+import (
+	"encoding/hex"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_nfold(t *testing.T) {
+	t.Parallel()
+	var tests = []struct {
+		n      int
+		b      []byte
+		folded string
+	}{
+		{64, []byte("012345"), "be072631276b1955"},
+		{56, []byte("password"), "78a07b6caf85fa"},
+		{64, []byte("Rough Consensus, and Running Code"), "bb6ed30870b7f0e0"},
+		{168, []byte("password"), "59e4a8ca7c0385c3c37b3f6d2000247cb6e6bd5b3e"},
+		{192, []byte("MASSACHVSETTS INSTITVTE OF TECHNOLOGY"), "db3b0d8f0b061e603282b308a50841229ad798fab9540c1b"},
+		{168, []byte("Q"), "518a54a215a8452a518a54a215a8452a518a54a215"},
+		{168, []byte("ba"), "fb25d531ae8974499f52fd92ea9857c4ba24cf297e"},
+	}
+	for _, test := range tests {
+		assert.Equal(t, test.folded, hex.EncodeToString(Nfold(test.b, test.n)), "Folded not as expected")
+	}
+}

+ 89 - 0
v8/crypto/rfc3962/encryption.go

@@ -0,0 +1,89 @@
+// Package rfc3962 provides encryption and checksum methods as specified in RFC 3962
+package rfc3962
+
+import (
+	"crypto/rand"
+	"errors"
+	"fmt"
+
+	"github.com/jcmturner/aescts"
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+)
+
+// EncryptData encrypts the data provided using methods specific to the etype provided as defined in RFC 3962.
+func EncryptData(key, data []byte, e etype.EType) ([]byte, []byte, error) {
+	if len(key) != e.GetKeyByteSize() {
+		return []byte{}, []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
+	}
+	ivz := make([]byte, e.GetCypherBlockBitLength()/8)
+	return aescts.Encrypt(key, ivz, data)
+}
+
+// EncryptMessage encrypts the message provided using the methods specific to the etype provided as defined in RFC 3962.
+// The encrypted data is concatenated with its integrity hash to create an encrypted message.
+func EncryptMessage(key, message []byte, usage uint32, e etype.EType) ([]byte, []byte, error) {
+	if len(key) != e.GetKeyByteSize() {
+		return []byte{}, []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
+	}
+	//confounder
+	c := make([]byte, e.GetConfounderByteSize())
+	_, err := rand.Read(c)
+	if err != nil {
+		return []byte{}, []byte{}, fmt.Errorf("could not generate random confounder: %v", err)
+	}
+	plainBytes := append(c, message...)
+
+	// Derive key for encryption from usage
+	var k []byte
+	if usage != 0 {
+		k, err = e.DeriveKey(key, common.GetUsageKe(usage))
+		if err != nil {
+			return []byte{}, []byte{}, fmt.Errorf("error deriving key for encryption: %v", err)
+		}
+	}
+
+	// Encrypt the data
+	iv, b, err := e.EncryptData(k, plainBytes)
+	if err != nil {
+		return iv, b, fmt.Errorf("error encrypting data: %v", err)
+	}
+
+	// Generate and append integrity hash
+	ih, err := common.GetIntegrityHash(plainBytes, key, usage, e)
+	if err != nil {
+		return iv, b, fmt.Errorf("error encrypting data: %v", err)
+	}
+	b = append(b, ih...)
+	return iv, b, nil
+}
+
+// DecryptData decrypts the data provided using the methods specific to the etype provided as defined in RFC 3962.
+func DecryptData(key, data []byte, e etype.EType) ([]byte, error) {
+	if len(key) != e.GetKeyByteSize() {
+		return []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
+	}
+	ivz := make([]byte, e.GetCypherBlockBitLength()/8)
+	return aescts.Decrypt(key, ivz, data)
+}
+
+// DecryptMessage decrypts the message provided using the methods specific to the etype provided as defined in RFC 3962.
+// The integrity of the message is also verified.
+func DecryptMessage(key, ciphertext []byte, usage uint32, e etype.EType) ([]byte, error) {
+	//Derive the key
+	k, err := e.DeriveKey(key, common.GetUsageKe(usage))
+	if err != nil {
+		return nil, fmt.Errorf("error deriving key: %v", err)
+	}
+	// Strip off the checksum from the end
+	b, err := e.DecryptData(k, ciphertext[:len(ciphertext)-e.GetHMACBitLength()/8])
+	if err != nil {
+		return nil, err
+	}
+	//Verify checksum
+	if !e.VerifyIntegrity(key, ciphertext, b, usage) {
+		return nil, errors.New("integrity verification failed")
+	}
+	//Remove the confounder bytes
+	return b[e.GetConfounderByteSize():], nil
+}

+ 58 - 0
v8/crypto/rfc3962/keyDerivation.go

@@ -0,0 +1,58 @@
+package rfc3962
+
+import (
+	"encoding/binary"
+	"encoding/hex"
+	"errors"
+
+	"github.com/jcmturner/gofork/x/crypto/pbkdf2"
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+)
+
+const (
+	s2kParamsZero = 4294967296
+)
+
+// StringToKey returns a key derived from the string provided according to the definition in RFC 3961.
+func StringToKey(secret, salt, s2kparams string, e etype.EType) ([]byte, error) {
+	i, err := S2KparamsToItertions(s2kparams)
+	if err != nil {
+		return nil, err
+	}
+	return StringToKeyIter(secret, salt, i, e)
+}
+
+// StringToPBKDF2 generates an encryption key from a pass phrase and salt string using the PBKDF2 function from PKCS #5 v2.0
+func StringToPBKDF2(secret, salt string, iterations int64, e etype.EType) []byte {
+	return pbkdf2.Key64([]byte(secret), []byte(salt), iterations, int64(e.GetKeyByteSize()), e.GetHashFunc())
+}
+
+// StringToKeyIter returns a key derived from the string provided according to the definition in RFC 3961.
+func StringToKeyIter(secret, salt string, iterations int64, e etype.EType) ([]byte, error) {
+	tkey := e.RandomToKey(StringToPBKDF2(secret, salt, iterations, e))
+	return e.DeriveKey(tkey, []byte("kerberos"))
+}
+
+// S2KparamsToItertions converts the string representation of iterations to an integer
+func S2KparamsToItertions(s2kparams string) (int64, error) {
+	//process s2kparams string
+	//The parameter string is four octets indicating an unsigned
+	//number in big-endian order.  This is the number of iterations to be
+	//performed.  If the value is 00 00 00 00, the number of iterations to
+	//be performed is 4,294,967,296 (2**32).
+	var i uint32
+	if len(s2kparams) != 8 {
+		return int64(s2kParamsZero), errors.New("invalid s2kparams length")
+	}
+	b, err := hex.DecodeString(s2kparams)
+	if err != nil {
+		return int64(s2kParamsZero), errors.New("invalid s2kparams, cannot decode string to bytes")
+	}
+	i = binary.BigEndian.Uint32(b)
+	//buf := bytes.NewBuffer(b)
+	//err = binary.Read(buf, binary.BigEndian, &i)
+	if err != nil {
+		return int64(s2kParamsZero), errors.New("invalid s2kparams, cannot convert to big endian int32")
+	}
+	return int64(i), nil
+}

+ 40 - 0
v8/crypto/rfc4757/checksum.go

@@ -0,0 +1,40 @@
+package rfc4757
+
+import (
+	"bytes"
+	"crypto/hmac"
+	"crypto/md5"
+	"io"
+)
+
+// Checksum returns a hash of the data in accordance with RFC 4757
+func Checksum(key []byte, usage uint32, data []byte) ([]byte, error) {
+	// Create hashing key
+	s := append([]byte(`signaturekey`), byte(0x00)) //includes zero octet at end
+	mac := hmac.New(md5.New, key)
+	mac.Write(s)
+	Ksign := mac.Sum(nil)
+
+	// Format data
+	tb := UsageToMSMsgType(usage)
+	p := append(tb, data...)
+	h := md5.New()
+	rb := bytes.NewReader(p)
+	_, err := io.Copy(h, rb)
+	if err != nil {
+		return []byte{}, err
+	}
+	tmp := h.Sum(nil)
+
+	// Generate HMAC
+	mac = hmac.New(md5.New, Ksign)
+	mac.Write(tmp)
+	return mac.Sum(nil), nil
+}
+
+// HMAC returns a keyed MD5 checksum of the data
+func HMAC(key []byte, data []byte) []byte {
+	mac := hmac.New(md5.New, key)
+	mac.Write(data)
+	return mac.Sum(nil)
+}

+ 80 - 0
v8/crypto/rfc4757/encryption.go

@@ -0,0 +1,80 @@
+// Package rfc4757 provides encryption and checksum methods as specified in RFC 4757
+package rfc4757
+
+import (
+	"crypto/hmac"
+	"crypto/rand"
+	"crypto/rc4"
+	"errors"
+	"fmt"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+)
+
+// EncryptData encrypts the data provided using methods specific to the etype provided as defined in RFC 4757.
+func EncryptData(key, data []byte, e etype.EType) ([]byte, error) {
+	if len(key) != e.GetKeyByteSize() {
+		return []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
+	}
+	rc4Cipher, err := rc4.NewCipher(key)
+	if err != nil {
+		return []byte{}, fmt.Errorf("error creating RC4 cipher: %v", err)
+	}
+	ed := make([]byte, len(data))
+	copy(ed, data)
+	rc4Cipher.XORKeyStream(ed, ed)
+	rc4Cipher.Reset()
+	return ed, nil
+}
+
+// DecryptData decrypts the data provided using the methods specific to the etype provided as defined in RFC 4757.
+func DecryptData(key, data []byte, e etype.EType) ([]byte, error) {
+	return EncryptData(key, data, e)
+}
+
+// EncryptMessage encrypts the message provided using the methods specific to the etype provided as defined in RFC 4757.
+// The encrypted data is concatenated with its RC4 header containing integrity checksum and confounder to create an encrypted message.
+func EncryptMessage(key, data []byte, usage uint32, export bool, e etype.EType) ([]byte, error) {
+	confounder := make([]byte, e.GetConfounderByteSize()) // size = 8
+	_, err := rand.Read(confounder)
+	if err != nil {
+		return []byte{}, fmt.Errorf("error generating confounder: %v", err)
+	}
+	k1 := key
+	k2 := HMAC(k1, UsageToMSMsgType(usage))
+	toenc := append(confounder, data...)
+	chksum := HMAC(k2, toenc)
+	k3 := HMAC(k2, chksum)
+
+	ed, err := EncryptData(k3, toenc, e)
+	if err != nil {
+		return []byte{}, fmt.Errorf("error encrypting data: %v", err)
+	}
+
+	msg := append(chksum, ed...)
+	return msg, nil
+}
+
+// DecryptMessage decrypts the message provided using the methods specific to the etype provided as defined in RFC 4757.
+// The integrity of the message is also verified.
+func DecryptMessage(key, data []byte, usage uint32, export bool, e etype.EType) ([]byte, error) {
+	checksum := data[:e.GetHMACBitLength()/8]
+	ct := data[e.GetHMACBitLength()/8:]
+	_, k2, k3 := deriveKeys(key, checksum, usage, export)
+
+	pt, err := DecryptData(k3, ct, e)
+	if err != nil {
+		return []byte{}, fmt.Errorf("error decrypting data: %v", err)
+	}
+
+	if !VerifyIntegrity(k2, pt, data, e) {
+		return []byte{}, errors.New("integrity checksum incorrect")
+	}
+	return pt[e.GetConfounderByteSize():], nil
+}
+
+// VerifyIntegrity checks the integrity checksum of the data matches that calculated from the decrypted data.
+func VerifyIntegrity(key, pt, data []byte, e etype.EType) bool {
+	chksum := HMAC(key, pt)
+	return hmac.Equal(chksum, data[:e.GetHMACBitLength()/8])
+}

+ 55 - 0
v8/crypto/rfc4757/keyDerivation.go

@@ -0,0 +1,55 @@
+package rfc4757
+
+import (
+	"bytes"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"io"
+
+	"golang.org/x/crypto/md4"
+)
+
+// StringToKey returns a key derived from the string provided according to the definition in RFC 4757.
+func StringToKey(secret string) ([]byte, error) {
+	b := make([]byte, len(secret)*2, len(secret)*2)
+	for i, r := range secret {
+		u := fmt.Sprintf("%04x", r)
+		c, err := hex.DecodeString(u)
+		if err != nil {
+			return []byte{}, errors.New("character could not be encoded")
+		}
+		// Swap round the two bytes to make little endian as we put into byte slice
+		b[2*i] = c[1]
+		b[2*i+1] = c[0]
+	}
+	r := bytes.NewReader(b)
+	h := md4.New()
+	_, err := io.Copy(h, r)
+	if err != nil {
+		return []byte{}, err
+	}
+	return h.Sum(nil), nil
+}
+
+func deriveKeys(key, checksum []byte, usage uint32, export bool) (k1, k2, k3 []byte) {
+	//if export {
+	//	L40 := make([]byte, 14, 14)
+	//	copy(L40, []byte(`fortybits`))
+	//	k1 = HMAC(key, L40)
+	//} else {
+	//	tb := MessageTypeBytes(usage)
+	//	k1 = HMAC(key, tb)
+	//}
+	//k2 = k1[:16]
+	//if export {
+	//	mask := []byte{0xAB,0xAB,0xAB,0xAB,0xAB,0xAB,0xAB,0xAB,0xAB}
+	//	copy(k1[7:16], mask)
+	//}
+	//k3 = HMAC(k1, checksum)
+	//return
+	k1 = key
+	k2 = HMAC(k1, UsageToMSMsgType(usage))
+	k3 = HMAC(k2, checksum)
+	return
+}

+ 23 - 0
v8/crypto/rfc4757/keyDerivation_test.go

@@ -0,0 +1,23 @@
+package rfc4757
+
+import (
+	"encoding/hex"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	testPassword = "foo"
+	testKey      = "ac8e657f83df82beea5d43bdaf7800cc"
+)
+
+func TestStringToKey(t *testing.T) {
+	t.Parallel()
+	kb, err := StringToKey(testPassword)
+	if err != nil {
+		t.Fatalf("Error deriving key from string: %v", err)
+	}
+	k := hex.EncodeToString(kb)
+	assert.Equal(t, testKey, k, "Key not as expected")
+}

+ 20 - 0
v8/crypto/rfc4757/msgtype.go

@@ -0,0 +1,20 @@
+package rfc4757
+
+import "encoding/binary"
+
+// UsageToMSMsgType converts Kerberos key usage numbers to Microsoft message type encoded as a little-endian four byte slice.
+func UsageToMSMsgType(usage uint32) []byte {
+	// Translate usage numbers to the Microsoft T numbers
+	switch usage {
+	case 3:
+		usage = 8
+	case 9:
+		usage = 8
+	case 23:
+		usage = 13
+	}
+	// Now convert to bytes
+	tb := make([]byte, 4) // We force an int32 input so we can't go over 4 bytes
+	binary.PutUvarint(tb, uint64(usage))
+	return tb
+}

+ 128 - 0
v8/crypto/rfc8009/encryption.go

@@ -0,0 +1,128 @@
+// Package rfc8009 provides encryption and checksum methods as specified in RFC 8009
+package rfc8009
+
+import (
+	"crypto/aes"
+	"crypto/hmac"
+	"crypto/rand"
+	"errors"
+	"fmt"
+
+	"github.com/jcmturner/aescts"
+	"github.com/jcmturner/gokrb5/v8/crypto/common"
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+)
+
+// EncryptData encrypts the data provided using methods specific to the etype provided as defined in RFC 8009.
+func EncryptData(key, data []byte, e etype.EType) ([]byte, []byte, error) {
+	kl := e.GetKeyByteSize()
+	if e.GetETypeID() == etypeID.AES256_CTS_HMAC_SHA384_192 {
+		kl = 32
+	}
+	if len(key) != kl {
+		return []byte{}, []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
+	}
+	ivz := make([]byte, aes.BlockSize)
+	return aescts.Encrypt(key, ivz, data)
+}
+
+// EncryptMessage encrypts the message provided using the methods specific to the etype provided as defined in RFC 8009.
+// The encrypted data is concatenated with its integrity hash to create an encrypted message.
+func EncryptMessage(key, message []byte, usage uint32, e etype.EType) ([]byte, []byte, error) {
+	kl := e.GetKeyByteSize()
+	if e.GetETypeID() == etypeID.AES256_CTS_HMAC_SHA384_192 {
+		kl = 32
+	}
+	if len(key) != kl {
+		return []byte{}, []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", kl, len(key))
+	}
+	if len(key) != e.GetKeyByteSize() {
+	}
+	//confounder
+	c := make([]byte, e.GetConfounderByteSize())
+	_, err := rand.Read(c)
+	if err != nil {
+		return []byte{}, []byte{}, fmt.Errorf("could not generate random confounder: %v", err)
+	}
+	plainBytes := append(c, message...)
+
+	// Derive key for encryption from usage
+	var k []byte
+	if usage != 0 {
+		k, err = e.DeriveKey(key, common.GetUsageKe(usage))
+		if err != nil {
+			return []byte{}, []byte{}, fmt.Errorf("error deriving key for encryption: %v", err)
+		}
+	}
+
+	// Encrypt the data
+	iv, b, err := e.EncryptData(k, plainBytes)
+	if err != nil {
+		return iv, b, fmt.Errorf("error encrypting data: %v", err)
+	}
+
+	ivz := make([]byte, e.GetConfounderByteSize())
+	ih, err := GetIntegityHash(ivz, b, key, usage, e)
+	if err != nil {
+		return iv, b, fmt.Errorf("error encrypting data: %v", err)
+	}
+	b = append(b, ih...)
+	return iv, b, nil
+}
+
+// DecryptData decrypts the data provided using the methods specific to the etype provided as defined in RFC 8009.
+func DecryptData(key, data []byte, e etype.EType) ([]byte, error) {
+	kl := e.GetKeyByteSize()
+	if e.GetETypeID() == etypeID.AES256_CTS_HMAC_SHA384_192 {
+		kl = 32
+	}
+	if len(key) != kl {
+		return []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", kl, len(key))
+	}
+	ivz := make([]byte, aes.BlockSize)
+	return aescts.Decrypt(key, ivz, data)
+}
+
+// DecryptMessage decrypts the message provided using the methods specific to the etype provided as defined in RFC 8009.
+// The integrity of the message is also verified.
+func DecryptMessage(key, ciphertext []byte, usage uint32, e etype.EType) ([]byte, error) {
+	//Derive the key
+	k, err := e.DeriveKey(key, common.GetUsageKe(usage))
+	if err != nil {
+		return nil, fmt.Errorf("error deriving key: %v", err)
+	}
+	// Strip off the checksum from the end
+	b, err := e.DecryptData(k, ciphertext[:len(ciphertext)-e.GetHMACBitLength()/8])
+	if err != nil {
+		return nil, err
+	}
+	//Verify checksum
+	if !e.VerifyIntegrity(key, ciphertext, b, usage) {
+		return nil, errors.New("integrity verification failed")
+	}
+	//Remove the confounder bytes
+	return b[e.GetConfounderByteSize():], nil
+}
+
+// GetIntegityHash returns a keyed integrity hash of the bytes provided as defined in RFC 8009
+func GetIntegityHash(iv, c, key []byte, usage uint32, e etype.EType) ([]byte, error) {
+	// Generate and append integrity hash
+	// The HMAC is calculated over the cipher state concatenated with the
+	// AES output, instead of being calculated over the confounder and
+	// plaintext.  This allows the message receiver to verify the
+	// integrity of the message before decrypting the message.
+	// H = HMAC(Ki, IV | C)
+	ib := append(iv, c...)
+	return common.GetIntegrityHash(ib, key, usage, e)
+}
+
+// VerifyIntegrity verifies the integrity of cipertext bytes ct.
+func VerifyIntegrity(key, ct []byte, usage uint32, etype etype.EType) bool {
+	h := make([]byte, etype.GetHMACBitLength()/8)
+	copy(h, ct[len(ct)-etype.GetHMACBitLength()/8:])
+	ivz := make([]byte, etype.GetConfounderByteSize())
+	ib := append(ivz, ct[:len(ct)-(etype.GetHMACBitLength()/8)]...)
+	expectedMAC, _ := common.GetIntegrityHash(ib, key, usage, etype)
+	return hmac.Equal(h, expectedMAC)
+}

+ 144 - 0
v8/crypto/rfc8009/keyDerivation.go

@@ -0,0 +1,144 @@
+package rfc8009
+
+import (
+	"crypto/hmac"
+	"encoding/binary"
+	"encoding/hex"
+	"errors"
+
+	"github.com/jcmturner/gokrb5/v8/crypto/etype"
+	"github.com/jcmturner/gokrb5/v8/iana/etypeID"
+	"golang.org/x/crypto/pbkdf2"
+)
+
+const (
+	s2kParamsZero = 32768
+)
+
+// DeriveRandom for key derivation as defined in RFC 8009
+func DeriveRandom(protocolKey, usage []byte, e etype.EType) ([]byte, error) {
+	h := e.GetHashFunc()()
+	return KDF_HMAC_SHA2(protocolKey, []byte("prf"), usage, h.Size(), e), nil
+}
+
+// DeriveKey derives a key from the protocol key based on the usage and the etype's specific methods.
+//
+// https://tools.ietf.org/html/rfc8009#section-5
+//
+// If the enctype is aes128-cts-hmac-sha256-128:
+// Kc = KDF-HMAC-SHA2(base-key, usage | 0x99, 128)
+// Ke = KDF-HMAC-SHA2(base-key, usage | 0xAA, 128)
+// Ki = KDF-HMAC-SHA2(base-key, usage | 0x55, 128)
+//
+// If the enctype is aes256-cts-hmac-sha384-192:
+// Kc = KDF-HMAC-SHA2(base-key, usage | 0x99, 192)
+// Ke = KDF-HMAC-SHA2(base-key, usage | 0xAA, 256)
+// Ki = KDF-HMAC-SHA2(base-key, usage | 0x55, 192)
+func DeriveKey(protocolKey, label []byte, e etype.EType) []byte {
+	var context []byte
+	var kl int
+	// Key length is longer for aes256-cts-hmac-sha384-192 is it is a Ke or from StringToKey (where label is "kerberos")
+	if e.GetETypeID() == etypeID.AES256_CTS_HMAC_SHA384_192 {
+		switch label[len(label)-1] {
+		case 0x73:
+			// 0x73 is "s" so label could be kerberos meaning StringToKey so now check if the label is "kerberos"
+			kerblabel := []byte("kerberos")
+			if len(label) != len(kerblabel) {
+				break
+			}
+			for i, b := range label {
+				if b != kerblabel[i] {
+					kl = e.GetKeySeedBitLength()
+					break
+				}
+			}
+			if kl == 0 {
+				// This is StringToKey
+				kl = 256
+			}
+		case 0xAA:
+			// This is a Ke
+			kl = 256
+		}
+	}
+	if kl == 0 {
+		kl = e.GetKeySeedBitLength()
+	}
+	return e.RandomToKey(KDF_HMAC_SHA2(protocolKey, label, context, kl, e))
+}
+
+// RandomToKey returns a key from the bytes provided according to the definition in RFC 8009.
+func RandomToKey(b []byte) []byte {
+	return b
+}
+
+// StringToKey returns a key derived from the string provided according to the definition in RFC 8009.
+func StringToKey(secret, salt, s2kparams string, e etype.EType) ([]byte, error) {
+	i, err := S2KparamsToItertions(s2kparams)
+	if err != nil {
+		return nil, err
+	}
+	return StringToKeyIter(secret, salt, i, e)
+}
+
+// StringToKeyIter returns a key derived from the string provided according to the definition in RFC 8009.
+func StringToKeyIter(secret, salt string, iterations int, e etype.EType) ([]byte, error) {
+	tkey := e.RandomToKey(StringToPBKDF2(secret, salt, iterations, e))
+	return e.DeriveKey(tkey, []byte("kerberos"))
+}
+
+// StringToPBKDF2 generates an encryption key from a pass phrase and salt string using the PBKDF2 function from PKCS #5 v2.0
+func StringToPBKDF2(secret, salt string, iterations int, e etype.EType) []byte {
+	kl := e.GetKeyByteSize()
+	if e.GetETypeID() == etypeID.AES256_CTS_HMAC_SHA384_192 {
+		kl = 32
+	}
+	return pbkdf2.Key([]byte(secret), []byte(salt), iterations, kl, e.GetHashFunc())
+}
+
+// KDF_HMAC_SHA2 key derivation: https://tools.ietf.org/html/rfc8009#section-3
+func KDF_HMAC_SHA2(protocolKey, label, context []byte, kl int, e etype.EType) []byte {
+	//k: Length in bits of the key to be outputted, expressed in big-endian binary representation in 4 bytes.
+	k := make([]byte, 4, 4)
+	binary.BigEndian.PutUint32(k, uint32(kl))
+
+	c := make([]byte, 4, 4)
+	binary.BigEndian.PutUint32(c, uint32(1))
+	c = append(c, label...)
+	c = append(c, byte(0))
+	if len(context) > 0 {
+		c = append(c, context...)
+	}
+	c = append(c, k...)
+
+	mac := hmac.New(e.GetHashFunc(), protocolKey)
+	mac.Write(c)
+	return mac.Sum(nil)[:(kl / 8)]
+}
+
+// GetSaltP returns the salt value based on the etype name: https://tools.ietf.org/html/rfc8009#section-4
+func GetSaltP(salt, ename string) string {
+	b := []byte(ename)
+	b = append(b, byte(0))
+	b = append(b, []byte(salt)...)
+	return string(b)
+}
+
+// S2KparamsToItertions converts the string representation of iterations to an integer for RFC 8009.
+func S2KparamsToItertions(s2kparams string) (int, error) {
+	var i uint32
+	if len(s2kparams) != 8 {
+		return s2kParamsZero, errors.New("Invalid s2kparams length")
+	}
+	b, err := hex.DecodeString(s2kparams)
+	if err != nil {
+		return s2kParamsZero, errors.New("Invalid s2kparams, cannot decode string to bytes")
+	}
+	i = binary.BigEndian.Uint32(b)
+	//buf := bytes.NewBuffer(b)
+	//err = binary.Read(buf, binary.BigEndian, &i)
+	if err != nil {
+		return s2kParamsZero, errors.New("Invalid s2kparams, cannot convert to big endian int32")
+	}
+	return int(i), nil
+}

+ 112 - 0
v8/examples/example-AD.go

@@ -0,0 +1,112 @@
+// +build examples
+
+package main
+
+import (
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"github.com/jcmturner/goidentity/v6"
+	"github.com/jcmturner/gokrb5/v8/client"
+	"github.com/jcmturner/gokrb5/v8/config"
+	"github.com/jcmturner/gokrb5/v8/credentials"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/service"
+	"github.com/jcmturner/gokrb5/v8/spnego"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/httptest"
+	"os"
+)
+
+func main() {
+	s := httpServer()
+	defer s.Close()
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_USERKRB5_AD_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	cl := client.NewWithKeytab("testuser1", "USER.GOKRB5", kt, c, client.DisablePAFXFAST(true))
+	httpRequest(s.URL, cl)
+
+	b, _ = hex.DecodeString(testdata.TESTUSER2_USERKRB5_AD_KEYTAB)
+	kt = keytab.New()
+	kt.Unmarshal(b)
+	c, _ = config.NewFromString(testdata.TEST_KRB5CONF)
+	cl = client.NewWithKeytab("testuser2", "USER.GOKRB5", kt, c, client.DisablePAFXFAST(true))
+	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)
+	}
+	r, _ := http.NewRequest("GET", url, nil)
+	err = spnego.SetSPNEGOHeader(cl, 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 Tests: ", log.Ldate|log.Ltime|log.Lshortfile)
+	b, _ := hex.DecodeString(testdata.HTTP_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	th := http.HandlerFunc(testAppHandler)
+	s := httptest.NewServer(spnego.SPNEGOKRB5Authenticate(th, kt, service.Logger(l)))
+	return s
+}
+
+func testAppHandler(w http.ResponseWriter, r *http.Request) {
+	creds := goidentity.FromHTTPRequestContext(r)
+	fmt.Fprint(w, "<html>\n<p><h1>TEST.GOKRB5 Handler</h1></p>\n")
+	if creds != nil && creds.Authenticated() {
+		fmt.Fprintf(w, "<ul><li>Authenticed user: %s</li>\n", creds.UserName())
+		fmt.Fprintf(w, "<li>User's realm: %s</li>\n", creds.Domain())
+		fmt.Fprint(w, "<li>Authz Attributes (Group Memberships):</li><ul>\n")
+		for _, s := range creds.AuthzAttributes() {
+			fmt.Fprintf(w, "<li>%v</li>\n", s)
+		}
+		fmt.Fprint(w, "</ul>\n")
+		if ADCredsJSON, ok := creds.Attributes()[credentials.AttributeKeyADCredentials]; ok {
+			ADCreds := new(credentials.ADCredentials)
+			err := json.Unmarshal([]byte(ADCredsJSON), ADCreds)
+			if err == nil {
+				// Now access the fields of the ADCredentials struct. For example:
+				fmt.Fprintf(w, "<li>EffectiveName: %v</li>\n", ADCreds.EffectiveName)
+				fmt.Fprintf(w, "<li>FullName: %v</li>\n", ADCreds.FullName)
+				fmt.Fprintf(w, "<li>UserID: %v</li>\n", ADCreds.UserID)
+				fmt.Fprintf(w, "<li>PrimaryGroupID: %v</li>\n", ADCreds.PrimaryGroupID)
+				fmt.Fprintf(w, "<li>Group SIDs: %v</li>\n", ADCreds.GroupMembershipSIDs)
+				fmt.Fprintf(w, "<li>LogOnTime: %v</li>\n", ADCreds.LogOnTime)
+				fmt.Fprintf(w, "<li>LogOffTime: %v</li>\n", ADCreds.LogOffTime)
+				fmt.Fprintf(w, "<li>PasswordLastSet: %v</li>\n", ADCreds.PasswordLastSet)
+				fmt.Fprintf(w, "<li>LogonServer: %v</li>\n", ADCreds.LogonServer)
+				fmt.Fprintf(w, "<li>LogonDomainName: %v</li>\n", ADCreds.LogonDomainName)
+				fmt.Fprintf(w, "<li>LogonDomainID: %v</li>\n", ADCreds.LogonDomainID)
+			}
+		}
+		fmt.Fprint(w, "</ul>")
+	} else {
+		w.WriteHeader(http.StatusUnauthorized)
+		fmt.Fprint(w, "Authentication failed")
+	}
+	fmt.Fprint(w, "</html>")
+	return
+}

+ 88 - 0
v8/examples/example.go

@@ -0,0 +1,88 @@
+// Package examples provides simple examples of gokrb5 use.
+// +build examples
+
+package main
+
+import (
+	"encoding/hex"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/httptest"
+	"os"
+
+	"github.com/jcmturner/goidentity/v6"
+	"github.com/jcmturner/gokrb5/v8/client"
+	"github.com/jcmturner/gokrb5/v8/config"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/service"
+	"github.com/jcmturner/gokrb5/v8/spnego"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+)
+
+func main() {
+	s := httpServer()
+	defer s.Close()
+
+	b, _ := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	c, _ := config.NewFromString(testdata.TEST_KRB5CONF)
+	c.LibDefaults.NoAddresses = true
+	cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c)
+	httpRequest(s.URL, cl)
+
+	b, _ = hex.DecodeString(testdata.TESTUSER2_KEYTAB)
+	kt = keytab.New()
+	kt.Unmarshal(b)
+	c, _ = config.NewFromString(testdata.TEST_KRB5CONF)
+	c.LibDefaults.NoAddresses = true
+	cl = client.NewWithKeytab("testuser2", "TEST.GOKRB5", kt, c)
+	httpRequest(s.URL, cl)
+}
+
+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)
+	}
+	r, _ := http.NewRequest("GET", url, nil)
+	err = spnego.SetSPNEGOHeader(cl, 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 Tests: ", log.Ldate|log.Ltime|log.Lshortfile)
+	b, _ := hex.DecodeString(testdata.HTTP_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+	th := http.HandlerFunc(testAppHandler)
+	s := httptest.NewServer(spnego.SPNEGOKRB5Authenticate(th, kt, service.Logger(l)))
+	return s
+}
+
+func testAppHandler(w http.ResponseWriter, r *http.Request) {
+	creds := goidentity.FromHTTPRequestContext(r)
+	fmt.Fprint(w, "<html>\n<p><h1>TEST.GOKRB5 Handler</h1></p>\n")
+	if creds != nil && creds.Authenticated() {
+		fmt.Fprintf(w, "<ul><li>Authenticed user: %s</li>\n", creds.UserName())
+		fmt.Fprintf(w, "<li>User's realm: %s</li></ul>\n", creds.Domain())
+	} else {
+		w.WriteHeader(http.StatusUnauthorized)
+		fmt.Fprint(w, "Authentication failed")
+	}
+	fmt.Fprint(w, "</html>")
+	return
+}

+ 94 - 0
v8/examples/httpClient.go

@@ -0,0 +1,94 @@
+// +build examples
+
+package main
+
+import (
+	"encoding/hex"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+
+	"github.com/jcmturner/gokrb5/v8/client"
+	"github.com/jcmturner/gokrb5/v8/config"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/spnego"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+)
+
+const (
+	port     = ":9080"
+	kRB5CONF = `[libdefaults]
+  default_realm = TEST.GOKRB5
+  dns_lookup_realm = false
+  dns_lookup_kdc = false
+  ticket_lifetime = 24h
+  forwardable = yes
+  default_tkt_enctypes = aes256-cts-hmac-sha1-96
+  default_tgs_enctypes = aes256-cts-hmac-sha1-96
+
+[realms]
+ TEST.GOKRB5 = {
+  kdc = 127.0.0.1:88
+  admin_server = 127.0.0.1:749
+  default_domain = test.gokrb5
+ }
+
+[domain_realm]
+ .test.gokrb5 = TEST.GOKRB5
+ test.gokrb5 = TEST.GOKRB5
+ `
+)
+
+func main() {
+	l := log.New(os.Stderr, "GOKRB5 Client: ", log.LstdFlags)
+
+	//defer profile.Start(profile.TraceProfile).Stop()
+	// Load the keytab
+	kb, _ := hex.DecodeString(testdata.TESTUSER2_KEYTAB)
+	kt := keytab.New()
+	err := kt.Unmarshal(kb)
+	if err != nil {
+		l.Fatalf("could not load client keytab: %v", err)
+	}
+
+	// Load the client krb5 config
+	conf, err := config.NewFromString(kRB5CONF)
+	if err != nil {
+		l.Fatalf("could not load krb5.conf: %v", err)
+	}
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr != "" {
+		conf.Realms[0].KDC = []string{addr + ":88"}
+	}
+
+	// Create the client with the keytab
+	cl := client.NewWithKeytab("testuser2", "TEST.GOKRB5", kt, conf, client.Logger(l), client.DisablePAFXFAST(true))
+
+	// Log in the client
+	err = cl.Login()
+	if err != nil {
+		l.Fatalf("could not login client: %v", err)
+	}
+
+	// Form the request
+	url := "http://localhost" + port
+	r, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		l.Fatalf("could create request: %v", err)
+	}
+
+	spnegoCl := spnego.NewClient(cl, nil, "HTTP/host.test.gokrb5")
+
+	// Make the request
+	resp, err := spnegoCl.Do(r)
+	if err != nil {
+		l.Fatalf("error making request: %v", err)
+	}
+	b, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		l.Fatalf("error reading response body: %v", err)
+	}
+	fmt.Println(string(b))
+}

+ 105 - 0
v8/examples/httpServer.go

@@ -0,0 +1,105 @@
+// +build examples
+
+package main
+
+import (
+	"encoding/hex"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+
+	"github.com/gorilla/sessions"
+	"github.com/jcmturner/goidentity/v6"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/service"
+	"github.com/jcmturner/gokrb5/v8/spnego"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+	"github.com/pkg/errors"
+)
+
+const (
+	port = ":9080"
+)
+
+func main() {
+	//defer profile.Start(profile.TraceProfile).Stop()
+	// Create logger
+	l := log.New(os.Stderr, "GOKRB5 Service: ", log.Ldate|log.Ltime|log.Lshortfile)
+
+	// Load the service's keytab
+	b, _ := hex.DecodeString(testdata.HTTP_KEYTAB)
+	kt := keytab.New()
+	kt.Unmarshal(b)
+
+	// Create the application's specific handler
+	th := http.HandlerFunc(testAppHandler)
+
+	// Set up handler mappings wrapping in the SPNEGOKRB5Authenticate handler wrapper
+	mux := http.NewServeMux()
+	mux.Handle("/", spnego.SPNEGOKRB5Authenticate(th, kt, service.Logger(l), service.SessionManager(NewSessionMgr("gokrb5"))))
+
+	// Start up the web server
+	log.Fatal(http.ListenAndServe(port, mux))
+}
+
+// Simple application specific handler
+func testAppHandler(w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(http.StatusOK)
+	creds := goidentity.FromHTTPRequestContext(r)
+	fmt.Fprintf(w,
+		`<html>
+<h1>GOKRB5 Handler</h1>
+<ul>
+<li>Authenticed user: %s</li>
+<li>User's realm: %s</li>
+<li>Authn time: %v</li>
+<li>Session ID: %s</li>
+<ul>
+</html>`,
+		creds.UserName(),
+		creds.Domain(),
+		creds.AuthTime(),
+		creds.SessionID(),
+	)
+	return
+}
+
+type SessionMgr struct {
+	skey       []byte
+	store      sessions.Store
+	cookieName string
+}
+
+func NewSessionMgr(cookieName string) SessionMgr {
+	skey := []byte("thisistestsecret") // Best practice is to load this key from a secure location.
+	return SessionMgr{
+		skey:       skey,
+		store:      sessions.NewCookieStore(skey),
+		cookieName: cookieName,
+	}
+}
+
+func (smgr SessionMgr) Get(r *http.Request, k string) ([]byte, error) {
+	s, err := smgr.store.Get(r, smgr.cookieName)
+	if err != nil {
+		return nil, err
+	}
+	if s == nil {
+		return nil, errors.New("nil session")
+	}
+	b, ok := s.Values[k].([]byte)
+	if !ok {
+		return nil, fmt.Errorf("could not get bytes held in session at %s", k)
+	}
+	return b, nil
+}
+
+func (smgr SessionMgr) New(w http.ResponseWriter, r *http.Request, k string, v []byte) error {
+	s, err := smgr.store.New(r, smgr.cookieName)
+	if err != nil {
+		return fmt.Errorf("could not get new session from session manager: %v", err)
+	}
+	s.Values[k] = v
+	return s.Save(r, w)
+}

+ 78 - 0
v8/examples/longRunningClient.go

@@ -0,0 +1,78 @@
+// +build examples
+
+package main
+
+import (
+	"encoding/hex"
+	"log"
+	"os"
+	"time"
+
+	"github.com/jcmturner/gokrb5/v8/client"
+	"github.com/jcmturner/gokrb5/v8/config"
+	"github.com/jcmturner/gokrb5/v8/keytab"
+	"github.com/jcmturner/gokrb5/v8/test/testdata"
+)
+
+const (
+	kRB5CONF = `[libdefaults]
+  default_realm = TEST.GOKRB5
+  dns_lookup_realm = false
+  dns_lookup_kdc = false
+  ticket_lifetime = 24h
+  forwardable = yes
+  default_tkt_enctypes = aes256-cts-hmac-sha1-96
+  default_tgs_enctypes = aes256-cts-hmac-sha1-96
+
+[realms]
+ TEST.GOKRB5 = {
+  kdc = 10.80.88.88:88
+  admin_server = 10.80.88.88:749
+  default_domain = test.gokrb5
+ }
+
+[domain_realm]
+ .test.gokrb5 = TEST.GOKRB5
+ test.gokrb5 = TEST.GOKRB5
+ `
+)
+
+func main() {
+	l := log.New(os.Stderr, "GOKRB5 Client: ", log.LstdFlags)
+
+	//defer profile.Start(profile.TraceProfile).Stop()
+	// Load the keytab
+	kb, _ := hex.DecodeString(testdata.TESTUSER2_KEYTAB)
+	kt := keytab.New()
+	err := kt.Unmarshal(kb)
+	if err != nil {
+		l.Fatalf("could not load client keytab: %v", err)
+	}
+
+	// Load the client krb5 config
+	conf, err := config.NewFromString(kRB5CONF)
+	if err != nil {
+		l.Fatalf("could not load krb5.conf: %v", err)
+	}
+	addr := os.Getenv("TEST_KDC_ADDR")
+	if addr != "" {
+		conf.Realms[0].KDC = []string{addr + ":88"}
+	}
+
+	// Create the client with the keytab
+	cl := client.NewWithKeytab("testuser2", "TEST.GOKRB5", kt, conf, client.Logger(l), client.DisablePAFXFAST(true))
+
+	// Log in the client
+	err = cl.Login()
+	if err != nil {
+		l.Fatalf("could not login client: %v", err)
+	}
+
+	for {
+		_, _, err := cl.GetServiceTicket("HTTP/host.test.gokrb5")
+		if err != nil {
+			l.Printf("failed to get service ticket: %v\n", err)
+		}
+		time.Sleep(time.Minute * 5)
+	}
+}

+ 15 - 0
v8/go.mod

@@ -0,0 +1,15 @@
+module github.com/jcmturner/gokrb5/v8
+
+go 1.13
+
+require (
+	github.com/gorilla/sessions v1.2.0
+	github.com/hashicorp/go-uuid v1.0.2
+	github.com/jcmturner/aescts v1.0.1
+	github.com/jcmturner/dnsutils v1.0.1
+	github.com/jcmturner/gofork v1.0.0
+	github.com/jcmturner/goidentity/v6 v6.0.1
+	github.com/jcmturner/rpc/v2 v2.0.2
+	github.com/stretchr/testify v1.4.0
+	golang.org/x/crypto v0.0.0-20200117160349-530e935923ad
+)

+ 37 - 0
v8/go.sum

@@ -0,0 +1,37 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
+github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
+github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/jcmturner/aescts v1.0.1 h1:5jhUSHbHSZjQeWFY//Lv8dpP/O3sMDOxrGV/IfCqh44=
+github.com/jcmturner/aescts v1.0.1/go.mod h1:k9gJoDUf1GH5r2IBtBjwjDCoLELYxOcEhitdP8RL7qQ=
+github.com/jcmturner/dnsutils v1.0.1 h1:zkF8SbVatbr5LGrvcPSes62SV68lASVv6+x9wo2De+w=
+github.com/jcmturner/dnsutils v1.0.1/go.mod h1:tqMo38L01jO8AKxT0S9OQVlGZu3dkEt+z5CA+LOhwB0=
+github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
+github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
+github.com/jcmturner/goidentity v6.0.1+incompatible h1:I+jJ9JbbrqUAiMB8sNLXlginFJ2lPrxst/N3kb67Dko=
+github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
+github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
+github.com/jcmturner/rpc v2.0.2+incompatible h1:GyRUpVSWVhLISk7Q5rtb6kmejBV4mgO8Q6eR4l6pDF8=
+github.com/jcmturner/rpc/v2 v2.0.2 h1:gMB4IwRXYsWw4Bc6o/az2HJgFUA1ffSh90i26ZJ6Xl0=
+github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
+golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików