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
6 changes: 6 additions & 0 deletions api/utils/keys/hardwarekey/hardwarekey.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ type ContextualKeyInfo struct {
Username string
// ClusterName is a Teleport cluster name that the key is associated with.
ClusterName string
// AgentKey specifies whether this key is being utilized through an agent.
// The hardware key service may impose additional restrictions in this case,
// such as checking that the PIV slot certificate matches the Teleport client
// metadata certificate format, to ensure the agent doesn't provide access to
// non teleport client PIV keys.
AgentKey bool
}

// SignatureAlgorithm is a signature key algorithm option.
Expand Down
3 changes: 3 additions & 0 deletions api/utils/keys/hardwarekeyagent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"log/slog"
"net"
"os"

Expand Down Expand Up @@ -100,6 +101,7 @@ func (s *agentService) Sign(ctx context.Context, req *hardwarekeyagentv1.SignReq
ProxyHost: req.KeyInfo.ProxyHost,
Username: req.KeyInfo.Username,
ClusterName: req.KeyInfo.ClusterName,
AgentKey: true,
}

var signerOpts crypto.SignerOpts
Expand All @@ -123,6 +125,7 @@ func (s *agentService) Sign(ctx context.Context, req *hardwarekeyagentv1.SignReq

signature, err := s.s.Sign(ctx, keyRef, keyInfo, rand.Reader, req.Digest, signerOpts)
if err != nil {
slog.DebugContext(ctx, "hardware key agent signature failed", "error", err)
return nil, trace.Wrap(err)
}

Expand Down
30 changes: 1 addition & 29 deletions api/utils/keys/piv/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ package piv
import (
"context"
"crypto"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -141,7 +138,7 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P

// Unknown cert found, this slot could be in use by a non-teleport client.
// Prompt the user before we overwrite the slot.
case len(cert.Subject.Organization) == 0 || cert.Subject.Organization[0] != certOrgName:
case !isTeleportMetadataCertificate(cert):
if err := s.promptOverwriteSlot(ctx, nonTeleportCertificateMessage(pivSlot, cert), config.ContextualKeyInfo); err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -310,28 +307,3 @@ func (s *YubiKeyService) promptOverwriteSlot(ctx context.Context, msg string, ke
}
return nil
}

func nonTeleportCertificateMessage(slot piv.Slot, cert *x509.Certificate) string {
// Gather a small list of user-readable x509 certificate fields to display to the user.
sum := sha256.Sum256(cert.Raw)
fingerPrint := hex.EncodeToString(sum[:])
return fmt.Sprintf(`Certificate in YubiKey PIV slot %q is not a Teleport client cert:
Slot %s:
Algorithm: %v
Subject DN: %v
Issuer DN: %v
Serial: %v
Fingerprint: %v
Not before: %v
Not after: %v
`,
slot, slot,
cert.SignatureAlgorithm,
cert.Subject,
cert.Issuer,
cert.SerialNumber,
fingerPrint,
cert.NotBefore,
cert.NotAfter,
)
}
83 changes: 78 additions & 5 deletions api/utils/keys/piv/yubikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"errors"
"fmt"
"io"
"math/big"
"strings"
Expand Down Expand Up @@ -140,7 +144,52 @@ const (
signTouchPromptDelay = time.Millisecond * 200
)

var (
ErrMissingTeleportCert = trace.BadParameterError{
Message: "hardware key agent cannot perform signatures on PIV slots that aren't configured for Teleport. " +
"The PIV slot should be configured automatically by the Teleport client during login. If you are " +
"are configuring the PIV slot manually, you must also generate a certificate in the slot with " +
"\"teleport\" as the organization name: " +
"e.g. \"ykman piv keys generate -a ECCP256 9a pub.pem && ykman piv certificate generate 9a pub.pem -s O=teleport\"",
}
)

func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, keyInfo hardwarekey.ContextualKeyInfo, prompt hardwarekey.Prompt, rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
pivSlot, err := parsePIVSlot(ref.SlotKey)
if err != nil {
return nil, trace.Wrap(err)
}

// Check that the public key in the slot matches our record.
slotCert, err := y.conn.attest(pivSlot)
if err != nil {
return nil, trace.Wrap(err)
}
type cryptoPublicKeyI interface {
Equal(x crypto.PublicKey) bool
}
if slotPub, ok := slotCert.PublicKey.(cryptoPublicKeyI); !ok {
return nil, trace.BadParameter("expected crypto.PublicKey but got %T", slotCert.PublicKey)
} else if !slotPub.Equal(ref.PublicKey) {
return nil, trace.CompareFailed("public key mismatch on PIV slot 0x%x", pivSlot.Key)
}

// If this sign request is coming from the hardware key agent, ensure that the requested PIV
// slot was configured by a Teleport client, or manually configured by the user / hardware key
// administrator. Manual configuration is used in cases where the default PIV management key
// is not used, e.g. when the hardware key is managed by a third party provider by an admin.
if keyInfo.AgentKey {
cert, err := y.getCertificate(pivSlot)
switch {
case errors.Is(err, piv.ErrNotFound):
return nil, trace.Wrap(&ErrMissingTeleportCert, "certificate not found in PIV slot 0x%x", pivSlot.Key)
case err != nil:
return nil, trace.Wrap(err)
case !isTeleportMetadataCertificate(cert):
return nil, trace.Wrap(&ErrMissingTeleportCert, nonTeleportCertificateMessage(pivSlot, cert))
}
}

ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)

