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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/client/webclient/webclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ type AuthenticationSettings struct {
Github *GithubSettings `json:"github,omitempty"`
// 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"`
// DeviceTrustDisabled provides a clue to Teleport clients on whether to avoid
// device authentication.
// Deprecated: Use DeviceTrust.Disabled instead.
Expand Down
7 changes: 5 additions & 2 deletions api/profile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ type Profile struct {

// PrivateKeyPolicy is a key policy enforced for this profile.
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"`
}

// Copy returns a shallow copy of p, or nil if p is nil.
Expand Down Expand Up @@ -241,7 +244,7 @@ func SetCurrentProfileName(dir string, name string) error {
}

path := keypaths.CurrentProfileFilePath(dir)
if err := os.WriteFile(path, []byte(strings.TrimSpace(name)+"\n"), 0660); err != nil {
if err := os.WriteFile(path, []byte(strings.TrimSpace(name)+"\n"), 0o660); err != nil {
return trace.Wrap(err)
}
return nil
Expand Down Expand Up @@ -392,7 +395,7 @@ func (p *Profile) saveToFile(filepath string) error {
if err != nil {
return trace.Wrap(err)
}
if err = os.WriteFile(filepath, bytes, 0660); err != nil {
if err = os.WriteFile(filepath, bytes, 0o660); err != nil {
return trace.Wrap(err)
}
return nil
Expand Down
4 changes: 4 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1818,6 +1818,10 @@ message AuthPreferenceSpecV2 {
// Okta is a set of options related to the Okta service in Teleport.
// Requires Teleport Enterprise.
OktaOptions Okta = 17 [(gogoproto.jsontag) = "okta,omitempty"];

// PIVSlot is a PIV slot that Teleport clients should use instead of the
// default based on private key policy. For example, "9a" or "9e".
string PIVSlot = 18 [(gogoproto.jsontag) = "piv_slot,omitempty"];
}

// U2F defines settings for U2F device.
Expand Down
7 changes: 7 additions & 0 deletions api/types/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ type AuthPreference interface {
GetRequireMFAType() RequireMFAType
// GetPrivateKeyPolicy returns the configured private key policy for the cluster.
GetPrivateKeyPolicy() keys.PrivateKeyPolicy
// GetPIVSlot returns the configured piv slot for the cluster.
GetPIVSlot() keys.PIVSlot

// GetDisconnectExpiredCert returns disconnect expired certificate setting
GetDisconnectExpiredCert() bool
Expand Down Expand Up @@ -392,6 +394,11 @@ func (c *AuthPreferenceV2) GetPrivateKeyPolicy() keys.PrivateKeyPolicy {
}
}

// GetPIVSlot returns the configured piv slot for the cluster.
func (c *AuthPreferenceV2) GetPIVSlot() keys.PIVSlot {
return keys.PIVSlot(c.Spec.PIVSlot)
}

// GetDisconnectExpiredCert returns disconnect expired certificate setting
func (c *AuthPreferenceV2) GetDisconnectExpiredCert() bool {
return c.Spec.DisconnectExpiredCert.Value
Expand Down
2,990 changes: 1,520 additions & 1,470 deletions api/types/types.pb.go

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions api/utils/keys/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ func (p PrivateKeyPolicy) VerifyPolicy(policy PrivateKeyPolicy) error {
return NewPrivateKeyPolicyError(p)
}

// IsHardwareKeyVerified return true if this private key policy requires a hardware key.
func (p PrivateKeyPolicy) IsHardwareKeyVerified() bool {
switch p {
case PrivateKeyPolicyHardwareKey, PrivateKeyPolicyHardwareKeyTouch:
return true
}
return false
}

// MFAVerified checks that meet this private key policy counts towards MFA verification.
func (p PrivateKeyPolicy) MFAVerified() bool {
return p == PrivateKeyPolicyHardwareKeyTouch
Expand Down
102 changes: 68 additions & 34 deletions api/utils/keys/yubikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"io"
"math/big"
"os"
"strconv"
"strings"
"sync"
"time"
Expand All @@ -53,27 +54,13 @@ const (
// 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(touchRequired bool) (*PrivateKey, error) {
// TODO(Joerger): Pass ctx through method.
ctx := context.TODO()

func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy PrivateKeyPolicy, slot PIVSlot) (*PrivateKey, error) {
// Use the first yubiKey we find.
y, err := FindYubiKey(0)
if err != nil {
return nil, trace.Wrap(err)
}

requiredKeyPolicy := PrivateKeyPolicyHardwareKey
if touchRequired {
requiredKeyPolicy = PrivateKeyPolicyHardwareKeyTouch
}

// Get the correct PIV slot and Touch policy for the given touch requirement.
pivSlot, err := GetDefaultKeySlot(requiredKeyPolicy)
if err != nil {
return nil, trace.Wrap(err)
}

promptOverwriteSlot := func(msg string) error {
promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg)
if confirmed, confirmErr := prompt.Confirmation(ctx, os.Stderr, prompt.Stdin(), promptQuestion); confirmErr != nil {
Expand All @@ -84,25 +71,40 @@ func getOrGenerateYubiKeyPrivateKey(touchRequired bool) (*PrivateKey, error) {
return nil
}

// Check the client certificate in the slot.
switch cert, err := y.getCertificate(pivSlot); {
case err == nil && (len(cert.Subject.Organization) == 0 || cert.Subject.Organization[0] != certOrgName):
// Unknown cert found, prompt the user before we overwrite the slot.
if err := promptOverwriteSlot(nonTeleportCertificateMessage(pivSlot, cert)); err != nil {
// If a specific slot was specified, use that. Otherwise, check for a key in the
// default slot for the given policy and generate a new one if needed.
var pivSlot piv.Slot
if slot != "" {
pivSlot, err = slot.parse()
if err != nil {
return nil, trace.Wrap(err)
}
} else {
pivSlot, err = GetDefaultKeySlot(requiredKeyPolicy)
if err != nil {
return nil, trace.Wrap(err)
}

// user confirmed, generate a new key.
fallthrough
case errors.Is(err, piv.ErrNotFound):
// no cert found, generate a new key.
priv, err := y.GeneratePrivateKey(pivSlot, requiredKeyPolicy)
return priv, trace.Wrap(err)
case err != nil:
return nil, trace.Wrap(err)
// Check the client certificate in the slot.
switch cert, err := y.getCertificate(pivSlot); {
case err == nil && (len(cert.Subject.Organization) == 0 || cert.Subject.Organization[0] != certOrgName):
// Unknown cert found, prompt the user before we overwrite the slot.
if err := promptOverwriteSlot(nonTeleportCertificateMessage(pivSlot, cert)); err != nil {
return nil, trace.Wrap(err)
}

// user confirmed, generate a new key.
fallthrough
case errors.Is(err, piv.ErrNotFound):
// no cert found, generate a new key.
priv, err := y.generatePrivateKeyAndCert(pivSlot, requiredKeyPolicy)
return priv, trace.Wrap(err)
case err != nil:
return nil, trace.Wrap(err)
}
}

// If a key was not generated during the cert check, then we found a teleport client cert. Get the key in the slot.
// Get the key in the slot, or generate a new one if needed.
priv, err := y.getPrivateKey(pivSlot)
switch {
case err == nil && requiredKeyPolicy.VerifyPolicy(priv.GetPrivateKeyPolicy()) != nil:
Expand All @@ -116,7 +118,7 @@ func getOrGenerateYubiKeyPrivateKey(touchRequired bool) (*PrivateKey, error) {
fallthrough
case trace.IsNotFound(err):
// no key found, generate a new key.
priv, err := y.GeneratePrivateKey(pivSlot, requiredKeyPolicy)
priv, err := y.generatePrivateKeyAndCert(pivSlot, requiredKeyPolicy)
return priv, trace.Wrap(err)
case err != nil:
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -363,9 +365,9 @@ func (y *YubiKey) Reset() error {
return trace.Wrap(err)
}

// GeneratePrivateKey generates a new private key from the given PIV slot with the given PIV policies.
func (y *YubiKey) GeneratePrivateKey(slot piv.Slot, requiredKeyPolicy PrivateKeyPolicy) (*PrivateKey, error) {
if err := y.generateYubiKeyPrivateKey(slot, requiredKeyPolicy); err != nil {
// generatePrivateKeyAndCert generates a new private key and client metadata cert in the given PIV slot.
func (y *YubiKey) generatePrivateKeyAndCert(slot piv.Slot, requiredKeyPolicy PrivateKeyPolicy) (*PrivateKey, error) {
if err := y.generatePrivateKey(slot, requiredKeyPolicy); err != nil {
return nil, trace.Wrap(err)
}

Expand Down Expand Up @@ -399,6 +401,7 @@ func (y *YubiKey) SetMetadataCertificate(slot piv.Slot, subject pkix.Name) error
return trace.Wrap(err)
}

// getCertificate gets a certificate from the given PIV slot.
func (y *YubiKey) getCertificate(slot piv.Slot) (*x509.Certificate, error) {
yk, err := y.open()
if err != nil {
Expand All @@ -410,7 +413,8 @@ func (y *YubiKey) getCertificate(slot piv.Slot) (*x509.Certificate, error) {
return cert, trace.Wrap(err)
}

func (y *YubiKey) generateYubiKeyPrivateKey(slot piv.Slot, requiredKeyPolicy PrivateKeyPolicy) error {
// generatePrivateKey generates a new private key in the given PIV slot.
func (y *YubiKey) generatePrivateKey(slot piv.Slot, requiredKeyPolicy PrivateKeyPolicy) error {
yk, err := y.open()
if err != nil {
return trace.Wrap(err)
Expand Down Expand Up @@ -457,6 +461,13 @@ func (y *YubiKey) getPrivateKey(slot piv.Slot) (*PrivateKey, error) {
return nil, trace.Wrap(err)
}

// We don't yet support pin policies so we must return a user readable error in case
// they passed a mis-configured slot. Otherwise they will get a PIV auth error during signing.
// TODO(Joerger): remove this check once PIN prompt is supported.
if attestation.PINPolicy != piv.PINPolicyNever {
return nil, trace.NotImplemented(`PIN policy is not currently supported. Please generate a key with PIN policy "never"`)
Comment thread
Joerger marked this conversation as resolved.
}

priv := &YubiKeyPrivateKey{
YubiKey: y,
pivSlot: slot,
Expand Down Expand Up @@ -570,6 +581,29 @@ 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 parsePIVSlotString(slotKeyString string) (piv.Slot, error) {
slotKey, err := strconv.ParseUint(slotKeyString, 16, 32)
if err != nil {
return piv.Slot{}, trace.Wrap(err)
}

return parsePIVSlot(uint32(slotKey))
}

func parsePIVSlot(slotKey uint32) (piv.Slot, error) {
switch slotKey {
case piv.SlotAuthentication.Key:
Expand Down
32 changes: 31 additions & 1 deletion api/utils/keys/yubikey_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,43 @@ limitations under the License.
package keys

import (
"context"

"github.com/gravitational/trace"
)

// GetYubiKeyPrivateKey attempt to retrieve a YubiKey private key matching the given hardware key policy
// from the given slot. If slot is unspecified, the default slot for the given key policy will be used.
// If the slot is empty, a new private key matching the given policy will be generated in the slot.
// - hardware_key: 9a
// - hardware_key_touch: 9c
func GetYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot) (*PrivateKey, error) {
priv, err := getOrGenerateYubiKeyPrivateKey(ctx, policy, slot)
if err != nil {
return nil, trace.Wrap(err, "failed to get a YubiKey private key")
}
return priv, nil
}

// TODO(Joerger): Deprecated in favor of GetYubiKeyPrivateKey.
// Delete once all references in /e are removed
func GetOrGenerateYubiKeyPrivateKey(touchRequired bool) (*PrivateKey, error) {
priv, err := getOrGenerateYubiKeyPrivateKey(touchRequired)
policy := PrivateKeyPolicyHardwareKey
if touchRequired {
policy = PrivateKeyPolicyHardwareKeyTouch
}

priv, err := getOrGenerateYubiKeyPrivateKey(context.TODO(), policy, "")
Comment thread
Joerger marked this conversation as resolved.
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())
}
7 changes: 6 additions & 1 deletion api/utils/keys/yubikey_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@ limitations under the License.
package keys

import (
"context"
"errors"

"github.com/gravitational/trace"
)

var errPIVUnavailable = errors.New("PIV is unavailable in current build")

func getOrGenerateYubiKeyPrivateKey(touchRequired bool) (*PrivateKey, error) {
func getOrGenerateYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot) (*PrivateKey, error) {
return nil, trace.Wrap(errPIVUnavailable)
}

func parseYubiKeyPrivateKeyData(keyDataBytes []byte) (*PrivateKey, error) {
return nil, trace.Wrap(errPIVUnavailable)
}

func (s PIVSlot) validate() error {
return trace.Wrap(errPIVUnavailable)
}
Loading