From 288713a2f5f736925d4ae08c09e1173e2119b4e1 Mon Sep 17 00:00:00 2001 From: joerger Date: Mon, 10 Mar 2025 12:45:35 -0700 Subject: [PATCH 01/10] * Add HardwareKeyService interface and HardwarePrivateKey implementation * Replace CustomHardwareKeyPrompt in ParsePrivateKeyOpts with HardwareKeyService * Export CLIPrompt --- api/utils/keys/cliprompt.go | 12 ++- api/utils/keys/hardwarekey.go | 140 +++++++++++++++++++++++++++++++ api/utils/keys/hardwaresigner.go | 2 + api/utils/keys/privatekey.go | 64 ++++++++++++-- api/utils/keys/yubikey.go | 4 +- 5 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 api/utils/keys/hardwarekey.go diff --git a/api/utils/keys/cliprompt.go b/api/utils/keys/cliprompt.go index 7dce20d211a7c..e7d8d88a29f11 100644 --- a/api/utils/keys/cliprompt.go +++ b/api/utils/keys/cliprompt.go @@ -1,5 +1,3 @@ -//go:build piv && !pivtest - // Copyright 2024 Gravitational, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,9 +25,9 @@ import ( "github.com/gravitational/teleport/api/utils/prompt" ) -type cliPrompt struct{} +type CLIPrompt struct{} -func (c *cliPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement) (string, error) { +func (c *CLIPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement) (string, error) { message := "Enter your YubiKey PIV PIN" if requirement == PINOptional { message = "Enter your YubiKey PIV PIN [blank to use default PIN]" @@ -38,12 +36,12 @@ func (c *cliPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement return password, trace.Wrap(err) } -func (c *cliPrompt) Touch(_ context.Context) error { +func (c *CLIPrompt) Touch(_ context.Context) error { _, err := fmt.Fprintln(os.Stderr, "Tap your YubiKey") return trace.Wrap(err) } -func (c *cliPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) { +func (c *CLIPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) { var pinAndPUK = &PINAndPUK{} for { fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PIN.\n") @@ -120,7 +118,7 @@ func (c *cliPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) { return pinAndPUK, nil } -func (c *cliPrompt) ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) { +func (c *CLIPrompt) ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) { confirmation, err := prompt.Confirmation(ctx, os.Stderr, prompt.Stdin(), message) return confirmation, trace.Wrap(err) } diff --git a/api/utils/keys/hardwarekey.go b/api/utils/keys/hardwarekey.go new file mode 100644 index 0000000000000..a196a9cd60f14 --- /dev/null +++ b/api/utils/keys/hardwarekey.go @@ -0,0 +1,140 @@ +// 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 keys + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/json" + "io" + + "github.com/gravitational/trace" +) + +// TODO: replace with PIV implementation +func NewHardwareKeyService(prompt HardwareKeyPrompt) HardwareKeyService { + return nil +} + +// HardwareKeyService is an interface for interacting with hardware private keys. +type HardwareKeyService interface { + // NewPrivateKey creates or retrieves a hardware private key from the given PIV slot matching + // the given private key policy and returns the details required to perform signatures with + // that key. + NewPrivateKey(ctx context.Context, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) + // Sign performs a cryptographic signature using the specified hardware + // private key and provided signature parameters. + Sign(ref *HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) +} + +// HardwarePrivateKey is a hardware private key. +type HardwarePrivateKey struct { + service HardwareKeyService + ref *HardwarePrivateKeyRef +} + +// Public implements [crypto.Signer]. +func (h *HardwarePrivateKey) Public() crypto.PublicKey { + return h.ref.PublicKey +} + +// Sign implements [crypto.Signer]. +func (h *HardwarePrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + return h.service.Sign(h.ref, rand, digest, opts) +} + +// GetAttestation returns the hardware private key attestation details. +func (h *HardwarePrivateKey) GetAttestationStatement() *AttestationStatement { + return h.ref.AttestationStatement +} + +// GetPrivateKeyPolicy returns the PrivateKeyPolicy satisfied by this key. +func (h *HardwarePrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy { + return h.ref.PrivateKeyPolicy +} + +// HardwarePrivateKeyRef references a specific hardware private key. +type HardwarePrivateKeyRef struct { + // SerialNumber is the hardware key's serial number. + SerialNumber uint32 `json:"serial_number"` + // SlotKey is the key name for the hardware key PIV slot, e.g. "9a". + SlotKey uint32 `json:"slot_key"` + // PublicKey is the public key paired with the hardware private key. + PublicKey crypto.PublicKey `json:"-"` // uses custom JSON marshaling in PKIX, ASN.1 DER form + // PrivateKeyPolicy is the private key policy satisfied by the hardware private key. + PrivateKeyPolicy PrivateKeyPolicy `json:"private_key_policy"` + // AttestationStatement contains the hardware private key's attestation statement, which is + // to attest the touch and pin requirements for this hardware private key during login. + AttestationStatement *AttestationStatement `json:"attestation_statement"` +} + +// encodeHardwarePrivateKeyRef encodes a [HardwarePrivateKeyRef] to JSON. +func encodeHardwarePrivateKeyRef(ref *HardwarePrivateKeyRef) ([]byte, error) { + keyRefBytes, err := json.Marshal(ref) + if err != nil { + return nil, trace.Wrap(err) + } + return keyRefBytes, nil +} + +// decodeHardwarePrivateKeyRef decodes a [HardwarePrivateKeyRef] from JSON. +func decodeHardwarePrivateKeyRef(encodedKeyRef []byte) (*HardwarePrivateKeyRef, error) { + // TODO: old clients would only have SerialNumber and SlotKey, gather missing information directly for backwards compatibility. + keyRef := &HardwarePrivateKeyRef{} + if err := json.Unmarshal(encodedKeyRef, keyRef); err != nil { + return nil, trace.Wrap(err) + } + return keyRef, nil +} + +// These types are used for custom marshaling of the crypto.PublicKey field in [HardwarePrivateKeyRef]. +type refAlias HardwarePrivateKeyRef +type hardwarePrivateKeyRefJSON struct { + // embedding an alias type instead of [HardwarePrivateKeyRef] prevents the custom marshaling + // from recursively applying, which would result in a stack overflow. + refAlias + PublicKeyDER []byte `json:"public_key"` +} + +// UnmarshalJSON marshals [HardwarePrivateKeyRef] with custom logic for the public key. +func (r HardwarePrivateKeyRef) MarshalJSON() ([]byte, error) { + pubDER, err := x509.MarshalPKIXPublicKey(r.PublicKey) + if err != nil { + return nil, trace.Wrap(err) + } + + return json.Marshal(&hardwarePrivateKeyRefJSON{ + refAlias: refAlias(r), + PublicKeyDER: pubDER, + }) +} + +// UnmarshalJSON unmarshals [HardwarePrivateKeyRef] with custom logic for the public key. +func (r *HardwarePrivateKeyRef) UnmarshalJSON(b []byte) error { + ref := hardwarePrivateKeyRefJSON{} + err := json.Unmarshal(b, &ref) + if err != nil { + return trace.Wrap(err) + } + + ref.refAlias.PublicKey, err = x509.ParsePKIXPublicKey(ref.PublicKeyDER) + if err != nil { + return trace.Wrap(err) + } + + *r = HardwarePrivateKeyRef(ref.refAlias) + return nil +} diff --git a/api/utils/keys/hardwaresigner.go b/api/utils/keys/hardwaresigner.go index d89b3053b9f4a..d1bbc73ab5d18 100644 --- a/api/utils/keys/hardwaresigner.go +++ b/api/utils/keys/hardwaresigner.go @@ -25,6 +25,8 @@ import ( // HardwareSigner is a crypto.Signer which can be attested as being backed by a hardware key. // This enables the ability to enforce hardware key private key policies. +// +// TODO: Delete in favor of HardwarePrivateKey type HardwareSigner interface { crypto.Signer diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index 1255acb6f7df2..a0873cb682d48 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -19,6 +19,7 @@ package keys import ( "bytes" + "context" "crypto" "crypto/ecdsa" "crypto/ed25519" @@ -91,6 +92,38 @@ func NewSoftwarePrivateKey(signer crypto.Signer) (*PrivateKey, error) { }, nil } +// NewHardwarePrivateKey creates or retrieves a hardware private key from from the given hardware key +// service that matches the given PIV slot and private key policy, returning the hardware private key +// as a [PrivateKey]. +func NewHardwarePrivateKey(ctx context.Context, s HardwareKeyService, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*PrivateKey, error) { + if s == nil { + return nil, trace.BadParameter("cannot create a new hardware private key without a hardware key service provided") + } + + keyRef, err := s.NewPrivateKey(ctx, customSlot, requiredPolicy) + if err != nil { + return nil, trace.Wrap(err) + } + + hwPrivateKey := &HardwarePrivateKey{ + service: s, + ref: keyRef, + } + + encodedKeyRef, err := encodeHardwarePrivateKeyRef(keyRef) + if err != nil { + return nil, trace.Wrap(err) + } + + keyRefPEM := pem.EncodeToMemory(&pem.Block{ + Type: pivYubiKeyPrivateKeyType, + Headers: nil, + Bytes: encodedKeyRef, + }) + + return NewPrivateKey(hwPrivateKey, keyRefPEM) +} + // SSHPublicKey returns the ssh.PublicKey representation of the public key. func (k *PrivateKey) SSHPublicKey() ssh.PublicKey { return k.sshPub @@ -211,6 +244,7 @@ type ParsePrivateKeyOptions struct { // for a hardware key PIN, touch, etc. // If empty, a default CLI prompt is used. CustomHardwareKeyPrompt HardwareKeyPrompt + HardwareKeyService HardwareKeyService } // ParsePrivateKeyOpt applies configuration options. @@ -223,6 +257,13 @@ func WithCustomPrompt(prompt HardwareKeyPrompt) ParsePrivateKeyOpt { } } +// WithHardwareKeyService sets the hardware key service. +func WithHardwareKeyService(hwKeyService HardwareKeyService) ParsePrivateKeyOpt { + return func(o *ParsePrivateKeyOptions) { + o.HardwareKeyService = hwKeyService + } +} + // 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) { @@ -238,8 +279,19 @@ func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, er switch block.Type { case pivYubiKeyPrivateKeyType: - priv, err := parseYubiKeyPrivateKeyData(block.Bytes, appliedOpts.CustomHardwareKeyPrompt) - return priv, trace.Wrap(err, "parsing YubiKey private key") + if appliedOpts.HardwareKeyService == nil { + return nil, trace.BadParameter("cannot parse hardware private key without an initialized hardware key service") + } + + keyRef, err := decodeHardwarePrivateKeyRef(block.Bytes) + if err != nil { + return nil, trace.Wrap(err, "failed to parse hardware private key") + } + + return NewPrivateKey(&HardwarePrivateKey{ + service: appliedOpts.HardwareKeyService, + ref: keyRef, + }, keyPEM) case OpenSSHPrivateKeyType: priv, err := ssh.ParseRawPrivateKey(keyPEM) if err != nil { @@ -316,7 +368,7 @@ func MarshalPrivateKey(key crypto.Signer) ([]byte, error) { } // LoadKeyPair returns the PrivateKey for the given private and public key files. -func LoadKeyPair(privFile, sshPubFile string, customPrompt HardwareKeyPrompt) (*PrivateKey, error) { +func LoadKeyPair(privFile, sshPubFile string, opts ...ParsePrivateKeyOpt) (*PrivateKey, error) { privPEM, err := os.ReadFile(privFile) if err != nil { return nil, trace.ConvertSystemError(err) @@ -327,7 +379,7 @@ func LoadKeyPair(privFile, sshPubFile string, customPrompt HardwareKeyPrompt) (* return nil, trace.ConvertSystemError(err) } - priv, err := ParseKeyPair(privPEM, marshaledSSHPub, customPrompt) + priv, err := ParseKeyPair(privPEM, marshaledSSHPub, opts...) if err != nil { return nil, trace.Wrap(err) } @@ -335,8 +387,8 @@ func LoadKeyPair(privFile, sshPubFile string, customPrompt HardwareKeyPrompt) (* } // ParseKeyPair returns the PrivateKey for the given private and public key PEM blocks. -func ParseKeyPair(privPEM, marshaledSSHPub []byte, customPrompt HardwareKeyPrompt) (*PrivateKey, error) { - priv, err := ParsePrivateKey(privPEM, WithCustomPrompt(customPrompt)) +func ParseKeyPair(privPEM, marshaledSSHPub []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, error) { + priv, err := ParsePrivateKey(privPEM, opts...) if err != nil { return nil, trace.Wrap(err) } diff --git a/api/utils/keys/yubikey.go b/api/utils/keys/yubikey.go index 1c1fe1aea5a82..90eab8a20e2ad 100644 --- a/api/utils/keys/yubikey.go +++ b/api/utils/keys/yubikey.go @@ -70,7 +70,7 @@ var ( // or previously generated by a Teleport client and reused. func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy PrivateKeyPolicy, slot PIVSlot, prompt HardwareKeyPrompt) (*PrivateKey, error) { if prompt == nil { - prompt = &cliPrompt{} + prompt = &CLIPrompt{} } cachedKeysMu.Lock() defer cachedKeysMu.Unlock() @@ -246,7 +246,7 @@ type yubiKeyPrivateKeyData struct { func parseYubiKeyPrivateKeyData(keyDataBytes []byte, prompt HardwareKeyPrompt) (*PrivateKey, error) { if prompt == nil { - prompt = &cliPrompt{} + prompt = &CLIPrompt{} } cachedKeysMu.Lock() defer cachedKeysMu.Unlock() From 390c84462ea7236d5949236a0a28fba9c1883756 Mon Sep 17 00:00:00 2001 From: joerger Date: Mon, 10 Mar 2025 16:25:28 -0700 Subject: [PATCH 02/10] Propogate hardware key service through client store; Remove custom hardware key prompt in favor in favor of hardware key service with prompt field. --- api/utils/keys/hardwarekey.go | 4 ++ api/utils/keys/policy.go | 12 ++-- api/utils/keys/privatekey.go | 13 +--- api/utils/keys/yubikey.go | 2 +- integration/proxy/teleterm_test.go | 12 ++-- integration/teleterm_test.go | 40 +++--------- lib/client/api.go | 17 ++--- lib/client/api_test.go | 8 +-- lib/client/client_store.go | 21 ++++--- lib/client/client_store_test.go | 8 +-- lib/client/cluster_client_test.go | 4 +- lib/client/identityfile/identity.go | 4 +- lib/client/identityfile/identity_test.go | 2 +- lib/client/keyagent_test.go | 12 ++-- lib/client/keystore.go | 66 +++++++------------- lib/client/keystore_test.go | 26 ++++---- lib/client/profile.go | 7 ++- lib/teleterm/clusters/config.go | 10 ++- lib/teleterm/clusters/storage.go | 3 +- lib/teleterm/daemon/daemon_test.go | 16 ++--- lib/teleterm/daemon/hardwarekeyprompt.go | 7 +-- lib/teleterm/teleterm.go | 17 ++--- lib/vnet/profile_osconfig_provider_darwin.go | 5 +- tool/tctl/common/config/profile.go | 7 ++- tool/tsh/common/db_test.go | 2 +- tool/tsh/common/tsh.go | 10 +-- tool/tsh/common/tsh_test.go | 2 +- tool/tsh/common/vnet_client_application.go | 5 +- 28 files changed, 146 insertions(+), 196 deletions(-) diff --git a/api/utils/keys/hardwarekey.go b/api/utils/keys/hardwarekey.go index a196a9cd60f14..d88d6c23b8c5d 100644 --- a/api/utils/keys/hardwarekey.go +++ b/api/utils/keys/hardwarekey.go @@ -38,6 +38,10 @@ type HardwareKeyService interface { // Sign performs a cryptographic signature using the specified hardware // private key and provided signature parameters. Sign(ref *HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) + // SetPrompt sets the hardware key prompt used by the hardware key service, if applicable. + // This is used by Teleport Connect which sets the prompt later than the hardware key service, + // due to process initialization constraints. + SetPrompt(prompt HardwareKeyPrompt) } // HardwarePrivateKey is a hardware private key. diff --git a/api/utils/keys/policy.go b/api/utils/keys/policy.go index 60ab361559261..f9dc57e5dbb64 100644 --- a/api/utils/keys/policy.go +++ b/api/utils/keys/policy.go @@ -66,17 +66,17 @@ func (requiredPolicy PrivateKeyPolicy) IsSatisfiedBy(keyPolicy PrivateKeyPolicy) case PrivateKeyPolicyHardwareKey: return keyPolicy.IsHardwareKeyPolicy() case PrivateKeyPolicyHardwareKeyTouch: - return keyPolicy.isHardwareKeyTouchVerified() + return keyPolicy.IsHardwareKeyTouchVerified() case PrivateKeyPolicyHardwareKeyPIN: - return keyPolicy.isHardwareKeyPINVerified() + return keyPolicy.IsHardwareKeyPINVerified() case PrivateKeyPolicyHardwareKeyTouchAndPIN: - return keyPolicy.isHardwareKeyTouchVerified() && keyPolicy.isHardwareKeyPINVerified() + return keyPolicy.IsHardwareKeyTouchVerified() && keyPolicy.IsHardwareKeyPINVerified() } return false } -func (p PrivateKeyPolicy) isHardwareKeyTouchVerified() bool { +func (p PrivateKeyPolicy) IsHardwareKeyTouchVerified() bool { switch p { case PrivateKeyPolicyHardwareKeyTouch, PrivateKeyPolicyHardwareKeyTouchAndPIN: return true @@ -84,7 +84,7 @@ func (p PrivateKeyPolicy) isHardwareKeyTouchVerified() bool { return false } -func (p PrivateKeyPolicy) isHardwareKeyPINVerified() bool { +func (p PrivateKeyPolicy) IsHardwareKeyPINVerified() bool { switch p { case PrivateKeyPolicyHardwareKeyPIN, PrivateKeyPolicyHardwareKeyTouchAndPIN: return true @@ -111,7 +111,7 @@ func (p PrivateKeyPolicy) IsHardwareKeyPolicy() bool { // of the connection, during the TLS/SSH handshake. For long term connections, MFA should // be re-verified through other methods (e.g. webauthn). func (p PrivateKeyPolicy) MFAVerified() bool { - return p.isHardwareKeyTouchVerified() || p.isHardwareKeyPINVerified() + return p.IsHardwareKeyTouchVerified() || p.IsHardwareKeyPINVerified() } func (p PrivateKeyPolicy) validate() error { diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index a0873cb682d48..dc703df077f5f 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -240,23 +240,12 @@ func LoadPrivateKey(keyFile string) (*PrivateKey, error) { // ParsePrivateKeyOptions contains config options for ParsePrivateKey. type ParsePrivateKeyOptions struct { - // CustomHardwareKeyPrompt is a custom hardware key prompt to use when asking - // for a hardware key PIN, touch, etc. - // If empty, a default CLI prompt is used. - CustomHardwareKeyPrompt HardwareKeyPrompt - HardwareKeyService HardwareKeyService + HardwareKeyService HardwareKeyService } // ParsePrivateKeyOpt applies configuration options. type ParsePrivateKeyOpt func(o *ParsePrivateKeyOptions) -// WithCustomPrompt sets a custom hardware key prompt. -func WithCustomPrompt(prompt HardwareKeyPrompt) ParsePrivateKeyOpt { - return func(o *ParsePrivateKeyOptions) { - o.CustomHardwareKeyPrompt = prompt - } -} - // WithHardwareKeyService sets the hardware key service. func WithHardwareKeyService(hwKeyService HardwareKeyService) ParsePrivateKeyOpt { return func(o *ParsePrivateKeyOptions) { diff --git a/api/utils/keys/yubikey.go b/api/utils/keys/yubikey.go index 90eab8a20e2ad..e62313a182bdb 100644 --- a/api/utils/keys/yubikey.go +++ b/api/utils/keys/yubikey.go @@ -99,7 +99,7 @@ func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy Priva } // If PIN is required, check that PIN and PUK are not the defaults. - if requiredKeyPolicy.isHardwareKeyPINVerified() { + if requiredKeyPolicy.IsHardwareKeyPINVerified() { if err := y.checkOrSetPIN(ctx); err != nil { return nil, trace.Wrap(err) } diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index 915fa1208a0d9..a33cf4cce23a2 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -246,11 +246,9 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer InsecureSkipVerify: tc.InsecureSkipVerify, // Inject a fake clock into clusters.Storage so we can control when the middleware thinks the // db cert has expired. - Clock: fakeClock, - WebauthnLogin: webauthnLogin, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { - return nil - }, + Clock: fakeClock, + WebauthnLogin: webauthnLogin, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -882,9 +880,7 @@ 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) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) daemonService, err := daemon.New(daemon.Config{ diff --git a/integration/teleterm_test.go b/integration/teleterm_test.go index baad6e2871e7e..ea9026491f1f6 100644 --- a/integration/teleterm_test.go +++ b/integration/teleterm_test.go @@ -258,9 +258,7 @@ 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) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -293,9 +291,7 @@ 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) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -378,9 +374,7 @@ func testGetClusterReturnsPropertiesFromAuthServer(t *testing.T, pack *dbhelpers storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -433,9 +427,7 @@ 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) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -504,9 +496,7 @@ func testClientCache(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers. Dir: tc.KeysDir, Clock: storageFakeClock, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -766,9 +756,7 @@ func testCreateConnectMyComputerRole(t *testing.T, pack *dbhelpers.DatabasePack) storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -885,9 +873,7 @@ func testCreateConnectMyComputerToken(t *testing.T, pack *dbhelpers.DatabasePack InsecureSkipVerify: tc.InsecureSkipVerify, Clock: fakeClock, WebauthnLogin: webauthnLogin, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -948,9 +934,7 @@ 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) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -1035,9 +1019,7 @@ 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) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -1265,9 +1247,7 @@ 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) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) diff --git a/lib/client/api.go b/lib/client/api.go index da2382162d683..814035c14dfa1 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -494,11 +494,6 @@ type Config struct { // SSOMFACeremonyConstructor is a custom SSO MFA ceremony constructor. SSOMFACeremonyConstructor func(rd *sso.Redirector) mfa.SSOMFACeremony - // CustomHardwareKeyPrompt is a custom hardware key prompt to use when asking - // for a hardware key PIN, touch, etc. - // If empty, a default CLI prompt is used. - CustomHardwareKeyPrompt keys.HardwareKeyPrompt - // DisableSSHResumption disables transparent SSH connection resumption. DisableSSHResumption bool @@ -1282,12 +1277,12 @@ func NewClient(c *Config) (tc *TeleportClient, err error) { if tc.TLS != nil || tc.AuthMethods != nil { // Client will use static auth methods instead of client store. // Initialize empty client store to prevent panics. - tc.ClientStore = NewMemClientStore() + tc.ClientStore = NewMemClientStore(nil /*hwKeyService*/) } else { - tc.ClientStore = NewFSClientStore(c.KeysDir) - if c.CustomHardwareKeyPrompt != nil { - tc.ClientStore.SetCustomHardwareKeyPrompt(tc.CustomHardwareKeyPrompt) - } + // TODO (Joerger): init hardware key service (and client store) earlier where it can + // be properly shared. + hardwareKeyService := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + tc.ClientStore = NewFSClientStore(c.KeysDir, hardwareKeyService) if c.AddKeysToAgent == AddKeysToAgentOnly { // Store client keys in memory, but still save trusted certs and profile to disk. tc.ClientStore.KeyStore = NewMemKeyStore() @@ -4002,7 +3997,7 @@ func (tc *TeleportClient) GetNewLoginKeyRing(ctx context.Context) (keyRing *KeyR if tc.PIVSlot != "" { log.DebugContext(ctx, "Using PIV slot specified by client or server settings", "piv_slot", tc.PIVSlot) } - priv, err := keys.GetYubiKeyPrivateKey(ctx, tc.PrivateKeyPolicy, tc.PIVSlot, tc.CustomHardwareKeyPrompt) + priv, err := tc.ClientStore.NewHardwarePrivateKey(ctx, tc.PIVSlot, tc.PrivateKeyPolicy) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/api_test.go b/lib/client/api_test.go index 248f6979049c6..aeac510ca4d90 100644 --- a/lib/client/api_test.go +++ b/lib/client/api_test.go @@ -1054,7 +1054,7 @@ func TestRootClusterName(t *testing.T) { }, { name: "key store", modifyCfg: func(c *Config) { - c.ClientStore = NewMemClientStore() + c.ClientStore = NewMemClientStore(nil /*hwKeyService*/) err := c.ClientStore.AddKeyRing(keyRing) require.NoError(t, err) }, @@ -1113,7 +1113,7 @@ func TestLoadTLSConfigForClusters(t *testing.T) { name: "key store no clusters", clusters: []string{}, modifyCfg: func(c *Config) { - c.ClientStore = NewMemClientStore() + c.ClientStore = NewMemClientStore(nil /*hwKeyService*/) err := c.ClientStore.AddKeyRing(keyRing) require.NoError(t, err) }, @@ -1122,7 +1122,7 @@ func TestLoadTLSConfigForClusters(t *testing.T) { name: "key store root cluster", clusters: []string{rootCluster}, modifyCfg: func(c *Config) { - c.ClientStore = NewMemClientStore() + c.ClientStore = NewMemClientStore(nil /*hwKeyService*/) err := c.ClientStore.AddKeyRing(keyRing) require.NoError(t, err) }, @@ -1131,7 +1131,7 @@ func TestLoadTLSConfigForClusters(t *testing.T) { name: "key store unknown clusters", clusters: []string{"leaf-1", "leaf-2"}, modifyCfg: func(c *Config) { - c.ClientStore = NewMemClientStore() + c.ClientStore = NewMemClientStore(nil /*hwKeyService*/) err := c.ClientStore.AddKeyRing(keyRing) require.NoError(t, err) }, diff --git a/lib/client/client_store.go b/lib/client/client_store.go index 5762f9d7ec8ee..7eb44750630f5 100644 --- a/lib/client/client_store.go +++ b/lib/client/client_store.go @@ -43,7 +43,8 @@ import ( // when using `tsh --add-keys-to-agent=only`, Store will be made up of an in-memory // key store and an FS (~/.tsh) profile and trusted certs store. type Store struct { - log *slog.Logger + log *slog.Logger + hwKeyService keys.HardwareKeyService KeyStore TrustedCertsStore @@ -51,20 +52,26 @@ type Store struct { } // NewMemClientStore initializes an FS backed client store with the given base dir. -func NewFSClientStore(dirPath string) *Store { +func NewFSClientStore(dirPath string, hwKeyService keys.HardwareKeyService) *Store { dirPath = profile.FullProfilePath(dirPath) return &Store{ log: slog.With(teleport.ComponentKey, teleport.ComponentKeyStore), + hwKeyService: hwKeyService, KeyStore: NewFSKeyStore(dirPath), TrustedCertsStore: NewFSTrustedCertsStore(dirPath), ProfileStore: NewFSProfileStore(dirPath), } } +func (s *Store) NewHardwarePrivateKey(ctx context.Context, customSlot keys.PIVSlot, requiredPolicy keys.PrivateKeyPolicy) (*keys.PrivateKey, error) { + return keys.NewHardwarePrivateKey(ctx, s.hwKeyService, customSlot, requiredPolicy) +} + // NewMemClientStore initializes a new in-memory client store. -func NewMemClientStore() *Store { +func NewMemClientStore(hwKeyService keys.HardwareKeyService) *Store { return &Store{ log: slog.With(teleport.ComponentKey, teleport.ComponentKeyStore), + hwKeyService: hwKeyService, KeyStore: NewMemKeyStore(), TrustedCertsStore: NewMemTrustedCertsStore(), ProfileStore: NewMemProfileStore(), @@ -83,12 +90,6 @@ func (s *Store) AddKeyRing(keyRing *KeyRing) error { return nil } -// SetCustomHardwareKeyPrompt sets a custom hardware key prompt -// used to interact with a YubiKey private key. -func (s *Store) SetCustomHardwareKeyPrompt(prompt keys.HardwareKeyPrompt) { - s.KeyStore.SetCustomHardwareKeyPrompt(prompt) -} - // ErrNoProfile is returned by the client store when a specific profile is not found. var ErrNoProfile = &trace.NotFoundError{Message: "no profile"} @@ -121,7 +122,7 @@ func IsNoCredentialsError(err error) bool { // certs store. If the key ring is not found or is missing data (certificates, etc.), // then an ErrNoCredentials error is returned. func (s *Store) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing, error) { - keyRing, err := s.KeyStore.GetKeyRing(idx, opts...) + keyRing, err := s.KeyStore.GetKeyRing(idx, s.hwKeyService, opts...) if trace.IsNotFound(err) { return nil, newNoCredentialsError(err) } else if err != nil { diff --git a/lib/client/client_store_test.go b/lib/client/client_store_test.go index 65d5429272cad..cf7d57238102c 100644 --- a/lib/client/client_store_test.go +++ b/lib/client/client_store_test.go @@ -160,7 +160,7 @@ func newSelfSignedCA(privateKey []byte, cluster string) (*tlsca.CertAuthority, a } func newTestFSClientStore(t *testing.T) *Store { - fsClientStore := NewFSClientStore(t.TempDir()) + fsClientStore := NewFSClientStore(t.TempDir(), nil /*hwKeyService*/) return fsClientStore } @@ -170,7 +170,7 @@ func testEachClientStore(t *testing.T, testFunc func(t *testing.T, clientStore * }) t.Run("Mem", func(t *testing.T) { - testFunc(t, NewMemClientStore()) + testFunc(t, NewMemClientStore(nil /*hwKeyService*/)) }) } @@ -198,7 +198,7 @@ func TestClientStore(t *testing.T) { require.Equal(t, keyRing.TrustedCerts, retrievedTrustedCerts) // Getting the key from the key store should have no trusted certs. - retrievedKeyRing, err := clientStore.KeyStore.GetKeyRing(idx, WithAllCerts...) + retrievedKeyRing, err := clientStore.KeyStore.GetKeyRing(idx, clientStore.hwKeyService, WithAllCerts...) require.NoError(t, err) expectKeyRing := keyRing.Copy() expectKeyRing.TrustedCerts = nil @@ -464,7 +464,7 @@ func BenchmarkLoadKeysToKubeFromStore(b *testing.B) { for _, kubeClusterName := range kubeClusterNames { go func() { defer wg.Done() - keyRing, err := fsKeyStore.GetKeyRing(keyRing.KeyRingIndex, WithKubeCerts{}) + keyRing, err := fsKeyStore.GetKeyRing(keyRing.KeyRingIndex, nil /*hwks*/, WithKubeCerts{}) require.NoError(b, err) require.NotNil(b, keyRing.KubeTLSCredentials[kubeClusterName].PrivateKey) require.NotEmpty(b, keyRing.KubeTLSCredentials[kubeClusterName].Cert) diff --git a/lib/client/cluster_client_test.go b/lib/client/cluster_client_test.go index e529b4737d1db..e30f4832c0c85 100644 --- a/lib/client/cluster_client_test.go +++ b/lib/client/cluster_client_test.go @@ -95,7 +95,7 @@ func TestIssueUserCertsWithMFA(t *testing.T) { clock := clockwork.NewFakeClock() agent, err := NewLocalAgent(LocalAgentConfig{ - ClientStore: NewMemClientStore(), + ClientStore: NewMemClientStore(nil /*hwKeyService*/), ProxyHost: "test", Username: "alice", Insecure: true, @@ -278,7 +278,7 @@ func TestIssueUserCertsWithMFA(t *testing.T) { { name: "no keys loaded", agent: &LocalKeyAgent{ - clientStore: NewMemClientStore(), + clientStore: NewMemClientStore(nil /*hwKeyService*/), }, assertion: func(t *testing.T, keyRing *KeyRing, mfaRequired proto.MFARequired, err error) { require.Error(t, err) diff --git a/lib/client/identityfile/identity.go b/lib/client/identityfile/identity.go index ad929ce4cc218..1e42622a49832 100644 --- a/lib/client/identityfile/identity.go +++ b/lib/client/identityfile/identity.go @@ -838,8 +838,8 @@ func KeyRingFromIdentityFile(identityPath, proxyHost, clusterName string) (*clie // This is necessary because identity files do not store the proxy address. // Additionally, the [clusterName] argument can ve used to target a leaf cluster // rather than the default root cluster. -func NewClientStoreFromIdentityFile(identityFile, proxyAddr, clusterName string) (*client.Store, error) { - clientStore := client.NewMemClientStore() +func NewClientStoreFromIdentityFile(identityFile, proxyAddr, clusterName string, hwKeyService keys.HardwareKeyService) (*client.Store, error) { + clientStore := client.NewMemClientStore(hwKeyService) if err := LoadIdentityFileIntoClientStore(clientStore, identityFile, proxyAddr, clusterName); err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/identityfile/identity_test.go b/lib/client/identityfile/identity_test.go index fe1d9df9a9857..9207ad62cd676 100644 --- a/lib/client/identityfile/identity_test.go +++ b/lib/client/identityfile/identity_test.go @@ -439,7 +439,7 @@ func TestNewClientStoreFromIdentityFile(t *testing.T) { }) require.NoError(t, err) - clientStore, err := NewClientStoreFromIdentityFile(identityFilePath, keyRing.ProxyHost+":3080", keyRing.ClusterName) + clientStore, err := NewClientStoreFromIdentityFile(identityFilePath, keyRing.ProxyHost+":3080", keyRing.ClusterName, nil /*hwKeyService*/) require.NoError(t, err) currentProfile, err := clientStore.CurrentProfile() diff --git a/lib/client/keyagent_test.go b/lib/client/keyagent_test.go index b937812f49ddb..d038601535706 100644 --- a/lib/client/keyagent_test.go +++ b/lib/client/keyagent_test.go @@ -330,7 +330,7 @@ func TestHostCertVerification(t *testing.T) { s := makeSuite(t) // Make a new local agent. - clientStore := NewFSClientStore(s.keyDir) + clientStore := NewFSClientStore(s.keyDir, nil /*hwKeyService*/) lka, err := NewLocalAgent(LocalAgentConfig{ ClientStore: clientStore, ProxyHost: s.hostname, @@ -471,7 +471,7 @@ func TestHostKeyVerification(t *testing.T) { s := makeSuite(t) // make a new local agent - keystore := NewFSClientStore(s.keyDir) + keystore := NewFSClientStore(s.keyDir, nil /*hwKeyService*/) lka, err := NewLocalAgent(LocalAgentConfig{ ClientStore: keystore, ProxyHost: s.hostname, @@ -562,7 +562,7 @@ func TestHostCertVerificationLoadAllCasProxyAddrEqClusterName(t *testing.T) { ) s := makeSuite(t, withClusterName(rootClusterName), withHostname(rootClusterName)) - clientStore := NewFSClientStore(s.keyDir) + clientStore := NewFSClientStore(s.keyDir, nil /*hwKeyService*/) lka, err := NewLocalAgent(LocalAgentConfig{ ClientStore: clientStore, ProxyHost: proxyHost, @@ -646,7 +646,7 @@ func TestDefaultHostPromptFunc(t *testing.T) { keygen := testauthority.New() - clientStore := NewFSClientStore(s.keyDir) + clientStore := NewFSClientStore(s.keyDir, nil /*hwKeyService*/) a, err := NewLocalAgent(LocalAgentConfig{ ClientStore: clientStore, ProxyHost: s.hostname, @@ -694,7 +694,7 @@ func TestLocalKeyAgent_AddDatabaseKey(t *testing.T) { s := makeSuite(t) // make a new local agent - clientStore := NewFSClientStore(s.keyDir) + clientStore := NewFSClientStore(s.keyDir, nil /*hwKeyService*/) lka, err := NewLocalAgent( LocalAgentConfig{ ClientStore: clientStore, @@ -855,7 +855,7 @@ func startDebugAgent(t *testing.T) error { func (s *KeyAgentTestSuite) newKeyAgent(t *testing.T) *LocalKeyAgent { // make a new local agent - clientStore := NewFSClientStore(s.keyDir) + clientStore := NewFSClientStore(s.keyDir, nil /*hwKeyService*/) keyAgent, err := NewLocalAgent(LocalAgentConfig{ ClientStore: clientStore, ProxyHost: s.hostname, diff --git a/lib/client/keystore.go b/lib/client/keystore.go index 43fa9b05c253e..cd974c416e8c0 100644 --- a/lib/client/keystore.go +++ b/lib/client/keystore.go @@ -73,7 +73,7 @@ type KeyStore interface { // GetKeyRing returns the user's key ring including the specified certs. The // key's TrustedCerts will be nil and should be filled in using a // TrustedCertsStore. - GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing, error) + GetKeyRing(idx KeyRingIndex, hwks keys.HardwareKeyService, opts ...CertOption) (*KeyRing, error) // DeleteKeyRing deletes the user's key with all its certs. DeleteKeyRing(idx KeyRingIndex) error @@ -88,10 +88,6 @@ type KeyStore interface { // GetSSHCertificates gets all certificates signed for the given user and proxy, // including certificates for trusted clusters. GetSSHCertificates(proxyHost, username string) ([]*ssh.Certificate, error) - - // SetCustomHardwareKeyPrompt sets a custom hardware key prompt - // used to interact with a YubiKey private key. - SetCustomHardwareKeyPrompt(prompt keys.HardwareKeyPrompt) } // FSKeyStore is an on-disk implementation of the KeyStore interface. @@ -103,10 +99,6 @@ type FSKeyStore struct { // KeyDir is the directory where all keys are stored. KeyDir string - // CustomHardwareKeyPrompt is a custom hardware key prompt to use when asking - // for a hardware key PIN, touch, etc. - // If nil, a default CLI prompt is used. - CustomHardwareKeyPrompt keys.HardwareKeyPrompt } // NewFSKeyStore initializes a new FSClientStore. @@ -193,12 +185,6 @@ func (fs *FSKeyStore) kubeCredPath(idx KeyRingIndex, kubename string) string { return keypaths.KubeCredPath(fs.KeyDir, idx.ProxyHost, idx.Username, idx.ClusterName, kubename) } -// SetCustomHardwareKeyPrompt sets a custom hardware key prompt -// used to interact with a YubiKey private key. -func (fs *FSKeyStore) SetCustomHardwareKeyPrompt(prompt keys.HardwareKeyPrompt) { - fs.CustomHardwareKeyPrompt = prompt -} - // AddKeyRing adds the given key ring to the store. func (fs *FSKeyStore) AddKeyRing(keyRing *KeyRing) error { if err := keyRing.KeyRingIndex.Check(); err != nil { @@ -301,12 +287,12 @@ func (fs *FSKeyStore) writeTLSCredential(cred TLSCredential, keyPath, certPath s return nil } -func readTLSCredential(keyPath, certPath string, customPrompt keys.HardwareKeyPrompt) (TLSCredential, error) { +func readTLSCredential(keyPath, certPath string, opts ...keys.ParsePrivateKeyOpt) (TLSCredential, error) { keyPEM, certPEM, err := readTLSCredentialFiles(keyPath, certPath) if err != nil { return TLSCredential{}, trace.Wrap(err) } - key, err := keys.ParsePrivateKey(keyPEM, keys.WithCustomPrompt(customPrompt)) + key, err := keys.ParsePrivateKey(keyPEM, opts...) if err != nil { return TLSCredential{}, trace.Wrap(err) } @@ -390,12 +376,12 @@ func (fs *FSKeyStore) writeKubeCredential(cred TLSCredential, path string) error // readKubeCredential reads a kube key and cert from a single file written by // [(*FSKeyStore).writeKubeCredential]. Compared to using separate files it is // more efficient for reading/writing and avoids file locks. -func readKubeCredential(path string) (TLSCredential, error) { +func readKubeCredential(path string, opts ...keys.ParsePrivateKeyOpt) (TLSCredential, error) { keyPEM, certPEM, err := readKubeCredentialFile(path) if err != nil { return TLSCredential{}, trace.Wrap(err) } - privateKey, err := keys.ParsePrivateKey(keyPEM) + privateKey, err := keys.ParsePrivateKey(keyPEM, opts...) if err != nil { return TLSCredential{}, trace.Wrap(err) } @@ -538,7 +524,7 @@ func (e *LegacyCertPathError) Unwrap() error { // GetKeyRing returns the user's key including the specified certs. // If the key is not found, returns trace.NotFound error. -func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing, error) { +func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks keys.HardwareKeyService, opts ...CertOption) (*KeyRing, error) { if len(opts) > 0 { if err := idx.Check(); err != nil { return nil, trace.Wrap(err, "GetKeyRing with CertOptions requires a fully specified KeyRingIndex") @@ -549,7 +535,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing return nil, trace.Wrap(err, "no session keys for %+v", idx) } - tlsCred, err := readTLSCredential(fs.userTLSKeyPath(idx), fs.tlsCertPath(idx), fs.CustomHardwareKeyPrompt) + tlsCred, err := readTLSCredential(fs.userTLSKeyPath(idx), fs.tlsCertPath(idx), keys.WithHardwareKeyService(hwks)) if err != nil { if trace.IsNotFound(err) { if _, statErr := os.Stat(fs.tlsCertPathLegacy(idx)); statErr == nil { @@ -560,7 +546,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing return nil, trace.Wrap(err) } - sshPriv, err := keys.LoadKeyPair(fs.userSSHKeyPath(idx), fs.publicKeyPath(idx), fs.CustomHardwareKeyPrompt) + sshPriv, err := keys.LoadKeyPair(fs.userSSHKeyPath(idx), fs.publicKeyPath(idx), keys.WithHardwareKeyService(hwks)) if err != nil { return nil, trace.ConvertSystemError(err) } @@ -570,7 +556,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing keyRing.TLSCert = tlsCred.Cert for _, o := range opts { - if err := fs.updateKeyRingWithCerts(o, keyRing); err != nil && !trace.IsNotFound(err) { + if err := fs.updateKeyRingWithCerts(o, hwks, keyRing); err != nil && !trace.IsNotFound(err) { return nil, trace.Wrap(err) } } @@ -582,8 +568,8 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing return keyRing, nil } -func (fs *FSKeyStore) updateKeyRingWithCerts(o CertOption, keyRing *KeyRing) error { - return trace.Wrap(o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, fs.CustomHardwareKeyPrompt)) +func (fs *FSKeyStore) updateKeyRingWithCerts(o CertOption, hwks keys.HardwareKeyService, keyRing *KeyRing) error { + return trace.Wrap(o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, keys.WithHardwareKeyService(hwks))) } // GetSSHCertificates gets all certificates signed for the given user and proxy. @@ -610,7 +596,7 @@ func (fs *FSKeyStore) GetSSHCertificates(proxyHost, username string) ([]*ssh.Cer return sshCerts, nil } -func getCredentialsByName(credentialDir string, customPrompt keys.HardwareKeyPrompt) (map[string]TLSCredential, error) { +func getCredentialsByName(credentialDir string, opts ...keys.ParsePrivateKeyOpt) (map[string]TLSCredential, error) { files, err := os.ReadDir(credentialDir) if err != nil { return nil, trace.ConvertSystemError(err) @@ -620,7 +606,7 @@ func getCredentialsByName(credentialDir string, customPrompt keys.HardwareKeyPro if keyName := keypaths.TrimKeyPathSuffix(file.Name()); keyName != file.Name() { keyPath := filepath.Join(credentialDir, file.Name()) certPath := filepath.Join(credentialDir, keyName+keypaths.FileExtTLSCert) - cred, err := readTLSCredential(keyPath, certPath, customPrompt) + cred, err := readTLSCredential(keyPath, certPath, opts...) if err != nil { if trace.IsNotFound(err) { // Somehow we have a key with no cert, skip it. This should @@ -635,7 +621,7 @@ func getCredentialsByName(credentialDir string, customPrompt keys.HardwareKeyPro return credsByName, nil } -func getKubeCredentialsByName(credentialDir string) (map[string]TLSCredential, error) { +func getKubeCredentialsByName(credentialDir string, opts ...keys.ParsePrivateKeyOpt) (map[string]TLSCredential, error) { files, err := os.ReadDir(credentialDir) if err != nil { return nil, trace.ConvertSystemError(err) @@ -644,7 +630,7 @@ func getKubeCredentialsByName(credentialDir string) (map[string]TLSCredential, e for _, file := range files { if credName := strings.TrimSuffix(file.Name(), keypaths.FileExtKubeCred); credName != file.Name() { credPath := filepath.Join(credentialDir, file.Name()) - cred, err := readKubeCredential(credPath) + cred, err := readKubeCredential(credPath, opts...) if err != nil { return nil, trace.Wrap(err) } @@ -658,7 +644,7 @@ func getKubeCredentialsByName(credentialDir string) (map[string]TLSCredential, e type CertOption interface { // updateKeyRing is used by [FSKeyStore] to add the relevant credentials // loaded from disk to [keyRing]. - updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, customPrompt keys.HardwareKeyPrompt) error + updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, opts ...keys.ParsePrivateKeyOpt) error // pathsToDelete is used by [FSKeyStore] to get all the paths (files and/or // directories) that should be deleted by [DeleteUserCerts]. pathsToDelete(keyDir string, idx KeyRingIndex) []string @@ -672,7 +658,7 @@ var WithAllCerts = []CertOption{WithSSHCerts{}, WithKubeCerts{}, WithDBCerts{}, // WithSSHCerts is a CertOption for handling SSH certificates. type WithSSHCerts struct{} -func (o WithSSHCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, _ keys.HardwareKeyPrompt) error { +func (o WithSSHCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, _ ...keys.ParsePrivateKeyOpt) error { certPath := keypaths.SSHCertPath(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) cert, err := os.ReadFile(certPath) if err != nil { @@ -696,9 +682,9 @@ func (o WithSSHCerts) deleteFromKeyRing(keyRing *KeyRing) { // WithKubeCerts is a CertOption for handling kubernetes certificates. type WithKubeCerts struct{} -func (o WithKubeCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, _ keys.HardwareKeyPrompt) error { +func (o WithKubeCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, opts ...keys.ParsePrivateKeyOpt) error { credentialDir := keypaths.KubeCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) - credsByName, err := getKubeCredentialsByName(credentialDir) + credsByName, err := getKubeCredentialsByName(credentialDir, opts...) if err != nil { return trace.Wrap(err) } @@ -722,9 +708,9 @@ type WithDBCerts struct { dbName string } -func (o WithDBCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, customPrompt keys.HardwareKeyPrompt) error { +func (o WithDBCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, opts ...keys.ParsePrivateKeyOpt) error { credentialDir := keypaths.DatabaseCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) - credsByName, err := getCredentialsByName(credentialDir, customPrompt) + credsByName, err := getCredentialsByName(credentialDir, opts...) if err != nil { return trace.Wrap(err) } @@ -754,9 +740,9 @@ type WithAppCerts struct { appName string } -func (o WithAppCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, customPrompt keys.HardwareKeyPrompt) error { +func (o WithAppCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing, opts ...keys.ParsePrivateKeyOpt) error { credentialDir := keypaths.AppCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) - credsByName, err := getCredentialsByName(credentialDir, customPrompt) + credsByName, err := getCredentialsByName(credentialDir, opts...) if err != nil { return trace.Wrap(err) } @@ -820,7 +806,7 @@ func (ms *MemKeyStore) AddKeyRing(keyRing *KeyRing) error { } // GetKeyRing returns the user's key ring including the specified certs. -func (ms *MemKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing, error) { +func (ms *MemKeyStore) GetKeyRing(idx KeyRingIndex, _ keys.HardwareKeyService, opts ...CertOption) (*KeyRing, error) { if len(opts) > 0 { if err := idx.Check(); err != nil { return nil, trace.Wrap(err, "GetKeyRing with CertOptions requires a fully specified KeyRingIndex") @@ -922,7 +908,3 @@ func (ms *MemKeyStore) GetSSHCertificates(proxyHost, username string) ([]*ssh.Ce return sshCerts, nil } - -// SetCustomHardwareKeyPrompt implements the KeyStore.SetCustomHardwareKeyPrompt interface. -// Does nothing. -func (ms *MemKeyStore) SetCustomHardwareKeyPrompt(_ keys.HardwareKeyPrompt) {} diff --git a/lib/client/keystore_test.go b/lib/client/keystore_test.go index b78820ace94ce..a336867be9355 100644 --- a/lib/client/keystore_test.go +++ b/lib/client/keystore_test.go @@ -67,7 +67,7 @@ func TestKeyStore(t *testing.T) { // check that the key exists in the store and is the same, // except the key's trusted certs should be empty, to be // filled in by a trusted certs store. - retrievedKeyRing, err := keyStore.GetKeyRing(idx, WithAllCerts...) + retrievedKeyRing, err := keyStore.GetKeyRing(idx, nil /*hwks*/, WithAllCerts...) require.NoError(t, err) keyRing.TrustedCerts = nil assertEqualKeyRings(t, keyRing, retrievedKeyRing) @@ -75,14 +75,14 @@ func TestKeyStore(t *testing.T) { // Delete just the db cred, reload & verify it's gone err = keyStore.DeleteUserCerts(idx, WithDBCerts{}) require.NoError(t, err) - retrievedKeyRing, err = keyStore.GetKeyRing(idx, WithSSHCerts{}, WithDBCerts{}) + retrievedKeyRing, err = keyStore.GetKeyRing(idx, nil /*hwks*/, WithSSHCerts{}, WithDBCerts{}) require.NoError(t, err) expectKeyRing := keyRing.Copy() expectKeyRing.DBTLSCredentials = make(map[string]TLSCredential) assertEqualKeyRings(t, expectKeyRing, retrievedKeyRing) // check for the key, now without cluster name - retrievedKeyRing, err = keyStore.GetKeyRing(KeyRingIndex{idx.ProxyHost, idx.Username, ""}) + retrievedKeyRing, err = keyStore.GetKeyRing(KeyRingIndex{idx.ProxyHost, idx.Username, ""}, nil /*hwks*/) require.NoError(t, err) expectKeyRing.ClusterName = "" expectKeyRing.Cert = nil @@ -93,7 +93,7 @@ func TestKeyStore(t *testing.T) { require.NoError(t, err) // check that the key doesn't exist in the store - retrievedKeyRing, err = keyStore.GetKeyRing(idx) + retrievedKeyRing, err = keyStore.GetKeyRing(idx, nil /*hwks*/) require.Error(t, err) require.True(t, trace.IsNotFound(err)) require.Nil(t, retrievedKeyRing) @@ -128,14 +128,14 @@ func TestListKeys(t *testing.T) { // read all bob keys: for i := 0; i < keyNum; i++ { - keyRing, err := keyStore.GetKeyRing(keys[i].KeyRingIndex, WithSSHCerts{}, WithDBCerts{}) + keyRing, err := keyStore.GetKeyRing(keys[i].KeyRingIndex, nil /*hwks*/, WithSSHCerts{}, WithDBCerts{}) require.NoError(t, err) keyRing.TrustedCerts = keys[i].TrustedCerts assertEqualKeyRings(t, &keys[i], keyRing) } // read sam's key and make sure it's the same: - skeyRing, err := keyStore.GetKeyRing(samIdx, WithSSHCerts{}) + skeyRing, err := keyStore.GetKeyRing(samIdx, nil /*hwks*/, WithSSHCerts{}) require.NoError(t, err) require.Equal(t, samKeyRing.Cert, skeyRing.Cert) require.Equal(t, samKeyRing.TLSCert, skeyRing.TLSCert) @@ -190,9 +190,9 @@ func TestDeleteAll(t *testing.T) { require.NoError(t, err) // check keys exist - _, err = keyStore.GetKeyRing(idxFoo) + _, err = keyStore.GetKeyRing(idxFoo, nil /*hwks*/) require.NoError(t, err) - _, err = keyStore.GetKeyRing(idxBar) + _, err = keyStore.GetKeyRing(idxBar, nil /*hwks*/) require.NoError(t, err) // delete all keys @@ -200,9 +200,9 @@ func TestDeleteAll(t *testing.T) { require.NoError(t, err) // verify keys are gone - _, err = keyStore.GetKeyRing(idxFoo) + _, err = keyStore.GetKeyRing(idxFoo, nil /*hwks*/) require.True(t, trace.IsNotFound(err)) - _, err = keyStore.GetKeyRing(idxBar) + _, err = keyStore.GetKeyRing(idxBar, nil /*hwks*/) require.Error(t, err) }) } @@ -225,7 +225,7 @@ func TestCheckKey(t *testing.T) { err = keyStore.AddKeyRing(keyRing) require.NoError(t, err) - _, err = keyStore.GetKeyRing(idx) + _, err = keyStore.GetKeyRing(idx, nil /*hwks*/) require.NoError(t, err) }) } @@ -254,7 +254,7 @@ func TestCheckKeyFIPS(t *testing.T) { require.NoError(t, err) // Should return trace.BadParameter error because only RSA keys are supported. - _, err = keyStore.GetKeyRing(idx) + _, err = keyStore.GetKeyRing(idx, nil /*hwks*/) require.True(t, trace.IsBadParameter(err)) }) } @@ -276,7 +276,7 @@ func TestAddKey_withoutSSHCert(t *testing.T) { require.ErrorIs(t, err, os.ErrNotExist) // check db creds - keyCopy, err := keyStore.GetKeyRing(idx, WithDBCerts{}) + keyCopy, err := keyStore.GetKeyRing(idx, nil /*hwks*/, WithDBCerts{}) require.NoError(t, err) require.Len(t, keyCopy.DBTLSCredentials, 1) } diff --git a/lib/client/profile.go b/lib/client/profile.go index 6d60f203981ef..7a8902825363f 100644 --- a/lib/client/profile.go +++ b/lib/client/profile.go @@ -35,6 +35,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/wrappers" "github.com/gravitational/teleport/api/utils/keypaths" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/sshca" "github.com/gravitational/teleport/lib/tlsca" @@ -597,8 +598,9 @@ func (p *ProfileStatus) DatabasesForCluster(clusterName string) ([]tlsca.RouteTo ClusterName: clusterName, } + hwks := keys.NewHardwareKeyService(&keys.CLIPrompt{}) store := NewFSKeyStore(p.Dir) - keyRing, err := store.GetKeyRing(idx, WithDBCerts{}) + keyRing, err := store.GetKeyRing(idx, hwks, WithDBCerts{}) if err != nil { return nil, trace.Wrap(err) } @@ -618,8 +620,9 @@ func (p *ProfileStatus) AppsForCluster(clusterName string) ([]tlsca.RouteToApp, ClusterName: clusterName, } + hwks := keys.NewHardwareKeyService(&keys.CLIPrompt{}) store := NewFSKeyStore(p.Dir) - keyRing, err := store.GetKeyRing(idx, WithAppCerts{}) + keyRing, err := store.GetKeyRing(idx, hwks, WithAppCerts{}) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/teleterm/clusters/config.go b/lib/teleterm/clusters/config.go index ff94f4fdb533a..aecf6cc99d8a9 100644 --- a/lib/teleterm/clusters/config.go +++ b/lib/teleterm/clusters/config.go @@ -27,7 +27,6 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/client" - "github.com/gravitational/teleport/lib/teleterm/api/uri" ) // Config is the cluster service config @@ -45,9 +44,8 @@ type Config struct { WebauthnLogin client.WebauthnLoginFunc // AddKeysToAgent is passed to [client.Config]. 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) keys.HardwareKeyPrompt + // HardwareKeyService is a service for interfacing with hardware keys. + HardwareKeyService keys.HardwareKeyService } // CheckAndSetDefaults checks the configuration for its validity and sets default values if needed @@ -56,8 +54,8 @@ 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.HardwareKeyService == nil { + return trace.BadParameter("missing hardware key service") } if c.Clock == nil { diff --git a/lib/teleterm/clusters/storage.go b/lib/teleterm/clusters/storage.go index 7d5b1292aa25e..d6e9d04e24d3f 100644 --- a/lib/teleterm/clusters/storage.go +++ b/lib/teleterm/clusters/storage.go @@ -280,13 +280,12 @@ func (s *Storage) loadProfileStatusAndClusterKey(clusterClient *client.TeleportC func (s *Storage) makeDefaultClientConfig(rootClusterURI uri.ResourceURI) *client.Config { cfg := client.MakeDefaultConfig() - cfg.HomePath = s.Dir cfg.KeysDir = s.Dir cfg.InsecureSkipVerify = s.InsecureSkipVerify cfg.AddKeysToAgent = s.AddKeysToAgent cfg.WebauthnLogin = s.WebauthnLogin - cfg.CustomHardwareKeyPrompt = s.HardwareKeyPromptConstructor(rootClusterURI) + cfg.ClientStore = client.NewFSClientStore(s.Dir, s.HardwareKeyService) cfg.DTAuthnRunCeremony = dtauthn.NewCeremony().Run cfg.DTAutoEnroll = dtenroll.AutoEnroll return cfg diff --git a/lib/teleterm/daemon/daemon_test.go b/lib/teleterm/daemon/daemon_test.go index 2e7cd9a70d4de..f1d04c090a8e1 100644 --- a/lib/teleterm/daemon/daemon_test.go +++ b/lib/teleterm/daemon/daemon_test.go @@ -348,9 +348,7 @@ func TestUpdateTshdEventsServerAddress(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: homeDir, InsecureSkipVerify: true, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -385,9 +383,7 @@ func TestUpdateTshdEventsServerAddress_CredsErr(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: homeDir, InsecureSkipVerify: true, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -489,9 +485,7 @@ func TestRetryWithRelogin(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) @@ -545,9 +539,7 @@ func TestConcurrentHeadlessAuthPrompts(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { - return nil - }, + HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), }) require.NoError(t, err) diff --git a/lib/teleterm/daemon/hardwarekeyprompt.go b/lib/teleterm/daemon/hardwarekeyprompt.go index ad75ccf6f699b..b20426f4c86c5 100644 --- a/lib/teleterm/daemon/hardwarekeyprompt.go +++ b/lib/teleterm/daemon/hardwarekeyprompt.go @@ -28,8 +28,7 @@ import ( "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,8 +45,8 @@ 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) keys.HardwareKeyPrompt { - return &hardwareKeyPrompter{s: s, rootClusterURI: rootClusterURI} +func (s *Service) NewHardwareKeyPrompt() keys.HardwareKeyPrompt { + return &hardwareKeyPrompter{s: s} } type hardwareKeyPrompter struct { diff --git a/lib/teleterm/teleterm.go b/lib/teleterm/teleterm.go index 0c6cde8efd031..63bcf12cb6237 100644 --- a/lib/teleterm/teleterm.go +++ b/lib/teleterm/teleterm.go @@ -33,7 +33,6 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/gravitational/teleport/api/utils/keys" - "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 +41,10 @@ import ( // Serve starts daemon service func Serve(ctx context.Context, cfg Config) error { - var hardwareKeyPromptConstructor func(clusterURI uri.ResourceURI) keys.HardwareKeyPrompt + // TODO(gzdunek): Move tshdEventsClient out of daemonService so that we can + // set the prompt before creating Storage. + hardwareKeyService := keys.NewHardwareKeyService(nil /*prompt*/) + if err := cfg.CheckAndSetDefaults(); err != nil { return trace.Wrap(err) } @@ -59,9 +61,7 @@ func Serve(ctx context.Context, cfg Config) error { Clock: clock, InsecureSkipVerify: cfg.InsecureSkipVerify, AddKeysToAgent: cfg.AddKeysToAgent, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { - return hardwareKeyPromptConstructor(rootClusterURI) - }, + HardwareKeyService: hardwareKeyService, }) if err != nil { return trace.Wrap(err) @@ -81,9 +81,10 @@ func Serve(ctx context.Context, cfg Config) error { return trace.Wrap(err) } - // TODO(gzdunek): Move tshdEventsClient out of daemonService so that we can - // construct the prompt before creating Storage. - hardwareKeyPromptConstructor = daemonService.NewHardwareKeyPromptConstructor + // TODO(Joerger): add rootClusterURI, or some other identifiable information, to the prompts + // at the client store level, since the hardware key service is shared across clusters. + hardwareKeyService.SetPrompt(daemonService.NewHardwareKeyPrompt()) + apiServer, err := apiserver.New(apiserver.Config{ HostAddr: cfg.Addr, InsecureSkipVerify: cfg.InsecureSkipVerify, diff --git a/lib/vnet/profile_osconfig_provider_darwin.go b/lib/vnet/profile_osconfig_provider_darwin.go index c83407a849579..2f5082b784706 100644 --- a/lib/vnet/profile_osconfig_provider_darwin.go +++ b/lib/vnet/profile_osconfig_provider_darwin.go @@ -27,6 +27,7 @@ import ( "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" "github.com/gravitational/teleport/lib/vnet/daemon" @@ -59,8 +60,10 @@ func newProfileOSConfigProvider(tunName, ipv6Prefix, dnsAddr, homePath string, d return nil, trace.Wrap(err) } + hwKeyService := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + p := &profileOSConfigProvider{ - clientStore: client.NewFSClientStore(homePath), + clientStore: client.NewFSClientStore(homePath, hwKeyService), clusterConfigCache: NewClusterConfigCache(clockwork.NewRealClock()), daemonClientCred: daemonClientCred, tunName: tunName, diff --git a/tool/tctl/common/config/profile.go b/tool/tctl/common/config/profile.go index 6e5da64ba1f84..325576d896860 100644 --- a/tool/tctl/common/config/profile.go +++ b/tool/tctl/common/config/profile.go @@ -29,6 +29,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/metadata" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/identityfile" @@ -39,16 +40,18 @@ import ( // LoadConfigFromProfile applies config from ~/.tsh/ profile if it's present func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authclient.Config, error) { + hwKeyService := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + ctx := context.TODO() proxyAddr := "" if len(ccf.AuthServerAddr) != 0 { proxyAddr = ccf.AuthServerAddr[0] } - clientStore := client.NewFSClientStore(cfg.TeleportHome) + clientStore := client.NewFSClientStore(cfg.TeleportHome, hwKeyService) if ccf.IdentityFilePath != "" { var err error - clientStore, err = identityfile.NewClientStoreFromIdentityFile(ccf.IdentityFilePath, proxyAddr, "") + clientStore, err = identityfile.NewClientStoreFromIdentityFile(ccf.IdentityFilePath, proxyAddr, "", hwKeyService) if err != nil { return nil, trace.Wrap(err) } diff --git a/tool/tsh/common/db_test.go b/tool/tsh/common/db_test.go index 06787491de421..c05387235a9fc 100644 --- a/tool/tsh/common/db_test.go +++ b/tool/tsh/common/db_test.go @@ -459,7 +459,7 @@ func testDatabaseLogin(t *testing.T) { require.NoError(t, err) // Fetch the active profile. - clientStore := client.NewFSClientStore(tmpHomePath) + clientStore := client.NewFSClientStore(tmpHomePath, nil /*hwKeyService*/) profile, err := clientStore.ReadProfileStatus(s.root.Config.Proxy.WebAddr.String()) require.NoError(t, err) require.Equal(t, s.user.GetName(), profile.Username) diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index a3171c6bbdf56..664ea82b84fae 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -4575,10 +4575,12 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err } func initClientStore(cf *CLIConf, proxy string) (*client.Store, error) { + hardwareKeyService := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + switch { case cf.IdentityFileIn != "": // Import identity file keys to in-memory client store. - clientStore, err := identityfile.NewClientStoreFromIdentityFile(cf.IdentityFileIn, proxy, cf.SiteName) + clientStore, err := identityfile.NewClientStoreFromIdentityFile(cf.IdentityFileIn, proxy, cf.SiteName, hardwareKeyService) if err != nil { return nil, trace.Wrap(err) } @@ -4587,16 +4589,16 @@ func initClientStore(cf *CLIConf, proxy string) (*client.Store, error) { case cf.IdentityFileOut != "", cf.AuthConnector == constants.HeadlessConnector: // Store client keys in memory, where they can be exported to non-standard // FS formats (e.g. identity file) or used for a single client call in memory. - return client.NewMemClientStore(), nil + return client.NewMemClientStore(hardwareKeyService), nil case cf.AddKeysToAgent == client.AddKeysToAgentOnly: // Store client keys in memory, but save trusted certs and profile to disk. - clientStore := client.NewFSClientStore(cf.HomePath) + clientStore := client.NewFSClientStore(cf.HomePath, hardwareKeyService) clientStore.KeyStore = client.NewMemKeyStore() return clientStore, nil default: - return client.NewFSClientStore(cf.HomePath), nil + return client.NewFSClientStore(cf.HomePath, hardwareKeyService), nil } } diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 2db166e61f5d9..850563d018420 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -5888,7 +5888,7 @@ func TestLogout(t *testing.T) { t.Parallel() tmpHomePath := t.TempDir() - store := client.NewFSClientStore(tmpHomePath) + store := client.NewFSClientStore(tmpHomePath, nil /*hwKeyService*/) err := store.AddKeyRing(clientKeyRing) require.NoError(t, err) store.SaveProfile(profile, true) diff --git a/tool/tsh/common/vnet_client_application.go b/tool/tsh/common/vnet_client_application.go index 7743edfc502c3..21c5d6b57fc07 100644 --- a/tool/tsh/common/vnet_client_application.go +++ b/tool/tsh/common/vnet_client_application.go @@ -28,6 +28,7 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/utils/keys" vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" @@ -45,7 +46,9 @@ type vnetClientApplication struct { } func newVnetClientApplication(cf *CLIConf) (*vnetClientApplication, error) { - clientStore := client.NewFSClientStore(cf.HomePath) + hwKeyService := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + + clientStore := client.NewFSClientStore(cf.HomePath, hwKeyService) p := &vnetClientApplication{ cf: cf, From 105eb78b172509ca69c184c90a4b13185714d8a2 Mon Sep 17 00:00:00 2001 From: joerger Date: Mon, 10 Mar 2025 16:37:07 -0700 Subject: [PATCH 03/10] Add key info to HardwareKeyPrompt args instead of setting it in the prompt itself. --- api/utils/keys/cliprompt.go | 8 ++++---- api/utils/keys/hardwarekey.go | 1 + api/utils/keys/privatekey.go | 19 ++++++++++++++++++- api/utils/keys/yubikey.go | 10 +++++----- api/utils/keys/yubikey_common.go | 8 ++++---- lib/client/keystore.go | 6 +++--- lib/teleterm/daemon/hardwarekeyprompt.go | 20 +++++++++----------- lib/teleterm/teleterm.go | 2 -- 8 files changed, 44 insertions(+), 30 deletions(-) diff --git a/api/utils/keys/cliprompt.go b/api/utils/keys/cliprompt.go index e7d8d88a29f11..d9be72ed9f185 100644 --- a/api/utils/keys/cliprompt.go +++ b/api/utils/keys/cliprompt.go @@ -27,7 +27,7 @@ import ( type CLIPrompt struct{} -func (c *CLIPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement) (string, error) { +func (c *CLIPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement, _ KeyInfo) (string, error) { message := "Enter your YubiKey PIV PIN" if requirement == PINOptional { message = "Enter your YubiKey PIV PIN [blank to use default PIN]" @@ -36,12 +36,12 @@ func (c *CLIPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement return password, trace.Wrap(err) } -func (c *CLIPrompt) Touch(_ context.Context) error { +func (c *CLIPrompt) Touch(_ context.Context, _ KeyInfo) error { _, err := fmt.Fprintln(os.Stderr, "Tap your YubiKey") return trace.Wrap(err) } -func (c *CLIPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) { +func (c *CLIPrompt) ChangePIN(ctx context.Context, _ KeyInfo) (*PINAndPUK, error) { var pinAndPUK = &PINAndPUK{} for { fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PIN.\n") @@ -118,7 +118,7 @@ func (c *CLIPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) { return pinAndPUK, nil } -func (c *CLIPrompt) ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) { +func (c *CLIPrompt) ConfirmSlotOverwrite(ctx context.Context, message string, _ KeyInfo) (bool, error) { confirmation, err := prompt.Confirmation(ctx, os.Stderr, prompt.Stdin(), message) return confirmation, trace.Wrap(err) } diff --git a/api/utils/keys/hardwarekey.go b/api/utils/keys/hardwarekey.go index d88d6c23b8c5d..edd8898c133d1 100644 --- a/api/utils/keys/hardwarekey.go +++ b/api/utils/keys/hardwarekey.go @@ -48,6 +48,7 @@ type HardwareKeyService interface { type HardwarePrivateKey struct { service HardwareKeyService ref *HardwarePrivateKeyRef + keyInfo KeyInfo } // Public implements [crypto.Signer]. diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index dc703df077f5f..ff5afa1cde92c 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -95,7 +95,7 @@ func NewSoftwarePrivateKey(signer crypto.Signer) (*PrivateKey, error) { // NewHardwarePrivateKey creates or retrieves a hardware private key from from the given hardware key // service that matches the given PIV slot and private key policy, returning the hardware private key // as a [PrivateKey]. -func NewHardwarePrivateKey(ctx context.Context, s HardwareKeyService, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*PrivateKey, error) { +func NewHardwarePrivateKey(ctx context.Context, s HardwareKeyService, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy, keyInfo KeyInfo) (*PrivateKey, error) { if s == nil { return nil, trace.BadParameter("cannot create a new hardware private key without a hardware key service provided") } @@ -108,6 +108,7 @@ func NewHardwarePrivateKey(ctx context.Context, s HardwareKeyService, customSlot hwPrivateKey := &HardwarePrivateKey{ service: s, ref: keyRef, + keyInfo: keyInfo, } encodedKeyRef, err := encodeHardwarePrivateKeyRef(keyRef) @@ -241,6 +242,7 @@ func LoadPrivateKey(keyFile string) (*PrivateKey, error) { // ParsePrivateKeyOptions contains config options for ParsePrivateKey. type ParsePrivateKeyOptions struct { HardwareKeyService HardwareKeyService + KeyInfo KeyInfo } // ParsePrivateKeyOpt applies configuration options. @@ -253,6 +255,20 @@ func WithHardwareKeyService(hwKeyService HardwareKeyService) ParsePrivateKeyOpt } } +// KeyInfo includes info relevant to the key being parsed. Useful for adding context +// to hardware key pin/touch prompts when performing signatures. +type KeyInfo struct { + // ProxyHost is the root proxy hostname that a key is associated with. + ProxyHost string +} + +// WithKeyInfo adds contextual key info to the parsed private key. +func WithKeyInfo(proxyHost string) ParsePrivateKeyOpt { + return func(o *ParsePrivateKeyOptions) { + o.KeyInfo = KeyInfo{ProxyHost: proxyHost} + } +} + // 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) { @@ -280,6 +296,7 @@ func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, er return NewPrivateKey(&HardwarePrivateKey{ service: appliedOpts.HardwareKeyService, ref: keyRef, + keyInfo: appliedOpts.KeyInfo, }, keyPEM) case OpenSSHPrivateKeyType: priv, err := ssh.ParseRawPrivateKey(keyPEM) diff --git a/api/utils/keys/yubikey.go b/api/utils/keys/yubikey.go index e62313a182bdb..7b084dd3cd216 100644 --- a/api/utils/keys/yubikey.go +++ b/api/utils/keys/yubikey.go @@ -107,7 +107,7 @@ func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy Priva promptOverwriteSlot := func(msg string) error { promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg) - if confirmed, confirmErr := prompt.ConfirmSlotOverwrite(ctx, promptQuestion); confirmErr != nil { + if confirmed, confirmErr := 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") @@ -348,7 +348,7 @@ func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []b select { case <-touchPromptDelayTimer.C: // Prompt for touch after a delay, in case the function succeeds without touch due to a cached touch. - err := y.prompt.Touch(ctx) + err := y.prompt.Touch(ctx, KeyInfo{}) if err != nil { // Cancel the entire function when an error occurs. // This is typically used for aborting the prompt. @@ -370,7 +370,7 @@ func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []b defer touchPromptDelayTimer.Reset(signTouchPromptDelay) } } - pass, err := y.prompt.AskPIN(ctx, PINRequired) + pass, err := y.prompt.AskPIN(ctx, PINRequired, KeyInfo{}) return pass, trace.Wrap(err) } @@ -662,7 +662,7 @@ func (y *YubiKey) SetPIN(oldPin, newPin string) error { // If the user provides the default PIN, they will be prompted to set a // non-default PIN and PUK before continuing. func (y *YubiKey) checkOrSetPIN(ctx context.Context) error { - pin, err := y.prompt.AskPIN(ctx, PINOptional) + pin, err := y.prompt.AskPIN(ctx, PINOptional, KeyInfo{}) if err != nil { return trace.Wrap(err) } @@ -878,7 +878,7 @@ func (c *sharedPIVConnection) verifyPIN(pin string) error { } func (c *sharedPIVConnection) setPINAndPUKFromDefault(ctx context.Context, prompt HardwareKeyPrompt) (string, error) { - pinAndPUK, err := prompt.ChangePIN(ctx) + pinAndPUK, err := prompt.ChangePIN(ctx, KeyInfo{}) if err != nil { return "", trace.Wrap(err) } diff --git a/api/utils/keys/yubikey_common.go b/api/utils/keys/yubikey_common.go index 5ed36f814580d..43b9f603c8a86 100644 --- a/api/utils/keys/yubikey_common.go +++ b/api/utils/keys/yubikey_common.go @@ -23,17 +23,17 @@ import ( type HardwareKeyPrompt 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 KeyInfo) (string, error) // Touch prompts the user to touch the hardware key. - Touch(ctx context.Context) error + Touch(ctx context.Context, keyInfo KeyInfo) 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 KeyInfo) (*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 KeyInfo) (bool, error) } // PINPromptRequirement specifies whether a PIN is required. diff --git a/lib/client/keystore.go b/lib/client/keystore.go index cd974c416e8c0..44355cf890ba6 100644 --- a/lib/client/keystore.go +++ b/lib/client/keystore.go @@ -535,7 +535,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks keys.HardwareKeyService, 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.WithKeyInfo(idx.ProxyHost)) if err != nil { if trace.IsNotFound(err) { if _, statErr := os.Stat(fs.tlsCertPathLegacy(idx)); statErr == nil { @@ -546,7 +546,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks keys.HardwareKeyService, 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.WithKeyInfo(idx.ProxyHost)) if err != nil { return nil, trace.ConvertSystemError(err) } @@ -569,7 +569,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks keys.HardwareKeyService, } func (fs *FSKeyStore) updateKeyRingWithCerts(o CertOption, hwks keys.HardwareKeyService, keyRing *KeyRing) error { - return trace.Wrap(o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, keys.WithHardwareKeyService(hwks))) + return trace.Wrap(o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, keys.WithHardwareKeyService(hwks), keys.WithKeyInfo(keyRing.ProxyHost))) } // GetSSHCertificates gets all certificates signed for the given user and proxy. diff --git a/lib/teleterm/daemon/hardwarekeyprompt.go b/lib/teleterm/daemon/hardwarekeyprompt.go index b20426f4c86c5..4346c7e9c7d39 100644 --- a/lib/teleterm/daemon/hardwarekeyprompt.go +++ b/lib/teleterm/daemon/hardwarekeyprompt.go @@ -25,7 +25,6 @@ import ( "github.com/gravitational/teleport/api/utils/keys" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" - "github.com/gravitational/teleport/lib/teleterm/api/uri" ) // NewHardwareKeyPrompt returns a new hardware key prompt. @@ -50,14 +49,13 @@ func (s *Service) NewHardwareKeyPrompt() keys.HardwareKeyPrompt { } 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 keys.KeyInfo) error { _, err := h.s.tshdEventsClient.PromptHardwareKeyTouch(ctx, &api.PromptHardwareKeyTouchRequest{ - RootClusterUri: h.rootClusterURI.String(), + RootClusterUri: keyInfo.ProxyHost, }) if err != nil { return trace.Wrap(err) @@ -66,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 keys.PINPromptRequirement) (string, error) { +func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement keys.PINPromptRequirement, keyInfo keys.KeyInfo) (string, error) { res, err := h.s.tshdEventsClient.PromptHardwareKeyPIN(ctx, &api.PromptHardwareKeyPINRequest{ - RootClusterUri: h.rootClusterURI.String(), + RootClusterUri: keyInfo.ProxyHost, PinOptional: requirement == keys.PINOptional, }) if err != nil { @@ -80,9 +78,9 @@ func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement keys.PINPr // 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) (*keys.PINAndPUK, error) { +func (h *hardwareKeyPrompter) ChangePIN(ctx context.Context, keyInfo keys.KeyInfo) (*keys.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) @@ -95,9 +93,9 @@ func (h *hardwareKeyPrompter) ChangePIN(ctx context.Context) (*keys.PINAndPUK, e } // 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 keys.KeyInfo) (bool, error) { res, err := h.s.tshdEventsClient.ConfirmHardwareKeySlotOverwrite(ctx, &api.ConfirmHardwareKeySlotOverwriteRequest{ - RootClusterUri: h.rootClusterURI.String(), + RootClusterUri: keyInfo.ProxyHost, Message: message, }) if err != nil { diff --git a/lib/teleterm/teleterm.go b/lib/teleterm/teleterm.go index 63bcf12cb6237..8de857265ca68 100644 --- a/lib/teleterm/teleterm.go +++ b/lib/teleterm/teleterm.go @@ -81,8 +81,6 @@ func Serve(ctx context.Context, cfg Config) error { return trace.Wrap(err) } - // TODO(Joerger): add rootClusterURI, or some other identifiable information, to the prompts - // at the client store level, since the hardware key service is shared across clusters. hardwareKeyService.SetPrompt(daemonService.NewHardwareKeyPrompt()) apiServer, err := apiserver.New(apiserver.Config{ From 1dabde1db916f27f2be9d981cc2b31cb22340414 Mon Sep 17 00:00:00 2001 From: joerger Date: Tue, 11 Mar 2025 12:30:11 -0700 Subject: [PATCH 04/10] Shift piv implementation into YubiKeyPIVService, with notable alterations noted below: * Replace global *YubiKeyPrivateKey cache with a global *YubiKey cache, which supports proper cross-cluster connection caching for Connect * Replace HardwareSigner interface with HardwarePrivateKey, since the service within the key is now the interface * Add context to signatures through the context passed to the hardware key service * Only attest the key when needed (login) to avoid its performance cost --- api/client/proxy/client.go | 9 +- api/utils/keys/hardwarekey.go | 68 ++- api/utils/keys/hardwaresigner.go | 92 --- api/utils/keys/hardwaresigner_test.go | 43 -- api/utils/keys/privatekey.go | 32 + api/utils/keys/privatekey_test.go | 17 + api/utils/keys/yubikey.go | 565 ++++-------------- api/utils/keys/yubikey_common.go | 18 +- api/utils/keys/yubikey_fake.go | 84 --- api/utils/keys/yubikey_other.go | 43 -- api/utils/keys/yubikey_service.go | 322 ++++++++++ api/utils/keys/yubikey_service_fake.go | 79 +++ ...ubikey_test.go => yubikey_service_test.go} | 48 +- api/utils/keys/yubikey_unavailable.go | 46 ++ integration/proxy/teleterm_test.go | 4 +- integration/teleterm_test.go | 20 +- lib/client/api.go | 6 +- lib/client/client_store.go | 4 +- lib/client/profile.go | 5 +- lib/teleterm/daemon/daemon_test.go | 8 +- lib/teleterm/teleterm.go | 2 +- lib/vnet/profile_osconfig_provider_darwin.go | 2 +- tool/tctl/common/config/profile.go | 5 +- tool/tsh/common/tsh.go | 2 +- tool/tsh/common/vnet_client_application.go | 2 +- 25 files changed, 722 insertions(+), 804 deletions(-) delete mode 100644 api/utils/keys/hardwaresigner.go delete mode 100644 api/utils/keys/hardwaresigner_test.go delete mode 100644 api/utils/keys/yubikey_fake.go delete mode 100644 api/utils/keys/yubikey_other.go create mode 100644 api/utils/keys/yubikey_service.go create mode 100644 api/utils/keys/yubikey_service_fake.go rename api/utils/keys/{yubikey_test.go => yubikey_service_test.go} (73%) create mode 100644 api/utils/keys/yubikey_unavailable.go diff --git a/api/client/proxy/client.go b/api/client/proxy/client.go index f1d1b05540ec4..da6846822a5bc 100644 --- a/api/client/proxy/client.go +++ b/api/client/proxy/client.go @@ -38,6 +38,7 @@ import ( transportv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/transport/v1" "github.com/gravitational/teleport/api/metadata" "github.com/gravitational/teleport/api/utils/grpc/interceptors" + "github.com/gravitational/teleport/api/utils/keys" ) // ClientConfig contains configuration needed for a Client @@ -124,7 +125,7 @@ func (c *ClientConfig) CheckAndSetDefaults(ctx context.Context) error { // before initiating the gRPC dial. // This approach works because the connection is cached for a few seconds, // allowing subsequent calls without requiring additional user action. - if priv, ok := cert.PrivateKey.(hardwareKeyWarmer); ok { + if priv, ok := cert.PrivateKey.(*keys.PrivateKey); ok { err := priv.WarmupHardwareKey(ctx) if err != nil { return nil, trace.Wrap(err) @@ -454,9 +455,3 @@ func (c *Client) Ping(ctx context.Context) error { _, _ = c.transport.ClusterDetails(ctx) return nil } - -// hardwareKeyWarmer performs a bogus call to the hardware key, -// to proactively prompt the user for a PIN/touch (if needed). -type hardwareKeyWarmer interface { - WarmupHardwareKey(ctx context.Context) error -} diff --git a/api/utils/keys/hardwarekey.go b/api/utils/keys/hardwarekey.go index edd8898c133d1..33706eafd1b95 100644 --- a/api/utils/keys/hardwarekey.go +++ b/api/utils/keys/hardwarekey.go @@ -15,19 +15,20 @@ package keys import ( + "bytes" "context" "crypto" + "crypto/rand" + "crypto/sha512" "crypto/x509" "encoding/json" "io" + "github.com/gogo/protobuf/jsonpb" "github.com/gravitational/trace" -) -// TODO: replace with PIV implementation -func NewHardwareKeyService(prompt HardwareKeyPrompt) HardwareKeyService { - return nil -} + attestationv1 "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" +) // HardwareKeyService is an interface for interacting with hardware private keys. type HardwareKeyService interface { @@ -37,7 +38,7 @@ type HardwareKeyService interface { NewPrivateKey(ctx context.Context, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) // Sign performs a cryptographic signature using the specified hardware // private key and provided signature parameters. - Sign(ref *HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) + Sign(ctx context.Context, ref *HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) // SetPrompt sets the hardware key prompt used by the hardware key service, if applicable. // This is used by Teleport Connect which sets the prompt later than the hardware key service, // due to process initialization constraints. @@ -48,6 +49,8 @@ type HardwareKeyService interface { type HardwarePrivateKey struct { service HardwareKeyService ref *HardwarePrivateKeyRef + // keyInfo contains additional key info which may be used to add context to prompts, + // such as the name of the Teleport user using the key. keyInfo KeyInfo } @@ -58,7 +61,8 @@ func (h *HardwarePrivateKey) Public() crypto.PublicKey { // Sign implements [crypto.Signer]. func (h *HardwarePrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { - return h.service.Sign(h.ref, rand, digest, opts) + // When context.TODO() is passed, the service should replace this with its own parent context. + return h.service.Sign(context.TODO(), h.ref, rand, digest, opts) } // GetAttestation returns the hardware private key attestation details. @@ -71,6 +75,20 @@ func (h *HardwarePrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy { return h.ref.PrivateKeyPolicy } +// WarmupHardwareKey performs a bogus sign() call to prompt the user for PIN/touch (if needed). +func (h *HardwarePrivateKey) WarmupHardwareKey(ctx context.Context) error { + if !h.ref.PrivateKeyPolicy.IsHardwareKeyPINVerified() && !h.ref.PrivateKeyPolicy.IsHardwareKeyTouchVerified() { + return nil + } + + // ed25519 keys only support sha512 hashing, or no hashing. Currently we don't support + // ed25519 hardware keys outside of the fake "pivtest" service, but we may extend support in + // the future as newer keys are being made with ed25519 support (YubiKey 5.7.x, SoloKey). + hash := sha512.Sum512(make([]byte, 512)) + _, err := h.service.Sign(ctx, h.ref, rand.Reader, hash[:], crypto.SHA512) + return trace.Wrap(err, "failed to perform warmup signature with hardware private key") +} + // HardwarePrivateKeyRef references a specific hardware private key. type HardwarePrivateKeyRef struct { // SerialNumber is the hardware key's serial number. @@ -143,3 +161,39 @@ func (r *HardwarePrivateKeyRef) UnmarshalJSON(b []byte) error { *r = HardwarePrivateKeyRef(ref.refAlias) return nil } + +// AttestationStatement is an attestation statement for a hardware private key +// that supports json marshaling through the standard json/encoding package. +type AttestationStatement attestationv1.AttestationStatement + +// ToProto converts this AttestationStatement to its protobuf form. +func (ar *AttestationStatement) ToProto() *attestationv1.AttestationStatement { + return (*attestationv1.AttestationStatement)(ar) +} + +// AttestationStatementFromProto converts an AttestationStatement from its protobuf form. +func AttestationStatementFromProto(att *attestationv1.AttestationStatement) *AttestationStatement { + return (*AttestationStatement)(att) +} + +// MarshalJSON implements custom protobuf json marshaling. +func (ar *AttestationStatement) MarshalJSON() ([]byte, error) { + buf := new(bytes.Buffer) + err := (&jsonpb.Marshaler{}).Marshal(buf, ar.ToProto()) + return buf.Bytes(), trace.Wrap(err) +} + +// UnmarshalJSON implements custom protobuf json unmarshaling. +func (ar *AttestationStatement) UnmarshalJSON(buf []byte) error { + return jsonpb.Unmarshal(bytes.NewReader(buf), ar.ToProto()) +} + +// AttestationData is verified attestation data for a public key. +type AttestationData struct { + // PublicKeyDER is the public key in PKIX, ASN.1 DER form. + PublicKeyDER []byte `json:"public_key"` + // PrivateKeyPolicy specifies the private key policy supported by the associated private key. + PrivateKeyPolicy PrivateKeyPolicy `json:"private_key_policy"` + // SerialNumber is the serial number of the Attested hardware key. + SerialNumber uint32 `json:"serial_number"` +} diff --git a/api/utils/keys/hardwaresigner.go b/api/utils/keys/hardwaresigner.go deleted file mode 100644 index d1bbc73ab5d18..0000000000000 --- a/api/utils/keys/hardwaresigner.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2022 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 keys - -import ( - "bytes" - "crypto" - - "github.com/gogo/protobuf/jsonpb" - "github.com/gravitational/trace" - - attestation "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" -) - -// HardwareSigner is a crypto.Signer which can be attested as being backed by a hardware key. -// This enables the ability to enforce hardware key private key policies. -// -// TODO: Delete in favor of HardwarePrivateKey -type HardwareSigner interface { - crypto.Signer - - // GetAttestationStatement returns an AttestationStatement for this private key. - GetAttestationStatement() *AttestationStatement - - // GetPrivateKeyPolicy returns the PrivateKeyPolicy supported by this private key. - GetPrivateKeyPolicy() PrivateKeyPolicy -} - -// GetAttestationStatement returns this key's AttestationStatement. If the key is -// not a hardware-backed key, this method returns nil. -func (k *PrivateKey) GetAttestationStatement() *AttestationStatement { - if attestedPriv, ok := k.Signer.(HardwareSigner); ok { - return attestedPriv.GetAttestationStatement() - } - // Just return a nil attestation statement and let this key fail any attestation checks. - return nil -} - -// GetPrivateKeyPolicy returns this key's PrivateKeyPolicy. -func (k *PrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy { - if attestedPriv, ok := k.Signer.(HardwareSigner); ok { - return attestedPriv.GetPrivateKeyPolicy() - } - return PrivateKeyPolicyNone -} - -// AttestationStatement is an attestation statement for a hardware private key -// that supports json marshaling through the standard json/encoding package. -type AttestationStatement attestation.AttestationStatement - -// ToProto converts this AttestationStatement to its protobuf form. -func (ar *AttestationStatement) ToProto() *attestation.AttestationStatement { - return (*attestation.AttestationStatement)(ar) -} - -// AttestationStatementFromProto converts an AttestationStatement from its protobuf form. -func AttestationStatementFromProto(att *attestation.AttestationStatement) *AttestationStatement { - return (*AttestationStatement)(att) -} - -// MarshalJSON implements custom protobuf json marshaling. -func (ar *AttestationStatement) MarshalJSON() ([]byte, error) { - buf := new(bytes.Buffer) - err := (&jsonpb.Marshaler{}).Marshal(buf, ar.ToProto()) - return buf.Bytes(), trace.Wrap(err) -} - -// UnmarshalJSON implements custom protobuf json unmarshaling. -func (ar *AttestationStatement) UnmarshalJSON(buf []byte) error { - return jsonpb.Unmarshal(bytes.NewReader(buf), ar.ToProto()) -} - -// AttestationData is verified attestation data for a public key. -type AttestationData struct { - // PublicKeyDER is the public key in PKIX, ASN.1 DER form. - PublicKeyDER []byte `json:"public_key"` - // PrivateKeyPolicy specifies the private key policy supported by the associated private key. - PrivateKeyPolicy PrivateKeyPolicy `json:"private_key_policy"` - // SerialNumber is the serial number of the Attested hardware key. - SerialNumber uint32 `json:"serial_number"` -} diff --git a/api/utils/keys/hardwaresigner_test.go b/api/utils/keys/hardwaresigner_test.go deleted file mode 100644 index 48bb77f9f2a3d..0000000000000 --- a/api/utils/keys/hardwaresigner_test.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2022 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 keys_test - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/gravitational/teleport/api/utils/keys" -) - -// TestNonHardwareSigner tests the HardwareSigner interface with non-hardware keys. -// -// HardwareSigners require the piv go tag and should be tested individually in tests -// like `TestGetYubiKeyPrivateKey_Interactive`. -func TestNonHardwareSigner(t *testing.T) { - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - key, err := keys.NewPrivateKey(priv, nil) - require.NoError(t, err) - - require.Nil(t, key.GetAttestationStatement()) - require.Equal(t, keys.PrivateKeyPolicyNone, key.GetPrivateKeyPolicy()) -} diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index ff5afa1cde92c..f013e732aae0b 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -225,6 +225,38 @@ func (k *PrivateKey) SoftwarePrivateKeyPEM() ([]byte, error) { return nil, trace.BadParameter("cannot get software key PEM for private key of type %T", k.Signer) } +// GetAttestationStatement returns this key's AttestationStatement. If the key is +// not a [HardwarePrivateKey], this method returns nil. +func (k *PrivateKey) GetAttestationStatement() *AttestationStatement { + if hwpk, ok := k.Signer.(*HardwarePrivateKey); ok { + return hwpk.GetAttestationStatement() + } + // Just return a nil attestation statement and let this key fail any attestation checks. + return nil +} + +// GetPrivateKeyPolicy returns this key's PrivateKeyPolicy. +func (k *PrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy { + if hwpk, ok := k.Signer.(*HardwarePrivateKey); ok { + return hwpk.GetPrivateKeyPolicy() + } + return PrivateKeyPolicyNone +} + +// IsHardware returns true if [k] is a [HardwarePrivateKey]. +func (k *PrivateKey) IsHardware() bool { + _, ok := k.Signer.(*HardwarePrivateKey) + return ok +} + +// WarmupHardwareKey checks if this is a [HardwarePrivateKey] and warms it up if it is. +func (k *PrivateKey) WarmupHardwareKey(ctx context.Context) error { + if hwpk, ok := k.Signer.(*HardwarePrivateKey); ok { + return hwpk.WarmupHardwareKey(ctx) + } + return nil +} + // LoadPrivateKey returns the PrivateKey for the given key file. func LoadPrivateKey(keyFile string) (*PrivateKey, error) { keyPEM, err := os.ReadFile(keyFile) diff --git a/api/utils/keys/privatekey_test.go b/api/utils/keys/privatekey_test.go index 6e4759eb44a2c..36223055c34b2 100644 --- a/api/utils/keys/privatekey_test.go +++ b/api/utils/keys/privatekey_test.go @@ -18,6 +18,7 @@ package keys import ( "bytes" + "context" "crypto" "crypto/ecdsa" "crypto/ed25519" @@ -274,7 +275,23 @@ func TestX509Certificate(t *testing.T) { tc.validateResult(t, cert) }) } +} + +// TestHardwareKeyMethods tests hardware key related methods with non-hardware keys. +// +// Testing these methods with actual hardware keys requires the piv go tag and should +// be tested individually in tests like `TestGetYubiKeyPrivateKey_Interactive`. +func TestHardwareKeyMethods(t *testing.T) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + key, err := NewPrivateKey(priv, nil) + require.NoError(t, err) + require.Nil(t, key.GetAttestationStatement()) + require.Equal(t, PrivateKeyPolicyNone, key.GetPrivateKeyPolicy()) + require.False(t, key.IsHardware()) + require.NoError(t, key.WarmupHardwareKey(context.Background())) } var ( diff --git a/api/utils/keys/yubikey.go b/api/utils/keys/yubikey.go index 7b084dd3cd216..61f98a92f355f 100644 --- a/api/utils/keys/yubikey.go +++ b/api/utils/keys/yubikey.go @@ -21,13 +21,8 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - "crypto/sha256" "crypto/x509" "crypto/x509/pkix" - "encoding/hex" - "encoding/json" - "encoding/pem" - "errors" "fmt" "io" "math/big" @@ -41,268 +36,42 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/api" - attestation "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" + attestationv1 "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" "github.com/gravitational/teleport/api/utils/retryutils" ) -const ( - // PIVCardTypeYubiKey is the PIV card type assigned to yubiKeys. - PIVCardTypeYubiKey = "yubikey" -) - -// Cache keys to prevent reconnecting to PIV module to discover a known key. -// -// Additionally, this allows the program to cache the key's PIN (if applicable) -// after the user is prompted the first time, preventing redundant prompts when -// the key is retrieved multiple times. -// -// Note: in most cases the connection caches the PIN itself, and connections can be -// reclaimed before they are fully closed (within a few seconds). However, in uncommon -// setups, this PIN caching does not actually work as expected, so we handle it instead. -// See https://github.com/go-piv/piv-go/issues/47 -var ( - cachedKeys = map[piv.Slot]*PrivateKey{} - cachedKeysMu sync.Mutex -) - -// getOrGenerateYubiKeyPrivateKey connects to a connected yubiKey and gets a private key -// matching the given touch requirement. This private key will either be newly generated -// or previously generated by a Teleport client and reused. -func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy PrivateKeyPolicy, slot PIVSlot, prompt HardwareKeyPrompt) (*PrivateKey, error) { - if prompt == nil { - prompt = &CLIPrompt{} - } - cachedKeysMu.Lock() - defer cachedKeysMu.Unlock() - - // Get the default PIV slot or the piv slot requested. - pivSlot, err := GetDefaultKeySlot(requiredKeyPolicy) - if err != nil { - return nil, trace.Wrap(err) - } - if slot != "" { - pivSlot, err = slot.parse() - if err != nil { - return nil, trace.Wrap(err) - } - } - - // If the program has already retrieved and cached this key, return it. - if key, ok := cachedKeys[pivSlot]; ok && key.GetPrivateKeyPolicy() == requiredKeyPolicy { - return key, nil - } - - // Use the first yubiKey we find. - y, err := FindYubiKey(0, prompt) - if err != nil { - return nil, trace.Wrap(err) - } - - // If PIN is required, check that PIN and PUK are not the defaults. - if requiredKeyPolicy.IsHardwareKeyPINVerified() { - if err := y.checkOrSetPIN(ctx); err != nil { - return nil, trace.Wrap(err) - } - } - - promptOverwriteSlot := func(msg string) error { - promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg) - if confirmed, confirmErr := 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") - } - return nil - } - - // 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 slot == "" { - pivSlot, err = GetDefaultKeySlot(requiredKeyPolicy) - if err != nil { - return nil, trace.Wrap(err) - } - - // Check the client certificate in the slot. - switch cert, err := y.getCertificate(pivSlot); { - case err == nil && (len(cert.Subject.Organization) == 0 || cert.Subject.Organization[0] != certOrgName): - // Unknown cert found, prompt the user before we overwrite the slot. - if err := promptOverwriteSlot(nonTeleportCertificateMessage(pivSlot, cert)); err != nil { - return nil, trace.Wrap(err) - } - - // user confirmed, generate a new key. - fallthrough - case errors.Is(err, piv.ErrNotFound): - // no cert found, generate a new key. - priv, err := y.generatePrivateKeyAndCert(pivSlot, requiredKeyPolicy) - return priv, trace.Wrap(err) - case err != nil: - return nil, trace.Wrap(err) - } - } - - // Get the key in the slot, or generate a new one if needed. - priv, err := y.getPrivateKey(pivSlot) - switch { - case err == nil && !requiredKeyPolicy.IsSatisfiedBy(priv.GetPrivateKeyPolicy()): - // Key does not meet the required key policy, prompt the user before we overwrite the slot. - msg := fmt.Sprintf("private key in YubiKey PIV slot %q does not meet private key policy %q.", pivSlot, requiredKeyPolicy) - if err := promptOverwriteSlot(msg); err != nil { - return nil, trace.Wrap(err) - } - - // user confirmed, generate a new key. - fallthrough - case trace.IsNotFound(err): - // no key found, generate a new key. - priv, err = y.generatePrivateKeyAndCert(pivSlot, requiredKeyPolicy) - return priv, trace.Wrap(err) - case err != nil: - return nil, trace.Wrap(err) - } - - return priv, nil -} - -func GetDefaultKeySlot(policy PrivateKeyPolicy) (piv.Slot, error) { - switch policy { - case PrivateKeyPolicyHardwareKey: - // private_key_policy: hardware_key -> 9a - return piv.SlotAuthentication, nil - case PrivateKeyPolicyHardwareKeyTouch: - // private_key_policy: hardware_key_touch -> 9c - return piv.SlotSignature, nil - case PrivateKeyPolicyHardwareKeyTouchAndPIN: - // private_key_policy: hardware_key_touch_and_pin -> 9d - return piv.SlotKeyManagement, nil - case PrivateKeyPolicyHardwareKeyPIN: - // private_key_policy: hardware_key_pin -> 9e - return piv.SlotCardAuthentication, nil - default: - return piv.Slot{}, trace.BadParameter("unexpected private key policy %v", policy) - } -} - -func getKeyPolicies(policy PrivateKeyPolicy) (piv.TouchPolicy, piv.PINPolicy, error) { - switch policy { - case PrivateKeyPolicyHardwareKey: - return piv.TouchPolicyNever, piv.PINPolicyNever, nil - case PrivateKeyPolicyHardwareKeyTouch: - return piv.TouchPolicyCached, piv.PINPolicyNever, nil - case PrivateKeyPolicyHardwareKeyPIN: - return piv.TouchPolicyNever, piv.PINPolicyOnce, nil - case PrivateKeyPolicyHardwareKeyTouchAndPIN: - return piv.TouchPolicyCached, piv.PINPolicyOnce, nil - default: - return piv.TouchPolicyNever, piv.PINPolicyNever, trace.BadParameter("unexpected private key policy %v", policy) - } -} - -func nonTeleportCertificateMessage(slot piv.Slot, cert *x509.Certificate) string { - // Gather a small list of user-readable x509 certificate fields to display to the user. - sum := sha256.Sum256(cert.Raw) - fingerPrint := hex.EncodeToString(sum[:]) - return fmt.Sprintf(`Certificate in YubiKey PIV slot %q is not a Teleport client cert: -Slot %s: - Algorithm: %v - Subject DN: %v - Issuer DN: %v - Serial: %v - Fingerprint: %v - Not before: %v - Not after: %v -`, - slot, slot, - cert.SignatureAlgorithm, - cert.Subject, - cert.Issuer, - cert.SerialNumber, - fingerPrint, - cert.NotBefore, - cert.NotAfter, - ) -} - -// YubiKeyPrivateKey is a YubiKey PIV private key. Cryptographical operations open -// a new temporary connection to the PIV card to perform the operation. -type YubiKeyPrivateKey struct { - // YubiKey is a specific YubiKey PIV module. - *YubiKey - - pivSlot piv.Slot - signMux sync.Mutex - - slotCert *x509.Certificate - attestationCert *x509.Certificate - attestation *piv.Attestation -} - -// yubiKeyPrivateKeyData is marshalable data used to retrieve a specific yubiKey PIV private key. -type yubiKeyPrivateKeyData struct { - SerialNumber uint32 `json:"serial_number"` - SlotKey uint32 `json:"slot_key"` +// YubiKey is a specific YubiKey PIV card. +type YubiKey struct { + // conn is a shared YubiKey PIV connection. + // + // PIV connections claim an exclusive lock on the PIV module until closed. + // In order to improve connection sharing for this program without locking + // out other programs during extended program executions (like "tsh proxy ssh"), + // this connections is opportunistically formed and released after being + // unused for a few seconds. + *sharedPIVConnection + // serialNumber is the YubiKey's 8 digit serial number. + serialNumber uint32 + // version is the YubiKey's version. + version piv.Version } -func parseYubiKeyPrivateKeyData(keyDataBytes []byte, prompt HardwareKeyPrompt) (*PrivateKey, error) { - if prompt == nil { - prompt = &CLIPrompt{} - } - cachedKeysMu.Lock() - defer cachedKeysMu.Unlock() - - var keyData yubiKeyPrivateKeyData - if err := json.Unmarshal(keyDataBytes, &keyData); err != nil { - return nil, trace.Wrap(err) - } - - pivSlot, err := parsePIVSlot(keyData.SlotKey) - if err != nil { - return nil, trace.Wrap(err) - } - - // If the program has already retrieved and cached this key, return it. - if key, ok := cachedKeys[pivSlot]; ok { - return key, nil - } - - y, err := FindYubiKey(keyData.SerialNumber, prompt) - if err != nil { - return nil, trace.Wrap(err) +func newYubiKey(card string) (*YubiKey, error) { + y := &YubiKey{ + sharedPIVConnection: &sharedPIVConnection{ + card: card, + }, } - priv, err := y.getPrivateKey(pivSlot) - if err != nil { + var err error + if y.serialNumber, err = y.getSerialNumber(); err != nil { return nil, trace.Wrap(err) } - - return priv, nil -} - -// Public returns the public key corresponding to this private key. -func (y *YubiKeyPrivateKey) Public() crypto.PublicKey { - return y.slotCert.PublicKey -} - -// WarmupHardwareKey performs a bogus sign() call to prompt the user for -// a PIN/touch (if needed). -func (y *YubiKeyPrivateKey) WarmupHardwareKey(ctx context.Context) error { - hash := sha256.Sum256(make([]byte, 256)) - _, err := y.sign(ctx, rand.Reader, hash[:], crypto.SHA256) - return trace.Wrap(err, "failed to access a YubiKey private key") -} - -// Sign implements crypto.Signer. -func (y *YubiKeyPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - signature, err := y.sign(ctx, rand, digest, opts) - if err != nil { + if y.version, err = y.getVersion(); err != nil { return nil, trace.Wrap(err) } - return signature, nil + return y, nil } // YubiKeys require touch when signing with a private key that requires touch. @@ -322,12 +91,9 @@ const ( signTouchPromptDelay = time.Millisecond * 200 ) -func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { - // To prevent concurrent calls to sign from failing due to PIV only handling a - // single connection, use a lock to queue through signature requests one at a time. - y.signMux.Lock() - defer y.signMux.Unlock() +func (y *YubiKey) sign(ctx context.Context, ref *HardwarePrivateKeyRef, prompt HardwareKeyPrompt, rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { ctx, cancel := context.WithCancelCause(ctx) + defer cancel(nil) // Lock the connection for the entire duration of the sign // process. Without this, the connection will be released, @@ -340,7 +106,7 @@ func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []b defer release() var touchPromptDelayTimer *time.Timer - if y.attestation.TouchPolicy != piv.TouchPolicyNever { + if ref.PrivateKeyPolicy.IsHardwareKeyTouchVerified() { touchPromptDelayTimer = time.NewTimer(signTouchPromptDelay) defer touchPromptDelayTimer.Stop() @@ -348,7 +114,7 @@ func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []b select { case <-touchPromptDelayTimer.C: // Prompt for touch after a delay, in case the function succeeds without touch due to a cached touch. - err := y.prompt.Touch(ctx, KeyInfo{}) + err := prompt.Touch(ctx, KeyInfo{}) if err != nil { // Cancel the entire function when an error occurs. // This is typically used for aborting the prompt. @@ -370,13 +136,18 @@ func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []b defer touchPromptDelayTimer.Reset(signTouchPromptDelay) } } - pass, err := y.prompt.AskPIN(ctx, PINRequired, KeyInfo{}) + pass, err := prompt.AskPIN(ctx, PINRequired, KeyInfo{}) return pass, trace.Wrap(err) } + pinPolicy := piv.PINPolicyNever + if ref.PrivateKeyPolicy.IsHardwareKeyPINVerified() { + pinPolicy = piv.PINPolicyOnce + } + auth := piv.KeyAuth{ PINPrompt: promptPIN, - PINPolicy: y.attestation.PINPolicy, + PINPolicy: pinPolicy, } // YubiKeys with firmware version 5.3.1 have a bug where insVerify(0x20, 0x00, 0x80, nil) @@ -386,14 +157,19 @@ func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []b // the signature fails. manualRetryWithPIN := false fw531 := piv.Version{Major: 5, Minor: 3, Patch: 1} - if auth.PINPolicy == piv.PINPolicyOnce && y.attestation.Version == fw531 { + if auth.PINPolicy == piv.PINPolicyOnce && y.conn.Version() == fw531 { // Set the keys PIN policy to never to skip the insVerify check. If PIN was provided in // a previous recent call, the signature will succeed as expected of the "once" policy. auth.PINPolicy = piv.PINPolicyNever manualRetryWithPIN = true } - privateKey, err := y.privateKey(y.pivSlot, y.Public(), auth) + pivSlot, err := parsePIVSlot(ref.SlotKey) + if err != nil { + return nil, trace.Wrap(err) + } + + privateKey, err := y.privateKey(pivSlot, ref.PublicKey, auth) if err != nil { return nil, trace.Wrap(err) } @@ -458,109 +234,42 @@ func abandonableSign(ctx context.Context, signer crypto.Signer, rand io.Reader, } } -func (y *YubiKeyPrivateKey) toPrivateKey() (*PrivateKey, error) { - keyPEM, err := y.keyPEM() - if err != nil { - return nil, trace.Wrap(err) - } - - return NewPrivateKey(y, keyPEM) +// Reset resets the YubiKey PIV module to default settings. +func (y *YubiKey) Reset() error { + err := y.reset() + return trace.Wrap(err) } -func (y *YubiKeyPrivateKey) keyPEM() ([]byte, error) { - keyDataBytes, err := json.Marshal(yubiKeyPrivateKeyData{ - SerialNumber: y.serialNumber, - SlotKey: y.pivSlot.Key, - }) - if err != nil { - return nil, trace.Wrap(err) +// generatePrivateKey generates a new private key in the given PIV slot. +func (y *YubiKey) generatePrivateKey(slot piv.Slot, requiredKeyPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) { + touchPolicy := piv.TouchPolicyNever + if requiredKeyPolicy.IsHardwareKeyTouchVerified() { + touchPolicy = piv.TouchPolicyCached } - return pem.EncodeToMemory(&pem.Block{ - Type: pivYubiKeyPrivateKeyType, - Headers: nil, - Bytes: keyDataBytes, - }), nil -} - -// GetAttestationStatement returns an AttestationStatement for this YubiKeyPrivateKey. -func (y *YubiKeyPrivateKey) GetAttestationStatement() *AttestationStatement { - return &AttestationStatement{ - AttestationStatement: &attestation.AttestationStatement_YubikeyAttestationStatement{ - YubikeyAttestationStatement: &attestation.YubiKeyAttestationStatement{ - SlotCert: y.slotCert.Raw, - AttestationCert: y.attestationCert.Raw, - }, - }, + pinPolicy := piv.PINPolicyNever + if requiredKeyPolicy.IsHardwareKeyPINVerified() { + pinPolicy = piv.PINPolicyOnce } -} - -// GetPrivateKeyPolicy returns the PrivateKeyPolicy supported by this YubiKeyPrivateKey. -func (y *YubiKeyPrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy { - return GetPrivateKeyPolicyFromAttestation(y.attestation) -} -// GetPrivateKeyPolicyFromAttestation returns the PrivateKeyPolicy satisfied by the given hardware key attestation. -func GetPrivateKeyPolicyFromAttestation(att *piv.Attestation) PrivateKeyPolicy { - isTouchPolicy := att.TouchPolicy == piv.TouchPolicyCached || - att.TouchPolicy == piv.TouchPolicyAlways - - isPINPolicy := att.PINPolicy == piv.PINPolicyOnce || - att.PINPolicy == piv.PINPolicyAlways - - switch { - case isPINPolicy && isTouchPolicy: - return PrivateKeyPolicyHardwareKeyTouchAndPIN - case isPINPolicy: - return PrivateKeyPolicyHardwareKeyPIN - case isTouchPolicy: - return PrivateKeyPolicyHardwareKeyTouch - default: - return PrivateKeyPolicyHardwareKey + opts := piv.Key{ + Algorithm: piv.AlgorithmEC256, + PINPolicy: pinPolicy, + TouchPolicy: touchPolicy, } -} - -// YubiKey is a specific YubiKey PIV card. -type YubiKey struct { - // conn is a shared YubiKey PIV connection. - // - // PIV connections claim an exclusive lock on the PIV module until closed. - // In order to improve connection sharing for this program without locking - // out other programs during extended program executions (like "tsh proxy ssh"), - // this connections is opportunistically formed and released after being - // unused for a few seconds. - *sharedPIVConnection - // serialNumber is the yubiKey's 8 digit serial number. - serialNumber uint32 - prompt HardwareKeyPrompt -} -func newYubiKey(card string, prompt HardwareKeyPrompt) (*YubiKey, error) { - y := &YubiKey{ - sharedPIVConnection: &sharedPIVConnection{ - card: card, - }, - prompt: prompt, + pub, err := y.generateKey(piv.DefaultManagementKey, slot, opts) + if err != nil { + return nil, trace.Wrap(err) } - serialNumber, err := y.serial() + slotCert, err := y.attest(slot) if err != nil { return nil, trace.Wrap(err) } - y.serialNumber = serialNumber - return y, nil -} - -// Reset resets the YubiKey PIV module to default settings. -func (y *YubiKey) Reset() error { - err := y.reset() - return trace.Wrap(err) -} - -// generatePrivateKeyAndCert generates a new private key and client metadata cert in the given PIV slot. -func (y *YubiKey) generatePrivateKeyAndCert(slot piv.Slot, requiredKeyPolicy PrivateKeyPolicy) (*PrivateKey, error) { - if err := y.generatePrivateKey(slot, requiredKeyPolicy); err != nil { + attCert, err := y.attestationCertificate() + if err != nil { return nil, trace.Wrap(err) } @@ -571,7 +280,20 @@ func (y *YubiKey) generatePrivateKeyAndCert(slot piv.Slot, requiredKeyPolicy Pri return nil, trace.Wrap(err) } - return y.getPrivateKey(slot) + return &HardwarePrivateKeyRef{ + SerialNumber: y.serialNumber, + SlotKey: slot.Key, + PublicKey: pub, + PrivateKeyPolicy: requiredKeyPolicy, + AttestationStatement: &AttestationStatement{ + AttestationStatement: &attestationv1.AttestationStatement_YubikeyAttestationStatement{ + YubikeyAttestationStatement: &attestationv1.YubiKeyAttestationStatement{ + SlotCert: slotCert.Raw, + AttestationCert: attCert.Raw, + }, + }, + }, + }, nil } // SetMetadataCertificate creates a self signed certificate and stores it in the YubiKey's @@ -594,62 +316,25 @@ func (y *YubiKey) getCertificate(slot piv.Slot) (*x509.Certificate, error) { return cert, trace.Wrap(err) } -// generatePrivateKey generates a new private key in the given PIV slot. -func (y *YubiKey) generatePrivateKey(slot piv.Slot, requiredKeyPolicy PrivateKeyPolicy) error { - touchPolicy, pinPolicy, err := getKeyPolicies(requiredKeyPolicy) - if err != nil { - return trace.Wrap(err) - } - - opts := piv.Key{ - Algorithm: piv.AlgorithmEC256, - PINPolicy: pinPolicy, - TouchPolicy: touchPolicy, - } - - _, err = y.generateKey(piv.DefaultManagementKey, slot, opts) - return trace.Wrap(err) -} - -// getPrivateKey gets an existing private key from the given PIV slot. -func (y *YubiKey) getPrivateKey(slot piv.Slot) (*PrivateKey, error) { - slotCert, err := y.attest(slot) - if errors.Is(err, piv.ErrNotFound) { - return nil, trace.NotFound("private key in YubiKey PIV slot %q not found.", slot.String()) - } else if err != nil { - return nil, trace.Wrap(err) - } - - attCert, err := y.attestationCertificate() +// attestKey attests the key in the given PIV slot. +// The key's public key can be found in the returned slotCert. +func (y *YubiKey) attestKey(slot piv.Slot) (slotCert *x509.Certificate, attCert *x509.Certificate, att *piv.Attestation, err error) { + slotCert, err = y.attest(slot) if err != nil { - return nil, trace.Wrap(err) + return nil, nil, nil, trace.Wrap(err) } - attestation, err := piv.Verify(attCert, slotCert) + attCert, err = y.attestationCertificate() if err != nil { - return nil, trace.Wrap(err) + return nil, nil, nil, trace.Wrap(err) } - priv := &YubiKeyPrivateKey{ - YubiKey: y, - pivSlot: slot, - slotCert: slotCert, - attestationCert: attCert, - attestation: attestation, - } - - keyPEM, err := priv.keyPEM() + att, err = piv.Verify(attCert, slotCert) if err != nil { - return nil, trace.Wrap(err) + return nil, nil, nil, trace.Wrap(err) } - key, err := NewPrivateKey(priv, keyPEM) - if err != nil { - return nil, trace.Wrap(err) - } - - cachedKeys[slot] = key - return key, nil + return slotCert, attCert, att, nil } // SetPIN sets the YubiKey PIV PIN. This doesn't require user interaction like touch, just the correct old PIN. @@ -661,8 +346,8 @@ func (y *YubiKey) SetPIN(oldPin, newPin string) 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 (y *YubiKey) checkOrSetPIN(ctx context.Context) error { - pin, err := y.prompt.AskPIN(ctx, PINOptional, KeyInfo{}) +func (y *YubiKey) checkOrSetPIN(ctx context.Context, prompt HardwareKeyPrompt) error { + pin, err := prompt.AskPIN(ctx, PINOptional, KeyInfo{}) if err != nil { return trace.Wrap(err) } @@ -672,7 +357,7 @@ func (y *YubiKey) checkOrSetPIN(ctx context.Context) error { fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN) fallthrough case "": - if pin, err = y.setPINAndPUKFromDefault(ctx, y.prompt); err != nil { + if pin, err = y.setPINAndPUKFromDefault(ctx, prompt); err != nil { return trace.Wrap(err) } } @@ -771,7 +456,7 @@ func (c *sharedPIVConnection) privateKey(slot piv.Slot, public crypto.PublicKey, return privateKey, trace.Wrap(err) } -func (c *sharedPIVConnection) serial() (uint32, error) { +func (c *sharedPIVConnection) getSerialNumber() (uint32, error) { release, err := c.connect() if err != nil { return 0, trace.Wrap(err) @@ -781,14 +466,21 @@ func (c *sharedPIVConnection) serial() (uint32, error) { return serial, trace.Wrap(err) } +func (c *sharedPIVConnection) getVersion() (piv.Version, error) { + release, err := c.connect() + if err != nil { + return piv.Version{}, trace.Wrap(err) + } + defer release() + return c.conn.Version(), nil +} + func (c *sharedPIVConnection) reset() error { release, err := c.connect() if err != nil { return trace.Wrap(err) } defer release() - // Clear cached keys. - cachedKeys = make(map[piv.Slot]*PrivateKey) return trace.Wrap(c.conn.Reset()) } @@ -915,52 +607,6 @@ func isRetryError(err error) bool { return strings.Contains(err.Error(), retryError) } -// FindYubiKey finds a yubiKey PIV card by serial number. If no serial -// number is provided, the first yubiKey found will be returned. -func FindYubiKey(serialNumber uint32, prompt HardwareKeyPrompt) (*YubiKey, error) { - yubiKeyCards, err := findYubiKeyCards() - if err != nil { - return nil, trace.Wrap(err) - } - - if len(yubiKeyCards) == 0 { - if serialNumber != 0 { - return nil, trace.ConnectionProblem(nil, "no YubiKey device connected with serial number %d", serialNumber) - } - return nil, trace.ConnectionProblem(nil, "no YubiKey device connected") - } - - for _, card := range yubiKeyCards { - y, err := newYubiKey(card, prompt) - if err != nil { - return nil, trace.Wrap(err) - } - - if serialNumber == 0 || y.serialNumber == serialNumber { - return y, nil - } - } - - return nil, trace.ConnectionProblem(nil, "no YubiKey device connected with serial number %d", serialNumber) -} - -// findYubiKeyCards returns a list of connected yubiKey PIV card names. -func findYubiKeyCards() ([]string, error) { - cards, err := piv.Cards() - if err != nil { - return nil, trace.Wrap(err) - } - - var yubiKeyCards []string - for _, card := range cards { - if strings.Contains(strings.ToLower(card), PIVCardTypeYubiKey) { - yubiKeyCards = append(yubiKeyCards, card) - } - } - - return yubiKeyCards, nil -} - func (s PIVSlot) validate() error { _, err := s.parse() return trace.Wrap(err) @@ -1019,12 +665,3 @@ func SelfSignedMetadataCertificate(subject pkix.Name) (*x509.Certificate, error) } return cert, nil } - -// IsHardware returns true if [k] is a hardware PIV key. -func (k *PrivateKey) IsHardware() bool { - switch k.Signer.(type) { - case *YubiKeyPrivateKey: - return true - } - return false -} diff --git a/api/utils/keys/yubikey_common.go b/api/utils/keys/yubikey_common.go index 43b9f603c8a86..a24044d258997 100644 --- a/api/utils/keys/yubikey_common.go +++ b/api/utils/keys/yubikey_common.go @@ -15,10 +15,13 @@ package keys import ( "context" + "errors" "github.com/gravitational/trace" ) +var errPIVUnavailable = errors.New("PIV is unavailable in current build") + // HardwareKeyPrompt provides methods to interact with a YubiKey hardware key. type HardwareKeyPrompt interface { // AskPIN prompts the user for a PIN. @@ -57,21 +60,6 @@ type PINAndPUK struct { PUKChanged bool } -// GetYubiKeyPrivateKey attempt to retrieve a YubiKey private key matching the given hardware key policy -// from the given slot. If slot is unspecified, the default slot for the given key policy will be used. -// If the slot is empty, a new private key matching the given policy will be generated in the slot. -// - hardware_key: 9a -// - hardware_key_touch: 9c -// - hardware_key_pin: 9d -// - hardware_key_touch_pin: 9e -func GetYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot, customPrompt HardwareKeyPrompt) (*PrivateKey, error) { - priv, err := getOrGenerateYubiKeyPrivateKey(ctx, policy, slot, customPrompt) - if err != nil { - return nil, trace.Wrap(err, "failed to get a YubiKey private key") - } - return priv, nil -} - // PIVSlot is the string representation of a PIV slot. e.g. "9a". type PIVSlot string diff --git a/api/utils/keys/yubikey_fake.go b/api/utils/keys/yubikey_fake.go deleted file mode 100644 index 33be8917815d5..0000000000000 --- a/api/utils/keys/yubikey_fake.go +++ /dev/null @@ -1,84 +0,0 @@ -//go:build pivtest - -/* -Copyright 2024 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 keys - -import ( - "context" - "crypto" - "crypto/ed25519" - "crypto/rand" - "errors" - - "github.com/gravitational/trace" -) - -var errPIVUnavailable = errors.New("PIV is unavailable in current build") - -// Return a fake YubiKey private key. -func getOrGenerateYubiKeyPrivateKey(_ context.Context, policy PrivateKeyPolicy, _ PIVSlot, _ HardwareKeyPrompt) (*PrivateKey, error) { - _, priv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return nil, trace.Wrap(err) - } - - keyPEM, err := MarshalPrivateKey(priv) - if err != nil { - return nil, trace.Wrap(err) - } - - signer := &fakeYubiKeyPrivateKey{ - Signer: priv, - privateKeyPolicy: policy, - } - - return NewPrivateKey(signer, keyPEM) -} - -func parseYubiKeyPrivateKeyData(_ []byte, _ HardwareKeyPrompt) (*PrivateKey, error) { - // TODO(Joerger): add custom marshal/unmarshal logic for fakeYubiKeyPrivateKey (if necessary). - return nil, trace.Wrap(errPIVUnavailable) -} - -func (s PIVSlot) validate() error { - return trace.Wrap(errPIVUnavailable) -} - -type fakeYubiKeyPrivateKey struct { - crypto.Signer - privateKeyPolicy PrivateKeyPolicy -} - -// GetAttestationStatement returns an AttestationStatement for this private key. -func (y *fakeYubiKeyPrivateKey) GetAttestationStatement() *AttestationStatement { - // Since this is only used in tests, we will ignore the attestation statement in the end. - // We just need it to be non-nil so that it goes through the test modules implementation - // of AttestHardwareKey. - return &AttestationStatement{} -} - -// GetPrivateKeyPolicy returns the PrivateKeyPolicy supported by this private key. -func (y *fakeYubiKeyPrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy { - return y.privateKeyPolicy -} - -// IsHardware returns true if [k] is a hardware PIV key. -func (k *PrivateKey) IsHardware() bool { - switch k.Signer.(type) { - case *fakeYubiKeyPrivateKey: - return true - } - return false -} diff --git a/api/utils/keys/yubikey_other.go b/api/utils/keys/yubikey_other.go deleted file mode 100644 index 77d7de29a2ea8..0000000000000 --- a/api/utils/keys/yubikey_other.go +++ /dev/null @@ -1,43 +0,0 @@ -//go:build !piv && !pivtest - -/* -Copyright 2022 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 keys - -import ( - "context" - "errors" - - "github.com/gravitational/trace" -) - -var errPIVUnavailable = errors.New("PIV is unavailable in current build") - -func getOrGenerateYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot, _ HardwareKeyPrompt) (*PrivateKey, error) { - return nil, trace.Wrap(errPIVUnavailable) -} - -func parseYubiKeyPrivateKeyData(keyDataBytes []byte, _ HardwareKeyPrompt) (*PrivateKey, error) { - return nil, trace.Wrap(errPIVUnavailable) -} - -func (s PIVSlot) validate() error { - return trace.Wrap(errPIVUnavailable) -} - -// IsHardware returns true if [k] is a hardware PIV key. -func (k *PrivateKey) IsHardware() bool { - // Built without PIV support - this must be false. - return false -} diff --git a/api/utils/keys/yubikey_service.go b/api/utils/keys/yubikey_service.go new file mode 100644 index 0000000000000..b6f3b18f72ca1 --- /dev/null +++ b/api/utils/keys/yubikey_service.go @@ -0,0 +1,322 @@ +//go:build piv && !pivtest + +// 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 keys + +import ( + "context" + "crypto" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" + "sync" + + "github.com/go-piv/piv-go/piv" + "github.com/gravitational/trace" + + attestationv1 "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" +) + +// The PIV daemon only allows a single PC/SC transaction (connection) at a time, +// so we cache the YubiKey connection for re-use across the process. +// +// TODO(Joerger): Rather than using a global cache, clients should be updated to +// create a single YubiKeyService and ensure it is reused across the program +// execution. +var ( + yubiKeys map[uint32]*YubiKey = map[uint32]*YubiKey{} + yubiKeysMux sync.Mutex +) + +type YubiKeyPIVService struct { + prompt HardwareKeyPrompt + + // ctx is provided to signature requests, since `crypto.Sign` does have + // context support directly. + ctx context.Context + + // TODO: do we need sign mutex to ensure signature requests are queued through without over-prompting? + // Should this logic go ino the hardware key prompt itself? Maybe even sync.Cond? + signMux sync.Mutex +} + +func NewYubiKeyPIVService(ctx context.Context, prompt HardwareKeyPrompt) HardwareKeyService { + if ctx == nil { + ctx = context.Background() + } + + if prompt == nil { + prompt = &CLIPrompt{} + } + + return &YubiKeyPIVService{ + ctx: ctx, + prompt: prompt, + } +} + +func (s *YubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) { + // Use the first yubiKey we find. + y, err := s.getYubiKey(0) + if err != nil { + return nil, trace.Wrap(err) + } + + // Get the requested or default PIV slot. + var pivSlot piv.Slot + if customSlot != "" { + pivSlot, err = customSlot.parse() + } else { + pivSlot, err = GetDefaultKeySlot(requiredPolicy) + } + if err != nil { + return nil, trace.Wrap(err) + } + + // If PIN is required, check that PIN and PUK are not the defaults. + if requiredPolicy.IsHardwareKeyPINVerified() { + if err := y.checkOrSetPIN(ctx, s.prompt); err != nil { + return nil, trace.Wrap(err) + } + } + + // 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 customSlot == "" { + // Check the client certificate in the slot. + switch cert, err := y.getCertificate(pivSlot); { + case err == nil && (len(cert.Subject.Organization) == 0 || cert.Subject.Organization[0] != certOrgName): + // Unknown cert found, prompt the user before we overwrite the slot. + if err := s.promptOverwriteSlot(ctx, nonTeleportCertificateMessage(pivSlot, cert), KeyInfo{}); err != nil { + return nil, trace.Wrap(err) + } + return y.generatePrivateKey(pivSlot, requiredPolicy) + case errors.Is(err, piv.ErrNotFound): + return y.generatePrivateKey(pivSlot, requiredPolicy) + case err != nil: + return nil, trace.Wrap(err) + } + } + + // Check for an existing key in the slot that satisfies the required private + // key policy, or generate a new one if needed. + slotCert, attCert, att, err := y.attestKey(pivSlot) + keyPolicy := GetPrivateKeyPolicyFromAttestation(att) + switch { + case err == nil && !requiredPolicy.IsSatisfiedBy(keyPolicy): + // Key does not meet the required key policy, prompt the user before we overwrite the slot. + msg := fmt.Sprintf("private key in YubiKey PIV slot %q does not meet private key policy %q.", pivSlot, requiredPolicy) + if err := s.promptOverwriteSlot(ctx, msg, KeyInfo{}); err != nil { + return nil, trace.Wrap(err) + } + return y.generatePrivateKey(pivSlot, requiredPolicy) + case errors.Is(err, piv.ErrNotFound): + return y.generatePrivateKey(pivSlot, requiredPolicy) + case err != nil: + return nil, trace.Wrap(err) + } + + return &HardwarePrivateKeyRef{ + SerialNumber: y.serialNumber, + SlotKey: pivSlot.Key, + PublicKey: slotCert.PublicKey, + PrivateKeyPolicy: keyPolicy, + AttestationStatement: &AttestationStatement{ + AttestationStatement: &attestationv1.AttestationStatement_YubikeyAttestationStatement{ + YubikeyAttestationStatement: &attestationv1.YubiKeyAttestationStatement{ + SlotCert: slotCert.Raw, + AttestationCert: attCert.Raw, + }, + }, + }, + }, nil +} + +func (s *YubiKeyPIVService) promptOverwriteSlot(ctx context.Context, msg string, keyInfo KeyInfo) error { + promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg) + if confirmed, confirmErr := s.prompt.ConfirmSlotOverwrite(ctx, promptQuestion, keyInfo); confirmErr != nil { + return trace.Wrap(confirmErr) + } else if !confirmed { + return trace.Wrap(trace.CompareFailed(msg), "user declined to overwrite slot") + } + return nil +} + +// Sign performs a cryptographic signature using the specified hardware +// private key and provided signature parameters. +func (s *YubiKeyPIVService) Sign(ctx context.Context, ref *HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + // Usually, Sign will be called without context through the [crypto.Signer] interface, + // so we opportunistically set the context. + if ctx == context.TODO() { + ctx = s.ctx + } + + // To prevent concurrent calls to sign from failing due to PIV only handling a + // single connection, use a lock to queue through signature requests one at a time. + s.signMux.Lock() + defer s.signMux.Unlock() + + y, err := s.getYubiKey(ref.SerialNumber) + if err != nil { + return nil, trace.Wrap(err) + } + + return y.sign(ctx, ref, s.prompt, rand, digest, opts) +} + +func (s *YubiKeyPIVService) SetPrompt(prompt HardwareKeyPrompt) { + s.prompt = prompt +} + +// Get the given YubiKey with the serial number. If the provided serialNumber is "0", +// return the first YubiKey found in the smart card list. +func (s *YubiKeyPIVService) getYubiKey(serialNumber uint32) (*YubiKey, error) { + yubiKeysMux.Lock() + defer yubiKeysMux.Unlock() + + if y, ok := yubiKeys[serialNumber]; ok { + return y, nil + } + + y, err := FindYubiKey(serialNumber) + if err != nil { + return nil, trace.Wrap(err) + } + + yubiKeys[y.serialNumber] = y + return y, nil +} + +// FindYubiKey finds a yubiKey PIV card by serial number. If no serial +// number is provided, the first yubiKey found will be returned. +func FindYubiKey(serialNumber uint32) (*YubiKey, error) { + yubiKeyCards, err := findYubiKeyCards() + if err != nil { + return nil, trace.Wrap(err) + } + + if len(yubiKeyCards) == 0 { + if serialNumber != 0 { + return nil, trace.ConnectionProblem(nil, "no YubiKey device connected with serial number %d", serialNumber) + } + return nil, trace.ConnectionProblem(nil, "no YubiKey device connected") + } + + for _, card := range yubiKeyCards { + y, err := newYubiKey(card) + if err != nil { + return nil, trace.Wrap(err) + } + + if serialNumber == 0 || y.serialNumber == serialNumber { + return y, nil + } + } + + return nil, trace.ConnectionProblem(nil, "no YubiKey device connected with serial number %d", serialNumber) +} + +// PIVCardTypeYubiKey is the PIV card type assigned to yubiKeys. +const PIVCardTypeYubiKey = "yubikey" + +// findYubiKeyCards returns a list of connected yubiKey PIV card names. +func findYubiKeyCards() ([]string, error) { + cards, err := piv.Cards() + if err != nil { + return nil, trace.Wrap(err) + } + + var yubiKeyCards []string + for _, card := range cards { + if strings.Contains(strings.ToLower(card), PIVCardTypeYubiKey) { + yubiKeyCards = append(yubiKeyCards, card) + } + } + + return yubiKeyCards, nil +} + +func GetDefaultKeySlot(policy PrivateKeyPolicy) (piv.Slot, error) { + switch policy { + case PrivateKeyPolicyHardwareKey: + // private_key_policy: hardware_key -> 9a + return piv.SlotAuthentication, nil + case PrivateKeyPolicyHardwareKeyTouch: + // private_key_policy: hardware_key_touch -> 9c + return piv.SlotSignature, nil + case PrivateKeyPolicyHardwareKeyTouchAndPIN: + // private_key_policy: hardware_key_touch_and_pin -> 9d + return piv.SlotKeyManagement, nil + case PrivateKeyPolicyHardwareKeyPIN: + // private_key_policy: hardware_key_pin -> 9e + return piv.SlotCardAuthentication, nil + default: + return piv.Slot{}, trace.BadParameter("unexpected private key policy %v", policy) + } +} + +// GetPrivateKeyPolicyFromAttestation returns the PrivateKeyPolicy satisfied by the given hardware key attestation. +func GetPrivateKeyPolicyFromAttestation(att *piv.Attestation) PrivateKeyPolicy { + if att == nil { + return PrivateKeyPolicyNone + } + + isTouchPolicy := att.TouchPolicy == piv.TouchPolicyCached || + att.TouchPolicy == piv.TouchPolicyAlways + + isPINPolicy := att.PINPolicy == piv.PINPolicyOnce || + att.PINPolicy == piv.PINPolicyAlways + + switch { + case isPINPolicy && isTouchPolicy: + return PrivateKeyPolicyHardwareKeyTouchAndPIN + case isPINPolicy: + return PrivateKeyPolicyHardwareKeyPIN + case isTouchPolicy: + return PrivateKeyPolicyHardwareKeyTouch + default: + return PrivateKeyPolicyHardwareKey + } +} + +func nonTeleportCertificateMessage(slot piv.Slot, cert *x509.Certificate) string { + // Gather a small list of user-readable x509 certificate fields to display to the user. + sum := sha256.Sum256(cert.Raw) + fingerPrint := hex.EncodeToString(sum[:]) + return fmt.Sprintf(`Certificate in YubiKey PIV slot %q is not a Teleport client cert: +Slot %s: + Algorithm: %v + Subject DN: %v + Issuer DN: %v + Serial: %v + Fingerprint: %v + Not before: %v + Not after: %v +`, + slot, slot, + cert.SignatureAlgorithm, + cert.Subject, + cert.Issuer, + cert.SerialNumber, + fingerPrint, + cert.NotBefore, + cert.NotAfter, + ) +} diff --git a/api/utils/keys/yubikey_service_fake.go b/api/utils/keys/yubikey_service_fake.go new file mode 100644 index 0000000000000..4f0b68518bd0f --- /dev/null +++ b/api/utils/keys/yubikey_service_fake.go @@ -0,0 +1,79 @@ +//go:build pivtest + +// 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 keys + +import ( + "context" + "crypto" + "crypto/ed25519" + "crypto/rand" + "io" + + "github.com/gravitational/trace" +) + +type fakeYubiKeyPIVService struct { + // TODO(Joerger): TestHardwareKeyLogin fails because the hardware key service is not being + // reused from login -> use, resulting in the key not being found. Rather than introducing + // a global key map, ensure that the hardware key service is set from a shared call stack. + keys map[crypto.PublicKey]crypto.Signer +} + +func NewYubiKeyPIVService(ctx context.Context, _ HardwareKeyPrompt) HardwareKeyService { + return &fakeYubiKeyPIVService{ + keys: map[crypto.PublicKey]crypto.Signer{}, + } +} + +func (s *fakeYubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, trace.Wrap(err) + } + + s.keys[string(pub)] = priv + + return &HardwarePrivateKeyRef{ + PrivateKeyPolicy: requiredPolicy, + PublicKey: pub, + // Since this is only used in tests, we will ignore the attestation statement in the end. + // We just need it to be non-nil so that it goes through the test modules implementation + // of AttestHardwareKey. + AttestationStatement: &AttestationStatement{}, + }, nil +} + +// Sign performs a cryptographic signature using the specified hardware +// private key and provided signature parameters. +func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + ed25519Pub, ok := ref.PublicKey.(ed25519.PublicKey) + if !ok { + return nil, trace.BadParameter("expected public key of type %T", ed25519.PublicKey{}) + } + priv, ok := s.keys[string(ed25519Pub)] + if !ok { + return nil, trace.NotFound("key not found") + } + + return priv.Sign(rand, digest, opts) +} + +func (s *fakeYubiKeyPIVService) SetPrompt(prompt HardwareKeyPrompt) {} + +func (s PIVSlot) validate() error { + return trace.Wrap(errPIVUnavailable) +} diff --git a/api/utils/keys/yubikey_test.go b/api/utils/keys/yubikey_service_test.go similarity index 73% rename from api/utils/keys/yubikey_test.go rename to api/utils/keys/yubikey_service_test.go index 72ac01537041c..9cca620cae0b9 100644 --- a/api/utils/keys/yubikey_test.go +++ b/api/utils/keys/yubikey_service_test.go @@ -17,7 +17,6 @@ package keys_test import ( "context" - "crypto/rand" "crypto/x509/pkix" "fmt" "os" @@ -33,8 +32,7 @@ import ( // TestGetYubiKeyPrivateKey_Interactive tests generation and retrieval of YubiKey private keys. func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { - // This test expects a yubiKey to be connected with default PIV - // settings and will overwrite any PIV data on the yubiKey. + // This test will overwrite any PIV data on the yubiKey. if os.Getenv("TELEPORT_TEST_YUBIKEY_PIV") == "" { t.Skipf("Skipping TestGenerateYubiKeyPrivateKey because TELEPORT_TEST_YUBIKEY_PIV is not set") } @@ -44,13 +42,23 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { } fmt.Println("This test is interactive, tap your YubiKey when prompted.") - ctx := context.Background() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + s := keys.NewYubiKeyPIVService(ctx, &keys.CLIPrompt{}) - y, err := keys.FindYubiKey(0, nil) + y, err := keys.FindYubiKey(0) require.NoError(t, err) + resetYubikey(t, y) t.Cleanup(func() { resetYubikey(t, y) }) + // Warmup the hardware key to prompt touch at the start of the test, + // rather than having this interaction later. + priv, err := keys.NewHardwarePrivateKey(ctx, s, "", keys.PrivateKeyPolicyHardwareKeyTouch, keys.KeyInfo{}) + require.NoError(t, err) + require.Nil(t, priv.WarmupHardwareKey(ctx)) + for _, policy := range []keys.PrivateKeyPolicy{ keys.PrivateKeyPolicyHardwareKey, keys.PrivateKeyPolicyHardwareKeyTouch, @@ -68,26 +76,25 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { slot = "9a" } - // GetYubiKeyPrivateKey should generate a new YubiKeyPrivateKey. - priv, err := keys.GetYubiKeyPrivateKey(ctx, policy, slot, nil) + // NewHardwarePrivateKey should generate a new hardware private key. + priv, err := keys.NewHardwarePrivateKey(ctx, s, slot, policy, keys.KeyInfo{}) require.NoError(t, err) // test HardwareSigner methods require.Equal(t, policy, priv.GetPrivateKeyPolicy()) require.NotNil(t, priv.GetAttestationStatement()) + require.True(t, priv.IsHardware()) - // Test Sign. - digest := []byte{100} - _, err = priv.Sign(rand.Reader, digest, nil) - require.NoError(t, err) + // Test bogus sign (warmup). + require.Nil(t, priv.WarmupHardwareKey(ctx)) - // Another call to GetYubiKeyPrivateKey should retrieve the previously generated key. - retrievePriv, err := keys.GetYubiKeyPrivateKey(ctx, policy, slot, nil) + // Another call to NewHardwarePrivateKey should retrieve the previously generated key. + retrievePriv, err := keys.NewHardwarePrivateKey(ctx, s, slot, policy, keys.KeyInfo{}) require.NoError(t, err) require.Equal(t, priv.Public(), retrievePriv.Public()) // parsing the key's private key PEM should produce the same key as well. - retrievePriv, err = keys.ParsePrivateKey(priv.PrivateKeyPEM()) + retrievePriv, err = keys.ParsePrivateKey(priv.PrivateKeyPEM(), keys.WithHardwareKeyService(s)) require.NoError(t, err) require.Equal(t, priv.Public(), retrievePriv.Public()) }) @@ -97,17 +104,18 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { } func TestOverwritePrompt(t *testing.T) { - // This test expects a yubiKey to be connected with default PIV - // settings and will overwrite any PIV data on the yubiKey. + // This test will overwrite any PIV data on the yubiKey. if os.Getenv("TELEPORT_TEST_YUBIKEY_PIV") == "" { t.Skipf("Skipping TestGenerateYubiKeyPrivateKey because TELEPORT_TEST_YUBIKEY_PIV is not set") } ctx := context.Background() + s := keys.NewYubiKeyPIVService(ctx, &keys.CLIPrompt{}) - y, err := keys.FindYubiKey(0, nil) + y, err := keys.FindYubiKey(0) require.NoError(t, err) + resetYubikey(t, y) t.Cleanup(func() { resetYubikey(t, y) }) // Use a custom slot. @@ -117,12 +125,12 @@ func TestOverwritePrompt(t *testing.T) { testOverwritePrompt := func(t *testing.T) { // Fail to overwrite slot when user denies prompt.SetStdin(prompt.NewFakeReader().AddString("n")) - _, err := keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKeyTouch, "" /* slot */, nil) + _, err := keys.NewHardwarePrivateKey(ctx, s, "" /*slot*/, keys.PrivateKeyPolicyHardwareKeyTouch, keys.KeyInfo{}) require.True(t, trace.IsCompareFailed(err), "Expected compare failed error but got %v", err) // Successfully overwrite slot when user accepts prompt.SetStdin(prompt.NewFakeReader().AddString("y")) - _, err = keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKeyTouch, "" /* slot */, nil) + _, err = keys.NewHardwarePrivateKey(ctx, s, "" /*slot*/, keys.PrivateKeyPolicyHardwareKeyTouch, keys.KeyInfo{}) require.NoError(t, err) } @@ -140,7 +148,7 @@ func TestOverwritePrompt(t *testing.T) { resetYubikey(t, y) // Generate a key that does not require touch in the slot that Teleport expects to require touch. - _, err := keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKey, keys.PIVSlot(pivSlot.String()), nil) + _, err := keys.NewHardwarePrivateKey(ctx, s, keys.PIVSlot(pivSlot.String()), keys.PrivateKeyPolicyHardwareKey, keys.KeyInfo{}) require.NoError(t, err) testOverwritePrompt(t) diff --git a/api/utils/keys/yubikey_unavailable.go b/api/utils/keys/yubikey_unavailable.go new file mode 100644 index 0000000000000..2b3b6bf817a3a --- /dev/null +++ b/api/utils/keys/yubikey_unavailable.go @@ -0,0 +1,46 @@ +//go:build !piv && !pivtest + +/* +Copyright 2024 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 keys + +import ( + "context" + "crypto" + "io" + + "github.com/gravitational/trace" +) + +func NewYubiKeyPIVService(ctx context.Context, _ HardwareKeyPrompt) HardwareKeyService { + return &unavailableYubiKeyPIVService{} +} + +type unavailableYubiKeyPIVService struct{} + +func (s *unavailableYubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) { + return nil, trace.Wrap(errPIVUnavailable) +} + +// Sign performs a cryptographic signature using the specified hardware +// private key and provided signature parameters. +func (s *unavailableYubiKeyPIVService) Sign(ctx context.Context, ref HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + return nil, trace.Wrap(errPIVUnavailable) +} + +func (s *unavailableYubiKeyPIVService) SetPrompt(prompt HardwareKeyPrompt) {} + +func (_ PIVSlot) validate() error { + return trace.Wrap(errPIVUnavailable) +} diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index a33cf4cce23a2..3eb7365d2d99c 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -248,7 +248,7 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer // db cert has expired. Clock: fakeClock, WebauthnLogin: webauthnLogin, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -880,7 +880,7 @@ func testTeletermAppGatewayTargetPortValidation(t *testing.T, pack *appaccess.Pa storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) daemonService, err := daemon.New(daemon.Config{ diff --git a/integration/teleterm_test.go b/integration/teleterm_test.go index ea9026491f1f6..671c7e2ad1ba3 100644 --- a/integration/teleterm_test.go +++ b/integration/teleterm_test.go @@ -258,7 +258,7 @@ func testAddingRootCluster(t *testing.T, pack *dbhelpers.DatabasePack, creds *he storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) @@ -291,7 +291,7 @@ func testListRootClustersReturnsLoggedInUser(t *testing.T, pack *dbhelpers.Datab storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) @@ -374,7 +374,7 @@ func testGetClusterReturnsPropertiesFromAuthServer(t *testing.T, pack *dbhelpers storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) @@ -427,7 +427,7 @@ func testHeadlessWatcher(t *testing.T, pack *dbhelpers.DatabasePack, creds *help storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -496,7 +496,7 @@ func testClientCache(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers. Dir: tc.KeysDir, Clock: storageFakeClock, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -756,7 +756,7 @@ func testCreateConnectMyComputerRole(t *testing.T, pack *dbhelpers.DatabasePack) storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -873,7 +873,7 @@ func testCreateConnectMyComputerToken(t *testing.T, pack *dbhelpers.DatabasePack InsecureSkipVerify: tc.InsecureSkipVerify, Clock: fakeClock, WebauthnLogin: webauthnLogin, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -934,7 +934,7 @@ func testWaitForConnectMyComputerNodeJoin(t *testing.T, pack *dbhelpers.Database storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -1019,7 +1019,7 @@ func testDeleteConnectMyComputerNode(t *testing.T, pack *dbhelpers.DatabasePack) storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -1247,7 +1247,7 @@ func testListDatabaseUsers(t *testing.T, pack *dbhelpers.DatabasePack) { storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), }) require.NoError(t, err) diff --git a/lib/client/api.go b/lib/client/api.go index 814035c14dfa1..24377db04ff4a 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -1281,7 +1281,7 @@ func NewClient(c *Config) (tc *TeleportClient, err error) { } else { // TODO (Joerger): init hardware key service (and client store) earlier where it can // be properly shared. - hardwareKeyService := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + hardwareKeyService := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) tc.ClientStore = NewFSClientStore(c.KeysDir, hardwareKeyService) if c.AddKeysToAgent == AddKeysToAgentOnly { // Store client keys in memory, but still save trusted certs and profile to disk. @@ -3997,7 +3997,9 @@ func (tc *TeleportClient) GetNewLoginKeyRing(ctx context.Context) (keyRing *KeyR if tc.PIVSlot != "" { log.DebugContext(ctx, "Using PIV slot specified by client or server settings", "piv_slot", tc.PIVSlot) } - priv, err := tc.ClientStore.NewHardwarePrivateKey(ctx, tc.PIVSlot, tc.PrivateKeyPolicy) + priv, err := tc.ClientStore.NewHardwarePrivateKey(ctx, tc.PIVSlot, tc.PrivateKeyPolicy, keys.KeyInfo{ + ProxyHost: tc.WebProxyHost(), + }) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/client_store.go b/lib/client/client_store.go index 7eb44750630f5..3c3b2e82d0714 100644 --- a/lib/client/client_store.go +++ b/lib/client/client_store.go @@ -63,8 +63,8 @@ func NewFSClientStore(dirPath string, hwKeyService keys.HardwareKeyService) *Sto } } -func (s *Store) NewHardwarePrivateKey(ctx context.Context, customSlot keys.PIVSlot, requiredPolicy keys.PrivateKeyPolicy) (*keys.PrivateKey, error) { - return keys.NewHardwarePrivateKey(ctx, s.hwKeyService, customSlot, requiredPolicy) +func (s *Store) NewHardwarePrivateKey(ctx context.Context, customSlot keys.PIVSlot, requiredPolicy keys.PrivateKeyPolicy, keyInfo keys.KeyInfo) (*keys.PrivateKey, error) { + return keys.NewHardwarePrivateKey(ctx, s.hwKeyService, customSlot, requiredPolicy, keyInfo) } // NewMemClientStore initializes a new in-memory client store. diff --git a/lib/client/profile.go b/lib/client/profile.go index 7a8902825363f..3dd48e53da17b 100644 --- a/lib/client/profile.go +++ b/lib/client/profile.go @@ -597,8 +597,7 @@ func (p *ProfileStatus) DatabasesForCluster(clusterName string) ([]tlsca.RouteTo Username: p.Username, ClusterName: clusterName, } - - hwks := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + hwks := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) store := NewFSKeyStore(p.Dir) keyRing, err := store.GetKeyRing(idx, hwks, WithDBCerts{}) if err != nil { @@ -620,7 +619,7 @@ func (p *ProfileStatus) AppsForCluster(clusterName string) ([]tlsca.RouteToApp, ClusterName: clusterName, } - hwks := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + hwks := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) store := NewFSKeyStore(p.Dir) keyRing, err := store.GetKeyRing(idx, hwks, WithAppCerts{}) if err != nil { diff --git a/lib/teleterm/daemon/daemon_test.go b/lib/teleterm/daemon/daemon_test.go index f1d04c090a8e1..b40cbb08859de 100644 --- a/lib/teleterm/daemon/daemon_test.go +++ b/lib/teleterm/daemon/daemon_test.go @@ -348,7 +348,7 @@ func TestUpdateTshdEventsServerAddress(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: homeDir, InsecureSkipVerify: true, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) @@ -383,7 +383,7 @@ func TestUpdateTshdEventsServerAddress_CredsErr(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: homeDir, InsecureSkipVerify: true, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) @@ -485,7 +485,7 @@ func TestRetryWithRelogin(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -539,7 +539,7 @@ func TestConcurrentHeadlessAuthPrompts(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyService: keys.NewHardwareKeyService(nil /*prompt*/), + HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), }) require.NoError(t, err) diff --git a/lib/teleterm/teleterm.go b/lib/teleterm/teleterm.go index 8de857265ca68..0cd6df0221139 100644 --- a/lib/teleterm/teleterm.go +++ b/lib/teleterm/teleterm.go @@ -43,7 +43,7 @@ import ( func Serve(ctx context.Context, cfg Config) error { // TODO(gzdunek): Move tshdEventsClient out of daemonService so that we can // set the prompt before creating Storage. - hardwareKeyService := keys.NewHardwareKeyService(nil /*prompt*/) + hardwareKeyService := keys.NewYubiKeyPIVService(ctx, nil /*prompt*/) if err := cfg.CheckAndSetDefaults(); err != nil { return trace.Wrap(err) diff --git a/lib/vnet/profile_osconfig_provider_darwin.go b/lib/vnet/profile_osconfig_provider_darwin.go index 2f5082b784706..0b09631b18f7c 100644 --- a/lib/vnet/profile_osconfig_provider_darwin.go +++ b/lib/vnet/profile_osconfig_provider_darwin.go @@ -60,7 +60,7 @@ func newProfileOSConfigProvider(tunName, ipv6Prefix, dnsAddr, homePath string, d return nil, trace.Wrap(err) } - hwKeyService := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + hwKeyService := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) p := &profileOSConfigProvider{ clientStore: client.NewFSClientStore(homePath, hwKeyService), diff --git a/tool/tctl/common/config/profile.go b/tool/tctl/common/config/profile.go index 325576d896860..2fde2bd5488a7 100644 --- a/tool/tctl/common/config/profile.go +++ b/tool/tctl/common/config/profile.go @@ -40,9 +40,10 @@ import ( // LoadConfigFromProfile applies config from ~/.tsh/ profile if it's present func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authclient.Config, error) { - hwKeyService := keys.NewHardwareKeyService(&keys.CLIPrompt{}) - ctx := context.TODO() + + hwKeyService := keys.NewYubiKeyPIVService(ctx, &keys.CLIPrompt{}) + proxyAddr := "" if len(ccf.AuthServerAddr) != 0 { proxyAddr = ccf.AuthServerAddr[0] diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 664ea82b84fae..198565a02cc91 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -4575,7 +4575,7 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err } func initClientStore(cf *CLIConf, proxy string) (*client.Store, error) { - hardwareKeyService := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + hardwareKeyService := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) switch { case cf.IdentityFileIn != "": diff --git a/tool/tsh/common/vnet_client_application.go b/tool/tsh/common/vnet_client_application.go index 21c5d6b57fc07..e6b1a1d150141 100644 --- a/tool/tsh/common/vnet_client_application.go +++ b/tool/tsh/common/vnet_client_application.go @@ -46,7 +46,7 @@ type vnetClientApplication struct { } func newVnetClientApplication(cf *CLIConf) (*vnetClientApplication, error) { - hwKeyService := keys.NewHardwareKeyService(&keys.CLIPrompt{}) + hwKeyService := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) clientStore := client.NewFSClientStore(cf.HomePath, hwKeyService) From a605a6b744a3b52b548e5212e1b0b95150963628 Mon Sep 17 00:00:00 2001 From: joerger Date: Thu, 13 Mar 2025 14:08:07 -0700 Subject: [PATCH 05/10] Add hardwarekey and piv packages. --- api/client/webclient/webclient.go | 3 +- api/profile/profile.go | 3 +- api/types/authentication.go | 9 +- api/utils/keys/alias.go | 23 ++ api/utils/keys/hardwarekey/attestation.go | 50 ++++ api/utils/keys/{ => hardwarekey}/cliprompt.go | 42 ++-- .../keys/{ => hardwarekey}/hardwarekey.go | 175 +++++++------ .../prompt.go} | 55 ++-- api/utils/keys/{ => piv}/yubikey.go | 113 ++++++--- api/utils/keys/{ => piv}/yubikey_service.go | 237 +++++++----------- .../keys/{ => piv}/yubikey_service_fake.go | 24 +- .../keys/{ => piv}/yubikey_service_test.go | 41 +-- .../keys/{ => piv}/yubikey_unavailable.go | 19 +- api/utils/keys/policy.go | 10 + api/utils/keys/policy_piv.go | 45 ++++ api/utils/keys/privatekey.go | 71 +++--- integration/proxy/teleterm_test.go | 6 +- integration/teleterm_test.go | 22 +- lib/auth/auth_with_roles.go | 8 +- lib/auth/github.go | 8 +- lib/client/api.go | 8 +- lib/client/client_store.go | 9 +- lib/client/identityfile/identity.go | 3 +- lib/client/keystore.go | 9 +- lib/client/profile.go | 7 +- lib/config/fileconf.go | 6 +- lib/modules/modules.go | 5 +- lib/teleterm/clusters/config.go | 4 +- lib/teleterm/daemon/daemon_test.go | 10 +- lib/teleterm/daemon/hardwarekeyprompt.go | 16 +- lib/teleterm/teleterm.go | 4 +- lib/vnet/profile_osconfig_provider_darwin.go | 5 +- tool/tctl/common/config/profile.go | 5 +- tool/tsh/common/tsh.go | 7 +- tool/tsh/common/vnet_client_application.go | 5 +- 35 files changed, 599 insertions(+), 468 deletions(-) create mode 100644 api/utils/keys/alias.go create mode 100644 api/utils/keys/hardwarekey/attestation.go rename api/utils/keys/{ => hardwarekey}/cliprompt.go (78%) rename api/utils/keys/{ => hardwarekey}/hardwarekey.go (51%) rename api/utils/keys/{yubikey_common.go => hardwarekey/prompt.go} (52%) rename api/utils/keys/{ => piv}/yubikey.go (87%) rename api/utils/keys/{ => piv}/yubikey_service.go (61%) rename api/utils/keys/{ => piv}/yubikey_service_fake.go (74%) rename api/utils/keys/{ => piv}/yubikey_service_test.go (81%) rename api/utils/keys/{ => piv}/yubikey_unavailable.go (59%) create mode 100644 api/utils/keys/policy_piv.go diff --git a/api/client/webclient/webclient.go b/api/client/webclient/webclient.go index 78d4c80c9aebc..4c01b0d707dff 100644 --- a/api/client/webclient/webclient.go +++ b/api/client/webclient/webclient.go @@ -45,6 +45,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) const ( @@ -528,7 +529,7 @@ type AuthenticationSettings struct { // PrivateKeyPolicy contains the cluster-wide private key policy. PrivateKeyPolicy keys.PrivateKeyPolicy `json:"private_key_policy"` // PIVSlot specifies a specific PIV slot to use with hardware key support. - PIVSlot keys.PIVSlot `json:"piv_slot"` + PIVSlot hardwarekey.PIVSlot `json:"piv_slot"` // DeviceTrust holds cluster-wide device trust settings. DeviceTrust DeviceTrustSettings `json:"device_trust,omitempty"` // HasMessageOfTheDay is a flag indicating that the cluster has MOTD diff --git a/api/profile/profile.go b/api/profile/profile.go index e652fe0153ac8..a8affcd56ce4d 100644 --- a/api/profile/profile.go +++ b/api/profile/profile.go @@ -35,6 +35,7 @@ import ( "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/utils/keypaths" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/api/utils/sshutils" ) @@ -107,7 +108,7 @@ type Profile struct { PrivateKeyPolicy keys.PrivateKeyPolicy `yaml:"private_key_policy"` // PIVSlot is a specific piv slot that Teleport clients should use for hardware key support. - PIVSlot keys.PIVSlot `yaml:"piv_slot"` + PIVSlot hardwarekey.PIVSlot `yaml:"piv_slot"` // MissingClusterDetails means this profile was created with limited cluster details. // Missing cluster details should be loaded into the profile by pinging the proxy. diff --git a/api/types/authentication.go b/api/types/authentication.go index b5872f4651b51..094d4514e1b2d 100644 --- a/api/types/authentication.go +++ b/api/types/authentication.go @@ -32,6 +32,7 @@ import ( "github.com/gravitational/teleport/api/defaults" "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/tlsutils" ) @@ -134,7 +135,7 @@ type AuthPreference interface { // GetHardwareKey returns the hardware key settings configured for the cluster. GetHardwareKey() (*HardwareKey, error) // GetPIVSlot returns the configured piv slot for the cluster. - GetPIVSlot() keys.PIVSlot + GetPIVSlot() hardwarekey.PIVSlot // GetHardwareKeySerialNumberValidation returns the cluster's hardware key // serial number validation settings. GetHardwareKeySerialNumberValidation() (*HardwareKeySerialNumberValidation, error) @@ -491,9 +492,9 @@ func (c *AuthPreferenceV2) GetHardwareKey() (*HardwareKey, error) { } // GetPIVSlot returns the configured piv slot for the cluster. -func (c *AuthPreferenceV2) GetPIVSlot() keys.PIVSlot { +func (c *AuthPreferenceV2) GetPIVSlot() hardwarekey.PIVSlot { if hk, err := c.GetHardwareKey(); err == nil { - return keys.PIVSlot(hk.PIVSlot) + return hardwarekey.PIVSlot(hk.PIVSlot) } return "" } @@ -840,7 +841,7 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { } if hk, err := c.GetHardwareKey(); err == nil && hk.PIVSlot != "" { - if err := keys.PIVSlot(hk.PIVSlot).Validate(); err != nil { + if err := hardwarekey.PIVSlot(hk.PIVSlot).Validate(); err != nil { return trace.Wrap(err) } } diff --git a/api/utils/keys/alias.go b/api/utils/keys/alias.go new file mode 100644 index 0000000000000..e8e9863c6a733 --- /dev/null +++ b/api/utils/keys/alias.go @@ -0,0 +1,23 @@ +/* +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 keys + +import "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + +// Temporary aliases for types moved to the hardwarekey or piv packages +// TODO(Joerger): Remove once /e no longer relies on them. + +type AttestationStatement = hardwarekey.AttestationStatement + +var AttestationStatementFromProto = hardwarekey.AttestationStatementFromProto diff --git a/api/utils/keys/hardwarekey/attestation.go b/api/utils/keys/hardwarekey/attestation.go new file mode 100644 index 0000000000000..fcf2ceaec5e0d --- /dev/null +++ b/api/utils/keys/hardwarekey/attestation.go @@ -0,0 +1,50 @@ +// 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 ( + "bytes" + + "github.com/gogo/protobuf/jsonpb" + "github.com/gravitational/trace" + + attestationv1 "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" +) + +// AttestationStatement is an attestation statement for a hardware private key +// that supports json marshaling through the standard json/encoding package. +type AttestationStatement attestationv1.AttestationStatement + +// ToProto converts this AttestationStatement to its protobuf form. +func (ar *AttestationStatement) ToProto() *attestationv1.AttestationStatement { + return (*attestationv1.AttestationStatement)(ar) +} + +// AttestationStatementFromProto converts an AttestationStatement from its protobuf form. +func AttestationStatementFromProto(att *attestationv1.AttestationStatement) *AttestationStatement { + return (*AttestationStatement)(att) +} + +// MarshalJSON implements custom protobuf json marshaling. +func (ar *AttestationStatement) MarshalJSON() ([]byte, error) { + buf := new(bytes.Buffer) + err := (&jsonpb.Marshaler{}).Marshal(buf, ar.ToProto()) + return buf.Bytes(), trace.Wrap(err) +} + +// UnmarshalJSON implements custom protobuf json unmarshaling. +func (ar *AttestationStatement) UnmarshalJSON(buf []byte) error { + return jsonpb.Unmarshal(bytes.NewReader(buf), ar.ToProto()) +} diff --git a/api/utils/keys/cliprompt.go b/api/utils/keys/hardwarekey/cliprompt.go similarity index 78% rename from api/utils/keys/cliprompt.go rename to api/utils/keys/hardwarekey/cliprompt.go index d9be72ed9f185..09c052cc23cfe 100644 --- a/api/utils/keys/cliprompt.go +++ b/api/utils/keys/hardwarekey/cliprompt.go @@ -1,4 +1,4 @@ -// Copyright 2024 Gravitational, Inc. +// 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. @@ -12,22 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -package keys +package hardwarekey import ( "context" "fmt" "os" - "github.com/go-piv/piv-go/piv" "github.com/gravitational/trace" "github.com/gravitational/teleport/api/utils/prompt" ) +var ( + // defaultPIN for the PIV applet. The PIN is used to change the Management Key, + // and slots can optionally require it to perform signing operations. + defaultPIN = "123456" + // defaultPUK for the PIV applet. The PUK is only used to reset the PIN when + // the card's PIN retries have been exhausted. + defaultPUK = "12345678" +) + type CLIPrompt struct{} -func (c *CLIPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement, _ KeyInfo) (string, error) { +func (c *CLIPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement, _ PrivateKeyInfo) (string, error) { message := "Enter your YubiKey PIV PIN" if requirement == PINOptional { message = "Enter your YubiKey PIV PIN [blank to use default PIN]" @@ -36,12 +44,12 @@ func (c *CLIPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement return password, trace.Wrap(err) } -func (c *CLIPrompt) Touch(_ context.Context, _ KeyInfo) error { +func (c *CLIPrompt) Touch(_ context.Context, _ PrivateKeyInfo) error { _, err := fmt.Fprintln(os.Stderr, "Tap your YubiKey") return trace.Wrap(err) } -func (c *CLIPrompt) ChangePIN(ctx context.Context, _ KeyInfo) (*PINAndPUK, error) { +func (c *CLIPrompt) ChangePIN(ctx context.Context, _ PrivateKeyInfo) (*PINAndPUK, error) { var pinAndPUK = &PINAndPUK{} for { fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PIN.\n") @@ -59,12 +67,12 @@ func (c *CLIPrompt) ChangePIN(ctx context.Context, _ KeyInfo) (*PINAndPUK, error continue } - if newPIN == piv.DefaultPIN { - fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN) + if newPIN == defaultPIN { + fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", defaultPIN) continue } - if !isPINLengthValid(newPIN) { + if !IsPINLengthValid(newPIN) { fmt.Fprintf(os.Stderr, "PIN must be 6-8 characters long.\n") continue } @@ -80,8 +88,8 @@ func (c *CLIPrompt) ChangePIN(ctx context.Context, _ KeyInfo) (*PINAndPUK, error pinAndPUK.PUK = puk switch puk { - case piv.DefaultPUK: - fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", piv.DefaultPUK) + case defaultPUK: + fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", defaultPUK) fallthrough case "": for { @@ -100,12 +108,12 @@ func (c *CLIPrompt) ChangePIN(ctx context.Context, _ KeyInfo) (*PINAndPUK, error continue } - if newPUK == piv.DefaultPUK { - fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", piv.DefaultPUK) + if newPUK == defaultPUK { + fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", defaultPUK) continue } - if !isPINLengthValid(newPUK) { + if !IsPINLengthValid(newPUK) { fmt.Fprintf(os.Stderr, "PUK must be 6-8 characters long.\n") continue } @@ -118,11 +126,7 @@ func (c *CLIPrompt) ChangePIN(ctx context.Context, _ KeyInfo) (*PINAndPUK, error return pinAndPUK, nil } -func (c *CLIPrompt) ConfirmSlotOverwrite(ctx context.Context, message string, _ KeyInfo) (bool, error) { +func (c *CLIPrompt) ConfirmSlotOverwrite(ctx context.Context, message string, _ PrivateKeyInfo) (bool, error) { confirmation, err := prompt.Confirmation(ctx, os.Stderr, prompt.Stdin(), message) return confirmation, trace.Wrap(err) } - -func isPINLengthValid(pin string) bool { - return len(pin) >= 6 && len(pin) <= 8 -} diff --git a/api/utils/keys/hardwarekey.go b/api/utils/keys/hardwarekey/hardwarekey.go similarity index 51% rename from api/utils/keys/hardwarekey.go rename to api/utils/keys/hardwarekey/hardwarekey.go index 33706eafd1b95..d42109c1bc314 100644 --- a/api/utils/keys/hardwarekey.go +++ b/api/utils/keys/hardwarekey/hardwarekey.go @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package keys +// Package hardwarekey defines types and interfaces for hardware private keys. +package hardwarekey import ( - "bytes" "context" "crypto" "crypto/rand" @@ -23,61 +23,103 @@ import ( "crypto/x509" "encoding/json" "io" + "strconv" - "github.com/gogo/protobuf/jsonpb" "github.com/gravitational/trace" - - attestationv1 "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" ) -// HardwareKeyService is an interface for interacting with hardware private keys. -type HardwareKeyService interface { +// Service for interfacing with hardware private keys. +type Service interface { // NewPrivateKey creates or retrieves a hardware private key from the given PIV slot matching - // the given private key policy and returns the details required to perform signatures with - // that key. - NewPrivateKey(ctx context.Context, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) + // the given policy and returns the details required to perform signatures with that key. + // + // If a customSlot is not provided, the service uses the default slot for the given policy: + // - !touch & !pin -> 9a + // - !touch & pin -> 9c + // - touch & pin -> 9d + // - touch & !pin -> 9e + NewPrivateKey(ctx context.Context, customSlot PIVSlot, policy PromptPolicy) (*PrivateKeyRef, error) // Sign performs a cryptographic signature using the specified hardware // private key and provided signature parameters. - Sign(ctx context.Context, ref *HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) + Sign(ctx context.Context, ref *PrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) // SetPrompt sets the hardware key prompt used by the hardware key service, if applicable. // This is used by Teleport Connect which sets the prompt later than the hardware key service, // due to process initialization constraints. - SetPrompt(prompt HardwareKeyPrompt) + SetPrompt(prompt Prompt) } -// HardwarePrivateKey is a hardware private key. -type HardwarePrivateKey struct { - service HardwareKeyService - ref *HardwarePrivateKeyRef +// PrivateKey is a hardware private key. +type PrivateKey struct { + service Service + ref *PrivateKeyRef // keyInfo contains additional key info which may be used to add context to prompts, // such as the name of the Teleport user using the key. - keyInfo KeyInfo + keyInfo PrivateKeyInfo +} + +// PrivateKeyRef references a specific hardware private key. +type PrivateKeyRef struct { + // SerialNumber is the hardware key's serial number. + SerialNumber uint32 `json:"serial_number"` + // SlotKey is the key name for the hardware key PIV slot, e.g. "9a". + SlotKey uint32 `json:"slot_key"` + // PublicKey is the public key paired with the hardware private key. + PublicKey crypto.PublicKey `json:"-"` // uses custom JSON marshaling in PKIX, ASN.1 DER form + // Policy specifies the hardware private key's PIN/touch prompt policies. + Policy PromptPolicy `json:"policy"` + // AttestationStatement contains the hardware private key's attestation statement, which is + // to attest the touch and pin requirements for this hardware private key during login. + AttestationStatement *AttestationStatement `json:"attestation_statement"` +} + +// PrivateKeyInfo includes info relevant to the key being parsed. Useful for adding context +// to hardware key pin/touch prompts when performing signatures. +type PrivateKeyInfo struct { + // ProxyHost is the root proxy hostname that a key is associated with. + ProxyHost string +} + +// PromptPolicy specifies a hardware private key's PIN/touch policies. +type PromptPolicy struct { + // TouchRequired means that touch is required for signatures. + TouchRequired bool + // PINRequired means that PIN is required for signatures. + PINRequired bool +} + +// NewPrivateKey returns a [PrivateKey] for the given service and ref. +func NewPrivateKey(s Service, ref *PrivateKeyRef, keyInfo PrivateKeyInfo) *PrivateKey { + return &PrivateKey{ + service: s, + ref: ref, + keyInfo: keyInfo, + } } // Public implements [crypto.Signer]. -func (h *HardwarePrivateKey) Public() crypto.PublicKey { +func (h *PrivateKey) Public() crypto.PublicKey { return h.ref.PublicKey } // Sign implements [crypto.Signer]. -func (h *HardwarePrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +func (h *PrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { // When context.TODO() is passed, the service should replace this with its own parent context. return h.service.Sign(context.TODO(), h.ref, rand, digest, opts) } // GetAttestation returns the hardware private key attestation details. -func (h *HardwarePrivateKey) GetAttestationStatement() *AttestationStatement { +func (h *PrivateKey) GetAttestationStatement() *AttestationStatement { return h.ref.AttestationStatement } // GetPrivateKeyPolicy returns the PrivateKeyPolicy satisfied by this key. -func (h *HardwarePrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy { - return h.ref.PrivateKeyPolicy +func (h *PrivateKey) GetPromptPolicy() PromptPolicy { + return h.ref.Policy } // WarmupHardwareKey performs a bogus sign() call to prompt the user for PIN/touch (if needed). -func (h *HardwarePrivateKey) WarmupHardwareKey(ctx context.Context) error { - if !h.ref.PrivateKeyPolicy.IsHardwareKeyPINVerified() && !h.ref.PrivateKeyPolicy.IsHardwareKeyTouchVerified() { +func (h *PrivateKey) WarmupHardwareKey(ctx context.Context) error { + if !h.ref.Policy.PINRequired && !h.ref.Policy.TouchRequired { return nil } @@ -89,23 +131,8 @@ func (h *HardwarePrivateKey) WarmupHardwareKey(ctx context.Context) error { return trace.Wrap(err, "failed to perform warmup signature with hardware private key") } -// HardwarePrivateKeyRef references a specific hardware private key. -type HardwarePrivateKeyRef struct { - // SerialNumber is the hardware key's serial number. - SerialNumber uint32 `json:"serial_number"` - // SlotKey is the key name for the hardware key PIV slot, e.g. "9a". - SlotKey uint32 `json:"slot_key"` - // PublicKey is the public key paired with the hardware private key. - PublicKey crypto.PublicKey `json:"-"` // uses custom JSON marshaling in PKIX, ASN.1 DER form - // PrivateKeyPolicy is the private key policy satisfied by the hardware private key. - PrivateKeyPolicy PrivateKeyPolicy `json:"private_key_policy"` - // AttestationStatement contains the hardware private key's attestation statement, which is - // to attest the touch and pin requirements for this hardware private key during login. - AttestationStatement *AttestationStatement `json:"attestation_statement"` -} - -// encodeHardwarePrivateKeyRef encodes a [HardwarePrivateKeyRef] to JSON. -func encodeHardwarePrivateKeyRef(ref *HardwarePrivateKeyRef) ([]byte, error) { +// encodeHardwarePrivateKeyRef encodes a [PrivateKeyRef] to JSON. +func EncodeHardwarePrivateKeyRef(ref *PrivateKeyRef) ([]byte, error) { keyRefBytes, err := json.Marshal(ref) if err != nil { return nil, trace.Wrap(err) @@ -113,18 +140,18 @@ func encodeHardwarePrivateKeyRef(ref *HardwarePrivateKeyRef) ([]byte, error) { return keyRefBytes, nil } -// decodeHardwarePrivateKeyRef decodes a [HardwarePrivateKeyRef] from JSON. -func decodeHardwarePrivateKeyRef(encodedKeyRef []byte) (*HardwarePrivateKeyRef, error) { +// decodeHardwarePrivateKeyRef decodes a [PrivateKeyRef] from JSON. +func DecodeHardwarePrivateKeyRef(encodedKeyRef []byte) (*PrivateKeyRef, error) { // TODO: old clients would only have SerialNumber and SlotKey, gather missing information directly for backwards compatibility. - keyRef := &HardwarePrivateKeyRef{} + keyRef := &PrivateKeyRef{} if err := json.Unmarshal(encodedKeyRef, keyRef); err != nil { return nil, trace.Wrap(err) } return keyRef, nil } -// These types are used for custom marshaling of the crypto.PublicKey field in [HardwarePrivateKeyRef]. -type refAlias HardwarePrivateKeyRef +// These types are used for custom marshaling of the crypto.PublicKey field in [PrivateKeyRef]. +type refAlias PrivateKeyRef type hardwarePrivateKeyRefJSON struct { // embedding an alias type instead of [HardwarePrivateKeyRef] prevents the custom marshaling // from recursively applying, which would result in a stack overflow. @@ -132,8 +159,8 @@ type hardwarePrivateKeyRefJSON struct { PublicKeyDER []byte `json:"public_key"` } -// UnmarshalJSON marshals [HardwarePrivateKeyRef] with custom logic for the public key. -func (r HardwarePrivateKeyRef) MarshalJSON() ([]byte, error) { +// UnmarshalJSON marshals [PrivateKeyRef] with custom logic for the public key. +func (r PrivateKeyRef) MarshalJSON() ([]byte, error) { pubDER, err := x509.MarshalPKIXPublicKey(r.PublicKey) if err != nil { return nil, trace.Wrap(err) @@ -145,8 +172,8 @@ func (r HardwarePrivateKeyRef) MarshalJSON() ([]byte, error) { }) } -// UnmarshalJSON unmarshals [HardwarePrivateKeyRef] with custom logic for the public key. -func (r *HardwarePrivateKeyRef) UnmarshalJSON(b []byte) error { +// UnmarshalJSON unmarshals [PrivateKeyRef] with custom logic for the public key. +func (r *PrivateKeyRef) UnmarshalJSON(b []byte) error { ref := hardwarePrivateKeyRefJSON{} err := json.Unmarshal(b, &ref) if err != nil { @@ -158,42 +185,24 @@ func (r *HardwarePrivateKeyRef) UnmarshalJSON(b []byte) error { return trace.Wrap(err) } - *r = HardwarePrivateKeyRef(ref.refAlias) + *r = PrivateKeyRef(ref.refAlias) return nil } -// AttestationStatement is an attestation statement for a hardware private key -// that supports json marshaling through the standard json/encoding package. -type AttestationStatement attestationv1.AttestationStatement - -// ToProto converts this AttestationStatement to its protobuf form. -func (ar *AttestationStatement) ToProto() *attestationv1.AttestationStatement { - return (*attestationv1.AttestationStatement)(ar) -} - -// AttestationStatementFromProto converts an AttestationStatement from its protobuf form. -func AttestationStatementFromProto(att *attestationv1.AttestationStatement) *AttestationStatement { - return (*AttestationStatement)(att) -} - -// MarshalJSON implements custom protobuf json marshaling. -func (ar *AttestationStatement) MarshalJSON() ([]byte, error) { - buf := new(bytes.Buffer) - err := (&jsonpb.Marshaler{}).Marshal(buf, ar.ToProto()) - return buf.Bytes(), trace.Wrap(err) -} +// PIVSlot is the string representation of a PIV slot. e.g. "9a". +type PIVSlot string -// UnmarshalJSON implements custom protobuf json unmarshaling. -func (ar *AttestationStatement) UnmarshalJSON(buf []byte) error { - return jsonpb.Unmarshal(bytes.NewReader(buf), ar.ToProto()) -} +// Validate that the PIV slot is a valid value. +func (s PIVSlot) Validate() error { + slotKey, err := strconv.ParseUint(string(s), 16, 32) + if err != nil { + return trace.Wrap(err) + } -// AttestationData is verified attestation data for a public key. -type AttestationData struct { - // PublicKeyDER is the public key in PKIX, ASN.1 DER form. - PublicKeyDER []byte `json:"public_key"` - // PrivateKeyPolicy specifies the private key policy supported by the associated private key. - PrivateKeyPolicy PrivateKeyPolicy `json:"private_key_policy"` - // SerialNumber is the serial number of the Attested hardware key. - SerialNumber uint32 `json:"serial_number"` + switch slotKey { + case 0x9a, 0x9c, 0x9d, 0x9e: + return nil + default: + return trace.BadParameter("invalid PIV slot %q", s) + } } diff --git a/api/utils/keys/yubikey_common.go b/api/utils/keys/hardwarekey/prompt.go similarity index 52% rename from api/utils/keys/yubikey_common.go rename to api/utils/keys/hardwarekey/prompt.go index a24044d258997..72545b82118a1 100644 --- a/api/utils/keys/yubikey_common.go +++ b/api/utils/keys/hardwarekey/prompt.go @@ -1,42 +1,38 @@ -/* -Copyright 2022 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 keys +// 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" - "errors" - - "github.com/gravitational/trace" ) -var errPIVUnavailable = errors.New("PIV is unavailable in current build") - -// HardwareKeyPrompt provides methods to interact with a YubiKey hardware key. -type HardwareKeyPrompt interface { +// Prompt provides methods to interact with a hardware [PrivateKey]. +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, keyInfo KeyInfo) (string, error) + AskPIN(ctx context.Context, requirement PINPromptRequirement, keyInfo PrivateKeyInfo) (string, error) // Touch prompts the user to touch the hardware key. - Touch(ctx context.Context, keyInfo KeyInfo) error + Touch(ctx context.Context, keyInfo PrivateKeyInfo) 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, keyInfo KeyInfo) (*PINAndPUK, error) + ChangePIN(ctx context.Context, keyInfo PrivateKeyInfo) (*PINAndPUK, error) // ConfirmSlotOverwrite asks the user if the slot's private key and certificate can be overridden. - ConfirmSlotOverwrite(ctx context.Context, message string, keyInfo KeyInfo) (bool, error) + ConfirmSlotOverwrite(ctx context.Context, message string, keyInfo PrivateKeyInfo) (bool, error) } // PINPromptRequirement specifies whether a PIN is required. @@ -60,10 +56,7 @@ type PINAndPUK struct { PUKChanged bool } -// PIVSlot is the string representation of a PIV slot. e.g. "9a". -type PIVSlot string - -// Validate that the PIV slot is a valid value. -func (s PIVSlot) Validate() error { - return trace.Wrap(s.validate()) +// IsPINLengthValid returns whether the given PIV PIN, or PUK, is of valid length (6-8 characters). +func IsPINLengthValid(pin string) bool { + return len(pin) >= 6 && len(pin) <= 8 } diff --git a/api/utils/keys/yubikey.go b/api/utils/keys/piv/yubikey.go similarity index 87% rename from api/utils/keys/yubikey.go rename to api/utils/keys/piv/yubikey.go index 61f98a92f355f..6cb30ef6ae384 100644 --- a/api/utils/keys/yubikey.go +++ b/api/utils/keys/piv/yubikey.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package keys +package piv import ( "context" @@ -27,7 +27,6 @@ import ( "io" "math/big" "os" - "strconv" "strings" "sync" "time" @@ -37,6 +36,7 @@ import ( "github.com/gravitational/teleport/api" attestationv1 "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/api/utils/retryutils" ) @@ -56,6 +56,55 @@ type YubiKey struct { version piv.Version } +// FindYubiKey finds a YubiKey PIV card by serial number. If no serial +// number is provided, the first YubiKey found will be returned. +func FindYubiKey(serialNumber uint32) (*YubiKey, error) { + yubiKeyCards, err := findYubiKeyCards() + if err != nil { + return nil, trace.Wrap(err) + } + + if len(yubiKeyCards) == 0 { + if serialNumber != 0 { + return nil, trace.ConnectionProblem(nil, "no YubiKey device connected with serial number %d", serialNumber) + } + return nil, trace.ConnectionProblem(nil, "no YubiKey device connected") + } + + for _, card := range yubiKeyCards { + y, err := newYubiKey(card) + if err != nil { + return nil, trace.Wrap(err) + } + + if serialNumber == 0 || y.serialNumber == serialNumber { + return y, nil + } + } + + return nil, trace.ConnectionProblem(nil, "no YubiKey device connected with serial number %d", serialNumber) +} + +// pivCardTypeYubiKey is the PIV card type assigned to yubiKeys. +const pivCardTypeYubiKey = "yubikey" + +// findYubiKeyCards returns a list of connected yubiKey PIV card names. +func findYubiKeyCards() ([]string, error) { + cards, err := piv.Cards() + if err != nil { + return nil, trace.Wrap(err) + } + + var yubiKeyCards []string + for _, card := range cards { + if strings.Contains(strings.ToLower(card), pivCardTypeYubiKey) { + yubiKeyCards = append(yubiKeyCards, card) + } + } + + return yubiKeyCards, nil +} + func newYubiKey(card string) (*YubiKey, error) { y := &YubiKey{ sharedPIVConnection: &sharedPIVConnection{ @@ -91,7 +140,7 @@ const ( signTouchPromptDelay = time.Millisecond * 200 ) -func (y *YubiKey) sign(ctx context.Context, ref *HardwarePrivateKeyRef, prompt HardwareKeyPrompt, rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, prompt hardwarekey.Prompt, rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { ctx, cancel := context.WithCancelCause(ctx) defer cancel(nil) @@ -106,7 +155,7 @@ func (y *YubiKey) sign(ctx context.Context, ref *HardwarePrivateKeyRef, prompt H defer release() var touchPromptDelayTimer *time.Timer - if ref.PrivateKeyPolicy.IsHardwareKeyTouchVerified() { + if ref.Policy.TouchRequired { touchPromptDelayTimer = time.NewTimer(signTouchPromptDelay) defer touchPromptDelayTimer.Stop() @@ -114,7 +163,7 @@ func (y *YubiKey) sign(ctx context.Context, ref *HardwarePrivateKeyRef, prompt H 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, KeyInfo{}) + err := prompt.Touch(ctx, hardwarekey.PrivateKeyInfo{}) if err != nil { // Cancel the entire function when an error occurs. // This is typically used for aborting the prompt. @@ -136,12 +185,12 @@ func (y *YubiKey) sign(ctx context.Context, ref *HardwarePrivateKeyRef, prompt H defer touchPromptDelayTimer.Reset(signTouchPromptDelay) } } - pass, err := prompt.AskPIN(ctx, PINRequired, KeyInfo{}) + pass, err := prompt.AskPIN(ctx, hardwarekey.PINRequired, hardwarekey.PrivateKeyInfo{}) return pass, trace.Wrap(err) } pinPolicy := piv.PINPolicyNever - if ref.PrivateKeyPolicy.IsHardwareKeyPINVerified() { + if ref.Policy.PINRequired { pinPolicy = piv.PINPolicyOnce } @@ -241,14 +290,14 @@ func (y *YubiKey) Reset() error { } // generatePrivateKey generates a new private key in the given PIV slot. -func (y *YubiKey) generatePrivateKey(slot piv.Slot, requiredKeyPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) { +func (y *YubiKey) generatePrivateKey(slot piv.Slot, policy hardwarekey.PromptPolicy) (*hardwarekey.PrivateKeyRef, error) { touchPolicy := piv.TouchPolicyNever - if requiredKeyPolicy.IsHardwareKeyTouchVerified() { + if policy.TouchRequired { touchPolicy = piv.TouchPolicyCached } pinPolicy := piv.PINPolicyNever - if requiredKeyPolicy.IsHardwareKeyPINVerified() { + if policy.PINRequired { pinPolicy = piv.PINPolicyOnce } @@ -280,12 +329,12 @@ func (y *YubiKey) generatePrivateKey(slot piv.Slot, requiredKeyPolicy PrivateKey return nil, trace.Wrap(err) } - return &HardwarePrivateKeyRef{ - SerialNumber: y.serialNumber, - SlotKey: slot.Key, - PublicKey: pub, - PrivateKeyPolicy: requiredKeyPolicy, - AttestationStatement: &AttestationStatement{ + return &hardwarekey.PrivateKeyRef{ + SerialNumber: y.serialNumber, + SlotKey: slot.Key, + PublicKey: pub, + Policy: policy, + AttestationStatement: &hardwarekey.AttestationStatement{ AttestationStatement: &attestationv1.AttestationStatement_YubikeyAttestationStatement{ YubikeyAttestationStatement: &attestationv1.YubiKeyAttestationStatement{ SlotCert: slotCert.Raw, @@ -346,8 +395,8 @@ func (y *YubiKey) SetPIN(oldPin, newPin string) 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 (y *YubiKey) checkOrSetPIN(ctx context.Context, prompt HardwareKeyPrompt) error { - pin, err := prompt.AskPIN(ctx, PINOptional, KeyInfo{}) +func (y *YubiKey) checkOrSetPIN(ctx context.Context, prompt hardwarekey.Prompt) error { + pin, err := prompt.AskPIN(ctx, hardwarekey.PINOptional, hardwarekey.PrivateKeyInfo{}) if err != nil { return trace.Wrap(err) } @@ -569,20 +618,20 @@ func (c *sharedPIVConnection) verifyPIN(pin string) error { return trace.Wrap(c.conn.VerifyPIN(pin)) } -func (c *sharedPIVConnection) setPINAndPUKFromDefault(ctx context.Context, prompt HardwareKeyPrompt) (string, error) { - pinAndPUK, err := prompt.ChangePIN(ctx, KeyInfo{}) +func (c *sharedPIVConnection) setPINAndPUKFromDefault(ctx context.Context, prompt hardwarekey.Prompt) (string, error) { + pinAndPUK, err := prompt.ChangePIN(ctx, hardwarekey.PrivateKeyInfo{}) if err != nil { return "", trace.Wrap(err) } // YubiKey requires that PIN and PUK be 6-8 characters. // Verify that we get valid values from the prompt. - if !isPINLengthValid(pinAndPUK.PIN) { + if !hardwarekey.IsPINLengthValid(pinAndPUK.PIN) { return "", trace.BadParameter("PIN must be 6-8 characters long") } if pinAndPUK.PIN == piv.DefaultPIN { return "", trace.BadParameter("The default PIN is not supported") } - if !isPINLengthValid(pinAndPUK.PUK) { + if !hardwarekey.IsPINLengthValid(pinAndPUK.PUK) { return "", trace.BadParameter("PUK must be 6-8 characters long") } if pinAndPUK.PUK == piv.DefaultPUK { @@ -607,20 +656,6 @@ func isRetryError(err error) bool { return strings.Contains(err.Error(), retryError) } -func (s PIVSlot) validate() error { - _, err := s.parse() - return trace.Wrap(err) -} - -func (s PIVSlot) parse() (piv.Slot, error) { - slotKey, err := strconv.ParseUint(string(s), 16, 32) - if err != nil { - return piv.Slot{}, trace.Wrap(err) - } - - return parsePIVSlot(uint32(slotKey)) -} - func parsePIVSlot(slotKey uint32) (piv.Slot, error) { switch slotKey { case piv.SlotAuthentication.Key: @@ -632,11 +667,7 @@ func parsePIVSlot(slotKey uint32) (piv.Slot, error) { case piv.SlotCardAuthentication.Key: return piv.SlotCardAuthentication, nil default: - retiredSlot, ok := piv.RetiredKeyManagementSlot(slotKey) - if !ok { - return piv.Slot{}, trace.BadParameter("slot %X does not exist", slotKey) - } - return retiredSlot, nil + return piv.Slot{}, trace.BadParameter("invalid slot %X", slotKey) } } diff --git a/api/utils/keys/yubikey_service.go b/api/utils/keys/piv/yubikey_service.go similarity index 61% rename from api/utils/keys/yubikey_service.go rename to api/utils/keys/piv/yubikey_service.go index b6f3b18f72ca1..56688195749f4 100644 --- a/api/utils/keys/yubikey_service.go +++ b/api/utils/keys/piv/yubikey_service.go @@ -14,7 +14,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -package keys +// Package piv provides a PIV implementation of [hardwarekey.Service]. +package piv import ( "context" @@ -25,13 +26,14 @@ import ( "errors" "fmt" "io" - "strings" + "strconv" "sync" "github.com/go-piv/piv-go/piv" "github.com/gravitational/trace" attestationv1 "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) // The PIV daemon only allows a single PC/SC transaction (connection) at a time, @@ -45,8 +47,9 @@ var ( yubiKeysMux sync.Mutex ) -type YubiKeyPIVService struct { - prompt HardwareKeyPrompt +// YubiKeyService is a YubiKey PIV implementation of [hardwarekey.Service]. +type YubiKeyService struct { + prompt hardwarekey.Prompt // ctx is provided to signature requests, since `crypto.Sign` does have // context support directly. @@ -57,22 +60,34 @@ type YubiKeyPIVService struct { signMux sync.Mutex } -func NewYubiKeyPIVService(ctx context.Context, prompt HardwareKeyPrompt) HardwareKeyService { +// Returns a new [YubiKeyService]. +// +// Only a single service should be created for each process to ensure the cached connections +// are shared and multiple services don't compete for PIV resources. +func NewYubiKeyService(ctx context.Context, prompt hardwarekey.Prompt) *YubiKeyService { if ctx == nil { ctx = context.Background() } if prompt == nil { - prompt = &CLIPrompt{} + prompt = &hardwarekey.CLIPrompt{} } - return &YubiKeyPIVService{ + return &YubiKeyService{ ctx: ctx, prompt: prompt, } } -func (s *YubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) { +// NewPrivateKey creates or retrieves a hardware private key from the given PIV slot matching +// the given policy and returns the details required to perform signatures with that key. +// +// If a customSlot is not provided, the service uses the default slot for the given policy: +// - !touch & !pin -> 9a +// - !touch & pin -> 9c +// - touch & pin -> 9d +// - touch & !pin -> 9e +func (s *YubiKeyService) NewPrivateKey(ctx context.Context, customSlot hardwarekey.PIVSlot, requiredPolicy hardwarekey.PromptPolicy) (*hardwarekey.PrivateKeyRef, error) { // Use the first yubiKey we find. y, err := s.getYubiKey(0) if err != nil { @@ -82,16 +97,20 @@ func (s *YubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlo // Get the requested or default PIV slot. var pivSlot piv.Slot if customSlot != "" { - pivSlot, err = customSlot.parse() + slotKey, err := strconv.ParseUint(string(customSlot), 16, 32) + if err != nil { + return nil, trace.Wrap(err) + } + pivSlot, err = parsePIVSlot(uint32(slotKey)) } else { - pivSlot, err = GetDefaultKeySlot(requiredPolicy) + pivSlot, err = getDefaultKeySlot(requiredPolicy) } if err != nil { return nil, trace.Wrap(err) } // If PIN is required, check that PIN and PUK are not the defaults. - if requiredPolicy.IsHardwareKeyPINVerified() { + if requiredPolicy.PINRequired { if err := y.checkOrSetPIN(ctx, s.prompt); err != nil { return nil, trace.Wrap(err) } @@ -104,7 +123,7 @@ func (s *YubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlo switch cert, err := y.getCertificate(pivSlot); { case err == nil && (len(cert.Subject.Organization) == 0 || cert.Subject.Organization[0] != certOrgName): // Unknown cert found, prompt the user before we overwrite the slot. - if err := s.promptOverwriteSlot(ctx, nonTeleportCertificateMessage(pivSlot, cert), KeyInfo{}); err != nil { + if err := s.promptOverwriteSlot(ctx, nonTeleportCertificateMessage(pivSlot, cert), hardwarekey.PrivateKeyInfo{}); err != nil { return nil, trace.Wrap(err) } return y.generatePrivateKey(pivSlot, requiredPolicy) @@ -118,12 +137,15 @@ func (s *YubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlo // Check for an existing key in the slot that satisfies the required private // key policy, or generate a new one if needed. slotCert, attCert, att, err := y.attestKey(pivSlot) - keyPolicy := GetPrivateKeyPolicyFromAttestation(att) + keyPolicy := hardwarekey.PromptPolicy{ + TouchRequired: att.TouchPolicy != piv.TouchPolicyNever, + PINRequired: att.PINPolicy != piv.PINPolicyNever, + } switch { - case err == nil && !requiredPolicy.IsSatisfiedBy(keyPolicy): + case err == nil && (requiredPolicy.TouchRequired && !keyPolicy.TouchRequired) || (requiredPolicy.PINRequired && !keyPolicy.PINRequired): // Key does not meet the required key policy, prompt the user before we overwrite the slot. - msg := fmt.Sprintf("private key in YubiKey PIV slot %q does not meet private key policy %q.", pivSlot, requiredPolicy) - if err := s.promptOverwriteSlot(ctx, msg, KeyInfo{}); err != nil { + msg := fmt.Sprintf("private key in YubiKey PIV slot %q does not meet prompt policy %v.", pivSlot, requiredPolicy) + if err := s.promptOverwriteSlot(ctx, msg, hardwarekey.PrivateKeyInfo{}); err != nil { return nil, trace.Wrap(err) } return y.generatePrivateKey(pivSlot, requiredPolicy) @@ -133,12 +155,15 @@ func (s *YubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlo return nil, trace.Wrap(err) } - return &HardwarePrivateKeyRef{ - SerialNumber: y.serialNumber, - SlotKey: pivSlot.Key, - PublicKey: slotCert.PublicKey, - PrivateKeyPolicy: keyPolicy, - AttestationStatement: &AttestationStatement{ + return &hardwarekey.PrivateKeyRef{ + SerialNumber: y.serialNumber, + SlotKey: pivSlot.Key, + PublicKey: slotCert.PublicKey, + Policy: hardwarekey.PromptPolicy{ + TouchRequired: att.TouchPolicy != piv.TouchPolicyNever, + PINRequired: att.PINPolicy != piv.PINPolicyNever, + }, + AttestationStatement: &hardwarekey.AttestationStatement{ AttestationStatement: &attestationv1.AttestationStatement_YubikeyAttestationStatement{ YubikeyAttestationStatement: &attestationv1.YubiKeyAttestationStatement{ SlotCert: slotCert.Raw, @@ -149,7 +174,47 @@ func (s *YubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlo }, nil } -func (s *YubiKeyPIVService) promptOverwriteSlot(ctx context.Context, msg string, keyInfo KeyInfo) error { +func getDefaultKeySlot(policy hardwarekey.PromptPolicy) (piv.Slot, error) { + switch policy { + case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: false}: + return piv.SlotAuthentication, nil + case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: false}: + return piv.SlotSignature, nil + case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: true}: + return piv.SlotKeyManagement, nil + case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: false}: + return piv.SlotCardAuthentication, nil + default: + return piv.Slot{}, trace.BadParameter("unexpected private key policy %v", policy) + } +} + +func nonTeleportCertificateMessage(slot piv.Slot, cert *x509.Certificate) string { + // Gather a small list of user-readable x509 certificate fields to display to the user. + sum := sha256.Sum256(cert.Raw) + fingerPrint := hex.EncodeToString(sum[:]) + return fmt.Sprintf(`Certificate in YubiKey PIV slot %q is not a Teleport client cert: +Slot %s: + Algorithm: %v + Subject DN: %v + Issuer DN: %v + Serial: %v + Fingerprint: %v + Not before: %v + Not after: %v +`, + slot, slot, + cert.SignatureAlgorithm, + cert.Subject, + cert.Issuer, + cert.SerialNumber, + fingerPrint, + cert.NotBefore, + cert.NotAfter, + ) +} + +func (s *YubiKeyService) promptOverwriteSlot(ctx context.Context, msg string, keyInfo hardwarekey.PrivateKeyInfo) error { promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg) if confirmed, confirmErr := s.prompt.ConfirmSlotOverwrite(ctx, promptQuestion, keyInfo); confirmErr != nil { return trace.Wrap(confirmErr) @@ -161,7 +226,7 @@ func (s *YubiKeyPIVService) promptOverwriteSlot(ctx context.Context, msg string, // Sign performs a cryptographic signature using the specified hardware // private key and provided signature parameters. -func (s *YubiKeyPIVService) Sign(ctx context.Context, ref *HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { +func (s *YubiKeyService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { // Usually, Sign will be called without context through the [crypto.Signer] interface, // so we opportunistically set the context. if ctx == context.TODO() { @@ -181,13 +246,16 @@ func (s *YubiKeyPIVService) Sign(ctx context.Context, ref *HardwarePrivateKeyRef return y.sign(ctx, ref, s.prompt, rand, digest, opts) } -func (s *YubiKeyPIVService) SetPrompt(prompt HardwareKeyPrompt) { +// SetPrompt sets the hardware key prompt used by the hardware key service, if applicable. +// This is used by Teleport Connect which sets the prompt later than the hardware key service, +// due to process initialization constraints. +func (s *YubiKeyService) SetPrompt(prompt hardwarekey.Prompt) { s.prompt = prompt } // Get the given YubiKey with the serial number. If the provided serialNumber is "0", // return the first YubiKey found in the smart card list. -func (s *YubiKeyPIVService) getYubiKey(serialNumber uint32) (*YubiKey, error) { +func (s *YubiKeyService) getYubiKey(serialNumber uint32) (*YubiKey, error) { yubiKeysMux.Lock() defer yubiKeysMux.Unlock() @@ -203,120 +271,3 @@ func (s *YubiKeyPIVService) getYubiKey(serialNumber uint32) (*YubiKey, error) { yubiKeys[y.serialNumber] = y return y, nil } - -// FindYubiKey finds a yubiKey PIV card by serial number. If no serial -// number is provided, the first yubiKey found will be returned. -func FindYubiKey(serialNumber uint32) (*YubiKey, error) { - yubiKeyCards, err := findYubiKeyCards() - if err != nil { - return nil, trace.Wrap(err) - } - - if len(yubiKeyCards) == 0 { - if serialNumber != 0 { - return nil, trace.ConnectionProblem(nil, "no YubiKey device connected with serial number %d", serialNumber) - } - return nil, trace.ConnectionProblem(nil, "no YubiKey device connected") - } - - for _, card := range yubiKeyCards { - y, err := newYubiKey(card) - if err != nil { - return nil, trace.Wrap(err) - } - - if serialNumber == 0 || y.serialNumber == serialNumber { - return y, nil - } - } - - return nil, trace.ConnectionProblem(nil, "no YubiKey device connected with serial number %d", serialNumber) -} - -// PIVCardTypeYubiKey is the PIV card type assigned to yubiKeys. -const PIVCardTypeYubiKey = "yubikey" - -// findYubiKeyCards returns a list of connected yubiKey PIV card names. -func findYubiKeyCards() ([]string, error) { - cards, err := piv.Cards() - if err != nil { - return nil, trace.Wrap(err) - } - - var yubiKeyCards []string - for _, card := range cards { - if strings.Contains(strings.ToLower(card), PIVCardTypeYubiKey) { - yubiKeyCards = append(yubiKeyCards, card) - } - } - - return yubiKeyCards, nil -} - -func GetDefaultKeySlot(policy PrivateKeyPolicy) (piv.Slot, error) { - switch policy { - case PrivateKeyPolicyHardwareKey: - // private_key_policy: hardware_key -> 9a - return piv.SlotAuthentication, nil - case PrivateKeyPolicyHardwareKeyTouch: - // private_key_policy: hardware_key_touch -> 9c - return piv.SlotSignature, nil - case PrivateKeyPolicyHardwareKeyTouchAndPIN: - // private_key_policy: hardware_key_touch_and_pin -> 9d - return piv.SlotKeyManagement, nil - case PrivateKeyPolicyHardwareKeyPIN: - // private_key_policy: hardware_key_pin -> 9e - return piv.SlotCardAuthentication, nil - default: - return piv.Slot{}, trace.BadParameter("unexpected private key policy %v", policy) - } -} - -// GetPrivateKeyPolicyFromAttestation returns the PrivateKeyPolicy satisfied by the given hardware key attestation. -func GetPrivateKeyPolicyFromAttestation(att *piv.Attestation) PrivateKeyPolicy { - if att == nil { - return PrivateKeyPolicyNone - } - - isTouchPolicy := att.TouchPolicy == piv.TouchPolicyCached || - att.TouchPolicy == piv.TouchPolicyAlways - - isPINPolicy := att.PINPolicy == piv.PINPolicyOnce || - att.PINPolicy == piv.PINPolicyAlways - - switch { - case isPINPolicy && isTouchPolicy: - return PrivateKeyPolicyHardwareKeyTouchAndPIN - case isPINPolicy: - return PrivateKeyPolicyHardwareKeyPIN - case isTouchPolicy: - return PrivateKeyPolicyHardwareKeyTouch - default: - return PrivateKeyPolicyHardwareKey - } -} - -func nonTeleportCertificateMessage(slot piv.Slot, cert *x509.Certificate) string { - // Gather a small list of user-readable x509 certificate fields to display to the user. - sum := sha256.Sum256(cert.Raw) - fingerPrint := hex.EncodeToString(sum[:]) - return fmt.Sprintf(`Certificate in YubiKey PIV slot %q is not a Teleport client cert: -Slot %s: - Algorithm: %v - Subject DN: %v - Issuer DN: %v - Serial: %v - Fingerprint: %v - Not before: %v - Not after: %v -`, - slot, slot, - cert.SignatureAlgorithm, - cert.Subject, - cert.Issuer, - cert.SerialNumber, - fingerPrint, - cert.NotBefore, - cert.NotAfter, - ) -} diff --git a/api/utils/keys/yubikey_service_fake.go b/api/utils/keys/piv/yubikey_service_fake.go similarity index 74% rename from api/utils/keys/yubikey_service_fake.go rename to api/utils/keys/piv/yubikey_service_fake.go index 4f0b68518bd0f..83d435a1e7d2a 100644 --- a/api/utils/keys/yubikey_service_fake.go +++ b/api/utils/keys/piv/yubikey_service_fake.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package keys +package piv import ( "context" @@ -24,6 +24,8 @@ import ( "io" "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) type fakeYubiKeyPIVService struct { @@ -33,13 +35,13 @@ type fakeYubiKeyPIVService struct { keys map[crypto.PublicKey]crypto.Signer } -func NewYubiKeyPIVService(ctx context.Context, _ HardwareKeyPrompt) HardwareKeyService { +func NewYubiKeyPIVService(ctx context.Context, _ hardwarekey.HardwareKeyPrompt) *fakeYubiKeyPIVService { return &fakeYubiKeyPIVService{ keys: map[crypto.PublicKey]crypto.Signer{}, } } -func (s *fakeYubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) { +func (s *fakeYubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot hardwarekey.PIVSlot, requiredPolicy hardwarekey.PromptPolicy) (*hardwarekey.PrivateKeyRef, error) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, trace.Wrap(err) @@ -47,19 +49,19 @@ func (s *fakeYubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PI s.keys[string(pub)] = priv - return &HardwarePrivateKeyRef{ - PrivateKeyPolicy: requiredPolicy, - PublicKey: pub, + return &hardwarekey.PrivateKeyRef{ + Policy: requiredPolicy, + PublicKey: pub, // Since this is only used in tests, we will ignore the attestation statement in the end. // We just need it to be non-nil so that it goes through the test modules implementation // of AttestHardwareKey. - AttestationStatement: &AttestationStatement{}, + AttestationStatement: &hardwarekey.AttestationStatement{}, }, nil } // Sign performs a cryptographic signature using the specified hardware // private key and provided signature parameters. -func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { +func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref hardwarekey.PrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { ed25519Pub, ok := ref.PublicKey.(ed25519.PublicKey) if !ok { return nil, trace.BadParameter("expected public key of type %T", ed25519.PublicKey{}) @@ -72,8 +74,4 @@ func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref HardwarePrivateKey return priv.Sign(rand, digest, opts) } -func (s *fakeYubiKeyPIVService) SetPrompt(prompt HardwareKeyPrompt) {} - -func (s PIVSlot) validate() error { - return trace.Wrap(errPIVUnavailable) -} +func (s *fakeYubiKeyPIVService) SetPrompt(prompt hardwarekey.HardwareKeyPrompt) {} diff --git a/api/utils/keys/yubikey_service_test.go b/api/utils/keys/piv/yubikey_service_test.go similarity index 81% rename from api/utils/keys/yubikey_service_test.go rename to api/utils/keys/piv/yubikey_service_test.go index 9cca620cae0b9..614098c7b3629 100644 --- a/api/utils/keys/yubikey_service_test.go +++ b/api/utils/keys/piv/yubikey_service_test.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package keys_test +package piv_test import ( "context" @@ -22,11 +22,13 @@ import ( "os" "testing" - "github.com/go-piv/piv-go/piv" + pivgo "github.com/go-piv/piv-go/piv" "github.com/gravitational/trace" "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/api/utils/prompt" ) @@ -45,9 +47,9 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - s := keys.NewYubiKeyPIVService(ctx, &keys.CLIPrompt{}) + s := piv.NewYubiKeyService(ctx, &hardwarekey.CLIPrompt{}) - y, err := keys.FindYubiKey(0) + y, err := piv.FindYubiKey(0) require.NoError(t, err) resetYubikey(t, y) @@ -55,7 +57,7 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { // Warmup the hardware key to prompt touch at the start of the test, // rather than having this interaction later. - priv, err := keys.NewHardwarePrivateKey(ctx, s, "", keys.PrivateKeyPolicyHardwareKeyTouch, keys.KeyInfo{}) + priv, err := keys.NewHardwarePrivateKey(ctx, s, "", keys.PrivateKeyPolicyHardwareKeyTouch, hardwarekey.PrivateKeyInfo{}) require.NoError(t, err) require.Nil(t, priv.WarmupHardwareKey(ctx)) @@ -71,13 +73,13 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { resetYubikey(t, y) setupPINPrompt(t, y) - var slot keys.PIVSlot = "" + var slot hardwarekey.PIVSlot = "" if customSlot { slot = "9a" } // NewHardwarePrivateKey should generate a new hardware private key. - priv, err := keys.NewHardwarePrivateKey(ctx, s, slot, policy, keys.KeyInfo{}) + priv, err := keys.NewHardwarePrivateKey(ctx, s, slot, policy, hardwarekey.PrivateKeyInfo{}) require.NoError(t, err) // test HardwareSigner methods @@ -89,7 +91,7 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { require.Nil(t, priv.WarmupHardwareKey(ctx)) // Another call to NewHardwarePrivateKey should retrieve the previously generated key. - retrievePriv, err := keys.NewHardwarePrivateKey(ctx, s, slot, policy, keys.KeyInfo{}) + retrievePriv, err := keys.NewHardwarePrivateKey(ctx, s, slot, policy, hardwarekey.PrivateKeyInfo{}) require.NoError(t, err) require.Equal(t, priv.Public(), retrievePriv.Public()) @@ -110,27 +112,26 @@ func TestOverwritePrompt(t *testing.T) { } ctx := context.Background() - s := keys.NewYubiKeyPIVService(ctx, &keys.CLIPrompt{}) + s := piv.NewYubiKeyService(ctx, &hardwarekey.CLIPrompt{}) - y, err := keys.FindYubiKey(0) + y, err := piv.FindYubiKey(0) require.NoError(t, err) resetYubikey(t, y) t.Cleanup(func() { resetYubikey(t, y) }) - // Use a custom slot. - pivSlot, err := keys.GetDefaultKeySlot(keys.PrivateKeyPolicyHardwareKeyTouch) - require.NoError(t, err) + // Get the default slot used for hardware_key_touch. + touchSlot := pivgo.SlotSignature testOverwritePrompt := func(t *testing.T) { // Fail to overwrite slot when user denies prompt.SetStdin(prompt.NewFakeReader().AddString("n")) - _, err := keys.NewHardwarePrivateKey(ctx, s, "" /*slot*/, keys.PrivateKeyPolicyHardwareKeyTouch, keys.KeyInfo{}) + _, err := keys.NewHardwarePrivateKey(ctx, s, "" /*slot*/, keys.PrivateKeyPolicyHardwareKeyTouch, hardwarekey.PrivateKeyInfo{}) require.True(t, trace.IsCompareFailed(err), "Expected compare failed error but got %v", err) // Successfully overwrite slot when user accepts prompt.SetStdin(prompt.NewFakeReader().AddString("y")) - _, err = keys.NewHardwarePrivateKey(ctx, s, "" /*slot*/, keys.PrivateKeyPolicyHardwareKeyTouch, keys.KeyInfo{}) + _, err = keys.NewHardwarePrivateKey(ctx, s, "" /*slot*/, keys.PrivateKeyPolicyHardwareKeyTouch, hardwarekey.PrivateKeyInfo{}) require.NoError(t, err) } @@ -138,7 +139,7 @@ func TestOverwritePrompt(t *testing.T) { resetYubikey(t, y) // Set a non-teleport certificate in the slot. - err = y.SetMetadataCertificate(pivSlot, pkix.Name{Organization: []string{"not-teleport"}}) + err = y.SetMetadataCertificate(touchSlot, pkix.Name{Organization: []string{"not-teleport"}}) require.NoError(t, err) testOverwritePrompt(t) @@ -148,7 +149,7 @@ func TestOverwritePrompt(t *testing.T) { resetYubikey(t, y) // Generate a key that does not require touch in the slot that Teleport expects to require touch. - _, err := keys.NewHardwarePrivateKey(ctx, s, keys.PIVSlot(pivSlot.String()), keys.PrivateKeyPolicyHardwareKey, keys.KeyInfo{}) + _, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PIVSlot(touchSlot.String()), keys.PrivateKeyPolicyHardwareKey, hardwarekey.PrivateKeyInfo{}) require.NoError(t, err) testOverwritePrompt(t) @@ -156,17 +157,17 @@ func TestOverwritePrompt(t *testing.T) { } // resetYubikey connects to the first yubiKey and resets it to defaults. -func resetYubikey(t *testing.T, y *keys.YubiKey) { +func resetYubikey(t *testing.T, y *piv.YubiKey) { t.Helper() require.NoError(t, y.Reset()) } -func setupPINPrompt(t *testing.T, y *keys.YubiKey) { +func setupPINPrompt(t *testing.T, y *piv.YubiKey) { t.Helper() // Set pin for tests. const testPIN = "123123" - require.NoError(t, y.SetPIN(piv.DefaultPIN, testPIN)) + require.NoError(t, y.SetPIN(pivgo.DefaultPIN, testPIN)) // Handle PIN prompt. oldStdin := prompt.Stdin() diff --git a/api/utils/keys/yubikey_unavailable.go b/api/utils/keys/piv/yubikey_unavailable.go similarity index 59% rename from api/utils/keys/yubikey_unavailable.go rename to api/utils/keys/piv/yubikey_unavailable.go index 2b3b6bf817a3a..77754ed58be0b 100644 --- a/api/utils/keys/yubikey_unavailable.go +++ b/api/utils/keys/piv/yubikey_unavailable.go @@ -13,34 +13,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -package keys +package piv import ( "context" "crypto" + "errors" "io" "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) -func NewYubiKeyPIVService(ctx context.Context, _ HardwareKeyPrompt) HardwareKeyService { +var errPIVUnavailable = errors.New("PIV is unavailable in current build") + +func NewYubiKeyPIVService(ctx context.Context, _ hardwarekey.PrivateKeyRef) *unavailableYubiKeyPIVService { return &unavailableYubiKeyPIVService{} } type unavailableYubiKeyPIVService struct{} -func (s *unavailableYubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy) (*HardwarePrivateKeyRef, error) { +func (s *unavailableYubiKeyPIVService) NewPrivateKey(_ context.Context, _ hardwarekey.PIVSlot, _ hardwarekey.PromptPolicy) (*hardwarekey.PrivateKeyRef, error) { return nil, trace.Wrap(errPIVUnavailable) } // Sign performs a cryptographic signature using the specified hardware // private key and provided signature parameters. -func (s *unavailableYubiKeyPIVService) Sign(ctx context.Context, ref HardwarePrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { +func (s *unavailableYubiKeyPIVService) Sign(_ context.Context, _ hardwarekey.PrivateKeyRef, _ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) { return nil, trace.Wrap(errPIVUnavailable) } -func (s *unavailableYubiKeyPIVService) SetPrompt(prompt HardwareKeyPrompt) {} - -func (_ PIVSlot) validate() error { - return trace.Wrap(errPIVUnavailable) -} +func (s *unavailableYubiKeyPIVService) SetPrompt(_ hardwarekey.Prompt) {} diff --git a/api/utils/keys/policy.go b/api/utils/keys/policy.go index f9dc57e5dbb64..85455ddc96972 100644 --- a/api/utils/keys/policy.go +++ b/api/utils/keys/policy.go @@ -189,3 +189,13 @@ func IsPrivateKeyPolicyError(err error) bool { } return privateKeyPolicyErrRegex.MatchString(err.Error()) } + +// AttestationData is attested information about the hardware private key matching the public key. +type AttestationData struct { + // PublicKeyDER is the public key in PKIX, ASN.1 DER form. + PublicKeyDER []byte `json:"public_key"` + // PrivateKeyPolicy specifies the private key policy supported by the associated private key. + PrivateKeyPolicy PrivateKeyPolicy `json:"private_key_policy"` + // SerialNumber is the serial number of the Attested hardware key. + SerialNumber uint32 `json:"serial_number"` +} diff --git a/api/utils/keys/policy_piv.go b/api/utils/keys/policy_piv.go new file mode 100644 index 0000000000000..a99fc532f3010 --- /dev/null +++ b/api/utils/keys/policy_piv.go @@ -0,0 +1,45 @@ +//go:build piv && !pivtest + +/* +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 keys + +import ( + "github.com/go-piv/piv-go/piv" +) + +// GetPrivateKeyPolicyFromAttestation returns the PrivateKeyPolicy satisfied by the given hardware key attestation. +// TODO(Joerger): Move to /e where this is used. +func GetPrivateKeyPolicyFromAttestation(att *piv.Attestation) PrivateKeyPolicy { + if att == nil { + return PrivateKeyPolicyNone + } + + isTouchPolicy := att.TouchPolicy == piv.TouchPolicyCached || + att.TouchPolicy == piv.TouchPolicyAlways + + isPINPolicy := att.PINPolicy == piv.PINPolicyOnce || + att.PINPolicy == piv.PINPolicyAlways + + switch { + case isPINPolicy && isTouchPolicy: + return PrivateKeyPolicyHardwareKeyTouchAndPIN + case isPINPolicy: + return PrivateKeyPolicyHardwareKeyPIN + case isTouchPolicy: + return PrivateKeyPolicyHardwareKeyTouch + default: + return PrivateKeyPolicyHardwareKey + } +} diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index f013e732aae0b..e6f93a16212cb 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -32,6 +32,7 @@ import ( "github.com/gravitational/trace" "golang.org/x/crypto/ssh" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/api/utils/sshutils/ppk" ) @@ -95,23 +96,21 @@ func NewSoftwarePrivateKey(signer crypto.Signer) (*PrivateKey, error) { // NewHardwarePrivateKey creates or retrieves a hardware private key from from the given hardware key // service that matches the given PIV slot and private key policy, returning the hardware private key // as a [PrivateKey]. -func NewHardwarePrivateKey(ctx context.Context, s HardwareKeyService, customSlot PIVSlot, requiredPolicy PrivateKeyPolicy, keyInfo KeyInfo) (*PrivateKey, error) { +func NewHardwarePrivateKey(ctx context.Context, s hardwarekey.Service, customSlot hardwarekey.PIVSlot, requiredPolicy PrivateKeyPolicy, keyInfo hardwarekey.PrivateKeyInfo) (*PrivateKey, error) { if s == nil { return nil, trace.BadParameter("cannot create a new hardware private key without a hardware key service provided") } - keyRef, err := s.NewPrivateKey(ctx, customSlot, requiredPolicy) + keyRef, err := s.NewPrivateKey(ctx, customSlot, hardwarekey.PromptPolicy{ + TouchRequired: requiredPolicy.IsHardwareKeyTouchVerified(), + PINRequired: requiredPolicy.IsHardwareKeyPINVerified(), + }) if err != nil { return nil, trace.Wrap(err) } - hwPrivateKey := &HardwarePrivateKey{ - service: s, - ref: keyRef, - keyInfo: keyInfo, - } - - encodedKeyRef, err := encodeHardwarePrivateKeyRef(keyRef) + hwPrivateKey := hardwarekey.NewPrivateKey(s, keyRef, keyInfo) + encodedKeyRef, err := hardwarekey.EncodeHardwarePrivateKeyRef(keyRef) if err != nil { return nil, trace.Wrap(err) } @@ -226,9 +225,9 @@ func (k *PrivateKey) SoftwarePrivateKeyPEM() ([]byte, error) { } // GetAttestationStatement returns this key's AttestationStatement. If the key is -// not a [HardwarePrivateKey], this method returns nil. -func (k *PrivateKey) GetAttestationStatement() *AttestationStatement { - if hwpk, ok := k.Signer.(*HardwarePrivateKey); ok { +// not a [hardwarekey.PrivateKey], this method returns nil. +func (k *PrivateKey) GetAttestationStatement() *hardwarekey.AttestationStatement { + if hwpk, ok := k.Signer.(*hardwarekey.PrivateKey); ok { return hwpk.GetAttestationStatement() } // Just return a nil attestation statement and let this key fail any attestation checks. @@ -237,21 +236,33 @@ func (k *PrivateKey) GetAttestationStatement() *AttestationStatement { // GetPrivateKeyPolicy returns this key's PrivateKeyPolicy. func (k *PrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy { - if hwpk, ok := k.Signer.(*HardwarePrivateKey); ok { - return hwpk.GetPrivateKeyPolicy() + if hwpk, ok := k.Signer.(*hardwarekey.PrivateKey); ok { + switch hwpk.GetPromptPolicy() { + case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: false}: + return PrivateKeyPolicyHardwareKey + + case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: false}: + return PrivateKeyPolicyHardwareKeyTouch + + case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: true}: + return PrivateKeyPolicyHardwareKeyTouchAndPIN + + case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: true}: + return PrivateKeyPolicyHardwareKeyPIN + } } return PrivateKeyPolicyNone } -// IsHardware returns true if [k] is a [HardwarePrivateKey]. +// IsHardware returns true if [k] is a [hardwarekey.PrivateKey]. func (k *PrivateKey) IsHardware() bool { - _, ok := k.Signer.(*HardwarePrivateKey) + _, ok := k.Signer.(*hardwarekey.PrivateKey) return ok } -// WarmupHardwareKey checks if this is a [HardwarePrivateKey] and warms it up if it is. +// WarmupHardwareKey checks if this is a [hardwarekey.PrivateKey] and warms it up if it is. func (k *PrivateKey) WarmupHardwareKey(ctx context.Context) error { - if hwpk, ok := k.Signer.(*HardwarePrivateKey); ok { + if hwpk, ok := k.Signer.(*hardwarekey.PrivateKey); ok { return hwpk.WarmupHardwareKey(ctx) } return nil @@ -273,31 +284,24 @@ func LoadPrivateKey(keyFile string) (*PrivateKey, error) { // ParsePrivateKeyOptions contains config options for ParsePrivateKey. type ParsePrivateKeyOptions struct { - HardwareKeyService HardwareKeyService - KeyInfo KeyInfo + HardwareKeyService hardwarekey.Service + KeyInfo hardwarekey.PrivateKeyInfo } // ParsePrivateKeyOpt applies configuration options. type ParsePrivateKeyOpt func(o *ParsePrivateKeyOptions) // WithHardwareKeyService sets the hardware key service. -func WithHardwareKeyService(hwKeyService HardwareKeyService) ParsePrivateKeyOpt { +func WithHardwareKeyService(hwKeyService hardwarekey.Service) ParsePrivateKeyOpt { return func(o *ParsePrivateKeyOptions) { o.HardwareKeyService = hwKeyService } } -// KeyInfo includes info relevant to the key being parsed. Useful for adding context -// to hardware key pin/touch prompts when performing signatures. -type KeyInfo struct { - // ProxyHost is the root proxy hostname that a key is associated with. - ProxyHost string -} - // WithKeyInfo adds contextual key info to the parsed private key. func WithKeyInfo(proxyHost string) ParsePrivateKeyOpt { return func(o *ParsePrivateKeyOptions) { - o.KeyInfo = KeyInfo{ProxyHost: proxyHost} + o.KeyInfo = hardwarekey.PrivateKeyInfo{ProxyHost: proxyHost} } } @@ -320,16 +324,13 @@ func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, er return nil, trace.BadParameter("cannot parse hardware private key without an initialized hardware key service") } - keyRef, err := decodeHardwarePrivateKeyRef(block.Bytes) + keyRef, err := hardwarekey.DecodeHardwarePrivateKeyRef(block.Bytes) if err != nil { return nil, trace.Wrap(err, "failed to parse hardware private key") } - return NewPrivateKey(&HardwarePrivateKey{ - service: appliedOpts.HardwareKeyService, - ref: keyRef, - keyInfo: appliedOpts.KeyInfo, - }, keyPEM) + hwPrivateKey := hardwarekey.NewPrivateKey(appliedOpts.HardwareKeyService, keyRef, appliedOpts.KeyInfo) + return NewPrivateKey(hwPrivateKey, keyPEM) case OpenSSHPrivateKeyType: priv, err := ssh.ParseRawPrivateKey(keyPEM) if err != nil { diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index 3eb7365d2d99c..263ead5f3f221 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -40,7 +40,7 @@ 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" + "github.com/gravitational/teleport/api/utils/keys/piv" 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,7 +248,7 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer // db cert has expired. Clock: fakeClock, WebauthnLogin: webauthnLogin, - HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -880,7 +880,7 @@ func testTeletermAppGatewayTargetPortValidation(t *testing.T, pack *appaccess.Pa storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) daemonService, err := daemon.New(daemon.Config{ diff --git a/integration/teleterm_test.go b/integration/teleterm_test.go index 671c7e2ad1ba3..76fa25b674516 100644 --- a/integration/teleterm_test.go +++ b/integration/teleterm_test.go @@ -43,7 +43,7 @@ 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" + "github.com/gravitational/teleport/api/utils/keys/piv" 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,7 +258,7 @@ func testAddingRootCluster(t *testing.T, pack *dbhelpers.DatabasePack, creds *he storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) @@ -291,7 +291,7 @@ func testListRootClustersReturnsLoggedInUser(t *testing.T, pack *dbhelpers.Datab storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) @@ -374,7 +374,7 @@ func testGetClusterReturnsPropertiesFromAuthServer(t *testing.T, pack *dbhelpers storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) @@ -427,7 +427,7 @@ func testHeadlessWatcher(t *testing.T, pack *dbhelpers.DatabasePack, creds *help storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -496,7 +496,7 @@ func testClientCache(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers. Dir: tc.KeysDir, Clock: storageFakeClock, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -756,7 +756,7 @@ func testCreateConnectMyComputerRole(t *testing.T, pack *dbhelpers.DatabasePack) storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -873,7 +873,7 @@ func testCreateConnectMyComputerToken(t *testing.T, pack *dbhelpers.DatabasePack InsecureSkipVerify: tc.InsecureSkipVerify, Clock: fakeClock, WebauthnLogin: webauthnLogin, - HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -934,7 +934,7 @@ func testWaitForConnectMyComputerNodeJoin(t *testing.T, pack *dbhelpers.Database storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -1019,7 +1019,7 @@ func testDeleteConnectMyComputerNode(t *testing.T, pack *dbhelpers.DatabasePack) storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -1247,7 +1247,7 @@ func testListDatabaseUsers(t *testing.T, pack *dbhelpers.DatabasePack) { storage, err := clusters.NewStorage(clusters.Config{ Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, - HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(ctx, nil /*prompt*/), }) require.NoError(t, err) diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 67fa7130f93a3..0080cdb2af27e 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -48,7 +48,7 @@ import ( apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/api/types/wrappers" 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/entitlements" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/auth/clusterconfig/clusterconfigv1" @@ -3447,9 +3447,9 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC return nil, trace.Wrap(err) } sshAttestationStatement, tlsAttestationStatement := authclient.UserAttestationStatements( - keys.AttestationStatementFromProto(req.AttestationStatement), //nolint:staticcheck // SA1019. Checking deprecated field that may be sent by older clients. - keys.AttestationStatementFromProto(req.SSHPublicKeyAttestationStatement), - keys.AttestationStatementFromProto(req.TLSPublicKeyAttestationStatement), + hardwarekey.AttestationStatementFromProto(req.AttestationStatement), //nolint:staticcheck // SA1019. Checking deprecated field that may be sent by older clients. + hardwarekey.AttestationStatementFromProto(req.SSHPublicKeyAttestationStatement), + hardwarekey.AttestationStatementFromProto(req.TLSPublicKeyAttestationStatement), ) // Generate certificate, note that the roles TTL will be ignored because diff --git a/lib/auth/github.go b/lib/auth/github.go index a9b834722f21f..1d626d4e02e51 100644 --- a/lib/auth/github.go +++ b/lib/auth/github.go @@ -40,7 +40,7 @@ import ( apidefaults "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" - "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/client/sso" @@ -699,9 +699,9 @@ func (a *Server) makeGithubAuthResponse( return nil, trace.Wrap(err) } sshAttestationStatement, tlsAttestationStatement := authclient.UserAttestationStatements( - keys.AttestationStatementFromProto(req.AttestationStatement), //nolint:staticcheck // SA1019. Checking deprecated field that may be sent by older clients. - keys.AttestationStatementFromProto(req.SshAttestationStatement), - keys.AttestationStatementFromProto(req.TlsAttestationStatement), + hardwarekey.AttestationStatementFromProto(req.AttestationStatement), //nolint:staticcheck // SA1019. Checking deprecated field that may be sent by older clients. + hardwarekey.AttestationStatementFromProto(req.SshAttestationStatement), + hardwarekey.AttestationStatementFromProto(req.TlsAttestationStatement), ) if len(sshPublicKey)+len(tlsPublicKey) > 0 { sshCert, tlsCert, err := a.CreateSessionCerts(ctx, &SessionCertsRequest{ diff --git a/lib/client/api.go b/lib/client/api.go index 24377db04ff4a..81fcf8a6024ac 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -71,6 +71,8 @@ import ( apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/api/utils/grpc/interceptors" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/auth/touchid" @@ -447,7 +449,7 @@ type Config struct { PrivateKeyPolicy keys.PrivateKeyPolicy // PIVSlot specifies a specific PIV slot to use with hardware key support. - PIVSlot keys.PIVSlot + PIVSlot hardwarekey.PIVSlot // LoadAllCAs indicates that tsh should load the CAs of all clusters // instead of just the current cluster. @@ -1281,7 +1283,7 @@ func NewClient(c *Config) (tc *TeleportClient, err error) { } else { // TODO (Joerger): init hardware key service (and client store) earlier where it can // be properly shared. - hardwareKeyService := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) + hardwareKeyService := piv.NewYubiKeyService(context.TODO(), &hardwarekey.CLIPrompt{}) tc.ClientStore = NewFSClientStore(c.KeysDir, hardwareKeyService) if c.AddKeysToAgent == AddKeysToAgentOnly { // Store client keys in memory, but still save trusted certs and profile to disk. @@ -3997,7 +3999,7 @@ func (tc *TeleportClient) GetNewLoginKeyRing(ctx context.Context) (keyRing *KeyR if tc.PIVSlot != "" { log.DebugContext(ctx, "Using PIV slot specified by client or server settings", "piv_slot", tc.PIVSlot) } - priv, err := tc.ClientStore.NewHardwarePrivateKey(ctx, tc.PIVSlot, tc.PrivateKeyPolicy, keys.KeyInfo{ + priv, err := tc.ClientStore.NewHardwarePrivateKey(ctx, tc.PIVSlot, tc.PrivateKeyPolicy, hardwarekey.PrivateKeyInfo{ ProxyHost: tc.WebProxyHost(), }) if err != nil { diff --git a/lib/client/client_store.go b/lib/client/client_store.go index 3c3b2e82d0714..c736835deb60e 100644 --- a/lib/client/client_store.go +++ b/lib/client/client_store.go @@ -32,6 +32,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/utils" ) @@ -44,7 +45,7 @@ import ( // key store and an FS (~/.tsh) profile and trusted certs store. type Store struct { log *slog.Logger - hwKeyService keys.HardwareKeyService + hwKeyService hardwarekey.Service KeyStore TrustedCertsStore @@ -52,7 +53,7 @@ type Store struct { } // NewMemClientStore initializes an FS backed client store with the given base dir. -func NewFSClientStore(dirPath string, hwKeyService keys.HardwareKeyService) *Store { +func NewFSClientStore(dirPath string, hwKeyService hardwarekey.Service) *Store { dirPath = profile.FullProfilePath(dirPath) return &Store{ log: slog.With(teleport.ComponentKey, teleport.ComponentKeyStore), @@ -63,12 +64,12 @@ func NewFSClientStore(dirPath string, hwKeyService keys.HardwareKeyService) *Sto } } -func (s *Store) NewHardwarePrivateKey(ctx context.Context, customSlot keys.PIVSlot, requiredPolicy keys.PrivateKeyPolicy, keyInfo keys.KeyInfo) (*keys.PrivateKey, error) { +func (s *Store) NewHardwarePrivateKey(ctx context.Context, customSlot hardwarekey.PIVSlot, requiredPolicy keys.PrivateKeyPolicy, keyInfo hardwarekey.PrivateKeyInfo) (*keys.PrivateKey, error) { return keys.NewHardwarePrivateKey(ctx, s.hwKeyService, customSlot, requiredPolicy, keyInfo) } // NewMemClientStore initializes a new in-memory client store. -func NewMemClientStore(hwKeyService keys.HardwareKeyService) *Store { +func NewMemClientStore(hwKeyService hardwarekey.Service) *Store { return &Store{ log: slog.With(teleport.ComponentKey, teleport.ComponentKeyStore), hwKeyService: hwKeyService, diff --git a/lib/client/identityfile/identity.go b/lib/client/identityfile/identity.go index 1e42622a49832..2c519a7a99caf 100644 --- a/lib/client/identityfile/identity.go +++ b/lib/client/identityfile/identity.go @@ -41,6 +41,7 @@ import ( "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/utils/keypaths" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/kube/kubeconfig" @@ -838,7 +839,7 @@ func KeyRingFromIdentityFile(identityPath, proxyHost, clusterName string) (*clie // This is necessary because identity files do not store the proxy address. // Additionally, the [clusterName] argument can ve used to target a leaf cluster // rather than the default root cluster. -func NewClientStoreFromIdentityFile(identityFile, proxyAddr, clusterName string, hwKeyService keys.HardwareKeyService) (*client.Store, error) { +func NewClientStoreFromIdentityFile(identityFile, proxyAddr, clusterName string, hwKeyService hardwarekey.Service) (*client.Store, error) { clientStore := client.NewMemClientStore(hwKeyService) if err := LoadIdentityFileIntoClientStore(clientStore, identityFile, proxyAddr, clusterName); err != nil { return nil, trace.Wrap(err) diff --git a/lib/client/keystore.go b/lib/client/keystore.go index 44355cf890ba6..db010574b7971 100644 --- a/lib/client/keystore.go +++ b/lib/client/keystore.go @@ -39,6 +39,7 @@ import ( "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/utils/keypaths" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" apisshutils "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/lib/utils" ) @@ -73,7 +74,7 @@ type KeyStore interface { // GetKeyRing returns the user's key ring including the specified certs. The // key's TrustedCerts will be nil and should be filled in using a // TrustedCertsStore. - GetKeyRing(idx KeyRingIndex, hwks keys.HardwareKeyService, opts ...CertOption) (*KeyRing, error) + GetKeyRing(idx KeyRingIndex, hwks hardwarekey.Service, opts ...CertOption) (*KeyRing, error) // DeleteKeyRing deletes the user's key with all its certs. DeleteKeyRing(idx KeyRingIndex) error @@ -524,7 +525,7 @@ func (e *LegacyCertPathError) Unwrap() error { // GetKeyRing returns the user's key including the specified certs. // If the key is not found, returns trace.NotFound error. -func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks keys.HardwareKeyService, opts ...CertOption) (*KeyRing, error) { +func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks hardwarekey.Service, opts ...CertOption) (*KeyRing, error) { if len(opts) > 0 { if err := idx.Check(); err != nil { return nil, trace.Wrap(err, "GetKeyRing with CertOptions requires a fully specified KeyRingIndex") @@ -568,7 +569,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks keys.HardwareKeyService, return keyRing, nil } -func (fs *FSKeyStore) updateKeyRingWithCerts(o CertOption, hwks keys.HardwareKeyService, keyRing *KeyRing) error { +func (fs *FSKeyStore) updateKeyRingWithCerts(o CertOption, hwks hardwarekey.Service, keyRing *KeyRing) error { return trace.Wrap(o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, keys.WithHardwareKeyService(hwks), keys.WithKeyInfo(keyRing.ProxyHost))) } @@ -806,7 +807,7 @@ func (ms *MemKeyStore) AddKeyRing(keyRing *KeyRing) error { } // GetKeyRing returns the user's key ring including the specified certs. -func (ms *MemKeyStore) GetKeyRing(idx KeyRingIndex, _ keys.HardwareKeyService, opts ...CertOption) (*KeyRing, error) { +func (ms *MemKeyStore) GetKeyRing(idx KeyRingIndex, _ hardwarekey.Service, opts ...CertOption) (*KeyRing, error) { if len(opts) > 0 { if err := idx.Check(); err != nil { return nil, trace.Wrap(err, "GetKeyRing with CertOptions requires a fully specified KeyRingIndex") diff --git a/lib/client/profile.go b/lib/client/profile.go index 3dd48e53da17b..f12f6bab71f2d 100644 --- a/lib/client/profile.go +++ b/lib/client/profile.go @@ -35,7 +35,8 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/wrappers" "github.com/gravitational/teleport/api/utils/keypaths" - "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/sshca" "github.com/gravitational/teleport/lib/tlsca" @@ -597,7 +598,7 @@ func (p *ProfileStatus) DatabasesForCluster(clusterName string) ([]tlsca.RouteTo Username: p.Username, ClusterName: clusterName, } - hwks := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) + hwks := piv.NewYubiKeyService(context.TODO(), &hardwarekey.CLIPrompt{}) store := NewFSKeyStore(p.Dir) keyRing, err := store.GetKeyRing(idx, hwks, WithDBCerts{}) if err != nil { @@ -619,7 +620,7 @@ func (p *ProfileStatus) AppsForCluster(clusterName string) ([]tlsca.RouteToApp, ClusterName: clusterName, } - hwks := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) + hwks := piv.NewYubiKeyService(context.TODO(), &hardwarekey.CLIPrompt{}) store := NewFSKeyStore(p.Dir) keyRing, err := store.GetKeyRing(idx, hwks, WithAppCerts{}) if err != nil { diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index daa70d9f545c0..a37fc776b358c 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -44,7 +44,7 @@ import ( "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" 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/tlsutils" "github.com/gravitational/teleport/lib/automaticupgrades" "github.com/gravitational/teleport/lib/backend" @@ -1044,7 +1044,7 @@ type AuthenticationConfig struct { DefaultSessionTTL types.Duration `yaml:"default_session_ttl"` // Deprecated. HardwareKey.PIVSlot should be used instead. - PIVSlot keys.PIVSlot `yaml:"piv_slot,omitempty"` + PIVSlot hardwarekey.PIVSlot `yaml:"piv_slot,omitempty"` // HardwareKey holds settings related to hardware key support. // Requires Teleport Enterprise. @@ -1277,7 +1277,7 @@ func (dt *DeviceTrust) Parse() (*types.DeviceTrust, error) { type HardwareKey struct { // PIVSlot is a PIV slot that Teleport clients should use instead of the // default based on private key policy. For example, "9a" or "9e". - PIVSlot keys.PIVSlot `yaml:"piv_slot,omitempty"` + PIVSlot hardwarekey.PIVSlot `yaml:"piv_slot,omitempty"` // SerialNumberValidation contains optional settings for hardware key // serial number validation, including whether it is enabled. diff --git a/lib/modules/modules.go b/lib/modules/modules.go index da15b036bd78c..0692c3b8fc00c 100644 --- a/lib/modules/modules.go +++ b/lib/modules/modules.go @@ -38,6 +38,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/accesslist" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/entitlements" "github.com/gravitational/teleport/lib/automaticupgrades" "github.com/gravitational/teleport/lib/tlsca" @@ -288,7 +289,7 @@ type Modules interface { // IsOSSBuild returns if the binary was built without enterprise modules IsOSSBuild() bool // AttestHardwareKey attests a hardware key and returns its associated private key policy. - AttestHardwareKey(context.Context, interface{}, *keys.AttestationStatement, crypto.PublicKey, time.Duration) (*keys.AttestationData, error) + AttestHardwareKey(context.Context, interface{}, *hardwarekey.AttestationStatement, crypto.PublicKey, time.Duration) (*keys.AttestationData, error) // GenerateAccessRequestPromotions generates a list of valid promotions for given access request. GenerateAccessRequestPromotions(context.Context, AccessResourcesGetter, types.AccessRequest) (*types.AccessRequestAllowedPromotions, error) // GetSuggestedAccessLists generates a list of valid promotions for given access request. @@ -427,7 +428,7 @@ func (p *defaultModules) IsBoringBinary() bool { } // AttestHardwareKey attests a hardware key. -func (p *defaultModules) AttestHardwareKey(_ context.Context, _ interface{}, _ *keys.AttestationStatement, _ crypto.PublicKey, _ time.Duration) (*keys.AttestationData, error) { +func (p *defaultModules) AttestHardwareKey(_ context.Context, _ interface{}, _ *hardwarekey.AttestationStatement, _ crypto.PublicKey, _ time.Duration) (*keys.AttestationData, error) { // Default modules do not support attesting hardware keys. return nil, trace.NotFound("no attestation data for the given key") } diff --git a/lib/teleterm/clusters/config.go b/lib/teleterm/clusters/config.go index aecf6cc99d8a9..ed2613ad9dd20 100644 --- a/lib/teleterm/clusters/config.go +++ b/lib/teleterm/clusters/config.go @@ -25,7 +25,7 @@ import ( "github.com/jonboulle/clockwork" "github.com/gravitational/teleport" - "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/lib/client" ) @@ -45,7 +45,7 @@ type Config struct { // AddKeysToAgent is passed to [client.Config]. AddKeysToAgent string // HardwareKeyService is a service for interfacing with hardware keys. - HardwareKeyService keys.HardwareKeyService + HardwareKeyService hardwarekey.Service } // CheckAndSetDefaults checks the configuration for its validity and sets default values if needed diff --git a/lib/teleterm/daemon/daemon_test.go b/lib/teleterm/daemon/daemon_test.go index b40cbb08859de..078b8824036d1 100644 --- a/lib/teleterm/daemon/daemon_test.go +++ b/lib/teleterm/daemon/daemon_test.go @@ -40,7 +40,7 @@ import ( "google.golang.org/grpc/status" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/piv" 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,7 +348,7 @@ func TestUpdateTshdEventsServerAddress(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: homeDir, InsecureSkipVerify: true, - HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) @@ -383,7 +383,7 @@ func TestUpdateTshdEventsServerAddress_CredsErr(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: homeDir, InsecureSkipVerify: true, - HardwareKeyService: keys.NewYubiKeyPIVService(context.TODO(), nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(context.TODO(), nil /*prompt*/), }) require.NoError(t, err) @@ -485,7 +485,7 @@ func TestRetryWithRelogin(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(ctx, nil /*prompt*/), }) require.NoError(t, err) @@ -539,7 +539,7 @@ func TestConcurrentHeadlessAuthPrompts(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyService: keys.NewYubiKeyPIVService(ctx, nil /*prompt*/), + HardwareKeyService: piv.NewYubiKeyService(ctx, nil /*prompt*/), }) require.NoError(t, err) diff --git a/lib/teleterm/daemon/hardwarekeyprompt.go b/lib/teleterm/daemon/hardwarekeyprompt.go index 4346c7e9c7d39..08bbd958e24cf 100644 --- a/lib/teleterm/daemon/hardwarekeyprompt.go +++ b/lib/teleterm/daemon/hardwarekeyprompt.go @@ -23,7 +23,7 @@ import ( "github.com/gravitational/trace" - "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" ) @@ -44,7 +44,7 @@ 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) NewHardwareKeyPrompt() keys.HardwareKeyPrompt { +func (s *Service) NewHardwareKeyPrompt() hardwarekey.Prompt { return &hardwareKeyPrompter{s: s} } @@ -53,7 +53,7 @@ type hardwareKeyPrompter struct { } // Touch prompts the user to touch the hardware key. -func (h *hardwareKeyPrompter) Touch(ctx context.Context, keyInfo keys.KeyInfo) error { +func (h *hardwareKeyPrompter) Touch(ctx context.Context, keyInfo hardwarekey.PrivateKeyInfo) error { _, err := h.s.tshdEventsClient.PromptHardwareKeyTouch(ctx, &api.PromptHardwareKeyTouchRequest{ RootClusterUri: keyInfo.ProxyHost, }) @@ -64,10 +64,10 @@ func (h *hardwareKeyPrompter) Touch(ctx context.Context, keyInfo keys.KeyInfo) e } // AskPIN prompts the user for a PIN. -func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement keys.PINPromptRequirement, keyInfo keys.KeyInfo) (string, error) { +func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement hardwarekey.PINPromptRequirement, keyInfo hardwarekey.PrivateKeyInfo) (string, error) { res, err := h.s.tshdEventsClient.PromptHardwareKeyPIN(ctx, &api.PromptHardwareKeyPINRequest{ RootClusterUri: keyInfo.ProxyHost, - PinOptional: requirement == keys.PINOptional, + PinOptional: requirement == hardwarekey.PINOptional, }) if err != nil { return "", trace.Wrap(err) @@ -78,14 +78,14 @@ func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement keys.PINPr // 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, keyInfo keys.KeyInfo) (*keys.PINAndPUK, error) { +func (h *hardwareKeyPrompter) ChangePIN(ctx context.Context, keyInfo hardwarekey.PrivateKeyInfo) (*hardwarekey.PINAndPUK, error) { res, err := h.s.tshdEventsClient.PromptHardwareKeyPINChange(ctx, &api.PromptHardwareKeyPINChangeRequest{ RootClusterUri: keyInfo.ProxyHost, }) if err != nil { return nil, trace.Wrap(err) } - return &keys.PINAndPUK{ + return &hardwarekey.PINAndPUK{ PIN: res.Pin, PUK: res.Puk, PUKChanged: res.PukChanged, @@ -93,7 +93,7 @@ func (h *hardwareKeyPrompter) ChangePIN(ctx context.Context, keyInfo keys.KeyInf } // ConfirmSlotOverwrite asks the user if the slot's private key and certificate can be overridden. -func (h *hardwareKeyPrompter) ConfirmSlotOverwrite(ctx context.Context, message string, keyInfo keys.KeyInfo) (bool, error) { +func (h *hardwareKeyPrompter) ConfirmSlotOverwrite(ctx context.Context, message string, keyInfo hardwarekey.PrivateKeyInfo) (bool, error) { res, err := h.s.tshdEventsClient.ConfirmHardwareKeySlotOverwrite(ctx, &api.ConfirmHardwareKeySlotOverwriteRequest{ RootClusterUri: keyInfo.ProxyHost, Message: message, diff --git a/lib/teleterm/teleterm.go b/lib/teleterm/teleterm.go index 0cd6df0221139..75c7877e916bf 100644 --- a/lib/teleterm/teleterm.go +++ b/lib/teleterm/teleterm.go @@ -32,7 +32,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/lib/teleterm/apiserver" "github.com/gravitational/teleport/lib/teleterm/clusteridcache" "github.com/gravitational/teleport/lib/teleterm/clusters" @@ -43,7 +43,7 @@ import ( func Serve(ctx context.Context, cfg Config) error { // TODO(gzdunek): Move tshdEventsClient out of daemonService so that we can // set the prompt before creating Storage. - hardwareKeyService := keys.NewYubiKeyPIVService(ctx, nil /*prompt*/) + hardwareKeyService := piv.NewYubiKeyService(ctx, nil /*prompt*/) if err := cfg.CheckAndSetDefaults(); err != nil { return trace.Wrap(err) diff --git a/lib/vnet/profile_osconfig_provider_darwin.go b/lib/vnet/profile_osconfig_provider_darwin.go index 0b09631b18f7c..38707d6551860 100644 --- a/lib/vnet/profile_osconfig_provider_darwin.go +++ b/lib/vnet/profile_osconfig_provider_darwin.go @@ -27,7 +27,8 @@ import ( "github.com/gravitational/teleport/api/profile" "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/keys/piv" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" "github.com/gravitational/teleport/lib/vnet/daemon" @@ -60,7 +61,7 @@ func newProfileOSConfigProvider(tunName, ipv6Prefix, dnsAddr, homePath string, d return nil, trace.Wrap(err) } - hwKeyService := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) + hwKeyService := piv.NewYubiKeyService(context.TODO(), &hardwarekey.CLIPrompt{}) p := &profileOSConfigProvider{ clientStore: client.NewFSClientStore(homePath, hwKeyService), diff --git a/tool/tctl/common/config/profile.go b/tool/tctl/common/config/profile.go index 2fde2bd5488a7..8491c7e8f57a7 100644 --- a/tool/tctl/common/config/profile.go +++ b/tool/tctl/common/config/profile.go @@ -29,7 +29,8 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/metadata" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/identityfile" @@ -42,7 +43,7 @@ import ( func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authclient.Config, error) { ctx := context.TODO() - hwKeyService := keys.NewYubiKeyPIVService(ctx, &keys.CLIPrompt{}) + hwKeyService := piv.NewYubiKeyService(context.TODO(), &hardwarekey.CLIPrompt{}) proxyAddr := "" if len(ccf.AuthServerAddr) != 0 { diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 198565a02cc91..9f8306a94c561 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -67,7 +67,8 @@ import ( "github.com/gravitational/teleport/api/types/accesslist" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/api/types/wrappers" - "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/lib/asciitable" "github.com/gravitational/teleport/lib/auth/authclient" @@ -4385,7 +4386,7 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err } if cf.PIVSlot != "" { - c.PIVSlot = keys.PIVSlot(cf.PIVSlot) + c.PIVSlot = hardwarekey.PIVSlot(cf.PIVSlot) if err = c.PIVSlot.Validate(); err != nil { return nil, trace.Wrap(err) } @@ -4575,7 +4576,7 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err } func initClientStore(cf *CLIConf, proxy string) (*client.Store, error) { - hardwareKeyService := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) + hardwareKeyService := piv.NewYubiKeyService(cf.Context, &hardwarekey.CLIPrompt{}) switch { case cf.IdentityFileIn != "": diff --git a/tool/tsh/common/vnet_client_application.go b/tool/tsh/common/vnet_client_application.go index e6b1a1d150141..812c5a9b19829 100644 --- a/tool/tsh/common/vnet_client_application.go +++ b/tool/tsh/common/vnet_client_application.go @@ -28,7 +28,8 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/piv" vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" @@ -46,7 +47,7 @@ type vnetClientApplication struct { } func newVnetClientApplication(cf *CLIConf) (*vnetClientApplication, error) { - hwKeyService := keys.NewYubiKeyPIVService(context.TODO(), &keys.CLIPrompt{}) + hwKeyService := piv.NewYubiKeyService(cf.Context, &hardwarekey.CLIPrompt{}) clientStore := client.NewFSClientStore(cf.HomePath, hwKeyService) From aedb42779bc3ad24d9fec74f9b03e33435a2630f Mon Sep 17 00:00:00 2001 From: joerger Date: Thu, 13 Mar 2025 14:56:49 -0700 Subject: [PATCH 06/10] Cleanup. --- api/utils/keys/hardwarekey/cliprompt.go | 8 +- api/utils/keys/hardwarekey/hardwarekey.go | 54 +++-- api/utils/keys/hardwarekey/prompt.go | 27 ++- api/utils/keys/piv/yubikey.go | 107 ++++------ api/utils/keys/piv/yubikey_service.go | 223 +++++++++++++-------- api/utils/keys/piv/yubikey_service_fake.go | 47 +++-- api/utils/keys/piv/yubikey_service_test.go | 32 ++- api/utils/keys/piv/yubikey_unavailable.go | 6 +- api/utils/keys/privatekey.go | 25 ++- lib/client/api.go | 11 +- lib/client/client_store.go | 9 +- lib/client/keystore.go | 6 +- lib/teleterm/daemon/hardwarekeyprompt.go | 8 +- 13 files changed, 323 insertions(+), 240 deletions(-) diff --git a/api/utils/keys/hardwarekey/cliprompt.go b/api/utils/keys/hardwarekey/cliprompt.go index 09c052cc23cfe..7c205e9d9a80a 100644 --- a/api/utils/keys/hardwarekey/cliprompt.go +++ b/api/utils/keys/hardwarekey/cliprompt.go @@ -35,7 +35,7 @@ var ( type CLIPrompt struct{} -func (c *CLIPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement, _ PrivateKeyInfo) (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]" @@ -44,12 +44,12 @@ func (c *CLIPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement return password, trace.Wrap(err) } -func (c *CLIPrompt) Touch(_ context.Context, _ PrivateKeyInfo) error { +func (c *CLIPrompt) Touch(_ context.Context, _ ContextualKeyInfo) error { _, err := fmt.Fprintln(os.Stderr, "Tap your YubiKey") return trace.Wrap(err) } -func (c *CLIPrompt) ChangePIN(ctx context.Context, _ PrivateKeyInfo) (*PINAndPUK, error) { +func (c *CLIPrompt) ChangePIN(ctx context.Context, _ ContextualKeyInfo) (*PINAndPUK, error) { var pinAndPUK = &PINAndPUK{} for { fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PIN.\n") @@ -126,7 +126,7 @@ func (c *CLIPrompt) ChangePIN(ctx context.Context, _ PrivateKeyInfo) (*PINAndPUK return pinAndPUK, nil } -func (c *CLIPrompt) ConfirmSlotOverwrite(ctx context.Context, message string, _ PrivateKeyInfo) (bool, error) { +func (c *CLIPrompt) ConfirmSlotOverwrite(ctx context.Context, message string, _ ContextualKeyInfo) (bool, error) { confirmation, err := prompt.Confirmation(ctx, os.Stderr, prompt.Stdin(), message) return confirmation, trace.Wrap(err) } diff --git a/api/utils/keys/hardwarekey/hardwarekey.go b/api/utils/keys/hardwarekey/hardwarekey.go index d42109c1bc314..61bc4a7b2d0ff 100644 --- a/api/utils/keys/hardwarekey/hardwarekey.go +++ b/api/utils/keys/hardwarekey/hardwarekey.go @@ -30,15 +30,8 @@ import ( // Service for interfacing with hardware private keys. type Service interface { - // NewPrivateKey creates or retrieves a hardware private key from the given PIV slot matching - // the given policy and returns the details required to perform signatures with that key. - // - // If a customSlot is not provided, the service uses the default slot for the given policy: - // - !touch & !pin -> 9a - // - !touch & pin -> 9c - // - touch & pin -> 9d - // - touch & !pin -> 9e - NewPrivateKey(ctx context.Context, customSlot PIVSlot, policy PromptPolicy) (*PrivateKeyRef, error) + // NewPrivateKey creates or retrieves a hardware private key for the given config. + NewPrivateKey(ctx context.Context, config PrivateKeyConfig) (*PrivateKey, 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) @@ -52,9 +45,6 @@ type Service interface { type PrivateKey struct { service Service ref *PrivateKeyRef - // keyInfo contains additional key info which may be used to add context to prompts, - // such as the name of the Teleport user using the key. - keyInfo PrivateKeyInfo } // PrivateKeyRef references a specific hardware private key. @@ -70,11 +60,16 @@ type PrivateKeyRef struct { // AttestationStatement contains the hardware private key's attestation statement, which is // to attest the touch and pin requirements for this hardware private key during login. AttestationStatement *AttestationStatement `json:"attestation_statement"` + // ContextualKeyInfo contains contextual key info which may be used to add context to prompts, + // such as the name of the Teleport user using the key. This information is not saved encoded + // in JSON as this info may depend on the context in which the client is using the key. + ContextualKeyInfo ContextualKeyInfo } -// PrivateKeyInfo includes info relevant to the key being parsed. Useful for adding context -// to hardware key pin/touch prompts when performing signatures. -type PrivateKeyInfo struct { +// ContextualKeyInfo contains contextual information associated with a hardware [PrivateKey]. +// TODO(Joerger): This is not hardware key specific, so it may be better placed in a more general package +// if it is used more broadly, though moving this to the keys package would cause an import cycle. +type ContextualKeyInfo struct { // ProxyHost is the root proxy hostname that a key is associated with. ProxyHost string } @@ -88,11 +83,11 @@ type PromptPolicy struct { } // NewPrivateKey returns a [PrivateKey] for the given service and ref. -func NewPrivateKey(s Service, ref *PrivateKeyRef, keyInfo PrivateKeyInfo) *PrivateKey { +// keyInfo is an optional argument to supply additional contextual info. +func NewPrivateKey(s Service, ref *PrivateKeyRef) *PrivateKey { return &PrivateKey{ service: s, ref: ref, - keyInfo: keyInfo, } } @@ -131,17 +126,17 @@ func (h *PrivateKey) WarmupHardwareKey(ctx context.Context) error { return trace.Wrap(err, "failed to perform warmup signature with hardware private key") } -// encodeHardwarePrivateKeyRef encodes a [PrivateKeyRef] to JSON. -func EncodeHardwarePrivateKeyRef(ref *PrivateKeyRef) ([]byte, error) { - keyRefBytes, err := json.Marshal(ref) +// Encode encodes [h]'s [PrivateKeyRef] to JSON. +func (h *PrivateKey) EncodeKeyRef() ([]byte, error) { + keyRefBytes, err := json.Marshal(h.ref) if err != nil { return nil, trace.Wrap(err) } return keyRefBytes, nil } -// decodeHardwarePrivateKeyRef decodes a [PrivateKeyRef] from JSON. -func DecodeHardwarePrivateKeyRef(encodedKeyRef []byte) (*PrivateKeyRef, error) { +// DecodeKeyRef decodes a [PrivateKeyRef] from JSON. +func DecodeKeyRef(encodedKeyRef []byte) (*PrivateKeyRef, error) { // TODO: old clients would only have SerialNumber and SlotKey, gather missing information directly for backwards compatibility. keyRef := &PrivateKeyRef{} if err := json.Unmarshal(encodedKeyRef, keyRef); err != nil { @@ -189,6 +184,21 @@ func (r *PrivateKeyRef) UnmarshalJSON(b []byte) error { return nil } +// PrivateKeyConfig contains config for creating a new hardware private key. +type PrivateKeyConfig struct { + // Policy is a prompt policy to require for the hardware private key. + Policy PromptPolicy + // CustomSlot is a specific PIV slot to generate the hardware private key in. + // If unset, the default slot for the given policy will be used. + // - !touch & !pin -> 9a + // - !touch & pin -> 9c + // - touch & pin -> 9d + // - touch & !pin -> 9e + CustomSlot PIVSlot + // ContextualKeyInfo contains additional info to associate with the key. + ContextualKeyInfo ContextualKeyInfo +} + // PIVSlot is the string representation of a PIV slot. e.g. "9a". type PIVSlot string diff --git a/api/utils/keys/hardwarekey/prompt.go b/api/utils/keys/hardwarekey/prompt.go index 72545b82118a1..49ad85947b485 100644 --- a/api/utils/keys/hardwarekey/prompt.go +++ b/api/utils/keys/hardwarekey/prompt.go @@ -16,23 +16,25 @@ package hardwarekey import ( "context" + + "github.com/gravitational/trace" ) // Prompt provides methods to interact with a hardware [PrivateKey]. 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, keyInfo PrivateKeyInfo) (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, keyInfo PrivateKeyInfo) 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, keyInfo PrivateKeyInfo) (*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, keyInfo PrivateKeyInfo) (bool, error) + ConfirmSlotOverwrite(ctx context.Context, message string, keyInfo ContextualKeyInfo) (bool, error) } // PINPromptRequirement specifies whether a PIN is required. @@ -56,6 +58,23 @@ type PINAndPUK struct { PUKChanged bool } +// Validate [p]. +func (p PINAndPUK) Validate() error { + if !IsPINLengthValid(p.PIN) { + return trace.BadParameter("PIN must be 6-8 characters long") + } + if p.PIN == defaultPIN { + return trace.BadParameter("The default PIN is not supported") + } + if !IsPINLengthValid(p.PUK) { + return trace.BadParameter("PUK must be 6-8 characters long") + } + if p.PUK == defaultPUK { + return trace.BadParameter("The default PUK is not supported") + } + return nil +} + // IsPINLengthValid returns whether the given PIV PIN, or PUK, is of valid length (6-8 characters). func IsPINLengthValid(pin string) bool { return len(pin) >= 6 && len(pin) <= 8 diff --git a/api/utils/keys/piv/yubikey.go b/api/utils/keys/piv/yubikey.go index 6cb30ef6ae384..6f3d4b50b4153 100644 --- a/api/utils/keys/piv/yubikey.go +++ b/api/utils/keys/piv/yubikey.go @@ -23,10 +23,8 @@ import ( "crypto/rand" "crypto/x509" "crypto/x509/pkix" - "fmt" "io" "math/big" - "os" "strings" "sync" "time" @@ -49,7 +47,7 @@ type YubiKey struct { // out other programs during extended program executions (like "tsh proxy ssh"), // this connections is opportunistically formed and released after being // unused for a few seconds. - *sharedPIVConnection + c *sharedPIVConnection // serialNumber is the YubiKey's 8 digit serial number. serialNumber uint32 // version is the YubiKey's version. @@ -107,16 +105,16 @@ func findYubiKeyCards() ([]string, error) { func newYubiKey(card string) (*YubiKey, error) { y := &YubiKey{ - sharedPIVConnection: &sharedPIVConnection{ + c: &sharedPIVConnection{ card: card, }, } var err error - if y.serialNumber, err = y.getSerialNumber(); err != nil { + if y.serialNumber, err = y.c.getSerialNumber(); err != nil { return nil, trace.Wrap(err) } - if y.version, err = y.getVersion(); err != nil { + if y.version, err = y.c.getVersion(); err != nil { return nil, trace.Wrap(err) } @@ -148,7 +146,7 @@ func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, prom // process. Without this, the connection will be released, // leading to a failure when providing PIN or touch input: // "verify pin: transmitting request: the supplied handle was invalid". - release, err := y.connect() + release, err := y.c.connect() if err != nil { return nil, trace.Wrap(err) } @@ -163,7 +161,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, hardwarekey.PrivateKeyInfo{}) + err := prompt.Touch(ctx, hardwarekey.ContextualKeyInfo{}) if err != nil { // Cancel the entire function when an error occurs. // This is typically used for aborting the prompt. @@ -185,7 +183,7 @@ func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, prom defer touchPromptDelayTimer.Reset(signTouchPromptDelay) } } - pass, err := prompt.AskPIN(ctx, hardwarekey.PINRequired, hardwarekey.PrivateKeyInfo{}) + pass, err := prompt.AskPIN(ctx, hardwarekey.PINRequired, hardwarekey.ContextualKeyInfo{}) return pass, trace.Wrap(err) } @@ -206,7 +204,7 @@ func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, prom // the signature fails. manualRetryWithPIN := false fw531 := piv.Version{Major: 5, Minor: 3, Patch: 1} - if auth.PINPolicy == piv.PINPolicyOnce && y.conn.Version() == fw531 { + if auth.PINPolicy == piv.PINPolicyOnce && y.c.conn.Version() == fw531 { // Set the keys PIN policy to never to skip the insVerify check. If PIN was provided in // a previous recent call, the signature will succeed as expected of the "once" policy. auth.PINPolicy = piv.PINPolicyNever @@ -218,7 +216,7 @@ func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, prom return nil, trace.Wrap(err) } - privateKey, err := y.privateKey(pivSlot, ref.PublicKey, auth) + privateKey, err := y.c.privateKey(pivSlot, ref.PublicKey, auth) if err != nil { return nil, trace.Wrap(err) } @@ -241,7 +239,7 @@ func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, prom if err != nil { return nil, trace.Wrap(err) } - if err := y.verifyPIN(pin); err != nil { + if err := y.c.verifyPIN(pin); err != nil { return nil, trace.Wrap(err) } signature, err := abandonableSign(ctx, signer, rand, digest, opts) @@ -285,7 +283,7 @@ func abandonableSign(ctx context.Context, signer crypto.Signer, rand io.Reader, // Reset resets the YubiKey PIV module to default settings. func (y *YubiKey) Reset() error { - err := y.reset() + err := y.c.reset() return trace.Wrap(err) } @@ -307,17 +305,17 @@ func (y *YubiKey) generatePrivateKey(slot piv.Slot, policy hardwarekey.PromptPol TouchPolicy: touchPolicy, } - pub, err := y.generateKey(piv.DefaultManagementKey, slot, opts) + pub, err := y.c.generateKey(piv.DefaultManagementKey, slot, opts) if err != nil { return nil, trace.Wrap(err) } - slotCert, err := y.attest(slot) + slotCert, err := y.c.attest(slot) if err != nil { return nil, trace.Wrap(err) } - attCert, err := y.attestationCertificate() + attCert, err := y.c.attestationCertificate() if err != nil { return nil, trace.Wrap(err) } @@ -355,25 +353,25 @@ func (y *YubiKey) SetMetadataCertificate(slot piv.Slot, subject pkix.Name) error return trace.Wrap(err) } - err = y.setCertificate(piv.DefaultManagementKey, slot, cert) + err = y.c.setCertificate(piv.DefaultManagementKey, slot, cert) return trace.Wrap(err) } // getCertificate gets a certificate from the given PIV slot. func (y *YubiKey) getCertificate(slot piv.Slot) (*x509.Certificate, error) { - cert, err := y.certificate(slot) + cert, err := y.c.certificate(slot) return cert, trace.Wrap(err) } // attestKey attests the key in the given PIV slot. // The key's public key can be found in the returned slotCert. func (y *YubiKey) attestKey(slot piv.Slot) (slotCert *x509.Certificate, attCert *x509.Certificate, att *piv.Attestation, err error) { - slotCert, err = y.attest(slot) + slotCert, err = y.c.attest(slot) if err != nil { return nil, nil, nil, trace.Wrap(err) } - attCert, err = y.attestationCertificate() + attCert, err = y.c.attestationCertificate() if err != nil { return nil, nil, nil, trace.Wrap(err) } @@ -388,30 +386,36 @@ func (y *YubiKey) attestKey(slot piv.Slot) (slotCert *x509.Certificate, attCert // SetPIN sets the YubiKey PIV PIN. This doesn't require user interaction like touch, just the correct old PIN. func (y *YubiKey) SetPIN(oldPin, newPin string) error { - err := y.setPIN(oldPin, newPin) + err := y.c.setPIN(oldPin, newPin) return trace.Wrap(err) } -// 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 (y *YubiKey) checkOrSetPIN(ctx context.Context, prompt hardwarekey.Prompt) error { - pin, err := prompt.AskPIN(ctx, hardwarekey.PINOptional, hardwarekey.PrivateKeyInfo{}) +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) + return "", trace.Wrap(err) + } + + if err := pinAndPUK.Validate(); err != nil { + return "", trace.Wrap(err) } - switch pin { - case piv.DefaultPIN: - fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN) - fallthrough - case "": - if pin, err = y.setPINAndPUKFromDefault(ctx, prompt); err != nil { - return trace.Wrap(err) + if pinAndPUK.PUKChanged { + if err := y.c.setPUK(piv.DefaultPUK, pinAndPUK.PUK); err != nil { + return "", trace.Wrap(err) } } - return trace.Wrap(y.verifyPIN(pin)) + if err := y.c.unblock(pinAndPUK.PUK, pinAndPUK.PIN); err != nil { + return "", trace.Wrap(err) + } + + return pinAndPUK.PIN, nil +} + +func (y *YubiKey) verifyPIN(pin string) error { + err := y.c.verifyPIN(pin) + return trace.Wrap(err) } type sharedPIVConnection struct { @@ -618,39 +622,6 @@ func (c *sharedPIVConnection) verifyPIN(pin string) error { return trace.Wrap(c.conn.VerifyPIN(pin)) } -func (c *sharedPIVConnection) setPINAndPUKFromDefault(ctx context.Context, prompt hardwarekey.Prompt) (string, error) { - pinAndPUK, err := prompt.ChangePIN(ctx, hardwarekey.PrivateKeyInfo{}) - if err != nil { - return "", trace.Wrap(err) - } - // YubiKey requires that PIN and PUK be 6-8 characters. - // Verify that we get valid values from the prompt. - if !hardwarekey.IsPINLengthValid(pinAndPUK.PIN) { - return "", trace.BadParameter("PIN must be 6-8 characters long") - } - if pinAndPUK.PIN == piv.DefaultPIN { - return "", trace.BadParameter("The default PIN is not supported") - } - if !hardwarekey.IsPINLengthValid(pinAndPUK.PUK) { - return "", trace.BadParameter("PUK must be 6-8 characters long") - } - if pinAndPUK.PUK == piv.DefaultPUK { - return "", trace.BadParameter("The default PUK is not supported") - } - - if pinAndPUK.PUKChanged { - if err := c.setPUK(piv.DefaultPUK, pinAndPUK.PUK); err != nil { - return "", trace.Wrap(err) - } - } - - if err := c.unblock(pinAndPUK.PUK, pinAndPUK.PIN); err != nil { - return "", trace.Wrap(err) - } - - return pinAndPUK.PIN, nil -} - func isRetryError(err error) bool { const retryError = "connecting to smart card: the smart card cannot be accessed because of other connections outstanding" return strings.Contains(err.Error(), retryError) diff --git a/api/utils/keys/piv/yubikey_service.go b/api/utils/keys/piv/yubikey_service.go index 56688195749f4..a1ffb2153c6c5 100644 --- a/api/utils/keys/piv/yubikey_service.go +++ b/api/utils/keys/piv/yubikey_service.go @@ -26,6 +26,7 @@ import ( "errors" "fmt" "io" + "os" "strconv" "sync" @@ -49,15 +50,14 @@ var ( // YubiKeyService is a YubiKey PIV implementation of [hardwarekey.Service]. type YubiKeyService struct { - prompt hardwarekey.Prompt + // promptMux is used during to prevent over-prompting, especially for back-to-back sign requests + // since touch/PIN from the first signature should be cached for following signatures. + promptMux sync.Mutex + prompt hardwarekey.Prompt // ctx is provided to signature requests, since `crypto.Sign` does have // context support directly. ctx context.Context - - // TODO: do we need sign mutex to ensure signature requests are queued through without over-prompting? - // Should this logic go ino the hardware key prompt itself? Maybe even sync.Cond? - signMux sync.Mutex } // Returns a new [YubiKeyService]. @@ -87,7 +87,7 @@ func NewYubiKeyService(ctx context.Context, prompt hardwarekey.Prompt) *YubiKeyS // - !touch & pin -> 9c // - touch & pin -> 9d // - touch & !pin -> 9e -func (s *YubiKeyService) NewPrivateKey(ctx context.Context, customSlot hardwarekey.PIVSlot, requiredPolicy hardwarekey.PromptPolicy) (*hardwarekey.PrivateKeyRef, error) { +func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.PrivateKeyConfig) (*hardwarekey.PrivateKey, error) { // Use the first yubiKey we find. y, err := s.getYubiKey(0) if err != nil { @@ -96,66 +96,81 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, customSlot hardwarek // Get the requested or default PIV slot. var pivSlot piv.Slot - if customSlot != "" { - slotKey, err := strconv.ParseUint(string(customSlot), 16, 32) + if config.CustomSlot != "" { + slotKey, err := strconv.ParseUint(string(config.CustomSlot), 16, 32) if err != nil { return nil, trace.Wrap(err) } pivSlot, err = parsePIVSlot(uint32(slotKey)) } else { - pivSlot, err = getDefaultKeySlot(requiredPolicy) + pivSlot, err = getDefaultKeySlot(config.Policy) } if err != nil { return nil, trace.Wrap(err) } // If PIN is required, check that PIN and PUK are not the defaults. - if requiredPolicy.PINRequired { - if err := y.checkOrSetPIN(ctx, s.prompt); err != nil { + if config.Policy.PINRequired { + if err := s.checkOrSetPIN(ctx, y, config.ContextualKeyInfo); err != nil { return nil, trace.Wrap(err) } } + generatePrivateKey := func() (*hardwarekey.PrivateKey, error) { + ref, err := y.generatePrivateKey(pivSlot, config.Policy) + if err != nil { + return nil, trace.Wrap(err) + } + + ref.ContextualKeyInfo = config.ContextualKeyInfo + return hardwarekey.NewPrivateKey(s, ref), nil + } + // 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 customSlot == "" { - // Check the client certificate in the slot. + if config.CustomSlot == "" { switch cert, err := y.getCertificate(pivSlot); { - case err == nil && (len(cert.Subject.Organization) == 0 || cert.Subject.Organization[0] != certOrgName): - // Unknown cert found, prompt the user before we overwrite the slot. - if err := s.promptOverwriteSlot(ctx, nonTeleportCertificateMessage(pivSlot, cert), hardwarekey.PrivateKeyInfo{}); err != nil { - return nil, trace.Wrap(err) - } - return y.generatePrivateKey(pivSlot, requiredPolicy) case errors.Is(err, piv.ErrNotFound): - return y.generatePrivateKey(pivSlot, requiredPolicy) + return generatePrivateKey() + case err != nil: return nil, trace.Wrap(err) + + // Unknown cert found, 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), config.ContextualKeyInfo); err != nil { + return nil, trace.Wrap(err) + } + return generatePrivateKey() } } // Check for an existing key in the slot that satisfies the required private // key policy, or generate a new one if needed. slotCert, attCert, att, err := y.attestKey(pivSlot) - keyPolicy := hardwarekey.PromptPolicy{ - TouchRequired: att.TouchPolicy != piv.TouchPolicyNever, - PINRequired: att.PINPolicy != piv.PINPolicyNever, - } switch { - case err == nil && (requiredPolicy.TouchRequired && !keyPolicy.TouchRequired) || (requiredPolicy.PINRequired && !keyPolicy.PINRequired): - // Key does not meet the required key policy, prompt the user before we overwrite the slot. - msg := fmt.Sprintf("private key in YubiKey PIV slot %q does not meet prompt policy %v.", pivSlot, requiredPolicy) - if err := s.promptOverwriteSlot(ctx, msg, hardwarekey.PrivateKeyInfo{}); err != nil { - return nil, trace.Wrap(err) - } - return y.generatePrivateKey(pivSlot, requiredPolicy) case errors.Is(err, piv.ErrNotFound): - return y.generatePrivateKey(pivSlot, requiredPolicy) + return generatePrivateKey() + case err != nil: return nil, trace.Wrap(err) + + case config.Policy.TouchRequired && att.TouchPolicy == piv.TouchPolicyNever: + msg := fmt.Sprintf("private key in YubiKey PIV slot %q does not require touch.", pivSlot) + if err := s.promptOverwriteSlot(ctx, msg, config.ContextualKeyInfo); err != nil { + return nil, trace.Wrap(err) + } + return generatePrivateKey() + + case config.Policy.PINRequired && att.PINPolicy == piv.PINPolicyNever: + msg := fmt.Sprintf("private key in YubiKey PIV slot %q does not require PIN", pivSlot) + if err := s.promptOverwriteSlot(ctx, msg, config.ContextualKeyInfo); err != nil { + return nil, trace.Wrap(err) + } + return generatePrivateKey() } - return &hardwarekey.PrivateKeyRef{ + return hardwarekey.NewPrivateKey(s, &hardwarekey.PrivateKeyRef{ SerialNumber: y.serialNumber, SlotKey: pivSlot.Key, PublicKey: slotCert.PublicKey, @@ -171,57 +186,8 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, customSlot hardwarek }, }, }, - }, nil -} - -func getDefaultKeySlot(policy hardwarekey.PromptPolicy) (piv.Slot, error) { - switch policy { - case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: false}: - return piv.SlotAuthentication, nil - case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: false}: - return piv.SlotSignature, nil - case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: true}: - return piv.SlotKeyManagement, nil - case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: false}: - return piv.SlotCardAuthentication, nil - default: - return piv.Slot{}, trace.BadParameter("unexpected private key policy %v", policy) - } -} - -func nonTeleportCertificateMessage(slot piv.Slot, cert *x509.Certificate) string { - // Gather a small list of user-readable x509 certificate fields to display to the user. - sum := sha256.Sum256(cert.Raw) - fingerPrint := hex.EncodeToString(sum[:]) - return fmt.Sprintf(`Certificate in YubiKey PIV slot %q is not a Teleport client cert: -Slot %s: - Algorithm: %v - Subject DN: %v - Issuer DN: %v - Serial: %v - Fingerprint: %v - Not before: %v - Not after: %v -`, - slot, slot, - cert.SignatureAlgorithm, - cert.Subject, - cert.Issuer, - cert.SerialNumber, - fingerPrint, - cert.NotBefore, - cert.NotAfter, - ) -} - -func (s *YubiKeyService) promptOverwriteSlot(ctx context.Context, msg string, keyInfo hardwarekey.PrivateKeyInfo) error { - promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg) - if confirmed, confirmErr := s.prompt.ConfirmSlotOverwrite(ctx, promptQuestion, keyInfo); confirmErr != nil { - return trace.Wrap(confirmErr) - } else if !confirmed { - return trace.Wrap(trace.CompareFailed(msg), "user declined to overwrite slot") - } - return nil + ContextualKeyInfo: config.ContextualKeyInfo, + }), nil } // Sign performs a cryptographic signature using the specified hardware @@ -233,16 +199,14 @@ func (s *YubiKeyService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRe ctx = s.ctx } - // To prevent concurrent calls to sign from failing due to PIV only handling a - // single connection, use a lock to queue through signature requests one at a time. - s.signMux.Lock() - defer s.signMux.Unlock() - y, err := s.getYubiKey(ref.SerialNumber) if err != nil { return nil, trace.Wrap(err) } + s.promptMux.Lock() + defer s.promptMux.Unlock() + return y.sign(ctx, ref, s.prompt, rand, digest, opts) } @@ -250,6 +214,8 @@ func (s *YubiKeyService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRe // This is used by Teleport Connect which sets the prompt later than the hardware key service, // due to process initialization constraints. func (s *YubiKeyService) SetPrompt(prompt hardwarekey.Prompt) { + s.promptMux.Lock() + defer s.promptMux.Unlock() s.prompt = prompt } @@ -271,3 +237,82 @@ func (s *YubiKeyService) getYubiKey(serialNumber uint32) (*YubiKey, error) { yubiKeys[y.serialNumber] = y return y, nil } + +// checkOrSetPIN prompts the user for PIN and verifies it with the YubiKey. +// If the user provides the default PIN, they will be prompted to set a +// non-default PIN and PUK before continuing. +func (s *YubiKeyService) checkOrSetPIN(ctx context.Context, y *YubiKey, keyInfo hardwarekey.ContextualKeyInfo) error { + s.promptMux.Lock() + defer s.promptMux.Unlock() + + pin, err := s.prompt.AskPIN(ctx, hardwarekey.PINOptional, keyInfo) + if err != nil { + return trace.Wrap(err) + } + + switch pin { + case piv.DefaultPIN: + fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN) + fallthrough + case "": + pin, err = y.setPINAndPUKFromDefault(ctx, s.prompt, keyInfo) + if err != nil { + return trace.Wrap(err) + } + } + + return trace.Wrap(y.verifyPIN(pin)) +} + +func (s *YubiKeyService) promptOverwriteSlot(ctx context.Context, msg string, keyInfo hardwarekey.ContextualKeyInfo) error { + s.promptMux.Lock() + defer s.promptMux.Unlock() + + promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg) + if confirmed, confirmErr := s.prompt.ConfirmSlotOverwrite(ctx, promptQuestion, keyInfo); confirmErr != nil { + return trace.Wrap(confirmErr) + } else if !confirmed { + return trace.Wrap(trace.CompareFailed(msg), "user declined to overwrite slot") + } + return nil +} + +func getDefaultKeySlot(policy hardwarekey.PromptPolicy) (piv.Slot, error) { + switch policy { + case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: false}: + return piv.SlotAuthentication, nil + case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: false}: + return piv.SlotSignature, nil + case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: true}: + return piv.SlotKeyManagement, nil + case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: false}: + return piv.SlotCardAuthentication, nil + default: + return piv.Slot{}, trace.BadParameter("unexpected private key policy %v", policy) + } +} + +func nonTeleportCertificateMessage(slot piv.Slot, cert *x509.Certificate) string { + // Gather a small list of user-readable x509 certificate fields to display to the user. + sum := sha256.Sum256(cert.Raw) + fingerPrint := hex.EncodeToString(sum[:]) + return fmt.Sprintf(`Certificate in YubiKey PIV slot %q is not a Teleport client cert: +Slot %s: + Algorithm: %v + Subject DN: %v + Issuer DN: %v + Serial: %v + Fingerprint: %v + Not before: %v + Not after: %v +`, + slot, slot, + cert.SignatureAlgorithm, + cert.Subject, + cert.Issuer, + cert.SerialNumber, + fingerPrint, + cert.NotBefore, + cert.NotAfter, + ) +} diff --git a/api/utils/keys/piv/yubikey_service_fake.go b/api/utils/keys/piv/yubikey_service_fake.go index 83d435a1e7d2a..40819c677fc07 100644 --- a/api/utils/keys/piv/yubikey_service_fake.go +++ b/api/utils/keys/piv/yubikey_service_fake.go @@ -22,51 +22,62 @@ import ( "crypto/ed25519" "crypto/rand" "io" + "sync" "github.com/gravitational/trace" "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) -type fakeYubiKeyPIVService struct { - // TODO(Joerger): TestHardwareKeyLogin fails because the hardware key service is not being - // reused from login -> use, resulting in the key not being found. Rather than introducing - // a global key map, ensure that the hardware key service is set from a shared call stack. - keys map[crypto.PublicKey]crypto.Signer -} +// TODO(Joerger): Rather than using a global cache, clients should be updated to +// create a single YubiKeyService and ensure it is reused across the program +// execution. +var ( + keys = map[string]crypto.Signer{} + keysMux sync.Mutex +) -func NewYubiKeyPIVService(ctx context.Context, _ hardwarekey.HardwareKeyPrompt) *fakeYubiKeyPIVService { - return &fakeYubiKeyPIVService{ - keys: map[crypto.PublicKey]crypto.Signer{}, - } +type fakeYubiKeyPIVService struct{} + +func NewYubiKeyService(ctx context.Context, _ hardwarekey.Prompt) *fakeYubiKeyPIVService { + return &fakeYubiKeyPIVService{} } -func (s *fakeYubiKeyPIVService) NewPrivateKey(ctx context.Context, customSlot hardwarekey.PIVSlot, requiredPolicy hardwarekey.PromptPolicy) (*hardwarekey.PrivateKeyRef, error) { +func (s *fakeYubiKeyPIVService) NewPrivateKey(ctx context.Context, config hardwarekey.PrivateKeyConfig) (*hardwarekey.PrivateKey, error) { + keysMux.Lock() + defer keysMux.Unlock() + pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, trace.Wrap(err) } - s.keys[string(pub)] = priv + keys[string(pub)] = priv - return &hardwarekey.PrivateKeyRef{ - Policy: requiredPolicy, + ref := &hardwarekey.PrivateKeyRef{ + Policy: config.Policy, PublicKey: pub, // Since this is only used in tests, we will ignore the attestation statement in the end. // We just need it to be non-nil so that it goes through the test modules implementation // of AttestHardwareKey. AttestationStatement: &hardwarekey.AttestationStatement{}, - }, nil + ContextualKeyInfo: config.ContextualKeyInfo, + } + + return hardwarekey.NewPrivateKey(s, ref), nil } // Sign performs a cryptographic signature using the specified hardware // private key and provided signature parameters. -func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref hardwarekey.PrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { +func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + keysMux.Lock() + defer keysMux.Unlock() + ed25519Pub, ok := ref.PublicKey.(ed25519.PublicKey) if !ok { return nil, trace.BadParameter("expected public key of type %T", ed25519.PublicKey{}) } - priv, ok := s.keys[string(ed25519Pub)] + priv, ok := keys[string(ed25519Pub)] if !ok { return nil, trace.NotFound("key not found") } @@ -74,4 +85,4 @@ func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref hardwarekey.Privat return priv.Sign(rand, digest, opts) } -func (s *fakeYubiKeyPIVService) SetPrompt(prompt hardwarekey.HardwareKeyPrompt) {} +func (s *fakeYubiKeyPIVService) SetPrompt(prompt hardwarekey.Prompt) {} diff --git a/api/utils/keys/piv/yubikey_service_test.go b/api/utils/keys/piv/yubikey_service_test.go index 614098c7b3629..97bddb002762e 100644 --- a/api/utils/keys/piv/yubikey_service_test.go +++ b/api/utils/keys/piv/yubikey_service_test.go @@ -57,7 +57,9 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { // Warmup the hardware key to prompt touch at the start of the test, // rather than having this interaction later. - priv, err := keys.NewHardwarePrivateKey(ctx, s, "", keys.PrivateKeyPolicyHardwareKeyTouch, hardwarekey.PrivateKeyInfo{}) + priv, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ + Policy: hardwarekey.PromptPolicy{TouchRequired: true}, + }) require.NoError(t, err) require.Nil(t, priv.WarmupHardwareKey(ctx)) @@ -79,7 +81,12 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { } // NewHardwarePrivateKey should generate a new hardware private key. - priv, err := keys.NewHardwarePrivateKey(ctx, s, slot, policy, hardwarekey.PrivateKeyInfo{}) + priv, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ + Policy: hardwarekey.PromptPolicy{ + TouchRequired: policy.IsHardwareKeyTouchVerified(), + PINRequired: policy.IsHardwareKeyPINVerified(), + }, + }) require.NoError(t, err) // test HardwareSigner methods @@ -91,7 +98,13 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { require.Nil(t, priv.WarmupHardwareKey(ctx)) // Another call to NewHardwarePrivateKey should retrieve the previously generated key. - retrievePriv, err := keys.NewHardwarePrivateKey(ctx, s, slot, policy, hardwarekey.PrivateKeyInfo{}) + retrievePriv, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ + CustomSlot: slot, + Policy: hardwarekey.PromptPolicy{ + TouchRequired: policy.IsHardwareKeyTouchVerified(), + PINRequired: policy.IsHardwareKeyPINVerified(), + }, + }) require.NoError(t, err) require.Equal(t, priv.Public(), retrievePriv.Public()) @@ -126,12 +139,16 @@ func TestOverwritePrompt(t *testing.T) { testOverwritePrompt := func(t *testing.T) { // Fail to overwrite slot when user denies prompt.SetStdin(prompt.NewFakeReader().AddString("n")) - _, err := keys.NewHardwarePrivateKey(ctx, s, "" /*slot*/, keys.PrivateKeyPolicyHardwareKeyTouch, hardwarekey.PrivateKeyInfo{}) + _, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ + Policy: hardwarekey.PromptPolicy{TouchRequired: true}, + }) require.True(t, trace.IsCompareFailed(err), "Expected compare failed error but got %v", err) // Successfully overwrite slot when user accepts prompt.SetStdin(prompt.NewFakeReader().AddString("y")) - _, err = keys.NewHardwarePrivateKey(ctx, s, "" /*slot*/, keys.PrivateKeyPolicyHardwareKeyTouch, hardwarekey.PrivateKeyInfo{}) + _, err = keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ + Policy: hardwarekey.PromptPolicy{TouchRequired: true}, + }) require.NoError(t, err) } @@ -149,7 +166,10 @@ func TestOverwritePrompt(t *testing.T) { resetYubikey(t, y) // Generate a key that does not require touch in the slot that Teleport expects to require touch. - _, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PIVSlot(touchSlot.String()), keys.PrivateKeyPolicyHardwareKey, hardwarekey.PrivateKeyInfo{}) + _, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ + CustomSlot: hardwarekey.PIVSlot(touchSlot.String()), + Policy: hardwarekey.PromptPolicy{TouchRequired: false}, + }) require.NoError(t, err) testOverwritePrompt(t) diff --git a/api/utils/keys/piv/yubikey_unavailable.go b/api/utils/keys/piv/yubikey_unavailable.go index 77754ed58be0b..6761a3b99eb1a 100644 --- a/api/utils/keys/piv/yubikey_unavailable.go +++ b/api/utils/keys/piv/yubikey_unavailable.go @@ -28,19 +28,19 @@ import ( var errPIVUnavailable = errors.New("PIV is unavailable in current build") -func NewYubiKeyPIVService(ctx context.Context, _ hardwarekey.PrivateKeyRef) *unavailableYubiKeyPIVService { +func NewYubiKeyService(ctx context.Context, _ hardwarekey.Prompt) *unavailableYubiKeyPIVService { return &unavailableYubiKeyPIVService{} } type unavailableYubiKeyPIVService struct{} -func (s *unavailableYubiKeyPIVService) NewPrivateKey(_ context.Context, _ hardwarekey.PIVSlot, _ hardwarekey.PromptPolicy) (*hardwarekey.PrivateKeyRef, error) { +func (s *unavailableYubiKeyPIVService) NewPrivateKey(_ context.Context, _ hardwarekey.PrivateKeyConfig) (*hardwarekey.PrivateKey, error) { return nil, trace.Wrap(errPIVUnavailable) } // 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, _ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) { return nil, trace.Wrap(errPIVUnavailable) } diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index e6f93a16212cb..d0ac0dd4f3965 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -96,21 +96,17 @@ func NewSoftwarePrivateKey(signer crypto.Signer) (*PrivateKey, error) { // NewHardwarePrivateKey creates or retrieves a hardware private key from from the given hardware key // service that matches the given PIV slot and private key policy, returning the hardware private key // as a [PrivateKey]. -func NewHardwarePrivateKey(ctx context.Context, s hardwarekey.Service, customSlot hardwarekey.PIVSlot, requiredPolicy PrivateKeyPolicy, keyInfo hardwarekey.PrivateKeyInfo) (*PrivateKey, error) { +func NewHardwarePrivateKey(ctx context.Context, s hardwarekey.Service, keyConfig hardwarekey.PrivateKeyConfig) (*PrivateKey, error) { if s == nil { return nil, trace.BadParameter("cannot create a new hardware private key without a hardware key service provided") } - keyRef, err := s.NewPrivateKey(ctx, customSlot, hardwarekey.PromptPolicy{ - TouchRequired: requiredPolicy.IsHardwareKeyTouchVerified(), - PINRequired: requiredPolicy.IsHardwareKeyPINVerified(), - }) + hwPrivateKey, err := s.NewPrivateKey(ctx, keyConfig) if err != nil { return nil, trace.Wrap(err) } - hwPrivateKey := hardwarekey.NewPrivateKey(s, keyRef, keyInfo) - encodedKeyRef, err := hardwarekey.EncodeHardwarePrivateKeyRef(keyRef) + encodedKeyRef, err := hwPrivateKey.EncodeKeyRef() if err != nil { return nil, trace.Wrap(err) } @@ -284,8 +280,10 @@ func LoadPrivateKey(keyFile string) (*PrivateKey, error) { // ParsePrivateKeyOptions contains config options for ParsePrivateKey. type ParsePrivateKeyOptions struct { + // HardwareKeyService is the hardware key service to use with parsed hardware private keys. HardwareKeyService hardwarekey.Service - KeyInfo hardwarekey.PrivateKeyInfo + // ContextualKeyInfo is contextual information associated with the key. + ContextualKeyInfo hardwarekey.ContextualKeyInfo } // ParsePrivateKeyOpt applies configuration options. @@ -298,10 +296,10 @@ func WithHardwareKeyService(hwKeyService hardwarekey.Service) ParsePrivateKeyOpt } } -// WithKeyInfo adds contextual key info to the parsed private key. -func WithKeyInfo(proxyHost string) ParsePrivateKeyOpt { +// WithContextualKeyInfo adds contextual key info to the parsed private key. +func WithContextualKeyInfo(proxyHost string) ParsePrivateKeyOpt { return func(o *ParsePrivateKeyOptions) { - o.KeyInfo = hardwarekey.PrivateKeyInfo{ProxyHost: proxyHost} + o.ContextualKeyInfo = hardwarekey.ContextualKeyInfo{ProxyHost: proxyHost} } } @@ -324,12 +322,13 @@ func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, er return nil, trace.BadParameter("cannot parse hardware private key without an initialized hardware key service") } - keyRef, err := hardwarekey.DecodeHardwarePrivateKeyRef(block.Bytes) + keyRef, err := hardwarekey.DecodeKeyRef(block.Bytes) if err != nil { return nil, trace.Wrap(err, "failed to parse hardware private key") } - hwPrivateKey := hardwarekey.NewPrivateKey(appliedOpts.HardwareKeyService, keyRef, appliedOpts.KeyInfo) + keyRef.ContextualKeyInfo = appliedOpts.ContextualKeyInfo + hwPrivateKey := hardwarekey.NewPrivateKey(appliedOpts.HardwareKeyService, keyRef) return NewPrivateKey(hwPrivateKey, keyPEM) case OpenSSHPrivateKeyType: priv, err := ssh.ParseRawPrivateKey(keyPEM) diff --git a/lib/client/api.go b/lib/client/api.go index 81fcf8a6024ac..ccb7c2fdd0075 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -3999,8 +3999,15 @@ func (tc *TeleportClient) GetNewLoginKeyRing(ctx context.Context) (keyRing *KeyR if tc.PIVSlot != "" { log.DebugContext(ctx, "Using PIV slot specified by client or server settings", "piv_slot", tc.PIVSlot) } - priv, err := tc.ClientStore.NewHardwarePrivateKey(ctx, tc.PIVSlot, tc.PrivateKeyPolicy, hardwarekey.PrivateKeyInfo{ - ProxyHost: tc.WebProxyHost(), + priv, err := tc.ClientStore.NewHardwarePrivateKey(ctx, hardwarekey.PrivateKeyConfig{ + CustomSlot: tc.PIVSlot, + Policy: hardwarekey.PromptPolicy{ + TouchRequired: tc.PrivateKeyPolicy.IsHardwareKeyTouchVerified(), + PINRequired: tc.PrivateKeyPolicy.IsHardwareKeyPINVerified(), + }, + ContextualKeyInfo: hardwarekey.ContextualKeyInfo{ + ProxyHost: tc.WebProxyHost(), + }, }) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/client/client_store.go b/lib/client/client_store.go index c736835deb60e..97032d57f3465 100644 --- a/lib/client/client_store.go +++ b/lib/client/client_store.go @@ -64,10 +64,6 @@ func NewFSClientStore(dirPath string, hwKeyService hardwarekey.Service) *Store { } } -func (s *Store) NewHardwarePrivateKey(ctx context.Context, customSlot hardwarekey.PIVSlot, requiredPolicy keys.PrivateKeyPolicy, keyInfo hardwarekey.PrivateKeyInfo) (*keys.PrivateKey, error) { - return keys.NewHardwarePrivateKey(ctx, s.hwKeyService, customSlot, requiredPolicy, keyInfo) -} - // NewMemClientStore initializes a new in-memory client store. func NewMemClientStore(hwKeyService hardwarekey.Service) *Store { return &Store{ @@ -79,6 +75,11 @@ func NewMemClientStore(hwKeyService hardwarekey.Service) *Store { } } +// NewHardwarePrivateKey create a new hardware private key with the given configuration in this client store. +func (s *Store) NewHardwarePrivateKey(ctx context.Context, config hardwarekey.PrivateKeyConfig) (*keys.PrivateKey, error) { + return keys.NewHardwarePrivateKey(ctx, s.hwKeyService, config) +} + // 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 { diff --git a/lib/client/keystore.go b/lib/client/keystore.go index db010574b7971..83e374515752d 100644 --- a/lib/client/keystore.go +++ b/lib/client/keystore.go @@ -536,7 +536,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), keys.WithKeyInfo(idx.ProxyHost)) + tlsCred, err := readTLSCredential(fs.userTLSKeyPath(idx), fs.tlsCertPath(idx), keys.WithHardwareKeyService(hwks), keys.WithContextualKeyInfo(idx.ProxyHost)) if err != nil { if trace.IsNotFound(err) { if _, statErr := os.Stat(fs.tlsCertPathLegacy(idx)); statErr == nil { @@ -547,7 +547,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), keys.WithKeyInfo(idx.ProxyHost)) + sshPriv, err := keys.LoadKeyPair(fs.userSSHKeyPath(idx), fs.publicKeyPath(idx), keys.WithHardwareKeyService(hwks), keys.WithContextualKeyInfo(idx.ProxyHost)) if err != nil { return nil, trace.ConvertSystemError(err) } @@ -570,7 +570,7 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks hardwarekey.Service, opt } func (fs *FSKeyStore) updateKeyRingWithCerts(o CertOption, hwks hardwarekey.Service, keyRing *KeyRing) error { - return trace.Wrap(o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, keys.WithHardwareKeyService(hwks), keys.WithKeyInfo(keyRing.ProxyHost))) + return trace.Wrap(o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, keys.WithHardwareKeyService(hwks), keys.WithContextualKeyInfo(keyRing.ProxyHost))) } // GetSSHCertificates gets all certificates signed for the given user and proxy. diff --git a/lib/teleterm/daemon/hardwarekeyprompt.go b/lib/teleterm/daemon/hardwarekeyprompt.go index 08bbd958e24cf..202ed86c1318c 100644 --- a/lib/teleterm/daemon/hardwarekeyprompt.go +++ b/lib/teleterm/daemon/hardwarekeyprompt.go @@ -53,7 +53,7 @@ type hardwareKeyPrompter struct { } // Touch prompts the user to touch the hardware key. -func (h *hardwareKeyPrompter) Touch(ctx context.Context, keyInfo hardwarekey.PrivateKeyInfo) error { +func (h *hardwareKeyPrompter) Touch(ctx context.Context, keyInfo hardwarekey.ContextualKeyInfo) error { _, err := h.s.tshdEventsClient.PromptHardwareKeyTouch(ctx, &api.PromptHardwareKeyTouchRequest{ RootClusterUri: keyInfo.ProxyHost, }) @@ -64,7 +64,7 @@ func (h *hardwareKeyPrompter) Touch(ctx context.Context, keyInfo hardwarekey.Pri } // AskPIN prompts the user for a PIN. -func (h *hardwareKeyPrompter) AskPIN(ctx context.Context, requirement hardwarekey.PINPromptRequirement, keyInfo hardwarekey.PrivateKeyInfo) (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: keyInfo.ProxyHost, PinOptional: requirement == hardwarekey.PINOptional, @@ -78,7 +78,7 @@ 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, keyInfo hardwarekey.PrivateKeyInfo) (*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: keyInfo.ProxyHost, }) @@ -93,7 +93,7 @@ func (h *hardwareKeyPrompter) ChangePIN(ctx context.Context, keyInfo hardwarekey } // ConfirmSlotOverwrite asks the user if the slot's private key and certificate can be overridden. -func (h *hardwareKeyPrompter) ConfirmSlotOverwrite(ctx context.Context, message string, keyInfo hardwarekey.PrivateKeyInfo) (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: keyInfo.ProxyHost, Message: message, From fcabceeebdf755444361c01668c33509a586b1bf Mon Sep 17 00:00:00 2001 From: joerger Date: Mon, 17 Mar 2025 12:57:30 -0700 Subject: [PATCH 07/10] Query hardware key for missing login key ref details. --- api/utils/keys/hardwarekey/hardwarekey.go | 2 +- api/utils/keys/piv/yubikey.go | 35 ++++++++++++++++++++++ api/utils/keys/piv/yubikey_service_fake.go | 5 ++++ api/utils/keys/piv/yubikey_unavailable.go | 5 ++++ api/utils/keys/privatekey.go | 11 +++++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/api/utils/keys/hardwarekey/hardwarekey.go b/api/utils/keys/hardwarekey/hardwarekey.go index 61bc4a7b2d0ff..9d98afd6365be 100644 --- a/api/utils/keys/hardwarekey/hardwarekey.go +++ b/api/utils/keys/hardwarekey/hardwarekey.go @@ -137,11 +137,11 @@ func (h *PrivateKey) EncodeKeyRef() ([]byte, error) { // DecodeKeyRef decodes a [PrivateKeyRef] from JSON. func DecodeKeyRef(encodedKeyRef []byte) (*PrivateKeyRef, error) { - // TODO: old clients would only have SerialNumber and SlotKey, gather missing information directly for backwards compatibility. keyRef := &PrivateKeyRef{} if err := json.Unmarshal(encodedKeyRef, keyRef); err != nil { return nil, trace.Wrap(err) } + return keyRef, nil } diff --git a/api/utils/keys/piv/yubikey.go b/api/utils/keys/piv/yubikey.go index 6f3d4b50b4153..26be990dfa111 100644 --- a/api/utils/keys/piv/yubikey.go +++ b/api/utils/keys/piv/yubikey.go @@ -667,3 +667,38 @@ func SelfSignedMetadataCertificate(subject pkix.Name) (*x509.Certificate, error) } return cert, nil } + +// UpdateKeyRef updates the key ref with missing information by querying the hardware key. +// Used for backwards compatibility with old logins. +// TODO(Joerger): DELETE IN v19.0.0 +func UpdateKeyRef(ref *hardwarekey.PrivateKeyRef) error { + y, err := FindYubiKey(ref.SerialNumber) + if err != nil { + return trace.Wrap(err) + } + + pivSlot, err := parsePIVSlot(ref.SlotKey) + if err != nil { + return trace.Wrap(err) + } + + slotCert, attCert, att, err := y.attestKey(pivSlot) + if err != nil { + return trace.Wrap(err) + } + + ref.PublicKey = slotCert.PublicKey + ref.Policy = hardwarekey.PromptPolicy{ + TouchRequired: att.TouchPolicy != piv.TouchPolicyNever, + PINRequired: att.PINPolicy != piv.PINPolicyNever, + } + ref.AttestationStatement = &hardwarekey.AttestationStatement{ + AttestationStatement: &attestationv1.AttestationStatement_YubikeyAttestationStatement{ + YubikeyAttestationStatement: &attestationv1.YubiKeyAttestationStatement{ + SlotCert: slotCert.Raw, + AttestationCert: attCert.Raw, + }, + }, + } + return nil +} diff --git a/api/utils/keys/piv/yubikey_service_fake.go b/api/utils/keys/piv/yubikey_service_fake.go index 40819c677fc07..72a8c10502d7e 100644 --- a/api/utils/keys/piv/yubikey_service_fake.go +++ b/api/utils/keys/piv/yubikey_service_fake.go @@ -86,3 +86,8 @@ func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref *hardwarekey.Priva } func (s *fakeYubiKeyPIVService) SetPrompt(prompt hardwarekey.Prompt) {} + +// TODO(Joerger): DELETE IN v19.0.0 +func UpdateKeyRef(ref *hardwarekey.PrivateKeyRef) error { + return nil +} diff --git a/api/utils/keys/piv/yubikey_unavailable.go b/api/utils/keys/piv/yubikey_unavailable.go index 6761a3b99eb1a..4bec50ad7a439 100644 --- a/api/utils/keys/piv/yubikey_unavailable.go +++ b/api/utils/keys/piv/yubikey_unavailable.go @@ -45,3 +45,8 @@ func (s *unavailableYubiKeyPIVService) Sign(_ context.Context, _ *hardwarekey.Pr } func (s *unavailableYubiKeyPIVService) SetPrompt(_ hardwarekey.Prompt) {} + +// TODO(Joerger): DELETE IN v19.0.0 +func UpdateKeyRef(ref *hardwarekey.PrivateKeyRef) error { + return trace.Wrap(errPIVUnavailable) +} diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index d0ac0dd4f3965..793964e5d17cc 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -33,6 +33,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/api/utils/sshutils/ppk" ) @@ -327,6 +328,16 @@ func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, er return nil, trace.Wrap(err, "failed to parse hardware private key") } + // If the public key is missing, this is likely an old login key with only + // the serial number and slot. Fetch missing data from the hardware key. + // This data will be saved to the login key on next login + // TODO(Joerger): DELETE IN v19.0.0 + if keyRef.PublicKey == nil { + if err := piv.UpdateKeyRef(keyRef); err != nil { + return nil, trace.Wrap(err) + } + } + keyRef.ContextualKeyInfo = appliedOpts.ContextualKeyInfo hwPrivateKey := hardwarekey.NewPrivateKey(appliedOpts.HardwareKeyService, keyRef) return NewPrivateKey(hwPrivateKey, keyPEM) From 5d6f8044417dde93f76ecdb471139750bf0562f1 Mon Sep 17 00:00:00 2001 From: joerger Date: Mon, 17 Mar 2025 17:29:18 -0700 Subject: [PATCH 08/10] Cleanup piv slot constants; Combine NewPrivateKey and NewSoftwarePrivateKey; Add tests for marshaling/parsing hardware private keys and hardware key methods. --- api/client/webclient/webclient.go | 2 +- api/profile/profile.go | 2 +- api/testhelpers/mtls/mtls.go | 4 +- api/types/authentication.go | 8 +- api/utils/keys/hardwarekey/hardwarekey.go | 48 ++++-------- api/utils/keys/hardwarekey/prompt.go | 19 +++++ api/utils/keys/hardwarekey/slot.go | 77 +++++++++++++++++++ api/utils/keys/piv/yubikey.go | 6 +- api/utils/keys/piv/yubikey_service.go | 33 +++----- api/utils/keys/piv/yubikey_service_fake.go | 86 ++++++++++++++++----- api/utils/keys/piv/yubikey_service_test.go | 5 +- api/utils/keys/privatekey.go | 74 +++++++++--------- api/utils/keys/privatekey_test.go | 87 +++++++++++++++++----- integration/helpers/helpers.go | 4 +- integration/helpers/usercreds.go | 4 +- lib/agentless/agentless.go | 2 +- lib/auth/keygen/keygen_test.go | 2 +- lib/client/api.go | 6 +- lib/client/client_store_test.go | 8 +- lib/client/conntest/ssh.go | 2 +- lib/client/db/database_certificates.go | 2 +- lib/client/db/oracle/oracle_test.go | 2 +- lib/client/interfaces.go | 2 +- lib/client/keyagent_test.go | 4 +- lib/config/fileconf.go | 4 +- lib/cryptosuites/suites.go | 2 +- lib/kube/kubeconfig/kubeconfig_test.go | 2 +- lib/services/suite/suite.go | 6 +- lib/srv/db/common/auth.go | 2 +- lib/tbot/service_ssh_host_output.go | 2 +- lib/teleterm/gateway/kube.go | 2 +- tool/tctl/common/auth_command.go | 6 +- tool/tsh/common/kube_proxy.go | 2 +- tool/tsh/common/kubectl_test.go | 2 +- tool/tsh/common/proxy_test.go | 2 +- tool/tsh/common/tsh.go | 2 +- tool/tsh/common/tsh_test.go | 4 +- 37 files changed, 338 insertions(+), 189 deletions(-) create mode 100644 api/utils/keys/hardwarekey/slot.go diff --git a/api/client/webclient/webclient.go b/api/client/webclient/webclient.go index 4c01b0d707dff..a7869607735ab 100644 --- a/api/client/webclient/webclient.go +++ b/api/client/webclient/webclient.go @@ -529,7 +529,7 @@ type AuthenticationSettings struct { // PrivateKeyPolicy contains the cluster-wide private key policy. PrivateKeyPolicy keys.PrivateKeyPolicy `json:"private_key_policy"` // PIVSlot specifies a specific PIV slot to use with hardware key support. - PIVSlot hardwarekey.PIVSlot `json:"piv_slot"` + PIVSlot hardwarekey.PIVSlotKeyString `json:"piv_slot"` // DeviceTrust holds cluster-wide device trust settings. DeviceTrust DeviceTrustSettings `json:"device_trust,omitempty"` // HasMessageOfTheDay is a flag indicating that the cluster has MOTD diff --git a/api/profile/profile.go b/api/profile/profile.go index a8affcd56ce4d..9b4efc7f8688a 100644 --- a/api/profile/profile.go +++ b/api/profile/profile.go @@ -108,7 +108,7 @@ type Profile struct { PrivateKeyPolicy keys.PrivateKeyPolicy `yaml:"private_key_policy"` // PIVSlot is a specific piv slot that Teleport clients should use for hardware key support. - PIVSlot hardwarekey.PIVSlot `yaml:"piv_slot"` + PIVSlot hardwarekey.PIVSlotKeyString `yaml:"piv_slot"` // MissingClusterDetails means this profile was created with limited cluster details. // Missing cluster details should be loaded into the profile by pinging the proxy. diff --git a/api/testhelpers/mtls/mtls.go b/api/testhelpers/mtls/mtls.go index 07476fbf65897..127f6ca6887a5 100644 --- a/api/testhelpers/mtls/mtls.go +++ b/api/testhelpers/mtls/mtls.go @@ -56,7 +56,7 @@ func generateCA(t *testing.T) (*keys.PrivateKey, *x509.Certificate) { caPub, caPriv, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) - caKey, err := keys.NewPrivateKey(caPriv, nil) + caKey, err := keys.NewPrivateKey(caPriv) require.NoError(t, err) // Create a self signed certificate. @@ -97,7 +97,7 @@ func generateChildTLSConfigFromCA(t *testing.T, caKey *keys.PrivateKey, caCert * pub, priv, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) - key, err := keys.NewPrivateKey(priv, nil) + key, err := keys.NewPrivateKey(priv) require.NoError(t, err) // Create a certificate signed by the CA. diff --git a/api/types/authentication.go b/api/types/authentication.go index 094d4514e1b2d..3ac711f5e6ea2 100644 --- a/api/types/authentication.go +++ b/api/types/authentication.go @@ -135,7 +135,7 @@ type AuthPreference interface { // GetHardwareKey returns the hardware key settings configured for the cluster. GetHardwareKey() (*HardwareKey, error) // GetPIVSlot returns the configured piv slot for the cluster. - GetPIVSlot() hardwarekey.PIVSlot + GetPIVSlot() hardwarekey.PIVSlotKeyString // GetHardwareKeySerialNumberValidation returns the cluster's hardware key // serial number validation settings. GetHardwareKeySerialNumberValidation() (*HardwareKeySerialNumberValidation, error) @@ -492,9 +492,9 @@ func (c *AuthPreferenceV2) GetHardwareKey() (*HardwareKey, error) { } // GetPIVSlot returns the configured piv slot for the cluster. -func (c *AuthPreferenceV2) GetPIVSlot() hardwarekey.PIVSlot { +func (c *AuthPreferenceV2) GetPIVSlot() hardwarekey.PIVSlotKeyString { if hk, err := c.GetHardwareKey(); err == nil { - return hardwarekey.PIVSlot(hk.PIVSlot) + return hardwarekey.PIVSlotKeyString(hk.PIVSlot) } return "" } @@ -841,7 +841,7 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { } if hk, err := c.GetHardwareKey(); err == nil && hk.PIVSlot != "" { - if err := hardwarekey.PIVSlot(hk.PIVSlot).Validate(); err != nil { + if err := hardwarekey.PIVSlotKeyString(hk.PIVSlot).Validate(); err != nil { return trace.Wrap(err) } } diff --git a/api/utils/keys/hardwarekey/hardwarekey.go b/api/utils/keys/hardwarekey/hardwarekey.go index 9d98afd6365be..4e55cb768a45d 100644 --- a/api/utils/keys/hardwarekey/hardwarekey.go +++ b/api/utils/keys/hardwarekey/hardwarekey.go @@ -23,7 +23,6 @@ import ( "crypto/x509" "encoding/json" "io" - "strconv" "github.com/gravitational/trace" ) @@ -52,7 +51,7 @@ type PrivateKeyRef struct { // SerialNumber is the hardware key's serial number. SerialNumber uint32 `json:"serial_number"` // SlotKey is the key name for the hardware key PIV slot, e.g. "9a". - SlotKey uint32 `json:"slot_key"` + SlotKey PIVSlotKey `json:"slot_key"` // PublicKey is the public key paired with the hardware private key. PublicKey crypto.PublicKey `json:"-"` // uses custom JSON marshaling in PKIX, ASN.1 DER form // Policy specifies the hardware private key's PIN/touch prompt policies. @@ -74,14 +73,6 @@ type ContextualKeyInfo struct { ProxyHost string } -// PromptPolicy specifies a hardware private key's PIN/touch policies. -type PromptPolicy struct { - // TouchRequired means that touch is required for signatures. - TouchRequired bool - // PINRequired means that PIN is required for signatures. - PINRequired bool -} - // NewPrivateKey returns a [PrivateKey] for the given service and ref. // keyInfo is an optional argument to supply additional contextual info. func NewPrivateKey(s Service, ref *PrivateKeyRef) *PrivateKey { @@ -156,9 +147,12 @@ type hardwarePrivateKeyRefJSON struct { // UnmarshalJSON marshals [PrivateKeyRef] with custom logic for the public key. func (r PrivateKeyRef) MarshalJSON() ([]byte, error) { - pubDER, err := x509.MarshalPKIXPublicKey(r.PublicKey) - if err != nil { - return nil, trace.Wrap(err) + var pubDER []byte + if r.PublicKey != nil { + var err error + if pubDER, err = x509.MarshalPKIXPublicKey(r.PublicKey); err != nil { + return nil, trace.Wrap(err) + } } return json.Marshal(&hardwarePrivateKeyRefJSON{ @@ -175,9 +169,11 @@ func (r *PrivateKeyRef) UnmarshalJSON(b []byte) error { return trace.Wrap(err) } - ref.refAlias.PublicKey, err = x509.ParsePKIXPublicKey(ref.PublicKeyDER) - if err != nil { - return trace.Wrap(err) + if len(ref.PublicKeyDER) > 0 { + ref.refAlias.PublicKey, err = x509.ParsePKIXPublicKey(ref.PublicKeyDER) + if err != nil { + return trace.Wrap(err) + } } *r = PrivateKeyRef(ref.refAlias) @@ -194,25 +190,7 @@ type PrivateKeyConfig struct { // - !touch & pin -> 9c // - touch & pin -> 9d // - touch & !pin -> 9e - CustomSlot PIVSlot + CustomSlot PIVSlotKeyString // ContextualKeyInfo contains additional info to associate with the key. ContextualKeyInfo ContextualKeyInfo } - -// PIVSlot is the string representation of a PIV slot. e.g. "9a". -type PIVSlot string - -// Validate that the PIV slot is a valid value. -func (s PIVSlot) Validate() error { - slotKey, err := strconv.ParseUint(string(s), 16, 32) - if err != nil { - return trace.Wrap(err) - } - - switch slotKey { - case 0x9a, 0x9c, 0x9d, 0x9e: - return nil - default: - return trace.BadParameter("invalid PIV slot %q", s) - } -} diff --git a/api/utils/keys/hardwarekey/prompt.go b/api/utils/keys/hardwarekey/prompt.go index 49ad85947b485..23e6da5d7b200 100644 --- a/api/utils/keys/hardwarekey/prompt.go +++ b/api/utils/keys/hardwarekey/prompt.go @@ -20,6 +20,25 @@ import ( "github.com/gravitational/trace" ) +var ( + // PromptPolicyNone is default the PromptPolicy for private key policy HardwareKey. + PromptPolicyNone = PromptPolicy{TouchRequired: false, PINRequired: false} + // PromptPolicyTouch is default the PromptPolicy for private key policy HardwareKeyTouch. + PromptPolicyTouch = PromptPolicy{TouchRequired: true, PINRequired: false} + // PromptPolicyPIN is default the PromptPolicy for private key policy HardwareKeyPIN. + PromptPolicyPIN = PromptPolicy{TouchRequired: false, PINRequired: true} + // PromptPolicyTouchAndPIN is default the PromptPolicy for private key policy HardwareKeyTouchAndPIN. + PromptPolicyTouchAndPIN = PromptPolicy{TouchRequired: true, PINRequired: true} +) + +// PromptPolicy specifies a hardware private key's PIN/touch policies. +type PromptPolicy struct { + // TouchRequired means that touch is required for signatures. + TouchRequired bool + // PINRequired means that PIN is required for signatures. + PINRequired bool +} + // Prompt provides methods to interact with a hardware [PrivateKey]. type Prompt interface { // AskPIN prompts the user for a PIN. diff --git a/api/utils/keys/hardwarekey/slot.go b/api/utils/keys/hardwarekey/slot.go new file mode 100644 index 0000000000000..80b785cd89bfe --- /dev/null +++ b/api/utils/keys/hardwarekey/slot.go @@ -0,0 +1,77 @@ +// 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 defines types and interfaces for hardware private keys. +package hardwarekey + +import ( + "strconv" + + "github.com/gravitational/trace" +) + +// PIVSlotKey is the key reference for a specific PIV slot. +// +// See: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=32 +type PIVSlotKey uint + +const ( + pivSlotKeyBasic PIVSlotKey = 0x9a + PivSlotKeyTouch PIVSlotKey = 0x9c + pivSlotKeyTouchAndPIN PIVSlotKey = 0x9d + pivSlotKeyPIN PIVSlotKey = 0x9e +) + +// PIVSlotKeyString is the string representation of a PIV slot key. +type PIVSlotKeyString string + +// Validate that the PIV slot is a valid value. +func (s PIVSlotKeyString) Validate() error { + slotKey, err := strconv.ParseUint(string(s), 16, 32) + if err != nil { + return trace.Wrap(err) + } + + switch PIVSlotKey(slotKey) { + case pivSlotKeyBasic, PivSlotKeyTouch, pivSlotKeyTouchAndPIN, pivSlotKeyPIN: + return nil + default: + return trace.BadParameter("invalid PIV slot %q", s) + } +} + +// Validate that the PIV slot is a valid value. +func (s PIVSlotKeyString) Parse() (PIVSlotKey, error) { + slotKey, err := strconv.ParseUint(string(s), 16, 32) + if err != nil { + return 0, trace.Wrap(err) + } + return PIVSlotKey(slotKey), nil +} + +// GetDefaultSlotKey gets the default PIV slot key for the given [policy]. +func GetDefaultSlotKey(policy PromptPolicy) (PIVSlotKey, error) { + switch policy { + case PromptPolicyNone: + return pivSlotKeyBasic, nil + case PromptPolicyTouch: + return PivSlotKeyTouch, nil + case PromptPolicyPIN: + return pivSlotKeyPIN, nil + case PromptPolicyTouchAndPIN: + return pivSlotKeyTouchAndPIN, nil + default: + return 0, trace.BadParameter("unexpected private key policy %v", policy) + } +} diff --git a/api/utils/keys/piv/yubikey.go b/api/utils/keys/piv/yubikey.go index 26be990dfa111..0e298db3ef296 100644 --- a/api/utils/keys/piv/yubikey.go +++ b/api/utils/keys/piv/yubikey.go @@ -329,7 +329,7 @@ func (y *YubiKey) generatePrivateKey(slot piv.Slot, policy hardwarekey.PromptPol return &hardwarekey.PrivateKeyRef{ SerialNumber: y.serialNumber, - SlotKey: slot.Key, + SlotKey: hardwarekey.PIVSlotKey(slot.Key), PublicKey: pub, Policy: policy, AttestationStatement: &hardwarekey.AttestationStatement{ @@ -627,8 +627,8 @@ func isRetryError(err error) bool { return strings.Contains(err.Error(), retryError) } -func parsePIVSlot(slotKey uint32) (piv.Slot, error) { - switch slotKey { +func parsePIVSlot(slotKey hardwarekey.PIVSlotKey) (piv.Slot, error) { + switch uint32(slotKey) { case piv.SlotAuthentication.Key: return piv.SlotAuthentication, nil case piv.SlotSignature.Key: diff --git a/api/utils/keys/piv/yubikey_service.go b/api/utils/keys/piv/yubikey_service.go index a1ffb2153c6c5..d4964de0eb60e 100644 --- a/api/utils/keys/piv/yubikey_service.go +++ b/api/utils/keys/piv/yubikey_service.go @@ -27,7 +27,6 @@ import ( "fmt" "io" "os" - "strconv" "sync" "github.com/go-piv/piv-go/piv" @@ -95,16 +94,17 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P } // Get the requested or default PIV slot. - var pivSlot piv.Slot + var slotKey hardwarekey.PIVSlotKey if config.CustomSlot != "" { - slotKey, err := strconv.ParseUint(string(config.CustomSlot), 16, 32) - if err != nil { - return nil, trace.Wrap(err) - } - pivSlot, err = parsePIVSlot(uint32(slotKey)) + slotKey, err = config.CustomSlot.Parse() } else { - pivSlot, err = getDefaultKeySlot(config.Policy) + slotKey, err = hardwarekey.GetDefaultSlotKey(config.Policy) + } + if err != nil { + return nil, trace.Wrap(err) } + + pivSlot, err := parsePIVSlot(slotKey) if err != nil { return nil, trace.Wrap(err) } @@ -172,7 +172,7 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P return hardwarekey.NewPrivateKey(s, &hardwarekey.PrivateKeyRef{ SerialNumber: y.serialNumber, - SlotKey: pivSlot.Key, + SlotKey: slotKey, PublicKey: slotCert.PublicKey, Policy: hardwarekey.PromptPolicy{ TouchRequired: att.TouchPolicy != piv.TouchPolicyNever, @@ -277,21 +277,6 @@ func (s *YubiKeyService) promptOverwriteSlot(ctx context.Context, msg string, ke return nil } -func getDefaultKeySlot(policy hardwarekey.PromptPolicy) (piv.Slot, error) { - switch policy { - case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: false}: - return piv.SlotAuthentication, nil - case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: false}: - return piv.SlotSignature, nil - case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: true}: - return piv.SlotKeyManagement, nil - case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: false}: - return piv.SlotCardAuthentication, nil - default: - return piv.Slot{}, trace.BadParameter("unexpected private key policy %v", policy) - } -} - func nonTeleportCertificateMessage(slot piv.Slot, cert *x509.Certificate) string { // Gather a small list of user-readable x509 certificate fields to display to the user. sum := sha256.Sum256(cert.Raw) diff --git a/api/utils/keys/piv/yubikey_service_fake.go b/api/utils/keys/piv/yubikey_service_fake.go index 72a8c10502d7e..0c0cc2c76b208 100644 --- a/api/utils/keys/piv/yubikey_service_fake.go +++ b/api/utils/keys/piv/yubikey_service_fake.go @@ -33,30 +33,66 @@ import ( // create a single YubiKeyService and ensure it is reused across the program // execution. var ( - keys = map[string]crypto.Signer{} - keysMux sync.Mutex + hardwarePrivateKeys = map[hardwareKeySlot]*fakeHardwarePrivateKey{} + hardwarePrivateKeysMux sync.Mutex ) +// Currently Teleport does not provide a way to choose a specific hardware key, +// so we just hard code a serial number for all tests. +const serialNumber uint32 = 12345678 + +type fakeHardwarePrivateKey struct { + crypto.Signer + ref *hardwarekey.PrivateKeyRef +} + +// hardwareKeySlot references a specific hardware key slot on a specific hardware key. +type hardwareKeySlot struct { + serialNumber uint32 + slot hardwarekey.PIVSlotKey +} + type fakeYubiKeyPIVService struct{} -func NewYubiKeyService(ctx context.Context, _ hardwarekey.Prompt) *fakeYubiKeyPIVService { +func NewYubiKeyService(_ context.Context, _ hardwarekey.Prompt) *fakeYubiKeyPIVService { return &fakeYubiKeyPIVService{} } func (s *fakeYubiKeyPIVService) NewPrivateKey(ctx context.Context, config hardwarekey.PrivateKeyConfig) (*hardwarekey.PrivateKey, error) { - keysMux.Lock() - defer keysMux.Unlock() + hardwarePrivateKeysMux.Lock() + defer hardwarePrivateKeysMux.Unlock() + + // Get the requested or default PIV slot. + var slotKey hardwarekey.PIVSlotKey + var err error + if config.CustomSlot != "" { + slotKey, err = config.CustomSlot.Parse() + } else { + slotKey, err = hardwarekey.GetDefaultSlotKey(config.Policy) + } + if err != nil { + return nil, trace.Wrap(err) + } + + keySlot := hardwareKeySlot{ + serialNumber: serialNumber, + slot: slotKey, + } + + if priv, ok := hardwarePrivateKeys[keySlot]; ok { + return hardwarekey.NewPrivateKey(s, priv.ref), nil + } pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, trace.Wrap(err) } - keys[string(pub)] = priv - ref := &hardwarekey.PrivateKeyRef{ - Policy: config.Policy, - PublicKey: pub, + SerialNumber: serialNumber, + SlotKey: slotKey, + PublicKey: pub, + Policy: config.Policy, // Since this is only used in tests, we will ignore the attestation statement in the end. // We just need it to be non-nil so that it goes through the test modules implementation // of AttestHardwareKey. @@ -64,22 +100,26 @@ func (s *fakeYubiKeyPIVService) NewPrivateKey(ctx context.Context, config hardwa ContextualKeyInfo: config.ContextualKeyInfo, } + hardwarePrivateKeys[keySlot] = &fakeHardwarePrivateKey{ + Signer: priv, + ref: ref, + } + return hardwarekey.NewPrivateKey(s, ref), nil } // Sign performs a cryptographic signature using the specified hardware // private key and provided signature parameters. func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { - keysMux.Lock() - defer keysMux.Unlock() + hardwarePrivateKeysMux.Lock() + defer hardwarePrivateKeysMux.Unlock() - ed25519Pub, ok := ref.PublicKey.(ed25519.PublicKey) + priv, ok := hardwarePrivateKeys[hardwareKeySlot{ + serialNumber: serialNumber, + slot: ref.SlotKey, + }] if !ok { - return nil, trace.BadParameter("expected public key of type %T", ed25519.PublicKey{}) - } - priv, ok := keys[string(ed25519Pub)] - if !ok { - return nil, trace.NotFound("key not found") + return nil, trace.NotFound("key not found in slot %d", ref.SlotKey) } return priv.Sign(rand, digest, opts) @@ -89,5 +129,17 @@ func (s *fakeYubiKeyPIVService) SetPrompt(prompt hardwarekey.Prompt) {} // TODO(Joerger): DELETE IN v19.0.0 func UpdateKeyRef(ref *hardwarekey.PrivateKeyRef) error { + hardwarePrivateKeysMux.Lock() + defer hardwarePrivateKeysMux.Unlock() + + priv, ok := hardwarePrivateKeys[hardwareKeySlot{ + serialNumber: serialNumber, + slot: ref.SlotKey, + }] + if !ok { + return trace.NotFound("key not found in slot %d", ref.SlotKey) + } + + *ref = *priv.ref return nil } diff --git a/api/utils/keys/piv/yubikey_service_test.go b/api/utils/keys/piv/yubikey_service_test.go index 97bddb002762e..3a3eb934e5ca7 100644 --- a/api/utils/keys/piv/yubikey_service_test.go +++ b/api/utils/keys/piv/yubikey_service_test.go @@ -75,13 +75,14 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { resetYubikey(t, y) setupPINPrompt(t, y) - var slot hardwarekey.PIVSlot = "" + var slot hardwarekey.PIVSlotKeyString = "" if customSlot { slot = "9a" } // NewHardwarePrivateKey should generate a new hardware private key. priv, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ + CustomSlot: slot, Policy: hardwarekey.PromptPolicy{ TouchRequired: policy.IsHardwareKeyTouchVerified(), PINRequired: policy.IsHardwareKeyPINVerified(), @@ -167,7 +168,7 @@ func TestOverwritePrompt(t *testing.T) { // Generate a key that does not require touch in the slot that Teleport expects to require touch. _, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ - CustomSlot: hardwarekey.PIVSlot(touchSlot.String()), + CustomSlot: hardwarekey.PIVSlotKeyString(touchSlot.String()), Policy: hardwarekey.PromptPolicy{TouchRequired: false}, }) require.NoError(t, err) diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index 793964e5d17cc..454f1121494f6 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -61,32 +61,24 @@ type PrivateKey struct { keyPEM []byte } -// NewPrivateKey returns a new PrivateKey for the given crypto.Signer with a -// pre-marshaled private key PEM, which may be a special PIV key PEM. -func NewPrivateKey(signer crypto.Signer, keyPEM []byte) (*PrivateKey, error) { - sshPub, err := ssh.NewPublicKey(signer.Public()) +// NewPrivateKey returns a new PrivateKey for a crypto.Signer. +// [signer] must be an *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey, or *hardwarekey.PrivateKey. +// TODO(Joerger): Remove the variadic argument once /e is updated to not provide it. +func NewPrivateKey(signer crypto.Signer, _ ...[]byte) (*PrivateKey, error) { + keyPEM, err := MarshalPrivateKey(signer) if err != nil { return nil, trace.Wrap(err) } - - return &PrivateKey{ - Signer: signer, - sshPub: sshPub, - keyPEM: keyPEM, - }, nil + return newPrivateKeyWithKeyPEM(signer, keyPEM) } -// NewSoftwarePrivateKey returns a new PrivateKey for a crypto.Signer. -// [signer] must be an *rsa.PrivateKey, *ecdsa.PrivateKey, or ed25519.PrivateKey. -func NewSoftwarePrivateKey(signer crypto.Signer) (*PrivateKey, error) { +// newPrivateKeyWithKeyPEM returns a new PrivateKey for the given crypto.Signer with a +// pre-marshaled private key PEM, which may be a special PIV key PEM. +func newPrivateKeyWithKeyPEM(signer crypto.Signer, keyPEM []byte) (*PrivateKey, error) { sshPub, err := ssh.NewPublicKey(signer.Public()) if err != nil { return nil, trace.Wrap(err) } - keyPEM, err := MarshalPrivateKey(signer) - if err != nil { - return nil, trace.Wrap(err) - } return &PrivateKey{ Signer: signer, sshPub: sshPub, @@ -107,18 +99,7 @@ func NewHardwarePrivateKey(ctx context.Context, s hardwarekey.Service, keyConfig return nil, trace.Wrap(err) } - encodedKeyRef, err := hwPrivateKey.EncodeKeyRef() - if err != nil { - return nil, trace.Wrap(err) - } - - keyRefPEM := pem.EncodeToMemory(&pem.Block{ - Type: pivYubiKeyPrivateKeyType, - Headers: nil, - Bytes: encodedKeyRef, - }) - - return NewPrivateKey(hwPrivateKey, keyRefPEM) + return NewPrivateKey(hwPrivateKey) } // SSHPublicKey returns the ssh.PublicKey representation of the public key. @@ -235,19 +216,20 @@ func (k *PrivateKey) GetAttestationStatement() *hardwarekey.AttestationStatement func (k *PrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy { if hwpk, ok := k.Signer.(*hardwarekey.PrivateKey); ok { switch hwpk.GetPromptPolicy() { - case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: false}: + case hardwarekey.PromptPolicyNone: return PrivateKeyPolicyHardwareKey - case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: false}: + case hardwarekey.PromptPolicyTouch: return PrivateKeyPolicyHardwareKeyTouch - case hardwarekey.PromptPolicy{TouchRequired: true, PINRequired: true}: - return PrivateKeyPolicyHardwareKeyTouchAndPIN - - case hardwarekey.PromptPolicy{TouchRequired: false, PINRequired: true}: + case hardwarekey.PromptPolicyPIN: return PrivateKeyPolicyHardwareKeyPIN + + case hardwarekey.PromptPolicyTouchAndPIN: + return PrivateKeyPolicyHardwareKeyTouchAndPIN } } + return PrivateKeyPolicyNone } @@ -340,7 +322,7 @@ func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, er keyRef.ContextualKeyInfo = appliedOpts.ContextualKeyInfo hwPrivateKey := hardwarekey.NewPrivateKey(appliedOpts.HardwareKeyService, keyRef) - return NewPrivateKey(hwPrivateKey, keyPEM) + return newPrivateKeyWithKeyPEM(hwPrivateKey, keyPEM) case OpenSSHPrivateKeyType: priv, err := ssh.ParseRawPrivateKey(keyPEM) if err != nil { @@ -356,7 +338,7 @@ func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, er if pEdwards, ok := cryptoSigner.(*ed25519.PrivateKey); ok { cryptoSigner = *pEdwards } - return NewPrivateKey(cryptoSigner, keyPEM) + return newPrivateKeyWithKeyPEM(cryptoSigner, keyPEM) case PKCS1PrivateKeyType, PKCS8PrivateKeyType, ECPrivateKeyType: // The DER format doesn't always exactly match the PEM header, various // versions of Teleport and OpenSSL have been guilty of writing PKCS#8 @@ -368,17 +350,17 @@ func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, er if !ok { return nil, trace.BadParameter("x509.ParsePKCS8PrivateKey returned an invalid private key of type %T", priv) } - return NewPrivateKey(signer, keyPEM) + return newPrivateKeyWithKeyPEM(signer, keyPEM) } else if block.Type == PKCS8PrivateKeyType { preferredErr = err } if signer, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { - return NewPrivateKey(signer, keyPEM) + return newPrivateKeyWithKeyPEM(signer, keyPEM) } else if block.Type == PKCS1PrivateKeyType { preferredErr = err } if signer, err := x509.ParseECPrivateKey(block.Bytes); err == nil { - return NewPrivateKey(signer, keyPEM) + return newPrivateKeyWithKeyPEM(signer, keyPEM) } else if block.Type == ECPrivateKeyType { preferredErr = err } @@ -411,6 +393,18 @@ func MarshalPrivateKey(key crypto.Signer) ([]byte, error) { Bytes: der, }) return privPEM, nil + case *hardwarekey.PrivateKey: + encodedKeyRef, err := privateKey.EncodeKeyRef() + if err != nil { + return nil, trace.Wrap(err) + } + + privPEM := pem.EncodeToMemory(&pem.Block{ + Type: pivYubiKeyPrivateKeyType, + Headers: nil, + Bytes: encodedKeyRef, + }) + return privPEM, nil default: return nil, trace.BadParameter("unsupported private key type %T", key) } diff --git a/api/utils/keys/privatekey_test.go b/api/utils/keys/privatekey_test.go index 36223055c34b2..51af7ce7057ee 100644 --- a/api/utils/keys/privatekey_test.go +++ b/api/utils/keys/privatekey_test.go @@ -1,3 +1,5 @@ +//go:build pivtest + /* Copyright 2022 Gravitational, Inc. @@ -14,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package keys +package keys_test import ( "bytes" @@ -34,6 +36,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/piv" ) func TestMarshalAndParseKey(t *testing.T) { @@ -45,21 +51,26 @@ func TestMarshalAndParseKey(t *testing.T) { _, edKey, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) + s := piv.NewYubiKeyService(context.TODO(), nil) + hwPriv, err := s.NewPrivateKey(context.TODO(), hardwarekey.PrivateKeyConfig{}) + require.NoError(t, err) + for keyType, key := range map[string]crypto.Signer{ - "rsa": rsaKey, - "ecdsa": ecKey, - "ed25519": edKey, + "rsa": rsaKey, + "ecdsa": ecKey, + "ed25519": edKey, + "hardware": hwPriv, } { t.Run(keyType, func(t *testing.T) { - keyPEM, err := MarshalPrivateKey(key) + keyPEM, err := keys.MarshalPrivateKey(key) require.NoError(t, err) - gotKey, err := ParsePrivateKey(keyPEM) + gotKey, err := keys.ParsePrivateKey(keyPEM, keys.WithHardwareKeyService(s)) require.NoError(t, err) require.Equal(t, key, gotKey.Signer) - pubKeyPEM, err := MarshalPublicKey(key.Public()) + pubKeyPEM, err := keys.MarshalPublicKey(key.Public()) require.NoError(t, err) - gotPubKey, err := ParsePublicKey(pubKeyPEM) + gotPubKey, err := keys.ParsePublicKey(pubKeyPEM) require.NoError(t, err) require.Equal(t, key.Public(), gotPubKey) }) @@ -67,7 +78,7 @@ func TestMarshalAndParseKey(t *testing.T) { } func TestParseMismatchedPEMHeader(t *testing.T) { - rsaKey, err := ParsePrivateKey(rsaKeyPEM) + rsaKey, err := keys.ParsePrivateKey(rsaKeyPEM) require.NoError(t, err) rsaPKCS1DER := x509.MarshalPKCS1PrivateKey(rsaKey.Signer.(*rsa.PrivateKey)) rsaPKCS8DER, err := x509.MarshalPKCS8PrivateKey(rsaKey.Signer) @@ -117,7 +128,7 @@ func TestParseMismatchedPEMHeader(t *testing.T) { }, } { t.Run(desc, func(t *testing.T) { - key, err := ParsePrivateKey(tc.pem) + key, err := keys.ParsePrivateKey(tc.pem) require.NoError(t, err) require.Equal(t, tc.expectKey, key.Signer) }) @@ -143,7 +154,7 @@ func TestParseMismatchedPEMHeader(t *testing.T) { }, } { t.Run(desc, func(t *testing.T) { - pubKey, err := ParsePublicKey(tc.pem) + pubKey, err := keys.ParsePublicKey(tc.pem) require.NoError(t, err) require.Equal(t, tc.expectKey, pubKey) }) @@ -162,7 +173,7 @@ func TestParseCorruptedKey(t *testing.T) { } { t.Run(tc, func(t *testing.T) { b := pem.EncodeToMemory(&pem.Block{Type: tc, Bytes: []byte("foo")}) - _, err := ParsePrivateKey(b) + _, err := keys.ParsePrivateKey(b) require.Error(t, err) }) } @@ -173,7 +184,7 @@ func TestParseCorruptedKey(t *testing.T) { } { t.Run(tc, func(t *testing.T) { b := pem.EncodeToMemory(&pem.Block{Type: tc, Bytes: []byte("foo")}) - _, err := ParsePublicKey(b) + _, err := keys.ParsePublicKey(b) require.Error(t, err) }) } @@ -207,7 +218,7 @@ func TestX509KeyPair(t *testing.T) { expectCert, err := tls.X509KeyPair(tc.certPEM, tc.keyPEM) require.NoError(t, err) - tlsCert, err := X509KeyPair(tc.certPEM, tc.keyPEM) + tlsCert, err := keys.X509KeyPair(tc.certPEM, tc.keyPEM) require.NoError(t, err) require.Empty(t, cmp.Diff(expectCert, tlsCert, cmpopts.IgnoreFields(tls.Certificate{}, "Leaf"))) @@ -267,7 +278,7 @@ func TestX509Certificate(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cert, rawCerts, err := X509Certificate(tc.certPEM) + cert, rawCerts, err := keys.X509Certificate(tc.certPEM) require.Len(t, rawCerts, tc.expectedLength) tc.expectedError(t, err) @@ -282,16 +293,54 @@ func TestX509Certificate(t *testing.T) { // Testing these methods with actual hardware keys requires the piv go tag and should // be tested individually in tests like `TestGetYubiKeyPrivateKey_Interactive`. func TestHardwareKeyMethods(t *testing.T) { + ctx := context.Background() + + // Test hardware key methods with a software key. priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - - key, err := NewPrivateKey(priv, nil) + key, err := keys.NewPrivateKey(priv) require.NoError(t, err) require.Nil(t, key.GetAttestationStatement()) - require.Equal(t, PrivateKeyPolicyNone, key.GetPrivateKeyPolicy()) + require.Equal(t, keys.PrivateKeyPolicyNone, key.GetPrivateKeyPolicy()) require.False(t, key.IsHardware()) - require.NoError(t, key.WarmupHardwareKey(context.Background())) + require.NoError(t, key.WarmupHardwareKey(ctx)) + + // Test hardware key methods with a mocked hardware key. + s := piv.NewYubiKeyService(ctx, nil) + hwKey, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ + Policy: hardwarekey.PromptPolicyTouch, + }) + require.NoError(t, err) + + require.NotNil(t, hwKey.GetAttestationStatement()) + require.Equal(t, keys.PrivateKeyPolicyHardwareKeyTouch, hwKey.GetPrivateKeyPolicy()) + require.True(t, hwKey.IsHardware()) + require.NoError(t, hwKey.WarmupHardwareKey(ctx)) +} + +// TODO(Joerger): DELETE in v19.0.0 +func TestHardwareKey_OldLogin(t *testing.T) { + s := piv.NewYubiKeyService(context.TODO(), nil) + hwPriv, err := s.NewPrivateKey(context.TODO(), hardwarekey.PrivateKeyConfig{ + CustomSlot: "9a", + }) + require.NoError(t, err) + + // If an old client logged in, the private key ref would only contain the PIV + // slot (and serial number which is irrelevant with pivtest). + hwPrivMissingInfo := hardwarekey.NewPrivateKey(s, &hardwarekey.PrivateKeyRef{ + SlotKey: 0x9a, + }) + keyPEM, err := keys.MarshalPrivateKey(hwPrivMissingInfo) + require.NoError(t, err) + require.NotEqual(t, hwPriv, hwPrivMissingInfo) + + // ParsePrivateKey should automatically get the missing hardware key info + // from the direct PIV implementation of [piv.UpdateKeyRef]. + parsedKey, err := keys.ParsePrivateKey(keyPEM, keys.WithHardwareKeyService(s)) + require.NoError(t, err) + require.Equal(t, hwPriv, parsedKey.Signer) } var ( diff --git a/integration/helpers/helpers.go b/integration/helpers/helpers.go index 54a35b0a32084..170e2d161c287 100644 --- a/integration/helpers/helpers.go +++ b/integration/helpers/helpers.go @@ -197,9 +197,9 @@ func MustCreateUserKeyRing(t *testing.T, tc *TeleInstance, username string, ttl } func mustCreateUserKeyRingWithKeys(t *testing.T, tc *TeleInstance, username string, ttl time.Duration, sshKey, tlsKey crypto.Signer) *client.KeyRing { - sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) + sshPriv, err := keys.NewPrivateKey(sshKey) require.NoError(t, err) - tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) + tlsPriv, err := keys.NewPrivateKey(tlsKey) require.NoError(t, err) keyRing := client.NewKeyRing(sshPriv, tlsPriv) keyRing.ClusterName = tc.Secrets.SiteName diff --git a/integration/helpers/usercreds.go b/integration/helpers/usercreds.go index 6f6c2192b7d0b..742f38a090ff0 100644 --- a/integration/helpers/usercreds.go +++ b/integration/helpers/usercreds.go @@ -120,11 +120,11 @@ func GenerateUserCreds(req UserCredsRequest) (*UserCreds, error) { if err != nil { return nil, trace.Wrap(err) } - sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) + sshPriv, err := keys.NewPrivateKey(sshKey) if err != nil { return nil, trace.Wrap(err) } - tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) + tlsPriv, err := keys.NewPrivateKey(tlsKey) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/agentless/agentless.go b/lib/agentless/agentless.go index c7346a4370765..1107fecd81b2e 100644 --- a/lib/agentless/agentless.go +++ b/lib/agentless/agentless.go @@ -177,7 +177,7 @@ func createAuthSigner(ctx context.Context, params certParams, localAccessPoint L if err != nil { return nil, trace.Wrap(err) } - priv, err := keys.NewSoftwarePrivateKey(key) + priv, err := keys.NewPrivateKey(key) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/keygen/keygen_test.go b/lib/auth/keygen/keygen_test.go index e82933b944885..9e5c65213a3b5 100644 --- a/lib/auth/keygen/keygen_test.go +++ b/lib/auth/keygen/keygen_test.go @@ -106,7 +106,7 @@ func TestBuildPrincipals(t *testing.T) { hostKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.Ed25519) require.NoError(t, err) - hostPrivateKey, err := keys.NewSoftwarePrivateKey(hostKey) + hostPrivateKey, err := keys.NewPrivateKey(hostKey) require.NoError(t, err) hostPublicKey := hostPrivateKey.MarshalSSHPublicKey() diff --git a/lib/client/api.go b/lib/client/api.go index ccb7c2fdd0075..e56e3e8b514ee 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -449,7 +449,7 @@ type Config struct { PrivateKeyPolicy keys.PrivateKeyPolicy // PIVSlot specifies a specific PIV slot to use with hardware key support. - PIVSlot hardwarekey.PIVSlot + PIVSlot hardwarekey.PIVSlotKeyString // LoadAllCAs indicates that tsh should load the CAs of all clusters // instead of just the current cluster. @@ -4036,11 +4036,11 @@ func (tc *TeleportClient) GetNewLoginKeyRing(ctx context.Context) (keyRing *KeyR } } - sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) + sshPriv, err := keys.NewPrivateKey(sshKey) if err != nil { return nil, trace.Wrap(err) } - tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) + tlsPriv, err := keys.NewPrivateKey(tlsKey) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/client_store_test.go b/lib/client/client_store_test.go index cf7d57238102c..bbff3d7d5b2b9 100644 --- a/lib/client/client_store_test.go +++ b/lib/client/client_store_test.go @@ -76,9 +76,9 @@ func (s *testAuthority) makeSignedKeyRing(t *testing.T, idx KeyRingIndex, makeEx return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, nil }) require.NoError(t, err) - sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) + sshPriv, err := keys.NewPrivateKey(sshKey) require.NoError(t, err) - tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) + tlsPriv, err := keys.NewPrivateKey(tlsKey) require.NoError(t, err) allowedLogins := []string{idx.Username, "root"} @@ -400,9 +400,7 @@ func BenchmarkLoadKeysToKubeFromStore(b *testing.B) { certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) require.NotEmpty(b, certPEM) - keyPEM, err := keys.MarshalPrivateKey(key) - require.NoError(b, err) - privateKey, err := keys.NewPrivateKey(key, keyPEM) + privateKey, err := keys.NewPrivateKey(key) require.NoError(b, err) kubeCred := TLSCredential{ diff --git a/lib/client/conntest/ssh.go b/lib/client/conntest/ssh.go index 948e7ab873eed..db485e45b901a 100644 --- a/lib/client/conntest/ssh.go +++ b/lib/client/conntest/ssh.go @@ -114,7 +114,7 @@ func (s *SSHConnectionTester) TestConnection(ctx context.Context, req TestConnec if err != nil { return nil, trace.Wrap(err) } - privateKey, err := keys.NewSoftwarePrivateKey(key) + privateKey, err := keys.NewPrivateKey(key) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/db/database_certificates.go b/lib/client/db/database_certificates.go index af75dd3b29f11..ed37daff9d9ce 100644 --- a/lib/client/db/database_certificates.go +++ b/lib/client/db/database_certificates.go @@ -103,7 +103,7 @@ func GenerateDatabaseServerCertificates(ctx context.Context, req GenerateDatabas if err != nil { return nil, trace.Wrap(err) } - privateKey, err := keys.NewSoftwarePrivateKey(key) + privateKey, err := keys.NewPrivateKey(key) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/db/oracle/oracle_test.go b/lib/client/db/oracle/oracle_test.go index 6bde950afb870..220c41ee0b4fe 100644 --- a/lib/client/db/oracle/oracle_test.go +++ b/lib/client/db/oracle/oracle_test.go @@ -40,7 +40,7 @@ func TestCreateJksWallet(t *testing.T) { publicPEM, err := keys.MarshalPublicKey(signer.Public()) require.NoError(t, err) - wrapped, err := keys.NewSoftwarePrivateKey(signer) + wrapped, err := keys.NewPrivateKey(signer) require.NoError(t, err) _, err = createJKSWallet(signer, publicPEM, publicPEM, "dummy") diff --git a/lib/client/interfaces.go b/lib/client/interfaces.go index c7299cf3824bc..f2e207b1717ed 100644 --- a/lib/client/interfaces.go +++ b/lib/client/interfaces.go @@ -162,7 +162,7 @@ func (k *KeyRing) generateSubjectTLSKey(ctx context.Context, tc *TeleportClient, if err != nil { return nil, trace.Wrap(err) } - priv, err := keys.NewSoftwarePrivateKey(key) + priv, err := keys.NewPrivateKey(key) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/keyagent_test.go b/lib/client/keyagent_test.go index d038601535706..c580dcdc37d92 100644 --- a/lib/client/keyagent_test.go +++ b/lib/client/keyagent_test.go @@ -772,9 +772,9 @@ func (s *KeyAgentTestSuite) makeKeyRing(t *testing.T, username, proxyHost string }) require.NoError(t, err) - sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) + sshPriv, err := keys.NewPrivateKey(sshKey) require.NoError(t, err) - tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) + tlsPriv, err := keys.NewPrivateKey(tlsKey) require.NoError(t, err) return &KeyRing{ diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index a37fc776b358c..dce50510b900d 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -1044,7 +1044,7 @@ type AuthenticationConfig struct { DefaultSessionTTL types.Duration `yaml:"default_session_ttl"` // Deprecated. HardwareKey.PIVSlot should be used instead. - PIVSlot hardwarekey.PIVSlot `yaml:"piv_slot,omitempty"` + PIVSlot hardwarekey.PIVSlotKeyString `yaml:"piv_slot,omitempty"` // HardwareKey holds settings related to hardware key support. // Requires Teleport Enterprise. @@ -1277,7 +1277,7 @@ func (dt *DeviceTrust) Parse() (*types.DeviceTrust, error) { type HardwareKey struct { // PIVSlot is a PIV slot that Teleport clients should use instead of the // default based on private key policy. For example, "9a" or "9e". - PIVSlot hardwarekey.PIVSlot `yaml:"piv_slot,omitempty"` + PIVSlot hardwarekey.PIVSlotKeyString `yaml:"piv_slot,omitempty"` // SerialNumberValidation contains optional settings for hardware key // serial number validation, including whether it is enabled. diff --git a/lib/cryptosuites/suites.go b/lib/cryptosuites/suites.go index 8a06d18998c2c..353af008f028d 100644 --- a/lib/cryptosuites/suites.go +++ b/lib/cryptosuites/suites.go @@ -447,7 +447,7 @@ func GeneratePrivateKeyWithAlgorithm(alg Algorithm) (*keys.PrivateKey, error) { if err != nil { return nil, trace.Wrap(err) } - privateKey, err := keys.NewSoftwarePrivateKey(key) + privateKey, err := keys.NewPrivateKey(key) return privateKey, trace.Wrap(err) } diff --git a/lib/kube/kubeconfig/kubeconfig_test.go b/lib/kube/kubeconfig/kubeconfig_test.go index 0ee118d4ad090..7a100d496f57e 100644 --- a/lib/kube/kubeconfig/kubeconfig_test.go +++ b/lib/kube/kubeconfig/kubeconfig_test.go @@ -569,7 +569,7 @@ func genUserKeyRing(hostname string) (*client.KeyRing, []byte, error) { if err != nil { return nil, nil, trace.Wrap(err) } - priv, err := keys.NewSoftwarePrivateKey(key) + priv, err := keys.NewPrivateKey(key) if err != nil { return nil, nil, trace.Wrap(err) } diff --git a/lib/services/suite/suite.go b/lib/services/suite/suite.go index b12d4e5562e2a..004892d1ba712 100644 --- a/lib/services/suite/suite.go +++ b/lib/services/suite/suite.go @@ -122,11 +122,7 @@ func NewTestCAWithConfig(config TestCAConfig) *types.CertAuthorityV2 { if err != nil { panic(err) } - keyPEM, err = keys.MarshalPrivateKey(signer) - if err != nil { - panic(err) - } - key, err = keys.NewPrivateKey(signer, keyPEM) + key, err = keys.NewPrivateKey(signer) if err != nil { panic(err) } diff --git a/lib/srv/db/common/auth.go b/lib/srv/db/common/auth.go index 67510a8c42c17..e810b0648f26a 100644 --- a/lib/srv/db/common/auth.go +++ b/lib/srv/db/common/auth.go @@ -1074,7 +1074,7 @@ func (a *dbAuth) GenerateDatabaseClientKey(ctx context.Context) (*keys.PrivateKe if err != nil { return nil, trace.Wrap(err) } - privateKey, err := keys.NewSoftwarePrivateKey(signer) + privateKey, err := keys.NewPrivateKey(signer) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/tbot/service_ssh_host_output.go b/lib/tbot/service_ssh_host_output.go index 716ccc6b219a0..f0dfcfc023241 100644 --- a/lib/tbot/service_ssh_host_output.go +++ b/lib/tbot/service_ssh_host_output.go @@ -136,7 +136,7 @@ func (s *SSHHostOutputService) generate(ctx context.Context) error { if err != nil { return trace.Wrap(err) } - privKey, err := keys.NewSoftwarePrivateKey(key) + privKey, err := keys.NewPrivateKey(key) if err != nil { return trace.Wrap(err) } diff --git a/lib/teleterm/gateway/kube.go b/lib/teleterm/gateway/kube.go index 87d1d4d4621ac..8827cb04fb85d 100644 --- a/lib/teleterm/gateway/kube.go +++ b/lib/teleterm/gateway/kube.go @@ -120,7 +120,7 @@ func newKubeCAKey(kubeCert tls.Certificate) (*keys.PrivateKey, error) { if err != nil { return nil, trace.Wrap(err) } - privateKey, err := keys.NewSoftwarePrivateKey(signer) + privateKey, err := keys.NewPrivateKey(signer) if err != nil { return nil, trace.Wrap(err) } diff --git a/tool/tctl/common/auth_command.go b/tool/tctl/common/auth_command.go index 6ee7f1f5a6450..834732f3cebb7 100644 --- a/tool/tctl/common/auth_command.go +++ b/tool/tctl/common/auth_command.go @@ -301,7 +301,7 @@ func (a *AuthCommand) GenerateKeys(ctx context.Context, clusterAPI authCommandCl if err != nil { return trace.Wrap(err) } - key, err := keys.NewSoftwarePrivateKey(signer) + key, err := keys.NewPrivateKey(signer) if err != nil { return trace.Wrap(err) } @@ -535,7 +535,7 @@ func (a *AuthCommand) generateHostKeys(ctx context.Context, clusterAPI certifica if err != nil { return trace.Wrap(err) } - key, err := keys.NewSoftwarePrivateKey(signer) + key, err := keys.NewPrivateKey(signer) if err != nil { return trace.Wrap(err) } @@ -895,7 +895,7 @@ func generateKeyRing(ctx context.Context, clusterAPI certificateSigner, purpose if err != nil { return nil, trace.Wrap(err) } - key, err := keys.NewSoftwarePrivateKey(signer) + key, err := keys.NewPrivateKey(signer) if err != nil { return nil, trace.Wrap(err) } diff --git a/tool/tsh/common/kube_proxy.go b/tool/tsh/common/kube_proxy.go index 3338851160e2d..f81a9128d2361 100644 --- a/tool/tsh/common/kube_proxy.go +++ b/tool/tsh/common/kube_proxy.go @@ -325,7 +325,7 @@ func makeKubeLocalProxy(cf *CLIConf, tc *client.TeleportClient, clusters kubecon if err != nil { return nil, trace.Wrap(err) } - localClientKey, err := keys.NewSoftwarePrivateKey(key) + localClientKey, err := keys.NewPrivateKey(key) if err != nil { return nil, trace.Wrap(err) } diff --git a/tool/tsh/common/kubectl_test.go b/tool/tsh/common/kubectl_test.go index d55e679dae3a1..05ccd0c19fcfd 100644 --- a/tool/tsh/common/kubectl_test.go +++ b/tool/tsh/common/kubectl_test.go @@ -269,7 +269,7 @@ func mustSetupKubeconfig(t *testing.T, tshHome, kubeCluster string) string { kubeconfigLocation := filepath.Join(tshHome, "kubeconfig") key, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) require.NoError(t, err) - priv, err := keys.NewSoftwarePrivateKey(key) + priv, err := keys.NewPrivateKey(key) require.NoError(t, err) err = kubeconfig.Update(kubeconfigLocation, kubeconfig.Values{ TeleportClusterName: "localhost", diff --git a/tool/tsh/common/proxy_test.go b/tool/tsh/common/proxy_test.go index eeb10f32dc236..c83bcd288d26d 100644 --- a/tool/tsh/common/proxy_test.go +++ b/tool/tsh/common/proxy_test.go @@ -1505,7 +1505,7 @@ func TestProxyAppWithIdentity(t *testing.T) { key, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) require.NoError(t, err) - privateKey, err := keys.NewSoftwarePrivateKey(key) + privateKey, err := keys.NewPrivateKey(key) require.NoError(t, err) // Identity files only support a single key for SSH/TLS keyRing := client.NewKeyRing(privateKey, privateKey) diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 9f8306a94c561..c55f649377af7 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -4386,7 +4386,7 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err } if cf.PIVSlot != "" { - c.PIVSlot = hardwarekey.PIVSlot(cf.PIVSlot) + c.PIVSlot = hardwarekey.PIVSlotKeyString(cf.PIVSlot) if err = c.PIVSlot.Validate(); err != nil { return nil, trace.Wrap(err) } diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 850563d018420..d51d82085bc73 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -5822,9 +5822,9 @@ func TestLogout(t *testing.T) { return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, nil }) require.NoError(t, err) - sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) + sshPriv, err := keys.NewPrivateKey(sshKey) require.NoError(t, err) - tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) + tlsPriv, err := keys.NewPrivateKey(tlsKey) require.NoError(t, err) clientKeyRing := &client.KeyRing{ KeyRingIndex: client.KeyRingIndex{ From f4eef27df28c3ab17534ed257eb549da90e7a8bf Mon Sep 17 00:00:00 2001 From: joerger Date: Tue, 25 Mar 2025 17:20:15 -0700 Subject: [PATCH 09/10] Cleanup. --- api/utils/keys/hardwarekey/cliprompt.go | 6 +- api/utils/keys/hardwarekey/hardwarekey.go | 104 +++++++++------ .../keys/hardwarekey/hardwarekey_test.go | 123 ++++++++++++++++++ api/utils/keys/hardwarekey/prompt.go | 24 ++-- api/utils/keys/hardwarekey/slot.go | 53 ++++---- api/utils/keys/hardwarekey/slot_test.go | 68 ++++++++++ api/utils/keys/piv/yubikey.go | 6 +- api/utils/keys/piv/yubikey_service.go | 28 ++-- api/utils/keys/piv/yubikey_service_fake.go | 5 +- api/utils/keys/piv/yubikey_unavailable.go | 2 +- api/utils/keys/privatekey.go | 26 +--- lib/client/api.go | 4 +- lib/client/interfaces.go | 9 ++ lib/client/keystore.go | 22 +++- 14 files changed, 354 insertions(+), 126 deletions(-) create mode 100644 api/utils/keys/hardwarekey/hardwarekey_test.go create mode 100644 api/utils/keys/hardwarekey/slot_test.go diff --git a/api/utils/keys/hardwarekey/cliprompt.go b/api/utils/keys/hardwarekey/cliprompt.go index 7c205e9d9a80a..cb84089f5191b 100644 --- a/api/utils/keys/hardwarekey/cliprompt.go +++ b/api/utils/keys/hardwarekey/cliprompt.go @@ -1,4 +1,4 @@ -// Copyright 2025 Gravitational, Inc. +// Copyright 2024 Gravitational, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -72,7 +72,7 @@ func (c *CLIPrompt) ChangePIN(ctx context.Context, _ ContextualKeyInfo) (*PINAnd continue } - if !IsPINLengthValid(newPIN) { + if !isPINLengthValid(newPIN) { fmt.Fprintf(os.Stderr, "PIN must be 6-8 characters long.\n") continue } @@ -113,7 +113,7 @@ func (c *CLIPrompt) ChangePIN(ctx context.Context, _ ContextualKeyInfo) (*PINAnd continue } - if !IsPINLengthValid(newPUK) { + if !isPINLengthValid(newPUK) { fmt.Fprintf(os.Stderr, "PUK must be 6-8 characters long.\n") continue } diff --git a/api/utils/keys/hardwarekey/hardwarekey.go b/api/utils/keys/hardwarekey/hardwarekey.go index 4e55cb768a45d..44525534ab1f9 100644 --- a/api/utils/keys/hardwarekey/hardwarekey.go +++ b/api/utils/keys/hardwarekey/hardwarekey.go @@ -33,53 +33,56 @@ type Service interface { NewPrivateKey(ctx context.Context, config PrivateKeyConfig) (*PrivateKey, 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) // SetPrompt sets the hardware key prompt used by the hardware key service, if applicable. // This is used by Teleport Connect which sets the prompt later than the hardware key service, // due to process initialization constraints. SetPrompt(prompt Prompt) } -// PrivateKey is a hardware private key. +// PrivateKey is a hardware private key implementation of [crypto.Signer]. type PrivateKey struct { service Service ref *PrivateKeyRef -} - -// PrivateKeyRef references a specific hardware private key. -type PrivateKeyRef struct { - // SerialNumber is the hardware key's serial number. - SerialNumber uint32 `json:"serial_number"` - // SlotKey is the key name for the hardware key PIV slot, e.g. "9a". - SlotKey PIVSlotKey `json:"slot_key"` - // PublicKey is the public key paired with the hardware private key. - PublicKey crypto.PublicKey `json:"-"` // uses custom JSON marshaling in PKIX, ASN.1 DER form - // Policy specifies the hardware private key's PIN/touch prompt policies. - Policy PromptPolicy `json:"policy"` - // AttestationStatement contains the hardware private key's attestation statement, which is - // to attest the touch and pin requirements for this hardware private key during login. - AttestationStatement *AttestationStatement `json:"attestation_statement"` - // ContextualKeyInfo contains contextual key info which may be used to add context to prompts, - // such as the name of the Teleport user using the key. This information is not saved encoded - // in JSON as this info may depend on the context in which the client is using the key. - ContextualKeyInfo ContextualKeyInfo -} - -// ContextualKeyInfo contains contextual information associated with a hardware [PrivateKey]. -// TODO(Joerger): This is not hardware key specific, so it may be better placed in a more general package -// if it is used more broadly, though moving this to the keys package would cause an import cycle. -type ContextualKeyInfo struct { - // ProxyHost is the root proxy hostname that a key is associated with. - ProxyHost string + keyInfo ContextualKeyInfo } // NewPrivateKey returns a [PrivateKey] for the given service and ref. // keyInfo is an optional argument to supply additional contextual info. -func NewPrivateKey(s Service, ref *PrivateKeyRef) *PrivateKey { +func NewPrivateKey(s Service, ref *PrivateKeyRef, keyInfo ContextualKeyInfo) *PrivateKey { return &PrivateKey{ service: s, ref: ref, + keyInfo: keyInfo, + } +} + +// Encode the hardware private key ref in a format understood by other Teleport clients. +func (p *PrivateKey) Encode() ([]byte, error) { + return p.ref.encode() +} + +// DecodePrivateKey decodes an encoded hardware private key for the given service. +// +// The updateKeyRef func is provided to fill in missing details for old client logins. +// TODO(Joerger): DELETE IN v19.0.0 +func DecodePrivateKey(s Service, encodedKey []byte, keyInfo ContextualKeyInfo, updateKeyRef func(ref *PrivateKeyRef) error) (*PrivateKey, error) { + ref, err := decodeKeyRef(encodedKey) + if err != nil { + return nil, trace.Wrap(err) + } + + // If the public key is missing, this is likely an old login key with only + // the serial number and slot. Fetch missing data from the hardware key. + // This data will be saved to the login key on next login + // TODO(Joerger): DELETE IN v19.0.0 + if ref.PublicKey == nil { + if err := updateKeyRef(ref); err != nil { + return nil, trace.Wrap(err) + } } + + return NewPrivateKey(s, ref, keyInfo), nil } // Public implements [crypto.Signer]. @@ -90,7 +93,7 @@ func (h *PrivateKey) Public() crypto.PublicKey { // Sign implements [crypto.Signer]. func (h *PrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { // When context.TODO() is passed, the service should replace this with its own parent context. - 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. @@ -113,21 +116,36 @@ func (h *PrivateKey) WarmupHardwareKey(ctx context.Context) error { // ed25519 hardware keys outside of the fake "pivtest" service, but we may extend support in // the future as newer keys are being made with ed25519 support (YubiKey 5.7.x, SoloKey). hash := sha512.Sum512(make([]byte, 512)) - _, err := h.service.Sign(ctx, h.ref, rand.Reader, hash[:], crypto.SHA512) + _, err := h.service.Sign(ctx, h.ref, h.keyInfo, rand.Reader, hash[:], crypto.SHA512) return trace.Wrap(err, "failed to perform warmup signature with hardware private key") } -// Encode encodes [h]'s [PrivateKeyRef] to JSON. -func (h *PrivateKey) EncodeKeyRef() ([]byte, error) { - keyRefBytes, err := json.Marshal(h.ref) +// PrivateKeyRef references a specific hardware private key. +type PrivateKeyRef struct { + // SerialNumber is the hardware key's serial number. + SerialNumber uint32 `json:"serial_number"` + // SlotKey is the key name for the hardware key PIV slot, e.g. "9a". + SlotKey PIVSlotKey `json:"slot_key"` + // PublicKey is the public key paired with the hardware private key. + PublicKey crypto.PublicKey `json:"-"` // uses custom JSON marshaling in PKIX, ASN.1 DER form + // Policy specifies the hardware private key's PIN/touch prompt policies. + Policy PromptPolicy `json:"policy"` + // AttestationStatement contains the hardware private key's attestation statement, which is + // to attest the touch and pin requirements for this hardware private key during login. + AttestationStatement *AttestationStatement `json:"attestation_statement"` +} + +// encode encodes a [PrivateKeyRef] to JSON. +func (r *PrivateKeyRef) encode() ([]byte, error) { + keyRefBytes, err := json.Marshal(r) if err != nil { return nil, trace.Wrap(err) } return keyRefBytes, nil } -// DecodeKeyRef decodes a [PrivateKeyRef] from JSON. -func DecodeKeyRef(encodedKeyRef []byte) (*PrivateKeyRef, error) { +// decodeKeyRef decodes a [PrivateKeyRef] from JSON. +func decodeKeyRef(encodedKeyRef []byte) (*PrivateKeyRef, error) { keyRef := &PrivateKeyRef{} if err := json.Unmarshal(encodedKeyRef, keyRef); err != nil { return nil, trace.Wrap(err) @@ -194,3 +212,15 @@ type PrivateKeyConfig struct { // ContextualKeyInfo contains additional info to associate with the key. ContextualKeyInfo ContextualKeyInfo } + +// ContextualKeyInfo contains contextual information associated with a hardware [PrivateKey]. +// TODO(Joerger): This is not hardware key specific, so it may be better placed in a more general package +// if it is used more broadly, though moving this to the keys package would cause an import cycle. +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 +} diff --git a/api/utils/keys/hardwarekey/hardwarekey_test.go b/api/utils/keys/hardwarekey/hardwarekey_test.go new file mode 100644 index 0000000000000..81ca04c03f769 --- /dev/null +++ b/api/utils/keys/hardwarekey/hardwarekey_test.go @@ -0,0 +1,123 @@ +// 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 defines types and interfaces for hardware private keys. + +package hardwarekey_test + +import ( + "context" + "crypto" + "crypto/ed25519" + "io" + "testing" + + "github.com/stretchr/testify/require" + + attestationv1 "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" +) + +// TestEncodeDecodePrivateKey tests encoding and decoding a hardware private key. +// In particular, this tests that the public key is properly encoded and that the +// contextual key info and missing key info (old client logins) is handled correctly. +func TestEncodeDecodePrivateKey(t *testing.T) { + s := &mockHardwareKeyService{} + + pub, _, err := ed25519.GenerateKey(nil) + require.NoError(t, err) + + fullRef := &hardwarekey.PrivateKeyRef{ + SerialNumber: 12345678, + SlotKey: hardwarekey.PivSlotKeyTouch, + PublicKey: pub, + Policy: hardwarekey.PromptPolicyTouch, + AttestationStatement: &hardwarekey.AttestationStatement{ + AttestationStatement: &attestationv1.AttestationStatement_YubikeyAttestationStatement{ + YubikeyAttestationStatement: &attestationv1.YubiKeyAttestationStatement{ + SlotCert: []byte{1}, + AttestationCert: []byte{2}, + }, + }, + }, + } + + contextualKeyInfo := hardwarekey.ContextualKeyInfo{ + ProxyHost: "billy.io", + Username: "Billy@billy.io", + ClusterName: "billy.io", + } + priv := hardwarekey.NewPrivateKey(s, fullRef, contextualKeyInfo) + + for _, tt := range []struct { + name string + ref *hardwarekey.PrivateKeyRef + updateKeyRef func(*hardwarekey.PrivateKeyRef) error + expectPriv *hardwarekey.PrivateKey + }{ + { + name: "new client encoding", + ref: fullRef, + expectPriv: priv, + }, + { + // Old client logins would only have encoded the serial number and slot key. + // TODO(Joerger): DELETE IN v19.0.0 + name: "old client encoding", + ref: &hardwarekey.PrivateKeyRef{ + SerialNumber: 12345678, + SlotKey: hardwarekey.PivSlotKeyTouch, + }, + updateKeyRef: func(ref *hardwarekey.PrivateKeyRef) error { + ref.PublicKey = pub + ref.Policy = hardwarekey.PromptPolicyTouch + ref.AttestationStatement = &hardwarekey.AttestationStatement{ + AttestationStatement: &attestationv1.AttestationStatement_YubikeyAttestationStatement{ + YubikeyAttestationStatement: &attestationv1.YubiKeyAttestationStatement{ + SlotCert: []byte{1}, + AttestationCert: []byte{2}, + }, + }, + } + return nil + }, + expectPriv: priv, + }, + } { + t.Run(tt.name, func(t *testing.T) { + priv := hardwarekey.NewPrivateKey(s, tt.ref, hardwarekey.ContextualKeyInfo{}) + encoded, err := priv.Encode() + require.NoError(t, err) + + decodedPriv, err := hardwarekey.DecodePrivateKey(s, encoded, contextualKeyInfo, tt.updateKeyRef) + require.NoError(t, err) + require.Equal(t, tt.expectPriv, decodedPriv) + }) + } + +} + +type mockHardwareKeyService struct{} + +func (s *mockHardwareKeyService) NewPrivateKey(_ context.Context, _ hardwarekey.PrivateKeyConfig) (*hardwarekey.PrivateKey, error) { + return nil, nil +} + +// Sign performs a cryptographic signature using the specified hardware +// private key and provided signature parameters. +func (s *mockHardwareKeyService) Sign(_ context.Context, _ *hardwarekey.PrivateKeyRef, _ hardwarekey.ContextualKeyInfo, _ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) { + return nil, nil +} + +func (s *mockHardwareKeyService) SetPrompt(_ hardwarekey.Prompt) {} diff --git a/api/utils/keys/hardwarekey/prompt.go b/api/utils/keys/hardwarekey/prompt.go index 23e6da5d7b200..8519fe338fd17 100644 --- a/api/utils/keys/hardwarekey/prompt.go +++ b/api/utils/keys/hardwarekey/prompt.go @@ -21,17 +21,13 @@ import ( ) var ( - // PromptPolicyNone is default the PromptPolicy for private key policy HardwareKey. - PromptPolicyNone = PromptPolicy{TouchRequired: false, PINRequired: false} - // PromptPolicyTouch is default the PromptPolicy for private key policy HardwareKeyTouch. - PromptPolicyTouch = PromptPolicy{TouchRequired: true, PINRequired: false} - // PromptPolicyPIN is default the PromptPolicy for private key policy HardwareKeyPIN. - PromptPolicyPIN = PromptPolicy{TouchRequired: false, PINRequired: true} - // PromptPolicyTouchAndPIN is default the PromptPolicy for private key policy HardwareKeyTouchAndPIN. + PromptPolicyNone = PromptPolicy{TouchRequired: false, PINRequired: false} + PromptPolicyTouch = PromptPolicy{TouchRequired: true, PINRequired: false} + PromptPolicyPIN = PromptPolicy{TouchRequired: false, PINRequired: true} PromptPolicyTouchAndPIN = PromptPolicy{TouchRequired: true, PINRequired: true} ) -// PromptPolicy specifies a hardware private key's PIN/touch policies. +// PromptPolicy specifies a hardware private key's PIN/touch prompt policies. type PromptPolicy struct { // TouchRequired means that touch is required for signatures. TouchRequired bool @@ -66,7 +62,7 @@ const ( PINRequired ) -// PINAndPUK describes a response returned from HardwareKeyPrompt.ChangePIN. +// PINAndPUK describes a response returned from [Prompt].ChangePIN. type PINAndPUK struct { // New PIN set by the user. PIN string @@ -77,15 +73,15 @@ type PINAndPUK struct { PUKChanged bool } -// Validate [p]. +// Validate the user-provided PIN and PUK. func (p PINAndPUK) Validate() error { - if !IsPINLengthValid(p.PIN) { + if !isPINLengthValid(p.PIN) { return trace.BadParameter("PIN must be 6-8 characters long") } if p.PIN == defaultPIN { return trace.BadParameter("The default PIN is not supported") } - if !IsPINLengthValid(p.PUK) { + if !isPINLengthValid(p.PUK) { return trace.BadParameter("PUK must be 6-8 characters long") } if p.PUK == defaultPUK { @@ -94,7 +90,7 @@ func (p PINAndPUK) Validate() error { return nil } -// IsPINLengthValid returns whether the given PIV PIN, or PUK, is of valid length (6-8 characters). -func IsPINLengthValid(pin string) bool { +// isPINLengthValid returns whether the given PIV PIN, or PUK, is of valid length (6-8 characters). +func isPINLengthValid(pin string) bool { return len(pin) >= 6 && len(pin) <= 8 } diff --git a/api/utils/keys/hardwarekey/slot.go b/api/utils/keys/hardwarekey/slot.go index 80b785cd89bfe..092177bd27e7d 100644 --- a/api/utils/keys/hardwarekey/slot.go +++ b/api/utils/keys/hardwarekey/slot.go @@ -33,33 +33,6 @@ const ( pivSlotKeyPIN PIVSlotKey = 0x9e ) -// PIVSlotKeyString is the string representation of a PIV slot key. -type PIVSlotKeyString string - -// Validate that the PIV slot is a valid value. -func (s PIVSlotKeyString) Validate() error { - slotKey, err := strconv.ParseUint(string(s), 16, 32) - if err != nil { - return trace.Wrap(err) - } - - switch PIVSlotKey(slotKey) { - case pivSlotKeyBasic, PivSlotKeyTouch, pivSlotKeyTouchAndPIN, pivSlotKeyPIN: - return nil - default: - return trace.BadParameter("invalid PIV slot %q", s) - } -} - -// Validate that the PIV slot is a valid value. -func (s PIVSlotKeyString) Parse() (PIVSlotKey, error) { - slotKey, err := strconv.ParseUint(string(s), 16, 32) - if err != nil { - return 0, trace.Wrap(err) - } - return PIVSlotKey(slotKey), nil -} - // GetDefaultSlotKey gets the default PIV slot key for the given [policy]. func GetDefaultSlotKey(policy PromptPolicy) (PIVSlotKey, error) { switch policy { @@ -72,6 +45,30 @@ func GetDefaultSlotKey(policy PromptPolicy) (PIVSlotKey, error) { case PromptPolicyTouchAndPIN: return pivSlotKeyTouchAndPIN, nil default: - return 0, trace.BadParameter("unexpected private key policy %v", policy) + return 0, trace.BadParameter("unexpected prompt policy %v", policy) + } +} + +// PIVSlotKeyString is the string representation of a [PIVSlotKey]. +type PIVSlotKeyString string + +// Validate that [s] parses into a valid [PIVSlotKey]. +func (s PIVSlotKeyString) Validate() error { + _, err := s.Parse() + return trace.Wrap(err) +} + +// Parse [s] into a [PIVSlotKey]. +func (s PIVSlotKeyString) Parse() (PIVSlotKey, error) { + slotKey, err := strconv.ParseUint(string(s), 16, 32) + if err != nil { + return 0, trace.Wrap(err, "failed to parse %q as a uint", s) + } + + switch p := PIVSlotKey(slotKey); p { + case pivSlotKeyBasic, PivSlotKeyTouch, pivSlotKeyTouchAndPIN, pivSlotKeyPIN: + return p, nil + default: + return 0, trace.BadParameter("invalid PIV slot %q", s) } } diff --git a/api/utils/keys/hardwarekey/slot_test.go b/api/utils/keys/hardwarekey/slot_test.go new file mode 100644 index 0000000000000..51ede5346b899 --- /dev/null +++ b/api/utils/keys/hardwarekey/slot_test.go @@ -0,0 +1,68 @@ +// 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 defines types and interfaces for hardware private keys. + +package hardwarekey_test + +import ( + "testing" + + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" +) + +func TestXxx(t *testing.T) { + for _, tt := range []struct { + slotString hardwarekey.PIVSlotKeyString + expectPIVSlotKey hardwarekey.PIVSlotKey + assertError require.ErrorAssertionFunc + }{ + { + slotString: "9a", + expectPIVSlotKey: 0x9a, + assertError: require.NoError, + }, { + slotString: "9c", + expectPIVSlotKey: 0x9c, + assertError: require.NoError, + }, { + slotString: "9d", + expectPIVSlotKey: 0x9d, + assertError: require.NoError, + }, { + slotString: "9e", + expectPIVSlotKey: 0x9e, + assertError: require.NoError, + }, { + slotString: "invalid_uint", + expectPIVSlotKey: 0, + assertError: require.Error, + }, { + slotString: "9b", // unsupported slot key + expectPIVSlotKey: 0, + assertError: func(t require.TestingT, err error, i ...interface{}) { + require.True(t, trace.IsBadParameter(err)) + }, + }, + } { + t.Run(string(tt.slotString), func(t *testing.T) { + pivSlotKey, err := tt.slotString.Parse() + tt.assertError(t, err) + require.Equal(t, tt.expectPIVSlotKey, pivSlotKey) + }) + } +} diff --git a/api/utils/keys/piv/yubikey.go b/api/utils/keys/piv/yubikey.go index 0e298db3ef296..bfe8f6b85bb74 100644 --- a/api/utils/keys/piv/yubikey.go +++ b/api/utils/keys/piv/yubikey.go @@ -138,7 +138,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) @@ -161,7 +161,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, hardwarekey.ContextualKeyInfo{}) + err := prompt.Touch(ctx, keyInfo) if err != nil { // Cancel the entire function when an error occurs. // This is typically used for aborting the prompt. @@ -183,7 +183,7 @@ func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, prom defer touchPromptDelayTimer.Reset(signTouchPromptDelay) } } - pass, err := prompt.AskPIN(ctx, hardwarekey.PINRequired, hardwarekey.ContextualKeyInfo{}) + pass, err := prompt.AskPIN(ctx, hardwarekey.PINRequired, keyInfo) return pass, trace.Wrap(err) } diff --git a/api/utils/keys/piv/yubikey_service.go b/api/utils/keys/piv/yubikey_service.go index d4964de0eb60e..456fa47dd90ca 100644 --- a/api/utils/keys/piv/yubikey_service.go +++ b/api/utils/keys/piv/yubikey_service.go @@ -54,8 +54,8 @@ type YubiKeyService struct { promptMux sync.Mutex prompt hardwarekey.Prompt - // ctx is provided to signature requests, since `crypto.Sign` does have - // context support directly. + // ctx is provided to signature requests since the [crypto.Signer] interface + // does not have context support directly. ctx context.Context } @@ -84,8 +84,8 @@ func NewYubiKeyService(ctx context.Context, prompt hardwarekey.Prompt) *YubiKeyS // If a customSlot is not provided, the service uses the default slot for the given policy: // - !touch & !pin -> 9a // - !touch & pin -> 9c -// - touch & pin -> 9d -// - touch & !pin -> 9e +// - touch & pin -> 9d +// - touch & !pin -> 9e func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.PrivateKeyConfig) (*hardwarekey.PrivateKey, error) { // Use the first yubiKey we find. y, err := s.getYubiKey(0) @@ -121,9 +121,7 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P if err != nil { return nil, trace.Wrap(err) } - - ref.ContextualKeyInfo = config.ContextualKeyInfo - return hardwarekey.NewPrivateKey(s, ref), nil + return hardwarekey.NewPrivateKey(s, ref, config.ContextualKeyInfo), nil } // If a custom slot was not specified, check for a key in the @@ -136,7 +134,8 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P case err != nil: return nil, trace.Wrap(err) - // Unknown cert found, prompt the user before we overwrite the slot. + // 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), config.ContextualKeyInfo); err != nil { return nil, trace.Wrap(err) @@ -145,8 +144,8 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P } } - // Check for an existing key in the slot that satisfies the required private - // key policy, or generate a new one if needed. + // Check for an existing key in the slot that satisfies the required + // prompt policy, or generate a new one if needed. slotCert, attCert, att, err := y.attestKey(pivSlot) switch { case errors.Is(err, piv.ErrNotFound): @@ -186,13 +185,12 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P }, }, }, - ContextualKeyInfo: config.ContextualKeyInfo, - }), nil + }, 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) { // Usually, Sign will be called without context through the [crypto.Signer] interface, // so we opportunistically set the context. if ctx == context.TODO() { @@ -207,7 +205,7 @@ func (s *YubiKeyService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRe s.promptMux.Lock() defer s.promptMux.Unlock() - return y.sign(ctx, ref, s.prompt, rand, digest, opts) + return y.sign(ctx, ref, keyInfo, s.prompt, rand, digest, opts) } // SetPrompt sets the hardware key prompt used by the hardware key service, if applicable. @@ -219,7 +217,7 @@ func (s *YubiKeyService) SetPrompt(prompt hardwarekey.Prompt) { s.prompt = prompt } -// Get the given YubiKey with the serial number. If the provided serialNumber is "0", +// Get the given YubiKey with the serial number. If the provided serialNumber is 0, // return the first YubiKey found in the smart card list. func (s *YubiKeyService) getYubiKey(serialNumber uint32) (*YubiKey, error) { yubiKeysMux.Lock() diff --git a/api/utils/keys/piv/yubikey_service_fake.go b/api/utils/keys/piv/yubikey_service_fake.go index 0c0cc2c76b208..538a21d76f8aa 100644 --- a/api/utils/keys/piv/yubikey_service_fake.go +++ b/api/utils/keys/piv/yubikey_service_fake.go @@ -29,6 +29,9 @@ import ( "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) +// TODO(Joerger): Instead of using a distinct build tag for tests, tests should inject +// a mock hardware key service, e.g. into the CLI conf. + // TODO(Joerger): Rather than using a global cache, clients should be updated to // create a single YubiKeyService and ensure it is reused across the program // execution. @@ -110,7 +113,7 @@ func (s *fakeYubiKeyPIVService) NewPrivateKey(ctx context.Context, config hardwa // Sign performs a cryptographic signature using the specified hardware // private key and provided signature parameters. -func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { +func (s *fakeYubiKeyPIVService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, _ hardwarekey.ContextualKeyInfo, rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { hardwarePrivateKeysMux.Lock() defer hardwarePrivateKeysMux.Unlock() diff --git a/api/utils/keys/piv/yubikey_unavailable.go b/api/utils/keys/piv/yubikey_unavailable.go index 4bec50ad7a439..f52a5cd7f7e16 100644 --- a/api/utils/keys/piv/yubikey_unavailable.go +++ b/api/utils/keys/piv/yubikey_unavailable.go @@ -40,7 +40,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) } diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index 454f1121494f6..d9e52c0038447 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -280,9 +280,9 @@ func WithHardwareKeyService(hwKeyService hardwarekey.Service) ParsePrivateKeyOpt } // WithContextualKeyInfo adds contextual key info to the parsed private key. -func WithContextualKeyInfo(proxyHost string) ParsePrivateKeyOpt { +func WithContextualKeyInfo(info hardwarekey.ContextualKeyInfo) ParsePrivateKeyOpt { return func(o *ParsePrivateKeyOptions) { - o.ContextualKeyInfo = hardwarekey.ContextualKeyInfo{ProxyHost: proxyHost} + o.ContextualKeyInfo = info } } @@ -305,23 +305,11 @@ func ParsePrivateKey(keyPEM []byte, opts ...ParsePrivateKeyOpt) (*PrivateKey, er return nil, trace.BadParameter("cannot parse hardware private key without an initialized hardware key service") } - keyRef, err := hardwarekey.DecodeKeyRef(block.Bytes) + hwPrivateKey, err := hardwarekey.DecodePrivateKey(appliedOpts.HardwareKeyService, block.Bytes, appliedOpts.ContextualKeyInfo, piv.UpdateKeyRef) if err != nil { return nil, trace.Wrap(err, "failed to parse hardware private key") } - // If the public key is missing, this is likely an old login key with only - // the serial number and slot. Fetch missing data from the hardware key. - // This data will be saved to the login key on next login - // TODO(Joerger): DELETE IN v19.0.0 - if keyRef.PublicKey == nil { - if err := piv.UpdateKeyRef(keyRef); err != nil { - return nil, trace.Wrap(err) - } - } - - keyRef.ContextualKeyInfo = appliedOpts.ContextualKeyInfo - hwPrivateKey := hardwarekey.NewPrivateKey(appliedOpts.HardwareKeyService, keyRef) return newPrivateKeyWithKeyPEM(hwPrivateKey, keyPEM) case OpenSSHPrivateKeyType: priv, err := ssh.ParseRawPrivateKey(keyPEM) @@ -394,15 +382,13 @@ func MarshalPrivateKey(key crypto.Signer) ([]byte, error) { }) return privPEM, nil case *hardwarekey.PrivateKey: - encodedKeyRef, err := privateKey.EncodeKeyRef() + encodedKey, err := privateKey.Encode() if err != nil { return nil, trace.Wrap(err) } - privPEM := pem.EncodeToMemory(&pem.Block{ - Type: pivYubiKeyPrivateKeyType, - Headers: nil, - Bytes: encodedKeyRef, + Type: pivYubiKeyPrivateKeyType, + Bytes: encodedKey, }) return privPEM, nil default: diff --git a/lib/client/api.go b/lib/client/api.go index e56e3e8b514ee..db1c252554013 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -4006,7 +4006,9 @@ func (tc *TeleportClient) GetNewLoginKeyRing(ctx context.Context) (keyRing *KeyR PINRequired: tc.PrivateKeyPolicy.IsHardwareKeyPINVerified(), }, ContextualKeyInfo: hardwarekey.ContextualKeyInfo{ - ProxyHost: tc.WebProxyHost(), + ProxyHost: tc.WebProxyHost(), + Username: tc.Username, + ClusterName: tc.SiteName, }, }) if err != nil { diff --git a/lib/client/interfaces.go b/lib/client/interfaces.go index f2e207b1717ed..2e0d6256aab57 100644 --- a/lib/client/interfaces.go +++ b/lib/client/interfaces.go @@ -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. diff --git a/lib/client/keystore.go b/lib/client/keystore.go index 83e374515752d..59427c7cda46b 100644 --- a/lib/client/keystore.go +++ b/lib/client/keystore.go @@ -536,7 +536,12 @@ 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), keys.WithContextualKeyInfo(idx.ProxyHost)) + 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 { @@ -547,7 +552,12 @@ 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), keys.WithContextualKeyInfo(idx.ProxyHost)) + 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) } @@ -570,7 +580,13 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, hwks hardwarekey.Service, opt } func (fs *FSKeyStore) updateKeyRingWithCerts(o CertOption, hwks hardwarekey.Service, keyRing *KeyRing) error { - return trace.Wrap(o.updateKeyRing(fs.KeyDir, keyRing.KeyRingIndex, keyRing, keys.WithHardwareKeyService(hwks), keys.WithContextualKeyInfo(keyRing.ProxyHost))) + 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. From f311fd3a87c1157ebde1a397d9c4d1ed17f4ab4b Mon Sep 17 00:00:00 2001 From: joerger Date: Wed, 26 Mar 2025 10:58:40 -0700 Subject: [PATCH 10/10] Remove temporary alias types; Update e ref. --- api/utils/keys/alias.go | 23 ---------- api/utils/keys/policy_piv.go | 45 ------------------- e | 2 +- .../autoupdate/tools/updater/modules.go | 3 +- lib/auth/auth.go | 11 ++--- lib/auth/authclient/clt.go | 9 ++-- lib/auth/sessions.go | 5 ++- lib/client/cluster_client.go | 3 +- lib/client/weblogin.go | 18 ++++---- lib/modules/test.go | 3 +- tool/teleport/testenv/test_server.go | 3 +- tool/tsh/common/tsh_test.go | 3 +- 12 files changed, 34 insertions(+), 94 deletions(-) delete mode 100644 api/utils/keys/alias.go delete mode 100644 api/utils/keys/policy_piv.go diff --git a/api/utils/keys/alias.go b/api/utils/keys/alias.go deleted file mode 100644 index e8e9863c6a733..0000000000000 --- a/api/utils/keys/alias.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -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 keys - -import "github.com/gravitational/teleport/api/utils/keys/hardwarekey" - -// Temporary aliases for types moved to the hardwarekey or piv packages -// TODO(Joerger): Remove once /e no longer relies on them. - -type AttestationStatement = hardwarekey.AttestationStatement - -var AttestationStatementFromProto = hardwarekey.AttestationStatementFromProto diff --git a/api/utils/keys/policy_piv.go b/api/utils/keys/policy_piv.go deleted file mode 100644 index a99fc532f3010..0000000000000 --- a/api/utils/keys/policy_piv.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build piv && !pivtest - -/* -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 keys - -import ( - "github.com/go-piv/piv-go/piv" -) - -// GetPrivateKeyPolicyFromAttestation returns the PrivateKeyPolicy satisfied by the given hardware key attestation. -// TODO(Joerger): Move to /e where this is used. -func GetPrivateKeyPolicyFromAttestation(att *piv.Attestation) PrivateKeyPolicy { - if att == nil { - return PrivateKeyPolicyNone - } - - isTouchPolicy := att.TouchPolicy == piv.TouchPolicyCached || - att.TouchPolicy == piv.TouchPolicyAlways - - isPINPolicy := att.PINPolicy == piv.PINPolicyOnce || - att.PINPolicy == piv.PINPolicyAlways - - switch { - case isPINPolicy && isTouchPolicy: - return PrivateKeyPolicyHardwareKeyTouchAndPIN - case isPINPolicy: - return PrivateKeyPolicyHardwareKeyPIN - case isTouchPolicy: - return PrivateKeyPolicyHardwareKeyTouch - default: - return PrivateKeyPolicyHardwareKey - } -} diff --git a/e b/e index 24ad965db73c3..ccba574b0619b 160000 --- a/e +++ b/e @@ -1 +1 @@ -Subproject commit 24ad965db73c347ba97793b0f3fc0005e1ec7d1a +Subproject commit ccba574b0619babb93e7cae55ff16a695907b207 diff --git a/integration/autoupdate/tools/updater/modules.go b/integration/autoupdate/tools/updater/modules.go index 6c1236393af78..5aac39757a2e4 100644 --- a/integration/autoupdate/tools/updater/modules.go +++ b/integration/autoupdate/tools/updater/modules.go @@ -31,6 +31,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/accesslist" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/entitlements" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/tlsca" @@ -99,7 +100,7 @@ func (p *TestModules) IsBoringBinary() bool { } // AttestHardwareKey attests a hardware key. -func (p *TestModules) AttestHardwareKey(context.Context, interface{}, *keys.AttestationStatement, crypto.PublicKey, time.Duration) (*keys.AttestationData, error) { +func (p *TestModules) AttestHardwareKey(context.Context, interface{}, *hardwarekey.AttestationStatement, crypto.PublicKey, time.Duration) (*keys.AttestationData, error) { return nil, trace.NotFound("no attestation data for the given key") } diff --git a/lib/auth/auth.go b/lib/auth/auth.go index ebb249be6bfc8..43071b3d1ce82 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -80,6 +80,7 @@ import ( "github.com/gravitational/teleport/api/types/wrappers" 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/retryutils" apisshutils "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/entitlements" @@ -2208,9 +2209,9 @@ type certRequest struct { // TLS certificate. tlsPublicKey []byte // sshPublicKeyAttestationStatement is an attestation statement associated with sshPublicKey. - sshPublicKeyAttestationStatement *keys.AttestationStatement + sshPublicKeyAttestationStatement *hardwarekey.AttestationStatement // tlsPublicKeyAttestationStatement is an attestation statement associated with tlsPublicKey. - tlsPublicKeyAttestationStatement *keys.AttestationStatement + tlsPublicKeyAttestationStatement *hardwarekey.AttestationStatement // user is a user to generate certificate for user services.UserState @@ -2453,8 +2454,8 @@ type GenerateUserTestCertsRequest struct { RouteToCluster string PinnedIP string MFAVerified string - SSHAttestationStatement *keys.AttestationStatement - TLSAttestationStatement *keys.AttestationStatement + SSHAttestationStatement *hardwarekey.AttestationStatement + TLSAttestationStatement *hardwarekey.AttestationStatement AppName string AppSessionID string } @@ -3476,7 +3477,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types. type attestHardwareKeyParams struct { requiredKeyPolicy keys.PrivateKeyPolicy pubKey crypto.PublicKey - attestationStatement *keys.AttestationStatement + attestationStatement *hardwarekey.AttestationStatement sessionTTL time.Duration readOnlyAuthPref readonly.AuthPreference userName string diff --git a/lib/auth/authclient/clt.go b/lib/auth/authclient/clt.go index 1e638d7561aeb..5d4c8766a79b6 100644 --- a/lib/auth/authclient/clt.go +++ b/lib/auth/authclient/clt.go @@ -63,6 +63,7 @@ import ( "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" accessgraphv1 "github.com/gravitational/teleport/gen/proto/go/accessgraph/v1alpha" wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" "github.com/gravitational/teleport/lib/defaults" @@ -1426,7 +1427,7 @@ func UserPublicKeys(pubIn, sshPubIn, tlsPubIn []byte) (sshPubOut, tlsPubOut []by // sshAttIn and tlsAttIn will be returned. // [sshAttIn] and [tlsAttIn] should be the SSH and TLS attestation statements // set by any post-17.0.0 client. -func UserAttestationStatements(attIn, sshAttIn, tlsAttIn *keys.AttestationStatement) (sshAttOut, tlsAttOut *keys.AttestationStatement) { +func UserAttestationStatements(attIn, sshAttIn, tlsAttIn *hardwarekey.AttestationStatement) (sshAttOut, tlsAttOut *hardwarekey.AttestationStatement) { if attIn == nil { return sshAttIn, tlsAttIn } @@ -1469,14 +1470,14 @@ type AuthenticateSSHRequest struct { // AttestationStatement is an attestation statement associated with the given public key. // // Deprecated: prefer SSHAttestationStatement and/or TLSAttestationStatement. - AttestationStatement *keys.AttestationStatement `json:"attestation_statement,omitempty"` + AttestationStatement *hardwarekey.AttestationStatement `json:"attestation_statement,omitempty"` // SSHAttestationStatement is an attestation statement associated with the // given SSH public key. - SSHAttestationStatement *keys.AttestationStatement `json:"ssh_attestation_statement,omitempty"` + SSHAttestationStatement *hardwarekey.AttestationStatement `json:"ssh_attestation_statement,omitempty"` // TLSAttestationStatement is an attestation statement associated with the // given TLS public key. - TLSAttestationStatement *keys.AttestationStatement `json:"tls_attestation_statement,omitempty"` + TLSAttestationStatement *hardwarekey.AttestationStatement `json:"tls_attestation_statement,omitempty"` } // CheckAndSetDefaults checks and sets default certificate values diff --git a/lib/auth/sessions.go b/lib/auth/sessions.go index caf98e262b0f8..a1d0958b7f51e 100644 --- a/lib/auth/sessions.go +++ b/lib/auth/sessions.go @@ -35,6 +35,7 @@ import ( "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/entitlements" "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" @@ -700,8 +701,8 @@ type SessionCertsRequest struct { SessionTTL time.Duration SSHPubKey []byte TLSPubKey []byte - SSHAttestationStatement *keys.AttestationStatement - TLSAttestationStatement *keys.AttestationStatement + SSHAttestationStatement *hardwarekey.AttestationStatement + TLSAttestationStatement *hardwarekey.AttestationStatement Compatibility string RouteToCluster string KubernetesCluster string diff --git a/lib/client/cluster_client.go b/lib/client/cluster_client.go index 903d68c7d08c0..b75ccf1c018e0 100644 --- a/lib/client/cluster_client.go +++ b/lib/client/cluster_client.go @@ -35,6 +35,7 @@ import ( mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/resumption" @@ -393,7 +394,7 @@ func (c *ClusterClient) prepareUserCertsRequest(ctx context.Context, params Reis } var sshPub, tlsPub []byte - var sshAttestationStatement, tlsAttestationStatement *keys.AttestationStatement + var sshAttestationStatement, tlsAttestationStatement *hardwarekey.AttestationStatement if sshSubjectKey != nil { sshPub = sshSubjectKey.MarshalSSHPublicKey() sshAttestationStatement = sshSubjectKey.GetAttestationStatement() diff --git a/lib/client/weblogin.go b/lib/client/weblogin.go index 0aa7f07ea6503..6640a48753e04 100644 --- a/lib/client/weblogin.go +++ b/lib/client/weblogin.go @@ -42,7 +42,7 @@ import ( "github.com/gravitational/teleport/api/client/webclient" "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/lib/auth/authclient" wancli "github.com/gravitational/teleport/lib/auth/webauthncli" wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" @@ -257,13 +257,13 @@ type UserPublicKeys struct { // Deprecated: prefer SSHAttestationStatement and/or TLSAttestationStatement. // TODO(nklaassen): DELETE IN 18.0.0 when all clients should be using // separate keys. - AttestationStatement *keys.AttestationStatement `json:"attestation_statement,omitempty"` + AttestationStatement *hardwarekey.AttestationStatement `json:"attestation_statement,omitempty"` // SSHAttestationStatement is an attestation statement associated with the // given SSH public key. - SSHAttestationStatement *keys.AttestationStatement `json:"ssh_attestation_statement,omitempty"` + SSHAttestationStatement *hardwarekey.AttestationStatement `json:"ssh_attestation_statement,omitempty"` // TLSAttestationStatement is an attestation statement associated with the // given TLS public key. - TLSAttestationStatement *keys.AttestationStatement `json:"tls_attestation_statement,omitempty"` + TLSAttestationStatement *hardwarekey.AttestationStatement `json:"tls_attestation_statement,omitempty"` } // CheckAndSetDefaults checks and sets default values. @@ -312,13 +312,13 @@ type SSOUserPublicKeys struct { // AttestationStatement is an attestation statement associated with the given public key. // // Deprecated: prefer SSHAttestationStatement and/or TLSAttestationStatement. - AttestationStatement *keys.AttestationStatement `json:"attestation_statement,omitempty"` + AttestationStatement *hardwarekey.AttestationStatement `json:"attestation_statement,omitempty"` // SSHAttestationStatement is an attestation statement associated with the // given SSH public key. - SSHAttestationStatement *keys.AttestationStatement `json:"ssh_attestation_statement,omitempty"` + SSHAttestationStatement *hardwarekey.AttestationStatement `json:"ssh_attestation_statement,omitempty"` // TLSAttestationStatement is an attestation statement associated with the // given TLS public key. - TLSAttestationStatement *keys.AttestationStatement `json:"tls_attestation_statement,omitempty"` + TLSAttestationStatement *hardwarekey.AttestationStatement `json:"tls_attestation_statement,omitempty"` } // CheckAndSetDefaults checks and sets default values. @@ -415,9 +415,9 @@ type SSHLogin struct { // credentials to. KubernetesCluster string // SSHAttestationStatement is an attestation statement for SSHPubKey. - SSHAttestationStatement *keys.AttestationStatement + SSHAttestationStatement *hardwarekey.AttestationStatement // TLSAttestationStatement is an attestation statement for TLSPubKey. - TLSAttestationStatement *keys.AttestationStatement + TLSAttestationStatement *hardwarekey.AttestationStatement // ExtraHeaders is a map of extra HTTP headers to be included in requests. ExtraHeaders map[string]string } diff --git a/lib/modules/test.go b/lib/modules/test.go index 82a4afd24d53f..efbe9c1e26684 100644 --- a/lib/modules/test.go +++ b/lib/modules/test.go @@ -27,6 +27,7 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) // TestModules implements the Modules interface for testing. @@ -104,7 +105,7 @@ func (m *TestModules) IsOSSBuild() bool { } // AttestHardwareKey attests a hardware key. -func (m *TestModules) AttestHardwareKey(ctx context.Context, obj interface{}, as *keys.AttestationStatement, pk crypto.PublicKey, d time.Duration) (*keys.AttestationData, error) { +func (m *TestModules) AttestHardwareKey(ctx context.Context, obj interface{}, as *hardwarekey.AttestationStatement, pk crypto.PublicKey, d time.Duration) (*keys.AttestationData, error) { if m.MockAttestationData != nil { return m.MockAttestationData, nil } diff --git a/tool/teleport/testenv/test_server.go b/tool/teleport/testenv/test_server.go index 1e74cedda3275..b36f269856b5b 100644 --- a/tool/teleport/testenv/test_server.go +++ b/tool/teleport/testenv/test_server.go @@ -45,6 +45,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/accesslist" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" apisshutils "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/entitlements" "github.com/gravitational/teleport/lib" @@ -546,7 +547,7 @@ func (p *cliModules) IsBoringBinary() bool { } // AttestHardwareKey attests a hardware key. -func (p *cliModules) AttestHardwareKey(_ context.Context, _ interface{}, _ *keys.AttestationStatement, _ crypto.PublicKey, _ time.Duration) (*keys.AttestationData, error) { +func (p *cliModules) AttestHardwareKey(_ context.Context, _ interface{}, _ *hardwarekey.AttestationStatement, _ crypto.PublicKey, _ time.Duration) (*keys.AttestationData, error) { return nil, trace.NotFound("no attestation data for the given key") } diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index d51d82085bc73..4f0932219a430 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -69,6 +69,7 @@ import ( apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/api/utils/keypaths" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/entitlements" "github.com/gravitational/teleport/integration/kube" @@ -266,7 +267,7 @@ func (p *cliModules) IsBoringBinary() bool { } // AttestHardwareKey attests a hardware key. -func (p *cliModules) AttestHardwareKey(_ context.Context, _ interface{}, _ *keys.AttestationStatement, _ crypto.PublicKey, _ time.Duration) (*keys.AttestationData, error) { +func (p *cliModules) AttestHardwareKey(_ context.Context, _ interface{}, _ *hardwarekey.AttestationStatement, _ crypto.PublicKey, _ time.Duration) (*keys.AttestationData, error) { return nil, trace.NotFound("no attestation data for the given key") }