Expand Down Expand Up @@ -213,11 +262,6 @@ func (y *YubiKey) sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, keyI
manualRetryWithPIN = true
}

pivSlot, err := parsePIVSlot(ref.SlotKey)
if err != nil {
return nil, trace.Wrap(err)
}

privateKey, err := y.conn.privateKey(pivSlot, ref.PublicKey, auth)
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -692,3 +736,32 @@ func SelfSignedMetadataCertificate(subject pkix.Name) (*x509.Certificate, error)
}
return cert, nil
}

func isTeleportMetadataCertificate(cert *x509.Certificate) bool {
return len(cert.Subject.Organization) > 0 && cert.Subject.Organization[0] == certOrgName
}

func nonTeleportCertificateMessage(slot piv.Slot, cert *x509.Certificate) string {
// Gather a small list of user-readable x509 certificate fields to display to the user.
sum := sha256.Sum256(cert.Raw)
fingerPrint := hex.EncodeToString(sum[:])
return fmt.Sprintf(`Certificate in YubiKey PIV slot %q is not a Teleport client cert:
Slot %s:
Algorithm: %v
Subject DN: %v
Issuer DN: %v
Serial: %v
Fingerprint: %v
Not before: %v
Not after: %v
`,
slot, slot,
cert.SignatureAlgorithm,
cert.Subject,
cert.Issuer,
cert.SerialNumber,
fingerPrint,
cert.NotBefore,
cert.NotAfter,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,27 @@ version: v2
Teleport clients generate keys in the slots specified using the default management key.

If your PIV key uses a different management key, you must generate the key yourself.
This can be done with the [YubiKey Manager CLI](https://developers.yubico.com/yubikey-manager/):
This can be done with the [YubiKey Manager CLI](https://developers.yubico.com/yubikey-manager/).
This command will prompt you to enter your management key to complete the request:

`ykman piv keys generate -a ECCP256 [slot] --touch-policy=[never|cached|always] --pin-policy=[never|once|always] -`
```code
$ ykman piv keys generate -a ECCP256 [slot] --touch-policy=[never|cached|always] --pin-policy=[never|once|always] pub.pem
```

For some features to work, you must also generate a certificate on the slot to mark
the slot for use by Teleport. This certificate can be self signed, or in the example
below, signed by the key in the PIV slot. The important detail is that the certificate
has "teleport" as the organization name in the subject field.

```code
$ ykman piv certificates generate 9e -s O=teleport pub.pem
```

This command will prompt you for the management key, as well as PIN or touch depending
on the policies of the key in the PIV slot.

After running this command, you're prompted to enter your management key to complete the request.
Make sure that the touch and PIN policy satisfy the hardware key requirement for your cluster and roles.
Make sure that the touch and PIN policies of the key satisfy the hardware key requirement
for your cluster and roles.

## Troubleshooting

Expand Down
Loading