Skip to content

Commit

Permalink
Configure custom PIV slot for hardware key support (#31732)
Browse files Browse the repository at this point in the history
* Update RFD.

* Add custom PIV slot logic.

* Add custom piv slot to cluster auth preference.

* Fix error handling of parsing private key policy errors.

* Add new PIVSlot string type.
  • Loading branch information
Joerger committed Oct 14, 2023
1 parent 0e744e2 commit e36f7d9
Show file tree
Hide file tree
Showing 15 changed files with 1,807 additions and 1,603 deletions.
2 changes: 2 additions & 0 deletions api/client/webclient/webclient.go
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
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
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
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
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
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"`)
}

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
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, "")
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
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)
}

0 comments on commit e36f7d9

Please sign in to comment.