Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/devicetrust"
dtauthn "github.com/gravitational/teleport/lib/devicetrust/authn"
dtenroll "github.com/gravitational/teleport/lib/devicetrust/enroll"
"github.com/gravitational/teleport/lib/events"
kubeutils "github.com/gravitational/teleport/lib/kube/utils"
"github.com/gravitational/teleport/lib/modules"
Expand Down Expand Up @@ -936,6 +937,9 @@ func (c *Config) ResourceFilter(kind string) *proto.ListResourcesRequest {
// dtAuthnRunCeremonyFunc matches the signature of [dtauthn.RunCeremony].
type dtAuthnRunCeremonyFunc func(context.Context, devicepb.DeviceTrustServiceClient, *devicepb.UserCertificates) (*devicepb.UserCertificates, error)

// dtAutoEnrollFunc matches the signature of [dtenroll.AutoEnroll].
type dtAutoEnrollFunc func(context.Context, devicepb.DeviceTrustServiceClient) (*devicepb.Device, error)

// TeleportClient is a wrapper around SSH client with teleport specific
// workflow built in.
// TeleportClient is NOT safe for concurrent use.
Expand All @@ -955,14 +959,20 @@ type TeleportClient struct {
// TeleportClient NOT safe for concurrent use.
lastPing *webclient.PingResponse

// dtAttemptLoginIgnorePing allows tests to override AttemptDeviceLogin's Ping
// response validation.
dtAttemptLoginIgnorePing bool
// dtAttemptLoginIgnorePing and dtAutoEnrollIgnorePing allow Device Trust
// tests to ignore Ping responses.
// Useful to force flows that only typically happen on Teleport Enterprise.
dtAttemptLoginIgnorePing, dtAutoEnrollIgnorePing bool

// dtAuthnRunCeremony allows tests to override the default device
// authentication function.
// Defaults to [dtauthn.RunCeremony].
dtAuthnRunCeremony dtAuthnRunCeremonyFunc

// dtAutoEnroll allows tests to override the default device auto-enroll
// function.
// Defaults to [dtenroll.AutoEnroll].
dtAutoEnroll dtAutoEnrollFunc
}

// ShellCreatedCallback can be supplied for every teleport client. It will
Expand Down Expand Up @@ -3161,7 +3171,9 @@ func (tc *TeleportClient) WithoutJumpHosts(fn func(tcNoJump *TeleportClient) err
eventsCh: make(chan events.EventFields, 1024),
lastPing: tc.lastPing,
dtAttemptLoginIgnorePing: tc.dtAttemptLoginIgnorePing,
dtAutoEnrollIgnorePing: tc.dtAutoEnrollIgnorePing,
dtAuthnRunCeremony: tc.dtAuthnRunCeremony,
dtAutoEnroll: tc.dtAutoEnroll,
}
tcNoJump.JumpHosts = nil

Expand Down Expand Up @@ -3358,8 +3370,11 @@ func (tc *TeleportClient) AttemptDeviceLogin(ctx context.Context, key *Key) erro
}

// DeviceLogin attempts to authenticate the current device with Teleport.
//
// The device must be previously registered and enrolled for the authentication
// to succeed (see `tsh device enroll`).
// to succeed (see `tsh device enroll`). Alternatively, if the cluster supports
// auto-enrollment, then DeviceLogin will attempt to auto-enroll the device on
// certain failures and login again.
//
// DeviceLogin may fail for a variety of reasons, some of them legitimate
// (non-Enterprise cluster, Device Trust is disabled, etc). Because of that, a
Expand All @@ -3383,11 +3398,36 @@ func (tc *TeleportClient) DeviceLogin(ctx context.Context, certs *devicepb.UserC
runCeremony = dtauthn.RunCeremony
}

newCerts, err := runCeremony(ctx, authClient.DevicesClient(), certs)
// Login without a previous auto-enroll attempt.
devicesClient := authClient.DevicesClient()
newCerts, loginErr := runCeremony(ctx, devicesClient, certs)
// Success or auto-enroll impossible.
if loginErr == nil || errors.Is(loginErr, devicetrust.ErrPlatformNotSupported) || trace.IsNotImplemented(loginErr) {
return newCerts, trace.Wrap(loginErr)
}

// Is auto-enroll enabled?
pingResp, err := tc.Ping(ctx)
if err != nil {
return nil, trace.Wrap(err)
log.WithError(err).Debug("Device Trust: swallowing Ping error for previous Login error")
return nil, trace.Wrap(loginErr) // err swallowed for loginErr
}
if !tc.dtAutoEnrollIgnorePing && !pingResp.Auth.DeviceTrust.AutoEnroll {
return nil, trace.Wrap(loginErr) // err swallowed for loginErr
}

