├── 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, " ") 77 | fmt.Fprintln(writer, " ") 78 | fmt.Fprintln(writer, " ") 79 | fmt.Fprintln(writer, " ") 80 | fmt.Fprintln(writer, " ") 81 | fmt.Fprintln(writer, " ") 82 | fmt.Fprintln(writer, " ") 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, "
UsernameLast DayLast WeekLast MonthMin LifetimeMed LifetimeMax Lifetime
") 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, " ") 121 | fmt.Fprintln(writer, " ") 122 | fmt.Fprintln(writer, " ") 123 | for _, pair := range pairs { 124 | fmt.Fprintln(writer, " ") 125 | fmt.Fprintf(writer, " \n", pair.url) 126 | fmt.Fprintf(writer, " \n", pair.count) 127 | fmt.Fprintln(writer, " ") 128 | } 129 | fmt.Fprintln(writer, "
Service Provider URLLogin Count
%s%d
") 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 | --------------------------------------------------------------------------------