Reading IOT2000 Serial Number and MAC Address in C/C++ on Linux

David Krause18 min read
Other TopicSiemensTechnical Reference
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Reading IOT2000 Serial Number and MAC Address in C/C++ on Linux

Authoritative reference for extracting hardware identifiers from the SIMATIC IOT2000 industrial IoT gateway using C/C++ on the Siemens Yocto Linux image. The guide covers SMBIOS table parsing, sysfs reads, ioctl-based permanent MAC retrieval, and tamper-resistant software-protection patterns, with full working code for each method.

Overview: Hardware Identification on SIMATIC IOT2000

The SIMATIC IOT2000 family (IOT2020, IOT2040) is Siemens' line of shielded, fanless industrial IoT gateways designed for shop-floor protocol conversion, edge analytics, and OPC UA bridging. The IOT2020 uses the Intel Quark (x86, 32-bit) platform derived from the Galileo design; the IOT2040 uses the Intel Apollo Lake (x86_64) SoC. Both ship with a customized Yocto-based Linux image (the SIMATIC IOT2000 SDK, formerly known as the "IOT2000 example image") that exposes hardware identifiers through the standard SMBIOS / DMI subsystem and the Linux net driver sysfs tree.

For application developers who must bind their software to a specific physical device, the canonical anchor is the SMBIOS Type 1 "System Information" record, populated at the Siemens production line. The system serial, the system UUID, and the per-NIC permanent MAC address are the three primary candidates. This reference compares all of the available C/C++ read paths, their privilege requirements, their tamper characteristics, and recommended patterns for production-grade software protection.

Why Read Hardware Identifiers Directly from C/C++?

High-level utilities such as dmidecode (a DMI table decoder shipped with most Linux distributions) read and pretty-print SMBIOS data. They are useful for diagnostics but introduce several problems when embedded in a protected application:

  1. Process spawning overhead — fork/exec adds tens of milliseconds and introduces a dependency on a binary whose path, version, and output format may differ across images.
  2. Output parsing fragility — text parsing breaks when the BIOS, the kernel's dmi-id driver, or the dmidecode version changes the formatting.
  3. Copyleft license contamination — dmidecode is GPLv2. The GPLv2 terms require that derivative works (including processes that pipe its output as their trust anchor) be distributed in source under compatible terms. This is generally unsuitable for closed-source applications.
  4. Trust boundary inversion — invoking a separate binary to read the very identifier you are using as a trust anchor creates a subversion point: an attacker with file-system access can replace dmidecode with a stub that returns a chosen value, and the application will trust it.

Direct C/C++ access reads the SMBIOS entry point and structure table in-process, or reads the kernel-prepared sysfs view. Both approaches are faster, license-clean, and remove the parser binary from the attack surface.

SMBIOS / DMI Architecture on IOT2000

The System Management BIOS (SMBIOS) Reference Specification, maintained by the DMTF (DSP0136), defines the data structures the BIOS writes into a physical address range. The Linux kernel's DMI driver reads those structures during boot and re-exports selected fields in two ways:

  • /sys/firmware/dmi/tables/smbios_entry_point — the 32-bit SMBIOS entry point structure (binary, 31 bytes for SMBIOS 2.x; 24 bytes for SMBIOS 3.x).
  • /sys/firmware/dmi/tables/DMI — the raw SMBIOS structure table (binary, variable length).
  • /sys/firmware/dmi/id/... — a sysfs view of selected fields: product_serial, product_uuid, product_name, product_version, product_sku, product_family, board_serial, board_asset_tag, chassis_serial, chassis_asset_tag.

On the IOT2020, the SMBIOS data is supplied by the Intel Quark firmware's SMBIOS producer; the system serial follows a vendor-specific format. On the IOT2040, the AMI BIOS populates SMBIOS Type 1 with a Siemens-assigned serial (8-character alphanumeric, often in the form SVPxxxxx). The kernel's DMI subsystem reads the entry point, validates the checksum, and publishes the parsed records to sysfs.

