fix(worker): resilient failover switch; FIPS-safe synthetic GUID; dup-reference guard + tests (Worker-026..028, Worker.Tests-031..033)

This commit is contained in:
Joseph Doherty
2026-06-15 02:56:15 -04:00
parent ddf2d84fbc
commit cebe67e9bd
8 changed files with 584 additions and 20 deletions
@@ -1,5 +1,4 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
@@ -11,8 +10,29 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
/// repeated transitions for the same alarm reference correlate downstream
/// (acknowledge, snapshot, OPC UA mapping) without an alarmmgr-supplied GUID.
/// </summary>
/// <remarks>
/// The 128-bit value is computed with a fixed FNV-1a hash over the UTF-8
/// bytes of the reference, deliberately <strong>not</strong> via
/// <c>System.Security.Cryptography</c>. On .NET Framework 4.8
/// <c>MD5.Create()</c> returns the non-validated
/// <c>MD5CryptoServiceProvider</c>, whose constructor throws under the
/// Windows FIPS-compliance policy ("not part of the Windows Platform FIPS
/// validated cryptographic algorithms"). Because this derivation needs only
/// determinism and distinctness — never cryptographic strength — a plain
/// non-crypto hash avoids the FIPS gate entirely, so the subtag fallback
/// keeps working on regulated (FIPS-enabled) hosts exactly when it is needed.
/// </remarks>
internal static class SyntheticAlarmGuid
{
// 64-bit FNV-1a constants (RFC-style; widely used reference values).
private const ulong FnvOffsetBasis = 14695981039346656037UL;
private const ulong FnvPrime = 1099511628211UL;
// A second independent seed for the high 8 bytes so the full 128-bit value
// is well-distributed across distinct references rather than two correlated
// halves of the same single-pass hash.
private const ulong FnvSecondSeed = 1469598103934665603UL;
/// <summary>
/// Produces a stable <see cref="Guid"/> for the given alarm reference.
/// The same reference always maps to the same GUID; distinct references
@@ -32,11 +52,39 @@ internal static class SyntheticAlarmGuid
byte[] bytes = Encoding.UTF8.GetBytes(reference);
// MD5 is used purely for a stable, non-cryptographic identity mapping
// (reference -> 16-byte GUID), never for security. Its 128-bit output
// fits a GUID exactly, which is why it is preferred here.
using MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(bytes);
return new Guid(hash);
// Two independent FNV-1a passes fill the low and high 64 bits of the
// 128-bit value. The second pass mixes the running length into its seed
// so single-character differences and re-orderings still diverge in both
// halves, avoiding correlated-half collisions a single pass would risk.
ulong low = FnvOffsetBasis;
ulong high = FnvSecondSeed;
for (int i = 0; i < bytes.Length; i++)
{
byte b = bytes[i];
low ^= b;
low *= FnvPrime;
high ^= unchecked(b + (ulong)i);
high *= FnvPrime;
}
// Fold the length in so the empty string and other short inputs are not
// degenerate (an all-zero / Guid.Empty result is undesirable downstream).
low ^= (ulong)bytes.Length;
low *= FnvPrime;
byte[] guidBytes = new byte[16];
WriteUInt64(guidBytes, 0, low);
WriteUInt64(guidBytes, 8, high);
return new Guid(guidBytes);
}
private static void WriteUInt64(byte[] buffer, int offset, ulong value)
{
for (int i = 0; i < 8; i++)
{
buffer[offset + i] = (byte)(value >> (i * 8));
}
}
}