diff --git a/.github/koji.conf b/.github/koji.conf index f5a14eb48..52e5e39de 100644 --- a/.github/koji.conf +++ b/.github/koji.conf @@ -66,3 +66,5 @@ plugins = runroot ; use the fast upload feature of koji by default use_fast_upload = yes + +serverca = /tmp/osbuild-composer-koji-test/ca-crt.pem diff --git a/.github/krb5.conf b/.github/krb5.conf new file mode 100644 index 000000000..ffafcfcf3 --- /dev/null +++ b/.github/krb5.conf @@ -0,0 +1,7 @@ +include /etc/krb5.conf + +[realms] + LOCAL = { + kdc = localhost + admin_server = localhost + } diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2daed1143..67c9922ae 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,6 +40,10 @@ jobs: - 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 + # 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 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. - name: Install koji client run: | - sudo apt-get install -y libkrb5-dev + sudo apt-get install -y libkrb5-dev krb5-config python -m pip install --upgrade pip pip install koji sudo cp .github/koji.conf /etc/koji.conf @@ -81,7 +85,7 @@ jobs: - name: Run unit tests run: | 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 - name: Send coverage to codecov.io diff --git a/cmd/osbuild-koji/main.go b/cmd/osbuild-koji/main.go index 08706e0e8..5a865a8c4 100644 --- a/cmd/osbuild-koji/main.go +++ b/cmd/osbuild-koji/main.go @@ -39,7 +39,7 @@ func main() { } defer file.Close() - k, err := koji.Login(server, "osbuild", "osbuildpass", http.DefaultTransport) + k, err := koji.NewFromPlain(server, "osbuild", "osbuildpass", http.DefaultTransport) if err != nil { println(err.Error()) return diff --git a/go.mod b/go.mod index 76154fed1..f33d11f67 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e github.com/pkg/errors v0.9.1 // indirect github.com/stretchr/testify v1.4.0 + github.com/ubccr/kerby v0.0.0-20170626144437-201a958fc453 github.com/vmware/govmomi v0.23.0 golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 diff --git a/go.sum b/go.sum index 6799ff050..d0a67648a 100644 --- a/go.sum +++ b/go.sum @@ -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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 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/go.mod h1:Y+Wq4lst78L85Ge/F8+ORXIWiKYqaro1vhAulACy9Lc= github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk= diff --git a/internal/upload/koji/koji.go b/internal/upload/koji/koji.go index 57cd9a1c8..e14ca6054 100644 --- a/internal/upload/koji/koji.go +++ b/internal/upload/koji/koji.go @@ -4,14 +4,17 @@ import ( "bytes" "crypto/md5" "encoding/json" + "errors" "fmt" "hash/adler32" "io" "io/ioutil" "net/http" "net/url" + "os" "github.com/kolo/xmlrpc" + "github.com/ubccr/kerby/khttp" ) type Koji struct { @@ -106,26 +109,17 @@ type CGImportResult struct { BuildID int `xmlrpc:"build_id"` } -func Login(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 - } +type GSSAPICredentials struct { + Principal string + KeyTab string +} - args := []interface{}{user, password} - var reply struct { - SessionID int64 `xmlrpc:"session-id"` - SessionKey string `xmlrpc:"session-key"` - } - err = loginClient.Call("login", args, &reply) - if err != nil { - return nil, err - } +type loginReply struct { + SessionID int64 `xmlrpc:"session-id"` + SessionKey string `xmlrpc:"session-key"` +} +func newKoji(server string, transport http.RoundTripper, reply loginReply) (*Koji, error) { // Create the final xmlrpc client with our custom RoundTripper handling // sessionID, sessionKey and callnum kojiTransport := &Transport{ @@ -134,7 +128,6 @@ func Login(server, user, password string, transport http.RoundTripper) (*Koji, e callnum: 0, transport: transport, } - client, err := xmlrpc.NewClient(server, kojiTransport) if err != nil { return nil, err @@ -147,6 +140,55 @@ func Login(server, user, password string, transport http.RoundTripper) (*Koji, e }, 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 func (k *Koji) GetAPIVersion() (int, error) { var version int @@ -309,3 +351,17 @@ func (rt *Transport) RoundTrip(req *http.Request) (*http.Response, error) { 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 +} diff --git a/internal/upload/koji/koji_test.go b/internal/upload/koji/koji_test.go index 8427f3f07..b179005c4 100644 --- a/internal/upload/koji/koji_test.go +++ b/internal/upload/koji/koji_test.go @@ -4,6 +4,8 @@ package koji_test import ( "crypto/rand" + "crypto/tls" + "crypto/x509" "io" "io/ioutil" "net/http" @@ -22,17 +24,36 @@ import ( func TestKojiImport(t *testing.T) { // define constants - server := "http://localhost:8080/kojihub" - user := "osbuild" - password := "osbuildpass" + server := "https://localhost/kojihub" filename := "image.qcow2" 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 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 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) defer func() { @@ -111,9 +132,9 @@ func TestKojiImport(t *testing.T) { cmd := exec.Command( "koji", "--server", server, - "--user", user, - "--password", password, - "--authtype", "password", + "-c", "../../../.github/koji.conf", + "--keytab", credentials.KeyTab, + "--principal", credentials.Principal, "list-builds", "--buildid", strconv.Itoa(result.BuildID), ) diff --git a/internal/upload/koji/run-koji-container.sh b/internal/upload/koji/run-koji-container.sh index 10f3a7679..8eca5772a 100755 --- a/internal/upload/koji/run-koji-container.sh +++ b/internal/upload/koji/run-koji-container.sh @@ -1,19 +1,27 @@ #!/bin/bash set -eu +SHARE_DIR=/tmp/osbuild-composer-koji-test + koji_stop () { echo "Shutting down containers, please wait..." ${CONTAINER_RUNTIME} stop 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} rm org.osbuild.koji.postgres || true ${CONTAINER_RUNTIME} network rm -f org.osbuild.koji || true + + rm -rf "${SHARE_DIR}" || true } koji_clean_up_bad_start () { + # remember the exit code, so we can report it later EXIT_CODE=$? echo "Start failed, removing containers." @@ -22,23 +30,80 @@ koji_clean_up_bad_start () { 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() { 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} run -d --name org.osbuild.koji.postgres --network org.osbuild.koji \ -e POSTGRES_USER=koji \ -e POSTGRES_PASSWORD=kojipass \ -e POSTGRES_DB=koji \ 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 \ - -p 8080:80 \ + -v "${SHARE_DIR}:/share:z" \ + -p 80:80 \ + -p 443:443 \ -e POSTGRES_USER=koji \ -e POSTGRES_PASSWORD=kojipass \ -e POSTGRES_DB=koji \ -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 "$0 stop" @@ -46,6 +111,7 @@ koji_start() { trap - EXIT } +# check arguments if [[ $# -ne 1 || ( "$1" != "start" && "$1" != "stop" ) ]]; then cat </dev/null >&2; then CONTAINER_RUNTIME=podman elif which docker 2>/dev/null >&2; then diff --git a/krb5.conf b/krb5.conf new file mode 100644 index 000000000..ffafcfcf3 --- /dev/null +++ b/krb5.conf @@ -0,0 +1,7 @@ +include /etc/krb5.conf + +[realms] + LOCAL = { + kdc = localhost + admin_server = localhost + } diff --git a/vendor/github.com/ubccr/kerby/.gitignore b/vendor/github.com/ubccr/kerby/.gitignore new file mode 100644 index 000000000..93b6ef823 --- /dev/null +++ b/vendor/github.com/ubccr/kerby/.gitignore @@ -0,0 +1 @@ +cmd/kerby/kerby diff --git a/vendor/github.com/ubccr/kerby/LICENSE b/vendor/github.com/ubccr/kerby/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/vendor/github.com/ubccr/kerby/LICENSE @@ -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. diff --git a/vendor/github.com/ubccr/kerby/README.rst b/vendor/github.com/ubccr/kerby/README.rst new file mode 100644 index 000000000..f2d499eb1 --- /dev/null +++ b/vendor/github.com/ubccr/kerby/README.rst @@ -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 + diff --git a/vendor/github.com/ubccr/kerby/base64.c b/vendor/github.com/ubccr/kerby/base64.c new file mode 100644 index 000000000..3e00af8a9 --- /dev/null +++ b/vendor/github.com/ubccr/kerby/base64.c @@ -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 +#include + +// 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; +} diff --git a/vendor/github.com/ubccr/kerby/base64.h b/vendor/github.com/ubccr/kerby/base64.h new file mode 100644 index 000000000..747f2602c --- /dev/null +++ b/vendor/github.com/ubccr/kerby/base64.h @@ -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 + +char *base64_encode(const unsigned char *value, size_t vlen); +unsigned char *base64_decode(const char *value, size_t *rlen); diff --git a/vendor/github.com/ubccr/kerby/kerberosgss.c b/vendor/github.com/ubccr/kerby/kerberosgss.c new file mode 100644 index 000000000..68eeb5c22 --- /dev/null +++ b/vendor/github.com/ubccr/kerby/kerberosgss.c @@ -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 +#include +#include + +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); +} + diff --git a/vendor/github.com/ubccr/kerby/kerberosgss.h b/vendor/github.com/ubccr/kerby/kerberosgss.h new file mode 100644 index 000000000..265e24bf3 --- /dev/null +++ b/vendor/github.com/ubccr/kerby/kerberosgss.h @@ -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 +#include // for close +#include +#include +#include + +#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 +); diff --git a/vendor/github.com/ubccr/kerby/kerby.go b/vendor/github.com/ubccr/kerby/kerby.go new file mode 100644 index 000000000..de30893d2 --- /dev/null +++ b/vendor/github.com/ubccr/kerby/kerby.go @@ -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 +#include +#include +*/ +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 + } +} diff --git a/vendor/github.com/ubccr/kerby/khttp/handler.go b/vendor/github.com/ubccr/kerby/khttp/handler.go new file mode 100644 index 000000000..ac1d95e20 --- /dev/null +++ b/vendor/github.com/ubccr/kerby/khttp/handler.go @@ -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) + }) +} diff --git a/vendor/github.com/ubccr/kerby/khttp/http.go b/vendor/github.com/ubccr/kerby/khttp/http.go new file mode 100644 index 000000000..79f2b7e25 --- /dev/null +++ b/vendor/github.com/ubccr/kerby/khttp/http.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 9eedd214b..24f14de48 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -130,6 +130,9 @@ github.com/pmezard/go-difflib/difflib github.com/stretchr/testify/assert github.com/stretchr/testify/require 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/find github.com/vmware/govmomi/govc/cli