Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions api/utils/keys/hardwarekey/cliprompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func NewCLIPrompt(w io.Writer, r prompt.StdinReader) *cliPrompt {

// AskPIN prompts the user for a PIN. If the requirement is [PINOptional],
// the prompt will offer the default PIN as a default value.
func (c *cliPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement) (string, error) {
func (c *cliPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement, _ ContextualKeyInfo) (string, error) {
message := "Enter your YubiKey PIV PIN"
if requirement == PINOptional {
message = "Enter your YubiKey PIV PIN [blank to use default PIN]"
Expand All @@ -68,7 +68,7 @@ func (c *cliPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement
}

// Touch prompts the user to touch the hardware key.
func (c *cliPrompt) Touch(_ context.Context) error {
func (c *cliPrompt) Touch(_ context.Context, _ ContextualKeyInfo) error {
_, err := fmt.Fprintln(c.writer, "Tap your YubiKey")
return trace.Wrap(err)
}
Expand All @@ -77,7 +77,7 @@ func (c *cliPrompt) Touch(_ context.Context) error {
// If the provided PUK is the default value, it will ask for a new PUK as well.
// If an invalid PIN or PUK is provided, the user will be re-prompted until a
// valid value is provided.
func (c *cliPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) {
func (c *cliPrompt) ChangePIN(ctx context.Context, _ ContextualKeyInfo) (*PINAndPUK, error) {
var pinAndPUK = &PINAndPUK{}
for {
fmt.Fprintf(c.writer, "Please set a new 6-8 character PIN.\n")
Expand Down Expand Up @@ -155,7 +155,7 @@ func (c *cliPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) {
}

// ConfirmSlotOverwrite asks the user if the slot's private key and certificate can be overridden.
func (c *cliPrompt) ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) {
func (c *cliPrompt) ConfirmSlotOverwrite(ctx context.Context, message string, _ ContextualKeyInfo) (bool, error) {
confirmation, err := prompt.Confirmation(ctx, c.writer, c.reader, message)
return confirmation, trace.Wrap(err)
}
2 changes: 1 addition & 1 deletion api/utils/keys/hardwarekey/cliprompt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func TestChangePIN(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()

PINAndPUK, err := prompt.ChangePIN(ctx)
PINAndPUK, err := prompt.ChangePIN(ctx, hardwarekey.ContextualKeyInfo{})
require.ErrorIs(t, err, tc.expectError)
require.Equal(t, tc.expectPINAndPUK, PINAndPUK)
})
Expand Down
28 changes: 22 additions & 6 deletions api/utils/keys/hardwarekey/hardwarekey.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type Service interface {
NewPrivateKey(ctx context.Context, config PrivateKeyConfig) (*Signer, error)
// Sign performs a cryptographic signature using the specified hardware
// private key and provided signature parameters.
Sign(ctx context.Context, ref *PrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error)
Sign(ctx context.Context, ref *PrivateKeyRef, keyInfo ContextualKeyInfo, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error)
// TODO(Joerger): DELETE IN v19.0.0
// GetFullKeyRef gets the full [PrivateKeyRef] for an existing hardware private
// key in the given slot of the hardware key with the given serial number.
Expand All @@ -44,13 +44,17 @@ type Service interface {
type Signer struct {
service Service
Ref *PrivateKeyRef
KeyInfo ContextualKeyInfo
}

// NewSigner returns a [Signer] for the given service and ref.
func NewSigner(s Service, ref *PrivateKeyRef) *Signer {
// keyInfo is an optional argument to supply additional contextual info
// used to add additional context to prompts, e.g. ProxyHost.
func NewSigner(s Service, ref *PrivateKeyRef, keyInfo ContextualKeyInfo) *Signer {
return &Signer{
service: s,
Ref: ref,
KeyInfo: keyInfo,
}
}

Expand All @@ -61,7 +65,7 @@ func (h *Signer) Public() crypto.PublicKey {

// Sign implements [crypto.Signer].
func (h *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
return h.service.Sign(context.TODO(), h.Ref, rand, digest, opts)
return h.service.Sign(context.TODO(), h.Ref, h.KeyInfo, rand, digest, opts)
}

// GetAttestation returns the hardware private key attestation details.
Expand All @@ -88,7 +92,7 @@ func (h *Signer) WarmupHardwareKey(ctx context.Context) error {
// We don't actually need to hash the digest, just make it match the hash size.
digest := make([]byte, hash.Size())

_, err := h.service.Sign(ctx, h.Ref, rand.Reader, digest, hash)
_, err := h.service.Sign(ctx, h.Ref, h.KeyInfo, rand.Reader, digest, hash)
return trace.Wrap(err, "failed to perform warmup signature with hardware private key")
}

Expand All @@ -98,13 +102,13 @@ func EncodeSigner(p *Signer) ([]byte, error) {
}

// DecodeSigner decodes an encoded hardware key signer for the given service.
func DecodeSigner(encodedKey []byte, s Service) (*Signer, error) {
func DecodeSigner(encodedKey []byte, s Service, keyInfo ContextualKeyInfo) (*Signer, error) {
ref, err := decodeKeyRef(encodedKey, s)
if err != nil {
return nil, trace.Wrap(err)
}

return NewSigner(s, ref), nil
return NewSigner(s, ref, keyInfo), nil
}

// PrivateKeyRef references a specific hardware private key.
Expand Down Expand Up @@ -230,4 +234,16 @@ type PrivateKeyConfig struct {
// - touch & pin -> 9d
// - touch & !pin -> 9e
CustomSlot PIVSlotKeyString
// ContextualKeyInfo contains additional info to associate with the key.
ContextualKeyInfo ContextualKeyInfo
}

// ContextualKeyInfo contains contextual information associated with a hardware [PrivateKey].
type ContextualKeyInfo struct {
// ProxyHost is the root proxy hostname that the key is associated with.
ProxyHost string
// Username is a Teleport username that the key is associated with.
Username string
// ClusterName is a Teleport cluster name that the key is associated with.
ClusterName string
}
17 changes: 12 additions & 5 deletions api/utils/keys/hardwarekey/hardwarekey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,23 @@ func TestPrivateKey_EncodeDecode(t *testing.T) {

ctx := context.Background()
s := hardwarekey.NewMockHardwareKeyService(nil /*prompt*/)

contextualKeyInfo := hardwarekey.ContextualKeyInfo{
ProxyHost: "billy.io",
Username: "Billy@billy.io",
ClusterName: "billy.io",
}

hwSigner, err := s.NewPrivateKey(ctx, hardwarekey.PrivateKeyConfig{
Policy: hardwarekey.PromptPolicyNone,
Policy: hardwarekey.PromptPolicyNone,
ContextualKeyInfo: contextualKeyInfo,
})
require.NoError(t, err)

priv := hardwarekey.NewSigner(s, hwSigner.Ref)
encoded, err := hardwarekey.EncodeSigner(priv)
encoded, err := hardwarekey.EncodeSigner(hwSigner)
require.NoError(t, err)

decodedSigner, err := hardwarekey.DecodeSigner(encoded, s)
decodedSigner, err := hardwarekey.DecodeSigner(encoded, s, contextualKeyInfo)
require.NoError(t, err)
require.Equal(t, hwSigner, decodedSigner)
}
Expand All @@ -74,7 +81,7 @@ func TestPrivateKey_DecodePartialKeyRef(t *testing.T) {
})
require.NoError(t, err)

decodedSigner, err := hardwarekey.DecodeSigner(partialKeyRefJSON, s)
decodedSigner, err := hardwarekey.DecodeSigner(partialKeyRefJSON, s, hardwarekey.ContextualKeyInfo{})
require.NoError(t, err)
require.Equal(t, hwSigner, decodedSigner)
}
Expand Down
8 changes: 4 additions & 4 deletions api/utils/keys/hardwarekey/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ type PromptPolicy struct {
type Prompt interface {
// AskPIN prompts the user for a PIN.
// The requirement tells if the PIN is required or optional.
AskPIN(ctx context.Context, requirement PINPromptRequirement) (string, error)
AskPIN(ctx context.Context, requirement PINPromptRequirement, keyInfo ContextualKeyInfo) (string, error)
// Touch prompts the user to touch the hardware key.
Touch(ctx context.Context) error
Touch(ctx context.Context, keyInfo ContextualKeyInfo) 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)
ChangePIN(ctx context.Context, keyInfo ContextualKeyInfo) (*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)
ConfirmSlotOverwrite(ctx context.Context, message string, keyInfo ContextualKeyInfo) (bool, error)
}

// PINPromptRequirement specifies whether a PIN is required.
Expand Down
16 changes: 8 additions & 8 deletions api/utils/keys/hardwarekey/service_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ func (s *MockHardwareKeyService) NewPrivateKey(ctx context.Context, config Priva
}

if priv, ok := s.fakeHardwarePrivateKeys[keySlot]; ok {
return NewSigner(s, priv.ref), nil
return NewSigner(s, priv.ref, config.ContextualKeyInfo), nil
}

// generating a new key with PIN/touch requirements requires the corresponding prompt.
if err := s.tryPrompt(ctx, config.Policy); err != nil {
if err := s.tryPrompt(ctx, config.Policy, config.ContextualKeyInfo); err != nil {
return nil, trace.Wrap(err)
}

Expand Down Expand Up @@ -115,12 +115,12 @@ func (s *MockHardwareKeyService) NewPrivateKey(ctx context.Context, config Priva
ref: ref,
}

return NewSigner(s, ref), nil
return NewSigner(s, ref, config.ContextualKeyInfo), nil
}

// Sign performs a cryptographic signature using the specified hardware
// private key and provided signature parameters.
func (s *MockHardwareKeyService) Sign(ctx context.Context, ref *PrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
func (s *MockHardwareKeyService) Sign(ctx context.Context, ref *PrivateKeyRef, keyInfo ContextualKeyInfo, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
s.fakeHardwarePrivateKeysMux.Lock()
defer s.fakeHardwarePrivateKeysMux.Unlock()

Expand All @@ -132,14 +132,14 @@ func (s *MockHardwareKeyService) Sign(ctx context.Context, ref *PrivateKeyRef, r
return nil, trace.NotFound("key not found in slot %d", ref.SlotKey)
}

if err := s.tryPrompt(ctx, ref.Policy); err != nil {
if err := s.tryPrompt(ctx, ref.Policy, keyInfo); err != nil {
return nil, trace.Wrap(err)
}

return priv.Sign(rand, digest, opts)
}

func (s *MockHardwareKeyService) tryPrompt(ctx context.Context, policy PromptPolicy) error {
func (s *MockHardwareKeyService) tryPrompt(ctx context.Context, policy PromptPolicy, keyInfo ContextualKeyInfo) error {
s.promptMu.Lock()
defer s.promptMu.Unlock()

Expand All @@ -150,14 +150,14 @@ func (s *MockHardwareKeyService) tryPrompt(ctx context.Context, policy PromptPol
if policy.PINRequired {
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
if _, err := s.prompt.AskPIN(ctx, PINRequired); err != nil {
if _, err := s.prompt.AskPIN(ctx, PINRequired, keyInfo); err != nil {
return trace.Wrap(err, "failed to handle pin prompt")
}
// We don't actually check the PIN for the current tests, any input is sufficient to unblock the prompt.
}

if policy.TouchRequired {
if err := s.prompt.Touch(ctx); err != nil {
if err := s.prompt.Touch(ctx, keyInfo); err != nil {
return trace.Wrap(err)
}
select {
Expand Down
26 changes: 13 additions & 13 deletions api/utils/keys/piv/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P

// If PIN is required, check that PIN and PUK are not the defaults.
if config.Policy.PINRequired {
if err := s.checkOrSetPIN(ctx, y); err != nil {
if err := s.checkOrSetPIN(ctx, y, config.ContextualKeyInfo); err != nil {
return nil, trace.Wrap(err)
}
}
Expand All @@ -111,7 +111,7 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P
if err != nil {
return nil, trace.Wrap(err)
}
return hardwarekey.NewSigner(s, ref), nil
return hardwarekey.NewSigner(s, ref, config.ContextualKeyInfo), nil
}

// If a custom slot was not specified, check for a key in the
Expand All @@ -127,7 +127,7 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P
// Unknown cert found, this slot could be in use by a non-teleport client.
// Prompt the user before we overwrite the slot.
case len(cert.Subject.Organization) == 0 || cert.Subject.Organization[0] != certOrgName:
if err := s.promptOverwriteSlot(ctx, nonTeleportCertificateMessage(pivSlot, cert)); err != nil {
if err := s.promptOverwriteSlot(ctx, nonTeleportCertificateMessage(pivSlot, cert), config.ContextualKeyInfo); err != nil {
return nil, trace.Wrap(err)
}
return generatePrivateKey()
Expand All @@ -146,25 +146,25 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P

case config.Policy.TouchRequired && !keyRef.Policy.TouchRequired:
msg := fmt.Sprintf("private key in YubiKey PIV slot %q does not require touch.", pivSlot)
if err := s.promptOverwriteSlot(ctx, msg); err != nil {
if err := s.promptOverwriteSlot(ctx, msg, config.ContextualKeyInfo); err != nil {
return nil, trace.Wrap(err)
}
return generatePrivateKey()

case config.Policy.PINRequired && !keyRef.Policy.PINRequired:
msg := fmt.Sprintf("private key in YubiKey PIV slot %q does not require PIN", pivSlot)
if err := s.promptOverwriteSlot(ctx, msg); err != nil {
if err := s.promptOverwriteSlot(ctx, msg, config.ContextualKeyInfo); err != nil {
return nil, trace.Wrap(err)
}
return generatePrivateKey()
}

return hardwarekey.NewSigner(s, keyRef), nil
return hardwarekey.NewSigner(s, keyRef, config.ContextualKeyInfo), nil
}

// Sign performs a cryptographic signature using the specified hardware
// private key and provided signature parameters.
func (s *YubiKeyService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
func (s *YubiKeyService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, keyInfo hardwarekey.ContextualKeyInfo, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
y, err := s.getYubiKey(ref.SerialNumber)
if err != nil {
return nil, trace.Wrap(err)
Expand All @@ -173,7 +173,7 @@ func (s *YubiKeyService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRe
promptMux.Lock()
defer promptMux.Unlock()

return y.sign(ctx, ref, s.prompt, rand, digest, opts)
return y.sign(ctx, ref, keyInfo, s.prompt, rand, digest, opts)
}

// TODO(Joerger): Re-attesting the key every time we decode a hardware key signer is very resource
Expand Down Expand Up @@ -246,11 +246,11 @@ func (s *YubiKeyService) getYubiKey(serialNumber uint32) (*YubiKey, error) {
// 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) error {
func (s *YubiKeyService) checkOrSetPIN(ctx context.Context, y *YubiKey, keyInfo hardwarekey.ContextualKeyInfo) error {
promptMux.Lock()
defer promptMux.Unlock()

pin, err := s.prompt.AskPIN(ctx, hardwarekey.PINOptional)
pin, err := s.prompt.AskPIN(ctx, hardwarekey.PINOptional, keyInfo)
if err != nil {
return trace.Wrap(err)
}
Expand All @@ -260,7 +260,7 @@ func (s *YubiKeyService) checkOrSetPIN(ctx context.Context, y *YubiKey) error {
fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN)
fallthrough
case "":
pin, err = y.setPINAndPUKFromDefault(ctx, s.prompt)
pin, err = y.setPINAndPUKFromDefault(ctx, s.prompt, keyInfo)
if err != nil {
return trace.Wrap(err)
}
Expand All @@ -269,12 +269,12 @@ func (s *YubiKeyService) checkOrSetPIN(ctx context.Context, y *YubiKey) error {
return trace.Wrap(y.verifyPIN(pin))
}

func (s *YubiKeyService) promptOverwriteSlot(ctx context.Context, msg string) error {
func (s *YubiKeyService) promptOverwriteSlot(ctx context.Context, msg string, keyInfo hardwarekey.ContextualKeyInfo) error {
promptMux.Lock()
defer 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); confirmErr != nil {
if confirmed, confirmErr := s.prompt.ConfirmSlotOverwrite(ctx, promptQuestion, keyInfo); confirmErr != nil {
return trace.Wrap(confirmErr)
} else if !confirmed {
return trace.Wrap(trace.CompareFailed(msg), "user declined to overwrite slot")
Expand Down
2 changes: 1 addition & 1 deletion api/utils/keys/piv/service_unavailable.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (s *unavailableYubiKeyPIVService) NewPrivateKey(_ context.Context, _ hardwa

// Sign performs a cryptographic signature using the specified hardware
// private key and provided signature parameters.
func (s *unavailableYubiKeyPIVService) Sign(_ context.Context, _ *hardwarekey.PrivateKeyRef, _ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) {
func (s *unavailableYubiKeyPIVService) Sign(_ context.Context, _ *hardwarekey.PrivateKeyRef, _ hardwarekey.ContextualKeyInfo, _ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) {
return nil, trace.Wrap(errPIVUnavailable)
}

Expand Down
10 changes: 5 additions & 5 deletions api/utils/keys/piv/yubikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ const (
signTouchPromptDelay = time.Millisecond * 200
)

func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, prompt hardwarekey.Prompt, rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, keyInfo hardwarekey.ContextualKeyInfo, prompt hardwarekey.Prompt, rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)

Expand All @@ -163,7 +163,7 @@ func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, prom
select {
case <-touchPromptDelayTimer.C:
// Prompt for touch after a delay, in case the function succeeds without touch due to a cached touch.
err := prompt.Touch(ctx)
err := prompt.Touch(ctx, keyInfo)
if err != nil {
// Cancel the entire function when an error occurs.
// This is typically used for aborting the prompt.
Expand All @@ -185,7 +185,7 @@ func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, prom
defer touchPromptDelayTimer.Reset(signTouchPromptDelay)
}
}
pass, err := prompt.AskPIN(ctx, hardwarekey.PINRequired)
pass, err := prompt.AskPIN(ctx, hardwarekey.PINRequired, keyInfo)
return pass, trace.Wrap(err)
}

Expand Down Expand Up @@ -399,8 +399,8 @@ func (y *YubiKey) SetPIN(oldPin, newPin string) error {
return trace.Wrap(err)
}

func (y *YubiKey) setPINAndPUKFromDefault(ctx context.Context, prompt hardwarekey.Prompt) (string, error) {
pinAndPUK, err := prompt.ChangePIN(ctx)
func (y *YubiKey) setPINAndPUKFromDefault(ctx context.Context, prompt hardwarekey.Prompt, keyInfo hardwarekey.ContextualKeyInfo) (string, error) {
pinAndPUK, err := prompt.ChangePIN(ctx, keyInfo)
if err != nil {
return "", trace.Wrap(err)
}
Expand Down
Loading
Loading