fix(mesh): Batch B review fixes — bounded store read, cache poison guard, test hardening

This commit is contained in:
Joseph Doherty
2026-08-06 04:10:03 -04:00
parent 68a416e4fc
commit 24e55d3395
7 changed files with 314 additions and 68 deletions
@@ -43,6 +43,12 @@ namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
/// unavoidably observable and are not sensitive.
/// </para>
/// <para>
/// Header reads go through <see cref="GrpcRequestHeaders"/>, shared with
/// <see cref="CentralCommandAuthInterceptor"/> — the two are clones, and header parsing is
/// the one place a clone pair can diverge into two different answers about which of several
/// <c>authorization</c> headers counts.
/// </para>
/// <para>
/// <b>Exactly one public constructor.</b> <c>Grpc.AspNetCore</c> silently stops invoking an
/// interceptor that has more than one public ctor — see
/// <c>MeshCommandAuthInterceptorTests.HasExactlyOnePublicConstructor</c>.
@@ -51,8 +57,6 @@ namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
public sealed class MeshCommandAuthInterceptor : Interceptor
{
private const string ServicePrefix = "/mesh_transport.v1.NodeCommandService/";
private const string AuthorizationHeader = "authorization";
private const string BearerPrefix = "Bearer ";
private readonly IOptions<MeshTransportOptions> _options;
private readonly ILogger<MeshCommandAuthInterceptor> _logger;
@@ -137,7 +141,7 @@ public sealed class MeshCommandAuthInterceptor : Interceptor
"Node command service is not accepting calls: no pre-shared key is configured on this node."));
}
var presented = ExtractBearerToken(context.RequestHeaders);
var presented = GrpcRequestHeaders.BearerToken(context.RequestHeaders);
if (presented is null || !FixedTimeEquals(presented, expected))
{
_logger.LogWarning(
@@ -150,23 +154,6 @@ public sealed class MeshCommandAuthInterceptor : Interceptor
}
}
private static string? ExtractBearerToken(Metadata headers)
{
// gRPC lowercases header keys on the wire; compare case-insensitively anyway so a hand-built
// Metadata in a test behaves the same as a real request.
foreach (var entry in headers)
{
if (!string.Equals(entry.Key, AuthorizationHeader, StringComparison.OrdinalIgnoreCase))
continue;
var value = entry.Value;
if (value is not null && value.StartsWith(BearerPrefix, StringComparison.OrdinalIgnoreCase))
return value[BearerPrefix.Length..];
}
return null;
}
private static bool FixedTimeEquals(string presented, string expected)
=> CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(presented), Encoding.UTF8.GetBytes(expected));