diff --git a/api/client/proto/joinservice.pb.go b/api/client/proto/joinservice.pb.go index f52c07e44d9..0dadaf7ecfc 100644 --- a/api/client/proto/joinservice.pb.go +++ b/api/client/proto/joinservice.pb.go @@ -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:]) diff --git a/api/proto/teleport/legacy/client/proto/joinservice.proto b/api/proto/teleport/legacy/client/proto/joinservice.proto index 145f67afdf0..2d7e2ac2f59 100644 --- a/api/proto/teleport/legacy/client/proto/joinservice.proto +++ b/api/proto/teleport/legacy/client/proto/joinservice.proto @@ -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 diff --git a/lib/auth/join/boundkeypair/boundkeypair.go b/lib/auth/join/boundkeypair/boundkeypair.go index ffff96253ca..0e4407e4f8e 100644 --- a/lib/auth/join/boundkeypair/boundkeypair.go +++ b/lib/auth/join/boundkeypair/boundkeypair.go @@ -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 } diff --git a/lib/auth/join/boundkeypair/boundkeypair_test.go b/lib/auth/join/boundkeypair/boundkeypair_test.go new file mode 100644 index 00000000000..325393d2698 --- /dev/null +++ b/lib/auth/join/boundkeypair/boundkeypair_test.go @@ -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 . + */ + +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) +} diff --git a/lib/auth/join/join.go b/lib/auth/join/join.go index d63c1ae3c9a..f5efad29af1 100644 --- a/lib/auth/join/join.go +++ b/lib/auth/join/join.go @@ -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, diff --git a/lib/auth/join_bound_keypair.go b/lib/auth/join_bound_keypair.go index f87d50bbc53..70dfcb78cb0 100644 --- a/lib/auth/join_bound_keypair.go +++ b/lib/auth/join_bound_keypair.go @@ -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( diff --git a/lib/auth/join_bound_keypair_test.go b/lib/auth/join_bound_keypair_test.go index 0f641af6174..8c9795ab29c 100644 --- a/lib/auth/join_bound_keypair_test.go +++ b/lib/auth/join_bound_keypair_test.go @@ -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) } }) } diff --git a/lib/tbot/service_bot_identity.go b/lib/tbot/service_bot_identity.go index 28409093a52..aabb97fe1db 100644 --- a/lib/tbot/service_bot_identity.go +++ b/lib/tbot/service_bot_identity.go @@ -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) } } diff --git a/tool/tbot/keypair.go b/tool/tbot/keypair.go index e91820a7be0..234f971fc38 100644 --- a/tool/tbot/keypair.go +++ b/tool/tbot/keypair.go @@ -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") } diff --git a/tool/tctl/common/bound_keypair_command.go b/tool/tctl/common/bound_keypair_command.go new file mode 100644 index 00000000000..b9c80026b68 --- /dev/null +++ b/tool/tctl/common/bound_keypair_command.go @@ -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 . + */ + +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 +} diff --git a/tool/tctl/common/cmds.go b/tool/tctl/common/cmds.go index 9c28bcb3c83..21cf58600a9 100644 --- a/tool/tctl/common/cmds.go +++ b/tool/tctl/common/cmds.go @@ -72,5 +72,6 @@ func Commands() []CLICommand { &AutoUpdateCommand{}, &stableunixusers.Command{}, &decision.Command{}, + &BoundKeypairCommand{}, } }