Skip to content

slashdevops/machineid

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

machineid

main branch GitHub go.mod Go version Go Reference Go Report Card license Release

A zero-dependency Go library that generates unique, deterministic machine identifiers from hardware characteristics. IDs are stable across reboots, sensitive to hardware changes, and ideal for software licensing, device fingerprinting, and telemetry correlation.

Features

  • Zero Dependencies — built entirely on the Go standard library
  • Cross-Platform — macOS, Linux, and Windows
  • Configurable — choose which hardware signals to include (CPU, Motherboard, System UUID, MAC, Disk)
  • Power-of-2 Output — 32, 64, 128, or 256 hex characters
  • SHA-256 Hashing — cryptographically secure, no collisions in practice
  • Salt Support — application-specific IDs on the same machine
  • VM Friendly — preset for virtual environments (CPU + UUID)
  • Thread-Safe — safe for concurrent use after configuration
  • Diagnostic API — inspect which components succeeded or failed
  • Optional Logging*slog.Logger support for observability with zero overhead when disabled
  • Structured Errors — sentinel errors and typed errors for programmatic handling via errors.Is / errors.As
  • Testable — dependency-injectable command executor

Installation

Library

Add the module to your Go project:

go get github.com/slashdevops/machineid

Requires Go 1.26+. No external dependencies.

CLI Tool

Using go install

go install github.com/slashdevops/machineid/cmd/machineid@latest

Make sure ~/go/bin is in your PATH:

mkdir -p ~/go/bin

# bash
cat >> ~/.bash_profile <<EOL
export PATH=\$PATH:~/go/bin
EOL

source ~/.bash_profile

# zsh
cat >> ~/.zshrc <<EOL
export PATH=\$PATH:~/go/bin
EOL

source ~/.zshrc

Installing a Precompiled Binary

Signed and notarized binaries for macOS, Linux, and Windows are available on the releases page.

macOS (signed & notarized universal .pkg installer — arm64 + amd64):

curl -L https://github.com/slashdevops/machineid/releases/latest/download/machineid-darwin-universal.pkg -o machineid.pkg
sudo installer -pkg machineid.pkg -target /

Or double-click the .pkg file in Finder to use the graphical installer.

Linux:

curl -L https://github.com/slashdevops/machineid/releases/latest/download/machineid-linux-amd64.zip -o machineid.zip
unzip machineid.zip && sudo mv machineid /usr/local/bin/

Windows (via PowerShell):

Invoke-WebRequest -Uri https://github.com/slashdevops/machineid/releases/latest/download/machineid-windows-amd64.zip -OutFile machineid.zip
Expand-Archive machineid.zip -DestinationPath $env:USERPROFILE\bin

See docs/macos-signing.md and docs/linux-signing.md for details on binary verification.

Building from Source

Clone the repository and build with version metadata via the provided Makefile:

git clone https://github.com/slashdevops/machineid.git
cd machineid
make build
./build/machineid -version

Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/slashdevops/machineid"
)