Field-proven caveat: The IOT2000 default Yocto image mounts sysfs read-only and the DMI id files are world-readable (mode 0444 root:root). SELinux is not present in the default image; on hardened derivatives (for example, the SIMATIC Edge security variant), the files may be label-restricted and require CAP_DAC_READ_SEARCH. Always verify with ls -l /sys/firmware/dmi/id/ on the target image.

Hardware ID Extraction Flow

SIMATIC IOT2000 Hardware ID Extraction Sysfs (DMI id) User privilege SMBIOS via /dev/mem Root + CAP_SYS_RAWIO ioctl ETHTOOL_GPERMADDR User (mostly) HMAC-SHA256(key, ID) OpenSSL / mbedTLS Constant-time compare CRYPTO_memcmp() Allow / Deny License decision Tamper-resistant: hash, never raw ID Use TPM 2.0 seal on IOT2040

Method 1: Reading the Serial Number from sysfs

The simplest, most portable, and most license-clean approach. The kernel's DMI driver creates a sysfs entry at /sys/firmware/dmi/id/product_serial containing the SMBIOS Type 1 "Serial Number" field as a UTF-8 string, NUL-terminated, with trailing whitespace stripped.

Prerequisites

  • Kernel built with CONFIG_DMI=y and CONFIG_DMI_SYSFS=y (both enabled in the IOT2000 default kernel configuration; see kernel sysfs-firmware-dmi ABI documentation).
  • Read access to /sys/firmware/dmi/id/product_serial for the application user.

C implementation

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define SERIAL_PATH "/sys/firmware/dmi/id/product_serial"
#define SERIAL_MAX  128

int read_serial_sysfs(char *out, size_t outsz) {
    int fd = open(SERIAL_PATH, O_RDONLY);
    if (fd < 0) return -errno;
    ssize_t n = read(fd, out, outsz - 1);
    close(fd);
    if (n < 0) return -errno;
    while (n > 0 && (out[n-1] == '\n' || out[n-1] == '\r' ||
                      out[n-1] == ' '  || out[n-1] == '\t')) n--;
    out[n] = '\0';
    return (int)n;
}

int main(void) {
    char serial[SERIAL_MAX];
    int rc = read_serial_sysfs(serial, sizeof(serial));
    if (rc < 0) {
        fprintf(stderr, "read_serial_sysfs failed: %s\n", strerror(-rc));
        return EXIT_FAILURE;
    }
    if (rc == 0) {
        fprintf(stderr, "Empty serial — BIOS not populated\n");
        return EXIT_FAILURE;
    }
    printf("System Serial: '%s' (len=%d)\n", serial, rc);
    return EXIT_SUCCESS;
}

Verification

Compare the output against cat /sys/firmware/dmi/id/product_serial and against the serial printed on the device label. On the IOT2040, the Siemens-allocated serial is typically 8 alphanumeric characters in the form SVPxxxxx (no hyphens). On the IOT2020, the format depends on the firmware build and may include a leading platform prefix.

Method 2: Direct SMBIOS Table Parsing

For environments where sysfs is unavailable (custom kernels, real-time variants, busybox-only initramfs, containerized workloads without the /sys/firmware/dmi bind mount) or when the application needs additional SMBIOS fields (UUID, BIOS version, baseboard asset tag), parse the SMBIOS entry point and structure table directly. This method does not depend on any userspace daemon.

