[M5] auth: deterministic HMAC fixture test rules out crypto stack
Adds end-to-end byte-equality test against a `.NET reference fixture captured via the new `MxAsbClient.Probe --dump-deterministic-hmac` flag. All inputs are pinned (passphrase, prime, generator, private- key bytes, remote-pub bytes, message number, connection ID, AES IV, consumer-data + IV bytes), so the test reproduces .NET's exact output for every crypto step: 1. shared = remote_pub^private_key mod prime — ✅ matches 2. crypto_key = shared || passphrase_utf8 — ✅ matches 3. hmac = HMAC-SHA1(crypto_key, xml_utf8) — ✅ matches 4. aes_key = PBKDF2-SHA1(base64(crypto_key), salt, 1000, 16) — ✅ 5. encrypted_mac = AES-CBC(aes_key, iv=zeros, hmac, PKCS7) — ✅ This conclusively rules out the entire crypto stack as the source of the live AuthenticateMe `dispatcher/fault`. Our DH math, HMAC engine, PBKDF2 derivation, AES-CBC PKCS7, and crypto_key concatenation are byte-equal to .NET. The remaining live failure must come from one of: (a) wire-level ConnectionValidator NBFX shape (DataContract field names, mustUnderstand attribute, namespace), (b) WCF binary message header (action+to dict pre-pop), or (c) a subtle XmlSerializer quirk for live values that the hardcoded fixtures don't exercise (Guid format edge case, base64 line wrapping, ulong text rendering). Fixture lives at `crates/mxaccess-asb-nettcp/tests/fixtures/ deterministic-hmac/authenticate-me.kv` (KV format, ASCII hex, lines trim CRLF/LF transparently). The companion `README.md` documents the capture procedure and the per-step decomposition. The test consumes the .NET-supplied canonical XML directly from the fixture's `xml_utf8_b64` so a Rust XML emitter bug would not mask a Rust crypto bug — XML byte-equality is verified separately by `mxaccess-asb::xml_canonical::tests` against the `signed-xml/*.xml` fixtures. Workspace: 710 unit tests pass (was 709 + 1 new). Clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -166,6 +166,136 @@ if (args.Any(arg => arg.Equals("--dump-signed-xml", StringComparison.OrdinalIgno
|
||||
return;
|
||||
}
|
||||
|
||||
// `--dump-deterministic-hmac` runs the AuthenticateMe sign path with
|
||||
// FIXED inputs end-to-end (no randomness): pinned passphrase, prime,
|
||||
// generator, private-key bytes, remote-pub bytes, connection ID,
|
||||
// message number, AES IV, and consumer-data/IV bytes. Output is the
|
||||
// resulting crypto_key, AES key, canonical XML, HMAC-SHA1, and
|
||||
// AES-CBC-encrypted MAC. The Rust port uses these as a fixture for a
|
||||
// byte-equality unit test that localises any HMAC/AES/derivation
|
||||
// divergence vs the .NET reference without depending on session
|
||||
// randomness. Mirrors the per-step decomposition of `AsbSystemAuthent
|
||||
// icator.Sign` (`AsbSystemAuthenticator.cs:62-82`) but inlines the
|
||||
// math so we control every byte of input.
|
||||
if (args.Any(arg => arg.Equals("--dump-deterministic-hmac", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
System.Numerics.BigInteger prime = System.Numerics.BigInteger.Parse(AsbSolutionCryptoParameters.DefaultPrimeText);
|
||||
System.Numerics.BigInteger generator = 22;
|
||||
|
||||
// 33 bytes: 0x01..0x20 with trailing 0x00 sign byte. Mirrors the
|
||||
// shape `AsbSystemAuthenticator.CreatePrivateKey` produces.
|
||||
byte[] privateKeyBytes = new byte[33];
|
||||
for (int i = 0; i < 32; i++) { privateKeyBytes[i] = (byte)(i + 1); }
|
||||
privateKeyBytes[32] = 0x00;
|
||||
|
||||
// Remote public key — 128 bytes (1024-bit), high bit clear so
|
||||
// .NET's BigInteger LE-two's-complement reads it as positive
|
||||
// without a sign-byte fix-up.
|
||||
byte[] remotePub = new byte[128];
|
||||
for (int i = 0; i < 127; i++) { remotePub[i] = (byte)((i * 7 + 13) & 0xFF); }
|
||||
remotePub[127] = 0x7F;
|
||||
|
||||
string passphrase = "deterministic-hmac-fixture-passphrase-rust-vs-dotnet";
|
||||
Guid connectionId = Guid.Parse("8cba964a-74c1-ef74-f6aa-761b3540191b");
|
||||
ulong messageNumber = 42;
|
||||
|
||||
// ConsumerAuthenticationData payload. Encrypted bytes are opaque
|
||||
// to the HMAC test (they get base64-embedded in the XML and
|
||||
// signed); use deterministic bytes 0x80..0xFF + 0x00..0x4F (208
|
||||
// bytes — same as a real AuthenticateMe under a 768-bit prime).
|
||||
byte[] consumerData = new byte[208];
|
||||
for (int i = 0; i < 208; i++) { consumerData[i] = (byte)((i * 3 + 7) & 0xFF); }
|
||||
byte[] consumerIv = new byte[16];
|
||||
for (int i = 0; i < 16; i++) { consumerIv[i] = (byte)((i * 11 + 5) & 0xFF); }
|
||||
|
||||
// Deterministic AES IV for encrypting the HMAC. We pick all-zeros
|
||||
// so the Rust test can reproduce without a random-IV injection
|
||||
// hack. (The real wire path uses a random IV per call; here we
|
||||
// bypass that to make the test reproducible.)
|
||||
byte[] aesIv = new byte[16];
|
||||
|
||||
// ---- crypto_key = shared_secret || passphrase_utf8 ----------
|
||||
System.Numerics.BigInteger sharedValue = System.Numerics.BigInteger.ModPow(
|
||||
new System.Numerics.BigInteger(remotePub),
|
||||
new System.Numerics.BigInteger(privateKeyBytes),
|
||||
prime);
|
||||
byte[] shared = sharedValue.ToByteArray();
|
||||
byte[] cryptoKey = [.. shared, .. System.Text.Encoding.UTF8.GetBytes(passphrase)];
|
||||
|
||||
// ---- canonical XML (empty MAC + IV) -------------------------
|
||||
AuthenticateMe req = new()
|
||||
{
|
||||
ConnectionValidator = new()
|
||||
{
|
||||
ConnectionId = connectionId,
|
||||
MessageNumber = messageNumber,
|
||||
MessageAuthenticationCode = [],
|
||||
SignatureInitializationVector = [],
|
||||
},
|
||||
ConsumerAuthenticationData = new AuthenticationData
|
||||
{
|
||||
Data = consumerData,
|
||||
InitializationVector = consumerIv,
|
||||
},
|
||||
};
|
||||
string xmlText = req.ToXml();
|
||||
byte[] xmlBytes = System.Text.Encoding.UTF8.GetBytes(xmlText);
|
||||
|
||||
// ---- HMAC-SHA1(crypto_key, xml_utf8) ------------------------
|
||||
using System.Security.Cryptography.HMACSHA1 hmac = new(cryptoKey);
|
||||
byte[] hash = hmac.ComputeHash(xmlBytes);
|
||||
|
||||
// ---- AES key = PBKDF2-SHA1(base64(crypto_key), salt, 1000) --
|
||||
byte[] salt = System.Text.Encoding.ASCII.GetBytes("ArchestrAService");
|
||||
byte[] aesKey = System.Security.Cryptography.Rfc2898DeriveBytes.Pbkdf2(
|
||||
Convert.ToBase64String(cryptoKey),
|
||||
salt,
|
||||
iterations: 1000,
|
||||
System.Security.Cryptography.HashAlgorithmName.SHA1,
|
||||
outputLength: 16);
|
||||
|
||||
// ---- AES-CBC encrypt(hash) with fixed IV --------------------
|
||||
byte[] encryptedMac;
|
||||
using (System.Security.Cryptography.Aes aes = System.Security.Cryptography.Aes.Create())
|
||||
{
|
||||
aes.Key = aesKey;
|
||||
aes.IV = aesIv;
|
||||
// CBC mode, PKCS7 padding (defaults).
|
||||
using System.IO.MemoryStream ms = new();
|
||||
using (System.Security.Cryptography.CryptoStream cs = new(
|
||||
ms,
|
||||
aes.CreateEncryptor(),
|
||||
System.Security.Cryptography.CryptoStreamMode.Write))
|
||||
{
|
||||
cs.Write(hash, 0, hash.Length);
|
||||
}
|
||||
encryptedMac = ms.ToArray();
|
||||
}
|
||||
|
||||
Console.WriteLine("# deterministic-hmac fixture (.NET reference output)");
|
||||
Console.WriteLine($"prime_decimal={prime}");
|
||||
Console.WriteLine($"generator={generator}");
|
||||
Console.WriteLine($"private_key_hex={Convert.ToHexString(privateKeyBytes)}");
|
||||
Console.WriteLine($"remote_pub_hex={Convert.ToHexString(remotePub)}");
|
||||
Console.WriteLine($"passphrase={passphrase}");
|
||||
Console.WriteLine($"connection_id={connectionId:D}");
|
||||
Console.WriteLine($"message_number={messageNumber}");
|
||||
Console.WriteLine($"consumer_data_hex={Convert.ToHexString(consumerData)}");
|
||||
Console.WriteLine($"consumer_iv_hex={Convert.ToHexString(consumerIv)}");
|
||||
Console.WriteLine($"aes_iv_hex={Convert.ToHexString(aesIv)}");
|
||||
Console.WriteLine($"shared_secret_hex={Convert.ToHexString(shared)}");
|
||||
Console.WriteLine($"shared_secret_len={shared.Length}");
|
||||
Console.WriteLine($"crypto_key_hex={Convert.ToHexString(cryptoKey)}");
|
||||
Console.WriteLine($"crypto_key_len={cryptoKey.Length}");
|
||||
Console.WriteLine($"xml_utf8_len={xmlBytes.Length}");
|
||||
Console.WriteLine($"xml_utf8_b64={Convert.ToBase64String(xmlBytes)}");
|
||||
Console.WriteLine($"hmac_sha1_hex={Convert.ToHexString(hash)}");
|
||||
Console.WriteLine($"aes_key_hex={Convert.ToHexString(aesKey)}");
|
||||
Console.WriteLine($"encrypted_mac_hex={Convert.ToHexString(encryptedMac)}");
|
||||
Console.WriteLine($"encrypted_mac_len={encryptedMac.Length}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (probeConnectFailure)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user