func main() {
    ctx := context.Background()
    id, err := machineid.New().
        WithCPU().
        WithSystemUUID().
        ID(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(id)
    // Output: 64-character hex string (e.g. b5c42832542981af…)
}

Usage

Selecting Hardware Components

Enable one or more hardware sources via the With* methods:

ctx := context.Background()
provider := machineid.New().
    WithCPU().            // processor ID and feature flags
    WithMotherboard().    // motherboard serial number
    WithSystemUUID().     // BIOS/UEFI system UUID
    WithMAC().            // physical network interface MAC addresses (default filter)

    WithDisk()            // internal disk serial numbers

id, err := provider.ID(ctx)

MAC Address Filtering

Control which network interfaces are included in the machine ID using MACFilter:

ctx := context.Background()

// Physical interfaces only (default, most stable for bare-metal)
id, _ := machineid.New().WithCPU().WithMAC().ID(ctx)

// All interfaces including virtual (VPN, Docker, bridges)
id, _ = machineid.New().WithCPU().WithMAC(machineid.MACFilterAll).ID(ctx)

// Only virtual interfaces (useful for container-specific fingerprinting)
id, _ = machineid.New().WithCPU().WithMAC(machineid.MACFilterVirtual).ID(ctx)
Filter Interfaces Included Best For
MACFilterPhysical en0, eth0, wlan0 (default) Bare-metal stability
MACFilterAll Physical + virtual (docker0, utun, bridge, etc.) Maximum uniqueness
MACFilterVirtual docker0, utun, bridge0, veth, vmnet, etc. Container fingerprinting

Output Formats

All formats produce pure hexadecimal strings without dashes:

ctx := context.Background()

// 32 characters (2^5) — compact
id, _ := machineid.New().WithCPU().WithSystemUUID().WithFormat(machineid.Format32).ID(ctx)

// 64 characters (2^6) — default, full SHA-256
id, _ = machineid.New().WithCPU().WithSystemUUID().WithFormat(machineid.Format64).ID(ctx)

// 128 characters (2^7) — extended
id, _ = machineid.New().WithCPU().WithSystemUUID().WithFormat(machineid.Format128).ID(ctx)

// 256 characters (2^8) — maximum
id, _ = machineid.New().WithCPU().WithSystemUUID().WithFormat(machineid.Format256).ID(ctx)
Format Length Bits Collision Probability (1 B IDs) Use Case
Format32 32 128 ~1.47 × 10⁻²¹ Compact identifiers
Format64 64 256 ~4.32 × 10⁻⁶⁰ Default, recommended
Format128 128 512 Virtually zero Extended security
Format256 256 1024 Astronomically low Maximum security

Custom Salt

A salt ensures the same machine produces different IDs for different applications:

ctx := context.Background()
id, _ := machineid.New().
    WithCPU().
    WithSystemUUID().
    WithSalt("my-app-v1").
    ID(ctx)

VM-Friendly Mode

For virtual machines where disk serials and MACs may be unstable:

ctx := context.Background()
id, _ := machineid.New().
    VMFriendly().  // CPU + System UUID only
    WithSalt("my-app").
    ID(ctx)

Validation

Check whether a stored ID still matches the current hardware:

ctx := context.Background()
provider := machineid.New().WithCPU().WithSystemUUID()
valid, err := provider.Validate(ctx, storedID)

Diagnostics

Inspect which hardware components were successfully collected:

ctx := context.Background()
provider := machineid.New().
    WithCPU().
    WithSystemUUID().
    WithDisk()

id, _ := provider.ID(ctx)

diag := provider.Diagnostics()
fmt.Println("Collected:", diag.Collected)  // e.g. [cpu uuid]
fmt.Println("Errors:", diag.Errors)        // e.g. map[disk: no internal disk identifiers found]

Logging

Enable optional logging with any *slog.Logger for observability. When no logger is set (the default), there is zero overhead:

import (
    "log/slog"
    "os"
)

ctx := context.Background()
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))

id, err := machineid.New().
    WithCPU().
    WithSystemUUID().
    WithLogger(logger).
    ID(ctx)
Log Level What's Logged
Info Component collected, fallback triggered, ID generation lifecycle
Warn Component failed or returned empty value
Debug Command execution details (name, args, duration), raw hardware values

The logger is compatible with slog.Default() which bridges to the standard log package:

// Use the standard library default logger
provider.WithLogger(slog.Default())

Error Handling

The package provides sentinel errors for errors.Is and typed errors for errors.As:

id, err := provider.ID(ctx)
if errors.Is(err, machineid.ErrNoIdentifiers) {
    // No hardware identifiers were collected
}

Sentinel Errors

Error Meaning
ErrNoIdentifiers No hardware identifiers collected with current config
ErrEmptyValue A component returned an empty value
ErrNoValues A multi-value component (MAC, disk) returned no values
ErrNotFound A value was not found in command output or system files
ErrOEMPlaceholder A value matches a BIOS/UEFI placeholder ("To be filled...")
ErrAllMethodsFailed All collection methods for a component were exhausted

Typed Errors

Use errors.As to extract structured context from errors:

// Check if a system command failed
var cmdErr *machineid.CommandError
if errors.As(err, &cmdErr) {
    fmt.Println("command:", cmdErr.Command) // e.g. "sysctl", "ioreg", "wmic"
}

// Check if output parsing failed
var parseErr *machineid.ParseError
if errors.As(err, &parseErr) {
    fmt.Println("source:", parseErr.Source) // e.g. "system_profiler JSON"
}

// Inspect diagnostic errors per component
diag := provider.Diagnostics()
var compErr *machineid.ComponentError
if errors.As(diag.Errors["cpu"], &compErr) {
    fmt.Println("component:", compErr.Component)
    fmt.Println("cause:", compErr.Err)
}

CLI Tool

A ready-to-use command-line tool is included.

See the Installation section above for all ways to install the CLI.

Examples

# Default: CPU + motherboard + UUID (64 hex chars)
machineid

# Specific components
machineid -cpu -uuid

# All hardware sources, compact 32-char format
machineid -all -format 32

# VM-friendly with custom salt
machineid -vm -salt "my-app"

# JSON output with diagnostics
machineid -all -json -diagnostics

# Validate a previously stored ID
machineid -cpu -uuid -validate "b5c42832542981af58c9dc3bc241219e780ff7d276cfad05fac222846edb84f7"

# Include all MACs (physical + virtual)
machineid -mac -mac-filter all

# Info-level logging (fallbacks, lifecycle events)
machineid -all -verbose

# Debug-level logging (command details, raw values, timing)
machineid -all -debug

# Version information
machineid -version
machineid -version-long

