diff --git a/.gitignore b/.gitignore index 311f5bfb..74c870b1 100644 --- a/.gitignore +++ b/.gitignore @@ -363,3 +363,6 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd + +# Visual Studio live unit testing configuration files. +*.lutconfig diff --git a/CHANGELOG.md b/CHANGELOG.md index d6743918..6f61453e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,20 @@ # Changelog -## [v1.1.6-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.6-pre1) (2024-02-07) +## [v1.1.7-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.7-pre1) (2024-02-14) + +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.7...v1.1.7-pre1) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.7...v1.1.6-pre1) +**Merged pull requests:** + +- Command Line Validation of Indirect Signatures [\#78](https://github.com/microsoft/CoseSignTool/pull/78) ([elantiguamsft](https://github.com/elantiguamsft)) ## [v1.1.7](https://github.com/microsoft/CoseSignTool/tree/v1.1.7) (2024-02-07) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.6...v1.1.7) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.6-pre1...v1.1.7) + +## [v1.1.6-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.6-pre1) (2024-02-07) + +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.6...v1.1.6-pre1) **Merged pull requests:** @@ -26,19 +34,19 @@ ## [v1.1.4-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.4-pre1) (2024-01-31) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.4...v1.1.4-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.3-pre1...v1.1.4-pre1) **Merged pull requests:** - write validation output to standard out [\#74](https://github.com/microsoft/CoseSignTool/pull/74) ([elantiguamsft](https://github.com/elantiguamsft)) -## [v1.1.4](https://github.com/microsoft/CoseSignTool/tree/v1.1.4) (2024-01-26) +## [v1.1.3-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.3-pre1) (2024-01-26) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.3-pre1...v1.1.4) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.4...v1.1.3-pre1) -## [v1.1.3-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.3-pre1) (2024-01-26) +## [v1.1.4](https://github.com/microsoft/CoseSignTool/tree/v1.1.4) (2024-01-26) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.3...v1.1.3-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.3...v1.1.4) **Merged pull requests:** @@ -70,19 +78,19 @@ ## [v1.1.1-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.1-pre1) (2024-01-17) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0-pre7...v1.1.1-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1...v1.1.1-pre1) **Merged pull requests:** - Move CreateChangelog to after build in PR build [\#70](https://github.com/microsoft/CoseSignTool/pull/70) ([lemccomb](https://github.com/lemccomb)) -## [v1.1.0-pre7](https://github.com/microsoft/CoseSignTool/tree/v1.1.0-pre7) (2024-01-12) +## [v1.1.1](https://github.com/microsoft/CoseSignTool/tree/v1.1.1) (2024-01-12) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1...v1.1.0-pre7) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0-pre7...v1.1.1) -## [v1.1.1](https://github.com/microsoft/CoseSignTool/tree/v1.1.1) (2024-01-12) +## [v1.1.0-pre7](https://github.com/microsoft/CoseSignTool/tree/v1.1.0-pre7) (2024-01-12) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0-pre6...v1.1.1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0-pre6...v1.1.0-pre7) **Closed issues:** @@ -139,7 +147,7 @@ ## [v1.1.0-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.0-pre1) (2023-11-03) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0...v1.1.0-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.10...v1.1.0-pre1) **Merged pull requests:** @@ -149,13 +157,13 @@ - DetachedSignatureFactory accepts pre-hashed content as payload [\#53](https://github.com/microsoft/CoseSignTool/pull/53) ([elantiguamsft](https://github.com/elantiguamsft)) - Add password support for certificate files [\#52](https://github.com/microsoft/CoseSignTool/pull/52) ([lemccomb](https://github.com/lemccomb)) -## [v1.1.0](https://github.com/microsoft/CoseSignTool/tree/v1.1.0) (2023-10-10) +## [v0.3.1-pre.10](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.10) (2023-10-10) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.10...v1.1.0) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0...v0.3.1-pre.10) -## [v0.3.1-pre.10](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.10) (2023-10-10) +## [v1.1.0](https://github.com/microsoft/CoseSignTool/tree/v1.1.0) (2023-10-10) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.2...v0.3.1-pre.10) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.2...v1.1.0) **Merged pull requests:** diff --git a/CoseDetachedSIgnature.Tests/CoseSign1MessageDetachedSignatureExtensionsTests.cs b/CoseDetachedSIgnature.Tests/CoseSign1MessageDetachedSignatureExtensionsTests.cs deleted file mode 100644 index df4197c6..00000000 --- a/CoseDetachedSIgnature.Tests/CoseSign1MessageDetachedSignatureExtensionsTests.cs +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace CoseDetachedSignature.Tests; - -using System.IO; -using CoseDetachedSignature; -using CoseDetachedSignature.Extensions; - -/// -/// Class for Testing Methods of -/// -public class CoseSign1MessageDetachedSignatureExtensionsTests -{ - [SetUp] - public void Setup() - { - } - - [Test] - public void TestTryGetDetachedSignatureAlgorithmSuccess() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetDetachedSignatureAlgorithmSuccess)); - DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - - CoseSign1Message detachedSignature = factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); - detachedSignature.TryGetDetachedSignatureAlgorithm(out HashAlgorithmName hashAlgorithmName).Should().BeTrue(); - hashAlgorithmName.Should().Be(HashAlgorithmName.SHA256); - } - - [Test] - public void TestTryGetDetachedSignatureAlgorithmFailure() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetDetachedSignatureAlgorithmFailure)); - - // no content type - Mock removeContentTypeHeaderExtender = new Mock(MockBehavior.Strict); - removeContentTypeHeaderExtender.Setup(m => m.ExtendProtectedHeaders(It.IsAny())).Returns((input) => - { - // remove ContentType - if (input.ContainsKey(CoseHeaderLabel.ContentType)) - { - input.Remove(CoseHeaderLabel.ContentType); - } - return input; - }); - removeContentTypeHeaderExtender.Setup(m => m.ExtendUnProtectedHeaders(It.IsAny())).Returns((input) => input); - - // empty content type - Mock emptyContentTypeHeaderExtender = new Mock(MockBehavior.Strict); - emptyContentTypeHeaderExtender.Setup(m => m.ExtendProtectedHeaders(It.IsAny())).Returns((input) => - { - // remove content type - input = removeContentTypeHeaderExtender.Object.ExtendProtectedHeaders(input); - - // add content type with an empty string value - input.Add(CoseHeaderLabel.ContentType, string.Empty); - - return input; - }); - emptyContentTypeHeaderExtender.Setup(m => m.ExtendUnProtectedHeaders(It.IsAny())).Returns((input) => input); - - DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - - // call TryGetDetachedSignatureAlgoithm on a null message object should fail - CoseSign1Message? objectUnderTest = null; - objectUnderTest.TryGetDetachedSignatureAlgorithm(out HashAlgorithmName hashAlgorithmName).Should().BeFalse(); - - // call TryGetDetachedSignatureAlgorithm on a CoseSign1Message with no ContentType header should fail - objectUnderTest = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, @"application\test.output", removeContentTypeHeaderExtender.Object); - objectUnderTest.TryGetDetachedSignatureAlgorithm(out hashAlgorithmName).Should().BeFalse("missing content type should fail the test"); - - // call TryGetDetachedSignatureAlgorithm on a CoseSign1Message with empty ContentType should fail - objectUnderTest = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, @"application\test.output", emptyContentTypeHeaderExtender.Object); - objectUnderTest.TryGetDetachedSignatureAlgorithm(out hashAlgorithmName).Should().BeFalse("empty content type should fail the test"); - - // call TryGetDetachedSignatureAlgorithm on a CoseSign1Message with invalid mime type hash extension ContentType should fail - objectUnderTest = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, @"application\test.output"); - objectUnderTest.TryGetDetachedSignatureAlgorithm(out hashAlgorithmName).Should().BeFalse("missing mime type hash extension in content type should fail the test"); - - // call TryGetDetachedSignatureAlgorithm on a CoseSign1Message with invalid mime type hash extension ContentType should succeed - objectUnderTest = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, @"application\test.output+hash-notavalidvalue"); - objectUnderTest.TryGetDetachedSignatureAlgorithm(out hashAlgorithmName).Should().BeTrue("invalid mime type hash extension in content type should not fail this call"); - } - - [Test] - public void TestIsDetachedSignatureSuccess() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetDetachedSignatureAlgorithmSuccess)); - DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - - CoseSign1Message detachedSignature = factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); - detachedSignature.IsDetachedSignature().Should().BeTrue(); - } - - [Test] - public void TestIsDetachedSignatureFailure() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetDetachedSignatureAlgorithmSuccess)); - DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - - CoseSign1Message detachedSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload"); - detachedSignature.IsDetachedSignature().Should().BeFalse(); - } - - [Test] - public void TestSignatureMatchesStreamSuccess() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetDetachedSignatureAlgorithmSuccess)); - DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - using MemoryStream stream = new(randomBytes); - - CoseSign1Message detachedSignature = factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); - detachedSignature.SignatureMatches(stream).Should().BeTrue(); - } - - [Test] - public void TestSignatureMatchesStreamFailure() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetDetachedSignatureAlgorithmSuccess)); - DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - byte[] randomBytes2 = new byte[50]; - new Random().NextBytes(randomBytes); - new Random().NextBytes(randomBytes2); - using MemoryStream stream = new(randomBytes2); - - // test mismatched signature - CoseSign1Message? detachedSignature = factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); - detachedSignature.SignatureMatches(stream).Should().BeFalse(); - stream.Dispose(); - using MemoryStream stream2 = new(randomBytes); - - // test invalid hash extension case - detachedSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload"); - detachedSignature.SignatureMatches(stream2).Should().BeFalse(); - stream2.Seek(stream2.Length, SeekOrigin.Begin); - - // test null object case - detachedSignature = null; - CoseSign1MessageDetachedSignatureExtensions.SignatureMatches(detachedSignature, stream2).Should().BeFalse(); - } - - [Test] - public void TestSignatureMatchesBytesSuccess() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetDetachedSignatureAlgorithmSuccess)); - DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - - CoseSign1Message detachedSignature = factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); - detachedSignature.SignatureMatches(randomBytes).Should().BeTrue(); - } - - [Test] - public void TestSignatureMatchesBytesFailure() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetDetachedSignatureAlgorithmSuccess)); - DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - byte[] randomBytes2 = new byte[50]; - new Random().NextBytes(randomBytes); - new Random().NextBytes(randomBytes2); - - // test mismatched signature - CoseSign1Message? detachedSignature = factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); - detachedSignature.SignatureMatches(randomBytes2).Should().BeFalse(); - - // test invalid hash extension case - detachedSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload"); - detachedSignature.SignatureMatches(randomBytes).Should().BeFalse(); - - // test null object case - detachedSignature = null; - CoseSign1MessageDetachedSignatureExtensions.SignatureMatches(detachedSignature, randomBytes).Should().BeFalse(); - } - - [Test] - public void TestTryGetHashAlgorithmSuccess() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetDetachedSignatureAlgorithmSuccess)); - DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - - CoseSign1Message detachedSignature = factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); - detachedSignature.TryGetHashAlgorithm(out HashAlgorithm? hashAlgorithm).Should().BeTrue(); - hashAlgorithm.Should().NotBeNull(); - hashAlgorithm.Should().BeAssignableTo(); - } - - [Test] - public void TestTryGetHashAlgorithmFailure() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetDetachedSignatureAlgorithmSuccess)); - DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - byte[] randomBytes2 = new byte[50]; - new Random().NextBytes(randomBytes); - new Random().NextBytes(randomBytes2); - - // Fail to extract a hash algorithm name - CoseSign1Message? detachedSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload"); - detachedSignature.TryGetHashAlgorithm(out HashAlgorithm? hashAlgorithm).Should().BeFalse(); - hashAlgorithm.Should().BeNull(); - - // COSE Sign1 Detached signature case with other things being valid - // content should be null in this case. - detachedSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: false, "application/test.payload+hash-sha256"); - detachedSignature.TryGetHashAlgorithm(out hashAlgorithm).Should().BeFalse(); - hashAlgorithm.Should().BeNull(); - - // Invalid hash definition case - detachedSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload+hash-notavalidhashalgorithm"); - detachedSignature.TryGetHashAlgorithm(out hashAlgorithm).Should().BeFalse(); - hashAlgorithm.Should().BeNull(); - - // test null object case - detachedSignature = null; - CoseSign1MessageDetachedSignatureExtensions.TryGetHashAlgorithm(detachedSignature, out hashAlgorithm).Should().BeFalse(); - hashAlgorithm.Should().BeNull(); - } - - private ICoseSigningKeyProvider SetupMockSigningKeyProvider(string testName) - { - Mock mockedSignerKeyProvider = new(MockBehavior.Strict); - X509Certificate2 selfSignedCertWithRSA = TestCertificateUtils.CreateCertificate(testName); - - mockedSignerKeyProvider.Setup(x => x.GetProtectedHeaders()).Returns(null); - mockedSignerKeyProvider.Setup(x => x.GetUnProtectedHeaders()).Returns(null); - mockedSignerKeyProvider.Setup(x => x.HashAlgorithm).Returns(HashAlgorithmName.SHA256); - mockedSignerKeyProvider.Setup(x => x.GetECDsaKey(It.IsAny())).Returns(null); - mockedSignerKeyProvider.Setup(x => x.GetRSAKey(It.IsAny())).Returns(selfSignedCertWithRSA.GetRSAPrivateKey()); - mockedSignerKeyProvider.Setup(x => x.IsRSA).Returns(true); - - return mockedSignerKeyProvider.Object; - } -} diff --git a/CoseDetachedSIgnature.Tests/DetachedSignatureFactoryTests.cs b/CoseDetachedSIgnature.Tests/DetachedSignatureFactoryTests.cs deleted file mode 100644 index 3ac560a5..00000000 --- a/CoseDetachedSIgnature.Tests/DetachedSignatureFactoryTests.cs +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace CoseDetachedSignature.Tests; - -/// -/// Class for Testing Methods of -/// -public class DetachedSignatureFactoryTests -{ - [SetUp] - public void Setup() - { - } - - [Test] - public void TestConstructors() - { - Mock mockFactory = new(MockBehavior.Strict); - using DetachedSignatureFactory factory = new(); - using DetachedSignatureFactory factory2 = new(HashAlgorithmName.SHA384); - using DetachedSignatureFactory factory3 = new(HashAlgorithmName.SHA512, mockFactory.Object); - - factory.HashAlgorithm.Should().BeAssignableTo(); - factory.HashAlgorithmName.Should().Be(HashAlgorithmName.SHA256); - factory.MessageFactory.Should().BeOfType(); - - factory2.HashAlgorithm.Should().BeAssignableTo(); - factory2.HashAlgorithmName.Should().Be(HashAlgorithmName.SHA384); - factory2.MessageFactory.Should().BeOfType(); - - factory3.HashAlgorithm.Should().BeAssignableTo(); - factory3.HashAlgorithmName.Should().Be(HashAlgorithmName.SHA512); - factory3.MessageFactory.Should().Be(mockFactory.Object); - } - - [Test] - public async Task TestCreateDetachedSignatureAsync() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateDetachedSignatureAsync)); - using DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - using MemoryStream memStream = new(randomBytes); - - // test the sync method - Assert.Throws(() => factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature = factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); - 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(memStream, coseSigningKeyProvider, string.Empty)); - memStream.Seek(0, SeekOrigin.Begin); - CoseSign1Message detachedSignature2 = factory.CreateDetachedSignature(memStream, coseSigningKeyProvider, "application/test.payload"); - detachedSignature2.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); - detachedSignature2.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); - detachedSignature2.SignatureMatches(randomBytes).Should().BeTrue(); - memStream.Seek(0, SeekOrigin.Begin); - - // test the async methods - Assert.ThrowsAsync(() => factory.CreateDetachedSignatureAsync(randomBytes, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature3 = await factory.CreateDetachedSignatureAsync(randomBytes, coseSigningKeyProvider, "application/test.payload"); - 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(memStream, coseSigningKeyProvider, string.Empty)); - memStream.Seek(0, SeekOrigin.Begin); - CoseSign1Message detachedSignature4 = await factory.CreateDetachedSignatureAsync(memStream, coseSigningKeyProvider, "application/test.payload"); - detachedSignature4.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); - detachedSignature4.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); - detachedSignature4.SignatureMatches(randomBytes).Should().BeTrue(); - 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.CreateDetachedSignatureFromHash(hash, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature = factory.CreateDetachedSignatureFromHash(hash, coseSigningKeyProvider, "application/test.payload"); - 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.CreateDetachedSignatureFromHash(hashStream, coseSigningKeyProvider, "application/test.payload"); - 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.CreateDetachedSignatureFromHashAsync(hash, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature3 = await factory.CreateDetachedSignatureFromHashAsync(hash, coseSigningKeyProvider, "application/test.payload"); - 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.CreateDetachedSignatureFromHashAsync(hashStream, coseSigningKeyProvider, string.Empty)); - hashStream.Seek(0, SeekOrigin.Begin); - CoseSign1Message detachedSignature4 = await factory.CreateDetachedSignatureFromHashAsync(hashStream, coseSigningKeyProvider, "application/test.payload"); - 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() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateDetachedSignatureBytesAsync)); - using DetachedSignatureFactory factory = new(); - byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - using MemoryStream memStream = new(randomBytes); - - // test the sync method - Assert.Throws(() => factory.CreateDetachedSignatureBytes(randomBytes, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature = CoseMessage.DecodeSign1(factory.CreateDetachedSignatureBytes(randomBytes, coseSigningKeyProvider, "application/test.payload").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(memStream, coseSigningKeyProvider, string.Empty)); - memStream.Seek(0, SeekOrigin.Begin); - CoseSign1Message detachedSignature2 = CoseMessage.DecodeSign1(factory.CreateDetachedSignatureBytes(memStream, coseSigningKeyProvider, "application/test.payload").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(); - memStream.Seek(0, SeekOrigin.Begin); - - // test the async methods - Assert.ThrowsAsync(() => factory.CreateDetachedSignatureBytesAsync(randomBytes, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature3 = CoseMessage.DecodeSign1((await factory.CreateDetachedSignatureBytesAsync(randomBytes, coseSigningKeyProvider, "application/test.payload")).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(memStream, coseSigningKeyProvider, string.Empty)); - memStream.Seek(0, SeekOrigin.Begin); - CoseSign1Message detachedSignature4 = CoseMessage.DecodeSign1((await factory.CreateDetachedSignatureBytesAsync(memStream, coseSigningKeyProvider, "application/test.payload")).ToArray()); - detachedSignature4.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); - detachedSignature4.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+hash-sha256"); - memStream.Seek(0, SeekOrigin.Begin); - 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.CreateDetachedSignatureBytesFromHash(hash, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature = CoseMessage.DecodeSign1(factory.CreateDetachedSignatureBytesFromHash(hash, coseSigningKeyProvider, "application/test.payload").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.CreateDetachedSignatureBytesFromHash(hashStream, coseSigningKeyProvider, string.Empty)); - hashStream.Seek(0, SeekOrigin.Begin); - CoseSign1Message detachedSignature2 = CoseMessage.DecodeSign1(factory.CreateDetachedSignatureBytesFromHash(hashStream, coseSigningKeyProvider, "application/test.payload").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.CreateDetachedSignatureBytesFromHashAsync(hash, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature3 = CoseMessage.DecodeSign1((await factory.CreateDetachedSignatureBytesFromHashAsync(hash, coseSigningKeyProvider, "application/test.payload")).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.CreateDetachedSignatureBytesFromHashAsync(hashStream, coseSigningKeyProvider, string.Empty)); - hashStream.Seek(0, SeekOrigin.Begin); - CoseSign1Message detachedSignature4 = CoseMessage.DecodeSign1((await factory.CreateDetachedSignatureBytesFromHashAsync(hashStream, coseSigningKeyProvider, "application/test.payload")).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() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateDetachedSignatureMd5)); - using DetachedSignatureFactory factory = new(HashAlgorithmName.MD5); - byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - - // test the sync method - Assert.Throws(() => factory.CreateDetachedSignature(randomBytes, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature = CoseMessage.DecodeSign1(factory.CreateDetachedSignatureBytes(randomBytes, coseSigningKeyProvider, "application/test.payload").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] - 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.CreateDetachedSignatureFromHash(hash, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature = CoseMessage.DecodeSign1(factory.CreateDetachedSignatureBytesFromHash(hash, coseSigningKeyProvider, "application/test.payload").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.CreateDetachedSignatureFromHash(randomBytes, coseSigningKeyProvider, "application/test.payload")); - } - - [Test] - public void TestCreateDetachedSignatureAlreadyProvided() - { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateDetachedSignatureAlreadyProvided)); - 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)}"); - ReadOnlyMemory hash = hasher!.ComputeHash(randomBytes); - - // test the sync method - Assert.Throws(() => factory.CreateDetachedSignature(hash, coseSigningKeyProvider, string.Empty)); - CoseSign1Message detachedSignature = CoseMessage.DecodeSign1(factory.CreateDetachedSignatureBytes(randomBytes, coseSigningKeyProvider, "application/test.payload+hash-sha256").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(); - } - - private ICoseSigningKeyProvider SetupMockSigningKeyProvider(string testName) - { - Mock mockedSignerKeyProvider = new(MockBehavior.Strict); - X509Certificate2 selfSignedCertWithRSA = TestCertificateUtils.CreateCertificate(testName); - - mockedSignerKeyProvider.Setup(x => x.GetProtectedHeaders()).Returns(null); - mockedSignerKeyProvider.Setup(x => x.GetUnProtectedHeaders()).Returns(null); - mockedSignerKeyProvider.Setup(x => x.HashAlgorithm).Returns(HashAlgorithmName.SHA256); - mockedSignerKeyProvider.Setup(x => x.GetECDsaKey(It.IsAny())).Returns(null); - mockedSignerKeyProvider.Setup(x => x.GetRSAKey(It.IsAny())).Returns(selfSignedCertWithRSA.GetRSAPrivateKey()); - mockedSignerKeyProvider.Setup(x => x.IsRSA).Returns(true); - - return mockedSignerKeyProvider.Object; - } -} diff --git a/CoseHandler.Tests/CoseSignValidateTests.cs b/CoseHandler.Tests/CoseSignValidateTests.cs index 9b642d07..822bada6 100644 --- a/CoseHandler.Tests/CoseSignValidateTests.cs +++ b/CoseHandler.Tests/CoseSignValidateTests.cs @@ -3,7 +3,7 @@ namespace CoseSignUnitTests; -using CoseDetachedSignature; +using CoseIndirectSignature; using System.Net.Mime; using System.Runtime.ConstrainedExecution; @@ -340,8 +340,8 @@ public void DetachedValidateModifiedPayload() [TestMethod] public void IndirectSignatureValidation() { - var msgFac = new DetachedSignatureFactory(); - byte[] signedBytes = msgFac.CreateDetachedSignatureBytes( + var msgFac = new IndirectSignatureFactory(); + byte[] signedBytes = msgFac.CreateIndirectSignatureBytes( payload: Payload1Bytes, contentType: "application/spdx+json", signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(Leaf1Priv)).ToArray(); @@ -358,8 +358,8 @@ public void IndirectSignatureValidation() [TestMethod] public void IndirectSignatureModifiedPayload() { - var msgFac = new DetachedSignatureFactory(); - byte[] signedBytes = msgFac.CreateDetachedSignatureBytes( + var msgFac = new IndirectSignatureFactory(); + byte[] signedBytes = msgFac.CreateIndirectSignatureBytes( payload: Payload1Bytes, contentType: "application/spdx+json", signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(Leaf1Priv)).ToArray(); @@ -381,8 +381,8 @@ public void IndirectSignatureModifiedPayload() [TestMethod] public void IndirectSignatureUntrustedSignature() { - var msgFac = new DetachedSignatureFactory(); - byte[] signedBytes = msgFac.CreateDetachedSignatureBytes( + var msgFac = new IndirectSignatureFactory(); + byte[] signedBytes = msgFac.CreateIndirectSignatureBytes( payload: Payload1Bytes, contentType: "application/spdx+json", signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(Leaf1Priv)).ToArray(); diff --git a/CoseHandler/CoseHandler.cs b/CoseHandler/CoseHandler.cs index 96f7fcc6..0c469a27 100644 --- a/CoseHandler/CoseHandler.cs +++ b/CoseHandler/CoseHandler.cs @@ -21,7 +21,7 @@ namespace CoseX509; using CoseSign1.Certificates.Local.Validators; using CoseSign1.Extensions; using CoseSign1.Interfaces; -using CoseDetachedSignature.Extensions; +using CoseIndirectSignature.Extensions; /// /// Contains static methods to generate and validate Cose X509 signatures. @@ -605,7 +605,7 @@ internal static ValidationResult ValidateInternal( // Determine the type of content validation to perform. // Check for an indirect signature, where the content header contains the hash of the payload, and the algorithm is stored in the message. // If this is the case and external content is provided, we can validate an external payload hash against the hash stored in the cose message content. - ContentValidationType cvt = msg.IsDetachedSignature() ? ContentValidationType.Indirect : + ContentValidationType cvt = msg.IsIndirectSignature() ? ContentValidationType.Indirect : (hasBytes || hasStream) ? ContentValidationType.Detached : ContentValidationType.Embedded; try diff --git a/CoseHandler/CoseHandler.csproj b/CoseHandler/CoseHandler.csproj index 619b4f22..c495a72e 100644 --- a/CoseHandler/CoseHandler.csproj +++ b/CoseHandler/CoseHandler.csproj @@ -19,7 +19,7 @@ - + diff --git a/CoseIndirectSignature.Tests/CoseHashVFuzzer.cs b/CoseIndirectSignature.Tests/CoseHashVFuzzer.cs new file mode 100644 index 00000000..b49d2f75 --- /dev/null +++ b/CoseIndirectSignature.Tests/CoseHashVFuzzer.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ignore Spelling: Cose Fuzzer + +namespace CoseIndirectSignature.Tests; +using System; +using CoseIndirectSignature.Exceptions; + +/// +/// Class used to fuzz the parsing logic for CoseHashV and CborReader under the covers. +/// +public static class CoseHashVFuzzer +{ + /// + /// Fuzz target method matching signatures expected for fuzzing. + /// + /// + public static void FuzzCoseHashVParser(ReadOnlySpan input) + { + try + { + CoseHashV objectUnderTest = CoseHashV.Deserialize(input); + } + // deserialize documents two exceptions to be thrown, so catch them as known "good" behavior. + catch(Exception ex) when (ex is InvalidCoseDataException || ex is ArgumentNullException) + { + } + } +} diff --git a/CoseIndirectSignature.Tests/CoseHashVTests.cs b/CoseIndirectSignature.Tests/CoseHashVTests.cs new file mode 100644 index 00000000..d10c970c --- /dev/null +++ b/CoseIndirectSignature.Tests/CoseHashVTests.cs @@ -0,0 +1,604 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ignore Spelling: Cose Deserialization + +namespace CoseIndirectSignature.Tests; + +using System.Formats.Cbor; +using System.Security.Cryptography; +using CoseIndirectSignature.Exceptions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NUnit.Framework.Internal; + +/// +/// Class for Testing Methods of +/// +public class CoseHashVTests +{ + [SetUp] + public void Setup() + { + } + + [Test] + [TestCase(1, Description = "Default constructor.")] + [TestCase(2, Description = "Sha256 with valid byte data.")] + [TestCase(3, Description = "Sha256 with valid stream data.")] + [TestCase(4, Description = "Sha256 with valid byte data, and a location.")] + [TestCase(5, Description = "Sha256 with valid byte data, a location, and additionalData.")] + [TestCase(6, Description = "Sha256 with valid stream, and a location.")] + [TestCase(7, Description = "Sha256 with a valid stream, a location, and additionalData.")] + [TestCase(8, Description = "Sha256 with a valid readonly memory.")] + [TestCase(9, Description = "Sha256 with a valid readonly memory, and a location.")] + [TestCase(10, Description = "Copy constructor.")] + [TestCase(11, Description = "Bypass hash validation for explicit construction")] + [TestCase(12, Description = "Bypass hash validation for explicit construction with a bogus algorithm, should serialize")] + public void TestCoseHashVConstructorSuccess(int testCase) + { + // arrange + byte[] testData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + using MemoryStream stream = new MemoryStream(testData); + ReadOnlyMemory rom = new ReadOnlyMemory(testData); + CoseHashV testObj = new CoseHashV(); + switch (testCase) + { + case 1: + testObj.Algorithm.Should().Be(CoseHashAlgorithm.Reserved); + testObj.HashValue.Should().BeEmpty(); + testObj.Location.Should().BeNullOrWhiteSpace(); + testObj.AdditionalData.Should().BeNull(); + break; + case 2: + testObj = new CoseHashV(CoseHashAlgorithm.SHA256, byteData: testData); + testObj.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + testObj.HashValue.Should().NotBeEmpty(); + testObj.HashValue.Length.Should().Be(32); + testObj.Location.Should().BeNullOrWhiteSpace(); + testObj.AdditionalData.Should().BeNull(); + break; + case 3: + testObj = new CoseHashV(CoseHashAlgorithm.SHA256, stream); + stream.Seek(0, SeekOrigin.Begin); + testObj.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + testObj.HashValue.Should().NotBeEmpty(); + testObj.HashValue.Length.Should().Be(32); + testObj.Location.Should().BeNullOrWhiteSpace(); + testObj.AdditionalData.Should().BeNull(); + break; + case 4: + testObj = new CoseHashV(CoseHashAlgorithm.SHA256, testData, "location"); + testObj.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + testObj.HashValue.Should().NotBeEmpty(); + testObj.HashValue.Length.Should().Be(32); + testObj.Location.Should().Be("location"); + testObj.AdditionalData.Should().BeNull(); + break; + case 5: + testObj = new CoseHashV(CoseHashAlgorithm.SHA256, testData, "location", testData); + testObj.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + testObj.HashValue.Should().NotBeEmpty(); + testObj.HashValue.Length.Should().Be(32); + testObj.Location.Should().Be("location"); + testObj.AdditionalData.Should().BeEquivalentTo(testData); + break; + case 6: + testObj = new CoseHashV(CoseHashAlgorithm.SHA256, stream, "location"); + stream.Seek(0, SeekOrigin.Begin); + testObj.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + testObj.HashValue.Should().NotBeEmpty(); + testObj.HashValue.Length.Should().Be(32); + testObj.Location.Should().Be("location"); + testObj.AdditionalData.Should().BeNull(); + break; + case 7: + testObj = new CoseHashV(CoseHashAlgorithm.SHA256, stream, "location", testData); + stream.Seek(0, SeekOrigin.Begin); + testObj.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + testObj.HashValue.Should().NotBeEmpty(); + testObj.HashValue.Length.Should().Be(32); + testObj.Location.Should().Be("location"); + testObj.AdditionalData.Should().BeEquivalentTo(testData); + break; + case 8: + testObj = new CoseHashV(CoseHashAlgorithm.SHA256, rom, "location"); + testObj.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + testObj.HashValue.Should().NotBeEmpty(); + testObj.HashValue.Length.Should().Be(32); + testObj.Location.Should().Be("location"); + testObj.AdditionalData.Should().BeNull(); + break; + case 9: + testObj = new CoseHashV(CoseHashAlgorithm.SHA256, rom, "location", rom); + testObj.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + testObj.HashValue.Should().NotBeEmpty(); + testObj.HashValue.Length.Should().Be(32); + testObj.Location.Should().Be("location"); + testObj.AdditionalData.Should().BeEquivalentTo(testData); + break; + case 10: + testObj.AdditionalData = [0x03, 0x02, 0x01]; + CoseHashV other = new CoseHashV(testObj); + other.Algorithm.Should().Be(testObj.Algorithm); + other.HashValue.Should().BeEquivalentTo(testObj.HashValue); + other.Location.Should().Be(testObj.Location); + other.AdditionalData.Should().BeEquivalentTo(testObj.AdditionalData); + break; + case 11: + CoseHashV testObject11 = new CoseHashV(CoseHashAlgorithm.SHA256, hashValue: [0x1, 0x2, 0x3], disableValidation: true); + testObject11.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + testObject11.HashValue.Should().BeEquivalentTo(new byte[] { 0x1, 0x2, 0x3 }); + break; + case 12: + CoseHashV testObject12 = new CoseHashV((CoseHashAlgorithm)(-100), hashValue: [0x1, 0x2, 0x3], disableValidation: true); + testObject12.Algorithm.Should().Be((CoseHashAlgorithm)(-100)); + testObject12.HashValue.Should().BeEquivalentTo(new byte[] { 0x1, 0x2, 0x3 }); + break; + default: + throw new InvalidDataException($"Test case {testCase} is not defined in {nameof(TestCoseHashVConstructorSuccess)}"); + } + } + + [Test] + [TestCase(1, Description = "Reserved algorithm case for byte ctor.")] + [TestCase(2, Description = "Hash value that is not the size of the expected hash algorithm.")] + [TestCase(3, Description = "Reserved algorithm case for stream ctor.")] + [TestCase(4, Description = "Reserved algorithm case for byte ctor. and location")] + [TestCase(5, Description = "Reserved algorithm case for byte ctor. and location and additionalData")] + [TestCase(6, Description = "Reserved algorithm case for stream ctor. and location")] + [TestCase(7, Description = "Reserved algorithm case for stream ctor. and location and additionalData")] + [TestCase(8, Description = "SHA256Trunc64 algorithm for readonly memory byte array, and location")] + [TestCase(9, Description = "SHAKE128 algorithm for readonly memory byte array and location and additionalData")] + [TestCase(10, Description = "Null stream.")] + [TestCase(11, Description = "Null byte data.")] + [TestCase(12, Description = "Null read only data.")] + [TestCase(13, Description = "Null hashValue")] + [TestCase(14, Description = "0 length hashValue")] + public void TestCoseHashVConstructorFailure(int testCase) + { + // arrange + byte[] testData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + using MemoryStream stream = new MemoryStream(testData); + ReadOnlyMemory rom = new ReadOnlyMemory(testData); + + // act and assert + Action act = () => new CoseHashV(CoseHashAlgorithm.Reserved, byteData: testData); + switch (testCase) + { + case 1: + act.Should().Throw(); + break; + case 2: + act = () => new CoseHashV(CoseHashAlgorithm.SHA256, hashValue: testData); + act.Should().Throw(); + break; + case 3: + act = () => new CoseHashV(CoseHashAlgorithm.Reserved, stream); + act.Should().Throw(); + break; + case 4: + act = () => new CoseHashV(CoseHashAlgorithm.Reserved, testData, "location"); + act.Should().Throw(); + break; + case 5: + act = () => new CoseHashV(CoseHashAlgorithm.SHAKE256, testData, "location", testData); + act.Should().Throw(); + break; + case 6: +#pragma warning disable CS0618 + act = () => new CoseHashV(CoseHashAlgorithm.SHA1, stream, "location"); + act.Should().Throw(); +#pragma warning restore CS0618 + break; + case 7: +#pragma warning disable CS0618 + act = () => new CoseHashV(CoseHashAlgorithm.SHA512Truc256, stream, "location", testData); + act.Should().Throw(); +#pragma warning restore CS0618 + break; + case 8: +#pragma warning disable CS0618 + act = () => new CoseHashV(CoseHashAlgorithm.SHA256Trunc64, rom, "location"); + act.Should().Throw(); +#pragma warning restore CS0618 + break; + case 9: + act = () => new CoseHashV(CoseHashAlgorithm.SHAKE128, rom, "location", rom); + act.Should().Throw(); + break; + case 10: +#nullable disable + act = () => new CoseHashV(CoseHashAlgorithm.SHA256, streamData: null); + act.Should().Throw(); +#nullable restore + break; + case 11: +#nullable disable + act = () => new CoseHashV(CoseHashAlgorithm.SHA256, byteData: null); + act.Should().Throw(); +#nullable restore + break; + case 12: + act = () => new CoseHashV(CoseHashAlgorithm.SHA256, readonlyData: null); + act.Should().Throw(); + break; + case 13: + act = () => new CoseHashV(CoseHashAlgorithm.SHA256, hashValue: null); + act.Should().Throw(); + break; + case 14: + act = () => new CoseHashV(CoseHashAlgorithm.SHA256, hashValue: []); + act.Should().Throw(); + break; + default: + throw new InvalidDataException($"Test case {testCase} is not defined in {nameof(TestCoseHashVConstructorFailure)}"); + } + } + + [Test] + public void CoseHashVContentStreamMatchesTests() + { + // arrange + byte[] testData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + using MemoryStream stream = new MemoryStream(testData); + ReadOnlyMemory rom = new ReadOnlyMemory(testData); + CoseHashV testObj = new CoseHashV(CoseHashAlgorithm.SHA256, byteData: testData); + + // act + bool result = testObj.ContentMatches(testData); + bool resultStream = testObj.ContentMatches(stream); + bool resultRom = testObj.ContentMatches(rom); + + // assert + result.Should().BeTrue(); + resultStream.Should().BeTrue(); + resultRom.Should().BeTrue(); + } + + [Test] + public void CoseHashVContentStreamMatchesAsyncTests() + { + // arrange + byte[] testData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + using MemoryStream stream = new MemoryStream(testData); + ReadOnlyMemory rom = new ReadOnlyMemory(testData); + CoseHashV testObj = new CoseHashV(CoseHashAlgorithm.SHA256, byteData: testData); + + // act + bool result = testObj.ContentMatchesAsync(testData).Result; + bool resultStream = testObj.ContentMatchesAsync(stream).Result; + bool resultRom = testObj.ContentMatchesAsync(rom).Result; + + // assert + result.Should().BeTrue(); + resultStream.Should().BeTrue(); + resultRom.Should().BeTrue(); + } + + [Test] + public void ContentMatchesNullDataFailureTests() + { + // arrange + CoseHashV testObj = new CoseHashV(CoseHashAlgorithm.SHA256, byteData: new byte[] { 0x01, 0x02, 0x03, 0x04 }); + + // act and assert +#nullable disable + Func act = async () => await testObj.ContentMatchesAsync(data: null).ConfigureAwait(false); + act.Should().ThrowAsync(); + + Action act2 = () => testObj.ContentMatches(stream: null); + act2.Should().Throw(); +#nullable restore + } + + [Test] + public void TestSerialization() + { + // arrange + byte[] testData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + CoseHashV testObj = new CoseHashV(CoseHashAlgorithm.SHA256, byteData: testData); + + // act + byte[] encoding = testObj.Serialize(); + CoseHashV newObj = CoseHashV.Deserialize(new CborReader(encoding)); + + // assert + encoding.Should().NotBeNull(); + encoding.Length.Should().BeGreaterThan(0); + newObj.Algorithm.Should().Be(testObj.Algorithm); + newObj.HashValue.Should().BeEquivalentTo(testObj.HashValue); + newObj.Location.Should().Be(testObj.Location); + newObj.AdditionalData.Should().BeEquivalentTo(testObj.AdditionalData); + } + + [Test] + public void TestSerializationWithOptionalFields() + { + // arrange + byte[] testData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + CoseHashV testObj = new CoseHashV(CoseHashAlgorithm.SHA256, testData, location: "this is the location"); + + // act + byte[] encoding = testObj.Serialize(); + CoseHashV newObj = CoseHashV.Deserialize(encoding); + + // assert + encoding.Should().NotBeNull(); + encoding.Length.Should().BeGreaterThan(0); + newObj.Algorithm.Should().Be(testObj.Algorithm); + newObj.HashValue.Should().BeEquivalentTo(testObj.HashValue); + newObj.Location.Should().Be(testObj.Location); + newObj.AdditionalData.Should().BeEquivalentTo(testObj.AdditionalData); + + testObj = new CoseHashV(CoseHashAlgorithm.SHA256, testData, location: "this is the location", additionalData: [0x01, 0x03, 0x04]); + + // act + encoding = testObj.Serialize(); + newObj = CoseHashV.Deserialize(encoding); + + // assert + encoding.Should().NotBeNull(); + encoding.Length.Should().BeGreaterThan(0); + newObj.Algorithm.Should().Be(testObj.Algorithm); + newObj.HashValue.Should().BeEquivalentTo(testObj.HashValue); + newObj.Location.Should().Be(testObj.Location); + newObj.AdditionalData.Should().BeEquivalentTo(testObj.AdditionalData); + } + + [Test] + [TestCase(1, Description = "Too few properties.")] + [TestCase(2, Description = "Too many properties.")] + [TestCase(3, Description = "Correct properties, but wrong type.")] + [TestCase(4, Description = "Correct algorithm and hash, but wrong location type.")] + [TestCase(5, Description = "Correct algorithm and hash, but wrong additionalData type.")] + [TestCase(6, Description = "Correct algorithm, but hash length does not match algorithm.")] + [TestCase(7, Description = "Not starting with start array")] + [TestCase(8, Description = "Null Cbor reader")] + [TestCase(9, Description = "Null byte array")] + [TestCase(10, Description = "Invalid tstr encoding")] + [TestCase(11, Description = "Valid tstr encoding")] + [TestCase(12, Description ="Invalid algorithm integer - negative.")] + [TestCase(13, Description = "Invalid algorithm integer - positive.")] + [TestCase(14, Description = "Invalid algorithm integer, random hash, should deserialize with flag.")] + [TestCase(15, Description = "Valid algorithm integer, random hash, should deserialize with flag.")] + [TestCase(16, Description = "0 bytes to ReadOnlySpan")] + [TestCase(17, Description = "Fuzz overflow")] + [TestCase(18, Description = "Fuzz enum overflow")] + public void TestObjectManualSerializationPaths(int testCase) + { + CborWriter? writer; + int propertyCount = 1; + byte[]? cborEcoding; + switch (testCase) + { + // handle too few properties + case 1: + writer = new(CborConformanceMode.Strict); + writer.WriteStartArray(propertyCount); + writer.WriteInt64((long)2); + writer.WriteEndArray(); + + cborEcoding = writer.Encode(); + Assert.ThrowsException(() => CoseHashV.Deserialize(cborEcoding)); + break; + // handle too many properties + case 2: + writer = new(CborConformanceMode.Strict); + propertyCount = 5; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteInt64((long)CoseHashAlgorithm.SHA256); + writer.WriteByteString(new byte[] { 0x01, 0x02, 0x03, 0x04 }); + writer.WriteTextString("location"); + writer.WriteByteString(new byte[] { 0x01, 0x02, 0x03, 0x04 }); + writer.WriteBoolean(true); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + Assert.ThrowsException(() => CoseHashV.Deserialize(cborEcoding)); + break; + // handle the correct amount, but the wrong types + case 3: + writer = new(CborConformanceMode.Strict); + propertyCount = 2; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteBoolean(true); + writer.WriteBoolean(false); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + Assert.ThrowsException(() => CoseHashV.Deserialize(cborEcoding)); + break; + // handle the correct algorithm and hash, but wrong type for location + case 4: + writer = new(CborConformanceMode.Strict); + propertyCount = 3; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteInt64((long)CoseHashAlgorithm.SHA256); + writer.WriteByteString(SHA256.HashData([0x01, 0x02, 0x03, 0x04])); + writer.WriteBoolean(true); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + Assert.ThrowsException(() => CoseHashV.Deserialize(cborEcoding)); + break; + // handle the correct algorithm, hash and location, but wrong type for additional data + case 5: + writer = new(CborConformanceMode.Strict); + propertyCount = 4; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteInt64((long)CoseHashAlgorithm.SHA256); + writer.WriteByteString(SHA256.HashData([0x01, 0x02, 0x03, 0x04])); + writer.WriteTextString("location"); + writer.WriteBoolean(false); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + Assert.ThrowsException(() => CoseHashV.Deserialize(cborEcoding)); + break; + // handle the correct algorithm but mismatched hash length. + case 6: + writer = new(CborConformanceMode.Strict); + propertyCount = 2; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteInt64((long)CoseHashAlgorithm.SHA256); + writer.WriteByteString([0x01, 0x02, 0x03, 0x04]); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + Assert.ThrowsException(() => CoseHashV.Deserialize(cborEcoding)); + break; + // handle not starting with start array. + case 7: + writer = new(CborConformanceMode.Strict); + writer.Reset(); + writer.WriteInt64((long)CoseHashAlgorithm.SHA256); + cborEcoding = writer.Encode(); + Assert.ThrowsException(() => CoseHashV.Deserialize(cborEcoding)); + break; + // handle null reader + case 8: + Assert.ThrowsException(() => CoseHashV.Deserialize(reader: null)); + break; + // handle null data + case 9: + Assert.ThrowsException(() => CoseHashV.Deserialize(data: null)); + break; + // handle invalid tstr encoding of algorithm + case 10: + writer = new(CborConformanceMode.Strict); + propertyCount = 2; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteTextString("broken"); + writer.WriteByteString(SHA256.HashData([0x1, 0x2, 0x3, 0x4])); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + Assert.ThrowsException(() => CoseHashV.Deserialize(cborEcoding)); + break; + // handle a valid tstr encoding of algorithm + case 11: + writer = new(CborConformanceMode.Strict); + propertyCount = 2; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteTextString(CoseHashAlgorithm.SHA256.ToString()); + writer.WriteByteString(SHA256.HashData([0x1, 0x2, 0x3, 0x4])); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + CoseHashV properDeserialization = CoseHashV.Deserialize(cborEcoding); + properDeserialization.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + properDeserialization.HashValue.Should().BeEquivalentTo(SHA256.HashData([0x1, 0x2, 0x3, 0x4])); + properDeserialization.Location.Should().BeNull(); + properDeserialization.AdditionalData.Should().BeNull(); + break; + // handle an invalid algorithm integer negative. + case 12: + writer = new(CborConformanceMode.Strict); + propertyCount = 2; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteInt64(-100); + writer.WriteByteString(SHA256.HashData([0x1, 0x2, 0x3, 0x4])); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + Assert.ThrowsException(() => CoseHashV.Deserialize(cborEcoding)); + break; + // handle an invalid algorithm integer positive. + case 13: + writer = new(CborConformanceMode.Strict); + propertyCount = 2; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteInt64(9999); + writer.WriteByteString(SHA256.HashData([0x1, 0x2, 0x3, 0x4])); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + Assert.ThrowsException(() => CoseHashV.Deserialize(cborEcoding)); + break; + // handle an invalid algorithm integer positive, a random hash and pass the ignore validation flag. + case 14: + writer = new(CborConformanceMode.Strict); + propertyCount = 2; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteInt64(-123); + writer.WriteByteString([0x1, 0x2, 0x3, 0x4]); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + CoseHashV testObject14 = CoseHashV.Deserialize(cborEcoding, disableValidation: true); + testObject14.Algorithm.Should().Be((CoseHashAlgorithm)(-123)); + testObject14.HashValue.Should().BeEquivalentTo([0x1, 0x2, 0x3, 0x4]); + break; + // handle an valid algorithm integer positive, a random hash and pass the ignore validation flag. + case 15: + writer = new(CborConformanceMode.Strict); + propertyCount = 2; + writer.Reset(); + writer.WriteStartArray(propertyCount); + writer.WriteInt64((long)CoseHashAlgorithm.SHA256); + writer.WriteByteString([0x1, 0x2, 0x3, 0x4]); + writer.WriteEndArray(); + cborEcoding = writer.Encode(); + CoseHashV testObject15 = CoseHashV.Deserialize(cborEcoding, disableValidation: true); + testObject15.Algorithm.Should().Be(CoseHashAlgorithm.SHA256); + testObject15.HashValue.Should().BeEquivalentTo([0x1, 0x2, 0x3, 0x4]); + break; + // handle 0 bytes to ReadOnlySpan + case 16: + Action test16 = () => _ = CoseHashV.Deserialize((ReadOnlySpan)[]); + test16.Should().Throw(); + break; + // handle fuzz overflow + case 17: + byte[] fuzzData17 = Convert.FromBase64String("gjv//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////w=="); + Action test17 = () => _ = CoseHashV.Deserialize(fuzzData17); + test17.Should().Throw(); + break; + // handle fuzz enum overflow + case 18: + byte[] fuzzData18 = Convert.FromBase64String("hHc0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0Cg=="); + Action test18 = () => _ = CoseHashV.Deserialize(fuzzData18); + test18.Should().Throw(); + break; + + default: + throw new InvalidDataException($"Test case {testCase} is not defined in {nameof(TestObjectManualSerializationPaths)}"); + } + } + + [Test] + public void TestMismatchedHashAlgorithmAndHashSize() + { + // arrange + byte[] testData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + byte[] hash = SHA512.HashData(testData); + CoseHashV testObj = new CoseHashV(CoseHashAlgorithm.SHA256, byteData: hash); + Assert.ThrowsException(() => testObj.HashValue = hash); + } + + [Test] + [TestCase(1, Description = "Set invalid hash length through setter.")] + [TestCase(2, Description = "Set null through setter.")] + [TestCase(3, Description = "Set 0-length array through setter.")] + public void TestSetHashWithoutAlgorithm(int testCase) + { + // arrange + byte[] testData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + byte[] hash = SHA256.HashData(testData); + CoseHashV testObj = new CoseHashV(); + switch (testCase) + { + case 1: + Assert.ThrowsException(() => testObj.HashValue = hash); + break; + case 2: + Assert.ThrowsException(() => testObj.HashValue = null); + break; + case 3: + Assert.ThrowsException(() => testObj.HashValue = new byte[0]); + break; + default: + throw new InvalidDataException($"Test case {testCase} is not defined in {nameof(TestSetHashWithoutAlgorithm)}"); + } + } +} diff --git a/CoseDetachedSIgnature.Tests/CoseDetachedSignature.Tests.csproj b/CoseIndirectSignature.Tests/CoseIndirectSignature.Tests.csproj similarity index 92% rename from CoseDetachedSIgnature.Tests/CoseDetachedSignature.Tests.csproj rename to CoseIndirectSignature.Tests/CoseIndirectSignature.Tests.csproj index 60e9a155..1d4e1c2d 100644 --- a/CoseDetachedSIgnature.Tests/CoseDetachedSignature.Tests.csproj +++ b/CoseIndirectSignature.Tests/CoseIndirectSignature.Tests.csproj @@ -22,7 +22,7 @@ - + @@ -36,7 +36,7 @@ - + diff --git a/CoseIndirectSignature.Tests/CoseSign1MessageIndirectSignatureExtensionsTests.cs b/CoseIndirectSignature.Tests/CoseSign1MessageIndirectSignatureExtensionsTests.cs new file mode 100644 index 00000000..eaa7363e --- /dev/null +++ b/CoseIndirectSignature.Tests/CoseSign1MessageIndirectSignatureExtensionsTests.cs @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ignore Spelling: Cose + +namespace CoseIndirectSignature.Tests; + +using System.IO; +using CoseIndirectSignature; +using CoseIndirectSignature.Exceptions; +using CoseIndirectSignature.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Class for Testing Methods of +/// +public class CoseSign1MessageIndirectSignatureExtensionsTests +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void TestTryGetIndirectSignatureAlgorithmSuccess() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetIndirectSignatureAlgorithmSuccess)); + IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + + CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", useOldFormat: true); + IndirectSignature.TryGetIndirectSignatureAlgorithm(out HashAlgorithmName hashAlgorithmName).Should().BeTrue(); + hashAlgorithmName.Should().Be(HashAlgorithmName.SHA256); + } + + [Test] + public void TestTryGetIndirectSignatureAlgorithmFailure() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetIndirectSignatureAlgorithmFailure)); + + // no content type + Mock removeContentTypeHeaderExtender = new Mock(MockBehavior.Strict); + removeContentTypeHeaderExtender.Setup(m => m.ExtendProtectedHeaders(It.IsAny())).Returns((input) => + { + // remove ContentType + if (input.ContainsKey(CoseHeaderLabel.ContentType)) + { + input.Remove(CoseHeaderLabel.ContentType); + } + return input; + }); + removeContentTypeHeaderExtender.Setup(m => m.ExtendUnProtectedHeaders(It.IsAny())).Returns((input) => input); + + // empty content type + Mock emptyContentTypeHeaderExtender = new Mock(MockBehavior.Strict); + emptyContentTypeHeaderExtender.Setup(m => m.ExtendProtectedHeaders(It.IsAny())).Returns((input) => + { + // remove content type + input = removeContentTypeHeaderExtender.Object.ExtendProtectedHeaders(input); + + // add content type with an empty string value + input.Add(CoseHeaderLabel.ContentType, string.Empty); + + return input; + }); + emptyContentTypeHeaderExtender.Setup(m => m.ExtendUnProtectedHeaders(It.IsAny())).Returns((input) => input); + + IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + + // call TryGetIndirectSignatureAlgoithm on a null message object should fail + CoseSign1Message? objectUnderTest = null; + objectUnderTest.TryGetIndirectSignatureAlgorithm(out HashAlgorithmName hashAlgorithmName).Should().BeFalse(); + + // call TryGetIndirectSignatureAlgorithm on a CoseSign1Message with no ContentType header should fail + objectUnderTest = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, @"application\test.output", removeContentTypeHeaderExtender.Object); + objectUnderTest.TryGetIndirectSignatureAlgorithm(out hashAlgorithmName).Should().BeFalse("missing content type should fail the test"); + + // call TryGetIndirectSignatureAlgorithm on a CoseSign1Message with empty ContentType should fail + objectUnderTest = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, @"application\test.output", emptyContentTypeHeaderExtender.Object); + objectUnderTest.TryGetIndirectSignatureAlgorithm(out hashAlgorithmName).Should().BeFalse("empty content type should fail the test"); + + // call TryGetIndirectSignatureAlgorithm on a CoseSign1Message with invalid mime type hash extension ContentType should fail + objectUnderTest = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, @"application\test.output"); + objectUnderTest.TryGetIndirectSignatureAlgorithm(out hashAlgorithmName).Should().BeFalse("missing mime type hash extension in content type should fail the test"); + + // call TryGetIndirectSignatureAlgorithm on a CoseSign1Message with invalid mime type hash extension ContentType should succeed + objectUnderTest = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, @"application\test.output+hash-notavalidvalue"); + objectUnderTest.TryGetIndirectSignatureAlgorithm(out hashAlgorithmName).Should().BeTrue("invalid mime type hash extension in content type should not fail this call"); + } + + [Test] + public void TestIsIndirectSignatureSuccess() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestIsIndirectSignatureSuccess)); + IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + + CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature.IsIndirectSignature().Should().BeTrue(); + } + + [Test] + public void TestIsIndirectSignatureFailure() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestIsIndirectSignatureFailure)); + IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + + CoseSign1Message IndirectSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload"); + IndirectSignature.IsIndirectSignature().Should().BeFalse(); + } + + [Test] + public void TestSignatureMatchesStreamSuccess() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestSignatureMatchesStreamSuccess)); + IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + using MemoryStream stream = new(randomBytes); + + CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature.SignatureMatches(stream).Should().BeTrue(); + } + + [Test] + public void TestSignatureMatchesStreamFailure() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestSignatureMatchesStreamFailure)); + IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + byte[] randomBytes2 = new byte[50]; + new Random().NextBytes(randomBytes); + new Random().NextBytes(randomBytes2); + using MemoryStream stream = new(randomBytes2); + + // test mismatched signature + CoseSign1Message? IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature.SignatureMatches(stream).Should().BeFalse(); + stream.Dispose(); + using MemoryStream stream2 = new(randomBytes); + + // test invalid hash extension case + IndirectSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload"); + IndirectSignature.SignatureMatches(stream2).Should().BeFalse(); + stream2.Seek(stream2.Length, SeekOrigin.Begin); + + // test null object case + IndirectSignature = null; + CoseSign1MessageIndirectSignatureExtensions.SignatureMatches(IndirectSignature, stream2).Should().BeFalse(); + } + + [Test] + public void TestSignatureMatchesBytesSuccess() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestSignatureMatchesBytesSuccess)); + IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + + CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature.SignatureMatches(randomBytes).Should().BeTrue(); + } + + [Test] + public void TestSignatureMatchesBytesFailure() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestSignatureMatchesBytesFailure)); + IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + byte[] randomBytes2 = new byte[50]; + new Random().NextBytes(randomBytes); + new Random().NextBytes(randomBytes2); + + // test mismatched signature + CoseSign1Message? IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature.SignatureMatches(randomBytes2).Should().BeFalse(); + + // test invalid hash extension case + IndirectSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload"); + IndirectSignature.SignatureMatches(randomBytes).Should().BeFalse(); + + // test null object case + IndirectSignature = null; + CoseSign1MessageIndirectSignatureExtensions.SignatureMatches(IndirectSignature, randomBytes).Should().BeFalse(); + } + + [Test] + public void TestTryGetHashAlgorithmSuccess() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetHashAlgorithmSuccess)); + IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + + CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", useOldFormat: true); + IndirectSignature.TryGetHashAlgorithm(out HashAlgorithm? hashAlgorithm).Should().BeTrue(); + hashAlgorithm.Should().NotBeNull(); + hashAlgorithm.Should().BeAssignableTo(); + } + + [Test] + [TestCase(1, Description = "Success return")] + [TestCase(2, Description = "Invalid CoseHashV - ContentType")] + [TestCase(3, Description = "Invalid CoseHashV - detached signature")] + [TestCase(4, Description = "Invalid CoseHshV - invalid content")] + [TestCase(5, Description = "Success TryGet")] + [TestCase(6, Description = "Failure TryGet")] + [TestCase(7, Description = "Get - Null")] + [TestCase(8, Description = "TryGet - Null")] + public void TestGetCoseHashVScenarios(int testCase) + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestSignatureMatchesBytesFailure)); + IndirectSignatureFactory signaturefactory = new(); + CoseSign1MessageFactory messageFactory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + + switch (testCase) + { + // test the fetching case + case 1: + CoseSign1Message? testObj1 = signaturefactory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + CoseHashV hashObject = testObj1.GetCoseHashV(); + hashObject.ContentMatches(randomBytes).Should().BeTrue(); + break; + // test the invalid content type case. + case 2: + CoseSign1Message? testObj2 = messageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload+hash-sha256"); + Action test2 = () => testObj2.GetCoseHashV(); + test2.Should().Throw(); + break; + // test detached signature. + case 3: + CoseSign1Message? testObj3 = messageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: false, "application/test.payload+hash-sha256"); + Action test3 = () => testObj3.GetCoseHashV(); + test3.Should().Throw(); + break; + // test invalid content + case 4: + CoseSign1Message? testObj4 = messageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload+cose-hash-v"); + Action test4 = () => testObj4.GetCoseHashV(); + test4.Should().Throw(); + break; + // tryget success + case 5: + CoseSign1Message? testObj5 = signaturefactory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + testObj5.TryGetCoseHashV(out CoseHashV? hashObject5).Should().BeTrue(); + hashObject5.ContentMatches(randomBytes).Should().BeTrue(); + break; + // tryget failure + case 6: + CoseSign1Message? testObj6 = messageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: false, "application/test.payload+hash-sha256"); + testObj6.TryGetCoseHashV(out _).Should().BeFalse(); + break; + // get null + case 7: +#nullable disable + Action test7 = () => ((CoseSign1Message)null).GetCoseHashV(); +#nullable enable + test7.Should().Throw(); + break; + // tryget null + case 8: +#nullable disable + ((CoseSign1Message)null).TryGetCoseHashV(out _).Should().BeFalse(); +#nullable enable + break; + default: + throw new InvalidDataException($"TestCase {testCase} is not defined in {nameof(TestGetCoseHashVScenarios)}"); + } + } + + [Test] + public void TestTryGetHashAlgorithmFailure() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestTryGetHashAlgorithmFailure)); + IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + byte[] randomBytes2 = new byte[50]; + new Random().NextBytes(randomBytes); + new Random().NextBytes(randomBytes2); + + // Fail to extract a hash algorithm name + CoseSign1Message? IndirectSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload"); + IndirectSignature.TryGetHashAlgorithm(out HashAlgorithm? hashAlgorithm).Should().BeFalse(); + hashAlgorithm.Should().BeNull(); + + // COSE Sign1 Indirect signature case with other things being valid + // content should be null in this case. + IndirectSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: false, "application/test.payload+hash-sha256"); + IndirectSignature.TryGetHashAlgorithm(out hashAlgorithm).Should().BeFalse(); + hashAlgorithm.Should().BeNull(); + + // Invalid hash definition case + IndirectSignature = factory.MessageFactory.CreateCoseSign1Message(randomBytes, coseSigningKeyProvider, embedPayload: true, "application/test.payload+hash-notavalidhashalgorithm"); + IndirectSignature.TryGetHashAlgorithm(out hashAlgorithm).Should().BeFalse(); + hashAlgorithm.Should().BeNull(); + + // test null object case + IndirectSignature = null; + CoseSign1MessageIndirectSignatureExtensions.TryGetHashAlgorithm(IndirectSignature, out hashAlgorithm).Should().BeFalse(); + hashAlgorithm.Should().BeNull(); + } + + private ICoseSigningKeyProvider SetupMockSigningKeyProvider(string testName) + { + Mock mockedSignerKeyProvider = new(MockBehavior.Strict); + X509Certificate2 selfSignedCertWithRSA = TestCertificateUtils.CreateCertificate(testName); + + mockedSignerKeyProvider.Setup(x => x.GetProtectedHeaders()).Returns(null); + mockedSignerKeyProvider.Setup(x => x.GetUnProtectedHeaders()).Returns(null); + mockedSignerKeyProvider.Setup(x => x.HashAlgorithm).Returns(HashAlgorithmName.SHA256); + mockedSignerKeyProvider.Setup(x => x.GetECDsaKey(It.IsAny())).Returns(null); + mockedSignerKeyProvider.Setup(x => x.GetRSAKey(It.IsAny())).Returns(selfSignedCertWithRSA.GetRSAPrivateKey()); + mockedSignerKeyProvider.Setup(x => x.IsRSA).Returns(true); + + return mockedSignerKeyProvider.Object; + } +} diff --git a/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs b/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs new file mode 100644 index 00000000..aaedbb51 --- /dev/null +++ b/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs @@ -0,0 +1,260 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseIndirectSignature.Tests; + +/// +/// Class for Testing Methods of +/// +public class IndirectSignatureFactoryTests +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void TestConstructors() + { + Mock mockFactory = new(MockBehavior.Strict); + using IndirectSignatureFactory factory = new(); + using IndirectSignatureFactory factory2 = new(HashAlgorithmName.SHA384); + using IndirectSignatureFactory factory3 = new(HashAlgorithmName.SHA512, mockFactory.Object); + + factory.HashAlgorithm.Should().BeAssignableTo(); + factory.HashAlgorithmName.Should().Be(HashAlgorithmName.SHA256); + factory.MessageFactory.Should().BeOfType(); + + factory2.HashAlgorithm.Should().BeAssignableTo(); + factory2.HashAlgorithmName.Should().Be(HashAlgorithmName.SHA384); + factory2.MessageFactory.Should().BeOfType(); + + factory3.HashAlgorithm.Should().BeAssignableTo(); + factory3.HashAlgorithmName.Should().Be(HashAlgorithmName.SHA512); + factory3.MessageFactory.Should().Be(mockFactory.Object); + } + + [Test] + public async Task TestCreateIndirectSignatureAsync() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateIndirectSignatureAsync)); + using IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + using MemoryStream memStream = new(randomBytes); + + // test the sync method + Assert.Throws(() => factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, string.Empty)); + CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.Throws(() => factory.CreateIndirectSignature(memStream, coseSigningKeyProvider, string.Empty)); + memStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message IndirectSignature2 = factory.CreateIndirectSignature(memStream, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature2.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature2.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature2.SignatureMatches(randomBytes).Should().BeTrue(); + memStream.Seek(0, SeekOrigin.Begin); + + // test the async methods + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureAsync(randomBytes, coseSigningKeyProvider, string.Empty)); + CoseSign1Message IndirectSignature3 = await factory.CreateIndirectSignatureAsync(randomBytes, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature3.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature3.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature3.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureAsync(memStream, coseSigningKeyProvider, string.Empty)); + memStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message IndirectSignature4 = await factory.CreateIndirectSignatureAsync(memStream, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature4.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature4.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature4.SignatureMatches(randomBytes).Should().BeTrue(); + memStream.Seek(0, SeekOrigin.Begin); + } + + [Test] + public async Task TestCreateIndirectSignatureHashProvidedAsync() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateIndirectSignatureHashProvidedAsync)); + using IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + using HashAlgorithm hasher = CoseSign1MessageIndirectSignatureExtensions.CreateHashAlgorithmFromName(factory.HashAlgorithmName) + ?? throw new Exception($"Failed to get hash algorithm from {nameof(CoseSign1MessageIndirectSignatureExtensions.CreateHashAlgorithmFromName)}"); + byte[] hash = hasher!.ComputeHash(randomBytes); + using MemoryStream hashStream = new(hash); + + // test the sync method + Assert.Throws(() => factory.CreateIndirectSignatureFromHash(hash, coseSigningKeyProvider, string.Empty)); + CoseSign1Message IndirectSignature = factory.CreateIndirectSignatureFromHash(hash, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.Throws(() => factory.CreateIndirectSignature(hashStream, coseSigningKeyProvider, string.Empty)); + hashStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message IndirectSignature2 = factory.CreateIndirectSignatureFromHash(hashStream, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature2.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature2.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature2.SignatureMatches(randomBytes).Should().BeTrue(); + hashStream.Seek(0, SeekOrigin.Begin); + + // test the async methods + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureFromHashAsync(hash, coseSigningKeyProvider, string.Empty)); + CoseSign1Message IndirectSignature3 = await factory.CreateIndirectSignatureFromHashAsync(hash, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature3.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature3.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature3.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureFromHashAsync(hashStream, coseSigningKeyProvider, string.Empty)); + hashStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message IndirectSignature4 = await factory.CreateIndirectSignatureFromHashAsync(hashStream, coseSigningKeyProvider, "application/test.payload"); + IndirectSignature4.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature4.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature4.SignatureMatches(randomBytes).Should().BeTrue(); + hashStream.Seek(0, SeekOrigin.Begin); + } + + [Test] + public async Task TestCreateIndirectSignatureBytesAsync() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateIndirectSignatureBytesAsync)); + using IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + using MemoryStream memStream = new(randomBytes); + + // test the sync method + Assert.Throws(() => factory.CreateIndirectSignatureBytes(randomBytes, coseSigningKeyProvider, string.Empty)); + CoseSign1Message IndirectSignature = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytes(randomBytes, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignature.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.Throws(() => factory.CreateIndirectSignatureBytes(memStream, coseSigningKeyProvider, string.Empty)); + memStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message IndirectSignature2 = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytes(memStream, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignature2.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature2.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature2.SignatureMatches(randomBytes).Should().BeTrue(); + memStream.Seek(0, SeekOrigin.Begin); + + // test the async methods + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureBytesAsync(randomBytes, coseSigningKeyProvider, string.Empty)); + CoseSign1Message IndirectSignature3 = CoseMessage.DecodeSign1((await factory.CreateIndirectSignatureBytesAsync(randomBytes, coseSigningKeyProvider, "application/test.payload")).ToArray()); + IndirectSignature3.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature3.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature3.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureBytesAsync(memStream, coseSigningKeyProvider, string.Empty)); + memStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message IndirectSignature4 = CoseMessage.DecodeSign1((await factory.CreateIndirectSignatureBytesAsync(memStream, coseSigningKeyProvider, "application/test.payload")).ToArray()); + IndirectSignature4.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature4.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + memStream.Seek(0, SeekOrigin.Begin); + IndirectSignature4.SignatureMatches(memStream).Should().BeTrue(); + } + + [Test] + public async Task TestCreateIndirectSignatureBytesHashProvidedAsync() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateIndirectSignatureBytesHashProvidedAsync)); + using IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + using HashAlgorithm hasher = CoseSign1MessageIndirectSignatureExtensions.CreateHashAlgorithmFromName(factory.HashAlgorithmName) + ?? throw new Exception($"Failed to get hash algorithm from {nameof(CoseSign1MessageIndirectSignatureExtensions.CreateHashAlgorithmFromName)}"); + byte[] hash = hasher!.ComputeHash(randomBytes); + using MemoryStream hashStream = new(hash); + + // test the sync method + Assert.Throws(() => factory.CreateIndirectSignatureBytesFromHash(hash, coseSigningKeyProvider, string.Empty)); + CoseSign1Message IndirectSignature = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytesFromHash(hash, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignature.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.Throws(() => factory.CreateIndirectSignatureBytesFromHash(hashStream, coseSigningKeyProvider, string.Empty)); + hashStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message IndirectSignature2 = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytesFromHash(hashStream, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignature2.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature2.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature2.SignatureMatches(randomBytes).Should().BeTrue(); + hashStream.Seek(0, SeekOrigin.Begin); + + // test the async methods + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureBytesFromHashAsync(hash, coseSigningKeyProvider, string.Empty)); + CoseSign1Message IndirectSignature3 = CoseMessage.DecodeSign1((await factory.CreateIndirectSignatureBytesFromHashAsync(hash, coseSigningKeyProvider, "application/test.payload")).ToArray()); + IndirectSignature3.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature3.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature3.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureBytesFromHashAsync(hashStream, coseSigningKeyProvider, string.Empty)); + hashStream.Seek(0, SeekOrigin.Begin); + CoseSign1Message IndirectSignature4 = CoseMessage.DecodeSign1((await factory.CreateIndirectSignatureBytesFromHashAsync(hashStream, coseSigningKeyProvider, "application/test.payload")).ToArray()); + IndirectSignature4.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature4.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + hashStream.Seek(0, SeekOrigin.Begin); + IndirectSignature4.SignatureMatches(randomBytes).Should().BeTrue(); + } + + [Test] + public void TestCreateIndirectSignatureMd5Failure() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateIndirectSignatureMd5Failure)); + Action act = () => { IndirectSignatureFactory factory = new(HashAlgorithmName.MD5); }; + act.Should().Throw(); + } + + [Test] + public void TestCreateIndirectSignatureMd5HashProvidedFailure() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateIndirectSignatureMd5HashProvidedFailure)); + using IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + using HashAlgorithm hasher = CoseSign1MessageIndirectSignatureExtensions.CreateHashAlgorithmFromName(HashAlgorithmName.MD5) + ?? throw new Exception($"Failed to get hash algorithm from {nameof(CoseSign1MessageIndirectSignatureExtensions.CreateHashAlgorithmFromName)}"); + byte[] hash = hasher!.ComputeHash(randomBytes); + + // test the sync method + Assert.Throws(() => factory.CreateIndirectSignatureFromHash(hash, coseSigningKeyProvider, string.Empty)); + Assert.Throws(() => factory.CreateIndirectSignatureBytesFromHash(hash, coseSigningKeyProvider, "application/test.payload")); + } + + [Test] + public void TestCreateIndirectSignatureAlreadyProvided() + { + ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(nameof(TestCreateIndirectSignatureAlreadyProvided)); + using IndirectSignatureFactory factory = new(); + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + using HashAlgorithm hasher = CoseSign1MessageIndirectSignatureExtensions.CreateHashAlgorithmFromName(factory.HashAlgorithmName) + ?? throw new Exception($"Failed to get hash algorithm from {nameof(CoseSign1MessageIndirectSignatureExtensions.CreateHashAlgorithmFromName)}"); + ReadOnlyMemory hash = hasher!.ComputeHash(randomBytes); + + // test the sync method + Assert.Throws(() => factory.CreateIndirectSignature(hash, coseSigningKeyProvider, string.Empty)); + CoseSign1Message IndirectSignature = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytes(randomBytes, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignature.ProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType).Should().BeTrue(); + IndirectSignature.ProtectedHeaders[CoseHeaderLabel.ContentType].GetValueAsString().Should().Be("application/test.payload+cose-hash-v"); + IndirectSignature.SignatureMatches(randomBytes).Should().BeTrue(); + } + + private ICoseSigningKeyProvider SetupMockSigningKeyProvider(string testName) + { + Mock mockedSignerKeyProvider = new(MockBehavior.Strict); + X509Certificate2 selfSignedCertWithRSA = TestCertificateUtils.CreateCertificate(testName); + + mockedSignerKeyProvider.Setup(x => x.GetProtectedHeaders()).Returns(null); + mockedSignerKeyProvider.Setup(x => x.GetUnProtectedHeaders()).Returns(null); + mockedSignerKeyProvider.Setup(x => x.HashAlgorithm).Returns(HashAlgorithmName.SHA256); + mockedSignerKeyProvider.Setup(x => x.GetECDsaKey(It.IsAny())).Returns(null); + mockedSignerKeyProvider.Setup(x => x.GetRSAKey(It.IsAny())).Returns(selfSignedCertWithRSA.GetRSAPrivateKey()); + mockedSignerKeyProvider.Setup(x => x.IsRSA).Returns(true); + + return mockedSignerKeyProvider.Object; + } +} diff --git a/CoseDetachedSIgnature.Tests/Usings.cs b/CoseIndirectSignature.Tests/Usings.cs similarity index 84% rename from CoseDetachedSIgnature.Tests/Usings.cs rename to CoseIndirectSignature.Tests/Usings.cs index 9b3cc5ba..85677a11 100644 --- a/CoseDetachedSIgnature.Tests/Usings.cs +++ b/CoseIndirectSignature.Tests/Usings.cs @@ -4,8 +4,8 @@ global using System.Security.Cryptography; global using System.Security.Cryptography.Cose; global using System.Security.Cryptography.X509Certificates; -global using CoseDetachedSignature; -global using CoseDetachedSignature.Extensions; +global using CoseIndirectSignature; +global using CoseIndirectSignature.Extensions; global using CoseSign1; global using CoseSign1.Abstractions.Interfaces; global using CoseSign1.Interfaces; diff --git a/CoseDetachedSignature.md b/CoseIndirectSignature.md similarity index 52% rename from CoseDetachedSignature.md rename to CoseIndirectSignature.md index 90a2bc33..f2b76069 100644 --- a/CoseDetachedSignature.md +++ b/CoseIndirectSignature.md @@ -1,37 +1,37 @@ -# [CoseDetachedSignature](https://github.com/microsoft/CoseSignTool/tree/main/CoseDetachedSignature) -**CoseDetachedSignature** is a .NET Standard 2.0 library containing a concrete implementation which embeds the hash of an object into the .Content of a CoseSign1Message object and updates the ContentType field to include a new content type extension of `+hash-{algorithm}` to indicate the content is a hash of the original content type. This functionality is exposed via a factory pattern in [**DetachedSignatureFactory**](https://github.com/microsoft/CoseSignTool/tree/main/CoseDetachedSignature/CoseSignatureFactory.cs) for use with Supply Chain Integrity Transparency and Trust [SCITT](https://scitt.io/). +# [CoseIndirectSignature](https://github.com/microsoft/CoseSignTool/tree/main/CoseIndirectSignature) +**CoseIndirectSignature** is a .NET Standard 2.0 library containing a concrete implementation which embeds the hash of an object into the .Content of a CoseSign1Message object and updates the ContentType field to include a new content type extension of `+cose-hash-v` to indicate the content is a cose_hash_v structure of the original content type. This functionality is exposed via a factory pattern in [**IndirectSignatureFactory**](https://github.com/microsoft/CoseSignTool/tree/main/CoseIndirectSignature/IndirectSignatureFactory.cs) for use with Supply Chain Integrity Transparency and Trust [SCITT](https://scitt.io/). ## Dependencies -**CoseDetachedSignature** has the following package dependencies +**CoseIndirecSignature** has the following package dependencies * CoseSign1 ## Creation This library includes the following classes: -### [**DetachedSignatureFactory**](https://github.com/microsoft/CoseSignTool/tree/main/CoseDetachedSignature/DetachedSignatureFactory.cs) -This class implements the creation of a CoseSign1Message object leveraging CoseSign1 which conforms to an embedded DetachedSignature format for content which is needed to be submitted to SCITT for receipt generation. +### [**IndirectSignatureFactory**](https://github.com/microsoft/CoseSignTool/tree/main/CoseIndirectSignature/IndirectSignatureFactory.cs) +This class implements the creation of a CoseSign1Message object leveraging CoseSign1 which conforms to an embedded IndirectSignature format for content which is needed to be submitted to SCITT for receipt generation. There are various `Create*` methods which support both synchronous and asynchronous operations. #### Example ``` -using CoseDetachedSignature; +using CoseIndirectSignature; using CoseSign1; using CoseSign1.Certificates.Local; ... -using DetachedSignatureFactory factory = new(); +using IndirectSignatureFactory factory = new(); byte[] randomBytes = new byte[50]; new Random().NextBytes(randomBytes); using MemoryStream memStream = new(randomBytes); X509Certificate2CoseSigningKeyProvider coseSigningKeyProvider = new(...); -CoseSign1Message detachedSignature = factory.CreateDetachedSignature(payload: randomBytes, signingKeyProvider: coseSigningKeyProvider, contentType: "application/test.payload"); +CoseSign1Message indirectSignature = factory.CreateIndirectSignature(payload: randomBytes, signingKeyProvider: coseSigningKeyProvider, contentType: "application/test.payload"); ``` ## Validation -To help with validation of DetachedSignatures which are embedded within a CoseSign1Message object, [CoseSign1MessageDetachedSignatureExtensions](https://github.com/microsoft/CoseSignTool/tree/main/CoseDetachedSignature/Extensions/CoseSign1MessageDetachedSignatureExtensions.cs) C# extension class is provided to add a `SignatureMatches(...)` overload that accepts **Stream** or **Byte[]** content. +To help with validation of IndirectSignatures which are embedded within a CoseSign1Message object, [CoseSign1MessageIndirectSignatureExtensions](https://github.com/microsoft/CoseSignTool/tree/main/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs) C# extension class is provided to add a `SignatureMatches(...)` overload that accepts **Stream** or **Byte[]** content. #### Example: ``` -using CoseDetachedSignature.Extensions; +using CoseIndirectSignature.Extensions; using CoseSign1; using CoseSign1.Certificates.Local; using System.IO; @@ -41,7 +41,7 @@ using System.IO; Stream coseFileStream = File.OpenRead(...); Stream originalContentStream = File.OpenRead(...); CoseSign1Message message = CoseMessage.DecodeSign1(coseFileStream); -if(message.IsDetachedSignature()) +if(message.IsIndirectSignature()) { return message.SignatureMatches(originalContentStream); } diff --git a/CoseIndirectSignature/CoseAlgorithms.cs b/CoseIndirectSignature/CoseAlgorithms.cs new file mode 100644 index 00000000..8f8dba36 --- /dev/null +++ b/CoseIndirectSignature/CoseAlgorithms.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ignore Spelling: Cose + +namespace CoseIndirectSignature; + +using System.ComponentModel; + +/// +/// COSE HashAlgorithm values from the IANA COSE Algorithms registry found at https://www.iana.org/assignments/cose/cose.xhtml#algorithms +/// +public enum CoseHashAlgorithm : long +{ + /// + /// Reserved + /// + [Browsable(false)] + Reserved = 0, + /// + /// SHA-1 Hash Algorithm + /// + /// SHA1 is not recommended for new data. + [Obsolete("Use CoseAlgorithm.SHA256 instead")] + SHA1 = -14, + /// + /// SHA-256 Truncated to 64 bits Hash Algorithm + /// + /// SHA256 truncated to 64 bits is not recommended for new data. + [Obsolete("Use CoseAlgorithm.SHA256 instead")] + SHA256Trunc64 = -15, + /// + /// SHA-256 Hash Algorithm + /// + SHA256 = -16, + /// + /// SHA-512 Truncated to 256 bits Hash Algorithm + /// + SHA512Truc256 = -17, + /// + /// SHAKE128 Hash Algorithm + /// + SHAKE128 = -18, + /// + /// SHA384 Hash Algorithm + /// + SHA384 = -43, + /// + /// SHA512 Hash Algorithm + /// + SHA512 = -44, + /// + /// SHAKE256 Hash Algorithm + /// + SHAKE256 = -45, +} diff --git a/CoseIndirectSignature/CoseHashV.cs b/CoseIndirectSignature/CoseHashV.cs new file mode 100644 index 00000000..29d35dca --- /dev/null +++ b/CoseIndirectSignature/CoseHashV.cs @@ -0,0 +1,562 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ignore Spelling: Cose Deserialize + +namespace CoseIndirectSignature; +using System; +using System.Formats.Cbor; +using System.IO; +using System.Threading.Tasks; +using CoseIndirectSignature.Exceptions; +using CoseSign1.Abstractions.Exceptions; + +/// +/// Represents the COSE_Hash_V structure as suggested in https://tools.ietf.org/html/rfc9054#section-2.1 +/// +public record CoseHashV +{ + /// + /// The hash algorithm used to generate the hash value. + /// + public CoseHashAlgorithm Algorithm { get; set; } + + private byte[]? InternalHashValue; + /// + /// The actual value from the hashing function + /// + public byte[] HashValue + { + get + { + return InternalHashValue ?? Array.Empty(); + } + set + { + // validate the input + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + if (value.Length == 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "The hash value cannot be empty."); + } + if (Algorithm == CoseHashAlgorithm.Reserved) + { + throw new ArgumentException("The algorithm must be set before the hash can be stored.", nameof(value)); + } + + // sanity check the length of the hash against the specified algorithm to be sure we're not allowing a mismatch. + HashAlgorithm algo = 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}"); + } + InternalHashValue = value; + } + } + + /// + /// Optional location of object that was hashed + /// + public string? Location { get; set; } + + /// + /// Optional array containing other details meaningful to the application. + /// + public byte[]? AdditionalData { get; set; } + + /// + /// Default constructor for the CoseHashV class. + /// + public CoseHashV() + { + } + + /// + /// Copy constructor for the CoseHashV class. + /// + /// The other to copy. + public CoseHashV(CoseHashV other) + { + Algorithm = other.Algorithm; + // deep copy the hash value over + InternalHashValue = new byte[other.HashValue.Length]; + other.HashValue.CopyTo(InternalHashValue, 0); + + // copy the location string + Location = other.Location; + + // deep copy the additional data over if present. + if (other.AdditionalData != null) + { + AdditionalData = new byte[other.AdditionalData.Length]; + other.AdditionalData.CopyTo(AdditionalData, 0); + } + } + + /// + /// Constructor for the CoseHashV class which takes a hash algorithm and a hash value. + /// + /// The CoseHashAlgorithm to be used for CoseHashV. + /// The hash value to be present. + /// True to disable the checks which ensure the decoded algorithm expected hash length and the length of the decoded hash match, False (default) to leave them enabled. + public CoseHashV( + CoseHashAlgorithm algorithm, + byte[] hashValue, + bool disableValidation = false) + : this(algorithm, null, null) + { + _ = hashValue ?? throw new ArgumentNullException(nameof(hashValue)); + if(hashValue.Length == 0) + { + throw new ArgumentOutOfRangeException(nameof(hashValue), "Hash value provided must contain > 0 elements."); + } + + if (disableValidation) + { + // bypass the property setter to avoid validation against the algorithm. + InternalHashValue = hashValue; + } + else + { + // use the property setter to validate the hash value against the algorithm verses directly assigning InternalHashValue. + HashValue = hashValue; + } + } + + /// + /// Creates a CoseHashV object from byte[] of data. + /// + /// The CoseHashAlgorithm to be used for CoseHashV. + /// The data to be hashed. + /// The optional location of the content represented by this hash. + /// The optional additional information. + public CoseHashV( + CoseHashAlgorithm algorithm, + byte[] byteData, + string? location = null, + byte[]? additionalData = null) + : this(algorithm, location, additionalData) + { + _= byteData ?? throw new ArgumentNullException(nameof(byteData)); + if(byteData.Length == 0) + { + throw new ArgumentOutOfRangeException(nameof(byteData), "The data to be hashed cannot be empty."); + } + using HashAlgorithm hashAlgorightm = GetHashAlgorithmFromCoseHashAlgorithm(algorithm); + // bypass the property setter since we are computing the hash value based on the algorithm directly. + InternalHashValue = hashAlgorightm.ComputeHash(byteData); + } + + /// + /// Creates a CoseHashV object from ReadOnlyMemory{byte} of data. + /// + /// The CoseHashAlgorithm to be used for CoseHashV. + /// The data to be hashed. + /// The optional location of the content represented by this hash. + /// The optional additional information. + public CoseHashV( + CoseHashAlgorithm algorithm, + ReadOnlyMemory readonlyData, + string? location = null, + ReadOnlyMemory? additionalData = null) + : this(algorithm, byteData: readonlyData.ToArray(), location, additionalData?.ToArray()) + { + } + + /// + /// Creates a CoseHashV object from a Stream of data. + /// + /// The CoseHashAlgorithm to be used for CoseHashV. + /// The data to be hashed. + /// The optional location of the content represented by this hash. + /// The optional additional information. + public CoseHashV( + CoseHashAlgorithm algorithm, + Stream streamData, + string? location = null, + byte[]? additionalData = null) + : this(algorithm, location, additionalData) + { + _= streamData ?? throw new ArgumentNullException(nameof(streamData)); + + using HashAlgorithm hashAlgorightm = GetHashAlgorithmFromCoseHashAlgorithm(algorithm); + // bypass the property setter since we are computing the hash value based on the algorithm directly. + InternalHashValue = hashAlgorightm.ComputeHash(streamData); + } + + /// + /// Private constructor to share and consolidate initialization code. + /// + /// The CoseHashAlgorithm to be used for CoseHashV. + /// The optional location of the content represented by this hash. + /// The optional additional information. + private CoseHashV( + CoseHashAlgorithm algorithm, + string? location = null, + byte[]? additionalData = null) + { + Algorithm = algorithm; + Location = location; + AdditionalData = additionalData; + } + + /// + /// Validates that the given data stored in the stream matches the hash value stored in this instance. + /// + /// The data bytes to check to match the hash. + /// True if the hash of matches HashBytes, False otherwise. + /// Thrown if data passed in is null or has a length of 0. + /// Thrown if the computed hash length and the stored hash length differ. + public Task ContentMatchesAsync(Stream stream) + => Task.FromResult(HashMatches(data: null, stream: stream)); + + /// + /// Validates that the given data stored in the stream matches the hash value stored in this instance. + /// + /// The data bytes to check to match the hash. + /// True if the hash of matches HashBytes, False otherwise. + /// Thrown if data passed in is null or has a length of 0. + /// Thrown if the computed hash length and the stored hash length differ. + public bool ContentMatches(Stream stream) + => HashMatches(data: null, stream: stream); + + /// + /// Validates that the given data in bytes matches the hash value stored in this instance. + /// + /// The data bytes to check to match the hash. + /// True if the hash of matches HashBytes, False otherwise. + /// Thrown if data passed in is null or has a length of 0. + /// Thrown if the computed hash length and the stored hash length differ. + public Task ContentMatchesAsync(byte[] data) + => Task.FromResult(HashMatches(data: data, stream: null)); + + /// + /// Validates that the given data in bytes matches the hash value stored in this instance. + /// + /// The data bytes to check to match the hash. + /// True if the hash of matches HashBytes, False otherwise. + /// Thrown if data passed in is null or has a length of 0. + /// Thrown if the computed hash length and the stored hash length differ. + public bool ContentMatches(ReadOnlyMemory data) + => HashMatches(data: data.ToArray(), stream: null); + + /// + /// Validates that the given data in bytes matches the hash value stored in this instance. + /// + /// The data bytes to check to match the hash. + /// True if the hash of matches HashBytes, False otherwise. + /// Thrown if data passed in is null or has a length of 0. + /// Thrown if the computed hash length and the stored hash length differ. + public Task ContentMatchesAsync(ReadOnlyMemory data) + => Task.FromResult(HashMatches(data: data.ToArray(), stream: null)); + + /// + /// Validates that the given data in bytes matches the hash value stored in this instance. + /// + /// The data bytes to check to match the hash. + /// True if the hash of matches HashBytes, False otherwise. + /// Thrown if data passed in is null or has a length of 0. + /// Thrown if the computed hash length and the stored hash length differ. + public bool ContentMatches(byte[] data) + => HashMatches(data: data, stream: null); + + /// + /// Method for handling byte[] and stream for the same logic. + /// + /// if specified, then will compute a hash of this data and compare to internal hash value. + /// if data is null and stream is specified, then will compute a hash of this stream and compare to internal hash value. + /// True if the hashes match, False otherwise. + /// Thrown if data is null or data length is 0 and stream is null, or if data is null and stream is null. + /// Thrown if the length of the computed hash does not match the internal stored hash length, thus the wrong hash algorithm is being used. + private bool HashMatches(byte[]? data, Stream? stream) + { + // handle input validation + if ( + (data == null || data.Length == 0) && + (stream == null)) + { + throw new ArgumentNullException(nameof(data)); + } + + // initialize and compute the hash + using HashAlgorithm hashAlgorithm = GetHashAlgorithmFromCoseHashAlgorithm(Algorithm); + byte[] hash = stream != null ? hashAlgorithm.ComputeHash(stream) : hashAlgorithm.ComputeHash(data); + + // handle the case where the algorithm we derived did not match the algorithm that was used to populate the CoseHashV instance. + return hash.Length != HashValue.Length + ? throw new CoseSign1Exception($@"The computed hash length of {hash.Length} for hash type {hashAlgorithm.GetType().FullName} created a hash different than the length of {HashValue.Length} which is unexpected.") + : hash.SequenceEqual(HashValue); + } + + /// + /// Writes the current CoseHashV instance to a cbor byte[]. + /// + /// a byte[] cbor representation of the CoseHashV object. + public byte[] Serialize() + { + CborWriter writer = new(CborConformanceMode.Strict); + + int propertyCount = 2; + + if (Location != null) + { + propertyCount++; + } + + if (AdditionalData != null) + { + propertyCount++; + } + + writer.WriteStartArray(propertyCount); + writer.WriteInt64((long)Algorithm); + writer.WriteByteString(HashValue); + if (Location != null) + { + writer.WriteTextString(Location); + } + if (AdditionalData != null) + { + writer.WriteByteString(AdditionalData); + } + writer.WriteEndArray(); + + return writer.Encode(); + } + + /// + /// Reads a COSE_Hash_V structure from the . + /// + /// A ReadOnlyMemory{byte} which represents a CoseHashV object. + /// True to disable the checks which ensure the decoded algorithm expected hash length and the length of the decoded hash match, False (default) to leave them enabled. + /// A proper COSE_Hash_V structure if read from the reader. + /// Thrown if is null. + /// Thrown if an invalid object state or format is detected. + public static CoseHashV Deserialize(ReadOnlyMemory data, bool disableValidation = false) + => Deserialize(new CborReader(data), disableValidation); + + /// + /// Reads a COSE_Hash_V structure from the . + /// + /// A ReadOnlySpan{byte} which represents a CoseHashV object. + /// True to disable the checks which ensure the decoded algorithm expected hash length and the length of the decoded hash match, False (default) to leave them enabled. + /// A proper COSE_Hash_V structure if read from the reader. + /// Thrown if is null. + /// Thrown if an invalid object state or format is detected. + public static CoseHashV Deserialize(ReadOnlySpan data, bool disableValidation = false) + => Deserialize(new CborReader(data.ToArray().AsMemory()), disableValidation); + + /// + /// Reads a COSE_Hash_V structure from the . + /// + /// A byte[] which represents a CoseHashV object. + /// True to disable the checks which ensure the decoded algorithm expected hash length and the length of the decoded hash match, False (default) to leave them enabled. + /// A proper COSE_Hash_V structure if read from the reader. + /// Thrown if is null. + /// Thrown if an invalid object state or format is detected. + public static CoseHashV Deserialize(byte[] data, bool disableValidation = false) + => Deserialize(new CborReader(data ?? throw new ArgumentNullException(nameof(data), "Cannot deserialize null bytes into a CoseHashV")), disableValidation); + + /// + /// Reads a COSE_Hash_V structure from the . + /// + /// The CBOR reader to be read from, it must have allowMultipleRootLevelValues set to true. + /// True to disable the checks which ensure the decoded algorithm expected hash length and the length of the decoded hash match, False (default) to leave them enabled. + /// A proper COSE_Hash_V structure if read from the reader. + /// Thrown if is null. + /// Thrown if an invalid object state or format is detected. + public static CoseHashV Deserialize(CborReader reader, bool disableValidation = false) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + CoseHashV returnValue = new CoseHashV(); + + // tracking state for error purposes. + uint propertiesRead = 0; + + try + { + if (PeekStateWithExceptionHandling(reader) != CborReaderState.StartArray) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, expected {nameof(CborReaderState.StartArray)} but got {reader.PeekState()} instead."); + } + + int? propertiesToRead; + try + { + propertiesToRead = reader.ReadStartArray(); + } + catch (Exception ex) when (ex is CborContentException) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, reading the state of the reader threw an exception: {ex.Message}", ex); + } + if (propertiesToRead < 2 || propertiesToRead > 4) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, expected 2 to 4 properties but got {propertiesToRead} instead."); + } + + // read the hash algorithm + CborReaderState state = PeekStateWithExceptionHandling(reader); + + if (state != CborReaderState.UnsignedInteger && + state != CborReaderState.NegativeInteger && + state != CborReaderState.TextString) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, expected {nameof(CborReaderState.UnsignedInteger)} or {nameof(CborReaderState.NegativeInteger)} or {nameof(CborReaderState.TextString)} but got {state} instead for \"hashAlg\" property."); + } + if (state == CborReaderState.TextString) + { + string? algorithmString; + try + { + algorithmString = reader.ReadTextString(); + } + catch (Exception ex) when (ex is InvalidOperationException || ex is CborContentException) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, the hash algorithm provided threw an exception: {ex.Message}", ex); + } + + try + { + returnValue.Algorithm = Enum.TryParse(algorithmString, ignoreCase: true, out CoseHashAlgorithm algorithm) + ? algorithm + : throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, the hash algorithm provided \"{algorithmString}\" could not be parsed into a valid {nameof(CoseHashAlgorithm)}."); + } + catch (ArgumentException ex) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, the hash algorithm provided \"{algorithmString}\" threw an exception: {ex.Message}", ex); + } + } + else + { + try + { + returnValue.Algorithm = (CoseHashAlgorithm)reader.ReadInt64(); + } + catch (Exception ex) when (ex is InvalidOperationException || ex is OverflowException || ex is CborContentException) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, the hash algorithm provided threw an exception: {ex.Message}", ex); + } + } + ++propertiesRead; + + state = PeekStateWithExceptionHandling(reader); + if (state != CborReaderState.ByteString) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, expected {nameof(CborReaderState.ByteString)} but got {state} instead for \"hashValue\" property."); + } + try + { + byte[]? value; + try + { + value = reader.ReadByteString(); + } + catch(Exception ex) when (ex is InvalidOperationException || ex is CborContentException) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, reading the hash value provided threw an exception: {ex.Message}", ex); + } + if (disableValidation) + { + // directly assign to the internal hash value to bypass the property setter. + returnValue.InternalHashValue = value; + } + else + { + // use the property setter to validate the hash value against the algorithm. + returnValue.HashValue = value; + } + } + catch (Exception ex) when (ex is ArgumentException || + ex is NotSupportedException) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, the hash value provided threw an exception: {ex.Message}", ex); + } + ++propertiesRead; + + // check for and read location as a text string. + state = PeekStateWithExceptionHandling(reader); + if (state == CborReaderState.TextString) + { + try + { + returnValue.Location = reader.ReadTextString(); + } + catch(Exception ex) when (ex is InvalidOperationException || ex is CborContentException) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, reading the location provided threw an exception: {ex.Message}", ex); + } + ++propertiesRead; + } + + // check for and read additional data as a byte string + state = PeekStateWithExceptionHandling(reader); + if (state == CborReaderState.ByteString) + { + try + { + returnValue.AdditionalData = reader.ReadByteString(); + } + catch(Exception ex) when (ex is InvalidOperationException || ex is CborContentException) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, reading the additional data provided threw an exception: {ex.Message}", ex); + } + ++propertiesRead; + } + + // validate the end of the structure is present. + state = PeekStateWithExceptionHandling(reader); + if (state != CborReaderState.EndArray) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, expected {nameof(CborReaderState.EndArray)} but got {state} instead after reading {propertiesRead} elements of {propertiesToRead} detected elements."); + } + try + { + reader.ReadEndArray(); + } + catch(Exception ex) when (ex is InvalidOperationException || ex is CborContentException) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, reading the end of the array threw an exception: {ex.Message}", ex); + } + } + catch(CborContentException ex) + { + throw new InvalidCoseDataException($"While processing content, a CborContentException was encountered: \"{ex.Message}\"", ex); + } + return returnValue; + } + + private static CborReaderState PeekStateWithExceptionHandling(CborReader reader) + { + try + { + return reader.PeekState(); + } + catch(Exception ex) when (ex is CborContentException) + { + throw new InvalidCoseDataException($"Invalid COSE_Hash_V structure, reading the state of the reader threw an exception: {ex.Message}", ex); + } + } + + /// + /// Get the hash algorithm from the specified CoseHashAlgorithm. + /// + /// The CoseHashAlgorithm to get a hashing type from. + /// The type of the hash object to use. + /// The CoseHashAlgorithm specified is not yet supported. + private static HashAlgorithm GetHashAlgorithmFromCoseHashAlgorithm(CoseHashAlgorithm algorithm) + { + return algorithm switch + { + CoseHashAlgorithm.SHA256 => new SHA256Managed(), + CoseHashAlgorithm.SHA512 => new SHA512Managed(), + CoseHashAlgorithm.SHA384 => new SHA384Managed(), + _ => throw new NotSupportedException($"The algorithm {algorithm} is not supported by {nameof(CoseHashV)}.") + }; + } +} diff --git a/CoseDetachedSignature/CoseDetachedSignature.csproj b/CoseIndirectSignature/CoseIndirectSignature.csproj similarity index 87% rename from CoseDetachedSignature/CoseDetachedSignature.csproj rename to CoseIndirectSignature/CoseIndirectSignature.csproj index 2b1465d8..6fcf8b69 100644 --- a/CoseDetachedSignature/CoseDetachedSignature.csproj +++ b/CoseIndirectSignature/CoseIndirectSignature.csproj @@ -15,6 +15,10 @@ true + + + + diff --git a/CoseIndirectSignature/Exceptions/CoseIndirectSignatureException.cs b/CoseIndirectSignature/Exceptions/CoseIndirectSignatureException.cs new file mode 100644 index 00000000..dc2ba23d --- /dev/null +++ b/CoseIndirectSignature/Exceptions/CoseIndirectSignatureException.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ignore Spelling: Cose + +namespace CoseIndirectSignature.Exceptions; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; +using CoseSign1.Abstractions.Exceptions; + +/// +/// Base exception class for the CoseIndirectSignature library. +/// +[Serializable] +[ExcludeFromCodeCoverage] +public class CoseIndirectSignatureException : CoseSign1Exception +{ + + /// + /// Default Constructor. + /// + public CoseIndirectSignatureException() + { + } + + /// + /// Creates an instance of with a specified message. + /// + /// The message for the exception. + public CoseIndirectSignatureException(string message) : base(message) + { + } + + /// + /// Creates an instance of with a specified message and inner exception. + /// + /// The message for the exception. + /// The inner exception for this exception. + public CoseIndirectSignatureException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Creates an instance of with a specified message and inner context. + /// + /// The serialization info for this exception. + /// The streaming context for this exception. + protected CoseIndirectSignatureException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } +} \ No newline at end of file diff --git a/CoseIndirectSignature/Exceptions/InvalidCoseDataException.cs b/CoseIndirectSignature/Exceptions/InvalidCoseDataException.cs new file mode 100644 index 00000000..9b1763ad --- /dev/null +++ b/CoseIndirectSignature/Exceptions/InvalidCoseDataException.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ignore Spelling: Cose + +namespace CoseIndirectSignature.Exceptions; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; + +/// +/// Exception thrown when the COSE data is invalid with Cose Indirect Signature library. +/// +[Serializable] +[ExcludeFromCodeCoverage] +public sealed class InvalidCoseDataException : CoseIndirectSignatureException +{ + /// + /// The default constructor. + /// + public InvalidCoseDataException() + { + } + + /// + /// Creates an instance of with a specified message. + /// + /// The message for the exception. + public InvalidCoseDataException(string message) : base(message) + { + } + + /// + /// Creates an instance of with a specified message and inner exception. + /// + /// The message for the exception. + /// The inner exception for this exception. + public InvalidCoseDataException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/CoseDetachedSignature/Extensions/CoseSign1MessageDetachedSignatureExtensions.cs b/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs similarity index 55% rename from CoseDetachedSignature/Extensions/CoseSign1MessageDetachedSignatureExtensions.cs rename to CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs index c2be7c54..7e5e44e7 100644 --- a/CoseDetachedSignature/Extensions/CoseSign1MessageDetachedSignatureExtensions.cs +++ b/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs @@ -1,20 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace CoseDetachedSignature.Extensions; +// Ignore Spelling: Cose + +namespace CoseIndirectSignature.Extensions; + +using CoseIndirectSignature.Exceptions; /// -/// Class which extends for detached signature use cases. +/// Class which extends for indirect signature use cases. /// /// /// Logging is done through Trace.TraceError and Debug.WriteLine. /// -public static class CoseSign1MessageDetachedSignatureExtensions +public static class CoseSign1MessageIndirectSignatureExtensions { private static readonly string AlgorithmGroupName = "algorithm"; // Regex looks for "+hash-sha256" and will parse it out as a named group of "extension" with value "+hash-sha256" an the algorithm group name of "sha256" // Will also work with "+hash-sha3_256" private static readonly Regex HashMimeTypeExtension = new(@$"(?\+hash-(?<{AlgorithmGroupName}>[\w_]+))", RegexOptions.Compiled); + private static readonly Regex CoseHashVMimeTypeExtension = new(@$"\+cose-hash-v", RegexOptions.Compiled); /// /// Lazy populate all known hash algorithms from System.Security.Cryptography into a runtime cache @@ -74,78 +79,130 @@ private static IEnumerable FindAllDerivedHashAlgorithms() : null; } + /// + /// Checks to see if a COSE Sign1 Message has the Content Type Protected Header set to include +cose-hash-v + /// + /// The CoseSign1Message to evaluate + /// True if +cose-hash-v is found, False otherwise. + public static bool TryGetIsCoseHashVContentType(this CoseSign1Message? @this) + { + if (@this == null) + { + Trace.TraceError($"{nameof(TryGetIsCoseHashVContentType)} was called on a null CoseSign1Message object"); + return false; + } + + if (@this.Content == null) + { + Trace.TraceWarning($"{nameof(TryGetIsCoseHashVContentType)} was called on a detached CoseSign1Message object, which is not valid."); + return false; + } + + if (!@this.ProtectedHeaders.TryGetValue(CoseHeaderLabel.ContentType, out CoseHeaderValue contentTypeValue)) + { + Trace.TraceWarning($"{nameof(TryGetIsCoseHashVContentType)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the ContentType protected header present."); + return false; + } + + string contentType = contentTypeValue.GetValueAsString(); + if (string.IsNullOrEmpty(contentType)) + { + Trace.TraceWarning($"{nameof(TryGetIsCoseHashVContentType)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the ContentType protected header being a string value."); + return false; + } + + Match mimeMatch = CoseHashVMimeTypeExtension.Match(contentType); + return mimeMatch.Success; + } + /// /// Extracts the hash algorithm name from the hash extension within the Content Type Protected Header if present. /// /// The CoseSign1Message to evaluate /// The discovered Hash Algorithm Name from the Content Type Protected Header value of the CoseSign1Message. /// True if successful in extracting a HashAlgorithmName from the Content Type Protected Header; False otherwise. - public static bool TryGetDetachedSignatureAlgorithm(this CoseSign1Message? @this, out HashAlgorithmName name) + public static bool TryGetIndirectSignatureAlgorithm(this CoseSign1Message? @this, out HashAlgorithmName name) { if (@this == null) { - Trace.TraceError($"{nameof(TryGetDetachedSignatureAlgorithm)} was called on a null CoseSign1Message object"); + Trace.TraceError($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a null CoseSign1Message object"); return false; } if (!@this.ProtectedHeaders.TryGetValue(CoseHeaderLabel.ContentType, out CoseHeaderValue contentTypeValue)) { - Trace.TraceWarning($"{nameof(TryGetDetachedSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the ContentType protected header present."); + Trace.TraceWarning($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the ContentType protected header present."); return false; } string contentType = contentTypeValue.GetValueAsString(); if (string.IsNullOrEmpty(contentType)) { - Trace.TraceWarning($"{nameof(TryGetDetachedSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the ContentType protected header being a string value."); + Trace.TraceWarning($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the ContentType protected header being a string value."); return false; } Match mimeMatch = HashMimeTypeExtension.Match(contentType); if (!mimeMatch.Success) { - Trace.TraceWarning($"{nameof(TryGetDetachedSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) with the ContentType protected header being \"{contentType}\" however it did not match the regex pattern \"{HashMimeTypeExtension}\"."); + Trace.TraceWarning($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) with the ContentType protected header being \"{contentType}\" however it did not match the regex pattern \"{HashMimeTypeExtension}\"."); return false; } name = new HashAlgorithmName(mimeMatch.Groups[AlgorithmGroupName].Value.ToUpperInvariant()); - Debug.WriteLine($"{nameof(TryGetDetachedSignatureAlgorithm)} extracted hash algorithm name: {name.Name}, returning true"); + Debug.WriteLine($"{nameof(TryGetIndirectSignatureAlgorithm)} extracted hash algorithm name: {name.Name}, returning true"); return true; } /// - /// Determines whether the current CoseSign1Message object includes a detached signature. + /// Determines whether the current CoseSign1Message object includes a Indirect signature. /// /// The CoseSign1Message to evaluate. - /// True if the CoseSign1Message is a encoded detached signature; False otherwise. - public static bool IsDetachedSignature(this CoseSign1Message? @this) => @this.TryGetDetachedSignatureAlgorithm(out _); + /// True if the CoseSign1Message is a encoded indirect signature; False otherwise. + public static bool IsIndirectSignature(this CoseSign1Message? @this) => @this.TryGetIsCoseHashVContentType() || @this.TryGetIndirectSignatureAlgorithm(out _); /// - /// Computes if the encoded detached signature within the CoseSign1Message object matches the given artifact stream. + /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact stream. /// /// The CoseSign1Message to evaluate. /// The artifact stream to evaluate. - /// True if the detached signature in the CoseSign1Message matches the signature of the artifact stream; False otherwise. + /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact stream; False otherwise. public static bool SignatureMatches(this CoseSign1Message? @this, Stream artifactStream) => SignatureMatchesInternal(@this, artifactStream: artifactStream); /// - /// Computes if the encoded detached signature within the CoseSign1Message object matches the given artifact bytes. + /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact bytes. /// /// The CoseSign1Message to evaluate. /// The artifact bytes to evaluate. - /// True if the detached signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise. + /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise. public static bool SignatureMatches(this CoseSign1Message? @this, ReadOnlyMemory artifactBytes) => SignatureMatchesInternal(@this, artifactBytes: artifactBytes); /// - /// Computes if the encoded detached signature within the CoseSign1Message object matches the given artifact bytes or artifact stream. + /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact bytes or artifact stream. /// /// The CoseSign1Message to evaluate. /// The artifact bytes to evaluate. /// The artifact stream to evaluate. - /// True if the detached signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise. + /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise. private static bool SignatureMatchesInternal(this CoseSign1Message? @this, ReadOnlyMemory? artifactBytes = null, Stream? artifactStream = null) + { + if(@this.TryGetIsCoseHashVContentType()) + { + return SignatureMatchesInternalCoseHashV(@this, artifactBytes, artifactStream); + } + return SignatureMatchesInternalDirect(@this, artifactBytes, artifactStream); + } + + /// + /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact bytes or artifact stream. + /// + /// The CoseSign1Message to evaluate. + /// The artifact bytes to evaluate. + /// The artifact stream to evaluate. + /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise. + private static bool SignatureMatchesInternalDirect(this CoseSign1Message @this, ReadOnlyMemory? artifactBytes = null, Stream? artifactStream = null) { if (!@this.TryGetHashAlgorithm(out HashAlgorithm? hasher)) { @@ -172,7 +229,7 @@ public static bool TryGetHashAlgorithm(this CoseSign1Message? @this, out HashAlg { hasher = null; - if (!@this.TryGetDetachedSignatureAlgorithm(out HashAlgorithmName algorithmName)) + if (!@this.TryGetIndirectSignatureAlgorithm(out HashAlgorithmName algorithmName)) { Trace.TraceWarning($"{nameof(TryGetHashAlgorithm)} was called on a CoseSign1Message[{@this?.GetHashCode()}] object which did not have a valid hashing algorithm defined"); return false; @@ -194,4 +251,57 @@ public static bool TryGetHashAlgorithm(this CoseSign1Message? @this, out HashAlg Debug.WriteLine($"{nameof(TryGetHashAlgorithm)} created a HashAlgorithm from Hash Algorithm Name: {algorithmName.Name}"); return true; } + + /// + /// Returns the CoseHashV object contained within the .Content of the CoseSign1Message if it is a CoseHashV encoded message. + /// + /// The CoseSign1Message to evaluate. + /// True to disable the checks which ensure the decoded algorithm expected hash length and the length of the decoded hash match, False (default) to leave them enabled. + /// A deserialized CoseHashV object if no errors, an exception otherwise. + /// Thrown if the CoseSign1Message is not a CoseHashV capable object. + /// Thrown if the content of this CoseSign1Message cannot be deserialized into a CoseHashV object. + public static CoseHashV GetCoseHashV(this CoseSign1Message @this, bool disableValidation = false) + { + return !@this.TryGetIsCoseHashVContentType() + ? throw new InvalidDataException($"The CoseSign1Message[{@this?.GetHashCode()}] object is not a CoseHashV capable object.") + : CoseHashV.Deserialize(@this!.Content.Value, disableValidation); + } + + /// + /// Returns true and populates indirectHash with the CoseHashV object contained within the .Content of the CoseSign1Message if it is a CoseHashV encoded message, false otherwise. + /// + /// The CoseSign1Message to evaluate. + /// True to disable the checks which ensure the decoded algorithm expected hash length and the length of the decoded hash match, False (default) to leave them enabled. + /// True if indirectHash is successfully populated, false otherwise. + public static bool TryGetCoseHashV(this CoseSign1Message @this, out CoseHashV? indirectHash, bool disableValidation = false) + { + indirectHash = null; + + try + { + indirectHash = @this.GetCoseHashV(disableValidation: disableValidation); + } + catch (Exception ex) when (ex is InvalidDataException || ex is InvalidCoseDataException) + { + Trace.TraceWarning($"Attempting to get CoseHashV from CoseSign1Message[{@this?.GetHashCode()}] failed, returning false."); + return false; + } + + return true; + } + + /// + /// Leverages the CoseHashV structure path to validate content against the stored indirect hash of the content. + /// + /// The CoseSign1Message to evaluate the CoseHashV structure from .Content + /// The artifact bytes to evaluate. + /// The artifact stream to evaluate. + /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise. + private static bool SignatureMatchesInternalCoseHashV(this CoseSign1Message @this, ReadOnlyMemory? artifactBytes = null, Stream? artifactStream = null) + { + CoseHashV hashStructure = CoseHashV.Deserialize(@this.Content.Value); + return artifactStream != null + ? hashStructure.ContentMatches(artifactStream) + : hashStructure.ContentMatches(artifactBytes!.Value); + } } diff --git a/CoseDetachedSignature/DetachedSignatureFactory.cs b/CoseIndirectSignature/IndirectSignatureFactory.cs similarity index 59% rename from CoseDetachedSignature/DetachedSignatureFactory.cs rename to CoseIndirectSignature/IndirectSignatureFactory.cs index d597800b..1c3ce387 100644 --- a/CoseDetachedSignature/DetachedSignatureFactory.cs +++ b/CoseIndirectSignature/IndirectSignatureFactory.cs @@ -1,19 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace CoseDetachedSignature; +// Ignore Spelling: cose + +namespace CoseIndirectSignature; /// -/// Class used to construct objects which contain a detached signature of a given payload. +/// Class used to construct objects which contain a indirect signature of a given payload. /// /// /// This class will append a "+hash-{hash-algorithm-name}" to the content type when storing it within the Content Type protected header of the using a (Defaulting to a ). /// The field will contain the hash value of the specified payload. /// The default hash algorithm used is . /// -public sealed class DetachedSignatureFactory : IDisposable +public sealed class IndirectSignatureFactory : IDisposable { private readonly HashAlgorithm InternalHashAlgorithm; + private readonly uint HashLength; + private readonly CoseHashAlgorithm InternalCoseHashAlgorithm; private readonly HashAlgorithmName InternalHashAlgorithmName; private readonly ICoseSign1MessageFactory InternalMessageFactory; @@ -34,328 +38,397 @@ public sealed class DetachedSignatureFactory : IDisposable /// - /// Creates a new instance of the class using the hash algorithm and a . + /// Creates a new instance of the class using the hash algorithm and a . /// - public DetachedSignatureFactory() : this(HashAlgorithmName.SHA256) + public IndirectSignatureFactory() : this(HashAlgorithmName.SHA256) { } /// - /// Creates a new instance of the class using the specified hash algorithm and a . + /// Creates a new instance of the class using the specified hash algorithm and a . /// /// The hashing algorithm name to be used when performing hashing operations. - public DetachedSignatureFactory(HashAlgorithmName hashAlgorithmName) : this(hashAlgorithmName, new CoseSign1MessageFactory()) + public IndirectSignatureFactory(HashAlgorithmName hashAlgorithmName) : this(hashAlgorithmName, new CoseSign1MessageFactory()) { } /// - /// Creates a new instance of the class using the specified hash algorithm and the specified . + /// Creates a new instance of the class using the specified hash algorithm and the specified . /// /// The hashing algorithm name to be used when performing hashing operations. /// The CoseSign1MessageFactory to be used when creating CoseSign1Messages. - public DetachedSignatureFactory(HashAlgorithmName hashAlgorithmName, ICoseSign1MessageFactory coseSign1MessageFactory) + public IndirectSignatureFactory(HashAlgorithmName hashAlgorithmName, ICoseSign1MessageFactory coseSign1MessageFactory) { InternalHashAlgorithmName = hashAlgorithmName; - InternalHashAlgorithm = CoseSign1MessageDetachedSignatureExtensions.CreateHashAlgorithmFromName(hashAlgorithmName) ?? throw new ArgumentOutOfRangeException(nameof(hashAlgorithmName), $"hashAlgorithmName[{hashAlgorithmName}] could not be instantiated into a valid HashAlgorithm"); + InternalHashAlgorithm = CoseSign1MessageIndirectSignatureExtensions.CreateHashAlgorithmFromName(hashAlgorithmName) ?? throw new ArgumentOutOfRangeException(nameof(hashAlgorithmName), $"hashAlgorithmName[{hashAlgorithmName}] could not be instantiated into a valid HashAlgorithm"); InternalMessageFactory = coseSign1MessageFactory; + HashLength = (uint)InternalHashAlgorithm.HashSize / 8; + InternalCoseHashAlgorithm = GetCoseHashAlgorithmFromHashAlgorithm(InternalHashAlgorithm); + } + + private CoseHashAlgorithm GetCoseHashAlgorithmFromHashAlgorithm(HashAlgorithm algorithm) + { + if (algorithm is SHA256) + { + return CoseHashAlgorithm.SHA256; + } + else if (algorithm is SHA384) + { + return CoseHashAlgorithm.SHA384; + } + else if (algorithm is SHA512) + { + return CoseHashAlgorithm.SHA512; + } + else + { + throw new ArgumentException($@"No mapping for hash algorithm {algorithm.GetType().FullName} to any {nameof(CoseHashAlgorithm)}"); + } } /// - /// Creates a detached signature of the specified payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. /// - /// The payload to create a detached signature for. + /// The payload to create a Indirect signature for. /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null - public CoseSign1Message CreateDetachedSignature( + public CoseSign1Message CreateIndirectSignature( ReadOnlyMemory payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( returnBytes: false, signingKeyProvider: signingKeyProvider, contentType: contentType, - bytePayload: payload); + bytePayload: payload, + useOldFormat: useOldFormat); /// - /// Creates a detached signature of the payload given a hash of the payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the payload given a hash of the payload returned as a following the rules in this class description. /// /// The raw hash of the payload /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null /// Hash size does not correspond to any known hash algorithms - public CoseSign1Message CreateDetachedSignatureFromHash( + public CoseSign1Message CreateIndirectSignatureFromHash( ReadOnlyMemory rawHash, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( returnBytes: false, signingKeyProvider: signingKeyProvider, contentType: contentType, bytePayload: rawHash, - payloadHashed: true); + payloadHashed: true, + useOldFormat: useOldFormat); /// - /// Creates a detached signature of the specified payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. /// - /// The payload to create a detached signature for. + /// The payload to create a Indirect signature for. /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A Task which can be awaited which will return a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A Task which can be awaited which will return a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null - public Task CreateDetachedSignatureAsync( + public Task CreateIndirectSignatureAsync( ReadOnlyMemory payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => Task.FromResult( + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( returnBytes: false, signingKeyProvider: signingKeyProvider, contentType: contentType, - bytePayload: payload)); + bytePayload: payload, + useOldFormat: useOldFormat)); /// - /// Creates a detached signature of the payload given a hash of the payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the payload given a hash of the payload returned as a following the rules in this class description. /// /// The raw hash of the payload /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null /// Hash size does not correspond to any known hash algorithms - public Task CreateDetachedSignatureFromHashAsync( + public Task CreateIndirectSignatureFromHashAsync( ReadOnlyMemory rawHash, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => Task.FromResult( + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( returnBytes: false, signingKeyProvider: signingKeyProvider, contentType: contentType, bytePayload: rawHash, - payloadHashed: true)); + payloadHashed: true, + useOldFormat: useOldFormat)); /// - /// Creates a detached signature of the specified payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. /// - /// The payload to create a detached signature for. + /// The payload to create a Indirect signature for. /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A Task which can be awaited which will return a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A Task which can be awaited which will return a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null - public CoseSign1Message CreateDetachedSignature( + public CoseSign1Message CreateIndirectSignature( Stream payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( returnBytes: false, signingKeyProvider: signingKeyProvider, contentType: contentType, - streamPayload: payload); + streamPayload: payload, + useOldFormat: useOldFormat); /// - /// Creates a detached signature of the payload given a hash of the payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the payload given a hash of the payload returned as a following the rules in this class description. /// /// The raw hash of the payload /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null /// Hash size does not correspond to any known hash algorithms - public CoseSign1Message CreateDetachedSignatureFromHash( + public CoseSign1Message CreateIndirectSignatureFromHash( Stream rawHash, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( returnBytes: false, signingKeyProvider: signingKeyProvider, contentType: contentType, streamPayload: rawHash, - payloadHashed: true); + payloadHashed: true, + useOldFormat: useOldFormat); /// - /// Creates a detached signature of the specified payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. /// - /// The payload to create a detached signature for. + /// The payload to create a Indirect signature for. /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A Task which can be awaited which will return a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A Task which can be awaited which will return a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null - public Task CreateDetachedSignatureAsync( + public Task CreateIndirectSignatureAsync( Stream payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => Task.FromResult( + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( returnBytes: false, signingKeyProvider: signingKeyProvider, contentType: contentType, - streamPayload: payload)); + streamPayload: payload, + useOldFormat: useOldFormat)); /// - /// Creates a detached signature of the payload given a hash of the payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the payload given a hash of the payload returned as a following the rules in this class description. /// /// The raw hash of the payload /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null /// Hash size does not correspond to any known hash algorithms - public Task CreateDetachedSignatureFromHashAsync( + public Task CreateIndirectSignatureFromHashAsync( Stream rawHash, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (CoseSign1Message)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => Task.FromResult( + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( returnBytes: false, signingKeyProvider: signingKeyProvider, contentType: contentType, streamPayload: rawHash, - payloadHashed: true)); + payloadHashed: true, + useOldFormat: useOldFormat)); /// - /// Creates a detached signature of the specified payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. /// - /// The payload to create a detached signature for. + /// The payload to create a Indirect signature for. /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A byte[] representation of a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A byte[] representation of a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null - public ReadOnlyMemory CreateDetachedSignatureBytes( + public ReadOnlyMemory CreateIndirectSignatureBytes( ReadOnlyMemory payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( returnBytes: true, signingKeyProvider: signingKeyProvider, contentType: contentType, - bytePayload: payload); + bytePayload: payload, + useOldFormat: useOldFormat); /// - /// Creates a detached signature of the payload given a hash of the payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the payload given a hash of the payload returned as a following the rules in this class description. /// /// The raw hash of the payload /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A byte[] representation of a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A byte[] representation of a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null /// Hash size does not correspond to any known hash algorithms - public ReadOnlyMemory CreateDetachedSignatureBytesFromHash( + public ReadOnlyMemory CreateIndirectSignatureBytesFromHash( ReadOnlyMemory rawHash, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( returnBytes: true, signingKeyProvider: signingKeyProvider, contentType: contentType, bytePayload: rawHash, - payloadHashed: true); + payloadHashed: true, + useOldFormat: useOldFormat); /// - /// Creates a detached signature of the specified payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. /// - /// The payload to create a detached signature for. + /// The payload to create a Indirect signature for. /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A Task which when completed returns a byte[] representation of a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A Task which when completed returns a byte[] representation of a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null - public Task> CreateDetachedSignatureBytesAsync( + public Task> CreateIndirectSignatureBytesAsync( ReadOnlyMemory payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => Task.FromResult( + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( returnBytes: true, signingKeyProvider: signingKeyProvider, contentType: contentType, - bytePayload: payload)); + bytePayload: payload, + useOldFormat: useOldFormat)); /// - /// Creates a detached signature of the payload given a hash of the payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the payload given a hash of the payload returned as a following the rules in this class description. /// /// The raw hash of the payload /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A Task which when completed returns a byte[] representation of a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A Task which when completed returns a byte[] representation of a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null /// Hash size does not correspond to any known hash algorithms - public Task> CreateDetachedSignatureBytesFromHashAsync( + public Task> CreateIndirectSignatureBytesFromHashAsync( ReadOnlyMemory rawHash, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => Task.FromResult( + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( returnBytes: true, signingKeyProvider: signingKeyProvider, contentType: contentType, bytePayload: rawHash, - payloadHashed: true)); + payloadHashed: true, + useOldFormat: useOldFormat)); /// - /// Creates a detached signature of the specified payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. /// - /// The payload to create a detached signature for. + /// The payload to create a Indirect signature for. /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A byte[] representation of a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A byte[] representation of a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null - public ReadOnlyMemory CreateDetachedSignatureBytes( + public ReadOnlyMemory CreateIndirectSignatureBytes( Stream payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( returnBytes: true, signingKeyProvider: signingKeyProvider, contentType: contentType, - streamPayload: payload); + streamPayload: payload, + useOldFormat: useOldFormat); /// - /// Creates a detached signature of the payload given a hash of the payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the payload given a hash of the payload returned as a following the rules in this class description. /// /// The raw hash of the payload /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A byte[] representation of a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A byte[] representation of a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null /// Hash size does not correspond to any known hash algorithms - public ReadOnlyMemory CreateDetachedSignatureBytesFromHash( + public ReadOnlyMemory CreateIndirectSignatureBytesFromHash( Stream rawHash, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( returnBytes: true, signingKeyProvider: signingKeyProvider, contentType: contentType, streamPayload: rawHash, - payloadHashed: true); + payloadHashed: true, + useOldFormat: useOldFormat); /// - /// Creates a detached signature of the specified payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. /// - /// The payload to create a detached signature for. + /// The payload to create a Indirect signature for. /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A Task which when completed returns a byte[] representation of a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A Task which when completed returns a byte[] representation of a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null - public Task> CreateDetachedSignatureBytesAsync( + public Task> CreateIndirectSignatureBytesAsync( Stream payload, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => Task.FromResult( + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( returnBytes: true, signingKeyProvider: signingKeyProvider, contentType: contentType, - streamPayload: payload)); + streamPayload: payload, + useOldFormat: useOldFormat)); /// - /// Creates a detached signature of the payload given a hash of the payload returned as a following the rules in this class description. + /// Creates a Indirect signature of the payload given a hash of the payload returned as a following the rules in this class description. /// /// The raw hash of the payload /// The COSE signing key provider to be used for the signing operation within the . /// A media type string following https://datatracker.ietf.org/doc/html/rfc6838. - /// A Task which when completed returns a byte[] representation of a CoseSign1Message which can be used as a detached signature validation of the payload. + /// True to use the older format, False to use CoseHashV format (default). + /// A Task which when completed returns a byte[] representation of a CoseSign1Message which can be used as a Indirect signature validation of the payload. /// The contentType parameter was empty or null /// Hash size does not correspond to any known hash algorithms - public Task> CreateDetachedSignatureBytesFromHashAsync( + public Task> CreateIndirectSignatureBytesFromHashAsync( Stream rawHash, ICoseSigningKeyProvider signingKeyProvider, - string contentType) => Task.FromResult( - (ReadOnlyMemory)CreateDetachedSignatureWithChecksInternal( + string contentType, + bool useOldFormat = false) => Task.FromResult( + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( returnBytes: true, signingKeyProvider: signingKeyProvider, contentType: contentType, streamPayload: rawHash, - payloadHashed: true)); - + payloadHashed: true, + useOldFormat: useOldFormat)); /// /// Does the heavy lifting for this class in computing the hash and creating the correct representation of the CoseSign1Message base on input. /// @@ -365,17 +438,19 @@ public Task> CreateDetachedSignatureBytesFromHashAsync( /// If not null, then Stream API's on the CoseSign1MessageFactory are used. /// If streamPayload is null then this must be specified and must not be null and will use the Byte API's on the CoseSign1MesssageFactory /// True if the payload represents the raw hash + /// True to use the older format, False to use CoseHashV format (default). /// Either a CoseSign1Message or a ReadOnlyMemory{byte} representing the CoseSign1Message object. /// The contentType parameter was empty or null /// Either streamPayload or bytePayload must be specified, but not both at the same time, or both cannot be null /// payloadHashed is set, but hash size does not correspond to any known hash algorithms - private object CreateDetachedSignatureWithChecksInternal( + private object CreateIndirectSignatureWithChecksInternal( bool returnBytes, ICoseSigningKeyProvider signingKeyProvider, string contentType, Stream? streamPayload = null, ReadOnlyMemory? bytePayload = null, - bool payloadHashed = false) + bool payloadHashed = false, + bool useOldFormat = false) { if (string.IsNullOrWhiteSpace(contentType)) { @@ -388,6 +463,105 @@ 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"); } + return useOldFormat + ? CreateIndirectSignatureWithChecksInternalOldFormat( + returnBytes, + signingKeyProvider, + contentType, + streamPayload, + bytePayload, + payloadHashed) + : CreateIndirectSignatureWithChecksInternalNewFormat( + returnBytes, + signingKeyProvider, + contentType, + streamPayload, + bytePayload, + payloadHashed); + } + + /// + /// Does the heavy lifting for this class in computing the hash and creating the correct representation of the CoseSign1Message base on input. + /// + /// True if ReadOnlyMemory form of CoseSign1Message is to be returned, False for a proper CoseSign1Message + /// The signing key provider used for COSE signing operations. + /// The user specified content type. + /// If not null, then Stream API's on the CoseSign1MessageFactory are used. + /// If streamPayload is null then this must be specified and must not be null and will use the Byte API's on the CoseSign1MesssageFactory + /// True if the payload represents the raw hash + /// Either a CoseSign1Message or a ReadOnlyMemory{byte} representing the CoseSign1Message object. + /// The contentType parameter was empty or null + /// Either streamPayload or bytePayload must be specified, but not both at the same time, or both cannot be null + /// payloadHashed is set, but hash size does not correspond to any known hash algorithms + private object CreateIndirectSignatureWithChecksInternalNewFormat( + bool returnBytes, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + Stream? streamPayload = null, + ReadOnlyMemory? bytePayload = null, + bool payloadHashed = false) + { + CoseHashV hash; + string extendedContentType = ExtendContentType(contentType); + if (!payloadHashed) + { + hash = streamPayload != null + ? new CoseHashV(InternalCoseHashAlgorithm, streamPayload) + : new CoseHashV(InternalCoseHashAlgorithm, bytePayload!.Value); + } + else + { + byte[] rawHash = streamPayload != null + ? streamPayload.GetBytes() + : bytePayload!.Value.ToArray(); + + if (rawHash.Length != HashLength) + { + throw new ArgumentException($"{nameof(payloadHashed)} is set, but payload length {rawHash.Length} does not correspond to the hash size for {InternalHashAlgorithmName} of {HashLength}."); + } + + hash = new CoseHashV(); + hash.Algorithm = InternalCoseHashAlgorithm; + hash.HashValue = rawHash; + } + + + return returnBytes + // return the raw bytes if asked + ? InternalMessageFactory.CreateCoseSign1MessageBytes( + hash.Serialize(), + signingKeyProvider, + embedPayload: true, + contentType: extendedContentType) + // return the CoseSign1Message object + : InternalMessageFactory.CreateCoseSign1Message( + hash.Serialize(), + signingKeyProvider, + embedPayload: true, + contentType: extendedContentType); + } + + /// + /// Does the heavy lifting for this class in computing the hash and creating the correct representation of the CoseSign1Message base on input. + /// + /// True if ReadOnlyMemory form of CoseSign1Message is to be returned, False for a proper CoseSign1Message + /// The signing key provider used for COSE signing operations. + /// The user specified content type. + /// If not null, then Stream API's on the CoseSign1MessageFactory are used. + /// If streamPayload is null then this must be specified and must not be null and will use the Byte API's on the CoseSign1MesssageFactory + /// True if the payload represents the raw hash + /// Either a CoseSign1Message or a ReadOnlyMemory{byte} representing the CoseSign1Message object. + /// The contentType parameter was empty or null + /// Either streamPayload or bytePayload must be specified, but not both at the same time, or both cannot be null + /// payloadHashed is set, but hash size does not correspond to any known hash algorithms + private object CreateIndirectSignatureWithChecksInternalOldFormat( + bool returnBytes, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + Stream? streamPayload = null, + ReadOnlyMemory? bytePayload = null, + bool payloadHashed = false) + { ReadOnlyMemory hash; string extendedContentType; if (!payloadHashed) @@ -395,7 +569,7 @@ private object CreateDetachedSignatureWithChecksInternal( hash = streamPayload != null ? InternalHashAlgorithm.ComputeHash(streamPayload) : InternalHashAlgorithm.ComputeHash(bytePayload!.Value.ToArray()); - extendedContentType = ExtendContentType(contentType); + extendedContentType = ExtendContentTypeOld(contentType); } else { @@ -405,7 +579,7 @@ private object CreateDetachedSignatureWithChecksInternal( try { HashAlgorithmName algoName = SizeInBytesToAlgorithm[hash.Length]; - extendedContentType = ExtendContentType(contentType, algoName); + extendedContentType = ExtendContentTypeOld(contentType, algoName); } catch (KeyNotFoundException e) { @@ -445,20 +619,19 @@ private object CreateDetachedSignatureWithChecksInternal( /// quick lookup map between algorithm name and mime extension /// private static readonly ConcurrentDictionary MimeExtensionMap = new( - new Dictionary() + new Dictionary() { { HashAlgorithmName.SHA256.Name, "+hash-sha256" }, { HashAlgorithmName.SHA384.Name, "+hash-sha384" }, { HashAlgorithmName.SHA512.Name, "+hash-sha512" } }); - private bool DisposedValue; /// /// 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. /// A string representing the content type with an appended hash algorithm - private string ExtendContentType(string contentType) => ExtendContentType(contentType, InternalHashAlgorithmName); + private string ExtendContentTypeOld(string contentType) => ExtendContentTypeOld(contentType, InternalHashAlgorithmName); /// /// Method which produces a mime type extension based on the given content type and hash algorithm name. @@ -466,7 +639,7 @@ private object CreateDetachedSignatureWithChecksInternal( /// The content type to append the hash value to if not already appended. /// The "HashAlgorithmName" to append if not already appended. /// A string representing the content type with an appended hash algorithm - private static string ExtendContentType(string contentType, HashAlgorithmName algorithmName) + private static string ExtendContentTypeOld(string contentType, HashAlgorithmName algorithmName) { // extract from the string cache to keep string allocations down. string extensionMapping = MimeExtensionMap.GetOrAdd(algorithmName.Name, (name) => $"+hash-{name.ToLowerInvariant()}"); @@ -479,6 +652,22 @@ private static string ExtendContentType(string contentType, HashAlgorithmName al : $"{contentType}{extensionMapping}"; } + /// + /// Method which produces a mime type extension for cose_hash_v + /// + /// The content type to append the cose_hash_v extension to if not already appended. + /// A string representing the content type with an appended cose_hash_v extension + private static string ExtendContentType(string contentType) + { + // only add the extension mapping, if it's not already present within the contentType + bool alreadyPresent = contentType.IndexOf("+cose-hash-v", StringComparison.InvariantCultureIgnoreCase) != -1; + + return alreadyPresent + ? contentType + : $"{contentType}+cose-hash-v"; + } + + private bool DisposedValue; /// /// Dispose pattern implementation /// diff --git a/CoseDetachedSignature/Usings.cs b/CoseIndirectSignature/Usings.cs similarity index 92% rename from CoseDetachedSignature/Usings.cs rename to CoseIndirectSignature/Usings.cs index b063cfd1..bd3812a5 100644 --- a/CoseDetachedSignature/Usings.cs +++ b/CoseIndirectSignature/Usings.cs @@ -12,7 +12,7 @@ global using System.Security.Cryptography.Cose; global using System.Text.RegularExpressions; global using System.Threading.Tasks; -global using CoseDetachedSignature.Extensions; +global using CoseIndirectSignature.Extensions; global using CoseSign1.Abstractions.Interfaces; global using CoseSign1.Extensions; global using CoseSign1.Interfaces; diff --git a/CoseSign1.Tests/CoseSign1.Tests.csproj b/CoseSign1.Tests/CoseSign1.Tests.csproj index 0b3c2bbd..c2b41276 100644 --- a/CoseSign1.Tests/CoseSign1.Tests.csproj +++ b/CoseSign1.Tests/CoseSign1.Tests.csproj @@ -33,7 +33,7 @@ - + diff --git a/CoseSignTool.sln b/CoseSignTool.sln index 33e8ee93..f92597c0 100644 --- a/CoseSignTool.sln +++ b/CoseSignTool.sln @@ -34,9 +34,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{36BAA2CE-A CHANGELOG.md = CHANGELOG.md docs\CODE_OF_CONDUCT.md = docs\CODE_OF_CONDUCT.md docs\CONTRIBUTING.md = docs\CONTRIBUTING.md - CoseDetachedSignature.md = CoseDetachedSignature.md docs\CoseHandler.md = docs\CoseHandler.md CoseSign1.Abstractions.md = CoseSign1.Abstractions.md + CoseIndirectSignature.md = CoseIndirectSignature.md CoseSign1.Certificates.md = CoseSign1.Certificates.md CoseSign1.md = CoseSign1.md docs\CoseSignTool.md = docs\CoseSignTool.md @@ -48,17 +48,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{36BAA2CE-A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoseHandler.Tests", "CoseHandler.Tests\CoseHandler.Tests.csproj", "{DD155201-7EDA-4979-9E6D-768EB655C8A8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoseDetachedSignature", "CoseDetachedSignature\CoseDetachedSignature.csproj", "{74F706B4-ED89-490D-8CD2-DE8E60D9CE31}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoseIndirectSignature", "CoseIndirectSignature\CoseIndirectSignature.csproj", "{74F706B4-ED89-490D-8CD2-DE8E60D9CE31}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Nuspecs", "Nuspecs", "{97AFB857-361D-4B32-856A-6190A6D1066C}" ProjectSection(SolutionItems) = preProject - Nuspec\CoseDetachedSignature.nuspec = Nuspec\CoseDetachedSignature.nuspec Nuspec\CoseSign1.Abstractions.nuspec = Nuspec\CoseSign1.Abstractions.nuspec + Nuspec\CoseIndirectSignature.nuspec = Nuspec\CoseIndirectSignature.nuspec Nuspec\CoseSign1.Certificates.nuspec = Nuspec\CoseSign1.Certificates.nuspec Nuspec\CoseSign1.nuspec = Nuspec\CoseSign1.nuspec EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoseDetachedSignature.Tests", "CoseDetachedSIgnature.Tests\CoseDetachedSignature.Tests.csproj", "{58984C00-6EA6-47ED-AF9D-717B187A168B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoseIndirectSignature.Tests", "CoseIndirectSignature.Tests\CoseIndirectSignature.Tests.csproj", "{58984C00-6EA6-47ED-AF9D-717B187A168B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{2E02B2A3-69D1-4460-9329-91540D7B56B5}" ProjectSection(SolutionItems) = preProject diff --git a/CoseSignTool.tests/ValidateCommandTests.cs b/CoseSignTool.tests/ValidateCommandTests.cs index 857f4fd7..aecfdcc7 100644 --- a/CoseSignTool.tests/ValidateCommandTests.cs +++ b/CoseSignTool.tests/ValidateCommandTests.cs @@ -5,7 +5,7 @@ namespace CoseSignUnitTests; using System; using System.Linq; -using CoseDetachedSignature; +using CoseIndirectSignature; using CoseSign1.Certificates.Local; using CoseX509; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -165,8 +165,8 @@ public void ValidateUntrustedFails() public void ValidateIndirectSucceedsWithRootPassedIn() { // sign indirectly - var msgFac = new DetachedSignatureFactory(); - byte[] signedBytes = msgFac.CreateDetachedSignatureBytes( + var msgFac = new IndirectSignatureFactory(); + byte[] signedBytes = msgFac.CreateIndirectSignatureBytes( payload: File.ReadAllBytes(PayloadFile), contentType: "application/spdx+json", signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(SelfSignedCert)).ToArray(); @@ -195,8 +195,8 @@ public void ValidateIndirectSucceedsWithRootPassedIn() public void ValidateIndirectFailsWithoutPayloadPassedIn() { // sign indirectly - var msgFac = new DetachedSignatureFactory(); - byte[] signedBytes = msgFac.CreateDetachedSignatureBytes( + var msgFac = new IndirectSignatureFactory(); + byte[] signedBytes = msgFac.CreateIndirectSignatureBytes( payload: File.ReadAllBytes(PayloadFile), contentType: "application/spdx+json", signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(SelfSignedCert)).ToArray(); @@ -226,8 +226,8 @@ public void ValidateIndirectFailsWithoutPayloadPassedIn() public void ValidateIndirectFailsWithModifiedPayload() { // sign indirectly - var msgFac = new DetachedSignatureFactory(); - byte[] signedBytes = msgFac.CreateDetachedSignatureBytes( + var msgFac = new IndirectSignatureFactory(); + byte[] signedBytes = msgFac.CreateIndirectSignatureBytes( payload: File.ReadAllBytes(PayloadFile), contentType: "application/spdx+json", signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(SelfSignedCert)).ToArray(); @@ -259,8 +259,8 @@ public void ValidateIndirectFailsWithModifiedPayload() public void ValidateIndirectFailsWithUntrustedRoot() { // sign indirectly - var msgFac = new DetachedSignatureFactory(); - byte[] signedBytes = msgFac.CreateDetachedSignatureBytes( + var msgFac = new IndirectSignatureFactory(); + byte[] signedBytes = msgFac.CreateIndirectSignatureBytes( payload: File.ReadAllBytes(PayloadFile), contentType: "application/spdx+json", signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(SelfSignedCert)).ToArray(); diff --git a/Nuspec/CoseDetachedSignature.nuspec b/Nuspec/CoseIndirectSignature.nuspec similarity index 85% rename from Nuspec/CoseDetachedSignature.nuspec rename to Nuspec/CoseIndirectSignature.nuspec index e014f43a..c5cc2266 100644 --- a/Nuspec/CoseDetachedSignature.nuspec +++ b/Nuspec/CoseIndirectSignature.nuspec @@ -1,12 +1,12 @@ - CoseDetachedSignature + CoseIndirectSignature $VersionNgt$ Microsoft false -Abstractions and classes required to manage detached signatures via COSE Sign1 message envelopes in a way that is compatible with Supply Chain Integrity Transparency and Trust (SCITT). +Abstractions and classes required to manage indirect signatures via COSE Sign1 message envelopes in a way that is compatible with Supply Chain Integrity Transparency and Trust (SCITT). @@ -15,8 +15,8 @@ Abstractions and classes required to manage detached signatures via COSE Sign1 m - - + +