-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: Hardware Key PIN caching #53976
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
785c3a0
ef0c6de
08024dc
4c0dfef
a599015
d440ae4
b4d3158
29a5b8d
94b2cf2
6e80abc
c7d9e24
c1ebde9
b30342e
a0286bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // Copyright 2025 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 hardwarekey | ||
|
|
||
| import ( | ||
| "context" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/gravitational/trace" | ||
| ) | ||
|
|
||
| // NewPINCachingPrompt returns a new pin caching HardwareKeyPrompt. | ||
| // If [innerPrompt] already is a PIN caching prompt, it will be | ||
| // returned with an updated [cacheTTL]. | ||
| func NewPINCachingPrompt(innerPrompt Prompt, cacheTTL time.Duration) *PINCachingPrompt { | ||
| if p, ok := innerPrompt.(*PINCachingPrompt); ok { | ||
| p.mu.Lock() | ||
| defer p.mu.Unlock() | ||
|
|
||
| p.cacheTTL = cacheTTL | ||
| return p | ||
| } | ||
|
|
||
| return &PINCachingPrompt{ | ||
| Prompt: innerPrompt, | ||
| cacheTTL: cacheTTL, | ||
| } | ||
| } | ||
|
|
||
| // PINCachingPrompt is a [Prompt] wrapped with PIN caching. | ||
| type PINCachingPrompt struct { | ||
| // Prompt is the inner prompt used to prompt the user for touch | ||
| // or PIN when it is not cached or is expired. | ||
| Prompt | ||
|
|
||
| // mu currently protects all fields. | ||
| mu sync.Mutex | ||
|
|
||
| // cacheTTL is the configured duration that a cached PIN will be valid. | ||
| cacheTTL time.Duration | ||
| // pin is the cached PIN. | ||
| pin string | ||
| // pinExpiry is the expiration time of the currently cached PIN. | ||
| pinExpiry time.Time | ||
| } | ||
|
|
||
| // AskPIN returned the cached PIN if it is not expired. Otherwise, it uses | ||
| // the inner prompt to prompt the user for PIN, caching and returning it. | ||
| func (p *PINCachingPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement, keyInfo ContextualKeyInfo) (string, error) { | ||
| p.mu.Lock() | ||
| defer p.mu.Unlock() | ||
|
|
||
| if p.pin != "" && time.Now().Before(p.pinExpiry) { | ||
| return p.pin, nil | ||
| } | ||
|
|
||
| pin, err := p.Prompt.AskPIN(ctx, requirement, keyInfo) | ||
| if err != nil { | ||
| return "", trace.Wrap(err) | ||
| } | ||
|
|
||
| p.pin = pin | ||
| p.pinExpiry = time.Now().Add(p.cacheTTL) | ||
|
|
||
| return pin, nil | ||
| } | ||
|
|
||
| // ChangePIN uses the inner prompt to prompt the user to change their PIN, then it caches the PIN | ||
| func (p *PINCachingPrompt) ChangePIN(ctx context.Context, keyInfo ContextualKeyInfo) (*PINAndPUK, error) { | ||
|
Joerger marked this conversation as resolved.
|
||
| p.mu.Lock() | ||
| defer p.mu.Unlock() | ||
|
|
||
| PINAndPUK, err := p.Prompt.ChangePIN(ctx, keyInfo) | ||
| if err != nil { | ||
| return nil, trace.Wrap(err) | ||
| } | ||
|
|
||
| p.pin = PINAndPUK.PIN | ||
| p.pinExpiry = time.Now().Add(p.cacheTTL) | ||
|
|
||
| return PINAndPUK, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| // Copyright 2025 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 hardwarekey_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "math/rand/v2" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/gravitational/teleport/api/utils/keys/hardwarekey" | ||
| ) | ||
|
|
||
| func TestPINCachingPrompt(t *testing.T) { | ||
| ctx := context.Background() | ||
|
|
||
| // Note: locally this test gets flaky around 10µs. | ||
| cacheTTL := 10 * time.Millisecond | ||
| cachingPrompt := hardwarekey.NewPINCachingPrompt(&randPINPrompt{}, cacheTTL) | ||
|
|
||
| t.Run("AskPIN", func(t *testing.T) { | ||
| // prompt and cache a new PIN for 100ms. | ||
| cachedPIN, err := cachingPrompt.AskPIN(ctx, hardwarekey.PINRequired, hardwarekey.ContextualKeyInfo{}) | ||
| require.NoError(t, err) | ||
| timer := time.NewTimer(cacheTTL) | ||
|
|
||
| // Check that the PIN remains cached. | ||
| for i := 0; i < 3; i++ { | ||
| pin, err := cachingPrompt.AskPIN(ctx, hardwarekey.PINRequired, hardwarekey.ContextualKeyInfo{}) | ||
| require.NoError(t, err) | ||
|
Joerger marked this conversation as resolved.
|
||
| require.Equal(t, cachedPIN, pin) | ||
| } | ||
|
|
||
| // Check that the PIN is not cached after 100ms. | ||
| <-timer.C | ||
| pin, err := cachingPrompt.AskPIN(ctx, hardwarekey.PINRequired, hardwarekey.ContextualKeyInfo{}) | ||
| require.NoError(t, err) | ||
| require.NotEqual(t, cachedPIN, pin) | ||
| }) | ||
|
|
||
| t.Run("ChangePIN", func(t *testing.T) { | ||
| // ChangePIN should prompt and cache a new PIN for 100ms. | ||
| pinAndPUK, err := cachingPrompt.ChangePIN(ctx, hardwarekey.ContextualKeyInfo{}) | ||
| require.NoError(t, err) | ||
| cachedPIN := pinAndPUK.PIN | ||
| timer := time.NewTimer(cacheTTL) | ||
|
|
||
| // Check that the PIN remains cached. | ||
| for i := 0; i < 3; i++ { | ||
| pin, err := cachingPrompt.AskPIN(ctx, hardwarekey.PINRequired, hardwarekey.ContextualKeyInfo{}) | ||
| require.NoError(t, err) | ||
| require.Equal(t, cachedPIN, pin) | ||
| } | ||
|
|
||
| // Check that the PIN is not cached after 100ms. | ||
| <-timer.C | ||
| pin, err := cachingPrompt.AskPIN(ctx, hardwarekey.PINRequired, hardwarekey.ContextualKeyInfo{}) | ||
| require.NoError(t, err) | ||
| require.NotEqual(t, cachedPIN, pin) | ||
| }) | ||
|
|
||
| } | ||
|
|
||
| type randPINPrompt struct{} | ||
|
|
||
| func (p *randPINPrompt) AskPIN(ctx context.Context, requirement hardwarekey.PINPromptRequirement, keyInfo hardwarekey.ContextualKeyInfo) (string, error) { | ||
| return p.randPIN(), nil | ||
| } | ||
|
|
||
| func (p *randPINPrompt) Touch(ctx context.Context, keyInfo hardwarekey.ContextualKeyInfo) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (p *randPINPrompt) ChangePIN(ctx context.Context, keyInfo hardwarekey.ContextualKeyInfo) (*hardwarekey.PINAndPUK, error) { | ||
| return &hardwarekey.PINAndPUK{ | ||
| PIN: p.randPIN(), | ||
| }, nil | ||
| } | ||
|
|
||
| func (p *randPINPrompt) ConfirmSlotOverwrite(ctx context.Context, message string, keyInfo hardwarekey.ContextualKeyInfo) (bool, error) { | ||
| return false, nil | ||
| } | ||
|
|
||
| func (p *randPINPrompt) randPIN() string { | ||
|
Collaborator
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. Isn't
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. You mean instead of If you mean instead of having the
Collaborator
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 seems the |
||
| return fmt.Sprintf("%08d", rand.IntN(100000000)) | ||
| } | ||
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.
@zmb3 @rosstimothy WDYT about this name change? During the RFD phase, "timeout" didn't feel right to me but "ttl" didn't come to mind until now.