Skip to content

Commit

Permalink
Add certificate details in slot overwrite prompt
Browse files Browse the repository at this point in the history
Use non PIV key for self signed metadata certificate

Update rfd.
  • Loading branch information
Joerger committed Sep 20, 2023
1 parent 3504aa1 commit 7aeaa0d
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 26 deletions.
44 changes: 32 additions & 12 deletions api/utils/keys/yubikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
Expand Down Expand Up @@ -322,19 +325,13 @@ func (y *yubiKey) generatePrivateKey(ctx context.Context, slot piv.Slot, private
defer cancelTouchPrompt()
}

pub, err := yk.GenerateKey(piv.DefaultManagementKey, slot, opts)
if err != nil {
return trace.Wrap(err)
}

priv, err := yk.PrivateKey(slot, pub, piv.KeyAuth{})
if err != nil {
if _, err := yk.GenerateKey(piv.DefaultManagementKey, slot, opts); err != nil {
return trace.Wrap(err)
}

// Generate a self signed cert to provide metadata about the private key in the slot.
// This is useful for users to discern where the key came from with tools like `ykman piv info`.
cert, err := selfSignedMetadataCert(priv, pub)
cert, err := selfSignedMetadataCert()
if err != nil {
return trace.Wrap(err)
}
Expand Down Expand Up @@ -364,7 +361,7 @@ func (y *yubiKey) getPrivateKey(slot piv.Slot, checkCert bool, requireKeyPolicy
if err != nil || cert == nil {
return nil, trace.NotFound("certificate in YubiKey PIV slot %q is empty, expected a Teleport Client cert", slot.String())
} else if len(cert.Subject.Organization) == 0 || cert.Subject.Organization[0] != certOrgName {
return nil, trace.CompareFailed("certificate in YubiKey PIV slot %q is not a Teleport Client cert:\n%+v", slot.String(), cert)
return nil, nonTeleportCertificateError(slot, cert)
}
}

Expand Down Expand Up @@ -532,27 +529,50 @@ func parsePIVSlot(slotKey uint32) (piv.Slot, error) {
// certOrgName is used to identify Teleport Client self-signed certificates stored in yubiKey PIV slots.
const certOrgName = "teleport"

func selfSignedMetadataCert(priv crypto.PrivateKey, pub crypto.PublicKey) (*x509.Certificate, error) {
func selfSignedMetadataCert() (*x509.Certificate, error) {
// generate a small rsa key to quickly generate a metadata cert.
priv, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
return nil, trace.Wrap(err)
}

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) // see crypto/tls/generate_cert.go
if err != nil {
return nil, trace.Wrap(err)
}
cert := &x509.Certificate{
SerialNumber: serialNumber,
PublicKey: pub,
PublicKey: priv.Public(),
Subject: pkix.Name{
Organization: []string{certOrgName},
OrganizationalUnit: []string{api.Version},
},
}

if cert.Raw, err = x509.CreateCertificate(rand.Reader, cert, cert, pub, priv); err != nil {
if cert.Raw, err = x509.CreateCertificate(rand.Reader, cert, cert, priv.Public(), priv); err != nil {
return nil, trace.Wrap(err)
}
return cert, nil
}

// nonTeleportCertificateError returns a user-readable CompareFailed error.
// when we run into a non-Teleport certificate, we need to get user input
// before we overwrite it to avoid breaking their non-Teleport PIV programs.
func nonTeleportCertificateError(slot piv.Slot, cert *x509.Certificate) error {
msg := fmt.Sprintf("Certificate in YubiKey PIV slot %q is not a Teleport client cert:\nSlot %s:\n", slot.String(), slot.String())
// Gather a small list of user-readable x509 certificate fields to append to the error message.
msg += fmt.Sprintf("\tAlgorithm:\t%v\n", cert.SignatureAlgorithm)
msg += fmt.Sprintf("\tSubject DN:\t%v\n", cert.Subject)
msg += fmt.Sprintf("\tIssuer DN:\t%v\n", cert.Issuer)
msg += fmt.Sprintf("\tSerial:\t\t%v\n", cert.SerialNumber)
sum := sha256.Sum256(cert.Raw)
msg += fmt.Sprintf("\tFingerprint:\t%v\n", hex.EncodeToString(sum[:]))
msg += fmt.Sprintf("\tNot before:\t%v\n", cert.NotBefore)
msg += fmt.Sprintf("\tNot after:\t%v\n", cert.NotAfter)
return trace.CompareFailed(msg)
}

// YubiKeys require touch when generating a private key that requires touch, or using
// a private key (Sign) with touch required. Unfortunately, there is no good way to
// check whether touch is cached by the PIV module at a given time. In order to require
Expand Down
19 changes: 5 additions & 14 deletions rfd/0080-hardware-key-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,13 @@ If the key policy is `hardware_key` or `hardware_key_touch`, then a private key

PIV provides us with up to 24 different slots. Each slot has a different intended purpose, but functionally they are the same. We will use the first two slots (`9a` and `9c`) to store up to two keys at a time (the first with `TouchPolicy=never` and the second with `TouchPolicy=cached`).

Each of these keys will be generated for the first time when a Teleport client is required to meet its respective private key policy. Once a key is generated, it will be reused by any other Teleport client required to meet the same private key policy. Reusable keys will be detectable by checking the slot's stored certificate, which will be given a self-signed metadata-containing certificate created by Teleport clients.
Each of these keys will be generated for the first time when a Teleport client is required to meet its respective private key policy. Once a key is generated, it will be reused by any other Teleport client required to meet the same private key policy.

If a Teleport Client notices that a slot is in use, but not by a Teleport client, then it should prompt the user before overwriting the slot. This will also be done by checking the certificate stored in the slot. We will share the certificate in the prompt, since it may contain identifying information. e.g:
Teleport clients will also store a self-signed metadata-containing certificate. When this certificate is present, Teleport clients will reuse or regenerate keys in the slot as needed. If the certificate in the slot is unknown or missing, Teleport clients will prompt the user for confirmation before overwriting an existing key or cert in the slot:

```bash
> tsh login
Hardware key slot 9a appears to be in use by another program.
Certificate in YubiKey PIV slot "9a" is not a Teleport client cert:
Slot 9a:
Algorithm: ECCP256
Subject DN: CN=SSH key
Expand All @@ -320,7 +320,7 @@ Slot 9a:
Fingerprint: 1ce4faf8bdbfc9668a9f532c20b03ccf1dbadcd06b51f235aeb3fe388bb1703b
Not before: 2022-08-19 01:10:14
Not after: 2064-08-19 01:10:14
Would you like to overwrite this slot? (y/N):
Would you like to overwrite this slot's private key and certificate? (y/N):
```
##### Custom slot configuration
Expand All @@ -331,16 +331,7 @@ To support non-standard use cases, users can also provide a specific PIV slot to
* server settings: `auth_service.authentication.piv_slot`
* cluster auth preference settings: `cluster_auth_preference.spec.piv_slot`
This value can be set to the hexadecimal string representing the slot, such as `9d`. Any existing key in the slot will be used. If no key exists, the Teleport Client will attempt to generate a key in the slot.

If the key does not meet the private key policy requirement for the user's cluster/roles, an error will be returned:

```
> tsh --piv-slot=9d login --user=dev
Enter password for Teleport user dev:
Tap any security key
ERROR: pre-generated YubiKey private key in PIV slot "9d" does not meet the private key policy requirement "hardware_key_touch".
```
This value can be set to the hexadecimal string representing the slot, such as `9d`. Any existing key in the slot will be used. If no key exists, or the key does not meet the required key policy, the Teleport Client will attempt to generate a key in the slot.
#### Private key interface
Expand Down

0 comments on commit 7aeaa0d

Please sign in to comment.