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 }