From 95af4c18084f2d7daf06883a271a81065b53dcbd Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Wed, 16 Oct 2024 12:00:03 +0200 Subject: [PATCH] Support custom hardware key prompt (#47273) * Allow passing custom prompt to YubiKey * Handle `prompt.Touch` cancellation * Pass `HardwareKeyPrompt` through all the layers * Add an empty `HardwareKeyPromptConstructor` to Connect * Remove `ParsePrivateKeyWithCustomPrompt` * Add missing godoc * Fix teleterm tests * Include `cliprompt.go` only for `go:build piv && !pivtest` * Lint and test fixes --- api/utils/keys/cliprompt.go | 126 ++++++++++++++++++++++++++ api/utils/keys/privatekey.go | 36 ++++++-- api/utils/keys/yubikey.go | 136 ++++++++++------------------- api/utils/keys/yubikey_common.go | 31 ++++++- api/utils/keys/yubikey_fake.go | 4 +- api/utils/keys/yubikey_other.go | 4 +- api/utils/keys/yubikey_test.go | 14 +-- integration/proxy/teleterm_test.go | 4 + integration/teleterm_test.go | 31 +++++++ lib/client/api.go | 10 ++- lib/client/client_store.go | 6 ++ lib/client/keystore.go | 46 +++++++--- lib/teleterm/clusters/config.go | 9 ++ lib/teleterm/clusters/storage.go | 12 +-- lib/teleterm/daemon/daemon_test.go | 13 +++ lib/teleterm/teleterm.go | 5 ++ 16 files changed, 359 insertions(+), 128 deletions(-) create mode 100644 api/utils/keys/cliprompt.go diff --git a/api/utils/keys/cliprompt.go b/api/utils/keys/cliprompt.go new file mode 100644 index 000000000000..8ac27790efe4 --- /dev/null +++ b/api/utils/keys/cliprompt.go @@ -0,0 +1,126 @@ +//go:build piv && !pivtest + +// Copyright 2024 Gravitational, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package keys + +import ( + "context" + "fmt" + "os" + + "github.com/go-piv/piv-go/piv" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/utils/prompt" +) + +type cliPrompt struct{} + +func (c *cliPrompt) AskPIN(ctx context.Context, message string) (string, error) { + password, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), message) + return password, trace.Wrap(err) +} + +func (c *cliPrompt) Touch(_ context.Context) error { + _, err := fmt.Fprintln(os.Stderr, "Tap your YubiKey") + return trace.Wrap(err) +} + +func (c *cliPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) { + var pinAndPUK = &PINAndPUK{} + for { + fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PIN.\n") + newPIN, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your new YubiKey PIV PIN") + if err != nil { + return nil, trace.Wrap(err) + } + newPINConfirm, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Confirm your new YubiKey PIV PIN") + if err != nil { + return nil, trace.Wrap(err) + } + + if newPIN != newPINConfirm { + fmt.Fprintf(os.Stderr, "PINs do not match.\n") + continue + } + + if newPIN == piv.DefaultPIN { + fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN) + continue + } + + if !isPINLengthValid(newPIN) { + fmt.Fprintf(os.Stderr, "PIN must be 6-8 characters long.\n") + continue + } + + pinAndPUK.PIN = newPIN + break + } + + puk, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your YubiKey PIV PUK to reset PIN [blank to use default PUK]") + if err != nil { + return nil, trace.Wrap(err) + } + pinAndPUK.PUK = puk + + switch puk { + case piv.DefaultPUK: + fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", piv.DefaultPUK) + fallthrough + case "": + for { + fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PUK (used to reset PIN).\n") + newPUK, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your new YubiKey PIV PUK") + if err != nil { + return nil, trace.Wrap(err) + } + newPUKConfirm, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Confirm your new YubiKey PIV PUK") + if err != nil { + return nil, trace.Wrap(err) + } + + if newPUK != newPUKConfirm { + fmt.Fprintf(os.Stderr, "PUKs do not match.\n") + continue + } + + if newPUK == piv.DefaultPUK { + fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", piv.DefaultPUK) + continue + } + + if !isPINLengthValid(newPUK) { + fmt.Fprintf(os.Stderr, "PUK must be 6-8 characters long.\n") + continue + } + + pinAndPUK.PUK = newPUK + pinAndPUK.PUKChanged = true + break + } + } + return pinAndPUK, nil +} + +func (c *cliPrompt) ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) { + confirmation, err := prompt.Confirmation(ctx, os.Stderr, prompt.Stdin(), message) + return confirmation, trace.Wrap(err) +} + +func isPINLengthValid(pin string) bool { + return len(pin) >= 6 && len(pin) <= 8 +} diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index 694aa5767962..57da1fa0474f 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -212,8 +212,32 @@ func LoadPrivateKey(keyFile string) (*PrivateKey, error) { return priv, nil } +// ParsePrivateKeyOptions contains config options for ParsePrivateKey. +type ParsePrivateKeyOptions struct { + // CustomHardwareKeyPrompt is a custom hardware key prompt to use when asking + // for a hardware key PIN, touch, etc. + // If empty, a default CLI prompt is used. + CustomHardwareKeyPrompt HardwareKeyPrompt +} + +// ParsePrivateKeyOpt applies configuration options. +type ParsePrivateKeyOpt func(o *ParsePrivateKeyOptions) + +// WithCustomPrompt sets a custom hardware key prompt. +func WithCustomPrompt(prompt HardwareKeyPrompt) ParsePrivateKeyOpt { + return func(o *ParsePrivateKeyOptions) { + o.CustomHardwareKeyPrompt = prompt + } +} + // ParsePrivateKey returns the PrivateKey for the given key PEM block. -func ParsePrivateKey(keyPEM []byte) (*PrivateKey, error) { +// Allows passing a custom hardware key prompt. +func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, error) { + var appliedOpts ParsePrivateKeyOptions + for _, o := range opts { + o(&appliedOpts) + } + block, _ := pem.Decode(keyPEM) if block == nil { return nil, trace.BadParameter("expected PEM encoded private key") @@ -221,7 +245,7 @@ func ParsePrivateKey(keyPEM []byte) (*PrivateKey, error) { switch block.Type { case pivYubiKeyPrivateKeyType: - priv, err := parseYubiKeyPrivateKeyData(block.Bytes) + priv, err := parseYubiKeyPrivateKeyData(block.Bytes, appliedOpts.CustomHardwareKeyPrompt) return priv, trace.Wrap(err, "parsing YubiKey private key") case OpenSSHPrivateKeyType: priv, err := ssh.ParseRawPrivateKey(keyPEM) @@ -299,7 +323,7 @@ func MarshalPrivateKey(key crypto.Signer) ([]byte, error) { } // LoadKeyPair returns the PrivateKey for the given private and public key files. -func LoadKeyPair(privFile, sshPubFile string) (*PrivateKey, error) { +func LoadKeyPair(privFile, sshPubFile string, customPrompt HardwareKeyPrompt) (*PrivateKey, error) { privPEM, err := os.ReadFile(privFile) if err != nil { return nil, trace.ConvertSystemError(err) @@ -310,7 +334,7 @@ func LoadKeyPair(privFile, sshPubFile string) (*PrivateKey, error) { return nil, trace.ConvertSystemError(err) } - priv, err := ParseKeyPair(privPEM, marshaledSSHPub) + priv, err := ParseKeyPair(privPEM, marshaledSSHPub, customPrompt) if err != nil { return nil, trace.Wrap(err) } @@ -318,8 +342,8 @@ func LoadKeyPair(privFile, sshPubFile string) (*PrivateKey, error) { } // ParseKeyPair returns the PrivateKey for the given private and public key PEM blocks. -func ParseKeyPair(privPEM, marshaledSSHPub []byte) (*PrivateKey, error) { - priv, err := ParsePrivateKey(privPEM) +func ParseKeyPair(privPEM, marshaledSSHPub []byte, customPrompt HardwareKeyPrompt) (*PrivateKey, error) { + priv, err := ParsePrivateKey(privPEM, WithCustomPrompt(customPrompt)) if err != nil { return nil, trace.Wrap(err) } diff --git a/api/utils/keys/yubikey.go b/api/utils/keys/yubikey.go index 3435df04c9d1..790d9a2a626b 100644 --- a/api/utils/keys/yubikey.go +++ b/api/utils/keys/yubikey.go @@ -42,7 +42,6 @@ import ( "github.com/gravitational/teleport/api" attestation "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" - "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/api/utils/retryutils" ) @@ -73,7 +72,10 @@ var ( // getOrGenerateYubiKeyPrivateKey connects to a connected yubiKey and gets a private key // matching the given touch requirement. This private key will either be newly generated // or previously generated by a Teleport client and reused. -func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy PrivateKeyPolicy, slot PIVSlot) (*PrivateKey, error) { +func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy PrivateKeyPolicy, slot PIVSlot, prompt HardwareKeyPrompt) (*PrivateKey, error) { + if prompt == nil { + prompt = &cliPrompt{} + } cachedKeysMu.Lock() defer cachedKeysMu.Unlock() @@ -95,7 +97,7 @@ func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy Priva } // Use the first yubiKey we find. - y, err := FindYubiKey(0) + y, err := FindYubiKey(0, prompt) if err != nil { return nil, trace.Wrap(err) } @@ -109,7 +111,7 @@ func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy Priva promptOverwriteSlot := func(msg string) error { promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg) - if confirmed, confirmErr := prompt.Confirmation(ctx, os.Stderr, prompt.Stdin(), promptQuestion); confirmErr != nil { + if confirmed, confirmErr := prompt.ConfirmSlotOverwrite(ctx, promptQuestion); confirmErr != nil { return trace.Wrap(confirmErr) } else if !confirmed { return trace.Wrap(trace.CompareFailed(msg), "user declined to overwrite slot") @@ -246,7 +248,10 @@ type yubiKeyPrivateKeyData struct { SlotKey uint32 `json:"slot_key"` } -func parseYubiKeyPrivateKeyData(keyDataBytes []byte) (*PrivateKey, error) { +func parseYubiKeyPrivateKeyData(keyDataBytes []byte, prompt HardwareKeyPrompt) (*PrivateKey, error) { + if prompt == nil { + prompt = &cliPrompt{} + } cachedKeysMu.Lock() defer cachedKeysMu.Unlock() @@ -265,7 +270,7 @@ func parseYubiKeyPrivateKeyData(keyDataBytes []byte) (*PrivateKey, error) { return key, nil } - y, err := FindYubiKey(keyData.SerialNumber) + y, err := FindYubiKey(keyData.SerialNumber, prompt) if err != nil { return nil, trace.Wrap(err) } @@ -326,6 +331,7 @@ func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []b // single connection, use a lock to queue through signature requests one at a time. y.signMux.Lock() defer y.signMux.Unlock() + ctx, cancel := context.WithCancelCause(ctx) // Lock the connection for the entire duration of the sign process. // Without this, the connection will be released after releaseConnectionDelay, @@ -346,7 +352,12 @@ func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []b select { case <-touchPromptDelayTimer.C: // Prompt for touch after a delay, in case the function succeeds without touch due to a cached touch. - fmt.Fprintln(os.Stderr, "Tap your YubiKey") + err := y.prompt.Touch(ctx) + if err != nil { + // Cancel the entire function when an error occurs. + // This is typically used for aborting the prompt. + cancel(trace.Wrap(err)) + } return case <-ctx.Done(): // touch cached, skip prompt. @@ -363,7 +374,8 @@ func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []b defer touchPromptDelayTimer.Reset(signTouchPromptDelay) } } - return prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your YubiKey PIV PIN") + pass, err := y.prompt.AskPIN(ctx, "Enter your YubiKey PIV PIN") + return pass, trace.Wrap(err) } auth := piv.KeyAuth{ @@ -524,13 +536,15 @@ type YubiKey struct { *sharedPIVConnection // serialNumber is the yubiKey's 8 digit serial number. serialNumber uint32 + prompt HardwareKeyPrompt } -func newYubiKey(card string) (*YubiKey, error) { +func newYubiKey(card string, prompt HardwareKeyPrompt) (*YubiKey, error) { y := &YubiKey{ sharedPIVConnection: &sharedPIVConnection{ card: card, }, + prompt: prompt, } serialNumber, err := y.serial() @@ -652,7 +666,7 @@ func (y *YubiKey) SetPIN(oldPin, newPin string) error { // If the user provides the default PIN, they will be prompted to set a // non-default PIN and PUK before continuing. func (y *YubiKey) checkOrSetPIN(ctx context.Context) error { - pin, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your YubiKey PIV PIN [blank to use default PIN]") + pin, err := y.prompt.AskPIN(ctx, "Enter your YubiKey PIV PIN [blank to use default PIN]") if err != nil { return trace.Wrap(err) } @@ -662,7 +676,7 @@ func (y *YubiKey) checkOrSetPIN(ctx context.Context) error { fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN) fallthrough case "": - if pin, err = y.setPINAndPUKFromDefault(ctx); err != nil { + if pin, err = y.setPINAndPUKFromDefault(ctx, y.prompt); err != nil { return trace.Wrap(err) } } @@ -871,93 +885,37 @@ func (c *sharedPIVConnection) verifyPIN(pin string) error { return trace.Wrap(c.conn.VerifyPIN(pin)) } -func (c *sharedPIVConnection) setPINAndPUKFromDefault(ctx context.Context) (string, error) { +func (c *sharedPIVConnection) setPINAndPUKFromDefault(ctx context.Context, prompt HardwareKeyPrompt) (string, error) { + pinAndPUK, err := prompt.ChangePIN(ctx) + if err != nil { + return "", trace.Wrap(err) + } // YubiKey requires that PIN and PUK be 6-8 characters. - isValid := func(pin string) bool { - return len(pin) >= 6 && len(pin) <= 8 + // Verify that we get valid values from the prompt. + if !isPINLengthValid(pinAndPUK.PIN) { + return "", trace.BadParameter("PIN must be 6-8 characters long") } - - var pin string - for { - fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PIN.\n") - newPIN, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your new YubiKey PIV PIN") - if err != nil { - return "", trace.Wrap(err) - } - newPINConfirm, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Confirm your new YubiKey PIV PIN") - if err != nil { - return "", trace.Wrap(err) - } - - if newPIN != newPINConfirm { - fmt.Fprintf(os.Stderr, "PINs do not match.\n") - continue - } - - if newPIN == piv.DefaultPIN { - fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN) - continue - } - - if !isValid(newPIN) { - fmt.Fprintf(os.Stderr, "PIN must be 6-8 characters long.\n") - continue - } - - pin = newPIN - break + if pinAndPUK.PIN == piv.DefaultPIN { + return "", trace.BadParameter("The default PIN is not supported") } - - puk, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your YubiKey PIV PUK to reset PIN [blank to use default PUK]") - if err != nil { - return "", trace.Wrap(err) + if !isPINLengthValid(pinAndPUK.PUK) { + return "", trace.BadParameter("PUK must be 6-8 characters long") + } + if pinAndPUK.PUK == piv.DefaultPUK { + return "", trace.BadParameter("The default PUK is not supported") } - switch puk { - case piv.DefaultPUK: - fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", piv.DefaultPUK) - fallthrough - case "": - for { - fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PUK (used to reset PIN).\n") - newPUK, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your new YubiKey PIV PUK") - if err != nil { - return "", trace.Wrap(err) - } - newPUKConfirm, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Confirm your new YubiKey PIV PUK") - if err != nil { - return "", trace.Wrap(err) - } - - if newPUK != newPUKConfirm { - fmt.Fprintf(os.Stderr, "PUKs do not match.\n") - continue - } - - if newPUK == piv.DefaultPUK { - fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", piv.DefaultPUK) - continue - } - - if !isValid(newPUK) { - fmt.Fprintf(os.Stderr, "PUK must be 6-8 characters long.\n") - continue - } - - if err := c.setPUK(piv.DefaultPUK, newPUK); err != nil { - return "", trace.Wrap(err) - } - - puk = newPUK - break + if pinAndPUK.PUKChanged { + if err := c.setPUK(piv.DefaultPUK, pinAndPUK.PUK); err != nil { + return "", trace.Wrap(err) } } - if err := c.unblock(puk, pin); err != nil { + if err := c.unblock(pinAndPUK.PUK, pinAndPUK.PIN); err != nil { return "", trace.Wrap(err) } - return pin, nil + return pinAndPUK.PIN, nil } func isRetryError(err error) bool { @@ -967,7 +925,7 @@ func isRetryError(err error) bool { // FindYubiKey finds a yubiKey PIV card by serial number. If no serial // number is provided, the first yubiKey found will be returned. -func FindYubiKey(serialNumber uint32) (*YubiKey, error) { +func FindYubiKey(serialNumber uint32, prompt HardwareKeyPrompt) (*YubiKey, error) { yubiKeyCards, err := findYubiKeyCards() if err != nil { return nil, trace.Wrap(err) @@ -981,7 +939,7 @@ func FindYubiKey(serialNumber uint32) (*YubiKey, error) { } for _, card := range yubiKeyCards { - y, err := newYubiKey(card) + y, err := newYubiKey(card, prompt) if err != nil { return nil, trace.Wrap(err) } diff --git a/api/utils/keys/yubikey_common.go b/api/utils/keys/yubikey_common.go index c76d748500f6..78ffd1f86c91 100644 --- a/api/utils/keys/yubikey_common.go +++ b/api/utils/keys/yubikey_common.go @@ -19,6 +19,33 @@ import ( "github.com/gravitational/trace" ) +// HardwareKeyPrompt provides methods to interact with a YubiKey hardware key. +type HardwareKeyPrompt interface { + // AskPIN prompts the user for a PIN. + AskPIN(ctx context.Context, message string) (string, error) + // Touch prompts the user to touch the hardware key. + Touch(ctx context.Context) error + // ChangePIN asks for a new PIN. + // If the PUK has a default value, it should ask for the new value for it. + // It is up to the implementer how the validation is handled. + // For example, CLI prompt can ask for a valid PIN/PUK in a loop, a GUI + // prompt can use the frontend validation. + ChangePIN(ctx context.Context) (*PINAndPUK, error) + // ConfirmSlotOverwrite asks the user if the slot's private key and certificate can be overridden. + ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) +} + +// PINAndPUK describes a response returned from HardwareKeyPrompt.ChangePIN. +type PINAndPUK struct { + // New PIN set by the user. + PIN string + // PUK used to change the PIN. + // This is a new PUK if it has not been changed (from the default PUK). + PUK string + // PUKChanged is true if the user changed the default PUK. + PUKChanged bool +} + // GetYubiKeyPrivateKey attempt to retrieve a YubiKey private key matching the given hardware key policy // from the given slot. If slot is unspecified, the default slot for the given key policy will be used. // If the slot is empty, a new private key matching the given policy will be generated in the slot. @@ -26,8 +53,8 @@ import ( // - hardware_key_touch: 9c // - hardware_key_pin: 9d // - hardware_key_touch_pin: 9e -func GetYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot) (*PrivateKey, error) { - priv, err := getOrGenerateYubiKeyPrivateKey(ctx, policy, slot) +func GetYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot, customPrompt HardwareKeyPrompt) (*PrivateKey, error) { + priv, err := getOrGenerateYubiKeyPrivateKey(ctx, policy, slot, customPrompt) if err != nil { return nil, trace.Wrap(err, "failed to get a YubiKey private key") } diff --git a/api/utils/keys/yubikey_fake.go b/api/utils/keys/yubikey_fake.go index 375c22f0d855..33be8917815d 100644 --- a/api/utils/keys/yubikey_fake.go +++ b/api/utils/keys/yubikey_fake.go @@ -28,7 +28,7 @@ import ( var errPIVUnavailable = errors.New("PIV is unavailable in current build") // Return a fake YubiKey private key. -func getOrGenerateYubiKeyPrivateKey(_ context.Context, policy PrivateKeyPolicy, _ PIVSlot) (*PrivateKey, error) { +func getOrGenerateYubiKeyPrivateKey(_ context.Context, policy PrivateKeyPolicy, _ PIVSlot, _ HardwareKeyPrompt) (*PrivateKey, error) { _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, trace.Wrap(err) @@ -47,7 +47,7 @@ func getOrGenerateYubiKeyPrivateKey(_ context.Context, policy PrivateKeyPolicy, return NewPrivateKey(signer, keyPEM) } -func parseYubiKeyPrivateKeyData(_ []byte) (*PrivateKey, error) { +func parseYubiKeyPrivateKeyData(_ []byte, _ HardwareKeyPrompt) (*PrivateKey, error) { // TODO(Joerger): add custom marshal/unmarshal logic for fakeYubiKeyPrivateKey (if necessary). return nil, trace.Wrap(errPIVUnavailable) } diff --git a/api/utils/keys/yubikey_other.go b/api/utils/keys/yubikey_other.go index d71d1e836763..77d7de29a2ea 100644 --- a/api/utils/keys/yubikey_other.go +++ b/api/utils/keys/yubikey_other.go @@ -24,11 +24,11 @@ import ( var errPIVUnavailable = errors.New("PIV is unavailable in current build") -func getOrGenerateYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot) (*PrivateKey, error) { +func getOrGenerateYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot, _ HardwareKeyPrompt) (*PrivateKey, error) { return nil, trace.Wrap(errPIVUnavailable) } -func parseYubiKeyPrivateKeyData(keyDataBytes []byte) (*PrivateKey, error) { +func parseYubiKeyPrivateKeyData(keyDataBytes []byte, _ HardwareKeyPrompt) (*PrivateKey, error) { return nil, trace.Wrap(errPIVUnavailable) } diff --git a/api/utils/keys/yubikey_test.go b/api/utils/keys/yubikey_test.go index 4e49315a56ba..72ac01537041 100644 --- a/api/utils/keys/yubikey_test.go +++ b/api/utils/keys/yubikey_test.go @@ -46,7 +46,7 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { ctx := context.Background() - y, err := keys.FindYubiKey(0) + y, err := keys.FindYubiKey(0, nil) require.NoError(t, err) t.Cleanup(func() { resetYubikey(t, y) }) @@ -69,7 +69,7 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { } // GetYubiKeyPrivateKey should generate a new YubiKeyPrivateKey. - priv, err := keys.GetYubiKeyPrivateKey(ctx, policy, slot) + priv, err := keys.GetYubiKeyPrivateKey(ctx, policy, slot, nil) require.NoError(t, err) // test HardwareSigner methods @@ -82,7 +82,7 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { require.NoError(t, err) // Another call to GetYubiKeyPrivateKey should retrieve the previously generated key. - retrievePriv, err := keys.GetYubiKeyPrivateKey(ctx, policy, slot) + retrievePriv, err := keys.GetYubiKeyPrivateKey(ctx, policy, slot, nil) require.NoError(t, err) require.Equal(t, priv.Public(), retrievePriv.Public()) @@ -105,7 +105,7 @@ func TestOverwritePrompt(t *testing.T) { ctx := context.Background() - y, err := keys.FindYubiKey(0) + y, err := keys.FindYubiKey(0, nil) require.NoError(t, err) t.Cleanup(func() { resetYubikey(t, y) }) @@ -117,12 +117,12 @@ func TestOverwritePrompt(t *testing.T) { testOverwritePrompt := func(t *testing.T) { // Fail to overwrite slot when user denies prompt.SetStdin(prompt.NewFakeReader().AddString("n")) - _, err := keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKeyTouch, "" /* slot */) + _, err := keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKeyTouch, "" /* slot */, nil) require.True(t, trace.IsCompareFailed(err), "Expected compare failed error but got %v", err) // Successfully overwrite slot when user accepts prompt.SetStdin(prompt.NewFakeReader().AddString("y")) - _, err = keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKeyTouch, "" /* slot */) + _, err = keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKeyTouch, "" /* slot */, nil) require.NoError(t, err) } @@ -140,7 +140,7 @@ func TestOverwritePrompt(t *testing.T) { resetYubikey(t, y) // Generate a key that does not require touch in the slot that Teleport expects to require touch. - _, err := keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKey, keys.PIVSlot(pivSlot.String())) + _, err := keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKey, keys.PIVSlot(pivSlot.String()), nil) require.NoError(t, err) testOverwritePrompt(t) diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index e6172b0e3e66..67feeda87944 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -38,6 +38,7 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/utils/keys" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/integration/appaccess" dbhelpers "github.com/gravitational/teleport/integration/db" @@ -240,6 +241,9 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer // db cert has expired. Clock: fakeClock, WebauthnLogin: webauthnLogin, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) diff --git a/integration/teleterm_test.go b/integration/teleterm_test.go index 186d08857909..2b02b92c7d40 100644 --- a/integration/teleterm_test.go +++ b/integration/teleterm_test.go @@ -43,6 +43,7 @@ import ( "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/teleport/api/utils/keys" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" dbhelpers "github.com/gravitational/teleport/integration/db" "github.com/gravitational/teleport/integration/helpers" @@ -257,6 +258,9 @@ func testAddingRootCluster(t *testing.T, pack *dbhelpers.DatabasePack, creds *he storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -289,6 +293,9 @@ func testListRootClustersReturnsLoggedInUser(t *testing.T, pack *dbhelpers.Datab storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -371,6 +378,9 @@ func testGetClusterReturnsPropertiesFromAuthServer(t *testing.T, pack *dbhelpers storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -423,6 +433,9 @@ func testHeadlessWatcher(t *testing.T, pack *dbhelpers.DatabasePack, creds *help storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -491,6 +504,9 @@ func testClientCache(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers. Dir: tc.KeysDir, Clock: storageFakeClock, InsecureSkipVerify: tc.InsecureSkipVerify, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -750,6 +766,9 @@ func testCreateConnectMyComputerRole(t *testing.T, pack *dbhelpers.DatabasePack) storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -866,6 +885,9 @@ func testCreateConnectMyComputerToken(t *testing.T, pack *dbhelpers.DatabasePack InsecureSkipVerify: tc.InsecureSkipVerify, Clock: fakeClock, WebauthnLogin: webauthnLogin, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -926,6 +948,9 @@ func testWaitForConnectMyComputerNodeJoin(t *testing.T, pack *dbhelpers.Database storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -1010,6 +1035,9 @@ func testDeleteConnectMyComputerNode(t *testing.T, pack *dbhelpers.DatabasePack) storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -1237,6 +1265,9 @@ func testListDatabaseUsers(t *testing.T, pack *dbhelpers.DatabasePack) { storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) diff --git a/lib/client/api.go b/lib/client/api.go index 94f07c9cd737..0a7b35dbd5e5 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -490,6 +490,11 @@ type Config struct { // MFAPromptConstructor is a custom MFA prompt constructor to use when prompting for MFA. MFAPromptConstructor func(cfg *libmfa.PromptConfig) mfa.Prompt + // CustomHardwareKeyPrompt is a custom hardware key prompt to use when asking + // for a hardware key PIN, touch, etc. + // If empty, a default CLI prompt is used. + CustomHardwareKeyPrompt keys.HardwareKeyPrompt + // DisableSSHResumption disables transparent SSH connection resumption. DisableSSHResumption bool @@ -1229,6 +1234,9 @@ func NewClient(c *Config) (tc *TeleportClient, err error) { tc.ClientStore = NewMemClientStore() } else { tc.ClientStore = NewFSClientStore(c.KeysDir) + if c.CustomHardwareKeyPrompt != nil { + tc.ClientStore.SetCustomHardwareKeyPrompt(tc.CustomHardwareKeyPrompt) + } if c.AddKeysToAgent == AddKeysToAgentOnly { // Store client keys in memory, but still save trusted certs and profile to disk. tc.ClientStore.KeyStore = NewMemKeyStore() @@ -3880,7 +3888,7 @@ func (tc *TeleportClient) GetNewLoginKeyRing(ctx context.Context) (keyRing *KeyR if tc.PIVSlot != "" { log.Debugf("Using PIV slot %q specified by client or server settings.", tc.PIVSlot) } - priv, err := keys.GetYubiKeyPrivateKey(ctx, tc.PrivateKeyPolicy, tc.PIVSlot) + priv, err := keys.GetYubiKeyPrivateKey(ctx, tc.PrivateKeyPolicy, tc.PIVSlot, tc.CustomHardwareKeyPrompt) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/client_store.go b/lib/client/client_store.go index 4d65be16d0dc..c62c36e9b3d8 100644 --- a/lib/client/client_store.go +++ b/lib/client/client_store.go @@ -81,6 +81,12 @@ func (s *Store) AddKeyRing(keyRing *KeyRing) error { return nil } +// SetCustomHardwareKeyPrompt sets a custom hardware key prompt +// used to interact with a YubiKey private key. +func (s *Store) SetCustomHardwareKeyPrompt(prompt keys.HardwareKeyPrompt) { + s.KeyStore.SetCustomHardwareKeyPrompt(prompt) +} + var ( // ErrNoCredentials is returned by the client store when a specific key is not found. // This error can be used to determine whether a client should retrieve new credentials, diff --git a/lib/client/keystore.go b/lib/client/keystore.go index c84c0506d48c..37e0a688b811 100644 --- a/lib/client/keystore.go +++ b/lib/client/keystore.go @@ -83,6 +83,10 @@ type KeyStore interface { // GetSSHCertificates gets all certificates signed for the given user and proxy, // including certificates for trusted clusters. GetSSHCertificates(proxyHost, username string) ([]*ssh.Certificate, error) + + // SetCustomHardwareKeyPrompt sets a custom hardware key prompt + // used to interact with a YubiKey private key. + SetCustomHardwareKeyPrompt(prompt keys.HardwareKeyPrompt) } // FSKeyStore is an on-disk implementation of the KeyStore interface. @@ -94,6 +98,10 @@ type FSKeyStore struct { // KeyDir is the directory where all keys are stored. KeyDir string + // CustomHardwareKeyPrompt is a custom hardware key prompt to use when asking + // for a hardware key PIN, touch, etc. + // If nil, a default CLI prompt is used. + CustomHardwareKeyPrompt keys.HardwareKeyPrompt } // NewFSKeyStore initializes a new FSClientStore. @@ -174,6 +182,12 @@ func (fs *FSKeyStore) kubeCredPath(idx KeyRingIndex, kubename string) string { return keypaths.KubeCredPath(fs.KeyDir, idx.ProxyHost, idx.Username, idx.ClusterName, kubename) } +// SetCustomHardwareKeyPrompt sets a custom hardware key prompt +// used to interact with a YubiKey private key. +func (fs *FSKeyStore) SetCustomHardwareKeyPrompt(prompt keys.HardwareKeyPrompt) { + fs.CustomHardwareKeyPrompt = prompt +} + // AddKeyRing adds the given key ring to the store. func (fs *FSKeyStore) AddKeyRing(keyRing *KeyRing) error { if err := keyRing.KeyRingIndex.Check(); err != nil { @@ -278,12 +292,12 @@ func (fs *FSKeyStore) writeTLSCredential(cred TLSCredential, keyPath, certPath s return nil } -func readTLSCredential(keyPath, certPath string) (TLSCredential, error) { +func readTLSCredential(keyPath, certPath string, customPrompt keys.HardwareKeyPrompt) (TLSCredential, error) { keyPEM, certPEM, err := readTLSCredentialFiles(keyPath, certPath) if err != nil { return TLSCredential{}, trace.Wrap(err) } - key, err := keys.ParsePrivateKey(keyPEM) + key, err := keys.ParsePrivateKey(keyPEM, keys.WithCustomPrompt(customPrompt)) if err != nil { return TLSCredential{}, trace.Wrap(err) } @@ -498,12 +512,12 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing return nil, trace.Wrap(err, "no session keys for %+v", idx) } - tlsCred, err := readTLSCredential(fs.userTLSKeyPath(idx), fs.tlsCertPath(idx)) + tlsCred, err := readTLSCredential(fs.userTLSKeyPath(idx), fs.tlsCertPath(idx), fs.CustomHardwareKeyPrompt) if err != nil { return nil, trace.Wrap(err) } - sshPriv, err := keys.LoadKeyPair(fs.userSSHKeyPath(idx), fs.publicKeyPath(idx)) + sshPriv, err := keys.LoadKeyPair(fs.userSSHKeyPath(idx), fs.publicKeyPath(idx), fs.CustomHardwareKeyPrompt) if err != nil { return nil, trace.ConvertSystemError(err) } @@ -526,7 +540,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing } func (fs *FSKeyStore) updateKeyRingWithCerts(o CertOption, keyRing *KeyRing) error { - return trace.Wrap(o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing)) + return trace.Wrap(o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, fs.CustomHardwareKeyPrompt)) } // GetSSHCertificates gets all certificates signed for the given user and proxy. @@ -553,7 +567,7 @@ func (fs *FSKeyStore) GetSSHCertificates(proxyHost, username string) ([]*ssh.Cer return sshCerts, nil } -func getCredentialsByName(credentialDir string) (map[string]TLSCredential, error) { +func getCredentialsByName(credentialDir string, customPrompt keys.HardwareKeyPrompt) (map[string]TLSCredential, error) { files, err := os.ReadDir(credentialDir) if err != nil { return nil, trace.ConvertSystemError(err) @@ -563,7 +577,7 @@ func getCredentialsByName(credentialDir string) (map[string]TLSCredential, error if keyName := keypaths.TrimKeyPathSuffix(file.Name()); keyName != file.Name() { keyPath := filepath.Join(credentialDir, file.Name()) certPath := filepath.Join(credentialDir, keyName+keypaths.FileExtTLSCert) - cred, err := readTLSCredential(keyPath, certPath) + cred, err := readTLSCredential(keyPath, certPath, customPrompt) if err != nil { if trace.IsNotFound(err) { // Somehow we have a key with no cert, skip it. This should @@ -601,7 +615,7 @@ func getKubeCredentialsByName(credentialDir string) (map[string]TLSCredential, e type CertOption interface { // updateKeyRing is used by [FSKeyStore] to add the relevant credentials // loaded from disk to [keyRing]. - updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing) error + updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, customPrompt keys.HardwareKeyPrompt) error // pathsToDelete is used by [FSKeyStore] to get all the paths (files and/or // directories) that should be deleted by [DeleteUserCerts]. pathsToDelete(keyDir string, idx KeyRingIndex) []string @@ -615,7 +629,7 @@ var WithAllCerts = []CertOption{WithSSHCerts{}, WithKubeCerts{}, WithDBCerts{}, // WithSSHCerts is a CertOption for handling SSH certificates. type WithSSHCerts struct{} -func (o WithSSHCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing) error { +func (o WithSSHCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, _ keys.HardwareKeyPrompt) error { certPath := keypaths.SSHCertPath(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) cert, err := os.ReadFile(certPath) if err != nil { @@ -639,7 +653,7 @@ func (o WithSSHCerts) deleteFromKeyRing(keyRing *KeyRing) { // WithKubeCerts is a CertOption for handling kubernetes certificates. type WithKubeCerts struct{} -func (o WithKubeCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing) error { +func (o WithKubeCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, _ keys.HardwareKeyPrompt) error { credentialDir := keypaths.KubeCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) credsByName, err := getKubeCredentialsByName(credentialDir) if err != nil { @@ -665,9 +679,9 @@ type WithDBCerts struct { dbName string } -func (o WithDBCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing) error { +func (o WithDBCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, customPrompt keys.HardwareKeyPrompt) error { credentialDir := keypaths.DatabaseCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) - credsByName, err := getCredentialsByName(credentialDir) + credsByName, err := getCredentialsByName(credentialDir, customPrompt) if err != nil { return trace.Wrap(err) } @@ -697,9 +711,9 @@ type WithAppCerts struct { appName string } -func (o WithAppCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing) error { +func (o WithAppCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, customPrompt keys.HardwareKeyPrompt) error { credentialDir := keypaths.AppCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) - credsByName, err := getCredentialsByName(credentialDir) + credsByName, err := getCredentialsByName(credentialDir, customPrompt) if err != nil { return trace.Wrap(err) } @@ -865,3 +879,7 @@ func (ms *MemKeyStore) GetSSHCertificates(proxyHost, username string) ([]*ssh.Ce return sshCerts, nil } + +// SetCustomHardwareKeyPrompt implements the KeyStore.SetCustomHardwareKeyPrompt interface. +// Does nothing. +func (ms *MemKeyStore) SetCustomHardwareKeyPrompt(_ keys.HardwareKeyPrompt) {} diff --git a/lib/teleterm/clusters/config.go b/lib/teleterm/clusters/config.go index 0630594639f7..6af0ad1bbfad 100644 --- a/lib/teleterm/clusters/config.go +++ b/lib/teleterm/clusters/config.go @@ -24,7 +24,9 @@ import ( "github.com/sirupsen/logrus" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/teleterm/api/uri" ) // Config is the cluster service config @@ -42,6 +44,9 @@ type Config struct { WebauthnLogin client.WebauthnLoginFunc // AddKeysToAgent is passed to [client.Config]. AddKeysToAgent string + // CustomHardwareKeyPrompt is a custom hardware key prompt to use when asking + // for a hardware key PIN, touch, etc. + HardwareKeyPromptConstructor func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt } // CheckAndSetDefaults checks the configuration for its validity and sets default values if needed @@ -50,6 +55,10 @@ func (c *Config) CheckAndSetDefaults() error { return trace.BadParameter("missing working directory") } + if c.HardwareKeyPromptConstructor == nil { + return trace.BadParameter("missing hardware key prompt constructor") + } + if c.Clock == nil { c.Clock = clockwork.NewRealClock() } diff --git a/lib/teleterm/clusters/storage.go b/lib/teleterm/clusters/storage.go index 19eb14177538..2be5817012fc 100644 --- a/lib/teleterm/clusters/storage.go +++ b/lib/teleterm/clusters/storage.go @@ -161,11 +161,12 @@ func (s *Storage) addCluster(ctx context.Context, dir, webProxyAddress string) ( return nil, nil, trace.BadParameter("cluster directory is missing") } - cfg := s.makeDefaultClientConfig() - cfg.WebProxyAddr = webProxyAddress - profileName := parseName(webProxyAddress) clusterURI := uri.NewClusterURI(profileName) + + cfg := s.makeDefaultClientConfig(clusterURI) + cfg.WebProxyAddr = webProxyAddress + clusterClient, err := client.NewClient(cfg) if err != nil { return nil, nil, trace.Wrap(err) @@ -215,7 +216,7 @@ func (s *Storage) fromProfile(profileName, leafClusterName string) (*Cluster, *c profileStore := client.NewFSProfileStore(s.Dir) - cfg := s.makeDefaultClientConfig() + cfg := s.makeDefaultClientConfig(clusterURI) if err := cfg.LoadProfile(profileStore, profileName); err != nil { return nil, nil, trace.Wrap(err) } @@ -276,7 +277,7 @@ func (s *Storage) loadProfileStatusAndClusterKey(clusterClient *client.TeleportC return status, nil } -func (s *Storage) makeDefaultClientConfig() *client.Config { +func (s *Storage) makeDefaultClientConfig(rootClusterURI uri.ResourceURI) *client.Config { cfg := client.MakeDefaultConfig() cfg.HomePath = s.Dir @@ -295,6 +296,7 @@ func (s *Storage) makeDefaultClientConfig() *client.Config { // true. cfg.AllowStdinHijack = true + cfg.CustomHardwareKeyPrompt = s.HardwareKeyPromptConstructor(rootClusterURI) cfg.DTAuthnRunCeremony = dtauthn.NewCeremony().Run cfg.DTAutoEnroll = dtenroll.AutoEnroll return cfg diff --git a/lib/teleterm/daemon/daemon_test.go b/lib/teleterm/daemon/daemon_test.go index 827a9c13cba9..00bf1176ce03 100644 --- a/lib/teleterm/daemon/daemon_test.go +++ b/lib/teleterm/daemon/daemon_test.go @@ -39,6 +39,7 @@ import ( "google.golang.org/grpc/status" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/utils/keys" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" @@ -298,6 +299,9 @@ func TestUpdateTshdEventsServerAddress(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: homeDir, InsecureSkipVerify: true, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -332,6 +336,9 @@ func TestUpdateTshdEventsServerAddress_CredsErr(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: homeDir, InsecureSkipVerify: true, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -433,6 +440,9 @@ func TestRetryWithRelogin(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) @@ -486,6 +496,9 @@ func TestImportantModalSemaphore(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) require.NoError(t, err) diff --git a/lib/teleterm/teleterm.go b/lib/teleterm/teleterm.go index 5341723154a2..fd868169642e 100644 --- a/lib/teleterm/teleterm.go +++ b/lib/teleterm/teleterm.go @@ -32,6 +32,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/teleterm/api/uri" "github.com/gravitational/teleport/lib/teleterm/apiserver" "github.com/gravitational/teleport/lib/teleterm/clusteridcache" "github.com/gravitational/teleport/lib/teleterm/clusters" @@ -56,6 +58,9 @@ func Serve(ctx context.Context, cfg Config) error { Clock: clock, InsecureSkipVerify: cfg.InsecureSkipVerify, AddKeysToAgent: cfg.AddKeysToAgent, + HardwareKeyPromptConstructor: func(clusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, }) if err != nil { return trace.Wrap(err)