fix: pin HashMAC first octet to 0x02

Previous impl forced only the IEEE 802 reserved bits (bit0=0 unicast,
bit1=1 LAA) on the first octet, leaving the upper 6 bits hash-derived.
Technically valid LAA unicast, but produced MACs like 4a:.. and fe:..
that visually encroach on vendor OUI space and tripped CI/ops checks
expecting all generated MACs to start with 02:.

Pin byte 0 to literal 0x02; hash bytes 1-5 from sha256[0:5]. Output
entropy drops from 46 to 40 bits — birthday-paradox 1% collision at
~150k ids, still 256x the old 24-bit bound (~580).
This commit is contained in:
joshsiv-mulga
2026-04-28 16:59:33 +10:00
parent f4d9c6c1a9
commit 7bfe3b2366
2 changed files with 13 additions and 23 deletions
+7 -5
View File
@@ -305,9 +305,11 @@ func humanBytes(b uint64) string {
}
// HashMAC returns a deterministic locally-administered unicast MAC derived
// from id. Output is "02:xx:xx:xx:xx:xx" with 46 bits of SHA-256-derived
// entropy in the low bits of the first octet plus the next 5 octets, and
// the IEEE 802 reserved bits forced (bit0=0 unicast, bit1=1 LAA).
// from id. Output is always "02:xx:xx:xx:xx:xx" — first octet is pinned to
// 0x02 (bit0=0 unicast, bit1=1 LAA, all other bits zero) so generated MACs
// are immediately recognisable as ours and cannot collide with registered
// vendor OUI space. The remaining 40 bits are SHA-256-derived. Birthday-
// paradox 1% collision at ~150k ids, 50% at ~1.2M.
//
// id must be globally unique across all MACs the deployment produces. No
// per-deployment salt is mixed in. Callers that share a base id between
@@ -317,8 +319,8 @@ func humanBytes(b uint64) string {
func HashMAC(id string) string {
sum := sha256.Sum256([]byte(id))
b := make([]byte, 6)
copy(b, sum[:6])
b[0] = (b[0] & 0xFC) | 0x02 // bit0=0 unicast, bit1=1 locally-administered
b[0] = 0x02
copy(b[1:], sum[:5])
return net.HardwareAddr(b).String()
}
+6 -18
View File
@@ -1004,27 +1004,15 @@ func TestReadPidFileFrom_EmptyDir(t *testing.T) {
assert.Error(t, err)
}
func TestHashMAC_FirstOctetVariesByInput(t *testing.T) {
// Old 24-bit impl pinned first octet to literal prefix byte (always
// 0x02). New 46-bit impl derives first octet from the hash, so it
// spans many values across distinct inputs.
seen := map[byte]struct{}{}
func TestHashMAC_FirstOctetPinnedTo02(t *testing.T) {
// First octet must be literal 0x02 across all inputs. Anything else
// (e.g. 0x4a, 0xfe — technically valid LAA) visually encroaches on
// vendor OUI space and is rejected by ops/CI. Second-octet entropy is
// covered by the distribution test.
for i := range 1000 {
hw, err := net.ParseMAC(HashMAC(fmt.Sprintf("id-%d", i)))
require.NoError(t, err)
seen[hw[0]] = struct{}{}
}
assert.Greater(t, len(seen), 10, "first octet should vary across inputs")
}
func TestHashMAC_LocallyAdministered(t *testing.T) {
// First octet must always have bit0=0 (unicast) and bit1=1 (LAA) per
// IEEE 802 reserved bits, regardless of hash output.
for i := range 1000 {
hw, err := net.ParseMAC(HashMAC(fmt.Sprintf("id-%d", i)))
require.NoError(t, err)
assert.Equal(t, byte(0x02), hw[0]&0x03,
"first octet %#x must be unicast+LAA", hw[0])
assert.Equal(t, byte(0x02), hw[0], "first octet must be 0x02, got %#x", hw[0])
}
}