using System.Reflection; using FinalFactory.Rendezvous.Contracts; using FinalFactory.Rendezvous.Server.Provisioning; namespace FinalFactory.Rendezvous.Tests.Provisioning; public sealed class ProvisioningSecurityTests { [Fact] public void StartupFailsClearlyWhenKeyMaterialIsAbsent() { ProvisioningOptions options = ProvisioningTestData.CreateOptions( ProvisioningTestData.CreateKey(secretReference: "missing-production-secret")); using DictionarySecretProvider empty = ProvisioningTestData.CreateSecrets(); ProvisioningConfigurationException exception = Assert.Throws( () => ProvisioningRuntime.Create(options, empty, ProvisioningTestData.Now)); Assert.Contains("key-1", exception.Message, StringComparison.Ordinal); Assert.DoesNotContain("missing-production-secret", exception.Message, StringComparison.Ordinal); } [Fact] public void StartupRequiresAnActivePublisherKeyForEveryEnabledPolicy() { ProvisioningOptions options = ProvisioningTestData.CreateOptions(); options.Games.Add(ProvisioningTestData.CreatePolicy("unscouted", "production")); using DictionarySecretProvider secrets = ProvisioningTestData.CreateSecrets("secret-1"); ProvisioningConfigurationException exception = Assert.Throws( () => ProvisioningRuntime.Create(options, secrets, ProvisioningTestData.Now)); Assert.Contains("unscouted/production", exception.Message, StringComparison.Ordinal); Assert.Contains("key", exception.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public void SecretMaterialCredentialsAndKeysAreRedactedFromDiagnostics() { byte[] knownSecret = Enumerable.Range(1, 32).Select(static value => (byte)value).ToArray(); string encodedSecret = Convert.ToBase64String(knownSecret); using SecretMaterial material = new(knownSecret); using DictionarySecretProvider secrets = new(new Dictionary { ["secret-1"] = knownSecret, }); using SigningKeyRing keys = SigningKeyRing.Create( [ProvisioningTestData.CreateKey()], secrets); PrincipalCredentialService service = new( "final-factory-rendezvous", "rendezvous-service", TimeSpan.FromSeconds(30), keys); string token = service.Issue( ProvisioningTestData.CreateDedicatedPublisher(), ProvisioningTestData.Now); CredentialValidationResult result = service.Validate(token, ProvisioningTestData.Now); string diagnostics = string.Join( '|', material, secrets, keys, service, result); Assert.DoesNotContain(encodedSecret, diagnostics, StringComparison.Ordinal); Assert.DoesNotContain(token, diagnostics, StringComparison.Ordinal); Assert.Contains("redacted", diagnostics, StringComparison.OrdinalIgnoreCase); } [Fact] public void PublicClientAndContractSurfacesContainNoProvisioningSecrets() { Type[] publicTypes = typeof(GameId).Assembly.GetExportedTypes() .Concat(Assembly.Load("FinalFactory.Rendezvous.Client").GetExportedTypes()) .ToArray(); string[] forbiddenTerms = [ "GameSecret", "SigningKey", "KeyMaterial", "PublisherCredential", "SecretProvider", ]; foreach (Type type in publicTypes) { IEnumerable names = type .GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) .Select(static member => member.Name) .Append(type.Name); Assert.DoesNotContain(names, name => forbiddenTerms.Any(term => name.Contains(term, StringComparison.OrdinalIgnoreCase))); } } }