diff --git a/CoseHandler.Tests/CoseX509ThumbprintTests.cs b/CoseHandler.Tests/CoseX509ThumbprintTests.cs
index a466e231..76d51113 100644
--- a/CoseHandler.Tests/CoseX509ThumbprintTests.cs
+++ b/CoseHandler.Tests/CoseX509ThumbprintTests.cs
@@ -50,4 +50,67 @@ public void ConstructThumbprintWithUnsupportedAlgo()
{
_ = new CoseX509Thumprint(SelfSignedCert1, HashAlgorithmName.SHA3_512);
}
+
+ ///
+ /// Validates that a single instance can safely call
+ /// from multiple threads concurrently without
+ /// throwing .
+ /// Regression test for https://github.com/microsoft/CoseSignTool/issues/191.
+ ///
+ [TestMethod]
+ public void ConcurrentMatchShouldNotThrow()
+ {
+ // Arrange — one shared thumbprint, many threads calling Match
+ CoseX509Thumprint thumbprint = new(SelfSignedCert1);
+ int degreeOfParallelism = Environment.ProcessorCount * 2;
+ int iterationsPerThread = 50;
+
+ // Act & Assert — hammer Match() from many threads at once
+ Action concurrentAction = () =>
+ {
+ Parallel.For(0, degreeOfParallelism * iterationsPerThread, new ParallelOptions { MaxDegreeOfParallelism = degreeOfParallelism }, i =>
+ {
+ // Alternate between matching and non-matching certs
+ if (i % 2 == 0)
+ {
+ thumbprint.Match(SelfSignedCert1).Should().BeTrue();
+ }
+ else
+ {
+ thumbprint.Match(SelfSignedCert2).Should().BeFalse();
+ }
+ });
+ };
+
+ concurrentAction.Should().NotThrow();
+ }
+
+ ///
+ /// Validates that concurrent construction of instances
+ /// with different hash algorithms is thread-safe.
+ /// Regression test for https://github.com/microsoft/CoseSignTool/issues/191.
+ ///
+ [TestMethod]
+ public void ConcurrentConstructionAndMatchShouldNotThrow()
+ {
+ // Arrange
+ HashAlgorithmName[] algorithms = new[] { HashAlgorithmName.SHA256, HashAlgorithmName.SHA384, HashAlgorithmName.SHA512 };
+ int degreeOfParallelism = Environment.ProcessorCount * 2;
+ int iterationsPerThread = 30;
+
+ // Act & Assert — create thumbprints and match from many threads
+ Action concurrentAction = () =>
+ {
+ Parallel.For(0, degreeOfParallelism * iterationsPerThread, new ParallelOptions { MaxDegreeOfParallelism = degreeOfParallelism }, i =>
+ {
+ HashAlgorithmName algo = algorithms[i % algorithms.Length];
+ CoseX509Thumprint thumbprint = new(SelfSignedCert1, algo);
+
+ thumbprint.Match(SelfSignedCert1).Should().BeTrue();
+ thumbprint.Match(SelfSignedCert2).Should().BeFalse();
+ });
+ };
+
+ concurrentAction.Should().NotThrow();
+ }
}
\ No newline at end of file
diff --git a/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs b/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs
index 53b4498b..5e17fce4 100644
--- a/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs
+++ b/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs
@@ -502,4 +502,81 @@ public async Task TestCreateIndirectSignatureAsyncWithoutPayloadLocation()
indirectSignature.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algo).Should().BeTrue();
algo!.Should().Be(CoseHashAlgorithm.SHA256);
}
+
+ ///
+ /// Validates that a single instance can safely produce
+ /// indirect signatures from multiple threads concurrently without throwing
+ ///
+ /// ("Concurrent operations from multiple threads on this type are not supported.").
+ /// Regression test for https://github.com/microsoft/CoseSignTool/issues/191.
+ ///
+ [Test]
+ public void ConcurrentHashComputationShouldNotThrow()
+ {
+ // Arrange — one shared factory, many threads
+ ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider();
+ using IndirectSignatureFactory factory = new();
+ int degreeOfParallelism = Environment.ProcessorCount * 2;
+ int iterationsPerThread = 20;
+
+ // Act — hammer the factory from many threads at once
+ Action concurrentAction = () =>
+ {
+ Parallel.For(0, degreeOfParallelism * iterationsPerThread, new ParallelOptions { MaxDegreeOfParallelism = degreeOfParallelism }, i =>
+ {
+ byte[] payload = new byte[128];
+ Random.Shared.NextBytes(payload);
+
+ CoseSign1Message result = factory.CreateIndirectSignature(
+ payload,
+ coseSigningKeyProvider,
+ "application/test.concurrent");
+
+ // Verify each result is valid
+ result.IsIndirectSignature().Should().BeTrue();
+ result.SignatureMatches(payload).Should().BeTrue();
+ });
+ };
+
+ // Assert — no CryptographicException from concurrent ComputeHash
+ concurrentAction.Should().NotThrow();
+ }
+
+ ///
+ /// 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
+ ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider();
+ using IndirectSignatureFactory factory = new();
+ 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);
+
+ CoseSign1Message result = await factory.CreateIndirectSignatureAsync(
+ payload,
+ coseSigningKeyProvider,
+ "application/test.concurrent.async");
+
+ // Verify each result is valid
+ result.IsIndirectSignature().Should().BeTrue();
+ result.SignatureMatches(payload).Should().BeTrue();
+ });
+ }
+
+ // Assert — all tasks complete without CryptographicException
+ Func awaitAll = async () => await Task.WhenAll(tasks);
+ await awaitAll.Should().NotThrowAsync();
+ }
}
diff --git a/CoseIndirectSignature/CoseHashV.cs b/CoseIndirectSignature/CoseHashV.cs
index 64912c35..30b137df 100644
--- a/CoseIndirectSignature/CoseHashV.cs
+++ b/CoseIndirectSignature/CoseHashV.cs
@@ -39,7 +39,7 @@ public byte[] HashValue
}
// sanity check the length of the hash against the specified algorithm to be sure we're not allowing a mismatch.
- HashAlgorithm algo = IndirectSignatureFactory.GetHashAlgorithmFromCoseHashAlgorithm(Algorithm);
+ using HashAlgorithm algo = IndirectSignatureFactory.GetHashAlgorithmFromCoseHashAlgorithm(Algorithm);
if (value.Length != (algo.HashSize / 8))
{
throw new ArgumentOutOfRangeException(nameof(value), @$"The hash value length of {value.Length} did not match the CoseHashAlgorithm {Algorithm} required length of {algo.HashSize / 8}");
diff --git a/CoseIndirectSignature/IndirectSignatureFactory.CoseHashEnvelope.cs b/CoseIndirectSignature/IndirectSignatureFactory.CoseHashEnvelope.cs
index f3428969..73f75b4a 100644
--- a/CoseIndirectSignature/IndirectSignatureFactory.CoseHashEnvelope.cs
+++ b/CoseIndirectSignature/IndirectSignatureFactory.CoseHashEnvelope.cs
@@ -39,9 +39,10 @@ private object CreateIndirectSignatureWithChecksInternalCoseHashEnvelopeFormat(
if (!payloadHashed)
{
+ using HashAlgorithm hasher = this.CreateHashAlgorithm();
hash = streamPayload != null
- ? InternalHashAlgorithm.ComputeHash(streamPayload)
- : InternalHashAlgorithm.ComputeHash(bytePayload!.Value.ToArray());
+ ? hasher.ComputeHash(streamPayload)
+ : hasher.ComputeHash(bytePayload!.Value.ToArray());
}
else
{
@@ -100,9 +101,10 @@ private async Task