From 636c945f94c7e9dee8080ff5aa89c3b1a845f337 Mon Sep 17 00:00:00 2001 From: joerger Date: Wed, 2 Apr 2025 10:43:28 -0700 Subject: [PATCH 1/4] Move hardware key files into new hardwarekey package. --- api/utils/keys/alias.go | 23 +++ api/utils/keys/cliprompt.go | 130 -------------- api/utils/keys/hardwarekey/attestation.go | 50 ++++++ api/utils/keys/hardwarekey/cliprompt.go | 153 +++++++++++++++++ api/utils/keys/hardwarekey/cliprompt_test.go | 161 ++++++++++++++++++ api/utils/keys/hardwarekey/prompt.go | 96 +++++++++++ api/utils/keys/hardwaresigner.go | 38 +---- api/utils/keys/policy_piv.go | 45 +++++ api/utils/keys/privatekey.go | 9 +- api/utils/keys/yubikey.go | 70 ++------ api/utils/keys/yubikey_common.go | 40 +---- api/utils/keys/yubikey_fake.go | 15 +- api/utils/keys/yubikey_other.go | 12 +- .../autoupdate/tools/updater/modules.go | 3 +- integration/proxy/teleterm_test.go | 6 +- integration/teleterm_test.go | 22 +-- lib/auth/auth.go | 11 +- lib/auth/auth_with_roles.go | 8 +- lib/auth/authclient/clt.go | 9 +- lib/auth/github.go | 8 +- lib/auth/sessions.go | 5 +- lib/client/api.go | 3 +- lib/client/client_store.go | 3 +- lib/client/cluster_client.go | 3 +- lib/client/keystore.go | 23 +-- lib/client/weblogin.go | 18 +- lib/modules/modules.go | 5 +- lib/modules/test.go | 3 +- lib/teleterm/clusters/config.go | 4 +- lib/teleterm/daemon/daemon_test.go | 10 +- lib/teleterm/daemon/hardwarekeyprompt.go | 12 +- lib/teleterm/teleterm.go | 6 +- tool/teleport/testenv/test_server.go | 3 +- tool/tsh/common/tsh_test.go | 3 +- 34 files changed, 656 insertions(+), 354 deletions(-) create mode 100644 api/utils/keys/alias.go delete mode 100644 api/utils/keys/cliprompt.go create mode 100644 api/utils/keys/hardwarekey/attestation.go create mode 100644 api/utils/keys/hardwarekey/cliprompt.go create mode 100644 api/utils/keys/hardwarekey/cliprompt_test.go create mode 100644 api/utils/keys/hardwarekey/prompt.go create mode 100644 api/utils/keys/policy_piv.go 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/cliprompt.go b/api/utils/keys/cliprompt.go deleted file mode 100644 index 7dce20d211a7c..0000000000000 --- a/api/utils/keys/cliprompt.go +++ /dev/null @@ -1,130 +0,0 @@ -//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" - "fmt" - "os" - - "github.com/go-piv/piv-go/piv" - "github.com/gravitational/trace" - - "github.com/gravitational/teleport/api/utils/prompt" -) - -type cliPrompt struct{} - -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]" - } - password, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), message) - return password, trace.Wrap(err) -} - -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) { - var pinAndPUK = &PINAndPUK{} - for { - fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PIN.\n") - newPIN, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your new YubiKey PIV PIN") - if err != nil { - return nil, trace.Wrap(err) - } - newPINConfirm, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Confirm your new YubiKey PIV PIN") - if err != nil { - return nil, trace.Wrap(err) - } - - if newPIN != newPINConfirm { - fmt.Fprintf(os.Stderr, "PINs do not match.\n") - continue - } - - if newPIN == piv.DefaultPIN { - fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN) - continue - } - - if !isPINLengthValid(newPIN) { - fmt.Fprintf(os.Stderr, "PIN must be 6-8 characters long.\n") - continue - } - - pinAndPUK.PIN = newPIN - break - } - - puk, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your YubiKey PIV PUK to reset PIN [blank to use default PUK]") - if err != nil { - return nil, trace.Wrap(err) - } - pinAndPUK.PUK = puk - - switch puk { - case piv.DefaultPUK: - fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", piv.DefaultPUK) - fallthrough - case "": - for { - fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PUK (used to reset PIN).\n") - newPUK, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your new YubiKey PIV PUK") - if err != nil { - return nil, trace.Wrap(err) - } - newPUKConfirm, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Confirm your new YubiKey PIV PUK") - if err != nil { - return nil, trace.Wrap(err) - } - - if newPUK != newPUKConfirm { - fmt.Fprintf(os.Stderr, "PUKs do not match.\n") - continue - } - - if newPUK == piv.DefaultPUK { - fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", piv.DefaultPUK) - continue - } - - if !isPINLengthValid(newPUK) { - fmt.Fprintf(os.Stderr, "PUK must be 6-8 characters long.\n") - continue - } - - pinAndPUK.PUK = newPUK - pinAndPUK.PUKChanged = true - break - } - } - return pinAndPUK, nil -} - -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) -} - -func isPINLengthValid(pin string) bool { - return len(pin) >= 6 && len(pin) <= 8 -} diff --git a/api/utils/keys/hardwarekey/attestation.go b/api/utils/keys/hardwarekey/attestation.go new file mode 100644 index 0000000000000..94de91e177dc0 --- /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 (a *AttestationStatement) ToProto() *attestationv1.AttestationStatement { + return (*attestationv1.AttestationStatement)(a) +} + +// AttestationStatementFromProto converts an AttestationStatement from its protobuf form. +func AttestationStatementFromProto(att *attestationv1.AttestationStatement) *AttestationStatement { + return (*AttestationStatement)(att) +} + +// MarshalJSON implements custom protobuf json marshaling. +func (a *AttestationStatement) MarshalJSON() ([]byte, error) { + buf := new(bytes.Buffer) + err := (&jsonpb.Marshaler{}).Marshal(buf, a.ToProto()) + return buf.Bytes(), trace.Wrap(err) +} + +// UnmarshalJSON implements custom protobuf json unmarshaling. +func (a *AttestationStatement) UnmarshalJSON(buf []byte) error { + return (&jsonpb.Unmarshaler{AllowUnknownFields: true}).Unmarshal(bytes.NewReader(buf), a.ToProto()) +} diff --git a/api/utils/keys/hardwarekey/cliprompt.go b/api/utils/keys/hardwarekey/cliprompt.go new file mode 100644 index 0000000000000..83ee48a030bc1 --- /dev/null +++ b/api/utils/keys/hardwarekey/cliprompt.go @@ -0,0 +1,153 @@ +// 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 hardwarekey + +import ( + "context" + "fmt" + "io" + "os" + + "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 { + writer io.Writer + reader prompt.StdinReader +} + +// NewStdCLIPrompt returns a new CLIPrompt with stderr and stdout. +func NewStdCLIPrompt() *cliPrompt { + return &cliPrompt{ + writer: os.Stderr, + reader: prompt.Stdin(), + } +} + +// NewStdCLIPrompt returns a new CLIPrompt with the given writer and reader. +// Used in tests. +func NewCLIPrompt(w io.Writer, r prompt.StdinReader) *cliPrompt { + return &cliPrompt{ + writer: w, + reader: r, + } +} + +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]" + } + password, err := prompt.Password(ctx, c.writer, c.reader, message) + return password, trace.Wrap(err) +} + +func (c *cliPrompt) Touch(_ context.Context) error { + _, err := fmt.Fprintln(c.writer, "Tap your YubiKey") + return trace.Wrap(err) +} + +func (c *cliPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) { + var pinAndPUK = &PINAndPUK{} + for { + fmt.Fprintf(c.writer, "Please set a new 6-8 character PIN.\n") + newPIN, err := prompt.Password(ctx, c.writer, c.reader, "Enter your new YubiKey PIV PIN") + if err != nil { + return nil, trace.Wrap(err) + } + newPINConfirm, err := prompt.Password(ctx, c.writer, c.reader, "Confirm your new YubiKey PIV PIN") + if err != nil { + return nil, trace.Wrap(err) + } + + if newPIN != newPINConfirm { + fmt.Fprintf(c.writer, "PINs do not match.\n") + continue + } + + if newPIN == DefaultPIN { + fmt.Fprintf(c.writer, "The default PIN %q is not supported.\n", DefaultPIN) + continue + } + + if !isPINLengthValid(newPIN) { + fmt.Fprintf(c.writer, "PIN must be 6-8 characters long.\n") + continue + } + + pinAndPUK.PIN = newPIN + break + } + + puk, err := prompt.Password(ctx, c.writer, c.reader, "Enter your YubiKey PIV PUK to reset PIN [blank to use default PUK]") + if err != nil { + return nil, trace.Wrap(err) + } + pinAndPUK.PUK = puk + + switch puk { + case DefaultPUK: + fmt.Fprintf(c.writer, "The default PUK %q is not supported.\n", DefaultPUK) + fallthrough + case "": + for { + fmt.Fprintf(c.writer, "Please set a new 6-8 character PUK (used to reset PIN).\n") + newPUK, err := prompt.Password(ctx, c.writer, c.reader, "Enter your new YubiKey PIV PUK") + if err != nil { + return nil, trace.Wrap(err) + } + newPUKConfirm, err := prompt.Password(ctx, c.writer, c.reader, "Confirm your new YubiKey PIV PUK") + if err != nil { + return nil, trace.Wrap(err) + } + + if newPUK != newPUKConfirm { + fmt.Fprintf(c.writer, "PUKs do not match.\n") + continue + } + + if newPUK == DefaultPUK { + fmt.Fprintf(c.writer, "The default PUK %q is not supported.\n", DefaultPUK) + continue + } + + if !isPINLengthValid(newPUK) { + fmt.Fprintf(c.writer, "PUK must be 6-8 characters long.\n") + continue + } + + pinAndPUK.PUK = newPUK + pinAndPUK.PUKChanged = true + break + } + } + return pinAndPUK, nil +} + +func (c *cliPrompt) ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) { + confirmation, err := prompt.Confirmation(ctx, c.writer, c.reader, message) + return confirmation, trace.Wrap(err) +} diff --git a/api/utils/keys/hardwarekey/cliprompt_test.go b/api/utils/keys/hardwarekey/cliprompt_test.go new file mode 100644 index 0000000000000..4c8351ac557c4 --- /dev/null +++ b/api/utils/keys/hardwarekey/cliprompt_test.go @@ -0,0 +1,161 @@ +// 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 ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/prompt" +) + +func TestChangePIN(t *testing.T) { + const validPINPUK = "1234567" + + for _, tc := range []struct { + name string + inputs []string + expectPINAndPUK *hardwarekey.PINAndPUK + expectOutput string + expectError error + }{ + { + name: "no input", + expectError: context.DeadlineExceeded, + }, + { + name: "set pin short", + inputs: []string{ + "123", // pin + "123", // confirm + }, + expectOutput: "PIN must be 6-8 characters long.", + expectError: context.DeadlineExceeded, + }, { + name: "set pin mismatch", + inputs: []string{ + "", // pin + hardwarekey.DefaultPIN, // confirm + }, + expectOutput: "PINs do not match.", + expectError: context.DeadlineExceeded, + }, { + name: "set pin default", + inputs: []string{ + hardwarekey.DefaultPIN, // pin + hardwarekey.DefaultPIN, // confirm + }, + expectOutput: fmt.Sprintf("The default PIN %q is not supported.", hardwarekey.DefaultPIN), + expectError: context.DeadlineExceeded, + }, { + name: "set puk short", + inputs: []string{ + validPINPUK, // pin + validPINPUK, // confirm + "", // empty puk -> trigger set puk + "123", // puk + "123", // confirm + }, + expectOutput: "PUK must be 6-8 characters long.", + expectError: context.DeadlineExceeded, + }, { + name: "set puk mismatch", + inputs: []string{ + validPINPUK, // pin + validPINPUK, // confirm + "", // empty puk -> trigger set puk + "", // puk + validPINPUK, // confirm + }, + expectOutput: "PUKs do not match.", + expectError: context.DeadlineExceeded, + }, { + name: "set puk default", + inputs: []string{ + validPINPUK, // pin + validPINPUK, // confirm + "", // empty puk -> trigger set puk + hardwarekey.DefaultPUK, // puk + hardwarekey.DefaultPUK, // confirm + }, + expectOutput: fmt.Sprintf("The default PUK %q is not supported.", hardwarekey.DefaultPUK), + expectError: context.DeadlineExceeded, + }, { + name: "set puk from empty", + inputs: []string{ + validPINPUK, // pin + validPINPUK, // confirm + "", // empty puk -> trigger set puk + validPINPUK, // puk + validPINPUK, // confirm + }, + expectPINAndPUK: &hardwarekey.PINAndPUK{ + PIN: validPINPUK, + PUK: validPINPUK, + PUKChanged: true, + }, + }, { + name: "set puk from default", + inputs: []string{ + validPINPUK, // pin + validPINPUK, // confirm + hardwarekey.DefaultPUK, // default puk -> trigger set puk + validPINPUK, // puk + validPINPUK, // confirm + }, + expectPINAndPUK: &hardwarekey.PINAndPUK{ + PIN: validPINPUK, + PUK: validPINPUK, + PUKChanged: true, + }, + }, { + name: "valid pin, valid puk", + inputs: []string{ + validPINPUK, // pin + validPINPUK, // confirm + validPINPUK, // puk + }, + expectPINAndPUK: &hardwarekey.PINAndPUK{ + PIN: validPINPUK, + PUK: validPINPUK, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + promptWriter := bytes.NewBuffer([]byte{}) + promptReader := prompt.NewFakeReader() + prompt := hardwarekey.NewCLIPrompt(promptWriter, promptReader) + + for _, input := range tc.inputs { + promptReader.AddString(input) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + PINAndPUK, err := prompt.ChangePIN(ctx) + require.ErrorIs(t, err, tc.expectError) + require.Equal(t, tc.expectPINAndPUK, PINAndPUK) + }) + } +} diff --git a/api/utils/keys/hardwarekey/prompt.go b/api/utils/keys/hardwarekey/prompt.go new file mode 100644 index 0000000000000..04781f7d6c4fa --- /dev/null +++ b/api/utils/keys/hardwarekey/prompt.go @@ -0,0 +1,96 @@ +// 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" + + "github.com/gravitational/trace" +) + +var ( + 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 prompt 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. + // The requirement tells if the PIN is required or optional. + AskPIN(ctx context.Context, requirement PINPromptRequirement) (string, error) + // Touch prompts the user to touch the hardware key. + Touch(ctx context.Context) 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) + // ConfirmSlotOverwrite asks the user if the slot's private key and certificate can be overridden. + ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) +} + +// PINPromptRequirement specifies whether a PIN is required. +type PINPromptRequirement int + +const ( + // PINOptional allows the user to proceed without entering a PIN. + PINOptional PINPromptRequirement = iota + // PINRequired enforces that a PIN must be entered to proceed. + PINRequired +) + +// PINAndPUK describes a response returned from [Prompt].ChangePIN. +type PINAndPUK struct { + // New PIN set by the user. + PIN string + // PUK used to change the PIN. + // This is a new PUK if it has not been changed (from the default PUK). + PUK string + // PUKChanged is true if the user changed the default PUK. + PUKChanged bool +} + +// Validate the user-provided PIN and PUK. +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/hardwaresigner.go b/api/utils/keys/hardwaresigner.go index 744733807470c..b806717be2401 100644 --- a/api/utils/keys/hardwaresigner.go +++ b/api/utils/keys/hardwaresigner.go @@ -14,13 +14,9 @@ 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" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) // HardwareSigner is a crypto.Signer which can be attested as being backed by a hardware key. @@ -29,7 +25,7 @@ type HardwareSigner interface { crypto.Signer // GetAttestationStatement returns an AttestationStatement for this private key. - GetAttestationStatement() *AttestationStatement + GetAttestationStatement() *hardwarekey.AttestationStatement // GetPrivateKeyPolicy returns the PrivateKeyPolicy supported by this private key. GetPrivateKeyPolicy() PrivateKeyPolicy @@ -37,7 +33,7 @@ type HardwareSigner interface { // GetAttestationStatement returns this key's AttestationStatement. If the key is // not a hardware-backed key, this method returns nil. -func (k *PrivateKey) GetAttestationStatement() *AttestationStatement { +func (k *PrivateKey) GetAttestationStatement() *hardwarekey.AttestationStatement { if attestedPriv, ok := k.Signer.(HardwareSigner); ok { return attestedPriv.GetAttestationStatement() } @@ -53,30 +49,10 @@ func (k *PrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy { 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.Unmarshaler{AllowUnknownFields: true}).Unmarshal(bytes.NewReader(buf), ar.ToProto()) +// IsHardware returns true if [k] is a hardware PIV key. +func (k *PrivateKey) IsHardware() bool { + _, ok := k.Signer.(HardwareSigner) + return ok } // AttestationData is verified attestation data for a public key. 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 1255acb6f7df2..7e9167b6073c7 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -31,6 +31,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" ) @@ -210,14 +211,14 @@ 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 + CustomHardwareKeyPrompt hardwarekey.Prompt } // ParsePrivateKeyOpt applies configuration options. type ParsePrivateKeyOpt func(o *ParsePrivateKeyOptions) // WithCustomPrompt sets a custom hardware key prompt. -func WithCustomPrompt(prompt HardwareKeyPrompt) ParsePrivateKeyOpt { +func WithCustomPrompt(prompt hardwarekey.Prompt) ParsePrivateKeyOpt { return func(o *ParsePrivateKeyOptions) { o.CustomHardwareKeyPrompt = prompt } @@ -316,7 +317,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, customPrompt hardwarekey.Prompt) (*PrivateKey, error) { privPEM, err := os.ReadFile(privFile) if err != nil { return nil, trace.ConvertSystemError(err) @@ -335,7 +336,7 @@ 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) { +func ParseKeyPair(privPEM, marshaledSSHPub []byte, customPrompt hardwarekey.Prompt) (*PrivateKey, error) { priv, err := ParsePrivateKey(privPEM, WithCustomPrompt(customPrompt)) if err != nil { return nil, trace.Wrap(err) diff --git a/api/utils/keys/yubikey.go b/api/utils/keys/yubikey.go index 1c1fe1aea5a82..adf8a9bc032e3 100644 --- a/api/utils/keys/yubikey.go +++ b/api/utils/keys/yubikey.go @@ -42,6 +42,7 @@ import ( "github.com/gravitational/teleport/api" attestation "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" ) @@ -68,9 +69,9 @@ var ( // 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) { +func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy PrivateKeyPolicy, slot PIVSlot, prompt hardwarekey.Prompt) (*PrivateKey, error) { if prompt == nil { - prompt = &cliPrompt{} + prompt = hardwarekey.NewStdCLIPrompt() } cachedKeysMu.Lock() defer cachedKeysMu.Unlock() @@ -244,9 +245,9 @@ type yubiKeyPrivateKeyData struct { SlotKey uint32 `json:"slot_key"` } -func parseYubiKeyPrivateKeyData(keyDataBytes []byte, prompt HardwareKeyPrompt) (*PrivateKey, error) { +func parseYubiKeyPrivateKeyData(keyDataBytes []byte, prompt hardwarekey.Prompt) (*PrivateKey, error) { if prompt == nil { - prompt = &cliPrompt{} + prompt = hardwarekey.NewStdCLIPrompt() } cachedKeysMu.Lock() defer cachedKeysMu.Unlock() @@ -370,7 +371,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, hardwarekey.PINRequired) return pass, trace.Wrap(err) } @@ -484,8 +485,8 @@ func (y *YubiKeyPrivateKey) keyPEM() ([]byte, error) { } // GetAttestationStatement returns an AttestationStatement for this YubiKeyPrivateKey. -func (y *YubiKeyPrivateKey) GetAttestationStatement() *AttestationStatement { - return &AttestationStatement{ +func (y *YubiKeyPrivateKey) GetAttestationStatement() *hardwarekey.AttestationStatement { + return &hardwarekey.AttestationStatement{ AttestationStatement: &attestation.AttestationStatement_YubikeyAttestationStatement{ YubikeyAttestationStatement: &attestation.YubiKeyAttestationStatement{ SlotCert: y.slotCert.Raw, @@ -500,26 +501,6 @@ 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 - } -} - // YubiKey is a specific YubiKey PIV card. type YubiKey struct { // conn is a shared YubiKey PIV connection. @@ -532,10 +513,10 @@ type YubiKey struct { *sharedPIVConnection // serialNumber is the yubiKey's 8 digit serial number. serialNumber uint32 - prompt HardwareKeyPrompt + prompt hardwarekey.Prompt } -func newYubiKey(card string, prompt HardwareKeyPrompt) (*YubiKey, error) { +func newYubiKey(card string, prompt hardwarekey.Prompt) (*YubiKey, error) { y := &YubiKey{ sharedPIVConnection: &sharedPIVConnection{ card: card, @@ -662,7 +643,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, hardwarekey.PINOptional) if err != nil { return trace.Wrap(err) } @@ -877,24 +858,14 @@ 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) { +func (c *sharedPIVConnection) setPINAndPUKFromDefault(ctx context.Context, prompt hardwarekey.Prompt) (string, error) { pinAndPUK, err := prompt.ChangePIN(ctx) 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) { - 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) { - 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 err := pinAndPUK.Validate(); err != nil { + return "", trace.Wrap(err) } if pinAndPUK.PUKChanged { @@ -917,7 +888,7 @@ func isRetryError(err error) bool { // 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) { +func FindYubiKey(serialNumber uint32, prompt hardwarekey.Prompt) (*YubiKey, error) { yubiKeyCards, err := findYubiKeyCards() if err != nil { return nil, trace.Wrap(err) @@ -1019,12 +990,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 5ed36f814580d..f337cf971b2d8 100644 --- a/api/utils/keys/yubikey_common.go +++ b/api/utils/keys/yubikey_common.go @@ -17,46 +17,10 @@ import ( "context" "github.com/gravitational/trace" -) - -// HardwareKeyPrompt provides methods to interact with a YubiKey hardware key. -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) - // Touch prompts the user to touch the hardware key. - Touch(ctx context.Context) 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) - // ConfirmSlotOverwrite asks the user if the slot's private key and certificate can be overridden. - ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) -} -// PINPromptRequirement specifies whether a PIN is required. -type PINPromptRequirement int - -const ( - // PINOptional allows the user to proceed without entering a PIN. - PINOptional PINPromptRequirement = iota - // PINRequired enforces that a PIN must be entered to proceed. - PINRequired + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) -// PINAndPUK describes a response returned from HardwareKeyPrompt.ChangePIN. -type PINAndPUK struct { - // New PIN set by the user. - PIN string - // PUK used to change the PIN. - // This is a new PUK if it has not been changed (from the default PUK). - PUK string - // PUKChanged is true if the user changed the default PUK. - 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. @@ -64,7 +28,7 @@ type PINAndPUK struct { // - 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) { +func GetYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot, customPrompt hardwarekey.Prompt) (*PrivateKey, error) { priv, err := getOrGenerateYubiKeyPrivateKey(ctx, policy, slot, customPrompt) if err != nil { return nil, trace.Wrap(err, "failed to get a YubiKey private key") diff --git a/api/utils/keys/yubikey_fake.go b/api/utils/keys/yubikey_fake.go index 33be8917815d5..722ddcd97d587 100644 --- a/api/utils/keys/yubikey_fake.go +++ b/api/utils/keys/yubikey_fake.go @@ -23,12 +23,14 @@ import ( "errors" "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) 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) { +func getOrGenerateYubiKeyPrivateKey(_ context.Context, policy PrivateKeyPolicy, _ PIVSlot, _ hardwarekey.Prompt) (*PrivateKey, error) { _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, trace.Wrap(err) @@ -47,7 +49,7 @@ func getOrGenerateYubiKeyPrivateKey(_ context.Context, policy PrivateKeyPolicy, return NewPrivateKey(signer, keyPEM) } -func parseYubiKeyPrivateKeyData(_ []byte, _ HardwareKeyPrompt) (*PrivateKey, error) { +func parseYubiKeyPrivateKeyData(_ []byte, _ hardwarekey.Prompt) (*PrivateKey, error) { // TODO(Joerger): add custom marshal/unmarshal logic for fakeYubiKeyPrivateKey (if necessary). return nil, trace.Wrap(errPIVUnavailable) } @@ -73,12 +75,3 @@ func (y *fakeYubiKeyPrivateKey) GetAttestationStatement() *AttestationStatement 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 index 77d7de29a2ea8..8e215678a49d8 100644 --- a/api/utils/keys/yubikey_other.go +++ b/api/utils/keys/yubikey_other.go @@ -20,24 +20,20 @@ import ( "errors" "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) var errPIVUnavailable = errors.New("PIV is unavailable in current build") -func getOrGenerateYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot, _ HardwareKeyPrompt) (*PrivateKey, error) { +func getOrGenerateYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot, _ hardwarekey.Prompt) (*PrivateKey, error) { return nil, trace.Wrap(errPIVUnavailable) } -func parseYubiKeyPrivateKeyData(keyDataBytes []byte, _ HardwareKeyPrompt) (*PrivateKey, error) { +func parseYubiKeyPrivateKeyData(keyDataBytes []byte, _ hardwarekey.Prompt) (*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/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/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index 915fa1208a0d9..0d3d622501e7d 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/hardwarekey" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/integration/appaccess" dbhelpers "github.com/gravitational/teleport/integration/db" @@ -248,7 +248,7 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer // db cert has expired. Clock: fakeClock, WebauthnLogin: webauthnLogin, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -882,7 +882,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 { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) diff --git a/integration/teleterm_test.go b/integration/teleterm_test.go index baad6e2871e7e..0c376350d964c 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/hardwarekey" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" dbhelpers "github.com/gravitational/teleport/integration/db" "github.com/gravitational/teleport/integration/helpers" @@ -258,7 +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 { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -293,7 +293,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 { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -378,7 +378,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 { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -433,7 +433,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 { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -504,7 +504,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 { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -766,7 +766,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 { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -885,7 +885,7 @@ func testCreateConnectMyComputerToken(t *testing.T, pack *dbhelpers.DatabasePack InsecureSkipVerify: tc.InsecureSkipVerify, Clock: fakeClock, WebauthnLogin: webauthnLogin, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -948,7 +948,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 { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -1035,7 +1035,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 { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -1265,7 +1265,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 { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) diff --git a/lib/auth/auth.go b/lib/auth/auth.go index f1f52a62c603d..5eb8d6fc76d8b 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" @@ -2216,9 +2217,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 @@ -2461,8 +2462,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 } @@ -3484,7 +3485,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/auth_with_roles.go b/lib/auth/auth_with_roles.go index d429a5108480a..47446073a6128 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" @@ -3399,9 +3399,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/authclient/clt.go b/lib/auth/authclient/clt.go index 906050137a534..dec62f76ee84b 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/github.go b/lib/auth/github.go index eac1519b6680a..96c23c7791948 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/auth/sessions.go b/lib/auth/sessions.go index 7368338b7d587..e4e2417c4d9f9 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/api.go b/lib/client/api.go index af5dd6051800a..ad76428ad28de 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -71,6 +71,7 @@ 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/prompt" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/auth/touchid" @@ -497,7 +498,7 @@ type Config 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 keys.HardwareKeyPrompt + CustomHardwareKeyPrompt hardwarekey.Prompt // DisableSSHResumption disables transparent SSH connection resumption. DisableSSHResumption bool diff --git a/lib/client/client_store.go b/lib/client/client_store.go index 5762f9d7ec8ee..1500b9a48da8b 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" ) @@ -85,7 +86,7 @@ func (s *Store) AddKeyRing(keyRing *KeyRing) error { // SetCustomHardwareKeyPrompt sets a custom hardware key prompt // used to interact with a YubiKey private key. -func (s *Store) SetCustomHardwareKeyPrompt(prompt keys.HardwareKeyPrompt) { +func (s *Store) SetCustomHardwareKeyPrompt(prompt hardwarekey.Prompt) { s.KeyStore.SetCustomHardwareKeyPrompt(prompt) } 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/keystore.go b/lib/client/keystore.go index 43fa9b05c253e..edc728fd1ea2a 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" ) @@ -91,7 +92,7 @@ type KeyStore interface { // SetCustomHardwareKeyPrompt sets a custom hardware key prompt // used to interact with a YubiKey private key. - SetCustomHardwareKeyPrompt(prompt keys.HardwareKeyPrompt) + SetCustomHardwareKeyPrompt(prompt hardwarekey.Prompt) } // FSKeyStore is an on-disk implementation of the KeyStore interface. @@ -106,7 +107,7 @@ type FSKeyStore struct { // 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 + CustomHardwareKeyPrompt hardwarekey.Prompt } // NewFSKeyStore initializes a new FSClientStore. @@ -195,7 +196,7 @@ func (fs *FSKeyStore) kubeCredPath(idx KeyRingIndex, kubename string) string { // SetCustomHardwareKeyPrompt sets a custom hardware key prompt // used to interact with a YubiKey private key. -func (fs *FSKeyStore) SetCustomHardwareKeyPrompt(prompt keys.HardwareKeyPrompt) { +func (fs *FSKeyStore) SetCustomHardwareKeyPrompt(prompt hardwarekey.Prompt) { fs.CustomHardwareKeyPrompt = prompt } @@ -301,7 +302,7 @@ 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, customPrompt hardwarekey.Prompt) (TLSCredential, error) { keyPEM, certPEM, err := readTLSCredentialFiles(keyPath, certPath) if err != nil { return TLSCredential{}, trace.Wrap(err) @@ -610,7 +611,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, customPrompt hardwarekey.Prompt) (map[string]TLSCredential, error) { files, err := os.ReadDir(credentialDir) if err != nil { return nil, trace.ConvertSystemError(err) @@ -658,7 +659,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, customPrompt hardwarekey.Prompt) 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 +673,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, _ hardwarekey.Prompt) error { certPath := keypaths.SSHCertPath(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) cert, err := os.ReadFile(certPath) if err != nil { @@ -696,7 +697,7 @@ 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, _ hardwarekey.Prompt) error { credentialDir := keypaths.KubeCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) credsByName, err := getKubeCredentialsByName(credentialDir) if err != nil { @@ -722,7 +723,7 @@ 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, customPrompt hardwarekey.Prompt) error { credentialDir := keypaths.DatabaseCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) credsByName, err := getCredentialsByName(credentialDir, customPrompt) if err != nil { @@ -754,7 +755,7 @@ 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, customPrompt hardwarekey.Prompt) error { credentialDir := keypaths.AppCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) credsByName, err := getCredentialsByName(credentialDir, customPrompt) if err != nil { @@ -925,4 +926,4 @@ func (ms *MemKeyStore) GetSSHCertificates(proxyHost, username string) ([]*ssh.Ce // SetCustomHardwareKeyPrompt implements the KeyStore.SetCustomHardwareKeyPrompt interface. // Does nothing. -func (ms *MemKeyStore) SetCustomHardwareKeyPrompt(_ keys.HardwareKeyPrompt) {} +func (ms *MemKeyStore) SetCustomHardwareKeyPrompt(_ hardwarekey.Prompt) {} 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/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/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/lib/teleterm/clusters/config.go b/lib/teleterm/clusters/config.go index ff94f4fdb533a..6c5b5ee02e4e4 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" "github.com/gravitational/teleport/lib/teleterm/api/uri" ) @@ -47,7 +47,7 @@ type Config struct { AddKeysToAgent string // CustomHardwareKeyPrompt is a custom hardware key prompt to use when asking // for a hardware key PIN, touch, etc. - HardwareKeyPromptConstructor func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt + HardwareKeyPromptConstructor func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt } // 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 2e7cd9a70d4de..e9f9daffdc728 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/hardwarekey" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" @@ -348,7 +348,7 @@ func TestUpdateTshdEventsServerAddress(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: homeDir, InsecureSkipVerify: true, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -385,7 +385,7 @@ func TestUpdateTshdEventsServerAddress_CredsErr(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: homeDir, InsecureSkipVerify: true, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -489,7 +489,7 @@ func TestRetryWithRelogin(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) @@ -545,7 +545,7 @@ func TestConcurrentHeadlessAuthPrompts(t *testing.T) { storage, err := clusters.NewStorage(clusters.Config{ Dir: t.TempDir(), InsecureSkipVerify: true, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return nil }, }) diff --git a/lib/teleterm/daemon/hardwarekeyprompt.go b/lib/teleterm/daemon/hardwarekeyprompt.go index ad75ccf6f699b..6a70b25f7659e 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" "github.com/gravitational/teleport/lib/teleterm/api/uri" ) @@ -46,7 +46,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) NewHardwareKeyPromptConstructor(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { +func (s *Service) NewHardwareKeyPromptConstructor(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return &hardwareKeyPrompter{s: s, rootClusterURI: rootClusterURI} } @@ -67,10 +67,10 @@ 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 hardwarekey.PINPromptRequirement) (string, error) { res, err := h.s.tshdEventsClient.PromptHardwareKeyPIN(ctx, &api.PromptHardwareKeyPINRequest{ RootClusterUri: h.rootClusterURI.String(), - PinOptional: requirement == keys.PINOptional, + PinOptional: requirement == hardwarekey.PINOptional, }) if err != nil { return "", trace.Wrap(err) @@ -81,14 +81,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) (*keys.PINAndPUK, error) { +func (h *hardwareKeyPrompter) ChangePIN(ctx context.Context) (*hardwarekey.PINAndPUK, error) { res, err := h.s.tshdEventsClient.PromptHardwareKeyPINChange(ctx, &api.PromptHardwareKeyPINChangeRequest{ RootClusterUri: h.rootClusterURI.String(), }) if err != nil { return nil, trace.Wrap(err) } - return &keys.PINAndPUK{ + return &hardwarekey.PINAndPUK{ PIN: res.Pin, PUK: res.Puk, PUKChanged: res.PukChanged, diff --git a/lib/teleterm/teleterm.go b/lib/teleterm/teleterm.go index 0c6cde8efd031..8c6b2a897f3e5 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/hardwarekey" "github.com/gravitational/teleport/lib/teleterm/api/uri" "github.com/gravitational/teleport/lib/teleterm/apiserver" "github.com/gravitational/teleport/lib/teleterm/clusteridcache" @@ -42,7 +42,7 @@ import ( // Serve starts daemon service func Serve(ctx context.Context, cfg Config) error { - var hardwareKeyPromptConstructor func(clusterURI uri.ResourceURI) keys.HardwareKeyPrompt + var hardwareKeyPromptConstructor func(clusterURI uri.ResourceURI) hardwarekey.Prompt if err := cfg.CheckAndSetDefaults(); err != nil { return trace.Wrap(err) } @@ -59,7 +59,7 @@ func Serve(ctx context.Context, cfg Config) error { Clock: clock, InsecureSkipVerify: cfg.InsecureSkipVerify, AddKeysToAgent: cfg.AddKeysToAgent, - HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) hardwarekey.Prompt { return hardwareKeyPromptConstructor(rootClusterURI) }, }) 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 263f4343760b0..700a0f53ea1b3 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") } From 94d5cabc1624807a14117a8fa05ae6adeaee0624 Mon Sep 17 00:00:00 2001 From: joerger Date: Wed, 2 Apr 2025 11:01:13 -0700 Subject: [PATCH 2/4] Tidy up PIVSlot logic and remove its piv-go dependency. --- api/client/webclient/webclient.go | 3 +- api/profile/profile.go | 3 +- api/types/authentication.go | 9 +-- api/utils/keys/hardwarekey/slot.go | 74 +++++++++++++++++++++++ api/utils/keys/hardwarekey/slot_test.go | 68 +++++++++++++++++++++ api/utils/keys/policy.go | 10 ++++ api/utils/keys/yubikey.go | 78 +++++++------------------ api/utils/keys/yubikey_common.go | 10 +--- api/utils/keys/yubikey_fake.go | 6 +- api/utils/keys/yubikey_other.go | 6 +- api/utils/keys/yubikey_test.go | 12 ++-- lib/client/api.go | 2 +- lib/config/fileconf.go | 6 +- tool/tsh/common/tsh.go | 4 +- 14 files changed, 197 insertions(+), 94 deletions(-) create mode 100644 api/utils/keys/hardwarekey/slot.go create mode 100644 api/utils/keys/hardwarekey/slot_test.go diff --git a/api/client/webclient/webclient.go b/api/client/webclient/webclient.go index 78d4c80c9aebc..a7869607735ab 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.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 e652fe0153ac8..9b4efc7f8688a 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.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/types/authentication.go b/api/types/authentication.go index b5872f4651b51..3ac711f5e6ea2 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.PIVSlotKeyString // 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.PIVSlotKeyString { if hk, err := c.GetHardwareKey(); err == nil { - return keys.PIVSlot(hk.PIVSlot) + return hardwarekey.PIVSlotKeyString(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.PIVSlotKeyString(hk.PIVSlot).Validate(); err != nil { return trace.Wrap(err) } } diff --git a/api/utils/keys/hardwarekey/slot.go b/api/utils/keys/hardwarekey/slot.go new file mode 100644 index 0000000000000..52e2e9eac8a74 --- /dev/null +++ b/api/utils/keys/hardwarekey/slot.go @@ -0,0 +1,74 @@ +// 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 +) + +// 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 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/policy.go b/api/utils/keys/policy.go index 60ab361559261..dc699170cd0ea 100644 --- a/api/utils/keys/policy.go +++ b/api/utils/keys/policy.go @@ -17,6 +17,8 @@ import ( "regexp" "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) // PrivateKeyPolicy is a requirement for client private key storage. @@ -189,3 +191,11 @@ func IsPrivateKeyPolicyError(err error) bool { } return privateKeyPolicyErrRegex.MatchString(err.Error()) } + +// GetPromptPolicy returns this corresponding [hardwarekey.PromptPolicy]. +func (p PrivateKeyPolicy) GetPromptPolicy() hardwarekey.PromptPolicy { + return hardwarekey.PromptPolicy{ + TouchRequired: p.isHardwareKeyTouchVerified(), + PINRequired: p.isHardwareKeyPINVerified(), + } +} diff --git a/api/utils/keys/yubikey.go b/api/utils/keys/yubikey.go index adf8a9bc032e3..811eeebc75374 100644 --- a/api/utils/keys/yubikey.go +++ b/api/utils/keys/yubikey.go @@ -32,7 +32,6 @@ import ( "io" "math/big" "os" - "strconv" "strings" "sync" "time" @@ -69,23 +68,30 @@ var ( // 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 hardwarekey.Prompt) (*PrivateKey, error) { +func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy PrivateKeyPolicy, slot hardwarekey.PIVSlotKeyString, prompt hardwarekey.Prompt) (*PrivateKey, error) { if prompt == nil { prompt = hardwarekey.NewStdCLIPrompt() } cachedKeysMu.Lock() defer cachedKeysMu.Unlock() - // Get the default PIV slot or the piv slot requested. - pivSlot, err := GetDefaultKeySlot(requiredKeyPolicy) + promptPolicy := requiredKeyPolicy.GetPromptPolicy() + + // Get the requested or default PIV slot. + var slotKey hardwarekey.PIVSlotKey + var err error + if slot != "" { + slotKey, err = slot.Parse() + } else { + slotKey, err = hardwarekey.GetDefaultSlotKey(promptPolicy) + } if err != nil { return nil, trace.Wrap(err) } - if slot != "" { - pivSlot, err = slot.parse() - if err != nil { - return nil, trace.Wrap(err) - } + + pivSlot, err := parsePIVSlot(slotKey) + if err != nil { + return nil, trace.Wrap(err) } // If the program has already retrieved and cached this key, return it. @@ -119,11 +125,6 @@ func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy Priva // 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): @@ -166,25 +167,6 @@ func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy Priva 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: @@ -241,8 +223,8 @@ type YubiKeyPrivateKey struct { // 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"` + SerialNumber uint32 `json:"serial_number"` + SlotKey hardwarekey.PIVSlotKey `json:"slot_key"` } func parseYubiKeyPrivateKeyData(keyDataBytes []byte, prompt hardwarekey.Prompt) (*PrivateKey, error) { @@ -471,7 +453,7 @@ func (y *YubiKeyPrivateKey) toPrivateKey() (*PrivateKey, error) { func (y *YubiKeyPrivateKey) keyPEM() ([]byte, error) { keyDataBytes, err := json.Marshal(yubiKeyPrivateKeyData{ SerialNumber: y.serialNumber, - SlotKey: y.pivSlot.Key, + SlotKey: hardwarekey.PIVSlotKey(y.pivSlot.Key), }) if err != nil { return nil, trace.Wrap(err) @@ -932,22 +914,8 @@ func findYubiKeyCards() ([]string, error) { return yubiKeyCards, nil } -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 { +func parsePIVSlot(slotKey hardwarekey.PIVSlotKey) (piv.Slot, error) { + switch uint32(slotKey) { case piv.SlotAuthentication.Key: return piv.SlotAuthentication, nil case piv.SlotSignature.Key: @@ -957,11 +925,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_common.go b/api/utils/keys/yubikey_common.go index f337cf971b2d8..d7c4616bb6a30 100644 --- a/api/utils/keys/yubikey_common.go +++ b/api/utils/keys/yubikey_common.go @@ -28,18 +28,10 @@ import ( // - hardware_key_touch: 9c // - hardware_key_pin: 9d // - hardware_key_touch_pin: 9e -func GetYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot, customPrompt hardwarekey.Prompt) (*PrivateKey, error) { +func GetYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot hardwarekey.PIVSlotKeyString, customPrompt hardwarekey.Prompt) (*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 - -// Validate that the PIV slot is a valid value. -func (s PIVSlot) Validate() error { - return trace.Wrap(s.validate()) -} diff --git a/api/utils/keys/yubikey_fake.go b/api/utils/keys/yubikey_fake.go index 722ddcd97d587..f6599a3428f4f 100644 --- a/api/utils/keys/yubikey_fake.go +++ b/api/utils/keys/yubikey_fake.go @@ -30,7 +30,7 @@ import ( var errPIVUnavailable = errors.New("PIV is unavailable in current build") // Return a fake YubiKey private key. -func getOrGenerateYubiKeyPrivateKey(_ context.Context, policy PrivateKeyPolicy, _ PIVSlot, _ hardwarekey.Prompt) (*PrivateKey, error) { +func getOrGenerateYubiKeyPrivateKey(_ context.Context, policy PrivateKeyPolicy, _ hardwarekey.PIVSlotKeyString, _ hardwarekey.Prompt) (*PrivateKey, error) { _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, trace.Wrap(err) @@ -54,10 +54,6 @@ func parseYubiKeyPrivateKeyData(_ []byte, _ hardwarekey.Prompt) (*PrivateKey, er return nil, trace.Wrap(errPIVUnavailable) } -func (s PIVSlot) validate() error { - return trace.Wrap(errPIVUnavailable) -} - type fakeYubiKeyPrivateKey struct { crypto.Signer privateKeyPolicy PrivateKeyPolicy diff --git a/api/utils/keys/yubikey_other.go b/api/utils/keys/yubikey_other.go index 8e215678a49d8..61c38bb71f077 100644 --- a/api/utils/keys/yubikey_other.go +++ b/api/utils/keys/yubikey_other.go @@ -26,14 +26,10 @@ import ( var errPIVUnavailable = errors.New("PIV is unavailable in current build") -func getOrGenerateYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot, _ hardwarekey.Prompt) (*PrivateKey, error) { +func getOrGenerateYubiKeyPrivateKey(ctx context.Context, _ PrivateKeyPolicy, _ hardwarekey.PIVSlotKeyString, _ hardwarekey.Prompt) (*PrivateKey, error) { return nil, trace.Wrap(errPIVUnavailable) } func parseYubiKeyPrivateKeyData(keyDataBytes []byte, _ hardwarekey.Prompt) (*PrivateKey, error) { return nil, trace.Wrap(errPIVUnavailable) } - -func (s PIVSlot) validate() error { - return trace.Wrap(errPIVUnavailable) -} diff --git a/api/utils/keys/yubikey_test.go b/api/utils/keys/yubikey_test.go index 72ac01537041c..b06d8a3b3508e 100644 --- a/api/utils/keys/yubikey_test.go +++ b/api/utils/keys/yubikey_test.go @@ -28,6 +28,7 @@ import ( "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/prompt" ) @@ -63,7 +64,7 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { resetYubikey(t, y) setupPINPrompt(t, y) - var slot keys.PIVSlot = "" + var slot hardwarekey.PIVSlotKeyString = "" if customSlot { slot = "9a" } @@ -110,9 +111,8 @@ func TestOverwritePrompt(t *testing.T) { 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 := piv.SlotSignature testOverwritePrompt := func(t *testing.T) { // Fail to overwrite slot when user denies @@ -130,7 +130,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) @@ -140,7 +140,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.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKey, hardwarekey.PIVSlotKeyString(touchSlot.String()), nil) require.NoError(t, err) testOverwritePrompt(t) diff --git a/lib/client/api.go b/lib/client/api.go index ad76428ad28de..c7060ebf46ad7 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -448,7 +448,7 @@ type Config struct { PrivateKeyPolicy keys.PrivateKeyPolicy // PIVSlot specifies a specific PIV slot to use with hardware key support. - PIVSlot keys.PIVSlot + PIVSlot hardwarekey.PIVSlotKeyString // LoadAllCAs indicates that tsh should load the CAs of all clusters // instead of just the current cluster. diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index 0348df9fe8896..c66791b07a87d 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.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 keys.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/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 84f17512a45b2..9522b6fc81ee6 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -67,7 +67,7 @@ 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/prompt" "github.com/gravitational/teleport/lib/asciitable" "github.com/gravitational/teleport/lib/auth/authclient" @@ -4385,7 +4385,7 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err } if cf.PIVSlot != "" { - c.PIVSlot = keys.PIVSlot(cf.PIVSlot) + c.PIVSlot = hardwarekey.PIVSlotKeyString(cf.PIVSlot) if err = c.PIVSlot.Validate(); err != nil { return nil, trace.Wrap(err) } From 0f7e4aa2abd994ac5bf0c57f4fb238376a92f39f Mon Sep 17 00:00:00 2001 From: joerger Date: Fri, 4 Apr 2025 11:26:07 -0700 Subject: [PATCH 3/4] Add godoc comments to cliPrompt methods; Update cliprompt.go license year. --- api/utils/keys/hardwarekey/cliprompt.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api/utils/keys/hardwarekey/cliprompt.go b/api/utils/keys/hardwarekey/cliprompt.go index 83ee48a030bc1..1a21e5bcc9674 100644 --- a/api/utils/keys/hardwarekey/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. @@ -56,6 +56,8 @@ func NewCLIPrompt(w io.Writer, r prompt.StdinReader) *cliPrompt { } } +// AskPIN prompts the user for a PIN. If the requirement is [PINOptional], +// the prompt will offer the default PIN as a default value. func (c *cliPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement) (string, error) { message := "Enter your YubiKey PIV PIN" if requirement == PINOptional { @@ -65,11 +67,16 @@ func (c *cliPrompt) AskPIN(ctx context.Context, requirement PINPromptRequirement return password, trace.Wrap(err) } +// Touch prompts the user to touch the hardware key. func (c *cliPrompt) Touch(_ context.Context) error { _, err := fmt.Fprintln(c.writer, "Tap your YubiKey") return trace.Wrap(err) } +// ChangePIN asks for a new PIN and the current PUK to change to the new PIN. +// If the provided PUK is the default value, it will ask for a new PUK as well. +// If an invalid PIN or PUK is provided, the user will be re-prompted until a +// valid value is provided. func (c *cliPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) { var pinAndPUK = &PINAndPUK{} for { @@ -147,6 +154,7 @@ func (c *cliPrompt) ChangePIN(ctx context.Context) (*PINAndPUK, error) { return pinAndPUK, nil } +// ConfirmSlotOverwrite asks the user if the slot's private key and certificate can be overridden. func (c *cliPrompt) ConfirmSlotOverwrite(ctx context.Context, message string) (bool, error) { confirmation, err := prompt.Confirmation(ctx, c.writer, c.reader, message) return confirmation, trace.Wrap(err) From 1571b5cf936a0bc51f9109d4a9dcff24f498c4ca Mon Sep 17 00:00:00 2001 From: joerger Date: Mon, 7 Apr 2025 11:08:26 -0700 Subject: [PATCH 4/4] Add godocs. --- api/utils/keys/alias.go | 3 +++ api/utils/keys/hardwarekey/prompt.go | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/api/utils/keys/alias.go b/api/utils/keys/alias.go index e8e9863c6a733..8323a9c9e53ff 100644 --- a/api/utils/keys/alias.go +++ b/api/utils/keys/alias.go @@ -18,6 +18,9 @@ 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. +// AttestationStatement is an attestation statement for a hardware private key +// that supports json marshaling through the standard json/encoding package. type AttestationStatement = hardwarekey.AttestationStatement +// AttestationStatementFromProto converts an AttestationStatement from its protobuf form. var AttestationStatementFromProto = hardwarekey.AttestationStatementFromProto diff --git a/api/utils/keys/hardwarekey/prompt.go b/api/utils/keys/hardwarekey/prompt.go index 04781f7d6c4fa..986a0c9311dc1 100644 --- a/api/utils/keys/hardwarekey/prompt.go +++ b/api/utils/keys/hardwarekey/prompt.go @@ -21,9 +21,13 @@ import ( ) var ( - PromptPolicyNone = PromptPolicy{TouchRequired: false, PINRequired: false} - PromptPolicyTouch = PromptPolicy{TouchRequired: true, PINRequired: false} - PromptPolicyPIN = PromptPolicy{TouchRequired: false, PINRequired: true} + // PromptPolicyNone does not require touch or pin. + PromptPolicyNone = PromptPolicy{TouchRequired: false, PINRequired: false} + // PromptPolicyTouch requires touch. + PromptPolicyTouch = PromptPolicy{TouchRequired: true, PINRequired: false} + // PromptPolicyPIN requires pin. + PromptPolicyPIN = PromptPolicy{TouchRequired: false, PINRequired: true} + // PromptPolicyTouchAndPIN requires touch and pin. PromptPolicyTouchAndPIN = PromptPolicy{TouchRequired: true, PINRequired: true} )