The 32-bit SMBIOS entry point lives at a 16-byte-aligned address in the legacy BIOS shadow (physical 0x000F0000–0x000FFFFF) or, on UEFI systems, in the EFI Configuration Table at the address referenced by the SMBIOS3 GUID. The standard search algorithm is:

  1. Scan the physical address range 0x000F0000–0x000FFFFF for the anchor string "_SM_" (SMBIOS 2.x) or "_SM3_" (SMBIOS 3.x).
  2. Validate the entry point checksum — the sum of all bytes at the entry point must equal 0x00 modulo 256.
  3. Follow the structure table address (offset 0x18 for 2.x, offset 0x10 for 3.x).
  4. Iterate records. Each record begins with a 1-byte type, 1-byte length, 2-byte handle, then variable-length data, then the string pool terminated by 0x00 0x00.

Type 1 (System Information) is the record containing the serial number. Its fields are at fixed offsets from the formatted section; strings are addressed by 1-based string indices.

SMBIOS Type 1 layout (formatted area)

Offset Field Type Description
0x00 Type u8 0x01 = System Information
0x01 Length u8 Length of the formatted area (0x19 for 2.0, 0x1B for 2.1+, 0x1F for 2.4+)
0x02 Handle u16 LE Record handle, unique within the table
0x04 Manufacturer u8 String index
0x05 Product Name u8 String index
0x06 Version u8 String index
0x07 Serial Number u8 String index (0 = not present)
0x08 UUID 16 bytes Big-endian GUID (SMBIOS ≥ 2.1)
0x18 Wake-up Type u8 SMBIOS ≥ 2.1
0x19 SKU Number u8 String index (SMBIOS ≥ 2.4)
0x1A Family u8 String index (SMBIOS ≥ 2.4)

Strings follow the fixed-size formatted area. The end of the strings section is marked by a double-zero (0x00 0x00).

Reading /dev/mem on IOT2000

To access physical memory containing the SMBIOS anchor, the process must open /dev/mem and mmap() the region. This requires CAP_SYS_RAWIO (i.e., root) and, for legacy regions below 1 MB, CONFIG_STRICT_DEVMEM must be disabled.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>

#define BIOS_SEARCH_START 0x000F0000UL
#define BIOS_SEARCH_END   0x000FFFFFUL
#define ANCHOR "_SM_"

static unsigned char ep_checksum(const unsigned char *p, size_t n) {
    unsigned char s = 0; while (n--) s += *p++; return s;
}

static const char *smbios_string(const unsigned char *record, int idx) {
    const unsigned char *p = record + record[1];
    int i = 1;
    while (*p || *(p+1)) {
        if (i++ == idx) return (const char *)p;
        p += strlen((const char *)p) + 1;
    }
    return "";
}

int read_serial_devmem(char *out, size_t outsz) {
    int fd = open("/dev/mem", O_RDONLY | O_SYNC);
    if (fd < 0) return -errno;
    size_t len = BIOS_SEARCH_END - BIOS_SEARCH_START + 1;
    volatile unsigned char *mem = mmap(NULL, len, PROT_READ,
                                       MAP_SHARED, fd, BIOS_SEARCH_START);
    if (mem == MAP_FAILED) { int e = -errno; close(fd); return e; }

    int rc = -ENOENT;
    for (size_t off = 0; off + 0x1F < len; off += 16) {
        if (memcmp((const void*)(mem + off), ANCHOR, 4) != 0) continue;
        if (ep_checksum(mem + off, 0x1F) != 0) continue;
        uint32_t table_addr =
            (uint32_t)mem[off+0x18]        |
            ((uint32_t)mem[off+0x19] << 8) |
            ((uint32_t)mem[off+0x1A] <<16) |
            ((uint32_t)mem[off+0x1B] <<24);
        munmap((void*)mem, len);
        len = 4096;
        mem = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, (off_t)table_addr);
        if (mem == MAP_FAILED) { rc = -ENOMEM; break; }

        size_t toff = 0;
        while (toff + 4 < len && mem[toff] != 0) {
            unsigned char type  = mem[toff];
            unsigned char rlen  = mem[toff+1];
            if (type == 1) {
                unsigned char serial_idx = mem[toff+0x07];
                if (serial_idx == 0) { rc = -ENODATA; break; }
                const char *s = smbios_string(mem + toff, serial_idx);
                strncpy(out, s, outsz-1); out[outsz-1] = '\0';
                rc = (int)strlen(out);
                break;
            }
            toff += rlen;
            while (toff + 1 < len &&
                   !(mem[toff] == 0 && mem[toff+1] == 0)) toff++;
            toff += 2;
        }
        break;
    }
    munmap((void*)mem, len); close(fd);
    return rc;
}
Kernel configuration note: CONFIG_STRICT_DEVMEM=y blocks mmap() of regions outside the 0xC0000000–0xFFFFFFFF ISA hole. The default IOT2000 kernel ships with this option disabled, but security-hardened derivatives (for example, the SIMATIC Edge security add-on) enable it. Verify with zcat /proc/config.gz | grep STRICT_DEVMEM on the target image before relying on /dev/mem access.

