Code/Endpoints/NetworkStorageRuntimeSecurityConfigLoad.cs

This class manages the loading and validation of runtime security configuration. It fetches configuration from a remote endpoint, verifies its integrity using cryptographic signatures and key pinning, and falls back to internally synced settings if the fetch fails or the configuration is untrusted.

Http CallsExternal DownloadNetworking
🌐 /security-config/{ProjectId}
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;

namespace Sandbox;

public static partial class NetworkStorage
{
	private const int SecurityConfigRetryDelaySeconds = 60;

	// A config that was received but cannot be trusted. Code is the stable error code; Message is for logs.
	internal sealed record SecurityConfigRefusal( string Code, string Message );

	private static async Task<bool> LoadRuntimeSecurityConfigAsync( string reason, bool forceRefresh )
	{
		var url = $"{ApiRoot}/security-config/{Uri.EscapeDataString( ProjectId )}";
		var generation = _securityConfigGeneration;
		if ( forceRefresh )
			url += "?refresh=1";

		try
		{
			var headers = new System.Collections.Generic.Dictionary<string, string>();
			AddRuntimeTargetHeaders( headers );
			var raw = await RequestStringAsync(url, "GET", null, headers);
			// The pin, API root or project changed while this fetch was in flight; its result must not be committed.
			if ( generation != _securityConfigGeneration ) return false;
			if ( !TryParseRuntimeSecurityConfig( raw, out var loaded, out var refusal, out var error ) )
				return refusal is not null ? RefuseSecurityConfig( reason, refusal ) : UseSyncedSecurityConfig( reason, error );

			var previousMode = _runtimeSecurityConfig?.ModeText ?? "none";
			_runtimeSecurityConfig = loaded with
			{
				Loaded = true,
				Source = forceRefresh ? "backend-refresh" : "backend",
				LoadedAt = DateTimeOffset.UtcNow,
				LastRefreshReason = reason ?? "",
				SyncedEnableAuthSessions = EnableAuthSessions,
				SyncedEnableEncryptedRequests = EnableEncryptedRequests
			};
			_securityConfigRetryAfter = default;
			EnableAuthSessions = loaded.EnableAuthSessions;
			EnableEncryptedRequests = loaded.EnableEncryptedRequests;
			Log.Info( $"[NetworkStorage] security config loaded reason={reason} {previousMode}->{_runtimeSecurityConfig.ModeText} version={loaded.ConfigVersion} ttl={loaded.TtlSeconds}s" );
			return true;
		}
		catch ( Exception ex )
		{
			if ( generation != _securityConfigGeneration ) return false;
			return UseSyncedSecurityConfig( reason, ex.Message );
		}
	}

	// Parses and authenticates a fetched config. Returns false with `refusal` when the config was received but cannot
	// be trusted, or with `error` when the response is unusable.
	private static bool TryParseRuntimeSecurityConfig( string raw, out RuntimeSecurityConfigSnapshot snapshot, out SecurityConfigRefusal refusal, out string error )
	{
		snapshot = null;
		refusal = null;
		error = "";

		if ( string.IsNullOrWhiteSpace( raw ) )
		{
			error = "empty response";
			return false;
		}

		try
		{
			using var doc = JsonDocument.Parse( raw );
			var root = doc.RootElement;
			if ( root.TryGetProperty( "ok", out var ok ) && ok.ValueKind == JsonValueKind.False )
			{
				error = "backend returned ok=false";
				return false;
			}

			var config = root.TryGetProperty( "config", out var configProp ) ? configProp : root;
			var projectId = ReadString( config, "projectId" );
			if ( !string.Equals( projectId, ProjectId, StringComparison.Ordinal ) )
			{
				error = $"project mismatch {projectId}";
				return false;
			}

			refusal = CheckSecurityConfigTrust( config, out var keyToRecord );
			if ( refusal is not null )
				return false;

			RuntimeEncryptionKey encryptionKey = null;
			if ( config.TryGetProperty( "encryption", out var encryptionProp ) )
			{
				if ( !TryReadEncryptionKey( encryptionProp, out encryptionKey, out var keyError ) )
				{
					refusal = new SecurityConfigRefusal( "ENCRYPTION_KEY_INVALID", keyError );
					return false;
				}
			}

			var settings = config.TryGetProperty( "settings", out var settingsProp ) ? settingsProp : default;
			ApplyAnalyticsRuntimeConfig( settings );
			snapshot = new RuntimeSecurityConfigSnapshot
			{
				ProjectId = projectId,
				ConfigVersion = ReadString( config, "configVersion" ),
				GeneratedAt = ReadDateTimeOffset( config, "generatedAt" ),
				TtlSeconds = ReadInt( config, "ttlSeconds", 60 ),
				EnableAuthSessions = ReadBool( settings, "enableAuthSessions", EnableAuthSessions ),
				EnableEncryptedRequests = ReadBool( settings, "enableEncryptedRequests", EnableEncryptedRequests ),
				EnablePlayerAnalytics = EnablePlayerAnalytics,
				SignaturePresent = true,
				Trusted = true,
				EncryptionKey = encryptionKey
			};
			if ( keyToRecord is not null )
				RememberTrustedSigningKey( ApiRoot, keyToRecord );
			return true;
		}
		catch ( Exception ex )
		{
			error = ex.Message;
			return false;
		}
	}

