using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using RunicSafety.Api; using RunicSentinel.Admission; using RunicSentinel.Api; using RunicSentinel.Contracts; using RunicSentinel.Core; using RunicSentinel.Runtime; using Steamworks; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Runic Sentinel Server")] [assembly: AssemblyDescription("Dedicated and listen-host Sentinel authority without client reporting or administrator GUI code")] [assembly: AssemblyCompany("Chazman")] [assembly: AssemblyProduct("Runic Sentinel Server")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: InternalsVisibleTo("RunicSentinelServer.Tests")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RunicSentinel { [BepInPlugin("chazman.RunicSentinelServer", "Runic Sentinel Server", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInIncompatibility("chazman.RunicSentinel")] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "chazman.RunicSentinelServer"; public const string Name = "Runic Sentinel Server"; public const string Version = "1.0.0"; public const string ModuleId = "runic.sentinel.server"; private static Plugin _instance; private SentinelRuntime _runtime; private SentinelEnforcementRuntime _enforcement; private SentinelOperatorCommands _operatorCommands; private SentinelManagedPolicyService _managedPolicy; private SentinelAdminControl _adminControl; private SentinelFlightRecorder _flightRecorder; private Harmony _failClosedHarmony; private Harmony _roleHarmony; private Harmony _authorityHarmony; private ZNet _authorityNetwork; private ZNet _destroyedAuthorityNetwork; private int _refreshRequested; private bool _authorityStarted; private bool _activationFailed; private bool _configuredRoleKnown; private bool _serverRoleConfigured; private bool _unavailableGateLogged; private bool _clientNoticeLogged; private void Awake() { SentinelConfig.Bind(((BaseUnityPlugin)this).Config); ConfigEntry enabled = SentinelConfig.Enabled; if (enabled != null && !enabled.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Server is disabled; no worker or network handlers were created."); return; } _instance = this; try { InstallPermanentFailClosedGate(); } catch (Exception ex) { _activationFailed = true; Shutdown(); ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Sentinel Server could not install its Required-mode safety gate and is inactive: " + ex)); return; } try { InstallRoleObservers(); TryActivateAuthority(ZNet.instance); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Server v1.0.0 installed. Authority services remain inert until Valheim selects a dedicated server or listen host role."); } catch (Exception ex2) { _activationFailed = true; ShutdownAuthority(); try { Harmony roleHarmony = _roleHarmony; if (roleHarmony != null) { roleHarmony.UnpatchSelf(); } } catch { } _roleHarmony = null; ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Sentinel Server role bootstrap failed. Required mode remains fail closed through the permanent world-load and connection gates: " + ex2)); } } private void InstallPermanentFailClosedGate() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Expected O, but got Unknown MethodInfo methodInfo = RequireInstanceVoid("RPC_ServerHandshake", typeof(ZRpc)); MethodInfo methodInfo2 = RequireInstanceVoid("LoadWorld"); _failClosedHarmony = new Harmony("chazman.RunicSentinelServer.fail-closed"); try { _failClosedHarmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "BeforeUnavailableServerHandshake", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _failClosedHarmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "BeforeUnavailableWorldLoad", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch { try { _failClosedHarmony.UnpatchSelf(); } catch { } _failClosedHarmony = null; throw; } } private void InstallRoleObservers() { //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Expected O, but got Unknown //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Expected O, but got Unknown //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Expected O, but got Unknown //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Expected O, but got Unknown //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "SetServer", new Type[6] { typeof(bool), typeof(bool), typeof(bool), typeof(string), typeof(string), typeof(World) }, (Type[])null); if (methodInfo == null || !methodInfo.IsStatic || methodInfo.ReturnType != typeof(void)) { throw new MissingMethodException(typeof(ZNet).FullName, "SetServer"); } MethodInfo methodInfo2 = RequireInstanceVoid("Awake"); MethodInfo methodInfo3 = RequireInstanceVoid("OnNewConnection", typeof(ZNetPeer)); MethodInfo methodInfo4 = RequireInstanceVoid("OnDestroy"); _roleHarmony = new Harmony("chazman.RunicSentinelServer.authority-role"); _roleHarmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "AfterSetServer", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _roleHarmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "AfterZNetAwake", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _roleHarmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "AfterNewConnection", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _roleHarmony.Patch((MethodBase)methodInfo4, (HarmonyMethod)null, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "AfterZNetDestroy", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static MethodInfo RequireInstanceVoid(string name, params Type[] parameters) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), name, parameters ?? Type.EmptyTypes, (Type[])null); if (methodInfo == null || methodInfo.IsStatic || methodInfo.ReturnType != typeof(void)) { throw new MissingMethodException(typeof(ZNet).FullName, name); } return methodInfo; } internal static void ObserveConfiguredRole(bool server) { Plugin instance = _instance; if (!((Object)(object)instance == (Object)null)) { instance._configuredRoleKnown = true; instance._serverRoleConfigured = server; instance._destroyedAuthorityNetwork = null; if (server) { instance.TryActivateAuthority(null); return; } instance.ShutdownAuthority(); instance.LogClientInertOnce(); } } internal static void ObserveNetwork(ZNet network) { _instance?.TryActivateAuthority(network); } internal static void ObserveConnection(ZNet network, ZNetPeer peer) { Plugin instance = _instance; if (!((Object)(object)instance == (Object)null)) { instance.TryActivateAuthority(network); if (instance._authorityStarted && (Object)(object)network != (Object)null && network.IsServer()) { instance._authorityNetwork = network; SentinelNetworkCompatibility.ObserveConnection(network, peer); } else { instance.BlockUnavailableConnection(network, peer, null); } } } internal static void ObserveNetworkDestroyed(ZNet network) { Plugin instance = _instance; if ((Object)(object)instance != (Object)null && instance._authorityStarted && network == instance._authorityNetwork) { instance._destroyedAuthorityNetwork = network; instance.ShutdownAuthority(); } } internal static bool AllowUnavailableServerHandshake(ZNet network, ZRpc rpc) { Plugin instance = _instance; if (!((Object)(object)instance == (Object)null)) { return !instance.BlockUnavailableConnection(network, null, rpc); } return true; } internal static void GuardUnavailableWorldLoad(ZNet network) { Plugin instance = _instance; if ((Object)(object)instance == (Object)null || !instance.ShouldBlockUnavailable(network)) { return; } instance.LogUnavailableGateOnce(); throw new InvalidOperationException("Runic Sentinel Server Required authority is unavailable; world load was blocked."); } private void TryActivateAuthority(ZNet network) { //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Expected O, but got Unknown if (_authorityStarted) { if ((Object)(object)network != (Object)null && network.IsServer()) { _authorityNetwork = network; _destroyedAuthorityNetwork = null; } } else { if (_activationFailed || network == _destroyedAuthorityNetwork) { return; } bool flag = _configuredRoleKnown && _serverRoleConfigured; if ((Object)(object)network != (Object)null) { try { flag |= network.IsServer(); } catch { } } if (flag) { try { _runtime = new SentinelRuntime(); _flightRecorder = new SentinelFlightRecorder(_runtime.Evidence, ((BaseUnityPlugin)this).Logger, Paths.ConfigPath); _runtime.AttachNetwork(SentinelConfig.RemoteAdmissionMode); _enforcement = new SentinelEnforcementRuntime(_runtime); SentinelIntegrationApi.Attach(_enforcement); SentinelTransitionBackup.Attach(_runtime, ((BaseUnityPlugin)this).Logger, Paths.ConfigPath); _managedPolicy = new SentinelManagedPolicyService(_runtime, ((BaseUnityPlugin)this).Logger, Paths.ConfigPath, (string reason) => SentinelTransitionBackup.CreateVerifiedBackupNow(reason)); _operatorCommands = new SentinelOperatorCommands(_runtime, ((BaseUnityPlugin)this).Logger, Paths.ConfigPath, _managedPolicy); _adminControl = new SentinelAdminControl(_runtime, _managedPolicy, _operatorCommands); _authorityHarmony = new Harmony("chazman.RunicSentinelServer.authority"); _authorityHarmony.PatchAll(typeof(Plugin).Assembly); SentinelConfig.Changed += Refresh; _runtime.Start(Paths.ConfigPath); _authorityNetwork = (((Object)(object)network != (Object)null && network.IsServer()) ? network : null); _authorityStarted = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Server authority services started. Required admission now gates the exact direct connection and the dedicated console/admin backend is available."); return; } catch (Exception ex) { _activationFailed = true; ShutdownAuthority(); ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Sentinel Server authority startup failed closed: " + ex)); return; } } if ((Object)(object)network != (Object)null || _configuredRoleKnown) { LogClientInertOnce(); } } } internal static bool UnavailableAuthorityFailsClosed(SentinelRemoteAdmissionMode mode, bool authoritative, bool authorityStarted) { if (authoritative && !authorityStarted) { return mode == SentinelRemoteAdmissionMode.Required; } return false; } private bool ShouldBlockUnavailable(ZNet network) { bool authoritative = false; try { authoritative = (Object)(object)network != (Object)null && network.IsServer(); } catch { } return UnavailableAuthorityFailsClosed(SentinelConfig.RemoteAdmissionMode, authoritative, _authorityStarted); } private bool BlockUnavailableConnection(ZNet network, ZNetPeer peer, ZRpc rpc) { if (!ShouldBlockUnavailable(network)) { return false; } LogUnavailableGateOnce(); try { ZNetPeer val = peer; if (val == null && rpc != null) { foreach (ZNetPeer peer2 in network.GetPeers()) { if (peer2 != null && peer2.m_rpc == rpc) { val = peer2; break; } } } if (val != null) { network.Disconnect(val); } } catch { } return true; } private void LogUnavailableGateOnce() { if (!_unavailableGateLogged) { _unavailableGateLogged = true; ((BaseUnityPlugin)this).Logger.LogError((object)"Runic Sentinel Server Required authority is unavailable. World loading and native client admission remain blocked until the server is restarted successfully."); } } private void LogClientInertOnce() { if (!_clientNoticeLogged) { _clientNoticeLogged = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Server detected a non-authoritative client and remains fully inert. Install Runic Sentinel Client in player-only profiles."); } } private void Refresh() { Interlocked.Exchange(ref _refreshRequested, 1); } private void Update() { ZNet instance = ZNet.instance; if (_authorityStarted && (Object)(object)instance != (Object)null && !instance.IsServer()) { ShutdownAuthority(); LogClientInertOnce(); return; } if (_authorityStarted && (Object)(object)instance != (Object)null && instance.IsServer()) { _authorityNetwork = instance; _destroyedAuthorityNetwork = null; } if (!_authorityStarted) { if ((Object)(object)instance != (Object)null) { TryActivateAuthority(instance); } return; } try { _runtime?.TickNetwork(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel server admission tick stopped safely: " + ex.Message)); } try { _runtime?.TickIntegrity(); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel server integrity check failed closed: " + ex2.Message)); } try { _adminControl?.Tick(); } catch (Exception ex3) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel server administrator transport stopped safely: " + ex3.Message)); } try { _operatorCommands?.TickDedicatedConsole(); } catch (Exception ex4) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel server console input stopped safely: " + ex4.Message)); } if (Interlocked.Exchange(ref _refreshRequested, 0) == 0 || _runtime == null) { return; } try { _runtime.Start(Paths.ConfigPath); } catch (Exception ex5) { ((BaseUnityPlugin)this).Logger.LogError((object)("Sentinel server policy refresh failed closed: " + ex5.Message)); } } private void OnDestroy() { Shutdown(); } private void Shutdown() { if (_instance == this) { _instance = null; } ShutdownAuthority(); try { Harmony roleHarmony = _roleHarmony; if (roleHarmony != null) { roleHarmony.UnpatchSelf(); } } catch { } _roleHarmony = null; try { Harmony failClosedHarmony = _failClosedHarmony; if (failClosedHarmony != null) { failClosedHarmony.UnpatchSelf(); } } catch { } _failClosedHarmony = null; } private void ShutdownAuthority() { SentinelConfig.Changed -= Refresh; Interlocked.Exchange(ref _refreshRequested, 0); _authorityStarted = false; _authorityNetwork = null; try { Harmony authorityHarmony = _authorityHarmony; if (authorityHarmony != null) { authorityHarmony.UnpatchSelf(); } } catch { } _authorityHarmony = null; try { _adminControl?.Dispose(); } catch { } _adminControl = null; SentinelTransitionBackup.Detach(); try { _operatorCommands?.Dispose(); } catch { } _operatorCommands = null; _managedPolicy = null; try { _flightRecorder?.Dispose(); } catch { } _flightRecorder = null; SentinelIntegrationApi.Detach(_enforcement); try { _enforcement?.Dispose(); } catch { } _enforcement = null; try { _runtime?.Dispose(); } catch { } _runtime = null; } } internal static class SentinelServerRoleBootstrap { internal static void AfterSetServer([HarmonyArgument(0)] bool server) { Plugin.ObserveConfiguredRole(server); } internal static void AfterZNetAwake(ZNet __instance) { Plugin.ObserveNetwork(__instance); } internal static void AfterNewConnection(ZNet __instance, [HarmonyArgument(0)] ZNetPeer peer) { Plugin.ObserveConnection(__instance, peer); } internal static void AfterZNetDestroy(ZNet __instance) { Plugin.ObserveNetworkDestroyed(__instance); } [HarmonyPriority(800)] internal static bool BeforeUnavailableServerHandshake(ZNet __instance, [HarmonyArgument(0)] ZRpc rpc) { return Plugin.AllowUnavailableServerHandshake(__instance, rpc); } [HarmonyPriority(800)] internal static void BeforeUnavailableWorldLoad(ZNet __instance) { Plugin.GuardUnavailableWorldLoad(__instance); } } internal static class SentinelConfig { internal static ConfigEntry Enabled; internal static ConfigEntry PolicyFile; internal static ConfigEntry SignatureFile; internal static ConfigEntry PublicKeyFile; internal static ConfigEntry TrustedPublicKeySha256; internal static ConfigEntry RemoteAdmissionPolicy; internal static ConfigEntry IntegrityCheckSeconds; internal static ConfigEntry BackupBeforeTransitions; internal static ConfigEntry VeryHighDisconnectCount; internal static ConfigEntry HighDisconnectCount; internal static ConfigEntry EnforcementWindowSeconds; internal static SentinelRemoteAdmissionMode RemoteAdmissionMode { get { if (!string.Equals(RemoteAdmissionPolicy?.Value?.Trim(), "Disabled", StringComparison.OrdinalIgnoreCase)) { if (!string.Equals(RemoteAdmissionPolicy?.Value?.Trim(), "Required", StringComparison.OrdinalIgnoreCase)) { return SentinelRemoteAdmissionMode.Optional; } return SentinelRemoteAdmissionMode.Required; } return SentinelRemoteAdmissionMode.Disabled; } } internal static event Action Changed; internal static void Bind(ConfigFile config) { //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Expected O, but got Unknown //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Expected O, but got Unknown //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Expected O, but got Unknown Enabled = config.Bind("General", "Enabled", true, "Enable bounded Sentinel local-snapshot, RSA policy, and evidence services. Sampled at startup; false registers nothing and starts no worker."); PolicyFile = config.Bind("Policy", "ManifestFile", "RunicSentinel.policy", "Canonical RUNIC-SENTINEL/3 policy path, relative to BepInEx/config unless absolute."); SignatureFile = config.Bind("Policy", "SignatureFile", "RunicSentinel.policy.sig", "Canonical Base64 detached RSA-3072/SHA-256 PKCS#1 v1.5 signature path."); PublicKeyFile = config.Bind("Policy", "PublicKeyFile", "RunicSentinel.policy.pub", "Canonical RUNIC-RSA-PUBLIC/1 verification public key path. The optional F3 workflow keeps its private key in a separate server-only directory."); TrustedPublicKeySha256 = config.Bind("Policy", "TrustedPublicKeySha256", string.Empty, "Required lowercase SHA-256 of the exact canonical public-key file. Empty or mismatched pins keep Sentinel monitor-only."); RemoteAdmissionPolicy = config.Bind("Remote Admission", "Policy", "Optional", "Sampled at startup; changing it requires a restart. Required withholds native admission and denies a missing, stale, malformed, or signed-policy-incompatible client report after a bounded grace period. Optional records evidence without delaying or disconnecting. Disabled does not register the direct Sentinel admission protocol."); IntegrityCheckSeconds = config.Bind("Runtime Integrity", "CheckIntervalSeconds", 15, "Metadata-check loaded plugin DLLs and active signed-passport files at this interval. A detected runtime change denies new strict admissions until restart. Range 5-300 seconds."); BackupBeforeTransitions = config.Bind("Transition Safety", "BackupWorldBeforeProfileChange", true, "Before a server loads an existing world with a different signed policy or plugin snapshot, require a verified Runic Safety backup of the world database and metadata."); VeryHighDisconnectCount = config.Bind("Automatic Enforcement", "VeryHighFindingsBeforeDisconnect", 2, new ConfigDescription("Disconnect after this many very-high-confidence violations in the enforcement window.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 10), Array.Empty())); HighDisconnectCount = config.Bind("Automatic Enforcement", "HighFindingsBeforeDisconnect", 3, new ConfigDescription("Disconnect after this many high-confidence violations in the enforcement window.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 20), Array.Empty())); EnforcementWindowSeconds = config.Bind("Automatic Enforcement", "FindingWindowSeconds", 60, new ConfigDescription("Rolling violation window used by graduated automatic enforcement.", (AcceptableValueBase)(object)new AcceptableValueRange(10, 600), Array.Empty())); PolicyFile.SettingChanged += Notify; SignatureFile.SettingChanged += Notify; PublicKeyFile.SettingChanged += Notify; TrustedPublicKeySha256.SettingChanged += Notify; } private static void Notify(object sender, EventArgs args) { SentinelConfig.Changed?.Invoke(); } } } namespace RunicSentinel.Admission { internal sealed class AdmissionPluginEvidence { internal string Id { get; } internal string Version { get; } internal string Sha256 { get; } internal AdmissionPluginEvidence(string id, string version, string sha256) { Id = AdmissionValidation.RequireAtom(id, 1, 128, "id"); Version = AdmissionValidation.RequireAtom(version, 1, 64, "version"); Sha256 = AdmissionValidation.RequireLowerHex(sha256, 64, "sha256"); } } internal sealed class AdmissionClientProfile { internal long CapturedUnixSeconds { get; } internal string Digest { get; } internal IReadOnlyList Plugins { get; } internal AdmissionClientProfile(long capturedUnixSeconds, string digest, IList plugins) { if (capturedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("capturedUnixSeconds"); } CapturedUnixSeconds = capturedUnixSeconds; Digest = AdmissionValidation.RequireLowerHex(digest, 64, "digest"); if (plugins == null) { throw new ArgumentNullException("plugins"); } Plugins = new ReadOnlyCollection(new List(plugins)); } } internal sealed class AdmissionChallenge { private readonly byte[] _nonce; internal string RequestId { get; } internal byte[] Nonce => (byte[])_nonce.Clone(); internal long IssuedUnixSeconds { get; } internal long DeadlineUnixSeconds { get; } internal AdmissionChallenge(string requestId, byte[] nonce, long issuedUnixSeconds, long deadlineUnixSeconds) { RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId"); if (nonce == null || nonce.Length != 32) { throw new ArgumentOutOfRangeException("nonce"); } if (issuedUnixSeconds < 0 || deadlineUnixSeconds < issuedUnixSeconds) { throw new ArgumentOutOfRangeException("issuedUnixSeconds"); } _nonce = (byte[])nonce.Clone(); IssuedUnixSeconds = issuedUnixSeconds; DeadlineUnixSeconds = deadlineUnixSeconds; } } internal sealed class AdmissionReport { internal string RequestId { get; } internal string ClientVersion { get; } internal long CapturedUnixSeconds { get; } internal long IssuedUnixSeconds { get; } internal string ProfileDigest { get; } internal string NonceBinding { get; } internal IReadOnlyList Plugins { get; } internal AdmissionReport(string requestId, string clientVersion, long capturedUnixSeconds, long issuedUnixSeconds, string profileDigest, string nonceBinding, IList plugins) { RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId"); ClientVersion = AdmissionValidation.RequireAtom(clientVersion, 1, 32, "clientVersion"); if (capturedUnixSeconds < 0 || issuedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("capturedUnixSeconds"); } ProfileDigest = AdmissionValidation.RequireLowerHex(profileDigest, 64, "profileDigest"); NonceBinding = AdmissionValidation.RequireLowerHex(nonceBinding, 64, "nonceBinding"); if (plugins == null || plugins.Count > 512) { throw new ArgumentOutOfRangeException("plugins"); } CapturedUnixSeconds = capturedUnixSeconds; IssuedUnixSeconds = issuedUnixSeconds; Plugins = new ReadOnlyCollection(new List(plugins)); } } internal sealed class AdmissionDecisionMessage { internal string RequestId { get; } internal bool Accepted { get; } internal bool ResumeHandshake { get; } internal string ReasonCode { get; } internal long PolicySequence { get; } internal string PolicyProfile { get; } internal long IssuedUnixSeconds { get; } internal AdmissionDecisionMessage(string requestId, bool accepted, bool resumeHandshake, string reasonCode, long policySequence, string policyProfile, long issuedUnixSeconds) { RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId"); ReasonCode = AdmissionValidation.RequireReason(reasonCode, "reasonCode"); if (policySequence < 0 || issuedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("policySequence"); } if (!string.IsNullOrEmpty(policyProfile)) { AdmissionValidation.RequireAtom(policyProfile, 1, 64, "policyProfile"); } if (resumeHandshake && !accepted) { throw new ArgumentException("Only an accepted decision may resume the native handshake.", "resumeHandshake"); } Accepted = accepted; ResumeHandshake = resumeHandshake; PolicySequence = policySequence; PolicyProfile = policyProfile ?? string.Empty; IssuedUnixSeconds = issuedUnixSeconds; } } internal static class AdmissionValidation { internal static string RequireAtom(string value, int minimum, int maximum, string parameterName) { if (!IsAtom(value, minimum, maximum)) { throw new ArgumentException("Value is not a canonical ASCII atom.", parameterName); } return value; } internal static bool IsAtom(string value, int minimum, int maximum) { if (value == null || value.Length < minimum || value.Length > maximum) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_') { return false; } } return true; } internal static string RequireLowerHex(string value, int length, string parameterName) { if (!IsLowerHex(value, length)) { throw new ArgumentException("Value is not canonical lowercase hexadecimal.", parameterName); } return value; } internal static bool IsLowerHex(string value, int length) { if (value == null || value.Length != length) { return false; } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { return false; } } return true; } internal static string RequireReason(string value, string parameterName) { if (!IsReason(value)) { throw new ArgumentException("Reason code is not canonical.", parameterName); } return value; } internal static bool IsReason(string value) { if (value == null || value.Length < 1 || value.Length > 96) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-' && c != '.') { return false; } } return true; } } internal static class AdmissionProfileCanonicalizer { internal const int MaximumCanonicalBytes = 262144; private const string Header = "RUNIC-SENTINEL-CLIENT-PROFILE/1\n"; internal static bool TryCreate(IEnumerable source, long capturedUnixSeconds, out AdmissionClientProfile profile, out string failure) { profile = null; failure = string.Empty; if (source == null || capturedUnixSeconds < 0) { failure = "profile-missing"; return false; } List list = new List(); foreach (AdmissionPluginEvidence item in source) { if (item == null) { failure = "profile-entry-null"; return false; } if (list.Count >= 512) { failure = "profile-plugin-cap"; return false; } list.Add(item); } list.Sort((AdmissionPluginEvidence left, AdmissionPluginEvidence right) => string.CompareOrdinal(left.Id, right.Id)); string a = null; StringBuilder stringBuilder = new StringBuilder("RUNIC-SENTINEL-CLIENT-PROFILE/1\n", Math.Min(32768, 64 + list.Count * 160)); foreach (AdmissionPluginEvidence item2 in list) { if (string.Equals(a, item2.Id, StringComparison.Ordinal)) { failure = "profile-plugin-duplicate"; return false; } a = item2.Id; stringBuilder.Append(item2.Id).Append('|').Append(item2.Version) .Append('|') .Append(item2.Sha256) .Append('\n'); if (stringBuilder.Length > 262144) { failure = "profile-size"; return false; } } byte[] bytes = Encoding.UTF8.GetBytes(stringBuilder.ToString()); if (bytes.Length > 262144) { failure = "profile-size"; return false; } string digest; using (SHA256 sHA = SHA256.Create()) { digest = Hex(sHA.ComputeHash(bytes)); } profile = new AdmissionClientProfile(capturedUnixSeconds, digest, list); return true; } internal static string Hex(byte[] bytes) { if (bytes == null) { throw new ArgumentNullException("bytes"); } StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2); for (int i = 0; i < bytes.Length; i++) { stringBuilder.Append(bytes[i].ToString("x2")); } return stringBuilder.ToString(); } internal static bool FixedTimeEquals(string left, string right) { if (left == null || right == null) { return false; } int num = left.Length ^ right.Length; int num2 = Math.Max(left.Length, right.Length); for (int i = 0; i < num2; i++) { char c = ((i < left.Length) ? left[i] : '\0'); char c2 = ((i < right.Length) ? right[i] : '\0'); num |= c ^ c2; } return num == 0; } } internal enum AdmissionMessageKind : byte { Challenge = 1, Report, Decision } internal static class AdmissionProtocolV2 { internal const string DirectRpcName = "chazman.RunicSentinel.Admission.v2"; internal const int WireSchema = 2; internal const int MaximumPlugins = 512; internal const int MaximumFrameBytes = 262144; internal const int NonceBytes = 32; internal const int RequestIdHexLength = 32; internal const long MaximumClockSkewSeconds = 60L; internal const long MaximumMessageAgeSeconds = 300L; internal const long MaximumChallengeLifetimeSeconds = 120L; private const int Magic = 843141970; private const int Terminal = 843337285; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static AdmissionChallenge CreateChallenge(long nowUnixSeconds, long lifetimeSeconds) { if (nowUnixSeconds < 0 || lifetimeSeconds < 1 || lifetimeSeconds > 120 || nowUnixSeconds > long.MaxValue - lifetimeSeconds) { throw new ArgumentOutOfRangeException("lifetimeSeconds"); } byte[] array = new byte[32]; byte[] array2 = new byte[16]; using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create()) { randomNumberGenerator.GetBytes(array); randomNumberGenerator.GetBytes(array2); } return new AdmissionChallenge(AdmissionProfileCanonicalizer.Hex(array2), array, nowUnixSeconds, nowUnixSeconds + lifetimeSeconds); } internal static bool IsChallengeCurrent(AdmissionChallenge challenge, long nowUnixSeconds, out string failure) { failure = string.Empty; if (challenge == null || nowUnixSeconds < 0) { failure = "challenge-missing"; return false; } if (challenge.DeadlineUnixSeconds < challenge.IssuedUnixSeconds || challenge.DeadlineUnixSeconds - challenge.IssuedUnixSeconds > 120) { failure = "challenge-lifetime"; return false; } if (challenge.IssuedUnixSeconds < nowUnixSeconds - 300 || challenge.IssuedUnixSeconds > nowUnixSeconds + 60 || nowUnixSeconds > challenge.DeadlineUnixSeconds) { failure = "challenge-stale"; return false; } return true; } internal static bool TryValidateReport(AdmissionChallenge challenge, AdmissionReport report, long receivedUnixSeconds, out AdmissionClientProfile profile, out string failure) { profile = null; failure = string.Empty; if (!IsChallengeCurrent(challenge, receivedUnixSeconds, out failure)) { return false; } if (report == null || !AdmissionProfileCanonicalizer.FixedTimeEquals(challenge.RequestId, report.RequestId)) { failure = "report-request-mismatch"; return false; } if (report.IssuedUnixSeconds < receivedUnixSeconds - 300 || report.IssuedUnixSeconds > receivedUnixSeconds + 60 || report.CapturedUnixSeconds > receivedUnixSeconds + 60) { failure = "report-stale"; return false; } if (!AdmissionProfileCanonicalizer.TryCreate(report.Plugins, report.CapturedUnixSeconds, out profile, out failure)) { return false; } if (!AdmissionProfileCanonicalizer.FixedTimeEquals(profile.Digest, report.ProfileDigest)) { profile = null; failure = "report-digest-mismatch"; return false; } if (!TryComputeNonceBinding(challenge.RequestId, challenge.Nonce, profile.Digest, out var binding) || !AdmissionProfileCanonicalizer.FixedTimeEquals(binding, report.NonceBinding)) { profile = null; failure = "report-binding-mismatch"; return false; } return true; } internal static bool TryComputeNonceBinding(string requestId, byte[] nonce, string profileDigest, out string binding) { binding = string.Empty; if (!AdmissionValidation.IsLowerHex(requestId, 32) || nonce == null || nonce.Length != 32 || !AdmissionValidation.IsLowerHex(profileDigest, 64)) { return false; } string s = "RUNIC-SENTINEL-ADMISSION-BINDING/2\n" + requestId + "\n" + AdmissionProfileCanonicalizer.Hex(nonce) + "\n" + profileDigest + "\n"; using (SHA256 sHA = SHA256.Create()) { binding = AdmissionProfileCanonicalizer.Hex(sHA.ComputeHash(Encoding.ASCII.GetBytes(s))); } return true; } internal static byte[] EncodeChallenge(AdmissionChallenge value) { if (value == null) { throw new ArgumentNullException("value"); } return Encode(AdmissionMessageKind.Challenge, delegate(BinaryWriter writer) { WriteString(writer, value.RequestId, 32); byte[] nonce = value.Nonce; writer.Write(nonce.Length); writer.Write(nonce); writer.Write(value.IssuedUnixSeconds); writer.Write(value.DeadlineUnixSeconds); }); } internal static byte[] EncodeDecision(AdmissionDecisionMessage value) { if (value == null) { throw new ArgumentNullException("value"); } return Encode(AdmissionMessageKind.Decision, delegate(BinaryWriter writer) { WriteString(writer, value.RequestId, 32); writer.Write(value.Accepted ? ((byte)1) : ((byte)0)); writer.Write(value.ResumeHandshake ? ((byte)1) : ((byte)0)); WriteString(writer, value.ReasonCode, 96); writer.Write(value.PolicySequence); WriteString(writer, value.PolicyProfile, 64); writer.Write(value.IssuedUnixSeconds); }); } internal static bool TryGetKind(byte[] bytes, out AdmissionMessageKind kind, out string failure) { kind = (AdmissionMessageKind)0; failure = string.Empty; try { using MemoryStream input = NewReadStream(bytes); using BinaryReader reader = new BinaryReader(input, StrictUtf8); if (!ReadHeader(reader, out kind, out failure)) { return false; } return true; } catch { kind = (AdmissionMessageKind)0; failure = "frame-malformed"; return false; } } internal static bool TryDecodeReport(byte[] bytes, out AdmissionReport value, out string failure) { value = null; failure = string.Empty; try { using MemoryStream memoryStream = NewReadStream(bytes); using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8); if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Report, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2) || !TryReadString(binaryReader, memoryStream, 32, out var value3)) { failure = "report-shape"; return false; } long num = binaryReader.ReadInt64(); long num2 = binaryReader.ReadInt64(); if (!TryReadString(binaryReader, memoryStream, 64, out var value4) || !TryReadString(binaryReader, memoryStream, 64, out var value5)) { failure = "report-shape"; return false; } int num3 = binaryReader.ReadInt32(); if (num3 < 0 || num3 > 512) { failure = "report-plugin-cap"; return false; } List list = new List(num3); string text = null; for (int i = 0; i < num3; i++) { if (!TryReadString(binaryReader, memoryStream, 128, out var value6) || !TryReadString(binaryReader, memoryStream, 64, out var value7) || !TryReadString(binaryReader, memoryStream, 64, out var value8) || !AdmissionValidation.IsAtom(value6, 1, 128) || !AdmissionValidation.IsAtom(value7, 1, 64) || !AdmissionValidation.IsLowerHex(value8, 64) || (text != null && string.CompareOrdinal(text, value6) >= 0)) { failure = "report-plugin-shape"; return false; } text = value6; list.Add(new AdmissionPluginEvidence(value6, value7, value8)); } if (!AdmissionValidation.IsLowerHex(value2, 32) || !AdmissionValidation.IsAtom(value3, 1, 32) || num < 0 || num2 < 0 || !AdmissionValidation.IsLowerHex(value4, 64) || !AdmissionValidation.IsLowerHex(value5, 64) || !ReadTerminal(binaryReader, memoryStream)) { failure = "report-shape"; return false; } value = new AdmissionReport(value2, value3, num, num2, value4, value5, list); return true; } catch { value = null; failure = "report-malformed"; return false; } } private static byte[] Encode(AdmissionMessageKind kind, Action body) { using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8); binaryWriter.Write(843141970); binaryWriter.Write(2); binaryWriter.Write((byte)kind); body(binaryWriter); binaryWriter.Write(843337285); binaryWriter.Flush(); if (memoryStream.Length <= 0 || memoryStream.Length > 262144) { throw new InvalidDataException("Admission frame exceeds its wire bound."); } return memoryStream.ToArray(); } private static MemoryStream NewReadStream(byte[] bytes) { if (bytes == null || bytes.Length == 0 || bytes.Length > 262144) { throw new InvalidDataException("Admission frame size is invalid."); } return new MemoryStream(bytes, writable: false); } private static bool ReadHeader(BinaryReader reader, out AdmissionMessageKind kind, out string failure) { kind = (AdmissionMessageKind)0; failure = string.Empty; if (reader.ReadInt32() != 843141970 || reader.ReadInt32() != 2) { failure = "frame-header"; return false; } kind = (AdmissionMessageKind)reader.ReadByte(); if ((int)kind < 1 || (int)kind > 3) { kind = (AdmissionMessageKind)0; failure = "frame-kind"; return false; } return true; } private static bool ReadExpectedHeader(BinaryReader reader, AdmissionMessageKind expected, out string failure) { if (!ReadHeader(reader, out var kind, out failure)) { return false; } if (kind == expected) { return true; } failure = "frame-kind"; return false; } private static void WriteString(BinaryWriter writer, string value, int maximumCharacters) { if (value == null || value.Length > maximumCharacters) { throw new InvalidDataException("Admission string exceeds its bound."); } byte[] bytes = StrictUtf8.GetBytes(value); if (bytes.Length > maximumCharacters) { throw new InvalidDataException("Admission string exceeds its byte bound."); } writer.Write(bytes.Length); writer.Write(bytes); } private static bool TryReadString(BinaryReader reader, MemoryStream stream, int maximumBytes, out string value) { value = string.Empty; int num = reader.ReadInt32(); if (num < 0 || num > maximumBytes || Remaining(stream) < num) { return false; } byte[] array = reader.ReadBytes(num); if (array.Length != num) { return false; } value = StrictUtf8.GetString(array); return value.Length <= maximumBytes; } private static bool ReadTerminal(BinaryReader reader, MemoryStream stream) { if (Remaining(stream) == 4 && reader.ReadInt32() == 843337285) { return Remaining(stream) == 0; } return false; } private static long Remaining(MemoryStream stream) { return stream.Length - stream.Position; } } } namespace RunicSentinel.Runtime { internal sealed class SentinelAdminControl : IDisposable { private sealed class ServerConnection { internal ZNetPeer Peer { get; } internal ZRpc Rpc { get; } internal long Ordinal { get; } internal ServerConnection(ZNetPeer peer, ZRpc rpc, long ordinal) { Peer = peer; Rpc = rpc; Ordinal = ordinal; } } private sealed class CachedResponse { internal byte[] RequestDigest; internal bool Accepted; internal string Reason; internal byte[] Payload; internal long Expires; } private const string RequestRpc = "runic.sentinel.admin.request.v1"; private const string ResponseRpc = "runic.sentinel.admin.response.v1"; private const int WireSchema = 1; private const int TerminalMarker = 1369914905; private const int MaximumEnvelopeBytes = 184320; private const int MaximumReplayEntries = 256; private const int MaximumTrackedPeers = 64; private static readonly long ReplayLifetimeTicks = TimeSpan.FromMinutes(1.0).Ticks; private readonly SentinelRuntime _runtime; private readonly SentinelManagedPolicyService _managed; private readonly SentinelOperatorCommands _commands; private readonly Dictionary _cache = new Dictionary(StringComparer.Ordinal); private readonly Queue _cacheOrder = new Queue(); private readonly Dictionary _serverConnections = new Dictionary(); private ZNet _network; private long _nextConnectionOrdinal; private bool _disposed; internal SentinelAdminControl(SentinelRuntime runtime, SentinelManagedPolicyService managed, SentinelOperatorCommands commands) { _runtime = runtime ?? throw new ArgumentNullException("runtime"); _managed = managed ?? throw new ArgumentNullException("managed"); _commands = commands ?? throw new ArgumentNullException("commands"); } internal void Tick() { if (_disposed) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { if ((Object)(object)_network != (Object)null) { ClearConnections("The authoritative server administrator channel disconnected."); _network = null; } } else { EnsureNetwork(instance); if (instance.IsServer()) { TickServer(instance); } else { ClearConnections("Runic Sentinel Server is inert on a non-authoritative client."); _network = null; } } long ticks = DateTime.UtcNow.Ticks; ExpireCache(ticks); } private void EnsureNetwork(ZNet network) { if (network != _network) { ClearConnections("The authoritative server administrator channel changed."); _network = network; } } private void TickServer(ZNet network) { List list; try { list = network.GetPeers(); } catch { list = null; } Dictionary dictionary = new Dictionary(); int num = 0; if (list != null) { foreach (ZNetPeer item in list) { if (num++ >= 64) { break; } ZRpc val = item?.m_rpc; if (val != null && IsReady(item) && !dictionary.ContainsKey(val)) { dictionary.Add(val, item); } } } foreach (ZRpc item2 in new List(_serverConnections.Keys)) { if (!dictionary.TryGetValue(item2, out var value) || !_serverConnections.TryGetValue(item2, out var value2) || value2.Peer != value || value.m_rpc != item2) { RemoveServerConnection(item2); } } foreach (KeyValuePair item3 in dictionary) { if (!_serverConnections.ContainsKey(item3.Key) && _serverConnections.Count < 64) { try { item3.Key.Register("runic.sentinel.admin.request.v1", (Action)ReceiveRequest); } catch { continue; } long ordinal = ((_nextConnectionOrdinal == long.MaxValue) ? long.MaxValue : (++_nextConnectionOrdinal)); _serverConnections.Add(item3.Key, new ServerConnection(item3.Value, item3.Key, ordinal)); } } } private void ReceiveRequest(ZRpc rpc, ZPackage package) { ZNet instance = ZNet.instance; if (_disposed || (Object)(object)instance == (Object)null || instance != _network || !instance.IsServer() || rpc == null || !_serverConnections.TryGetValue(rpc, out var value)) { return; } ZNetPeer val = FindExactReadyPeer(instance, rpc); if (val == null || val != value.Peer || val.m_rpc != value.Rpc) { return; } if (!TryReadRequest(package, out var id, out var action, out var payload, out var issued)) { SendResponse(value, string.Empty, accepted: false, "Malformed administrator request.", Array.Empty()); return; } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (issued < num - 30 || issued > num + 30) { SendResponse(value, id, accepted: false, "Administrator request expired.", Array.Empty()); return; } string key = value.Ordinal.ToString(CultureInfo.InvariantCulture) + ":" + id; byte[] array = Digest(action, payload, issued); ExpireCache(DateTime.UtcNow.Ticks); string authority; string subject; if (_cache.TryGetValue(key, out var value2)) { if (!Fixed(value2.RequestDigest, array)) { SendResponse(value, id, accepted: false, "Administrator request identity was reused.", Array.Empty()); } else { SendResponse(value, id, value2.Accepted, value2.Reason, value2.Payload); } } else if (!SentinelTransportIdentity.TryResolvePeer(val, out authority, out subject)) { SendResponse(value, id, accepted: false, "Authenticated backend identity is unavailable.", Array.Empty()); } else { Execute(authority, subject, action, payload, out var accepted, out var response, out var reason); Cache(key, array, accepted, reason, response); SendResponse(value, id, accepted, reason, response); } } private void Execute(string authority, string subject, string action, byte[] payload, out bool accepted, out byte[] response, out string reason) { accepted = false; response = Array.Empty(); reason = "Administrator access denied."; if (_runtime.IsBanned(authority, subject)) { reason = "This account is banned."; } else { if (!_runtime.IsAdministrator(authority, subject)) { return; } try { SentinelAdminDocument value; if (action == "status" && payload.Length == 0) { response = SentinelAdminProtocol.Encode(_managed.CreateDocument("Authenticated by " + authority + " backend identity.")); } else if (action == "apply" && SentinelAdminProtocol.TryDecode(payload, out value)) { response = SentinelAdminProtocol.EncodeMessage(_managed.Apply(value, authority, subject)); } else { if (!(action == "tool") || !SentinelAdminProtocol.TryDecodeTool(payload, out var tool)) { reason = "Administrator operation is invalid."; return; } response = SentinelAdminProtocol.EncodeMessage(RunToolCore(tool)); } accepted = true; reason = "ok"; } catch (Exception ex) { reason = Bounded(ex.Message); } } } private string RunToolCore(string tool) { return tool switch { "report" => "Support report created: " + _commands.WriteReport(), "networks" => "Network map created: " + _commands.WriteReport(includeNetworks: true), "backup" => "Verified backup created: " + SentinelTransitionBackup.CreateVerifiedBackupNow("runic-sentinel-admin-tool"), _ => throw new InvalidOperationException("Unknown administrator tool."), }; } private static bool TryReadRequest(ZPackage package, out string id, out string action, out byte[] payload, out long issued) { id = (action = string.Empty); payload = null; issued = 0L; try { if (package == null || package.Size() < 1 || package.Size() > 184320 || package.ReadInt() != 1) { return false; } id = package.ReadString(); action = package.ReadString(); issued = package.ReadLong(); payload = package.ReadByteArray(); return CanonicalId(id) && (action == "status" || action == "apply" || action == "tool") && payload != null && payload.Length <= 122880 && package.ReadInt() == 1369914905 && package.GetPos() == package.Size(); } catch { return false; } } private void SendResponse(ServerConnection connection, string id, bool accepted, string reason, byte[] payload) { ZNet network = _network; if (_disposed || (Object)(object)network == (Object)null || !network.IsServer() || connection == null || FindExactReadyPeer(network, connection.Rpc) != connection.Peer) { return; } ZPackage val = WriteResponse(id, accepted, reason, payload); if (val.Size() > 184320) { return; } try { connection.Rpc.Invoke("runic.sentinel.admin.response.v1", new object[1] { val }); } catch { } } private static ZPackage WriteResponse(string id, bool accepted, string reason, byte[] payload) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(id ?? string.Empty); val.Write(accepted); val.Write(Bounded(reason)); val.Write(payload ?? Array.Empty()); val.Write(1369914905); return val; } private static ZNetPeer FindExactReadyPeer(ZNet network, ZRpc rpc) { if ((Object)(object)network == (Object)null || rpc == null || !network.IsServer()) { return null; } List peers; try { peers = network.GetPeers(); } catch { return null; } if (peers == null) { return null; } int num = 0; foreach (ZNetPeer item in peers) { if (num++ >= 64) { break; } if (item != null && item.m_rpc == rpc && IsReady(item)) { return item; } } return null; } private static bool IsReady(ZNetPeer peer) { try { return peer != null && peer.IsReady(); } catch { return false; } } private void RemoveServerConnection(ZRpc rpc) { if (rpc == null || !_serverConnections.Remove(rpc)) { return; } try { rpc.Unregister("runic.sentinel.admin.request.v1"); } catch { } } private void ClearServerConnections() { foreach (ZRpc item in new List(_serverConnections.Keys)) { try { if (item != null) { item.Unregister("runic.sentinel.admin.request.v1"); } } catch { } } _serverConnections.Clear(); } private void ClearConnections(string reason) { ClearServerConnections(); } private void Cache(string key, byte[] digest, bool accepted, string reason, byte[] payload) { while (_cache.Count >= 256 && _cacheOrder.Count != 0) { _cache.Remove(_cacheOrder.Dequeue()); } _cache[key] = new CachedResponse { RequestDigest = digest, Accepted = accepted, Reason = Bounded(reason), Payload = (byte[])(payload ?? Array.Empty()).Clone(), Expires = DateTime.UtcNow.Ticks + ReplayLifetimeTicks }; _cacheOrder.Enqueue(key); } private void ExpireCache(long now) { while (_cacheOrder.Count != 0) { string key = _cacheOrder.Peek(); if (!_cache.TryGetValue(key, out var value) || value.Expires <= now) { _cacheOrder.Dequeue(); _cache.Remove(key); continue; } break; } } private static byte[] Digest(string action, byte[] payload, long issued) { byte[] bytes = Encoding.UTF8.GetBytes(action + "\n" + issued.ToString(CultureInfo.InvariantCulture) + "\n"); byte[] array = new byte[bytes.Length + payload.Length]; Buffer.BlockCopy(bytes, 0, array, 0, bytes.Length); Buffer.BlockCopy(payload, 0, array, bytes.Length, payload.Length); using SHA256 sHA = SHA256.Create(); return sHA.ComputeHash(array); } private static bool Fixed(byte[] left, byte[] right) { if (left != null && right != null && left.Length == right.Length) { return CryptographicOperations.FixedTimeEquals(left, right); } return false; } private static bool CanonicalId(string id) { Guid result; if (id != null && id.Length == 32) { return Guid.TryParseExact(id, "N", out result); } return false; } private static string Bounded(string reason) { if (!string.IsNullOrWhiteSpace(reason)) { if (reason.Length > 512) { return reason.Substring(0, 512); } return reason; } return "Administrator operation failed."; } public void Dispose() { _disposed = true; ClearConnections("Administrator control stopped."); _cache.Clear(); _cacheOrder.Clear(); _network = null; } } internal sealed class SentinelAdminDocument { internal long Sequence; internal string Profile = "runic-suite"; internal string ExpiresUnixSeconds = "0"; internal string UnknownMods = "Forbidden"; internal string RequiredMods = string.Empty; internal string OptionalMods = string.Empty; internal string GrayListMods = string.Empty; internal string ForbiddenMods = string.Empty; internal string Administrators = string.Empty; internal string BannedUsers = string.Empty; internal string Modules = string.Empty; internal string DetectedProfile = string.Empty; internal string Integrity = string.Empty; internal string LastDenial = string.Empty; internal string AdmissionMode = "Optional"; internal string IntegritySeconds = "15"; internal string VeryHighThreshold = "2"; internal string HighThreshold = "3"; internal string EnforcementWindowSeconds = "60"; internal bool BackupTransitions = true; internal bool ManagedSigningKey; internal string SigningKeyPin = string.Empty; internal string Status = string.Empty; } internal static class SentinelAdminProtocol { internal const int MaximumWireBytes = 122880; private const string Header = "RUNIC-SENTINEL-ADMIN/1\n"; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static byte[] Encode(SentinelAdminDocument value) { if (value == null) { throw new ArgumentNullException("value"); } Dictionary obj = new Dictionary(StringComparer.Ordinal) { ["sequence"] = value.Sequence.ToString(CultureInfo.InvariantCulture), ["profile"] = value.Profile, ["expires"] = value.ExpiresUnixSeconds, ["unknown"] = value.UnknownMods, ["required"] = value.RequiredMods, ["optional"] = value.OptionalMods, ["gray"] = value.GrayListMods, ["forbidden"] = value.ForbiddenMods, ["admins"] = value.Administrators, ["bans"] = value.BannedUsers, ["modules"] = value.Modules, ["detected"] = value.DetectedProfile, ["integrity"] = value.Integrity, ["last-denial"] = value.LastDenial, ["admission"] = value.AdmissionMode, ["integrity-seconds"] = value.IntegritySeconds, ["very-high"] = value.VeryHighThreshold, ["high"] = value.HighThreshold, ["window"] = value.EnforcementWindowSeconds, ["backup"] = (value.BackupTransitions ? "1" : "0"), ["managed-key"] = (value.ManagedSigningKey ? "1" : "0"), ["key-pin"] = value.SigningKeyPin, ["status"] = value.Status }; StringBuilder stringBuilder = new StringBuilder("RUNIC-SENTINEL-ADMIN/1\n"); foreach (KeyValuePair item in obj) { stringBuilder.Append(item.Key).Append('=').Append(Convert.ToBase64String(StrictUtf8.GetBytes(item.Value ?? string.Empty))) .Append('\n'); } byte[] bytes = StrictUtf8.GetBytes(stringBuilder.ToString()); if (bytes.Length > 122880) { throw new InvalidDataException("admin-document-too-large"); } return bytes; } internal static bool TryDecode(byte[] bytes, out SentinelAdminDocument value) { value = null; if (bytes == null || bytes.Length == 0 || bytes.Length > 122880) { return false; } string text; try { text = StrictUtf8.GetString(bytes); } catch { return false; } if (!text.StartsWith("RUNIC-SENTINEL-ADMIN/1\n", StringComparison.Ordinal) || text.IndexOf('\r') >= 0 || !text.EndsWith("\n", StringComparison.Ordinal)) { return false; } string[] array = text.Split('\n'); Dictionary dictionary = new Dictionary(StringComparer.Ordinal); for (int i = 1; i < array.Length - 1; i++) { int num = array[i].IndexOf('='); if (num <= 0 || !dictionary.TryAdd(array[i].Substring(0, num), Decode(array[i].Substring(num + 1)))) { return false; } } if (!TryLong(dictionary, "sequence", out var result)) { return false; } value = new SentinelAdminDocument { Sequence = result, Profile = Get(dictionary, "profile"), ExpiresUnixSeconds = Get(dictionary, "expires"), UnknownMods = Get(dictionary, "unknown"), RequiredMods = Get(dictionary, "required"), OptionalMods = Get(dictionary, "optional"), GrayListMods = Get(dictionary, "gray"), ForbiddenMods = Get(dictionary, "forbidden"), Administrators = Get(dictionary, "admins"), BannedUsers = Get(dictionary, "bans"), Modules = Get(dictionary, "modules"), DetectedProfile = Get(dictionary, "detected"), Integrity = Get(dictionary, "integrity"), LastDenial = Get(dictionary, "last-denial"), AdmissionMode = Get(dictionary, "admission"), IntegritySeconds = Get(dictionary, "integrity-seconds"), VeryHighThreshold = Get(dictionary, "very-high"), HighThreshold = Get(dictionary, "high"), EnforcementWindowSeconds = Get(dictionary, "window"), BackupTransitions = (Get(dictionary, "backup") == "1"), ManagedSigningKey = (Get(dictionary, "managed-key") == "1"), SigningKeyPin = Get(dictionary, "key-pin"), Status = Get(dictionary, "status") }; return true; } internal static byte[] EncodeTool(string tool) { string text = tool ?? string.Empty; if (text != "report" && text != "networks" && text != "backup") { throw new ArgumentException("Unknown admin tool.", "tool"); } return StrictUtf8.GetBytes("RUNIC-SENTINEL-ADMIN-TOOL/1\n" + text + "\n"); } internal static bool TryDecodeTool(byte[] bytes, out string tool) { tool = string.Empty; if (bytes == null || bytes.Length > 128) { return false; } string text; try { text = StrictUtf8.GetString(bytes); } catch { return false; } if (!text.StartsWith("RUNIC-SENTINEL-ADMIN-TOOL/1\n", StringComparison.Ordinal) || !text.EndsWith("\n", StringComparison.Ordinal)) { return false; } tool = text.Substring("RUNIC-SENTINEL-ADMIN-TOOL/1\n".Length, text.Length - "RUNIC-SENTINEL-ADMIN-TOOL/1\n".Length - 1); if (!(tool == "report") && !(tool == "networks")) { return tool == "backup"; } return true; } internal static byte[] EncodeMessage(string value) { byte[] bytes = StrictUtf8.GetBytes(value ?? string.Empty); if (bytes.Length > 4096) { throw new InvalidDataException("admin-message-too-large"); } return bytes; } internal static string DecodeMessage(byte[] bytes) { if (bytes == null || bytes.Length > 4096) { return "invalid-response"; } try { return StrictUtf8.GetString(bytes); } catch { return "invalid-response"; } } private static string Decode(string value) { try { return StrictUtf8.GetString(Convert.FromBase64String(value)); } catch { return null; } } private static string Get(IDictionary values, string key) { if (!values.TryGetValue(key, out var value) || value == null) { return string.Empty; } return value; } private static bool TryLong(IDictionary values, string key, out long result) { if (long.TryParse(Get(values, key), NumberStyles.None, CultureInfo.InvariantCulture, out result)) { return result >= 0; } return false; } } internal static class SentinelDraftExporter { internal const string FileName = "RunicSentinel.current-profile.json"; internal static void TryWrite(string configRoot, AttestationSnapshot snapshot) { try { if (snapshot != null && !string.IsNullOrEmpty(configRoot)) { string text = Path.Combine(Path.GetFullPath(configRoot), "RunicSentinel.current-profile.json"); string text2 = text + ".tmp"; byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(Build(snapshot)); using (FileStream fileStream = new FileStream(text2, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.WriteThrough)) { fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } if (File.Exists(text)) { File.Replace(text2, text, null); } else { File.Move(text2, text); } } } catch { } } private static string Build(AttestationSnapshot snapshot) { StringBuilder stringBuilder = new StringBuilder(4096 + snapshot.Plugins.Count * 160); stringBuilder.Append("{\n \"profile\": \"runic-suite\",\n \"sequence\": 1,\n \"issued\": ").Append(DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)).Append(",\n \"expires\": 0,\n \"unknownMods\": \"Forbidden\",\n") .Append(" \"requiredMods\": [\n"); for (int i = 0; i < snapshot.Plugins.Count; i++) { AttestedPlugin attestedPlugin = snapshot.Plugins[i]; stringBuilder.Append(" { \"id\": \"").Append(Json(attestedPlugin.Id)).Append("\", \"version\": \"") .Append(Json(attestedPlugin.Version)) .Append("\", \"sha256\": \"") .Append(attestedPlugin.Sha256) .Append("\" }") .Append((i + 1 == snapshot.Plugins.Count) ? "\n" : ",\n"); } stringBuilder.Append(" ],\n \"optionalMods\": [],\n \"grayListMods\": [],\n").Append(" \"forbiddenMods\": [],\n \"modules\": [],\n").Append(" \"administrators\": [],\n \"bannedUsers\": []\n}\n"); return stringBuilder.ToString(); } private static string Json(string value) { StringBuilder stringBuilder = new StringBuilder(value?.Length ?? 0); string text = value ?? string.Empty; foreach (char c in text) { switch (c) { case '\\': stringBuilder.Append("\\\\"); continue; case '"': stringBuilder.Append("\\\""); continue; case '\b': stringBuilder.Append("\\b"); continue; case '\f': stringBuilder.Append("\\f"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } } internal sealed class SentinelEnforcementRuntime : IDisposable { private sealed class EscalationState { internal long Started { get; } internal int High { get; set; } internal int VeryHigh { get; set; } internal EscalationState(long started) { Started = started; } } private const int MaximumTrackedPeers = 256; private readonly object _gate = new object(); private readonly SentinelRuntime _runtime; private readonly ISentinelEvidenceProviderLease _evidence; private readonly Dictionary _states = new Dictionary(); private bool _disposed; internal SentinelEnforcementRuntime(SentinelRuntime runtime) { _runtime = runtime ?? throw new ArgumentNullException("runtime"); _evidence = runtime.Evidence.RegisterProvider("runic.sentinel.enforcement"); } internal bool ReportRejectedServerRequest(string sourceModuleId, long peerId, string actor, string rule, string correlationId, FindingConfidence confidence, string detail) { if (sourceModuleId != "runic.portals" || peerId == 0L || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return false; } bool flag = false; long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); lock (_gate) { if (_disposed) { return false; } Prune(num); long num2 = Math.Max(10, Math.Min(600, SentinelConfig.EnforcementWindowSeconds?.Value ?? 60)); int num3 = Math.Max(1, Math.Min(10, SentinelConfig.VeryHighDisconnectCount?.Value ?? 2)); int num4 = Math.Max(1, Math.Min(20, SentinelConfig.HighDisconnectCount?.Value ?? 3)); if (!_states.TryGetValue(peerId, out var value) || num - value.Started > num2) { value = new EscalationState(num); } if (confidence >= FindingConfidence.High) { value.High++; } if (confidence >= FindingConfidence.VeryHigh) { value.VeryHigh++; } flag = confidence == FindingConfidence.Conclusive || value.VeryHigh >= num3 || value.High >= num4; _states[peerId] = value; _evidence.Sink.TryAppend(Safe(actor, "peer:" + peerId), Safe(rule, "security-violation"), Safe(correlationId, Guid.NewGuid().ToString("N")), confidence, flag ? EnforcementAction.Disconnect : EnforcementAction.Cancel, Safe(detail, "request-denied"), out var _); } if (!flag) { return true; } try { ZNetPeer peer = ZNet.instance.GetPeer(peerId); if (peer != null && peer.m_uid == peerId && peer.IsReady()) { ZNet.instance.Disconnect(peer); } } catch { } return true; } private void Prune(long now) { long num = Math.Max(10, Math.Min(600, SentinelConfig.EnforcementWindowSeconds?.Value ?? 60)); List list = new List(); foreach (KeyValuePair state in _states) { if (now - state.Value.Started > num) { list.Add(state.Key); } } foreach (long item in list) { _states.Remove(item); } if (_states.Count < 256) { return; } long key = 0L; long num2 = long.MaxValue; foreach (KeyValuePair state2 in _states) { if (state2.Value.Started < num2) { num2 = state2.Value.Started; key = state2.Key; } } _states.Remove(key); } private static string Safe(string value, string fallback) { string text = (string.IsNullOrEmpty(value) ? fallback : value); if (text.Length > 128) { text = text.Substring(0, 128); } return text; } public void Dispose() { lock (_gate) { if (_disposed) { return; } _disposed = true; _states.Clear(); } try { _evidence.Dispose(); } catch { } } } internal sealed class SentinelFlightRecorder : IDisposable { internal const long MaximumFileBytes = 524288L; private static readonly UTF8Encoding Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); private static readonly byte[] Header = Utf8.GetBytes("RUNIC-SENTINEL-FLIGHT/1\n"); private readonly object _gate = new object(); private readonly EvidenceLedger _ledger; private readonly ManualLogSource _log; private readonly string _activePath; private readonly string _previousPath; private bool _disposed; private bool _faultLogged; internal string ActivePath => _activePath; internal SentinelFlightRecorder(EvidenceLedger ledger, ManualLogSource log, string configRoot) { _ledger = ledger ?? throw new ArgumentNullException("ledger"); _log = log; string path = Path.Combine(Path.GetFullPath(configRoot), "RunicSentinel", "flight-recorder"); _activePath = Path.Combine(path, "security-current.log"); _previousPath = Path.Combine(path, "security-previous.log"); _ledger.Accepted += OnAccepted; } private void OnAccepted(SecurityEvidence evidence) { if (evidence == null) { return; } byte[] bytes = Utf8.GetBytes(Encode(evidence)); lock (_gate) { if (_disposed) { return; } try { Directory.CreateDirectory(Path.GetDirectoryName(_activePath)); long num = (File.Exists(_activePath) ? new FileInfo(_activePath).Length : 0); long num2 = (long)bytes.Length + (long)((num == 0L) ? Header.Length : 0); if (num + num2 > 524288) { if (File.Exists(_previousPath)) { File.Delete(_previousPath); } if (File.Exists(_activePath)) { File.Move(_activePath, _previousPath); } num = 0L; } using FileStream fileStream = new FileStream(_activePath, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.WriteThrough); if (num == 0L) { fileStream.Write(Header, 0, Header.Length); } fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } catch (Exception ex) { if (!_faultLogged) { _faultLogged = true; ManualLogSource log = _log; if (log != null) { log.LogWarning((object)("Sentinel flight-recorder write failed; enforcement remains active: " + ex.GetType().Name + ".")); } } } } } private static string Encode(SecurityEvidence value) { return value.Sequence.ToString(CultureInfo.InvariantCulture) + "|" + value.UnixSeconds.ToString(CultureInfo.InvariantCulture) + "|" + Base64(value.ProviderModuleId) + "|" + Base64(value.Actor) + "|" + Base64(value.Rule) + "|" + Base64(value.CorrelationId) + "|" + ((int)value.Confidence).ToString(CultureInfo.InvariantCulture) + "|" + ((int)value.RequestedAction).ToString(CultureInfo.InvariantCulture) + "|" + ((int)value.EffectiveAction).ToString(CultureInfo.InvariantCulture) + "|" + value.PolicySequence.ToString(CultureInfo.InvariantCulture) + "|" + Base64(value.Detail) + "\n"; } private static string Base64(string value) { return Convert.ToBase64String(Utf8.GetBytes(value ?? string.Empty)); } public void Dispose() { lock (_gate) { if (_disposed) { return; } _disposed = true; } _ledger.Accepted -= OnAccepted; } } internal enum SentinelIntegrityState { Unavailable, MonitorOnly, Ready, Compromised } internal sealed class SentinelIntegritySnapshot { internal SentinelIntegrityState State { get; } internal long CheckedUnixSeconds { get; } internal string ReasonCode { get; } internal string PolicyDigest { get; } internal SentinelIntegritySnapshot(SentinelIntegrityState state, long checkedUnixSeconds, string reasonCode, string policyDigest) { if (!Enum.IsDefined(typeof(SentinelIntegrityState), state) || checkedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("state"); } State = state; CheckedUnixSeconds = checkedUnixSeconds; ReasonCode = (string.IsNullOrEmpty(reasonCode) ? "unavailable" : reasonCode); PolicyDigest = policyDigest ?? string.Empty; } } internal sealed class SentinelManagedPolicyService { private sealed class Rule { internal string Classification { get; } internal string Id { get; } internal string Version { get; } internal string Hash { get; } internal Rule(string classification, string id, string version, string hash) { Classification = classification; Id = id; Version = version; Hash = hash; } } private const string PrivateHeader = "RUNIC-RSA-PRIVATE/1"; private readonly object _gate = new object(); private readonly SentinelRuntime _runtime; private readonly ManualLogSource _log; private readonly string _configRoot; private readonly string _privatePath; private readonly Func _backup; internal bool HasManagedKey => File.Exists(_privatePath); internal SentinelManagedPolicyService(SentinelRuntime runtime, ManualLogSource log, string configRoot, Func backup) { _runtime = runtime ?? throw new ArgumentNullException("runtime"); _log = log; _configRoot = Path.GetFullPath(configRoot); _privatePath = Path.Combine(_configRoot, "RunicSentinel", "server-private", "RunicSentinel.private.key"); _backup = backup; } internal SentinelAdminDocument CreateDocument(string status = "Ready") { SentinelAdminDocument sentinelAdminDocument = new SentinelAdminDocument { Status = status, AdmissionMode = _runtime.EffectiveRemoteAdmissionMode.ToString(), IntegritySeconds = (SentinelConfig.IntegrityCheckSeconds?.Value ?? 15).ToString(CultureInfo.InvariantCulture), VeryHighThreshold = (SentinelConfig.VeryHighDisconnectCount?.Value ?? 2).ToString(CultureInfo.InvariantCulture), HighThreshold = (SentinelConfig.HighDisconnectCount?.Value ?? 3).ToString(CultureInfo.InvariantCulture), EnforcementWindowSeconds = (SentinelConfig.EnforcementWindowSeconds?.Value ?? 60).ToString(CultureInfo.InvariantCulture), BackupTransitions = (SentinelConfig.BackupBeforeTransitions?.Value ?? true), ManagedSigningKey = HasManagedKey, Integrity = _runtime.GetIntegritySnapshot().State.ToString() + ":" + _runtime.GetIntegritySnapshot().ReasonCode, LastDenial = _runtime.LastAdmissionFailure }; if (_runtime.TryGetVerifiedPolicy(out var policy)) { sentinelAdminDocument.Sequence = policy.Sequence; sentinelAdminDocument.Profile = policy.Profile; sentinelAdminDocument.ExpiresUnixSeconds = policy.ExpiresUnixSeconds.ToString(CultureInfo.InvariantCulture); sentinelAdminDocument.UnknownMods = policy.Unknown.ToString(); sentinelAdminDocument.RequiredMods = PluginLines(policy, PluginClassification.Required); sentinelAdminDocument.OptionalMods = PluginLines(policy, PluginClassification.ApprovedOptional); sentinelAdminDocument.GrayListMods = PluginLines(policy, PluginClassification.Unmanaged); sentinelAdminDocument.ForbiddenMods = PluginLines(policy, PluginClassification.Forbidden); sentinelAdminDocument.Administrators = IdentityLines(policy.Administrators); sentinelAdminDocument.BannedUsers = IdentityLines(policy.BannedUsers); sentinelAdminDocument.Modules = "Standalone Sentinel transport; no Runic Core or Runic Persistence dependency."; sentinelAdminDocument.SigningKeyPin = SentinelConfig.TrustedPublicKeySha256?.Value ?? string.Empty; } AttestationSnapshot snapshot; string status2; string text = ((!_runtime.TryGetCurrent(out snapshot, out status2)) ? ("Snapshot unavailable: " + status2) : string.Join("\n", snapshot.Plugins.Select((AttestedPlugin plugin) => plugin.Id + "|" + plugin.Version + "|" + plugin.Sha256))); AdmissionClientProfile profile; string text2 = (_runtime.TryGetLastRemoteAdmissionProfile(out profile) ? string.Join("\n", profile.Plugins.Select((AdmissionPluginEvidence plugin) => plugin.Id + "|" + plugin.Version + "|" + plugin.Sha256)) : "No client report observed in this process lifetime."); sentinelAdminDocument.DetectedProfile = "SERVER PROFILE (not a client allowlist)\n" + text + "\n\nMOST RECENT CLIENT REPORT\n" + text2; return sentinelAdminDocument; } internal string Bootstrap(string authority, string subject) { lock (_gate) { if (!CanonicalAuthority(authority) || !CanonicalSubject(subject)) { throw new InvalidDataException("bootstrap-identity-invalid"); } if (File.Exists(_privatePath)) { throw new InvalidOperationException("managed-signing-key-already-exists"); } if (!_runtime.TryGetCurrent(out var snapshot, out var _)) { throw new InvalidOperationException("sentinel-snapshot-not-ready"); } RSAParameters rSAParameters; using (RSA rSA = CreateManagedRsa3072()) { rSAParameters = rSA.ExportParameters(includePrivateParameters: true); } SentinelPolicy policy; SentinelAdminDocument sentinelAdminDocument = (_runtime.TryGetVerifiedPolicy(out policy) ? CreateDocument("Bootstrap") : DefaultDocument(snapshot)); SortedDictionary sortedDictionary = ParseIdentities(sentinelAdminDocument.Administrators, "administrators"); sortedDictionary[authority + ":" + Uri.EscapeDataString(subject)] = new SentinelAdministratorRole(authority, subject); sentinelAdminDocument.Administrators = IdentityLines(sortedDictionary.Values); string text = ApplyCore(sentinelAdminDocument, null, rSAParameters, changingTrustRoot: true); Directory.CreateDirectory(Path.GetDirectoryName(_privatePath)); try { WriteExclusive(_privatePath, EncodePrivate(rSAParameters)); } catch { throw new IOException("managed-key-persistence-failed-after-policy-signing"); } return text + " Initial administrator: " + authority + ":" + subject + "."; } } internal string Apply(SentinelAdminDocument draft, string callerAuthority, string callerSubject) { if (draft == null) { throw new ArgumentNullException("draft"); } lock (_gate) { if (!File.Exists(_privatePath)) { throw new InvalidOperationException("server-managed-signing-key-required"); } if (!_runtime.TryGetVerifiedPolicy(out var policy)) { throw new InvalidOperationException("verified-policy-required"); } if (draft.Sequence != policy.Sequence) { throw new InvalidOperationException("policy-sequence-stale"); } RSAParameters privateParameters = DecodePrivate(File.ReadAllBytes(_privatePath)); return ApplyCore(draft, callerAuthority + ":" + Uri.EscapeDataString(callerSubject), privateParameters, changingTrustRoot: false); } } private string ApplyCore(SentinelAdminDocument draft, string callerKey, RSAParameters privateParameters, bool changingTrustRoot) { ValidateSettings(draft); if (!string.Equals(draft.AdmissionMode, _runtime.EffectiveRemoteAdmissionMode.ToString(), StringComparison.Ordinal)) { throw new InvalidDataException("admission-mode-restart-required"); } SentinelPolicy policy; long num = (_runtime.TryGetVerifiedPolicy(out policy) ? checked(policy.Sequence + 1) : 1); long issued = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); byte[] array = BuildPolicy(draft, num, issued, callerKey); byte[] array2; byte[] array3; string text; using (RSA rSA = ImportManagedRsa3072(privateParameters)) { array2 = rSA.SignData(array, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); array3 = EncodePublic(rSA.ExportParameters(includePrivateParameters: false)); text = Sha256(array3); } if (!PinnedRsaPublicKey.TryParse(array3, text, out var key, out var failure) || !SentinelPolicy.TryParseAndVerify(array, array2, key, out var policy2, out failure)) { throw new InvalidDataException("generated-policy-invalid-" + failure); } if (policy2.Administrators.Count == 0) { throw new InvalidDataException("at-least-one-administrator-required"); } string left = SentinelConfig.TrustedPublicKeySha256?.Value ?? string.Empty; if (!changingTrustRoot && !SentinelPolicy.FixedTimeHexEquals(left, text)) { throw new InvalidOperationException("managed-key-does-not-match-active-trust-root"); } string text2 = _backup?.Invoke("runic-sentinel-admin-policy-apply") ?? "no-world-loaded"; ArchiveCurrent(num); AtomicWrite(Resolve(SentinelConfig.PolicyFile?.Value), array); AtomicWrite(Resolve(SentinelConfig.SignatureFile?.Value), Encoding.ASCII.GetBytes(Convert.ToBase64String(array2) + "\n")); AtomicWrite(Resolve(SentinelConfig.PublicKeyFile?.Value), array3); if (SentinelConfig.TrustedPublicKeySha256 != null && !string.Equals(SentinelConfig.TrustedPublicKeySha256.Value, text, StringComparison.Ordinal)) { SentinelConfig.TrustedPublicKeySha256.Value = text; } ApplySettings(draft); _runtime.Start(_configRoot); ManualLogSource log = _log; if (log != null) { log.LogWarning((object)("Raven's Gate administrator applied signed policy sequence " + num + "; backup=" + text2 + ". Connected clients must receive the public passport before their next strict admission.")); } return "Applied signed policy sequence " + num + ". Backup: " + text2 + ". Public-key pin: " + text + ". Admission mode remains " + _runtime.EffectiveRemoteAdmissionMode.ToString() + "."; } internal static RSA CreateManagedRsa3072() { RSA rSA = null; try { rSA = RSA.Create(); rSA.KeySize = 3072; if (IsExactRsa3072(rSA, requirePrivate: true)) { return rSA; } } catch { } rSA?.Dispose(); try { rSA = new RSACryptoServiceProvider(3072) { PersistKeyInCsp = false }; if (IsExactRsa3072(rSA, requirePrivate: true)) { return rSA; } } catch { } rSA?.Dispose(); throw new CryptographicException("rsa-3072-unavailable"); } internal static RSA ImportManagedRsa3072(RSAParameters parameters) { RSA rSA = null; try { rSA = RSA.Create(); rSA.ImportParameters(parameters); if (IsExactRsa3072(rSA, requirePrivate: true)) { return rSA; } } catch { } rSA?.Dispose(); try { rSA = new RSACryptoServiceProvider { PersistKeyInCsp = false }; rSA.ImportParameters(parameters); if (IsExactRsa3072(rSA, requirePrivate: true)) { return rSA; } } catch { } rSA?.Dispose(); throw new CryptographicException("managed-key-not-rsa-3072"); } private static bool IsExactRsa3072(RSA rsa, bool requirePrivate) { if (rsa == null || rsa.KeySize != 3072) { return false; } try { RSAParameters rSAParameters = rsa.ExportParameters(requirePrivate); return rSAParameters.Modulus != null && rSAParameters.Modulus.Length == 384 && rSAParameters.Exponent != null && rSAParameters.Exponent.Length == 3 && rSAParameters.Exponent[0] == 1 && rSAParameters.Exponent[1] == 0 && rSAParameters.Exponent[2] == 1 && (!requirePrivate || (rSAParameters.D != null && rSAParameters.D.Length != 0)); } catch { return false; } } private byte[] BuildPolicy(SentinelAdminDocument draft, long sequence, long issued, string callerKey) { if (!SentinelPolicy.CanonicalAtom(draft.Profile, 1, 64)) { throw new InvalidDataException("profile-invalid"); } if (!long.TryParse(draft.ExpiresUnixSeconds, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < 0 || (result != 0L && result <= issued)) { throw new InvalidDataException("expiration-must-be-zero-or-future-unix-time"); } if (draft.UnknownMods != "Forbidden" && draft.UnknownMods != "Quarantined" && draft.UnknownMods != "Unmanaged") { throw new InvalidDataException("unknown-mod-policy-invalid"); } SortedDictionary sortedDictionary = new SortedDictionary(StringComparer.Ordinal); AddRules(draft.RequiredMods, "Required", sortedDictionary); AddRules(draft.OptionalMods, "ApprovedOptional", sortedDictionary); AddRules(draft.GrayListMods, "Unmanaged", sortedDictionary); AddRules(draft.ForbiddenMods, "Forbidden", sortedDictionary); SortedDictionary sortedDictionary2 = ParseIdentities(draft.Administrators, "administrators"); SortedDictionary sortedDictionary3 = ParseIdentities(draft.BannedUsers, "banned-users"); if (sortedDictionary2.Keys.Any(sortedDictionary3.ContainsKey)) { throw new InvalidDataException("identity-cannot-be-admin-and-banned"); } if (sortedDictionary2.Count == 0) { throw new InvalidDataException("at-least-one-administrator-required"); } if (callerKey != null && !sortedDictionary2.ContainsKey(callerKey) && sortedDictionary2.Count < 1) { throw new InvalidDataException("last-administrator-cannot-be-removed"); } IReadOnlyList source = Array.Empty(); StringBuilder stringBuilder = new StringBuilder(4096); stringBuilder.Append("RUNIC-SENTINEL/3\nprofile=").Append(draft.Profile).Append("\nsequence=") .Append(sequence.ToString(CultureInfo.InvariantCulture)) .Append("\nissued=") .Append(issued.ToString(CultureInfo.InvariantCulture)) .Append("\nexpires=") .Append(result.ToString(CultureInfo.InvariantCulture)) .Append("\nunknown=") .Append(draft.UnknownMods) .Append("\nunknown-capability=Forbidden\n"); foreach (Rule value in sortedDictionary.Values) { stringBuilder.Append("rule=").Append(value.Classification).Append('|') .Append(value.Id) .Append('|') .Append(value.Version) .Append('|') .Append(value.Hash) .Append('\n'); } foreach (SentinelModuleRule item in source.OrderBy((SentinelModuleRule value) => value.Id, StringComparer.Ordinal)) { stringBuilder.Append("module=").Append(item.Scope).Append('|') .Append(item.Id) .Append('|') .Append(item.Version) .Append('|') .Append(item.Protocol.ToString(CultureInfo.InvariantCulture)) .Append('|') .Append(string.Join(",", item.Capabilities)) .Append('\n'); } foreach (SentinelAdministratorRole value2 in sortedDictionary2.Values) { stringBuilder.Append("role=").Append(value2.Authority).Append('|') .Append(Uri.EscapeDataString(value2.Subject)) .Append('\n'); } foreach (SentinelAdministratorRole value3 in sortedDictionary3.Values) { stringBuilder.Append("ban=").Append(value3.Authority).Append('|') .Append(Uri.EscapeDataString(value3.Subject)) .Append('\n'); } byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetBytes(stringBuilder.ToString()); if (bytes.Length > 1048576 || bytes.Length > 122880) { throw new InvalidDataException("policy-exceeds-admin-panel-bound"); } return bytes; } private SentinelAdminDocument DefaultDocument(AttestationSnapshot snapshot) { return new SentinelAdminDocument { Profile = "runic-suite", Sequence = 0L, ExpiresUnixSeconds = "0", UnknownMods = "Unmanaged", RequiredMods = string.Empty, Modules = string.Empty, AdmissionMode = (SentinelConfig.RemoteAdmissionPolicy?.Value ?? "Optional"), IntegritySeconds = "15", VeryHighThreshold = "2", HighThreshold = "3", EnforcementWindowSeconds = "60", BackupTransitions = true }; } private static void AddRules(string text, string classification, IDictionary target) { foreach (string item in Lines(text)) { string[] array = item.Split('|'); if (array.Length != 3 || !SentinelPolicy.CanonicalPluginId(array[0]) || !SentinelPolicy.CanonicalVersionOrWildcard(array[1]) || !SentinelPolicy.CanonicalHashOrWildcard(array[2])) { throw new InvalidDataException("plugin-rule-invalid-" + item); } if (target.ContainsKey(array[0])) { throw new InvalidDataException("plugin-listed-more-than-once-" + array[0]); } target.Add(array[0], new Rule(classification, array[0], array[1], array[2])); } } private static SortedDictionary ParseIdentities(string text, string label) { SortedDictionary sortedDictionary = new SortedDictionary(StringComparer.Ordinal); foreach (string item in Lines(text)) { string[] array = item.Split('|'); if (array.Length != 2 || !CanonicalAuthority(array[0]) || !CanonicalSubject(array[1])) { throw new InvalidDataException(label + "-identity-invalid-" + item); } string text2 = array[0] + ":" + Uri.EscapeDataString(array[1]); if (sortedDictionary.ContainsKey(text2)) { throw new InvalidDataException(label + "-duplicate-" + text2); } sortedDictionary.Add(text2, new SentinelAdministratorRole(array[0], array[1])); } return sortedDictionary; } private void ApplySettings(SentinelAdminDocument value) { int value2 = BoundedInt(value.IntegritySeconds, 5, 300, "integrity-seconds"); int value3 = BoundedInt(value.VeryHighThreshold, 1, 10, "very-high-threshold"); int value4 = BoundedInt(value.HighThreshold, 1, 20, "high-threshold"); int value5 = BoundedInt(value.EnforcementWindowSeconds, 10, 600, "enforcement-window"); if (value.AdmissionMode != "Disabled" && value.AdmissionMode != "Optional" && value.AdmissionMode != "Required") { throw new InvalidDataException("admission-mode-invalid"); } Set(SentinelConfig.IntegrityCheckSeconds, value2); Set(SentinelConfig.VeryHighDisconnectCount, value3); Set(SentinelConfig.HighDisconnectCount, value4); Set(SentinelConfig.EnforcementWindowSeconds, value5); Set(SentinelConfig.BackupBeforeTransitions, value.BackupTransitions); } private static void ValidateSettings(SentinelAdminDocument value) { BoundedInt(value.IntegritySeconds, 5, 300, "integrity-seconds"); BoundedInt(value.VeryHighThreshold, 1, 10, "very-high-threshold"); BoundedInt(value.HighThreshold, 1, 20, "high-threshold"); BoundedInt(value.EnforcementWindowSeconds, 10, 600, "enforcement-window"); if (value.AdmissionMode != "Disabled" && value.AdmissionMode != "Optional" && value.AdmissionMode != "Required") { throw new InvalidDataException("admission-mode-invalid"); } } private void ArchiveCurrent(long nextSequence) { string text = Path.Combine(_configRoot, "RunicSentinel", "policy-history", "before-sequence-" + nextSequence.ToString(CultureInfo.InvariantCulture)); Directory.CreateDirectory(text); CopyIfPresent(Resolve(SentinelConfig.PolicyFile?.Value), Path.Combine(text, "RunicSentinel.policy")); CopyIfPresent(Resolve(SentinelConfig.SignatureFile?.Value), Path.Combine(text, "RunicSentinel.policy.sig")); CopyIfPresent(Resolve(SentinelConfig.PublicKeyFile?.Value), Path.Combine(text, "RunicSentinel.policy.pub")); } private string Resolve(string configured) { if (!Path.IsPathRooted(configured ?? string.Empty)) { return Path.GetFullPath(Path.Combine(_configRoot, configured ?? string.Empty)); } return Path.GetFullPath(configured); } private static void AtomicWrite(string path, byte[] bytes) { Directory.CreateDirectory(Path.GetDirectoryName(path)); string text = path + ".admin.tmp"; using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.WriteThrough)) { fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } if (File.Exists(path)) { File.Replace(text, path, null); } else { File.Move(text, path); } } private static void WriteExclusive(string path, byte[] bytes) { using FileStream fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough); fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } private static byte[] EncodePrivate(RSAParameters value) { return Encoding.ASCII.GetBytes("RUNIC-RSA-PRIVATE/1\n" + Text("modulus", value.Modulus) + Text("exponent", value.Exponent) + Text("d", value.D) + Text("p", value.P) + Text("q", value.Q) + Text("dp", value.DP) + Text("dq", value.DQ) + Text("inverseq", value.InverseQ)); static string Text(string name, byte[] bytes) { return name + "=" + Convert.ToBase64String(bytes) + "\n"; } } private static RSAParameters DecodePrivate(byte[] bytes) { if (bytes == null || bytes.Length == 0 || bytes.Length > 65536) { throw new InvalidDataException("managed-key-size-invalid"); } string[] array = Encoding.ASCII.GetString(bytes).Split('\n'); if (array.Length != 10 || array[0] != "RUNIC-RSA-PRIVATE/1" || array[9].Length != 0) { throw new InvalidDataException("managed-key-format-invalid"); } Dictionary values = new Dictionary(StringComparer.Ordinal); for (int i = 1; i < 9; i++) { int num = array[i].IndexOf('='); if (num <= 0 || values.ContainsKey(array[i].Substring(0, num))) { throw new InvalidDataException("managed-key-field-invalid"); } try { values.Add(array[i].Substring(0, num), Convert.FromBase64String(array[i].Substring(num + 1))); } catch { throw new InvalidDataException("managed-key-base64-invalid"); } } return new RSAParameters { Modulus = Get("modulus"), Exponent = Get("exponent"), D = Get("d"), P = Get("p"), Q = Get("q"), DP = Get("dp"), DQ = Get("dq"), InverseQ = Get("inverseq") }; byte[] Get(string key) { if (!values.TryGetValue(key, out var value) || value.Length == 0) { throw new InvalidDataException("managed-key-field-missing-" + key); } return value; } } private static byte[] EncodePublic(RSAParameters value) { return Encoding.ASCII.GetBytes("RUNIC-RSA-PUBLIC/1\nmodulus=" + Convert.ToBase64String(value.Modulus) + "\nexponent=" + Convert.ToBase64String(value.Exponent) + "\n"); } private static string Sha256(byte[] bytes) { using SHA256 sHA = SHA256.Create(); return SentinelPolicy.Hex(sHA.ComputeHash(bytes)); } private static string PluginLines(SentinelPolicy policy, PluginClassification kind) { return string.Join("\n", from value in policy.Rules where value.Classification == kind select value.Id + "|" + value.Version + "|" + value.Sha256); } private static string IdentityLines(IEnumerable values) { return string.Join("\n", from value in values.OrderBy((SentinelAdministratorRole value) => value.CanonicalKey, StringComparer.Ordinal) select value.Authority + "|" + value.Subject); } private static string ModuleLines(IEnumerable values) { return string.Join("\n", from value in values.OrderBy((SentinelModuleRule value) => value.Id, StringComparer.Ordinal) select value.Scope.ToString() + "|" + value.Id + "|" + value.Version + "|" + value.Protocol + "|" + string.Join(",", value.Capabilities)); } private static IEnumerable Lines(string value) { return from line in (value ?? string.Empty).Replace("\r", string.Empty).Split('\n') select line.Trim() into line where line.Length > 0 select line; } private static bool CanonicalAuthority(string value) { if (SentinelPolicy.CanonicalAtom(value, 1, 64)) { return value.All((char character) => character < 'A' || character > 'Z'); } return false; } private static bool CanonicalSubject(string value) { if (value != null && value.Length > 0 && value.Length <= 256 && !value.Any(char.IsControl) && !char.IsWhiteSpace(value[0])) { return !char.IsWhiteSpace(value[value.Length - 1]); } return false; } private static int BoundedInt(string value, int minimum, int maximum, string label) { if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < minimum || result > maximum) { throw new InvalidDataException(label + "-invalid"); } return result; } private static void Set(ConfigEntry entry, T value) { if (entry != null && !EqualityComparer.Default.Equals(entry.Value, value)) { entry.Value = value; } } private static void CopyIfPresent(string source, string destination) { if (File.Exists(source) && !File.Exists(destination)) { File.Copy(source, destination, overwrite: false); } } } internal static class SentinelNetworkMapWriter { private const int MaximumZdos = 16384; private const int MaximumEdges = 2048; private static readonly FieldInfo ObjectsField = AccessTools.Field(typeof(ZDOMan), "m_objectsByID"); private static readonly string[] ProductionRoles = new string[4] { "input", "fuel", "output", "replenishment" }; internal static bool TryAppend(StringBuilder builder, out string failure) { //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Unknown result type (might be due to invalid IL or missing references) //IL_022b: Unknown result type (might be due to invalid IL or missing references) //IL_02a2: Unknown result type (might be due to invalid IL or missing references) failure = string.Empty; if (builder == null) { failure = "builder-missing"; return false; } ZNet instance = ZNet.instance; ZDOMan instance2 = ZDOMan.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || instance2 == null) { failure = "server-console-required"; return false; } if (!(ObjectsField?.GetValue(instance2) is Dictionary dictionary)) { failure = "world-index-unavailable"; return false; } int val = 0; int num = 0; int num2 = 0; builder.Append("network-map=server-local-snapshot\n"); foreach (KeyValuePair item in dictionary) { if (val++ >= 16384) { break; } ZDO value = item.Value; if (value == null || !value.IsValid()) { continue; } string value2 = value.GetString("runic.portals.record", string.Empty); string value3 = Safe(value.GetString("runic.portals.network", string.Empty), 64); if (!string.IsNullOrEmpty(value2) || !string.IsNullOrEmpty(value3)) { num++; builder.Append("portal=").Append(Id(item.Key)).Append('|') .Append(value3) .Append('|') .Append(Safe(value.GetString("runic.portals.name", string.Empty), 64)) .Append('|') .Append(value.GetInt("runic.portals.networkKind", 0).ToString(CultureInfo.InvariantCulture)) .Append('|') .Append(Safe(value.GetString("runic.portals.group", string.Empty), 64)) .Append('|') .Append(Position(value.GetPosition())) .Append('\n'); } for (int i = 0; i < ProductionRoles.Length; i++) { if (num2 >= 2048) { break; } string text = ProductionRoles[i]; string text2 = value.GetString("runic.production." + text + ".record", string.Empty); if (!string.IsNullOrEmpty(text2) && !(text2 == "!")) { num2++; if (!TryReadProductionTarget(text2, i, out var linkId, out var target)) { target = "record-invalid"; } builder.Append("production-edge=").Append(Id(item.Key)).Append('|') .Append(value.GetPrefab().ToString(CultureInfo.InvariantCulture)) .Append('|') .Append(text) .Append('|') .Append(Safe(linkId, 128)) .Append('|') .Append(Safe(target, 160)) .Append('|') .Append(Position(value.GetPosition())) .Append('\n'); } } } builder.Append("network-map-summary=zdos:").Append(Math.Min(val, 16384).ToString(CultureInfo.InvariantCulture)).Append(",portals:") .Append(num.ToString(CultureInfo.InvariantCulture)) .Append(",production-edges:") .Append(num2.ToString(CultureInfo.InvariantCulture)) .Append(",truncated:") .Append((dictionary.Count > 16384 || num2 >= 2048) ? "true" : "false") .Append('\n'); return true; } private static bool TryReadProductionTarget(string encoded, int expectedRole, out string linkId, out string target) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Expected O, but got Unknown linkId = string.Empty; target = string.Empty; try { if (encoded.Length > 2048) { return false; } byte[] array = Convert.FromBase64String(encoded); if (array.Length == 0 || array.Length > 1536) { return false; } ZPackage val = new ZPackage(array); byte[] array2 = val.ReadByteArray(); byte[] array3 = val.ReadByteArray(); if (val.GetPos() != val.Size() || array2 == null || array2.Length == 0 || array2.Length > 1536 || array3 == null || array3.Length != 32) { return false; } using (SHA256 sHA = SHA256.Create()) { byte[] array4 = sHA.ComputeHash(array2); int num = 0; for (int i = 0; i < array4.Length; i++) { num |= array4[i] ^ array3[i]; } if (num != 0) { return false; } } ZPackage val2 = new ZPackage(array2); if (val2.ReadInt() != 1 || val2.ReadInt() != expectedRole) { return false; } linkId = val2.ReadString(); target = val2.ReadString(); return val2.GetPos() <= val2.Size() && linkId.Length <= 128 && target.Length <= 160; } catch { linkId = string.Empty; target = string.Empty; return false; } } private unsafe static string Id(ZDOID id) { return Safe(((object)(*(ZDOID*)(&id))/*cast due to .constrained prefix*/).ToString(), 96); } private static string Position(Vector3 value) { return value.x.ToString("F1", CultureInfo.InvariantCulture) + "," + value.y.ToString("F1", CultureInfo.InvariantCulture) + "," + value.z.ToString("F1", CultureInfo.InvariantCulture); } private static string Safe(string value, int maximum) { if (string.IsNullOrEmpty(value)) { return string.Empty; } if (value.Length > maximum) { value = value.Substring(0, maximum); } for (int i = 0; i < value.Length; i++) { if (char.IsControl(value[i]) || value[i] == '|') { return "invalid-text"; } } return value; } } internal sealed class SentinelOperatorCommands : IDisposable { private const int MaximumReportBytes = 524288; private readonly SentinelRuntime _runtime; private readonly ManualLogSource _log; private readonly string _reportRoot; private readonly SentinelManagedPolicyService _managed; private readonly ConsoleCommand _command; private readonly ConcurrentQueue _dedicatedInput = new ConcurrentQueue(); private Thread _dedicatedInputThread; private int _queuedDedicatedLines; private bool _disposed; internal SentinelOperatorCommands(SentinelRuntime runtime, ManualLogSource log, string configRoot, SentinelManagedPolicyService managed) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Expected O, but got Unknown //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Expected O, but got Unknown _runtime = runtime ?? throw new ArgumentNullException("runtime"); _log = log; _managed = managed ?? throw new ArgumentNullException("managed"); _reportRoot = Path.Combine(Path.GetFullPath(configRoot), "RunicSentinel", "reports"); _command = new ConsoleCommand("runic_sentinel", "Raven's Gate: status | report | networks | bootstrap ", new ConsoleEvent(OnCommand), false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); if (Application.isBatchMode) { StartDedicatedConsoleInput(); } } private void OnCommand(ConsoleEventArgs args) { if (!_disposed && !((Object)(object)args?.Context == (Object)null)) { Execute(args.Args, delegate(string value) { args.Context.AddString(value); }); } } internal void TickDedicatedConsole() { if (_disposed) { return; } int num = 0; string result; while (num++ < 8 && _dedicatedInput.TryDequeue(out result)) { Interlocked.Decrement(ref _queuedDedicatedLines); string[] array = (result ?? string.Empty).Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 0) { continue; } if (!string.Equals(array[0], "runic_sentinel", StringComparison.OrdinalIgnoreCase)) { ManualLogSource log = _log; if (log != null) { log.LogMessage((object)"Unknown dedicated-server command. Sentinel commands begin with runic_sentinel."); } continue; } Execute(array, delegate(string value) { ManualLogSource log2 = _log; if (log2 != null) { log2.LogMessage((object)value); } }); } } private void Execute(string[] arguments, Action output) { if (_disposed || arguments == null || output == null) { return; } switch ((arguments.Length > 1) ? arguments[1].Trim().ToLowerInvariant() : "status") { case "status": { SentinelIntegritySnapshot integritySnapshot = _runtime.GetIntegritySnapshot(); output("Raven's Gate: " + integritySnapshot.State.ToString() + "; profile=" + ((_runtime.PolicyProfile.Length == 0) ? "none" : _runtime.PolicyProfile) + "; sequence=" + _runtime.PolicySequence.ToString(CultureInfo.InvariantCulture) + "; admission=" + (_runtime.AuthoritativeTransportReady ? "direct-pre-handshake" : "unavailable") + "; last-denial=" + ((_runtime.LastAdmissionFailure.Length == 0) ? "none" : _runtime.LastAdmissionFailure) + "."); break; } case "report": try { string text = WriteReport(); output("Runic Sentinel support report created: " + text); break; } catch (Exception ex2) { output("Runic Sentinel report failed closed: " + ex2.GetType().Name + "."); ManualLogSource log = _log; if (log != null) { log.LogWarning((object)("Sentinel report failed: " + ex2.GetType().Name + ".")); } break; } case "networks": try { string text2 = WriteReport(includeNetworks: true); output("Runic Sentinel administrator network snapshot created: " + text2); break; } catch (Exception ex3) { output((ex3.Message == "server-console-required") ? "Runic Sentinel network maps are available only on the authoritative server." : ("Runic Sentinel network snapshot failed closed: " + ex3.GetType().Name + ".")); break; } case "bootstrap": if (arguments.Length != 4) { output("Usage: runic_sentinel bootstrap "); break; } try { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { throw new InvalidOperationException("authoritative-server-console-required"); } output(_managed.Bootstrap(arguments[2], arguments[3])); break; } catch (Exception ex) { output("Runic Sentinel bootstrap failed closed: " + ex.Message); break; } default: output("Usage: runic_sentinel status | report | networks | bootstrap "); break; } } private void StartDedicatedConsoleInput() { _dedicatedInputThread = new Thread(ReadDedicatedConsole) { IsBackground = true, Name = "RunicSentinel.DedicatedConsole" }; _dedicatedInputThread.Start(); ManualLogSource log = _log; if (log != null) { log.LogMessage((object)"Runic Sentinel dedicated console input is ready. Type runic_sentinel status for help."); } } private void ReadDedicatedConsole() { while (!_disposed) { string text; try { text = Console.ReadLine(); } catch { break; } if (text == null) { break; } if (text.Length > 1024) { ManualLogSource log = _log; if (log != null) { log.LogWarning((object)"An oversized dedicated-server console line was ignored."); } } else if (Interlocked.Increment(ref _queuedDedicatedLines) > 32) { Interlocked.Decrement(ref _queuedDedicatedLines); ManualLogSource log2 = _log; if (log2 != null) { log2.LogWarning((object)"The bounded dedicated-server command queue is full."); } } else { _dedicatedInput.Enqueue(text); } } } internal string WriteReport(bool includeNetworks = false) { StringBuilder stringBuilder = new StringBuilder(16384); SentinelIntegritySnapshot integritySnapshot = _runtime.GetIntegritySnapshot(); stringBuilder.Append("RUNIC-SENTINEL-SUPPORT/1\n").Append("created-utc=").Append(DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)) .Append('\n') .Append("integrity=") .Append(integritySnapshot.State) .Append('\n') .Append("integrity-reason=") .Append(integritySnapshot.ReasonCode) .Append('\n') .Append("policy-profile=") .Append(_runtime.PolicyProfile) .Append('\n') .Append("policy-sequence=") .Append(_runtime.PolicySequence.ToString(CultureInfo.InvariantCulture)) .Append('\n') .Append("policy-digest=") .Append(integritySnapshot.PolicyDigest) .Append('\n') .Append("admission-transport=") .Append(_runtime.AuthoritativeTransportReady ? "ready" : "unavailable") .Append('\n') .Append("last-admission-denial=") .Append(_runtime.LastAdmissionFailure) .Append('\n'); if (_runtime.TryGetCurrent(out var snapshot, out var status)) { stringBuilder.Append("snapshot-status=").Append(status).Append('\n') .Append("snapshot-digest=") .Append(snapshot.Digest) .Append('\n') .Append("plugins=") .Append(snapshot.Plugins.Count.ToString(CultureInfo.InvariantCulture)) .Append('\n'); foreach (AttestedPlugin plugin in snapshot.Plugins) { stringBuilder.Append("plugin=").Append(plugin.Id).Append('|') .Append(plugin.Version) .Append('|') .Append(plugin.Sha256) .Append('\n'); } } else { stringBuilder.Append("snapshot-status=").Append(status).Append('\n'); } stringBuilder.Append("transport=standalone-valheim\n"); EvidenceReadSnapshot evidenceReadSnapshot = _runtime.Evidence.ReadAfter(0L, 256); stringBuilder.Append("evidence-newest=").Append(evidenceReadSnapshot.NewestSequence.ToString(CultureInfo.InvariantCulture)).Append('\n'); foreach (SecurityEvidence entry in evidenceReadSnapshot.Entries) { stringBuilder.Append("evidence=").Append(entry.Sequence.ToString(CultureInfo.InvariantCulture)).Append('|') .Append(entry.UnixSeconds.ToString(CultureInfo.InvariantCulture)) .Append('|') .Append(entry.ProviderModuleId) .Append('|') .Append(entry.Rule) .Append('|') .Append(entry.Confidence) .Append('|') .Append(entry.EffectiveAction) .Append('|') .Append(entry.Detail) .Append('\n'); } if (includeNetworks && !SentinelNetworkMapWriter.TryAppend(stringBuilder, out var failure)) { throw new InvalidOperationException(failure); } byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(stringBuilder.ToString()); if (bytes.Length > 524288) { throw new InvalidDataException("The bounded support report exceeded 512 KiB."); } Directory.CreateDirectory(_reportRoot); string text = Path.Combine(_reportRoot, "sentinel-report-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + ".txt"); using FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.Read, 65536, FileOptions.WriteThrough); fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); return text; } public void Dispose() { _disposed = true; string result; while (_dedicatedInput.TryDequeue(out result)) { } Interlocked.Exchange(ref _queuedDedicatedLines, 0); } } internal sealed class SentinelRuntime : ISentinelAttestationService, ISentinelAdmissionService, IDisposable { private sealed class FileEvidence { internal string Sha256 { get; } internal long Length { get; } internal long LastWriteUtcTicks { get; } internal FileEvidence(string sha256, long length, long lastWriteUtcTicks) { Sha256 = sha256; Length = length; LastWriteUtcTicks = lastWriteUtcTicks; } } private sealed class IntegrityFileStamp { internal string Path { get; } internal long Length { get; } internal long LastWriteUtcTicks { get; } internal bool PolicyAsset { get; } internal IntegrityFileStamp(string path, long length, long lastWriteUtcTicks, bool policyAsset) { Path = path; Length = length; LastWriteUtcTicks = lastWriteUtcTicks; PolicyAsset = policyAsset; } } private sealed class WorkerInputs { internal string ConfigRoot { get; } internal string PolicyPath { get; } internal string SignaturePath { get; } internal string PublicKeyPath { get; } internal string PinnedPublicKeySha256 { get; } internal WorkerInputs(string configRoot, string policyPath, string signaturePath, string publicKeyPath, string pinnedPublicKeySha256) { ConfigRoot = configRoot; PolicyPath = policyPath; SignaturePath = signaturePath; PublicKeyPath = publicKeyPath; PinnedPublicKeySha256 = pinnedPublicKeySha256; } } private sealed class PluginDescriptor { internal string Id { get; } internal string Version { get; } internal string Path { get; } internal string[] Dependencies { get; } internal string[] Capabilities { get; } internal PluginDescriptor(string id, string version, string path, string[] dependencies, string[] capabilities) { Id = id; Version = version; Path = path; Dependencies = dependencies; Capabilities = capabilities; } } private const long MaximumPluginBytes = 536870912L; private const long MaximumTotalPluginBytes = 4294967296L; private static readonly StringComparer PathComparer = (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); private readonly object _gate = new object(); private CancellationTokenSource _cancel; private AttestationSnapshot _snapshot; private AdmissionClientProfile _lastRemoteProfile; private SentinelPolicy _policy; private SentinelNetworkCompatibility _network; private string _lastAdmissionFailure = string.Empty; private IntegrityFileStamp[] _integrityFiles = Array.Empty(); private bool _integrityCompromised; private string _integrityReason = "snapshot-unavailable"; private long _nextIntegrityCheckUtcTicks; private string _status = "NotStarted"; private long _generation; private long _highestPolicySequence; private string _highestPolicyDigest = string.Empty; private bool _disposed; internal EvidenceLedger Evidence { get; } public bool ProvidesClientAuthenticityProof => false; public bool PolicyReady { get { lock (_gate) { return _policy != null; } } } public bool AuthoritativeTransportReady { get { lock (_gate) { return _network != null && _network.IsActive; } } } public long PolicySequence { get { lock (_gate) { return _policy?.Sequence ?? 0; } } } public string PolicyProfile { get { lock (_gate) { return _policy?.Profile ?? string.Empty; } } } internal SentinelRemoteAdmissionMode EffectiveRemoteAdmissionMode { get { SentinelNetworkCompatibility network; lock (_gate) { network = _network; } return network?.Mode ?? SentinelRemoteAdmissionMode.Disabled; } } internal string LastAdmissionFailure { get { lock (_gate) { return _lastAdmissionFailure; } } } internal SentinelRuntime() { Evidence = new EvidenceLedger(); } internal bool TryGetVerifiedPolicy(out SentinelPolicy policy) { lock (_gate) { policy = (PolicyCurrentLocked() ? _policy : null); return policy != null; } } internal void Start(string configDirectory) { long generation; CancellationTokenSource cancel; CancellationTokenSource cancellationTokenSource; lock (_gate) { if (_disposed) { throw new ObjectDisposedException("SentinelRuntime"); } if (_generation == long.MaxValue) { throw new InvalidOperationException("Sentinel worker generation space is exhausted."); } generation = ++_generation; cancel = _cancel; cancellationTokenSource = (_cancel = new CancellationTokenSource()); _snapshot = null; _lastRemoteProfile = null; _policy = null; _integrityFiles = Array.Empty(); _integrityCompromised = false; _integrityReason = "snapshot-pending"; _nextIntegrityCheckUtcTicks = 0L; _status = "PendingLocalSnapshot"; Evidence.SetPolicySequence(0L); } CancelAndDispose(cancel); PluginDescriptor[] descriptors; WorkerInputs inputs; try { descriptors = CaptureDescriptors(); inputs = CaptureInputs(configDirectory); } catch (Exception ex) { PublishFailure(generation, cancellationTokenSource.Token, ex.GetType().Name); CancelAndDispose(cancellationTokenSource); throw; } CancellationToken token = cancellationTokenSource.Token; Task.Run(delegate { Build(generation, descriptors, inputs, token); }, token); } public bool TryGetCurrent(out AttestationSnapshot snapshot, out string status) { lock (_gate) { snapshot = _snapshot; status = _status; return snapshot != null; } } public bool TryComputeNonceBinding(string nonceHex, out string bindingHex, out string status) { lock (_gate) { status = _status + ":UnauthenticatedNonceBinding"; if (_snapshot == null) { bindingHex = string.Empty; return false; } return AttestationPolicy.TryComputeNonceBinding(nonceHex, _snapshot.Digest, out bindingHex); } } public AdmissionDecision Evaluate(AttestationSnapshot snapshot, string role) { lock (_gate) { return AdmissionPolicy.Evaluate(_policy, snapshot, role ?? string.Empty); } } internal void AttachNetwork(SentinelRemoteAdmissionMode mode) { SentinelNetworkCompatibility sentinelNetworkCompatibility = new SentinelNetworkCompatibility(this, Evidence, mode); lock (_gate) { if (_disposed || _network != null) { sentinelNetworkCompatibility.Dispose(); if (_disposed) { throw new ObjectDisposedException("SentinelRuntime"); } throw new InvalidOperationException("Sentinel network compatibility is already attached."); } _network = sentinelNetworkCompatibility; } } internal void TickNetwork() { _network?.Tick(); } internal bool IsAdministrator(string authority, string subject) { lock (_gate) { return PolicyCurrentLocked() && _policy.Administrators.Any((SentinelAdministratorRole role) => string.Equals(role.Authority, authority, StringComparison.Ordinal) && string.Equals(role.Subject, subject, StringComparison.Ordinal)); } } internal bool IsBanned(string authority, string subject) { lock (_gate) { return PolicyCurrentLocked() && _policy.BannedUsers.Any((SentinelAdministratorRole role) => string.Equals(role.Authority, authority, StringComparison.Ordinal) && string.Equals(role.Subject, subject, StringComparison.Ordinal)); } } internal SentinelIntegritySnapshot GetIntegritySnapshot() { lock (_gate) { return new SentinelIntegritySnapshot(_integrityCompromised ? SentinelIntegrityState.Compromised : ((_snapshot != null) ? ((_policy == null) ? SentinelIntegrityState.MonitorOnly : SentinelIntegrityState.Ready) : SentinelIntegrityState.Unavailable), DateTimeOffset.UtcNow.ToUnixTimeSeconds(), _integrityReason, _policy?.PayloadDigest ?? string.Empty); } } internal void TickIntegrity() { long ticks = DateTime.UtcNow.Ticks; IntegrityFileStamp[] integrityFiles; lock (_gate) { if (_disposed || _integrityCompromised || ticks < _nextIntegrityCheckUtcTicks) { return; } int num = Math.Max(5, Math.Min(300, SentinelConfig.IntegrityCheckSeconds?.Value ?? 15)); _nextIntegrityCheckUtcTicks = ticks + TimeSpan.FromSeconds(num).Ticks; if (_policy != null && _policy.ExpiresUnixSeconds != 0L && DateTimeOffset.UtcNow.ToUnixTimeSeconds() >= _policy.ExpiresUnixSeconds) { _integrityCompromised = true; _integrityReason = "passport-expired"; _lastAdmissionFailure = "sentinel-passport-expired"; return; } integrityFiles = _integrityFiles; } foreach (IntegrityFileStamp integrityFileStamp in integrityFiles) { FileInfo fileInfo = new FileInfo(integrityFileStamp.Path); if (!fileInfo.Exists || fileInfo.Length != integrityFileStamp.Length || fileInfo.LastWriteTimeUtc.Ticks != integrityFileStamp.LastWriteUtcTicks) { lock (_gate) { _integrityCompromised = true; _integrityReason = (integrityFileStamp.PolicyAsset ? "passport-file-changed" : "plugin-file-changed"); _lastAdmissionFailure = "sentinel-runtime-integrity-changed"; break; } } } } private bool PolicyCurrentLocked() { if (_policy == null || _integrityCompromised) { return false; } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (_policy.IssuedUnixSeconds <= num + 300) { if (_policy.ExpiresUnixSeconds != 0L) { return num < _policy.ExpiresUnixSeconds; } return true; } return false; } internal void RecordAdmissionFailure(string reason) { if (string.IsNullOrEmpty(reason) || reason.Length > 128 || !RunicIdentifier.IsValid(reason)) { reason = "sentinel-admission-denied"; } lock (_gate) { _lastAdmissionFailure = reason; } } internal bool TryGetTransitionFingerprint(out string fingerprint) { lock (_gate) { if (_snapshot == null || _policy == null || _integrityCompromised) { fingerprint = string.Empty; return false; } using SHA256 sHA = SHA256.Create(); fingerprint = SentinelPolicy.Hex(sHA.ComputeHash(Encoding.ASCII.GetBytes("RUNIC-TRANSITION/1\n" + _policy.PayloadDigest + "\n" + _snapshot.Digest + "\n"))); return true; } } internal AdmissionDecision EvaluateAdmissionClientProfile(SentinelPolicy policy, AdmissionClientProfile profile, string role) { if (policy == null || profile == null) { return AdmissionPolicy.Evaluate(null, null, role ?? string.Empty); } if (!AttestationPolicy.TryCanonicalize(profile.Plugins.Select((AdmissionPluginEvidence value) => new AttestedPlugin(value.Id, value.Version, value.Sha256, Array.Empty(), Array.Empty())).ToArray(), out var plugins, out var canonical, out var _)) { return AdmissionPolicy.Evaluate(policy, null, role ?? string.Empty); } AttestationSnapshot snapshot = new AttestationSnapshot(AttestationPolicy.Digest(canonical), plugins, profile.CapturedUnixSeconds); return AdmissionPolicy.Evaluate(policy, snapshot, role ?? string.Empty); } internal void ObserveRemoteAdmissionProfile(AdmissionClientProfile profile) { if (profile == null) { return; } lock (_gate) { if (!_disposed) { _lastRemoteProfile = profile; } } } internal bool TryGetLastRemoteAdmissionProfile(out AdmissionClientProfile profile) { lock (_gate) { profile = _lastRemoteProfile; return profile != null; } } public void Dispose() { CancellationTokenSource cancel; SentinelNetworkCompatibility network; lock (_gate) { if (_disposed) { return; } _disposed = true; if (_generation < long.MaxValue) { _generation++; } cancel = _cancel; _cancel = null; _snapshot = null; _lastRemoteProfile = null; _policy = null; _integrityFiles = Array.Empty(); _integrityCompromised = false; _integrityReason = "disposed"; network = _network; _network = null; _status = "Disposed"; Evidence.SetPolicySequence(0L); } try { network?.Dispose(); } catch (Exception) { } CancelAndDispose(cancel); } private void Build(long generation, PluginDescriptor[] descriptors, WorkerInputs inputs, CancellationToken token) { try { List list = new List(descriptors.Length); Dictionary dictionary = new Dictionary(PathComparer); long num = 0L; byte[] buffer = new byte[65536]; for (int i = 0; i < descriptors.Length; i++) { token.ThrowIfCancellationRequested(); PluginDescriptor pluginDescriptor = descriptors[i]; string fullPath = Path.GetFullPath(pluginDescriptor.Path); if (!dictionary.TryGetValue(fullPath, out var value)) { FileInfo fileInfo = new FileInfo(fullPath); if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > 536870912 || num > 4294967296L - fileInfo.Length) { throw new InvalidDataException("Plugin file bound failed."); } long length = fileInfo.Length; DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; num += length; string sha = HashStablePlugin(fullPath, length, buffer, token); fileInfo.Refresh(); if (!fileInfo.Exists || fileInfo.Length != length || fileInfo.LastWriteTimeUtc != lastWriteTimeUtc) { throw new IOException("Plugin changed during local snapshot hashing."); } value = new FileEvidence(sha, length, lastWriteTimeUtc.Ticks); dictionary.Add(fullPath, value); } list.Add(new AttestedPlugin(pluginDescriptor.Id, pluginDescriptor.Version, value.Sha256, pluginDescriptor.Dependencies, pluginDescriptor.Capabilities)); } if (!AttestationPolicy.TryCanonicalize(list, out var plugins, out var canonical, out var failure)) { throw new InvalidDataException(failure); } AttestationSnapshot snapshot = new AttestationSnapshot(AttestationPolicy.Digest(canonical), plugins, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); string status; SentinelPolicy sentinelPolicy = TryLoadPolicy(inputs, token, out status); SentinelDraftExporter.TryWrite(inputs.ConfigRoot, snapshot); Publish(generation, token, snapshot, sentinelPolicy, status, CaptureIntegrityStamps(dictionary, inputs, sentinelPolicy != null)); } catch (OperationCanceledException) { } catch (Exception ex2) { PublishFailure(generation, token, ex2.GetType().Name); } } private void Publish(long generation, CancellationToken token, AttestationSnapshot snapshot, SentinelPolicy policy, string policyStatus, IntegrityFileStamp[] integrityFiles) { lock (_gate) { if (_disposed || token.IsCancellationRequested || generation != _generation) { return; } if (policy != null) { if (policy.Sequence < _highestPolicySequence || (policy.Sequence == _highestPolicySequence && _highestPolicySequence > 0 && !string.Equals(policy.PayloadDigest, _highestPolicyDigest, StringComparison.Ordinal))) { policyStatus = "PolicyRollbackOrEquivocation"; policy = null; } else if (policy.Sequence > _highestPolicySequence) { _highestPolicySequence = policy.Sequence; _highestPolicyDigest = policy.PayloadDigest; } } _snapshot = snapshot; _policy = policy; _integrityFiles = integrityFiles ?? Array.Empty(); _integrityCompromised = false; _integrityReason = ((policy == null) ? "passport-unavailable" : "verified"); _status = ((policy == null) ? ("ReadyLocalSnapshot:MonitorOnly:" + policyStatus) : "ReadyLocalSnapshot:VerifiedRsaPolicy"); Evidence.SetPolicySequence(policy?.Sequence ?? 0); } } private void PublishFailure(long generation, CancellationToken token, string failure) { lock (_gate) { if (!_disposed && !token.IsCancellationRequested && generation == _generation) { _snapshot = null; _policy = null; _status = "FailedLocalSnapshot:" + failure; _integrityFiles = Array.Empty(); _integrityCompromised = true; _integrityReason = "snapshot-failed"; Evidence.SetPolicySequence(0L); } } } private static SentinelPolicy TryLoadPolicy(WorkerInputs inputs, CancellationToken token, out string status) { status = "PolicyFilesUnavailable"; if (!TryReadStableBounded(inputs.PolicyPath, 1L, 1048576L, token, out var bytes)) { status = "PolicySizeOrChanged"; return null; } if (!TryReadStableBounded(inputs.SignaturePath, 1L, 1024L, token, out var bytes2)) { status = "SignatureSizeOrChanged"; return null; } if (!TryReadStableBounded(inputs.PublicKeyPath, 1L, 1024L, token, out var bytes3)) { status = "PublicKeySizeOrChanged"; return null; } if (!PinnedRsaPublicKey.TryParse(bytes3, inputs.PinnedPublicKeySha256, out var key, out status)) { return null; } if (!SentinelPolicy.TryDecodeSignatureFile(bytes2, out var signature, out status)) { return null; } if (!SentinelPolicy.TryParseAndVerify(bytes, signature, key, out var policy, out status)) { return null; } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (policy.IssuedUnixSeconds > num + 300) { status = "PolicyNotYetValid"; return null; } if (policy.ExpiresUnixSeconds != 0L && num >= policy.ExpiresUnixSeconds) { status = "PolicyExpired"; return null; } status = "Verified"; return policy; } private static string HashStablePlugin(string path, long expectedLength, byte[] buffer, CancellationToken token) { if (expectedLength <= 0 || expectedLength > 536870912 || buffer == null || buffer.Length == 0) { throw new InvalidDataException("Plugin file bound failed."); } using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, buffer.Length, FileOptions.SequentialScan); using SHA256 sHA = SHA256.Create(); if (fileStream.Length != expectedLength) { throw new IOException("Plugin changed before local snapshot hashing."); } long num = expectedLength; while (num > 0) { token.ThrowIfCancellationRequested(); int count = (int)Math.Min(buffer.Length, num); int num2 = fileStream.Read(buffer, 0, count); if (num2 <= 0) { throw new EndOfStreamException("Plugin ended during local snapshot hashing."); } sHA.TransformBlock(buffer, 0, num2, buffer, 0); num -= num2; } if (fileStream.ReadByte() != -1 || fileStream.Length != expectedLength) { throw new IOException("Plugin grew during local snapshot hashing."); } sHA.TransformFinalBlock(Array.Empty(), 0, 0); return AttestationPolicy.Hex(sHA.Hash); } private static bool TryReadStableBounded(string path, long minimumBytes, long maximumBytes, CancellationToken token, out byte[] bytes) { bytes = null; try { token.ThrowIfCancellationRequested(); FileInfo fileInfo = new FileInfo(path); if (!fileInfo.Exists || fileInfo.Length < minimumBytes || fileInfo.Length > maximumBytes || fileInfo.Length > int.MaxValue) { return false; } long length = fileInfo.Length; DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; bytes = new byte[(int)length]; using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, Math.Max(1, Math.Min(65536, bytes.Length)), FileOptions.SequentialScan)) { if (fileStream.Length != length) { bytes = null; return false; } int num; for (int i = 0; i < bytes.Length; i += num) { token.ThrowIfCancellationRequested(); num = fileStream.Read(bytes, i, bytes.Length - i); if (num <= 0) { bytes = null; return false; } } if (fileStream.ReadByte() != -1 || fileStream.Length != length) { bytes = null; return false; } } fileInfo.Refresh(); if (!fileInfo.Exists || fileInfo.Length != length || fileInfo.LastWriteTimeUtc != lastWriteTimeUtc) { bytes = null; return false; } return true; } catch (OperationCanceledException) { throw; } catch { bytes = null; return false; } } private static PluginDescriptor[] CaptureDescriptors() { List list = new List(); if (Chainloader.PluginInfos.Count > 512) { throw new InvalidDataException("Plugin cap exceeded."); } foreach (KeyValuePair item in Chainloader.PluginInfos.OrderBy, string>((KeyValuePair keyValuePair) => keyValuePair.Key, StringComparer.Ordinal)) { if (list.Count >= 512) { throw new InvalidDataException("Plugin cap exceeded."); } PluginInfo value = item.Value; string location = value.Location; if (string.IsNullOrWhiteSpace(location)) { throw new InvalidDataException("Plugin path unavailable."); } string text = value.Metadata.Version.ToString(); if (!SentinelPolicy.CanonicalPluginId(item.Key) || !SentinelPolicy.CanonicalAtom(text, 1, 64)) { throw new InvalidDataException("Plugin identity evidence is invalid."); } string[] array = value.Dependencies.Select((BepInDependency dependency) => dependency.DependencyGUID).Take(65).OrderBy((string id) => id, StringComparer.Ordinal) .ToArray(); if (array.Length > 64) { throw new InvalidDataException("Plugin dependency cap exceeded."); } list.Add(new PluginDescriptor(item.Key, text, Path.GetFullPath(location), array, Array.Empty())); } return list.ToArray(); } private static WorkerInputs CaptureInputs(string configDirectory) { string fullPath = Path.GetFullPath(configDirectory ?? string.Empty); return new WorkerInputs(fullPath, Resolve(fullPath, SentinelConfig.PolicyFile?.Value), Resolve(fullPath, SentinelConfig.SignatureFile?.Value), Resolve(fullPath, SentinelConfig.PublicKeyFile?.Value), SentinelConfig.TrustedPublicKeySha256?.Value ?? string.Empty); } private static IntegrityFileStamp[] CaptureIntegrityStamps(IReadOnlyDictionary pluginFiles, WorkerInputs inputs, bool includePolicyAssets) { List result = new List(pluginFiles.Count + 3); foreach (KeyValuePair item in pluginFiles.OrderBy, string>((KeyValuePair value) => value.Key, PathComparer)) { result.Add(new IntegrityFileStamp(item.Key, item.Value.Length, item.Value.LastWriteUtcTicks, policyAsset: false)); } if (includePolicyAssets) { AddPolicy(inputs.PolicyPath); AddPolicy(inputs.SignaturePath); AddPolicy(inputs.PublicKeyPath); } return result.ToArray(); void AddPolicy(string path) { FileInfo fileInfo = new FileInfo(path); if (!fileInfo.Exists) { throw new IOException("Verified passport asset disappeared."); } result.Add(new IntegrityFileStamp(fileInfo.FullName, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks, policyAsset: true)); } } private static string Resolve(string root, string configured) { if (string.IsNullOrWhiteSpace(configured)) { return string.Empty; } if (!Path.IsPathRooted(configured)) { return Path.GetFullPath(Path.Combine(root, configured)); } return Path.GetFullPath(configured); } private static void CancelAndDispose(CancellationTokenSource cancellation) { if (cancellation == null) { return; } try { cancellation.Cancel(); } catch { } try { cancellation.Dispose(); } catch { } } } [HarmonyPatch(typeof(ZNet), "LoadWorld")] internal static class SentinelTransitionBackup { private static readonly object Gate = new object(); private static SentinelRuntime _runtime; private static ManualLogSource _log; private static string _stateRoot; internal static void Attach(SentinelRuntime runtime, ManualLogSource log, string configRoot) { lock (Gate) { _runtime = runtime; _log = log; _stateRoot = Path.Combine(Path.GetFullPath(configRoot), "RunicSentinel", "transitions"); } } internal static void Detach() { lock (Gate) { _runtime = null; _log = null; _stateRoot = null; } } internal static string CreateVerifiedBackupNow(string reason) { SentinelRuntime runtime; string stateRoot; lock (Gate) { runtime = _runtime; stateRoot = _stateRoot; } ZNet instance = ZNet.instance; World world = ZNet.World; if (runtime == null || (Object)(object)instance == (Object)null || !instance.IsServer() || world == null) { return "no-world-loaded"; } IMigrationBackupService backups = SafetyIntegrationApi.Backups; if (backups == null) { throw new InvalidOperationException("runic-safety-backup-unavailable"); } List list = new List(); List list2 = new List(); string text = Path.Combine(stateRoot, "staging", Guid.NewGuid().ToString("N")); try { AddBackupSource(world, world.GetDBPath(), "world-database", "world.db", text, list, list2); AddBackupSource(world, world.GetMetaPath(), "world-metadata", "world.fwl", text, list, list2); if (list.Count == 0) { throw new FileNotFoundException("world-backup-sources-missing"); } MigrationBackupRequest val = backups.CreateDefaultRequest(string.IsNullOrWhiteSpace(reason) ? "runic-sentinel-admin-backup" : reason, (IEnumerable)list); MigrationBackupResult val2 = backups.CreateBackup(val, CancellationToken.None); string empty = string.Empty; if (val2 == null || !val2.Succeeded || !backups.ValidateBackup(val2.BackupDirectory, ref empty)) { throw new IOException("verified-world-backup-failed-" + (((val2 != null) ? val2.FailureCode : null) ?? empty ?? "unknown")); } return val2.CorrelationId; } finally { foreach (string item in list2) { try { if (File.Exists(item)) { File.Delete(item); } } catch { } } try { if (Directory.Exists(text) && Directory.GetFileSystemEntries(text).Length == 0) { Directory.Delete(text, recursive: false); } } catch { } } } [HarmonyPrefix] private static void BeforeWorldLoad(ZNet __instance) { ConfigEntry backupBeforeTransitions = SentinelConfig.BackupBeforeTransitions; if ((backupBeforeTransitions != null && !backupBeforeTransitions.Value) || (Object)(object)__instance == (Object)null || !__instance.IsServer()) { return; } SentinelRuntime runtime; ManualLogSource log; string stateRoot; lock (Gate) { runtime = _runtime; log = _log; stateRoot = _stateRoot; } World world = ZNet.World; if (runtime == null || world == null) { return; } if (!runtime.TryGetTransitionFingerprint(out var fingerprint)) { if (SentinelConfig.RemoteAdmissionMode != SentinelRemoteAdmissionMode.Required) { return; } runtime.RecordAdmissionFailure("sentinel-transition-profile-unverified"); throw new InvalidOperationException("Raven's Gate cannot verify the active profile before world load."); } string path = Path.Combine(stateRoot, __instance.GetWorldUID() + ".state"); if (string.Equals(ReadMarker(path), fingerprint, StringComparison.Ordinal)) { return; } IMigrationBackupService backups = SafetyIntegrationApi.Backups; if (backups == null) { runtime.RecordAdmissionFailure("sentinel-transition-backup-unavailable"); throw new InvalidOperationException("Runic Safety backup service is required before this profile transition."); } List sources = new List(); List staged = new List(); string staging = Path.Combine(stateRoot, "staging", Guid.NewGuid().ToString("N")); try { Add(world.GetDBPath(), "world-database", "world.db"); Add(world.GetMetaPath(), "world-metadata", "world.fwl"); if (sources.Count == 0) { throw new FileNotFoundException("No existing world files were available for the required transition backup."); } MigrationBackupRequest val = backups.CreateDefaultRequest("runic-sentinel-profile-transition", (IEnumerable)sources); MigrationBackupResult val2 = backups.CreateBackup(val, CancellationToken.None); string empty = string.Empty; if (val2 == null || !val2.Succeeded || !backups.ValidateBackup(val2.BackupDirectory, ref empty)) { runtime.RecordAdmissionFailure("sentinel-transition-backup-failed"); throw new IOException("Required profile-transition backup failed: " + (((val2 != null) ? val2.FailureCode : null) ?? empty ?? "unknown")); } WriteMarker(path, fingerprint); if (log != null) { log.LogWarning((object)("Raven's Gate created and verified a world backup before applying a new modpack or policy profile. Correlation: " + val2.CorrelationId + ".")); } } finally { foreach (string item in staged) { try { if (File.Exists(item)) { File.Delete(item); } } catch { } } try { if (Directory.Exists(staging) && Directory.GetFileSystemEntries(staging).Length == 0) { Directory.Delete(staging, recursive: false); } } catch { } } void Add(string text, string logicalName, string stagingName) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Invalid comparison between Unknown and I4 //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Expected O, but got Unknown //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Expected O, but got Unknown if (!string.IsNullOrEmpty(text)) { if ((int)world.m_fileSource == 2) { if (FileHelpers.Exists(text, (FileSource)2)) { Directory.CreateDirectory(staging); string text2 = Path.Combine(staging, stagingName); FileHelpers.FileCopyOutFromCloud(text, text2, true); if (!File.Exists(text2)) { throw new IOException("Steam Cloud world staging did not produce a readable file."); } staged.Add(text2); sources.Add(new MigrationBackupSource(text2, logicalName)); } } else if (File.Exists(text)) { sources.Add(new MigrationBackupSource(text, logicalName)); } } } } private static string ReadMarker(string path) { try { if (!File.Exists(path)) { return string.Empty; } string text = File.ReadAllText(path, Encoding.ASCII).Trim(); return SentinelPolicy.IsLowerHex(text, 64) ? text : string.Empty; } catch { return string.Empty; } } private static void AddBackupSource(World world, string path, string logicalName, string stagingName, string staging, ICollection sources, ICollection staged) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Expected O, but got Unknown //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown if (string.IsNullOrEmpty(path)) { return; } if ((int)world.m_fileSource == 2) { if (FileHelpers.Exists(path, (FileSource)2)) { Directory.CreateDirectory(staging); string text = Path.Combine(staging, stagingName); FileHelpers.FileCopyOutFromCloud(path, text, true); if (!File.Exists(text)) { throw new IOException("cloud-world-staging-failed"); } staged.Add(text); sources.Add(new MigrationBackupSource(text, logicalName)); } } else if (File.Exists(path)) { sources.Add(new MigrationBackupSource(path, logicalName)); } } private static void WriteMarker(string path, string value) { Directory.CreateDirectory(Path.GetDirectoryName(path)); string text = path + ".tmp"; byte[] bytes = Encoding.ASCII.GetBytes(value + "\n"); using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } if (File.Exists(path)) { File.Replace(text, path, null); } else { File.Move(text, path); } } } internal static class SentinelTransportIdentity { private static readonly FieldInfo SteamConnectionField = typeof(ZSteamSocket).GetField("m_con", BindingFlags.Instance | BindingFlags.NonPublic); internal static bool TryResolvePeer(ZNetPeer peer, out string authority, out string subject) { authority = string.Empty; subject = string.Empty; if (peer == null || !peer.IsReady()) { return false; } return TryResolveConnection(peer, out authority, out subject); } internal static bool TryResolveConnection(ZNetPeer peer, out string authority, out string subject) { authority = string.Empty; subject = string.Empty; if (peer == null || peer.m_socket == null) { return false; } ISocket socket = peer.m_socket; ZSteamSocket val = (ZSteamSocket)(object)((socket is ZSteamSocket) ? socket : null); if (val != null && TrySteam(val, out subject)) { authority = "steam"; return true; } ISocket socket2 = peer.m_socket; ZPlayFabSocket val2 = (ZPlayFabSocket)(object)((socket2 is ZPlayFabSocket) ? socket2 : null); if (val2 != null && TryPlayFab(val2, out subject)) { authority = "playfab.entity"; return true; } return false; } internal static bool TryResolveLocal(out string authority, out string subject) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) authority = string.Empty; subject = string.Empty; try { ulong steamID = SteamUser.GetSteamID().m_SteamID; if (steamID == 0L) { return false; } authority = "steam"; subject = steamID.ToString(CultureInfo.InvariantCulture); return true; } catch { return false; } } private static bool TrySteam(ZSteamSocket steam, out string subject) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) subject = string.Empty; if (steam == null || SteamConnectionField == null) { return false; } try { string hostName = steam.GetHostName(); string value = ((object)steam.GetPeerID()/*cast due to .constrained prefix*/).ToString(); if (!(SteamConnectionField.GetValue(steam) is HSteamNetConnection handle) || !TryConnectionInfo(handle, out var info)) { return false; } string value2 = ((object)((SteamNetworkingIdentity)(ref info.m_identityRemote)).GetSteamID()/*cast due to .constrained prefix*/).ToString(); if ((info.m_nFlags & 1) != 0 || !Canonical(hostName, out var parsed) || !Canonical(value, out var parsed2) || !Canonical(value2, out var parsed3) || parsed != parsed2 || parsed2 != parsed3) { return false; } subject = parsed2.ToString(CultureInfo.InvariantCulture); return true; } catch { return false; } } private static bool TryConnectionInfo(HSteamNetConnection handle, out SteamNetConnectionInfo_t info) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) info = default(SteamNetConnectionInfo_t); try { return ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsDedicated()) ? SteamGameServerNetworkingSockets.GetConnectionInfo(handle, ref info) : SteamNetworkingSockets.GetConnectionInfo(handle, ref info); } catch { info = default(SteamNetConnectionInfo_t); return false; } } private static bool TryPlayFab(ZPlayFabSocket socket, out string subject) { subject = string.Empty; try { string text = typeof(ZPlayFabSocket).GetField("m_remotePlayerId", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(socket) as string; if (string.IsNullOrWhiteSpace(text) || text.Length > 256 || text.Trim() != text) { return false; } subject = text; return true; } catch { return false; } } private static bool Canonical(string value, out ulong parsed) { if (ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out parsed) && parsed != 0L) { return parsed.ToString(CultureInfo.InvariantCulture) == value; } return false; } } } namespace RunicSentinel.Core { internal static class AttestationPolicy { internal const int MaximumPlugins = 512; internal const int MaximumRelations = 64; internal static bool TryCanonicalize(IEnumerable source, out IReadOnlyList plugins, out string canonical, out string failure) { plugins = Array.Empty(); canonical = string.Empty; failure = string.Empty; if (source == null) { failure = "MissingAttestation"; return false; } List list = new List(); foreach (AttestedPlugin item in source) { if (list.Count >= 512) { failure = "PluginCap"; return false; } list.Add(item); } AttestedPlugin[] array = list.ToArray(); Array.Sort(array, (AttestedPlugin a, AttestedPlugin b) => string.CompareOrdinal(a?.Id, b?.Id)); HashSet hashSet = new HashSet(StringComparer.Ordinal); StringBuilder stringBuilder = new StringBuilder(Math.Min(65536, 128 + array.Length * 96)); stringBuilder.Append("RUNIC-ATTESTATION/1\n"); AttestedPlugin[] array2 = array; foreach (AttestedPlugin attestedPlugin in array2) { if (attestedPlugin == null || !SentinelPolicy.CanonicalPluginId(attestedPlugin.Id) || !SentinelPolicy.CanonicalAtom(attestedPlugin.Version, 1, 64) || !SentinelPolicy.IsLowerHex(attestedPlugin.Sha256, 64) || !hashSet.Add(attestedPlugin.Id) || attestedPlugin.Dependencies.Count > 64 || attestedPlugin.Capabilities.Count > 64) { failure = "PluginEvidence"; return false; } bool valid; string[] value = CanonicalRelations(attestedPlugin.Dependencies, out valid); bool valid2; string[] value2 = CanonicalRelations(attestedPlugin.Capabilities, out valid2); if (!valid || !valid2) { failure = "PluginRelations"; return false; } stringBuilder.Append(attestedPlugin.Id).Append('|').Append(attestedPlugin.Version) .Append('|') .Append(attestedPlugin.Sha256) .Append('|') .Append(string.Join(",", value)) .Append('|') .Append(string.Join(",", value2)) .Append('\n'); if (stringBuilder.Length > 1048576) { failure = "AttestationSize"; return false; } } plugins = array; canonical = stringBuilder.ToString(); return true; } internal static string Digest(string canonical) { using SHA256 sHA = SHA256.Create(); return Hex(sHA.ComputeHash(Encoding.UTF8.GetBytes(canonical ?? string.Empty))); } internal static bool TryComputeNonceBinding(string nonceHex, string snapshotDigest, out string response) { response = string.Empty; if (!SentinelPolicy.IsLowerHex(nonceHex, 64) || !SentinelPolicy.IsLowerHex(snapshotDigest, 64)) { return false; } using SHA256 sHA = SHA256.Create(); response = Hex(sHA.ComputeHash(Encoding.ASCII.GetBytes("RUNIC-NONCE-BINDING/2\n" + nonceHex + "\n" + snapshotDigest))); return true; } internal static string Hex(byte[] bytes) { StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2); foreach (byte b in bytes) { stringBuilder.Append(b.ToString("x2")); } return stringBuilder.ToString(); } private static string[] CanonicalRelations(IReadOnlyList values, out bool valid) { valid = true; string[] array = values.ToArray(); Array.Sort(array, (IComparer?)StringComparer.Ordinal); string text = null; string[] array2 = array; foreach (string text2 in array2) { if (!SentinelPolicy.CanonicalAtom(text2, 1, 128) || text2 == text) { valid = false; return Array.Empty(); } text = text2; } return array; } } internal static class AdmissionPolicy { internal static AdmissionDecision Evaluate(SentinelPolicy policy, AttestationSnapshot snapshot, string role) { if (policy == null || snapshot == null) { return new AdmissionDecision(AdmissionDisposition.Unavailable, 0L, string.Empty, new AdmissionFinding[1] { new AdmissionFinding("PolicyUnavailable", string.Empty, "No verified server policy is active.", FindingConfidence.High) }); } if (!AttestationPolicy.TryCanonicalize(snapshot.Plugins, out var plugins, out var canonical, out var failure) || !string.Equals(AttestationPolicy.Digest(canonical), snapshot.Digest, StringComparison.Ordinal)) { return new AdmissionDecision(AdmissionDisposition.Deny, policy.Sequence, policy.Profile, new AdmissionFinding[1] { new AdmissionFinding("InvalidAttestation", string.Empty, (failure.Length == 0) ? "DigestMismatch" : failure, FindingConfidence.Conclusive) }); } List findings = new List(); Dictionary dictionary = plugins.ToDictionary((AttestedPlugin attestedPlugin) => attestedPlugin.Id, StringComparer.Ordinal); AdmissionDisposition disposition = AdmissionDisposition.Allow; foreach (SentinelPolicyRule rule in policy.Rules) { AttestedPlugin value; bool flag = dictionary.TryGetValue(rule.Id, out value); if (rule.Classification == PluginClassification.Required && !flag) { Add("RequiredMissing", rule.Id, FindingConfidence.Conclusive, AdmissionDisposition.Deny); } else { if (!flag) { continue; } if (rule.Classification == PluginClassification.Forbidden) { Add("ForbiddenPresent", rule.Id, FindingConfidence.Conclusive, AdmissionDisposition.Deny); continue; } if (rule.Classification == PluginClassification.ServerOnly && !string.Equals(role, "server", StringComparison.Ordinal)) { Add("ServerOnlyOnClient", rule.Id, FindingConfidence.Conclusive, AdmissionDisposition.Deny); continue; } if (rule.Classification == PluginClassification.AdministratorOnly && !string.Equals(role, "administrator", StringComparison.Ordinal)) { Add("AdministratorOnly", rule.Id, FindingConfidence.Conclusive, AdmissionDisposition.Deny); continue; } bool num = rule.Version == "*" || rule.Version == value.Version; bool flag2 = rule.Sha256 == "*" || rule.Sha256 == value.Sha256; if (!num || !flag2) { AdmissionDisposition next = ((rule.Classification == PluginClassification.Required) ? AdmissionDisposition.Deny : AdmissionDisposition.Quarantine); Add("PluginMismatch", rule.Id, FindingConfidence.VeryHigh, next); } if (rule.Classification == PluginClassification.Quarantined) { Add("PolicyQuarantine", rule.Id, FindingConfidence.High, AdmissionDisposition.Quarantine); } else if (rule.Classification == PluginClassification.Unmanaged) { Add("GrayListPresent", rule.Id, FindingConfidence.Informational, AdmissionDisposition.Allow); } } } HashSet hashSet = new HashSet(policy.Rules.Select((SentinelPolicyRule sentinelPolicyRule) => sentinelPolicyRule.Id), StringComparer.Ordinal); foreach (AttestedPlugin item in plugins) { if (!hashSet.Contains(item.Id)) { if (policy.Unknown == PluginClassification.Forbidden) { Add("UnknownForbidden", item.Id, FindingConfidence.High, AdmissionDisposition.Deny); } else if (policy.Unknown == PluginClassification.Quarantined) { Add("UnknownQuarantine", item.Id, FindingConfidence.Moderate, AdmissionDisposition.Quarantine); } } } return new AdmissionDecision(disposition, policy.Sequence, policy.Profile, findings); void Add(string rule, string id, FindingConfidence confidence, AdmissionDisposition admissionDisposition) { findings.Add(new AdmissionFinding(rule, id, rule, confidence)); if (admissionDisposition == AdmissionDisposition.Deny || disposition == AdmissionDisposition.Allow) { disposition = admissionDisposition; } } } } internal sealed class EvidenceLedger : ISentinelEvidenceService { private sealed class ProviderState { internal string ModuleId { get; } internal Queue Entries { get; } internal long Token { get; set; } internal long Accepted { get; set; } internal long Dropped { get; set; } internal ProviderState(string moduleId) { ModuleId = moduleId; Entries = new Queue(8); } } private sealed class ProviderSink : ISentinelEvidenceSink { private readonly EvidenceLedger _owner; private readonly string _moduleId; private readonly long _token; internal ProviderSink(EvidenceLedger owner, string moduleId, long token) { _owner = owner; _moduleId = moduleId; _token = token; } public bool TryAppend(string actor, string rule, string correlationId, FindingConfidence confidence, EnforcementAction requestedAction, string detail, out SecurityEvidence accepted) { return _owner.TryAppend(_moduleId, _token, actor, rule, correlationId, confidence, requestedAction, detail, out accepted); } } private sealed class ProviderLease : ISentinelEvidenceProviderLease, IDisposable { private EvidenceLedger _owner; private readonly long _token; public string ProviderModuleId { get; } public bool IsActive { get { if (_owner != null) { return _owner.IsLeaseActive(ProviderModuleId, _token); } return false; } } public ISentinelEvidenceSink Sink { get; } internal ProviderLease(EvidenceLedger owner, string moduleId, long token, ISentinelEvidenceSink sink) { _owner = owner; ProviderModuleId = moduleId; _token = token; Sink = sink; } public void Dispose() { EvidenceLedger owner = _owner; _owner = null; owner?.Release(ProviderModuleId, _token); } } internal const int Capacity = 256; internal const int MaximumProviders = 32; internal const int CapacityPerProvider = 8; private readonly object _gate = new object(); private readonly Dictionary _providers = new Dictionary(StringComparer.Ordinal); private long _sequence; private long _token; private long _policySequence; internal event Action Accepted; public ISentinelEvidenceProviderLease RegisterProvider(string providerId) { string text = RunicIdentifier.Require(providerId, "providerId"); lock (_gate) { if (_token == long.MaxValue) { throw new InvalidOperationException("The evidence provider token space is exhausted."); } _providers.TryGetValue(text, out var value); if (value != null && IsActiveLocked(value)) { throw new InvalidOperationException("An active lease owns this evidence provider ID."); } if (value == null) { PruneInactiveLocked(); if (_providers.Count >= 32) { throw new InvalidOperationException("The bounded evidence provider registry is full."); } value = new ProviderState(text); _providers.Add(text, value); } long num = ++_token; long token = (value.Token = num); ProviderSink sink = new ProviderSink(this, text, token); return new ProviderLease(this, text, token, sink); } } public EvidenceReadSnapshot ReadAfter(long sequence, int maximum) { maximum = Math.Max(0, Math.Min(256, maximum)); long num = Math.Max(0L, sequence); lock (_gate) { List list = new List(); List list2 = new List(_providers.Count); foreach (ProviderState value in _providers.Values) { foreach (SecurityEvidence entry in value.Entries) { if (entry.Sequence > num) { list.Add(entry); } } list2.Add(new EvidenceProviderStatus(value.ModuleId, IsActiveLocked(value), value.Entries.Count, value.Accepted, value.Dropped)); } list.Sort((SecurityEvidence left, SecurityEvidence right) => left.Sequence.CompareTo(right.Sequence)); if (list.Count > maximum) { list.RemoveRange(maximum, list.Count - maximum); } list2.Sort((EvidenceProviderStatus left, EvidenceProviderStatus right) => StringComparer.Ordinal.Compare(left.ProviderModuleId, right.ProviderModuleId)); return new EvidenceReadSnapshot(_sequence, _policySequence, list, list2); } } internal void SetPolicySequence(long sequence) { lock (_gate) { _policySequence = Math.Max(0L, sequence); } } private bool TryAppend(string moduleId, long token, string actor, string rule, string correlationId, FindingConfidence confidence, EnforcementAction requestedAction, string detail, out SecurityEvidence accepted) { accepted = null; Action action; lock (_gate) { if (!_providers.TryGetValue(moduleId, out var value) || value.Token != token) { return false; } if (!IsActiveLocked(value) || !BoundedSafe(actor, 128) || !BoundedSafe(rule, 128) || !BoundedSafe(correlationId, 128) || !BoundedSafe(detail, 512) || !IsConfidence(confidence) || !IsAction(requestedAction) || _sequence == long.MaxValue) { value.Dropped = SaturatingIncrement(value.Dropped); return false; } EnforcementAction enforcementAction = requestedAction; if (confidence <= FindingConfidence.Moderate && enforcementAction > EnforcementAction.Warn) { enforcementAction = EnforcementAction.Warn; } if (confidence < FindingConfidence.Conclusive && enforcementAction == EnforcementAction.Ban) { enforcementAction = EnforcementAction.Quarantine; } long sequence = _sequence + 1; accepted = new SecurityEvidence(sequence, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), moduleId, actor, rule, correlationId, confidence, requestedAction, enforcementAction, _policySequence, detail); _sequence = sequence; if (value.Entries.Count == 8) { value.Entries.Dequeue(); value.Dropped = SaturatingIncrement(value.Dropped); } value.Entries.Enqueue(accepted); value.Accepted = SaturatingIncrement(value.Accepted); action = this.Accepted; } try { action?.Invoke(accepted); } catch { } return true; } private static bool IsActiveLocked(ProviderState state) { return state.Token > 0; } private void Release(string moduleId, long token) { lock (_gate) { if (_providers.TryGetValue(moduleId, out var value) && value.Token == token) { value.Token = 0L; } } } private bool IsLeaseActive(string moduleId, long token) { lock (_gate) { ProviderState value; return _providers.TryGetValue(moduleId, out value) && value.Token == token && IsActiveLocked(value); } } private void PruneInactiveLocked() { List list = new List(); foreach (KeyValuePair provider in _providers) { if (!IsActiveLocked(provider.Value)) { list.Add(provider.Key); } } for (int i = 0; i < list.Count; i++) { _providers.Remove(list[i]); } } private static bool BoundedSafe(string value, int maximum) { if (value == null || value.Length == 0 || value.Length > maximum) { return false; } for (int i = 0; i < value.Length; i++) { UnicodeCategory unicodeCategory = char.GetUnicodeCategory(value[i]); if (char.IsControl(value[i]) || unicodeCategory == UnicodeCategory.Format || unicodeCategory == UnicodeCategory.Surrogate) { return false; } } return true; } private static bool IsConfidence(FindingConfidence value) { if (value >= FindingConfidence.Informational) { return value <= FindingConfidence.Conclusive; } return false; } private static bool IsAction(EnforcementAction value) { if (value >= EnforcementAction.Log) { return value <= EnforcementAction.Ban; } return false; } private static long SaturatingIncrement(long value) { if (value != long.MaxValue) { return value + 1; } return value; } } internal sealed class PinnedRsaPublicKey { internal const int ModulusBytes = 384; internal const int MaximumFileBytes = 1024; private static readonly byte[] CanonicalExponent = new byte[3] { 1, 0, 1 }; private readonly byte[] _modulus; internal string Fingerprint { get; } private PinnedRsaPublicKey(byte[] modulus, string fingerprint) { _modulus = (byte[])modulus.Clone(); Fingerprint = fingerprint; } internal RSAParameters CreateParameters() { return new RSAParameters { Modulus = (byte[])_modulus.Clone(), Exponent = (byte[])CanonicalExponent.Clone() }; } internal static bool TryParse(byte[] payload, string pinnedFingerprint, out PinnedRsaPublicKey key, out string failure) { key = null; failure = string.Empty; if (payload == null || payload.Length == 0 || payload.Length > 1024) { failure = "PublicKeySize"; return false; } if (!SentinelPolicy.IsLowerHex(pinnedFingerprint, 64)) { failure = "PublicKeyPin"; return false; } string text; try { text = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetString(payload); } catch { failure = "PublicKeyUtf8"; return false; } if (text.IndexOf('\r') >= 0 || text.IndexOf('\0') >= 0 || !text.EndsWith("\n", StringComparison.Ordinal)) { failure = "PublicKeyCanonical"; return false; } string[] array = text.Split('\n'); if (array.Length != 4 || array[0] != "RUNIC-RSA-PUBLIC/1" || !array[1].StartsWith("modulus=", StringComparison.Ordinal) || array[2] != "exponent=AQAB" || array[3].Length != 0) { failure = "PublicKeyCanonical"; return false; } byte[] array2; try { array2 = Convert.FromBase64String(array[1].Substring(8)); } catch { failure = "PublicKeyEncoding"; return false; } if (array2.Length != 384 || (array2[0] & 0x80) == 0 || Convert.ToBase64String(array2) != array[1].Substring(8)) { failure = "PublicKeyShape"; return false; } string text2; using (SHA256 sHA = SHA256.Create()) { text2 = SentinelPolicy.Hex(sHA.ComputeHash(payload)); } if (!SentinelPolicy.FixedTimeHexEquals(text2, pinnedFingerprint)) { failure = "PublicKeyPinMismatch"; return false; } key = new PinnedRsaPublicKey(array2, text2); return true; } } internal static class RunicIdentifier { internal static bool IsValid(string value) { if (string.IsNullOrEmpty(value) || value.Length > 128) { return false; } bool flag = false; bool flag2 = false; foreach (char c in value) { if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { flag = true; flag2 = false; continue; } switch (c) { case '-': if (!flag || flag2) { return false; } flag2 = true; break; case '.': if (!flag || flag2) { return false; } flag = false; flag2 = false; break; default: return false; } } if (flag) { return !flag2; } return false; } internal static string Require(string value, string parameterName) { if (!IsValid(value)) { throw new ArgumentException("Runic identifiers must be lowercase dot-separated ASCII tokens.", parameterName); } return value; } } internal enum SentinelRemoteAdmissionMode { Disabled, Optional, Required } internal readonly struct SentinelAdmissionCheck { internal bool Compatible { get; } internal string Reason { get; } internal SentinelAdmissionCheck(bool compatible, string reason) { Compatible = compatible; Reason = reason ?? string.Empty; } } internal sealed class SentinelNetworkCompatibility : IDisposable { private sealed class ServerConnection { internal long StartedTicks; internal long DeadlineTicks; internal long NextChallengeTicks; internal long NextDecisionTicks; internal long DisconnectAtTicks; internal long ResumeDeadlineTicks; internal long PeerInfoDeadlineTicks; internal int ChallengeAttempts; internal int DecisionAttempts; internal AdmissionChallenge Challenge; internal SentinelPolicy Policy; internal string AcceptedReportDigest; internal string DenialReason; internal object RegisteredHandler; internal bool Compliant; internal bool ApprovedAwaitingResume; internal bool ReleaseInProgress; internal bool NativeReleased; internal bool PeerInfoReleased; internal bool Denied; internal ZNetPeer Peer { get; } internal ZRpc Rpc { get; } internal ISocket Socket { get; } internal long Ordinal { get; } internal ServerConnection(ZNetPeer peer, ZRpc rpc, long ordinal) { Peer = peer; Rpc = rpc; Socket = peer?.m_socket; Ordinal = ordinal; AcceptedReportDigest = string.Empty; DenialReason = string.Empty; } internal bool IsExact() { if (Peer != null && Rpc != null && Peer.m_rpc == Rpc) { return Peer.m_socket == Socket; } return false; } } internal const int MaximumTrackedConnections = 64; internal const int MaximumChallengeAttempts = 3; internal const int MaximumOuterPackageBytes = 262152; internal const long AdmissionGraceSeconds = 20L; internal const long ResumeGraceSeconds = 10L; internal const long PeerInfoGraceSeconds = 120L; private static readonly long RetryTicks = DurationTicks(2L); private static readonly long DecisionRetryTicks = DurationTicks(1L); private static readonly long DisconnectGraceTicks = DurationTicks(1L); private const string EvidenceProviderId = "runic.sentinel.network"; private static readonly object ActiveLock = new object(); private static readonly FieldInfo RpcFunctionsField = typeof(ZRpc).GetField("m_functions", BindingFlags.Instance | BindingFlags.NonPublic); private static SentinelNetworkCompatibility _active; private readonly object _gate = new object(); private readonly SentinelRuntime _runtime; private readonly SentinelRemoteAdmissionMode _mode; private readonly ISentinelEvidenceProviderLease _evidence; private readonly Dictionary _serverConnections = new Dictionary(); private ZNet _network; private long _nextConnectionOrdinal; private bool _disposed; internal bool IsActive { get { lock (_gate) { return !_disposed && _mode != SentinelRemoteAdmissionMode.Disabled && (Object)(object)_network != (Object)null; } } } internal SentinelRemoteAdmissionMode Mode { get { lock (_gate) { return _mode; } } } internal SentinelNetworkCompatibility(SentinelRuntime runtime, EvidenceLedger evidence, SentinelRemoteAdmissionMode mode) { _runtime = runtime ?? throw new ArgumentNullException("runtime"); if (mode < SentinelRemoteAdmissionMode.Disabled || mode > SentinelRemoteAdmissionMode.Required) { throw new ArgumentOutOfRangeException("mode"); } ValidatePatchSeams(); _mode = mode; _evidence = (evidence ?? throw new ArgumentNullException("evidence")).RegisterProvider("runic.sentinel.network"); lock (ActiveLock) { if (_active != null && !_active._disposed) { throw new InvalidOperationException("Sentinel admission transport is already active."); } _active = this; } } internal void Tick() { lock (_gate) { if (_disposed || _mode == SentinelRemoteAdmissionMode.Disabled) { return; } ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null)) { EnsureNetworkLocked(instance); long nowTicks = MonotonicTicks(); if (instance.IsServer()) { TickServerLocked(instance, nowTicks); return; } ClearConnectionsLocked(); _network = null; } } } internal static SentinelAdmissionCheck Evaluate(SentinelRuntime runtime, SentinelPolicy policy, AdmissionClientProfile profile, string role) { if (runtime == null || policy == null) { return Fail("sentinel-server-policy-unavailable"); } if (profile == null) { return Fail("sentinel-client-profile-invalid"); } AdmissionDecision admissionDecision; try { admissionDecision = runtime.EvaluateAdmissionClientProfile(policy, profile, (role == "administrator") ? "administrator" : "player"); } catch { return Fail("sentinel-client-profile-invalid"); } if (admissionDecision == null || admissionDecision.Disposition != AdmissionDisposition.Allow) { return Fail(ReasonFor(admissionDecision)); } return new SentinelAdmissionCheck(compatible: true, "compatible"); } internal static bool DisconnectsForFailure(SentinelRemoteAdmissionMode mode) { return mode == SentinelRemoteAdmissionMode.Required; } internal static bool AllowsPeerInfo(SentinelRemoteAdmissionMode mode, bool exactConnection, bool compliant, bool nativeHandshakeReleased, bool denied) { if (mode == SentinelRemoteAdmissionMode.Required) { if (exactConnection && compliant && nativeHandshakeReleased) { return !denied; } return false; } return true; } internal static void ObserveConnection(ZNet network, ZNetPeer peer) { Current()?.ObserveConnectionCore(network, peer); } internal static bool BeforeServerHandshake(ZNet network, ZRpc rpc, out bool approvedResume) { approvedResume = false; return Current()?.BeforeServerHandshakeCore(network, rpc, out approvedResume) ?? true; } internal static void AfterServerHandshake(ZNet network, ZRpc rpc, bool approvedResume) { if (approvedResume) { Current()?.AfterServerHandshakeCore(network, rpc); } } internal static void ServerHandshakeFailed(ZNet network, ZRpc rpc, bool approvedResume) { if (approvedResume) { Current()?.ServerHandshakeFailedCore(network, rpc); } } internal static bool BeforePeerInfo(ZNet network, ZRpc rpc, out bool approvedAdmission) { approvedAdmission = false; return Current()?.BeforePeerInfoCore(network, rpc, out approvedAdmission) ?? true; } internal static void AfterPeerInfo(ZNet network, ZRpc rpc, bool approvedAdmission) { if (approvedAdmission) { Current()?.AfterPeerInfoCore(network, rpc); } } internal static void PeerInfoFailed(ZNet network, ZRpc rpc, bool approvedAdmission) { if (approvedAdmission) { Current()?.PeerInfoFailedCore(network, rpc); } } internal static void ForgetConnection(ZNet network, ZNetPeer peer) { Current()?.ForgetConnectionCore(network, peer); } internal static void ForgetNetwork(ZNet network) { Current()?.ForgetNetworkCore(network); } public void Dispose() { lock (ActiveLock) { if (_active == this) { _active = null; } } lock (_gate) { if (_disposed) { return; } _disposed = true; ClearConnectionsLocked(); _network = null; } try { _evidence?.Dispose(); } catch { } } private static SentinelNetworkCompatibility Current() { lock (ActiveLock) { return (_active != null && !_active._disposed) ? _active : null; } } private void ObserveConnectionCore(ZNet network, ZNetPeer peer) { if ((Object)(object)network == (Object)null || peer?.m_rpc == null) { return; } lock (_gate) { if (!_disposed && _mode != SentinelRemoteAdmissionMode.Disabled) { EnsureNetworkLocked(network); if (network.IsServer()) { AddServerConnectionLocked(peer); } } } } private bool BeforeServerHandshakeCore(ZNet network, ZRpc rpc, out bool approvedResume) { approvedResume = false; if ((Object)(object)network == (Object)null || rpc == null || _mode == SentinelRemoteAdmissionMode.Disabled) { return true; } lock (_gate) { if (_disposed || !network.IsServer()) { return true; } EnsureNetworkLocked(network); if (!_serverConnections.TryGetValue(rpc, out var value)) { ZNetPeer peer = FindExactPeer(network, rpc); value = AddServerConnectionLocked(peer); } if (value == null || !value.IsExact()) { if (_mode == SentinelRemoteAdmissionMode.Required && value != null) { DenyLocked(value, "sentinel-peer-binding-invalid", MonotonicTicks()); } return _mode != SentinelRemoteAdmissionMode.Required; } long nowTicks = MonotonicTicks(); if (_mode == SentinelRemoteAdmissionMode.Optional) { value.NativeReleased = true; BeginAdmissionLocked(value, nowTicks); return true; } if (value.ApprovedAwaitingResume) { if (!PolicyIsCurrentLocked(value)) { FailAdmissionLocked(value, "sentinel-policy-changed", nowTicks); return false; } value.ApprovedAwaitingResume = false; value.ReleaseInProgress = true; approvedResume = true; return true; } if (value.NativeReleased || value.ReleaseInProgress || value.Denied) { return false; } BeginAdmissionLocked(value, nowTicks); return false; } } private void AfterServerHandshakeCore(ZNet network, ZRpc rpc) { lock (_gate) { if (!_disposed && network == _network && rpc != null && _serverConnections.TryGetValue(rpc, out var value)) { value.ReleaseInProgress = false; if (!PolicyIsCurrentLocked(value)) { FailAdmissionLocked(value, "sentinel-policy-changed", MonotonicTicks()); return; } value.NativeReleased = true; value.PeerInfoDeadlineTicks = MonotonicTicks() + DurationTicks(120L); } } } private void ServerHandshakeFailedCore(ZNet network, ZRpc rpc) { lock (_gate) { if (!_disposed && network == _network && rpc != null && _serverConnections.TryGetValue(rpc, out var value)) { value.ReleaseInProgress = false; FailAdmissionLocked(value, "sentinel-native-handshake-failed", MonotonicTicks()); } } } private bool BeforePeerInfoCore(ZNet network, ZRpc rpc, out bool approvedAdmission) { approvedAdmission = false; if ((Object)(object)network == (Object)null || rpc == null || _mode == SentinelRemoteAdmissionMode.Disabled) { return true; } lock (_gate) { if (_disposed || !network.IsServer()) { return true; } EnsureNetworkLocked(network); if (!_serverConnections.TryGetValue(rpc, out var value)) { ZNetPeer peer = FindExactPeer(network, rpc); value = AddServerConnectionLocked(peer); } if (_mode == SentinelRemoteAdmissionMode.Optional) { if (value != null && value.IsExact()) { value.NativeReleased = true; BeginAdmissionLocked(value, MonotonicTicks()); } return true; } bool flag = AllowsPeerInfo(_mode, value?.IsExact() ?? false, value?.Compliant ?? false, value?.NativeReleased ?? false, value?.Denied ?? true); if (flag && !PolicyIsCurrentLocked(value)) { FailAdmissionLocked(value, "sentinel-policy-changed", MonotonicTicks()); return false; } if (flag) { approvedAdmission = true; return true; } if (value != null && value.IsExact() && !value.Denied) { long nowTicks = MonotonicTicks(); BeginAdmissionLocked(value, nowTicks); FailAdmissionLocked(value, "sentinel-peer-info-before-admission", nowTicks); } return false; } } private void AfterPeerInfoCore(ZNet network, ZRpc rpc) { lock (_gate) { if (_disposed || network != _network || rpc == null || !_serverConnections.TryGetValue(rpc, out var value) || !value.IsExact()) { return; } if (!PolicyIsCurrentLocked(value)) { FailAdmissionLocked(value, "sentinel-policy-changed", MonotonicTicks()); return; } bool flag; try { flag = value.Peer.IsReady(); } catch { flag = false; } if (!flag) { FailAdmissionLocked(value, "sentinel-native-peer-info-incomplete", MonotonicTicks()); return; } value.PeerInfoReleased = true; value.PeerInfoDeadlineTicks = 0L; } } private void PeerInfoFailedCore(ZNet network, ZRpc rpc) { lock (_gate) { if (!_disposed && network == _network && rpc != null && _serverConnections.TryGetValue(rpc, out var value)) { FailAdmissionLocked(value, "sentinel-native-peer-info-failed", MonotonicTicks()); } } } private void ForgetConnectionCore(ZNet network, ZNetPeer peer) { if (peer?.m_rpc == null) { return; } lock (_gate) { if (!_disposed && network == _network) { RemoveConnectionLocked(peer.m_rpc); } } } private void ForgetNetworkCore(ZNet network) { lock (_gate) { if (!_disposed && network == _network) { ClearConnectionsLocked(); _network = null; } } } private void EnsureNetworkLocked(ZNet network) { if (_network != network) { ClearConnectionsLocked(); _network = network; } } private void TickServerLocked(ZNet network, long nowTicks) { List peers; try { peers = network.GetPeers(); } catch { return; } int num = 0; if (peers != null) { foreach (ZNetPeer item in peers) { if (num++ >= 64) { break; } if (item?.m_rpc != null) { AddServerConnectionLocked(item); } } } foreach (ServerConnection item2 in new List(_serverConnections.Values)) { if (!item2.IsExact() || !SocketConnected(item2.Peer)) { RemoveConnectionLocked(item2.Rpc); } else if (item2.Denied) { if (item2.DisconnectAtTicks != 0L && nowTicks >= item2.DisconnectAtTicks) { ZNetPeer peer = item2.Peer; RemoveConnectionLocked(item2.Rpc); try { network.Disconnect(peer); } catch { } } } else { if (item2.PeerInfoReleased && item2.Compliant) { continue; } if (item2.Compliant) { if (!PolicyIsCurrentLocked(item2)) { FailAdmissionLocked(item2, "sentinel-policy-changed", nowTicks); } else if (!item2.NativeReleased && item2.ResumeDeadlineTicks != 0L && nowTicks >= item2.ResumeDeadlineTicks) { FailAdmissionLocked(item2, "sentinel-native-handshake-resume-timeout", nowTicks); } else if (item2.NativeReleased && !item2.PeerInfoReleased && item2.PeerInfoDeadlineTicks != 0L && nowTicks >= item2.PeerInfoDeadlineTicks) { FailAdmissionLocked(item2, "sentinel-peer-info-timeout", nowTicks); } else if (!item2.NativeReleased && item2.ApprovedAwaitingResume && item2.DecisionAttempts < 3 && nowTicks >= item2.NextDecisionTicks) { SendDecisionLocked(item2, accepted: true, resume: true, "compatible", nowTicks); } } else { if (item2.StartedTicks == 0L) { continue; } if (nowTicks >= item2.DeadlineTicks) { string reason = ((item2.Challenge == null) ? "sentinel-server-policy-unavailable" : "sentinel-client-absent"); FailAdmissionLocked(item2, reason, nowTicks); continue; } EnsureChallengeLocked(item2, nowTicks); if (item2.Challenge != null && !item2.Compliant && item2.ChallengeAttempts < 3 && nowTicks >= item2.NextChallengeTicks) { SendChallengeLocked(item2, nowTicks); } } } } } private ServerConnection AddServerConnectionLocked(ZNetPeer peer) { ZRpc val = peer?.m_rpc; if (val == null) { return null; } if (_serverConnections.TryGetValue(val, out var value)) { if (!value.IsExact()) { return null; } return value; } if (_serverConnections.Count >= 64) { if (_mode == SentinelRemoteAdmissionMode.Required) { try { ZNet network = _network; if (network != null) { network.Disconnect(peer); } } catch { } } return null; } long ordinal = ((_nextConnectionOrdinal == long.MaxValue) ? long.MaxValue : (++_nextConnectionOrdinal)); ServerConnection serverConnection = new ServerConnection(peer, val, ordinal); _serverConnections.Add(val, serverConnection); if (!TryRegisterDirectHandler(val, out var registered, out var failure)) { if (_mode == SentinelRemoteAdmissionMode.Required) { FailAdmissionLocked(serverConnection, failure, MonotonicTicks()); } else { _runtime.RecordAdmissionFailure(failure); } return serverConnection; } serverConnection.RegisteredHandler = registered; if (_mode == SentinelRemoteAdmissionMode.Required) { BeginAdmissionLocked(serverConnection, MonotonicTicks()); } return serverConnection; } private void BeginAdmissionLocked(ServerConnection state, long nowTicks) { if (state.StartedTicks == 0L && state.RegisteredHandler != null) { state.StartedTicks = nowTicks; state.DeadlineTicks = nowTicks + DurationTicks(20L); EnsureChallengeLocked(state, nowTicks); if (state.Challenge != null) { SendChallengeLocked(state, nowTicks); } } } private void EnsureChallengeLocked(ServerConnection state, long nowTicks) { if (state.Challenge != null || state.Denied) { return; } SentinelPolicy policy; try { if (!_runtime.TryGetVerifiedPolicy(out policy) || policy == null) { return; } } catch { return; } long nowUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); long lifetimeSeconds = Math.Max(1L, Math.Min(120L, (state.DeadlineTicks - nowTicks) / Stopwatch.Frequency)); state.Policy = policy; state.Challenge = AdmissionProtocolV2.CreateChallenge(nowUnixSeconds, lifetimeSeconds); state.NextChallengeTicks = nowTicks; state.ChallengeAttempts = 0; } private void SendChallengeLocked(ServerConnection state, long nowTicks) { if (state.Challenge == null || !state.IsExact()) { return; } try { InvokeFrame(state.Rpc, AdmissionProtocolV2.EncodeChallenge(state.Challenge)); state.ChallengeAttempts++; state.NextChallengeTicks = nowTicks + RetryTicks; } catch { state.NextChallengeTicks = nowTicks + RetryTicks; } } private void ReceiveDirect(ZRpc rpc, ZPackage package) { lock (_gate) { if (!_disposed && rpc != null && TryReadFrame(package, out var frame) && AdmissionProtocolV2.TryGetKind(frame, out var kind, out var _)) { ZNet network = _network; if (!((Object)(object)network == (Object)null) && network.IsServer() && kind == AdmissionMessageKind.Report) { ReceiveServerReportLocked(rpc, frame, MonotonicTicks()); } } } } private void ReceiveServerReportLocked(ZRpc rpc, byte[] frame, long nowTicks) { if (!_serverConnections.TryGetValue(rpc, out var value) || value.Denied || !value.IsExact()) { return; } if (!AdmissionProtocolV2.TryDecodeReport(frame, out var value2, out var failure)) { FailAdmissionLocked(value, failure, nowTicks); return; } if (value.Challenge == null || value.Policy == null) { FailAdmissionLocked(value, "sentinel-challenge-unavailable", nowTicks); return; } if (!PolicyIsCurrentLocked(value)) { FailAdmissionLocked(value, "sentinel-policy-changed", nowTicks); return; } if (value.AcceptedReportDigest.Length != 0) { if (string.Equals(value.AcceptedReportDigest, value2.ProfileDigest, StringComparison.Ordinal)) { SendDecisionLocked(value, value.Compliant, _mode == SentinelRemoteAdmissionMode.Required && !value.NativeReleased, value.Compliant ? "compatible" : "sentinel-report-rejected", nowTicks); } else { FailAdmissionLocked(value, "sentinel-report-equivocation", nowTicks); } return; } long receivedUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (!AdmissionProtocolV2.TryValidateReport(value.Challenge, value2, receivedUnixSeconds, out var profile, out failure)) { FailAdmissionLocked(value, failure, nowTicks); return; } _runtime.ObserveRemoteAdmissionProfile(profile); string role = "player"; if (SentinelTransportIdentity.TryResolveConnection(value.Peer, out var authority, out var subject)) { if (_runtime.IsBanned(authority, subject)) { FailAdmissionLocked(value, "sentinel-account-banned", nowTicks); return; } if (_runtime.IsAdministrator(authority, subject)) { role = "administrator"; } } SentinelAdmissionCheck sentinelAdmissionCheck = Evaluate(_runtime, value.Policy, profile, role); value.AcceptedReportDigest = value2.ProfileDigest; if (!sentinelAdmissionCheck.Compatible) { FailAdmissionLocked(value, sentinelAdmissionCheck.Reason, nowTicks); return; } value.Compliant = true; value.ApprovedAwaitingResume = _mode == SentinelRemoteAdmissionMode.Required && !value.NativeReleased; value.ResumeDeadlineTicks = (value.ApprovedAwaitingResume ? (nowTicks + DurationTicks(10L)) : 0); SendDecisionLocked(value, accepted: true, value.ApprovedAwaitingResume, "compatible", nowTicks); } private void SendDecisionLocked(ServerConnection state, bool accepted, bool resume, string reason, long nowTicks) { if (state.Challenge == null || !state.IsExact()) { return; } long policySequence = state.Policy?.Sequence ?? 0; string policyProfile = state.Policy?.Profile ?? string.Empty; try { AdmissionDecisionMessage value = new AdmissionDecisionMessage(state.Challenge.RequestId, accepted, accepted && resume, CanonicalReason(reason), policySequence, policyProfile, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); InvokeFrame(state.Rpc, AdmissionProtocolV2.EncodeDecision(value)); state.DecisionAttempts++; state.NextDecisionTicks = nowTicks + DecisionRetryTicks; } catch { state.NextDecisionTicks = nowTicks + DecisionRetryTicks; } } private void FailAdmissionLocked(ServerConnection state, string reason, long nowTicks) { string reason2 = CanonicalReason(reason); RecordLocked(state, reason2); _runtime.RecordAdmissionFailure(reason2); if (_mode == SentinelRemoteAdmissionMode.Required) { SendDecisionLocked(state, accepted: false, resume: false, reason2, nowTicks); DenyLocked(state, reason2, nowTicks); } else { ResetOptionalAdmissionExchangeLocked(state); } } private static void ResetOptionalAdmissionExchangeLocked(ServerConnection state) { state.StartedTicks = 0L; state.DeadlineTicks = 0L; state.NextChallengeTicks = 0L; state.NextDecisionTicks = 0L; state.ResumeDeadlineTicks = 0L; state.ChallengeAttempts = 0; state.DecisionAttempts = 0; state.Challenge = null; state.Policy = null; state.AcceptedReportDigest = string.Empty; state.Compliant = false; state.ApprovedAwaitingResume = false; state.ReleaseInProgress = false; } private void DenyLocked(ServerConnection state, string reason, long nowTicks) { state.Denied = true; state.DenialReason = CanonicalReason(reason); state.ApprovedAwaitingResume = false; state.ReleaseInProgress = false; state.DisconnectAtTicks = nowTicks + DisconnectGraceTicks; } private void RecordLocked(ServerConnection state, string reason) { ISentinelEvidenceSink sentinelEvidenceSink = _evidence?.Sink; if (sentinelEvidenceSink != null && state != null) { string correlationId = state.Challenge?.RequestId ?? ("connection-" + state.Ordinal); sentinelEvidenceSink.TryAppend("connection:" + state.Ordinal, "sentinel.remote-admission", correlationId, (_mode == SentinelRemoteAdmissionMode.Required) ? FindingConfidence.High : FindingConfidence.Moderate, (_mode == SentinelRemoteAdmissionMode.Required) ? EnforcementAction.Disconnect : EnforcementAction.Warn, "reason-" + CanonicalReason(reason) + ".direct-peer-bound", out var _); } } private static string ReasonFor(AdmissionDecision decision) { if (decision == null) { return "sentinel-policy-not-allow"; } if (decision.Disposition == AdmissionDisposition.Quarantine) { return "sentinel-policy-quarantine"; } if (decision.Findings != null && decision.Findings.Count != 0) { string text = decision.Findings[0]?.Rule; if (AdmissionValidation.IsAtom(text, 1, 64)) { return "sentinel-policy-" + text.ToLowerInvariant(); } } return "sentinel-policy-not-allow"; } private static string CanonicalReason(string value) { if (!AdmissionValidation.IsReason(value)) { return "sentinel-admission-rejected"; } return value; } private static long MonotonicTicks() { return Stopwatch.GetTimestamp(); } private static long DurationTicks(long seconds) { return checked(seconds * Stopwatch.Frequency); } private bool PolicyIsCurrentLocked(ServerConnection state) { if (state?.Policy == null) { return false; } try { SentinelPolicy policy; return _runtime.TryGetVerifiedPolicy(out policy) && policy == state.Policy; } catch { return false; } } private static SentinelAdmissionCheck Fail(string reason) { return new SentinelAdmissionCheck(compatible: false, CanonicalReason(reason)); } private static ZNetPeer FindExactPeer(ZNet network, ZRpc rpc) { List list; try { list = ((network != null) ? network.GetPeers() : null); } catch { return null; } if (list == null) { return null; } int num = 0; foreach (ZNetPeer item in list) { if (num++ >= 64) { break; } if (item != null && item.m_rpc == rpc) { return item; } } return null; } private static bool SocketConnected(ZNetPeer peer) { try { return peer?.m_socket != null && peer.m_socket.IsConnected(); } catch { return false; } } private static void InvokeFrame(ZRpc rpc, byte[] frame) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown if (rpc == null || frame == null || frame.Length == 0 || frame.Length > 262144) { throw new ArgumentOutOfRangeException("frame"); } ZPackage val = new ZPackage(frame); if (val.Size() > 262152) { throw new InvalidOperationException("Admission package exceeded its wire bound."); } rpc.Invoke("chazman.RunicSentinel.Admission.v2", new object[1] { val }); } private static bool TryReadFrame(ZPackage package, out byte[] frame) { frame = null; try { if (package == null || package.Size() <= 0 || package.Size() > 262152) { return false; } frame = package.GetArray(); return frame != null && frame.Length != 0 && frame.Length <= 262144; } catch { frame = null; return false; } } private void RemoveConnectionLocked(ZRpc rpc) { if (rpc != null && _serverConnections.TryGetValue(rpc, out var value)) { _serverConnections.Remove(rpc); UnregisterOwnedHandler(rpc, value.RegisteredHandler); } } private void ClearConnectionsLocked() { foreach (ServerConnection item in new List(_serverConnections.Values)) { UnregisterOwnedHandler(item.Rpc, item.RegisteredHandler); } _serverConnections.Clear(); } private bool TryRegisterDirectHandler(ZRpc rpc, out object registered, out string failure) { registered = null; failure = "sentinel-rpc-registration-failed"; if (!TryGetFunctionMap(rpc, out var functions)) { failure = "sentinel-rpc-registry-unavailable"; return false; } int num = StableHash("chazman.RunicSentinel.Admission.v2"); try { if (functions.Contains(num)) { failure = "sentinel-rpc-name-collision"; return false; } rpc.Register("chazman.RunicSentinel.Admission.v2", (Action)ReceiveDirect); registered = functions[num]; return registered != null; } catch { registered = null; return false; } } private static void UnregisterOwnedHandler(ZRpc rpc, object registered) { if (rpc == null || registered == null || !TryGetFunctionMap(rpc, out var functions)) { return; } int num = StableHash("chazman.RunicSentinel.Admission.v2"); try { if (functions[num] == registered) { rpc.Unregister("chazman.RunicSentinel.Admission.v2"); } } catch { } } private static bool TryGetFunctionMap(ZRpc rpc, out IDictionary functions) { functions = null; if (rpc == null || RpcFunctionsField == null) { return false; } try { functions = RpcFunctionsField.GetValue(rpc) as IDictionary; return functions != null; } catch { functions = null; return false; } } private static int StableHash(string value) { int num = 5381; int num2 = num; for (int i = 0; i < value.Length && value[i] != 0; i += 2) { num = ((num << 5) + num) ^ value[i]; if (i == value.Length - 1 || value[i + 1] == '\0') { break; } num2 = ((num2 << 5) + num2) ^ value[i + 1]; } return num + num2 * 1566083941; } private static void ValidatePatchSeams() { ValidateVoid("OnNewConnection", typeof(ZNetPeer)); ValidateVoid("RPC_ServerHandshake", typeof(ZRpc)); ValidateVoid("RPC_PeerInfo", typeof(ZRpc), typeof(ZPackage)); ValidateVoid("Disconnect", typeof(ZNetPeer)); } private static void ValidateVoid(string name, params Type[] parameters) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), name, parameters, (Type[])null); if (methodInfo == null || methodInfo.IsStatic || methodInfo.ReturnType != typeof(void)) { throw new MissingMethodException(typeof(ZNet).FullName, name); } } } [HarmonyPatch(typeof(ZNet), "RPC_ServerHandshake", new Type[] { typeof(ZRpc) })] internal static class SentinelServerHandshakePatch { [HarmonyPrefix] [HarmonyPriority(800)] private static bool Prefix(ZNet __instance, [HarmonyArgument(0)] ZRpc rpc, ref bool __state) { return SentinelNetworkCompatibility.BeforeServerHandshake(__instance, rpc, out __state); } [HarmonyPostfix] private static void Postfix(ZNet __instance, [HarmonyArgument(0)] ZRpc rpc, bool __state) { SentinelNetworkCompatibility.AfterServerHandshake(__instance, rpc, __state); } [HarmonyFinalizer] private static Exception Finalizer(ZNet __instance, [HarmonyArgument(0)] ZRpc rpc, bool __state, Exception __exception) { if (__exception != null) { SentinelNetworkCompatibility.ServerHandshakeFailed(__instance, rpc, __state); } return __exception; } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo", new Type[] { typeof(ZRpc), typeof(ZPackage) })] internal static class SentinelPeerInfoPatch { [HarmonyPrefix] [HarmonyPriority(800)] private static bool Prefix(ZNet __instance, [HarmonyArgument(0)] ZRpc rpc, ref bool __state) { return SentinelNetworkCompatibility.BeforePeerInfo(__instance, rpc, out __state); } [HarmonyPostfix] private static void Postfix(ZNet __instance, [HarmonyArgument(0)] ZRpc rpc, bool __state) { SentinelNetworkCompatibility.AfterPeerInfo(__instance, rpc, __state); } [HarmonyFinalizer] private static Exception Finalizer(ZNet __instance, [HarmonyArgument(0)] ZRpc rpc, bool __state, Exception __exception) { if (__exception != null) { SentinelNetworkCompatibility.PeerInfoFailed(__instance, rpc, __state); } return __exception; } } [HarmonyPatch(typeof(ZNet), "Disconnect", new Type[] { typeof(ZNetPeer) })] internal static class SentinelDisconnectPatch { [HarmonyPrefix] private static void Prefix(ZNet __instance, [HarmonyArgument(0)] ZNetPeer peer) { SentinelNetworkCompatibility.ForgetConnection(__instance, peer); } } [HarmonyPatch(typeof(ZNet), "OnDestroy")] internal static class SentinelNetworkDestroyPatch { [HarmonyPrefix] private static void Prefix(ZNet __instance) { SentinelNetworkCompatibility.ForgetNetwork(__instance); } } internal sealed class SentinelPolicyRule { internal PluginClassification Classification { get; } internal string Id { get; } internal string Version { get; } internal string Sha256 { get; } internal SentinelPolicyRule(PluginClassification classification, string id, string version, string sha256) { Classification = classification; Id = id; Version = version; Sha256 = sha256; } } internal enum SentinelModuleScope { Both, Client, Server } internal sealed class SentinelModuleRule { internal SentinelModuleScope Scope { get; } internal string Id { get; } internal string Version { get; } internal int Protocol { get; } internal IReadOnlyList Capabilities { get; } internal SentinelModuleRule(SentinelModuleScope scope, string id, string version, int protocol, IReadOnlyList capabilities) { Scope = scope; Id = id; Version = version; Protocol = protocol; Capabilities = capabilities; } } internal sealed class SentinelAdministratorRole { internal string Authority { get; } internal string Subject { get; } internal string CanonicalKey => Authority + ":" + Uri.EscapeDataString(Subject); internal SentinelAdministratorRole(string authority, string subject) { Authority = authority; Subject = subject; } } internal sealed class SentinelPolicy { internal const int MaximumBytes = 1048576; internal const int MaximumRules = 2048; internal const int SignatureBytes = 384; internal const int MaximumSignatureFileBytes = 1024; internal int FormatVersion { get; } internal string Profile { get; } internal long Sequence { get; } internal long IssuedUnixSeconds { get; } internal long ExpiresUnixSeconds { get; } internal PluginClassification Unknown { get; } internal IReadOnlyList Rules { get; } internal IReadOnlyList Modules { get; } internal IReadOnlyList Administrators { get; } internal IReadOnlyList BannedUsers { get; } internal string PayloadDigest { get; } internal SentinelPolicy(int formatVersion, string profile, long sequence, long issuedUnixSeconds, long expiresUnixSeconds, PluginClassification unknown, IReadOnlyList rules, IReadOnlyList modules, IReadOnlyList administrators, IReadOnlyList bannedUsers, string payloadDigest) { FormatVersion = formatVersion; Profile = profile; Sequence = sequence; IssuedUnixSeconds = issuedUnixSeconds; ExpiresUnixSeconds = expiresUnixSeconds; Unknown = unknown; Rules = rules; Modules = modules; Administrators = administrators; BannedUsers = bannedUsers; PayloadDigest = payloadDigest; } internal static bool TryDecodeSignatureFile(byte[] file, out byte[] signature, out string failure) { signature = null; failure = string.Empty; if (file == null || file.Length == 0 || file.Length > 1024) { failure = "SignatureSize"; return false; } string text; try { text = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetString(file); } catch { failure = "SignatureUtf8"; return false; } if (text.IndexOf('\r') >= 0 || text.IndexOf('\0') >= 0 || !text.EndsWith("\n", StringComparison.Ordinal) || text.IndexOf('\n') != text.Length - 1) { failure = "SignatureCanonical"; return false; } string text2 = text.Substring(0, text.Length - 1); try { signature = Convert.FromBase64String(text2); } catch { failure = "SignatureEncoding"; return false; } if (signature.Length != 384 || Convert.ToBase64String(signature) != text2) { signature = null; failure = "SignatureShape"; return false; } return true; } internal static bool TryParseAndVerify(byte[] payload, byte[] signature, PinnedRsaPublicKey publicKey, out SentinelPolicy policy, out string failure) { policy = null; failure = string.Empty; if (payload == null || payload.Length == 0 || payload.Length > 1048576) { failure = "PolicySize"; return false; } if (signature == null || signature.Length != 384 || publicKey == null) { failure = "SignatureShape"; return false; } try { using RSA rSA = RSA.Create(); rSA.ImportParameters(publicKey.CreateParameters()); if (rSA.KeySize != 3072 || !rSA.VerifyData(payload, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)) { failure = "SignatureMismatch"; return false; } } catch { failure = "SignatureVerification"; return false; } string text; try { text = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetString(payload); } catch { failure = "PolicyUtf8"; return false; } if (text.IndexOf('\r') >= 0 || text.IndexOf('\0') >= 0 || !text.EndsWith("\n", StringComparison.Ordinal)) { failure = "PolicyCanonical"; return false; } string[] array = text.Split('\n'); bool num = array.Length >= 7 && array[0] == "RUNIC-SENTINEL/2"; bool flag = array.Length >= 8 && array[0] == "RUNIC-SENTINEL/3"; if ((!num && !flag) || array[^1].Length != 0) { failure = "PolicyHeader"; return false; } if (!TryRequired(array[1], "profile=", 64, out var value) || !TryCanonicalLong(array[2], "sequence=", allowZero: false, out var value2) || !TryCanonicalLong(array[3], "issued=", allowZero: true, out var value3) || !TryCanonicalLong(array[4], "expires=", allowZero: true, out var value4) || (value4 != 0L && value4 <= value3) || !TryUnknown(array[5], out var value5)) { failure = "PolicyPreamble"; return false; } int num2 = 6; if (flag) { if (array[6] != "unknown-capability=Forbidden") { failure = "CapabilityDefault"; return false; } num2 = 7; } List list = new List(); List list2 = new List(); List list3 = new List(); List list4 = new List(); string text2 = null; string text3 = null; string text4 = null; string text5 = null; int num3 = 0; for (int i = num2; i < array.Length - 1; i++) { string text6 = array[i]; if (text6.StartsWith("rule=", StringComparison.Ordinal) && num3 <= 0) { if (list.Count >= 2048) { failure = "PolicyRule"; return false; } string[] array2 = text6.Substring(5).Split('|'); if (array2.Length != 4 || !TryClassification(array2[0], out var value6) || !CanonicalPluginId(array2[1]) || !CanonicalVersionOrWildcard(array2[2]) || !CanonicalHashOrWildcard(array2[3])) { failure = "PolicyRule"; return false; } if (text2 != null && string.CompareOrdinal(text2, array2[1]) >= 0) { failure = "RuleOrder"; return false; } text2 = array2[1]; list.Add(new SentinelPolicyRule(value6, array2[1], array2[2], array2[3])); continue; } if (flag && text6.StartsWith("module=", StringComparison.Ordinal) && num3 <= 1) { num3 = 1; if (list2.Count >= 128) { failure = "ModuleCap"; return false; } string[] array3 = text6.Substring(7).Split('|'); if (array3.Length != 5 || !TryModuleScope(array3[0], out var scope) || !CanonicalPluginId(array3[1]) || !CanonicalVersionOrWildcard(array3[2]) || !TryCanonicalInt(array3[3], out var value7) || !TryCapabilities(array3[4], out var capabilities)) { failure = "ModuleRule"; return false; } if (text3 != null && string.CompareOrdinal(text3, array3[1]) >= 0) { failure = "ModuleOrder"; return false; } text3 = array3[1]; list2.Add(new SentinelModuleRule(scope, array3[1], array3[2], value7, capabilities)); continue; } if (flag && text6.StartsWith("role=", StringComparison.Ordinal) && num3 <= 2) { num3 = 2; if (list3.Count >= 256) { failure = "RoleCap"; return false; } string[] array4 = text6.Substring(5).Split('|'); if (array4.Length != 2 || !CanonicalAuthority(array4[0]) || !TryCanonicalSubject(array4[1], out var subject)) { failure = "RoleRule"; return false; } string text7 = array4[0] + ":" + array4[1]; if (text4 != null && string.CompareOrdinal(text4, text7) >= 0) { failure = "RoleOrder"; return false; } text4 = text7; list3.Add(new SentinelAdministratorRole(array4[0], subject)); continue; } if (flag && text6.StartsWith("ban=", StringComparison.Ordinal) && num3 <= 3) { num3 = 3; if (list4.Count >= 4096) { failure = "BanCap"; return false; } string[] array5 = text6.Substring(4).Split('|'); if (array5.Length != 2 || !CanonicalAuthority(array5[0]) || !TryCanonicalSubject(array5[1], out var subject2)) { failure = "BanRule"; return false; } string text8 = array5[0] + ":" + array5[1]; if (text5 != null && string.CompareOrdinal(text5, text8) >= 0) { failure = "BanOrder"; return false; } text5 = text8; list4.Add(new SentinelAdministratorRole(array5[0], subject2)); continue; } failure = (flag ? "PolicyEntry" : "PolicyRule"); return false; } string payloadDigest; using (SHA256 sHA = SHA256.Create()) { payloadDigest = Hex(sHA.ComputeHash(payload)); } policy = new SentinelPolicy(flag ? 3 : 2, value, value2, value3, value4, value5, list.AsReadOnly(), list2.AsReadOnly(), list3.AsReadOnly(), list4.AsReadOnly(), payloadDigest); return true; } internal static bool CanonicalPluginId(string value) { return CanonicalAtom(value, 1, 128); } internal static bool CanonicalVersionOrWildcard(string value) { if (!(value == "*")) { return CanonicalAtom(value, 1, 64); } return true; } internal static bool CanonicalHashOrWildcard(string value) { if (!(value == "*")) { return IsLowerHex(value, 64); } return true; } internal static bool CanonicalAtom(string value, int minimum, int maximum) { if (value == null || value.Length < minimum || value.Length > maximum) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_') { return false; } } return true; } internal static bool IsLowerHex(string value, int length) { if (value == null || value.Length != length) { return false; } for (int i = 0; i < value.Length; i++) { if ((value[i] < '0' || value[i] > '9') && (value[i] < 'a' || value[i] > 'f')) { return false; } } return true; } internal static string Hex(byte[] bytes) { StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2); for (int i = 0; i < bytes.Length; i++) { stringBuilder.Append(bytes[i].ToString("x2", CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } internal static bool FixedTimeHexEquals(string left, string right) { if (left == null || right == null) { return false; } int num = left.Length ^ right.Length; int num2 = Math.Max(left.Length, right.Length); for (int i = 0; i < num2; i++) { char c = ((i < left.Length) ? left[i] : '\0'); char c2 = ((i < right.Length) ? right[i] : '\0'); num |= c ^ c2; } return num == 0; } private static bool TryRequired(string line, string prefix, int maximum, out string value) { value = string.Empty; if (!line.StartsWith(prefix, StringComparison.Ordinal)) { return false; } value = line.Substring(prefix.Length); return CanonicalAtom(value, 1, maximum); } private static bool TryCanonicalLong(string line, string prefix, bool allowZero, out long value) { value = 0L; if (!line.StartsWith(prefix, StringComparison.Ordinal)) { return false; } string text = line.Substring(prefix.Length); if (text.Length == 0 || text.Length > 19 || (text.Length > 1 && text[0] == '0') || !long.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out value)) { return false; } if (!allowZero) { return value > 0; } return value >= 0; } private static bool TryUnknown(string line, out PluginClassification value) { value = PluginClassification.Unknown; if (!line.StartsWith("unknown=", StringComparison.Ordinal)) { return false; } switch (line.Substring(8)) { case "Quarantined": value = PluginClassification.Quarantined; break; case "Unmanaged": value = PluginClassification.Unmanaged; break; case "Forbidden": value = PluginClassification.Forbidden; break; default: return false; } return true; } private static bool TryClassification(string text, out PluginClassification value) { value = PluginClassification.Unknown; switch (text) { case "Required": value = PluginClassification.Required; return true; case "ApprovedOptional": value = PluginClassification.ApprovedOptional; return true; case "ServerOnly": value = PluginClassification.ServerOnly; return true; case "Forbidden": value = PluginClassification.Forbidden; return true; case "Unmanaged": value = PluginClassification.Unmanaged; return true; case "AdministratorOnly": value = PluginClassification.AdministratorOnly; return true; case "Quarantined": value = PluginClassification.Quarantined; return true; default: return false; } } private static bool TryModuleScope(string text, out SentinelModuleScope scope) { scope = SentinelModuleScope.Both; switch (text) { case "Both": return true; case "Client": scope = SentinelModuleScope.Client; return true; case "Server": scope = SentinelModuleScope.Server; return true; default: return false; } } private static bool TryCanonicalInt(string text, out int value) { value = 0; if (text != null && text.Length > 0 && text.Length <= 10 && (text.Length == 1 || text[0] != '0') && int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out value)) { return value > 0; } return false; } private static bool TryCapabilities(string text, out IReadOnlyList capabilities) { capabilities = Array.Empty(); if (string.IsNullOrEmpty(text)) { return true; } string[] array = text.Split(','); if (array.Length > 64) { return false; } string text2 = null; for (int i = 0; i < array.Length; i++) { if (!CanonicalAtom(array[i], 1, 128) || (text2 != null && string.CompareOrdinal(text2, array[i]) >= 0)) { return false; } text2 = array[i]; } capabilities = Array.AsReadOnly(array); return true; } private static bool CanonicalAuthority(string value) { if (!CanonicalAtom(value, 1, 64)) { return false; } for (int i = 0; i < value.Length; i++) { if (value[i] >= 'A' && value[i] <= 'Z') { return false; } } return true; } private static bool TryCanonicalSubject(string encoded, out string subject) { subject = string.Empty; if (string.IsNullOrEmpty(encoded) || encoded.Length > 768) { return false; } try { subject = Uri.UnescapeDataString(encoded); if (string.IsNullOrEmpty(subject) || subject.Length > 256 || Uri.EscapeDataString(subject) != encoded) { return false; } for (int i = 0; i < subject.Length; i++) { if (char.IsControl(subject[i])) { return false; } } return true; } catch { subject = string.Empty; return false; } } } internal static class SentinelVersion { internal const string Current = "1.0.0"; } } namespace RunicSentinel.Contracts { public enum PluginClassification { Unknown, Required, ApprovedOptional, ServerOnly, Forbidden, Unmanaged, AdministratorOnly, Quarantined } public enum AdmissionDisposition { Unavailable, Allow, Quarantine, Deny } public enum FindingConfidence { Informational, Low, Moderate, High, VeryHigh, Conclusive } public enum EnforcementAction { Log, Validate, Cancel, Warn, Disconnect, Quarantine, Ban } public sealed class AttestedPlugin { public const int MaximumRelations = 64; public const int MaximumInspectedRelations = 256; public string Id { get; } public string Version { get; } public string Sha256 { get; } public IReadOnlyList Dependencies { get; } public IReadOnlyList Capabilities { get; } public AttestedPlugin(string id, string version, string sha256, IEnumerable dependencies, IEnumerable capabilities) { Id = SecurityContractValidation.RequireAtom(id, 1, 128, "id"); Version = SecurityContractValidation.RequireAtom(version, 1, 64, "version"); Sha256 = SecurityContractValidation.RequireLowerHex(sha256, 64, "sha256"); Dependencies = SecurityContractValidation.CopyAtoms(dependencies, 64, 256, "dependencies"); Capabilities = SecurityContractValidation.CopyAtoms(capabilities, 64, 256, "capabilities"); } } public sealed class AttestationSnapshot { public const int MaximumPlugins = 512; public string Digest { get; } public IReadOnlyList Plugins { get; } public long CapturedUnixSeconds { get; } public AttestationSnapshot(string digest, IEnumerable plugins, long capturedUnixSeconds) { Digest = SecurityContractValidation.RequireLowerHex(digest, 64, "digest"); if (capturedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("capturedUnixSeconds"); } List list = new List(); if (plugins != null) { foreach (AttestedPlugin plugin in plugins) { if (list.Count >= 512) { throw new ArgumentOutOfRangeException("plugins"); } list.Add(plugin ?? throw new ArgumentException("Attested plugins cannot be null.", "plugins")); } } Plugins = list.AsReadOnly(); CapturedUnixSeconds = capturedUnixSeconds; } } public sealed class AdmissionFinding { public string Rule { get; } public string PluginId { get; } public string Detail { get; } public FindingConfidence Confidence { get; } public AdmissionFinding(string rule, string pluginId, string detail, FindingConfidence confidence) { Rule = SecurityContractValidation.RequireSafeText(rule, 1, 128, "rule"); PluginId = (string.IsNullOrEmpty(pluginId) ? string.Empty : SecurityContractValidation.RequireAtom(pluginId, 1, 128, "pluginId")); Detail = SecurityContractValidation.RequireSafeText(detail, 1, 512, "detail"); if (!SecurityContractValidation.IsConfidence(confidence)) { throw new ArgumentOutOfRangeException("confidence"); } Confidence = confidence; } } public sealed class AdmissionDecision { public AdmissionDisposition Disposition { get; } public long PolicySequence { get; } public string PolicyProfile { get; } public IReadOnlyList Findings { get; } public AdmissionDecision(AdmissionDisposition disposition, long policySequence, string policyProfile, IEnumerable findings) { if (!SecurityContractValidation.IsDisposition(disposition)) { throw new ArgumentOutOfRangeException("disposition"); } if (policySequence < 0) { throw new ArgumentOutOfRangeException("policySequence"); } Disposition = disposition; PolicySequence = policySequence; PolicyProfile = (string.IsNullOrEmpty(policyProfile) ? string.Empty : SecurityContractValidation.RequireAtom(policyProfile, 1, 64, "policyProfile")); List list = new List(); if (findings != null) { foreach (AdmissionFinding finding in findings) { if (list.Count >= 4096) { throw new ArgumentOutOfRangeException("findings"); } list.Add(finding ?? throw new ArgumentException("Findings cannot contain null.", "findings")); } } Findings = list.AsReadOnly(); } } public sealed class SecurityEvidence { public long Sequence { get; } public long UnixSeconds { get; } public string ProviderModuleId { get; } public string Actor { get; } public string Rule { get; } public string CorrelationId { get; } public FindingConfidence Confidence { get; } public EnforcementAction RequestedAction { get; } public EnforcementAction EffectiveAction { get; } public long PolicySequence { get; } public string Detail { get; } public SecurityEvidence(long sequence, long unixSeconds, string providerModuleId, string actor, string rule, string correlationId, FindingConfidence confidence, EnforcementAction requestedAction, EnforcementAction effectiveAction, long policySequence, string detail) { if (sequence <= 0) { throw new ArgumentOutOfRangeException("sequence"); } if (unixSeconds < 0) { throw new ArgumentOutOfRangeException("unixSeconds"); } ProviderModuleId = RunicIdentifier.Require(providerModuleId, "providerModuleId"); Actor = SecurityContractValidation.RequireSafeText(actor, 1, 128, "actor"); Rule = SecurityContractValidation.RequireSafeText(rule, 1, 128, "rule"); CorrelationId = SecurityContractValidation.RequireSafeText(correlationId, 1, 128, "correlationId"); Detail = SecurityContractValidation.RequireSafeText(detail, 1, 512, "detail"); if (!SecurityContractValidation.IsConfidence(confidence)) { throw new ArgumentOutOfRangeException("confidence"); } if (!SecurityContractValidation.IsAction(requestedAction)) { throw new ArgumentOutOfRangeException("requestedAction"); } if (!SecurityContractValidation.IsAction(effectiveAction)) { throw new ArgumentOutOfRangeException("effectiveAction"); } if (policySequence < 0) { throw new ArgumentOutOfRangeException("policySequence"); } Sequence = sequence; UnixSeconds = unixSeconds; Confidence = confidence; RequestedAction = requestedAction; EffectiveAction = effectiveAction; PolicySequence = policySequence; } } public sealed class EvidenceProviderStatus { public string ProviderModuleId { get; } public bool Active { get; } public int BufferedEntries { get; } public long AcceptedEntries { get; } public long DroppedEntries { get; } public EvidenceProviderStatus(string providerModuleId, bool active, int bufferedEntries, long acceptedEntries, long droppedEntries) { ProviderModuleId = RunicIdentifier.Require(providerModuleId, "providerModuleId"); if (bufferedEntries < 0 || acceptedEntries < 0 || droppedEntries < 0) { throw new ArgumentOutOfRangeException("bufferedEntries"); } Active = active; BufferedEntries = bufferedEntries; AcceptedEntries = acceptedEntries; DroppedEntries = droppedEntries; } } public sealed class EvidenceReadSnapshot { public long NewestSequence { get; } public long PolicySequence { get; } public IReadOnlyList Entries { get; } public IReadOnlyList Providers { get; } public EvidenceReadSnapshot(long newestSequence, long policySequence, IEnumerable entries, IEnumerable providers) { if (newestSequence < 0 || policySequence < 0) { throw new ArgumentOutOfRangeException("newestSequence"); } NewestSequence = newestSequence; PolicySequence = policySequence; Entries = Copy(entries, 256, "entries"); Providers = Copy(providers, 32, "providers"); } private static IReadOnlyList Copy(IEnumerable source, int maximum, string name) where T : class { List list = new List(); if (source != null) { foreach (T item in source) { if (list.Count >= maximum) { throw new ArgumentOutOfRangeException(name); } list.Add(item ?? throw new ArgumentException("Snapshot values cannot be null.", name)); } } return list.AsReadOnly(); } } public interface ISentinelAttestationService { bool ProvidesClientAuthenticityProof { get; } bool TryGetCurrent(out AttestationSnapshot snapshot, out string status); bool TryComputeNonceBinding(string nonceHex, out string bindingHex, out string status); } public interface ISentinelAdmissionService { bool PolicyReady { get; } bool AuthoritativeTransportReady { get; } long PolicySequence { get; } string PolicyProfile { get; } AdmissionDecision Evaluate(AttestationSnapshot snapshot, string role); } public interface ISentinelEvidenceSink { bool TryAppend(string actor, string rule, string correlationId, FindingConfidence confidence, EnforcementAction requestedAction, string detail, out SecurityEvidence accepted); } public interface ISentinelEvidenceProviderLease : IDisposable { string ProviderModuleId { get; } bool IsActive { get; } ISentinelEvidenceSink Sink { get; } } public interface ISentinelEvidenceService { ISentinelEvidenceProviderLease RegisterProvider(string providerId); EvidenceReadSnapshot ReadAfter(long sequence, int maximum); } internal static class SecurityContractValidation { internal static string RequireAtom(string value, int minimum, int maximum, string name) { if (value == null || value.Length < minimum || value.Length > maximum) { throw new ArgumentOutOfRangeException(name); } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_') { throw new ArgumentException("Value is not a canonical atom.", name); } } return value; } internal static string RequireLowerHex(string value, int length, string name) { if (value == null || value.Length != length) { throw new ArgumentOutOfRangeException(name); } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { throw new ArgumentException("Value is not canonical lowercase hexadecimal.", name); } } return value; } internal static string RequireSafeText(string value, int minimum, int maximum, string name) { if (value == null || value.Length < minimum || value.Length > maximum) { throw new ArgumentOutOfRangeException(name); } foreach (char c in value) { UnicodeCategory unicodeCategory = char.GetUnicodeCategory(c); if (char.IsControl(c) || unicodeCategory == UnicodeCategory.Format || unicodeCategory == UnicodeCategory.Surrogate) { throw new ArgumentException("Text contains unsafe control or formatting characters.", name); } } return value; } internal static IReadOnlyList CopyAtoms(IEnumerable values, int maximumUnique, int maximumInspected, string name) { SortedSet sortedSet = new SortedSet(StringComparer.Ordinal); int num = 0; if (values != null) { foreach (string value in values) { if (++num > maximumInspected) { throw new ArgumentOutOfRangeException(name); } string item = RequireAtom(value, 1, 128, name); if (!sortedSet.Add(item)) { throw new ArgumentException("Duplicate relation.", name); } if (sortedSet.Count > maximumUnique) { throw new ArgumentOutOfRangeException(name); } } } return new List(sortedSet).AsReadOnly(); } internal static bool IsClassification(PluginClassification value) { if (value >= PluginClassification.Required) { return value <= PluginClassification.Quarantined; } return false; } internal static bool IsDisposition(AdmissionDisposition value) { if (value >= AdmissionDisposition.Unavailable) { return value <= AdmissionDisposition.Deny; } return false; } internal static bool IsConfidence(FindingConfidence value) { if (value >= FindingConfidence.Informational) { return value <= FindingConfidence.Conclusive; } return false; } internal static bool IsAction(EnforcementAction value) { if (value >= EnforcementAction.Log) { return value <= EnforcementAction.Ban; } return false; } } public static class SentinelCapabilityIds { public const string ProtocolVersion = "3.0"; public const string Admission = "security.admission"; public const string Attestation = "security.attest"; public const string Evidence = "security.evidence"; public const string Roles = "security.roles"; public const string Enforcement = "security.enforcement"; public const string RuntimeIntegrity = "security.runtime-integrity"; public static IReadOnlyList Published { get; } = Array.AsReadOnly(new string[6] { "security.admission", "security.attest", "security.enforcement", "security.evidence", "security.roles", "security.runtime-integrity" }); } } namespace RunicSentinel.Api { public static class SentinelIntegrationApi { private static readonly object Gate = new object(); private static SentinelEnforcementRuntime _service; internal static void Attach(SentinelEnforcementRuntime service) { lock (Gate) { _service = service; } } internal static void Detach(SentinelEnforcementRuntime service) { lock (Gate) { if (_service == service) { _service = null; } } } public static bool ReportRejectedServerRequest(string sourceModuleId, long peerId, string actor, string rule, string correlationId, int confidence, string detail) { if (confidence < 0 || confidence > 5) { return false; } SentinelEnforcementRuntime service; lock (Gate) { service = _service; } return service?.ReportRejectedServerRequest(sourceModuleId, peerId, actor, rule, correlationId, (FindingConfidence)confidence, detail) ?? false; } } }