diff --git a/CoseSign1.Tests/DetachedSignatureFactoryTests.cs b/CoseSign1.Tests/DetachedSignatureFactoryTests.cs index 297549f7..8217f1dc 100644 --- a/CoseSign1.Tests/DetachedSignatureFactoryTests.cs +++ b/CoseSign1.Tests/DetachedSignatureFactoryTests.cs @@ -3,6 +3,8 @@ namespace CoseSign1.Tests; +using System.Runtime.Intrinsics.Arm; + /// /// Class for Testing Methods of /// @@ -74,6 +76,49 @@ public async Task TestCreateDetachedSignatureAsync() memStream.Seek(0, SeekOrigin.Begin); } + [Test] + public async Task TestCreateDetachedSignatureHashProvidedAsync() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateDetachedSignatureHashProvidedAsync)); + using DetachedSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + using HashAlgorithm hasher = CoseSign1MessageDetachedSignatureExtensions.CreateHashAlgorithmFromName(factory.HashAlgorithmName) + ?? throw new Exception($"Failed to get hash algorithm from {nameof(CoseSign1MessageDetachedSignatureExtensions.CreateHashAlgorithmFromName)}"); + byte[] hash = hasher!.ComputeHash(randomBytes); + using MemoryStream hashStream = new(hash); + + // test the sync method + Assert.Throws(() => factory.CreateDetachedSignature(hash, coseSigningKeyProvider, string.Empty)); + CoseSign1Message detachedSignature = factory.CreateDetachedSignature(hash, coseSigningKeyProvider, "application/test.payload", payloadHashed: true); + detachedSignature.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + detachedSignature.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); + detachedSignature.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.Throws(() => factory.CreateDetachedSignature(hashStream, coseSigningKeyProvider, string.Empty)); + hashStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message detachedSignature2 = factory.CreateDetachedSignature(hashStream, coseSigningKeyProvider, "application/test.payload", payloadHashed: true); + detachedSignature2.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + detachedSignature2.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); + detachedSignature2.SignatureMatches(randomBytes).Should().BeTrue(); + hashStream.Seek(0, SeekOrigin.Begin); + + // test the async methods + Assert.ThrowsAsync(() => factory.CreateDetachedSignatureAsync(hash, coseSigningKeyProvider, string.Empty)); + CoseSign1Message detachedSignature3 = await factory.CreateDetachedSignatureAsync(hash, coseSigningKeyProvider, "application/test.payload", payloadHashed: true); + detachedSignature3.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + detachedSignature3.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); + detachedSignature3.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.ThrowsAsync(() => factory.CreateDetachedSignatureAsync(hashStream, coseSigningKeyProvider, string.Empty)); + hashStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message detachedSignature4 = await factory.CreateDetachedSignatureAsync(hashStream, coseSigningKeyProvider, "application/test.payload", payloadHashed: true); + detachedSignature4.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + detachedSignature4.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); + detachedSignature4.SignatureMatches(randomBytes).Should().BeTrue(); + hashStream.Seek(0, SeekOrigin.Begin); + } + [Test] public async Task TestCreateDetachedSignatureBytesAsync() { @@ -114,6 +159,49 @@ public async Task TestCreateDetachedSignatureBytesAsync() detachedSignature4.SignatureMatches(memStream).Should().BeTrue(); } + [Test] + public async Task TestCreateDetachedSignatureBytesHashProvidedAsync() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateDetachedSignatureBytesHashProvidedAsync)); + using DetachedSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + using HashAlgorithm hasher = CoseSign1MessageDetachedSignatureExtensions.CreateHashAlgorithmFromName(factory.HashAlgorithmName) + ?? throw new Exception($"Failed to get hash algorithm from {nameof(CoseSign1MessageDetachedSignatureExtensions.CreateHashAlgorithmFromName)}"); + byte[] hash = hasher!.ComputeHash(randomBytes); + using MemoryStream hashStream = new(hash); + + // test the sync method + Assert.Throws(() => factory.CreateDetachedSignatureBytes(hash, coseSigningKeyProvider, string.Empty)); + CoseSign1Message detachedSignature = CoseMessage.DecodeSign1(factory.CreateDetachedSignatureBytes(hash, coseSigningKeyProvider, "application/test.payload", payloadHashed: true).ToArray()); + detachedSignature.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + detachedSignature.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); + detachedSignature.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.Throws(() => factory.CreateDetachedSignatureBytes(hashStream, coseSigningKeyProvider, string.Empty)); + hashStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message detachedSignature2 = CoseMessage.DecodeSign1(factory.CreateDetachedSignatureBytes(hashStream, coseSigningKeyProvider, "application/test.payload", payloadHashed: true).ToArray()); + detachedSignature2.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + detachedSignature2.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); + detachedSignature2.SignatureMatches(randomBytes).Should().BeTrue(); + hashStream.Seek(0, SeekOrigin.Begin); + + // test the async methods + Assert.ThrowsAsync(() => factory.CreateDetachedSignatureBytesAsync(hash, coseSigningKeyProvider, string.Empty)); + CoseSign1Message detachedSignature3 = CoseMessage.DecodeSign1((await factory.CreateDetachedSignatureBytesAsync(hash, coseSigningKeyProvider, "application/test.payload", payloadHashed: true)).ToArray()); + detachedSignature3.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + detachedSignature3.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); + detachedSignature3.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.ThrowsAsync(() => factory.CreateDetachedSignatureBytesAsync(hashStream, coseSigningKeyProvider, string.Empty)); + hashStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message detachedSignature4 = CoseMessage.DecodeSign1((await factory.CreateDetachedSignatureBytesAsync(hashStream, coseSigningKeyProvider, "application/test.payload", payloadHashed: true)).ToArray()); + detachedSignature4.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + detachedSignature4.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); + hashStream.Seek(0, SeekOrigin.Begin); + detachedSignature4.SignatureMatches(randomBytes).Should().BeTrue(); + } + [Test] public void TestCreateDetachedSignatureMd5() { @@ -130,6 +218,29 @@ public void TestCreateDetachedSignatureMd5() detachedSignature.SignatureMatches(randomBytes).Should().BeTrue(); } + [Test] + public void TestCreateDetachedSignatureMd5HashProvided() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateDetachedSignatureMd5)); + using DetachedSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + using HashAlgorithm hasher = CoseSign1MessageDetachedSignatureExtensions.CreateHashAlgorithmFromName(HashAlgorithmName.MD5) + ?? throw new Exception($"Failed to get hash algorithm from {nameof(CoseSign1MessageDetachedSignatureExtensions.CreateHashAlgorithmFromName)}"); + byte[] hash = hasher!.ComputeHash(randomBytes); + + // test the sync method + Assert.Throws(() => factory.CreateDetachedSignature(hash, coseSigningKeyProvider, string.Empty)); + CoseSign1Message detachedSignature = CoseMessage.DecodeSign1(factory.CreateDetachedSignatureBytes(hash, coseSigningKeyProvider, "application/test.payload", payloadHashed: true).ToArray()); + detachedSignature.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + detachedSignature.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-md5"); + detachedSignature.SignatureMatches(randomBytes).Should().BeTrue(); + + // test unknown hash length + // test the sync method + Assert.Throws(() => factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", payloadHashed: true)); + } + [Test] public void TestCreateDetachedSignatureAlreadyProvided() { diff --git a/CoseSign1/DetachedSignatureFactory.cs b/CoseSign1/DetachedSignatureFactory.cs index cfdb4775..8b429bb7 100644 --- a/CoseSign1/DetachedSignatureFactory.cs +++ b/CoseSign1/DetachedSignatureFactory.cs @@ -71,11 +71,13 @@ public DetachedSignatureFactory(HashAlgorithmName hashAlgorithmName, ICoseSign1M public CoseSign1Message CreateDetachedSignature( ReadOnlyMemory payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: payload); + string contentType, + bool payloadHashed = false) => (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: payload, + payloadHashed: payloadHashed); /// /// Creates a detached signature of the specified payload returned as a following the rules in this class description. @@ -88,12 +90,14 @@ public CoseSign1Message CreateDetachedSignature( public Task CreateDetachedSignatureAsync( ReadOnlyMemory payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: payload)); + string contentType, + bool payloadHashed = false) => Task.FromResult( + (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: payload, + payloadHashed: payloadHashed)); /// /// Creates a detached signature of the specified payload returned as a following the rules in this class description. @@ -106,11 +110,13 @@ public Task CreateDetachedSignatureAsync( public CoseSign1Message CreateDetachedSignature( Stream payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: payload); + string contentType, + bool payloadHashed = false) => (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: payload, + payloadHashed: payloadHashed); /// /// Creates a detached signature of the specified payload returned as a following the rules in this class description. @@ -123,12 +129,14 @@ public CoseSign1Message CreateDetachedSignature( public Task CreateDetachedSignatureAsync( Stream payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: payload)); + string contentType, + bool payloadHashed = false) => Task.FromResult( + (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: payload, + payloadHashed: payloadHashed)); /// /// Creates a detached signature of the specified payload returned as a following the rules in this class description. @@ -141,11 +149,13 @@ public Task CreateDetachedSignatureAsync( public ReadOnlyMemory CreateDetachedSignatureBytes( ReadOnlyMemory payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: payload); + string contentType, + bool payloadHashed = false) => (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: payload, + payloadHashed: payloadHashed); /// /// Creates a detached signature of the specified payload returned as a following the rules in this class description. @@ -158,12 +168,14 @@ public ReadOnlyMemory CreateDetachedSignatureBytes( public Task> CreateDetachedSignatureBytesAsync( ReadOnlyMemory payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: payload)); + string contentType, + bool payloadHashed = false) => Task.FromResult( + (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: payload, + payloadHashed: payloadHashed)); /// /// Creates a detached signature of the specified payload returned as a following the rules in this class description. @@ -176,11 +188,13 @@ public Task> CreateDetachedSignatureBytesAsync( public ReadOnlyMemory CreateDetachedSignatureBytes( Stream payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: payload); + string contentType, + bool payloadHashed = false) => (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: payload, + payloadHashed: payloadHashed); /// /// Creates a detached signature of the specified payload returned as a following the rules in this class description. @@ -193,12 +207,14 @@ public ReadOnlyMemory CreateDetachedSignatureBytes( public Task> CreateDetachedSignatureBytesAsync( Stream payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: payload)); + string contentType, + bool payloadHashed = false) => Task.FromResult( + (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: payload, + payloadHashed: payloadHashed)); /// /// Does the heavy lifting for this class in computing the hash and creating the correct representation of the CoseSign1Message base on input. @@ -215,7 +231,8 @@ private object CreateDetachedSignatureWithChecksInternal( ICoseSigningKeyProvider signingKeyProvider, string contentType, Stream? streamPayload = null, - ReadOnlyMemory? bytePayload = null) + ReadOnlyMemory? bytePayload = null, + bool payloadHashed = false) { if (string.IsNullOrWhiteSpace(contentType)) { @@ -228,22 +245,57 @@ private object CreateDetachedSignatureWithChecksInternal( throw new ArgumentNullException("payload", "Either streamPayload or bytePayload must be specified, but not both at the same time, or both cannot be null"); } - ReadOnlyMemory hash = streamPayload != null - ? InternalHashAlgorithm.ComputeHash(streamPayload) - : InternalHashAlgorithm.ComputeHash(bytePayload!.Value.ToArray()); + ReadOnlyMemory hash; + string extendedContentType; + if (!payloadHashed) + { + hash = streamPayload != null + ? InternalHashAlgorithm.ComputeHash(streamPayload) + : InternalHashAlgorithm.ComputeHash(bytePayload!.Value.ToArray()); + extendedContentType = ExtendContentType(contentType); + } else + { + hash = streamPayload != null + ? streamPayload.GetBytes() + : bytePayload!.Value.ToArray(); + try + { + HashAlgorithmName algoName = SizeToAlgorithm[hash.Length]; + extendedContentType = ExtendContentType(contentType, algoName); + } + catch (KeyNotFoundException e) + { + throw new ArgumentException($"{nameof(payloadHashed)} is set, but payload size does not correspond to any known hash sizes in {nameof(HashAlgorithmName)}", e); + } + } + + return returnBytes ? InternalMessageFactory.CreateCoseSign1MessageBytes( hash, signingKeyProvider, embedPayload: true, - contentType: ExtendContentType(contentType)) + contentType: extendedContentType) : InternalMessageFactory.CreateCoseSign1Message( hash, signingKeyProvider, embedPayload: true, - contentType: ExtendContentType(contentType)); + contentType: extendedContentType); } + /// + /// quick lookup of algorithm name based on hash size + /// + private static readonly ConcurrentDictionary SizeToAlgorithm = new( + new Dictionary() + { + { 16, HashAlgorithmName.MD5 }, + { 20, HashAlgorithmName.SHA1 }, + { 32, HashAlgorithmName.SHA256 }, + { 48, HashAlgorithmName.SHA384 }, + { 64, HashAlgorithmName.SHA512 } + }); + /// /// quick lookup map between algorithm name and mime extension /// @@ -261,10 +313,17 @@ private object CreateDetachedSignatureWithChecksInternal( /// /// The content type to append the hash value to if not already appended. /// - private string ExtendContentType(string contentType) + private string ExtendContentType(string contentType) => ExtendContentType(contentType, InternalHashAlgorithmName); + + /// + /// Method which produces a mime type extension based on the given content type and hash algorithm name. + /// + /// The content type to append the hash value to if not already appended. + /// + private string ExtendContentType(string contentType, HashAlgorithmName algorithmName) { // extract from the string cache to keep string allocations down. - string extensionMapping = MimeExtensionMap.GetOrAdd(InternalHashAlgorithmName.Name, (name) => $"+hash-{name.ToLowerInvariant()}"); + string extensionMapping = MimeExtensionMap.GetOrAdd(algorithmName.Name, (name) => $"+hash-{name.ToLowerInvariant()}"); // only add the extension mapping, if it's not already present within the contentType bool alreadyPresent = contentType.IndexOf("+hash-", StringComparison.InvariantCultureIgnoreCase) != -1;