From 72aeb0648247c68fb4a8a0979c9062ba253b3ecf Mon Sep 17 00:00:00 2001 From: Joshua Blum Date: Mon, 27 Jul 2026 12:30:36 -0400 Subject: [PATCH] Avoid owner device lookup during provisioning --- go/engine/login_provision.go | 18 ++++------- go/engine/login_test.go | 4 +++ go/engine/paperprovision.go | 13 ++------ go/engine/selfprovision.go | 2 +- go/libkb/active_device.go | 60 +++++++++++++++--------------------- go/libkb/device_with_keys.go | 41 +++++++++++------------- 6 files changed, 56 insertions(+), 82 deletions(-) diff --git a/go/engine/login_provision.go b/go/engine/login_provision.go index 02612c3acb1e..a2eb62390e67 100644 --- a/go/engine/login_provision.go +++ b/go/engine/login_provision.go @@ -374,9 +374,9 @@ func (e *loginProvision) getValidPaperKeyOnce(m libkb.MetaContext, i int, lastEr return nil, err } - // use the KID to find the uid, deviceID and deviceName - var uid keybase1.UID - uid, err = keys.Populate(m) + // Use the KID to find the device in the user we force-reloaded before + // entering provisioning. + err = keys.PopulateFromUser(e.arg.User) if err != nil { m.Debug("getValidPaperKeyOnce attempt %d (%s): %s", i, prefix, err) @@ -391,10 +391,6 @@ func (e *loginProvision) getValidPaperKeyOnce(m libkb.MetaContext, i int, lastEr return nil, err } - if uid.NotEqual(e.arg.User.GetUID()) { - return nil, paperKeyNotFound - } - // found a paper key that can be used for signing m.Debug("found paper key (%s) match for %s", prefix, e.arg.User.GetName()) return keys, nil @@ -872,8 +868,9 @@ func (e *loginProvision) preloadedPaperKey(m libkb.MetaContext, devices []libkb. return libkb.NoPaperKeysError{} } - // use the KID to find the uid, deviceID and deviceName - uid, err := keys.Populate(m) + // Resolve the exact KID against the user we force-reloaded before entering + // provisioning. The prefix match above is only a UI convenience. + err = keys.PopulateFromUser(e.arg.User) if err != nil { switch err := err.(type) { case libkb.NotFoundError: @@ -885,9 +882,6 @@ func (e *loginProvision) preloadedPaperKey(m libkb.MetaContext, devices []libkb. } return err } - if uid.NotEqual(e.arg.User.GetUID()) { - return paperKeyNotFound - } return e.paper(m, matchedDevice, keys) } diff --git a/go/engine/login_test.go b/go/engine/login_test.go index c66486af2124..06ff1beb92a7 100644 --- a/go/engine/login_test.go +++ b/go/engine/login_test.go @@ -1314,6 +1314,10 @@ func TestProvisionPaperCommandLine(t *testing.T) { err = AssertProvisioned(tc2) require.NoError(t, err) + deviceCtime, err := tc2.G.ActiveDevice.Ctime(m) + require.NoError(t, err) + require.NotZero(t, deviceCtime) + require.Equal(t, provUI.calledChooseDeviceType, 0) require.Equal(t, provLoginUI.CalledGetEmailOrUsername, 0) } diff --git a/go/engine/paperprovision.go b/go/engine/paperprovision.go index e1ded26ba073..31e675c0c62b 100644 --- a/go/engine/paperprovision.go +++ b/go/engine/paperprovision.go @@ -5,7 +5,6 @@ package engine import ( "errors" - "fmt" "github.com/keybase/client/go/libkb" keybase1 "github.com/keybase/client/go/protocol/keybase1" @@ -97,18 +96,12 @@ func (e *PaperProvisionEngine) Run(m libkb.MetaContext) (err error) { keys := bkeng.DeviceWithKeys() - // Make sure the key matches the logged in user - // use the KID to find the uid - uid, err := keys.Populate(m) - if err != nil { + // Resolve the exact KID against the user loaded for this provisioning + // attempt. + if err := keys.PopulateFromUser(e.User); err != nil { return err } - if uid.NotEqual(e.User.GetUID()) { - e.G().Log.Debug("paper key entered was for a different user") - return fmt.Errorf("paper key valid, but for %s, not %s", uid, e.User.GetUID()) - } - e.perUserKeyring, err = libkb.NewPerUserKeyring(e.G(), e.User.GetUID()) if err != nil { return err diff --git a/go/engine/selfprovision.go b/go/engine/selfprovision.go index d9735af0ee6f..b014c16b5f31 100644 --- a/go/engine/selfprovision.go +++ b/go/engine/selfprovision.go @@ -146,7 +146,7 @@ func (e *SelfProvisionEngine) loadUserAndActiveDeviceKeys(m libkb.MetaContext) ( if err != nil { return nil, err } - if _, err := keys.Populate(m); err != nil { + if err := keys.PopulateFromUser(e.User); err != nil { return nil, err } diff --git a/go/libkb/active_device.go b/go/libkb/active_device.go index a56797ad377e..18f66dcd6b97 100644 --- a/go/libkb/active_device.go +++ b/go/libkb/active_device.go @@ -77,6 +77,7 @@ func NewActiveDeviceWithDeviceWithKeys(m MetaContext, uv keybase1.UserVersion, d uv: uv, deviceID: d.deviceID, deviceName: d.deviceName, + deviceCtime: d.deviceCtime, signingKey: d.signingKey, encryptionKey: d.encryptionKey, nistFactory: NewNISTFactory(m.G(), uv.Uid, d.deviceID, d.signingKey), @@ -446,63 +447,50 @@ func (a *ActiveDevice) valid() bool { } func (a *ActiveDevice) Ctime(m MetaContext) (keybase1.Time, error) { - // make sure the device id doesn't change throughout this function - deviceID := a.DeviceID() - - // check if we have a cached ctime already - ctime, err := a.ctimeCached(deviceID) - if err != nil { - return 0, err + uv, deviceID, ctime := a.ctimeInfo() + if uv.IsNil() || deviceID.IsNil() { + return 0, errors.New("active device is not valid") } if ctime > 0 { return ctime, nil } - // need to build a device and ask the server for ctimes - decKeys, err := a.deviceKeys(deviceID) + // Resolve the device through the validated UPAK cache. This retries with a + // forced poll if the device is missing, which handles newly provisioned + // devices without relying on the unauthenticated key/owner/device endpoint. + upak, err := m.G().GetUPAKLoader().LoadUPAKWithDeviceID(m.Ctx(), uv.Uid, deviceID) if err != nil { return 0, err } - // Note: decKeys.Populate() makes a network API call - if _, err := decKeys.Populate(m); err != nil { - return 0, nil + if !upak.Current.ToUserVersion().Eq(uv) { + return 0, NewUIDMismatchError("active user changed during ctime lookup") + } + device := upak.Current.FindSigningDeviceKey(deviceID) + if device == nil { + return 0, NoKeyError{"no signing device key found for user"} + } + if device.Base.Revocation != nil { + return 0, NewKeyRevokedError("active device") + } + if device.Base.CTime <= 0 { + return 0, NotFoundError{Msg: "device ctime not found"} } // set the ctime value under a write lock a.Lock() defer a.Unlock() - if !a.deviceID.Eq(deviceID) { + if !a.uv.Eq(uv) || !a.deviceID.Eq(deviceID) { return 0, errors.New("active device changed during ctime lookup") } - a.deviceCtime = decKeys.DeviceCtime() + a.deviceCtime = device.Base.CTime return a.deviceCtime, nil } -func (a *ActiveDevice) ctimeCached(deviceID keybase1.DeviceID) (keybase1.Time, error) { +func (a *ActiveDevice) ctimeInfo() (keybase1.UserVersion, keybase1.DeviceID, keybase1.Time) { a.RLock() defer a.RUnlock() - - if !a.deviceID.Eq(deviceID) { - return 0, errors.New("active device changed during ctime lookup") - } - - return a.deviceCtime, nil -} - -func (a *ActiveDevice) deviceKeys(deviceID keybase1.DeviceID) (*DeviceWithKeys, error) { - a.RLock() - defer a.RUnlock() - - if !a.valid() { - return nil, errors.New("active device is not valid") - } - - if !a.deviceID.Eq(deviceID) { - return nil, errors.New("active device changed") - } - - return NewDeviceWithKeysOnly(a.signingKey, a.encryptionKey, a.keychainMode), nil + return a.uv, a.deviceID, a.deviceCtime } func (a *ActiveDevice) DeviceKeys() (*DeviceWithKeys, error) { diff --git a/go/libkb/device_with_keys.go b/go/libkb/device_with_keys.go index acb21ddfc02b..db07f433e0e7 100644 --- a/go/libkb/device_with_keys.go +++ b/go/libkb/device_with_keys.go @@ -111,32 +111,27 @@ func (s *SelfDestructingDeviceWithKeys) DeviceWithKeys() *DeviceWithKeys { return &ret } -type ownerDeviceReply struct { - Status AppStatus `json:"status"` - UID keybase1.UID `json:"uid"` - DeviceID keybase1.DeviceID `json:"device_id"` - DeviceName string `json:"device_name"` - DeviceCtime keybase1.Time `json:"device_ctime"` -} - -func (o *ownerDeviceReply) GetAppStatus() *AppStatus { - return &o.Status -} +// PopulateFromUser fills device metadata from an already-loaded and verified +// user sigchain. +func (d *DeviceWithKeys) PopulateFromUser(u *User) error { + if u == nil || d.signingKey == nil { + return NotFoundError{Msg: "KID not found"} + } -func (d *DeviceWithKeys) Populate(m MetaContext) (uid keybase1.UID, err error) { - arg := APIArg{ - Endpoint: "key/owner/device", - SessionType: APISessionTypeNONE, - Args: HTTPArgs{"kid": S{Val: d.signingKey.GetKID().String()}}, + ckf := u.GetComputedKeyFamily() + if ckf == nil { + return NotFoundError{Msg: "KID not found"} } - var res ownerDeviceReply - if err = m.G().API.GetDecode(m, arg, &res); err != nil { - return uid, err + + device, err := ckf.GetDeviceForKID(d.signingKey.GetKID()) + if err != nil || device == nil || !device.IsActive() || device.Description == nil { + return NotFoundError{Msg: "KID not found"} } - d.deviceID = res.DeviceID - d.deviceName = res.DeviceName - d.deviceCtime = res.DeviceCtime - return res.UID, nil + + d.deviceID = device.ID + d.deviceName = *device.Description + d.deviceCtime = device.CTime + return nil } func (d *DeviceWithKeys) ToProvisioningKeyActiveDevice(m MetaContext, uv keybase1.UserVersion) *ActiveDevice {