diff --git a/api/gen/proto/go/teleport/hardwarekeyagent/v1/hardwarekeyagent_service.pb.go b/api/gen/proto/go/teleport/hardwarekeyagent/v1/hardwarekeyagent_service.pb.go new file mode 100644 index 0000000000000..dadf5268e106c --- /dev/null +++ b/api/gen/proto/go/teleport/hardwarekeyagent/v1/hardwarekeyagent_service.pb.go @@ -0,0 +1,646 @@ +// 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 . + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: teleport/hardwarekeyagent/v1/hardwarekeyagent_service.proto + +package hardwarekeyagentv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// PIVSlotKey is the key reference for a specific PIV slot. +type PIVSlotKey int32 + +const ( + // PIV slot key not specified. + PIVSlotKey_PIV_SLOT_KEY_UNSPECIFIED PIVSlotKey = 0 + // PIV slot key 9a. This is the default slot for pin_policy=never, touch_policy=never. + PIVSlotKey_PIV_SLOT_KEY_9A PIVSlotKey = 1 + // PIV slot key 9c. This is the default slot for pin_policy=never, touch_policy=cached. + PIVSlotKey_PIV_SLOT_KEY_9C PIVSlotKey = 2 + // PIV slot key 9d. This is the default slot for pin_policy=once, touch_policy=cached. + PIVSlotKey_PIV_SLOT_KEY_9D PIVSlotKey = 3 + // PIV slot key 9e. This is the default slot for pin_policy=once, touch_policy=never. + PIVSlotKey_PIV_SLOT_KEY_9E PIVSlotKey = 4 +) + +// Enum value maps for PIVSlotKey. +var ( + PIVSlotKey_name = map[int32]string{ + 0: "PIV_SLOT_KEY_UNSPECIFIED", + 1: "PIV_SLOT_KEY_9A", + 2: "PIV_SLOT_KEY_9C", + 3: "PIV_SLOT_KEY_9D", + 4: "PIV_SLOT_KEY_9E", + } + PIVSlotKey_value = map[string]int32{ + "PIV_SLOT_KEY_UNSPECIFIED": 0, + "PIV_SLOT_KEY_9A": 1, + "PIV_SLOT_KEY_9C": 2, + "PIV_SLOT_KEY_9D": 3, + "PIV_SLOT_KEY_9E": 4, + } +) + +func (x PIVSlotKey) Enum() *PIVSlotKey { + p := new(PIVSlotKey) + *p = x + return p +} + +func (x PIVSlotKey) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PIVSlotKey) Descriptor() protoreflect.EnumDescriptor { + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_enumTypes[0].Descriptor() +} + +func (PIVSlotKey) Type() protoreflect.EnumType { + return &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_enumTypes[0] +} + +func (x PIVSlotKey) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PIVSlotKey.Descriptor instead. +func (PIVSlotKey) EnumDescriptor() ([]byte, []int) { + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescGZIP(), []int{0} +} + +// Hash refers to a specific hash function used during signing. +type Hash int32 + +const ( + Hash_HASH_UNSPECIFIED Hash = 0 + Hash_HASH_NONE Hash = 1 + Hash_HASH_SHA256 Hash = 2 + Hash_HASH_SHA512 Hash = 3 +) + +// Enum value maps for Hash. +var ( + Hash_name = map[int32]string{ + 0: "HASH_UNSPECIFIED", + 1: "HASH_NONE", + 2: "HASH_SHA256", + 3: "HASH_SHA512", + } + Hash_value = map[string]int32{ + "HASH_UNSPECIFIED": 0, + "HASH_NONE": 1, + "HASH_SHA256": 2, + "HASH_SHA512": 3, + } +) + +func (x Hash) Enum() *Hash { + p := new(Hash) + *p = x + return p +} + +func (x Hash) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Hash) Descriptor() protoreflect.EnumDescriptor { + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_enumTypes[1].Descriptor() +} + +func (Hash) Type() protoreflect.EnumType { + return &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_enumTypes[1] +} + +func (x Hash) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Hash.Descriptor instead. +func (Hash) EnumDescriptor() ([]byte, []int) { + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescGZIP(), []int{1} +} + +// PingRequest is a request to Ping. +type PingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingRequest) Reset() { + *x = PingRequest{} + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingRequest) ProtoMessage() {} + +func (x *PingRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. +func (*PingRequest) Descriptor() ([]byte, []int) { + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescGZIP(), []int{0} +} + +// PingResponse is a response to Ping. +type PingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // PID is the PID of the client process running the agent. + Pid uint32 `protobuf:"varint,1,opt,name=pid,proto3" json:"pid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingResponse) Reset() { + *x = PingResponse{} + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingResponse) ProtoMessage() {} + +func (x *PingResponse) ProtoReflect() protoreflect.Message { + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingResponse.ProtoReflect.Descriptor instead. +func (*PingResponse) Descriptor() ([]byte, []int) { + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescGZIP(), []int{1} +} + +func (x *PingResponse) GetPid() uint32 { + if x != nil { + return x.Pid + } + return 0 +} + +// SignRequest is a request to perform a signature with a specific hardware private key. +type SignRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Digest is a hashed message to sign. + Digest []byte `protobuf:"bytes,1,opt,name=digest,proto3" json:"digest,omitempty"` + // Hash is the hash function used to prepare the digest. + Hash Hash `protobuf:"varint,2,opt,name=hash,proto3,enum=teleport.hardwarekeyagent.v1.Hash" json:"hash,omitempty"` + // SaltLength specifies the length of the salt added to the digest before a signature. + // This salt length is precomputed by the client, following the crypto/rsa implementation. + // Only used, and required, for PSS RSA signatures. + SaltLength uint32 `protobuf:"varint,3,opt,name=salt_length,json=saltLength,proto3" json:"salt_length,omitempty"` + // KeyRef references a specific hardware private key. + KeyRef *KeyRef `protobuf:"bytes,4,opt,name=key_ref,json=keyRef,proto3" json:"key_ref,omitempty"` + // KeyInfo contains additional, optional key info which generally will improve UX by + // giving the agent context about the key, such as whether PIN/touch prompts are + // expected, or what cluster login is trying to interface with the key. + KeyInfo *KeyInfo `protobuf:"bytes,5,opt,name=key_info,json=keyInfo,proto3" json:"key_info,omitempty"` + // Command is the client command or action requiring a signature, e.g. "tsh ssh server01". + // The agent can include this detail in PIN/touch prompts to show the origin of the + // signature request to the user. + Command string `protobuf:"bytes,6,opt,name=command,proto3" json:"command,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignRequest) Reset() { + *x = SignRequest{} + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignRequest) ProtoMessage() {} + +func (x *SignRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignRequest.ProtoReflect.Descriptor instead. +func (*SignRequest) Descriptor() ([]byte, []int) { + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescGZIP(), []int{2} +} + +func (x *SignRequest) GetDigest() []byte { + if x != nil { + return x.Digest + } + return nil +} + +func (x *SignRequest) GetHash() Hash { + if x != nil { + return x.Hash + } + return Hash_HASH_UNSPECIFIED +} + +func (x *SignRequest) GetSaltLength() uint32 { + if x != nil { + return x.SaltLength + } + return 0 +} + +func (x *SignRequest) GetKeyRef() *KeyRef { + if x != nil { + return x.KeyRef + } + return nil +} + +func (x *SignRequest) GetKeyInfo() *KeyInfo { + if x != nil { + return x.KeyInfo + } + return nil +} + +func (x *SignRequest) GetCommand() string { + if x != nil { + return x.Command + } + return "" +} + +// Signature is a private key signature. +type Signature struct { + state protoimpl.MessageState `protogen:"open.v1"` + // For an RSA key, signature should be either a PKCS #1 v1.5 or PSS signature, + // depending on the hash and salt chosen. For an (EC)DSA key, it should be a + // DER-serialised, ASN.1 signature structure. + Signature []byte `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Signature) Reset() { + *x = Signature{} + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Signature) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Signature) ProtoMessage() {} + +func (x *Signature) ProtoReflect() protoreflect.Message { + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Signature.ProtoReflect.Descriptor instead. +func (*Signature) Descriptor() ([]byte, []int) { + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescGZIP(), []int{3} +} + +func (x *Signature) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +// KeyRef references a specific hardware private key. +type KeyRef struct { + state protoimpl.MessageState `protogen:"open.v1"` + // SerialNumber is the serial number of the hardware key. + SerialNumber uint32 `protobuf:"varint,1,opt,name=serial_number,json=serialNumber,proto3" json:"serial_number,omitempty"` + // SlotKey is a PIV slot key reference. + SlotKey PIVSlotKey `protobuf:"varint,2,opt,name=slot_key,json=slotKey,proto3,enum=teleport.hardwarekeyagent.v1.PIVSlotKey" json:"slot_key,omitempty"` + // PublicKey is the public key encoded in PKIX, ASN.1 DER form. If the public key does + // not match the private key currently in the hardware key's PIV slot, the signature + // will fail early. + PublicKeyDer []byte `protobuf:"bytes,3,opt,name=public_key_der,json=publicKeyDer,proto3" json:"public_key_der,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *KeyRef) Reset() { + *x = KeyRef{} + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *KeyRef) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyRef) ProtoMessage() {} + +func (x *KeyRef) ProtoReflect() protoreflect.Message { + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyRef.ProtoReflect.Descriptor instead. +func (*KeyRef) Descriptor() ([]byte, []int) { + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescGZIP(), []int{4} +} + +func (x *KeyRef) GetSerialNumber() uint32 { + if x != nil { + return x.SerialNumber + } + return 0 +} + +func (x *KeyRef) GetSlotKey() PIVSlotKey { + if x != nil { + return x.SlotKey + } + return PIVSlotKey_PIV_SLOT_KEY_UNSPECIFIED +} + +func (x *KeyRef) GetPublicKeyDer() []byte { + if x != nil { + return x.PublicKeyDer + } + return nil +} + +// KeyInfo contains additional information about a hardware private key. +type KeyInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // TouchRequired is a client hint as to whether the hardware private key requires touch. + // The agent will use this to provide the ideal UX for the touch prompt. If this client + // hint is incorrect, touch will still be prompted. + TouchRequired bool `protobuf:"varint,1,opt,name=touch_required,json=touchRequired,proto3" json:"touch_required,omitempty"` + // PinRequired is a client hint as to whether the hardware private key requires PIN. + // The agent will use this to provide the ideal UX for the PIN prompt. If this client + // hint is incorrect, PIN will still be prompted for YubiKey versions >= 4.3.0, and + // failing with an auth error otherwise. + PinRequired bool `protobuf:"varint,2,opt,name=pin_required,json=pinRequired,proto3" json:"pin_required,omitempty"` + // ProxyHost is a Teleport proxy hostname that the key is associated with. + // May be used to add context to PIN/touch prompts. + ProxyHost string `protobuf:"bytes,3,opt,name=proxy_host,json=proxyHost,proto3" json:"proxy_host,omitempty"` + // Username is a Teleport username that the key is associated with. + // May be used to add context to PIN/touch prompts. + Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"` + // ClusterName is a Teleport cluster name that the key is associated with. + // May be used to add context to PIN/touch prompts. + ClusterName string `protobuf:"bytes,5,opt,name=cluster_name,json=clusterName,proto3" json:"cluster_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *KeyInfo) Reset() { + *x = KeyInfo{} + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *KeyInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyInfo) ProtoMessage() {} + +func (x *KeyInfo) ProtoReflect() protoreflect.Message { + mi := &file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyInfo.ProtoReflect.Descriptor instead. +func (*KeyInfo) Descriptor() ([]byte, []int) { + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescGZIP(), []int{5} +} + +func (x *KeyInfo) GetTouchRequired() bool { + if x != nil { + return x.TouchRequired + } + return false +} + +func (x *KeyInfo) GetPinRequired() bool { + if x != nil { + return x.PinRequired + } + return false +} + +func (x *KeyInfo) GetProxyHost() string { + if x != nil { + return x.ProxyHost + } + return "" +} + +func (x *KeyInfo) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *KeyInfo) GetClusterName() string { + if x != nil { + return x.ClusterName + } + return "" +} + +var File_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto protoreflect.FileDescriptor + +const file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDesc = "" + + "\n" + + ";teleport/hardwarekeyagent/v1/hardwarekeyagent_service.proto\x12\x1cteleport.hardwarekeyagent.v1\"\r\n" + + "\vPingRequest\" \n" + + "\fPingResponse\x12\x10\n" + + "\x03pid\x18\x01 \x01(\rR\x03pid\"\x99\x02\n" + + "\vSignRequest\x12\x16\n" + + "\x06digest\x18\x01 \x01(\fR\x06digest\x126\n" + + "\x04hash\x18\x02 \x01(\x0e2\".teleport.hardwarekeyagent.v1.HashR\x04hash\x12\x1f\n" + + "\vsalt_length\x18\x03 \x01(\rR\n" + + "saltLength\x12=\n" + + "\akey_ref\x18\x04 \x01(\v2$.teleport.hardwarekeyagent.v1.KeyRefR\x06keyRef\x12@\n" + + "\bkey_info\x18\x05 \x01(\v2%.teleport.hardwarekeyagent.v1.KeyInfoR\akeyInfo\x12\x18\n" + + "\acommand\x18\x06 \x01(\tR\acommand\")\n" + + "\tSignature\x12\x1c\n" + + "\tsignature\x18\x01 \x01(\fR\tsignature\"\x98\x01\n" + + "\x06KeyRef\x12#\n" + + "\rserial_number\x18\x01 \x01(\rR\fserialNumber\x12C\n" + + "\bslot_key\x18\x02 \x01(\x0e2(.teleport.hardwarekeyagent.v1.PIVSlotKeyR\aslotKey\x12$\n" + + "\x0epublic_key_der\x18\x03 \x01(\fR\fpublicKeyDer\"\xb1\x01\n" + + "\aKeyInfo\x12%\n" + + "\x0etouch_required\x18\x01 \x01(\bR\rtouchRequired\x12!\n" + + "\fpin_required\x18\x02 \x01(\bR\vpinRequired\x12\x1d\n" + + "\n" + + "proxy_host\x18\x03 \x01(\tR\tproxyHost\x12\x1a\n" + + "\busername\x18\x04 \x01(\tR\busername\x12!\n" + + "\fcluster_name\x18\x05 \x01(\tR\vclusterName*~\n" + + "\n" + + "PIVSlotKey\x12\x1c\n" + + "\x18PIV_SLOT_KEY_UNSPECIFIED\x10\x00\x12\x13\n" + + "\x0fPIV_SLOT_KEY_9A\x10\x01\x12\x13\n" + + "\x0fPIV_SLOT_KEY_9C\x10\x02\x12\x13\n" + + "\x0fPIV_SLOT_KEY_9D\x10\x03\x12\x13\n" + + "\x0fPIV_SLOT_KEY_9E\x10\x04*M\n" + + "\x04Hash\x12\x14\n" + + "\x10HASH_UNSPECIFIED\x10\x00\x12\r\n" + + "\tHASH_NONE\x10\x01\x12\x0f\n" + + "\vHASH_SHA256\x10\x02\x12\x0f\n" + + "\vHASH_SHA512\x10\x032\xd8\x01\n" + + "\x17HardwareKeyAgentService\x12_\n" + + "\x04Ping\x12).teleport.hardwarekeyagent.v1.PingRequest\x1a*.teleport.hardwarekeyagent.v1.PingResponse\"\x00\x12\\\n" + + "\x04Sign\x12).teleport.hardwarekeyagent.v1.SignRequest\x1a'.teleport.hardwarekeyagent.v1.Signature\"\x00BdZbgithub.com/gravitational/teleport/api/gen/proto/go/teleport/hardwarekeyagent/v1;hardwarekeyagentv1b\x06proto3" + +var ( + file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescOnce sync.Once + file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescData []byte +) + +func file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescGZIP() []byte { + file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescOnce.Do(func() { + file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDesc), len(file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDesc))) + }) + return file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDescData +} + +var file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_goTypes = []any{ + (PIVSlotKey)(0), // 0: teleport.hardwarekeyagent.v1.PIVSlotKey + (Hash)(0), // 1: teleport.hardwarekeyagent.v1.Hash + (*PingRequest)(nil), // 2: teleport.hardwarekeyagent.v1.PingRequest + (*PingResponse)(nil), // 3: teleport.hardwarekeyagent.v1.PingResponse + (*SignRequest)(nil), // 4: teleport.hardwarekeyagent.v1.SignRequest + (*Signature)(nil), // 5: teleport.hardwarekeyagent.v1.Signature + (*KeyRef)(nil), // 6: teleport.hardwarekeyagent.v1.KeyRef + (*KeyInfo)(nil), // 7: teleport.hardwarekeyagent.v1.KeyInfo +} +var file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_depIdxs = []int32{ + 1, // 0: teleport.hardwarekeyagent.v1.SignRequest.hash:type_name -> teleport.hardwarekeyagent.v1.Hash + 6, // 1: teleport.hardwarekeyagent.v1.SignRequest.key_ref:type_name -> teleport.hardwarekeyagent.v1.KeyRef + 7, // 2: teleport.hardwarekeyagent.v1.SignRequest.key_info:type_name -> teleport.hardwarekeyagent.v1.KeyInfo + 0, // 3: teleport.hardwarekeyagent.v1.KeyRef.slot_key:type_name -> teleport.hardwarekeyagent.v1.PIVSlotKey + 2, // 4: teleport.hardwarekeyagent.v1.HardwareKeyAgentService.Ping:input_type -> teleport.hardwarekeyagent.v1.PingRequest + 4, // 5: teleport.hardwarekeyagent.v1.HardwareKeyAgentService.Sign:input_type -> teleport.hardwarekeyagent.v1.SignRequest + 3, // 6: teleport.hardwarekeyagent.v1.HardwareKeyAgentService.Ping:output_type -> teleport.hardwarekeyagent.v1.PingResponse + 5, // 7: teleport.hardwarekeyagent.v1.HardwareKeyAgentService.Sign:output_type -> teleport.hardwarekeyagent.v1.Signature + 6, // [6:8] is the sub-list for method output_type + 4, // [4:6] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_init() } +func file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_init() { + if File_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDesc), len(file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_rawDesc)), + NumEnums: 2, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_goTypes, + DependencyIndexes: file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_depIdxs, + EnumInfos: file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_enumTypes, + MessageInfos: file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_msgTypes, + }.Build() + File_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto = out.File + file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_goTypes = nil + file_teleport_hardwarekeyagent_v1_hardwarekeyagent_service_proto_depIdxs = nil +} diff --git a/api/gen/proto/go/teleport/hardwarekeyagent/v1/hardwarekeyagent_service_grpc.pb.go b/api/gen/proto/go/teleport/hardwarekeyagent/v1/hardwarekeyagent_service_grpc.pb.go new file mode 100644 index 0000000000000..1a8cba08332e5 --- /dev/null +++ b/api/gen/proto/go/teleport/hardwarekeyagent/v1/hardwarekeyagent_service_grpc.pb.go @@ -0,0 +1,194 @@ +// 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 . + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: teleport/hardwarekeyagent/v1/hardwarekeyagent_service.proto + +package hardwarekeyagentv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + HardwareKeyAgentService_Ping_FullMethodName = "/teleport.hardwarekeyagent.v1.HardwareKeyAgentService/Ping" + HardwareKeyAgentService_Sign_FullMethodName = "/teleport.hardwarekeyagent.v1.HardwareKeyAgentService/Sign" +) + +// HardwareKeyAgentServiceClient is the client API for HardwareKeyAgentService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// HardwareKeyAgentService provides an agent service for hardware key (PIV) signatures. +// This allows multiple Teleport clients to share a PIV connection rather than blocking +// each other, due to the exclusive nature of PIV connections. This also enabled shared +// hardware key states, such as a custom PIN cache shared across Teleport clients. +type HardwareKeyAgentServiceClient interface { + // Ping the agent service to check if it is active. + Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) + // Sign produces a signature with the provided options for the specified hardware private key + // + // This rpc implements Go's crypto.Signer interface. + Sign(ctx context.Context, in *SignRequest, opts ...grpc.CallOption) (*Signature, error) +} + +type hardwareKeyAgentServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewHardwareKeyAgentServiceClient(cc grpc.ClientConnInterface) HardwareKeyAgentServiceClient { + return &hardwareKeyAgentServiceClient{cc} +} + +func (c *hardwareKeyAgentServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PingResponse) + err := c.cc.Invoke(ctx, HardwareKeyAgentService_Ping_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *hardwareKeyAgentServiceClient) Sign(ctx context.Context, in *SignRequest, opts ...grpc.CallOption) (*Signature, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Signature) + err := c.cc.Invoke(ctx, HardwareKeyAgentService_Sign_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// HardwareKeyAgentServiceServer is the server API for HardwareKeyAgentService service. +// All implementations must embed UnimplementedHardwareKeyAgentServiceServer +// for forward compatibility. +// +// HardwareKeyAgentService provides an agent service for hardware key (PIV) signatures. +// This allows multiple Teleport clients to share a PIV connection rather than blocking +// each other, due to the exclusive nature of PIV connections. This also enabled shared +// hardware key states, such as a custom PIN cache shared across Teleport clients. +type HardwareKeyAgentServiceServer interface { + // Ping the agent service to check if it is active. + Ping(context.Context, *PingRequest) (*PingResponse, error) + // Sign produces a signature with the provided options for the specified hardware private key + // + // This rpc implements Go's crypto.Signer interface. + Sign(context.Context, *SignRequest) (*Signature, error) + mustEmbedUnimplementedHardwareKeyAgentServiceServer() +} + +// UnimplementedHardwareKeyAgentServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedHardwareKeyAgentServiceServer struct{} + +func (UnimplementedHardwareKeyAgentServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") +} +func (UnimplementedHardwareKeyAgentServiceServer) Sign(context.Context, *SignRequest) (*Signature, error) { + return nil, status.Errorf(codes.Unimplemented, "method Sign not implemented") +} +func (UnimplementedHardwareKeyAgentServiceServer) mustEmbedUnimplementedHardwareKeyAgentServiceServer() { +} +func (UnimplementedHardwareKeyAgentServiceServer) testEmbeddedByValue() {} + +// UnsafeHardwareKeyAgentServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HardwareKeyAgentServiceServer will +// result in compilation errors. +type UnsafeHardwareKeyAgentServiceServer interface { + mustEmbedUnimplementedHardwareKeyAgentServiceServer() +} + +func RegisterHardwareKeyAgentServiceServer(s grpc.ServiceRegistrar, srv HardwareKeyAgentServiceServer) { + // If the following call pancis, it indicates UnimplementedHardwareKeyAgentServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&HardwareKeyAgentService_ServiceDesc, srv) +} + +func _HardwareKeyAgentService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HardwareKeyAgentServiceServer).Ping(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HardwareKeyAgentService_Ping_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HardwareKeyAgentServiceServer).Ping(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HardwareKeyAgentService_Sign_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HardwareKeyAgentServiceServer).Sign(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HardwareKeyAgentService_Sign_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HardwareKeyAgentServiceServer).Sign(ctx, req.(*SignRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// HardwareKeyAgentService_ServiceDesc is the grpc.ServiceDesc for HardwareKeyAgentService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var HardwareKeyAgentService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "teleport.hardwarekeyagent.v1.HardwareKeyAgentService", + HandlerType: (*HardwareKeyAgentServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ping", + Handler: _HardwareKeyAgentService_Ping_Handler, + }, + { + MethodName: "Sign", + Handler: _HardwareKeyAgentService_Sign_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "teleport/hardwarekeyagent/v1/hardwarekeyagent_service.proto", +} diff --git a/api/proto/teleport/hardwarekeyagent/v1/hardwarekeyagent_service.proto b/api/proto/teleport/hardwarekeyagent/v1/hardwarekeyagent_service.proto new file mode 100644 index 0000000000000..e6b1481c9efaf --- /dev/null +++ b/api/proto/teleport/hardwarekeyagent/v1/hardwarekeyagent_service.proto @@ -0,0 +1,129 @@ +// 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 . + +syntax = "proto3"; + +package teleport.hardwarekeyagent.v1; + +option go_package = "github.com/gravitational/teleport/api/gen/proto/go/teleport/hardwarekeyagent/v1;hardwarekeyagentv1"; + +// HardwareKeyAgentService provides an agent service for hardware key (PIV) signatures. +// This allows multiple Teleport clients to share a PIV connection rather than blocking +// each other, due to the exclusive nature of PIV connections. This also enabled shared +// hardware key states, such as a custom PIN cache shared across Teleport clients. +service HardwareKeyAgentService { + // Ping the agent service to check if it is active. + rpc Ping(PingRequest) returns (PingResponse) {} + // Sign produces a signature with the provided options for the specified hardware private key + // + // This rpc implements Go's crypto.Signer interface. + rpc Sign(SignRequest) returns (Signature) {} +} + +// PingRequest is a request to Ping. +message PingRequest {} + +// PingResponse is a response to Ping. +message PingResponse { + // PID is the PID of the client process running the agent. + uint32 pid = 1; +} + +// SignRequest is a request to perform a signature with a specific hardware private key. +message SignRequest { + // Digest is a hashed message to sign. + bytes digest = 1; + // Hash is the hash function used to prepare the digest. + Hash hash = 2; + // SaltLength specifies the length of the salt added to the digest before a signature. + // This salt length is precomputed by the client, following the crypto/rsa implementation. + // Only used, and required, for PSS RSA signatures. + uint32 salt_length = 3; + // KeyRef references a specific hardware private key. + KeyRef key_ref = 4; + // KeyInfo contains additional, optional key info which generally will improve UX by + // giving the agent context about the key, such as whether PIN/touch prompts are + // expected, or what cluster login is trying to interface with the key. + KeyInfo key_info = 5; + // Command is the client command or action requiring a signature, e.g. "tsh ssh server01". + // The agent can include this detail in PIN/touch prompts to show the origin of the + // signature request to the user. + string command = 6; +} + +// Signature is a private key signature. +message Signature { + // For an RSA key, signature should be either a PKCS #1 v1.5 or PSS signature, + // depending on the hash and salt chosen. For an (EC)DSA key, it should be a + // DER-serialised, ASN.1 signature structure. + bytes signature = 1; +} + +// KeyRef references a specific hardware private key. +message KeyRef { + // SerialNumber is the serial number of the hardware key. + uint32 serial_number = 1; + // SlotKey is a PIV slot key reference. + PIVSlotKey slot_key = 2; + // PublicKey is the public key encoded in PKIX, ASN.1 DER form. If the public key does + // not match the private key currently in the hardware key's PIV slot, the signature + // will fail early. + bytes public_key_der = 3; +} + +// KeyInfo contains additional information about a hardware private key. +message KeyInfo { + // TouchRequired is a client hint as to whether the hardware private key requires touch. + // The agent will use this to provide the ideal UX for the touch prompt. If this client + // hint is incorrect, touch will still be prompted. + bool touch_required = 1; + // PinRequired is a client hint as to whether the hardware private key requires PIN. + // The agent will use this to provide the ideal UX for the PIN prompt. If this client + // hint is incorrect, PIN will still be prompted for YubiKey versions >= 4.3.0, and + // failing with an auth error otherwise. + bool pin_required = 2; + // ProxyHost is a Teleport proxy hostname that the key is associated with. + // May be used to add context to PIN/touch prompts. + string proxy_host = 3; + // Username is a Teleport username that the key is associated with. + // May be used to add context to PIN/touch prompts. + string username = 4; + // ClusterName is a Teleport cluster name that the key is associated with. + // May be used to add context to PIN/touch prompts. + string cluster_name = 5; +} + +// PIVSlotKey is the key reference for a specific PIV slot. +enum PIVSlotKey { + // PIV slot key not specified. + PIV_SLOT_KEY_UNSPECIFIED = 0; + // PIV slot key 9a. This is the default slot for pin_policy=never, touch_policy=never. + PIV_SLOT_KEY_9A = 1; + // PIV slot key 9c. This is the default slot for pin_policy=never, touch_policy=cached. + PIV_SLOT_KEY_9C = 2; + // PIV slot key 9d. This is the default slot for pin_policy=once, touch_policy=cached. + PIV_SLOT_KEY_9D = 3; + // PIV slot key 9e. This is the default slot for pin_policy=once, touch_policy=never. + PIV_SLOT_KEY_9E = 4; +} + +// Hash refers to a specific hash function used during signing. +enum Hash { + HASH_UNSPECIFIED = 0; + HASH_NONE = 1; + HASH_SHA256 = 2; + HASH_SHA512 = 3; +} diff --git a/api/utils/keys/hardwarekey/hardwarekey.go b/api/utils/keys/hardwarekey/hardwarekey.go index 49be739742ca7..fba453a87f970 100644 --- a/api/utils/keys/hardwarekey/hardwarekey.go +++ b/api/utils/keys/hardwarekey/hardwarekey.go @@ -241,6 +241,9 @@ type PrivateKeyConfig struct { // - touch & pin -> 9d // - touch & !pin -> 9e CustomSlot PIVSlotKeyString + // Algorithm is the key algorithm to use. Defaults to [AlgorithmEC256]. + // [AlgorithmEd25519] is not supported by all hardware keys. + Algorithm SignatureAlgorithm // ContextualKeyInfo contains additional info to associate with the key. ContextualKeyInfo ContextualKeyInfo } @@ -254,3 +257,12 @@ type ContextualKeyInfo struct { // ClusterName is a Teleport cluster name that the key is associated with. ClusterName string } + +// SignatureAlgorithm is a signature key algorithm option. +type SignatureAlgorithm int + +const ( + SignatureAlgorithmEC256 SignatureAlgorithm = iota + 1 + SignatureAlgorithmEd25519 + SignatureAlgorithmRSA2048 +) diff --git a/api/utils/keys/hardwarekey/service_mock.go b/api/utils/keys/hardwarekey/service_mock.go index 5b8a1f4827e69..2625fb629493a 100644 --- a/api/utils/keys/hardwarekey/service_mock.go +++ b/api/utils/keys/hardwarekey/service_mock.go @@ -17,8 +17,11 @@ package hardwarekey import ( "context" "crypto" + "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" "crypto/rand" + "crypto/rsa" "io" "sync" "time" @@ -90,7 +93,20 @@ func (s *MockHardwareKeyService) NewPrivateKey(ctx context.Context, config Priva return nil, trace.Wrap(err) } - pub, priv, err := ed25519.GenerateKey(rand.Reader) + var priv crypto.Signer + switch config.Algorithm { + // Use ECDSA key by default. + case SignatureAlgorithmEC256, 0: + priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case SignatureAlgorithmEd25519: + _, priv, err = ed25519.GenerateKey(rand.Reader) + case SignatureAlgorithmRSA2048: + //nolint:forbidigo // Allow /api to generate RSA key without importing Teleport. + priv, err = rsa.GenerateKey(rand.Reader, 2048) + default: + return nil, trace.BadParameter("unknown algorithm option %v", config.Algorithm) + } + if err != nil { return nil, trace.Wrap(err) } @@ -98,7 +114,7 @@ func (s *MockHardwareKeyService) NewPrivateKey(ctx context.Context, config Priva ref := &PrivateKeyRef{ SerialNumber: serialNumber, SlotKey: slotKey, - PublicKey: pub, + PublicKey: priv.Public(), Policy: config.Policy, // Since this is only used in tests, we will ignore the attestation statement in the end. // We just need it to be non-nil so that it goes through the test modules implementation @@ -129,7 +145,7 @@ func (s *MockHardwareKeyService) Sign(ctx context.Context, ref *PrivateKeyRef, k slot: ref.SlotKey, }] if !ok { - return nil, trace.NotFound("key not found in slot %d", ref.SlotKey) + return nil, trace.NotFound("key not found in slot 0x%x", ref.SlotKey) } if err := s.tryPrompt(ctx, ref.Policy, keyInfo); err != nil { @@ -192,7 +208,7 @@ func (s *MockHardwareKeyService) GetFullKeyRef(serialNumber uint32, slotKey PIVS slot: slotKey, }] if !ok { - return nil, trace.NotFound("key not found in slot %d", slotKey) + return nil, trace.NotFound("key not found in slot 0x%x", slotKey) } return priv.ref, nil @@ -201,3 +217,9 @@ func (s *MockHardwareKeyService) GetFullKeyRef(serialNumber uint32, slotKey PIVS func (s *MockHardwareKeyService) MockTouch() { s.mockTouch <- struct{}{} } + +func (s *MockHardwareKeyService) Reset() { + s.fakeHardwarePrivateKeysMux.Lock() + defer s.fakeHardwarePrivateKeysMux.Unlock() + s.fakeHardwarePrivateKeys = map[hardwareKeySlot]*fakeHardwarePrivateKey{} +} diff --git a/api/utils/keys/hardwarekey/slot.go b/api/utils/keys/hardwarekey/slot.go index 744b55631ee45..7d603dfa2ea9d 100644 --- a/api/utils/keys/hardwarekey/slot.go +++ b/api/utils/keys/hardwarekey/slot.go @@ -19,6 +19,8 @@ import ( "strconv" "github.com/gravitational/trace" + + hardwarekeyagentv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/hardwarekeyagent/v1" ) // PIVSlotKey is the key reference for a specific PIV slot. @@ -27,23 +29,37 @@ import ( type PIVSlotKey uint32 const ( - pivSlotKeyBasic PIVSlotKey = 0x9a - pivSlotKeyTouch PIVSlotKey = 0x9c - pivSlotKeyTouchAndPIN PIVSlotKey = 0x9d - pivSlotKeyPIN PIVSlotKey = 0x9e + pivSlot9A PIVSlotKey = 0x9a + pivSlot9C PIVSlotKey = 0x9c + pivSlot9D PIVSlotKey = 0x9d + pivSlot9E PIVSlotKey = 0x9e ) +// Validate the slot key value. +func (k PIVSlotKey) Validate() error { + switch k { + case pivSlot9A, pivSlot9C, pivSlot9D, pivSlot9E: + return nil + default: + return trace.BadParameter("invalid PIV slot key 0x%x", k) + } +} + // GetDefaultSlotKey gets the default PIV slot key for the given [policy]. +// - 9A for PromptPolicyNone +// - 9C for PromptPolicyTouch +// - 9D for PromptPolicyTouchAndPIN +// - 9E for PromptPolicyPIN func GetDefaultSlotKey(policy PromptPolicy) (PIVSlotKey, error) { switch policy { case PromptPolicyNone: - return pivSlotKeyBasic, nil + return pivSlot9A, nil case PromptPolicyTouch: - return pivSlotKeyTouch, nil + return pivSlot9C, nil case PromptPolicyPIN: - return pivSlotKeyPIN, nil + return pivSlot9E, nil case PromptPolicyTouchAndPIN: - return pivSlotKeyTouchAndPIN, nil + return pivSlot9D, nil default: return 0, trace.BadParameter("unexpected prompt policy %v", policy) } @@ -60,15 +76,54 @@ func (s PIVSlotKeyString) Validate() error { // Parse [s] into a [PIVSlotKey]. func (s PIVSlotKeyString) Parse() (PIVSlotKey, error) { - slotKey, err := strconv.ParseUint(string(s), 16, 32) + slotKeyUint, err := strconv.ParseUint(string(s), 16, 32) if err != nil { return 0, trace.Wrap(err, "failed to parse %q as a uint", s) } - switch p := PIVSlotKey(slotKey); p { - case pivSlotKeyBasic, pivSlotKeyTouch, pivSlotKeyTouchAndPIN, pivSlotKeyPIN: - return p, nil + slotKey := PIVSlotKey(slotKeyUint) + if err := slotKey.Validate(); err != nil { + return 0, trace.Wrap(err) + } + + return slotKey, nil +} + +// PIVSlotKeyFromProto convert the piv slot key from proto. +func PIVSlotKeyFromProto(pivSlot hardwarekeyagentv1.PIVSlotKey) (PIVSlotKey, error) { + var slotKey PIVSlotKey + switch pivSlot { + case hardwarekeyagentv1.PIVSlotKey_PIV_SLOT_KEY_9A: + slotKey = pivSlot9A + case hardwarekeyagentv1.PIVSlotKey_PIV_SLOT_KEY_9C: + slotKey = pivSlot9C + case hardwarekeyagentv1.PIVSlotKey_PIV_SLOT_KEY_9D: + slotKey = pivSlot9D + case hardwarekeyagentv1.PIVSlotKey_PIV_SLOT_KEY_9E: + slotKey = pivSlot9E + default: + return 0, trace.BadParameter("unknown piv slot key for proto enum %d", pivSlot) + } + + if err := slotKey.Validate(); err != nil { + return 0, trace.Wrap(err) + } + + return slotKey, nil +} + +// PIVSlotKeyFromProto convert the piv slot key to proto. +func PIVSlotKeyToProto(slotKey PIVSlotKey) (hardwarekeyagentv1.PIVSlotKey, error) { + switch slotKey { + case pivSlot9A: + return hardwarekeyagentv1.PIVSlotKey_PIV_SLOT_KEY_9A, nil + case pivSlot9C: + return hardwarekeyagentv1.PIVSlotKey_PIV_SLOT_KEY_9C, nil + case pivSlot9D: + return hardwarekeyagentv1.PIVSlotKey_PIV_SLOT_KEY_9D, nil + case pivSlot9E: + return hardwarekeyagentv1.PIVSlotKey_PIV_SLOT_KEY_9E, nil default: - return 0, trace.BadParameter("invalid PIV slot %q", s) + return 0, trace.BadParameter("unknown proto enum for piv slot key %d", slotKey) } } diff --git a/api/utils/keys/hardwarekeyagent/agent.go b/api/utils/keys/hardwarekeyagent/agent.go new file mode 100644 index 0000000000000..270a7932734f9 --- /dev/null +++ b/api/utils/keys/hardwarekeyagent/agent.go @@ -0,0 +1,139 @@ +// 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 hardwarekeyagent provides a hardware key agent implementation of [hardwarekey.Service]. +package hardwarekeyagent + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "net" + "os" + + "github.com/gravitational/trace" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + hardwarekeyagentv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/hardwarekeyagent/v1" + "github.com/gravitational/teleport/api/utils/grpc/interceptors" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" +) + +// NewClient creates a new hardware key agent client. +func NewClient(ctx context.Context, socketPath string, creds credentials.TransportCredentials) (hardwarekeyagentv1.HardwareKeyAgentServiceClient, error) { + if _, err := os.Stat(socketPath); err != nil { + return nil, trace.Wrap(err) + } + + cc, err := grpc.NewClient("passthrough:", + grpc.WithTransportCredentials(creds), + grpc.WithUnaryInterceptor(interceptors.GRPCClientUnaryErrorInterceptor), + // The [grpc] library fails to resolve unix sockets on Windows, so + // we provide "passthrough:" to skip grpc's address resolution and + // a custom [net] dialer to connect to the socket. + grpc.WithContextDialer(func(_ context.Context, addr string) (net.Conn, error) { + return net.Dial("unix", socketPath) + }), + ) + if err != nil { + return nil, trace.Wrap(err) + } + + return hardwarekeyagentv1.NewHardwareKeyAgentServiceClient(cc), nil +} + +// NewServer returns a new hardware key agent server. +func NewServer(ctx context.Context, s hardwarekey.Service, creds credentials.TransportCredentials) *grpc.Server { + grpcServer := grpc.NewServer( + grpc.Creds(creds), + grpc.UnaryInterceptor(interceptors.GRPCServerUnaryErrorInterceptor), + ) + hardwarekeyagentv1.RegisterHardwareKeyAgentServiceServer(grpcServer, &agentService{s: s}) + return grpcServer +} + +// agentService implements [hardwarekeyagentv1.HardwareKeyAgentServiceServer]. +type agentService struct { + hardwarekeyagentv1.UnimplementedHardwareKeyAgentServiceServer + s hardwarekey.Service +} + +// Sign the given digest with the specified hardware private key. +func (s *agentService) Sign(ctx context.Context, req *hardwarekeyagentv1.SignRequest) (*hardwarekeyagentv1.Signature, error) { + slotKey, err := hardwarekey.PIVSlotKeyFromProto(req.KeyRef.SlotKey) + if err != nil { + return nil, trace.Wrap(err) + } + + pub, err := x509.ParsePKIXPublicKey(req.KeyRef.PublicKeyDer) + if err != nil { + return nil, trace.Wrap(err) + } + + keyRef := &hardwarekey.PrivateKeyRef{ + SerialNumber: req.KeyRef.SerialNumber, + SlotKey: slotKey, + PublicKey: pub, + Policy: hardwarekey.PromptPolicy{ + TouchRequired: req.KeyInfo.TouchRequired, + PINRequired: req.KeyInfo.PinRequired, + }, + } + + keyInfo := hardwarekey.ContextualKeyInfo{ + ProxyHost: req.KeyInfo.ProxyHost, + Username: req.KeyInfo.Username, + ClusterName: req.KeyInfo.ClusterName, + } + + var signerOpts crypto.SignerOpts + switch req.Hash { + case hardwarekeyagentv1.Hash_HASH_NONE: + signerOpts = crypto.Hash(0) + case hardwarekeyagentv1.Hash_HASH_SHA256: + signerOpts = crypto.SHA256 + case hardwarekeyagentv1.Hash_HASH_SHA512: + signerOpts = crypto.SHA512 + default: + return nil, trace.BadParameter("unsupported hash %q", req.Hash.String()) + } + + if req.SaltLength > 0 { + signerOpts = &rsa.PSSOptions{ + Hash: signerOpts.HashFunc(), + SaltLength: int(req.SaltLength), + } + } + + signature, err := s.s.Sign(ctx, keyRef, keyInfo, rand.Reader, req.Digest, signerOpts) + if err != nil { + return nil, trace.Wrap(err) + } + + return &hardwarekeyagentv1.Signature{ + Signature: signature, + }, nil +} + +// Ping the server and get its PID. +func (s *agentService) Ping(ctx context.Context, req *hardwarekeyagentv1.PingRequest) (*hardwarekeyagentv1.PingResponse, error) { + return &hardwarekeyagentv1.PingResponse{ + Pid: uint32(os.Getpid()), + }, nil +} diff --git a/api/utils/keys/hardwarekeyagent/service.go b/api/utils/keys/hardwarekeyagent/service.go new file mode 100644 index 0000000000000..a00a92580dddd --- /dev/null +++ b/api/utils/keys/hardwarekeyagent/service.go @@ -0,0 +1,168 @@ +// 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 hardwarekeyagent + +import ( + "context" + "crypto" + "crypto/rsa" + "crypto/x509" + "io" + "log/slog" + "math" + + "github.com/gravitational/trace" + + hardwarekeyagentv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/hardwarekeyagent/v1" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" +) + +// Service is an agent implementation of [hardwarekey.AgentService]. +type Service struct { + // Used for signature requests to the service. + agentClient hardwarekeyagentv1.HardwareKeyAgentServiceClient + // Used for non signature methods and as a fallback for signatures if the + // agent client fails to handle a sign request. + fallbackService hardwarekey.Service +} + +// NewService creates a new hardware key agent service from the given +// agent client and fallback service. The fallback service is used for +// non-signature methods of [hardwarekey.Service] which are not implemented +// by the agent. Generally this fallback service is only used during login. +func NewService(agentClient hardwarekeyagentv1.HardwareKeyAgentServiceClient, fallbackService hardwarekey.Service) *Service { + return &Service{ + agentClient: agentClient, + fallbackService: fallbackService, + } +} + +// NewPrivateKey creates or retrieves a hardware private key for the given config. +func (s *Service) NewPrivateKey(ctx context.Context, config hardwarekey.PrivateKeyConfig) (*hardwarekey.Signer, error) { + return s.fallbackService.NewPrivateKey(ctx, config) +} + +// Sign performs a cryptographic signature using the specified hardware +// private key and provided signature parameters. +func (s *Service) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, keyInfo hardwarekey.ContextualKeyInfo, rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + // First try to sign with the agent, then fallback to the direct service if needed. + signature, err := s.agentSign(ctx, ref, keyInfo, rand, digest, opts) + if err != nil { + slog.ErrorContext(ctx, "Failed to perform signature over hardware key agent, falling back to fallback service", "agent_err", err) + signature, err = s.fallbackService.Sign(ctx, ref, keyInfo, rand, digest, opts) + } + + return signature, trace.Wrap(err) +} + +func (s *Service) agentSign(ctx context.Context, ref *hardwarekey.PrivateKeyRef, keyInfo hardwarekey.ContextualKeyInfo, rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + slotKey, err := hardwarekey.PIVSlotKeyToProto(ref.SlotKey) + if err != nil { + return nil, trace.Wrap(err) + } + + publicKeyDER, err := x509.MarshalPKIXPublicKey(ref.PublicKey) + if err != nil { + return nil, trace.Wrap(err) + } + + var hash hardwarekeyagentv1.Hash + switch opts.HashFunc() { + case 0: + hash = hardwarekeyagentv1.Hash_HASH_NONE + case crypto.SHA256: + hash = hardwarekeyagentv1.Hash_HASH_SHA256 + case crypto.SHA512: + hash = hardwarekeyagentv1.Hash_HASH_SHA512 + default: + return nil, trace.BadParameter("unsupported hash func %q", opts.HashFunc().String()) + } + + var saltLength int + if pssOpts, ok := opts.(*rsa.PSSOptions); ok { + if pssOpts.Hash == 0 { + return nil, trace.BadParameter("hash must be specified for PSS signature") + } + + rsaPub, ok := ref.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, trace.BadParameter("cannot perform PSS signature for non-rsa key") + } + + saltLength = pssOpts.SaltLength + + // If the salt length is [rsa.PSSSaltLengthEqualsHash] or [rsa.PSSSaltLengthAuto], + // pre-calculate the salt length so we can send it over the gRPC message. + switch saltLength { + case rsa.PSSSaltLengthEqualsHash: + saltLength = pssOpts.Hash.Size() + case rsa.PSSSaltLengthAuto: + // We use the same salt length calculation as the crypto/rsa package. + // https://github.com/golang/go/blob/21483099632c11743d01ec6f38577f31de26b0d0/src/crypto/internal/fips140/rsa/pkcs1v22.go#L253 + saltLength = (rsaPub.N.BitLen()-1+7)/8 - 2 - pssOpts.Hash.Size() + } + + if saltLength < 0 { + return nil, rsa.ErrMessageTooLong + } + + if saltLength > math.MaxUint32 { + return nil, trace.BadParameter("invalid salt length %d", saltLength) + } + } + + req := &hardwarekeyagentv1.SignRequest{ + Digest: digest, + Hash: hash, + SaltLength: uint32(saltLength), + KeyRef: &hardwarekeyagentv1.KeyRef{ + SerialNumber: ref.SerialNumber, + SlotKey: slotKey, + PublicKeyDer: publicKeyDER, + }, + KeyInfo: &hardwarekeyagentv1.KeyInfo{ + TouchRequired: ref.Policy.TouchRequired, + PinRequired: ref.Policy.PINRequired, + ProxyHost: keyInfo.ProxyHost, + Username: keyInfo.Username, + ClusterName: keyInfo.ClusterName, + }, + // TODO: Add command to sign request for prompt context. + } + + resp, err := s.agentClient.Sign(ctx, req) + if err != nil { + return nil, trace.Wrap(err) + } + + return resp.Signature, nil +} + +// TODO(Joerger): DELETE IN v19.0.0 +func (s *Service) GetFullKeyRef(serialNumber uint32, slotKey hardwarekey.PIVSlotKey) (*hardwarekey.PrivateKeyRef, error) { + return s.fallbackService.GetFullKeyRef(serialNumber, slotKey) +} + +// SetPrompt for the fallback service. +func (s *Service) SetPrompt(prompt hardwarekey.Prompt) { + s.fallbackService.SetPrompt(prompt) +} + +// GetPrompt for the fallback service. +func (s *Service) GetPrompt() hardwarekey.Prompt { + return s.fallbackService.GetPrompt() +} diff --git a/api/utils/keys/hardwarekeyagent/service_test.go b/api/utils/keys/hardwarekeyagent/service_test.go new file mode 100644 index 0000000000000..a78895103bbea --- /dev/null +++ b/api/utils/keys/hardwarekeyagent/service_test.go @@ -0,0 +1,213 @@ +// 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 hardwarekeyagent_test + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "net" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc/credentials/insecure" + + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/hardwarekeyagent" +) + +func TestHardwareKeyAgentService(t *testing.T) { + ctx := context.Background() + + // Prepare the agent server + mockService := hardwarekey.NewMockHardwareKeyService(nil /*prompt*/) + server := hardwarekeyagent.NewServer(ctx, mockService, insecure.NewCredentials()) + t.Cleanup(server.Stop) + + agentDir := t.TempDir() + socketPath := filepath.Join(agentDir, "agent.sock") + l, err := net.Listen("unix", socketPath) + require.NoError(t, err) + + serverErr := make(chan error, 1) + go func() { + serverErr <- server.Serve(l) + }() + + // Prepare the agent client + agentClient, err := hardwarekeyagent.NewClient(ctx, socketPath, insecure.NewCredentials()) + require.NoError(t, err) + + agentService := hardwarekeyagent.NewService(agentClient, hardwarekey.NewMockHardwareKeyService(nil /*prompt*/)) + agentServiceWithFallback := hardwarekeyagent.NewService(agentClient, mockService) + + for _, tc := range []struct { + name string + algorithm hardwarekey.SignatureAlgorithm + opts crypto.SignerOpts + expectErr bool + }{ + { + name: "EC256 Unsupported hash", + algorithm: hardwarekey.SignatureAlgorithmEC256, + opts: crypto.MD5, + expectErr: true, // unsupported hash + }, + { + name: "EC256 No hash", + algorithm: hardwarekey.SignatureAlgorithmEC256, + opts: crypto.Hash(0), + }, + { + name: "EC256 SHA256", + algorithm: hardwarekey.SignatureAlgorithmEC256, + opts: crypto.SHA256, + }, + { + name: "EC256 SHA512", + algorithm: hardwarekey.SignatureAlgorithmEC256, + opts: crypto.SHA512, + }, + { + name: "ED25519 No hash", + algorithm: hardwarekey.SignatureAlgorithmEd25519, + opts: crypto.Hash(0), + }, + { + name: "ED25519 SHA256", + algorithm: hardwarekey.SignatureAlgorithmEd25519, + opts: crypto.SHA256, + expectErr: true, // sha256 not supported + }, + { + name: "ED25519 SHA512", + algorithm: hardwarekey.SignatureAlgorithmEd25519, + opts: crypto.SHA512, + }, + { + name: "RSA2048 No hash", + algorithm: hardwarekey.SignatureAlgorithmRSA2048, + opts: crypto.Hash(0), + }, + { + name: "RSA2048 SHA256", + algorithm: hardwarekey.SignatureAlgorithmRSA2048, + opts: crypto.SHA256, + }, + { + name: "RSA2048 SHA512", + algorithm: hardwarekey.SignatureAlgorithmRSA2048, + opts: crypto.SHA512, + }, + { + name: "RSA2048 No hash PSS signature", + algorithm: hardwarekey.SignatureAlgorithmRSA2048, + opts: &rsa.PSSOptions{ + SaltLength: 10, + Hash: crypto.Hash(0), + }, + expectErr: true, // hash required for pss signature + }, + { + name: "RSA2048 SHA256 PSS signature", + algorithm: hardwarekey.SignatureAlgorithmRSA2048, + opts: &rsa.PSSOptions{ + SaltLength: 10, + Hash: crypto.SHA256, + }, + }, + { + name: "RSA2048 SHA256 PSSSaltLengthAuto", + algorithm: hardwarekey.SignatureAlgorithmRSA2048, + opts: &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA256, + }, + }, + { + name: "RSA2048 SHA256 PSSSaltLengthEqualsHash", + algorithm: hardwarekey.SignatureAlgorithmRSA2048, + opts: &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + Hash: crypto.SHA256, + }, + }, + { + name: "RSA2048 SHA512 PSS signature", + algorithm: hardwarekey.SignatureAlgorithmRSA2048, + opts: &rsa.PSSOptions{ + SaltLength: 10, + Hash: crypto.SHA512, + }, + }, + { + name: "RSA2048 SHA512 PSSSaltLengthAuto", + algorithm: hardwarekey.SignatureAlgorithmRSA2048, + opts: &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA512, + }, + }, + { + name: "RSA2048 SHA512 PSSSaltLengthEqualsHash", + algorithm: hardwarekey.SignatureAlgorithmRSA2048, + opts: &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + Hash: crypto.SHA512, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Cleanup(mockService.Reset) + + // Create a new key directly in the service. + hwSigner, err := mockService.NewPrivateKey(ctx, hardwarekey.PrivateKeyConfig{ + Algorithm: tc.algorithm, + }) + require.NoError(t, err) + + // Mock a hashed digest. + digest := make([]byte, 100) + if hash := tc.opts.HashFunc(); hash != 0 { + digest = make([]byte, hash.Size()) + } + + // Perform a signature over the agent. + _, err = agentService.Sign(ctx, hwSigner.Ref, hwSigner.KeyInfo, rand.Reader, digest, tc.opts) + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } + + t.Run("fallback", func(t *testing.T) { + mockService.Reset() + + // Create a new key. + hwSigner, err := agentServiceWithFallback.NewPrivateKey(ctx, hardwarekey.PrivateKeyConfig{}) + require.NoError(t, err) + + // If the server stops, the service should fallback to the fallback service. + server.Stop() + err = hwSigner.WarmupHardwareKey(ctx) + require.NoError(t, err) + }) +} diff --git a/api/utils/keys/piv/service.go b/api/utils/keys/piv/service.go index 3633922a87e0f..90f1425011225 100644 --- a/api/utils/keys/piv/service.go +++ b/api/utils/keys/piv/service.go @@ -122,7 +122,7 @@ func (s *YubiKeyService) NewPrivateKey(ctx context.Context, config hardwarekey.P } generatePrivateKey := func() (*hardwarekey.Signer, error) { - ref, err := y.generatePrivateKey(pivSlot, config.Policy) + ref, err := y.generatePrivateKey(pivSlot, config.Policy, config.Algorithm) if err != nil { return nil, trace.Wrap(err) } diff --git a/api/utils/keys/piv/service_test.go b/api/utils/keys/piv/service_test.go index 1bce1e1f64441..f2993f2b2163f 100644 --- a/api/utils/keys/piv/service_test.go +++ b/api/utils/keys/piv/service_test.go @@ -34,8 +34,8 @@ import ( "github.com/gravitational/teleport/api/utils/prompt" ) -// TestGetYubiKeyPrivateKey_Interactive tests generation and retrieval of YubiKey private keys. -func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { +// TestNewHardwarePrivateKey_Interactive tests generation and retrieval of YubiKey private keys. +func TestNewHardwarePrivateKey_Interactive(t *testing.T) { // This test will overwrite any PIV data on the yubiKey. if os.Getenv("TELEPORT_TEST_YUBIKEY_PIV") == "" { t.Skipf("Skipping TestGenerateYubiKeyPrivateKey because TELEPORT_TEST_YUBIKEY_PIV is not set") @@ -56,74 +56,103 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) { y, err := piv.FindYubiKey(0) require.NoError(t, err) - resetYubikey(t, y) t.Cleanup(func() { resetYubikey(t, y) }) - // Warmup the hardware key to prompt touch at the start of the test, - // rather than having this interaction later. - priv, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ - Policy: hardwarekey.PromptPolicy{TouchRequired: true}, - }) - require.NoError(t, err) - require.NoError(t, priv.WarmupHardwareKey(ctx)) + // Test creating a new key, retrieving it, and using it for signatures. + testNewPrivateKey := func(t *testing.T, config hardwarekey.PrivateKeyConfig) { + t.Helper() + + // NewHardwarePrivateKey should generate a new hardware private key. + priv, err := keys.NewHardwarePrivateKey(ctx, s, config) + require.NoError(t, err) + hwSigner, ok := priv.Signer.(*hardwarekey.Signer) + require.True(t, ok, "expected hardwarekey.Signer but got %T", priv.Signer) + + // Check that config was applied correctly. + require.Equal(t, config.Policy, hwSigner.Ref.Policy) + if config.CustomSlot != "" { + expectSlot, err := config.CustomSlot.Parse() + require.NoError(t, err) + require.Equal(t, expectSlot, hwSigner.Ref.SlotKey) + } + + // test Hardware Key methods + require.Equal(t, config.Policy, priv.GetPrivateKeyPolicy().GetPromptPolicy()) + require.NotNil(t, priv.GetAttestationStatement()) + require.True(t, priv.IsHardware()) + + // Test bogus sign (warmup). + require.NoError(t, priv.WarmupHardwareKey(ctx)) - // Set pin and handle expected prompts. - setupPINPrompt := func(t *testing.T) { + // Another call to NewHardwarePrivateKey should retrieve the previously generated key. + retrievePriv, err := keys.NewHardwarePrivateKey(ctx, s, config) + require.NoError(t, err) + require.Equal(t, priv.Public(), retrievePriv.Public()) + + // parsing the key's private key PEM should produce the same key as well. + retrievePriv, err = keys.ParsePrivateKey(priv.PrivateKeyPEM(), keys.WithHardwareKeyService(s)) + require.NoError(t, err) + require.Equal(t, priv.Public(), retrievePriv.Public()) + } + + t.Run("PromptPolicies", func(t *testing.T) { + // Warmup the hardware key to prompt touch at the start of the test, + // rather than having this interaction later. + priv, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ + Policy: hardwarekey.PromptPolicy{TouchRequired: true}, + }) + require.NoError(t, err) + require.NoError(t, priv.WarmupHardwareKey(ctx)) + + resetYubikey(t, y) + + // Set pin. const testPIN = "123123" require.NoError(t, y.SetPIN(pivgo.DefaultPIN, testPIN)) - promptReader.AddString(testPIN).AddString(testPIN) - } - for _, policy := range []hardwarekey.PromptPolicy{ - hardwarekey.PromptPolicyNone, - hardwarekey.PromptPolicyTouch, - hardwarekey.PromptPolicyPIN, - hardwarekey.PromptPolicyTouchAndPIN, - } { - for _, customSlot := range []bool{true, false} { + for _, policy := range []hardwarekey.PromptPolicy{ + hardwarekey.PromptPolicyNone, + hardwarekey.PromptPolicyTouch, + hardwarekey.PromptPolicyPIN, + hardwarekey.PromptPolicyTouchAndPIN, + } { t.Run(fmt.Sprintf("policy:%+v", policy), func(t *testing.T) { - t.Run(fmt.Sprintf("custom slot:%v", customSlot), func(t *testing.T) { - setupPINPrompt(t) - t.Cleanup(func() { - resetYubikey(t, y) - }) - - var slot hardwarekey.PIVSlotKeyString = "" - if customSlot { - slot = "9a" - } - - // NewHardwarePrivateKey should generate a new hardware private key. - priv, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ - CustomSlot: slot, - Policy: policy, - }) - require.NoError(t, err) - - // test HardwareSigner methods - require.Equal(t, policy, priv.GetPrivateKeyPolicy().GetPromptPolicy()) - require.NotNil(t, priv.GetAttestationStatement()) - require.True(t, priv.IsHardware()) - - // Test bogus sign (warmup). - require.NoError(t, priv.WarmupHardwareKey(ctx)) - - // Another call to NewHardwarePrivateKey should retrieve the previously generated key. - retrievePriv, err := keys.NewHardwarePrivateKey(ctx, s, hardwarekey.PrivateKeyConfig{ - CustomSlot: slot, - Policy: policy, - }) - require.NoError(t, err) - require.Equal(t, priv.Public(), retrievePriv.Public()) - - // parsing the key's private key PEM should produce the same key as well. - retrievePriv, err = keys.ParsePrivateKey(priv.PrivateKeyPEM(), keys.WithHardwareKeyService(s)) - require.NoError(t, err) - require.Equal(t, priv.Public(), retrievePriv.Public()) + // Handle pin prompts (1 for generating the key, 1 for signing). + if policy.PINRequired { + promptReader.AddString(testPIN).AddString(testPIN) + } + + testNewPrivateKey(t, hardwarekey.PrivateKeyConfig{ + Policy: policy, }) }) } - } + }) + + t.Run("CustomSlot", func(t *testing.T) { + resetYubikey(t, y) + testNewPrivateKey(t, hardwarekey.PrivateKeyConfig{ + CustomSlot: "9c", + }) + }) + + t.Run("Algorithms", func(t *testing.T) { + for algorithm, config := range map[string]hardwarekey.PrivateKeyConfig{ + "EC256": { + CustomSlot: "9a", + Algorithm: hardwarekey.SignatureAlgorithmEC256, + }, + "RSA2048": { + CustomSlot: "9c", + Algorithm: hardwarekey.SignatureAlgorithmRSA2048, + }, + } { + t.Run(fmt.Sprintf("algorithm:%v", algorithm), func(t *testing.T) { + resetYubikey(t, y) + testNewPrivateKey(t, config) + }) + } + }) } func TestOverwritePrompt(t *testing.T) { diff --git a/api/utils/keys/piv/yubikey.go b/api/utils/keys/piv/yubikey.go index bee6f3f86e66e..a31ed2b0b98a1 100644 --- a/api/utils/keys/piv/yubikey.go +++ b/api/utils/keys/piv/yubikey.go @@ -290,7 +290,7 @@ func (y *YubiKey) Reset() error { } // generatePrivateKey generates a new private key in the given PIV slot. -func (y *YubiKey) generatePrivateKey(slot piv.Slot, policy hardwarekey.PromptPolicy) (*hardwarekey.PrivateKeyRef, error) { +func (y *YubiKey) generatePrivateKey(slot piv.Slot, policy hardwarekey.PromptPolicy, algorithm hardwarekey.SignatureAlgorithm) (*hardwarekey.PrivateKeyRef, error) { touchPolicy := piv.TouchPolicyNever if policy.TouchRequired { touchPolicy = piv.TouchPolicyCached @@ -301,8 +301,24 @@ func (y *YubiKey) generatePrivateKey(slot piv.Slot, policy hardwarekey.PromptPol pinPolicy = piv.PINPolicyOnce } + var alg piv.Algorithm + switch algorithm { + // Use ECDSA key by default. + case hardwarekey.SignatureAlgorithmEC256, 0: + alg = piv.AlgorithmEC256 + case hardwarekey.SignatureAlgorithmEd25519: + // TODO(Joerger): Currently algorithms are only specified in tests, but some users pre-generate + // their keys in custom slots with custom algorithms, so we should try to support Ed25519 keys. + // Currently the Ed25519 algorithm is only supported by SoloKeys and YubiKeys v5.7.x+ + return nil, trace.BadParameter("Ed25519 keys are not currently supported") + case hardwarekey.SignatureAlgorithmRSA2048: + alg = piv.AlgorithmRSA2048 + default: + return nil, trace.BadParameter("unknown algorithm option %v", algorithm) + } + opts := piv.Key{ - Algorithm: piv.AlgorithmEC256, + Algorithm: alg, PINPolicy: pinPolicy, TouchPolicy: touchPolicy, } diff --git a/lib/client/api.go b/lib/client/api.go index 83a4026643872..1b636665d0bf0 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -72,7 +72,6 @@ import ( "github.com/gravitational/teleport/api/utils/grpc/interceptors" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/keys/hardwarekey" - "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/auth/touchid" @@ -87,6 +86,7 @@ import ( "github.com/gravitational/teleport/lib/devicetrust" dtauthntypes "github.com/gravitational/teleport/lib/devicetrust/authn/types" "github.com/gravitational/teleport/lib/events" + libhwk "github.com/gravitational/teleport/lib/hardwarekey" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/multiplexer" "github.com/gravitational/teleport/lib/observability/tracing" @@ -1293,7 +1293,7 @@ func NewClient(c *Config) (tc *TeleportClient, err error) { } else { // TODO (Joerger): init hardware key service (and client store) earlier where it can // be properly shared. - hardwareKeyService := piv.NewYubiKeyService(tc.CustomHardwareKeyPrompt) + hardwareKeyService := libhwk.NewService(context.TODO(), tc.CustomHardwareKeyPrompt) tc.ClientStore = NewFSClientStore(c.KeysDir, WithHardwareKeyService(hardwareKeyService)) if c.AddKeysToAgent == AddKeysToAgentOnly { // Store client keys in memory, but still save trusted certs and profile to disk. diff --git a/lib/hardwarekey/agent.go b/lib/hardwarekey/agent.go new file mode 100644 index 0000000000000..090ba9b9143ee --- /dev/null +++ b/lib/hardwarekey/agent.go @@ -0,0 +1,168 @@ +// 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 hardwarekey + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "log/slog" + "net" + "os" + "path/filepath" + "syscall" + + "github.com/gravitational/trace" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + hardwarekeyagentv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/hardwarekeyagent/v1" + "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/hardwarekeyagent" + "github.com/gravitational/teleport/lib/utils/cert" +) + +const ( + dirName = ".Teleport-PIV" + sockName = "agent.sock" + certFileName = "cert.pem" +) + +// DefaultAgentDir is the default dir for the hardware key agent's socket and certificate files. +func DefaultAgentDir() string { + return filepath.Join(os.TempDir(), dirName) +} + +// NewAgentClient opens a new hardware key agent client connected to the +// server based out of the given directory. +// +// [DefaultAgentDir] should be used for [keyAgentDir] outside of tests. +func NewAgentClient(ctx context.Context, keyAgentDir string) (hardwarekeyagentv1.HardwareKeyAgentServiceClient, error) { + socketPath := filepath.Join(keyAgentDir, sockName) + certPath := filepath.Join(keyAgentDir, certFileName) + + creds, err := credentials.NewClientTLSFromFile(certPath, "localhost") + if err != nil { + return nil, err + } + + return hardwarekeyagent.NewClient(ctx, socketPath, creds) +} + +// Server implementation [hardwarekeyagentv1.HardwareKeyAgentServiceServer]. +type Server struct { + grpcServer *grpc.Server + listener net.Listener + dir string +} + +// NewAgentServer returns a new hardware key agent server based out of the given directory. +// The given directory will be created when the server is served and destroyed with the server is stopped. +// +// [DefaultAgentDir] should be used for [keyAgentDir] outside of tests. +func NewAgentServer(ctx context.Context, s hardwarekey.Service, keyAgentDir string) (*Server, error) { + if err := os.MkdirAll(keyAgentDir, 0o700); err != nil { + return nil, trace.Wrap(err) + } + + l, err := newAgentListener(ctx, keyAgentDir) + if err != nil { + return nil, trace.Wrap(err) + } + + cert, err := generateServerCert(keyAgentDir) + if err != nil { + l.Close() + return nil, trace.Wrap(err) + } + + grpcServer := hardwarekeyagent.NewServer(ctx, s, credentials.NewServerTLSFromCert(&cert)) + return &Server{ + grpcServer: grpcServer, + listener: l, + dir: keyAgentDir, + }, nil +} + +func newAgentListener(ctx context.Context, keyAgentDir string) (net.Listener, error) { + socketPath := filepath.Join(keyAgentDir, sockName) + l, err := net.Listen("unix", socketPath) + if err == nil { + return l, nil + } else if !errors.Is(err, syscall.EADDRINUSE) { + return nil, trace.Wrap(err) + } + + // A hardware key agent already exists in the given path. Before replacing it, + // try to connect to it and see if it is active. + client, err := NewAgentClient(ctx, keyAgentDir) + if err == nil { + pong, err := client.Ping(ctx, &hardwarekeyagentv1.PingRequest{}) + if err == nil { + return nil, trace.AlreadyExists("another agent instance is already running; PID: %d", pong.Pid) + } + } + + // If it isn't running, remove the socket and try again. + if err := os.Remove(socketPath); err != nil { + return nil, trace.Wrap(err) + } + + if l, err = net.Listen("unix", socketPath); err != nil { + return nil, trace.Wrap(err) + } + + return l, nil +} + +func generateServerCert(keyAgentDir string) (tls.Certificate, error) { + creds, err := cert.GenerateSelfSignedCert([]string{"localhost"}, nil /*ipAddresses*/) + if err != nil { + return tls.Certificate{}, trace.Wrap(err, "failed to generate the certificate") + } + + certPath := filepath.Join(keyAgentDir, certFileName) + f, err := os.OpenFile(certPath, os.O_RDWR|os.O_CREATE, 0o600) + if err != nil { + return tls.Certificate{}, trace.Wrap(err) + } + if _, err = f.Write(creds.Cert); err != nil { + return tls.Certificate{}, trace.Wrap(err) + } + if err = f.Close(); err != nil { + return tls.Certificate{}, trace.Wrap(err) + } + + return keys.X509KeyPair(creds.Cert, creds.PrivateKey) +} + +// Serve the hardware key agent server. +func (s *Server) Serve(ctx context.Context) error { + fmt.Fprintln(os.Stderr, "Listening for hardware key agent requests") + context.AfterFunc(ctx, s.Stop) + return trace.Wrap(s.grpcServer.Serve(s.listener)) +} + +// Stop the hardware key agent server. +func (s *Server) Stop() { + s.grpcServer.GracefulStop() + if err := os.RemoveAll(s.dir); err != nil { + slog.DebugContext(context.TODO(), "failed to clear hardware key agent directory") + } +} diff --git a/lib/hardwarekey/agent_test.go b/lib/hardwarekey/agent_test.go new file mode 100644 index 0000000000000..99a551fa9db8d --- /dev/null +++ b/lib/hardwarekey/agent_test.go @@ -0,0 +1,75 @@ +// 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 hardwarekey_test + +import ( + "context" + "errors" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + + hardwarekeyagentv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/hardwarekeyagent/v1" + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + libhwk "github.com/gravitational/teleport/lib/hardwarekey" +) + +func TestHardwareKeyAgent_Server(t *testing.T) { + ctx := context.Background() + agentDir := t.TempDir() + + // Prepare the agent server + mockService := hardwarekey.NewMockHardwareKeyService(nil /*prompt*/) + server, err := libhwk.NewAgentServer(ctx, mockService, agentDir) + require.NoError(t, err) + t.Cleanup(server.Stop) + + serverErr := make(chan error, 1) + go func() { + serverErr <- server.Serve(ctx) + }() + + // Should fail to open a new server in the same directory. + _, err = libhwk.NewAgentServer(ctx, mockService, agentDir) + require.Error(t, err) + + // Existing server should be unaffected. + clt, err := libhwk.NewAgentClient(ctx, agentDir) + require.NoError(t, err) + _, err = clt.Ping(ctx, &hardwarekeyagentv1.PingRequest{}) + require.NoError(t, err) + + // If the server stops gracefully, the directory should be cleaned up and a new server can be started. + server.Stop() + require.Eventually(t, func() bool { + _, err := os.Stat(agentDir) + return errors.Is(err, os.ErrNotExist) + }, 5*time.Second, 100*time.Millisecond) + server, err = libhwk.NewAgentServer(ctx, mockService, agentDir) + require.NoError(t, err) + t.Cleanup(server.Stop) + + // If the server is unresponsive, it should be replaced by a call to NewServer. + // Use a timeoutCtx so that the failed Ping request fails quickly. + timeoutCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) + defer cancel() + server, err = libhwk.NewAgentServer(timeoutCtx, mockService, agentDir) + require.NoError(t, err) + t.Cleanup(server.Stop) +} diff --git a/lib/hardwarekey/service.go b/lib/hardwarekey/service.go new file mode 100644 index 0000000000000..6af7853045ba5 --- /dev/null +++ b/lib/hardwarekey/service.go @@ -0,0 +1,41 @@ +// 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 hardwarekey provides common hardware key types and functions. +// Hardware key types and functions which are not used within /api should be placed here. +package hardwarekey + +import ( + "context" + + "github.com/gravitational/teleport/api/utils/keys/hardwarekey" + "github.com/gravitational/teleport/api/utils/keys/hardwarekeyagent" + "github.com/gravitational/teleport/api/utils/keys/piv" +) + +// NewService prepares a new hardware key service. If a running hardware key agent +// is found, this will return a hardware key agent service with a direct PIV service as backup. +// Otherwise, the direct PIV service will be returned. +func NewService(ctx context.Context, prompt hardwarekey.Prompt) hardwarekey.Service { + hwks := piv.NewYubiKeyService(prompt) + + agentClient, err := NewAgentClient(ctx, DefaultAgentDir()) + if err == nil { + return hardwarekeyagent.NewService(agentClient, hwks) + } + + return hwks +} diff --git a/lib/teleterm/config.go b/lib/teleterm/config.go index 42320f4a17192..3c4fa7d284fda 100644 --- a/lib/teleterm/config.go +++ b/lib/teleterm/config.go @@ -49,6 +49,8 @@ type Config struct { InstallationID string // AddKeysToAgent is passed to [client.Config]. AddKeysToAgent string + // HardwareKeyAgent determines whether the daemon will run the hardware key agent. + HardwareKeyAgent bool } // CheckAndSetDefaults checks and sets default config values. diff --git a/lib/teleterm/teleterm.go b/lib/teleterm/teleterm.go index e721285906a45..75cffb256e238 100644 --- a/lib/teleterm/teleterm.go +++ b/lib/teleterm/teleterm.go @@ -32,6 +32,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/gravitational/teleport/api/utils/keys/piv" + libhwk "github.com/gravitational/teleport/lib/hardwarekey" "github.com/gravitational/teleport/lib/teleterm/apiserver" "github.com/gravitational/teleport/lib/teleterm/clusteridcache" "github.com/gravitational/teleport/lib/teleterm/clusters" @@ -99,6 +101,21 @@ func Serve(ctx context.Context, cfg Config) error { serverAPIWait <- err }() + var hardwareKeyAgentServer *libhwk.Server + if cfg.HardwareKeyAgent { + hardwareKeyService := piv.NewYubiKeyService(daemonService.NewHardwareKeyPrompt()) + hardwareKeyAgentServer, err = libhwk.NewAgentServer(ctx, hardwareKeyService, libhwk.DefaultAgentDir()) + if err != nil { + slog.WarnContext(ctx, "failed to create the hardware key agent server", "err", err) + } else { + go func() { + if err := hardwareKeyAgentServer.Serve(ctx); err != nil { + slog.WarnContext(ctx, "hardware key agent server error", "err", err) + } + }() + } + } + // Wait for shutdown signals go func() { shutdownSignals := []os.Signal{os.Interrupt, syscall.SIGTERM} @@ -114,6 +131,10 @@ func Serve(ctx context.Context, cfg Config) error { daemonService.Stop() apiServer.Stop() + + if hardwareKeyAgentServer != nil { + hardwareKeyAgentServer.Stop() + } }() errAPI := <-serverAPIWait diff --git a/tool/tctl/common/config/profile.go b/tool/tctl/common/config/profile.go index e6252a551ed23..881068a919834 100644 --- a/tool/tctl/common/config/profile.go +++ b/tool/tctl/common/config/profile.go @@ -29,10 +29,10 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/metadata" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/identityfile" + libhwk "github.com/gravitational/teleport/lib/hardwarekey" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" logutils "github.com/gravitational/teleport/lib/utils/log" @@ -46,7 +46,7 @@ func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authcl proxyAddr = ccf.AuthServerAddr[0] } - hwks := piv.NewYubiKeyService(nil /*prompt*/) + hwks := libhwk.NewService(ctx, nil /*prompt*/) clientStore := client.NewFSClientStore(cfg.TeleportHome, client.WithHardwareKeyService(hwks)) if ccf.IdentityFilePath != "" { var err error diff --git a/tool/tsh/common/daemon.go b/tool/tsh/common/daemon.go index dda2baf01c97c..311e7caddc709 100644 --- a/tool/tsh/common/daemon.go +++ b/tool/tsh/common/daemon.go @@ -51,6 +51,7 @@ func onDaemonStart(cf *CLIConf) error { AgentsDir: cf.DaemonAgentsDir, InstallationID: cf.DaemonInstallationID, AddKeysToAgent: cf.AddKeysToAgent, + HardwareKeyAgent: cf.HardwareKeyAgent, }) if err != nil { return trace.Wrap(err) diff --git a/tool/tsh/common/piv.go b/tool/tsh/common/piv.go new file mode 100644 index 0000000000000..16d6097e6e7c6 --- /dev/null +++ b/tool/tsh/common/piv.go @@ -0,0 +1,59 @@ +/* + * 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 ( + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/utils/keys/piv" + libhwk "github.com/gravitational/teleport/lib/hardwarekey" +) + +type pivCommands struct { + agent *pivAgentCommand +} + +func newPIVCommands(app *kingpin.Application) pivCommands { + piv := app.Command("piv", "PIV commands.").Hidden() + return pivCommands{ + agent: newPIVAgentCommand(piv), + } +} + +// pivAgentCommand implements `tsh piv agent`. +type pivAgentCommand struct { + *kingpin.CmdClause +} + +func newPIVAgentCommand(parent *kingpin.CmdClause) *pivAgentCommand { + cmd := &pivAgentCommand{ + CmdClause: parent.Command("agent", "Start PIV key agent."), + } + return cmd +} + +func (c *pivAgentCommand) run(cf *CLIConf) error { + hwKeyService := piv.NewYubiKeyService(nil /*prompt*/) + s, err := libhwk.NewAgentServer(cf.Context, hwKeyService, libhwk.DefaultAgentDir()) + if err != nil { + return trace.Wrap(err) + } + return s.Serve(cf.Context) +} diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index f645cd1eb208c..53d93cb6e7e20 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -68,7 +68,6 @@ import ( apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/api/types/wrappers" "github.com/gravitational/teleport/api/utils/keys/hardwarekey" - "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/lib/asciitable" "github.com/gravitational/teleport/lib/auth/authclient" @@ -82,6 +81,7 @@ import ( "github.com/gravitational/teleport/lib/defaults" dtauthn "github.com/gravitational/teleport/lib/devicetrust/authn" dtenroll "github.com/gravitational/teleport/lib/devicetrust/enroll" + libhwk "github.com/gravitational/teleport/lib/hardwarekey" "github.com/gravitational/teleport/lib/kube/kubeconfig" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/observability/tracing" @@ -578,6 +578,9 @@ type CLIConf struct { // lookPathOverride overrides return of LookPath(). used in tests. lookPathOverride string + + // HardwareKeyAgent determines whether the daemon will run the hardware key agent. + HardwareKeyAgent bool } // Stdout returns the stdout writer. @@ -859,6 +862,7 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { daemonStart.Flag("kubeconfigs-dir", "Directory containing kubeconfig for Kubernetes Access").StringVar(&cf.DaemonKubeconfigsDir) daemonStart.Flag("agents-dir", "Directory containing agent config files and data directories for Connect My Computer").StringVar(&cf.DaemonAgentsDir) daemonStart.Flag("installation-id", "Unique ID identifying a specific Teleport Connect installation").StringVar(&cf.DaemonInstallationID) + daemonStart.Flag("hardware-key-agent", "Serve the hardware key agent as part of the daemon process").BoolVar(&cf.HardwareKeyAgent) daemonStop := daemon.Command("stop", "Gracefully stops a process on Windows by sending Ctrl-Break to it.").Hidden() daemonStop.Flag("pid", "PID to be stopped").IntVar(&cf.DaemonPid) @@ -1270,6 +1274,7 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { vnetUninstallServiceCommand := newVnetUninstallServiceCommand(app) gitCmd := newGitCommands(app) + pivCmd := newPIVCommands(app) if runtime.GOOS == constants.WindowsOS { bench.Hidden() @@ -1669,6 +1674,8 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { err = gitCmd.config.run(&cf) case gitCmd.clone.FullCommand(): err = gitCmd.clone.run(&cf) + case pivCmd.agent.FullCommand(): + err = pivCmd.agent.run(&cf) default: // Handle commands that might not be available. switch { @@ -4599,7 +4606,7 @@ func setEnvVariables(c *client.Config, options Options) { } func initClientStore(cf *CLIConf, proxy string) (*client.Store, error) { - hwks := piv.NewYubiKeyService(nil /*prompt*/) + hwks := libhwk.NewService(cf.Context, nil /*prompt*/) switch { case cf.IdentityFileIn != "": diff --git a/tool/tsh/common/vnet_client_application.go b/tool/tsh/common/vnet_client_application.go index 87bba66f56729..79d4a61fc330a 100644 --- a/tool/tsh/common/vnet_client_application.go +++ b/tool/tsh/common/vnet_client_application.go @@ -28,10 +28,10 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/utils/keys/piv" vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" + libhwk "github.com/gravitational/teleport/lib/hardwarekey" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/vnet" ) @@ -46,7 +46,7 @@ type vnetClientApplication struct { } func newVnetClientApplication(cf *CLIConf) (*vnetClientApplication, error) { - hwks := piv.NewYubiKeyService(nil /*prompt*/) + hwks := libhwk.NewService(cf.Context, nil /*prompt*/) clientStore := client.NewFSClientStore(cf.HomePath, client.WithHardwareKeyService(hwks)) p := &vnetClientApplication{ diff --git a/web/packages/teleterm/src/mainProcess/mainProcess.ts b/web/packages/teleterm/src/mainProcess/mainProcess.ts index 5607072526168..a2d465e5e75e4 100644 --- a/web/packages/teleterm/src/mainProcess/mainProcess.ts +++ b/web/packages/teleterm/src/mainProcess/mainProcess.ts @@ -229,6 +229,9 @@ export default class MainProcess { `--add-keys-to-agent=${this.configService.get('sshAgent.addKeysToAgent').value}`, ]; + if (this.configService.get('hardwareKeyAgent.enabled').value) { + flags.unshift('--hardware-key-agent'); + } if (settings.insecure) { flags.unshift('--insecure'); } diff --git a/web/packages/teleterm/src/services/config/appConfigSchema.ts b/web/packages/teleterm/src/services/config/appConfigSchema.ts index d24d72343fbc9..2c63bf352f44a 100644 --- a/web/packages/teleterm/src/services/config/appConfigSchema.ts +++ b/web/packages/teleterm/src/services/config/appConfigSchema.ts @@ -213,6 +213,12 @@ export const createAppConfigSchema = (settings: RuntimeSettings) => { "'no' never attempts to add them, 'yes' always attempts to add them, " + "'only' always attempts to add the keys to the agent but it does not save them on disk." ), + // Defaults to true for prod, false for dev. Otherwise dev instances would + // claim the hardware key agent runner over prod instances by default. + 'hardwareKeyAgent.enabled': z + .boolean() + .default(!settings.dev) + .describe('Controls whether the hardware key agent will be started.'), }); };