Skip to content

Commit

Permalink
improve: keep the trust certificate between logins
Browse files Browse the repository at this point in the history
  • Loading branch information
dnephin committed Jun 13, 2022
1 parent 8a6b728 commit dcfdd0c
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 152 deletions.
139 changes: 69 additions & 70 deletions internal/cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ type loginCmdOptions struct {
AccessKey string
Provider string
SkipTLSVerify bool
// TODO: add option to pass a trusted certificate
NonInteractive bool
// TODO: add flag for trusted certificate
TrustedCertificate []byte
NonInteractive bool
}

type loginMethod int8
Expand Down Expand Up @@ -84,15 +85,32 @@ $ infra login --key 1M4CWy9wF5.fAKeKEy5sMLH9ZZzAur0ZIjy`,
}

func login(cli *CLI, options loginCmdOptions) error {
var err error
config, err := readConfig()
if err != nil {
return err
}

if options.Server == "" {
options.Server, err = promptServer(cli, options)
if options.NonInteractive {
return Error{Message: "Non-interactive login requires the [SERVER] argument"}
}

options.Server, err = promptServer(cli, config)
if err != nil {
return err
}
}

if len(options.TrustedCertificate) == 0 {
// Attempt to find a previously trusted certificate
for _, hc := range config.Hosts {
// TODO: comparison fails if one of these includes a scheme
if hc.Host == options.Server {
options.TrustedCertificate = hc.TrustedCertificate
}
}
}

lc, err := newLoginClient(cli, options)
if err != nil {
return err
Expand Down Expand Up @@ -376,76 +394,66 @@ type loginClient struct {

// Only used when logging in or switching to a new session, since user has no credentials. Otherwise, use defaultAPIClient().
func newLoginClient(cli *CLI, options loginCmdOptions) (loginClient, error) {
c := loginClient{}
if !options.SkipTLSVerify {
// Prompt user only if server fails the TLS verification
if err := attemptTLSRequest(options.Server); err != nil {
var uaErr x509.UnknownAuthorityError
if !errors.As(err, &uaErr) {
return c, err
}
cfg := &ClientHostConfig{
TrustedCertificate: options.TrustedCertificate,
SkipTLSVerify: options.SkipTLSVerify,
}
c := loginClient{
APIClient: apiClient(options.Server, "", httpTransportForHostConfig(cfg)),
TrustedCertificate: options.TrustedCertificate,
}
if options.SkipTLSVerify {
return c, nil
}

if options.NonInteractive {
// TODO: add the --tls-ca flag
// TODO: give a different error if the flag was set
return c, Error{
Message: "The authenticity of the server could not be verified. " +
"Use the --tls-ca flag to specify a trusted CA, or run " +
"in interactive mode.",
}
}
// Prompt user only if server fails the TLS verification
if err := attemptTLSRequest(c.APIClient); err != nil {
var uaErr x509.UnknownAuthorityError
if !errors.As(err, &uaErr) {
return c, err
}

if err = promptVerifyTLSCert(cli, uaErr.Cert); err != nil {
return c, err
}
// TODO: cleanup
pool, err := x509.SystemCertPool()
if err != nil {
return c, err
}
pool.AddCert(uaErr.Cert)
transport := &http.Transport{
TLSClientConfig: &tls.Config{
// set min version to the same as default to make gosec linter happy
MinVersion: tls.VersionTLS12,
RootCAs: pool,
},
if options.NonInteractive {
// TODO: add the --tls-ca flag
// TODO: give a different error if the flag was set
return c, Error{
Message: "The authenticity of the server could not be verified. " +
"Use the --tls-ca flag to specify a trusted CA, or run " +
"in interactive mode.",
}
c.APIClient = apiClient(options.Server, "", transport)
c.TrustedCertificate = uaErr.Cert.Raw
return c, nil
}
}

transport := &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec // We may purposely set insecureskipverify via a flag
InsecureSkipVerify: options.SkipTLSVerify,
// TODO: read options.TrustedCertificate
//RootCAs: pool,
},
if err = promptVerifyTLSCert(cli, uaErr.Cert); err != nil {
return c, err
}

pool, err := x509.SystemCertPool()
if err != nil {
return c, err
}
pool.AddCert(uaErr.Cert)
transport := &http.Transport{
TLSClientConfig: &tls.Config{
// set min version to the same as default to make gosec linter happy
MinVersion: tls.VersionTLS12,
RootCAs: pool,
},
}
c.APIClient = apiClient(options.Server, "", transport)
c.TrustedCertificate = uaErr.Cert.Raw
}
c.APIClient = apiClient(options.Server, "", transport)
return c, nil
}

func attemptTLSRequest(host string) error {
// TODO: use apiClient here so that we set the right user agent, and can re-use
// error handling from the client. Use the /api/version endpoint.
reqURL, err := urlx.Parse(host)
if err != nil {
return fmt.Errorf("failed to parse the server hostname: %w", err)
}
reqURL.Scheme = "https"

req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, reqURL.String(), nil)
func attemptTLSRequest(client *api.Client) error {
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, client.URL, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

logging.S.Debugf("call server: test tls for %q", host)
logging.S.Debugf("call server: test tls for %q", client.URL)
urlErr := &url.Error{}
res, err := http.DefaultClient.Do(req)
res, err := client.HTTP.Do(req)
switch {
case err == nil:
res.Body.Close()
Expand Down Expand Up @@ -604,16 +612,7 @@ to manually verify the certificate can be trusted.
}

// Returns the host address of the Infra server that user would like to log into
func promptServer(cli *CLI, options loginCmdOptions) (string, error) {
if options.NonInteractive {
return "", Error{Message: "Non-interactive login requires the [SERVER] argument"}
}

config, err := readConfig()
if err != nil {
return "", err
}

func promptServer(cli *CLI, config *ClientConfig) (string, error) {
servers := config.Hosts

if len(servers) == 0 {
Expand Down
Loading

0 comments on commit dcfdd0c

Please sign in to comment.