upload/koji: add support for GSSAPI/Kerberos auth

Prior this commit we only had support for username/password authentication
in the koji integration. This wasn't particularly useful because this
auth type isn't used in any production instance.

This commit adds the support for GSSAPI/Kerberos authentication.
The implementation uses kerby library which is very lightweight wrapper
around C gssapi library.

Also, the koji unit test and the run-koji-container script were modified
so the GSSAPI auth is fully tested.
This commit is contained in:
Ondřej Budai 2020-08-19 15:14:20 +02:00 committed by Tom Gundersen
parent ecc7340570
commit 05fd221bd4
21 changed files with 1637 additions and 31 deletions

2
.github/koji.conf vendored
View file

@ -66,3 +66,5 @@ plugins = runroot
; use the fast upload feature of koji by default ; use the fast upload feature of koji by default
use_fast_upload = yes use_fast_upload = yes
serverca = /tmp/osbuild-composer-koji-test/ca-crt.pem

7
.github/krb5.conf vendored Normal file
View file

@ -0,0 +1,7 @@
include /etc/krb5.conf
[realms]
LOCAL = {
kdc = localhost
admin_server = localhost
}

View file

@ -40,6 +40,10 @@ jobs:
- name: Install golangci-lint - name: Install golangci-lint
run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.30.0 run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.30.0
# This is needed to lint internal/upload/koji package
- name: Install kerberos devel package
run: sudo apt-get install -y libkrb5-dev
- name: Run golangci-lint - name: Run golangci-lint
run: $(go env GOPATH)/bin/golangci-lint run --timeout 5m0s run: $(go env GOPATH)/bin/golangci-lint run --timeout 5m0s
@ -73,7 +77,7 @@ jobs:
# and installed here. See the last line of the script. # and installed here. See the last line of the script.
- name: Install koji client - name: Install koji client
run: | run: |
sudo apt-get install -y libkrb5-dev sudo apt-get install -y libkrb5-dev krb5-config
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install koji pip install koji
sudo cp .github/koji.conf /etc/koji.conf sudo cp .github/koji.conf /etc/koji.conf
@ -81,7 +85,7 @@ jobs:
- name: Run unit tests - name: Run unit tests
run: | run: |
sudo internal/upload/koji/run-koji-container.sh start sudo internal/upload/koji/run-koji-container.sh start
go test -v -race -covermode atomic -coverprofile=coverage.txt -tags koji_test ./internal/upload/koji env KRB5_CONFIG=../../../.github/krb5.conf go test -v -race -covermode atomic -coverprofile=coverage.txt -tags koji_test ./internal/upload/koji
sudo internal/upload/koji/run-koji-container.sh stop sudo internal/upload/koji/run-koji-container.sh stop
- name: Send coverage to codecov.io - name: Send coverage to codecov.io

View file

