Files
netbird/client/internal/certproof/consoleuser_darwin.go
T
mlsmaycon 422912e7d5 read the console user's keychain through a user session helper
A root daemon cannot reach a login keychain: securityd is per session and a
key ACL needs a session to prompt in, so dropping uid is not enough. The
daemon now answers certificate challenges from the System keychain itself,
where MDM installs device identities, and launches "netbird posture
cert-proof" into the console user's desktop session with launchctl asuser
for the login keychain. Only the signature and the chain cross back, never
the private key.

The console user comes from SCDynamicStoreCopyConsoleUser, bound with purego
like the keychain calls. The login window reports no user, root, or
"loginwindow", and all three are treated as no keychain to read, so a Mac at
the lock screen sends device proofs alone.

Adds info logging across the path: the keychain search list, per class query
status and item counts, the chain built per candidate, and the verification
error for every rejected candidate. A run that sends nothing now says why.

README.md documents the trust model, the console user limitation and how to
read the logs.
2026-09-13 14:40:23 +02:00

114 lines
3.4 KiB
Go

package certproof
import (
"bytes"
"fmt"
"sync"
"github.com/ebitengine/purego"
log "github.com/sirupsen/logrus"
)
const (
systemConfigurationFramework = "/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration"
encodingUTF8 = 0x08000100
consoleNameBufSize = 256
)
var (
consoleOnce sync.Once
consoleErr error
scDynamicStoreCopyConsoleUser func(store uintptr, uid, gid *uint32) uintptr
cfStringGetCString func(str uintptr, buffer *byte, size int, encoding uint32) bool
)
// ConsoleUser is the account whose desktop session owns the display. Its login keychain
// is the only user keychain a NetBird daemon can reach, and only while it is logged in.
type ConsoleUser struct {
Name string
UID uint32
GID uint32
}
// CurrentConsoleUser reports the user sitting at the desktop. The second return value is
// false when nobody is: at the login window macOS either reports no console user at all
// or attributes the session to root, and neither has a login keychain to offer.
func CurrentConsoleUser() (ConsoleUser, bool) {
if err := loadConsoleUser(); err != nil {
log.Infof("console user lookup unavailable: %v", err)
return ConsoleUser{}, false
}
var uid, gid uint32
name := scDynamicStoreCopyConsoleUser(0, &uid, &gid)
if name == 0 {
log.Info("no console user is logged in, no login keychain is reachable")
return ConsoleUser{}, false
}
defer cfRelease(name)
user := ConsoleUser{Name: cfString(name), UID: uid, GID: gid}
if !user.hasDesktop() {
log.Infof("console session belongs to %q uid=%d, which is not a desktop login, no login keychain is reachable", user.Name, user.UID)
return ConsoleUser{}, false
}
return user, true
}
// hasDesktop reports whether the console session is a real user desktop. The login
// window runs as root and some macOS releases name it "loginwindow" instead.
func (u ConsoleUser) hasDesktop() bool {
switch u.Name {
case "", "root", "loginwindow":
return false
}
return u.UID != 0
}
func cfString(str uintptr) string {
buf := make([]byte, consoleNameBufSize)
if !cfStringGetCString(str, &buf[0], len(buf), encodingUTF8) {
return ""
}
if end := bytes.IndexByte(buf, 0); end >= 0 {
return string(buf[:end])
}
return string(buf)
}
// loadConsoleUser resolves the console user symbols. It loads the keychain bindings
// first because CFRelease is resolved there and released strings depend on it.
func loadConsoleUser() error {
if err := loadKeychain(); err != nil {
return err
}
consoleOnce.Do(func() { consoleErr = resolveConsoleUser() })
return consoleErr
}
func resolveConsoleUser() error {
systemConfiguration, err := purego.Dlopen(systemConfigurationFramework, purego.RTLD_LAZY|purego.RTLD_GLOBAL)
if err != nil {
return fmt.Errorf("open %s: %w", systemConfigurationFramework, err)
}
coreFoundation, err := purego.Dlopen(coreFoundationFramework, purego.RTLD_LAZY|purego.RTLD_GLOBAL)
if err != nil {
return fmt.Errorf("open %s: %w", coreFoundationFramework, err)
}
symbol, err := purego.Dlsym(systemConfiguration, "SCDynamicStoreCopyConsoleUser")
if err != nil {
return fmt.Errorf("resolve SCDynamicStoreCopyConsoleUser: %w", err)
}
purego.RegisterFunc(&scDynamicStoreCopyConsoleUser, symbol)
symbol, err = purego.Dlsym(coreFoundation, "CFStringGetCString")
if err != nil {
return fmt.Errorf("resolve CFStringGetCString: %w", err)
}
purego.RegisterFunc(&cfStringGetCString, symbol)
return nil
}