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