got u2f login working on the CLI client.

also grouped the u2f webapi endpoints together,
and fixed the default u2f AppID so it works out of the box
This commit is contained in:
jcj83429 2016-10-22 20:43:44 -07:00
parent b24fc74e48
commit 739308c5ae
9 changed files with 221 additions and 14 deletions

View file

@ -119,6 +119,8 @@ func NewStandardPermissions() PermissionChecker {
sp.permissions[teleport.RoleU2fUser] = map[string]bool{
ActionPreAuthSignIn: true,
ActionGenerateUserCert: true,
ActionGetCertAuthorities: true,
}
return &sp
@ -165,6 +167,7 @@ var StandardRoles = teleport.Roles{
teleport.RoleProvisionToken,
teleport.RoleSignup,
teleport.RoleU2fSign,
teleport.RoleU2fUser,
}
const (

View file

@ -119,6 +119,10 @@ type Config struct {
// that uses local cache to validate hosts
HostKeyCallback HostKeyCallback
// SecondFactorType indicates whether HOTP, OIDC or U2F should be used
// for the second factor
SecondFactorType string
// ConnectorID is used to authenticate user via OpenID Connect
// registered connector
ConnectorID string
@ -810,18 +814,24 @@ func (tc *TeleportClient) Login() error {
}
var response *web.SSHLoginResponse
if tc.ConnectorID == "" {
switch tc.SecondFactorType {
case "hotp":
response, err = tc.directLogin(key.Pub)
if err != nil {
return trace.Wrap(err)
}
} else {
case "oidc":
response, err = tc.oidcLogin(tc.ConnectorID, key.Pub)
if err != nil {
return trace.Wrap(err)
}
// in this case identity is returned by the proxy
tc.Username = response.Username
case "u2f":
response, err = tc.u2fLogin(key.Pub)
if err != nil {
return trace.Wrap(err)
}
}
key.Cert = response.Cert
// save the key:
@ -907,6 +917,30 @@ func (tc *TeleportClient) oidcLogin(connectorID string, pub []byte) (*web.SSHLog
return response, trace.Wrap(err)
}
// directLogin asks for a password and performs the challenge-response authentication
func (tc *TeleportClient) u2fLogin(pub []byte) (*web.SSHLoginResponse, error) {
// U2F login requires the official u2f-host executable
_, err := exec.LookPath("u2f-host")
if err != nil {
return nil, err
}
httpsProxyHostPort := tc.Config.ProxyHostPort(true)
certPool := loopbackPool(httpsProxyHostPort)
password, err := tc.AskPassword()
response, err := web.SSHAgentU2fLogin(httpsProxyHostPort,
tc.Config.Username,
password,
pub,
tc.KeyTTL,
tc.InsecureSkipVerify,
certPool)
return response, trace.Wrap(err)
}
// loopbackPool reads trusted CAs if it finds it in a predefined location
// and will work only if target proxy address is loopback
func loopbackPool(proxyAddr string) *x509.CertPool {
@ -983,6 +1017,18 @@ func (tc *TeleportClient) AskPasswordAndHOTP() (pwd string, token string, err er
return pwd, token, nil
}
// AskPassword prompts the user to enter the password
func (tc *TeleportClient) AskPassword() (pwd string, err error) {
fmt.Printf("Enter password for Teleport user %v:\n", tc.Config.Username)
pwd, err = passwordFromConsole()
if err != nil {
fmt.Println(err)
return "", trace.Wrap(err)
}
return pwd, nil
}
// passwordFromConsole reads from stdin without echoing typed characters to stdout
func passwordFromConsole() (string, error) {
fd := syscall.Stdin

View file

@ -233,8 +233,8 @@ func MakeSampleFileConfig() (fc *FileConfig) {
var a Auth
a.ListenAddress = conf.Auth.SSHAddr.Addr
a.EnabledFlag = "yes"
a.U2fAppId = conf.Hostname
a.U2fTrustedFacets = []string{conf.Hostname}
a.U2fAppId = conf.Auth.U2fAppId
a.U2fTrustedFacets = conf.Auth.U2fTrustedFacets
// sample proxy config:
var p Proxy

View file

@ -306,8 +306,8 @@ func ApplyDefaults(cfg *Config) {
cfg.Auth.KeysBackend.Params = boltParams(defaults.DataDir, defaults.KeysBoltFile)
cfg.Auth.RecordsBackend.Type = defaults.BackendType
cfg.Auth.RecordsBackend.Params = boltParams(defaults.DataDir, defaults.RecordsBoltFile)
cfg.Auth.U2fAppId = hostname
cfg.Auth.U2fTrustedFacets = []string{hostname}
cfg.Auth.U2fAppId = "https://" + hostname
cfg.Auth.U2fTrustedFacets = []string{fmt.Sprintf("%s:%d", cfg.Auth.U2fAppId, defaults.HTTPListenPort)}
defaults.ConfigureLimiter(&cfg.Auth.Limiter)
// defaults for the SSH proxy service:

View file

@ -260,9 +260,6 @@ func (s *sessionCache) AuthWithU2fSignResponse(user string, u2fSignResponse *u2f
if err != nil {
return nil, trace.Wrap(err)
}
// we are always closing this client, because we will not be using
// this connection initiated using password based credentials
// down the road, so it's a one call client
defer clt.Close()
session, err := clt.PreAuthenticatedSignIn(user)
if err != nil {
@ -303,6 +300,35 @@ func (s *sessionCache) GetCertificate(c createSSHCertReq) (*SSHLoginResponse, er
}, nil
}
func (s *sessionCache) GetCertificateWithU2f(c createSSHCertWithU2fReq) (*SSHLoginResponse, error) {
method, err := auth.NewWebU2fSignResponseAuth(c.User, &c.U2fSignResponse)
if err != nil {
return nil, trace.Wrap(err)
}
clt, err := auth.NewTunClient("web.auth-u2f-sign-response", s.authServers, c.User, method)
if err != nil {
return nil, trace.Wrap(err)
}
cert, err := clt.GenerateUserCert(c.PubKey, c.User, c.TTL)
if err != nil {
return nil, trace.Wrap(err)
}
hostSigners, err := clt.GetCertAuthorities(services.HostCA, false)
if err != nil {
return nil, trace.Wrap(err)
}
signers := []services.CertAuthority{}
for _, hs := range hostSigners {
signers = append(signers, *hs)
}
return &SSHLoginResponse{
Cert: cert,
HostSigners: signers,
}, nil
}
func (s *sessionCache) GetUserInviteInfo(token string) (user string,
QRImg []byte, hotpFirstValues []string, e error) {

View file

@ -3,7 +3,9 @@ package web
import (
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
@ -16,6 +18,8 @@ import (
"github.com/gravitational/roundtrip"
"github.com/gravitational/trace"
"github.com/mailgun/lemma/secret"
"github.com/tstranex/u2f"
)
const (
@ -196,6 +200,89 @@ func SSHAgentLogin(proxyAddr, user, password, hotpToken string, pubKey []byte, t
return out, nil
}
// SSHAgentU2fLogin requests a U2F sign request (authentication challenge) via the proxy.
// If the credentials are valid, the proxy wiil return a challenge.
// We then call the official u2f-host binary to perform the signing and pass the signature to the proxy.
// If the authentication succeeds, we will get a temporary certificate back
func SSHAgentU2fLogin(proxyAddr, user, password string, pubKey []byte, ttl time.Duration, insecure bool, pool *x509.CertPool) (*SSHLoginResponse, error) {
clt, _, err := initClient(proxyAddr, insecure, pool)
if err != nil {
return nil, trace.Wrap(err)
}
u2fSignRequest, err := clt.PostJSON(clt.Endpoint("webapi", "u2f", "sign_request"), u2fSignRequestReq{
User: user,
Pass: password,
})
if err != nil {
return nil, trace.Wrap(err)
}
// Pass the JSON-encoded data undecoded to the u2f-host binary
u2fFacet := "https://" + proxyAddr
cmd := exec.Command("u2f-host", "-aauthenticate", "-o", u2fFacet)
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, trace.Wrap(err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, trace.Wrap(err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, trace.Wrap(err)
}
cmd.Start()
stdin.Write(u2fSignRequest.Bytes())
stdin.Close()
fmt.Println("Please press the button on your U2F key")
// The origin URL is passed back base64-encoded and the keyHandle is passed back as is.
// A very long proxy hostname or keyHandle can overflow a fixed-size buffer.
signResponseLen := 500 + len(u2fSignRequest.Bytes()) + len(proxyAddr) * 4 / 3
signResponseBuf := make([]byte, signResponseLen)
signResponseLen, err = io.ReadFull(stdout, signResponseBuf)
// unexpected EOF means we have read the data completely.
if err == nil {
return nil, errors.New("u2f sign response exceeded buffer size")
}
// Read error message (if any). 100 bytes is more than enough for any error message u2f-host outputs
errMsgBuf := make([]byte, 100)
errMsgLen, _ := io.ReadFull(stderr, errMsgBuf)
err = cmd.Wait()
if err != nil {
return nil, errors.New("u2f-host returned error: " + string(errMsgBuf[:errMsgLen]))
}
var u2fSignResponse *u2f.SignResponse
err = json.Unmarshal(signResponseBuf[:signResponseLen], &u2fSignResponse)
if err != nil {
return nil, trace.Wrap(err)
}
re, err := clt.PostJSON(clt.Endpoint("webapi", "u2f", "certs"), createSSHCertWithU2fReq{
User: user,
U2fSignResponse: *u2fSignResponse,
PubKey: pubKey,
TTL: ttl,
})
if err != nil {
return nil, trace.Wrap(err)
}
var out *SSHLoginResponse
err = json.Unmarshal(re.Bytes(), &out)
if err != nil {
return nil, trace.Wrap(err)
}
return out, nil
}
func initClient(proxyAddr string, insecure bool, pool *x509.CertPool) (*webClient, *url.URL, error) {
// validate proxyAddr:
host, port, err := net.SplitHostPort(proxyAddr)

View file

@ -125,16 +125,12 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*Handler, error) {
// Web sessions
h.POST("/webapi/sessions", httplib.MakeHandler(h.createSession))
h.POST("/webapi/sessions_u2f_sign", httplib.MakeHandler(h.u2fSignRequest))
h.POST("/webapi/sessions_u2f", httplib.MakeHandler(h.createSessionWithU2fSignResponse))
h.DELETE("/webapi/sessions/:sid", h.withAuth(h.deleteSession))
h.POST("/webapi/sessions/renew", h.withAuth(h.renewSession))
// Users
h.GET("/webapi/users/invites/:token", httplib.MakeHandler(h.renderUserInvite))
h.GET("/webapi/users/invites_u2f_register/:token", httplib.MakeHandler(h.u2fRegisterRequest))
h.POST("/webapi/users", httplib.MakeHandler(h.createNewUser))
h.POST("/webapi/users_u2f", httplib.MakeHandler(h.createNewU2fUser))
// Issues SSH temp certificates based on 2FA access creds
h.POST("/webapi/ssh/certs", httplib.MakeHandler(h.createSSHCert))
@ -170,6 +166,13 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*Handler, error) {
h.POST("/webapi/oidc/login/console", httplib.MakeHandler(h.oidcLoginConsole))
h.GET("/webapi/oidc/callback", httplib.MakeHandler(h.oidcCallback))
// U2F related APIs
h.GET("/webapi/u2f/invite_register_request/:token", httplib.MakeHandler(h.u2fRegisterRequest))
h.POST("/webapi/u2f/new_user", httplib.MakeHandler(h.createNewU2fUser))
h.POST("/webapi/u2f/sign_request", httplib.MakeHandler(h.u2fSignRequest))
h.POST("/webapi/u2f/sessions", httplib.MakeHandler(h.createSessionWithU2fSignResponse))
h.POST("/webapi/u2f/certs", httplib.MakeHandler(h.createSSHCertWithU2fSignResponse))
// if Web UI is enabled, chekc the assets dir:
var (
writeSettings http.HandlerFunc
@ -1232,6 +1235,35 @@ func (h *Handler) createSSHCert(w http.ResponseWriter, r *http.Request, p httpro
return cert, nil
}
// createSSHCertWithU2fReq are passed by web client
// to authenticate against teleport server and receive
// a temporary cert signed by auth server authority
type createSSHCertWithU2fReq struct {
// User is a teleport username
User string `json:"user"`
// We only issue U2F sign requests after checking the password, so there's no need to check again.
// U2fSignResponse is the signature from the U2F device
U2fSignResponse u2f.SignResponse `json:"u2f_sign_response"`
// PubKey is a public key user wishes to sign
PubKey []byte `json:"pub_key"`
// TTL is a desired TTL for the cert (max is still capped by server,
// however user can shorten the time)
TTL time.Duration `json:"ttl"`
}
func (h *Handler) createSSHCertWithU2fSignResponse(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *createSSHCertWithU2fReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
cert, err := h.auth.GetCertificateWithU2f(*req)
if err != nil {
return nil, trace.Wrap(err)
}
return cert, nil
}
func (h *Handler) String() string {
return fmt.Sprintf("multi site")
}

View file

@ -107,7 +107,7 @@ const (
RoleProvisionToken Role = "ProvisionToken"
// RoleSignup is for first time signing up users
RoleSignup Role = "Signup"
// RoleU2fSign is for requesting a U2F auth challenge
// RoleU2fSign is for partially authenticated U2F users who need to request a U2F auth challenge
RoleU2fSign = "U2fSign"
// RoleU2fUser is for teleport SSH user already authenticated with U2F
RoleU2fUser = "U2fUser"

View file

@ -73,6 +73,8 @@ type CLIConf struct {
LocalExec bool
// ExternalAuth is used to authenticate using external OIDC method
ExternalAuth string
// U2f switches to logging in using U2F as the second factor
U2f bool
// SiteName specifies remote site go login to
SiteName string
// Interactive, when set to true, launches remote command with the terminal attached
@ -92,6 +94,7 @@ func run(args []string, underTest bool) {
app.Flag("login", "Remote host login").Short('l').Envar("TELEPORT_LOGIN").StringVar(&cf.NodeLogin)
app.Flag("user", fmt.Sprintf("SSH proxy user [%s]", client.Username())).Envar("TELEPORT_USER").StringVar(&cf.Username)
app.Flag("auth", "[EXPERIMENTAL] Use external authentication, e.g. 'google'").Envar("TELEPORT_AUTH").Hidden().StringVar(&cf.ExternalAuth)
app.Flag("u2f", "Use U2F as second factor").Default("false").BoolVar(&cf.U2f)
app.Flag("cluster", "Specify the cluster to connect").Envar("TELEPORT_SITE").StringVar(&cf.SiteName)
app.Flag("proxy", "SSH proxy host or IP address").Envar("TELEPORT_PROXY").StringVar(&cf.Proxy)
app.Flag("ttl", "Minutes to live for a SSH session").Int32Var(&cf.MinsToLive)
@ -383,6 +386,15 @@ func makeClient(cf *CLIConf) (tc *client.TeleportClient, err error) {
return nil, err
}
var secondFactorType string
if cf.ExternalAuth != "" {
secondFactorType = "oidc"
} else if cf.U2f {
secondFactorType = "u2f"
} else {
secondFactorType = "hotp"
}
// prep client config:
c := &client.Config{
Stdout: os.Stdout,
@ -397,6 +409,7 @@ func makeClient(cf *CLIConf) (tc *client.TeleportClient, err error) {
KeyTTL: time.Minute * time.Duration(cf.MinsToLive),
InsecureSkipVerify: cf.InsecureSkipVerify,
LocalForwardPorts: fPorts,
SecondFactorType: secondFactorType,
ConnectorID: cf.ExternalAuth,
SiteName: cf.SiteName,
Interactive: cf.Interactive,