diff options
Diffstat (limited to 'pam_fscrypt')
| -rw-r--r-- | pam_fscrypt/config | 8 | ||||
| -rw-r--r-- | pam_fscrypt/pam_fscrypt.go | 236 | ||||
| -rw-r--r-- | pam_fscrypt/run_fscrypt.go | 94 | ||||
| -rw-r--r-- | pam_fscrypt/run_test.go | 2 |
4 files changed, 266 insertions, 74 deletions
diff --git a/pam_fscrypt/config b/pam_fscrypt/config index 795a4f8..f83dab2 100644 --- a/pam_fscrypt/config +++ b/pam_fscrypt/config @@ -1,13 +1,13 @@ Name: fscrypt PAM passphrase support Default: yes -Priority: 0 +Priority: 100 Auth-Type: Additional Auth-Final: - optional pam_fscrypt.so + optional PAM_INSTALL_PATH Session-Type: Additional Session-Interactive-Only: yes Session-Final: - optional pam_fscrypt.so drop_caches lock_policies + optional PAM_INSTALL_PATH Password-Type: Additional Password-Final: - optional pam_fscrypt.so + optional PAM_INSTALL_PATH diff --git a/pam_fscrypt/pam_fscrypt.go b/pam_fscrypt/pam_fscrypt.go index 571a42b..15066c1 100644 --- a/pam_fscrypt/pam_fscrypt.go +++ b/pam_fscrypt/pam_fscrypt.go @@ -17,8 +17,6 @@ * the License. */ -// +build linux,cgo - package main /* @@ -31,13 +29,18 @@ package main */ import "C" import ( + "fmt" "log" + "log/syslog" + "os" + "strconv" "unsafe" "github.com/pkg/errors" "github.com/google/fscrypt/actions" "github.com/google/fscrypt/crypto" + "github.com/google/fscrypt/keyring" "github.com/google/fscrypt/pam" "github.com/google/fscrypt/security" ) @@ -46,21 +49,47 @@ const ( moduleName = "pam_fscrypt" // authtokLabel tags the AUTHTOK in the PAM data. authtokLabel = "fscrypt_authtok" + // pidLabel tags the pid in the PAM data. + pidLabel = "fscrypt_pid" // These flags are used to toggle behavior of the PAM module. debugFlag = "debug" - lockFlag = "lock_policies" - cacheFlag = "drop_caches" + + // This option is accepted for compatibility with existing config files, + // but now we lock policies by default and this option is a no-op. + lockPoliciesFlag = "lock_policies" + + // Only unlock directories, don't lock them. + unlockOnlyFlag = "unlock_only" + + // This option is accepted for compatibility with existing config files, + // but it no longer does anything. pam_fscrypt now drops caches if and + // only if it is needed. (Usually it is not needed anymore, as the + // FS_IOC_REMOVE_ENCRYPTION_KEY ioctl handles this automatically.) + dropCachesFlag = "drop_caches" +) + +var ( + // PamFuncs for our 4 provided methods + authenticateFunc = PamFunc{"Authenticate", Authenticate} + openSessionFunc = PamFunc{"OpenSession", OpenSession} + closeSessionFunc = PamFunc{"CloseSession", CloseSession} + chauthtokFunc = PamFunc{"Chauthtok", Chauthtok} ) // Authenticate copies the AUTHTOK (if necessary) into the PAM data so it can be // used in pam_sm_open_session. func Authenticate(handle *pam.Handle, _ map[string]bool) error { - log.Print("Authenticate()") if err := handle.StartAsPamUser(); err != nil { return err } defer handle.StopAsPamUser() + // Save the PID in the PAM data so that the Session hook can try to + // detect the unsupported situation where the process was forked. + if err := handle.SetString(pidLabel, strconv.Itoa(os.Getpid())); err != nil { + return errors.Wrap(err, "could not save pid in PAM data") + } + // If this user doesn't have a login protector, no unlocking is needed. if _, err := loginProtector(handle); err != nil { log.Printf("no protector, no need for AUTHTOK: %s", err) @@ -76,10 +105,77 @@ func Authenticate(handle *pam.Handle, _ map[string]bool) error { return errors.Wrap(err, "could not set AUTHTOK data") } +func beginProvisioningOp(handle *pam.Handle, policy *actions.Policy) error { + if policy.NeedsRootToProvision() { + return handle.StopAsPamUser() + } + return nil +} + +func endProvisioningOp(handle *pam.Handle, policy *actions.Policy) error { + if policy.NeedsRootToProvision() { + return handle.StartAsPamUser() + } + return nil +} + +// Set up the PAM user's keyring if needed by any encryption policies. +func setupUserKeyringIfNeeded(handle *pam.Handle, policies []*actions.Policy) error { + needed := false + for _, policy := range policies { + if policy.NeedsUserKeyring() { + needed = true + break + } + } + if !needed { + return nil + } + err := handle.StopAsPamUser() + if err != nil { + return err + } + _, err = keyring.UserKeyringID(handle.PamUser, true) + if err != nil { + log.Printf("Setting up keyrings in PAM: %v", err) + } + return handle.StartAsPamUser() +} + +// The Go runtime doesn't support being forked, as it is multithreaded but +// fork() deletes all threads except one. Some programs, such as xrdp, misuse +// libpam by fork()-ing the process between pam_authenticate() and +// pam_open_session(). Try to detect such unsupported cases and bail out early +// rather than deadlocking the Go runtime, which would prevent the user from +// logging in entirely. This isn't guaranteed to work, as we are already +// running Go code here, so we may have already deadlocked. But in practice the +// deadlock doesn't occur until hashing the login passphrase is attempted. +func isUnsupportedFork(handle *pam.Handle) bool { + pidString, err := handle.GetString(pidLabel) + if err != nil { + return false + } + expectedPid, err := strconv.Atoi(pidString) + if err != nil { + log.Printf("%s parse error: %v", pidLabel, err) + return false + } + if os.Getpid() == expectedPid { + return false + } + handle.InfoMessage(fmt.Sprintf("%s couldn't automatically unlock directories, see syslog", moduleName)) + if logger, err := syslog.New(syslog.LOG_WARNING, moduleName); err == nil { + fmt.Fprintf(logger, + "not unlocking directories because %s forked the process between authenticating the user and opening the session, which is incompatible with %s. See https://github.com/google/fscrypt/issues/350", + handle.GetServiceName(), moduleName) + logger.Close() + } + return true +} + // OpenSession provisions any policies protected with the login protector. func OpenSession(handle *pam.Handle, _ map[string]bool) error { - log.Print("OpenSession()") - // We will always clear the the AUTHTOK data + // We will always clear the AUTHTOK data defer handle.ClearData(authtokLabel) // Increment the count as we add a session if _, err := AdjustCount(handle, +1); err != nil { @@ -97,12 +193,20 @@ func OpenSession(handle *pam.Handle, _ map[string]bool) error { log.Printf("no protector to unlock: %s", err) return nil } - policies := policiesUsingProtector(protector) + policies := policiesUsingProtector(protector, false) if len(policies) == 0 { log.Print("no policies to unlock") return nil } + if isUnsupportedFork(handle) { + return nil + } + + if err = setupUserKeyringIfNeeded(handle, policies); err != nil { + return errors.Wrapf(err, "setting up user keyring") + } + log.Printf("unlocking %d policies protected with AUTHTOK", len(policies)) keyFn := func(_ actions.ProtectorInfo, retry bool) (*crypto.Key, error) { if retry { @@ -130,21 +234,25 @@ func OpenSession(handle *pam.Handle, _ map[string]bool) error { // We don't stop provisioning polices on error, we try all of them. for _, policy := range policies { - if policy.IsProvisioned() { - log.Printf("policy %s already provisioned", policy.Descriptor()) - continue - } if err := policy.UnlockWithProtector(protector); err != nil { log.Printf("unlocking policy %s: %s", policy.Descriptor(), err) continue } defer policy.Lock() - if err := policy.Provision(); err != nil { - log.Printf("provisioning policy %s: %s", policy.Descriptor(), err) + if err := beginProvisioningOp(handle, policy); err != nil { + return err + } + provisionErr := policy.Provision() + if err := endProvisioningOp(handle, policy); err != nil { + return err + } + if provisionErr != nil { + log.Printf("provisioning policy %s: %s", policy.Descriptor(), provisionErr) continue } - log.Printf("policy %s provisioned", policy.Descriptor()) + log.Printf("policy %s provisioned by %v", policy.Descriptor(), + handle.PamUser.Username) } return nil } @@ -152,36 +260,48 @@ func OpenSession(handle *pam.Handle, _ map[string]bool) error { // CloseSession can deprovision all keys provisioned at the start of the // session. It can also clear the cache so these changes take effect. func CloseSession(handle *pam.Handle, args map[string]bool) error { - log.Printf("CloseSession(%v)", args) // Only do stuff on session close when we are the last session if count, err := AdjustCount(handle, -1); err != nil || count != 0 { log.Printf("count is %d and we are not locking", count) return err } - var errLock, errCache error - // Don't automatically drop privileges, we may need them to drop caches. - if args[lockFlag] { - log.Print("locking polices protected with login protector") - errLock = lockLoginPolicies(handle) + if args[lockPoliciesFlag] { + log.Print("ignoring deprecated 'lock_policies' option (now the default)") } - if args[cacheFlag] { - log.Print("dropping appropriate filesystem caches at session close") - errCache = security.DropFilesystemCache() + if args[dropCachesFlag] { + log.Print("ignoring deprecated 'drop_caches' option (now auto-detected)") } - if errLock != nil { - return errLock + // Don't automatically drop privileges, since we may need them to + // deprovision policies or to drop caches. + + if !args[unlockOnlyFlag] { + log.Print("locking policies protected with login protector") + needDropCaches, errLock := lockLoginPolicies(handle) + + var errCache error + if needDropCaches { + log.Print("dropping appropriate filesystem caches at session close") + errCache = security.DropFilesystemCache() + } + if errLock != nil { + return errLock + } + return errCache } - return errCache + return nil } -// lockLoginPolicies deprovisions all policy keys that are protected by -// the user's login protector. -func lockLoginPolicies(handle *pam.Handle) error { +// lockLoginPolicies deprovisions all policy keys that are protected by the +// user's login protector. It returns true if dropping filesystem caches will +// be needed afterwards to completely lock the relevant directories. +func lockLoginPolicies(handle *pam.Handle) (bool, error) { + needDropCaches := false + if err := handle.StartAsPamUser(); err != nil { - return err + return needDropCaches, err } defer handle.StopAsPamUser() @@ -189,32 +309,49 @@ func lockLoginPolicies(handle *pam.Handle) error { protector, err := loginProtector(handle) if err != nil { log.Printf("nothing to lock: %s", err) - return nil + return needDropCaches, nil } - policies := policiesUsingProtector(protector) + policies := policiesUsingProtector(protector, true) if len(policies) == 0 { log.Print("no policies to lock") - return nil + return needDropCaches, nil + } + + if err = setupUserKeyringIfNeeded(handle, policies); err != nil { + return needDropCaches, errors.Wrapf(err, "setting up user keyring") } // We will try to deprovision all of the policies. for _, policy := range policies { - if !policy.IsProvisioned() { - log.Printf("policy %s not provisioned", policy.Descriptor()) - continue + if policy.NeedsUserKeyring() { + needDropCaches = true + } + if err := beginProvisioningOp(handle, policy); err != nil { + return needDropCaches, err } - if err := policy.Deprovision(); err != nil { - log.Printf("deprovisioning policy %s: %s", policy.Descriptor(), err) + deprovisionErr := policy.Deprovision(false) + if err := endProvisioningOp(handle, policy); err != nil { + return needDropCaches, err + } + if deprovisionErr != nil { + log.Printf("deprovisioning policy %s: %s", policy.Descriptor(), deprovisionErr) continue } - log.Printf("policy %s deprovisioned", policy.Descriptor()) + log.Printf("policy %s deprovisioned by %v", policy.Descriptor(), handle.PamUser.Username) } - return nil + return needDropCaches, nil } +var noOldAuthTokMessage string = ` +pam_fscrypt: cannot update login protector for '%s' because old passphrase +was not given. This is expected when changing a user's passphrase as root. +You'll need to manually update the protector's passphrase using: + + fscrypt metadata change-passphrase --protector=%s:%s +` + // Chauthtok rewraps the login protector when the passphrase changes. func Chauthtok(handle *pam.Handle, _ map[string]bool) error { - log.Print("Chauthtok()") if err := handle.StartAsPamUser(); err != nil { return err } @@ -235,6 +372,9 @@ func Chauthtok(handle *pam.Handle, _ map[string]bool) error { } authtok, err := handle.GetItem(pam.Oldauthtok) if err != nil { + handle.InfoMessage(fmt.Sprintf(noOldAuthTokMessage, + handle.PamUser.Username, + protector.Context.Mount.Path, protector.Descriptor())) return nil, errors.Wrap(err, "could not get OLDAUTHTOK") } return crypto.NewKeyFromCString(authtok) @@ -259,10 +399,11 @@ func Chauthtok(handle *pam.Handle, _ map[string]bool) error { //export pam_sm_authenticate func pam_sm_authenticate(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int { - return RunPamFunc(Authenticate, pamh, argc, argv) + return authenticateFunc.Run(pamh, argc, argv) } -// pam_sm_stecred needed because we use pam_sm_authenticate. +// pam_sm_setcred needed because we use pam_sm_authenticate. +// //export pam_sm_setcred func pam_sm_setcred(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int { return C.PAM_SUCCESS @@ -270,12 +411,12 @@ func pam_sm_setcred(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int //export pam_sm_open_session func pam_sm_open_session(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int { - return RunPamFunc(OpenSession, pamh, argc, argv) + return openSessionFunc.Run(pamh, argc, argv) } //export pam_sm_close_session func pam_sm_close_session(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int { - return RunPamFunc(CloseSession, pamh, argc, argv) + return closeSessionFunc.Run(pamh, argc, argv) } //export pam_sm_chauthtok @@ -284,8 +425,7 @@ func pam_sm_chauthtok(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.i if pam.Flag(flags)&pam.PrelimCheck != 0 { return C.PAM_SUCCESS } - - return RunPamFunc(Chauthtok, pamh, argc, argv) + return chauthtokFunc.Run(pamh, argc, argv) } // main() is needed to make a shared library compile diff --git a/pam_fscrypt/run_fscrypt.go b/pam_fscrypt/run_fscrypt.go index 6414d99..af9537f 100644 --- a/pam_fscrypt/run_fscrypt.go +++ b/pam_fscrypt/run_fscrypt.go @@ -31,10 +31,10 @@ import "C" import ( "fmt" "io" - "io/ioutil" "log" "log/syslog" "os" + "os/user" "path/filepath" "runtime/debug" "unsafe" @@ -57,13 +57,30 @@ const ( countDirectoryPermissions = 0700 countFilePermissions = 0600 countFileFormat = "%d\n" + // uidMin is the first UID that can be used for a regular user (as + // opposed to a system user or root). This value is fairly standard + // across Linux distros, but it can be adjusted if needed. + uidMin = 1000 ) -// PamFunc is used to define the various actions in the PAM module -type PamFunc func(handle *pam.Handle, args map[string]bool) error +// PamFunc is used to define the various actions in the PAM module. +type PamFunc struct { + // Name of the function being executed + name string + // Go implementation of this function + impl func(handle *pam.Handle, args map[string]bool) error +} + +// isSystemUser checks if a user is a system user. pam_fscrypt should never +// need to do anything for system users since they should never have login +// protectors. Therefore, we detect them early to avoid wasting resources. +func isSystemUser(user *user.User) bool { + uid := util.AtoiOrPanic(user.Uid) + return uid < uidMin && uid != 0 +} -// RunPamFunc is used to convert between the Go functions and exported C funcs. -func RunPamFunc(f PamFunc, pamh unsafe.Pointer, argc C.int, argv **C.char) (ret C.int) { +// Run is used to convert between the Go functions and exported C funcs. +func (f *PamFunc) Run(pamh unsafe.Pointer, argc C.int, argv **C.char) (ret C.int) { args := parseArgs(argc, argv) errorWriter := setupLogging(args) @@ -72,20 +89,27 @@ func RunPamFunc(f PamFunc, pamh unsafe.Pointer, argc C.int, argv **C.char) (ret if r := recover(); r != nil { ret = C.PAM_SERVICE_ERR fmt.Fprintf(errorWriter, - "pam func panicked: %s\nPlease open an issue.\n%s", - r, debug.Stack()) + "%s(%v) panicked: %s\nPlease open a bug.\n%s", + f.name, args, r, debug.Stack()) } }() + log.Printf("%s(%v) starting", f.name, args) handle, err := pam.NewHandle(pamh) if err == nil { - err = f(handle, args) + if isSystemUser(handle.PamUser) { + log.Printf("invoked for system user %q (%s), doing nothing", + handle.PamUser.Username, handle.PamUser.Uid) + err = nil + } else { + err = f.impl(handle, args) + } } if err != nil { - fmt.Fprintf(errorWriter, "pam func failed: %s", err) + fmt.Fprintf(errorWriter, "%s(%v) failed: %s", f.name, args, err) return C.PAM_SERVICE_ERR } - log.Print("pam func succeeded") + log.Printf("%s(%v) succeeded", f.name, args) return C.PAM_SUCCESS } @@ -107,7 +131,7 @@ func parseArgs(argc C.int, argv **C.char) map[string]bool { // syslog. func setupLogging(args map[string]bool) io.Writer { log.SetFlags(0) // Syslog already includes time data itself - log.SetOutput(ioutil.Discard) + log.SetOutput(io.Discard) if args[debugFlag] { debugWriter, err := syslog.New(syslog.LOG_DEBUG, moduleName) if err == nil { @@ -117,7 +141,7 @@ func setupLogging(args map[string]bool) io.Writer { errorWriter, err := syslog.New(syslog.LOG_ERR, moduleName) if err != nil { - return ioutil.Discard + return io.Discard } return errorWriter } @@ -126,10 +150,18 @@ func setupLogging(args map[string]bool) io.Writer { // one exists. This protector descriptor (if found) will be cached in the pam // data, under descriptorLabel. func loginProtector(handle *pam.Handle) (*actions.Protector, error) { - ctx, err := actions.NewContextFromMountpoint("/", handle.PamUser) + ctx, err := actions.NewContextFromMountpoint(actions.LoginProtectorMountpoint, + handle.PamUser) if err != nil { return nil, err } + // Ensure that pam_fscrypt only processes metadata files owned by the + // user or root, even if the user is root themselves. (Normally, when + // fscrypt is run as root it is allowed to process all metadata files. + // This implements stricter behavior for pam_fscrypt.) + if !ctx.Config.GetAllowCrossUserMetadata() { + ctx.TrustedUser = handle.PamUser + } // Find the user's PAM protector. options, err := ctx.ProtectorOptions() @@ -146,8 +178,10 @@ func loginProtector(handle *pam.Handle) (*actions.Protector, error) { } // policiesUsingProtector searches all the mountpoints for any policies -// protected with the specified protector. -func policiesUsingProtector(protector *actions.Protector) []*actions.Policy { +// protected with the specified protector. If provisioned is true, then only +// policies provisioned by the target user are returned; otherwise only policies +// *not* provisioned by the target user are returned. +func policiesUsingProtector(protector *actions.Protector, provisioned bool) []*actions.Policy { mounts, err := filesystem.AllFilesystems() if err != nil { log.Print(err) @@ -157,10 +191,14 @@ func policiesUsingProtector(protector *actions.Protector) []*actions.Policy { var policies []*actions.Policy for _, mount := range mounts { // Skip mountpoints that do not use the protector. - if _, _, err := mount.GetProtector(protector.Descriptor()); err != nil { + if _, _, err := mount.GetProtector(protector.Descriptor(), + protector.Context.TrustedUser); err != nil { + if _, ok := err.(*filesystem.ErrNotSetup); !ok { + log.Print(err) + } continue } - policyDescriptors, err := mount.ListPolicies() + policyDescriptors, err := mount.ListPolicies(protector.Context.TrustedUser) if err != nil { log.Printf("listing policies: %s", err) continue @@ -176,9 +214,23 @@ func policiesUsingProtector(protector *actions.Protector) []*actions.Policy { continue } - if policy.UsesProtector(protector) { - policies = append(policies, policy) + if !policy.UsesProtector(protector) { + continue + } + if provisioned { + if !policy.IsProvisionedByTargetUser() { + log.Printf("policy %s not provisioned by %v", + policy.Descriptor(), ctx.TargetUser.Username) + continue + } + } else { + if policy.IsProvisionedByTargetUser() { + log.Printf("policy %s already provisioned by %v", + policy.Descriptor(), ctx.TargetUser.Username) + continue + } } + policies = append(policies, policy) } } return policies @@ -186,7 +238,7 @@ func policiesUsingProtector(protector *actions.Protector) []*actions.Policy { // AdjustCount changes the session count for the pam user by the specified // amount. If the count file does not exist, create it as if it had a count of -// zero. If the adjustment would be the count below zero, the count is set to +// zero. If the adjustment would bring the count below zero, the count is set to // zero. The value of the new count is returned. Requires root privileges. func AdjustCount(handle *pam.Handle, delta int) (int, error) { // Make sure the directory exists @@ -199,7 +251,7 @@ func AdjustCount(handle *pam.Handle, delta int) (int, error) { if err != nil { return 0, err } - if err := unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil { + if err = unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil { return 0, err } defer file.Close() diff --git a/pam_fscrypt/run_test.go b/pam_fscrypt/run_test.go index 1e74528..40ace4c 100644 --- a/pam_fscrypt/run_test.go +++ b/pam_fscrypt/run_test.go @@ -1,5 +1,5 @@ /* - * run_test.go - tests that the PAM helper functionsd work properly + * run_test.go - tests that the PAM helper functions work properly * * Copyright 2017 Google Inc. * Author: Joe Richey (joerichey@google.com) |