diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f855a677..eabf3899 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -34,11 +34,11 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: 'csharp' queries: security-extended,security-and-quality @@ -50,6 +50,6 @@ jobs: # Do the analysis - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:csharp" diff --git a/CHANGELOG.md b/CHANGELOG.md index c1690821..40a7230b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [v1.3.0-pre3](https://github.com/microsoft/CoseSignTool/tree/v1.3.0-pre3) (2024-12-28) + +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.3.0-pre2...v1.3.0-pre3) + +**Merged pull requests:** + +- Remove Weak Algorithms [\#123](https://github.com/microsoft/CoseSignTool/pull/123) ([elantiguamsft](https://github.com/elantiguamsft)) + ## [v1.3.0-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.3.0-pre2) (2024-11-20) [Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.3.0-pre1...v1.3.0-pre2) @@ -62,19 +70,19 @@ ## [v1.2.8-pre3](https://github.com/microsoft/CoseSignTool/tree/v1.2.8-pre3) (2024-10-15) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8-pre2...v1.2.8-pre3) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8-pre1...v1.2.8-pre3) **Merged pull requests:** - Increase timeout for checking for empty streams [\#113](https://github.com/microsoft/CoseSignTool/pull/113) ([lemccomb](https://github.com/lemccomb)) -## [v1.2.8-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.2.8-pre2) (2024-09-25) +## [v1.2.8-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.8-pre1) (2024-09-25) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8-pre1...v1.2.8-pre2) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8-pre2...v1.2.8-pre1) -## [v1.2.8-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.8-pre1) (2024-09-25) +## [v1.2.8-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.2.8-pre2) (2024-09-25) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8...v1.2.8-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8...v1.2.8-pre2) **Merged pull requests:** @@ -152,19 +160,19 @@ ## [v1.2.4-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.4-pre1) (2024-07-15) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.4...v1.2.4-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.3-pre7...v1.2.4-pre1) **Merged pull requests:** - User/lemccomb/fileread [\#94](https://github.com/microsoft/CoseSignTool/pull/94) ([lemccomb](https://github.com/lemccomb)) -## [v1.2.4](https://github.com/microsoft/CoseSignTool/tree/v1.2.4) (2024-06-14) +## [v1.2.3-pre7](https://github.com/microsoft/CoseSignTool/tree/v1.2.3-pre7) (2024-06-14) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.3-pre7...v1.2.4) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.4...v1.2.3-pre7) -## [v1.2.3-pre7](https://github.com/microsoft/CoseSignTool/tree/v1.2.3-pre7) (2024-06-14) +## [v1.2.4](https://github.com/microsoft/CoseSignTool/tree/v1.2.4) (2024-06-14) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.3-pre6...v1.2.3-pre7) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.3-pre6...v1.2.4) **Merged pull requests:** @@ -224,7 +232,7 @@ ## [v1.2.1-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.2.1-pre2) (2024-03-15) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.1-pre1...v1.2.1-pre2) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.2...v1.2.1-pre2) **Closed issues:** @@ -234,13 +242,13 @@ - more granular error codes [\#86](https://github.com/microsoft/CoseSignTool/pull/86) ([lemccomb](https://github.com/lemccomb)) -## [v1.2.1-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.1-pre1) (2024-03-12) +## [v1.2.2](https://github.com/microsoft/CoseSignTool/tree/v1.2.2) (2024-03-12) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.2...v1.2.1-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.1-pre1...v1.2.2) -## [v1.2.2](https://github.com/microsoft/CoseSignTool/tree/v1.2.2) (2024-03-12) +## [v1.2.1-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.1-pre1) (2024-03-12) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.1...v1.2.2) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.1...v1.2.1-pre1) **Merged pull requests:** @@ -296,19 +304,19 @@ ## [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.6-pre1...v1.1.7-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) +## [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-pre1...v1.1.7) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.7...v1.1.6-pre1) -## [v1.1.6-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.6-pre1) (2024-02-07) +## [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.6-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.6...v1.1.7) **Merged pull requests:** @@ -328,19 +336,19 @@ ## [v1.1.5](https://github.com/microsoft/CoseSignTool/tree/v1.1.5) (2024-01-31) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.3-pre1...v1.1.5) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.4...v1.1.5) **Merged pull requests:** - write validation output to standard out [\#74](https://github.com/microsoft/CoseSignTool/pull/74) ([elantiguamsft](https://github.com/elantiguamsft)) -## [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.4...v1.1.3-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.3-pre1...v1.1.4) -## [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...v1.1.4) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.3...v1.1.3-pre1) **Merged pull requests:** @@ -352,19 +360,19 @@ ## [v1.1.2-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.2-pre1) (2024-01-24) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1-pre2...v1.1.2-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.2...v1.1.2-pre1) **Merged pull requests:** - Updating snk for internal package compatibility [\#72](https://github.com/microsoft/CoseSignTool/pull/72) ([elantiguamsft](https://github.com/elantiguamsft)) -## [v1.1.1-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.1.1-pre2) (2024-01-18) +## [v1.1.2](https://github.com/microsoft/CoseSignTool/tree/v1.1.2) (2024-01-18) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.2...v1.1.1-pre2) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1-pre2...v1.1.2) -## [v1.1.2](https://github.com/microsoft/CoseSignTool/tree/v1.1.2) (2024-01-18) +## [v1.1.1-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.1.1-pre2) (2024-01-18) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1-pre1...v1.1.2) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1-pre1...v1.1.1-pre2) **Merged pull requests:** @@ -441,7 +449,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/v0.3.1-pre.10...v1.1.0-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0...v1.1.0-pre1) **Merged pull requests:** @@ -451,13 +459,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)) -## [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/v1.1.0...v0.3.1-pre.10) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.10...v1.1.0) -## [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.2...v1.1.0) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.2...v0.3.1-pre.10) **Merged pull requests:** diff --git a/CoseIndirectSignature.Tests/CoseHashEnvelopeTests.cs b/CoseIndirectSignature.Tests/CoseHashEnvelopeTests.cs new file mode 100644 index 00000000..81caa034 --- /dev/null +++ b/CoseIndirectSignature.Tests/CoseHashEnvelopeTests.cs @@ -0,0 +1,390 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ignore Spelling: Cose Deserialization + +namespace CoseIndirectSignature.Tests; + +using System.Net.Mime; +using Microsoft.VisualStudio.TestTools.UnitTesting; // Do not make global because it will conflict with NUnit. + +public class CoseHashEnvelopeTests +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void TestFactoryDefaultCreatesCoseHashEnvelop() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + 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); + + CoseSign1Message coseSign1Message = factory.CreateIndirectSignature( + randomBytes, + coseSigningKeyProvider, + "application/test.payload"); + + // should be CoseHashEnvelope + coseSign1Message.TryGetIsCoseHashEnvelope().Should().BeTrue(); + // should not be CoseHashV + coseSign1Message.TryGetIsCoseHashVContentType().Should().BeFalse(); + // should not be IndirectSignature type. + coseSign1Message.TryGetIndirectSignatureAlgorithm(out _).Should().BeFalse(); + + // check attributes + coseSign1Message.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algoName).Should().BeTrue(); + algoName.Should().Be(CoseHashAlgorithm.SHA256); + + coseSign1Message.TryGetPreImageContentType(out string? contentType).Should().BeTrue(); + contentType.Should().Be("application/test.payload"); + + coseSign1Message.TryGetPayloadLocation(out string? payloadLocation).Should().BeFalse(); + payloadLocation.Should().BeNull(); + + // hashes should match + coseSign1Message.Content.Value.ToArray().Should().BeEquivalentTo(hash); + + // signatures should match + coseSign1Message.SignatureMatches(randomBytes).Should().BeTrue(); + } + + [Test] + public void TestFactoryExplicitCreatesCoseHashEnvelop() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + 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); + + CoseSign1Message coseSign1Message = factory.CreateIndirectSignature( + randomBytes, coseSigningKeyProvider, + "application/test.payload", + IndirectSignatureFactory.IndirectSignatureVersion.CoseHashEnvelope); + // should be CoseHashEnvelope + coseSign1Message.TryGetIsCoseHashEnvelope().Should().BeTrue(); + // should not be CoseHashV + coseSign1Message.TryGetIsCoseHashVContentType().Should().BeFalse(); + // should not be IndirectSignature type. + coseSign1Message.TryGetIndirectSignatureAlgorithm(out _).Should().BeFalse(); + + // check attributes + coseSign1Message.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algoName).Should().BeTrue(); + algoName.Should().Be(CoseHashAlgorithm.SHA256); + + coseSign1Message.TryGetPreImageContentType(out string? contentType).Should().BeTrue(); + contentType.Should().Be("application/test.payload"); + + coseSign1Message.TryGetPayloadLocation(out string? payloadLocation).Should().BeFalse(); + payloadLocation.Should().BeNull(); + + // hashes should match + coseSign1Message.Content.Value.ToArray().Should().BeEquivalentTo(hash); + + // signatures should match + coseSign1Message.SignatureMatches(randomBytes).Should().BeTrue(); + } + + [Test] + [TestCase(1, Description = "TryGetIsCoseHashEnvelope")] + [TestCase(2, Description = "TryGetPayloadHashAlgorithm")] + [TestCase(3, Description = "TryGetPreImageContentType")] + [TestCase(4, Description = "TryGetPayloadLocation")] + public void TestExtensionMethodNullHandling(int testCase) + { + CoseSign1Message? coseSign1Message = null; + switch (testCase) + { + case 1: + coseSign1Message.TryGetIsCoseHashEnvelope().Should().BeFalse(); + break; + case 2: + coseSign1Message.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algoName).Should().BeFalse(); + algoName.Should().BeNull(); + break; + case 3: + coseSign1Message.TryGetPreImageContentType(out string? contentType).Should().BeFalse(); + contentType.Should().BeNull(); + break; + case 4: + coseSign1Message.TryGetPayloadLocation(out string? payloadLocation).Should().BeFalse(); + payloadLocation.Should().BeNull(); + break; + } + } + + [Test] + public void ValidCoseHashEnvelopeMinusContentShouldInvalidate() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + CoseSign1MessageFactory factory = new(); + + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + + CoseSign1Message? message = factory.CreateCoseSign1Message( + randomBytes, + coseSigningKeyProvider, + embedPayload: false, + headerExtender: new CoseHashEnvelopeHeaderExtender(HashAlgorithmName.SHA256, "application/test")); + message.Should().NotBeNull(); + message!.TryGetIsCoseHashEnvelope().Should().BeFalse(); + message.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algoName).Should().BeTrue(); + algoName.Should().Be(CoseHashAlgorithm.SHA256); + message.TryGetPreImageContentType(out string? contentType).Should().BeTrue(); + contentType.Should().Be("application/test"); + } + + [Test] + public void ValidCoseHashEnvelopePayloadHashAlgorithmUnprotectedHeaderShouldInvalidate() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + CoseSign1MessageFactory factory = new(); + + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + Mock mockHeaderExtender = new(MockBehavior.Strict); + CoseHeaderMap protectedHeader = new(); + CoseHeaderMap unProtectedHeader = new(); + + CoseHashEnvelopeHeaderExtender headerExtender = new CoseHashEnvelopeHeaderExtender(HashAlgorithmName.SHA256, "application/test"); + protectedHeader = headerExtender.ExtendProtectedHeaders(protectedHeader); + protectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadHashAlg]); + unProtectedHeader = headerExtender.ExtendProtectedHeaders(unProtectedHeader); + unProtectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PreimageContentType]); + unProtectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadLocation]); + + mockHeaderExtender.Setup(x => x.ExtendProtectedHeaders(It.IsAny())).Returns(protectedHeader); + mockHeaderExtender.Setup(x => x.ExtendUnProtectedHeaders(It.IsAny())).Returns(unProtectedHeader); + + CoseSign1Message? message = factory.CreateCoseSign1Message( + randomBytes, + coseSigningKeyProvider, + embedPayload: true, + headerExtender: mockHeaderExtender.Object); + message.Should().NotBeNull(); + message!.TryGetIsCoseHashEnvelope().Should().BeFalse(); + message.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algoName).Should().BeFalse(); + algoName.Should().BeNull(); + message.TryGetPreImageContentType(out string? contentType).Should().BeTrue(); + contentType.Should().Be("application/test"); + } + + [Test] + public void ValidCoseHashEnvelopeInvalidPayloadHashAlgorithmShouldInvalidate() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + CoseSign1MessageFactory factory = new(); + + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + Mock mockHeaderExtender = new(MockBehavior.Strict); + CoseHeaderMap protectedHeader = new(); + + CoseHashEnvelopeHeaderExtender headerExtender = new CoseHashEnvelopeHeaderExtender(HashAlgorithmName.SHA256, "application/test"); + protectedHeader = headerExtender.ExtendProtectedHeaders(protectedHeader); + protectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadHashAlg]); + // add a bogus payload hash algo + protectedHeader.Add(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadHashAlg], CoseHeaderValue.FromInt32(9953)); + + mockHeaderExtender.Setup(x => x.ExtendProtectedHeaders(It.IsAny())).Returns(protectedHeader); + mockHeaderExtender.Setup(x => x.ExtendUnProtectedHeaders(It.IsAny())).Returns([]); + + CoseSign1Message? message = factory.CreateCoseSign1Message( + randomBytes, + coseSigningKeyProvider, + embedPayload: true, + headerExtender: mockHeaderExtender.Object); + message.Should().NotBeNull(); + message!.TryGetIsCoseHashEnvelope().Should().BeFalse(); + message.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algoName).Should().BeFalse(); + algoName.Should().BeNull(); + message.TryGetPreImageContentType(out string? contentType).Should().BeTrue(); + contentType.Should().Be("application/test"); + } + + [Test] + public void ValidCoseHashEnvelopePayloadPreImageContentTypeUnprotectedHeaderShouldValidate() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + CoseSign1MessageFactory factory = new(); + + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + Mock mockHeaderExtender = new(MockBehavior.Strict); + CoseHeaderMap protectedHeader = new(); + CoseHeaderMap unProtectedHeader = new(); + + CoseHashEnvelopeHeaderExtender headerExtender = new CoseHashEnvelopeHeaderExtender(HashAlgorithmName.SHA256, "application/test"); + protectedHeader = headerExtender.ExtendProtectedHeaders(protectedHeader); + protectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PreimageContentType]); + unProtectedHeader = headerExtender.ExtendProtectedHeaders(unProtectedHeader); + unProtectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadHashAlg]); + unProtectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadLocation]); + + mockHeaderExtender.Setup(x => x.ExtendProtectedHeaders(It.IsAny())).Returns(protectedHeader); + mockHeaderExtender.Setup(x => x.ExtendUnProtectedHeaders(It.IsAny())).Returns(unProtectedHeader); + + CoseSign1Message? message = factory.CreateCoseSign1Message( + randomBytes, + coseSigningKeyProvider, + embedPayload: true, + headerExtender: mockHeaderExtender.Object); + message.Should().NotBeNull(); + message!.TryGetIsCoseHashEnvelope().Should().BeTrue(); + message.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algoName).Should().BeTrue(); + algoName.Should().Be(CoseHashAlgorithm.SHA256); + message.TryGetPreImageContentType(out string? contentType).Should().BeTrue(); + contentType.Should().Be("application/test"); + } + + [Test] + public void ValidCoseHashEnvelopePayloadNoPreImageContentShouldValidate() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + CoseSign1MessageFactory factory = new(); + + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + Mock mockHeaderExtender = new(MockBehavior.Strict); + CoseHeaderMap protectedHeader = new(); + CoseHeaderMap unProtectedHeader = new(); + + CoseHashEnvelopeHeaderExtender headerExtender = new CoseHashEnvelopeHeaderExtender(HashAlgorithmName.SHA256, "application/test"); + protectedHeader = headerExtender.ExtendProtectedHeaders(protectedHeader); + protectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PreimageContentType]); + + mockHeaderExtender.Setup(x => x.ExtendProtectedHeaders(It.IsAny())).Returns(protectedHeader); + mockHeaderExtender.Setup(x => x.ExtendUnProtectedHeaders(It.IsAny())).Returns(unProtectedHeader); + + CoseSign1Message? message = factory.CreateCoseSign1Message( + randomBytes, + coseSigningKeyProvider, + embedPayload: true, + headerExtender: mockHeaderExtender.Object); + message.Should().NotBeNull(); + message!.TryGetIsCoseHashEnvelope().Should().BeTrue(); + message.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algoName).Should().BeTrue(); + algoName.Should().Be(CoseHashAlgorithm.SHA256); + message.TryGetPreImageContentType(out string? contentType).Should().BeFalse(); + contentType.Should().BeNullOrEmpty(); + } + + [Test] + public void ValidCoseHashEnvelopePayloadLocationProtectedHeaderShouldValidate() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + CoseSign1MessageFactory factory = new(); + + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + + CoseHashEnvelopeHeaderExtender headerExtender = new CoseHashEnvelopeHeaderExtender(HashAlgorithmName.SHA256, "application/test", "payload_location"); + CoseSign1Message? message = factory.CreateCoseSign1Message( + randomBytes, + coseSigningKeyProvider, + embedPayload: true, + headerExtender: headerExtender); + message.Should().NotBeNull(); + message!.TryGetIsCoseHashEnvelope().Should().BeTrue(); + message.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algoName).Should().BeTrue(); + algoName.Should().Be(CoseHashAlgorithm.SHA256); + message.TryGetPreImageContentType(out string? contentType).Should().BeTrue(); + contentType.Should().Be("application/test"); + message.TryGetPayloadLocation(out string? payloadLocation).Should().BeTrue(); + payloadLocation.Should().Be("payload_location"); + } + + [Test] + public void ValidCoseHashEnvelopePayloadLocationUnProtectedHeaderShouldInvalidate() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + CoseSign1MessageFactory factory = new(); + + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + Mock mockHeaderExtender = new(MockBehavior.Strict); + CoseHeaderMap protectedHeader = new(); + CoseHeaderMap unProtectedHeader = new(); + + CoseHashEnvelopeHeaderExtender headerExtender = new CoseHashEnvelopeHeaderExtender(HashAlgorithmName.SHA256, "application/test", "payload_location"); + protectedHeader = headerExtender.ExtendProtectedHeaders(protectedHeader); + protectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadLocation]); + unProtectedHeader = headerExtender.ExtendProtectedHeaders(unProtectedHeader); + unProtectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadHashAlg]); + unProtectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PreimageContentType]); + + mockHeaderExtender.Setup(x => x.ExtendProtectedHeaders(It.IsAny())).Returns(protectedHeader); + mockHeaderExtender.Setup(x => x.ExtendUnProtectedHeaders(It.IsAny())).Returns(unProtectedHeader); + + CoseSign1Message? message = factory.CreateCoseSign1Message( + randomBytes, + coseSigningKeyProvider, + embedPayload: true, + headerExtender: mockHeaderExtender.Object); + message.Should().NotBeNull(); + message!.TryGetIsCoseHashEnvelope().Should().BeTrue(); + message.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algoName).Should().BeTrue(); + algoName.Should().Be(CoseHashAlgorithm.SHA256); + message.TryGetPreImageContentType(out string? contentType).Should().BeTrue(); + contentType.Should().Be("application/test"); + message.TryGetPayloadLocation(out string? payloadLocation).Should().BeFalse(); + payloadLocation.Should().BeNullOrEmpty(); + } + + [Test] + public void CoseMessage1MinusContentShouldNotHashMatch() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + CoseSign1MessageFactory factory = new(); + + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + + CoseSign1Message? message = factory.CreateCoseSign1Message( + randomBytes, + coseSigningKeyProvider, + embedPayload: false, + headerExtender: new CoseHashEnvelopeHeaderExtender(HashAlgorithmName.SHA256, "application/test")); + message.Should().NotBeNull(); + Assert.ThrowsException(() => message.SignatureMatchesInternalCoseHashEnvelope(randomBytes)); + } + + [Test] + public void CoseMessage1BadAlgorithmShouldNotHashMatch() + { + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); + CoseSign1MessageFactory factory = new(); + + byte[] randomBytes = new byte[50]; + new Random().NextBytes(randomBytes); + Mock mockHeaderExtender = new(MockBehavior.Strict); + CoseHeaderMap protectedHeader = new(); + + CoseHashEnvelopeHeaderExtender headerExtender = new CoseHashEnvelopeHeaderExtender(HashAlgorithmName.SHA256, "application/test"); + protectedHeader = headerExtender.ExtendProtectedHeaders(protectedHeader); + protectedHeader.Remove(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadHashAlg]); + // add a bogus payload hash algo + protectedHeader.Add(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadHashAlg], CoseHeaderValue.FromInt32(9953)); + + mockHeaderExtender.Setup(x => x.ExtendProtectedHeaders(It.IsAny())).Returns(protectedHeader); + mockHeaderExtender.Setup(x => x.ExtendUnProtectedHeaders(It.IsAny())).Returns([]); + + CoseSign1Message? message = factory.CreateCoseSign1Message( + randomBytes, + coseSigningKeyProvider, + embedPayload: true, + headerExtender: mockHeaderExtender.Object); + message.Should().NotBeNull(); + Assert.ThrowsException(() => message.SignatureMatchesInternalCoseHashEnvelope(randomBytes)); + } +} diff --git a/CoseIndirectSignature.Tests/CoseSign1MessageIndirectSignatureExtensionsTests.cs b/CoseIndirectSignature.Tests/CoseSign1MessageIndirectSignatureExtensionsTests.cs index 9611c4ac..1bba30c3 100644 --- a/CoseIndirectSignature.Tests/CoseSign1MessageIndirectSignatureExtensionsTests.cs +++ b/CoseIndirectSignature.Tests/CoseSign1MessageIndirectSignatureExtensionsTests.cs @@ -23,9 +23,10 @@ public void TestTryGetIndirectSignatureAlgorithmSuccess() ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(); IndirectSignatureFactory factory = new(); byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - - CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", useOldFormat: true); + new Random().NextBytes(randomBytes); +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.Direct); +#pragma warning restore CS0618 // Type or member is obsolete IndirectSignature.TryGetIndirectSignatureAlgorithm(out HashAlgorithmName hashAlgorithmName).Should().BeTrue(); hashAlgorithmName.Should().Be(HashAlgorithmName.SHA256); } @@ -93,9 +94,10 @@ public void TestIsIndirectSignatureSuccess() ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(); IndirectSignatureFactory factory = new(); byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - - CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + new Random().NextBytes(randomBytes); +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.Direct); +#pragma warning restore CS0618 // Type or member is obsolete IndirectSignature.IsIndirectSignature().Should().BeTrue(); } @@ -192,9 +194,11 @@ public void TestTryGetHashAlgorithmSuccess() ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(); IndirectSignatureFactory factory = new(); byte[] randomBytes = new byte[50]; - new Random().NextBytes(randomBytes); - - CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", useOldFormat: true); + new Random().NextBytes(randomBytes); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.Direct); +#pragma warning restore CS0618 // Type or member is obsolete IndirectSignature.TryGetHashAlgorithm(out HashAlgorithm? hashAlgorithm).Should().BeTrue(); hashAlgorithm.Should().NotBeNull(); hashAlgorithm.Should().BeAssignableTo(); @@ -221,7 +225,7 @@ public void TestGetCoseHashVScenarios(int testCase) { // test the fetching case case 1: - CoseSign1Message? testObj1 = signaturefactory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + CoseSign1Message? testObj1 = signaturefactory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", useOldFormat: true); CoseHashV hashObject = testObj1.GetCoseHashV(); hashObject.ContentMatches(randomBytes).Should().BeTrue(); break; @@ -245,7 +249,7 @@ public void TestGetCoseHashVScenarios(int testCase) break; // tryget success case 5: - CoseSign1Message? testObj5 = signaturefactory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + CoseSign1Message? testObj5 = signaturefactory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", useOldFormat: true); testObj5.TryGetCoseHashV(out CoseHashV? hashObject5).Should().BeTrue(); hashObject5.ContentMatches(randomBytes).Should().BeTrue(); break; diff --git a/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs b/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs index 4d8af466..b76f07c0 100644 --- a/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs +++ b/CoseIndirectSignature.Tests/IndirectSignatureFactoryTests.cs @@ -37,37 +37,76 @@ public void TestConstructors() [Test] public async Task TestCreateIndirectSignatureAsync() { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(); + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); 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"); + // test the sync method + Assert.Throws(() => factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, string.Empty)); + + CoseSign1Message IndirectSignatureCurrent = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload"); + IndirectSignatureCurrent.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureCurrent.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureCurrent.TryGetPreImageContentType(out string? payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureCurrent.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature = factory.CreateIndirectSignature(randomBytes, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV); +#pragma warning restore CS0618 // Type or member is obsolete 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"); + memStream.Seek(0, SeekOrigin.Begin); + + + Assert.Throws(() => factory.CreateIndirectSignature(memStream, coseSigningKeyProvider, string.Empty)); + memStream.Seek(0, SeekOrigin.Begin); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature2 = factory.CreateIndirectSignature(memStream, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV); +#pragma warning restore CS0618 // Type or member is obsolete 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"); + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureAsync(randomBytes, coseSigningKeyProvider, string.Empty)); + + CoseSign1Message IndirectSignatureCurrentAsync = await factory.CreateIndirectSignatureAsync(randomBytes, coseSigningKeyProvider, "application/test.payload"); + IndirectSignatureCurrentAsync.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureCurrentAsync.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureCurrentAsync.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureCurrentAsync.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature3 = await factory.CreateIndirectSignatureAsync(randomBytes, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV); +#pragma warning restore CS0618 // Type or member is obsolete 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"); + memStream.Seek(0, SeekOrigin.Begin); + + CoseSign1Message IndirectSignatureCurrentStreamAsync = await factory.CreateIndirectSignatureAsync(memStream, coseSigningKeyProvider, "application/test.payload"); + IndirectSignatureCurrentStreamAsync.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureCurrentStreamAsync.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureCurrentStreamAsync.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureCurrentStreamAsync.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + memStream.Seek(0, SeekOrigin.Begin); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature4 = await factory.CreateIndirectSignatureAsync(memStream, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV); +#pragma warning restore CS0618 // Type or member is obsolete 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(); @@ -77,7 +116,7 @@ public async Task TestCreateIndirectSignatureAsync() [Test] public async Task TestCreateIndirectSignatureHashProvidedAsync() { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(); + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); using IndirectSignatureFactory factory = new(); byte[] randomBytes = new byte[50]; new Random().NextBytes(randomBytes); @@ -87,30 +126,77 @@ public async Task TestCreateIndirectSignatureHashProvidedAsync() 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"); + Assert.Throws(() => factory.CreateIndirectSignatureFromHash(hash, coseSigningKeyProvider, string.Empty)); + + CoseSign1Message IndirectSignatureCurrent = factory.CreateIndirectSignatureFromHash(hash, coseSigningKeyProvider, "application/test.payload"); + IndirectSignatureCurrent.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureCurrent.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureCurrent.TryGetPreImageContentType(out string? payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureCurrent.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature = factory.CreateIndirectSignatureFromHash(hash, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV); +#pragma warning restore CS0618 // Type or member is obsolete 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"); + IndirectSignature.SignatureMatches(randomBytes).Should().BeTrue(); + + Assert.Throws(() => factory.CreateIndirectSignatureFromHash(hashStream, coseSigningKeyProvider, string.Empty)); + hashStream.Seek(0, SeekOrigin.Begin); + + CoseSign1Message IndirectSignatureStreamCurrent = factory.CreateIndirectSignatureFromHash(hashStream, coseSigningKeyProvider, "application/test.payload"); + IndirectSignatureStreamCurrent.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureStreamCurrent.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureStreamCurrent.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureStreamCurrent.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + hashStream.Seek(0, SeekOrigin.Begin); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature2 = factory.CreateIndirectSignatureFromHash(hashStream, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV); +#pragma warning restore CS0618 // Type or member is obsolete 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"); + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureFromHashAsync(hash, coseSigningKeyProvider, string.Empty)); + + CoseSign1Message IndirectSignatureCurrentAsync = factory.CreateIndirectSignatureFromHash(hash, coseSigningKeyProvider, "application/test.payload"); + IndirectSignatureCurrentAsync.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureCurrentAsync.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureCurrentAsync.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureCurrentAsync.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + hashStream.Seek(0, SeekOrigin.Begin); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature3 = await factory.CreateIndirectSignatureFromHashAsync(hash, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV); +#pragma warning restore CS0618 // Type or member is obsolete 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"); + hashStream.Seek(0, SeekOrigin.Begin); + + CoseSign1Message IndirectSignatureCurrentStreamAsync = factory.CreateIndirectSignatureFromHash(hash, coseSigningKeyProvider, "application/test.payload"); + IndirectSignatureCurrentStreamAsync.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureCurrentStreamAsync.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureCurrentStreamAsync.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureCurrentStreamAsync.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + hashStream.Seek(0, SeekOrigin.Begin); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature4 = await factory.CreateIndirectSignatureFromHashAsync(hashStream, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV); +#pragma warning restore CS0618 // Type or member is obsolete 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(); @@ -120,37 +206,83 @@ public async Task TestCreateIndirectSignatureHashProvidedAsync() [Test] public async Task TestCreateIndirectSignatureBytesAsync() { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(); + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); 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()); + Assert.Throws(() => factory.CreateIndirectSignatureBytes(randomBytes, coseSigningKeyProvider, string.Empty)); + + CoseSign1Message IndirectSignatureCurrent = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytes(randomBytes, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignatureCurrent.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureCurrent.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureCurrent.TryGetPreImageContentType(out string? payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureCurrent.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytes(randomBytes, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV).ToArray()); +#pragma warning restore CS0618 // Type or member is obsolete 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()); + + Assert.Throws(() => factory.CreateIndirectSignatureBytes(memStream, coseSigningKeyProvider, string.Empty)); + memStream.Seek(0, SeekOrigin.Begin); + + CoseSign1Message IndirectSignatureStreamCurrent = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytes(memStream, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignatureStreamCurrent.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureStreamCurrent.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureStreamCurrent.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureStreamCurrent.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + memStream.Seek(0, SeekOrigin.Begin); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature2 = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytes(memStream, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV).ToArray()); +#pragma warning restore CS0618 // Type or member is obsolete 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()); + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureBytesAsync(randomBytes, coseSigningKeyProvider, string.Empty)); + + CoseSign1Message IndirectSignatureCurrentAsync = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytes(randomBytes, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignatureCurrentAsync.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureCurrentAsync.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureCurrentAsync.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureCurrentAsync.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature3 = CoseMessage.DecodeSign1((await factory.CreateIndirectSignatureBytesAsync(randomBytes, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV)).ToArray()); +#pragma warning restore CS0618 // Type or member is obsolete 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()); + memStream.Seek(0, SeekOrigin.Begin); + + CoseSign1Message IndirectSignatureCurrentStreamAsync = CoseMessage.DecodeSign1((await factory.CreateIndirectSignatureBytesAsync(memStream, coseSigningKeyProvider, "application/test.payload")).ToArray()); + IndirectSignatureCurrentStreamAsync.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureCurrentStreamAsync.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureCurrentStreamAsync.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureCurrentStreamAsync.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + memStream.Seek(0, SeekOrigin.Begin); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature4 = CoseMessage.DecodeSign1((await factory.CreateIndirectSignatureBytesAsync(memStream, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV)).ToArray()); +#pragma warning restore CS0618 // Type or member is obsolete 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); @@ -160,7 +292,7 @@ public async Task TestCreateIndirectSignatureBytesAsync() [Test] public async Task TestCreateIndirectSignatureBytesHashProvidedAsync() { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(); + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); using IndirectSignatureFactory factory = new(); byte[] randomBytes = new byte[50]; new Random().NextBytes(randomBytes); @@ -170,30 +302,76 @@ public async Task TestCreateIndirectSignatureBytesHashProvidedAsync() 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()); + Assert.Throws(() => factory.CreateIndirectSignatureBytesFromHash(hash, coseSigningKeyProvider, string.Empty)); + + CoseSign1Message IndirectSignatureCurrent = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytesFromHash(hash, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignatureCurrent.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureCurrent.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureCurrent.TryGetPreImageContentType(out string? payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureCurrent.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytesFromHash(hash, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV).ToArray()); +#pragma warning restore CS0618 // Type or member is obsolete 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()); + Assert.Throws(() => factory.CreateIndirectSignatureBytesFromHash(hashStream, coseSigningKeyProvider, string.Empty)); + hashStream.Seek(0, SeekOrigin.Begin); + + CoseSign1Message IndirectSignatureStreamCurrent = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytesFromHash(hashStream, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignatureStreamCurrent.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureStreamCurrent.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureStreamCurrent.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureStreamCurrent.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + hashStream.Seek(0, SeekOrigin.Begin); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature2 = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytesFromHash(hashStream, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV).ToArray()); +#pragma warning restore CS0618 // Type or member is obsolete 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()); + Assert.ThrowsAsync(() => factory.CreateIndirectSignatureBytesFromHashAsync(hash, coseSigningKeyProvider, string.Empty)); + + CoseSign1Message IndirectSignatureHashCurrent = CoseMessage.DecodeSign1(factory.CreateIndirectSignatureBytesFromHash(hash, coseSigningKeyProvider, "application/test.payload").ToArray()); + IndirectSignatureHashCurrent.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureHashCurrent.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureHashCurrent.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureHashCurrent.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature3 = CoseMessage.DecodeSign1((await factory.CreateIndirectSignatureBytesFromHashAsync(hash, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV)).ToArray()); +#pragma warning restore CS0618 // Type or member is obsolete 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()); + hashStream.Seek(0, SeekOrigin.Begin); + + CoseSign1Message IndirectSignatureHashCurrentAsync = CoseMessage.DecodeSign1((await factory.CreateIndirectSignatureBytesFromHashAsync(hashStream, coseSigningKeyProvider, "application/test.payload")).ToArray()); + IndirectSignatureHashCurrentAsync.IsIndirectSignature().Should().BeTrue(); + IndirectSignatureHashCurrentAsync.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignatureHashCurrentAsync.TryGetPreImageContentType(out payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignatureHashCurrentAsync.TryGetPayloadHashAlgorithm(out algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); + hashStream.Seek(0, SeekOrigin.Begin); + +#pragma warning disable CS0618 // Type or member is obsolete + CoseSign1Message IndirectSignature4 = CoseMessage.DecodeSign1((await factory.CreateIndirectSignatureBytesFromHashAsync(hashStream, coseSigningKeyProvider, "application/test.payload", IndirectSignatureFactory.IndirectSignatureVersion.CoseHashV)).ToArray()); +#pragma warning restore CS0618 // Type or member is obsolete 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); @@ -210,7 +388,7 @@ public void TestCreateIndirectSignatureUnsupportedAlgorithmFailure() [Test] public void TestCreateIndirectSignatureAlreadyProvided() { - ICoseSigningKeyProvider coseSigningKeyProvider = SetupMockSigningKeyProvider(); + ICoseSigningKeyProvider coseSigningKeyProvider = TestUtils.SetupMockSigningKeyProvider(); using IndirectSignatureFactory factory = new(); byte[] randomBytes = new byte[50]; new Random().NextBytes(randomBytes); @@ -221,23 +399,11 @@ public void TestCreateIndirectSignatureAlreadyProvided() // 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 static ICoseSigningKeyProvider SetupMockSigningKeyProvider([CallerMemberName] string testName = "none") - { - 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; + IndirectSignature.IsIndirectSignature().Should().BeTrue(); + IndirectSignature.SignatureMatches(randomBytes).Should().BeTrue(); + IndirectSignature.TryGetPreImageContentType(out string? payloadType).Should().Be(true); + payloadType!.Should().Be("application/test.payload"); + IndirectSignature.TryGetPayloadHashAlgorithm(out CoseHashAlgorithm? algo).Should().BeTrue(); + algo!.Should().Be(CoseHashAlgorithm.SHA256); } } diff --git a/CoseIndirectSignature.Tests/TestUtils.cs b/CoseIndirectSignature.Tests/TestUtils.cs new file mode 100644 index 00000000..cf99e6dd --- /dev/null +++ b/CoseIndirectSignature.Tests/TestUtils.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseIndirectSignature.Tests; + +/// +/// Test utility methods. +/// +public static class TestUtils +{ + /// + /// Sets up a mock signing key provider for testing purposes. + /// + /// The name of the test, defaults to the calling member. + /// A which uses a self-signed certificate for signing operations. + public static ICoseSigningKeyProvider SetupMockSigningKeyProvider([CallerMemberName] string testName = "none") + { + 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/CoseHashEnvelopeHeaderExtender.cs b/CoseIndirectSignature/CoseHashEnvelopeHeaderExtender.cs new file mode 100644 index 00000000..272ca5c6 --- /dev/null +++ b/CoseIndirectSignature/CoseHashEnvelopeHeaderExtender.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseIndirectSignature; + +using System.Collections.Generic; +using System.Security.Cryptography.Cose; + +/// +/// Enumeration of COSE Hash Envelope header labels. +/// From https://www.iana.org/assignments/cose/cose.xhtml +/// +public enum CoseHashEnvelopeHeaderLabels : long +{ + /// + /// No header label is defined. + /// + None = 0, + /// + /// The hash algorithm used to produce the payload. + /// + PayloadHashAlg = 258, + /// + /// The content type of the bytes that were hashed (preimage) to produce the payload, given as a content-format number or as a media-type name optionally with parameters. + /// + PreimageContentType = 259, + /// + /// An identifier enabling retrieval of the original resource (preimage) identified by the payload. + /// + PayloadLocation = 260 +} + +/// +/// This class implements the interface for a CoseHashEnvelope indirect signature. +/// +public class CoseHashEnvelopeHeaderExtender: ICoseHeaderExtender +{ + private readonly HashAlgorithmName HashAlgoName; + private readonly string ContentType; + private readonly string? PayloadLocation; + + /// + /// Initializes a new instance of the class. + /// + /// The that the CoseHashEnvelope will use. + /// The content type of the original content being indirectly signed. + /// The optional payload location which will be stored in the protected header if presented. + public CoseHashEnvelopeHeaderExtender(HashAlgorithmName hashAlgoName, string contentType, string? payloadLocation = null) + { + HashAlgoName = hashAlgoName; + if(string.IsNullOrWhiteSpace(contentType)) + { + throw new ArgumentNullException(nameof(contentType)); + } + ContentType = contentType; + PayloadLocation = payloadLocation; + + if(!HashAlgorithmToCoseHeaderValue.ContainsKey(hashAlgoName)) + { + throw new CoseIndirectSignatureException($"Unsupported hash algorithm in {nameof(CoseHashEnvelopeHeaderExtender)}: {hashAlgoName}"); + } + } + /// + /// A dictionary of COSE Hash Envelope header labels and their corresponding CoseHeaderLabel values. + /// + public static readonly Dictionary CoseHashEnvelopeHeaderLabels = new() + { + { CoseIndirectSignature.CoseHashEnvelopeHeaderLabels.PayloadHashAlg, new CoseHeaderLabel(258) }, // payload-hash-alg + { CoseIndirectSignature.CoseHashEnvelopeHeaderLabels.PreimageContentType, new CoseHeaderLabel(259) }, // preimage content type + { CoseIndirectSignature.CoseHashEnvelopeHeaderLabels.PayloadLocation, new CoseHeaderLabel(260) } // payload-location + }; + + /// + /// quick lookup of Cose Hash Algorithm name/value based on HashAlgorithmName + /// + private static readonly Dictionary HashAlgorithmToCoseHeaderValue = new() + { + { HashAlgorithmName.SHA256, CoseHeaderValue.FromInt32((int)CoseHashAlgorithm.SHA256) }, + { HashAlgorithmName.SHA384, CoseHeaderValue.FromInt32((int)CoseHashAlgorithm.SHA384) }, + { HashAlgorithmName.SHA512, CoseHeaderValue.FromInt32((int)CoseHashAlgorithm.SHA512) } + }; + + /// + /// + /// 3. Header Parameters + /// This document specifies the following new header parameters commonly + /// used alongside hashes to identify resources: + /// 258: the hash algorithm used to produce the payload. + /// 259: the content type of the bytes that were hashed (preimage) to + /// produce the payload, given as a content-format number + /// (Section 12.3 of[RFC7252]) or as a media-type name optionally + /// with parameters(Section 8.3 of[RFC9110]). + /// 260: an identifier enabling retrieval of the original resource + /// (preimage) identified by the payload. + /// + public CoseHeaderMap ExtendProtectedHeaders(CoseHeaderMap protectedHeaders) + { + _ = protectedHeaders ?? throw new ArgumentNullException(nameof(protectedHeaders)); + + // Add the payload hash algorithm to the protected headers. + protectedHeaders.Add( + CoseHashEnvelopeHeaderLabels[CoseIndirectSignature.CoseHashEnvelopeHeaderLabels.PayloadHashAlg], + HashAlgorithmToCoseHeaderValue[this.HashAlgoName] + ); + + // Add the preimage content type to the protected headers. + protectedHeaders.Add( + CoseHashEnvelopeHeaderLabels[CoseIndirectSignature.CoseHashEnvelopeHeaderLabels.PreimageContentType], + CoseHeaderValue.FromString(ContentType) + ); + + // Add the payload location to the protected headers if it is not null. + if (PayloadLocation != null) + { + protectedHeaders.Add( + CoseHashEnvelopeHeaderLabels[CoseIndirectSignature.CoseHashEnvelopeHeaderLabels.PayloadLocation], + CoseHeaderValue.FromString(PayloadLocation) + ); + } + + // If the ContentType header is present, remove it. + if (protectedHeaders.ContainsKey(CoseHeaderLabel.ContentType)) + { + /* + Label 3 (content_type) MUST NOT be present in the protected or + unprotected headers. + + Label 3 is easily confused with label TBD_2 + payload_preimage_content_type. The difference between content_type + (3) and payload_preimage_content_type (TBD2) is content_type is used + to identify the content format associated with payload, whereas + payload_preimage_content_type is used to identify the content format + of the bytes which are hashed to produce the payload. + */ + protectedHeaders.Remove(CoseHeaderLabel.ContentType); + } + + return protectedHeaders; + } + + /// + public CoseHeaderMap ExtendUnProtectedHeaders(CoseHeaderMap? unProtectedHeaders) + { + if(unProtectedHeaders != null) + { + if(unProtectedHeaders.ContainsKey(CoseHeaderLabel.ContentType)) + { + // If the ContentType header is present, remove it. + /* + Label 3 (content_type) MUST NOT be present in the protected or + unprotected headers. + */ + unProtectedHeaders.Remove(CoseHeaderLabel.ContentType); + } + return unProtectedHeaders; + } + return []; + } +} diff --git a/CoseIndirectSignature/CoseHashV.cs b/CoseIndirectSignature/CoseHashV.cs index c59ad04f..64912c35 100644 --- a/CoseIndirectSignature/CoseHashV.cs +++ b/CoseIndirectSignature/CoseHashV.cs @@ -39,7 +39,7 @@ public byte[] HashValue } // sanity check the length of the hash against the specified algorithm to be sure we're not allowing a mismatch. - HashAlgorithm algo = GetHashAlgorithmFromCoseHashAlgorithm(Algorithm); + HashAlgorithm algo = IndirectSignatureFactory.GetHashAlgorithmFromCoseHashAlgorithm(Algorithm); if (value.Length != (algo.HashSize / 8)) { throw new ArgumentOutOfRangeException(nameof(value), @$"The hash value length of {value.Length} did not match the CoseHashAlgorithm {Algorithm} required length of {algo.HashSize / 8}"); @@ -136,9 +136,9 @@ public CoseHashV( { throw new ArgumentOutOfRangeException(nameof(byteData), "The data to be hashed cannot be empty."); } - using HashAlgorithm hashAlgorightm = GetHashAlgorithmFromCoseHashAlgorithm(algorithm); + using HashAlgorithm hashAlgorithm = IndirectSignatureFactory.GetHashAlgorithmFromCoseHashAlgorithm(algorithm); // bypass the property setter since we are computing the hash value based on the algorithm directly. - InternalHashValue = hashAlgorightm.ComputeHash(byteData); + InternalHashValue = hashAlgorithm.ComputeHash(byteData); } /// @@ -173,7 +173,7 @@ public CoseHashV( { _= streamData ?? throw new ArgumentNullException(nameof(streamData)); - using HashAlgorithm hashAlgorightm = GetHashAlgorithmFromCoseHashAlgorithm(algorithm); + using HashAlgorithm hashAlgorightm = IndirectSignatureFactory.GetHashAlgorithmFromCoseHashAlgorithm(algorithm); // bypass the property setter since we are computing the hash value based on the algorithm directly. InternalHashValue = hashAlgorightm.ComputeHash(streamData); } @@ -202,7 +202,7 @@ private CoseHashV( /// 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)); + => Task.FromResult(IndirectSignatureFactory.HashMatches(hashAlgorithm: Algorithm, hashValue: HashValue, data: null, stream: stream)); /// /// Validates that the given data stored in the stream matches the hash value stored in this instance. @@ -212,7 +212,7 @@ public Task ContentMatchesAsync(Stream stream) /// 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); + => IndirectSignatureFactory.HashMatches(hashAlgorithm: Algorithm, hashValue: HashValue, data: null, stream: stream); /// /// Validates that the given data in bytes matches the hash value stored in this instance. @@ -222,7 +222,7 @@ public bool ContentMatches(Stream stream) /// 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)); + => Task.FromResult(IndirectSignatureFactory.HashMatches(hashAlgorithm: Algorithm, hashValue: HashValue, data: data, stream: null)); /// /// Validates that the given data in bytes matches the hash value stored in this instance. @@ -232,7 +232,7 @@ public Task ContentMatchesAsync(byte[] data) /// 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); + => IndirectSignatureFactory.HashMatches(hashAlgorithm: Algorithm, hashValue: HashValue, data: data.ToArray(), stream: null); /// /// Validates that the given data in bytes matches the hash value stored in this instance. @@ -242,7 +242,7 @@ public bool ContentMatches(ReadOnlyMemory data) /// 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)); + => Task.FromResult(IndirectSignatureFactory.HashMatches(hashAlgorithm: Algorithm, hashValue: HashValue, data: data.ToArray(), stream: null)); /// /// Validates that the given data in bytes matches the hash value stored in this instance. @@ -252,35 +252,7 @@ public Task ContentMatchesAsync(ReadOnlyMemory data) /// 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); - } + => IndirectSignatureFactory.HashMatches(hashAlgorithm: Algorithm, hashValue: HashValue, data: data, stream: null); /// /// Writes the current CoseHashV instance to a cbor byte[]. @@ -533,21 +505,4 @@ private static CborReaderState PeekStateWithExceptionHandling(CborReader reader) 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/CoseIndirectSignature/CoseIndirectSignature.csproj b/CoseIndirectSignature/CoseIndirectSignature.csproj index d5efd728..dcd26093 100644 --- a/CoseIndirectSignature/CoseIndirectSignature.csproj +++ b/CoseIndirectSignature/CoseIndirectSignature.csproj @@ -37,11 +37,12 @@ - + + @@ -54,4 +55,12 @@ + + + <_Parameter1> + CoseIndirectSignature.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9 + + + + diff --git a/CoseIndirectSignature/Extensions/CoseSign1MessageCoseHashEnvelopeExtensions.cs b/CoseIndirectSignature/Extensions/CoseSign1MessageCoseHashEnvelopeExtensions.cs new file mode 100644 index 00000000..7e3c6c06 --- /dev/null +++ b/CoseIndirectSignature/Extensions/CoseSign1MessageCoseHashEnvelopeExtensions.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseIndirectSignature.Extensions; + +using System; + +/// +/// Extensions for to support COSE Hash Envelope. +/// +public static class CoseSign1MessageCoseHashEnvelopeExtensions +{ + /// + /// Checks to see if the COSE Sign1 Message is a CoseHashEnvelope. + /// https://datatracker.ietf.org/doc/draft-ietf-cose-hash-envelope/03/ + /// + /// The to check. + /// True if is a CoseHashEnvelope, False otherwise. + public static bool TryGetIsCoseHashEnvelope(this CoseSign1Message? @this) + { + if (@this == null) + { + Trace.TraceError($"{nameof(TryGetIsCoseHashEnvelope)} was called on a null CoseSign1Message object"); + return false; + } + + if (@this.Content == null) + { + Trace.TraceWarning($"{nameof(TryGetIsCoseHashEnvelope)} was called on a detached CoseSign1Message object, which is not valid."); + return false; + } + + if(!@this.TryGetPayloadHashAlgorithm(out _)) + { + Trace.TraceWarning($"{nameof(TryGetIsCoseHashEnvelope)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the PayloadHashAlg header present."); + return false; + } + + // This is a CoseHashEnvelope CoseSign1Message. + return true; + } + + /// + /// Tries to get the payload hash algorithm from the protected headers of the CoseSign1Message. + /// + /// The to be checked for PayloadHashAlgorithm protected header value. + /// ill be set to valid if the return value is True. null otherwise. + /// True if the message has a valid PayloadHashAlgorithm header value, false otherwise. + /// Returns false if the value is found in the unprotected headers at all. + public static bool TryGetPayloadHashAlgorithm(this CoseSign1Message? @this, out CoseHashAlgorithm? payloadHashAlgorithm) + { + if (@this == null) + { + Trace.TraceError($"{nameof(TryGetPayloadHashAlgorithm)} was called on a null CoseSign1Message object"); + payloadHashAlgorithm = null; + return false; + } + + CoseHashAlgorithm? extractedValue = null; + // and MUST NOT be present in the unprotected header. + if (@this.UnprotectedHeaders.TryGetValue(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadHashAlg], out CoseHeaderValue payloadHashAlgorithmValue)) + { + Trace.TraceWarning($"{nameof(TryGetPayloadHashAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) with the PayloadHashAlg present in unprotected headers which is not valid."); + payloadHashAlgorithm = null; + return false; + } + + // Label TBD_1(payload hash algorithm) MUST be present in the protected header + if (@this.ProtectedHeaders.TryGetValue(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadHashAlg], out payloadHashAlgorithmValue)) + { + extractedValue = GetCoseHashAlgorithmFromHeaderValue(payloadHashAlgorithmValue); + if (extractedValue == null) + { + Trace.TraceWarning($"{nameof(TryGetPayloadHashAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) with the PayloadHashAlg header value not defined in the CoseHashAlgorithm enum."); + payloadHashAlgorithm = null; + return false; + } + + payloadHashAlgorithm = extractedValue; + return true; + } + + Trace.TraceWarning($"{nameof(TryGetPayloadHashAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the PayloadHashAlg protected header present."); + payloadHashAlgorithm = null; + return false; + } + + private static CoseHashAlgorithm? GetCoseHashAlgorithmFromHeaderValue(CoseHeaderValue payloadHeaderValue) + { + long value = payloadHeaderValue.GetValueAsInt32(); + if (Enum.IsDefined(typeof(CoseHashAlgorithm), value)) + { + return (CoseHashAlgorithm)value; + } + else + { + Trace.TraceWarning($"Value {value} is not defined in the CoseHashAlgorithm enum."); + return null; + } + } + + /// + /// Tries to get the preimage content type from the headers of the CoseSign1Message. + /// + /// The to evaluate. + /// OUT param which will have the value of the PreImageContentType Cose Header. + /// True if the PreImageContentType header is found and will be non-null, False otherwise. + public static bool TryGetPreImageContentType(this CoseSign1Message? @this, out string? preImageContentType) + { + if (@this == null) + { + Trace.TraceError($"{nameof(TryGetPayloadHashAlgorithm)} was called on a null CoseSign1Message object"); + preImageContentType = null; + return false; + } + + // Label TBD_2(content type of the preimage of the payload) MAY be + // present in the protected header or unprotected header. + + // first check protected headers as its preferred to be present there + if (@this.ProtectedHeaders.TryGetValue(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PreimageContentType], out CoseHeaderValue preImageContentTypeValue)) + { + preImageContentType = preImageContentTypeValue.GetValueAsString(); + return true; + } + + // second check unprotected headers + if (@this.UnprotectedHeaders.TryGetValue(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PreimageContentType], out preImageContentTypeValue)) + { + preImageContentType = preImageContentTypeValue.GetValueAsString(); + return true; + } + + Trace.TraceWarning($"{nameof(TryGetPreImageContentType)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the PreimageContentType header present."); + preImageContentType = null; + return false; + } + + /// + /// Tries to get the payload location from the protected headers of the CoseSign1Message. + /// + /// The this extension method will apply to. + /// OUT reference to they payload location if the payload location is present in the message. + /// True if the payload location was extracted, false otherwise. + /// Will also return False if the value is found in the unprotected header. + public static bool TryGetPayloadLocation(this CoseSign1Message? @this, out string? payloadLocation) + { + if (@this == null) + { + Trace.TraceError($"{nameof(TryGetPayloadHashAlgorithm)} was called on a null CoseSign1Message object"); + payloadLocation = null; + return false; + } + + // Label TBD_3(payload_location) MAY be added to the protected + // header and MUST NOT be presented in the unprotected header. + + if(@this.UnprotectedHeaders?.TryGetValue(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadLocation], out CoseHeaderValue payloadLocationValue) ?? false) + { + // If the payload location is present in the unprotected headers, return false. + Trace.TraceWarning($"{nameof(TryGetPayloadLocation)} was called on a CoseSign1Message object({@this.GetHashCode()}) with the PayloadLocation present in unprotected headers which is not valid."); + payloadLocation = null; + return false; + } + + if (@this.ProtectedHeaders.TryGetValue(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadLocation], out payloadLocationValue)) + { + payloadLocation = payloadLocationValue.GetValueAsString(); + return true; + } + + Trace.TraceWarning($"{nameof(TryGetPayloadLocation)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the PayloadLocation header present."); + payloadLocation = null; + return false; + } + + /// + /// Leverages the CoseHashEnvelope rules to validate content against the stored indirect hash of the content. + /// + /// https://datatracker.ietf.org/doc/draft-ietf-cose-hash-envelope/03/ + /// The to evaluate the CoseHashEnvelope structure + /// The artifact bytes to evaluate. + /// The artifact stream to evaluate. + /// Thrown if the object is not a valid CoseHashEnvelope message. + /// Thrown if is null. + /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise. + internal static bool SignatureMatchesInternalCoseHashEnvelope(this CoseSign1Message @this, ReadOnlyMemory? artifactBytes = null, Stream? artifactStream = null) + { + if (@this == null) + { + Trace.TraceError($"{nameof(SignatureMatchesInternalCoseHashEnvelope)} was called on a null CoseSign1Message object"); + throw new ArgumentNullException(nameof(@this)); + } + _ = @this.ProtectedHeaders.TryGetValue(CoseHashEnvelopeHeaderExtender.CoseHashEnvelopeHeaderLabels[CoseHashEnvelopeHeaderLabels.PayloadHashAlg], out CoseHeaderValue hashAlgValue); + CoseHashAlgorithm? hashAlg = GetCoseHashAlgorithmFromHeaderValue(hashAlgValue); + if (hashAlg == null) + { + string logMessage = $"The CoseSign1Message[{@this?.GetHashCode()}] object does not contain a valid PayloadHashAlg header value."; + Trace.TraceWarning(logMessage); + throw new InvalidCoseDataException(logMessage); + } + + if(@this.Content == null || @this.Content.Value.Length == 0) + { + string logMessage = $"The CoseSign1Message[{@this?.GetHashCode()}] object does not contain a valid Content value for CoseHashEnvelope usage."; + Trace.TraceWarning(logMessage); + throw new InvalidCoseDataException(logMessage); + } + + return IndirectSignatureFactory.HashMatches((CoseHashAlgorithm)hashAlg, @this.Content.Value, artifactBytes, artifactStream); + } +} diff --git a/CoseIndirectSignature/Extensions/CoseSign1MessageCoseHashVExtensions.cs b/CoseIndirectSignature/Extensions/CoseSign1MessageCoseHashVExtensions.cs new file mode 100644 index 00000000..53fbc387 --- /dev/null +++ b/CoseIndirectSignature/Extensions/CoseSign1MessageCoseHashVExtensions.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseIndirectSignature.Extensions; + +/// +/// Extensions for to support COSE Hash V. +/// +public static class CoseSign1MessageCoseHashVExtensions +{ + /// + /// Regex to match the +cose-hash-v content/mime type extension. + /// + private static readonly Regex CoseHashVMimeTypeExtension = new(@$"\+cose-hash-v", RegexOptions.Compiled); + + /// + /// 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; + } + + /// + /// 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. + internal 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/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs b/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs index 8e0cd773..44dcc764 100644 --- a/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs +++ b/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs @@ -17,7 +17,6 @@ public static class CoseSign1MessageIndirectSignatureExtensions // 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 @@ -75,44 +74,8 @@ private static IEnumerable FindAllDerivedHashAlgorithms() return methodInfo != null ? (HashAlgorithm)methodInfo.Invoke(null, null) : 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. /// @@ -157,7 +120,7 @@ public static bool TryGetIndirectSignatureAlgorithm(this CoseSign1Message? @this /// /// The CoseSign1Message to evaluate. /// True if the CoseSign1Message is a encoded indirect signature; False otherwise. - public static bool IsIndirectSignature(this CoseSign1Message? @this) => @this.TryGetIsCoseHashVContentType() || @this.TryGetIndirectSignatureAlgorithm(out _); + public static bool IsIndirectSignature(this CoseSign1Message? @this) => @this.TryGetIsCoseHashEnvelope() || @this.TryGetIsCoseHashVContentType() || @this.TryGetIndirectSignatureAlgorithm(out _); /// /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact stream. @@ -185,33 +148,11 @@ public static bool SignatureMatches(this CoseSign1Message? @this, ReadOnlyMemory /// 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 SignatureMatchesInternal(this CoseSign1Message? @this, ReadOnlyMemory? artifactBytes = null, Stream? artifactStream = null) - => @this.TryGetIsCoseHashVContentType() - ? SignatureMatchesInternalCoseHashV(@this, artifactBytes, artifactStream) - : 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)) - { - Trace.TraceError($"{nameof(SignatureMatches)} failed to extract a valid HashAlgorithm from the provided CoseSign1Message[{@this?.GetHashCode()}]"); - return false; - } - - ReadOnlyMemory artifactHash = artifactBytes.HasValue - ? hasher!.ComputeHash(artifactBytes.Value.ToArray()) - : hasher!.ComputeHash(artifactStream!); - - bool equals = @this!.Content.Value.Span.SequenceEqual(artifactHash.Span); - Debug.WriteLine($"{nameof(SignatureMatches)} compared the two hashes with lengths ({artifactHash.Length},{@this!.Content.Value.Length}) for equality and returned {equals}"); - return equals; - } + => @this.TryGetIsCoseHashEnvelope() + ? @this.SignatureMatchesInternalCoseHashEnvelope(artifactBytes, artifactStream) + : @this.TryGetIsCoseHashVContentType() + ? @this.SignatureMatchesInternalCoseHashV(artifactBytes, artifactStream) + : @this.SignatureMatchesInternalDirect(artifactBytes, artifactStream); /// /// Extracts a HashAlgoritm used to compute hashes from a given CoseSign1Message object if possible. @@ -246,56 +187,27 @@ public static bool TryGetHashAlgorithm(this CoseSign1Message? @this, out HashAlg 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); + /// + /// 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)) + { + Trace.TraceError($"{nameof(SignatureMatchesInternalDirect)} failed to extract a valid HashAlgorithm from the provided CoseSign1Message[{@this?.GetHashCode()}]"); + return false; + } + + ReadOnlyMemory artifactHash = artifactBytes.HasValue + ? hasher!.ComputeHash(artifactBytes.Value.ToArray()) + : hasher!.ComputeHash(artifactStream!); + + bool equals = @this!.Content.Value.Span.SequenceEqual(artifactHash.Span); + Debug.WriteLine($"{nameof(SignatureMatchesInternalDirect)} compared the two hashes with lengths ({artifactHash.Length},{@this!.Content.Value.Length}) for equality and returned {equals}"); + return equals; } } diff --git a/CoseIndirectSignature/IndirectSignatureFactory.Bytes.cs b/CoseIndirectSignature/IndirectSignatureFactory.Bytes.cs new file mode 100644 index 00000000..bbf8b66f --- /dev/null +++ b/CoseIndirectSignature/IndirectSignatureFactory.Bytes.cs @@ -0,0 +1,410 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseIndirectSignature; + +/// +/// Byte methods and overloads for the IndirectSignatureFactory class. +/// +public sealed partial class IndirectSignatureFactory +{ + #region ReadonlyMemory overloads return CoseSign1Message + #region sync old signature - backwards compatibility + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignature( + ReadOnlyMemory payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignature( + payload: payload, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignatureFromHash( + ReadOnlyMemory rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureFromHash( + rawHash: rawHash, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + #endregion + #region async old signature - backwards compatibility + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignatureAsync( + ReadOnlyMemory payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureAsync( + payload: payload, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignatureFromHashAsync( + ReadOnlyMemory rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureFromHashAsync( + rawHash: rawHash, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + + #endregion + #region sync new signature + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// The this factory should create. + /// A CoseSign1Message which can be used as a Indirect signature validation of the payload. + /// The contentType parameter was empty or null + public CoseSign1Message CreateIndirectSignature( + ReadOnlyMemory payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: payload, + signatureVersion: signatureVersion); + + /// + /// 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. + /// The this factory should create. + /// 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 CreateIndirectSignatureFromHash( + ReadOnlyMemory rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: rawHash, + payloadHashed: true, + signatureVersion: signatureVersion); + #endregion + #region async new signature + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// The this factory should create.. + /// 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 CreateIndirectSignatureAsync( + ReadOnlyMemory payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + Task.FromResult( + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: payload, + signatureVersion: signatureVersion)); + + /// + /// 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. + /// The this factory should create. + /// 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 CreateIndirectSignatureFromHashAsync( + ReadOnlyMemory rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + Task.FromResult( + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: rawHash, + payloadHashed: true, + signatureVersion: signatureVersion)); + #endregion + #endregion + + #region Readonly overloads return byte[] + #region sync old signature - backwards compatibility + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignatureBytes( + ReadOnlyMemory payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureBytes( + payload: payload, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignatureBytesFromHash( + ReadOnlyMemory rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureBytesFromHash( + rawHash: rawHash, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + #endregion + #region async old signature - backwards compatibility + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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> CreateIndirectSignatureBytesAsync( + ReadOnlyMemory payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureBytesAsync( + payload: payload, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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> CreateIndirectSignatureBytesFromHashAsync( + ReadOnlyMemory rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureBytesFromHashAsync( + rawHash: rawHash, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + #endregion + #region sync new signature + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// The this factory should create. + /// 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 CreateIndirectSignatureBytes( + ReadOnlyMemory payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: payload, + signatureVersion: signatureVersion); + + /// + /// 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. + /// The this factory should create. + /// 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 CreateIndirectSignatureBytesFromHash( + ReadOnlyMemory rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: rawHash, + payloadHashed: true, + signatureVersion: signatureVersion); + #endregion + + #region async old signature - backwards compatibility + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// The this factory should create. + /// 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> CreateIndirectSignatureBytesAsync( + ReadOnlyMemory payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + Task.FromResult( + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: payload, + signatureVersion: signatureVersion)); + + /// + /// 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. + /// The this factory should create. + /// 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> CreateIndirectSignatureBytesFromHashAsync( + ReadOnlyMemory rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + Task.FromResult( + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + bytePayload: rawHash, + payloadHashed: true, + signatureVersion: signatureVersion)); + #endregion + #endregion +} diff --git a/CoseIndirectSignature/IndirectSignatureFactory.CoseHashEnvelope.cs b/CoseIndirectSignature/IndirectSignatureFactory.CoseHashEnvelope.cs new file mode 100644 index 00000000..894e58d7 --- /dev/null +++ b/CoseIndirectSignature/IndirectSignatureFactory.CoseHashEnvelope.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseIndirectSignature; + +/// +/// Methods to create indirect signatures in the COSE Hash Envelope format. +/// +public sealed partial class IndirectSignatureFactory +{ + /// + /// Does the heavy lifting for this class in computing the hash and creating the correct representation of the CoseSign1Message base on input + /// for https://datatracker.ietf.org/doc/draft-ietf-cose-hash-envelope/03/ + /// + /// 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 CreateIndirectSignatureWithChecksInternalCoseHashEnvelopeFormat( + bool returnBytes, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + Stream? streamPayload = null, + ReadOnlyMemory? bytePayload = null, + bool payloadHashed = false) + { + ReadOnlyMemory hash; + HashAlgorithmName algoName = InternalHashAlgorithmName; + + if (!payloadHashed) + { + hash = streamPayload != null + ? InternalHashAlgorithm.ComputeHash(streamPayload) + : InternalHashAlgorithm.ComputeHash(bytePayload!.Value.ToArray()); + } + else + { + hash = streamPayload != null + ? streamPayload.GetBytes() + : bytePayload!.Value.ToArray(); + try + { + algoName = SizeInBytesToAlgorithm[hash.Length]; + } + catch (KeyNotFoundException e) + { + throw new ArgumentException($"{nameof(payloadHashed)} is set, but payload size does not correspond to any known hash sizes in {nameof(HashAlgorithmName)}", e); + } + } + + return returnBytes + ? InternalMessageFactory.CreateCoseSign1MessageBytes( + hash, + signingKeyProvider, + embedPayload: true, + headerExtender: new CoseHashEnvelopeHeaderExtender(algoName, contentType, null)) + : InternalMessageFactory.CreateCoseSign1Message( + hash, + signingKeyProvider, + embedPayload: true, + headerExtender: new CoseHashEnvelopeHeaderExtender(algoName, contentType, null)); + } +} diff --git a/CoseIndirectSignature/IndirectSignatureFactory.CoseHashV.cs b/CoseIndirectSignature/IndirectSignatureFactory.CoseHashV.cs new file mode 100644 index 00000000..eacc44bd --- /dev/null +++ b/CoseIndirectSignature/IndirectSignatureFactory.CoseHashV.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseIndirectSignature; + +/// +/// Methods to create indirect signatures in the COSE Hash V format. +/// +public sealed partial class IndirectSignatureFactory +{ + /// + /// 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 CreateIndirectSignatureWithChecksInternalCoseHashVFormat( + bool returnBytes, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + Stream? streamPayload = null, + ReadOnlyMemory? bytePayload = null, + bool payloadHashed = false) + { + CoseHashV hash; + string extendedContentType = ExtendContentTypeCoseHashV(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 + { + Algorithm = InternalCoseHashAlgorithm, + 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); + } + + /// + /// 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 ExtendContentTypeCoseHashV(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"; + } +} diff --git a/CoseIndirectSignature/IndirectSignatureFactory.Direct.cs b/CoseIndirectSignature/IndirectSignatureFactory.Direct.cs new file mode 100644 index 00000000..47496ad7 --- /dev/null +++ b/CoseIndirectSignature/IndirectSignatureFactory.Direct.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseIndirectSignature; + +/// +/// Methods to create indirect signatures in the original direct hash format. +/// +public sealed partial class IndirectSignatureFactory +{ + /// + /// 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 CreateIndirectSignatureWithChecksInternalDirectFormat( + bool returnBytes, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + Stream? streamPayload = null, + ReadOnlyMemory? bytePayload = null, + bool payloadHashed = false) + { + ReadOnlyMemory hash; + string extendedContentType; + if (!payloadHashed) + { + hash = streamPayload != null + ? InternalHashAlgorithm.ComputeHash(streamPayload) + : InternalHashAlgorithm.ComputeHash(bytePayload!.Value.ToArray()); + extendedContentType = ExtendContentTypeDirect(contentType, HashAlgorithmName); + } + else + { + hash = streamPayload != null + ? streamPayload.GetBytes() + : bytePayload!.Value.ToArray(); + try + { + HashAlgorithmName algoName = SizeInBytesToAlgorithm[hash.Length]; + extendedContentType = ExtendContentTypeDirect(contentType, algoName); + } + catch (KeyNotFoundException e) + { + throw new ArgumentException($"{nameof(payloadHashed)} is set, but payload size does not correspond to any known hash sizes in {nameof(HashAlgorithmName)}", e); + } + } + + return returnBytes + ? InternalMessageFactory.CreateCoseSign1MessageBytes( + hash, + signingKeyProvider, + embedPayload: true, + contentType: extendedContentType) + : InternalMessageFactory.CreateCoseSign1Message( + hash, + signingKeyProvider, + embedPayload: true, + contentType: extendedContentType); + } + + /// + /// 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. + /// The "HashAlgorithmName" to append if not already appended. + /// A string representing the content type with an appended hash algorithm + private static string ExtendContentTypeDirect(string contentType, HashAlgorithmName algorithmName) + { + // extract from the string cache to keep string allocations down. + string extensionMapping = DirectContentTypeExtensionMap.GetOrAdd(algorithmName.Name, (name) => $"+hash-{name.ToLowerInvariant()}"); + + // only add the extension mapping, if it's not already present within the contentType + bool alreadyPresent = contentType.IndexOf("+hash-", StringComparison.InvariantCultureIgnoreCase) != -1; + + return alreadyPresent + ? contentType + : $"{contentType}{extensionMapping}"; + } + + /// + /// quick lookup map between algorithm name and mime extension + /// + private static readonly ConcurrentDictionary DirectContentTypeExtensionMap = new( + new Dictionary() + { + { HashAlgorithmName.SHA256.Name, "+hash-sha256" }, + { HashAlgorithmName.SHA384.Name, "+hash-sha384" }, + { HashAlgorithmName.SHA512.Name, "+hash-sha512" } + }); +} diff --git a/CoseIndirectSignature/IndirectSignatureFactory.Stream.cs b/CoseIndirectSignature/IndirectSignatureFactory.Stream.cs new file mode 100644 index 00000000..0f310762 --- /dev/null +++ b/CoseIndirectSignature/IndirectSignatureFactory.Stream.cs @@ -0,0 +1,409 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseIndirectSignature; + +/// +/// Stream methods and overloads for the IndirectSignatureFactory class. +/// +public sealed partial class IndirectSignatureFactory +{ + #region Stream overloads return CoseSign1Message + #region sync old signature - backwards compatibility + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignature( + Stream payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignature( + payload: payload, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignatureFromHash( + Stream rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureFromHash( + rawHash: rawHash, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignatureAsync( + Stream payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureAsync( + payload: payload, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignatureFromHashAsync( + Stream rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureFromHashAsync( + rawHash: rawHash, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + #endregion + #region new sync signature + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// The this factory should create. + /// 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 CreateIndirectSignature( + Stream payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: payload, + signatureVersion: signatureVersion); + + /// + /// 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. + /// The this factory should create. + /// 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 CreateIndirectSignatureFromHash( + Stream rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: rawHash, + payloadHashed: true, + signatureVersion: signatureVersion); + #endregion + #region new async signature + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// The this factory should create. + /// 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 CreateIndirectSignatureAsync( + Stream payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + Task.FromResult( + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: payload, + signatureVersion: signatureVersion)); + + /// + /// 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. + /// The this factory should create. + /// 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 CreateIndirectSignatureFromHashAsync( + Stream rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + Task.FromResult( + (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( + returnBytes: false, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: rawHash, + payloadHashed: true, + signatureVersion: signatureVersion)); + #endregion + #endregion + + #region Stream overloads return byte[] + #region sync old signature - backwards compatibility + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignatureBytes( + Stream payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureBytes( + payload: payload, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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 CreateIndirectSignatureBytesFromHash( + Stream rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureBytesFromHash( + rawHash: rawHash, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + #endregion + #region async old signature - backwards compatibility + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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> CreateIndirectSignatureBytesAsync( + Stream payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureBytesAsync( + payload: payload, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + + /// + /// 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. + /// True to use the older format - CoseHashV, False to use CoseHashEnvelope 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> CreateIndirectSignatureBytesFromHashAsync( + Stream rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + bool useOldFormat = false) => + CreateIndirectSignatureBytesFromHashAsync( + rawHash: rawHash, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + signatureVersion: + useOldFormat +#pragma warning disable CS0618 // Type or member is obsolete + ? IndirectSignatureVersion.CoseHashV +#pragma warning restore CS0618 // Type or member is obsolete + : IndirectSignatureVersion.CoseHashEnvelope); + #endregion + #region new sync signature + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// The this factory should create. + /// 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 CreateIndirectSignatureBytes( + Stream payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: payload, + payloadHashed: false, + signatureVersion: signatureVersion); + + /// + /// 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. + /// The this factory should create. + /// 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 CreateIndirectSignatureBytesFromHash( + Stream rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: rawHash, + payloadHashed: true, + signatureVersion: signatureVersion); + #endregion + #region new async signature + /// + /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. + /// + /// 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. + /// The this factory should create. + /// 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> CreateIndirectSignatureBytesAsync( + Stream payload, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + Task.FromResult( + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: payload, + payloadHashed: false, + signatureVersion: signatureVersion)); + + /// + /// 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. + /// The this factory should create. + /// 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> CreateIndirectSignatureBytesFromHashAsync( + Stream rawHash, + ICoseSigningKeyProvider signingKeyProvider, + string contentType, + IndirectSignatureVersion signatureVersion) => + Task.FromResult( + (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( + returnBytes: true, + signingKeyProvider: signingKeyProvider, + contentType: contentType, + streamPayload: rawHash, + payloadHashed: true, + signatureVersion: signatureVersion)); + #endregion + #endregion +} diff --git a/CoseIndirectSignature/IndirectSignatureFactory.cs b/CoseIndirectSignature/IndirectSignatureFactory.cs index 7ecd17b9..7086a5bc 100644 --- a/CoseIndirectSignature/IndirectSignatureFactory.cs +++ b/CoseIndirectSignature/IndirectSignatureFactory.cs @@ -13,8 +13,29 @@ namespace CoseIndirectSignature; /// The field will contain the hash value of the specified payload. /// The default hash algorithm used is . /// -public sealed class IndirectSignatureFactory : IDisposable -{ +public sealed partial class IndirectSignatureFactory : IDisposable +{ + /// + /// The version of the indirect signature to be used. + /// + public enum IndirectSignatureVersion + { + /// + /// The older format, which is not recommended for new applications and is included for backwards compatibility. + /// + [Obsolete("Use CoseHashEnvelope instead")] + Direct, + /// + /// The CoseHashV format, which is not recommended for new applications and is included for backwards compatibility. + /// + [Obsolete("Use CoseHashEnvelope instead")] + CoseHashV, + /// + /// The CoseHashEnvelope format, which is the recommended format for new applications. + /// + CoseHashEnvelope + } + private readonly HashAlgorithm InternalHashAlgorithm; private readonly uint HashLength; private readonly CoseHashAlgorithm InternalCoseHashAlgorithm; @@ -75,351 +96,8 @@ private CoseHashAlgorithm GetCoseHashAlgorithmFromHashAlgorithm(HashAlgorithm al SHA512 => CoseHashAlgorithm.SHA512, _ => throw new ArgumentException($@"No mapping for hash algorithm {algorithm.GetType().FullName} to any {nameof(CoseHashAlgorithm)}") }; - } - - /// - /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. - /// - /// 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. - /// 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 CreateIndirectSignature( - ReadOnlyMemory payload, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: payload, - useOldFormat: useOldFormat); - - /// - /// 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. - /// 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 CreateIndirectSignatureFromHash( - ReadOnlyMemory rawHash, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: rawHash, - payloadHashed: true, - useOldFormat: useOldFormat); - - /// - /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. - /// - /// 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. - /// 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 CreateIndirectSignatureAsync( - ReadOnlyMemory payload, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => Task.FromResult( - (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: payload, - useOldFormat: useOldFormat)); - - /// - /// 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. - /// 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 CreateIndirectSignatureFromHashAsync( - ReadOnlyMemory rawHash, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => Task.FromResult( - (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: rawHash, - payloadHashed: true, - useOldFormat: useOldFormat)); - - /// - /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. - /// - /// 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. - /// 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 CreateIndirectSignature( - Stream payload, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: payload, - useOldFormat: useOldFormat); - - /// - /// 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. - /// 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 CreateIndirectSignatureFromHash( - Stream rawHash, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: rawHash, - payloadHashed: true, - useOldFormat: useOldFormat); - - /// - /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. - /// - /// 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. - /// 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 CreateIndirectSignatureAsync( - Stream payload, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => Task.FromResult( - (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: payload, - useOldFormat: useOldFormat)); - - /// - /// 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. - /// 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 CreateIndirectSignatureFromHashAsync( - Stream rawHash, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => Task.FromResult( - (CoseSign1Message)CreateIndirectSignatureWithChecksInternal( - returnBytes: false, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: rawHash, - payloadHashed: true, - useOldFormat: useOldFormat)); - - /// - /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. - /// - /// 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. - /// 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 CreateIndirectSignatureBytes( - ReadOnlyMemory payload, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: payload, - useOldFormat: useOldFormat); - - /// - /// 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. - /// 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 CreateIndirectSignatureBytesFromHash( - ReadOnlyMemory rawHash, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: rawHash, - payloadHashed: true, - useOldFormat: useOldFormat); - - /// - /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. - /// - /// 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. - /// 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> CreateIndirectSignatureBytesAsync( - ReadOnlyMemory payload, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => Task.FromResult( - (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: payload, - useOldFormat: useOldFormat)); - - /// - /// 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. - /// 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> CreateIndirectSignatureBytesFromHashAsync( - ReadOnlyMemory rawHash, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => Task.FromResult( - (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - bytePayload: rawHash, - payloadHashed: true, - useOldFormat: useOldFormat)); - - /// - /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. - /// - /// 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. - /// 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 CreateIndirectSignatureBytes( - Stream payload, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: payload, - useOldFormat: useOldFormat); - - /// - /// 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. - /// 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 CreateIndirectSignatureBytesFromHash( - Stream rawHash, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: rawHash, - payloadHashed: true, - useOldFormat: useOldFormat); - - /// - /// Creates a Indirect signature of the specified payload returned as a following the rules in this class description. - /// - /// 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. - /// 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> CreateIndirectSignatureBytesAsync( - Stream payload, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => Task.FromResult( - (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: payload, - useOldFormat: useOldFormat)); - - /// - /// 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. - /// 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> CreateIndirectSignatureBytesFromHashAsync( - Stream rawHash, - ICoseSigningKeyProvider signingKeyProvider, - string contentType, - bool useOldFormat = false) => Task.FromResult( - (ReadOnlyMemory)CreateIndirectSignatureWithChecksInternal( - returnBytes: true, - signingKeyProvider: signingKeyProvider, - contentType: contentType, - streamPayload: rawHash, - 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. /// @@ -429,7 +107,7 @@ public Task> CreateIndirectSignatureBytesFromHashAsync( /// 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). + /// The this factory should create. /// 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 @@ -437,11 +115,11 @@ public Task> CreateIndirectSignatureBytesFromHashAsync( private object CreateIndirectSignatureWithChecksInternal( bool returnBytes, ICoseSigningKeyProvider signingKeyProvider, - string contentType, + string contentType, + IndirectSignatureVersion signatureVersion, Stream? streamPayload = null, ReadOnlyMemory? bytePayload = null, - bool payloadHashed = false, - bool useOldFormat = false) + bool payloadHashed = false) { if (string.IsNullOrWhiteSpace(contentType)) { @@ -453,145 +131,85 @@ private object CreateIndirectSignatureWithChecksInternal( { 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); + + switch(signatureVersion) + { +#pragma warning disable CS0618 // Type or member is obsolete + case IndirectSignatureVersion.Direct: +#pragma warning restore CS0618 // Type or member is obsolete + return CreateIndirectSignatureWithChecksInternalDirectFormat( + returnBytes, + signingKeyProvider, + contentType, + streamPayload, + bytePayload, + payloadHashed); +#pragma warning disable CS0618 // Type or member is obsolete + case IndirectSignatureVersion.CoseHashV: +#pragma warning restore CS0618 // Type or member is obsolete + return CreateIndirectSignatureWithChecksInternalCoseHashVFormat( + returnBytes, + signingKeyProvider, + contentType, + streamPayload, + bytePayload, + payloadHashed); + case IndirectSignatureVersion.CoseHashEnvelope: + return CreateIndirectSignatureWithChecksInternalCoseHashEnvelopeFormat( + returnBytes, + signingKeyProvider, + contentType, + streamPayload, + bytePayload, + payloadHashed); + default: + throw new ArgumentOutOfRangeException(nameof(signatureVersion), "Unknown signature version"); + } } - + /// - /// Does the heavy lifting for this class in computing the hash and creating the correct representation of the CoseSign1Message base on input. + /// Get the hash algorithm from the specified CoseHashAlgorithm. /// - /// 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) + /// The CoseHashAlgorithm to get a hashing type from. + /// The type of the hash object to use. + /// The CoseHashAlgorithm specified is not yet supported. + public static HashAlgorithm GetHashAlgorithmFromCoseHashAlgorithm(CoseHashAlgorithm algorithm) { - CoseHashV hash; - string extendedContentType = ExtendContentType(contentType); - if (!payloadHashed) - { - hash = streamPayload != null - ? new CoseHashV(InternalCoseHashAlgorithm, streamPayload) - : new CoseHashV(InternalCoseHashAlgorithm, bytePayload!.Value); - } - else + return algorithm switch { - 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 - { - Algorithm = InternalCoseHashAlgorithm, - 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); - } - + CoseHashAlgorithm.SHA256 => new SHA256Managed(), + CoseHashAlgorithm.SHA512 => new SHA512Managed(), + CoseHashAlgorithm.SHA384 => new SHA384Managed(), + _ => throw new NotSupportedException($"The algorithm {algorithm} is not supported by {nameof(CoseHashV)}.") + }; + } + /// - /// Does the heavy lifting for this class in computing the hash and creating the correct representation of the CoseSign1Message base on input. + /// Method for handling byte[] and stream for the same logic. /// - /// 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) + /// 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. + internal static bool HashMatches(CoseHashAlgorithm hashAlgorithm, ReadOnlyMemory hashValue, ReadOnlyMemory? data, Stream? stream) { - ReadOnlyMemory hash; - string extendedContentType; - if (!payloadHashed) + // handle input validation + if ( + (data == null || data.Value.Length == 0) && + (stream == null)) { - hash = streamPayload != null - ? InternalHashAlgorithm.ComputeHash(streamPayload) - : InternalHashAlgorithm.ComputeHash(bytePayload!.Value.ToArray()); - extendedContentType = ExtendContentTypeOld(contentType); - } - else - { - hash = streamPayload != null - ? streamPayload.GetBytes() - : bytePayload!.Value.ToArray(); - try - { - HashAlgorithmName algoName = SizeInBytesToAlgorithm[hash.Length]; - extendedContentType = ExtendContentTypeOld(contentType, algoName); - } - catch (KeyNotFoundException e) - { - throw new ArgumentException($"{nameof(payloadHashed)} is set, but payload size does not correspond to any known hash sizes in {nameof(HashAlgorithmName)}", e); - } + throw new ArgumentNullException(nameof(data)); } + // initialize and compute the hash + using HashAlgorithm hashAlgorithmImpl = GetHashAlgorithmFromCoseHashAlgorithm(hashAlgorithm); + byte[] hash = stream != null ? hashAlgorithmImpl.ComputeHash(stream) : hashAlgorithmImpl.ComputeHash(data!.Value.ToArray()); - return returnBytes - ? InternalMessageFactory.CreateCoseSign1MessageBytes( - hash, - signingKeyProvider, - embedPayload: true, - contentType: extendedContentType) - : InternalMessageFactory.CreateCoseSign1Message( - hash, - signingKeyProvider, - embedPayload: true, - contentType: extendedContentType); + // 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.ToArray()); } /// @@ -604,60 +222,8 @@ private object CreateIndirectSignatureWithChecksInternalOldFormat( { 32, HashAlgorithmName.SHA256 }, { 48, HashAlgorithmName.SHA384 }, { 64, HashAlgorithmName.SHA512 } - }); - - /// - /// quick lookup map between algorithm name and mime extension - /// - private static readonly ConcurrentDictionary MimeExtensionMap = new( - new Dictionary() - { - { HashAlgorithmName.SHA256.Name, "+hash-sha256" }, - { HashAlgorithmName.SHA384.Name, "+hash-sha384" }, - { HashAlgorithmName.SHA512.Name, "+hash-sha512" } - }); - - /// - /// 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 ExtendContentTypeOld(string contentType) => ExtendContentTypeOld(contentType, InternalHashAlgorithmName); - - /// - /// Method which produces a mime type extension based on the given content type and hash algorithm name. - /// - /// The content type to append the hash value to if not already appended. - /// The "HashAlgorithmName" to append if not already appended. - /// A string representing the content type with an appended hash algorithm - 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()}"); - - // only add the extension mapping, if it's not already present within the contentType - bool alreadyPresent = contentType.IndexOf("+hash-", StringComparison.InvariantCultureIgnoreCase) != -1; - - return alreadyPresent - ? contentType - : $"{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/CoseSign1.Abstractions/CoseSign1.Abstractions.csproj b/CoseSign1.Abstractions/CoseSign1.Abstractions.csproj index 1dd43543..5718d293 100644 --- a/CoseSign1.Abstractions/CoseSign1.Abstractions.csproj +++ b/CoseSign1.Abstractions/CoseSign1.Abstractions.csproj @@ -37,7 +37,7 @@ - + diff --git a/CoseSign1.Certificates/CoseSign1.Certificates.csproj b/CoseSign1.Certificates/CoseSign1.Certificates.csproj index 49594fba..23770f47 100644 --- a/CoseSign1.Certificates/CoseSign1.Certificates.csproj +++ b/CoseSign1.Certificates/CoseSign1.Certificates.csproj @@ -36,7 +36,7 @@ - + diff --git a/CoseSign1.Tests/CoseSign1.Tests.csproj b/CoseSign1.Tests/CoseSign1.Tests.csproj index f88281f0..c5c4e625 100644 --- a/CoseSign1.Tests/CoseSign1.Tests.csproj +++ b/CoseSign1.Tests/CoseSign1.Tests.csproj @@ -32,8 +32,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/CoseSignTool.Tests/MainTests.cs b/CoseSignTool.Tests/MainTests.cs index 45ed184c..7865899b 100644 --- a/CoseSignTool.Tests/MainTests.cs +++ b/CoseSignTool.Tests/MainTests.cs @@ -9,12 +9,9 @@ public class MainTests // Certificates private static readonly X509Certificate2 SelfSignedCert = TestCertificateUtils.CreateCertificate(nameof(MainTests) + " self signed"); // A self-signed cert private static readonly X509Certificate2Collection CertChain1 = TestCertificateUtils.CreateTestChain(nameof(MainTests) + " set 1"); // Two complete cert chains - private static readonly X509Certificate2Collection CertChain2 = TestCertificateUtils.CreateTestChain(nameof(MainTests) + " set 2"); - private static readonly X509Certificate2 Root1Priv = CertChain1[0]; // Roots from the chains - private static readonly X509Certificate2 Root2Priv = CertChain2[0]; + private static readonly X509Certificate2 Root1Priv = CertChain1[0]; // Roots from the chains private static readonly X509Certificate2 Int1Priv = CertChain1[1]; private static readonly X509Certificate2 Leaf1Priv = CertChain1[^1]; // Leaf node certs - private static readonly X509Certificate2 Leaf2Priv = CertChain2[^1]; // File paths to export them to private static readonly string PrivateKeyCertFileSelfSigned = Path.GetTempFileName() + "_SelfSigned.pfx"; diff --git a/CoseSignTool.Tests/ValidateCommandTests.cs b/CoseSignTool.Tests/ValidateCommandTests.cs index 279f6846..36c89d1a 100644 --- a/CoseSignTool.Tests/ValidateCommandTests.cs +++ b/CoseSignTool.Tests/ValidateCommandTests.cs @@ -258,7 +258,8 @@ public void ValidateIndirectSucceedsWithRootPassedIn() coseStream, new FileInfo(Path.Combine(OutputPath!, "UnitTestPayload.json")), [root], - revocationMode); + revocationMode, + allowOutdated: true); result.Success.Should().BeTrue(result.ToString(true, true)); result.ContentValidationType.Should().Be(ContentValidationType.Indirect); diff --git a/README.md b/README.md index 5dc22c5c..ef8c0ac6 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,6 @@ The planned work is currently tracked only in an internal Microsoft ADO instance ## Requirements CoseSignTool runs on .NET 8. It depends on the libraries from this package and [Microsoft.Extensions.Configuration.CommandLine](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.CommandLine) from NuGet package version 7.0.0. -The libraries depend on [System.Formats.Cbor](https://www.nuget.org/packages/System.Formats.Cbor/) version 7.0.0, [System.Security.Cryptography.Cose](https://www.nuget.org/packages/System.Security.Cryptography.Cose) version 7.0.0, and [System.Runtime.Caching](https://www.nuget.org/packages/System.Runtime.Caching) version 7.0.0 via NuGet package. Do not attempt to use later versions of System.Formats.Cbor or System.Security.Cryptography.Cose, as this breaks some of the fundamental data structures the libraries depend on. - The API libraries all run on .NET Standard 2.0. ### Trademarks