diff --git a/api/utils/keys/hardwarekey/hardwarekey.go b/api/utils/keys/hardwarekey/hardwarekey.go index fba453a87f970..92121b233c1fc 100644 --- a/api/utils/keys/hardwarekey/hardwarekey.go +++ b/api/utils/keys/hardwarekey/hardwarekey.go @@ -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. diff --git a/api/utils/keys/hardwarekeyagent/agent.go b/api/utils/keys/hardwarekeyagent/agent.go index 270a7932734f9..44b4b06c92e87 100644 --- a/api/utils/keys/hardwarekeyagent/agent.go +++ b/api/utils/keys/hardwarekeyagent/agent.go @@ -23,6 +23,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "log/slog" "net" "os" @@ -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 @@ -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) } diff --git a/api/utils/keys/piv/service.go b/api/utils/keys/piv/service.go index 90f1425011225..e050d57bead16 100644 --- a/api/utils/keys/piv/service.go +++ b/api/utils/keys/piv/service.go @@ -20,9 +20,6 @@ package piv import ( "context" "crypto" - "crypto/sha256" - "crypto/x509" - "encoding/hex" "errors" "fmt" "io" @@ -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) } @@ -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, - ) -} diff --git a/api/utils/keys/piv/yubikey.go b/api/utils/keys/piv/yubikey.go index a31ed2b0b98a1..d0a6e02ad70f3 100644 --- a/api/utils/keys/piv/yubikey.go +++ b/api/utils/keys/piv/yubikey.go @@ -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" @@ -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) @@ -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) @@ -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, + ) +} diff --git a/docs/pages/admin-guides/access-controls/guides/hardware-key-support.mdx b/docs/pages/admin-guides/access-controls/guides/hardware-key-support.mdx index ff0c087969a41..2b335d672cdd8 100644 --- a/docs/pages/admin-guides/access-controls/guides/hardware-key-support.mdx +++ b/docs/pages/admin-guides/access-controls/guides/hardware-key-support.mdx @@ -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