From 5791a9888a4820e1230bab2cddec3640f0135808 Mon Sep 17 00:00:00 2001 From: Forrest <30576607+fspmarshall@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:51:37 -0800 Subject: [PATCH] Add host fields and protos for ssh identities (#51024) * add host fields to sshca.Identity * add ssh identity proto --- .../decision/v1alpha1/ssh_identity.pb.go | 674 +++++++++++++++++- .../decision/v1alpha1/ssh_identity.proto | 167 ++++- integration/helpers/instance.go | 9 +- lib/auth/auth.go | 45 +- lib/auth/auth_test.go | 2 +- lib/auth/auth_with_roles.go | 8 +- lib/auth/init_test.go | 41 +- lib/auth/keygen/keygen.go | 69 +- lib/auth/keygen/keygen_test.go | 28 +- lib/auth/sessions.go | 4 +- lib/auth/test/suite.go | 37 +- lib/auth/testauthority/testauthority.go | 5 +- lib/client/client_store_test.go | 11 +- lib/client/identityfile/identity_test.go | 4 +- lib/client/keyagent_test.go | 33 +- lib/client/known_hosts_migrate_test.go | 8 +- lib/decision/ssh_identity.go | 143 ++++ lib/decision/ssh_identity_test.go | 101 +++ lib/reversetunnel/srv_test.go | 11 +- lib/services/authority.go | 42 -- lib/srv/authhandlers_test.go | 8 +- lib/srv/git/forward_test.go | 10 +- lib/sshca/identity.go | 117 ++- lib/sshca/identity_test.go | 49 +- lib/sshca/sshca.go | 54 +- 25 files changed, 1402 insertions(+), 278 deletions(-) create mode 100644 lib/decision/ssh_identity.go create mode 100644 lib/decision/ssh_identity_test.go diff --git a/api/gen/proto/go/teleport/decision/v1alpha1/ssh_identity.pb.go b/api/gen/proto/go/teleport/decision/v1alpha1/ssh_identity.pb.go index afd8582b4e39d..8e47b7ca4109f 100644 --- a/api/gen/proto/go/teleport/decision/v1alpha1/ssh_identity.pb.go +++ b/api/gen/proto/go/teleport/decision/v1alpha1/ssh_identity.pb.go @@ -21,8 +21,10 @@ package decisionpb import ( + v1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/trait/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -35,11 +37,199 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// CertExtensionMode specifies the type of extension to use in the cert. This type +// must be kept up to date with types.CertExtensionMode. +type CertExtensionMode int32 + +const ( + // CERT_EXTENSION_MODE_UNSPECIFIED is the default value and should not be used. + CertExtensionMode_CERT_EXTENSION_MODE_UNSPECIFIED CertExtensionMode = 0 + // EXTENSION represents a cert extension that may or may not be + // honored by the server. + CertExtensionMode_CERT_EXTENSION_MODE_EXTENSION CertExtensionMode = 1 +) + +// Enum value maps for CertExtensionMode. +var ( + CertExtensionMode_name = map[int32]string{ + 0: "CERT_EXTENSION_MODE_UNSPECIFIED", + 1: "CERT_EXTENSION_MODE_EXTENSION", + } + CertExtensionMode_value = map[string]int32{ + "CERT_EXTENSION_MODE_UNSPECIFIED": 0, + "CERT_EXTENSION_MODE_EXTENSION": 1, + } +) + +func (x CertExtensionMode) Enum() *CertExtensionMode { + p := new(CertExtensionMode) + *p = x + return p +} + +func (x CertExtensionMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CertExtensionMode) Descriptor() protoreflect.EnumDescriptor { + return file_teleport_decision_v1alpha1_ssh_identity_proto_enumTypes[0].Descriptor() +} + +func (CertExtensionMode) Type() protoreflect.EnumType { + return &file_teleport_decision_v1alpha1_ssh_identity_proto_enumTypes[0] +} + +func (x CertExtensionMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CertExtensionMode.Descriptor instead. +func (CertExtensionMode) EnumDescriptor() ([]byte, []int) { + return file_teleport_decision_v1alpha1_ssh_identity_proto_rawDescGZIP(), []int{0} +} + +// CertExtensionType represents the certificate type the extension is for. +// Currently only ssh is supported. This type must be kept up to date with +// types.CertExtensionType. +type CertExtensionType int32 + +const ( + // CERT_EXTENSION_TYPE_UNSPECIFIED is the default value and should not be used. + CertExtensionType_CERT_EXTENSION_TYPE_UNSPECIFIED CertExtensionType = 0 + // SSH is used when extending an ssh certificate + CertExtensionType_CERT_EXTENSION_TYPE_SSH CertExtensionType = 1 +) + +// Enum value maps for CertExtensionType. +var ( + CertExtensionType_name = map[int32]string{ + 0: "CERT_EXTENSION_TYPE_UNSPECIFIED", + 1: "CERT_EXTENSION_TYPE_SSH", + } + CertExtensionType_value = map[string]int32{ + "CERT_EXTENSION_TYPE_UNSPECIFIED": 0, + "CERT_EXTENSION_TYPE_SSH": 1, + } +) + +func (x CertExtensionType) Enum() *CertExtensionType { + p := new(CertExtensionType) + *p = x + return p +} + +func (x CertExtensionType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CertExtensionType) Descriptor() protoreflect.EnumDescriptor { + return file_teleport_decision_v1alpha1_ssh_identity_proto_enumTypes[1].Descriptor() +} + +func (CertExtensionType) Type() protoreflect.EnumType { + return &file_teleport_decision_v1alpha1_ssh_identity_proto_enumTypes[1] +} + +func (x CertExtensionType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CertExtensionType.Descriptor instead. +func (CertExtensionType) EnumDescriptor() ([]byte, []int) { + return file_teleport_decision_v1alpha1_ssh_identity_proto_rawDescGZIP(), []int{1} +} + // SSHIdentity is the identity used for SSH connections. type SSHIdentity struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + // ValidAfter is the unix timestamp that marks the start time for when the certificate should + // be considered valid. + ValidAfter uint64 `protobuf:"varint,1,opt,name=valid_after,json=validAfter,proto3" json:"valid_after,omitempty"` + // ValidBefore is the unix timestamp that marks the end time for when the certificate should + // be considered valid. + ValidBefore uint64 `protobuf:"varint,2,opt,name=valid_before,json=validBefore,proto3" json:"valid_before,omitempty"` + // CertType indicates what type of cert this is (user or host). + CertType uint32 `protobuf:"varint,3,opt,name=cert_type,json=certType,proto3" json:"cert_type,omitempty"` + // Principals is the list of SSH principals associated with the certificate (this means the + // list of allowed unix logins in the case of user certs). + Principals []string `protobuf:"bytes,4,rep,name=principals,proto3" json:"principals,omitempty"` + // ClusterName is the name of the cluster within which a node lives + ClusterName string `protobuf:"bytes,5,opt,name=cluster_name,json=clusterName,proto3" json:"cluster_name,omitempty"` + // SystemRole identifies the system role of a Teleport instance + SystemRole string `protobuf:"bytes,6,opt,name=system_role,json=systemRole,proto3" json:"system_role,omitempty"` + // Username is teleport username + Username string `protobuf:"bytes,7,opt,name=username,proto3" json:"username,omitempty"` + // Impersonator is set when a user requests certificate for another user + Impersonator string `protobuf:"bytes,8,opt,name=impersonator,proto3" json:"impersonator,omitempty"` + // PermitX11Forwarding permits X11 forwarding for this cert + PermitX11Forwarding bool `protobuf:"varint,9,opt,name=permit_x11_forwarding,json=permitX11Forwarding,proto3" json:"permit_x11_forwarding,omitempty"` + // PermitAgentForwarding permits agent forwarding for this cert + PermitAgentForwarding bool `protobuf:"varint,10,opt,name=permit_agent_forwarding,json=permitAgentForwarding,proto3" json:"permit_agent_forwarding,omitempty"` + // PermitPortForwarding permits port forwarding. + PermitPortForwarding bool `protobuf:"varint,11,opt,name=permit_port_forwarding,json=permitPortForwarding,proto3" json:"permit_port_forwarding,omitempty"` + // Roles is a list of roles assigned to this user + Roles []string `protobuf:"bytes,12,rep,name=roles,proto3" json:"roles,omitempty"` + // RouteToCluster specifies the target cluster + // if present in the certificate, will be used + // to route the requests to + RouteToCluster string `protobuf:"bytes,13,opt,name=route_to_cluster,json=routeToCluster,proto3" json:"route_to_cluster,omitempty"` + // Traits hold claim data used to populate a role at runtime. + Traits []*v1.Trait `protobuf:"bytes,14,rep,name=traits,proto3" json:"traits,omitempty"` + // ActiveRequests tracks privilege escalation requests applied during + // certificate construction. + ActiveRequests []string `protobuf:"bytes,15,rep,name=active_requests,json=activeRequests,proto3" json:"active_requests,omitempty"` + // MFAVerified is the UUID of an MFA device when this Identity was + // confirmed immediately after an MFA check. + MfaVerified string `protobuf:"bytes,16,opt,name=mfa_verified,json=mfaVerified,proto3" json:"mfa_verified,omitempty"` + // PreviousIdentityExpires is the expiry time of the identity/cert that this + // identity/cert was derived from. It is used to determine a session's hard + // deadline in cases where both require_session_mfa and disconnect_expired_cert + // are enabled. See https://github.com/gravitational/teleport/issues/18544. + PreviousIdentityExpires *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=previous_identity_expires,json=previousIdentityExpires,proto3" json:"previous_identity_expires,omitempty"` + // LoginIP is an observed IP of the client on the moment of certificate creation. + LoginIp string `protobuf:"bytes,18,opt,name=login_ip,json=loginIp,proto3" json:"login_ip,omitempty"` + // PinnedIP is an IP from which client must communicate with Teleport. + PinnedIp string `protobuf:"bytes,19,opt,name=pinned_ip,json=pinnedIp,proto3" json:"pinned_ip,omitempty"` + // DisallowReissue flags that any attempt to request new certificates while + // authenticated with this cert should be denied. + DisallowReissue bool `protobuf:"varint,20,opt,name=disallow_reissue,json=disallowReissue,proto3" json:"disallow_reissue,omitempty"` + // CertificateExtensions are user configured ssh key extensions (note: this field also + // ends up aggregating all *unknown* extensions during cert parsing, meaning that this + // can sometimes contain fields that were inserted by a newer version of teleport). + CertificateExtensions []*CertExtension `protobuf:"bytes,21,rep,name=certificate_extensions,json=certificateExtensions,proto3" json:"certificate_extensions,omitempty"` + // Renewable indicates this certificate is renewable. + Renewable bool `protobuf:"varint,22,opt,name=renewable,proto3" json:"renewable,omitempty"` + // Generation counts the number of times a certificate has been renewed, with a generation of 1 + // meaning the cert has never been renewed. A generation of zero means the cert's generation is + // not being tracked. + Generation uint64 `protobuf:"varint,23,opt,name=generation,proto3" json:"generation,omitempty"` + // BotName is set to the name of the bot, if the user is a Machine ID bot user. + // Empty for human users. + BotName string `protobuf:"bytes,24,opt,name=bot_name,json=botName,proto3" json:"bot_name,omitempty"` + // BotInstanceID is the unique identifier for the bot instance, if this is a + // Machine ID bot. It is empty for human users. + BotInstanceId string `protobuf:"bytes,25,opt,name=bot_instance_id,json=botInstanceId,proto3" json:"bot_instance_id,omitempty"` + // AllowedResourceIDs lists the resources the user should be able to access. + AllowedResourceIds []*ResourceId `protobuf:"bytes,26,rep,name=allowed_resource_ids,json=allowedResourceIds,proto3" json:"allowed_resource_ids,omitempty"` + // ConnectionDiagnosticID references the ConnectionDiagnostic that we should use to append traces when testing a Connection. + ConnectionDiagnosticId string `protobuf:"bytes,27,opt,name=connection_diagnostic_id,json=connectionDiagnosticId,proto3" json:"connection_diagnostic_id,omitempty"` + // PrivateKeyPolicy is the private key policy supported by this certificate. + PrivateKeyPolicy string `protobuf:"bytes,28,opt,name=private_key_policy,json=privateKeyPolicy,proto3" json:"private_key_policy,omitempty"` + // DeviceID is the trusted device identifier. + DeviceId string `protobuf:"bytes,29,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"` + // DeviceAssetTag is the device inventory identifier. + DeviceAssetTag string `protobuf:"bytes,30,opt,name=device_asset_tag,json=deviceAssetTag,proto3" json:"device_asset_tag,omitempty"` + // DeviceCredentialID is the identifier for the credential used by the device + // to authenticate itself. + DeviceCredentialId string `protobuf:"bytes,31,opt,name=device_credential_id,json=deviceCredentialId,proto3" json:"device_credential_id,omitempty"` + // GitHubUserID indicates the GitHub user ID identified by the GitHub + // connector. + GithubUserId string `protobuf:"bytes,32,opt,name=github_user_id,json=githubUserId,proto3" json:"github_user_id,omitempty"` + // GitHubUsername indicates the GitHub username identified by the GitHub + // connector. + GithubUsername string `protobuf:"bytes,33,opt,name=github_username,json=githubUsername,proto3" json:"github_username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SSHIdentity) Reset() { @@ -72,6 +262,315 @@ func (*SSHIdentity) Descriptor() ([]byte, []int) { return file_teleport_decision_v1alpha1_ssh_identity_proto_rawDescGZIP(), []int{0} } +func (x *SSHIdentity) GetValidAfter() uint64 { + if x != nil { + return x.ValidAfter + } + return 0 +} + +func (x *SSHIdentity) GetValidBefore() uint64 { + if x != nil { + return x.ValidBefore + } + return 0 +} + +func (x *SSHIdentity) GetCertType() uint32 { + if x != nil { + return x.CertType + } + return 0 +} + +func (x *SSHIdentity) GetPrincipals() []string { + if x != nil { + return x.Principals + } + return nil +} + +func (x *SSHIdentity) GetClusterName() string { + if x != nil { + return x.ClusterName + } + return "" +} + +func (x *SSHIdentity) GetSystemRole() string { + if x != nil { + return x.SystemRole + } + return "" +} + +func (x *SSHIdentity) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *SSHIdentity) GetImpersonator() string { + if x != nil { + return x.Impersonator + } + return "" +} + +func (x *SSHIdentity) GetPermitX11Forwarding() bool { + if x != nil { + return x.PermitX11Forwarding + } + return false +} + +func (x *SSHIdentity) GetPermitAgentForwarding() bool { + if x != nil { + return x.PermitAgentForwarding + } + return false +} + +func (x *SSHIdentity) GetPermitPortForwarding() bool { + if x != nil { + return x.PermitPortForwarding + } + return false +} + +func (x *SSHIdentity) GetRoles() []string { + if x != nil { + return x.Roles + } + return nil +} + +func (x *SSHIdentity) GetRouteToCluster() string { + if x != nil { + return x.RouteToCluster + } + return "" +} + +func (x *SSHIdentity) GetTraits() []*v1.Trait { + if x != nil { + return x.Traits + } + return nil +} + +func (x *SSHIdentity) GetActiveRequests() []string { + if x != nil { + return x.ActiveRequests + } + return nil +} + +func (x *SSHIdentity) GetMfaVerified() string { + if x != nil { + return x.MfaVerified + } + return "" +} + +func (x *SSHIdentity) GetPreviousIdentityExpires() *timestamppb.Timestamp { + if x != nil { + return x.PreviousIdentityExpires + } + return nil +} + +func (x *SSHIdentity) GetLoginIp() string { + if x != nil { + return x.LoginIp + } + return "" +} + +func (x *SSHIdentity) GetPinnedIp() string { + if x != nil { + return x.PinnedIp + } + return "" +} + +func (x *SSHIdentity) GetDisallowReissue() bool { + if x != nil { + return x.DisallowReissue + } + return false +} + +func (x *SSHIdentity) GetCertificateExtensions() []*CertExtension { + if x != nil { + return x.CertificateExtensions + } + return nil +} + +func (x *SSHIdentity) GetRenewable() bool { + if x != nil { + return x.Renewable + } + return false +} + +func (x *SSHIdentity) GetGeneration() uint64 { + if x != nil { + return x.Generation + } + return 0 +} + +func (x *SSHIdentity) GetBotName() string { + if x != nil { + return x.BotName + } + return "" +} + +func (x *SSHIdentity) GetBotInstanceId() string { + if x != nil { + return x.BotInstanceId + } + return "" +} + +func (x *SSHIdentity) GetAllowedResourceIds() []*ResourceId { + if x != nil { + return x.AllowedResourceIds + } + return nil +} + +func (x *SSHIdentity) GetConnectionDiagnosticId() string { + if x != nil { + return x.ConnectionDiagnosticId + } + return "" +} + +func (x *SSHIdentity) GetPrivateKeyPolicy() string { + if x != nil { + return x.PrivateKeyPolicy + } + return "" +} + +func (x *SSHIdentity) GetDeviceId() string { + if x != nil { + return x.DeviceId + } + return "" +} + +func (x *SSHIdentity) GetDeviceAssetTag() string { + if x != nil { + return x.DeviceAssetTag + } + return "" +} + +func (x *SSHIdentity) GetDeviceCredentialId() string { + if x != nil { + return x.DeviceCredentialId + } + return "" +} + +func (x *SSHIdentity) GetGithubUserId() string { + if x != nil { + return x.GithubUserId + } + return "" +} + +func (x *SSHIdentity) GetGithubUsername() string { + if x != nil { + return x.GithubUsername + } + return "" +} + +// CertExtension represents a key/value for a certificate extension. This type must +// be kept up to date with types.CertExtension. +type CertExtension struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Type represents the certificate type being extended, only ssh + // is supported at this time. + // 0 is "ssh". + Type CertExtensionType `protobuf:"varint,1,opt,name=type,proto3,enum=teleport.decision.v1alpha1.CertExtensionType" json:"type,omitempty"` + // Mode is the type of extension to be used -- currently + // critical-option is not supported. + // 0 is "extension". + Mode CertExtensionMode `protobuf:"varint,2,opt,name=mode,proto3,enum=teleport.decision.v1alpha1.CertExtensionMode" json:"mode,omitempty"` + // Name specifies the key to be used in the cert extension. + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // Value specifies the value to be used in the cert extension. + Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CertExtension) Reset() { + *x = CertExtension{} + mi := &file_teleport_decision_v1alpha1_ssh_identity_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CertExtension) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertExtension) ProtoMessage() {} + +func (x *CertExtension) ProtoReflect() protoreflect.Message { + mi := &file_teleport_decision_v1alpha1_ssh_identity_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 CertExtension.ProtoReflect.Descriptor instead. +func (*CertExtension) Descriptor() ([]byte, []int) { + return file_teleport_decision_v1alpha1_ssh_identity_proto_rawDescGZIP(), []int{1} +} + +func (x *CertExtension) GetType() CertExtensionType { + if x != nil { + return x.Type + } + return CertExtensionType_CERT_EXTENSION_TYPE_UNSPECIFIED +} + +func (x *CertExtension) GetMode() CertExtensionMode { + if x != nil { + return x.Mode + } + return CertExtensionMode_CERT_EXTENSION_MODE_UNSPECIFIED +} + +func (x *CertExtension) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CertExtension) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + var File_teleport_decision_v1alpha1_ssh_identity_proto protoreflect.FileDescriptor var file_teleport_decision_v1alpha1_ssh_identity_proto_rawDesc = string([]byte{ @@ -79,14 +578,134 @@ var file_teleport_decision_v1alpha1_ssh_identity_proto_rawDesc = string([]byte{ 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x73, 0x73, 0x68, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1a, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x22, 0x0d, 0x0a, 0x0b, 0x53, - 0x53, 0x48, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x42, 0x5a, 0x5a, 0x58, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, - 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x64, 0x65, 0x63, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2d, 0x74, 0x65, + 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2f, + 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x74, 0x6c, 0x73, 0x5f, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1d, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x74, 0x72, 0x61, 0x69, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x74, + 0x72, 0x61, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9a, 0x0b, 0x0a, 0x0b, 0x53, + 0x53, 0x48, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x1b, + 0x0a, 0x09, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x08, 0x63, 0x65, 0x72, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, + 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0a, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x63, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, + 0x0a, 0x0b, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x6f, 0x6c, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x69, + 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x12, + 0x32, 0x0a, 0x15, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x5f, 0x78, 0x31, 0x31, 0x5f, 0x66, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x58, 0x31, 0x31, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x69, 0x6e, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x5f, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x16, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x70, 0x65, 0x72, + 0x6d, 0x69, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, + 0x67, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x5f, 0x74, 0x6f, 0x5f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x54, 0x6f, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, + 0x72, 0x12, 0x30, 0x0a, 0x06, 0x74, 0x72, 0x61, 0x69, 0x74, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x74, 0x72, 0x61, + 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x69, 0x74, 0x52, 0x06, 0x74, 0x72, 0x61, + 0x69, 0x74, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x21, 0x0a, 0x0c, + 0x6d, 0x66, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x10, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x66, 0x61, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, + 0x56, 0x0a, 0x19, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x11, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x17, + 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x69, 0x6e, + 0x5f, 0x69, 0x70, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x69, 0x6e, + 0x49, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x18, + 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x49, 0x70, 0x12, + 0x29, 0x0a, 0x10, 0x64, 0x69, 0x73, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x65, 0x69, 0x73, + 0x73, 0x75, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x69, 0x73, 0x73, 0x75, 0x65, 0x12, 0x60, 0x0a, 0x16, 0x63, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x15, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x45, 0x78, 0x74, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x15, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, + 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x16, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x09, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x67, 0x65, + 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x17, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, + 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6f, + 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x18, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x6f, + 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x62, 0x6f, 0x74, 0x5f, 0x69, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x19, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x62, 0x6f, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x58, 0x0a, + 0x14, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x1a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x64, 0x52, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x73, 0x12, 0x38, 0x0a, 0x18, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, + 0x5f, 0x69, 0x64, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x49, + 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, + 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x1d, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x61, 0x67, + 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x54, 0x61, 0x67, 0x12, 0x30, 0x0a, 0x14, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x1f, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x20, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x27, + 0x0a, 0x0f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x21, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x55, + 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xbf, 0x01, 0x0a, 0x0d, 0x43, 0x65, 0x72, 0x74, + 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x41, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x61, 0x6c, + 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, + 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x41, 0x0a, 0x04, + 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x45, 0x78, 0x74, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x5b, 0x0a, 0x11, 0x43, 0x65, 0x72, + 0x74, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x23, + 0x0a, 0x1f, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x45, 0x58, 0x54, 0x45, 0x4e, 0x53, 0x49, 0x4f, 0x4e, + 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x45, 0x58, 0x54, 0x45, + 0x4e, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x45, 0x58, 0x54, 0x45, 0x4e, + 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x01, 0x2a, 0x55, 0x0a, 0x11, 0x43, 0x65, 0x72, 0x74, 0x45, 0x78, + 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x23, 0x0a, 0x1f, 0x43, + 0x45, 0x52, 0x54, 0x5f, 0x45, 0x58, 0x54, 0x45, 0x4e, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x45, 0x58, 0x54, 0x45, 0x4e, 0x53, 0x49, + 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x53, 0x48, 0x10, 0x01, 0x42, 0x5a, 0x5a, + 0x58, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, + 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x64, 0x65, 0x63, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x64, + 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, }) var ( @@ -101,16 +720,29 @@ func file_teleport_decision_v1alpha1_ssh_identity_proto_rawDescGZIP() []byte { return file_teleport_decision_v1alpha1_ssh_identity_proto_rawDescData } -var file_teleport_decision_v1alpha1_ssh_identity_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_teleport_decision_v1alpha1_ssh_identity_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_teleport_decision_v1alpha1_ssh_identity_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_teleport_decision_v1alpha1_ssh_identity_proto_goTypes = []any{ - (*SSHIdentity)(nil), // 0: teleport.decision.v1alpha1.SSHIdentity + (CertExtensionMode)(0), // 0: teleport.decision.v1alpha1.CertExtensionMode + (CertExtensionType)(0), // 1: teleport.decision.v1alpha1.CertExtensionType + (*SSHIdentity)(nil), // 2: teleport.decision.v1alpha1.SSHIdentity + (*CertExtension)(nil), // 3: teleport.decision.v1alpha1.CertExtension + (*v1.Trait)(nil), // 4: teleport.trait.v1.Trait + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*ResourceId)(nil), // 6: teleport.decision.v1alpha1.ResourceId } var file_teleport_decision_v1alpha1_ssh_identity_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 4, // 0: teleport.decision.v1alpha1.SSHIdentity.traits:type_name -> teleport.trait.v1.Trait + 5, // 1: teleport.decision.v1alpha1.SSHIdentity.previous_identity_expires:type_name -> google.protobuf.Timestamp + 3, // 2: teleport.decision.v1alpha1.SSHIdentity.certificate_extensions:type_name -> teleport.decision.v1alpha1.CertExtension + 6, // 3: teleport.decision.v1alpha1.SSHIdentity.allowed_resource_ids:type_name -> teleport.decision.v1alpha1.ResourceId + 1, // 4: teleport.decision.v1alpha1.CertExtension.type:type_name -> teleport.decision.v1alpha1.CertExtensionType + 0, // 5: teleport.decision.v1alpha1.CertExtension.mode:type_name -> teleport.decision.v1alpha1.CertExtensionMode + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_teleport_decision_v1alpha1_ssh_identity_proto_init() } @@ -118,18 +750,20 @@ func file_teleport_decision_v1alpha1_ssh_identity_proto_init() { if File_teleport_decision_v1alpha1_ssh_identity_proto != nil { return } + file_teleport_decision_v1alpha1_tls_identity_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_decision_v1alpha1_ssh_identity_proto_rawDesc), len(file_teleport_decision_v1alpha1_ssh_identity_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, + NumEnums: 2, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, GoTypes: file_teleport_decision_v1alpha1_ssh_identity_proto_goTypes, DependencyIndexes: file_teleport_decision_v1alpha1_ssh_identity_proto_depIdxs, + EnumInfos: file_teleport_decision_v1alpha1_ssh_identity_proto_enumTypes, MessageInfos: file_teleport_decision_v1alpha1_ssh_identity_proto_msgTypes, }.Build() File_teleport_decision_v1alpha1_ssh_identity_proto = out.File diff --git a/api/proto/teleport/decision/v1alpha1/ssh_identity.proto b/api/proto/teleport/decision/v1alpha1/ssh_identity.proto index 01f4ea2af2d58..c63fa2f73850c 100644 --- a/api/proto/teleport/decision/v1alpha1/ssh_identity.proto +++ b/api/proto/teleport/decision/v1alpha1/ssh_identity.proto @@ -16,9 +16,174 @@ syntax = "proto3"; package teleport.decision.v1alpha1; +import "google/protobuf/timestamp.proto"; +import "teleport/decision/v1alpha1/tls_identity.proto"; +import "teleport/trait/v1/trait.proto"; + option go_package = "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1;decisionpb"; // SSHIdentity is the identity used for SSH connections. message SSHIdentity { - // TBD + // --- common identity fields --- + + // ValidAfter is the unix timestamp that marks the start time for when the certificate should + // be considered valid. + uint64 valid_after = 1; + + // ValidBefore is the unix timestamp that marks the end time for when the certificate should + // be considered valid. + uint64 valid_before = 2; + + // CertType indicates what type of cert this is (user or host). + uint32 cert_type = 3; + + // Principals is the list of SSH principals associated with the certificate (this means the + // list of allowed unix logins in the case of user certs). + repeated string principals = 4; + + // --- host identity fields --- + + // ClusterName is the name of the cluster within which a node lives + string cluster_name = 5; + // SystemRole identifies the system role of a Teleport instance + string system_role = 6; + + // -- user identity fields --- + + // Username is teleport username + string username = 7; + + // Impersonator is set when a user requests certificate for another user + string impersonator = 8; + + // PermitX11Forwarding permits X11 forwarding for this cert + bool permit_x11_forwarding = 9; + + // PermitAgentForwarding permits agent forwarding for this cert + bool permit_agent_forwarding = 10; + + // PermitPortForwarding permits port forwarding. + bool permit_port_forwarding = 11; + + // Roles is a list of roles assigned to this user + repeated string roles = 12; + + // RouteToCluster specifies the target cluster + // if present in the certificate, will be used + // to route the requests to + string route_to_cluster = 13; + + // Traits hold claim data used to populate a role at runtime. + repeated teleport.trait.v1.Trait traits = 14; + + // ActiveRequests tracks privilege escalation requests applied during + // certificate construction. + repeated string active_requests = 15; + + // MFAVerified is the UUID of an MFA device when this Identity was + // confirmed immediately after an MFA check. + string mfa_verified = 16; + + // PreviousIdentityExpires is the expiry time of the identity/cert that this + // identity/cert was derived from. It is used to determine a session's hard + // deadline in cases where both require_session_mfa and disconnect_expired_cert + // are enabled. See https://github.com/gravitational/teleport/issues/18544. + google.protobuf.Timestamp previous_identity_expires = 17; + + // LoginIP is an observed IP of the client on the moment of certificate creation. + string login_ip = 18; + + // PinnedIP is an IP from which client must communicate with Teleport. + string pinned_ip = 19; + + // DisallowReissue flags that any attempt to request new certificates while + // authenticated with this cert should be denied. + bool disallow_reissue = 20; + + // CertificateExtensions are user configured ssh key extensions (note: this field also + // ends up aggregating all *unknown* extensions during cert parsing, meaning that this + // can sometimes contain fields that were inserted by a newer version of teleport). + repeated CertExtension certificate_extensions = 21; + + // Renewable indicates this certificate is renewable. + bool renewable = 22; + + // Generation counts the number of times a certificate has been renewed, with a generation of 1 + // meaning the cert has never been renewed. A generation of zero means the cert's generation is + // not being tracked. + uint64 generation = 23; + + // BotName is set to the name of the bot, if the user is a Machine ID bot user. + // Empty for human users. + string bot_name = 24; + + // BotInstanceID is the unique identifier for the bot instance, if this is a + // Machine ID bot. It is empty for human users. + string bot_instance_id = 25; + + // AllowedResourceIDs lists the resources the user should be able to access. + repeated ResourceId allowed_resource_ids = 26; + + // ConnectionDiagnosticID references the ConnectionDiagnostic that we should use to append traces when testing a Connection. + string connection_diagnostic_id = 27; + + // PrivateKeyPolicy is the private key policy supported by this certificate. + string private_key_policy = 28; + + // DeviceID is the trusted device identifier. + string device_id = 29; + + // DeviceAssetTag is the device inventory identifier. + string device_asset_tag = 30; + + // DeviceCredentialID is the identifier for the credential used by the device + // to authenticate itself. + string device_credential_id = 31; + + // GitHubUserID indicates the GitHub user ID identified by the GitHub + // connector. + string github_user_id = 32; + + // GitHubUsername indicates the GitHub username identified by the GitHub + // connector. + string github_username = 33; +} + +// CertExtensionMode specifies the type of extension to use in the cert. This type +// must be kept up to date with types.CertExtensionMode. +enum CertExtensionMode { + // CERT_EXTENSION_MODE_UNSPECIFIED is the default value and should not be used. + CERT_EXTENSION_MODE_UNSPECIFIED = 0; + + // EXTENSION represents a cert extension that may or may not be + // honored by the server. + CERT_EXTENSION_MODE_EXTENSION = 1; +} + +// CertExtensionType represents the certificate type the extension is for. +// Currently only ssh is supported. This type must be kept up to date with +// types.CertExtensionType. +enum CertExtensionType { + // CERT_EXTENSION_TYPE_UNSPECIFIED is the default value and should not be used. + CERT_EXTENSION_TYPE_UNSPECIFIED = 0; + + // SSH is used when extending an ssh certificate + CERT_EXTENSION_TYPE_SSH = 1; +} + +// CertExtension represents a key/value for a certificate extension. This type must +// be kept up to date with types.CertExtension. +message CertExtension { + // Type represents the certificate type being extended, only ssh + // is supported at this time. + // 0 is "ssh". + CertExtensionType type = 1; + // Mode is the type of extension to be used -- currently + // critical-option is not supported. + // 0 is "extension". + CertExtensionMode mode = 2; + // Name specifies the key to be used in the cert extension. + string name = 3; + // Value specifies the value to be used in the cert extension. + string value = 4; } diff --git a/integration/helpers/instance.go b/integration/helpers/instance.go index 7e7deb03567a8..7e21a3f2dd42b 100644 --- a/integration/helpers/instance.go +++ b/integration/helpers/instance.go @@ -66,6 +66,7 @@ import ( "github.com/gravitational/teleport/lib/service" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/sshca" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" @@ -373,14 +374,16 @@ func NewInstance(t *testing.T, cfg InstanceConfig) *TeleInstance { fatalIf(err) keygen := keygen.New(context.TODO()) - cert, err := keygen.GenerateHostCert(services.HostCertParams{ + cert, err := keygen.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: sshSigner, PublicHostKey: cfg.Pub, HostID: cfg.HostID, NodeName: cfg.NodeName, - ClusterName: cfg.ClusterName, - Role: types.RoleAdmin, TTL: 24 * time.Hour, + Identity: sshca.Identity{ + ClusterName: cfg.ClusterName, + SystemRole: types.RoleAdmin, + }, }) fatalIf(err) tlsCA, err := tlsca.FromKeys(tlsCACert, cfg.Priv) diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 1e6f954b75b31..045c8883ee6db 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -2130,20 +2130,22 @@ func (a *Server) GenerateHostCert(ctx context.Context, hostPublicKey []byte, hos } // create and sign! - return a.generateHostCert(ctx, services.HostCertParams{ + return a.generateHostCert(ctx, sshca.HostCertificateRequest{ CASigner: caSigner, PublicHostKey: hostPublicKey, HostID: hostID, NodeName: nodeName, - Principals: principals, - ClusterName: clusterName, - Role: role, TTL: ttl, + Identity: sshca.Identity{ + Principals: principals, + ClusterName: clusterName, + SystemRole: role, + }, }) } func (a *Server) generateHostCert( - ctx context.Context, p services.HostCertParams, + ctx context.Context, req sshca.HostCertificateRequest, ) ([]byte, error) { readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx) if err != nil { @@ -2151,7 +2153,7 @@ func (a *Server) generateHostCert( } var locks []types.LockTarget - switch p.Role { + switch req.Identity.SystemRole { case types.RoleNode: // Node role is a special case because it was previously suported as a // lock target that only locked the `ssh_service`. If the same Teleport server @@ -2164,9 +2166,9 @@ func (a *Server) generateHostCert( // and `Node` fields if the role is `Node` so that the previous behavior // is preserved. // This is a legacy behavior that we need to support for backwards compatibility. - locks = []types.LockTarget{{ServerID: p.HostID, Node: p.HostID}, {ServerID: HostFQDN(p.HostID, p.ClusterName), Node: HostFQDN(p.HostID, p.ClusterName)}} + locks = []types.LockTarget{{ServerID: req.HostID, Node: req.HostID}, {ServerID: HostFQDN(req.HostID, req.Identity.ClusterName), Node: HostFQDN(req.HostID, req.Identity.ClusterName)}} default: - locks = []types.LockTarget{{ServerID: p.HostID}, {ServerID: HostFQDN(p.HostID, p.ClusterName)}} + locks = []types.LockTarget{{ServerID: req.HostID}, {ServerID: HostFQDN(req.HostID, req.Identity.ClusterName)}} } if lockErr := a.checkLockInForce(readOnlyAuthPref.GetLockingMode(), locks, @@ -2174,7 +2176,7 @@ func (a *Server) generateHostCert( return nil, trace.Wrap(lockErr) } - return a.Authority.GenerateHostCert(p) + return a.Authority.GenerateHostCert(req) } // GetKeyStore returns the KeyStore used by the auth server @@ -2226,7 +2228,7 @@ type certRequest struct { traits wrappers.Traits // activeRequests tracks privilege escalation requests applied // during the construction of the certificate. - activeRequests services.RequestIDs + activeRequests []string // appSessionID is the session ID of the application session. appSessionID string // appPublicAddr is the public address of the application. @@ -3081,7 +3083,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types. defaultMode: readOnlyAuthPref.GetLockingMode(), username: req.user.GetName(), mfaVerified: req.mfaVerified, - activeAccessRequests: req.activeRequests.AccessRequests, + activeAccessRequests: req.activeRequests, deviceID: req.deviceExtensions.DeviceID, }); err != nil { return nil, trace.Wrap(err) @@ -3210,11 +3212,6 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types. // All users have access to this and join RBAC rules are checked after the connection is established. allowedLogins = append(allowedLogins, teleport.SSHSessionJoinPrincipal) - requestedResourcesStr, err := types.ResourceIDsToString(req.checker.GetAllowedResourceIDs()) - if err != nil { - return nil, trace.Wrap(err) - } - pinnedIP := "" if caType == types.UserCA && (req.checker.PinSourceIP() || req.pinIP) { if req.loginIP == "" { @@ -3254,7 +3251,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types. Identity: sshca.Identity{ Username: req.user.GetName(), Impersonator: req.impersonator, - AllowedLogins: allowedLogins, + Principals: allowedLogins, Roles: req.checker.RoleNames(), PermitPortForwarding: req.checker.CanPortForward(), PermitAgentForwarding: req.checker.CanForwardAgents(), @@ -3272,7 +3269,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types. BotName: req.botName, BotInstanceID: req.botInstanceID, CertificateExtensions: req.checker.CertificateExtensions(), - AllowedResourceIDs: requestedResourcesStr, + AllowedResourceIDs: req.checker.GetAllowedResourceIDs(), ConnectionDiagnosticID: req.connectionDiagnosticID, PrivateKeyPolicy: attestedKeyPolicy, DeviceID: req.deviceExtensions.DeviceID, @@ -3367,7 +3364,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types. AWSRoleARNs: roleARNs, AzureIdentities: azureIdentities, GCPServiceAccounts: gcpAccounts, - ActiveRequests: req.activeRequests.AccessRequests, + ActiveRequests: req.activeRequests, DisallowReissue: req.disallowReissue, Renewable: req.renewable, Generation: req.generation, @@ -4734,14 +4731,16 @@ func (a *Server) GenerateHostCerts(ctx context.Context, req *proto.HostCertsRequ return nil, trace.Wrap(err) } // generate host SSH certificate - hostSSHCert, err := a.generateHostCert(ctx, services.HostCertParams{ + hostSSHCert, err := a.generateHostCert(ctx, sshca.HostCertificateRequest{ CASigner: caSigner, PublicHostKey: req.PublicSSHKey, HostID: req.HostID, NodeName: req.NodeName, - ClusterName: clusterName.GetClusterName(), - Role: req.Role, - Principals: req.AdditionalPrincipals, + Identity: sshca.Identity{ + ClusterName: clusterName.GetClusterName(), + SystemRole: req.Role, + Principals: req.AdditionalPrincipals, + }, }) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index b42e5dde88040..88e17a326e1f6 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -2642,7 +2642,7 @@ func TestGenerateUserCertWithLocks(t *testing.T) { mfaVerified: mfaID, sshPublicKey: sshPubKey, tlsPublicKey: tlsPubKey, - activeRequests: services.RequestIDs{AccessRequests: []string{requestID}}, + activeRequests: []string{requestID}, deviceExtensions: DeviceExtensions{ DeviceID: deviceID, AssetTag: "assettag1", diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 98fa97190a779..f140796d2100a 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -3440,11 +3440,9 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC checker: checker, // Copy IP from current identity to the generated certificate, if present, // to avoid generateUserCerts() being used to drop IP pinning in the new certificates. - loginIP: a.context.Identity.GetIdentity().LoginIP, - traits: accessInfo.Traits, - activeRequests: services.RequestIDs{ - AccessRequests: req.AccessRequests, - }, + loginIP: a.context.Identity.GetIdentity().LoginIP, + traits: accessInfo.Traits, + activeRequests: req.AccessRequests, connectionDiagnosticID: req.ConnectionDiagnosticID, botName: getBotName(user), diff --git a/lib/auth/init_test.go b/lib/auth/init_test.go index 291458bd196e8..f4218479f302b 100644 --- a/lib/auth/init_test.go +++ b/lib/auth/init_test.go @@ -62,6 +62,7 @@ import ( "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/services/suite" "github.com/gravitational/teleport/lib/srv/db/common/databaseobjectimportrule" + "github.com/gravitational/teleport/lib/sshca" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/proxy" @@ -77,14 +78,16 @@ func TestReadIdentity(t *testing.T) { caSigner, err := ssh.ParsePrivateKey(priv) require.NoError(t, err) - cert, err := a.GenerateHostCert(services.HostCertParams{ + cert, err := a.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: caSigner, PublicHostKey: pub, HostID: "id1", NodeName: "node-name", - ClusterName: "example.com", - Role: types.RoleNode, TTL: 0, + Identity: sshca.Identity{ + ClusterName: "example.com", + SystemRole: types.RoleNode, + }, }) require.NoError(t, err) @@ -98,14 +101,16 @@ func TestReadIdentity(t *testing.T) { // test TTL by converting the generated cert to text -> back and making sure ExpireAfter is valid ttl := 10 * time.Second expiryDate := clock.Now().Add(ttl) - bytes, err := a.GenerateHostCert(services.HostCertParams{ + bytes, err := a.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: caSigner, PublicHostKey: pub, HostID: "id1", NodeName: "node-name", - ClusterName: "example.com", - Role: types.RoleNode, TTL: ttl, + Identity: sshca.Identity{ + ClusterName: "example.com", + SystemRole: types.RoleNode, + }, }) require.NoError(t, err) copy, err := apisshutils.ParseCertificate(bytes) @@ -125,14 +130,16 @@ func TestBadIdentity(t *testing.T) { require.IsType(t, trace.BadParameter(""), err) // missing authority domain - cert, err := a.GenerateHostCert(services.HostCertParams{ + cert, err := a.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: caSigner, PublicHostKey: pub, HostID: "id2", NodeName: "", - ClusterName: "", - Role: types.RoleNode, TTL: 0, + Identity: sshca.Identity{ + ClusterName: "", + SystemRole: types.RoleNode, + }, }) require.NoError(t, err) @@ -140,14 +147,16 @@ func TestBadIdentity(t *testing.T) { require.IsType(t, trace.BadParameter(""), err) // missing host uuid - cert, err = a.GenerateHostCert(services.HostCertParams{ + cert, err = a.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: caSigner, PublicHostKey: pub, HostID: "example.com", NodeName: "", - ClusterName: "", - Role: types.RoleNode, TTL: 0, + Identity: sshca.Identity{ + ClusterName: "", + SystemRole: types.RoleNode, + }, }) require.NoError(t, err) @@ -155,14 +164,16 @@ func TestBadIdentity(t *testing.T) { require.IsType(t, trace.BadParameter(""), err) // unrecognized role - cert, err = a.GenerateHostCert(services.HostCertParams{ + cert, err = a.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: caSigner, PublicHostKey: pub, HostID: "example.com", NodeName: "", - ClusterName: "id1", - Role: "bad role", TTL: 0, + Identity: sshca.Identity{ + ClusterName: "id1", + SystemRole: "bad role", + }, }) require.NoError(t, err) diff --git a/lib/auth/keygen/keygen.go b/lib/auth/keygen/keygen.go index 5f47b3a90ac16..6133a90c907c7 100644 --- a/lib/auth/keygen/keygen.go +++ b/lib/auth/keygen/keygen.go @@ -33,9 +33,7 @@ import ( "github.com/gravitational/teleport/api/types" apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/lib/modules" - "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/sshca" - "github.com/gravitational/teleport/lib/utils" ) // Keygen is a key generator that precomputes keys to provide quick access to @@ -69,58 +67,64 @@ func New(_ context.Context, opts ...Option) *Keygen { // GenerateHostCert generates a host certificate with the passed in parameters. // The private key of the CA to sign the certificate must be provided. -func (k *Keygen) GenerateHostCert(c services.HostCertParams) ([]byte, error) { - if err := c.Check(); err != nil { +func (k *Keygen) GenerateHostCert(req sshca.HostCertificateRequest) ([]byte, error) { + if err := req.Check(); err != nil { return nil, trace.Wrap(err) } - return k.GenerateHostCertWithoutValidation(c) + return k.GenerateHostCertWithoutValidation(req) } // GenerateHostCertWithoutValidation generates a host certificate with the // passed in parameters without validating them. For use in tests only. -func (k *Keygen) GenerateHostCertWithoutValidation(c services.HostCertParams) ([]byte, error) { - pubKey, _, _, _, err := ssh.ParseAuthorizedKey(c.PublicHostKey) +func (k *Keygen) GenerateHostCertWithoutValidation(req sshca.HostCertificateRequest) ([]byte, error) { + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(req.PublicHostKey) if err != nil { return nil, trace.Wrap(err) } + // create shallow copy of identity since we want to make some local changes + ident := req.Identity + + ident.CertType = ssh.HostCert + // Build a valid list of principals from the HostID and NodeName and then // add in any additional principals passed in. - principals := BuildPrincipals(c.HostID, c.NodeName, c.ClusterName, types.SystemRoles{c.Role}) - principals = append(principals, c.Principals...) + principals := BuildPrincipals(req.HostID, req.NodeName, ident.ClusterName, types.SystemRoles{ident.SystemRole}) + principals = append(principals, ident.Principals...) if len(principals) == 0 { - return nil, trace.BadParameter("no principals provided: %v, %v, %v", - c.HostID, c.NodeName, c.Principals) + return nil, trace.BadParameter("cannot generate host certificate without principals") } principals = apiutils.Deduplicate(principals) + ident.Principals = principals - // create certificate - validBefore := uint64(ssh.CertTimeInfinity) - if c.TTL != 0 { - b := k.clock.Now().UTC().Add(c.TTL) - validBefore = uint64(b.Unix()) + // calculate ValidBefore based on the outer request TTL + ident.ValidBefore = uint64(ssh.CertTimeInfinity) + if req.TTL != 0 { + b := k.clock.Now().UTC().Add(req.TTL) + ident.ValidBefore = uint64(b.Unix()) } - cert := &ssh.Certificate{ - ValidPrincipals: principals, - Key: pubKey, - ValidAfter: uint64(k.clock.Now().UTC().Add(-1 * time.Minute).Unix()), - ValidBefore: validBefore, - CertType: ssh.HostCert, + + ident.ValidAfter = uint64(k.clock.Now().UTC().Add(-1 * time.Minute).Unix()) + + // encode the identity into a certificate + cert, err := ident.Encode("") + if err != nil { + return nil, trace.Wrap(err) } - cert.Permissions.Extensions = make(map[string]string) - cert.Permissions.Extensions[utils.CertExtensionRole] = c.Role.String() - cert.Permissions.Extensions[utils.CertExtensionAuthority] = c.ClusterName + + // set the public key of the certificate + cert.Key = pubKey // sign host certificate with private signing key of certificate authority - if err := cert.SignCert(rand.Reader, c.CASigner); err != nil { + if err := cert.SignCert(rand.Reader, req.CASigner); err != nil { return nil, trace.Wrap(err) } slog.DebugContext( context.TODO(), "Generated SSH host certificate.", - "role", c.Role, "principals", principals, + "role", ident.SystemRole, "principals", ident.Principals, ) return ssh.MarshalAuthorizedKey(cert), nil } @@ -145,14 +149,7 @@ func (k *Keygen) GenerateUserCertWithoutValidation(req sshca.UserCertificateRequ // create shallow copy of identity since we want to make some local changes ident := req.Identity - // since this method ignores the supplied values for ValidBefore/ValidAfter, avoid confusing by - // rejecting identities where they are set. - if ident.ValidBefore != 0 { - return nil, trace.BadParameter("ValidBefore should not be set in calls to GenerateUserCert") - } - if ident.ValidAfter != 0 { - return nil, trace.BadParameter("ValidAfter should not be set in calls to GenerateUserCert") - } + ident.CertType = ssh.UserCert // calculate ValidBefore based on the outer request TTL ident.ValidBefore = uint64(ssh.CertTimeInfinity) @@ -162,7 +159,7 @@ func (k *Keygen) GenerateUserCertWithoutValidation(req sshca.UserCertificateRequ slog.DebugContext( context.TODO(), "Generated user key with expiry.", - "allowed_logins", ident.AllowedLogins, + "allowed_logins", ident.Principals, "valid_before_unix_ts", ident.ValidBefore, "valid_before", b, ) diff --git a/lib/auth/keygen/keygen_test.go b/lib/auth/keygen/keygen_test.go index d6c243b3ee986..e82933b944885 100644 --- a/lib/auth/keygen/keygen_test.go +++ b/lib/auth/keygen/keygen_test.go @@ -37,7 +37,6 @@ import ( "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/lib/auth/test" "github.com/gravitational/teleport/lib/cryptosuites" - "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/sshca" ) @@ -176,16 +175,17 @@ func TestBuildPrincipals(t *testing.T) { // run tests for _, tc := range tests { t.Logf("Running test case: %q", tc.desc) - hostCertificateBytes, err := tt.suite.A.GenerateHostCert( - services.HostCertParams{ - CASigner: caSigner, - PublicHostKey: hostPublicKey, - HostID: tc.inHostID, - NodeName: tc.inNodeName, - ClusterName: tc.inClusterName, - Role: tc.inRole, - TTL: time.Hour, - }) + hostCertificateBytes, err := tt.suite.A.GenerateHostCert(sshca.HostCertificateRequest{ + CASigner: caSigner, + PublicHostKey: hostPublicKey, + HostID: tc.inHostID, + NodeName: tc.inNodeName, + TTL: time.Hour, + Identity: sshca.Identity{ + ClusterName: tc.inClusterName, + SystemRole: tc.inRole, + }, + }) require.NoError(t, err) hostCertificate, err := sshutils.ParseCertificate(hostCertificateBytes) @@ -233,9 +233,9 @@ func TestUserCertCompatibility(t *testing.T) { TTL: time.Hour, CertificateFormat: tc.inCompatibility, Identity: sshca.Identity{ - Username: "user", - AllowedLogins: []string{"centos", "root"}, - Roles: []string{"foo"}, + Username: "user", + Principals: []string{"centos", "root"}, + Roles: []string{"foo"}, CertificateExtensions: []*types.CertExtension{{ Type: types.CertExtensionType_SSH, Mode: types.CertExtensionMode_EXTENSION, diff --git a/lib/auth/sessions.go b/lib/auth/sessions.go index 7f202bd9110b3..caf98e262b0f8 100644 --- a/lib/auth/sessions.go +++ b/lib/auth/sessions.go @@ -290,7 +290,7 @@ func (a *Server) newWebSession( tlsPublicKey: tlsPublicKeyPEM, checker: checker, traits: req.Traits, - activeRequests: services.RequestIDs{AccessRequests: req.AccessRequests}, + activeRequests: req.AccessRequests, } var hasDeviceExtensions bool if opts != nil && opts.deviceExtensions != nil { @@ -557,7 +557,7 @@ func (a *Server) CreateAppSessionFromReq(ctx context.Context, req NewAppSessionR checker: checker, ttl: req.SessionTTL, traits: req.Traits, - activeRequests: services.RequestIDs{AccessRequests: req.AccessRequests}, + activeRequests: req.AccessRequests, // Set the app session ID in the certificate - used in auditing from the App Service. appSessionID: sessionID, // Only allow this certificate to be used for applications. diff --git a/lib/auth/test/suite.go b/lib/auth/test/suite.go index 14d22f8265647..ac1a9ee4cd2d1 100644 --- a/lib/auth/test/suite.go +++ b/lib/auth/test/suite.go @@ -64,16 +64,17 @@ func (s *AuthSuite) GenerateHostCert(t *testing.T) { caSigner, err := ssh.ParsePrivateKey(priv) require.NoError(t, err) - cert, err := s.A.GenerateHostCert( - services.HostCertParams{ - CASigner: caSigner, - PublicHostKey: pub, - HostID: "00000000-0000-0000-0000-000000000000", - NodeName: "auth.example.com", - ClusterName: "example.com", - Role: types.RoleAdmin, - TTL: time.Hour, - }) + cert, err := s.A.GenerateHostCert(sshca.HostCertificateRequest{ + CASigner: caSigner, + PublicHostKey: pub, + HostID: "00000000-0000-0000-0000-000000000000", + NodeName: "auth.example.com", + TTL: time.Hour, + Identity: sshca.Identity{ + ClusterName: "example.com", + SystemRole: types.RoleAdmin, + }, + }) require.NoError(t, err) certificate, err := sshutils.ParseCertificate(cert) @@ -102,7 +103,7 @@ func (s *AuthSuite) GenerateUserCert(t *testing.T) { CertificateFormat: constants.CertificateFormatStandard, Identity: sshca.Identity{ Username: "user", - AllowedLogins: []string{"centos", "root"}, + Principals: []string{"centos", "root"}, PermitAgentForwarding: true, PermitPortForwarding: true, }, @@ -121,7 +122,7 @@ func (s *AuthSuite) GenerateUserCert(t *testing.T) { CertificateFormat: constants.CertificateFormatStandard, Identity: sshca.Identity{ Username: "user", - AllowedLogins: []string{"root"}, + Principals: []string{"root"}, PermitAgentForwarding: true, PermitPortForwarding: true, }, @@ -137,7 +138,7 @@ func (s *AuthSuite) GenerateUserCert(t *testing.T) { CertificateFormat: constants.CertificateFormatStandard, Identity: sshca.Identity{ Username: "user", - AllowedLogins: []string{"root"}, + Principals: []string{"root"}, PermitAgentForwarding: true, PermitPortForwarding: true, }, @@ -153,7 +154,7 @@ func (s *AuthSuite) GenerateUserCert(t *testing.T) { CertificateFormat: constants.CertificateFormatStandard, Identity: sshca.Identity{ Username: "user", - AllowedLogins: []string{"root"}, + Principals: []string{"root"}, PermitAgentForwarding: true, PermitPortForwarding: true, }, @@ -170,7 +171,7 @@ func (s *AuthSuite) GenerateUserCert(t *testing.T) { Identity: sshca.Identity{ Username: "user", Impersonator: impersonator, - AllowedLogins: []string{"root"}, + Principals: []string{"root"}, PermitAgentForwarding: true, PermitPortForwarding: true, Roles: inRoles, @@ -195,7 +196,7 @@ func (s *AuthSuite) GenerateUserCert(t *testing.T) { CertificateFormat: constants.CertificateFormatStandard, Identity: sshca.Identity{ Username: "user", - AllowedLogins: []string{"root"}, + Principals: []string{"root"}, MFAVerified: "mfa-device-id", PreviousIdentityExpires: clock.Now().Add(time.Hour), }, @@ -219,7 +220,7 @@ func (s *AuthSuite) GenerateUserCert(t *testing.T) { PublicUserKey: pub, // Required. Identity: sshca.Identity{ Username: "llama", // Required. - AllowedLogins: []string{"llama"}, // Required. + Principals: []string{"llama"}, // Required. DeviceID: devID, DeviceAssetTag: devTag, DeviceCredentialID: devCred, @@ -242,7 +243,7 @@ func (s *AuthSuite) GenerateUserCert(t *testing.T) { PublicUserKey: pub, // Required. Identity: sshca.Identity{ Username: "llama", // Required. - AllowedLogins: []string{"llama"}, // Required. + Principals: []string{"llama"}, // Required. GitHubUserID: githubUserID, GitHubUsername: githubUsername, }, diff --git a/lib/auth/testauthority/testauthority.go b/lib/auth/testauthority/testauthority.go index b58f9ac27493d..dbb14c56c20cb 100644 --- a/lib/auth/testauthority/testauthority.go +++ b/lib/auth/testauthority/testauthority.go @@ -28,7 +28,6 @@ import ( "github.com/gravitational/teleport/lib/auth/keygen" "github.com/gravitational/teleport/lib/cryptosuites" - "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/sshca" ) @@ -57,8 +56,8 @@ func (n *Keygen) GenerateKeyPair() (priv []byte, pub []byte, err error) { return privateKey.PrivateKeyPEM(), privateKey.MarshalSSHPublicKey(), nil } -func (n *Keygen) GenerateHostCert(c services.HostCertParams) ([]byte, error) { - return n.GenerateHostCertWithoutValidation(c) +func (n *Keygen) GenerateHostCert(req sshca.HostCertificateRequest) ([]byte, error) { + return n.GenerateHostCertWithoutValidation(req) } func (n *Keygen) GenerateUserCert(c sshca.UserCertificateRequest) ([]byte, error) { diff --git a/lib/client/client_store_test.go b/lib/client/client_store_test.go index 71239884aaaba..f62aaefeacf00 100644 --- a/lib/client/client_store_test.go +++ b/lib/client/client_store_test.go @@ -44,7 +44,6 @@ import ( "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" - "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/sshca" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/tlsca" @@ -111,7 +110,7 @@ func (s *testAuthority) makeSignedKeyRing(t *testing.T, idx KeyRingIndex, makeEx TTL: ttl, Identity: sshca.Identity{ Username: idx.Username, - AllowedLogins: allowedLogins, + Principals: allowedLogins, PermitAgentForwarding: false, PermitPortForwarding: true, GitHubUserID: "1234567", @@ -311,13 +310,15 @@ func TestProxySSHConfig(t *testing.T) { caSigner, err := ssh.ParsePrivateKey(CAPriv) require.NoError(t, err) - hostCert, err := auth.keygen.GenerateHostCert(services.HostCertParams{ + hostCert, err := auth.keygen.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: caSigner, PublicHostKey: hostPub, HostID: "127.0.0.1", NodeName: "127.0.0.1", - ClusterName: "host-cluster-name", - Role: types.RoleNode, + Identity: sshca.Identity{ + ClusterName: "host-cluster-name", + SystemRole: types.RoleNode, + }, }) require.NoError(t, err) diff --git a/lib/client/identityfile/identity_test.go b/lib/client/identityfile/identity_test.go index 9d8eeb62a894d..fe1d9df9a9857 100644 --- a/lib/client/identityfile/identity_test.go +++ b/lib/client/identityfile/identity_test.go @@ -112,8 +112,8 @@ func newClientKeyRing(t *testing.T, modifiers ...func(*tlsca.Identity)) *client. CASigner: caSigner, PublicUserKey: ssh.MarshalAuthorizedKey(privateKey.SSHPublicKey()), Identity: sshca.Identity{ - Username: "testuser", - AllowedLogins: []string{"testuser"}, + Username: "testuser", + Principals: []string{"testuser"}, }, }) require.NoError(t, err) diff --git a/lib/client/keyagent_test.go b/lib/client/keyagent_test.go index a8dfdae28da95..b937812f49ddb 100644 --- a/lib/client/keyagent_test.go +++ b/lib/client/keyagent_test.go @@ -49,7 +49,6 @@ import ( "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/fixtures" - "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/sshca" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" @@ -366,17 +365,19 @@ func TestHostCertVerification(t *testing.T) { // Generate a host certificate for node with role "node". _, rootHostPub, err := keygen.GenerateKeyPair() require.NoError(t, err) - rootHostCertBytes, err := keygen.GenerateHostCert(services.HostCertParams{ + rootHostCertBytes, err := keygen.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: root.signer, PublicHostKey: rootHostPub, HostID: "5ff40d80-9007-4f28-8f49-7d4fda2f574d", NodeName: "server01", - Principals: []string{ - "127.0.0.1", + TTL: 1 * time.Hour, + Identity: sshca.Identity{ + Principals: []string{ + "127.0.0.1", + }, + ClusterName: "example.com", + SystemRole: types.RoleNode, }, - ClusterName: "example.com", - Role: types.RoleNode, - TTL: 1 * time.Hour, }) require.NoError(t, err) rootHostPublicKey, _, _, _, err := ssh.ParseAuthorizedKey(rootHostCertBytes) @@ -384,14 +385,16 @@ func TestHostCertVerification(t *testing.T) { _, leafHostPub, err := keygen.GenerateKeyPair() require.NoError(t, err) - leafHostCertBytes, err := keygen.GenerateHostCert(services.HostCertParams{ + leafHostCertBytes, err := keygen.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: leaf.signer, PublicHostKey: leafHostPub, HostID: "620bb71c-c9eb-4f6d-9823-f7d9125ebb1d", NodeName: "server02", - ClusterName: "leaf.example.com", - Role: types.RoleNode, TTL: 1 * time.Hour, + Identity: sshca.Identity{ + ClusterName: "leaf.example.com", + SystemRole: types.RoleNode, + }, }) require.NoError(t, err) leafHostPublicKey, _, _, _, err := ssh.ParseAuthorizedKey(leafHostCertBytes) @@ -620,14 +623,16 @@ func TestHostCertVerificationLoadAllCasProxyAddrEqClusterName(t *testing.T) { func mustGenerateHostPublicCert(t *testing.T, keygen *testauthority.Keygen, signer ssh.Signer, nodeName, clusterName string) ssh.PublicKey { _, leafHostPub, err := keygen.GenerateKeyPair() require.NoError(t, err) - leafHostCertBytes, err := keygen.GenerateHostCert(services.HostCertParams{ + leafHostCertBytes, err := keygen.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: signer, PublicHostKey: leafHostPub, HostID: uuid.NewString(), NodeName: nodeName, - ClusterName: clusterName, - Role: types.RoleNode, TTL: 1 * time.Hour, + Identity: sshca.Identity{ + ClusterName: clusterName, + SystemRole: types.RoleNode, + }, }) require.NoError(t, err) leafCerts, err := sshutils.ParseAuthorizedKeys([][]byte{leafHostCertBytes}) @@ -759,7 +764,7 @@ func (s *KeyAgentTestSuite) makeKeyRing(t *testing.T, username, proxyHost string TTL: ttl, Identity: sshca.Identity{ Username: username, - AllowedLogins: []string{username}, + Principals: []string{username}, PermitAgentForwarding: true, PermitPortForwarding: true, RouteToCluster: s.clusterName, diff --git a/lib/client/known_hosts_migrate_test.go b/lib/client/known_hosts_migrate_test.go index 612e7d3082f06..cba71bda212d6 100644 --- a/lib/client/known_hosts_migrate_test.go +++ b/lib/client/known_hosts_migrate_test.go @@ -28,7 +28,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/gravitational/teleport/lib/auth/testauthority" - "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/sshca" ) type knownHostsMigrateTest struct { @@ -48,12 +48,14 @@ func generateHostCert(t *testing.T, s *knownHostsMigrateTest, clusterName string caSigner, err := ssh.ParsePrivateKey(CAPriv) require.NoError(t, err) - cert, err := s.keygen.GenerateHostCert(services.HostCertParams{ + cert, err := s.keygen.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: caSigner, HostID: "127.0.0.1", NodeName: "127.0.0.1", - ClusterName: clusterName, PublicHostKey: hostPub, + Identity: sshca.Identity{ + ClusterName: clusterName, + }, }) require.NoError(t, err) diff --git a/lib/decision/ssh_identity.go b/lib/decision/ssh_identity.go new file mode 100644 index 0000000000000..0bf120d5307a2 --- /dev/null +++ b/lib/decision/ssh_identity.go @@ -0,0 +1,143 @@ +// 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 decision + +import ( + decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/sshca" +) + +// SSHIdentityToSSHCA transforms a [decisionpb.SSHIdentity] into its +// equivalent [sshca.Identity]. +// Note that certain types, like slices, are not deep-copied. +func SSHIdentityToSSHCA(id *decisionpb.SSHIdentity) *sshca.Identity { + if id == nil { + return nil + } + + return &sshca.Identity{ + ValidAfter: id.ValidAfter, + ValidBefore: id.ValidBefore, + CertType: id.CertType, + ClusterName: id.ClusterName, + SystemRole: types.SystemRole(id.SystemRole), + Username: id.Username, + Impersonator: id.Impersonator, + Principals: id.Principals, + PermitX11Forwarding: id.PermitX11Forwarding, + PermitAgentForwarding: id.PermitAgentForwarding, + PermitPortForwarding: id.PermitPortForwarding, + Roles: id.Roles, + RouteToCluster: id.RouteToCluster, + Traits: traitToWrappers(id.Traits), + ActiveRequests: id.ActiveRequests, + MFAVerified: id.MfaVerified, + PreviousIdentityExpires: timestampToGoTime(id.PreviousIdentityExpires), + LoginIP: id.LoginIp, + PinnedIP: id.PinnedIp, + DisallowReissue: id.DisallowReissue, + CertificateExtensions: certExtensionsFromProto(id.CertificateExtensions), + Renewable: id.Renewable, + Generation: id.Generation, + BotName: id.BotName, + BotInstanceID: id.BotInstanceId, + AllowedResourceIDs: resourceIDsToTypes(id.AllowedResourceIds), + ConnectionDiagnosticID: id.ConnectionDiagnosticId, + PrivateKeyPolicy: keys.PrivateKeyPolicy(id.PrivateKeyPolicy), + DeviceID: id.DeviceId, + DeviceAssetTag: id.DeviceAssetTag, + DeviceCredentialID: id.DeviceCredentialId, + GitHubUserID: id.GithubUserId, + GitHubUsername: id.GithubUsername, + } +} + +func SSHIdentityFromSSHCA(id *sshca.Identity) *decisionpb.SSHIdentity { + if id == nil { + return nil + } + + return &decisionpb.SSHIdentity{ + ValidAfter: id.ValidAfter, + ValidBefore: id.ValidBefore, + CertType: id.CertType, + ClusterName: id.ClusterName, + SystemRole: string(id.SystemRole), + Username: id.Username, + Impersonator: id.Impersonator, + Principals: id.Principals, + PermitX11Forwarding: id.PermitX11Forwarding, + PermitAgentForwarding: id.PermitAgentForwarding, + PermitPortForwarding: id.PermitPortForwarding, + Roles: id.Roles, + RouteToCluster: id.RouteToCluster, + Traits: traitFromWrappers(id.Traits), + ActiveRequests: id.ActiveRequests, + MfaVerified: id.MFAVerified, + PreviousIdentityExpires: timestampFromGoTime(id.PreviousIdentityExpires), + LoginIp: id.LoginIP, + PinnedIp: id.PinnedIP, + DisallowReissue: id.DisallowReissue, + CertificateExtensions: certExtensionsToProto(id.CertificateExtensions), + Renewable: id.Renewable, + Generation: id.Generation, + BotName: id.BotName, + BotInstanceId: id.BotInstanceID, + AllowedResourceIds: resourceIDsFromTypes(id.AllowedResourceIDs), + ConnectionDiagnosticId: id.ConnectionDiagnosticID, + PrivateKeyPolicy: string(id.PrivateKeyPolicy), + DeviceId: id.DeviceID, + DeviceAssetTag: id.DeviceAssetTag, + DeviceCredentialId: id.DeviceCredentialID, + GithubUserId: id.GitHubUserID, + GithubUsername: id.GitHubUsername, + } +} + +func certExtensionsFromProto(extensions []*decisionpb.CertExtension) []*types.CertExtension { + if len(extensions) == 0 { + return nil + } + out := make([]*types.CertExtension, 0, len(extensions)) + for _, extension := range extensions { + out = append(out, &types.CertExtension{ + Mode: types.CertExtensionMode(int32(extension.Mode) - 1), // enum is equivalent but off by 1 + Type: types.CertExtensionType(int32(extension.Type) - 1), // enum is equivalent but off by 1 + Name: extension.Name, + Value: extension.Value, + }) + } + return out +} + +func certExtensionsToProto(extensions []*types.CertExtension) []*decisionpb.CertExtension { + if len(extensions) == 0 { + return nil + } + out := make([]*decisionpb.CertExtension, 0, len(extensions)) + for _, extension := range extensions { + out = append(out, &decisionpb.CertExtension{ + Mode: decisionpb.CertExtensionMode(int32(extension.Mode) + 1), // enum is equivalent but off by 1 + Type: decisionpb.CertExtensionType(int32(extension.Type) + 1), // enum is equivalent but off by 1 + Name: extension.Name, + Value: extension.Value, + }) + } + return out +} diff --git a/lib/decision/ssh_identity_test.go b/lib/decision/ssh_identity_test.go new file mode 100644 index 0000000000000..9bd1412143010 --- /dev/null +++ b/lib/decision/ssh_identity_test.go @@ -0,0 +1,101 @@ +// 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 decision + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/wrappers" + "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/sshca" + "github.com/gravitational/teleport/lib/utils/testutils" +) + +func TestSSHIdentityConversion(t *testing.T) { + ident := &sshca.Identity{ + ValidAfter: 1, + ValidBefore: 2, + CertType: ssh.UserCert, + ClusterName: "some-cluster", + SystemRole: types.RoleNode, + Username: "user", + Impersonator: "impersonator", + Principals: []string{"login1", "login2"}, + PermitX11Forwarding: true, + PermitAgentForwarding: true, + PermitPortForwarding: true, + Roles: []string{"role1", "role2"}, + RouteToCluster: "cluster", + Traits: wrappers.Traits{"trait1": []string{"value1"}, "trait2": []string{"value2"}}, + ActiveRequests: []string{uuid.NewString()}, + MFAVerified: "mfa", + PreviousIdentityExpires: time.Unix(12345, 0), + LoginIP: "127.0.0.1", + PinnedIP: "127.0.0.1", + DisallowReissue: true, + CertificateExtensions: []*types.CertExtension{&types.CertExtension{ + Name: "extname", + Value: "extvalue", + Type: types.CertExtensionType_SSH, + Mode: types.CertExtensionMode_EXTENSION, + }}, + Renewable: true, + Generation: 3, + BotName: "bot", + BotInstanceID: "instance", + AllowedResourceIDs: []types.ResourceID{{ + ClusterName: "cluster", + Kind: types.KindKubePod, // must use a kube resource kind for parsing of sub-resource to work correctly + Name: "name", + SubResourceName: "sub/sub", + }}, + ConnectionDiagnosticID: "diag", + PrivateKeyPolicy: keys.PrivateKeyPolicy("policy"), + DeviceID: "device", + DeviceAssetTag: "asset", + DeviceCredentialID: "cred", + GitHubUserID: "github", + GitHubUsername: "ghuser", + } + + ignores := []string{ + "CertExtension.Type", // only currently defined enum variant is a zero value + "CertExtension.Mode", // only currently defined enum variant is a zero value + // TODO(fspmarshall): figure out a mechanism for making ignore of grpc fields more convenient + "CertExtension.XXX_NoUnkeyedLiteral", + "CertExtension.XXX_unrecognized", + "CertExtension.XXX_sizecache", + "ResourceID.XXX_NoUnkeyedLiteral", + "ResourceID.XXX_unrecognized", + "ResourceID.XXX_sizecache", + } + + require.True(t, testutils.ExhaustiveNonEmpty(ident, ignores...), "empty=%+v", testutils.FindAllEmpty(ident, ignores...)) + + proto := SSHIdentityFromSSHCA(ident) + + ident2 := SSHIdentityToSSHCA(proto) + + require.Empty(t, cmp.Diff(ident, ident2)) +} diff --git a/lib/reversetunnel/srv_test.go b/lib/reversetunnel/srv_test.go index 8794a8323f0f1..678cb46a7aa72 100644 --- a/lib/reversetunnel/srv_test.go +++ b/lib/reversetunnel/srv_test.go @@ -38,7 +38,6 @@ import ( "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/auth/testauthority" - "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/sshca" "github.com/gravitational/teleport/lib/utils" ) @@ -80,13 +79,15 @@ func TestServerKeyAuth(t *testing.T) { { desc: "host cert", key: func() ssh.PublicKey { - rawCert, err := ta.GenerateHostCert(services.HostCertParams{ + rawCert, err := ta.GenerateHostCert(sshca.HostCertificateRequest{ CASigner: caSigner, PublicHostKey: pub, HostID: "host-id", NodeName: con.User(), - ClusterName: "host-cluster-name", - Role: types.RoleNode, + Identity: sshca.Identity{ + ClusterName: "host-cluster-name", + SystemRole: types.RoleNode, + }, }) require.NoError(t, err) key, _, _, _, err := ssh.ParseAuthorizedKey(rawCert) @@ -111,7 +112,7 @@ func TestServerKeyAuth(t *testing.T) { TTL: time.Minute, Identity: sshca.Identity{ Username: con.User(), - AllowedLogins: []string{con.User()}, + Principals: []string{con.User()}, Roles: []string{"dev", "admin"}, RouteToCluster: "user-cluster-name", }, diff --git a/lib/services/authority.go b/lib/services/authority.go index 2345342b1195b..bd04c8c7c284a 100644 --- a/lib/services/authority.go +++ b/lib/services/authority.go @@ -23,14 +23,12 @@ import ( "crypto/tls" "crypto/x509" "encoding/json" - "time" "github.com/gogo/protobuf/proto" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "golang.org/x/crypto/ssh" "github.com/gravitational/teleport/api/types" apiutils "github.com/gravitational/teleport/api/utils" @@ -279,46 +277,6 @@ func GetSSHCheckingKeys(ca types.CertAuthority) [][]byte { return out } -// HostCertParams defines all parameters needed to generate a host certificate -type HostCertParams struct { - // CASigner is the signer that will sign the public key of the host with the CA private key. - CASigner ssh.Signer - // PublicHostKey is the public key of the host - PublicHostKey []byte - // HostID is used by Teleport to uniquely identify a node within a cluster - HostID string - // Principals is a list of additional principals to add to the certificate. - Principals []string - // NodeName is the DNS name of the node - NodeName string - // ClusterName is the name of the cluster within which a node lives - ClusterName string - // Role identifies the role of a Teleport instance - Role types.SystemRole - // TTL defines how long a certificate is valid for - TTL time.Duration -} - -// Check checks parameters for errors -func (c HostCertParams) Check() error { - if c.CASigner == nil { - return trace.BadParameter("CASigner is required") - } - if c.HostID == "" && len(c.Principals) == 0 { - return trace.BadParameter("HostID [%q] or Principals [%q] are required", - c.HostID, c.Principals) - } - if c.ClusterName == "" { - return trace.BadParameter("ClusterName [%q] is required", c.ClusterName) - } - - if err := c.Role.Check(); err != nil { - return trace.Wrap(err) - } - - return nil -} - // CertPoolFromCertAuthorities returns a certificate pool from the TLS certificates // set up in the certificate authorities list, as well as the number of certificates // that were added to the pool. diff --git a/lib/srv/authhandlers_test.go b/lib/srv/authhandlers_test.go index 8e009819e2108..9c5ce5b43b1d7 100644 --- a/lib/srv/authhandlers_test.go +++ b/lib/srv/authhandlers_test.go @@ -220,8 +220,8 @@ func TestRBAC(t *testing.T) { CASigner: caSigner, PublicUserKey: ssh.MarshalAuthorizedKey(privateKey.SSHPublicKey()), Identity: sshca.Identity{ - Username: "testuser", - AllowedLogins: []string{"testuser"}, + Username: "testuser", + Principals: []string{"testuser"}, }, }) require.NoError(t, err) @@ -395,8 +395,8 @@ func TestRBACJoinMFA(t *testing.T) { PublicUserKey: privateKey.MarshalSSHPublicKey(), CertificateFormat: constants.CertificateFormatStandard, Identity: sshca.Identity{ - Username: username, - AllowedLogins: []string{username}, + Username: username, + Principals: []string{username}, Traits: wrappers.Traits{ teleport.TraitInternalPrefix: []string{""}, }, diff --git a/lib/srv/git/forward_test.go b/lib/srv/git/forward_test.go index 3b4438cfa3a99..4ea3566bd9744 100644 --- a/lib/srv/git/forward_test.go +++ b/lib/srv/git/forward_test.go @@ -282,11 +282,11 @@ func makeUserCert(t *testing.T, caSigner ssh.Signer) ssh.Signer { PublicUserKey: clientPrivateKey.MarshalSSHPublicKey(), CertificateFormat: constants.CertificateFormatStandard, Identity: sshca.Identity{ - Username: "alice", - AllowedLogins: []string{"does-not-matter"}, - GitHubUserID: "1234567", - Traits: wrappers.Traits{}, - Roles: []string{"editor"}, + Username: "alice", + Principals: []string{"does-not-matter"}, + GitHubUserID: "1234567", + Traits: wrappers.Traits{}, + Roles: []string{"editor"}, }, }) require.NoError(t, err) diff --git a/lib/sshca/identity.go b/lib/sshca/identity.go index 19f40bfdf336d..32e1ad5f8ffb8 100644 --- a/lib/sshca/identity.go +++ b/lib/sshca/identity.go @@ -35,22 +35,39 @@ import ( "github.com/gravitational/teleport/api/types/wrappers" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/utils" ) // Identity is a user identity. All identity fields map directly to an ssh certificate field. type Identity struct { + + // --- common identity fields --- + // ValidAfter is the unix timestamp that marks the start time for when the certificate should // be considered valid. ValidAfter uint64 // ValidBefore is the unix timestamp that marks the end time for when the certificate should // be considered valid. ValidBefore uint64 + // CertType indicates what type of cert this is (user or host). + CertType uint32 + // Principals is the list of SSH principals associated with the certificate (this means the + // list of allowed unix logins in the case of user certs). + Principals []string + + // --- host identity fields --- + + // ClusterName is the name of the cluster within which a node lives + ClusterName string + // SystemRole identifies the system role of a Teleport instance + SystemRole types.SystemRole + + // -- user identity fields --- + // Username is teleport username Username string // Impersonator is set when a user requests certificate for another user Impersonator string - // AllowedLogins is a list of SSH principals - AllowedLogins []string // PermitX11Forwarding permits X11 forwarding for this cert PermitX11Forwarding bool // PermitAgentForwarding permits agent forwarding for this cert @@ -67,7 +84,7 @@ type Identity struct { Traits wrappers.Traits // ActiveRequests tracks privilege escalation requests applied during // certificate construction. - ActiveRequests services.RequestIDs + ActiveRequests []string // MFAVerified is the UUID of an MFA device when this Identity was // confirmed immediately after an MFA check. MFAVerified string @@ -100,7 +117,7 @@ type Identity struct { // Machine ID bot. It is empty for human users. BotInstanceID string // AllowedResourceIDs lists the resources the user should be able to access. - AllowedResourceIDs string + AllowedResourceIDs []types.ResourceID // ConnectionDiagnosticID references the ConnectionDiagnostic that we should use to append traces when testing a Connection. ConnectionDiagnosticID string // PrivateKeyPolicy is the private key policy supported by this certificate. @@ -120,15 +137,6 @@ type Identity struct { GitHubUsername string } -// Check performs validation of certain fields in the identity. -func (i *Identity) Check() error { - if len(i.AllowedLogins) == 0 { - return trace.BadParameter("ssh user identity missing allowed logins") - } - - return nil -} - // Encode encodes the identity into an ssh certificate. Note that the returned certificate is incomplete // and must be have its public key set before signing. func (i *Identity) Encode(certFormat string) (*ssh.Certificate, error) { @@ -140,18 +148,38 @@ func (i *Identity) Encode(certFormat string) (*ssh.Certificate, error) { if validAfter == 0 { validAfter = uint64(time.Now().UTC().Add(-1 * time.Minute).Unix()) } + + if i.CertType == 0 { + return nil, trace.BadParameter("cannot encode ssh identity missing required field CertType") + } + cert := &ssh.Certificate{ // we have to use key id to identify teleport user KeyId: i.Username, - ValidPrincipals: i.AllowedLogins, + ValidPrincipals: i.Principals, ValidAfter: validAfter, ValidBefore: validBefore, - CertType: ssh.UserCert, + CertType: i.CertType, + } + + cert.Permissions.Extensions = make(map[string]string) + + if i.CertType == ssh.UserCert { + cert.Permissions.Extensions[teleport.CertExtensionPermitPTY] = "" + } + + // --- host extensions --- + + if sr := i.SystemRole.String(); sr != "" { + cert.Permissions.Extensions[utils.CertExtensionRole] = sr } - cert.Permissions.Extensions = map[string]string{ - teleport.CertExtensionPermitPTY: "", + + if i.ClusterName != "" { + cert.Permissions.Extensions[utils.CertExtensionAuthority] = i.ClusterName } + // --- user extensions --- + if i.PermitX11Forwarding { cert.Permissions.Extensions[teleport.CertExtensionPermitX11Forwarding] = "" } @@ -188,8 +216,12 @@ func (i *Identity) Encode(certFormat string) (*ssh.Certificate, error) { if i.BotInstanceID != "" { cert.Permissions.Extensions[teleport.CertExtensionBotInstanceID] = i.BotInstanceID } - if i.AllowedResourceIDs != "" { - cert.Permissions.Extensions[teleport.CertExtensionAllowedResources] = i.AllowedResourceIDs + if len(i.AllowedResourceIDs) != 0 { + requestedResourcesStr, err := types.ResourceIDsToString(i.AllowedResourceIDs) + if err != nil { + return nil, trace.Wrap(err) + } + cert.Permissions.Extensions[teleport.CertExtensionAllowedResources] = requestedResourcesStr } if i.ConnectionDiagnosticID != "" { cert.Permissions.Extensions[teleport.CertExtensionConnectionDiagnosticID] = i.ConnectionDiagnosticID @@ -257,8 +289,11 @@ func (i *Identity) Encode(certFormat string) (*ssh.Certificate, error) { if i.RouteToCluster != "" { cert.Permissions.Extensions[teleport.CertExtensionTeleportRouteToCluster] = i.RouteToCluster } - if !i.ActiveRequests.IsEmpty() { - requests, err := i.ActiveRequests.Marshal() + if len(i.ActiveRequests) != 0 { + reqs := services.RequestIDs{ + AccessRequests: i.ActiveRequests, + } + requests, err := reqs.Marshal() if err != nil { return nil, trace.Wrap(err) } @@ -271,14 +306,12 @@ func (i *Identity) Encode(certFormat string) (*ssh.Certificate, error) { // DecodeIdentity decodes an ssh certificate into an identity. func DecodeIdentity(cert *ssh.Certificate) (*Identity, error) { - if cert.CertType != ssh.UserCert { - return nil, trace.BadParameter("DecodeIdentity intended for use with user certs, got %v", cert.CertType) - } ident := &Identity{ - Username: cert.KeyId, - AllowedLogins: cert.ValidPrincipals, - ValidAfter: cert.ValidAfter, - ValidBefore: cert.ValidBefore, + Username: cert.KeyId, + Principals: cert.ValidPrincipals, + ValidAfter: cert.ValidAfter, + ValidBefore: cert.ValidBefore, + CertType: cert.CertType, } // clone the extension map and remove entries from the clone as they are processed so @@ -304,9 +337,19 @@ func DecodeIdentity(cert *ssh.Certificate) (*Identity, error) { return ok } - // ignore the permit pty extension, it's always set + // ignore the permit pty extension, teleport considers this permission implied for all users _, _ = takeExtension(teleport.CertExtensionPermitPTY) + // --- host extensions --- + + if v, ok := takeExtension(utils.CertExtensionRole); ok { + ident.SystemRole = types.SystemRole(v) + } + + ident.ClusterName = takeValue(utils.CertExtensionAuthority) + + // --- user extensions --- + ident.PermitX11Forwarding = takeBool(teleport.CertExtensionPermitX11Forwarding) ident.PermitAgentForwarding = takeBool(teleport.CertExtensionPermitAgentForwarding) ident.PermitPortForwarding = takeBool(teleport.CertExtensionPermitPortForwarding) @@ -335,7 +378,15 @@ func DecodeIdentity(cert *ssh.Certificate) (*Identity, error) { ident.BotName = takeValue(teleport.CertExtensionBotName) ident.BotInstanceID = takeValue(teleport.CertExtensionBotInstanceID) - ident.AllowedResourceIDs = takeValue(teleport.CertExtensionAllowedResources) + + if v, ok := takeExtension(teleport.CertExtensionAllowedResources); ok { + resourceIDs, err := types.ResourceIDsFromString(v) + if err != nil { + return nil, trace.BadParameter("failed to parse value %q for extension %q as resource IDs: %v", v, teleport.CertExtensionAllowedResources, err) + } + ident.AllowedResourceIDs = resourceIDs + } + ident.ConnectionDiagnosticID = takeValue(teleport.CertExtensionConnectionDiagnosticID) ident.PrivateKeyPolicy = keys.PrivateKeyPolicy(takeValue(teleport.CertExtensionPrivateKeyPolicy)) ident.DeviceID = takeValue(teleport.CertExtensionDeviceID) @@ -371,11 +422,11 @@ func DecodeIdentity(cert *ssh.Certificate) (*Identity, error) { ident.RouteToCluster = takeValue(teleport.CertExtensionTeleportRouteToCluster) if v, ok := takeExtension(teleport.CertExtensionTeleportActiveRequests); ok { - var requests services.RequestIDs - if err := requests.Unmarshal([]byte(v)); err != nil { + var reqs services.RequestIDs + if err := reqs.Unmarshal([]byte(v)); err != nil { return nil, trace.BadParameter("failed to unmarshal value %q for extension %q as active requests: %v", v, teleport.CertExtensionTeleportActiveRequests, err) } - ident.ActiveRequests = requests + ident.ActiveRequests = reqs.AccessRequests } // aggregate all remaining extensions into the CertificateExtensions field diff --git a/lib/sshca/identity_test.go b/lib/sshca/identity_test.go index 5c7c6db75b3e8..ef5b721f993a4 100644 --- a/lib/sshca/identity_test.go +++ b/lib/sshca/identity_test.go @@ -26,31 +26,32 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/wrappers" "github.com/gravitational/teleport/api/utils/keys" - "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils/testutils" ) func TestIdentityConversion(t *testing.T) { ident := &Identity{ - ValidAfter: 1, - ValidBefore: 2, - Username: "user", - Impersonator: "impersonator", - AllowedLogins: []string{"login1", "login2"}, - PermitX11Forwarding: true, - PermitAgentForwarding: true, - PermitPortForwarding: true, - Roles: []string{"role1", "role2"}, - RouteToCluster: "cluster", - Traits: wrappers.Traits{"trait1": []string{"value1"}, "trait2": []string{"value2"}}, - ActiveRequests: services.RequestIDs{ - AccessRequests: []string{uuid.NewString()}, - }, + ValidAfter: 1, + ValidBefore: 2, + CertType: ssh.UserCert, + ClusterName: "some-cluster", + SystemRole: types.RoleNode, + Username: "user", + Impersonator: "impersonator", + Principals: []string{"login1", "login2"}, + PermitX11Forwarding: true, + PermitAgentForwarding: true, + PermitPortForwarding: true, + Roles: []string{"role1", "role2"}, + RouteToCluster: "cluster", + Traits: wrappers.Traits{"trait1": []string{"value1"}, "trait2": []string{"value2"}}, + ActiveRequests: []string{uuid.NewString()}, MFAVerified: "mfa", PreviousIdentityExpires: time.Unix(12345, 0), LoginIP: "127.0.0.1", @@ -62,11 +63,16 @@ func TestIdentityConversion(t *testing.T) { Type: types.CertExtensionType_SSH, Mode: types.CertExtensionMode_EXTENSION, }}, - Renewable: true, - Generation: 3, - BotName: "bot", - BotInstanceID: "instance", - AllowedResourceIDs: "resource", + Renewable: true, + Generation: 3, + BotName: "bot", + BotInstanceID: "instance", + AllowedResourceIDs: []types.ResourceID{{ + ClusterName: "cluster", + Kind: types.KindKubePod, // must use a kube resource kind for parsing of sub-resource to work correctly + Name: "name", + SubResourceName: "sub/sub", + }}, ConnectionDiagnosticID: "diag", PrivateKeyPolicy: keys.PrivateKeyPolicy("policy"), DeviceID: "device", @@ -83,6 +89,9 @@ func TestIdentityConversion(t *testing.T) { "CertExtension.XXX_NoUnkeyedLiteral", "CertExtension.XXX_unrecognized", "CertExtension.XXX_sizecache", + "ResourceID.XXX_NoUnkeyedLiteral", + "ResourceID.XXX_unrecognized", + "ResourceID.XXX_sizecache", } require.True(t, testutils.ExhaustiveNonEmpty(ident, ignores...), "empty=%+v", testutils.FindAllEmpty(ident, ignores...)) diff --git a/lib/sshca/sshca.go b/lib/sshca/sshca.go index 15f5dcf6c1aeb..95f3c1fb7a17e 100644 --- a/lib/sshca/sshca.go +++ b/lib/sshca/sshca.go @@ -26,7 +26,6 @@ import ( "golang.org/x/crypto/ssh" apidefaults "github.com/gravitational/teleport/api/defaults" - "github.com/gravitational/teleport/lib/services" ) // Authority implements minimal key-management facility for generating OpenSSH @@ -35,13 +34,54 @@ type Authority interface { // GenerateHostCert takes the private key of the CA, public key of the new host, // along with metadata (host ID, node name, cluster name, roles, and ttl) and generates // a host certificate. - GenerateHostCert(certParams services.HostCertParams) ([]byte, error) + GenerateHostCert(HostCertificateRequest) ([]byte, error) // GenerateUserCert generates user ssh certificate, it takes pkey as a signing // private key (user certificate authority) GenerateUserCert(UserCertificateRequest) ([]byte, error) } +// HostCertificateRequest is a request to generate a new ssh host certificate. +type HostCertificateRequest struct { + // CASigner is the signer that will sign the public key of the host with the CA private key + CASigner ssh.Signer + // PublicHostKey is the public key of the host + PublicHostKey []byte + // HostID is used by Teleport to uniquely identify a node within a cluster (this is used to help infill + // Identity.Princiapals and is not a standalone cert field). + HostID string + // NodeName is the DNS name of the node (this is used to help infill Identity.Princiapals and is not a + // standalone cert field). + NodeName string + // TTL defines how long a certificate is valid for + TTL time.Duration + // Identity is the host identity to be encoded in the certificate. + Identity Identity +} + +func (r *HostCertificateRequest) Check() error { + if r.CASigner == nil { + return trace.BadParameter("ssh host certificate request missing ca signer") + } + if r.HostID == "" && len(r.Identity.Principals) == 0 { + return trace.BadParameter("ssh host certificate request missing host ID and principals") + } + if r.Identity.ClusterName == "" { + return trace.BadParameter("ssh host certificate request missing cluster name") + } + if r.Identity.ValidBefore != 0 { + return trace.BadParameter("ValidBefore should not be set in host cert requests (derived from TTL)") + } + if r.Identity.ValidAfter != 0 { + return trace.BadParameter("ValidAfter should not be set in host cert requests (derived from TTL)") + } + if err := r.Identity.SystemRole.Check(); err != nil { + return trace.Wrap(err) + } + + return nil +} + // UserCertificateRequest is a request to generate a new ssh user certificate. type UserCertificateRequest struct { // CASigner is the signer that will sign the public key of the user with the CA private key @@ -64,8 +104,14 @@ func (r *UserCertificateRequest) CheckAndSetDefaults() error { if r.TTL < apidefaults.MinCertDuration { r.TTL = apidefaults.MinCertDuration } - if err := r.Identity.Check(); err != nil { - return trace.Wrap(err) + if len(r.Identity.Principals) == 0 { + return trace.BadParameter("ssh user identity missing allowed logins") + } + if r.Identity.ValidBefore != 0 { + return trace.BadParameter("ValidBefore should not be set in user cert requests (derived from TTL)") + } + if r.Identity.ValidAfter != 0 { + return trace.BadParameter("ValidAfter should not be set in user cert requests (derived from TTL)") } return nil