diff --git a/V2/CoseSign1.Factories.Tests/IndirectSignatureFactoryTests.cs b/V2/CoseSign1.Factories.Tests/IndirectSignatureFactoryTests.cs index 06b3163b..5a1ba69b 100644 --- a/V2/CoseSign1.Factories.Tests/IndirectSignatureFactoryTests.cs +++ b/V2/CoseSign1.Factories.Tests/IndirectSignatureFactoryTests.cs @@ -946,6 +946,82 @@ public async Task CreateCoseSign1MessageBytesAsync_VerifiesSignatureBeforeReturn Assert.That(result, Is.Not.Empty); } + /// + /// Validates that a single instance can safely produce + /// indirect signatures from multiple threads concurrently without throwing + /// . + /// Regression test for https://github.com/microsoft/CoseSignTool/issues/191. + /// + [Test] + public void ConcurrentHashComputationShouldNotThrow() + { + // Arrange — one shared factory, many threads + Mock> mockSigningService = CreateMockSigningService(); + mockSigningService + .Setup(s => s.GetCoseSigner(It.IsAny())) + .Returns(CreateMockCoseSigner); + + IndirectSignatureFactory factory = new(mockSigningService.Object); + int degreeOfParallelism = Environment.ProcessorCount * 2; + int iterationsPerThread = 20; + + // Act — hammer the factory from many threads at once + Assert.DoesNotThrow(() => + { + Parallel.For(0, degreeOfParallelism * iterationsPerThread, new ParallelOptions { MaxDegreeOfParallelism = degreeOfParallelism }, i => + { + byte[] payload = new byte[128]; + Random.Shared.NextBytes(payload); + + byte[] result = factory.CreateCoseSign1MessageBytes( + payload, + "application/test.concurrent"); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Not.Empty); + }); + }); + } + + /// + /// Validates that the async indirect signature creation path is also safe under + /// concurrent usage from multiple threads. + /// Regression test for https://github.com/microsoft/CoseSignTool/issues/191. + /// + [Test] + public async Task ConcurrentAsyncHashComputationShouldNotThrow() + { + // Arrange — one shared factory, many concurrent tasks + Mock> mockSigningService = CreateMockSigningService(); + mockSigningService + .Setup(s => s.GetCoseSigner(It.IsAny())) + .Returns(CreateMockCoseSigner); + + IndirectSignatureFactory factory = new(mockSigningService.Object); + int concurrentTasks = Environment.ProcessorCount * 4; + + // Act — launch many concurrent async operations + Task[] tasks = new Task[concurrentTasks]; + for (int i = 0; i < concurrentTasks; i++) + { + tasks[i] = Task.Run(async () => + { + byte[] payload = new byte[128]; + Random.Shared.NextBytes(payload); + + byte[] result = await factory.CreateCoseSign1MessageBytesAsync( + payload, + "application/test.concurrent.async"); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Not.Empty); + }); + } + + // Assert — all tasks complete without CryptographicException + Assert.DoesNotThrowAsync(async () => await Task.WhenAll(tasks)); + } + private CoseSigner CreateMockCoseSigner() { // Create a real CoseSigner with RSA key for testing