diff --git a/spinifex/utils/utils.go b/spinifex/utils/utils.go index 0d6067856..f655ee12a 100644 --- a/spinifex/utils/utils.go +++ b/spinifex/utils/utils.go @@ -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() } diff --git a/spinifex/utils/utils_test.go b/spinifex/utils/utils_test.go index 4435dc521..46984b94e 100644 --- a/spinifex/utils/utils_test.go +++ b/spinifex/utils/utils_test.go @@ -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]) } }