Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate go-configfs-tsm library into go-tdx-guest to enable attestation quote fetch via ConfigFS #30

Merged
merged 3 commits into from
Dec 21, 2023
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
42 changes: 41 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ type Device interface {
Ioctl(command uintptr, argument any) (uintptr, error)
}

// QuoteProvider encapsulates calls to attestation quote.
type QuoteProvider interface {
IsSupported() error
GetRawQuote(reportData [64]byte) ([]uint8, error)
}

// UseDefaultTdxGuestDevice returns true if tdxGuestPath=default.
func UseDefaultTdxGuestDevice() bool {
return *tdxGuestPath == "default"
Expand Down Expand Up @@ -102,9 +108,43 @@ func GetQuote(d Device, reportData [64]byte) (*pb.QuoteV4, error) {
if len(quotebytes) > int(size) {
quotebytes = quotebytes[:size]
}
quote, err := abi.QuoteToProto(quotebytes)
return convertRawQuoteToProto(quotebytes)
}

// GetRawQuoteViaProvider use QuoteProvider to fetch quote in byte array format.
func GetRawQuoteViaProvider(qp QuoteProvider, reportData [64]byte) ([]uint8, error) {
if err := qp.IsSupported(); err == nil {
return qp.GetRawQuote(reportData)
}
return fallbackToDeviceForRawQuote(reportData)
}

// GetQuoteViaProvider use QuoteProvider to fetch attestation quote.
func GetQuoteViaProvider(qp QuoteProvider, reportData [64]byte) (*pb.QuoteV4, error) {
bytes, err := GetRawQuoteViaProvider(qp, reportData)
if err != nil {
return nil, err
}
return convertRawQuoteToProto(bytes)
}

// convertRawQuoteToProto converts raw quote in byte array format to proto.
func convertRawQuoteToProto(rawQuote []byte) (*pb.QuoteV4, error) {
quote, err := abi.QuoteToProto(rawQuote)
if err != nil {
return nil, err
}
return quote, nil
}

// fallbackToDeviceForRawQuote opens tdx_guest device to fetch raw quote.
func fallbackToDeviceForRawQuote(reportData [64]byte) ([]uint8, error) {
// Fall back to TDX device driver.
device, err := OpenDevice()
if err != nil {
return nil, fmt.Errorf("neither tdx device, nor configFs is available to fetch attestation quote")
}
bytes, _, err := GetRawQuote(device, reportData)
device.Close()
return bytes, err
}
36 changes: 34 additions & 2 deletions client/client_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"unsafe"

labi "github.com/google/go-tdx-guest/client/linuxabi"
"github.com/google/go-tdx-guest/external/go_configfs_tsm/configfs/linuxtsm"
"github.com/google/go-tdx-guest/external/go_configfs_tsm/report"
"golang.org/x/sys/unix"
)

Expand Down Expand Up @@ -83,8 +85,9 @@ func (d *LinuxDevice) Ioctl(command uintptr, req any) (uintptr, error) {
abi.Finish(sreq)
if errno == unix.EBUSY {
return 0, errno
}
if errno != 0 {
} else if errno == unix.ENOTTY {
return 0, fmt.Errorf("invalid ioctl! use QuoteProvider to fetch attestation quote")
} else if errno != 0 {
return 0, errno
}
return result, nil
Expand All @@ -97,3 +100,32 @@ func (d *LinuxDevice) Ioctl(command uintptr, req any) (uintptr, error) {
}
return 0, fmt.Errorf("unexpected request value: %v", req)
}

// LinuxConfigFsQuoteProvider implements the QuoteProvider interface to fetch
// attestation quote via ConfigFS.
type LinuxConfigFsQuoteProvider struct{}

// IsSupported checks if TSM client can be created to use ConfigFS system.
func (p *LinuxConfigFsQuoteProvider) IsSupported() error {
_, err := linuxtsm.MakeClient()
return err
}

// GetRawQuote returns byte format attestation quote via ConfigFS.
func (p *LinuxConfigFsQuoteProvider) GetRawQuote(reportData [64]byte) ([]uint8, error) {
req := &report.Request{
InBlob: reportData[:],
GetAuxBlob: false,
}
resp, err := linuxtsm.GetReport(req)
if err != nil {
return nil, err
}
tdReport := resp.OutBlob
return tdReport, nil
}

