-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: Hardware Key Agent - consolidate globally shared PIV service variables #53974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,37 +35,52 @@ import ( | |
| "github.com/gravitational/teleport/api/utils/keys/hardwarekey" | ||
| ) | ||
|
|
||
| // TODO(Joerger): Rather than using a global cache and mutexes, clients should be updated | ||
| // to create a single YubiKeyService and ensure it is reused across the program execution. | ||
| var ( | ||
| // yubiKeyService is a global YubiKeyService used to share yubikey connections | ||
| // and prompt mutex logic across the process in cases where [NewYubiKeyService] | ||
| // is called multiple times. | ||
| // | ||
| // TODO(Joerger): Ensure all clients initialize [NewYubiKeyService] only once so we can | ||
| // remove this global variable. | ||
| var yubiKeyService *YubiKeyService | ||
| var yubiKeyServiceMux sync.Mutex | ||
|
|
||
| // YubiKeyService is a YubiKey PIV implementation of [hardwarekey.Service]. | ||
| type YubiKeyService struct { | ||
| prompt hardwarekey.Prompt | ||
| promptMux sync.Mutex | ||
|
|
||
| // yubiKeys is a shared, thread-safe [YubiKey] cache by serial number. It allows for | ||
| // separate goroutines to share a YubiKey connection to work around the single PC/SC | ||
| // transaction (connection) per-yubikey limit. | ||
| yubiKeys map[uint32]*YubiKey = map[uint32]*YubiKey{} | ||
| yubiKeys map[uint32]*YubiKey | ||
| yubiKeysMux sync.Mutex | ||
|
|
||
| // promptMux is used to prevent over-prompting, especially for back-to-back sign requests | ||
| // since touch/PIN from the first signature should be cached for following signatures. | ||
| promptMux sync.Mutex | ||
| ) | ||
|
|
||
| // YubiKeyService is a YubiKey PIV implementation of [hardwarekey.Service]. | ||
| type YubiKeyService struct { | ||
| prompt hardwarekey.Prompt | ||
| } | ||
|
|
||
| // Returns a new [YubiKeyService]. If [customPrompt] is nil, the default CLI prompt will be used. | ||
| // | ||
| // Only a single service should be created for each process to ensure the cached connections | ||
| // are shared and multiple services don't compete for PIV resources. | ||
| func NewYubiKeyService(customPrompt hardwarekey.Prompt) *YubiKeyService { | ||
| yubiKeyServiceMux.Lock() | ||
| defer yubiKeyServiceMux.Unlock() | ||
|
|
||
| if yubiKeyService != nil { | ||
| // If a prompt is provided, prioritize it over the existing prompt value. | ||
| if customPrompt != nil { | ||
| yubiKeyService.prompt = customPrompt | ||
| } | ||
| return yubiKeyService | ||
| } | ||
|
Comment on lines
+67
to
+73
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we share the same set of yubikeys but have different prompts? Is that something that we ever need?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not something we need currently, or should ever need. Right now we are primarily handling cases like: // pre-action checks in profile/cert store (view keys, expiration,
// active profile, etc.), no prompt expected
piv.NewYubiKeyService(nil) // defaults to CLIPrompt, but no prompt expected (no signatures)
// client and client store initialization in `client.NewClient`.
// This store is/should be used for all key interactions.
piv.NewYubiKeyService(customPrompt) // overwrite with custom prompt e.g. for Connect
// adhoc `keys.ParsePrivateKey` calls which fall outside of the
// client store's path (no hwKeyService to pass).
piv.NewYubiKeyService(nil) // continue using custom prompt or default cli promptOr for non tctl/tsh/connect clients, it's likely they will just call |
||
|
|
||
| if customPrompt == nil { | ||
| customPrompt = hardwarekey.NewStdCLIPrompt() | ||
| } | ||
|
|
||
| return &YubiKeyService{ | ||
| prompt: customPrompt, | ||
| yubiKeyService = &YubiKeyService{ | ||
| prompt: customPrompt, | ||
| yubiKeys: map[uint32]*YubiKey{}, | ||
| } | ||
| return yubiKeyService | ||
| } | ||
|
|
||
| // NewPrivateKey creates a hardware private key that satisfies the provided [config], | ||
|
|
@@ -170,8 +185,8 @@ func (s *YubiKeyService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRe | |
| return nil, trace.Wrap(err) | ||
| } | ||
|
|
||
| promptMux.Lock() | ||
| defer promptMux.Unlock() | ||
| s.promptMux.Lock() | ||
| defer s.promptMux.Unlock() | ||
|
|
||
| return y.sign(ctx, ref, keyInfo, s.prompt, rand, digest, opts) | ||
| } | ||
|
|
@@ -227,10 +242,10 @@ func (s *YubiKeyService) GetFullKeyRef(serialNumber uint32, slotKey hardwarekey. | |
| // Get the given YubiKey with the serial number. If the provided serialNumber is "0", | ||
| // return the first YubiKey found in the smart card list. | ||
| func (s *YubiKeyService) getYubiKey(serialNumber uint32) (*YubiKey, error) { | ||
| yubiKeysMux.Lock() | ||
| defer yubiKeysMux.Unlock() | ||
| s.yubiKeysMux.Lock() | ||
| defer s.yubiKeysMux.Unlock() | ||
|
Comment on lines
+245
to
+246
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this mux protecting
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see how it could look like that, but no the mutex is just protecting the yubikey map.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI this is exercised in |
||
|
|
||
| if y, ok := yubiKeys[serialNumber]; ok { | ||
| if y, ok := s.yubiKeys[serialNumber]; ok { | ||
| return y, nil | ||
| } | ||
|
|
||
|
|
@@ -239,16 +254,16 @@ func (s *YubiKeyService) getYubiKey(serialNumber uint32) (*YubiKey, error) { | |
| return nil, trace.Wrap(err) | ||
| } | ||
|
|
||
| yubiKeys[y.serialNumber] = y | ||
| s.yubiKeys[y.serialNumber] = y | ||
| return y, nil | ||
| } | ||
|
|
||
| // checkOrSetPIN prompts the user for PIN and verifies it with the YubiKey. | ||
| // If the user provides the default PIN, they will be prompted to set a | ||
| // non-default PIN and PUK before continuing. | ||
| func (s *YubiKeyService) checkOrSetPIN(ctx context.Context, y *YubiKey, keyInfo hardwarekey.ContextualKeyInfo) error { | ||
| promptMux.Lock() | ||
| defer promptMux.Unlock() | ||
| s.promptMux.Lock() | ||
| defer s.promptMux.Unlock() | ||
|
|
||
| pin, err := s.prompt.AskPIN(ctx, hardwarekey.PINOptional, keyInfo) | ||
| if err != nil { | ||
|
|
@@ -270,8 +285,8 @@ func (s *YubiKeyService) checkOrSetPIN(ctx context.Context, y *YubiKey, keyInfo | |
| } | ||
|
|
||
| func (s *YubiKeyService) promptOverwriteSlot(ctx context.Context, msg string, keyInfo hardwarekey.ContextualKeyInfo) error { | ||
| promptMux.Lock() | ||
| defer promptMux.Unlock() | ||
| s.promptMux.Lock() | ||
| defer s.promptMux.Unlock() | ||
|
|
||
| promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg) | ||
| if confirmed, confirmErr := s.prompt.ConfirmSlotOverwrite(ctx, promptQuestion, keyInfo); confirmErr != nil { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the plan for this TODO?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR would address
tsh,tctl, and Teleport Connect. For arbitrary API clients or tbot we can do the same type of initialization, or continue to provide a global service through a separate method.