From bc0f2eed39cb7f934a53184ba6db42d420309efe Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Wed, 14 Aug 2024 14:53:20 -0700 Subject: [PATCH 01/13] Added support for integer and string headers --- CoseHandler/CoseHandler.cs | 8 +- CoseHandler/CoseHandler.csproj | 1 + CoseHandler/Usings.cs | 2 + .../CoseHeaderFactoryTests.cs | 114 ++++++++++++ .../CoseSign1.Headers.Tests.csproj | 39 ++++ CoseSign1.Headers.Tests/Usings.cs | 6 + CoseSign1.Headers/CoseHeaderExtender.cs | 32 ++++ CoseSign1.Headers/CoseHeaderFactory.cs | 166 ++++++++++++++++++ CoseSign1.Headers/CoseSign1.Headers.csproj | 52 ++++++ .../Interfaces/ICoseHeaderFactory.cs | 38 ++++ CoseSign1.Headers/Local/CoseHeader.cs | 53 ++++++ CoseSign1.Headers/Usings.cs | 12 ++ CoseSign1.Tests.Common/FileSystemUtils.cs | 7 + CoseSignTool.Tests/CoseSignTool.Tests.csproj | 1 + CoseSignTool.Tests/MainTests.cs | 90 +++++++++- .../Properties/launchSettings.json | 8 + CoseSignTool.Tests/SignCommandTests.cs | 49 +++++- CoseSignTool.sln | 28 +++ CoseSignTool/CoseCommand.cs | 44 +++++ CoseSignTool/CoseSignTool.csproj | 2 + CoseSignTool/Local/HeaderStringConverter.cs | 31 ++++ CoseSignTool/SignCommand.cs | 54 +++++- CoseSignTool/Usings.cs | 6 +- 23 files changed, 834 insertions(+), 9 deletions(-) create mode 100644 CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs create mode 100644 CoseSign1.Headers.Tests/CoseSign1.Headers.Tests.csproj create mode 100644 CoseSign1.Headers.Tests/Usings.cs create mode 100644 CoseSign1.Headers/CoseHeaderExtender.cs create mode 100644 CoseSign1.Headers/CoseHeaderFactory.cs create mode 100644 CoseSign1.Headers/CoseSign1.Headers.csproj create mode 100644 CoseSign1.Headers/Interfaces/ICoseHeaderFactory.cs create mode 100644 CoseSign1.Headers/Local/CoseHeader.cs create mode 100644 CoseSign1.Headers/Usings.cs create mode 100644 CoseSignTool.Tests/Properties/launchSettings.json create mode 100644 CoseSignTool/Local/HeaderStringConverter.cs diff --git a/CoseHandler/CoseHandler.cs b/CoseHandler/CoseHandler.cs index de115468..ac474584 100644 --- a/CoseHandler/CoseHandler.cs +++ b/CoseHandler/CoseHandler.cs @@ -14,6 +14,9 @@ public static class CoseHandler // static instance of the factory for creating new CoseSign1Messages private static readonly CoseSign1MessageFactory Factory = new(); + // static instance of the factory for managing headers. + public static readonly CoseHeaderFactory HeaderFactory = CoseHeaderFactory.Instance(); + #region Sign Overloads /// /// Signs the payload content with the supplied certificate and returns a ReadOnlyMemory object containing the COSE signatureFile. @@ -55,11 +58,12 @@ public static ReadOnlyMemory Sign( X509Certificate2 certificate, bool embedSign = false, FileInfo? signatureFile = null, - string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE) + string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE, + ICoseHeaderExtender? headerExtender = null) => Sign( payload, signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(null, certificate), - embedSign, signatureFile, contentType); + embedSign, signatureFile, contentType, headerExtender); /// /// Signs the payload content with the supplied certificate and returns a ReadOnlyMemory object containing the COSE signatureFile. diff --git a/CoseHandler/CoseHandler.csproj b/CoseHandler/CoseHandler.csproj index b5b183c4..3812fda2 100644 --- a/CoseHandler/CoseHandler.csproj +++ b/CoseHandler/CoseHandler.csproj @@ -47,6 +47,7 @@ + diff --git a/CoseHandler/Usings.cs b/CoseHandler/Usings.cs index fa7fdc38..23d52bc5 100644 --- a/CoseHandler/Usings.cs +++ b/CoseHandler/Usings.cs @@ -21,3 +21,5 @@ global using CoseSign1.Certificates.Local; global using CoseSign1.Certificates.Local.Validators; global using CoseSign1.Extensions; +global using CoseSign1.Headers; + diff --git a/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs b/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs new file mode 100644 index 00000000..16117622 --- /dev/null +++ b/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseSign1.Headers.Tests; + +using System; +using System.Security.Cryptography.Cose; +using CoseSign1.Headers.Local; + +public class Tests +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void AddHeadersCountTest() + { + CoseHeaderFactory factory = CoseHeaderFactory.Instance(); + + List> intProtectedHeaders = new(); + List> stringProtectedHeaders = new(); + + List> intUnProtectedHeaders = new(); + + // Add protected headers + intProtectedHeaders.Add(new CoseHeader("header1", 1000, true)); + intProtectedHeaders.Add(new CoseHeader("header2", 87234, true)); + + stringProtectedHeaders.Add(new CoseHeader("header3", "value1", true)); + + // Add unprotected headers + intUnProtectedHeaders.Add(new CoseHeader("header4", 100, false)); + + // Add the headers to the factory + factory.AddProtectedHeaders(intProtectedHeaders); + factory.AddProtectedHeaders(stringProtectedHeaders); + factory.AddUnProtectedHeaders(intUnProtectedHeaders); + + int expectedProtectedHeaderCount = 3; + int expectedUnProtectedHeaderCount = 1; + + Assert.That(factory.ProtectedHeadersCount, Is.EqualTo(expectedProtectedHeaderCount)); + Assert.That(factory.UnProtectedHeadersCount, Is.EqualTo(expectedUnProtectedHeaderCount)); + + factory.Dispose(); + } + + [Test] + public void AddHeadersTest() + { + CoseHeaderFactory factory = CoseHeaderFactory.Instance(); + + CoseHeaderMap coseProtectedHeaders = new(); + coseProtectedHeaders.Add(new CoseHeaderLabel("Label1"), 32); + factory.ExtendProtectedHeaders(coseProtectedHeaders); + + CoseHeaderMap coseUnProtectedHeaders = new(); + coseUnProtectedHeaders.Add(new CoseHeaderLabel("Label2"), "value1"); + factory.ExtendUnProtectedHeaders(coseUnProtectedHeaders); + + List> stringProtectedHeaders = new(); + List> intUnProtectedHeaders = new(); + + stringProtectedHeaders.Add(new CoseHeader("Label3", "value2", true)); + + intUnProtectedHeaders.Add(new CoseHeader("Label4", 45, false)); + intUnProtectedHeaders.Add(new CoseHeader("Label5", 132, false)); + + factory.AddProtectedHeaders(stringProtectedHeaders); + factory.AddUnProtectedHeaders(intUnProtectedHeaders); + + factory.ExtendProtectedHeaders(coseProtectedHeaders); + factory.ExtendUnProtectedHeaders(coseUnProtectedHeaders); + + Assert.That(coseProtectedHeaders.Count, Is.EqualTo(2)); + Assert.That(coseUnProtectedHeaders.Count, Is.EqualTo(3)); + + factory.Dispose(); + } + + [Test] + public void AddEmptyStringValueThrowsExceptionTest() + { + Assert.Throws(() => + { + List> stringProtectedHeaders = new(); + stringProtectedHeaders.Add(new CoseHeader("header3", "", true)); + + CoseHeaderFactory.Instance().AddProtectedHeaders(stringProtectedHeaders); + CoseHeaderFactory.Instance().Dispose(); + }, + "A non-empty string value must be supplied for the header 'header3'"); + } + + [Test] + public void AddNullHeadersThrowsExceptionTest() + { + Assert.Throws(() => { CoseHeaderFactory.Instance().AddProtectedHeaders(null); CoseHeaderFactory.Instance().Dispose(); }, "Protected headers cannot be null"); + Assert.Throws(() => { CoseHeaderFactory.Instance().AddUnProtectedHeaders(null); CoseHeaderFactory.Instance().Dispose(); }, "unProtected headers cannot be null"); + } + + [Test] + public void AddUnsupportedValueTypeTest() + { + Assert.Throws(() => + { + CoseHeaderFactory.Instance().AddProtectedHeaders(new List> { new ("key1", 100, true) }); + CoseHeaderFactory.Instance().Dispose(); + }, + $"A header value of type {typeof(long)} is unsupported"); + } +} \ No newline at end of file diff --git a/CoseSign1.Headers.Tests/CoseSign1.Headers.Tests.csproj b/CoseSign1.Headers.Tests/CoseSign1.Headers.Tests.csproj new file mode 100644 index 00000000..d4a8ad53 --- /dev/null +++ b/CoseSign1.Headers.Tests/CoseSign1.Headers.Tests.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + false + false + latest + True + True + ..\StrongNameKeys\35MSSharedLib1024.snk + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/CoseSign1.Headers.Tests/Usings.cs b/CoseSign1.Headers.Tests/Usings.cs new file mode 100644 index 00000000..04496960 --- /dev/null +++ b/CoseSign1.Headers.Tests/Usings.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using System.Collections.Generic; +global using NUnit.Framework; + diff --git a/CoseSign1.Headers/CoseHeaderExtender.cs b/CoseSign1.Headers/CoseHeaderExtender.cs new file mode 100644 index 00000000..af377e39 --- /dev/null +++ b/CoseSign1.Headers/CoseHeaderExtender.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseSign1.Headers; + +/// +/// An implementation of the header extender. +/// +public class CoseHeaderExtender : ICoseHeaderExtender +{ + /// + /// Add protected headers supplied by the user to the supplied header map. + /// + /// The header map where user supplied protected header(s) will be added. + /// A header map with user supplied protected headers. + public CoseHeaderMap ExtendProtectedHeaders(CoseHeaderMap protectedHeaders) + { + CoseHeaderFactory.Instance().ExtendProtectedHeaders(protectedHeaders); + return protectedHeaders; + } + + /// + /// Add unprotected headers supplied by the user to the supplied header map. + /// + /// The header map where user supplied unprotected header(s) will be added. + /// A header map with user supplied unprotected headers. + public CoseHeaderMap ExtendUnProtectedHeaders(CoseHeaderMap? unProtectedHeaders) + { + CoseHeaderFactory.Instance().ExtendUnProtectedHeaders(unProtectedHeaders); + return unProtectedHeaders; + } +} diff --git a/CoseSign1.Headers/CoseHeaderFactory.cs b/CoseSign1.Headers/CoseHeaderFactory.cs new file mode 100644 index 00000000..61d8648b --- /dev/null +++ b/CoseSign1.Headers/CoseHeaderFactory.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseSign1.Headers; + +/// +/// A factory class to manage protected and unprotected headers. +/// +public sealed class CoseHeaderFactory : ICoseHeaderFactory, IDisposable +{ + /// + /// A single instance of the factory class. + /// + private static CoseHeaderFactory? SingletonInstance = null; + + /// + /// A collection of headers where the value is an integer. + /// CoseHeaderMap value accepts Int32 only. + /// + private List> IntHeaders { get; set; } + + /// + /// A collection of headers where the value is a string. + /// + private List> StringHeaders { get; set; } + + /// + /// Count of protected headers. + /// + public int ProtectedHeadersCount => IntHeaders.Where(h => h.IsProtected).ToList().Count() + StringHeaders.Where(h => h.IsProtected).ToList().Count(); + + /// + /// Count of unprotected headers. + /// + public int UnProtectedHeadersCount => IntHeaders.Where(h => !h.IsProtected).ToList().Count() + StringHeaders.Where(h => !h.IsProtected).ToList().Count(); + + /// + /// A private constructor. + /// + private CoseHeaderFactory() + { + IntHeaders = new(); + StringHeaders = new(); + } + + /// + /// Returns the singleton instance of the factory class. + /// + /// The singleton instance of the factory class. + public static CoseHeaderFactory Instance() + { + if (SingletonInstance == null) + { + SingletonInstance = new(); + } + + return SingletonInstance; + } + + /// + /// A helper method to add headers to the internal header collection. + /// + /// Data type of the header value. + /// A collection of user supplied headers. + /// A flag to indicate if the collection is protected. + /// A non-empty string header value is expected. + private void AddHeadersInternal(IEnumerable> headers, bool isProtected) + { + switch (typeof(TypeV)) + { + case var x when x == typeof(int): + headers.ToList().ForEach(h => + { + // We do not validate the supplied value as it is strongly typed to int. + // Caller is responsible for validations. + IntHeaders.Add(new CoseHeader(h.Label, Convert.ToInt32(h.Value), isProtected)); + }); + + break; + case var x when x == typeof(string): + headers.ToList().ForEach(h => + { + // We do not allow null or empty string as a string value although the caller might. + if (!CoseHeader.IsValid((value) => {return string.IsNullOrEmpty(value) ? false : true;}, h.Value.ToString())) + { + throw new ArgumentException($"A non-empty string value must be supplied for the header '{h.Label}'"); + } + + StringHeaders.Add(new CoseHeader(h.Label, h.Value.ToString(), isProtected)); + }); + + break; + default: + throw new NotImplementedException($"A header value of type {typeof(TypeV)} is unsupported"); + } + } + + /// + /// A wrapper method to facilitate the addition of protected headers to the internal collection. + /// + /// The data type of the header value. + /// A collection containing the protected headers. + public void AddProtectedHeaders(IEnumerable> headers) + { + if(headers == null) + { + throw new ArgumentNullException("Protected headers cannot be null"); + } + + AddHeadersInternal(headers, true); + } + /// + /// A wrapper method to facilitate the addition of unprotected headers to the internal collection. + /// + /// The data type of the header value. + /// A collection containing the unprotected headers. + public void AddUnProtectedHeaders(IEnumerable> headers) + { + if (headers == null) + { + throw new ArgumentNullException("UnProtected headers cannot be null"); + } + + AddHeadersInternal(headers, false); + } + + /// + /// Add the protected headers to the supplied header map. + /// + /// The user-supplied protected headers will be added to this map. + public void ExtendProtectedHeaders(CoseHeaderMap protectedHeaders) + { + if (protectedHeaders == null) + { + return; + } + + IntHeaders.Where(h => h.IsProtected).ToList().ForEach(h => protectedHeaders.Add(new CoseHeaderLabel(h.Label), h.Value)); + StringHeaders.Where(h => h.IsProtected).ToList().ForEach(h => protectedHeaders.Add(new CoseHeaderLabel(h.Label), h.Value)); + } + + /// + /// Add the unprotected headers to the supplied header map. + /// + /// The user-supplied unprotected headers will be added to this map. + public void ExtendUnProtectedHeaders(CoseHeaderMap unProtectedHeaders) + { + if (unProtectedHeaders == null) + { + return; + } + + IntHeaders.Where(h => !h.IsProtected).ToList().ForEach(h => unProtectedHeaders.Add(new CoseHeaderLabel(h.Label), h.Value)); + StringHeaders.Where(h => !h.IsProtected).ToList().ForEach(h => unProtectedHeaders.Add(new CoseHeaderLabel(h.Label), h.Value)); + } + + /// + /// Dispose. + /// + public void Dispose() + { + IntHeaders.Clear(); + StringHeaders.Clear(); + SingletonInstance = null; + } +} diff --git a/CoseSign1.Headers/CoseSign1.Headers.csproj b/CoseSign1.Headers/CoseSign1.Headers.csproj new file mode 100644 index 00000000..a88c0124 --- /dev/null +++ b/CoseSign1.Headers/CoseSign1.Headers.csproj @@ -0,0 +1,52 @@ + + + + + netstandard2.0 + latest + + + + + true + enable + true + true + latest + true + + + + + True + True + ..\StrongNameKeys\35MSSharedLib1024.snk + + + + + $(MsBuildProjectName) + $(VersionNgt) + Microsoft + LICENSE + false + readme.md + ChangeLog.md + Abstractions and classes required to extend or enhance Microsoft.CoseSign1.Abstractions for all certificate based signing. + + + + + + + + + + + + + + + + + diff --git a/CoseSign1.Headers/Interfaces/ICoseHeaderFactory.cs b/CoseSign1.Headers/Interfaces/ICoseHeaderFactory.cs new file mode 100644 index 00000000..02941c41 --- /dev/null +++ b/CoseSign1.Headers/Interfaces/ICoseHeaderFactory.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseSign1.Headers.Interfaces; + +/// +/// An interface to manage the protected and unprotected headers. +/// +public interface ICoseHeaderFactory +{ + /// + /// Add protected headers to the supplied header map. + /// + /// A collection of protected headers. + void ExtendProtectedHeaders(CoseHeaderMap protectedHeaders); + + /// + /// Add unprotected headers to the supplied header map. + /// + /// A collection of unprotected headers. + void ExtendUnProtectedHeaders(CoseHeaderMap unProtectedHeaders); + + /// + /// Adds the supplied headers to an internal collection representing the protected headers. + /// The headers in this collection will be signed and added to the Cose envelop. + /// + /// Data type of the header value + /// A collection of protected headers. + void AddProtectedHeaders(IEnumerable> headers); + + /// + /// Adds the supplied headers to and internal collection representing the unprotected headers. + /// The headers in this collection will be added to the Cose envelop. + /// + /// Data type of the header value + /// A collection of unprotected headers + void AddUnProtectedHeaders(IEnumerable> headers); +} diff --git a/CoseSign1.Headers/Local/CoseHeader.cs b/CoseSign1.Headers/Local/CoseHeader.cs new file mode 100644 index 00000000..1545f541 --- /dev/null +++ b/CoseSign1.Headers/Local/CoseHeader.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseSign1.Headers.Local; + +/// +/// A type to represent a header. +/// +/// The data type of the header value. +public class CoseHeader +{ + /// + /// Gets or sets the Header label. + /// + [JsonProperty(Required = Required.Always, PropertyName = "label")] + public string Label { get; set; } + + /// + /// Gets or sets the Header value. + /// + [JsonProperty(Required = Required.Always, PropertyName = "value")] + public TypeV Value { get; set; } + + /// + /// Gets or sets a value to indicate if this header is protected. + /// + [JsonProperty(PropertyName = "protected", DefaultValueHandling = DefaultValueHandling.Populate)] + public bool IsProtected { get; set; } + + /// + /// Creates a new instance of this type. + /// + /// The header label + /// The header value. + /// A flag to indicate if the header is protected. + public CoseHeader(string label, TypeV value, bool isProtected) + { + Label = label; + Value = value; + IsProtected = isProtected; + } + + /// + /// A method to check if the header value is valid. + /// + /// A function delegate that takes the value as input and returns a bool + /// The header value + /// True to indicate a valid value. False, otherwise. + public static bool IsValid(Func validate, TypeV value) + { + return validate(value); + } +} diff --git a/CoseSign1.Headers/Usings.cs b/CoseSign1.Headers/Usings.cs new file mode 100644 index 00000000..23f9bbe8 --- /dev/null +++ b/CoseSign1.Headers/Usings.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using System; +global using System.Collections.Generic; +global using System.ComponentModel; +global using System.Linq; +global using System.Security.Cryptography.Cose; +global using CoseSign1.Abstractions.Interfaces; +global using CoseSign1.Headers.Interfaces; +global using CoseSign1.Headers.Local; +global using Newtonsoft.Json; diff --git a/CoseSign1.Tests.Common/FileSystemUtils.cs b/CoseSign1.Tests.Common/FileSystemUtils.cs index 4a64263d..1822e573 100644 --- a/CoseSign1.Tests.Common/FileSystemUtils.cs +++ b/CoseSign1.Tests.Common/FileSystemUtils.cs @@ -43,4 +43,11 @@ public static string GeneratePayloadFile([CallerMemberName] string caller = "", File.WriteAllBytes(fileName, bytes); return new(fileName); } + + public static string GenerateHeadersFile(string? content = null) + { + string fileName = Path.GetTempFileName().Replace(".tmp", $".headers.json"); + File.WriteAllText(fileName, content); + return new(fileName); + } } \ No newline at end of file diff --git a/CoseSignTool.Tests/CoseSignTool.Tests.csproj b/CoseSignTool.Tests/CoseSignTool.Tests.csproj index 14ec0f9b..651d9720 100644 --- a/CoseSignTool.Tests/CoseSignTool.Tests.csproj +++ b/CoseSignTool.Tests/CoseSignTool.Tests.csproj @@ -15,6 +15,7 @@ None true + x64 diff --git a/CoseSignTool.Tests/MainTests.cs b/CoseSignTool.Tests/MainTests.cs index 0f6f3461..e390a351 100644 --- a/CoseSignTool.Tests/MainTests.cs +++ b/CoseSignTool.Tests/MainTests.cs @@ -214,4 +214,92 @@ public void ReturnsHelpRequestedWhenNoOptionsAfterVerb() string[] args = [ "sign" ]; CoseSignTool.Main(args).Should().Be((int)ExitCode.HelpRequested); } -} \ No newline at end of file + + [TestMethod] + public void SignWithIntegerHeadersSuccess() + { + string integerHeadersFile = FileSystemUtils.GenerateHeadersFile(@"[{""label"":""created-at"",""value"":1723588348,""protected"":true}]"); + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign with integer headers + string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/ih", integerHeadersFile]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.Success, "Payload must be signed."); + } + + [TestMethod] + public void SignWithMissingValueIntegerHeaders() + { + string integerHeadersFile = FileSystemUtils.GenerateHeadersFile(@"[{""label"":""created-at"",""value"":,""protected"":true}]"); + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign with integer headers + string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/ih", integerHeadersFile]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Invalid integer header value."); + } + + [TestMethod] + public void SignWithOutOfRangeValueIntegerHeaders() + { + string integerHeadersFile = FileSystemUtils.GenerateHeadersFile(@"[{""label"":""created-at"",""value"":-999999999999999,""protected"":true}]"); + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign with integer headers + string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/ih", integerHeadersFile]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Invalid integer header value."); + } + + [TestMethod] + public void SignWithDeserializationErrorIntegerHeaders() + { + string integerHeadersFile = FileSystemUtils.GenerateHeadersFile(@"[{""label"":""created-at"","""":-999999999999999,""protected"":true}]"); + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign with integer headers + string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/ih", integerHeadersFile]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Integer headers file deserialization error."); + } + + [TestMethod] + public void SignWithMissingIntegerHeadersFile() + { + string integerHeadersFile = FileSystemUtils.GenerateHeadersFile(@"[{""label"":""created-at"",""value"":2312345,""protected"":true}]"); + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign with integer headers + string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/ih"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UserSpecifiedFileNotFound, "Missing integer headers file."); + } + + [TestMethod] + public void SignWithMissingStringHeadersFile() + { + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign with string headers + string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/sh"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UserSpecifiedFileNotFound, "Missing string headers file."); + } + + [TestMethod] + public void SignWithMissingValueStringHeaders() + { + string headersFile = FileSystemUtils.GenerateHeadersFile(@"[{""label"":""created-at"",""value"":"""",""protected"":false}]"); + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign with string headers + string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/sh", headersFile]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.MissingRequiredOption, "Missing value in string headers file."); + } + + [TestMethod] + public void SignWithDeserializationErrorStringHeaders() + { + string headersFile = FileSystemUtils.GenerateHeadersFile(@"[{""label"":""created-at"",""protected"":false}]"); + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign with string headers + string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/sh", headersFile]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "String headers file could not be deserialized."); + } +} + diff --git a/CoseSignTool.Tests/Properties/launchSettings.json b/CoseSignTool.Tests/Properties/launchSettings.json new file mode 100644 index 00000000..9eaad5a9 --- /dev/null +++ b/CoseSignTool.Tests/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "CoseSignTool.Tests": { + "commandName": "Project", + "commandLineArgs": "sign /p \"C:\\Users\\settiy.NORTHAMERICA\\Downloads\\remove_member.json\" /pfx \"C:\\Users\\settiy.NORTHAMERICA\\Downloads\\member1.pfx\" /ep /po /sh" + } + } +} \ No newline at end of file diff --git a/CoseSignTool.Tests/SignCommandTests.cs b/CoseSignTool.Tests/SignCommandTests.cs index 972f8538..67d99af5 100644 --- a/CoseSignTool.Tests/SignCommandTests.cs +++ b/CoseSignTool.Tests/SignCommandTests.cs @@ -3,7 +3,52 @@ namespace CoseSignTool.Tests; -internal class SignCommandTests +[TestClass] +public class SignCommandTests { - // Placeholder for tests for the SignCommand class + // Certificates + private static readonly X509Certificate2 SelfSignedCert = TestCertificateUtils.CreateCertificate(nameof(SignCommandTests) + " self signed"); // A self-signed cert + private static readonly X509Certificate2Collection CertChain1 = TestCertificateUtils.CreateTestChain(nameof(SignCommandTests) + " set 1"); // A complete cert chain + private static readonly X509Certificate2 Root1Priv = CertChain1[0]; + private static readonly X509Certificate2 Int1Priv = CertChain1[1]; + private static readonly X509Certificate2 Leaf1Priv = CertChain1[^1]; + + // File paths to export them to + private static readonly string PrivateKeyCertFileSelfSigned = Path.GetTempFileName() + "_SelfSigned.pfx"; + private static readonly string PublicKeyCertFileSelfSigned = Path.GetTempFileName() + "_SelfSigned.cer"; + private static readonly string PrivateKeyRootCertFile = Path.GetTempFileName() + ".pfx"; + private static readonly string PublicKeyIntermediateCertFile = Path.GetTempFileName() + ".cer"; + private static readonly string PublicKeyRootCertFile = Path.GetTempFileName() + ".cer"; + private static readonly string PrivateKeyCertFileChained = Path.GetTempFileName() + ".pfx"; + private static readonly string PrivateKeyCertFileChainedWithPassword = Path.GetTempFileName() + ".pfx"; + private static readonly string CertPassword = Guid.NewGuid().ToString(); + + public SignCommandTests() + { + // export generated certs to files + File.WriteAllBytes(PrivateKeyCertFileSelfSigned, SelfSignedCert.Export(X509ContentType.Pkcs12)); + File.WriteAllBytes(PublicKeyCertFileSelfSigned, SelfSignedCert.Export(X509ContentType.Cert)); + File.WriteAllBytes(PrivateKeyRootCertFile, Root1Priv.Export(X509ContentType.Pkcs12)); + File.WriteAllBytes(PublicKeyRootCertFile, Root1Priv.Export(X509ContentType.Cert)); + File.WriteAllBytes(PublicKeyIntermediateCertFile, Int1Priv.Export(X509ContentType.Cert)); + File.WriteAllBytes(PrivateKeyCertFileChained, Leaf1Priv.Export(X509ContentType.Pkcs12)); + File.WriteAllBytes(PrivateKeyCertFileChainedWithPassword, Leaf1Priv.Export(X509ContentType.Pkcs12, CertPassword)); + } + + [TestMethod] + public void SignWithDefaultProtectedFlagInHeaderFile() + { + string headersFile = FileSystemUtils.GenerateHeadersFile(@"[{""label"":""created-at"",""value"":190}]"); + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign + string[] args = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileSelfSigned, @"/ih", headersFile, @"/ep"]; + var provider = CoseCommand.LoadCommandLineArgs(args, SignCommand.Options, out string? badArg)!; + badArg.Should().BeNull("badArg should be null."); + + var cmd1 = new SignCommand(); + cmd1.ApplyOptions(provider); + + cmd1.IntHeaders.ForEach(h => h.IsProtected.Should().Be(false, "Protected flag is not set to default value of false when unsupplied")); + } } diff --git a/CoseSignTool.sln b/CoseSignTool.sln index 61c69fa2..7fb758c3 100644 --- a/CoseSignTool.sln +++ b/CoseSignTool.sln @@ -62,6 +62,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{ .github\workflows\OpenPR.yml = .github\workflows\OpenPR.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Headers", "CoseSign1.Headers\CoseSign1.Headers.csproj", "{DE822040-D5AB-41C8-83B2-4938FF00C7E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Headers.Tests", "CoseSign1.Headers.Tests\CoseSign1.Headers.Tests.csproj", "{5181310A-CA82-4399-9197-86B468F7FCE9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -216,6 +220,30 @@ Global {58984C00-6EA6-47ED-AF9D-717B187A168B}.Release|ARM64.Build.0 = Release|Any CPU {58984C00-6EA6-47ED-AF9D-717B187A168B}.Release|x64.ActiveCfg = Release|Any CPU {58984C00-6EA6-47ED-AF9D-717B187A168B}.Release|x64.Build.0 = Release|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Debug|ARM64.Build.0 = Debug|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Debug|x64.Build.0 = Debug|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Release|Any CPU.Build.0 = Release|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Release|ARM64.ActiveCfg = Release|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Release|ARM64.Build.0 = Release|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Release|x64.ActiveCfg = Release|Any CPU + {DE822040-D5AB-41C8-83B2-4938FF00C7E5}.Release|x64.Build.0 = Release|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Debug|ARM64.Build.0 = Debug|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Debug|x64.ActiveCfg = Debug|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Debug|x64.Build.0 = Debug|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Release|Any CPU.Build.0 = Release|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Release|ARM64.ActiveCfg = Release|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Release|ARM64.Build.0 = Release|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Release|x64.ActiveCfg = Release|Any CPU + {5181310A-CA82-4399-9197-86B468F7FCE9}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CoseSignTool/CoseCommand.cs b/CoseSignTool/CoseCommand.cs index 8af9ed9f..463e2981 100644 --- a/CoseSignTool/CoseCommand.cs +++ b/CoseSignTool/CoseCommand.cs @@ -3,6 +3,9 @@ namespace CoseSignTool; +using System; + + /// /// A base class for console commands that handle COSE signatures. /// @@ -208,6 +211,47 @@ protected static string[] GetOptionArray(CommandLineConfigurationProvider provid } } + /// + /// Checks whether a header type command line option has been set. + /// + /// A CommandLineConfigurationProvider object to make the check. + /// The name of the command line option. + /// Optional. A default value to use if the option was not set. Defaults to null. + /// The comma-separated list the option was set to on the command line, split into an array, or the default value otherwise. + [return: NotNullIfNotNull(nameof(defaultValue))] + protected static List>? GetOptionHeaders(CommandLineConfigurationProvider provider, string name, List>? defaultValue = null, JsonConverter? converter = null) + { + FileInfo? file = GetOptionFile(provider, name, null); + + if (file == null) + { + return defaultValue; + } + + try + { + using StreamReader reader = new StreamReader(file.FullName) ; + string json = reader.ReadToEnd(); + List> headers = converter != null ? JsonConvert.DeserializeObject>>(json, converter) : JsonConvert.DeserializeObject>>(json); + return headers; + } + catch(Exception ex) + { + if (ex is ArgumentException) + { + throw; + } + else if (ex is FileNotFoundException) + { + throw new FileNotFoundException($"The file specified in /{name} was not found: {file.FullName}"); + } + else + { + throw new ArgumentException($"Input file '{file.FullName}' could not be parsed. {ex.Message}"); + } + } + } + // One liner for file existence checks protected static void ThrowIfMissing(string file, string message) { diff --git a/CoseSignTool/CoseSignTool.csproj b/CoseSignTool/CoseSignTool.csproj index c1c3b337..cb92190c 100644 --- a/CoseSignTool/CoseSignTool.csproj +++ b/CoseSignTool/CoseSignTool.csproj @@ -46,12 +46,14 @@ + + diff --git a/CoseSignTool/Local/HeaderStringConverter.cs b/CoseSignTool/Local/HeaderStringConverter.cs new file mode 100644 index 00000000..5aab2c2a --- /dev/null +++ b/CoseSignTool/Local/HeaderStringConverter.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseSignTool.Local; + +internal class HeaderStringConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(string); + } + + /// + /// Ensure that the value is not null or empty. + /// + /// + /// + /// + /// + /// + /// + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + return reader.Value == null || string.IsNullOrEmpty(reader.Value.ToString()) ? throw new ArgumentNullException("String header value cannot be null or empty.") : reader.Value.ToString(); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} diff --git a/CoseSignTool/SignCommand.cs b/CoseSignTool/SignCommand.cs index e0ac5c48..3cd23c8f 100644 --- a/CoseSignTool/SignCommand.cs +++ b/CoseSignTool/SignCommand.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. namespace CoseSignTool; @@ -29,12 +29,20 @@ public class SignCommand : CoseCommand ["-sl"] = "StoreLocation", ["-ContentType"] = "ContentType", ["-cty"] = "ContentType", + ["-IntHeaders"] = "IntHeaders", + ["-ih"] = "IntHeaders", + ["-StringHeaders"] = "StringHeaders", + ["-sh"] = "StringHeaders" }; // Inherited default values private const string DefaultStoreName = "My"; private const string DefaultStoreLocation = "CurrentUser"; + private IEnumerable> ProtectedHeadersInteger { get; set; } + + private IEnumerable> ProtectedHeadersString { get; set; } + // public static new readonly Dictionary Options = CoseCommand.Options.Concat(PrivateOptions).ToDictionary(k => k.Key, k => k.Value); @@ -83,6 +91,17 @@ public class SignCommand : CoseCommand /// Optional. Gets or sets the content type of the payload to be set in protected header. Default value is "application/cose". /// public string? ContentType { get; set; } + + /// + /// Optional. Gets or sets the headers with Int32 values. + /// + public List>? IntHeaders { get; set; } + + /// + /// Optional. Gets or sets the headers with string values. + /// + public List>? StringHeaders { get; set; } + #endregion /// @@ -143,9 +162,30 @@ public override ExitCode Run() } try - { + { + // Extend the headers. + CoseHeaderExtender? headerExtender = null; + + if (IntHeaders != null || + StringHeaders != null) + { + headerExtender = new(); + } + + if (IntHeaders != null) + { + CoseHandler.HeaderFactory.AddProtectedHeaders(IntHeaders.ToList().Where(h => h.IsProtected)); + CoseHandler.HeaderFactory.AddUnProtectedHeaders(IntHeaders.ToList().Where(h => !h.IsProtected)); + } + + if(StringHeaders != null) + { + CoseHandler.HeaderFactory.AddProtectedHeaders(StringHeaders.ToList().Where(h => h.IsProtected)); + CoseHandler.HeaderFactory.AddUnProtectedHeaders(StringHeaders.ToList().Where(h => !h.IsProtected)); + } + // Sign the content. - ReadOnlyMemory signedBytes = CoseHandler.Sign(payloadStream, cert, EmbedPayload, SignatureFile, ContentType ?? CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE); + ReadOnlyMemory signedBytes = CoseHandler.Sign(payloadStream, cert, EmbedPayload, SignatureFile, ContentType ?? CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE, headerExtender); // Write the signature to stream or file. if (PipeOutput) @@ -187,6 +227,8 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p StoreName = GetOptionString(provider, nameof(StoreName), DefaultStoreName); string? sl = GetOptionString(provider, nameof(StoreLocation), DefaultStoreLocation); StoreLocation = sl is not null ? Enum.Parse(sl) : StoreLocation.CurrentUser; + IntHeaders = GetOptionHeaders(provider, nameof(IntHeaders), null); + StringHeaders = GetOptionHeaders(provider, nameof(StringHeaders), null, new HeaderStringConverter()); base.ApplyOptions(provider); } @@ -265,5 +307,11 @@ to file. ContentType /cty: Optional. A MIME type to specify as Content Type in the COSE signature header. Default value is 'application/cose'. + + IntHeaders /ih: Optional. Path to a JSON file with headers to add to the signed message. The label is string and the value is int32. + JSON format is [{""label"":""created-at"",""value"":1723672289,""protected"":true},...]. Protected is optional and when ignored, it is set to false. + + StringHeaders /sh: Optional. Path to a JSON file with headers to add to the signed message. The label and value are strings. + JSON format is [{""label"":""company"",""value"":""Microsoft"",""protected"":true},...]. Protected is optional and when ignored, it is set to false. "; } diff --git a/CoseSignTool/Usings.cs b/CoseSignTool/Usings.cs index df1e0da7..304874f8 100644 --- a/CoseSignTool/Usings.cs +++ b/CoseSignTool/Usings.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. global using System; @@ -16,7 +16,11 @@ global using CoseSign1.Certificates.Exceptions; global using CoseSign1.Certificates.Extensions; global using CoseSign1.Extensions; +global using CoseSign1.Headers; +global using CoseSign1.Headers.Local; global using CoseX509; global using Microsoft.Extensions.Configuration.CommandLine; +global using Newtonsoft.Json; +global using CoseSignTool.Local; From 4c1996b2f8e2227ca6d6efea86387e05c2db2242 Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Wed, 14 Aug 2024 23:51:01 -0700 Subject: [PATCH 02/13] Added command line option to provide headers --- CoseSignTool/CoseCommand.cs | 38 +++++++++++++++++- CoseSignTool/SignCommand.cs | 79 +++++++++++++++++++++++++++++++++++-- docs/CoseSignTool.md | 2 + 3 files changed, 115 insertions(+), 4 deletions(-) diff --git a/CoseSignTool/CoseCommand.cs b/CoseSignTool/CoseCommand.cs index 463e2981..33464e86 100644 --- a/CoseSignTool/CoseCommand.cs +++ b/CoseSignTool/CoseCommand.cs @@ -219,7 +219,7 @@ protected static string[] GetOptionArray(CommandLineConfigurationProvider provid /// Optional. A default value to use if the option was not set. Defaults to null. /// The comma-separated list the option was set to on the command line, split into an array, or the default value otherwise. [return: NotNullIfNotNull(nameof(defaultValue))] - protected static List>? GetOptionHeaders(CommandLineConfigurationProvider provider, string name, List>? defaultValue = null, JsonConverter? converter = null) + protected static List>? GetOptionHeadersFromFile(CommandLineConfigurationProvider provider, string name, List>? defaultValue = null, JsonConverter? converter = null) { FileInfo? file = GetOptionFile(provider, name, null); @@ -252,6 +252,42 @@ protected static string[] GetOptionArray(CommandLineConfigurationProvider provid } } + /// + /// Checks whether a header type command line option has been set. + /// + /// A CommandLineConfigurationProvider object to make the check. + /// The name of the command line option. + /// A flag to indicate if the header is protected. + /// A method to convert the header value to the required type. + /// A collection of headers. + /// The comma-separated list the option was set to on the command line, split into an array, or the default value otherwise. + protected static void GetOptionHeadersFromCommandLine(CommandLineConfigurationProvider provider, string name, bool isProtected, Func? converter = null, List>? headers = null) + { + string[] inputs = GetOptionArray(provider, name); + + if (inputs.Length == 0) + { + return; + } + + if (headers == null) + { + headers = new(); + } + + try + { + inputs.ToList().ForEach(header => { + string[] labelValue = header.Split("="); + headers.Add(new CoseHeader(labelValue[0], converter(labelValue), isProtected)); + }); + } + catch (Exception) + { + throw; + } + } + // One liner for file existence checks protected static void ThrowIfMissing(string file, string message) { diff --git a/CoseSignTool/SignCommand.cs b/CoseSignTool/SignCommand.cs index 3cd23c8f..091a6662 100644 --- a/CoseSignTool/SignCommand.cs +++ b/CoseSignTool/SignCommand.cs @@ -32,7 +32,15 @@ public class SignCommand : CoseCommand ["-IntHeaders"] = "IntHeaders", ["-ih"] = "IntHeaders", ["-StringHeaders"] = "StringHeaders", - ["-sh"] = "StringHeaders" + ["-sh"] = "StringHeaders", + ["-IntProtectedHeaders"] = "IntProtectedHeaders", + ["-iph"] = "IntProtectedHeaders", + ["-StringProtectedHeaders"] = "StringProtectedHeaders", + ["-sph"] = "StringProtectedHeaders", + ["-IntUnProtectedHeaders"] = "IntUnProtectedHeaders", + ["-iuh"] = "IntUnProtectedHeaders", + ["-StringUnProtectedHeaders"] = "StringUnProtectedHeaders", + ["-suh"] = "StringUnProtectedHeaders" }; // Inherited default values @@ -227,11 +235,76 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p StoreName = GetOptionString(provider, nameof(StoreName), DefaultStoreName); string? sl = GetOptionString(provider, nameof(StoreLocation), DefaultStoreLocation); StoreLocation = sl is not null ? Enum.Parse(sl) : StoreLocation.CurrentUser; - IntHeaders = GetOptionHeaders(provider, nameof(IntHeaders), null); - StringHeaders = GetOptionHeaders(provider, nameof(StringHeaders), null, new HeaderStringConverter()); + IntHeaders = GetOptionHeadersFromFile(provider, nameof(IntHeaders), null); + StringHeaders = GetOptionHeadersFromFile(provider, nameof(StringHeaders), null, new HeaderStringConverter()); + + if (IntHeaders == null) + { + IntHeaders = new(); + + // IntProtectedHeaders + GetOptionHeadersFromCommandLine(provider, "IntProtectedHeaders", true, HeaderValueConverter, IntHeaders); + + // IntUnProtectedHeaders + GetOptionHeadersFromCommandLine(provider, "IntUnProtectedHeaders", false, HeaderValueConverter, IntHeaders); + } + + if (StringHeaders == null) + { + StringHeaders = new(); + + // StringProtectedHeaders + GetOptionHeadersFromCommandLine(provider, "StringProtectedHeaders", true, HeaderValueConverter, StringHeaders); + + // StringUnProtectedHeaders + GetOptionHeadersFromCommandLine(provider, "StringUnProtectedHeaders", false, HeaderValueConverter, StringHeaders); + } + base.ApplyOptions(provider); } + /// + /// A helper method to convert the header value to the required type. + /// + /// The type of the header value. + /// A string array containing the header label and value. + /// The value converted to the correct type. + /// Throws if the conversion failed. + private static TypeV HeaderValueConverter(string[]? labelValue = null) + { + if(labelValue == null || labelValue.Length < 2) + { + throw new ArgumentException("Invalid header. Header label and value must be provided."); + } + + // Validate label + if (string.IsNullOrEmpty(labelValue[0])) + { + throw new ArgumentException("Header label cannot be null"); + } + + // Validate value + switch (typeof(TypeV)) + { + case var x when x == typeof(int): + if (!int.TryParse(labelValue[1], out int value)) + { + throw new ArgumentException($"Invalid header int32 value {labelValue[1]}"); + } + + return (TypeV)Convert.ChangeType(value, typeof(TypeV)); + case var x when x == typeof(string): + if(string.IsNullOrEmpty(labelValue[1])) + { + throw new ArgumentException($"Invalid header string value {labelValue[1]}"); + } + + return (TypeV)Convert.ChangeType(labelValue[1], typeof(TypeV)); + default: + throw new ArgumentException($"Header value of type {typeof(TypeV)} is not supported."); + } + } + /// /// Tries to load the certificate to sign with. /// diff --git a/docs/CoseSignTool.md b/docs/CoseSignTool.md index 184a1d07..98d1013c 100644 --- a/docs/CoseSignTool.md +++ b/docs/CoseSignTool.md @@ -20,6 +20,8 @@ You may also want to specify: 1. Specify an output file with **/SignatureFile** 1. Let CoseSignTool decide. It will write to *payload-file*.cose for detached or *payload-file*.csm for embedded signatures. But if you don't specify a payload file at all, it will exit with an error. * What certificate store to use. If you passed in a thumbprint instead of a .pfx certificate, CoseSignTool will assume that certificate is in the default store (My/CurrentUser on Windows) unless you tell it otherwise. Use the **/StoreName** and **/StoreLocation** options to specify a store. +* Headers: +* Currently, integers and string headers are supported. For both types, the header label is a string and the value is either an int32 or a string. >Pro tip: Certificate store operations run faster if you use a custom store containing only the certificates you will sign with. You can create a custom store by adding a certificate to a store with a unique Store Name and pre-defined Store Location. For example, in Powershell: ~~~ Import-Certificate -FilePath 'c:\my\cert.pfx' -CertStoreLocation 'Cert:CurrentUser\MyNewStore' From a4b517b3eb85c56ed6c32c0127873effdb8f6999 Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Thu, 15 Aug 2024 13:08:49 -0700 Subject: [PATCH 03/13] Update documentation on cosesigntool.md --- CoseSignTool.Tests/MainTests.cs | 83 ++++++++++++++++++++++++++ CoseSignTool.Tests/SignCommandTests.cs | 20 +++++++ CoseSignTool/SignCommand.cs | 4 +- docs/CoseSignTool.md | 48 ++++++++++++++- 4 files changed, 151 insertions(+), 4 deletions(-) diff --git a/CoseSignTool.Tests/MainTests.cs b/CoseSignTool.Tests/MainTests.cs index e390a351..cf1b83fb 100644 --- a/CoseSignTool.Tests/MainTests.cs +++ b/CoseSignTool.Tests/MainTests.cs @@ -301,5 +301,88 @@ public void SignWithDeserializationErrorStringHeaders() string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/sh", headersFile]; CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "String headers file could not be deserialized."); } + + [TestMethod] + public void SignWithCommandLineIntAndStringHeaders() + { + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign with int protected headers + string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/sph", "sph1=sphv1,sph2=sphv2", @"/iph", "iph1=12345,iph2=123"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.Success, "Signing with protected int headers in command line failed."); + + // sign with int and string unprotected headers + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/suh", "suh1=testsigning,suh2=value2", @"/iuh", "iuh1=12345,iuh2=123"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.Success, "Signing with int and string unprotected headers in command line failed."); + + // sign with string unprotected headers + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/suh", "suh3=testsigning,suh4=value2"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.Success, "Signing with unprotected string headers in command line failed."); + + // sign with string protected headers + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/sph", "sph3=testsigning"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.Success, "Signing with protected string headers in command line failed."); + } + + [TestMethod] + public void SignWithCommandLineProtectedAndUnprotectedHeaders() + { + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign with string protected headers + + } + + [TestMethod] + public void SignWithMissingAndInvalidCommandLineHeaders() + { + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // protected int headers + string[] args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/iph", "created-at=,"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing int value in int protected headers in command line succeeded."); + + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/iph"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing int headers in int protected headers in command line succeeded."); + + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/iph", "created-at=abc"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with invalid int value in headers in int protected headers in command line succeeded."); + + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/iph", "created-at"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing int value in headers in int protected headers in command line succeeded."); + + // protected string headers + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/sph", "message-type=,"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing string value in string protected headers in command line succeeded."); + + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/sph"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing string headers in string protected headers in command line succeeded."); + + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/iph", "message-type"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing string value in headers in string protected headers in command line succeeded."); + + // unprotected int headers + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/iuh", "created-at=,"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing int value in int unprotected headers in command line succeeded."); + + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/iuh"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing int headers in int unprotected headers in command line succeeded."); + + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/iuh", "created-at=abc"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with invalid int value in headers in int unprotected headers in command line succeeded."); + + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/iuh", "created-at"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing int value in headers in int unprotected headers in command line succeeded."); + + // unprotected string headers + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/suh", "message-type=,"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing string value in string unprotected headers in command line succeeded."); + + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/suh"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing string headers in string unprotected headers in command line succeeded."); + + args1 = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword, @"/ep", @"/iuh", "message-type"]; + CoseSignTool.Main(args1).Should().Be((int)ExitCode.UnknownError, "Signing with missing string value in headers in string unprotected headers in command line succeeded."); + } } diff --git a/CoseSignTool.Tests/SignCommandTests.cs b/CoseSignTool.Tests/SignCommandTests.cs index 67d99af5..77854e9a 100644 --- a/CoseSignTool.Tests/SignCommandTests.cs +++ b/CoseSignTool.Tests/SignCommandTests.cs @@ -51,4 +51,24 @@ public void SignWithDefaultProtectedFlagInHeaderFile() cmd1.IntHeaders.ForEach(h => h.IsProtected.Should().Be(false, "Protected flag is not set to default value of false when unsupplied")); } + + [TestMethod] + public void SignWithCommandLineAndInputHeaderFile() + { + string headersFile = FileSystemUtils.GenerateHeadersFile(@"[{""label"":""created-at"",""value"":190,""protected"":true},{""label"":""header2"",""value"":88897,""protected"":true}]"); + string payloadFile = FileSystemUtils.GeneratePayloadFile(); + + // sign + // The unprotected header on the command line must be ignored + string[] args = ["sign", @"/p", payloadFile, @"/pfx", PrivateKeyCertFileSelfSigned, @"/ih", headersFile, @"/ep", "iuh", "created-at=1234567"]; + var provider = CoseCommand.LoadCommandLineArgs(args, SignCommand.Options, out string? badArg)!; + badArg.Should().BeNull("badArg should be null."); + + var cmd1 = new SignCommand(); + cmd1.ApplyOptions(provider); + + cmd1.IntHeaders.Count.Should().Be(2, "When both input file and command line headers are supplied, the command line headers are not ignored"); + + cmd1.IntHeaders.ForEach(h => h.IsProtected.Should().Be(true, "Protected flag is not set to default value of false when unsupplied")); + } } diff --git a/CoseSignTool/SignCommand.cs b/CoseSignTool/SignCommand.cs index 091a6662..765cc0a1 100644 --- a/CoseSignTool/SignCommand.cs +++ b/CoseSignTool/SignCommand.cs @@ -180,13 +180,13 @@ public override ExitCode Run() headerExtender = new(); } - if (IntHeaders != null) + if (IntHeaders != null && IntHeaders.Count > 0) { CoseHandler.HeaderFactory.AddProtectedHeaders(IntHeaders.ToList().Where(h => h.IsProtected)); CoseHandler.HeaderFactory.AddUnProtectedHeaders(IntHeaders.ToList().Where(h => !h.IsProtected)); } - if(StringHeaders != null) + if(StringHeaders != null && StringHeaders.Count > 0) { CoseHandler.HeaderFactory.AddProtectedHeaders(StringHeaders.ToList().Where(h => h.IsProtected)); CoseHandler.HeaderFactory.AddUnProtectedHeaders(StringHeaders.ToList().Where(h => !h.IsProtected)); diff --git a/docs/CoseSignTool.md b/docs/CoseSignTool.md index 98d1013c..d240321e 100644 --- a/docs/CoseSignTool.md +++ b/docs/CoseSignTool.md @@ -20,12 +20,56 @@ You may also want to specify: 1. Specify an output file with **/SignatureFile** 1. Let CoseSignTool decide. It will write to *payload-file*.cose for detached or *payload-file*.csm for embedded signatures. But if you don't specify a payload file at all, it will exit with an error. * What certificate store to use. If you passed in a thumbprint instead of a .pfx certificate, CoseSignTool will assume that certificate is in the default store (My/CurrentUser on Windows) unless you tell it otherwise. Use the **/StoreName** and **/StoreLocation** options to specify a store. -* Headers: -* Currently, integers and string headers are supported. For both types, the header label is a string and the value is either an int32 or a string. >Pro tip: Certificate store operations run faster if you use a custom store containing only the certificates you will sign with. You can create a custom store by adding a certificate to a store with a unique Store Name and pre-defined Store Location. For example, in Powershell: ~~~ Import-Certificate -FilePath 'c:\my\cert.pfx' -CertStoreLocation 'Cert:CurrentUser\MyNewStore' ~~~ +* Headers: +* There are two ways to supply headers: (1) command-line, and, (2) a JSON file. Both options support providing protected and un-protected headers with int32 and string values. The header label is always a string value. +>Note: When both file and command-line header options are specified, the command-line input is ignored. + + * Command-line: + * /IntProtectedHeaders, /iph - A collection of name-value pairs (separated by comma ',') with the value being an int32. Example: /IntProtectedHeaders created-at=12345678,customer-count=10 + * /StringProtectedHeaders, /sph - A collection of name-value pairs (separated by comma ',') with the value being a string. Example: /StringProtectedHeaders message-type=cose,customer-name=contoso + * /IntUnProtectedHeaders, /iuh - A collection of name-value pairs (separated by comma ',') with the value being an int32. Example: /IntUnProtectedHeaders created-at=12345678,customer-count=10 + * /StringUnProtectedHeaders, /suh - A collection of name-value pairs (separated by comma ',') with the value being a string. Example: /StringProtectedHeaders message-type=cose,customer-name=contoso + * File: + * /IntHeaders, /ih - A JSON file containing the headers with the value being an int32. + * /StringHeaders, /sh - A JSON file containing the headers with the value being a string. + +The JSON schema is the same for both types of header files. Sample int32 and string headers file are shown below. + +>Note: protected is optional. When ignored, it defaults to False. + +~~~ +[ + { + "label":"created-at", + "value": 12345678, + "protected": true + }, + { + "label": "customer-count", + "value": 10, + "protected": false + }, +] +~~~ + +~~~ +[ + { + "label":"message-type", + "value": "cose", + "protected": false + }, + { + "label": "customer-name", + "value": "contoso", + "protected": true + }, +] +~~~ Run *CoseSignTool sign /?* for the complete command line usage. From d8073b2693bcb65a70f60b5f678acb55fb3e647f Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Thu, 15 Aug 2024 13:12:18 -0700 Subject: [PATCH 04/13] Remove unused packages --- CoseSign1.Headers/Usings.cs | 1 - CoseSignTool/Usings.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CoseSign1.Headers/Usings.cs b/CoseSign1.Headers/Usings.cs index 23f9bbe8..b0e83b4d 100644 --- a/CoseSign1.Headers/Usings.cs +++ b/CoseSign1.Headers/Usings.cs @@ -3,7 +3,6 @@ global using System; global using System.Collections.Generic; -global using System.ComponentModel; global using System.Linq; global using System.Security.Cryptography.Cose; global using CoseSign1.Abstractions.Interfaces; diff --git a/CoseSignTool/Usings.cs b/CoseSignTool/Usings.cs index 304874f8..0f95c9e3 100644 --- a/CoseSignTool/Usings.cs +++ b/CoseSignTool/Usings.cs @@ -18,9 +18,9 @@ global using CoseSign1.Extensions; global using CoseSign1.Headers; global using CoseSign1.Headers.Local; +global using CoseSignTool.Local; global using CoseX509; global using Microsoft.Extensions.Configuration.CommandLine; global using Newtonsoft.Json; -global using CoseSignTool.Local; From 156dd2ef8fcc60f33a6fe97d6ed268e9697d475c Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Thu, 15 Aug 2024 13:21:43 -0700 Subject: [PATCH 05/13] Bug fix --- CoseSignTool/CoseCommand.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CoseSignTool/CoseCommand.cs b/CoseSignTool/CoseCommand.cs index 33464e86..7b4cfd65 100644 --- a/CoseSignTool/CoseCommand.cs +++ b/CoseSignTool/CoseCommand.cs @@ -3,9 +3,6 @@ namespace CoseSignTool; -using System; - - /// /// A base class for console commands that handle COSE signatures. /// @@ -272,7 +269,7 @@ protected static void GetOptionHeadersFromCommandLine(CommandLineConfigur if (headers == null) { - headers = new(); + throw new ArgumentException("Headers collection cannot be null"); } try From 5bd2e52ffa35636a34380dca2394661492d33712 Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Thu, 15 Aug 2024 13:35:47 -0700 Subject: [PATCH 06/13] Fixed the help text for sign command --- CoseSignTool/SignCommand.cs | 16 ++++++++++++---- docs/CoseSignTool.md | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CoseSignTool/SignCommand.cs b/CoseSignTool/SignCommand.cs index 765cc0a1..9581465a 100644 --- a/CoseSignTool/SignCommand.cs +++ b/CoseSignTool/SignCommand.cs @@ -381,10 +381,18 @@ to file. ContentType /cty: Optional. A MIME type to specify as Content Type in the COSE signature header. Default value is 'application/cose'. - IntHeaders /ih: Optional. Path to a JSON file with headers to add to the signed message. The label is string and the value is int32. - JSON format is [{""label"":""created-at"",""value"":1723672289,""protected"":true},...]. Protected is optional and when ignored, it is set to false. + IntHeaders /ih: Optional. Path to a JSON file containing the header collection to be added to the cose message. The label is a string and the value is int32. + Sample file. [{""label"":""created-at"",""value"":12345678,""protected"":true},{""label"":""customer-count"",""value"":10,""protected"":false}] - StringHeaders /sh: Optional. Path to a JSON file with headers to add to the signed message. The label and value are strings. - JSON format is [{""label"":""company"",""value"":""Microsoft"",""protected"":true},...]. Protected is optional and when ignored, it is set to false. + StringHeaders /sh: Optional. Path to a JSON file containing the header collection to be added to the cose message. Both the label and value are strings. + Sample file. [{""label"":""message-type"",""value"":""cose"",""protected"":false},{""label"":""customer-name"",""value"":""contoso"",""protected"":true}] + + IntProtectedHeders /iph: A collection of name-value pairs with a string label and an int32 value. Sample input: /IntProtectedHeaders created-at=12345678,customer-count=10 + + StringProtectedHeders /sph: A collection of name-value pairs with a string label and value. Sample input: /StringProtectedHeaders message-type=cose,customer-name=contoso + + IntUnProtectedHeders /iuh: A collection of name-value pairs with a string label and an int32 value. Sample input: /IntUnProtectedHeaders created-at=12345678,customer-count=10 + + StringUnProtectedHeders /suh: A collection of name-value pairs with a string label and value. Sample input: /StringUnProtectedHeaders message-type=cose,customer-name=contoso "; } diff --git a/docs/CoseSignTool.md b/docs/CoseSignTool.md index d240321e..48f1cf5b 100644 --- a/docs/CoseSignTool.md +++ b/docs/CoseSignTool.md @@ -52,7 +52,7 @@ The JSON schema is the same for both types of header files. Sample int32 and str "label": "customer-count", "value": 10, "protected": false - }, + } ] ~~~ @@ -67,7 +67,7 @@ The JSON schema is the same for both types of header files. Sample int32 and str "label": "customer-name", "value": "contoso", "protected": true - }, + } ] ~~~ From 9ee7a8b62425db3015eb32b30219ac47a6a5fb2b Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Thu, 15 Aug 2024 21:52:21 -0700 Subject: [PATCH 07/13] Fixed review comments --- CoseHandler/CoseHandler.cs | 1 + CoseSign1.Tests.Common/FileSystemUtils.cs | 5 +++++ CoseSignTool.Tests/CoseSignTool.Tests.csproj | 1 - docs/CoseSignTool.md | 4 ++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CoseHandler/CoseHandler.cs b/CoseHandler/CoseHandler.cs index ac474584..c3be70b7 100644 --- a/CoseHandler/CoseHandler.cs +++ b/CoseHandler/CoseHandler.cs @@ -51,6 +51,7 @@ public static ReadOnlyMemory Sign( /// .Optional. Writes the COSE signature to the specified file location. /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A provider to add custom headers to the signed message. /// Unsupported certificate type for COSE signing, or the certificate chain could not be built. /// The signing certificate is null or invalid. public static ReadOnlyMemory Sign( diff --git a/CoseSign1.Tests.Common/FileSystemUtils.cs b/CoseSign1.Tests.Common/FileSystemUtils.cs index 1822e573..bec9b4e3 100644 --- a/CoseSign1.Tests.Common/FileSystemUtils.cs +++ b/CoseSign1.Tests.Common/FileSystemUtils.cs @@ -44,6 +44,11 @@ public static string GeneratePayloadFile([CallerMemberName] string caller = "", return new(fileName); } + /// + /// Creates a randomly generated headers file on disk for signature testing. + /// + /// The content to be written to the file. + /// The path to the new file. public static string GenerateHeadersFile(string? content = null) { string fileName = Path.GetTempFileName().Replace(".tmp", $".headers.json"); diff --git a/CoseSignTool.Tests/CoseSignTool.Tests.csproj b/CoseSignTool.Tests/CoseSignTool.Tests.csproj index 651d9720..14ec0f9b 100644 --- a/CoseSignTool.Tests/CoseSignTool.Tests.csproj +++ b/CoseSignTool.Tests/CoseSignTool.Tests.csproj @@ -15,7 +15,6 @@ None true - x64 diff --git a/docs/CoseSignTool.md b/docs/CoseSignTool.md index 48f1cf5b..ccd19aae 100644 --- a/docs/CoseSignTool.md +++ b/docs/CoseSignTool.md @@ -30,9 +30,9 @@ Import-Certificate -FilePath 'c:\my\cert.pfx' -CertStoreLocation 'Cert:CurrentUs * Command-line: * /IntProtectedHeaders, /iph - A collection of name-value pairs (separated by comma ',') with the value being an int32. Example: /IntProtectedHeaders created-at=12345678,customer-count=10 - * /StringProtectedHeaders, /sph - A collection of name-value pairs (separated by comma ',') with the value being a string. Example: /StringProtectedHeaders message-type=cose,customer-name=contoso + * /StringProtectedHeaders, /sph - A collection of name-value pairs (separated by comma ',') with the value being a string. Example: /StringProtectedHeaders message-type="cose",customer-name="contoso" * /IntUnProtectedHeaders, /iuh - A collection of name-value pairs (separated by comma ',') with the value being an int32. Example: /IntUnProtectedHeaders created-at=12345678,customer-count=10 - * /StringUnProtectedHeaders, /suh - A collection of name-value pairs (separated by comma ',') with the value being a string. Example: /StringProtectedHeaders message-type=cose,customer-name=contoso + * /StringUnProtectedHeaders, /suh - A collection of name-value pairs (separated by comma ',') with the value being a string. Example: /StringUnProtectedHeaders message-type="cose",customer-name="contoso" * File: * /IntHeaders, /ih - A JSON file containing the headers with the value being an int32. * /StringHeaders, /sh - A JSON file containing the headers with the value being a string. From e9a33fc5c7c96d195c3327da2f98d4e8afdb74ae Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Fri, 16 Aug 2024 11:20:02 -0700 Subject: [PATCH 08/13] Delete unused tests --- CoseSignTool.Tests/MainTests.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CoseSignTool.Tests/MainTests.cs b/CoseSignTool.Tests/MainTests.cs index cf1b83fb..936615e1 100644 --- a/CoseSignTool.Tests/MainTests.cs +++ b/CoseSignTool.Tests/MainTests.cs @@ -324,15 +324,6 @@ public void SignWithCommandLineIntAndStringHeaders() CoseSignTool.Main(args1).Should().Be((int)ExitCode.Success, "Signing with protected string headers in command line failed."); } - [TestMethod] - public void SignWithCommandLineProtectedAndUnprotectedHeaders() - { - string payloadFile = FileSystemUtils.GeneratePayloadFile(); - - // sign with string protected headers - - } - [TestMethod] public void SignWithMissingAndInvalidCommandLineHeaders() { From f1917f1e147dd6a4572210084b6c19df6f0bcaf1 Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Fri, 16 Aug 2024 17:04:14 -0700 Subject: [PATCH 09/13] Added header extender provider to all the sign overload methods in cose handler project --- CoseHandler/CoseHandler.cs | 55 ++++++++++++-------- CoseSignTool.Tests/CoseSignTool.Tests.csproj | 5 ++ CoseSignTool/CoseCommand.cs | 17 ++---- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/CoseHandler/CoseHandler.cs b/CoseHandler/CoseHandler.cs index c3be70b7..c1f21ca4 100644 --- a/CoseHandler/CoseHandler.cs +++ b/CoseHandler/CoseHandler.cs @@ -27,7 +27,8 @@ public static class CoseHandler /// By default, the COSE signature uses a hash match to compare to the original content. This is called "detached" signing. /// .Optional. Writes the COSE signature to the specified file location. /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. - /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A provider to add custom headers to the signed message. /// Unsupported certificate type for COSE signing, or the certificate chain could not be built. /// The signing certificate is null or invalid. public static ReadOnlyMemory Sign( @@ -35,11 +36,12 @@ public static ReadOnlyMemory Sign( X509Certificate2 certificate, bool embedSign = false, FileInfo? signatureFile = null, - string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE) + string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE, + ICoseHeaderExtender? headerExtender = null) => Sign( payload, signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(null, certificate), - embedSign, signatureFile, contentType); + embedSign, signatureFile, contentType, headerExtender); /// /// Signs the payload content with the supplied certificate and returns a ReadOnlyMemory object containing the COSE signatureFile. @@ -50,7 +52,7 @@ public static ReadOnlyMemory Sign( /// By default, the COSE signature uses a hash match to compare to the original content. This is called "detached" signing. /// .Optional. Writes the COSE signature to the specified file location. /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. - /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". /// Optional. A provider to add custom headers to the signed message. /// Unsupported certificate type for COSE signing, or the certificate chain could not be built. /// The signing certificate is null or invalid. @@ -75,7 +77,8 @@ public static ReadOnlyMemory Sign( /// By default, the COSE signature uses a hash match to compare to the original content. This is called "detached" signing. /// .Optional. Writes the COSE signature to the specified file location. /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. - /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A provider to add custom headers to the signed message. /// Unsupported certificate type for COSE signing, or the certificate chain could not be built. /// The signing certificate is null or invalid. /// The payload file could not be found. @@ -87,11 +90,12 @@ public static ReadOnlyMemory Sign( X509Certificate2 certificate, bool embedSign = false, FileInfo? signatureFile = null, - string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE) + string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE, + ICoseHeaderExtender? headerExtender = null) => SignInternal( payloadBytes: null, payloadStream: null, payloadFile: payload, signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(null, certificate), - embedSign, signatureFile, contentType); + embedSign, signatureFile, contentType, headerExtender); /// /// Signs the payload content with the supplied certificate and returns a ReadOnlyMemory object containing the COSE signatureFile. @@ -104,7 +108,8 @@ public static ReadOnlyMemory Sign( /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. /// Optional. The name of the certificate store that contains the signing certificate. Default is "My". /// Optional. The location of the certificate store that contains the signing certificate. Default is "CurrentUser". - /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A provider to add custom headers to the signed message. /// Unsupported certificate type for COSE signing, or the certificate chain could not be built. /// The signing certificate is null or invalid. public static ReadOnlyMemory Sign( @@ -114,11 +119,12 @@ public static ReadOnlyMemory Sign( FileInfo? signatureFile = null, string storeName = "My", StoreLocation storeLocation = StoreLocation.CurrentUser, - string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE) + string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE, + ICoseHeaderExtender? headerExtender = null) => Sign( payload, certificate: LookupCertificate(thumbprint, storeName, storeLocation), - embedSign, signatureFile, contentType); + embedSign, signatureFile, contentType, headerExtender); /// /// Signs the payload content with the supplied certificate and returns a ReadOnlyMemory object containing the COSE signatureFile. @@ -131,7 +137,8 @@ public static ReadOnlyMemory Sign( /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. /// Optional. The name of the certificate store that contains the signing certificate. Default is "My". /// Optional. The location of the certificate store that contains the signing certificate. Default is "CurrentUser". - /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A provider to add custom headers to the signed message. /// Unsupported certificate type for COSE signing, or the certificate chain could not be built. /// The signing certificate is null or invalid. public static ReadOnlyMemory Sign( @@ -141,11 +148,12 @@ public static ReadOnlyMemory Sign( FileInfo? signatureFile = null, string storeName = "My", StoreLocation storeLocation = StoreLocation.CurrentUser, - string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE) + string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE, + ICoseHeaderExtender? headerExtender = null) => Sign( payload, certificate: LookupCertificate(thumbprint, storeName, storeLocation), - embedSign, signatureFile, contentType); + embedSign, signatureFile, contentType, headerExtender); /// /// Signs the payload content with the supplied certificate and returns a ReadOnlyMemory object containing the COSE signatureFile. @@ -158,6 +166,8 @@ public static ReadOnlyMemory Sign( /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. /// Optional. The name of the certificate store that contains the signing certificate. Default is "My". /// Optional. The location of the certificate store that contains the signing certificate. Default is "CurrentUser". + /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A provider to add custom headers to the signed message. /// Unsupported certificate type for COSE signing, or the certificate chain could not be built. /// The signing certificate is null or invalid. /// The payload file could not be found. @@ -171,12 +181,13 @@ public static ReadOnlyMemory Sign( FileInfo? signatureFile = null, string storeName = "My", StoreLocation storeLocation = StoreLocation.CurrentUser, - string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE) + string contentType = CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE, + ICoseHeaderExtender? headerExtender = null) => SignInternal( payloadBytes: null, payloadStream: null, payloadFile: payload, signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(null, LookupCertificate(thumbprint, storeName, storeLocation)), - embedSign, signatureFile, contentType); + embedSign, signatureFile, contentType, headerExtender); /// /// Signs the payload content with the supplied certificate and returns a ReadOnlyMemory object containing the COSE signatureFile. @@ -187,8 +198,8 @@ public static ReadOnlyMemory Sign( /// By default, the COSE signature uses a hash match to compare to the original content. This is called "detached" signing. /// .Optional. Writes the COSE signature to the specified file location. /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. - /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". - /// Optional. Provides the ability to add custom Cose Headers + /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A provider to add custom headers to the signed message. /// Unsupported certificate type for COSE signing, or the certificate chain could not be built. /// The signing certificate is null or invalid. public static ReadOnlyMemory Sign( @@ -211,8 +222,8 @@ public static ReadOnlyMemory Sign( /// By default, the COSE signature uses a hash match to compare to the original content. This is called "detached" signing. /// .Optional. Writes the COSE signature to the specified file location. /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. - /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". - /// Optional. Provides the ability to add custom Cose Headers + /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A provider to add custom headers to the signed message. /// Unsupported certificate type for COSE signing, or the certificate chain could not be built. /// The signing certificate is null or invalid. public static ReadOnlyMemory Sign( @@ -235,7 +246,8 @@ public static ReadOnlyMemory Sign( /// By default, the COSE signature uses a hash match to compare to the original content. This is called "detached" signing. /// .Optional. Writes the COSE signature to the specified file location. /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. - /// Optional. Provides the ability to add custom Cose Headers + /// Optional. A MIME type value to set as the Content Type of the payload. Default value is "application/cose". + /// Optional. A provider to add custom headers to the signed message. /// The signing certificate is null or invalid. /// The payload file could not be found. /// The parent directory of the payload file could not be found. @@ -260,7 +272,8 @@ public static ReadOnlyMemory Sign( /// True to embed an encoded copy of the payload content into the COSE signature structure. /// .Optional. Writes the COSE signature to the specified file location. /// For file extension, we recommend ".cose" for detached signatures, or ".csm" if the file is embed-signed. - /// Optional. Provides the ability to add custom Cose Headers + /// A MIME type value to set as the Content Type of the payload. + /// Optional. A provider to add custom headers to the signed message. /// The COSE signature structure in a read-only byte array. internal static ReadOnlyMemory SignInternal( byte[]? payloadBytes, diff --git a/CoseSignTool.Tests/CoseSignTool.Tests.csproj b/CoseSignTool.Tests/CoseSignTool.Tests.csproj index 14ec0f9b..c5847968 100644 --- a/CoseSignTool.Tests/CoseSignTool.Tests.csproj +++ b/CoseSignTool.Tests/CoseSignTool.Tests.csproj @@ -17,6 +17,10 @@ true + + + + @@ -32,6 +36,7 @@ + diff --git a/CoseSignTool/CoseCommand.cs b/CoseSignTool/CoseCommand.cs index 7b4cfd65..e120b25c 100644 --- a/CoseSignTool/CoseCommand.cs +++ b/CoseSignTool/CoseCommand.cs @@ -271,18 +271,11 @@ protected static void GetOptionHeadersFromCommandLine(CommandLineConfigur { throw new ArgumentException("Headers collection cannot be null"); } - - try - { - inputs.ToList().ForEach(header => { - string[] labelValue = header.Split("="); - headers.Add(new CoseHeader(labelValue[0], converter(labelValue), isProtected)); - }); - } - catch (Exception) - { - throw; - } + + inputs.ToList().ForEach(header => { + string[] labelValue = header.Split("="); + headers.Add(new CoseHeader(labelValue[0], converter(labelValue), isProtected)); + }); } // One liner for file existence checks From acb23a174ef701b23c13bea204597b1f63a3e9e5 Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Fri, 16 Aug 2024 17:11:13 -0700 Subject: [PATCH 10/13] Removed unused properties directory --- CoseSignTool.Tests/CoseSignTool.Tests.csproj | 5 ----- CoseSignTool.Tests/Properties/launchSettings.json | 8 -------- 2 files changed, 13 deletions(-) delete mode 100644 CoseSignTool.Tests/Properties/launchSettings.json diff --git a/CoseSignTool.Tests/CoseSignTool.Tests.csproj b/CoseSignTool.Tests/CoseSignTool.Tests.csproj index c5847968..14ec0f9b 100644 --- a/CoseSignTool.Tests/CoseSignTool.Tests.csproj +++ b/CoseSignTool.Tests/CoseSignTool.Tests.csproj @@ -17,10 +17,6 @@ true - - - - @@ -36,7 +32,6 @@ - diff --git a/CoseSignTool.Tests/Properties/launchSettings.json b/CoseSignTool.Tests/Properties/launchSettings.json deleted file mode 100644 index 9eaad5a9..00000000 --- a/CoseSignTool.Tests/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "CoseSignTool.Tests": { - "commandName": "Project", - "commandLineArgs": "sign /p \"C:\\Users\\settiy.NORTHAMERICA\\Downloads\\remove_member.json\" /pfx \"C:\\Users\\settiy.NORTHAMERICA\\Downloads\\member1.pfx\" /ep /po /sh" - } - } -} \ No newline at end of file From 73c98d9aeb8d0577198ceef87f2d4fbb594b047e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 17 Aug 2024 00:16:53 +0000 Subject: [PATCH 11/13] Update changelog for release --- CHANGELOG.md | 98 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af41fa4..bbf96abc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,28 @@ # Changelog -## [v1.2.4-pre3](https://github.com/microsoft/CoseSignTool/tree/v1.2.4-pre3) (2024-08-06) +## [v1.2.5-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.2.5-pre2) (2024-08-15) + +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.5-pre1...v1.2.5-pre2) + +**Merged pull requests:** + +- Fix typos on README.md [\#95](https://github.com/microsoft/CoseSignTool/pull/95) ([Jaxelr](https://github.com/Jaxelr)) + +## [v1.2.5-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.5-pre1) (2024-08-14) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.5...v1.2.4-pre3) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.5...v1.2.5-pre1) + +**Merged pull requests:** + +- Update package dependencies [\#99](https://github.com/microsoft/CoseSignTool/pull/99) ([lemccomb](https://github.com/lemccomb)) ## [v1.2.5](https://github.com/microsoft/CoseSignTool/tree/v1.2.5) (2024-08-06) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.4-pre2...v1.2.5) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.4-pre3...v1.2.5) + +## [v1.2.4-pre3](https://github.com/microsoft/CoseSignTool/tree/v1.2.4-pre3) (2024-08-06) + +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.4-pre2...v1.2.4-pre3) **Merged pull requests:** @@ -22,19 +38,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:** @@ -74,19 +90,19 @@ ## [v1.2.3-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.3-pre1) (2024-05-31) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.2-pre1...v1.2.3-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.3...v1.2.3-pre1) **Merged pull requests:** - upgrade to .NET 8, add docs to package, add retry loop for revocation server [\#89](https://github.com/microsoft/CoseSignTool/pull/89) ([lemccomb](https://github.com/lemccomb)) -## [v1.2.2-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.2-pre1) (2024-03-20) +## [v1.2.3](https://github.com/microsoft/CoseSignTool/tree/v1.2.3) (2024-03-20) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.3...v1.2.2-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.2-pre1...v1.2.3) -## [v1.2.3](https://github.com/microsoft/CoseSignTool/tree/v1.2.3) (2024-03-20) +## [v1.2.2-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.2-pre1) (2024-03-20) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.1-pre2...v1.2.3) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.1-pre2...v1.2.2-pre1) **Merged pull requests:** @@ -94,7 +110,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.2...v1.2.1-pre2) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.1-pre1...v1.2.1-pre2) **Closed issues:** @@ -104,13 +120,13 @@ - more granular error codes [\#86](https://github.com/microsoft/CoseSignTool/pull/86) ([lemccomb](https://github.com/lemccomb)) -## [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-pre1...v1.2.2) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.2...v1.2.1-pre1) -## [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.1...v1.2.1-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.1...v1.2.2) **Merged pull requests:** @@ -130,27 +146,27 @@ ## [v1.2.exeTest](https://github.com/microsoft/CoseSignTool/tree/v1.2.exeTest) (2024-03-06) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.0...v1.2.exeTest) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.8-pre1...v1.2.exeTest) -## [v1.2.0](https://github.com/microsoft/CoseSignTool/tree/v1.2.0) (2024-03-04) +## [v1.1.8-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.8-pre1) (2024-03-04) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.8-pre1...v1.2.0) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.0...v1.1.8-pre1) -## [v1.1.8-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.8-pre1) (2024-03-04) +## [v1.2.0](https://github.com/microsoft/CoseSignTool/tree/v1.2.0) (2024-03-04) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.7-pre3...v1.1.8-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.8...v1.2.0) **Merged pull requests:** - Update Nuspec for CoseIndirectSignature [\#80](https://github.com/microsoft/CoseSignTool/pull/80) ([elantiguamsft](https://github.com/elantiguamsft)) -## [v1.1.7-pre3](https://github.com/microsoft/CoseSignTool/tree/v1.1.7-pre3) (2024-03-02) +## [v1.1.8](https://github.com/microsoft/CoseSignTool/tree/v1.1.8) (2024-03-02) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.8...v1.1.7-pre3) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.7-pre3...v1.1.8) -## [v1.1.8](https://github.com/microsoft/CoseSignTool/tree/v1.1.8) (2024-03-02) +## [v1.1.7-pre3](https://github.com/microsoft/CoseSignTool/tree/v1.1.7-pre3) (2024-03-02) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.7-pre2...v1.1.8) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.7-pre2...v1.1.7-pre3) **Merged pull requests:** @@ -198,31 +214,31 @@ ## [v1.1.4-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.4-pre1) (2024-01-31) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.3-pre1...v1.1.4-pre1) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.4...v1.1.4-pre1) **Merged pull requests:** - write validation output to standard out [\#74](https://github.com/microsoft/CoseSignTool/pull/74) ([elantiguamsft](https://github.com/elantiguamsft)) -## [v1.1.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.2-pre1...v1.1.3-pre1) **Merged pull requests:** - Adding Validation Option to Output Certificate Chain [\#73](https://github.com/microsoft/CoseSignTool/pull/73) ([elantiguamsft](https://github.com/elantiguamsft)) -## [v1.1.3](https://github.com/microsoft/CoseSignTool/tree/v1.1.3) (2024-01-24) +## [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.2-pre1...v1.1.3) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.3...v1.1.2-pre1) -## [v1.1.2-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.2-pre1) (2024-01-24) +## [v1.1.3](https://github.com/microsoft/CoseSignTool/tree/v1.1.3) (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.1-pre2...v1.1.3) **Merged pull requests:** @@ -327,7 +343,7 @@ ## [v1.1.0](https://github.com/microsoft/CoseSignTool/tree/v1.1.0) (2023-10-10) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.2...v1.1.0) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.9...v1.1.0) **Merged pull requests:** @@ -337,13 +353,13 @@ - Port changes from ADO repo to GitHub repo [\#46](https://github.com/microsoft/CoseSignTool/pull/46) ([lemccomb](https://github.com/lemccomb)) - Re-enable CodeQL [\#45](https://github.com/microsoft/CoseSignTool/pull/45) ([lemccomb](https://github.com/lemccomb)) -## [v0.3.2](https://github.com/microsoft/CoseSignTool/tree/v0.3.2) (2023-09-28) +## [v0.3.1-pre.9](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.9) (2023-09-28) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.9...v0.3.2) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.2...v0.3.1-pre.9) -## [v0.3.1-pre.9](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.9) (2023-09-28) +## [v0.3.2](https://github.com/microsoft/CoseSignTool/tree/v0.3.2) (2023-09-28) -[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.8...v0.3.1-pre.9) +[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.8...v0.3.2) **Merged pull requests:** From 4e36758abddca30086d934e48bdafbf51c956b37 Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Fri, 16 Aug 2024 22:04:56 -0700 Subject: [PATCH 12/13] Fixed cose header tests to ensure that the factory instance is disposed during exceptions --- CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs b/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs index 16117622..8d619d40 100644 --- a/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs +++ b/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs @@ -17,8 +17,7 @@ public void Setup() [Test] public void AddHeadersCountTest() { - CoseHeaderFactory factory = CoseHeaderFactory.Instance(); - + using CoseHeaderFactory factory = CoseHeaderFactory.Instance(); List> intProtectedHeaders = new(); List> stringProtectedHeaders = new(); @@ -43,14 +42,12 @@ public void AddHeadersCountTest() Assert.That(factory.ProtectedHeadersCount, Is.EqualTo(expectedProtectedHeaderCount)); Assert.That(factory.UnProtectedHeadersCount, Is.EqualTo(expectedUnProtectedHeaderCount)); - - factory.Dispose(); } [Test] public void AddHeadersTest() { - CoseHeaderFactory factory = CoseHeaderFactory.Instance(); + using CoseHeaderFactory factory = CoseHeaderFactory.Instance(); CoseHeaderMap coseProtectedHeaders = new(); coseProtectedHeaders.Add(new CoseHeaderLabel("Label1"), 32); @@ -76,7 +73,6 @@ public void AddHeadersTest() Assert.That(coseProtectedHeaders.Count, Is.EqualTo(2)); Assert.That(coseUnProtectedHeaders.Count, Is.EqualTo(3)); - factory.Dispose(); } From 008d7cf18c1f6b2a08106be42ae9b74494a8be99 Mon Sep 17 00:00:00 2001 From: Yagnesh Setti Subramanian Date: Fri, 16 Aug 2024 22:15:32 -0700 Subject: [PATCH 13/13] Fixed tests --- CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs | 1 - CoseSign1.Headers/CoseHeaderFactory.cs | 3 +-- CoseSignTool.Tests/MainTests.cs | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs b/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs index 8d619d40..40a03595 100644 --- a/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs +++ b/CoseSign1.Headers.Tests/CoseHeaderFactoryTests.cs @@ -73,7 +73,6 @@ public void AddHeadersTest() Assert.That(coseProtectedHeaders.Count, Is.EqualTo(2)); Assert.That(coseUnProtectedHeaders.Count, Is.EqualTo(3)); - factory.Dispose(); } [Test] diff --git a/CoseSign1.Headers/CoseHeaderFactory.cs b/CoseSign1.Headers/CoseHeaderFactory.cs index 61d8648b..116ee1dc 100644 --- a/CoseSign1.Headers/CoseHeaderFactory.cs +++ b/CoseSign1.Headers/CoseHeaderFactory.cs @@ -81,7 +81,7 @@ private void AddHeadersInternal(IEnumerable> headers, b headers.ToList().ForEach(h => { // We do not allow null or empty string as a string value although the caller might. - if (!CoseHeader.IsValid((value) => {return string.IsNullOrEmpty(value) ? false : true;}, h.Value.ToString())) + if (!CoseHeader.IsValid((value) => { return !string.IsNullOrEmpty(value); }, h.Value.ToString())) { throw new ArgumentException($"A non-empty string value must be supplied for the header '{h.Label}'"); } @@ -161,6 +161,5 @@ public void Dispose() { IntHeaders.Clear(); StringHeaders.Clear(); - SingletonInstance = null; } } diff --git a/CoseSignTool.Tests/MainTests.cs b/CoseSignTool.Tests/MainTests.cs index 936615e1..81d1614f 100644 --- a/CoseSignTool.Tests/MainTests.cs +++ b/CoseSignTool.Tests/MainTests.cs @@ -262,7 +262,6 @@ public void SignWithDeserializationErrorIntegerHeaders() [TestMethod] public void SignWithMissingIntegerHeadersFile() { - string integerHeadersFile = FileSystemUtils.GenerateHeadersFile(@"[{""label"":""created-at"",""value"":2312345,""protected"":true}]"); string payloadFile = FileSystemUtils.GeneratePayloadFile(); // sign with integer headers