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;