package session import ( "context" "crypto/ed25519" "crypto/rand" "crypto/x509" "fmt" "math/big" mathrand "math/rand" "strings" "time" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/timestamp" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/structwrapping" "google.golang.org/protobuf/types/known/timestamppb" ) const ( defaultSessionTableName = "session" ) // ComposedOf defines the boundary data that is referenced to compose a session. type ComposedOf struct { // UserId of the session UserId string // HostId of the session HostId string // TargetId of the session TargetId string // HostSetId of the session HostSetId string // AuthTokenId of the session AuthTokenId string // ScopeId of the session ScopeId string // Endpoint. This is generated by the target, but is not stored in the // warehouse as the worker may need to e.g. resolve DNS. This is to round // trip the information to the worker when it validates a session. Endpoint string // Expiration time for the session ExpirationTime *timestamp.Timestamp // Max connections for the session ConnectionLimit int32 } // Session contains information about a user's session with a target type Session struct { // PublicId is used to access the session via an API PublicId string `json:"public_id,omitempty" gorm:"primary_key"` // UserId for the session UserId string `json:"user_id,omitempty" gorm:"default:null"` // HostId of the session HostId string `json:"host_id,omitempty" gorm:"default:null"` // ServerId that proxied the session ServerId string `json:"server_id,omitempty" gorm:"default:null"` // ServerType that proxied the session ServerType string `json:"server_type,omitempty" gorm:"default:null"` // TargetId for the session TargetId string `json:"target_id,omitempty" gorm:"default:null"` // HostSetId for the session HostSetId string `json:"host_set_id,omitempty" gorm:"default:null"` // AuthTokenId for the session AuthTokenId string `json:"auth_token_id,omitempty" gorm:"default:null"` // ScopeId for the session ScopeId string `json:"scope_id,omitempty" gorm:"default:null"` // Certificate to use when connecting (or if using custom certs, to // serve as the "login"). Raw DER bytes. Private key is not, and should not be // stored in the database. Certificate []byte `json:"certificate,omitempty" gorm:"default:null"` // ExpirationTime - after this time the connection will be expired, e.g. forcefully terminated ExpirationTime *timestamp.Timestamp `json:"expiration_time,omitempty" gorm:"default:null"` // CtTofuToken is the ciphertext Tofutoken value stored in the database CtTofuToken []byte `json:"ct_tofu_token,omitempty" gorm:"column:tofu_token;default:null" wrapping:"ct,tofu_token"` // TofuToken - plain text of the "trust on first use" token for session TofuToken []byte `json:"tofu_token,omitempty" gorm:"-" wrapping:"pt,tofu_token"` // termination_reason for the session TerminationReason string `json:"termination_reason,omitempty" gorm:"default:null"` // CreateTime from the RDBMS CreateTime *timestamp.Timestamp `json:"create_time,omitempty" gorm:"default:current_timestamp"` // UpdateTime from the RDBMS UpdateTime *timestamp.Timestamp `json:"update_time,omitempty" gorm:"default:current_timestamp"` // Version for the session Version uint32 `json:"version,omitempty" gorm:"default:null"` // Endpoint Endpoint string `json:"-" gorm:"default:null"` // Maximum number of connections in a session ConnectionLimit int32 `json:"connection_limit,omitempty" gorm:"default:null"` // key_id is the key ID that was used for the encryption operation. It can be // used to identify a specific version of the key needed to decrypt the value, // which is useful for caching purposes. // @inject_tag: `gorm:"not_null"` KeyId string `json:"key_id,omitempty" gorm:"not_null"` // States for the session which are for read only and are ignored during // write operations States []*State `gorm:"-"` tableName string `gorm:"-"` } func (s *Session) GetPublicId() string { return s.PublicId } var _ Cloneable = (*Session)(nil) var _ db.VetForWriter = (*Session)(nil) // New creates a new in memory session. func New(c ComposedOf, opt ...Option) (*Session, error) { s := Session{ UserId: c.UserId, HostId: c.HostId, TargetId: c.TargetId, HostSetId: c.HostSetId, AuthTokenId: c.AuthTokenId, ScopeId: c.ScopeId, Endpoint: c.Endpoint, ExpirationTime: c.ExpirationTime, ConnectionLimit: c.ConnectionLimit, } if err := s.validateNewSession("new session:"); err != nil { return nil, err } return &s, nil } // AllocSession will allocate a Session func AllocSession() Session { return Session{} } // Clone creates a clone of the Session func (s *Session) Clone() interface{} { clone := &Session{ PublicId: s.PublicId, UserId: s.UserId, HostId: s.HostId, ServerId: s.ServerId, ServerType: s.ServerType, TargetId: s.TargetId, HostSetId: s.HostSetId, AuthTokenId: s.AuthTokenId, ScopeId: s.ScopeId, TerminationReason: s.TerminationReason, Version: s.Version, Endpoint: s.Endpoint, ConnectionLimit: s.ConnectionLimit, } if len(s.States) > 0 { clone.States = make([]*State, 0, len(s.States)) for _, ss := range s.States { cp := ss.Clone().(*State) clone.States = append(clone.States, cp) } } if s.TofuToken != nil { clone.TofuToken = make([]byte, len(s.TofuToken)) copy(clone.TofuToken, s.TofuToken) } if s.CtTofuToken != nil { clone.CtTofuToken = make([]byte, len(s.CtTofuToken)) copy(clone.CtTofuToken, s.CtTofuToken) } if s.Certificate != nil { clone.Certificate = make([]byte, len(s.Certificate)) copy(clone.Certificate, s.Certificate) } if s.ExpirationTime != nil { clone.ExpirationTime = ×tamp.Timestamp{ Timestamp: ×tamppb.Timestamp{ Seconds: s.ExpirationTime.Timestamp.Seconds, Nanos: s.ExpirationTime.Timestamp.Nanos, }, } } if s.CreateTime != nil { clone.CreateTime = ×tamp.Timestamp{ Timestamp: ×tamppb.Timestamp{ Seconds: s.CreateTime.Timestamp.Seconds, Nanos: s.CreateTime.Timestamp.Nanos, }, } } if s.UpdateTime != nil { clone.UpdateTime = ×tamp.Timestamp{ Timestamp: ×tamppb.Timestamp{ Seconds: s.UpdateTime.Timestamp.Seconds, Nanos: s.UpdateTime.Timestamp.Nanos, }, } } return clone } // VetForWrite implements db.VetForWrite() interface and validates the session // before it's written. func (s *Session) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { opts := db.GetOpts(opt...) if s.PublicId == "" { return fmt.Errorf("session vet for write: missing public id: %w", db.ErrInvalidParameter) } switch opType { case db.CreateOp: if err := s.validateNewSession("session vet for write:"); err != nil { return err } if len(s.Certificate) == 0 { return fmt.Errorf("session vet for write: certificate is missing: %w", db.ErrInvalidParameter) } case db.UpdateOp: switch { case contains(opts.WithFieldMaskPaths, "PublicId"): return fmt.Errorf("session vet for write: public id is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "UserId"): return fmt.Errorf("session vet for write: user id is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "HostId"): return fmt.Errorf("session vet for write: host id is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "TargetId"): return fmt.Errorf("session vet for write: target id is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "HostSetId"): return fmt.Errorf("session vet for write: host set id is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "AuthTokenId"): return fmt.Errorf("session vet for write: auth token id is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "Certificate"): return fmt.Errorf("session vet for write: certificate is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "CreateTime"): return fmt.Errorf("session vet for write: create time is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "UpdateTime"): return fmt.Errorf("session vet for write: update time is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "Endpoint"): return fmt.Errorf("session vet for write: endpoint is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "ExpirationTime"): return fmt.Errorf("session vet for write: expiration time is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "ConnectionLimit"): return fmt.Errorf("session vet for write: connection limit is immutable: %w", db.ErrInvalidParameter) case contains(opts.WithFieldMaskPaths, "TerminationReason"): if _, err := convertToReason(s.TerminationReason); err != nil { return fmt.Errorf("session vet for write: termination reason '%s' is invalid: %w", s.TerminationReason, db.ErrInvalidParameter) } } } return nil } // TableName returns the tablename to override the default gorm table name func (s *Session) TableName() string { if s.tableName != "" { return s.tableName } return defaultSessionTableName } // SetTableName sets the tablename and satisfies the ReplayableMessage // interface. If the caller attempts to set the name to "" the name will be // reset to the default name. func (s *Session) SetTableName(n string) { s.tableName = n } // validateNewSession checks everything but the session's PublicId func (s *Session) validateNewSession(errorPrefix string) error { if s.UserId == "" { return fmt.Errorf("%s missing user id: %w", errorPrefix, db.ErrInvalidParameter) } if s.HostId == "" { return fmt.Errorf("%s missing host id: %w", errorPrefix, db.ErrInvalidParameter) } if s.TargetId == "" { return fmt.Errorf("%s missing target id: %w", errorPrefix, db.ErrInvalidParameter) } if s.HostSetId == "" { return fmt.Errorf("%s missing host set id: %w", errorPrefix, db.ErrInvalidParameter) } if s.AuthTokenId == "" { return fmt.Errorf("%s missing auth token id: %w", errorPrefix, db.ErrInvalidParameter) } if s.ScopeId == "" { return fmt.Errorf("%s missing scope id: %w", errorPrefix, db.ErrInvalidParameter) } if s.Endpoint == "" { return fmt.Errorf("%s missing endpoint: %w", errorPrefix, db.ErrInvalidParameter) } if s.ExpirationTime.GetTimestamp().AsTime().IsZero() { return fmt.Errorf("%s missing expiration time: %w", errorPrefix, db.ErrInvalidParameter) } if s.TerminationReason != "" { return fmt.Errorf("%s termination reason must be empty: %w", errorPrefix, db.ErrInvalidParameter) } if s.ServerId != "" { return fmt.Errorf("%s server id must be empty: %w", errorPrefix, db.ErrInvalidParameter) } if s.ServerType != "" { return fmt.Errorf("%s server type must be empty: %w", errorPrefix, db.ErrInvalidParameter) } if s.TofuToken != nil { return fmt.Errorf("%s tofu token must be empty: %w", errorPrefix, db.ErrInvalidParameter) } if s.CtTofuToken != nil { return fmt.Errorf("%s ct must be empty: %w", errorPrefix, db.ErrInvalidParameter) } return nil } func contains(ss []string, t string) bool { for _, s := range ss { if strings.EqualFold(s, t) { return true } } return false } func newCert(wrapper wrapping.Wrapper, userId, jobId string, exp time.Time) (ed25519.PrivateKey, []byte, error) { if wrapper == nil { return nil, nil, fmt.Errorf("new session cert: missing wrapper: %w", db.ErrInvalidParameter) } if userId == "" { return nil, nil, fmt.Errorf("new session cert: missing user id: %w", db.ErrInvalidParameter) } if jobId == "" { return nil, nil, fmt.Errorf("new session cert: missing job id: %w", db.ErrInvalidParameter) } pubKey, privKey, err := DeriveED25519Key(wrapper, userId, jobId) if err != nil { return nil, nil, fmt.Errorf("new session cert: ") } template := &x509.Certificate{ ExtKeyUsage: []x509.ExtKeyUsage{ x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth, }, DNSNames: []string{jobId}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign, SerialNumber: big.NewInt(mathrand.Int63()), NotBefore: time.Now().Add(-1 * time.Minute), NotAfter: exp, BasicConstraintsValid: true, IsCA: true, } certBytes, err := x509.CreateCertificate(rand.Reader, template, template, pubKey, privKey) if err != nil { return nil, nil, fmt.Errorf("new session cert: %w", err) } return privKey, certBytes, nil } func (s *Session) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { if err := structwrapping.WrapStruct(ctx, cipher, s, nil); err != nil { return fmt.Errorf("error encrypting session: %w", err) } s.KeyId = cipher.KeyID() return nil } func (s *Session) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { if err := structwrapping.UnwrapStruct(ctx, cipher, s, nil); err != nil { return fmt.Errorf("error decrypting session: %w", err) } return nil } type sessionView struct { // Session fields PublicId string `json:"public_id,omitempty" gorm:"primary_key"` UserId string `json:"user_id,omitempty" gorm:"default:null"` HostId string `json:"host_id,omitempty" gorm:"default:null"` ServerId string `json:"server_id,omitempty" gorm:"default:null"` ServerType string `json:"server_type,omitempty" gorm:"default:null"` TargetId string `json:"target_id,omitempty" gorm:"default:null"` HostSetId string `json:"host_set_id,omitempty" gorm:"default:null"` AuthTokenId string `json:"auth_token_id,omitempty" gorm:"default:null"` ScopeId string `json:"scope_id,omitempty" gorm:"default:null"` Certificate []byte `json:"certificate,omitempty" gorm:"default:null"` ExpirationTime *timestamp.Timestamp `json:"expiration_time,omitempty" gorm:"default:null"` CtTofuToken []byte `json:"ct_tofu_token,omitempty" gorm:"column:tofu_token;default:null" wrapping:"ct,tofu_token"` TofuToken []byte `json:"tofu_token,omitempty" gorm:"-" wrapping:"pt,tofu_token"` TerminationReason string `json:"termination_reason,omitempty" gorm:"default:null"` CreateTime *timestamp.Timestamp `json:"create_time,omitempty" gorm:"default:current_timestamp"` UpdateTime *timestamp.Timestamp `json:"update_time,omitempty" gorm:"default:current_timestamp"` Version uint32 `json:"version,omitempty" gorm:"default:null"` Endpoint string `json:"-" gorm:"default:null"` ConnectionLimit int32 `json:"connection_limit,omitempty" gorm:"default:null"` KeyId string `json:"key_id,omitempty" gorm:"not_null"` // State fields Status string `json:"state,omitempty" gorm:"column:state"` PreviousEndTime *timestamp.Timestamp `json:"previous_end_time,omitempty" gorm:"default:current_timestamp"` StartTime *timestamp.Timestamp `json:"start_time,omitempty" gorm:"default:current_timestamp;primary_key"` EndTime *timestamp.Timestamp `json:"end_time,omitempty" gorm:"default:current_timestamp"` } // TableName returns the tablename to override the default gorm table name func (s *sessionView) TableName() string { return "session_with_state" }