6d0f5c4b8f
Implements the reverse-engineered v8 credential token in pure managed code and
wires the full event-connection auth chain. Live result: the v8 OpenConnection
now AUTHENTICATES against the 2023 R2 server (past the 132/171 AuthenticationFailed
wall) — the crypto is solved.
- HistorianNativeHandshake.DeriveExchangeKeyClientKey: client key = SHA256(ECDH
shared secret) via ECDiffieHellman.DeriveKeyFromHash(SHA256), matching the native
ECDiffieHellmanCng{Hash,SHA256}.DeriveKeyMaterial.
- BuildExchangeKeyCredentialToken + Rc4: token = RC4(password-UTF16LE, key=MD5(clientKey)).
Reproduces a live-captured token EXACTLY (verified offline) — the native
HistorianCrypto.NRC4_V2.aahCryptV2 scheme (MD5-keyed RC4). Pure managed; nothing
AVEVA shipped. RC4 pinned by the standard test vector.
- OpenSession(eventConnection:true): ExchangeKey -> derive client key -> token ->
v8 OpenConnection with ConnectionType=Event + the token. Orchestrator re-armed.
- HistorianAddTagsProtocol.SerializeCmEventEnsureTagsGrpc: the 86-byte native gRPC
CM_EVENT EnsureTags (8-byte header + ...2f27 event-type GUID), replacing the
2020 WCF 83-byte CTagMetadata on the gRPC event registration.
Goldens: RC4 standard vector + token construction. 326/326 offline.
KNOWN REMAINING: the event query still returns zero rows (GetNext yields a 10-byte
zero-row buffer). Auth + StartEventQuery succeed; the query-layer detail (vs the
native row-returning capture) is the last step. Gated test still pins the no-row
throw; opt-in diagnostic (HISTORIAN_GRPC_EVENT_DIAG) surfaces the journey.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
348 lines
13 KiB
C#
348 lines
13 KiB
C#
using System.Text;
|
|
using AVEVA.Historian.Client.Wcf;
|
|
|
|
namespace AVEVA.Historian.Client.Tests;
|
|
|
|
public sealed class WcfOpen2ProtocolTests
|
|
{
|
|
[Fact]
|
|
public void Rc4MatchesStandardTestVector()
|
|
{
|
|
// Standard RC4 test vector (key "Key", plaintext "Plaintext" -> BBF316E8D940AF0AD3). Pins the
|
|
// RC4 used by the v8 credential token = RC4(password-UTF16LE, key=MD5(SHA256(ECDH secret))).
|
|
byte[] actual = HistorianNativeHandshake.Rc4(
|
|
System.Text.Encoding.ASCII.GetBytes("Key"),
|
|
System.Text.Encoding.ASCII.GetBytes("Plaintext"));
|
|
|
|
Assert.Equal(Convert.FromHexString("BBF316E8D940AF0AD3"), actual);
|
|
}
|
|
|
|
[Fact]
|
|
public void CredentialToken_IsRc4OfPasswordKeyedByMd5OfClientKey()
|
|
{
|
|
// token = RC4(password-UTF16LE, key = MD5(clientKey)). Verify against an independently computed
|
|
// reference for a fixed clientKey + password.
|
|
byte[] clientKey = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
|
|
byte[] token = HistorianNativeHandshake.BuildExchangeKeyCredentialToken(clientKey, "Pw1!");
|
|
|
|
byte[] rc4Key = System.Security.Cryptography.MD5.HashData(clientKey);
|
|
byte[] expected = HistorianNativeHandshake.Rc4(rc4Key, System.Text.Encoding.Unicode.GetBytes("Pw1!"));
|
|
Assert.Equal(expected, token);
|
|
Assert.Equal("Pw1!".Length * 2, token.Length); // UTF-16LE length
|
|
}
|
|
|
|
[Fact]
|
|
public void Version8EventSerializerReproducesCapturedNativeStructure()
|
|
{
|
|
// Field sizes chosen to match the live 2023 R2 Event-connection capture (302 bytes):
|
|
// username=12, token=26, serverNode=15, clientNode=32, datasource=16.
|
|
// See docs/reverse-engineering/grpc-event-query-capture.md.
|
|
var clientKey = new Guid("11223344-5566-7788-99aa-bbccddeeff00");
|
|
byte[] actual = HistorianOpen2Protocol.SerializeNativeOpenConnectionVersion8(
|
|
new HistorianClientCommonInfo(
|
|
FormatVersion: 3,
|
|
ServerNodeName: new string('S', 15),
|
|
ClientNodeName: new string('C', 32),
|
|
ProcessId: 0x11223344,
|
|
HcalVersion: 18,
|
|
ProcessName: string.Empty,
|
|
Proxy: string.Empty,
|
|
DataSourceId: new string('D', 16),
|
|
ShardId: new Guid(Enumerable.Repeat((byte)0xFF, 16).ToArray()),
|
|
ClientVersion: 999_999,
|
|
ClientTimestamp: 0x01DD02426F9B6F6C,
|
|
ClientDllVersion: string.Empty),
|
|
clientKey,
|
|
connectionType: 1, // Event
|
|
connectionFlag: 1, // Event
|
|
userName: new string('U', 12),
|
|
credentialToken: new byte[26]);
|
|
|
|
Assert.Equal(302, actual.Length); // exact native length for these field sizes
|
|
Assert.Equal(0x08, actual[0]); // format version 8
|
|
Assert.Equal(0xF0, actual[1]); // marker
|
|
Assert.Equal(0x01, actual[21]); // marker
|
|
Assert.Equal(clientKey.ToByteArray(), actual[22..38]);
|
|
Assert.Equal(0x04, actual[94]); // ClientType
|
|
Assert.Equal(0x01, actual[95]); // ConnectionType = Event
|
|
Assert.Equal(0x01, actual[96]); // type flag = Event
|
|
Assert.Equal([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], actual[270..286]); // ShardId
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyVersion1SerializerMatchesDecompiledSaveOpenConnectionParamsLayout()
|
|
{
|
|
byte[] actual = HistorianOpen2Protocol.SerializeLegacyVersion1(new HistorianOpen2Request(
|
|
HostName: "H",
|
|
ProcessName: "P",
|
|
ProcessId: 0x01020304,
|
|
UserName: "U",
|
|
Password: Encoding.Unicode.GetBytes("pw"),
|
|
ClientType: 4,
|
|
ClientVersion: 11,
|
|
ConnectionMode: 2,
|
|
MetadataNamespace: HistorianMetadataNamespace.Empty));
|
|
|
|
byte[] expected =
|
|
[
|
|
0x01, 0x00,
|
|
0x01, 0x00, 0x00, 0x00, 0x48, 0x00,
|
|
0x01, 0x00, 0x00, 0x00, 0x50, 0x00,
|
|
0x04, 0x03, 0x02, 0x01,
|
|
0x01, 0x00, 0x00, 0x00, 0x55, 0x00,
|
|
0x04, 0x00, 0x00, 0x00, 0x70, 0x00, 0x77, 0x00,
|
|
0x04,
|
|
0x0B, 0x00,
|
|
0x02, 0x00, 0x00, 0x00,
|
|
0x01,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00
|
|
];
|
|
|
|
Assert.Equal(expected, actual);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyVersion1SerializerUsesUtf16CodeUnitStringLengths()
|
|
{
|
|
byte[] actual = HistorianOpen2Protocol.SerializeLegacyVersion1(new HistorianOpen2Request(
|
|
HostName: "A\ud83d\ude00",
|
|
ProcessName: string.Empty,
|
|
ProcessId: 0,
|
|
UserName: string.Empty,
|
|
Password: [],
|
|
ClientType: 4,
|
|
ClientVersion: 0,
|
|
ConnectionMode: 2,
|
|
MetadataNamespace: HistorianMetadataNamespace.Empty));
|
|
|
|
Assert.Equal([0x03, 0x00, 0x00, 0x00], actual[2..6]);
|
|
Assert.Equal(Encoding.Unicode.GetBytes("A\ud83d\ude00"), actual[6..12]);
|
|
}
|
|
|
|
[Fact]
|
|
public void NativeErrorParserReadsObservedFiveByteBuffers()
|
|
{
|
|
HistorianNativeError? error = HistorianOpen2Protocol.TryReadNativeError([0x04, 0xAB, 0x00, 0x00, 0x00]);
|
|
|
|
Assert.NotNull(error);
|
|
Assert.Equal(4, error.Type);
|
|
Assert.Equal<uint>(171, error.Code);
|
|
Assert.Equal("AuthenticationFailed", error.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void NativeErrorParserRejectsShortBuffers()
|
|
{
|
|
Assert.Null(HistorianOpen2Protocol.TryReadNativeError([0x04, 0xAB, 0x00, 0x00]));
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyOpen2OutputParserReadsObservedWcfLayout()
|
|
{
|
|
byte[] buffer =
|
|
[
|
|
0x78, 0x56, 0x34, 0x12,
|
|
0x33, 0x22, 0x11, 0x00,
|
|
0x55, 0x44,
|
|
0x77, 0x66,
|
|
0x88, 0x99, 0xAA, 0xBB,
|
|
0xCC, 0xDD, 0xEE, 0xFF,
|
|
0x08, 0x07, 0x06, 0x05,
|
|
0x04, 0x03, 0x02, 0x01,
|
|
0x44, 0x33, 0x22, 0x11
|
|
];
|
|
|
|
HistorianLegacyOpen2Output? output = HistorianOpen2Protocol.TryReadLegacyOpen2Output(buffer);
|
|
|
|
Assert.NotNull(output);
|
|
Assert.Equal<uint>(0x12345678, output.Handle);
|
|
Assert.Equal(new Guid("00112233-4455-6677-8899-aabbccddeeff"), output.StorageSessionId);
|
|
Assert.Equal(0x0102030405060708, output.ConnectTimeFileTimeUtc);
|
|
Assert.Equal<uint>(0x11223344, output.ServerStatus);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyOpen2OutputParserRejectsNonLegacyLength()
|
|
{
|
|
Assert.Null(HistorianOpen2Protocol.TryReadLegacyOpen2Output([0x00]));
|
|
}
|
|
|
|
[Fact]
|
|
public void NativeOpen3OutputParserReadsObservedDeserializerLayout()
|
|
{
|
|
byte[] buffer =
|
|
[
|
|
0x03,
|
|
0x78, 0x56, 0x34, 0x12,
|
|
0x33, 0x22, 0x11, 0x00,
|
|
0x55, 0x44,
|
|
0x77, 0x66,
|
|
0x88, 0x99, 0xAA, 0xBB,
|
|
0xCC, 0xDD, 0xEE, 0xFF,
|
|
0x08, 0x07, 0x06, 0x05,
|
|
0x04, 0x03, 0x02, 0x01,
|
|
0x18, 0x17, 0x16, 0x15,
|
|
0x14, 0x13, 0x12, 0x11,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x00
|
|
];
|
|
|
|
HistorianNativeOpen3Output? output = HistorianOpen2Protocol.TryReadNativeOpen3Output(buffer);
|
|
|
|
Assert.NotNull(output);
|
|
Assert.Equal(3, output.ProtocolVersion);
|
|
Assert.Equal<uint>(0x12345678, output.Handle);
|
|
Assert.Equal(new Guid("00112233-4455-6677-8899-aabbccddeeff"), output.StorageSessionId);
|
|
Assert.Equal(0x0102030405060708, output.ConnectTimeFileTimeUtc);
|
|
Assert.Equal(0x1112131415161718, output.ServerTimeFileTimeUtc);
|
|
Assert.Equal([0x44, 0x33, 0x22, 0x11, 0x00], output.TrailingBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void NativeOpen3OutputParserRejectsUnsupportedVersion()
|
|
{
|
|
Assert.Null(HistorianOpen2Protocol.TryReadNativeOpen3Output([0x01, 0x00, 0x00, 0x00]));
|
|
}
|
|
|
|
[Fact]
|
|
public void NativeVersion3SerializerMatchesDecompiledFieldOrder()
|
|
{
|
|
byte[] actual = HistorianOpen2Protocol.SerializeNativeVersion3(
|
|
new HistorianOpen2Request(
|
|
HostName: "H",
|
|
ProcessName: "P",
|
|
ProcessId: 0x01020304,
|
|
UserName: string.Empty,
|
|
Password: [0xAA, 0xBB],
|
|
ClientType: 4,
|
|
ClientVersion: 11,
|
|
ConnectionMode: 1026,
|
|
MetadataNamespace: HistorianMetadataNamespace.Empty),
|
|
new HistorianClientCommonInfo(
|
|
FormatVersion: 3,
|
|
ServerNodeName: "S",
|
|
ClientNodeName: "C",
|
|
ProcessId: 0x11223344,
|
|
HcalVersion: 17,
|
|
ProcessName: "Proc",
|
|
Proxy: string.Empty,
|
|
DataSourceId: string.Empty,
|
|
ShardId: new Guid("00112233-4455-6677-8899-aabbccddeeff"),
|
|
ClientVersion: 0x55667788,
|
|
ClientTimestamp: 0x0102030405060708,
|
|
ClientDllVersion: string.Empty));
|
|
|
|
byte[] expectedPrefix =
|
|
[
|
|
0x03,
|
|
0x01, 0x00, 0x00, 0x00, 0x48, 0x00,
|
|
0x02, 0x00, 0xAA, 0xBB,
|
|
0x04,
|
|
0x02, 0x04, 0x00, 0x00
|
|
];
|
|
Assert.Equal(expectedPrefix, actual[..expectedPrefix.Length]);
|
|
|
|
Assert.Contains<byte>(0x03, actual);
|
|
byte[] expectedSuffix = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
|
|
Assert.Equal(expectedSuffix, actual[^expectedSuffix.Length..]);
|
|
}
|
|
|
|
[Fact]
|
|
public void NativeOpenConnection3Version6SerializerAddsObservedPrefixBeforeContent()
|
|
{
|
|
HistorianOpen2Request request = new(
|
|
HostName: "H",
|
|
ProcessName: "P",
|
|
ProcessId: 0x01020304,
|
|
UserName: string.Empty,
|
|
Password: [0xAA, 0xBB],
|
|
ClientType: 4,
|
|
ClientVersion: 11,
|
|
ConnectionMode: 1026,
|
|
MetadataNamespace: HistorianMetadataNamespace.Empty);
|
|
HistorianClientCommonInfo commonInfo = new(
|
|
FormatVersion: 3,
|
|
ServerNodeName: "S",
|
|
ClientNodeName: "C",
|
|
ProcessId: 0x11223344,
|
|
HcalVersion: 17,
|
|
ProcessName: "Proc",
|
|
Proxy: string.Empty,
|
|
DataSourceId: string.Empty,
|
|
ShardId: new Guid("00112233-4455-6677-8899-aabbccddeeff"),
|
|
ClientVersion: 0x55667788,
|
|
ClientTimestamp: 0x0102030405060708,
|
|
ClientDllVersion: string.Empty);
|
|
|
|
byte[] actual = HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6(
|
|
request,
|
|
commonInfo,
|
|
new Guid("00112233-4455-6677-8899-aabbccddeeff"));
|
|
|
|
byte[] expectedPrefix =
|
|
[
|
|
0x06,
|
|
0x33, 0x22, 0x11, 0x00,
|
|
0x55, 0x44,
|
|
0x77, 0x66,
|
|
0x88, 0x99, 0xAA, 0xBB,
|
|
0xCC, 0xDD, 0xEE, 0xFF,
|
|
0x00
|
|
];
|
|
Assert.Equal(expectedPrefix, actual[..expectedPrefix.Length]);
|
|
byte[] expectedContentPrefix =
|
|
[
|
|
0x01, 0x00, 0x00, 0x00, 0x48, 0x00,
|
|
0x02, 0x00, 0xAA, 0xBB,
|
|
0x04,
|
|
0x02, 0x04, 0x00, 0x00,
|
|
0x01,
|
|
0x01, 0x00, 0x00,
|
|
0x01, 0x00, 0x00,
|
|
0x01, 0x00, 0x00
|
|
];
|
|
Assert.Equal(expectedContentPrefix, actual[expectedPrefix.Length..(expectedPrefix.Length + expectedContentPrefix.Length)]);
|
|
}
|
|
|
|
[Fact]
|
|
public void NativeOpenConnection3Version6SerializerCanUseSeparateCredentialBlock()
|
|
{
|
|
HistorianOpen2Request request = new(
|
|
HostName: "H",
|
|
ProcessName: "P",
|
|
ProcessId: 0x01020304,
|
|
UserName: string.Empty,
|
|
Password: [0xAA, 0xBB],
|
|
ClientType: 4,
|
|
ClientVersion: 11,
|
|
ConnectionMode: 1026,
|
|
MetadataNamespace: HistorianMetadataNamespace.Empty);
|
|
HistorianClientCommonInfo commonInfo = new(
|
|
FormatVersion: 2,
|
|
ServerNodeName: string.Empty,
|
|
ClientNodeName: string.Empty,
|
|
ProcessId: 0,
|
|
HcalVersion: 17,
|
|
ProcessName: string.Empty,
|
|
Proxy: string.Empty,
|
|
DataSourceId: string.Empty,
|
|
ShardId: Guid.Empty,
|
|
ClientVersion: 0,
|
|
ClientTimestamp: 0,
|
|
ClientDllVersion: string.Empty);
|
|
|
|
byte[] actual = HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6(
|
|
request,
|
|
commonInfo,
|
|
Guid.Empty,
|
|
[0x00, 0x00, 0x00, 0x00]);
|
|
|
|
int hostLengthOffset = 18;
|
|
int credentialLengthOffset = hostLengthOffset + 4 + Encoding.Unicode.GetByteCount("H");
|
|
Assert.Equal([0x04, 0x00], actual[credentialLengthOffset..(credentialLengthOffset + 2)]);
|
|
Assert.Equal([0x00, 0x00, 0x00, 0x00], actual[(credentialLengthOffset + 2)..(credentialLengthOffset + 6)]);
|
|
}
|
|
}
|