github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/utils/keys/yubikey_test.go (about)

     1  //go:build piv
     2  
     3  /*
     4  Copyright 2022 Gravitational, Inc.
     5  Licensed under the Apache License, Version 2.0 (the "License");
     6  you may not use this file except in compliance with the License.
     7  You may obtain a copy of the License at
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package keys_test
    17  
    18  import (
    19  	"context"
    20  	"crypto/rand"
    21  	"crypto/x509/pkix"
    22  	"fmt"
    23  	"os"
    24  	"testing"
    25  
    26  	"github.com/go-piv/piv-go/piv"
    27  	"github.com/gravitational/trace"
    28  	"github.com/stretchr/testify/require"
    29  
    30  	"github.com/gravitational/teleport/api/utils/keys"
    31  	"github.com/gravitational/teleport/api/utils/prompt"
    32  )
    33  
    34  // TestGetYubiKeyPrivateKey_Interactive tests generation and retrieval of YubiKey private keys.
    35  func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) {
    36  	// This test expects a yubiKey to be connected with default PIV
    37  	// settings and will overwrite any PIV data on the yubiKey.
    38  	if os.Getenv("TELEPORT_TEST_YUBIKEY_PIV") == "" {
    39  		t.Skipf("Skipping TestGenerateYubiKeyPrivateKey because TELEPORT_TEST_YUBIKEY_PIV is not set")
    40  	}
    41  
    42  	if !testing.Verbose() {
    43  		t.Fatal("This test is interactive and must be called with the -v verbose flag to see touch prompts.")
    44  	}
    45  	fmt.Println("This test is interactive, tap your YubiKey when prompted.")
    46  
    47  	ctx := context.Background()
    48  	t.Cleanup(func() { resetYubikey(t) })
    49  
    50  	for _, policy := range []keys.PrivateKeyPolicy{
    51  		keys.PrivateKeyPolicyHardwareKey,
    52  		keys.PrivateKeyPolicyHardwareKeyTouch,
    53  		keys.PrivateKeyPolicyHardwareKeyPIN,
    54  		keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
    55  	} {
    56  		for _, customSlot := range []bool{true, false} {
    57  			t.Run(fmt.Sprintf("policy:%q", policy), func(t *testing.T) {
    58  				t.Run(fmt.Sprintf("custom slot:%v", customSlot), func(t *testing.T) {
    59  					resetYubikey(t)
    60  					setupPINPrompt(t)
    61  
    62  					var slot keys.PIVSlot = ""
    63  					if customSlot {
    64  						slot = "9a"
    65  					}
    66  
    67  					// GetYubiKeyPrivateKey should generate a new YubiKeyPrivateKey.
    68  					priv, err := keys.GetYubiKeyPrivateKey(ctx, policy, slot)
    69  					require.NoError(t, err)
    70  
    71  					// test HardwareSigner methods
    72  					require.Equal(t, policy, priv.GetPrivateKeyPolicy())
    73  					require.NotNil(t, priv.GetAttestationStatement())
    74  
    75  					// Test Sign.
    76  					digest := []byte{100}
    77  					_, err = priv.Sign(rand.Reader, digest, nil)
    78  					require.NoError(t, err)
    79  
    80  					// Another call to GetYubiKeyPrivateKey should retrieve the previously generated key.
    81  					retrievePriv, err := keys.GetYubiKeyPrivateKey(ctx, policy, slot)
    82  					require.NoError(t, err)
    83  					require.Equal(t, priv.Public(), retrievePriv.Public())
    84  
    85  					// parsing the key's private key PEM should produce the same key as well.
    86  					retrievePriv, err = keys.ParsePrivateKey(priv.PrivateKeyPEM())
    87  					require.NoError(t, err)
    88  					require.Equal(t, priv.Public(), retrievePriv.Public())
    89  				})
    90  			})
    91  		}
    92  	}
    93  }
    94  
    95  func TestOverwritePrompt(t *testing.T) {
    96  	// This test expects a yubiKey to be connected with default PIV
    97  	// settings and will overwrite any PIV data on the yubiKey.
    98  	if os.Getenv("TELEPORT_TEST_YUBIKEY_PIV") == "" {
    99  		t.Skipf("Skipping TestGenerateYubiKeyPrivateKey because TELEPORT_TEST_YUBIKEY_PIV is not set")
   100  	}
   101  
   102  	ctx := context.Background()
   103  	t.Cleanup(func() { resetYubikey(t) })
   104  
   105  	// Use a custom slot.
   106  	pivSlot, err := keys.GetDefaultKeySlot(keys.PrivateKeyPolicyHardwareKeyTouch)
   107  	require.NoError(t, err)
   108  
   109  	testOverwritePrompt := func(t *testing.T) {
   110  		// Fail to overwrite slot when user denies
   111  		prompt.SetStdin(prompt.NewFakeReader().AddString("n"))
   112  		_, err := keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKeyTouch, "" /* slot */)
   113  		require.True(t, trace.IsCompareFailed(err), "Expected compare failed error but got %v", err)
   114  
   115  		// Successfully overwrite slot when user accepts
   116  		prompt.SetStdin(prompt.NewFakeReader().AddString("y"))
   117  		_, err = keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKeyTouch, "" /* slot */)
   118  		require.NoError(t, err)
   119  	}
   120  
   121  	t.Run("invalid metadata cert", func(t *testing.T) {
   122  		resetYubikey(t)
   123  
   124  		// Set a non-teleport certificate in the slot.
   125  		y, err := keys.FindYubiKey(0)
   126  		require.NoError(t, err)
   127  		err = y.SetMetadataCertificate(pivSlot, pkix.Name{Organization: []string{"not-teleport"}})
   128  		require.NoError(t, err)
   129  
   130  		testOverwritePrompt(t)
   131  	})
   132  
   133  	t.Run("invalid key policies", func(t *testing.T) {
   134  		resetYubikey(t)
   135  
   136  		// Generate a key that does not require touch in the slot that Teleport expects to require touch.
   137  		_, err := keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKey, keys.PIVSlot(pivSlot.String()))
   138  		require.NoError(t, err)
   139  
   140  		testOverwritePrompt(t)
   141  	})
   142  }
   143  
   144  // resetYubikey connects to the first yubiKey and resets it to defaults.
   145  func resetYubikey(t *testing.T) {
   146  	t.Helper()
   147  	y, err := keys.FindYubiKey(0)
   148  	require.NoError(t, err)
   149  	require.NoError(t, y.Reset())
   150  }
   151  
   152  func setupPINPrompt(t *testing.T) {
   153  	t.Helper()
   154  	y, err := keys.FindYubiKey(0)
   155  	require.NoError(t, err)
   156  
   157  	// Set pin for tests.
   158  	const testPIN = "123123"
   159  	require.NoError(t, y.SetPIN(piv.DefaultPIN, testPIN))
   160  
   161  	// Handle PIN prompt.
   162  	oldStdin := prompt.Stdin()
   163  	t.Cleanup(func() { prompt.SetStdin(oldStdin) })
   164  	prompt.SetStdin(prompt.NewFakeReader().AddString(testPIN).AddString(testPIN))
   165  }