feat: Hardware Key Agent - Propagate contextual key info from key store to hardware key prompts (#53703)

* Propagate contextual key info from key store through to hardware key prompts.

* Remove HardwareKeyPromptConstructor.

* Cleanup; add tests.
This commit is contained in:
Brian Joerger
2025-04-11 16:55:04 +00:00
committed by GitHub
parent 8b1943bcbf
commit fe3193a4b1
23 changed files with 153 additions and 140 deletions
+4 -4
View File
@@ -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]"
@@ -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)
}
@@ -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")
@@ -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)
}
+1 -1
View File
@@ -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)
})
+22 -6
View File
@@ -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.
@@ -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,
}
}
@@ -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.
@@ -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")
}
@@ -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.
@@ -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
}
+12 -5
View File
@@ -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)
}
@@ -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)
}
+4 -4
View File
@@ -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.
+8 -8
View File
@@ -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)
}
@@ -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()
@@ -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()
@@ -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 {
+13 -13
View File
@@ -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)
}
}
@@ -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
@@ -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()
@@ -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)
@@ -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
@@ -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)
}
@@ -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)
}
@@ -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")
+1 -1
View File
@@ -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)
}
+5 -5
View File
@@ -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)
@@ -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.
@@ -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)
}
@@ -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)
}
+10 -1
View File
@@ -253,6 +253,8 @@ func LoadPrivateKey(keyFile string) (*PrivateKey, error) {
type ParsePrivateKeyOptions struct {
// HardwareKeyService is the hardware key service to use with parsed hardware private keys.
HardwareKeyService hardwarekey.Service
// ContextualKeyInfo is contextual information associated with the key.
ContextualKeyInfo hardwarekey.ContextualKeyInfo
}
// ParsePrivateKeyOpt applies configuration options.
@@ -265,6 +267,13 @@ func WithHardwareKeyService(hwKeyService hardwarekey.Service) ParsePrivateKeyOpt
}
}
// WithContextualKeyInfo adds contextual key info to the parsed private key.
func WithContextualKeyInfo(info hardwarekey.ContextualKeyInfo) ParsePrivateKeyOpt {
return func(o *ParsePrivateKeyOptions) {
o.ContextualKeyInfo = info
}
}
// ParsePrivateKey returns the PrivateKey for the given key PEM block.
// Allows passing a custom hardware key prompt.
func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, error) {
@@ -296,7 +305,7 @@ func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, er
hwks = piv.NewYubiKeyService(nil /*prompt*/)
}
hwSigner, err := hardwarekey.DecodeSigner(block.Bytes, hwks)
hwSigner, err := hardwarekey.DecodeSigner(block.Bytes, hwks, appliedOpts.ContextualKeyInfo)
if err != nil {
return nil, trace.Wrap(err, "failed to parse hardware key signer")
}
+9 -2
View File
@@ -49,8 +49,15 @@ func TestMarshalAndParseKey(t *testing.T) {
_, edKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
contextualKeyInfo := hardwarekey.ContextualKeyInfo{
ProxyHost: "billy.io",
Username: "Billy@billy.io",
ClusterName: "billy.io",
}
s := hardwarekey.NewMockHardwareKeyService(nil /*prompt*/)
hwPriv, err := s.NewPrivateKey(context.TODO(), hardwarekey.PrivateKeyConfig{})
hwPriv, err := s.NewPrivateKey(context.TODO(), hardwarekey.PrivateKeyConfig{
ContextualKeyInfo: contextualKeyInfo,
})
require.NoError(t, err)
for keyType, key := range map[string]crypto.Signer{
@@ -62,7 +69,7 @@ func TestMarshalAndParseKey(t *testing.T) {
t.Run(keyType, func(t *testing.T) {
keyPEM, err := keys.MarshalPrivateKey(key)
require.NoError(t, err)
gotKey, err := keys.ParsePrivateKey(keyPEM, keys.WithHardwareKeyService(s))
gotKey, err := keys.ParsePrivateKey(keyPEM, keys.WithHardwareKeyService(s), keys.WithContextualKeyInfo(contextualKeyInfo))
require.NoError(t, err)
assert.Empty(t, cmp.Diff(key, gotKey.Signer, cmpopts.IgnoreUnexported(hardwarekey.Signer{})), "parsed private key is not equal to the original")
-7
View File
@@ -40,7 +40,6 @@ 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/hardwarekey"
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"
@@ -248,9 +247,6 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer
// db cert has expired.
Clock: fakeClock,
WebauthnLogin: webauthnLogin,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -882,9 +878,6 @@ func testTeletermAppGatewayTargetPortValidation(t *testing.T, pack *appaccess.Pa
storage, err := clusters.NewStorage(clusters.Config{
Dir: tc.KeysDir,
InsecureSkipVerify: tc.InsecureSkipVerify,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
daemonService, err := daemon.New(daemon.Config{
-31
View File
@@ -43,7 +43,6 @@ 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/hardwarekey"
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"
@@ -258,9 +257,6 @@ 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) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -293,9 +289,6 @@ 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) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -378,9 +371,6 @@ func testGetClusterReturnsPropertiesFromAuthServer(t *testing.T, pack *dbhelpers
storage, err := clusters.NewStorage(clusters.Config{
Dir: tc.KeysDir,
InsecureSkipVerify: tc.InsecureSkipVerify,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -433,9 +423,6 @@ 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) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -504,9 +491,6 @@ func testClientCache(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers.
Dir: tc.KeysDir,
Clock: storageFakeClock,
InsecureSkipVerify: tc.InsecureSkipVerify,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -766,9 +750,6 @@ func testCreateConnectMyComputerRole(t *testing.T, pack *dbhelpers.DatabasePack)
storage, err := clusters.NewStorage(clusters.Config{
Dir: t.TempDir(),
InsecureSkipVerify: true,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -885,9 +866,6 @@ func testCreateConnectMyComputerToken(t *testing.T, pack *dbhelpers.DatabasePack
InsecureSkipVerify: tc.InsecureSkipVerify,
Clock: fakeClock,
WebauthnLogin: webauthnLogin,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -948,9 +926,6 @@ 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) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -1035,9 +1010,6 @@ 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) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -1265,9 +1237,6 @@ 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) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
+5
View File
@@ -4007,6 +4007,11 @@ func (tc *TeleportClient) GetNewLoginKeyRing(ctx context.Context) (keyRing *KeyR
priv, err := tc.ClientStore.NewHardwarePrivateKey(ctx, hardwarekey.PrivateKeyConfig{
Policy: tc.PrivateKeyPolicy.GetPromptPolicy(),
CustomSlot: tc.PIVSlot,
ContextualKeyInfo: hardwarekey.ContextualKeyInfo{
ProxyHost: tc.WebProxyHost(),
Username: tc.Username,
ClusterName: tc.SiteName,
},
})
if err != nil {
return nil, trace.Wrap(err)
+20 -1
View File
@@ -192,7 +192,10 @@ func TestClientStore(t *testing.T) {
// create a test software and hardware key.
idx := KeyRingIndex{"test.proxy.com", "test-user", "root"}
softKeyRing := a.makeSignedKeyRing(t, idx, false)
hwPriv, err := keys.NewHardwarePrivateKey(ctx, hwks, hardwarekey.PrivateKeyConfig{})
keyInfo := idx.contextualKeyInfo()
hwPriv, err := keys.NewHardwarePrivateKey(ctx, hwks, hardwarekey.PrivateKeyConfig{
ContextualKeyInfo: keyInfo,
})
require.NoError(t, err)
hardKeyRing := NewKeyRing(hwPriv, hwPriv)
hardKeyRing.KeyRingIndex = idx
@@ -230,6 +233,22 @@ func TestClientStore(t *testing.T) {
require.NoError(t, err)
assertEqualKeyRings(t, keyRing, retrievedKeyRing)
// Get the key, now without cluster name. It should retrieve the key without certs
// and without a cluster name in the KeyRingIndex or ContextualKeyInfo (hardware keys).
retrievedKeyRing, err = clientStore.GetKeyRing(KeyRingIndex{idx.ProxyHost, idx.Username, ""})
require.NoError(t, err)
expectKeyRing = keyRing.Copy()
expectKeyRing.ClusterName = ""
expectKeyRing.Cert = nil
expectKeyRing.DBTLSCredentials = make(map[string]TLSCredential)
if hwPriv, ok := expectKeyRing.TLSPrivateKey.Signer.(*hardwarekey.Signer); ok {
hwPriv.KeyInfo.ClusterName = ""
}
if hwPriv, ok := expectKeyRing.SSHPrivateKey.Signer.(*hardwarekey.Signer); ok {
hwPriv.KeyInfo.ClusterName = ""
}
assertEqualKeyRings(t, expectKeyRing, retrievedKeyRing)
var profileDir string
if fs, ok := clientStore.KeyStore.(*FSKeyStore); ok {
profileDir = fs.KeyDir
+9
View File
@@ -40,6 +40,7 @@ import (
"github.com/gravitational/teleport/api/constants"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/api/utils/keys/hardwarekey"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/cryptosuites"
@@ -90,6 +91,14 @@ func (idx KeyRingIndex) LogValue() slog.Value {
)
}
func (idx KeyRingIndex) contextualKeyInfo() hardwarekey.ContextualKeyInfo {
return hardwarekey.ContextualKeyInfo{
ProxyHost: idx.ProxyHost,
Username: idx.Username,
ClusterName: idx.ClusterName,
}
}
// TLSCredential holds a signed TLS certificate and matching private key.
type TLSCredential struct {
// PrivateKey is the private key of the credential.
+3 -3
View File
@@ -540,7 +540,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks hardwarekey.Service, opt
return nil, trace.Wrap(err, "no session keys for %+v", idx)
}
tlsCred, err := readTLSCredential(fs.userTLSKeyPath(idx), fs.tlsCertPath(idx), keys.WithHardwareKeyService(hwks))
tlsCred, err := readTLSCredential(fs.userTLSKeyPath(idx), fs.tlsCertPath(idx), keys.WithHardwareKeyService(hwks), keys.WithContextualKeyInfo(idx.contextualKeyInfo()))
if err != nil {
if trace.IsNotFound(err) {
if _, statErr := os.Stat(fs.tlsCertPathLegacy(idx)); statErr == nil {
@@ -551,7 +551,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks hardwarekey.Service, opt
return nil, trace.Wrap(err)
}
sshPriv, err := keys.LoadKeyPair(fs.userSSHKeyPath(idx), fs.publicKeyPath(idx), keys.WithHardwareKeyService(hwks))
sshPriv, err := keys.LoadKeyPair(fs.userSSHKeyPath(idx), fs.publicKeyPath(idx), keys.WithHardwareKeyService(hwks), keys.WithContextualKeyInfo(idx.contextualKeyInfo()))
if err != nil {
return nil, trace.ConvertSystemError(err)
}
@@ -574,7 +574,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks hardwarekey.Service, opt
}
func (fs *FSKeyStore) updateKeyRingWithCerts(o CertOption, hwks hardwarekey.Service, keyRing *KeyRing) error {
return o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, keys.WithHardwareKeyService(hwks))
return o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, keys.WithHardwareKeyService(hwks), keys.WithContextualKeyInfo(keyRing.contextualKeyInfo()))
}
// GetSSHCertificates gets all certificates signed for the given user and proxy.
+11 -2
View File
@@ -61,7 +61,9 @@ func TestKeyStore(t *testing.T) {
// create a test software and hardware key.
idx := KeyRingIndex{"test.proxy.com", "test-user", "root"}
softKeyRing := s.makeSignedKeyRing(t, idx, false)
hwPriv, err := keys.NewHardwarePrivateKey(ctx, hwks, hardwarekey.PrivateKeyConfig{})
hwPriv, err := keys.NewHardwarePrivateKey(ctx, hwks, hardwarekey.PrivateKeyConfig{
ContextualKeyInfo: idx.contextualKeyInfo(),
})
require.NoError(t, err)
hardKeyRing := NewKeyRing(hwPriv, hwPriv)
hardKeyRing.KeyRingIndex = idx
@@ -98,11 +100,18 @@ func TestKeyStore(t *testing.T) {
expectKeyRing.DBTLSCredentials = make(map[string]TLSCredential)
assertEqualKeyRings(t, expectKeyRing, retrievedKeyRing)
// check for the key, now without cluster name
// Get the key, now without cluster name. It should retrieve the key without certs
// and without a cluster name in the KeyRingIndex or ContextualKeyInfo (hardware keys).
retrievedKeyRing, err = keyStore.GetKeyRing(KeyRingIndex{idx.ProxyHost, idx.Username, ""}, hwks)
require.NoError(t, err)
expectKeyRing.ClusterName = ""
expectKeyRing.Cert = nil
if hwPriv, ok := expectKeyRing.TLSPrivateKey.Signer.(*hardwarekey.Signer); ok {
hwPriv.KeyInfo.ClusterName = ""
}
if hwPriv, ok := expectKeyRing.SSHPrivateKey.Signer.(*hardwarekey.Signer); ok {
hwPriv.KeyInfo.ClusterName = ""
}
assertEqualKeyRings(t, expectKeyRing, retrievedKeyRing)
// delete the key
+1 -6
View File
@@ -27,7 +27,6 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/utils/keys/hardwarekey"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/teleterm/api/uri"
)
// Config is the cluster service config
@@ -47,7 +46,7 @@ type Config struct {
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) hardwarekey.Prompt
CustomHardwareKeyPrompt hardwarekey.Prompt
}
// CheckAndSetDefaults checks the configuration for its validity and sets default values if needed
@@ -56,10 +55,6 @@ 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()
}
+1 -5
View File
@@ -286,11 +286,7 @@ func (s *Storage) makeDefaultClientConfig(rootClusterURI uri.ResourceURI) *clien
cfg.InsecureSkipVerify = s.InsecureSkipVerify
cfg.AddKeysToAgent = s.AddKeysToAgent
cfg.WebauthnLogin = s.WebauthnLogin
// TODO(Joerger): Remove the rootClusterURI dependency from the teleterm prompt so that
// the storage service can share a single hardware key service+prompt. This allows the
// process to properly share PIV connections, prevents duplicate prompts, and enables
// PIN caching across clusters (if both clusters allow PIN caching).
cfg.CustomHardwareKeyPrompt = s.HardwareKeyPromptConstructor(rootClusterURI)
cfg.CustomHardwareKeyPrompt = s.CustomHardwareKeyPrompt
cfg.DTAuthnRunCeremony = dtauthn.NewCeremony().Run
cfg.DTAutoEnroll = dtenroll.AutoEnroll
return cfg
-13
View File
@@ -40,7 +40,6 @@ import (
"google.golang.org/grpc/status"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/keys/hardwarekey"
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"
@@ -348,9 +347,6 @@ func TestUpdateTshdEventsServerAddress(t *testing.T) {
storage, err := clusters.NewStorage(clusters.Config{
Dir: homeDir,
InsecureSkipVerify: true,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -385,9 +381,6 @@ func TestUpdateTshdEventsServerAddress_CredsErr(t *testing.T) {
storage, err := clusters.NewStorage(clusters.Config{
Dir: homeDir,
InsecureSkipVerify: true,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -489,9 +482,6 @@ func TestRetryWithRelogin(t *testing.T) {
storage, err := clusters.NewStorage(clusters.Config{
Dir: t.TempDir(),
InsecureSkipVerify: true,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
@@ -545,9 +535,6 @@ func TestConcurrentHeadlessAuthPrompts(t *testing.T) {
storage, err := clusters.NewStorage(clusters.Config{
Dir: t.TempDir(),
InsecureSkipVerify: true,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return nil
},
})
require.NoError(t, err)
+12 -15
View File
@@ -25,11 +25,9 @@ import (
"github.com/gravitational/teleport/api/utils/keys/hardwarekey"
api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1"
"github.com/gravitational/teleport/lib/teleterm/api/uri"
)
// NewHardwareKeyPromptConstructor returns a new hardware key prompt constructor
// for this service and the given root cluster URI.
// NewHardwareKeyPrompt returns a new hardware key prompt.
//
// TODO(gzdunek): Improve multi-cluster and multi-hardware keys support.
// The code in yubikey.go doesn't really support using multiple hardware keys (like one per cluster):
@@ -46,19 +44,18 @@ import (
// Because the code in yubikey.go assumes you use a single key, we don't have any mutex here.
// (unlike other modals triggered by tshd).
// We don't expect receiving prompts from different hardware keys.
func (s *Service) NewHardwareKeyPromptConstructor(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return &hardwareKeyPrompter{s: s, rootClusterURI: rootClusterURI}
func (s *Service) NewHardwareKeyPrompt() hardwarekey.Prompt {
return &hardwareKeyPrompter{s: s}
}
type hardwareKeyPrompter struct {
s *Service
rootClusterURI uri.ResourceURI
s *Service
}
// Touch prompts the user to touch the hardware key.
func (h *hardwareKeyPrompter) Touch(ctx context.Context) error {
func (h *hardwareKeyPrompter) Touch(ctx context.Context, keyInfo hardwarekey.ContextualKeyInfo) error {
_, err := h.s.tshdEventsClient.PromptHardwareKeyTouch(ctx, &api.PromptHardwareKeyTouchRequest{
RootClusterUri: h.rootClusterURI.String(),
RootClusterUri: keyInfo.ProxyHost,
})
if err != nil {
return trace.Wrap(err)
@@ -67,9 +64,9 @@ func (h *hardwareKeyPrompter) Touch(ctx context.Context) error {
}
// AskPIN prompts the user for a PIN.
func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement hardwarekey.PINPromptRequirement) (string, error) {
func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement hardwarekey.PINPromptRequirement, keyInfo hardwarekey.ContextualKeyInfo) (string, error) {
res, err := h.s.tshdEventsClient.PromptHardwareKeyPIN(ctx, &api.PromptHardwareKeyPINRequest{
RootClusterUri: h.rootClusterURI.String(),
RootClusterUri: keyInfo.ProxyHost,
PinOptional: requirement == hardwarekey.PINOptional,
})
if err != nil {
@@ -81,9 +78,9 @@ func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement hardwareke
// ChangePIN asks for a new PIN.
// The Electron app prompt must handle default values for PIN and PUK,
// preventing the user from submitting empty/default values.
func (h *hardwareKeyPrompter) ChangePIN(ctx context.Context) (*hardwarekey.PINAndPUK, error) {
func (h *hardwareKeyPrompter) ChangePIN(ctx context.Context, keyInfo hardwarekey.ContextualKeyInfo) (*hardwarekey.PINAndPUK, error) {
res, err := h.s.tshdEventsClient.PromptHardwareKeyPINChange(ctx, &api.PromptHardwareKeyPINChangeRequest{
RootClusterUri: h.rootClusterURI.String(),
RootClusterUri: keyInfo.ProxyHost,
})
if err != nil {
return nil, trace.Wrap(err)
@@ -96,9 +93,9 @@ func (h *hardwareKeyPrompter) ChangePIN(ctx context.Context) (*hardwarekey.PINAn
}
// ConfirmSlotOverwrite asks the user if the slot's private key and certificate can be overridden.
func (h *hardwareKeyPrompter) ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) {
func (h *hardwareKeyPrompter) ConfirmSlotOverwrite(ctx context.Context, message string, keyInfo hardwarekey.ContextualKeyInfo) (bool, error) {
res, err := h.s.tshdEventsClient.ConfirmHardwareKeySlotOverwrite(ctx, &api.ConfirmHardwareKeySlotOverwriteRequest{
RootClusterUri: h.rootClusterURI.String(),
RootClusterUri: keyInfo.ProxyHost,
Message: message,
})
if err != nil {
+2 -7
View File
@@ -32,8 +32,6 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/gravitational/teleport/api/utils/keys/hardwarekey"
"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"
@@ -42,7 +40,6 @@ import (
// Serve starts daemon service
func Serve(ctx context.Context, cfg Config) error {
var hardwareKeyPromptConstructor func(clusterURI uri.ResourceURI) hardwarekey.Prompt
if err := cfg.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
@@ -59,9 +56,6 @@ func Serve(ctx context.Context, cfg Config) error {
Clock: clock,
InsecureSkipVerify: cfg.InsecureSkipVerify,
AddKeysToAgent: cfg.AddKeysToAgent,
HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt {
return hardwareKeyPromptConstructor(rootClusterURI)
},
})
if err != nil {
return trace.Wrap(err)
@@ -83,7 +77,8 @@ func Serve(ctx context.Context, cfg Config) error {
// TODO(gzdunek): Move tshdEventsClient out of daemonService so that we can
// construct the prompt before creating Storage.
hardwareKeyPromptConstructor = daemonService.NewHardwareKeyPromptConstructor
storage.CustomHardwareKeyPrompt = daemonService.NewHardwareKeyPrompt()
apiServer, err := apiserver.New(apiserver.Config{
HostAddr: cfg.Addr,
InsecureSkipVerify: cfg.InsecureSkipVerify,