Method 3: Reading MAC Addresses from sysfs

Network interface MAC addresses are exposed by the kernel's net driver subsystem at /sys/class/net/<iface>/address. On the IOT2040, the two on-board Intel i210 Ethernet controllers present as enp1s0 and enp2s0 under systemd's predictable naming scheme. On the IOT2020, the single interface is typically eno1.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <dirent.h>

int read_mac_sysfs(const char *iface, char *out, size_t outsz) {
    char path[256];
    snprintf(path, sizeof(path), "/sys/class/net/%s/address", iface);
    int fd = open(path, O_RDONLY);
    if (fd < 0) return -errno;
    ssize_t n = read(fd, out, outsz - 1);
    close(fd);
    if (n < 0) return -errno;
    while (n > 0 && (out[n-1] == '\n' || out[n-1] == '\r' ||
                      out[n-1] == ' '  || out[n-1] == '\t')) n--;
    out[n] = '\0';
    return (int)n;
}

int list_ifaces(void) {
    DIR *d = opendir("/sys/class/net");
    if (!d) return -errno;
    struct dirent *e;
    printf("Available interfaces:\n");
    while ((e = readdir(d)) != NULL) {
        if (e->d_name[0] == '.') continue;
        char mac[32] = {0};
        int rc = read_mac_sysfs(e->d_name, mac, sizeof(mac));
        if (rc > 0) printf("  %-12s %s\n", e->d_name, mac);
    }
    closedir(d);
    return 0;
}

Method 4: ioctl SIOCETHTOOL for Permanent MAC

The SIOCETHTOOL ioctl, when called with ETHTOOL_GPERMADDR, returns the permanent (hardware-burned) MAC address stored in the NIC's EEPROM/OTP, regardless of the runtime address currently configured by the kernel. This is the only method that survives a MAC-spoofing attack in which a user changes the runtime MAC via ip link set dev X address ... or ifconfig hw ether.

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <unistd.h>

int read_perm_mac(const char *iface, unsigned char mac[6]) {
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) return -errno;
    struct ifreq ifr;
    strncpy(ifr.ifr_name, iface, IFNAMSIZ-1);
    ifr.ifr_name[IFNAMSIZ-1] = '\0';
    struct ethtool_perm_addr *ep = (void *)&ifr.ifr_data;
    ep->cmd  = ETHTOOL_GPERMADDR;
    ep->size = 6;
    int rc = ioctl(fd, SIOCETHTOOL, 𝔦);
    if (rc < 0) { int e = -errno; close(fd); return e; }
    memcpy(mac, ep->data, 6);
    close(fd);
    return 0;
}

void print_mac(const unsigned char m[6]) {
    printf("%02X:%02X:%02X:%02X:%02X:%02X\n",
           m[0], m[1], m[2], m[3], m[4], m[5]);
}

The Intel i210 on the IOT2040 stores the 64-bit MAC in the controller's EEPROM at word address 0x00–0x02; the first MAC reported by ETHTOOL_GPERMADDR corresponds to the LAN MAC. The Intel Quark on the IOT2020 also supports the ioctl through the smsc95xx driver; verify with ethtool -P <iface> on the target.

