Skip to content

Conversation

@JeromySt
Copy link
Member

@JeromySt JeromySt commented Dec 6, 2025

This pull request updates the codebase and test suite to replace all references to "CTS" (Code Transparency Service) with "MST" (Microsoft's Signing Transparency), reflecting a naming change across related projects, tests, and workflow configurations. The changes ensure consistency in naming and project references throughout the repository.

Project and workflow updates:

  • Renamed all CTS-related test projects and code references to MST equivalents in the .github/workflows/dotnet.yml workflow configuration, updating both test execution and build steps. [1] [2]
  • Renamed the test project file from CoseSign1.Transparent.CTS.Tests.csproj to CoseSign1.Transparent.MST.Tests.csproj and updated its project references to use MST.

Documentation updates:

  • Updated all documentation to reflect the rename and added links to the official blog post

- Renamed projects: CoseSign1.Transparent.CTS -> MST, CoseSignTool.CTS.Plugin -> MST.Plugin
- Updated all namespaces, classes, and methods from CTS/AzureCts to MST
- Renamed commands: cts_register -> mst_register, cts_verify -> mst_verify
- Updated all documentation to reflect Microsoft's Signing Transparency branding
- Added official blog post link to MST.md
- All 935 unit tests passing
@JeromySt JeromySt linked an issue Dec 6, 2025 that may be closed by this pull request
@JeromySt JeromySt closed this Dec 6, 2025
@JeromySt JeromySt reopened this Dec 6, 2025
Comment on lines +153 to +154
(CoseSign1Message? message, byte[] signatureBytes, PluginExitCode result) =
await TestMstCommand.TestReadAndDecodeCoseMessage(tempFile, CancellationToken.None, null);

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning test

This assignment to
message
is useless, since its value is never read.
This assignment to
signatureBytes
is useless, since its value is never read.

Copilot Autofix

AI about 5 hours ago

To fix the problem, remove the tuple variables (message and signatureBytes) whose values are never read or used after assignment. In C#, when deconstructing a tuple and you do not need certain values, you can use the discard operator _ for those tuple elements. Specifically, change the variable names message and signatureBytes to _ in the tuple deconstruction on line 153 of CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs. This makes it clear that these values are intentionally not used and silences the warning. No additional methods, imports, or definitions are required for this fix.

Suggested changeset 1
CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs b/CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs
--- a/CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs
+++ b/CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs
@@ -150,7 +150,7 @@
         try
         {
             // Act & Assert - should not throw
-            (CoseSign1Message? message, byte[] signatureBytes, PluginExitCode result) = 
+            (_, _, PluginExitCode result) = 
                 await TestMstCommand.TestReadAndDecodeCoseMessage(tempFile, CancellationToken.None, null);
             Assert.AreEqual(PluginExitCode.InvalidArgumentValue, result);
         }
EOF
@@ -150,7 +150,7 @@
try
{
// Act & Assert - should not throw
(CoseSign1Message? message, byte[] signatureBytes, PluginExitCode result) =
(_, _, PluginExitCode result) =
await TestMstCommand.TestReadAndDecodeCoseMessage(tempFile, CancellationToken.None, null);
Assert.AreEqual(PluginExitCode.InvalidArgumentValue, result);
}
Copilot is powered by AI and may make mistakes. Always verify output.
// Arrange
MockLogger logger = new MockLogger();
IConfiguration configuration = CreateConfiguration(new Dictionary<string, string?>());
CancellationTokenSource cts = new CancellationTokenSource();

Check warning

Code scanning / CodeQL

Missing Dispose call on local IDisposable Warning test

Disposable 'CancellationTokenSource' is created but not disposed.

Copilot Autofix

AI about 5 hours ago

The best way to fix this issue is to ensure that the CancellationTokenSource is disposed after use. The simplest and safest way is to wrap its usage in a using statement within the unit test. Specifically, in the HandleCommonException_WithCancellation_LogsError test method in CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs, the code block that creates and uses cts should use a using statement so that Dispose() is called automatically, even if an exception is thrown during the test's execution. No additional methods, imports, or definitions are required.


Suggested changeset 1
CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs b/CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs
--- a/CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs
+++ b/CoseSignTool.MST.Plugin.Tests/MstCommandBaseLoggingTests.cs
@@ -246,16 +246,18 @@
         // Arrange
         MockLogger logger = new MockLogger();
         IConfiguration configuration = CreateConfiguration(new Dictionary<string, string?>());
-        CancellationTokenSource cts = new CancellationTokenSource();
-        cts.Cancel();
-        OperationCanceledException exception = new OperationCanceledException(cts.Token);
+        using (CancellationTokenSource cts = new CancellationTokenSource())
+        {
+            cts.Cancel();
+            OperationCanceledException exception = new OperationCanceledException(cts.Token);
 
-        // Act
-        PluginExitCode result = TestMstCommand.TestHandleCommonException(exception, configuration, cts.Token, logger);
+            // Act
+            PluginExitCode result = TestMstCommand.TestHandleCommonException(exception, configuration, cts.Token, logger);
 
-        // Assert
-        Assert.AreEqual(PluginExitCode.UnknownError, result);
-        Assert.IsTrue(logger.LoggedMessages.Any(m => m.Message.Contains("Operation was cancelled")));
+            // Assert
+            Assert.AreEqual(PluginExitCode.UnknownError, result);
+            Assert.IsTrue(logger.LoggedMessages.Any(m => m.Message.Contains("Operation was cancelled")));
+        }
     }
 
     [TestMethod]