autoEnroll := tc.dtAutoEnroll
if autoEnroll == nil {
autoEnroll = dtenroll.AutoEnroll
}

// Auto-enroll and Login again.
if _, err := autoEnroll(ctx, devicesClient); err != nil {
log.WithError(err).Debug("Device Trust: device auto-enroll failed")
return nil, trace.Wrap(loginErr) // err swallowed for loginErr
}
return newCerts, nil
newCerts, err = runCeremony(ctx, devicesClient, certs)
return newCerts, trace.Wrap(err)
}

// getSSHLoginFunc returns an SSHLoginFunc that matches client and cluster settings.
Expand Down
34 changes: 34 additions & 0 deletions lib/client/api_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,40 @@ func TestTeleportClient_DeviceLogin(t *testing.T) {
"AttemptDeviceLogin failed")
assert.False(t, runCeremonyCalled, "AttemptDeviceLogin called DeviceLogin/dtAuthnRunCeremony, despite the Ping response")
})

t.Run("device auto-enroll", func(t *testing.T) {
// Setup:
// - Ignore ping value for auto-enroll (only Enterprise can truly enable
// auto-enroll)
// - RunCeremony only succeeds after AutoEnroll is called (simulate an
// unenrolled device)
var enrolled bool
var runCeremonyCalls, autoEnrollCalls int
teleportClient.SetDTAutoEnrollIgnorePing(true)
teleportClient.SetDTAuthnRunCeremony(func(_ context.Context, _ devicepb.DeviceTrustServiceClient, _ *devicepb.UserCertificates) (*devicepb.UserCertificates, error) {
runCeremonyCalls++
if !enrolled {
return nil, errors.New("device not enrolled")
}
return validCerts, nil
})
teleportClient.SetDTAutoEnroll(func(_ context.Context, _ devicepb.DeviceTrustServiceClient) (*devicepb.Device, error) {
autoEnrollCalls++
enrolled = true
return &devicepb.Device{
Id: "mydevice",
}, nil
})

// Test!
got, err := teleportClient.DeviceLogin(ctx, &devicepb.UserCertificates{
SshAuthorizedKey: key.Cert,
})
require.NoError(t, err, "DeviceLogin failed")
assert.Equal(t, got, validCerts, "DeviceLogin mismatch")
assert.Equal(t, 2, runCeremonyCalls, "RunCeremony called an unexpected number of times")
assert.Equal(t, 1, autoEnrollCalls, "AutoEnroll called an unexpected number of times")
})
}

type standaloneBundle struct {
Expand Down
8 changes: 8 additions & 0 deletions lib/client/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ func (tc *TeleportClient) SetDTAttemptLoginIgnorePing(val bool) {
tc.dtAttemptLoginIgnorePing = val
}

func (tc *TeleportClient) SetDTAutoEnrollIgnorePing(val bool) {
tc.dtAutoEnrollIgnorePing = val
}

func (tc *TeleportClient) SetDTAuthnRunCeremony(fn dtAuthnRunCeremonyFunc) {
tc.dtAuthnRunCeremony = fn
}

func (tc *TeleportClient) SetDTAutoEnroll(fn dtAutoEnrollFunc) {
tc.dtAutoEnroll = fn
}
43 changes: 43 additions & 0 deletions lib/devicetrust/enroll/auto_enroll.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package enroll

import (
"context"

"github.com/gravitational/trace"

devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
)

// AutoEnroll attempts to create an auto-enroll token via
// [devicepb.DeviceTrustServiceClient.CreateDeviceEnrollToken] and enrolls the
// device by calling [RunCeremony].
func AutoEnroll(ctx context.Context, devicesClient devicepb.DeviceTrustServiceClient) (*devicepb.Device, error) {
cd, err := collectDeviceData()
if err != nil {
return nil, trace.Wrap(err, "collecting device data")
}

token, err := devicesClient.CreateDeviceEnrollToken(ctx, &devicepb.CreateDeviceEnrollTokenRequest{
DeviceData: cd,
})
if err != nil {
return nil, trace.Wrap(err, "creating auto-token")
}

dev, err := RunCeremony(ctx, devicesClient, token.Token)
return dev, trace.Wrap(err)
}
60 changes: 60 additions & 0 deletions lib/devicetrust/enroll/auto_enroll_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package enroll_test

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/devicetrust/enroll"
"github.com/gravitational/teleport/lib/devicetrust/testenv"
)