Tamper Resistance: Hashing the Identifier

Hard-coding a serial number into source code is equivalent to hard-coding a password. Anyone with strings(1) or a hex editor can extract it and patch the binary to skip the check. The standard defense is to compute a salted, keyed hash of the identifier at first run, store only the hash, and constant-time-compare on subsequent runs.

Pattern

  1. On first install, read the serial, compute HMAC-SHA256(key, serial), and write the digest to /var/lib/<app>/hwid.bin with file mode 0600.
  2. On each launch, recompute the HMAC of the current serial and constant-time-compare against the stored value. If they differ, refuse to start.
  3. The HMAC key is split across a compile-time constant in the binary and a per-deployment secret delivered out of band (e.g., a license file or hardware token).
#include <string.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>

int compute_hwid_hash(const char *serial,
                      const unsigned char *key, size_t keylen,
                      unsigned char out[32]) {
    unsigned int outlen = 32;
    if (!HMAC(EVP_sha256(), key, (int)keylen,
              (const unsigned char *)serial, strlen(serial),
              out, &outlen)) return -1;
    return outlen == 32 ? 0 : -1;
}
Threat-model caveat: An attacker with root on the device can read /var/lib/<app>/hwid.bin, re-bind it to a clone's serial, or replace the sysfs path with a FUSE mount returning a chosen value. The HMAC only raises the bar; it does not eliminate the attack. For high-value software, combine with a signed license file tied to the IOT2040's TPM 2.0 module (Apollo Lake supports tpm_tis and tpm-crb) or an external license server that issues challenge-response exchanges. See the TPM Library Specification for the relevant PCR and sealing primitives.

Method Comparison

Method Target Privilege Tamper resistance License Use case
sysfs /sys/firmware/dmi/id/product_serial System serial User (0444) Low–Medium (kernel populates from SMBIOS, no userspace daemon) LGPL (kernel) First-line binding, asset tags
SMBIOS table parse via /dev/mem System serial, UUID, BIOS fields Root + CAP_SYS_RAWIO Medium (no userspace dependency) None (own code) Hardened images, initramfs
sysfs /sys/class/net/<iface>/address Runtime MAC User Low (spoofable with ip link) LGPL (kernel) Display, asset tracking
ioctl ETHTOOL_GPERMADDR Permanent MAC User (CAP_NET_ADMIN on some drivers) Medium (EEPROM value, modifiable only with vendor tools) LGPL (kernel uAPI) Anti-spoof binding
dmidecode subprocess All DMI fields Root Very low (binary is the trust anchor) GPLv2 (incompatible with closed link) Diagnostics only
TPM 2.0 seal (IOT2040) Any identifier + PCR state User (kernel TSS) High (bound to TPM endorsement key) BSD (tpm2-tss) High-value software protection

UUID as a Secondary Anchor

SMBIOS Type 1 also contains a 16-byte UUID, exposed at /sys/firmware/dmi/id/product_uuid as a hyphenated ASCII string (e.g., 4C4C4544-0044-4810-8051-B2C04F325631). The IOT2040's firmware populates it with a value derived from a non-unique seed during Apollo Lake silicon production; on many IOT2000s of the same batch the lower bytes may be identical. Verify uniqueness within your fleet before relying on the UUID as a primary key.

The UUID byte order is mixed-endian per RFC 4122: the first three groups (time_low, time_mid, time_hi_and_version) are little-endian on the wire but are stored little-endian in the SMBIOS Type 1 record; the last two groups (clock_seq and node) are big-endian. Applications that interpret the UUID as RFC 4122 must byte-swap the first three groups when reading from SMBIOS, or read the sysfs presentation which is already in human-readable form.

Edge Cases and Field Notes

