posture: add get serial support for Windows/Linux

This commit adds support for getting serial numbers from SMBIOS
on Windows/Linux (and BSD) using go-smbios.

Updates #5902

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2023-10-05 13:38:20 +02:00 committed by Kristoffer Dalby
parent 249edaa349
commit 9eedf86563
8 changed files with 217 additions and 3 deletions

View File

@ -86,6 +86,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 github.com/dblohm7/wingoes/com/automation from tailscale.com/util/osdiag/internal/wsc
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
github.com/fxamacker/cbor/v2 from tailscale.com/tka
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet

1
go.mod
View File

@ -20,6 +20,7 @@ require (
github.com/creack/pty v1.1.18
github.com/dave/jennifer v1.7.0
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
github.com/dsnet/try v0.0.3
github.com/evanw/esbuild v0.19.4
github.com/frankban/quicktest v1.14.5

2
go.sum
View File

@ -233,6 +233,8 @@ github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077 h1:WphxHslVftszsr0
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU=
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY=
github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=

View File

@ -232,7 +232,7 @@ func (b *LocalBackend) handleC2NPostureIdentityGet(w http.ResponseWriter, r *htt
// TODO(kradalby): Use syspolicy + envknob to allow Win registry,
// macOS defaults and env to override this setting.
if b.Prefs().PostureChecking() {
sns, err := posture.GetSerialNumbers()
sns, err := posture.GetSerialNumbers(b.logf)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -0,0 +1,143 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Build on Windows, Linux and *BSD
//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd
package posture
import (
"errors"
"fmt"
"strings"
"github.com/digitalocean/go-smbios/smbios"
"tailscale.com/types/logger"
"tailscale.com/util/multierr"
)
// getByteFromSmbiosStructure retrieves a 8-bit unsigned integer at the given specOffset.
func getByteFromSmbiosStructure(s *smbios.Structure, specOffset int) uint8 {
// the `Formatted` byte slice is missing the first 4 bytes of the structure that are stripped out as header info.
// so we need to subtract 4 from the offset mentioned in the SMBIOS documentation to get the right value.
index := specOffset - 4
if index >= len(s.Formatted) || index < 0 {
return 0
}
return s.Formatted[index]
}
// getStringFromSmbiosStructure retrieves a string at the given specOffset.
// Returns an empty string if no string was present.
func getStringFromSmbiosStructure(s *smbios.Structure, specOffset int) (string, error) {
index := getByteFromSmbiosStructure(s, specOffset)
if index == 0 || int(index) > len(s.Strings) {
return "", errors.New("specified offset does not exist in smbios structure")
}
str := s.Strings[index-1]
trimmed := strings.TrimSpace(str)
return trimmed, nil
}
// Product Table (Type 1) structure
// https://web.archive.org/web/20220126173219/https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf
// Page 34 and onwards.
const (
// Serial is present at the same offset in all IDs
serialNumberOffset = 0x07
productID = 1
baseboardID = 2
chassisID = 3
)
var (
idToTableName = map[int]string{
1: "product",
2: "baseboard",
3: "chassis",
}
validTables []string
numOfTables int
)
func init() {
for _, table := range idToTableName {
validTables = append(validTables, table)
}
numOfTables = len(validTables)
}
// serialFromSmbiosStructure extracts a serial number from a product,
// baseboard or chassis SMBIOS table.
func serialFromSmbiosStructure(s *smbios.Structure) (string, error) {
id := s.Header.Type
if (id != productID) && (id != baseboardID) && (id != chassisID) {
return "", fmt.Errorf(
"cannot get serial table type %d, supported tables are %v",
id,
validTables,
)
}
serial, err := getStringFromSmbiosStructure(s, serialNumberOffset)
if err != nil {
return "", fmt.Errorf(
"failed to get serial from %s table: %w",
idToTableName[int(s.Header.Type)],
err,
)
}
return serial, nil
}
func GetSerialNumbers(logf logger.Logf) ([]string, error) {
// Find SMBIOS data in operating system-specific location.
rc, _, err := smbios.Stream()
if err != nil {
return nil, fmt.Errorf("failed to open dmi/smbios stream: %w", err)
}
defer rc.Close()
// Decode SMBIOS structures from the stream.
d := smbios.NewDecoder(rc)
ss, err := d.Decode()
if err != nil {
return nil, fmt.Errorf("failed to decode dmi/smbios structures: %w", err)
}
serials := make([]string, 0, numOfTables)
errs := make([]error, 0, numOfTables)
for _, s := range ss {
switch s.Header.Type {
case productID, baseboardID, chassisID:
serial, err := serialFromSmbiosStructure(s)
if err != nil {
errs = append(errs, err)
continue
}
serials = append(serials, serial)
}
}
err = multierr.New(errs...)
// if there were no serial numbers, check if any errors were
// returned and combine them.
if len(serials) == 0 && err != nil {
return nil, err
}
logf("got serial numbers %v (errors: %s)", serials, err)
return serials, nil
}

View File

@ -0,0 +1,38 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Build on Windows, Linux and *BSD
//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd
package posture
import (
"fmt"
"testing"
"tailscale.com/types/logger"
)
func TestGetSerialNumberNotMac(t *testing.T) {
// This test is intentionally skipped as it will
// require root on Linux to get access to the serials.
// The test case is intended for local testing.
// Comment out skip for local testing.
t.Skip()
sns, err := GetSerialNumbers(logger.Discard)
if err != nil {
t.Fatalf("failed to get serial number: %s", err)
}
if len(sns) == 0 {
t.Fatalf("expected at least one serial number, got %v", sns)
}
if len(sns[0]) <= 0 {
t.Errorf("expected a serial number with more than zero characters, got %s", sns[0])
}
fmt.Printf("serials: %v\n", sns)
}

View File

@ -1,11 +1,24 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// darwin: not implemented
// andoird: not implemented
// js: not implemented
// plan9: not implemented
// solaris: currently unsupported by go-smbios:
// https://github.com/digitalocean/go-smbios/pull/21
//go:build darwin || android || js || plan9 || solaris
package posture
import "errors"
import (
"errors"
"tailscale.com/types/logger"
)
// GetSerialNumber returns client machine serial number(s).
func GetSerialNumbers() ([]string, error) {
func GetSerialNumbers(_ logger.Logf) ([]string, error) {
return nil, errors.New("not implemented")
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package posture
import (
"testing"
"tailscale.com/types/logger"
)
func TestGetSerialNumber(t *testing.T) {
// ensure GetSerialNumbers is implemented
// or covered by a stub on a given platform.
_, _ = GetSerialNumbers(logger.Discard)
}