mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
feat: Hardware Key Agent validates known keys (#54691)
* * Have hardware key agent validate known keys instead of always validating by the PIV slot cert. * Refactor cert check logic with a custom error. * Add test. * Update rfd. * Fix lint. * Address comments. * Require knownKeyFn to be provided. * Require fallbackService to be provided to agent service. * Address comments.
This commit is contained in:
@@ -69,8 +69,8 @@ func (c *cliPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement
|
||||
|
||||
// If this is a hardware key agent request with command context info,
|
||||
// include the command in the prompt.
|
||||
if keyInfo.Command != "" {
|
||||
msg = fmt.Sprintf("%v to continue with command %q", msg, keyInfo.Command)
|
||||
if keyInfo.AgentKeyInfo.Command != "" {
|
||||
msg = fmt.Sprintf("%v to continue with command %q", msg, keyInfo.AgentKeyInfo.Command)
|
||||
}
|
||||
|
||||
pin, err := prompt.Password(ctx, c.writer, c.reader, msg)
|
||||
@@ -88,8 +88,8 @@ func (c *cliPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement
|
||||
// Touch prompts the user to touch the hardware key.
|
||||
func (c *cliPrompt) Touch(_ context.Context, keyInfo ContextualKeyInfo) error {
|
||||
msg := "Tap your YubiKey"
|
||||
if keyInfo.Command != "" {
|
||||
msg = fmt.Sprintf("%v to continue with command %q", msg, keyInfo.Command)
|
||||
if keyInfo.AgentKeyInfo.Command != "" {
|
||||
msg = fmt.Sprintf("%v to continue with command %q", msg, keyInfo.AgentKeyInfo.Command)
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintln(c.writer, msg)
|
||||
|
||||
@@ -254,13 +254,21 @@ type ContextualKeyInfo struct {
|
||||
Username string
|
||||
// ClusterName is a Teleport cluster name that the key is associated with.
|
||||
ClusterName string
|
||||
// AgentKey specifies whether this key is being utilized through an agent.
|
||||
// The hardware key service may impose additional restrictions in this case,
|
||||
// such as checking that the PIV slot certificate matches the Teleport client
|
||||
// metadata certificate format, to ensure the agent doesn't provide access to
|
||||
// non teleport client PIV keys.
|
||||
AgentKey bool
|
||||
// Command is the running command utilizing this key.
|
||||
// AgentKeyInfo contains info associated with an hardware key agent signature request.
|
||||
AgentKeyInfo AgentKeyInfo
|
||||
}
|
||||
|
||||
// AgentKeyInfo contains info associated with an hardware key agent signature request.
|
||||
type AgentKeyInfo struct {
|
||||
// UnknownAgentKey indicates whether this hardware private key is known to the hardware key agent
|
||||
// process, usually based on whether a matching key is found in the process's client key store.
|
||||
//
|
||||
// For unknown agent keys, the hardware key service will check that the certificate in the same
|
||||
// slot as the key matches a Teleport client metadata certificate in order to ensure the agent
|
||||
// doesn't provide access to non teleport client PIV keys.
|
||||
UnknownAgentKey bool
|
||||
// Command is the command reported by the agent client which this agent key is being utilized to
|
||||
// complete, e.g. `tsh ssh server01`.
|
||||
Command string
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ type MockHardwareKeyService struct {
|
||||
|
||||
fakeHardwarePrivateKeys map[hardwareKeySlot]*fakeHardwarePrivateKey
|
||||
fakeHardwarePrivateKeysMux sync.Mutex
|
||||
|
||||
// mock a PIV slot with a key but no teleport metadata cert.
|
||||
unknownAgentKey map[hardwareKeySlot]bool
|
||||
}
|
||||
|
||||
// NewMockHardwareKeyService returns a [mockHardwareKeyService] for use in tests.
|
||||
@@ -60,6 +63,7 @@ func NewMockHardwareKeyService(prompt Prompt) *MockHardwareKeyService {
|
||||
prompt: prompt,
|
||||
mockTouch: make(chan struct{}),
|
||||
fakeHardwarePrivateKeys: map[hardwareKeySlot]*fakeHardwarePrivateKey{},
|
||||
unknownAgentKey: map[hardwareKeySlot]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,10 +145,16 @@ func (s *MockHardwareKeyService) Sign(ctx context.Context, ref *PrivateKeyRef, k
|
||||
s.fakeHardwarePrivateKeysMux.Lock()
|
||||
defer s.fakeHardwarePrivateKeysMux.Unlock()
|
||||
|
||||
priv, ok := s.fakeHardwarePrivateKeys[hardwareKeySlot{
|
||||
slot := hardwareKeySlot{
|
||||
serialNumber: serialNumber,
|
||||
slot: ref.SlotKey,
|
||||
}]
|
||||
}
|
||||
|
||||
if keyInfo.AgentKeyInfo.UnknownAgentKey && s.unknownAgentKey[slot] {
|
||||
return nil, trace.BadParameter("unknown agent key")
|
||||
}
|
||||
|
||||
priv, ok := s.fakeHardwarePrivateKeys[slot]
|
||||
if !ok {
|
||||
return nil, trace.NotFound("key not found in slot 0x%x", ref.SlotKey)
|
||||
}
|
||||
@@ -193,6 +203,13 @@ func (s *MockHardwareKeyService) SetPrompt(prompt Prompt) {
|
||||
s.prompt = prompt
|
||||
}
|
||||
|
||||
func (s *MockHardwareKeyService) AddUnknownAgentKey(ref *PrivateKeyRef) {
|
||||
s.unknownAgentKey[hardwareKeySlot{
|
||||
serialNumber: ref.SerialNumber,
|
||||
slot: ref.SlotKey,
|
||||
}] = true
|
||||
}
|
||||
|
||||
// TODO(Joerger): DELETE IN v19.0.0
|
||||
func (s *MockHardwareKeyService) GetFullKeyRef(serialNumber uint32, slotKey PIVSlotKey) (*PrivateKeyRef, error) {
|
||||
s.fakeHardwarePrivateKeysMux.Lock()
|
||||
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
)
|
||||
|
||||
// NewClient creates a new hardware key agent client.
|
||||
func NewClient(ctx context.Context, socketPath string, creds credentials.TransportCredentials) (hardwarekeyagentv1.HardwareKeyAgentServiceClient, error) {
|
||||
func NewClient(socketPath string, creds credentials.TransportCredentials) (hardwarekeyagentv1.HardwareKeyAgentServiceClient, error) {
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -61,19 +61,38 @@ func NewClient(ctx context.Context, socketPath string, creds credentials.Transpo
|
||||
}
|
||||
|
||||
// NewServer returns a new hardware key agent server.
|
||||
func NewServer(ctx context.Context, s hardwarekey.Service, creds credentials.TransportCredentials) *grpc.Server {
|
||||
func NewServer(s hardwarekey.Service, creds credentials.TransportCredentials, knownKeyFn KnownHardwareKeyFn) (*grpc.Server, error) {
|
||||
if knownKeyFn == nil {
|
||||
return nil, trace.BadParameter("knownKeyFn must be provided")
|
||||
}
|
||||
|
||||
grpcServer := grpc.NewServer(
|
||||
grpc.Creds(creds),
|
||||
grpc.UnaryInterceptor(interceptors.GRPCServerUnaryErrorInterceptor),
|
||||
)
|
||||
hardwarekeyagentv1.RegisterHardwareKeyAgentServiceServer(grpcServer, &agentService{s: s})
|
||||
return grpcServer
|
||||
hardwarekeyagentv1.RegisterHardwareKeyAgentServiceServer(grpcServer, &agentService{s: s, knownKeyFn: knownKeyFn})
|
||||
return grpcServer, nil
|
||||
}
|
||||
|
||||
// KnownHardwareKeyFn is a function to determine if the hardware private key, described by the given
|
||||
// key ref and key info, is known by this process. This is usually based on whether a matching key
|
||||
// is found in the process's client key store.
|
||||
type KnownHardwareKeyFn func(ref *hardwarekey.PrivateKeyRef, keyInfo hardwarekey.ContextualKeyInfo) (bool, error)
|
||||
|
||||
// agentService implements [hardwarekeyagentv1.HardwareKeyAgentServiceServer].
|
||||
type agentService struct {
|
||||
hardwarekeyagentv1.UnimplementedHardwareKeyAgentServiceServer
|
||||
s hardwarekey.Service
|
||||
|
||||
// knownKeyFn is a function to determine if the hardware private key, described by the given
|
||||
// key ref and key info, is known by this process. This is usually based on whether a matching key
|
||||
// is found in the process's client key store.
|
||||
//
|
||||
// Unknown keys will treated with additional restrictions in [agentService.Sign] requests to
|
||||
// ensure the PIV slot is intended for Teleport client usage, e.g. the agent will require that
|
||||
// the PIV slot has a self-signed metadata certificate used to identify PIV keys generated
|
||||
// specifically for Teleport use.
|
||||
knownKeyFn KnownHardwareKeyFn
|
||||
}
|
||||
|
||||
// Sign the given digest with the specified hardware private key.
|
||||
@@ -108,8 +127,16 @@ func (s *agentService) Sign(ctx context.Context, req *hardwarekeyagentv1.SignReq
|
||||
ProxyHost: req.KeyInfo.ProxyHost,
|
||||
Username: req.KeyInfo.Username,
|
||||
ClusterName: req.KeyInfo.ClusterName,
|
||||
AgentKey: true,
|
||||
Command: req.Command,
|
||||
}
|
||||
|
||||
knownKey, err := s.knownKeyFn(keyRef, keyInfo)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
keyInfo.AgentKeyInfo = hardwarekey.AgentKeyInfo{
|
||||
UnknownAgentKey: !knownKey,
|
||||
Command: req.Command,
|
||||
}
|
||||
|
||||
var signerOpts crypto.SignerOpts
|
||||
|
||||
@@ -44,9 +44,10 @@ type Service struct {
|
||||
}
|
||||
|
||||
// NewService creates a new hardware key agent service from the given
|
||||
// agent client and fallback service. The fallback service is used for
|
||||
// non-signature methods of [hardwarekey.Service] which are not implemented
|
||||
// by the agent. Generally this fallback service is only used during login.
|
||||
// agent client and fallback service.
|
||||
//
|
||||
// The fallback service is used for methods unsupported by the agent service,
|
||||
// such as [Service.NewPrivateKey], and as a fallback for failed agent signatures.
|
||||
func NewService(agentClient hardwarekeyagentv1.HardwareKeyAgentServiceClient, fallbackService hardwarekey.Service) *Service {
|
||||
return &Service{
|
||||
agentClient: agentClient,
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"crypto/rsa"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -35,9 +36,18 @@ import (
|
||||
func TestHardwareKeyAgentService(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Mock known keys. Usually the server's login session storage would be used to check for known keys.
|
||||
var serverKnownKeySlots []hardwarekey.PIVSlotKey
|
||||
knownKeyFn := func(ref *hardwarekey.PrivateKeyRef, _ hardwarekey.ContextualKeyInfo) (bool, error) {
|
||||
return slices.ContainsFunc(serverKnownKeySlots, func(s hardwarekey.PIVSlotKey) bool {
|
||||
return ref.SlotKey == s
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Prepare the agent server
|
||||
mockService := hardwarekey.NewMockHardwareKeyService(nil /*prompt*/)
|
||||
server := hardwarekeyagent.NewServer(ctx, mockService, insecure.NewCredentials())
|
||||
server, err := hardwarekeyagent.NewServer(mockService, insecure.NewCredentials(), knownKeyFn)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(server.Stop)
|
||||
|
||||
agentDir := t.TempDir()
|
||||
@@ -51,10 +61,11 @@ func TestHardwareKeyAgentService(t *testing.T) {
|
||||
}()
|
||||
|
||||
// Prepare the agent client
|
||||
agentClient, err := hardwarekeyagent.NewClient(ctx, socketPath, insecure.NewCredentials())
|
||||
agentClient, err := hardwarekeyagent.NewClient(socketPath, insecure.NewCredentials())
|
||||
require.NoError(t, err)
|
||||
|
||||
agentService := hardwarekeyagent.NewService(agentClient, hardwarekey.NewMockHardwareKeyService(nil /*prompt*/))
|
||||
unusedService := hardwarekey.NewMockHardwareKeyService(nil /*prompt*/)
|
||||
agentServiceNoFallback := hardwarekeyagent.NewService(agentClient, unusedService)
|
||||
agentServiceWithFallback := hardwarekeyagent.NewService(agentClient, mockService)
|
||||
|
||||
for _, tc := range []struct {
|
||||
@@ -189,7 +200,7 @@ func TestHardwareKeyAgentService(t *testing.T) {
|
||||
}
|
||||
|
||||
// Perform a signature over the agent.
|
||||
_, err = agentService.Sign(ctx, hwSigner.Ref, hwSigner.KeyInfo, rand.Reader, digest, tc.opts)
|
||||
_, err = agentServiceNoFallback.Sign(ctx, hwSigner.Ref, hwSigner.KeyInfo, rand.Reader, digest, tc.opts)
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
@@ -198,6 +209,23 @@ func TestHardwareKeyAgentService(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("unknown agent key", func(t *testing.T) {
|
||||
mockService.Reset()
|
||||
|
||||
hwSigner, err := mockService.NewPrivateKey(ctx, hardwarekey.PrivateKeyConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Mark the hardware key as unknown by the Hardware Key Service.
|
||||
mockService.AddUnknownAgentKey(hwSigner.Ref)
|
||||
_, err = agentServiceNoFallback.Sign(ctx, hwSigner.Ref, hwSigner.KeyInfo, rand.Reader, []byte{}, crypto.Hash(0))
|
||||
require.Error(t, err)
|
||||
|
||||
// Make the hardware key as known by the Hardware Key Agent Server.
|
||||
serverKnownKeySlots = append(serverKnownKeySlots, hwSigner.Ref.SlotKey)
|
||||
_, err = agentServiceNoFallback.Sign(ctx, hwSigner.Ref, hwSigner.KeyInfo, rand.Reader, []byte{}, crypto.Hash(0))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("fallback", func(t *testing.T) {
|
||||
mockService.Reset()
|
||||
|
||||
|
||||
@@ -135,20 +135,20 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P
|
||||
// If a custom slot was not specified, check for a key in the
|
||||
// default slot for the given policy and generate a new one if needed.
|
||||
if config.CustomSlot == "" {
|
||||
switch cert, err := y.getCertificate(pivSlot); {
|
||||
case errors.Is(err, piv.ErrNotFound):
|
||||
switch err := y.checkCertificate(pivSlot); {
|
||||
case trace.IsNotFound(err):
|
||||
return generatePrivateKey()
|
||||
|
||||
// Unknown cert found, this slot could be in use by a non-teleport client.
|
||||
// Prompt the user before we overwrite the slot.
|
||||
case errors.As(err, &nonTeleportCertError{}):
|
||||
if err := s.promptOverwriteSlot(ctx, err.Error(), config.ContextualKeyInfo); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return generatePrivateKey()
|
||||
|
||||
case err != nil:
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
// Unknown cert found, this slot could be in use by a non-teleport client.
|
||||
// Prompt the user before we overwrite the slot.
|
||||
case !isTeleportMetadataCertificate(cert):
|
||||
if err := s.promptOverwriteSlot(ctx, nonTeleportCertificateMessage(pivSlot, cert), config.ContextualKeyInfo); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return generatePrivateKey()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,16 +147,6 @@ const (
|
||||
signTouchPromptDelay = time.Millisecond * 200
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingTeleportCert = trace.BadParameterError{
|
||||
Message: "hardware key agent cannot perform signatures on PIV slots that aren't configured for Teleport. " +
|
||||
"The PIV slot should be configured automatically by the Teleport client during login. If you are " +
|
||||
"are configuring the PIV slot manually, you must also generate a certificate in the slot with " +
|
||||
"\"teleport\" as the organization name: " +
|
||||
"e.g. \"ykman piv keys generate -a ECCP256 9a pub.pem && ykman piv certificate generate 9a pub.pem -s O=teleport\"",
|
||||
}
|
||||
)
|
||||
|
||||
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) {
|
||||
pivSlot, err := parsePIVSlot(ref.SlotKey)
|
||||
if err != nil {
|
||||
@@ -177,19 +167,14 @@ func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, keyI
|
||||
return nil, trace.CompareFailed("public key mismatch on PIV slot 0x%x", pivSlot.Key)
|
||||
}
|
||||
|
||||
// If this sign request is coming from the hardware key agent, ensure that the requested PIV
|
||||
// slot was configured by a Teleport client, or manually configured by the user / hardware key
|
||||
// administrator. Manual configuration is used in cases where the default PIV management key
|
||||
// is not used, e.g. when the hardware key is managed by a third party provider by an admin.
|
||||
if keyInfo.AgentKey {
|
||||
cert, err := y.getCertificate(pivSlot)
|
||||
switch {
|
||||
case errors.Is(err, piv.ErrNotFound):
|
||||
return nil, trace.Wrap(&ErrMissingTeleportCert, "certificate not found in PIV slot 0x%x", pivSlot.Key)
|
||||
// If the sign request is for an unknown agent key, ensure that the requested PIV slot was
|
||||
// configured with a self-signed Teleport metadata certificate.
|
||||
if keyInfo.AgentKeyInfo.UnknownAgentKey {
|
||||
switch err := y.checkCertificate(pivSlot); {
|
||||
case trace.IsNotFound(err), errors.As(err, &nonTeleportCertError{}):
|
||||
return nil, trace.Wrap(err, agentRequiresTeleportCertMessage)
|
||||
case err != nil:
|
||||
return nil, trace.Wrap(err)
|
||||
case !isTeleportMetadataCertificate(cert):
|
||||
return nil, trace.Wrap(&ErrMissingTeleportCert, nonTeleportCertificateMessage(pivSlot, cert))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,10 +389,22 @@ func (y *YubiKey) SetMetadataCertificate(slot piv.Slot, subject pkix.Name) error
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// getCertificate gets a certificate from the given PIV slot.
|
||||
func (y *YubiKey) getCertificate(slot piv.Slot) (*x509.Certificate, error) {
|
||||
// checkCertificate checks for a certificate on the PIV slot matching a Teleport client
|
||||
// metadata certificate. Expected errors include [trace.NotFoundError] and [nonTeleportCertError].
|
||||
func (y *YubiKey) checkCertificate(slot piv.Slot) error {
|
||||
cert, err := y.conn.certificate(slot)
|
||||
return cert, trace.Wrap(err)
|
||||
switch {
|
||||
case errors.Is(err, piv.ErrNotFound):
|
||||
return trace.NotFound("certificate not found in PIV slot 0x%x", slot.Key)
|
||||
case err != nil:
|
||||
return trace.Wrap(err)
|
||||
case !isTeleportMetadataCertificate(cert):
|
||||
return nonTeleportCertError{
|
||||
slot: slot,
|
||||
cert: cert,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// attestKey attests the key in the given PIV slot.
|
||||
@@ -810,9 +807,14 @@ func isTeleportMetadataCertificate(cert *x509.Certificate) bool {
|
||||
return len(cert.Subject.Organization) > 0 && cert.Subject.Organization[0] == certOrgName
|
||||
}
|
||||
|
||||
func nonTeleportCertificateMessage(slot piv.Slot, cert *x509.Certificate) string {
|
||||
type nonTeleportCertError struct {
|
||||
slot piv.Slot
|
||||
cert *x509.Certificate
|
||||
}
|
||||
|
||||
func (e nonTeleportCertError) Error() string {
|
||||
// Gather a small list of user-readable x509 certificate fields to display to the user.
|
||||
sum := sha256.Sum256(cert.Raw)
|
||||
sum := sha256.Sum256(e.cert.Raw)
|
||||
fingerPrint := hex.EncodeToString(sum[:])
|
||||
return fmt.Sprintf(`Certificate in YubiKey PIV slot %q is not a Teleport client cert:
|
||||
Slot %s:
|
||||
@@ -824,13 +826,19 @@ Slot %s:
|
||||
Not before: %v
|
||||
Not after: %v
|
||||
`,
|
||||
slot, slot,
|
||||
cert.SignatureAlgorithm,
|
||||
cert.Subject,
|
||||
cert.Issuer,
|
||||
cert.SerialNumber,
|
||||
e.slot, e.slot,
|
||||
e.cert.SignatureAlgorithm,
|
||||
e.cert.Subject,
|
||||
e.cert.Issuer,
|
||||
e.cert.SerialNumber,
|
||||
fingerPrint,
|
||||
cert.NotBefore,
|
||||
cert.NotAfter,
|
||||
e.cert.NotBefore,
|
||||
e.cert.NotAfter,
|
||||
)
|
||||
}
|
||||
|
||||
const agentRequiresTeleportCertMessage = "hardware key agent cannot perform signatures on PIV slots that aren't configured for Teleport. " +
|
||||
"The PIV slot should be configured automatically by the Teleport client during login. If you are " +
|
||||
"are configuring the PIV slot manually, you must also generate a certificate in the slot with " +
|
||||
"\"teleport\" as the organization name: " +
|
||||
"e.g. \"ykman piv keys generate -a ECCP256 9a pub.pem && ykman piv certificate generate 9a pub.pem -s O=teleport\""
|
||||
|
||||
@@ -118,6 +118,35 @@ func (s *Store) NewHardwarePrivateKey(ctx context.Context, config hardwarekey.Pr
|
||||
return keys.NewHardwarePrivateKey(ctx, s.HardwareKeyService, config)
|
||||
}
|
||||
|
||||
// KnownHardwareKey returns whether the given hardware key ref and info corresponds to a hardware key known
|
||||
// to this client store.
|
||||
func (s *Store) KnownHardwareKey(ref *hardwarekey.PrivateKeyRef, keyInfo hardwarekey.ContextualKeyInfo) (bool, error) {
|
||||
keyRing, err := s.GetKeyRing(KeyRingIndex{
|
||||
ProxyHost: keyInfo.ProxyHost,
|
||||
Username: keyInfo.Username,
|
||||
ClusterName: keyInfo.ClusterName,
|
||||
})
|
||||
if trace.IsNotFound(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// There is a known key matching the key info from the agent client, now check
|
||||
// if it is the same key, with the same hardware key reference.
|
||||
hwSigner, ok := keyRing.TLSPrivateKey.Signer.(*hardwarekey.Signer)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// We only need to compare the serial number and slot key. Other values, like the
|
||||
// public key and prompt policy, will be validated against the hardware key directly
|
||||
// when needed.
|
||||
sameKeyRef := hwSigner.Ref.SerialNumber == ref.SerialNumber && hwSigner.Ref.SlotKey == ref.SlotKey
|
||||
|
||||
return sameKeyRef, nil
|
||||
}
|
||||
|
||||
// AddKeyRing adds the given key ring to the key store. The key's trusted certificates are
|
||||
// added to the trusted certs store.
|
||||
func (s *Store) AddKeyRing(keyRing *KeyRing) error {
|
||||
|
||||
@@ -61,7 +61,7 @@ func NewAgentClient(ctx context.Context, keyAgentDir string) (hardwarekeyagentv1
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hardwarekeyagent.NewClient(ctx, socketPath, creds)
|
||||
return hardwarekeyagent.NewClient(socketPath, creds)
|
||||
}
|
||||
|
||||
// Server implementation [hardwarekeyagentv1.HardwareKeyAgentServiceServer].
|
||||
@@ -75,7 +75,11 @@ type Server struct {
|
||||
// The given directory will be created when the server is served and destroyed with the server is stopped.
|
||||
//
|
||||
// [DefaultAgentDir] should be used for [keyAgentDir] outside of tests.
|
||||
func NewAgentServer(ctx context.Context, s hardwarekey.Service, keyAgentDir string) (*Server, error) {
|
||||
func NewAgentServer(ctx context.Context, s hardwarekey.Service, keyAgentDir string, knownKeyFn hardwarekeyagent.KnownHardwareKeyFn) (*Server, error) {
|
||||
if knownKeyFn == nil {
|
||||
return nil, trace.BadParameter("knownKeyFn must be provided")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(keyAgentDir, 0o700); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -91,7 +95,12 @@ func NewAgentServer(ctx context.Context, s hardwarekey.Service, keyAgentDir stri
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
grpcServer := hardwarekeyagent.NewServer(ctx, s, credentials.NewServerTLSFromCert(&cert))
|
||||
grpcServer, err := hardwarekeyagent.NewServer(s, credentials.NewServerTLSFromCert(&cert), knownKeyFn)
|
||||
if err != nil {
|
||||
l.Close()
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &Server{
|
||||
grpcServer: grpcServer,
|
||||
listener: l,
|
||||
|
||||
@@ -34,9 +34,15 @@ func TestHardwareKeyAgent_Server(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
agentDir := t.TempDir()
|
||||
|
||||
// Prepare the agent server
|
||||
mockService := hardwarekey.NewMockHardwareKeyService(nil /*prompt*/)
|
||||
server, err := libhwk.NewAgentServer(ctx, mockService, agentDir)
|
||||
|
||||
// treat all keys as unknown (agent) keys.
|
||||
knownKeyFn := func(_ *hardwarekey.PrivateKeyRef, _ hardwarekey.ContextualKeyInfo) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Prepare the agent server
|
||||
server, err := libhwk.NewAgentServer(ctx, mockService, agentDir, knownKeyFn)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(server.Stop)
|
||||
|
||||
@@ -46,7 +52,7 @@ func TestHardwareKeyAgent_Server(t *testing.T) {
|
||||
}()
|
||||
|
||||
// Should fail to open a new server in the same directory.
|
||||
_, err = libhwk.NewAgentServer(ctx, mockService, agentDir)
|
||||
_, err = libhwk.NewAgentServer(ctx, mockService, agentDir, knownKeyFn)
|
||||
require.Error(t, err)
|
||||
|
||||
// Existing server should be unaffected.
|
||||
@@ -61,7 +67,7 @@ func TestHardwareKeyAgent_Server(t *testing.T) {
|
||||
_, err := os.Stat(agentDir)
|
||||
return errors.Is(err, os.ErrNotExist)
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
server, err = libhwk.NewAgentServer(ctx, mockService, agentDir)
|
||||
server, err = libhwk.NewAgentServer(ctx, mockService, agentDir, knownKeyFn)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(server.Stop)
|
||||
|
||||
@@ -69,7 +75,7 @@ func TestHardwareKeyAgent_Server(t *testing.T) {
|
||||
// Use a timeoutCtx so that the failed Ping request fails quickly.
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
|
||||
defer cancel()
|
||||
server, err = libhwk.NewAgentServer(timeoutCtx, mockService, agentDir)
|
||||
server, err = libhwk.NewAgentServer(timeoutCtx, mockService, agentDir, knownKeyFn)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(server.Stop)
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ type hardwareKeyPrompter struct {
|
||||
// Touch prompts the user to touch the hardware key.
|
||||
func (h *hardwareKeyPrompter) Touch(ctx context.Context, keyInfo hardwarekey.ContextualKeyInfo) error {
|
||||
// Don't include "tsh daemon" commands.
|
||||
if strings.Contains(keyInfo.Command, "tsh daemon") {
|
||||
keyInfo.Command = ""
|
||||
if strings.Contains(keyInfo.AgentKeyInfo.Command, "tsh daemon") {
|
||||
keyInfo.AgentKeyInfo.Command = ""
|
||||
}
|
||||
|
||||
clt, err := h.c.GetClient(ctx)
|
||||
@@ -67,7 +67,7 @@ func (h *hardwareKeyPrompter) Touch(ctx context.Context, keyInfo hardwarekey.Con
|
||||
|
||||
_, err = clt.PromptHardwareKeyTouch(ctx, &api.PromptHardwareKeyTouchRequest{
|
||||
ProxyHostname: keyInfo.ProxyHost,
|
||||
Command: keyInfo.Command,
|
||||
Command: keyInfo.AgentKeyInfo.Command,
|
||||
})
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
@@ -78,8 +78,8 @@ func (h *hardwareKeyPrompter) Touch(ctx context.Context, keyInfo hardwarekey.Con
|
||||
// AskPIN prompts the user for a PIN.
|
||||
func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement hardwarekey.PINPromptRequirement, keyInfo hardwarekey.ContextualKeyInfo) (string, error) {
|
||||
// Don't include "tsh daemon" commands.
|
||||
if strings.Contains(keyInfo.Command, "tsh daemon") {
|
||||
keyInfo.Command = ""
|
||||
if strings.Contains(keyInfo.AgentKeyInfo.Command, "tsh daemon") {
|
||||
keyInfo.AgentKeyInfo.Command = ""
|
||||
}
|
||||
|
||||
clt, err := h.c.GetClient(ctx)
|
||||
@@ -90,7 +90,7 @@ func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement hardwareke
|
||||
res, err := clt.PromptHardwareKeyPIN(ctx, &api.PromptHardwareKeyPINRequest{
|
||||
ProxyHostname: keyInfo.ProxyHost,
|
||||
PinOptional: requirement == hardwarekey.PINOptional,
|
||||
Command: keyInfo.Command,
|
||||
Command: keyInfo.AgentKeyInfo.Command,
|
||||
})
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
|
||||
@@ -106,7 +106,7 @@ func Serve(ctx context.Context, cfg Config) error {
|
||||
|
||||
var hardwareKeyAgentServer *libhwk.Server
|
||||
if cfg.HardwareKeyAgent {
|
||||
hardwareKeyAgentServer, err = libhwk.NewAgentServer(ctx, hwks, libhwk.DefaultAgentDir())
|
||||
hardwareKeyAgentServer, err = libhwk.NewAgentServer(ctx, hwks, libhwk.DefaultAgentDir(), storage.ClientStore.KnownHardwareKey)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx, "failed to create the hardware key agent server", "err", err)
|
||||
} else {
|
||||
|
||||
@@ -680,7 +680,8 @@ the hardware key agent for non Teleport client keys. e.g PIV keys traditionally
|
||||
used for email encryption.
|
||||
|
||||
The hardware key agent will determine whether a key in a PIV slot is a Teleport
|
||||
client key by checking for the [self-signed metadata certificate](./0080-hardware-key-support.md#piv-slot-logic)
|
||||
client key by checking for a recent login session matching the key or checking
|
||||
for the [self-signed metadata certificate](./0080-hardware-key-support.md#piv-slot-logic)
|
||||
generated by Teleport clients on hardware key login.
|
||||
|
||||
Note: When user hardware keys are externally managed, administrators are currently
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/utils/keys/piv"
|
||||
libhwk "github.com/gravitational/teleport/lib/hardwarekey"
|
||||
)
|
||||
|
||||
@@ -50,8 +49,9 @@ func newPIVAgentCommand(parent *kingpin.CmdClause) *pivAgentCommand {
|
||||
}
|
||||
|
||||
func (c *pivAgentCommand) run(cf *CLIConf) error {
|
||||
hwKeyService := piv.NewYubiKeyService(nil /*prompt*/)
|
||||
s, err := libhwk.NewAgentServer(cf.Context, hwKeyService, libhwk.DefaultAgentDir())
|
||||
cf.disableHardwareKeyAgentClient = true
|
||||
store := cf.getClientStore()
|
||||
s, err := libhwk.NewAgentServer(cf.Context, store.HardwareKeyService, libhwk.DefaultAgentDir(), store.KnownHardwareKey)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user