Empty serial on early firmware. IOT2020 units shipped with firmware versions prior to V2.1.0 reported an empty product_serial. Confirm the firmware level with dmidecode -s system-version and update via the Siemens Industry Online Support download portal before relying on sysfs serial reads.

Yocto read-only rootfs. The default IOT2000 image has a read-only root filesystem. /sys/firmware/dmi/id/product_serial lives in sysfs and is unaffected, but storing the HMAC digest in /var/lib requires either an init script that creates the directory or a tmpfiles.d entry to provision it on first boot.

Container isolation. When the application runs inside a Docker container (the IOT2000 supports containerized workloads via the SIMATIC Edge runtime), the container's /sys/firmware/dmi must be bind-mounted or the container must run privileged. The default containerd configuration on the Siemens IOT2000 image includes the bind mount; verify with ls -l /sys/firmware/dmi/id/ from within the container.

Locale handling. The serial is a byte string in sysfs, not localized text. Reading with open(2) bypasses locale handling. Do not use std::string constructors that depend on the C locale; use raw read(2) into a std::vector<uint8_t> or a fixed-size C buffer.

Boot-time ordering. If the application is started before systemd-sysfs.service completes (early-boot init script), the sysfs DMI id files may not yet exist. Wait for the sys-firmware-dmi-id-device.path unit or poll the file for up to 30 seconds with a 200 ms interval before failing.

TPM 2.0 on IOT2040. The Apollo Lake SoC includes a discrete Infineon TPM 2.0 module. Confirm presence with ls /dev/tpm* and the tpm2_getcap properties-fixed command from the tpm2-tools package. For highest-value software, use tpm2_create to seal a key against PCR0 (the firmware-measurement PCR) and bind your license to that sealed blob.

Troubleshooting Matrix

Symptom Likely cause Diagnostic Resolution
Empty serial returned BIOS not populated or firmware < V2.1.0 on IOT2020 cat /sys/firmware/dmi/id/product_serial returns 0 bytes Update Siemens firmware via the IOT2000 SDK image
EPERM on /dev/mem CONFIG_STRICT_DEVMEM=y on hardened image zcat /proc/config.gz | grep STRICT_DEVMEM Fall back to Method 1 (sysfs) or rebuild kernel with CONFIG_STRICT_DEVMEM=n
ioctl ETHTOOL_GPERMADDR fails with EOPNOTSUPP NIC driver does not implement get_perm_addr ethtool -P <iface> returns permaddr: 00:00:00:00:00:00 or errors Use sysfs runtime MAC; for the IOT2000 stock drivers this affects only aftermarket USB-Ethernet adapters
open() returns EACCES on /sys/firmware/dmi/id/product_serial Hardened image restricts DMI id files to root ls -l /sys/firmware/dmi/id/ Run as root, grant CAP_DAC_READ_SEARCH, or wrap in a setuid helper
Container sees empty /sys/firmware/dmi Bind mount missing in container config ls /sys/firmware/dmi/ from inside the container Add -v /sys/firmware/dmi:/sys/firmware/dmi:ro to the docker run command
dmidecode returns 0 for serial SMBIOS Type 1 string index 0 (not present) dmidecode -t 1 shows "Serial Number: Not Specified" Hardware fault or unprovisioned unit; return to Siemens for replacement
Two IOT2000s have identical UUIDs Silicon batch with shared seed Compare cat /sys/firmware/dmi/id/product_uuid across fleet Use system serial as primary anchor, UUID as secondary
MAC read returns zeros NIC not yet up or driver not bound ip link show shows "NO-CARRIER" or interface missing Bring interface up with ip link set dev <iface> up before read

Build and Cross-Compilation for IOT2000