@ -39,7 +39,7 @@ func main() {
} }
defer file.Close() defer file.Close()
k, err := koji.Login(server, "osbuild", "osbuildpass", http.DefaultTransport) k, err := koji.NewFromPlain(server, "osbuild", "osbuildpass", http.DefaultTransport)
if err != nil { if err != nil {
println(err.Error()) println(err.Error())
return return

1
go.mod
View file

@ -21,6 +21,7 @@ require (
github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
github.com/ubccr/kerby v0.0.0-20170626144437-201a958fc453
github.com/vmware/govmomi v0.23.0 github.com/vmware/govmomi v0.23.0
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4

2
go.sum
View file

@ -81,6 +81,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ubccr/kerby v0.0.0-20170626144437-201a958fc453 h1:rN0NwUFS6oK9ESlk2QyKfucb/gL4opUutNlCS2bBlvA=
github.com/ubccr/kerby v0.0.0-20170626144437-201a958fc453/go.mod h1:s59e1aOY3F3KNsRx5W8cMdbtbt49aSKL7alLp6EKn48=
github.com/vmware/govmomi v0.23.0 h1:DC97v1FdSr3cPfq3eBKD5C1O4JtYxo+NTcbGTKe2k48= github.com/vmware/govmomi v0.23.0 h1:DC97v1FdSr3cPfq3eBKD5C1O4JtYxo+NTcbGTKe2k48=
github.com/vmware/govmomi v0.23.0/go.mod h1:Y+Wq4lst78L85Ge/F8+ORXIWiKYqaro1vhAulACy9Lc= github.com/vmware/govmomi v0.23.0/go.mod h1:Y+Wq4lst78L85Ge/F8+ORXIWiKYqaro1vhAulACy9Lc=
github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk= github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk=

View file

@ -4,14 +4,17 @@ import (
"bytes" "bytes"
"crypto/md5" "crypto/md5"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"hash/adler32" "hash/adler32"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os"
"github.com/kolo/xmlrpc" "github.com/kolo/xmlrpc"
"github.com/ubccr/kerby/khttp"
) )
type Koji struct { type Koji struct {
@ -106,26 +109,17 @@ type CGImportResult struct {
BuildID int `xmlrpc:"build_id"` BuildID int `xmlrpc:"build_id"`
} }
func Login(server, user, password string, transport http.RoundTripper) (*Koji, error) { type GSSAPICredentials struct {
// Create a temporary xmlrpc client. Principal string
// The API doesn't require sessionID, sessionKey and callnum yet, KeyTab string
// so there's no need to use the custom Koji RoundTripper, }
// let's just use the one that the called passed in.
loginClient, err := xmlrpc.NewClient(server, http.DefaultTransport)
if err != nil {
return nil, err
}
args := []interface{}{user, password} type loginReply struct {
var reply struct {
SessionID int64 `xmlrpc:"session-id"` SessionID int64 `xmlrpc:"session-id"`
SessionKey string `xmlrpc:"session-key"` SessionKey string `xmlrpc:"session-key"`
} }
err = loginClient.Call("login", args, &reply)
if err != nil {
return nil, err
}
func newKoji(server string, transport http.RoundTripper, reply loginReply) (*Koji, error) {
// Create the final xmlrpc client with our custom RoundTripper handling // Create the final xmlrpc client with our custom RoundTripper handling
// sessionID, sessionKey and callnum // sessionID, sessionKey and callnum
kojiTransport := &Transport{ kojiTransport := &Transport{
@ -134,7 +128,6 @@ func Login(server, user, password string, transport http.RoundTripper) (*Koji, e
callnum: 0, callnum: 0,
transport: transport, transport: transport,
} }
client, err := xmlrpc.NewClient(server, kojiTransport) client, err := xmlrpc.NewClient(server, kojiTransport)
if err != nil { if err != nil {
return nil, err return nil, err
@ -147,6 +140,55 @@ func Login(server, user, password string, transport http.RoundTripper) (*Koji, e
}, nil }, nil
} }
// NewFromPlain creates a new Koji sessions =authenticated using the plain
// username/password method. If you want to speak to a public koji instance,
// you probably cannot use this method.
func NewFromPlain(server, user, password string, transport http.RoundTripper) (*Koji, error) {
// Create a temporary xmlrpc client.
// The API doesn't require sessionID, sessionKey and callnum yet,
// so there's no need to use the custom Koji RoundTripper,
// let's just use the one that the called passed in.
loginClient, err := xmlrpc.NewClient(server, http.DefaultTransport)
if err != nil {
return nil, err
}
args := []interface{}{user, password}
var reply loginReply
err = loginClient.Call("login", args, &reply)
if err != nil {
return nil, err
}
return newKoji(server, transport, reply)
}
// NewFromGSSAPI creates a new Koji session authenticated using GSSAPI.
// Principal and keytab used for the session is passed using credentials
// parameter.
func NewFromGSSAPI(server string, credentials *GSSAPICredentials, transport http.RoundTripper) (*Koji, error) {
// Create a temporary xmlrpc client with kerberos transport.
// The API doesn't require sessionID, sessionKey and callnum yet,
// so there's no need to use the custom Koji RoundTripper,
// let's just use the one that the called passed in.
loginClient, err := xmlrpc.NewClient(server+"/ssllogin", &khttp.Transport{
KeyTab: credentials.KeyTab,
Principal: credentials.Principal,
Next: transport,
})
if err != nil {
return nil, err
}
var reply loginReply
err = loginClient.Call("sslLogin", nil, &reply)
if err != nil {
return nil, err
}
return newKoji(server, transport, reply)
}
// GetAPIVersion gets the version of the API of the remote Koji instance // GetAPIVersion gets the version of the API of the remote Koji instance
func (k *Koji) GetAPIVersion() (int, error) { func (k *Koji) GetAPIVersion() (int, error) {
var version int var version int
@ -309,3 +351,17 @@ func (rt *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
return rt.transport.RoundTrip(rClone) return rt.transport.RoundTrip(rClone)
} }
func GSSAPICredentialsFromEnv() (*GSSAPICredentials, error) {
principal, principalExists := os.LookupEnv("OSBUILD_COMPOSER_KOJI_PRINCIPAL")
keyTab, keyTabExists := os.LookupEnv("OSBUILD_COMPOSER_KOJI_KEYTAB")
if !principalExists || !keyTabExists {
return nil, errors.New("Both OSBUILD_COMPOSER_KOJI_PRINCIPAL and OSBUILD_COMPOSER_KOJI_KEYTAB must be set")
}
return &GSSAPICredentials{
Principal: principal,
KeyTab: keyTab,
}, nil
}

View file

@ -4,6 +4,8 @@ package koji_test
import ( import (
"crypto/rand" "crypto/rand"
"crypto/tls"
"crypto/x509"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -22,17 +24,36 @@ import (
func TestKojiImport(t *testing.T) { func TestKojiImport(t *testing.T) {
// define constants // define constants
server := "http://localhost:8080/kojihub" server := "https://localhost/kojihub"
user := "osbuild"
password := "osbuildpass"
filename := "image.qcow2" filename := "image.qcow2"
filesize := 1024 filesize := 1024
shareDir := "/tmp/osbuild-composer-koji-test"
// you cannot create two build with a same name, let's create a random one each time // you cannot create two build with a same name, let's create a random one each time
buildName := "osbuild-image-" + uuid.Must(uuid.NewRandom()).String() buildName := "osbuild-image-" + uuid.Must(uuid.NewRandom()).String()
// koji needs to specify a directory to which the upload should happen, let's reuse the build name // koji needs to specify a directory to which the upload should happen, let's reuse the build name
uploadDirectory := buildName uploadDirectory := buildName
k, err := koji.Login(server, user, password, http.DefaultTransport) // base our transport on the default one
transport := http.DefaultTransport.(*http.Transport).Clone()
// use the self-signed certificate generated by run-koji-container
certPool := x509.NewCertPool()
cert, err := ioutil.ReadFile(shareDir + "/ca-crt.pem")
require.NoError(t, err)
ok := certPool.AppendCertsFromPEM(cert)
require.True(t, ok)
transport.TLSClientConfig = &tls.Config{
RootCAs: certPool,
}
// login
credentials := &koji.GSSAPICredentials{
Principal: "osbuild-krb@LOCAL",
KeyTab: shareDir + "/client.keytab",
}
k, err := koji.NewFromGSSAPI(server, credentials, transport)
require.NoError(t, err) require.NoError(t, err)
defer func() { defer func() {
@ -111,9 +132,9 @@ func TestKojiImport(t *testing.T) {
cmd := exec.Command( cmd := exec.Command(
"koji", "koji",
"--server", server, "--server", server,
"--user", user, "-c", "../../../.github/koji.conf",
"--password", password, "--keytab", credentials.KeyTab,
"--authtype", "password", "--principal", credentials.Principal,
"list-builds", "list-builds",
"--buildid", strconv.Itoa(result.BuildID), "--buildid", strconv.Itoa(result.BuildID),
) )

View file

@ -1,19 +1,27 @@
#!/bin/bash #!/bin/bash
set -eu set -eu
SHARE_DIR=/tmp/osbuild-composer-koji-test
koji_stop () { koji_stop () {
echo "Shutting down containers, please wait..." echo "Shutting down containers, please wait..."
${CONTAINER_RUNTIME} stop org.osbuild.koji.koji || true ${CONTAINER_RUNTIME} stop org.osbuild.koji.koji || true
${CONTAINER_RUNTIME} rm org.osbuild.koji.koji || true ${CONTAINER_RUNTIME} rm org.osbuild.koji.koji || true
${CONTAINER_RUNTIME} stop org.osbuild.koji.kdc || true
${CONTAINER_RUNTIME} rm org.osbuild.koji.kdc || true
${CONTAINER_RUNTIME} stop org.osbuild.koji.postgres || true ${CONTAINER_RUNTIME} stop org.osbuild.koji.postgres || true
${CONTAINER_RUNTIME} rm org.osbuild.koji.postgres || true ${CONTAINER_RUNTIME} rm org.osbuild.koji.postgres || true
${CONTAINER_RUNTIME} network rm -f org.osbuild.koji || true ${CONTAINER_RUNTIME} network rm -f org.osbuild.koji || true
rm -rf "${SHARE_DIR}" || true
} }
koji_clean_up_bad_start () { koji_clean_up_bad_start () {
# remember the exit code, so we can report it later
EXIT_CODE=$? EXIT_CODE=$?
echo "Start failed, removing containers." echo "Start failed, removing containers."
@ -22,23 +30,80 @@ koji_clean_up_bad_start () {
exit $EXIT_CODE exit $EXIT_CODE
} }
# helper to simplify sql queries to the postgres instance
psql_cmd () {
${CONTAINER_RUNTIME} exec org.osbuild.koji.postgres psql -U koji -d koji "$@"
}
# helper to simplify running commands in the kdc container
kdc_exec() {
${CONTAINER_RUNTIME} exec org.osbuild.koji.kdc "$@"
}
koji_start() { koji_start() {
trap koji_clean_up_bad_start EXIT trap koji_clean_up_bad_start EXIT
# create a share directory which is used to share files between the host and containers
mkdir "${SHARE_DIR}"
# generate self-signed certificates in the share directory
openssl req -new -nodes -x509 -days 365 -keyout "${SHARE_DIR}/ca-key.pem" -out "${SHARE_DIR}/ca-crt.pem" -subj "/CN=osbuild.org"
openssl genrsa -out "${SHARE_DIR}/key.pem" 2048
openssl req -new -sha256 -key "${SHARE_DIR}/key.pem" -out "${SHARE_DIR}/csr.pem" -subj "/CN=localhost"
openssl x509 -req -in "${SHARE_DIR}/csr.pem" -CA "${SHARE_DIR}/ca-crt.pem" -CAkey "${SHARE_DIR}/ca-key.pem" -CAcreateserial -out "${SHARE_DIR}/crt.pem"
${CONTAINER_RUNTIME} network create org.osbuild.koji ${CONTAINER_RUNTIME} network create org.osbuild.koji
${CONTAINER_RUNTIME} run -d --name org.osbuild.koji.postgres --network org.osbuild.koji \ ${CONTAINER_RUNTIME} run -d --name org.osbuild.koji.postgres --network org.osbuild.koji \
-e POSTGRES_USER=koji \ -e POSTGRES_USER=koji \
-e POSTGRES_PASSWORD=kojipass \ -e POSTGRES_PASSWORD=kojipass \
-e POSTGRES_DB=koji \ -e POSTGRES_DB=koji \
docker.io/library/postgres:12-alpine docker.io/library/postgres:12-alpine
${CONTAINER_RUNTIME} run -d --name org.osbuild.koji.kdc \
--network org.osbuild.koji \
-v "${SHARE_DIR}:/share:z" \
-p 88:88/udp \
quay.io/osbuild/kdc:v1
# initialize krb pricipals and create keytabs for them
# HTTP/localhost@LOCAL for kojihub
kdc_exec kadmin.local -r LOCAL add_principal -randkey HTTP/localhost@LOCAL
kdc_exec kadmin.local -r LOCAL ktadd -k /share/koji.keytab HTTP/localhost@LOCAL
kdc_exec chmod 644 /share/koji.keytab
# osbuild-krb@LOCAL for koji clients
kdc_exec kadmin.local -r LOCAL add_principal -randkey osbuild-krb@LOCAL
kdc_exec kadmin.local -r LOCAL ktadd -k /share/client.keytab osbuild-krb@LOCAL
kdc_exec chmod 644 /share/client.keytab
${CONTAINER_RUNTIME} run -d --name org.osbuild.koji.koji --network org.osbuild.koji \ ${CONTAINER_RUNTIME} run -d --name org.osbuild.koji.koji --network org.osbuild.koji \
-p 8080:80 \ -v "${SHARE_DIR}:/share:z" \
-p 80:80 \
-p 443:443 \
-e POSTGRES_USER=koji \ -e POSTGRES_USER=koji \
-e POSTGRES_PASSWORD=kojipass \ -e POSTGRES_PASSWORD=kojipass \
-e POSTGRES_DB=koji \ -e POSTGRES_DB=koji \
-e POSTGRES_HOST=org.osbuild.koji.postgres \ -e POSTGRES_HOST=org.osbuild.koji.postgres \
quay.io/osbuild/ghci-koji:v1 quay.io/osbuild/koji:v1
# TODO: we need to wait for the database to be initialized here. A better method should be used.
sleep 2
# create koji users
# kojiadmin/kojipass - admin
# osbuild/osbuildpass - regular user
# osbuild-krb: - regular user authenticated with Kerberos principal osbuild-krb@LOCAL
psql_cmd -c "insert into users (name, password, status, usertype) values ('kojiadmin', 'kojipass', 0, 0)" >/dev/null
psql_cmd -c "insert into user_perms (user_id, perm_id, creator_id) values (1, 1, 1)" >/dev/null
psql_cmd -c "insert into users (name, password, status, usertype) values ('osbuild', 'osbuildpass', 0, 0)" >/dev/null
psql_cmd -c "insert into users (name, status, usertype) values ('osbuild-krb', 0, 0)" >/dev/null
psql_cmd -c "insert into user_krb_principals (user_id, krb_principal) values (3, 'osbuild-krb@LOCAL')" >/dev/null
# create content generator osbuild, give osbuild and osbuild-krb users access to it
psql_cmd -c "insert into content_generator (name) values ('osbuild')" >/dev/null
psql_cmd -c "insert into cg_users (cg_id, user_id, creator_id, active) values (1, 2, 1, true), (1, 3, 1, true)" >/dev/null
echo "Containers are running, to stop them use:" echo "Containers are running, to stop them use:"
echo "$0 stop" echo "$0 stop"
@ -46,6 +111,7 @@ koji_start() {
trap - EXIT trap - EXIT
} }
# check arguments
if [[ $# -ne 1 || ( "$1" != "start" && "$1" != "stop" ) ]]; then if [[ $# -ne 1 || ( "$1" != "start" && "$1" != "stop" ) ]]; then
cat <<DOC cat <<DOC
usage: $0 start|stop usage: $0 start|stop
@ -56,11 +122,13 @@ DOC
exit 3 exit 3
fi fi
# this script must be run as root
if [ $UID != 0 ]; then if [ $UID != 0 ]; then
echo This script must be run as root. echo This script must be run as root.
exit 1 exit 1
fi fi
# decide whether podman or docker should be used
if which podman 2>/dev/null >&2; then if which podman 2>/dev/null >&2; then
CONTAINER_RUNTIME=podman CONTAINER_RUNTIME=podman
elif which docker 2>/dev/null >&2; then elif which docker 2>/dev/null >&2; then

7
krb5.conf Normal file
View file

@ -0,0 +1,7 @@
include /etc/krb5.conf
[realms]
LOCAL = {
kdc = localhost
admin_server = localhost
}

1
vendor/github.com/ubccr/kerby/.gitignore generated vendored Normal file
View file

@ -0,0 +1 @@
cmd/kerby/kerby

202
vendor/github.com/ubccr/kerby/LICENSE generated vendored Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

136
vendor/github.com/ubccr/kerby/README.rst generated vendored Normal file
View file

@ -0,0 +1,136 @@
===============================================================================
Kerby - Go wrapper for Kerberos GSSAPI
===============================================================================
|godoc|
This is a port of the PyKerberos library in Go. The main motivation for this
library was to provide HTTP client authentication using Kerberos. The khttp
package provides a transport that authenticates all outgoing requests using
SPNEGO (negotiate authentication) http://tools.ietf.org/html/rfc4559.
The C code is adapted from PyKerberos http://calendarserver.org/wiki/PyKerberos.
------------------------------------------------------------------------
Usage
------------------------------------------------------------------------
Note: You need the have the krb5-libs/GSSAPI packages installed for your OS.
Install using go tools::
$ go get github.com/ubccr/kerby
To run the unit tests you must have a valid Kerberos setup on the test machine
and you should ensure that you have valid Kerberos tickets (run 'klist' on the
command line). If you're authentication using a client keytab file you can
optionally export the env variable KRB5_CLIENT_KTNAME::
$ export KRB5_CLIENT_KTNAME=/path/to/client.keytab
$ export KERBY_TEST_SERVICE="service@REALM"
$ export KERBY_TEST_PRINC="princ@REALM"
$ go test
Example HTTP Kerberos client authentication using a client keytab file::
package main
import (
"fmt"
"io/ioutil"
"bytes"
"net/http"
"github.com/ubccr/kerby/khttp"
)
func main() {
payload := []byte(`{"method":"hello_world"}`)
req, err := http.NewRequest(
"POST",
"https://server.example.com/json",
bytes.NewBuffer(payload))
req.Header.Set("Content-Type", "application/json")
t := &khttp.Transport{
KeyTab: "/path/to/client.keytab",
Principal: "principal@REALM"}
client := &http.Client{Transport: t}
res, err := client.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Printf("%d\n", res.StatusCode)
fmt.Printf("%s", data)
}
Example HTTP handler supporting Kerberose authentication::
func handler(w http.ResponseWriter, req *http.Request) {
authReq := strings.Split(req.Header.Get(authorizationHeader), " ")
if len(authReq) != 2 || authReq[0] != negotiateHeader {
w.Header().Set(wwwAuthenticateHeader, negotiateHeader)
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
return
}
ks := new(kerby.KerbServer)
err := ks.Init("")
if err != nil {
log.Printf("KerbServer Init Error: %s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer ks.Clean()
err = ks.Step(authReq[1])
w.Header().Set(wwwAuthenticateHeader, negotiateHeader+" "+ks.Response())
if err != nil {
log.Printf("KerbServer Step Error: %s", err.Error())
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
user := ks.UserName()
fmt.Fprintf(w, "Hello, %s", user)
}
Example adding Kerberos authentication to an http.FileServer using khttp.Handler::
package main
import (
"github.com/ubccr/kerby/khttp"
"log"
"net/http"
)
func main() {
http.Handle("/", khttp.Handler(http.FileServer(http.Dir("/tmp"))))
log.Fatal(http.ListenAndServe(":8000", nil))
}
------------------------------------------------------------------------
License
------------------------------------------------------------------------
Kerby is released under the Apache 2.0 License. See the LICENSE file.
.. |godoc| image:: https://godoc.org/github.com/golang/gddo?status.svg
:target: https://godoc.org/github.com/ubccr/kerby
:alt: Godoc

125
vendor/github.com/ubccr/kerby/base64.c generated vendored Normal file
View file

@ -0,0 +1,125 @@
/**
* Copyright (c) 2006-2015 Apple Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
#include "base64.h"
#include <stdlib.h>
#include <string.h>
// base64 tables
static char basis_64[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static signed char index_64[128] =
{
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1,-1,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
-1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
};
#define CHAR64(c) (((c) < 0 || (c) > 127) ? -1 : index_64[(c)])
// base64_encode : base64 encode
//
// value : data to encode
// vlen : length of data
// (result) : new char[] - c-str of result
char *base64_encode(const unsigned char *value, size_t vlen)
{
char *result = (char *)malloc((vlen * 4) / 3 + 5);
char *out = result;
while (vlen >= 3)
{
*out++ = basis_64[value[0] >> 2];
*out++ = basis_64[((value[0] << 4) & 0x30) | (value[1] >> 4)];
*out++ = basis_64[((value[1] << 2) & 0x3C) | (value[2] >> 6)];
*out++ = basis_64[value[2] & 0x3F];
value += 3;
vlen -= 3;
}
if (vlen > 0)
{
*out++ = basis_64[value[0] >> 2];
unsigned char oval = (value[0] << 4) & 0x30;
if (vlen > 1) oval |= value[1] >> 4;
*out++ = basis_64[oval];
*out++ = (vlen < 2) ? '=' : basis_64[(value[1] << 2) & 0x3C];
*out++ = '=';
}
*out = '\0';
return result;
}
// base64_decode : base64 decode
//
// value : c-str to decode
// rlen : length of decoded result
// (result) : new unsigned char[] - decoded result
unsigned char *base64_decode(const char *value, size_t *rlen)
{
*rlen = 0;
int c1, c2, c3, c4;
size_t vlen = strlen(value);
unsigned char *result =(unsigned char *)malloc((vlen * 3) / 4 + 1);
unsigned char *out = result;
while (1) {
if (value[0]==0) {
return result;
}
c1 = value[0];
if (CHAR64(c1) == -1) {
goto base64_decode_error;;
}
c2 = value[1];
if (CHAR64(c2) == -1) {
goto base64_decode_error;;
}
c3 = value[2];
if ((c3 != '=') && (CHAR64(c3) == -1)) {
goto base64_decode_error;;
}
c4 = value[3];
if ((c4 != '=') && (CHAR64(c4) == -1)) {
goto base64_decode_error;;
}
value += 4;
*out++ = (CHAR64(c1) << 2) | (CHAR64(c2) >> 4);
*rlen += 1;
if (c3 != '=') {
*out++ = ((CHAR64(c2) << 4) & 0xf0) | (CHAR64(c3) >> 2);
*rlen += 1;
if (c4 != '=') {
*out++ = ((CHAR64(c3) << 6) & 0xc0) | CHAR64(c4);
*rlen += 1;
}
}
}
base64_decode_error:
*result = 0;
*rlen = 0;
return result;
}

20
vendor/github.com/ubccr/kerby/base64.h generated vendored Normal file
View file

@ -0,0 +1,20 @@
/**
* Copyright (c) 2006-2015 Apple Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
#include <stddef.h>
char *base64_encode(const unsigned char *value, size_t vlen);
unsigned char *base64_decode(const char *value, size_t *rlen);

461
vendor/github.com/ubccr/kerby/kerberosgss.c generated vendored Normal file
View file

@ -0,0 +1,461 @@
/**
* Adopted from PyKerberos. Modified for use with Kerby.
*
* Copyright (c) 2006-2015 Apple Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
#include "kerberosgss.h"
#include "base64.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
gss_client_state* new_gss_client_state() {
gss_client_state *state;
state = (gss_client_state *) malloc(sizeof(gss_client_state));
return state;
}
gss_server_state* new_gss_server_state() {
gss_server_state *state;
state = (gss_server_state *) malloc(sizeof(gss_server_state));
return state;
}
void free_gss_client_state(gss_client_state *state) {
free(state);
}
void free_gss_server_state(gss_server_state *state) {
free(state);
}
int authenticate_gss_client_init(
const char* service, const char* principal, long int gss_flags,
gss_server_state* delegatestate, gss_client_state* state
)
{
gss_buffer_desc name_token = GSS_C_EMPTY_BUFFER;
gss_buffer_desc principal_token = GSS_C_EMPTY_BUFFER;
int ret = AUTH_GSS_COMPLETE;
state->server_name = GSS_C_NO_NAME;
state->context = GSS_C_NO_CONTEXT;
state->gss_flags = gss_flags;
state->client_creds = GSS_C_NO_CREDENTIAL;
state->username = NULL;
state->response = NULL;
// Import server name first
name_token.length = strlen(service);
name_token.value = (char *)service;
state->maj_stat = gss_import_name(
&state->min_stat, &name_token, gss_krb5_nt_service_name, &state->server_name
);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
// Use the delegate credentials if they exist
if (delegatestate && delegatestate->client_creds != GSS_C_NO_CREDENTIAL) {
state->client_creds = delegatestate->client_creds;
}
// If available use the principal to extract its associated credentials
else if (principal && *principal) {
gss_name_t name;
principal_token.length = strlen(principal);
principal_token.value = (char *)principal;
state->maj_stat = gss_import_name(
&state->min_stat, &principal_token, GSS_C_NT_USER_NAME, &name
);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
state->maj_stat = gss_acquire_cred(
&state->min_stat, name, GSS_C_INDEFINITE, GSS_C_NO_OID_SET,
GSS_C_INITIATE, &state->client_creds, NULL, NULL
);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
state->maj_stat = gss_release_name(&state->min_stat, &name);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
}
end:
return ret;
}
int authenticate_gss_client_clean(gss_client_state *state)
{
OM_uint32 maj_stat;
OM_uint32 min_stat;
int ret = AUTH_GSS_COMPLETE;
if (state->context != GSS_C_NO_CONTEXT) {
maj_stat = gss_delete_sec_context(
&min_stat, &state->context, GSS_C_NO_BUFFER
);
}
if (state->server_name != GSS_C_NO_NAME) {
maj_stat = gss_release_name(&min_stat, &state->server_name);
}
if (
state->client_creds != GSS_C_NO_CREDENTIAL &&
! (state->gss_flags & GSS_C_DELEG_FLAG)
) {
maj_stat = gss_release_cred(&min_stat, &state->client_creds);
}
if (state->username != NULL) {
free(state->username);
state->username = NULL;
}
if (state->response != NULL) {
free(state->response);
state->response = NULL;
}
return ret;
}
int authenticate_gss_client_step(
gss_client_state* state, const char* challenge
) {
gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER;
gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER;
int ret = AUTH_GSS_CONTINUE;
// Always clear out the old response
if (state->response != NULL) {
free(state->response);
state->response = NULL;
}
// If there is a challenge (data from the server) we need to give it to GSS
if (challenge && *challenge) {
size_t len;
input_token.value = base64_decode(challenge, &len);
input_token.length = len;
}
// Do GSSAPI step
state->maj_stat = gss_init_sec_context(
&state->min_stat,
state->client_creds,
&state->context,
state->server_name,
GSS_C_NO_OID,
(OM_uint32)state->gss_flags,
0,
GSS_C_NO_CHANNEL_BINDINGS,
&input_token,
NULL,
&output_token,
NULL,
NULL
);
if ((state->maj_stat != GSS_S_COMPLETE) && (state->maj_stat != GSS_S_CONTINUE_NEEDED)) {
ret = AUTH_GSS_ERROR;
goto end;
}
ret = (state->maj_stat == GSS_S_COMPLETE) ? AUTH_GSS_COMPLETE : AUTH_GSS_CONTINUE;
// Grab the client response to send back to the server
if (output_token.length) {
state->response = base64_encode((const unsigned char *)output_token.value, output_token.length);;
state->maj_stat = gss_release_buffer(&state->min_stat, &output_token);
}
// Try to get the user name if we have completed all GSS operations
if (ret == AUTH_GSS_COMPLETE) {
gss_name_t gssuser = GSS_C_NO_NAME;
state->maj_stat = gss_inquire_context(&state->min_stat, state->context, &gssuser, NULL, NULL, NULL, NULL, NULL, NULL);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
gss_buffer_desc name_token;
name_token.length = 0;
state->maj_stat = gss_display_name(&state->min_stat, gssuser, &name_token, NULL);
if (GSS_ERROR(state->maj_stat)) {
if (name_token.value)
gss_release_buffer(&state->min_stat, &name_token);
gss_release_name(&state->min_stat, &gssuser);
ret = AUTH_GSS_ERROR;
goto end;
} else {
state->username = (char *)malloc(name_token.length + 1);
strncpy(state->username, (char*) name_token.value, name_token.length);
state->username[name_token.length] = 0;
gss_release_buffer(&state->min_stat, &name_token);
gss_release_name(&state->min_stat, &gssuser);
}
}
end:
if (output_token.value) {
gss_release_buffer(&state->min_stat, &output_token);
}
if (input_token.value) {
free(input_token.value);
}
return ret;
}
int authenticate_gss_server_init(const char *service, gss_server_state *state)
{
gss_buffer_desc name_token = GSS_C_EMPTY_BUFFER;
int ret = AUTH_GSS_COMPLETE;
state->context = GSS_C_NO_CONTEXT;
state->server_name = GSS_C_NO_NAME;
state->client_name = GSS_C_NO_NAME;
state->server_creds = GSS_C_NO_CREDENTIAL;
state->client_creds = GSS_C_NO_CREDENTIAL;
state->username = NULL;
state->targetname = NULL;
state->response = NULL;
state->ccname = NULL;
// Server name may be empty which means we aren't going to create our own creds
size_t service_len = strlen(service);
if (service_len != 0) {
// Import server name first
name_token.length = strlen(service);
name_token.value = (char *)service;
state->maj_stat = gss_import_name(
&state->min_stat, &name_token, GSS_C_NT_HOSTBASED_SERVICE,
&state->server_name
);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
// Get credentials
state->maj_stat = gss_acquire_cred(
&state->min_stat, GSS_C_NO_NAME, GSS_C_INDEFINITE, GSS_C_NO_OID_SET,
GSS_C_BOTH, &state->server_creds, NULL, NULL
);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
}
end:
return ret;
}
int authenticate_gss_server_clean(gss_server_state *state)
{
int ret = AUTH_GSS_COMPLETE;
if (state->context != GSS_C_NO_CONTEXT) {
state->maj_stat = gss_delete_sec_context(
&state->min_stat, &state->context, GSS_C_NO_BUFFER
);
}
if (state->server_name != GSS_C_NO_NAME) {
state->maj_stat = gss_release_name(&state->min_stat, &state->server_name);
}
if (state->client_name != GSS_C_NO_NAME) {
state->maj_stat = gss_release_name(&state->min_stat, &state->client_name);
}
if (state->server_creds != GSS_C_NO_CREDENTIAL) {
state->maj_stat = gss_release_cred(&state->min_stat, &state->server_creds);
}
if (state->client_creds != GSS_C_NO_CREDENTIAL) {
state->maj_stat = gss_release_cred(&state->min_stat, &state->client_creds);
}
if (state->username != NULL) {
free(state->username);
state->username = NULL;
}
if (state->targetname != NULL) {
free(state->targetname);
state->targetname = NULL;
}
if (state->response != NULL) {
free(state->response);
state->response = NULL;
}
if (state->ccname != NULL) {
free(state->ccname);
state->ccname = NULL;
}
return ret;
}
int authenticate_gss_server_step(
gss_server_state *state, const char *challenge
) {
gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER;
gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER;
int ret = AUTH_GSS_CONTINUE;
// Always clear out the old response
if (state->response != NULL) {
free(state->response);
state->response = NULL;
}
// If there is a challenge (data from the server) we need to give it to GSS
if (challenge && *challenge) {
size_t len;
input_token.value = base64_decode(challenge, &len);
input_token.length = len;
} else {
// XXX No challenge parameter in request from client
// XXX How to pass error string to state?
ret = AUTH_GSS_ERROR;
goto end;
}
state->maj_stat = gss_accept_sec_context(
&state->min_stat,
&state->context,
state->server_creds,
&input_token,
GSS_C_NO_CHANNEL_BINDINGS,
&state->client_name,
NULL,
&output_token,
NULL,
NULL,
&state->client_creds
);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
// Grab the server response to send back to the client
if (output_token.length) {
state->response = base64_encode(
(const unsigned char *)output_token.value, output_token.length
);;
state->maj_stat = gss_release_buffer(&state->min_stat, &output_token);
}
// Get the user name
state->maj_stat = gss_display_name(
&state->min_stat, state->client_name, &output_token, NULL
);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
state->username = (char *)malloc(output_token.length + 1);
strncpy(state->username, (char*) output_token.value, output_token.length);
state->username[output_token.length] = 0;
// Get the target name if no server creds were supplied
if (state->server_creds == GSS_C_NO_CREDENTIAL) {
gss_name_t target_name = GSS_C_NO_NAME;
state->maj_stat = gss_inquire_context(
&state->min_stat, state->context, NULL, &target_name, NULL, NULL, NULL,
NULL, NULL
);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
state->maj_stat = gss_display_name(
&state->min_stat, target_name, &output_token, NULL
);
if (GSS_ERROR(state->maj_stat)) {
ret = AUTH_GSS_ERROR;
goto end;
}
state->targetname = (char *)malloc(output_token.length + 1);
strncpy(
state->targetname, (char*) output_token.value, output_token.length
);
state->targetname[output_token.length] = 0;
}
ret = AUTH_GSS_COMPLETE;
end:
if (output_token.length) {
gss_release_buffer(&state->min_stat, &output_token);
}
if (input_token.value) {
free(input_token.value);
}
return ret;
}
void get_gss_error(OM_uint32 err_maj, char *buf_maj, OM_uint32 err_min, char *buf_min)
{
OM_uint32 maj_stat, min_stat;
OM_uint32 msg_ctx = 0;
gss_buffer_desc status_string;
do {
maj_stat = gss_display_status(
&min_stat,
err_maj,
GSS_C_GSS_CODE,
GSS_C_NO_OID,
&msg_ctx,
&status_string
);
if (GSS_ERROR(maj_stat)) {
break;
}
strncpy(buf_maj, (char*) status_string.value, GSS_ERRBUF_SIZE);
gss_release_buffer(&min_stat, &status_string);
maj_stat = gss_display_status(
&min_stat,
err_min,
GSS_C_MECH_CODE,
GSS_C_NULL_OID,
&msg_ctx,
&status_string
);
if (! GSS_ERROR(maj_stat)) {
strncpy(buf_min, (char*) status_string.value, GSS_ERRBUF_SIZE);
gss_release_buffer(&min_stat, &status_string);
}
} while (!GSS_ERROR(maj_stat) && msg_ctx != 0);
}

86
vendor/github.com/ubccr/kerby/kerberosgss.h generated vendored Normal file
View file

@ -0,0 +1,86 @@
/**
* Adopted from PyKerberos. Modified for use with Kerby.
*
* Copyright (c) 2006-2015 Apple Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
#include <errno.h>
#include <unistd.h> // for close
#include <gssapi/gssapi.h>
#include <gssapi/gssapi_generic.h>
#include <gssapi/gssapi_krb5.h>
#define krb5_get_err_text(context,code) error_message(code)
#define AUTH_GSS_ERROR -1
#define AUTH_GSS_COMPLETE 1
#define AUTH_GSS_CONTINUE 0
#define GSS_AUTH_P_NONE 1
#define GSS_AUTH_P_INTEGRITY 2
#define GSS_AUTH_P_PRIVACY 4
#define GSS_ERRBUF_SIZE 512
typedef struct {
gss_ctx_id_t context;
gss_name_t server_name;
long int gss_flags;
gss_cred_id_t client_creds;
char* username;
char* response;
int responseConf;
OM_uint32 maj_stat;
OM_uint32 min_stat;
} gss_client_state;
typedef struct {
gss_ctx_id_t context;
gss_name_t server_name;
gss_name_t client_name;
gss_cred_id_t server_creds;
gss_cred_id_t client_creds;
char* username;
char* targetname;
char* response;
char* ccname;
OM_uint32 maj_stat;
OM_uint32 min_stat;
} gss_server_state;
gss_client_state* new_gss_client_state();
void free_gss_client_state(gss_client_state *state);
gss_server_state* new_gss_server_state();
void free_gss_server_state(gss_server_state *state);
void get_gss_error(OM_uint32 err_maj, char *buf_maj, OM_uint32 err_min, char *buf_min);
int authenticate_gss_client_init(
const char* service, const char* principal, long int gss_flags,
gss_server_state* delegatestate, gss_client_state* state
);
int authenticate_gss_client_clean(
gss_client_state *state
);
int authenticate_gss_client_step(
gss_client_state *state, const char *challenge
);
int authenticate_gss_server_init(
const char* service, gss_server_state* state
);
int authenticate_gss_server_clean(
gss_server_state *state
);
int authenticate_gss_server_step(
gss_server_state *state, const char *challenge
);

268
vendor/github.com/ubccr/kerby/kerby.go generated vendored Normal file
View file

@ -0,0 +1,268 @@
// Copyright 2015 Andrew E. Bruno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package kerby is a cgo wrapper for Kerberos GSSAPI
package kerby
/*
#cgo CFLAGS: -std=gnu99
#cgo LDFLAGS: -lgssapi_krb5 -lkrb5 -lk5crypto -lcom_err
#include "kerberosgss.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
*/
import "C"
import (
"errors"
"fmt"
"strings"
"unsafe"
)
// Kerberos GSSAPI Client
type KerbClient struct {
state *C.gss_client_state
}
// Kerberos GSSAPI Server
type KerbServer struct {
state *C.gss_server_state
}
// Returns the last major/minor GSSAPI error messages
func (kc KerbClient) GssError() error {
bufMaj := (*C.char)(C.calloc(C.GSS_ERRBUF_SIZE, 1))
bufMin := (*C.char)(C.calloc(C.GSS_ERRBUF_SIZE, 1))
defer C.free(unsafe.Pointer(bufMaj))
defer C.free(unsafe.Pointer(bufMin))
C.get_gss_error(kc.state.maj_stat, bufMaj, kc.state.min_stat, bufMin)
return errors.New(C.GoString(bufMaj) + " - " + C.GoString(bufMin))
}
// Initializes a context for Kerberos GSSAPI client-side authentication.
// KerbClient.Clean must be called after this function returns succesfully to
// dispose of the context once all GSSAPI operations are complete. srv is the
// service principal in the form "type@fqdn". princ is the client principal in the
// form "user@realm".
func (kc *KerbClient) Init(srv, princ string) error {
service := C.CString(srv)
defer C.free(unsafe.Pointer(service))
principal := C.CString(princ)
defer C.free(unsafe.Pointer(principal))
var delegatestate *C.gss_server_state
gss_flags := C.long(C.GSS_C_MUTUAL_FLAG | C.GSS_C_SEQUENCE_FLAG)
result := 0
kc.state = C.new_gss_client_state()
if kc.state == nil {
return errors.New("Failed to allocate memory for gss_client_state")
}
result = int(C.authenticate_gss_client_init(service, principal, gss_flags, delegatestate, kc.state))
if result == C.AUTH_GSS_ERROR {
return kc.GssError()
}
return nil
}
// Get the client response from the last successful GSSAPI client-side step.
func (kc *KerbClient) Response() string {
return C.GoString(kc.state.response)
}
// Processes a single GSSAPI client-side step using the supplied server data.
func (kc *KerbClient) Step(chlg string) error {
challenge := C.CString(chlg)
defer C.free(unsafe.Pointer(challenge))
result := 0
if kc.state == nil {
return errors.New("Invalid client state")
}
result = int(C.authenticate_gss_client_step(kc.state, challenge))
if result == C.AUTH_GSS_ERROR {
return kc.GssError()
}
return nil
}
// Destroys the context for GSSAPI client-side authentication. After this call
// the KerbClient.state object is invalid and should not be used again.
func (kc *KerbClient) Clean() {
if kc.state != nil {
C.authenticate_gss_client_clean(kc.state)
C.free_gss_client_state(kc.state)
kc.state = nil
}
}
// Returns the service principal for the server given a service type and
// hostname. Adopted from PyKerberos.
func ServerPrincipalDetails(service, hostname string) (string, error) {
var code C.krb5_error_code
var kcontext C.krb5_context
var kt C.krb5_keytab
var cursor C.krb5_kt_cursor
var entry C.krb5_keytab_entry
var pname *C.char
match := fmt.Sprintf("%s/%s@", service, hostname)
code = C.krb5_init_context(&kcontext)
if code != 0 {
return "", fmt.Errorf("Cannot initialize Kerberos5 context: %d", code)
}
code = C.krb5_kt_default(kcontext, &kt)
if code != 0 {
return "", fmt.Errorf("Cannot get default keytab: %d", int(code))
}
code = C.krb5_kt_start_seq_get(kcontext, kt, &cursor)
if code != 0 {
return "", fmt.Errorf("Cannot get sequence cursor from keytab: %d", int(code))
}
result := ""
for {
code = C.krb5_kt_next_entry(kcontext, kt, &entry, &cursor)
if code != 0 {
break
}
code = C.krb5_unparse_name(kcontext, entry.principal, &pname)
if code != 0 {
return "", fmt.Errorf("Cannot parse principal name from keytab: %d", int(code))
}
result = C.GoString(pname)
if strings.HasPrefix(result, match) {
C.krb5_free_unparsed_name(kcontext, pname)
C.krb5_free_keytab_entry_contents(kcontext, &entry)
break
}
result = ""
C.krb5_free_unparsed_name(kcontext, pname)
C.krb5_free_keytab_entry_contents(kcontext, &entry)
}
if len(result) == 0 {
return "", errors.New("Principal not found in keytab")
}
if cursor != nil {
C.krb5_kt_end_seq_get(kcontext, kt, &cursor)
}
if kt != nil {
C.krb5_kt_close(kcontext, kt)
}
C.krb5_free_context(kcontext)
return result, nil
}
// Returns the last major/minor GSSAPI error messages
func (ks KerbServer) GssError() error {
bufMaj := (*C.char)(C.calloc(C.GSS_ERRBUF_SIZE, 1))
bufMin := (*C.char)(C.calloc(C.GSS_ERRBUF_SIZE, 1))
defer C.free(unsafe.Pointer(bufMaj))
defer C.free(unsafe.Pointer(bufMin))
C.get_gss_error(ks.state.maj_stat, bufMaj, ks.state.min_stat, bufMin)
return errors.New(C.GoString(bufMaj) + " - " + C.GoString(bufMin))
}
// Initializes a context for GSSAPI server-side authentication with the given
// service principal. KerbServer.Clean must be called after this function
// returns succesfully to dispose of the context once all GSSAPI operations are
// complete. srv is the service principal in the form "type@fqdn".
func (ks *KerbServer) Init(srv string) error {
service := C.CString(srv)
defer C.free(unsafe.Pointer(service))
result := 0
ks.state = C.new_gss_server_state()
if ks.state == nil {
return errors.New("Failed to allocate memory for gss_server_state")
}
result = int(C.authenticate_gss_server_init(service, ks.state))
if result == C.AUTH_GSS_ERROR {
return ks.GssError()
}
return nil
}
// Get the user name of the principal trying to authenticate to the server.
// This method must only be called after KerbServer.Step returns a complete or
// continue response code.
func (ks *KerbServer) UserName() string {
return C.GoString(ks.state.username)
}
// Get the target name if the server did not supply its own credentials. This
// method must only be called after KerbServer.Step returns a complete or
// continue response code.
func (ks *KerbServer) TargetName() string {
return C.GoString(ks.state.targetname)
}
// Get the server response from the last successful GSSAPI server-side step.
func (ks *KerbServer) Response() string {
return C.GoString(ks.state.response)
}
// Processes a single GSSAPI server-side step using the supplied client data.
func (ks *KerbServer) Step(chlg string) error {
challenge := C.CString(chlg)
defer C.free(unsafe.Pointer(challenge))
result := 0
if ks.state == nil {
return errors.New("Invalid client state")
}
result = int(C.authenticate_gss_server_step(ks.state, challenge))
if result == C.AUTH_GSS_ERROR {
return ks.GssError()
}
return nil
}
// Destroys the context for GSSAPI server-side authentication. After this call
// the KerbServer.state object is invalid and should not be used again.
func (ks *KerbServer) Clean() {
if ks.state != nil {
C.authenticate_gss_server_clean(ks.state)
C.free_gss_server_state(ks.state)
ks.state = nil
}
}

39
vendor/github.com/ubccr/kerby/khttp/handler.go generated vendored Normal file
View file

@ -0,0 +1,39 @@
package khttp
import (
"fmt"
"github.com/ubccr/kerby"
"log"
"net/http"
"strings"
)
func Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authReq := strings.Split(r.Header.Get(authorizationHeader), " ")
if len(authReq) != 2 || authReq[0] != negotiateHeader {
w.Header().Set(wwwAuthenticateHeader, negotiateHeader)
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
return
}
ks := new(kerby.KerbServer)
err := ks.Init("")
if err != nil {
log.Printf("KerbServer Init Error: %s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer ks.Clean()
err = ks.Step(authReq[1])
if err != nil {
log.Printf("KerbServer Step Error: %s", err.Error())
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
w.Header().Set(wwwAuthenticateHeader, fmt.Sprintf("%s %s", negotiateHeader, ks.Response()))
h.ServeHTTP(w, r)
})
}

97
vendor/github.com/ubccr/kerby/khttp/http.go generated vendored Normal file
View file

@ -0,0 +1,97 @@
// Copyright 2015 Andrew E. Bruno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package khttp is a transport that authenticates all outgoing requests using
// SPNEGO (negotiate authentication) http://tools.ietf.org/html/rfc4559.
package khttp
import (
"errors"
"fmt"
"net"
"net/http"
"os"
"strings"
"github.com/ubccr/kerby"
)
var (
negotiateHeader = "Negotiate"
wwwAuthenticateHeader = "WWW-Authenticate"
authorizationHeader = "Authorization"
)
// HTTP client transport that authenticates all outgoing
// requests using SPNEGO. Implements the http.RoundTripper interface
type Transport struct {
// keytab file to use
KeyTab string
// principal
Principal string
// Next specifies the next transport to be used or http.DefaultTransport if nil.
Next http.RoundTripper
}
// RoundTrip executes a single HTTP transaction performing SPNEGO negotiate
// authentication.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
if len(t.KeyTab) > 0 {
os.Setenv("KRB5_CLIENT_KTNAME", t.KeyTab)
}
host, _, err := net.SplitHostPort(req.URL.Host)
if err != nil {
host = req.URL.Host
}
service := fmt.Sprintf("HTTP@%s", host)
kc := new(kerby.KerbClient)
err = kc.Init(service, t.Principal)
if err != nil {
return nil, err
}
defer kc.Clean()
err = kc.Step("")
if err != nil {
return nil, err
}
req.Header.Set(authorizationHeader, negotiateHeader+" "+kc.Response())
tr := t.Next
if tr == nil {
tr = http.DefaultTransport
if tr == nil {
return nil, errors.New("khttp: no Next transport or DefaultTransport")
}
}
resp, err := tr.RoundTrip(req)
if err != nil {
return nil, err
}
authReply := strings.Split(resp.Header.Get(wwwAuthenticateHeader), " ")
if len(authReply) != 2 || strings.ToLower(authReply[0]) != strings.ToLower(negotiateHeader) {
return nil, errors.New("khttp: server replied with invalid www-authenticate header")
}
// Authenticate the reply from the server
err = kc.Step(authReply[1])
if err != nil {
return nil, err
}
return resp, nil
}

3
vendor/modules.txt vendored
View file

@ -130,6 +130,9 @@ github.com/pmezard/go-difflib/difflib
github.com/stretchr/testify/assert github.com/stretchr/testify/assert
github.com/stretchr/testify/require github.com/stretchr/testify/require
github.com/stretchr/testify/suite github.com/stretchr/testify/suite
# github.com/ubccr/kerby v0.0.0-20170626144437-201a958fc453
github.com/ubccr/kerby
github.com/ubccr/kerby/khttp
# github.com/vmware/govmomi v0.23.0 # github.com/vmware/govmomi v0.23.0
github.com/vmware/govmomi/find github.com/vmware/govmomi/find
github.com/vmware/govmomi/govc/cli github.com/vmware/govmomi/govc/cli