mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 21:52:02 +08:00
Implement the client-side device authentication ceremony. Device authentication requires a previously registered and enrolled device. In exchange for solving a challenge, the ceremony allows the user to augment their certificates with device extensions, which are necessary to clear device-aware authentication. Wiring to `tsh` to be done in a follow-up PR. gravitational/teleport.e#514
139 lines
3.7 KiB
Go
139 lines
3.7 KiB
Go
// Copyright 2022 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 authn_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
|
|
"github.com/gravitational/teleport/lib/devicetrust/authn"
|
|
"github.com/gravitational/teleport/lib/devicetrust/testenv"
|
|
)
|
|
|
|
func TestRunCeremony(t *testing.T) {
|
|
env := testenv.MustNew()
|
|
defer env.Close()
|
|
t.Cleanup(resetNative())
|
|
|
|
devices := env.DevicesClient
|
|
ctx := context.Background()
|
|
|
|
// Create a fake device and enroll it, so the fake server has the necessary
|
|
// data to verify challenge signatures.
|
|
macOSDev1, err := testenv.NewFakeMacOSDevice()
|
|
require.NoError(t, err, "NewFakeMacOSDevice failed")
|
|
require.NoError(t, enrollDevice(ctx, devices, macOSDev1), "enrollDevice failed")
|
|
|
|
tests := []struct {
|
|
name string
|
|
dev *testenv.FakeMacOSDevice
|
|
certs *devicepb.UserCertificates
|
|
}{
|
|
{
|
|
name: "ok",
|
|
dev: macOSDev1,
|
|
certs: &devicepb.UserCertificates{
|
|
// SshAuthorizedKey is not parsed by the fake server.
|
|
SshAuthorizedKey: []byte("<a proper SSH certificate goes here>"),
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
*authn.GetDeviceCredential = func() (*devicepb.DeviceCredential, error) {
|
|
return test.dev.DeviceCredential(), nil
|
|
}
|
|
*authn.CollectDeviceData = test.dev.CollectDeviceData
|
|
*authn.SignChallenge = test.dev.SignChallenge
|
|
|
|
_, err := authn.RunCeremony(ctx, devices, test.certs)
|
|
// A nil error is good enough for this test.
|
|
assert.NoError(t, err, "RunCeremony failed")
|
|
})
|
|
}
|
|
}
|
|
|
|
func resetNative() func() {
|
|
gdc := authn.GetDeviceCredential
|
|
cdd := authn.CollectDeviceData
|
|
sc := authn.SignChallenge
|
|
return func() {
|
|
authn.GetDeviceCredential = gdc
|
|
authn.CollectDeviceData = cdd
|
|
authn.SignChallenge = sc
|
|
}
|
|
}
|
|
|
|
func enrollDevice(ctx context.Context, devices devicepb.DeviceTrustServiceClient, dev *testenv.FakeMacOSDevice) error {
|
|
stream, err := devices.EnrollDevice(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 1. Init.
|
|
cd, err := dev.CollectDeviceData()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := stream.Send(&devicepb.EnrollDeviceRequest{
|
|
Payload: &devicepb.EnrollDeviceRequest_Init{
|
|
Init: &devicepb.EnrollDeviceInit{
|
|
Token: "fake enroll token",
|
|
CredentialId: dev.ID,
|
|
DeviceData: cd,
|
|
Macos: &devicepb.MacOSEnrollPayload{
|
|
PublicKeyDer: dev.PubKeyDER,
|
|
},
|
|
},
|
|
},
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 2. Challenge.
|
|
resp, err := stream.Recv()
|
|
if err != nil {
|
|
return fmt.Errorf("challenge Recv: %w", err)
|
|
}
|
|
sig, err := dev.SignChallenge(resp.GetMacosChallenge().Challenge)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := stream.Send(&devicepb.EnrollDeviceRequest{
|
|
Payload: &devicepb.EnrollDeviceRequest_MacosChallengeResponse{
|
|
MacosChallengeResponse: &devicepb.MacOSEnrollChallengeResponse{
|
|
Signature: sig,
|
|
},
|
|
},
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 3. Success.
|
|
resp, err = stream.Recv()
|
|
switch {
|
|
case err != nil:
|
|
return fmt.Errorf("challenge response Recv: %w", err)
|
|
case resp.GetSuccess() == nil:
|
|
return fmt.Errorf("success response is nil, got %T instead", resp.Payload)
|
|
}
|
|
return nil
|
|
}
|