All Flags

Flag Description
-cpu Include CPU identifier
-motherboard Include motherboard serial number
-uuid Include system UUID (BIOS/UEFI)
-mac Include network interface MAC addresses
-mac-filter F MAC filter: physical (default), all, or virtual
-disk Include disk serial numbers
-all Include all hardware identifiers (CPU, motherboard, UUID, MAC, disk)
-vm VM-friendly mode: CPU + UUID only
-format N Output length: 32, 64 (default), 128, or 256 hex chars
-salt STRING Application-specific salt for unique IDs per app
-validate ID Check a stored ID against the current machine
-diagnostics Show which hardware components were collected or failed
-json Format output as JSON
-verbose Info-level logs to stderr (fallbacks, lifecycle)
-debug Debug-level logs to stderr (commands, values, timing)
-version Print version and exit
-version-long Print detailed build information and exit

When no component flags are specified, the default is -cpu -motherboard -uuid.

How It Works

  1. Collect — gather hardware identifiers based on the provider configuration
  2. Sort — sort identifiers alphabetically for deterministic ordering
  3. Hash — apply SHA-256 to the concatenated identifiers (with optional salt)
  4. Format — truncate or extend the hash to the selected power-of-2 length

Platform Details

Platform CPU UUID Motherboard Disk MAC
macOS sysctl, system_profiler system_profiler, ioreg system_profiler, ioreg system_profiler net.Interfaces
Linux /proc/cpuinfo /sys/class/dmi/id, /etc/machine-id /sys/class/dmi/id lsblk, /sys/block net.Interfaces
Windows wmic, PowerShell wmic, PowerShell wmic, PowerShell wmic, PowerShell net.Interfaces

Each source has fallback methods for resilience across OS versions and configurations.

Performance note: On Windows, all hardware queries run concurrently using goroutines. This reduces total latency from the sum of all wmic/PowerShell calls (which are slow due to process startup overhead) to the maximum of any single call — typically cutting ID generation time from ~8-12s to ~2-3s.

Testing

The library supports dependency injection for deterministic testing without real system commands:

type mockExecutor struct {
    mu      sync.RWMutex
    outputs map[string]string
}

func (m *mockExecutor) Execute(ctx context.Context, name string, args ...string) (string, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    if output, ok := m.outputs[name]; ok {
        return output, nil
    }
    return "", fmt.Errorf("command not found: %s", name)
}

ctx := context.Background()
provider := machineid.New().
    WithExecutor(&mockExecutor{
        outputs: map[string]string{
            "sysctl": "Intel Core i9",
        },
    }).
    WithCPU()

id, err := provider.ID(ctx)

Note: Custom executors must be safe for concurrent use since Windows collects hardware identifiers in parallel goroutines.

Run the test suite:

go test -v -race ./...

Security Considerations

  • SHA-256 is a cryptographically secure one-way hash — hardware details cannot be recovered from an ID
  • Sorting ensures consistent output regardless of collection order
  • Salt support prevents cross-application ID reuse
  • No personally identifiable information (PII) is exposed in the output

Best Practices

Choosing a Format

Format Recommendation
Format32 Embedded systems or storage-constrained environments
Format64 Recommended for most use cases (default)
Format128 Extra security margin or regulatory requirements
Format256 Maximum security for critical applications

Hardware Identifier Selection

ctx := context.Background()

// Minimal (VMs, containers)
id, _ := machineid.New().VMFriendly().ID(ctx)

// Balanced (recommended)
id, _ = machineid.New().
    WithCPU().
    WithSystemUUID().
    WithMotherboard().
    ID(ctx)

// Maximum (most unique, but sensitive to hardware changes)
id, _ = machineid.New().
    WithCPU().
    WithSystemUUID().
    WithMotherboard().
    WithMAC().
    WithDisk().
    ID(ctx)

Troubleshooting

Git Tag Push Error: "push declined due to repository rule violations"

If you encounter this error when trying to push a tag:

! [remote rejected] v0.0.1 -> v0.0.1 (push declined due to repository rule violations)
error: failed to push some refs to 'github.com:slashdevops/machineid.git'

Cause: This happens when you try to create a tag with a version number that is older than existing tags. GitHub repository rules enforce semantic versioning order to prevent version rollback.

Solution: Create a tag with a version number higher than all existing tags.

  1. Check existing tags:

    git tag -l
  2. Create the next appropriate version:

    # If the latest tag is v0.0.2, use v0.0.3 or higher
    git tag -a "v0.0.3" -m "Release v0.0.3"
    git push origin v0.0.3

For more information about versioning and releases, see CONTRIBUTING.md.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines on how to contribute, including information about versioning, testing, and code style.

License

Apache License 2.0

About

Library and program to provides functionality to generate unique machine identifiers based on hardware characteristics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

Packages

 
 
 

Contributors