The Siemens SIMATIC IOT2000 SDK provides a Yocto SDK installer (for example, poky-glibc-x86_64-meta-toolchain-iottest-image-cortexa8hf-neon-toolchain-3.0.4.sh for older IOT2020 images; for the IOT2040 use the matching x86_64 Poky SDK). The host application is built for i586 on the IOT2020 and x86_64 on the IOT2040, then deployed via SCP, the SIMATIC Automation Workbench, or the IOT2000 Web-Panel. For applications that need OpenSSL HMAC, add openssl to the Yocto image's IMAGE_INSTALL list, or link statically against mbedTLS to reduce the on-device footprint.

For a bare-metal read with no external dependencies, the sysfs code in Methods 1 and 3 builds against the C standard library only and links with the Yocto toolchain's -static option to produce a fully self-contained binary suitable for initramfs or emergency-recovery environments.

Standards Reference

The SMBIOS Reference Specification is published by the DMTF as DSP0136; the current 3.6.0 revision is the authoritative source for the data structures parsed in Methods 1 and 2. SMBIOS 2.x entry points remain in widespread use on x86 industrial platforms and are documented in earlier editions of DSP0136.

RFC 4122 (and its 2024 update, RFC 9562) defines the textual representation of the 16-byte UUID exposed at /sys/firmware/dmi/id/product_uuid.

The Linux kernel DMI/SMBIOS driver documentation is included in the kernel source tree under Documentation/ABI/testing/sysfs-firmware-dmi and is mirrored in the kernel.org online documentation.

Frequently Asked Questions

Why is the IOT2000 serial number a stronger identifier than the MAC address?

The system serial is written to the SMBIOS Type 1 record by the BIOS firmware and is not re-writable from userspace. The MAC address, by contrast, can be changed at runtime with the standard ip link set dev <iface> address command and is therefore trivial to spoof for licensing bypass. For binding software to a device, prefer the SMBIOS serial; use ETHTOOL_GPERMADDR for the permanent MAC only as a secondary anchor.

Can the IOT2000 serial number be modified by a user?

On the IOT2020, the SMBIOS data lives in the firmware configuration space and is not writable from Linux without reflashing the BIOS. On the IOT2040, the AMI BIOS Setup exposes SMBIOS fields including the system serial under "Advanced → SMBIOS Information"; an attacker with physical, IPMI, or firmware-update access could change it. The serial is therefore resistant to remote software tampering on the IOT2020, but a determined attacker with shell access can defeat most software-only protections on either platform.

Is the dmidecode binary safe to call from a protected application?

No, on two counts. First, dmidecode is GPLv2, so closed-source applications that invoke it as part of their trust verification may become subject to the GPL's source-distribution obligations, depending on the degree of coupling. Second, dmidecode is the trust anchor for the very check you are trying to protect, so substituting a fake dmidecode that returns a chosen value defeats the check. Prefer direct sysfs reads or in-process SMBIOS parsing for production licensing code.

What file permissions are required to read /sys/firmware/dmi/id/product_serial?

On the default IOT2000 Yocto image the file is mode 0444 root:root, so any unprivileged user can read it. If a hardened variant has restricted it to root (mode 0400), the application must either run as root, be granted CAP_DAC_READ_SEARCH, or use the kernel's SMBIOS in-process parser behind a setuid wrapper. Verify with ls -l /sys/firmware/dmi/id/product_serial on the target image before deployment.

Should I store the hashed serial on the device or on a server?

For low-value software, a local hash in /var/lib/<app>/hwid.bin with file mode 0600 is sufficient and avoids the latency and connectivity dependency of a server check. For high-value software, prefer a server-issued license file signed by your private key and bound to the device's serial; on each launch the application sends a challenge (a random nonce plus a hash of the serial) and the server validates against the issued license. The IOT2040's Apollo Lake platform additionally supports TPM 2.0 sealing via the tpm2-tss stack, which binds the license key to the TPM's endorsement key and prevents cloning at the cost of additional setup and dependency on the discrete Infineon TPM present on most IOT2040 SKUs.

Back to blog