feat(grpc-events): ExchangeKey ECDH (Path B) — clears the v8 client-key check
Implements HistoryService.ExchangeKey as a pure-managed P-256 ECDH key exchange and wires it ahead of the v8 Event OpenConnection. - HistorianNativeHandshake.BuildExchangeKeyClientHello / DeriveExchangeKeySecret: .NET ECDiffieHellman (nistP256); wire format "ECK1" + u32(32) + X(32) + Y(32), decoded from the live capture. No native AVEVA dependency. - HistorianGrpcHandshake.OpenSession(eventConnection: true): runs ExchangeKey on the context-key handle before the v8 OpenConnection. - Guardrail HistorianGrpcHandshakeRoutingTests scoped to the token-loop closure: still pins that the Negotiate token loop routes to ValidateClientCredential (not ExchangeKey), while allowing the legitimate ExchangeKey call in OpenSession. Live result: ExchangeKey succeeds (server accepts our public key) and the v8 OpenConnection error advances from 132/34 "Failed to get client key" to 132/171 AuthenticationFailed — the ECDH cleared the client-key layer. The remaining blocker is the 26-byte v8 credential token, which must be derived from the ECDH shared secret (token KDF, not yet recovered). Orchestrator stays on v6 (set eventConnection: true to re-arm once the KDF lands). 323/323 offline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
@@ -18,26 +18,31 @@ namespace AVEVA.Historian.Client.Tests;
|
||||
public sealed class HistorianGrpcHandshakeRoutingTests
|
||||
{
|
||||
[Fact]
|
||||
public void Handshake_UsesValidateClientCredential_NotExchangeKey()
|
||||
public void Handshake_TokenLoop_UsesValidateClientCredential_NotExchangeKey()
|
||||
{
|
||||
// The auth token loop lives in the shared handshake helper (reused by the read, status,
|
||||
// and future browse/metadata gRPC paths).
|
||||
HashSet<string> calledMethods = CollectCalledMethodNames(
|
||||
"AVEVA.Historian.Client.Grpc.HistorianGrpcHandshake");
|
||||
// The auth token loop is a compiler-generated lambda (nested closure) passed to RunTokenRounds.
|
||||
// It MUST carry the SSPI/Negotiate token via StorageService.ValidateClientCredential, never via
|
||||
// HistoryService.ExchangeKey (the earlier regression). ExchangeKey is now legitimately called
|
||||
// directly in OpenSession for the SEPARATE v8 event-connection key exchange (ECDH) — that is
|
||||
// allowed, so the guardrail scopes the no-ExchangeKey rule to the token-loop closures only.
|
||||
HashSet<string> tokenLoopCalls = CollectCalledMethodNames(
|
||||
"AVEVA.Historian.Client.Grpc.HistorianGrpcHandshake", nestedClosuresOnly: true);
|
||||
|
||||
Assert.Contains("ValidateClientCredential", calledMethods);
|
||||
Assert.DoesNotContain("ExchangeKey", calledMethods);
|
||||
Assert.Contains("ValidateClientCredential", tokenLoopCalls);
|
||||
Assert.DoesNotContain("ExchangeKey", tokenLoopCalls);
|
||||
}
|
||||
|
||||
private static HashSet<string> CollectCalledMethodNames(string typeFullName)
|
||||
private static HashSet<string> CollectCalledMethodNames(string typeFullName, bool nestedClosuresOnly = false)
|
||||
{
|
||||
Assembly sdk = typeof(HistorianClientOptions).Assembly;
|
||||
Type orchestrator = sdk.GetType(typeFullName, throwOnError: true)!;
|
||||
Module module = orchestrator.Module;
|
||||
|
||||
// The orchestrator type plus its compiler-generated nested types (lambda closures).
|
||||
IEnumerable<Type> types = new[] { orchestrator }
|
||||
.Concat(orchestrator.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic));
|
||||
// The compiler-generated nested types (lambda closures) — optionally with the orchestrator type
|
||||
// itself. The token loop lives inside a closure; ExchangeKey (event key exchange) lives in the
|
||||
// OpenSession body, so scoping to closures isolates the token-loop routing.
|
||||
IEnumerable<Type> nested = orchestrator.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic);
|
||||
IEnumerable<Type> types = nestedClosuresOnly ? nested : new[] { orchestrator }.Concat(nested);
|
||||
|
||||
var names = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (Type t in types)
|
||||
|
||||
Reference in New Issue
Block a user