diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 909ebdbf..3fc9653f 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -56,6 +56,8 @@ jobs:
dotnet test --no-restore CoseSign1.Certificates.Tests/CoseSign1.Certificates.Tests.csproj
dotnet test --no-restore CoseSign1.Headers.Tests/CoseSign1.Headers.Tests.csproj
dotnet test --no-restore CoseIndirectSignature.Tests/CoseIndirectSignature.Tests.csproj
+ dotnet test --no-restore CoseSign1.Transparent.Tests/CoseSign1.Transparent.Tests.csproj
+ dotnet test --no-restore CoseSign1.Transparent.CTS.Tests/CoseSign1.Transparent.CTS.Tests.csproj
dotnet test --no-restore CoseHandler.Tests/CoseHandler.Tests.csproj
dotnet test --no-restore CoseSignTool.Tests/CoseSignTool.Tests.csproj
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6620c72..6d4ac924 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [v1.3.0-pre5](https://github.com/microsoft/CoseSignTool/tree/v1.3.0-pre5) (2025-03-18)
+
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.4.0...v1.3.0-pre5)
+
+## [v1.4.0](https://github.com/microsoft/CoseSignTool/tree/v1.4.0) (2025-03-18)
+
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.3.0-pre4...v1.4.0)
+
+**Merged pull requests:**
+
+- Add support for CoAP preimage content type parsing for validation [\#126](https://github.com/microsoft/CoseSignTool/pull/126) ([JeromySt](https://github.com/JeromySt))
+
## [v1.3.0-pre4](https://github.com/microsoft/CoseSignTool/tree/v1.3.0-pre4) (2025-03-13)
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.3.0-pre3...v1.3.0-pre4)
@@ -40,10 +52,6 @@
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.3.0...v1.2.8-pre7)
-**Closed issues:**
-
-- Revocation and Root Trust [\#119](https://github.com/microsoft/CoseSignTool/issues/119)
-
**Merged pull requests:**
- Adds CLI install instructions [\#116](https://github.com/microsoft/CoseSignTool/pull/116) ([ivarprudnikov](https://github.com/ivarprudnikov))
@@ -78,31 +86,31 @@
## [v1.2.8-pre3](https://github.com/microsoft/CoseSignTool/tree/v1.2.8-pre3) (2024-10-15)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8-pre2...v1.2.8-pre3)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8-pre1...v1.2.8-pre3)
**Merged pull requests:**
- Increase timeout for checking for empty streams [\#113](https://github.com/microsoft/CoseSignTool/pull/113) ([lemccomb](https://github.com/lemccomb))
-## [v1.2.8-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.2.8-pre2) (2024-09-25)
+## [v1.2.8-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.8-pre1) (2024-09-25)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8-pre1...v1.2.8-pre2)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8-pre2...v1.2.8-pre1)
-## [v1.2.8-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.8-pre1) (2024-09-25)
+## [v1.2.8-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.2.8-pre2) (2024-09-25)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.6-pre2...v1.2.8-pre1)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8...v1.2.8-pre2)
**Merged pull requests:**
- Make the advanced stream handling optional. Use simple retry instead. [\#112](https://github.com/microsoft/CoseSignTool/pull/112) ([lemccomb](https://github.com/lemccomb))
-## [v1.2.6-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.2.6-pre2) (2024-09-23)
+## [v1.2.8](https://github.com/microsoft/CoseSignTool/tree/v1.2.8) (2024-09-23)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.8...v1.2.6-pre2)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.6-pre2...v1.2.8)
-## [v1.2.8](https://github.com/microsoft/CoseSignTool/tree/v1.2.8) (2024-09-23)
+## [v1.2.6-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.2.6-pre2) (2024-09-23)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.6-pre1...v1.2.8)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.6-pre1...v1.2.6-pre2)
**Merged pull requests:**
@@ -168,19 +176,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.3-pre7...v1.2.4-pre1)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.4...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.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.4...v1.2.3-pre7)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.3-pre7...v1.2.4)
-## [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-pre6...v1.2.4)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.3-pre6...v1.2.3-pre7)
**Merged pull requests:**
@@ -242,10 +250,6 @@
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.2...v1.2.1-pre2)
-**Closed issues:**
-
-- Question: How to use certificate with password? [\#82](https://github.com/microsoft/CoseSignTool/issues/82)
-
**Merged pull requests:**
- more granular error codes [\#86](https://github.com/microsoft/CoseSignTool/pull/86) ([lemccomb](https://github.com/lemccomb))
@@ -256,19 +260,19 @@
## [v1.2.1-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.1-pre1) (2024-03-12)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.1...v1.2.1-pre1)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.0-pre1...v1.2.1-pre1)
**Merged pull requests:**
- Revert "Add .exe to CoseSignTool NuGet" [\#83](https://github.com/microsoft/CoseSignTool/pull/83) ([elantiguamsft](https://github.com/elantiguamsft))
-## [v1.2.1](https://github.com/microsoft/CoseSignTool/tree/v1.2.1) (2024-03-07)
+## [v1.2.0-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.0-pre1) (2024-03-07)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.0-pre1...v1.2.1)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.1...v1.2.0-pre1)
-## [v1.2.0-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.2.0-pre1) (2024-03-07)
+## [v1.2.1](https://github.com/microsoft/CoseSignTool/tree/v1.2.1) (2024-03-07)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.exeTest...v1.2.0-pre1)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.2.exeTest...v1.2.1)
**Merged pull requests:**
@@ -284,19 +288,19 @@
## [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...v1.1.8-pre1)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.7-pre3...v1.1.8-pre1)
**Merged pull requests:**
- Update Nuspec for CoseIndirectSignature [\#80](https://github.com/microsoft/CoseSignTool/pull/80) ([elantiguamsft](https://github.com/elantiguamsft))
-## [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-pre3...v1.1.8)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.8...v1.1.7-pre3)
-## [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.7-pre2...v1.1.7-pre3)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.7-pre2...v1.1.8)
**Merged pull requests:**
@@ -332,19 +336,19 @@
## [v1.1.6](https://github.com/microsoft/CoseSignTool/tree/v1.1.6) (2024-02-07)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.4-pre1...v1.1.6)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.5...v1.1.6)
**Merged pull requests:**
- Only hit iterator once [\#75](https://github.com/microsoft/CoseSignTool/pull/75) ([JeromySt](https://github.com/JeromySt))
-## [v1.1.4-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.4-pre1) (2024-01-31)
+## [v1.1.5](https://github.com/microsoft/CoseSignTool/tree/v1.1.5) (2024-01-31)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.5...v1.1.4-pre1)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.4-pre1...v1.1.5)
-## [v1.1.5](https://github.com/microsoft/CoseSignTool/tree/v1.1.5) (2024-01-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.4...v1.1.5)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.4...v1.1.4-pre1)
**Merged pull requests:**
@@ -368,19 +372,19 @@
## [v1.1.2-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.2-pre1) (2024-01-24)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.2...v1.1.2-pre1)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1-pre2...v1.1.2-pre1)
**Merged pull requests:**
- Updating snk for internal package compatibility [\#72](https://github.com/microsoft/CoseSignTool/pull/72) ([elantiguamsft](https://github.com/elantiguamsft))
-## [v1.1.2](https://github.com/microsoft/CoseSignTool/tree/v1.1.2) (2024-01-18)
+## [v1.1.1-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.1.1-pre2) (2024-01-18)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1-pre2...v1.1.2)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.2...v1.1.1-pre2)
-## [v1.1.1-pre2](https://github.com/microsoft/CoseSignTool/tree/v1.1.1-pre2) (2024-01-18)
+## [v1.1.2](https://github.com/microsoft/CoseSignTool/tree/v1.1.2) (2024-01-18)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1-pre1...v1.1.1-pre2)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1-pre1...v1.1.2)
**Merged pull requests:**
@@ -402,10 +406,6 @@
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0-pre6...v1.1.1)
-**Closed issues:**
-
-- Running the CoseSignTool without any arguments hits an exception rather than printing the helptext [\#65](https://github.com/microsoft/CoseSignTool/issues/65)
-
**Merged pull requests:**
- Revert use of primary constructor for CoseValidationError [\#69](https://github.com/microsoft/CoseSignTool/pull/69) ([lemccomb](https://github.com/lemccomb))
@@ -414,10 +414,6 @@
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0-pre5...v1.1.0-pre6)
-**Closed issues:**
-
-- CoseSignTool.exe `validate` incorrectly passes validation [\#66](https://github.com/microsoft/CoseSignTool/issues/66)
-
**Merged pull requests:**
- Fix missing Microsoft.Bcl.Hash reference [\#68](https://github.com/microsoft/CoseSignTool/pull/68) ([lemccomb](https://github.com/lemccomb))
@@ -473,7 +469,7 @@
## [v0.3.1-pre.10](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.10) (2023-10-10)
-[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.9...v0.3.1-pre.10)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.2...v0.3.1-pre.10)
**Merged pull requests:**
@@ -483,13 +479,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.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.2...v0.3.1-pre.9)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.9...v0.3.2)
-## [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.8...v0.3.2)
+[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.8...v0.3.1-pre.9)
**Merged pull requests:**
@@ -577,10 +573,6 @@
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/171c25c3ada781341ce98149bf0d98794c2c8b68...v0.3.1)
-**Closed issues:**
-
-- Action required: self-attest your goal for this repository [\#8](https://github.com/microsoft/CoseSignTool/issues/8)
-
**Merged pull requests:**
- Add publish step [\#13](https://github.com/microsoft/CoseSignTool/pull/13) ([lemccomb](https://github.com/lemccomb))
diff --git a/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs b/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs
index 44dcc764..69b157f0 100644
--- a/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs
+++ b/CoseIndirectSignature/Extensions/CoseSign1MessageIndirectSignatureExtensions.cs
@@ -1,192 +1,192 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-// Ignore Spelling: Cose
-
-namespace CoseIndirectSignature.Extensions;
-
-///
-/// Class which extends for indirect signature use cases.
-///
-///
-/// Logging is done through Trace.TraceError and Debug.WriteLine.
-///
-public static class CoseSign1MessageIndirectSignatureExtensions
-{
- private static readonly string AlgorithmGroupName = "algorithm";
- // Regex looks for "+hash-sha256" and will parse it out as a named group of "extension" with value "+hash-sha256" an the algorithm group name of "sha256"
- // Will also work with "+hash-sha3_256"
- private static readonly Regex HashMimeTypeExtension = new(@$"(?\+hash-(?<{AlgorithmGroupName}>[\w_]+))", RegexOptions.Compiled);
-
- ///
- /// Lazy populate all known hash algorithms from System.Security.Cryptography into a runtime cache
- ///
- /// This was done as is obsolete and instead it's recommended to call the Create method on each type directly.
- internal static readonly Lazy> HashAlgorithmLookup = new(() =>
- {
- Dictionary hashLookup = [];
-
- foreach (Type hashAlgorithm in FindAllDerivedHashAlgorithms())
- {
- if (hashLookup.ContainsKey(hashAlgorithm.BaseType.Name))
- {
- // ignore derived types of derived types for now.
- continue;
- }
-
- hashLookup.Add(hashAlgorithm.Name.ToUpperInvariant(), hashAlgorithm);
- }
-
- return hashLookup;
- });
-
- ///
- /// Finds all derived types within assembly.
- ///
- /// An enumerable to types which are derived from
- private static IEnumerable FindAllDerivedHashAlgorithms()
- {
- // The type to find all derived implementations from.
- Type baseType = typeof(HashAlgorithm);
-
- // loop through each type in the assembly containing System.Security.Cryptography.SHA256Managed and grab any type that are derived from the base type
- foreach (Type derivedType in Assembly.GetAssembly(typeof(SHA256Managed)).GetTypes().Where(t => t != baseType && baseType.IsAssignableFrom(t)))
- {
- // return this algorithm.
- yield return derivedType;
- }
- }
-
- ///
- /// Method which will create a
- ///
- /// The name of the intended HashAlgorithm to create. See Derived Types from https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.hashalgorithm?view=netstandard-2.0
- /// for examples names. I.E. SHA1|SHA256|SHA512|SHA3_256
- /// A HashAlgorithm which is created from the specified HashAlgorithmName or Null if none matched.
- public static HashAlgorithm? CreateHashAlgorithmFromName(HashAlgorithmName hashAlgorithmName)
- {
- if (!HashAlgorithmLookup.Value.TryGetValue(hashAlgorithmName.Name.ToUpperInvariant(), out Type hashAlgorithmType))
- {
- return null;
- }
-
- MethodInfo? methodInfo = hashAlgorithmType.GetMethod("Create", []);
- return methodInfo != null
- ? (HashAlgorithm)methodInfo.Invoke(null, null)
- : null;
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Ignore Spelling: Cose
+
+namespace CoseIndirectSignature.Extensions;
+
+///
+/// Class which extends for indirect signature use cases.
+///
+///
+/// Logging is done through Trace.TraceError and Debug.WriteLine.
+///
+public static class CoseSign1MessageIndirectSignatureExtensions
+{
+ private static readonly string AlgorithmGroupName = "algorithm";
+ // Regex looks for "+hash-sha256" and will parse it out as a named group of "extension" with value "+hash-sha256" an the algorithm group name of "sha256"
+ // Will also work with "+hash-sha3_256"
+ private static readonly Regex HashMimeTypeExtension = new(@$"(?\+hash-(?<{AlgorithmGroupName}>[\w_]+))", RegexOptions.Compiled);
+
+ ///
+ /// Lazy populate all known hash algorithms from System.Security.Cryptography into a runtime cache
+ ///
+ /// This was done as is obsolete and instead it's recommended to call the Create method on each type directly.
+ internal static readonly Lazy> HashAlgorithmLookup = new(() =>
+ {
+ Dictionary hashLookup = [];
+
+ foreach (Type hashAlgorithm in FindAllDerivedHashAlgorithms())
+ {
+ if (hashLookup.ContainsKey(hashAlgorithm.BaseType.Name))
+ {
+ // ignore derived types of derived types for now.
+ continue;
+ }
+
+ hashLookup.Add(hashAlgorithm.Name.ToUpperInvariant(), hashAlgorithm);
+ }
+
+ return hashLookup;
+ });
+
+ ///
+ /// Finds all derived types within assembly.
+ ///
+ /// An enumerable to types which are derived from
+ private static IEnumerable FindAllDerivedHashAlgorithms()
+ {
+ // The type to find all derived implementations from.
+ Type baseType = typeof(HashAlgorithm);
+
+ // loop through each type in the assembly containing System.Security.Cryptography.SHA256Managed and grab any type that are derived from the base type
+ foreach (Type derivedType in Assembly.GetAssembly(typeof(SHA256Managed)).GetTypes().Where(t => t != baseType && baseType.IsAssignableFrom(t)))
+ {
+ // return this algorithm.
+ yield return derivedType;
+ }
+ }
+
+ ///
+ /// Method which will create a
+ ///
+ /// The name of the intended HashAlgorithm to create. See Derived Types from https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.hashalgorithm?view=netstandard-2.0
+ /// for examples names. I.E. SHA1|SHA256|SHA512|SHA3_256
+ /// A HashAlgorithm which is created from the specified HashAlgorithmName or Null if none matched.
+ public static HashAlgorithm? CreateHashAlgorithmFromName(HashAlgorithmName hashAlgorithmName)
+ {
+ if (!HashAlgorithmLookup.Value.TryGetValue(hashAlgorithmName.Name.ToUpperInvariant(), out Type hashAlgorithmType))
+ {
+ return null;
+ }
+
+ MethodInfo? methodInfo = hashAlgorithmType.GetMethod("Create", []);
+ return methodInfo != null
+ ? (HashAlgorithm)methodInfo.Invoke(null, null)
+ : null;
+ }
+
+ ///
+ /// Extracts the hash algorithm name from the hash extension within the Content Type Protected Header if present.
+ ///
+ /// The CoseSign1Message to evaluate
+ /// The discovered Hash Algorithm Name from the Content Type Protected Header value of the CoseSign1Message.
+ /// True if successful in extracting a HashAlgorithmName from the Content Type Protected Header; False otherwise.
+ public static bool TryGetIndirectSignatureAlgorithm(this CoseSign1Message? @this, out HashAlgorithmName name)
+ {
+ if (@this == null)
+ {
+ Trace.TraceError($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a null CoseSign1Message object");
+ return false;
+ }
+
+ if (!@this.ProtectedHeaders.TryGetValue(CoseHeaderLabel.ContentType, out CoseHeaderValue contentTypeValue))
+ {
+ Trace.TraceWarning($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the ContentType protected header present.");
+ return false;
+ }
+
+ string contentType = contentTypeValue.GetValueAsString();
+ if (string.IsNullOrEmpty(contentType))
+ {
+ Trace.TraceWarning($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the ContentType protected header being a string value.");
+ return false;
+ }
+
+ Match mimeMatch = HashMimeTypeExtension.Match(contentType);
+ if (!mimeMatch.Success)
+ {
+ Trace.TraceWarning($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) with the ContentType protected header being \"{contentType}\" however it did not match the regex pattern \"{HashMimeTypeExtension}\".");
+ return false;
+ }
+
+ name = new HashAlgorithmName(mimeMatch.Groups[AlgorithmGroupName].Value.ToUpperInvariant());
+ Debug.WriteLine($"{nameof(TryGetIndirectSignatureAlgorithm)} extracted hash algorithm name: {name.Name}, returning true");
+ return true;
}
- ///
- /// Extracts the hash algorithm name from the hash extension within the Content Type Protected Header if present.
- ///
- /// The CoseSign1Message to evaluate
- /// The discovered Hash Algorithm Name from the Content Type Protected Header value of the CoseSign1Message.
- /// True if successful in extracting a HashAlgorithmName from the Content Type Protected Header; False otherwise.
- public static bool TryGetIndirectSignatureAlgorithm(this CoseSign1Message? @this, out HashAlgorithmName name)
- {
- if (@this == null)
- {
- Trace.TraceError($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a null CoseSign1Message object");
- return false;
- }
-
- if (!@this.ProtectedHeaders.TryGetValue(CoseHeaderLabel.ContentType, out CoseHeaderValue contentTypeValue))
- {
- Trace.TraceWarning($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the ContentType protected header present.");
- return false;
- }
-
- string contentType = contentTypeValue.GetValueAsString();
- if (string.IsNullOrEmpty(contentType))
- {
- Trace.TraceWarning($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) without the ContentType protected header being a string value.");
- return false;
- }
-
- Match mimeMatch = HashMimeTypeExtension.Match(contentType);
- if (!mimeMatch.Success)
- {
- Trace.TraceWarning($"{nameof(TryGetIndirectSignatureAlgorithm)} was called on a CoseSign1Message object({@this.GetHashCode()}) with the ContentType protected header being \"{contentType}\" however it did not match the regex pattern \"{HashMimeTypeExtension}\".");
- return false;
- }
-
- name = new HashAlgorithmName(mimeMatch.Groups[AlgorithmGroupName].Value.ToUpperInvariant());
- Debug.WriteLine($"{nameof(TryGetIndirectSignatureAlgorithm)} extracted hash algorithm name: {name.Name}, returning true");
- return true;
- }
-
- ///
- /// Determines whether the current CoseSign1Message object includes a Indirect signature.
- ///
- /// The CoseSign1Message to evaluate.
- /// True if the CoseSign1Message is a encoded indirect signature; False otherwise.
- public static bool IsIndirectSignature(this CoseSign1Message? @this) => @this.TryGetIsCoseHashEnvelope() || @this.TryGetIsCoseHashVContentType() || @this.TryGetIndirectSignatureAlgorithm(out _);
-
- ///
- /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact stream.
- ///
- /// The CoseSign1Message to evaluate.
- /// The artifact stream to evaluate.
- /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact stream; False otherwise.
- public static bool SignatureMatches(this CoseSign1Message? @this, Stream artifactStream)
- => SignatureMatchesInternal(@this, artifactStream: artifactStream);
-
- ///
- /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact bytes.
- ///
- /// The CoseSign1Message to evaluate.
- /// The artifact bytes to evaluate.
- /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise.
- public static bool SignatureMatches(this CoseSign1Message? @this, ReadOnlyMemory artifactBytes)
- => SignatureMatchesInternal(@this, artifactBytes: artifactBytes);
-
- ///
- /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact bytes or artifact stream.
- ///
- /// The CoseSign1Message to evaluate.
- /// The artifact bytes to evaluate.
- /// The artifact stream to evaluate.
- /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise.
- private static bool SignatureMatchesInternal(this CoseSign1Message? @this, ReadOnlyMemory? artifactBytes = null, Stream? artifactStream = null)
+ ///
+ /// Determines whether the current CoseSign1Message object includes a Indirect signature.
+ ///
+ /// The CoseSign1Message to evaluate.
+ /// True if the CoseSign1Message is a encoded indirect signature; False otherwise.
+ public static bool IsIndirectSignature(this CoseSign1Message? @this) => @this.TryGetIsCoseHashEnvelope() || @this.TryGetIsCoseHashVContentType() || @this.TryGetIndirectSignatureAlgorithm(out _);
+
+ ///
+ /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact stream.
+ ///
+ /// The CoseSign1Message to evaluate.
+ /// The artifact stream to evaluate.
+ /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact stream; False otherwise.
+ public static bool SignatureMatches(this CoseSign1Message? @this, Stream artifactStream)
+ => SignatureMatchesInternal(@this, artifactStream: artifactStream);
+
+ ///
+ /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact bytes.
+ ///
+ /// The CoseSign1Message to evaluate.
+ /// The artifact bytes to evaluate.
+ /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise.
+ public static bool SignatureMatches(this CoseSign1Message? @this, ReadOnlyMemory artifactBytes)
+ => SignatureMatchesInternal(@this, artifactBytes: artifactBytes);
+
+ ///
+ /// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact bytes or artifact stream.
+ ///
+ /// The CoseSign1Message to evaluate.
+ /// The artifact bytes to evaluate.
+ /// The artifact stream to evaluate.
+ /// True if the Indirect signature in the CoseSign1Message matches the signature of the artifact bytes; False otherwise.
+ private static bool SignatureMatchesInternal(this CoseSign1Message? @this, ReadOnlyMemory? artifactBytes = null, Stream? artifactStream = null)
=> @this.TryGetIsCoseHashEnvelope()
? @this.SignatureMatchesInternalCoseHashEnvelope(artifactBytes, artifactStream)
- : @this.TryGetIsCoseHashVContentType()
- ? @this.SignatureMatchesInternalCoseHashV(artifactBytes, artifactStream)
- : @this.SignatureMatchesInternalDirect(artifactBytes, artifactStream);
-
- ///
- /// Extracts a HashAlgoritm used to compute hashes from a given CoseSign1Message object if possible.
- ///
- /// The CoseSign1Message to evaluate.
- /// Set to a valid HashAlgorithm if return value is True, null otherwise.
- /// True if a valid HashAlgorithm was returned from the Content Type Protected Header; False otherwise.
- public static bool TryGetHashAlgorithm(this CoseSign1Message? @this, out HashAlgorithm? hasher)
- {
- hasher = null;
-
- if (!@this.TryGetIndirectSignatureAlgorithm(out HashAlgorithmName algorithmName))
- {
- Trace.TraceWarning($"{nameof(TryGetHashAlgorithm)} was called on a CoseSign1Message[{@this?.GetHashCode()}] object which did not have a valid hashing algorithm defined");
- return false;
- }
-
- if (!@this!.Content.HasValue)
- {
- Trace.TraceWarning($"{nameof(TryGetHashAlgorithm)} was called on a CoseSign1Message object which did not have a content value, unable to compute signature match.");
- return false;
- }
-
- // note that HashAlgorithm.Create() does not throw for names which do not properly map, null is returned.
- hasher = CreateHashAlgorithmFromName(algorithmName);
- if (hasher == null)
- {
- Trace.TraceWarning($"{nameof(TryGetHashAlgorithm)} was called on a CoseSign1Message object which did not have a hashing algorithm ({algorithmName.Name}) which could be instantiated.");
- return false;
- }
- Debug.WriteLine($"{nameof(TryGetHashAlgorithm)} created a HashAlgorithm from Hash Algorithm Name: {algorithmName.Name}");
- return true;
- }
-
+ : @this.TryGetIsCoseHashVContentType()
+ ? @this.SignatureMatchesInternalCoseHashV(artifactBytes, artifactStream)
+ : @this.SignatureMatchesInternalDirect(artifactBytes, artifactStream);
+
+ ///
+ /// Extracts a HashAlgoritm used to compute hashes from a given CoseSign1Message object if possible.
+ ///
+ /// The CoseSign1Message to evaluate.
+ /// Set to a valid HashAlgorithm if return value is True, null otherwise.
+ /// True if a valid HashAlgorithm was returned from the Content Type Protected Header; False otherwise.
+ public static bool TryGetHashAlgorithm(this CoseSign1Message? @this, out HashAlgorithm? hasher)
+ {
+ hasher = null;
+
+ if (!@this.TryGetIndirectSignatureAlgorithm(out HashAlgorithmName algorithmName))
+ {
+ Trace.TraceWarning($"{nameof(TryGetHashAlgorithm)} was called on a CoseSign1Message[{@this?.GetHashCode()}] object which did not have a valid hashing algorithm defined");
+ return false;
+ }
+
+ if (!@this!.Content.HasValue)
+ {
+ Trace.TraceWarning($"{nameof(TryGetHashAlgorithm)} was called on a CoseSign1Message object which did not have a content value, unable to compute signature match.");
+ return false;
+ }
+
+ // note that HashAlgorithm.Create() does not throw for names which do not properly map, null is returned.
+ hasher = CreateHashAlgorithmFromName(algorithmName);
+ if (hasher == null)
+ {
+ Trace.TraceWarning($"{nameof(TryGetHashAlgorithm)} was called on a CoseSign1Message object which did not have a hashing algorithm ({algorithmName.Name}) which could be instantiated.");
+ return false;
+ }
+ Debug.WriteLine($"{nameof(TryGetHashAlgorithm)} created a HashAlgorithm from Hash Algorithm Name: {algorithmName.Name}");
+ return true;
+ }
+
///
/// Computes if the encoded Indirect signature within the CoseSign1Message object matches the given artifact bytes or artifact stream.
///
@@ -209,5 +209,5 @@ private static bool SignatureMatchesInternalDirect(this CoseSign1Message @this,
bool equals = @this!.Content.Value.Span.SequenceEqual(artifactHash.Span);
Debug.WriteLine($"{nameof(SignatureMatchesInternalDirect)} compared the two hashes with lengths ({artifactHash.Length},{@this!.Content.Value.Length}) for equality and returned {equals}");
return equals;
- }
-}
+ }
+}
diff --git a/CoseSign1.Tests.Common/CoseSign1.Tests.Common.csproj b/CoseSign1.Tests.Common/CoseSign1.Tests.Common.csproj
index f0d3d85d..f0fbf45b 100644
--- a/CoseSign1.Tests.Common/CoseSign1.Tests.Common.csproj
+++ b/CoseSign1.Tests.Common/CoseSign1.Tests.Common.csproj
@@ -15,4 +15,8 @@
+
+
+
+
diff --git a/CoseSign1.Tests.Common/TestChainBuilder.cs b/CoseSign1.Tests.Common/TestChainBuilder.cs
new file mode 100644
index 00000000..a7619142
--- /dev/null
+++ b/CoseSign1.Tests.Common/TestChainBuilder.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Tests.Common;
+
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using CoseSign1.Certificates.Interfaces;
+
+///
+/// Custom Chain Builder Class Created For Integration Tests Purpose
+///
+public class TestChainBuilder : ICertificateChainBuilder, IDisposable
+{
+ private readonly X509Chain DefaultChainBuilder;
+
+ private readonly string? TestName;
+
+ public TestChainBuilder()
+ {
+ DefaultChainBuilder = new X509Chain();
+ }
+
+ ///
+ /// Added this just for the purpose of tests
+ ///
+ /// his would be used as the testName while creating the test chain in ChainElements()
+ public TestChainBuilder([CallerMemberName] string testName = "none")
+ {
+ DefaultChainBuilder = new X509Chain();
+ TestName = testName;
+ }
+
+ ///
+ /// overloading this behavior so as to make the integration tests work
+ ///
+ public IReadOnlyCollection ChainElements
+ {
+ get
+ {
+ X509Certificate2Collection testChain = TestCertificateUtils.CreateTestChain(TestName);
+
+ List elements = new(testChain);
+
+ return elements;
+ }
+ }
+
+ //overloading this so as to ensure there are no dependencies for the integration tests on X509Chain Build() method
+ public bool Build(X509Certificate2 certificate)
+ {
+ return true;
+ }
+
+ ///
+ public X509ChainPolicy ChainPolicy { get => DefaultChainBuilder.ChainPolicy; set => DefaultChainBuilder.ChainPolicy = value; }
+
+ ///
+ public X509ChainStatus[] ChainStatus => DefaultChainBuilder.ChainStatus;
+
+ ///
+ public void Dispose()
+ {
+ DefaultChainBuilder.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/CoseSign1.Transparent.CTS.Tests/AzureCtsTransparencyServiceExtensionsTests.cs b/CoseSign1.Transparent.CTS.Tests/AzureCtsTransparencyServiceExtensionsTests.cs
new file mode 100644
index 00000000..7460a9bb
--- /dev/null
+++ b/CoseSign1.Transparent.CTS.Tests/AzureCtsTransparencyServiceExtensionsTests.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using Azure.Security.CodeTransparency;
+using CoseSign1.Transparent.CTS;
+using CoseSign1.Transparent.CTS.Extensions;
+using CoseSign1.Transparent.Interfaces;
+using Moq;
+using NUnit.Framework;
+
+namespace CoseSign1.Transparent.CTS.Tests;
+
+///
+/// Unit tests for the class.
+///
+[TestFixture]
+public class AzureCtsTransparencyServiceExtensionsTests
+{
+ ///
+ /// Tests the method
+ /// to ensure it throws an when the input client is null.
+ ///
+ [Test]
+ public void ToCoseSign1TransparencyService_ThrowsArgumentNullException_WhenClientIsNull()
+ {
+ // Arrange
+ CodeTransparencyClient client = null;
+
+ // Act & Assert
+ Assert.That(
+ () => client.ToCoseSign1TransparencyService(),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("client"));
+ }
+
+ ///
+ /// Tests the method
+ /// to ensure it returns a valid instance when the input client is valid.
+ ///
+ [Test]
+ public void ToCoseSign1TransparencyService_ReturnsTransparencyService_WhenClientIsValid()
+ {
+ // Arrange
+ Mock mockClient = new Mock();
+
+ // Act
+ ITransparencyService result = mockClient.Object.ToCoseSign1TransparencyService();
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result, Is.InstanceOf());
+ }
+}
diff --git a/CoseSign1.Transparent.CTS.Tests/AzureCtsTransparencyServiceTests.cs b/CoseSign1.Transparent.CTS.Tests/AzureCtsTransparencyServiceTests.cs
new file mode 100644
index 00000000..f1022004
--- /dev/null
+++ b/CoseSign1.Transparent.CTS.Tests/AzureCtsTransparencyServiceTests.cs
@@ -0,0 +1,300 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Transparent.Tests;
+
+using System;
+using System.Formats.Cbor;
+using System.Security.Cryptography.Cose;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Security.CodeTransparency;
+using CoseSign1.Abstractions.Interfaces;
+using CoseSign1.Certificates.Interfaces;
+using CoseSign1.Certificates.Local;
+using CoseSign1.Interfaces;
+using CoseSign1.Tests.Common;
+using CoseSign1.Transparent.CTS;
+using CoseSign1.Transparent.Extensions;
+using Moq;
+
+///
+/// Unit tests for the class.
+///
+[TestFixture]
+public class AzureCtsTransparencyServiceTests
+{
+ private CoseSign1MessageFactory? messageFactory;
+ private ICoseSigningKeyProvider? signingKeyProvider;
+
+ [SetUp]
+ public void Setup()
+ {
+ X509Certificate2 testSigningCert = TestCertificateUtils.CreateCertificate();
+
+ //create object of custom ChainBuilder
+ ICertificateChainBuilder testChainBuilder = new TestChainBuilder();
+
+ //create coseSignKeyProvider with custom chainbuilder and local cert
+ //if no chainbuilder is specified, it will default to X509ChainBuilder, but that can't be used for integration tests
+ signingKeyProvider = new X509Certificate2CoseSigningKeyProvider(testChainBuilder, testSigningCert);
+
+ messageFactory = new();
+ }
+
+ ///
+ /// Tests the constructor of for null arguments.
+ ///
+ [Test]
+ public void Constructor_ThrowsArgumentNullException_WhenTransparencyClientIsNull()
+ {
+ // Act & Assert
+ Assert.That(
+ () => new AzureCtsTransparencyService(null),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("transparencyClient"));
+ }
+
+ ///
+ /// Tests the method for null arguments.
+ ///
+ [Test]
+ public void MakeTransparentAsync_ThrowsArgumentNullException_WhenMessageIsNull()
+ {
+ // Arrange
+ CodeTransparencyClient mockClient = Mock.Of();
+ AzureCtsTransparencyService service = new AzureCtsTransparencyService(mockClient);
+
+ // Act & Assert
+ Assert.That(
+ () => service.MakeTransparentAsync(null),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("message"));
+ }
+
+ ///
+ /// Tests the method for a failed operation.
+ ///
+ [Test]
+ public void MakeTransparentAsync_ThrowsInvalidOperationException_WhenOperationFails()
+ {
+ // Arrange
+ Mock mockClient = new Mock();
+ mockClient
+ .Setup(client => client.CreateEntryAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(MockFailedOperation());
+
+ AzureCtsTransparencyService service = new AzureCtsTransparencyService(mockClient.Object);
+ CoseSign1Message message = CreateMockCoseSign1Message();
+
+ // Act & Assert
+ Assert.That(
+ async () => await service.MakeTransparentAsync(message),
+ Throws.TypeOf().With.Message.Contains("CreateEntryAsync failed to return a response"));
+ }
+
+ ///
+ /// Tests the method for a failed operation with an invalid entry Id.
+ ///
+ [Test]
+ public void MakeTransparentAsync_ThrowsInvalidOperationException_WhenOperationFails_WithInvalidEntryId()
+ {
+ // Arrange
+ Mock mockClient = new Mock();
+ mockClient
+ .Setup(client => client.CreateEntryAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(MockSuccessfulOperationWithInvalidResponse());
+
+ AzureCtsTransparencyService service = new AzureCtsTransparencyService(mockClient.Object);
+ CoseSign1Message message = CreateMockCoseSign1Message();
+
+ // Act & Assert
+ Assert.That(
+ async () => await service.MakeTransparentAsync(message),
+ Throws.TypeOf().With.Message.Contains("The transparency operation failed, content was not a valid CBOR-encoded entryId."));
+ }
+
+ ///
+ /// Tests the method for a successful operation.
+ ///
+ [Test]
+ public async Task MakeTransparentAsync_ReturnsExpectedResult_WhenOperationSucceeds()
+ {
+ // Arrange
+ Mock mockClient = new Mock();
+ CoseSign1Message message = CreateMockCoseSign1Message();
+ message.AddReceipts(new List() { new byte[] { 1, 2, 3 } });
+ BinaryData mockEntryStatement = BinaryData.FromBytes(message.Encode());
+ mockClient
+ .Setup(client => client.CreateEntryAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(MockSuccessfulOperation());
+ mockClient
+ .Setup(client => client.GetEntryStatementAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Response.FromValue(mockEntryStatement, Mock.Of()));
+
+ AzureCtsTransparencyService service = new AzureCtsTransparencyService(mockClient.Object);
+
+ // Act
+ CoseSign1Message result = await service.MakeTransparentAsync(message);
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ }
+
+ ///
+ /// Tests the method for null arguments.
+ ///
+ [Test]
+ public void VerifyTransparencyAsync_ThrowsArgumentNullException_WhenMessageIsNull()
+ {
+ // Arrange
+ CodeTransparencyClient mockClient = Mock.Of();
+ AzureCtsTransparencyService service = new AzureCtsTransparencyService(mockClient);
+
+ // Act & Assert
+ Assert.That(
+ () => service.VerifyTransparencyAsync(null),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("message"));
+ }
+
+ ///
+ /// Tests the method for a message without a transparency header.
+ ///
+ [Test]
+ public void VerifyTransparencyAsync_ThrowsInvalidOperationException_WhenMessageLacksTransparencyHeader()
+ {
+ // Arrange
+ CodeTransparencyClient mockClient = Mock.Of();
+ AzureCtsTransparencyService service = new AzureCtsTransparencyService(mockClient);
+ CoseSign1Message message = CreateMockCoseSign1Message();
+
+ // Act & Assert
+ Assert.That(
+ () => service.VerifyTransparencyAsync(message),
+ Throws.TypeOf().With.Message.Contains("does not contain a transparency header"));
+ }
+
+ ///
+ /// Tests the method for a successful verification.
+ ///
+ [Test]
+ [Ignore("This test is ignored because RunTransparentStatementVerification is not virtual and cannot be mocked. This has been escalated to the package owners.")]
+ public async Task VerifyTransparencyAsync_ReturnsTrue_WhenVerificationSucceeds()
+ {
+ // Arrange
+ Mock mockClient = new Mock();
+ mockClient
+ .Setup(client => client.RunTransparentStatementVerification(It.IsAny()))
+ .Verifiable();
+
+ AzureCtsTransparencyService service = new AzureCtsTransparencyService(mockClient.Object);
+ CoseSign1Message message = CreateMessageWithTransparencyHeader();
+
+ // Act
+ bool result = await service.VerifyTransparencyAsync(message);
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ ///
+ /// Tests the method for null arguments.
+ ///
+ [Test]
+ public void VerifyTransparencyAsync_WithReceipt_ThrowsArgumentNullException_WhenArgumentsAreNull()
+ {
+ // Arrange
+ CodeTransparencyClient mockClient = Mock.Of();
+ AzureCtsTransparencyService service = new AzureCtsTransparencyService(mockClient);
+
+ // Act & Assert
+ Assert.That(
+ () => service.VerifyTransparencyAsync(null, new byte[] { 1, 2, 3 }),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("message"));
+
+ Assert.That(
+ () => service.VerifyTransparencyAsync(CreateMockCoseSign1Message(), null),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("receipt"));
+ }
+
+ ///
+ /// Tests the method for an empty receipt.
+ ///
+ [Test]
+ public void VerifyTransparencyAsync_WithReceipt_ThrowsArgumentOutOfRangeException_WhenReceiptIsEmpty()
+ {
+ // Arrange
+ CodeTransparencyClient mockClient = Mock.Of();
+ AzureCtsTransparencyService service = new AzureCtsTransparencyService(mockClient);
+ CoseSign1Message message = CreateMockCoseSign1Message();
+
+ // Act & Assert
+ Assert.That(
+ () => service.VerifyTransparencyAsync(message, Array.Empty()),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("receipt"));
+ }
+
+ ///
+ /// Helper method to create a mock failed operation.
+ ///
+ /// A mock failed operation.
+ private static Operation MockFailedOperation()
+ {
+ Mock> mockOperation = new Mock>();
+ mockOperation.Setup(op => op.HasValue).Returns(false);
+ mockOperation.Setup(op => op.GetRawResponse()).Returns(Mock.Of());
+ return mockOperation.Object;
+ }
+
+ ///
+ /// Helper method to create a mock successful operation but with an invalid Operati9onId.
+ ///
+ /// A mock successful operation.
+ private static Operation MockSuccessfulOperationWithInvalidResponse()
+ {
+ Mock> mockOperation = new Mock>();
+ mockOperation.Setup(op => op.HasValue).Returns(true);
+ CborWriter cborWriter = new CborWriter();
+ cborWriter.WriteStartMap(1);
+ cborWriter.WriteTextString("fooBar");
+ cborWriter.WriteTextString("12345");
+ cborWriter.WriteEndMap();
+ mockOperation.Setup(op => op.Value).Returns(BinaryData.FromBytes(cborWriter.Encode()));
+ return mockOperation.Object;
+ }
+
+ ///
+ /// Helper method to create a mock successful operation.
+ ///
+ /// A mock successful operation.
+ private static Operation MockSuccessfulOperation()
+ {
+ Mock> mockOperation = new Mock>();
+ mockOperation.Setup(op => op.HasValue).Returns(true);
+ CborWriter cborWriter = new CborWriter();
+ cborWriter.WriteStartMap(1);
+ cborWriter.WriteTextString("EntryId");
+ cborWriter.WriteTextString("12345");
+ cborWriter.WriteEndMap();
+ mockOperation.Setup(op => op.Value).Returns(BinaryData.FromBytes(cborWriter.Encode()));
+ return mockOperation.Object;
+ }
+
+ ///
+ /// Helper method to create a with a transparency header.
+ ///
+ /// A with a transparency header.
+ private CoseSign1Message CreateMessageWithTransparencyHeader()
+ {
+ CoseSign1Message message = CreateMockCoseSign1Message();
+ message.AddReceipts(new List() { new byte[] { 1, 2, 3 } });
+ return message;
+ }
+
+ private CoseSign1Message CreateMockCoseSign1Message()
+ {
+ byte[] testPayload = Encoding.ASCII.GetBytes("Payload1!");
+ return messageFactory!.CreateCoseSign1Message(testPayload, signingKeyProvider!, embedPayload: false);
+ }
+}
diff --git a/CoseSign1.Transparent.CTS.Tests/BinaryDataExtensionsTests.cs b/CoseSign1.Transparent.CTS.Tests/BinaryDataExtensionsTests.cs
new file mode 100644
index 00000000..bb47c772
--- /dev/null
+++ b/CoseSign1.Transparent.CTS.Tests/BinaryDataExtensionsTests.cs
@@ -0,0 +1,144 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Transparent.CTS.Tests;
+
+using System.Formats.Cbor;
+using CoseSign1.Transparent.CTS.Extensions;
+
+///
+/// Unit tests for the class.
+///
+[TestFixture]
+[Parallelizable(ParallelScope.All)]
+public class BinaryDataExtensionsTests
+{
+ ///
+ /// Tests the method for null input.
+ ///
+ [Test]
+ public void TryGetCtsEntryId_ReturnsFalse_WhenBinaryDataIsNull()
+ {
+ // Arrange
+ BinaryData binaryData = null;
+
+ // Act
+ bool result = binaryData.TryGetCtsEntryId(out string? entryId);
+
+ // Assert
+ Assert.That(result, Is.False);
+ Assert.That(entryId, Is.EqualTo(string.Empty));
+ }
+
+ ///
+ /// Tests the method for valid CBOR data containing "EntryId".
+ ///
+ [Test]
+ public void TryGetCtsEntryId_ReturnsTrue_WhenEntryIdIsPresent()
+ {
+ // Arrange
+ string expectedEntryId = "12345";
+ BinaryData binaryData = CreateCborBinaryDataWithEntryId(expectedEntryId);
+
+ // Act
+ bool result = binaryData.TryGetCtsEntryId(out string? entryId);
+
+ // Assert
+ Assert.That(result, Is.True);
+ Assert.That(entryId, Is.EqualTo(expectedEntryId));
+ }
+
+ ///
+ /// Tests the method for valid CBOR data without "EntryId".
+ ///
+ [Test]
+ public void TryGetCtsEntryId_ReturnsFalse_WhenEntryIdIsNotPresent()
+ {
+ // Arrange
+ BinaryData binaryData = CreateCborBinaryDataWithoutEntryId();
+
+ // Act
+ bool result = binaryData.TryGetCtsEntryId(out string? entryId);
+
+ // Assert
+ Assert.That(result, Is.False);
+ Assert.That(entryId, Is.EqualTo(string.Empty));
+ }
+
+ ///
+ /// Tests the method for invalid CBOR data.
+ ///
+ [Test]
+ public void TryGetCtsEntryId_ReturnsFalse_WhenCborDataIsInvalid()
+ {
+ // Arrange
+ BinaryData binaryData = BinaryData.FromBytes(new byte[] { 0xFF, 0xFF, 0xFF });
+
+ // Act
+ bool result = binaryData.TryGetCtsEntryId(out string? entryId);
+
+ // Assert
+ Assert.That(result, Is.False);
+ Assert.That(entryId, Is.EqualTo(string.Empty));
+ }
+
+ ///
+ /// Tests the method for CBOR data with unexpected structure.
+ ///
+ [Test]
+ public void TryGetCtsEntryId_ReturnsFalse_WhenCborDataHasUnexpectedStructure()
+ {
+ // Arrange
+ BinaryData binaryData = CreateCborBinaryDataWithUnexpectedStructure();
+
+ // Act
+ bool result = binaryData.TryGetCtsEntryId(out string? entryId);
+
+ // Assert
+ Assert.That(result, Is.False);
+ Assert.That(entryId, Is.EqualTo(string.Empty));
+ }
+
+ ///
+ /// Helper method to create a object with valid CBOR data containing "EntryId".
+ ///
+ /// The "EntryId" value to include in the CBOR data.
+ /// A object containing the CBOR-encoded data.
+ private static BinaryData CreateCborBinaryDataWithEntryId(string entryId)
+ {
+ CborWriter cborWriter = new CborWriter();
+ cborWriter.WriteStartMap(1);
+ cborWriter.WriteTextString("EntryId");
+ cborWriter.WriteTextString(entryId);
+ cborWriter.WriteEndMap();
+ return BinaryData.FromBytes(cborWriter.Encode());
+ }
+
+ ///
+ /// Helper method to create a object with valid CBOR data without "EntryId".
+ ///
+ /// A object containing the CBOR-encoded data.
+ private static BinaryData CreateCborBinaryDataWithoutEntryId()
+ {
+ CborWriter cborWriter = new CborWriter();
+ cborWriter.WriteStartMap(1);
+ cborWriter.WriteTextString("OtherKey");
+ cborWriter.WriteTextString("OtherValue");
+ cborWriter.WriteEndMap();
+ return BinaryData.FromBytes(cborWriter.Encode());
+ }
+
+ ///
+ /// Helper method to create a object with CBOR data having an unexpected structure.
+ ///
+ /// A object containing the CBOR-encoded data.
+ private static BinaryData CreateCborBinaryDataWithUnexpectedStructure()
+ {
+ CborWriter cborWriter = new CborWriter();
+ cborWriter.WriteStartArray(2);
+ cborWriter.WriteTextString("EntryId");
+ cborWriter.WriteTextString("12345");
+ cborWriter.WriteEndArray();
+ return BinaryData.FromBytes(cborWriter.Encode());
+ }
+}
diff --git a/CoseSign1.Transparent.CTS.Tests/CoseSign1.Transparent.CTS.Tests.csproj b/CoseSign1.Transparent.CTS.Tests/CoseSign1.Transparent.CTS.Tests.csproj
new file mode 100644
index 00000000..1b6e6c7f
--- /dev/null
+++ b/CoseSign1.Transparent.CTS.Tests/CoseSign1.Transparent.CTS.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CoseSign1.Transparent.CTS/AzureCtsTransparencyService.cs b/CoseSign1.Transparent.CTS/AzureCtsTransparencyService.cs
new file mode 100644
index 00000000..92a020df
--- /dev/null
+++ b/CoseSign1.Transparent.CTS/AzureCtsTransparencyService.cs
@@ -0,0 +1,165 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Transparent.CTS;
+
+using System;
+using System.Collections.Generic;
+using System.Formats.Cbor;
+using System.Security.Cryptography.Cose;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Security.CodeTransparency;
+using CoseSign1.Transparent.CTS.Extensions;
+using CoseSign1.Transparent.Extensions;
+using CoseSign1.Transparent.Interfaces;
+
+///
+/// Provides an implementation of the interface using Azure Code Transparency Service (CTS).
+/// This service enables the creation and verification of transparent COSE Sign1 messages.
+///
+public class AzureCtsTransparencyService : ITransparencyService
+{
+ private readonly CodeTransparencyClient TransparencyClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The used to interact with the Azure CTS.
+ /// Thrown if is null.
+ public AzureCtsTransparencyService(CodeTransparencyClient transparencyClient)
+ {
+ TransparencyClient = transparencyClient ?? throw new ArgumentNullException(nameof(transparencyClient));
+ }
+
+ ///
+ /// Creates a new transparent COSE Sign1 message by embedding additional metadata or headers
+ /// into the provided COSE Sign1 message using Azure CTS.
+ ///
+ /// The original to be transformed into a transparent message.
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a new
+ /// with the transparency metadata or headers applied.
+ ///
+ /// Thrown if is null.
+ /// Thrown if the transparency operation fails.
+ public async Task MakeTransparentAsync(CoseSign1Message message, CancellationToken cancellationToken = default)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+
+ // Encode the CoseSign1Message to a byte array
+ BinaryData content = BinaryData.FromBytes(message.Encode());
+
+ // Request the entry be created in the transparency service
+ Operation operation = await TransparencyClient.CreateEntryAsync(WaitUntil.Completed, content, cancellationToken).ConfigureAwait(false);
+
+ // Check if the operation was successful
+ if (!operation.HasValue)
+ {
+ throw new InvalidOperationException($"The transparency operation CreateEntryAsync failed to return a response: {operation.GetRawResponse().ReasonPhrase}");
+ }
+
+ // Get the entryId from the operation result
+ if (!operation.Value.TryGetCtsEntryId(out string entryId))
+ {
+ throw new InvalidOperationException($"The transparency operation failed, content was not a valid CBOR-encoded entryId.");
+ }
+
+ // Query the transparency service for the entry statement
+ Response transparentStatement = await TransparencyClient.GetEntryStatementAsync(entryId, cancellationToken).ConfigureAwait(false);
+
+ // Azure CTS replies with the full CoseSign1Message which will include the receipts already, so return it to the caller.
+ return CoseMessage.DecodeSign1(transparentStatement.Value.ToArray());
+ }
+
+ ///
+ /// Verifies the transparency of a given COSE Sign1 message by checking its metadata or headers
+ /// against the expected transparency rules using Azure CTS.
+ ///
+ /// The to verify for transparency.
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result is a boolean value indicating
+ /// whether the message meets the transparency requirements (true if valid, false otherwise).
+ ///
+ /// Thrown if is null.
+ /// Thrown if the message does not contain a transparency header.
+ public Task VerifyTransparencyAsync(CoseSign1Message message, CancellationToken cancellationToken = default)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+
+ // Check if the message contains a transparency header
+ if (!message.ContainsTransparencyHeader())
+ {
+ throw new InvalidOperationException($"The message does not contain a transparency header and cannot be verified.");
+ }
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Ask CTS to verify the entry
+ try
+ {
+ TransparencyClient.RunTransparentStatementVerification(message.Encode());
+ return Task.FromResult(true);
+ }
+ catch(InvalidOperationException)
+ {
+ return Task.FromResult(false);
+ }
+ catch(CborContentException)
+ {
+ return Task.FromResult(false);
+ }
+ catch(ArgumentException)
+ {
+ return Task.FromResult(false);
+ }
+ }
+
+ ///
+ /// Verifies the transparency of a given COSE Sign1 message using a specific receipt.
+ ///
+ /// The to verify for transparency.
+ /// The receipt to use for verification.
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result is a boolean value indicating
+ /// whether the message meets the transparency requirements when verified with the provided receipt (true if valid, false otherwise).
+ ///
+ /// Thrown if or is null.
+ /// Thrown if is empty.
+ public Task VerifyTransparencyAsync(CoseSign1Message message, byte[] receipt, CancellationToken cancellationToken = default)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+ if (receipt == null)
+ {
+ throw new ArgumentNullException(nameof(receipt));
+ }
+ if (receipt.Length == 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(receipt), "The receipt cannot be empty");
+ }
+
+ // CTS requires the receipt to be embedded in the CoseSign1Message for verification, so embed it
+ message.AddReceipts(new List { receipt });
+
+ // Verify the transparency of the message using the provided service
+ return VerifyTransparencyAsync(message, cancellationToken);
+ }
+}
diff --git a/CoseSign1.Transparent.CTS/CoseSign1.Transparent.CTS.csproj b/CoseSign1.Transparent.CTS/CoseSign1.Transparent.CTS.csproj
new file mode 100644
index 00000000..663112c9
--- /dev/null
+++ b/CoseSign1.Transparent.CTS/CoseSign1.Transparent.CTS.csproj
@@ -0,0 +1,55 @@
+
+
+
+
+ netstandard2.0
+ latest
+
+
+
+
+ true
+ enable
+ true
+ true
+ latest
+ true
+
+
+
+
+ True
+ True
+ ..\StrongNameKeys\35MSSharedLib1024.snk
+
+
+
+
+ $(MsBuildProjectName)
+ $(VersionNgt)
+ Microsoft
+ LICENSE
+ false
+ readme.md
+ ChangeLog.md
+ Extensions to CoseSign1.Transparent to enable Azure Code Transparency Service (CTS) as a transparency registration and validation service.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CoseSign1.Transparent.CTS/Extensions/AzureCtsTransparencyServiceExtensions.cs b/CoseSign1.Transparent.CTS/Extensions/AzureCtsTransparencyServiceExtensions.cs
new file mode 100644
index 00000000..ce33caf9
--- /dev/null
+++ b/CoseSign1.Transparent.CTS/Extensions/AzureCtsTransparencyServiceExtensions.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Transparent.CTS.Extensions;
+
+using System;
+using Azure.Security.CodeTransparency;
+using CoseSign1.Transparent.Interfaces;
+
+///
+/// Provides extension methods for working with the
+/// to integrate it with the interface.
+///
+public static class AzureCtsTransparencyServiceExtensions
+{
+ ///
+ /// Converts a instance into an implementation.
+ ///
+ /// The to be converted.
+ ///
+ /// An instance of that wraps the provided .
+ ///
+ /// Thrown if is null.
+ ///
+ /// This extension method simplifies the integration of the Azure Code Transparency Service (CTS)
+ /// with the interface, enabling seamless usage of CTS
+ /// within the CoseSign1 transparency ecosystem.
+ ///
+ public static ITransparencyService ToCoseSign1TransparencyService(this CodeTransparencyClient client)
+ {
+ if (client == null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return new AzureCtsTransparencyService(client);
+ }
+}
diff --git a/CoseSign1.Transparent.CTS/Extensions/BinaryDataExtensions.cs b/CoseSign1.Transparent.CTS/Extensions/BinaryDataExtensions.cs
new file mode 100644
index 00000000..a77b5e9c
--- /dev/null
+++ b/CoseSign1.Transparent.CTS/Extensions/BinaryDataExtensions.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Transparent.CTS.Extensions;
+
+using System;
+using System.Formats.Cbor;
+using Azure;
+
+///
+/// Provides extension methods for working with objects,
+/// specifically for extracting information from CBOR-encoded data.
+///
+public static class BinaryDataExtensions
+{
+ ///
+ /// Attempts to extract the "EntryId" value from the CBOR-encoded content of a .
+ ///
+ /// The containing the CBOR-encoded data.
+ ///
+ /// When this method returns, contains the extracted "EntryId" value if the operation was successful;
+ /// otherwise, contains null.
+ ///
+ ///
+ /// true if the "EntryId" was successfully extracted; otherwise, false.
+ ///
+ ///
+ /// This method reads the CBOR-encoded data as a map and searches for a key named "EntryId".
+ /// If the key is found, its corresponding value is returned as a string.
+ /// If the data is not valid CBOR or does not contain the "EntryId" key, the method returns false.
+ ///
+ /// Thrown if is null.
+ public static bool TryGetCtsEntryId(this BinaryData binaryData, out string? entryId)
+ {
+ entryId = string.Empty;
+
+ if (binaryData == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ CborReader cborReader = new(binaryData);
+ cborReader.ReadStartMap();
+ while (cborReader.PeekState() != CborReaderState.EndMap)
+ {
+ string key = cborReader.ReadTextString();
+ if (key == "EntryId")
+ {
+ entryId = cborReader.ReadTextString();
+ return true;
+ }
+ else
+ {
+ cborReader.SkipValue();
+ }
+ }
+ }
+ catch(InvalidOperationException)
+ {
+ return false;
+ }
+ catch (FormatException)
+ {
+ return false;
+ }
+ catch (CborContentException)
+ {
+ return false;
+ }
+
+ return false;
+ }
+}
diff --git a/CoseSign1.Transparent.Tests/CoseSign1.Transparent.Tests.csproj b/CoseSign1.Transparent.Tests/CoseSign1.Transparent.Tests.csproj
new file mode 100644
index 00000000..acbd78de
--- /dev/null
+++ b/CoseSign1.Transparent.Tests/CoseSign1.Transparent.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CoseSign1.Transparent.Tests/CoseSign1TransparencyMessageExtensionsTests.cs b/CoseSign1.Transparent.Tests/CoseSign1TransparencyMessageExtensionsTests.cs
new file mode 100644
index 00000000..dd4aaf83
--- /dev/null
+++ b/CoseSign1.Transparent.Tests/CoseSign1TransparencyMessageExtensionsTests.cs
@@ -0,0 +1,330 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Transparent.Tests;
+
+using System;
+using System.Collections.Generic;
+using System.Formats.Cbor;
+using System.Security.Cryptography;
+using System.Security.Cryptography.Cose;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using CoseSign1.Abstractions.Interfaces;
+using CoseSign1.Certificates.Interfaces;
+using CoseSign1.Certificates.Local;
+using CoseSign1.Interfaces;
+using CoseSign1.Tests.Common;
+using CoseSign1.Transparent.Extensions;
+using CoseSign1.Transparent.Interfaces;
+using Moq;
+using NUnit.Framework;
+
+///
+/// Unit tests for the class.
+///
+[TestFixture]
+[Parallelizable(ParallelScope.All)]
+public class CoseSign1TransparencyMessageExtensionsTests
+{
+ private CoseSign1MessageFactory? messageFactory;
+ private ICoseSigningKeyProvider? signingKeyProvider;
+
+ [SetUp]
+ public void Setup()
+ {
+ X509Certificate2 testSigningCert = TestCertificateUtils.CreateCertificate();
+
+ //create object of custom ChainBuilder
+ ICertificateChainBuilder testChainBuilder = new TestChainBuilder();
+
+ //create coseSignKeyProvider with custom chainbuilder and local cert
+ //if no chainbuilder is specified, it will default to X509ChainBuilder, but that can't be used for integration tests
+ signingKeyProvider = new X509Certificate2CoseSigningKeyProvider(testChainBuilder, testSigningCert);
+
+ messageFactory = new();
+ }
+
+ ///
+ /// Tests the method.
+ ///
+ /// Indicates whether the message is null.
+ /// Indicates whether the transparency service is null.
+ [Test]
+ [TestCase(true, false, TestName = "MakeTransparentAsync_ThrowsArgumentNullException_WhenMessageIsNull")]
+ [TestCase(false, true, TestName = "MakeTransparentAsync_ThrowsArgumentNullException_WhenServiceIsNull")]
+ public void MakeTransparentAsync_ThrowsArgumentNullException(bool messageIsNull, bool serviceIsNull)
+ {
+ // Arrange
+ CoseSign1Message message = messageIsNull ? null : CreateMockCoseSign1Message();
+ ITransparencyService transparencyService = serviceIsNull ? null : Mock.Of();
+
+ // Act & Assert
+ Assert.That(
+ () => message.MakeTransparentAsync(transparencyService),
+ Throws.TypeOf());
+ }
+
+ ///
+ /// Tests the method for successful execution.
+ ///
+ [Test]
+ public async Task MakeTransparentAsync_ReturnsExpectedResult()
+ {
+ // Arrange
+ CoseSign1Message message = CreateMockCoseSign1Message();
+ MockCoseHeaderValue(message, new List { new byte[] { 1, 2, 3 } });
+ Mock mockService = new Mock();
+ CoseSign1Message expectedMessage = CreateMockCoseSign1Message();
+ MockCoseHeaderValue(expectedMessage, new List { new byte[] { 1, 2, 3 } });
+ mockService
+ .Setup(service => service.MakeTransparentAsync(message, It.IsAny()))
+ .ReturnsAsync(expectedMessage);
+
+ // Act
+ CoseSign1Message result = await message.MakeTransparentAsync(mockService.Object);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(expectedMessage));
+ }
+
+ ///
+ /// Tests the method.
+ ///
+ /// Indicates whether the message is null.
+ [Test]
+ [TestCase(true, TestName = "ContainsTransparencyHeader_ThrowsArgumentNullException_WhenMessageIsNull")]
+ public void ContainsTransparencyHeader_ThrowsArgumentNullException(bool messageIsNull)
+ {
+ // Arrange
+ CoseSign1Message message = messageIsNull ? null : CreateMockCoseSign1Message();
+
+ // Act & Assert
+ Assert.That(
+ () => message.ContainsTransparencyHeader(),
+ Throws.TypeOf());
+ }
+
+ ///
+ /// Tests the method for valid cases.
+ ///
+ [Test]
+ public void ContainsTransparencyHeader_ReturnsExpectedResult()
+ {
+ // Arrange
+ CoseSign1Message message = CreateMockCoseSign1Message();
+ MockCoseHeaderValue(message, new List { new byte[] { 1, 2, 3 } });
+
+ // Act
+ bool result = message.ContainsTransparencyHeader();
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ ///
+ /// Tests the method.
+ ///
+ /// Indicates whether the message is null.
+ /// Indicates whether the transparency service is null.
+ [Test]
+ [TestCase(true, false, TestName = "VerifyTransparencyAsync_ThrowsArgumentNullException_WhenMessageIsNull")]
+ [TestCase(false, true, TestName = "VerifyTransparencyAsync_ThrowsArgumentNullException_WhenServiceIsNull")]
+ public void VerifyTransparencyAsync_ThrowsArgumentNullException(bool messageIsNull, bool serviceIsNull)
+ {
+ // Arrange
+ CoseSign1Message message = messageIsNull ? null : CreateMockCoseSign1Message();
+ ITransparencyService transparencyService = serviceIsNull ? null : Mock.Of();
+
+ // Act & Assert
+ Assert.That(
+ () => message.VerifyTransparencyAsync(transparencyService),
+ Throws.TypeOf());
+ }
+
+ ///
+ /// Tests the method for successful execution.
+ ///
+ [Test]
+ public async Task VerifyTransparencyAsync_ReturnsExpectedResult()
+ {
+ // Arrange
+ CoseSign1Message message = CreateMockCoseSign1Message();
+ MockCoseHeaderValue(message, new List { new byte[] { 1, 2, 3 } });
+ Mock mockService = new Mock();
+ mockService
+ .Setup(service => service.VerifyTransparencyAsync(message, It.IsAny()))
+ .ReturnsAsync(true);
+
+ // Act
+ bool result = await message.VerifyTransparencyAsync(mockService.Object);
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ ///
+ /// Tests the method.
+ ///
+ [Test]
+ public void TryGetReceipts_ReturnsExpectedResult()
+ {
+ // Arrange
+ CoseSign1Message message = CreateMockCoseSign1Message();
+ List expectedReceipts = new List { new byte[] { 1, 2, 3 } };
+ MockCoseHeaderValue(message, expectedReceipts);
+
+
+ // Act
+ bool result = message.TryGetReceipts(out List? receipts);
+
+ // Assert
+ Assert.That(result, Is.True);
+ Assert.That(receipts, Is.EquivalentTo(expectedReceipts));
+ }
+
+ ///
+ /// Tests the method.
+ ///
+ [Test]
+ public void TryGetReceipts_ThrowsArgumentNullException_WhenArgumentsAreNull()
+ {
+ // Arrange
+ CoseSign1Message message = null;
+
+ // Act & Assert
+ Assert.That(
+ () => message.TryGetReceipts(out _),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("message"));
+ }
+
+ ///
+ /// Tests the method.
+ ///
+ [Test]
+ public void TryGetReceipts_NoProtectedHeader_ReturnsFalse()
+ {
+ // Arrange
+ CoseSign1Message message = CreateMockCoseSign1Message();
+
+ // Act & Assert
+ Assert.That(message.TryGetReceipts(out _), Is.False);
+ }
+
+ ///
+ /// Tests the method.
+ ///
+ [Test]
+ public void TryGetReceipts_InvalidProtectedHeader_ReturnsFalse()
+ {
+ // Arrange
+ CoseSign1Message message = CreateMockCoseSign1Message();
+ message.UnprotectedHeaders.Add(CoseSign1TransparencyMessageExtensions.TransparencyHeaderLabel, CoseHeaderValue.FromBytes(new byte[]{ 1, 2, 3}));
+
+ // Act & Assert
+ Assert.That(message.TryGetReceipts(out _), Is.False);
+ }
+
+ ///
+ /// Tests the method.
+ ///
+ [Test]
+ public void TryGetReceipts_ValidProtectedHeader_AdditionalFields_ReturnsTrue()
+ {
+ // Arrange
+ CoseSign1Message message = CreateMockCoseSign1Message();
+ CborWriter cborWriter = new CborWriter();
+ cborWriter.WriteStartArray(2);
+ cborWriter.WriteDouble(1.0);
+ cborWriter.WriteByteString(new byte[] { 1, 2, 3 });
+ cborWriter.WriteEndArray();
+
+ message.UnprotectedHeaders.Add(CoseSign1TransparencyMessageExtensions.TransparencyHeaderLabel, CoseHeaderValue.FromEncodedValue(cborWriter.Encode()));
+
+ // Act & Assert
+ Assert.That(message.TryGetReceipts(out _), Is.True);
+ }
+
+ ///
+ /// Tests the method for null arguments.
+ ///
+ [Test]
+ public void AddReceipts_ThrowsArgumentNullException_WhenArgumentsAreNull()
+ {
+ // Arrange
+ CoseSign1Message message = null;
+ CoseSign1Message message2 = CreateMockCoseSign1Message();
+ List receipts = null;
+ List receipts2 = new List();
+
+ // Act & Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(
+ () => message.AddReceipts(receipts),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("message"));
+
+ Assert.That(
+ () => message2.AddReceipts(receipts),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("receipts"));
+
+ Assert.That(
+ () => message2.AddReceipts(receipts2),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("receipts"));
+ });
+ }
+
+ ///
+ /// Tests the method for valid cases.
+ ///
+ [Test]
+ public void AddReceipts_AddsReceiptsSuccessfully()
+ {
+ // Arrange
+ CoseSign1Message message = CreateMockCoseSign1Message();
+ List receipts = new List { new byte[] { 1, 2, 3 } };
+ // Act
+ message.AddReceipts(receipts);
+ // Assert
+ Assert.That(message.TryGetReceipts(out List? result), Is.True);
+ Assert.That(result, Is.EquivalentTo(receipts));
+ }
+
+ ///
+ /// Tests the method for valid cases.
+ ///
+ [Test]
+ public void AddReceipts_AddsReceipts_WithExistingReceipts_Successfully()
+ {
+ // Arrange
+ CoseSign1Message message = CreateMockCoseSign1Message();
+ byte[] firstReceipt = new byte[] { 4, 5, 6 };
+ byte[] secondReceipt = new byte[] { 1, 2, 3 };
+ message.AddReceipts(new List { firstReceipt });
+ List receipts = new List { secondReceipt };
+ // Act
+ message.AddReceipts(receipts);
+ // Assert
+ Assert.That(message.TryGetReceipts(out List? result), Is.True);
+ Assert.That(result[0], Is.EquivalentTo(firstReceipt));
+ Assert.That(result[1], Is.EquivalentTo(secondReceipt));
+ }
+
+ ///
+ /// Helper method to mock the behavior of a for receipts.
+ ///
+ /// The to mock.
+ /// The list of receipts to return.
+ private static void MockCoseHeaderValue(CoseSign1Message message, List receipts)
+ {
+ message.AddReceipts(receipts);
+ }
+
+ private CoseSign1Message CreateMockCoseSign1Message()
+ {
+ byte[] testPayload = Encoding.ASCII.GetBytes("Payload1!");
+ return messageFactory!.CreateCoseSign1Message(testPayload, signingKeyProvider!, embedPayload: false);
+ }
+}
diff --git a/CoseSign1.Transparent/CoseSign1.Transparent.csproj b/CoseSign1.Transparent/CoseSign1.Transparent.csproj
new file mode 100644
index 00000000..942060a0
--- /dev/null
+++ b/CoseSign1.Transparent/CoseSign1.Transparent.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
+ Extensions to CoseSign1Message to enable Transparency registration and validation.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CoseSign1.Transparent/Extensions/CoseSign1TransparencyMessageExtensions.cs b/CoseSign1.Transparent/Extensions/CoseSign1TransparencyMessageExtensions.cs
new file mode 100644
index 00000000..96b25092
--- /dev/null
+++ b/CoseSign1.Transparent/Extensions/CoseSign1TransparencyMessageExtensions.cs
@@ -0,0 +1,249 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Transparent.Extensions;
+
+using System;
+using System.Collections.Generic;
+using System.Formats.Cbor;
+using System.Security.Cryptography.Cose;
+using System.Threading;
+using System.Threading.Tasks;
+using CoseSign1.Transparent.Interfaces;
+
+///
+/// Provides extension methods for enhancing the functionality of
+/// with transparency features using an .
+///
+public static class CoseSign1TransparencyMessageExtensions
+{
+ ///
+ /// The header label used to indicate transparency in COSE Sign1 messages in SCITT.
+ ///
+ ///
+ /// The label value 394 was a previously proposed identifier for transparency in COSE Sign1 messages.
+ /// However, it is not yet finalized in the IANA registry. The SCITT draft now uses a placeholder
+ /// value (TBD_0) for this label:
+ /// https://github.com/ietf-wg-scitt/draft-ietf-scitt-architecture/commit/fbcb3715e95ee709da6b1051498cf561bc5069a4
+ ///
+ public static CoseHeaderLabel TransparencyHeaderLabel = new CoseHeaderLabel(394);
+
+ ///
+ /// Asynchronously transforms a into a transparent message
+ /// by leveraging the provided .
+ ///
+ /// The original to be made transparent.
+ /// The used to apply transparency.
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a new
+ /// with transparency metadata or headers applied.
+ ///
+ ///
+ /// Thrown if or is null.
+ ///
+ public static Task MakeTransparentAsync(this CoseSign1Message message, ITransparencyService transparencyService, CancellationToken cancellationToken = default)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+ if (transparencyService == null)
+ {
+ throw new ArgumentNullException(nameof(transparencyService));
+ }
+
+ return transparencyService.MakeTransparentAsync(message, cancellationToken);
+ }
+
+ ///
+ /// Checks whether the given contains a transparency-related header.
+ ///
+ /// The to check for transparency headers.
+ ///
+ /// True if the message contains the transparency header in either the protected or unprotected headers; otherwise, false.
+ ///
+ /// Thrown if is null.
+ public static bool ContainsTransparencyHeader(this CoseSign1Message message)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+
+ // Check for the presence of transparency-related headers
+ return message.UnprotectedHeaders.ContainsKey(TransparencyHeaderLabel);
+ }
+
+ ///
+ /// Asynchronously verifies the transparency of a given
+ /// using the provided .
+ ///
+ /// The to verify for transparency.
+ /// The used to perform the verification.
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result is a boolean value indicating
+ /// whether the message meets the transparency requirements (true if valid, false otherwise).
+ ///
+ ///
+ /// Thrown if or is null.
+ ///
+ public static Task VerifyTransparencyAsync(this CoseSign1Message message, ITransparencyService transparencyService, CancellationToken cancellationToken = default)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+ if (transparencyService == null)
+ {
+ throw new ArgumentNullException(nameof(transparencyService));
+ }
+
+ // Verify the transparency of the message using the provided service
+ return transparencyService.VerifyTransparencyAsync(message, cancellationToken);
+ }
+
+ ///
+ /// Attempts to extract receipts from the transparency-related header of a .
+ ///
+ /// The to extract receipts from.
+ ///
+ /// When this method returns, contains a list of byte arrays representing the receipts if the operation was successful;
+ /// otherwise, contains null.
+ ///
+ ///
+ /// True if the receipts were successfully extracted; otherwise, false.
+ ///
+ /// Thrown if is null.
+ public static bool TryGetReceipts(this CoseSign1Message message, out List? receipts)
+ {
+ receipts = null;
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+
+ // The Transparency header is required for receipts to be embedded.
+ if (!message.UnprotectedHeaders.TryGetValue(TransparencyHeaderLabel, out CoseHeaderValue receiptValue))
+ {
+ return false;
+ }
+
+ // parse the header value into a list of byte arrays
+ try
+ {
+ receipts = receiptValue.ParseCoseHeaderToArray();
+ return true;
+ }
+ catch(FormatException)
+ {
+ return false;
+ }
+ catch (InvalidOperationException)
+ {
+ return false;
+ }
+ catch (CborContentException)
+ {
+ return false;
+ }
+ catch(ArgumentOutOfRangeException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Adds receipts to the transparency-related header of a .
+ ///
+ /// The to which receipts will be added.
+ /// The list of byte arrays representing the receipts to add.
+ ///
+ /// Thrown if is null or if is null.
+ ///
+ public static void AddReceipts(this CoseSign1Message message, List receipts)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+ if (receipts == null)
+ {
+ throw new ArgumentNullException(nameof(receipts));
+ }
+ if(receipts.Count == 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(receipts), "Receipts cannot be empty.");
+ }
+
+ _ = message.TryGetReceipts(out List? existingReceiptsList);
+
+ // Write the receipts to a CBOR-encoded array
+ CborWriter cborWriter = new();
+ cborWriter.WriteStartArray(receipts.Count + (existingReceiptsList?.Count ?? 0));
+
+ // Add existing receipts to the array if they exist
+ if (existingReceiptsList != null)
+ {
+ foreach (byte[] receipt in existingReceiptsList)
+ {
+ cborWriter.WriteByteString(receipt);
+ }
+ }
+
+ // Add the new receipts to the array
+ foreach (byte[] receipt in receipts)
+ {
+ cborWriter.WriteByteString(receipt);
+ }
+
+ // End the CBOR array
+ cborWriter.WriteEndArray();
+
+ // Remove the existing receipts from the unprotected headers
+ if (message.UnprotectedHeaders.ContainsKey(TransparencyHeaderLabel))
+ {
+ message.UnprotectedHeaders.Remove(TransparencyHeaderLabel);
+ }
+
+ // Add the new receipts to the unprotected headers
+ message.UnprotectedHeaders.Add(TransparencyHeaderLabel, CoseHeaderValue.FromEncodedValue(cborWriter.Encode()));
+ }
+
+ ///
+ /// Parses a into a list of byte arrays.
+ ///
+ /// The to parse.
+ /// A list of byte arrays extracted from the header value.
+ ///
+ /// Thrown if the header value is not in a valid CBOR array format.
+ ///
+ private static List ParseCoseHeaderToArray(this CoseHeaderValue headerValue)
+ {
+ List values = new();
+ CborReader cborReader = new(headerValue.EncodedValue);
+ if (cborReader.PeekState() != CborReaderState.StartArray)
+ {
+ throw new InvalidOperationException("Invalid CBOR format for receipts, they must be an array.");
+ }
+ cborReader.ReadStartArray();
+
+ while (cborReader.PeekState() != CborReaderState.EndArray)
+ {
+ if (cborReader.PeekState() == CborReaderState.ByteString)
+ {
+ values.Add(cborReader.ReadByteString());
+ }
+ else
+ {
+ cborReader.SkipValue();
+ }
+ }
+ return values;
+ }
+}
diff --git a/CoseSign1.Transparent/Interfaces/ITransparencyService.cs b/CoseSign1.Transparent/Interfaces/ITransparencyService.cs
new file mode 100644
index 00000000..cfb6b366
--- /dev/null
+++ b/CoseSign1.Transparent/Interfaces/ITransparencyService.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Transparent.Interfaces;
+
+using System.Security.Cryptography.Cose;
+using System.Threading;
+using System.Threading.Tasks;
+
+///
+/// Defines a service for creating and verifying transparent COSE Sign1 messages.
+/// Transparency in this context refers to embedding additional metadata or headers
+/// into COSE Sign1 messages to ensure traceability and auditability.
+///
+public interface ITransparencyService
+{
+ ///
+ /// Creates a new transparent COSE Sign1 message by embedding additional metadata or headers
+ /// into the provided COSE Sign1 message.
+ ///
+ /// The original to be transformed into a transparent message.
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a new
+ /// with the transparency metadata or headers applied.
+ ///
+ /// Thrown if is null.
+ Task MakeTransparentAsync(CoseSign1Message message, CancellationToken cancellationToken = default);
+
+ ///
+ /// Verifies the transparency of a given COSE Sign1 message by checking its metadata or headers
+ /// against the expected transparency rules.
+ ///
+ /// The to verify for transparency.
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result is a boolean value indicating
+ /// whether the message meets the transparency requirements (true if valid, false otherwise).
+ ///
+ /// Thrown if is null.
+ Task VerifyTransparencyAsync(CoseSign1Message message, CancellationToken cancellationToken = default);
+
+ ///
+ /// Verifies the transparency of a given COSE Sign1 message using a specific receipt.
+ ///
+ /// The to verify for transparency.
+ /// The receipt to use for verification.
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result is a boolean value indicating
+ /// whether the message meets the transparency requirements when verified with the provided receipt (true if valid, false otherwise).
+ ///
+ ///
+ /// Thrown if or is null.
+ ///
+ Task VerifyTransparencyAsync(CoseSign1Message message, byte[] receipt, CancellationToken cancellationToken = default);
+}
diff --git a/CoseSignTool.sln b/CoseSignTool.sln
index 5fe0dee6..797a6368 100644
--- a/CoseSignTool.sln
+++ b/CoseSignTool.sln
@@ -37,11 +37,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{36BAA2CE-A
docs\CODE_OF_CONDUCT.md = docs\CODE_OF_CONDUCT.md
docs\CONTRIBUTING.md = docs\CONTRIBUTING.md
docs\CoseHandler.md = docs\CoseHandler.md
- CoseIndirectSignature.md = CoseIndirectSignature.md
- CoseSign1.Abstractions.md = CoseSign1.Abstractions.md
- CoseSign1.Certificates.md = CoseSign1.Certificates.md
+ docs\CoseIndirectSignature.md = docs\CoseIndirectSignature.md
+ docs\CoseSign1.Abstractions.md = docs\CoseSign1.Abstractions.md
+ docs\CoseSign1.Certificates.md = docs\CoseSign1.Certificates.md
CoseSign1.md = CoseSign1.md
docs\CoseSignTool.md = docs\CoseSignTool.md
+ docs\CoseSign1.Transparent.md = docs\CoseSign1.Transparent.md
docs\SECURITY.md = docs\SECURITY.md
docs\STYLE.md = docs\STYLE.md
docs\SUPPORT.md = docs\SUPPORT.md
@@ -67,6 +68,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoseSign1.Headers", "CoseSi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoseSign1.Headers.Tests", "CoseSign1.Headers.Tests\CoseSign1.Headers.Tests.csproj", "{5181310A-CA82-4399-9197-86B468F7FCE9}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Transparent", "CoseSign1.Transparent\CoseSign1.Transparent.csproj", "{F01E36FD-EF26-4F61-B025-1E9C603047D3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Transparent.Tests", "CoseSign1.Transparent.Tests\CoseSign1.Transparent.Tests.csproj", "{033430CC-6D03-415A-A476-96A3922231D0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Transparent.CTS.Tests", "CoseSign1.Transparent.CTS.Tests\CoseSign1.Transparent.CTS.Tests.csproj", "{DDDE7449-C4C6-4BA0-A815-0BD532CFA331}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Transparent.CTS", "CoseSign1.Transparent.CTS\CoseSign1.Transparent.CTS.csproj", "{A9F5FBE2-C980-0ACD-E02C-49C4640D227E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -245,6 +254,54 @@ Global
{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
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Debug|x64.Build.0 = Debug|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Release|ARM64.Build.0 = Release|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Release|x64.ActiveCfg = Release|Any CPU
+ {F01E36FD-EF26-4F61-B025-1E9C603047D3}.Release|x64.Build.0 = Release|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Debug|x64.Build.0 = Debug|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Release|ARM64.Build.0 = Release|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Release|x64.ActiveCfg = Release|Any CPU
+ {033430CC-6D03-415A-A476-96A3922231D0}.Release|x64.Build.0 = Release|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Debug|x64.Build.0 = Debug|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Release|ARM64.Build.0 = Release|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Release|x64.ActiveCfg = Release|Any CPU
+ {DDDE7449-C4C6-4BA0-A815-0BD532CFA331}.Release|x64.Build.0 = Release|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Debug|x64.Build.0 = Debug|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Release|ARM64.Build.0 = Release|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Release|x64.ActiveCfg = Release|Any CPU
+ {A9F5FBE2-C980-0ACD-E02C-49C4640D227E}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/README.md b/README.md
index ef8c0ac6..0ffc55e7 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,7 @@ For advanced topics such as time stamping, see [Advanced](./docs/Advanced.md)
## How do I make this better?
You would like to help? Great!
First [check to make sure the work isn't already planned](#state-of-the-project), then...
-* If you find a bug or have a feature reccomendation, [log an issue.](https://github.com/microsoft/CoseSignTool/issues)
+* If you find a bug or have a feature recommendation, [log an issue.](https://github.com/microsoft/CoseSignTool/issues)
* If you would like to contribute actual code to the repo or comment on the pull requests of others, read our [contributor guidelines](./docs/CONTRIBUTING.md) and [style guidelines](./docs/STYLE.md), and then make your contribution.
## State of the project
@@ -79,7 +79,6 @@ This is an alpha release, so there are some planned features that are not yet in
The planned work is currently tracked only in an internal Microsoft ADO instance but will be moved to Github Issues soon. In the meantime, here is some of the work currently planned.
#### New features
-* Add suport for SCITT time stamping
* Enable specifying a mandatory cert chain root for validation
* Simplify digest signing scenario
* Support batch operations in CoseSignTool to reduce file and cert store reads
diff --git a/CoseIndirectSignature.md b/docs/CoseIndirectSignature.md
similarity index 100%
rename from CoseIndirectSignature.md
rename to docs/CoseIndirectSignature.md
diff --git a/CoseSign1.Abstractions.md b/docs/CoseSign1.Abstractions.md
similarity index 100%
rename from CoseSign1.Abstractions.md
rename to docs/CoseSign1.Abstractions.md
diff --git a/CoseSign1.Certificates.md b/docs/CoseSign1.Certificates.md
similarity index 100%
rename from CoseSign1.Certificates.md
rename to docs/CoseSign1.Certificates.md
diff --git a/docs/CoseSign1.Transparent.md b/docs/CoseSign1.Transparent.md
new file mode 100644
index 00000000..a146c550
--- /dev/null
+++ b/docs/CoseSign1.Transparent.md
@@ -0,0 +1,238 @@
+# CoseSign1.Transparent Documentation
+
+## Overview
+The `CoseSign1.Transparent` project provides extensions to the `CoseSign1Message` class, enabling the creation and validation of transparent COSE (CBOR Object Signing and Encryption) messages. These extensions are designed to facilitate transparency registration and validation workflows, ensuring secure and traceable message handling.
+
+## Prerequisites
+Before using this library, ensure the following:
+- Your project targets `.NET Standard 2.0` or higher.
+- You have included the `CoseSign1.Transparent` package in your project.
+- You have access to the `CoseSign1.Abstractions` project, as it is referenced by this library.
+
+## Installation
+To use this library, add a reference to the `CoseSign1.Transparent` project in your solution. If using NuGet, ensure the package is installed:
+
+```text
+dotnet add package CoseSign1.Transparency
+```
+## Namespace
+Include the following namesapces in your code:
+```csharp
+using CoseSign1.Transparent.Extensions;
+using CoseSign1.Transparent.Interfaces;
+```
+
+## Features
+- **Transparent Message Creation**: Generate COSE messages with transparency metadata.
+- **Validation**: Validate COSE messages against transparency requirements.
+
+## Usage
+### 1. Creating a Transparent COSE Message
+To create a transparent COSE Sign1 message, use the `MakeTransparentAsync` method. This method embeds transparency metadata into the message.
+#### Example: Creating a Transparent Message:
+```csharp
+using System;
+using System.Security.Cryptography.Cose;
+using System.Threading.Tasks;
+using CoseSign1.Transparent.Interfaces;
+using CoseSign1.Transparent.CTS;
+
+public class TransparencyExample
+{
+ public async Task CreateTransparentMessage()
+ {
+ // Create a COSE Sign1 message (example payload)
+ CoseSign1Message message = new CoseSign1Message
+ {
+ Content = new byte[] { 1, 2, 3, 4 }
+ };
+
+ // Initialize the transparency service (using Azure CTS as an example)
+ CodeTransparencyClient transparencyClient = new CodeTransparencyClient();
+ ITransparencyService transparencyService = new AzureCtsTransparencyService(transparencyClient);
+
+ // Make the message transparent
+ CoseSign1Message transparentMessage = await message.MakeTransparentAsync(transparencyService);
+
+ Console.WriteLine("Transparent message created successfully.");
+ }
+}
+```
+### 2. Verifying Transparency
+To verify the transparency of a COSE Sign1 message, use the `VerifyTransparencyAsync` method. This ensures the message complies with transparency rules.
+#### Example: Verifying a Transparent Message with embedded receipt:
+```csharp
+using System;
+using System.Security.Cryptography.Cose;
+using System.Threading.Tasks;
+using CoseSign1.Transparent.Interfaces;
+using CoseSign1.Transparent.CTS;
+
+public class TransparencyExample
+{
+ public async Task VerifyTransparentMessage()
+ {
+ // Example COSE Sign1 message
+ CoseSign1Message message = new CoseSign1Message
+ {
+ Content = new byte[] { 1, 2, 3, 4 }
+ };
+
+ // Initialize the transparency service
+ ITransparencyService transparencyService = new CodeTransparencyClient().ToCoseSign1TransparentService();
+
+ // Verify the transparency of the message
+ bool isTransparent = await message.VerifyTransparencyAsync(transparencyService);
+
+ Console.WriteLine($"Message transparency verification result: {isTransparent}");
+ }
+}
+```
+#### Example: Verifying a Transparent Message without an embedded receipt:
+```csharp
+using System;
+using System.Security.Cryptography.Cose;
+using System.Threading.Tasks;
+using CoseSign1.Transparent.Interfaces;
+using CoseSign1.Transparent.CTS;
+
+public class TransparencyExample
+{
+ public async Task VerifyTransparentMessageWithReceipt()
+ {
+ // Example COSE Sign1 message
+ CoseSign1Message message = new CoseSign1Message
+ {
+ Content = new byte[] { 1, 2, 3, 4 }
+ };
+
+ // Example receipt
+ byte[] receipt = new byte[] { 5, 6, 7, 8 };
+
+ // Initialize the transparency service
+ ITransparencyService transparencyService = new CodeTransparencyClient().ToCoseSign1TransparentService();
+
+ // Verify the transparency of the message with the receipt
+ bool isTransparent = await message.VerifyTransparencyAsync(transparencyService, receipt);
+
+ Console.WriteLine($"Message transparency verification with receipt result: {isTransparent}");
+ }
+}
+
+```
+### 3. Managing Receipts
+Receipts may embedded in the transparency-related headers of COSE Sign1 messages. You can extract or add receipts using the following methods:
+#### Extracting Receipts
+Use the `TryGetReceipts` method to extract receipts from a COSE Sign1 message.
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography.Cose;
+using CoseSign1.Transparent.Extensions;
+
+public class ReceiptExample
+{
+ public void ExtractReceipts()
+ {
+ // Example COSE Sign1 message
+ CoseSign1Message message = new CoseSign1Message();
+
+ // Extract receipts
+ if (message.TryGetReceipts(out List? receipts))
+ {
+ Console.WriteLine("Receipts extracted successfully.");
+ }
+ else
+ {
+ Console.WriteLine("No receipts found.");
+ }
+ }
+}
+```
+#### Adding Receipts
+Use the `AddReceipts` method to add receipts to a COSE Sign1 message.
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography.Cose;
+using CoseSign1.Transparent.Extensions;
+
+public class ReceiptExample
+{
+ public void AddReceipts()
+ {
+ // Example COSE Sign1 message
+ CoseSign1Message message = new CoseSign1Message();
+
+ // Example receipts
+ List receipts = new List
+ {
+ new byte[] { 1, 2, 3 },
+ new byte[] { 4, 5, 6 }
+ };
+
+ // Add receipts to the message
+ message.AddReceipts(receipts);
+
+ Console.WriteLine("Receipts added successfully.");
+ }
+}
+```
+## Advanced Topics
+### Transparency Header
+The transparency header is identified by the `TransparencyHeaderLabel` field. This label is used to embed and retrieve transparency-related metadata.
+```csharp
+using CoseSign1.Transparent.Extensions;
+
+Console.WriteLine($"Transparency Header Label: {CoseSign1TransparencyMessageExtensions.TransparencyHeaderLabel}");
+```
+### Custom Transparency Services
+You can implement your own transparency service by creating a class that implements the `ITransparencyService` interface. This allows you to define custom behavior for creating and verifying transparent messages.
+
+### Error Handling
+#### Common Exceptions
+- `InvalidOperationException`: Thrown when attempting to create or verify a transparent message without the necessary metadata.
+- `ArgumentNullException`: Thrown when required parameters are null.
+
+##### Example:
+```csharp
+try
+{
+ // Example usage
+}
+catch (ArgumentNullException ex)
+{
+ Console.WriteLine($"Argument null: {ex.ParamName}");
+}
+catch (InvalidOperationException ex)
+{
+ Console.WriteLine($"Invalid operation: {ex.Message}");
+}
+
+```
+## Configuration
+The project is configured to enforce strict code quality and static analysis rules. Ensure your development environment supports the following:
+- Nullable reference types (`enable`).
+- Latest C# language features (`latest`).
+
+## Strong Name Signing
+The assembly is strong-name signed using the key file located at `..\StrongNameKeys\35MSSharedLib1024.snk`. Ensure your build environment has access to this key file.
+
+## Packaging
+The project is packaged with the following metadata:
+- License: `LICENSE`
+- Readme: `readme.md`
+- Release Notes: `ChangeLog.md`
+
+## Additional Resources
+- [COSE Specification](https://datatracker.ietf.org/doc/html/rfc8152)
+- [CBOR Specification](https://datatracker.ietf.org/doc/html/rfc7049)
+
+## Support
+For issues or feature requests, please contact the maintainers or open an issue in the repository.
+
+## Conclusion
+The `CoseSign1.Transparency`` library provides a robust solution for creating and verifying transparent COSE Sign1 messages. By embedding transparency metadata, you can ensure traceability and auditability in your software supply chain. For advanced scenarios, consider implementing custom transparency services or managing receipts programmatically.
+
+
+For more information, refer to the ./docs/CoseSignTool.md.