mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-17 17:40:30 +08:00
MWI: Bound Keypair Rotation (#55240)
* MWI: Bound Keypair Joining: Keypair rotation This adds keypair rotation for bound keypair rotation. When a rotation flag is set in the token spec, joining clients will be required to generate a new keypair and complete an additional joining challenge against the new keypair. The flag is a timestamp token to allow for some level of idempotency; to make setting this flag easier, a new `tctl` command is included: `tctl bound-keypair request-rotation [token]`. This sets the flag to the current timestamp, and joining clients will be required to perform a rotation on their next authentication attempt. Closes #55084 * Properly initialize the tctl command * Refactor ClientState to allow storing intermediate state during rotation * Fix invalid comparison and mutation logic * Log signature suite and use cryptosuites helper * Remove outdated TODO * Frontload MFA check to avoid prompting twice * Fix tctl command logging * Fix incomplete docstring * Fix imports * Fix typo in log message * Add tests for server-side rotation Adjusts the test harness a bit and adds a batch of test cases for keypair rotation. Also fixes a lint error. * Add additional test case for reused keys * Add ClientState unit test * Remove unnecessary log * Fix test lints * Fix reference to wrong key field Now that the key can change, fix a dangling reference to the initial key field. Also s/marshalled/marshaled * Wrap KeyHistoryEntry in a containing struct This should allow for some future extension if needed.
This commit is contained in:
@@ -1443,9 +1443,11 @@ func (m *RegisterUsingBoundKeypairCertificates) GetPublicKey() string {
|
||||
// RegisterUsingBoundKeypairRotationRequest is the response sent by the server
|
||||
// when a keypair rotation is required.
|
||||
type RegisterUsingBoundKeypairRotationRequest struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
// The signature algorithm suite in use by the cluster.
|
||||
SignatureAlgorithmSuite types.SignatureAlgorithmSuite `protobuf:"varint,1,opt,name=signature_algorithm_suite,json=signatureAlgorithmSuite,proto3,enum=types.SignatureAlgorithmSuite" json:"signature_algorithm_suite,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *RegisterUsingBoundKeypairRotationRequest) Reset() {
|
||||
@@ -1483,6 +1485,13 @@ func (m *RegisterUsingBoundKeypairRotationRequest) XXX_DiscardUnknown() {
|
||||
|
||||
var xxx_messageInfo_RegisterUsingBoundKeypairRotationRequest proto.InternalMessageInfo
|
||||
|
||||
func (m *RegisterUsingBoundKeypairRotationRequest) GetSignatureAlgorithmSuite() types.SignatureAlgorithmSuite {
|
||||
if m != nil {
|
||||
return m.SignatureAlgorithmSuite
|
||||
}
|
||||
return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED
|
||||
}
|
||||
|
||||
// RegisterUsingBoundKeypairMethodResponse is a response sent by the server
|
||||
// during the bound-keypair joining process. Multiple requests and responses are
|
||||
// expected to be exchanged during one join process.
|
||||
@@ -1623,86 +1632,88 @@ func init() {
|
||||
}
|
||||
|
||||
var fileDescriptor_d7e760ce923b836e = []byte{
|
||||
// 1254 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0x4b, 0x6f, 0xdb, 0xc6,
|
||||
0x13, 0x17, 0xe5, 0xd8, 0x89, 0x46, 0xca, 0x43, 0x9b, 0x20, 0x7f, 0x59, 0x88, 0x5f, 0xfc, 0x3b,
|
||||
0xb1, 0x9c, 0x36, 0x92, 0xab, 0x5e, 0x8a, 0x9c, 0xea, 0x17, 0x20, 0xc7, 0x75, 0x6b, 0x30, 0x29,
|
||||
0x8a, 0xf6, 0x42, 0xac, 0xa8, 0xad, 0xbc, 0x11, 0x43, 0xb2, 0xbb, 0x2b, 0x03, 0xea, 0xb9, 0x97,
|
||||
0x7e, 0x80, 0x9c, 0xfb, 0x09, 0x0a, 0xf4, 0xd0, 0x73, 0xcf, 0x3d, 0x14, 0x45, 0x6f, 0xbd, 0x16,
|
||||
0xfe, 0x0e, 0xbd, 0x17, 0xdc, 0x5d, 0x4a, 0x24, 0x45, 0x4a, 0x32, 0x90, 0xf6, 0x62, 0x99, 0x33,
|
||||
0xb3, 0xbf, 0x79, 0xcf, 0x2c, 0x09, 0x4d, 0x41, 0x5c, 0x12, 0xf8, 0x4c, 0xb4, 0x5c, 0xd2, 0xc7,
|
||||
0xce, 0xa8, 0xe5, 0xb8, 0x94, 0x78, 0xa2, 0x15, 0x30, 0x5f, 0xf8, 0xad, 0xd7, 0x3e, 0xf5, 0x38,
|
||||
0x61, 0x97, 0xd4, 0x21, 0x4d, 0x49, 0x41, 0xcb, 0xf2, 0xa7, 0xde, 0x98, 0x79, 0xcc, 0x21, 0x4c,
|
||||
0x70, 0x75, 0xa0, 0xbe, 0x95, 0x96, 0x14, 0xa3, 0x80, 0x70, 0xf5, 0x57, 0x89, 0x98, 0x3f, 0x1b,
|
||||
0xb0, 0x66, 0x91, 0x3e, 0xe5, 0x82, 0xb0, 0xcf, 0x39, 0xf5, 0xfa, 0x27, 0xfb, 0x67, 0x67, 0x44,
|
||||
0x5c, 0xf8, 0x3d, 0x8b, 0x7c, 0x33, 0x24, 0x5c, 0x20, 0x0c, 0x8f, 0x98, 0x16, 0xb0, 0x87, 0xa1,
|
||||
0x84, 0x2d, 0xfc, 0x01, 0xf1, 0x6c, 0xa6, 0xf8, 0x35, 0x63, 0xd3, 0x68, 0x94, 0xdb, 0x9b, 0x4d,
|
||||
0x85, 0x9a, 0xc0, 0x7a, 0x15, 0x0a, 0x6a, 0x1c, 0x6b, 0x95, 0xe5, 0xb1, 0xd0, 0x1e, 0x3c, 0xe0,
|
||||
0x82, 0xdb, 0xb4, 0x47, 0x3c, 0x41, 0xc5, 0x68, 0x0c, 0x5d, 0xdc, 0x34, 0x1a, 0x15, 0x0b, 0x71,
|
||||
0xc1, 0x4f, 0x34, 0x4b, 0x9f, 0x30, 0xbb, 0xb0, 0x9e, 0x67, 0x35, 0x0f, 0x7c, 0x8f, 0x13, 0xf4,
|
||||
0x08, 0x4a, 0xce, 0x05, 0x76, 0x5d, 0xe2, 0xf5, 0x89, 0xb4, 0xb1, 0x64, 0x4d, 0x08, 0xc8, 0x84,
|
||||
0x65, 0x19, 0x28, 0xa9, 0xa2, 0xdc, 0xae, 0xa8, 0x68, 0x34, 0x0f, 0x43, 0x9a, 0xa5, 0x58, 0xe6,
|
||||
0x6f, 0x06, 0x6c, 0x24, 0x94, 0xec, 0x7f, 0x3b, 0x64, 0xe4, 0x3f, 0x0f, 0xce, 0xff, 0xe1, 0x36,
|
||||
0x16, 0x82, 0x70, 0x41, 0x7a, 0x76, 0x0f, 0x0b, 0xac, 0xa3, 0x52, 0x89, 0x88, 0x47, 0x58, 0x60,
|
||||
0xb4, 0x05, 0x15, 0xec, 0x38, 0x84, 0x73, 0xa5, 0xbf, 0xb6, 0x24, 0x1d, 0x2e, 0x2b, 0x9a, 0x84,
|
||||
0x33, 0x7b, 0xb0, 0x99, 0xef, 0xcd, 0x3b, 0x0b, 0xda, 0x31, 0xec, 0x24, 0xbd, 0x3c, 0xd7, 0x89,
|
||||
0x39, 0x8c, 0x60, 0xc6, 0xca, 0xea, 0x70, 0x8b, 0xfb, 0xee, 0x50, 0x50, 0xdf, 0x93, 0xba, 0x2a,
|
||||
0xd6, 0xf8, 0xd9, 0xfc, 0xdb, 0x80, 0xed, 0x6c, 0x9c, 0x13, 0x8f, 0x0a, 0x8a, 0xdd, 0x28, 0x3a,
|
||||
0x87, 0x50, 0x09, 0x1b, 0xe5, 0xda, 0x01, 0x2f, 0x87, 0xa7, 0x22, 0x90, 0x55, 0xb8, 0x49, 0x06,
|
||||
0x76, 0xe8, 0x80, 0x0a, 0x6e, 0xa7, 0x60, 0xad, 0x90, 0x41, 0xe8, 0x17, 0xfa, 0x1f, 0xac, 0x90,
|
||||
0x81, 0x3d, 0x20, 0x23, 0x19, 0xd2, 0x90, 0xb3, 0x4c, 0x06, 0xa7, 0x64, 0x84, 0x3e, 0x05, 0xa4,
|
||||
0x32, 0x80, 0x43, 0x83, 0xed, 0x00, 0x33, 0xfc, 0x86, 0xd7, 0x6e, 0x48, 0xf5, 0x1b, 0x3a, 0x32,
|
||||
0xaf, 0xce, 0xcf, 0xf6, 0x27, 0x32, 0xe7, 0xa1, 0x08, 0x11, 0x84, 0x71, 0xab, 0x8a, 0x53, 0x64,
|
||||
0x7e, 0x70, 0x03, 0x8a, 0x64, 0x60, 0xfe, 0x9e, 0x6e, 0xc7, 0xb1, 0xdf, 0x91, 0xad, 0xfb, 0x70,
|
||||
0x83, 0x7a, 0x34, 0x72, 0xf4, 0x3d, 0xad, 0x69, 0x91, 0x58, 0x75, 0x0a, 0x96, 0x3c, 0x8a, 0x6c,
|
||||
0x40, 0xe3, 0xa4, 0xda, 0x4c, 0xa7, 0x43, 0x27, 0xb5, 0x39, 0x13, 0x70, 0x2a, 0x89, 0x9d, 0x82,
|
||||
0x55, 0x75, 0xd2, 0xc4, 0x83, 0x12, 0xdc, 0x0c, 0xf0, 0xc8, 0xf5, 0x71, 0xcf, 0xfc, 0xc1, 0x48,
|
||||
0x75, 0x6a, 0xcc, 0x21, 0x5d, 0x07, 0x9f, 0x40, 0x35, 0x6e, 0x4e, 0x3c, 0x8f, 0x6b, 0x93, 0x40,
|
||||
0x1e, 0x7b, 0x0e, 0x1b, 0x05, 0x82, 0xf4, 0x0e, 0x19, 0x91, 0xc3, 0x00, 0xbb, 0x9d, 0x82, 0x75,
|
||||
0x2f, 0xa6, 0x5c, 0xc5, 0x67, 0x7b, 0x46, 0x91, 0x86, 0xd9, 0x93, 0xcc, 0xb8, 0x85, 0x3f, 0x1a,
|
||||
0x50, 0xcb, 0x4b, 0x14, 0x7a, 0x08, 0x2b, 0xc1, 0xb0, 0xeb, 0x52, 0x47, 0x57, 0xa8, 0x7e, 0x42,
|
||||
0x1b, 0x50, 0x76, 0x18, 0xc1, 0x82, 0xc4, 0x5b, 0x12, 0x14, 0x49, 0x36, 0xe4, 0x33, 0x40, 0x5a,
|
||||
0x20, 0x96, 0x6a, 0x55, 0x43, 0x56, 0x55, 0x71, 0x62, 0x1a, 0xd1, 0x2e, 0xdc, 0xd3, 0xe2, 0x9c,
|
||||
0xf6, 0x3d, 0x2c, 0x86, 0x8c, 0xc8, 0x5a, 0xaa, 0x58, 0x77, 0x15, 0xfd, 0x65, 0x44, 0x36, 0xbf,
|
||||
0x84, 0x87, 0xd9, 0xe1, 0x40, 0x3b, 0x10, 0x0a, 0xeb, 0x27, 0xbb, 0xeb, 0xfa, 0x5d, 0x6d, 0xf5,
|
||||
0x9d, 0x09, 0xf9, 0xc0, 0xf5, 0xbb, 0xa1, 0x57, 0x9c, 0x38, 0x8c, 0x44, 0x13, 0x56, 0x3f, 0x99,
|
||||
0x3f, 0x15, 0xe1, 0xfe, 0x67, 0x0c, 0x3b, 0xae, 0x54, 0x47, 0x62, 0x35, 0x77, 0xf3, 0x82, 0xe0,
|
||||
0x1e, 0x61, 0xbc, 0x66, 0x6c, 0x2e, 0x35, 0xca, 0xed, 0x1d, 0x1d, 0xd5, 0x0c, 0xe1, 0x66, 0x47,
|
||||
0x49, 0x1e, 0x7b, 0x82, 0x8d, 0xac, 0xe8, 0x1c, 0xfa, 0x02, 0xee, 0xea, 0x80, 0xdb, 0x11, 0x54,
|
||||
0x51, 0x42, 0x35, 0x67, 0x40, 0x9d, 0xab, 0x13, 0x09, 0xc4, 0x3b, 0x41, 0x82, 0x58, 0x7f, 0x0e,
|
||||
0x95, 0x38, 0x1f, 0xdd, 0x83, 0xa5, 0xb0, 0x5b, 0xd5, 0xf0, 0x0a, 0xff, 0x45, 0x0f, 0x60, 0xf9,
|
||||
0x12, 0xbb, 0x43, 0x55, 0xe1, 0x25, 0x4b, 0x3d, 0x3c, 0x2f, 0x7e, 0x64, 0xd4, 0xf7, 0xe1, 0x7e,
|
||||
0x86, 0x8a, 0xeb, 0x40, 0x98, 0x7f, 0x1a, 0xa9, 0xb1, 0xaa, 0xfc, 0x48, 0xf6, 0xac, 0xf3, 0x6e,
|
||||
0xb6, 0x44, 0xa7, 0x30, 0x6b, 0x4f, 0x1c, 0xc2, 0x1d, 0x5f, 0xea, 0x4e, 0xac, 0xcf, 0x72, 0xbb,
|
||||
0x9e, 0x1f, 0xe0, 0x4e, 0xc1, 0xba, 0xad, 0xce, 0x68, 0x42, 0xd8, 0x17, 0xfa, 0xb4, 0x39, 0x84,
|
||||
0xad, 0x19, 0x8e, 0xe9, 0xde, 0x5d, 0x9f, 0x5a, 0x18, 0x9d, 0x42, 0x7c, 0x65, 0x2c, 0xd6, 0x8d,
|
||||
0x00, 0xb7, 0xa2, 0x31, 0x14, 0x4e, 0xc0, 0xe4, 0x06, 0x39, 0xf0, 0x87, 0x5e, 0xef, 0x94, 0x8c,
|
||||
0x02, 0x4c, 0xd9, 0xbf, 0x31, 0xfc, 0x9b, 0x70, 0x9f, 0x2a, 0x58, 0x5b, 0x82, 0xc5, 0x3a, 0xa3,
|
||||
0x64, 0x55, 0x35, 0xeb, 0x85, 0x4f, 0xbd, 0x97, 0x92, 0x11, 0xca, 0x07, 0x8c, 0x5c, 0x52, 0x7f,
|
||||
0xc8, 0xf5, 0x01, 0x81, 0x05, 0x89, 0x5a, 0x3b, 0x62, 0xc9, 0x03, 0x21, 0xc3, 0xec, 0xc0, 0xd3,
|
||||
0x5c, 0x7f, 0xae, 0xb7, 0x14, 0x5f, 0xc0, 0x6e, 0x2e, 0x92, 0xe5, 0xab, 0x49, 0x32, 0x06, 0x5a,
|
||||
0x03, 0x50, 0xb3, 0xca, 0x9e, 0xd4, 0x72, 0x49, 0x51, 0x4e, 0xc9, 0xc8, 0xfc, 0xa5, 0x08, 0x4f,
|
||||
0x72, 0xc1, 0x92, 0xd5, 0x7b, 0x94, 0xd8, 0x38, 0x99, 0x0b, 0x22, 0x3f, 0x47, 0xe3, 0xa5, 0xd3,
|
||||
0x9d, 0xb1, 0x74, 0x3e, 0x98, 0x87, 0xb9, 0xd8, 0xde, 0x41, 0x36, 0x54, 0x99, 0x8e, 0xc3, 0x44,
|
||||
0xc5, 0x92, 0x54, 0xb1, 0x37, 0x4f, 0x45, 0x3a, 0x80, 0xe1, 0x72, 0x61, 0x29, 0x5a, 0x7c, 0x6d,
|
||||
0x60, 0x30, 0xe7, 0x9b, 0x3b, 0x27, 0x0b, 0xc9, 0xfb, 0x56, 0x31, 0x75, 0xdf, 0x32, 0xbf, 0x37,
|
||||
0xe0, 0x71, 0xbe, 0x0e, 0xc2, 0x04, 0xfd, 0x9a, 0x3a, 0x58, 0x10, 0x3e, 0xb9, 0x99, 0x19, 0xb9,
|
||||
0x37, 0xb3, 0xd0, 0x94, 0x58, 0xb9, 0xaa, 0xc1, 0x5f, 0x7a, 0x1d, 0x95, 0x69, 0xca, 0xd2, 0xa5,
|
||||
0x74, 0xbd, 0x3c, 0x85, 0xc6, 0x02, 0xa1, 0x53, 0x93, 0xe3, 0x6d, 0x71, 0x46, 0x0b, 0xa7, 0x06,
|
||||
0xc8, 0x49, 0x7a, 0x80, 0x94, 0xdb, 0xbb, 0x0b, 0x57, 0x43, 0x72, 0xd6, 0x1c, 0x25, 0x67, 0xcd,
|
||||
0xfb, 0x73, 0x61, 0x62, 0x11, 0x1c, 0xcf, 0x22, 0x74, 0x06, 0xb7, 0xa2, 0xb4, 0xeb, 0xd2, 0x69,
|
||||
0x2d, 0x5e, 0x3a, 0x51, 0xc9, 0x8f, 0x21, 0xe2, 0xa3, 0xad, 0xfd, 0x76, 0x19, 0xca, 0x6a, 0x90,
|
||||
0xc8, 0xb7, 0x3a, 0x44, 0xe1, 0x61, 0xf6, 0x4b, 0x0c, 0xda, 0xce, 0x52, 0x99, 0x7e, 0x33, 0xab,
|
||||
0x3f, 0x9e, 0x23, 0xa5, 0xd4, 0x36, 0x8c, 0x3d, 0x03, 0xf9, 0x50, 0xcb, 0xbb, 0xfc, 0xa3, 0x27,
|
||||
0x59, 0x30, 0xd3, 0xef, 0x3a, 0xf5, 0x9d, 0xb9, 0x72, 0x31, 0x85, 0x69, 0xdf, 0xc6, 0xd7, 0xbe,
|
||||
0x6c, 0xdf, 0xd2, 0xd7, 0xdc, 0x6c, 0xdf, 0xa6, 0xee, 0x8e, 0x52, 0x15, 0x83, 0xd5, 0xdc, 0x45,
|
||||
0x85, 0x32, 0x8d, 0xce, 0xd8, 0xd1, 0xf5, 0xc6, 0x7c, 0xc1, 0x98, 0xce, 0xef, 0xd2, 0xef, 0x86,
|
||||
0xd3, 0x25, 0x8e, 0x9e, 0xcd, 0xab, 0x9b, 0xa4, 0x01, 0xcd, 0x45, 0xc5, 0x63, 0x66, 0x1c, 0x01,
|
||||
0x9a, 0xde, 0x72, 0x68, 0xee, 0x02, 0xac, 0x27, 0x06, 0xc4, 0xc1, 0xc7, 0xbf, 0x5e, 0xad, 0x1b,
|
||||
0x7f, 0x5c, 0xad, 0x1b, 0x7f, 0x5d, 0xad, 0x1b, 0x5f, 0xb5, 0xfb, 0x54, 0x5c, 0x0c, 0xbb, 0x4d,
|
||||
0xc7, 0x7f, 0xd3, 0xea, 0x33, 0x7c, 0x49, 0x55, 0x25, 0x63, 0xb7, 0x35, 0xfe, 0x9a, 0x80, 0x03,
|
||||
0x9a, 0xf8, 0xe8, 0xd0, 0x5d, 0x91, 0x3f, 0x1f, 0xfe, 0x13, 0x00, 0x00, 0xff, 0xff, 0x72, 0x17,
|
||||
0x9a, 0x35, 0xd2, 0x10, 0x00, 0x00,
|
||||
// 1294 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0xcf, 0x8f, 0xd3, 0xc6,
|
||||
0x17, 0x8f, 0xb3, 0xec, 0x42, 0x5e, 0xc2, 0xc2, 0x0e, 0x08, 0x76, 0x23, 0x08, 0x8b, 0xbf, 0xc0,
|
||||
0x86, 0x6f, 0x4b, 0x42, 0xd3, 0x4b, 0xc5, 0xa9, 0xd9, 0x5d, 0xa4, 0x2c, 0x74, 0xdb, 0x95, 0xa1,
|
||||
0xaa, 0xca, 0xc5, 0x9a, 0x38, 0xd3, 0xec, 0x10, 0x63, 0xbb, 0x33, 0xe3, 0x95, 0xd2, 0x73, 0x55,
|
||||
0xa9, 0x7f, 0x00, 0xe7, 0xfe, 0x05, 0x95, 0x7a, 0xe8, 0xb9, 0xe7, 0x1e, 0xaa, 0xaa, 0xb7, 0x5e,
|
||||
0x2b, 0xfe, 0x87, 0xde, 0x2b, 0xcf, 0x8c, 0x1d, 0xdb, 0xb1, 0x93, 0x20, 0xd1, 0x5e, 0x58, 0xfc,
|
||||
0xde, 0x9b, 0xcf, 0xfb, 0xfd, 0xde, 0x4c, 0xa0, 0x23, 0x88, 0x4b, 0x02, 0x9f, 0x89, 0xae, 0x4b,
|
||||
0xc6, 0xd8, 0x99, 0x76, 0x1d, 0x97, 0x12, 0x4f, 0x74, 0x03, 0xe6, 0x0b, 0xbf, 0xfb, 0xd2, 0xa7,
|
||||
0x1e, 0x27, 0xec, 0x8c, 0x3a, 0xa4, 0x23, 0x29, 0x68, 0x5d, 0xfe, 0x69, 0xb6, 0x17, 0x1e, 0x73,
|
||||
0x08, 0x13, 0x5c, 0x1d, 0x68, 0xde, 0xce, 0x4b, 0x8a, 0x69, 0x40, 0xb8, 0xfa, 0x57, 0x89, 0x98,
|
||||
0x3f, 0x1b, 0x70, 0xd3, 0x22, 0x63, 0xca, 0x05, 0x61, 0x9f, 0x73, 0xea, 0x8d, 0x8f, 0xfa, 0xc7,
|
||||
0xc7, 0x44, 0x9c, 0xfa, 0x23, 0x8b, 0x7c, 0x1d, 0x12, 0x2e, 0x10, 0x86, 0x1b, 0x4c, 0x0b, 0xd8,
|
||||
0x61, 0x24, 0x61, 0x0b, 0x7f, 0x42, 0x3c, 0x9b, 0x29, 0xfe, 0xb6, 0xb1, 0x6b, 0xb4, 0xeb, 0xbd,
|
||||
0xdd, 0x8e, 0x42, 0xcd, 0x60, 0x3d, 0x8f, 0x04, 0x35, 0x8e, 0xb5, 0xc3, 0xca, 0x58, 0xe8, 0x21,
|
||||
0x5c, 0xe5, 0x82, 0xdb, 0x74, 0x44, 0x3c, 0x41, 0xc5, 0x34, 0x81, 0xae, 0xee, 0x1a, 0xed, 0x86,
|
||||
0x85, 0xb8, 0xe0, 0x47, 0x9a, 0xa5, 0x4f, 0x98, 0x43, 0x68, 0x95, 0x59, 0xcd, 0x03, 0xdf, 0xe3,
|
||||
0x04, 0xdd, 0x80, 0x9a, 0x73, 0x8a, 0x5d, 0x97, 0x78, 0x63, 0x22, 0x6d, 0xac, 0x59, 0x33, 0x02,
|
||||
0x32, 0x61, 0x5d, 0x06, 0x4a, 0xaa, 0xa8, 0xf7, 0x1a, 0x2a, 0x1a, 0x9d, 0x83, 0x88, 0x66, 0x29,
|
||||
0x96, 0xf9, 0x9b, 0x01, 0xb7, 0x32, 0x4a, 0xfa, 0xdf, 0x84, 0x8c, 0xfc, 0xe7, 0xc1, 0xf9, 0x1f,
|
||||
0x5c, 0xc4, 0x42, 0x10, 0x2e, 0xc8, 0xc8, 0x1e, 0x61, 0x81, 0x75, 0x54, 0x1a, 0x31, 0xf1, 0x10,
|
||||
0x0b, 0x8c, 0x6e, 0x43, 0x03, 0x3b, 0x0e, 0xe1, 0x5c, 0xe9, 0xdf, 0x5e, 0x93, 0x0e, 0xd7, 0x15,
|
||||
0x4d, 0xc2, 0x99, 0x23, 0xd8, 0x2d, 0xf7, 0xe6, 0x9d, 0x05, 0xed, 0x31, 0xec, 0x65, 0xbd, 0x3c,
|
||||
0xd1, 0x89, 0x39, 0x88, 0x61, 0x12, 0x65, 0x4d, 0xb8, 0xc0, 0x7d, 0x37, 0x14, 0xd4, 0xf7, 0xa4,
|
||||
0xae, 0x86, 0x95, 0x7c, 0x9b, 0x7f, 0x1b, 0x70, 0xa7, 0x18, 0xe7, 0xc8, 0xa3, 0x82, 0x62, 0x37,
|
||||
0x8e, 0xce, 0x01, 0x34, 0xa2, 0x46, 0x79, 0xeb, 0x80, 0xd7, 0xa3, 0x53, 0x31, 0xc8, 0x0e, 0x9c,
|
||||
0x27, 0x13, 0x3b, 0x72, 0x40, 0x05, 0x77, 0x50, 0xb1, 0x36, 0xc8, 0x24, 0xf2, 0x0b, 0x5d, 0x87,
|
||||
0x0d, 0x32, 0xb1, 0x27, 0x64, 0x2a, 0x43, 0x1a, 0x71, 0xd6, 0xc9, 0xe4, 0x29, 0x99, 0xa2, 0x4f,
|
||||
0x01, 0xa9, 0x0c, 0xe0, 0xc8, 0x60, 0x3b, 0xc0, 0x0c, 0xbf, 0xe2, 0xdb, 0xe7, 0xa4, 0xfa, 0x5b,
|
||||
0x3a, 0x32, 0xcf, 0x4f, 0x8e, 0xfb, 0x33, 0x99, 0x93, 0x48, 0x84, 0x08, 0xc2, 0xb8, 0xb5, 0x85,
|
||||
0x73, 0x64, 0xbe, 0x7f, 0x0e, 0xaa, 0x64, 0x62, 0xfe, 0x9e, 0x6f, 0xc7, 0xc4, 0xef, 0xd8, 0xd6,
|
||||
0x3e, 0x9c, 0xa3, 0x1e, 0x8d, 0x1d, 0x7d, 0x4f, 0x6b, 0x5a, 0x25, 0x56, 0x83, 0x8a, 0x25, 0x8f,
|
||||
0x22, 0x1b, 0x50, 0x92, 0x54, 0x9b, 0xe9, 0x74, 0xe8, 0xa4, 0x76, 0x16, 0x02, 0xce, 0x25, 0x71,
|
||||
0x50, 0xb1, 0xb6, 0x9c, 0x3c, 0x71, 0xbf, 0x06, 0xe7, 0x03, 0x3c, 0x75, 0x7d, 0x3c, 0x32, 0x7f,
|
||||
0x30, 0x72, 0x9d, 0x9a, 0x72, 0x48, 0xd7, 0xc1, 0x27, 0xb0, 0x95, 0x36, 0x27, 0x9d, 0xc7, 0x9b,
|
||||
0xb3, 0x40, 0x3e, 0xf6, 0x1c, 0x36, 0x0d, 0x04, 0x19, 0x1d, 0x30, 0x22, 0x87, 0x01, 0x76, 0x07,
|
||||
0x15, 0xeb, 0x72, 0x4a, 0xb9, 0x8a, 0xcf, 0x9d, 0x05, 0x45, 0x1a, 0x65, 0x4f, 0x32, 0xd3, 0x16,
|
||||
0xfe, 0x68, 0xc0, 0x76, 0x59, 0xa2, 0xd0, 0x35, 0xd8, 0x08, 0xc2, 0xa1, 0x4b, 0x1d, 0x5d, 0xa1,
|
||||
0xfa, 0x0b, 0xdd, 0x82, 0xba, 0xc3, 0x08, 0x16, 0x24, 0xdd, 0x92, 0xa0, 0x48, 0xb2, 0x21, 0x1f,
|
||||
0x00, 0xd2, 0x02, 0xa9, 0x54, 0xab, 0x1a, 0xb2, 0xb6, 0x14, 0x27, 0xa5, 0x11, 0xdd, 0x87, 0xcb,
|
||||
0x5a, 0x9c, 0xd3, 0xb1, 0x87, 0x45, 0xc8, 0x88, 0xac, 0xa5, 0x86, 0x75, 0x49, 0xd1, 0x9f, 0xc5,
|
||||
0x64, 0xf3, 0x4b, 0xb8, 0x56, 0x1c, 0x0e, 0xb4, 0x07, 0x91, 0xb0, 0xfe, 0xb2, 0x87, 0xae, 0x3f,
|
||||
0xd4, 0x56, 0x6f, 0xce, 0xc8, 0xfb, 0xae, 0x3f, 0x8c, 0xbc, 0xe2, 0xc4, 0x61, 0x24, 0x9e, 0xb0,
|
||||
0xfa, 0xcb, 0xfc, 0xa9, 0x0a, 0x57, 0x3e, 0x63, 0xd8, 0x71, 0xa5, 0x3a, 0x92, 0xaa, 0xb9, 0xf3,
|
||||
0xa7, 0x04, 0x8f, 0x08, 0xe3, 0xdb, 0xc6, 0xee, 0x5a, 0xbb, 0xde, 0xdb, 0xd3, 0x51, 0x2d, 0x10,
|
||||
0xee, 0x0c, 0x94, 0xe4, 0x63, 0x4f, 0xb0, 0xa9, 0x15, 0x9f, 0x43, 0x5f, 0xc0, 0x25, 0x1d, 0x70,
|
||||
0x3b, 0x86, 0xaa, 0x4a, 0xa8, 0xce, 0x02, 0xa8, 0x13, 0x75, 0x22, 0x83, 0xb8, 0x19, 0x64, 0x88,
|
||||
0xcd, 0x47, 0xd0, 0x48, 0xf3, 0xd1, 0x65, 0x58, 0x8b, 0xba, 0x55, 0x0d, 0xaf, 0xe8, 0xbf, 0xe8,
|
||||
0x2a, 0xac, 0x9f, 0x61, 0x37, 0x54, 0x15, 0x5e, 0xb3, 0xd4, 0xc7, 0xa3, 0xea, 0x47, 0x46, 0xb3,
|
||||
0x0f, 0x57, 0x0a, 0x54, 0xbc, 0x0d, 0x84, 0xf9, 0xa7, 0x91, 0x1b, 0xab, 0xca, 0x8f, 0x6c, 0xcf,
|
||||
0x3a, 0xef, 0x66, 0x4b, 0x0c, 0x2a, 0x8b, 0xf6, 0xc4, 0x01, 0x6c, 0xfa, 0x52, 0x77, 0x66, 0x7d,
|
||||
0xd6, 0x7b, 0xcd, 0xf2, 0x00, 0x0f, 0x2a, 0xd6, 0x45, 0x75, 0x46, 0x13, 0xa2, 0xbe, 0xd0, 0xa7,
|
||||
0xcd, 0x10, 0x6e, 0x2f, 0x70, 0x4c, 0xf7, 0x6e, 0x6b, 0x6e, 0x61, 0x0c, 0x2a, 0xe9, 0x95, 0xb1,
|
||||
0x5a, 0x37, 0x02, 0x5c, 0x88, 0xc7, 0x50, 0x34, 0x01, 0xb3, 0x1b, 0x64, 0xdf, 0x0f, 0xbd, 0xd1,
|
||||
0x53, 0x32, 0x0d, 0x30, 0x65, 0xff, 0xc6, 0xf0, 0xef, 0xc0, 0x15, 0xaa, 0x60, 0x6d, 0x09, 0x96,
|
||||
0xea, 0x8c, 0x9a, 0xb5, 0xa5, 0x59, 0x4f, 0x7c, 0xea, 0x3d, 0x93, 0x8c, 0x48, 0x3e, 0x60, 0xe4,
|
||||
0x8c, 0xfa, 0x21, 0xd7, 0x07, 0x04, 0x16, 0x24, 0x6e, 0xed, 0x98, 0x25, 0x0f, 0x44, 0x0c, 0x73,
|
||||
0x00, 0xff, 0x2f, 0xf5, 0xe7, 0xed, 0x96, 0xe2, 0x13, 0xb8, 0x5f, 0x8a, 0x64, 0xf9, 0x6a, 0x92,
|
||||
0x24, 0x40, 0x37, 0x01, 0xd4, 0xac, 0xb2, 0x67, 0xb5, 0x5c, 0x53, 0x94, 0xa7, 0x64, 0x6a, 0xfe,
|
||||
0x52, 0x85, 0x7b, 0xa5, 0x60, 0xd9, 0xea, 0x3d, 0xcc, 0x6c, 0x9c, 0xc2, 0x05, 0x51, 0x9e, 0xa3,
|
||||
0x64, 0xe9, 0x0c, 0x17, 0x2c, 0x9d, 0x0f, 0x96, 0x61, 0xae, 0xb6, 0x77, 0x90, 0x0d, 0x5b, 0x4c,
|
||||
0xc7, 0x61, 0xa6, 0x62, 0x4d, 0xaa, 0x78, 0xb8, 0x4c, 0x45, 0x3e, 0x80, 0xd1, 0x72, 0x61, 0x39,
|
||||
0x5a, 0x7a, 0x6d, 0x60, 0x30, 0x97, 0x9b, 0xbb, 0x24, 0x0b, 0xd9, 0xfb, 0x56, 0x35, 0x77, 0xdf,
|
||||
0x32, 0xbf, 0x37, 0xe0, 0x6e, 0xb9, 0x0e, 0xc2, 0x04, 0xfd, 0x8a, 0x3a, 0x58, 0x10, 0x3e, 0xbb,
|
||||
0x99, 0x19, 0xa5, 0x37, 0xb3, 0xc8, 0x94, 0x54, 0xb9, 0xaa, 0xc1, 0x5f, 0x7b, 0x19, 0x97, 0x69,
|
||||
0xce, 0xd2, 0xb5, 0x7c, 0xbd, 0x7c, 0x67, 0x40, 0x7b, 0x85, 0xd8, 0xa9, 0x8a, 0x79, 0x01, 0x3b,
|
||||
0xc9, 0x1a, 0xb3, 0xb1, 0x3b, 0xf6, 0x19, 0x15, 0xa7, 0xaf, 0x6c, 0x1e, 0x52, 0xa1, 0xa6, 0xc4,
|
||||
0x66, 0xaf, 0xa5, 0x9b, 0x34, 0xd9, 0x6b, 0xfd, 0x58, 0xec, 0x59, 0x24, 0x65, 0x5d, 0xe7, 0xc5,
|
||||
0x0c, 0xf3, 0x75, 0x75, 0xc1, 0x7c, 0xc8, 0x4d, 0xa7, 0xa3, 0xfc, 0x74, 0xaa, 0xf7, 0xee, 0xaf,
|
||||
0x5c, 0x6a, 0xd9, 0x41, 0x76, 0x98, 0x1d, 0x64, 0xef, 0x2f, 0x85, 0x49, 0xa5, 0x27, 0x19, 0x74,
|
||||
0xe8, 0x18, 0x2e, 0xc4, 0x35, 0xa5, 0xeb, 0xb2, 0xbb, 0x7a, 0x5d, 0xc6, 0xfd, 0x94, 0x40, 0xa4,
|
||||
0xe7, 0x66, 0xef, 0xf5, 0x3a, 0xd4, 0xd5, 0x94, 0x92, 0x4f, 0x46, 0x44, 0xe1, 0x5a, 0xf1, 0x0b,
|
||||
0x09, 0xdd, 0x29, 0x52, 0x99, 0x7f, 0xf6, 0x35, 0xef, 0x2e, 0x91, 0x52, 0x6a, 0xdb, 0xc6, 0x43,
|
||||
0x03, 0xf9, 0xb0, 0x5d, 0xf6, 0xb2, 0x40, 0xf7, 0x8a, 0x60, 0xe6, 0x1f, 0x52, 0xcd, 0xbd, 0xa5,
|
||||
0x72, 0x29, 0x85, 0x79, 0xdf, 0x92, 0x3b, 0x65, 0xb1, 0x6f, 0xf9, 0x3b, 0x74, 0xb1, 0x6f, 0x73,
|
||||
0x17, 0x53, 0xa9, 0x8a, 0xc1, 0x4e, 0xe9, 0x16, 0x44, 0x85, 0x46, 0x17, 0x5c, 0x00, 0x9a, 0xed,
|
||||
0xe5, 0x82, 0x29, 0x9d, 0xdf, 0xe6, 0x1f, 0x9e, 0xf3, 0x25, 0x8e, 0x1e, 0x2c, 0xab, 0x9b, 0xac,
|
||||
0x01, 0x9d, 0x55, 0xc5, 0x53, 0x66, 0x1c, 0x02, 0x9a, 0x5f, 0xa1, 0x68, 0xe9, 0x76, 0x6d, 0x66,
|
||||
0xa6, 0xcf, 0xfe, 0xc7, 0xbf, 0xbe, 0x69, 0x19, 0x7f, 0xbc, 0x69, 0x19, 0x7f, 0xbd, 0x69, 0x19,
|
||||
0x2f, 0x7a, 0x63, 0x2a, 0x4e, 0xc3, 0x61, 0xc7, 0xf1, 0x5f, 0x75, 0xc7, 0x0c, 0x9f, 0x51, 0x55,
|
||||
0xc9, 0xd8, 0xed, 0x26, 0x3f, 0x55, 0xe0, 0x80, 0x66, 0x7e, 0xd1, 0x18, 0x6e, 0xc8, 0x3f, 0x1f,
|
||||
0xfe, 0x13, 0x00, 0x00, 0xff, 0xff, 0x83, 0x41, 0x97, 0x10, 0x2f, 0x11, 0x00, 0x00,
|
||||
}
|
||||
|
||||
func (m *RegisterUsingIAMMethodRequest) Marshal() (dAtA []byte, err error) {
|
||||
@@ -2826,6 +2837,11 @@ func (m *RegisterUsingBoundKeypairRotationRequest) MarshalToSizedBuffer(dAtA []b
|
||||
i -= len(m.XXX_unrecognized)
|
||||
copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
if m.SignatureAlgorithmSuite != 0 {
|
||||
i = encodeVarintJoinservice(dAtA, i, uint64(m.SignatureAlgorithmSuite))
|
||||
i--
|
||||
dAtA[i] = 0x8
|
||||
}
|
||||
return len(dAtA) - i, nil
|
||||
}
|
||||
|
||||
@@ -3473,6 +3489,9 @@ func (m *RegisterUsingBoundKeypairRotationRequest) Size() (n int) {
|
||||
}
|
||||
var l int
|
||||
_ = l
|
||||
if m.SignatureAlgorithmSuite != 0 {
|
||||
n += 1 + sovJoinservice(uint64(m.SignatureAlgorithmSuite))
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
@@ -6189,6 +6208,25 @@ func (m *RegisterUsingBoundKeypairRotationRequest) Unmarshal(dAtA []byte) error
|
||||
return fmt.Errorf("proto: RegisterUsingBoundKeypairRotationRequest: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field SignatureAlgorithmSuite", wireType)
|
||||
}
|
||||
m.SignatureAlgorithmSuite = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowJoinservice
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
m.SignatureAlgorithmSuite |= types.SignatureAlgorithmSuite(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipJoinservice(dAtA[iNdEx:])
|
||||
|
||||
@@ -276,7 +276,8 @@ message RegisterUsingBoundKeypairCertificates {
|
||||
// RegisterUsingBoundKeypairRotationRequest is the response sent by the server
|
||||
// when a keypair rotation is required.
|
||||
message RegisterUsingBoundKeypairRotationRequest {
|
||||
// This is a marker, no contents at this time.
|
||||
// The signature algorithm suite in use by the cluster.
|
||||
types.SignatureAlgorithmSuite signature_algorithm_suite = 1;
|
||||
}
|
||||
|
||||
// RegisterUsingBoundKeypairMethodResponse is a response sent by the server
|
||||
|
||||
@@ -20,8 +20,13 @@ package boundkeypair
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"golang.org/x/crypto/ssh"
|
||||
@@ -29,35 +34,60 @@ import (
|
||||
"github.com/gravitational/teleport/api/utils/keys"
|
||||
"github.com/gravitational/teleport/lib/auth/join"
|
||||
"github.com/gravitational/teleport/lib/cryptosuites"
|
||||
"github.com/gravitational/teleport/lib/sshutils"
|
||||
)
|
||||
|
||||
const (
|
||||
PrivateKeyPath = "id_bkp"
|
||||
PublicKeyPath = PrivateKeyPath + ".pub"
|
||||
JoinStatePath = "bkp_state"
|
||||
PrivateKeyPath = "id_bkp"
|
||||
PublicKeyPath = PrivateKeyPath + ".pub"
|
||||
JoinStatePath = "bkp_state"
|
||||
KeyHistoryPath = "bkp_key_history.json"
|
||||
KeyHistoryLength = 10
|
||||
|
||||
StandardFileWriteMode = 0600
|
||||
)
|
||||
|
||||
// KeyHistoryEntry records a private key and the timestamp it was generated.
|
||||
type KeyHistoryEntry struct {
|
||||
// Time is the time this key was inserted into the history.
|
||||
Time time.Time `json:"time"`
|
||||
|
||||
// PrivateKey is the private key, encoded in PEM format.
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
|
||||
// KeyHistory is a collection of `KeyHistoryEntry`.
|
||||
type KeyHistory struct {
|
||||
Entries []KeyHistoryEntry `json:"entries"`
|
||||
}
|
||||
|
||||
// ClientState contains state parameters stored on disk needed to complete the
|
||||
// bound keypair join process.
|
||||
type ClientState struct {
|
||||
mu sync.Mutex
|
||||
fs FS
|
||||
|
||||
// PrivateKey is the parsed private key.
|
||||
PrivateKey *keys.PrivateKey
|
||||
|
||||
// PrivateKeyBytes contains the private key bytes. This value should always
|
||||
// be nonempty.
|
||||
// PrivateKeyBytes contains the active private key bytes. This value should
|
||||
// always be nonempty.
|
||||
PrivateKeyBytes []byte
|
||||
|
||||
// PublicKeyBytes contains the public key bytes. This value is not used at
|
||||
// runtime, and is only set when a public key should be written to disk,
|
||||
// like on first creation or during rotation. To consistently access the
|
||||
// public key, use `.PrivateKey.Public()`.
|
||||
// PublicKeyBytes contains the active public key bytes. This value is not
|
||||
// used at runtime, and is only set when a public key should be written to
|
||||
// disk, like on first creation or during rotation. To consistently access
|
||||
// the public key, use `.PrivateKey.Public()`.
|
||||
PublicKeyBytes []byte
|
||||
|
||||
// JoinStateBytes contains join state bytes. This value will be empty if
|
||||
// this client has not yet joined.
|
||||
JoinStateBytes []byte
|
||||
|
||||
// KeyHistory records previous keypairs. In the event of a cluster rollback,
|
||||
// this history will allow clients to rejoin if the cluster requests a
|
||||
// keypair not currently marked as active.
|
||||
KeyHistory []KeyHistoryEntry
|
||||
}
|
||||
|
||||
// ToJoinParams creates joining parameters for use with `join.Register()` from
|
||||
@@ -70,11 +100,28 @@ func (c *ClientState) ToJoinParams(initialJoinSecret string) *join.BoundKeypairP
|
||||
}
|
||||
|
||||
return &join.BoundKeypairParams{
|
||||
// Note: pass the internal signer because go-jose does type assertions
|
||||
// on the standard library types.
|
||||
CurrentKey: c.PrivateKey.Signer,
|
||||
PreviousJoinState: c.JoinStateBytes,
|
||||
InitialJoinSecret: initialJoinSecret,
|
||||
GetSigner: func(pubKey string) (crypto.Signer, error) {
|
||||
return c.SignerForPublicKey([]byte(pubKey))
|
||||
},
|
||||
RequestNewKeypair: func(ctx context.Context, getSuite cryptosuites.GetSuiteFunc) (crypto.Signer, error) {
|
||||
signer, err := c.GenerateKeypair(ctx, getSuite)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Make sure to store the intermediate state. We don't want to risk
|
||||
// losing a private key if an error occurs between here and the end
|
||||
// of the join process, but also don't want to force
|
||||
// `GenerateKeypair()` to trigger a `Store()` on every call, so it's
|
||||
// reasonably done here.
|
||||
if err := c.Store(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return signer, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,15 +131,26 @@ func (c *ClientState) UpdateFromRegisterResult(result *join.RegisterResult) erro
|
||||
return trace.BadParameter("register result is missing bound keypair parameters")
|
||||
}
|
||||
|
||||
c.JoinStateBytes = result.BoundKeypair.JoinState
|
||||
signer, err := c.SignerForPublicKey([]byte(result.BoundKeypair.BoundPublicKey))
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "fetching key requested by auth")
|
||||
}
|
||||
|
||||
// TODO: When implementing rotation, use the bound public key value to set
|
||||
// the current public key.
|
||||
if err := c.SetActiveKey(signer); err != nil {
|
||||
return trace.Wrap(err, "setting new active key")
|
||||
}
|
||||
|
||||
// The helpers above may lock the mutex, so don't lock it until we're
|
||||
// touching fields directly.
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.JoinStateBytes = result.BoundKeypair.JoinState
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToPublicKeyBytes returns the public key bytes in ssh authorized_keys format.
|
||||
// ToPublicKeyBytes returns the active public key in ssh authorized_keys format.
|
||||
func (c *ClientState) ToPublicKeyBytes() ([]byte, error) {
|
||||
sshPubKey, err := ssh.NewPublicKey(c.PrivateKey.Public())
|
||||
if err != nil {
|
||||
@@ -102,6 +160,148 @@ func (c *ClientState) ToPublicKeyBytes() ([]byte, error) {
|
||||
return ssh.MarshalAuthorizedKey(sshPubKey), nil
|
||||
}
|
||||
|
||||
// pubKeyEqual compares the two public keys per their `Equal()` implementation.
|
||||
func pubKeyEqual(a, b crypto.PublicKey) (bool, error) {
|
||||
aEq, ok := a.(interface {
|
||||
Equal(x crypto.PublicKey) bool
|
||||
})
|
||||
if !ok {
|
||||
return false, trace.BadParameter("unsupported key type %T", a)
|
||||
}
|
||||
|
||||
return aEq.Equal(b), nil
|
||||
}
|
||||
|
||||
// SignerForPublicKey attempts to resolve a signer for the given public key
|
||||
// encoded in authorized_keys format.
|
||||
func (c *ClientState) SignerForPublicKey(authorizedKeysBytes []byte) (crypto.Signer, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
desiredPubKey, err := sshutils.CryptoPublicKey(authorizedKeysBytes)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Check the active key first.
|
||||
activePubKeyBytes, err := c.ToPublicKeyBytes()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
equal, err := pubKeyEqual(desiredPubKey, activePubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
} else if equal {
|
||||
// Parse a fresh copy of the key since this will escape the mutex and we
|
||||
// can't be sure our local copy is thread safe.
|
||||
key, err := keys.ParsePrivateKey(c.PrivateKeyBytes)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return key.Signer, nil
|
||||
}
|
||||
|
||||
// Otherwise, search through the key history. If a keypair rotation was
|
||||
// requested an `GenerateKeypair` was called, the new keypair should have
|
||||
// been inserted at the top of this list.
|
||||
for _, entry := range c.KeyHistory {
|
||||
pk, err := keys.ParsePrivateKey([]byte(entry.PrivateKey))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
equal, err := pubKeyEqual(desiredPubKey, pk.Signer.Public())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if equal {
|
||||
return pk.Signer, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, trace.NotFound("no matching key found")
|
||||
}
|
||||
|
||||
// GenerateKeypair generates a new keypair, adds it to the key history, and
|
||||
// returns the resulting signer signer.
|
||||
func (c *ClientState) GenerateKeypair(ctx context.Context, getSuite cryptosuites.GetSuiteFunc) (crypto.Signer, error) {
|
||||
key, err := cryptosuites.GenerateKey(ctx, getSuite, cryptosuites.BoundKeypairJoining)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "generating keypair")
|
||||
}
|
||||
|
||||
privateKeyBytes, err := keys.MarshalPrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "marshallng private key")
|
||||
}
|
||||
|
||||
// prepend the new key to the top of the list for faster lookup
|
||||
c.KeyHistory = append([]KeyHistoryEntry{{
|
||||
Time: time.Now(),
|
||||
PrivateKey: string(privateKeyBytes),
|
||||
}}, c.KeyHistory...)
|
||||
|
||||
// Trim if necessary.
|
||||
if len(c.KeyHistory) > KeyHistoryLength {
|
||||
c.KeyHistory = c.KeyHistory[:min(len(c.KeyHistory), KeyHistoryLength)]
|
||||
}
|
||||
|
||||
sshPubKey, err := ssh.NewPublicKey(key.Public())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "creating ssh public key")
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "Generated new keypair", "public_key", string(ssh.MarshalAuthorizedKey(sshPubKey)))
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// SetActiveKey updates the active keypair to reflect the given signer. Has no
|
||||
// effect if the active keypair's public key is already equal to the given
|
||||
// signer's public key, per its `Equals` implementation. Note that
|
||||
// `StoreClientState` still must be called after this to commit the changes to
|
||||
// the storage backend.
|
||||
func (c *ClientState) SetActiveKey(signer crypto.Signer) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
equal, err := pubKeyEqual(signer.Public(), c.PrivateKey.Public())
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if equal {
|
||||
// nothing to do; specified key is already the active key
|
||||
return nil
|
||||
}
|
||||
|
||||
key, err := keys.NewPrivateKey(signer)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
privateKeyBytes, err := keys.MarshalPrivateKey(key.Signer)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "marshallng private key")
|
||||
}
|
||||
|
||||
sshPubKey, err := ssh.NewPublicKey(key.Public())
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "creating ssh public key")
|
||||
}
|
||||
|
||||
c.PrivateKey = key
|
||||
c.PrivateKeyBytes = privateKeyBytes
|
||||
c.PublicKeyBytes = ssh.MarshalAuthorizedKey(sshPubKey)
|
||||
|
||||
slog.InfoContext(context.Background(), "Set new active keypair", "public_key", string(c.PublicKeyBytes))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type FS interface {
|
||||
Read(ctx context.Context, name string) ([]byte, error)
|
||||
Write(ctx context.Context, name string, data []byte) error
|
||||
@@ -133,6 +333,16 @@ func NewStandardFS(parentDir string) FS {
|
||||
}
|
||||
}
|
||||
|
||||
// parseKeyHistory parses marshaled key history from JSON bytes
|
||||
func parseKeyHistory(data []byte) (KeyHistory, error) {
|
||||
var history KeyHistory
|
||||
if err := json.Unmarshal(data, &history); err != nil {
|
||||
return KeyHistory{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return history, nil
|
||||
}
|
||||
|
||||
// LoadClientState attempts to load bound keypair client state from the given
|
||||
// filesystem implementation. Callers should expect to handle NotFound errors
|
||||
// returned here if a private key is not found; this indicates no prior client
|
||||
@@ -158,46 +368,83 @@ func LoadClientState(ctx context.Context, fs FS) (*ClientState, error) {
|
||||
return nil, trace.Wrap(err, "parsing private key")
|
||||
}
|
||||
|
||||
return &ClientState{
|
||||
PrivateKey: pk,
|
||||
var keyHistory KeyHistory
|
||||
keyHistoryBytes, err := fs.Read(ctx, KeyHistoryPath)
|
||||
if trace.IsNotFound(err) {
|
||||
// No history, this is allowed.
|
||||
} else if err != nil {
|
||||
slog.WarnContext(ctx, "unable to read key history, may be unable to recover in the event of a cluster rollback", "error", err)
|
||||
} else if len(keyHistoryBytes) > 0 {
|
||||
keyHistory, err = parseKeyHistory(keyHistoryBytes)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx, "unable to parse key history, may be unable to recover in the event of a cluster rollback", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If the key history is empty, initialize it with just the current key.
|
||||
if len(keyHistory.Entries) == 0 {
|
||||
keyHistory.Entries = []KeyHistoryEntry{{
|
||||
Time: time.Now(),
|
||||
PrivateKey: string(privateKeyBytes),
|
||||
}}
|
||||
}
|
||||
|
||||
return &ClientState{
|
||||
fs: fs,
|
||||
|
||||
PrivateKey: pk,
|
||||
PrivateKeyBytes: privateKeyBytes,
|
||||
JoinStateBytes: joinStateBytes,
|
||||
KeyHistory: keyHistory.Entries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StoreClientState writes bound keypair client state to the given filesystem
|
||||
// wrapper. Public keys and join state will only be written if
|
||||
func StoreClientState(ctx context.Context, fs FS, state *ClientState) error {
|
||||
if err := fs.Write(ctx, PrivateKeyPath, state.PrivateKeyBytes); err != nil {
|
||||
func (c *ClientState) Store(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if err := c.fs.Write(ctx, PrivateKeyPath, c.PrivateKeyBytes); err != nil {
|
||||
return trace.Wrap(err, "writing private key")
|
||||
}
|
||||
|
||||
// TODO: maybe consider just not writing the public key at all. End users
|
||||
// aren't really meant to look in the internal storage, and we can just
|
||||
// derive the public key whenever we want.
|
||||
|
||||
// Only write the public key if it was explicitly provided. This helps save
|
||||
// an unnecessary file write.
|
||||
if len(state.PublicKeyBytes) > 0 {
|
||||
if err := fs.Write(ctx, PublicKeyPath, state.PublicKeyBytes); err != nil {
|
||||
if len(c.PublicKeyBytes) > 0 {
|
||||
if err := c.fs.Write(ctx, PublicKeyPath, c.PublicKeyBytes); err != nil {
|
||||
return trace.Wrap(err, "writing public key")
|
||||
}
|
||||
}
|
||||
|
||||
if len(state.JoinStateBytes) > 0 {
|
||||
if err := fs.Write(ctx, JoinStatePath, state.JoinStateBytes); err != nil {
|
||||
if len(c.JoinStateBytes) > 0 {
|
||||
if err := c.fs.Write(ctx, JoinStatePath, c.JoinStateBytes); err != nil {
|
||||
return trace.Wrap(err, "writing previous join state")
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.KeyHistory) > 0 {
|
||||
bytes, err := json.Marshal(KeyHistory{
|
||||
Entries: c.KeyHistory,
|
||||
})
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "marshaling key key history")
|
||||
}
|
||||
|
||||
if err := c.fs.Write(ctx, KeyHistoryPath, bytes); err != nil {
|
||||
return trace.Wrap(err, "writing key history")
|
||||
}
|
||||
}
|
||||
|
||||
slog.DebugContext(ctx, "stored new bound keypair client state")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewUnboundClientState creates a new client state that has not yet been bound,
|
||||
// i.e. a new keypair that has not been registered with Auth, and no prior join
|
||||
// state.
|
||||
func NewUnboundClientState(ctx context.Context, getSuite cryptosuites.GetSuiteFunc) (*ClientState, error) {
|
||||
func NewUnboundClientState(ctx context.Context, fs FS, getSuite cryptosuites.GetSuiteFunc) (*ClientState, error) {
|
||||
key, err := cryptosuites.GenerateKey(ctx, getSuite, cryptosuites.BoundKeypairJoining)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "generating keypair")
|
||||
@@ -220,9 +467,19 @@ func NewUnboundClientState(ctx context.Context, getSuite cryptosuites.GetSuiteFu
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
history := []KeyHistoryEntry{
|
||||
{
|
||||
Time: time.Now(),
|
||||
PrivateKey: string(privateKeyBytes),
|
||||
},
|
||||
}
|
||||
|
||||
return &ClientState{
|
||||
fs: fs,
|
||||
|
||||
PrivateKeyBytes: privateKeyBytes,
|
||||
PublicKeyBytes: publicKeyBytes,
|
||||
PrivateKey: pk,
|
||||
KeyHistory: history,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2025 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package boundkeypair
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/cryptosuites"
|
||||
)
|
||||
|
||||
// memoryFS is a trivial in memory fs backend for testing use.
|
||||
type memoryFS struct {
|
||||
files map[string][]byte
|
||||
writes uint
|
||||
}
|
||||
|
||||
func (f *memoryFS) Read(ctx context.Context, name string) ([]byte, error) {
|
||||
data, ok := f.files[name]
|
||||
if !ok {
|
||||
return nil, trace.NotFound("not found: %s", name)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (f *memoryFS) Write(ctx context.Context, name string, data []byte) error {
|
||||
f.writes++
|
||||
f.files[name] = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestClientState(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := &memoryFS{
|
||||
files: map[string][]byte{},
|
||||
}
|
||||
|
||||
getSuite := cryptosuites.StaticAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1)
|
||||
|
||||
state, err := NewUnboundClientState(ctx, fs, getSuite)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Nothing should be written until `Store` is called explicitly
|
||||
require.Empty(t, fs.files)
|
||||
|
||||
require.NoError(t, state.Store(ctx))
|
||||
require.Len(t, fs.files, 3)
|
||||
require.Len(t, state.KeyHistory, 1)
|
||||
require.EqualValues(t, 3, fs.writes)
|
||||
|
||||
require.Equal(t, fs.files[PrivateKeyPath], state.PrivateKeyBytes)
|
||||
|
||||
// Keep the original key for later.
|
||||
firstKey := state.PublicKeyBytes
|
||||
|
||||
prevKey := firstKey
|
||||
expectWrites := 3
|
||||
|
||||
// Simulate writes up to the key history recording length.
|
||||
for i := 0; i < KeyHistoryLength-1; i++ {
|
||||
// We should still be able to load the original signer (< KeyHistoryLength)
|
||||
_, err := state.SignerForPublicKey(firstKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Similarly, the previous key should still be accessible.
|
||||
_, err = state.SignerForPublicKey(prevKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
prevKey = state.PrivateKey.MarshalSSHPublicKey()
|
||||
|
||||
// Generate a new keypair. It should be added to the history, but not marked
|
||||
// as active.
|
||||
signer, err := state.GenerateKeypair(ctx, getSuite)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, state.KeyHistory, 2+i)
|
||||
require.EqualValues(t, expectWrites, fs.writes, "no new writes expected")
|
||||
require.Equal(t, fs.files[PrivateKeyPath], state.PrivateKeyBytes, "active key should not change on generation")
|
||||
require.NotEqual(t, state.KeyHistory[0].PrivateKey, state.KeyHistory[1].PrivateKey)
|
||||
|
||||
// Now explicitly set the new active key
|
||||
require.NoError(t, state.SetActiveKey(signer))
|
||||
require.NotEqual(t, state.PublicKeyBytes, prevKey, "public key bytes must change for new key")
|
||||
|
||||
require.NoError(t, state.Store(ctx))
|
||||
expectWrites += 3
|
||||
|
||||
// Load a fresh state for the next iteration.
|
||||
state, err = LoadClientState(ctx, fs)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Generate a final keypair. This should push out the initial key. We'll
|
||||
// have reached the history limit, so the total length should not change.
|
||||
require.Len(t, state.KeyHistory, 10)
|
||||
_, err = state.GenerateKeypair(ctx, getSuite)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, state.KeyHistory, 10)
|
||||
|
||||
require.NoError(t, state.Store(ctx))
|
||||
state, err = LoadClientState(ctx, fs)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to load the original key again; it should fail.
|
||||
_, err = state.SignerForPublicKey(firstKey)
|
||||
require.Error(t, err)
|
||||
}
|
||||
+45
-33
@@ -85,20 +85,34 @@ type GitlabParams struct {
|
||||
EnvVarName string
|
||||
}
|
||||
|
||||
// GetSignerFunc is a function that fetches a keypair from bound keypair client
|
||||
// state.
|
||||
type GetSignerFunc func(pubKey string) (crypto.Signer, error)
|
||||
|
||||
// KeygenFunc is a function to generate a new keypair for bound keypair joining.
|
||||
// Clients will generally need to store this for future use, so this function
|
||||
// should include some mechanism for storage and retrieval.
|
||||
type KeygenFunc func(ctx context.Context, getSuite cryptosuites.GetSuiteFunc) (crypto.Signer, error)
|
||||
|
||||
// BoundKeypairParams are parameters specific to bound-keypair joining.
|
||||
type BoundKeypairParams struct {
|
||||
// InitialJoinSecret is a one-time-use joining token for use on first join.
|
||||
// May be unset if a keypair was registered with Auth out of band.
|
||||
InitialJoinSecret string
|
||||
|
||||
// CurrentKey is the keypair currently registered with Auth. On initial
|
||||
// join using `InitialJoinSecret`, this should be nil in favor of `NewKey`.
|
||||
CurrentKey crypto.Signer
|
||||
|
||||
// PreviousJoinState is the previous join state document provided by Auth
|
||||
// alongside the previous set of certs. If this is initial registration, it
|
||||
// can be empty.
|
||||
PreviousJoinState []byte
|
||||
|
||||
// GetSigner is a function that fetches a signer from the client keystore.
|
||||
GetSigner GetSignerFunc
|
||||
|
||||
// RequestNewKeypair is a callback function used to request a new keypair.
|
||||
// This may be called at initial onboarding when `InitialJoinSecret` is set,
|
||||
// or on any join (including the initial join) if `RotateAfter` is set on
|
||||
// the backing token and its value has elapsed.
|
||||
RequestNewKeypair KeygenFunc
|
||||
}
|
||||
|
||||
// RegisterParams specifies parameters
|
||||
@@ -947,47 +961,23 @@ func registerUsingBoundKeypairMethod(
|
||||
) (*RegisterResult, error) {
|
||||
bkParams := params.BoundKeypairParams
|
||||
|
||||
// Build a map of all public keys to signers. At the moment, this is just
|
||||
// the current key, but may include e.g. previous and next keys for use in
|
||||
// case of a failed rotation attempt.
|
||||
// TODO: This implementation is likely to change when rotation is
|
||||
// implemented.
|
||||
signers := map[string]crypto.Signer{}
|
||||
|
||||
if bkParams.CurrentKey != nil {
|
||||
pub, err := sshPubKeyFromSigner(bkParams.CurrentKey)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "generating ssh public key from current key signer")
|
||||
}
|
||||
|
||||
signers[pub] = bkParams.CurrentKey
|
||||
}
|
||||
|
||||
initReq := &proto.RegisterUsingBoundKeypairInitialRequest{
|
||||
JoinRequest: registerUsingTokenRequestForParams(token, hostKeys, params),
|
||||
InitialJoinSecret: bkParams.InitialJoinSecret,
|
||||
PreviousJoinState: bkParams.PreviousJoinState,
|
||||
}
|
||||
|
||||
// TODO: When implementing rotation, we should make use of the returned
|
||||
// public key to ensure that key is marked as the primary.
|
||||
regResponse, err := client.RegisterUsingBoundKeypairMethod(
|
||||
ctx,
|
||||
initReq,
|
||||
func(resp *proto.RegisterUsingBoundKeypairMethodResponse) (*proto.RegisterUsingBoundKeypairMethodRequest, error) {
|
||||
switch kind := resp.GetResponse().(type) {
|
||||
case *proto.RegisterUsingBoundKeypairMethodResponse_Challenge:
|
||||
// Unlike other join methods, this function may be called multiple
|
||||
// times to complete challenges using one or both signers, so we'll
|
||||
// use the passed publicKey hint to resolve the proper signer to
|
||||
// use.
|
||||
signer, ok := signers[kind.Challenge.PublicKey]
|
||||
if !ok {
|
||||
return nil, trace.NotFound("could not complete challenge for unknown public key: %+#v", kind.Challenge.PublicKey)
|
||||
signer, err := bkParams.GetSigner(kind.Challenge.PublicKey)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "could not lookup signer for public key %+v", kind.Challenge.PublicKey)
|
||||
}
|
||||
|
||||
// TODO: might not be worth exporting this func; may be cheaper to
|
||||
// just copy the function here instead.
|
||||
alg, err := jwt.AlgorithmForPublicKey(signer.Public())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "determining signing algorithm for public key")
|
||||
@@ -1022,8 +1012,29 @@ func registerUsingBoundKeypairMethod(
|
||||
},
|
||||
}, nil
|
||||
case *proto.RegisterUsingBoundKeypairMethodResponse_Rotation:
|
||||
// TODO: Follow up implementation
|
||||
return nil, trace.NotImplemented("keypair rotation not yet implemented")
|
||||
if bkParams.RequestNewKeypair == nil {
|
||||
return nil, trace.BadParameter("RequestNewKeypair is required")
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "Server has requested keypair rotation", "suite", kind.Rotation.SignatureAlgorithmSuite)
|
||||
|
||||
newSigner, err := bkParams.RequestNewKeypair(ctx, cryptosuites.StaticAlgorithmSuite(kind.Rotation.SignatureAlgorithmSuite))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "requesting new keypair")
|
||||
}
|
||||
|
||||
newPubkey, err := sshPubKeyFromSigner(newSigner)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &proto.RegisterUsingBoundKeypairMethodRequest{
|
||||
Payload: &proto.RegisterUsingBoundKeypairMethodRequest_RotationResponse{
|
||||
RotationResponse: &proto.RegisterUsingBoundKeypairRotationResponse{
|
||||
PublicKey: newPubkey,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
// Note: certs variant is handled by RegisterUsingBoundKeypairMethod()
|
||||
return nil, trace.BadParameter("received unexpected challenge response: %v", resp.GetResponse())
|
||||
@@ -1033,6 +1044,7 @@ func registerUsingBoundKeypairMethod(
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Implementation note, callers are expected to call
|
||||
return &RegisterResult{
|
||||
PrivateKey: hostKeys.privateKey,
|
||||
Certs: regResponse.Certs,
|
||||
|
||||
+160
-16
@@ -52,10 +52,6 @@ func validateBoundKeypairTokenSpec(spec *types.ProvisionTokenSpecV2BoundKeypair)
|
||||
return trace.BadParameter("bound keypair joining experiment is not enabled")
|
||||
}
|
||||
|
||||
if spec.RotateAfter != nil {
|
||||
return trace.NotImplemented("spec.bound_keypair.rotate_after is not yet implemented")
|
||||
}
|
||||
|
||||
if spec.Onboarding.RegistrationSecret != "" {
|
||||
return trace.NotImplemented("spec.bound_keypair.onboarding.registration_secret is not yet implemented")
|
||||
}
|
||||
@@ -133,10 +129,10 @@ func (a *Server) UpsertBoundKeypairToken(ctx context.Context, token types.Provis
|
||||
// response.
|
||||
func (a *Server) issueBoundKeypairChallenge(
|
||||
ctx context.Context,
|
||||
marshalledKey string,
|
||||
marshaledKey string,
|
||||
challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc,
|
||||
) error {
|
||||
key, err := libsshutils.CryptoPublicKey([]byte(marshalledKey))
|
||||
key, err := libsshutils.CryptoPublicKey([]byte(marshaledKey))
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "parsing bound public key")
|
||||
}
|
||||
@@ -175,7 +171,7 @@ func (a *Server) issueBoundKeypairChallenge(
|
||||
response, err := challengeResponse(&proto.RegisterUsingBoundKeypairMethodResponse{
|
||||
Response: &proto.RegisterUsingBoundKeypairMethodResponse_Challenge{
|
||||
Challenge: &proto.RegisterUsingBoundKeypairChallenge{
|
||||
PublicKey: marshalledKey,
|
||||
PublicKey: marshaledKey,
|
||||
Challenge: string(marshalledChallenge),
|
||||
},
|
||||
},
|
||||
@@ -202,6 +198,98 @@ func (a *Server) issueBoundKeypairChallenge(
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldRequestBoundKeypairRotation determines if a keypair rotation should be
|
||||
// requested given configured token field values.
|
||||
func shouldRequestBoundKeypairRotation(rotateAfter, lastRotatedAt *time.Time, now time.Time) bool {
|
||||
if rotateAfter == nil {
|
||||
// Field not set, nothing to do.
|
||||
return false
|
||||
}
|
||||
|
||||
if rotateAfter.After(now) {
|
||||
// We haven't reached the rotation threshold, nothing to do.
|
||||
return false
|
||||
}
|
||||
|
||||
if lastRotatedAt == nil {
|
||||
// There has not been a previous rotation, so rotate now.
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise, rotate only if a rotation hasn't already taken place, i.e.
|
||||
// `lastRotatedAt` is before the requested timestamp
|
||||
return lastRotatedAt.Before(*rotateAfter)
|
||||
}
|
||||
|
||||
// ensurePublicKeysNotEqual ensures the two public keys, in ssh authorized_keys
|
||||
// format, are parseable public keys and are not equal to one another. This can
|
||||
// be used to validate that clients actually provided a new key after receiving
|
||||
// a rotation request.
|
||||
func ensurePublicKeysNotEqual(a, b string) error {
|
||||
aParsed, err := libsshutils.CryptoPublicKey([]byte(a))
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
aEq, ok := aParsed.(interface {
|
||||
Equal(x crypto.PublicKey) bool
|
||||
})
|
||||
if !ok {
|
||||
return trace.BadParameter("invalid public key type %T", aParsed)
|
||||
}
|
||||
|
||||
bParsed, err := libsshutils.CryptoPublicKey([]byte(b))
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if aEq.Equal(bParsed) {
|
||||
return trace.BadParameter("public key may not be reused after rotation")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// requestBoundKeypairRotation requests that clients generate a new keypair and
|
||||
// send the public key, then issues a signing challenge to ensure ownership of
|
||||
// the new key.
|
||||
func (a *Server) requestBoundKeypairRotation(
|
||||
ctx context.Context,
|
||||
challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc,
|
||||
) (string, error) {
|
||||
cap, err := a.GetAuthPreference(ctx)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
a.logger.InfoContext(ctx, "requesting bound keypair rotation", "suite", cap.GetSignatureAlgorithmSuite())
|
||||
|
||||
// Request a new marshaled public key from the client.
|
||||
response, err := challengeResponse(&proto.RegisterUsingBoundKeypairMethodResponse{
|
||||
Response: &proto.RegisterUsingBoundKeypairMethodResponse_Rotation{
|
||||
Rotation: &proto.RegisterUsingBoundKeypairRotationRequest{
|
||||
SignatureAlgorithmSuite: cap.GetSignatureAlgorithmSuite(),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err, "requesting a new public key")
|
||||
}
|
||||
|
||||
pubKeyResponse, ok := response.Payload.(*proto.RegisterUsingBoundKeypairMethodRequest_RotationResponse)
|
||||
if !ok {
|
||||
return "", trace.BadParameter("client provided unexpected keypair request response type %T", response.Payload)
|
||||
}
|
||||
|
||||
// Issue a challenge against this new key to ensure ownership.
|
||||
pubKey := pubKeyResponse.RotationResponse.PublicKey
|
||||
if err := a.issueBoundKeypairChallenge(ctx, pubKey, challengeResponse); err != nil {
|
||||
return "", trace.Wrap(err, "solving challenge for new public key")
|
||||
}
|
||||
|
||||
return pubKey, nil
|
||||
}
|
||||
|
||||
// boundKeypairStatusMutator is a function called to mutate a bound keypair
|
||||
// status during a call to PatchProvisionToken(). These functions may be called
|
||||
// repeatedly if e.g. revision checks fail. To ensure invariants remain in
|
||||
@@ -266,6 +354,38 @@ func mutateStatusBoundBotInstance(newBotInstance, expectPreviousBotInstance stri
|
||||
}
|
||||
}
|
||||
|
||||
// mutateStatusLastRotatedAt updates the `status.LastRotatedAt` field to
|
||||
// indicate a keypair rotation has taken place. It ensures the previous value
|
||||
// has not changed before performing the update.
|
||||
func mutateStatusLastRotatedAt(newValue, expectPrevValue *time.Time) boundKeypairStatusMutator {
|
||||
return func(_ *types.ProvisionTokenSpecV2BoundKeypair, status *types.ProvisionTokenStatusV2BoundKeypair) error {
|
||||
switch {
|
||||
case expectPrevValue == nil && status.LastRotatedAt == nil:
|
||||
// no issue
|
||||
case expectPrevValue != nil && status.LastRotatedAt == nil:
|
||||
fallthrough
|
||||
case expectPrevValue == nil && status.LastRotatedAt != nil:
|
||||
fallthrough
|
||||
case !expectPrevValue.Equal(*status.LastRotatedAt):
|
||||
return trace.AccessDenied("unexpected backend state")
|
||||
}
|
||||
|
||||
status.LastRotatedAt = newValue
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// formatTimePointer stringifies a *time.Time for logging, but gracefully
|
||||
// handles nil values.
|
||||
func formatTimePointer(t *time.Time) string {
|
||||
if t == nil {
|
||||
return "nil"
|
||||
}
|
||||
|
||||
return t.String()
|
||||
}
|
||||
|
||||
// RegisterUsingBoundKeypairMethod handles joining requests for the bound
|
||||
// keypair join method. If successful, returns
|
||||
func (a *Server) RegisterUsingBoundKeypairMethod(
|
||||
@@ -308,6 +428,8 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
|
||||
return nil, trace.BadParameter("specified join token is not for `%s` method", types.JoinMethodBoundKeypair)
|
||||
}
|
||||
|
||||
log := a.logger.With("token", ptv2.GetName())
|
||||
|
||||
if ptv2.Status == nil {
|
||||
ptv2.Status = &types.ProvisionTokenStatusV2{}
|
||||
}
|
||||
@@ -363,7 +485,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
|
||||
return nil, trace.AccessDenied("previous join state is required but was not provided")
|
||||
}
|
||||
|
||||
a.logger.DebugContext(ctx, "join state verification required, verifying")
|
||||
log.DebugContext(ctx, "join state verification required, verifying")
|
||||
joinState, err = boundkeypair.VerifyJoinState(
|
||||
ca,
|
||||
string(req.PreviousJoinState),
|
||||
@@ -377,7 +499,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
|
||||
// TODO: Once we have token-specific locking, generate a lock; this
|
||||
// indicates the keypair may have been compromised.
|
||||
// TODO: Audit log event for this.
|
||||
a.logger.ErrorContext(ctx, "bound keypair join state verification failed", "error", err)
|
||||
log.ErrorContext(ctx, "bound keypair join state verification failed", "error", err)
|
||||
return nil, trace.AccessDenied("join state verification failed")
|
||||
}
|
||||
|
||||
@@ -386,7 +508,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
|
||||
// instance is generated.
|
||||
req.JoinRequest.PreviousBotInstanceID = joinState.BotInstanceID
|
||||
|
||||
a.logger.DebugContext(ctx, "join state verified successfully", "join_state", joinState)
|
||||
log.DebugContext(ctx, "join state verified successfully", "join_state", joinState)
|
||||
|
||||
// Note: we don't verify join state if it isn't expected. This is partly
|
||||
// to ensure server-side recovery will work if join state desyncs - a
|
||||
@@ -445,7 +567,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
|
||||
|
||||
if err := a.issueBoundKeypairChallenge(
|
||||
ctx,
|
||||
spec.Onboarding.InitialPublicKey,
|
||||
status.BoundPublicKey,
|
||||
challengeResponse,
|
||||
); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
@@ -475,7 +597,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
|
||||
|
||||
expectNewBotInstance = true
|
||||
default:
|
||||
a.logger.ErrorContext(
|
||||
log.ErrorContext(
|
||||
ctx, "unexpected state",
|
||||
"has_bound_public_key", hasBoundPublicKey,
|
||||
"has_bound_bot_instance", hasBoundBotInstance,
|
||||
@@ -486,10 +608,32 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
|
||||
return nil, trace.BadParameter("unexpected state")
|
||||
}
|
||||
|
||||
if spec.RotateAfter != nil {
|
||||
// TODO, to be implemented in a future PR. `boundPublicKey` will need to
|
||||
// be updated.
|
||||
return nil, trace.NotImplemented("key rotation not yet supported")
|
||||
// If we've crossed a keypair rotation threshold, request one now.
|
||||
now := a.clock.Now()
|
||||
if shouldRequestBoundKeypairRotation(spec.RotateAfter, status.LastRotatedAt, now) {
|
||||
log.DebugContext(
|
||||
ctx, "requesting keypair rotation",
|
||||
"rotate_after", formatTimePointer(spec.RotateAfter),
|
||||
"last_rotated_at", formatTimePointer(status.LastRotatedAt),
|
||||
)
|
||||
newPubKey, err := a.requestBoundKeypairRotation(ctx, challengeResponse)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Don't let clients provide the same key again.
|
||||
if err := ensurePublicKeysNotEqual(boundPublicKey, newPubKey); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
mutators = append(mutators,
|
||||
mutateStatusBoundPublicKey(newPubKey, boundPublicKey),
|
||||
mutateStatusLastRotatedAt(&now, status.LastRotatedAt),
|
||||
)
|
||||
|
||||
boundPublicKey = newPubKey
|
||||
|
||||
// TODO: Follow up with an audit log event for this.
|
||||
}
|
||||
|
||||
certs, botInstanceID, err := a.generateCertsBot(
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
|
||||
"github.com/go-jose/go-jose/v3/jwt"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/client"
|
||||
@@ -96,9 +97,13 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, correctPublicKey := testBoundKeypair(t)
|
||||
_, rotatedPublicKey := testBoundKeypair(t)
|
||||
_, incorrectPublicKey := testBoundKeypair(t)
|
||||
|
||||
srv := newTestTLSServer(t)
|
||||
clock := clockwork.NewFakeClockAt(time.Now().Round(time.Second).UTC())
|
||||
startTime := clock.Now()
|
||||
|
||||
srv := newTestTLSServer(t, withClock(clock))
|
||||
auth := srv.Auth()
|
||||
auth.createBoundKeypairValidator = func(subject, clusterName string, publicKey crypto.PublicKey) (boundKeypairValidator, error) {
|
||||
return &mockBoundKeypairValidator{
|
||||
@@ -225,20 +230,55 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
makeSolver := func(publicKey string) client.RegisterUsingBoundKeypairChallengeResponseFunc {
|
||||
return func(challenge *proto.RegisterUsingBoundKeypairMethodResponse) (*proto.RegisterUsingBoundKeypairMethodRequest, error) {
|
||||
type wrappedSolver struct {
|
||||
rotatedPubKey string
|
||||
|
||||
rotationCount uint32
|
||||
challengeCount uint32
|
||||
solutions []string
|
||||
|
||||
wrapped client.RegisterUsingBoundKeypairChallengeResponseFunc
|
||||
}
|
||||
|
||||
makeSolver := func(initialPubKey string, mutators ...func(s *wrappedSolver)) *wrappedSolver {
|
||||
wrapper := &wrappedSolver{}
|
||||
for _, mutator := range mutators {
|
||||
mutator(wrapper)
|
||||
}
|
||||
|
||||
wrapper.wrapped = func(challenge *proto.RegisterUsingBoundKeypairMethodResponse) (*proto.RegisterUsingBoundKeypairMethodRequest, error) {
|
||||
switch r := challenge.Response.(type) {
|
||||
case *proto.RegisterUsingBoundKeypairMethodResponse_Challenge:
|
||||
if r.Challenge.PublicKey != publicKey {
|
||||
wrapper.challengeCount++
|
||||
|
||||
switch r.Challenge.PublicKey {
|
||||
case initialPubKey:
|
||||
case wrapper.rotatedPubKey:
|
||||
default:
|
||||
return nil, trace.BadParameter("wrong public key")
|
||||
}
|
||||
|
||||
wrapper.solutions = append(wrapper.solutions, r.Challenge.PublicKey)
|
||||
|
||||
return &proto.RegisterUsingBoundKeypairMethodRequest{
|
||||
Payload: &proto.RegisterUsingBoundKeypairMethodRequest_ChallengeResponse{
|
||||
ChallengeResponse: &proto.RegisterUsingBoundKeypairChallengeResponse{
|
||||
// For testing purposes, we'll just reply with the
|
||||
// public key, to avoid needing to parse the JWT.
|
||||
Solution: []byte(publicKey),
|
||||
Solution: []byte(r.Challenge.PublicKey),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
case *proto.RegisterUsingBoundKeypairMethodResponse_Rotation:
|
||||
wrapper.rotationCount++
|
||||
if wrapper.rotatedPubKey == "" {
|
||||
return nil, trace.BadParameter("can't generate key")
|
||||
}
|
||||
|
||||
return &proto.RegisterUsingBoundKeypairMethodRequest{
|
||||
Payload: &proto.RegisterUsingBoundKeypairMethodRequest_RotationResponse{
|
||||
RotationResponse: &proto.RegisterUsingBoundKeypairRotationResponse{
|
||||
PublicKey: wrapper.rotatedPubKey,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
@@ -246,17 +286,30 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
return nil, trace.BadParameter("invalid response type")
|
||||
}
|
||||
}
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
withRotatedPubKey := func(pubKey string) func(s *wrappedSolver) {
|
||||
return func(s *wrappedSolver) {
|
||||
s.rotatedPubKey = pubKey
|
||||
}
|
||||
}
|
||||
|
||||
// Advance the clock a bit. Tests may reference `startTime` for a past
|
||||
// reference point.
|
||||
clock.Advance(time.Hour)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
token types.ProvisionTokenV2
|
||||
initReq *proto.RegisterUsingBoundKeypairInitialRequest
|
||||
solver client.RegisterUsingBoundKeypairChallengeResponseFunc
|
||||
solver *wrappedSolver
|
||||
|
||||
assertError require.ErrorAssertionFunc
|
||||
assertSuccess func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse)
|
||||
assertError require.ErrorAssertionFunc
|
||||
assertResponse func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse)
|
||||
assertSolverState func(t *testing.T, s *wrappedSolver)
|
||||
}{
|
||||
{
|
||||
// no bound key, no bound bot instance, aka initial join without
|
||||
@@ -268,7 +321,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
solver: makeSolver(correctPublicKey),
|
||||
|
||||
assertError: require.NoError,
|
||||
assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
|
||||
// join count should be incremented
|
||||
require.Equal(t, uint32(1), v2.Status.BoundKeypair.RecoveryCount)
|
||||
require.NotEmpty(t, v2.Status.BoundKeypair.BoundBotInstanceID)
|
||||
@@ -303,7 +356,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
solver: makeSolver(correctPublicKey),
|
||||
|
||||
assertError: require.NoError,
|
||||
assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
|
||||
// join count should not be incremented
|
||||
require.Equal(t, uint32(0), v2.Status.BoundKeypair.RecoveryCount)
|
||||
},
|
||||
@@ -340,7 +393,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
solver: makeSolver(correctPublicKey),
|
||||
|
||||
assertError: require.NoError,
|
||||
assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, uint32(1), v2.Status.BoundKeypair.RecoveryCount)
|
||||
|
||||
// Should generate a new bot instance
|
||||
@@ -383,32 +436,13 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
require.ErrorContains(tt, err, "bot instance mismatch")
|
||||
},
|
||||
},
|
||||
{
|
||||
// TODO: rotation is not yet implemented.
|
||||
name: "rotation-requested",
|
||||
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
t := time.Now()
|
||||
v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
|
||||
v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
|
||||
v2.Spec.BoundKeypair.RotateAfter = &t
|
||||
// TODO: test clock?
|
||||
}),
|
||||
initReq: makeInitReq(),
|
||||
solver: makeSolver(correctPublicKey),
|
||||
|
||||
assertError: func(tt require.TestingT, err error, i ...interface{}) {
|
||||
require.Error(tt, err)
|
||||
require.ErrorContains(tt, err, "key rotation not yet supported")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "standard-initial-recovery-success",
|
||||
token: makeToken(withRecovery("standard", 0, 1, "")),
|
||||
initReq: makeInitReq(),
|
||||
solver: makeSolver(correctPublicKey),
|
||||
assertError: require.NoError,
|
||||
assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, uint32(1), v2.Status.BoundKeypair.RecoveryCount)
|
||||
|
||||
require.NotNil(t, res)
|
||||
@@ -421,7 +455,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
initReq: makeInitReq(withJoinState(jwtSigner, withToken(withRecovery("standard", 1, 2, "id")))),
|
||||
solver: makeSolver(correctPublicKey),
|
||||
assertError: require.NoError,
|
||||
assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, uint32(2), v2.Status.BoundKeypair.RecoveryCount)
|
||||
require.NotNil(t, res)
|
||||
state := parseJoinState(t, res.JoinState)
|
||||
@@ -502,7 +536,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
initReq: makeInitReq(withJoinState(jwtSigner, withToken(withRecovery("relaxed", 1, 0, "id")))),
|
||||
solver: makeSolver(correctPublicKey),
|
||||
assertError: require.NoError,
|
||||
assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, uint32(2), v2.Status.BoundKeypair.RecoveryCount)
|
||||
|
||||
require.NotNil(t, res)
|
||||
@@ -512,28 +546,192 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
require.Equal(t, v2.Status.BoundKeypair.RecoveryCount, state.RecoverySequence)
|
||||
},
|
||||
},
|
||||
{
|
||||
// Initial rotation, i.e. `LastRotatedAt` isn't set. This should
|
||||
// trigger as soon as the `RotateAfter` threshold has been crossed.
|
||||
name: "first-rotation-success",
|
||||
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
v2.Spec.BoundKeypair.RotateAfter = &startTime
|
||||
|
||||
v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
|
||||
v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
|
||||
}),
|
||||
initReq: makeInitReq(),
|
||||
solver: makeSolver(correctPublicKey, withRotatedPubKey(rotatedPublicKey)),
|
||||
|
||||
assertError: require.NoError,
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, rotatedPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Equal(t, rotatedPublicKey, res.BoundPublicKey)
|
||||
},
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 2, s.challengeCount)
|
||||
require.EqualValues(t, 1, s.rotationCount)
|
||||
require.Equal(t, []string{correctPublicKey, rotatedPublicKey}, s.solutions)
|
||||
},
|
||||
},
|
||||
{
|
||||
// Initial rotation timestamp hasn't been reached
|
||||
name: "first-rotation-skipped",
|
||||
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
rotateAfter := clock.Now().Add(time.Minute)
|
||||
v2.Spec.BoundKeypair.RotateAfter = &rotateAfter
|
||||
|
||||
v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
|
||||
v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
|
||||
}),
|
||||
initReq: makeInitReq(),
|
||||
solver: makeSolver(correctPublicKey, withRotatedPubKey(rotatedPublicKey)),
|
||||
|
||||
assertError: require.NoError,
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, correctPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Equal(t, correctPublicKey, res.BoundPublicKey)
|
||||
},
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 1, s.challengeCount)
|
||||
require.EqualValues(t, 0, s.rotationCount)
|
||||
require.Equal(t, []string{correctPublicKey}, s.solutions)
|
||||
},
|
||||
},
|
||||
{
|
||||
// This should only trigger after `RotateAfter` has been crossed and
|
||||
// `LastRotatedAt` isn't after it.
|
||||
name: "second-rotation-success",
|
||||
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
rotateAfter := startTime.Add(10 * time.Minute)
|
||||
v2.Spec.BoundKeypair.RotateAfter = &rotateAfter
|
||||
|
||||
v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
|
||||
v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
|
||||
v2.Status.BoundKeypair.LastRotatedAt = &startTime
|
||||
}),
|
||||
initReq: makeInitReq(),
|
||||
solver: makeSolver(correctPublicKey, withRotatedPubKey(rotatedPublicKey)),
|
||||
|
||||
assertError: require.NoError,
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, rotatedPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Equal(t, rotatedPublicKey, res.BoundPublicKey)
|
||||
},
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 2, s.challengeCount)
|
||||
require.EqualValues(t, 1, s.rotationCount)
|
||||
require.Equal(t, []string{correctPublicKey, rotatedPublicKey}, s.solutions)
|
||||
},
|
||||
},
|
||||
{
|
||||
// We shouldn't try to rotate again if LastRotatedAt is greater than
|
||||
// RotateAfter.
|
||||
name: "second-rotation-skipped",
|
||||
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
v2.Spec.BoundKeypair.RotateAfter = &startTime
|
||||
|
||||
v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
|
||||
v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
|
||||
|
||||
rotatedAt := startTime.Add(10 * time.Minute)
|
||||
v2.Status.BoundKeypair.LastRotatedAt = &rotatedAt
|
||||
}),
|
||||
initReq: makeInitReq(),
|
||||
solver: makeSolver(correctPublicKey, withRotatedPubKey(rotatedPublicKey)),
|
||||
|
||||
assertError: require.NoError,
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, correctPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Equal(t, correctPublicKey, res.BoundPublicKey)
|
||||
},
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 1, s.challengeCount)
|
||||
require.EqualValues(t, 0, s.rotationCount)
|
||||
require.Equal(t, []string{correctPublicKey}, s.solutions)
|
||||
},
|
||||
},
|
||||
{
|
||||
// If the client doesn't complete rotation, an error should be
|
||||
// returned and the key should not change on the server.
|
||||
name: "rotation-failure",
|
||||
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
v2.Spec.BoundKeypair.RotateAfter = &startTime
|
||||
|
||||
v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
|
||||
v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
|
||||
}),
|
||||
initReq: makeInitReq(),
|
||||
solver: makeSolver(correctPublicKey),
|
||||
|
||||
assertError: func(tt require.TestingT, err error, i ...interface{}) {
|
||||
require.ErrorContains(tt, err, "requesting a new public key")
|
||||
},
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 1, s.challengeCount)
|
||||
require.EqualValues(t, 1, s.rotationCount)
|
||||
require.Equal(t, []string{correctPublicKey}, s.solutions)
|
||||
},
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, correctPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Nil(t, res)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rotation-same-key-not-allowed",
|
||||
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
v2.Spec.BoundKeypair.RotateAfter = &startTime
|
||||
|
||||
v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
|
||||
v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
|
||||
}),
|
||||
initReq: makeInitReq(),
|
||||
solver: makeSolver(correctPublicKey, withRotatedPubKey(correctPublicKey)),
|
||||
|
||||
assertError: func(tt require.TestingT, err error, i ...interface{}) {
|
||||
require.ErrorContains(tt, err, "public key may not be reused after rotation")
|
||||
},
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 2, s.challengeCount)
|
||||
require.EqualValues(t, 1, s.rotationCount)
|
||||
|
||||
// note: the client does complete the challenge for the
|
||||
// duplicate key, but the attempt will ultimately be rejected
|
||||
require.Equal(t, []string{correctPublicKey, correctPublicKey}, s.solutions)
|
||||
},
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, correctPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Nil(t, res)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
token, err := types.NewProvisionTokenFromSpecAndStatus(
|
||||
tt.name, time.Now().Add(time.Minute), tt.token.Spec, tt.token.Status,
|
||||
tt.name, time.Now().Add(2*time.Hour), tt.token.Spec, tt.token.Status,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, auth.CreateToken(ctx, token))
|
||||
tt.initReq.JoinRequest.Token = tt.name
|
||||
|
||||
response, err := auth.RegisterUsingBoundKeypairMethod(ctx, tt.initReq, tt.solver)
|
||||
response, err := auth.RegisterUsingBoundKeypairMethod(ctx, tt.initReq, tt.solver.wrapped)
|
||||
tt.assertError(t, err)
|
||||
|
||||
if tt.assertSuccess != nil {
|
||||
if tt.assertResponse != nil {
|
||||
pt, err := auth.GetToken(ctx, tt.name)
|
||||
require.NoError(t, err)
|
||||
|
||||
ptv2, ok := pt.(*types.ProvisionTokenV2)
|
||||
require.True(t, ok)
|
||||
|
||||
tt.assertSuccess(t, ptv2, response)
|
||||
tt.assertResponse(t, ptv2, response)
|
||||
}
|
||||
|
||||
if tt.assertSolverState != nil {
|
||||
tt.assertSolverState(t, tt.solver)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -477,7 +477,6 @@ func botIdentityFromToken(
|
||||
}
|
||||
|
||||
// Only set during bound keypair joining, but used both before and after.
|
||||
var boundKeypairAdapter boundkeypair.FS
|
||||
var boundKeypairState *boundkeypair.ClientState
|
||||
|
||||
switch params.JoinMethod {
|
||||
@@ -494,8 +493,8 @@ func botIdentityFromToken(
|
||||
case types.JoinMethodBoundKeypair:
|
||||
joinSecret := cfg.Onboarding.BoundKeypair.InitialJoinSecret
|
||||
|
||||
boundKeypairAdapter = config.NewBoundkeypairDestinationAdapter(cfg.Storage.Destination)
|
||||
boundKeypairState, err = boundkeypair.LoadClientState(ctx, boundKeypairAdapter)
|
||||
adapter := config.NewBoundkeypairDestinationAdapter(cfg.Storage.Destination)
|
||||
boundKeypairState, err = boundkeypair.LoadClientState(ctx, adapter)
|
||||
if trace.IsNotFound(err) && joinSecret != "" {
|
||||
return nil, trace.NotImplemented("no existing client state was found and join secrets are not yet supported")
|
||||
} else if err != nil {
|
||||
@@ -515,9 +514,7 @@ func botIdentityFromToken(
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
log.DebugContext(ctx, "updating bound keypair client state")
|
||||
|
||||
if err := boundkeypair.StoreClientState(ctx, boundKeypairAdapter, boundKeypairState); err != nil {
|
||||
if err := boundKeypairState.Store(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,13 +121,14 @@ func onKeypairCreateCommand(ctx context.Context, globals *cli.GlobalArgs, cmd *c
|
||||
|
||||
state, err = boundkeypair.NewUnboundClientState(
|
||||
ctx,
|
||||
fsAdapter,
|
||||
getSuiteFromProxy(cmd.ProxyServer, globals.Insecure),
|
||||
)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "initializing new client state")
|
||||
}
|
||||
|
||||
if err := boundkeypair.StoreClientState(ctx, fsAdapter, state); err != nil {
|
||||
if err := state.Store(ctx); err != nil {
|
||||
return trace.Wrap(err, "writing bound keypair state")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2025 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/mfa"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
"github.com/gravitational/teleport/lib/service/servicecfg"
|
||||
commonclient "github.com/gravitational/teleport/tool/tctl/common/client"
|
||||
tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config"
|
||||
)
|
||||
|
||||
type BoundKeypairCommand struct {
|
||||
token string
|
||||
|
||||
requestRotation *kingpin.CmdClause
|
||||
}
|
||||
|
||||
func (c *BoundKeypairCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) {
|
||||
cmd := app.Command("bound-keypair", "Manage bound-keypair joining tokens")
|
||||
|
||||
c.requestRotation = cmd.Command("request-rotation", "Request a keypair rotation on the next join attempt.")
|
||||
c.requestRotation.Arg("name", "The name of the token").Required().StringVar(&c.token)
|
||||
}
|
||||
|
||||
func (c *BoundKeypairCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) {
|
||||
var commandFunc func(ctx context.Context, client *authclient.Client) error
|
||||
|
||||
switch cmd {
|
||||
case c.requestRotation.FullCommand():
|
||||
commandFunc = c.RequestRotation
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
|
||||
client, closeFn, err := clientFunc(ctx)
|
||||
if err != nil {
|
||||
return false, trace.Wrap(err)
|
||||
}
|
||||
|
||||
err = commandFunc(ctx, client)
|
||||
closeFn(ctx)
|
||||
|
||||
return true, trace.Wrap(err)
|
||||
}
|
||||
|
||||
func (c *BoundKeypairCommand) RequestRotation(ctx context.Context, client *authclient.Client) error {
|
||||
// Perform MFA checks now since we'll otherwise need to prompt twice.
|
||||
if _, err := mfa.MFAResponseFromContext(ctx); err == nil {
|
||||
// Nothing to do.
|
||||
} else {
|
||||
mfaResponse, err := mfa.PerformAdminActionMFACeremony(ctx, client.PerformMFACeremony, true /*allowReuse*/)
|
||||
if err == nil {
|
||||
ctx = mfa.ContextWithMFAResponse(ctx, mfaResponse)
|
||||
} else if !errors.Is(err, &mfa.ErrMFANotRequired) && !errors.Is(err, &mfa.ErrMFANotSupported) {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
token, err := client.GetToken(ctx, c.token)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if token.GetJoinMethod() != types.JoinMethodBoundKeypair {
|
||||
return trace.BadParameter(
|
||||
"token %s is of type %s, not %s",
|
||||
c.token, token.GetJoinMethod(), types.JoinMethodBoundKeypair,
|
||||
)
|
||||
}
|
||||
|
||||
v2, ok := token.(*types.ProvisionTokenV2)
|
||||
if !ok {
|
||||
return trace.BadParameter("unsupported token type %T", token)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
v2.Spec.BoundKeypair.RotateAfter = &now
|
||||
|
||||
if err := client.UpsertToken(ctx, v2); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Token rotation flag has been set, rotation will be required during the next authentication attempt.\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -72,5 +72,6 @@ func Commands() []CLICommand {
|
||||
&AutoUpdateCommand{},
|
||||
&stableunixusers.Command{},
|
||||
&decision.Command{},
|
||||
&BoundKeypairCommand{},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user