// GetQuoteProvider returns an instance of LinuxConfigFsQuoteProvider.
func GetQuoteProvider() (*LinuxConfigFsQuoteProvider, error) {
return &LinuxConfigFsQuoteProvider{}, nil
}
18 changes: 18 additions & 0 deletions client/client_macos.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,21 @@ func (*MacOSDevice) Close() error {
func (*MacOSDevice) Ioctl(_ uintptr, _ any) (uintptr, error) {
return 0, fmt.Errorf("MacOS is unsupported")
}

// MacOsConfigFsQuoteProvider implements the QuoteProvider interface to fetch attestation quote via ConfigFS.
type MacOsConfigFsQuoteProvider struct{}

// IsSupported is not supported on MacOS.
func (p *MacOsConfigFsQuoteProvider) IsSupported() error {
return fmt.Errorf("MacOS is unsupported")
}

// GetRawQuote is not supported on MacOS.
func (p *MacOsConfigFsQuoteProvider) GetRawQuote(reportData [64]byte) ([]uint8, error) {
return nil, fmt.Errorf("MacOS is unsupported")
}

// GetQuoteProvider is not supported on MacOS.
func GetQuoteProvider() (*MacOsConfigFsQuoteProvider, error) {
return nil, fmt.Errorf("MacOS is unsupported")
}
53 changes: 48 additions & 5 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,24 @@ import (

var devMu sync.Once
var device Device
var quoteProvider QuoteProvider
var tests []test.TestCase

const defaultTDXDevicePath = "/dev/tdx-guest"

func initDevice() {
func initialize() {
for _, tc := range test.TestCases() {
// Don't test faked errors when running real hardware tests.
if !UseDefaultTdxGuestDevice() && tc.WantErr != "" {
continue
}
tests = append(tests, tc)
}
tdxTestQuoteProvider, err := test.TcQuoteProvider(tests)
if err != nil {
panic(fmt.Sprintf("failed to create test quote provider: %v", err))
}
quoteProvider = tdxTestQuoteProvider
// Choose a mock device or a real device depending on the --tdx_guest_device_path flag.
if UseDefaultTdxGuestDevice() {
tdxTestDevice, err := test.TcDevice(tests)
Expand All @@ -58,7 +64,7 @@ func initDevice() {
device = client
}
func TestGetReport(t *testing.T) {
devMu.Do(initDevice)
devMu.Do(initialize)
for _, tc := range test.TestCases() {
t.Run(tc.Name, func(t *testing.T) {
got, err := getReport(device, tc.Input)
Expand All @@ -75,7 +81,7 @@ func TestGetReport(t *testing.T) {
}
}
func TestGetRawQuote(t *testing.T) {
devMu.Do(initDevice)
devMu.Do(initialize)
for _, tc := range test.TestCases() {
t.Run(tc.Name, func(t *testing.T) {
got, _, err := GetRawQuote(device, tc.Input)
Expand All @@ -92,7 +98,7 @@ func TestGetRawQuote(t *testing.T) {
}
}
func TestGetQuote(t *testing.T) {
devMu.Do(initDevice)
devMu.Do(initialize)
for _, tc := range test.TestCases() {
t.Run(tc.Name, func(t *testing.T) {
got, err := GetQuote(device, tc.Input)
Expand All @@ -105,7 +111,44 @@ func TestGetQuote(t *testing.T) {
t.Error(err)
}
if diff := cmp.Diff(got, quote, protocmp.Transform()); diff != "" {
t.Errorf("difference in quote: %s", diff)
t.Errorf("Difference in quote: %s", diff)
}
}
})
}
}
func TestGetRawQuoteViaProvider(t *testing.T) {
devMu.Do(initialize)
for _, tc := range test.TestCases() {
t.Run(tc.Name, func(t *testing.T) {
got, err := GetRawQuoteViaProvider(quoteProvider, tc.Input)
if !test.Match(err, tc.WantErr) {
t.Fatalf("GetRawQuoteViaProvider(quoteProvider, %v) = %v, %v. Want err: %q", tc.Input, got, err, tc.WantErr)
}
if tc.WantErr == "" {
want := tc.Quote
if !bytes.Equal(got, want) {
t.Errorf("GetRawQuoteViaProvider(quoteProvider, %v) = %v want %v", tc.Input, got, want)
}
}
})
}
}
func TestGetQuoteViaProvider(t *testing.T) {
devMu.Do(initialize)
for _, tc := range test.TestCases() {
t.Run(tc.Name, func(t *testing.T) {
got, err := GetQuoteViaProvider(quoteProvider, tc.Input)
if !test.Match(err, tc.WantErr) {
t.Fatalf("Expected %v got err: %v", err, tc.WantErr)
}
if tc.WantErr == "" {
quote, err := abi.QuoteToProto(tc.Quote)
if err != nil {
t.Error(err)
}
if diff := cmp.Diff(got, quote, protocmp.Transform()); diff != "" {
t.Errorf("Difference in quote: %s", diff)
}
}
})
Expand Down
18 changes: 18 additions & 0 deletions client/client_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,21 @@ func (*WindowsDevice) Ioctl(_ uintptr, _ any) (uintptr, error) {
// The GuestAttestation library on Windows is closed source.
return 0, fmt.Errorf("Windows is unsupported")
}

// WindowsConfigFsQuoteProvider implements the QuoteProvider interface to fetch attestation quote via ConfigFS.
type WindowsConfigFsQuoteProvider struct{}

// IsSupported is not supported on Windows.
func (p *WindowsConfigFsQuoteProvider) IsSupported() error {
return fmt.Errorf("Windows is unsupported")
}

// GetRawQuote is not supported on Windows.
func (p *WindowsConfigFsQuoteProvider) GetRawQuote(reportData [64]byte) ([]uint8, error) {
return nil, fmt.Errorf("Windows is unsupported")
}

// GetQuoteProvider is not supported on Windows.
func GetQuoteProvider() (*WindowsConfigFsQuoteProvider, error) {
return nil, fmt.Errorf("Windows is unsupported")
}
131 changes: 131 additions & 0 deletions external/go_configfs_tsm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# go-configfs-tsm

This library wraps the configfs/tsm Linux subsystem for Trusted Security Module operations.

Please note that this is a temporary library added for now. NOT FOR EXTERNAL USE.

## `report` library

This library wraps the configfs/tsm/report subsystem for safely generating
attestation reports.

The TSM `report` subsystem provides a vendor-agnostic interface for collecting a
signed document for the Trusted Execution Environment's (TEE) state for remote
verification. For simplicity, we call this document an "attestation report",
though other sources may sometimes refer to it as a "quote".

Signing keys are expected to be rooted back to the manufacturer. Certificates
may be present in the `auxblob` attribute or as part of the report in `outblob`.

The core functionality of attestation report interaction is nonce in, report
out. For testability, we abstract the file operations that are needed for
creating configfs report entries, reading and writing attributes, and final
reclaiming of resources.

```golang
func Get(client configfsi.Client, req *report.Request) (*report.Response, error)
```

Where

```golang
type Request struct {
InBlob []byte
Privilege *Privilege
GetAuxBlob bool
}


type Response struct {
Provider string
OutBlob []byte
AuxBlob []byte
}

type Privilege struct {
Level int
}
```

The provider may not implement an `AuxBlob` delivery mechanism, so if
`GetAuxBlob` is true, then `AuxBlob` still must be checked for length 0.

### Errors

Since this is a file-based system, there's always a chance that an operation may
fail with a permission error. By default, the TSM system requires root access.

The host may also add rate limiting to requests, such that an outblob read fails
with `EBUSY`. The kernel may or may not try again on behalf of the user.

Finally, due to the fact that the TSM report system only requests an attestation
report when reading `outblob` or `auxblob`, there is a chance the input
attributes may have been changed to unexpected values from an interfering
process. This interference is a bug in user space that the kernel does not block
for simplicity. Interference is evident through the `generation` attribute. When
`generation` does not match the expectations that the `report` package tracks,
`report.Get` returns a `*report.GenerationErr` or an error that wraps
`*report.GenerationErr`.

Use `func GetGenerationErr(error) *GenerationErr` to extract a `*GenerationErr`
from an error if it is or contains a `*GenerationErr`. If present, the caller
should try to identify the source of interference and remove it. Meanwhile, the
caller may try again.

## `configfsi.Client` interface

Most users will only want to use the client from `linuxtsm.MakeClient`.

A client on real hardware is just the filesystem, since the configfs
interactions will interact with the hardware. In unit tests though, we can
emulate the behavior that has been proposed in v7 of the patch series

```golang
type Client interface {
MkdirTemp(dir, pattern string) (string, error)
ReadFile(name string) ([]byte, error)
WriteFile(name string, contents []byte) error
RemoveAll(path string) error
}
```

The `RemoveAll` function is the only oddly named method, since the real
interface would just `rmdir` the report directory
([`os.Remove`](https://pkg.go.dev/os#Remove) in Golang), even when there are
apparent files underneath. Non-empty directory removal is generally not allowed,
so the `RemoveAll` name is clearer with what it does.

## `linuxtsm` package

The `linuxtsm` package defines an implementation of `configfsi.Client` with

```golang
func MakeClient() (configfsi.Client, error)
```

For further convenience, `linuxtsm` provides an alias for `MakeClient` combined with `report.Get` as

```golang
func GetReport(req *report.Request) (*report.Response, error)
```

The usage is the same as for `report.Get`.

## `faketsm` package

The `faketsm.Client` implementation allows tests to provide custom behavior for subsystems by name:

```golang
type Client struct {
Subsystems map[string]configfsi.Client
}
```

The `faketsm.ReportSubsystem` type implements a client that emulates the
concurrent behavior and `generation` attribute semantics. To test negative
behavior as well, the subsystem allows the user to override `Mkdir`, `ReadFile`,
existing entries' values, and the error behavior of `WriteFile`.

## Disclaimer

This is not an officially supported Google product.
Loading
Loading