func TestAutoEnroll(t *testing.T) {
env := testenv.MustNew()
defer env.Close()
t.Cleanup(resetNative())

devices := env.DevicesClient
ctx := context.Background()

macOSDev1, err := testenv.NewFakeMacOSDevice()
require.NoError(t, err, "NewFakeMacOSDevice failed")

tests := []struct {
name string
dev fakeDevice
}{
{
name: "macOS device",
dev: macOSDev1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
*enroll.GetOSType = test.dev.GetOSType
*enroll.CollectDeviceData = test.dev.CollectDeviceData
*enroll.EnrollInit = test.dev.EnrollDeviceInit
*enroll.SignChallenge = test.dev.SignChallenge

dev, err := enroll.AutoEnroll(ctx, devices)
require.NoError(t, err, "AutoEnroll failed")
assert.NotNil(t, dev, "AutoEnroll returned nil device")
})
}
}
8 changes: 0 additions & 8 deletions lib/devicetrust/enroll/enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,6 @@ import (

devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
"github.com/gravitational/teleport/lib/devicetrust"
"github.com/gravitational/teleport/lib/devicetrust/native"
)

// vars below are used to fake OSes and switch implementations for tests.
var (
getOSType = getDeviceOSType
enrollInit = native.EnrollDeviceInit
signChallenge = native.SignChallenge
)

// RunCeremony performs the client-side device enrollment ceremony.
Expand Down
11 changes: 7 additions & 4 deletions lib/devicetrust/enroll/enroll_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestRunCeremony(t *testing.T) {
*enroll.SignChallenge = test.dev.SignChallenge

got, err := enroll.RunCeremony(ctx, devices, "faketoken")
assert.NoError(t, err, "RunCeremony failed")
require.NoError(t, err, "RunCeremony failed")
assert.NotNil(t, got, "RunCeremony returned nil device")
})
}
Expand All @@ -67,19 +67,22 @@ func resetNative() func() {
}
os.Setenv(guardKey, "1")

getOSType := *enroll.GetOSType
collectDeviceData := *enroll.CollectDeviceData
enrollDeviceInit := *enroll.EnrollInit
getOSType := *enroll.GetOSType
signChallenge := *enroll.SignChallenge
return func() {
*enroll.GetOSType = getOSType
*enroll.CollectDeviceData = collectDeviceData
*enroll.EnrollInit = enrollDeviceInit
*enroll.GetOSType = getOSType
*enroll.SignChallenge = signChallenge
os.Unsetenv(guardKey)
}
}

type fakeDevice interface {
GetOSType() devicepb.OSType
CollectDeviceData() (*devicepb.DeviceCollectedData, error)
EnrollDeviceInit() (*devicepb.EnrollDeviceInit, error)
GetOSType() devicepb.OSType
SignChallenge(chal []byte) (sig []byte, err error)
}
9 changes: 6 additions & 3 deletions lib/devicetrust/enroll/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

package enroll

var GetOSType = &getOSType
var EnrollInit = &enrollInit
var SignChallenge = &signChallenge
var (
CollectDeviceData = &collectDeviceData
EnrollInit = &enrollInit
GetOSType = &getOSType
SignChallenge = &signChallenge
)
25 changes: 25 additions & 0 deletions lib/devicetrust/enroll/native_shim.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package enroll

import "github.com/gravitational/teleport/lib/devicetrust/native"

// vars below are used to fake OSes and switch implementations for tests.
var (
collectDeviceData = native.CollectDeviceData
enrollInit = native.EnrollDeviceInit
getOSType = getDeviceOSType
signChallenge = native.SignChallenge
)
21 changes: 21 additions & 0 deletions lib/devicetrust/testenv/fake_device_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package testenv

import (
"context"
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
Expand Down Expand Up @@ -44,6 +45,26 @@ func newFakeDeviceService() *fakeDeviceService {
return &fakeDeviceService{}
}

// CreateDeviceEnrollToken implements the creation of fake device enrollment
// tokens.
//
// Only auto-enrollment is supported by the fake.
//
// Neither the device or token are stored, as the fake EnrollDevice doesn't
// verify tokens.
func (s *fakeDeviceService) CreateDeviceEnrollToken(ctx context.Context, req *devicepb.CreateDeviceEnrollTokenRequest) (*devicepb.DeviceEnrollToken, error) {
if req.DeviceId != "" {
return nil, trace.AccessDenied("device ID token issuance not supported")
}
if err := validateCollectedData(req.DeviceData); err != nil {
return nil, trace.AccessDenied(err.Error())
}

return &devicepb.DeviceEnrollToken{
Token: "fakedeviceenrolltoken",
}, nil
}

// EnrollDevice implements a fake, server-side device enrollment ceremony.
//
// As long as all required fields are non-nil and the challenge signature
Expand Down