using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using GameCult.Networking; using NUnit.Framework; namespace GameCult.Mesh.Tests; public sealed class CultMeshAuthorityProofTests { private static readonly DateTimeOffset Now = DateTimeOffset.FromUnixTimeMilliseconds(1_800_000_000_000); [Test] public void SignedRouteBindsEveryAuthorityFieldToConsumerTrust() { using var odin = ECDsa.Create(ECCurve.NamedCurves.nistP256); using var provider = ECDsa.Create(ECCurve.NamedCurves.nistP256); var route = SignedRoute(odin, provider); var trust = Trust(odin); trust.Validate("aetheria", route, Now); var wireRoundTrip = new CultMeshVerseDescriptor( "aetheria", "Aetheria", CultMeshVerseAuthorityModel.OperatorCluster, new CultMeshVerseCompatibility("cultmesh.v0", "rules"), authorityRoutes: new[] { route }) .ToMessage() .ToVerseDescriptor() .AuthorityRoutes[0]; trust.Validate("aetheria", wireRoundTrip, Now); wireRoundTrip.Certificate!.ProviderKey.KeyId.Should().Be("provider-1"); Action mutatedEndpoint = () => trust.Validate("aetheria", new CultMeshAuthorityRoute( route.AuthorityRuntimeId, "wss://evil.example/mesh", route.ProtocolIds, route.Priority, route.Generation, route.Certificate), Now); mutatedEndpoint.Should().Throw() .Which.Failure.Reason.Should().Be(CultMeshSessionFailureReason.Authentication); } [Test] public void RemoteTrustRejectsUnknownExpiredAndUnsignedRoutes() { using var odin = ECDsa.Create(ECCurve.NamedCurves.nistP256); using var provider = ECDsa.Create(ECCurve.NamedCurves.nistP256); using var stranger = ECDsa.Create(ECCurve.NamedCurves.nistP256); var route = SignedRoute(odin, provider); Action unknownRoot = () => Trust(stranger).Validate("aetheria", route, Now); Action expired = () => Trust(odin).Validate("aetheria", route, Now.AddHours(2)); Action unsigned = () => Trust(odin).Validate("aetheria", new CultMeshAuthorityRoute( "aetheria-daemon", "wss://provider.example/mesh", new[] { CultMeshProtocols.Documents.Value }, generation: "generation-1"), Now); unknownRoot.Should().Throw(); expired.Should().Throw(); unsigned.Should().Throw(); } [Test] public void UnsignedRouteRequiresExplicitLoopbackDevelopmentPolicy() { var local = new CultMeshAuthorityRoute( "aetheria-daemon", "rudp://127.0.0.1:3076", new[] { CultMeshProtocols.Documents.Value }, generation: "local"); var remote = new CultMeshAuthorityRoute( "aetheria-daemon", "rudp://192.0.2.10:3076", new[] { CultMeshProtocols.Documents.Value }, generation: "remote"); var policy = new CultMeshAuthorityTrustPolicy(CultMeshAuthorityTrustMode.LocalDevelopment); policy.Validate("aetheria", local, Now); Action remoteAttempt = () => policy.Validate("aetheria", remote, Now); remoteAttempt.Should().Throw(); } [Test] public void ProviderProofRejectsCredentialFreeEchoAndNonceReplay() { using var odin = ECDsa.Create(ECCurve.NamedCurves.nistP256); using var provider = ECDsa.Create(ECCurve.NamedCurves.nistP256); var route = SignedRoute(odin, provider); var trust = Trust(odin); var signer = new CultMeshSessionProofSigner(route, provider); var request = Request("nonce-a"); var signed = Accepted(request, signer.Sign(request)); CultMeshAuthorityProof.VerifySessionProof(request, signed, "aetheria", route, trust, Now).Should().BeTrue(); var echo = Accepted(request, string.Empty); CultMeshAuthorityProof.VerifySessionProof(request, echo, "aetheria", route, trust, Now).Should().BeFalse(); var replayedRequest = Request("nonce-b"); CultMeshAuthorityProof.VerifySessionProof(replayedRequest, signed, "aetheria", route, trust, Now).Should().BeFalse(); } [Test] public async Task SessionManagerRejectsPeerThatOnlyEchoesCertifiedIdentityStrings() { using var odin = ECDsa.Create(ECCurve.NamedCurves.nistP256); using var provider = ECDsa.Create(ECCurve.NamedCurves.nistP256); var now = DateTimeOffset.UtcNow; var route = CultMeshAuthorityProof.CreateSignedRoute( "aetheria", "aetheria-daemon", "wss://provider.example/mesh", new[] { CultMeshProtocols.Documents.Value }, 0, "generation-live", CultMeshEcdsaP256PublicKey.From("provider-live", provider), "odin-live", now.AddMinutes(-1), now.AddMinutes(5), odin); var descriptor = new CultMeshVerseDescriptor( "aetheria", "Aetheria", CultMeshVerseAuthorityModel.OperatorCluster, new CultMeshVerseCompatibility("cultmesh.v0", "rules"), authorityRoutes: new[] { route }); using var discovery = new CultMeshDiscoveryService(new[] { new SignedRouteSource(descriptor) }); using var manager = new CultMeshSessionManager( discovery, new[] { new EchoConnector() }, new CultMeshSessionManagerOptions { Trust = new CultMeshAuthorityTrustPolicy( CultMeshAuthorityTrustMode.AuthenticatedRemote, new[] { CultMeshEcdsaP256PublicKey.From("odin-live", odin) }) }); Func connect = async () => await manager.ConnectAsync( new CultMeshSessionTarget("aetheria", "aetheria-daemon"), CultMeshProtocols.Documents); (await connect.Should().ThrowAsync()) .Which.Failure.Reason.Should().Be(CultMeshSessionFailureReason.Authority); } private static CultMeshAuthorityRoute SignedRoute(ECDsa odin, ECDsa provider) => CultMeshAuthorityProof.CreateSignedRoute( "aetheria", "aetheria-daemon", "wss://provider.example/mesh", new[] { CultMeshProtocols.Documents.Value }, 0, "generation-1", CultMeshEcdsaP256PublicKey.From("provider-1", provider), "odin-root-1", Now.AddMinutes(-1), Now.AddHours(1), odin); private static CultMeshAuthorityTrustPolicy Trust(ECDsa odin) => new( CultMeshAuthorityTrustMode.AuthenticatedRemote, new[] { CultMeshEcdsaP256PublicKey.From("odin-root-1", odin) }); private static GameCult.Networking.CultMeshSessionOpenMessage Request(string nonceLabel) { using var hash = SHA256.Create(); return new GameCult.Networking.CultMeshSessionOpenMessage { MessageId = "message-1", SourceRuntimeId = "browser-1", VerseId = "aetheria", AuthorityRuntimeId = "aetheria-daemon", ProtocolId = CultMeshProtocols.Documents.Value, RouteGeneration = "generation-1", ClientNonce = Convert.ToBase64String(hash.ComputeHash(System.Text.Encoding.UTF8.GetBytes(nonceLabel))) }; } private static GameCult.Networking.CultMeshSessionAcceptedMessage Accepted( GameCult.Networking.CultMeshSessionOpenMessage request, string signature) => new() { MessageId = request.MessageId, Accepted = true, VerseId = request.VerseId, AuthorityRuntimeId = request.AuthorityRuntimeId, ProtocolId = request.ProtocolId, RouteGeneration = request.RouteGeneration, ClientNonce = request.ClientNonce, ProviderKeyId = "provider-1", ProviderSignature = signature }; private sealed class SignedRouteSource : ICultMeshLookupSource { private readonly CultMeshVerseDescriptor _descriptor; public SignedRouteSource(CultMeshVerseDescriptor descriptor) => _descriptor = descriptor; public string SourceId => "signed-odin"; public Task> LookupAsync( CultMeshDiscoveryQuery query, CancellationToken cancellationToken = default) => Task.FromResult>(new[] { new CultMeshDiscoveryObservation( _descriptor, SourceId, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddMinutes(1), CultMeshDiscoveryTrust.Signed) }); } private sealed class EchoConnector : ICultMeshTransportConnector { public string ConnectorId => "credential-free-echo"; public int Priority => 0; public bool CanConnect(CultMeshTransportCandidate candidate) => true; public Task ConnectAsync( CultMeshTransportCandidate candidate, CultMeshProtocolId protocol, CancellationToken cancellationToken = default) => Task.FromResult(new EchoClient()); } private sealed class EchoClient : ICultNetSchemaClient { private Action? _accepted; public bool Connected => true; public void Connect(string host, int port) { } public void SendCultNet(T message) where T : ICultNetSchemaMessage { if (message is not CultMeshSessionOpenMessage request) return; _accepted?.Invoke(new CultMeshSessionAcceptedMessage { MessageId = request.MessageId, Accepted = true, VerseId = request.VerseId, AuthorityRuntimeId = request.AuthorityRuntimeId, ProtocolId = request.ProtocolId, RouteGeneration = request.RouteGeneration, ClientNonce = request.ClientNonce, ProviderKeyId = "provider-live", ProviderSignature = string.Empty }); } public void OnCultNet(Action callback) where T : ICultNetSchemaMessage { if (typeof(T) == typeof(CultMeshSessionAcceptedMessage)) _accepted = response => callback((T)(object)response); } public void Dispose() { } } }