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:
Joseph Doherty
2026-06-23 10:31:37 -04:00
parent 7284fdc976
commit d67f6f5e96
4 changed files with 111 additions and 18 deletions
@@ -138,11 +138,12 @@ internal sealed class HistorianGrpcEventOrchestrator
private List<HistorianEvent> RunEventChain(DateTime startUtc, DateTime endUtc, HistorianEventFilter? filter, CancellationToken cancellationToken)
{
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options);
// NOTE: the event read needs an Event-type (v8) connection, but the v8 OpenConnection is
// server-coupled to HistoryService.ExchangeKey (ECDH) — it rejects a v8 open made on our
// ValidateClientCredential session with native 132/34 "EstablishConnection Failed to get client
// key" (Path A, disproven 2026-06-23). Staying on the v6 session until ExchangeKey is
// implemented (Path B). The v8 serializer + BuildEventOpenConnectionVersion8Request are ready.
// Event reads need an Event-type (v8) connection. Path B established the v8 ExchangeKey (ECDH)
// client key — which cleared the "Failed to get client key" check — but the v8 OpenConnection
// then fails at native 132/171 AuthenticationFailed: the 26-byte credential token must be derived
// from the ECDH shared secret (the token-KDF is not yet reverse-engineered). Staying on the v6
// session until that derivation lands; the ExchangeKey + v8 serializer are ready (set
// eventConnection: true to re-arm). See docs/reverse-engineering/grpc-event-query-capture.md.
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, _options, cancellationToken);
RegisterCmEventTag(connection, session, cancellationToken);