├── cmd
├── keymaster
│ └── .gitignore
├── keymasterd
│ ├── .gitignore
│ ├── customization_data
│ │ ├── templates
│ │ │ ├── header_extra.tmpl
│ │ │ ├── login_extra.tmpl
│ │ │ └── footer_extra.tmpl
│ │ └── web_resources
│ │ │ └── customization.css
│ ├── static_files
│ │ ├── favicon.ico
│ │ ├── keymaster.css
│ │ ├── webui-2fa-symc-vip.js
│ │ ├── webui-2fa-u2f.js
│ │ └── keymaster-u2f.js
│ ├── config_test.go
│ ├── logFilter.go
│ ├── adminDashboard.go
│ ├── storage_test.go
│ ├── dependency_monitor.go
│ ├── jwt.go
│ ├── certgen_test.go
│ ├── auth_oauth2.go
│ ├── auth_oauth2_test.go
│ ├── idp_oidc_test.go
│ ├── dependency_monitor_test.go
│ └── 2fa_vip.go
├── keymaster-eventmond
│ ├── config.go
│ ├── process.go
│ └── main.go
└── keymaster-unlocker
│ └── main.go
├── .gitignore
├── templates
└── config_host_go
├── docs
└── keymaster-overview.png
├── lib
├── client
│ ├── net
│ │ └── api.go
│ ├── twofa
│ │ ├── vip
│ │ │ ├── api.go
│ │ │ └── vip.go
│ │ ├── u2f
│ │ │ ├── api.go
│ │ │ └── u2f.go
│ │ ├── api.go
│ │ ├── twofa_test.go
│ │ └── twofa.go
│ ├── config
│ │ ├── api.go
│ │ ├── config.go
│ │ └── config_test.go
│ └── util
│ │ ├── api.go
│ │ ├── util.go
│ │ └── util_test.go
├── constants
│ └── constants.go
├── webapi
│ └── v0
│ │ └── proto
│ │ └── api.go
├── pwauth
│ ├── api.go
│ ├── command
│ │ ├── impl.go
│ │ ├── command_test.go
│ │ └── api.go
│ ├── okta
│ │ ├── api.go
│ │ ├── impl.go
│ │ └── okta_test.go
│ └── ldap
│ │ ├── api.go
│ │ └── impl.go
├── simplestorage
│ ├── api.go
│ └── memstore
│ │ └── memstore.go
├── util
│ └── util.go
├── instrumentedwriter
│ ├── instrumentedWriter_test.go
│ └── instrumentedWriter.go
├── certgen
│ ├── iprestricted_test.go
│ └── iprestricted.go
└── vip
│ └── vip_test.go
├── misc
└── startup
│ ├── keymaster-eventmond.service
│ ├── keymaster.service
│ └── keymaster-eventmond.Debian-7
├── .travis.yml
├── eventmon
├── httpd
│ ├── api.go
│ ├── status.go
│ └── showActivity.go
├── monitord
│ └── api.go
└── eventrecorder
│ ├── api.go
│ └── impl.go
├── keymasterd
├── admincache
│ ├── api.go
│ ├── cache.go
│ └── cache_test.go
└── eventnotifier
│ ├── api.go
│ └── impl.go
├── proto
└── eventmon
│ └── messages.go
├── Makefile
└── keymaster.spec
/cmd/keymaster/.gitignore:
--------------------------------------------------------------------------------
1 | config_host.go
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.key
2 | *.pem
3 | config.yml
4 |
--------------------------------------------------------------------------------
/cmd/keymasterd/.gitignore:
--------------------------------------------------------------------------------
1 | cachedDB.sqlite3
2 | userProfiles.sqlite3
3 |
--------------------------------------------------------------------------------
/templates/config_host_go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | const defaultConfigHost = ""
4 |
--------------------------------------------------------------------------------
/docs/keymaster-overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Symantec/keymaster/HEAD/docs/keymaster-overview.png
--------------------------------------------------------------------------------
/cmd/keymasterd/customization_data/templates/header_extra.tmpl:
--------------------------------------------------------------------------------
1 | {{define "header_extra"}}
2 | Keymaster
3 | {{end}}
4 |
--------------------------------------------------------------------------------
/cmd/keymasterd/static_files/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Symantec/keymaster/HEAD/cmd/keymasterd/static_files/favicon.ico
--------------------------------------------------------------------------------
/cmd/keymasterd/customization_data/templates/login_extra.tmpl:
--------------------------------------------------------------------------------
1 | {{define "login_pre_password"}}
2 | {{end}}
3 |
4 | {{define "login_form_footer"}}
5 | {{end}}
6 |
--------------------------------------------------------------------------------
/lib/client/net/api.go:
--------------------------------------------------------------------------------
1 | package net
2 |
3 | import (
4 | "context"
5 | "net"
6 | )
7 |
8 | type Dialer interface {
9 | Dial(network, address string) (net.Conn, error)
10 | DialContext(ctx context.Context, network, address string) (net.Conn, error)
11 | }
12 |
--------------------------------------------------------------------------------
/cmd/keymasterd/customization_data/templates/footer_extra.tmpl:
--------------------------------------------------------------------------------
1 | {{define "footer_extra"}}
2 | | Design Doc | Source Code
3 | {{end}}
4 |
--------------------------------------------------------------------------------
/misc/startup/keymaster-eventmond.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Keymaster event monitor server
3 | After=network.target
4 |
5 | [Service]
6 | ExecStart=/usr/local/sbin/keymaster-eventmond
7 | ExecReload=/bin/kill -HUP $MAINPID
8 | Restart=always
9 | RestartSec=1
10 | User=nobody
11 |
12 | [Install]
13 | WantedBy=multi-user.target
14 |
--------------------------------------------------------------------------------
/lib/constants/constants.go:
--------------------------------------------------------------------------------
1 | package constants
2 |
3 | const (
4 | DefaultKeymasterAdminPortNumber = 6920
5 | DefaultKeymasterServicePortNumber = 443
6 |
7 | DefaultEventmonPortNumber = 6921
8 | DefaultKeymasterEventmonConfigFile = "/etc/keymaster-eventmond/config.yml"
9 | DefaultKeymasterEventmonStateDir = "/var/lib/keymaster-eventmond"
10 | )
11 |
--------------------------------------------------------------------------------
/cmd/keymasterd/static_files/keymaster.css:
--------------------------------------------------------------------------------
1 |
2 | body{
3 | padding:0;
4 | border:0;
5 | margin:0;
6 | height:100%;
7 | font-family: "Droid Sans", sans-serif;
8 | color: #212424;
9 | background-color: #f4f4f4;
10 | }
11 |
12 | h1,h2,h3{
13 | line-height:1.2;
14 | }
15 |
16 | .bodyContainer {
17 | }
18 |
19 | @media print{body{max-width:none}}
20 |
--------------------------------------------------------------------------------
/misc/startup/keymaster.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Keymaster cert generation service
3 | After=network.target
4 |
5 | [Service]
6 | PermissionsStartOnly=true
7 | ExecStartPre=/usr/sbin/setcap cap_net_bind_service=+ep /usr/sbin/keymasterd
8 | ExecStart=/usr/sbin/keymasterd -config /etc/keymaster/server_config.yml
9 | Restart=always
10 | RestartSec=20
11 | User=keymaster
12 | Group=keymaster
13 |
14 | [Install]
15 | WantedBy=multi-user.target
16 |
--------------------------------------------------------------------------------
/lib/webapi/v0/proto/api.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | const LoginPath = "/api/v0/login"
4 |
5 | const (
6 | AuthTypePassword = "password"
7 | AuthTypeFederated = "federated"
8 | AuthTypeU2F = "U2F"
9 | AuthTypeSymantecVIP = "SymantecVIP"
10 | AuthTypeIPCertificate = "IPCertificate"
11 | AuthTypeTOTP = "TOTP"
12 | )
13 |
14 | type LoginResponse struct {
15 | Message string `json:"message"`
16 | CertAuthBackend []string `json:"auth_backend"`
17 | }
18 |
--------------------------------------------------------------------------------
/lib/client/twofa/vip/api.go:
--------------------------------------------------------------------------------
1 | // Package vip does two factor authentication with Symantec VIP
2 | package vip
3 |
4 | import (
5 | "net/http"
6 |
7 | "github.com/Symantec/Dominator/lib/log"
8 | )
9 |
10 | // DoVIPAuthenticate performs two factor authentication with Symantec VIP
11 | func DoVIPAuthenticate(
12 | client *http.Client,
13 | baseURL string,
14 | userAgentString string,
15 | logger log.DebugLogger) error {
16 | return doVIPAuthenticate(client, baseURL, userAgentString, logger)
17 | }
18 |
--------------------------------------------------------------------------------
/lib/pwauth/api.go:
--------------------------------------------------------------------------------
1 | package pwauth
2 |
3 | import (
4 | "github.com/Symantec/keymaster/lib/simplestorage"
5 | )
6 |
7 | // PasswordAuthenticator is an interface type that defines how to authenticate a
8 | // user with a username and password.
9 | type PasswordAuthenticator interface {
10 | // PasswordAuthenticate will authenticate a user using the provided username
11 | // and password. It returns whether the authentication succeeded and an
12 | // error.
13 | PasswordAuthenticate(username string, password []byte) (bool, error)
14 | UpdateStorage(storage simplestorage.SimpleStore) error
15 | }
16 |
--------------------------------------------------------------------------------
/cmd/keymasterd/customization_data/web_resources/customization.css:
--------------------------------------------------------------------------------
1 | .header {
2 | font-size: 95%;
3 | height:35px;
4 | color: #f4f4f4;
5 | background-color: #213c60; /*Symantec yelllow: #FDBB30*/
6 | width:100%;
7 | }
8 |
9 | .header a:link {
10 | font-size: 85%;
11 | color: #FDBB30
12 | }
13 | .header a:visited {
14 | font-size: 85%;
15 | color: #FDBB30
16 | }
17 |
18 | .header_extra{
19 | font-size: 140%;
20 | padding-left: 1.2em;
21 | line-height:normal;
22 | }
23 |
24 | .footer {
25 | font-size: 95%;
26 | height:60px;
27 | position:absolute;
28 | width:100%;
29 | bottom:0;
30 | }
31 |
--------------------------------------------------------------------------------
/lib/client/twofa/u2f/api.go:
--------------------------------------------------------------------------------
1 | // Package twofa contains routines for getting short lived certificate.
2 | package u2f
3 |
4 | import (
5 | "net/http"
6 |
7 | "github.com/Symantec/Dominator/lib/log"
8 | )
9 |
10 | // CheckU2FDevices checks the U2F devices and terminates the application by
11 | // calling Fatal on the passed logger if the U2F devices cannot be read.
12 | func CheckU2FDevices(logger log.Logger) {
13 | checkU2FDevices(logger)
14 | }
15 |
16 | // DoU2FAuthenticate does U2F authentication
17 | func DoU2FAuthenticate(
18 | client *http.Client,
19 | baseURL string,
20 | userAgentString string,
21 | logger log.DebugLogger) error {
22 | return doU2FAuthenticate(client, baseURL, userAgentString, logger)
23 | }
24 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | # github.com/flynn/hid is broken on go1.12 on MacOS X, so pin to go1.11.x
4 | go:
5 | - 1.13.x
6 |
7 | os:
8 | - linux
9 | - osx
10 |
11 | before_install:
12 | - go get github.com/mattn/goveralls
13 | - REPO_NAME=$(basename $PWD)
14 | - GITHUB_PATH=$(dirname $(dirname $PWD))
15 | - SYMANTEC_PROJECT_DIR=${GITHUB_PATH}/Symantec/${REPO_NAME}
16 | - mkdir -p ${SYMANTEC_PROJECT_DIR}
17 | - rsync -az ${TRAVIS_BUILD_DIR}/ ${SYMANTEC_PROJECT_DIR}/
18 | - export TRAVIS_BUILD_DIR=${SYMANTEC_PROJECT_DIR}
19 | - cd ${SYMANTEC_PROJECT_DIR}
20 |
21 | install: make get-deps
22 |
23 | script:
24 | - go test -v -covermode=count -coverprofile=coverage.out ./...
25 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci
26 | - make all test
27 |
--------------------------------------------------------------------------------
/lib/simplestorage/api.go:
--------------------------------------------------------------------------------
1 | package simplestorage
2 |
3 | // Simple is an interface type that defines how to store, retrieve and
4 | // delete simple strings. Values are uniquely defined by the key and the type
5 | // The data MUST be signed if stored out of the memory space of the running process.
6 | // The data however is not required to be encrypted.
7 | type SimpleStore interface {
8 | // Upsert will insert or update the data and expiration of a string
9 | UpsertSigned(key string, dataType int, expiration int64, data string) error
10 | // Deletes a value from the storage service
11 | DeleteSigned(key string, dataType int) error
12 | // Gets the value from the storage service if the value exists and
13 | // is not expired will return true, the value and nill.
14 | // if the value does not exist or is expired will return false, empty string
15 | // and nil. Any other case is an error.
16 | GetSigned(key string, dataType int) (bool, string, error)
17 | }
18 |
--------------------------------------------------------------------------------
/lib/pwauth/command/impl.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "bytes"
5 | "os/exec"
6 | "syscall"
7 |
8 | "github.com/Symantec/Dominator/lib/log"
9 | )
10 |
11 | func newAuthenticator(command string, args []string, logger log.Logger) (
12 | *PasswordAuthenticator, error) {
13 | command, err := exec.LookPath(command)
14 | if err != nil {
15 | return nil, err
16 | }
17 | return &PasswordAuthenticator{command, args, logger}, nil
18 | }
19 |
20 | func (pa *PasswordAuthenticator) passwordAuthenticate(username string,
21 | password []byte) (bool, error) {
22 | args := []string{username}
23 | args = append(args, pa.args...)
24 | cmd := exec.Command(pa.command, args...)
25 | cmd.Stdin = bytes.NewReader(password)
26 | if _, err := cmd.Output(); err != nil {
27 | if e, ok := err.(*exec.ExitError); ok {
28 | if e.Exited() && e.Sys().(syscall.WaitStatus).ExitStatus() == 1 {
29 | return false, nil
30 | }
31 | }
32 | pa.logger.Println(err)
33 | return false, err
34 | }
35 | return true, nil
36 | }
37 |
--------------------------------------------------------------------------------
/cmd/keymasterd/config_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "io/ioutil"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 | "testing"
10 | )
11 |
12 | func TestGenerateNewConfigInternal(t *testing.T) {
13 | t.Logf("hello")
14 | dir, err := ioutil.TempDir("", "config_testing")
15 | if err != nil {
16 | t.Fatal(err)
17 | }
18 | defer os.RemoveAll(dir) // clean up
19 | configFilename := filepath.Join(dir, "config-test.yml")
20 |
21 | readerContent := dir + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
22 | baseReader := strings.NewReader(readerContent)
23 | reader := bufio.NewReader(baseReader)
24 | passphrase := []byte("passphrase")
25 | err = generateNewConfigInternal(reader, configFilename, 2048, passphrase)
26 | if err != nil {
27 | t.Fatal(err)
28 | }
29 | datapath := filepath.Join(dir, "var/lib/keymaster")
30 | err = os.MkdirAll(datapath, 0750)
31 | if err != nil {
32 | t.Fatal(err)
33 | }
34 | // AND not try to load
35 | _, err = loadVerifyConfigFile(configFilename)
36 | if err != nil {
37 | t.Fatal(err)
38 | }
39 |
40 | // TODO: test decrypt file
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/eventmon/httpd/api.go:
--------------------------------------------------------------------------------
1 | package httpd
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net"
7 | "net/http"
8 |
9 | "github.com/Symantec/keymaster/eventmon/eventrecorder"
10 | "github.com/Symantec/keymaster/eventmon/monitord"
11 | )
12 |
13 | type HtmlWriter interface {
14 | WriteHtml(writer io.Writer)
15 | }
16 |
17 | var htmlWriters []HtmlWriter
18 |
19 | type state struct {
20 | eventRecorder *eventrecorder.EventRecorder
21 | monitor *monitord.Monitor
22 | }
23 |
24 | func StartServer(portNum uint, eventRecorder *eventrecorder.EventRecorder,
25 | monitor *monitord.Monitor, daemon bool) error {
26 | listener, err := net.Listen("tcp", fmt.Sprintf(":%d", portNum))
27 | if err != nil {
28 | return err
29 | }
30 | myState := state{eventRecorder, monitor}
31 | http.HandleFunc("/", myState.statusHandler)
32 | http.HandleFunc("/showActivity", myState.showActivityHandler)
33 | if daemon {
34 | go http.Serve(listener, nil)
35 | } else {
36 | http.Serve(listener, nil)
37 | }
38 | return nil
39 | }
40 |
41 | func AddHtmlWriter(htmlWriter HtmlWriter) {
42 | htmlWriters = append(htmlWriters, htmlWriter)
43 | }
44 |
--------------------------------------------------------------------------------
/cmd/keymaster-eventmond/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io/ioutil"
5 | "text/template"
6 |
7 | "github.com/Symantec/keymaster/lib/constants"
8 | "gopkg.in/yaml.v2"
9 | )
10 |
11 | func loadConfig(filename string) (*configurationType, error) {
12 | rawConfig, err := ioutil.ReadFile(filename)
13 | if err != nil {
14 | return nil, err
15 | }
16 | config := &configurationType{
17 | KeymasterServerPortNum: constants.DefaultKeymasterAdminPortNumber,
18 | }
19 | if err := yaml.Unmarshal(rawConfig, config); err != nil {
20 | return nil, err
21 | }
22 | if err := parseCommand(&config.SshCertParametersCommand); err != nil {
23 | return nil, err
24 | }
25 | if err := parseCommand(&config.X509CertParametersCommand); err != nil {
26 | return nil, err
27 | }
28 | return config, nil
29 | }
30 |
31 | func parseCommand(command *certCommand) error {
32 | for _, parameter := range command.Parameters {
33 | templ := template.New("")
34 | templ, err := templ.Parse(parameter)
35 | if err != nil {
36 | return err
37 | }
38 | command.templates = append(command.templates, templ)
39 | }
40 | return nil
41 | }
42 |
--------------------------------------------------------------------------------
/keymasterd/admincache/api.go:
--------------------------------------------------------------------------------
1 | // Package admin cache caches admin credentials
2 | package admincache
3 |
4 | import (
5 | "sync"
6 | "time"
7 | )
8 |
9 | // Cache caches admin credentials
10 | type Cache struct {
11 | clock clock
12 | maxDuration time.Duration
13 | mu sync.Mutex
14 | data map[string]cacheEntry
15 | }
16 |
17 | // New creates a new cache that expires entries older than maxDuration.
18 | func New(maxDuration time.Duration) *Cache {
19 | return newForTesting(maxDuration, kSystemClock)
20 | }
21 |
22 | // Get returns cached admin credentials for given user. isAdmin is true
23 | // if user has admin credentials. valid is true if cache entry for user has
24 | // not expired. Initially, Get returns false, false for all users.
25 | // If c is nil, Get always returns false, false
26 | func (c *Cache) Get(user string) (isAdmin, valid bool) {
27 | return c.get(user)
28 | }
29 |
30 | // Put replaces admin credentials for given user. Admin credentials for user
31 | // will remain valid for maxDuration time. If c is nil, Put is a no-op.
32 | func (c *Cache) Put(user string, isAdmin bool) {
33 | c.put(user, isAdmin)
34 | }
35 |
--------------------------------------------------------------------------------
/lib/client/config/api.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/Symantec/Dominator/lib/log"
7 | )
8 |
9 | type BaseConfig struct {
10 | Gen_Cert_URLS string `yaml:"gen_cert_urls"`
11 | Username string `yaml:"username"`
12 | FilePrefix string `yaml:"file_prefix"`
13 | AddGroups bool `yaml:"add_groups"`
14 | }
15 |
16 | // AppConfigFile represents a keymaster client configuration file
17 | type AppConfigFile struct {
18 | Base BaseConfig
19 | }
20 |
21 | // LoadVerifyConfigFile reads, verifies, and returns the contents of
22 | // a keymaster configuration file. LoadVerifyConfigFile returns an error if the
23 | // configuration file is invalid.
24 | func LoadVerifyConfigFile(configFilename string) (AppConfigFile, error) {
25 | return loadVerifyConfigFile(configFilename)
26 | }
27 |
28 | // GetConfigFromHost grabs a default config file from a given host and stores
29 | // it in the local file system.
30 | func GetConfigFromHost(
31 | configFilename string,
32 | hostname string,
33 | client *http.Client,
34 | logger log.Logger) error {
35 | return getConfigFromHost(configFilename, hostname, client, logger)
36 | }
37 |
--------------------------------------------------------------------------------
/proto/eventmon/messages.go:
--------------------------------------------------------------------------------
1 | package eventmon
2 |
3 | const (
4 | ConnectString = "200 Connected to keymaster eventmon service"
5 | HttpPath = "/eventmon/v0"
6 |
7 | AuthTypePassword = "Password"
8 | AuthTypeSymantecVIP = "SymantecVIP"
9 | AuthTypeU2F = "U2F"
10 |
11 | EventTypeAuth = "Auth"
12 | EventTypeServiceProviderLogin = "ServiceProviderLogin"
13 | EventTypeSSHCert = "SSHCert"
14 | EventTypeWebLogin = "WebLogin"
15 | EventTypeX509Cert = "X509Cert"
16 |
17 | VIPAuthTypeOTP = "VIPAuthOTP"
18 | VIPAuthTypePush = "VIPAuthPush"
19 | )
20 |
21 | // Client sends no data. Server sends a sequence of events.
22 |
23 | type EventV0 struct {
24 | Type string
25 |
26 | // Present for SSH and X509 certificate events.
27 | CertData []byte `json:",omitempty"`
28 |
29 | AuthType string `json:",omitempty"` // Present for Auth events.
30 | ServiceProviderUrl string `json:",omitempty"` // Present for SPLogin events.
31 | Username string `json:",omitempty"` // Auth, SPLogin and WebLogin
32 |
33 | VIPAuthType string `json:",omitempty"` // Present for VIP Auth events.
34 | }
35 |
--------------------------------------------------------------------------------
/keymasterd/admincache/cache.go:
--------------------------------------------------------------------------------
1 | package admincache
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type clock interface {
8 | Now() time.Time
9 | }
10 |
11 | type systemClockType struct{}
12 |
13 | func (s systemClockType) Now() time.Time {
14 | return time.Now()
15 | }
16 |
17 | var (
18 | kSystemClock systemClockType
19 | )
20 |
21 | type cacheEntry struct {
22 | IsAdmin bool
23 | Ts time.Time
24 | }
25 |
26 | func newForTesting(maxDuration time.Duration, clock clock) *Cache {
27 | return &Cache{
28 | data: make(map[string]cacheEntry),
29 | clock: clock,
30 | maxDuration: maxDuration}
31 | }
32 |
33 | func (c *Cache) get(user string) (isAdmin, valid bool) {
34 | if c == nil {
35 | return false, false
36 | }
37 | c.mu.Lock()
38 | defer c.mu.Unlock()
39 | entry := c.data[user]
40 | return entry.IsAdmin, c.isValid(entry.Ts)
41 | }
42 |
43 | func (c *Cache) put(user string, isAdmin bool) {
44 | if c == nil {
45 | return
46 | }
47 | c.mu.Lock()
48 | defer c.mu.Unlock()
49 | c.data[user] = cacheEntry{IsAdmin: isAdmin, Ts: c.clock.Now()}
50 | }
51 |
52 | func (c *Cache) isValid(ts time.Time) bool {
53 | if ts.IsZero() {
54 | return false
55 | }
56 | return c.clock.Now().Sub(ts) < c.maxDuration
57 | }
58 |
--------------------------------------------------------------------------------
/lib/pwauth/okta/api.go:
--------------------------------------------------------------------------------
1 | package okta
2 |
3 | import (
4 | "github.com/Symantec/Dominator/lib/log"
5 | "github.com/Symantec/keymaster/lib/simplestorage"
6 | )
7 |
8 | type PasswordAuthenticator struct {
9 | authnURL string
10 | logger log.Logger
11 | }
12 |
13 | // New creates a new PasswordAuthenticator using Okta as the backend. The Okta
14 | // Public Application API is used, so rate limits apply.
15 | // The Okta domain to check must be given by oktaDomain.
16 | // Log messages are written to logger. A new *PasswordAuthenticator is returned.
17 | func NewPublic(oktaDomain string, logger log.Logger) (
18 | *PasswordAuthenticator, error) {
19 | return newPublicAuthenticator(oktaDomain, logger)
20 | }
21 |
22 | // PasswordAuthenticate will authenticate a user using the provided username and
23 | // password.
24 | // It returns true if the user is authenticated, else false (due to either
25 | // invalid username or incorrect password), and an error.
26 | func (pa *PasswordAuthenticator) PasswordAuthenticate(username string,
27 | password []byte) (bool, error) {
28 | return pa.passwordAuthenticate(username, password)
29 | }
30 |
31 | func (pa *PasswordAuthenticator) UpdateStorage(storage simplestorage.SimpleStore) error {
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/lib/client/twofa/api.go:
--------------------------------------------------------------------------------
1 | // Package twofa contains routines for getting short lived certificate.
2 | package twofa
3 |
4 | import (
5 | "crypto"
6 | "flag"
7 | "net/http"
8 | "time"
9 |
10 | "github.com/Symantec/Dominator/lib/log"
11 | )
12 |
13 | var (
14 | // Duration of generated cert. Default 16 hours.
15 | Duration = flag.Duration("duration", 16*time.Hour, "Duration of the requested certificates in golang duration format (ex: 30s, 5m, 12h)")
16 | // If set, Do not use U2F as second factor
17 | noU2F = flag.Bool("noU2F", false, "Don't use U2F as second factor")
18 | // If set, Do not use VIPAccess as second factor.
19 | noVIPAccess = flag.Bool("noVIPAccess", false, "Don't use VIPAccess as second factor")
20 | )
21 |
22 | // GetCertFromTargetUrls gets a signed cert from the given target URLs.
23 | func GetCertFromTargetUrls(
24 | signer crypto.Signer,
25 | userName string,
26 | password []byte,
27 | targetUrls []string,
28 | skipu2f bool,
29 | addGroups bool,
30 | client *http.Client,
31 | userAgentString string,
32 | logger log.DebugLogger) (sshCert []byte, x509Cert []byte, kubernetesCert []byte, err error) {
33 | return getCertFromTargetUrls(
34 | signer, userName, password, targetUrls, skipu2f, addGroups,
35 | client, userAgentString, logger)
36 | }
37 |
--------------------------------------------------------------------------------
/cmd/keymaster-eventmond/process.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "crypto/x509"
6 | "os"
7 | "os/exec"
8 |
9 | "golang.org/x/crypto/ssh"
10 | )
11 |
12 | func processRawCert(command string, cert []byte) error {
13 | cmd := exec.Command(command)
14 | cmd.Stdin = bytes.NewBuffer(cert)
15 | cmd.Stdout = os.Stdout
16 | cmd.Stderr = os.Stderr
17 | return cmd.Run()
18 | }
19 |
20 | func (c certCommand) processSshCert(cert *ssh.Certificate) error {
21 | args := make([]string, 0, len(c.Parameters))
22 | for _, template := range c.templates {
23 | buffer := &bytes.Buffer{}
24 | if err := template.Execute(buffer, cert); err != nil {
25 | return err
26 | }
27 | args = append(args, buffer.String())
28 | }
29 | cmd := exec.Command(c.Command, args...)
30 | cmd.Stdout = os.Stdout
31 | cmd.Stderr = os.Stderr
32 | return cmd.Run()
33 | }
34 |
35 | func (c certCommand) processX509Cert(cert *x509.Certificate) error {
36 | args := make([]string, 0, len(c.Parameters))
37 | for _, template := range c.templates {
38 | buffer := &bytes.Buffer{}
39 | if err := template.Execute(buffer, cert); err != nil {
40 | return err
41 | }
42 | args = append(args, buffer.String())
43 | }
44 | cmd := exec.Command(c.Command, args...)
45 | cmd.Stdout = os.Stdout
46 | cmd.Stderr = os.Stderr
47 | return cmd.Run()
48 | }
49 |
--------------------------------------------------------------------------------
/eventmon/httpd/status.go:
--------------------------------------------------------------------------------
1 | package httpd
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io"
7 | "net/http"
8 |
9 | "github.com/Symantec/Dominator/lib/html"
10 | )
11 |
12 | func (s state) statusHandler(w http.ResponseWriter, req *http.Request) {
13 | if req.URL.Path != "/" {
14 | http.NotFound(w, req)
15 | return
16 | }
17 | writer := bufio.NewWriter(w)
18 | defer writer.Flush()
19 | fmt.Fprintln(writer, "
keymaster-eventmond status page")
20 | fmt.Fprintln(writer, ``)
25 | fmt.Fprintln(writer, "")
26 | fmt.Fprintln(writer, "")
27 | fmt.Fprintln(writer, "keymaster-eventmond status page
")
28 | fmt.Fprintln(writer, "")
29 | html.WriteHeaderWithRequest(writer, req)
30 | fmt.Fprintln(writer, "")
31 | s.writeDashboard(writer)
32 | for _, htmlWriter := range htmlWriters {
33 | htmlWriter.WriteHtml(writer)
34 | }
35 | fmt.Fprintln(writer, "
")
36 | fmt.Fprintln(writer, "
")
37 | html.WriteFooter(writer)
38 | fmt.Fprintln(writer, "")
39 | }
40 |
41 | func (s state) writeDashboard(writer io.Writer) {
42 | fmt.Fprintln(writer, `Show activity
`)
43 | }
44 |
--------------------------------------------------------------------------------
/lib/simplestorage/memstore/memstore.go:
--------------------------------------------------------------------------------
1 | package memstore
2 |
3 | // This is a demo package.. please dont use except for testing of the
4 | // consumers of the SimpleStpre interface
5 |
6 | import (
7 | "time"
8 | )
9 |
10 | type Index struct {
11 | Key string
12 | DataType int
13 | }
14 |
15 | type MemDatum struct {
16 | Data string
17 | Expiration int64
18 | }
19 |
20 | type MemStore struct {
21 | mstore map[Index]MemDatum
22 | }
23 |
24 | func New() *MemStore {
25 | var mstore MemStore
26 | mstore.mstore = make(map[Index]MemDatum)
27 | return &mstore
28 | }
29 |
30 | func (ms *MemStore) UpsertSigned(key string, dataType int, expiration int64, data string) error {
31 | datum := MemDatum{Data: data, Expiration: expiration}
32 | index := Index{Key: key, DataType: dataType}
33 | ms.mstore[index] = datum
34 | return nil
35 | }
36 | func (ms *MemStore) DeleteSigned(key string, dataType int) error {
37 | index := Index{Key: key, DataType: dataType}
38 | delete(ms.mstore, index)
39 | return nil
40 | }
41 | func (ms *MemStore) GetSigned(key string, dataType int) (bool, string, error) {
42 | index := Index{Key: key, DataType: dataType}
43 | datum, ok := ms.mstore[index]
44 | if !ok {
45 | return false, "", nil
46 | }
47 | if datum.Expiration < time.Now().Unix() {
48 | delete(ms.mstore, index)
49 | return false, "", nil
50 | }
51 | return true, datum.Data, nil
52 | }
53 |
--------------------------------------------------------------------------------
/keymasterd/eventnotifier/api.go:
--------------------------------------------------------------------------------
1 | package eventnotifier
2 |
3 | import (
4 | "net/http"
5 | "sync"
6 |
7 | "github.com/Symantec/Dominator/lib/log"
8 | "github.com/Symantec/keymaster/proto/eventmon"
9 | )
10 |
11 | type EventNotifier struct {
12 | logger log.DebugLogger
13 | mutex sync.Mutex
14 | // Protected by lock.
15 | transmitChannels map[chan<- eventmon.EventV0]chan<- eventmon.EventV0
16 | }
17 |
18 | func New(logger log.DebugLogger) *EventNotifier {
19 | return newEventNotifier(logger)
20 | }
21 |
22 | func (n *EventNotifier) PublishAuthEvent(authType, username string) {
23 | n.publishAuthEvent(authType, username)
24 | }
25 |
26 | func (n *EventNotifier) PublishServiceProviderLoginEvent(url, username string) {
27 | n.publishServiceProviderLoginEvent(url, username)
28 | }
29 |
30 | func (n *EventNotifier) PublishSSH(cert []byte) {
31 | n.publishCert(eventmon.EventTypeSSHCert, cert)
32 | }
33 |
34 | func (n *EventNotifier) PublishWebLoginEvent(username string) {
35 | n.publishWebLoginEvent(username)
36 | }
37 |
38 | func (n *EventNotifier) PublishVIPAuthEvent(vipAuthType, username string) {
39 | n.publishVIPAuthEvent(vipAuthType, username)
40 | }
41 |
42 | func (n *EventNotifier) PublishX509(cert []byte) {
43 | n.publishCert(eventmon.EventTypeX509Cert, cert)
44 | }
45 |
46 | func (n *EventNotifier) ServeHTTP(w http.ResponseWriter, req *http.Request) {
47 | n.serveHTTP(w, req)
48 | }
49 |
--------------------------------------------------------------------------------
/cmd/keymasterd/logFilter.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | )
7 |
8 | type logFilterType struct {
9 | handler http.Handler
10 | publicLogs bool
11 | }
12 |
13 | func NewLogFilterHandler(handler http.Handler, disableFilter bool) http.Handler {
14 | return &logFilterType{
15 | handler: handler,
16 | publicLogs: disableFilter,
17 | }
18 | }
19 |
20 | func getValidAdminRemoteUsername(w http.ResponseWriter,
21 | r *http.Request) (string, error) {
22 | if r.TLS != nil {
23 | logger.Debugf(4, "request is TLS %+v", r.TLS)
24 | if len(r.TLS.VerifiedChains) > 0 {
25 | logger.Debugf(4, "%+v", r.TLS.VerifiedChains[0][0].Subject)
26 | clientName := r.TLS.VerifiedChains[0][0].Subject.CommonName
27 | if clientName != "" {
28 | clientName = r.TLS.VerifiedChains[0][0].Subject.String()
29 | }
30 | return clientName, nil
31 | }
32 | }
33 | return "", nil
34 | }
35 |
36 | func (h *logFilterType) ServeHTTP(w http.ResponseWriter,
37 | req *http.Request) {
38 | if strings.HasPrefix(req.URL.Path, "/logs") {
39 | if !h.publicLogs {
40 | username, err := getValidAdminRemoteUsername(w, req)
41 | if err != nil {
42 | http.Error(w, "Check auth Failed", http.StatusInternalServerError)
43 | return
44 | }
45 | if username == "" {
46 | http.Error(w, "Invalid/Unknown Authentication", http.StatusUnauthorized)
47 | return
48 | }
49 | }
50 | }
51 |
52 | h.handler.ServeHTTP(w, req)
53 | }
54 |
--------------------------------------------------------------------------------
/lib/client/util/api.go:
--------------------------------------------------------------------------------
1 | // Package configutil contains utility routines for the keymaster client.
2 | package util
3 |
4 | import (
5 | "crypto"
6 | "crypto/rand"
7 | "crypto/rsa"
8 | "crypto/tls"
9 | "net/http"
10 | "os/user"
11 |
12 | "github.com/Symantec/Dominator/lib/log"
13 | "github.com/Symantec/keymaster/lib/client/net"
14 | )
15 |
16 | // GetUserCreds prompts the user for thier password and returns it.
17 | func GetUserCreds(userName string) (password []byte, err error) {
18 | return getUserCreds(userName)
19 | }
20 |
21 | // GetUserHomeDir returns the user's home directory.
22 | func GetUserHomeDir(usr *user.User) (string, error) {
23 | // TODO: verify on Windows... see: http://stackoverflow.com/questions/7922270/obtain-users-home-directory
24 | return usr.HomeDir, nil
25 | }
26 |
27 | // GenKeyPair uses internal golang functions to be portable
28 | func GenKeyPair(
29 | privateKeyPath string, identity string, logger log.Logger) (
30 | privateKey crypto.Signer, publicKeyPath string, err error) {
31 | return genKeyPair(privateKeyPath, identity, logger)
32 | }
33 |
34 | // GetHttpClient returns an http client instance to use given a
35 | // particular TLS configuration.
36 | func GetHttpClient(tlsConfig *tls.Config,
37 | dialer net.Dialer) (*http.Client, error) {
38 | return getHttpClient(tlsConfig, dialer)
39 | }
40 |
41 | // GenerateKey generates a random 2048 byte rsa key
42 | func GenerateKey() (*rsa.PrivateKey, error) {
43 | return rsa.GenerateKey(rand.Reader, rsaKeySize)
44 | }
45 |
--------------------------------------------------------------------------------
/lib/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "log"
7 | "mime/multipart"
8 | "net/http"
9 | "strings"
10 | )
11 |
12 | func CreateSimpleDataBodyRequest(method string, urlStr string, filebytes []byte, contentType string) (*http.Request, error) {
13 | bodyBuf := bytes.NewBuffer(filebytes)
14 | req, err := http.NewRequest(method, urlStr, bodyBuf)
15 | if err != nil {
16 | return nil, err
17 | }
18 | req.Header.Set("Content-Type", contentType)
19 | return req, nil
20 | }
21 |
22 | // This is now copy-paste from the server test side... probably make public and reuse.
23 | func CreateFormDataBodyRequest(method, urlStr, filedata string, fieldname string, filename string) (*http.Request, error) {
24 | //create attachment....
25 | bodyBuf := &bytes.Buffer{}
26 | bodyWriter := multipart.NewWriter(bodyBuf)
27 |
28 | //
29 | fileWriter, err := bodyWriter.CreateFormFile(fieldname, filename)
30 | if err != nil {
31 | log.Println("error writing to buffer")
32 | return nil, err
33 | }
34 | // When using a file this used to be: fh, err := os.Open(pubKeyFilename)
35 | fh := strings.NewReader(filedata)
36 |
37 | _, err = io.Copy(fileWriter, fh)
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | contentType := bodyWriter.FormDataContentType()
43 | bodyWriter.Close()
44 |
45 | req, err := http.NewRequest(method, urlStr, bodyBuf)
46 | if err != nil {
47 | return nil, err
48 | }
49 | req.Header.Set("Content-Type", contentType)
50 |
51 | return req, nil
52 | }
53 |
--------------------------------------------------------------------------------
/lib/pwauth/command/command_test.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/Symantec/Dominator/lib/log/testlogger"
7 | )
8 |
9 | func TestTrueCommand(t *testing.T) {
10 | pa, err := New("true", nil, testlogger.New(t))
11 | if err != nil {
12 | t.Fatalf("unable to create PasswordAuthenticator")
13 | }
14 | if ok, err := pa.PasswordAuthenticate("u", []byte("p")); !ok {
15 | if err != nil {
16 | t.Fatalf("unexpected error: %s", err)
17 | } else {
18 | t.Fatalf("true did not return 0")
19 | }
20 | }
21 | }
22 |
23 | func TestFalseCommand(t *testing.T) {
24 | pa, err := New("false", nil, testlogger.New(t))
25 | if err != nil {
26 | t.Fatalf("unable to create PasswordAuthenticator")
27 | }
28 | if ok, err := pa.PasswordAuthenticate("u", []byte("p")); !ok {
29 | if err != nil {
30 | t.Fatalf("unexpected error: %s", err)
31 | }
32 | } else {
33 | t.Fatalf("false did not return 1")
34 | }
35 | }
36 |
37 | func TestMissingCommand(t *testing.T) {
38 | _, err := New("/should-not-exist/test.foo-bar_baz", nil, testlogger.New(t))
39 | if err == nil {
40 | t.Fatalf("missing command did not generate error")
41 | }
42 | }
43 |
44 | func TestBrokenCommand(t *testing.T) {
45 | pa, err := New("true", nil, testlogger.New(t))
46 | if err != nil {
47 | t.Fatalf("unable to create PasswordAuthenticator")
48 | }
49 | pa.command = "/should-not-exist/test.foo-bar_baz"
50 | if _, err := pa.PasswordAuthenticate("u", []byte("p")); err == nil {
51 | t.Fatalf("missing command did not generate error")
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/pwauth/command/api.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "github.com/Symantec/Dominator/lib/log"
5 | "github.com/Symantec/keymaster/lib/simplestorage"
6 | )
7 |
8 | type PasswordAuthenticator struct {
9 | command string
10 | args []string
11 | logger log.Logger
12 | }
13 |
14 | // New creates a new PasswordAuthenticator. The command used to authenticate the
15 | // user is command and args may contain optional arguments to pass after the
16 | // username. Log messages are written to logger. A new *PasswordAuthenticator is
17 | // returned if the command exists, else an error is returned.
18 | // The command should exit with 0 for a successful authentication, 1 if the
19 | // authentication is not successful (bad username/password) and any other value
20 | // if an error occurs.
21 | func New(command string, args []string, logger log.Logger) (
22 | *PasswordAuthenticator, error) {
23 | return newAuthenticator(command, args, logger)
24 | }
25 |
26 | // PasswordAuthenticate will authenticate a user using the provided username and
27 | // password. The password is provided on the standard input of the
28 | // authentication command.
29 | // It returns true if the user is authenticated, else false (due to either
30 | // invalid username or incorrect password), and an error.
31 | func (pa *PasswordAuthenticator) PasswordAuthenticate(username string,
32 | password []byte) (bool, error) {
33 | return pa.passwordAuthenticate(username, password)
34 | }
35 |
36 | func (pa *PasswordAuthenticator) UpdateStorage(storage simplestorage.SimpleStore) error {
37 | return nil
38 | }
39 |
--------------------------------------------------------------------------------
/lib/pwauth/ldap/api.go:
--------------------------------------------------------------------------------
1 | package ldap
2 |
3 | import (
4 | "crypto/x509"
5 | "net/url"
6 | "time"
7 |
8 | "github.com/Symantec/Dominator/lib/log"
9 | "github.com/Symantec/keymaster/lib/simplestorage"
10 | )
11 |
12 | type cacheCredentialEntry struct {
13 | Expiration time.Time
14 | Hash string
15 | }
16 |
17 | type PasswordAuthenticator struct {
18 | ldapURL []*url.URL
19 | bindPattern []string
20 | timeoutSecs uint
21 | rootCAs *x509.CertPool
22 | logger log.DebugLogger
23 | expirationDuration time.Duration
24 | storage simplestorage.SimpleStore
25 | cachedCredentials map[string]cacheCredentialEntry
26 | }
27 |
28 | func New(url []string, bindPattern []string, timeoutSecs uint, rootCAs *x509.CertPool, storage simplestorage.SimpleStore, logger log.DebugLogger) (
29 | *PasswordAuthenticator, error) {
30 | return newAuthenticator(url, bindPattern, timeoutSecs, rootCAs, storage, logger)
31 | }
32 |
33 | func (pa *PasswordAuthenticator) UpdateStorage(storage simplestorage.SimpleStore) error {
34 | pa.storage = storage
35 | return nil
36 | }
37 |
38 | // PasswordAuthenticate will authenticate a user using the provided username and
39 | // password. The password is provided on the standard input of the
40 | // authentication command.
41 | // It returns true if the user is authenticated, else false (due to either
42 | // invalid username or incorrect password), and an error.
43 | func (pa *PasswordAuthenticator) PasswordAuthenticate(username string,
44 | password []byte) (bool, error) {
45 | return pa.passwordAuthenticate(username, password)
46 | }
47 |
--------------------------------------------------------------------------------
/lib/instrumentedwriter/instrumentedWriter_test.go:
--------------------------------------------------------------------------------
1 | package instrumentedwriter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/http/httptest"
7 | "strings"
8 | "testing"
9 | )
10 |
11 | type customLogger struct {
12 | buf string
13 | }
14 |
15 | func (l *customLogger) Log(record LogRecord) {
16 | fields := make([]string, 0)
17 | fields = append(fields, "method:"+record.Method)
18 | fields = append(fields, "uri:"+record.Uri)
19 | fields = append(fields, "protocol:"+record.Protocol)
20 | fields = append(fields, "username:"+record.Username)
21 | fields = append(fields, "host:"+record.Host)
22 | fields = append(fields, "status:"+fmt.Sprintf("%d", record.Status))
23 | fields = append(fields, "customRecords:"+fmt.Sprintf("%v", record.CustomRecords))
24 | l.buf += strings.Join(fields, ",")
25 | l.buf += "\n"
26 | }
27 |
28 | func okHandler(w http.ResponseWriter, req *http.Request) {
29 | w.(*LoggingWriter).SetCustomLogRecord("x-user-id", "1")
30 | w.Write([]byte(`ok`))
31 | }
32 |
33 | func newRequest(method, url string) *http.Request {
34 | req, err := http.NewRequest(method, url, nil)
35 | if err != nil {
36 | panic(err)
37 | }
38 | req.Host = "example.com"
39 | return req
40 | }
41 |
42 | func TestOutput(t *testing.T) {
43 | logger := customLogger{}
44 | loggingHandler := NewLoggingHandler(http.HandlerFunc(okHandler), &logger)
45 | writer := httptest.NewRecorder()
46 | loggingHandler.ServeHTTP(writer, newRequest("GET", "/"))
47 |
48 | expected := "method:GET,uri:,protocol:HTTP/1.1,username:-,host:example.com,status:200,customRecords:map[x-user-id:1]\n"
49 | output := logger.buf
50 | if output != expected {
51 | t.Errorf("expected %s but got %s", expected, output)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/cmd/keymasterd/adminDashboard.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/Symantec/Dominator/lib/html"
9 | )
10 |
11 | type adminDashboardType struct {
12 | htmlWriter html.HtmlWriter
13 | ready bool
14 | publicLogs bool
15 | }
16 |
17 | func newAdminDashboard(htmlWriter html.HtmlWriter, publicLogs bool) *adminDashboardType {
18 | return &adminDashboardType{
19 | htmlWriter: htmlWriter,
20 | publicLogs: publicLogs,
21 | }
22 | }
23 |
24 | func (dashboard *adminDashboardType) ServeHTTP(w http.ResponseWriter,
25 | req *http.Request) {
26 | if req.URL.Path != "/" {
27 | http.NotFound(w, req)
28 | return
29 | }
30 | writer := bufio.NewWriter(w)
31 | defer writer.Flush()
32 | setSecurityHeaders(w)
33 | fmt.Fprintln(writer, "keymaster status page")
34 | fmt.Fprintln(writer, "")
35 | fmt.Fprintln(writer, "")
36 | fmt.Fprintln(writer, "keymaster status page
")
37 | fmt.Fprintln(writer, "")
38 | html.WriteHeaderWithRequest(writer, req)
39 | fmt.Fprintln(writer, "")
40 | if dashboard.ready {
41 | fmt.Fprintln(writer,
42 | `Keymaster is ready
`)
43 | } else {
44 | fmt.Fprintln(writer, `Keymaster is sealed
`)
45 | }
46 | if dashboard.publicLogs {
47 | dashboard.htmlWriter.WriteHtml(writer)
48 | }
49 | fmt.Fprintln(writer, "
")
50 | fmt.Fprintln(writer, "
")
51 | if Version != "" {
52 | fmt.Fprintf(writer, "Keymasterd version: %s
", Version)
53 | }
54 | html.WriteFooter(writer)
55 | fmt.Fprintln(writer, "")
56 | }
57 |
58 | func (dashboard *adminDashboardType) setReady() {
59 | dashboard.ready = true
60 | }
61 |
--------------------------------------------------------------------------------
/lib/client/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "errors"
5 | "io/ioutil"
6 | "net/http"
7 | "os"
8 |
9 | "github.com/Symantec/Dominator/lib/log"
10 | "gopkg.in/yaml.v2"
11 | )
12 |
13 | func loadVerifyConfigFile(configFilename string) (AppConfigFile, error) {
14 | var config AppConfigFile
15 | if _, err := os.Stat(configFilename); os.IsNotExist(err) {
16 | err = errors.New("No config file: please re-run with -configHost")
17 | return config, err
18 | }
19 | source, err := ioutil.ReadFile(configFilename)
20 | if err != nil {
21 | err = errors.New("cannot read config file")
22 | return config, err
23 | }
24 | err = yaml.Unmarshal(source, &config)
25 | if err != nil {
26 | err = errors.New("Cannot parse config file")
27 | return config, err
28 | }
29 |
30 | if len(config.Base.Gen_Cert_URLS) < 1 {
31 | err = errors.New("Invalid Config file... no place get the certs")
32 | return config, err
33 | }
34 | // TODO: ensure all enpoints are https urls
35 |
36 | return config, nil
37 | }
38 |
39 | const hostConfigPath = "/public/clientConfig"
40 |
41 | func getConfigFromHost(
42 | configFilename string,
43 | hostname string,
44 | client *http.Client,
45 | logger log.Logger) error {
46 | configUrl := "https://" + hostname + hostConfigPath
47 | resp, err := client.Get(configUrl)
48 | if err != nil {
49 | logger.Printf("got error from req")
50 | logger.Println(err)
51 | return err
52 | }
53 | defer resp.Body.Close()
54 | if resp.StatusCode != 200 {
55 | logger.Printf("got error from getconfig call %s", resp)
56 | return err
57 | }
58 | configData, err := ioutil.ReadAll(resp.Body)
59 | if err != nil {
60 | return err
61 | }
62 | return ioutil.WriteFile(configFilename, configData, 0644)
63 | }
64 |
--------------------------------------------------------------------------------
/cmd/keymasterd/static_files/webui-2fa-symc-vip.js:
--------------------------------------------------------------------------------
1 | function singleVipPoll() {
2 | var xhr = new XMLHttpRequest();
3 | xhr.onreadystatechange = function() {
4 | if (this.readyState == 4 && this.status == 200) {
5 | // Action to be performed when the document is read;
6 | var destination = document.getElementById("vip_login_destination").innerHTML;
7 | window.location.href = destination;
8 | }
9 | };
10 | xhr.open("GET", "/api/v0/vipPollCheck", true);
11 | xhr.send();
12 | }
13 |
14 | function startVipPoll() {
15 | console.log("start of vip poll");
16 | poller = setInterval(singleVipPoll, 3000);
17 | var startPushButton = document.getElementById("start_vip_push_button")
18 | if (startPushButton) {
19 | startPushButton.addEventListener('click', startVipPush, false);
20 | setTimeout(startVipPush,6000)
21 | } else {
22 | startVipPush();
23 | }
24 | setTimeout(clearInterval,60000, poller)
25 | }
26 | function startVipPush() {
27 | var xhr = new XMLHttpRequest();
28 | xhr.onreadystatechange = function() {
29 | if (this.readyState == 4 && this.status == 200) {
30 | // Action to be performed when the document is read;
31 | //var destination = document.getElementById("vip_login_destination").innerHTML;
32 | //window.location.href = destination;
33 | cosole.log("success vip push start")
34 | }
35 | };
36 | xhr.open("GET", "/api/v0/vipPushStart", true);
37 | xhr.send();
38 | }
39 |
40 | document.addEventListener('DOMContentLoaded', function () {
41 | //document.getElementById('auth_button').addEventListener('click', sign);
42 | startVipPoll();
43 | });
44 |
--------------------------------------------------------------------------------
/eventmon/monitord/api.go:
--------------------------------------------------------------------------------
1 | package monitord
2 |
3 | import (
4 | "crypto/x509"
5 | "io"
6 | "sync"
7 |
8 | "github.com/Symantec/Dominator/lib/log"
9 | "golang.org/x/crypto/ssh"
10 | )
11 |
12 | type AuthInfo struct {
13 | AuthType string
14 | Username string
15 | VIPAuthType string
16 | }
17 |
18 | type SPLoginInfo struct {
19 | URL string
20 | Username string
21 | }
22 |
23 | type Monitor struct {
24 | keymasterServerHostname string
25 | keymasterServerPortNum uint
26 | closers map[string]chan<- struct{} // [addr]close notifier.
27 | // Transmit side channels (private).
28 | authChannel chan<- AuthInfo
29 | serviceProviderLoginChannel chan<- SPLoginInfo
30 | sshRawCertChannel chan<- []byte
31 | sshCertChannel chan<- *ssh.Certificate
32 | webLoginChannel chan<- string
33 | x509RawCertChannel chan<- []byte
34 | x509CertChannel chan<- *x509.Certificate
35 | // Receive side channels (public).
36 | AuthChannel <-chan AuthInfo
37 | ServiceProviderLoginChannel <-chan SPLoginInfo
38 | SshRawCertChannel <-chan []byte
39 | SshCertChannel <-chan *ssh.Certificate
40 | WebLoginChannel <-chan string
41 | X509RawCertChannel <-chan []byte
42 | X509CertChannel <-chan *x509.Certificate
43 | mutex sync.RWMutex // Lock all below.
44 | keymasterStatus map[string]error // Key: IP address.
45 | }
46 |
47 | func New(keymasterServerHostname string, keymasterServerPortNum uint,
48 | logger log.Logger) (*Monitor, error) {
49 | return newMonitor(keymasterServerHostname, keymasterServerPortNum, logger)
50 | }
51 |
52 | func (m *Monitor) WriteHtml(writer io.Writer) {
53 | m.writeHtml(writer)
54 | }
55 |
--------------------------------------------------------------------------------
/eventmon/eventrecorder/api.go:
--------------------------------------------------------------------------------
1 | package eventrecorder
2 |
3 | import (
4 | "crypto/x509"
5 | "time"
6 |
7 | "github.com/Symantec/Dominator/lib/log"
8 | "golang.org/x/crypto/ssh"
9 | )
10 |
11 | const (
12 | AuthTypeNone = iota
13 | AuthTypePassword
14 | AuthTypeSymantecVIP
15 | AuthTypeU2F
16 | )
17 |
18 | const (
19 | VIPAuthTypeOTP = iota
20 | VIPAuthTypePush
21 | )
22 |
23 | type AuthInfo struct {
24 | AuthType uint
25 | Username string
26 | VIPAuthType uint8
27 | }
28 |
29 | type Events struct {
30 | ComputeTime time.Duration
31 | Events EventsMap
32 | }
33 |
34 | type EventsMap map[string][]EventType // Key: username.
35 |
36 | type eventsListType struct {
37 | newest *eventType
38 | oldest *eventType
39 | }
40 |
41 | type EventType struct {
42 | AuthType uint
43 | CreateTime uint64 // Seconds since Epoch.
44 | LifetimeSeconds uint32
45 | ServiceProviderUrl string
46 | Ssh bool
47 | WebLogin bool
48 | X509 bool
49 | VIPAuthType uint8
50 | }
51 |
52 | type eventType struct {
53 | EventType
54 | newer *eventType
55 | older *eventType
56 | }
57 |
58 | type EventRecorder struct {
59 | AuthChannel chan<- *AuthInfo
60 | RequestEventsChannel chan<- chan<- Events
61 | ServiceProviderLoginChannel chan<- *SPLoginInfo
62 | SshCertChannel chan<- *ssh.Certificate
63 | WebLoginChannel chan<- string
64 | X509CertChannel chan<- *x509.Certificate
65 | filename string
66 | logger log.Logger
67 | eventsMap map[string]*eventsListType // Key: username.
68 | }
69 |
70 | type SPLoginInfo struct {
71 | URL string
72 | Username string
73 | }
74 |
75 | func New(filename string, logger log.Logger) (*EventRecorder, error) {
76 | return newEventRecorder(filename, logger)
77 | }
78 |
--------------------------------------------------------------------------------
/lib/pwauth/okta/impl.go:
--------------------------------------------------------------------------------
1 | package okta
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/Symantec/Dominator/lib/log"
8 | "net/http"
9 | )
10 |
11 | const (
12 | authPath = "/api/v1/authn"
13 | authEndpointFormat = "https://%s.okta.com" + authPath
14 | )
15 |
16 | type loginDataType struct {
17 | Password string `json:"password,omitempty"`
18 | Username string `json:"username,omitempty"`
19 | }
20 |
21 | type responseType struct {
22 | Status string `json:"status,omitempty"`
23 | }
24 |
25 | func newPublicAuthenticator(oktaDomain string, logger log.Logger) (
26 | *PasswordAuthenticator, error) {
27 | return &PasswordAuthenticator{
28 | authnURL: fmt.Sprintf(authEndpointFormat, oktaDomain),
29 | logger: logger,
30 | }, nil
31 | }
32 |
33 | func (pa *PasswordAuthenticator) passwordAuthenticate(username string,
34 | password []byte) (bool, error) {
35 | loginData := loginDataType{Password: string(password), Username: username}
36 | body := &bytes.Buffer{}
37 | encoder := json.NewEncoder(body)
38 | encoder.SetIndent("", " ") // Make life easier for debugging.
39 | if err := encoder.Encode(loginData); err != nil {
40 | return false, err
41 | }
42 | req, err := http.NewRequest("POST", pa.authnURL, body)
43 | if err != nil {
44 | return false, err
45 | }
46 | req.Header.Add("Accept", "application/json")
47 | req.Header.Add("Content-Type", "application/json")
48 | resp, err := http.DefaultClient.Do(req)
49 | if err != nil {
50 | return false, err
51 | }
52 | if resp.StatusCode == http.StatusUnauthorized {
53 | return false, nil
54 | }
55 | if resp.StatusCode != http.StatusOK {
56 | return false, fmt.Errorf("bad status: %s", resp.Status)
57 | }
58 | decoder := json.NewDecoder(resp.Body)
59 | var response responseType
60 | if err := decoder.Decode(&response); err != nil {
61 | return false, err
62 | }
63 | switch response.Status {
64 | case "SUCCESS", "MFA_REQUIRED":
65 | return true, nil
66 | default:
67 | return false, nil
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Set GOPATH to a sensible default if not already set.
2 | ifdef USERPROFILE
3 | GOPATH ?= $(USERPROFILE)\go
4 | else
5 | GOPATH ?= $(HOME)/go
6 | endif
7 |
8 |
9 | # This is how we want to name the binary output
10 | BINARY=keymaster
11 |
12 | # These are the values we want to pass for Version and BuildTime
13 | VERSION=1.7.0
14 | #BUILD_TIME=`date +%FT%T%z`
15 |
16 | # Setup the -ldflags option for go build here, interpolate the variable values
17 | #LDFLAGS=-ldflags "-X github.com/ariejan/roll/core.Version=${VERSION} -X github.com/ariejan/roll/core.BuildTime=${BUILD_TIME}"
18 |
19 | all: init-config-host
20 | cd $(GOPATH)/src; go install -ldflags "-X main.Version=${VERSION}" github.com/Symantec/keymaster/cmd/*
21 |
22 | win-client:
23 | cd $(GOPATH)\src && go install -ldflags "-X main.Version=${VERSION}" github.com\Symantec\keymaster\cmd\keymaster
24 | cd $(GOPATH)\src\github.com\Symantec\keymaster\cmd\keymaster && go test -v ./...
25 |
26 | get-deps: init-config-host
27 | go get -t ./...
28 |
29 | clean:
30 | rm -f bin/*
31 | rm -f keymaster-*.tar.gz
32 |
33 | init-config-host:
34 | @test -f cmd/keymaster/config_host.go || (cp -p templates/config_host_go cmd/keymaster/config_host.go && echo 'Created initial cmd/keymaster/config_host.go')
35 |
36 | ${BINARY}-${VERSION}.tar.gz:
37 | mkdir ${BINARY}-${VERSION}
38 | rsync -av --exclude="config.yml" --exclude="*.pem" --exclude="*.out" lib/ ${BINARY}-${VERSION}/lib/
39 | rsync -av --exclude="config.yml" --exclude="*.pem" --exclude="*.out" --exclude="*.key" cmd/ ${BINARY}-${VERSION}/cmd/
40 | rsync -av misc/ ${BINARY}-${VERSION}/misc/
41 | cp LICENSE Makefile keymaster.spec README.md ${BINARY}-${VERSION}/
42 | tar -cvzf ${BINARY}-${VERSION}.tar.gz ${BINARY}-${VERSION}/
43 | rm -rf ${BINARY}-${VERSION}/
44 |
45 | rpm: ${BINARY}-${VERSION}.tar.gz
46 | rpmbuild -ta ${BINARY}-${VERSION}.tar.gz
47 |
48 | tar: ${BINARY}-${VERSION}.tar.gz
49 |
50 | test: init-config-host
51 | go test ./...
52 |
53 | verbose-test: init-config-host
54 | go test -v ./...
55 |
56 | format:
57 | gofmt -s -w .
58 |
59 | format-imports:
60 | goimports -w .
61 |
--------------------------------------------------------------------------------
/cmd/keymaster-unlocker/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/tls"
5 | "flag"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "net/url"
10 | "os"
11 | "strconv"
12 |
13 | "github.com/Symantec/Dominator/lib/log/cmdlogger"
14 | "github.com/howeyc/gopass"
15 | )
16 |
17 | var (
18 | Version = "No version provided"
19 | certFile = flag.String("cert", "client.pem", "A PEM eoncoded certificate file.")
20 | keyFile = flag.String("key", "key.pem", "A PEM encoded private key file.")
21 | targetHost = flag.String("keymasterHostname", "", "The hostname/port for keymaster")
22 | targetPort = flag.Int("keymasterPort", 6920, "The port for keymaster control port")
23 | )
24 |
25 | func Usage() {
26 | fmt.Fprintf(os.Stderr, "Usage of %s (version %s):\n", os.Args[0], Version)
27 | flag.PrintDefaults()
28 | }
29 |
30 | func main() {
31 | flag.Parse()
32 | logger := cmdlogger.New()
33 |
34 | if len(*targetHost) < 1 {
35 | logger.Fatal("keymasterHostname paramteter is required")
36 | }
37 |
38 | // Load client cert
39 | cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
40 | if err != nil {
41 | logger.Fatal(err)
42 | }
43 |
44 | fmt.Printf("Password for unlocking %s: ", *targetHost)
45 | password, err := gopass.GetPasswd()
46 | if err != nil {
47 | logger.Fatal(err)
48 | // Handle gopass.ErrInterrupted or getch() read error
49 | }
50 |
51 | // Setup HTTPS client
52 | tlsConfig := &tls.Config{
53 | Certificates: []tls.Certificate{cert},
54 | }
55 | tlsConfig.BuildNameToCertificate()
56 | transport := &http.Transport{TLSClientConfig: tlsConfig}
57 | client := &http.Client{Transport: transport}
58 |
59 | // Do GET something
60 | resp, err := client.PostForm("https://"+*targetHost+":"+strconv.Itoa(*targetPort)+"/admin/inject",
61 | url.Values{"ssh_ca_password": {string(password[:])}})
62 | //resp, err := client.Get("https://goldportugal.local:8443")
63 | if err != nil {
64 | logger.Fatal(err)
65 | }
66 | defer resp.Body.Close()
67 |
68 | // Dump response
69 | data, err := ioutil.ReadAll(resp.Body)
70 | if err != nil {
71 | logger.Fatal(err)
72 | }
73 | logger.Println(string(data))
74 | }
75 |
--------------------------------------------------------------------------------
/cmd/keymasterd/storage_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | stdlog "log"
5 | "os"
6 | "testing"
7 | //"time"
8 |
9 | "github.com/Symantec/Dominator/lib/log/debuglogger"
10 | "github.com/Symantec/keymaster/keymasterd/eventnotifier"
11 | )
12 |
13 | func init() {
14 | slogger := stdlog.New(os.Stderr, "", stdlog.LstdFlags)
15 | logger = debuglogger.New(slogger)
16 | eventNotifier = eventnotifier.New(logger)
17 | }
18 |
19 | func TestDBCopy(t *testing.T) {
20 | var state RuntimeState
21 | err := initDB(&state)
22 | if err != nil {
23 | t.Fatal(err)
24 | }
25 | // copy blank db
26 | err = copyDBIntoSQLite(state.db, state.cacheDB, "sqlite")
27 | if err != nil {
28 | t.Fatal(err)
29 | }
30 | // make a profile and push back to db
31 | profile, _, _, err := state.LoadUserProfile("username")
32 | if err != nil {
33 | t.Fatal(err)
34 | }
35 | err = state.SaveUserProfile("username", profile)
36 | if err != nil {
37 | t.Fatal(err)
38 | }
39 | // copy the db now with one user
40 | err = copyDBIntoSQLite(state.db, state.cacheDB, "sqlite")
41 | if err != nil {
42 | t.Fatal(err)
43 | }
44 | }
45 |
46 | func TestFetchFromCache(t *testing.T) {
47 | var state RuntimeState
48 | err := initDB(&state)
49 | if err != nil {
50 | t.Fatal(err)
51 | }
52 | // make a profile and push back to db
53 | profile, _, _, err := state.LoadUserProfile("username")
54 | if err != nil {
55 | t.Fatal(err)
56 | }
57 | err = state.SaveUserProfile("username", profile)
58 | if err != nil {
59 | t.Fatal(err)
60 | }
61 | // copy blank with one user...
62 | err = copyDBIntoSQLite(state.db, state.cacheDB, "sqlite")
63 | if err != nil {
64 | t.Fatal(err)
65 | }
66 | state.remoteDBQueryTimeout = 0
67 | _, _, fromCache, err := state.LoadUserProfile("username")
68 | if err != nil {
69 | t.Fatal(err)
70 | }
71 | if !fromCache {
72 | t.Fatal("did NOT got data from cache")
73 | }
74 | _, ok, fromCache, err := state.LoadUserProfile("unknown-user")
75 | if err != nil {
76 | t.Fatal(err)
77 | }
78 |
79 | if !fromCache {
80 | t.Fatal("did NOT got data from cache")
81 | }
82 | if ok {
83 | t.Fatal("This should have failed for invalid user")
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/cmd/keymasterd/static_files/webui-2fa-u2f.js:
--------------------------------------------------------------------------------
1 | function serverError(data) {
2 | console.log(data);
3 | alert('Server error code ' + data.status + ': ' + data.responseText);
4 | }
5 |
6 | function checkError(resp) {
7 | if (!('errorCode' in resp)) {
8 | return false;
9 | }
10 | //if (resp.errorCode === u2f.ErrorCodes['OK']) {
11 | if (resp.errorCode == 0) {
12 | return false;
13 | }
14 | var msg = 'U2F error code ' + resp.errorCode;
15 | for (name in u2f.ErrorCodes) {
16 | if (u2f.ErrorCodes[name] === resp.errorCode) {
17 | msg += ' (' + name + ')';
18 | }
19 | }
20 | if (resp.errorMessage) {
21 | msg += ': ' + resp.errorMessage;
22 | }
23 | console.log(msg);
24 | alert(msg);
25 | return true;
26 | }
27 | function hideAllU2FElements() {
28 | document.getElementById('auth_action_text').style.display="none";
29 | var manualStartVipDiv = document.getElementById("manual_start_vip_div")
30 | if (manualStartVipDiv) {
31 | manualStartVipDiv.style.display="none";
32 | }
33 | var otpOrU2fMessageDiv = document.getElementById("otp_or_u2f_message")
34 | if (otpOrU2fMessageDiv) {
35 | otpOrU2fMessageDiv.style.display="none";
36 | }
37 | }
38 |
39 | function u2fSigned(resp) {
40 | //document.getElementById('auth_action_text').style.display="none";
41 | hideAllU2FElements();
42 | //console.log(resp);
43 | if (checkError(resp)) {
44 | return;
45 | }
46 | $.post('/u2f/SignResponse', JSON.stringify(resp)).done(function() {
47 | //alert('Success');
48 | var destination = document.getElementById("u2f_login_destination").innerHTML;
49 | window.location.href = destination;
50 | }).fail(serverError);
51 | }
52 | function sign() {
53 | document.getElementById('auth_action_text').style.display="block";
54 | $.getJSON('/u2f/SignRequest').done(function(req) {
55 | console.log(req);
56 | u2f.sign(req.appId, req.challenge, req.registeredKeys, u2fSigned, 45);
57 | }).fail(serverError);
58 | }
59 |
60 | document.addEventListener('DOMContentLoaded', function () {
61 | //document.getElementById('auth_button').addEventListener('click', sign);
62 | sign();
63 | });
64 |
--------------------------------------------------------------------------------
/cmd/keymasterd/static_files/keymaster-u2f.js:
--------------------------------------------------------------------------------
1 | function serverError(data) {
2 | console.log(data);
3 | alert('Server error code ' + data.status + ': ' + data.responseText);
4 | }
5 |
6 | function checkError(resp) {
7 | if (!('errorCode' in resp)) {
8 | return false;
9 | }
10 |
11 | //if (resp.errorCode === u2f.ErrorCodes['OK']) {
12 | if (resp.errorCode == 0) {
13 | return false;
14 | }
15 | var msg = 'U2F error code ' + resp.errorCode;
16 | for (name in u2f.ErrorCodes) {
17 | if (u2f.ErrorCodes[name] === resp.errorCode) {
18 | msg += ' (' + name + ')';
19 | }
20 | }
21 | if (resp.errorMessage) {
22 | msg += ': ' + resp.errorMessage;
23 | }
24 | console.log(msg);
25 | alert(msg);
26 | return true;
27 | }
28 | function u2fRegistered(resp) {
29 | var username = document.getElementById('username').textContent;
30 | console.log(resp);
31 | if (checkError(resp)) {
32 | return;
33 | }
34 | $.post('/u2f/RegisterResponse/' + username, JSON.stringify(resp)).done(function() {
35 | alert('Success');
36 | location.reload();
37 | }).fail(serverError);
38 | }
39 | function register() {
40 | var username = document.getElementById('username').textContent;
41 | document.getElementById('register_action_text').style.display="block";
42 | $.getJSON('/u2f/RegisterRequest/' + username).done(function(req) {
43 | console.log(req);
44 | u2f.register(req.appId, req.registerRequests, req.registeredKeys, u2fRegistered, 30);
45 | }).fail(serverError);
46 | }
47 | function u2fSigned(resp) {
48 | document.getElementById('auth_action_text').style.display="none";
49 | console.log(resp);
50 | if (checkError(resp)) {
51 | return;
52 | }
53 | $.post('/u2f/SignResponse', JSON.stringify(resp)).done(function() {
54 | alert('Success');
55 | }).fail(serverError);
56 | }
57 | function sign() {
58 | document.getElementById('auth_action_text').style.display="block";
59 | $.getJSON('/u2f/SignRequest').done(function(req) {
60 | console.log(req);
61 | u2f.sign(req.appId, req.challenge, req.registeredKeys, u2fSigned, 30);
62 | }).fail(serverError);
63 | }
64 |
65 | document.addEventListener('DOMContentLoaded', function () {
66 | document.getElementById('auth_button').addEventListener('click', sign);
67 | document.getElementById('register_button').addEventListener('click', register);
68 | // main();
69 | });
70 |
--------------------------------------------------------------------------------
/keymasterd/admincache/cache_test.go:
--------------------------------------------------------------------------------
1 | package admincache
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | type testClockType struct {
9 | NowTime time.Time
10 | }
11 |
12 | func (t *testClockType) Now() time.Time {
13 | return t.NowTime
14 | }
15 |
16 | func (t *testClockType) Advance(d time.Duration) {
17 | t.NowTime = t.NowTime.Add(d)
18 | }
19 |
20 | func TestAPIBasic(t *testing.T) {
21 | var cache *Cache
22 | isAdmin, valid := cache.Get("user")
23 | if valid {
24 | t.Fatalf("should have been invalid")
25 | }
26 | if isAdmin {
27 | t.Fatalf("should have been non admin")
28 | }
29 | cache.Put("user", true)
30 |
31 | cache2 := New(time.Minute)
32 | cache2.Put("user2", true)
33 | isAdmin, valid = cache2.Get("user2")
34 | if !valid {
35 | t.Fatalf("should have been valid")
36 | }
37 | if !isAdmin {
38 | t.Fatalf("should have been admin")
39 | }
40 | }
41 |
42 | func TestExpiration(t *testing.T) {
43 | testClock := &testClockType{
44 | NowTime: time.Date(2018, 1, 9, 12, 34, 56, 0, time.Local),
45 | }
46 | cache := newForTesting(5*time.Minute, testClock)
47 | //initial cache should be invalid/false
48 | admin, valid := cache.Get("user")
49 | if valid {
50 | t.Fatalf("should have been invalid")
51 | }
52 | if admin {
53 | t.Fatalf("should have been non admin")
54 | }
55 | //Expire works as expected
56 | cache.Put("user1", false)
57 | cache.Put("user2", true)
58 | testClock.Advance(4 * time.Minute)
59 | admin, valid = cache.Get("user1")
60 | if !valid {
61 | t.Fatalf("should have been valid")
62 | }
63 | if admin {
64 | t.Fatalf("should have been non admin")
65 | }
66 |
67 | admin, valid = cache.Get("user2")
68 | if !valid {
69 | t.Fatalf("should have been valid")
70 | }
71 | if !admin {
72 | t.Fatalf("should have been admin")
73 | }
74 | admin, valid = cache.Get("user")
75 | if valid {
76 | t.Fatalf("should have been invalid")
77 | }
78 | if admin {
79 | t.Fatalf("should have been non admin")
80 | }
81 |
82 | // This causes our two cache entries to expire
83 | // Because 5 minutes will have elapsed
84 | testClock.Advance(time.Minute)
85 | admin, valid = cache.Get("user1")
86 | if valid {
87 | t.Fatalf("should have been invalid")
88 | }
89 | if admin {
90 | t.Fatalf("should have been non admin")
91 | }
92 | admin, valid = cache.Get("user2")
93 | if valid {
94 | t.Fatalf("should have been invalid")
95 | }
96 | if !admin {
97 | t.Fatalf("should have been admin")
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/lib/certgen/iprestricted_test.go:
--------------------------------------------------------------------------------
1 | package certgen
2 |
3 | import (
4 | "crypto/x509/pkix"
5 | "encoding/asn1"
6 | "encoding/hex"
7 | "errors"
8 | "net"
9 | "testing"
10 | )
11 |
12 | func TestComputePublicKeyKeyID(t *testing.T) {
13 | userPub, _, _ := setupX509Generator(t)
14 | _, err := ComputePublicKeyKeyID(userPub)
15 | if err != nil {
16 | t.Fatal(err)
17 | }
18 | }
19 |
20 | func TestGenDelegationExtension(t *testing.T) {
21 |
22 | netblock := net.IPNet{
23 | IP: net.ParseIP("10.11.12.0"),
24 | Mask: net.CIDRMask(24, 32),
25 | }
26 | netblock2 := net.IPNet{
27 | IP: net.ParseIP("13.14.128.0"),
28 | Mask: net.CIDRMask(20, 32),
29 | }
30 | netblockList := []net.IPNet{netblock, netblock2}
31 | var extension *pkix.Extension
32 | var err error
33 | extension, err = genDelegationExtension(netblockList)
34 | if err != nil {
35 | t.Fatal(err)
36 | }
37 | extensionDer, err := asn1.Marshal(*extension)
38 | if err != nil {
39 | t.Fatal(err)
40 | }
41 |
42 | t.Logf("encodedExt=\n%s", hex.Dump(extensionDer))
43 | var addressFamilyList []IpAdressFamily
44 | _, err = asn1.Unmarshal(extension.Value, &addressFamilyList)
45 | if err != nil {
46 | t.Fatal(err)
47 | }
48 | t.Logf("%+v", addressFamilyList)
49 | var roundTripBlockList []net.IPNet
50 | for _, encodedNetblock := range addressFamilyList[0].Addresses {
51 | decoded, err := decodeIPV4AddressChoice(encodedNetblock)
52 | if err != nil {
53 | t.Fatal(err)
54 | }
55 | roundTripBlockList = append(roundTripBlockList, decoded)
56 | }
57 | t.Logf("%+v", roundTripBlockList)
58 | if len(roundTripBlockList) != len(netblockList) {
59 | t.Fatal(errors.New("bad rountrip lenght"))
60 | }
61 |
62 | }
63 |
64 | func TestGenIPRestrictedX509Cert(t *testing.T) {
65 | userPub, caCert, caPriv := setupX509Generator(t)
66 | netblock := net.IPNet{
67 | IP: net.ParseIP("127.0.0.0"),
68 | Mask: net.CIDRMask(8, 32),
69 | }
70 | netblock2 := net.IPNet{
71 | IP: net.ParseIP("10.0.0.0"),
72 | Mask: net.CIDRMask(8, 32),
73 | }
74 | netblockList := []net.IPNet{netblock, netblock2}
75 | derCert, err := GenIPRestrictedX509Cert("username", userPub, caCert, caPriv, netblockList, testDuration, nil, nil)
76 | if err != nil {
77 | t.Fatal(err)
78 | }
79 | cert, _, err := derBytesCertToCertAndPem(derCert)
80 | if err != nil {
81 | t.Fatal(err)
82 | }
83 | //t.Logf("%+v", cert)
84 | var ok bool
85 | ok, err = VerifyIPRestrictedX509CertIP(cert, "10.0.0.1:234")
86 | if err != nil {
87 | t.Fatal(err)
88 | }
89 | if !ok {
90 | t.Fatal("should have passed")
91 | }
92 | ok, err = VerifyIPRestrictedX509CertIP(cert, "1.1.1.1:234")
93 | if err != nil {
94 | t.Fatal(err)
95 | }
96 | if ok {
97 | t.Fatal("should have failed bad ip range")
98 | }
99 | ok, err = VerifyIPRestrictedX509CertIP(caCert, "1.1.1.1:234")
100 | if err != nil {
101 | t.Fatal(err)
102 | }
103 | if ok {
104 | t.Fatal("should have failed extension not found")
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/misc/startup/keymaster-eventmond.Debian-7:
--------------------------------------------------------------------------------
1 | #! /bin/bash --posix
2 |
3 | ### BEGIN INIT INFO
4 | # Provides: keymaster-eventmond
5 | # Required-Start: $local_fs $network $syslog
6 | # Required-Stop: $local_fs $network $syslog
7 | # Default-Start: 2 3 4 5
8 | # Default-Stop: 0 1 6
9 | # Short-Description: Keymaster event monitor server
10 | ### END INIT INFO
11 |
12 | # /etc/init.d/keymaster-eventmond: start and stop the Keymaster event monitor
13 |
14 | . /lib/lsb/init-functions
15 |
16 | umask 022
17 |
18 | readonly default_log_dir='/var/log/keymaster-eventmond'
19 | readonly default_config_file='/etc/keymaster-eventmond/config.yml'
20 | readonly default_state_dir='/var/lib/keymaster-eventmond'
21 |
22 | CONFIG_FILE="$default_config_file"
23 | DAEMON='/usr/local/sbin/keymaster-eventmond'
24 | LOG_DIR="$default_log_dir"
25 | LOG_QUOTA=
26 | LOGBUF_LINES=
27 | LOOP_PIDFILE='/var/run/keymaster-eventmond.loop.pid'
28 | PIDFILE='/var/run/keymaster-eventmond.pid'
29 | STATE_DIR="$default_state_dir"
30 | USERNAME='eventmon'
31 |
32 | PROG_ARGS=
33 |
34 | [ -f /etc/default/keymaster-eventmond ] && . /etc/default/keymaster-eventmond
35 |
36 | test -x "$DAEMON" || exit 0
37 |
38 | export PATH="${PATH:+$PATH:}/usr/local/bin:/usr/local/sbin:/usr/sbin:/sbin"
39 |
40 | mkdir -m 0755 -p "$LOG_DIR" "$STATE_DIR"
41 | chown "$USERNAME" "$LOG_DIR" "$STATE_DIR"
42 |
43 | if [ -n "$CONFIG_FILE" ] && [ "$CONFIG_FILE" != "$default_config_file" ]; then
44 | PROG_ARGS="$PROG_ARGS -configFile=$CONFIG_FILE"
45 | fi
46 |
47 | if [ -n "$LOG_DIR" ] && [ "$LOG_DIR" != "$default_log_dir" ]; then
48 | PROG_ARGS="$PROG_ARGS -logDir=$LOG_DIR"
49 | fi
50 |
51 | if [ -n "$LOG_QUOTA" ]; then
52 | PROG_ARGS="$PROG_ARGS -logQuota=$LOG_QUOTA"
53 | fi
54 |
55 | if [ -n "$LOGBUF_LINES" ]; then
56 | PROG_ARGS="$PROG_ARGS -logbufLines=$LOGBUF_LINES"
57 | fi
58 |
59 | if [ -n "$STATE_DIR" ] && [ "$STATE_DIR" != "$default_state_dir" ]; then
60 | PROG_ARGS="$PROG_ARGS -stateDir=$STATE_DIR"
61 | fi
62 |
63 | do_start ()
64 | {
65 | start-stop-daemon --start --quiet --pidfile "$PIDFILE" \
66 | --exec "$DAEMON" --chuid "$USERNAME" --make-pidfile -- \
67 | $PROG_ARGS
68 | }
69 |
70 | start_loop ()
71 | {
72 | echo "$BASHPID" > "$LOOP_PIDFILE"
73 | while true; do
74 | do_start
75 | rm -f "$PIDFILE"
76 | sleep 1
77 | done
78 | }
79 |
80 | case "$1" in
81 | start)
82 | log_daemon_msg "Starting keymaster-eventmond daemon" "keymaster-eventmond" || true
83 | (start_loop < /dev/null &> /dev/null &)
84 | ;;
85 | stop)
86 | log_daemon_msg "Stopping keymaster-eventmond daemon" "keymaster-eventmond" || true
87 | [ -s "$LOOP_PIDFILE" ] && kill -KILL $(cat "$LOOP_PIDFILE")
88 | [ -s "$PIDFILE" ] && kill -TERM $(cat "$PIDFILE")
89 | rm -f "$LOOP_PIDFILE" "$PIDFILE"
90 | ;;
91 |
92 | reload|force-reload)
93 | $0 stop
94 | $0 start
95 | ;;
96 |
97 | restart)
98 | $0 stop
99 | $0 start
100 | ;;
101 |
102 | *)
103 | log_action_msg "Usage: /etc/init.d/keymaster-eventmond {start|stop|reload|force-reload|restart}" || true
104 | exit 1
105 | esac
106 |
107 | exit 0
108 |
--------------------------------------------------------------------------------
/lib/client/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net/http"
7 | "net/http/httptest"
8 | "os"
9 | "strings"
10 | "testing"
11 |
12 | "github.com/Symantec/Dominator/lib/log/testlogger"
13 | )
14 |
15 | const simpleValidConfigFile = `base:
16 | gen_cert_urls: "https://localhost:33443/"
17 | `
18 |
19 | const invalidConfigFileNoGenUrls = `base:
20 | `
21 |
22 | func createTempFileWithStringContent(prefix string, content string) (f *os.File, err error) {
23 | f, err = ioutil.TempFile("", prefix)
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | if _, err = f.Write([]byte(content)); err != nil {
29 | os.Remove(f.Name())
30 | return nil, err
31 | }
32 | return f, nil
33 | }
34 |
35 | func TestLoadVerifyConfigFileSuccess(t *testing.T) {
36 | tmpfile, err := createTempFileWithStringContent("test_LoadVerifyConfig", simpleValidConfigFile)
37 | if err != nil {
38 | t.Fatal(err)
39 | }
40 |
41 | defer os.Remove(tmpfile.Name()) // clean up
42 |
43 | if err := tmpfile.Close(); err != nil {
44 | t.Fatal(err)
45 | }
46 | _, err = loadVerifyConfigFile(tmpfile.Name())
47 |
48 | if err != nil {
49 | t.Fatal(err)
50 | }
51 | // TODO: validate loaded file contents
52 | }
53 |
54 | func TestLoadVerifyConfigFileFailNotYAML(t *testing.T) {
55 | tmpfile, err := createTempFileWithStringContent("test_LoadVerifyConfigFail_", "Some random string")
56 | if err != nil {
57 | t.Fatal(err)
58 | }
59 | defer os.Remove(tmpfile.Name()) // clean up
60 | if err := tmpfile.Close(); err != nil {
61 | t.Fatal(err)
62 | }
63 |
64 | _, err = loadVerifyConfigFile(tmpfile.Name())
65 | if err == nil {
66 | t.Fatal("Should have failed not a YAML file")
67 | }
68 | }
69 |
70 | func TestLoadVerifyConfigFileFailNoGenCertUrls(t *testing.T) {
71 | tmpfile, err := createTempFileWithStringContent("test_LoadVerifyConfigFail_", invalidConfigFileNoGenUrls)
72 | if err != nil {
73 | t.Fatal(err)
74 | }
75 | defer os.Remove(tmpfile.Name()) // clean up
76 | if err := tmpfile.Close(); err != nil {
77 | t.Fatal(err)
78 | }
79 |
80 | _, err = loadVerifyConfigFile(tmpfile.Name())
81 | if err == nil {
82 | t.Fatal("Should have failed no genurls in config")
83 | }
84 | }
85 |
86 | func TestLoadVerifyConfigFileFailNoSuchFile(t *testing.T) {
87 | _, err := loadVerifyConfigFile("NonExistentFile")
88 | if err == nil {
89 | t.Fatal("Success on loading Nonexistent File!")
90 | }
91 |
92 | }
93 |
94 | func TestGetConfigFromHost(t *testing.T) {
95 | ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
96 | fmt.Fprintf(w, "%s", simpleValidConfigFile)
97 | }))
98 | defer ts.Close()
99 | tsURL := ts.URL
100 | hostname := strings.TrimPrefix(tsURL, "https://")
101 |
102 | tmpfile, err := ioutil.TempFile("", "test_getConfigFromHost_")
103 | if err != nil {
104 | t.Fatal(err)
105 | }
106 |
107 | defer os.Remove(tmpfile.Name()) // clean up
108 |
109 | err = GetConfigFromHost(
110 | tmpfile.Name(),
111 | hostname,
112 | ts.Client(),
113 | testlogger.New(t))
114 | if err != nil {
115 | t.Fatal(err)
116 | }
117 |
118 | //server.netClient = ts.Client()
119 | //server.staticConfig.OpenID.TokenURL = ts.URL
120 | }
121 |
--------------------------------------------------------------------------------
/lib/client/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "crypto"
6 | "crypto/rand"
7 | "crypto/rsa"
8 | "crypto/tls"
9 | "crypto/x509"
10 | "encoding/pem"
11 | "fmt"
12 | "io/ioutil"
13 | "net/http"
14 | "net/http/cookiejar"
15 | "net/url"
16 | "os"
17 | "time"
18 |
19 | "github.com/Symantec/Dominator/lib/log"
20 | "github.com/Symantec/keymaster/lib/client/net"
21 | "github.com/howeyc/gopass"
22 | "golang.org/x/crypto/ssh"
23 | "golang.org/x/net/publicsuffix"
24 | )
25 |
26 | const rsaKeySize = 2048
27 |
28 | func getUserCreds(userName string) (password []byte, err error) {
29 | fmt.Printf("Password for %s: ", userName)
30 | password, err = gopass.GetPasswd()
31 | if err != nil {
32 | return nil, err
33 | // Handle gopass.ErrInterrupted or getch() read error
34 | }
35 | return password, nil
36 | }
37 |
38 | // mostly comes from: http://stackoverflow.com/questions/21151714/go-generate-an-ssh-public-key
39 | func genKeyPair(
40 | privateKeyPath string, identity string, logger log.Logger) (
41 | crypto.Signer, string, error) {
42 | privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
43 | if err != nil {
44 | return nil, "", err
45 | }
46 | // privateKeyPath := BasePath + prefix
47 | pubKeyPath := privateKeyPath + ".pub"
48 |
49 | err = ioutil.WriteFile(
50 | privateKeyPath,
51 | pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}),
52 | 0600)
53 | if err != nil {
54 | logger.Printf("Failed to save privkey")
55 | return nil, "", err
56 | }
57 |
58 | // generate and write public key
59 | pub, err := ssh.NewPublicKey(&privateKey.PublicKey)
60 | if err != nil {
61 | return nil, "", err
62 | }
63 | marshaledPubKeyBytes := ssh.MarshalAuthorizedKey(pub)
64 | marshaledPubKeyBytes = bytes.TrimRight(marshaledPubKeyBytes, "\r\n")
65 | var pubKeyBuffer bytes.Buffer
66 | _, err = pubKeyBuffer.Write(marshaledPubKeyBytes)
67 | if err != nil {
68 | return nil, "", err
69 | }
70 | _, err = pubKeyBuffer.Write([]byte(" " + identity + "\n"))
71 | if err != nil {
72 | return nil, "", err
73 | }
74 | return privateKey, pubKeyPath, ioutil.WriteFile(pubKeyPath, pubKeyBuffer.Bytes(), 0644)
75 | }
76 |
77 | func getHttpClient(tlsConfig *tls.Config,
78 | dialer net.Dialer) (*http.Client, error) {
79 | clientTransport := &http.Transport{
80 | TLSClientConfig: tlsConfig,
81 | DialContext: dialer.DialContext,
82 | }
83 |
84 | // proxy env variables in ascending order of preference, lower case 'http_proxy' dominates
85 | // just like curl
86 | proxyEnvVariables := []string{"HTTP_PROXY", "HTTPS_PROXY", "http_proxy"}
87 | for _, proxyVar := range proxyEnvVariables {
88 | httpProxy, err := getParseURLEnvVariable(proxyVar)
89 | if err == nil && httpProxy != nil {
90 | clientTransport.Proxy = http.ProxyURL(httpProxy)
91 | }
92 | }
93 | jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
94 | if err != nil {
95 | return nil, err
96 | }
97 |
98 | // TODO: change timeout const for a flag
99 | client := &http.Client{Transport: clientTransport, Jar: jar, Timeout: 25 * time.Second}
100 | return client, nil
101 | }
102 |
103 | func getParseURLEnvVariable(name string) (*url.URL, error) {
104 | envVariable := os.Getenv(name)
105 | if len(envVariable) < 1 {
106 | return nil, nil
107 | }
108 | envUrl, err := url.Parse(envVariable)
109 | if err != nil {
110 | return nil, err
111 | }
112 |
113 | return envUrl, nil
114 | }
115 |
--------------------------------------------------------------------------------
/lib/client/util/util_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | "io/ioutil"
7 | "net"
8 | "os"
9 | "os/user"
10 | "testing"
11 |
12 | "github.com/Symantec/Dominator/lib/log/testlogger"
13 | "github.com/Symantec/keymaster/lib/certgen"
14 | )
15 |
16 | func TestGenKeyPairSuccess(t *testing.T) {
17 | tmpfile, err := ioutil.TempFile("", "test_genKeyPair_")
18 | if err != nil {
19 | t.Fatal(err)
20 | }
21 |
22 | defer os.Remove(tmpfile.Name()) // clean up
23 |
24 | _, _, err = GenKeyPair(tmpfile.Name(), "test", testlogger.New(t))
25 | if err != nil {
26 | t.Fatal(err)
27 | }
28 | fileBytes, err := ioutil.ReadFile(tmpfile.Name())
29 | if err != nil {
30 | t.Fatal(err)
31 | }
32 | _, err = certgen.GetSignerFromPEMBytes(fileBytes)
33 | if err != nil {
34 | t.Fatal(err)
35 | }
36 | //TODO: verify written signer matches our signer.
37 | }
38 |
39 | func TestGenKeyPairFailNoPerms(t *testing.T) {
40 | _, _, err := GenKeyPair("/proc/something", "test", testlogger.New(t))
41 | if err == nil {
42 | t.Logf("Should have failed")
43 | t.Fatal(err)
44 | }
45 | }
46 |
47 | func TestGetUserHomeDirSuccess(t *testing.T) {
48 | usr, err := user.Current()
49 | if err != nil {
50 | t.Logf("cannot get current user info")
51 | t.Fatal(err)
52 | }
53 | homeDir, err := GetUserHomeDir(usr)
54 | if err != nil {
55 | t.Fatal(err)
56 | }
57 | if len(homeDir) < 1 {
58 | t.Fatal("invalid homedir")
59 |
60 | }
61 | }
62 |
63 | func TestGetParseURLEnvVariable(t *testing.T) {
64 | testName := "TEST_ENV_KEYMASTER_11111"
65 | os.Setenv(testName, "http://localhost:12345")
66 | val, err := getParseURLEnvVariable(testName)
67 | if err != nil {
68 | t.Fatal(err)
69 | }
70 | if val == nil {
71 | t.Fatal("Should have found value")
72 | }
73 |
74 | //Not a URL
75 | /*
76 | os.Setenv(testName, "")
77 | if err == nil {
78 | t.Fatal("should have failed to parse")
79 | }
80 | */
81 |
82 | //Unexistent
83 | // TODO: check for the return error
84 | val, _ = getParseURLEnvVariable("Foobar")
85 | if val != nil {
86 | t.Fatal("SHOULD not have found anything ")
87 | }
88 | //
89 |
90 | }
91 |
92 | // ------------WARN-------- Next name copied from https://github.com/howeyc/gopass/blob/master/pass_test.go for using
93 | // gopass checks
94 | func TestPipe(t *testing.T) {
95 | _, err := pipeToStdin("password\n")
96 | if err != nil {
97 | t.Fatal(err)
98 | }
99 | password, err := GetUserCreds("userame")
100 | if err != nil {
101 | t.Fatal(err)
102 | }
103 | if string(password) != "password" {
104 | t.Fatal("password Does NOT match")
105 | }
106 |
107 | }
108 |
109 | // ------------WARN--------------
110 | // THE next two functions are litierly copied from: https://github.com/howeyc/gopass/blob/master/pass_test.go
111 | // pipeToStdin pipes the given string onto os.Stdin by replacing it with an
112 | // os.Pipe. The write end of the pipe is closed so that EOF is read after the
113 | // final byte.
114 | func pipeToStdin(s string) (int, error) {
115 | pipeReader, pipeWriter, err := os.Pipe()
116 | if err != nil {
117 | fmt.Println("Error getting os pipes:", err)
118 | os.Exit(1)
119 | }
120 | os.Stdin = pipeReader
121 | w, err := pipeWriter.WriteString(s)
122 | pipeWriter.Close()
123 | return w, err
124 | }
125 |
126 | func TestGetHttpClientMinimal(t *testing.T) {
127 | tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
128 | _, err := GetHttpClient(tlsConfig, &net.Dialer{})
129 | if err != nil {
130 | t.Fatal(err)
131 | }
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/lib/pwauth/okta/okta_test.go:
--------------------------------------------------------------------------------
1 | package okta
2 |
3 | import (
4 | "encoding/json"
5 | "net"
6 | "net/http"
7 | "testing"
8 | "time"
9 | )
10 |
11 | var authnURL string
12 |
13 | func authnHandler(w http.ResponseWriter, req *http.Request) {
14 | if req.Method != http.MethodPost {
15 | w.WriteHeader(http.StatusMethodNotAllowed)
16 | return
17 | }
18 | var loginData loginDataType
19 | decoder := json.NewDecoder(req.Body)
20 | if err := decoder.Decode(&loginData); err != nil {
21 | w.WriteHeader(http.StatusBadRequest)
22 | return
23 | }
24 | if loginData.Username != "a-user" {
25 | w.WriteHeader(http.StatusUnauthorized)
26 | return
27 | }
28 | switch loginData.Password {
29 | case "good-password":
30 | writeStatus(w, "SUCCESS")
31 | return
32 | case "needs-2FA":
33 | writeStatus(w, "MFA_REQUIRED")
34 | return
35 | case "password-expired":
36 | writeStatus(w, "PASSWORD_EXPIRED")
37 | return
38 | default:
39 | w.WriteHeader(http.StatusUnauthorized)
40 | return
41 | }
42 | }
43 |
44 | func setupServer() {
45 | if authnURL != "" {
46 | return
47 | }
48 | if listener, err := net.Listen("tcp", ""); err != nil {
49 | panic(err)
50 | } else {
51 | addr := listener.Addr().String()
52 | authnURL = "http://" + addr + authPath
53 | serveMux := http.NewServeMux()
54 | serveMux.HandleFunc(authPath, authnHandler)
55 | go http.Serve(listener, serveMux)
56 | for {
57 | if conn, err := net.Dial("tcp", addr); err == nil {
58 | conn.Close()
59 | break
60 | }
61 | time.Sleep(time.Millisecond * 10)
62 | }
63 | return
64 | }
65 | }
66 |
67 | func writeStatus(w http.ResponseWriter, status string) {
68 | encoder := json.NewEncoder(w)
69 | encoder.SetIndent("", " ") // Make life easier for debugging.
70 | if err := encoder.Encode(responseType{status}); err != nil {
71 | w.WriteHeader(http.StatusInternalServerError)
72 | }
73 | }
74 |
75 | func TestNonExistantUser(t *testing.T) {
76 | setupServer()
77 | pa := &PasswordAuthenticator{authnURL: authnURL}
78 | ok, err := pa.PasswordAuthenticate("bad-user", []byte("dummy-password"))
79 | if err != nil {
80 | t.Fatalf("unpexpected error: %s", err)
81 | } else if ok {
82 | t.Fatalf("non-existant user did not fail")
83 | }
84 | }
85 |
86 | func TestBadPassword(t *testing.T) {
87 | setupServer()
88 | pa := &PasswordAuthenticator{authnURL: authnURL}
89 | ok, err := pa.PasswordAuthenticate("a-user", []byte("bad-password"))
90 | if err != nil {
91 | t.Fatalf("unpexpected error: %s", err)
92 | } else if ok {
93 | t.Fatalf("bad password did not fail")
94 | }
95 | }
96 |
97 | func TestGoodPassword(t *testing.T) {
98 | setupServer()
99 | pa := &PasswordAuthenticator{authnURL: authnURL}
100 | ok, err := pa.PasswordAuthenticate("a-user", []byte("good-password"))
101 | if err != nil {
102 | t.Fatalf("unpexpected error: %s", err)
103 | } else if !ok {
104 | t.Fatalf("good password failed")
105 | }
106 | }
107 |
108 | func TestMfaRequired(t *testing.T) {
109 | setupServer()
110 | pa := &PasswordAuthenticator{authnURL: authnURL}
111 | ok, err := pa.PasswordAuthenticate("a-user", []byte("needs-2FA"))
112 | if err != nil {
113 | t.Fatalf("unpexpected error: %s", err)
114 | } else if !ok {
115 | t.Fatalf("good password needing 2FA failed")
116 | }
117 | }
118 |
119 | func TestUserLockedOut(t *testing.T) {
120 | setupServer()
121 | pa := &PasswordAuthenticator{authnURL: authnURL}
122 | ok, err := pa.PasswordAuthenticate("a-user", []byte("password-expired"))
123 | if err != nil {
124 | t.Fatalf("unpexpected error: %s", err)
125 | } else if ok {
126 | t.Fatalf("expired password suceeded")
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/keymaster.spec:
--------------------------------------------------------------------------------
1 | Name: keymaster
2 | Version: 1.7.0
3 | Release: 1%{?dist}
4 | Summary: Short term access certificate generator and client
5 |
6 | #Group:
7 | License: ASL 2.0
8 | URL: https://github.com/cviecco/simple-cloud-encrypt/
9 | Source0: keymaster-%{version}.tar.gz
10 |
11 | #BuildRequires: golang
12 | #Requires:
13 | Requires(pre): /usr/sbin/useradd, /usr/bin/getent
14 | Requires(postun): /usr/sbin/userdel
15 |
16 | #no debug package as this is go
17 | %define debug_package %{nil}
18 |
19 | %description
20 | Simple utilites for checking state of ldap infrastructure
21 |
22 |
23 | %prep
24 | %setup -n %{name}-%{version}
25 |
26 |
27 | %build
28 | make
29 |
30 |
31 | %install
32 | #%make_install
33 | %{__install} -Dp -m0755 ~/go/bin/keymasterd %{buildroot}%{_sbindir}/keymasterd
34 | %{__install} -Dp -m0755 ~/go/bin/keymaster %{buildroot}%{_bindir}/keymaster
35 | %{__install} -Dp -m0755 ~/go/bin/keymaster-unlocker %{buildroot}%{_bindir}/keymaster-unlocker
36 | install -d %{buildroot}/usr/lib/systemd/system
37 | install -p -m 0644 misc/startup/keymaster.service %{buildroot}/usr/lib/systemd/system/keymaster.service
38 | install -d %{buildroot}/%{_datarootdir}/keymasterd/static_files/
39 | install -p -m 0644 cmd/keymasterd/static_files/u2f-api.js %{buildroot}/%{_datarootdir}/keymasterd/static_files/u2f-api.js
40 | install -p -m 0644 cmd/keymasterd/static_files/keymaster-u2f.js %{buildroot}/%{_datarootdir}/keymasterd/static_files/keymaster-u2f.js
41 | install -p -m 0644 cmd/keymasterd/static_files/webui-2fa-u2f.js %{buildroot}/%{_datarootdir}/keymasterd/static_files/webui-2fa-u2f.js
42 | install -p -m 0644 cmd/keymasterd/static_files/webui-2fa-symc-vip.js %{buildroot}/%{_datarootdir}/keymasterd/static_files/webui-2fa-symc-vip.js
43 | install -p -m 0644 cmd/keymasterd/static_files/keymaster.css %{buildroot}/%{_datarootdir}/keymasterd/static_files/keymaster.css
44 | install -p -m 0644 cmd/keymasterd/static_files/jquery-3.4.1.min.js %{buildroot}/%{_datarootdir}/keymasterd/static_files/jquery-3.4.1.min.js
45 | install -p -m 0644 cmd/keymasterd/static_files/favicon.ico %{buildroot}/%{_datarootdir}/keymasterd/static_files/favicon.ico
46 | install -d %{buildroot}/%{_datarootdir}/keymasterd/customization_data/templates
47 | install -p -m 0644 cmd/keymasterd/customization_data/templates/header_extra.tmpl %{buildroot}/%{_datarootdir}/keymasterd/customization_data/templates/header_extra.tmpl
48 | install -p -m 0644 cmd/keymasterd/customization_data/templates/footer_extra.tmpl %{buildroot}/%{_datarootdir}/keymasterd/customization_data/templates/footer_extra.tmpl
49 | install -p -m 0644 cmd/keymasterd/customization_data/templates/login_extra.tmpl %{buildroot}/%{_datarootdir}/keymasterd/customization_data/templates/login_extra.tmpl
50 | install -d %{buildroot}/%{_datarootdir}/keymasterd/customization_data/web_resources
51 | install -p -m 0644 cmd/keymasterd/customization_data/web_resources/customization.css %{buildroot}/%{_datarootdir}/keymasterd/customization_data/web_resources/customization.css
52 | %pre
53 | /usr/bin/getent passwd keymaster || useradd -d /var/lib/keymaster -s /bin/false -U -r keymaster
54 |
55 | %post
56 | mkdir -p /etc/keymaster/
57 | mkdir -p /var/lib/keymaster
58 | chown keymaster /var/lib/keymaster
59 | systemctl daemon-reload
60 |
61 | %postun
62 | /usr/sbin/userdel keymaster
63 | systemctl daemon-reload
64 |
65 | %files
66 | #%doc
67 | %{_sbindir}/keymasterd
68 | %{_bindir}/keymaster
69 | %{_bindir}/keymaster-unlocker
70 | /usr/lib/systemd/system/keymaster.service
71 | %{_datarootdir}/keymasterd/static_files/*
72 | %config(noreplace) %{_datarootdir}/keymasterd/customization_data/web_resources/*
73 | %config(noreplace) %{_datarootdir}/keymasterd/customization_data/templates/*
74 | %changelog
75 |
76 |
77 |
--------------------------------------------------------------------------------
/cmd/keymasterd/dependency_monitor.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/x509"
5 | "errors"
6 | "strings"
7 | "time"
8 |
9 | "github.com/Symantec/keymaster/lib/authutil"
10 |
11 | "github.com/Symantec/tricorder/go/tricorder"
12 | "github.com/Symantec/tricorder/go/tricorder/units"
13 | "github.com/prometheus/client_golang/prometheus"
14 | )
15 |
16 | var (
17 | dependencyLatency = prometheus.NewSummaryVec(
18 | prometheus.SummaryOpts{
19 | Name: "keymaster_dependency_check_duration_seconds",
20 | Help: "Dependency latency.",
21 | Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
22 | },
23 | []string{"type", "name", "target"},
24 | )
25 | dependencyLastSuccessSecondsGauge = prometheus.NewGaugeVec(
26 | prometheus.GaugeOpts{
27 | Name: "keymaster_dependency_durations_since_last_success_seconds",
28 | Help: "Seconds since last update",
29 | },
30 | []string{"type", "name"},
31 | )
32 | lastSuccessLDAPPasswordTime time.Time
33 | lastSuccessLDAPUserInfoTime time.Time
34 | )
35 |
36 | const timeoutSecs = 5
37 |
38 | func init() {
39 | prometheus.MustRegister(dependencyLatency)
40 | prometheus.MustRegister(dependencyLastSuccessSecondsGauge)
41 | tricorder.RegisterMetric(
42 | "keymaster/dependency_status/LDAP/PasswordDurationSinceLastSuccessfulCheck",
43 | func() time.Duration {
44 | return time.Now().Sub(lastSuccessLDAPPasswordTime)
45 | },
46 | units.Second,
47 | "Time since last successful LDAP check for Password(s)")
48 | tricorder.RegisterMetric(
49 | "keymaster/dependency_status/LDAP/UserinfoDurationSinceLastSuccessfulCheck",
50 | func() time.Duration {
51 | return time.Now().Sub(lastSuccessLDAPUserInfoTime)
52 | },
53 | units.Second,
54 | "Time since last successful LDAP check for UserInfo(s)")
55 | }
56 |
57 | func checkLDAPURLs(ldapURLs string, name string, rootCAs *x509.CertPool) error {
58 | if len(ldapURLs) <= 0 {
59 | return errors.New("No data to check")
60 | }
61 | urlList := strings.Split(ldapURLs, ",")
62 | for _, stringURL := range urlList {
63 | url, err := authutil.ParseLDAPURL(stringURL)
64 | if err != nil {
65 | return err
66 | }
67 | startTime := time.Now()
68 | err = authutil.CheckLDAPConnection(*url, timeoutSecs, rootCAs)
69 | if err != nil {
70 | continue
71 | }
72 | dependencyLatency.WithLabelValues("ldap", name, stringURL).Observe(time.Now().Sub(startTime).Seconds())
73 | return nil
74 | }
75 | return errors.New("Check Failed")
76 | }
77 |
78 | func checkLDAPConfigs(config AppConfigFile, rootCAs *x509.CertPool) {
79 | if len(config.Ldap.LDAPTargetURLs) > 0 {
80 | err := checkLDAPURLs(config.Ldap.LDAPTargetURLs, "passwd", rootCAs)
81 | if err != nil {
82 | logger.Debugf(1, "password LDAP check Failed %s", err)
83 | } else {
84 | lastSuccessLDAPPasswordTime = time.Now()
85 | }
86 | dependencyLastSuccessSecondsGauge.WithLabelValues("ldap", "passwd").
87 | Set(time.Now().Sub(lastSuccessLDAPPasswordTime).Seconds())
88 | }
89 | ldapConfig := config.UserInfo.Ldap
90 | if len(ldapConfig.LDAPTargetURLs) > 0 {
91 | err := checkLDAPURLs(ldapConfig.LDAPTargetURLs, "userinfo", rootCAs)
92 | if err != nil {
93 | logger.Debugf(1, "userinfo LDAP check Failed %s", err)
94 | } else {
95 | lastSuccessLDAPUserInfoTime = time.Now()
96 | }
97 | dependencyLastSuccessSecondsGauge.WithLabelValues("ldap", "userinfo").
98 | Set(time.Now().Sub(lastSuccessLDAPUserInfoTime).Seconds())
99 | }
100 | }
101 |
102 | func (state *RuntimeState) doDependencyMonitoring(secsBetweenChecks int) {
103 | for {
104 | checkLDAPConfigs(state.Config, nil)
105 | time.Sleep(time.Duration(secsBetweenChecks) * time.Second)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/lib/pwauth/ldap/impl.go:
--------------------------------------------------------------------------------
1 | package ldap
2 |
3 | import (
4 | "crypto/x509"
5 | "errors"
6 | "fmt"
7 | "time"
8 |
9 | "github.com/Symantec/Dominator/lib/log"
10 | "github.com/Symantec/keymaster/lib/authutil"
11 | "github.com/Symantec/keymaster/lib/simplestorage"
12 | )
13 |
14 | const defaultCacheDuration = time.Hour * 96
15 | const passwordDataType = 1
16 | const browserResponseTimeoutSeconds = 7
17 |
18 | func newAuthenticator(urllist []string, bindPattern []string,
19 | timeoutSecs uint, rootCAs *x509.CertPool,
20 | storage simplestorage.SimpleStore, logger log.DebugLogger) (
21 | *PasswordAuthenticator, error) {
22 | var authenticator PasswordAuthenticator
23 | for _, stringURL := range urllist {
24 | url, err := authutil.ParseLDAPURL(stringURL)
25 | if err != nil {
26 | return nil, err
27 | }
28 | authenticator.ldapURL = append(authenticator.ldapURL, url)
29 | }
30 | authenticator.bindPattern = bindPattern
31 | authenticator.timeoutSecs = timeoutSecs
32 | if timeoutSecs*uint(len(authenticator.ldapURL)) > uint(browserResponseTimeoutSeconds) {
33 | authenticator.timeoutSecs = uint(browserResponseTimeoutSeconds) / uint(len(authenticator.ldapURL))
34 | }
35 | authenticator.rootCAs = rootCAs
36 | authenticator.logger = logger
37 | authenticator.expirationDuration = defaultCacheDuration
38 | authenticator.storage = storage
39 | authenticator.cachedCredentials = make(map[string]cacheCredentialEntry)
40 | return &authenticator, nil
41 | }
42 |
43 | func convertToBindDN(username string, bind_pattern string) string {
44 | return fmt.Sprintf(bind_pattern, username)
45 | }
46 |
47 | func (pa *PasswordAuthenticator) updateOrDeletePasswordHash(valid bool, username string, password []byte) error {
48 | if pa.storage == nil {
49 | return errors.New("No db for updating password")
50 | }
51 |
52 | if valid {
53 | hash, err := authutil.Argon2MakeNewHash(password)
54 | if err != nil {
55 | if pa.logger != nil {
56 | pa.logger.Debugf(0, "Failure making new hash for password for user %s", username)
57 | }
58 | return nil
59 | }
60 | Expiration := time.Now().Add(pa.expirationDuration)
61 | err = pa.storage.UpsertSigned(username, passwordDataType, Expiration.Unix(), hash)
62 | if err != nil && pa.logger != nil {
63 | pa.logger.Debugf(0, "Failure inserting password into db for user %s", username)
64 | }
65 | return err
66 |
67 | } else {
68 | ok, hash, err := pa.storage.GetSigned(username, passwordDataType)
69 | if err != nil {
70 | return nil
71 | }
72 | if ok {
73 | err := authutil.Argon2CompareHashAndPassword(hash, password)
74 | if err == nil {
75 | pa.storage.DeleteSigned(username, passwordDataType)
76 | }
77 | }
78 | }
79 | return nil
80 | }
81 |
82 | func (pa *PasswordAuthenticator) passwordAuthenticate(username string,
83 | password []byte) (valid bool, err error) {
84 | valid = false
85 | for _, u := range pa.ldapURL {
86 | for _, bindPattern := range pa.bindPattern {
87 | bindDN := convertToBindDN(username, bindPattern)
88 | valid, err = authutil.CheckLDAPUserPassword(*u, bindDN, string(password), pa.timeoutSecs, pa.rootCAs)
89 | if err != nil {
90 | if pa.logger != nil {
91 | pa.logger.Debugf(1, "Error checking LDAP user password url= %s", u)
92 | }
93 | continue
94 | }
95 | err = pa.updateOrDeletePasswordHash(valid, username, password)
96 | if err != nil && pa.logger != nil {
97 | pa.logger.Debugf(0, "Updating local password hash for user %s", username)
98 | }
99 | return valid, nil
100 |
101 | }
102 | }
103 | if pa.storage != nil {
104 | if pa.logger != nil {
105 | pa.logger.Printf("Failed to check password against LDAP servers, using local hash db")
106 | }
107 | ok, hash, err := pa.storage.GetSigned(username, passwordDataType)
108 | if err != nil {
109 | return false, nil
110 | }
111 | if ok {
112 | err = authutil.Argon2CompareHashAndPassword(hash, password)
113 | if err == nil {
114 | return true, nil
115 | }
116 | }
117 |
118 | }
119 |
120 | return false, nil
121 | }
122 |
--------------------------------------------------------------------------------
/cmd/keymaster-eventmond/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 | "path"
7 | "text/template"
8 |
9 | "github.com/Symantec/Dominator/lib/log/serverlogger"
10 | "github.com/Symantec/keymaster/eventmon/eventrecorder"
11 | "github.com/Symantec/keymaster/eventmon/httpd"
12 | "github.com/Symantec/keymaster/eventmon/monitord"
13 | "github.com/Symantec/keymaster/lib/constants"
14 | "github.com/Symantec/keymaster/proto/eventmon"
15 | "github.com/Symantec/tricorder/go/tricorder"
16 | )
17 |
18 | var (
19 | configFile = flag.String("configFile",
20 | constants.DefaultKeymasterEventmonConfigFile, "Configuration file")
21 | portNum = flag.Uint("portNum", constants.DefaultEventmonPortNumber,
22 | "Port number to allocate and listed on for HTTP/RPC")
23 | stateDir = flag.String("stateDir",
24 | constants.DefaultKeymasterEventmonStateDir, "Saved state directory")
25 | )
26 |
27 | type configurationType struct {
28 | KeymasterServerHostname string `yaml:"keymaster_server_hostname"`
29 | KeymasterServerPortNum uint `yaml:"keymaster_server_port_num"`
30 | SshCertParametersCommand certCommand `yaml:"ssh_cert_parameters_command"`
31 | SshCertRawCommand string `yaml:"ssh_cert_raw_command"`
32 | X509CertParametersCommand certCommand `yaml:"x509_cert_parameters_command"`
33 | X509CertRawCommand string `yaml:"x509_cert_raw_command"`
34 | }
35 |
36 | type certCommand struct {
37 | Command string `yaml:"command"`
38 | Parameters []string `yaml:"parameters"`
39 | templates []*template.Template
40 | }
41 |
42 | func main() {
43 | flag.Parse()
44 | tricorder.RegisterFlags()
45 | logger := serverlogger.NewWithFlags("", log.LstdFlags|log.Lmicroseconds)
46 | configuration, err := loadConfig(*configFile)
47 | if err != nil {
48 | logger.Fatalf("Cannot load configuration: %s\n", err)
49 | }
50 | recorder, err := eventrecorder.New(path.Join(*stateDir, "events.gob"),
51 | logger)
52 | if err != nil {
53 | logger.Fatalf("Cannot start event recorder: %s\n", err)
54 | }
55 | monitor, err := monitord.New(configuration.KeymasterServerHostname,
56 | configuration.KeymasterServerPortNum, logger)
57 | if err != nil {
58 | logger.Fatalf("Cannot start monitor: %s\n", err)
59 | }
60 | httpd.AddHtmlWriter(monitor)
61 | httpd.AddHtmlWriter(logger)
62 | if err = httpd.StartServer(*portNum, recorder, monitor, true); err != nil {
63 | logger.Fatalf("Unable to create http server: %s\n", err)
64 | }
65 | for {
66 | select {
67 | case auth := <-monitor.AuthChannel:
68 | data := &eventrecorder.AuthInfo{Username: auth.Username}
69 | switch auth.AuthType {
70 | case eventmon.AuthTypePassword:
71 | data.AuthType = eventrecorder.AuthTypePassword
72 | case eventmon.AuthTypeSymantecVIP:
73 | data.AuthType = eventrecorder.AuthTypeSymantecVIP
74 | if auth.VIPAuthType == eventmon.VIPAuthTypeOTP {
75 | data.VIPAuthType = eventrecorder.VIPAuthTypeOTP
76 | } else if auth.VIPAuthType == eventmon.VIPAuthTypePush {
77 | data.VIPAuthType = eventrecorder.VIPAuthTypePush
78 | }
79 | case eventmon.AuthTypeU2F:
80 | data.AuthType = eventrecorder.AuthTypeU2F
81 | default:
82 | continue
83 | }
84 | recorder.AuthChannel <- data
85 | case spLogin := <-monitor.ServiceProviderLoginChannel:
86 | data := &eventrecorder.SPLoginInfo{
87 | URL: spLogin.URL,
88 | Username: spLogin.Username,
89 | }
90 | recorder.ServiceProviderLoginChannel <- data
91 | case cert := <-monitor.SshCertChannel:
92 | recorder.SshCertChannel <- cert
93 | configuration.SshCertParametersCommand.processSshCert(cert)
94 | case cert := <-monitor.SshRawCertChannel:
95 | processRawCert(configuration.SshCertRawCommand, cert)
96 | case username := <-monitor.WebLoginChannel:
97 | recorder.WebLoginChannel <- username
98 | case cert := <-monitor.X509CertChannel:
99 | recorder.X509CertChannel <- cert
100 | configuration.X509CertParametersCommand.processX509Cert(cert)
101 | case cert := <-monitor.X509RawCertChannel:
102 | processRawCert(configuration.X509CertRawCommand, cert)
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/keymasterd/eventnotifier/impl.go:
--------------------------------------------------------------------------------
1 | package eventnotifier
2 |
3 | import (
4 | "bufio"
5 | "encoding/json"
6 | "io"
7 | "net"
8 | "net/http"
9 | "time"
10 |
11 | "github.com/Symantec/Dominator/lib/log"
12 | "github.com/Symantec/keymaster/proto/eventmon"
13 | )
14 |
15 | const (
16 | bufferLength = 16
17 | )
18 |
19 | func newEventNotifier(logger log.DebugLogger) *EventNotifier {
20 | channels := make(map[chan<- eventmon.EventV0]chan<- eventmon.EventV0)
21 | return &EventNotifier{
22 | logger: logger,
23 | transmitChannels: channels,
24 | }
25 | }
26 |
27 | func (n *EventNotifier) publishAuthEvent(authType, username string) {
28 | transmitData := eventmon.EventV0{
29 | Type: eventmon.EventTypeAuth,
30 | AuthType: authType,
31 | Username: username,
32 | }
33 | n.transmitEvent(transmitData)
34 | }
35 |
36 | func (n *EventNotifier) publishCert(certType string, certData []byte) {
37 | transmitData := eventmon.EventV0{Type: certType, CertData: certData}
38 | n.mutex.Lock()
39 | defer n.mutex.Unlock()
40 | for ch := range n.transmitChannels {
41 | select {
42 | case ch <- transmitData:
43 | default:
44 | }
45 | }
46 | }
47 |
48 | func (n *EventNotifier) publishServiceProviderLoginEvent(url, username string) {
49 | transmitData := eventmon.EventV0{
50 | Type: eventmon.EventTypeServiceProviderLogin,
51 | ServiceProviderUrl: url,
52 | Username: username,
53 | }
54 | n.transmitEvent(transmitData)
55 | }
56 |
57 | func (n *EventNotifier) publishWebLoginEvent(username string) {
58 | transmitData := eventmon.EventV0{
59 | Type: eventmon.EventTypeWebLogin,
60 | Username: username,
61 | }
62 | n.transmitEvent(transmitData)
63 | }
64 |
65 | func (n *EventNotifier) publishVIPAuthEvent(vipAuthType, username string) {
66 | transmitData := eventmon.EventV0{
67 | Type: eventmon.EventTypeAuth,
68 | AuthType: eventmon.AuthTypeSymantecVIP,
69 | VIPAuthType: vipAuthType,
70 | Username: username,
71 | }
72 | n.transmitEvent(transmitData)
73 | }
74 |
75 | func (n *EventNotifier) serveHTTP(w http.ResponseWriter, req *http.Request) {
76 | if req.Method != "CONNECT" {
77 | w.Header().Set("Content-Type", "text/plain; charset=utf-8")
78 | w.WriteHeader(http.StatusMethodNotAllowed)
79 | return
80 | }
81 | hijacker, ok := w.(http.Hijacker)
82 | if !ok {
83 | w.Header().Set("Content-Type", "text/plain; charset=utf-8")
84 | w.WriteHeader(http.StatusInternalServerError)
85 | return
86 | }
87 | conn, bufRw, err := hijacker.Hijack()
88 | if err != nil {
89 | n.logger.Println("eventmon hijacking ", req.RemoteAddr, ": ",
90 | err.Error())
91 | w.Header().Set("Content-Type", "text/plain; charset=utf-8")
92 | w.WriteHeader(http.StatusInternalServerError)
93 | return
94 | }
95 | defer conn.Close()
96 | defer bufRw.Flush()
97 | if tcpConn, ok := conn.(*net.TCPConn); ok {
98 | if err := tcpConn.SetKeepAlive(true); err != nil {
99 | n.logger.Println("error setting keepalive: ", err.Error())
100 | return
101 | }
102 | if err := tcpConn.SetKeepAlivePeriod(time.Minute * 5); err != nil {
103 | n.logger.Println("error setting keepalive period: ",
104 | err.Error())
105 | return
106 | }
107 | }
108 | _, err = io.WriteString(conn, "HTTP/1.0 "+eventmon.ConnectString+"\n\n")
109 | if err != nil {
110 | n.logger.Println("error writing connect message: ", err.Error())
111 | return
112 | }
113 | n.logger.Println("eventmon client connected")
114 | n.handleConnection(bufRw)
115 | }
116 |
117 | func (n *EventNotifier) handleConnection(rw *bufio.ReadWriter) {
118 | transmitChannel := make(chan eventmon.EventV0, bufferLength)
119 | closeChannel := getCloseNotifier(rw)
120 | n.mutex.Lock()
121 | n.transmitChannels[transmitChannel] = transmitChannel
122 | n.mutex.Unlock()
123 | defer func() {
124 | n.mutex.Lock()
125 | delete(n.transmitChannels, transmitChannel)
126 | n.mutex.Unlock()
127 | }()
128 | for {
129 | select {
130 | case transmitData := <-transmitChannel:
131 | if err := transmitV0(rw, transmitData); err != nil {
132 | n.logger.Println(err)
133 | return
134 | }
135 | case err := <-closeChannel:
136 | if err == io.EOF {
137 | n.logger.Println("eventmon client disconnected")
138 | return
139 | }
140 | n.logger.Println(err)
141 | return
142 | }
143 | if err := rw.Flush(); err != nil {
144 | n.logger.Println(err)
145 | return
146 | }
147 | }
148 | }
149 |
150 | func (n *EventNotifier) transmitEvent(event eventmon.EventV0) {
151 | n.mutex.Lock()
152 | defer n.mutex.Unlock()
153 | for ch := range n.transmitChannels {
154 | select {
155 | case ch <- event:
156 | default:
157 | }
158 | }
159 | }
160 |
161 | func transmitV0(writer io.Writer, event eventmon.EventV0) error {
162 | encoder := json.NewEncoder(writer)
163 | encoder.SetIndent("", " ")
164 | return encoder.Encode(event)
165 | }
166 |
167 | func getCloseNotifier(reader io.Reader) <-chan error {
168 | closeChannel := make(chan error)
169 | go func() {
170 | for {
171 | buf := make([]byte, 1)
172 | if _, err := reader.Read(buf); err != nil {
173 | closeChannel <- err
174 | return
175 | }
176 | }
177 | }()
178 | return closeChannel
179 | }
180 |
--------------------------------------------------------------------------------
/cmd/keymasterd/jwt.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto"
5 | "crypto/sha256"
6 | "errors"
7 | "fmt"
8 | "time"
9 |
10 | "golang.org/x/crypto/ssh"
11 | "gopkg.in/square/go-jose.v2"
12 | "gopkg.in/square/go-jose.v2/jwt"
13 | )
14 |
15 | // This actually gets the SSH key fingerprint
16 | func getKeyFingerprint(key crypto.PublicKey) (string, error) {
17 | sshPublicKey, err := ssh.NewPublicKey(key)
18 | if err != nil {
19 | return "", err
20 | }
21 | h := sha256.New()
22 | h.Write(sshPublicKey.Marshal())
23 | fp := fmt.Sprintf("%x", h.Sum(nil))
24 | return fp, nil
25 | }
26 |
27 | func (state *RuntimeState) idpGetIssuer() string {
28 | issuer := "https://" + state.HostIdentity
29 | if state.Config.Base.HttpAddress != ":443" {
30 | issuer = issuer + state.Config.Base.HttpAddress
31 | }
32 | return issuer
33 | }
34 |
35 | func (state *RuntimeState) JWTClaims(t *jwt.JSONWebToken, dest ...interface{}) (err error) {
36 | for _, key := range state.KeymasterPublicKeys {
37 | err = t.Claims(key, dest...)
38 | if err == nil {
39 | return nil
40 | }
41 | }
42 | if err != nil {
43 | return err
44 | }
45 | err = errors.New("No valid key found")
46 | return err
47 | }
48 |
49 | func (state *RuntimeState) genNewSerializedAuthJWT(username string, authLevel int) (string, error) {
50 | signerOptions := (&jose.SignerOptions{}).WithType("JWT")
51 | signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: state.Signer}, signerOptions)
52 | if err != nil {
53 | return "", err
54 | }
55 | issuer := state.idpGetIssuer()
56 | authToken := authInfoJWT{Issuer: issuer, Subject: username,
57 | Audience: []string{issuer}, AuthType: authLevel, TokenType: "keymaster_auth"}
58 | authToken.NotBefore = time.Now().Unix()
59 | authToken.IssuedAt = authToken.NotBefore
60 | authToken.Expiration = authToken.IssuedAt + maxAgeSecondsAuthCookie // TODO seek the actual duration
61 |
62 | return jwt.Signed(signer).Claims(authToken).CompactSerialize()
63 | }
64 |
65 | func (state *RuntimeState) getAuthInfoFromAuthJWT(serializedToken string) (rvalue authInfo, err error) {
66 | tok, err := jwt.ParseSigned(serializedToken)
67 | if err != nil {
68 | return rvalue, err
69 | }
70 | inboundJWT := authInfoJWT{}
71 | if err := state.JWTClaims(tok, &inboundJWT); err != nil {
72 | logger.Printf("err=%s", err)
73 | return rvalue, err
74 | }
75 | //At this stage is now crypto verified, now is time to verify sane values
76 | issuer := state.idpGetIssuer()
77 | if inboundJWT.Issuer != issuer || inboundJWT.TokenType != "keymaster_auth" ||
78 | inboundJWT.NotBefore > time.Now().Unix() {
79 | err = errors.New("invalid JWT values")
80 | return rvalue, err
81 | }
82 |
83 | rvalue.Username = inboundJWT.Subject
84 | rvalue.AuthType = inboundJWT.AuthType
85 | rvalue.ExpiresAt = time.Unix(inboundJWT.Expiration, 0)
86 | return rvalue, nil
87 | }
88 |
89 | func (state *RuntimeState) updateAuthJWTWithNewAuthLevel(intoken string, newAuthLevel int) (string, error) {
90 | signerOptions := (&jose.SignerOptions{}).WithType("JWT")
91 | signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: state.Signer}, signerOptions)
92 | if err != nil {
93 | return "", err
94 | }
95 |
96 | tok, err := jwt.ParseSigned(intoken)
97 | if err != nil {
98 | return "", err
99 | }
100 | parsedJWT := authInfoJWT{}
101 | if err := state.JWTClaims(tok, &parsedJWT); err != nil {
102 | logger.Printf("err=%s", err)
103 | return "", err
104 | }
105 | issuer := state.idpGetIssuer()
106 | if parsedJWT.Issuer != issuer || parsedJWT.TokenType != "keymaster_auth" ||
107 | parsedJWT.NotBefore > time.Now().Unix() {
108 | err = errors.New("invalid JWT values")
109 | return "", err
110 | }
111 | parsedJWT.AuthType = newAuthLevel
112 | return jwt.Signed(signer).Claims(parsedJWT).CompactSerialize()
113 | }
114 |
115 | func (state *RuntimeState) genNewSerializedStorageStringDataJWT(username string, dataType int, data string, expiration int64) (string, error) {
116 | signerOptions := (&jose.SignerOptions{}).WithType("JWT")
117 | signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: state.Signer}, signerOptions)
118 | if err != nil {
119 | return "", err
120 | }
121 | issuer := state.idpGetIssuer()
122 | storageToken := storageStringDataJWT{Issuer: issuer, Subject: username,
123 | Audience: []string{issuer}, DataType: dataType,
124 | TokenType: "storage_data", Data: data}
125 | storageToken.NotBefore = time.Now().Unix()
126 | storageToken.IssuedAt = storageToken.NotBefore
127 | storageToken.Expiration = expiration
128 |
129 | return jwt.Signed(signer).Claims(storageToken).CompactSerialize()
130 | }
131 |
132 | func (state *RuntimeState) getStorageDataFromStorageStringDataJWT(serializedToken string) (rvalue storageStringDataJWT, err error) {
133 | tok, err := jwt.ParseSigned(serializedToken)
134 | if err != nil {
135 | return rvalue, err
136 | }
137 | inboundJWT := storageStringDataJWT{}
138 | if err := state.JWTClaims(tok, &inboundJWT); err != nil {
139 | logger.Printf("err=%s", err)
140 | return rvalue, err
141 | }
142 | // At this stage crypto has been verified (data actually comes from a valid signer),
143 | // Now is time to do semantic validation
144 | issuer := state.idpGetIssuer()
145 | if inboundJWT.Issuer != issuer || inboundJWT.TokenType != "storage_data" ||
146 | inboundJWT.NotBefore > time.Now().Unix() {
147 | err = errors.New("invalid JWT values")
148 | return rvalue, err
149 | }
150 | return inboundJWT, nil
151 | }
152 |
--------------------------------------------------------------------------------
/cmd/keymasterd/certgen_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto"
5 | "crypto/tls"
6 | "crypto/x509"
7 | "encoding/pem"
8 | "errors"
9 | "fmt"
10 | "net"
11 | "net/http"
12 | "os"
13 | "testing"
14 | "time"
15 |
16 | "github.com/Symantec/keymaster/lib/certgen"
17 | "github.com/Symantec/keymaster/lib/webapi/v0/proto"
18 | )
19 |
20 | const testSignerX509Cert = `-----BEGIN CERTIFICATE-----
21 | MIIDeTCCAmGgAwIBAgIJAMSRCvyhZiyzMA0GCSqGSIb3DQEBCwUAMFMxCzAJBgNV
22 | BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxFDASBgNVBAoMC0V4YW1wbGUu
23 | Y29tMRcwFQYDVQQDDA5FeGFtcGxlIElzc3VlcjAeFw0xNzA0MjYxODAyMzJaFw0y
24 | NzA0MjQxODAyMzJaMFMxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENp
25 | dHkxFDASBgNVBAoMC0V4YW1wbGUuY29tMRcwFQYDVQQDDA5FeGFtcGxlIElzc3Vl
26 | cjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9ieOuCqGGzgzCG7ZE1
27 | efIOv9EE3VCGIDsJ8z3WjDVbQjsU4kAyFhG86r9RxD/YX87mkih1B7P/dXuCY9qm
28 | YIUsDxyxu0ys7vvwkSvbGvY76BhFcYOso4mz53Uxri1abWQnDiHBxF1Yj+Iq2iOn
29 | cm4hg5ZnhfdlkCXSO5eBLIOFuFqdeyRVjR28pSqBQdH3tm+Kuac2lzAcDbnuASir
30 | 7mudTQXLUcuC2WMIhr1SaZRG4wVO78AgEtF+Ju9stibyqIPfx3xiqNq2fbHtMBXv
31 | ZjJI49MLipA4fdaTs7AWf9D1tMx3ItqvciVcRdG+NXvfq2l2vJL0Y8RexBALlK/T
32 | r9sCAwEAAaNQME4wHQYDVR0OBBYEFP9MhquAFRFLT7fzbru/pHUZd7izMB8GA1Ud
33 | IwQYMBaAFP9MhquAFRFLT7fzbru/pHUZd7izMAwGA1UdEwQFMAMBAf8wDQYJKoZI
34 | hvcNAQELBQADggEBAAS+HXeUf/WG6g2AbNvd3F+8KkoWmNnRZ8OHuXYQxSQeXHon
35 | Bi0CAc7BZo43n9GSOy4mW0F6Z3JVkK06gH3pFRoKkqqpzk5WaCIYoofRRIOsF/l6
36 | tng3ucauQ3wYGftwid623D6nnbkhPj0jmTyGD6d772dueWEneR2JcN/5G7Xf8HEl
37 | a0fmpm1BG1ZrT2Vp4cb50VeFH+oZn9UW6j+w3Lx4D6pwJvJ11MFjkIfw7Q1hl0j9
38 | Unc9jsYhX7DR3SV8vcFqduUmSH8vdc/zJEk76T2D+qe1aWqtr84QpxXBTrIKvSXD
39 | igkmavdG2gu3SpbFzNxuVCrxQ88Kte0xYJTe7vY=
40 | -----END CERTIFICATE-----`
41 |
42 | const testDuration = time.Duration(120 * time.Second)
43 |
44 | /// X509section (this is from certgen TODO: make public)
45 | func getPubKeyFromPem(pubkey string) (pub interface{}, err error) {
46 | block, rest := pem.Decode([]byte(pubkey))
47 | if block == nil || block.Type != "PUBLIC KEY" {
48 | err := errors.New(fmt.Sprintf("Cannot decode user public Key '%s' rest='%s'", pubkey, string(rest)))
49 | if block != nil {
50 | err = errors.New(fmt.Sprintf("public key bad type %s", block.Type))
51 | }
52 | return nil, err
53 | }
54 | return x509.ParsePKIXPublicKey(block.Bytes)
55 | }
56 |
57 | func setupX509Generator(t *testing.T) (interface{}, *x509.Certificate, crypto.Signer) {
58 | userPub, err := getPubKeyFromPem(testUserPEMPublicKey)
59 | if err != nil {
60 | t.Fatal(err)
61 | }
62 | //caPriv, err := getPrivateKeyFromPem(testSignerPrivateKey)
63 | caPriv, err := certgen.GetSignerFromPEMBytes([]byte(testSignerPrivateKey))
64 | if err != nil {
65 | t.Fatal(err)
66 | }
67 |
68 | caCertBlock, _ := pem.Decode([]byte(testSignerX509Cert))
69 | if caCertBlock == nil || caCertBlock.Type != "CERTIFICATE" {
70 | t.Fatal(err)
71 | }
72 | caCert, err := x509.ParseCertificate(caCertBlock.Bytes)
73 | if err != nil {
74 | t.Fatal(err)
75 | }
76 | return userPub, caCert, caPriv
77 | }
78 |
79 | func TestSuccessFullSigningX509IPCert(t *testing.T) {
80 | state, passwdFile, err := setupValidRuntimeStateSigner()
81 | if err != nil {
82 | t.Fatal(err)
83 | }
84 | defer os.Remove(passwdFile.Name()) // clean up
85 |
86 | state.Config.Base.AllowedAuthBackendsForCerts = append(state.Config.Base.AllowedAuthBackendsForCerts, proto.AuthTypeIPCertificate)
87 |
88 | // Get request
89 | req, err := createKeyBodyRequest("POST", "/certgen/username?type=x509", testUserPEMPublicKey, "")
90 | if err != nil {
91 | t.Fatal(err)
92 | }
93 | _, err = checkRequestHandlerCode(req, state.certGenHandler, http.StatusUnauthorized)
94 | if err != nil {
95 | t.Fatal(err)
96 | }
97 | // now we add an ipcert to the request
98 |
99 | userPub, caCert, caPriv := setupX509Generator(t)
100 | netblock := net.IPNet{
101 | IP: net.ParseIP("127.0.0.0"),
102 | Mask: net.CIDRMask(8, 32),
103 | }
104 | netblock2 := net.IPNet{
105 | IP: net.ParseIP("10.0.0.0"),
106 | Mask: net.CIDRMask(8, 32),
107 | }
108 | netblockList := []net.IPNet{netblock, netblock2}
109 | derCert, err := certgen.GenIPRestrictedX509Cert("username", userPub, caCert, caPriv, netblockList, testDuration, nil, nil)
110 |
111 | cert, err := x509.ParseCertificate(derCert)
112 | if err != nil {
113 | t.Fatal(err)
114 | }
115 | req.RemoteAddr = "127.0.0.1:12345"
116 | var fakePeerCertificates []*x509.Certificate
117 | var fakeVerifiedChains [][]*x509.Certificate
118 | fakePeerCertificates = append(fakePeerCertificates, cert)
119 | fakeVerifiedChains = append(fakeVerifiedChains, fakePeerCertificates)
120 | connectionState := &tls.ConnectionState{
121 | VerifiedChains: fakeVerifiedChains,
122 | PeerCertificates: fakePeerCertificates}
123 | req.TLS = connectionState
124 |
125 | // Will fail due to bad username
126 | _, err = checkRequestHandlerCode(req, state.certGenHandler, http.StatusUnauthorized)
127 | if err != nil {
128 | t.Fatal(err)
129 | }
130 | // Now add username to the set of valid users to get certs from
131 | state.Config.Base.AutomationUsers = append(state.Config.Base.AutomationUsers, "username")
132 | _, err = checkRequestHandlerCode(req, state.certGenHandler, http.StatusOK)
133 | if err != nil {
134 | t.Fatal(err)
135 | }
136 | //TODO check return content
137 |
138 | //now test with failure
139 | req.RemoteAddr = "192.168.255.255:12345"
140 | _, err = checkRequestHandlerCode(req, state.certGenHandler, http.StatusUnauthorized)
141 | if err != nil {
142 | t.Fatal(err)
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/lib/instrumentedwriter/instrumentedWriter.go:
--------------------------------------------------------------------------------
1 | package instrumentedwriter
2 |
3 | /* Original Instpiration comes from https://github.com/mash/go-accesslog/
4 |
5 | */
6 |
7 | import (
8 | "bufio"
9 | "fmt"
10 | "net"
11 | "net/http"
12 | "strings"
13 | "time"
14 |
15 | "github.com/prometheus/client_golang/prometheus"
16 | )
17 |
18 | type LogRecord struct {
19 | Time time.Time
20 | Ip, Method, Uri, Protocol, Username, Host string
21 | UserAgent string
22 | Status int
23 | Size int64
24 | ElapsedTime time.Duration
25 | RequestHeader http.Header
26 | CustomRecords map[string]string
27 | }
28 |
29 | type LoggingWriter struct {
30 | http.ResponseWriter
31 | logRecord LogRecord
32 | }
33 |
34 | var (
35 | httpRequestDurationSummary = prometheus.NewSummaryVec(
36 | prometheus.SummaryOpts{
37 | Name: "service_http_request_processing_duration_seconds",
38 | Help: "Http request processing time seconds",
39 | Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
40 | },
41 | []string{"code", "port"},
42 | )
43 | httpRequestSizeHistogram = prometheus.NewHistogramVec(
44 | prometheus.HistogramOpts{
45 | Name: "service_http_request_response_size_bytes",
46 | Help: "HTTP request response historgram",
47 | Buckets: prometheus.ExponentialBuckets(10.0, 1.5, 20),
48 | },
49 | []string{"code", "port"},
50 | )
51 | )
52 |
53 | func init() {
54 | // Register the summary and the histogram with Prometheus's default registry.
55 | prometheus.MustRegister(httpRequestDurationSummary)
56 | prometheus.MustRegister(httpRequestSizeHistogram)
57 | }
58 |
59 | func (r *LoggingWriter) Write(p []byte) (int, error) {
60 | if r.logRecord.Status == 0 {
61 | // The status will be StatusOK if WriteHeader has not been called yet
62 | r.logRecord.Status = http.StatusOK
63 | }
64 | written, err := r.ResponseWriter.Write(p)
65 | r.logRecord.Size += int64(written)
66 | return written, err
67 | }
68 |
69 | func (r *LoggingWriter) WriteHeader(status int) {
70 | r.logRecord.Status = status
71 | r.ResponseWriter.WriteHeader(status)
72 | }
73 |
74 | // w.(accesslogger.LoggingWriter).SetCustomLogRecord("X-User-Id", "3")
75 | func (r *LoggingWriter) SetCustomLogRecord(key, value string) {
76 | if r.logRecord.CustomRecords == nil {
77 | r.logRecord.CustomRecords = map[string]string{}
78 | }
79 | r.logRecord.CustomRecords[key] = value
80 | }
81 |
82 | // w.(accesslogger.LoggingWriter).SetUsername("alice")
83 | func (r *LoggingWriter) SetUsername(username string) {
84 | r.logRecord.Username = username
85 | }
86 |
87 | // http.CloseNotifier interface
88 | func (r *LoggingWriter) CloseNotify() <-chan bool {
89 | if w, ok := r.ResponseWriter.(http.CloseNotifier); ok {
90 | return w.CloseNotify()
91 | }
92 | return make(chan bool)
93 | }
94 |
95 | func (r *LoggingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
96 | if hijacker, ok := r.ResponseWriter.(http.Hijacker); ok {
97 | return hijacker.Hijack()
98 | }
99 | return nil, nil, fmt.Errorf("ResponseWriter doesn't support Hijacker interface")
100 | }
101 |
102 | // http.Flusher
103 | func (r *LoggingWriter) Flush() {
104 | flusher, ok := r.ResponseWriter.(http.Flusher)
105 | if ok {
106 | flusher.Flush()
107 | }
108 | }
109 |
110 | // http.Pusher
111 | func (r *LoggingWriter) Push(target string, opts *http.PushOptions) error {
112 | pusher, ok := r.ResponseWriter.(http.Pusher)
113 | if ok {
114 | return pusher.Push(target, opts)
115 | }
116 | return fmt.Errorf("ResponseWriter doesn't support Pusher interface")
117 | }
118 |
119 | type Logger interface {
120 | Log(record LogRecord)
121 | }
122 |
123 | type LoggingHandler struct {
124 | handler http.Handler
125 | logger Logger
126 | }
127 |
128 | func NewLoggingHandler(handler http.Handler, logger Logger) http.Handler {
129 | return &LoggingHandler{
130 | handler: handler,
131 | logger: logger,
132 | }
133 | }
134 |
135 | // readIp return the real ip when behide nginx or apache
136 | func (h *LoggingHandler) realIp(r *http.Request) string {
137 | ip, _, err := net.SplitHostPort(r.RemoteAddr)
138 | if err != nil {
139 | ip = r.RemoteAddr
140 | }
141 | if ip != "127.0.0.1" {
142 | return ip
143 | }
144 | // Check if behide nginx or apache
145 | xRealIP := r.Header.Get("X-Real-Ip")
146 | xForwardedFor := r.Header.Get("X-Forwarded-For")
147 |
148 | for _, address := range strings.Split(xForwardedFor, ",") {
149 | address = strings.TrimSpace(address)
150 | if address != "" {
151 | return address
152 | }
153 | }
154 |
155 | if xRealIP != "" {
156 | return xRealIP
157 | }
158 | return ip
159 | }
160 |
161 | func (h *LoggingHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
162 | ip := h.realIp(r)
163 | username := "-"
164 | if r.URL.User != nil {
165 | if name := r.URL.User.Username(); name != "" {
166 | username = name
167 | }
168 | }
169 |
170 | startTime := time.Now()
171 | writer := &LoggingWriter{
172 | ResponseWriter: rw,
173 | logRecord: LogRecord{
174 | Time: startTime.UTC(),
175 | Ip: ip,
176 | Method: r.Method,
177 | Uri: r.RequestURI,
178 | Username: username,
179 | Protocol: r.Proto,
180 | Host: r.Host,
181 | UserAgent: r.UserAgent(),
182 | Status: 0,
183 | Size: 0,
184 | ElapsedTime: time.Duration(0),
185 | RequestHeader: r.Header,
186 | },
187 | }
188 |
189 | h.handler.ServeHTTP(writer, r)
190 | finishTime := time.Now()
191 |
192 | writer.logRecord.Time = finishTime.UTC()
193 | writer.logRecord.ElapsedTime = finishTime.Sub(startTime)
194 |
195 | _, port, err := net.SplitHostPort(r.Host)
196 | if err != nil {
197 | port = ""
198 | }
199 |
200 | httpRequestDurationSummary.WithLabelValues(fmt.Sprintf("%d", writer.logRecord.Status),
201 | port).Observe(writer.logRecord.ElapsedTime.Seconds())
202 | httpRequestSizeHistogram.WithLabelValues(fmt.Sprintf("%d", writer.logRecord.Status),
203 | port).Observe(float64(writer.logRecord.Size))
204 | h.logger.Log(writer.logRecord)
205 | }
206 |
--------------------------------------------------------------------------------
/lib/certgen/iprestricted.go:
--------------------------------------------------------------------------------
1 | package certgen
2 |
3 | import (
4 | "bytes"
5 | "crypto"
6 | "crypto/rand"
7 | "crypto/sha1"
8 | "crypto/x509"
9 | "crypto/x509/pkix"
10 | "encoding/asn1"
11 | "errors"
12 | //"log"
13 | "math/big"
14 | "net"
15 | "time"
16 | )
17 |
18 | //We aim to build certs compatible with
19 | // https://tools.ietf.org/html/rfc3779
20 |
21 | type IpAdressFamily struct {
22 | AddressFamily []byte
23 | Addresses []asn1.BitString
24 | }
25 |
26 | var oidIPAddressDelegation = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 7}
27 | var ipV4FamilyEncoding = []byte{0, 1, 1}
28 |
29 | //For now ipv4 only
30 | func encodeIpAddressChoice(netBlock net.IPNet) (asn1.BitString, error) {
31 | ones, bits := netBlock.Mask.Size()
32 | if bits != 32 {
33 | return asn1.BitString{}, errors.New("not an ipv4 address")
34 | }
35 | //unusedLen = uint8(ones) % 8
36 | var output []byte
37 | outlen := ((ones + 7) / 8)
38 | //log.Printf("outlen=%d, ones=%d", outlen, ones)
39 | output = make([]byte, outlen, outlen)
40 | //log.Printf("len netbloclen=%+v,", len(netBlock.IP))
41 | increment := 12
42 | if len(netBlock.IP) == 4 {
43 | increment = 0
44 | }
45 | for i := 0; i < outlen; i++ {
46 | output[i] = netBlock.IP[increment+i]
47 | }
48 | //log.Printf("%+v", output)
49 | bitString := asn1.BitString{
50 | Bytes: output,
51 | BitLength: ones,
52 | }
53 |
54 | return bitString, nil
55 | }
56 |
57 | func genDelegationExtension(ipv4Netblocks []net.IPNet) (*pkix.Extension, error) {
58 | ipv4AddressFamily := IpAdressFamily{
59 | AddressFamily: ipV4FamilyEncoding,
60 | }
61 | for _, netblock := range ipv4Netblocks {
62 | encodedNetBlock, err := encodeIpAddressChoice(netblock)
63 | if err != nil {
64 | return nil, err
65 | }
66 | ipv4AddressFamily.Addresses = append(ipv4AddressFamily.Addresses, encodedNetBlock)
67 | }
68 | addressFamilyList := []IpAdressFamily{ipv4AddressFamily}
69 |
70 | encodedAddressFamily, err := asn1.Marshal(addressFamilyList)
71 | if err != nil {
72 | return nil, err
73 | }
74 | ipDelegationExtension := pkix.Extension{
75 | Id: oidIPAddressDelegation,
76 | Value: encodedAddressFamily,
77 | }
78 | return &ipDelegationExtension, nil
79 | }
80 |
81 | func decodeIPV4AddressChoice(encodedBlock asn1.BitString) (net.IPNet, error) {
82 | var encodedIP [4]byte
83 | for i := 0; (i * 8) < encodedBlock.BitLength; i++ {
84 | encodedIP[i] = encodedBlock.Bytes[i]
85 | }
86 | netBlock := net.IPNet{
87 | IP: net.IPv4(encodedIP[0], encodedIP[1], encodedIP[2], encodedIP[3]),
88 | Mask: net.CIDRMask(encodedBlock.BitLength, 32),
89 | }
90 | return netBlock, nil
91 | }
92 |
93 | //
94 | type subjectPublicKeyInfo struct {
95 | Algorithm pkix.AlgorithmIdentifier
96 | SubjectPublicKey asn1.BitString
97 | }
98 |
99 | // ComputePublicKeyKeyID computes the SHA-1 digest of a public Key
100 | func ComputePublicKeyKeyID(PublicKey interface{}) ([]byte, error) {
101 | encodedPub, err := x509.MarshalPKIXPublicKey(PublicKey)
102 | if err != nil {
103 | return nil, err
104 | }
105 |
106 | var subPKI subjectPublicKeyInfo
107 | _, err = asn1.Unmarshal(encodedPub, &subPKI)
108 | if err != nil {
109 | return nil, err
110 | }
111 |
112 | pubHash := sha1.Sum(subPKI.SubjectPublicKey.Bytes)
113 | return pubHash[:], nil
114 | }
115 |
116 | // GenIPRestrictedX509Cert returns an x509 cert that has the username in
117 | // the common name, with the allowed netyblocks specified
118 | func GenIPRestrictedX509Cert(userName string, userPub interface{},
119 | caCert *x509.Certificate, caPriv crypto.Signer,
120 | ipv4Netblocks []net.IPNet, duration time.Duration,
121 | crlURL []string, OCPServer []string) ([]byte, error) {
122 | // Now do the actual work...
123 | notBefore := time.Now()
124 | notAfter := notBefore.Add(duration)
125 |
126 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
127 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
128 | if err != nil {
129 | return nil, err
130 | }
131 | subject := pkix.Name{
132 | CommonName: userName,
133 | }
134 | ipDelegationExtension, err := genDelegationExtension(ipv4Netblocks)
135 | if err != nil {
136 | return nil, err
137 | }
138 | template := x509.Certificate{
139 | SerialNumber: serialNumber,
140 | Subject: subject,
141 | NotBefore: notBefore,
142 | NotAfter: notAfter,
143 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
144 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
145 | IssuingCertificateURL: crlURL,
146 | OCSPServer: OCPServer,
147 | BasicConstraintsValid: true,
148 | IsCA: false,
149 | }
150 | if ipDelegationExtension != nil {
151 | template.ExtraExtensions = append(template.ExtraExtensions,
152 | *ipDelegationExtension)
153 | }
154 | return x509.CreateCertificate(rand.Reader, &template, caCert, userPub, caPriv)
155 | }
156 |
157 | // VerifyIPRestrictedX509CertIP takes a x509 cert and verifies that it is valid given
158 | // an incoming remote address. If the cert does not contain an IP restriction extension
159 | // the verification is considered failed.
160 | func VerifyIPRestrictedX509CertIP(userCert *x509.Certificate, remoteAddr string) (bool, error) {
161 | host, _, err := net.SplitHostPort(remoteAddr)
162 | if err != nil {
163 | return false, err
164 | }
165 | remoteIP := net.ParseIP(host)
166 | var extension *pkix.Extension = nil
167 | for _, certExtension := range userCert.Extensions {
168 | if certExtension.Id.Equal(oidIPAddressDelegation) {
169 | extension = &certExtension
170 | break
171 | }
172 | }
173 | if extension == nil {
174 | return false, nil
175 | }
176 | var ipAddressFamilyList []IpAdressFamily
177 | _, err = asn1.Unmarshal(extension.Value, &ipAddressFamilyList)
178 | if err != nil {
179 | return false, err
180 | }
181 | for _, addressList := range ipAddressFamilyList {
182 | if !bytes.Equal(addressList.AddressFamily, ipV4FamilyEncoding) {
183 | continue
184 | }
185 | for _, encodedNetblock := range addressList.Addresses {
186 | decoded, err := decodeIPV4AddressChoice(encodedNetblock)
187 | if err != nil {
188 | return false, err
189 | }
190 | if decoded.Contains(remoteIP) {
191 | return true, nil
192 | }
193 | }
194 | }
195 | return false, nil
196 | }
197 |
--------------------------------------------------------------------------------
/cmd/keymasterd/auth_oauth2.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "strings"
9 | "time"
10 |
11 | "golang.org/x/net/context"
12 | )
13 |
14 | const maxAgeSecondsRedirCookie = 120
15 | const redirCookieName = "oauth2_redir"
16 |
17 | const oauth2LoginBeginPath = "/auth/oauth2/login"
18 |
19 | func (state *RuntimeState) oauth2DoRedirectoToProviderHandler(w http.ResponseWriter, r *http.Request) {
20 |
21 | if state.Config.Oauth2.Config == nil {
22 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "error internal")
23 | logger.Println("asking for oauth2, but it is not defined")
24 | return
25 | }
26 | if !state.Config.Oauth2.Enabled {
27 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Oauth2 is not enabled in for this system")
28 | logger.Println("asking for oauth2, but it is not enabled")
29 | return
30 | }
31 | cookieVal, err := genRandomString()
32 | if err != nil {
33 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "error internal")
34 | logger.Println(err)
35 | return
36 | }
37 |
38 | // we have to create new context and set redirector...
39 | expiration := time.Now().Add(time.Duration(maxAgeSecondsRedirCookie) * time.Second)
40 |
41 | stateString, err := genRandomString()
42 | if err != nil {
43 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "error internal")
44 | logger.Println(err)
45 | return
46 | }
47 |
48 | cookie := http.Cookie{Name: redirCookieName, Value: cookieVal,
49 | Expires: expiration, Path: "/", HttpOnly: true}
50 | http.SetCookie(w, &cookie)
51 |
52 | pending := pendingAuth2Request{
53 | ExpiresAt: expiration,
54 | state: stateString,
55 | ctx: context.Background()}
56 | state.Mutex.Lock()
57 | state.pendingOauth2[cookieVal] = pending
58 | state.Mutex.Unlock()
59 | http.Redirect(w, r, state.Config.Oauth2.Config.AuthCodeURL(stateString), http.StatusFound)
60 | }
61 |
62 | func httpGet(client *http.Client, url string) ([]byte, error) {
63 | r, err := client.Get(url)
64 | if err != nil {
65 | return nil, err
66 | }
67 |
68 | defer r.Body.Close()
69 |
70 | body, err := ioutil.ReadAll(r.Body)
71 | if err != nil {
72 | return nil, err
73 | }
74 |
75 | if r.StatusCode >= 300 {
76 | return nil, fmt.Errorf(string(body))
77 | }
78 |
79 | logger.Debugf(8, "HTTP GET %s: %s %s", url, r.Status, string(body))
80 |
81 | return body, nil
82 | }
83 |
84 | func (state *RuntimeState) oauth2RedirectPathHandler(w http.ResponseWriter, r *http.Request) {
85 |
86 | if state.Config.Oauth2.Config == nil {
87 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "error internal")
88 | logger.Println("asking for oauth2, but it is not defined")
89 | return
90 | }
91 | if !state.Config.Oauth2.Enabled {
92 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Oauth2 is not enabled in for this system")
93 | logger.Println("asking for oauth2, but it is not enabled")
94 | return
95 | }
96 |
97 | redirCookie, err := r.Cookie(redirCookieName)
98 | if err != nil {
99 | if err == http.ErrNoCookie {
100 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Missing setup cookie!")
101 | logger.Println(err)
102 | return
103 | }
104 | // TODO: this is probably a user error? send back to oath2 login path?
105 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "error internal")
106 | logger.Println(err)
107 | return
108 | }
109 | index := redirCookie.Value
110 | state.Mutex.Lock()
111 | pending, ok := state.pendingOauth2[index]
112 | state.Mutex.Unlock()
113 | if !ok {
114 | // clear cookie here!!!!
115 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Invalid setup cookie!")
116 | logger.Println(err)
117 | return
118 | }
119 |
120 | if r.URL.Query().Get("state") != pending.state {
121 | logger.Printf("state does not match")
122 | http.Error(w, "state did not match", http.StatusBadRequest)
123 | return
124 | }
125 | //if Debug {
126 | //logger.Printf("req : %+v", r)
127 | //}
128 | oauth2Token, err := state.Config.Oauth2.Config.Exchange(pending.ctx, r.URL.Query().Get("code"))
129 | if err != nil {
130 | logger.Printf("failed to get token: ctx: %+v", pending.ctx)
131 | http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
132 | return
133 | }
134 | client := state.Config.Oauth2.Config.Client(pending.ctx, oauth2Token)
135 | //client.Get("...")
136 | body, err := httpGet(client, state.Config.Oauth2.UserinfoUrl)
137 | if err != nil {
138 | logger.Printf("fail to fetch %s (%s) ", state.Config.Oauth2.UserinfoUrl, err.Error())
139 | http.Error(w, "Failed to get userinfo from url: "+err.Error(), http.StatusInternalServerError)
140 | return
141 | }
142 |
143 | var data struct {
144 | Name string `json:"name"`
145 | DisplayName string `json:"display_name"`
146 | Login string `json:"login"`
147 | Username string `json:"username"`
148 | Email string `json:"email"`
149 | Attributes map[string][]string `json:"attributes"`
150 | }
151 |
152 | logger.Debugf(3, "Userinfo body:'%s'", string(body))
153 | err = json.Unmarshal(body, &data)
154 | if err != nil {
155 | logger.Printf("failed to unmarshall userinfo to fetch %s ", body)
156 | http.Error(w, "Failed to get unmarshall userinfo: "+err.Error(), http.StatusInternalServerError)
157 | return
158 | }
159 |
160 | // The Name field could also be useful
161 | logger.Debugf(2, "%+v", data)
162 |
163 | // Check if name is there..
164 |
165 | // TODO: we need a more robust way to get the username and to add some filters. This
166 | // mechanism is ok for 0.2 but not for 0.3.
167 | username := data.Login
168 | if username == "" {
169 | components := strings.Split(data.Email, "@")
170 | if len(components[0]) < 1 {
171 | http.Error(w, "Email from userinfo is invalid: ", http.StatusInternalServerError)
172 | return
173 | }
174 | username = strings.ToLower(components[0])
175 | }
176 |
177 | //Make new auth cookie
178 | _, err = state.setNewAuthCookie(w, username, AuthTypeFederated)
179 | if err != nil {
180 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "error internal")
181 | logger.Println(err)
182 | return
183 | }
184 |
185 | // delete peding cookie
186 | state.Mutex.Lock()
187 | delete(state.pendingOauth2, index)
188 | state.Mutex.Unlock()
189 |
190 | eventNotifier.PublishWebLoginEvent(username)
191 | //and redirect to profile page
192 | http.Redirect(w, r, profilePath, 302)
193 | }
194 |
--------------------------------------------------------------------------------
/lib/client/twofa/vip/vip.go:
--------------------------------------------------------------------------------
1 | package vip
2 |
3 | import (
4 | "bufio"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "io/ioutil"
10 | "net/http"
11 | "net/url"
12 | "os"
13 | "strconv"
14 | "strings"
15 | "time"
16 |
17 | "github.com/Symantec/Dominator/lib/log"
18 | "github.com/Symantec/keymaster/lib/webapi/v0/proto"
19 | )
20 |
21 | const vipCheckTimeoutSecs = 180
22 |
23 | func startVIPPush(client *http.Client,
24 | baseURL string,
25 | userAgentString string,
26 | logger log.DebugLogger) error {
27 |
28 | VIPPushStartURL := baseURL + "/api/v0/vipPushStart"
29 |
30 | req, err := http.NewRequest("GET", VIPPushStartURL, nil)
31 | if err != nil {
32 | return err
33 | }
34 | req.Header.Add("Accept", "application/json")
35 | req.Header.Set("User-Agent", userAgentString)
36 |
37 | pushStartResp, err := client.Do(req)
38 | if err != nil {
39 | logger.Printf("got error from pushStart")
40 | logger.Println(err)
41 | return err
42 | }
43 | defer pushStartResp.Body.Close()
44 | // since we dont care about content we just consume it all.
45 | io.Copy(ioutil.Discard, pushStartResp.Body)
46 | if pushStartResp.StatusCode != 200 {
47 | logger.Printf("got error from vipStart call %s", pushStartResp.Status)
48 | err := errors.New("bad vip response code")
49 | return err
50 | }
51 | //at this moment we dont actually return json data... so we can just return here
52 | return nil
53 | }
54 |
55 | func checkVIPPollStatus(client *http.Client,
56 | baseURL string,
57 | userAgentString string,
58 | logger log.DebugLogger) (bool, error) {
59 |
60 | VIPPollCheckURL := baseURL + "/api/v0/vipPollCheck"
61 |
62 | req, err := http.NewRequest("GET", VIPPollCheckURL, nil)
63 | if err != nil {
64 | return false, err
65 | }
66 | req.Header.Add("Accept", "application/json")
67 | req.Header.Set("User-Agent", userAgentString)
68 |
69 | pollCheckResp, err := client.Do(req)
70 | if err != nil {
71 | logger.Printf("got error from vipPollCheck")
72 | logger.Println(err)
73 | return false, err
74 | }
75 | defer pollCheckResp.Body.Close()
76 | // we dont care about content (for now) so consume it all
77 | io.Copy(ioutil.Discard, pollCheckResp.Body)
78 | if pollCheckResp.StatusCode != 200 {
79 | if pollCheckResp.StatusCode == 412 {
80 | logger.Debugf(1, "got error from vipPollCheck call %s", pollCheckResp.Status)
81 | return false, nil
82 | }
83 | logger.Printf("got error from vipPollCheck call %s", pollCheckResp.Status)
84 | //err := errors.New("bad vip response code")
85 | return false, nil
86 | }
87 |
88 | return true, nil
89 | }
90 |
91 | func doVIPPushCheck(client *http.Client,
92 | baseURL string,
93 | userAgentString string,
94 | logger log.DebugLogger,
95 | errorReturnDuration time.Duration) error {
96 |
97 | err := startVIPPush(client, baseURL, userAgentString, logger)
98 | if err != nil {
99 | logger.Printf("got error from pushStart, will sleep to allow code to be entered")
100 | logger.Println(err)
101 | time.Sleep(errorReturnDuration)
102 | return err
103 | }
104 | endTime := time.Now().Add(errorReturnDuration)
105 | //initial sleep
106 | for time.Now().Before(endTime) {
107 | ok, err := checkVIPPollStatus(client, baseURL, userAgentString, logger)
108 | if err != nil {
109 | logger.Printf("got error from vipPollCheck, will sleep to allow code to be entered")
110 | logger.Println(err)
111 | time.Sleep(errorReturnDuration)
112 | return err
113 | }
114 | if ok {
115 | logger.Printf("") //To do a CR
116 | return nil
117 | }
118 | time.Sleep(3 * time.Second)
119 | }
120 |
121 | err = errors.New("Vip Push Checked timeout out")
122 | return err
123 | }
124 |
125 | func VIPAuthenticateWithToken(
126 | client *http.Client,
127 | baseURL string,
128 | userAgentString string,
129 | logger log.DebugLogger) error {
130 | logger.Printf("top of doVIPAuthenticate")
131 |
132 | // Read VIP token from client
133 |
134 | reader := bufio.NewReader(os.Stdin)
135 | fmt.Print("Enter VIP/OTP code (or wait for VIP push): ")
136 | otpText, err := reader.ReadString('\n')
137 | if err != nil {
138 | logger.Debugf(0, "codeText: Failure to get string %s", err)
139 | return err
140 | }
141 | otpText = strings.TrimSpace(otpText)
142 | //fmt.Println(codeText)
143 | logger.Debugf(1, "codeText: '%s'", otpText)
144 |
145 | // TODO: add some client side validation that the codeText is actually a six digit
146 | // integer
147 |
148 | VIPLoginURL := baseURL + "/api/v0/vipAuth"
149 |
150 | form := url.Values{}
151 | form.Add("OTP", otpText)
152 | //form.Add("password", string(password[:]))
153 | req, err := http.NewRequest("POST", VIPLoginURL, strings.NewReader(form.Encode()))
154 | if err != nil {
155 | return err
156 | }
157 |
158 | req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
159 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
160 | req.Header.Add("Accept", "application/json")
161 | req.Header.Set("User-Agent", userAgentString)
162 |
163 | loginResp, err := client.Do(req) //client.Get(targetUrl)
164 | if err != nil {
165 | logger.Printf("got error from req")
166 | logger.Println(err)
167 | // TODO: differentiate between 400 and 500 errors
168 | // is OK to fail.. try next
169 | return err
170 | }
171 | defer loginResp.Body.Close()
172 | if loginResp.StatusCode != 200 {
173 | logger.Printf("got error from login call %s", loginResp.Status)
174 | return err
175 | }
176 |
177 | loginJSONResponse := proto.LoginResponse{}
178 | //body := jsonrr.Result().Body
179 | err = json.NewDecoder(loginResp.Body).Decode(&loginJSONResponse)
180 | if err != nil {
181 | return err
182 | }
183 | io.Copy(ioutil.Discard, loginResp.Body)
184 |
185 | logger.Debugf(1, "This the login response=%v\n", loginJSONResponse)
186 |
187 | return nil
188 | }
189 |
190 | func doVIPAuthenticate(
191 | client *http.Client,
192 | baseURL string,
193 | userAgentString string,
194 | logger log.DebugLogger) error {
195 |
196 | timeout := time.Duration(time.Duration(vipCheckTimeoutSecs) * time.Second)
197 | ch := make(chan error, 1)
198 | go func() {
199 | err := VIPAuthenticateWithToken(client, baseURL, userAgentString, logger)
200 | ch <- err
201 | }()
202 | go func() {
203 | err := doVIPPushCheck(client, baseURL,
204 | userAgentString,
205 | logger, timeout)
206 | ch <- err
207 |
208 | }()
209 | select {
210 | case err := <-ch:
211 | if err != nil {
212 | logger.Printf("Problem with vip ='%s'", err)
213 | return err
214 | }
215 | return nil
216 | case <-time.After(timeout):
217 | err := errors.New("vip timeout")
218 | return err
219 | }
220 | return nil
221 | }
222 |
--------------------------------------------------------------------------------
/cmd/keymasterd/auth_oauth2_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | stdlog "log"
7 | "net/http"
8 | "os"
9 | "testing"
10 | "time"
11 |
12 | "github.com/Symantec/Dominator/lib/log/debuglogger"
13 | "golang.org/x/net/context"
14 | "golang.org/x/oauth2"
15 | )
16 |
17 | func handler(w http.ResponseWriter, r *http.Request) {
18 | logger.Printf("top of generic handller")
19 | fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
20 | }
21 |
22 | type oauth2TokenJSON struct {
23 | AccessToken string `json:"access_token"`
24 | TokenType string `json:"token_type"`
25 | RefreshToken string `json:"refresh_token"`
26 | ExpiresIn int `json:"expires_in"`
27 | }
28 |
29 | const testAccessTokenValue = "1234567890"
30 |
31 | func tokenHandler(w http.ResponseWriter, r *http.Request) {
32 | logger.Printf("inside tokenHandler")
33 | token := oauth2TokenJSON{
34 | AccessToken: testAccessTokenValue,
35 | TokenType: "Bearer",
36 | RefreshToken: testAccessTokenValue,
37 | ExpiresIn: 300,
38 | }
39 | w.Header().Set("Content-Type", "application/json")
40 | if err := json.NewEncoder(w).Encode(token); err != nil {
41 | logger.Printf("broken stuff")
42 | }
43 | //fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
44 | }
45 |
46 | type oauth2claimsTestJSON struct {
47 | Sub string `json:"sub"`
48 | Email string `json:"email"`
49 | Name string `json:"name"`
50 | }
51 |
52 | func userinfoHandler(w http.ResponseWriter, r *http.Request) {
53 | logger.Printf("isseuserinfo handller")
54 | userinfo := oauth2claimsTestJSON{
55 | Sub: "username@example.com",
56 | Email: "userbane@example.com",
57 | Name: "username",
58 | }
59 | w.Header().Set("Content-Type", "application/json")
60 | if err := json.NewEncoder(w).Encode(userinfo); err != nil {
61 | logger.Printf("broken stuff")
62 | }
63 | //fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
64 | }
65 |
66 | func init() {
67 | //logger = stdlog.New(os.Stderr, "", stdlog.LstdFlags)
68 | slogger := stdlog.New(os.Stderr, "", stdlog.LstdFlags)
69 | logger = debuglogger.New(slogger)
70 | http.HandleFunc("/userinfo", userinfoHandler)
71 | http.HandleFunc("/token", tokenHandler)
72 | http.HandleFunc("/", handler)
73 | logger.Printf("about to start server")
74 | go http.ListenAndServe("127.0.0.1:12345", nil)
75 | time.Sleep(20 * time.Millisecond)
76 | _, err := http.Get("http://localhost:12345")
77 | if err != nil {
78 | logger.Fatal(err)
79 | }
80 | }
81 |
82 | var testOauth2Config = oauth2.Config{
83 | ClientID: "foo",
84 | ClientSecret: "bar",
85 | Endpoint: oauth2.Endpoint{
86 | AuthURL: "http://localhost:12345/auth",
87 | TokenURL: "http://localhost:12345/token"},
88 | RedirectURL: "https://example.com" + redirectPath,
89 | Scopes: []string{"openidc", "email"},
90 | }
91 |
92 | func TestOauth2DoRedirectoToProviderHandlerSuccess(t *testing.T) {
93 | state, passwdFile, err := setupValidRuntimeStateSigner()
94 | if err != nil {
95 | t.Fatal(err)
96 | }
97 | defer os.Remove(passwdFile.Name()) // clean up
98 | state.pendingOauth2 = make(map[string]pendingAuth2Request)
99 |
100 | req, err := http.NewRequest("GET", oauth2LoginBeginPath, nil)
101 | if err != nil {
102 | t.Fatal(err)
103 | }
104 |
105 | // It is now broken because there is no valid oauth2 config
106 | _, err = checkRequestHandlerCode(req, state.oauth2DoRedirectoToProviderHandler, http.StatusInternalServerError)
107 | if err != nil {
108 | t.Fatal(err)
109 | }
110 |
111 | state.Config.Oauth2.Config = &testOauth2Config
112 | // Still failure because it it not enabled
113 | _, err = checkRequestHandlerCode(req, state.oauth2DoRedirectoToProviderHandler, http.StatusBadRequest)
114 | if err != nil {
115 | t.Fatal(err)
116 | }
117 |
118 | state.Config.Oauth2.Enabled = true
119 | //and now we succeed.
120 | _, err = checkRequestHandlerCode(req, state.oauth2DoRedirectoToProviderHandler, http.StatusFound)
121 | if err != nil {
122 | t.Fatal(err)
123 | }
124 |
125 | // Todo Check for the response contents
126 | }
127 |
128 | func TestOauth2RedirectPathHandlerSuccess(t *testing.T) {
129 | state, passwdFile, err := setupValidRuntimeStateSigner()
130 | if err != nil {
131 | t.Fatal(err)
132 | }
133 | defer os.Remove(passwdFile.Name()) // clean up
134 | state.pendingOauth2 = make(map[string]pendingAuth2Request)
135 | state.Config.Oauth2.UserinfoUrl = "http://localhost:12345/userinfo"
136 |
137 | //initially the request should fail for lack of preconditions
138 | req, err := http.NewRequest("GET", redirectPath, nil)
139 | if err != nil {
140 | t.Fatal(err)
141 | }
142 | // oath2 config is invalid
143 | _, err = checkRequestHandlerCode(req, state.oauth2RedirectPathHandler, http.StatusInternalServerError)
144 | if err != nil {
145 | t.Fatal(err)
146 | }
147 | state.Config.Oauth2.Config = &testOauth2Config
148 |
149 | // oath2 is not enabled
150 | _, err = checkRequestHandlerCode(req, state.oauth2RedirectPathHandler, http.StatusBadRequest)
151 | if err != nil {
152 | t.Fatal(err)
153 | }
154 | state.Config.Oauth2.Enabled = true
155 |
156 | // request has no cookies
157 | _, err = checkRequestHandlerCode(req, state.oauth2RedirectPathHandler, http.StatusBadRequest)
158 | if err != nil {
159 | t.Fatal(err)
160 | }
161 | // has a cookie.. but is not known to the server:
162 | cookieVal := "supersecret"
163 | pendingCookie := http.Cookie{Name: redirCookieName, Value: cookieVal}
164 | req.AddCookie(&pendingCookie)
165 | // request has no cookies
166 | _, err = checkRequestHandlerCode(req, state.oauth2RedirectPathHandler, http.StatusBadRequest)
167 | if err != nil {
168 | t.Fatal(err)
169 | }
170 |
171 | //Now add the cookie... but no state variable on the query
172 | expiration := time.Now().Add(time.Duration(maxAgeSecondsRedirCookie) * time.Second)
173 | expectedState := "somestate"
174 | state.pendingOauth2[cookieVal] = pendingAuth2Request{
175 | ExpiresAt: expiration,
176 | state: expectedState,
177 | ctx: context.Background()}
178 |
179 | _, err = checkRequestHandlerCode(req, state.oauth2RedirectPathHandler, http.StatusBadRequest)
180 | if err != nil {
181 | t.Fatal(err)
182 | }
183 | //TODO: add invalid state
184 | q := req.URL.Query()
185 | q.Set("state", "foo")
186 | q.Set("code", "123")
187 | req.URL.RawQuery = q.Encode()
188 | _, err = checkRequestHandlerCode(req, state.oauth2RedirectPathHandler, http.StatusBadRequest)
189 | if err != nil {
190 | t.Fatal(err)
191 | }
192 | //now we add valid state
193 | q.Set("state", expectedState)
194 | req.URL.RawQuery = q.Encode()
195 | _, err = checkRequestHandlerCode(req, state.oauth2RedirectPathHandler, http.StatusFound)
196 | if err != nil {
197 | t.Fatal(err)
198 | }
199 |
200 | }
201 |
--------------------------------------------------------------------------------
/lib/client/twofa/u2f/u2f.go:
--------------------------------------------------------------------------------
1 | package u2f
2 |
3 | import (
4 | "bytes"
5 | "crypto/sha256"
6 | "encoding/base64"
7 | "encoding/json"
8 | "errors"
9 | "io"
10 | "io/ioutil"
11 | "net/http"
12 | "time"
13 |
14 | "github.com/Symantec/Dominator/lib/log"
15 | "github.com/flynn/u2f/u2fhid"
16 | "github.com/flynn/u2f/u2ftoken"
17 | "github.com/tstranex/u2f"
18 | )
19 |
20 | const clientDataAuthenticationTypeValue = "navigator.id.getAssertion"
21 |
22 | func checkU2FDevices(logger log.Logger) {
23 | // TODO: move this to initialization code, ans pass the device list to this function?
24 | // or maybe pass the token?...
25 | devices, err := u2fhid.Devices()
26 | if err != nil {
27 | logger.Fatal(err)
28 | }
29 | if len(devices) == 0 {
30 | logger.Fatal("no U2F tokens found")
31 | }
32 |
33 | // TODO: transform this into an iteration over all found devices
34 | for _, d := range devices {
35 | //d := devices[0]
36 | logger.Printf("manufacturer = %q, product = %q, vid = 0x%04x, pid = 0x%04x", d.Manufacturer, d.Product, d.ProductID, d.VendorID)
37 |
38 | dev, err := u2fhid.Open(d)
39 | if err != nil {
40 | logger.Fatal(err)
41 | }
42 | defer dev.Close()
43 | }
44 |
45 | }
46 |
47 | func doU2FAuthenticate(
48 | client *http.Client,
49 | baseURL string,
50 | userAgentString string,
51 | logger log.DebugLogger) error {
52 | logger.Printf("top of doU2fAuthenticate")
53 | url := baseURL + "/u2f/SignRequest"
54 | signRequest, err := http.NewRequest("GET", url, nil)
55 | if err != nil {
56 | logger.Fatal(err)
57 | }
58 | signRequest.Header.Set("User-Agent", userAgentString)
59 |
60 | signRequestResp, err := client.Do(signRequest) // Client.Get(targetUrl)
61 | if err != nil {
62 | logger.Printf("Failure to sign request req %s", err)
63 | return err
64 | }
65 | logger.Debugf(0, "Get url request did not failed %+v", signRequestResp)
66 |
67 | // Dont defer the body response Close ... as we need to close it explicitly
68 | // in the body of the function so that we can reuse the connection
69 | if signRequestResp.StatusCode != 200 {
70 | signRequestResp.Body.Close()
71 | logger.Printf("got error from call %s, url='%s'\n", signRequestResp.Status, url)
72 | err = errors.New("failed respose from sign request")
73 | return err
74 | }
75 |
76 | var webSignRequest u2f.WebSignRequest
77 | if err := json.NewDecoder(signRequestResp.Body).Decode(&webSignRequest); err != nil {
78 | //http.Error(w, "invalid response: "+err.Error(), http.StatusBadRequest)
79 | // return
80 | logger.Fatal(err)
81 | }
82 | io.Copy(ioutil.Discard, signRequestResp.Body)
83 | signRequestResp.Body.Close()
84 |
85 | // TODO: move this to initialization code, ans pass the device list to this function?
86 | // or maybe pass the token?...
87 | devices, err := u2fhid.Devices()
88 | if err != nil {
89 | logger.Fatal(err)
90 | return err
91 | }
92 | if len(devices) == 0 {
93 | err = errors.New("no U2F tokens found")
94 | logger.Println(err)
95 | return err
96 | }
97 |
98 | // TODO: transform this into an iteration over all found devices
99 | d := devices[0]
100 | logger.Printf("manufacturer = %q, product = %q, vid = 0x%04x, pid = 0x%04x", d.Manufacturer, d.Product, d.ProductID, d.VendorID)
101 |
102 | dev, err := u2fhid.Open(d)
103 | if err != nil {
104 | logger.Fatal(err)
105 | }
106 | defer dev.Close()
107 | t := u2ftoken.NewToken(dev)
108 |
109 | version, err := t.Version()
110 | if err != nil {
111 | logger.Fatal(err)
112 | }
113 | // TODO: Maybe use Debugf()?
114 | logger.Println("version:", version)
115 |
116 | ///////
117 | tokenAuthenticationClientData := u2f.ClientData{Typ: clientDataAuthenticationTypeValue, Challenge: webSignRequest.Challenge, Origin: webSignRequest.AppID}
118 | tokenAuthenticationBuf := new(bytes.Buffer)
119 | err = json.NewEncoder(tokenAuthenticationBuf).Encode(tokenAuthenticationClientData)
120 | if err != nil {
121 | logger.Fatal(err)
122 | }
123 | reqSignChallenge := sha256.Sum256(tokenAuthenticationBuf.Bytes())
124 |
125 | // TODO: update creation to silence linter
126 | challenge := make([]byte, 32)
127 | app := make([]byte, 32)
128 |
129 | challenge = reqSignChallenge[:]
130 | reqSingApp := sha256.Sum256([]byte(webSignRequest.AppID))
131 | app = reqSingApp[:]
132 |
133 | // We find out what key is associated to the currently inserted device.
134 | keyIsKnown := false
135 | var req u2ftoken.AuthenticateRequest
136 | var keyHandle []byte
137 | for _, registeredKey := range webSignRequest.RegisteredKeys {
138 | decodedHandle, err := base64.RawURLEncoding.DecodeString(registeredKey.KeyHandle)
139 | if err != nil {
140 | logger.Fatal(err)
141 | }
142 | keyHandle = decodedHandle
143 |
144 | req = u2ftoken.AuthenticateRequest{
145 | Challenge: challenge,
146 | Application: app,
147 | KeyHandle: keyHandle,
148 | }
149 |
150 | //logger.Printf("%+v", req)
151 | if err := t.CheckAuthenticate(req); err == nil {
152 | keyIsKnown = true
153 | break
154 | }
155 | }
156 | if !keyIsKnown {
157 | err = errors.New("key is not known")
158 | return err
159 | }
160 |
161 | // Now we ask the token to sign/authenticate
162 | logger.Println("authenticating, provide user presence")
163 | var rawBytes []byte
164 | for {
165 | res, err := t.Authenticate(req)
166 | if err == u2ftoken.ErrPresenceRequired {
167 | time.Sleep(200 * time.Millisecond)
168 | continue
169 | } else if err != nil {
170 | logger.Fatal(err)
171 | }
172 | rawBytes = res.RawResponse
173 | logger.Printf("counter = %d, signature = %x", res.Counter, res.Signature)
174 | break
175 | }
176 |
177 | // now we do the last request
178 | var signRequestResponse u2f.SignResponse
179 | signRequestResponse.KeyHandle = base64.RawURLEncoding.EncodeToString(keyHandle)
180 | signRequestResponse.SignatureData = base64.RawURLEncoding.EncodeToString(rawBytes)
181 | signRequestResponse.ClientData = base64.RawURLEncoding.EncodeToString(tokenAuthenticationBuf.Bytes())
182 |
183 | //
184 | webSignRequestBuf := &bytes.Buffer{}
185 | err = json.NewEncoder(webSignRequestBuf).Encode(signRequestResponse)
186 | if err != nil {
187 | logger.Fatal(err)
188 | }
189 |
190 | url = baseURL + "/u2f/SignResponse"
191 | webSignRequest2, err := http.NewRequest("POST", url, webSignRequestBuf)
192 | if err != nil {
193 | logger.Printf("Failure to make http request")
194 | return err
195 | }
196 | webSignRequest2.Header.Set("User-Agent", userAgentString)
197 |
198 | signRequestResp2, err := client.Do(webSignRequest2) // Client.Get(targetUrl)
199 | if err != nil {
200 | logger.Printf("Failure to sign request req %s", err)
201 | return err
202 | }
203 |
204 | defer signRequestResp2.Body.Close()
205 | if signRequestResp2.StatusCode != 200 {
206 | logger.Printf("got error from call %s, url='%s'\n", signRequestResp2.Status, url)
207 | return err
208 | }
209 | io.Copy(ioutil.Discard, signRequestResp2.Body)
210 | return nil
211 | }
212 |
--------------------------------------------------------------------------------
/cmd/keymasterd/idp_oidc_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | //"fmt"
6 | stdlog "log"
7 | "net/http"
8 | "net/url"
9 | "os"
10 | "strconv"
11 | "strings"
12 | "testing"
13 | //"time"
14 |
15 | "github.com/Symantec/Dominator/lib/log/debuglogger"
16 | "gopkg.in/square/go-jose.v2/jwt"
17 | //"golang.org/x/net/context"
18 | //"golang.org/x/oauth2"
19 | )
20 |
21 | func init() {
22 | //logger = stdlog.New(os.Stderr, "", stdlog.LstdFlags)
23 | slogger := stdlog.New(os.Stderr, "", stdlog.LstdFlags)
24 | logger = debuglogger.New(slogger)
25 | /*
26 | http.HandleFunc("/userinfo", userinfoHandler)
27 | http.HandleFunc("/token", tokenHandler)
28 | http.HandleFunc("/", handler)
29 | logger.Printf("about to start server")
30 | go http.ListenAndServe(":12345", nil)
31 | time.Sleep(20 * time.Millisecond)
32 | _, err := http.Get("http://localhost:12345")
33 | if err != nil {
34 | logger.Fatal(err)
35 | }
36 | */
37 | }
38 |
39 | func TestIDPOpenIDCMetadataHandler(t *testing.T) {
40 | state, passwdFile, err := setupValidRuntimeStateSigner()
41 | if err != nil {
42 | t.Fatal(err)
43 | }
44 | defer os.Remove(passwdFile.Name()) // clean up
45 | state.pendingOauth2 = make(map[string]pendingAuth2Request)
46 |
47 | url := idpOpenIDCConfigurationDocumentPath
48 | req, err := http.NewRequest("GET", url, nil)
49 | if err != nil {
50 | t.Fatal(err)
51 | }
52 | _, err = checkRequestHandlerCode(req, state.idpOpenIDCDiscoveryHandler, http.StatusOK)
53 | if err != nil {
54 | t.Fatal(err)
55 | }
56 | }
57 |
58 | func TestIDPOpenIDCJWKSHandler(t *testing.T) {
59 | state, passwdFile, err := setupValidRuntimeStateSigner()
60 | if err != nil {
61 | t.Fatal(err)
62 | }
63 | defer os.Remove(passwdFile.Name()) // clean up
64 | state.pendingOauth2 = make(map[string]pendingAuth2Request)
65 |
66 | url := idpOpenIDCJWKSPath
67 | req, err := http.NewRequest("GET", url, nil)
68 | if err != nil {
69 | t.Fatal(err)
70 | }
71 | _, err = checkRequestHandlerCode(req, state.idpOpenIDCJWKSHandler, http.StatusOK)
72 | if err != nil {
73 | t.Fatal(err)
74 | }
75 | }
76 |
77 | func TestIDPOpenIDCAuthorizationHandlerSuccess(t *testing.T) {
78 | state, passwdFile, err := setupValidRuntimeStateSigner()
79 | if err != nil {
80 | t.Fatal(err)
81 | }
82 | defer os.Remove(passwdFile.Name()) // clean up
83 | state.pendingOauth2 = make(map[string]pendingAuth2Request)
84 | state.Config.Base.AllowedAuthBackendsForWebUI = []string{"password"}
85 | state.signerPublicKeyToKeymasterKeys()
86 | state.HostIdentity = "localhost"
87 |
88 | valid_client_id := "valid_client_id"
89 | valid_client_secret := "secret_password"
90 | valid_redirect_uri := "https://localhost:12345"
91 | clientConfig := OpenIDConnectClientConfig{ClientID: valid_client_id, ClientSecret: valid_client_secret, AllowedRedirectURLRE: []string{"localhost"}}
92 | state.Config.OpenIDConnectIDP.Client = append(state.Config.OpenIDConnectIDP.Client, clientConfig)
93 |
94 | //url := idpOpenIDCAuthorizationPath
95 | req, err := http.NewRequest("GET", idpOpenIDCAuthorizationPath, nil)
96 | if err != nil {
97 | t.Fatal(err)
98 | }
99 |
100 | //First we do a simple request.. no auth should fail for now.. after build out it
101 | // should be a redirect to the login page
102 | _, err = checkRequestHandlerCode(req, state.idpOpenIDCAuthorizationHandler, http.StatusUnauthorized)
103 | if err != nil {
104 | t.Fatal(err)
105 | }
106 | // now we add a cookie for auth
107 | cookieVal, err := state.setNewAuthCookie(nil, "username", AuthTypePassword)
108 | if err != nil {
109 | t.Fatal(err)
110 | }
111 | authCookie := http.Cookie{Name: authCookieName, Value: cookieVal}
112 | req.AddCookie(&authCookie)
113 | // and we retry with no params... it should fail again
114 | _, err = checkRequestHandlerCode(req, state.idpOpenIDCAuthorizationHandler, http.StatusBadRequest)
115 | if err != nil {
116 | t.Fatal(err)
117 | }
118 | // add the required params
119 | form := url.Values{}
120 | form.Add("scope", "openid")
121 | form.Add("response_type", "code")
122 | form.Add("client_id", valid_client_id)
123 | form.Add("redirect_uri", valid_redirect_uri)
124 | form.Add("nonce", "123456789")
125 | form.Add("state", "this is my state")
126 |
127 | postReq, err := http.NewRequest("POST", idpOpenIDCAuthorizationPath, strings.NewReader(form.Encode()))
128 | if err != nil {
129 | t.Fatal(err)
130 | }
131 | postReq.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
132 | postReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
133 |
134 | postReq.AddCookie(&authCookie)
135 |
136 | rr, err := checkRequestHandlerCode(postReq, state.idpOpenIDCAuthorizationHandler, http.StatusFound)
137 | if err != nil {
138 | t.Fatal(err)
139 | }
140 | t.Logf("%+v", rr)
141 | locationText := rr.Header().Get("Location")
142 | t.Logf("location=%s", locationText)
143 | location, err := url.Parse(locationText)
144 | if err != nil {
145 | t.Fatal(err)
146 | }
147 | rCode := location.Query().Get("code")
148 | t.Logf("rCode=%s", rCode)
149 | tok, err := jwt.ParseSigned(rCode)
150 | if err != nil {
151 | t.Fatal(err)
152 | }
153 | t.Logf("tok=%+v", tok)
154 | //out := jwt.Claims{}
155 | out := keymasterdCodeToken{}
156 | if err := tok.Claims(state.Signer.Public(), &out); err != nil {
157 | t.Fatal(err)
158 | }
159 | t.Logf("out=%+v", out)
160 |
161 | //now we do a token request
162 | tokenForm := url.Values{}
163 | tokenForm.Add("grant_type", "authorization_code")
164 | tokenForm.Add("redirect_uri", valid_redirect_uri)
165 | tokenForm.Add("code", rCode)
166 |
167 | tokenReq, err := http.NewRequest("POST", idpOpenIDCTokenPath, strings.NewReader(tokenForm.Encode()))
168 | if err != nil {
169 | t.Fatal(err)
170 | }
171 | tokenReq.Header.Add("Content-Length", strconv.Itoa(len(tokenForm.Encode())))
172 | tokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
173 | tokenReq.SetBasicAuth(valid_client_id, valid_client_secret)
174 | //idpOpenIDCTokenHandler
175 |
176 | tokenRR, err := checkRequestHandlerCode(tokenReq, state.idpOpenIDCTokenHandler, http.StatusOK)
177 | if err != nil {
178 | t.Fatal(err)
179 | }
180 | resultAccessToken := accessToken{}
181 | body := tokenRR.Result().Body
182 | err = json.NewDecoder(body).Decode(&resultAccessToken)
183 | if err != nil {
184 | t.Fatal(err)
185 | }
186 | t.Logf("resultAccessToken='%+v'", resultAccessToken)
187 |
188 | //now the userinfo
189 | userinfoForm := url.Values{}
190 | userinfoForm.Add("access_token", resultAccessToken.AccessToken)
191 |
192 | userinfoReq, err := http.NewRequest("POST", idpOpenIDCUserinfoPath, strings.NewReader(userinfoForm.Encode()))
193 | if err != nil {
194 | t.Fatal(err)
195 | }
196 | userinfoReq.Header.Add("Content-Length", strconv.Itoa(len(userinfoForm.Encode())))
197 | userinfoReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
198 |
199 | _, err = checkRequestHandlerCode(userinfoReq, state.idpOpenIDCUserinfoHandler, http.StatusOK)
200 | if err != nil {
201 | t.Fatal(err)
202 | }
203 |
204 | }
205 |
--------------------------------------------------------------------------------
/cmd/keymasterd/dependency_monitor_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "net"
7 | "testing"
8 | "time"
9 | )
10 |
11 | //These are the same certs on authutil_test.go
12 |
13 | const testRootCAPem = `-----BEGIN CERTIFICATE-----
14 | MIIE1jCCAr4CAQowDQYJKoZIhvcNAQELBQAwMTELMAkGA1UEBhMCVVMxEDAOBgNV
15 | BAoMB1Rlc3RPcmcxEDAOBgNVBAsMB1Rlc3QgQ0EwHhcNMTcwMTA1MTc0NzQ1WhcN
16 | MzYxMjMxMTc0NzQ1WjAxMQswCQYDVQQGEwJVUzEQMA4GA1UECgwHVGVzdE9yZzEQ
17 | MA4GA1UECwwHVGVzdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
18 | AK178Hyl2iJ8l4k/iHWgACtkLLa4n6sJo6DB8s1t98ILgU5ykT30jslWPH/QJvfL
19 | /OAvgDcq2+ScwZThGFMxotBKnoufy88wAV/8SAwdy6rbAatW6v6K8+dgPEcka2jf
20 | aqOby27+vrglQePQjjUMZoqr4qAizCUwCGZQUPhfSorBUcyWupKVZe8kDmD395yT
21 | yRf3z5rJAMFzJmNOv/6ZOA2Scv14xZWTezBlr3E2zBCr0iYpYsqh5dG2ube9DYyX
22 | 0fXfrfaa8jstlu9jrYltxRmlCBAdoFB1N7eN6V/CsKc9nKc4dFkNDJng1Z4dpPYW
23 | v+HzYI4UBsnkYFtpt5VV3M+Ys/FE1ARE4ah4R69FK+eBKCkpRUszbm/fjnt/QvDX
24 | pCBcpa8vddgAgFKz9kvpO3lfA+jBb2euzX1lOL3ETooE0sEtFPK81P2M62BxsoIa
25 | ztAWOQlLQotDtY156YaUoREXCjLkpiitEhpn9+nlMAPlA9X2iVQWIgpkAepbu+rU
26 | ouODruaOoxc67GyTXUT7NIj3IE3PxPn5LBta+SOJ1DsUcC7aBG/x10/ifYLq/H4J
27 | JxnHhac+S5aKxeCBTzT84JKltbYiqhhoGVaXYp6AwbOkqUMYyhEKelcvtdKc7fKF
28 | +0DRAymVdGzV+nhwxGqwgarlXzZwsWkSj/A0+J7l2Ty9AgMBAAEwDQYJKoZIhvcN
29 | AQELBQADggIBAKsYxpXFY4dyarnvMjAmX6EXcwqDfOJXgQdnX7QMzYlygMb+m2h1
30 | Nuk4HTMlmQtkLba6JQd5NQw42RBYl1ef0PwwJoVlznht7Hec9wEopa5pyzyerSPT
31 | nblh1TRKVffLQ1SyTO6yPgdn8rct5n2M0tW+nW7SZuWkzsc6swVEfJyTykXbMYHg
32 | aSap7oMUr0MaffQjihzwk585fY8GvPeqdrer/k7d6MD05NWkqeXaMitI4hNWTyTu
33 | 7yjppKcGRVaNHmhk4867Jz6RZzxWbZBQe7tqaqmdqKLvz8/7j/VFjBTO2NebE0FV
34 | LxKcpv6QklH0UWqzWBn8LDZRYz6D1PglGjgh8ERHOTKJW0BRdIlzljZwND2f5lSx
35 | 0HBSJYqTU38iBCHkxf8hYdiPI8Jw2CCt4l+hCwQhtIWgCrENSIa1sT9j3TPy+zwq
36 | 2GHf9xTpjV1pVEyuFPf1bllPUeOFXprJiq2J4rnE/fqyabO0uSX35ucdG+YGVyMa
37 | BkwaEPvqvwremmh+xLYye6scQ+A/Wn9fN/8VN0W22t37O2VQNgsTANyIZwKZlxJc
38 | fSkkhF5M7t/rWTtO0MXnCuIDJu3QJneRvOSBlvIabkVGEt9tZQCzK9wiJqsBkLSy
39 | FCdYqoFk7gKsLkv51iMd+oItlEBuSEJSs1N+F5knShYfJdpHYDY+84ul
40 | -----END CERTIFICATE-----`
41 |
42 | const localhostCertPem = `-----BEGIN CERTIFICATE-----
43 | MIIDvTCCAaUCBQLd4CbMMA0GCSqGSIb3DQEBCwUAMDExCzAJBgNVBAYTAlVTMRAw
44 | DgYDVQQKDAdUZXN0T3JnMRAwDgYDVQQLDAdUZXN0IENBMB4XDTE3MDEwNTE3NTQw
45 | NVoXDTM2MTIzMTE3NTQwNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkq
46 | hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3w9PziLdiuYkq4sxVEQkq0HIq6Ad/51o
47 | k98AG87DqbG4hcPcbP/ItEHv4NdYZOtagY5twzb+sjcDC4plubQ9ewMKsGeBlDG4
48 | xxkaMYJeqx8usmiT+fisiI6B+tM38ahxtYqcyCp3ou+WrSEuRb+70UXnztR8RT4R
49 | ISkpq0wuj1Qcmxp9GkyA6aH4gwlj7EocqhIUCF2LxS8U0u2xvbCxHVSQFf4VxrrW
50 | ZwoHaI8GWAZb/IAe9gxoau5rtJ90gpesO5AVx1VPX+WhpSCmVGPnJdknMXC6mjDJ
51 | rMUW0KX8/dX7OdXAtSdDLzm6Yd4cI2DTnGVfckGIzsIYlGhBwxIUGwIDAQABMA0G
52 | CSqGSIb3DQEBCwUAA4ICAQAlendYr/Xrh2h9tWRNB7kCS8OSaJqsaLP7nDWlwbj/
53 | 93QRB51qg2BHUy+tCzXAChq9HnaVcOLrDa0V3Amx5J6LauIBTnRBlHQGaw9Tu4Bo
54 | UqMusHKM9/brNTDRUq8eafJhdfsXWs+cwKj9+Zh1UX0gc8yzgJSLCfkJgeuf62vP
55 | tLAiJAxanxwT2hqtHnuVLu/UUmfx4n0IOALE8tARcLwZkKfmbsXiIY0ZIb/kwCuF
56 | APYy4bmjRXfA9CKnHcfwOxYNqsAPad/MLme9bSBtOuY75VY3UDeno6Uz5PZL4163
57 | 8q+MedT6yinEtGaEllnpWMHa4NC0w+Klpk28fONEIxfqjCvlugRkIlCS3T9qfS9R
58 | vhqwn1V+13JRYxLwMtVpXPdfQbBy7PG9VaQAyRsMrIGsG8esHx+OUMKP3hvh07gs
59 | Lhmjn8SWaFpazldaNRcbOKazxHcwY+yL21VEL5CdA8GcjXEls3YaCuw54QBPJaoB
60 | Yg4ybiaio7h8od1Nydf3mbQ9gmMruLpGHw7RKAGxBD6Ukt0uPAMKOgaL9H2YOSzB
61 | SsYyE/ONrTbxpHZPQG1SszKuKUzGsPEwlMTwt8NHVTixKy/ttMA7NhN8KAYJrJQw
62 | Z65R0mZFpYSL31jrfV4Q4mhFj6/Cr8rgmH++82FWfg88gf4lPk6/iDZtHvMMBUXy
63 | Pg==
64 | -----END CERTIFICATE-----`
65 |
66 | const localhostKeyPem = `-----BEGIN PRIVATE KEY-----
67 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDfD0/OIt2K5iSr
68 | izFURCSrQciroB3/nWiT3wAbzsOpsbiFw9xs/8i0Qe/g11hk61qBjm3DNv6yNwML
69 | imW5tD17AwqwZ4GUMbjHGRoxgl6rHy6yaJP5+KyIjoH60zfxqHG1ipzIKnei75at
70 | IS5Fv7vRRefO1HxFPhEhKSmrTC6PVBybGn0aTIDpofiDCWPsShyqEhQIXYvFLxTS
71 | 7bG9sLEdVJAV/hXGutZnCgdojwZYBlv8gB72DGhq7mu0n3SCl6w7kBXHVU9f5aGl
72 | IKZUY+cl2ScxcLqaMMmsxRbQpfz91fs51cC1J0MvObph3hwjYNOcZV9yQYjOwhiU
73 | aEHDEhQbAgMBAAECggEBALK97lFclvLK4O+lpm3D/S5OlKMSt3cxh6+WrtuZoCjH
74 | BPoLrQKbJRVtEO+3IFoeTnQq0cHwu7/LXWFOEZ3x1KJSGaqqBqfeABdrAhZSRdIS
75 | NrU4H/vbTUZQC9AWmWnIdPXokSHFBgFGxBMP16iEr9hOkCapFrvVtJxCA+YEMfsf
76 | CKK9azdS/6aA4LxFKFuf7EwZz3uD5BqQXM/1vrAjmmATzE5yoJUsUPwJNwTlwTLs
77 | 53tOoZAIhYiWMXL1USXcKm3z8IJq8SgfgOUsK9X6IEEIga/IMwimPl966RlJyIsR
78 | U4RzqG+cP5D2bC9n1M3aBUmUGcvWV7E3nVg+bbuNYIECgYEA76lfyMCbnnBzqagx
79 | UpNR6PXn53RQicQktt+wFFexknf01ZmX+Tn3slSVtsms2xJRtUeNmljPKa7CIMWi
80 | CaBLA2fsUjkPB0EQk6v8MzJeEJOpfFPWC+miKZhnV17rNkuuCwUdPFIz7g66/HU5
81 | /W4gzrUkttw597cpOkOoiUrd16sCgYEA7kQzBa0ille35TlicqSQqeLQrTSga7b2
82 | U0NjSwu0szCGw2LNV87H6Fhxw+MqIQM5VDTPb3bp9T0Uv2n0moENbGO5dD4ZGuNC
83 | mA+AmKNeUBx81Jx4DumGxaU3eATkg6KlNKNccHtXF64k8blM9Y6q6ncCtr4UVz3H
84 | ekSGNXx/hVECgYBf+o7XkPtBmntXqHoIPeOBzmlPMi/G3HxvmGml2/DLXar5mAda
85 | 0jI2gtVqXJ4TJeT/GmbFN2fPo6MvCLb57+3asVXdH+i62P3QhgH8ZuFw9hHcLp78
86 | Kla9HcHVJbhBCFHtK+EndSxC3DdaP4A31FDjN3w6lzvHztx97vah9Q+e/QKBgQCk
87 | 8Y+EuXO9MmJ7DDvL84K2KO+fSFRZ3SIvR/JgDG1+svRIJIjU5bBcd4XiPst2aR3x
88 | 3lFP77lM7YkEbdxIbViWlX7YKvkENRlv3SOAB3CN8vqz0NIIOL/06Ug6DOEJA7ps
89 | cz7WG3ySRxsKP+Y4BBjsEZFOYs4ACyOhz/g85L/+0QKBgQCjjTLjcSNg+fBZRXHC
90 | YwzyBA/WXBPve5qo17Bt91knZ4m+xOVmRcswNG2U0eFrm+nNlk84Kj3TMRAv8Stx
91 | GuCdIOQpn0IWClccTMjwc0AhJStSckNdSUQcsRl6LRnRHa3oCIs3hxnkiEHYch6e
92 | dcxWzhBDbzeIV9SvcTwLx/ghQg==
93 | -----END PRIVATE KEY-----`
94 |
95 | func getTLSconfig() (*tls.Config, error) {
96 | cert, err := tls.X509KeyPair([]byte(localhostCertPem), []byte(localhostKeyPem))
97 | if err != nil {
98 | return &tls.Config{}, err
99 | }
100 |
101 | return &tls.Config{
102 | MinVersion: tls.VersionSSL30,
103 | MaxVersion: tls.VersionTLS12,
104 | Certificates: []tls.Certificate{cert},
105 | ServerName: "localhost",
106 | }, nil
107 | }
108 |
109 | func init() {
110 | //we also make a simple tls listener
111 | //
112 | config, _ := getTLSconfig()
113 | ln, _ := tls.Listen("tcp", "127.0.0.1:10638", config)
114 | go func(ln net.Listener) {
115 | for {
116 | conn, err := ln.Accept()
117 | if err != nil {
118 | continue
119 | }
120 | //log.Printf("Got connection!!!!")
121 | conn.Write([]byte("hello\n"))
122 | conn.Close()
123 | }
124 | }(ln)
125 | // On single core systems we needed to ensure that the server is started before
126 | // we create other testing goroutines. By sleeping we yield the cpu and allow
127 | // ListenAndServe to progress
128 | time.Sleep(20 * time.Millisecond)
129 | }
130 |
131 | func TestCheckLDAPURLsSuccess(t *testing.T) {
132 | certPool := x509.NewCertPool()
133 | ok := certPool.AppendCertsFromPEM([]byte(testRootCAPem))
134 | if !ok {
135 | t.Fatal("cannot add certs to certpool")
136 | }
137 | err := checkLDAPURLs("ldaps://localhost:10638", "somename", certPool)
138 | if err != nil {
139 | t.Logf("Failed to check ldap url")
140 | t.Fatal(err)
141 | }
142 | }
143 |
144 | func TestCheckLDAPURLsFailNoValidTargets(t *testing.T) {
145 | certPool := x509.NewCertPool()
146 | ok := certPool.AppendCertsFromPEM([]byte(testRootCAPem))
147 | if !ok {
148 | t.Fatal("cannot add certs to certpool")
149 | }
150 | err := checkLDAPURLs("ldap://localhost:10638", "somename", certPool)
151 | if err == nil {
152 | t.Fatal("Should have failed")
153 | }
154 | }
155 |
156 | func TestCheckLDAPConfigsSuccessBoth(t *testing.T) {
157 | certPool := x509.NewCertPool()
158 | ok := certPool.AppendCertsFromPEM([]byte(testRootCAPem))
159 | if !ok {
160 | t.Fatal("cannot add certs to certpool")
161 | }
162 | var config AppConfigFile
163 | config.Ldap.LDAPTargetURLs = "ldaps://localhost:10638"
164 | config.UserInfo.Ldap.LDAPTargetURLs = "ldaps://localhost:10638"
165 | checkLDAPConfigs(config, certPool)
166 | }
167 |
--------------------------------------------------------------------------------
/eventmon/eventrecorder/impl.go:
--------------------------------------------------------------------------------
1 | package eventrecorder
2 |
3 | import (
4 | "bufio"
5 | "crypto/x509"
6 | "encoding/gob"
7 | "os"
8 | "syscall"
9 | "time"
10 |
11 | "github.com/Symantec/Dominator/lib/fsutil"
12 | "github.com/Symantec/Dominator/lib/log"
13 | "golang.org/x/crypto/ssh"
14 | )
15 |
16 | const (
17 | bufferLength = 16
18 | filePerms = syscall.S_IRUSR | syscall.S_IWUSR | syscall.S_IRGRP |
19 | syscall.S_IROTH
20 | durationMonth = time.Hour * 24 * 31
21 | )
22 |
23 | func newEventRecorder(filename string, logger log.Logger) (
24 | *EventRecorder, error) {
25 | eventsMap, err := loadEvents(filename)
26 | if err != nil && !os.IsNotExist(err) {
27 | return nil, err
28 | }
29 | authChannel := make(chan *AuthInfo, bufferLength)
30 | requestEventsChannel := make(chan chan<- Events, bufferLength)
31 | serviceProviderLoginChannel := make(chan *SPLoginInfo, bufferLength)
32 | sshCertChannel := make(chan *ssh.Certificate, bufferLength)
33 | webLoginChannel := make(chan string, bufferLength)
34 | x509CertChannel := make(chan *x509.Certificate, bufferLength)
35 | sr := &EventRecorder{
36 | filename: filename,
37 | logger: logger,
38 | eventsMap: eventsMap,
39 | AuthChannel: authChannel,
40 | RequestEventsChannel: requestEventsChannel,
41 | ServiceProviderLoginChannel: serviceProviderLoginChannel,
42 | SshCertChannel: sshCertChannel,
43 | WebLoginChannel: webLoginChannel,
44 | X509CertChannel: x509CertChannel,
45 | }
46 | go sr.eventLoop(authChannel, requestEventsChannel,
47 | serviceProviderLoginChannel, sshCertChannel, webLoginChannel,
48 | x509CertChannel)
49 | return sr, nil
50 | }
51 |
52 | func loadEvents(filename string) (map[string]*eventsListType, error) {
53 | file, err := os.Open(filename)
54 | if err != nil {
55 | return make(map[string]*eventsListType), err
56 | }
57 | defer file.Close()
58 | reader := bufio.NewReader(file)
59 | decoder := gob.NewDecoder(reader)
60 | var events EventsMap
61 | if err := decoder.Decode(&events); err != nil {
62 | return nil, err
63 | }
64 | eventsMap := make(map[string]*eventsListType, len(events))
65 | minCreateTime := uint64(time.Now().Add(-durationMonth).Unix())
66 | for username, eventsSlice := range events {
67 | eventsList := &eventsListType{}
68 | for _, savedEvent := range eventsSlice {
69 | if savedEvent.CreateTime < minCreateTime {
70 | continue
71 | }
72 | event := &eventType{
73 | EventType: savedEvent,
74 | older: eventsList.newest,
75 | }
76 | if eventsList.newest != nil {
77 | eventsList.newest.newer = event
78 | }
79 | eventsList.newest = event
80 | if eventsList.oldest == nil {
81 | eventsList.oldest = event
82 | }
83 | }
84 | eventsMap[username] = eventsList
85 | }
86 | return eventsMap, nil
87 | }
88 |
89 | func (sr *EventRecorder) eventLoop(authChannel <-chan *AuthInfo,
90 | requestEventsChannel <-chan chan<- Events,
91 | serviceProviderLoginChannel <-chan *SPLoginInfo,
92 | sshCertChannel <-chan *ssh.Certificate, webLoginChannel <-chan string,
93 | x509CertChannel <-chan *x509.Certificate) {
94 | var lastEvents *Events
95 | sr.getEventsList(&lastEvents)
96 | hourlyTimer := time.NewTimer(time.Hour)
97 | saveTimer := time.NewTimer(time.Hour)
98 | saveTimer.Stop()
99 | for {
100 | select {
101 | case auth := <-authChannel:
102 | saveTimer.Reset(time.Second * 5)
103 | lastEvents = nil
104 | sr.recordAuthEvent(auth.Username, auth.AuthType, auth.VIPAuthType)
105 | case spLogin := <-serviceProviderLoginChannel:
106 | saveTimer.Reset(time.Second * 5)
107 | lastEvents = nil
108 | sr.recordSPLoginEvent(spLogin.Username, spLogin.URL)
109 | case cert := <-sshCertChannel:
110 | saveTimer.Reset(time.Second * 5)
111 | lastEvents = nil
112 | sr.recordCertEvent(cert.ValidPrincipals[0],
113 | time.Until(time.Unix(int64(cert.ValidBefore), 0)),
114 | true, false)
115 | case username := <-webLoginChannel:
116 | saveTimer.Reset(time.Second * 5)
117 | lastEvents = nil
118 | sr.recordWebLoginEvent(username)
119 | case cert := <-x509CertChannel:
120 | saveTimer.Reset(time.Second * 5)
121 | lastEvents = nil
122 | sr.recordCertEvent(cert.Subject.CommonName,
123 | time.Until(cert.NotAfter), false, true)
124 | case <-hourlyTimer.C:
125 | hourlyTimer.Reset(time.Hour)
126 | if sr.expireOldEvents() {
127 | saveTimer.Reset(time.Second * 5)
128 | lastEvents = nil
129 | }
130 | case replyChannel := <-requestEventsChannel:
131 | select { // Non-blocking.
132 | case replyChannel <- *sr.getEventsList(&lastEvents):
133 | default:
134 | }
135 | case <-saveTimer.C:
136 | sr.getEventsList(&lastEvents)
137 | if err := saveEvents(sr.filename, lastEvents.Events); err != nil {
138 | sr.logger.Println(err)
139 | }
140 | }
141 | }
142 | }
143 |
144 | func (sr *EventRecorder) recordAuthEvent(username string, authType uint,
145 | vipAuthType uint8) {
146 | event := &eventType{EventType: EventType{
147 | AuthType: authType,
148 | VIPAuthType: vipAuthType},
149 | }
150 | sr.recordEvent(username, event)
151 | }
152 |
153 | func (sr *EventRecorder) recordCertEvent(username string,
154 | lifetime time.Duration, ssh, x509 bool) {
155 | lifetimeSeconds := uint32(lifetime.Seconds() + 0.5)
156 | if lifetimeSeconds >= 3600 {
157 | hours := lifetimeSeconds / 3600
158 | hoursPlus := (lifetimeSeconds + 60) / 3600
159 | if hoursPlus > hours {
160 | lifetimeSeconds = hoursPlus * 3600
161 | }
162 | } else if lifetimeSeconds >= 60 {
163 | minutes := lifetimeSeconds / 60
164 | minutesPlus := (lifetimeSeconds + 1) / 60
165 | if minutesPlus > minutes {
166 | lifetimeSeconds = minutesPlus * 60
167 | }
168 | }
169 | event := &eventType{
170 | EventType: EventType{
171 | LifetimeSeconds: lifetimeSeconds,
172 | Ssh: ssh,
173 | X509: x509,
174 | },
175 | }
176 | sr.recordEvent(username, event)
177 | }
178 |
179 | func (sr *EventRecorder) recordEvent(username string, event *eventType) {
180 | eventsList := sr.eventsMap[username]
181 | if eventsList == nil {
182 | eventsList = &eventsListType{}
183 | sr.eventsMap[username] = eventsList
184 | }
185 | event.CreateTime = uint64(time.Now().Unix())
186 | event.older = eventsList.newest
187 | if eventsList.newest != nil {
188 | eventsList.newest.newer = event
189 | }
190 | eventsList.newest = event
191 | if eventsList.oldest == nil {
192 | eventsList.oldest = event
193 | }
194 | }
195 |
196 | func (sr *EventRecorder) recordSPLoginEvent(username, url string) {
197 | event := &eventType{EventType: EventType{ServiceProviderUrl: url}}
198 | sr.recordEvent(username, event)
199 | }
200 |
201 | func (sr *EventRecorder) recordWebLoginEvent(username string) {
202 | event := &eventType{EventType: EventType{WebLogin: true}}
203 | sr.recordEvent(username, event)
204 | }
205 |
206 | func (sr *EventRecorder) getEventsList(lastEvents **Events) *Events {
207 | if *lastEvents != nil {
208 | return *lastEvents
209 | }
210 | startTime := time.Now()
211 | eventsMap := make(map[string][]EventType, len(sr.eventsMap))
212 | for username, eventsList := range sr.eventsMap {
213 | events := make([]EventType, 0)
214 | for event := eventsList.newest; event != nil; event = event.older {
215 | events = append(events, event.EventType)
216 | }
217 | eventsMap[username] = events
218 | }
219 | *lastEvents = &Events{time.Since(startTime), eventsMap}
220 | return *lastEvents
221 | }
222 |
223 | func saveEvents(filename string, eventsMap EventsMap) error {
224 | file, err := fsutil.CreateRenamingWriter(filename, filePerms)
225 | if err != nil {
226 | return err
227 | }
228 | defer file.Close()
229 | writer := bufio.NewWriter(file)
230 | defer writer.Flush()
231 | encoder := gob.NewEncoder(writer)
232 | if err := encoder.Encode(eventsMap); err != nil {
233 | return err
234 | }
235 | return nil
236 | }
237 |
238 | func (sr *EventRecorder) expireOldEvents() bool {
239 | minCreateTime := uint64(time.Now().Add(-durationMonth).Unix())
240 | changed := false
241 | for _, eventsList := range sr.eventsMap {
242 | for event := eventsList.oldest; event != nil; event = event.newer {
243 | if event.CreateTime >= minCreateTime {
244 | break
245 | }
246 | eventsList.oldest = event.newer
247 | if event.newer == nil {
248 | eventsList.newest = nil
249 | } else {
250 | event.newer.older = nil
251 | }
252 | changed = true
253 | }
254 | }
255 | return changed
256 | }
257 |
--------------------------------------------------------------------------------
/lib/vip/vip_test.go:
--------------------------------------------------------------------------------
1 | package vip
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "fmt"
7 | "net/http"
8 | "testing"
9 | "time"
10 | )
11 |
12 | const rootCAPem = `-----BEGIN CERTIFICATE-----
13 | MIIE1jCCAr4CAQowDQYJKoZIhvcNAQELBQAwMTELMAkGA1UEBhMCVVMxEDAOBgNV
14 | BAoMB1Rlc3RPcmcxEDAOBgNVBAsMB1Rlc3QgQ0EwHhcNMTcwMTA1MTc0NzQ1WhcN
15 | MzYxMjMxMTc0NzQ1WjAxMQswCQYDVQQGEwJVUzEQMA4GA1UECgwHVGVzdE9yZzEQ
16 | MA4GA1UECwwHVGVzdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
17 | AK178Hyl2iJ8l4k/iHWgACtkLLa4n6sJo6DB8s1t98ILgU5ykT30jslWPH/QJvfL
18 | /OAvgDcq2+ScwZThGFMxotBKnoufy88wAV/8SAwdy6rbAatW6v6K8+dgPEcka2jf
19 | aqOby27+vrglQePQjjUMZoqr4qAizCUwCGZQUPhfSorBUcyWupKVZe8kDmD395yT
20 | yRf3z5rJAMFzJmNOv/6ZOA2Scv14xZWTezBlr3E2zBCr0iYpYsqh5dG2ube9DYyX
21 | 0fXfrfaa8jstlu9jrYltxRmlCBAdoFB1N7eN6V/CsKc9nKc4dFkNDJng1Z4dpPYW
22 | v+HzYI4UBsnkYFtpt5VV3M+Ys/FE1ARE4ah4R69FK+eBKCkpRUszbm/fjnt/QvDX
23 | pCBcpa8vddgAgFKz9kvpO3lfA+jBb2euzX1lOL3ETooE0sEtFPK81P2M62BxsoIa
24 | ztAWOQlLQotDtY156YaUoREXCjLkpiitEhpn9+nlMAPlA9X2iVQWIgpkAepbu+rU
25 | ouODruaOoxc67GyTXUT7NIj3IE3PxPn5LBta+SOJ1DsUcC7aBG/x10/ifYLq/H4J
26 | JxnHhac+S5aKxeCBTzT84JKltbYiqhhoGVaXYp6AwbOkqUMYyhEKelcvtdKc7fKF
27 | +0DRAymVdGzV+nhwxGqwgarlXzZwsWkSj/A0+J7l2Ty9AgMBAAEwDQYJKoZIhvcN
28 | AQELBQADggIBAKsYxpXFY4dyarnvMjAmX6EXcwqDfOJXgQdnX7QMzYlygMb+m2h1
29 | Nuk4HTMlmQtkLba6JQd5NQw42RBYl1ef0PwwJoVlznht7Hec9wEopa5pyzyerSPT
30 | nblh1TRKVffLQ1SyTO6yPgdn8rct5n2M0tW+nW7SZuWkzsc6swVEfJyTykXbMYHg
31 | aSap7oMUr0MaffQjihzwk585fY8GvPeqdrer/k7d6MD05NWkqeXaMitI4hNWTyTu
32 | 7yjppKcGRVaNHmhk4867Jz6RZzxWbZBQe7tqaqmdqKLvz8/7j/VFjBTO2NebE0FV
33 | LxKcpv6QklH0UWqzWBn8LDZRYz6D1PglGjgh8ERHOTKJW0BRdIlzljZwND2f5lSx
34 | 0HBSJYqTU38iBCHkxf8hYdiPI8Jw2CCt4l+hCwQhtIWgCrENSIa1sT9j3TPy+zwq
35 | 2GHf9xTpjV1pVEyuFPf1bllPUeOFXprJiq2J4rnE/fqyabO0uSX35ucdG+YGVyMa
36 | BkwaEPvqvwremmh+xLYye6scQ+A/Wn9fN/8VN0W22t37O2VQNgsTANyIZwKZlxJc
37 | fSkkhF5M7t/rWTtO0MXnCuIDJu3QJneRvOSBlvIabkVGEt9tZQCzK9wiJqsBkLSy
38 | FCdYqoFk7gKsLkv51iMd+oItlEBuSEJSs1N+F5knShYfJdpHYDY+84ul
39 | -----END CERTIFICATE-----`
40 |
41 | const localhostCertPem = `-----BEGIN CERTIFICATE-----
42 | MIIDvTCCAaUCBQLd4CbMMA0GCSqGSIb3DQEBCwUAMDExCzAJBgNVBAYTAlVTMRAw
43 | DgYDVQQKDAdUZXN0T3JnMRAwDgYDVQQLDAdUZXN0IENBMB4XDTE3MDEwNTE3NTQw
44 | NVoXDTM2MTIzMTE3NTQwNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkq
45 | hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3w9PziLdiuYkq4sxVEQkq0HIq6Ad/51o
46 | k98AG87DqbG4hcPcbP/ItEHv4NdYZOtagY5twzb+sjcDC4plubQ9ewMKsGeBlDG4
47 | xxkaMYJeqx8usmiT+fisiI6B+tM38ahxtYqcyCp3ou+WrSEuRb+70UXnztR8RT4R
48 | ISkpq0wuj1Qcmxp9GkyA6aH4gwlj7EocqhIUCF2LxS8U0u2xvbCxHVSQFf4VxrrW
49 | ZwoHaI8GWAZb/IAe9gxoau5rtJ90gpesO5AVx1VPX+WhpSCmVGPnJdknMXC6mjDJ
50 | rMUW0KX8/dX7OdXAtSdDLzm6Yd4cI2DTnGVfckGIzsIYlGhBwxIUGwIDAQABMA0G
51 | CSqGSIb3DQEBCwUAA4ICAQAlendYr/Xrh2h9tWRNB7kCS8OSaJqsaLP7nDWlwbj/
52 | 93QRB51qg2BHUy+tCzXAChq9HnaVcOLrDa0V3Amx5J6LauIBTnRBlHQGaw9Tu4Bo
53 | UqMusHKM9/brNTDRUq8eafJhdfsXWs+cwKj9+Zh1UX0gc8yzgJSLCfkJgeuf62vP
54 | tLAiJAxanxwT2hqtHnuVLu/UUmfx4n0IOALE8tARcLwZkKfmbsXiIY0ZIb/kwCuF
55 | APYy4bmjRXfA9CKnHcfwOxYNqsAPad/MLme9bSBtOuY75VY3UDeno6Uz5PZL4163
56 | 8q+MedT6yinEtGaEllnpWMHa4NC0w+Klpk28fONEIxfqjCvlugRkIlCS3T9qfS9R
57 | vhqwn1V+13JRYxLwMtVpXPdfQbBy7PG9VaQAyRsMrIGsG8esHx+OUMKP3hvh07gs
58 | Lhmjn8SWaFpazldaNRcbOKazxHcwY+yL21VEL5CdA8GcjXEls3YaCuw54QBPJaoB
59 | Yg4ybiaio7h8od1Nydf3mbQ9gmMruLpGHw7RKAGxBD6Ukt0uPAMKOgaL9H2YOSzB
60 | SsYyE/ONrTbxpHZPQG1SszKuKUzGsPEwlMTwt8NHVTixKy/ttMA7NhN8KAYJrJQw
61 | Z65R0mZFpYSL31jrfV4Q4mhFj6/Cr8rgmH++82FWfg88gf4lPk6/iDZtHvMMBUXy
62 | Pg==
63 | -----END CERTIFICATE-----`
64 |
65 | const localhostKeyPem = `-----BEGIN PRIVATE KEY-----
66 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDfD0/OIt2K5iSr
67 | izFURCSrQciroB3/nWiT3wAbzsOpsbiFw9xs/8i0Qe/g11hk61qBjm3DNv6yNwML
68 | imW5tD17AwqwZ4GUMbjHGRoxgl6rHy6yaJP5+KyIjoH60zfxqHG1ipzIKnei75at
69 | IS5Fv7vRRefO1HxFPhEhKSmrTC6PVBybGn0aTIDpofiDCWPsShyqEhQIXYvFLxTS
70 | 7bG9sLEdVJAV/hXGutZnCgdojwZYBlv8gB72DGhq7mu0n3SCl6w7kBXHVU9f5aGl
71 | IKZUY+cl2ScxcLqaMMmsxRbQpfz91fs51cC1J0MvObph3hwjYNOcZV9yQYjOwhiU
72 | aEHDEhQbAgMBAAECggEBALK97lFclvLK4O+lpm3D/S5OlKMSt3cxh6+WrtuZoCjH
73 | BPoLrQKbJRVtEO+3IFoeTnQq0cHwu7/LXWFOEZ3x1KJSGaqqBqfeABdrAhZSRdIS
74 | NrU4H/vbTUZQC9AWmWnIdPXokSHFBgFGxBMP16iEr9hOkCapFrvVtJxCA+YEMfsf
75 | CKK9azdS/6aA4LxFKFuf7EwZz3uD5BqQXM/1vrAjmmATzE5yoJUsUPwJNwTlwTLs
76 | 53tOoZAIhYiWMXL1USXcKm3z8IJq8SgfgOUsK9X6IEEIga/IMwimPl966RlJyIsR
77 | U4RzqG+cP5D2bC9n1M3aBUmUGcvWV7E3nVg+bbuNYIECgYEA76lfyMCbnnBzqagx
78 | UpNR6PXn53RQicQktt+wFFexknf01ZmX+Tn3slSVtsms2xJRtUeNmljPKa7CIMWi
79 | CaBLA2fsUjkPB0EQk6v8MzJeEJOpfFPWC+miKZhnV17rNkuuCwUdPFIz7g66/HU5
80 | /W4gzrUkttw597cpOkOoiUrd16sCgYEA7kQzBa0ille35TlicqSQqeLQrTSga7b2
81 | U0NjSwu0szCGw2LNV87H6Fhxw+MqIQM5VDTPb3bp9T0Uv2n0moENbGO5dD4ZGuNC
82 | mA+AmKNeUBx81Jx4DumGxaU3eATkg6KlNKNccHtXF64k8blM9Y6q6ncCtr4UVz3H
83 | ekSGNXx/hVECgYBf+o7XkPtBmntXqHoIPeOBzmlPMi/G3HxvmGml2/DLXar5mAda
84 | 0jI2gtVqXJ4TJeT/GmbFN2fPo6MvCLb57+3asVXdH+i62P3QhgH8ZuFw9hHcLp78
85 | Kla9HcHVJbhBCFHtK+EndSxC3DdaP4A31FDjN3w6lzvHztx97vah9Q+e/QKBgQCk
86 | 8Y+EuXO9MmJ7DDvL84K2KO+fSFRZ3SIvR/JgDG1+svRIJIjU5bBcd4XiPst2aR3x
87 | 3lFP77lM7YkEbdxIbViWlX7YKvkENRlv3SOAB3CN8vqz0NIIOL/06Ug6DOEJA7ps
88 | cz7WG3ySRxsKP+Y4BBjsEZFOYs4ACyOhz/g85L/+0QKBgQCjjTLjcSNg+fBZRXHC
89 | YwzyBA/WXBPve5qo17Bt91knZ4m+xOVmRcswNG2U0eFrm+nNlk84Kj3TMRAv8Stx
90 | GuCdIOQpn0IWClccTMjwc0AhJStSckNdSUQcsRl6LRnRHa3oCIs3hxnkiEHYch6e
91 | dcxWzhBDbzeIV9SvcTwLx/ghQg==
92 | -----END PRIVATE KEY-----`
93 |
94 | // These two are actual working values for the validate api
95 | const exampleResponseValueTextFail = `
96 |
97 |
98 |
99 | 1234567
100 | 6009
101 | Authentication failed.
102 | 49B5
103 | Failed with an invalid OTP
104 |
105 |
106 | `
107 | const exampleRequestValueText = `
114 |
115 | AVT333666999 534201
116 |
117 |
118 | `
119 |
120 | func getTLSconfig() (*tls.Config, error) {
121 | cert, err := tls.X509KeyPair([]byte(localhostCertPem), []byte(localhostKeyPem))
122 | if err != nil {
123 | return &tls.Config{}, err
124 | }
125 |
126 | return &tls.Config{
127 | MinVersion: tls.VersionTLS11,
128 | MaxVersion: tls.VersionTLS12,
129 | Certificates: []tls.Certificate{cert},
130 | ServerName: "localhost",
131 | }, nil
132 | }
133 |
134 | const localHttpsTarget = "https://localhost:23443/"
135 |
136 | //var testAllowedCertBackends = []string{proto.AuthTypePassword, proto.AuthTypeU2F}
137 |
138 | func handler(w http.ResponseWriter, r *http.Request) {
139 | //authCookie := http.Cookie{Name: "somename", Value: "somevalue"}
140 | //http.SetCookie(w, &authCookie)
141 | switch r.URL.Path {
142 | case "/validate/fail":
143 | fmt.Printf("inside validate/fail")
144 | fmt.Fprintf(w, "%s", exampleResponseValueTextFail)
145 | default:
146 | fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
147 | }
148 | }
149 |
150 | func init() {
151 | tlsConfig, _ := getTLSconfig()
152 | //_, _ = tls.Listen("tcp", ":11443", config)
153 | srv := &http.Server{
154 | Addr: "127.0.0.1:23443",
155 | TLSConfig: tlsConfig,
156 | }
157 | http.HandleFunc("/", handler)
158 | go srv.ListenAndServeTLS("", "")
159 | time.Sleep(20 * time.Millisecond)
160 | }
161 |
162 | func TestVerifySingleTokenFail(t *testing.T) {
163 | client, err := NewClient([]byte(localhostCertPem), []byte(localhostKeyPem))
164 | if err != nil {
165 | t.Fatal(err)
166 | }
167 | client.VipUserServiceAuthenticationURL = localHttpsTarget + "validate/fail"
168 | certPool := x509.NewCertPool()
169 | ok := certPool.AppendCertsFromPEM([]byte(rootCAPem))
170 | if !ok {
171 | t.Fatal("cannot add certs to certpool")
172 | }
173 | client.RootCAs = certPool
174 |
175 | ok, err = client.VerifySingleToken("tokenID", 123456)
176 | if err != nil {
177 | t.Fatal(err)
178 | }
179 | if ok {
180 | t.Log("should have failed")
181 | t.Fatal(err)
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/lib/client/twofa/twofa_test.go:
--------------------------------------------------------------------------------
1 | package twofa
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "encoding/json"
7 | "fmt"
8 | "net"
9 | "net/http"
10 | "testing"
11 |
12 | "github.com/Symantec/Dominator/lib/log/testlogger"
13 | "github.com/Symantec/keymaster/lib/client/util"
14 | "github.com/Symantec/keymaster/lib/webapi/v0/proto"
15 | )
16 |
17 | const rootCAPem = `-----BEGIN CERTIFICATE-----
18 | MIIE1jCCAr4CAQowDQYJKoZIhvcNAQELBQAwMTELMAkGA1UEBhMCVVMxEDAOBgNV
19 | BAoMB1Rlc3RPcmcxEDAOBgNVBAsMB1Rlc3QgQ0EwHhcNMTcwMTA1MTc0NzQ1WhcN
20 | MzYxMjMxMTc0NzQ1WjAxMQswCQYDVQQGEwJVUzEQMA4GA1UECgwHVGVzdE9yZzEQ
21 | MA4GA1UECwwHVGVzdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
22 | AK178Hyl2iJ8l4k/iHWgACtkLLa4n6sJo6DB8s1t98ILgU5ykT30jslWPH/QJvfL
23 | /OAvgDcq2+ScwZThGFMxotBKnoufy88wAV/8SAwdy6rbAatW6v6K8+dgPEcka2jf
24 | aqOby27+vrglQePQjjUMZoqr4qAizCUwCGZQUPhfSorBUcyWupKVZe8kDmD395yT
25 | yRf3z5rJAMFzJmNOv/6ZOA2Scv14xZWTezBlr3E2zBCr0iYpYsqh5dG2ube9DYyX
26 | 0fXfrfaa8jstlu9jrYltxRmlCBAdoFB1N7eN6V/CsKc9nKc4dFkNDJng1Z4dpPYW
27 | v+HzYI4UBsnkYFtpt5VV3M+Ys/FE1ARE4ah4R69FK+eBKCkpRUszbm/fjnt/QvDX
28 | pCBcpa8vddgAgFKz9kvpO3lfA+jBb2euzX1lOL3ETooE0sEtFPK81P2M62BxsoIa
29 | ztAWOQlLQotDtY156YaUoREXCjLkpiitEhpn9+nlMAPlA9X2iVQWIgpkAepbu+rU
30 | ouODruaOoxc67GyTXUT7NIj3IE3PxPn5LBta+SOJ1DsUcC7aBG/x10/ifYLq/H4J
31 | JxnHhac+S5aKxeCBTzT84JKltbYiqhhoGVaXYp6AwbOkqUMYyhEKelcvtdKc7fKF
32 | +0DRAymVdGzV+nhwxGqwgarlXzZwsWkSj/A0+J7l2Ty9AgMBAAEwDQYJKoZIhvcN
33 | AQELBQADggIBAKsYxpXFY4dyarnvMjAmX6EXcwqDfOJXgQdnX7QMzYlygMb+m2h1
34 | Nuk4HTMlmQtkLba6JQd5NQw42RBYl1ef0PwwJoVlznht7Hec9wEopa5pyzyerSPT
35 | nblh1TRKVffLQ1SyTO6yPgdn8rct5n2M0tW+nW7SZuWkzsc6swVEfJyTykXbMYHg
36 | aSap7oMUr0MaffQjihzwk585fY8GvPeqdrer/k7d6MD05NWkqeXaMitI4hNWTyTu
37 | 7yjppKcGRVaNHmhk4867Jz6RZzxWbZBQe7tqaqmdqKLvz8/7j/VFjBTO2NebE0FV
38 | LxKcpv6QklH0UWqzWBn8LDZRYz6D1PglGjgh8ERHOTKJW0BRdIlzljZwND2f5lSx
39 | 0HBSJYqTU38iBCHkxf8hYdiPI8Jw2CCt4l+hCwQhtIWgCrENSIa1sT9j3TPy+zwq
40 | 2GHf9xTpjV1pVEyuFPf1bllPUeOFXprJiq2J4rnE/fqyabO0uSX35ucdG+YGVyMa
41 | BkwaEPvqvwremmh+xLYye6scQ+A/Wn9fN/8VN0W22t37O2VQNgsTANyIZwKZlxJc
42 | fSkkhF5M7t/rWTtO0MXnCuIDJu3QJneRvOSBlvIabkVGEt9tZQCzK9wiJqsBkLSy
43 | FCdYqoFk7gKsLkv51iMd+oItlEBuSEJSs1N+F5knShYfJdpHYDY+84ul
44 | -----END CERTIFICATE-----`
45 |
46 | const localhostCertPem = `-----BEGIN CERTIFICATE-----
47 | MIIDvTCCAaUCBQLd4CbMMA0GCSqGSIb3DQEBCwUAMDExCzAJBgNVBAYTAlVTMRAw
48 | DgYDVQQKDAdUZXN0T3JnMRAwDgYDVQQLDAdUZXN0IENBMB4XDTE3MDEwNTE3NTQw
49 | NVoXDTM2MTIzMTE3NTQwNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkq
50 | hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3w9PziLdiuYkq4sxVEQkq0HIq6Ad/51o
51 | k98AG87DqbG4hcPcbP/ItEHv4NdYZOtagY5twzb+sjcDC4plubQ9ewMKsGeBlDG4
52 | xxkaMYJeqx8usmiT+fisiI6B+tM38ahxtYqcyCp3ou+WrSEuRb+70UXnztR8RT4R
53 | ISkpq0wuj1Qcmxp9GkyA6aH4gwlj7EocqhIUCF2LxS8U0u2xvbCxHVSQFf4VxrrW
54 | ZwoHaI8GWAZb/IAe9gxoau5rtJ90gpesO5AVx1VPX+WhpSCmVGPnJdknMXC6mjDJ
55 | rMUW0KX8/dX7OdXAtSdDLzm6Yd4cI2DTnGVfckGIzsIYlGhBwxIUGwIDAQABMA0G
56 | CSqGSIb3DQEBCwUAA4ICAQAlendYr/Xrh2h9tWRNB7kCS8OSaJqsaLP7nDWlwbj/
57 | 93QRB51qg2BHUy+tCzXAChq9HnaVcOLrDa0V3Amx5J6LauIBTnRBlHQGaw9Tu4Bo
58 | UqMusHKM9/brNTDRUq8eafJhdfsXWs+cwKj9+Zh1UX0gc8yzgJSLCfkJgeuf62vP
59 | tLAiJAxanxwT2hqtHnuVLu/UUmfx4n0IOALE8tARcLwZkKfmbsXiIY0ZIb/kwCuF
60 | APYy4bmjRXfA9CKnHcfwOxYNqsAPad/MLme9bSBtOuY75VY3UDeno6Uz5PZL4163
61 | 8q+MedT6yinEtGaEllnpWMHa4NC0w+Klpk28fONEIxfqjCvlugRkIlCS3T9qfS9R
62 | vhqwn1V+13JRYxLwMtVpXPdfQbBy7PG9VaQAyRsMrIGsG8esHx+OUMKP3hvh07gs
63 | Lhmjn8SWaFpazldaNRcbOKazxHcwY+yL21VEL5CdA8GcjXEls3YaCuw54QBPJaoB
64 | Yg4ybiaio7h8od1Nydf3mbQ9gmMruLpGHw7RKAGxBD6Ukt0uPAMKOgaL9H2YOSzB
65 | SsYyE/ONrTbxpHZPQG1SszKuKUzGsPEwlMTwt8NHVTixKy/ttMA7NhN8KAYJrJQw
66 | Z65R0mZFpYSL31jrfV4Q4mhFj6/Cr8rgmH++82FWfg88gf4lPk6/iDZtHvMMBUXy
67 | Pg==
68 | -----END CERTIFICATE-----`
69 |
70 | const localhostKeyPem = `-----BEGIN PRIVATE KEY-----
71 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDfD0/OIt2K5iSr
72 | izFURCSrQciroB3/nWiT3wAbzsOpsbiFw9xs/8i0Qe/g11hk61qBjm3DNv6yNwML
73 | imW5tD17AwqwZ4GUMbjHGRoxgl6rHy6yaJP5+KyIjoH60zfxqHG1ipzIKnei75at
74 | IS5Fv7vRRefO1HxFPhEhKSmrTC6PVBybGn0aTIDpofiDCWPsShyqEhQIXYvFLxTS
75 | 7bG9sLEdVJAV/hXGutZnCgdojwZYBlv8gB72DGhq7mu0n3SCl6w7kBXHVU9f5aGl
76 | IKZUY+cl2ScxcLqaMMmsxRbQpfz91fs51cC1J0MvObph3hwjYNOcZV9yQYjOwhiU
77 | aEHDEhQbAgMBAAECggEBALK97lFclvLK4O+lpm3D/S5OlKMSt3cxh6+WrtuZoCjH
78 | BPoLrQKbJRVtEO+3IFoeTnQq0cHwu7/LXWFOEZ3x1KJSGaqqBqfeABdrAhZSRdIS
79 | NrU4H/vbTUZQC9AWmWnIdPXokSHFBgFGxBMP16iEr9hOkCapFrvVtJxCA+YEMfsf
80 | CKK9azdS/6aA4LxFKFuf7EwZz3uD5BqQXM/1vrAjmmATzE5yoJUsUPwJNwTlwTLs
81 | 53tOoZAIhYiWMXL1USXcKm3z8IJq8SgfgOUsK9X6IEEIga/IMwimPl966RlJyIsR
82 | U4RzqG+cP5D2bC9n1M3aBUmUGcvWV7E3nVg+bbuNYIECgYEA76lfyMCbnnBzqagx
83 | UpNR6PXn53RQicQktt+wFFexknf01ZmX+Tn3slSVtsms2xJRtUeNmljPKa7CIMWi
84 | CaBLA2fsUjkPB0EQk6v8MzJeEJOpfFPWC+miKZhnV17rNkuuCwUdPFIz7g66/HU5
85 | /W4gzrUkttw597cpOkOoiUrd16sCgYEA7kQzBa0ille35TlicqSQqeLQrTSga7b2
86 | U0NjSwu0szCGw2LNV87H6Fhxw+MqIQM5VDTPb3bp9T0Uv2n0moENbGO5dD4ZGuNC
87 | mA+AmKNeUBx81Jx4DumGxaU3eATkg6KlNKNccHtXF64k8blM9Y6q6ncCtr4UVz3H
88 | ekSGNXx/hVECgYBf+o7XkPtBmntXqHoIPeOBzmlPMi/G3HxvmGml2/DLXar5mAda
89 | 0jI2gtVqXJ4TJeT/GmbFN2fPo6MvCLb57+3asVXdH+i62P3QhgH8ZuFw9hHcLp78
90 | Kla9HcHVJbhBCFHtK+EndSxC3DdaP4A31FDjN3w6lzvHztx97vah9Q+e/QKBgQCk
91 | 8Y+EuXO9MmJ7DDvL84K2KO+fSFRZ3SIvR/JgDG1+svRIJIjU5bBcd4XiPst2aR3x
92 | 3lFP77lM7YkEbdxIbViWlX7YKvkENRlv3SOAB3CN8vqz0NIIOL/06Ug6DOEJA7ps
93 | cz7WG3ySRxsKP+Y4BBjsEZFOYs4ACyOhz/g85L/+0QKBgQCjjTLjcSNg+fBZRXHC
94 | YwzyBA/WXBPve5qo17Bt91knZ4m+xOVmRcswNG2U0eFrm+nNlk84Kj3TMRAv8Stx
95 | GuCdIOQpn0IWClccTMjwc0AhJStSckNdSUQcsRl6LRnRHa3oCIs3hxnkiEHYch6e
96 | dcxWzhBDbzeIV9SvcTwLx/ghQg==
97 | -----END PRIVATE KEY-----`
98 |
99 | const testUserPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI09fpMWTeYw7/EO/+FywS/sghNXdTeTWxX7K2N17owsQJX8s76LGVIdVeYrWg4QSmYlpf6EVSCpx/fbCazrsG7FJVTRhExzFbRT9asmvzS+viXSbSvnavhOz/paihyaMsVPKVv24vF6MOs8DgfwehcKCPjKoIPnlYXZaZcy05KOcZmsvYu2kNOP6sSjDFF+ru+T+DLp3DUGw+MPr45IuR7iDnhXhklqyUn0d7ou0rOHXz9GdHIzpr+DAoQGmTDkpbQEo067Rjfu406gYL8pVFD1F7asCjU39llQCcU/HGyPym5fa29Nubw0dzZZXGZUVFalxo02YMM7P9I6ZjeCsv cviecco@example.com`
100 |
101 | func getTLSconfig() (*tls.Config, error) {
102 | cert, err := tls.X509KeyPair([]byte(localhostCertPem), []byte(localhostKeyPem))
103 | if err != nil {
104 | return &tls.Config{}, err
105 | }
106 |
107 | return &tls.Config{
108 | MinVersion: tls.VersionTLS11,
109 | MaxVersion: tls.VersionTLS12,
110 | Certificates: []tls.Certificate{cert},
111 | ServerName: "localhost",
112 | }, nil
113 | }
114 |
115 | const localHttpsTarget = "https://localhost:22443/"
116 |
117 | var testAllowedCertBackends = []string{proto.AuthTypePassword, proto.AuthTypeU2F}
118 |
119 | func handler(w http.ResponseWriter, r *http.Request) {
120 | authCookie := http.Cookie{Name: "somename", Value: "somevalue"}
121 | http.SetCookie(w, &authCookie)
122 | switch r.URL.Path {
123 | case proto.LoginPath:
124 | loginResponse := proto.LoginResponse{Message: "success",
125 | CertAuthBackend: testAllowedCertBackends}
126 | w.WriteHeader(200)
127 | json.NewEncoder(w).Encode(loginResponse)
128 |
129 | default:
130 | fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
131 | }
132 | }
133 |
134 | func init() {
135 | tlsConfig, _ := getTLSconfig()
136 | //_, _ = tls.Listen("tcp", ":11443", config)
137 | srv := &http.Server{
138 | Addr: "127.0.0.1:22443",
139 | TLSConfig: tlsConfig,
140 | }
141 | http.HandleFunc("/", handler)
142 | go srv.ListenAndServeTLS("", "")
143 | //http.Serve(ln, nil)
144 | }
145 |
146 | func TestGetCertFromTargetUrlsSuccessOneURL(t *testing.T) {
147 | certPool := x509.NewCertPool()
148 | ok := certPool.AppendCertsFromPEM([]byte(rootCAPem))
149 | if !ok {
150 | t.Fatal("cannot add certs to certpool")
151 | }
152 | tlsConfig := &tls.Config{RootCAs: certPool, MinVersion: tls.VersionTLS12}
153 | client, err := util.GetHttpClient(tlsConfig, &net.Dialer{})
154 | if err != nil {
155 | t.Fatal(err)
156 | }
157 | privateKey, err := util.GenerateKey()
158 | if err != nil {
159 | t.Fatal(err)
160 | }
161 | skipu2f := true
162 | _, _, _, err = GetCertFromTargetUrls(
163 | privateKey,
164 | "username",
165 | []byte("password"),
166 | []string{localHttpsTarget},
167 | skipu2f,
168 | false,
169 | client,
170 | "someUserAgent",
171 | testlogger.New(t)) //(cert []byte, err error)
172 | if err != nil {
173 | t.Fatal(err)
174 | }
175 | }
176 |
177 | func TestGetCertFromTargetUrlsFailUntrustedCA(t *testing.T) {
178 | privateKey, err := util.GenerateKey()
179 | if err != nil {
180 | t.Fatal(err)
181 | }
182 | tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
183 | client, err := util.GetHttpClient(tlsConfig, &net.Dialer{})
184 | if err != nil {
185 | t.Fatal(err)
186 | }
187 | skipu2f := true
188 | _, _, _, err = GetCertFromTargetUrls(
189 | privateKey,
190 | "username",
191 | []byte("password"),
192 | []string{localHttpsTarget},
193 | skipu2f,
194 | false,
195 | client,
196 | "someUserAgent",
197 | testlogger.New(t))
198 | if err == nil {
199 | t.Fatal("Should have failed to connect untrusted CA")
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/eventmon/httpd/showActivity.go:
--------------------------------------------------------------------------------
1 | package httpd
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | "net/url"
9 | "sort"
10 | "time"
11 |
12 | "github.com/Symantec/Dominator/lib/format"
13 | "github.com/Symantec/Dominator/lib/html"
14 | "github.com/Symantec/keymaster/eventmon/eventrecorder"
15 | )
16 |
17 | const (
18 | durationDay = time.Hour * 24
19 | durationWeek = durationDay * 7
20 | durationMonth = durationDay * 31
21 | )
22 |
23 | type counterType struct {
24 | authPassword uint64
25 | authSymantecVIPotp uint64
26 | authSymantecVIPpush uint64
27 | authU2F uint64
28 | spLogin uint64
29 | ssh uint64
30 | webLogin uint64
31 | x509 uint64
32 | }
33 |
34 | func (s state) showActivityHandler(w http.ResponseWriter, req *http.Request) {
35 | writer := bufio.NewWriter(w)
36 | defer writer.Flush()
37 | fmt.Fprintln(writer, "keymaster-eventmond activity report")
38 | fmt.Fprintln(writer, ``)
43 | fmt.Fprintln(writer, "")
44 | fmt.Fprintln(writer, "")
45 | fmt.Fprintln(writer, "keymaster-eventmond activity report
")
46 | fmt.Fprintln(writer, "")
47 | eventsChannel := make(chan eventrecorder.Events, 1)
48 | s.eventRecorder.RequestEventsChannel <- eventsChannel
49 | eventsMap := <-eventsChannel
50 | startTime := time.Now()
51 | usernames := make([]string, 0, len(eventsMap.Events))
52 | for username := range eventsMap.Events {
53 | if len(eventsMap.Events[username]) > 0 {
54 | usernames = append(usernames, username)
55 | }
56 | }
57 | sort.Strings(usernames)
58 | s.writeActivity(writer, usernames, eventsMap.Events)
59 | fmt.Fprintln(writer, "
")
60 | s.writeSPLoginActivity(writer, eventsMap.Events)
61 | renderTime := time.Since(startTime)
62 | fmt.Fprintf(writer,
63 | "
Fetch time: %s render time: %s\n",
64 | format.Duration(eventsMap.ComputeTime), format.Duration(renderTime))
65 | fmt.Fprintln(writer, "
")
66 | html.WriteFooter(writer)
67 | fmt.Fprintln(writer, "")
68 | }
69 |
70 | func (s state) writeActivity(writer io.Writer, usernames []string,
71 | eventsMap eventrecorder.EventsMap) {
72 | fmt.Fprintln(writer,
73 | "SPlogin/SSH/Web/X509 Password/VIPotp/VIPpush/U2F")
74 | fmt.Fprintln(writer, ``)
75 | fmt.Fprintln(writer, " ")
76 | fmt.Fprintln(writer, " | Username | ")
77 | fmt.Fprintln(writer, " Last Day | ")
78 | fmt.Fprintln(writer, " Last Week | ")
79 | fmt.Fprintln(writer, " Last Month | ")
80 | fmt.Fprintln(writer, " Min Lifetime | ")
81 | fmt.Fprintln(writer, " Med Lifetime | ")
82 | fmt.Fprintln(writer, " Max Lifetime | ")
83 | fmt.Fprintln(writer, "
")
84 | totals := &statsType{minLifetime: durationMonth * 120, maxLifetime: -1}
85 | for _, username := range usernames {
86 | writeUser(writer, username, eventsMap[username], time.Now(), totals)
87 | }
88 | totals.writeHtml(writer, "ALL USERS")
89 | fmt.Fprintln(writer, "
")
90 | }
91 |
92 | func (s state) writeSPLoginActivity(writer io.Writer,
93 | eventsMap eventrecorder.EventsMap) {
94 | urlToCountTable := make(map[string]uint64)
95 | for _, events := range eventsMap {
96 | for _, event := range events {
97 | if rawURL := event.ServiceProviderUrl; rawURL != "" {
98 | var URL string
99 | if u, err := url.Parse(rawURL); err != nil {
100 | URL = rawURL
101 | } else {
102 | if len(u.Port()) > 0 {
103 | URL = u.Scheme + "://" + u.Hostname() + ":" + u.Port() +
104 | "/"
105 | } else {
106 | URL = u.Scheme + "://" + u.Hostname() + "/"
107 | }
108 | }
109 | urlToCountTable[URL] = urlToCountTable[URL] + 1
110 | }
111 | }
112 | }
113 | var pairs stringCountPairs
114 | for url, count := range urlToCountTable {
115 | pairs = append(pairs, stringCountPair{url, count})
116 | }
117 | sort.Sort(sort.Reverse(pairs))
118 | fmt.Fprintln(writer, ``)
119 | fmt.Fprintln(writer, " ")
120 | fmt.Fprintln(writer, " | Service Provider URL | ")
121 | fmt.Fprintln(writer, " Login Count | ")
122 | fmt.Fprintln(writer, "
")
123 | for _, pair := range pairs {
124 | fmt.Fprintln(writer, " ")
125 | fmt.Fprintf(writer, " | %s | \n", pair.url)
126 | fmt.Fprintf(writer, " %d | \n", pair.count)
127 | fmt.Fprintln(writer, "
")
128 | }
129 | fmt.Fprintln(writer, "
")
130 | }
131 |
132 | func writeUser(writer io.Writer, username string,
133 | events []eventrecorder.EventType, now time.Time, totals *statsType) {
134 | stats := &statsType{
135 | lifetimes: make([]int, 0, len(events)),
136 | minLifetime: durationMonth * 120,
137 | maxLifetime: -1,
138 | }
139 | for _, event := range events {
140 | if event.LifetimeSeconds > 0 {
141 | lifetime := time.Duration(event.LifetimeSeconds) * time.Second
142 | stats.lifetimes = append(stats.lifetimes,
143 | int(event.LifetimeSeconds))
144 | totals.lifetimes = append(totals.lifetimes,
145 | int(event.LifetimeSeconds))
146 | if lifetime < stats.minLifetime {
147 | stats.minLifetime = lifetime
148 | }
149 | if lifetime > stats.maxLifetime {
150 | stats.maxLifetime = lifetime
151 | }
152 | if lifetime < totals.minLifetime {
153 | totals.minLifetime = lifetime
154 | }
155 | if lifetime > totals.maxLifetime {
156 | totals.maxLifetime = lifetime
157 | }
158 | }
159 | age := now.Sub(time.Unix(int64(event.CreateTime), 0))
160 | if age <= durationDay {
161 | stats.countOverLastDay.increment(event)
162 | totals.countOverLastDay.increment(event)
163 | }
164 | if age <= durationWeek {
165 | stats.countOverLastWeek.increment(event)
166 | totals.countOverLastWeek.increment(event)
167 | }
168 | if age <= durationMonth {
169 | stats.countOverLastMonth.increment(event)
170 | totals.countOverLastMonth.increment(event)
171 | }
172 | }
173 | stats.writeHtml(writer, username)
174 | }
175 |
176 | func (counter *counterType) increment(event eventrecorder.EventType) {
177 | switch event.AuthType {
178 | case eventrecorder.AuthTypePassword:
179 | counter.authPassword++
180 | case eventrecorder.AuthTypeSymantecVIP:
181 | switch event.VIPAuthType {
182 | case eventrecorder.VIPAuthTypeOTP:
183 | counter.authSymantecVIPotp++
184 | case eventrecorder.VIPAuthTypePush:
185 | counter.authSymantecVIPpush++
186 | }
187 | case eventrecorder.AuthTypeU2F:
188 | counter.authU2F++
189 | }
190 | if event.ServiceProviderUrl != "" {
191 | counter.spLogin++
192 | }
193 | if event.Ssh {
194 | counter.ssh++
195 | }
196 | if event.WebLogin {
197 | counter.webLogin++
198 | }
199 | if event.X509 {
200 | counter.x509++
201 | }
202 | }
203 |
204 | func (counter *counterType) string() string {
205 | return fmt.Sprintf("%d/%d/%d/%d %d/%d/%d/%d",
206 | counter.spLogin, counter.ssh, counter.webLogin, counter.x509,
207 | counter.authPassword, counter.authSymantecVIPotp,
208 | counter.authSymantecVIPpush, counter.authU2F)
209 | }
210 |
211 | type stringCountPairs []stringCountPair
212 |
213 | type stringCountPair struct {
214 | url string
215 | count uint64
216 | }
217 |
218 | func (pairs stringCountPairs) Len() int {
219 | return len(pairs)
220 | }
221 |
222 | func (pairs stringCountPairs) Less(left, right int) bool {
223 | return pairs[left].count < pairs[right].count
224 | }
225 |
226 | func (pairs stringCountPairs) Swap(left, right int) {
227 | pairs[left], pairs[right] = pairs[right], pairs[left]
228 | }
229 |
230 | type statsType struct {
231 | countOverLastDay counterType
232 | countOverLastWeek counterType
233 | countOverLastMonth counterType
234 | lifetimes []int
235 | minLifetime time.Duration
236 | maxLifetime time.Duration
237 | }
238 |
239 | func (stats *statsType) writeHtml(writer io.Writer, username string) {
240 | fmt.Fprintf(writer, " \n")
241 | fmt.Fprintf(writer, " | %s | \n", username)
242 | fmt.Fprintf(writer, " %s | \n", stats.countOverLastDay.string())
243 | fmt.Fprintf(writer, " %s | \n", stats.countOverLastWeek.string())
244 | fmt.Fprintf(writer, " %s | \n", stats.countOverLastMonth.string())
245 | if len(stats.lifetimes) > 0 {
246 | sort.Ints(stats.lifetimes)
247 | medLifetime := time.Duration(
248 | stats.lifetimes[len(stats.lifetimes)/2]) * time.Second
249 | fmt.Fprintf(writer, " %s | \n",
250 | format.Duration(stats.minLifetime))
251 | fmt.Fprintf(writer, " %s | \n", format.Duration(medLifetime))
252 | fmt.Fprintf(writer, " %s | \n",
253 | format.Duration(stats.maxLifetime))
254 | } else {
255 | fmt.Fprintln(writer, " | ")
256 | fmt.Fprintln(writer, " | ")
257 | fmt.Fprintln(writer, " | ")
258 | }
259 | fmt.Fprintf(writer, "
\n")
260 | }
261 |
--------------------------------------------------------------------------------
/lib/client/twofa/twofa.go:
--------------------------------------------------------------------------------
1 | package twofa
2 |
3 | import (
4 | "bytes"
5 | "crypto"
6 | "crypto/x509"
7 | "encoding/json"
8 | "encoding/pem"
9 | "errors"
10 | "fmt"
11 | "io"
12 | "io/ioutil"
13 | "mime/multipart"
14 | "net/http"
15 | "net/url"
16 | "os"
17 | "runtime"
18 | "strconv"
19 | "strings"
20 |
21 | "github.com/Symantec/Dominator/lib/log"
22 | "github.com/Symantec/keymaster/lib/client/twofa/u2f"
23 | "github.com/Symantec/keymaster/lib/client/twofa/vip"
24 | "github.com/Symantec/keymaster/lib/webapi/v0/proto"
25 | "github.com/flynn/u2f/u2fhid" // client side (interface with hardware)
26 | "golang.org/x/crypto/ssh"
27 | )
28 |
29 | const clientDataAuthenticationTypeValue = "navigator.id.getAssertion"
30 |
31 | // This is now copy-paste from the server test side... probably make public and reuse.
32 | func createKeyBodyRequest(method, urlStr, filedata string) (*http.Request, error) {
33 | //create attachment....
34 | bodyBuf := &bytes.Buffer{}
35 | bodyWriter := multipart.NewWriter(bodyBuf)
36 |
37 | //
38 | fileWriter, err := bodyWriter.CreateFormFile("pubkeyfile", "somefilename.pub")
39 | if err != nil {
40 | fmt.Println("error writing to buffer")
41 | return nil, err
42 | }
43 | // When using a file this used to be: fh, err := os.Open(pubKeyFilename)
44 | fh := strings.NewReader(filedata)
45 |
46 | _, err = io.Copy(fileWriter, fh)
47 | if err != nil {
48 | return nil, err
49 | }
50 |
51 | err = bodyWriter.WriteField("duration", (*Duration).String())
52 | if err != nil {
53 | return nil, err
54 | }
55 |
56 | contentType := bodyWriter.FormDataContentType()
57 | bodyWriter.Close()
58 |
59 | req, err := http.NewRequest(method, urlStr, bodyBuf)
60 | if err != nil {
61 | return nil, err
62 | }
63 | req.Header.Set("Content-Type", contentType)
64 |
65 | return req, nil
66 | }
67 |
68 | func doCertRequest(client *http.Client, authCookies []*http.Cookie, url, filedata string,
69 | userAgentString string, logger log.Logger) ([]byte, error) {
70 |
71 | req, err := createKeyBodyRequest("POST", url, filedata)
72 | if err != nil {
73 | return nil, err
74 | }
75 | // Add the login cookies
76 | for _, cookie := range authCookies {
77 | req.AddCookie(cookie)
78 | }
79 | req.Header.Set("User-Agent", userAgentString)
80 | resp, err := client.Do(req) // Client.Get(targetUrl)
81 | if err != nil {
82 | logger.Printf("Failure to do cert request %s", err)
83 | return nil, err
84 | }
85 |
86 | defer resp.Body.Close()
87 | if resp.StatusCode != 200 {
88 | return nil, fmt.Errorf("got error from call %s, url='%s'\n", resp.Status, url)
89 | }
90 | return ioutil.ReadAll(resp.Body)
91 |
92 | }
93 |
94 | func getCertsFromServer(
95 | signer crypto.Signer,
96 | userName string,
97 | password []byte,
98 | baseUrl string,
99 | skip2fa bool,
100 | addGroups bool,
101 | client *http.Client,
102 | userAgentString string,
103 | logger log.DebugLogger) (sshCert []byte, x509Cert []byte, kubernetesCert []byte, err error) {
104 |
105 | loginUrl := baseUrl + proto.LoginPath
106 | form := url.Values{}
107 | form.Add("username", userName)
108 | form.Add("password", string(password[:]))
109 | req, err := http.NewRequest("POST", loginUrl,
110 | strings.NewReader(form.Encode()))
111 | if err != nil {
112 | return nil, nil, nil, err
113 | }
114 | req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
115 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
116 | req.Header.Add("Accept", "application/json")
117 | req.Header.Set("User-Agent", userAgentString)
118 |
119 | logger.Debugf(1, "About to start login request\n")
120 | loginResp, err := client.Do(req) //client.Get(targetUrl)
121 | if err != nil {
122 | logger.Printf("got error from req")
123 | logger.Println(err)
124 | // TODO: differentiate between 400 and 500 errors
125 | // is OK to fail.. try next
126 | return nil, nil, nil, err
127 | }
128 | defer loginResp.Body.Close()
129 | if loginResp.StatusCode != 200 {
130 | logger.Printf("got error from login call %s", loginResp.Status)
131 | return nil, nil, nil, err
132 | }
133 | //Enusre we have at least one cookie
134 | if len(loginResp.Cookies()) < 1 {
135 | err = errors.New("No cookies from login")
136 | return nil, nil, nil, err
137 | }
138 |
139 | loginJSONResponse := proto.LoginResponse{}
140 | //body := jsonrr.Result().Body
141 | err = json.NewDecoder(loginResp.Body).Decode(&loginJSONResponse)
142 | if err != nil {
143 | return nil, nil, nil, err
144 | }
145 | io.Copy(ioutil.Discard, loginResp.Body) // We also need to read ALL of the body
146 | loginResp.Body.Close() //so that we can reuse the channel
147 | logger.Debugf(1, "This the login response=%v\n", loginJSONResponse)
148 |
149 | allowVIP := false
150 | allowU2F := false
151 | for _, backend := range loginJSONResponse.CertAuthBackend {
152 | if backend == proto.AuthTypePassword {
153 | skip2fa = true
154 | }
155 | if backend == proto.AuthTypeSymantecVIP {
156 | allowVIP = true
157 | //remote next statemente later
158 | //skipu2f = true
159 | }
160 | if backend == proto.AuthTypeU2F {
161 | allowU2F = true
162 | }
163 | }
164 |
165 | // Dont try U2F if chosen by user
166 | if *noU2F {
167 | allowU2F = false
168 | }
169 | if *noVIPAccess {
170 | allowVIP = false
171 | }
172 |
173 | // on linux disable U2F is the /sys/class/hidraw is missing
174 | if runtime.GOOS == "linux" && allowU2F {
175 | if _, err := os.Stat("/sys/class/hidraw"); os.IsNotExist(err) {
176 | allowU2F = false
177 | }
178 |
179 | }
180 |
181 | // upgrade to u2f
182 | successful2fa := false
183 | if !skip2fa {
184 | if allowU2F {
185 | devices, err := u2fhid.Devices()
186 | if err != nil {
187 | logger.Fatal(err)
188 | return nil, nil, nil, err
189 | }
190 | if len(devices) > 0 {
191 |
192 | err = u2f.DoU2FAuthenticate(
193 | client, baseUrl, userAgentString, logger)
194 | if err != nil {
195 |
196 | return nil, nil, nil, err
197 | }
198 | successful2fa = true
199 | }
200 | }
201 |
202 | if allowVIP && !successful2fa {
203 | err = vip.DoVIPAuthenticate(
204 | client, baseUrl, userAgentString, logger)
205 | if err != nil {
206 |
207 | return nil, nil, nil, err
208 | }
209 | successful2fa = true
210 | }
211 |
212 | if !successful2fa {
213 | err = errors.New("Failed to Pefrom 2FA (as requested from server)")
214 | return nil, nil, nil, err
215 | }
216 |
217 | }
218 |
219 | logger.Debugf(1, "Authentication Phase complete")
220 |
221 | //now get x509 cert
222 | pubKey := signer.Public()
223 | derKey, err := x509.MarshalPKIXPublicKey(pubKey)
224 | if err != nil {
225 | return nil, nil, nil, err
226 | }
227 | pemKey := string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: derKey}))
228 |
229 | var urlPostfix string
230 | if addGroups {
231 | urlPostfix = "&addGroups=true"
232 | logger.Debugln(0, "adding \"addGroups\" to request")
233 | }
234 | // TODO: urlencode the userName
235 | x509Cert, err = doCertRequest(
236 | client,
237 | loginResp.Cookies(),
238 | baseUrl+"/certgen/"+userName+"?type=x509"+urlPostfix,
239 | pemKey,
240 | userAgentString,
241 | logger)
242 | if err != nil {
243 | return nil, nil, nil, err
244 | }
245 |
246 | kubernetesCert, err = doCertRequest(
247 | client,
248 | loginResp.Cookies(),
249 | baseUrl+"/certgen/"+userName+"?type=x509-kubernetes",
250 | pemKey,
251 | userAgentString,
252 | logger)
253 | if err != nil {
254 | //logger.Printf("Warning: could not get the kubernets cert (old server?) err=%s \n", err)
255 | kubernetesCert = nil
256 | //return nil, nil, nil, err
257 | }
258 |
259 | //// Now we do sshCert!
260 | // generate and write public key
261 | sshPub, err := ssh.NewPublicKey(pubKey)
262 | if err != nil {
263 | return nil, nil, nil, err
264 | }
265 | sshAuthFile := string(ssh.MarshalAuthorizedKey(sshPub))
266 | sshCert, err = doCertRequest(
267 | client,
268 | loginResp.Cookies(),
269 | baseUrl+"/certgen/"+userName+"?type=ssh",
270 | sshAuthFile,
271 | userAgentString,
272 | logger)
273 | if err != nil {
274 | return nil, nil, nil, err
275 | }
276 |
277 | return sshCert, x509Cert, kubernetesCert, nil
278 | }
279 |
280 | func getCertFromTargetUrls(
281 | signer crypto.Signer,
282 | userName string,
283 | password []byte,
284 | targetUrls []string,
285 | skipu2f bool,
286 | addGroups bool,
287 | client *http.Client,
288 | userAgentString string,
289 | logger log.DebugLogger) (sshCert []byte, x509Cert []byte, kubernetesCert []byte, err error) {
290 | success := false
291 |
292 | for _, baseUrl := range targetUrls {
293 | logger.Printf("attempting to target '%s' for '%s'\n", baseUrl, userName)
294 | sshCert, x509Cert, kubernetesCert, err = getCertsFromServer(
295 | signer, userName, password, baseUrl, skipu2f, addGroups,
296 | client, userAgentString, logger)
297 | if err != nil {
298 | logger.Println(err)
299 | continue
300 | }
301 | success = true
302 | break
303 |
304 | }
305 | if !success {
306 | err := errors.New("Failed to get creds")
307 | return nil, nil, nil, err
308 | }
309 |
310 | return sshCert, x509Cert, kubernetesCert, nil
311 | }
312 |
--------------------------------------------------------------------------------
/cmd/keymasterd/2fa_vip.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "github.com/Symantec/keymaster/lib/instrumentedwriter"
7 | "net/http"
8 | "strconv"
9 | "time"
10 |
11 | "github.com/Symantec/keymaster/lib/webapi/v0/proto"
12 | "github.com/Symantec/keymaster/proto/eventmon"
13 | )
14 |
15 | func (state *RuntimeState) startVIPPush(cookieVal string, username string) error {
16 | transactionId, err := state.Config.SymantecVIP.Client.StartUserVIPPush(username)
17 | if err != nil {
18 | logger.Println(err)
19 | return err
20 | }
21 | newLocalData := pushPollTransaction{Username: username, TransactionID: transactionId, ExpiresAt: time.Now().Add(maxAgeSecondsVIPCookie * time.Second)}
22 | state.Mutex.Lock()
23 | defer state.Mutex.Unlock()
24 | state.vipPushCookie[cookieVal] = newLocalData
25 |
26 | return nil
27 | }
28 |
29 | ///
30 | const vipAuthPath = "/api/v0/vipAuth"
31 |
32 | func (state *RuntimeState) VIPAuthHandler(w http.ResponseWriter, r *http.Request) {
33 | if state.sendFailureToClientIfLocked(w, r) {
34 | return
35 | }
36 |
37 | //Check for valid method here?
38 | switch r.Method {
39 | case "GET":
40 | logger.Debugf(3, "Got client GET connection")
41 | err := r.ParseForm()
42 | if err != nil {
43 | logger.Println(err)
44 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Error parsing form")
45 | return
46 | }
47 | case "POST":
48 | logger.Debugf(3, "Got client POST connection")
49 | err := r.ParseForm()
50 | if err != nil {
51 | logger.Println(err)
52 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Error parsing form")
53 | return
54 | }
55 | default:
56 | state.writeFailureResponse(w, r, http.StatusMethodNotAllowed, "")
57 | return
58 | }
59 | //authUser, authType, err := state.checkAuth(w, r, AuthTypeAny)
60 | authUser, currentAuthLevel, err := state.checkAuth(w, r, AuthTypeAny)
61 | if err != nil {
62 | logger.Debugf(1, "%v", err)
63 | return
64 | }
65 | w.(*instrumentedwriter.LoggingWriter).SetUsername(authUser)
66 |
67 | var OTPString string
68 | if val, ok := r.Form["OTP"]; ok {
69 | if len(val) > 1 {
70 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Just one OTP Value allowed")
71 | logger.Printf("Login with multiple OTP Values")
72 | return
73 | }
74 | OTPString = val[0]
75 | }
76 | otpValue, err := strconv.Atoi(OTPString)
77 | if err != nil {
78 | logger.Println(err)
79 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Error parsing OTP value")
80 | return
81 | }
82 | if !state.Config.SymantecVIP.Enabled {
83 | logger.Printf("request for VIP auth, but VIP not enabled")
84 | state.writeFailureResponse(w, r, http.StatusPreconditionFailed, "VIP not enabled")
85 | return
86 | }
87 |
88 | start := time.Now()
89 | valid, err := state.Config.SymantecVIP.Client.ValidateUserOTP(authUser, otpValue)
90 | if err != nil {
91 | logger.Println(err)
92 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "Failure when validating VIP token")
93 | return
94 | }
95 |
96 | metricLogExternalServiceDuration("vip", time.Since(start))
97 |
98 | //
99 | metricLogAuthOperation(getClientType(r), proto.AuthTypeSymantecVIP, valid)
100 | if !valid {
101 | logger.Printf("Invalid VIP OTP value login for %s", authUser)
102 | // TODO if client is html then do a redirect back to vipLoginPage
103 | state.writeFailureResponse(w, r, http.StatusUnauthorized, "")
104 | return
105 |
106 | }
107 |
108 | // OTP check was successful
109 | logger.Debugf(1, "Successful vipOTP auth for user: %s", authUser)
110 | eventNotifier.PublishVIPAuthEvent(eventmon.VIPAuthTypeOTP, authUser)
111 | _, err = state.updateAuthCookieAuthlevel(w, r, currentAuthLevel|AuthTypeSymantecVIP)
112 | if err != nil {
113 | logger.Printf("Auth Cookie NOT found ? %s", err)
114 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "Failure when validating VIP token")
115 | return
116 | }
117 |
118 | // Now we send to the appropiate place
119 | returnAcceptType := getPreferredAcceptType(r)
120 |
121 | // TODO: The cert backend should depend also on per user preferences.
122 | loginResponse := proto.LoginResponse{Message: "success"} //CertAuthBackend: certBackends
123 | switch returnAcceptType {
124 | case "text/html":
125 | loginDestination := getLoginDestination(r)
126 | eventNotifier.PublishWebLoginEvent(authUser)
127 | http.Redirect(w, r, loginDestination, 302)
128 | default:
129 | w.WriteHeader(200)
130 | json.NewEncoder(w).Encode(loginResponse)
131 | //fmt.Fprintf(w, "Success!")
132 | }
133 | return
134 | }
135 |
136 | func (state *RuntimeState) getPushPollTransaction(cookieValue string) (pushPollTransaction, bool) {
137 | state.Mutex.Lock()
138 | defer state.Mutex.Unlock()
139 | value, ok := state.vipPushCookie[cookieValue]
140 | return value, ok
141 | }
142 |
143 | ///////////////////////////
144 | const vipPushStartPath = "/api/v0/vipPushStart"
145 |
146 | func (state *RuntimeState) vipPushStartHandler(w http.ResponseWriter, r *http.Request) {
147 | if state.sendFailureToClientIfLocked(w, r) {
148 | return
149 | }
150 | if !state.Config.SymantecVIP.Enabled {
151 | logger.Printf("asked for push status but VIP is not enabled")
152 | state.writeFailureResponse(w, r, http.StatusBadRequest, "")
153 | return
154 | }
155 | authUser, _, err := state.checkAuth(w, r, AuthTypeAny)
156 | if err != nil {
157 | logger.Debugf(1, "%v", err)
158 | return
159 | }
160 | w.(*instrumentedwriter.LoggingWriter).SetUsername(authUser)
161 | logger.Debugf(0, "Vip push start authuser=%s", authUser)
162 | vipPushCookie, err := r.Cookie(vipTransactionCookieName)
163 | if err != nil {
164 | logger.Printf("%v", err)
165 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Missing Cookie")
166 | return
167 | }
168 | pushTransaction, ok := state.getPushPollTransaction(vipPushCookie.Value)
169 | if ok {
170 | err := errors.New("push transaction found will not start another one")
171 | logger.Println(err)
172 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "Push already sent")
173 | return
174 | }
175 | if len(pushTransaction.TransactionID) > 0 {
176 | err := errors.New("VIP push transaction already initiated")
177 | logger.Println(err)
178 | state.writeFailureResponse(w, r, http.StatusPreconditionFailed, "Push already sent")
179 | return
180 | }
181 | err = state.startVIPPush(vipPushCookie.Value, authUser)
182 | if err != nil {
183 | logger.Println(err)
184 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "Cookie not setup ")
185 | return
186 | }
187 |
188 | w.WriteHeader(http.StatusOK)
189 | return
190 | }
191 |
192 | ////////////////////////////
193 | const vipPollCheckPath = "/api/v0/vipPollCheck"
194 |
195 | func (state *RuntimeState) VIPPollCheckHandler(w http.ResponseWriter, r *http.Request) {
196 | if state.sendFailureToClientIfLocked(w, r) {
197 | return
198 | }
199 | if !state.Config.SymantecVIP.Enabled {
200 | logger.Printf("asked for push status but VIP is not enabled")
201 | state.writeFailureResponse(w, r, http.StatusBadRequest, "")
202 | return
203 | }
204 |
205 | //Check for valid method here?
206 | switch r.Method {
207 | case "GET":
208 | logger.Debugf(3, "Got client GET connection")
209 | err := r.ParseForm()
210 | if err != nil {
211 | logger.Println(err)
212 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Error parsing form")
213 | return
214 | }
215 | case "POST":
216 | logger.Debugf(3, "Got client POST connection")
217 | err := r.ParseForm()
218 | if err != nil {
219 | logger.Println(err)
220 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Error parsing form")
221 | return
222 | }
223 | default:
224 | state.writeFailureResponse(w, r, http.StatusMethodNotAllowed, "")
225 | return
226 | }
227 | authUser, currentAuthLevel, err := state.checkAuth(w, r, AuthTypeAny)
228 | if err != nil {
229 | logger.Debugf(1, "%v", err)
230 | return
231 | }
232 | w.(*instrumentedwriter.LoggingWriter).SetUsername(authUser)
233 | logger.Debugf(1, "VIPPollCheckHandler: authuser=%s", authUser)
234 | vipPollCookie, err := r.Cookie(vipTransactionCookieName)
235 | if err != nil {
236 | logger.Printf("VIPPollCheckHandler: error getting poll cookie %v", err)
237 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Missing Cookie")
238 | return
239 | }
240 | pushTransaction, ok := state.getPushPollTransaction(vipPollCookie.Value)
241 | if !ok {
242 | err := errors.New("VIPPollCheckHandler: push transaction not found for user")
243 | logger.Println(err)
244 | state.writeFailureResponse(w, r, http.StatusPreconditionFailed, "Error parsing form")
245 | return
246 | }
247 | //TODO: check username
248 | valid, err := state.Config.SymantecVIP.Client.VipPushHasBeenApproved(pushTransaction.TransactionID)
249 | if err != nil {
250 | logger.Println(err)
251 | state.writeFailureResponse(w, r, http.StatusBadRequest, "Error checking push transaction")
252 | return
253 | }
254 | if !valid {
255 | err := errors.New("Not yet") // usually it is not valid, no need to spam the log
256 | logger.Debugf(1, "%s", err)
257 | state.writeFailureResponse(w, r, http.StatusPreconditionFailed, "VIP Push Poller unsuccessful")
258 | return
259 | }
260 |
261 | // VIP Push check was successful
262 | _, err = state.updateAuthCookieAuthlevel(w, r, currentAuthLevel|AuthTypeSymantecVIP)
263 | if err != nil {
264 | logger.Printf("VIPPollCheckHandler: Failure to update AuthCookie %s", err)
265 | state.writeFailureResponse(w, r, http.StatusInternalServerError, "Failure when validating VIP token")
266 | return
267 | }
268 | eventNotifier.PublishVIPAuthEvent(eventmon.VIPAuthTypePush, authUser)
269 |
270 | // TODO make something more fancy: JSON?
271 | w.WriteHeader(http.StatusOK)
272 | return
273 |
274 | }
275 |
--------------------------------------------------------------------------------