EOF
@@ -246,16 +246,18 @@
// Arrange
MockLogger logger = new MockLogger();
IConfiguration configuration = CreateConfiguration(new Dictionary<string, string?>());
CancellationTokenSource cts = new CancellationTokenSource();
cts.Cancel();
OperationCanceledException exception = new OperationCanceledException(cts.Token);
using (CancellationTokenSource cts = new CancellationTokenSource())
{
cts.Cancel();
OperationCanceledException exception = new OperationCanceledException(cts.Token);

// Act
PluginExitCode result = TestMstCommand.TestHandleCommonException(exception, configuration, cts.Token, logger);
// Act
PluginExitCode result = TestMstCommand.TestHandleCommonException(exception, configuration, cts.Token, logger);

// Assert
Assert.AreEqual(PluginExitCode.UnknownError, result);
Assert.IsTrue(logger.LoggedMessages.Any(m => m.Message.Contains("Operation was cancelled")));
// Assert
Assert.AreEqual(PluginExitCode.UnknownError, result);
Assert.IsTrue(logger.LoggedMessages.Any(m => m.Message.Contains("Operation was cancelled")));
}
}

[TestMethod]
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +56 to +63
foreach (KeyValuePair<string, string> kvp in filePaths)
{
if (!File.Exists(kvp.Value))
{
logger?.LogError($"{kvp.Key} file not found: {kvp.Value}");
return PluginExitCode.UserSpecifiedFileNotFound;
}
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.

Copilot Autofix

AI about 5 hours ago

To fix this issue, we should replace the foreach loop and internal if statement with a LINQ query that finds the first missing file path (where File.Exists(kvp.Value) returns false). If such a file is found, log the error and return the respective exit code, else return success. This can be accomplished by using .FirstOrDefault(...) on the dictionary's entries filtered via .Where(kvp => !File.Exists(kvp.Value)). This removes a level of indentation and makes it clear that only entries for nonexistent files are relevant. No new imports are needed, as LINQ is available by default. Edit only the lines within ValidateFilePaths in CoseSignTool.MST.Plugin/MstCommandBase.cs.


Suggested changeset 1
CoseSignTool.MST.Plugin/MstCommandBase.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/CoseSignTool.MST.Plugin/MstCommandBase.cs b/CoseSignTool.MST.Plugin/MstCommandBase.cs
--- a/CoseSignTool.MST.Plugin/MstCommandBase.cs
+++ b/CoseSignTool.MST.Plugin/MstCommandBase.cs
@@ -53,15 +53,15 @@
     /// <returns>PluginExitCode indicating validation result.</returns>
     protected static PluginExitCode ValidateFilePaths(Dictionary<string, string> filePaths, IPluginLogger? logger = null)
     {
-        foreach (KeyValuePair<string, string> kvp in filePaths)
+        var missingFile = filePaths.FirstOrDefault(kvp => !File.Exists(kvp.Value));
+        if (!missingFile.Equals(default(KeyValuePair<string, string>)))
         {
-            if (!File.Exists(kvp.Value))
-            {
-                logger?.LogError($"{kvp.Key} file not found: {kvp.Value}");
-                return PluginExitCode.UserSpecifiedFileNotFound;
-            }
+            logger?.LogError($"{missingFile.Key} file not found: {missingFile.Value}");
+            return PluginExitCode.UserSpecifiedFileNotFound;
         }
 
+
+
         return PluginExitCode.Success;
     }
 
EOF
@@ -53,15 +53,15 @@
/// <returns>PluginExitCode indicating validation result.</returns>
protected static PluginExitCode ValidateFilePaths(Dictionary<string, string> filePaths, IPluginLogger? logger = null)
{
foreach (KeyValuePair<string, string> kvp in filePaths)
var missingFile = filePaths.FirstOrDefault(kvp => !File.Exists(kvp.Value));
if (!missingFile.Equals(default(KeyValuePair<string, string>)))
{
if (!File.Exists(kvp.Value))
{
logger?.LogError($"{kvp.Key} file not found: {kvp.Value}");
return PluginExitCode.UserSpecifiedFileNotFound;
}
logger?.LogError($"{missingFile.Key} file not found: {missingFile.Value}");
return PluginExitCode.UserSpecifiedFileNotFound;
}



return PluginExitCode.Success;
}

Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +84 to +89
catch (Exception ex)
{
logger?.LogError($"Failed to decode COSE Sign1 message from {signaturePath}: {ex.Message}");
logger?.LogException(ex);
return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.

Copilot Autofix

AI about 5 hours ago

To fix the problem, we should replace the overly broad catch (Exception ex) with a set of more specific catch blocks tailored for the kinds of errors likely to occur in this context:

  • For await File.ReadAllBytesAsync(signaturePath, ...), possible exceptions include IOException, UnauthorizedAccessException, ArgumentException, and PathTooLongException.
  • For CoseMessage.DecodeSign1(signatureBytes), common exceptions might include CryptographicException if the data is invalid/malformed.

The best approach is to use multiple specific catch blocks for:

  • IOException
  • UnauthorizedAccessException
  • ArgumentException
  • PathTooLongException
  • CryptographicException (from System.Security.Cryptography)
  • An optional final catch for any other Exception types if you require catching everything else (but ideally not catch critical exceptions that inherit from SystemException directly).

You should also add using System.Security.Cryptography; if it is not already present for CryptographicException.

Only the exception blocks in the ReadAndDecodeCoseMessage method, within CoseSignTool.MST.Plugin/MstCommandBase.cs, need changing. If logging the exception remains important, keep the log lines in each specific catch.


Suggested changeset 1
CoseSignTool.MST.Plugin/MstCommandBase.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/CoseSignTool.MST.Plugin/MstCommandBase.cs b/CoseSignTool.MST.Plugin/MstCommandBase.cs
--- a/CoseSignTool.MST.Plugin/MstCommandBase.cs
+++ b/CoseSignTool.MST.Plugin/MstCommandBase.cs
@@ -5,7 +5,7 @@
 
 using System.Text.Json;
 using System.Security.Cryptography.Cose;
-
+using System.Security.Cryptography;
 /// <summary>
 /// Base class for Microsoft's Signing Transparency (MST) commands that provides common functionality
 /// for parameter validation, file operations, error handling, and result output.
@@ -81,8 +81,32 @@
             CoseSign1Message message = CoseMessage.DecodeSign1(signatureBytes);
             return (message, signatureBytes, PluginExitCode.Success);
         }
-        catch (Exception ex)
+        catch (IOException ex)
         {
+            logger?.LogError($"IO error reading signature file {signaturePath}: {ex.Message}");
+            logger?.LogException(ex);
+            return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
+        }
+        catch (UnauthorizedAccessException ex)
+        {
+            logger?.LogError($"Access denied to signature file {signaturePath}: {ex.Message}");
+            logger?.LogException(ex);
+            return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
+        }
+        catch (ArgumentException ex)
+        {
+            logger?.LogError($"Invalid path for signature file {signaturePath}: {ex.Message}");
+            logger?.LogException(ex);
+            return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
+        }
+        catch (PathTooLongException ex)
+        {
+            logger?.LogError($"File path too long: {signaturePath}: {ex.Message}");
+            logger?.LogException(ex);
+            return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
+        }
+        catch (System.Security.Cryptography.CryptographicException ex)
+        {
             logger?.LogError($"Failed to decode COSE Sign1 message from {signaturePath}: {ex.Message}");
             logger?.LogException(ex);
             return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
EOF
@@ -5,7 +5,7 @@

using System.Text.Json;
using System.Security.Cryptography.Cose;

using System.Security.Cryptography;
/// <summary>
/// Base class for Microsoft's Signing Transparency (MST) commands that provides common functionality
/// for parameter validation, file operations, error handling, and result output.
@@ -81,8 +81,32 @@
CoseSign1Message message = CoseMessage.DecodeSign1(signatureBytes);
return (message, signatureBytes, PluginExitCode.Success);
}
catch (Exception ex)
catch (IOException ex)
{
logger?.LogError($"IO error reading signature file {signaturePath}: {ex.Message}");
logger?.LogException(ex);
return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
}
catch (UnauthorizedAccessException ex)
{
logger?.LogError($"Access denied to signature file {signaturePath}: {ex.Message}");
logger?.LogException(ex);
return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
}
catch (ArgumentException ex)
{
logger?.LogError($"Invalid path for signature file {signaturePath}: {ex.Message}");
logger?.LogException(ex);
return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
}
catch (PathTooLongException ex)
{
logger?.LogError($"File path too long: {signaturePath}: {ex.Message}");
logger?.LogException(ex);
return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
}
catch (System.Security.Cryptography.CryptographicException ex)
{
logger?.LogError($"Failed to decode COSE Sign1 message from {signaturePath}: {ex.Message}");
logger?.LogException(ex);
return (null, Array.Empty<byte>(), PluginExitCode.InvalidArgumentValue);
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +263 to +266
catch (Exception ex)
{
return HandleCommonException(ex, configuration, cancellationToken, Logger);
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.

Copilot Autofix

AI about 5 hours ago

To address the generic catch clause, modify the catch (Exception ex) in ExecuteAsync to catch only known, operational exception types that can realistically occur during command execution and be meaningfully handled (such as IOException, TimeoutException, JsonException, and COSE-related exceptions). Fatal exceptions (such as OutOfMemoryException, StackOverflowException, ThreadAbortException) should not be caught, as per established guidelines. These changes should only be made in ExecuteAsync method in CoseSignTool.MST.Plugin/MstCommandBase.cs as shown.

To implement this, replace the generic catch block at lines 263-266 with multiple specific catch blocks for expected exceptions, each calling HandleCommonException as before. If the handler already expects these types, you do not need to change its logic. If additional types (such as OperationCanceledException) are not handled, you may wish to add a final catch for these or allow them to propagate.

No changes are needed to imports, unless you wish to reference additional exception types (which are all standard here).


Suggested changeset 1
CoseSignTool.MST.Plugin/MstCommandBase.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/CoseSignTool.MST.Plugin/MstCommandBase.cs b/CoseSignTool.MST.Plugin/MstCommandBase.cs
--- a/CoseSignTool.MST.Plugin/MstCommandBase.cs
+++ b/CoseSignTool.MST.Plugin/MstCommandBase.cs
@@ -260,10 +260,26 @@
 
             return operationResult.exitCode;
         }
-        catch (Exception ex)
+        catch (IOException ex)
         {
             return HandleCommonException(ex, configuration, cancellationToken, Logger);
         }
+        catch (TimeoutException ex)
+        {
+            return HandleCommonException(ex, configuration, cancellationToken, Logger);
+        }
+        catch (JsonException ex)
+        {
+            return HandleCommonException(ex, configuration, cancellationToken, Logger);
+        }
+        catch (CryptographicException ex)
+        {
+            return HandleCommonException(ex, configuration, cancellationToken, Logger);
+        }
+        catch (OperationCanceledException ex) // If cancellation is to be handled gracefully
+        {
+            return HandleCommonException(ex, configuration, cancellationToken, Logger);
+        }
     }
 
     /// <summary>
EOF
@@ -260,10 +260,26 @@

return operationResult.exitCode;
}
catch (Exception ex)
catch (IOException ex)
{
return HandleCommonException(ex, configuration, cancellationToken, Logger);
}
catch (TimeoutException ex)
{
return HandleCommonException(ex, configuration, cancellationToken, Logger);
}
catch (JsonException ex)
{
return HandleCommonException(ex, configuration, cancellationToken, Logger);
}
catch (CryptographicException ex)
{
return HandleCommonException(ex, configuration, cancellationToken, Logger);
}
catch (OperationCanceledException ex) // If cancellation is to be handled gracefully
{
return HandleCommonException(ex, configuration, cancellationToken, Logger);
}
}

/// <summary>
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update AzureCTS naming to Microsoft's Signing Transparency

4 participants