	// Decides whether a signed config may be used. Returns null when trusted. A key learned on first use, or reached by an
	// announced rotation, is returned in `keyToRecord`; the caller records it only after the whole config has been accepted.
	private static SecurityConfigRefusal CheckSecurityConfigTrust( JsonElement config, out TrustedSigningKey keyToRecord )
	{
		keyToRecord = null;
		if ( !NetworkStorageSecuritySignature.TryReadSigningKey( config, out var modulus, out var exponent ) )
			return new SecurityConfigRefusal( "ENCRYPTION_CONFIG_UNVERIFIED", "security config has no usable signing key" );

		// The config is verified with the key it carries. A pin, or the key trusted for this API root, must name that key.
		var embedded = new TrustedSigningKey( NetworkStorageRsaKey.Fingerprint( modulus, exponent ), modulus, exponent );
		var rotatedFrom = "";
		if ( SecuritySigningKeyPin.Length > 0 )
		{
			if ( embedded.Fingerprint != SecuritySigningKeyPin )
				return new SecurityConfigRefusal( "ENCRYPTION_KEY_PIN_MISMATCH", "security config signing key does not match the pinned fingerprint" );
		}
		else if ( !TrustedSigningKeys.TryGetValue( ApiRoot, out var stored ) )
		{
			keyToRecord = embedded;
		}
		else if ( stored.Fingerprint != embedded.Fingerprint )
		{
			// Only a rotation statement signed by the trusted key may move trust. Any other replacement is refused.
			var refusal = FollowSigningKeyRotations( config, stored, embedded );
			if ( refusal is not null )
			{
				Log.Warning( $"[NetworkStorage] server signing key for {ApiRoot} changed from {stored.Fingerprint} to {embedded.Fingerprint}: {refusal.Message}. Refusing the replacement; verify the server identity before changing its pin or local trust store." );
				return refusal;
			}

			rotatedFrom = stored.Fingerprint;
			keyToRecord = embedded;
		}

		if ( !NetworkStorageSecuritySignature.VerifySecurityConfigSignature( config, embedded.Modulus, embedded.Exponent ) )
			return new SecurityConfigRefusal( "ENCRYPTION_CONFIG_UNVERIFIED", "security config signature did not verify with its signing key" );

		if ( rotatedFrom.Length > 0 )
			Log.Info( $"[NetworkStorage] server signing key for {ApiRoot} rotated from {rotatedFrom} to {embedded.Fingerprint} by an announced rotation." );
		return null;
	}

	// A received config was refused. A trusted config is kept; otherwise the refusal is recorded so encrypted calls stop
	// with its code. Synced settings are the mode baseline only and never grant trust.
	private static bool RefuseSecurityConfig( string reason, SecurityConfigRefusal refusal )
	{
		var kept = _runtimeSecurityConfig?.Trusted == true;
		if ( kept )
		{
			KeepCurrentSecurityConfig( reason, refusal.Code );
		}
		else
		{
			_runtimeSecurityConfig = RuntimeSecurityConfigSnapshot.FromSyncedSettings() with
			{
				Loaded = true,
				Source = "refused",
				LoadedAt = DateTimeOffset.UtcNow,
				TtlSeconds = SecurityConfigRetryDelaySeconds,
				LastRefreshReason = reason ?? "",
				LastMismatchCode = refusal.Code,
				RefusalCode = refusal.Code,
				RefusalMessage = refusal.Message
			};
		}

		Log.Warning( $"[NetworkStorage] security config refused reason={reason} code={refusal.Code} keptTrusted={kept}: {refusal.Message}" );
		if ( NetworkStorageLogConfig.LogErrors )
			NetLog.Error( "security-config", $"{refusal.Code}: {refusal.Message}" );
		return false;
	}

	// Keeps the current trusted or refused config through a failed refresh. Its freshness is not extended; the next
	// non-forced refresh is throttled instead.
	private static void KeepCurrentSecurityConfig( string reason, string code )
	{
		_runtimeSecurityConfig = _runtimeSecurityConfig with
		{
			LastRefreshReason = reason ?? "",
			LastMismatchCode = code ?? ""
		};
		_securityConfigRetryAfter = DateTimeOffset.UtcNow.AddSeconds( SecurityConfigRetryDelaySeconds );
	}

	private static bool UseSyncedSecurityConfig( string reason, string error )
	{
		var isEndpointMissing = (error ?? "").Contains( "404", StringComparison.OrdinalIgnoreCase )
			|| (error ?? "").Contains( "Not Found", StringComparison.OrdinalIgnoreCase );

		if ( _runtimeSecurityConfig?.Trusted == true || !string.IsNullOrEmpty( _runtimeSecurityConfig?.RefusalCode ) )
		{
			KeepCurrentSecurityConfig( reason, error );
			Log.Warning( $"[NetworkStorage] security config unavailable reason={reason} error={error}; keeping {_runtimeSecurityConfig.Source}" );
			_securityConfigUnavailableLogged = true;
			return false;
		}

		_runtimeSecurityConfig = RuntimeSecurityConfigSnapshot.FromSyncedSettings() with
		{
			Loaded = true,
			Source = isEndpointMissing ? "backend-missing-fallback" : "synced-fallback",
			LoadedAt = DateTimeOffset.UtcNow,
			TtlSeconds = isEndpointMissing ? 10 : 60,
			LastRefreshReason = reason ?? "",
			LastMismatchCode = error ?? ""
		};

		if ( isEndpointMissing )
		{
			if ( !_securityConfigUnavailableLogged )
				Log.Info( $"[NetworkStorage] security config endpoint unavailable; using synced config authSessions={EnableAuthSessions} encryptedRequests={EnableEncryptedRequests}" );
		}
		else
		{
			Log.Warning( $"[NetworkStorage] security config unavailable reason={reason} error={error}; using synced config authSessions={EnableAuthSessions} encryptedRequests={EnableEncryptedRequests}" );
		}

		_securityConfigUnavailableLogged = true;
		return false;
	}
}