diff --git a/CoseSign1.Certificates.Tests/CoseSign1MessageExtensionsTests.cs b/CoseSign1.Certificates.Tests/CoseSign1MessageExtensionsTests.cs
new file mode 100644
index 00000000..fd0ff37e
--- /dev/null
+++ b/CoseSign1.Certificates.Tests/CoseSign1MessageExtensionsTests.cs
@@ -0,0 +1,295 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Certificates.Tests;
+
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Security.Cryptography.Cose;
+using System.Security.Cryptography.X509Certificates;
+using CoseSign1.Certificates.Extensions;
+
+///
+/// Unit tests for the CoseSign1MessageExtensions methods that verify COSE_Sign1 messages with embedded certificates.
+/// Covers all overloads and edge cases.
+///
+public class CoseSign1MessageExtensionsTests
+{
+ ///
+ /// Setup method
+ ///
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ private static CoseSign1MessageFactory CoseSign1MessageFactory = new CoseSign1MessageFactory();
+ // Helper: Create a valid COSE_Sign1 message with embedded certificate and content
+ private static CoseSign1Message CreateValidCoseSign1Message(byte[] content, X509Certificate2 cert, out byte[] coseBytes, bool detached = false)
+ {
+ X509Certificate2CoseSigningKeyProvider testObjRsa = new(cert);
+ coseBytes = CoseSign1MessageFactory.CreateCoseSign1MessageBytes(content, testObjRsa, !detached).ToArray();
+ return CoseMessage.DecodeSign1(coseBytes);
+ }
+
+ ///
+ /// Helper to create a COSE_Sign1 message with NO embedded certificate (no x5c/x5t headers).
+ ///
+ private static CoseSign1Message CreateCoseSign1MessageWithoutCert(byte[] content)
+ {
+ using var rsa = RSA.Create(2048);
+
+ var coseSigner = new CoseSigner(rsa, RSASignaturePadding.Pkcs1, HashAlgorithmName.SHA256, new CoseHeaderMap(), new CoseHeaderMap());
+ var coseBytes = CoseSign1Message.SignEmbedded(content, coseSigner);
+ return CoseMessage.DecodeSign1(coseBytes);
+ }
+
+ ///
+ /// Test that VerifyEmbeddedWithCertificate returns true for a valid message with embedded certificate.
+ ///
+ [Test]
+ public void VerifyEmbeddedWithCertificate_ValidMessage_ReturnsTrue()
+ {
+ // Arrange
+ var cert = TestCertificateUtils.CreateCertificate();
+ var content = new byte[] { 1, 2, 3, 4 };
+ var msg = CreateValidCoseSign1Message(content, cert, out _);
+
+ // Act
+ bool result = msg.VerifyEmbeddedWithCertificate();
+
+ // Assert
+ Assert.That(result, Is.True, "Expected verification to succeed for valid message.");
+ }
+
+ ///
+ /// Test that VerifyEmbeddedWithCertificate returns false if message is null.
+ ///
+ [Test]
+ public void VerifyEmbeddedWithCertificate_NullMessage_ReturnsFalse()
+ {
+ // Arrange
+ CoseSign1Message? msg = null!;
+
+ // Act & Assert
+ Assert.That(msg.VerifyEmbeddedWithCertificate(), Is.False, "Expected false when message is null.");
+ }
+
+ ///
+ /// Test that VerifyEmbeddedWithCertificate returns false if no embedded certificate is present.
+ ///
+ [Test]
+ public void VerifyEmbeddedWithCertificate_NoCertificate_ReturnsFalse()
+ {
+ // Arrange
+ var content = new byte[] { 1, 2, 3, 4 };
+ var msg = CreateCoseSign1MessageWithoutCert(content);
+ // Act
+ bool result = msg.VerifyEmbeddedWithCertificate();
+ // Assert
+ Assert.That(result, Is.False, "Expected verification to fail when no certificate is present.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (byte[]) returns true for valid message and content.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_ByteArray_Valid_ReturnsTrue()
+ {
+ // Arrange
+ var cert = TestCertificateUtils.CreateCertificate();
+ var content = new byte[] { 10, 20, 30 };
+ var msg = CreateValidCoseSign1Message(content, cert, out _, detached: true); // Detached
+ // Act
+ bool result = msg.VerifyDetachedWithCertificate(content);
+ // Assert
+ Assert.That(result, Is.True, "Expected verification to succeed for valid detached message.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (byte[]) returns false if message is null.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_ByteArray_NullMessage_ReturnsFalse()
+ {
+ // Arrange
+ CoseSign1Message msg = null!;
+ var content = new byte[] { 1, 2 };
+ // Act & Assert
+ Assert.That(msg.VerifyDetachedWithCertificate(content), Is.False, "Expected false when message is null.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (byte[]) returns false if content is null.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_ByteArray_NullContent_ReturnsFalse()
+ {
+ // Arrange
+ var cert = TestCertificateUtils.CreateCertificate();
+ var content = new byte[] { 1, 2 };
+ var msg = CreateValidCoseSign1Message(content, cert, out _, detached: true); // Detached
+ // Act & Assert
+ Assert.That(msg.VerifyDetachedWithCertificate((byte[])null!), Is.False, "Expected false when detached content is null.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (ReadOnlySpan) returns true for valid message and content.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_ReadOnlySpan_Valid_ReturnsTrue()
+ {
+ // Arrange
+ var cert = TestCertificateUtils.CreateCertificate();
+ var content = new byte[] { 42, 43, 44 };
+ var msg = CreateValidCoseSign1Message(content, cert, out _, detached: true); // Detached
+ // Act
+ bool result = msg.VerifyDetachedWithCertificate(content.AsSpan());
+ // Assert
+ Assert.That(result, Is.True, "Expected verification to succeed for valid detached message.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (ReadOnlySpan) returns false if content is empty.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_ReadOnlySpan_EmptyContent_ReturnsFalse()
+ {
+ // Arrange
+ var cert = TestCertificateUtils.CreateCertificate();
+ var contentCreate = new byte[] { 99, 100, 101 };
+ var contentVerify = Array.Empty();
+ var msg = CreateValidCoseSign1Message(contentCreate, cert, out _, detached: true); // Detached
+ // Act
+ bool result = msg.VerifyDetachedWithCertificate(contentVerify.AsSpan());
+ // Assert
+ Assert.That(result, Is.False, "Expected verification to fail for empty detached content.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (Stream) returns true for valid message and content.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_Stream_Valid_ReturnsTrue()
+ {
+ // Arrange
+ var cert = TestCertificateUtils.CreateCertificate();
+ var content = new byte[] { 99, 100, 101 };
+ using var stream = new MemoryStream(content);
+ var msg = CreateValidCoseSign1Message(content, cert, out _, detached: true); // Detached
+ // Act
+ bool result = msg.VerifyDetachedWithCertificate(stream);
+ // Assert
+ Assert.That(result, Is.True, "Expected verification to succeed for valid detached message.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (Stream) returns false if message is null.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_Stream_NullMessage_ReturnsFalse()
+ {
+ // Arrange
+ CoseSign1Message msg = null!;
+ using var stream = new MemoryStream(new byte[] { 1 });
+ // Act & Assert
+ Assert.That(msg.VerifyDetachedWithCertificate(stream), Is.False, "Expected false when message is null.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (Stream) returns false if stream is null.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_Stream_NullStream_ReturnsFalse()
+ {
+ // Arrange
+ var cert = TestCertificateUtils.CreateCertificate();
+ var content = new byte[] { 99, 100, 101 };
+ var msg = CreateValidCoseSign1Message(content, cert, out _, detached: true); // Detached
+ // Act & Assert
+ Assert.That(msg.VerifyDetachedWithCertificate((Stream)null!), Is.False, "Expected false when detached stream is null.");
+ }
+
+ ///
+ /// Test that VerifyEmbeddedWithCertificate returns false for a detached message (Content is null).
+ ///
+ [Test]
+ public void VerifyEmbeddedWithCertificate_DetachedMessage_ReturnsFalse()
+ {
+ // Arrange
+ var cert = TestCertificateUtils.CreateCertificate();
+ var content = new byte[] { 1, 2, 3, 4 };
+ byte[] coseBytes;
+ // Create a detached message: pass null as content and detached=true
+ var msg = CreateValidCoseSign1Message(content, cert, out coseBytes, detached: true);
+
+ // Act
+ bool result = msg.VerifyEmbeddedWithCertificate();
+
+ // Assert
+ Assert.That(result, Is.False, "Expected verification to fail for detached message (no content).");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (byte[]) returns false if no signing certificate is present.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_ByteArray_NoCertificate_ReturnsFalse()
+ {
+ // Arrange
+ var content = new byte[] { 1, 2, 3 };
+ var msg = CreateCoseSign1MessageWithoutCert(content); // No cert in headers
+ // Act
+ bool result = msg.VerifyDetachedWithCertificate(content);
+ // Assert
+ Assert.That(result, Is.False, "Expected verification to fail when no signing certificate is present.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (ReadOnlySpan) returns false if no signing certificate is present.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_ReadOnlySpan_NoCertificate_ReturnsFalse()
+ {
+ // Arrange
+ var content = new byte[] { 1, 2, 3 };
+ var msg = CreateCoseSign1MessageWithoutCert(content); // No cert in headers
+ // Act
+ bool result = msg.VerifyDetachedWithCertificate(content.AsSpan());
+ // Assert
+ Assert.That(result, Is.False, "Expected verification to fail when no signing certificate is present.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (Stream) returns false if no signing certificate is present.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_Stream_NoCertificate_ReturnsFalse()
+ {
+ // Arrange
+ var content = new byte[] { 1, 2, 3 };
+ using var stream = new MemoryStream(content);
+ var msg = CreateCoseSign1MessageWithoutCert(content); // No cert in headers
+ // Act
+ bool result = msg.VerifyDetachedWithCertificate(stream);
+ // Assert
+ Assert.That(result, Is.False, "Expected verification to fail when no signing certificate is present.");
+ }
+
+ ///
+ /// Test that VerifyDetachedWithCertificate (Stream) returns false if no signing certificate is present or public key is null.
+ ///
+ [Test]
+ public void VerifyDetachedWithCertificate_Stream_NoCertificateOrPublicKey_ReturnsFalse()
+ {
+ // Arrange
+ var content = new byte[] { 1, 2, 3 };
+ using var stream = new MemoryStream(content);
+ var msg = CreateCoseSign1MessageWithoutCert(content); // No cert in headers
+ // Act
+ bool result = msg.VerifyDetachedWithCertificate(stream);
+ // Assert
+ Assert.That(result, Is.False, "Expected verification to fail when no signing certificate or public key is present.");
+ }
+}
diff --git a/CoseSign1.Certificates/CertificateCoseHeaderLabels.cs b/CoseSign1.Certificates/CertificateCoseHeaderLabels.cs
index 7a8692a6..76db8bb7 100644
--- a/CoseSign1.Certificates/CertificateCoseHeaderLabels.cs
+++ b/CoseSign1.Certificates/CertificateCoseHeaderLabels.cs
@@ -6,19 +6,21 @@ namespace CoseSign1.Certificates;
///
/// objects which are specific to certificate signed objects.
///
-internal class CertificateCoseHeaderLabels
+public static class CertificateCoseHeaderLabels
{
// Taken from https://www.iana.org/assignments/cose/cose.xhtml
///
/// Represents an unordered list of certificates.
///
- internal static readonly CoseHeaderLabel X5Bag = new(32);
+ public static readonly CoseHeaderLabel X5Bag = new(32);
+
///
/// Represents an ordered list (leaf first) of the certificate chain for the certificate used to sign the object.
///
- internal static readonly CoseHeaderLabel X5Chain = new(33);
+ public static readonly CoseHeaderLabel X5Chain = new(33);
+
///
/// Represents the thumbprint for the certificate used to sign the object.
///
- internal static readonly CoseHeaderLabel X5T = new(34);
+ public static readonly CoseHeaderLabel X5T = new(34);
}
diff --git a/CoseSign1.Certificates/Extensions/CoseSign1MessageExtensions.cs b/CoseSign1.Certificates/Extensions/CoseSign1MessageExtensions.cs
index a222b8d8..51625808 100644
--- a/CoseSign1.Certificates/Extensions/CoseSign1MessageExtensions.cs
+++ b/CoseSign1.Certificates/Extensions/CoseSign1MessageExtensions.cs
@@ -4,6 +4,12 @@
namespace CoseSign1.Certificates.Extensions;
using System.Diagnostics;
+using System.Formats.Cbor;
+using System.IO;
+using System.Runtime.Caching;
+using System.Security.Cryptography.Cose;
+using System.Security.Cryptography.X509Certificates;
+using CoseSign1.Certificates;
///
/// Extension methods for the objects related to certificate operations.
@@ -84,9 +90,9 @@ public static bool TryGetSigningCertificate(
// cache the certificate so we can return it faster in the future.
MemoryCache.Default.Add(cacheEntry, signingCert, new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(5) });
}
- else
- {
- Trace.TraceWarning($"Failed to find a certificate matching the x5t thumbprint of {thumbprint} in the certificate chain present in the x5chain header, unable to obtain the signing certificate from msg: {msgHashCode}");
+ else
+ {
+ Trace.TraceWarning($"Failed to find a certificate matching the x5t thumbprint of {thumbprint} in the certificate chain present in the x5chain header, unable to obtain the signing certificate from msg: {msgHashCode}");
}
}
finally
@@ -199,4 +205,174 @@ private static bool TryGetCertificateList(
return false;
}
+
+ ///
+ /// Attempts to verify the COSE_Sign1 message using the embedded signing certificate if present and the message is not detached.
+ ///
+ /// The COSE_Sign1 message to verify.
+ /// True if the unprotected headers should be allowed to contribute, false (default - more secure) otherwise.
+ /// True if the message is verified with the embedded signing certificate; false otherwise.
+ public static bool VerifyEmbeddedWithCertificate(
+ this CoseSign1Message @this,
+ bool allowUnprotected = false)
+ {
+ if(@this is null)
+ {
+ Trace.TraceWarning($"{nameof(@this)} is null and cannot be verified.");
+ return false;
+ }
+
+ AsymmetricAlgorithm? publicKey = @this.GetEmbeddedPublicKey(allowUnprotected, out bool foundCert);
+ if (!foundCert || publicKey is null)
+ {
+ return false;
+ }
+
+ // If the message is detached, we cannot verify without the payload.
+ if (@this.Content is null)
+ {
+ Trace.TraceWarning($"Message is a detached payload; cannot verify without payload.");
+ return false;
+ }
+
+ try
+ {
+ return @this.VerifyEmbedded(publicKey);
+ }
+ catch (CryptographicException ex)
+ {
+ Trace.TraceWarning($"Verification failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Attempts to verify the COSE_Sign1 message using the embedded signing certificate and the provided detached content (byte array).
+ ///
+ /// The COSE_Sign1 message to verify.
+ /// The detached content to verify against.
+ /// True if the unprotected headers should be allowed to contribute, false (default - more secure) otherwise.
+ /// True if the message is verified with the embedded signing certificate; false otherwise.
+ public static bool VerifyDetachedWithCertificate(this CoseSign1Message @this, byte[] detachedContent, bool allowUnprotected = false)
+ {
+ if(@this is null)
+ {
+ Trace.TraceWarning($"{nameof(@this)} is null and cannot be verified.");
+ return false;
+ }
+
+ if (detachedContent is null || detachedContent.Length == 0)
+ {
+ Trace.TraceWarning($"Detached content is null or empty; cannot verify.");
+ return false;
+ }
+
+ AsymmetricAlgorithm? publicKey = @this.GetEmbeddedPublicKey(allowUnprotected, out bool foundCert);
+ if (!foundCert || publicKey is null)
+ {
+ return false;
+ }
+ try
+ {
+ return @this.VerifyDetached(publicKey, detachedContent);
+ }
+ catch (CryptographicException ex)
+ {
+ Trace.TraceWarning($"Verification failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Attempts to verify the COSE_Sign1 message using the embedded signing certificate and the provided detached content (ReadOnlySpan).
+ ///
+ /// The COSE_Sign1 message to verify.
+ /// The detached content to verify against.
+ /// True if the unprotected headers should be allowed to contribute, false (default - more secure) otherwise.
+ /// True if the message is verified with the embedded signing certificate; false otherwise.
+ public static bool VerifyDetachedWithCertificate(this CoseSign1Message @this, ReadOnlySpan detachedContent, bool allowUnprotected = false)
+ {
+ if(@this is null)
+ {
+ Trace.TraceWarning($"{nameof(@this)} is null and cannot be verified.");
+ return false;
+ }
+
+ if (detachedContent.IsEmpty)
+ {
+ Trace.TraceWarning($"Detached content is empty; cannot verify.");
+ return false;
+ }
+
+ AsymmetricAlgorithm? publicKey = @this.GetEmbeddedPublicKey(allowUnprotected, out bool foundCert);
+ if (!foundCert || publicKey is null)
+ {
+ return false;
+ }
+ try
+ {
+ return @this.VerifyDetached(publicKey, detachedContent);
+ }
+ catch (CryptographicException ex)
+ {
+ Trace.TraceWarning($"Verification failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Attempts to verify the COSE_Sign1 message using the embedded signing certificate and the provided detached content (Stream).
+ ///
+ /// The COSE_Sign1 message to verify.
+ /// The detached content to verify against.
+ /// True if the unprotected headers should be allowed to contribute, false (default - more secure) otherwise.
+ /// True if the message is verified with the embedded signing certificate; false otherwise.
+ public static bool VerifyDetachedWithCertificate(this CoseSign1Message @this, Stream detachedContent, bool allowUnprotected = false)
+ {
+ if(@this is null)
+ {
+ Trace.TraceWarning($"{nameof(@this)} is null and cannot be verified.");
+ return false;
+ }
+
+ if(detachedContent is null)
+ {
+ Trace.TraceWarning($"Detached content stream is null; cannot verify.");
+ return false;
+ }
+
+ AsymmetricAlgorithm? publicKey = @this.GetEmbeddedPublicKey(allowUnprotected, out bool foundCert);
+ if (!foundCert || publicKey is null)
+ {
+ return false;
+ }
+ try
+ {
+ return @this.VerifyDetached(publicKey, detachedContent);
+ }
+ catch (CryptographicException ex)
+ {
+ Trace.TraceWarning($"Verification failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ // Shared logic to extract the public key from the embedded signing certificate
+ private static AsymmetricAlgorithm? GetEmbeddedPublicKey(this CoseSign1Message @this, bool allowUnprotected, out bool foundCert)
+ {
+ foundCert = false;
+ if (!@this.TryGetSigningCertificate(out X509Certificate2 signingCert, allowUnprotected) || signingCert == null)
+ {
+ Trace.TraceWarning($"No signing certificate found in message.");
+ return null;
+ }
+ foundCert = true;
+ AsymmetricAlgorithm? publicKey = (AsymmetricAlgorithm?)signingCert.GetRSAPublicKey() ?? signingCert.GetECDsaPublicKey();
+ if (publicKey is null)
+ {
+ Trace.TraceWarning($"Signing certificate does not contain a valid public key for verification.");
+ return null;
+ }
+ return publicKey;
+ }
}