mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-21 11:46:37 +00:00
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:
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user