package certproof import ( "fmt" "strings" "unsafe" log "github.com/sirupsen/logrus" "golang.org/x/sys/windows" ) const ( noActiveSession = 0xFFFFFFFF servicesSession = 0 // wtsCurrentServer is WTS_CURRENT_SERVER_HANDLE; wtsActive and wtsDisconnected are // WTSActive and WTSDisconnected of WTS_CONNECTSTATE_CLASS. None is exported by // x/sys/windows. wtsCurrentServer = windows.Handle(0) wtsActive = 0 wtsDisconnected = 4 ) // DesktopUser is an interactive session and the account signed into it. The user's // certificate store is readable only from a process running as that account, because // its private keys are protected against the user profile rather than the machine. type DesktopUser struct { Session uint32 Name string Token windows.Token } // Close releases the session token. func (u DesktopUser) Close() { if err := u.Token.Close(); err != nil { log.Debugf("failed closing desktop session token: %v", err) } } // CurrentDesktopUser returns a token for the session whose certificate store should be // asked: a session of owner, the account the active profile belongs to, with the // physical console preferred over remote sessions. With no owner only the console user // counts. Picking any signed-in user instead would let whoever else is logged in to a // terminal server or VDI host decide the result. The second return is false when no // such session exists, and only machine certificates can then be proven. // // Obtaining the token needs SE_TCB_NAME, which the LocalSystem service has and an // ordinary process does not. func CurrentDesktopUser(owner string) (DesktopUser, bool) { console := windows.WTSGetActiveConsoleSessionId() if owner == "" { return consoleUser(console) } match, err := ownerMatcher(owner) if err != nil { log.Debugf("certificate posture: %v", err) return DesktopUser{}, false } sessions, err := userSessions(console) if err != nil { log.Debugf("cannot enumerate terminal sessions: %v", err) return DesktopUser{}, false } for _, session := range sessions { user, ok := desktopUser(session) if !ok { continue } if match(user) { return user, true } user.Close() } log.Debugf("certificate posture: profile owner %s has no signed-in session, no user certificate store is reachable", owner) return DesktopUser{}, false } func consoleUser(console uint32) (DesktopUser, bool) { if console == noActiveSession || console == servicesSession { log.Debug("no console session, no user certificate store is reachable") return DesktopUser{}, false } user, ok := desktopUser(console) if !ok { log.Debugf("console session %d has nobody signed in, no user certificate store is reachable", console) } return user, ok } // ownerMatcher reports whether a session user is owner. Accounts are compared by SID, // which is what identifies a Windows account; the name comparison is a fallback for an // owner name that no longer resolves, and is case-insensitive like Windows account names. func ownerMatcher(owner string) (func(DesktopUser) bool, error) { ownerSID, _, _, err := windows.LookupSID("", owner) if err != nil { log.Debugf("certificate posture: resolving profile owner %s: %v, matching by name", owner, err) return func(user DesktopUser) bool { return sameAccountName(user.Name, owner) }, nil } return func(user DesktopUser) bool { tokenUser, err := user.Token.GetTokenUser() if err != nil { log.Debugf("failed reading token user of session %d: %v", user.Session, err) return false } return tokenUser.User.Sid.Equals(ownerSID) }, nil } // sameAccountName compares DOMAIN\account names case-insensitively, and an owner given // without a domain against the account part alone. func sameAccountName(sessionName, owner string) bool { if strings.EqualFold(sessionName, owner) { return true } if strings.Contains(owner, `\`) { return false } _, account, found := strings.Cut(sessionName, `\`) return found && strings.EqualFold(account, owner) } func desktopUser(session uint32) (DesktopUser, bool) { var token windows.Token if err := windows.WTSQueryUserToken(session, &token); err != nil { log.Debugf("no user token for session %d: %v", session, err) return DesktopUser{}, false } name, err := tokenAccount(token) if err != nil { log.Debugf("session %d token has no readable account: %v", session, err) if closeErr := token.Close(); closeErr != nil { log.Debugf("failed closing session token: %v", closeErr) } return DesktopUser{}, false } return DesktopUser{Session: session, Name: name, Token: token}, true } func tokenAccount(token windows.Token) (string, error) { user, err := token.GetTokenUser() if err != nil { return "", fmt.Errorf("read token user: %w", err) } account, domain, _, err := user.User.Sid.LookupAccount("") if err != nil { return "", fmt.Errorf("look up account: %w", err) } if domain == "" { return account, nil } return domain + `\` + account, nil } // userSessions lists the sessions a user can be signed in to, console first, then // active remote sessions, then disconnected ones, whose user is still signed in. Session // 0 is skipped: it hosts services and never belongs to an interactive user. func userSessions(console uint32) ([]uint32, error) { var info *windows.WTS_SESSION_INFO var count uint32 if err := windows.WTSEnumerateSessions(wtsCurrentServer, 0, 1, &info, &count); err != nil { return nil, fmt.Errorf("enumerate sessions: %w", err) } defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(info))) var sessions []uint32 if console != noActiveSession && console != servicesSession { sessions = append(sessions, console) } for _, state := range []uint32{wtsActive, wtsDisconnected} { for _, session := range unsafe.Slice(info, count) { if session.SessionID == servicesSession || session.SessionID == console || session.State != state { continue } sessions = append(sessions, session.SessionID) } } return sessions, nil } // runningAsLocalSystem reports whether this process is the service. The helper runs as // the signed-in user and must read its own store rather than launching another helper. func runningAsLocalSystem() bool { user, err := windows.GetCurrentProcessToken().GetTokenUser() if err != nil { log.Debugf("failed reading own token user: %v", err) return false } return user.User.Sid.IsWellKnown(windows.WinLocalSystemSid) }