Documentation
¶
Overview ¶
Package machineid generates unique, deterministic machine identifiers derived from hardware characteristics. The generated IDs are stable across reboots but sensitive to hardware changes, making them suitable for software licensing, device fingerprinting, and telemetry correlation.
Zero Dependencies ¶
This package relies exclusively on the Go standard library and OS-level commands. There are no third-party dependencies.
Overview ¶
A Provider collects hardware signals (CPU, motherboard serial, system UUID, MAC addresses, disk serials), sorts and concatenates them, then produces a SHA-256 based hexadecimal fingerprint. The result length is always a power of two: 32, 64, 128, or 256 characters, controlled by FormatMode.
Quick Start ¶
id, err := machineid.New(). WithCPU(). WithSystemUUID(). ID(ctx)
Configuring Hardware Sources ¶
Enable individual hardware components via the With* methods:
- Provider.WithCPU — processor identifier and feature flags
- Provider.WithMotherboard — motherboard / baseboard serial number
- Provider.WithSystemUUID — BIOS / UEFI system UUID
- Provider.WithMAC — MAC addresses of network interfaces (filterable)
- Provider.WithDisk — serial numbers of internal disks
Or use Provider.VMFriendly to select a minimal, virtual-machine-safe subset (CPU + System UUID).
MAC Address Filtering ¶
Provider.WithMAC accepts an optional MACFilter to control which network interfaces contribute to the machine ID:
- MACFilterPhysical — only physical interfaces (default)
- MACFilterAll — all non-loopback, up interfaces (physical + virtual)
- MACFilterVirtual — only virtual interfaces (VPN, bridge, container)
Examples:
// Physical interfaces only (default, most stable) provider.WithMAC() // Include all interfaces provider.WithMAC(machineid.MACFilterAll) // Only virtual interfaces (containers, VPNs) provider.WithMAC(machineid.MACFilterVirtual)
Output Formats ¶
Set the output length with Provider.WithFormat:
- Format32 — 32 hex characters (128 bits, truncated SHA-256)
- Format64 — 64 hex characters (256 bits, full SHA-256, default)
- Format128 — 128 hex characters (512 bits, double SHA-256)
- Format256 — 256 hex characters (1024 bits, quadruple SHA-256)
All formats produce pure hexadecimal strings without dashes.
Salt ¶
Provider.WithSalt mixes an application-specific string into the hash so that two applications on the same machine produce different IDs:
id, _ := machineid.New().
WithCPU().
WithSystemUUID().
WithSalt("my-app-v1").
ID(ctx)
Validation ¶
Provider.Validate regenerates the ID and compares it to a previously stored value:
valid, err := provider.Validate(ctx, storedID)
Diagnostics ¶
After calling Provider.ID, call Provider.Diagnostics to inspect which components were collected and which encountered errors:
diag := provider.Diagnostics()
fmt.Println("Collected:", diag.Collected)
fmt.Println("Errors:", diag.Errors)
Logging ¶
Provider.WithLogger accepts a *log/slog.Logger for optional observability. When set, the provider logs component collection results, fallback paths, command execution timing, and errors. A nil logger (the default) disables all logging with zero overhead.
logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) id, err := machineid.New(). WithCPU(). WithSystemUUID(). WithLogger(logger). ID(ctx)
Log levels:
- Info: component collected, fallback triggered, ID generation lifecycle
- Warn: component failed or returned empty value
- Debug: command execution details, raw hardware values, timing
Errors ¶
The package provides sentinel errors for programmatic error handling:
- ErrNoIdentifiers — no hardware identifiers collected
- ErrEmptyValue — a component returned an empty value
- ErrNoValues — a multi-value component returned no values
- ErrNotFound — a value was not found in command output or system files
- ErrOEMPlaceholder — a value matches a BIOS/UEFI OEM placeholder
- ErrAllMethodsFailed — all collection methods for a component were exhausted
Typed errors provide structured context for errors.As:
- CommandError — a system command execution failed (includes the command name)
- ParseError — output parsing failed (includes the data source)
- ComponentError — a hardware component failed (includes the component name)
Errors in [DiagnosticInfo.Errors] are wrapped in ComponentError, so callers can inspect both the component name and the underlying cause:
var compErr *machineid.ComponentError
if errors.As(diag.Errors["cpu"], &compErr) {
fmt.Println("component:", compErr.Component)
fmt.Println("cause:", compErr.Err)
}
Thread Safety ¶
A Provider is safe for concurrent use after configuration is complete. The first successful call to Provider.ID freezes the configuration and caches the result; subsequent calls return the cached value.
Testing ¶
Inject a custom CommandExecutor via Provider.WithExecutor to replace real system commands with deterministic test doubles:
provider := machineid.New(). WithExecutor(myMock). WithCPU()
Platform Support ¶
Supported operating systems: macOS (darwin), Linux, and Windows. Each platform uses native tools (system_profiler / ioreg, /sys / lsblk, wmic / PowerShell) to collect hardware data.
Installation ¶
To use machineid as a library in your Go project:
go get github.com/slashdevops/machineid
To install the CLI tool:
go install github.com/slashdevops/machineid/cmd/machineid@latest
Precompiled binaries for macOS, Linux, and Windows are available on the [releases page]: https://github.com/slashdevops/machineid/releases
CLI Tool ¶
A ready-to-use command-line tool is provided in cmd/machineid:
machineid -cpu -uuid machineid -all -format 32 -json machineid -vm -salt "my-app" -diagnostics machineid -mac -mac-filter all machineid -cpu -uuid -verbose machineid -all -debug machineid -version machineid -version.long
Example (CollisionResistance) ¶
Example_collisionResistance demonstrates the collision resistance of different formats.
package main
import (
"fmt"
"math"
)
func main() {
// Calculate collision probability (simplified)
format32Bits := 128.0 // 32 hex chars = 128 bits
format64Bits := 256.0 // 64 hex chars = 256 bits
// For random IDs, probability of collision after N IDs (birthday paradox):
// P(collision) ≈ N^2 / (2 * 2^bits)
// For no collision with 1 billion IDs:
n := 1e9 // 1 billion IDs
// Format32 (128 bits)
collisionProb32 := (n * n) / (2 * math.Pow(2, format32Bits))
fmt.Printf("Format32 collision probability with 1B IDs: %.2e\n", collisionProb32)
// Format64 (256 bits) - essentially zero
collisionProb64 := (n * n) / (2 * math.Pow(2, format64Bits))
fmt.Printf("Format64 collision probability with 1B IDs: %.2e\n", collisionProb64)
fmt.Printf("Format64 is more secure: %v\n", collisionProb64 < collisionProb32)
}
Output: Format32 collision probability with 1B IDs: 1.47e-21 Format64 collision probability with 1B IDs: 4.32e-60 Format64 is more secure: true
Example (PowerOfTwo) ¶
Example_powerOfTwo demonstrates why power-of-2 lengths are beneficial.
package main
import (
"context"
"fmt"
"github.com/slashdevops/machineid"
)
func main() {
// Format32: 32 hex chars = 128 bits = 2^128 possible values
id32, _ := machineid.New().WithCPU().WithSystemUUID().WithFormat(machineid.Format32).ID(context.Background())
fmt.Printf("Format32 (2^5 chars): %d characters\n", len(id32))
fmt.Printf("Format32 bits: %d (2^%d possible values)\n", len(id32)*4, len(id32)*4)
// Format64: 64 hex chars = 256 bits = 2^256 possible values (full SHA-256)
id64, _ := machineid.New().WithCPU().WithSystemUUID().WithFormat(machineid.Format64).ID(context.Background())
fmt.Printf("Format64 (2^6 chars): %d characters\n", len(id64))
fmt.Printf("Format64 bits: %d (2^%d possible values)\n", len(id64)*4, len(id64)*4)
}
Output: Format32 (2^5 chars): 32 characters Format32 bits: 128 (2^128 possible values) Format64 (2^6 chars): 64 characters Format64 bits: 256 (2^256 possible values)
Index ¶
- Constants
- Variables
- type CommandError
- type CommandExecutor
- type ComponentError
- type DiagnosticInfo
- type FormatMode
- type MACFilter
- type ParseError
- type Provider
- func (p *Provider) Diagnostics() *DiagnosticInfo
- func (p *Provider) ID(ctx context.Context) (string, error)
- func (p *Provider) VMFriendly() *Provider
- func (p *Provider) Validate(ctx context.Context, id string) (bool, error)
- func (p *Provider) WithCPU() *Provider
- func (p *Provider) WithDisk() *Provider
- func (p *Provider) WithExecutor(executor CommandExecutor) *Provider
- func (p *Provider) WithFormat(mode FormatMode) *Provider
- func (p *Provider) WithLogger(logger *slog.Logger) *Provider
- func (p *Provider) WithMAC(filter ...MACFilter) *Provider
- func (p *Provider) WithMotherboard() *Provider
- func (p *Provider) WithSalt(salt string) *Provider
- func (p *Provider) WithSystemUUID() *Provider
Examples ¶
Constants ¶
const ( ComponentCPU = "cpu" ComponentMotherboard = "motherboard" ComponentSystemUUID = "uuid" ComponentMAC = "mac" ComponentDisk = "disk" ComponentMachineID = "machine-id" // Linux systemd machine-id )
Component names used as keys in DiagnosticInfo.
Variables ¶
var ( // ErrNoIdentifiers is returned when no hardware identifiers could be // collected with the current configuration. ErrNoIdentifiers = errors.New("no hardware identifiers found with current configuration") // ErrEmptyValue is returned in [DiagnosticInfo.Errors] when a hardware // component returned an empty value. ErrEmptyValue = errors.New("empty value returned") // ErrNoValues is returned in [DiagnosticInfo.Errors] when a hardware // component returned no values. ErrNoValues = errors.New("no values found") // ErrNotFound is returned when a hardware value is not found in // command output or system files. ErrNotFound = errors.New("value not found") // ErrOEMPlaceholder is returned when a hardware value matches a // BIOS/UEFI OEM placeholder such as "To be filled by O.E.M.". ErrOEMPlaceholder = errors.New("value is OEM placeholder") // ErrAllMethodsFailed is returned when all collection methods for a // hardware component have been exhausted without success. ErrAllMethodsFailed = errors.New("all collection methods failed") )
Sentinel errors returned by Provider.ID and recorded in [DiagnosticInfo.Errors].
Functions ¶
This section is empty.
Types ¶
type CommandError ¶
type CommandError struct {
Command string // command name, e.g. "sysctl", "ioreg", "wmic"
Err error // underlying error from exec
}
CommandError records a failed system command execution. Use errors.As to extract the command name from wrapped errors.
func (*CommandError) Error ¶
func (e *CommandError) Error() string
Error returns a human-readable description of the command failure.
func (*CommandError) Unwrap ¶
func (e *CommandError) Unwrap() error
Unwrap returns the underlying error.
type CommandExecutor ¶
type CommandExecutor interface {
Execute(ctx context.Context, name string, args ...string) (string, error)
}
CommandExecutor is an interface for executing system commands, allowing for dependency injection and testing.
type ComponentError ¶
type ComponentError struct {
Component string // component name, e.g. "cpu", "uuid", "disk"
Err error // underlying error
}
ComponentError records a failure while collecting a specific hardware component. These errors appear in [DiagnosticInfo.Errors] and can be inspected with errors.As.
func (*ComponentError) Error ¶
func (e *ComponentError) Error() string
Error returns a human-readable description of the component failure.
func (*ComponentError) Unwrap ¶
func (e *ComponentError) Unwrap() error
Unwrap returns the underlying error.
type DiagnosticInfo ¶
type DiagnosticInfo struct {
Errors map[string]error // Component names that failed with their errors
Collected []string // Component names that were successfully collected
}
DiagnosticInfo contains information about what was collected during ID generation. Use Provider.Diagnostics to retrieve this information after calling Provider.ID.
type FormatMode ¶
type FormatMode int
FormatMode defines the output format and length of the machine ID.
const ( // Format64 outputs 64 hex characters (2^6), default SHA-256 output without dashes. Format64 FormatMode = iota // Format32 outputs 32 hex characters (2^5), truncated SHA-256. Format32 // Format128 outputs 128 hex characters (2^7), double SHA-256. Format128 // Format256 outputs 256 hex characters (2^8), quadruple SHA-256. Format256 )
type MACFilter ¶
type MACFilter int
MACFilter controls which network interfaces are included in MAC address collection.
const ( // MACFilterPhysical includes only physical network interfaces (default). // Virtual, VPN, bridge, and container interfaces are excluded. MACFilterPhysical MACFilter = iota // MACFilterAll includes all non-loopback, up network interfaces (physical and virtual). MACFilterAll // MACFilterVirtual includes only virtual network interfaces // (VPN, bridge, container, and hypervisor interfaces). MACFilterVirtual )
type ParseError ¶
type ParseError struct {
Source string // data source, e.g. "system_profiler JSON", "wmic output"
Err error // underlying parse error
}
ParseError records a failure while parsing command or system output. Use errors.As to extract the source from wrapped errors.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
Error returns a human-readable description of the parse failure.
func (*ParseError) Unwrap ¶
func (e *ParseError) Unwrap() error
Unwrap returns the underlying error.
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider configures and generates unique machine IDs. After the first call to Provider.ID, the configuration is frozen and the result is cached. Provider methods are safe for concurrent use after configuration is complete.
Example ¶
ExampleProvider demonstrates basic usage of the machineid package. This example shows how to generate a unique machine identifier that is stable across reboots and suitable for licensing.
package main
import (
"context"
"fmt"
"github.com/slashdevops/machineid"
)
func main() {
// Create a new provider with CPU and System UUID
// These are typically stable identifiers on most systems
provider := machineid.New().
WithCPU().
WithSystemUUID().
WithSalt("my-application-v1")
// Generate the machine ID
id, err := provider.ID(context.Background())
if err != nil {
fmt.Printf("Error generating ID: %v\n", err)
return
}
// The ID is a 64-character hexadecimal string (SHA-256 hash, power of 2)
fmt.Printf("Machine ID length: %d\n", len(id))
fmt.Printf("Machine ID is hexadecimal: %v\n", isHexString(id))
// Validate the ID
valid, err := provider.Validate(context.Background(), id)
if err != nil {
fmt.Printf("Error validating ID: %v\n", err)
return
}
fmt.Printf("ID is valid: %v\n", valid)
}
// isHexString reports whether s is a non-empty string of lowercase hex digits.
func isHexString(s string) bool {
if len(s) == 0 {
return false
}
for _, c := range s {
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
return false
}
}
return true
}
Output: Machine ID length: 64 Machine ID is hexadecimal: true ID is valid: true
func New ¶
func New() *Provider
New creates a new Provider with default settings. The provider uses real system commands by default. Default format is Format64 (64 hex characters, 2^6).
Example ¶
ExampleNew demonstrates the simplest way to generate a machine ID.
package main
import (
"context"
"fmt"
"github.com/slashdevops/machineid"
)
func main() {
provider := machineid.New().
WithCPU().
WithSystemUUID()
id, err := provider.ID(context.Background())
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
fmt.Printf("ID length: %d\n", len(id))
}
Output: ID length: 64
func (*Provider) Diagnostics ¶
func (p *Provider) Diagnostics() *DiagnosticInfo
Diagnostics returns information about which hardware components were successfully collected and which ones failed during the last call to [ID]. Returns nil if [ID] has not been called yet.
func (*Provider) ID ¶
ID generates the machine ID based on the configured options. It caches the result, so subsequent calls return the same ID. The configuration is frozen after the first successful call. The provided context controls the timeout and cancellation of any system commands executed during hardware identifier collection. This method is safe for concurrent use.
Example (AllComponents) ¶
ExampleProvider_ID_allComponents shows using every available hardware source.
package main
import (
"context"
"fmt"
"github.com/slashdevops/machineid"
)
func main() {
provider := machineid.New().
WithCPU().
WithMotherboard().
WithSystemUUID().
WithMAC().
WithDisk().
WithSalt("full-example")
id, err := provider.ID(context.Background())
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
fmt.Printf("Full ID length: %d\n", len(id))
fmt.Printf("Is hex: %v\n", isAllHex(id))
}
func isAllHex(s string) bool {
for _, c := range s {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
return false
}
}
return len(s) > 0
}
Output: Full ID length: 64 Is hex: true
func (*Provider) VMFriendly ¶
VMFriendly configures the provider for virtual machines (CPU + UUID only).
Example ¶
ExampleProvider_VMFriendly demonstrates creating a VM-friendly machine ID. This configuration works well in virtual machine environments where hardware like disk serials and MAC addresses may change frequently.
package main
import (
"context"
"fmt"
"github.com/slashdevops/machineid"
)
func main() {
provider := machineid.New().
VMFriendly().
WithSalt("vm-app")
id, err := provider.ID(context.Background())
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Generated VM-friendly ID: %v\n", len(id) == 64)
}
Output: Generated VM-friendly ID: true
Example (Preset) ¶
ExampleProvider_VMFriendly_preset demonstrates the VM-friendly preset.
package main
import (
"context"
"fmt"
"github.com/slashdevops/machineid"
)
func main() {
id, err := machineid.New().
VMFriendly().
ID(context.Background())
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
fmt.Printf("VM-friendly ID length: %d\n", len(id))
}
Output: VM-friendly ID length: 64
func (*Provider) Validate ¶
Validate reports whether the provided ID matches the current machine ID. The provided context is forwarded to Provider.ID if it needs to generate the ID.
Example ¶
ExampleProvider_Validate shows how to check a stored ID against the current machine.
package main
import (
"context"
"fmt"
"github.com/slashdevops/machineid"
)
func main() {
provider := machineid.New().
WithCPU().
WithSystemUUID()
id, _ := provider.ID(context.Background())
// Validate the correct ID
valid, _ := provider.Validate(context.Background(), id)
fmt.Printf("Correct ID valid: %v\n", valid)
// Validate an incorrect ID
valid, _ = provider.Validate(context.Background(), "0000000000000000000000000000000000000000000000000000000000000000")
fmt.Printf("Wrong ID valid: %v\n", valid)
}
Output: Correct ID valid: true Wrong ID valid: false
func (*Provider) WithExecutor ¶
func (p *Provider) WithExecutor(executor CommandExecutor) *Provider
WithExecutor sets a custom CommandExecutor, enabling deterministic testing without real system commands.
func (*Provider) WithFormat ¶
func (p *Provider) WithFormat(mode FormatMode) *Provider
WithFormat sets the output format and length. Use Format64 (default), Format32, Format128, or Format256.
Example ¶
ExampleProvider_WithFormat demonstrates the four power-of-two output formats.
package main
import (
"context"
"fmt"
"github.com/slashdevops/machineid"
)
func main() {
ctx := context.Background()
base := func(mode machineid.FormatMode) int {
id, err := machineid.New().
WithCPU().
WithSystemUUID().
WithFormat(mode).
ID(ctx)
if err != nil {
return -1
}
return len(id)
}
fmt.Printf("Format32: %d chars\n", base(machineid.Format32))
fmt.Printf("Format64: %d chars\n", base(machineid.Format64))
fmt.Printf("Format128: %d chars\n", base(machineid.Format128))
fmt.Printf("Format256: %d chars\n", base(machineid.Format256))
}
Output: Format32: 32 chars Format64: 64 chars Format128: 128 chars Format256: 256 chars
func (*Provider) WithLogger ¶
WithLogger sets an optional *slog.Logger for observability. When set, the provider logs component collection, fallback paths, command execution timing, and errors. A nil logger (the default) disables all logging with zero overhead.
Compatible with any *slog.Logger, including slog.Default which bridges to the standard log package.
func (*Provider) WithMAC ¶
WithMAC includes network interface MAC addresses in the generation. An optional MACFilter controls which interfaces are included. Default is MACFilterPhysical, which excludes virtual, VPN, bridge, and container interfaces for stability.
func (*Provider) WithMotherboard ¶
WithMotherboard includes the motherboard serial number in the generation.
func (*Provider) WithSalt ¶
WithSalt sets a custom salt for additional entropy.
Example ¶
ExampleProvider_WithSalt shows how a salt produces application-specific IDs.
package main
import (
"context"
"fmt"
"github.com/slashdevops/machineid"
)
func main() {
ctx := context.Background()
id1, _ := machineid.New().
WithCPU().
WithSystemUUID().
WithSalt("app-one").
ID(ctx)
id2, _ := machineid.New().
WithCPU().
WithSystemUUID().
WithSalt("app-two").
ID(ctx)
fmt.Printf("Same length: %v\n", len(id1) == len(id2))
fmt.Printf("Different IDs: %v\n", id1 != id2)
}
Output: Same length: true Different IDs: true
func (*Provider) WithSystemUUID ¶
WithSystemUUID includes the system UUID in the generation.